my-promise-poller

手把手实现 JS 的 Promise 轮训机制

MIT License

Stars
9

promise-poller

https://github.com/Haixiang6123/my-promise-poller

http://yanhaixiang.com/my-promise-poller/

https://www.npmjs.com/package/promise-poller

setInterval setInterval promise-poller

setInterval Low

const promisePoller = (taskFn: Function, interval: number) => {
  setInterval(() => {
    taskFn();
  }, interval)
}

so easy

setInterval setTimeout()setInterval() setTimeout so easy

interface Options {
  taskFn: Function
  interval: number
}

const promisePoller = (options: Options) => {
  const {taskFn, interval} = options

  const poll = () => {
    setTimeout(() => {
      taskFn()
      poll()
    }, interval)
  }

  poll()
}

Options

setTimeout setTimeout delay delay poll

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) //  delay  setTimeout 
  }

  poll()
}

promisify

"promise" promisePoller Promise promisify

Promise

const promisePoller = (options: Options) => {
  const {taskFn, interval, masterTimeout} = options

  return new Promise((resolve, reject) => { //  Promise
    const poll = () => {
      const result = taskFn()

      delay(interval).then(poll)
    }

    poll()
  })
}

reject resolve reject resolve

reject timeout masterTimeout Options setTimeout

interface Options {
  taskFn: Function
  interval: number
  masterTimeout?: number //  timeout 
}

const promisePoller = (options: Options) => {
  const {taskFn, interval, masterTimeout} = options

  let timeoutId

  return new Promise((resolve, reject) => {
    if (masterTimeout) {
      timeoutId = setTimeout(() => {
        reject('Master timeout') // 
      }, masterTimeout)
    }

    const poll = () => {
      taskFn()

      delay(interval).then(poll)
    }

    poll()
  })
}

resolve shouldContinue

interface Options {
  taskFn: Function
  interval: number
  shouldContinue: (err: string | null, result: any) => boolean // 
  masterTimeout?: 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') // 
      }, masterTimeout)
    }

    const poll = () => {
      const result = taskFn()

      if (shouldContinue(null, result)) { 
        delay(interval).then(poll)  // 
      } else {
        if (timeoutId !== null) {  //  timeoutId 
          clearTimeout(timeoutId)
        }
        
        resolve(result) //  taskFn 
      }
    }

    poll()
  })
}

Promisify poller

timeout

masterTimeout timeout Options taskTimeout

taskFn timeout taskFn taskFn promisify promise timeout

interface Options {
  taskFn: Function
  interval: number
  shouldContinue: (err: string | null, result: any) => boolean
  masterTimeout?: number
  taskTimeout?: number //  timeout
}

//  promise 
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()) //  promisify

      if (taskTimeout) {
        taskPromise = timeout(taskPromise, taskTimeout) // 
      }

      taskPromise
        .then(result => {
          if (shouldContinue(null, result)) {
            delay(interval).then(poll)
          } else {
            if (timeoutId !== null) {
              clearTimeout(timeoutId)
            }
            resolve(result)
          }
        })
        .catch(error => {

        })
    }

    poll()
  })
}
  1. taskFn promisify
  2. timeout taskFn timeout
  3. taskFn reject taskPromise catch

timeout reject

Options retries

interface Options {
  taskFn: Function 
  interval: number 
  shouldContinue: (err: string | null, result?: any) => boolean 
  masterTimeout?: number 
  taskTimeout?: number 
  retries?: number // 
}

catch retries 0 shouldContinue true 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) //  rejections 
          
          if (--retriesRemain === 0 || !shouldContinue(error)) { // 
            reject(rejections) // 
          } else {
            delay(interval).then(poll); // 
          }
        })
    }

    poll()
  })
}

rejections error 10 2 2

1/3 ... 2/3 ... 3/3 ... Options retriesRemain

interface Options {
  taskFn: Function 
  interval: number 
  shouldContinue: (err: string | null, result?: any) => boolean 
  progressCallback?: (retriesRemain: number, error: Error) => unknown // 
  masterTimeout?: number 
  taskTimeout?: number 
  retries?: number // 
}

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) //  rejections 

          if (progressCallback) {
             progressCallback(retriesRemain, error) //  retriesRemain
          }
          
          if (--retriesRemain === 0 || !shouldContinue(error)) { // 
            reject(rejections) // 
          } else {
            delay(interval).then(poll); // 
          }
        })
    }

    poll()
  })
}

shouldContinue taskFn promisePoller

taskFn return false reject("CANCEL_TOKEN")

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) { // 
        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) { // 
            reject(rejections)
            polling = false
          }

          rejections.push(error)
          
          if (progressCallback) {
             progressCallback(retriesRemain, error)
          }

          if (--retriesRemain === 0 || !shouldContinue(error)) {
            reject(rejections)
          } else if (polling) { //  polling  true
            delay(interval).then(poll);
          }
        })
    }

    poll()
  })
}

taskFn false catch error CANCEL_TOKEN taskFn polling false reject

polling true poll

interval 2 linear-backoff exponential-backoff interval

export const strategies = {
  'fixed-interval': {
    defaults: {
      interval: 1000
    },
    getNextInterval: function(count: number, options: Options) {
      return options.interval;
    }
  },

  'linear-backoff': {
    defaults: {
      start: 1000,
      increment: 1000
    },
    getNextInterval: function(count: number, options: Options) {
      return options.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));
    }
  }
};

getNextInterval Options

type StrategyName = 'fixed-interval' | 'linear-backoff' | 'exponential-backoff'

interface Options {
  taskFn: Function
  shouldContinue: (err: Error | null, result?: any) => boolean // 
  progressCallback?: (retriesRemain: number, error: Error) => unknown // 
  strategy?: StrategyName // 
  masterTimeout?: number
  taskTimeout?: number
  retries?: number
  // fixed-interval 
  interval?: number
  // linear-backoff 
  start?: number
  increment?: number
  // exponential-backoff 
  min?: number
  max?: number
}

poll delay nextInterval delay(nextInterval)

const promisePoller = (options: Options) => {
  const strategy = strategies[options.strategy] || strategies['fixed-interval'] //  fixed-interval

  const mergedOptions = {...strategy.defaults, ...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(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) // 
            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) // 
            delay(nextInterval).then(poll);
          }
        })
    }

    poll()
  })
}

promisePoller

  1. promise
  2. fixed-interval, linear-backoff, exponential-backoff

npm promise-poller