@outputai/core 0.10.1-dev.b7b2fbe.0 → 0.10.1-next.1070949.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 (77) 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 +5 -2
  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 +5 -1
  19. package/src/helpers/object.spec.js +5 -0
  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 +24 -52
  42. package/src/interface/workflow.spec.js +109 -108
  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/runtime/events.d.ts +4 -6
  48. package/src/sdk/runtime/events.js +2 -15
  49. package/src/sdk/runtime/events.spec.js +14 -92
  50. package/src/tracing/trace_engine.js +9 -11
  51. package/src/tracing/trace_engine.spec.js +67 -28
  52. package/src/worker/catalog_workflow/catalog_job.js +3 -3
  53. package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
  54. package/src/worker/configs.js +0 -3
  55. package/src/worker/global_functions.js +2 -2
  56. package/src/worker/global_functions.spec.js +3 -3
  57. package/src/worker/index.js +20 -11
  58. package/src/worker/index.spec.js +29 -16
  59. package/src/worker/interceptors/activity.js +34 -21
  60. package/src/worker/interceptors/activity.spec.js +77 -172
  61. package/src/worker/interceptors/workflow.js +53 -11
  62. package/src/worker/interceptors/workflow.spec.js +176 -49
  63. package/src/worker/loader/workflows.js +16 -3
  64. package/src/worker/loader/workflows.spec.js +37 -5
  65. package/src/worker/log_hooks.js +22 -19
  66. package/src/worker/log_hooks.spec.js +14 -6
  67. package/src/worker/sinks.js +8 -8
  68. package/src/worker/sinks.spec.js +9 -9
  69. package/src/worker/telemetry.js +2 -1
  70. package/src/worker/temporal_logger.js +33 -0
  71. package/src/worker/temporal_logger.spec.js +90 -0
  72. package/src/helpers/aggregations.js +0 -44
  73. package/src/helpers/aggregations.spec.js +0 -92
  74. package/src/tracing/tools/utils.js +0 -42
  75. package/src/tracing/tools/utils.spec.js +0 -83
  76. package/src/worker/proxy.js +0 -20
  77. package/src/worker/proxy.spec.js +0 -51
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.10.1-dev.b7b2fbe.0",
3
+ "version": "0.10.1-next.1070949.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "engines": {
@@ -41,17 +41,17 @@
41
41
  "@babel/parser": "7.29.2",
42
42
  "@babel/traverse": "7.29.0",
43
43
  "@babel/types": "7.29.0",
44
- "@temporalio/activity": "1.17.0",
45
- "@temporalio/client": "1.17.0",
46
- "@temporalio/common": "1.17.0",
47
- "@temporalio/worker": "1.17.0",
48
- "@temporalio/workflow": "1.17.0",
44
+ "@temporalio/activity": "1.20.3",
45
+ "@temporalio/client": "1.20.3",
46
+ "@temporalio/common": "1.20.3",
47
+ "@temporalio/worker": "1.20.3",
48
+ "@temporalio/workflow": "1.20.3",
49
49
  "decimal.js": "10.6.0",
50
50
  "folder-hash": "4.1.3",
51
51
  "json-stream-stringify": "3.1.6",
52
52
  "redis": "5.12.1",
53
53
  "stacktrace-parser": "0.1.11",
54
- "undici": "8.5.0",
54
+ "undici": "8.9.0",
55
55
  "winston": "3.19.0"
56
56
  },
