@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.
Files changed (55) hide show
  1. package/package.json +8 -7
  2. package/src/bus.js +58 -20
  3. package/src/bus.spec.js +135 -77
  4. package/src/consts.js +2 -1
  5. package/src/helpers/fetch.js +1 -1
  6. package/src/helpers/object.js +48 -35
  7. package/src/helpers/object.spec.js +123 -75
  8. package/src/hooks/index.d.ts +48 -45
  9. package/src/hooks/index.js +56 -49
  10. package/src/hooks/index.spec.js +88 -97
  11. package/src/hooks/pending_hooks.js +24 -0
  12. package/src/hooks/pending_hooks.spec.js +62 -0
  13. package/src/interface/index.js +2 -1
  14. package/src/interface/workflow.js +92 -85
  15. package/src/interface/workflow.spec.js +135 -142
  16. package/src/interface/workflow_utils.d.ts +15 -0
  17. package/src/interface/workflow_utils.js +19 -0
  18. package/src/interface/workflow_utils.spec.js +82 -1
  19. package/src/sdk/helpers/objects.d.ts +24 -19
  20. package/src/sdk/runtime/events.d.ts +4 -6
  21. package/src/sdk/runtime/events.js +2 -15
  22. package/src/sdk/runtime/events.spec.js +14 -92
  23. package/src/worker/configs.js +0 -3
  24. package/src/worker/global_functions.js +2 -2
  25. package/src/worker/global_functions.spec.js +3 -3
  26. package/src/worker/index.js +14 -10
  27. package/src/worker/index.spec.js +13 -8
  28. package/src/worker/interceptors/activity.js +4 -4
  29. package/src/worker/interceptors/activity.spec.js +9 -9
  30. package/src/worker/interceptors/workflow.js +5 -5
  31. package/src/worker/interceptors/workflow.spec.js +7 -7
  32. package/src/worker/loader/activities.js +56 -40
  33. package/src/worker/loader/activities.spec.js +21 -22
  34. package/src/worker/loader/workflows.spec.js +1 -2
  35. package/src/worker/log_hooks.js +9 -9
  36. package/src/worker/log_hooks.spec.js +2 -2
  37. package/src/worker/sinks.js +8 -8
  38. package/src/worker/sinks.spec.js +9 -9
  39. package/src/worker/webpack_loaders/consts.js +6 -0
  40. package/src/worker/webpack_loaders/tools.js +1 -146
  41. package/src/worker/webpack_loaders/tools.spec.js +0 -23
  42. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
  43. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
  44. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
  45. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
  46. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
  47. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
  48. package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
  49. package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
  50. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
  51. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
  52. package/src/worker/proxy.js +0 -20
  53. package/src/worker/proxy.spec.js +0 -51
  54. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
  55. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
