import { type PromiseWithResolvers, promiseWithResolvers, } from "./promiseWithResolvers.js"; export type InnerQueue = { task: taskType; resolve: (arg: returnType) => void; reject: (error: Error) => void; }[]; export type Queue = { size: () => number; pending: () => Promise; add: (task: taskType) => Promise; clear: ( callback?: (e: InnerQueue[number]) => void, ) => void; isStarted: () => boolean; start: () => Promise; pause: () => void; onIdle: () => Promise; onEmpty: () => Promise; setParameters: ( parameters: Pick< CreateQueueParameters, "frequency" | "concurrency" >, ) => void; }; export type CreateQueueParameters = { worker: (task: taskType) => Promise; initialStart?: boolean; browser?: boolean; } & ( | { concurrency: number; frequency: number; } | { concurrency: number; frequency?: undefined } | { concurrency?: undefined; frequency: number } ); const validateParameters = ({ concurrency, frequency, }: Pick< CreateQueueParameters, "frequency" | "concurrency" >) => { if (concurrency === undefined && frequency === undefined) { throw new Error( "Invalid queue configuration, must specify either 'concurrency' or 'frequency'.", ); } if (concurrency !== undefined && concurrency <= 0) { throw new Error( `Invalid value for queue 'concurrency' option. Got ${concurrency}, expected a number greater than zero.`, ); } if (frequency !== undefined && frequency <= 0) { throw new Error( `Invalid value for queue 'frequency' option. Got ${frequency}, expected a number greater than zero.`, ); } }; export const createQueue = ({ worker, initialStart = false, browser = true, ..._parameters }: CreateQueueParameters): Queue< returnType, taskType > => { validateParameters(_parameters); const parameters: Pick< CreateQueueParameters, "frequency" | "concurrency" > = _parameters; let queue = new Array[number]>(); let pending = 0; let timestamp = 0; let requests = 0; let isStarted = initialStart; let timer: NodeJS.Timeout | undefined; let emptyPromiseWithResolvers: | (PromiseWithResolvers & { completed: boolean }) | undefined = undefined; let idlePromiseWithResolvers: | (PromiseWithResolvers & { completed: boolean }) | undefined = undefined; const next = () => { if (!isStarted) return; const _timestamp = Date.now(); if (Math.floor(_timestamp / 1_000) !== timestamp) { requests = 0; timestamp = Math.floor(_timestamp / 1_000); } if (timer) return; while ( (parameters.frequency !== undefined ? requests < parameters.frequency : true) && (parameters.concurrency !== undefined ? pending < parameters.concurrency : true) && queue.length > 0 ) { const { task, resolve, reject } = queue.shift()!; requests++; pending++; worker(task) .then(resolve) .catch(reject) .finally(() => { pending--; if ( idlePromiseWithResolvers !== undefined && queue.length === 0 && pending === 0 ) { idlePromiseWithResolvers.resolve(); idlePromiseWithResolvers.completed = true; } browser ? next() : process.nextTick(next); }); if (emptyPromiseWithResolvers !== undefined && queue.length === 0) { emptyPromiseWithResolvers.resolve(); emptyPromiseWithResolvers.completed = true; } } if ( parameters.frequency !== undefined && requests >= parameters.frequency ) { timer = setTimeout( () => { timer = undefined; next(); }, 1_000 - (_timestamp % 1_000), ); return; } }; return { size: () => queue.length, pending: () => { if (browser) { return new Promise((resolve) => setTimeout(() => resolve(pending)), ); } else { return new Promise((resolve) => setImmediate(() => resolve(pending)), ); } }, add: (task: taskType) => { const { promise, resolve, reject } = promiseWithResolvers(); queue.push({ task, resolve, reject }); next(); return promise; }, clear: (callback) => { if (callback) { for (const e of queue) { callback(e); } } queue = new Array[number]>(); clearTimeout(timer); timer = undefined; }, isStarted: () => isStarted, start: () => { if (browser) { return new Promise((resolve) => setTimeout(() => resolve(pending)), ).then(() => { isStarted = true; next(); }); } else { return new Promise((resolve) => process.nextTick(() => resolve(pending)), ).then(() => { isStarted = true; next(); }); } }, pause: () => { isStarted = false; }, onIdle: () => { if ( idlePromiseWithResolvers === undefined || idlePromiseWithResolvers.completed ) { if (queue.length === 0 && pending === 0) return Promise.resolve(); idlePromiseWithResolvers = { ...promiseWithResolvers(), completed: false, }; } return idlePromiseWithResolvers.promise; }, onEmpty: () => { if ( emptyPromiseWithResolvers === undefined || emptyPromiseWithResolvers.completed ) { if (queue.length === 0) return Promise.resolve(); emptyPromiseWithResolvers = { ...promiseWithResolvers(), completed: false, }; } return emptyPromiseWithResolvers.promise; }, setParameters: (_parameters) => { validateParameters(_parameters); if ("frequency" in _parameters) { parameters.frequency = _parameters.frequency; } if ("concurrency" in _parameters) { parameters.concurrency = _parameters.concurrency; } }, } as Queue; };