@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.
- package/package.json +13 -7
- package/src/bus.js +58 -20
- package/src/bus.spec.js +135 -77
- package/src/consts.js +7 -3
- 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 +48 -35
- package/src/helpers/object.spec.js +123 -75
- 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 +64 -85
- package/src/interface/workflow.spec.js +220 -226
- 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/helpers/objects.d.ts +24 -19
- 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/temporal/client.js +47 -0
- package/src/temporal/client.spec.js +124 -0
- package/src/temporal/index.d.ts +32 -0
- package/src/temporal/index.js +1 -0
- 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 +24 -11
- package/src/worker/index.spec.js +36 -16
- package/src/worker/interceptors/activity.js +34 -21
- package/src/worker/interceptors/activity.spec.js +77 -172
- package/src/worker/interceptors/workflow.js +58 -16
- package/src/worker/interceptors/workflow.spec.js +183 -56
- package/src/worker/loader/activities.js +56 -40
- package/src/worker/loader/activities.spec.js +21 -22
- package/src/worker/loader/workflows.js +16 -3
- package/src/worker/loader/workflows.spec.js +36 -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/worker/webpack_loaders/consts.js +6 -0
- package/src/worker/webpack_loaders/tools.js +1 -146
- package/src/worker/webpack_loaders/tools.spec.js +0 -23
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
- 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/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
- 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 {
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
60
|
-
vi.mock( '../temp/__activity_options.js', () => ( { default:
|
|
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
|
|
95
|
-
|
|
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
|
|
114
|
+
memo: { traceInfo: workflowInfo.memo.traceInfo }
|
|
99
115
|
} );
|
|
100
116
|
memoToHeadersMock.mockReturnValue( {} );
|
|
101
|
-
deepMergeMock.mockReturnValue( {
|
|
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( {
|
|
113
|
-
|
|
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( '
|
|
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 ).
|
|
231
|
+
expect( workflowErrorMock ).not.toHaveBeenCalled();
|
|
145
232
|
expect( workflowEndMock ).not.toHaveBeenCalled();
|
|
146
233
|
} );
|
|
147
234
|
|
|
148
|
-
it(
|
|
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
|
|
153
|
-
|
|
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
|
|
163
|
-
|
|
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(
|
|
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( '
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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: '
|
|
192
|
-
type: '
|
|
284
|
+
message: 'provider rejected request',
|
|
285
|
+
type: 'TypeError',
|
|
193
286
|
nonRetryable: true,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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(
|
|
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(
|
|
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( '
|
|
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,
|
|
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
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
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,
|
|
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
|
|
24
|
+
const activities = new Map();
|
|
25
|
+
const activityOptions = new Map();
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
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
|
|
73
|
-
activities
|
|
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, '/
|
|
147
|
-
expect( matchFilesMock ).toHaveBeenNthCalledWith( 2, '/
|
|
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,
|
|
151
|
-
expect( importComponentsMock ).toHaveBeenNthCalledWith( 2,
|
|
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(
|
|
174
|
-
expect( activities['
|
|
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['
|
|
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: '
|
|
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: '
|
|
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['
|
|
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['
|
|
209
|
-
expect( activities['
|
|
210
|
-
expect( activities['
|
|
211
|
-
expect( activities['
|
|
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
|
} );
|