@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.
Files changed (77) hide show
  1. package/bin/worker.sh +6 -0
  2. package/package.json +1 -1
  3. package/src/consts.js +0 -4
  4. package/src/errors.js +6 -2
  5. package/src/interface/evaluator.js +7 -20
  6. package/src/interface/evaluator.spec.js +117 -1
  7. package/src/interface/step.js +8 -9
  8. package/src/interface/step.spec.js +124 -0
  9. package/src/interface/validations/index.js +108 -0
  10. package/src/interface/validations/index.spec.js +182 -0
  11. package/src/interface/validations/schemas.js +113 -0
  12. package/src/interface/validations/schemas.spec.js +209 -0
  13. package/src/interface/webhook.js +1 -1
  14. package/src/interface/webhook.spec.js +1 -1
  15. package/src/interface/workflow.d.ts +10 -9
  16. package/src/interface/workflow.js +76 -164
  17. package/src/interface/workflow.spec.js +637 -521
  18. package/src/interface/workflow_activity_options.js +16 -0
  19. package/src/interface/workflow_utils.js +1 -1
  20. package/src/interface/zod_integration.spec.js +2 -2
  21. package/src/internal_utils/aggregations.js +0 -10
  22. package/src/internal_utils/aggregations.spec.js +1 -48
  23. package/src/internal_utils/errors.js +14 -8
  24. package/src/internal_utils/errors.spec.js +73 -27
  25. package/src/utils/index.d.ts +19 -0
  26. package/src/utils/utils.js +53 -0
  27. package/src/utils/utils.spec.js +105 -1
  28. package/src/worker/bundle.js +26 -0
  29. package/src/worker/bundle.spec.js +53 -0
  30. package/src/worker/bundler_options.js +1 -1
  31. package/src/worker/bundler_options.spec.js +1 -1
  32. package/src/worker/catalog_workflow/catalog_job.js +148 -0
  33. package/src/worker/catalog_workflow/catalog_job.spec.js +232 -0
  34. package/src/worker/check.js +24 -0
  35. package/src/worker/connection_monitor.js +112 -0
  36. package/src/worker/connection_monitor.spec.js +199 -0
  37. package/src/worker/index.js +146 -41
  38. package/src/worker/index.spec.js +281 -109
  39. package/src/worker/interceptors/activity.js +7 -24
  40. package/src/worker/interceptors/activity.spec.js +97 -66
  41. package/src/worker/interceptors/index.js +4 -7
  42. package/src/worker/interceptors/modules.js +15 -0
  43. package/src/worker/interceptors/workflow.js +6 -8
  44. package/src/worker/interceptors/workflow.spec.js +49 -42
  45. package/src/worker/interruption.js +33 -0
  46. package/src/worker/interruption.spec.js +98 -0
  47. package/src/worker/loader/activities.js +75 -0
  48. package/src/worker/loader/activities.spec.js +213 -0
  49. package/src/worker/loader/hooks.js +28 -0
  50. package/src/worker/loader/hooks.spec.js +64 -0
  51. package/src/worker/loader/matchers.js +46 -0
  52. package/src/worker/loader/matchers.spec.js +140 -0
  53. package/src/worker/{loader_tools.js → loader/tools.js} +19 -67
  54. package/src/worker/{loader_tools.spec.js → loader/tools.spec.js} +53 -85
  55. package/src/worker/loader/workflows.js +82 -0
  56. package/src/worker/loader/workflows.spec.js +256 -0
  57. package/src/worker/{setup_telemetry.js → telemetry.js} +9 -4
  58. package/src/worker/{setup_telemetry.spec.js → telemetry.spec.js} +3 -3
  59. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +5 -109
  60. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +31 -103
  61. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +5 -6
  62. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +11 -83
  63. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +8 -11
  64. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +9 -9
  65. package/src/interface/validations/runtime.js +0 -20
  66. package/src/interface/validations/runtime.spec.js +0 -29
  67. package/src/interface/validations/schema_utils.js +0 -8
  68. package/src/interface/validations/schema_utils.spec.js +0 -67
  69. package/src/interface/validations/static.js +0 -137
  70. package/src/interface/validations/static.spec.js +0 -397
  71. package/src/interface/workflow.replay_compatibility.spec.js +0 -254
  72. package/src/worker/loader.js +0 -202
  73. package/src/worker/loader.spec.js +0 -498
  74. package/src/worker/shutdown.js +0 -26
  75. package/src/worker/shutdown.spec.js +0 -82
  76. package/src/worker/start_catalog.js +0 -96
  77. 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
