@outputai/core 0.10.1-next.b7b2fbe.0 → 0.10.1-next.bf95a6f.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 (99) hide show
  1. package/package.json +13 -7
  2. package/src/bus.js +58 -20
  3. package/src/bus.spec.js +135 -77
  4. package/src/consts.js +7 -3
  5. package/src/errors.d.ts +12 -2
  6. package/src/errors.js +17 -3
  7. package/src/errors.spec.js +19 -0
  8. package/src/helpers/activity_options.js +21 -0
  9. package/src/helpers/activity_options.spec.js +55 -0
  10. package/src/helpers/error_serializer.js +236 -0
  11. package/src/helpers/error_serializer.spec.js +413 -0
  12. package/src/helpers/errors.js +23 -14
  13. package/src/helpers/errors.spec.js +31 -75
  14. package/src/helpers/fetch.js +1 -38
  15. package/src/helpers/fetch.spec.js +1 -39
  16. package/src/helpers/function.js +13 -0
  17. package/src/helpers/function.spec.js +23 -1
  18. package/src/helpers/object.js +48 -35
  19. package/src/helpers/object.spec.js +123 -75
  20. package/src/helpers/redact.js +51 -0
  21. package/src/helpers/redact.spec.js +66 -0
  22. package/src/helpers/string.js +10 -0
  23. package/src/helpers/string.spec.js +36 -1
  24. package/src/hooks/index.d.ts +76 -78
  25. package/src/hooks/index.js +57 -49
  26. package/src/hooks/index.spec.js +104 -105
  27. package/src/hooks/pending_hooks.js +24 -0
  28. package/src/hooks/pending_hooks.spec.js +62 -0
  29. package/src/interface/evaluator.d.ts +10 -9
  30. package/src/interface/evaluator.js +1 -6
  31. package/src/interface/evaluator.spec.js +10 -10
  32. package/src/interface/index.js +2 -1
  33. package/src/interface/step.d.ts +16 -15
  34. package/src/interface/step.js +1 -6
  35. package/src/interface/step.spec.js +10 -10
  36. package/src/interface/validations/index.js +33 -53
  37. package/src/interface/validations/index.spec.js +25 -17
  38. package/src/interface/webhook.js +1 -2
  39. package/src/interface/webhook.spec.js +25 -19
  40. package/src/interface/workflow.d.ts +25 -18
  41. package/src/interface/workflow.js +64 -85
  42. package/src/interface/workflow.spec.js +220 -226
  43. package/src/interface/workflow_activity_options.js +2 -5
  44. package/src/interface/workflow_utils.d.ts +15 -0
  45. package/src/interface/workflow_utils.js +19 -0
  46. package/src/interface/workflow_utils.spec.js +82 -1
  47. package/src/sdk/helpers/objects.d.ts +24 -19
  48. package/src/sdk/runtime/events.d.ts +4 -6
  49. package/src/sdk/runtime/events.js +2 -15
  50. package/src/sdk/runtime/events.spec.js +14 -92
  51. package/src/temporal/client.js +47 -0
  52. package/src/temporal/client.spec.js +124 -0
  53. package/src/temporal/index.d.ts +32 -0
  54. package/src/temporal/index.js +1 -0
  55. package/src/tracing/trace_engine.js +9 -11
  56. package/src/tracing/trace_engine.spec.js +67 -28
  57. package/src/worker/catalog_workflow/catalog_job.js +3 -3
  58. package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
  59. package/src/worker/configs.js +0 -3
  60. package/src/worker/global_functions.js +2 -2
  61. package/src/worker/global_functions.spec.js +3 -3
  62. package/src/worker/index.js +24 -11
  63. package/src/worker/index.spec.js +36 -16
  64. package/src/worker/interceptors/activity.js +34 -21
  65. package/src/worker/interceptors/activity.spec.js +77 -172
  66. package/src/worker/interceptors/workflow.js +58 -16
  67. package/src/worker/interceptors/workflow.spec.js +183 -56
  68. package/src/worker/loader/activities.js +56 -40
  69. package/src/worker/loader/activities.spec.js +21 -22
  70. package/src/worker/loader/workflows.js +16 -3
  71. package/src/worker/loader/workflows.spec.js +36 -5
  72. package/src/worker/log_hooks.js +22 -19
  73. package/src/worker/log_hooks.spec.js +14 -6
  74. package/src/worker/sinks.js +8 -8
  75. package/src/worker/sinks.spec.js +9 -9
  76. package/src/worker/telemetry.js +2 -1
  77. package/src/worker/temporal_logger.js +33 -0
  78. package/src/worker/temporal_logger.spec.js +90 -0
  79. package/src/worker/webpack_loaders/consts.js +6 -0
  80. package/src/worker/webpack_loaders/tools.js +1 -146
  81. package/src/worker/webpack_loaders/tools.spec.js +0 -23
  82. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
  83. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
  84. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
  85. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
  86. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
  87. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
  88. package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
  89. package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
  90. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
  91. package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
  92. package/src/helpers/aggregations.js +0 -44
  93. package/src/helpers/aggregations.spec.js +0 -92
  94. package/src/tracing/tools/utils.js +0 -42
  95. package/src/tracing/tools/utils.spec.js +0 -83
  96. package/src/worker/proxy.js +0 -20
  97. package/src/worker/proxy.spec.js +0 -51
  98. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
  99. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
