a-promise-queue
Version:
A native es6 promise queue with optional retry attempts.
142 lines (128 loc) • 4.17 kB
JavaScript
class PromiseQueue {
constructor (options, callback) {
const cb = (typeof options === 'function') ? options : callback
const opts = (options && typeof options === 'object') ? options : {}
this.flushing = false
this.Promise = opts.promise || Promise
this.concurrency = (!opts.concurrency || typeof opts.concurrency !== 'number') ? 1 : opts.concurrency
this.promises = []
this.queue = []
this.callback = cb
if (!cb) {
this._makePromise()
}
}
_makePromise () {
this._finalPromise = new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
}
then (onResolve, onReject) {
if (this.callback) throw new Error('Cannot use PromiseQueue as a Promise if callback has been is set')
return this._finalPromise.then(onResolve, onReject)
}
catch (onReject) {
if (this.callback) throw new Error('Cannot use PromiseQueue as a Promise if callback has been is set')
return this._finalPromise.catch(onReject)
}
add (fn, opts) {
if (typeof fn !== 'function') throw new Error('PromiseQueue.add() expects a function as an argument.')
return new this.Promise((resolve, reject) => {
const attempts = (opts && opts.attempts && opts.attempts > 0) ? opts.attempts : 1
if (this.promises.length < this.concurrency) {
const id = (this.promises.length) ? this.promises[this.promises.length - 1].id + 1 : 1
this.promises.push({
id,
promise: this._wrap(fn, id, resolve, reject, attempts)
})
} else {
// shift order based on priority
const next = {
fn,
attempts,
priority: (opts && opts.priority) ? opts.priority : 0,
resolve,
reject
}
if (!opts || !opts.priority) {
this.queue.push(next)
} else {
let found = false
for (let i = this.length - 1; i >= 0; i--) {
if (this.queue[i].priority && this.queue[i].priority >= opts.priority) {
this.queue.splice(i + 1, 0, next)
found = true
break
}
}
if (!found) {
this.queue.unshift(next)
}
}
}
})
}
flush () {
const currentPromises = this.promises.map(p => p.promise)
const concurrent = [...currentPromises, ...this.queue.map(queued => this._promised(queued.fn).then(queued.resolve, queued.reject))]
this.flushing = true
this.queue = []
const flushed = () => {
this.flushing = false
if (this.length > 0) {
// start processing new additions
const nextFn = this.queue.shift()
const id = (this.promises.length) ? this.promises[this.promises.length - 1].id + 1 : 1
const promise = this._wrap(nextFn.fn, id, nextFn.resolve, nextFn.reject, nextFn.attempts)
this.promises.push({ id, promise })
}
}
return this.Promise.all(concurrent)
.then(flushed, flushed)
}
get length () {
return this.queue.length
}
_promised (fn) {
try {
return this.Promise.resolve(fn())
} catch (e) {
return this.Promise.reject(e)
}
}
_next (id) {
if (this.flushing) return
if (this.length > 0) {
const nextFn = this.queue.shift()
return this._wrap(nextFn.fn, id, nextFn.resolve, nextFn.reject, nextFn.attempts)
}
const promiseId = this.promises.findIndex(p => {
return p.id === id
})
this.promises.splice(promiseId, 1)
if (this.promises.length === 0) this._done()
return true
}
_wrap (fn, id, resolve, reject, attempts) {
let retryCount = 0
const retry = (err) => {
if (retryCount >= attempts) {
throw err || new Error('Unknown Error')
}
retryCount += 1
return this._promised(fn).catch(retry)
}
return retry()
.then((r) => { resolve(r) }, (e) => { reject(e) })
.then(() => this._next(id))
}
_done (err) {
if (typeof this.callback === 'function') this.callback(err)
else if (this._finalPromise) {
this.resolve()
this._makePromise()
}
}
}
module.exports = PromiseQueue