@forgehive/task 0.1.7 → 0.1.9
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/dist/index.d.ts +19 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -6
- package/dist/index.js.map +1 -1
- package/dist/test/safe-replay-complex-boundary.test.d.ts +2 -0
- package/dist/test/safe-replay-complex-boundary.test.d.ts.map +1 -0
- package/dist/test/safe-replay-complex-boundary.test.js +314 -0
- package/dist/test/safe-replay-complex-boundary.test.js.map +1 -0
- package/dist/test/safe-replay.test.d.ts +2 -0
- package/dist/test/safe-replay.test.d.ts.map +1 -0
- package/dist/test/safe-replay.test.js +194 -0
- package/dist/test/safe-replay.test.js.map +1 -0
- package/dist/test/task-with-boundaries.test.js.map +1 -1
- package/dist/utils/boundary.d.ts +24 -5
- package/dist/utils/boundary.d.ts.map +1 -1
- package/dist/utils/boundary.js +16 -10
- package/dist/utils/boundary.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +168 -22
- package/src/test/safe-replay-complex-boundary.test.ts +385 -0
- package/src/test/safe-replay.test.ts +231 -0
- package/src/test/task-with-boundaries.test.ts +2 -2
- package/src/utils/boundary.ts +44 -17
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { Schema } from '@forgehive/schema'
|
|
2
|
+
import { createTask, ExecutionRecord } from '../index'
|
|
3
|
+
|
|
4
|
+
describe('safeReplay functionality tests', () => {
|
|
5
|
+
// Common variables
|
|
6
|
+
let prices: Record<string, number>
|
|
7
|
+
let boundaries: {
|
|
8
|
+
fetchData: (ticker: string) => Promise<number>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ToDo: Add correct type for schema and getTickerPrice
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
let schema: Schema<Record<string, any>>
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
let getTickerPrice: any // Using any temporarily until we implement safeReplay
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// Create a schema for the task
|
|
19
|
+
schema = new Schema({
|
|
20
|
+
ticker: Schema.string()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Mock price data
|
|
24
|
+
prices = {
|
|
25
|
+
'AAPL': 150.23
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Define the boundaries
|
|
29
|
+
boundaries = {
|
|
30
|
+
fetchData: async (ticker: string): Promise<number> => {
|
|
31
|
+
// check if the ticker is in the prices object
|
|
32
|
+
if (!prices[ticker as keyof typeof prices]) {
|
|
33
|
+
throw new Error(`Ticker ${ticker} not found in prices`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return prices[ticker as keyof typeof prices]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Create the task using createTask
|
|
41
|
+
getTickerPrice = createTask(
|
|
42
|
+
schema,
|
|
43
|
+
boundaries,
|
|
44
|
+
async ({ ticker }, { fetchData }) => {
|
|
45
|
+
const price = await fetchData(ticker)
|
|
46
|
+
return {
|
|
47
|
+
ticker,
|
|
48
|
+
price
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('Should replay a previous execution using the execution log and replay the fetchData boundary', async () => {
|
|
55
|
+
// Create a manual execution log
|
|
56
|
+
const executionLog: ExecutionRecord = {
|
|
57
|
+
input: { ticker: 'AAPL' },
|
|
58
|
+
output: {
|
|
59
|
+
ticker: 'AAPL',
|
|
60
|
+
price: 160.23
|
|
61
|
+
},
|
|
62
|
+
boundaries: {
|
|
63
|
+
fetchData: [
|
|
64
|
+
{
|
|
65
|
+
input: ['AAPL'],
|
|
66
|
+
output: 160.23
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// No safeReplay method yet, this will be implemented later
|
|
73
|
+
// This will be our test for that functionality
|
|
74
|
+
const [replayResult, replayError, replayLog] = await getTickerPrice.safeReplay(
|
|
75
|
+
executionLog
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// Verify the replay execution
|
|
79
|
+
expect(replayError).toBeNull()
|
|
80
|
+
expect(replayResult).toMatchObject({
|
|
81
|
+
ticker: 'AAPL',
|
|
82
|
+
price: 150.23
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(replayLog).toMatchObject({
|
|
86
|
+
input: { ticker: 'AAPL' },
|
|
87
|
+
output: {
|
|
88
|
+
ticker: 'AAPL',
|
|
89
|
+
price: 150.23
|
|
90
|
+
},
|
|
91
|
+
boundaries: {
|
|
92
|
+
fetchData: [
|
|
93
|
+
{
|
|
94
|
+
input: ['AAPL'],
|
|
95
|
+
output: 150.23,
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('Should execute with mixed boundaries modes', async () => {
|
|
103
|
+
// Create a manual execution log for testing
|
|
104
|
+
const executionLog: ExecutionRecord = {
|
|
105
|
+
input: { ticker: 'AAPL' },
|
|
106
|
+
output: {
|
|
107
|
+
ticker: 'AAPL',
|
|
108
|
+
price: 160.23
|
|
109
|
+
},
|
|
110
|
+
boundaries: {
|
|
111
|
+
fetchData: [
|
|
112
|
+
{
|
|
113
|
+
input: ['AAPL'],
|
|
114
|
+
output: 160.23
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Use mixed mode - replay for fetchData but execute logAccess
|
|
121
|
+
const [replayResult, replayError, replayLog] = await getTickerPrice.safeReplay(
|
|
122
|
+
executionLog,
|
|
123
|
+
{
|
|
124
|
+
boundaries: {
|
|
125
|
+
fetchData: 'replay',
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
// Verify the replay execution
|
|
131
|
+
expect(replayError).toBeNull()
|
|
132
|
+
expect(replayResult).toMatchObject({
|
|
133
|
+
ticker: 'AAPL',
|
|
134
|
+
price: 160.23
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
expect(replayLog).toMatchObject({
|
|
138
|
+
input: { ticker: 'AAPL' },
|
|
139
|
+
output: {
|
|
140
|
+
ticker: 'AAPL',
|
|
141
|
+
price: 160.23
|
|
142
|
+
},
|
|
143
|
+
boundaries: {
|
|
144
|
+
fetchData: [
|
|
145
|
+
{
|
|
146
|
+
input: ['AAPL'],
|
|
147
|
+
output: 160.23
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('Should properly handle errors in boundary replay mode', async () => {
|
|
155
|
+
// Create a manual execution log with an error in the boundary
|
|
156
|
+
const executionLog: ExecutionRecord = {
|
|
157
|
+
input: { ticker: 'AAPL' },
|
|
158
|
+
output: null,
|
|
159
|
+
error: 'API error: Rate limit exceeded',
|
|
160
|
+
boundaries: {
|
|
161
|
+
fetchData: [
|
|
162
|
+
{
|
|
163
|
+
input: ['AAPL'],
|
|
164
|
+
error: 'API error: Rate limit exceeded'
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Use replay mode for fetchData
|
|
171
|
+
const [replayResult, replayError, replayLog] = await getTickerPrice.safeReplay(
|
|
172
|
+
executionLog,
|
|
173
|
+
{
|
|
174
|
+
boundaries: {
|
|
175
|
+
fetchData: 'replay',
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
// Verify the replay execution - should have an error
|
|
181
|
+
expect(replayResult).toBeNull()
|
|
182
|
+
expect(replayError).not.toBeNull()
|
|
183
|
+
expect(replayError?.message).toBe('API error: Rate limit exceeded')
|
|
184
|
+
|
|
185
|
+
// The log should contain the error from the boundary
|
|
186
|
+
expect(replayLog.error).toBeDefined()
|
|
187
|
+
expect(replayLog.boundaries.fetchData[0].error).toBe('API error: Rate limit exceeded')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('Should handle boundaries with both output and error as null', async () => {
|
|
191
|
+
// Create a manual execution log with null output and error in the boundary
|
|
192
|
+
const executionLog: ExecutionRecord = {
|
|
193
|
+
input: { ticker: 'AAPL' },
|
|
194
|
+
output: { ticker: 'AAPL', price: 160.23 },
|
|
195
|
+
boundaries: {
|
|
196
|
+
fetchData: [
|
|
197
|
+
{
|
|
198
|
+
input: ['AAPL'],
|
|
199
|
+
output: 160.23,
|
|
200
|
+
error: null
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Use replay mode for fetchData
|
|
207
|
+
const [replayResult, replayError, replayLog] = await getTickerPrice.safeReplay(
|
|
208
|
+
executionLog,
|
|
209
|
+
{
|
|
210
|
+
boundaries: {
|
|
211
|
+
fetchData: 'replay',
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
// Verify the replay execution
|
|
217
|
+
expect(replayError).toBeNull()
|
|
218
|
+
expect(replayResult).toMatchObject({
|
|
219
|
+
ticker: 'AAPL',
|
|
220
|
+
price: 160.23
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// The log should contain the output from the boundary
|
|
224
|
+
expect(replayLog.output).toMatchObject({
|
|
225
|
+
ticker: 'AAPL',
|
|
226
|
+
price: 160.23
|
|
227
|
+
})
|
|
228
|
+
expect(replayLog.boundaries.fetchData[0].output).toBe(160.23)
|
|
229
|
+
expect(replayLog.boundaries.fetchData[0].error).toBeNull()
|
|
230
|
+
})
|
|
231
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTask, Schema, TaskRecord } from '../index'
|
|
1
|
+
import { createTask, Schema, TaskRecord, BoundaryTapeData } from '../index'
|
|
2
2
|
|
|
3
3
|
// Need to add proxy cache mode to the boundaries
|
|
4
4
|
describe('Boundaries tasks tests', () => {
|
|
@@ -316,7 +316,7 @@ describe('Boundaries tasks tests', () => {
|
|
|
316
316
|
return value * externalData
|
|
317
317
|
},
|
|
318
318
|
{
|
|
319
|
-
boundariesData: boundariesData3,
|
|
319
|
+
boundariesData: boundariesData3 as BoundaryTapeData,
|
|
320
320
|
mode: 'proxy-pass'
|
|
321
321
|
}
|
|
322
322
|
)
|
package/src/utils/boundary.ts
CHANGED
|
@@ -14,17 +14,39 @@ export type Mode = 'proxy' | 'proxy-pass' | 'proxy-catch' | 'replay'
|
|
|
14
14
|
export type BoundaryFunction<TReturn = any> = (...args: any[]) => Promise<TReturn>
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Success record for a boundary function call
|
|
18
18
|
* @template TInput - The type of input data
|
|
19
19
|
* @template TOutput - The type of output data
|
|
20
20
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
error?: string
|
|
21
|
+
export type BoundarySuccessRecord<TInput = unknown[], TOutput = unknown> = {
|
|
22
|
+
input: TInput;
|
|
23
|
+
output: TOutput;
|
|
24
|
+
error?: null;
|
|
26
25
|
}
|
|
27
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Error record for a boundary function call
|
|
29
|
+
* @template TInput - The type of input data
|
|
30
|
+
*/
|
|
31
|
+
export type BoundaryErrorRecord<TInput = unknown[]> = {
|
|
32
|
+
input: TInput;
|
|
33
|
+
output?: null;
|
|
34
|
+
error: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Represents a record of a boundary function call - either success or error
|
|
39
|
+
* @template TInput - The type of input data
|
|
40
|
+
* @template TOutput - The type of output data
|
|
41
|
+
*/
|
|
42
|
+
export type BoundaryRecord<TInput = unknown[], TOutput = unknown> =
|
|
43
|
+
BoundarySuccessRecord<TInput, TOutput> | BoundaryErrorRecord<TInput>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Represents the tape data for all boundaries
|
|
47
|
+
*/
|
|
48
|
+
export type BoundaryTapeData = Record<string, Array<BoundaryRecord>>;
|
|
49
|
+
|
|
28
50
|
/**
|
|
29
51
|
* Represents a wrapped boundary function with additional methods
|
|
30
52
|
*/
|
|
@@ -79,10 +101,6 @@ export const createBoundary = <Func extends BaseBoundary>(fn: Func): WrappedBoun
|
|
|
79
101
|
return result
|
|
80
102
|
}
|
|
81
103
|
|
|
82
|
-
const record: RecordType = {
|
|
83
|
-
input: args
|
|
84
|
-
}
|
|
85
|
-
|
|
86
104
|
if (mode === 'proxy-pass') {
|
|
87
105
|
const record = findRecord(args, cacheTape)
|
|
88
106
|
|
|
@@ -101,7 +119,8 @@ export const createBoundary = <Func extends BaseBoundary>(fn: Func): WrappedBoun
|
|
|
101
119
|
throw new Error('No tape value for this inputs')
|
|
102
120
|
}
|
|
103
121
|
|
|
104
|
-
if
|
|
122
|
+
// Check if this is an error record by checking if error property exists
|
|
123
|
+
if (record.error !== undefined && record.error !== null) {
|
|
105
124
|
throw new Error(record.error)
|
|
106
125
|
}
|
|
107
126
|
|
|
@@ -124,18 +143,26 @@ export const createBoundary = <Func extends BaseBoundary>(fn: Func): WrappedBoun
|
|
|
124
143
|
return prevRecord.output as unknown as ReturnType<Func>
|
|
125
144
|
})()
|
|
126
145
|
} else {
|
|
127
|
-
|
|
146
|
+
// Create an error record
|
|
147
|
+
const errorRecord: BoundaryErrorRecord<FuncInput> = {
|
|
148
|
+
input: args,
|
|
149
|
+
error: error.message
|
|
150
|
+
}
|
|
128
151
|
|
|
129
|
-
if (hasRun) { runLog.push(
|
|
130
|
-
cacheTape.push(
|
|
152
|
+
if (hasRun) { runLog.push(errorRecord) }
|
|
153
|
+
cacheTape.push(errorRecord)
|
|
131
154
|
|
|
132
155
|
throw error
|
|
133
156
|
}
|
|
134
157
|
} else {
|
|
135
|
-
|
|
158
|
+
// Create a success record
|
|
159
|
+
const successRecord: BoundarySuccessRecord<FuncInput, FuncOutput> = {
|
|
160
|
+
input: args,
|
|
161
|
+
output: result as FuncOutput
|
|
162
|
+
}
|
|
136
163
|
|
|
137
|
-
if (hasRun) { runLog.push(
|
|
138
|
-
cacheTape.push(
|
|
164
|
+
if (hasRun) { runLog.push(successRecord) }
|
|
165
|
+
cacheTape.push(successRecord)
|
|
139
166
|
|
|
140
167
|
return result as ReturnType<Func>
|
|
141
168
|
}
|