@praxisjs/concurrent 1.2.13 → 1.3.0

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/__tests__/decorators.test.js +60 -3
  3. package/dist/__tests__/decorators.test.js.map +1 -1
  4. package/dist/__tests__/pool.test.js +103 -70
  5. package/dist/__tests__/pool.test.js.map +1 -1
  6. package/dist/__tests__/queue.test.js +86 -71
  7. package/dist/__tests__/queue.test.js.map +1 -1
  8. package/dist/__tests__/task.test.js +90 -68
  9. package/dist/__tests__/task.test.js.map +1 -1
  10. package/dist/__tests__/utils.test.d.ts +2 -0
  11. package/dist/__tests__/utils.test.d.ts.map +1 -0
  12. package/dist/__tests__/utils.test.js +32 -0
  13. package/dist/__tests__/utils.test.js.map +1 -0
  14. package/dist/decorators.d.ts +13 -3
  15. package/dist/decorators.d.ts.map +1 -1
  16. package/dist/decorators.js +8 -3
  17. package/dist/decorators.js.map +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/pool.d.ts +4 -1
  23. package/dist/pool.d.ts.map +1 -1
  24. package/dist/pool.js +29 -5
  25. package/dist/pool.js.map +1 -1
  26. package/dist/queue.d.ts +3 -1
  27. package/dist/queue.d.ts.map +1 -1
  28. package/dist/queue.js +35 -9
  29. package/dist/queue.js.map +1 -1
  30. package/dist/task.d.ts +3 -1
  31. package/dist/task.d.ts.map +1 -1
  32. package/dist/task.js +16 -3
  33. package/dist/task.js.map +1 -1
  34. package/dist/utils.d.ts +9 -0
  35. package/dist/utils.d.ts.map +1 -0
  36. package/dist/utils.js +26 -0
  37. package/dist/utils.js.map +1 -0
  38. package/package.json +4 -4
  39. package/src/__tests__/decorators.test.ts +78 -3
  40. package/src/__tests__/pool.test.ts +110 -102
  41. package/src/__tests__/queue.test.ts +103 -114
  42. package/src/__tests__/task.test.ts +96 -90
  43. package/src/__tests__/utils.test.ts +48 -0
  44. package/src/decorators.ts +23 -6
  45. package/src/index.ts +1 -0
  46. package/src/pool.ts +33 -4
  47. package/src/queue.ts +36 -8
  48. package/src/task.ts +18 -2
  49. 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
- describe("pool", () => {
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
- 1,
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 task;
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
- throw new Error("pool error");
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()?.message).toBe("pool 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
- const r0 = p(0);
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, tasks execute serially", async () => {
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
- expect(result).toBe("result");
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.loading()).toBe(false);
129
+ expect(p.active()).toBe(0);
142
130
  });
131
+ });
132
+
133
+ // ── With AbortSignal (opt-in via first param named "signal") ──────────────────
143
134
 
144
- it("active() never exceeds concurrency", async () => {
145
- const concurrency = 2;
146
- let maxObservedActive = 0;
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
- const p = pool(concurrency, async () => {
149
- maxObservedActive = Math.max(maxObservedActive, p.active());
150
- await new Promise((r) => setTimeout(r, 5));
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
- await Promise.all([p(), p(), p(), p()]);
154
- expect(maxObservedActive).toBeLessThanOrEqual(concurrency);
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("pool(-5, fn) — clamps to 1, runs tasks serially", async () => {
158
- const order: number[] = [];
176
+ it("cancelAll() resolves pending tasks as undefined", async () => {
159
177
  let resolveFirst!: () => void;
160
-
161
- const p = pool(-5, async (n: unknown) => {
162
- if (n === 0) await new Promise<void>((r) => { resolveFirst = r; });
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
- expect(p.active()).toBe(1);
170
- expect(p.pending()).toBe(1);
171
-
184
+ const t3 = p(2);
185
+ await Promise.resolve();
186
+ p.cancelAll();
172
187
  resolveFirst();
173
- await t1;
174
- await t2;
175
-
176
- expect(order).toEqual([0, 1]);
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("active() never goes negative even if task throws synchronously", async () => {
194
- const p = pool(2, async () => {
195
- throw new Error("sync-ish throw");
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 p();
199
- await p();
200
-
201
- expect(p.active()).toBeGreaterThanOrEqual(0);
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
- describe("queue", () => {
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
- const result = await q(5);
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
- (_: unknown, idx: unknown) =>
38
- idx === 0
39
- ? new Promise<void>((r) => { resolveFirst = r; })
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
- const q = queue(async () => {
66
- // eslint-disable-next-line @typescript-eslint/no-throw-literal
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 p0 = q(0); // will reject
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() empties the pending queue", async () => {
83
+ it("clear() with pending tasks rejects their promises with QueueClearedError", async () => {
90
84
  let resolveFirst!: () => void;
91
- const q = queue(
92
- (_: unknown, idx: unknown) =>
93
- idx === 0
94
- ? new Promise<void>((r) => { resolveFirst = r; })
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
- (n: unknown, stall: unknown) =>
138
- stall
139
- ? new Promise<number>((r) => { resolveFirst = () => r(n as number); })
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(0, true); // running — stalls
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("error in task does not corrupt pending counter", async () => {
163
- const q = queue(async (n: unknown) => {
164
- if (n === 0) throw new Error("bad");
165
- return n as number;
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
- it("multiple tasks error — only last error stored", async () => {
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
- await expect(p0).rejects.toThrow("error 0");
188
- await expect(p1).rejects.toThrow("error 1");
189
- await expect(p2).rejects.toThrow("error 2");
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
- // Only the last error is stored
192
- expect(q.error()?.message).toBe("error 2");
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("very large queue (100 tasks, concurrency=1) — all execute in order", async () => {
196
- const order: number[] = [];
197
- const q = queue(async (n: unknown) => {
198
- order.push(n as number);
199
- return n as number;
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
- const tasks = Array.from({ length: 100 }, (_, i) => q(i));
203
- await Promise.all(tasks);
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
- expect(order).toEqual(Array.from({ length: 100 }, (_, i) => i));
206
- expect(q.pending()).toBe(0);
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
  });