/** * Bounded serial work queue (default concurrency 1) that the interaction * processor uses to serialize connection/memory creation so concurrent inbound * casts don't race. `submit` resolves/rejects with the wrapped work; * `waitUntilFinished` awaits drain. */ export class AsyncQueue { private queue: (() => Promise)[] = []; private running = 0; private emptyListeners: (() => void)[] = []; private maxConcurrent: number; constructor(maxConcurrent: number = 1) { this.maxConcurrent = maxConcurrent; } async submit(work: () => Promise): Promise { return new Promise((resolve, reject) => { this.queue.push(async () => { try { resolve(await work()); } catch (err) { reject(err); } }); void this.doNextWork(); }); } private async doNextWork(): Promise { if (this.running >= this.maxConcurrent) { return; } const work = this.queue.shift(); if (!work) { this.checkIfEmptyAndNotify(); return; } this.running++; try { await work(); } catch { // error-policy:J5 the queued closure built by `submit` already routes any // rejection to the caller's promise via `reject(err)` and never re-throws, // so this catch cannot fire in practice; it only guards the drain loop from // a defensive standpoint. The real failure surfaces at the `submit()` caller. } finally { this.running--; void this.doNextWork(); } } async size(): Promise { return this.queue.length; } async waitUntilFinished(): Promise { return new Promise((resolve) => { if (this.queue.length === 0 && this.running === 0) { resolve(); } else { this.emptyListeners.push(resolve); } }); } private checkIfEmptyAndNotify(): void { if (this.queue.length === 0 && this.running === 0) { while (this.emptyListeners.length) { const listener = this.emptyListeners.shift(); if (listener) listener(); } } } }