@forgehive/task 0.2.3 → 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 (72) hide show
  1. package/dist/index.d.ts +9 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +38 -9
  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/metrics-collection.test.d.ts +2 -0
  18. package/dist/test/metrics-collection.test.d.ts.map +1 -0
  19. package/dist/test/metrics-collection.test.js +409 -0
  20. package/dist/test/metrics-collection.test.js.map +1 -0
  21. package/dist/test/performance-edge-cases.test.d.ts +2 -0
  22. package/dist/test/performance-edge-cases.test.d.ts.map +1 -0
  23. package/dist/test/performance-edge-cases.test.js +502 -0
  24. package/dist/test/performance-edge-cases.test.js.map +1 -0
  25. package/dist/test/run-boundary.test.js +27 -3
  26. package/dist/test/run-boundary.test.js.map +1 -1
  27. package/dist/test/safe-replay-complex-boundary.test.js +110 -9
  28. package/dist/test/safe-replay-complex-boundary.test.js.map +1 -1
  29. package/dist/test/safe-replay.test.js +35 -5
  30. package/dist/test/safe-replay.test.js.map +1 -1
  31. package/dist/test/safe-run.test.js +46 -4
  32. package/dist/test/safe-run.test.js.map +1 -1
  33. package/dist/test/setmetrics-boundary.test.d.ts +2 -0
  34. package/dist/test/setmetrics-boundary.test.d.ts.map +1 -0
  35. package/dist/test/setmetrics-boundary.test.js +195 -0
  36. package/dist/test/setmetrics-boundary.test.js.map +1 -0
  37. package/dist/test/task-with-boundaries.test.js +63 -2
  38. package/dist/test/task-with-boundaries.test.js.map +1 -1
  39. package/dist/test/timing-capture.test.d.ts +2 -0
  40. package/dist/test/timing-capture.test.d.ts.map +1 -0
  41. package/dist/test/timing-capture.test.js +304 -0
  42. package/dist/test/timing-capture.test.js.map +1 -0
  43. package/dist/test/timing-utilities.test.d.ts +2 -0
  44. package/dist/test/timing-utilities.test.d.ts.map +1 -0
  45. package/dist/test/timing-utilities.test.js +127 -0
  46. package/dist/test/timing-utilities.test.js.map +1 -0
  47. package/dist/types.d.ts +93 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +78 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/utils/boundary.d.ts +3 -0
  52. package/dist/utils/boundary.d.ts.map +1 -1
  53. package/dist/utils/boundary.js +11 -2
  54. package/dist/utils/boundary.js.map +1 -1
  55. package/package.json +3 -2
  56. package/src/index.ts +63 -8
  57. package/src/test/add-listener-with-boundaries.test.ts +78 -7
  58. package/src/test/add-listener.test.ts +36 -0
  59. package/src/test/boundary-modes.test.ts +45 -5
  60. package/src/test/execution-record-boundaries.test.ts +12 -2
  61. package/src/test/metrics-collection.test.ts +476 -0
  62. package/src/test/performance-edge-cases.test.ts +596 -0
  63. package/src/test/run-boundary.test.ts +27 -3
  64. package/src/test/safe-replay-complex-boundary.test.ts +115 -10
  65. package/src/test/safe-replay.test.ts +35 -5
  66. package/src/test/safe-run.test.ts +46 -4
  67. package/src/test/setmetrics-boundary.test.ts +223 -0
  68. package/src/test/task-with-boundaries.test.ts +71 -5
  69. package/src/test/timing-capture.test.ts +348 -0
  70. package/src/test/timing-utilities.test.ts +145 -0
  71. package/src/types.ts +139 -0
  72. package/src/utils/boundary.ts +15 -2
@@ -1,5 +1,5 @@
1
1
  import { Schema } from '@forgehive/schema'
2
- import { createTask, ExecutionRecord, getExecutionRecordType } from '../index'
2
+ import { createTask, ExecutionRecord, getExecutionRecordType, BoundaryRecord } from '../index'
3
3
 
