@furystack/utils 8.1.9 → 8.2.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.
- package/CHANGELOG.md +66 -0
- package/esm/event-hub.d.ts +21 -0
- package/esm/event-hub.d.ts.map +1 -1
- package/esm/event-hub.js +48 -1
- package/esm/event-hub.js.map +1 -1
- package/esm/event-hub.spec.js +112 -0
- package/esm/event-hub.spec.js.map +1 -1
- package/esm/index.d.ts +1 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -0
- package/esm/index.js.map +1 -1
- package/esm/observable-value.d.ts +9 -0
- package/esm/observable-value.d.ts.map +1 -1
- package/esm/observable-value.js +13 -2
- package/esm/observable-value.js.map +1 -1
- package/esm/observable-value.spec.d.ts.map +1 -1
- package/esm/observable-value.spec.js +160 -76
- package/esm/observable-value.spec.js.map +1 -1
- package/esm/semaphore.d.ts +104 -0
- package/esm/semaphore.d.ts.map +1 -0
- package/esm/semaphore.js +185 -0
- package/esm/semaphore.js.map +1 -0
- package/esm/semaphore.spec.d.ts +2 -0
- package/esm/semaphore.spec.d.ts.map +1 -0
- package/esm/semaphore.spec.js +356 -0
- package/esm/semaphore.spec.js.map +1 -0
- package/package.json +2 -2
- package/src/event-hub.spec.ts +153 -0
- package/src/event-hub.ts +57 -1
- package/src/index.ts +1 -0
- package/src/observable-value.spec.ts +197 -81
- package/src/observable-value.ts +18 -2
- package/src/semaphore.spec.ts +467 -0
- package/src/semaphore.ts +237 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { sleepAsync } from './sleep-async.js';
|
|
3
|
+
import { using } from './using.js';
|
|
4
|
+
import { Semaphore, SemaphoreDisposedError } from './semaphore.js';
|
|
5
|
+
export const semaphoreTests = describe('Semaphore', () => {
|
|
6
|
+
it('should be constructed with a given concurrency limit', () => {
|
|
7
|
+
using(new Semaphore(3), (s) => {
|
|
8
|
+
expect(s).toBeInstanceOf(Semaphore);
|
|
9
|
+
expect(s.getMaxConcurrent()).toBe(3);
|
|
10
|
+
expect(s.pendingCount.getValue()).toBe(0);
|
|
11
|
+
expect(s.runningCount.getValue()).toBe(0);
|
|
12
|
+
expect(s.completedCount.getValue()).toBe(0);
|
|
13
|
+
expect(s.failedCount.getValue()).toBe(0);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
it('should execute a single task and return its result', async () => {
|
|
17
|
+
const s = new Semaphore(2);
|
|
18
|
+
const result = await s.execute(async () => 42);
|
|
19
|
+
expect(result).toBe(42);
|
|
20
|
+
expect(s.completedCount.getValue()).toBe(1);
|
|
21
|
+
expect(s.runningCount.getValue()).toBe(0);
|
|
22
|
+
s[Symbol.dispose]();
|
|
23
|
+
});
|
|
24
|
+
it('should execute up to N tasks concurrently and queue the rest', async () => {
|
|
25
|
+
const s = new Semaphore(2);
|
|
26
|
+
const running = [];
|
|
27
|
+
const resolvers = [];
|
|
28
|
+
const createTask = (name) => s.execute(async () => {
|
|
29
|
+
running.push(name);
|
|
30
|
+
await new Promise((resolve) => resolvers.push(resolve));
|
|
31
|
+
return name;
|
|
32
|
+
});
|
|
33
|
+
const p1 = createTask('a');
|
|
34
|
+
const p2 = createTask('b');
|
|
35
|
+
const p3 = createTask('c');
|
|
36
|
+
await sleepAsync(10);
|
|
37
|
+
expect(running).toEqual(['a', 'b']);
|
|
38
|
+
expect(s.runningCount.getValue()).toBe(2);
|
|
39
|
+
expect(s.pendingCount.getValue()).toBe(1);
|
|
40
|
+
resolvers[0]();
|
|
41
|
+
await p1;
|
|
42
|
+
await sleepAsync(10);
|
|
43
|
+
expect(running).toEqual(['a', 'b', 'c']);
|
|
44
|
+
expect(s.runningCount.getValue()).toBe(2);
|
|
45
|
+
expect(s.pendingCount.getValue()).toBe(0);
|
|
46
|
+
resolvers[1]();
|
|
47
|
+
resolvers[2]();
|
|
48
|
+
await Promise.all([p2, p3]);
|
|
49
|
+
expect(s.completedCount.getValue()).toBe(3);
|
|
50
|
+
expect(s.runningCount.getValue()).toBe(0);
|
|
51
|
+
s[Symbol.dispose]();
|
|
52
|
+
});
|
|
53
|
+
it('should propagate task rejection to the caller and continue processing', async () => {
|
|
54
|
+
const s = new Semaphore(1);
|
|
55
|
+
const taskError = new Error('task failed');
|
|
56
|
+
const p1 = s.execute(async () => {
|
|
57
|
+
throw taskError;
|
|
58
|
+
});
|
|
59
|
+
const p2 = s.execute(async () => 'ok');
|
|
60
|
+
await expect(p1).rejects.toThrow('task failed');
|
|
61
|
+
const result = await p2;
|
|
62
|
+
expect(result).toBe('ok');
|
|
63
|
+
expect(s.failedCount.getValue()).toBe(1);
|
|
64
|
+
expect(s.completedCount.getValue()).toBe(1);
|
|
65
|
+
s[Symbol.dispose]();
|
|
66
|
+
});
|
|
67
|
+
describe('ObservableValue counters', () => {
|
|
68
|
+
it('should update pendingCount and runningCount on transitions', async () => {
|
|
69
|
+
const s = new Semaphore(1);
|
|
70
|
+
const pendingChanges = [];
|
|
71
|
+
const runningChanges = [];
|
|
72
|
+
s.pendingCount.subscribe((v) => {
|
|
73
|
+
pendingChanges.push(v);
|
|
74
|
+
});
|
|
75
|
+
s.runningCount.subscribe((v) => {
|
|
76
|
+
runningChanges.push(v);
|
|
77
|
+
});
|
|
78
|
+
let resolve;
|
|
79
|
+
const p1 = s.execute(async () => {
|
|
80
|
+
await new Promise((r) => (resolve = r));
|
|
81
|
+
});
|
|
82
|
+
const p2 = s.execute(async () => 'done');
|
|
83
|
+
await sleepAsync(10);
|
|
84
|
+
expect(pendingChanges).toContain(1);
|
|
85
|
+
expect(runningChanges).toContain(1);
|
|
86
|
+
resolve();
|
|
87
|
+
await p1;
|
|
88
|
+
await sleepAsync(10);
|
|
89
|
+
await p2;
|
|
90
|
+
expect(s.pendingCount.getValue()).toBe(0);
|
|
91
|
+
expect(s.runningCount.getValue()).toBe(0);
|
|
92
|
+
expect(s.completedCount.getValue()).toBe(2);
|
|
93
|
+
s[Symbol.dispose]();
|
|
94
|
+
});
|
|
95
|
+
it('should update completedCount and failedCount correctly', async () => {
|
|
96
|
+
const s = new Semaphore(2);
|
|
97
|
+
await s.execute(async () => 'ok');
|
|
98
|
+
await s
|
|
99
|
+
.execute(async () => {
|
|
100
|
+
throw new Error('fail');
|
|
101
|
+
})
|
|
102
|
+
.catch(() => { });
|
|
103
|
+
expect(s.completedCount.getValue()).toBe(1);
|
|
104
|
+
expect(s.failedCount.getValue()).toBe(1);
|
|
105
|
+
s[Symbol.dispose]();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('AbortSignal support', () => {
|
|
109
|
+
it('should abort a pending task when the caller signal aborts', async () => {
|
|
110
|
+
const s = new Semaphore(1);
|
|
111
|
+
let resolve;
|
|
112
|
+
const p1 = s.execute(async () => {
|
|
113
|
+
await new Promise((r) => (resolve = r));
|
|
114
|
+
});
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
const p2 = s.execute(async () => 'should not run', { signal: controller.signal });
|
|
117
|
+
await sleepAsync(10);
|
|
118
|
+
expect(s.pendingCount.getValue()).toBe(1);
|
|
119
|
+
controller.abort(new Error('cancelled'));
|
|
120
|
+
await expect(p2).rejects.toThrow('cancelled');
|
|
121
|
+
expect(s.pendingCount.getValue()).toBe(0);
|
|
122
|
+
resolve();
|
|
123
|
+
await p1;
|
|
124
|
+
s[Symbol.dispose]();
|
|
125
|
+
});
|
|
126
|
+
it('should reject immediately if the caller signal is already aborted', async () => {
|
|
127
|
+
const s = new Semaphore(1);
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
controller.abort(new Error('pre-aborted'));
|
|
130
|
+
await expect(s.execute(async () => 'should not run', { signal: controller.signal })).rejects.toThrow('pre-aborted');
|
|
131
|
+
expect(s.pendingCount.getValue()).toBe(0);
|
|
132
|
+
expect(s.runningCount.getValue()).toBe(0);
|
|
133
|
+
s[Symbol.dispose]();
|
|
134
|
+
});
|
|
135
|
+
it('should clean up the caller signal listener when the task completes normally', async () => {
|
|
136
|
+
const s = new Semaphore(1);
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
const removeSpy = vi.spyOn(controller.signal, 'removeEventListener');
|
|
139
|
+
await s.execute(async () => 'done', { signal: controller.signal });
|
|
140
|
+
expect(removeSpy).toHaveBeenCalledWith('abort', expect.any(Function));
|
|
141
|
+
removeSpy.mockRestore();
|
|
142
|
+
s[Symbol.dispose]();
|
|
143
|
+
});
|
|
144
|
+
it('should abort the signal passed to a running task when the caller signal aborts', async () => {
|
|
145
|
+
const s = new Semaphore(1);
|
|
146
|
+
const signalAborted = vi.fn();
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
const p = s.execute(async ({ signal }) => {
|
|
149
|
+
signal.addEventListener('abort', signalAborted);
|
|
150
|
+
await new Promise((resolve) => {
|
|
151
|
+
signal.addEventListener('abort', () => resolve());
|
|
152
|
+
});
|
|
153
|
+
throw signal.reason;
|
|
154
|
+
}, { signal: controller.signal });
|
|
155
|
+
await sleepAsync(10);
|
|
156
|
+
expect(s.runningCount.getValue()).toBe(1);
|
|
157
|
+
controller.abort(new Error('stop'));
|
|
158
|
+
await expect(p).rejects.toThrow('stop');
|
|
159
|
+
expect(signalAborted).toBeCalledTimes(1);
|
|
160
|
+
s[Symbol.dispose]();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('EventHub events', () => {
|
|
164
|
+
it('should emit taskStarted when a task begins running', async () => {
|
|
165
|
+
const s = new Semaphore(1);
|
|
166
|
+
const listener = vi.fn();
|
|
167
|
+
s.subscribe('taskStarted', listener);
|
|
168
|
+
await s.execute(async () => 'done');
|
|
169
|
+
expect(listener).toBeCalledTimes(1);
|
|
170
|
+
s[Symbol.dispose]();
|
|
171
|
+
});
|
|
172
|
+
it('should emit taskCompleted when a task resolves', async () => {
|
|
173
|
+
const s = new Semaphore(1);
|
|
174
|
+
const listener = vi.fn();
|
|
175
|
+
s.subscribe('taskCompleted', listener);
|
|
176
|
+
await s.execute(async () => 'done');
|
|
177
|
+
expect(listener).toBeCalledTimes(1);
|
|
178
|
+
s[Symbol.dispose]();
|
|
179
|
+
});
|
|
180
|
+
it('should emit taskFailed with the error when a task rejects', async () => {
|
|
181
|
+
const s = new Semaphore(1);
|
|
182
|
+
const listener = vi.fn();
|
|
183
|
+
s.subscribe('taskFailed', listener);
|
|
184
|
+
const taskError = new Error('boom');
|
|
185
|
+
await s
|
|
186
|
+
.execute(async () => {
|
|
187
|
+
throw taskError;
|
|
188
|
+
})
|
|
189
|
+
.catch(() => { });
|
|
190
|
+
expect(listener).toBeCalledTimes(1);
|
|
191
|
+
expect(listener).toBeCalledWith({ error: taskError });
|
|
192
|
+
s[Symbol.dispose]();
|
|
193
|
+
});
|
|
194
|
+
it('should emit events in correct order for queued tasks', async () => {
|
|
195
|
+
const s = new Semaphore(1);
|
|
196
|
+
const events = [];
|
|
197
|
+
s.subscribe('taskStarted', () => {
|
|
198
|
+
events.push('started');
|
|
199
|
+
});
|
|
200
|
+
s.subscribe('taskCompleted', () => {
|
|
201
|
+
events.push('completed');
|
|
202
|
+
});
|
|
203
|
+
await Promise.all([s.execute(async () => 'a'), s.execute(async () => 'b')]);
|
|
204
|
+
await sleepAsync(10);
|
|
205
|
+
expect(events).toEqual(['started', 'completed', 'started', 'completed']);
|
|
206
|
+
s[Symbol.dispose]();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
describe('setMaxConcurrent', () => {
|
|
210
|
+
it('should return the updated value from getMaxConcurrent', () => {
|
|
211
|
+
const s = new Semaphore(2);
|
|
212
|
+
s.setMaxConcurrent(5);
|
|
213
|
+
expect(s.getMaxConcurrent()).toBe(5);
|
|
214
|
+
s[Symbol.dispose]();
|
|
215
|
+
});
|
|
216
|
+
it('should throw when given a non-positive integer', () => {
|
|
217
|
+
const s = new Semaphore(2);
|
|
218
|
+
expect(() => s.setMaxConcurrent(0)).toThrow('maxConcurrent must be a positive integer');
|
|
219
|
+
expect(() => s.setMaxConcurrent(-1)).toThrow('maxConcurrent must be a positive integer');
|
|
220
|
+
expect(() => s.setMaxConcurrent(1.5)).toThrow('maxConcurrent must be a positive integer');
|
|
221
|
+
s[Symbol.dispose]();
|
|
222
|
+
});
|
|
223
|
+
it('should immediately start queued tasks when increased', async () => {
|
|
224
|
+
const s = new Semaphore(1);
|
|
225
|
+
const running = [];
|
|
226
|
+
const resolvers = [];
|
|
227
|
+
const createTask = (name) => s.execute(async () => {
|
|
228
|
+
running.push(name);
|
|
229
|
+
await new Promise((resolve) => resolvers.push(resolve));
|
|
230
|
+
return name;
|
|
231
|
+
});
|
|
232
|
+
const p1 = createTask('a');
|
|
233
|
+
const p2 = createTask('b');
|
|
234
|
+
const p3 = createTask('c');
|
|
235
|
+
await sleepAsync(10);
|
|
236
|
+
expect(running).toEqual(['a']);
|
|
237
|
+
expect(s.runningCount.getValue()).toBe(1);
|
|
238
|
+
expect(s.pendingCount.getValue()).toBe(2);
|
|
239
|
+
s.setMaxConcurrent(3);
|
|
240
|
+
await sleepAsync(10);
|
|
241
|
+
expect(running).toEqual(['a', 'b', 'c']);
|
|
242
|
+
expect(s.runningCount.getValue()).toBe(3);
|
|
243
|
+
expect(s.pendingCount.getValue()).toBe(0);
|
|
244
|
+
resolvers.forEach((r) => r());
|
|
245
|
+
await Promise.all([p1, p2, p3]);
|
|
246
|
+
s[Symbol.dispose]();
|
|
247
|
+
});
|
|
248
|
+
it('should not abort running tasks when decreased', async () => {
|
|
249
|
+
const s = new Semaphore(3);
|
|
250
|
+
const resolvers = [];
|
|
251
|
+
const createTask = () => s.execute(async () => {
|
|
252
|
+
await new Promise((resolve) => resolvers.push(resolve));
|
|
253
|
+
});
|
|
254
|
+
const p1 = createTask();
|
|
255
|
+
const p2 = createTask();
|
|
256
|
+
const p3 = createTask();
|
|
257
|
+
await sleepAsync(10);
|
|
258
|
+
expect(s.runningCount.getValue()).toBe(3);
|
|
259
|
+
s.setMaxConcurrent(1);
|
|
260
|
+
expect(s.runningCount.getValue()).toBe(3);
|
|
261
|
+
resolvers.forEach((r) => r());
|
|
262
|
+
await Promise.all([p1, p2, p3]);
|
|
263
|
+
expect(s.completedCount.getValue()).toBe(3);
|
|
264
|
+
s[Symbol.dispose]();
|
|
265
|
+
});
|
|
266
|
+
it('should not start new tasks until running count drops below new lower limit', async () => {
|
|
267
|
+
const s = new Semaphore(2);
|
|
268
|
+
const running = [];
|
|
269
|
+
const resolvers = [];
|
|
270
|
+
const createTask = (name) => s.execute(async () => {
|
|
271
|
+
running.push(name);
|
|
272
|
+
await new Promise((resolve) => resolvers.push(resolve));
|
|
273
|
+
return name;
|
|
274
|
+
});
|
|
275
|
+
const p1 = createTask('a');
|
|
276
|
+
const p2 = createTask('b');
|
|
277
|
+
const p3 = createTask('c');
|
|
278
|
+
await sleepAsync(10);
|
|
279
|
+
expect(running).toEqual(['a', 'b']);
|
|
280
|
+
expect(s.pendingCount.getValue()).toBe(1);
|
|
281
|
+
s.setMaxConcurrent(1);
|
|
282
|
+
resolvers[0]();
|
|
283
|
+
await p1;
|
|
284
|
+
await sleepAsync(10);
|
|
285
|
+
expect(running).toEqual(['a', 'b']);
|
|
286
|
+
expect(s.pendingCount.getValue()).toBe(1);
|
|
287
|
+
resolvers[1]();
|
|
288
|
+
await p2;
|
|
289
|
+
await sleepAsync(10);
|
|
290
|
+
expect(running).toEqual(['a', 'b', 'c']);
|
|
291
|
+
expect(s.pendingCount.getValue()).toBe(0);
|
|
292
|
+
resolvers[2]();
|
|
293
|
+
await p3;
|
|
294
|
+
s[Symbol.dispose]();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
describe('Disposal', () => {
|
|
298
|
+
it('should reject all pending tasks with SemaphoreDisposedError', async () => {
|
|
299
|
+
const s = new Semaphore(1);
|
|
300
|
+
let resolve;
|
|
301
|
+
const p1 = s.execute(async () => {
|
|
302
|
+
await new Promise((r) => (resolve = r));
|
|
303
|
+
});
|
|
304
|
+
const p2 = s.execute(async () => 'pending1');
|
|
305
|
+
const p3 = s.execute(async () => 'pending2');
|
|
306
|
+
await sleepAsync(10);
|
|
307
|
+
expect(s.pendingCount.getValue()).toBe(2);
|
|
308
|
+
s[Symbol.dispose]();
|
|
309
|
+
await expect(p2).rejects.toThrow('Semaphore already disposed');
|
|
310
|
+
await expect(p3).rejects.toThrow('Semaphore already disposed');
|
|
311
|
+
await expect(p2).rejects.toBeInstanceOf(SemaphoreDisposedError);
|
|
312
|
+
await expect(p3).rejects.toBeInstanceOf(SemaphoreDisposedError);
|
|
313
|
+
resolve();
|
|
314
|
+
await p1;
|
|
315
|
+
});
|
|
316
|
+
it('should abort signals of running tasks on disposal', async () => {
|
|
317
|
+
const s = new Semaphore(1);
|
|
318
|
+
const signalAborted = vi.fn();
|
|
319
|
+
const p = s.execute(async ({ signal }) => {
|
|
320
|
+
signal.addEventListener('abort', signalAborted);
|
|
321
|
+
await new Promise((resolve) => {
|
|
322
|
+
signal.addEventListener('abort', () => resolve());
|
|
323
|
+
});
|
|
324
|
+
throw signal.reason;
|
|
325
|
+
});
|
|
326
|
+
await sleepAsync(10);
|
|
327
|
+
expect(s.runningCount.getValue()).toBe(1);
|
|
328
|
+
s[Symbol.dispose]();
|
|
329
|
+
await expect(p).rejects.toBeInstanceOf(SemaphoreDisposedError);
|
|
330
|
+
expect(signalAborted).toBeCalledTimes(1);
|
|
331
|
+
});
|
|
332
|
+
it('should throw SemaphoreDisposedError when calling execute() after disposal', () => {
|
|
333
|
+
const s = new Semaphore(1);
|
|
334
|
+
s[Symbol.dispose]();
|
|
335
|
+
expect(() => s.execute(async () => 'too late')).toThrow('Semaphore already disposed');
|
|
336
|
+
expect(() => s.execute(async () => 'too late')).toThrow(SemaphoreDisposedError);
|
|
337
|
+
});
|
|
338
|
+
it('should dispose all ObservableValues', () => {
|
|
339
|
+
const s = new Semaphore(1);
|
|
340
|
+
s[Symbol.dispose]();
|
|
341
|
+
expect(s.pendingCount.isDisposed).toBe(true);
|
|
342
|
+
expect(s.runningCount.isDisposed).toBe(true);
|
|
343
|
+
expect(s.completedCount.isDisposed).toBe(true);
|
|
344
|
+
expect(s.failedCount.isDisposed).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
it('should clear event listeners via super', () => {
|
|
347
|
+
const s = new Semaphore(1);
|
|
348
|
+
const listener = vi.fn();
|
|
349
|
+
s.subscribe('taskStarted', listener);
|
|
350
|
+
s[Symbol.dispose]();
|
|
351
|
+
s.emit('taskStarted', undefined);
|
|
352
|
+
expect(listener).not.toBeCalled();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
//# sourceMappingURL=semaphore.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semaphore.spec.js","sourceRoot":"","sources":["../src/semaphore.spec.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@furystack/utils",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.2.0",
|
|
4
4
|
"description": "Utility functions and helpers for FuryStack including observables, events, and disposables",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"homepage": "https://github.com/furystack/furystack",
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^5.9.3",
|
|
42
|
-
"vitest": "^4.0.
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=22.0.0"
|
package/src/event-hub.spec.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import type { ListenerErrorPayload } from './event-hub.js'
|
|
2
3
|
import { EventHub } from './event-hub.js'
|
|
4
|
+
import { sleepAsync } from './sleep-async.js'
|
|
3
5
|
|
|
4
6
|
describe('EventHub', () => {
|
|
5
7
|
it('Should fail on type errors', () => {
|
|
@@ -97,4 +99,155 @@ describe('EventHub', () => {
|
|
|
97
99
|
hub.emit('test', 'test')
|
|
98
100
|
expect(listener).not.toBeCalled()
|
|
99
101
|
})
|
|
102
|
+
|
|
103
|
+
describe('Error resilience', () => {
|
|
104
|
+
it('should catch sync throws from listeners and still notify other listeners', () => {
|
|
105
|
+
const hub = new EventHub<{ test: number }>()
|
|
106
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
107
|
+
|
|
108
|
+
const throwingListener = () => {
|
|
109
|
+
throw new Error('listener error')
|
|
110
|
+
}
|
|
111
|
+
const goodListener = vi.fn()
|
|
112
|
+
|
|
113
|
+
hub.addListener('test', throwingListener)
|
|
114
|
+
hub.addListener('test', goodListener)
|
|
115
|
+
|
|
116
|
+
hub.emit('test', 42)
|
|
117
|
+
|
|
118
|
+
expect(goodListener).toBeCalledWith(42)
|
|
119
|
+
expect(goodListener).toBeCalledTimes(1)
|
|
120
|
+
expect(consoleErrorSpy).toHaveBeenCalled()
|
|
121
|
+
|
|
122
|
+
consoleErrorSpy.mockRestore()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should catch async rejections from listeners', async () => {
|
|
126
|
+
const hub = new EventHub<{ test: number }>()
|
|
127
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
128
|
+
|
|
129
|
+
const rejectingListener = async () => {
|
|
130
|
+
throw new Error('async listener error')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
hub.addListener('test', rejectingListener)
|
|
134
|
+
hub.emit('test', 42)
|
|
135
|
+
|
|
136
|
+
await sleepAsync(10)
|
|
137
|
+
|
|
138
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Unhandled EventHub listener error', {
|
|
139
|
+
event: 'test',
|
|
140
|
+
error: expect.any(Error) as Error,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
consoleErrorSpy.mockRestore()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should route sync errors to onListenerError listeners when registered', () => {
|
|
147
|
+
const hub = new EventHub<{ test: number; onListenerError: ListenerErrorPayload }>()
|
|
148
|
+
const errorHandler = vi.fn()
|
|
149
|
+
|
|
150
|
+
hub.addListener('onListenerError', errorHandler)
|
|
151
|
+
hub.addListener('test', () => {
|
|
152
|
+
throw new Error('boom')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
hub.emit('test', 1)
|
|
156
|
+
|
|
157
|
+
expect(errorHandler).toBeCalledTimes(1)
|
|
158
|
+
expect(errorHandler).toBeCalledWith({
|
|
159
|
+
event: 'test',
|
|
160
|
+
error: expect.any(Error) as Error,
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should route async rejections to onListenerError listeners when registered', async () => {
|
|
165
|
+
const hub = new EventHub<{ test: number; onListenerError: ListenerErrorPayload }>()
|
|
166
|
+
const errorHandler = vi.fn()
|
|
167
|
+
|
|
168
|
+
hub.addListener('onListenerError', errorHandler)
|
|
169
|
+
hub.addListener('test', async () => {
|
|
170
|
+
throw new Error('async boom')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
hub.emit('test', 1)
|
|
174
|
+
await sleepAsync(10)
|
|
175
|
+
|
|
176
|
+
expect(errorHandler).toBeCalledTimes(1)
|
|
177
|
+
expect(errorHandler).toBeCalledWith({
|
|
178
|
+
event: 'test',
|
|
179
|
+
error: expect.any(Error) as Error,
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should fall back to console.error if onListenerError handler returns a rejected promise', async () => {
|
|
184
|
+
const hub = new EventHub<{ test: number; onListenerError: ListenerErrorPayload }>()
|
|
185
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
186
|
+
|
|
187
|
+
hub.addListener('onListenerError', async () => {
|
|
188
|
+
throw new Error('async error handler fails')
|
|
189
|
+
})
|
|
190
|
+
hub.addListener('test', () => {
|
|
191
|
+
throw new Error('original error')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
hub.emit('test', 1)
|
|
195
|
+
await sleepAsync(10)
|
|
196
|
+
|
|
197
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error in onListenerError handler', expect.any(Error))
|
|
198
|
+
|
|
199
|
+
consoleErrorSpy.mockRestore()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('should fall back to console.error if onListenerError handler itself throws', () => {
|
|
203
|
+
const hub = new EventHub<{ test: number; onListenerError: ListenerErrorPayload }>()
|
|
204
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
205
|
+
|
|
206
|
+
hub.addListener('onListenerError', () => {
|
|
207
|
+
throw new Error('error handler also fails')
|
|
208
|
+
})
|
|
209
|
+
hub.addListener('test', () => {
|
|
210
|
+
throw new Error('original error')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
hub.emit('test', 1)
|
|
214
|
+
|
|
215
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error in onListenerError handler', expect.any(Error))
|
|
216
|
+
|
|
217
|
+
consoleErrorSpy.mockRestore()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should go straight to console.error when onListenerError event itself has a failing listener', () => {
|
|
221
|
+
const hub = new EventHub<{ onListenerError: ListenerErrorPayload }>()
|
|
222
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
223
|
+
|
|
224
|
+
hub.addListener('onListenerError', () => {
|
|
225
|
+
throw new Error('meta error')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
hub.emit('onListenerError', { event: 'test', error: new Error('original') })
|
|
229
|
+
|
|
230
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error in onListenerError handler', expect.any(Error))
|
|
231
|
+
|
|
232
|
+
consoleErrorSpy.mockRestore()
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('should fall back to console.error when no onListenerError listeners are registered', () => {
|
|
236
|
+
const hub = new EventHub<{ test: number }>()
|
|
237
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
238
|
+
|
|
239
|
+
hub.addListener('test', () => {
|
|
240
|
+
throw new Error('no handler')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
hub.emit('test', 1)
|
|
244
|
+
|
|
245
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Unhandled EventHub listener error', {
|
|
246
|
+
event: 'test',
|
|
247
|
+
error: expect.any(Error) as Error,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
consoleErrorSpy.mockRestore()
|
|
251
|
+
})
|
|
252
|
+
})
|
|
100
253
|
})
|
package/src/event-hub.ts
CHANGED
|
@@ -2,10 +2,25 @@ type ListenerFunction<EventTypeMap extends object, T extends keyof EventTypeMap>
|
|
|
2
2
|
arg: EventTypeMap[T],
|
|
3
3
|
) => void | PromiseLike<void>
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Payload emitted when a listener throws or rejects during {@link EventHub.emit}.
|
|
7
|
+
* Subscribe to the `onListenerError` event to receive these.
|
|
8
|
+
*/
|
|
9
|
+
export type ListenerErrorPayload = {
|
|
10
|
+
/** The event name that was being emitted when the error occurred */
|
|
11
|
+
event: string | number | symbol
|
|
12
|
+
/** The error thrown or rejected by the listener */
|
|
13
|
+
error: unknown
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
/**
|
|
6
17
|
* A typed event emitter that provides type-safe event subscription and emission.
|
|
7
18
|
* Use this to create strongly-typed pub/sub patterns in your application.
|
|
8
19
|
*
|
|
20
|
+
* Listener errors (sync throws and async rejections) are caught automatically.
|
|
21
|
+
* If `onListenerError` listeners are registered, errors are routed there.
|
|
22
|
+
* Otherwise, they are logged via `console.error`.
|
|
23
|
+
*
|
|
9
24
|
* @typeParam EventTypeMap - An object type where keys are event names and values are event payload types
|
|
10
25
|
* @example
|
|
11
26
|
* ```ts
|
|
@@ -13,6 +28,7 @@ type ListenerFunction<EventTypeMap extends object, T extends keyof EventTypeMap>
|
|
|
13
28
|
* userLoggedIn: { userId: string }
|
|
14
29
|
* userLoggedOut: { userId: string }
|
|
15
30
|
* dataUpdated: { items: string[] }
|
|
31
|
+
* onListenerError: ListenerErrorPayload
|
|
16
32
|
* }
|
|
17
33
|
*
|
|
18
34
|
* const hub = new EventHub<MyEvents>()
|
|
@@ -22,6 +38,11 @@ type ListenerFunction<EventTypeMap extends object, T extends keyof EventTypeMap>
|
|
|
22
38
|
* console.log('User logged in:', event.userId)
|
|
23
39
|
* })
|
|
24
40
|
*
|
|
41
|
+
* // Handle listener errors
|
|
42
|
+
* hub.subscribe('onListenerError', ({ event, error }) => {
|
|
43
|
+
* console.error(`Listener for "${String(event)}" failed:`, error)
|
|
44
|
+
* })
|
|
45
|
+
*
|
|
25
46
|
* // Emit events
|
|
26
47
|
* hub.emit('userLoggedIn', { userId: '123' })
|
|
27
48
|
*
|
|
@@ -59,9 +80,44 @@ export class EventHub<EventTypeMap extends object> implements Disposable {
|
|
|
59
80
|
return { [Symbol.dispose]: () => this.removeListener(event, listener) }
|
|
60
81
|
}
|
|
61
82
|
|
|
83
|
+
private handleListenerError(event: keyof EventTypeMap, error: unknown) {
|
|
84
|
+
if (event === 'onListenerError') {
|
|
85
|
+
console.error('Error in onListenerError handler', error)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
const errorListeners = this.listeners.get('onListenerError' as keyof EventTypeMap)
|
|
89
|
+
if (errorListeners?.size) {
|
|
90
|
+
for (const errorListener of errorListeners) {
|
|
91
|
+
try {
|
|
92
|
+
const result = errorListener({ event, error } as EventTypeMap[keyof EventTypeMap])
|
|
93
|
+
if (result && typeof result.then === 'function') {
|
|
94
|
+
result.then(undefined, (err: unknown) => {
|
|
95
|
+
console.error('Error in onListenerError handler', err)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error('Error in onListenerError handler', err)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
console.error('Unhandled EventHub listener error', { event, error })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
62
107
|
public emit<TEvent extends keyof EventTypeMap>(event: TEvent, arg: EventTypeMap[TEvent]) {
|
|
63
108
|
if (this.listeners.has(event)) {
|
|
64
|
-
this.listeners.get(event)!.forEach((listener) =>
|
|
109
|
+
this.listeners.get(event)!.forEach((listener) => {
|
|
110
|
+
try {
|
|
111
|
+
const result = listener(arg)
|
|
112
|
+
if (result && typeof result.then === 'function') {
|
|
113
|
+
result.then(undefined, (error: unknown) => {
|
|
114
|
+
this.handleListenerError(event, error)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
this.handleListenerError(event, error)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
65
121
|
}
|
|
66
122
|
}
|
|
67
123
|
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './is-async-disposable.js'
|
|
|
5
5
|
export * from './is-disposable.js'
|
|
6
6
|
export * from './observable-value.js'
|
|
7
7
|
export * from './path-helper.js'
|
|
8
|
+
export * from './semaphore.js'
|
|
8
9
|
export * from './sleep-async.js'
|
|
9
10
|
export * from './sort-by.js'
|
|
10
11
|
export * from './tuple.js'
|