@@ -1,12 +1,14 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { ApplicationFailure } from '@temporalio/common';
3
- import { METADATA_ACCESS_SYMBOL } from '#consts';
3
+ import { FatalError, TransparentFatalError, ValidationError } from '#errors';
4
4
 
5
5
  const workflowInfoMock = vi.fn();
6
6
  const workflowStartMock = vi.fn();
7
7
  const workflowEndMock = vi.fn();
8
8
  const workflowErrorMock = vi.fn();
9
9
  const isCancellationMock = vi.fn();
10
+ const upsertMemoMock = vi.fn();
11
+ const traceInfoBuildMock = vi.fn();
10
12
  const startTime = new Date( '2026-06-02T09:00:00.000Z' );
11
13
  const runStartTime = new Date( '2026-06-02T09:05:00.000Z' );
12
14
  const workflowDetails = {
@@ -36,19 +38,26 @@ const workflowInfo = {
36
38
  memo: { traceInfo: { runId: 'root-run' } }
37
39
  };
38
40
 
39
- vi.mock( '@temporalio/workflow', () => ( {
40
- workflowInfo: ( ...args ) => workflowInfoMock( ...args ),
41
- proxySinks: () => ( {
42
- workflow: { start: workflowStartMock, end: workflowEndMock, error: workflowErrorMock }
43
- } ),
44
- ContinueAsNew: class ContinueAsNew extends Error {
45
- constructor() {
46
- super( 'ContinueAsNew' );
47
- this.name = 'ContinueAsNew';
48
- }
49
- },
50
- isCancellation: ( ...args ) => isCancellationMock( ...args )
51
- } ) );
41
+ vi.mock( '@temporalio/workflow', async importOriginal => {
42
+ const actual = await importOriginal();
43
+ return {
44
+ ...actual,
45
+ workflowInfo: ( ...args ) => workflowInfoMock( ...args ),
46
+ proxySinks: () => ( {
47
+ workflow: { start: workflowStartMock, end: workflowEndMock, error: workflowErrorMock }
48
+ } ),
49
+ ContinueAsNew: class ContinueAsNew extends Error {
50
+ constructor() {
51
+ super( 'ContinueAsNew' );
52
+ this.name = 'ContinueAsNew';
53
+ }
54
+ },
55
+ isCancellation: ( ...args ) => isCancellationMock( ...args ),
56
+ upsertMemo: ( ...args ) => upsertMemoMock( ...args )
57
+ };
58
+ } );
59
+
60
+ vi.mock( '#helpers/trace_info', () => ( { TraceInfo: { build: ( ...args ) => traceInfoBuildMock( ...args ) } } ) );
52
61
 
53
62
  const memoToHeadersMock = vi.fn( memo => ( memo ? { ...memo, __asHeaders: true } : {} ) );
54
63
  vi.mock( './headers.js', () => ( { memoToHeaders: ( ...args ) => memoToHeadersMock( ...args ) } ) );
@@ -56,13 +65,20 @@ vi.mock( './headers.js', () => ( { memoToHeaders: ( ...args ) => memoToHeadersMo
56
65
  const deepMergeMock = vi.fn( ( a, b ) => ( { ...( a || {} ), ...( b || {} ) } ) );
57
66
  vi.mock( '#helpers/object', () => ( { deepMerge: ( ...args ) => deepMergeMock( ...args ) } ) );
58
67
 
59
- const stepOptionsDefault = {};
60
- vi.mock( '../temp/__activity_options.js', () => ( { default: stepOptionsDefault } ) );
68
+ const activityOptionsDefault = {};
69
+ vi.mock( '../temp/__activity_options.js', () => ( { default: activityOptionsDefault } ) );
70
+
71
+ const workflowOptionsDefault = {};
72
+ vi.mock( '../temp/__workflow_options.js', () => ( { default: workflowOptionsDefault } ) );
61
73
 
62
74
  describe( 'workflow interceptors', () => {
63
75
  beforeEach( () => {
64
76
  vi.clearAllMocks();
77
+ Object.keys( activityOptionsDefault ).forEach( key => delete activityOptionsDefault[key] );
78
+ Object.keys( workflowOptionsDefault ).forEach( key => delete workflowOptionsDefault[key] );
79
+ workflowOptionsDefault.MyWorkflow = { disableTrace: false };
65
80
  isCancellationMock.mockReturnValue( false );
81
+ traceInfoBuildMock.mockReturnValue( { workflowId: 'workflow-1', runId: 'run-1' } );
66
82
  workflowInfoMock.mockReturnValue( workflowInfo );
67
83
  } );
68
84
 
@@ -71,7 +87,7 @@ describe( 'workflow interceptors', () => {
71
87
  const { interceptors } = await import( './workflow.js' );
72
88
  const { outbound } = interceptors();
73
89
  const interceptor = outbound[0];
74
- const input = { headers: { existing: 'header' }, activityType: 'MyWorkflow#step1' };
90
+ const input = { headers: { existing: 'header' }, activityType: 'MyWorkflow#step1', options: {} };
75
91
  const next = vi.fn().mockResolvedValue( 'result' );
76
92
 
77
93
  memoToHeadersMock.mockReturnValue( { traceInfo: workflowInfo.memo.traceInfo, workflowDetails } );
@@ -91,30 +107,101 @@ describe( 'workflow interceptors', () => {
91
107
  expect( out ).toBe( 'result' );
92
108
  } );
93
109
 
94
- it( 'merges stepOptions with memo.activityOptions when stepOptions exist for activityType', async () => {
95
- stepOptionsDefault['MyWorkflow#step1'] = { scheduleToCloseTimeout: 60 };
110
+ it( 'merges component activity options over the scheduled activity input options', async () => {
111
+ activityOptionsDefault['MyWorkflow#step1'] = { scheduleToCloseTimeout: 60 };
96
112
  workflowInfoMock.mockReturnValue( {
97
113
  ...workflowInfo,
98
- memo: { traceInfo: workflowInfo.memo.traceInfo, activityOptions: { heartbeatTimeout: 10 } }
114
+ memo: { traceInfo: workflowInfo.memo.traceInfo }
99
115
  } );
100
116
  memoToHeadersMock.mockReturnValue( {} );
101
- deepMergeMock.mockReturnValue( { heartbeatTimeout: 10, scheduleToCloseTimeout: 60 } );
117
+ deepMergeMock.mockReturnValue( {
118
+ heartbeatTimeout: 10,
119
+ scheduleToCloseTimeout: 60,
120
+ retry: { maximumAttempts: 2, nonRetryableErrorTypes: [ 'StepError' ] }
121
+ } );
102
122
 
103
123
  const { interceptors } = await import( './workflow.js' );
104
124
  const { outbound } = interceptors();
105
125
  const interceptor = outbound[0];
106
- const input = { headers: {}, activityType: 'MyWorkflow#step1' };
126
+ const input = { headers: {}, activityType: 'MyWorkflow#step1', options: { heartbeatTimeout: 10 } };
107
127
  const next = vi.fn().mockResolvedValue( undefined );
108
128
 
109
129
  await interceptor.scheduleActivity( input, next );
110
130
 
111
131
  expect( deepMergeMock ).toHaveBeenCalledWith( { heartbeatTimeout: 10 }, { scheduleToCloseTimeout: 60 } );
112
- expect( input.options ).toEqual( { heartbeatTimeout: 10, scheduleToCloseTimeout: 60 } );
113
- delete stepOptionsDefault['MyWorkflow#step1'];
132
+ expect( input.options ).toEqual( {
133
+ heartbeatTimeout: 10,
134
+ scheduleToCloseTimeout: 60,
135
+ retry: {
136
+ maximumAttempts: 2,
137
+ nonRetryableErrorTypes: [ 'StepError', 'FatalError' ]
138
+ }
139
+ } );
140
+ } );
141
+
142
+ it( 'adds FatalError to activity retry options without duplicates', async () => {
143
+ const { interceptors } = await import( './workflow.js' );
144
+ const { outbound } = interceptors();
145
+ const interceptor = outbound[0];
146
+ const input = {
147
+ headers: {},
148
+ activityType: 'MyWorkflow#step1',
149
+ options: {
150
+ retry: {
151
+ maximumAttempts: 4,
152
+ nonRetryableErrorTypes: [ 'DomainError', 'FatalError' ]
153
+ }
154
+ }
155
+ };
156
+ const next = vi.fn().mockResolvedValue( undefined );
157
+
158
+ await interceptor.scheduleActivity( input, next );
159
+
160
+ expect( input.options.retry ).toEqual( {
161
+ maximumAttempts: 4,
162
+ nonRetryableErrorTypes: [ 'DomainError', 'FatalError' ]
163
+ } );
114
164
  } );
115
165
  } );
116
166
 
117
167
  describe( 'WorkflowExecutionInterceptor', () => {
168
+ it( 'upserts root trace info before starting the workflow trace', async () => {
169
+ const { interceptors } = await import( './workflow.js' );
170
+ const { inbound } = interceptors();
171
+ const traceInfo = { workflowId: 'workflow-1', runId: 'run-1' };
172
+ traceInfoBuildMock.mockReturnValue( traceInfo );
173
+
174
+ await inbound[0].execute( { args: [ {} ] }, vi.fn().mockResolvedValue( undefined ) );
175
+
176
+ expect( traceInfoBuildMock ).toHaveBeenCalledOnce();
177
+ expect( upsertMemoMock ).toHaveBeenCalledWith( { payloadVersion: '2', traceInfo } );
178
+ expect( upsertMemoMock.mock.invocationCallOrder[0] ).toBeLessThan( workflowStartMock.mock.invocationCallOrder[0] );
179
+ } );
180
+
181
+ it.each( [
182
+ {
183
+ name: 'tracing is disabled',
184
+ info: workflowInfo,
185
+ options: { disableTrace: true }
186
+ },
187
+ {
188
+ name: 'the workflow has a root',
189
+ info: { ...workflowInfo, root: { workflowId: 'root-workflow', runId: 'root-run' } },
190
+ options: { disableTrace: false }
191
+ }
192
+ ] )( 'does not build trace info when $name', async ( { info, options } ) => {
193
+ workflowInfoMock.mockReturnValue( info );
194
+ workflowOptionsDefault.MyWorkflow = options;
195
+ const { interceptors } = await import( './workflow.js' );
196
+ const { inbound } = interceptors();
197
+
198
+ await inbound[0].execute( { args: [ {} ] }, vi.fn().mockResolvedValue( undefined ) );
199
+
200
+ expect( traceInfoBuildMock ).not.toHaveBeenCalled();
201
+ expect( upsertMemoMock ).toHaveBeenCalledOnce();
202
+ expect( upsertMemoMock ).toHaveBeenCalledWith( { payloadVersion: '2' } );
203
+ } );
204
+
118
205
  it( 'calls sinks.workflow.start, next, then sinks.workflow.end on success', async () => {
119
206
  const { interceptors } = await import( './workflow.js' );
120
207
  const { inbound } = interceptors();
@@ -131,7 +218,7 @@ describe( 'workflow interceptors', () => {
131
218
  expect( workflowErrorMock ).not.toHaveBeenCalled();
132
219
  } );
133
220
 
134
- it( 'calls sinks.workflow.error and rethrows workflow errors', async () => {
221
+ it( 'rethrows native errors as Workflow Task failures without a terminal sink event', async () => {
135
222
  const { interceptors } = await import( './workflow.js' );
136
223
  const { inbound } = interceptors();
137
224
  const interceptor = inbound[0];
@@ -141,17 +228,19 @@ describe( 'workflow interceptors', () => {
141
228
 
142
229
  await expect( interceptor.execute( input, next ) ).rejects.toBe( err );
143
230
  expect( workflowStartMock ).toHaveBeenCalled();
144
- expect( workflowErrorMock ).toHaveBeenCalledWith( err );
231
+ expect( workflowErrorMock ).not.toHaveBeenCalled();
145
232
  expect( workflowEndMock ).not.toHaveBeenCalled();
146
233
  } );
147
234
 
148
- it( 'wraps workflow errors with metadata in ApplicationFailure details', async () => {
235
+ it.each( [
236
+ { ErrorType: FatalError, type: 'FatalError' },
237
+ { ErrorType: ValidationError, type: 'ValidationError' }
238
+ ] )( 'converts $type into a non-retryable ApplicationFailure with serialized details', async ( { ErrorType, type } ) => {
149
239
  const { interceptors } = await import( './workflow.js' );
150
240
  const { inbound } = interceptors();
151
241
  const interceptor = inbound[0];
152
- const trace = { trace: { destinations: { local: '/tmp/trace' } } };
153
- const err = new Error( 'workflow failed' );
154
- err[METADATA_ACCESS_SYMBOL] = trace;
242
+ const err = new ErrorType( 'workflow failed' );
243
+ err.code = 'EWORKFLOW';
155
244
  const next = vi.fn().mockRejectedValue( err );
156
245
 
157
246
  const thrown = await interceptor.execute( { args: [ {} ] }, next ).catch( e => e );
@@ -159,44 +248,78 @@ describe( 'workflow interceptors', () => {
159
248
  expect( thrown ).toBeInstanceOf( ApplicationFailure );
160
249
  expect( thrown ).toMatchObject( {
161
250
  message: 'workflow failed',
162
- type: 'Error',
163
- details: [ trace ],
251
+ type,
252
+ nonRetryable: true,
253
+ details: [ {
254
+ error: {
255
+ name: type,
256
+ message: 'workflow failed',
257
+ code: 'EWORKFLOW'
258
+ }
259
+ } ],
164
260
  cause: err
165
261
  } );
166
- expect( workflowErrorMock ).toHaveBeenCalledWith( err );
262
+ expect( workflowErrorMock ).toHaveBeenCalledWith( expect.objectContaining( {
263
+ name: type,
264
+ message: 'workflow failed',
265
+ code: 'EWORKFLOW',
266
+ stack: expect.any( String )
267
+ } ) );
268
+ expect( workflowErrorMock.mock.calls[0][0] ).not.toBeInstanceOf( Error );
167
269
  expect( workflowEndMock ).not.toHaveBeenCalled();
168
270
  } );
169
271
 
170
- it( 'preserves ApplicationFailure metadata when wrapping workflow errors with details', async () => {
272
+ it( 'uses a transparent error cause for the sink and ApplicationFailure', async () => {
171
273
  const { interceptors } = await import( './workflow.js' );
172
274
  const { inbound } = interceptors();
173
275
  const interceptor = inbound[0];
174
- const trace = { trace: { destinations: { local: '/tmp/trace' } } };
175
- const err = ApplicationFailure.create( {
176
- message: 'domain failed',
177
- type: 'DomainFailure',
178
- nonRetryable: true,
179
- details: [ { domain: { reason: 'bad-input' } } ]
180
- } );
181
- err[METADATA_ACCESS_SYMBOL] = trace;
182
- const next = vi.fn().mockRejectedValue( err );
276
+ const cause = new TypeError( 'provider rejected request' );
277
+ cause.code = 'EAUTH';
278
+ const error = new TransparentFatalError( cause );
279
+ const next = vi.fn().mockRejectedValue( error );
183
280
 
184
281
  const thrown = await interceptor.execute( { args: [ {} ] }, next ).catch( e => e );
185
282
 
186
- expect( thrown ).toBeInstanceOf( ApplicationFailure );
187
- expect( thrown ).not.toBe( err );
188
- expect( thrown.cause ).toBe( err );
189
- expect( thrown.cause ).not.toBe( thrown );
190
283
  expect( thrown ).toMatchObject( {
191
- message: 'domain failed',
192
- type: 'DomainFailure',
284
+ message: 'provider rejected request',
285
+ type: 'TypeError',
193
286
  nonRetryable: true,
194
- details: [
195
- { domain: { reason: 'bad-input' } },
196
- trace
197
- ]
287
+ cause,
288
+ details: [ {
289
+ error: {
290
+ name: 'TypeError',
291
+ message: 'provider rejected request',
292
+ code: 'EAUTH'
293
+ }
294
+ } ]
198
295
  } );
199
- expect( workflowErrorMock ).toHaveBeenCalledWith( err );
296
+ expect( workflowErrorMock ).toHaveBeenCalledWith( expect.objectContaining( {
297
+ name: 'TypeError',
298
+ message: 'provider rejected request',
299
+ code: 'EAUTH',
300
+ stack: expect.any( String )
301
+ } ) );
302
+ expect( workflowErrorMock.mock.calls[0][0] ).not.toHaveProperty( 'cause' );
303
+ expect( workflowEndMock ).not.toHaveBeenCalled();
304
+ } );
305
+
306
+ it( 'sinks and rethrows existing Temporal failures unchanged', async () => {
307
+ const { interceptors } = await import( './workflow.js' );
308
+ const { inbound } = interceptors();
309
+ const interceptor = inbound[0];
310
+ const err = ApplicationFailure.retryable( 'domain failed', 'DomainFailure', { reason: 'invalid' } );
311
+ const next = vi.fn().mockRejectedValue( err );
312
+
313
+ await expect( interceptor.execute( { args: [ {} ] }, next ) ).rejects.toBe( err );
314
+ expect( workflowErrorMock ).toHaveBeenCalledWith( expect.objectContaining( {
315
+ name: 'ApplicationFailure',
316
+ message: 'domain failed',
317
+ type: 'DomainFailure',
318
+ details: [ { reason: 'invalid' } ],
319
+ stack: expect.any( String )
320
+ } ) );
321
+ expect( workflowErrorMock.mock.calls[0][0] ).not.toBeInstanceOf( Error );
322
+ expect( workflowEndMock ).not.toHaveBeenCalled();
200
323
  } );
201
324
 
202
325
  it( 'calls sinks.workflow.error and rethrows cancellation errors without wrapping', async () => {
@@ -209,11 +332,15 @@ describe( 'workflow interceptors', () => {
209
332
 
210
333
  await expect( interceptor.execute( { args: [ {} ] }, next ) ).rejects.toBe( cancellation );
211
334
  expect( isCancellationMock ).toHaveBeenCalledWith( cancellation );
212
- expect( workflowErrorMock ).toHaveBeenCalledWith( cancellation );
335
+ expect( workflowErrorMock ).toHaveBeenCalledWith( {
336
+ name: 'Error',
337
+ message: 'Workflow cancelled'
338
+ } );
339
+ expect( workflowErrorMock.mock.calls[0][0] ).not.toBeInstanceOf( Error );
213
340
  expect( workflowEndMock ).not.toHaveBeenCalled();
214
341
  } );
215
342
 
216
- it( 'on ContinueAsNew calls sinks.trace.addWorkflowEventEnd and rethrows', async () => {
343
+ it( 'ends the workflow trace and rethrows ContinueAsNew', async () => {
217
344
  const { ContinueAsNew } = await import( '@temporalio/workflow' );
218
345
  const { WorkflowSpecialOutput } = await import( '#consts' );
219
346
  const { interceptors } = await import( './workflow.js' );
@@ -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
  };
@@ -5,8 +5,7 @@ vi.mock( '#consts', () => ( {
5
5
  ACTIVITY_GET_TRACE_DESTINATIONS: '__internal#getTraceDestinations',
6
6
  WORKFLOWS_INDEX_FILENAME: '__workflows_entrypoint.js',
7
7
  WORKFLOW_CATALOG: 'catalog',
8
- ACTIVITY_OPTIONS_FILENAME: '__activity_options.js',
9
- SHARED_STEP_PREFIX: '$shared'
8
+ ACTIVITY_OPTIONS_FILENAME: '__activity_options.js'
10
9
  } ) );
11
10
 
12
11
  const sendHttpRequestMock = vi.fn();
@@ -63,6 +62,7 @@ describe( 'loadActivities', () => {
63
62
  it( 'loadActivities returns map including system activity and writes options file', async () => {
64
63
  const { loadActivities } = await import( './activities.js' );
65
64
 
65
+ importComponentsMock.mockImplementationOnce( async function *() {} );
66
66
  importComponentsMock.mockImplementationOnce( async function *() {
67
67
  yield {
68
68
  fn: () => {},
@@ -70,7 +70,6 @@ describe( 'loadActivities', () => {
70
70
  path: '/a/steps.js'
71
71
  };
72
72
  } );
73
- importComponentsMock.mockImplementationOnce( async function *() {} );
74
73
 
75
74
  const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
76
75
  const { activities, optionsFile } = await loadActivities( '/root', workflows );
@@ -89,11 +88,11 @@ describe( 'loadActivities', () => {
89
88
 
90
89
  it( 'loadActivities omits activity options when component has no options or no activityOptions', async () => {
91
90
  const { loadActivities } = await import( './activities.js' );
91
+ importComponentsMock.mockImplementationOnce( async function *() {} );
92
92
  importComponentsMock.mockImplementationOnce( async function *() {
93
93
  yield { fn: () => {}, metadata: { name: 'NoOptions' }, path: '/a/steps.js' };
94
94
  yield { fn: () => {}, metadata: { name: 'EmptyOptions', options: {} }, path: '/a/steps2.js' };
95
95
  } );
96
- importComponentsMock.mockImplementationOnce( async function *() {} );
97
96
 
98
97
  await loadActivities( '/root', [ { name: 'A', path: '/a/workflow.js' } ] );
99
98
  const written = JSON.parse(
@@ -105,6 +104,7 @@ describe( 'loadActivities', () => {
105
104
 
106
105
  it( 'loadActivities throws when two activities in the same workflow share a name', async () => {
107
106
  const { loadActivities } = await import( './activities.js' );
107
+ importComponentsMock.mockImplementationOnce( async function *() {} );
108
108
  importComponentsMock.mockImplementationOnce( async function *() {
109
109
  yield { fn: () => {}, metadata: { name: 'DuplicateActivity' }, path: '/a/steps.js' };
110
110
  yield { fn: () => {}, metadata: { name: 'DuplicateActivity' }, path: '/a/evaluators.js' };
@@ -118,7 +118,6 @@ Activity names must be unique within a workflow.'
118
118
 
119
119
  it( 'loadActivities throws when two shared activities share a name', async () => {
120
120
  const { loadActivities } = await import( './activities.js' );
121
- importComponentsMock.mockImplementationOnce( async function *() {} );
122
121
  importComponentsMock.mockImplementationOnce( async function *() {
123
122
  yield { fn: () => {}, metadata: { name: 'DuplicateShared' }, path: '/root/shared/steps/a.js' };
124
123
  yield { fn: () => {}, metadata: { name: 'DuplicateShared' }, path: '/root/shared/evaluators/a.js' };
@@ -133,8 +132,8 @@ Activity names must be unique within a workflow.'
133
132
  const { loadActivities } = await import( './activities.js' );
134
133
  const workflowFiles = [ { path: '/a/steps/foo.js' } ];
135
134
  const sharedFiles = [ { path: '/root/shared/steps/baz.js' } ];
136
- matchFilesMock.mockReturnValueOnce( workflowFiles );
137
135
  matchFilesMock.mockReturnValueOnce( sharedFiles );
136
+ matchFilesMock.mockReturnValueOnce( workflowFiles );
138
137
  importComponentsMock.mockImplementationOnce( async function *() {} );
139
138
  importComponentsMock.mockImplementationOnce( async function *() {} );
140
139
 
@@ -143,12 +142,12 @@ Activity names must be unique within a workflow.'
143
142
 
144
143
  expect( matchFilesMock ).toHaveBeenCalledTimes( 2 );
145
144
  expect( buildActivityMatcherMock ).toHaveBeenCalledWith( '/a' );
146
- expect( matchFilesMock ).toHaveBeenNthCalledWith( 1, '/a', [ activityMatcherMock ] );
147
- expect( matchFilesMock ).toHaveBeenNthCalledWith( 2, '/root', [ sharedStepsDirMock, sharedEvaluatorsDirMock ] );
145
+ expect( matchFilesMock ).toHaveBeenNthCalledWith( 1, '/root', [ sharedStepsDirMock, sharedEvaluatorsDirMock ] );
146
+ expect( matchFilesMock ).toHaveBeenNthCalledWith( 2, '/a', [ activityMatcherMock ] );
148
147
 
149
148
  expect( importComponentsMock ).toHaveBeenCalledTimes( 2 );
150
- expect( importComponentsMock ).toHaveBeenNthCalledWith( 1, workflowFiles );
151
- expect( importComponentsMock ).toHaveBeenNthCalledWith( 2, sharedFiles );
149
+ expect( importComponentsMock ).toHaveBeenNthCalledWith( 1, sharedFiles );
150
+ expect( importComponentsMock ).toHaveBeenNthCalledWith( 2, workflowFiles );
152
151
  } );
153
152
 
154
153
  it( 'loads shared activities from external workflow packages', async () => {
@@ -157,8 +156,6 @@ Activity names must be unique within a workflow.'
157
156
  const localWorkflow = { name: 'Local', path: '/root/workflows/local/workflow.js' };
158
157
  const externalWorkflow = { name: 'External', path: '/root/node_modules/pkg/workflows/a/workflow.js', external: true };
159
158
  findSharedActivitiesFromWorkflowsMock.mockReturnValue( externalSharedFiles );
160
- importComponentsMock.mockImplementationOnce( async function *() {} );
161
- importComponentsMock.mockImplementationOnce( async function *() {} );
162
159
  importComponentsMock.mockImplementationOnce( async function *() {
163
160
  yield {
164
161
  fn: () => {},
@@ -170,27 +167,29 @@ Activity names must be unique within a workflow.'
170
167
  const { activities } = await loadActivities( '/root', [ localWorkflow, externalWorkflow ] );
171
168
 
172
169
  expect( findSharedActivitiesFromWorkflowsMock ).toHaveBeenCalledWith( [ externalWorkflow ] );
173
- expect( importComponentsMock ).toHaveBeenNthCalledWith( 3, externalSharedFiles );
174
- expect( activities['$shared#ExternalShared'] ).toBeTypeOf( 'function' );
170
+ expect( importComponentsMock ).toHaveBeenNthCalledWith( 1, externalSharedFiles );
171
+ expect( activities['Local#ExternalShared'] ).toBeTypeOf( 'function' );
172
+ expect( activities['External#ExternalShared'] ).toBeTypeOf( 'function' );
175
173
  const written = JSON.parse(
176
174
  writeFileInTempDirMock.mock.calls[0][0].replace( /^export default\s*/, '' ).replace( /;\s*$/, '' )
177
175
  );
178
- expect( written['$shared#ExternalShared'] ).toEqual( { retry: { maximumAttempts: 2 } } );
176
+ expect( written['Local#ExternalShared'] ).toEqual( { retry: { maximumAttempts: 2 } } );
177
+ expect( written['External#ExternalShared'] ).toEqual( { retry: { maximumAttempts: 2 } } );
179
178
  } );
180
179
 
181
180
  it( 'loadActivities includes nested workflow steps and shared evaluators', async () => {
182
181
  const { loadActivities } = await import( './activities.js' );
183
182
  importComponentsMock.mockImplementationOnce( async function *() {
184
- yield { fn: () => {}, metadata: { name: 'ActNested' }, path: '/a/steps/foo.js' };
183
+ yield { fn: () => {}, metadata: { name: 'SharedEval' }, path: '/root/shared/evaluators/bar.js' };
185
184
  } );
186
185
  importComponentsMock.mockImplementationOnce( async function *() {
187
- yield { fn: () => {}, metadata: { name: 'SharedEval' }, path: '/root/shared/evaluators/bar.js' };
186
+ yield { fn: () => {}, metadata: { name: 'ActNested' }, path: '/a/steps/foo.js' };
188
187
  } );
189
188
 
190
189
  const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
191
190
  const { activities } = await loadActivities( '/root', workflows );
192
191
  expect( activities['A#ActNested'] ).toBeTypeOf( 'function' );
193
- expect( activities['$shared#SharedEval'] ).toBeTypeOf( 'function' );
192
+ expect( activities['A#SharedEval'] ).toBeTypeOf( 'function' );
194
193
  } );
195
194
 
196
195
  it( 'collects shared nested steps and evaluators across multiple subfolders', async () => {
@@ -205,9 +204,9 @@ Activity names must be unique within a workflow.'
205
204
 
206
205
  const workflows = [ { name: 'A', path: '/a/workflow.js' } ];
207
206
  const { activities } = await loadActivities( '/root', workflows );
208
- expect( activities['$shared#SharedStepPrimary'] ).toBeTypeOf( 'function' );
209
- expect( activities['$shared#SharedStepSecondary'] ).toBeTypeOf( 'function' );
210
- expect( activities['$shared#SharedEvalPrimary'] ).toBeTypeOf( 'function' );
211
- expect( activities['$shared#SharedEvalSecondary'] ).toBeTypeOf( 'function' );
207
+ expect( activities['A#SharedStepPrimary'] ).toBeTypeOf( 'function' );
208
+ expect( activities['A#SharedStepSecondary'] ).toBeTypeOf( 'function' );
209
+ expect( activities['A#SharedEvalPrimary'] ).toBeTypeOf( 'function' );
210
+ expect( activities['A#SharedEvalSecondary'] ).toBeTypeOf( 'function' );
212
211
  } );
213
212
  } );