@outputai/core 0.10.0 → 0.10.1-next.09ed166.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 (45) hide show
  1. package/package.json +1 -1
  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/object.js +48 -35
  6. package/src/helpers/object.spec.js +123 -75
  7. package/src/hooks/index.d.ts +48 -45
  8. package/src/hooks/index.js +49 -44
  9. package/src/hooks/index.spec.js +74 -102
  10. package/src/interface/workflow.js +92 -85
  11. package/src/interface/workflow.spec.js +135 -142
  12. package/src/sdk/helpers/objects.d.ts +24 -19
  13. package/src/sdk/runtime/events.d.ts +4 -6
  14. package/src/sdk/runtime/events.js +2 -15
  15. package/src/sdk/runtime/events.spec.js +14 -92
  16. package/src/worker/global_functions.js +2 -2
  17. package/src/worker/global_functions.spec.js +3 -3
  18. package/src/worker/index.js +3 -3
  19. package/src/worker/index.spec.js +6 -6
  20. package/src/worker/interceptors/activity.js +4 -4
  21. package/src/worker/interceptors/activity.spec.js +9 -9
  22. package/src/worker/interceptors/workflow.js +5 -5
  23. package/src/worker/interceptors/workflow.spec.js +7 -7
  24. package/src/worker/loader/activities.js +56 -40
  25. package/src/worker/loader/activities.spec.js +21 -22
  26. package/src/worker/loader/workflows.spec.js +1 -2
  27. package/src/worker/log_hooks.js +9 -9
  28. package/src/worker/log_hooks.spec.js +2 -2
  29. package/src/worker/sinks.js +8 -8
  30. package/src/worker/sinks.spec.js +9 -9
  31. package/src/worker/webpack_loaders/consts.js +6 -0
  32. package/src/worker/webpack_loaders/tools.js +1 -146
  33. package/src/worker/webpack_loaders/tools.spec.js +0 -23
  34. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
  35. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
  36. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
  37. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
  38. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
  39. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
  40. package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
  41. package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
  42. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
  43. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
  44. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
  45. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
@@ -1,118 +1,40 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
 
3
- const loadMock = vi.hoisted( () => vi.fn() );
4
3
  const emitMock = vi.hoisted( () => vi.fn() );
5
4
 
6
- vi.mock( '#async_storage', () => ( {
7
- Storage: { load: loadMock }
8
- } ) );
9
-
10
5
  vi.mock( '#bus', () => ( {
11
- messageBus: { emit: emitMock }
6
+ stepEventBus: { emit: emitMock }
12
7
  } ) );
13
8
 
14
9
  import { Event } from './events.js';
15
10
 
