@forgehive/task 0.1.5 → 0.1.6

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
@@ -23,6 +23,7 @@ export interface TaskConfig<B extends Boundaries = Boundaries> {
23
23
  boundariesData?: Record<string, unknown>
24
24
  }
25
25
 
26
+ // ToDo: Add a type for the boundaries data
26
27
  /**
27
28
  * Represents the record passed to task listeners
28
29
  */
@@ -60,9 +61,14 @@ export interface TaskInstanceType<Func extends BaseFunction = BaseFunction, B ex
60
61
  getBoundaries: () => WrappedBoundaries<B>
61
62
  setBoundariesData: (boundariesData: Record<string, unknown>) => void
62
63
  getBondariesData: () => Record<string, unknown>
63
- getBondariesRunLog: () => Record<string, unknown>
64
- startRunLog: () => void
64
+
65
+ // Mocking methods for testing
66
+ mockBoundary: <K extends keyof B>(name: K, mockFn: WrappedBoundaryFunction) => void
67
+ resetMock: <K extends keyof B>(name: K) => void
68
+ resetMocks: () => void
69
+
65
70
  run: (argv?: Parameters<Func>[0]) => Promise<ReturnType<Func>>
71
+ safeRun: (argv?: Parameters<Func>[0]) => Promise<[Error | null, ReturnType<Func> | null, Record<string, unknown> | null]>
66
72
  }
67
73
 
68
74
  // Helper type to infer schema type
@@ -74,6 +80,9 @@ export type TaskFunction<S, B extends Boundaries> =
74
80
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
81
  (argv: InferSchemaType<S>, boundaries: WrappedBoundaries<B>) => Promise<any>;
76
82
 
83
+ // Define a type for the accumulated boundary data
84
+ type BoundaryData = Array<{input: unknown[], output?: unknown}>
85
+
77
86
  export const Task = class Task<
78
87
  B extends Boundaries = Boundaries,
79
88
  Func extends BaseFunction = BaseFunction
@@ -84,8 +93,11 @@ export const Task = class Task<
84
93
  _description?: string
85
94
 
86
95
  _boundariesDefinition: B
87
- _boundaries: WrappedBoundaries<B>
88
96
  _boundariesData: Record<string, unknown> | null
97
+ _accumulatedBoundariesData: Record<string, BoundaryData> = {}
98
+
99
+ // For storing mocks
100
+ _boundaryMocks: Record<string, WrappedBoundaryFunction> = {}
89
101
 
90
102
  _schema: Schema<Record<string, SchemaType>> | undefined
91
103
  _listener?: ((record: TaskRecord<Parameters<Func>[0], ReturnType<Func>>) => void) | undefined
@@ -110,13 +122,25 @@ export const Task = class Task<
110
122
  // Cool down time before killing the process on cli runner
111
123
  this._coolDown = 1000
112
124
 
113
- // Review this assignment
125
+ // Initialize boundaries data
114
126
  this._boundariesData = conf.boundariesData ?? null
115
- this._boundaries = this._createBounderies({
116
- definition: this._boundariesDefinition,
117
- baseData: this._boundariesData,
118
- mode: this._mode
119
- })
127
+
128
+ // Initialize empty accumulated boundaries data structure
129
+ for (const name in this._boundariesDefinition) {
130
+ this._accumulatedBoundariesData[name] = []
131
+ }
132
+
133
+ // Initialize accumulated boundaries data from initial boundaries data
134
+ if (this._boundariesData) {
135
+ // Type assertion to handle initial data safely
136
+ for (const name in this._boundariesData) {
137
+ if (Array.isArray(this._boundariesData[name])) {
138
+ this._accumulatedBoundariesData[name] = this._boundariesData[name] as BoundaryData
139
+ } else {
140
+ this._accumulatedBoundariesData[name] = []
141
+ }
142
+ }
143
+ }
120
144
  }
121
145
 
122
146
  getMode (): Mode {
@@ -124,12 +148,6 @@ export const Task = class Task<
124
148
  }
125
149
 
126
150
  setMode (mode: Mode): void {
127
- for (const name in this._boundaries) {
128
- const boundary = this._boundaries[name]
129
-
130
- boundary.setMode(mode)
131
- }
132
-
133
151
  this._mode = mode
134
152
  }
135
153
 
