@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,18 +1,5 @@
1
- import { messageBus } from '#bus';
2
- import { Storage } from '#async_storage';
1
+ import { stepEventBus } from '#bus';
3
2
 
4
3
  export const Event = {
5
- emit: ( eventName, payload ) => {
6
- const ctx = Storage.load();
7
-
8
- messageBus.emit( `external:${eventName}`, {
9
- ...payload ?? {},
10
- ...( ctx && {
11
- activityInfo: ctx.activityInfo,
12
- workflowDetails: ctx.workflowDetails,
13
- outputActivityKind: ctx.outputActivityKind
14
- } )
15
- } );
16
- }
4
+ emit: ( eventName, payload ) => stepEventBus.emit( `sdk:${eventName}`, payload )
17
5
  };
18
-
@@ -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
  } );
@@ -36,8 +36,6 @@ const envVarSchema = z.object( {
36
36
  OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 2 * 60 * 1000 ) ), // 2min
37
37
  // Whether to send activity heartbeats (enabled by default)
38
38
  OUTPUT_ACTIVITY_HEARTBEAT_ENABLED: z.transform( v => v === undefined ? true : isStringboolTrue( v ) ),
39
- // Time to allow for hooks to flush before shutdown
40
- OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY: z.preprocess( coalesceEmptyString, z.coerce.number().int().positive().default( 3000 ) ),
41
39
  // Set temporal worker shutdown force time
42
40
  TEMPORAL_SHUTDOWN_FORCE_TIME: durationSchema,
43
41
  // Set temporal worker shutdown grace time
@@ -70,7 +68,6 @@ export const catalogId = envVars.OUTPUT_CATALOG_ID;
70
68
  export const workerTelemetryIntervalMs = envVars.OUTPUT_WORKER_TELEMETRY_INTERVAL_MS;
71
69
  export const activityHeartbeatIntervalMs = envVars.OUTPUT_ACTIVITY_HEARTBEAT_INTERVAL_MS;
72
70
  export const activityHeartbeatEnabled = envVars.OUTPUT_ACTIVITY_HEARTBEAT_ENABLED;
73
- export const processFailureShutdownDelay = envVars.OUTPUT_PROCESS_FAILURE_SHUTDOWN_DELAY;
74
71
  export const shutdownForceTime = envVars.TEMPORAL_SHUTDOWN_FORCE_TIME;
75
72
  export const shutdownGraceTime = envVars.TEMPORAL_SHUTDOWN_GRACE_TIME;
76
73
  export const grpcProxy = envVars.TEMPORAL_GRPC_PROXY;
@@ -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,
@@ -12,8 +12,8 @@ import { initInterceptors } from './interceptors/index.js';
12
12
  import { createChildLogger } from '#logger';
13
13
  import { setupInterruptionHandler } from './interruption.js';
14
14
  import { CatalogJob } from './catalog_workflow/catalog_job.js';
15
- import { bootstrapFetchProxy } from './proxy.js';
16
- import { messageBus } from '#bus';
15
+ import { mainEventBus } from '#bus';
16
+ import { flushPendingHooks } from '#hooks/pending_hooks';
17
17
  import { BusEventType } from '#consts';
18
18
  import { setupTelemetry } from './telemetry.js';
19
19
  import { TemporalConnectionMonitor } from './connection_monitor.js';
@@ -60,8 +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 );
64
- bootstrapFetchProxy();
63
+ mainEventBus.emit( BusEventType.WORKER_BEFORE_START );
65
64
 
66
65
  log.info( 'Initializing tracing...' );
67
66
  await initTracing();
@@ -214,13 +213,18 @@ execute()
214
213
  .catch( e => log.warn( 'Connection close error', { error: e.message } ) );
215
214
  }
216
215
  } )
