@outputai/core 0.7.1-next.bd6bd49.0 → 0.7.1-next.c005dac.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/bin/worker.sh +6 -0
- package/package.json +1 -1
- package/src/consts.js +0 -4
- package/src/errors.js +6 -2
- package/src/interface/evaluator.js +7 -20
- package/src/interface/evaluator.spec.js +117 -1
- package/src/interface/step.js +8 -9
- package/src/interface/step.spec.js +124 -0
- package/src/interface/validations/index.js +108 -0
- package/src/interface/validations/index.spec.js +182 -0
- package/src/interface/validations/schemas.js +113 -0
- package/src/interface/validations/schemas.spec.js +209 -0
- package/src/interface/webhook.js +1 -1
- package/src/interface/webhook.spec.js +1 -1
- package/src/interface/workflow.d.ts +10 -9
- package/src/interface/workflow.js +76 -164
- package/src/interface/workflow.spec.js +637 -521
- package/src/interface/workflow_activity_options.js +16 -0
- package/src/interface/workflow_utils.js +1 -1
- package/src/interface/zod_integration.spec.js +2 -2
- package/src/internal_utils/aggregations.js +0 -10
- package/src/internal_utils/aggregations.spec.js +1 -48
- package/src/internal_utils/errors.js +14 -8
- package/src/internal_utils/errors.spec.js +73 -27
- package/src/worker/bundle.js +26 -0
- package/src/worker/bundle.spec.js +52 -0
- package/src/worker/check.js +24 -0
- package/src/worker/index.js +1 -1
- package/src/worker/index.spec.js +1 -1
- package/src/worker/interceptors/activity.js +7 -24
- package/src/worker/interceptors/activity.spec.js +97 -66
- package/src/worker/interceptors/index.js +4 -7
- package/src/worker/interceptors/modules.js +15 -0
- package/src/worker/interceptors/workflow.js +4 -7
- package/src/worker/interceptors/workflow.spec.js +49 -42
- package/src/worker/loader_tools.js +1 -1
- package/src/worker/loader_tools.spec.js +36 -0
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +5 -109
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +31 -103
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -6
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +11 -83
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +8 -11
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +9 -9
- package/src/interface/validations/runtime.js +0 -20
- package/src/interface/validations/runtime.spec.js +0 -29
- package/src/interface/validations/schema_utils.js +0 -8
- package/src/interface/validations/schema_utils.spec.js +0 -67
- package/src/interface/validations/static.js +0 -137
- package/src/interface/validations/static.spec.js +0 -397
- package/src/interface/workflow.replay_compatibility.spec.js +0 -254
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FatalError, ValidationError } from '#errors';
|
|
2
|
+
|
|
3
|
+
export const defaultOptions = {
|
|
4
|
+
activityOptions: {
|
|
5
|
+
startToCloseTimeout: '20m',
|
|
6
|
+
heartbeatTimeout: '5m',
|
|
7
|
+
retry: {
|
|
8
|
+
initialInterval: '10s',
|
|
9
|
+
backoffCoefficient: 2.0,
|
|
10
|
+
maximumInterval: '2m',
|
|
11
|
+
maximumAttempts: 3,
|
|
12
|
+
nonRetryableErrorTypes: [ ValidationError.name, FatalError.name ]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
disableTrace: false
|
|
16
|
+
};
|
|
@@ -495,7 +495,7 @@ describe( 'Zod Schema Integration Tests', () => {
|
|
|
495
495
|
await errorStep( { age: 16, email: 'invalid' } );
|
|
496
496
|
expect.fail( 'Should have thrown an error' );
|
|
497
497
|
} catch ( error ) {
|
|
498
|
-
expect( error.message ).toContain( 'Step error_test input validation failed' );
|
|
498
|
+
expect( error.message ).toContain( 'Step "error_test" input validation failed' );
|
|
499
499
|
}
|
|
500
500
|
} );
|
|
501
501
|
|
|
@@ -527,7 +527,7 @@ describe( 'Zod Schema Integration Tests', () => {
|
|
|
527
527
|
const metadata = testStep[METADATA_ACCESS_SYMBOL];
|
|
528
528
|
expect( metadata.inputSchema ).toBe( zodSchema );
|
|
529
529
|
expect( metadata.inputSchema ).not.toBe( null );
|
|
530
|
-
expect( metadata.inputSchema.
|
|
530
|
+
expect( metadata.inputSchema._zod?.def ).toBeDefined(); // Zod v4-specific property
|
|
531
531
|
} );
|
|
532
532
|
|
|
533
533
|
it( 'should handle deeply nested Zod schemas', async () => {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { deepMergeWithResolver } from '#utils';
|
|
2
1
|
import { Attribute } from '#trace_attribute';
|
|
3
2
|
import Decimal from 'decimal.js';
|
|
4
3
|
|
|
@@ -43,12 +42,3 @@ export const aggregateAttributes = attributes => ( {
|
|
|
43
42
|
total: attributes.filter( a => Attribute.HTTPRequestCount.TYPE === a.type ).length
|
|
44
43
|
}
|
|
45
44
|
} );
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Combine two or more aggregation objects into a single Aggregation, adding up the totals and merging keys.
|
|
49
|
-
*
|
|
50
|
-
* @param {Aggregation} aggregations
|
|
51
|
-
* @returns {Aggregation}
|
|
52
|
-
*/
|
|
53
|
-
export const mergeAggregations = ( ...aggregations ) =>
|
|
54
|
-
aggregations.reduce( ( final, item ) => deepMergeWithResolver( final, item, ( a, b ) => Decimal( a ?? 0 ).add( b ?? 0 ).toNumber() ), {} );
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { Attribute } from '#trace_attribute';
|
|
3
|
-
import { aggregateAttributes
|
|
3
|
+
import { aggregateAttributes } from './aggregations.js';
|
|
4
4
|
|
|
5
5
|
describe( 'aggregateAttributes', () => {
|
|
6
6
|
it( 'returns zeroed aggregations when there are no attributes', () => {
|
|
@@ -90,50 +90,3 @@ describe( 'aggregateAttributes', () => {
|
|
|
90
90
|
} );
|
|
91
91
|
} );
|
|
92
92
|
|
|
93
|
-
describe( 'mergeAggregations', () => {
|
|
94
|
-
it( 'returns an empty object when no aggregations are provided', () => {
|
|
95
|
-
expect( mergeAggregations() ).toEqual( {} );
|
|
96
|
-
} );
|
|
97
|
-
|
|
98
|
-
it( 'sums nested aggregation values', () => {
|
|
99
|
-
expect( mergeAggregations(
|
|
100
|
-
{
|
|
101
|
-
cost: { total: 1.2 },
|
|
102
|
-
tokens: { total: 10, input: 6 },
|
|
103
|
-
httpRequests: { total: 2 }
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
cost: { total: 0.3 },
|
|
107
|
-
tokens: { total: 5, input: 2, output: 3 },
|
|
108
|
-
httpRequests: { total: 1 }
|
|
109
|
-
}
|
|
110
|
-
) ).toEqual( {
|
|
111
|
-
cost: { total: 1.5 },
|
|
112
|
-
tokens: { total: 15, input: 8, output: 3 },
|
|
113
|
-
httpRequests: { total: 3 }
|
|
114
|
-
} );
|
|
115
|
-
} );
|
|
116
|
-
|
|
117
|
-
it( 'handles undefined or partial aggregation objects', () => {
|
|
118
|
-
expect( mergeAggregations(
|
|
119
|
-
undefined,
|
|
120
|
-
{ cost: { total: 2 } },
|
|
121
|
-
{ tokens: { total: 4, reasoning: 1 } },
|
|
122
|
-
{ httpRequests: { total: 3 } }
|
|
123
|
-
) ).toEqual( {
|
|
124
|
-
cost: { total: 2 },
|
|
125
|
-
tokens: { total: 4, reasoning: 1 },
|
|
126
|
-
httpRequests: { total: 3 }
|
|
127
|
-
} );
|
|
128
|
-
} );
|
|
129
|
-
|
|
130
|
-
it( 'does not mutate source aggregation objects', () => {
|
|
131
|
-
const first = { cost: { total: 1 }, tokens: { total: 2 }, httpRequests: { total: 3 } };
|
|
132
|
-
const second = { cost: { total: 4 }, tokens: { total: 5 }, httpRequests: { total: 6 } };
|
|
133
|
-
|
|
134
|
-
mergeAggregations( first, second );
|
|
135
|
-
|
|
136
|
-
expect( first ).toEqual( { cost: { total: 1 }, tokens: { total: 2 }, httpRequests: { total: 3 } } );
|
|
137
|
-
expect( second ).toEqual( { cost: { total: 4 }, tokens: { total: 5 }, httpRequests: { total: 6 } } );
|
|
138
|
-
} );
|
|
139
|
-
} );
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { ApplicationFailure } from '@temporalio/common';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* @
|
|
6
|
-
* @param {string} key
|
|
7
|
-
* @returns {any} The value of the property
|
|
4
|
+
* Builds a Temporal ApplicationFailure based on an error attaching info to its details
|
|
5
|
+
* @param {Error} error
|
|
6
|
+
* @param {unknown} info
|
|
7
|
+
* @returns {ApplicationFailure}
|
|
8
8
|
*/
|
|
9
|
-
export const
|
|
10
|
-
|
|
9
|
+
export const buildApplicationFailureWithDetails = ( error, info ) =>
|
|
10
|
+
ApplicationFailure.create( {
|
|
11
|
+
message: error.message,
|
|
12
|
+
type: error.type ?? error.constructor?.name ?? error.name,
|
|
13
|
+
nonRetryable: error.nonRetryable,
|
|
14
|
+
details: ( Array.isArray( error.details ) ? error.details : [] ).concat( info ),
|
|
15
|
+
cause: error
|
|
16
|
+
} );
|
|
@@ -1,43 +1,89 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { ApplicationFailure } from '@temporalio/common';
|
|
3
|
+
import { buildApplicationFailureWithDetails } from './errors.js';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
it( 'returns a matching value from error details', () => {
|
|
6
|
-
const error = new Error( 'failed' );
|
|
7
|
-
error.details = [
|
|
8
|
-
{ requestId: 'req-1' },
|
|
9
|
-
{ workflowId: 'workflow-1' }
|
|
10
|
-
];
|
|
5
|
+
class CustomFailure extends Error {}
|
|
11
6
|
|
|
12
|
-
|
|
7
|
+
describe( 'buildApplicationFailureWithDetails', () => {
|
|
8
|
+
it( 'wraps a regular error in an ApplicationFailure with appended details', () => {
|
|
9
|
+
const error = new Error( 'step failed' );
|
|
10
|
+
const info = { aggregations: { cost: { total: 1 } } };
|
|
11
|
+
|
|
12
|
+
const failure = buildApplicationFailureWithDetails( error, info );
|
|
13
|
+
|
|
14
|
+
expect( failure ).toBeInstanceOf( ApplicationFailure );
|
|
15
|
+
expect( failure ).toMatchObject( {
|
|
16
|
+
message: 'step failed',
|
|
17
|
+
type: 'Error',
|
|
18
|
+
nonRetryable: false,
|
|
19
|
+
details: [ info ],
|
|
20
|
+
cause: error
|
|
21
|
+
} );
|
|
13
22
|
} );
|
|
14
23
|
|
|
15
|
-
it( '
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const wrapped = new Error( 'wrapped', { cause: root } );
|
|
24
|
+
it( 'uses the original constructor name for custom errors', () => {
|
|
25
|
+
const error = new CustomFailure( 'custom failed' );
|
|
26
|
+
const info = { trace: { destinations: { local: '/tmp/trace' } } };
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
const failure = buildApplicationFailureWithDetails( error, info );
|
|
29
|
+
|
|
30
|
+
expect( failure ).toMatchObject( {
|
|
31
|
+
message: 'custom failed',
|
|
32
|
+
type: 'CustomFailure',
|
|
33
|
+
details: [ info ],
|
|
34
|
+
cause: error
|
|
35
|
+
} );
|
|
21
36
|
} );
|
|
22
37
|
|
|
23
|
-
it( '
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
it( 'preserves existing details and appends new info without mutating the original error', () => {
|
|
39
|
+
const existingDetails = [ { domain: { reason: 'bad-input' } } ];
|
|
40
|
+
const error = new Error( 'step failed' );
|
|
41
|
+
error.details = existingDetails;
|
|
42
|
+
const info = { aggregations: { httpRequests: { total: 1 } } };
|
|
43
|
+
|
|
44
|
+
const failure = buildApplicationFailureWithDetails( error, info );
|
|
28
45
|
|
|
29
|
-
expect(
|
|
46
|
+
expect( failure.details ).toEqual( [
|
|
47
|
+
{ domain: { reason: 'bad-input' } },
|
|
48
|
+
info
|
|
49
|
+
] );
|
|
50
|
+
expect( error.details ).toBe( existingDetails );
|
|
51
|
+
expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
|
|
30
52
|
} );
|
|
31
53
|
|
|
32
|
-
it( '
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const
|
|
54
|
+
it( 'ignores non-array details on the original error', () => {
|
|
55
|
+
const error = new Error( 'step failed' );
|
|
56
|
+
error.details = { domain: { reason: 'bad-input' } };
|
|
57
|
+
const info = { aggregations: { tokens: { total: 3 } } };
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
const failure = buildApplicationFailureWithDetails( error, info );
|
|
60
|
+
|
|
61
|
+
expect( failure.details ).toEqual( [ info ] );
|
|
38
62
|
} );
|
|
39
63
|
|
|
40
|
-
it( '
|
|
41
|
-
|
|
64
|
+
it( 'preserves ApplicationFailure type, nonRetryable flag, and details while avoiding self-cause', () => {
|
|
65
|
+
const original = ApplicationFailure.create( {
|
|
66
|
+
message: 'application failed',
|
|
67
|
+
type: 'DomainFailure',
|
|
68
|
+
nonRetryable: true,
|
|
69
|
+
details: [ { domain: { reason: 'bad-input' } } ]
|
|
70
|
+
} );
|
|
71
|
+
const info = { aggregations: { cost: { total: 2 } } };
|
|
72
|
+
|
|
73
|
+
const failure = buildApplicationFailureWithDetails( original, info );
|
|
74
|
+
|
|
75
|
+
expect( failure ).toBeInstanceOf( ApplicationFailure );
|
|
76
|
+
expect( failure ).not.toBe( original );
|
|
77
|
+
expect( failure.cause ).toBe( original );
|
|
78
|
+
expect( failure.cause ).not.toBe( failure );
|
|
79
|
+
expect( failure ).toMatchObject( {
|
|
80
|
+
message: 'application failed',
|
|
81
|
+
type: 'DomainFailure',
|
|
82
|
+
nonRetryable: true,
|
|
83
|
+
details: [
|
|
84
|
+
{ domain: { reason: 'bad-input' } },
|
|
85
|
+
info
|
|
86
|
+
]
|
|
87
|
+
} );
|
|
42
88
|
} );
|
|
43
89
|
} );
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { bundleWorkflowCode } from '@temporalio/worker';
|
|
2
|
+
import { loadWorkflows, loadActivities, createWorkflowsEntryPoint } from './loader.js';
|
|
3
|
+
import { webpackConfigHook } from './bundler_options.js';
|
|
4
|
+
import { workflowInterceptorModules } from './interceptors/modules.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bundle a project's workflows exactly as the worker does, without a Temporal server.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors the worker's startup prep (`loadWorkflows` -> `loadActivities` ->
|
|
10
|
+
* `createWorkflowsEntryPoint`) and then runs the same bundler (`bundleWorkflowCode`)
|
|
11
|
+
* with the same inputs `Worker.create` derives — `webpackConfigHook` and
|
|
12
|
+
* `workflowInterceptorModules` — so it stays in parity with worker startup. Rejects if
|
|
13
|
+
* the Temporal webpack bundler fails, e.g. a `node:` built-in in the workflow's
|
|
14
|
+
* transitive import graph.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} rootDir directory to discover workflows in
|
|
17
|
+
* @returns {Promise<import('@temporalio/worker').WorkflowBundleWithSourceMap>}
|
|
18
|
+
*/
|
|
19
|
+
export async function bundleWorkflows( rootDir ) {
|
|
20
|
+
const workflows = await loadWorkflows( rootDir );
|
|
21
|
+
// Writes worker/temp/__activity_options.js, which the workflow interceptor module
|
|
22
|
+
// imports — the worker generates it via loadActivities() before Worker.create bundles.
|
|
23
|
+
await loadActivities( rootDir, workflows );
|
|
24
|
+
const workflowsPath = createWorkflowsEntryPoint( workflows );
|
|
25
|
+
return bundleWorkflowCode( { workflowsPath, workflowInterceptorModules, webpackConfigHook } );
|
|
26
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Stub the activity interceptor so the real interceptors/index.js imports cleanly.
|
|
4
|
+
vi.mock( './interceptors/activity.js', () => ( { ActivityExecutionInterceptor: class {} } ) );
|
|
5
|
+
|
|
6
|
+
vi.mock( '@temporalio/worker', () => ( {
|
|
7
|
+
bundleWorkflowCode: vi.fn().mockResolvedValue( { code: '', sourceMap: '' } )
|
|
8
|
+
} ) );
|
|
9
|
+
|
|
10
|
+
vi.mock( './loader.js', () => ( {
|
|
11
|
+
loadWorkflows: vi.fn().mockResolvedValue( [] ),
|
|
12
|
+
loadActivities: vi.fn().mockResolvedValue( {} ),
|
|
13
|
+
createWorkflowsEntryPoint: vi.fn().mockReturnValue( '/fake/workflows/entrypoint.js' )
|
|
14
|
+
} ) );
|
|
15
|
+
|
|
16
|
+
vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
|
|
17
|
+
|
|
18
|
+
import { bundleWorkflowCode } from '@temporalio/worker';
|
|
19
|
+
import { loadWorkflows, loadActivities, createWorkflowsEntryPoint } from './loader.js';
|
|
20
|
+
import { webpackConfigHook } from './bundler_options.js';
|
|
21
|
+
import { initInterceptors } from './interceptors/index.js';
|
|
22
|
+
import { workflowInterceptorModules } from './interceptors/modules.js';
|
|
23
|
+
import { bundleWorkflows } from './bundle.js';
|
|
24
|
+
|
|
25
|
+
describe( 'output-worker --check parity', () => {
|
|
26
|
+
beforeEach( () => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
loadWorkflows.mockResolvedValue( [] );
|
|
29
|
+
loadActivities.mockResolvedValue( {} );
|
|
30
|
+
createWorkflowsEntryPoint.mockReturnValue( '/fake/workflows/entrypoint.js' );
|
|
31
|
+
bundleWorkflowCode.mockResolvedValue( { code: '', sourceMap: '' } );
|
|
32
|
+
} );
|
|
33
|
+
|
|
34
|
+
it( 'worker registers the shared workflow interceptor modules', () => {
|
|
35
|
+
const { workflowModules } = initInterceptors( { activities: {}, workflows: [], connection: {} } );
|
|
36
|
+
// The check (bundleWorkflows) and the worker must register the very same modules.
|
|
37
|
+
expect( workflowModules ).toBe( workflowInterceptorModules );
|
|
38
|
+
} );
|
|
39
|
+
|
|
40
|
+
it( 'check bundles with the same inputs Worker.create derives', async () => {
|
|
41
|
+
await bundleWorkflows( '/project' );
|
|
42
|
+
|
|
43
|
+
expect( loadWorkflows ).toHaveBeenCalledWith( '/project' );
|
|
44
|
+
expect( loadActivities ).toHaveBeenCalledWith( '/project', [] );
|
|
45
|
+
expect( createWorkflowsEntryPoint ).toHaveBeenCalledWith( [] );
|
|
46
|
+
expect( bundleWorkflowCode ).toHaveBeenCalledWith( {
|
|
47
|
+
workflowsPath: '/fake/workflows/entrypoint.js',
|
|
48
|
+
workflowInterceptorModules,
|
|
49
|
+
webpackConfigHook
|
|
50
|
+
} );
|
|
51
|
+
} );
|
|
52
|
+
} );
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { bundleWorkflows } from './bundle.js';
|
|
2
|
+
|
|
3
|
+
// `output-worker --check` entry: bundle the workflows exactly as the worker would, then
|
|
4
|
+
// exit 0 (ok) / 1 (fail). The exit code is the signal. Workflow-discovery logs write to
|
|
5
|
+
// stdout via the worker logger, so mute stdout to keep output clean for CI/tooling; only
|
|
6
|
+
// real errors surface, on stderr. Catches bad workflow imports (e.g. `node:` built-ins)
|
|
7
|
+
// at build/CI time instead of crash-looping the worker at startup.
|
|
8
|
+
const callerDir = process.argv[2] ?? process.cwd();
|
|
9
|
+
|
|
10
|
+
process.stdout.write = ( ...args ) => {
|
|
11
|
+
const callback = args.find( arg => typeof arg === 'function' );
|
|
12
|
+
if ( callback ) {
|
|
13
|
+
callback();
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
( async () => {
|
|
19
|
+
await bundleWorkflows( callerDir );
|
|
20
|
+
process.exit( 0 );
|
|
21
|
+
} )().catch( error => {
|
|
22
|
+
console.error( error );
|
|
23
|
+
process.exit( 1 );
|
|
24
|
+
} );
|
package/src/worker/index.js
CHANGED
|
@@ -74,7 +74,7 @@ const callerDir = process.argv[2];
|
|
|
74
74
|
workflowsPath,
|
|
75
75
|
activities,
|
|
76
76
|
sinks,
|
|
77
|
-
interceptors: initInterceptors( { activities, workflows
|
|
77
|
+
interceptors: initInterceptors( { activities, workflows } ),
|
|
78
78
|
maxConcurrentWorkflowTaskExecutions,
|
|
79
79
|
maxConcurrentActivityTaskExecutions,
|
|
80
80
|
maxCachedWorkflows,
|
package/src/worker/index.spec.js
CHANGED
|
@@ -130,7 +130,7 @@ describe( 'worker/index', () => {
|
|
|
130
130
|
maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
|
|
131
131
|
maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
|
|
132
132
|
} ) );
|
|
133
|
-
expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: []
|
|
133
|
+
expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [] } );
|
|
134
134
|
expect( registerShutdownMock ).toHaveBeenCalledWith( { worker: mockWorker, log: mockLog } );
|
|
135
135
|
expect( setupTelemetryMock ).toHaveBeenCalledWith( { worker: mockWorker } );
|
|
136
136
|
expect( startCatalogMock ).toHaveBeenCalledWith( {
|
|
@@ -2,14 +2,11 @@ import { Context, activityInfo as activityInfoFn } from '@temporalio/activity';
|
|
|
2
2
|
import { Storage } from '#async_storage';
|
|
3
3
|
import * as Tracing from '#tracing';
|
|
4
4
|
import { headersToObject } from './headers.js';
|
|
5
|
-
import { ACTIVITY_WRAPPER_VERSION_FIELD, BusEventType, METADATA_ACCESS_SYMBOL
|
|
6
|
-
import { activityHeartbeatEnabled, activityHeartbeatIntervalMs
|
|
5
|
+
import { ACTIVITY_WRAPPER_VERSION_FIELD, BusEventType, METADATA_ACCESS_SYMBOL } from '#consts';
|
|
6
|
+
import { activityHeartbeatEnabled, activityHeartbeatIntervalMs } from '../configs.js';
|
|
7
7
|
import { messageBus } from '#bus';
|
|
8
|
-
import { Client } from '@temporalio/client';
|
|
9
|
-
import { createChildLogger } from '#logger';
|
|
10
8
|
import { aggregateAttributes } from '#internal_utils/aggregations';
|
|
11
|
-
|
|
12
|
-
const log = createChildLogger( 'ActivityInterceptor' );
|
|
9
|
+
import { buildApplicationFailureWithDetails } from '#internal_utils/errors';
|
|
13
10
|
|
|
14
11
|
/*
|
|
15
12
|
This interceptor wraps every activity execution with cross-cutting concerns:
|
|
@@ -28,7 +25,7 @@ const log = createChildLogger( 'ActivityInterceptor' );
|
|
|
28
25
|
- Headers injected by the workflow interceptor
|
|
29
26
|
*/
|
|
30
27
|
export class ActivityExecutionInterceptor {
|
|
31
|
-
constructor( { activities, workflows
|
|
28
|
+
constructor( { activities, workflows } ) {
|
|
32
29
|
// convert activities{} object to a map: activityType:kind
|
|
33
30
|
this.activityKindMap = new Map( Object.entries( activities )
|
|
34
31
|
.map( ( [ type, fn ] ) => ( [ type, fn[METADATA_ACCESS_SYMBOL].type ] ) ) );
|
|
@@ -37,12 +34,11 @@ export class ActivityExecutionInterceptor {
|
|
|
37
34
|
this.workflowsPathMap = new Map( workflows.flatMap( ( { name, aliases, path } ) =>
|
|
38
35
|
[ name, ...aliases ?? [] ].map( a => ( [ a, path ] ) )
|
|
39
36
|
) );
|
|
40
|
-
this.connection = connection;
|
|
41
37
|
};
|
|
42
38
|
|
|
43
39
|
async execute( input, next ) {
|
|
44
40
|
const activityInfo = activityInfoFn();
|
|
45
|
-
const { workflowExecution: {
|
|
41
|
+
const { workflowExecution: { runId }, activityId, activityType, workflowType } = activityInfo;
|
|
46
42
|
const { traceInfo, workflowDetails } = headersToObject( input.headers );
|
|
47
43
|
const outputActivityKind = this.activityKindMap.get( activityType );
|
|
48
44
|
const workflowFilename = this.workflowsPathMap.get( workflowType );
|
|
@@ -61,19 +57,6 @@ export class ActivityExecutionInterceptor {
|
|
|
61
57
|
|
|
62
58
|
const addAttribute = attribute => state.attributes.push( attribute );
|
|
63
59
|
|
|
64
|
-
const sendAggregationsViaSignal = async () => {
|
|
65
|
-
if ( state.attributes.length > 0 ) {
|
|
66
|
-
try {
|
|
67
|
-
const client = new Client( { connection: this.connection, namespace } );
|
|
68
|
-
const workflowHandle = client.workflow.getHandle( workflowId );
|
|
69
|
-
await workflowHandle.signal( Signal.SEND_AGGREGATIONS, aggregateAttributes( state.attributes ) );
|
|
70
|
-
} catch ( error ) {
|
|
71
|
-
const errorContext = { message: error.message, stack: error.stack, activityId, activityType, workflowId, workflowType, runId };
|
|
72
|
-
log.warn( `Signal "${Signal.SEND_AGGREGATIONS}" failed`, errorContext );
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
60
|
// Adds context accessible information
|
|
78
61
|
const storageContext = {
|
|
79
62
|
parentId: activityId,
|
|
@@ -107,9 +90,9 @@ export class ActivityExecutionInterceptor {
|
|
|
107
90
|
messageBus.emit( BusEventType.ACTIVITY_ERROR, { activityInfo, workflowDetails, outputActivityKind, error } );
|
|
108
91
|
Tracing.addEventError( { id: activityId, details: error, traceInfo } );
|
|
109
92
|
|
|
110
|
-
|
|
93
|
+
const aggregations = state.attributes.length > 0 ? aggregateAttributes( state.attributes ) : null;
|
|
111
94
|
|
|
112
|
-
throw error;
|
|
95
|
+
throw aggregations ? buildApplicationFailureWithDetails( error, { aggregations } ) : error;
|
|
113
96
|
} finally {
|
|
114
97
|
clearInterval( state.heartbeat );
|
|
115
98
|
}
|