@output.ai/core 0.5.10 → 0.5.11
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/healthcheck.spec.js +3 -3
- package/package.json +19 -15
- package/src/context/index.d.ts +27 -0
- package/src/context/index.js +17 -0
- package/src/context/index.spec.js +42 -0
- package/src/worker/index.js +1 -1
- package/src/worker/index.spec.js +2 -1
- package/src/worker/interceptors/activity.js +8 -2
- package/src/worker/interceptors/activity.spec.js +20 -8
- package/src/worker/interceptors.js +2 -2
- package/src/worker/start_catalog.spec.js +8 -6
package/bin/healthcheck.spec.js
CHANGED
|
@@ -5,9 +5,9 @@ const connectionMock = { close: closeMock };
|
|
|
5
5
|
|
|
6
6
|
const queryMock = vi.fn();
|
|
7
7
|
const getHandleMock = vi.fn().mockReturnValue( { query: queryMock } );
|
|
8
|
-
const ClientMock = vi.fn().mockImplementation( ()
|
|
9
|
-
workflow: { getHandle: getHandleMock }
|
|
10
|
-
} )
|
|
8
|
+
const ClientMock = vi.fn().mockImplementation( function () {
|
|
9
|
+
return { workflow: { getHandle: getHandleMock } };
|
|
10
|
+
} );
|
|
11
11
|
const connectMock = vi.fn().mockResolvedValue( connectionMock );
|
|
12
12
|
|
|
13
13
|
vi.mock( '@temporalio/client', () => ( {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@output.ai/core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"./hooks": {
|
|
20
20
|
"types": "./src/hooks/index.d.ts",
|
|
21
21
|
"import": "./src/hooks/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./context": {
|
|
24
|
+
"types": "./src/context/index.d.ts",
|
|
25
|
+
"import": "./src/context/index.js"
|
|
22
26
|
}
|
|
23
27
|
},
|
|
24
28
|
"files": [
|
|
@@ -31,21 +35,21 @@
|
|
|
31
35
|
"output-healthcheck": "./bin/healthcheck.mjs"
|
|
32
36
|
},
|
|
33
37
|
"dependencies": {
|
|
34
|
-
"@aws-sdk/client-s3": "3.
|
|
35
|
-
"@babel/generator": "7.
|
|
36
|
-
"@babel/parser": "7.
|
|
37
|
-
"@babel/traverse": "7.
|
|
38
|
-
"@babel/types": "7.
|
|
39
|
-
"@temporalio/activity": "1.
|
|
40
|
-
"@temporalio/client": "1.
|
|
41
|
-
"@temporalio/common": "1.
|
|
42
|
-
"@temporalio/worker": "1.
|
|
43
|
-
"@temporalio/workflow": "1.
|
|
44
|
-
"redis": "5.
|
|
38
|
+
"@aws-sdk/client-s3": "3.1000.0",
|
|
39
|
+
"@babel/generator": "7.29.1",
|
|
40
|
+
"@babel/parser": "7.29.0",
|
|
41
|
+
"@babel/traverse": "7.29.0",
|
|
42
|
+
"@babel/types": "7.29.0",
|
|
43
|
+
"@temporalio/activity": "1.15.0",
|
|
44
|
+
"@temporalio/client": "1.15.0",
|
|
45
|
+
"@temporalio/common": "1.15.0",
|
|
46
|
+
"@temporalio/worker": "1.15.0",
|
|
47
|
+
"@temporalio/workflow": "1.15.0",
|
|
48
|
+
"redis": "5.11.0",
|
|
45
49
|
"stacktrace-parser": "0.1.11",
|
|
46
|
-
"undici": "7.
|
|
47
|
-
"winston": "3.
|
|
48
|
-
"zod": "
|
|
50
|
+
"undici": "7.22.0",
|
|
51
|
+
"winston": "3.19.0",
|
|
52
|
+
"zod": "4.3.6"
|
|
49
53
|
},
|
|
50
54
|
"license": "Apache-2.0",
|
|
51
55
|
"publishConfig": {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context returned by {@link getContext} when running inside a Temporal Activity (step or evaluator).
|
|
3
|
+
*/
|
|
4
|
+
export type Context = {
|
|
5
|
+
/** Information about the current workflow execution */
|
|
6
|
+
workflow: {
|
|
7
|
+
/** Temporal's workflow execution id */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Workflow name (Temporal's workflow "type" value) */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Path of the workflow file */
|
|
12
|
+
filename: string;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns information about the current Temporal execution.
|
|
18
|
+
*
|
|
19
|
+
* Only available when called from within a step or evaluator (Temporal Activities) running in the Temporal runtime.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* - Returns `null` when not called inside a Temporal Activity (steps/evaluators);
|
|
23
|
+
* - Returns `null` when not called from within a running Temporal worker, like in unit tests environment;
|
|
24
|
+
*
|
|
25
|
+
* @returns The workflow context, or `null` if unavailable or incomplete.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getContext(): Context | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Storage } from '#async_storage';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns information trapped on AsyncStorage about the workflow invoking an activity
|
|
5
|
+
*
|
|
6
|
+
* @returns {object}
|
|
7
|
+
*/
|
|
8
|
+
export const getContext = () => {
|
|
9
|
+
const ctx = Storage.load();
|
|
10
|
+
|
|
11
|
+
if ( !ctx?.executionContext || !ctx?.workflowFilename ) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { workflowId: id, workflowName: name } = ctx.executionContext;
|
|
16
|
+
return { workflow: { id, name, filename: ctx.workflowFilename } };
|
|
17
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const loadMock = vi.fn();
|
|
4
|
+
vi.mock( '#async_storage', () => ( {
|
|
5
|
+
Storage: { load: loadMock }
|
|
6
|
+
} ) );
|
|
7
|
+
|
|
8
|
+
describe( 'context/getContext', () => {
|
|
9
|
+
beforeEach( () => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
vi.resetModules();
|
|
12
|
+
} );
|
|
13
|
+
|
|
14
|
+
it( 'returns null when no context is stored', async () => {
|
|
15
|
+
loadMock.mockReturnValue( undefined );
|
|
16
|
+
const { getContext } = await import( './index.js' );
|
|
17
|
+
expect( getContext() ).toBeNull();
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
it( 'returns null when executionContext is missing', async () => {
|
|
21
|
+
loadMock.mockReturnValue( { workflowFilename: '/workflows/foo.js' } );
|
|
22
|
+
const { getContext } = await import( './index.js' );
|
|
23
|
+
expect( getContext() ).toBeNull();
|
|
24
|
+
} );
|
|
25
|
+
|
|
26
|
+
it( 'returns null when workflowFilename is missing', async () => {
|
|
27
|
+
loadMock.mockReturnValue( { executionContext: { workflowId: 'wf-1', workflowName: 'myWorkflow' } } );
|
|
28
|
+
const { getContext } = await import( './index.js' );
|
|
29
|
+
expect( getContext() ).toBeNull();
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
it( 'returns workflow context when storage has full context', async () => {
|
|
33
|
+
loadMock.mockReturnValue( {
|
|
34
|
+
executionContext: { workflowId: 'wf-1', workflowName: 'myWorkflow' },
|
|
35
|
+
workflowFilename: '/workflows/myWorkflow.js'
|
|
36
|
+
} );
|
|
37
|
+
const { getContext } = await import( './index.js' );
|
|
38
|
+
expect( getContext() ).toEqual( {
|
|
39
|
+
workflow: { id: 'wf-1', name: 'myWorkflow', filename: '/workflows/myWorkflow.js' }
|
|
40
|
+
} );
|
|
41
|
+
} );
|
|
42
|
+
} );
|
package/src/worker/index.js
CHANGED
|
@@ -59,7 +59,7 @@ const callerDir = process.argv[2];
|
|
|
59
59
|
workflowsPath,
|
|
60
60
|
activities,
|
|
61
61
|
sinks,
|
|
62
|
-
interceptors: initInterceptors( { activities } ),
|
|
62
|
+
interceptors: initInterceptors( { activities, workflows } ),
|
|
63
63
|
maxConcurrentWorkflowTaskExecutions,
|
|
64
64
|
maxConcurrentActivityTaskExecutions,
|
|
65
65
|
maxCachedWorkflows,
|
package/src/worker/index.spec.js
CHANGED
|
@@ -112,7 +112,7 @@ describe( 'worker/index', () => {
|
|
|
112
112
|
maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
|
|
113
113
|
maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
|
|
114
114
|
} ) );
|
|
115
|
-
expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {} } );
|
|
115
|
+
expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {}, workflows: [] } );
|
|
116
116
|
expect( registerShutdownMock ).toHaveBeenCalledWith( { worker: mockWorker, log: mockLog } );
|
|
117
117
|
expect( startCatalogMock ).toHaveBeenCalledWith( {
|
|
118
118
|
connection: mockConnection,
|
|
@@ -140,6 +140,7 @@ describe( 'worker/index', () => {
|
|
|
140
140
|
apiKey: 'secret'
|
|
141
141
|
} ) );
|
|
142
142
|
} );
|
|
143
|
+
runState.resolve();
|
|
143
144
|
await vi.waitFor( () => expect( exitMock ).toHaveBeenCalled() );
|
|
144
145
|
} );
|
|
145
146
|
|
|
@@ -25,8 +25,9 @@ const log = createChildLogger( 'Activity' );
|
|
|
25
25
|
- Headers injected by the workflow interceptor (executionContext)
|
|
26
26
|
*/
|
|
27
27
|
export class ActivityExecutionInterceptor {
|
|
28
|
-
constructor( activities ) {
|
|
28
|
+
constructor( { activities, workflows } ) {
|
|
29
29
|
this.activities = activities;
|
|
30
|
+
this.workflowsMap = workflows.reduce( ( map, w ) => map.set( w.name, w ), new Map() );
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
async execute( input, next ) {
|
|
@@ -46,7 +47,12 @@ export class ActivityExecutionInterceptor {
|
|
|
46
47
|
// Sends heartbeat to communicate that activity is still alive
|
|
47
48
|
intervals.heartbeat = activityHeartbeatEnabled && setInterval( () => Context.current().heartbeat(), activityHeartbeatIntervalMs );
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
// Wraps the execution with accessible metadata for the activity
|
|
51
|
+
const output = await Storage.runWithContext( async _ => next( input ), {
|
|
52
|
+
parentId: activityId,
|
|
53
|
+
executionContext,
|
|
54
|
+
workflowFilename: this.workflowsMap.get( workflowName ).path
|
|
55
|
+
} );
|
|
50
56
|
|
|
51
57
|
log.info( `Ended ${activityType} ${kind}`, { event: LifecycleEvent.END, kind, ...logContext, durationMs: Date.now() - startDate } );
|
|
52
58
|
addEventEnd( { details: output, ...traceArguments } );
|
|
@@ -3,10 +3,12 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
3
3
|
const METADATA_ACCESS_SYMBOL = vi.hoisted( () => Symbol( '__metadata' ) );
|
|
4
4
|
|
|
5
5
|
const heartbeatMock = vi.fn();
|
|
6
|
+
const runWithContextMock = vi.hoisted( () => vi.fn().mockImplementation( async fn => fn() ) );
|
|
6
7
|
const contextInfoMock = {
|
|
7
8
|
workflowExecution: { workflowId: 'wf-1' },
|
|
8
9
|
activityId: 'act-1',
|
|
9
|
-
activityType: 'myWorkflow#myStep'
|
|
10
|
+
activityType: 'myWorkflow#myStep',
|
|
11
|
+
workflowType: 'myWorkflow'
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
vi.mock( '@temporalio/activity', () => ( {
|
|
@@ -20,7 +22,7 @@ vi.mock( '@temporalio/activity', () => ( {
|
|
|
20
22
|
|
|
21
23
|
vi.mock( '#async_storage', () => ( {
|
|
22
24
|
Storage: {
|
|
23
|
-
runWithContext:
|
|
25
|
+
runWithContext: runWithContextMock
|
|
24
26
|
}
|
|
25
27
|
} ) );
|
|
26
28
|
|
|
@@ -59,6 +61,8 @@ const makeActivities = () => ( {
|
|
|
59
61
|
'myWorkflow#myStep': { [METADATA_ACCESS_SYMBOL]: { type: 'step' } }
|
|
60
62
|
} );
|
|
61
63
|
|
|
64
|
+
const makeWorkflows = () => [ { name: 'myWorkflow', path: '/workflows/myWorkflow.js' } ];
|
|
65
|
+
|
|
62
66
|
const makeInput = () => ( {
|
|
63
67
|
args: [ { someInput: 'data' } ],
|
|
64
68
|
headers: {}
|
|
@@ -81,7 +85,7 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
81
85
|
|
|
82
86
|
it( 'records trace start and end events on successful execution', async () => {
|
|
83
87
|
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
84
|
-
const interceptor = new ActivityExecutionInterceptor( makeActivities() );
|
|
88
|
+
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
85
89
|
const next = vi.fn().mockResolvedValue( { result: 'ok' } );
|
|
86
90
|
|
|
87
91
|
const promise = interceptor.execute( makeInput(), next );
|
|
@@ -92,11 +96,19 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
92
96
|
expect( addEventStartMock ).toHaveBeenCalledOnce();
|
|
93
97
|
expect( addEventEndMock ).toHaveBeenCalledOnce();
|
|
94
98
|
expect( addEventErrorMock ).not.toHaveBeenCalled();
|
|
99
|
+
expect( runWithContextMock ).toHaveBeenCalledWith(
|
|
100
|
+
expect.any( Function ),
|
|
101
|
+
{
|
|
102
|
+
parentId: 'act-1',
|
|
103
|
+
executionContext: { workflowId: 'wf-1' },
|
|
104
|
+
workflowFilename: '/workflows/myWorkflow.js'
|
|
105
|
+
}
|
|
106
|
+
);
|
|
95
107
|
} );
|
|
96
108
|
|
|
97
109
|
it( 'records trace error event on failed execution', async () => {
|
|
98
110
|
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
99
|
-
const interceptor = new ActivityExecutionInterceptor( makeActivities() );
|
|
111
|
+
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
100
112
|
const error = new Error( 'step failed' );
|
|
101
113
|
const next = vi.fn().mockRejectedValue( error );
|
|
102
114
|
|
|
@@ -111,7 +123,7 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
111
123
|
|
|
112
124
|
it( 'sends periodic heartbeats during activity execution', async () => {
|
|
113
125
|
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
114
|
-
const interceptor = new ActivityExecutionInterceptor( makeActivities() );
|
|
126
|
+
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
115
127
|
|
|
116
128
|
// next() resolves only after we manually resolve it, simulating a long-running activity
|
|
117
129
|
const deferred = { resolve: null };
|
|
@@ -138,7 +150,7 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
138
150
|
|
|
139
151
|
it( 'clears heartbeat interval after activity completes', async () => {
|
|
140
152
|
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
141
|
-
const interceptor = new ActivityExecutionInterceptor( makeActivities() );
|
|
153
|
+
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
142
154
|
const next = vi.fn().mockResolvedValue( { result: 'ok' } );
|
|
143
155
|
|
|
144
156
|
const promise = interceptor.execute( makeInput(), next );
|
|
@@ -152,7 +164,7 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
152
164
|
|
|
153
165
|
it( 'clears heartbeat interval after activity fails', async () => {
|
|
154
166
|
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
155
|
-
const interceptor = new ActivityExecutionInterceptor( makeActivities() );
|
|
167
|
+
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
156
168
|
const next = vi.fn().mockRejectedValue( new Error( 'boom' ) );
|
|
157
169
|
|
|
158
170
|
const promise = interceptor.execute( makeInput(), next );
|
|
@@ -167,7 +179,7 @@ describe( 'ActivityExecutionInterceptor', () => {
|
|
|
167
179
|
it( 'does not heartbeat when OUTPUT_ACTIVITY_HEARTBEAT_ENABLED is false', async () => {
|
|
168
180
|
vi.stubEnv( 'OUTPUT_ACTIVITY_HEARTBEAT_ENABLED', 'false' );
|
|
169
181
|
const { ActivityExecutionInterceptor } = await import( './activity.js' );
|
|
170
|
-
const interceptor = new ActivityExecutionInterceptor( makeActivities() );
|
|
182
|
+
const interceptor = new ActivityExecutionInterceptor( { activities: makeActivities(), workflows: makeWorkflows() } );
|
|
171
183
|
|
|
172
184
|
const deferred = { resolve: null };
|
|
173
185
|
const next = vi.fn().mockImplementation( () => new Promise( r => {
|
|
@@ -4,7 +4,7 @@ import { ActivityExecutionInterceptor } from './interceptors/activity.js';
|
|
|
4
4
|
|
|
5
5
|
const __dirname = dirname( fileURLToPath( import.meta.url ) );
|
|
6
6
|
|
|
7
|
-
export const initInterceptors = ( { activities } ) => ( {
|
|
7
|
+
export const initInterceptors = ( { activities, workflows } ) => ( {
|
|
8
8
|
workflowModules: [ join( __dirname, './interceptors/workflow.js' ) ],
|
|
9
|
-
activityInbound: [ () => new ActivityExecutionInterceptor( activities ) ]
|
|
9
|
+
activityInbound: [ () => new ActivityExecutionInterceptor( { activities, workflows } ) ]
|
|
10
10
|
} );
|
|
@@ -17,12 +17,14 @@ vi.mock( '@temporalio/client', async importOriginal => {
|
|
|
17
17
|
const actual = await importOriginal();
|
|
18
18
|
return {
|
|
19
19
|
...actual,
|
|
20
|
-
Client: vi.fn().mockImplementation( ()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
Client: vi.fn().mockImplementation( function () {
|
|
21
|
+
return {
|
|
22
|
+
workflow: {
|
|
23
|
+
start: workflowStartMock,
|
|
24
|
+
getHandle: () => ( { describe: describeMock, executeUpdate: executeUpdateMock } )
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
} )
|
|
26
28
|
};
|
|
27
29
|
} );
|
|
28
30
|
|