@forgehive/task 0.1.6 → 0.1.8

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/src/index.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import { Schema, type SchemaType, type InferSchema, type SchemaDescription } from '@forgehive/schema'
2
- import { createBoundary, type Mode, type Boundaries, type WrappedBoundaries, type WrappedBoundaryFunction } from './utils/boundary'
2
+ import {
3
+ createBoundary,
4
+ type Mode,
5
+ type Boundaries,
6
+ type WrappedBoundaries,
7
+ type WrappedBoundaryFunction,
8
+ type BoundaryRecord,
9
+ type BoundaryTapeData
10
+ } from './utils/boundary'
3
11
 
4
12
  export interface Task {
5
13
  id: string;
@@ -11,7 +19,17 @@ export interface Task {
11
19
  export type BaseFunction = (...args: any[]) => any
12
20
 
13
21
  // Re-export the boundary types for external use
14
- export type { BoundaryFunction, WrappedBoundaryFunction, Boundaries, WrappedBoundaries, Mode } from './utils/boundary'
22
+ export type {
23
+ BoundaryFunction,
24
+ WrappedBoundaryFunction,
25
+ Boundaries,
26
+ WrappedBoundaries,
27
+ Mode,
28
+ BoundarySuccessRecord,
29
+ BoundaryErrorRecord,
30
+ BoundaryRecord,
31
+ BoundaryTapeData
32
+ } from './utils/boundary'
15
33
 
16
34
  // Re-export Schema for external use
17
35
  export { Schema }
@@ -20,7 +38,14 @@ export interface TaskConfig<B extends Boundaries = Boundaries> {
20
38
  schema?: Schema<Record<string, SchemaType>>
21
39
  mode?: Mode
22
40
  boundaries?: B
23
- boundariesData?: Record<string, unknown>
41
+ boundariesData?: BoundaryTapeData
42
+ }
43
+
44
+ // Interface for safeReplay configuration
45
+ export interface ReplayConfig<B extends Boundaries = Boundaries> {
46
+ boundaries: {
47
+ [K in keyof B]?: Mode
48
+ }
24
49
  }
25
50
 
26
51
  // ToDo: Add a type for the boundaries data
@@ -38,7 +63,33 @@ export interface TaskRecord<InputType = unknown, OutputType = unknown> {
38
63
  boundaries?: Record<string, unknown>;
39
64
  }
40
65
 
66
+ // Make BoundaryLog generic
67
+ export type BoundaryLog<I extends unknown[] = unknown[], O = unknown> = BoundaryRecord<I, O>;
68
+
69
+ // Mapped type for boundaries
70
+ export type BoundaryLogsFor<B extends Boundaries> = {
71
+ [K in keyof B]: B[K] extends (...args: infer I) => Promise<infer O>
72
+ ? BoundaryLog<I, O>[]
73
+ : BoundaryLog[]
74
+ }
75
+
76
+ /**
77
+ * Represents the execution record of a task, including input, output, error, and boundary data
78
+ */
79
+ export interface ExecutionRecord<InputType = unknown, OutputType = unknown, B extends Boundaries = Boundaries> {
80
+ /** The input arguments passed to the task */
81
+ input: InputType
82
+ /** The output returned by the task (if successful) */
83
+ output?: OutputType | null
84
+ /** The error message if the task failed */
85
+ error?: string
86
+ /** Boundary execution data */
87
+ boundaries: BoundaryLogsFor<B>
88
+ }
89
+
41
90
  export interface TaskInstanceType<Func extends BaseFunction = BaseFunction, B extends Boundaries = Boundaries> {
91
+ version: string
92
+
42
93
  getMode: () => Mode
43
94
  setMode: (mode: Mode) => void
44
95
  setSchema: (base: Schema<Record<string, SchemaType>>) => void
@@ -59,7 +110,7 @@ export interface TaskInstanceType<Func extends BaseFunction = BaseFunction, B ex
59
110
  // Boundary methods
60
111
  asBoundary: () => (args: Parameters<Func>[0]) => Promise<ReturnType<Func>>
61
112
  getBoundaries: () => WrappedBoundaries<B>
62
- setBoundariesData: (boundariesData: Record<string, unknown>) => void
113
+ setBoundariesData: (boundariesData: BoundaryTapeData) => void
63
114
  getBondariesData: () => Record<string, unknown>
64
115
 
65
116
  // Mocking methods for testing
@@ -68,9 +119,18 @@ export interface TaskInstanceType<Func extends BaseFunction = BaseFunction, B ex
68
119
  resetMocks: () => void
69
120
 
70
121
  run: (argv?: Parameters<Func>[0]) => Promise<ReturnType<Func>>
71
- safeRun: (argv?: Parameters<Func>[0]) => Promise<[Error | null, ReturnType<Func> | null, Record<string, unknown> | null]>
122
+ safeRun: (argv?: Parameters<Func>[0]) => Promise<[ReturnType<Func> | null, Error | null, ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>]>
123
+
124
+ // Method for replaying task execution
125
+ safeReplay: (
126
+ executionLog: ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>,
127
+ config: ReplayConfig<B>
128
+ ) => Promise<[ReturnType<Func> | null, Error | null, ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>]>
72
129
  }
73
130
 
131
+ // Define a type for the accumulated boundary data
132
+ type BoundaryData = Array<{input: unknown[], output?: unknown}>
133
+
74
134
  // Helper type to infer schema type
75
135
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
136
  export type InferSchemaType<S> = S extends Schema<any> ? InferSchema<S> : Record<string, unknown>;
@@ -80,20 +140,19 @@ export type TaskFunction<S, B extends Boundaries> =
80
140
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
141
  (argv: InferSchemaType<S>, boundaries: WrappedBoundaries<B>) => Promise<any>;
82
142
 
83
- // Define a type for the accumulated boundary data
84
- type BoundaryData = Array<{input: unknown[], output?: unknown}>
85
-
86
143
  export const Task = class Task<
87
144
  B extends Boundaries = Boundaries,
88
145
  Func extends BaseFunction = BaseFunction
89
146
  > implements TaskInstanceType<Func, B> {
147
+ public version: string = '0.1.7'
148
+
90
149
  _fn: Func
91
150
  _mode: Mode
92
151
  _coolDown: number
93
152
  _description?: string
94
153
 
95
154
  _boundariesDefinition: B
96
- _boundariesData: Record<string, unknown> | null
155
+ _boundariesData: BoundaryTapeData | null
97
156
  _accumulatedBoundariesData: Record<string, BoundaryData> = {}
98
157
 
99
158
  // For storing mocks
@@ -218,7 +277,7 @@ export const Task = class Task<
218
277
  })
219
278
  }
