@outputai/core 0.3.3-dev.422151e.0 → 0.3.3-dev.e8eff63.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/core",
|
|
3
|
-
"version": "0.3.3-dev.
|
|
3
|
+
"version": "0.3.3-dev.e8eff63.0",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -45,7 +45,12 @@
|
|
|
45
45
|
"redis": "5.12.1",
|
|
46
46
|
"stacktrace-parser": "0.1.11",
|
|
47
47
|
"undici": "8.1.0",
|
|
48
|
-
"winston": "3.19.0"
|
|
48
|
+
"winston": "3.19.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"zod": "^4.3.6"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
49
54
|
"zod": "4.3.6"
|
|
50
55
|
},
|
|
51
56
|
"license": "Apache-2.0",
|
package/src/consts.js
CHANGED
|
@@ -139,9 +139,9 @@ export type WorkflowFunction<
|
|
|
139
139
|
*/
|
|
140
140
|
export type WorkflowFunctionWrapper<WorkflowFunction> =
|
|
141
141
|
[Parameters<WorkflowFunction>[0]] extends [undefined | null] ?
|
|
142
|
-
( input?: undefined | null, config?: WorkflowInvocationConfiguration
|
|
142
|
+
( input?: undefined | null, config?: WorkflowInvocationConfiguration ) =>
|
|
143
143
|
ReturnType<WorkflowFunction> :
|
|
144
|
-
( input: Parameters<WorkflowFunction>[0], config?: WorkflowInvocationConfiguration
|
|
144
|
+
( input: Parameters<WorkflowFunction>[0], config?: WorkflowInvocationConfiguration ) =>
|
|
145
145
|
ReturnType<WorkflowFunction>;
|
|
146
146
|
|
|
147
147
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
|
-
import { workflowInfo, proxySinks, ApplicationFailure, ContinueAsNew } from '@temporalio/workflow';
|
|
2
|
+
import { workflowInfo, proxySinks, ApplicationFailure, ContinueAsNew, isCancellation } from '@temporalio/workflow';
|
|
3
3
|
import { memoToHeaders } from '../sandboxed_utils.js';
|
|
4
4
|
import { deepMerge } from '#utils';
|
|
5
|
-
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
5
|
+
import { METADATA_ACCESS_SYMBOL, WorkflowSpecialOutput } from '#consts';
|
|
6
6
|
// this is a dynamic generated file with activity configs overwrites
|
|
7
7
|
import stepOptions from '../temp/__activity_options.js';
|
|
8
8
|
|
|
@@ -44,7 +44,12 @@ class WorkflowExecutionInterceptor {
|
|
|
44
44
|
* a new trace file will be generated
|
|
45
45
|
*/
|
|
46
46
|
if ( error instanceof ContinueAsNew ) {
|
|
47
|
-
sinks.workflow.end(
|
|
47
|
+
sinks.workflow.end( WorkflowSpecialOutput.CONTINUED_AS_NEW );
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ( isCancellation( error ) ) {
|
|
52
|
+
sinks.workflow.error( error );
|
|
48
53
|
throw error;
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -6,6 +6,7 @@ const workflowInfoMock = vi.fn();
|
|
|
6
6
|
const workflowStartMock = vi.fn();
|
|
7
7
|
const workflowEndMock = vi.fn();
|
|
8
8
|
const workflowErrorMock = vi.fn();
|
|
9
|
+
const isCancellationMock = vi.fn();
|
|
9
10
|
vi.mock( '@temporalio/workflow', () => ( {
|
|
10
11
|
workflowInfo: ( ...args ) => workflowInfoMock( ...args ),
|
|
11
12
|
proxySinks: () => ( {
|
|
@@ -26,7 +27,8 @@ vi.mock( '@temporalio/workflow', () => ( {
|
|
|
26
27
|
super( 'ContinueAsNew' );
|
|
27
28
|
this.name = 'ContinueAsNew';
|
|
28
29
|
}
|
|
29
|
-
}
|
|
30
|
+
},
|
|
31
|
+
isCancellation: ( ...args ) => isCancellationMock( ...args )
|
|
30
32
|
} ) );
|
|
31
33
|
|
|
32
34
|
const memoToHeadersMock = vi.fn( memo => ( memo ? { ...memo, __asHeaders: true } : {} ) );
|
|
@@ -50,6 +52,7 @@ vi.mock( '../temp/__activity_options.js', () => ( { default: stepOptionsDefault
|
|
|
50
52
|
describe( 'workflow interceptors', () => {
|
|
51
53
|
beforeEach( () => {
|
|
52
54
|
vi.clearAllMocks();
|
|
55
|
+
isCancellationMock.mockReturnValue( false );
|
|
53
56
|
workflowInfoMock.mockReturnValue( { workflowType: 'MyWorkflow', memo: { executionContext: { id: 'ctx-1' } } } );
|
|
54
57
|
} );
|
|
55
58
|
|
|
@@ -151,8 +154,25 @@ describe( 'workflow interceptors', () => {
|
|
|
151
154
|
expect( error.details ).toEqual( [ meta ] );
|
|
152
155
|
} );
|
|
153
156
|
|
|
157
|
+
it( 'calls sinks.workflow.error and rethrows cancellation errors without wrapping', async () => {
|
|
158
|
+
const { interceptors } = await import( './workflow.js' );
|
|
159
|
+
const { ApplicationFailure } = await import( '@temporalio/workflow' );
|
|
160
|
+
const { inbound } = interceptors();
|
|
161
|
+
const interceptor = inbound[0];
|
|
162
|
+
const cancellation = new Error( 'Workflow cancelled' );
|
|
163
|
+
const next = vi.fn().mockRejectedValue( cancellation );
|
|
164
|
+
isCancellationMock.mockReturnValue( true );
|
|
165
|
+
|
|
166
|
+
await expect( interceptor.execute( { args: [ {} ] }, next ) ).rejects.toBe( cancellation );
|
|
167
|
+
expect( isCancellationMock ).toHaveBeenCalledWith( cancellation );
|
|
168
|
+
expect( cancellation ).not.toBeInstanceOf( ApplicationFailure );
|
|
169
|
+
expect( workflowErrorMock ).toHaveBeenCalledWith( cancellation );
|
|
170
|
+
expect( workflowEndMock ).not.toHaveBeenCalled();
|
|
171
|
+
} );
|
|
172
|
+
|
|
154
173
|
it( 'on ContinueAsNew calls sinks.trace.addWorkflowEventEnd and rethrows', async () => {
|
|
155
174
|
const { ContinueAsNew } = await import( '@temporalio/workflow' );
|
|
175
|
+
const { WorkflowSpecialOutput } = await import( '#consts' );
|
|
156
176
|
const { interceptors } = await import( './workflow.js' );
|
|
157
177
|
const { inbound } = interceptors();
|
|
158
178
|
const interceptor = inbound[0];
|
|
@@ -160,7 +180,7 @@ describe( 'workflow interceptors', () => {
|
|
|
160
180
|
const next = vi.fn().mockRejectedValue( continueErr );
|
|
161
181
|
|
|
162
182
|
await expect( interceptor.execute( { args: [ {} ] }, next ) ).rejects.toThrow( ContinueAsNew );
|
|
163
|
-
expect( workflowEndMock ).toHaveBeenCalledWith(
|
|
183
|
+
expect( workflowEndMock ).toHaveBeenCalledWith( WorkflowSpecialOutput.CONTINUED_AS_NEW );
|
|
164
184
|
expect( workflowErrorMock ).not.toHaveBeenCalled();
|
|
165
185
|
} );
|
|
166
186
|
} );
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Client } from '@temporalio/client';
|
|
1
|
+
import { Client, WorkflowNotFoundError } from '@temporalio/client';
|
|
2
2
|
import { WorkflowIdConflictPolicy } from '@temporalio/common';
|
|
3
3
|
import { WORKFLOW_CATALOG } from '#consts';
|
|
4
4
|
import { catalogId, taskQueue } from './configs.js';
|
|
@@ -8,12 +8,29 @@ const log = createChildLogger( 'Catalog' );
|
|
|
8
8
|
|
|
9
9
|
export const startCatalog = async ( { connection, namespace, catalog } ) => {
|
|
10
10
|
const client = new Client( { connection, namespace } );
|
|
11
|
+
const catalogWorkflowHandle = client.workflow.getHandle( catalogId );
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const catalogWorkflowDescription = await catalogWorkflowHandle.describe();
|
|
15
|
+
if ( !catalogWorkflowDescription.closeTime ) {
|
|
16
|
+
log.info( 'Completing previous catalog workflow...' );
|
|
17
|
+
await catalogWorkflowHandle.executeUpdate( 'complete', { args: [] } );
|
|
18
|
+
}
|
|
19
|
+
} catch ( error ) {
|
|
20
|
+
// When "not found", it's either a cold start or the catalog was already stopped/terminated, ignore it.
|
|
21
|
+
// Otherwise, create a log and try the next operation:
|
|
22
|
+
// A. If the workflow is still running, the start() will fail and throw;
|
|
23
|
+
// B. If the workflow is no running, the start() will succeed, and the error was transient;
|
|
24
|
+
if ( !( error instanceof WorkflowNotFoundError ) ) {
|
|
25
|
+
log.warn( 'Error interacting with previous catalog workflow', { error } );
|
|
26
|
+
}
|
|
27
|
+
}
|
|
11
28
|
|
|
12
29
|
log.info( 'Starting catalog workflow...' );
|
|
13
30
|
await client.workflow.start( WORKFLOW_CATALOG, {
|
|
14
31
|
taskQueue,
|
|
15
|
-
workflowId: catalogId,
|
|
16
|
-
workflowIdConflictPolicy: WorkflowIdConflictPolicy.
|
|
32
|
+
workflowId: catalogId, // use the name of the task queue as the catalog name, ensuring uniqueness
|
|
33
|
+
workflowIdConflictPolicy: WorkflowIdConflictPolicy.FAIL,
|
|
17
34
|
args: [ catalog ]
|
|
18
35
|
} );
|
|
19
36
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { WorkflowNotFoundError } from '@temporalio/client';
|
|
2
3
|
|
|
3
4
|
const mockLog = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
4
5
|
vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
|
|
@@ -9,18 +10,25 @@ const catalogId = 'test-catalog';
|
|
|
9
10
|
const taskQueue = 'test-queue';
|
|
10
11
|
vi.mock( './configs.js', () => ( { catalogId, taskQueue } ) );
|
|
11
12
|
|
|
13
|
+
const describeMock = vi.fn();
|
|
14
|
+
const executeUpdateMock = vi.fn();
|
|
12
15
|
const workflowStartMock = vi.fn().mockResolvedValue( undefined );
|
|
13
16
|
vi.mock( '@temporalio/client', async importOriginal => {
|
|
14
17
|
const actual = await importOriginal();
|
|
15
18
|
return {
|
|
16
19
|
...actual,
|
|
17
20
|
Client: vi.fn().mockImplementation( function () {
|
|
18
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
workflow: {
|
|
23
|
+
start: workflowStartMock,
|
|
24
|
+
getHandle: () => ( { describe: describeMock, executeUpdate: executeUpdateMock } )
|
|
25
|
+
}
|
|
26
|
+
};
|
|
19
27
|
} )
|
|
20
28
|
};
|
|
21
29
|
} );
|
|
22
30
|
|
|
23
|
-
vi.mock( '@temporalio/common', () => ( { WorkflowIdConflictPolicy: {
|
|
31
|
+
vi.mock( '@temporalio/common', () => ( { WorkflowIdConflictPolicy: { FAIL: 'FAIL' } } ) );
|
|
24
32
|
|
|
25
33
|
describe( 'worker/start_catalog', () => {
|
|
26
34
|
const mockConnection = {};
|
|
@@ -32,24 +40,79 @@ describe( 'worker/start_catalog', () => {
|
|
|
32
40
|
workflowStartMock.mockResolvedValue( undefined );
|
|
33
41
|
} );
|
|
34
42
|
|
|
35
|
-
it( '
|
|
43
|
+
it( 'when previous catalog still running: completes it then starts catalog workflow', async () => {
|
|
44
|
+
describeMock.mockResolvedValue( { closeTime: undefined } );
|
|
45
|
+
executeUpdateMock.mockResolvedValue( undefined );
|
|
46
|
+
|
|
47
|
+
const { startCatalog } = await import( './start_catalog.js' );
|
|
48
|
+
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
49
|
+
|
|
50
|
+
expect( describeMock ).toHaveBeenCalled();
|
|
51
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Completing previous catalog workflow...' );
|
|
52
|
+
expect( executeUpdateMock ).toHaveBeenCalledWith( 'complete', { args: [] } );
|
|
53
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
54
|
+
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
55
|
+
taskQueue,
|
|
56
|
+
workflowId: catalogId,
|
|
57
|
+
workflowIdConflictPolicy: 'FAIL',
|
|
58
|
+
args: [ catalog ]
|
|
59
|
+
} );
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
it( 'when no previous catalog: ignores and starts catalog workflow', async () => {
|
|
63
|
+
describeMock.mockRejectedValue( new WorkflowNotFoundError( 'not found' ) );
|
|
64
|
+
|
|
65
|
+
const { startCatalog } = await import( './start_catalog.js' );
|
|
66
|
+
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
67
|
+
|
|
68
|
+
expect( describeMock ).toHaveBeenCalled();
|
|
69
|
+
expect( mockLog.warn ).not.toHaveBeenCalled();
|
|
70
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
71
|
+
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
72
|
+
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
73
|
+
taskQueue,
|
|
74
|
+
workflowId: catalogId,
|
|
75
|
+
workflowIdConflictPolicy: 'FAIL',
|
|
76
|
+
args: [ catalog ]
|
|
77
|
+
} );
|
|
78
|
+
} );
|
|
79
|
+
|
|
80
|
+
it( 'when previous catalog already closed: skips complete and starts catalog workflow', async () => {
|
|
81
|
+
describeMock.mockResolvedValue( { closeTime: '2024-01-01T00:00:00Z' } );
|
|
82
|
+
|
|
36
83
|
const { startCatalog } = await import( './start_catalog.js' );
|
|
37
84
|
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
38
85
|
|
|
86
|
+
expect( describeMock ).toHaveBeenCalled();
|
|
87
|
+
expect( mockLog.info ).not.toHaveBeenCalledWith( 'Completing previous catalog workflow...' );
|
|
88
|
+
expect( executeUpdateMock ).not.toHaveBeenCalled();
|
|
39
89
|
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
40
90
|
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
41
91
|
taskQueue,
|
|
42
92
|
workflowId: catalogId,
|
|
43
|
-
workflowIdConflictPolicy: '
|
|
93
|
+
workflowIdConflictPolicy: 'FAIL',
|
|
44
94
|
args: [ catalog ]
|
|
45
95
|
} );
|
|
46
96
|
} );
|
|
47
97
|
|
|
48
|
-
it( '
|
|
49
|
-
|
|
98
|
+
it( 'when describe or complete fails with other error: logs warn and still starts catalog workflow', async () => {
|
|
99
|
+
describeMock.mockResolvedValue( { closeTime: undefined } );
|
|
100
|
+
executeUpdateMock.mockRejectedValue( new Error( 'Connection refused' ) );
|
|
50
101
|
|
|
51
102
|
const { startCatalog } = await import( './start_catalog.js' );
|
|
52
|
-
await
|
|
53
|
-
|
|
103
|
+
await startCatalog( { connection: mockConnection, namespace, catalog } );
|
|
104
|
+
|
|
105
|
+
expect( describeMock ).toHaveBeenCalled();
|
|
106
|
+
expect( executeUpdateMock ).toHaveBeenCalledWith( 'complete', { args: [] } );
|
|
107
|
+
expect( mockLog.warn ).toHaveBeenCalledWith( 'Error interacting with previous catalog workflow', {
|
|
108
|
+
error: expect.any( Error )
|
|
109
|
+
} );
|
|
110
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Starting catalog workflow...' );
|
|
111
|
+
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
112
|
+
taskQueue,
|
|
113
|
+
workflowId: catalogId,
|
|
114
|
+
workflowIdConflictPolicy: 'FAIL',
|
|
115
|
+
args: [ catalog ]
|
|
116
|
+
} );
|
|
54
117
|
} );
|
|
55
118
|
} );
|