@stina/extension-api 0.21.0 → 0.22.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/dist/{chunk-WIDGIYRV.js → chunk-U3PEHSBG.js} +1 -1
- package/dist/chunk-U3PEHSBG.js.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -6
- package/dist/index.d.ts +67 -6
- package/dist/index.js +1 -1
- package/dist/runtime.cjs +242 -0
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +2 -2
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +243 -1
- package/dist/runtime.js.map +1 -1
- package/dist/{types.tools-BQrCW_wq.d.cts → types.tools-BXGZf8zc.d.cts} +188 -1
- package/dist/{types.tools-BQrCW_wq.d.ts → types.tools-BXGZf8zc.d.ts} +188 -1
- package/package.json +1 -1
- package/src/background.test.ts +525 -0
- package/src/background.ts +261 -0
- package/src/index.ts +14 -0
- package/src/messages.ts +71 -0
- package/src/runtime.ts +113 -0
- package/src/types.context.ts +217 -0
- package/src/types.permissions.ts +1 -0
- package/src/types.ts +7 -0
- package/dist/chunk-WIDGIYRV.js.map +0 -1
package/package.json
CHANGED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkerBackgroundTaskManager Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
6
|
+
import { WorkerBackgroundTaskManager, type WorkerBackgroundTaskManagerOptions } from './background.js'
|
|
7
|
+
import type { BackgroundTaskConfig, BackgroundTaskContext } from './types.js'
|
|
8
|
+
|
|
9
|
+
describe('WorkerBackgroundTaskManager', () => {
|
|
10
|
+
let manager: WorkerBackgroundTaskManager
|
|
11
|
+
let sendTaskRegistered: ReturnType<typeof vi.fn>
|
|
12
|
+
let sendTaskStatus: ReturnType<typeof vi.fn>
|
|
13
|
+
let sendHealthReport: ReturnType<typeof vi.fn>
|
|
14
|
+
let createLogAPI: ReturnType<typeof vi.fn>
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
sendTaskRegistered = vi.fn()
|
|
18
|
+
sendTaskStatus = vi.fn()
|
|
19
|
+
sendHealthReport = vi.fn()
|
|
20
|
+
createLogAPI = vi.fn(() => ({
|
|
21
|
+
debug: vi.fn(),
|
|
22
|
+
info: vi.fn(),
|
|
23
|
+
warn: vi.fn(),
|
|
24
|
+
error: vi.fn(),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
const options: WorkerBackgroundTaskManagerOptions = {
|
|
28
|
+
extensionId: 'test-extension',
|
|
29
|
+
extensionVersion: '1.0.0',
|
|
30
|
+
storagePath: '/fake/path',
|
|
31
|
+
sendTaskRegistered,
|
|
32
|
+
sendTaskStatus,
|
|
33
|
+
sendHealthReport,
|
|
34
|
+
createLogAPI,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
manager = new WorkerBackgroundTaskManager(options)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('start', () => {
|
|
41
|
+
it('should register a task and notify host', async () => {
|
|
42
|
+
const config: BackgroundTaskConfig = {
|
|
43
|
+
id: 'task-1',
|
|
44
|
+
name: 'Test Task',
|
|
45
|
+
userId: 'user-1',
|
|
46
|
+
restartPolicy: { type: 'never' },
|
|
47
|
+
}
|
|
48
|
+
const callback = vi.fn()
|
|
49
|
+
|
|
50
|
+
await manager.start(config, callback)
|
|
51
|
+
|
|
52
|
+
expect(manager.hasTask('task-1')).toBe(true)
|
|
53
|
+
expect(sendTaskRegistered).toHaveBeenCalledWith(
|
|
54
|
+
'task-1',
|
|
55
|
+
'Test Task',
|
|
56
|
+
'user-1',
|
|
57
|
+
{ type: 'never' },
|
|
58
|
+
undefined
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should throw error if task already exists', async () => {
|
|
63
|
+
const config: BackgroundTaskConfig = {
|
|
64
|
+
id: 'task-1',
|
|
65
|
+
name: 'Test Task',
|
|
66
|
+
userId: 'user-1',
|
|
67
|
+
restartPolicy: { type: 'never' },
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await manager.start(config, vi.fn())
|
|
71
|
+
|
|
72
|
+
await expect(manager.start(config, vi.fn())).rejects.toThrow(
|
|
73
|
+
"Background task with id 'task-1' is already registered"
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should return a disposable that stops the task', async () => {
|
|
78
|
+
const config: BackgroundTaskConfig = {
|
|
79
|
+
id: 'task-1',
|
|
80
|
+
name: 'Test Task',
|
|
81
|
+
userId: 'user-1',
|
|
82
|
+
restartPolicy: { type: 'never' },
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const disposable = await manager.start(config, vi.fn())
|
|
86
|
+
disposable.dispose()
|
|
87
|
+
|
|
88
|
+
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'stopped')
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('stop', () => {
|
|
93
|
+
it('should abort running task and notify host', async () => {
|
|
94
|
+
const config: BackgroundTaskConfig = {
|
|
95
|
+
id: 'task-1',
|
|
96
|
+
name: 'Test Task',
|
|
97
|
+
userId: 'user-1',
|
|
98
|
+
restartPolicy: { type: 'never' },
|
|
99
|
+
}
|
|
100
|
+
const callback = vi.fn(async ({ signal }) => {
|
|
101
|
+
// Simulate long-running task
|
|
102
|
+
return new Promise<void>((resolve) => {
|
|
103
|
+
const timeout = setTimeout(resolve, 10000)
|
|
104
|
+
signal.addEventListener('abort', () => {
|
|
105
|
+
clearTimeout(timeout)
|
|
106
|
+
resolve()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
await manager.start(config, callback)
|
|
112
|
+
sendTaskStatus.mockClear()
|
|
113
|
+
|
|
114
|
+
// Start the task
|
|
115
|
+
const handlePromise = manager.handleStart('task-1')
|
|
116
|
+
|
|
117
|
+
// Wait a bit for task to start
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
119
|
+
|
|
120
|
+
// Stop it
|
|
121
|
+
manager.stop('task-1')
|
|
122
|
+
|
|
123
|
+
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'stopped')
|
|
124
|
+
|
|
125
|
+
// Wait for handleStart to complete
|
|
126
|
+
await handlePromise
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should do nothing if task does not exist', () => {
|
|
130
|
+
manager.stop('non-existent')
|
|
131
|
+
|
|
132
|
+
expect(sendTaskStatus).not.toHaveBeenCalled()
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('handleStart', () => {
|
|
137
|
+
it('should execute task callback and notify host', async () => {
|
|
138
|
+
const config: BackgroundTaskConfig = {
|
|
139
|
+
id: 'task-1',
|
|
140
|
+
name: 'Test Task',
|
|
141
|
+
userId: 'user-1',
|
|
142
|
+
restartPolicy: { type: 'never' },
|
|
143
|
+
}
|
|
144
|
+
const callback = vi.fn(async () => {
|
|
145
|
+
// Task completes successfully
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
await manager.start(config, callback)
|
|
149
|
+
sendTaskStatus.mockClear()
|
|
150
|
+
|
|
151
|
+
await manager.handleStart('task-1')
|
|
152
|
+
|
|
153
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
154
|
+
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'running')
|
|
155
|
+
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'stopped')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should abort previous execution if already running', async () => {
|
|
159
|
+
const config: BackgroundTaskConfig = {
|
|
160
|
+
id: 'task-1',
|
|
161
|
+
name: 'Test Task',
|
|
162
|
+
userId: 'user-1',
|
|
163
|
+
restartPolicy: { type: 'never' },
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let firstAborted = false
|
|
167
|
+
let secondCompleted = false
|
|
168
|
+
|
|
169
|
+
const callback = vi.fn(async ({ signal }) => {
|
|
170
|
+
return new Promise<void>((resolve) => {
|
|
171
|
+
const timeout = setTimeout(() => {
|
|
172
|
+
secondCompleted = true
|
|
173
|
+
resolve()
|
|
174
|
+
}, 100)
|
|
175
|
+
|
|
176
|
+
signal.addEventListener('abort', () => {
|
|
177
|
+
clearTimeout(timeout)
|
|
178
|
+
firstAborted = true
|
|
179
|
+
resolve()
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
await manager.start(config, callback)
|
|
185
|
+
|
|
186
|
+
// Start first execution
|
|
187
|
+
const firstPromise = manager.handleStart('task-1')
|
|
188
|
+
|
|
189
|
+
// Start second execution before first completes - should abort first
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
191
|
+
const secondPromise = manager.handleStart('task-1')
|
|
192
|
+
|
|
193
|
+
await Promise.all([firstPromise, secondPromise])
|
|
194
|
+
|
|
195
|
+
expect(callback).toHaveBeenCalledTimes(2)
|
|
196
|
+
expect(firstAborted).toBe(true)
|
|
197
|
+
expect(secondCompleted).toBe(true)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should handle task callback errors', async () => {
|
|
201
|
+
const config: BackgroundTaskConfig = {
|
|
202
|
+
id: 'task-1',
|
|
203
|
+
name: 'Test Task',
|
|
204
|
+
userId: 'user-1',
|
|
205
|
+
restartPolicy: { type: 'never' },
|
|
206
|
+
}
|
|
207
|
+
const callback = vi.fn(async () => {
|
|
208
|
+
throw new Error('Task failed')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
await manager.start(config, callback)
|
|
212
|
+
sendTaskStatus.mockClear()
|
|
213
|
+
|
|
214
|
+
await manager.handleStart('task-1')
|
|
215
|
+
|
|
216
|
+
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'running')
|
|
217
|
+
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'failed', 'Task failed')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should detect aborted task', async () => {
|
|
221
|
+
const config: BackgroundTaskConfig = {
|
|
222
|
+
id: 'task-1',
|
|
223
|
+
name: 'Test Task',
|
|
224
|
+
userId: 'user-1',
|
|
225
|
+
restartPolicy: { type: 'never' },
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let wasAborted = false
|
|
229
|
+
|
|
230
|
+
const callback = vi.fn(async ({ signal }) => {
|
|
231
|
+
return new Promise<void>((resolve) => {
|
|
232
|
+
const timeout = setTimeout(resolve, 100)
|
|
233
|
+
signal.addEventListener('abort', () => {
|
|
234
|
+
clearTimeout(timeout)
|
|
235
|
+
wasAborted = true
|
|
236
|
+
resolve()
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
await manager.start(config, callback)
|
|
242
|
+
sendTaskStatus.mockClear()
|
|
243
|
+
|
|
244
|
+
const handlePromise = manager.handleStart('task-1')
|
|
245
|
+
|
|
246
|
+
// Abort the task
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
248
|
+
manager.stop('task-1')
|
|
249
|
+
|
|
250
|
+
await handlePromise
|
|
251
|
+
|
|
252
|
+
expect(wasAborted).toBe(true)
|
|
253
|
+
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'stopped')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should do nothing if task does not exist', async () => {
|
|
257
|
+
await manager.handleStart('non-existent')
|
|
258
|
+
|
|
259
|
+
expect(sendTaskStatus).not.toHaveBeenCalled()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should provide correct execution context', async () => {
|
|
263
|
+
const config: BackgroundTaskConfig = {
|
|
264
|
+
id: 'task-1',
|
|
265
|
+
name: 'Test Task',
|
|
266
|
+
userId: 'user-1',
|
|
267
|
+
restartPolicy: { type: 'never' },
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let receivedContext: BackgroundTaskContext | null = null
|
|
271
|
+
|
|
272
|
+
const callback = vi.fn(async (context) => {
|
|
273
|
+
receivedContext = context
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
await manager.start(config, callback)
|
|
277
|
+
await manager.handleStart('task-1')
|
|
278
|
+
|
|
279
|
+
expect(receivedContext).toBeDefined()
|
|
280
|
+
expect(receivedContext!.userId).toBe('user-1')
|
|
281
|
+
expect(receivedContext!.extension.id).toBe('test-extension')
|
|
282
|
+
expect(receivedContext!.extension.version).toBe('1.0.0')
|
|
283
|
+
expect(receivedContext!.extension.storagePath).toBe('/fake/path')
|
|
284
|
+
expect(receivedContext!.signal).toBeDefined()
|
|
285
|
+
expect(receivedContext!.reportHealth).toBeDefined()
|
|
286
|
+
expect(receivedContext!.log).toBeDefined()
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe('handleStop', () => {
|
|
291
|
+
it('should stop the task', async () => {
|
|
292
|
+
const config: BackgroundTaskConfig = {
|
|
293
|
+
id: 'task-1',
|
|
294
|
+
name: 'Test Task',
|
|
295
|
+
userId: 'user-1',
|
|
296
|
+
restartPolicy: { type: 'never' },
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
await manager.start(config, vi.fn())
|
|
300
|
+
sendTaskStatus.mockClear()
|
|
301
|
+
|
|
302
|
+
manager.handleStop('task-1')
|
|
303
|
+
|
|
304
|
+
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'stopped')
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
describe('reportHealth', () => {
|
|
309
|
+
it('should send health report to host', async () => {
|
|
310
|
+
const config: BackgroundTaskConfig = {
|
|
311
|
+
id: 'task-1',
|
|
312
|
+
name: 'Test Task',
|
|
313
|
+
userId: 'user-1',
|
|
314
|
+
restartPolicy: { type: 'never' },
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let reportHealthFn: ((status: string) => void) | null = null
|
|
318
|
+
|
|
319
|
+
const callback = vi.fn(async (context) => {
|
|
320
|
+
reportHealthFn = context.reportHealth
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
await manager.start(config, callback)
|
|
324
|
+
await manager.handleStart('task-1')
|
|
325
|
+
|
|
326
|
+
expect(reportHealthFn).toBeDefined()
|
|
327
|
+
|
|
328
|
+
reportHealthFn!('Processing 100 items...')
|
|
329
|
+
|
|
330
|
+
expect(sendHealthReport).toHaveBeenCalledWith(
|
|
331
|
+
'task-1',
|
|
332
|
+
'Processing 100 items...',
|
|
333
|
+
expect.any(String)
|
|
334
|
+
)
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
describe('getStatus', () => {
|
|
339
|
+
it('should return status of all tasks', async () => {
|
|
340
|
+
const config1: BackgroundTaskConfig = {
|
|
341
|
+
id: 'task-1',
|
|
342
|
+
name: 'Test Task 1',
|
|
343
|
+
userId: 'user-1',
|
|
344
|
+
restartPolicy: { type: 'never' },
|
|
345
|
+
}
|
|
346
|
+
const config2: BackgroundTaskConfig = {
|
|
347
|
+
id: 'task-2',
|
|
348
|
+
name: 'Test Task 2',
|
|
349
|
+
userId: 'user-2',
|
|
350
|
+
restartPolicy: { type: 'always' },
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
await manager.start(config1, vi.fn())
|
|
354
|
+
await manager.start(config2, vi.fn())
|
|
355
|
+
|
|
356
|
+
const statuses = manager.getStatus()
|
|
357
|
+
|
|
358
|
+
expect(statuses).toHaveLength(2)
|
|
359
|
+
expect(statuses.find((s) => s.taskId === 'task-1')).toMatchObject({
|
|
360
|
+
taskId: 'task-1',
|
|
361
|
+
name: 'Test Task 1',
|
|
362
|
+
userId: 'user-1',
|
|
363
|
+
status: 'pending',
|
|
364
|
+
restartCount: 0,
|
|
365
|
+
})
|
|
366
|
+
expect(statuses.find((s) => s.taskId === 'task-2')).toMatchObject({
|
|
367
|
+
taskId: 'task-2',
|
|
368
|
+
name: 'Test Task 2',
|
|
369
|
+
userId: 'user-2',
|
|
370
|
+
status: 'pending',
|
|
371
|
+
restartCount: 0,
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
describe('dispose', () => {
|
|
377
|
+
it('should abort all running tasks', async () => {
|
|
378
|
+
const config1: BackgroundTaskConfig = {
|
|
379
|
+
id: 'task-1',
|
|
380
|
+
name: 'Test Task 1',
|
|
381
|
+
userId: 'user-1',
|
|
382
|
+
restartPolicy: { type: 'never' },
|
|
383
|
+
}
|
|
384
|
+
const config2: BackgroundTaskConfig = {
|
|
385
|
+
id: 'task-2',
|
|
386
|
+
name: 'Test Task 2',
|
|
387
|
+
userId: 'user-2',
|
|
388
|
+
restartPolicy: { type: 'never' },
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let task1Aborted = false
|
|
392
|
+
let task2Aborted = false
|
|
393
|
+
|
|
394
|
+
const callback1 = vi.fn(async ({ signal }) => {
|
|
395
|
+
return new Promise<void>((resolve) => {
|
|
396
|
+
const timeout = setTimeout(resolve, 1000)
|
|
397
|
+
signal.addEventListener('abort', () => {
|
|
398
|
+
clearTimeout(timeout)
|
|
399
|
+
task1Aborted = true
|
|
400
|
+
resolve()
|
|
401
|
+
})
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
const callback2 = vi.fn(async ({ signal }) => {
|
|
406
|
+
return new Promise<void>((resolve) => {
|
|
407
|
+
const timeout = setTimeout(resolve, 1000)
|
|
408
|
+
signal.addEventListener('abort', () => {
|
|
409
|
+
clearTimeout(timeout)
|
|
410
|
+
task2Aborted = true
|
|
411
|
+
resolve()
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
await manager.start(config1, callback1)
|
|
417
|
+
await manager.start(config2, callback2)
|
|
418
|
+
|
|
419
|
+
const promise1 = manager.handleStart('task-1')
|
|
420
|
+
const promise2 = manager.handleStart('task-2')
|
|
421
|
+
|
|
422
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
423
|
+
|
|
424
|
+
manager.dispose()
|
|
425
|
+
|
|
426
|
+
await Promise.all([promise1, promise2])
|
|
427
|
+
|
|
428
|
+
expect(task1Aborted).toBe(true)
|
|
429
|
+
expect(task2Aborted).toBe(true)
|
|
430
|
+
expect(manager.getStatus()).toHaveLength(0)
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
describe('race condition scenarios', () => {
|
|
435
|
+
it('should handle dispose while task is running', async () => {
|
|
436
|
+
const config: BackgroundTaskConfig = {
|
|
437
|
+
id: 'task-1',
|
|
438
|
+
name: 'Test Task',
|
|
439
|
+
userId: 'user-1',
|
|
440
|
+
restartPolicy: { type: 'never' },
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let disposed = false
|
|
444
|
+
|
|
445
|
+
const callback = vi.fn(async ({ signal }) => {
|
|
446
|
+
return new Promise<void>((resolve) => {
|
|
447
|
+
const timeout = setTimeout(resolve, 100)
|
|
448
|
+
signal.addEventListener('abort', () => {
|
|
449
|
+
clearTimeout(timeout)
|
|
450
|
+
disposed = true
|
|
451
|
+
resolve()
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
const disposable = await manager.start(config, callback)
|
|
457
|
+
const handlePromise = manager.handleStart('task-1')
|
|
458
|
+
|
|
459
|
+
// Dispose while running
|
|
460
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
461
|
+
disposable.dispose()
|
|
462
|
+
|
|
463
|
+
await handlePromise
|
|
464
|
+
|
|
465
|
+
expect(disposed).toBe(true)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('should handle stop before task starts', async () => {
|
|
469
|
+
const config: BackgroundTaskConfig = {
|
|
470
|
+
id: 'task-1',
|
|
471
|
+
name: 'Test Task',
|
|
472
|
+
userId: 'user-1',
|
|
473
|
+
restartPolicy: { type: 'never' },
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
await manager.start(config, vi.fn())
|
|
477
|
+
|
|
478
|
+
// Stop before handleStart is called
|
|
479
|
+
manager.stop('task-1')
|
|
480
|
+
sendTaskStatus.mockClear()
|
|
481
|
+
|
|
482
|
+
// Now start it
|
|
483
|
+
await manager.handleStart('task-1')
|
|
484
|
+
|
|
485
|
+
// Should immediately detect it's aborted
|
|
486
|
+
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'running')
|
|
487
|
+
expect(sendTaskStatus).toHaveBeenCalledWith('task-1', 'stopped')
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('should handle multiple rapid start/stop cycles', async () => {
|
|
491
|
+
const config: BackgroundTaskConfig = {
|
|
492
|
+
id: 'task-1',
|
|
493
|
+
name: 'Test Task',
|
|
494
|
+
userId: 'user-1',
|
|
495
|
+
restartPolicy: { type: 'never' },
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const callback = vi.fn(async ({ signal }) => {
|
|
499
|
+
return new Promise<void>((resolve) => {
|
|
500
|
+
const timeout = setTimeout(resolve, 50)
|
|
501
|
+
signal.addEventListener('abort', () => {
|
|
502
|
+
clearTimeout(timeout)
|
|
503
|
+
resolve()
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
await manager.start(config, callback)
|
|
509
|
+
|
|
510
|
+
// Rapidly start and stop
|
|
511
|
+
const promises: Promise<void>[] = []
|
|
512
|
+
for (let i = 0; i < 5; i++) {
|
|
513
|
+
promises.push(manager.handleStart('task-1'))
|
|
514
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
515
|
+
manager.stop('task-1')
|
|
516
|
+
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
await Promise.all(promises)
|
|
520
|
+
|
|
521
|
+
// Should have handled all starts
|
|
522
|
+
expect(callback).toHaveBeenCalledTimes(5)
|
|
523
|
+
})
|
|
524
|
+
})
|
|
525
|
+
})
|