@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
@@ -3,8 +3,8 @@ import { z } from 'zod';
3
3
  import {
4
4
  ACTIVITY_GET_TRACE_DESTINATIONS,
5
5
  ACTIVITY_WRAPPER_VERSION_FIELD,
6
+ INVOKE_ACTIVITY_SYMBOL,
6
7
  METADATA_ACCESS_SYMBOL,
7
- SHARED_STEP_PREFIX,
8
8
  WORKFLOW_WRAPPER_VERSION_FIELD
9
9
  } from '#consts';
10
10
  import { ValidationError } from '#errors';
@@ -91,6 +91,13 @@ const mockActivities = handlers => {
91
91
  return activities;
92
92
  };
93
93
 
94
+ const installGlobalDispatcher = runId => {
95
+ const dispatcher = vi.fn();
96
+ dispatcher.runId = runId;
97
+ globalThis[INVOKE_ACTIVITY_SYMBOL] = dispatcher;
98
+ return dispatcher;
99
+ };
100
+
94
101
  const workflowDefinition = overrides => ( {
95
102
  name: 'test_wf',
96
103
  description: 'Test workflow',
@@ -100,11 +107,10 @@ const workflowDefinition = overrides => ( {
100
107
  ...overrides
101
108
  } );
102
109
 
103
- const invokeWorkflowFromHelper = ( wf, input, options ) => wf( input, options );
104
-
105
110
  describe( 'workflow()', () => {
106
111
  beforeEach( () => {
107
112
  vi.clearAllMocks();
113
+ delete globalThis[INVOKE_ACTIVITY_SYMBOL];
108
114
  inWorkflowContextMock.mockReturnValue( true );
109
115
  executeChildMock.mockResolvedValue( { output: {} } );
110
116
  setWorkflowInfo();
@@ -256,15 +262,18 @@ describe( 'workflow()', () => {
256
262
  } );
257
263
 
258
264
  describe( 'child workflow trigger path', () => {
259
- it( 'starts a child workflow when memo.stack already contains the current workflowId', async () => {
265
+ it( 'starts a child workflow and forwards child-safe invocation options', async () => {
260
266
  const { workflow } = await import( './workflow.js' );
261
267
  const { ParentClosePolicy } = await import( '@temporalio/workflow' );
268
+ installGlobalDispatcher( 'run-123' );
262
269
  const memo = {
263
- stack: [ 'root-workflow', 'workflow-123' ],
264
270
  traceInfo: { workflowId: 'root-workflow' },
265
- activityOptions: {
266
- startToCloseTimeout: '10m',
267
- retry: { maximumAttempts: 2 }
271
+ parentActivityOptions: {
272
+ heartbeatTimeout: '30s',
273
+ retry: {
274
+ maximumInterval: '20s',
275
+ maximumAttempts: 4
276
+ }
268
277
  }
269
278
  };
270
279
  setWorkflowInfo( { memo } );
@@ -279,6 +288,7 @@ describe( 'workflow()', () => {
279
288
 
280
289
  await expect( wf( { id: 1 }, {
281
290
  detached: true,
291
+ context: { testOnly: true },
282
292
  activityOptions: {
283
293
  startToCloseTimeout: '2m',
284
294
  retry: { maximumAttempts: 7 }
@@ -286,6 +296,7 @@ describe( 'workflow()', () => {
286
296
  } ) ).resolves.toEqual( { child: 'ok' } );
287
297
  expect( validateInvocationOptionsMock ).toHaveBeenCalledWith( {
288
298
  detached: true,
299
+ context: { testOnly: true },
289
300
  activityOptions: {
290
301
  startToCloseTimeout: '2m',
291
302
  retry: { maximumAttempts: 7 }
@@ -293,25 +304,28 @@ describe( 'workflow()', () => {
293
304
  } );
294
305
 
295
306
  expect( executeChildMock ).toHaveBeenCalledWith( 'child_target_wf', {
296
- args: [ { id: 1 } ],
297
- workflowId: expect.stringMatching( /^workflow-123-/ ),
298
- parentClosePolicy: ParentClosePolicy.ABANDON,
299
- memo: {
300
- stack: [ 'root-workflow', 'workflow-123' ],
301
- traceInfo: { workflowId: 'root-workflow' },
307
+ args: [ {
308
+ id: 1
309
+ }, {
302
310
  activityOptions: {
303
311
  startToCloseTimeout: '2m',
304
312
  retry: { maximumAttempts: 7 }
305
313
  }
306
- }
314
+ } ],
315
+ workflowId: expect.stringMatching( /^workflow-123-/ ),
316
+ parentClosePolicy: ParentClosePolicy.ABANDON,
317
+ memo
307
318
  } );
319
+ expect( executeChildMock.mock.calls[0][1].args[1] ).not.toHaveProperty( 'detached' );
320
+ expect( executeChildMock.mock.calls[0][1].args[1] ).not.toHaveProperty( 'context' );
308
321
  expect( proxyActivitiesMock ).not.toHaveBeenCalled();
309
322
  } );
310
323
 
311
- it( 'uses empty args and terminate policy by default for child workflow execution', async () => {
324
+ it( 'uses undefined input and terminate policy by default for child workflow execution', async () => {
312
325
  const { workflow } = await import( './workflow.js' );
313
326
  const { ParentClosePolicy } = await import( '@temporalio/workflow' );
314
- setWorkflowInfo( { memo: { stack: [ 'workflow-123' ], activityOptions: { heartbeatTimeout: '1m' } } } );
327
+ installGlobalDispatcher( 'run-123' );
328
+ setWorkflowInfo( { memo: { traceInfo: { workflowId: 'root-workflow' } } } );
315
329
  executeChildMock.mockResolvedValueOnce( { output: 'done' } );
316
330
 
317
331
  const wf = workflow( workflowDefinition( {
@@ -323,89 +337,42 @@ describe( 'workflow()', () => {
323
337
 
324
338
  await expect( wf() ).resolves.toBe( 'done' );
325
339
  expect( executeChildMock ).toHaveBeenCalledWith( 'no_input_child_wf', expect.objectContaining( {
326
- args: [],
340
+ args: [ undefined, { activityOptions: undefined } ],
327
341
  parentClosePolicy: ParentClosePolicy.TERMINATE,
328
- memo: { stack: [ 'workflow-123' ], activityOptions: { heartbeatTimeout: '1m' } }
342
+ memo: { traceInfo: { workflowId: 'root-workflow' } }
329
343
  } ) );
330
344
  } );
331
345
 
332
- it( 'starts a child workflow when the call is made through a helper outside the handler', async () => {
346
+ it( 'propagates executeChild errors without root ApplicationFailure wrapping', async () => {
333
347
  const { workflow } = await import( './workflow.js' );
334
- const { ParentClosePolicy } = await import( '@temporalio/workflow' );
335
- const getTraceDestinations = vi.fn().mockResolvedValue( activityOutput( null ) );
336
- const info = setWorkflowInfo( { workflowType: 'indirect_parent_wf', memo: {} } );
337
- mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: getTraceDestinations } );
338
- executeChildMock.mockResolvedValueOnce( { output: { child: 'ok' } } );
339
- const childFn = vi.fn();
348
+ const error = new Error( 'child failed' );
349
+ installGlobalDispatcher( 'run-123' );
350
+ executeChildMock.mockRejectedValueOnce( error );
340
351
 
341
- const childWorkflow = workflow( workflowDefinition( {
342
- name: 'indirect_child_wf',
343
- inputSchema: z.object( { id: z.number() } ),
344
- outputSchema: z.object( { child: z.string() } ),
345
- fn: childFn
346
- } ) );
347
- const parentWorkflow = workflow( workflowDefinition( {
348
- name: 'indirect_parent_wf',
349
- outputSchema: z.object( { child: z.string() } ),
350
- fn: async () => invokeWorkflowFromHelper( childWorkflow, { id: 1 }, {
351
- activityOptions: {
352
- retry: { maximumAttempts: 1 }
353
- }
354
- } )
355
- } ) );
352
+ const wf = workflow( workflowDefinition( { name: 'failing_child_wf' } ) );
356
353
 
357
- await expect( parentWorkflow( {} ) ).resolves.toEqual( {
358
- [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
359
- output: { child: 'ok' },
360
- trace: { destinations: {} }
361
- } );
362
- expect( childFn ).not.toHaveBeenCalled();
363
- expect( executeChildMock ).toHaveBeenCalledWith( 'indirect_child_wf', expect.objectContaining( {
364
- args: [ { id: 1 } ],
365
- parentClosePolicy: ParentClosePolicy.TERMINATE,
366
- memo: expect.objectContaining( {
367
- stack: [ 'workflow-123' ],
368
- traceInfo: info.memo.traceInfo,
369
- activityOptions: expect.objectContaining( {
370
- retry: expect.objectContaining( { maximumAttempts: 1 } )
371
- } )
372
- } )
373
- } ) );
374
- expect( getTraceDestinations ).toHaveBeenCalledWith( info.memo.traceInfo );
354
+ await expect( wf( {} ) ).rejects.toBe( error );
375
355
  } );
356
+ } );
376
357
 
377
- it( 'does not fallback to child execution when the replayed workflow type matches an alias', async () => {
358
+ describe( 'workflow execution path', () => {
359
+ it( 'rejects a global activity dispatcher left by another workflow run', async () => {
378
360
  const { workflow } = await import( './workflow.js' );
379
- setWorkflowInfo( { workflowType: 'old_root_wf', memo: {} } );
361
+ const fn = vi.fn().mockResolvedValue( {} );
362
+ installGlobalDispatcher( 'stale-run' );
363
+ setWorkflowInfo( { runId: 'current-run' } );
380
364
 
381
365
  const wf = workflow( workflowDefinition( {
382
- name: 'renamed_root_wf',
383
- aliases: [ 'old_root_wf' ],
384
- outputSchema: z.object( { ok: z.boolean() } ),
385
- fn: async () => ( { ok: true } )
366
+ name: 'contamination_wf',
367
+ fn
386
368
  } ) );
387
369
 
388
- await expect( wf( {} ) ).resolves.toEqual( {
389
- [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
390
- output: { ok: true },
391
- trace: { destinations: { local: '/tmp/trace' } }
392
- } );
370
+ await expect( wf( {} ) ).rejects.toThrow( /Contamination of the workflow Node global context/ );
393
371
  expect( executeChildMock ).not.toHaveBeenCalled();
372
+ expect( proxyActivitiesMock ).not.toHaveBeenCalled();
373
+ expect( fn ).not.toHaveBeenCalled();
394
374
  } );
395
375
 
396
- it( 'propagates executeChild errors without root ApplicationFailure wrapping', async () => {
397
- const { workflow } = await import( './workflow.js' );
398
- const error = new Error( 'child failed' );
399
- setWorkflowInfo( { memo: { stack: [ 'workflow-123' ] } } );
400
- executeChildMock.mockRejectedValueOnce( error );
401
-
402
- const wf = workflow( workflowDefinition( { name: 'failing_child_wf' } ) );
403
-
404
- await expect( wf( {} ) ).rejects.toBe( error );
405
- } );
406
- } );
407
-
408
- describe( 'workflow execution path', () => {
409
376
  it( 'initializes root memo, skips trace destinations when trace is disabled, validates output, and returns an envelope', async () => {
410
377
  const { workflow } = await import( './workflow.js' );
411
378
  const getTraceDestinations = vi.fn().mockResolvedValue( activityOutput( { local: '/tmp/root-trace' } ) );
@@ -430,24 +397,79 @@ describe( 'workflow()', () => {
430
397
  output: { ok: true },
431
398
  trace: { destinations: {} }
432
399
  } );
433
- expect( info.memo.stack ).toEqual( [ 'workflow-123' ] );
434
400
  expect( info.memo.traceInfo ).toBeUndefined();
435
- expect( info.memo.activityOptions ).toEqual( expect.objectContaining( {
401
+ expect( info.memo.activityOptions ).toBeUndefined();
402
+ expect( info.memo.parentActivityOptions ).toEqual( expect.objectContaining( {
403
+ startToCloseTimeout: '5m',
404
+ heartbeatTimeout: '5m',
405
+ retry: expect.objectContaining( { maximumAttempts: 5 } )
406
+ } ) );
407
+ expect( proxyActivitiesMock ).toHaveBeenCalledWith( expect.objectContaining( {
436
408
  startToCloseTimeout: '5m',
437
409
  heartbeatTimeout: '5m',
438
410
  retry: expect.objectContaining( { maximumAttempts: 5 } )
439
411
  } ) );
440
- expect( proxyActivitiesMock ).toHaveBeenCalledWith( info.memo.activityOptions );
441
412
  expect( getTraceDestinations ).not.toHaveBeenCalled();
442
413
  } );
443
414
 
415
+ it( 'resolves activity options by invocation, definition, inherited memo, then default precedence', async () => {
416
+ const { workflow } = await import( './workflow.js' );
417
+ const info = setWorkflowInfo( {
418
+ workflowType: 'activity_options_wf',
419
+ memo: {
420
+ parentActivityOptions: {
421
+ heartbeatTimeout: '30s',
422
+ retry: {
423
+ maximumInterval: '30s',
424
+ maximumAttempts: 4
425
+ }
426
+ }
427
+ }
428
+ } );
429
+
430
+ const wf = workflow( workflowDefinition( {
431
+ name: 'activity_options_wf',
432
+ options: {
433
+ activityOptions: {
434
+ startToCloseTimeout: '5m',
435
+ retry: {
436
+ backoffCoefficient: 3,
437
+ maximumAttempts: 2
438
+ }
439
+ }
440
+ }
441
+ } ) );
442
+
443
+ await wf( {}, {
444
+ activityOptions: {
445
+ heartbeatTimeout: '1m',
446
+ retry: {
447
+ initialInterval: '1s',
448
+ maximumAttempts: 9
449
+ }
450
+ }
451
+ } );
452
+
453
+ expect( proxyActivitiesMock ).toHaveBeenCalledWith( {
454
+ startToCloseTimeout: '5m',
455
+ heartbeatTimeout: '1m',
456
+ retry: {
457
+ initialInterval: '1s',
458
+ backoffCoefficient: 3,
459
+ maximumInterval: '30s',
460
+ maximumAttempts: 9,
461
+ nonRetryableErrorTypes: [ ValidationError.name, 'FatalError' ]
462
+ }
463
+ } );
464
+ expect( info.memo.parentActivityOptions ).toEqual( proxyActivitiesMock.mock.calls[0][0] );
465
+ } );
466
+
444
467
  it( 'runs non-root workflow execution without rebuilding trace info or fetching trace destinations', async () => {
445
468
  const { workflow } = await import( './workflow.js' );
446
469
  const getTraceDestinations = vi.fn().mockResolvedValue( activityOutput( { local: '/tmp/trace' } ) );
447
470
  const memo = {
448
- stack: [ 'root-workflow' ],
449
471
  traceInfo: { workflowId: 'root-workflow' },
450
- activityOptions: {
472
+ parentActivityOptions: {
451
473
  startToCloseTimeout: '9m',
452
474
  retry: { maximumAttempts: 8 }
453
475
  }
@@ -475,13 +497,18 @@ describe( 'workflow()', () => {
475
497
  [WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
476
498
  output: { ok: true }
477
499
  } );
478
- expect( info.memo.stack ).toEqual( [ 'root-workflow', 'child-workflow' ] );
479
500
  expect( info.memo.traceInfo ).toBe( memo.traceInfo );
480
- expect( info.memo.activityOptions ).toEqual( expect.objectContaining( {
481
- startToCloseTimeout: '9m',
482
- retry: expect.objectContaining( { maximumAttempts: 8 } )
501
+ expect( info.memo.activityOptions ).toBeUndefined();
502
+ expect( info.memo.parentActivityOptions ).toEqual( expect.objectContaining( {
503
+ startToCloseTimeout: '1m',
504
+ heartbeatTimeout: '5m',
505
+ retry: expect.objectContaining( { maximumAttempts: 2 } )
506
+ } ) );
507
+ expect( proxyActivitiesMock ).toHaveBeenCalledWith( expect.objectContaining( {
508
+ startToCloseTimeout: '1m',
509
+ heartbeatTimeout: '5m',
510
+ retry: expect.objectContaining( { maximumAttempts: 2 } )
483
511
  } ) );
484
- expect( proxyActivitiesMock ).toHaveBeenCalledWith( info.memo.activityOptions );
485
512
  expect( getTraceDestinations ).not.toHaveBeenCalled();
486
513
  } );
487
514
 
@@ -522,6 +549,7 @@ describe( 'workflow()', () => {
522
549
  await expect( wf( { value: 1 } ) ).rejects.toBe( inputError );
523
550
  expect( inputError[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
524
551
 
552
+ delete globalThis[INVOKE_ACTIVITY_SYMBOL];
525
553
  setWorkflowInfo( { workflowType: 'runtime_validation_wf', memo: {} } );
526
554
  validateOutputMock.mockImplementationOnce( () => {
527
555
  throw outputError;
@@ -532,7 +560,7 @@ describe( 'workflow()', () => {
532
560
  } );
533
561
 
534
562
  describe( 'activity dispatchers', () => {
535
- it( 'invokes workflow-scoped steps and evaluators and unwraps activity output', async () => {
563
+ it( 'invokes workflow-scoped activities and unwraps activity output', async () => {
536
564
  const { workflow } = await import( './workflow.js' );
537
565
  setWorkflowInfo( { workflowType: 'dispatch_wf' } );
538
566
  const step = vi.fn().mockResolvedValue( activityOutput( 'step-output' ) );
@@ -546,10 +574,10 @@ describe( 'workflow()', () => {
546
574
  const wf = workflow( workflowDefinition( {
547
575
  name: 'dispatch_wf',
548
576
  outputSchema: z.object( { stepResult: z.string(), evalResult: z.string() } ),
549
- async fn() {
577
+ fn: async () => {
550
578
  return {
551
- stepResult: await this.invokeStep( 'stepA', { a: 1 }, { b: 2 } ),
552
- evalResult: await this.invokeEvaluator( 'evalA', { c: 3 } )
579
+ stepResult: await globalThis[INVOKE_ACTIVITY_SYMBOL]( 'stepA', { a: 1 }, { b: 2 } ),
580
+ evalResult: await globalThis[INVOKE_ACTIVITY_SYMBOL]( 'evalA', { c: 3 } )
553
581
  };
554
582
  }
555
583
  } ) );
@@ -563,24 +591,24 @@ describe( 'workflow()', () => {
563
591
  expect( evaluator ).toHaveBeenCalledWith( { c: 3 } );
564
592
  } );
565
593
 
566
- it( 'invokes shared steps and shared evaluators with the shared prefix', async () => {
594
+ it( 'invokes shared activities through the workflow namespace', async () => {
567
595
  const { workflow } = await import( './workflow.js' );
568
596
  setWorkflowInfo( { workflowType: 'shared_dispatch_wf' } );
569
597
  const sharedStep = vi.fn().mockResolvedValue( activityOutput( 'shared-step-output' ) );
570
598
  const sharedEvaluator = vi.fn().mockResolvedValue( activityOutput( 'shared-eval-output' ) );
571
599
  mockActivities( {
572
600
  [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( null ) ),
573
- [`${SHARED_STEP_PREFIX}#stepA`]: sharedStep,
574
- [`${SHARED_STEP_PREFIX}#evalA`]: sharedEvaluator
601
+ 'shared_dispatch_wf#stepA': sharedStep,
602
+ 'shared_dispatch_wf#evalA': sharedEvaluator
575
603
  } );
576
604
 
577
605
  const wf = workflow( workflowDefinition( {
578
606
  name: 'shared_dispatch_wf',
579
607
  outputSchema: z.object( { stepResult: z.string(), evalResult: z.string() } ),
580
- async fn() {
608
+ fn: async () => {
581
609
  return {
582
- stepResult: await this.invokeSharedStep( 'stepA' ),
583
- evalResult: await this.invokeSharedEvaluator( 'evalA', { x: 1 } )
610
+ stepResult: await globalThis[INVOKE_ACTIVITY_SYMBOL]( 'stepA' ),
611
+ evalResult: await globalThis[INVOKE_ACTIVITY_SYMBOL]( 'evalA', { x: 1 } )
584
612
  };
585
613
  }
586
614
  } ) );
@@ -596,9 +624,10 @@ describe( 'workflow()', () => {
596
624
  } );
597
625
 
598
626
  describe( 'error handling', () => {
599
- it( 'attaches root trace destinations to root workflow errors before rethrowing', async () => {
627
+ it( 'attaches root trace destinations to root workflow errors and preserves existing details', async () => {
600
628
  const { workflow } = await import( './workflow.js' );
601
- const error = new Error( 'root failed' );
629
+ const error = new Error( 'root failed with details' );
630
+ error.details = [ { domain: { reason: 'bad-input' } } ];
602
631
  setWorkflowInfo( { workflowType: 'root_error_wf' } );
603
632
 
604
633
  const wf = workflow( workflowDefinition( {
@@ -610,23 +639,6 @@ describe( 'workflow()', () => {
610
639
 
611
640
  const thrown = await wf( {} ).catch( e => e );
612
641
  expect( thrown ).toBe( error );
613
- expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
614
- } );
615
-
616
- it( 'preserves existing error details when attaching root trace metadata', async () => {
617
- const { workflow } = await import( './workflow.js' );
618
- const error = new Error( 'root failed with details' );
619
- error.details = [ { domain: { reason: 'bad-input' } } ];
620
- setWorkflowInfo( { workflowType: 'root_error_existing_details_wf' } );
621
-
622
- const wf = workflow( workflowDefinition( {
623
- name: 'root_error_existing_details_wf',
624
- fn: async () => {
625
- throw error;
626
- }
627
- } ) );
628
-
629
- await expect( wf( {} ) ).rejects.toBe( error );
630
642
  expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
631
643
  expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: { local: '/tmp/trace' } } } );
632
644
  } );
@@ -648,25 +660,6 @@ describe( 'workflow()', () => {
648
660
  expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: {} } } );
649
661
  } );
650
662
 
651
- it( 'preserves existing error details when no trace destinations are available', async () => {
652
- const { workflow } = await import( './workflow.js' );
653
- setWorkflowInfo( { workflowType: 'root_error_existing_details_no_trace_wf' } );
654
- mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( {} ) ) } );
655
- const error = new Error( 'root failed without trace' );
656
- error.details = [ { domain: { reason: 'bad-input' } } ];
657
-
658
- const wf = workflow( workflowDefinition( {
659
- name: 'root_error_existing_details_no_trace_wf',
660
- fn: async () => {
661
- throw error;
662
- }
663
- } ) );
664
-
665
- await expect( wf( {} ) ).rejects.toBe( error );
666
- expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
667
- expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: {} } } );
668
- } );
669
-
670
663
  it( 'attaches trace metadata to existing root ApplicationFailure without wrapping it', async () => {
671
664
  const { workflow } = await import( './workflow.js' );
672
665
  const { ApplicationFailure } = await import( '@temporalio/workflow' );
@@ -699,7 +692,7 @@ describe( 'workflow()', () => {
699
692
  setWorkflowInfo( {
700
693
  workflowId: 'nested-workflow',
701
694
  root: { workflowId: 'root-workflow', runId: 'root-run' },
702
- memo: { stack: [ 'root-workflow' ], traceInfo: { workflowId: 'root-workflow' } }
695
+ memo: { traceInfo: { workflowId: 'root-workflow' } }
703
696
  } );
704
697
 
705
698
  const wf = workflow( workflowDefinition( {
@@ -23,29 +23,34 @@ export declare const Objects: {
23
23
  isPlainObject( object: unknown ): boolean,
24
24
 
25
25
  /**
26
- * Creates a new object by merging object `b` onto object `a`, biased toward `b`:
27
- * - Fields in `b` overwrite fields in `a`.
28
- * - Fields in `b` that don't exist in `a` are created.
29
- * - Fields in `a` that don't exist in `b` are left unchanged.
30
- *
31
- * @param a - The base object.
32
- * @param b - The overriding object.
33
- * @throws {Error} If either `a` or `b` is not a plain object.
26
+ * Creates a new object by recursively merging overlays onto a base object.
27
+ * Later objects overwrite fields from earlier objects.
28
+ *
29
+ * Non-object overlays are ignored.
30
+ *
31
+ * @param base - The base object.
32
+ * @param overlays - The overriding objects, applied from left to right.
33
+ * @throws {Error} If `base` is not a plain object.
34
34
  * @returns A new merged object.
35
35
  */
36
- deepMerge( a: object, b: object | null | undefined ): object,
36
+ deepMerge( base: object, ...overlays: Array<object | null | undefined> ): object,
37
37
 
38
38
  /**
39
- * Creates a new object by merging object `b` onto object `a`, biased toward `b`:
40
- * - Fields in `b` that don't exist in `a` are created.
41
- * - Fields in `a` that don't exist in `b` are left unchanged.
42
- * - Fields in `a` and `b` are passed as arguments to the resolve function (a,b) and its return assigns the new value.
43
- *
44
- * @param a - The base object.
45
- * @param b - The overriding object.
46
- * @param resolver - The resolver function.
47
- * @throws {Error} If either `a` or `b` is not a plain object.
39
+ * Creates a new object by recursively merging overlays onto a base object.
40
+ * Existing leaf values are resolved by the resolver function.
41
+ *
42
+ * Non-object overlays are ignored.
43
+ *
44
+ * @param base - The base object.
45
+ * @param args - The overriding objects, applied from left to right, followed by the resolver function.
46
+ * @throws {Error} If `base` is not a plain object.
48
47
  * @returns A new merged object.
49
48
  */
50
- deepMergeWithResolver( a: object, b: object | null | undefined, resolver: ( a: unknown, b: unknown ) => unknown ): object
49
+ deepMergeWithResolver(
50
+ base: object,
51
+ ...args: [
52
+ ...overlays: Array<object | null | undefined>,
53
+ resolver: ( a: unknown, b: unknown ) => unknown
54
+ ]
55
+ ): object
51
56
  };
@@ -2,14 +2,12 @@
2
2
  * Tools to interact with Events
3
3
  */
4
4
  export declare const Event: {
5
+
5
6
  /**
6
- * Emits a custom event on the in-process message bus.
7
- *
8
- * When called inside an Output activity context, the framework automatically
9
- * attaches `activityInfo`, `workflowDetails`, and `outputActivityKind` onto the emitted payload.
7
+ * Emits an event on step message bus.
10
8
  *
11
9
  * @param eventName - The name of the event to emit
12
- * @param payload - An optional payload to send to the event
10
+ * @param payload - An optional payload to send to the event.
13
11
  */
14
- emit( eventName: string, payload?: object ): void;
12
+ emit( eventName: string, payload?: object ): boolean;
15
13
  };
@@ -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
-