@praxisjs/concurrent 1.2.13 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/__tests__/decorators.test.js +60 -3
- package/dist/__tests__/decorators.test.js.map +1 -1
- package/dist/__tests__/pool.test.js +103 -70
- package/dist/__tests__/pool.test.js.map +1 -1
- package/dist/__tests__/queue.test.js +86 -71
- package/dist/__tests__/queue.test.js.map +1 -1
- package/dist/__tests__/task.test.js +90 -68
- package/dist/__tests__/task.test.js.map +1 -1
- package/dist/__tests__/utils.test.d.ts +2 -0
- package/dist/__tests__/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils.test.js +32 -0
- package/dist/__tests__/utils.test.js.map +1 -0
- package/dist/decorators.d.ts +13 -3
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +8 -3
- package/dist/decorators.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pool.d.ts +4 -1
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +29 -5
- package/dist/pool.js.map +1 -1
- package/dist/queue.d.ts +3 -1
- package/dist/queue.d.ts.map +1 -1
- package/dist/queue.js +35 -9
- package/dist/queue.js.map +1 -1
- package/dist/task.d.ts +3 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +16 -3
- package/dist/task.js.map +1 -1
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +26 -0
- package/dist/utils.js.map +1 -0
- package/package.json +4 -4
- package/src/__tests__/decorators.test.ts +78 -3
- package/src/__tests__/pool.test.ts +110 -102
- package/src/__tests__/queue.test.ts +103 -114
- package/src/__tests__/task.test.ts +96 -90
- package/src/__tests__/utils.test.ts +48 -0
- package/src/decorators.ts +23 -6
- package/src/index.ts +1 -0
- package/src/pool.ts +33 -4
- package/src/queue.ts +36 -8
- package/src/task.ts +18 -2
- package/src/utils.ts +24 -0
|
@@ -2,7 +2,20 @@ import { describe, it, expect, vi } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import { pool } from "../pool";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** Abortable delay — rejects with AbortError when signal fires. */
|
|
6
|
+
function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const timer = setTimeout(resolve, ms);
|
|
9
|
+
signal?.addEventListener("abort", () => {
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
reject(Object.assign(new Error("Aborted"), { name: "AbortError" }));
|
|
12
|
+
}, { once: true });
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── Without AbortSignal (existing behaviour) ───────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe("pool — without signal", () => {
|
|
6
19
|
it("executes a task and resolves with its result", async () => {
|
|
7
20
|
const p = pool(2, async (x: unknown) => (x as number) * 3);
|
|
8
21
|
expect(await p(4)).toBe(12);
|
|
@@ -11,25 +24,19 @@ describe("pool", () => {
|
|
|
11
24
|
it("respects concurrency limit", async () => {
|
|
12
25
|
let active = 0;
|
|
13
26
|
let maxActive = 0;
|
|
14
|
-
|
|
15
27
|
const p = pool(2, async () => {
|
|
16
28
|
active++;
|
|
17
29
|
maxActive = Math.max(maxActive, active);
|
|
18
30
|
await new Promise((r) => setTimeout(r, 10));
|
|
19
31
|
active--;
|
|
20
32
|
});
|
|
21
|
-
|
|
22
33
|
await Promise.all([p(), p(), p(), p()]);
|
|
23
34
|
expect(maxActive).toBeLessThanOrEqual(2);
|
|
24
35
|
});
|
|
25
36
|
|
|
26
37
|
it("active() reflects currently running tasks", async () => {
|
|
27
38
|
const resolvers: Array<() => void> = [];
|
|
28
|
-
const p = pool(
|
|
29
|
-
3,
|
|
30
|
-
() => new Promise<void>((r) => resolvers.push(r)),
|
|
31
|
-
);
|
|
32
|
-
|
|
39
|
+
const p = pool(3, () => new Promise<void>((r) => resolvers.push(r)));
|
|
33
40
|
const tasks = [p(), p()];
|
|
34
41
|
expect(p.active()).toBe(2);
|
|
35
42
|
resolvers.forEach((r) => { r(); });
|
|
@@ -39,38 +46,42 @@ describe("pool", () => {
|
|
|
39
46
|
|
|
40
47
|
it("loading is true when active > 0", async () => {
|
|
41
48
|
let resolve!: () => void;
|
|
42
|
-
const p = pool(
|
|
43
|
-
|
|
44
|
-
() => new Promise<void>((r) => { resolve = r; }),
|
|
45
|
-
);
|
|
46
|
-
const task = p();
|
|
49
|
+
const p = pool(1, () => new Promise<void>((r) => { resolve = r; }));
|
|
50
|
+
const t = p();
|
|
47
51
|
expect(p.loading()).toBe(true);
|
|
48
52
|
resolve();
|
|
49
|
-
await
|
|
53
|
+
await t;
|
|
50
54
|
expect(p.loading()).toBe(false);
|
|
51
55
|
});
|
|
52
56
|
|
|
53
57
|
it("captures errors in the error signal and resolves undefined", async () => {
|
|
54
|
-
const p = pool(1, async () => {
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const p = pool(1, async () => { throw new Error("pool error"); });
|
|
59
|
+
expect(await p()).toBeUndefined();
|
|
60
|
+
expect(p.error()?.message).toBe("pool error");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("wraps non-Error throws in an Error object", async () => {
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
|
65
|
+
const p = pool(1, async () => { throw "plain string error"; });
|
|
66
|
+
expect(await p()).toBeUndefined();
|
|
67
|
+
expect(p.error()).toBeInstanceOf(Error);
|
|
68
|
+
expect(p.error()?.message).toBe("plain string error");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("AbortError thrown by fn itself (no cancelAll) is not stored in error()", async () => {
|
|
72
|
+
const p = pool(1, async () => { throw new DOMException("Aborted", "AbortError"); });
|
|
57
73
|
const result = await p();
|
|
58
74
|
expect(result).toBeUndefined();
|
|
59
|
-
expect(p.error()
|
|
75
|
+
expect(p.error()).toBeNull();
|
|
60
76
|
});
|
|
61
77
|
|
|
62
78
|
it("pending count decrements as tasks start", async () => {
|
|
63
79
|
let resolveFirst!: () => void;
|
|
64
|
-
const started: number[] = [];
|
|
65
80
|
const p = pool(1, async (idx: unknown) => {
|
|
66
|
-
started.push(idx as number);
|
|
67
81
|
if (idx === 0) await new Promise<void>((r) => { resolveFirst = r; });
|
|
68
82
|
});
|
|
69
|
-
|
|
70
83
|
const t1 = p(0);
|
|
71
84
|
const t2 = p(1);
|
|
72
|
-
|
|
73
|
-
// second task is pending while first runs
|
|
74
85
|
expect(p.pending()).toBe(1);
|
|
75
86
|
resolveFirst();
|
|
76
87
|
await t1;
|
|
@@ -78,127 +89,124 @@ describe("pool", () => {
|
|
|
78
89
|
expect(p.pending()).toBe(0);
|
|
79
90
|
});
|
|
80
91
|
|
|
81
|
-
it("wraps non-Error throws in an Error object", async () => {
|
|
82
|
-
const p = pool(1, async () => {
|
|
83
|
-
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
|
84
|
-
throw "plain string error";
|
|
85
|
-
});
|
|
86
|
-
const result = await p();
|
|
87
|
-
expect(result).toBeUndefined();
|
|
88
|
-
expect(p.error()).toBeInstanceOf(Error);
|
|
89
|
-
expect(p.error()?.message).toBe("plain string error");
|
|
90
|
-
});
|
|
91
|
-
|
|
92
92
|
it("concurrency=1 guarantees serial execution", async () => {
|
|
93
93
|
const order: number[] = [];
|
|
94
94
|
let resolveFirst!: () => void;
|
|
95
|
-
|
|
96
95
|
const p = pool(1, async (n: unknown) => {
|
|
97
96
|
if (n === 0) await new Promise<void>((r) => { resolveFirst = r; });
|
|
98
97
|
order.push(n as number);
|
|
99
98
|
});
|
|
100
|
-
|
|
101
99
|
const t1 = p(0);
|
|
102
100
|
const t2 = p(1);
|
|
103
|
-
|
|
104
|
-
// t2 cannot start until t1 finishes
|
|
105
101
|
expect(p.active()).toBe(1);
|
|
106
102
|
expect(p.pending()).toBe(1);
|
|
107
|
-
|
|
108
103
|
resolveFirst();
|
|
109
104
|
await t1;
|
|
110
105
|
await t2;
|
|
111
|
-
|
|
112
|
-
expect(order).toEqual([0, 1]); // serial order preserved
|
|
106
|
+
expect(order).toEqual([0, 1]);
|
|
113
107
|
});
|
|
114
108
|
|
|
115
109
|
it("error in one task does not block subsequent tasks", async () => {
|
|
116
|
-
const results: Array<number | undefined> = [];
|
|
117
110
|
const p = pool(1, async (n: unknown) => {
|
|
118
111
|
if (n === 0) throw new Error("task 0 failed");
|
|
119
112
|
return n as number;
|
|
120
113
|
});
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const r1 = p(1);
|
|
124
|
-
|
|
125
|
-
results.push(await r0);
|
|
126
|
-
results.push(await r1);
|
|
127
|
-
|
|
128
|
-
expect(results[0]).toBeUndefined(); // error → undefined
|
|
129
|
-
expect(results[1]).toBe(1); // subsequent task ran
|
|
114
|
+
expect(await p(0)).toBeUndefined();
|
|
115
|
+
expect(await p(1)).toBe(1);
|
|
130
116
|
});
|
|
131
117
|
|
|
132
|
-
it("concurrency=0 — clamps to 1
|
|
118
|
+
it("concurrency=0 — clamps to 1", async () => {
|
|
133
119
|
const fn = vi.fn(async () => "result");
|
|
134
120
|
const p = pool(0, fn);
|
|
135
|
-
|
|
136
|
-
// concurrency=0 is clamped to 1, so the task runs immediately
|
|
137
|
-
const result = await p();
|
|
121
|
+
expect(await p()).toBe("result");
|
|
138
122
|
expect(fn).toHaveBeenCalledOnce();
|
|
139
|
-
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("cancelAll() when idle — no-op, no crash", () => {
|
|
126
|
+
const p = pool(2, async () => "ok");
|
|
127
|
+
expect(() => p.cancelAll()).not.toThrow();
|
|
140
128
|
expect(p.pending()).toBe(0);
|
|
141
|
-
expect(p.
|
|
129
|
+
expect(p.active()).toBe(0);
|
|
142
130
|
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ── With AbortSignal (opt-in via first param named "signal") ──────────────────
|
|
143
134
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
let
|
|
135
|
+
describe("pool — with signal", () => {
|
|
136
|
+
it("passes a live AbortSignal to fn", async () => {
|
|
137
|
+
let capturedSignal!: AbortSignal;
|
|
138
|
+
const p = pool(1, async (signal) => { capturedSignal = signal as AbortSignal; });
|
|
139
|
+
await p();
|
|
140
|
+
expect(capturedSignal).toBeInstanceOf(AbortSignal);
|
|
141
|
+
expect(capturedSignal.aborted).toBe(false);
|
|
142
|
+
});
|
|
147
143
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
144
|
+
it("executes tasks and passes correct args alongside signal", async () => {
|
|
145
|
+
const p = pool(2, async (signal, x: unknown) => { void (signal as AbortSignal); return (x as number) * 3; });
|
|
146
|
+
expect(await p(4)).toBe(12);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("cancelAll() aborts all active signals", async () => {
|
|
150
|
+
const signals: AbortSignal[] = [];
|
|
151
|
+
const resolvers: Array<() => void> = [];
|
|
152
|
+
const p = pool(3, async (signal) => {
|
|
153
|
+
signals.push(signal as AbortSignal);
|
|
154
|
+
await new Promise<void>((r) => resolvers.push(r));
|
|
151
155
|
});
|
|
156
|
+
const t1 = p();
|
|
157
|
+
const t2 = p();
|
|
158
|
+
const t3 = p();
|
|
159
|
+
await Promise.resolve();
|
|
160
|
+
p.cancelAll();
|
|
161
|
+
resolvers.forEach((r) => r());
|
|
162
|
+
await Promise.all([t1, t2, t3]);
|
|
163
|
+
expect(signals.every((s) => s.aborted)).toBe(true);
|
|
164
|
+
});
|
|
152
165
|
|
|
153
|
-
|
|
154
|
-
|
|
166
|
+
it("cancelAll() resolves active tasks as undefined without setting error()", async () => {
|
|
167
|
+
const p = pool(1, async (signal) => { await delay(10000, signal as AbortSignal); });
|
|
168
|
+
const t = p();
|
|
169
|
+
await Promise.resolve();
|
|
170
|
+
p.cancelAll();
|
|
171
|
+
expect(await t).toBeUndefined();
|
|
172
|
+
expect(p.error()).toBeNull();
|
|
173
|
+
expect(p.active()).toBe(0);
|
|
155
174
|
});
|
|
156
175
|
|
|
157
|
-
it("
|
|
158
|
-
const order: number[] = [];
|
|
176
|
+
it("cancelAll() resolves pending tasks as undefined", async () => {
|
|
159
177
|
let resolveFirst!: () => void;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
order.push(n as number);
|
|
178
|
+
const p = pool(1, async (signal, idx: unknown) => {
|
|
179
|
+
void (signal as AbortSignal);
|
|
180
|
+
if (idx === 0) await new Promise<void>((r) => { resolveFirst = r; });
|
|
164
181
|
});
|
|
165
|
-
|
|
166
182
|
const t1 = p(0);
|
|
167
183
|
const t2 = p(1);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
const t3 = p(2);
|
|
185
|
+
await Promise.resolve();
|
|
186
|
+
p.cancelAll();
|
|
172
187
|
resolveFirst();
|
|
173
|
-
await t1;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
expect(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
it("multiple tasks error — error signal reflects last error", async () => {
|
|
180
|
-
let callCount = 0;
|
|
181
|
-
const p = pool(1, async () => {
|
|
182
|
-
callCount++;
|
|
183
|
-
throw new Error(`error ${callCount}`);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
await p();
|
|
187
|
-
await p();
|
|
188
|
-
await p();
|
|
189
|
-
|
|
190
|
-
expect(p.error()?.message).toBe("error 3");
|
|
188
|
+
const [r1, r2, r3] = await Promise.all([t1, t2, t3]);
|
|
189
|
+
expect(r1).toBeUndefined();
|
|
190
|
+
expect(r2).toBeUndefined();
|
|
191
|
+
expect(r3).toBeUndefined();
|
|
192
|
+
expect(p.pending()).toBe(0);
|
|
193
|
+
expect(p.error()).toBeNull();
|
|
191
194
|
});
|
|
192
195
|
|
|
193
|
-
it("
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
it("cancelAll() then new runs work normally", async () => {
|
|
197
|
+
let resolve!: () => void;
|
|
198
|
+
const p = pool(1, async (signal, which: unknown) => {
|
|
199
|
+
void (signal as AbortSignal);
|
|
200
|
+
if (which === "stale") await new Promise<void>((r) => { resolve = r; });
|
|
201
|
+
return which as string;
|
|
196
202
|
});
|
|
197
|
-
|
|
198
|
-
await
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
const stale = p("stale");
|
|
204
|
+
await Promise.resolve();
|
|
205
|
+
p.cancelAll();
|
|
206
|
+
resolve();
|
|
207
|
+
await stale;
|
|
208
|
+
expect(await p("fresh")).toBe("fresh");
|
|
202
209
|
expect(p.active()).toBe(0);
|
|
210
|
+
expect(p.error()).toBeNull();
|
|
203
211
|
});
|
|
204
212
|
});
|
|
@@ -2,28 +2,35 @@ import { describe, it, expect } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import { queue, QueueClearedError } from "../queue";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** Abortable delay — rejects with AbortError when signal fires. */
|
|
6
|
+
function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const timer = setTimeout(resolve, ms);
|
|
9
|
+
signal?.addEventListener("abort", () => {
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
reject(Object.assign(new Error("Aborted"), { name: "AbortError" }));
|
|
12
|
+
}, { once: true });
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── Without AbortSignal (existing behaviour) ───────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe("queue — without signal", () => {
|
|
6
19
|
it("executes the task and resolves the promise", async () => {
|
|
7
20
|
const q = queue(async (x: unknown) => (x as number) * 2);
|
|
8
|
-
|
|
9
|
-
expect(result).toBe(10);
|
|
21
|
+
expect(await q(5)).toBe(10);
|
|
10
22
|
});
|
|
11
23
|
|
|
12
24
|
it("runs tasks sequentially (FIFO)", async () => {
|
|
13
25
|
const order: number[] = [];
|
|
14
|
-
const q = queue(async (n: unknown) => {
|
|
15
|
-
order.push(n as number);
|
|
16
|
-
return n as number;
|
|
17
|
-
});
|
|
26
|
+
const q = queue(async (n: unknown) => { order.push(n as number); return n as number; });
|
|
18
27
|
await Promise.all([q(1), q(2), q(3)]);
|
|
19
28
|
expect(order).toEqual([1, 2, 3]);
|
|
20
29
|
});
|
|
21
30
|
|
|
22
31
|
it("loading is true while running, false when done", async () => {
|
|
23
32
|
let resolve!: () => void;
|
|
24
|
-
const q = queue(
|
|
25
|
-
() => new Promise<void>((r) => { resolve = r; }),
|
|
26
|
-
);
|
|
33
|
+
const q = queue(() => new Promise<void>((r) => { resolve = r; }));
|
|
27
34
|
const p = q();
|
|
28
35
|
expect(q.loading()).toBe(true);
|
|
29
36
|
resolve();
|
|
@@ -33,39 +40,29 @@ describe("queue", () => {
|
|
|
33
40
|
|
|
34
41
|
it("pending decrements as tasks complete", async () => {
|
|
35
42
|
let resolveFirst!: () => void;
|
|
36
|
-
const q = queue(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
: Promise.resolve(),
|
|
43
|
+
const q = queue((_: unknown, idx: unknown) =>
|
|
44
|
+
idx === 0
|
|
45
|
+
? new Promise<void>((r) => { resolveFirst = r; })
|
|
46
|
+
: Promise.resolve(),
|
|
41
47
|
);
|
|
42
|
-
|
|
43
48
|
const p1 = q(null, 0);
|
|
44
49
|
const p2 = q(null, 1);
|
|
45
|
-
|
|
46
|
-
// Both enqueued — first is running, second is pending
|
|
47
50
|
expect(q.pending()).toBeGreaterThanOrEqual(0);
|
|
48
|
-
|
|
49
51
|
resolveFirst();
|
|
50
52
|
await p1;
|
|
51
53
|
await p2;
|
|
52
|
-
|
|
53
54
|
expect(q.pending()).toBe(0);
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
it("captures errors in the error signal", async () => {
|
|
57
|
-
const q = queue(async () => {
|
|
58
|
-
throw new Error("boom");
|
|
59
|
-
});
|
|
58
|
+
const q = queue(async () => { throw new Error("boom"); });
|
|
60
59
|
await expect(q()).rejects.toThrow("boom");
|
|
61
60
|
expect(q.error()?.message).toBe("boom");
|
|
62
61
|
});
|
|
63
62
|
|
|
64
63
|
it("wraps non-Error throws in an Error", async () => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
throw "string error";
|
|
68
|
-
});
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
|
65
|
+
const q = queue(async () => { throw "string error"; });
|
|
69
66
|
await expect(q()).rejects.toThrow("string error");
|
|
70
67
|
expect(q.error()?.message).toBe("string error");
|
|
71
68
|
});
|
|
@@ -75,81 +72,43 @@ describe("queue", () => {
|
|
|
75
72
|
if (n === 0) throw new Error("task 0 failed");
|
|
76
73
|
return n as number;
|
|
77
74
|
});
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
const p1 = q(1); // should still run despite p0 failing
|
|
75
|
+
const p0 = q(0);
|
|
76
|
+
const p1 = q(1);
|
|
81
77
|
const p2 = q(2);
|
|
82
|
-
|
|
83
78
|
await expect(p0).rejects.toThrow("task 0 failed");
|
|
84
79
|
expect(await p1).toBe(1);
|
|
85
80
|
expect(await p2).toBe(2);
|
|
86
|
-
expect(q.error()?.message).toBe("task 0 failed");
|
|
87
81
|
});
|
|
88
82
|
|
|
89
|
-
it("clear()
|
|
83
|
+
it("clear() with pending tasks rejects their promises with QueueClearedError", async () => {
|
|
90
84
|
let resolveFirst!: () => void;
|
|
91
|
-
const q = queue(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
: Promise.resolve(),
|
|
85
|
+
const q = queue((_: unknown, idx: unknown) =>
|
|
86
|
+
idx === 0
|
|
87
|
+
? new Promise<void>((r) => { resolveFirst = r; })
|
|
88
|
+
: Promise.resolve(),
|
|
96
89
|
);
|
|
97
|
-
|
|
98
90
|
q(null, 0);
|
|
99
91
|
const p1 = q(null, 1);
|
|
100
92
|
const p2 = q(null, 2);
|
|
101
|
-
|
|
102
|
-
// Suppress expected rejections from clear()
|
|
103
|
-
p1.catch(() => undefined);
|
|
104
|
-
p2.catch(() => undefined);
|
|
105
|
-
|
|
106
93
|
q.clear();
|
|
107
|
-
resolveFirst();
|
|
108
|
-
|
|
109
|
-
// After clearing, pending count should be 0
|
|
110
|
-
expect(q.pending()).toBe(0);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("clear() with pending tasks rejects their promises with QueueClearedError", async () => {
|
|
114
|
-
let resolveFirst!: () => void;
|
|
115
|
-
const q = queue(
|
|
116
|
-
(_: unknown, idx: unknown) =>
|
|
117
|
-
idx === 0
|
|
118
|
-
? new Promise<void>((r) => { resolveFirst = r; })
|
|
119
|
-
: Promise.resolve(),
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
q(null, 0); // running — not in _queue
|
|
123
|
-
const p1 = q(null, 1); // pending — in _queue
|
|
124
|
-
const p2 = q(null, 2); // pending — in _queue
|
|
125
|
-
|
|
126
|
-
q.clear();
|
|
127
|
-
|
|
128
94
|
await expect(p1).rejects.toBeInstanceOf(QueueClearedError);
|
|
129
95
|
await expect(p2).rejects.toBeInstanceOf(QueueClearedError);
|
|
130
|
-
|
|
131
96
|
resolveFirst();
|
|
132
97
|
});
|
|
133
98
|
|
|
134
99
|
it("clear() then new enqueue() — queue works normally after being cleared", async () => {
|
|
135
100
|
let resolveFirst!: () => void;
|
|
136
|
-
const q = queue(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
: Promise.resolve(n as number),
|
|
101
|
+
const q = queue((n: unknown, stall: unknown) =>
|
|
102
|
+
stall
|
|
103
|
+
? new Promise<number>((r) => { resolveFirst = () => r(n as number); })
|
|
104
|
+
: Promise.resolve(n as number),
|
|
141
105
|
);
|
|
142
|
-
|
|
143
|
-
q(
|
|
144
|
-
const p1 = q(1, false); // pending
|
|
106
|
+
q(0, true);
|
|
107
|
+
const p1 = q(1, false);
|
|
145
108
|
q.clear();
|
|
146
|
-
|
|
147
109
|
await expect(p1).rejects.toBeInstanceOf(QueueClearedError);
|
|
148
110
|
resolveFirst();
|
|
149
|
-
|
|
150
|
-
// Queue should work normally after clearing
|
|
151
|
-
const result = await q(42, false);
|
|
152
|
-
expect(result).toBe(42);
|
|
111
|
+
expect(await q(42, false)).toBe(42);
|
|
153
112
|
expect(q.pending()).toBe(0);
|
|
154
113
|
});
|
|
155
114
|
|
|
@@ -159,50 +118,80 @@ describe("queue", () => {
|
|
|
159
118
|
expect(q.pending()).toBe(0);
|
|
160
119
|
});
|
|
161
120
|
|
|
162
|
-
it("
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
const p0 = q(0);
|
|
169
|
-
const p1 = q(1);
|
|
170
|
-
const p2 = q(2);
|
|
171
|
-
|
|
172
|
-
await expect(p0).rejects.toThrow("bad");
|
|
173
|
-
expect(await p1).toBe(1);
|
|
174
|
-
expect(await p2).toBe(2);
|
|
121
|
+
it("very large queue (100 tasks) — all execute in order", async () => {
|
|
122
|
+
const order: number[] = [];
|
|
123
|
+
const q = queue(async (n: unknown) => { order.push(n as number); return n as number; });
|
|
124
|
+
await Promise.all(Array.from({ length: 100 }, (_, i) => q(i)));
|
|
125
|
+
expect(order).toEqual(Array.from({ length: 100 }, (_, i) => i));
|
|
175
126
|
expect(q.pending()).toBe(0);
|
|
176
127
|
});
|
|
128
|
+
});
|
|
177
129
|
|
|
178
|
-
|
|
179
|
-
const q = queue(async (n: unknown) => {
|
|
180
|
-
throw new Error(`error ${n as number}`);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
const p0 = q(0);
|
|
184
|
-
const p1 = q(1);
|
|
185
|
-
const p2 = q(2);
|
|
130
|
+
// ── With AbortSignal (opt-in via first param named "signal") ──────────────────
|
|
186
131
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
132
|
+
describe("queue — with signal", () => {
|
|
133
|
+
it("passes a live AbortSignal to fn", async () => {
|
|
134
|
+
let capturedSignal!: AbortSignal;
|
|
135
|
+
const q = queue(async (signal) => { capturedSignal = signal as AbortSignal; });
|
|
136
|
+
await q();
|
|
137
|
+
expect(capturedSignal).toBeInstanceOf(AbortSignal);
|
|
138
|
+
expect(capturedSignal.aborted).toBe(false);
|
|
139
|
+
});
|
|
190
140
|
|
|
191
|
-
|
|
192
|
-
|
|
141
|
+
it("executes tasks and passes correct args alongside signal", async () => {
|
|
142
|
+
const q = queue(async (signal, x: unknown) => { void (signal as AbortSignal); return (x as number) * 2; });
|
|
143
|
+
expect(await q(5)).toBe(10);
|
|
193
144
|
});
|
|
194
145
|
|
|
195
|
-
it("
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
146
|
+
it("clear() aborts the running item's signal", async () => {
|
|
147
|
+
let capturedSignal!: AbortSignal;
|
|
148
|
+
let resolve!: () => void;
|
|
149
|
+
const q = queue(async (signal) => {
|
|
150
|
+
capturedSignal = signal as AbortSignal;
|
|
151
|
+
await new Promise<void>((r) => { resolve = r; });
|
|
200
152
|
});
|
|
153
|
+
const p = q();
|
|
154
|
+
p.catch(() => undefined);
|
|
155
|
+
await Promise.resolve(); // let drain start
|
|
156
|
+
q.clear();
|
|
157
|
+
resolve();
|
|
158
|
+
await p.catch(() => undefined);
|
|
159
|
+
expect(capturedSignal.aborted).toBe(true);
|
|
160
|
+
});
|
|
201
161
|
|
|
202
|
-
|
|
203
|
-
await
|
|
162
|
+
it("clear() rejects the running item with QueueClearedError", async () => {
|
|
163
|
+
const q = queue(async (signal) => { await delay(10000, signal as AbortSignal); });
|
|
164
|
+
const p = q();
|
|
165
|
+
await Promise.resolve();
|
|
166
|
+
q.clear();
|
|
167
|
+
await expect(p).rejects.toBeInstanceOf(QueueClearedError);
|
|
168
|
+
});
|
|
204
169
|
|
|
205
|
-
|
|
206
|
-
|
|
170
|
+
it("AbortError from clear() is not stored in error()", async () => {
|
|
171
|
+
const q = queue(async (signal) => { await delay(10000, signal as AbortSignal); });
|
|
172
|
+
const p = q();
|
|
173
|
+
await Promise.resolve();
|
|
174
|
+
q.clear();
|
|
175
|
+
await p.catch(() => undefined);
|
|
176
|
+
expect(q.error()).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("each pending item gets its own AbortSignal, all aborted on clear()", async () => {
|
|
180
|
+
const signals: AbortSignal[] = [];
|
|
181
|
+
let resolveFirst!: () => void;
|
|
182
|
+
const q = queue(async (signal, idx: unknown) => {
|
|
183
|
+
signals.push(signal as AbortSignal);
|
|
184
|
+
if (idx === 0) await new Promise<void>((r) => { resolveFirst = r; });
|
|
185
|
+
});
|
|
186
|
+
const p0 = q(0);
|
|
187
|
+
const p1 = q(1);
|
|
188
|
+
const p2 = q(2);
|
|
189
|
+
p1.catch(() => undefined);
|
|
190
|
+
p2.catch(() => undefined);
|
|
191
|
+
await Promise.resolve();
|
|
192
|
+
q.clear();
|
|
193
|
+
resolveFirst();
|
|
194
|
+
await Promise.all([p0.catch(() => undefined), p1.catch(() => undefined), p2.catch(() => undefined)]);
|
|
195
|
+
expect(signals[0]?.aborted).toBe(true);
|
|
207
196
|
});
|
|
208
197
|
});
|