@outputai/core 0.10.1-next.b7b2fbe.0 → 0.10.1-next.bf95a6f.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/package.json +13 -7
- package/src/bus.js +58 -20
- package/src/bus.spec.js +135 -77
- package/src/consts.js +7 -3
- package/src/errors.d.ts +12 -2
- package/src/errors.js +17 -3
- package/src/errors.spec.js +19 -0
- package/src/helpers/activity_options.js +21 -0
- package/src/helpers/activity_options.spec.js +55 -0
- package/src/helpers/error_serializer.js +236 -0
- package/src/helpers/error_serializer.spec.js +413 -0
- package/src/helpers/errors.js +23 -14
- package/src/helpers/errors.spec.js +31 -75
- package/src/helpers/fetch.js +1 -38
- package/src/helpers/fetch.spec.js +1 -39
- package/src/helpers/function.js +13 -0
- package/src/helpers/function.spec.js +23 -1
- package/src/helpers/object.js +48 -35
- package/src/helpers/object.spec.js +123 -75
- package/src/helpers/redact.js +51 -0
- package/src/helpers/redact.spec.js +66 -0
- package/src/helpers/string.js +10 -0
- package/src/helpers/string.spec.js +36 -1
- package/src/hooks/index.d.ts +76 -78
- package/src/hooks/index.js +57 -49
- package/src/hooks/index.spec.js +104 -105
- package/src/hooks/pending_hooks.js +24 -0
- package/src/hooks/pending_hooks.spec.js +62 -0
- package/src/interface/evaluator.d.ts +10 -9
- package/src/interface/evaluator.js +1 -6
- package/src/interface/evaluator.spec.js +10 -10
- package/src/interface/index.js +2 -1
- package/src/interface/step.d.ts +16 -15
- package/src/interface/step.js +1 -6
- package/src/interface/step.spec.js +10 -10
- package/src/interface/validations/index.js +33 -53
- package/src/interface/validations/index.spec.js +25 -17
- package/src/interface/webhook.js +1 -2
- package/src/interface/webhook.spec.js +25 -19
- package/src/interface/workflow.d.ts +25 -18
- package/src/interface/workflow.js +64 -85
- package/src/interface/workflow.spec.js +220 -226
- package/src/interface/workflow_activity_options.js +2 -5
- package/src/interface/workflow_utils.d.ts +15 -0
- package/src/interface/workflow_utils.js +19 -0
- package/src/interface/workflow_utils.spec.js +82 -1
- package/src/sdk/helpers/objects.d.ts +24 -19
- package/src/sdk/runtime/events.d.ts +4 -6
- package/src/sdk/runtime/events.js +2 -15
- package/src/sdk/runtime/events.spec.js +14 -92
- package/src/temporal/client.js +47 -0
- package/src/temporal/client.spec.js +124 -0
- package/src/temporal/index.d.ts +32 -0
- package/src/temporal/index.js +1 -0
- package/src/tracing/trace_engine.js +9 -11
- package/src/tracing/trace_engine.spec.js +67 -28
- package/src/worker/catalog_workflow/catalog_job.js +3 -3
- package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
- package/src/worker/configs.js +0 -3
- package/src/worker/global_functions.js +2 -2
- package/src/worker/global_functions.spec.js +3 -3
- package/src/worker/index.js +24 -11
- package/src/worker/index.spec.js +36 -16
- package/src/worker/interceptors/activity.js +34 -21
- package/src/worker/interceptors/activity.spec.js +77 -172
- package/src/worker/interceptors/workflow.js +58 -16
- package/src/worker/interceptors/workflow.spec.js +183 -56
- package/src/worker/loader/activities.js +56 -40
- package/src/worker/loader/activities.spec.js +21 -22
- package/src/worker/loader/workflows.js +16 -3
- package/src/worker/loader/workflows.spec.js +36 -5
- package/src/worker/log_hooks.js +22 -19
- package/src/worker/log_hooks.spec.js +14 -6
- package/src/worker/sinks.js +8 -8
- package/src/worker/sinks.spec.js +9 -9
- package/src/worker/telemetry.js +2 -1
- package/src/worker/temporal_logger.js +33 -0
- package/src/worker/temporal_logger.spec.js +90 -0
- package/src/worker/webpack_loaders/consts.js +6 -0
- package/src/worker/webpack_loaders/tools.js +1 -146
- package/src/worker/webpack_loaders/tools.spec.js +0 -23
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.js +24 -32
- package/src/worker/webpack_loaders/workflow_rewriter/collect_target_imports.spec.js +82 -279
- package/src/worker/webpack_loaders/workflow_rewriter/index.mjs +6 -7
- package/src/worker/webpack_loaders/workflow_rewriter/index.spec.js +57 -119
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.js +88 -0
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_activity_calls.spec.js +165 -0
- package/src/worker/webpack_loaders/workflow_validator/index.mjs +96 -29
- package/src/worker/webpack_loaders/workflow_validator/index.spec.js +110 -583
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.js → workflow_validator/npm_workflow_export_resolve.js} +1 -1
- package/src/worker/webpack_loaders/{npm_workflow_export_resolve.spec.js → workflow_validator/npm_workflow_export_resolve.spec.js} +1 -1
- package/src/helpers/aggregations.js +0 -44
- package/src/helpers/aggregations.spec.js +0 -92
- package/src/tracing/tools/utils.js +0 -42
- package/src/tracing/tools/utils.spec.js +0 -83
- package/src/worker/proxy.js +0 -20
- package/src/worker/proxy.spec.js +0 -51
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.js +0 -193
- package/src/worker/webpack_loaders/workflow_rewriter/rewrite_fn_bodies.spec.js +0 -123
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { FatalError, ValidationError } from '#errors';
|
|
2
|
-
|
|
3
1
|
export const defaultOptions = {
|
|
4
2
|
activityOptions: {
|
|
5
3
|
startToCloseTimeout: '20m',
|
|
@@ -9,8 +7,7 @@ export const defaultOptions = {
|
|
|
9
7
|
backoffCoefficient: 2.0,
|
|
10
8
|
maximumInterval: '2m',
|
|
11
9
|
maximumAttempts: 3,
|
|
12
|
-
nonRetryableErrorTypes: [
|
|
10
|
+
nonRetryableErrorTypes: []
|
|
13
11
|
}
|
|
14
|
-
}
|
|
15
|
-
disableTrace: false
|
|
12
|
+
}
|
|
16
13
|
};
|
|
@@ -74,3 +74,18 @@ export declare function executeInParallel<T extends readonly ( () => Promise<unk
|
|
|
74
74
|
onJobCompleted?: ( result: ParallelResults<T>[number] ) => void;
|
|
75
75
|
}
|
|
76
76
|
): Promise<ParallelResults<T>>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Checks whether an error or any error in its cause chain matches an Error class.
|
|
80
|
+
*
|
|
81
|
+
* Temporal Application Failures are matched through their serialized `type`,
|
|
82
|
+
* in addition to standard `instanceof` and `name` checks.
|
|
83
|
+
*
|
|
84
|
+
* @param error - Error or failure chain to inspect
|
|
85
|
+
* @param ErrorType - Error class to match
|
|
86
|
+
* @returns Whether the Error class appears in the chain
|
|
87
|
+
*/
|
|
88
|
+
export declare function hasErrorType<T extends Error>(
|
|
89
|
+
error: unknown,
|
|
90
|
+
ErrorType: abstract new ( ...args: never[] ) => T
|
|
91
|
+
): boolean;
|
|
@@ -48,3 +48,22 @@ export const executeInParallel = async ( { jobs, concurrency = Infinity, onJobCo
|
|
|
48
48
|
|
|
49
49
|
return results.sort( ( a, b ) => a.index - b.index );
|
|
50
50
|
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if an error, or any parts of its cause chain are instances of a given Error class.
|
|
54
|
+
* Also check .name and .type
|
|
55
|
+
*
|
|
56
|
+
* @param {Error} error
|
|
57
|
+
* @param {Error} ErrorType
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
export const hasErrorType = ( error, ErrorType, depth = 0 ) => {
|
|
61
|
+
if ( depth > 20 || typeof ErrorType !== 'function' ) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const expectedType = ErrorType.name;
|
|
65
|
+
if ( error instanceof ErrorType || error?.type === expectedType || error?.name === expectedType || error === expectedType ) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return error?.cause ? hasErrorType( error.cause, ErrorType, depth + 1 ) : false;
|
|
69
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { executeInParallel } from './workflow_utils.js';
|
|
2
|
+
import { hasErrorType, executeInParallel } from './workflow_utils.js';
|
|
3
3
|
|
|
4
4
|
describe( 'executeInParallel', () => {
|
|
5
5
|
it( 'returns empty array for empty jobs', async () => {
|
|
@@ -188,3 +188,84 @@ describe( 'executeInParallel', () => {
|
|
|
188
188
|
] );
|
|
189
189
|
} );
|
|
190
190
|
} );
|
|
191
|
+
|
|
192
|
+
describe( 'hasErrorType', () => {
|
|
193
|
+
class FooError extends Error {}
|
|
194
|
+
|
|
195
|
+
it( 'matches a direct error instance', () => {
|
|
196
|
+
expect( hasErrorType( new FooError( 'foo' ), FooError ) ).toBe( true );
|
|
197
|
+
} );
|
|
198
|
+
|
|
199
|
+
it( 'matches an error instance in the cause chain', () => {
|
|
200
|
+
const error = new Error( 'outer', {
|
|
201
|
+
cause: new Error( 'middle', {
|
|
202
|
+
cause: new FooError( 'foo' )
|
|
203
|
+
} )
|
|
204
|
+
} );
|
|
205
|
+
|
|
206
|
+
expect( hasErrorType( error, FooError ) ).toBe( true );
|
|
207
|
+
} );
|
|
208
|
+
|
|
209
|
+
it( 'matches Temporal ApplicationFailure type in the cause chain', () => {
|
|
210
|
+
const applicationFailure = Object.assign( new Error( 'foo' ), {
|
|
211
|
+
name: 'ApplicationFailure',
|
|
212
|
+
type: 'FooError'
|
|
213
|
+
} );
|
|
214
|
+
const activityFailure = Object.assign( new Error( 'Activity execution failed', {
|
|
215
|
+
cause: applicationFailure
|
|
216
|
+
} ), {
|
|
217
|
+
name: 'ActivityFailure'
|
|
218
|
+
} );
|
|
219
|
+
|
|
220
|
+
expect( hasErrorType( activityFailure, FooError ) ).toBe( true );
|
|
221
|
+
} );
|
|
222
|
+
|
|
223
|
+
it( 'matches a serialized error name', () => {
|
|
224
|
+
const error = Object.assign( new Error( 'foo' ), { name: 'FooError' } );
|
|
225
|
+
|
|
226
|
+
expect( hasErrorType( error, FooError ) ).toBe( true );
|
|
227
|
+
} );
|
|
228
|
+
|
|
229
|
+
it( 'matches an error serialized as its type name', () => {
|
|
230
|
+
expect( hasErrorType( 'FooError', FooError ) ).toBe( true );
|
|
231
|
+
} );
|
|
232
|
+
|
|
233
|
+
it( 'matches an error type in a plain object cause chain', () => {
|
|
234
|
+
const error = {
|
|
235
|
+
name: 'ActivityFailure',
|
|
236
|
+
cause: {
|
|
237
|
+
name: 'ApplicationFailure',
|
|
238
|
+
type: 'FooError'
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
expect( hasErrorType( error, FooError ) ).toBe( true );
|
|
243
|
+
} );
|
|
244
|
+
|
|
245
|
+
it.each( [ null, undefined, '', 42, false ] )( 'returns false for unsupported error value %j', error => {
|
|
246
|
+
expect( hasErrorType( error, FooError ) ).toBe( false );
|
|
247
|
+
} );
|
|
248
|
+
|
|
249
|
+
it( 'returns false when ErrorType is not a class', () => {
|
|
250
|
+
expect( hasErrorType( new FooError( 'foo' ), null ) ).toBe( false );
|
|
251
|
+
} );
|
|
252
|
+
|
|
253
|
+
it( 'returns false when the requested error type is absent', () => {
|
|
254
|
+
const error = new Error( 'outer', { cause: new TypeError( 'inner' ) } );
|
|
255
|
+
|
|
256
|
+
expect( hasErrorType( error, FooError ) ).toBe( false );
|
|
257
|
+
} );
|
|
258
|
+
|
|
259
|
+
it( 'keeps plain Error matching backwards compatible', () => {
|
|
260
|
+
expect( hasErrorType( new Error( 'foo' ), Error ) ).toBe( true );
|
|
261
|
+
} );
|
|
262
|
+
|
|
263
|
+
it( 'stops traversing cause chains after the depth limit', () => {
|
|
264
|
+
const createCauseChain = depth => depth === 20 ?
|
|
265
|
+
new FooError( 'foo' ) :
|
|
266
|
+
new Error( `depth-${depth}`, { cause: createCauseChain( depth + 1 ) } );
|
|
267
|
+
const root = new Error( 'root', { cause: createCauseChain( 0 ) } );
|
|
268
|
+
|
|
269
|
+
expect( hasErrorType( root, FooError ) ).toBe( false );
|
|
270
|
+
} );
|
|
271
|
+
} );
|
|
@@ -23,29 +23,34 @@ export declare const Objects: {
|
|
|
23
23
|
isPlainObject( object: unknown ): boolean,
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* Creates a new object by merging
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* -
|
|
30
|
-
*
|
|
31
|
-
* @param
|
|
32
|
-
* @param
|
|
33
|
-
* @throws {Error} If
|
|
26
|
+
* Creates a new object by recursively merging overlays onto a base object.
|
|
27
|
+
* Later objects overwrite fields from earlier objects.
|
|
28
|
+
*
|
|
29
|
+
* Non-object overlays are ignored.
|
|
30
|
+
*
|
|
31
|
+
* @param base - The base object.
|
|
32
|
+
* @param overlays - The overriding objects, applied from left to right.
|
|
33
|
+
* @throws {Error} If `base` is not a plain object.
|
|
34
34
|
* @returns A new merged object.
|
|
35
35
|
*/
|
|
36
|
-
deepMerge(
|
|
36
|
+
deepMerge( base: object, ...overlays: Array<object | null | undefined> ): object,
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Creates a new object by merging
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* -
|
|
43
|
-
*
|
|
44
|
-
* @param
|
|
45
|
-
* @param
|
|
46
|
-
* @
|
|
47
|
-
* @throws {Error} If either `a` or `b` is not a plain object.
|
|
39
|
+
* Creates a new object by recursively merging overlays onto a base object.
|
|
40
|
+
* Existing leaf values are resolved by the resolver function.
|
|
41
|
+
*
|
|
42
|
+
* Non-object overlays are ignored.
|
|
43
|
+
*
|
|
44
|
+
* @param base - The base object.
|
|
45
|
+
* @param args - The overriding objects, applied from left to right, followed by the resolver function.
|
|
46
|
+
* @throws {Error} If `base` is not a plain object.
|
|
48
47
|
* @returns A new merged object.
|
|
49
48
|
*/
|
|
50
|
-
deepMergeWithResolver(
|
|
49
|
+
deepMergeWithResolver(
|
|
50
|
+
base: object,
|
|
51
|
+
...args: [
|
|
52
|
+
...overlays: Array<object | null | undefined>,
|
|
53
|
+
resolver: ( a: unknown, b: unknown ) => unknown
|
|
54
|
+
]
|
|
55
|
+
): object
|
|
51
56
|
};
|
|
@@ -2,14 +2,12 @@
|
|
|
2
2
|
* Tools to interact with Events
|
|
3
3
|
*/
|
|
4
4
|
export declare const Event: {
|
|
5
|
+
|
|
5
6
|
/**
|
|
6
|
-
* Emits
|
|
7
|
-
*
|
|
8
|
-
* When called inside an Output activity context, the framework automatically
|
|
9
|
-
* attaches `activityInfo`, `workflowDetails`, and `outputActivityKind` onto the emitted payload.
|
|
7
|
+
* Emits an event on step message bus.
|
|
10
8
|
*
|
|
11
9
|
* @param eventName - The name of the event to emit
|
|
12
|
-
* @param payload - An optional payload to send to the event
|
|
10
|
+
* @param payload - An optional payload to send to the event.
|
|
13
11
|
*/
|
|
14
|
-
emit( eventName: string, payload?: object ):
|
|
12
|
+
emit( eventName: string, payload?: object ): boolean;
|
|
15
13
|
};
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Storage } from '#async_storage';
|
|
1
|
+
import { stepEventBus } from '#bus';
|
|
3
2
|
|
|
4
3
|
export const Event = {
|
|
5
|
-
emit: ( eventName, payload ) => {
|
|
6
|
-
const ctx = Storage.load();
|
|
7
|
-
|
|
8
|
-
messageBus.emit( `external:${eventName}`, {
|
|
9
|
-
...payload ?? {},
|
|
10
|
-
...( ctx && {
|
|
11
|
-
activityInfo: ctx.activityInfo,
|
|
12
|
-
workflowDetails: ctx.workflowDetails,
|
|
13
|
-
outputActivityKind: ctx.outputActivityKind
|
|
14
|
-
} )
|
|
15
|
-
} );
|
|
16
|
-
}
|
|
4
|
+
emit: ( eventName, payload ) => stepEventBus.emit( `sdk:${eventName}`, payload )
|
|
17
5
|
};
|
|
18
|
-
|
|
@@ -1,118 +1,40 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
|
|
3
|
-
const loadMock = vi.hoisted( () => vi.fn() );
|
|
4
3
|
const emitMock = vi.hoisted( () => vi.fn() );
|
|
5
4
|
|
|
6
|
-
vi.mock( '#async_storage', () => ( {
|
|
7
|
-
Storage: { load: loadMock }
|
|
8
|
-
} ) );
|
|
9
|
-
|
|
10
5
|
vi.mock( '#bus', () => ( {
|
|
11
|
-
|
|
6
|
+
stepEventBus: { emit: emitMock }
|
|
12
7
|
} ) );
|
|
13
8
|
|
|
14
9
|
import { Event } from './events.js';
|
|
15
10
|
|
|
16
|
-
// `eventId` stamping is the bus layer's responsibility (see bus.spec.js).
|
|
17
|
-
// Assertions here use `objectContaining` so they don't have to know about that enrichment.
|
|
18
11
|
describe( 'Event.emit', () => {
|
|
19
12
|
beforeEach( () => {
|
|
20
13
|
vi.clearAllMocks();
|
|
21
14
|
} );
|
|
22
15
|
|
|
23
|
-
it( 'forwards
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
workflowType: 'workflow'
|
|
29
|
-
};
|
|
30
|
-
const workflowDetails = {
|
|
31
|
-
workflowId: 'wf-1',
|
|
32
|
-
runId: 'run-1',
|
|
33
|
-
workflowType: 'workflow'
|
|
34
|
-
};
|
|
35
|
-
loadMock.mockReturnValue( {
|
|
36
|
-
activityInfo,
|
|
37
|
-
workflowDetails,
|
|
38
|
-
outputActivityKind: 'step'
|
|
39
|
-
} );
|
|
16
|
+
it( 'forwards class instance payloads unchanged', () => {
|
|
17
|
+
class Payload {
|
|
18
|
+
value = 'test';
|
|
19
|
+
}
|
|
20
|
+
const payload = new Payload();
|
|
40
21
|
|
|
41
|
-
Event.emit( '
|
|
22
|
+
Event.emit( 'test:event', payload );
|
|
42
23
|
|
|
43
|
-
expect( emitMock ).toHaveBeenCalledWith( '
|
|
44
|
-
activityInfo,
|
|
45
|
-
workflowDetails,
|
|
46
|
-
outputActivityKind: 'step',
|
|
47
|
-
modelId: 'gpt-4o'
|
|
48
|
-
} ) );
|
|
24
|
+
expect( emitMock ).toHaveBeenCalledWith( 'sdk:test:event', payload );
|
|
49
25
|
} );
|
|
50
26
|
|
|
51
|
-
it( 'emits
|
|
52
|
-
|
|
27
|
+
it( 'emits plain object payloads on the SDK event channel', () => {
|
|
28
|
+
const payload = { modelId: 'gpt-4o' };
|
|
53
29
|
|
|
54
|
-
Event.emit( '
|
|
30
|
+
Event.emit( 'cost:llm:request', payload );
|
|
55
31
|
|
|
56
|
-
expect( emitMock ).toHaveBeenCalledWith( '
|
|
32
|
+
expect( emitMock ).toHaveBeenCalledWith( 'sdk:cost:llm:request', payload );
|
|
57
33
|
} );
|
|
58
34
|
|
|
59
|
-
it( '
|
|
60
|
-
const activityInfo = {
|
|
61
|
-
activityId: 'act-2',
|
|
62
|
-
activityType: 'step',
|
|
63
|
-
workflowExecution: { workflowId: 'wf-2', runId: 'run-2' },
|
|
64
|
-
workflowType: 'workflow'
|
|
65
|
-
};
|
|
66
|
-
const workflowDetails = {
|
|
67
|
-
workflowId: 'wf-2',
|
|
68
|
-
runId: 'run-2',
|
|
69
|
-
workflowType: 'workflow'
|
|
70
|
-
};
|
|
71
|
-
loadMock.mockReturnValue( {
|
|
72
|
-
activityInfo,
|
|
73
|
-
workflowDetails,
|
|
74
|
-
outputActivityKind: 'step'
|
|
75
|
-
} );
|
|
76
|
-
|
|
35
|
+
it( 'forwards a missing payload as undefined', () => {
|
|
77
36
|
Event.emit( 'lifecycle:start' );
|
|
78
37
|
|
|
79
|
-
expect( emitMock ).toHaveBeenCalledWith( '
|
|
80
|
-
activityInfo,
|
|
81
|
-
workflowDetails,
|
|
82
|
-
outputActivityKind: 'step'
|
|
83
|
-
} ) );
|
|
84
|
-
} );
|
|
85
|
-
|
|
86
|
-
it( 'does not let payload override activityInfo, workflowDetails, or outputActivityKind', () => {
|
|
87
|
-
const activityInfo = {
|
|
88
|
-
activityId: 'act-3',
|
|
89
|
-
activityType: 'step',
|
|
90
|
-
workflowExecution: { workflowId: 'wf-3', runId: 'run-3' },
|
|
91
|
-
workflowType: 'workflow'
|
|
92
|
-
};
|
|
93
|
-
const workflowDetails = {
|
|
94
|
-
workflowId: 'wf-3',
|
|
95
|
-
runId: 'run-3',
|
|
96
|
-
workflowType: 'workflow'
|
|
97
|
-
};
|
|
98
|
-
loadMock.mockReturnValue( {
|
|
99
|
-
activityInfo,
|
|
100
|
-
workflowDetails,
|
|
101
|
-
outputActivityKind: 'step'
|
|
102
|
-
} );
|
|
103
|
-
|
|
104
|
-
Event.emit( 'cost:http:request', {
|
|
105
|
-
activityInfo: { activityId: 'should-be-overridden' },
|
|
106
|
-
workflowDetails: { workflowId: 'should-be-overridden' },
|
|
107
|
-
outputActivityKind: 'should-be-overridden',
|
|
108
|
-
url: 'https://example.com'
|
|
109
|
-
} );
|
|
110
|
-
|
|
111
|
-
expect( emitMock ).toHaveBeenCalledWith( 'external:cost:http:request', expect.objectContaining( {
|
|
112
|
-
activityInfo,
|
|
113
|
-
workflowDetails,
|
|
114
|
-
outputActivityKind: 'step',
|
|
115
|
-
url: 'https://example.com'
|
|
116
|
-
} ) );
|
|
38
|
+
expect( emitMock ).toHaveBeenCalledWith( 'sdk:lifecycle:start', undefined );
|
|
117
39
|
} );
|
|
118
40
|
} );
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Client } from '@temporalio/client';
|
|
2
|
+
import { Storage } from '#async_storage';
|
|
3
|
+
import { FatalError } from '#errors';
|
|
4
|
+
|
|
5
|
+
// store the instance creation configs
|
|
6
|
+
const config = {
|
|
7
|
+
connection: null,
|
|
8
|
+
namespace: null
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// store a single instance that is created on demand
|
|
12
|
+
const instance = {
|
|
13
|
+
client: null
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Adds the config necessary to init Clients
|
|
17
|
+
export const setupClientConfig = ( { connection, namespace } ) => {
|
|
18
|
+
config.connection = new Proxy( connection, {
|
|
19
|
+
get( target, property, receiver ) {
|
|
20
|
+
if ( property === 'close' ) {
|
|
21
|
+
return async () => {
|
|
22
|
+
throw new FatalError( 'Client connection cannot be closed. It is owned by the worker.' );
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return Reflect.get( target, property, receiver );
|
|
26
|
+
}
|
|
27
|
+
} );
|
|
28
|
+
config.namespace = namespace;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Return a new Temporal client instance */
|
|
32
|
+
export const createTemporalClient = () => {
|
|
33
|
+
if ( !config.connection ) {
|
|
34
|
+
throw new FatalError( 'createTemporalClient() can only be called from within the worker runtime.' );
|
|
35
|
+
}
|
|
36
|
+
return new Client( { ...config } );
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Get the workflow handle for the current workflow execution */
|
|
40
|
+
export const getCurrentWorkflowHandle = () => {
|
|
41
|
+
const ctx = Storage.load();
|
|
42
|
+
if ( !ctx || !config.connection ) {
|
|
43
|
+
throw new FatalError( 'getCurrentWorkflowHandle() can only be called from Temporal activities running in the worker.' );
|
|
44
|
+
}
|
|
45
|
+
const { workflowId, runId } = ctx.activityInfo.workflowExecution;
|
|
46
|
+
return ( instance.client ??= new Client( config ) ).workflow.getHandle( workflowId, runId );
|
|
47
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const loadMock = vi.fn();
|
|
4
|
+
vi.mock( '#async_storage', () => ( {
|
|
5
|
+
Storage: { load: loadMock }
|
|
6
|
+
} ) );
|
|
7
|
+
|
|
8
|
+
const getHandleMock = vi.fn();
|
|
9
|
+
const clientConstructorMock = vi.fn( function () {
|
|
10
|
+
this.workflow = { getHandle: getHandleMock };
|
|
11
|
+
} );
|
|
12
|
+
vi.mock( '@temporalio/client', () => ( {
|
|
13
|
+
Client: clientConstructorMock
|
|
14
|
+
} ) );
|
|
15
|
+
|
|
16
|
+
const connection = {
|
|
17
|
+
fake: 'native-connection',
|
|
18
|
+
close: vi.fn().mockResolvedValue( undefined )
|
|
19
|
+
};
|
|
20
|
+
const activityContext = {
|
|
21
|
+
activityInfo: {
|
|
22
|
+
workflowExecution: {
|
|
23
|
+
workflowId: 'workflow-1',
|
|
24
|
+
runId: 'run-1'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe( 'Temporal client', () => {
|
|
30
|
+
beforeEach( () => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
vi.resetModules();
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
describe( 'createTemporalClient', () => {
|
|
36
|
+
it( 'throws a FatalError outside the worker runtime', async () => {
|
|
37
|
+
const { createTemporalClient } = await import( './client.js' );
|
|
38
|
+
const { FatalError } = await import( '#errors' );
|
|
39
|
+
|
|
40
|
+
expect( () => createTemporalClient() ).toThrow( FatalError );
|
|
41
|
+
} );
|
|
42
|
+
|
|
43
|
+
it( 'creates a client with the worker connection and namespace', async () => {
|
|
44
|
+
const { createTemporalClient, setupClientConfig } = await import( './client.js' );
|
|
45
|
+
setupClientConfig( { connection, namespace: 'namespace-1' } );
|
|
46
|
+
|
|
47
|
+
const client = createTemporalClient();
|
|
48
|
+
|
|
49
|
+
expect( clientConstructorMock ).toHaveBeenCalledWith( {
|
|
50
|
+
connection: expect.anything(),
|
|
51
|
+
namespace: 'namespace-1'
|
|
52
|
+
} );
|
|
53
|
+
expect( clientConstructorMock.mock.calls[0][0].connection ).not.toBe( connection );
|
|
54
|
+
expect( client ).toBeInstanceOf( clientConstructorMock );
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
it( 'delegates connection access but prevents closing the worker connection', async () => {
|
|
58
|
+
const { createTemporalClient, setupClientConfig } = await import( './client.js' );
|
|
59
|
+
const { FatalError } = await import( '#errors' );
|
|
60
|
+
setupClientConfig( { connection, namespace: 'namespace-1' } );
|
|
61
|
+
|
|
62
|
+
createTemporalClient();
|
|
63
|
+
const clientConnection = clientConstructorMock.mock.calls[0][0].connection;
|
|
64
|
+
|
|
65
|
+
expect( clientConnection.fake ).toBe( 'native-connection' );
|
|
66
|
+
await expect( clientConnection.close() ).rejects.toThrow( FatalError );
|
|
67
|
+
expect( connection.close ).not.toHaveBeenCalled();
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
it( 'returns a new client on each call', async () => {
|
|
71
|
+
const { createTemporalClient, setupClientConfig } = await import( './client.js' );
|
|
72
|
+
setupClientConfig( { connection, namespace: 'namespace-1' } );
|
|
73
|
+
|
|
74
|
+
expect( createTemporalClient() ).not.toBe( createTemporalClient() );
|
|
75
|
+
expect( clientConstructorMock ).toHaveBeenCalledTimes( 2 );
|
|
76
|
+
} );
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
describe( 'getCurrentWorkflowHandle', () => {
|
|
80
|
+
it( 'throws a FatalError outside a Temporal activity context', async () => {
|
|
81
|
+
loadMock.mockReturnValue( undefined );
|
|
82
|
+
const { getCurrentWorkflowHandle, setupClientConfig } = await import( './client.js' );
|
|
83
|
+
const { FatalError } = await import( '#errors' );
|
|
84
|
+
setupClientConfig( { connection, namespace: 'namespace-1' } );
|
|
85
|
+
|
|
86
|
+
expect( () => getCurrentWorkflowHandle() ).toThrow( FatalError );
|
|
87
|
+
} );
|
|
88
|
+
|
|
89
|
+
it( 'throws a FatalError outside the worker runtime', async () => {
|
|
90
|
+
loadMock.mockReturnValue( activityContext );
|
|
91
|
+
const { getCurrentWorkflowHandle } = await import( './client.js' );
|
|
92
|
+
const { FatalError } = await import( '#errors' );
|
|
93
|
+
|
|
94
|
+
expect( () => getCurrentWorkflowHandle() ).toThrow( FatalError );
|
|
95
|
+
} );
|
|
96
|
+
|
|
97
|
+
it( 'returns the handle for the current workflow execution', async () => {
|
|
98
|
+
const handle = { signal: vi.fn() };
|
|
99
|
+
loadMock.mockReturnValue( activityContext );
|
|
100
|
+
getHandleMock.mockReturnValue( handle );
|
|
101
|
+
const { getCurrentWorkflowHandle, setupClientConfig } = await import( './client.js' );
|
|
102
|
+
setupClientConfig( { connection, namespace: 'namespace-1' } );
|
|
103
|
+
|
|
104
|
+
expect( getCurrentWorkflowHandle() ).toBe( handle );
|
|
105
|
+
expect( clientConstructorMock ).toHaveBeenCalledWith( {
|
|
106
|
+
connection: expect.anything(),
|
|
107
|
+
namespace: 'namespace-1'
|
|
108
|
+
} );
|
|
109
|
+
expect( getHandleMock ).toHaveBeenCalledWith( 'workflow-1', 'run-1' );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( 'reuses its client across calls', async () => {
|
|
113
|
+
loadMock.mockReturnValue( activityContext );
|
|
114
|
+
const { getCurrentWorkflowHandle, setupClientConfig } = await import( './client.js' );
|
|
115
|
+
setupClientConfig( { connection, namespace: 'namespace-1' } );
|
|
116
|
+
|
|
117
|
+
getCurrentWorkflowHandle();
|
|
118
|
+
getCurrentWorkflowHandle();
|
|
119
|
+
|
|
120
|
+
expect( clientConstructorMock ).toHaveBeenCalledTimes( 1 );
|
|
121
|
+
expect( getHandleMock ).toHaveBeenCalledTimes( 2 );
|
|
122
|
+
} );
|
|
123
|
+
} );
|
|
124
|
+
} );
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Client, WorkflowHandle } from '@temporalio/client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Worker-side access to the Temporal layer.
|
|
5
|
+
*
|
|
6
|
+
* `createTemporalClient()` is available after the worker establishes its Temporal
|
|
7
|
+
* connection. `getCurrentWorkflowHandle()` is only available from Temporal Activities.
|
|
8
|
+
*
|
|
9
|
+
* > [!WARNING]
|
|
10
|
+
* > These are not Temporal sandbox safe, do not import in workflows directly.
|
|
11
|
+
* > These require the worker runtime, so in unit tests, mock the functions.
|
|
12
|
+
*
|
|
13
|
+
* @packageDocumentation
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a Temporal [Client](https://typescript.temporal.io/api/classes/client.Client)
|
|
18
|
+
* instance with the same connection and namespace as the worker.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* - Only available after the worker establishes its Temporal connection.
|
|
22
|
+
*/
|
|
23
|
+
export declare const createTemporalClient: () => Client;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return the [WorkflowHandle](https://typescript.temporal.io/api/interfaces/client.WorkflowHandle)
|
|
27
|
+
* for the current workflow execution.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* - Only available from a Temporal Activity running in the worker.
|
|
31
|
+
*/
|
|
32
|
+
export declare const getCurrentWorkflowHandle: () => WorkflowHandle;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createTemporalClient, getCurrentWorkflowHandle } from './client.js';
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Storage } from '#async_storage';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
-
import { serializeError } from '
|
|
3
|
+
import { serializeError } from '#helpers/error_serializer';
|
|
4
4
|
import { isStringboolTrue } from '#helpers/string';
|
|
5
5
|
import * as localProcessor from './processors/local/index.js';
|
|
6
6
|
import * as s3Processor from './processors/s3/index.js';
|
|
7
7
|
import { createChildLogger } from '#logger';
|
|
8
8
|
import { EventAction } from './trace_consts.js';
|
|
9
9
|
import { BaseAttribute } from './trace_attribute.js';
|
|
10
|
+
import { TransparentFatalError } from '#errors';
|
|
10
11
|
|
|
11
12
|
const log = createChildLogger( 'Tracing' );
|
|
12
13
|
|
|
@@ -51,7 +52,7 @@ export const init = async () => {
|
|
|
51
52
|
try {
|
|
52
53
|
await p.exec( ...args );
|
|
53
54
|
} catch ( error ) {
|
|
54
|
-
log.error( 'Processor execution error', { processor: p.name, error: error
|
|
55
|
+
log.error( 'Processor execution error', { processor: p.name, error: serializeError( error ) } );
|
|
55
56
|
}
|
|
56
57
|
} );
|
|
57
58
|
}
|
|
@@ -87,15 +88,12 @@ export const addEventAction = ( action, { kind, name, id, parentId, details, tra
|
|
|
87
88
|
export function addEventActionWithContext( action, options ) {
|
|
88
89
|
const storeContent = Storage.load();
|
|
89
90
|
if ( storeContent ) { // If there is no storageContext this was not called from a Temporal environment
|
|
90
|
-
const { parentId, traceInfo
|
|
91
|
-
if ( action === EventAction.ADD_ATTR ) {
|
|
92
|
-
|
|
93
|
-
if ( !( attribute instanceof BaseAttribute ) ) {
|
|
94
|
-
throw new Error( `Event ${EventAction.ADD_ATTR} argument is not a BaseAttribute instance` );
|
|
95
|
-
} else {
|
|
96
|
-
addAttribute( options.details );
|
|
97
|
-
}
|
|
91
|
+
const { parentId, traceInfo } = storeContent;
|
|
92
|
+
if ( action === EventAction.ADD_ATTR && !( options.details instanceof BaseAttribute ) ) {
|
|
93
|
+
throw new Error( `Event ${EventAction.ADD_ATTR} argument is not a BaseAttribute instance` );
|
|
98
94
|
}
|
|
99
|
-
|
|
95
|
+
/** Unwraps Transparent error set in details so it behaves like in the interceptors */
|
|
96
|
+
const details = options.details instanceof TransparentFatalError ? options.details.cause : options.details;
|
|
97
|
+
addEventAction( action, { ...options, details, parentId, traceInfo } );
|
|
100
98
|
}
|
|
101
99
|
};
|