@forgehive/task 0.2.1 → 0.2.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/README.md +164 -10
- package/dist/index.d.ts +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +70 -10
- package/dist/index.js.map +1 -1
- package/dist/test/execution-record-boundaries.test.d.ts +2 -0
- package/dist/test/execution-record-boundaries.test.d.ts.map +1 -0
- package/dist/test/execution-record-boundaries.test.js +220 -0
- package/dist/test/execution-record-boundaries.test.js.map +1 -0
- package/dist/test/listen-execution-records.test.d.ts +2 -0
- package/dist/test/listen-execution-records.test.d.ts.map +1 -0
- package/dist/test/listen-execution-records.test.js +223 -0
- package/dist/test/listen-execution-records.test.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +104 -14
- package/src/test/execution-record-boundaries.test.ts +266 -0
- package/src/test/listen-execution-records.test.ts +295 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { Task, createTask, Schema } from '../index'
|
|
2
|
+
|
|
3
|
+
describe('Task.listenExecutionRecords', () => {
|
|
4
|
+
let mockListener: jest.Mock
|
|
5
|
+
let consoleErrorSpy: jest.SpyInstance
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockListener = jest.fn()
|
|
9
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
10
|
+
// Clear any existing global listener
|
|
11
|
+
Task.globalListener = undefined
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
// Clean up global listener after each test
|
|
16
|
+
Task.globalListener = undefined
|
|
17
|
+
consoleErrorSpy.mockRestore()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('static listenExecutionRecords method', () => {
|
|
21
|
+
it('should set global listener', () => {
|
|
22
|
+
Task.listenExecutionRecords(mockListener)
|
|
23
|
+
expect(Task.globalListener).toBe(mockListener)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should replace existing global listener', () => {
|
|
27
|
+
const firstListener = jest.fn()
|
|
28
|
+
const secondListener = jest.fn()
|
|
29
|
+
|
|
30
|
+
Task.listenExecutionRecords(firstListener)
|
|
31
|
+
expect(Task.globalListener).toBe(firstListener)
|
|
32
|
+
|
|
33
|
+
Task.listenExecutionRecords(secondListener)
|
|
34
|
+
expect(Task.globalListener).toBe(secondListener)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('global listener execution', () => {
|
|
39
|
+
it('should call global listener when task is executed via safeRun', async () => {
|
|
40
|
+
Task.listenExecutionRecords(mockListener)
|
|
41
|
+
|
|
42
|
+
const schema = new Schema({
|
|
43
|
+
value: Schema.number()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const testTask = createTask({
|
|
47
|
+
schema,
|
|
48
|
+
boundaries: {},
|
|
49
|
+
fn: async (input: { value: number }) => ({ result: input.value * 2 })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await testTask.safeRun({ value: 5 })
|
|
53
|
+
await new Promise(resolve => process.nextTick(resolve))
|
|
54
|
+
|
|
55
|
+
expect(mockListener).toHaveBeenCalledTimes(1)
|
|
56
|
+
expect(mockListener).toHaveBeenCalledWith(
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
input: { value: 5 },
|
|
59
|
+
output: { result: 10 },
|
|
60
|
+
type: 'success'
|
|
61
|
+
})
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should call global listener when task is executed via run', async () => {
|
|
66
|
+
Task.listenExecutionRecords(mockListener)
|
|
67
|
+
|
|
68
|
+
const schema = new Schema({
|
|
69
|
+
value: Schema.number()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const testTask = createTask({
|
|
73
|
+
schema,
|
|
74
|
+
boundaries: {},
|
|
75
|
+
fn: async (input: { value: number }) => ({ result: input.value * 2 })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
await testTask.run({ value: 3 })
|
|
79
|
+
await new Promise(resolve => process.nextTick(resolve))
|
|
80
|
+
|
|
81
|
+
expect(mockListener).toHaveBeenCalledTimes(1)
|
|
82
|
+
expect(mockListener).toHaveBeenCalledWith(
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
input: { value: 3 },
|
|
85
|
+
output: { result: 6 },
|
|
86
|
+
type: 'success'
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should call global listener when task fails', async () => {
|
|
92
|
+
Task.listenExecutionRecords(mockListener)
|
|
93
|
+
|
|
94
|
+
const schema = new Schema({})
|
|
95
|
+
|
|
96
|
+
const errorTask = createTask({
|
|
97
|
+
schema,
|
|
98
|
+
boundaries: {},
|
|
99
|
+
fn: async () => {
|
|
100
|
+
throw new Error('Test error')
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
await errorTask.safeRun({})
|
|
105
|
+
await new Promise(resolve => process.nextTick(resolve))
|
|
106
|
+
|
|
107
|
+
expect(mockListener).toHaveBeenCalledTimes(1)
|
|
108
|
+
expect(mockListener).toHaveBeenCalledWith(
|
|
109
|
+
expect.objectContaining({
|
|
110
|
+
input: {},
|
|
111
|
+
error: 'Test error',
|
|
112
|
+
type: 'error'
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should call both instance and global listeners', async () => {
|
|
118
|
+
const instanceListener = jest.fn()
|
|
119
|
+
Task.listenExecutionRecords(mockListener)
|
|
120
|
+
|
|
121
|
+
const schema = new Schema({
|
|
122
|
+
value: Schema.number()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const testTask = createTask({
|
|
126
|
+
schema,
|
|
127
|
+
boundaries: {},
|
|
128
|
+
fn: async (input: { value: number }) => ({ result: input.value * 2 })
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
testTask.addListener(instanceListener)
|
|
132
|
+
await testTask.safeRun({ value: 7 })
|
|
133
|
+
await new Promise(resolve => process.nextTick(resolve))
|
|
134
|
+
|
|
135
|
+
expect(instanceListener).toHaveBeenCalledTimes(1)
|
|
136
|
+
expect(mockListener).toHaveBeenCalledTimes(1)
|
|
137
|
+
|
|
138
|
+
const expectedRecord = expect.objectContaining({
|
|
139
|
+
input: { value: 7 },
|
|
140
|
+
output: { result: 14 },
|
|
141
|
+
type: 'success'
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
expect(instanceListener).toHaveBeenCalledWith(expectedRecord)
|
|
145
|
+
expect(mockListener).toHaveBeenCalledWith(expectedRecord)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should work when no global listener is set', async () => {
|
|
149
|
+
// No global listener set
|
|
150
|
+
const schema = new Schema({
|
|
151
|
+
value: Schema.number()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const testTask = createTask({
|
|
155
|
+
schema,
|
|
156
|
+
boundaries: {},
|
|
157
|
+
fn: async (input: { value: number }) => ({ result: input.value * 2 })
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Should not throw error
|
|
161
|
+
await expect(testTask.safeRun({ value: 1 })).resolves.toBeDefined()
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('async listener support', () => {
|
|
166
|
+
it('should support async global listeners', async () => {
|
|
167
|
+
const asyncListener = jest.fn().mockImplementation(async (_record) => {
|
|
168
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
169
|
+
return Promise.resolve()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
Task.listenExecutionRecords(asyncListener)
|
|
173
|
+
|
|
174
|
+
const schema = new Schema({
|
|
175
|
+
value: Schema.number()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const testTask = createTask({
|
|
179
|
+
schema,
|
|
180
|
+
boundaries: {},
|
|
181
|
+
fn: async (input: { value: number }) => ({ result: input.value * 2 })
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
await testTask.safeRun({ value: 4 })
|
|
185
|
+
await new Promise(resolve => process.nextTick(resolve))
|
|
186
|
+
|
|
187
|
+
expect(asyncListener).toHaveBeenCalledTimes(1)
|
|
188
|
+
expect(asyncListener).toHaveBeenCalledWith(
|
|
189
|
+
expect.objectContaining({
|
|
190
|
+
input: { value: 4 },
|
|
191
|
+
output: { result: 8 },
|
|
192
|
+
type: 'success'
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should handle long-running async listeners without blocking task execution', async () => {
|
|
198
|
+
const slowListener = jest.fn().mockImplementation(async () => {
|
|
199
|
+
await new Promise(resolve => setTimeout(resolve, 100)) // 100ms
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
Task.listenExecutionRecords(slowListener)
|
|
203
|
+
|
|
204
|
+
const schema = new Schema({
|
|
205
|
+
value: Schema.number()
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const testTask = createTask({
|
|
209
|
+
schema,
|
|
210
|
+
boundaries: {},
|
|
211
|
+
fn: async (input: { value: number }) => ({ result: input.value * 2 })
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const startTime = Date.now()
|
|
215
|
+
await testTask.safeRun({ value: 1 })
|
|
216
|
+
const endTime = Date.now()
|
|
217
|
+
|
|
218
|
+
// Task should complete quickly without waiting for listener
|
|
219
|
+
expect(endTime - startTime).toBeLessThan(50) // Should complete in under 50ms
|
|
220
|
+
|
|
221
|
+
// Wait for next tick to ensure listener was called
|
|
222
|
+
await new Promise(resolve => process.nextTick(resolve))
|
|
223
|
+
expect(slowListener).toHaveBeenCalledTimes(1)
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('error handling', () => {
|
|
228
|
+
it('should catch and log listener errors without affecting task execution', async () => {
|
|
229
|
+
const errorListener = jest.fn().mockImplementation(() => {
|
|
230
|
+
throw new Error('Listener error')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
Task.listenExecutionRecords(errorListener)
|
|
234
|
+
|
|
235
|
+
const schema = new Schema({
|
|
236
|
+
value: Schema.number()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const testTask = createTask({
|
|
240
|
+
schema,
|
|
241
|
+
boundaries: {},
|
|
242
|
+
fn: async (input: { value: number }) => ({ result: input.value * 2 })
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const [result, error] = await testTask.safeRun({ value: 5 })
|
|
246
|
+
await new Promise(resolve => process.nextTick(resolve))
|
|
247
|
+
|
|
248
|
+
// Task should complete successfully despite listener error
|
|
249
|
+
expect(result).toEqual({ result: 10 })
|
|
250
|
+
expect(error).toBeNull()
|
|
251
|
+
|
|
252
|
+
// Listener should have been called
|
|
253
|
+
expect(errorListener).toHaveBeenCalledTimes(1)
|
|
254
|
+
|
|
255
|
+
// Error should have been logged
|
|
256
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
257
|
+
'ExecutionRecord listener error:',
|
|
258
|
+
expect.any(Error)
|
|
259
|
+
)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should catch and log async listener errors', async () => {
|
|
263
|
+
const asyncErrorListener = jest.fn().mockImplementation(async () => {
|
|
264
|
+
throw new Error('Async listener error')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
Task.listenExecutionRecords(asyncErrorListener)
|
|
268
|
+
|
|
269
|
+
const schema = new Schema({
|
|
270
|
+
value: Schema.number()
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const testTask = createTask({
|
|
274
|
+
schema,
|
|
275
|
+
boundaries: {},
|
|
276
|
+
fn: async (input: { value: number }) => ({ result: input.value * 2 })
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const [result, error] = await testTask.safeRun({ value: 3 })
|
|
280
|
+
|
|
281
|
+
// Wait a bit for the async error to be logged
|
|
282
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
283
|
+
|
|
284
|
+
// Task should complete successfully
|
|
285
|
+
expect(result).toEqual({ result: 6 })
|
|
286
|
+
expect(error).toBeNull()
|
|
287
|
+
|
|
288
|
+
// Error should have been logged
|
|
289
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
290
|
+
'ExecutionRecord listener error:',
|
|
291
|
+
expect.any(Error)
|
|
292
|
+
)
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
})
|