Project code: github.com/Haixiang612…
Preview link: yanhaixiang.com/my-promise-…
Reference the wheels: www.npmjs.com/package/pro…
Polling is a very common operation in the front end. However, for many people, setInterval is the first reaction to implement it. SetInterval is unstable as polling. Let’s write a promise-Poller wheel.
Starting from scratch
Starting with the setInterval method above, the lowest polling is as follows:
const promisePoller = (taskFn: Function, interval: number) = > {
setInterval(() = > {
taskFn();
}, interval)
}
Copy the code
The first parameter is polling task, and the second parameter is time interval, so easy.
As mentioned earlier, setInterval is unstable. See why setTimeout() is more stable than setInterval(). Using setTimeout iteration calls to do polling will be more stable, so easy.
interface Options {
taskFn: Function
interval: number
}
const promisePoller = (options: Options) = > {
const {taskFn, interval} = options
const poll = () = > {
setTimeout(() = > {
taskFn()
poll()
}, interval)
}
poll()
}
Copy the code
The input parameter is also sealed as an Options type to make it easier to extend.
We’re still not happy with this code. We can’t stand another callback in setTimeout. It’s ugly. Therefore, setTimeout can be wrapped as a delay function and poll can be called after the delay is complete.
export const delay = (interval: number) = > new Promise(resolve= > {
setTimeout(resolve, interval)
})
const promisePoller = (options: Options) = > {
const {taskFn, interval} = options
const poll = () = > {
taskFn()
delay(interval).then(poll) // Replace setTimeout callback with delay
}
poll()
}
Copy the code
Is it much cleaner?
promisify
If the wheel is named with “Promise”, the promisePoller function must return a promise. So this step is going to take this function promisify.
First return a Promise.
const promisePoller = (options: Options) = > {
const {taskFn, interval, masterTimeout} = options
return new Promise((resolve, reject) = > { // Return a Promise
const poll = () = > {
const result = taskFn()
delay(interval).then(poll)
}
poll()
})
}
Copy the code
The question is: When is it a reject? When should resolve? Reject the whole poll, reject the whole poll, resolve the whole poll.
Look at the reject timing: the whole polling failure is generally a timeout is cool bai, so here to add a masterTimeout Options, said overall polling timeout, then add a setTimeout timer for the polling process.
interface Options {
taskFn: Function
interval: numbermasterTimeout? :number // The timeout length of the entire polling process
}
const promisePoller = (options: Options) = > {
const {taskFn, interval, masterTimeout} = options
let timeoutId
return new Promise((resolve, reject) = > {
if (masterTimeout) {
timeoutId = setTimeout(() = > {
reject('Master timeout') // The whole polling timed out
}, masterTimeout)
}
const poll = () = > {
taskFn()
delay(interval).then(poll)
}
poll()
})
}
Copy the code
Resolve: If the last polling task is executed, the whole polling task is successful. Well, we don’t know, we only know if the caller tells us, so add a shouldContinue callback to let the caller tell us whether the polling shouldContinue or not. If not, it will be the last time.
interface Options {
taskFn: Function
interval: number
shouldContinue: (err: string | null, result: any) = > boolean // Whether to continue after the second pollingmasterTimeout? :number
}
const promisePoller = (options: Options) = > {
const {taskFn, interval, masterTimeout, shouldContinue} = options
let timeoutId: null | number
return new Promise((resolve, reject) = > {
if (masterTimeout) {
timeoutId = window.setTimeout(() = > {
reject('Master timeout') // The whole polling process timed out
}, masterTimeout)
}
const poll = () = > {
const result = taskFn()
if (shouldContinue(null, result)) {
delay(interval).then(poll) // Continue polling
} else {
if(timeoutId ! = =null) { // No polling is required, timeoutId is cleared
clearTimeout(timeoutId)
}
resolve(result) // The last polling task completes and returns the result of the last taskFn
}
}
poll()
})
}
Copy the code
At this point, a Promisify post-Poller function is almost complete. Is there anything else to optimize? There are!
Poll the timeout of the task
You can also poll a single task with a masterTimeout, so add a taskTimeout field in Options.
No, wait! TaskFn should also support asynchronous functions. TaskFn should also support asynchronous functions. So, when taskFn is called, the result is promisify and then the promise is tested for timeout.
interface Options {
taskFn: Function
interval: number
shouldContinue: (err: string | null, result: any) = > booleanmasterTimeout? :numbertaskTimeout? :number // Poll the task timeout
}
// Determine if the promise has timed out
const timeout = (promise: Promise<any>, interval: number) = > {
return new Promise((resolve, reject) = > {
const timeoutId = setTimeout(() = > reject('Task timeout'), interval)
promise.then(result= > {
clearTimeout(timeoutId)
resolve(result)
})
})
}
const promisePoller = (options: Options) = > {
const {taskFn, interval, masterTimeout, taskTimeout, shouldContinue} = options
let timeoutId: null | number
return new Promise((resolve, reject) = > {
if (masterTimeout) {
timeoutId = window.setTimeout(() = > {
reject('Master timeout')
}, masterTimeout)
}
const poll = () = > {
let taskPromise = Promise.resolve(taskFn()) // set the result promisify
if (taskTimeout) {
taskPromise = timeout(taskPromise, taskTimeout) // Check whether the polling task has timed out
}
taskPromise
.then(result= > {
if (shouldContinue(null, result)) {
delay(interval).then(poll)
} else {
if(timeoutId ! = =null) {
clearTimeout(timeoutId)
}
resolve(result)
}
})
.catch(error= > {
})
}
poll()
})
}
Copy the code
There are three steps:
- will
taskFn
The results of the promisify - add
timeout
Function is used to determinetaskFn
Whether or not to timeout (generally not for synchronous functions, because the result is returned immediately) - judge
taskFn
If you timeout, reject directly, it goes totaskPromise
The catch of
So what do you do after timeout reject? Of course, this is to tell the polling of the main process: Hey, this task has timed out, should I try again? So, here’s another retry feature.
retry
First, add a retries field to Options to indicate the number of retries.
interface Options {
taskFn: Function
interval: number
shouldContinue: (err: string | null, result? :any) = > booleanmasterTimeout? :numbertaskTimeout? :numberretries? :number // Number of retries after the polling task failed
}
Copy the code
Then, in the catch, determine if retries is 0 (I really need to try again) and shouldContinue is true (I really need to try again). Retry only if both are true.
const promisePoller = (options: Options) = >{...let rejections: Array<Error | string> = []
let retriesRemain = retries
return new Promise((resolve, reject) = >{...const poll = () = >{... taskPromise .then(result= >{... }) .catch(error= > {
rejections.push(error) // Add to the Rejections error list
if (--retriesRemain === 0| |! shouldContinue(error)) {// Determine whether to retry
reject(rejections) // If you do not retry, the system fails
} else {
delay(interval).then(poll); / / try again
}
})
}
poll()
})
}
Copy the code
It also added Rejections variables to hold multiple error messages. This is done because it is possible that 2 out of 10 tasks will fail, and both will eventually be returned, so you need an array of error messages.
Here’s another optimization point: We often see other pages fail to fetch data and show 1/3 fetch… Two-thirds of obtain… 3/3 obtain… Until really get failure, equivalent to a progress bar, so the user is also better. So, Options can provide a callback that is called every time retriesRemain is reduced.
interface Options {
taskFn: Function
interval: number
shouldContinue: (err: string | null, result? :any) = > booleanprogressCallback? :(retriesRemain: number, error: Error) = > unknown // The remaining number of callbacksmasterTimeout? :numbertaskTimeout? :numberretries? :number // Number of retries after the polling task failed
}
const promisePoller = (options: Options) = >{...let rejections: Array<Error | string> = []
let retriesRemain = retries
return new Promise((resolve, reject) = >{...const poll = () = >{... taskPromise .then(result= >{... }) .catch(error= > {
rejections.push(error) // Add to the Rejections error list
if (progressCallback) {
progressCallback(retriesRemain, error) // The callback retrieves retriesRemain
}
if (--retriesRemain === 0| |! shouldContinue(error)) {// Determine whether to retry
reject(rejections) // If you do not retry, the system fails
} else {
delay(interval).then(poll); / / try again
}
})
}
poll()
})
}
Copy the code
Actively stop polling
ShouldContinue is already an effective way to control whether or not a process should abort, but it’s a bit passive to wait until the next poll has started. PromisePoller is more flexible if taskFn is actively stopped while it is executing.
TaskFn can be either synchronous or asynchronous. For synchronous functions, return false disables polling, reject(“CANCEL_TOKEN”) disables polling. Rewrite the function as follows:
const CANCEL_TOKEN = 'CANCEL_TOKEN'
const promisePoller = (options: Options) = > {
const {taskFn, masterTimeout, taskTimeout, progressCallback, shouldContinue, retries = 5} = mergedOptions
let polling = true
let timeoutId: null | number
let rejections: Array<Error | string> = []
let retriesRemain = retries
return new Promise((resolve, reject) = > {
if (masterTimeout) {
timeoutId = window.setTimeout(() = > {
reject('Master timeout')
polling = false
}, masterTimeout)
}
const poll = () = > {
let taskResult = taskFn()
if (taskResult === false) { // End the synchronization task
taskResult = Promise.reject(taskResult)
reject(rejections)
polling = false
}
let taskPromise = Promise.resolve(taskResult)
if (taskTimeout) {
taskPromise = timeout(taskPromise, taskTimeout)
}
taskPromise
.then(result= >{... }) .catch(error= > {
if (error === CANCEL_TOKEN) { // End the asynchronous task
reject(rejections)
polling = false
}
rejections.push(error)
if (progressCallback) {
progressCallback(retriesRemain, error)
}
if (--retriesRemain === 0| |! shouldContinue(error)) { reject(rejections) }else if (polling) { // When you retry, you need to check that polling is true
delay(interval).then(poll);
}
})
}
poll()
})
}
Copy the code
This code checks whether taskFn returns false and CANCEL_TOKEN in a catch. If taskFn asks to abort polling, set polling to false and reject the entire process.
Here is another detail: to improve security, polling should be re-conducted only after checking whether polling is true in the retry area.
Polling strategy
At present, we design linear polling, an interval is done. To improve scalability, we provide two additional polling strategies: Linear-backoff and exponential-backoff, which increase the interval linearly and exponentially, respectively, rather than uniformly.
Set some default parameters for the policy:
export const strategies = {
'fixed-interval': {
defaults: {
interval: 1000
},
getNextInterval: function(count: number, options: Options) {
returnoptions.interval; }},'linear-backoff': {
defaults: {
start: 1000.increment: 1000
},
getNextInterval: function(count: number, options: Options) {
returnoptions.start + options.increment * count; }},'exponential-backoff': {
defaults: {
min: 1000.max: 30000
},
getNextInterval: function(count: number, options: Options) {
return Math.min(options.max, Math.round(Math.random() * (Math.pow(2, count) * 1000- options.min) + options.min)); }}};Copy the code
Each policy has its own parameters and the getNextInterval method, which gets the next polling interval in real time during polling. Because of the start parameter, the Options parameter should also be changed.
type StrategyName = 'fixed-interval' | 'linear-backoff' | 'exponential-backoff'
interface Options {
taskFn: Function
shouldContinue: (err: Error | null, result? :any) = > boolean // Whether to continue after the second pollingprogressCallback? :(retriesRemain: number, error: Error) = > unknown // The remaining number of callbacksstrategy? : StrategyName// Polling policymasterTimeout? :numbertaskTimeout? :numberretries? :number
/ / fixed - interval strategyinterval? :number
/ / linear - backoff strategystart? :numberincrement? :number
/ / an exponential backoff strategymin? :numbermax? :number
}
Copy the code
In the poll function, this is easy. You just need to get the nextInterval before delay, and then delay(nextInterval).
const promisePoller = (options: Options) = > {
const strategy = strategies[options.strategy] || strategies['fixed-interval'] // Obtains the current polling policy. By default, fixed-interval is used
constmergedOptions = {... strategy.defaults, ... options}// Merge the initial parameters of the polling policy
const {taskFn, masterTimeout, taskTimeout, progressCallback, shouldContinue, retries = 5} = mergedOptions
let polling = true
let timeoutId: null | number
let rejections: Array<Error | string> = []
let retriesRemain = retries
return new Promise((resolve, reject) = > {
if (masterTimeout) {
timeoutId = window.setTimeout(() = > {
reject(new Error('Master timeout'))
polling = false
}, masterTimeout)
}
const poll = () = > {
let taskResult = taskFn()
if (taskResult === false) {
taskResult = Promise.reject(taskResult)
reject(rejections)
polling = false
}
let taskPromise = Promise.resolve(taskResult)
if (taskTimeout) {
taskPromise = timeout(taskPromise, taskTimeout)
}
taskPromise
.then(result= > {
if (shouldContinue(null, result)) {
const nextInterval = strategy.getNextInterval(retriesRemain, mergedOptions) // Gets the interval for the next poll
delay(nextInterval).then(poll)
} else {
if(timeoutId ! = =null) {
clearTimeout(timeoutId)
}
resolve(result)
}
})
.catch((error: Error) = > {
if (error.message === CANCEL_TOKEN) {
reject(rejections)
polling = false
}
rejections.push(error)
if (progressCallback) {
progressCallback(retriesRemain, error)
}
if (--retriesRemain === 0| |! shouldContinue(error)) { reject(rejections) }else if (polling) {
const nextInterval = strategy.getNextInterval(retriesRemain, options) // Gets the interval for the next poll
delay(nextInterval).then(poll);
}
})
}
poll()
})
}
Copy the code
conclusion
This promisePoller mainly completes:
- Basic polling operations
- Return to the promise
- Provides active and passive abort polling methods
- Provides the retry function of polling tasks and provides the callback of retry progress
- Provides a variety of polling strategies: fixed-interval, Linear-backoff, and exponent-backoff
The above is the NPM package promise-Poller source code implementation.