4
4
  describe('Complex boundary replay tests', () => {
5
5
  // Helper function to create ExecutionRecord with computed type
@@ -190,21 +190,41 @@ describe('Complex boundary replay tests', () => {
190
190
  { ticker: 'MSFT', quantity: 5 },
191
191
  { ticker: 'GOOGL', quantity: 8 }
192
192
  ]
193
+ },
194
+ timing: {
195
+ startTime: 1000,
196
+ endTime: 1100,
197
+ duration: 100
193
198
  }
194
199
  }
195
200
  ],
196
201
  fetchPrice: [
197
202
  {
198
203
  input: ['AAPL'],
199
- output: 190.50
204
+ output: 190.50,
205
+ timing: {
206
+ startTime: 1100,
207
+ endTime: 1200,
208
+ duration: 100
209
+ }
200
210
  },
201
211
  {
202
212
  input: ['MSFT'],
203
- output: 405.75
213
+ output: 405.75,
214
+ timing: {
215
+ startTime: 1200,
216
+ endTime: 1300,
217
+ duration: 100
218
+ }
204
219
  },
205
220
  {
206
221
  input: ['GOOGL'],
207
- output: 161.16
222
+ output: 161.16,
223
+ timing: {
224
+ startTime: 1300,
225
+ endTime: 1400,
226
+ duration: 100
227
+ }
208
228
  }
209
229
  ]
210
230
  }
@@ -239,6 +259,22 @@ describe('Complex boundary replay tests', () => {
239
259
  // Verify that boundary calls were properly replayed
240
260
  expect(replayLog.boundaries.fetchPortfolio).toHaveLength(1)
241
261
  expect(replayLog.boundaries.fetchPrice).toHaveLength(3)
262
+
263
+ // Verify timing information is preserved
264
+ expect(replayLog.boundaries.fetchPortfolio[0]).toMatchObject({
265
+ timing: expect.objectContaining({
266
+ startTime: expect.any(Number),
267
+ endTime: expect.any(Number),
268
+ duration: expect.any(Number)
269
+ })
270
+ })
271
+ expect(replayLog.boundaries.fetchPrice[0]).toMatchObject({
272
+ timing: expect.objectContaining({
273
+ startTime: expect.any(Number),
274
+ endTime: expect.any(Number),
275
+ duration: expect.any(Number)
276
+ })
277
+ })
242
278
  })
243
279
 
244
280
  it('Should handle errors during replay', async () => {
@@ -258,21 +294,41 @@ describe('Complex boundary replay tests', () => {
258
294
  { ticker: 'MSFT', quantity: 5 },
259
295
  { ticker: 'GOOGL', quantity: 8 }
260
296
  ]
297
+ },
298
+ timing: {
299
+ startTime: 2000,
300
+ endTime: 2100,
301
+ duration: 100
261
302
  }
262
303
  }
263
304
  ],
264
305
  fetchPrice: [
265
306
  {
266
307
  input: ['AAPL'],
267
- output: 190.50
308
+ output: 190.50,
309
+ timing: {
310
+ startTime: 2100,
311
+ endTime: 2200,
312
+ duration: 100
313
+ }
268
314
  },
269
315
  {
270
316
  input: ['MSFT'],
271
- output: 405.75
317
+ output: 405.75,
318
+ timing: {
319
+ startTime: 2200,
320
+ endTime: 2300,
321
+ duration: 100
322
+ }
272
323
  },
273
324
  {
274
325
  input: ['GOOGL'],
275
- error: 'Price data not available for ticker: GOOGL'
326
+ error: 'Price data not available for ticker: GOOGL',
327
+ timing: {
328
+ startTime: 2300,
329
+ endTime: 2350,
330
+ duration: 50
331
+ }
276
332
  }
277
333
  ]
278
334
  }
