@outputai/core 0.10.1-next.fc0a41f.0 → 0.11.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 +12 -7
- 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 +55 -13
- package/src/helpers/errors.spec.js +166 -66
- 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/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 +35 -44
- package/src/hooks/index.js +2 -1
- package/src/hooks/index.spec.js +15 -7
- 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/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/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/index.js +10 -1
- package/src/worker/index.spec.js +23 -8
- package/src/worker/interceptors/activity.js +32 -19
- package/src/worker/interceptors/activity.spec.js +71 -166
- 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 +13 -10
- package/src/worker/log_hooks.spec.js +12 -4
- package/src/worker/sinks.js +3 -1
- package/src/worker/sinks.spec.js +21 -4
- 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
|
@@ -2,10 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import {
|
|
4
4
|
ACTIVITY_GET_TRACE_DESTINATIONS,
|
|
5
|
-
|
|
6
|
-
INVOKE_ACTIVITY_SYMBOL,
|
|
7
|
-
METADATA_ACCESS_SYMBOL,
|
|
8
|
-
WORKFLOW_WRAPPER_VERSION_FIELD
|
|
5
|
+
INVOKE_ACTIVITY_SYMBOL
|
|
9
6
|
} from '#consts';
|
|
10
7
|
import { ValidationError } from '#errors';
|
|
11
8
|
|
|
@@ -13,11 +10,12 @@ const inWorkflowContextMock = vi.hoisted( () => vi.fn() );
|
|
|
13
10
|
const proxyActivitiesMock = vi.hoisted( () => vi.fn() );
|
|
14
11
|
const executeChildMock = vi.hoisted( () => vi.fn() );
|
|
15
12
|
const workflowInfoMock = vi.hoisted( () => vi.fn() );
|
|
13
|
+
const upsertMemoMock = vi.hoisted( () => vi.fn() );
|
|
16
14
|
const continueAsNewMock = vi.hoisted( () => vi.fn() );
|
|
17
15
|
const validateDefinitionMock = vi.hoisted( () => vi.fn() );
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
16
|
+
const parseInputMock = vi.hoisted( () => vi.fn( input => input ) );
|
|
17
|
+
const parseOutputMock = vi.hoisted( () => vi.fn( output => output ) );
|
|
18
|
+
const parseInvocationOptionsMock = vi.hoisted( () => vi.fn( options => options ) );
|
|
21
19
|
const validatorConstructorMock = vi.hoisted( () => vi.fn() );
|
|
22
20
|
const createWorkflowMock = vi.hoisted( () => vi.fn( ( { handler } ) => handler ) );
|
|
23
21
|
|
|
@@ -29,9 +27,9 @@ vi.mock( './validations/index.js', () => {
|
|
|
29
27
|
|
|
30
28
|
constructor( ...args ) {
|
|
31
29
|
validatorConstructorMock( ...args );
|
|
32
|
-
this.
|
|
33
|
-
this.
|
|
34
|
-
this.
|
|
30
|
+
this.parseInput = parseInputMock;
|
|
31
|
+
this.parseOutput = parseOutputMock;
|
|
32
|
+
this.parseInvocationOptions = parseInvocationOptionsMock;
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
|
|
@@ -50,6 +48,7 @@ vi.mock( '@temporalio/workflow', async importOriginal => {
|
|
|
50
48
|
proxyActivities: proxyActivitiesMock,
|
|
51
49
|
executeChild: executeChildMock,
|
|
52
50
|
workflowInfo: workflowInfoMock,
|
|
51
|
+
upsertMemo: upsertMemoMock,
|
|
53
52
|
continueAsNew: continueAsNewMock,
|
|
54
53
|
uuid4: () => '550e8400e29b41d4a716446655440000'
|
|
55
54
|
};
|
|
@@ -60,28 +59,24 @@ const baseWorkflowInfo = () => ( {
|
|
|
60
59
|
workflowType: 'test_wf',
|
|
61
60
|
runId: 'run-123',
|
|
62
61
|
startTime: new Date( '2025-01-01T00:00:00.000Z' ),
|
|
63
|
-
memo: {},
|
|
62
|
+
memo: { traceInfo: { workflowId: 'workflow-123' } },
|
|
64
63
|
continueAsNewSuggested: false
|
|
65
64
|
} );
|
|
66
65
|
|
|
67
66
|
const setWorkflowInfo = overrides => {
|
|
67
|
+
const base = baseWorkflowInfo();
|
|
68
68
|
const info = {
|
|
69
|
-
...
|
|
69
|
+
...base,
|
|
70
70
|
...overrides,
|
|
71
|
-
memo: overrides?.memo ??
|
|
71
|
+
memo: overrides?.memo ?? base.memo
|
|
72
72
|
};
|
|
73
73
|
workflowInfoMock.mockImplementation( () => info );
|
|
74
74
|
return info;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
const activityOutput = output => ( {
|
|
78
|
-
output,
|
|
79
|
-
[ACTIVITY_WRAPPER_VERSION_FIELD]: 1
|
|
80
|
-
} );
|
|
81
|
-
|
|
82
77
|
const createActivities = handlers => new Proxy( {}, {
|
|
83
78
|
get: ( _, prop ) => typeof prop === 'string' ?
|
|
84
|
-
handlers[prop] ?? vi.fn().mockResolvedValue(
|
|
79
|
+
handlers[prop] ?? vi.fn().mockResolvedValue( undefined ) :
|
|
85
80
|
undefined
|
|
86
81
|
} );
|
|
87
82
|
|
|
@@ -112,10 +107,10 @@ describe( 'workflow()', () => {
|
|
|
112
107
|
vi.clearAllMocks();
|
|
113
108
|
delete globalThis[INVOKE_ACTIVITY_SYMBOL];
|
|
114
109
|
inWorkflowContextMock.mockReturnValue( true );
|
|
115
|
-
executeChildMock.mockResolvedValue( {
|
|
110
|
+
executeChildMock.mockResolvedValue( {} );
|
|
116
111
|
setWorkflowInfo();
|
|
117
112
|
mockActivities( {
|
|
118
|
-
[ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue(
|
|
113
|
+
[ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( { local: '/tmp/trace' } )
|
|
119
114
|
} );
|
|
120
115
|
} );
|
|
121
116
|
|
|
@@ -210,9 +205,9 @@ describe( 'workflow()', () => {
|
|
|
210
205
|
value: 'test-workflow-ok',
|
|
211
206
|
extra: 'custom'
|
|
212
207
|
} );
|
|
213
|
-
expect(
|
|
214
|
-
expect(
|
|
215
|
-
expect(
|
|
208
|
+
expect( parseInvocationOptionsMock ).toHaveBeenCalledWith( { context: { extra: 'custom' } } );
|
|
209
|
+
expect( parseInputMock ).toHaveBeenCalledWith( { suffix: '-ok' } );
|
|
210
|
+
expect( parseOutputMock ).toHaveBeenCalledWith( { value: 'test-workflow-ok', extra: 'custom' } );
|
|
216
211
|
expect( workflowInfoMock ).not.toHaveBeenCalled();
|
|
217
212
|
expect( proxyActivitiesMock ).not.toHaveBeenCalled();
|
|
218
213
|
} );
|
|
@@ -220,7 +215,7 @@ describe( 'workflow()', () => {
|
|
|
220
215
|
it( 'does not run fn when plain function input validation fails', async () => {
|
|
221
216
|
const { workflow } = await import( './workflow.js' );
|
|
222
217
|
const error = new ValidationError( 'invalid input' );
|
|
223
|
-
|
|
218
|
+
parseInputMock.mockImplementationOnce( () => {
|
|
224
219
|
throw error;
|
|
225
220
|
} );
|
|
226
221
|
const fn = vi.fn();
|
|
@@ -234,13 +229,13 @@ describe( 'workflow()', () => {
|
|
|
234
229
|
|
|
235
230
|
await expect( wf( { value: 1 } ) ).rejects.toBe( error );
|
|
236
231
|
expect( fn ).not.toHaveBeenCalled();
|
|
237
|
-
expect(
|
|
232
|
+
expect( parseOutputMock ).not.toHaveBeenCalled();
|
|
238
233
|
} );
|
|
239
234
|
|
|
240
235
|
it( 'propagates plain function output validation errors after fn runs', async () => {
|
|
241
236
|
const { workflow } = await import( './workflow.js' );
|
|
242
237
|
const error = new ValidationError( 'invalid output' );
|
|
243
|
-
|
|
238
|
+
parseOutputMock.mockImplementationOnce( () => {
|
|
244
239
|
throw error;
|
|
245
240
|
} );
|
|
246
241
|
const output = { result: 1 };
|
|
@@ -257,7 +252,7 @@ describe( 'workflow()', () => {
|
|
|
257
252
|
expect( fn ).toHaveBeenCalledWith( { value: 'ok' }, expect.objectContaining( {
|
|
258
253
|
info: { workflowId: 'test-workflow', runId: 'test-run' }
|
|
259
254
|
} ) );
|
|
260
|
-
expect(
|
|
255
|
+
expect( parseOutputMock ).toHaveBeenCalledWith( output );
|
|
261
256
|
} );
|
|
262
257
|
} );
|
|
263
258
|
|
|
@@ -268,7 +263,7 @@ describe( 'workflow()', () => {
|
|
|
268
263
|
installGlobalDispatcher( 'run-123' );
|
|
269
264
|
const memo = {
|
|
270
265
|
traceInfo: { workflowId: 'root-workflow' },
|
|
271
|
-
|
|
266
|
+
activityOptions: {
|
|
272
267
|
heartbeatTimeout: '30s',
|
|
273
268
|
retry: {
|
|
274
269
|
maximumInterval: '20s',
|
|
@@ -277,7 +272,7 @@ describe( 'workflow()', () => {
|
|
|
277
272
|
}
|
|
278
273
|
};
|
|
279
274
|
setWorkflowInfo( { memo } );
|
|
280
|
-
executeChildMock.mockResolvedValueOnce( {
|
|
275
|
+
executeChildMock.mockResolvedValueOnce( { child: 'ok' } );
|
|
281
276
|
|
|
282
277
|
const wf = workflow( workflowDefinition( {
|
|
283
278
|
name: 'child_target_wf',
|
|
@@ -294,7 +289,7 @@ describe( 'workflow()', () => {
|
|
|
294
289
|
retry: { maximumAttempts: 7 }
|
|
295
290
|
}
|
|
296
291
|
} ) ).resolves.toEqual( { child: 'ok' } );
|
|
297
|
-
expect(
|
|
292
|
+
expect( parseInvocationOptionsMock ).toHaveBeenCalledWith( {
|
|
298
293
|
detached: true,
|
|
299
294
|
context: { testOnly: true },
|
|
300
295
|
activityOptions: {
|
|
@@ -326,7 +321,7 @@ describe( 'workflow()', () => {
|
|
|
326
321
|
const { ParentClosePolicy } = await import( '@temporalio/workflow' );
|
|
327
322
|
installGlobalDispatcher( 'run-123' );
|
|
328
323
|
setWorkflowInfo( { memo: { traceInfo: { workflowId: 'root-workflow' } } } );
|
|
329
|
-
executeChildMock.mockResolvedValueOnce(
|
|
324
|
+
executeChildMock.mockResolvedValueOnce( 'done' );
|
|
330
325
|
|
|
331
326
|
const wf = workflow( workflowDefinition( {
|
|
332
327
|
name: 'no_input_child_wf',
|
|
@@ -356,6 +351,20 @@ describe( 'workflow()', () => {
|
|
|
356
351
|
} );
|
|
357
352
|
|
|
358
353
|
describe( 'workflow execution path', () => {
|
|
354
|
+
it( 'validates invocation options before reading workflow information', async () => {
|
|
355
|
+
const { workflow } = await import( './workflow.js' );
|
|
356
|
+
const error = new ValidationError( 'invalid invocation options' );
|
|
357
|
+
parseInvocationOptionsMock.mockImplementationOnce( () => {
|
|
358
|
+
throw error;
|
|
359
|
+
} );
|
|
360
|
+
const wf = workflow( workflowDefinition() );
|
|
361
|
+
|
|
362
|
+
await expect( wf( {}, { invalid: true } ) ).rejects.toBe( error );
|
|
363
|
+
|
|
364
|
+
expect( upsertMemoMock ).not.toHaveBeenCalled();
|
|
365
|
+
expect( workflowInfoMock ).not.toHaveBeenCalled();
|
|
366
|
+
} );
|
|
367
|
+
|
|
359
368
|
it( 'rejects a global activity dispatcher left by another workflow run', async () => {
|
|
360
369
|
const { workflow } = await import( './workflow.js' );
|
|
361
370
|
const fn = vi.fn().mockResolvedValue( {} );
|
|
@@ -373,9 +382,9 @@ describe( 'workflow()', () => {
|
|
|
373
382
|
expect( fn ).not.toHaveBeenCalled();
|
|
374
383
|
} );
|
|
375
384
|
|
|
376
|
-
it( '
|
|
385
|
+
it( 'skips trace destinations when trace is disabled and returns raw output', async () => {
|
|
377
386
|
const { workflow } = await import( './workflow.js' );
|
|
378
|
-
const getTraceDestinations = vi.fn().mockResolvedValue(
|
|
387
|
+
const getTraceDestinations = vi.fn().mockResolvedValue( { local: '/tmp/root-trace' } );
|
|
379
388
|
const info = setWorkflowInfo( { workflowType: 'root_wf', memo: {} } );
|
|
380
389
|
mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: getTraceDestinations } );
|
|
381
390
|
|
|
@@ -392,32 +401,28 @@ describe( 'workflow()', () => {
|
|
|
392
401
|
fn: async ( _, context ) => ( { ok: context.info.workflowId === 'workflow-123' } )
|
|
393
402
|
} ) );
|
|
394
403
|
|
|
395
|
-
await expect( wf( {} ) ).resolves.toEqual( {
|
|
396
|
-
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
397
|
-
output: { ok: true },
|
|
398
|
-
trace: { destinations: {} }
|
|
399
|
-
} );
|
|
404
|
+
await expect( wf( {} ) ).resolves.toEqual( { ok: true } );
|
|
400
405
|
expect( info.memo.traceInfo ).toBeUndefined();
|
|
401
406
|
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
407
|
expect( proxyActivitiesMock ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
408
408
|
startToCloseTimeout: '5m',
|
|
409
409
|
heartbeatTimeout: '5m',
|
|
410
410
|
retry: expect.objectContaining( { maximumAttempts: 5 } )
|
|
411
411
|
} ) );
|
|
412
412
|
expect( getTraceDestinations ).not.toHaveBeenCalled();
|
|
413
|
+
expect( upsertMemoMock ).toHaveBeenCalledOnce();
|
|
414
|
+
expect( upsertMemoMock ).toHaveBeenCalledWith( {
|
|
415
|
+
activityOptions: proxyActivitiesMock.mock.calls[0][0]
|
|
416
|
+
} );
|
|
413
417
|
} );
|
|
414
418
|
|
|
415
419
|
it( 'resolves activity options by invocation, definition, inherited memo, then default precedence', async () => {
|
|
416
420
|
const { workflow } = await import( './workflow.js' );
|
|
417
|
-
|
|
421
|
+
setWorkflowInfo( {
|
|
418
422
|
workflowType: 'activity_options_wf',
|
|
419
423
|
memo: {
|
|
420
|
-
|
|
424
|
+
traceInfo: { workflowId: 'workflow-123' },
|
|
425
|
+
activityOptions: {
|
|
421
426
|
heartbeatTimeout: '30s',
|
|
422
427
|
retry: {
|
|
423
428
|
maximumInterval: '30s',
|
|
@@ -458,18 +463,22 @@ describe( 'workflow()', () => {
|
|
|
458
463
|
backoffCoefficient: 3,
|
|
459
464
|
maximumInterval: '30s',
|
|
460
465
|
maximumAttempts: 9,
|
|
461
|
-
nonRetryableErrorTypes: [
|
|
466
|
+
nonRetryableErrorTypes: [ 'FatalError' ]
|
|
462
467
|
}
|
|
463
468
|
} );
|
|
464
|
-
expect(
|
|
469
|
+
expect( upsertMemoMock ).toHaveBeenCalledOnce();
|
|
470
|
+
expect( upsertMemoMock ).toHaveBeenCalledWith( {
|
|
471
|
+
activityOptions: proxyActivitiesMock.mock.calls[0][0],
|
|
472
|
+
trace: { local: '/tmp/trace' }
|
|
473
|
+
} );
|
|
465
474
|
} );
|
|
466
475
|
|
|
467
476
|
it( 'runs non-root workflow execution without rebuilding trace info or fetching trace destinations', async () => {
|
|
468
477
|
const { workflow } = await import( './workflow.js' );
|
|
469
|
-
const getTraceDestinations = vi.fn().mockResolvedValue(
|
|
478
|
+
const getTraceDestinations = vi.fn().mockResolvedValue( { local: '/tmp/trace' } );
|
|
470
479
|
const memo = {
|
|
471
480
|
traceInfo: { workflowId: 'root-workflow' },
|
|
472
|
-
|
|
481
|
+
activityOptions: {
|
|
473
482
|
startToCloseTimeout: '9m',
|
|
474
483
|
retry: { maximumAttempts: 8 }
|
|
475
484
|
}
|
|
@@ -493,29 +502,27 @@ describe( 'workflow()', () => {
|
|
|
493
502
|
fn: async () => ( { ok: true } )
|
|
494
503
|
} ) );
|
|
495
504
|
|
|
496
|
-
await expect( wf( {} ) ).resolves.toEqual( {
|
|
497
|
-
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
498
|
-
output: { ok: true }
|
|
499
|
-
} );
|
|
505
|
+
await expect( wf( {} ) ).resolves.toEqual( { ok: true } );
|
|
500
506
|
expect( info.memo.traceInfo ).toBe( memo.traceInfo );
|
|
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
507
|
expect( proxyActivitiesMock ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
508
508
|
startToCloseTimeout: '1m',
|
|
509
509
|
heartbeatTimeout: '5m',
|
|
510
510
|
retry: expect.objectContaining( { maximumAttempts: 2 } )
|
|
511
511
|
} ) );
|
|
512
512
|
expect( getTraceDestinations ).not.toHaveBeenCalled();
|
|
513
|
+
expect( upsertMemoMock ).toHaveBeenCalledOnce();
|
|
514
|
+
expect( upsertMemoMock ).toHaveBeenCalledWith( {
|
|
515
|
+
activityOptions: proxyActivitiesMock.mock.calls[0][0]
|
|
516
|
+
} );
|
|
513
517
|
} );
|
|
514
518
|
|
|
515
|
-
it( '
|
|
519
|
+
it( 'stores empty trace destinations in memo and returns raw output', async () => {
|
|
516
520
|
const { workflow } = await import( './workflow.js' );
|
|
517
|
-
setWorkflowInfo( {
|
|
518
|
-
|
|
521
|
+
setWorkflowInfo( {
|
|
522
|
+
workflowType: 'no_trace_dest_wf',
|
|
523
|
+
memo: { traceInfo: { workflowId: 'workflow-123' } }
|
|
524
|
+
} );
|
|
525
|
+
mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( {} ) } );
|
|
519
526
|
|
|
520
527
|
const wf = workflow( workflowDefinition( {
|
|
521
528
|
name: 'no_trace_dest_wf',
|
|
@@ -523,10 +530,11 @@ describe( 'workflow()', () => {
|
|
|
523
530
|
fn: async () => ( { ok: true } )
|
|
524
531
|
} ) );
|
|
525
532
|
|
|
526
|
-
await expect( wf( {} ) ).resolves.toEqual( {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
533
|
+
await expect( wf( {} ) ).resolves.toEqual( { ok: true } );
|
|
534
|
+
expect( upsertMemoMock ).toHaveBeenCalledOnce();
|
|
535
|
+
expect( upsertMemoMock ).toHaveBeenCalledWith( {
|
|
536
|
+
activityOptions: proxyActivitiesMock.mock.calls[0][0],
|
|
537
|
+
trace: {}
|
|
530
538
|
} );
|
|
531
539
|
} );
|
|
532
540
|
|
|
@@ -543,30 +551,38 @@ describe( 'workflow()', () => {
|
|
|
543
551
|
fn: async () => ( { result: 1 } )
|
|
544
552
|
} ) );
|
|
545
553
|
|
|
546
|
-
|
|
554
|
+
parseInputMock.mockImplementationOnce( () => {
|
|
547
555
|
throw inputError;
|
|
548
556
|
} );
|
|
549
557
|
await expect( wf( { value: 1 } ) ).rejects.toBe( inputError );
|
|
550
|
-
expect(
|
|
558
|
+
expect( upsertMemoMock ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
559
|
+
trace: { local: '/tmp/trace' }
|
|
560
|
+
} ) );
|
|
551
561
|
|
|
552
562
|
delete globalThis[INVOKE_ACTIVITY_SYMBOL];
|
|
553
|
-
setWorkflowInfo( {
|
|
554
|
-
|
|
563
|
+
setWorkflowInfo( {
|
|
564
|
+
workflowType: 'runtime_validation_wf',
|
|
565
|
+
memo: { traceInfo: { workflowId: 'workflow-123' } }
|
|
566
|
+
} );
|
|
567
|
+
upsertMemoMock.mockClear();
|
|
568
|
+
parseOutputMock.mockImplementationOnce( () => {
|
|
555
569
|
throw outputError;
|
|
556
570
|
} );
|
|
557
571
|
await expect( wf( { value: 'ok' } ) ).rejects.toBe( outputError );
|
|
558
|
-
expect(
|
|
572
|
+
expect( upsertMemoMock ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
573
|
+
trace: { local: '/tmp/trace' }
|
|
574
|
+
} ) );
|
|
559
575
|
} );
|
|
560
576
|
} );
|
|
561
577
|
|
|
562
578
|
describe( 'activity dispatchers', () => {
|
|
563
|
-
it( 'invokes workflow-scoped activities and
|
|
579
|
+
it( 'invokes workflow-scoped activities and returns their raw outputs', async () => {
|
|
564
580
|
const { workflow } = await import( './workflow.js' );
|
|
565
581
|
setWorkflowInfo( { workflowType: 'dispatch_wf' } );
|
|
566
|
-
const step = vi.fn().mockResolvedValue(
|
|
567
|
-
const evaluator = vi.fn().mockResolvedValue(
|
|
582
|
+
const step = vi.fn().mockResolvedValue( 'step-output' );
|
|
583
|
+
const evaluator = vi.fn().mockResolvedValue( 'eval-output' );
|
|
568
584
|
mockActivities( {
|
|
569
|
-
[ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue(
|
|
585
|
+
[ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( {} ),
|
|
570
586
|
'dispatch_wf#stepA': step,
|
|
571
587
|
'dispatch_wf#evalA': evaluator
|
|
572
588
|
} );
|
|
@@ -582,11 +598,7 @@ describe( 'workflow()', () => {
|
|
|
582
598
|
}
|
|
583
599
|
} ) );
|
|
584
600
|
|
|
585
|
-
await expect( wf( {} ) ).resolves.toEqual( {
|
|
586
|
-
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
587
|
-
output: { stepResult: 'step-output', evalResult: 'eval-output' },
|
|
588
|
-
trace: { destinations: {} }
|
|
589
|
-
} );
|
|
601
|
+
await expect( wf( {} ) ).resolves.toEqual( { stepResult: 'step-output', evalResult: 'eval-output' } );
|
|
590
602
|
expect( step ).toHaveBeenCalledWith( { a: 1 }, { b: 2 } );
|
|
591
603
|
expect( evaluator ).toHaveBeenCalledWith( { c: 3 } );
|
|
592
604
|
} );
|
|
@@ -594,10 +606,10 @@ describe( 'workflow()', () => {
|
|
|
594
606
|
it( 'invokes shared activities through the workflow namespace', async () => {
|
|
595
607
|
const { workflow } = await import( './workflow.js' );
|
|
596
608
|
setWorkflowInfo( { workflowType: 'shared_dispatch_wf' } );
|
|
597
|
-
const sharedStep = vi.fn().mockResolvedValue(
|
|
598
|
-
const sharedEvaluator = vi.fn().mockResolvedValue(
|
|
609
|
+
const sharedStep = vi.fn().mockResolvedValue( 'shared-step-output' );
|
|
610
|
+
const sharedEvaluator = vi.fn().mockResolvedValue( 'shared-eval-output' );
|
|
599
611
|
mockActivities( {
|
|
600
|
-
[ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue(
|
|
612
|
+
[ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( {} ),
|
|
601
613
|
'shared_dispatch_wf#stepA': sharedStep,
|
|
602
614
|
'shared_dispatch_wf#evalA': sharedEvaluator
|
|
603
615
|
} );
|
|
@@ -614,9 +626,8 @@ describe( 'workflow()', () => {
|
|
|
614
626
|
} ) );
|
|
615
627
|
|
|
616
628
|
await expect( wf( {} ) ).resolves.toEqual( {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
trace: { destinations: {} }
|
|
629
|
+
stepResult: 'shared-step-output',
|
|
630
|
+
evalResult: 'shared-eval-output'
|
|
620
631
|
} );
|
|
621
632
|
expect( sharedStep ).toHaveBeenCalledWith();
|
|
622
633
|
expect( sharedEvaluator ).toHaveBeenCalledWith( { x: 1 } );
|
|
@@ -624,7 +635,7 @@ describe( 'workflow()', () => {
|
|
|
624
635
|
} );
|
|
625
636
|
|
|
626
637
|
describe( 'error handling', () => {
|
|
627
|
-
it( '
|
|
638
|
+
it( 'stores root trace destinations in memo and preserves workflow errors', async () => {
|
|
628
639
|
const { workflow } = await import( './workflow.js' );
|
|
629
640
|
const error = new Error( 'root failed with details' );
|
|
630
641
|
error.details = [ { domain: { reason: 'bad-input' } } ];
|
|
@@ -640,27 +651,12 @@ describe( 'workflow()', () => {
|
|
|
640
651
|
const thrown = await wf( {} ).catch( e => e );
|
|
641
652
|
expect( thrown ).toBe( error );
|
|
642
653
|
expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
|
|
643
|
-
expect(
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
it( 'attaches empty trace destinations to root workflow errors when no destinations are available', async () => {
|
|
647
|
-
const { workflow } = await import( './workflow.js' );
|
|
648
|
-
setWorkflowInfo( { workflowType: 'root_error_no_trace_wf' } );
|
|
649
|
-
mockActivities( { [ACTIVITY_GET_TRACE_DESTINATIONS]: vi.fn().mockResolvedValue( activityOutput( {} ) ) } );
|
|
650
|
-
const error = new Error( 'root failed without trace' );
|
|
651
|
-
|
|
652
|
-
const wf = workflow( workflowDefinition( {
|
|
653
|
-
name: 'root_error_no_trace_wf',
|
|
654
|
-
fn: async () => {
|
|
655
|
-
throw error;
|
|
656
|
-
}
|
|
654
|
+
expect( upsertMemoMock ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
655
|
+
trace: { local: '/tmp/trace' }
|
|
657
656
|
} ) );
|
|
658
|
-
|
|
659
|
-
await expect( wf( {} ) ).rejects.toBe( error );
|
|
660
|
-
expect( error[METADATA_ACCESS_SYMBOL] ).toEqual( { trace: { destinations: {} } } );
|
|
661
657
|
} );
|
|
662
658
|
|
|
663
|
-
it( '
|
|
659
|
+
it( 'preserves existing root ApplicationFailure metadata', async () => {
|
|
664
660
|
const { workflow } = await import( './workflow.js' );
|
|
665
661
|
const { ApplicationFailure } = await import( '@temporalio/workflow' );
|
|
666
662
|
setWorkflowInfo( { workflowType: 'root_application_failure_wf' } );
|
|
@@ -683,7 +679,9 @@ describe( 'workflow()', () => {
|
|
|
683
679
|
expect( thrown ).toBe( error );
|
|
684
680
|
expect( error.type ).toBe( 'OriginalType' );
|
|
685
681
|
expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
|
|
686
|
-
expect(
|
|
682
|
+
expect( upsertMemoMock ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
683
|
+
trace: { local: '/tmp/trace' }
|
|
684
|
+
} ) );
|
|
687
685
|
} );
|
|
688
686
|
|
|
689
687
|
it( 'rethrows non-root workflow errors without ApplicationFailure wrapping', async () => {
|
|
@@ -703,7 +701,10 @@ describe( 'workflow()', () => {
|
|
|
703
701
|
} ) );
|
|
704
702
|
|
|
705
703
|
await expect( wf( {} ) ).rejects.toBe( error );
|
|
706
|
-
expect(
|
|
704
|
+
expect( upsertMemoMock ).toHaveBeenCalledOnce();
|
|
705
|
+
expect( upsertMemoMock ).toHaveBeenCalledWith( {
|
|
706
|
+
activityOptions: proxyActivitiesMock.mock.calls[0][0]
|
|
707
|
+
} );
|
|
707
708
|
} );
|
|
708
709
|
} );
|
|
709
710
|
} );
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { FatalError, ValidationError } from '#errors';
|
|
2
|
-
|
|
3
1
|
export const defaultOptions = {
|
|
4
2
|
activityOptions: {
|
|
5
3
|
startToCloseTimeout: '20m',
|
|
@@ -9,8 +7,7 @@ export const defaultOptions = {
|
|
|
9
7
|
backoffCoefficient: 2.0,
|
|
10
8
|
maximumInterval: '2m',
|
|
11
9
|
maximumAttempts: 3,
|
|
12
|
-
nonRetryableErrorTypes: [
|
|
10
|
+
nonRetryableErrorTypes: []
|
|
13
11
|
}
|
|
14
|
-
}
|
|
15
|
-
disableTrace: false
|
|
12
|
+
}
|
|
16
13
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Client } from '@temporalio/client';
|
|
2
|
+
import { Storage } from '#async_storage';
|
|
3
|
+
import { FatalError } from '#errors';
|
|
4
|
+
|
|
5
|
+
// store the instance creation configs
|
|
6
|
+
const config = {
|
|
7
|
+
connection: null,
|
|
8
|
+
namespace: null
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// store a single instance that is created on demand
|
|
12
|
+
const instance = {
|
|
13
|
+
client: null
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Adds the config necessary to init Clients
|
|
17
|
+
export const setupClientConfig = ( { connection, namespace } ) => {
|
|
18
|
+
config.connection = new Proxy( connection, {
|
|
19
|
+
get( target, property, receiver ) {
|
|
20
|
+
if ( property === 'close' ) {
|
|
21
|
+
return async () => {
|
|
22
|
+
throw new FatalError( 'Client connection cannot be closed. It is owned by the worker.' );
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return Reflect.get( target, property, receiver );
|
|
26
|
+
}
|
|
27
|
+
} );
|
|
28
|
+
config.namespace = namespace;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Return a new Temporal client instance */
|
|
32
|
+
export const createTemporalClient = () => {
|
|
33
|
+
if ( !config.connection ) {
|
|
34
|
+
throw new FatalError( 'createTemporalClient() can only be called from within the worker runtime.' );
|
|
35
|
+
}
|
|
36
|
+
return new Client( { ...config } );
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Get the workflow handle for the current workflow execution */
|
|
40
|
+
export const getCurrentWorkflowHandle = () => {
|
|
41
|
+
const ctx = Storage.load();
|
|
42
|
+
if ( !ctx || !config.connection ) {
|
|
43
|
+
throw new FatalError( 'getCurrentWorkflowHandle() can only be called from Temporal activities running in the worker.' );
|
|
44
|
+
}
|
|
45
|
+
const { workflowId, runId } = ctx.activityInfo.workflowExecution;
|
|
46
|
+
return ( instance.client ??= new Client( config ) ).workflow.getHandle( workflowId, runId );
|
|
47
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const loadMock = vi.fn();
|
|
4
|
+
vi.mock( '#async_storage', () => ( {
|
|
5
|
+
Storage: { load: loadMock }
|
|
6
|
+
} ) );
|
|
7
|
+
|
|
8
|
+
const getHandleMock = vi.fn();
|
|
9
|
+
const clientConstructorMock = vi.fn( function () {
|
|
10
|
+
this.workflow = { getHandle: getHandleMock };
|
|
11
|
+
} );
|
|
12
|
+
vi.mock( '@temporalio/client', () => ( {
|
|
13
|
+
Client: clientConstructorMock
|
|
14
|
+
} ) );
|
|
15
|
+
|
|
16
|
+
const connection = {
|
|
17
|
+
fake: 'native-connection',
|
|
18
|
+
close: vi.fn().mockResolvedValue( undefined )
|
|
19
|
+
};
|
|
20
|
+
const activityContext = {
|
|
21
|
+
activityInfo: {
|
|
22
|
+
workflowExecution: {
|
|
23
|
+
workflowId: 'workflow-1',
|
|
24
|
+
runId: 'run-1'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe( 'Temporal client', () => {
|
|
30
|
+
beforeEach( () => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
vi.resetModules();
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
describe( 'createTemporalClient', () => {
|
|
36
|
+
it( 'throws a FatalError outside the worker runtime', async () => {
|
|
37
|
+
const { createTemporalClient } = await import( './client.js' );
|
|
38
|
+
const { FatalError } = await import( '#errors' );
|
|
39
|
+
|
|
40
|
+
expect( () => createTemporalClient() ).toThrow( FatalError );
|
|
41
|
+
} );
|
|
42
|
+
|
|
43
|
+
it( 'creates a client with the worker connection and namespace', async () => {
|
|
44
|
+
const { createTemporalClient, setupClientConfig } = await import( './client.js' );
|
|
45
|
+
setupClientConfig( { connection, namespace: 'namespace-1' } );
|
|
46
|
+
|
|
47
|
+
const client = createTemporalClient();
|
|
48
|
+
|
|
49
|
+
expect( clientConstructorMock ).toHaveBeenCalledWith( {
|
|
50
|
+
connection: expect.anything(),
|
|
51
|
+
namespace: 'namespace-1'
|
|
52
|
+
} );
|
|
53
|
+
expect( clientConstructorMock.mock.calls[0][0].connection ).not.toBe( connection );
|
|
54
|
+
expect( client ).toBeInstanceOf( clientConstructorMock );
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
it( 'delegates connection access but prevents closing the worker connection', async () => {
|
|
58
|
+
const { createTemporalClient, setupClientConfig } = await import( './client.js' );
|
|
59
|
+
const { FatalError } = await import( '#errors' );
|
|
60
|
+
setupClientConfig( { connection, namespace: 'namespace-1' } );
|
|
61
|
+
|
|
62
|
+
createTemporalClient();
|
|
63
|
+
const clientConnection = clientConstructorMock.mock.calls[0][0].connection;
|
|
64
|
+
|
|
65
|
+
expect( clientConnection.fake ).toBe( 'native-connection' );
|
|
66
|
+
await expect( clientConnection.close() ).rejects.toThrow( FatalError );
|
|
67
|
+
expect( connection.close ).not.toHaveBeenCalled();
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
it( 'returns a new client on each call', async () => {
|
|
71
|
+
const { createTemporalClient, setupClientConfig } = await import( './client.js' );
|
|
72
|
+
setupClientConfig( { connection, namespace: 'namespace-1' } );
|
|
73
|
+
|
|
74
|
+
expect( createTemporalClient() ).not.toBe( createTemporalClient() );
|
|
75
|
+
expect( clientConstructorMock ).toHaveBeenCalledTimes( 2 );
|
|
76
|
+
} );
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
describe( 'getCurrentWorkflowHandle', () => {
|
|
80
|
+
it( 'throws a FatalError outside a Temporal activity context', async () => {
|
|
81
|
+
loadMock.mockReturnValue( undefined );
|
|
82
|
+
const { getCurrentWorkflowHandle, setupClientConfig } = await import( './client.js' );
|
|
83
|
+
const { FatalError } = await import( '#errors' );
|
|
84
|
+
setupClientConfig( { connection, namespace: 'namespace-1' } );
|
|
85
|
+
|
|
86
|
+
expect( () => getCurrentWorkflowHandle() ).toThrow( FatalError );
|
|
87
|
+
} );
|
|
88
|
+
|
|
89
|
+
it( 'throws a FatalError outside the worker runtime', async () => {
|
|
90
|
+
loadMock.mockReturnValue( activityContext );
|
|
91
|
+
const { getCurrentWorkflowHandle } = await import( './client.js' );
|
|
92
|
+
const { FatalError } = await import( '#errors' );
|
|
93
|
+
|
|
94
|
+
expect( () => getCurrentWorkflowHandle() ).toThrow( FatalError );
|
|
95
|
+
} );
|
|
96
|
+
|
|
97
|
+
it( 'returns the handle for the current workflow execution', async () => {
|
|
98
|
+
const handle = { signal: vi.fn() };
|
|
99
|
+
loadMock.mockReturnValue( activityContext );
|
|
100
|
+
getHandleMock.mockReturnValue( handle );
|
|
101
|
+
const { getCurrentWorkflowHandle, setupClientConfig } = await import( './client.js' );
|
|
102
|
+
setupClientConfig( { connection, namespace: 'namespace-1' } );
|
|
103
|
+
|
|
104
|
+
expect( getCurrentWorkflowHandle() ).toBe( handle );
|
|
105
|
+
expect( clientConstructorMock ).toHaveBeenCalledWith( {
|
|
106
|
+
connection: expect.anything(),
|
|
107
|
+
namespace: 'namespace-1'
|
|
108
|
+
} );
|
|
109
|
+
expect( getHandleMock ).toHaveBeenCalledWith( 'workflow-1', 'run-1' );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( 'reuses its client across calls', async () => {
|
|
113
|
+
loadMock.mockReturnValue( activityContext );
|
|
114
|
+
const { getCurrentWorkflowHandle, setupClientConfig } = await import( './client.js' );
|
|
115
|
+
setupClientConfig( { connection, namespace: 'namespace-1' } );
|
|
116
|
+
|
|
117
|
+
getCurrentWorkflowHandle();
|
|
118
|
+
getCurrentWorkflowHandle();
|
|
119
|
+
|
|
120
|
+
expect( clientConstructorMock ).toHaveBeenCalledTimes( 1 );
|
|
121
|
+
expect( getHandleMock ).toHaveBeenCalledTimes( 2 );
|
|
122
|
+
} );
|
|
123
|
+
} );
|
|
124
|
+
} );
|