UNPKG

@henrygd/queue

Version:

Tiny async queue with concurrency control. Like p-limit or fastq, but smaller and faster.

153 lines (139 loc) 4.56 kB
/** * This version of `@henrygd/queue` supports rate limiting. * * @module */ /** List node */ type Node<T> = { /** input promise wrapper */ p: () => T /** resolve returned promise */ res: (value: T) => void /** reject returned promise */ rej: (reason: any) => void /** next node pointer */ next?: Node<T> } /** Queue interface */ export interface Queue { /** Add an async function / promise wrapper to the queue */ add<T>(promiseFunction: () => PromiseLike<T>): Promise<T> /** Returns a promise that resolves when the queue is empty */ done(): Promise<void> /** Empties the queue (active promises are not cancelled) */ clear(): void /** Returns the number of promises currently running */ active(): number /** Returns the total number of promises in the queue */ size(): number /** Adds promises (or wrappers) to the queue and resolves like Promise.all */ all<T>(promiseFunctions: Array<PromiseLike<T> | (() => PromiseLike<T>)>): Promise<T[]> } // this just saves a few bytes let Promize = Promise /** * Creates a new queue with concurrency and optional rate limiting. * * @param {number} concurrency - The maximum number of concurrent operations. * @param {number} [rate] - The maximum number of operations that can start within the interval. * @param {number} [interval] - The time window in milliseconds for rate limiting. * @return {Queue} - The newly created queue. */ export let newQueue = (concurrency: number, rate?: number, interval?: number): Queue => { let active = 0 let size = 0 let head: Node<PromiseLike<any>> | undefined | null let tail: Node<PromiseLike<any>> | undefined | null let resolveDonePromise: (value: void | PromiseLike<void>) => void let donePromise: Promise<void> | void let queue: Queue let scheduled = false let startTimes: number[] = [] let afterRun = () => { active-- if (--size) { run() } else { donePromise = resolveDonePromise?.() } } let run = () => { // If already scheduled or no items in queue, skip if (scheduled || !head) return // Check concurrency limit if (active >= concurrency) return // Check rate limit if configured if (rate !== undefined && interval !== undefined) { let now = Date.now() // Remove timestamps outside the current interval window startTimes = startTimes.filter((t) => now - t < interval) // If we've hit the rate limit, schedule a retry if (startTimes.length >= rate) { scheduled = true let oldestStart = startTimes[0] let delay = interval - (now - oldestStart) setTimeout(() => { scheduled = false run() }, delay) return } // Track this task's start time startTimes.push(now) } // Execute the task active++ let curHead = head head = head.next curHead.p().then( (v) => (curHead.res(v), afterRun()), (e) => (curHead.rej(e), afterRun()) ) // Try to run more tasks if available if (head && active < concurrency) { run() } } return (queue = { add<T>(p: () => PromiseLike<T>) { let node = { p } as Node<PromiseLike<T>> let promise = new Promize((res, rej) => { node.res = res node.rej = rej }) if (head) { tail = tail!.next = node } else { tail = head = node } size++ run() return promise as Promise<T> }, done: () => { if (!size) { return Promize.resolve() } if (donePromise) { return donePromise } return (donePromise = new Promize((resolve) => (resolveDonePromise = resolve))) }, clear() { for (let node = head; node; node = node.next) { node.rej(new Error('Queue cleared')) } head = tail = null size = active startTimes = [] // If size is now 0 and there's a pending done() promise, resolve it if (!size && donePromise) { donePromise = resolveDonePromise?.() } }, active: () => active, size: () => size, all: <T>(fns: Array<PromiseLike<T> | (() => PromiseLike<T>)>): Promise<T[]> => Promize.all(fns.map(fn => queue.add(typeof fn === 'function' ? fn : () => fn))), }) }