@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.
- package/package.json +8 -7
- package/src/bus.js +58 -20
- package/src/bus.spec.js +135 -77
- package/src/consts.js +5 -2
- package/src/errors.d.ts +12 -2
- package/src/errors.js +17 -3
- package/src/errors.spec.js +19 -0
- package/src/helpers/activity_options.js +21 -0
- package/src/helpers/activity_options.spec.js +55 -0
- package/src/helpers/error_serializer.js +236 -0
- package/src/helpers/error_serializer.spec.js +413 -0
- package/src/helpers/errors.js +23 -14
- package/src/helpers/errors.spec.js +31 -75
- package/src/helpers/fetch.js +1 -38
- package/src/helpers/fetch.spec.js +1 -39
- package/src/helpers/function.js +13 -0
- package/src/helpers/function.spec.js +23 -1
- package/src/helpers/object.js +5 -1
- package/src/helpers/object.spec.js +5 -0
- package/src/helpers/redact.js +51 -0
- package/src/helpers/redact.spec.js +66 -0
- package/src/helpers/string.js +10 -0
- package/src/helpers/string.spec.js +36 -1
- package/src/hooks/index.d.ts +76 -78
- package/src/hooks/index.js +57 -49
- package/src/hooks/index.spec.js +104 -105
- package/src/hooks/pending_hooks.js +24 -0
- package/src/hooks/pending_hooks.spec.js +62 -0
- package/src/interface/evaluator.d.ts +10 -9
- package/src/interface/evaluator.js +1 -6
- package/src/interface/evaluator.spec.js +10 -10
- package/src/interface/index.js +2 -1
- package/src/interface/step.d.ts +16 -15
- package/src/interface/step.js +1 -6
- package/src/interface/step.spec.js +10 -10
- package/src/interface/validations/index.js +33 -53
- package/src/interface/validations/index.spec.js +25 -17
- package/src/interface/webhook.js +1 -2
- package/src/interface/webhook.spec.js +25 -19
- package/src/interface/workflow.d.ts +25 -18
- package/src/interface/workflow.js +24 -52
- package/src/interface/workflow.spec.js +109 -108
- package/src/interface/workflow_activity_options.js +2 -5
- package/src/interface/workflow_utils.d.ts +15 -0
- package/src/interface/workflow_utils.js +19 -0
- package/src/interface/workflow_utils.spec.js +82 -1
- package/src/sdk/runtime/events.d.ts +4 -6
- package/src/sdk/runtime/events.js +2 -15
- package/src/sdk/runtime/events.spec.js +14 -92
- package/src/tracing/trace_engine.js +9 -11
- package/src/tracing/trace_engine.spec.js +67 -28
- package/src/worker/catalog_workflow/catalog_job.js +3 -3
- package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
- package/src/worker/configs.js +0 -3
- package/src/worker/global_functions.js +2 -2
- package/src/worker/global_functions.spec.js +3 -3
- package/src/worker/index.js +20 -11
- package/src/worker/index.spec.js +29 -16
- package/src/worker/interceptors/activity.js +34 -21
- package/src/worker/interceptors/activity.spec.js +77 -172
- package/src/worker/interceptors/workflow.js +53 -11
- package/src/worker/interceptors/workflow.spec.js +176 -49
- package/src/worker/loader/workflows.js +16 -3
- package/src/worker/loader/workflows.spec.js +37 -5
- package/src/worker/log_hooks.js +22 -19
- package/src/worker/log_hooks.spec.js +14 -6
- package/src/worker/sinks.js +8 -8
- package/src/worker/sinks.spec.js +9 -9
- package/src/worker/telemetry.js +2 -1
- package/src/worker/temporal_logger.js +33 -0
- package/src/worker/temporal_logger.spec.js +90 -0
- package/src/helpers/aggregations.js +0 -44
- package/src/helpers/aggregations.spec.js +0 -92
- package/src/tracing/tools/utils.js +0 -42
- package/src/tracing/tools/utils.spec.js +0 -83
- package/src/worker/proxy.js +0 -20
- 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-
|
|
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.
|
|
45
|
-
"@temporalio/client": "1.
|
|
46
|
-
"@temporalio/common": "1.
|
|
47
|
-
"@temporalio/worker": "1.
|
|
48
|
-
"@temporalio/workflow": "1.
|
|
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.
|
|
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 {
|
|
3
|
+
import { Storage } from '#async_storage';
|
|
4
|
+
import { WORKFLOW_CATALOG } from '#consts';
|
|
4
5
|
|
|
5
|
-
const
|
|
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
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
68
|
+
export { mainEventBus, stepEventBus };
|
package/src/bus.spec.js
CHANGED
|
@@ -1,135 +1,193 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
|
|
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( '
|
|
18
|
+
describe( 'event buses', () => {
|
|
7
19
|
beforeEach( () => {
|
|
8
|
-
|
|
20
|
+
mainEventBus.removeAllListeners();
|
|
21
|
+
stepEventBus.removeAllListeners();
|
|
22
|
+
loadMock.mockReset();
|
|
9
23
|
} );
|
|
10
24
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
+
mainEventBus.emit( 'test:event', { foo: 'bar' } );
|
|
17
38
|
|
|
18
|
-
expect( handler ).toHaveBeenCalledWith(
|
|
19
|
-
|
|
20
|
-
|
|
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( '
|
|
46
|
+
it( 'preserves caller-supplied event metadata', () => {
|
|
25
47
|
const handler = vi.fn();
|
|
26
|
-
|
|
48
|
+
mainEventBus.on( 'test:event', handler );
|
|
27
49
|
|
|
28
|
-
|
|
29
|
-
messageBus.emit( 'test:event', { i: 2 } );
|
|
50
|
+
mainEventBus.emit( 'test:event', { eventId: 'fixed-id', eventDate: 1234 } );
|
|
30
51
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
expect( first ).not.toBe( second );
|
|
52
|
+
expect( handler ).toHaveBeenCalledWith( {
|
|
53
|
+
eventId: 'fixed-id',
|
|
54
|
+
eventDate: 1234
|
|
55
|
+
} );
|
|
36
56
|
} );
|
|
37
57
|
|
|
38
|
-
it( '
|
|
58
|
+
it( 'does not mutate the caller-supplied payload', () => {
|
|
39
59
|
const handler = vi.fn();
|
|
40
|
-
|
|
60
|
+
const payload = { foo: 'bar' };
|
|
61
|
+
mainEventBus.on( 'test:event', handler );
|
|
41
62
|
|
|
42
|
-
|
|
63
|
+
mainEventBus.emit( 'test:event', payload );
|
|
43
64
|
|
|
44
|
-
expect( handler ).
|
|
45
|
-
|
|
46
|
-
|
|
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( '
|
|
88
|
+
it( 'filters catalog workflow events', () => {
|
|
51
89
|
const handler = vi.fn();
|
|
52
|
-
|
|
53
|
-
vi.useFakeTimers();
|
|
54
|
-
vi.setSystemTime( now );
|
|
55
|
-
messageBus.on( 'test:event', handler );
|
|
90
|
+
mainEventBus.on( 'workflow:start', handler );
|
|
56
91
|
|
|
57
|
-
|
|
92
|
+
const emitted = mainEventBus.emit( 'workflow:start', {
|
|
93
|
+
workflowDetails: { workflowType: '$catalog' }
|
|
94
|
+
} );
|
|
58
95
|
|
|
59
|
-
expect(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
96
|
+
expect( emitted ).toBe( false );
|
|
97
|
+
expect( handler ).not.toHaveBeenCalled();
|
|
98
|
+
} );
|
|
99
|
+
} );
|
|
63
100
|
|
|
64
|
-
|
|
101
|
+
describe( 'stepEventBus', () => {
|
|
102
|
+
beforeEach( () => {
|
|
103
|
+
loadMock.mockReturnValue( ACTIVITY_CONTEXT );
|
|
65
104
|
} );
|
|
66
105
|
|
|
67
|
-
it( '
|
|
106
|
+
it( 'wraps payloads with event metadata and activity context', () => {
|
|
68
107
|
const handler = vi.fn();
|
|
69
|
-
|
|
108
|
+
const payload = { foo: 'bar' };
|
|
109
|
+
stepEventBus.on( 'test:event', handler );
|
|
70
110
|
|
|
71
|
-
|
|
111
|
+
stepEventBus.emit( 'test:event', payload );
|
|
72
112
|
|
|
73
|
-
expect( handler ).toHaveBeenCalledWith(
|
|
74
|
-
|
|
75
|
-
|
|
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( '
|
|
121
|
+
it( 'omits activity fields outside activity context', () => {
|
|
80
122
|
const handler = vi.fn();
|
|
81
|
-
|
|
123
|
+
loadMock.mockReturnValue( undefined );
|
|
124
|
+
stepEventBus.on( 'test:event', handler );
|
|
82
125
|
|
|
83
|
-
|
|
84
|
-
messageBus.emit( 'test:event', payload );
|
|
126
|
+
stepEventBus.emit( 'test:event', { foo: 'bar' } );
|
|
85
127
|
|
|
86
|
-
expect(
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
it( 'passes primitive payloads through unchanged', () => {
|
|
135
|
+
it( 'preserves arbitrary payloads unchanged inside the envelope', () => {
|
|
93
136
|
const handler = vi.fn();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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( '
|
|
152
|
+
it( 'keeps payload metadata separate from envelope metadata', () => {
|
|
106
153
|
const handler = vi.fn();
|
|
107
|
-
|
|
154
|
+
const payload = { eventId: 'payload-id', eventDate: 1234 };
|
|
155
|
+
stepEventBus.on( 'test:event', handler );
|
|
108
156
|
|
|
109
|
-
|
|
110
|
-
messageBus.emit( 'test:event' );
|
|
157
|
+
stepEventBus.emit( 'test:event', payload );
|
|
111
158
|
|
|
112
|
-
|
|
113
|
-
expect(
|
|
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( '
|
|
166
|
+
it( 'gives distinct emits distinct eventIds', () => {
|
|
117
167
|
const handler = vi.fn();
|
|
118
|
-
|
|
168
|
+
stepEventBus.on( 'test:event', handler );
|
|
119
169
|
|
|
120
|
-
|
|
170
|
+
stepEventBus.emit( 'test:event', { i: 1 } );
|
|
171
|
+
stepEventBus.emit( 'test:event', { i: 2 } );
|
|
121
172
|
|
|
122
|
-
|
|
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
|
|
180
|
+
it( 'forwards additional positional arguments unchanged', () => {
|
|
126
181
|
const handler = vi.fn();
|
|
127
|
-
|
|
182
|
+
stepEventBus.on( 'test:event', handler );
|
|
128
183
|
|
|
129
|
-
|
|
184
|
+
stepEventBus.emit( 'test:event', { foo: 'bar' }, 'extra', 99 );
|
|
130
185
|
|
|
131
186
|
expect( handler ).toHaveBeenCalledWith(
|
|
132
|
-
expect.objectContaining( {
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
14
|
+
* It is a specialization of the FatalError to indicate validation problems
|
|
15
15
|
*/
|
|
16
|
-
export class ValidationError extends
|
|
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
|
+
} );
|