📄 src/downloader/queue.ts
D-OPEN SOVEREIGN
1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library — The World Library
4 *
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 *
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15// Bounded-concurrency queue — attaches waiters to in-flight tasks rather than duplicating them.
16
17export class ConcurrentProcessor<T = void> {
18 private readonly max: number;
19 private active = 0;
20 private queue: Array<{ key: string; run: () => void; reject: (err: unknown) => void }> = [];
21 // key → promise for every task currently in-flight or waiting to run.
22 private waiters: Map<string, Promise<T>> = new Map();
23
24 constructor(maxConcurrent: number) {
25 this.max = maxConcurrent;
26 }
27
28 add(key: string, task: () => Promise<T>): Promise<T> {
29 // Return the existing promise so duplicate callers share the result.
30 const existing = this.waiters.get(key);
31 if (existing) return existing;
32
33 const p = new Promise<T>((resolve, reject) => {
34 const run = () => {
35 this.active++;
36 // Promise.resolve().then(task) guarantees a synchronous throw inside
37 // task() becomes a rejection instead of leaking the concurrency slot
38 // and the waiters entry forever (task() would otherwise throw before
39 // .then(resolve, reject) ever got attached).
40 Promise.resolve()
41 .then(task)
42 .then(resolve, reject)
43 .finally(() => {
44 this.waiters.delete(key);
45 this.active--;
46 this._drain();
47 });
48 };
49
50 if (this.active < this.max) {
51 run();
52 } else {
53 this.queue.push({ key, run, reject });
54 }
55 });
56
57 this.waiters.set(key, p);
58 return p;
59 }
60
61 start(): void {
62 // No-op for now — tasks run immediately on add when concurrency allows.
63 }
64
65 // Drops queued-but-not-started tasks and rejects their callers (so they never
66 // hang waiting on a task that will now never run), then resolves once every
67 // already-active task has settled.
68 stop(): Promise<void> {
69 const pending = this.queue;
70 this.queue = [];
71 for (const { key, reject } of pending) {
72 this.waiters.delete(key);
73 reject(new Error('[queue] stopped before task started'));
74 }
75 const remaining = [...this.waiters.values()];
76 return Promise.allSettled(remaining).then(() => undefined);
77 }
78
79 private _drain(): void {
80 if (this.queue.length > 0 && this.active < this.max) {
81 const next = this.queue.shift()!;
82 next.run();
83 }
84 }
85}
86