@forgehive/task 0.2.2 → 0.2.4

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.
Files changed (77) hide show
  1. package/dist/index.d.ts +12 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +66 -13
  4. package/dist/index.js.map +1 -1
  5. package/dist/test/add-listener-with-boundaries.test.js +78 -7
  6. package/dist/test/add-listener-with-boundaries.test.js.map +1 -1
  7. package/dist/test/add-listener.test.js +36 -0
  8. package/dist/test/add-listener.test.js.map +1 -1
  9. package/dist/test/boundary-modes.test.js +45 -5
  10. package/dist/test/boundary-modes.test.js.map +1 -1
  11. package/dist/test/execution-record-boundaries.test.js +12 -2
  12. package/dist/test/execution-record-boundaries.test.js.map +1 -1
  13. package/dist/test/integration-enhanced-records.test.d.ts +2 -0
  14. package/dist/test/integration-enhanced-records.test.d.ts.map +1 -0
  15. package/dist/test/integration-enhanced-records.test.js +467 -0
  16. package/dist/test/integration-enhanced-records.test.js.map +1 -0
  17. package/dist/test/listen-execution-records.test.d.ts +2 -0
  18. package/dist/test/listen-execution-records.test.d.ts.map +1 -0
  19. package/dist/test/listen-execution-records.test.js +223 -0
  20. package/dist/test/listen-execution-records.test.js.map +1 -0
  21. package/dist/test/metrics-collection.test.d.ts +2 -0
  22. package/dist/test/metrics-collection.test.d.ts.map +1 -0
  23. package/dist/test/metrics-collection.test.js +409 -0
  24. package/dist/test/metrics-collection.test.js.map +1 -0
  25. package/dist/test/performance-edge-cases.test.d.ts +2 -0
  26. package/dist/test/performance-edge-cases.test.d.ts.map +1 -0
  27. package/dist/test/performance-edge-cases.test.js +502 -0
  28. package/dist/test/performance-edge-cases.test.js.map +1 -0
  29. package/dist/test/run-boundary.test.js +27 -3
  30. package/dist/test/run-boundary.test.js.map +1 -1
  31. package/dist/test/safe-replay-complex-boundary.test.js +110 -9
  32. package/dist/test/safe-replay-complex-boundary.test.js.map +1 -1
  33. package/dist/test/safe-replay.test.js +35 -5
  34. package/dist/test/safe-replay.test.js.map +1 -1
  35. package/dist/test/safe-run.test.js +46 -4
  36. package/dist/test/safe-run.test.js.map +1 -1
  37. package/dist/test/setmetrics-boundary.test.d.ts +2 -0
  38. package/dist/test/setmetrics-boundary.test.d.ts.map +1 -0
  39. package/dist/test/setmetrics-boundary.test.js +195 -0
  40. package/dist/test/setmetrics-boundary.test.js.map +1 -0
  41. package/dist/test/task-with-boundaries.test.js +63 -2
  42. package/dist/test/task-with-boundaries.test.js.map +1 -1
  43. package/dist/test/timing-capture.test.d.ts +2 -0
  44. package/dist/test/timing-capture.test.d.ts.map +1 -0
  45. package/dist/test/timing-capture.test.js +304 -0
  46. package/dist/test/timing-capture.test.js.map +1 -0
  47. package/dist/test/timing-utilities.test.d.ts +2 -0
  48. package/dist/test/timing-utilities.test.d.ts.map +1 -0
  49. package/dist/test/timing-utilities.test.js +127 -0
  50. package/dist/test/timing-utilities.test.js.map +1 -0
  51. package/dist/types.d.ts +93 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +78 -0
  54. package/dist/types.js.map +1 -0
  55. package/dist/utils/boundary.d.ts +3 -0
  56. package/dist/utils/boundary.d.ts.map +1 -1
  57. package/dist/utils/boundary.js +11 -2
  58. package/dist/utils/boundary.js.map +1 -1
  59. package/package.json +3 -2
  60. package/src/index.ts +97 -14
  61. package/src/test/add-listener-with-boundaries.test.ts +78 -7
  62. package/src/test/add-listener.test.ts +36 -0
  63. package/src/test/boundary-modes.test.ts +45 -5
  64. package/src/test/execution-record-boundaries.test.ts +12 -2
  65. package/src/test/listen-execution-records.test.ts +295 -0
  66. package/src/test/metrics-collection.test.ts +476 -0
  67. package/src/test/performance-edge-cases.test.ts +596 -0
  68. package/src/test/run-boundary.test.ts +27 -3
  69. package/src/test/safe-replay-complex-boundary.test.ts +115 -10
  70. package/src/test/safe-replay.test.ts +35 -5
  71. package/src/test/safe-run.test.ts +46 -4
  72. package/src/test/setmetrics-boundary.test.ts +223 -0
  73. package/src/test/task-with-boundaries.test.ts +71 -5
  74. package/src/test/timing-capture.test.ts +348 -0
  75. package/src/test/timing-utilities.test.ts +145 -0
  76. package/src/types.ts +139 -0
  77. package/src/utils/boundary.ts +15 -2
@@ -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
+ })