@outputai/core 0.7.1-next.de30052.0 → 0.8.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/utils/index.d.ts +19 -0
- package/src/utils/utils.js +53 -0
- package/src/utils/utils.spec.js +105 -1
- package/src/worker/bundle.js +26 -0
- package/src/worker/bundle.spec.js +53 -0
- package/src/worker/bundler_options.js +1 -1
- package/src/worker/bundler_options.spec.js +1 -1
- package/src/worker/catalog_workflow/catalog_job.js +148 -0
- package/src/worker/catalog_workflow/catalog_job.spec.js +232 -0
- package/src/worker/check.js +24 -0
- package/src/worker/connection_monitor.js +112 -0
- package/src/worker/connection_monitor.spec.js +199 -0
- package/src/worker/index.js +146 -41
- package/src/worker/index.spec.js +281 -109
- 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 +6 -8
- package/src/worker/interceptors/workflow.spec.js +49 -42
- package/src/worker/interruption.js +33 -0
- package/src/worker/interruption.spec.js +98 -0
- package/src/worker/loader/activities.js +75 -0
- package/src/worker/loader/activities.spec.js +213 -0
- package/src/worker/loader/hooks.js +28 -0
- package/src/worker/loader/hooks.spec.js +64 -0
- package/src/worker/loader/matchers.js +46 -0
- package/src/worker/loader/matchers.spec.js +140 -0
- package/src/worker/{loader_tools.js → loader/tools.js} +19 -67
- package/src/worker/{loader_tools.spec.js → loader/tools.spec.js} +53 -85
- package/src/worker/loader/workflows.js +82 -0
- package/src/worker/loader/workflows.spec.js +256 -0
- package/src/worker/{setup_telemetry.js → telemetry.js} +9 -4
- package/src/worker/{setup_telemetry.spec.js → telemetry.spec.js} +3 -3
- 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
- package/src/worker/loader.js +0 -202
- package/src/worker/loader.spec.js +0 -498
- package/src/worker/shutdown.js +0 -26
- package/src/worker/shutdown.spec.js +0 -82
- package/src/worker/start_catalog.js +0 -96
- package/src/worker/start_catalog.spec.js +0 -179
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
import { EvaluationResult } from '../evaluation_result.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detects if a value behaves like a Zod v4 Classic/Mini schema.
|
|
6
|
+
*
|
|
7
|
+
* Zod v4 schemas from different package instances do not share the same
|
|
8
|
+
* prototype, so `instanceof z.ZodType` is too fragile here. The `_zod`
|
|
9
|
+
* property is the documented v4 runtime marker, and `safeParse` is required
|
|
10
|
+
* because the validators call it directly.
|
|
11
|
+
*
|
|
12
|
+
* @param {unknown} schema - The schema to check
|
|
13
|
+
* @returns {boolean} True if the schema is a Zod schema
|
|
14
|
+
*/
|
|
15
|
+
export const isZodSchema = schema =>
|
|
16
|
+
Boolean(
|
|
17
|
+
schema &&
|
|
18
|
+
typeof schema === 'object' &&
|
|
19
|
+
typeof schema._zod?.def?.type === 'string' &&
|
|
20
|
+
typeof schema.safeParse === 'function'
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const refineSchema = ( value, ctx ) => {
|
|
24
|
+
if ( !value || isZodSchema( value ) ) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ctx.addIssue( {
|
|
29
|
+
code: 'invalid_type',
|
|
30
|
+
message: 'Schema must be a Zod schema'
|
|
31
|
+
} );
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const durationSchema = z.union( [ z.string().regex(
|
|
35
|
+
/^(\d+)(ms|s|m|h|d)$/,
|
|
36
|
+
'Expected duration like "500ms", "10s", "5m", "2h", or "1d"'
|
|
37
|
+
), z.number() ] );
|
|
38
|
+
|
|
39
|
+
const prioritySchema = z.object( {
|
|
40
|
+
fairnessKey: z.string().optional(),
|
|
41
|
+
fairnessWeight: z.number().min( 0.0001 ).max( 1000 ).optional(),
|
|
42
|
+
priorityKey: z.number().min( 1 ).optional()
|
|
43
|
+
} );
|
|
44
|
+
|
|
45
|
+
const activityOptionsSchema = z.strictObject( {
|
|
46
|
+
activityId: z.string().optional(),
|
|
47
|
+
cancellationType: z.enum( [ 'TRY_CANCEL', 'WAIT_CANCELLATION_COMPLETED', 'ABANDON' ] ).optional(),
|
|
48
|
+
heartbeatTimeout: durationSchema.optional(),
|
|
49
|
+
priority: prioritySchema.optional(),
|
|
50
|
+
retry: z.strictObject( {
|
|
51
|
+
initialInterval: durationSchema.optional(),
|
|
52
|
+
backoffCoefficient: z.number().gte( 1 ).optional(),
|
|
53
|
+
maximumInterval: durationSchema.optional(),
|
|
54
|
+
maximumAttempts: z.number().gte( 1 ).int().optional(),
|
|
55
|
+
nonRetryableErrorTypes: z.array( z.string() ).optional()
|
|
56
|
+
} ).optional(),
|
|
57
|
+
scheduleToCloseTimeout: durationSchema.optional(),
|
|
58
|
+
scheduleToStartTimeout: durationSchema.optional(),
|
|
59
|
+
startToCloseTimeout: durationSchema.optional(),
|
|
60
|
+
summary: z.string().optional()
|
|
61
|
+
} );
|
|
62
|
+
|
|
63
|
+
const baseSchema = z.strictObject( {
|
|
64
|
+
name: z.string().regex( /^[a-z_][a-z0-9_]*$/i ),
|
|
65
|
+
description: z.string().optional(),
|
|
66
|
+
inputSchema: z.any().optional().superRefine( refineSchema ),
|
|
67
|
+
outputSchema: z.any().optional().superRefine( refineSchema ),
|
|
68
|
+
fn: z.function(),
|
|
69
|
+
options: z.object( {
|
|
70
|
+
activityOptions: activityOptionsSchema.optional()
|
|
71
|
+
} ).optional()
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
export const stepSchema = baseSchema;
|
|
75
|
+
|
|
76
|
+
export const workflowSchema = baseSchema.extend( {
|
|
77
|
+
aliases: z.array( z.string().regex( /^[a-z_][a-z0-9_]*$/i ) ).optional().default( [] ),
|
|
78
|
+
options: baseSchema.shape.options.unwrap().extend( {
|
|
79
|
+
disableTrace: z.boolean().optional().default( false )
|
|
80
|
+
} ).optional()
|
|
81
|
+
} );
|
|
82
|
+
|
|
83
|
+
export const evaluatorSchema = baseSchema.omit( { outputSchema: true } );
|
|
84
|
+
|
|
85
|
+
export const evaluatorOutputSchema = z.instanceof( EvaluationResult );
|
|
86
|
+
|
|
87
|
+
export const httpRequestSchema = z.object( {
|
|
88
|
+
url: z.url( { protocol: /^https?$/ } ),
|
|
89
|
+
method: z.enum( [ 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE' ] ),
|
|
90
|
+
payload: z.any().optional(),
|
|
91
|
+
headers: z.record( z.string(), z.string() ).optional()
|
|
92
|
+
} );
|
|
93
|
+
|
|
94
|
+
export const workflowInvocationOptionsSchema = z.strictObject( {
|
|
95
|
+
detached: z.boolean().optional(),
|
|
96
|
+
activityOptions: activityOptionsSchema.optional(),
|
|
97
|
+
context: z.object( {
|
|
98
|
+
control: z.object( {
|
|
99
|
+
continueAsNew: z.function().optional(),
|
|
100
|
+
isContinueAsNewSuggested: z.function().optional()
|
|
101
|
+
} ).loose().optional(),
|
|
102
|
+
info: z.object( {
|
|
103
|
+
workflowId: z.string().optional(),
|
|
104
|
+
runId: z.string().optional()
|
|
105
|
+
} ).loose().optional()
|
|
106
|
+
} ).loose().optional()
|
|
107
|
+
} ).optional();
|
|
108
|
+
|
|
109
|
+
export const executeInParallelSchema = z.object( {
|
|
110
|
+
jobs: z.array( z.function() ),
|
|
111
|
+
concurrency: z.number().min( 1 ).or( z.literal( Infinity ) ),
|
|
112
|
+
onJobCompleted: z.function().optional()
|
|
113
|
+
} );
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { EvaluationResult } from '../evaluation_result.js';
|
|
4
|
+
import {
|
|
5
|
+
evaluatorOutputSchema,
|
|
6
|
+
evaluatorSchema,
|
|
7
|
+
executeInParallelSchema,
|
|
8
|
+
httpRequestSchema,
|
|
9
|
+
isZodSchema,
|
|
10
|
+
stepSchema,
|
|
11
|
+
workflowInvocationOptionsSchema,
|
|
12
|
+
workflowSchema
|
|
13
|
+
} from './schemas.js';
|
|
14
|
+
|
|
15
|
+
const fn = () => {};
|
|
16
|
+
const validBase = {
|
|
17
|
+
name: 'valid_name',
|
|
18
|
+
description: 'Valid description',
|
|
19
|
+
inputSchema: z.object( { value: z.string() } ),
|
|
20
|
+
outputSchema: z.object( { result: z.string() } ),
|
|
21
|
+
fn,
|
|
22
|
+
options: {
|
|
23
|
+
activityOptions: {
|
|
24
|
+
startToCloseTimeout: '5m',
|
|
25
|
+
heartbeatTimeout: 1000,
|
|
26
|
+
retry: {
|
|
27
|
+
initialInterval: '10s',
|
|
28
|
+
backoffCoefficient: 2,
|
|
29
|
+
maximumInterval: '1m',
|
|
30
|
+
maximumAttempts: 3,
|
|
31
|
+
nonRetryableErrorTypes: [ 'FatalError' ]
|
|
32
|
+
},
|
|
33
|
+
priority: {
|
|
34
|
+
fairnessKey: 'tenant',
|
|
35
|
+
fairnessWeight: 1,
|
|
36
|
+
priorityKey: 2
|
|
37
|
+
},
|
|
38
|
+
summary: 'Short summary'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe( 'validation schemas', () => {
|
|
44
|
+
describe( 'isZodSchema()', () => {
|
|
45
|
+
it( 'identifies Zod schemas', () => {
|
|
46
|
+
expect( isZodSchema( z.object( { name: z.string() } ) ) ).toBe( true );
|
|
47
|
+
expect( isZodSchema( z.string() ) ).toBe( true );
|
|
48
|
+
expect( isZodSchema( z.array( z.number() ) ) ).toBe( true );
|
|
49
|
+
expect( isZodSchema( z.union( [ z.string(), z.number() ] ) ) ).toBe( true );
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
it( 'uses the Zod v4 runtime shape instead of instanceof', () => {
|
|
53
|
+
const schemaLikeFromAnotherPackageInstance = {
|
|
54
|
+
_zod: {
|
|
55
|
+
def: {
|
|
56
|
+
type: 'object'
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
safeParse: () => ( { success: true, data: {} } )
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
expect( isZodSchema( schemaLikeFromAnotherPackageInstance ) ).toBe( true );
|
|
63
|
+
} );
|
|
64
|
+
|
|
65
|
+
it( 'rejects non-Zod values', () => {
|
|
66
|
+
expect( isZodSchema( { type: 'object' } ) ).toBe( false );
|
|
67
|
+
expect( isZodSchema( {} ) ).toBe( false );
|
|
68
|
+
expect( isZodSchema( [] ) ).toBe( false );
|
|
69
|
+
expect( isZodSchema( null ) ).toBe( false );
|
|
70
|
+
expect( isZodSchema( undefined ) ).toBe( false );
|
|
71
|
+
expect( isZodSchema( 'string' ) ).toBe( false );
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
it( 'rejects objects that only partially match the Zod shape', () => {
|
|
75
|
+
expect( isZodSchema( { safeParse: () => ( { success: true } ) } ) ).toBe( false );
|
|
76
|
+
expect( isZodSchema( { _zod: { def: { type: 'string' } } } ) ).toBe( false );
|
|
77
|
+
expect( isZodSchema( { _zod: { def: {} }, safeParse: () => ( { success: true } ) } ) ).toBe( false );
|
|
78
|
+
expect( isZodSchema( { _zod: null, safeParse: () => ( { success: true } ) } ) ).toBe( false );
|
|
79
|
+
} );
|
|
80
|
+
} );
|
|
81
|
+
|
|
82
|
+
describe( 'stepSchema', () => {
|
|
83
|
+
it( 'accepts a valid step definition', () => {
|
|
84
|
+
expect( stepSchema.safeParse( validBase ).success ).toBe( true );
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
it( 'rejects invalid names, non-Zod schemas, invalid activity options, and unknown top-level keys', () => {
|
|
88
|
+
expect( stepSchema.safeParse( { ...validBase, name: 'invalid-name' } ).success ).toBe( false );
|
|
89
|
+
expect( stepSchema.safeParse( { ...validBase, inputSchema: { type: 'object' } } ).success ).toBe( false );
|
|
90
|
+
expect( stepSchema.safeParse( {
|
|
91
|
+
...validBase,
|
|
92
|
+
options: { activityOptions: { retry: { maximumAttempts: 0 } } }
|
|
93
|
+
} ).success ).toBe( false );
|
|
94
|
+
expect( stepSchema.safeParse( { ...validBase, unexpected: true } ).success ).toBe( false );
|
|
95
|
+
} );
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
describe( 'workflowSchema', () => {
|
|
99
|
+
it( 'accepts workflow-specific options and aliases', () => {
|
|
100
|
+
const result = workflowSchema.safeParse( {
|
|
101
|
+
...validBase,
|
|
102
|
+
aliases: [ 'old_name' ],
|
|
103
|
+
options: {
|
|
104
|
+
...validBase.options,
|
|
105
|
+
disableTrace: true
|
|
106
|
+
}
|
|
107
|
+
} );
|
|
108
|
+
|
|
109
|
+
expect( result.success ).toBe( true );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( 'defaults aliases and rejects invalid workflow fields', () => {
|
|
113
|
+
const result = workflowSchema.safeParse( validBase );
|
|
114
|
+
expect( result.success && result.data.aliases ).toEqual( [] );
|
|
115
|
+
|
|
116
|
+
expect( workflowSchema.safeParse( { ...validBase, aliases: [ 'bad-alias' ] } ).success ).toBe( false );
|
|
117
|
+
expect( workflowSchema.safeParse( {
|
|
118
|
+
...validBase,
|
|
119
|
+
options: { ...validBase.options, disableTrace: 'yes' }
|
|
120
|
+
} ).success ).toBe( false );
|
|
121
|
+
} );
|
|
122
|
+
} );
|
|
123
|
+
|
|
124
|
+
describe( 'evaluatorSchema and evaluatorOutputSchema', () => {
|
|
125
|
+
it( 'accepts evaluator definitions without outputSchema', () => {
|
|
126
|
+
const { outputSchema: _outputSchema, ...validEvaluator } = validBase;
|
|
127
|
+
expect( evaluatorSchema.safeParse( validEvaluator ).success ).toBe( true );
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
it( 'rejects evaluator definitions with outputSchema', () => {
|
|
131
|
+
expect( evaluatorSchema.safeParse( validBase ).success ).toBe( false );
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
it( 'accepts only EvaluationResult instances as evaluator output', () => {
|
|
135
|
+
expect( evaluatorOutputSchema.safeParse( new EvaluationResult( { value: 'ok', confidence: 1 } ) ).success ).toBe( true );
|
|
136
|
+
expect( evaluatorOutputSchema.safeParse( { value: 'ok', confidence: 1 } ).success ).toBe( false );
|
|
137
|
+
} );
|
|
138
|
+
} );
|
|
139
|
+
|
|
140
|
+
describe( 'httpRequestSchema', () => {
|
|
141
|
+
it( 'accepts valid HTTP request payloads', () => {
|
|
142
|
+
expect( httpRequestSchema.safeParse( {
|
|
143
|
+
url: 'https://example.com',
|
|
144
|
+
method: 'POST',
|
|
145
|
+
payload: { ok: true },
|
|
146
|
+
headers: { authorization: 'Bearer token' }
|
|
147
|
+
} ).success ).toBe( true );
|
|
148
|
+
} );
|
|
149
|
+
|
|
150
|
+
it( 'rejects invalid URL protocols, methods, and header values', () => {
|
|
151
|
+
expect( httpRequestSchema.safeParse( { url: 'ftp://example.com', method: 'GET' } ).success ).toBe( false );
|
|
152
|
+
expect( httpRequestSchema.safeParse( { url: 'https://example.com', method: 'OPTIONS' } ).success ).toBe( false );
|
|
153
|
+
expect( httpRequestSchema.safeParse( {
|
|
154
|
+
url: 'https://example.com',
|
|
155
|
+
method: 'GET',
|
|
156
|
+
headers: { count: 1 }
|
|
157
|
+
} ).success ).toBe( false );
|
|
158
|
+
} );
|
|
159
|
+
} );
|
|
160
|
+
|
|
161
|
+
describe( 'workflowInvocationOptionsSchema', () => {
|
|
162
|
+
it( 'accepts omitted options and valid invocation configuration', () => {
|
|
163
|
+
expect( workflowInvocationOptionsSchema.safeParse( undefined ).success ).toBe( true );
|
|
164
|
+
expect( workflowInvocationOptionsSchema.safeParse( {
|
|
165
|
+
detached: true,
|
|
166
|
+
activityOptions: { retry: { maximumAttempts: 1 } },
|
|
167
|
+
context: {
|
|
168
|
+
control: {
|
|
169
|
+
continueAsNew: fn,
|
|
170
|
+
isContinueAsNewSuggested: fn,
|
|
171
|
+
extraControl: true
|
|
172
|
+
},
|
|
173
|
+
info: {
|
|
174
|
+
workflowId: 'wf',
|
|
175
|
+
runId: 'run',
|
|
176
|
+
extraInfo: true
|
|
177
|
+
},
|
|
178
|
+
extraContext: true
|
|
179
|
+
}
|
|
180
|
+
} ).success ).toBe( true );
|
|
181
|
+
} );
|
|
182
|
+
|
|
183
|
+
it( 'rejects stale option shapes and invalid invocation values', () => {
|
|
184
|
+
expect( workflowInvocationOptionsSchema.safeParse( {
|
|
185
|
+
options: { activityOptions: { retry: { maximumAttempts: 1 } } }
|
|
186
|
+
} ).success ).toBe( false );
|
|
187
|
+
expect( workflowInvocationOptionsSchema.safeParse( { detached: 'true' } ).success ).toBe( false );
|
|
188
|
+
expect( workflowInvocationOptionsSchema.safeParse( {
|
|
189
|
+
activityOptions: { retry: { maximumAttempts: 0 } }
|
|
190
|
+
} ).success ).toBe( false );
|
|
191
|
+
expect( workflowInvocationOptionsSchema.safeParse( {
|
|
192
|
+
context: { control: { continueAsNew: 'nope' } }
|
|
193
|
+
} ).success ).toBe( false );
|
|
194
|
+
} );
|
|
195
|
+
} );
|
|
196
|
+
|
|
197
|
+
describe( 'executeInParallelSchema', () => {
|
|
198
|
+
it( 'accepts valid execution configs', () => {
|
|
199
|
+
expect( executeInParallelSchema.safeParse( { jobs: [ fn ], concurrency: 1 } ).success ).toBe( true );
|
|
200
|
+
expect( executeInParallelSchema.safeParse( { jobs: [ fn ], concurrency: Infinity, onJobCompleted: fn } ).success ).toBe( true );
|
|
201
|
+
} );
|
|
202
|
+
|
|
203
|
+
it( 'rejects invalid execution configs', () => {
|
|
204
|
+
expect( executeInParallelSchema.safeParse( { jobs: [], concurrency: 0 } ).success ).toBe( false );
|
|
205
|
+
expect( executeInParallelSchema.safeParse( { jobs: [ 'not-a-function' ], concurrency: 1 } ).success ).toBe( false );
|
|
206
|
+
expect( executeInParallelSchema.safeParse( { jobs: [ fn ], concurrency: 1, onJobCompleted: 'nope' } ).success ).toBe( false );
|
|
207
|
+
} );
|
|
208
|
+
} );
|
|
209
|
+
} );
|
package/src/interface/webhook.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { defineSignal, setHandler, proxyActivities, workflowInfo, proxySinks, uuid4, Trigger } from '@temporalio/workflow';
|
|
3
3
|
import { ACTIVITY_SEND_HTTP_REQUEST } from '#consts';
|
|
4
4
|
import { FatalError } from '#errors';
|
|
5
|
-
import { validateRequestPayload } from './validations/
|
|
5
|
+
import { validateRequestPayload } from './validations/index.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Call the internal activity to make a HTTP request and returns its response.
|
|
@@ -79,20 +79,21 @@ export type WorkflowContext<
|
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
82
|
+
* Options for workflow invocations.
|
|
83
83
|
*
|
|
84
84
|
* Allows overriding Temporal Activity options for this workflow.
|
|
85
85
|
*/
|
|
86
|
-
export type
|
|
86
|
+
export type WorkflowInvocationOptions<Context extends WorkflowContext = WorkflowContext> = {
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* Temporal activity options for this invocation (overrides the workflow's default activity options).
|
|
90
90
|
*/
|
|
91
|
-
|
|
91
|
+
activityOptions?: TemporalActivityOptions,
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* Configures whether this workflow runs detached
|
|
95
|
-
*
|
|
94
|
+
* Configures whether this workflow runs detached:
|
|
95
|
+
* - `detached=true` maps to `ParentClosePolicy.ABANDON`: if parent closes before child, the child keeps executing.
|
|
96
|
+
* - `detached=false` maps to `ParentClosePolicy.TERMINATE`: if parent closes before child, the child is terminated.
|
|
96
97
|
*/
|
|
97
98
|
detached?: boolean,
|
|
98
99
|
|
|
@@ -140,19 +141,19 @@ export type WorkflowFunction<
|
|
|
140
141
|
*
|
|
141
142
|
* It accepts the same input and returns the same value, calling the user function inside.
|
|
142
143
|
*
|
|
143
|
-
* The second argument is a
|
|
144
|
+
* The second argument is a WorkflowInvocationOptions object, allowing workflows configuration overwrite.
|
|
144
145
|
*
|
|
145
146
|
* It adds input and output validation based on the `inputSchema`, `outputSchema`.
|
|
146
147
|
*
|
|
147
148
|
* @param input - The workflow input; it matches the schema defined by `inputSchema`.
|
|
148
|
-
* @param
|
|
149
|
+
* @param options - Additional options for the invocation.
|
|
149
150
|
* @returns A value matching the schema defined by `outputSchema`.
|
|
150
151
|
*/
|
|
151
152
|
export type WorkflowFunctionWrapper<WorkflowFunction> =
|
|
152
153
|
[Parameters<WorkflowFunction>[0]] extends [undefined | null] ?
|
|
153
|
-
( input?: undefined | null,
|
|
154
|
+
( input?: undefined | null, options?: WorkflowInvocationOptions ) =>
|
|
154
155
|
ReturnType<WorkflowFunction> :
|
|
155
|
-
( input: Parameters<WorkflowFunction>[0],
|
|
156
|
+
( input: Parameters<WorkflowFunction>[0], options?: WorkflowInvocationOptions ) =>
|
|
156
157
|
ReturnType<WorkflowFunction>;
|
|
157
158
|
|
|
158
159
|
/**
|
|
@@ -1,202 +1,114 @@
|
|
|
1
1
|
// THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
2
|
import { proxyActivities, inWorkflowContext, executeChild, workflowInfo, uuid4, ParentClosePolicy } from '@temporalio/workflow';
|
|
3
|
-
import {
|
|
4
|
-
import { validateWorkflow } from './validations/static.js';
|
|
5
|
-
import { validateWithSchema } from './validations/runtime.js';
|
|
3
|
+
import { WorkflowValidator } from './validations/index.js';
|
|
6
4
|
import { deepMerge, setMetadata, toUrlSafeBase64 } from '#utils';
|
|
7
|
-
import { FatalError, ValidationError } from '#errors';
|
|
8
5
|
import { WorkflowContext } from '#internal_utils/workflow_context';
|
|
9
|
-
import { aggregateAttributes, mergeAggregations } from '#internal_utils/aggregations';
|
|
10
|
-
import { extractErrorDetail } from '#internal_utils/errors';
|
|
11
6
|
import { TraceInfo } from '#internal_utils/trace_info';
|
|
7
|
+
import { defaultOptions } from './workflow_activity_options.js';
|
|
12
8
|
import {
|
|
13
|
-
ACTIVITY_GET_TRACE_DESTINATIONS,
|
|
14
9
|
ACTIVITY_WRAPPER_VERSION_FIELD,
|
|
10
|
+
ACTIVITY_GET_TRACE_DESTINATIONS,
|
|
15
11
|
METADATA_ACCESS_SYMBOL,
|
|
16
12
|
SHARED_STEP_PREFIX,
|
|
17
|
-
Signal,
|
|
18
13
|
WORKFLOW_WRAPPER_VERSION_FIELD
|
|
19
14
|
} from '#consts';
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
initialInterval: '10s',
|
|
27
|
-
backoffCoefficient: 2.0,
|
|
28
|
-
maximumInterval: '2m',
|
|
29
|
-
maximumAttempts: 3,
|
|
30
|
-
nonRetryableErrorTypes: [ ValidationError.name, FatalError.name ]
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
disableTrace: false
|
|
34
|
-
};
|
|
16
|
+
/**
|
|
17
|
+
* @temp
|
|
18
|
+
* This is to keep backwards compatibility [OUT-468]
|
|
19
|
+
*/
|
|
20
|
+
const parseActivityOutput = p => Object.hasOwn( p ?? {}, ACTIVITY_WRAPPER_VERSION_FIELD ) ? p.output : p;
|
|
35
21
|
|
|
36
22
|
/**
|
|
37
|
-
*
|
|
23
|
+
* @temp This is a TEMP fallback method to allow workflow child checks on replays without memo. [OUT-468]
|
|
24
|
+
* This workflows for most scenarios, only does not supports recursion with the same name.
|
|
38
25
|
*/
|
|
39
|
-
const
|
|
26
|
+
const checkChildFallback = ( { workflowType, name, aliases } ) => workflowType !== name && !aliases.includes( workflowType );
|
|
40
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Create a new workflow and return a wrapper function around its fn handler
|
|
30
|
+
*/
|
|
41
31
|
export function workflow( { name, description, inputSchema, outputSchema, fn, options = {}, aliases = [] } ) {
|
|
42
|
-
|
|
32
|
+
WorkflowValidator.validateDefinition( { name, description, inputSchema, outputSchema, fn, options, aliases } );
|
|
43
33
|
|
|
44
34
|
const { disableTrace, activityOptions } = deepMerge( defaultOptions, options );
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Wraps the `fn` function of the workflow
|
|
49
|
-
*
|
|
50
|
-
* @param {unknown} input - The input, must match the inputSchema
|
|
51
|
-
* @param {object} extra - Workflow configurations (received directly only in unit tests)
|
|
52
|
-
* @returns {unknown} The result, will match the outputSchema
|
|
53
|
-
*/
|
|
35
|
+
const validator = new WorkflowValidator( { name, inputSchema, outputSchema } );
|
|
36
|
+
|
|
54
37
|
const wrapper = async ( input, extra = {} ) => {
|
|
38
|
+
validator.validateInvocationOptions( extra );
|
|
39
|
+
|
|
55
40
|
// this returns a plain function, for example, in unit tests
|
|
56
41
|
if ( !inWorkflowContext() ) {
|
|
57
|
-
|
|
58
|
-
const output = await fn( input, deepMerge( WorkflowContext.build(), extra
|
|
59
|
-
|
|
42
|
+
validator.validateInput( input );
|
|
43
|
+
const output = await fn( input, deepMerge( WorkflowContext.build(), extra?.context ) );
|
|
44
|
+
validator.validateOutput( output );
|
|
60
45
|
return output;
|
|
61
46
|
}
|
|
62
47
|
|
|
63
|
-
const { workflowId, memo, root } = workflowInfo();
|
|
64
|
-
|
|
48
|
+
const { workflowId, workflowType, memo, root } = workflowInfo();
|
|
49
|
+
|
|
50
|
+
// if the stack already includes this workflowId, means the workflow() function was called
|
|
51
|
+
// from within a running workflow, meaning it is suppose to start a child workflow
|
|
52
|
+
const isChild = Array.isArray( memo.stack ) ? memo.stack.includes( workflowId ) :
|
|
53
|
+
checkChildFallback( { workflowType, aliases, name } );
|
|
54
|
+
|
|
55
|
+
if ( isChild ) {
|
|
56
|
+
const result = await executeChild( name, {
|
|
57
|
+
args: undefined === input ? [] : [ input ],
|
|
58
|
+
workflowId: `${workflowId}-${toUrlSafeBase64( uuid4() )}`,
|
|
59
|
+
parentClosePolicy: ParentClosePolicy[extra?.detached ? 'ABANDON' : 'TERMINATE'],
|
|
60
|
+
memo: {
|
|
61
|
+
...memo, // Preserve memo and mix activityOptions, if provided
|
|
62
|
+
...( extra?.activityOptions && {
|
|
63
|
+
activityOptions: deepMerge( memo?.activityOptions ?? {}, extra?.activityOptions )
|
|
64
|
+
} )
|
|
65
|
+
}
|
|
66
|
+
} );
|
|
67
|
+
return result.output;
|
|
68
|
+
}
|
|
65
69
|
|
|
66
70
|
const isRoot = !root;
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* @IMPORTANT Keep support for deprecated non-wrapped activity result to allow for Temporal replays.
|
|
79
|
-
* @TODO [OUT-468]
|
|
80
|
-
*/
|
|
81
|
-
const getTraceDestinations = async () => {
|
|
82
|
-
const result = await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( memo.traceInfo );
|
|
83
|
-
return isActivityResultWrapped( result ) ? result.output : result;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// Creates the result wrapper with information about the workflow
|
|
87
|
-
const workflowResult = {
|
|
88
|
-
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
89
|
-
aggregations: null,
|
|
90
|
-
...( isRoot && {
|
|
91
|
-
trace: {
|
|
92
|
-
destinations: await getTraceDestinations()
|
|
93
|
-
}
|
|
94
|
-
} )
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
// Combine aggregations in the workflow result aggregations, mutating it
|
|
98
|
-
const mergeAggregationsInWorkflowResult = aggregations => {
|
|
99
|
-
workflowResult.aggregations = mergeAggregations( workflowResult.aggregations, aggregations );
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
setHandler( defineSignal( Signal.SEND_AGGREGATIONS ), aggregations => {
|
|
103
|
-
mergeAggregationsInWorkflowResult( aggregations );
|
|
104
|
-
} );
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* @IMPORTANT Keep support for deprecated add_attribute Signal to allow for Temporal replays.
|
|
108
|
-
* @TODO This can be removed 30days after this release
|
|
109
|
-
*/
|
|
110
|
-
setHandler( defineSignal( 'add_attribute' ), attribute => {
|
|
111
|
-
mergeAggregationsInWorkflowResult( aggregateAttributes( [ attribute ] ) );
|
|
112
|
-
} );
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Invoke a step and unwraps the result to extract and merge "aggregations" and return only the output.
|
|
116
|
-
*
|
|
117
|
-
* @IMPORTANT Keep support for deprecated non-wrapped activity result to allow for Temporal replays.
|
|
118
|
-
* @TODO [OUT-468]
|
|
119
|
-
* @param {Function} step
|
|
120
|
-
* @param {...any} args
|
|
121
|
-
* @returns {any} The step "output"
|
|
122
|
-
*/
|
|
123
|
-
const callStepAndUnwrapResult = async ( step, ...args ) => {
|
|
124
|
-
const result = await step( ...args );
|
|
125
|
-
if ( !isActivityResultWrapped( result ) ) {
|
|
126
|
-
return result;
|
|
127
|
-
}
|
|
128
|
-
const { output, aggregations } = result;
|
|
129
|
-
if ( aggregations ) {
|
|
130
|
-
mergeAggregationsInWorkflowResult( aggregations );
|
|
131
|
-
}
|
|
132
|
-
return output;
|
|
133
|
-
};
|
|
72
|
+
memo.stack = [ ...memo.stack ?? [], workflowId ];
|
|
73
|
+
// Parent options have prevalence on nested calls, child will be overwritten
|
|
74
|
+
memo.activityOptions = deepMerge( activityOptions, memo.activityOptions );
|
|
75
|
+
// Trace info is only added in the root workflow
|
|
76
|
+
if ( isRoot ) {
|
|
77
|
+
memo.traceInfo = TraceInfo.build( { disableTrace } );
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const steps = proxyActivities( memo.activityOptions );
|
|
81
|
+
const traceDest = isRoot && parseActivityOutput( await steps[ACTIVITY_GET_TRACE_DESTINATIONS]( memo.traceInfo ) );
|
|
134
82
|
|
|
135
83
|
try {
|
|
136
|
-
|
|
137
|
-
validateWithSchema( inputSchema, input, `Workflow ${name} input` );
|
|
84
|
+
validator.validateInput( input );
|
|
138
85
|
|
|
86
|
+
// Creates an activity caller based on a prefix
|
|
87
|
+
const createCaller = prefix => async ( t, ...args ) => parseActivityOutput( await steps[`${prefix}#${t}`]( ...args ) );
|
|
88
|
+
|
|
89
|
+
// This are functions used by the AST to replace direct activity (step/evaluator) calls
|
|
139
90
|
const dispatchers = {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
callStepAndUnwrapResult( steps[`${SHARED_STEP_PREFIX}#${stepName}`], input, options ),
|
|
145
|
-
invokeEvaluator: async ( evaluatorName, input, options ) =>
|
|
146
|
-
callStepAndUnwrapResult( steps[`${name}#${evaluatorName}`], input, options ),
|
|
147
|
-
invokeSharedEvaluator: async ( evaluatorName, input, options ) =>
|
|
148
|
-
callStepAndUnwrapResult( steps[`${SHARED_STEP_PREFIX}#${evaluatorName}`], input, options ),
|
|
149
|
-
|
|
150
|
-
// Start a new child workflow
|
|
151
|
-
startWorkflow: async ( childName, input, extra = {} ) => {
|
|
152
|
-
try {
|
|
153
|
-
const result = await executeChild( childName, {
|
|
154
|
-
args: undefined === input ? [] : [ input ],
|
|
155
|
-
workflowId: `${workflowId}-${toUrlSafeBase64( uuid4() )}`,
|
|
156
|
-
parentClosePolicy: ParentClosePolicy[extra?.detached ? 'ABANDON' : 'TERMINATE'],
|
|
157
|
-
memo: {
|
|
158
|
-
...memo,
|
|
159
|
-
...( extra?.options?.activityOptions && { activityOptions: deepMerge( activityOptions, extra.options.activityOptions ) } )
|
|
160
|
-
}
|
|
161
|
-
} );
|
|
162
|
-
/**
|
|
163
|
-
* @IMPORTANT Keep support for deprecated ".attributes" from workflow results to allow for Temporal replays.
|
|
164
|
-
* @TODO [OUT-468]
|
|
165
|
-
*/
|
|
166
|
-
if ( result?.attributes ) {
|
|
167
|
-
mergeAggregationsInWorkflowResult( aggregateAttributes( result.attributes ) );
|
|
168
|
-
}
|
|
169
|
-
if ( result?.aggregations ) {
|
|
170
|
-
mergeAggregationsInWorkflowResult( result.aggregations );
|
|
171
|
-
}
|
|
172
|
-
return result.output;
|
|
173
|
-
} catch ( error ) {
|
|
174
|
-
/**
|
|
175
|
-
* @IMPORTANT Keep support for deprecated ".attributes" from workflow errors to allow for Temporal replays.
|
|
176
|
-
* @TODO [OUT-468]
|
|
177
|
-
*/
|
|
178
|
-
const attributesFromError = extractErrorDetail( error, 'attributes' );
|
|
179
|
-
if ( attributesFromError ) {
|
|
180
|
-
mergeAggregationsInWorkflowResult( aggregateAttributes( attributesFromError ) );
|
|
181
|
-
}
|
|
182
|
-
const aggregationsFromError = extractErrorDetail( error, 'aggregations' );
|
|
183
|
-
if ( aggregationsFromError ) {
|
|
184
|
-
mergeAggregationsInWorkflowResult( aggregationsFromError );
|
|
185
|
-
}
|
|
186
|
-
throw error;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
91
|
+
invokeStep: createCaller( name ),
|
|
92
|
+
invokeSharedStep: createCaller( SHARED_STEP_PREFIX ),
|
|
93
|
+
invokeEvaluator: createCaller( name ),
|
|
94
|
+
invokeSharedEvaluator: createCaller( SHARED_STEP_PREFIX )
|
|
189
95
|
};
|
|
190
96
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
97
|
+
// The workflow function execution with "this" set with the dispatchers
|
|
98
|
+
const output = await fn.call( dispatchers, input, WorkflowContext.build() );
|
|
99
|
+
validator.validateOutput( output );
|
|
194
100
|
|
|
195
|
-
return
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
101
|
+
return {
|
|
102
|
+
[WORKFLOW_WRAPPER_VERSION_FIELD]: 1,
|
|
103
|
+
output,
|
|
104
|
+
...( traceDest && { trace: { destinations: traceDest } } )
|
|
105
|
+
};
|
|
106
|
+
} catch ( error ) {
|
|
107
|
+
if ( isRoot && traceDest ) {
|
|
108
|
+
// Append the trace destinations so it is carried to interceptor
|
|
109
|
+
error[METADATA_ACCESS_SYMBOL] = { trace: { destinations: traceDest } };
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
200
112
|
}
|
|
201
113
|
};
|
|
202
114
|
|