@@ -304,6 +360,15 @@ describe('Complex boundary replay tests', () => {
304
360
  )
305
361
  expect(googPriceCall).toBeDefined()
306
362
  expect(googPriceCall?.error).toBe('Price data not available for ticker: GOOGL')
363
+
364
+ // Verify timing information is preserved even with errors
365
+ expect(googPriceCall).toMatchObject({
366
+ timing: expect.objectContaining({
367
+ startTime: expect.any(Number),
368
+ endTime: expect.any(Number),
369
+ duration: expect.any(Number)
370
+ })
371
+ })
307
372
  })
308
373
 
309
374
  it('Should support mixed replay with some boundaries in replay mode and others in proxy mode', async () => {
@@ -335,21 +400,41 @@ describe('Complex boundary replay tests', () => {
335
400
  { ticker: 'MSFT', quantity: 5 },
336
401
  { ticker: 'GOOGL', quantity: 8 }
337
402
  ]
403
+ },
404
+ timing: {
405
+ startTime: 3000,
406
+ endTime: 3100,
407
+ duration: 100
338
408
  }
339
409
  }
340
410
  ],
341
411
  fetchPrice: [
342
412
  {
343
413
  input: ['AAPL'],
344
- output: 190.50
414
+ output: 190.50,
415
+ timing: {
416
+ startTime: 3100,
417
+ endTime: 3200,
418
+ duration: 100
419
+ }
345
420
  },
346
421
  {
347
422
  input: ['MSFT'],
348
- output: 405.75
423
+ output: 405.75,
424
+ timing: {
425
+ startTime: 3200,
426
+ endTime: 3300,
427
+ duration: 100
428
+ }
349
429
  },
350
430
  {
351
431
  input: ['GOOGL'],
352
- output: 161.16
432
+ output: 161.16,
433
+ timing: {
434
+ startTime: 3300,
435
+ endTime: 3400,
436
+ duration: 100
437
+ }
353
438
  }
354
439
  ]
355
440
  }
@@ -389,5 +474,25 @@ describe('Complex boundary replay tests', () => {
389
474
  // Verify the log shows the mixed sources correctly
390
475
  expect(replayLog.boundaries.fetchPortfolio).toHaveLength(1)
391
476
  expect(replayLog.boundaries.fetchPrice).toHaveLength(3)
477
+
478
+ // Verify timing information for mixed execution
479
+ expect(replayLog.boundaries.fetchPortfolio[0]).toMatchObject({
480
+ timing: expect.objectContaining({
481
+ startTime: expect.any(Number),
482
+ endTime: expect.any(Number),
483
+ duration: expect.any(Number)
484
+ })
485
+ })
486
+
487
+ // All price fetches should have timing since they execute in proxy mode
488
+ replayLog.boundaries.fetchPrice.forEach((call: BoundaryRecord) => {
489
+ expect(call).toMatchObject({
490
+ timing: expect.objectContaining({
491
+ startTime: expect.any(Number),
492
+ endTime: expect.any(Number),
493
+ duration: expect.any(Number)
494
+ })
495
+ })
496
+ })
392
497
  })
393
498
  })
