@output.ai/core 0.5.0 → 0.5.2
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 +1 -1
- package/src/errors.d.ts +15 -0
- package/src/index.d.ts +17 -864
- package/src/index.js +3 -28
- package/src/interface/evaluation_result.d.ts +160 -0
- package/src/interface/evaluation_result.js +202 -0
- package/src/interface/evaluator.d.ts +70 -0
- package/src/interface/evaluator.js +1 -202
- package/src/interface/evaluator.spec.js +1 -1
- package/src/interface/index.d.ts +9 -0
- package/src/interface/index.js +19 -0
- package/src/interface/step.d.ts +138 -0
- package/src/interface/step.js +1 -0
- package/src/interface/types.d.ts +27 -0
- package/src/interface/webhook.d.ts +84 -0
- package/src/interface/workflow.d.ts +273 -0
- package/src/interface/workflow.js +7 -38
- package/src/interface/workflow.spec.js +495 -0
- package/src/interface/workflow_context.js +31 -0
- package/src/interface/workflow_utils.d.ts +53 -0
- package/src/interface/workflow_utils.js +1 -0
- package/src/utils/index.d.ts +1 -9
- package/src/utils/utils.js +2 -12
- package/src/utils/utils.spec.js +39 -52
- package/src/worker/index.spec.js +180 -0
- package/src/worker/interceptors/workflow.js +2 -2
package/src/utils/utils.spec.js
CHANGED
|
@@ -2,7 +2,6 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
3
|
import {
|
|
4
4
|
clone,
|
|
5
|
-
mergeActivityOptions,
|
|
6
5
|
serializeBodyAndInferContentType,
|
|
7
6
|
serializeFetchResponse,
|
|
8
7
|
deepMerge,
|
|
@@ -223,50 +222,6 @@ describe( 'serializeBodyAndInferContentType', () => {
|
|
|
223
222
|
} );
|
|
224
223
|
} );
|
|
225
224
|
|
|
226
|
-
describe( 'mergeActivityOptions', () => {
|
|
227
|
-
it( 'recursively merges nested objects', () => {
|
|
228
|
-
const base = {
|
|
229
|
-
taskQueue: 'q1',
|
|
230
|
-
retry: { maximumAttempts: 3, backoffCoefficient: 2 }
|
|
231
|
-
};
|
|
232
|
-
const ext = {
|
|
233
|
-
retry: { maximumAttempts: 5, initialInterval: '1s' }
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const result = mergeActivityOptions( base, ext );
|
|
237
|
-
|
|
238
|
-
expect( result ).toEqual( {
|
|
239
|
-
taskQueue: 'q1',
|
|
240
|
-
retry: { maximumAttempts: 5, backoffCoefficient: 2, initialInterval: '1s' }
|
|
241
|
-
} );
|
|
242
|
-
} );
|
|
243
|
-
|
|
244
|
-
it( 'omitted properties in second do not overwrite first', () => {
|
|
245
|
-
const base = {
|
|
246
|
-
taskQueue: 'q2',
|
|
247
|
-
retry: { initialInterval: '2s', backoffCoefficient: 2 }
|
|
248
|
-
};
|
|
249
|
-
const ext = {
|
|
250
|
-
retry: { backoffCoefficient: 3 }
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const result = mergeActivityOptions( base, ext );
|
|
254
|
-
|
|
255
|
-
expect( result.retry.initialInterval ).toBe( '2s' );
|
|
256
|
-
expect( result.retry.backoffCoefficient ).toBe( 3 );
|
|
257
|
-
expect( result.taskQueue ).toBe( 'q2' );
|
|
258
|
-
} );
|
|
259
|
-
|
|
260
|
-
it( 'handles omitted second argument by returning a clone', () => {
|
|
261
|
-
const base = { taskQueue: 'q3', retry: { maximumAttempts: 2 } };
|
|
262
|
-
|
|
263
|
-
const result = mergeActivityOptions( base );
|
|
264
|
-
|
|
265
|
-
expect( result ).toEqual( base );
|
|
266
|
-
expect( result ).not.toBe( base );
|
|
267
|
-
} );
|
|
268
|
-
} );
|
|
269
|
-
|
|
270
225
|
describe( 'deepMerge', () => {
|
|
271
226
|
it( 'Overwrites properties in object "a"', () => {
|
|
272
227
|
const a = {
|
|
@@ -316,13 +271,43 @@ describe( 'deepMerge', () => {
|
|
|
316
271
|
} );
|
|
317
272
|
} );
|
|
318
273
|
|
|
319
|
-
it( '
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
274
|
+
it( 'Merge object is a clone', () => {
|
|
275
|
+
const a = {
|
|
276
|
+
a: 1
|
|
277
|
+
};
|
|
278
|
+
const b = {
|
|
279
|
+
b: 1
|
|
280
|
+
};
|
|
281
|
+
const result = deepMerge( a, b );
|
|
282
|
+
a.a = 2;
|
|
283
|
+
b.b = 2;
|
|
284
|
+
expect( result.a ).toEqual( 1 );
|
|
285
|
+
} );
|
|
286
|
+
|
|
287
|
+
it( 'Returns copy of "a" if "b" is not an object', () => {
|
|
288
|
+
const a = {
|
|
289
|
+
a: 1
|
|
290
|
+
};
|
|
291
|
+
expect( deepMerge( a, null ) ).toEqual( { a: 1 } );
|
|
292
|
+
expect( deepMerge( a, undefined ) ).toEqual( { a: 1 } );
|
|
293
|
+
} );
|
|
294
|
+
|
|
295
|
+
it( 'Copy of object "a" is a clone', () => {
|
|
296
|
+
const a = {
|
|
297
|
+
a: 1
|
|
298
|
+
};
|
|
299
|
+
const result = deepMerge( a, null );
|
|
300
|
+
a.a = 2;
|
|
301
|
+
expect( result.a ).toEqual( 1 );
|
|
302
|
+
} );
|
|
303
|
+
|
|
304
|
+
it( 'Throws when first argument is not a plain object', () => {
|
|
305
|
+
expect( () => deepMerge( Function ) ).toThrow( Error );
|
|
306
|
+
expect( () => deepMerge( () => {} ) ).toThrow( Error );
|
|
307
|
+
expect( () => deepMerge( 'a' ) ).toThrow( Error );
|
|
308
|
+
expect( () => deepMerge( true ) ).toThrow( Error );
|
|
309
|
+
expect( () => deepMerge( /a/ ) ).toThrow( Error );
|
|
310
|
+
expect( () => deepMerge( [] ) ).toThrow( Error );
|
|
326
311
|
expect( () => deepMerge( class Foo {}, class Foo {} ) ).toThrow( Error );
|
|
327
312
|
expect( () => deepMerge( Number.constructor, Number.constructor ) ).toThrow( Error );
|
|
328
313
|
expect( () => deepMerge( Number.constructor.prototype, Number.constructor.prototype ) ).toThrow( Error );
|
|
@@ -377,6 +362,8 @@ describe( 'isPlainObject', () => {
|
|
|
377
362
|
} );
|
|
378
363
|
|
|
379
364
|
it( 'Returns false for primitives', () => {
|
|
365
|
+
expect( isPlainObject( null ) ).toBe( false );
|
|
366
|
+
expect( isPlainObject( undefined ) ).toBe( false );
|
|
380
367
|
expect( isPlainObject( false ) ).toBe( false );
|
|
381
368
|
expect( isPlainObject( true ) ).toBe( false );
|
|
382
369
|
expect( isPlainObject( 1 ) ).toBe( false );
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockLog = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
4
|
+
vi.mock( '#logger', () => ( { createChildLogger: () => mockLog } ) );
|
|
5
|
+
|
|
6
|
+
vi.mock( '#consts', () => ( { WORKFLOW_CATALOG: 'catalog' } ) );
|
|
7
|
+
|
|
8
|
+
vi.mock( '#tracing', () => ( { init: vi.fn().mockResolvedValue( undefined ) } ) );
|
|
9
|
+
|
|
10
|
+
const configValues = {
|
|
11
|
+
address: 'localhost:7233',
|
|
12
|
+
apiKey: undefined,
|
|
13
|
+
namespace: 'default',
|
|
14
|
+
taskQueue: 'test-queue',
|
|
15
|
+
catalogId: 'test-catalog',
|
|
16
|
+
maxConcurrentWorkflowTaskExecutions: 200,
|
|
17
|
+
maxConcurrentActivityTaskExecutions: 40,
|
|
18
|
+
maxCachedWorkflows: 1000,
|
|
19
|
+
maxConcurrentActivityTaskPolls: 5,
|
|
20
|
+
maxConcurrentWorkflowTaskPolls: 5
|
|
21
|
+
};
|
|
22
|
+
vi.mock( './configs.js', () => configValues );
|
|
23
|
+
|
|
24
|
+
const loadWorkflowsMock = vi.fn().mockResolvedValue( [] );
|
|
25
|
+
const loadActivitiesMock = vi.fn().mockResolvedValue( {} );
|
|
26
|
+
const createWorkflowsEntryPointMock = vi.fn().mockReturnValue( '/fake/workflows/path.js' );
|
|
27
|
+
vi.mock( './loader.js', () => ( {
|
|
28
|
+
loadWorkflows: loadWorkflowsMock,
|
|
29
|
+
loadActivities: loadActivitiesMock,
|
|
30
|
+
createWorkflowsEntryPoint: createWorkflowsEntryPointMock
|
|
31
|
+
} ) );
|
|
32
|
+
|
|
33
|
+
vi.mock( './sinks.js', () => ( { sinks: {} } ) );
|
|
34
|
+
|
|
35
|
+
const createCatalogMock = vi.fn().mockReturnValue( { workflows: [], activities: {} } );
|
|
36
|
+
vi.mock( './catalog_workflow/index.js', () => ( { createCatalog: createCatalogMock } ) );
|
|
37
|
+
|
|
38
|
+
vi.mock( './bundler_options.js', () => ( { webpackConfigHook: vi.fn() } ) );
|
|
39
|
+
|
|
40
|
+
const initInterceptorsMock = vi.fn().mockReturnValue( [] );
|
|
41
|
+
vi.mock( './interceptors.js', () => ( { initInterceptors: initInterceptorsMock } ) );
|
|
42
|
+
|
|
43
|
+
const runState = { resolve: null };
|
|
44
|
+
const runPromise = new Promise( r => {
|
|
45
|
+
runState.resolve = r;
|
|
46
|
+
} );
|
|
47
|
+
const shutdownMock = vi.fn();
|
|
48
|
+
const mockConnection = { close: vi.fn().mockResolvedValue( undefined ) };
|
|
49
|
+
const mockWorker = { run: () => runPromise, shutdown: shutdownMock };
|
|
50
|
+
|
|
51
|
+
vi.mock( '@temporalio/worker', () => ( {
|
|
52
|
+
Worker: { create: vi.fn().mockResolvedValue( mockWorker ) },
|
|
53
|
+
NativeConnection: { connect: vi.fn().mockResolvedValue( mockConnection ) }
|
|
54
|
+
} ) );
|
|
55
|
+
|
|
56
|
+
const workflowStartMock = vi.fn().mockResolvedValue( undefined );
|
|
57
|
+
vi.mock( '@temporalio/client', () => ( {
|
|
58
|
+
Client: vi.fn().mockImplementation( () => ( {
|
|
59
|
+
workflow: { start: workflowStartMock }
|
|
60
|
+
} ) )
|
|
61
|
+
} ) );
|
|
62
|
+
|
|
63
|
+
vi.mock( '@temporalio/common', () => ( {
|
|
64
|
+
WorkflowIdConflictPolicy: { TERMINATE_EXISTING: 'TERMINATE_EXISTING' }
|
|
65
|
+
} ) );
|
|
66
|
+
|
|
67
|
+
describe( 'worker/index', () => {
|
|
68
|
+
const exitMock = vi.fn();
|
|
69
|
+
const originalArgv = process.argv;
|
|
70
|
+
const originalExit = process.exit;
|
|
71
|
+
const originalOn = process.on;
|
|
72
|
+
|
|
73
|
+
beforeEach( () => {
|
|
74
|
+
vi.clearAllMocks();
|
|
75
|
+
process.argv = [ ...originalArgv.slice( 0, 2 ), '/test/caller/dir' ];
|
|
76
|
+
process.exit = exitMock;
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
afterEach( () => {
|
|
80
|
+
process.argv = originalArgv;
|
|
81
|
+
process.exit = originalExit;
|
|
82
|
+
process.on = originalOn;
|
|
83
|
+
configValues.apiKey = undefined;
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
it( 'loads configs, workflows, activities and creates worker with correct options', async () => {
|
|
87
|
+
const { Worker, NativeConnection } = await import( '@temporalio/worker' );
|
|
88
|
+
const { Client } = await import( '@temporalio/client' );
|
|
89
|
+
const { init: initTracing } = await import( '#tracing' );
|
|
90
|
+
|
|
91
|
+
import( './index.js' );
|
|
92
|
+
|
|
93
|
+
await vi.waitFor( () => {
|
|
94
|
+
expect( loadWorkflowsMock ).toHaveBeenCalledWith( '/test/caller/dir' );
|
|
95
|
+
} );
|
|
96
|
+
expect( loadActivitiesMock ).toHaveBeenCalledWith( '/test/caller/dir', [] );
|
|
97
|
+
expect( createWorkflowsEntryPointMock ).toHaveBeenCalledWith( [] );
|
|
98
|
+
expect( initTracing ).toHaveBeenCalled();
|
|
99
|
+
expect( createCatalogMock ).toHaveBeenCalledWith( { workflows: [], activities: {} } );
|
|
100
|
+
expect( NativeConnection.connect ).toHaveBeenCalledWith( {
|
|
101
|
+
address: configValues.address,
|
|
102
|
+
tls: false,
|
|
103
|
+
apiKey: undefined
|
|
104
|
+
} );
|
|
105
|
+
expect( Worker.create ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
106
|
+
namespace: configValues.namespace,
|
|
107
|
+
taskQueue: configValues.taskQueue,
|
|
108
|
+
workflowsPath: '/fake/workflows/path.js',
|
|
109
|
+
activities: {},
|
|
110
|
+
maxConcurrentWorkflowTaskExecutions: configValues.maxConcurrentWorkflowTaskExecutions,
|
|
111
|
+
maxConcurrentActivityTaskExecutions: configValues.maxConcurrentActivityTaskExecutions,
|
|
112
|
+
maxCachedWorkflows: configValues.maxCachedWorkflows,
|
|
113
|
+
maxConcurrentActivityTaskPolls: configValues.maxConcurrentActivityTaskPolls,
|
|
114
|
+
maxConcurrentWorkflowTaskPolls: configValues.maxConcurrentWorkflowTaskPolls
|
|
115
|
+
} ) );
|
|
116
|
+
expect( initInterceptorsMock ).toHaveBeenCalledWith( { activities: {} } );
|
|
117
|
+
expect( Client ).toHaveBeenCalledWith( { connection: mockConnection, namespace: configValues.namespace } );
|
|
118
|
+
expect( workflowStartMock ).toHaveBeenCalledWith( 'catalog', {
|
|
119
|
+
taskQueue: configValues.taskQueue,
|
|
120
|
+
workflowId: configValues.catalogId,
|
|
121
|
+
workflowIdConflictPolicy: 'TERMINATE_EXISTING',
|
|
122
|
+
args: [ { workflows: [], activities: {} } ]
|
|
123
|
+
} );
|
|
124
|
+
|
|
125
|
+
runState.resolve();
|
|
126
|
+
await vi.waitFor( () => {
|
|
127
|
+
expect( mockConnection.close ).toHaveBeenCalled();
|
|
128
|
+
} );
|
|
129
|
+
expect( exitMock ).toHaveBeenCalledWith( 0 );
|
|
130
|
+
} );
|
|
131
|
+
|
|
132
|
+
it( 'enables TLS when apiKey is set', async () => {
|
|
133
|
+
configValues.apiKey = 'secret';
|
|
134
|
+
vi.resetModules();
|
|
135
|
+
|
|
136
|
+
const { NativeConnection } = await import( '@temporalio/worker' );
|
|
137
|
+
import( './index.js' );
|
|
138
|
+
|
|
139
|
+
await vi.waitFor( () => {
|
|
140
|
+
expect( NativeConnection.connect ).toHaveBeenCalledWith( expect.objectContaining( {
|
|
141
|
+
tls: true,
|
|
142
|
+
apiKey: 'secret'
|
|
143
|
+
} ) );
|
|
144
|
+
} );
|
|
145
|
+
await vi.waitFor( () => expect( exitMock ).toHaveBeenCalled() );
|
|
146
|
+
} );
|
|
147
|
+
|
|
148
|
+
it( 'registers SIGTERM and SIGINT handlers that shut down worker', async () => {
|
|
149
|
+
const onMock = vi.fn();
|
|
150
|
+
process.on = onMock;
|
|
151
|
+
vi.resetModules();
|
|
152
|
+
|
|
153
|
+
import( './index.js' );
|
|
154
|
+
|
|
155
|
+
await vi.waitFor( () => {
|
|
156
|
+
expect( onMock ).toHaveBeenCalledWith( 'SIGTERM', expect.any( Function ) );
|
|
157
|
+
expect( onMock ).toHaveBeenCalledWith( 'SIGINT', expect.any( Function ) );
|
|
158
|
+
} );
|
|
159
|
+
|
|
160
|
+
const sigtermHandler = onMock.mock.calls.find( c => c[0] === 'SIGTERM' )?.[1];
|
|
161
|
+
expect( sigtermHandler ).toBeDefined();
|
|
162
|
+
sigtermHandler();
|
|
163
|
+
expect( shutdownMock ).toHaveBeenCalled();
|
|
164
|
+
|
|
165
|
+
runState.resolve();
|
|
166
|
+
await vi.waitFor( () => expect( exitMock ).toHaveBeenCalled() );
|
|
167
|
+
} );
|
|
168
|
+
|
|
169
|
+
it( 'calls process.exit(1) on fatal error', async () => {
|
|
170
|
+
loadWorkflowsMock.mockRejectedValueOnce( new Error( 'load failed' ) );
|
|
171
|
+
vi.resetModules();
|
|
172
|
+
|
|
173
|
+
import( './index.js' );
|
|
174
|
+
|
|
175
|
+
await vi.waitFor( () => {
|
|
176
|
+
expect( mockLog.error ).toHaveBeenCalledWith( 'Fatal error', expect.any( Object ) );
|
|
177
|
+
} );
|
|
178
|
+
expect( exitMock ).toHaveBeenCalledWith( 1 );
|
|
179
|
+
} );
|
|
180
|
+
} );
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// THIS RUNS IN THE TEMPORAL'S SANDBOX ENVIRONMENT
|
|
2
2
|
import { workflowInfo, proxySinks, ApplicationFailure, ContinueAsNew } from '@temporalio/workflow';
|
|
3
3
|
import { memoToHeaders } from '../sandboxed_utils.js';
|
|
4
|
-
import {
|
|
4
|
+
import { deepMerge } from '#utils';
|
|
5
5
|
import { METADATA_ACCESS_SYMBOL } from '#consts';
|
|
6
6
|
// this is a dynamic generated file with activity configs overwrites
|
|
7
7
|
import stepOptions from '../temp/__activity_options.js';
|
|
@@ -22,7 +22,7 @@ class HeadersInjectionInterceptor {
|
|
|
22
22
|
// apply per-invocation options passed as second argument by rewritten calls
|
|
23
23
|
const options = stepOptions[input.activityType];
|
|
24
24
|
if ( options ) {
|
|
25
|
-
input.options =
|
|
25
|
+
input.options = deepMerge( memo.activityOptions, options );
|
|
26
26
|
}
|
|
27
27
|
return next( input );
|
|
28
28
|
}
|