@@ -188,45 +206,60 @@ export const Task = class Task<
188
206
  emit (data: Partial<TaskRecord>): void {
189
207
  if (typeof this._listener === 'undefined') { return }
190
208
 
191
- const event = {
192
- ...data,
193
- boundaries: this.getBondariesRunLog()
194
- } as TaskRecord<Parameters<Func>[0], ReturnType<Func>>
195
-
196
- this._listener(event)
209
+ this._listener(data as TaskRecord<Parameters<Func>[0], ReturnType<Func>>)
197
210
  }
198
211
 
199
212
  getBoundaries (): WrappedBoundaries<B> {
200
- return this._boundaries
213
+ // Create fresh boundaries when requested
214
+ return this._createBounderies({
215
+ definition: this._boundariesDefinition,
216
+ baseData: this._boundariesData,
217
+ mode: this._mode
218
+ })
201
219
  }
202
220
 
203
221
  setBoundariesData (boundariesData: Record<string, unknown>): void {
204
- for (const name in this._boundaries) {
205
- const boundary = this._boundaries[name]
206
-
207
- let tape
208
- if (typeof boundariesData !== 'undefined') {
209
- tape = boundariesData[name]
210
- }
211
-
212
- if (typeof boundary !== 'undefined' && typeof tape !== 'undefined') {
213
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
214
- boundary.setTape(tape as any)
222
+ this._boundariesData = boundariesData
223
+
224
+ // Update accumulated data as well
225
+ // Type assertion to handle provided data safely
226
+ for (const name in boundariesData) {
227
+ if (Array.isArray(boundariesData[name])) {
228
+ this._accumulatedBoundariesData[name] = boundariesData[name] as BoundaryData
229
+ } else {
230
+ this._accumulatedBoundariesData[name] = []
215
231
  }
216
232
  }
217
233
  }
218
234
 
219
235
  getBondariesData (): Record<string, unknown> {
220
- const boundaries = this._boundaries
221
- const boundariesData: Record<string, unknown> = {}
236
+ return this._accumulatedBoundariesData
237
+ }
222
238
 
223
- for (const name in boundaries) {
224
- const boundary = boundaries[name]
239
+ /**
240
+ * Mocks a specific boundary function for testing
241
+ * @param name The name of the boundary to mock
242
+ * @param mockFn The mock function to use
243
+ */
244
+ mockBoundary<K extends keyof B>(name: K, mockFn: WrappedBoundaryFunction): void {
245
+ this._boundaryMocks[name as string] = mockFn
246
+ }
225
247
 
226
- boundariesData[name] = boundary.getTape()
248
+ /**
249
+ * Resets a specific mocked boundary back to its original function
250
+ * @param name The name of the boundary to reset
251
+ */
252
+ resetMock<K extends keyof B>(name: K): void {
253
+ if (this._boundaryMocks[name as string]) {
254
+ delete this._boundaryMocks[name as string]
227
255
  }
256
+ }
228
257
 
229
- return boundariesData
258
+ /**
259
+ * Resets all mocked boundaries back to their original functions
260
+ */
261
+ resetMocks(): void {
262
+ this._boundaryMocks = {}
230
263
  }
231
264
 
232
265
  _createBounderies ({
@@ -241,6 +274,13 @@ export const Task = class Task<
241
274
  const boundariesFns: Record<string, WrappedBoundaryFunction> = {}
242
275
 
243
276
  for (const name in definition) {
277
+ // Check if we have a mock for this boundary
278
+ if (this._boundaryMocks[name]) {
279
+ boundariesFns[name] = this._boundaryMocks[name]
280
+ continue
281
+ }
282
+
283
+ // Otherwise create the normal boundary
244
284
  const boundary = createBoundary(definition[name])
245
285
 
246
286
  if (baseData !== null && typeof baseData[name] !== 'undefined') {
@@ -257,44 +297,17 @@ export const Task = class Task<
257
297
  return boundariesFns as WrappedBoundaries<B>
258
298
  }
259
299
 
260
- getBondariesRunLog (): Record<string, unknown> {
261
- const boundaries = this._boundaries
262
- const boundariesRunLog: Record<string, unknown> = {}
263
-
264
- for (const name in boundaries) {
265
- const boundary = boundaries[name]
266
-
267
- boundariesRunLog[name] = boundary.getRunData()
268
- }
269
-
270
- return boundariesRunLog
271
- }
272
-
273
- startRunLog (): void {
274
- const boundaries = this._boundaries
275
-
276
- for (const name in boundaries) {
277
- const boundary = boundaries[name]
278
-
279
- boundary.startRun()
280
- }
281
- }
282
-
283
300
  asBoundary (): (args: Parameters<Func>[0]) => Promise<ReturnType<Func>> {
284
301
  return async (args: Parameters<Func>[0]): Promise<ReturnType<Func>> => {
285
302
  return await this.run(args)
286
303
  }
287
304
  }
288
305
 
289
- async run (argv?: Parameters<Func>[0]): Promise<ReturnType<Func>> {
290
- // start run log
291
- this.startRunLog()
292
- const boundaries = this._boundaries
293
-
294
- const q = new Promise<ReturnType<Func>>((resolve, reject) => {
295
- if (this._schema) {
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 {
296
310
  const validation = this._schema.safeParse(argv)
297
-
298
311
  if (!validation.success) {
299
312
  const errorDetails = validation.error?.errors.map(err =>
300
313
  `${err.path.join('.')}: ${err.message}`
@@ -304,40 +317,117 @@ export const Task = class Task<
304
317
  ? `Invalid input on: ${errorDetails}`
305
318
  : 'Invalid input'
306
319
 
320
+ // Emit the validation error
307
321
  this.emit({
308
322
  input: argv,
309
323
  error: errorMessage
310
324
  })
311
325
 
312
- throw new Error(errorMessage)
326
+ return [new Error(errorMessage), null, null]
313
327
  }
328
+ } catch (error) {
329
+ return [error instanceof Error ? error : new Error(String(error)), null, null]
314
330
  }
331
+ }
315
332
 
316
- (async (): Promise<ReturnType<Func>> => {
317
- // Use proper typing for the function call
318
- const output = await this._fn(argv as Parameters<Func>[0], boundaries as unknown as Parameters<Func>[1])
319
-
320
- return output
321
- })().then((output) => {
322
- this.emit({
323
- input: argv,
324
- output
325
- })
326
-
327
- resolve(output)
328
- }).catch((error) => {
329
- this.emit({
330
- input: argv,
331
- error: error.message
332
- })
333
-
334
- reject(error)
335
- })
333
+ // Create fresh boundaries for this execution
334
+ const executionBoundaries = this._createBounderies({
335
+ definition: this._boundariesDefinition,
336
+ baseData: this._boundariesData,
337
+ mode: this._mode
336
338
  })
337
339
 
338
- const result = await q
340
+ // Start run for each boundary
341
+ for (const name in executionBoundaries) {
342
+ const boundary = executionBoundaries[name]
343
+ boundary.startRun()
344
+ }
339
345
 
340
- return result
346
+ try {
347
+ // Execute the task function
348
+ const output = await this._fn(
349
+ argv as Parameters<Func>[0],
350
+ executionBoundaries as unknown as Parameters<Func>[1]
351
+ )
352
+
353
+ // Process boundary data after successful execution
354
+ const boundariesRunLog: Record<string, unknown> = {}
355
+
356
+ for (const name in executionBoundaries) {
357
+ const boundary = executionBoundaries[name]
358
+ const runData = boundary.getRunData()
359
+
360
+ // Add to the run log
361
+ boundariesRunLog[name] = runData
362
+
363
+ // Accumulate in the task's total boundaries data
364
+ if (!this._accumulatedBoundariesData[name]) {
365
+ this._accumulatedBoundariesData[name] = []
366
+ }
367
+
368
+ // Get the current accumulated data for this boundary
369
+ const currentData = this._accumulatedBoundariesData[name]
370
+
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
+ }
376
+ }
377
+
378
+ // Emit the success event with boundary data
379
+ this.emit({
380
+ input: argv,
381
+ output,
382
+ boundaries: boundariesRunLog
383
+ })
384
+
385
+ return [null, output, boundariesRunLog]
386
+ } catch (error) {
387
+ // Process boundary data after error
388
+ const boundariesRunLog: Record<string, unknown> = {}
389
+
390
+ for (const name in executionBoundaries) {
391
+ const boundary = executionBoundaries[name]
392
+ const runData = boundary.getRunData()
393
+
394
+ // Add to the run log
395
+ boundariesRunLog[name] = runData
396
+
397
+ // Accumulate in the task's total boundaries data
398
+ if (!this._accumulatedBoundariesData[name]) {
399
+ this._accumulatedBoundariesData[name] = []
400
+ }
401
+
402
+ // Get the current accumulated data for this boundary
403
+ const currentData = this._accumulatedBoundariesData[name]
404
+
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
+ }
410
+ }
411
+
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
+ })
418
+
419
+ return [error instanceof Error ? error : new Error(String(error)), null, boundariesRunLog]
420
+ }
421
+ }
422
+
423
+ async run (argv?: Parameters<Func>[0]): Promise<ReturnType<Func>> {
424
+ const [error, result] = await this.safeRun(argv)
425
+
426
+ if (error) {
427
+ throw error
428
+ }
429
+
430
+ return result as ReturnType<Func>
341
431
  }
342
432
  }
343
433
 
@@ -0,0 +1,214 @@
1
+ import { createTask, Schema } from '../index'
2
+
3
+ describe('Task safeRun tests', () => {
4
+ it('returns [null, result, boundaryLogs] on successful execution', async () => {
5
+ // Create a simple schema
6
+ const schema = new Schema({
7
+ value: Schema.number()
8
+ })
9
+
10
+ // Define the boundaries
11
+ const boundaries = {
12
+ fetchData: async (value: number): Promise<number> => {
13
+ return value * 2
14
+ }
15
+ }
16
+
17
+ // Create the task
18
+ const successTask = createTask(
19
+ schema,
20
+ boundaries,
21
+ async function ({ value }, { fetchData }) {
22
+ const result = await fetchData(value)
23
+ return { result, success: true }
24
+ }
25
+ )
26
+
27
+ // Call safeRun with valid input
28
+ const [error, result, boundaryLogs] = await successTask.safeRun({ value: 5 })
29
+
30
+ // Verify success case
31
+ expect(error).toBeNull()
32
+ expect(result).toEqual({ result: 10, success: true })
33
+ expect(boundaryLogs).not.toBeNull()
34
+ expect(boundaryLogs).toHaveProperty('fetchData')
35
+ })
36
+
37
+ it('returns [error, null, boundaryLogs] on failed execution', async () => {
38
+ // Create a simple schema
39
+ const schema = new Schema({
40
+ value: Schema.number()
41
+ })
42
+
43
+ // Define the boundaries with a function that will throw an error
44
+ const boundaries = {
45
+ fetchData: async (value: number): Promise<number> => {
46
+ if (value < 0) {
47
+ throw new Error('Value cannot be negative')
48
+ }
49
+ return value * 2
50
+ }
51
+ }
52
+
53
+ // Create the task
54
+ const errorTask = createTask(
55
+ schema,
56
+ boundaries,
57
+ async function ({ value }, { fetchData }) {
58
+ const result = await fetchData(value)
59
+ return { result, success: true }
60
+ }
61
+ )
62
+
63
+ // Call safeRun with problematic input that will cause an error
64
+ const [error, result, boundaryLogs] = await errorTask.safeRun({ value: -5 })
65
+
66
+ // Verify error case
67
+ expect(error).toBeInstanceOf(Error)
68
+ expect(error?.message).toContain('Value cannot be negative')
69
+ expect(result).toBeNull()
70
+ expect(boundaryLogs).not.toBeNull()
71
+ expect(boundaryLogs).toHaveProperty('fetchData')
72
+ })
73
+
74
+ it('returns [error, null, null] on schema validation failure', async () => {
75
+ // Create a schema that requires a positive number
76
+ const schema = new Schema({
77
+ value: Schema.number().min(1, 'Value must be positive')
78
+ })
79
+
80
+ // Define the boundaries
81
+ const boundaries = {
82
+ fetchData: async (value: number): Promise<number> => {
83
+ return value * 2
84
+ }
85
+ }
86
+
87
+ // Create the task
88
+ const validationTask = createTask(
89
+ schema,
90
+ boundaries,
91
+ async function ({ value }, { fetchData }) {
92
+ const result = await fetchData(value)
93
+ return { result, success: true }
94
+ }
95
+ )
96
+
97
+ // Call safeRun with invalid input that will fail schema validation
98
+ const [error, result, boundaryLogs] = await validationTask.safeRun({ value: 0 })
99
+
100
+ // Verify validation error case
101
+ expect(error).toBeInstanceOf(Error)
102
+ expect(error?.message).toContain('Value must be positive')
103
+ expect(result).toBeNull()
104
+ expect(boundaryLogs).toBeNull() // No boundary calls were made due to validation failure
105
+ })
106
+
107
+ it('properly calls the listener with safeRun and run', async () => {
108
+ // Create a schema
109
+ const schema = new Schema({
110
+ value: Schema.number()
111
+ })
112
+
113
+ // Define the boundaries
114
+ const boundaries = {
115
+ fetchData: async (value: number): Promise<number> => {
116
+ return value * 2
117
+ }
118
+ }
119
+
120
+ // Create the task
121
+ const listenerTask = createTask(
122
+ schema,
123
+ boundaries,
124
+ async function ({ value }, { fetchData }) {
125
+ const result = await fetchData(value)
126
+ return result
127
+ }
128
+ )
129
+
130
+ // Create a mock listener
131
+ const originalListener = jest.fn()
132
+ listenerTask.addListener(originalListener)
133
+
134
+ // Call safeRun - this should call the listener once
135
+ await listenerTask.safeRun({ value: 10 })
136
+
137
+ // Run the task normally - this should call the listener again through safeRun
138
+ await listenerTask.run({ value: 20 })
139
+
140
+ // The original listener should have been called for both runs
141
+ expect(originalListener).toHaveBeenCalledTimes(2)
142
+
143
+ // First call should be for safeRun with value 10
144
+ expect(originalListener).toHaveBeenNthCalledWith(
145
+ 1,
146
+ expect.objectContaining({
147
+ input: { value: 10 },
148
+ output: 20
149
+ })
150
+ )
151
+
152
+ // Second call should be for run with value 20
153
+ expect(originalListener).toHaveBeenNthCalledWith(
154
+ 2,
155
+ expect.objectContaining({
156
+ input: { value: 20 },
157
+ output: 40
158
+ })
159
+ )
160
+ })
161
+
162
+ it('handles multiple boundary calls correctly', async () => {
163
+ // Create a schema
164
+ const schema = new Schema({
165
+ values: Schema.array(Schema.number())
166
+ })
167
+
168
+ // Define multiple boundaries
169
+ const boundaries = {
170
+ doubleValue: async (value: number): Promise<number> => {
171
+ return value * 2
172
+ },
173
+ sumValues: async (values: number[]): Promise<number> => {
174
+ return values.reduce((sum, val) => sum + val, 0)
175
+ }
176
+ }
177
+
178
+ // Create a task that uses multiple boundaries
179
+ const multiBoundaryTask = createTask(
180
+ schema,
181
+ boundaries,
182
+ async function ({ values }, { doubleValue, sumValues }) {
183
+ const doubled = await Promise.all(values.map(value => doubleValue(value)))
184
+ const total = await sumValues(doubled)
185
+ return { doubled, total }
186
+ }
187
+ )
188
+
189
+ // Call safeRun
190
+ const [error, result, boundaryLogs] = await multiBoundaryTask.safeRun({ values: [1, 2, 3] })
191
+
192
+ // Verify success
193
+ expect(error).toBeNull()
194
+ expect(result).toEqual({
195
+ doubled: [2, 4, 6],
196
+ total: 12
197
+ })
198
+
199
+ // Verify boundary logs for both boundaries
200
+ expect(boundaryLogs).not.toBeNull()
201
+ expect(boundaryLogs).toHaveProperty('doubleValue')
202
+ expect(boundaryLogs).toHaveProperty('sumValues')
203
+
204
+ // Check that doubleValue was called 3 times (once for each input value)
205
+ // @ts-expect-error - we know the boundaryLogs is not null here
206
+ expect(boundaryLogs.doubleValue).toHaveLength(3)
207
+
208
+ // Check that sumValues was called once with the doubled values
209
+ // @ts-expect-error - we know the boundaryLogs is not null here
210
+ expect(boundaryLogs.sumValues).toHaveLength(1)
211
+ // @ts-expect-error - we know the boundaryLogs is not null here
212
+ expect(boundaryLogs.sumValues[0].input).toEqual([[2, 4, 6]])
213
+ })
214
+ })