UNPKG

kuyruk

Version:

Multifunctional Asynchronous Concurrent Queue

311 lines (280 loc) 7.76 kB
'use strict'; const { FixedQueue } = require('@tsevimli/collections'); const { debounce } = require('./utils.js'); class Kuyruk { constructor({ concurrency = 1, size = Infinity }) { this.concurrency = concurrency; this.size = size; this.count = 0; this.waiting = new FixedQueue(); this.destination = null; this.paused = false; this.factor = undefined; this.waitTimeout = Infinity; this.processTimeout = Infinity; this.debounceInterval = 0; this.debounceCount = Infinity; this.fifoMode = true; this.roundRobinMode = false; this.priorityMode = false; this.debounceMode = false; this.onProcess = null; this.onDone = null; this.onSuccess = null; this.onTimeout = null; this.onFailure = null; this.onDrain = null; } static channels({ concurrency, size }) { return new Kuyruk({ concurrency, size }); } #next(item) { let timer = null; let finished = false; this.count++; let execute = (err = null, res = item) => { if (!finished) { finished = true; if (timer) { clearTimeout(timer); timer = null; } this.count--; setTimeout(() => { if (this.waiting.length > 0) this.#takeNext(); }, 0); this.finish(err, res); } }; if ( this.debounceMode && this.debounceInterval > 0 && this.debounceCount-- > 0 ) { execute = debounce(execute, this.debounceInterval); } if (this.processTimeout !== Infinity) { timer = this.#prepareProcessTimeout(execute); } if (typeof item === 'function') { this.#runItem(item, execute); } else { if (!this.onProcess) { throw new Error('Process is not defined'); } const result = this.onProcess(item, execute); if (result && typeof result.then === 'function') { result.then((res) => void execute(null, res), execute); } } } #takeNext() { if (!this.paused) { const { waiting, waitTimeout } = this; const task = waiting.shift(); if (waitTimeout !== Infinity) { const delay = Date.now() - task.start; if (delay > waitTimeout) { const err = new Error('Waiting timed out'); this.finish(err, task.item); if (waiting.length > 0) { setTimeout(() => { if (!this.paused && waiting.length > 0) { this.#takeNext(); } }, 0); } return; } } const hasChannel = this.count < this.concurrency; if (hasChannel) this.#next(task.item); } } #prepareProcessTimeout(execute) { return setTimeout(() => { const err = new Error('Process timed out!'); if (this.onTimeout) this.onTimeout(err); execute(err); }, this.processTimeout); } #runItem(item, execute) { try { const result = item(); if (result && typeof result.then === 'function') { result.then((res) => void execute(null, res), execute); } else { execute(null, result); } } catch (err) { execute(err); } } #cloneQueue({ factor, item }) { const { concurrency, size } = this; const queue = Kuyruk.channels({ concurrency, size }) .process(this.onProcess) .setFactor(factor) .add(item); if (this.priorityMode) queue.priority(); if (!this.fifoMode) queue.lifo(); if (this.waitTimeout !== Infinity) queue.wait(this.waitTimeout); if (this.processTimeout !== Infinity) { queue.timeout(this.processTimeout, this.onTimeout); } if (this.debounceMode) { queue.debounce(this.debounceCount, this.debounceInterval); } queue.finish = this.finish.bind(this); return queue; } finish(err, res) { const { onFailure, onSuccess, onDone, onDrain } = this; const details = { factor: this.factor }; if (err) { if (onFailure) onFailure(err, res, details); else throw err; } else if (onSuccess) { onSuccess(res, details); } if (onDone) onDone(err, res, details); if (this.destination) this.destination.add(res); if (onDrain) { if (!this.roundRobinMode) { if (this.count === 0 && this.waiting.length === 0) onDrain(); } else { const queuesIsDrain = this.waiting.every( (queue) => queue.waiting.length === 0 && queue.count === 0, ); if (queuesIsDrain) onDrain(); } } } add(item, { factor = 0, priority = 0 } = {}) { if (this.size > this.waiting.length) { if (this.priorityMode && !this.roundRobinMode) { priority = factor; factor = 0; } if (this.roundRobinMode) { let queue = this.waiting.find((q) => q.factor === factor); if (queue) { queue.add(item); } else { queue = this.#cloneQueue({ factor, item }); this.waiting.push(queue); } } else if (!this.paused && this.concurrency > this.count) { this.#next(item); } else { const task = { item, priority, start: Date.now() }; if (this.fifoMode) this.waiting.push(task); else this.waiting.unshift(task); if (this.priorityMode) { const compare = this.fifoMode ? (a, b) => b - a : (a, b) => a - b; this.waiting.sort(({ priority: a }, { priority: b }) => compare(a, b), ); } } } } isEmpty() { return this.waiting.length === 0 && this.count === 0; } pipe(dest) { if (!Object.getPrototypeOf(dest) === Kuyruk.prototype) { const msg = 'Pipe method only work with "Kuyruk" instances'; throw new Error(msg); } this.destination = dest; return dest; } timeout(msec = 0, onTimeout = null) { if (msec <= 0) { const msg = 'Timeout interval must be greater than 0 milliseconds'; throw new Error(msg); } this.processTimeout = msec; this.onTimeout = onTimeout; return this; } wait(msec = 0) { if (this.debounceMode && msec > this.debounceInterval) { const msg = 'Cannot use wait longer than debounce interval'; throw new Error(msg); } this.waitTimeout = msec; return this; } debounce(count = Infinity, interval = 0) { if (this.waitTimeout > 0 && interval > this.waitTimeout) { const msg = 'Cannot use debounce interval greater than wait timeout'; throw new Error(msg); } this.debounceMode = true; this.debounceCount = count; this.debounceInterval = interval; return this; } resume() { this.paused = false; const emptyChannels = this.concurrency - this.count; let launchCount = Math.min(emptyChannels, this.waiting.length); while (launchCount-- > 0) { this.#takeNext(); } return this; } pause() { this.paused = true; return this; } clear() { this.count = 0; this.waiting = []; this.destination = null; return this; } process(listener) { this.onProcess = listener; return this; } done(listener) { this.onDone = listener; return this; } success(listener) { this.onSuccess = listener; return this; } failure(listener) { this.onFailure = listener; return this; } drain(listener) { this.onDrain = listener; return this; } fifo() { this.fifoMode = true; return this; } lifo() { this.fifoMode = false; return this; } priority(flag = true) { this.priorityMode = flag; return this; } setFactor(factor) { this.factor = factor; return this; } roundRobin(flag = true) { this.roundRobinMode = flag; return this; } } module.exports = { Kuyruk };