@@ -72,7 +72,12 @@ describe('safeReplay functionality tests', () => {
72
72
  fetchData: [
73
73
  {
74
74
  input: ['AAPL'],
75
- output: 160.23
75
+ output: 160.23,
76
+ timing: {
77
+ startTime: 1000,
78
+ endTime: 1100,
79
+ duration: 100
80
+ }
76
81
  }
77
82
  ]
78
83
  }
@@ -102,6 +107,11 @@ describe('safeReplay functionality tests', () => {
102
107
  {
103
108
  input: ['AAPL'],
104
109
  output: 150.23,
110
+ timing: expect.objectContaining({
111
+ startTime: expect.any(Number),
112
+ endTime: expect.any(Number),
113
+ duration: expect.any(Number)
114
+ })
105
115
  }
106
116
  ]
107
117
  }
@@ -120,7 +130,12 @@ describe('safeReplay functionality tests', () => {
120
130
  fetchData: [
121
131
  {
122
132
  input: ['AAPL'],
123
- output: 160.23
133
+ output: 160.23,
134
+ timing: {
135
+ startTime: 1000,
136
+ endTime: 1100,
137
+ duration: 100
138
+ }
124
139
  }
125
140
  ]
126
141
  }
@@ -153,7 +168,12 @@ describe('safeReplay functionality tests', () => {
153
168
  fetchData: [
154
169
  {
155
170
  input: ['AAPL'],
156
- output: 160.23
171
+ output: 160.23,
172
+ timing: expect.objectContaining({
173
+ startTime: expect.any(Number),
174
+ endTime: expect.any(Number),
175
+ duration: expect.any(Number)
176
+ })
157
177
  }
158
178
  ]
159
179
  }
@@ -170,7 +190,12 @@ describe('safeReplay functionality tests', () => {
170
190
  fetchData: [
171
191
  {
172
192
  input: ['AAPL'],
173
- error: 'API error: Rate limit exceeded'
193
+ error: 'API error: Rate limit exceeded',
194
+ timing: {
195
+ startTime: 1000,
196
+ endTime: 1100,
197
+ duration: 100
198
+ }
174
199
  }
175
200
  ]
176
201
  }
@@ -206,7 +231,12 @@ describe('safeReplay functionality tests', () => {
206
231
  {
207
232
  input: ['AAPL'],
208
233
  output: 160.23,
209
- error: null
234
+ error: null,
235
+ timing: {
236
+ startTime: 1000,
237
+ endTime: 1100,
238
+ duration: 100
239
+ }
210
240
  }
211
241
  ]
212
242
  }
@@ -40,6 +40,11 @@ describe('Task safeRun tests', () => {
40
40
  expect(data.input).toEqual([5])
41
41
  expect(data.output).toEqual(10)
42
42
  expect(data.error).toBeUndefined()
43
+ expect(data.timing).toEqual(expect.objectContaining({
44
+ startTime: expect.any(Number),
45
+ endTime: expect.any(Number),
46
+ duration: expect.any(Number)
47
+ }))
43
48
  })
44
49
 
45
50
  it('returns [null, error, record] on failed execution', async () => {
@@ -87,6 +92,11 @@ describe('Task safeRun tests', () => {
87
92
  expect(data.input).toEqual([-5])
88
93
  expect(data.error).toContain('Value cannot be negative')
89
94
  expect(data.output).toBeUndefined()
95
+ expect(data.timing).toEqual(expect.objectContaining({
96
+ startTime: expect.any(Number),
97
+ endTime: expect.any(Number),
98
+ duration: expect.any(Number)
99
+ }))
90
100
  })
91
101
 
92
102
  it('returns [null, error, record] on schema validation failure', async () => {
@@ -238,9 +248,41 @@ describe('Task safeRun tests', () => {
238
248
 
239
249
  expect(record.boundaries.doubleValue).toHaveLength(3)
240
250
  expect(record.boundaries.sumValues).toHaveLength(1)
241
- expect(record.boundaries.doubleValue[0]).toEqual({ input: [1], output: 2 })
242
- expect(record.boundaries.doubleValue[1]).toEqual({ input: [2], output: 4 })
243
- expect(record.boundaries.doubleValue[2]).toEqual({ input: [3], output: 6 })
244
- expect(record.boundaries.sumValues[0]).toEqual({ input: [[2, 4, 6]], output: 12 })
251
+ expect(record.boundaries.doubleValue[0]).toEqual({
252
+ input: [1],
253
+ output: 2,
254
+ timing: expect.objectContaining({
255
+ startTime: expect.any(Number),
256
+ endTime: expect.any(Number),
257
+ duration: expect.any(Number)
258
+ })
259
+ })
260
+ expect(record.boundaries.doubleValue[1]).toEqual({
261
+ input: [2],
262
+ output: 4,
263
+ timing: expect.objectContaining({
264
+ startTime: expect.any(Number),
265
+ endTime: expect.any(Number),
266
+ duration: expect.any(Number)
267
+ })
268
+ })
269
+ expect(record.boundaries.doubleValue[2]).toEqual({
270
+ input: [3],
271
+ output: 6,
272
+ timing: expect.objectContaining({
273
+ startTime: expect.any(Number),
274
+ endTime: expect.any(Number),
275
+ duration: expect.any(Number)
276
+ })
277
+ })
278
+ expect(record.boundaries.sumValues[0]).toEqual({
279
+ input: [[2, 4, 6]],
280
+ output: 12,
281
+ timing: expect.objectContaining({
282
+ startTime: expect.any(Number),
283
+ endTime: expect.any(Number),
284
+ duration: expect.any(Number)
285
+ })
286
+ })
245
287
  })
246
288
  })
@@ -0,0 +1,223 @@
1
+ import { createTask, type Metric, validateMetric, createMetric } from '../index'
2
+ import { Schema } from '@forgehive/schema'
3
+
4
+ describe('setMetrics boundary', () => {
5
+ describe('validateMetric function', () => {
6
+ it('should validate correct metrics', () => {
7
+ const validMetric: Metric = {
8
+ type: 'performance',
9
+ name: 'response_time',
10
+ value: 150
11
+ }
12
+
13
+ expect(validateMetric(validMetric)).toBe(true)
14
+ })
15
+
16
+ it('should reject invalid metrics', () => {
17
+ expect(validateMetric(null)).toBe(false)
18
+ expect(validateMetric(undefined)).toBe(false)
19
+ expect(validateMetric({})).toBe(false)
20
+ expect(validateMetric({ type: '', name: 'test', value: 1 })).toBe(false)
21
+ expect(validateMetric({ type: 'test', name: '', value: 1 })).toBe(false)
22
+ expect(validateMetric({ type: 'test', name: 'test', value: NaN })).toBe(false)
23
+ expect(validateMetric({ type: 'test', name: 'test', value: Infinity })).toBe(false)
24
+ })
25
+ })
26
+
27
+ describe('createMetric function', () => {
28
+ it('should create valid metrics', () => {
29
+ const metric = createMetric('performance', 'response_time', 150)
30
+
31
+ expect(metric).toEqual({
32
+ type: 'performance',
33
+ name: 'response_time',
34
+ value: 150
35
+ })
36
+ })
37
+
38
+ it('should throw for invalid metrics', () => {
39
+ expect(() => createMetric('', 'test', 1)).toThrow('Invalid metric')
40
+ expect(() => createMetric('test', '', 1)).toThrow('Invalid metric')
41
+ expect(() => createMetric('test', 'test', NaN)).toThrow('Invalid metric')
42
+ })
43
+ })
44
+
45
+ describe('setMetrics boundary integration', () => {
46
+ it('should collect metrics during task execution', async () => {
47
+ const schema = new Schema({
48
+ userId: Schema.string()
49
+ })
50
+
51
+ const task = createTask({
52
+ name: 'getUserData',
53
+ schema,
54
+ boundaries: {
55
+ fetchUser: async (userId: string) => ({ id: userId, name: 'Test User' })
56
+ },
57
+ fn: async ({ userId }, { fetchUser, setMetrics }) => {
58
+ // Add a performance metric
59
+ await setMetrics({
60
+ type: 'performance',
61
+ name: 'api_response_time',
62
+ value: 250
63
+ })
64
+
65
+ const user = await fetchUser(userId)
66
+
67
+ // Add a business metric
68
+ await setMetrics({
69
+ type: 'business',
70
+ name: 'users_processed',
71
+ value: 1
72
+ })
73
+
74
+ return { user }
75
+ }
76
+ })
77
+
78
+ const [result, error, record] = await task.safeRun({ userId: 'user123' })
79
+
80
+ expect(error).toBeNull()
81
+ expect(result).toEqual({
82
+ user: { id: 'user123', name: 'Test User' }
83
+ })
84
+
85
+ // Check that metrics were collected
86
+ expect(record.metrics).toHaveLength(2)
87
+ expect(record.metrics?.[0]).toEqual({
88
+ type: 'performance',
89
+ name: 'api_response_time',
90
+ value: 250
91
+ })
92
+ expect(record.metrics?.[1]).toEqual({
93
+ type: 'business',
94
+ name: 'users_processed',
95
+ value: 1
96
+ })
97
+
98
+ // Check that timing was captured
99
+ expect(record.timing).toEqual(expect.objectContaining({
100
+ startTime: expect.any(Number),
101
+ endTime: expect.any(Number),
102
+ duration: expect.any(Number)
103
+ }))
104
+ })
105
+
106
+ it('should validate metrics and reject invalid ones', async () => {
107
+ const schema = new Schema({
108
+ value: Schema.number()
109
+ })
110
+
111
+ const task = createTask({
112
+ name: 'testValidation',
113
+ schema,
114
+ boundaries: {},
115
+ fn: async ({ value }, { setMetrics }) => {
116
+ // Try to set an invalid metric
117
+ await setMetrics({
118
+ type: '',
119
+ name: 'invalid_metric',
120
+ value: value
121
+ } as Metric)
122
+
123
+ return { result: value }
124
+ }
125
+ })
126
+
127
+ const [result, error, record] = await task.safeRun({ value: 42 })
128
+
129
+ expect(result).toBeNull()
130
+ expect(error).not.toBeNull()
131
+ expect(error?.message).toContain('Invalid metric provided')
132
+ expect(record.type).toBe('error')
133
+ })
134
+
135
+ it('should accumulate multiple metrics correctly', async () => {
136
+ const schema = new Schema({
137
+ count: Schema.number()
138
+ })
139
+
140
+ const task = createTask({
141
+ name: 'accumulateMetrics',
142
+ schema,
143
+ boundaries: {},
144
+ fn: async ({ count }, { setMetrics }) => {
145
+ // Add metrics in a loop
146
+ for (let i = 0; i < count; i++) {
147
+ await setMetrics({
148
+ type: 'processing',
149
+ name: `item_${i}`,
150
+ value: i * 10
151
+ })
152
+ }
153
+
154
+ return { processedCount: count }
155
+ }
156
+ })
157
+
158
+ const [result, error, record] = await task.safeRun({ count: 3 })
159
+
160
+ expect(error).toBeNull()
161
+ expect(result).toEqual({ processedCount: 3 })
162
+
163
+ // Check that all metrics were collected
164
+ expect(record.metrics).toHaveLength(3)
165
+ expect(record.metrics?.[0]).toEqual({
166
+ type: 'processing',
167
+ name: 'item_0',
168
+ value: 0
169
+ })
170
+ expect(record.metrics?.[1]).toEqual({
171
+ type: 'processing',
172
+ name: 'item_1',
173
+ value: 10
174
+ })
175
+ expect(record.metrics?.[2]).toEqual({
176
+ type: 'processing',
177
+ name: 'item_2',
178
+ value: 20
179
+ })
180
+ })
181
+
182
+ it('should not include setMetrics calls in boundary logs', async () => {
183
+ const schema = new Schema({
184
+ value: Schema.number()
185
+ })
186
+
187
+ const task = createTask({
188
+ name: 'metricsNotInBoundaries',
189
+ schema,
190
+ boundaries: {
191
+ calculate: async (val: number) => val * 2
192
+ },
193
+ fn: async ({ value }, { calculate, setMetrics }) => {
194
+ const result = await calculate(value)
195
+
196
+ await setMetrics({
197
+ type: 'calculation',
198
+ name: 'result_value',
199
+ value: result
200
+ })
201
+
202
+ return { result }
203
+ }
204
+ })
205
+
206
+ const [, error, record] = await task.safeRun({ value: 5 })
207
+
208
+ expect(error).toBeNull()
209
+
210
+ // Verify that setMetrics is not in the boundary logs
211
+ expect(record.boundaries).toHaveProperty('calculate')
212
+ expect(record.boundaries).not.toHaveProperty('setMetrics')
213
+
214
+ // But metrics should be collected
215
+ expect(record.metrics).toHaveLength(1)
216
+ expect(record.metrics?.[0]).toEqual({
217
+ type: 'calculation',
218
+ name: 'result_value',
219
+ value: 10
220
+ })
221
+ })
222
+ })
223
+ })