UNPKG

@durable-streams/client

Version:

TypeScript client for the Durable Streams protocol

115 lines (102 loc) 2.83 kB
/** * Concurrency-limited async task queue. * * Replacement for fastq that works in all JS runtimes (browsers, Deno, Bun, * Workers, etc.) without depending on process.nextTick. Promise .then() * microtask scheduling provides equivalent "don't call back synchronously" * guarantees. * * Uses a head-pointer instead of Array.shift() for O(1) dequeue amortised. */ interface QueueEntry<T, R> { task: T resolve: (r: R) => void reject: (e: unknown) => void } export class AsyncQueue<T, R = void> { #worker: (task: T) => Promise<R> #concurrency: number #running = 0 #queue: Array<QueueEntry<T, R> | undefined> = [] #head = 0 #drainResolvers: Array<() => void> = [] constructor(worker: (task: T) => Promise<R>, concurrency: number) { if (!(concurrency >= 1)) { throw new Error(`concurrency must be >= 1`) } this.#worker = worker this.#concurrency = concurrency } push(task: T): Promise<R> { return new Promise<R>((resolve, reject) => { this.#queue.push({ task, resolve, reject }) this.#processNext() }) } /** * Number of tasks waiting to run. */ length(): number { return this.#queue.length - this.#head } /** * Number of tasks currently running. */ running(): number { return this.#running } /** * True when nothing is queued or running. */ idle(): boolean { return this.#head >= this.#queue.length && this.#running === 0 } /** * Resolves when every queued and in-flight task has completed. * Resolves immediately if the queue is already idle. */ async drained(): Promise<void> { if (this.idle()) return return new Promise<void>((resolve) => { this.#drainResolvers.push(resolve) }) } #processNext(): void { while ( this.#running < this.#concurrency && this.#head < this.#queue.length ) { const item = this.#queue[this.#head]! this.#queue[this.#head] = undefined this.#head++ // Compact when the consumed portion exceeds both 1024 slots and half // the array, to avoid unbounded memory growth on long-lived queues. if (this.#head > 1024 && this.#head > this.#queue.length >>> 1) { this.#queue = this.#queue.slice(this.#head) this.#head = 0 } this.#running++ this.#worker(item.task).then( (result) => { this.#running-- item.resolve(result) this.#checkDrained() this.#processNext() }, (error) => { this.#running-- item.reject(error) this.#checkDrained() this.#processNext() } ) } } #checkDrained(): void { if (this.idle()) { const resolvers = this.#drainResolvers this.#drainResolvers = [] for (const r of resolvers) r() } } }