@furystack/utils 8.2.0 → 8.2.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.
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it, vi } from 'vitest'
2
2
  import { sleepAsync } from './sleep-async.js'
3
3
  import { using } from './using.js'
4
+ import { usingAsync } from './using-async.js'
4
5
  import { Semaphore, SemaphoreDisposedError } from './semaphore.js'
5
6
 
6
7
  export const semaphoreTests = describe('Semaphore', () => {
@@ -16,375 +17,374 @@ export const semaphoreTests = describe('Semaphore', () => {
16
17
  })
17
18
 
18
19
  it('should execute a single task and return its result', async () => {
19
- const s = new Semaphore(2)
20
- const result = await s.execute(async () => 42)
21
- expect(result).toBe(42)
22
- expect(s.completedCount.getValue()).toBe(1)
23
- expect(s.runningCount.getValue()).toBe(0)
24
- s[Symbol.dispose]()
20
+ await usingAsync(new Semaphore(2), async (s) => {
21
+ const result = await s.execute(async () => 42)
22
+ expect(result).toBe(42)
23
+ expect(s.completedCount.getValue()).toBe(1)
24
+ expect(s.runningCount.getValue()).toBe(0)
25
+ })
25
26
  })
26
27
 
27
28
  it('should execute up to N tasks concurrently and queue the rest', async () => {
28
- const s = new Semaphore(2)
29
- const running: string[] = []
30
- const resolvers: Array<() => void> = []
31
-
32
- const createTask = (name: string) =>
33
- s.execute(async () => {
34
- running.push(name)
35
- await new Promise<void>((resolve) => resolvers.push(resolve))
36
- return name
37
- })
29
+ await usingAsync(new Semaphore(2), async (s) => {
30
+ const running: string[] = []
31
+ const resolvers: Array<() => void> = []
38
32
 
39
- const p1 = createTask('a')
40
- const p2 = createTask('b')
41
- const p3 = createTask('c')
33
+ const createTask = (name: string) =>
34
+ s.execute(async () => {
35
+ running.push(name)
36
+ await new Promise<void>((resolve) => resolvers.push(resolve))
37
+ return name
38
+ })
42
39
 
43
- await sleepAsync(10)
40
+ const p1 = createTask('a')
41
+ const p2 = createTask('b')
42
+ const p3 = createTask('c')
44
43
 
45
- expect(running).toEqual(['a', 'b'])
46
- expect(s.runningCount.getValue()).toBe(2)
47
- expect(s.pendingCount.getValue()).toBe(1)
44
+ await sleepAsync(10)
48
45
 
49
- resolvers[0]()
50
- await p1
46
+ expect(running).toEqual(['a', 'b'])
47
+ expect(s.runningCount.getValue()).toBe(2)
48
+ expect(s.pendingCount.getValue()).toBe(1)
51
49
 
52
- await sleepAsync(10)
50
+ resolvers[0]()
51
+ await p1
53
52
 
54
- expect(running).toEqual(['a', 'b', 'c'])
55
- expect(s.runningCount.getValue()).toBe(2)
56
- expect(s.pendingCount.getValue()).toBe(0)
53
+ await sleepAsync(10)
57
54
 
58
- resolvers[1]()
59
- resolvers[2]()
60
- await Promise.all([p2, p3])
55
+ expect(running).toEqual(['a', 'b', 'c'])
56
+ expect(s.runningCount.getValue()).toBe(2)
57
+ expect(s.pendingCount.getValue()).toBe(0)
61
58
 
62
- expect(s.completedCount.getValue()).toBe(3)
63
- expect(s.runningCount.getValue()).toBe(0)
64
- s[Symbol.dispose]()
59
+ resolvers[1]()
60
+ resolvers[2]()
61
+ await Promise.all([p2, p3])
62
+
63
+ expect(s.completedCount.getValue()).toBe(3)
64
+ expect(s.runningCount.getValue()).toBe(0)
65
+ })
65
66
  })
66
67
 
67
68
  it('should propagate task rejection to the caller and continue processing', async () => {
68
- const s = new Semaphore(1)
69
- const taskError = new Error('task failed')
69
+ await usingAsync(new Semaphore(1), async (s) => {
70
+ const taskError = new Error('task failed')
70
71
 
71
- const p1 = s.execute(async () => {
72
- throw taskError
73
- })
74
- const p2 = s.execute(async () => 'ok')
72
+ const p1 = s.execute(async () => {
73
+ throw taskError
74
+ })
75
+ const p2 = s.execute(async () => 'ok')
75
76
 
76
- await expect(p1).rejects.toThrow('task failed')
77
+ await expect(p1).rejects.toThrow('task failed')
77
78
 
78
- const result = await p2
79
- expect(result).toBe('ok')
80
- expect(s.failedCount.getValue()).toBe(1)
81
- expect(s.completedCount.getValue()).toBe(1)
82
- s[Symbol.dispose]()
79
+ const result = await p2
80
+ expect(result).toBe('ok')
81
+ expect(s.failedCount.getValue()).toBe(1)
82
+ expect(s.completedCount.getValue()).toBe(1)
83
+ })
83
84
  })
84
85
 
85
86
  describe('ObservableValue counters', () => {
86
87
  it('should update pendingCount and runningCount on transitions', async () => {
87
- const s = new Semaphore(1)
88
- const pendingChanges: number[] = []
89
- const runningChanges: number[] = []
88
+ await usingAsync(new Semaphore(1), async (s) => {
89
+ const pendingChanges: number[] = []
90
+ const runningChanges: number[] = []
90
91
 
91
- s.pendingCount.subscribe((v) => {
92
- pendingChanges.push(v)
93
- })
94
- s.runningCount.subscribe((v) => {
95
- runningChanges.push(v)
96
- })
92
+ s.pendingCount.subscribe((v) => {
93
+ pendingChanges.push(v)
94
+ })
95
+ s.runningCount.subscribe((v) => {
96
+ runningChanges.push(v)
97
+ })
97
98
 
98
- let resolve!: () => void
99
- const p1 = s.execute(async () => {
100
- await new Promise<void>((r) => (resolve = r))
101
- })
99
+ let resolve!: () => void
100
+ const p1 = s.execute(async () => {
101
+ await new Promise<void>((r) => (resolve = r))
102
+ })
102
103
 
103
- const p2 = s.execute(async () => 'done')
104
+ const p2 = s.execute(async () => 'done')
104
105
 
105
- await sleepAsync(10)
106
+ await sleepAsync(10)
106
107
 
107
- expect(pendingChanges).toContain(1)
108
- expect(runningChanges).toContain(1)
108
+ expect(pendingChanges).toContain(1)
109
+ expect(runningChanges).toContain(1)
109
110
 
110
- resolve()
111
- await p1
112
- await sleepAsync(10)
113
- await p2
111
+ resolve()
112
+ await p1
113
+ await sleepAsync(10)
114
+ await p2
114
115
 
115
- expect(s.pendingCount.getValue()).toBe(0)
116
- expect(s.runningCount.getValue()).toBe(0)
117
- expect(s.completedCount.getValue()).toBe(2)
118
- s[Symbol.dispose]()
116
+ expect(s.pendingCount.getValue()).toBe(0)
117
+ expect(s.runningCount.getValue()).toBe(0)
118
+ expect(s.completedCount.getValue()).toBe(2)
119
+ })
119
120
  })
120
121
 
121
122
  it('should update completedCount and failedCount correctly', async () => {
122
- const s = new Semaphore(2)
123
-
124
- await s.execute(async () => 'ok')
125
- await s
126
- .execute(async () => {
127
- throw new Error('fail')
128
- })
129
- .catch(() => {})
123
+ await usingAsync(new Semaphore(2), async (s) => {
124
+ await s.execute(async () => 'ok')
125
+ await s
126
+ .execute(async () => {
127
+ throw new Error('fail')
128
+ })
129
+ .catch(() => {})
130
130
 
131
- expect(s.completedCount.getValue()).toBe(1)
132
- expect(s.failedCount.getValue()).toBe(1)
133
- s[Symbol.dispose]()
131
+ expect(s.completedCount.getValue()).toBe(1)
132
+ expect(s.failedCount.getValue()).toBe(1)
133
+ })
134
134
  })
135
135
  })
136
136
 
137
137
  describe('AbortSignal support', () => {
138
138
  it('should abort a pending task when the caller signal aborts', async () => {
139
- const s = new Semaphore(1)
140
- let resolve!: () => void
139
+ await usingAsync(new Semaphore(1), async (s) => {
140
+ let resolve!: () => void
141
141
 
142
- const p1 = s.execute(async () => {
143
- await new Promise<void>((r) => (resolve = r))
144
- })
142
+ const p1 = s.execute(async () => {
143
+ await new Promise<void>((r) => (resolve = r))
144
+ })
145
145
 
146
- const controller = new AbortController()
147
- const p2 = s.execute(async () => 'should not run', { signal: controller.signal })
146
+ const controller = new AbortController()
147
+ const p2 = s.execute(async () => 'should not run', { signal: controller.signal })
148
148
 
149
- await sleepAsync(10)
150
- expect(s.pendingCount.getValue()).toBe(1)
149
+ await sleepAsync(10)
150
+ expect(s.pendingCount.getValue()).toBe(1)
151
151
 
152
- controller.abort(new Error('cancelled'))
153
- await expect(p2).rejects.toThrow('cancelled')
154
- expect(s.pendingCount.getValue()).toBe(0)
152
+ controller.abort(new Error('cancelled'))
153
+ await expect(p2).rejects.toThrow('cancelled')
154
+ expect(s.pendingCount.getValue()).toBe(0)
155
155
 
156
- resolve()
157
- await p1
158
- s[Symbol.dispose]()
156
+ resolve()
157
+ await p1
158
+ })
159
159
  })
160
160
 
161
161
  it('should reject immediately if the caller signal is already aborted', async () => {
162
- const s = new Semaphore(1)
163
- const controller = new AbortController()
164
- controller.abort(new Error('pre-aborted'))
162
+ await usingAsync(new Semaphore(1), async (s) => {
163
+ const controller = new AbortController()
164
+ controller.abort(new Error('pre-aborted'))
165
165
 
166
- await expect(s.execute(async () => 'should not run', { signal: controller.signal })).rejects.toThrow(
167
- 'pre-aborted',
168
- )
166
+ await expect(s.execute(async () => 'should not run', { signal: controller.signal })).rejects.toThrow(
167
+ 'pre-aborted',
168
+ )
169
169
 
170
- expect(s.pendingCount.getValue()).toBe(0)
171
- expect(s.runningCount.getValue()).toBe(0)
172
- s[Symbol.dispose]()
170
+ expect(s.pendingCount.getValue()).toBe(0)
171
+ expect(s.runningCount.getValue()).toBe(0)
172
+ })
173
173
  })
174
174
 
175
175
  it('should clean up the caller signal listener when the task completes normally', async () => {
176
- const s = new Semaphore(1)
177
- const controller = new AbortController()
176
+ await usingAsync(new Semaphore(1), async (s) => {
177
+ const controller = new AbortController()
178
178
 
179
- const removeSpy = vi.spyOn(controller.signal, 'removeEventListener')
179
+ const removeSpy = vi.spyOn(controller.signal, 'removeEventListener')
180
180
 
181
- await s.execute(async () => 'done', { signal: controller.signal })
181
+ await s.execute(async () => 'done', { signal: controller.signal })
182
182
 
183
- expect(removeSpy).toHaveBeenCalledWith('abort', expect.any(Function))
184
- removeSpy.mockRestore()
185
- s[Symbol.dispose]()
183
+ expect(removeSpy).toHaveBeenCalledWith('abort', expect.any(Function))
184
+ removeSpy.mockRestore()
185
+ })
186
186
  })
187
187
 
188
188
  it('should abort the signal passed to a running task when the caller signal aborts', async () => {
189
- const s = new Semaphore(1)
190
- const signalAborted = vi.fn()
191
-
192
- const controller = new AbortController()
193
- const p = s.execute(
194
- async ({ signal }) => {
195
- signal.addEventListener('abort', signalAborted)
196
- await new Promise<void>((resolve) => {
197
- signal.addEventListener('abort', () => resolve())
198
- })
199
- throw signal.reason
200
- },
201
- { signal: controller.signal },
202
- )
203
-
204
- await sleepAsync(10)
205
- expect(s.runningCount.getValue()).toBe(1)
206
-
207
- controller.abort(new Error('stop'))
208
- await expect(p).rejects.toThrow('stop')
209
- expect(signalAborted).toBeCalledTimes(1)
210
- s[Symbol.dispose]()
189
+ await usingAsync(new Semaphore(1), async (s) => {
190
+ const signalAborted = vi.fn()
191
+
192
+ const controller = new AbortController()
193
+ const p = s.execute(
194
+ async ({ signal }) => {
195
+ signal.addEventListener('abort', signalAborted)
196
+ await new Promise<void>((resolve) => {
197
+ signal.addEventListener('abort', () => resolve())
198
+ })
199
+ throw signal.reason
200
+ },
201
+ { signal: controller.signal },
202
+ )
203
+
204
+ await sleepAsync(10)
205
+ expect(s.runningCount.getValue()).toBe(1)
206
+
207
+ controller.abort(new Error('stop'))
208
+ await expect(p).rejects.toThrow('stop')
209
+ expect(signalAborted).toBeCalledTimes(1)
210
+ })
211
211
  })
212
212
  })
213
213
 
214
214
  describe('EventHub events', () => {
215
215
  it('should emit taskStarted when a task begins running', async () => {
216
- const s = new Semaphore(1)
217
- const listener = vi.fn()
218
- s.subscribe('taskStarted', listener)
216
+ await usingAsync(new Semaphore(1), async (s) => {
217
+ const listener = vi.fn()
218
+ s.subscribe('taskStarted', listener)
219
219
 
220
- await s.execute(async () => 'done')
220
+ await s.execute(async () => 'done')
221
221
 
222
- expect(listener).toBeCalledTimes(1)
223
- s[Symbol.dispose]()
222
+ expect(listener).toBeCalledTimes(1)
223
+ })
224
224
  })
225
225
 
226
226
  it('should emit taskCompleted when a task resolves', async () => {
227
- const s = new Semaphore(1)
228
- const listener = vi.fn()
229
- s.subscribe('taskCompleted', listener)
227
+ await usingAsync(new Semaphore(1), async (s) => {
228
+ const listener = vi.fn()
229
+ s.subscribe('taskCompleted', listener)
230
230
 
231
- await s.execute(async () => 'done')
231
+ await s.execute(async () => 'done')
232
232
 
233
- expect(listener).toBeCalledTimes(1)
234
- s[Symbol.dispose]()
233
+ expect(listener).toBeCalledTimes(1)
234
+ })
235
235
  })
236
236
 
237
237
  it('should emit taskFailed with the error when a task rejects', async () => {
238
- const s = new Semaphore(1)
239
- const listener = vi.fn()
240
- s.subscribe('taskFailed', listener)
241
-
242
- const taskError = new Error('boom')
243
- await s
244
- .execute(async () => {
245
- throw taskError
246
- })
247
- .catch(() => {})
238
+ await usingAsync(new Semaphore(1), async (s) => {
239
+ const listener = vi.fn()
240
+ s.subscribe('taskFailed', listener)
241
+
242
+ const taskError = new Error('boom')
243
+ await s
244
+ .execute(async () => {
245
+ throw taskError
246
+ })
247
+ .catch(() => {})
248
248
 
249
- expect(listener).toBeCalledTimes(1)
250
- expect(listener).toBeCalledWith({ error: taskError })
251
- s[Symbol.dispose]()
249
+ expect(listener).toBeCalledTimes(1)
250
+ expect(listener).toBeCalledWith({ error: taskError })
251
+ })
252
252
  })
253
253
 
254
254
  it('should emit events in correct order for queued tasks', async () => {
255
- const s = new Semaphore(1)
256
- const events: string[] = []
255
+ await usingAsync(new Semaphore(1), async (s) => {
256
+ const events: string[] = []
257
257
 
258
- s.subscribe('taskStarted', () => {
259
- events.push('started')
260
- })
261
- s.subscribe('taskCompleted', () => {
262
- events.push('completed')
263
- })
258
+ s.subscribe('taskStarted', () => {
259
+ events.push('started')
260
+ })
261
+ s.subscribe('taskCompleted', () => {
262
+ events.push('completed')
263
+ })
264
264
 
265
- await Promise.all([s.execute(async () => 'a'), s.execute(async () => 'b')])
265
+ await Promise.all([s.execute(async () => 'a'), s.execute(async () => 'b')])
266
266
 
267
- await sleepAsync(10)
267
+ await sleepAsync(10)
268
268
 
269
- expect(events).toEqual(['started', 'completed', 'started', 'completed'])
270
- s[Symbol.dispose]()
269
+ expect(events).toEqual(['started', 'completed', 'started', 'completed'])
270
+ })
271
271
  })
272
272
  })
273
273
 
274
274
  describe('setMaxConcurrent', () => {
275
275
  it('should return the updated value from getMaxConcurrent', () => {
276
- const s = new Semaphore(2)
277
- s.setMaxConcurrent(5)
278
- expect(s.getMaxConcurrent()).toBe(5)
279
- s[Symbol.dispose]()
276
+ using(new Semaphore(2), (s) => {
277
+ s.setMaxConcurrent(5)
278
+ expect(s.getMaxConcurrent()).toBe(5)
279
+ })
280
280
  })
281
281
 
282
282
  it('should throw when given a non-positive integer', () => {
283
- const s = new Semaphore(2)
284
- expect(() => s.setMaxConcurrent(0)).toThrow('maxConcurrent must be a positive integer')
285
- expect(() => s.setMaxConcurrent(-1)).toThrow('maxConcurrent must be a positive integer')
286
- expect(() => s.setMaxConcurrent(1.5)).toThrow('maxConcurrent must be a positive integer')
287
- s[Symbol.dispose]()
283
+ using(new Semaphore(2), (s) => {
284
+ expect(() => s.setMaxConcurrent(0)).toThrow('maxConcurrent must be a positive integer')
285
+ expect(() => s.setMaxConcurrent(-1)).toThrow('maxConcurrent must be a positive integer')
286
+ expect(() => s.setMaxConcurrent(1.5)).toThrow('maxConcurrent must be a positive integer')
287
+ })
288
288
  })
289
289
 
290
290
  it('should immediately start queued tasks when increased', async () => {
291
- const s = new Semaphore(1)
292
- const running: string[] = []
293
- const resolvers: Array<() => void> = []
291
+ await usingAsync(new Semaphore(1), async (s) => {
292
+ const running: string[] = []
293
+ const resolvers: Array<() => void> = []
294
+
295
+ const createTask = (name: string) =>
296
+ s.execute(async () => {
297
+ running.push(name)
298
+ await new Promise<void>((resolve) => resolvers.push(resolve))
299
+ return name
300
+ })
294
301
 
295
- const createTask = (name: string) =>
296
- s.execute(async () => {
297
- running.push(name)
298
- await new Promise<void>((resolve) => resolvers.push(resolve))
299
- return name
300
- })
302
+ const p1 = createTask('a')
303
+ const p2 = createTask('b')
304
+ const p3 = createTask('c')
301
305
 
302
- const p1 = createTask('a')
303
- const p2 = createTask('b')
304
- const p3 = createTask('c')
306
+ await sleepAsync(10)
307
+ expect(running).toEqual(['a'])
308
+ expect(s.runningCount.getValue()).toBe(1)
309
+ expect(s.pendingCount.getValue()).toBe(2)
305
310
 
306
- await sleepAsync(10)
307
- expect(running).toEqual(['a'])
308
- expect(s.runningCount.getValue()).toBe(1)
309
- expect(s.pendingCount.getValue()).toBe(2)
310
-
311
- s.setMaxConcurrent(3)
311
+ s.setMaxConcurrent(3)
312
312
 
313
- await sleepAsync(10)
314
- expect(running).toEqual(['a', 'b', 'c'])
315
- expect(s.runningCount.getValue()).toBe(3)
316
- expect(s.pendingCount.getValue()).toBe(0)
313
+ await sleepAsync(10)
314
+ expect(running).toEqual(['a', 'b', 'c'])
315
+ expect(s.runningCount.getValue()).toBe(3)
316
+ expect(s.pendingCount.getValue()).toBe(0)
317
317
 
318
- resolvers.forEach((r) => r())
319
- await Promise.all([p1, p2, p3])
320
- s[Symbol.dispose]()
318
+ resolvers.forEach((r) => r())
319
+ await Promise.all([p1, p2, p3])
320
+ })
321
321
  })
322
322
 
323
323
  it('should not abort running tasks when decreased', async () => {
324
- const s = new Semaphore(3)
325
- const resolvers: Array<() => void> = []
324
+ await usingAsync(new Semaphore(3), async (s) => {
325
+ const resolvers: Array<() => void> = []
326
326
 
327
- const createTask = () =>
328
- s.execute(async () => {
329
- await new Promise<void>((resolve) => resolvers.push(resolve))
330
- })
327
+ const createTask = () =>
328
+ s.execute(async () => {
329
+ await new Promise<void>((resolve) => resolvers.push(resolve))
330
+ })
331
331
 
332
- const p1 = createTask()
333
- const p2 = createTask()
334
- const p3 = createTask()
332
+ const p1 = createTask()
333
+ const p2 = createTask()
334
+ const p3 = createTask()
335
335
 
336
- await sleepAsync(10)
337
- expect(s.runningCount.getValue()).toBe(3)
336
+ await sleepAsync(10)
337
+ expect(s.runningCount.getValue()).toBe(3)
338
338
 
339
- s.setMaxConcurrent(1)
339
+ s.setMaxConcurrent(1)
340
340
 
341
- expect(s.runningCount.getValue()).toBe(3)
341
+ expect(s.runningCount.getValue()).toBe(3)
342
342
 
343
- resolvers.forEach((r) => r())
344
- await Promise.all([p1, p2, p3])
345
- expect(s.completedCount.getValue()).toBe(3)
346
- s[Symbol.dispose]()
343
+ resolvers.forEach((r) => r())
344
+ await Promise.all([p1, p2, p3])
345
+ expect(s.completedCount.getValue()).toBe(3)
346
+ })
347
347
  })
348
348
 
349
349
  it('should not start new tasks until running count drops below new lower limit', async () => {
350
- const s = new Semaphore(2)
351
- const running: string[] = []
352
- const resolvers: Array<() => void> = []
353
-
354
- const createTask = (name: string) =>
355
- s.execute(async () => {
356
- running.push(name)
357
- await new Promise<void>((resolve) => resolvers.push(resolve))
358
- return name
359
- })
350
+ await usingAsync(new Semaphore(2), async (s) => {
351
+ const running: string[] = []
352
+ const resolvers: Array<() => void> = []
353
+
354
+ const createTask = (name: string) =>
355
+ s.execute(async () => {
356
+ running.push(name)
357
+ await new Promise<void>((resolve) => resolvers.push(resolve))
358
+ return name
359
+ })
360
360
 
361
- const p1 = createTask('a')
362
- const p2 = createTask('b')
363
- const p3 = createTask('c')
361
+ const p1 = createTask('a')
362
+ const p2 = createTask('b')
363
+ const p3 = createTask('c')
364
364
 
365
- await sleepAsync(10)
366
- expect(running).toEqual(['a', 'b'])
367
- expect(s.pendingCount.getValue()).toBe(1)
365
+ await sleepAsync(10)
366
+ expect(running).toEqual(['a', 'b'])
367
+ expect(s.pendingCount.getValue()).toBe(1)
368
368
 
369
- s.setMaxConcurrent(1)
369
+ s.setMaxConcurrent(1)
370
370
 
371
- resolvers[0]()
372
- await p1
373
- await sleepAsync(10)
371
+ resolvers[0]()
372
+ await p1
373
+ await sleepAsync(10)
374
374
 
375
- expect(running).toEqual(['a', 'b'])
376
- expect(s.pendingCount.getValue()).toBe(1)
375
+ expect(running).toEqual(['a', 'b'])
376
+ expect(s.pendingCount.getValue()).toBe(1)
377
377
 
378
- resolvers[1]()
379
- await p2
380
- await sleepAsync(10)
378
+ resolvers[1]()
379
+ await p2
380
+ await sleepAsync(10)
381
381
 
382
- expect(running).toEqual(['a', 'b', 'c'])
383
- expect(s.pendingCount.getValue()).toBe(0)
382
+ expect(running).toEqual(['a', 'b', 'c'])
383
+ expect(s.pendingCount.getValue()).toBe(0)
384
384
 
385
- resolvers[2]()
386
- await p3
387
- s[Symbol.dispose]()
385
+ resolvers[2]()
386
+ await p3
387
+ })
388
388
  })
389
389
  })
390
390
 
@@ -460,7 +460,7 @@ export const semaphoreTests = describe('Semaphore', () => {
460
460
 
461
461
  s[Symbol.dispose]()
462
462
 
463
- s.emit('taskStarted', undefined)
463
+ expect(() => s.emit('taskStarted', undefined)).not.toThrow()
464
464
  expect(listener).not.toBeCalled()
465
465
  })
466
466
  })