217
- .then( () => log.info( 'Bye' ) )
218
- .catch( error => {
216
+ .then( async () => {
217
+ log.info( 'Flushing hook callbacks...' );
218
+ await flushPendingHooks();
219
+ log.info( 'Bye' );
220
+ } )
221
+ .catch( async error => {
219
222
  log.error( 'Fatal error', { error: error.message, stack: error.stack } );
220
223
 
221
- messageBus.emit( BusEventType.RUNTIME_ERROR, { error } );
224
+ mainEventBus.emit( BusEventType.RUNTIME_ERROR, { error } );
222
225
 
223
- const timeToFlushEvent = configs.processFailureShutdownDelay;
224
- log.info( `Exiting in ${timeToFlushEvent}ms` );
225
- setTimeout( () => process.exit( 1 ), timeToFlushEvent );
226
+ log.info( 'Flushing hook callbacks...' );
227
+ await flushPendingHooks();
228
+ log.info( 'Exiting...' );
229
+ setTimeout( () => process.exit( 1 ) );
226
230
  } );
@@ -6,8 +6,9 @@ const {
6
6
  configValues,
7
7
  connectionMonitorInstance,
8
8
  createCatalogMock,
9
+ flushPendingHooksMock,
9
10
  initInterceptorsMock,
10
- messageBusMock,
11
+ mainEventBusMock,
11
12
  mockConnection,
12
13
  mockLog,
13
14
  mockWorker,
@@ -46,7 +47,6 @@ const {
46
47
  maxConcurrentActivityTaskPolls: 5,
47
48
  maxConcurrentWorkflowTaskPolls: 5,
48
49
  workerTuner: undefined,
49
- processFailureShutdownDelay: 0,
50
50
  shutdownForceTime: undefined,
51
51
  shutdownGraceTime: undefined
52
52
  };
@@ -105,12 +105,13 @@ const {
105
105
  connectionMonitorInstance,
106
106
  createCatalogMock: vi.fn().mockReturnValue( { workflows: [], activities: {} } ),
107
107
  createWorkflowsEntryPointMock: vi.fn().mockReturnValue( '/fake/workflows/path.js' ),
108
+ flushPendingHooksMock: vi.fn().mockResolvedValue( undefined ),
108
109
  hashSourceCodeMock: vi.fn().mockResolvedValue( 'catalog-hash' ),
109
110
  initInterceptorsMock: vi.fn().mockReturnValue( [] ),
110
111
  loadActivitiesMock: vi.fn().mockResolvedValue( {} ),
111
112
  loadHooksMock: vi.fn().mockResolvedValue( undefined ),
112
113
  loadWorkflowsMock: vi.fn().mockResolvedValue( [] ),
113
- messageBusMock: { emit: vi.fn(), on: vi.fn() },
114
+ mainEventBusMock: { emit: vi.fn(), on: vi.fn() },
114
115
  mockConnection: { close: vi.fn().mockResolvedValue( undefined ) },
115
116
  mockLog: { error: vi.fn(), info: vi.fn(), warn: vi.fn() },
116
117
  mockWorker,
@@ -128,7 +129,8 @@ vi.mock( '#consts', async importOriginal => {
128
129
  } );
129
130
  const initTracing = vi.fn().mockResolvedValue( undefined );
130
131
  vi.mock( '#tracing', () => ( { init: initTracing } ) );
131
- vi.mock( '#bus', () => ( { messageBus: messageBusMock } ) );
132
+ vi.mock( '#bus', () => ( { mainEventBus: mainEventBusMock } ) );
133
+ vi.mock( '#hooks/pending_hooks', () => ( { flushPendingHooks: flushPendingHooksMock } ) );
132
134
 
133
135
  const loadWorkflowsMock = vi.fn().mockResolvedValue( { workflows: [], entrypoint: '/fake/workflows/path.js' } );
134
136
  const loadActivitiesMock = vi.fn().mockResolvedValue( { activities: {} } );
@@ -145,7 +147,6 @@ vi.mock( './sinks.js', () => ( { sinks: {} } ) );
145
147
  vi.mock( './catalog_workflow/index.js', () => ( { createCatalog: createCatalogMock } ) );
146
148
  vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
147
149
  vi.mock( './interceptors/index.js', () => ( { initInterceptors: initInterceptorsMock } ) );
148
- vi.mock( './proxy.js', () => ( { bootstrapFetchProxy: vi.fn() } ) );
149
150
  vi.mock( './telemetry.js', () => ( { setupTelemetry: setupTelemetryMock } ) );
150
151
  vi.mock( './interruption.js', () => ( { setupInterruptionHandler: setupInterruptionHandlerMock } ) );
151
152
  vi.mock( './global_functions.js', () => ( { bindGlobalFunctions: bindGlobalFunctionsMock } ) );
@@ -262,6 +263,7 @@ describe( 'worker/index', () => {
262
263
  expect( catalogJobInstance.run ).toHaveBeenCalled();
263
264
 
264
265
  await settleWorker();
266
+ await vi.waitFor( () => expect( flushPendingHooksMock ).toHaveBeenCalledOnce() );
265
267
  expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
266
268
  } );
267
269
 
@@ -357,8 +359,11 @@ describe( 'worker/index', () => {
357
359
  error: 'Big Failure'
358
360
  } ) );
359
361
  } );
360
- expect( messageBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
362
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
361
363
  await vi.waitFor( () => expect( exitMock ).toHaveBeenCalledWith( 1 ) );
364
+ expect( flushPendingHooksMock ).toHaveBeenCalledOnce();
365
+ expect( mainEventBusMock.emit.mock.invocationCallOrder[0] ).toBeLessThan( flushPendingHooksMock.mock.invocationCallOrder[0] );
366
+ expect( flushPendingHooksMock.mock.invocationCallOrder[0] ).toBeLessThan( exitMock.mock.invocationCallOrder[0] );
362
367
  } );
363
368
 
364
369
  it( 'throws connection monitor errors after graceful shutdown', async () => {
@@ -378,7 +383,7 @@ describe( 'worker/index', () => {
378
383
  } );
379
384
  expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
380
385
  expect( catalogJobInstance.interrupt ).toHaveBeenCalledOnce();
381
- expect( messageBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
386
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
382
387
  } );
383
388
 
384
389
  it( 'throws catalog job errors after graceful shutdown', async () => {
@@ -398,7 +403,7 @@ describe( 'worker/index', () => {
398
403
  } );
399
404
  expect( mockWorker.shutdown ).toHaveBeenCalledOnce();
400
405
  expect( connectionMonitorInstance.stop ).toHaveBeenCalledOnce();
401
- expect( messageBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
406
+ expect( mainEventBusMock.emit ).toHaveBeenCalledWith( expect.any( String ), { error } );
402
407
  } );
403
408
 
404
409
  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
  };