@forgehive/record-tape 0.1.6 → 0.2.0

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
@@ -4,30 +4,6 @@ import { type ExecutionRecord, type Boundaries } from '@forgehive/task'
4
4
 
5
5
  export interface LogRecord<TInput = unknown, TOutput = unknown, B extends Boundaries = Boundaries> extends ExecutionRecord<TInput, TOutput, B> {
6
6
  name: string
7
- type: 'success' | 'error'
8
- context?: Record<string, string>
9
- }
10
-
11
- export interface SuccessLogItem<TInput = unknown, TOutput = unknown> {
12
- input: TInput
13
- output: TOutput
14
- boundaries?: Record<string, unknown>
15
- }
16
-
17
- export interface ErrorLogItem<TInput = unknown> {
18
- input: TInput
19
- error: unknown
20
- boundaries?: Record<string, unknown>
21
- }
22
-
23
- export type LogItem<TInput = unknown, TOutput = unknown> = SuccessLogItem<TInput, TOutput> | ErrorLogItem<TInput>
24
-
25
- // Additional type to handle TaskRecord compatibility
26
- export type TaskLogItem<TInput = unknown, TOutput = unknown> = LogItem<TInput, TOutput> | {
27
- input: TInput;
28
- output?: TOutput;
29
- error?: unknown;
30
- boundaries?: Record<string, unknown>;
31
7
  }
32
8
 
33
9
  interface Config<TInput = unknown, TOutput = unknown, B extends Boundaries = Boundaries> {
@@ -62,65 +38,35 @@ export class RecordTape<TInput = unknown, TOutput = unknown, B extends Boundarie
62
38
  this._mode = mode
63
39
  }
64
40
 