+ } );
@@ -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/static.js';
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.
@@ -6,7 +6,7 @@ vi.mock( '#consts', () => ( {
6
6
  } ) );
7
7
 
8
8
  const validateRequestPayloadMock = vi.fn();
9
- vi.mock( './validations/static.js', () => ( {
9
+ vi.mock( './validations/index.js', () => ( {
10
10
  validateRequestPayload: validateRequestPayloadMock
11
11
  } ) );
12
12
 
@@ -79,20 +79,21 @@ export type WorkflowContext<
79
79
  };
80
80
 
81
81
  /**
82
- * Configuration for workflow invocations.
82
+ * Options for workflow invocations.
83
83
  *
84
84
  * Allows overriding Temporal Activity options for this workflow.
85
85
  */
86
- export type WorkflowInvocationConfiguration<Context extends WorkflowContext = WorkflowContext> = {
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
- options?: TemporalActivityOptions,
91
+ activityOptions?: TemporalActivityOptions,
92
92
 
93
93
  /**
94
- * Configures whether this workflow runs detached.
95
- * Detached workflows called without explicitly awaiting the result are "fire-and-forget" and may outlive the parent.
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 WorkflowInvocationConfiguration object, allowing workflows configuration overwrite.
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 config - Additional configuration for the invocation.
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, config?: WorkflowInvocationConfiguration ) =>
154
+ ( input?: undefined | null, options?: WorkflowInvocationOptions ) =>
154
155
  ReturnType<WorkflowFunction> :
155
- ( input: Parameters<WorkflowFunction>[0], config?: WorkflowInvocationConfiguration ) =>
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 { defineSignal, setHandler } from '@temporalio/workflow';
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
- const defaultOptions = {
22
- activityOptions: {
23
- startToCloseTimeout: '20m',
24
- heartbeatTimeout: '5m',
25
- retry: {
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
- * Checks if the activity result uses the internal wrapper
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 isActivityResultWrapped = result => result?.[ACTIVITY_WRAPPER_VERSION_FIELD] > 0;
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
- validateWorkflow( { name, description, inputSchema, outputSchema, fn, options, aliases } );
32
+ WorkflowValidator.validateDefinition( { name, description, inputSchema, outputSchema, fn, options, aliases } );
43
33
 
44
34
  const { disableTrace, activityOptions } = deepMerge( defaultOptions, options );
45
- const steps = proxyActivities( activityOptions );
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
- validateWithSchema( inputSchema, input, `Workflow ${name} input` );
58
- const output = await fn( input, deepMerge( WorkflowContext.build(), extra.context ) );
59
- validateWithSchema( outputSchema, output, `Workflow ${name} output` );
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
- const context = WorkflowContext.build();
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
- // Creates the immutable memo that will be used by all nested workflows/activities
69
- // Preserves all info that already exists in the memo object, so new child workflows inherit this
70
- Object.assign( memo, {
71
- traceInfo: memo.traceInfo ?? TraceInfo.build( { disableTrace } ),
72
- activityOptions: memo.activityOptions ?? activityOptions
73
- } );
74
-
75
- /**
76
- * Run the internal activity to retrieve the workflow trace destinations
77
- * This only happens at the root workflow because nested share the same trace file
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
- // validation comes after setting memo to have that info already set for interceptor even if validations fail
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
- /* This are shortcuts to invoke activities as steps/evaluators both shared and non shared */
141
- invokeStep: async ( stepName, input, options ) =>
142
- callStepAndUnwrapResult( steps[`${name}#${stepName}`], input, options ),
143
- invokeSharedStep: async ( stepName, input, options ) =>
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
- workflowResult.output = await fn.call( dispatchers, input, context );
192
-
193
- validateWithSchema( outputSchema, workflowResult.output, `Workflow ${name} output` );
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 workflowResult;
196
- } catch ( e ) {
197
- // Append the result as metadata of the error, so it can be read by the interceptor.
198
- e[METADATA_ACCESS_SYMBOL] = { ...( e[METADATA_ACCESS_SYMBOL] ?? {} ), ...workflowResult };
199
- throw e;
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