@outputai/core 0.10.1-next.b7b2fbe.0 → 0.10.1-next.c717e35.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/package.json +8 -7
- package/src/bus.js +58 -20
- package/src/bus.spec.js +135 -77
- package/src/consts.js +2 -1
- package/src/helpers/fetch.js +1 -1
- package/src/helpers/object.js +48 -35
- package/src/helpers/object.spec.js +123 -75
- package/src/hooks/index.d.ts +48 -45
- package/src/hooks/index.js +56 -49
- package/src/hooks/index.spec.js +88 -97
- package/src/hooks/pending_hooks.js +24 -0
- package/src/hooks/pending_hooks.spec.js +62 -0
- package/src/interface/index.js +2 -1
- package/src/interface/workflow.js +92 -85
- package/src/interface/workflow.spec.js +135 -142
- package/src/interface/workflow_utils.d.ts +15 -0
- package/src/interface/workflow_utils.js +19 -0
- package/src/interface/workflow_utils.spec.js +82 -1
- package/src/sdk/helpers/objects.d.ts +24 -19
- package/src/sdk/runtime/events.d.ts +4 -6
- package/src/sdk/runtime/events.js +2 -15
- package/src/sdk/runtime/events.spec.js +14 -92
- package/src/worker/configs.js +0 -3
- package/src/worker/global_functions.js +2 -2
- package/src/worker/global_functions.spec.js +3 -3
- package/src/worker/index.js +14 -10
- package/src/worker/index.spec.js +13 -8
- package/src/worker/interceptors/activity.js +4 -4
- package/src/worker/interceptors/activity.spec.js +9 -9
- package/src/worker/interceptors/workflow.js +5 -5
- package/src/worker/interceptors/workflow.spec.js +7 -7
- package/src/worker/loader/activities.js +56 -40
- package/src/worker/loader/activities.spec.js +21 -22
- package/src/worker/loader/workflows.spec.js +1 -2
- package/src/worker/log_hooks.js +9 -9
- package/src/worker/log_hooks.spec.js +2 -2
- package/src/worker/sinks.js +8 -8
- package/src/worker/sinks.spec.js +9 -9
- package/src/worker/webpack_loaders/consts.js +6 -0
- package/src/worker/webpack_loaders/tools.js +1 -146
- package/src/worker/webpack_loaders/tools.spec.js +0 -23
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
- package/src/worker/proxy.js +0 -20
- package/src/worker/proxy.spec.js +0 -51
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
package/src/hooks/index.spec.js
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { BusEventType
|
|
2
|
+
import { BusEventType } from '#consts';
|
|
3
3
|
|
|
4
4
|
const logErrorMock = vi.hoisted( () => vi.fn() );
|
|
5
5
|
const createChildLoggerMock = vi.hoisted( () =>
|
|
6
6
|
vi.fn( () => ( { error: logErrorMock } ) )
|
|
7
7
|
);
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const mainOnHandlers = vi.hoisted( () => ( {} ) );
|
|
10
|
+
const stepOnHandlers = vi.hoisted( () => ( {} ) );
|
|
11
|
+
const mainEventBusMock = vi.hoisted( () => ( {
|
|
11
12
|
on: vi.fn( ( eventType, handler ) => {
|
|
12
|
-
|
|
13
|
+
mainOnHandlers[eventType] = handler;
|
|
14
|
+
} )
|
|
15
|
+
} ) );
|
|
16
|
+
const stepEventBusMock = vi.hoisted( () => ( {
|
|
17
|
+
emit: vi.fn( () => true ),
|
|
18
|
+
on: vi.fn( ( eventType, handler ) => {
|
|
19
|
+
stepOnHandlers[eventType] = handler;
|
|
13
20
|
} )
|
|
14
21
|
} ) );
|
|
15
22
|
|
|
16
23
|
vi.mock( '#logger', () => ( { createChildLogger: createChildLoggerMock } ) );
|
|
17
|
-
vi.mock( '#bus', () => ( {
|
|
24
|
+
vi.mock( '#bus', () => ( {
|
|
25
|
+
mainEventBus: mainEventBusMock,
|
|
26
|
+
stepEventBus: stepEventBusMock
|
|
27
|
+
} ) );
|
|
18
28
|
|
|
19
29
|
import {
|
|
30
|
+
emit,
|
|
20
31
|
on,
|
|
21
32
|
onActivityEnd,
|
|
22
33
|
onActivityError,
|
|
@@ -27,6 +38,7 @@ import {
|
|
|
27
38
|
onWorkflowError,
|
|
28
39
|
onWorkflowStart
|
|
29
40
|
} from './index.js';
|
|
41
|
+
import { pendingHooks } from './pending_hooks.js';
|
|
30
42
|
|
|
31
43
|
const workflowDetails = {
|
|
32
44
|
workflowId: 'wf-1',
|
|
@@ -38,11 +50,6 @@ const workflowDetails = {
|
|
|
38
50
|
attempt: 1
|
|
39
51
|
};
|
|
40
52
|
|
|
41
|
-
const catalogWorkflowDetails = {
|
|
42
|
-
...workflowDetails,
|
|
43
|
-
workflowType: WORKFLOW_CATALOG
|
|
44
|
-
};
|
|
45
|
-
|
|
46
53
|
const activityInfo = {
|
|
47
54
|
activityId: 'act-1',
|
|
48
55
|
activityType: 'wf#step',
|
|
@@ -61,8 +68,12 @@ const aggregations = {
|
|
|
61
68
|
describe( 'hooks/index', () => {
|
|
62
69
|
beforeEach( () => {
|
|
63
70
|
vi.clearAllMocks();
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
pendingHooks.clear();
|
|
72
|
+
Object.keys( mainOnHandlers ).forEach( k => {
|
|
73
|
+
delete mainOnHandlers[k];
|
|
74
|
+
} );
|
|
75
|
+
Object.keys( stepOnHandlers ).forEach( k => {
|
|
76
|
+
delete stepOnHandlers[k];
|
|
66
77
|
} );
|
|
67
78
|
} );
|
|
68
79
|
|
|
@@ -71,9 +82,9 @@ describe( 'hooks/index', () => {
|
|
|
71
82
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
72
83
|
onError( handler );
|
|
73
84
|
|
|
74
|
-
expect(
|
|
75
|
-
expect(
|
|
76
|
-
expect(
|
|
85
|
+
expect( mainEventBusMock.on ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, expect.any( Function ) );
|
|
86
|
+
expect( mainEventBusMock.on ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, expect.any( Function ) );
|
|
87
|
+
expect( mainEventBusMock.on ).toHaveBeenCalledWith( BusEventType.RUNTIME_ERROR, expect.any( Function ) );
|
|
77
88
|
} );
|
|
78
89
|
|
|
79
90
|
it( 'invokes handler with activity-shaped payload, forwarding bus fields', async () => {
|
|
@@ -81,7 +92,7 @@ describe( 'hooks/index', () => {
|
|
|
81
92
|
onError( handler );
|
|
82
93
|
|
|
83
94
|
const err = new Error( 'act-fail' );
|
|
84
|
-
await
|
|
95
|
+
await mainOnHandlers[BusEventType.ACTIVITY_ERROR]( {
|
|
85
96
|
eventId: 'evt-act-1',
|
|
86
97
|
eventDate,
|
|
87
98
|
activityInfo,
|
|
@@ -108,7 +119,7 @@ describe( 'hooks/index', () => {
|
|
|
108
119
|
onError( handler );
|
|
109
120
|
|
|
110
121
|
const err = new Error( 'wf-fail' );
|
|
111
|
-
await
|
|
122
|
+
await mainOnHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
112
123
|
eventId: 'evt-wf-1',
|
|
113
124
|
eventDate,
|
|
114
125
|
workflowDetails,
|
|
@@ -131,7 +142,7 @@ describe( 'hooks/index', () => {
|
|
|
131
142
|
onError( handler );
|
|
132
143
|
|
|
133
144
|
const error = new Error( 'rt' );
|
|
134
|
-
await
|
|
145
|
+
await mainOnHandlers[BusEventType.RUNTIME_ERROR]( { eventId: 'evt-rt-1', eventDate, error } );
|
|
135
146
|
|
|
136
147
|
expect( handler ).toHaveBeenCalledWith( { eventId: 'evt-rt-1', eventDate, source: 'runtime', error } );
|
|
137
148
|
} );
|
|
@@ -142,68 +153,52 @@ describe( 'hooks/index', () => {
|
|
|
142
153
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
143
154
|
onBeforeWorkerStart( handler );
|
|
144
155
|
|
|
145
|
-
expect(
|
|
146
|
-
await
|
|
156
|
+
expect( mainEventBusMock.on ).toHaveBeenCalledWith( BusEventType.WORKER_BEFORE_START, expect.any( Function ) );
|
|
157
|
+
await mainOnHandlers[BusEventType.WORKER_BEFORE_START]();
|
|
147
158
|
|
|
148
159
|
expect( handler ).toHaveBeenCalledWith( undefined );
|
|
149
160
|
} );
|
|
150
161
|
} );
|
|
151
162
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( {
|
|
158
|
-
eventId: 'evt-ignored', eventDate, workflowDetails: catalogWorkflowDetails
|
|
159
|
-
} ) );
|
|
160
|
-
expect( handler ).not.toHaveBeenCalled();
|
|
161
|
-
|
|
162
|
-
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( {
|
|
163
|
-
eventId: 'evt-start-1', eventDate, workflowDetails, extra: 'passthrough'
|
|
164
|
-
} ) );
|
|
165
|
-
expect( handler ).toHaveBeenCalledWith( {
|
|
166
|
-
eventId: 'evt-start-1', eventDate, workflowDetails, extra: 'passthrough'
|
|
167
|
-
} );
|
|
163
|
+
it( 'tracks a hook callback until it settles', async () => {
|
|
164
|
+
const deferred = { resolve: null };
|
|
165
|
+
const handlerPromise = new Promise( resolve => {
|
|
166
|
+
deferred.resolve = resolve;
|
|
168
167
|
} );
|
|
169
|
-
|
|
168
|
+
const handler = vi.fn( () => handlerPromise );
|
|
169
|
+
onActivityEnd( handler );
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
it( 'skips catalog workflow and forwards bus fields for real workflows', async () => {
|
|
173
|
-
const handler = vi.fn().mockResolvedValue( undefined );
|
|
174
|
-
onWorkflowEnd( handler );
|
|
171
|
+
const callbackPromise = mainOnHandlers[BusEventType.ACTIVITY_END]( { activityInfo } );
|
|
175
172
|
|
|
176
|
-
|
|
177
|
-
eventId: 'evt-ignored', eventDate, workflowDetails: catalogWorkflowDetails
|
|
178
|
-
} ) );
|
|
179
|
-
expect( handler ).not.toHaveBeenCalled();
|
|
173
|
+
expect( pendingHooks.size ).toBe( 1 );
|
|
180
174
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
expect( handler ).toHaveBeenCalledWith( {
|
|
185
|
-
eventId: 'evt-end-1', eventDate, workflowDetails, extra: 'passthrough'
|
|
186
|
-
} );
|
|
187
|
-
} );
|
|
175
|
+
deferred.resolve();
|
|
176
|
+
await callbackPromise;
|
|
177
|
+
expect( pendingHooks.size ).toBe( 0 );
|
|
188
178
|
} );
|
|
189
179
|
|
|
190
|
-
describe( '
|
|
191
|
-
|
|
180
|
+
describe( 'workflow lifecycle hooks', () => {
|
|
181
|
+
const cases = [
|
|
182
|
+
[ 'onWorkflowStart', onWorkflowStart, BusEventType.WORKFLOW_START, {} ],
|
|
183
|
+
[ 'onWorkflowEnd', onWorkflowEnd, BusEventType.WORKFLOW_END, {} ],
|
|
184
|
+
[ 'onWorkflowError', onWorkflowError, BusEventType.WORKFLOW_ERROR, { error: new Error( 'workflow failed' ) } ]
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
it.each( cases )( '%s forwards bus fields', async ( _name, registerHook, eventType, extraFields ) => {
|
|
192
188
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
193
|
-
const
|
|
194
|
-
|
|
189
|
+
const payload = {
|
|
190
|
+
eventId: 'evt-workflow-1',
|
|
191
|
+
eventDate,
|
|
192
|
+
workflowDetails,
|
|
193
|
+
extra: 'passthrough',
|
|
194
|
+
...extraFields
|
|
195
|
+
};
|
|
196
|
+
registerHook( handler );
|
|
195
197
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
} ) );
|
|
199
|
-
expect( handler ).not.toHaveBeenCalled();
|
|
198
|
+
expect( mainEventBusMock.on ).toHaveBeenCalledWith( eventType, expect.any( Function ) );
|
|
199
|
+
await mainOnHandlers[eventType]( payload );
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
eventId: 'evt-err-1', eventDate, workflowDetails, error: err, extra: 'passthrough'
|
|
203
|
-
} ) );
|
|
204
|
-
expect( handler ).toHaveBeenCalledWith( {
|
|
205
|
-
eventId: 'evt-err-1', eventDate, workflowDetails, error: err, extra: 'passthrough'
|
|
206
|
-
} );
|
|
201
|
+
expect( handler ).toHaveBeenCalledWith( payload );
|
|
207
202
|
} );
|
|
208
203
|
} );
|
|
209
204
|
|
|
@@ -214,53 +209,49 @@ describe( 'hooks/index', () => {
|
|
|
214
209
|
[ 'onActivityError', onActivityError, BusEventType.ACTIVITY_ERROR, { error: new Error( 'activity failed' ) } ]
|
|
215
210
|
];
|
|
216
211
|
|
|
217
|
-
it.each( cases )( '%s
|
|
212
|
+
it.each( cases )( '%s forwards internal activity bus fields', async ( _name, registerHook, eventType, extraFields = {} ) => {
|
|
218
213
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
expect( messageBusMock.on ).toHaveBeenCalledWith( eventType, expect.any( Function ) );
|
|
222
|
-
|
|
223
|
-
await Promise.resolve( onHandlers[eventType]( {
|
|
224
|
-
eventId: 'evt-ignored',
|
|
225
|
-
eventDate,
|
|
226
|
-
activityInfo,
|
|
227
|
-
workflowDetails,
|
|
228
|
-
outputActivityKind: ComponentType.INTERNAL_STEP,
|
|
229
|
-
...extraFields
|
|
230
|
-
} ) );
|
|
231
|
-
expect( handler ).not.toHaveBeenCalled();
|
|
232
|
-
|
|
233
|
-
await Promise.resolve( onHandlers[eventType]( {
|
|
214
|
+
const payload = {
|
|
234
215
|
eventId: 'evt-activity-1',
|
|
235
216
|
eventDate,
|
|
236
217
|
activityInfo,
|
|
237
218
|
workflowDetails,
|
|
238
|
-
outputActivityKind:
|
|
219
|
+
outputActivityKind: 'internal_step',
|
|
239
220
|
extra: 'passthrough',
|
|
240
221
|
...extraFields
|
|
241
|
-
}
|
|
222
|
+
};
|
|
223
|
+
registerHook( handler );
|
|
242
224
|
|
|
243
|
-
expect(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
workflowDetails,
|
|
248
|
-
outputActivityKind: ComponentType.STEP,
|
|
249
|
-
extra: 'passthrough',
|
|
250
|
-
...extraFields
|
|
251
|
-
} );
|
|
225
|
+
expect( mainEventBusMock.on ).toHaveBeenCalledWith( eventType, expect.any( Function ) );
|
|
226
|
+
await mainOnHandlers[eventType]( payload );
|
|
227
|
+
|
|
228
|
+
expect( handler ).toHaveBeenCalledWith( payload );
|
|
252
229
|
} );
|
|
253
230
|
} );
|
|
254
231
|
|
|
255
232
|
describe( 'on', () => {
|
|
256
|
-
it( 'subscribes to
|
|
233
|
+
it( 'subscribes to SDK and user event channels and forwards payloads', async () => {
|
|
257
234
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
258
235
|
on( 'myEvent', handler );
|
|
259
236
|
|
|
260
|
-
expect(
|
|
261
|
-
|
|
237
|
+
expect( stepEventBusMock.on ).toHaveBeenCalledWith( 'sdk:myEvent', expect.any( Function ) );
|
|
238
|
+
expect( stepEventBusMock.on ).toHaveBeenCalledWith( 'usr:myEvent', expect.any( Function ) );
|
|
239
|
+
await stepOnHandlers['sdk:myEvent']( { payload: { source: 'sdk' } } );
|
|
240
|
+
await stepOnHandlers['usr:myEvent']( { payload: { source: 'user' } } );
|
|
241
|
+
|
|
242
|
+
expect( handler ).toHaveBeenNthCalledWith( 1, { payload: { source: 'sdk' } } );
|
|
243
|
+
expect( handler ).toHaveBeenNthCalledWith( 2, { payload: { source: 'user' } } );
|
|
244
|
+
} );
|
|
245
|
+
} );
|
|
246
|
+
|
|
247
|
+
describe( 'emit', () => {
|
|
248
|
+
it( 'emits payloads on the user event channel', () => {
|
|
249
|
+
const payload = { foo: 1 };
|
|
250
|
+
|
|
251
|
+
const emitted = emit( 'myEvent', payload );
|
|
262
252
|
|
|
263
|
-
expect(
|
|
253
|
+
expect( stepEventBusMock.emit ).toHaveBeenCalledWith( 'usr:myEvent', payload );
|
|
254
|
+
expect( emitted ).toBe( true );
|
|
264
255
|
} );
|
|
265
256
|
} );
|
|
266
257
|
} );
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createChildLogger } from '#logger';
|
|
2
|
+
|
|
3
|
+
export const pendingHooks = new Set();
|
|
4
|
+
|
|
5
|
+
const log = createChildLogger( 'Hooks' );
|
|
6
|
+
const flushTimeoutMs = 30_000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Await all pending hooks to flush for a certain time
|
|
10
|
+
*/
|
|
11
|
+
export const flushPendingHooks = async () => {
|
|
12
|
+
const state = { timeout: null };
|
|
13
|
+
try {
|
|
14
|
+
const flushed = await Promise.race( [
|
|
15
|
+
Promise.allSettled( [ ...pendingHooks ] ).then( _ => true ),
|
|
16
|
+
new Promise( r => state.timeout = setTimeout( () => r( false ), flushTimeoutMs ) )
|
|
17
|
+
] );
|
|
18
|
+
if ( !flushed ) {
|
|
19
|
+
log.warn( 'Some hook callbacks exceeded the timeout and will not be awaited', { count: pendingHooks.size } );
|
|
20
|
+
}
|
|
21
|
+
} finally {
|
|
22
|
+
clearTimeout( state.timeout );
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const logMock = vi.hoisted( () => ( { warn: vi.fn() } ) );
|
|
4
|
+
|
|
5
|
+
vi.mock( '#logger', () => ( { createChildLogger: () => logMock } ) );
|
|
6
|
+
|
|
7
|
+
import { flushPendingHooks, pendingHooks } from './pending_hooks.js';
|
|
8
|
+
|
|
9
|
+
describe( 'pending hooks', () => {
|
|
10
|
+
beforeEach( () => {
|
|
11
|
+
pendingHooks.clear();
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
} );
|
|
14
|
+
|
|
15
|
+
afterEach( () => {
|
|
16
|
+
pendingHooks.clear();
|
|
17
|
+
vi.useRealTimers();
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
it( 'waits for pending hooks to settle', async () => {
|
|
21
|
+
const deferred = { resolve: null };
|
|
22
|
+
const hookPromise = new Promise( resolve => {
|
|
23
|
+
deferred.resolve = resolve;
|
|
24
|
+
} );
|
|
25
|
+
pendingHooks.add( hookPromise );
|
|
26
|
+
|
|
27
|
+
const flushPromise = flushPendingHooks();
|
|
28
|
+
const state = { flushed: false };
|
|
29
|
+
flushPromise.then( () => {
|
|
30
|
+
state.flushed = true;
|
|
31
|
+
} );
|
|
32
|
+
|
|
33
|
+
await Promise.resolve();
|
|
34
|
+
expect( state.flushed ).toBe( false );
|
|
35
|
+
|
|
36
|
+
deferred.resolve();
|
|
37
|
+
await flushPromise;
|
|
38
|
+
expect( state.flushed ).toBe( true );
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
it( 'stops waiting after the timeout', async () => {
|
|
42
|
+
vi.useFakeTimers();
|
|
43
|
+
pendingHooks.add( new Promise( () => {} ) );
|
|
44
|
+
|
|
45
|
+
const flushPromise = flushPendingHooks();
|
|
46
|
+
await vi.advanceTimersByTimeAsync( 30_000 );
|
|
47
|
+
|
|
48
|
+
await expect( flushPromise ).resolves.toBeUndefined();
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
it( 'logs the number of hooks that exceeded the timeout', async () => {
|
|
52
|
+
vi.useFakeTimers();
|
|
53
|
+
pendingHooks.add( new Promise( () => {} ) );
|
|
54
|
+
pendingHooks.add( new Promise( () => {} ) );
|
|
55
|
+
|
|
56
|
+
const flushPromise = flushPendingHooks();
|
|
57
|
+
await vi.advanceTimersByTimeAsync( 30_000 );
|
|
58
|
+
await flushPromise;
|
|
59
|
+
|
|
60
|
+
expect( logMock.warn ).toHaveBeenCalledWith( expect.any( String ), { count: 2 } );
|
|
61
|
+
} );
|
|
62
|
+
} );
|
package/src/interface/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { evaluator } from './evaluator.js';
|
|
9
9
|
import { step } from './step.js';
|
|
10
10
|
import { workflow } from './workflow.js';
|
|
11
|
-
import { executeInParallel } from './workflow_utils.js';
|
|
11
|
+
import { executeInParallel, hasErrorType } from './workflow_utils.js';
|
|
12
12
|
import { sendHttpRequest, sendPostRequestAndAwaitWebhook } from './webhook.js';
|
|
13
13
|
import { Logger } from './logger.js';
|
|
14
14
|
|
|
@@ -23,6 +23,7 @@ export {
|
|
|
23
23
|
EvaluationVerdictResult,
|
|
24
24
|
EvaluationFeedback,
|
|
25
25
|
executeInParallel,
|
|
26
|
+
hasErrorType,
|
|
26
27
|
sendHttpRequest,
|
|
27
28
|
sendPostRequestAndAwaitWebhook
|
|
28
29
|
};
|
|
@@ -7,104 +7,111 @@ import { TraceInfo } from '#helpers/trace_info';
|
|
|
7
7
|
import { deepMerge } from '#helpers/object';
|
|
8
8
|
import { defaultOptions } from './workflow_activity_options.js';
|
|
9
9
|
import { createWorkflow } from '#helpers/component';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
METADATA_ACCESS_SYMBOL,
|
|
13
|
-
SHARED_STEP_PREFIX,
|
|
14
|
-
WORKFLOW_WRAPPER_VERSION_FIELD
|
|
15
|
-
} from '#consts';
|
|
10
|
+
import { FatalError } from '#errors';
|
|
11
|
+
import * as C from '#consts';
|
|
16
12
|
|
|
17
13
|
/**
|
|
18
|
-
*
|
|
14
|
+
* Execute the workflow without temporal, using the fn handler function.
|
|
15
|
+
* This is important to allow for workflows to have unit tests
|
|
19
16
|
*/
|
|
17
|
+
const executeWithoutTemporal = async ( { input, validator, handler, contextOverrides = {} } ) => {
|
|
18
|
+
validator.validateInput( input );
|
|
19
|
+
const output = await handler( input, deepMerge( WorkflowContext.build(), contextOverrides ) );
|
|
20
|
+
validator.validateOutput( output );
|
|
21
|
+
return output;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Add a global dispatcher function to be used to invoke activities.
|
|
26
|
+
* This will replace direct activity invocation in the user code by the webpack loader.
|
|
27
|
+
*
|
|
28
|
+
* Important: Keep this as a configurable global assignment (configurable=true, enumerable=true),
|
|
29
|
+
* so Temporal's reusable VM can delete it when switching workflow scopes.
|
|
30
|
+
*/
|
|
31
|
+
const createGlobalDispatcher = ( { runId, workflowType, activities } ) => {
|
|
32
|
+
const dispatcher = async ( activityType, ...args ) => activities[`${workflowType}#${activityType}`]( ...args ).then( r => r.output );
|
|
33
|
+
dispatcher.runId = runId;
|
|
34
|
+
globalThis[C.INVOKE_ACTIVITY_SYMBOL] = dispatcher;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Validate if the global dispatcher wasn't set by another workflow, indicating global context contamination. */
|
|
38
|
+
const checkGlobalContextContamination = runId => {
|
|
39
|
+
const globalContextRunId = globalThis?.[C.INVOKE_ACTIVITY_SYMBOL]?.runId;
|
|
40
|
+
if ( globalContextRunId && globalContextRunId !== runId ) {
|
|
41
|
+
throw new FatalError( 'Contamination of the workflow Node global context.' +
|
|
42
|
+
` Var "globalThis[${String( C.INVOKE_ACTIVITY_SYMBOL )}]" was set by another workflow (${globalContextRunId})` );
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Create a new workflow and return a wrapper function around its fn handler */
|
|
20
47
|
export function workflow( { name, description, inputSchema, outputSchema, fn, options = {}, aliases = [] } ) {
|
|
21
48
|
WorkflowValidator.validateDefinition( { name, description, inputSchema, outputSchema, fn, options, aliases } );
|
|
22
49
|
|
|
23
|
-
|
|
50
|
+
// Disable trace can only be defined at the definition level
|
|
51
|
+
const disableTrace = options.disableTrace ?? defaultOptions.disableTrace;
|
|
24
52
|
const validator = new WorkflowValidator( { name, inputSchema, outputSchema } );
|
|
25
53
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
description,
|
|
29
|
-
inputSchema,
|
|
30
|
-
outputSchema,
|
|
31
|
-
options,
|
|
32
|
-
aliases,
|
|
33
|
-
handler: async ( input, extra = {} ) => {
|
|
34
|
-
validator.validateInvocationOptions( extra );
|
|
35
|
-
|
|
36
|
-
// this returns a plain function, for example, in unit tests
|
|
37
|
-
if ( !inWorkflowContext() ) {
|
|
38
|
-
validator.validateInput( input );
|
|
39
|
-
const output = await fn( input, deepMerge( WorkflowContext.build(), extra?.context ) );
|
|
40
|
-
validator.validateOutput( output );
|
|
41
|
-
return output;
|
|
42
|
-
}
|
|
54
|
+
const handler = async ( input, invocationOptions = {} ) => {
|
|
55
|
+
validator.validateInvocationOptions( invocationOptions );
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if ( isChild ) {
|
|
51
|
-
const result = await executeChild( name, {
|
|
52
|
-
args: undefined === input ? [] : [ input ],
|
|
53
|
-
workflowId: `${workflowId}-${toUrlSafeBase64( uuid4() )}`,
|
|
54
|
-
parentClosePolicy: ParentClosePolicy[extra?.detached ? 'ABANDON' : 'TERMINATE'],
|
|
55
|
-
memo: {
|
|
56
|
-
...memo, // Preserve memo and mix activityOptions, if provided
|
|
57
|
-
...( extra?.activityOptions && {
|
|
58
|
-
activityOptions: deepMerge( memo?.activityOptions ?? {}, extra?.activityOptions )
|
|
59
|
-
} )
|
|
60
|
-
}
|
|
61
|
-
} );
|
|
62
|
-
return result.output;
|
|
63
|
-
}
|
|
57
|
+
// If called outside Temporal workflow context, just execute the handler function
|
|
58
|
+
if ( !inWorkflowContext() ) {
|
|
59
|
+
return executeWithoutTemporal( { input, validator, handler: fn, contextOverrides: invocationOptions?.context } );
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { workflowId, runId, memo, root } = workflowInfo();
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
checkGlobalContextContamination( runId );
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
// If the parent workflow already installed the activity dispatcher, it means that calls to workflow() will trigger child workflows
|
|
67
|
+
const isChildWorkflowCall = !!globalThis[C.INVOKE_ACTIVITY_SYMBOL];
|
|
68
|
+
if ( isChildWorkflowCall ) {
|
|
69
|
+
const parentClosePolicy = ParentClosePolicy[invocationOptions?.detached ? 'ABANDON' : 'TERMINATE'];
|
|
70
|
+
const childWorkflowId = `${workflowId}-${toUrlSafeBase64( uuid4() )}`;
|
|
71
|
+
const args = [ input, { activityOptions: invocationOptions?.activityOptions } ];
|
|
72
|
+
return executeChild( name, { args, workflowId: childWorkflowId, parentClosePolicy, memo } ).then( r => r.output );
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isRoot = !root; // Check if this is the root most workflow
|
|
76
|
+
|
|
77
|
+
// Trace info is only added in the root and only when trace is not disabled
|
|
78
|
+
if ( isRoot && !disableTrace ) {
|
|
79
|
+
memo.traceInfo = TraceInfo.build();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Resolve the activity options: invocation options > definition options > parent options > default options
|
|
83
|
+
const activityOptions = deepMerge(
|
|
84
|
+
defaultOptions.activityOptions, // default
|
|
85
|
+
memo?.parentActivityOptions, // parent options
|
|
86
|
+
options?.activityOptions, // definition options
|
|
87
|
+
invocationOptions.activityOptions // invocation options
|
|
88
|
+
);
|
|
89
|
+
// Resolved activity options are added to memo so child workflow executions can continue the policy chain.
|
|
90
|
+
memo.parentActivityOptions = activityOptions;
|
|
91
|
+
const activities = proxyActivities( activityOptions );
|
|
92
|
+
|
|
93
|
+
createGlobalDispatcher( { runId, workflowType: name, activities } );
|
|
94
|
+
|
|
95
|
+
const traceDestinations = isRoot && {
|
|
96
|
+
trace: {
|
|
97
|
+
destinations: disableTrace ? {} : await activities[C.ACTIVITY_GET_TRACE_DESTINATIONS]( memo.traceInfo ).then( r => r.output ) ?? {}
|
|
73
98
|
}
|
|
99
|
+
};
|
|
74
100
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Creates an activity caller based on a prefix
|
|
86
|
-
const createCaller = prefix => async ( t, ...args ) => ( await steps[`${prefix}#${t}`]( ...args ) ).output;
|
|
87
|
-
|
|
88
|
-
// This are functions used by the AST to replace direct activity (step/evaluator) calls
|
|
89
|
-
const dispatchers = {
|
|
90
|
-
invokeStep: createCaller( name ),
|
|
91
|
-
invokeSharedStep: createCaller( SHARED_STEP_PREFIX ),
|
|
92
|
-
invokeEvaluator: createCaller( name ),
|
|
93
|
-
invokeSharedEvaluator: createCaller( SHARED_STEP_PREFIX )
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
// The workflow function execution with "this" set with the dispatchers
|
|
97
|
-
const output = await fn.call( dispatchers, input, WorkflowContext.build() );
|
|
98
|
-
validator.validateOutput( output );
|
|
99
|
-
|
|
100
|
-
return { [WORKFLOW_WRAPPER_VERSION_FIELD]: 1, output, ...traceDestinations };
|
|
101
|
-
} catch ( error ) {
|
|
102
|
-
if ( traceDestinations ) {
|
|
103
|
-
// Append the trace destinations so it is carried to interceptor
|
|
104
|
-
error[METADATA_ACCESS_SYMBOL] = traceDestinations;
|
|
105
|
-
}
|
|
106
|
-
throw error;
|
|
101
|
+
try {
|
|
102
|
+
validator.validateInput( input );
|
|
103
|
+
const output = await fn( input, WorkflowContext.build() );
|
|
104
|
+
validator.validateOutput( output );
|
|
105
|
+
|
|
106
|
+
return { [C.WORKFLOW_WRAPPER_VERSION_FIELD]: 1, output, ...traceDestinations };
|
|
107
|
+
} catch ( error ) {
|
|
108
|
+
if ( traceDestinations ) {
|
|
109
|
+
// Append the trace destinations so it is carried to interceptor
|
|
110
|
+
error[C.METADATA_ACCESS_SYMBOL] = traceDestinations;
|
|
107
111
|
}
|
|
112
|
+
throw error;
|
|
108
113
|
}
|
|
109
|
-
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return createWorkflow( { name, description, inputSchema, outputSchema, options, aliases, handler } );
|
|
110
117
|
}
|