@praxisjs/concurrent 1.3.8 → 1.3.9
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 +23 -0
- package/dist/decorators.d.ts +3 -3
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +23 -38
- package/dist/decorators.js.map +1 -1
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +64 -43
- package/dist/pool.js.map +1 -1
- package/dist/queue.d.ts.map +1 -1
- package/dist/queue.js +53 -50
- package/dist/queue.js.map +1 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +39 -33
- package/dist/task.js.map +1 -1
- package/dist/utils.d.ts +12 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +42 -0
- package/dist/utils.js.map +1 -1
- package/package.json +15 -6
- package/src/decorators.ts +37 -38
- package/src/pool.ts +64 -46
- package/src/queue.ts +60 -56
- package/src/task.ts +38 -34
- package/src/utils.ts +55 -0
- package/dist/__tests__/decorators.test.d.ts +0 -2
- package/dist/__tests__/decorators.test.d.ts.map +0 -1
- package/dist/__tests__/decorators.test.js +0 -195
- package/dist/__tests__/decorators.test.js.map +0 -1
- package/dist/__tests__/pool.test.d.ts +0 -2
- package/dist/__tests__/pool.test.d.ts.map +0 -1
- package/dist/__tests__/pool.test.js +0 -196
- package/dist/__tests__/pool.test.js.map +0 -1
- package/dist/__tests__/queue.test.d.ts +0 -2
- package/dist/__tests__/queue.test.d.ts.map +0 -1
- package/dist/__tests__/queue.test.js +0 -173
- package/dist/__tests__/queue.test.js.map +0 -1
- package/dist/__tests__/task.test.d.ts +0 -2
- package/dist/__tests__/task.test.d.ts.map +0 -1
- package/dist/__tests__/task.test.js +0 -182
- package/dist/__tests__/task.test.js.map +0 -1
- package/dist/__tests__/utils.test.d.ts +0 -2
- package/dist/__tests__/utils.test.d.ts.map +0 -1
- package/dist/__tests__/utils.test.js +0 -32
- package/dist/__tests__/utils.test.js.map +0 -1
- package/src/__tests__/decorators.test.ts +0 -267
- package/src/__tests__/pool.test.ts +0 -212
- package/src/__tests__/queue.test.ts +0 -197
- package/src/__tests__/task.test.ts +0 -205
- package/src/__tests__/utils.test.ts +0 -48
- package/tsconfig.json +0 -8
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { Task, Queue, Pool } from "../decorators";
|
|
4
|
-
import { QueueClearedError } from "../queue";
|
|
5
|
-
|
|
6
|
-
function fieldCtx(name: string) {
|
|
7
|
-
const initializers: Array<(this: unknown) => void> = [];
|
|
8
|
-
return {
|
|
9
|
-
ctx: {
|
|
10
|
-
name,
|
|
11
|
-
kind: "field" as const,
|
|
12
|
-
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
13
|
-
} as ClassFieldDecoratorContext,
|
|
14
|
-
run(instance: unknown) { initializers.forEach((fn) => { fn.call(instance); }); },
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// ── Task ──────────────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
describe("Task decorator", () => {
|
|
21
|
-
it("wraps the named method and exposes signals as sub-properties", async () => {
|
|
22
|
-
const { ctx, run } = fieldCtx("taskLoad");
|
|
23
|
-
Task("load")(undefined, ctx);
|
|
24
|
-
|
|
25
|
-
const instance: Record<string, unknown> = {
|
|
26
|
-
load: async (x: unknown) => (x as number) * 2,
|
|
27
|
-
};
|
|
28
|
-
run(instance);
|
|
29
|
-
|
|
30
|
-
const taskLoad = instance.taskLoad as {
|
|
31
|
-
(...args: unknown[]): Promise<number>;
|
|
32
|
-
loading: () => boolean;
|
|
33
|
-
error: () => Error | null;
|
|
34
|
-
lastResult: () => number | null;
|
|
35
|
-
cancelAll: () => void;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
expect(typeof taskLoad).toBe("function");
|
|
39
|
-
expect(taskLoad.loading).toBeDefined();
|
|
40
|
-
expect(taskLoad.error).toBeDefined();
|
|
41
|
-
expect(taskLoad.lastResult).toBeDefined();
|
|
42
|
-
expect(taskLoad.cancelAll).toBeDefined();
|
|
43
|
-
|
|
44
|
-
const result = await taskLoad(5);
|
|
45
|
-
expect(result).toBe(10);
|
|
46
|
-
expect(taskLoad.lastResult()).toBe(10);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("sets loading while running", async () => {
|
|
50
|
-
const { ctx, run } = fieldCtx("taskFetch");
|
|
51
|
-
Task("fetch")(undefined, ctx);
|
|
52
|
-
|
|
53
|
-
let resolve!: (v: string) => void;
|
|
54
|
-
const instance: Record<string, unknown> = {
|
|
55
|
-
fetch: async () => new Promise<string>((r) => { resolve = r; }),
|
|
56
|
-
};
|
|
57
|
-
run(instance);
|
|
58
|
-
|
|
59
|
-
const taskFetch = instance.taskFetch as { (): Promise<string>; loading: () => boolean };
|
|
60
|
-
const p = taskFetch();
|
|
61
|
-
expect(taskFetch.loading()).toBe(true);
|
|
62
|
-
resolve("done");
|
|
63
|
-
await p;
|
|
64
|
-
expect(taskFetch.loading()).toBe(false);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("cancelAll() aborts the in-flight signal", async () => {
|
|
68
|
-
const { ctx, run } = fieldCtx("taskFetch");
|
|
69
|
-
Task("fetch")(undefined, ctx);
|
|
70
|
-
|
|
71
|
-
let capturedSignal!: AbortSignal;
|
|
72
|
-
let resolve!: () => void;
|
|
73
|
-
const instance: Record<string, unknown> = {
|
|
74
|
-
fetch: async (signal: AbortSignal) => {
|
|
75
|
-
capturedSignal = signal;
|
|
76
|
-
await new Promise<void>((r) => { resolve = r; });
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
run(instance);
|
|
80
|
-
|
|
81
|
-
const taskFetch = instance.taskFetch as { (): Promise<void>; cancelAll: () => void };
|
|
82
|
-
const p = taskFetch();
|
|
83
|
-
taskFetch.cancelAll();
|
|
84
|
-
resolve();
|
|
85
|
-
await p;
|
|
86
|
-
|
|
87
|
-
expect(capturedSignal.aborted).toBe(true);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// ── Queue ─────────────────────────────────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
describe("Queue decorator", () => {
|
|
94
|
-
it("wraps the named method and exposes signals", async () => {
|
|
95
|
-
const { ctx, run } = fieldCtx("taskSave");
|
|
96
|
-
Queue("save")(undefined, ctx);
|
|
97
|
-
|
|
98
|
-
const instance: Record<string, unknown> = {
|
|
99
|
-
save: async (x: unknown) => String(x),
|
|
100
|
-
};
|
|
101
|
-
run(instance);
|
|
102
|
-
|
|
103
|
-
const taskSave = instance.taskSave as {
|
|
104
|
-
(...args: unknown[]): Promise<string>;
|
|
105
|
-
loading: () => boolean;
|
|
106
|
-
pending: () => number;
|
|
107
|
-
error: () => Error | null;
|
|
108
|
-
clear: () => void;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
expect(typeof taskSave).toBe("function");
|
|
112
|
-
expect(taskSave.loading).toBeDefined();
|
|
113
|
-
expect(taskSave.pending).toBeDefined();
|
|
114
|
-
expect(taskSave.error).toBeDefined();
|
|
115
|
-
expect(taskSave.clear).toBeDefined();
|
|
116
|
-
|
|
117
|
-
const result = await taskSave("hello");
|
|
118
|
-
expect(result).toBe("hello");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("clear() rejects queued calls with QueueClearedError", async () => {
|
|
122
|
-
const { ctx, run } = fieldCtx("taskSave");
|
|
123
|
-
Queue("save")(undefined, ctx);
|
|
124
|
-
|
|
125
|
-
let resolveFirst!: () => void;
|
|
126
|
-
const instance: Record<string, unknown> = {
|
|
127
|
-
save: async (signal: AbortSignal, _: unknown, idx: unknown) => {
|
|
128
|
-
void signal;
|
|
129
|
-
return idx === 0
|
|
130
|
-
? new Promise<void>((r) => { resolveFirst = r; })
|
|
131
|
-
: Promise.resolve();
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
run(instance);
|
|
135
|
-
|
|
136
|
-
const taskSave = instance.taskSave as { (...args: unknown[]): Promise<unknown>; clear: () => void };
|
|
137
|
-
taskSave(null, 0);
|
|
138
|
-
const p1 = taskSave(null, 1);
|
|
139
|
-
taskSave.clear();
|
|
140
|
-
|
|
141
|
-
await expect(p1).rejects.toThrow("Queue cleared");
|
|
142
|
-
resolveFirst();
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// ── Pool ──────────────────────────────────────────────────────────────────────
|
|
147
|
-
|
|
148
|
-
describe("Pool decorator", () => {
|
|
149
|
-
it("wraps the named method and exposes signals", async () => {
|
|
150
|
-
const { ctx, run } = fieldCtx("taskProcess");
|
|
151
|
-
Pool("process", 2)(undefined, ctx);
|
|
152
|
-
|
|
153
|
-
const instance: Record<string, unknown> = {
|
|
154
|
-
process: async (x: unknown) => (x as number) + 1,
|
|
155
|
-
};
|
|
156
|
-
run(instance);
|
|
157
|
-
|
|
158
|
-
const taskProcess = instance.taskProcess as {
|
|
159
|
-
(...args: unknown[]): Promise<number>;
|
|
160
|
-
loading: () => boolean;
|
|
161
|
-
active: () => number;
|
|
162
|
-
pending: () => number;
|
|
163
|
-
error: () => Error | null;
|
|
164
|
-
cancelAll: () => void;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
expect(typeof taskProcess).toBe("function");
|
|
168
|
-
expect(taskProcess.loading).toBeDefined();
|
|
169
|
-
expect(taskProcess.active).toBeDefined();
|
|
170
|
-
expect(taskProcess.pending).toBeDefined();
|
|
171
|
-
expect(taskProcess.error).toBeDefined();
|
|
172
|
-
expect(taskProcess.cancelAll).toBeDefined();
|
|
173
|
-
|
|
174
|
-
const result = await taskProcess(9);
|
|
175
|
-
expect(result).toBe(10);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("respects concurrency limit", async () => {
|
|
179
|
-
const { ctx, run } = fieldCtx("taskWork");
|
|
180
|
-
Pool("work", 2)(undefined, ctx);
|
|
181
|
-
|
|
182
|
-
const resolvers: Array<() => void> = [];
|
|
183
|
-
const instance: Record<string, unknown> = {
|
|
184
|
-
work: async () => new Promise<void>((r) => resolvers.push(r)),
|
|
185
|
-
};
|
|
186
|
-
run(instance);
|
|
187
|
-
|
|
188
|
-
const taskWork = instance.taskWork as { (): Promise<void>; active: () => number; pending: () => number };
|
|
189
|
-
const t1 = taskWork();
|
|
190
|
-
const t2 = taskWork();
|
|
191
|
-
taskWork();
|
|
192
|
-
|
|
193
|
-
expect(taskWork.active()).toBe(2);
|
|
194
|
-
expect(taskWork.pending()).toBe(1);
|
|
195
|
-
|
|
196
|
-
resolvers.forEach((r) => { r(); });
|
|
197
|
-
await Promise.all([t1, t2]);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("defaults concurrency to 1", async () => {
|
|
201
|
-
const { ctx, run } = fieldCtx("taskWork");
|
|
202
|
-
Pool("work")(undefined, ctx);
|
|
203
|
-
|
|
204
|
-
const resolvers: Array<() => void> = [];
|
|
205
|
-
const instance: Record<string, unknown> = {
|
|
206
|
-
work: async () => new Promise<void>((r) => resolvers.push(r)),
|
|
207
|
-
};
|
|
208
|
-
run(instance);
|
|
209
|
-
|
|
210
|
-
const taskWork = instance.taskWork as { (): Promise<void>; active: () => number; pending: () => number };
|
|
211
|
-
const t1 = taskWork();
|
|
212
|
-
const t2 = taskWork();
|
|
213
|
-
|
|
214
|
-
expect(taskWork.active()).toBe(1);
|
|
215
|
-
expect(taskWork.pending()).toBe(1);
|
|
216
|
-
|
|
217
|
-
resolvers[0]();
|
|
218
|
-
await t1;
|
|
219
|
-
resolvers[1]();
|
|
220
|
-
await t2;
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("cancelAll() cancels pending and active tasks", async () => {
|
|
224
|
-
const { ctx, run } = fieldCtx("taskWork");
|
|
225
|
-
Pool("work", 1)(undefined, ctx);
|
|
226
|
-
|
|
227
|
-
let resolveFirst!: () => void;
|
|
228
|
-
const instance: Record<string, unknown> = {
|
|
229
|
-
work: async (signal: AbortSignal, idx: unknown) => {
|
|
230
|
-
void signal;
|
|
231
|
-
if (idx === 0) await new Promise<void>((r) => { resolveFirst = r; });
|
|
232
|
-
},
|
|
233
|
-
};
|
|
234
|
-
run(instance);
|
|
235
|
-
|
|
236
|
-
const taskWork = instance.taskWork as {
|
|
237
|
-
(...args: unknown[]): Promise<void>;
|
|
238
|
-
active: () => number;
|
|
239
|
-
pending: () => number;
|
|
240
|
-
cancelAll: () => void;
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const t1 = taskWork(0);
|
|
244
|
-
const t2 = taskWork(1);
|
|
245
|
-
|
|
246
|
-
await Promise.resolve();
|
|
247
|
-
taskWork.cancelAll();
|
|
248
|
-
resolveFirst();
|
|
249
|
-
|
|
250
|
-
await Promise.all([t1, t2]);
|
|
251
|
-
|
|
252
|
-
expect(taskWork.active()).toBe(0);
|
|
253
|
-
expect(taskWork.pending()).toBe(0);
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
// ── QueueClearedError re-export ────────────────────────────────────────────────
|
|
258
|
-
|
|
259
|
-
describe("QueueClearedError", () => {
|
|
260
|
-
it("is exported from the package", () => {
|
|
261
|
-
expect(QueueClearedError).toBeDefined();
|
|
262
|
-
const e = new QueueClearedError();
|
|
263
|
-
expect(e).toBeInstanceOf(Error);
|
|
264
|
-
expect(e.name).toBe("QueueClearedError");
|
|
265
|
-
expect(e.message).toBe("Queue cleared");
|
|
266
|
-
});
|
|
267
|
-
});
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { pool } from "../pool";
|
|
4
|
-
|
|
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", () => {
|
|
19
|
-
it("executes a task and resolves with its result", async () => {
|
|
20
|
-
const p = pool(2, async (x: unknown) => (x as number) * 3);
|
|
21
|
-
expect(await p(4)).toBe(12);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("respects concurrency limit", async () => {
|
|
25
|
-
let active = 0;
|
|
26
|
-
let maxActive = 0;
|
|
27
|
-
const p = pool(2, async () => {
|
|
28
|
-
active++;
|
|
29
|
-
maxActive = Math.max(maxActive, active);
|
|
30
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
31
|
-
active--;
|
|
32
|
-
});
|
|
33
|
-
await Promise.all([p(), p(), p(), p()]);
|
|
34
|
-
expect(maxActive).toBeLessThanOrEqual(2);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("active() reflects currently running tasks", async () => {
|
|
38
|
-
const resolvers: Array<() => void> = [];
|
|
39
|
-
const p = pool(3, () => new Promise<void>((r) => resolvers.push(r)));
|
|
40
|
-
const tasks = [p(), p()];
|
|
41
|
-
expect(p.active()).toBe(2);
|
|
42
|
-
resolvers.forEach((r) => { r(); });
|
|
43
|
-
await Promise.all(tasks);
|
|
44
|
-
expect(p.active()).toBe(0);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("loading is true when active > 0", async () => {
|
|
48
|
-
let resolve!: () => void;
|
|
49
|
-
const p = pool(1, () => new Promise<void>((r) => { resolve = r; }));
|
|
50
|
-
const t = p();
|
|
51
|
-
expect(p.loading()).toBe(true);
|
|
52
|
-
resolve();
|
|
53
|
-
await t;
|
|
54
|
-
expect(p.loading()).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("captures errors in the error signal and resolves undefined", async () => {
|
|
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"); });
|
|
73
|
-
const result = await p();
|
|
74
|
-
expect(result).toBeUndefined();
|
|
75
|
-
expect(p.error()).toBeNull();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("pending count decrements as tasks start", async () => {
|
|
79
|
-
let resolveFirst!: () => void;
|
|
80
|
-
const p = pool(1, async (idx: unknown) => {
|
|
81
|
-
if (idx === 0) await new Promise<void>((r) => { resolveFirst = r; });
|
|
82
|
-
});
|
|
83
|
-
const t1 = p(0);
|
|
84
|
-
const t2 = p(1);
|
|
85
|
-
expect(p.pending()).toBe(1);
|
|
86
|
-
resolveFirst();
|
|
87
|
-
await t1;
|
|
88
|
-
await t2;
|
|
89
|
-
expect(p.pending()).toBe(0);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("concurrency=1 guarantees serial execution", async () => {
|
|
93
|
-
const order: number[] = [];
|
|
94
|
-
let resolveFirst!: () => void;
|
|
95
|
-
const p = pool(1, async (n: unknown) => {
|
|
96
|
-
if (n === 0) await new Promise<void>((r) => { resolveFirst = r; });
|
|
97
|
-
order.push(n as number);
|
|
98
|
-
});
|
|
99
|
-
const t1 = p(0);
|
|
100
|
-
const t2 = p(1);
|
|
101
|
-
expect(p.active()).toBe(1);
|
|
102
|
-
expect(p.pending()).toBe(1);
|
|
103
|
-
resolveFirst();
|
|
104
|
-
await t1;
|
|
105
|
-
await t2;
|
|
106
|
-
expect(order).toEqual([0, 1]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("error in one task does not block subsequent tasks", async () => {
|
|
110
|
-
const p = pool(1, async (n: unknown) => {
|
|
111
|
-
if (n === 0) throw new Error("task 0 failed");
|
|
112
|
-
return n as number;
|
|
113
|
-
});
|
|
114
|
-
expect(await p(0)).toBeUndefined();
|
|
115
|
-
expect(await p(1)).toBe(1);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("concurrency=0 — clamps to 1", async () => {
|
|
119
|
-
const fn = vi.fn(async () => "result");
|
|
120
|
-
const p = pool(0, fn);
|
|
121
|
-
expect(await p()).toBe("result");
|
|
122
|
-
expect(fn).toHaveBeenCalledOnce();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("cancelAll() when idle — no-op, no crash", () => {
|
|
126
|
-
const p = pool(2, async () => "ok");
|
|
127
|
-
expect(() => p.cancelAll()).not.toThrow();
|
|
128
|
-
expect(p.pending()).toBe(0);
|
|
129
|
-
expect(p.active()).toBe(0);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// ── With AbortSignal (opt-in via first param named "signal") ──────────────────
|
|
134
|
-
|
|
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
|
-
});
|
|
143
|
-
|
|
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));
|
|
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
|
-
});
|
|
165
|
-
|
|
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);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("cancelAll() resolves pending tasks as undefined", async () => {
|
|
177
|
-
let resolveFirst!: () => void;
|
|
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; });
|
|
181
|
-
});
|
|
182
|
-
const t1 = p(0);
|
|
183
|
-
const t2 = p(1);
|
|
184
|
-
const t3 = p(2);
|
|
185
|
-
await Promise.resolve();
|
|
186
|
-
p.cancelAll();
|
|
187
|
-
resolveFirst();
|
|
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();
|
|
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;
|
|
202
|
-
});
|
|
203
|
-
const stale = p("stale");
|
|
204
|
-
await Promise.resolve();
|
|
205
|
-
p.cancelAll();
|
|
206
|
-
resolve();
|
|
207
|
-
await stale;
|
|
208
|
-
expect(await p("fresh")).toBe("fresh");
|
|
209
|
-
expect(p.active()).toBe(0);
|
|
210
|
-
expect(p.error()).toBeNull();
|
|
211
|
-
});
|
|
212
|
-
});
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { queue, QueueClearedError } from "../queue";
|
|
4
|
-
|
|
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", () => {
|
|
19
|
-
it("executes the task and resolves the promise", async () => {
|
|
20
|
-
const q = queue(async (x: unknown) => (x as number) * 2);
|
|
21
|
-
expect(await q(5)).toBe(10);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("runs tasks sequentially (FIFO)", async () => {
|
|
25
|
-
const order: number[] = [];
|
|
26
|
-
const q = queue(async (n: unknown) => { order.push(n as number); return n as number; });
|
|
27
|
-
await Promise.all([q(1), q(2), q(3)]);
|
|
28
|
-
expect(order).toEqual([1, 2, 3]);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("loading is true while running, false when done", async () => {
|
|
32
|
-
let resolve!: () => void;
|
|
33
|
-
const q = queue(() => new Promise<void>((r) => { resolve = r; }));
|
|
34
|
-
const p = q();
|
|
35
|
-
expect(q.loading()).toBe(true);
|
|
36
|
-
resolve();
|
|
37
|
-
await p;
|
|
38
|
-
expect(q.loading()).toBe(false);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("pending decrements as tasks complete", async () => {
|
|
42
|
-
let resolveFirst!: () => void;
|
|
43
|
-
const q = queue((_: unknown, idx: unknown) =>
|
|
44
|
-
idx === 0
|
|
45
|
-
? new Promise<void>((r) => { resolveFirst = r; })
|
|
46
|
-
: Promise.resolve(),
|
|
47
|
-
);
|
|
48
|
-
const p1 = q(null, 0);
|
|
49
|
-
const p2 = q(null, 1);
|
|
50
|
-
expect(q.pending()).toBeGreaterThanOrEqual(0);
|
|
51
|
-
resolveFirst();
|
|
52
|
-
await p1;
|
|
53
|
-
await p2;
|
|
54
|
-
expect(q.pending()).toBe(0);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("captures errors in the error signal", async () => {
|
|
58
|
-
const q = queue(async () => { throw new Error("boom"); });
|
|
59
|
-
await expect(q()).rejects.toThrow("boom");
|
|
60
|
-
expect(q.error()?.message).toBe("boom");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("wraps non-Error throws in an Error", async () => {
|
|
64
|
-
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
|
65
|
-
const q = queue(async () => { throw "string error"; });
|
|
66
|
-
await expect(q()).rejects.toThrow("string error");
|
|
67
|
-
expect(q.error()?.message).toBe("string error");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("error in one task does not prevent subsequent tasks from running", async () => {
|
|
71
|
-
const q = queue(async (n: unknown) => {
|
|
72
|
-
if (n === 0) throw new Error("task 0 failed");
|
|
73
|
-
return n as number;
|
|
74
|
-
});
|
|
75
|
-
const p0 = q(0);
|
|
76
|
-
const p1 = q(1);
|
|
77
|
-
const p2 = q(2);
|
|
78
|
-
await expect(p0).rejects.toThrow("task 0 failed");
|
|
79
|
-
expect(await p1).toBe(1);
|
|
80
|
-
expect(await p2).toBe(2);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("clear() with pending tasks rejects their promises with QueueClearedError", async () => {
|
|
84
|
-
let resolveFirst!: () => void;
|
|
85
|
-
const q = queue((_: unknown, idx: unknown) =>
|
|
86
|
-
idx === 0
|
|
87
|
-
? new Promise<void>((r) => { resolveFirst = r; })
|
|
88
|
-
: Promise.resolve(),
|
|
89
|
-
);
|
|
90
|
-
q(null, 0);
|
|
91
|
-
const p1 = q(null, 1);
|
|
92
|
-
const p2 = q(null, 2);
|
|
93
|
-
q.clear();
|
|
94
|
-
await expect(p1).rejects.toBeInstanceOf(QueueClearedError);
|
|
95
|
-
await expect(p2).rejects.toBeInstanceOf(QueueClearedError);
|
|
96
|
-
resolveFirst();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("clear() then new enqueue() — queue works normally after being cleared", async () => {
|
|
100
|
-
let resolveFirst!: () => void;
|
|
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),
|
|
105
|
-
);
|
|
106
|
-
q(0, true);
|
|
107
|
-
const p1 = q(1, false);
|
|
108
|
-
q.clear();
|
|
109
|
-
await expect(p1).rejects.toBeInstanceOf(QueueClearedError);
|
|
110
|
-
resolveFirst();
|
|
111
|
-
expect(await q(42, false)).toBe(42);
|
|
112
|
-
expect(q.pending()).toBe(0);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("clear() with no pending tasks — no-op, no crash", () => {
|
|
116
|
-
const q = queue(async () => "ok");
|
|
117
|
-
expect(() => q.clear()).not.toThrow();
|
|
118
|
-
expect(q.pending()).toBe(0);
|
|
119
|
-
});
|
|
120
|
-
|
|
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));
|
|
126
|
-
expect(q.pending()).toBe(0);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// ── With AbortSignal (opt-in via first param named "signal") ──────────────────
|
|
131
|
-
|
|
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
|
-
});
|
|
140
|
-
|
|
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);
|
|
144
|
-
});
|
|
145
|
-
|
|
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; });
|
|
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
|
-
});
|
|
161
|
-
|
|
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
|
-
});
|
|
169
|
-
|
|
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);
|
|
196
|
-
});
|
|
197
|
-
});
|