@outputai/core 0.7.1-next.de30052.0 → 0.7.1-next.ed233ce.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 +86 -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,16 @@
|
|
|
1
|
+
import { FatalError, ValidationError } from '#errors';
|
|
2
|
+
|
|
3
|
+
export const defaultOptions = {
|
|
4
|
+
activityOptions: {
|
|
5
|
+
startToCloseTimeout: '20m',
|
|
6
|
+
heartbeatTimeout: '5m',
|
|
7
|
+
retry: {
|
|
8
|
+
initialInterval: '10s',
|
|
9
|
+
backoffCoefficient: 2.0,
|
|
10
|
+
maximumInterval: '2m',
|
|
11
|
+
maximumAttempts: 3,
|
|
12
|
+
nonRetryableErrorTypes: [ ValidationError.name, FatalError.name ]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
disableTrace: false
|
|
16
|
+
};
|
|
@@ -495,7 +495,7 @@ describe( 'Zod Schema Integration Tests', () => {
|
|
|
495
495
|
await errorStep( { age: 16, email: 'invalid' } );
|
|
496
496
|
expect.fail( 'Should have thrown an error' );
|
|
497
497
|
} catch ( error ) {
|
|
498
|
-
expect( error.message ).toContain( 'Step error_test input validation failed' );
|
|
498
|
+
expect( error.message ).toContain( 'Step "error_test" input validation failed' );
|
|
499
499
|
}
|
|
500
500
|
} );
|
|
501
501
|
|
|
@@ -527,7 +527,7 @@ describe( 'Zod Schema Integration Tests', () => {
|
|
|
527
527
|
const metadata = testStep[METADATA_ACCESS_SYMBOL];
|
|
528
528
|
expect( metadata.inputSchema ).toBe( zodSchema );
|
|
529
529
|
expect( metadata.inputSchema ).not.toBe( null );
|
|
530
|
-
expect( metadata.inputSchema.
|
|
530
|
+
expect( metadata.inputSchema._zod?.def ).toBeDefined(); // Zod v4-specific property
|
|
531
531
|
} );
|
|
532
532
|
|
|
533
533
|
it( 'should handle deeply nested Zod schemas', async () => {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { deepMergeWithResolver } from '#utils';
|
|
2
1
|
import { Attribute } from '#trace_attribute';
|
|
3
2
|
import Decimal from 'decimal.js';
|
|
4
3
|
|
|
@@ -43,12 +42,3 @@ export const aggregateAttributes = attributes => ( {
|
|
|
43
42
|
total: attributes.filter( a => Attribute.HTTPRequestCount.TYPE === a.type ).length
|
|
44
43
|
}
|
|
45
44
|
} );
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Combine two or more aggregation objects into a single Aggregation, adding up the totals and merging keys.
|
|
49
|
-
*
|
|
50
|
-
* @param {Aggregation} aggregations
|
|
51
|
-
* @returns {Aggregation}
|
|
52
|
-
*/
|
|
53
|
-
export const mergeAggregations = ( ...aggregations ) =>
|
|
54
|
-
aggregations.reduce( ( final, item ) => deepMergeWithResolver( final, item, ( a, b ) => Decimal( a ?? 0 ).add( b ?? 0 ).toNumber() ), {} );
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { Attribute } from '#trace_attribute';
|
|
3
|
-
import { aggregateAttributes
|
|
3
|
+
import { aggregateAttributes } from './aggregations.js';
|
|
4
4
|
|
|
5
5
|
describe( 'aggregateAttributes', () => {
|
|
6
6
|
it( 'returns zeroed aggregations when there are no attributes', () => {
|
|
@@ -90,50 +90,3 @@ describe( 'aggregateAttributes', () => {
|
|
|
90
90
|
} );
|
|
91
91
|
} );
|
|
92
92
|
|
|
93
|
-
describe( 'mergeAggregations', () => {
|
|
94
|
-
it( 'returns an empty object when no aggregations are provided', () => {
|
|
95
|
-
expect( mergeAggregations() ).toEqual( {} );
|
|
96
|
-
} );
|
|
97
|
-
|
|
98
|
-
it( 'sums nested aggregation values', () => {
|
|
99
|
-
expect( mergeAggregations(
|
|
100
|
-
{
|
|
101
|
-
cost: { total: 1.2 },
|
|
102
|
-
tokens: { total: 10, input: 6 },
|
|
103
|
-
httpRequests: { total: 2 }
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
cost: { total: 0.3 },
|
|
107
|
-
tokens: { total: 5, input: 2, output: 3 },
|
|
108
|
-
httpRequests: { total: 1 }
|
|
109
|
-
}
|
|
110
|
-
) ).toEqual( {
|
|
111
|
-
cost: { total: 1.5 },
|
|
112
|
-
tokens: { total: 15, input: 8, output: 3 },
|
|
113
|
-
httpRequests: { total: 3 }
|
|
114
|
-
} );
|
|
115
|
-
} );
|
|
116
|
-
|
|
117
|
-
it( 'handles undefined or partial aggregation objects', () => {
|
|
118
|
-
expect( mergeAggregations(
|
|
119
|
-
undefined,
|
|
120
|
-
{ cost: { total: 2 } },
|
|
121
|
-
{ tokens: { total: 4, reasoning: 1 } },
|
|
122
|
-
{ httpRequests: { total: 3 } }
|
|
123
|
-
) ).toEqual( {
|
|
124
|
-
cost: { total: 2 },
|
|
125
|
-
tokens: { total: 4, reasoning: 1 },
|
|
126
|
-
httpRequests: { total: 3 }
|
|
127
|
-
} );
|
|
128
|
-
} );
|
|
129
|
-
|
|
130
|
-
it( 'does not mutate source aggregation objects', () => {
|
|
131
|
-
const first = { cost: { total: 1 }, tokens: { total: 2 }, httpRequests: { total: 3 } };
|
|
132
|
-
const second = { cost: { total: 4 }, tokens: { total: 5 }, httpRequests: { total: 6 } };
|
|
133
|
-
|
|
134
|
-
mergeAggregations( first, second );
|
|
135
|
-
|
|
136
|
-
expect( first ).toEqual( { cost: { total: 1 }, tokens: { total: 2 }, httpRequests: { total: 3 } } );
|
|
137
|
-
expect( second ).toEqual( { cost: { total: 4 }, tokens: { total: 5 }, httpRequests: { total: 6 } } );
|
|
138
|
-
} );
|
|
139
|
-
} );
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { ApplicationFailure } from '@temporalio/common';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* @
|
|
6
|
-
* @param {string} key
|
|
7
|
-
* @returns {any} The value of the property
|
|
4
|
+
* Builds a Temporal ApplicationFailure based on an error attaching info to its details
|
|
5
|
+
* @param {Error} error
|
|
6
|
+
* @param {unknown} info
|
|
7
|
+
* @returns {ApplicationFailure}
|
|
8
8
|
*/
|
|
9
|
-
export const
|
|
10
|
-
|
|
9
|
+
export const buildApplicationFailureWithDetails = ( error, info ) =>
|
|
10
|
+
ApplicationFailure.create( {
|
|
11
|
+
message: error.message,
|
|
12
|
+
type: error.type ?? error.constructor?.name ?? error.name,
|
|
13
|
+
nonRetryable: error.nonRetryable,
|
|
14
|
+
details: ( Array.isArray( error.details ) ? error.details : [] ).concat( info ),
|
|
15
|
+
cause: error
|
|
16
|
+
} );
|
|
@@ -1,43 +1,89 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { ApplicationFailure } from '@temporalio/common';
|
|
3
|
+
import { buildApplicationFailureWithDetails } from './errors.js';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
it( 'returns a matching value from error details', () => {
|
|
6
|
-
const error = new Error( 'failed' );
|
|
7
|
-
error.details = [
|
|
8
|
-
{ requestId: 'req-1' },
|
|
9
|
-
{ workflowId: 'workflow-1' }
|
|
10
|
-
];
|
|
5
|
+
class CustomFailure extends Error {}
|
|
11
6
|
|
|
12
|
-
|
|
7
|
+
describe( 'buildApplicationFailureWithDetails', () => {
|
|
8
|
+
it( 'wraps a regular error in an ApplicationFailure with appended details', () => {
|
|
9
|
+
const error = new Error( 'step failed' );
|
|
10
|
+
const info = { aggregations: { cost: { total: 1 } } };
|
|
11
|
+
|
|
12
|
+
const failure = buildApplicationFailureWithDetails( error, info );
|
|
13
|
+
|
|
14
|
+
expect( failure ).toBeInstanceOf( ApplicationFailure );
|
|
15
|
+
expect( failure ).toMatchObject( {
|
|
16
|
+
message: 'step failed',
|
|
17
|
+
type: 'Error',
|
|
18
|
+
nonRetryable: false,
|
|
19
|
+
details: [ info ],
|
|
20
|
+
cause: error
|
|
21
|
+
} );
|
|
13
22
|
} );
|
|
14
23
|
|
|
15
|
-
it( '
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const wrapped = new Error( 'wrapped', { cause: root } );
|
|
24
|
+
it( 'uses the original constructor name for custom errors', () => {
|
|
25
|
+
const error = new CustomFailure( 'custom failed' );
|
|
26
|
+
const info = { trace: { destinations: { local: '/tmp/trace' } } };
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
const failure = buildApplicationFailureWithDetails( error, info );
|
|
29
|
+
|
|
30
|
+
expect( failure ).toMatchObject( {
|
|
31
|
+
message: 'custom failed',
|
|
32
|
+
type: 'CustomFailure',
|
|
33
|
+
details: [ info ],
|
|
34
|
+
cause: error
|
|
35
|
+
} );
|
|
21
36
|
} );
|
|
22
37
|
|
|
23
|
-
it( '
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
it( 'preserves existing details and appends new info without mutating the original error', () => {
|
|
39
|
+
const existingDetails = [ { domain: { reason: 'bad-input' } } ];
|
|
40
|
+
const error = new Error( 'step failed' );
|
|
41
|
+
error.details = existingDetails;
|
|
42
|
+
const info = { aggregations: { httpRequests: { total: 1 } } };
|
|
43
|
+
|
|
44
|
+
const failure = buildApplicationFailureWithDetails( error, info );
|
|
28
45
|
|
|
29
|
-
expect(
|
|
46
|
+
expect( failure.details ).toEqual( [
|
|
47
|
+
{ domain: { reason: 'bad-input' } },
|
|
48
|
+
info
|
|
49
|
+
] );
|
|
50
|
+
expect( error.details ).toBe( existingDetails );
|
|
51
|
+
expect( error.details ).toEqual( [ { domain: { reason: 'bad-input' } } ] );
|
|
30
52
|
} );
|
|
31
53
|
|
|
32
|
-
it( '
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const
|
|
54
|
+
it( 'ignores non-array details on the original error', () => {
|
|
55
|
+
const error = new Error( 'step failed' );
|
|
56
|
+
error.details = { domain: { reason: 'bad-input' } };
|
|
57
|
+
const info = { aggregations: { tokens: { total: 3 } } };
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
const failure = buildApplicationFailureWithDetails( error, info );
|
|
60
|
+
|
|
61
|
+
expect( failure.details ).toEqual( [ info ] );
|
|
38
62
|
} );
|
|
39
63
|
|
|
40
|
-
it( '
|
|
41
|
-
|
|
64
|
+
it( 'preserves ApplicationFailure type, nonRetryable flag, and details while avoiding self-cause', () => {
|
|
65
|
+
const original = ApplicationFailure.create( {
|
|
66
|
+
message: 'application failed',
|
|
67
|
+
type: 'DomainFailure',
|
|
68
|
+
nonRetryable: true,
|
|
69
|
+
details: [ { domain: { reason: 'bad-input' } } ]
|
|
70
|
+
} );
|
|
71
|
+
const info = { aggregations: { cost: { total: 2 } } };
|
|
72
|
+
|
|
73
|
+
const failure = buildApplicationFailureWithDetails( original, info );
|
|
74
|
+
|
|
75
|
+
expect( failure ).toBeInstanceOf( ApplicationFailure );
|
|
76
|
+
expect( failure ).not.toBe( original );
|
|
77
|
+
expect( failure.cause ).toBe( original );
|
|
78
|
+
expect( failure.cause ).not.toBe( failure );
|
|
79
|
+
expect( failure ).toMatchObject( {
|
|
80
|
+
message: 'application failed',
|
|
81
|
+
type: 'DomainFailure',
|
|
82
|
+
nonRetryable: true,
|
|
83
|
+
details: [
|
|
84
|
+
{ domain: { reason: 'bad-input' } },
|
|
85
|
+
info
|
|
86
|
+
]
|
|
87
|
+
} );
|
|
42
88
|
} );
|
|
43
89
|
} );
|
package/src/utils/index.d.ts
CHANGED
|
@@ -159,3 +159,22 @@ export function allSettledWithTimeout<T>(
|
|
|
159
159
|
promises: Array<T | PromiseLike<T>>,
|
|
160
160
|
timeoutMs: number
|
|
161
161
|
): Promise<PromiseSettledResult<Awaited<T>>[]>;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Promise wrapper that can be resolved externally.
|
|
165
|
+
*/
|
|
166
|
+
export class CancellablePromise {
|
|
167
|
+
/** The internal promise */
|
|
168
|
+
readonly promise: Promise<void>;
|
|
169
|
+
/** Whether the promise is already resolved or not */
|
|
170
|
+
readonly completed: boolean;
|
|
171
|
+
/** Resolves the promise */
|
|
172
|
+
complete(): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns a function that invokes the wrapped function once.
|
|
177
|
+
*/
|
|
178
|
+
export function runOnce<Args extends unknown[], Return>(
|
|
179
|
+
fn: ( ...args: Args ) => Return
|
|
180
|
+
): ( ...args: Args ) => Return;
|
package/src/utils/utils.js
CHANGED
|
@@ -279,3 +279,56 @@ export const allSettledWithTimeout = ( () => {
|
|
|
279
279
|
}
|
|
280
280
|
};
|
|
281
281
|
} )();
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Builds a promise that can be resolved from the outside.
|
|
285
|
+
*/
|
|
286
|
+
export class CancellablePromise {
|
|
287
|
+
#promise = null;
|
|
288
|
+
#complete = null;
|
|
289
|
+
#completed = false;
|
|
290
|
+
|
|
291
|
+
constructor() {
|
|
292
|
+
this.#promise = new Promise( resolve => {
|
|
293
|
+
this.#complete = () => {
|
|
294
|
+
resolve();
|
|
295
|
+
this.#completed = true;
|
|
296
|
+
};
|
|
297
|
+
} );
|
|
298
|
+
}
|
|
299
|
+
/** Retrieves the promise */
|
|
300
|
+
get promise() {
|
|
301
|
+
return this.#promise;
|
|
302
|
+
}
|
|
303
|
+
/** Returns whether the promise is resolved or not */
|
|
304
|
+
get completed() {
|
|
305
|
+
return this.#completed;
|
|
306
|
+
}
|
|
307
|
+
/** Resolves the promise */
|
|
308
|
+
complete() {
|
|
309
|
+
this.#complete();
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Returns a function that invokes the fn argument when called once, further calls do nothing.
|
|
315
|
+
* @param {Function} fn
|
|
316
|
+
* @returns {Function}
|
|
317
|
+
*/
|
|
318
|
+
export const runOnce = fn => {
|
|
319
|
+
const state = { executed: false, result: undefined };
|
|
320
|
+
return ( ...args ) => {
|
|
321
|
+
if ( !state.executed ) {
|
|
322
|
+
state.executed = true;
|
|
323
|
+
return state.result = fn( ...args );
|
|
324
|
+
}
|
|
325
|
+
return state.result;
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Escape regexp characters in a string
|
|
331
|
+
* @param {*} value
|
|
332
|
+
* @returns
|
|
333
|
+
*/
|
|
334
|
+
export const rxEscape = v => v.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
|
package/src/utils/utils.spec.js
CHANGED
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
deepMergeWithResolver,
|
|
9
9
|
isPlainObject,
|
|
10
10
|
toUrlSafeBase64,
|
|
11
|
-
allSettledWithTimeout
|
|
11
|
+
allSettledWithTimeout,
|
|
12
|
+
CancellablePromise,
|
|
13
|
+
runOnce,
|
|
14
|
+
rxEscape
|
|
12
15
|
} from './utils.js';
|
|
13
16
|
|
|
14
17
|
describe( 'clone', () => {
|
|
@@ -142,6 +145,107 @@ describe( 'allSettledWithTimeout', () => {
|
|
|
142
145
|
} );
|
|
143
146
|
} );
|
|
144
147
|
|
|
148
|
+
describe( 'CancellablePromise', () => {
|
|
149
|
+
it( 'exposes a pending promise until it is completed', async () => {
|
|
150
|
+
const cancellable = new CancellablePromise();
|
|
151
|
+
const onComplete = vi.fn();
|
|
152
|
+
|
|
153
|
+
cancellable.promise.then( onComplete );
|
|
154
|
+
await Promise.resolve();
|
|
155
|
+
|
|
156
|
+
expect( cancellable.completed ).toBe( false );
|
|
157
|
+
expect( onComplete ).not.toHaveBeenCalled();
|
|
158
|
+
|
|
159
|
+
cancellable.complete();
|
|
160
|
+
await cancellable.promise;
|
|
161
|
+
|
|
162
|
+
expect( cancellable.completed ).toBe( true );
|
|
163
|
+
expect( onComplete ).toHaveBeenCalledOnce();
|
|
164
|
+
} );
|
|
165
|
+
|
|
166
|
+
it( 'can be completed multiple times without resolving again', async () => {
|
|
167
|
+
const cancellable = new CancellablePromise();
|
|
168
|
+
const onComplete = vi.fn();
|
|
169
|
+
|
|
170
|
+
cancellable.promise.then( onComplete );
|
|
171
|
+
cancellable.complete();
|
|
172
|
+
cancellable.complete();
|
|
173
|
+
await cancellable.promise;
|
|
174
|
+
await Promise.resolve();
|
|
175
|
+
|
|
176
|
+
expect( cancellable.completed ).toBe( true );
|
|
177
|
+
expect( onComplete ).toHaveBeenCalledOnce();
|
|
178
|
+
} );
|
|
179
|
+
} );
|
|
180
|
+
|
|
181
|
+
describe( 'runOnce', () => {
|
|
182
|
+
it( 'calls the wrapped function only once', () => {
|
|
183
|
+
const fn = vi.fn();
|
|
184
|
+
const once = runOnce( fn );
|
|
185
|
+
|
|
186
|
+
once();
|
|
187
|
+
once();
|
|
188
|
+
once();
|
|
189
|
+
|
|
190
|
+
expect( fn ).toHaveBeenCalledOnce();
|
|
191
|
+
} );
|
|
192
|
+
|
|
193
|
+
it( 'passes arguments and replays the first call result', () => {
|
|
194
|
+
const fn = vi.fn( ( a, b ) => a + b );
|
|
195
|
+
const once = runOnce( fn );
|
|
196
|
+
|
|
197
|
+
expect( once( 2, 3 ) ).toBe( 5 );
|
|
198
|
+
expect( once( 4, 5 ) ).toBe( 5 );
|
|
199
|
+
expect( once( 6, 7 ) ).toBe( 5 );
|
|
200
|
+
expect( fn ).toHaveBeenCalledWith( 2, 3 );
|
|
201
|
+
expect( fn ).toHaveBeenCalledOnce();
|
|
202
|
+
} );
|
|
203
|
+
|
|
204
|
+
it( 'replays the first returned promise', () => {
|
|
205
|
+
const result = Promise.resolve( 'done' );
|
|
206
|
+
const fn = vi.fn( () => result );
|
|
207
|
+
const once = runOnce( fn );
|
|
208
|
+
|
|
209
|
+
expect( once() ).toBe( result );
|
|
210
|
+
expect( once() ).toBe( result );
|
|
211
|
+
expect( fn ).toHaveBeenCalledOnce();
|
|
212
|
+
} );
|
|
213
|
+
|
|
214
|
+
it( 'does not retry when the first call throws', () => {
|
|
215
|
+
const error = new Error( 'boom' );
|
|
216
|
+
const fn = vi.fn( () => {
|
|
217
|
+
throw error;
|
|
218
|
+
} );
|
|
219
|
+
const once = runOnce( fn );
|
|
220
|
+
|
|
221
|
+
expect( () => once() ).toThrow( error );
|
|
222
|
+
expect( once() ).toBeUndefined();
|
|
223
|
+
expect( fn ).toHaveBeenCalledOnce();
|
|
224
|
+
} );
|
|
225
|
+
} );
|
|
226
|
+
|
|
227
|
+
describe( 'rxEscape', () => {
|
|
228
|
+
it( 'escapes all regexp metacharacters', () => {
|
|
229
|
+
expect( rxEscape( '.*+?^${}()|[]\\' ) ).toBe( '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\' );
|
|
230
|
+
} );
|
|
231
|
+
|
|
232
|
+
it( 'keeps file URL paths matchable as literal regexp input', () => {
|
|
233
|
+
const path = 'file://foo/bar';
|
|
234
|
+
const rx = new RegExp( `^${rxEscape( path )}$` );
|
|
235
|
+
|
|
236
|
+
expect( rx.test( path ) ).toBe( true );
|
|
237
|
+
expect( rx.test( 'file://foo/bar/baz' ) ).toBe( false );
|
|
238
|
+
} );
|
|
239
|
+
|
|
240
|
+
it( 'keeps Windows paths matchable as literal regexp input', () => {
|
|
241
|
+
const path = String.raw`C:\foo\bar`;
|
|
242
|
+
const rx = new RegExp( `^${rxEscape( path )}$` );
|
|
243
|
+
|
|
244
|
+
expect( rx.test( path ) ).toBe( true );
|
|
245
|
+
expect( rx.test( String.raw`C:\foo\bar\baz` ) ).toBe( false );
|
|
246
|
+
} );
|
|
247
|
+
} );
|
|
248
|
+
|
|
145
249
|
describe( 'serializeFetchResponse', () => {
|
|
146
250
|
it( 'serializes JSON response body and flattens headers', async () => {
|
|
147
251
|
const payload = { a: 1, b: 'two' };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { bundleWorkflowCode } from '@temporalio/worker';
|
|
2
|
+
import { loadWorkflows } from './loader/workflows.js';
|
|
3
|
+
import { loadActivities } from './loader/activities.js';
|
|
4
|
+
import { webpackConfigHook } from './bundler_options.js';
|
|
5
|
+
import { workflowInterceptorModules } from './interceptors/modules.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Bundle a project's workflows exactly as the worker does, without a Temporal server.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the worker's startup prep (`loadWorkflows` -> `loadActivities` ->
|
|
11
|
+
* `createWorkflowsEntryPoint`) and then runs the same bundler (`bundleWorkflowCode`)
|
|
12
|
+
* with the same inputs `Worker.create` derives — `webpackConfigHook` and
|
|
13
|
+
* `workflowInterceptorModules` — so it stays in parity with worker startup. Rejects if
|
|
14
|
+
* the Temporal webpack bundler fails, e.g. a `node:` built-in in the workflow's
|
|
15
|
+
* transitive import graph.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} rootDir directory to discover workflows in
|
|
18
|
+
* @returns {Promise<import('@temporalio/worker').WorkflowBundleWithSourceMap>}
|
|
19
|
+
*/
|
|
20
|
+
export async function bundleWorkflows( rootDir ) {
|
|
21
|
+
const { workflows, entrypoint: workflowsPath } = await loadWorkflows( rootDir );
|
|
22
|
+
// Writes worker/temp/__activity_options.js, which the workflow interceptor module
|
|
23
|
+
// imports — the worker generates it via loadActivities() before Worker.create bundles.
|
|
24
|
+
await loadActivities( rootDir, workflows );
|
|
25
|
+
return bundleWorkflowCode( { workflowsPath, workflowInterceptorModules, webpackConfigHook } );
|
|
26
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Stub the activity interceptor so the real interceptors/index.js imports cleanly.
|
|
4
|
+
vi.mock( './interceptors/activity.js', () => ( { ActivityExecutionInterceptor: class {} } ) );
|
|
5
|
+
|
|
6
|
+
vi.mock( '@temporalio/worker', () => ( {
|
|
7
|
+
bundleWorkflowCode: vi.fn().mockResolvedValue( { code: '', sourceMap: '' } )
|
|
8
|
+
} ) );
|
|
9
|
+
|
|
10
|
+
vi.mock( './loader/workflows.js', () => ( {
|
|
11
|
+
loadWorkflows: vi.fn().mockResolvedValue( { workflows: [], entrypoint: '/fake/workflows/entrypoint.js' } )
|
|
12
|
+
} ) );
|
|
13
|
+
|
|
14
|
+
vi.mock( './loader/activities.js', () => ( {
|
|
15
|
+
loadActivities: vi.fn().mockResolvedValue( { activities: {} } )
|
|
16
|
+
} ) );
|
|
17
|
+
|
|
18
|
+
vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
|
|
19
|
+
|
|
20
|
+
import { bundleWorkflowCode } from '@temporalio/worker';
|
|
21
|
+
import { loadWorkflows } from './loader/workflows.js';
|
|
22
|
+
import { loadActivities } from './loader/activities.js';
|
|
23
|
+
import { webpackConfigHook } from './bundler_options.js';
|
|
24
|
+
import { initInterceptors } from './interceptors/index.js';
|
|
25
|
+
import { workflowInterceptorModules } from './interceptors/modules.js';
|
|
26
|
+
import { bundleWorkflows } from './bundle.js';
|
|
27
|
+
|
|
28
|
+
describe( 'output-worker --check parity', () => {
|
|
29
|
+
beforeEach( () => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
loadWorkflows.mockResolvedValue( { workflows: [], entrypoint: '/fake/workflows/entrypoint.js' } );
|
|
32
|
+
loadActivities.mockResolvedValue( { activities: {} } );
|
|
33
|
+
bundleWorkflowCode.mockResolvedValue( { code: '', sourceMap: '' } );
|
|
34
|
+
} );
|
|
35
|
+
|
|
36
|
+
it( 'worker registers the shared workflow interceptor modules', () => {
|
|
37
|
+
const { workflowModules } = initInterceptors( { activities: {}, workflows: [], connection: {} } );
|
|
38
|
+
// The check (bundleWorkflows) and the worker must register the very same modules.
|
|
39
|
+
expect( workflowModules ).toBe( workflowInterceptorModules );
|
|
40
|
+
} );
|
|
41
|
+
|
|
42
|
+
it( 'check bundles with the same inputs Worker.create derives', async () => {
|
|
43
|
+
await bundleWorkflows( '/project' );
|
|
44
|
+
|
|
45
|
+
expect( loadWorkflows ).toHaveBeenCalledWith( '/project' );
|
|
46
|
+
expect( loadActivities ).toHaveBeenCalledWith( '/project', [] );
|
|
47
|
+
expect( bundleWorkflowCode ).toHaveBeenCalledWith( {
|
|
48
|
+
workflowsPath: '/fake/workflows/entrypoint.js',
|
|
49
|
+
workflowInterceptorModules,
|
|
50
|
+
webpackConfigHook
|
|
51
|
+
} );
|
|
52
|
+
} );
|
|
53
|
+
} );
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
findPackageRoot,
|
|
5
5
|
isPathDescendentFromNodeModules,
|
|
6
6
|
packageExposesWorkflows
|
|
7
|
-
} from './
|
|
7
|
+
} from './loader/tools.js';
|
|
8
8
|
|
|
9
9
|
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
10
10
|
const workerDir = __dirname; // sdk/core/src/worker
|
|
@@ -33,7 +33,7 @@ describe( 'webpackConfigHook loader excludes', () => {
|
|
|
33
33
|
|
|
34
34
|
it( 'excludes worker and interface internals', () => {
|
|
35
35
|
for ( const exclude of buildExcludes() ) {
|
|
36
|
-
expect( exclude( join( __dirname, 'loader.js' ) ) ).toBe( true );
|
|
36
|
+
expect( exclude( join( __dirname, 'loader', 'tools.js' ) ) ).toBe( true );
|
|
37
37
|
expect( exclude( join( __dirname, '..', 'interface', 'index.js' ) ) ).toBe( true );
|
|
38
38
|
}
|
|
39
39
|
} );
|