@@ -1,22 +1,33 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { BusEventType, ComponentType, WORKFLOW_CATALOG } from '#consts';
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 onHandlers = vi.hoisted( () => ( {} ) );
10
- const messageBusMock = vi.hoisted( () => ( {
9
+ const mainOnHandlers = vi.hoisted( () => ( {} ) );
10
+ const stepOnHandlers = vi.hoisted( () => ( {} ) );
11
+ const mainEventBusMock = vi.hoisted( () => ( {
11
12
  on: vi.fn( ( eventType, handler ) => {
12
- onHandlers[eventType] = handler;
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', () => ( { messageBus: messageBusMock } ) );
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
- Object.keys( onHandlers ).forEach( k => {
65
- delete onHandlers[k];
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( messageBusMock.on ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, expect.any( Function ) );
75
- expect( messageBusMock.on ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, expect.any( Function ) );
76
- expect( messageBusMock.on ).toHaveBeenCalledWith( BusEventType.RUNTIME_ERROR, expect.any( Function ) );
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 onHandlers[BusEventType.ACTIVITY_ERROR]( {
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 onHandlers[BusEventType.WORKFLOW_ERROR]( {
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 onHandlers[BusEventType.RUNTIME_ERROR]( { eventId: 'evt-rt-1', eventDate, error } );
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( messageBusMock.on ).toHaveBeenCalledWith( BusEventType.WORKER_BEFORE_START, expect.any( Function ) );
146
- await onHandlers[BusEventType.WORKER_BEFORE_START]();
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
- describe( 'onWorkflowStart', () => {
153
- it( 'skips catalog workflow and forwards bus fields for real workflows', async () => {
154
- const handler = vi.fn().mockResolvedValue( undefined );
155
- onWorkflowStart( handler );
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
- describe( 'onWorkflowEnd', () => {
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
- await Promise.resolve( onHandlers[BusEventType.WORKFLOW_END]( {
177
- eventId: 'evt-ignored', eventDate, workflowDetails: catalogWorkflowDetails
178
- } ) );
179
- expect( handler ).not.toHaveBeenCalled();
173
+ expect( pendingHooks.size ).toBe( 1 );
180
174
 
181
- await Promise.resolve( onHandlers[BusEventType.WORKFLOW_END]( {
182
- eventId: 'evt-end-1', eventDate, workflowDetails, extra: 'passthrough'
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( 'onWorkflowError', () => {
191
- it( 'skips catalog workflow and forwards bus fields for real workflows', async () => {
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 err = new Error( 'wf' );
194
- onWorkflowError( handler );
189
+ const payload = {
190
+ eventId: 'evt-workflow-1',
191
+ eventDate,
192
+ workflowDetails,
193
+ extra: 'passthrough',
194
+ ...extraFields
195
+ };
196
+ registerHook( handler );
195
197
 
196
- await Promise.resolve( onHandlers[BusEventType.WORKFLOW_ERROR]( {
197
- eventId: 'evt-ignored', eventDate, workflowDetails: catalogWorkflowDetails, error: err
198
- } ) );
199
- expect( handler ).not.toHaveBeenCalled();
198
+ expect( mainEventBusMock.on ).toHaveBeenCalledWith( eventType, expect.any( Function ) );
199
+ await mainOnHandlers[eventType]( payload );
200
200
 
201
- await Promise.resolve( onHandlers[BusEventType.WORKFLOW_ERROR]( {
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 skips internal activities and forwards bus fields', async ( _name, registerHook, eventType, extraFields = {} ) => {
212
+ it.each( cases )( '%s forwards internal activity bus fields', async ( _name, registerHook, eventType, extraFields = {} ) => {
218
213
  const handler = vi.fn().mockResolvedValue( undefined );
219
- registerHook( handler );
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: ComponentType.STEP,
219
+ outputActivityKind: 'internal_step',
239
220
  extra: 'passthrough',
240
221
  ...extraFields
241
- } ) );
222
+ };
223
+ registerHook( handler );
242
224
 
243
- expect( handler ).toHaveBeenCalledWith( {
244
- eventId: 'evt-activity-1',
245
- eventDate,
246
- activityInfo,
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 external event channel and forwards payload', async () => {
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( messageBusMock.on ).toHaveBeenCalledWith( 'external:myEvent', expect.any( Function ) );
261
- await onHandlers['external:myEvent']( { foo: 1 } );
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( handler ).toHaveBeenCalledWith( { foo: 1 } );
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
+ } );
@@ -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
- ACTIVITY_GET_TRACE_DESTINATIONS,
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
- * Create a new workflow and return a wrapper function around its fn handler
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
- const { disableTrace, activityOptions } = deepMerge( defaultOptions, options );
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
- return createWorkflow( {
27
- name,
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
- const { workflowId, memo, root } = workflowInfo();
45
-
46
- // if the stack already includes this workflowId, means the workflow() function was called
47
- // from within a running workflow, meaning it is suppose to start a child workflow
48
- const isChild = Array.isArray( memo.stack ) ? memo.stack.includes( workflowId ) : false;
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
- const isRoot = !root;
64
+ checkGlobalContextContamination( runId );
66
65
 
67
- memo.stack = [ ...memo.stack ?? [], workflowId ];
68
- // Parent options have prevalence on nested calls, child will be overwritten
69
- memo.activityOptions = deepMerge( activityOptions, memo.activityOptions );
70
- // Trace info is only added in the root and only when trace is not disabled
71
- if ( isRoot && !disableTrace ) {
72
- memo.traceInfo = TraceInfo.build();
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
- const steps = proxyActivities( memo.activityOptions );
76
- const traceDestinations = isRoot && {
77
- trace: {
78
- destinations: disableTrace ? {} : ( await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( memo.traceInfo ) ).output ?? {}
79
- }
80
- };
81
-
82
- try {
83
- validator.validateInput( input );
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
  }