@outputai/core 0.1.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 (114) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +11 -0
  3. package/bin/healthcheck.mjs +36 -0
  4. package/bin/healthcheck.spec.js +90 -0
  5. package/bin/worker.sh +26 -0
  6. package/package.json +67 -0
  7. package/src/activity_integration/context.d.ts +27 -0
  8. package/src/activity_integration/context.js +17 -0
  9. package/src/activity_integration/context.spec.js +42 -0
  10. package/src/activity_integration/events.d.ts +7 -0
  11. package/src/activity_integration/events.js +10 -0
  12. package/src/activity_integration/index.d.ts +9 -0
  13. package/src/activity_integration/index.js +3 -0
  14. package/src/activity_integration/tracing.d.ts +32 -0
  15. package/src/activity_integration/tracing.js +37 -0
  16. package/src/async_storage.js +19 -0
  17. package/src/bus.js +3 -0
  18. package/src/consts.js +32 -0
  19. package/src/errors.d.ts +15 -0
  20. package/src/errors.js +14 -0
  21. package/src/hooks/index.d.ts +28 -0
  22. package/src/hooks/index.js +32 -0
  23. package/src/index.d.ts +49 -0
  24. package/src/index.js +4 -0
  25. package/src/interface/evaluation_result.d.ts +173 -0
  26. package/src/interface/evaluation_result.js +215 -0
  27. package/src/interface/evaluator.d.ts +70 -0
  28. package/src/interface/evaluator.js +34 -0
  29. package/src/interface/evaluator.spec.js +565 -0
  30. package/src/interface/index.d.ts +9 -0
  31. package/src/interface/index.js +26 -0
  32. package/src/interface/step.d.ts +138 -0
  33. package/src/interface/step.js +22 -0
  34. package/src/interface/types.d.ts +27 -0
  35. package/src/interface/validations/runtime.js +20 -0
  36. package/src/interface/validations/runtime.spec.js +29 -0
  37. package/src/interface/validations/schema_utils.js +8 -0
  38. package/src/interface/validations/schema_utils.spec.js +67 -0
  39. package/src/interface/validations/static.js +136 -0
  40. package/src/interface/validations/static.spec.js +366 -0
  41. package/src/interface/webhook.d.ts +84 -0
  42. package/src/interface/webhook.js +64 -0
  43. package/src/interface/webhook.spec.js +122 -0
  44. package/src/interface/workflow.d.ts +273 -0
  45. package/src/interface/workflow.js +128 -0
  46. package/src/interface/workflow.spec.js +467 -0
  47. package/src/interface/workflow_context.js +31 -0
  48. package/src/interface/workflow_utils.d.ts +76 -0
  49. package/src/interface/workflow_utils.js +50 -0
  50. package/src/interface/workflow_utils.spec.js +190 -0
  51. package/src/interface/zod_integration.spec.js +646 -0
  52. package/src/internal_activities/index.js +66 -0
  53. package/src/internal_activities/index.spec.js +102 -0
  54. package/src/logger.js +73 -0
  55. package/src/tracing/internal_interface.js +71 -0
  56. package/src/tracing/processors/local/index.js +111 -0
  57. package/src/tracing/processors/local/index.spec.js +149 -0
  58. package/src/tracing/processors/s3/configs.js +31 -0
  59. package/src/tracing/processors/s3/configs.spec.js +64 -0
  60. package/src/tracing/processors/s3/index.js +114 -0
  61. package/src/tracing/processors/s3/index.spec.js +153 -0
  62. package/src/tracing/processors/s3/redis_client.js +62 -0
  63. package/src/tracing/processors/s3/redis_client.spec.js +185 -0
  64. package/src/tracing/processors/s3/s3_client.js +27 -0
  65. package/src/tracing/processors/s3/s3_client.spec.js +62 -0
  66. package/src/tracing/tools/build_trace_tree.js +83 -0
  67. package/src/tracing/tools/build_trace_tree.spec.js +135 -0
  68. package/src/tracing/tools/utils.js +21 -0
  69. package/src/tracing/tools/utils.spec.js +14 -0
  70. package/src/tracing/trace_engine.js +97 -0
  71. package/src/tracing/trace_engine.spec.js +199 -0
  72. package/src/utils/index.d.ts +134 -0
  73. package/src/utils/index.js +2 -0
  74. package/src/utils/resolve_invocation_dir.js +34 -0
  75. package/src/utils/resolve_invocation_dir.spec.js +102 -0
  76. package/src/utils/utils.js +211 -0
  77. package/src/utils/utils.spec.js +448 -0
  78. package/src/worker/bundler_options.js +43 -0
  79. package/src/worker/catalog_workflow/catalog.js +114 -0
  80. package/src/worker/catalog_workflow/index.js +54 -0
  81. package/src/worker/catalog_workflow/index.spec.js +196 -0
  82. package/src/worker/catalog_workflow/workflow.js +24 -0
  83. package/src/worker/configs.js +49 -0
  84. package/src/worker/configs.spec.js +130 -0
  85. package/src/worker/index.js +89 -0
  86. package/src/worker/index.spec.js +177 -0
  87. package/src/worker/interceptors/activity.js +62 -0
  88. package/src/worker/interceptors/activity.spec.js +212 -0
  89. package/src/worker/interceptors/workflow.js +70 -0
  90. package/src/worker/interceptors/workflow.spec.js +167 -0
  91. package/src/worker/interceptors.js +10 -0
  92. package/src/worker/loader.js +151 -0
  93. package/src/worker/loader.spec.js +236 -0
  94. package/src/worker/loader_tools.js +132 -0
  95. package/src/worker/loader_tools.spec.js +156 -0
  96. package/src/worker/log_hooks.js +95 -0
  97. package/src/worker/log_hooks.spec.js +217 -0
  98. package/src/worker/sandboxed_utils.js +18 -0
  99. package/src/worker/shutdown.js +26 -0
  100. package/src/worker/shutdown.spec.js +82 -0
  101. package/src/worker/sinks.js +74 -0
  102. package/src/worker/start_catalog.js +36 -0
  103. package/src/worker/start_catalog.spec.js +118 -0
  104. package/src/worker/webpack_loaders/consts.js +9 -0
  105. package/src/worker/webpack_loaders/tools.js +548 -0
  106. package/src/worker/webpack_loaders/tools.spec.js +330 -0
  107. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +221 -0
  108. package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +336 -0
  109. package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +61 -0
  110. package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +216 -0
  111. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +196 -0
  112. package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +123 -0
  113. package/src/worker/webpack_loaders/workflow_validator/index.mjs +205 -0
  114. package/src/worker/webpack_loaders/workflow_validator/index.spec.js +613 -0