57
57
  "peerDependencies": {
@@ -69,6 +69,7 @@
69
69
  "#consts": "./src/consts.js",
70
70
  "#errors": "./src/errors.js",
71
71
  "#logger": "./src/logger/index.js",
72
+ "#hooks/*": "./src/hooks/*.js",
72
73
  "#helpers/*": "./src/helpers/*.js",
73
74
  "#tracing": "./src/tracing/internal_interface.js",
74
75
  "#trace_attribute": "./src/tracing/trace_attribute.js",
package/src/bus.js CHANGED
@@ -1,30 +1,68 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { randomUUID } from 'node:crypto';
3
- import { isPlainObject } from '#helpers/object';
3
+ import { Storage } from '#async_storage';
4
+ import { WORKFLOW_CATALOG } from '#consts';
4
5
 
5
- const emitter = new EventEmitter();
6
+ const mainEventBus = new EventEmitter();
7
+ const stepEventBus = new EventEmitter();
8
+
9
+ const getContext = () => {
10
+ const ctx = Storage.load();
11
+ if ( ctx ) {
12
+ const { outputActivityKind, activityInfo, workflowDetails } = ctx;
13
+ return { outputActivityKind, activityInfo, workflowDetails };
14
+ }
15
+ return {};
16
+ };
6
17
 
7
18
  /**
8
- * Every object payload emitted through `messageBus` is stamped with a UUID v4 `eventId`.
19
+ * Attaches information and filter out events from the bus by proxying .emit().
20
+ *
21
+ * If createEnvelope=true, wrap payload:
22
+ * ```js
23
+ * {
24
+ * eventId,
25
+ * eventDate,
26
+ * outputActivityKind,
27
+ * activityInfo,
28
+ * workflowDetails,
29
+ * payload: <original payload>
30
+ * }
31
+ * ```
32
+ *
33
+ * if createEnvelope=false, adds eventId and eventDate to the original payload object
34
+ *
35
+ * @param {object} args
36
+ * @param {object} args.bus The event emitter to proxy
37
+ * @param {boolean} args.createEnvelope Whether to wrap event around an envelope
38
+ * @param {function} args.filter Filter function to drop events
9
39
  */
10
- emitter.emit = new Proxy( emitter.emit, {
11
- apply( target, thisArg, args ) {
12
- const [ eventName, payload, ...extras ] = args;
13
- const newArguments = [];
14
-
15
- // do now push arguments that dont exist. if payload is undefined with len=1, means it was never defined
16
- // if payload is undefined with len>1, means user passed 'undefined' as argument
17
- if ( args.length > 1 ) {
18
- newArguments.push( isPlainObject( payload ) ? {
40
+ const proxyBus = ( { bus, createEnvelope = false, filter } ) => {
41
+ bus.emit = new Proxy( bus.emit, {
42
+ apply( target, thisArg, args ) {
43
+ const [ eventName, payload, ...rest ] = args;
44
+
45
+ const shouldEmit = filter?.( eventName, payload ) ?? true;
46
+ if ( !shouldEmit ) {
47
+ return false;
48
+ }
49
+
50
+ const eventFields = {
19
51
  eventId: randomUUID(),
20
- eventDate: Date.now(),
21
- ...payload
22
- } : payload );
23
- newArguments.push( ...extras );
52
+ eventDate: Date.now()
53
+ };
54
+
55
+ const newPayload = createEnvelope ? { ...eventFields, ...getContext(), payload } : { ...eventFields, ...payload };
56
+ return Reflect.apply( target, thisArg, [ eventName, newPayload, ...rest ] );
24
57
  }
58
+ } );
59
+ };
25
60
 
26
- return Reflect.apply( target, thisArg, [ eventName, ...newArguments ] );
27
- }
28
- } );
61
+ const catalogWfFilter = ( _, payload ) => payload?.workflowDetails?.workflowType !== WORKFLOW_CATALOG;
62
+
63
+ // Main bus is not used inside steps, so it doesn't need context
64
+ proxyBus( { bus: mainEventBus, createEnvelope: false, filter: catalogWfFilter } );
65
+ // This receives SDK and user events from within steps, so add context to it
66
+ proxyBus( { bus: stepEventBus, createEnvelope: true } );
29
67
 
30
- export const messageBus = emitter;
68
+ export { mainEventBus, stepEventBus };
package/src/bus.spec.js CHANGED
@@ -1,135 +1,193 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { messageBus } from './bus.js';
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ const loadMock = vi.hoisted( () => vi.fn() );
4
+
5
+ vi.mock( '#async_storage', () => ( {
6
+ Storage: { load: loadMock }
7
+ } ) );
8
+
9
+ import { mainEventBus, stepEventBus } from './bus.js';
3
10
 
4
11
  const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
12
+ const ACTIVITY_CONTEXT = {
13
+ activityInfo: { activityId: 'activity-id' },
14
+ workflowDetails: { workflowId: 'workflow-id' },
15
+ outputActivityKind: 'step'
16
+ };
5
17
 
6
- describe( 'messageBus', () => {
18
+ describe( 'event buses', () => {
7
19
  beforeEach( () => {
8
- messageBus.removeAllListeners();
20
+ mainEventBus.removeAllListeners();
21
+ stepEventBus.removeAllListeners();
22
+ loadMock.mockReset();
9
23
  } );
10
24
 
11
- describe( 'eventId stamping', () => {
12
- it( 'stamps a UUID v4 eventId on every object payload', () => {
25
+ afterEach( () => {
26
+ vi.useRealTimers();
27
+ } );
28
+
29
+ describe( 'mainEventBus', () => {
30
+ it( 'adds event metadata to payload fields', () => {
13
31
  const handler = vi.fn();
14
- messageBus.on( 'test:event', handler );
32
+ const now = new Date( '2026-06-02T12:00:00.000Z' );
33
+ vi.useFakeTimers();
34
+ vi.setSystemTime( now );
35
+ mainEventBus.on( 'test:event', handler );
15
36
 
16
- messageBus.emit( 'test:event', { foo: 'bar' } );
37
+ mainEventBus.emit( 'test:event', { foo: 'bar' } );
17
38
 
18
- expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
19
- foo: 'bar',
20
- eventId: expect.stringMatching( UUID_V4_REGEX )
21
- } ) );
39
+ expect( handler ).toHaveBeenCalledWith( {
40
+ eventId: expect.stringMatching( UUID_V4_REGEX ),
41
+ eventDate: now.getTime(),
42
+ foo: 'bar'
43
+ } );
22
44
  } );
23
45
 
24
- it( 'gives distinct emits distinct eventIds', () => {
46
+ it( 'preserves caller-supplied event metadata', () => {
25
47
  const handler = vi.fn();
26
- messageBus.on( 'test:event', handler );
48
+ mainEventBus.on( 'test:event', handler );
27
49
 
28
- messageBus.emit( 'test:event', { i: 1 } );
29
- messageBus.emit( 'test:event', { i: 2 } );
50
+ mainEventBus.emit( 'test:event', { eventId: 'fixed-id', eventDate: 1234 } );
30
51
 
31
- const first = handler.mock.calls[0][0].eventId;
32
- const second = handler.mock.calls[1][0].eventId;
33
- expect( first ).toMatch( UUID_V4_REGEX );
34
- expect( second ).toMatch( UUID_V4_REGEX );
35
- expect( first ).not.toBe( second );
52
+ expect( handler ).toHaveBeenCalledWith( {
53
+ eventId: 'fixed-id',
54
+ eventDate: 1234
55
+ } );
36
56
  } );
37
57
 
38
- it( 'preserves a caller-supplied eventId (deterministic retry case)', () => {
58
+ it( 'does not mutate the caller-supplied payload', () => {
39
59
  const handler = vi.fn();
40
- messageBus.on( 'test:event', handler );
60
+ const payload = { foo: 'bar' };
61
+ mainEventBus.on( 'test:event', handler );
41
62
 
42
- messageBus.emit( 'test:event', { eventId: 'fixed-id', foo: 'bar' } );
63
+ mainEventBus.emit( 'test:event', payload );
43
64
 
44
- expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
45
- eventId: 'fixed-id',
46
- foo: 'bar'
65
+ expect( handler.mock.calls[0][0] ).not.toBe( payload );
66
+ expect( payload ).toEqual( { foo: 'bar' } );
67
+ } );
68
+
69
+ it( 'does not attach activity context', () => {
70
+ const handler = vi.fn();
71
+ loadMock.mockReturnValue( {
72
+ activityInfo: { activityId: 'activity-id' },
73
+ workflowDetails: { workflowId: 'workflow-id' },
74
+ outputActivityKind: 'step'
75
+ } );
76
+ mainEventBus.on( 'test:event', handler );
77
+
78
+ mainEventBus.emit( 'test:event', { foo: 'bar' } );
79
+
80
+ expect( handler ).toHaveBeenCalledWith( expect.not.objectContaining( {
81
+ activityInfo: expect.anything(),
82
+ workflowDetails: expect.anything(),
83
+ outputActivityKind: expect.anything()
47
84
  } ) );
85
+ expect( loadMock ).not.toHaveBeenCalled();
48
86
  } );
49
87
 
50
- it( 'stamps eventDate on every object payload', () => {
88
+ it( 'filters catalog workflow events', () => {
51
89
  const handler = vi.fn();
52
- const now = new Date( '2026-06-02T12:00:00.000Z' );
53
- vi.useFakeTimers();
54
- vi.setSystemTime( now );
55
- messageBus.on( 'test:event', handler );
90
+ mainEventBus.on( 'workflow:start', handler );
56
91
 
57
- messageBus.emit( 'test:event', { foo: 'bar' } );
92
+ const emitted = mainEventBus.emit( 'workflow:start', {
93
+ workflowDetails: { workflowType: '$catalog' }
94
+ } );
58
95
 
59
- expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
60
- eventDate: now.getTime(),
61
- foo: 'bar'
62
- } ) );
96
+ expect( emitted ).toBe( false );
97
+ expect( handler ).not.toHaveBeenCalled();
98
+ } );
99
+ } );
63
100
 
64
- vi.useRealTimers();
101
+ describe( 'stepEventBus', () => {
102
+ beforeEach( () => {
103
+ loadMock.mockReturnValue( ACTIVITY_CONTEXT );
65
104
  } );
66
105
 
67
- it( 'preserves a caller-supplied eventDate', () => {
106
+ it( 'wraps payloads with event metadata and activity context', () => {
68
107
  const handler = vi.fn();
69
- messageBus.on( 'test:event', handler );
108
+ const payload = { foo: 'bar' };
109
+ stepEventBus.on( 'test:event', handler );
70
110
 
71
- messageBus.emit( 'test:event', { eventDate: 1234, foo: 'bar' } );
111
+ stepEventBus.emit( 'test:event', payload );
72
112
 
73
- expect( handler ).toHaveBeenCalledWith( expect.objectContaining( {
74
- eventDate: 1234,
75
- foo: 'bar'
76
- } ) );
113
+ expect( handler ).toHaveBeenCalledWith( {
114
+ eventId: expect.stringMatching( UUID_V4_REGEX ),
115
+ eventDate: expect.any( Number ),
116
+ ...ACTIVITY_CONTEXT,
117
+ payload
118
+ } );
77
119
  } );
78
120
 
79
- it( 'does not mutate the caller-supplied payload object', () => {
121
+ it( 'omits activity fields outside activity context', () => {
80
122
  const handler = vi.fn();
81
- messageBus.on( 'test:event', handler );
123
+ loadMock.mockReturnValue( undefined );
124
+ stepEventBus.on( 'test:event', handler );
82
125
 
83
- const payload = { foo: 'bar' };
84
- messageBus.emit( 'test:event', payload );
126
+ stepEventBus.emit( 'test:event', { foo: 'bar' } );
85
127
 
86
- expect( payload ).not.toHaveProperty( 'eventId' );
87
- expect( payload ).not.toHaveProperty( 'eventDate' );
128
+ expect( handler ).toHaveBeenCalledWith( {
129
+ eventId: expect.stringMatching( UUID_V4_REGEX ),
130
+ eventDate: expect.any( Number ),
131
+ payload: { foo: 'bar' }
132
+ } );
88
133
  } );
89
- } );
90
134
 
91
- describe( 'pass-through behavior', () => {
92
- it( 'passes primitive payloads through unchanged', () => {
135
+ it( 'preserves arbitrary payloads unchanged inside the envelope', () => {
93
136
  const handler = vi.fn();
94
- messageBus.on( 'test:event', handler );
95
-
96
- messageBus.emit( 'test:event', 'a-string' );
97
- messageBus.emit( 'test:event', 42 );
98
- messageBus.emit( 'test:event', true );
99
-
100
- expect( handler ).toHaveBeenNthCalledWith( 1, 'a-string' );
101
- expect( handler ).toHaveBeenNthCalledWith( 2, 42 );
102
- expect( handler ).toHaveBeenNthCalledWith( 3, true );
137
+ const array = [ 1, 2, 3 ];
138
+ const instance = new Date();
139
+ stepEventBus.on( 'test:event', handler );
140
+
141
+ stepEventBus.emit( 'test:event', 'value' );
142
+ stepEventBus.emit( 'test:event', array );
143
+ stepEventBus.emit( 'test:event', instance );
144
+ stepEventBus.emit( 'test:event' );
145
+
146
+ expect( handler.mock.calls[0][0].payload ).toBe( 'value' );
147
+ expect( handler.mock.calls[1][0].payload ).toBe( array );
148
+ expect( handler.mock.calls[2][0].payload ).toBe( instance );
149
+ expect( handler.mock.calls[3][0] ).toHaveProperty( 'payload', undefined );
103
150
  } );
104
151
 
105
- it( 'passes null and undefined payloads through unchanged', () => {
152
+ it( 'keeps payload metadata separate from envelope metadata', () => {
106
153
  const handler = vi.fn();
107
- messageBus.on( 'test:event', handler );
154
+ const payload = { eventId: 'payload-id', eventDate: 1234 };
155
+ stepEventBus.on( 'test:event', handler );
108
156
 
109
- messageBus.emit( 'test:event', null );
110
- messageBus.emit( 'test:event' );
157
+ stepEventBus.emit( 'test:event', payload );
111
158
 
112
- expect( handler ).toHaveBeenNthCalledWith( 1, null );
113
- expect( handler ).toHaveBeenNthCalledWith( 2 );
159
+ const envelope = handler.mock.calls[0][0];
160
+ expect( envelope.eventId ).toMatch( UUID_V4_REGEX );
161
+ expect( envelope.eventId ).not.toBe( payload.eventId );
162
+ expect( envelope.eventDate ).not.toBe( payload.eventDate );
163
+ expect( envelope.payload ).toBe( payload );
114
164
  } );
115
165
 
116
- it( 'passes array payloads through unchanged (no key injection)', () => {
166
+ it( 'gives distinct emits distinct eventIds', () => {
117
167
  const handler = vi.fn();
118
- messageBus.on( 'test:event', handler );
168
+ stepEventBus.on( 'test:event', handler );
119
169
 
120
- messageBus.emit( 'test:event', [ 1, 2, 3 ] );
170
+ stepEventBus.emit( 'test:event', { i: 1 } );
171
+ stepEventBus.emit( 'test:event', { i: 2 } );
121
172
 
122
- expect( handler ).toHaveBeenCalledWith( [ 1, 2, 3 ] );
173
+ const first = handler.mock.calls[0][0].eventId;
174
+ const second = handler.mock.calls[1][0].eventId;
175
+ expect( first ).toMatch( UUID_V4_REGEX );
176
+ expect( second ).toMatch( UUID_V4_REGEX );
177
+ expect( first ).not.toBe( second );
123
178
  } );
124
179
 
125
- it( 'forwards additional positional args untouched', () => {
180
+ it( 'forwards additional positional arguments unchanged', () => {
126
181
  const handler = vi.fn();
127
- messageBus.on( 'test:event', handler );
182
+ stepEventBus.on( 'test:event', handler );
128
183
 
129
- messageBus.emit( 'test:event', { foo: 'bar' }, 'extra', 99 );
184
+ stepEventBus.emit( 'test:event', { foo: 'bar' }, 'extra', 99 );
130
185
 
131
186
  expect( handler ).toHaveBeenCalledWith(
132
- expect.objectContaining( { foo: 'bar', eventId: expect.stringMatching( UUID_V4_REGEX ) } ),
187
+ expect.objectContaining( {
188
+ eventId: expect.stringMatching( UUID_V4_REGEX ),
189
+ payload: { foo: 'bar' }
190
+ } ),
133
191
  'extra',
134
192
  99
135
193
  );
package/src/consts.js CHANGED
@@ -1,12 +1,11 @@
1
1
  export const ACTIVITY_GET_TRACE_DESTINATIONS = '__internal#getTraceDestinations';
2
2
  export const ACTIVITY_OPTIONS_FILENAME = '__activity_options.js';
3
3
  export const ACTIVITY_SEND_HTTP_REQUEST = '__internal#sendHttpRequest';
4
- export const ACTIVITY_WRAPPER_VERSION_FIELD = '__output_activity_wrapper_version';
5
4
  export const ACTIVITY_LOGGER_SYMBOL = Symbol.for( '__activity_logger' );
6
5
  export const METADATA_ACCESS_SYMBOL = Symbol( '__metadata' );
7
6
  export const WORKFLOW_CATALOG = '$catalog';
7
+ export const WORKFLOW_OPTIONS_FILENAME = '__workflow_options.js';
8
8
  export const WORKFLOWS_INDEX_FILENAME = '__workflows_entrypoint.js';
9
- export const WORKFLOW_WRAPPER_VERSION_FIELD = '__output_workflow_wrapper_version';
10
9
 
11
10
  export const INVOKE_ACTIVITY_SYMBOL = Symbol.for( '@outputai/core:__invoke_activity' );
12
11
 
@@ -42,3 +41,7 @@ export const BusEventType = {
42
41
  export const WorkflowSpecialOutput = {
43
42
  CONTINUED_AS_NEW: '<<continued_as_new>>'
44
43
  };
44
+
45
+ export const ActivitySpecialOutput = {
46
+ ASYNC_HANDOFF: '<<async_handoff>>'
47
+ };
package/src/errors.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Throw this error to end the workflow execution altogether without retries.
5
5
  */
6
- export class FatalError extends Error { }
6
+ export class FatalError extends Error {}
7
7
 
8
8
  /**
9
9
  * Error indicating invalid input or schema validation issues.
@@ -12,4 +12,14 @@ export class FatalError extends Error { }
12
12
  *
13
13
  * It will end the workflow execution without retries.
14
14
  */
15
- export class ValidationError extends Error { }
15
+ export class ValidationError extends FatalError {}
16
+
17
+ /**
18
+ * It serves to indicate a FatalError, but has no properties of its own, except cause.
19
+ *
20
+ * The SDK will discard it and use its cause in its place for logs, traces and the thrown error.
21
+ */
22
+ export class TransparentFatalError extends FatalError {
23
+ readonly cause: Error;
24
+ constructor( cause: Error );
25
+ }
package/src/errors.js CHANGED
@@ -4,15 +4,29 @@
4
4
  */
5
5
 
6
6
  /**
7
- * Any generic fatal errors
7
+ * A FatalError breaks workflow execution
8
8
  */
9
9
  export class FatalError extends Error {
10
10
  name = 'FatalError';
11
11
  }
12
12
 
13
13
  /**
14
- * Any validation error
14
+ * It is a specialization of the FatalError to indicate validation problems
15
15
  */
16
- export class ValidationError extends Error {
16
+ export class ValidationError extends FatalError {
17
17
  name = 'ValidationError';
18
18
  }
19
+
20
+ /**
21
+ * It serves to indicate a FatalError, but has no properties of its own, except cause.
22
+ *
23
+ * The SDK will discard it and use its cause in its place for logs, traces and the thrown error.
24
+ */
25
+ export class TransparentFatalError extends FatalError {
26
+ constructor( cause ) {
27
+ if ( !( cause instanceof Error ) ) {
28
+ throw new FatalError( 'TransparentFatalError argument must be an Error instance' );
29
+ }
30
+ super( undefined, { cause } );
31
+ }
32
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { FatalError, TransparentFatalError, ValidationError } from './errors.js';
3
+
4
+ describe( 'SDK errors', () => {
5
+ it( 'treats validation and transparent errors as fatal errors', () => {
6
+ const cause = new TypeError( 'provider rejected request' );
7
+ const transparentError = new TransparentFatalError( cause );
8
+
9
+ expect( new ValidationError( 'invalid input' ) ).toBeInstanceOf( FatalError );
10
+ expect( transparentError ).toBeInstanceOf( FatalError );
11
+ expect( transparentError.cause ).toBe( cause );
12
+ } );
13
+
14
+ it( 'rejects transparent errors without an Error cause', () => {
15
+ expect( () => new TransparentFatalError( 'provider rejected request' ) ).toThrow(
16
+ new FatalError( 'TransparentFatalError argument must be an Error instance' )
17
+ );
18
+ } );
19
+ } );
@@ -0,0 +1,21 @@
1
+ import { FatalError } from '#errors';
2
+
3
+ /**
4
+ * Returns a copy of activity options with framework-required policies enforced.
5
+ * @param {object} activityOptions
6
+ * @returns {object}
7
+ */
8
+ export const enforceActivityOptions = activityOptions => {
9
+ const options = activityOptions ?? {};
10
+ const retry = options.retry ?? {};
11
+ const nonRetryableErrorTypes = retry.nonRetryableErrorTypes ?? [];
12
+ return {
13
+ ...options,
14
+ retry: {
15
+ ...retry,
16
+ nonRetryableErrorTypes: [
17
+ ...new Set( nonRetryableErrorTypes.concat( FatalError.name ) )
18
+ ]
19
+ }
20
+ };
21
+ };
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { enforceActivityOptions } from './activity_options.js';
3
+
4
+ describe( 'enforceActivityOptions', () => {
5
+ it( 'adds FatalError while preserving existing activity and retry options', () => {
6
+ const activityOptions = {
7
+ startToCloseTimeout: '5m',
8
+ retry: {
9
+ maximumAttempts: 4,
10
+ nonRetryableErrorTypes: [ 'DomainError' ]
11
+ }
12
+ };
13
+
14
+ expect( enforceActivityOptions( activityOptions ) ).toEqual( {
15
+ startToCloseTimeout: '5m',
16
+ retry: {
17
+ maximumAttempts: 4,
18
+ nonRetryableErrorTypes: [ 'DomainError', 'FatalError' ]
19
+ }
20
+ } );
21
+ } );
22
+
23
+ it( 'does not duplicate FatalError', () => {
24
+ const result = enforceActivityOptions( {
25
+ retry: { nonRetryableErrorTypes: [ 'FatalError', 'DomainError', 'FatalError' ] }
26
+ } );
27
+
28
+ expect( result.retry.nonRetryableErrorTypes ).toEqual( [ 'FatalError', 'DomainError' ] );
29
+ } );
30
+
31
+ it( 'returns a new object without mutating the provided options', () => {
32
+ const activityOptions = {
33
+ retry: {
34
+ maximumAttempts: 2,
35
+ nonRetryableErrorTypes: [ 'DomainError' ]
36
+ }
37
+ };
38
+ const result = enforceActivityOptions( activityOptions );
39
+
40
+ expect( result ).not.toBe( activityOptions );
41
+ expect( result.retry ).not.toBe( activityOptions.retry );
42
+ expect( activityOptions ).toEqual( {
43
+ retry: {
44
+ maximumAttempts: 2,
45
+ nonRetryableErrorTypes: [ 'DomainError' ]
46
+ }
47
+ } );
48
+ } );
49
+
50
+ it( 'creates required retry options when options are omitted', () => {
51
+ expect( enforceActivityOptions() ).toEqual( {
52
+ retry: { nonRetryableErrorTypes: [ 'FatalError' ] }
53
+ } );
54
+ } );
55
+ } );