220
279
 
221
- setBoundariesData (boundariesData: Record<string, unknown>): void {
280
+ setBoundariesData (boundariesData: BoundaryTapeData): void {
222
281
  this._boundariesData = boundariesData
223
282
 
224
283
  // Update accumulated data as well
@@ -265,31 +324,36 @@ export const Task = class Task<
265
324
  _createBounderies ({
266
325
  definition,
267
326
  baseData,
268
- mode = 'proxy'
327
+ mode = 'proxy',
328
+ boundaryModes = {}
269
329
  }: {
270
330
  definition: B;
271
- baseData: Record<string, unknown> | null;
331
+ baseData: BoundaryTapeData | null;
272
332
  mode?: Mode;
333
+ boundaryModes?: Record<string, Mode | undefined>;
273
334
  }): WrappedBoundaries<B> {
274
335
  const boundariesFns: Record<string, WrappedBoundaryFunction> = {}
275
336
 
276
337
  for (const name in definition) {
338
+ // Get the configured mode for this boundary or use default
339
+ const boundaryMode = boundaryModes[name] || mode
340
+
277
341
  // Check if we have a mock for this boundary
278
342
  if (this._boundaryMocks[name]) {
279
343
  boundariesFns[name] = this._boundaryMocks[name]
280
344
  continue
281
345
  }
282
346
 
283
- // Otherwise create the normal boundary
347
+ // Create the boundary
284
348
  const boundary = createBoundary(definition[name])
285
349
 
286
350
  if (baseData !== null && typeof baseData[name] !== 'undefined') {
287
- const tape = baseData[name]
288
-
289
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
290
- boundary.setTape(tape as any)
351
+ const boundaryData = baseData[name] as Array<BoundaryRecord<Parameters<B[Extract<keyof B, string>]>, Awaited<ReturnType<B[Extract<keyof B, string>]>>>>
352
+ boundary.setTape(boundaryData)
291
353
  }
292
- boundary.setMode(mode as Mode)
354
+
355
+ // Set the mode after setting the tape
356
+ boundary.setMode(boundaryMode)
293
357
 
294
358
  boundariesFns[name] = boundary
295
359
  }
@@ -303,31 +367,15 @@ export const Task = class Task<
303
367
  }
304
368
  }
305
369
 
306
- async safeRun (argv?: Parameters<Func>[0]): Promise<[Error | null, ReturnType<Func> | null, Record<string, unknown> | null]> {
307
- // Handle schema validation
308
- if (this._schema) {
309
- try {
310
- const validation = this._schema.safeParse(argv)
311
- if (!validation.success) {
312
- const errorDetails = validation.error?.errors.map(err =>
313
- `${err.path.join('.')}: ${err.message}`
314
- ).join(', ')
315
-
316
- const errorMessage = errorDetails
317
- ? `Invalid input on: ${errorDetails}`
318
- : 'Invalid input'
319
-
320
- // Emit the validation error
321
- this.emit({
322
- input: argv,
323
- error: errorMessage
324
- })
325
-
326
- return [new Error(errorMessage), null, null]
327
- }
328
- } catch (error) {
329
- return [error instanceof Error ? error : new Error(String(error)), null, null]
330
- }
370
+ async safeRun (argv?: Parameters<Func>[0]): Promise<[
371
+ ReturnType<Func> | null,
372
+ Error | null,
373
+ ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>
374
+ ]> {
375
+ // Initialize log item
376
+ const logItem: ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B> = {
377
+ input: argv as Parameters<Func>[0],
378
+ boundaries: {} as BoundaryLogsFor<B>
331
379
  }
332
380
 
333
381
  // Create fresh boundaries for this execution
@@ -343,85 +391,199 @@ export const Task = class Task<
343
391
  boundary.startRun()
344
392
  }
345
393
 
394
+ // Handle schema validation
395
+ if (this._schema) {
396
+ const validation = this._schema.safeParse(argv)
397
+ if (!validation.success) {
398
+ const errorDetails = validation.error?.errors.map(err =>
399
+ `${err.path.join('.')}: ${err.message}`
400
+ ).join(', ')
401
+
402
+ const errorMessage = errorDetails
403
+ ? `Invalid input on: ${errorDetails}`
404
+ : 'Invalid input'
405
+
406
+ logItem.error = errorMessage
407
+ logItem.boundaries = {} as BoundaryLogsFor<B>
408
+
409
+ // Add boundary elements empty
410
+ for (const name in executionBoundaries) {
411
+ logItem.boundaries[name as keyof B] = [] as unknown as BoundaryLogsFor<B>[typeof name]
412
+ }
413
+
414
+ this.emit(logItem)
415
+ return [null, new Error(errorMessage), logItem]
416
+ }
417
+ }
418
+
419
+ let output: ReturnType<Func> | null = null
420
+ let error: Error | null = null
421
+
346
422
  try {
347
423
  // Execute the task function
348
- const output = await this._fn(
424
+ output = await this._fn(
349
425
  argv as Parameters<Func>[0],
350
426
  executionBoundaries as unknown as Parameters<Func>[1]
351
427
  )
352
428
 
353
- // Process boundary data after successful execution
354
- const boundariesRunLog: Record<string, unknown> = {}
429
+ logItem.output = output
430
+ } catch (caughtError) {
431
+ const errorMessage = caughtError instanceof Error ? caughtError.message : String(caughtError)
432
+ logItem.error = errorMessage
433
+ error = new Error(errorMessage)
434
+ }
355
435
 
356
- for (const name in executionBoundaries) {
357
- const boundary = executionBoundaries[name]
358
- const runData = boundary.getRunData()
436
+ // Process boundary data after execution (both success and error cases)
437
+ const boundariesRunLog: BoundaryLogsFor<B> = {} as BoundaryLogsFor<B>
359
438
 
360
- // Add to the run log
361
- boundariesRunLog[name] = runData
439
+ for (const name in executionBoundaries) {
440
+ const boundary = executionBoundaries[name]
441
+ const runData = boundary.getRunData()
362
442
 
363
- // Accumulate in the task's total boundaries data
364
- if (!this._accumulatedBoundariesData[name]) {
365
- this._accumulatedBoundariesData[name] = []
366
- }
443
+ // Add to the run log
444
+ boundariesRunLog[name as keyof B] = runData as unknown as BoundaryLogsFor<B>[typeof name]
445
+
446
+ // Accumulate in the task's total boundaries data
447
+ if (!this._accumulatedBoundariesData[name]) {
448
+ this._accumulatedBoundariesData[name] = []
449
+ }
367
450
 
368
- // Get the current accumulated data for this boundary
369
- const currentData = this._accumulatedBoundariesData[name]
451
+ // Get the current accumulated data for this boundary
452
+ const currentData = this._accumulatedBoundariesData[name]
370
453
 
371
- // Add the new run data
372
- if (Array.isArray(runData) && runData.length > 0) {
373
- // Cast the run data to the correct type
374
- this._accumulatedBoundariesData[name] = [...currentData, ...(runData as BoundaryData)]
375
- }
454
+ // Add the new run data
455
+ if (Array.isArray(runData) && runData.length > 0) {
456
+ // Cast the run data to the correct type
457
+ this._accumulatedBoundariesData[name] = [...currentData, ...(runData as BoundaryData)]
376
458
  }
459
+ }
377
460
 
378
- // Emit the success event with boundary data
379
- this.emit({
380
- input: argv,
381
- output,
382
- boundaries: boundariesRunLog
383
- })
461
+ // Set boundaries in log item before emitting
462
+ logItem.boundaries = boundariesRunLog
384
463
 
385
- return [null, output, boundariesRunLog]
386
- } catch (error) {
387
- // Process boundary data after error
388
- const boundariesRunLog: Record<string, unknown> = {}
464
+ // Emit the log item
465
+ this.emit(logItem)
389
466
 
390
- for (const name in executionBoundaries) {
391
- const boundary = executionBoundaries[name]
392
- const runData = boundary.getRunData()
467
+ // Return the error, output and log item
468
+ return [output, error, logItem]
469
+ }
393
470
 
394
- // Add to the run log
395
- boundariesRunLog[name] = runData
471
+ async safeReplay(
472
+ executionLog: ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>,
473
+ config: ReplayConfig<B> = {
474
+ boundaries: {}
475
+ }
476
+ ): Promise<[
477
+ ReturnType<Func> | null,
478
+ Error | null,
479
+ ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B>
480
+ ]> {
481
+ // Extract the input from the execution log
482
+ const argv = executionLog.input
483
+
484
+ // Initialize log item for this replay
485
+ const logItem: ExecutionRecord<Parameters<Func>[0], ReturnType<Func>, B> = {
486
+ input: argv,
487
+ boundaries: {} as BoundaryLogsFor<B>
488
+ }
396
489
 
397
- // Accumulate in the task's total boundaries data
398
- if (!this._accumulatedBoundariesData[name]) {
399
- this._accumulatedBoundariesData[name] = []
400
- }
490
+ // Create boundaries for this replay execution with custom modes based on config
491
+ const boundariesConfig: BoundaryTapeData = {}
401
492
 
402
- // Get the current accumulated data for this boundary
403
- const currentData = this._accumulatedBoundariesData[name]
493
+ // Setup boundary data for replay mode boundaries
494
+ for (const name in this._boundariesDefinition) {
495
+ // Check if this boundary is configured for replay mode
496
+ const mode = config.boundaries[name] || 'proxy'
404
497
 
405
- // Add the new run data
406
- if (Array.isArray(runData) && runData.length > 0) {
407
- // Cast the run data to the correct type
408
- this._accumulatedBoundariesData[name] = [...currentData, ...(runData as BoundaryData)]
409
- }
498
+ if (mode === 'replay' && executionLog.boundaries[name]) {
499
+ // Add boundary data from the execution log for replay mode
500
+ boundariesConfig[name] = executionLog.boundaries[name]
410
501
  }
502
+ }
411
503
 
412
- // Emit the error event with boundary data
413
- this.emit({
414
- input: argv,
415
- error: error instanceof Error ? error.message : String(error),
416
- boundaries: boundariesRunLog
417
- })
504
+ // Create fresh boundaries for this execution
505
+ const executionBoundaries = this._createBounderies({
506
+ definition: this._boundariesDefinition,
507
+ baseData: boundariesConfig,
508
+ mode: 'proxy',
509
+ boundaryModes: config.boundaries
510
+ })
418
511
 
419
- return [error instanceof Error ? error : new Error(String(error)), null, boundariesRunLog]
512
+ // Start run for each boundary
513
+ for (const name in executionBoundaries) {
514
+ const boundary = executionBoundaries[name]
515
+ boundary.startRun()
420
516
  }
517
+
518
+ // Handle schema validation - reusing the input from the execution log
519
+ if (this._schema) {
520
+ const validation = this._schema.safeParse(argv)
521
+ if (!validation.success) {
522
+ const errorDetails = validation.error?.errors.map(err =>
523
+ `${err.path.join('.')}: ${err.message}`
524
+ ).join(', ')
525
+
526
+ const errorMessage = errorDetails
527
+ ? `Invalid input on: ${errorDetails}`
528
+ : 'Invalid input'
529
+
530
+ logItem.error = errorMessage
531
+ logItem.output = executionLog.output // Keep the original output
532
+
533
+ // Copy the boundary data from the execution log
534
+ logItem.boundaries = executionLog.boundaries
535
+
536
+ this.emit(logItem)
537
+ return [null, new Error(errorMessage), logItem]
538
+ }
539
+ }
540
+
541
+ let output: ReturnType<Func> | null = null
542
+ let error: Error | null = null
543
+
544
+ try {
545
+ // Execute the task function with replay boundaries
546
+ output = await this._fn(
547
+ argv,
548
+ executionBoundaries as unknown as Parameters<Func>[1]
549
+ )
550
+
551
+ logItem.output = output
552
+ } catch (caughtError) {
553
+ const errorMessage = caughtError instanceof Error ? caughtError.message : String(caughtError)
554
+ logItem.error = errorMessage
555
+ error = new Error(errorMessage)
556
+ }
557
+
558
+ // Process boundary data after execution
559
+ const boundariesRunLog: BoundaryLogsFor<B> = {} as BoundaryLogsFor<B>
560
+
561
+ for (const name in executionBoundaries) {
562
+ const boundary = executionBoundaries[name]
563
+ const runData = boundary.getRunData()
564
+
565
+ // For boundaries in replay mode, use the original log data instead
566
+ const mode = config.boundaries[name] || 'proxy'
567
+ if (mode === 'replay' && executionLog.boundaries[name]) {
568
+ boundariesRunLog[name as keyof B] = executionLog.boundaries[name as keyof B]
569
+ } else {
570
+ // For other modes, use the actual run data
571
+ boundariesRunLog[name as keyof B] = runData as unknown as BoundaryLogsFor<B>[typeof name]
572
+ }
573
+ }
574
+
575
+ // Set boundaries in log item before emitting
576
+ logItem.boundaries = boundariesRunLog
577
+
578
+ // Emit the log item
579
+ this.emit(logItem)
580
+
581
+ // Return the output, error, and log item
582
+ return [output, error, logItem]
421
583
  }
422
584
 
423
585
  async run (argv?: Parameters<Func>[0]): Promise<ReturnType<Func>> {
424
- const [error, result] = await this.safeRun(argv)
586
+ const [result, error] = await this.safeRun(argv)
425
587
 
426
588
  if (error) {
427
589
  throw error