65
- addLogItem(name: string, logItem: LogItem<TInput, TOutput>): void {
41
+ addExecutionRecord(name: string, record: ExecutionRecord<TInput, TOutput, B>): void {
66
42
  if (this._mode === 'replay') {
67
43
  return
68
44
  }
69
45
 
70
- // Format boundaries to ensure both error and output fields are set if needed
46
+ // Format boundaries to ensure consistent structure
71
47
  const formattedBoundaries: Record<string, unknown> = {}
72
- if (logItem.boundaries) {
73
- for (const key in logItem.boundaries) {
74
- // Check if the source is from safe-run (if it has error field in entries)
75
- const boundaryEntries = logItem.boundaries[key] as Array<Record<string, unknown>>
76
- const isSafeRun = boundaryEntries.some(entry => entry.error !== undefined)
77
-
78
- formattedBoundaries[key] = boundaryEntries.map(entry => {
79
- // Only add error field if it's from safe-run
80
- return isSafeRun ?
81
- {
82
- input: entry.input,
83
- output: entry.output ?? null,
84
- error: entry.error ?? null
85
- } :
86
- {
87
- input: entry.input,
88
- output: entry.output
89
- }
90
- })
48
+ if (record.boundaries) {
49
+ for (const key in record.boundaries) {
50
+ const boundaryEntries = record.boundaries[key] as Array<Record<string, unknown>>
51
+ formattedBoundaries[key] = boundaryEntries.map(entry => ({
52
+ input: entry.input,
53
+ output: entry.output ?? null,
54
+ error: entry.error ?? null
55
+ }))
91
56
  }
92
57
  }
93
58
 
94
- // Handle LogItem interface - need to type cast to access properties safely
95
- const typedLogItem = logItem as (SuccessLogItem<TInput, TOutput> | ErrorLogItem<TInput>)
96
-
97
- if ('output' in typedLogItem && typedLogItem.output !== undefined) {
98
- const { input, output } = typedLogItem
99
- this._log.push({
100
- name,
101
- type: 'success',
102
- input,
103
- output,
104
- boundaries: formattedBoundaries
105
- } as LogRecord<TInput, TOutput, B>)
106
- } else if ('error' in typedLogItem && typedLogItem.error !== undefined) {
107
- const { input, error } = typedLogItem
108
- this._log.push({
109
- name,
110
- type: 'error',
111
- input,
112
- error,
113
- boundaries: formattedBoundaries
114
- } as LogRecord<TInput, TOutput, B>)
115
- } else {
116
- throw new Error('invalid log item')
117
- }
59
+ this._log.push({
60
+ name,
61
+ ...record,
62
+ boundaries: formattedBoundaries
63
+ } as LogRecord<TInput, TOutput, B>)
118
64
  }
119
65
 
120
66
  push(
121
67
  name: string,
122
68
  record: ExecutionRecord<TInput, unknown, B>,
123
- context?: Record<string, string>
69
+ metadata?: Record<string, string>
124
70
  ): LogRecord<TInput, TOutput, B> {
125
71
  if (this._mode === 'replay') {
126
72
  return {} as LogRecord<TInput, TOutput, B>
@@ -141,43 +87,35 @@ export class RecordTape<TInput = unknown, TOutput = unknown, B extends Boundarie
141
87
  }
142
88
  }
143
89
 
144
- let logRecord: LogRecord<TInput, TOutput, B>
145
-
146
- if ('output' in record && record.output !== undefined) {
147
- const input = record.input
148
- // Handle Promise outputs by setting to null in the log
149
- const output = record.output instanceof Promise ? null : record.output
150
-
151
- logRecord = {
152
- name,
153
- type: 'success',
154
- input,
155
- output,
156
- boundaries: formattedBoundaries,
157
- context
158
- } as LogRecord<TInput, TOutput, B>
159
- this._log.push(logRecord)
160
- } else if ('error' in record && record.error !== undefined) {
161
- const input = record.input
162
- const error = record.error
163
-
164
- logRecord = {
165
- name,
166
- type: 'error',
167
- input,
168
- error,
169
- boundaries: formattedBoundaries,
170
- context
171
- } as LogRecord<TInput, TOutput, B>
172
- this._log.push(logRecord)
173
- } else {
174
- throw new Error('invalid record type')
175
- }
90
+ // Use the type from ExecutionRecord if available, otherwise derive it
91
+ const recordType = ('type' in record && record.type) ? record.type :
92
+ (record.output !== undefined && record.output !== null) ? 'success' :
93
+ (record.error !== undefined) ? 'error' : 'pending'
94
+
95
+ // Handle Promise outputs by setting to null in the log
96
+ const output = record.output instanceof Promise ? null : record.output
97
+
98
+ // Merge metadata from record and parameter (parameter takes precedence)
99
+ const mergedMetadata = { ...record.metadata, ...metadata }
100
+
101
+ const logRecord = {
102
+ name,
103
+ ...record,
104
+ type: recordType,
105
+ output,
106
+ boundaries: formattedBoundaries,
107
+ metadata: mergedMetadata
108
+ } as LogRecord<TInput, TOutput, B>
109
+
110
+ this._log.push(logRecord)
176
111
 
177
112
  return logRecord
178
113
  }
179
114
 
180
115
  addLogRecord(logRecord: LogRecord<TInput, TOutput, B>): void {
116
+ if (this._mode === 'replay') {
117
+ return
118
+ }
181
119
  this._log.push(logRecord)
182
120
  }
183
121
 
@@ -219,11 +157,11 @@ export class RecordTape<TInput = unknown, TOutput = unknown, B extends Boundarie
219
157
  }
220
158
 
221
159
  recordFrom(name: string, task: { _listener?: unknown; setBoundariesData: (data: Record<string, unknown>) => void }): void {
222
- // Add listner
223
- task._listener = async (logItem: LogItem<TInput, TOutput>, _boundaries: Record<string, unknown>): Promise<void> => {
160
+ // Add listener for ExecutionRecord
161
+ task._listener = async (executionRecord: ExecutionRecord<TInput, TOutput, B>, _boundaries: Record<string, unknown>): Promise<void> => {
224
162
  // Only update if mode is record
225
163
  if (this.getMode() === 'record') {
226
- this.addLogItem(name, logItem)
164
+ this.addExecutionRecord(name, executionRecord)
227
165
  }
228
166
  }
229
167
 
@@ -59,8 +59,8 @@ describe('Base tests', () => {
59
59
  }
60
60
 
61
61
  const tape = new RecordTape<InputType, OutputType>({ path: emptyPath })
62
- tape.addLogItem('test', { input: [{name: 'test'}], output: { age: 1 } })
63
- tape.addLogItem('test', { input: [{name: 'test'}], error: new Error('test') })
62
+ tape.addLogRecord({ name: 'test', input: [{name: 'test'}], output: { age: 1 }, type: 'success', boundaries: {} })
63
+ tape.addLogRecord({ name: 'test', input: [{name: 'test'}], error: 'test', type: 'error', boundaries: {} })
64
64
 
65
65
  const data = tape.getLog()
66
66
  expect(data.length).toBe(2)
@@ -82,7 +82,7 @@ describe('Base tests', () => {
82
82
 
83
83
  expect(input2).toEqual([{name: 'test'}])
84
84
  expect(output2).toBeUndefined()
85
- expect(error2).toEqual(new Error('test'))
85
+ expect(error2).toEqual('test')
86
86
  expect(type2).toBe('error')
87
87
  })
88
88
  })
@@ -1,48 +1,36 @@
1
1
  import { RecordTape } from '../index'
2
2
 
3
- const baseTapeData = [
4
- {
5
- name: 'name',
6
- type: 'success',
7
- input: [true],
8
- output: true,
9
- boundaries: {}
10
- },
11
- {
12
- name: 'name',
13
- type: 'error',
14
- input: [true],
15
- error: 'invalid data',
16
- boundaries: {}
17
- }
18
- ]
19
-
20
- const logFileData = '{"name":"name","type":"success","input":[true],"output":true,"boundaries":{}}\n{"name":"name","type":"error","input":[true],"error":"invalid data","boundaries":{}}\n'
21
-
22
- describe('Log format', () => {
23
- it('Should ensure format', () => {
24
- type InputType = boolean[]
3
+ describe('Test log item formating', () => {
4
+ it('Should format a log items into valid JSON', async () => {
5
+ type InputType = boolean
25
6
  type OutputType = boolean
26
7
 
27
8
  const tape = new RecordTape<InputType, OutputType>({})
9
+ tape.addLogRecord({ name: 'name', input: true, output: true, boundaries: {}, type: 'success' })
10
+ tape.addLogRecord({ name: 'name', input: true, error: 'invalid data', boundaries: {}, type: 'error' })
28
11
 
29
- tape.addLogItem('name', { input: [true], output: true, boundaries: {} })
30
- tape.addLogItem('name', { input: [true], error: 'invalid data', boundaries: {} })
12
+ const str = tape.stringify()
31
13
 
32
- expect(tape.getLog()).toEqual(baseTapeData)
14
+ expect(str).toEqual(`{"name":"name","input":true,"output":true,"boundaries":{},"type":"success"}
15
+ {"name":"name","input":true,"error":"invalid data","boundaries":{},"type":"error"}
16
+ `)
33
17
  })
34
18
 
35
- it('Should serialize to one line per item', () => {
36
- type InputType = boolean[]
19
+ it('Should parse the string to a list of records', async () => {
20
+ type InputType = boolean
37
21
  type OutputType = boolean
38
22
 
39
23
  const tape = new RecordTape<InputType, OutputType>({})
24
+ tape.addLogRecord({ name: 'name', input: true, output: true, boundaries: {}, type: 'success' })
25
+ tape.addLogRecord({ name: 'name', input: true, error: 'invalid data', boundaries: {}, type: 'error' })
40
26
 
41
- tape.addLogItem('name', { input: [true], output: true, boundaries: {} })
42
- tape.addLogItem('name', { input: [true], error: 'invalid data', boundaries: {} })
27
+ const str = tape.stringify()
28
+ const tape2 = new RecordTape<InputType, OutputType>({})
29
+ const records = tape2.parse(str)
43
30
 
44
- const logFile = tape.stringify()
45
-
46
- expect(logFile).toBe(logFileData)
31
+ expect(records).toEqual([
32
+ { name: 'name', input: true, output: true, boundaries: {}, type: 'success' },
33
+ { name: 'name', input: true, error: 'invalid data', boundaries: {}, type: 'error' }
34
+ ])
47
35
  })
48
36
  })
@@ -1,92 +1,105 @@
1
1
  import { RecordTape } from '../index'
2
2
 
3
- describe('Mode behavior', () => {
4
- it('Should start in record mode by default', () => {
5
- const tape = new RecordTape()
6
- expect(tape.getMode()).toBe('record')
7
- })
8
-
9
- it('Should not add log items in replay mode', () => {
10
- type InputType = { value: number }
11
- type OutputType = { result: number }
3
+ describe('Test mode', () => {
4
+ it('Should record items if mode is record', () => {
5
+ type InputType = { name: string }
6
+ type OutputType = { age: number }
12
7
 
13
- const tape = new RecordTape<InputType, OutputType>()
14
- tape.setMode('replay')
8
+ const tape = new RecordTape<InputType, OutputType>({})
9
+ tape.setMode('record')
15
10
 
16
- tape.addLogItem('test', {
17
- input: { value: 1 },
18
- output: { result: 2 }
11
+ tape.addLogRecord({
12
+ name: 'test',
13
+ input: { name: 'test' },
14
+ output: { age: 1 },
15
+ boundaries: {},
16
+ type: 'success'
19
17
  })
20
18
 
21
- expect(tape.getLog()).toEqual([])
19
+ const data = tape.getLog()
20
+
21
+ expect(data).toEqual([{
22
+ name: 'test',
23
+ input: { name: 'test' },
24
+ output: { age: 1 },
25
+ boundaries: {},
26
+ type: 'success'
27
+ }])
22
28
  })
23
29
 
24
- it('Should add log items in record mode', () => {
25
- type InputType = { value: number }
26
- type OutputType = { result: number }
30
+ it('Should not record items if mode is replay', () => {
31
+ type InputType = { name: string }
32
+ type OutputType = { age: number }
27
33
 
28
- const tape = new RecordTape<InputType, OutputType>()
29
- tape.setMode('record')
34
+ const tape = new RecordTape<InputType, OutputType>({})
35
+ tape.setMode('replay')
30
36
 
31
- tape.addLogItem('test', {
32
- input: { value: 1 },
33
- output: { result: 2 }
37
+ tape.addLogRecord({
38
+ name: 'test',
39
+ input: { name: 'test' },
40
+ output: { age: 1 },
41
+ boundaries: {},
42
+ type: 'success'
34
43
  })
35
44
 
36
- expect(tape.getLog()).toEqual([
37
- {
38
- name: 'test',
39
- type: 'success',
40
- input: { value: 1 },
41
- output: { result: 2 },
42
- boundaries: {}
43
- }
44
- ])
45
+ const data = tape.getLog()
46
+ expect(data).toEqual([])
45
47
  })
46
48
 
47
- it('Should switch between modes', () => {
48
- type InputType = { value: number }
49
- type OutputType = { result: number }
49
+ it('Should append logs with same name', () => {
50
+ type InputType = { name: string }
51
+ type OutputType = { age: number }
50
52
 
51
- const tape = new RecordTape<InputType, OutputType>()
53
+ const tape = new RecordTape<InputType, OutputType>({})
54
+ tape.setMode('record')
52
55
 
53
- // Start in record mode
54
- expect(tape.getMode()).toBe('record')
55
- tape.addLogItem('test1', {
56
- input: { value: 1 },
57
- output: { result: 2 }
56
+ tape.addLogRecord({
57
+ name: 'test1',
58
+ input: { name: 'test' },
59
+ output: { age: 1 },
60
+ boundaries: {},
61
+ type: 'success'
58
62
  })
59
63
 
60
- // Switch to replay mode
61
- tape.setMode('replay')
62
- expect(tape.getMode()).toBe('replay')
63
- tape.addLogItem('test2', {
64
- input: { value: 3 },
65
- output: { result: 4 }
64
+ tape.addLogRecord({
65
+ name: 'test2',
66
+ input: { name: 'test' },
67
+ output: { age: 2 },
68
+ boundaries: {},
69
+ type: 'success'
66
70
  })
67
71
 
68
- // Switch back to record mode
69
- tape.setMode('record')
70
- expect(tape.getMode()).toBe('record')
71
- tape.addLogItem('test3', {
72
- input: { value: 5 },
73
- output: { result: 6 }
72
+ tape.addLogRecord({
73
+ name: 'test3',
74
+ input: { name: 'test' },
75
+ output: { age: 3 },
76
+ boundaries: {},
77
+ type: 'success'
74
78
  })
75
79
 
76
- expect(tape.getLog()).toEqual([
80
+ const data = tape.getLog()
81
+
82
+ expect(data).toEqual([
77
83
  {
78
84
  name: 'test1',
79
- type: 'success',
80
- input: { value: 1 },
81
- output: { result: 2 },
82
- boundaries: {}
85
+ input: { name: 'test' },
86
+ output: { age: 1 },
87
+ boundaries: {},
88
+ type: 'success'
89
+ },
90
+ {
91
+ name: 'test2',
92
+ input: { name: 'test' },
93
+ output: { age: 2 },
94
+ boundaries: {},
95
+ type: 'success'
83
96
  },
84
97
  {
85
98
  name: 'test3',
86
- type: 'success',
87
- input: { value: 5 },
88
- output: { result: 6 },
89
- boundaries: {}
99
+ input: { name: 'test' },
100
+ output: { age: 3 },
101
+ boundaries: {},
102
+ type: 'success'
90
103
  }
91
104
  ])
92
105
  })
@@ -1,4 +1,4 @@
1
- import { RecordTape, type LogItem } from '../index'
1
+ import { RecordTape } from '../index'
2
2
  import { createTask, Schema, type ExecutionRecord, type TaskRecord } from '@forgehive/task'
3
3
 
4
4
  describe('RecordTape safeRun integration tests', () => {
@@ -16,14 +16,14 @@ describe('RecordTape safeRun integration tests', () => {
16
16
  }
17
17
 
18
18
  // Create the task
19
- const task = createTask(
19
+ const task = createTask({
20
20
  schema,
21
21
  boundaries,
22
- async function ({ value }, { fetchData }) {
22
+ fn: async function ({ value }, { fetchData }) {
23
23
  const result = await fetchData(value)
24
24
  return { result, success: true }
25
25
  }
26
- )
26
+ })
27
27
 
28
28
  // Create a record tape
29
29
  const tape = new RecordTape<{ value: number }, { result: number; success: boolean }, typeof boundaries>()
@@ -70,14 +70,14 @@ describe('RecordTape safeRun integration tests', () => {
70
70
  }
71
71
 
72
72
  // Create the task
73
- const task = createTask(
73
+ const task = createTask({
74
74
  schema,
75
75
  boundaries,
76
- async function ({ value }, { fetchData }) {
76
+ fn: async function ({ value }, { fetchData }) {
77
77
  const result = await fetchData(value)
78
78
  return { result, success: true }
79
79
  }
80
- )
80
+ })
81
81
 
82
82
  // Create a record tape
83
83
  const tape = new RecordTape<{ value: number }, { result: number; success: boolean }, typeof boundaries>()
@@ -93,8 +93,9 @@ describe('RecordTape safeRun integration tests', () => {
93
93
  }))
94
94
  }
95
95
 
96
- // Cast the record to LogItem type to satisfy TypeScript
97
- tape.addLogItem('test-task', record as unknown as LogItem<{ value: number }, { result: number; success: boolean }>)
96
+ // Add the record using push method
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
+ tape.push('test-task', record as any)
98
99
  })
99
100
 
100
101
  // Run the task with safeRun
@@ -120,7 +121,9 @@ describe('RecordTape safeRun integration tests', () => {
120
121
  output: 10,
121
122
  error: null
122
123
  }]
123
- }
124
+ },
125
+ metadata: {},
126
+ taskName: undefined
124
127
  })
125
128
  })
126
129
 
@@ -141,14 +144,14 @@ describe('RecordTape safeRun integration tests', () => {
141
144
  }
142
145
 
143
146
  // Create the task
144
- const task = createTask(
147
+ const task = createTask({
145
148
  schema,
146
149
  boundaries,
147
- async function ({ value }, { fetchData }) {
150
+ fn: async function ({ value }, { fetchData }) {
148
151
  const result = await fetchData(value)
149
152
  return { result, success: true }
150
153
  }
151
- )
154
+ })
152
155
 
153
156
  // Create a record tape
154
157
  const tape = new RecordTape<{ value: number }, { result: number; success: boolean }, typeof boundaries>()
@@ -164,8 +167,9 @@ describe('RecordTape safeRun integration tests', () => {
164
167
  }))
165
168
  }
166
169
 
167
- // Cast the record to LogItem type to satisfy TypeScript
168
- tape.addLogItem('test-task', record as unknown as LogItem<{ value: number }, { result: number; success: boolean }>)
170
+ // Add the record using push method
171
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
172
+ tape.push('test-task', record as any)
169
173
  })
170
174
 
171
175
  // Run the task with safeRun with a value that will cause an error
@@ -192,7 +196,10 @@ describe('RecordTape safeRun integration tests', () => {
192
196
  error: 'Value cannot be negative',
193
197
  output: null
194
198
  }]
195
- }
199
+ },
200
+ metadata: {},
201
+ output: undefined,
202
+ taskName: undefined
196
203
  })
197
204
  })
198
205
 
@@ -213,14 +220,14 @@ describe('RecordTape safeRun integration tests', () => {
213
220
  }
214
221
 
215
222
  // Create the task
216
- const task = createTask(
223
+ const task = createTask({
217
224
  schema,
218
225
  boundaries,
219
- async function ({ value }, { fetchData }) {
226
+ fn: async function ({ value }, { fetchData }) {
220
227
  const result = await fetchData(value)
221
228
  return { result, success: true }
222
229
  }
223
- )
230
+ })
224
231
 
225
232
  // Create a record tape
226
233
  const tape = new RecordTape<{ value: number }, { result: number; success: boolean }, typeof boundaries>()
@@ -255,7 +262,10 @@ describe('RecordTape safeRun integration tests', () => {
255
262
  output: null,
256
263
  error: 'Value cannot be negative'
257
264
  }]
258
- }
265
+ },
266
+ metadata: {},
267
+ output: undefined,
268
+ taskName: undefined
259
269
  })
260
270
  })
261
271
 
@@ -267,6 +277,7 @@ describe('RecordTape safeRun integration tests', () => {
267
277
  const customRecord: ExecutionRecord<{ value: number }, { result: number }, { fetchData: (n: number) => Promise<number> }> = {
268
278
  input: { value: 10 },
269
279
  output: { result: 20 },
280
+ type: 'success',
270
281
  boundaries: {
271
282
  fetchData: [
272
283
  {
@@ -297,7 +308,7 @@ describe('RecordTape safeRun integration tests', () => {
297
308
  error: null
298
309
  }]
299
310
  },
300
- context: undefined
311
+ metadata: {}
301
312
  })
302
313
  })
303
314
 
@@ -310,6 +321,7 @@ describe('RecordTape safeRun integration tests', () => {
310
321
  const promiseRecord: ExecutionRecord<{ value: number }, Promise<{ result: number }>, { fetchData: (n: number) => Promise<number> }> = {
311
322
  input: { value: 15 },
312
323
  output: promiseResult,
324
+ type: 'success',
313
325
  boundaries: {
314
326
  fetchData: [
315
327
  {
@@ -339,7 +351,8 @@ describe('RecordTape safeRun integration tests', () => {
339
351
  output: 30,
340
352
  error: null
341
353
  }]
342
- }
354
+ },
355
+ metadata: {}
343
356
  })
344
357
  })
345
358
  })