16
- // `eventId` stamping is the bus layer's responsibility (see bus.spec.js).
17
- // Assertions here use `objectContaining` so they don't have to know about that enrichment.
18
11
  describe( 'Event.emit', () => {
19
12
  beforeEach( () => {
20
13
  vi.clearAllMocks();
21
14
  } );
22
15
 
23
- it( 'forwards activityInfo, workflowDetails, and outputActivityKind from storage', () => {
24
- const activityInfo = {
25
- activityId: 'act-1',
26
- activityType: 'step',
27
- workflowExecution: { workflowId: 'wf-1', runId: 'run-1' },
28
- workflowType: 'workflow'
29
- };
30
- const workflowDetails = {
31
- workflowId: 'wf-1',
32
- runId: 'run-1',
33
- workflowType: 'workflow'
34
- };
35
- loadMock.mockReturnValue( {
36
- activityInfo,
37
- workflowDetails,
38
- outputActivityKind: 'step'
39
- } );
16
+ it( 'forwards class instance payloads unchanged', () => {
17
+ class Payload {
18
+ value = 'test';
19
+ }
20
+ const payload = new Payload();
40
21
 
41
- Event.emit( 'cost:llm:request', { modelId: 'gpt-4o' } );
22
+ Event.emit( 'test:event', payload );
42
23
 
43
- expect( emitMock ).toHaveBeenCalledWith( 'external:cost:llm:request', expect.objectContaining( {
44
- activityInfo,
45
- workflowDetails,
46
- outputActivityKind: 'step',
47
- modelId: 'gpt-4o'
48
- } ) );
24
+ expect( emitMock ).toHaveBeenCalledWith( 'sdk:test:event', payload );
49
25
  } );
50
26
 
51
- it( 'emits payload without context when storage is missing', () => {
52
- loadMock.mockReturnValue( undefined );
27
+ it( 'emits plain object payloads on the SDK event channel', () => {
28
+ const payload = { modelId: 'gpt-4o' };
53
29
 
54
- Event.emit( 'foo:bar', { x: 1 } );
30
+ Event.emit( 'cost:llm:request', payload );
55
31
 
56
- expect( emitMock ).toHaveBeenCalledWith( 'external:foo:bar', { x: 1 } );
32
+ expect( emitMock ).toHaveBeenCalledWith( 'sdk:cost:llm:request', payload );
57
33
  } );
58
34
 
59
- it( 'handles missing payload', () => {
60
- const activityInfo = {
61
- activityId: 'act-2',
62
- activityType: 'step',
63
- workflowExecution: { workflowId: 'wf-2', runId: 'run-2' },
64
- workflowType: 'workflow'
65
- };
66
- const workflowDetails = {
67
- workflowId: 'wf-2',
68
- runId: 'run-2',
69
- workflowType: 'workflow'
70
- };
71
- loadMock.mockReturnValue( {
72
- activityInfo,
73
- workflowDetails,
74
- outputActivityKind: 'step'
75
- } );
76
-
35
+ it( 'forwards a missing payload as undefined', () => {
77
36
  Event.emit( 'lifecycle:start' );
78
37
 
79
- expect( emitMock ).toHaveBeenCalledWith( 'external:lifecycle:start', expect.objectContaining( {
80
- activityInfo,
81
- workflowDetails,
82
- outputActivityKind: 'step'
83
- } ) );
84
- } );
85
-
86
- it( 'does not let payload override activityInfo, workflowDetails, or outputActivityKind', () => {
87
- const activityInfo = {
88
- activityId: 'act-3',
89
- activityType: 'step',
90
- workflowExecution: { workflowId: 'wf-3', runId: 'run-3' },
91
- workflowType: 'workflow'
92
- };
93
- const workflowDetails = {
94
- workflowId: 'wf-3',
95
- runId: 'run-3',
96
- workflowType: 'workflow'
97
- };
98
- loadMock.mockReturnValue( {
99
- activityInfo,
100
- workflowDetails,
101
- outputActivityKind: 'step'
102
- } );
103
-
104
- Event.emit( 'cost:http:request', {
105
- activityInfo: { activityId: 'should-be-overridden' },
106
- workflowDetails: { workflowId: 'should-be-overridden' },
107
- outputActivityKind: 'should-be-overridden',
108
- url: 'https://example.com'
109
- } );
110
-
111
- expect( emitMock ).toHaveBeenCalledWith( 'external:cost:http:request', expect.objectContaining( {
112
- activityInfo,
113
- workflowDetails,
114
- outputActivityKind: 'step',
115
- url: 'https://example.com'
116
- } ) );
38
+ expect( emitMock ).toHaveBeenCalledWith( 'sdk:lifecycle:start', undefined );
117
39
  } );
118
40
  } );
@@ -1,4 +1,4 @@
1
- import { messageBus } from '#bus';
1
+ import { mainEventBus } from '#bus';
2
2
  import { ACTIVITY_LOGGER_SYMBOL, BusEventType } from '#consts';
3
3
  import { activityInfo as activityInfoFn } from '@temporalio/activity';
4
4
  import { assignImmutableProperty } from '#helpers/object';
@@ -9,6 +9,6 @@ import { assignImmutableProperty } from '#helpers/object';
9
9
  export const bindGlobalFunctions = () => {
10
10
  /** Defines the activity logger function, accessible in activity context via logger interface */
11
11
  assignImmutableProperty( globalThis, ACTIVITY_LOGGER_SYMBOL, ( { level, message, metadata } ) =>
12
- messageBus.emit( BusEventType.ACTIVITY_LOG, { level, message, metadata, activityInfo: activityInfoFn() } )
12
+ mainEventBus.emit( BusEventType.ACTIVITY_LOG, { level, message, metadata, activityInfo: activityInfoFn() } )
13
13
  );
14
14
  };
@@ -10,7 +10,7 @@ const {
10
10
  }
11
11
  } ) );
12
12
 
13
- const messageBusMock = vi.hoisted( () => ( {
13
+ const mainEventBusMock = vi.hoisted( () => ( {
14
14
  emit: vi.fn()
15
15
  } ) );
16
16
  const activityInfoMock = vi.hoisted( () => vi.fn( () => ( {
@@ -19,7 +19,7 @@ const activityInfoMock = vi.hoisted( () => vi.fn( () => ( {
19
19
  } ) ) );
20
20
 
21
21
  vi.mock( '#consts', () => ( { ACTIVITY_LOGGER_SYMBOL, BusEventType } ) );
22
- vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
22
+ vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
23
23
  vi.mock( '@temporalio/activity', () => ( {
24
24
  activityInfo: activityInfoMock
25
25
  } ) );
@@ -42,7 +42,7 @@ describe( 'worker/global_functions', () => {
42
42
  } );
43
43
 
44
44
  expect( activityInfoMock ).toHaveBeenCalledTimes( 1 );
45
- expect( messageBusMock.emit ).toHaveBeenCalledWith( BusEventType.ACTIVITY_LOG, {
45
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( BusEventType.ACTIVITY_LOG, {
46
46
  level: 'debug',
47
47
  message: 'activity detail',
48
48
  metadata,
@@ -13,7 +13,7 @@ import { createChildLogger } from '#logger';
13
13
  import { setupInterruptionHandler } from './interruption.js';
14
14
  import { CatalogJob } from './catalog_workflow/catalog_job.js';
15
15
  import { bootstrapFetchProxy } from './proxy.js';
16
- import { messageBus } from '#bus';
16
+ import { mainEventBus } from '#bus';
17
17
  import { BusEventType } from '#consts';
18
18
  import { setupTelemetry } from './telemetry.js';
19
19
  import { TemporalConnectionMonitor } from './connection_monitor.js';
@@ -60,7 +60,7 @@ const execute = async () => {
60
60
  log.info( 'Loading activities...', { callerDir } );
61
61
  const { activities } = await loadActivities( callerDir, workflows );
62
62
 
63
- messageBus.emit( BusEventType.WORKER_BEFORE_START );
63
+ mainEventBus.emit( BusEventType.WORKER_BEFORE_START );
64
64
  bootstrapFetchProxy();
65
65
 
66
66
  log.info( 'Initializing tracing...' );
@@ -218,7 +218,7 @@ execute()
218
218
  .catch( error => {
219
219
  log.error( 'Fatal error', { error: error.message, stack: error.stack } );
220
220
 
221
- messageBus.emit( BusEventType.RUNTIME_ERROR, { error } );
221
+ mainEventBus.emit( BusEventType.RUNTIME_ERROR, { error } );
222
222
 
223
223
  const timeToFlushEvent = configs.processFailureShutdownDelay;
224
224
  log.info( `Exiting in ${timeToFlushEvent}ms` );
@@ -7,7 +7,7 @@ const {
7
7
  connectionMonitorInstance,
8
8
  createCatalogMock,
9
9
  initInterceptorsMock,
10
- messageBusMock,
10
+ mainEventBusMock,
11
11
  mockConnection,
12
12
  mockLog,
13
13
  mockWorker,
@@ -110,7 +110,7 @@ const {
110
110
  loadActivitiesMock: vi.fn().mockResolvedValue( {} ),
111
111
  loadHooksMock: vi.fn().mockResolvedValue( undefined ),
112
112
  loadWorkflowsMock: vi.fn().mockResolvedValue( [] ),
113
- messageBusMock: { emit: vi.fn(), on: vi.fn() },
113
+ mainEventBusMock: { emit: vi.fn(), on: vi.fn() },
114
114
  mockConnection: { close: vi.fn().mockResolvedValue( undefined ) },
115
115
  mockLog: { error: vi.fn(), info: vi.fn(), warn: vi.fn() },
116
116
  mockWorker,
@@ -128,7 +128,7 @@ vi.mock( '#consts', async importOriginal => {
128
128
  } );
129
129
  const initTracing = vi.fn().mockResolvedValue( undefined );
130
130
  vi.mock( '#tracing', () => ( { init: initTracing } ) );
131
- vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
131
+ vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
132
132
 
133
133
  const loadWorkflowsMock = vi.fn().mockResolvedValue( { workflows: [], entrypoint: '/fake/workflows/path.js' } );
134
134
  const loadActivitiesMock = vi.fn().mockResolvedValue( { activities: {} } );
@@ -357,7 +357,7 @@ describe( 'worker/index', () => {
357
357
  error: 'Big Failure'
358
358
  } ) );
359
359
  } );
360
- expect( messageBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
360
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
361
361
  await vi.waitFor( () => expect( exitMock ).toHaveBeenCalledWith( 1 ) );
362
362
  } );
363
363
 
@@ -378,7 +378,7 @@ describe( 'worker/index', () => {
378
378
  } );
379
379
  expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
380
380
  expect( catalogJobInstance.interrupt ).toHaveBeenCalledOnce();
381
- expect( messageBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
381
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
382
382
  } );
383
383
 
384
384
  it( 'throws catalog job errors after graceful shutdown', async () => {
@@ -398,7 +398,7 @@ describe( 'worker/index', () => {
398
398
  } );
399
399
  expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
400
400
  expect( connectionMonitorInstance.stop ).toHaveBeenCalledOnce();
401
- expect( messageBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
401
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
402
402
  } );
403
403
 
404
404
  it( 'cleans up partial startup failures after connecting', async () => {
@@ -4,7 +4,7 @@ import * as Tracing from '#tracing';
4
4
  import { headersToObject } from './headers.js';
5
5
  import { ACTIVITY_WRAPPER_VERSION_FIELD, BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
6
6
  import { activityHeartbeatEnabled, activityHeartbeatIntervalMs } from '../configs.js';
7
- import { messageBus } from '#bus';
7
+ import { mainEventBus } from '#bus';
8
8
  import { aggregateAttributes } from '#helpers/aggregations';
9
9
  import { buildApplicationFailureWithDetails } from '#helpers/errors';
10
10
 
@@ -68,7 +68,7 @@ export class ActivityExecutionInterceptor {
68
68
  addAttribute
69
69
  };
70
70
 
71
- messageBus.emit( BusEventType.ACTIVITY_START, { activityInfo, workflowDetails, outputActivityKind } );
71
+ mainEventBus.emit( BusEventType.ACTIVITY_START, { activityInfo, workflowDetails, outputActivityKind } );
72
72
  Tracing.addEventStart( { id: activityId, name: activityType, kind: outputActivityKind, parentId: runId, details: input.args[0], traceInfo } );
73
73
 
74
74
  try {
@@ -79,14 +79,14 @@ export class ActivityExecutionInterceptor {
79
79
 
80
80
  const aggregations = state.attributes.length > 0 ? aggregateAttributes( state.attributes ) : null;
81
81
 
82
- messageBus.emit( BusEventType.ACTIVITY_END, { activityInfo, aggregations, workflowDetails, outputActivityKind } );
82
+ mainEventBus.emit( BusEventType.ACTIVITY_END, { activityInfo, aggregations, workflowDetails, outputActivityKind } );
83
83
  Tracing.addEventEnd( { id: activityId, details: output, traceInfo } );
84
84
 
85
85
  return { [ACTIVITY_WRAPPER_VERSION_FIELD]: 1, output, aggregations };
86
86
 
87
87
  } catch ( error ) {
88
88
  const aggregations = state.attributes.length > 0 ? aggregateAttributes( state.attributes ) : null;
89
- messageBus.emit( BusEventType.ACTIVITY_ERROR, { activityInfo, aggregations, workflowDetails, outputActivityKind, error } );
89
+ mainEventBus.emit( BusEventType.ACTIVITY_ERROR, { activityInfo, aggregations, workflowDetails, outputActivityKind, error } );
90
90
  Tracing.addEventError( { id: activityId, details: error, traceInfo } );
91
91
 
92
92
  throw aggregations ? buildApplicationFailureWithDetails( error, { aggregations } ) : error;
@@ -61,8 +61,8 @@ vi.mock( './headers.js', () => ( {
61
61
  headersToObject: () => ( { traceInfo: traceInfoMock, workflowDetails: workflowDetailsMock } )
62
62
  } ) );
63
63
 
64
- const messageBusEmitMock = vi.fn();
65
- vi.mock( '#bus', () => ( { messageBus: { emit: messageBusEmitMock } } ) );
64
+ const mainEventBusEmitMock = vi.fn();
65
+ vi.mock( '#bus', () => ( { mainEventBus: { emit: mainEventBusEmitMock } } ) );
66
66
 
67
67
  vi.mock( '#consts', async importOriginal => {
68
68
  const actual = await importOriginal();
@@ -135,11 +135,11 @@ describe( 'ActivityExecutionInterceptor', () => {
135
135
  aggregations: null,
136
136
  [ACTIVITY_WRAPPER_VERSION_FIELD]: 1
137
137
  } );
138
- expect( messageBusEmitMock ).toHaveBeenCalledWith(
138
+ expect( mainEventBusEmitMock ).toHaveBeenCalledWith(
139
139
  BusEventType.ACTIVITY_START,
140
140
  { activityInfo: activityInfoMock, workflowDetails: workflowDetailsMock, outputActivityKind: 'step' }
141
141
  );
142
- expect( messageBusEmitMock ).toHaveBeenCalledWith(
142
+ expect( mainEventBusEmitMock ).toHaveBeenCalledWith(
143
143
  BusEventType.ACTIVITY_END,
144
144
  { activityInfo: activityInfoMock, aggregations: null, workflowDetails: workflowDetailsMock, outputActivityKind: 'step' }
145
145
  );
@@ -178,7 +178,7 @@ describe( 'ActivityExecutionInterceptor', () => {
178
178
  [ACTIVITY_WRAPPER_VERSION_FIELD]: 1
179
179
  } );
180
180
 
181
- expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, expect.any( Object ) );
181
+ expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, expect.any( Object ) );
182
182
  expect( addEventEndMock ).toHaveBeenCalledWith( { id: 'act-1', details: { result: 'sync' }, traceInfo: traceInfoMock } );
183
183
  expect( addEventErrorMock ).not.toHaveBeenCalled();
184
184
  } );
@@ -198,7 +198,7 @@ describe( 'ActivityExecutionInterceptor', () => {
198
198
  [ACTIVITY_WRAPPER_VERSION_FIELD]: 1
199
199
  } );
200
200
 
201
- expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, {
201
+ expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_END, {
202
202
  activityInfo: activityInfoMock,
203
203
  aggregations: httpRequestAggregations,
204
204
  workflowDetails: workflowDetailsMock,
@@ -226,7 +226,7 @@ describe( 'ActivityExecutionInterceptor', () => {
226
226
  } ],
227
227
  cause: error
228
228
  } );
229
- expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
229
+ expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
230
230
  activityInfo: activityInfoMock,
231
231
  aggregations: httpRequestAggregations,
232
232
  workflowDetails: workflowDetailsMock,
@@ -318,8 +318,8 @@ describe( 'ActivityExecutionInterceptor', () => {
318
318
  vi.advanceTimersByTime( 0 );
319
319
 
320
320
  await expect( promise ).rejects.toThrow( 'step failed' );
321
- expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_START, expect.any( Object ) );
322
- expect( messageBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
321
+ expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_START, expect.any( Object ) );
322
+ expect( mainEventBusEmitMock ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, {
323
323
  activityInfo: activityInfoMock,
324
324
  aggregations: null,
325
325
  workflowDetails: workflowDetailsMock,
@@ -7,7 +7,7 @@ import { METADATA_ACCESS_SYMBOL, WorkflowSpecialOutput } from '#consts';
7
7
  import { createWorkflowDetails } from '#helpers/temporal_context';
8
8
 
9
9
  // this is a dynamic generated file with activity configs overwrites
10
- import stepOptions from '../temp/__activity_options.js';
10
+ import activityOptionsMap from '../temp/__activity_options.js';
11
11
 
12
12
  /*
13
13
  This interceptor adds Memo and serialized workflowInfo() to the Activity invocation headers.
@@ -22,10 +22,10 @@ class HeadersInjectionInterceptor {
22
22
  ...memo,
23
23
  workflowDetails: createWorkflowDetails( info )
24
24
  } ) );
25
- // apply per-invocation options passed as second argument by rewritten calls
26
- const options = stepOptions[input.activityType];
27
- if ( options ) {
28
- input.options = deepMerge( memo.activityOptions, options );
25
+ // Apply component-level activity options on top of workflow options.
26
+ const activityOptionsOverrides = activityOptionsMap[input.activityType];
27
+ if ( activityOptionsOverrides ) {
28
+ input.options = deepMerge( input.options ?? {}, activityOptionsOverrides );
29
29
  }
30
30
  return next( input );
31
31
  }
@@ -56,12 +56,13 @@ vi.mock( './headers.js', () => ( { memoToHeaders: ( ...args ) => memoToHeadersMo
56
56
  const deepMergeMock = vi.fn( ( a, b ) => ( { ...( a || {} ), ...( b || {} ) } ) );
57
57
  vi.mock( '#helpers/object', () => ( { deepMerge: ( ...args ) => deepMergeMock( ...args ) } ) );
58
58
 
59
- const stepOptionsDefault = {};
60
- vi.mock( '../temp/__activity_options.js', () => ( { default: stepOptionsDefault } ) );
59
+ const activityOptionsDefault = {};
60
+ vi.mock( '../temp/__activity_options.js', () => ( { default: activityOptionsDefault } ) );
61
61
 
62
62
  describe( 'workflow interceptors', () => {
63
63
  beforeEach( () => {
64
64
  vi.clearAllMocks();
65
+ Object.keys( activityOptionsDefault ).forEach( key => delete activityOptionsDefault[key] );
65
66
  isCancellationMock.mockReturnValue( false );
66
67
  workflowInfoMock.mockReturnValue( workflowInfo );
67
68
  } );
@@ -91,11 +92,11 @@ describe( 'workflow interceptors', () => {
91
92
  expect( out ).toBe( 'result' );
92
93
  } );
93
94
 
94
- it( 'merges stepOptions with memo.activityOptions when stepOptions exist for activityType', async () => {
95
- stepOptionsDefault['MyWorkflow#step1'] = { scheduleToCloseTimeout: 60 };
95
+ it( 'merges component activity options over the scheduled activity input options', async () => {
96
+ activityOptionsDefault['MyWorkflow#step1'] = { scheduleToCloseTimeout: 60 };
96
97
  workflowInfoMock.mockReturnValue( {
97
98
  ...workflowInfo,
98
- memo: { traceInfo: workflowInfo.memo.traceInfo, activityOptions: { heartbeatTimeout: 10 } }
99
+ memo: { traceInfo: workflowInfo.memo.traceInfo }
99
100
  } );
100
101
  memoToHeadersMock.mockReturnValue( {} );
101
102
  deepMergeMock.mockReturnValue( { heartbeatTimeout: 10, scheduleToCloseTimeout: 60 } );
@@ -103,14 +104,13 @@ describe( 'workflow interceptors', () => {
103
104
  const { interceptors } = await import( './workflow.js' );
104
105
  const { outbound } = interceptors();
105
106
  const interceptor = outbound[0];
106
- const input = { headers: {}, activityType: 'MyWorkflow#step1' };
107
+ const input = { headers: {}, activityType: 'MyWorkflow#step1', options: { heartbeatTimeout: 10 } };
107
108
  const next = vi.fn().mockResolvedValue( undefined );
108
109
 
109
110
  await interceptor.scheduleActivity( input, next );
110
111
 
111
112
  expect( deepMergeMock ).toHaveBeenCalledWith( { heartbeatTimeout: 10 }, { scheduleToCloseTimeout: 60 } );
112
113
  expect( input.options ).toEqual( { heartbeatTimeout: 10, scheduleToCloseTimeout: 60 } );
113
- delete stepOptionsDefault['MyWorkflow#step1'];
114
114
  } );
115
115
  } );
116
116
 
@@ -2,74 +2,90 @@ import { dirname } from 'node:path';
2
2
  import { getTraceDestinations, sendHttpRequest } from '#internal_activities';
3
3
  import { findSharedActivitiesFromWorkflows, importComponents, matchFiles, writeFileInTempDir } from './tools.js';
4
4
  import { buildActivityMatcher, staticMatchers } from './matchers.js';
5
- import { ACTIVITY_SEND_HTTP_REQUEST, ACTIVITY_OPTIONS_FILENAME, SHARED_STEP_PREFIX, ACTIVITY_GET_TRACE_DESTINATIONS } from '#consts';
5
+ import { ACTIVITY_SEND_HTTP_REQUEST, ACTIVITY_OPTIONS_FILENAME, ACTIVITY_GET_TRACE_DESTINATIONS } from '#consts';
6
6
  import { createChildLogger } from '#logger';
7
7
  import { ValidationError } from '#errors';
8
8
 
9
9
  const log = createChildLogger( 'Activities Loader' );
10
10
 
11
11
  /**
12
- * Load activities:
13
- * - Scans activities based on workflows, using each workflow folder as a point to lookup for steps, evaluators files;
14
- * - Scans shared activities in the rootDir;
15
- * - Loads internal activities as well;
12
+ * Load activities
13
+ * - Scan local project and external workflow npm packages and look for shared activities.
14
+ * - For each workflow, register shared activities under the workflow namespace.
15
+ * - For each workflow, load activities declared relative to that workflow directory.
16
16
  *
17
- * Builds a map of activities, where they is generated according to the type of activity and the value is the function itself and return it.
18
- * - Shared activity keys have a common prefix followed by the activity name;
19
- * - Internal activities are registered with a fixed key;
20
- * - Workflow activities keys are composed using the workflow name and the activity name;
17
+ * Builds a map of activities, key is workflowType#activityType, value is the component wrapper function.
21
18
  *
22
19
  * @param {string} rootDir
23
20
  * @param {import('./workflows.js').Workflow[]} workflows
24
21
  * @returns {object}
25
22
  */
26
23
  export async function loadActivities( rootDir, workflows ) {
27
- const activities = {};
28
- const activityOptionsMap = {};
24
+ const activities = new Map();
25
+ const activityOptions = new Map();
29
26
 
30
- // Load workflow-based activities
31
- for ( const { path: workflowPath, name: workflowName, external } of workflows ) {
32
- const dir = dirname( workflowPath );
33
- for await ( const { fn, metadata, path } of importComponents( matchFiles( dir, [ buildActivityMatcher( dir ) ] ) ) ) {
34
- // Activities loaded from a workflow path will use the workflow name as a namespace, which is unique across the platform, avoiding collision
35
- const activityKey = `${workflowName}#${metadata.name}`;
36
-
37
- log.info( metadata.name, { workflow: workflowName, type: metadata.type, ...( external && { external } ), path } );
38
-
39
- if ( activities[activityKey] ) {
40
- throw new ValidationError( `Activity "${metadata.name}" in workflow "${workflowName}" conflicts with another \
41
- activity in the same workflow. Activity names must be unique within a workflow.` );
42
- }
43
- activities[activityKey] = fn;
44
- // propagate the custom options set on the step()/evaluator() constructor
45
- activityOptionsMap[activityKey] = metadata.options?.activityOptions ?? undefined;
46
- }
47
- }
27
+ const sharedActivities = new Map();
28
+ const sharedActivitiesOptions = new Map();
48
29
 
49
30
  // Load shared activities/evaluators from local and external npm modules
50
31
  const localSharedActivities = matchFiles( rootDir, [ staticMatchers.sharedStepsDir, staticMatchers.sharedEvaluatorsDir ] );
51
32
  const externalSharedActivities = findSharedActivitiesFromWorkflows( workflows.filter( w => w.external ) );
52
33
  for await ( const { fn, metadata, path } of importComponents( [ ...localSharedActivities, ...externalSharedActivities ] ) ) {
53
34
  const external = externalSharedActivities.some( a => a.path === path );
54
- // Uses a global namespace for shared activities
55
- const activityKey = `${SHARED_STEP_PREFIX}#${metadata.name}`;
56
35
 
36
+ if ( sharedActivities.has( metadata.name ) ) {
37
+ throw new ValidationError( `Shared activity "${metadata.name}" conflicts with another shared activity.` +
38
+ ' Shared activity names must be unique.' );
39
+ }
57
40
  log.info( metadata.name, { shared: true, type: metadata.type, ...( external && { external } ), path } );
58
41
 
59
- if ( activities[activityKey] ) {
60
- throw new ValidationError( `Shared activity "${metadata.name}" conflicts with another shared activity. \
61
- Shared activity names must be unique.` );
42
+ sharedActivities.set( metadata.name, fn );
43
+ if ( metadata.options?.activityOptions ) {
44
+ sharedActivitiesOptions.set( metadata.name, metadata.options.activityOptions );
45
+ }
46
+ }
47
+
48
+ // Discover and load workflow activities
49
+ for ( const { path: workflowPath, name: workflowName, external } of workflows ) {
50
+ const dir = dirname( workflowPath );
51
+
52
+ // Add shared activities to this workflow namespace
53
+ for ( const [ name, fn ] of sharedActivities ) {
54
+ const id = `${workflowName}#${name}`;
55
+ activities.set( id, fn );
56
+ if ( sharedActivitiesOptions.has( name ) ) {
57
+ activityOptions.set( id, sharedActivitiesOptions.get( name ) );
58
+ }
59
+ }
60
+
61
+ for await ( const { fn, metadata, path } of importComponents( matchFiles( dir, [ buildActivityMatcher( dir ) ] ) ) ) {
62
+ // Activities loaded from a workflow path will use the workflow name as a namespace, which is unique across the platform, avoiding collision
63
+ const id = `${workflowName}#${metadata.name}`;
64
+
65
+ if ( sharedActivities.has( metadata.name ) ) {
66
+ throw new ValidationError( `Activity "${metadata.name}" in workflow "${workflowName}" conflicts with a shared activity.` +
67
+ ' Workflow activity names must not overlap with shared activity names.' );
68
+ }
69
+
70
+ if ( activities.has( id ) ) {
71
+ throw new ValidationError( `Activity "${metadata.name}" in workflow "${workflowName}" conflicts with another activity in the same workflow.` +
72
+ ' Activity names must be unique within a workflow.' );
73
+ }
74
+
75
+ log.info( metadata.name, { workflow: workflowName, type: metadata.type, ...( external && { external } ), path } );
76
+ activities.set( id, fn );
77
+ if ( metadata.options?.activityOptions ) {
78
+ activityOptions.set( id, metadata.options.activityOptions );
79
+ }
62
80
  }
63
- activities[activityKey] = fn;
64
- activityOptionsMap[activityKey] = metadata.options?.activityOptions ?? undefined;
65
81
  }
66
82
 
67
83
  // writes down the activity option overrides
68
- const optionsContent = `export default ${JSON.stringify( activityOptionsMap, undefined, 2 )};`;
84
+ const optionsContent = `export default ${JSON.stringify( Object.fromEntries( activityOptions ), undefined, 2 )};`;
69
85
  const optionsFile = writeFileInTempDir( optionsContent, ACTIVITY_OPTIONS_FILENAME );
70
86
 
71
87
  // system activities
72
- activities[ACTIVITY_SEND_HTTP_REQUEST] = sendHttpRequest;
73
- activities[ACTIVITY_GET_TRACE_DESTINATIONS] = getTraceDestinations;
74
- return { activities, optionsFile };
88
+ activities.set( ACTIVITY_SEND_HTTP_REQUEST, sendHttpRequest );
89
+ activities.set( ACTIVITY_GET_TRACE_DESTINATIONS, getTraceDestinations );
90
+ return { activities: Object.fromEntries( activities ), optionsFile };
75
91
  };