@@ -0,0 +1,467 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { z } from 'zod';
3
+
4
+ const inWorkflowContextMock = vi.hoisted( () => vi.fn( () => true ) );
5
+ const traceDestinationsStepMock = vi.fn().mockResolvedValue( { local: '/tmp/trace' } );
6
+ const executeChildMock = vi.fn().mockResolvedValue( undefined );
7
+ const continueAsNewMock = vi.fn().mockResolvedValue( undefined );
8
+
9
+ const createStepsProxy = ( stepSpy = vi.fn() ) =>
10
+ new Proxy( {}, {
11
+ get: ( _, prop ) => {
12
+ if ( prop === '__internal#getTraceDestinations' ) {
13
+ return traceDestinationsStepMock;
14
+ }
15
+ if ( typeof prop === 'string' && ( prop.includes( '#' ) ) ) {
16
+ return stepSpy;
17
+ }
18
+ return vi.fn();
19
+ }
20
+ } );
21
+
22
+ const stepSpyRef = { current: vi.fn().mockResolvedValue( {} ) };
23
+ const proxyActivitiesMock = vi.fn( () => {
24
+ stepSpyRef.current = vi.fn().mockResolvedValue( {} );
25
+ return createStepsProxy( stepSpyRef.current );
26
+ } );
27
+
28
+ const workflowInfoReturn = {
29
+ workflowId: 'wf-test-123',
30
+ workflowType: 'test_wf',
31
+ memo: {},
32
+ startTime: new Date( '2025-01-01T00:00:00Z' ),
33
+ continueAsNewSuggested: false
34
+ };
35
+ const workflowInfoMock = vi.fn( () => ( { ...workflowInfoReturn } ) );
36
+
37
+ vi.mock( '@temporalio/workflow', () => ( {
38
+ proxyActivities: ( ...args ) => proxyActivitiesMock( ...args ),
39
+ inWorkflowContext: inWorkflowContextMock,
40
+ executeChild: ( ...args ) => executeChildMock( ...args ),
41
+ workflowInfo: workflowInfoMock,
42
+ uuid4: () => '550e8400e29b41d4a716446655440000',
43
+ ParentClosePolicy: { TERMINATE: 'TERMINATE', ABANDON: 'ABANDON' },
44
+ continueAsNew: continueAsNewMock
45
+ } ) );
46
+
47
+ vi.mock( '#consts', async importOriginal => {
48
+ const actual = await importOriginal();
49
+ return {
50
+ ...actual,
51
+ SHARED_STEP_PREFIX: '__shared',
52
+ ACTIVITY_GET_TRACE_DESTINATIONS: '__internal#getTraceDestinations'
53
+ };
54
+ } );
55
+
56
+ describe( 'workflow()', () => {
57
+ beforeEach( () => {
58
+ vi.clearAllMocks();
59
+ inWorkflowContextMock.mockReturnValue( true );
60
+ workflowInfoMock.mockReturnValue( { ...workflowInfoReturn } );
61
+ workflowInfoReturn.memo = {};
62
+ proxyActivitiesMock.mockImplementation( () => {
63
+ stepSpyRef.current = vi.fn().mockResolvedValue( {} );
64
+ return createStepsProxy( stepSpyRef.current );
65
+ } );
66
+ } );
67
+
68
+ describe( 'options and defaults', () => {
69
+ it( 'does not throw when options is omitted (disableTrace defaults to false)', async () => {
70
+ const { workflow } = await import( './workflow.js' );
71
+
72
+ const wf = workflow( {
73
+ name: 'no_options_wf',
74
+ description: 'Workflow without options',
75
+ inputSchema: z.object( { value: z.string() } ),
76
+ outputSchema: z.object( { value: z.string() } ),
77
+ fn: async ( { value } ) => ( { value } )
78
+ } );
79
+
80
+ const result = await wf( { value: 'hello' } );
81
+ expect( result.output ).toEqual( { value: 'hello' } );
82
+ } );
83
+
84
+ it( 'respects disableTrace: true when options is provided', async () => {
85
+ const { workflow } = await import( './workflow.js' );
86
+
87
+ const wf = workflow( {
88
+ name: 'trace_disabled_wf',
89
+ description: 'Workflow with tracing disabled',
90
+ inputSchema: z.object( { value: z.string() } ),
91
+ outputSchema: z.object( { value: z.string() } ),
92
+ options: { disableTrace: true },
93
+ fn: async ( { value } ) => ( { value } )
94
+ } );
95
+
96
+ const result = await wf( { value: 'hello' } );
97
+ expect( result.output ).toEqual( { value: 'hello' } );
98
+ } );
99
+
100
+ it( 'merges custom activityOptions with defaults via deepMerge', async () => {
101
+ const { workflow } = await import( './workflow.js' );
102
+
103
+ workflow( {
104
+ name: 'custom_activity_wf',
105
+ description: 'Workflow with custom activity options',
106
+ inputSchema: z.object( {} ),
107
+ outputSchema: z.object( {} ),
108
+ options: {
109
+ activityOptions: {
110
+ startToCloseTimeout: '5m',
111
+ retry: { maximumAttempts: 5 }
112
+ }
113
+ },
114
+ fn: async () => ( {} )
115
+ } );
116
+
117
+ expect( proxyActivitiesMock ).toHaveBeenCalledWith(
118
+ expect.objectContaining( {
119
+ startToCloseTimeout: '5m',
120
+ retry: expect.objectContaining( { maximumAttempts: 5 } )
121
+ } )
122
+ );
123
+ } );
124
+ } );
125
+
126
+ describe( 'wrapper metadata', () => {
127
+ it( 'attaches name, description, inputSchema, outputSchema to wrapper via setMetadata', async () => {
128
+ const { workflow } = await import( './workflow.js' );
129
+ const inputSchema = z.object( { x: z.number() } );
130
+ const outputSchema = z.object( { y: z.number() } );
131
+
132
+ const wf = workflow( {
133
+ name: 'meta_wf',
134
+ description: 'Meta workflow',
135
+ inputSchema,
136
+ outputSchema,
137
+ fn: async input => ( { y: input.x } )
138
+ } );
139
+
140
+ const symbols = Object.getOwnPropertySymbols( wf );
141
+ expect( symbols ).toHaveLength( 1 );
142
+ const meta = wf[symbols[0]];
143
+ expect( meta ).toEqual( { name: 'meta_wf', description: 'Meta workflow', inputSchema, outputSchema } );
144
+ } );
145
+ } );
146
+
147
+ describe( 'when not in workflow context (unit-test path)', () => {
148
+ it( 'validates input, runs fn with test context, validates output, returns plain output', async () => {
149
+ inWorkflowContextMock.mockReturnValue( false );
150
+ const { workflow } = await import( './workflow.js' );
151
+
152
+ const wf = workflow( {
153
+ name: 'unit_path_wf',
154
+ description: 'Unit path',
155
+ inputSchema: z.object( { a: z.string() } ),
156
+ outputSchema: z.object( { b: z.string() } ),
157
+ fn: async ( input, context ) => ( {
158
+ b: String( context.info.workflowId ) + input.a
159
+ } )
160
+ } );
161
+
162
+ const result = await wf( { a: '-ok' } );
163
+ expect( result ).toEqual( { b: 'test-workflow-ok' } );
164
+ expect( workflowInfoMock ).not.toHaveBeenCalled();
165
+ expect( traceDestinationsStepMock ).not.toHaveBeenCalled();
166
+ } );
167
+
168
+ it( 'merges extra.context into context when provided', async () => {
169
+ inWorkflowContextMock.mockReturnValue( false );
170
+ const { workflow } = await import( './workflow.js' );
171
+
172
+ const wf = workflow( {
173
+ name: 'extra_ctx_wf',
174
+ description: 'Extra context',
175
+ inputSchema: z.object( {} ),
176
+ outputSchema: z.object( { id: z.string() } ),
177
+ fn: async ( _, context ) => ( { id: context.extraId ?? 'default' } )
178
+ } );
179
+
180
+ const result = await wf( {}, { context: { extraId: 'injected' } } );
181
+ expect( result ).toEqual( { id: 'injected' } );
182
+ } );
183
+ } );
184
+
185
+ describe( 'input and output validation', () => {
186
+ it( 'throws ValidationError when input does not match inputSchema', async () => {
187
+ const { workflow } = await import( './workflow.js' );
188
+ const { ValidationError } = await import( '#errors' );
189
+
190
+ const wf = workflow( {
191
+ name: 'validate_in_wf',
192
+ description: 'Input validation',
193
+ inputSchema: z.object( { required: z.string() } ),
194
+ outputSchema: z.object( {} ),
195
+ fn: async () => ( {} )
196
+ } );
197
+
198
+ await expect( wf( { wrong: 1 } ) ).rejects.toThrow( ValidationError );
199
+ await expect( wf( { wrong: 1 } ) ).rejects.toThrow( /Workflow validate_in_wf input/ );
200
+ } );
201
+
202
+ it( 'throws ValidationError when output does not match outputSchema', async () => {
203
+ const { workflow } = await import( './workflow.js' );
204
+ const { ValidationError } = await import( '#errors' );
205
+
206
+ const wf = workflow( {
207
+ name: 'validate_out_wf',
208
+ description: 'Output validation',
209
+ inputSchema: z.object( {} ),
210
+ outputSchema: z.object( { required: z.string() } ),
211
+ fn: async () => ( { other: 1 } )
212
+ } );
213
+
214
+ await expect( wf( {} ) ).rejects.toThrow( ValidationError );
215
+ await expect( wf( {} ) ).rejects.toThrow( /Workflow validate_out_wf output/ );
216
+ } );
217
+ } );
218
+
219
+ describe( 'root workflow (in workflow context)', () => {
220
+ it( 'calls getTraceDestinations, returns { output, trace } and assigns executionContext to memo', async () => {
221
+ const { workflow } = await import( './workflow.js' );
222
+
223
+ const wf = workflow( {
224
+ name: 'root_wf',
225
+ description: 'Root',
226
+ inputSchema: z.object( {} ),
227
+ outputSchema: z.object( { v: z.number() } ),
228
+ fn: async () => ( { v: 42 } )
229
+ } );
230
+
231
+ const result = await wf( {} );
232
+ expect( traceDestinationsStepMock ).toHaveBeenCalledTimes( 1 );
233
+ expect( result ).toEqual( {
234
+ output: { v: 42 },
235
+ trace: { destinations: { local: '/tmp/trace' } }
236
+ } );
237
+ const memo = workflowInfoMock().memo;
238
+ expect( memo.executionContext ).toEqual( {
239
+ workflowId: 'wf-test-123',
240
+ workflowName: 'root_wf',
241
+ disableTrace: false,
242
+ startTime: new Date( '2025-01-01T00:00:00Z' ).getTime()
243
+ } );
244
+ } );
245
+
246
+ it( 'sets executionContext.disableTrace when options.disableTrace is true', async () => {
247
+ const { workflow } = await import( './workflow.js' );
248
+
249
+ const wf = workflow( {
250
+ name: 'root_no_trace_wf',
251
+ description: 'Root no trace',
252
+ inputSchema: z.object( {} ),
253
+ outputSchema: z.object( {} ),
254
+ options: { disableTrace: true },
255
+ fn: async () => ( {} )
256
+ } );
257
+
258
+ await wf( {} );
259
+ expect( workflowInfoMock().memo.executionContext.disableTrace ).toBe( true );
260
+ } );
261
+ } );
262
+
263
+ describe( 'child workflow (memo.executionContext already set)', () => {
264
+ it( 'does not call getTraceDestinations and returns plain output', async () => {
265
+ workflowInfoMock.mockReturnValue( {
266
+ ...workflowInfoReturn,
267
+ memo: { executionContext: { workflowId: 'parent-1', workflowName: 'parent_wf' } }
268
+ } );
269
+ const { workflow } = await import( './workflow.js' );
270
+
271
+ const wf = workflow( {
272
+ name: 'child_wf',
273
+ description: 'Child',
274
+ inputSchema: z.object( {} ),
275
+ outputSchema: z.object( { x: z.string() } ),
276
+ fn: async () => ( { x: 'child' } )
277
+ } );
278
+
279
+ const result = await wf( {} );
280
+ expect( traceDestinationsStepMock ).not.toHaveBeenCalled();
281
+ expect( result ).toEqual( { x: 'child' } );
282
+ } );
283
+ } );
284
+
285
+ describe( 'bound this: invokeStep, invokeSharedStep, invokeEvaluator', () => {
286
+ it( 'invokeStep calls steps with workflowName#stepName', async () => {
287
+ const getCalls = [];
288
+ proxyActivitiesMock.mockImplementation( () => new Proxy( {}, {
289
+ get: ( _, prop ) => {
290
+ if ( prop === '__internal#getTraceDestinations' ) {
291
+ return traceDestinationsStepMock;
292
+ }
293
+ if ( typeof prop === 'string' && prop.includes( '#' ) ) {
294
+ getCalls.push( prop );
295
+ return vi.fn().mockResolvedValue( {} );
296
+ }
297
+ return vi.fn();
298
+ }
299
+ } ) );
300
+
301
+ const { workflow } = await import( './workflow.js' );
302
+
303
+ const wf = workflow( {
304
+ name: 'invoke_wf',
305
+ description: 'Invoke',
306
+ inputSchema: z.object( {} ),
307
+ outputSchema: z.object( {} ),
308
+ async fn() {
309
+ await this.invokeStep( 'myStep', { foo: 1 } );
310
+ return {};
311
+ }
312
+ } );
313
+
314
+ await wf( {} );
315
+ expect( getCalls ).toContain( 'invoke_wf#myStep' );
316
+ } );
317
+
318
+ it( 'invokeSharedStep calls steps with SHARED_STEP_PREFIX#stepName', async () => {
319
+ const { workflow } = await import( './workflow.js' );
320
+ const sharedSpy = vi.fn().mockResolvedValue( {} );
321
+ proxyActivitiesMock.mockImplementation( () => new Proxy( {}, {
322
+ get: ( _, prop ) => {
323
+ if ( prop === '__internal#getTraceDestinations' ) {
324
+ return traceDestinationsStepMock;
325
+ }
326
+ if ( prop === '__shared#sharedStep' ) {
327
+ return sharedSpy;
328
+ }
329
+ return vi.fn();
330
+ }
331
+ } ) );
332
+
333
+ const wf = workflow( {
334
+ name: 'shared_wf',
335
+ description: 'Shared',
336
+ inputSchema: z.object( {} ),
337
+ outputSchema: z.object( {} ),
338
+ async fn() {
339
+ await this.invokeSharedStep( 'sharedStep', { data: 2 } );
340
+ return {};
341
+ }
342
+ } );
343
+
344
+ await wf( {} );
345
+ expect( sharedSpy ).toHaveBeenCalledWith( { data: 2 }, undefined );
346
+ } );
347
+
348
+ it( 'invokeEvaluator calls steps with workflowName#evaluatorName', async () => {
349
+ const evalSpy = vi.fn().mockResolvedValue( true );
350
+ proxyActivitiesMock.mockImplementation( () => new Proxy( {}, {
351
+ get: ( _, prop ) => {
352
+ if ( prop === '__internal#getTraceDestinations' ) {
353
+ return traceDestinationsStepMock;
354
+ }
355
+ if ( prop === 'eval_wf#myEvaluator' ) {
356
+ return evalSpy;
357
+ }
358
+ return vi.fn();
359
+ }
360
+ } ) );
361
+
362
+ const { workflow } = await import( './workflow.js' );
363
+
364
+ const wf = workflow( {
365
+ name: 'eval_wf',
366
+ description: 'Eval',
367
+ inputSchema: z.object( {} ),
368
+ outputSchema: z.object( {} ),
369
+ async fn() {
370
+ await this.invokeEvaluator( 'myEvaluator', { x: 3 } );
371
+ return {};
372
+ }
373
+ } );
374
+
375
+ await wf( {} );
376
+ expect( evalSpy ).toHaveBeenCalledWith( { x: 3 }, undefined );
377
+ } );
378
+ } );
379
+
380
+ describe( 'startWorkflow', () => {
381
+ it( 'calls executeChild with correct args and TERMINATE when not detached', async () => {
382
+ const { workflow } = await import( './workflow.js' );
383
+ const { ParentClosePolicy } = await import( '@temporalio/workflow' );
384
+
385
+ const wf = workflow( {
386
+ name: 'parent_wf',
387
+ description: 'Parent',
388
+ inputSchema: z.object( {} ),
389
+ outputSchema: z.object( {} ),
390
+ async fn() {
391
+ await this.startWorkflow( 'child_wf', { id: 1 } );
392
+ return {};
393
+ }
394
+ } );
395
+
396
+ await wf( {} );
397
+ expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', {
398
+ args: [ { id: 1 } ],
399
+ workflowId: expect.stringMatching( /^wf-test-123-/ ),
400
+ parentClosePolicy: ParentClosePolicy.TERMINATE,
401
+ memo: expect.objectContaining( {
402
+ executionContext: expect.any( Object ),
403
+ parentId: 'wf-test-123'
404
+ } )
405
+ } );
406
+ } );
407
+
408
+ it( 'uses ABANDON when extra.detached is true', async () => {
409
+ const { workflow } = await import( './workflow.js' );
410
+ const { ParentClosePolicy } = await import( '@temporalio/workflow' );
411
+
412
+ const wf = workflow( {
413
+ name: 'detach_wf',
414
+ description: 'Detach',
415
+ inputSchema: z.object( {} ),
416
+ outputSchema: z.object( {} ),
417
+ async fn() {
418
+ await this.startWorkflow( 'child_wf', null, { detached: true } );
419
+ return {};
420
+ }
421
+ } );
422
+
423
+ await wf( {} );
424
+ expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', expect.objectContaining( {
425
+ parentClosePolicy: ParentClosePolicy.ABANDON
426
+ } ) );
427
+ } );
428
+
429
+ it( 'passes empty args when input is null/omitted', async () => {
430
+ const { workflow } = await import( './workflow.js' );
431
+
432
+ const wf = workflow( {
433
+ name: 'no_input_wf',
434
+ description: 'No input',
435
+ inputSchema: z.object( {} ),
436
+ outputSchema: z.object( {} ),
437
+ async fn() {
438
+ await this.startWorkflow( 'child_wf' );
439
+ return {};
440
+ }
441
+ } );
442
+
443
+ await wf( {} );
444
+ expect( executeChildMock ).toHaveBeenCalledWith( 'child_wf', expect.objectContaining( {
445
+ args: []
446
+ } ) );
447
+ } );
448
+ } );
449
+
450
+ describe( 'error handling (root workflow)', () => {
451
+ it( 'rethrows error from fn and rejects with same message', async () => {
452
+ const { workflow } = await import( './workflow.js' );
453
+
454
+ const wf = workflow( {
455
+ name: 'err_wf',
456
+ description: 'Error',
457
+ inputSchema: z.object( {} ),
458
+ outputSchema: z.object( {} ),
459
+ fn: async () => {
460
+ throw new Error( 'workflow failed' );
461
+ }
462
+ } );
463
+
464
+ await expect( wf( {} ) ).rejects.toThrow( 'workflow failed' );
465
+ } );
466
+ } );
467
+ } );
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Context instance builder
3
+ */
4
+ export class Context {
5
+
6
+ /**
7
+ * Builds a new context instance
8
+ * @param {object} options - Arguments to build a new context instance
9
+ * @param {string} workflowId
10
+ * @param {function} continueAsNew
11
+ * @param {function} isContinueAsNewSuggested
12
+ * @returns {object} context
13
+ */
14
+ static build( { workflowId, continueAsNew, isContinueAsNewSuggested } ) {
15
+ return {
16
+ /**
17
+ * Control namespace: This object adds functions to interact with Temporal flow mechanisms
18
+ */
19
+ control: {
20
+ continueAsNew,
21
+ isContinueAsNewSuggested
22
+ },
23
+ /**
24
+ * Info namespace: abstracts workflowInfo()
25
+ */
26
+ info: {
27
+ workflowId
28
+ }
29
+ };
30
+ }
31
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Result of a single job executed by `executeInParallel`.
3
+ *
4
+ * @typeParam T - The return type of the job function
5
+ */
6
+ export type ParallelJobResult<T> =
7
+ | { ok: true; result: T; index: number } |
8
+ { ok: false; error: Error; index: number };
9
+
10
+ // For a single job function type F, produce ParallelJobResult<R> where R is the resolved return type.
11
+ // We use ( ...args: never[] ) => so we match any no-arg callable; Promise<infer R> | infer R lets us
12
+ // infer R from either a Promise or a plain value. Awaited<R> normalizes so we always get the
13
+ // unwrapped type (e.g. Awaited<Promise<X>> = X).
14
+ type InferJobResult<F> = F extends ( ...args: never[] ) => Promise<infer R> | infer R ?
15
+ ParallelJobResult<Awaited<R>> :
16
+ never;
17
+
18
+ // Map the jobs tuple T to a tuple of result types: result[i] has the type for jobs[i]'s return.
19
+ // Constraint "readonly ( () => Promise<unknown> | unknown )[]" does two things: (1) T must be an
20
+ // array/tuple so we can map over keyof T (indices). (2) "readonly" preserves tuple inference when
21
+ // you pass a literal like [ () => a(), () => b() ] — without it, T can be inferred as a mutable
22
+ // array and we lose per-index types (all results would get the same element type).
23
+ type ParallelResults<T extends readonly ( () => Promise<unknown> | unknown )[]> = {
24
+ [K in keyof T]: InferJobResult<T[K]>;
25
+ };
26
+
27
+ /**
28
+ * Execute jobs in parallel with optional concurrency limit.
29
+ *
30
+ * Returns all job results (successes and failures) sorted by original job index.
31
+ * Each result contains `ok` (boolean), `index` (original position), and either
32
+ * `result` (on success) or `error` (on failure).
33
+ *
34
+ * Jobs must be wrapped in arrow functions—do not pass promises directly.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const results = await executeInParallel( {
39
+ * jobs: [
40
+ * () => myStep( data1 ),
41
+ * () => myStep( data2 ),
42
+ * () => myStep( data3 )
43
+ * ],
44
+ * concurrency: 2
45
+ * } );
46
+ *
47
+ * // Handle the discriminated union (result only exists when ok is true)
48
+ * const successfulResults = results.filter( r => r.ok ).map( r => r.result );
49
+ *
50
+ * // Or handle each result individually
51
+ * for ( const r of results ) {
52
+ * if ( r.ok ) {
53
+ * console.log( `Job ${r.index} succeeded:`, r.result );
54
+ * } else {
55
+ * console.log( `Job ${r.index} failed:`, r.error );
56
+ * }
57
+ * }
58
+ * ```
59
+ *
60
+ * @param params - Parameters object
61
+ * @param params.jobs - Array of arrow functions returning step/activity calls (not promises directly)
62
+ * @param params.concurrency - Max concurrent jobs (default: Infinity)
63
+ * @param params.onJobCompleted - Optional callback invoked as each job completes (in completion order)
64
+ * @returns Array of results sorted by original job index
65
+ */
66
+ // T extends readonly (...)[] so T is inferred as a tuple when a literal array is passed, giving
67
+ // per-index result types. onJobCompleted gets ParallelResults<T>[number] (union of all result
68
+ // types); when all jobs return the same type that union is a single type, so the callback is
69
+ // (result: ParallelJobResult<ThatType>) => void. When jobs have mixed return types it's a union.
70
+ export declare function executeInParallel<T extends readonly ( () => Promise<unknown> | unknown )[]>(
71
+ params: {
72
+ jobs: T;
73
+ concurrency?: number;
74
+ onJobCompleted?: ( result: ParallelResults<T>[number] ) => void;
75
+ }
76
+ ): Promise<ParallelResults<T>>;
@@ -0,0 +1,50 @@
1
+ // THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
2
+ import { validateExecuteInParallel } from './validations/static.js';
3
+
4
+ /**
5
+ * Execute jobs in parallel with optional concurrency limit.
6
+ *
7
+ * Returns all job results (successes and failures) sorted by original job index.
8
+ *
9
+ * @param {Array<Function>} jobs Array of functions to execute
10
+ * @param {Number} [concurrency] Max concurrent jobs, default is Infinity (no concurrency limit)
11
+ * @param {Function} [onJobCompleted] Optional callback invoked as each job completes
12
+ */
13
+ export const executeInParallel = async ( { jobs, concurrency = Infinity, onJobCompleted } ) => {
14
+ validateExecuteInParallel( { jobs, concurrency, onJobCompleted } );
15
+ // allows this function to be called without testing over and over to check if it is not null;
16
+ const onJobCompletedSafeCb = onJobCompleted ?? ( _ => 0 );
17
+ const results = [];
18
+ const jobsCount = jobs.length;
19
+ const jobsPool = jobs.slice().map( ( job, index ) => ( {
20
+ index,
21
+ fn: async () => {
22
+ try {
23
+ const result = await job();
24
+ return { ok: true, result, index };
25
+ } catch ( error ) {
26
+ return { ok: false, error, index };
27
+ }
28
+ },
29
+ promise: null
30
+ } ) );
31
+
32
+ const activeJobs = jobsPool.splice( 0, concurrency );
33
+ activeJobs.forEach( job => job.promise = job.fn() ); // start jobs
34
+
35
+ while ( results.length < jobsCount ) {
36
+ const result = await Promise.race( activeJobs.map( job => job.promise ) );
37
+ results.push( result );
38
+ onJobCompletedSafeCb( result );
39
+
40
+ activeJobs.splice( activeJobs.findIndex( job => job.index === result.index ), 1 ); // remove completed job
41
+
42
+ if ( jobsPool.length > 0 ) {
43
+ const nextJob = jobsPool.shift();
44
+ nextJob.promise = nextJob.fn();
45
+ activeJobs.push( nextJob );
46
+ }
47
+ }
48
+
49
+ return results.sort( ( a, b ) => a.index - b.index );
50
+ };