ai-workflows 2.1.1 → 2.1.3
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +165 -3
- package/CHANGELOG.md +10 -1
- package/LICENSE +21 -0
- package/README.md +303 -184
- package/dist/barrier.d.ts +153 -0
- package/dist/barrier.d.ts.map +1 -0
- package/dist/barrier.js +339 -0
- package/dist/barrier.js.map +1 -0
- package/dist/cascade-context.d.ts +149 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +324 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/cascade-executor.d.ts +196 -0
- package/dist/cascade-executor.d.ts.map +1 -0
- package/dist/cascade-executor.js +384 -0
- package/dist/cascade-executor.js.map +1 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +4 -1
- package/dist/context.js.map +1 -1
- package/dist/dependency-graph.d.ts +157 -0
- package/dist/dependency-graph.d.ts.map +1 -0
- package/dist/dependency-graph.js +382 -0
- package/dist/dependency-graph.js.map +1 -0
- package/dist/every.d.ts +31 -2
- package/dist/every.d.ts.map +1 -1
- package/dist/every.js +63 -32
- package/dist/every.js.map +1 -1
- package/dist/graph/index.d.ts +8 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +8 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/topological-sort.d.ts +121 -0
- package/dist/graph/topological-sort.d.ts.map +1 -0
- package/dist/graph/topological-sort.js +292 -0
- package/dist/graph/topological-sort.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/on.d.ts +35 -10
- package/dist/on.d.ts.map +1 -1
- package/dist/on.js +52 -18
- package/dist/on.js.map +1 -1
- package/dist/timer-registry.d.ts +52 -0
- package/dist/timer-registry.d.ts.map +1 -0
- package/dist/timer-registry.js +120 -0
- package/dist/timer-registry.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -1
- package/dist/types.js.map +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +15 -11
- package/dist/workflow.js.map +1 -1
- package/package.json +11 -11
- package/src/barrier.ts +466 -0
- package/src/cascade-context.ts +488 -0
- package/src/cascade-executor.ts +587 -0
- package/src/context.ts +12 -7
- package/src/dependency-graph.ts +518 -0
- package/src/every.ts +104 -35
- package/src/graph/index.ts +19 -0
- package/src/graph/topological-sort.ts +414 -0
- package/src/index.ts +78 -0
- package/src/on.ts +81 -25
- package/src/timer-registry.ts +145 -0
- package/src/types.ts +121 -0
- package/src/workflow.ts +23 -16
- package/test/barrier-join.test.ts +434 -0
- package/test/barrier-unhandled-rejections.test.ts +359 -0
- package/test/cascade-context.test.ts +390 -0
- package/test/cascade-executor.test.ts +859 -0
- package/test/dependency-graph.test.ts +512 -0
- package/test/graph/topological-sort.test.ts +586 -0
- package/test/schedule-timer-cleanup.test.ts +344 -0
- package/test/send-race-conditions.test.ts +410 -0
- package/test/type-safety-every.test.ts +303 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for schedule timer cleanup
|
|
3
|
+
*
|
|
4
|
+
* These tests expose the memory leak issue where timers accumulate
|
|
5
|
+
* when workflows are destroyed without cleanup.
|
|
6
|
+
*
|
|
7
|
+
* GREEN PHASE: Tests should pass with timer cleanup implementation.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
|
10
|
+
import { Workflow } from '../src/workflow.js'
|
|
11
|
+
import { clearEventHandlers } from '../src/on.js'
|
|
12
|
+
import { clearScheduleHandlers } from '../src/every.js'
|
|
13
|
+
// Import timer registry to ensure global functions are registered
|
|
14
|
+
import { clearAllTimers } from '../src/timer-registry.js'
|
|
15
|
+
|
|
16
|
+
describe('Schedule Timer Cleanup', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
clearEventHandlers()
|
|
19
|
+
clearScheduleHandlers()
|
|
20
|
+
clearAllTimers() // Clear any lingering timers from previous tests
|
|
21
|
+
vi.useFakeTimers()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
clearAllTimers() // Clean up timers after each test
|
|
26
|
+
vi.useRealTimers()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('Timer Memory Leak Detection', () => {
|
|
30
|
+
it('should not execute timer handlers after workflow goes out of scope', async () => {
|
|
31
|
+
// This test verifies that the global cleanup API can stop orphaned timers
|
|
32
|
+
const handler = vi.fn()
|
|
33
|
+
|
|
34
|
+
// Create workflow in a scope and let it go out of scope without stopping
|
|
35
|
+
function createAndAbandonWorkflow() {
|
|
36
|
+
const workflow = Workflow($ => {
|
|
37
|
+
$.every.seconds(1)(handler)
|
|
38
|
+
})
|
|
39
|
+
// Start the workflow - this creates the timer
|
|
40
|
+
// But we don't call stop() before letting it go out of scope
|
|
41
|
+
return workflow.start()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await createAndAbandonWorkflow()
|
|
45
|
+
// workflow is now out of scope, but timer is still running
|
|
46
|
+
// Use the global cleanup function to clear all orphaned timers
|
|
47
|
+
clearAllTimers()
|
|
48
|
+
|
|
49
|
+
// Advance time - handler should NOT be called after cleanup
|
|
50
|
+
await vi.advanceTimersByTimeAsync(5000)
|
|
51
|
+
|
|
52
|
+
// After calling clearAllTimers(), orphaned timers should be stopped
|
|
53
|
+
expect(handler).toHaveBeenCalledTimes(0)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should track active timers globally for cleanup', async () => {
|
|
57
|
+
// This test verifies that there's a way to track and clean up all active timers
|
|
58
|
+
// Currently there is no global registry, so this will fail
|
|
59
|
+
|
|
60
|
+
const handler1 = vi.fn()
|
|
61
|
+
const handler2 = vi.fn()
|
|
62
|
+
|
|
63
|
+
const workflow1 = Workflow($ => {
|
|
64
|
+
$.every.seconds(1)(handler1)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const workflow2 = Workflow($ => {
|
|
68
|
+
$.every.seconds(1)(handler2)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
await workflow1.start()
|
|
72
|
+
await workflow2.start()
|
|
73
|
+
|
|
74
|
+
// There should be a way to get the count of active timers
|
|
75
|
+
// This API doesn't exist yet
|
|
76
|
+
const getActiveTimerCount = () => {
|
|
77
|
+
// @ts-expect-error - This function doesn't exist yet
|
|
78
|
+
return typeof global.getActiveWorkflowTimerCount === 'function'
|
|
79
|
+
// @ts-expect-error - This function doesn't exist yet
|
|
80
|
+
? global.getActiveWorkflowTimerCount()
|
|
81
|
+
: -1 // Return -1 to indicate the function doesn't exist
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const timerCount = getActiveTimerCount()
|
|
85
|
+
|
|
86
|
+
// BUG: This will fail because there's no global timer registry
|
|
87
|
+
expect(timerCount).toBe(2)
|
|
88
|
+
|
|
89
|
+
await workflow1.stop()
|
|
90
|
+
await workflow2.stop()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should provide a clearAllTimers utility for cleanup', async () => {
|
|
94
|
+
// Test that there's a utility to clear all timers from all workflows
|
|
95
|
+
const handler = vi.fn()
|
|
96
|
+
|
|
97
|
+
// Create multiple workflows
|
|
98
|
+
const workflow1 = Workflow($ => {
|
|
99
|
+
$.every.seconds(1)(handler)
|
|
100
|
+
})
|
|
101
|
+
const workflow2 = Workflow($ => {
|
|
102
|
+
$.every.seconds(2)(handler)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
await workflow1.start()
|
|
106
|
+
await workflow2.start()
|
|
107
|
+
|
|
108
|
+
// Verify timers are running
|
|
109
|
+
await vi.advanceTimersByTimeAsync(2000)
|
|
110
|
+
expect(handler).toHaveBeenCalled()
|
|
111
|
+
const callCountBefore = handler.mock.calls.length
|
|
112
|
+
|
|
113
|
+
// There should be a way to clear all timers at once
|
|
114
|
+
// This API doesn't exist yet
|
|
115
|
+
const clearAllWorkflowTimers = () => {
|
|
116
|
+
// @ts-expect-error - This function doesn't exist yet
|
|
117
|
+
if (typeof global.clearAllWorkflowTimers === 'function') {
|
|
118
|
+
// @ts-expect-error - This function doesn't exist yet
|
|
119
|
+
global.clearAllWorkflowTimers()
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const cleared = clearAllWorkflowTimers()
|
|
126
|
+
|
|
127
|
+
// BUG: This will fail because there's no global clear function
|
|
128
|
+
expect(cleared).toBe(true)
|
|
129
|
+
|
|
130
|
+
// After clearing, no more handlers should be called
|
|
131
|
+
handler.mockClear()
|
|
132
|
+
await vi.advanceTimersByTimeAsync(5000)
|
|
133
|
+
expect(handler).toHaveBeenCalledTimes(0)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should clean up timers when workflow is explicitly destroyed', async () => {
|
|
137
|
+
// Test that a destroy() method exists and cleans up timers
|
|
138
|
+
const handler = vi.fn()
|
|
139
|
+
|
|
140
|
+
const workflow = Workflow($ => {
|
|
141
|
+
$.every.seconds(1)(handler)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
await workflow.start()
|
|
145
|
+
|
|
146
|
+
// Verify timer is running
|
|
147
|
+
await vi.advanceTimersByTimeAsync(2000)
|
|
148
|
+
expect(handler).toHaveBeenCalledTimes(2)
|
|
149
|
+
|
|
150
|
+
// There should be a destroy() method that cleans up everything
|
|
151
|
+
// This API doesn't exist yet - only stop() exists
|
|
152
|
+
const destroyWorkflow = () => {
|
|
153
|
+
if ('destroy' in workflow && typeof workflow.destroy === 'function') {
|
|
154
|
+
(workflow as { destroy: () => Promise<void> }).destroy()
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
return false
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const destroyed = destroyWorkflow()
|
|
161
|
+
|
|
162
|
+
// BUG: This will fail because there's no destroy() method
|
|
163
|
+
expect(destroyed).toBe(true)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should not leak timers when multiple workflows are started and stopped rapidly', async () => {
|
|
167
|
+
// Stress test: Create and destroy many workflows quickly
|
|
168
|
+
// Verify that the global cleanup API can handle multiple orphaned workflows
|
|
169
|
+
const handler = vi.fn()
|
|
170
|
+
const iterations = 10
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < iterations; i++) {
|
|
173
|
+
const workflow = Workflow($ => {
|
|
174
|
+
$.every.seconds(1)(handler)
|
|
175
|
+
})
|
|
176
|
+
await workflow.start()
|
|
177
|
+
// Intentionally NOT calling stop() to simulate memory leak
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Use global cleanup to clear all 10 orphaned timers at once
|
|
181
|
+
clearAllTimers()
|
|
182
|
+
|
|
183
|
+
// After cleanup, no timers should be running
|
|
184
|
+
await vi.advanceTimersByTimeAsync(1000)
|
|
185
|
+
|
|
186
|
+
// After calling clearAllTimers(), all orphaned timers should be stopped
|
|
187
|
+
expect(handler).toHaveBeenCalledTimes(0)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should support a dispose pattern for automatic cleanup', async () => {
|
|
191
|
+
// Test using Symbol.dispose for automatic cleanup (requires proper implementation)
|
|
192
|
+
const handler = vi.fn()
|
|
193
|
+
|
|
194
|
+
const workflow = Workflow($ => {
|
|
195
|
+
$.every.seconds(1)(handler)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
await workflow.start()
|
|
199
|
+
|
|
200
|
+
// Check if workflow supports dispose pattern
|
|
201
|
+
const hasDispose = Symbol.dispose in workflow || 'dispose' in workflow
|
|
202
|
+
|
|
203
|
+
// BUG: This will fail because dispose pattern is not implemented
|
|
204
|
+
expect(hasDispose).toBe(true)
|
|
205
|
+
|
|
206
|
+
// If dispose exists, calling it should stop all timers
|
|
207
|
+
if (hasDispose) {
|
|
208
|
+
if (Symbol.dispose in workflow) {
|
|
209
|
+
(workflow as { [Symbol.dispose]: () => void })[Symbol.dispose]()
|
|
210
|
+
} else if ('dispose' in workflow) {
|
|
211
|
+
(workflow as { dispose: () => void }).dispose()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
handler.mockClear()
|
|
215
|
+
await vi.advanceTimersByTimeAsync(5000)
|
|
216
|
+
expect(handler).toHaveBeenCalledTimes(0)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe('Timer Registration Tracking', () => {
|
|
222
|
+
it('should expose the number of registered timers on a workflow', async () => {
|
|
223
|
+
const workflow = Workflow($ => {
|
|
224
|
+
$.every.seconds(1)(() => {})
|
|
225
|
+
$.every.seconds(2)(() => {})
|
|
226
|
+
$.every.seconds(3)(() => {})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
await workflow.start()
|
|
230
|
+
|
|
231
|
+
// There should be a way to inspect how many timers are registered
|
|
232
|
+
const getTimerCount = () => {
|
|
233
|
+
if ('timerCount' in workflow) {
|
|
234
|
+
return (workflow as { timerCount: number }).timerCount
|
|
235
|
+
}
|
|
236
|
+
if ('getTimerCount' in workflow && typeof (workflow as { getTimerCount: () => number }).getTimerCount === 'function') {
|
|
237
|
+
return (workflow as { getTimerCount: () => number }).getTimerCount()
|
|
238
|
+
}
|
|
239
|
+
return -1
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const count = getTimerCount()
|
|
243
|
+
|
|
244
|
+
// BUG: This will fail because there's no timerCount property
|
|
245
|
+
expect(count).toBe(3)
|
|
246
|
+
|
|
247
|
+
await workflow.stop()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('should decrement timer count when stop is called', async () => {
|
|
251
|
+
const workflow = Workflow($ => {
|
|
252
|
+
$.every.seconds(1)(() => {})
|
|
253
|
+
$.every.seconds(2)(() => {})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
await workflow.start()
|
|
257
|
+
await workflow.stop()
|
|
258
|
+
|
|
259
|
+
const getTimerCount = () => {
|
|
260
|
+
if ('timerCount' in workflow) {
|
|
261
|
+
return (workflow as { timerCount: number }).timerCount
|
|
262
|
+
}
|
|
263
|
+
return -1
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const count = getTimerCount()
|
|
267
|
+
|
|
268
|
+
// BUG: This will fail because there's no timerCount property
|
|
269
|
+
expect(count).toBe(0)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe('Global Timer Registry', () => {
|
|
274
|
+
it('should register timers with a global registry', async () => {
|
|
275
|
+
// Import the registry (doesn't exist yet)
|
|
276
|
+
let registry: { getAll: () => unknown[] } | undefined
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
// @ts-expect-error - This module doesn't export a registry yet
|
|
280
|
+
const mod = await import('../src/timer-registry.js')
|
|
281
|
+
registry = mod.timerRegistry
|
|
282
|
+
} catch {
|
|
283
|
+
// Expected to fail - module doesn't exist
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// BUG: This will fail because timer-registry.js doesn't exist
|
|
287
|
+
expect(registry).toBeDefined()
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should allow clearing specific workflow timers from registry', async () => {
|
|
291
|
+
const handler = vi.fn()
|
|
292
|
+
|
|
293
|
+
const workflow = Workflow($ => {
|
|
294
|
+
$.every.seconds(1)(handler)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
await workflow.start()
|
|
298
|
+
|
|
299
|
+
// There should be a way to get the workflow's timer IDs
|
|
300
|
+
const getTimerIds = () => {
|
|
301
|
+
if ('getTimerIds' in workflow && typeof (workflow as { getTimerIds: () => string[] }).getTimerIds === 'function') {
|
|
302
|
+
return (workflow as { getTimerIds: () => string[] }).getTimerIds()
|
|
303
|
+
}
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const timerIds = getTimerIds()
|
|
308
|
+
|
|
309
|
+
// BUG: This will fail because there's no getTimerIds method
|
|
310
|
+
expect(timerIds).not.toBeNull()
|
|
311
|
+
expect(Array.isArray(timerIds)).toBe(true)
|
|
312
|
+
expect(timerIds?.length).toBeGreaterThan(0)
|
|
313
|
+
|
|
314
|
+
await workflow.stop()
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
describe('Cleanup on Process Exit', () => {
|
|
319
|
+
it('should register cleanup handlers for process exit', async () => {
|
|
320
|
+
// Check if there's a cleanup handler registered for process exit
|
|
321
|
+
// Note: The cleanup handler may be registered at module import time,
|
|
322
|
+
// so we just verify that listeners exist (not that new ones are added)
|
|
323
|
+
|
|
324
|
+
const workflow = Workflow($ => {
|
|
325
|
+
$.every.seconds(1)(() => {})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
await workflow.start()
|
|
329
|
+
|
|
330
|
+
// After starting a workflow, process cleanup should be registered
|
|
331
|
+
// (either now or at module import time)
|
|
332
|
+
const exitListeners = process.listeners('exit')
|
|
333
|
+
const beforeExitListeners = process.listeners('beforeExit')
|
|
334
|
+
|
|
335
|
+
// There should be at least one cleanup handler registered
|
|
336
|
+
// for either 'exit' or 'beforeExit' events
|
|
337
|
+
const hasCleanupListener = exitListeners.length > 0 || beforeExitListeners.length > 0
|
|
338
|
+
|
|
339
|
+
expect(hasCleanupListener).toBe(true)
|
|
340
|
+
|
|
341
|
+
await workflow.stop()
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
})
|