@outputai/core 0.10.1-dev.b7b2fbe.0 → 0.10.1-next.2caa4a1.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 +8 -7
- package/src/bus.js +58 -20
- package/src/bus.spec.js +135 -77
- package/src/helpers/fetch.js +1 -1
- package/src/helpers/object.js +5 -1
- package/src/helpers/object.spec.js +5 -0
- package/src/hooks/index.d.ts +48 -45
- package/src/hooks/index.js +56 -49
- package/src/hooks/index.spec.js +88 -97
- package/src/hooks/pending_hooks.js +24 -0
- package/src/hooks/pending_hooks.spec.js +62 -0
- package/src/interface/index.js +2 -1
- 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/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/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 +14 -10
- package/src/worker/index.spec.js +13 -8
- package/src/worker/interceptors/activity.js +4 -4
- package/src/worker/interceptors/activity.spec.js +9 -9
- package/src/worker/log_hooks.js +9 -9
- package/src/worker/log_hooks.spec.js +2 -2
- package/src/worker/sinks.js +8 -8
- package/src/worker/sinks.spec.js +9 -9
- package/src/worker/proxy.js +0 -20
- package/src/worker/proxy.spec.js +0 -51
package/src/hooks/index.js
CHANGED
|
@@ -1,68 +1,75 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { BusEventType
|
|
1
|
+
import { mainEventBus, stepEventBus } from '#bus';
|
|
2
|
+
import { BusEventType } from '#consts';
|
|
3
3
|
import { createChildLogger } from '#logger';
|
|
4
|
+
import { pendingHooks } from './pending_hooks.js';
|
|
4
5
|
|
|
5
6
|
const log = createChildLogger( 'Hooks' );
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Invokes
|
|
9
|
+
* Invokes a function within a try catch and log the error.
|
|
9
10
|
*
|
|
10
11
|
* @param {Function} fn
|
|
11
12
|
* @param {any} args - Args to invoke the function with
|
|
12
13
|
* @param {string} hookName - hookName to identify this hook function in the logs
|
|
13
14
|
*/
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
const callHookCb = ( fn, args, hookName ) => {
|
|
16
|
+
const promise = Promise.resolve()
|
|
17
|
+
.then( () => fn( args ) )
|
|
18
|
+
.catch( e => log.error( `${hookName} hook error`, { message: e.message, stack: e.stack } ) )
|
|
19
|
+
.finally( () => pendingHooks.delete( promise ) );
|
|
20
|
+
pendingHooks.add( promise );
|
|
21
|
+
return promise;
|
|
20
22
|
};
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
// General Life-cycle
|
|
25
|
+
// --------------------------------------
|
|
26
|
+
const onError = cb => {
|
|
27
|
+
mainEventBus.on( BusEventType.ACTIVITY_ERROR, async payload =>
|
|
28
|
+
callHookCb( cb, { source: 'activity', ...payload }, 'onError' ) );
|
|
29
|
+
mainEventBus.on( BusEventType.WORKFLOW_ERROR, async payload =>
|
|
30
|
+
callHookCb( cb, { source: 'workflow', ...payload }, 'onError' ) );
|
|
31
|
+
mainEventBus.on( BusEventType.RUNTIME_ERROR, async payload =>
|
|
32
|
+
callHookCb( cb, { source: 'runtime', ...payload }, 'onError' ) );
|
|
30
33
|
};
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
export const onBeforeWorkerStart = handler => messageBus.on( BusEventType.WORKER_BEFORE_START, () =>
|
|
34
|
-
safeInvoke( handler, undefined, 'onBeforeWorkerStart' ) );
|
|
35
|
+
const onBeforeWorkerStart = cb => mainEventBus.on( BusEventType.WORKER_BEFORE_START, () => callHookCb( cb, undefined, 'onBeforeWorkerStart' ) );
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
// Workflow Life-cycle
|
|
38
|
+
// --------------------------------------
|
|
39
|
+
const onWorkflowStart = cb => mainEventBus.on( BusEventType.WORKFLOW_START, payload => callHookCb( cb, payload, 'onWorkflowStart' ) );
|
|
40
|
+
const onWorkflowEnd = cb => mainEventBus.on( BusEventType.WORKFLOW_END, payload => callHookCb( cb, payload, 'onWorkflowEnd' ) );
|
|
41
|
+
const onWorkflowError = cb => mainEventBus.on( BusEventType.WORKFLOW_ERROR, payload => callHookCb( cb, payload, 'onWorkflowError' ) );
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
// Activity Life-cycle
|
|
44
|
+
// --------------------------------------
|
|
45
|
+
const onActivityStart = cb => mainEventBus.on( BusEventType.ACTIVITY_START, fields => callHookCb( cb, fields, 'onActivityStart' ) );
|
|
46
|
+
const onActivityEnd = cb => mainEventBus.on( BusEventType.ACTIVITY_END, fields => callHookCb( cb, fields, 'onActivityEnd' ) );
|
|
47
|
+
const onActivityError = cb => mainEventBus.on( BusEventType.ACTIVITY_ERROR, fields => callHookCb( cb, fields, 'onActivityError' ) );
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
/** Internal activities do not trigger hooks */
|
|
52
|
-
const shouldEmitActivityEvent = outputActivityKind => outputActivityKind !== ComponentType.INTERNAL_STEP;
|
|
53
|
-
|
|
54
|
-
/** Listen to workflow start events, excludes catalog workflow */
|
|
55
|
-
export const onActivityStart = handler => messageBus.on( BusEventType.ACTIVITY_START, ( { outputActivityKind, ...eventFields } ) =>
|
|
56
|
-
shouldEmitActivityEvent( outputActivityKind ) ? safeInvoke( handler, { outputActivityKind, ...eventFields }, 'onActivityStart' ) : null );
|
|
57
|
-
|
|
58
|
-
/** Listen to workflow end events, excludes catalog workflow */
|
|
59
|
-
export const onActivityEnd = handler => messageBus.on( BusEventType.ACTIVITY_END, ( { outputActivityKind, ...eventFields } ) =>
|
|
60
|
-
shouldEmitActivityEvent( outputActivityKind ) ? safeInvoke( handler, { outputActivityKind, ...eventFields }, 'onActivityEnd' ) : null );
|
|
49
|
+
// Generic Events
|
|
50
|
+
// --------------------------------------
|
|
51
|
+
/** Listen to both sdk and custom events */
|
|
52
|
+
const on = ( eventName, cb ) => {
|
|
53
|
+
stepEventBus.on( `sdk:${eventName}`, payload => callHookCb( cb, payload, eventName ) );
|
|
54
|
+
stepEventBus.on( `usr:${eventName}`, payload => callHookCb( cb, payload, eventName ) );
|
|
55
|
+
};
|
|
61
56
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Emits a custom event
|
|
59
|
+
* @param {string} eventName
|
|
60
|
+
* @param {any} payload
|
|
61
|
+
*/
|
|
62
|
+
const emit = ( eventName, payload ) => stepEventBus.emit( `usr:${eventName}`, payload );
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
export {
|
|
65
|
+
emit,
|
|
66
|
+
on,
|
|
67
|
+
onActivityEnd,
|
|
68
|
+
onActivityError,
|
|
69
|
+
onActivityStart,
|
|
70
|
+
onBeforeWorkerStart,
|
|
71
|
+
onError,
|
|
72
|
+
onWorkflowEnd,
|
|
73
|
+
onWorkflowError,
|
|
74
|
+
onWorkflowStart
|
|
75
|
+
};
|
package/src/hooks/index.spec.js
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { BusEventType
|
|
2
|
+
import { BusEventType } from '#consts';
|
|
3
3
|
|
|
4
4
|
const logErrorMock = vi.hoisted( () => vi.fn() );
|
|
5
5
|
const createChildLoggerMock = vi.hoisted( () =>
|
|
6
6
|
vi.fn( () => ( { error: logErrorMock } ) )
|
|
7
7
|
);
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const mainOnHandlers = vi.hoisted( () => ( {} ) );
|
|
10
|
+
const stepOnHandlers = vi.hoisted( () => ( {} ) );
|
|
11
|
+
const mainEventBusMock = vi.hoisted( () => ( {
|
|
11
12
|
on: vi.fn( ( eventType, handler ) => {
|
|
12
|
-
|
|
13
|
+
mainOnHandlers[eventType] = handler;
|
|
14
|
+
} )
|
|
15
|
+
} ) );
|
|
16
|
+
const stepEventBusMock = vi.hoisted( () => ( {
|
|
17
|
+
emit: vi.fn( () => true ),
|
|
18
|
+
on: vi.fn( ( eventType, handler ) => {
|
|
19
|
+
stepOnHandlers[eventType] = handler;
|
|
13
20
|
} )
|
|
14
21
|
} ) );
|
|
15
22
|
|
|
16
23
|
vi.mock( '#logger', () => ( { createChildLogger: createChildLoggerMock } ) );
|
|
17
|
-
vi.mock( '#bus', () => ( {
|
|
24
|
+
vi.mock( '#bus', () => ( {
|
|
25
|
+
mainEventBus: mainEventBusMock,
|
|
26
|
+
stepEventBus: stepEventBusMock
|
|
27
|
+
} ) );
|
|
18
28
|
|
|
19
29
|
import {
|
|
30
|
+
emit,
|
|
20
31
|
on,
|
|
21
32
|
onActivityEnd,
|
|
22
33
|
onActivityError,
|
|
@@ -27,6 +38,7 @@ import {
|
|
|
27
38
|
onWorkflowError,
|
|
28
39
|
onWorkflowStart
|
|
29
40
|
} from './index.js';
|
|
41
|
+
import { pendingHooks } from './pending_hooks.js';
|
|
30
42
|
|
|
31
43
|
const workflowDetails = {
|
|
32
44
|
workflowId: 'wf-1',
|
|
@@ -38,11 +50,6 @@ const workflowDetails = {
|
|
|
38
50
|
attempt: 1
|
|
39
51
|
};
|
|
40
52
|
|
|
41
|
-
const catalogWorkflowDetails = {
|
|
42
|
-
...workflowDetails,
|
|
43
|
-
workflowType: WORKFLOW_CATALOG
|
|
44
|
-
};
|
|
45
|
-
|
|
46
53
|
const activityInfo = {
|
|
47
54
|
activityId: 'act-1',
|
|
48
55
|
activityType: 'wf#step',
|
|
@@ -61,8 +68,12 @@ const aggregations = {
|
|
|
61
68
|
describe( 'hooks/index', () => {
|
|
62
69
|
beforeEach( () => {
|
|
63
70
|
vi.clearAllMocks();
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
pendingHooks.clear();
|
|
72
|
+
Object.keys( mainOnHandlers ).forEach( k => {
|
|
73
|
+
delete mainOnHandlers[k];
|
|
74
|
+
} );
|
|
75
|
+
Object.keys( stepOnHandlers ).forEach( k => {
|
|
76
|
+
delete stepOnHandlers[k];
|
|
66
77
|
} );
|
|
67
78
|
} );
|
|
68
79
|
|
|
@@ -71,9 +82,9 @@ describe( 'hooks/index', () => {
|
|
|
71
82
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
72
83
|
onError( handler );
|
|
73
84
|
|
|
74
|
-
expect(
|
|
75
|
-
expect(
|
|
76
|
-
expect(
|
|
85
|
+
expect( mainEventBusMock.on ).toHaveBeenCalledWith( BusEventType.ACTIVITY_ERROR, expect.any( Function ) );
|
|
86
|
+
expect( mainEventBusMock.on ).toHaveBeenCalledWith( BusEventType.WORKFLOW_ERROR, expect.any( Function ) );
|
|
87
|
+
expect( mainEventBusMock.on ).toHaveBeenCalledWith( BusEventType.RUNTIME_ERROR, expect.any( Function ) );
|
|
77
88
|
} );
|
|
78
89
|
|
|
79
90
|
it( 'invokes handler with activity-shaped payload, forwarding bus fields', async () => {
|
|
@@ -81,7 +92,7 @@ describe( 'hooks/index', () => {
|
|
|
81
92
|
onError( handler );
|
|
82
93
|
|
|
83
94
|
const err = new Error( 'act-fail' );
|
|
84
|
-
await
|
|
95
|
+
await mainOnHandlers[BusEventType.ACTIVITY_ERROR]( {
|
|
85
96
|
eventId: 'evt-act-1',
|
|
86
97
|
eventDate,
|
|
87
98
|
activityInfo,
|
|
@@ -108,7 +119,7 @@ describe( 'hooks/index', () => {
|
|
|
108
119
|
onError( handler );
|
|
109
120
|
|
|
110
121
|
const err = new Error( 'wf-fail' );
|
|
111
|
-
await
|
|
122
|
+
await mainOnHandlers[BusEventType.WORKFLOW_ERROR]( {
|
|
112
123
|
eventId: 'evt-wf-1',
|
|
113
124
|
eventDate,
|
|
114
125
|
workflowDetails,
|
|
@@ -131,7 +142,7 @@ describe( 'hooks/index', () => {
|
|
|
131
142
|
onError( handler );
|
|
132
143
|
|
|
133
144
|
const error = new Error( 'rt' );
|
|
134
|
-
await
|
|
145
|
+
await mainOnHandlers[BusEventType.RUNTIME_ERROR]( { eventId: 'evt-rt-1', eventDate, error } );
|
|
135
146
|
|
|
136
147
|
expect( handler ).toHaveBeenCalledWith( { eventId: 'evt-rt-1', eventDate, source: 'runtime', error } );
|
|
137
148
|
} );
|
|
@@ -142,68 +153,52 @@ describe( 'hooks/index', () => {
|
|
|
142
153
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
143
154
|
onBeforeWorkerStart( handler );
|
|
144
155
|
|
|
145
|
-
expect(
|
|
146
|
-
await
|
|
156
|
+
expect( mainEventBusMock.on ).toHaveBeenCalledWith( BusEventType.WORKER_BEFORE_START, expect.any( Function ) );
|
|
157
|
+
await mainOnHandlers[BusEventType.WORKER_BEFORE_START]();
|
|
147
158
|
|
|
148
159
|
expect( handler ).toHaveBeenCalledWith( undefined );
|
|
149
160
|
} );
|
|
150
161
|
} );
|
|
151
162
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( {
|
|
158
|
-
eventId: 'evt-ignored', eventDate, workflowDetails: catalogWorkflowDetails
|
|
159
|
-
} ) );
|
|
160
|
-
expect( handler ).not.toHaveBeenCalled();
|
|
161
|
-
|
|
162
|
-
await Promise.resolve( onHandlers[BusEventType.WORKFLOW_START]( {
|
|
163
|
-
eventId: 'evt-start-1', eventDate, workflowDetails, extra: 'passthrough'
|
|
164
|
-
} ) );
|
|
165
|
-
expect( handler ).toHaveBeenCalledWith( {
|
|
166
|
-
eventId: 'evt-start-1', eventDate, workflowDetails, extra: 'passthrough'
|
|
167
|
-
} );
|
|
163
|
+
it( 'tracks a hook callback until it settles', async () => {
|
|
164
|
+
const deferred = { resolve: null };
|
|
165
|
+
const handlerPromise = new Promise( resolve => {
|
|
166
|
+
deferred.resolve = resolve;
|
|
168
167
|
} );
|
|
169
|
-
|
|
168
|
+
const handler = vi.fn( () => handlerPromise );
|
|
169
|
+
onActivityEnd( handler );
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
it( 'skips catalog workflow and forwards bus fields for real workflows', async () => {
|
|
173
|
-
const handler = vi.fn().mockResolvedValue( undefined );
|
|
174
|
-
onWorkflowEnd( handler );
|
|
171
|
+
const callbackPromise = mainOnHandlers[BusEventType.ACTIVITY_END]( { activityInfo } );
|
|
175
172
|
|
|
176
|
-
|
|
177
|
-
eventId: 'evt-ignored', eventDate, workflowDetails: catalogWorkflowDetails
|
|
178
|
-
} ) );
|
|
179
|
-
expect( handler ).not.toHaveBeenCalled();
|
|
173
|
+
expect( pendingHooks.size ).toBe( 1 );
|
|
180
174
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
expect( handler ).toHaveBeenCalledWith( {
|
|
185
|
-
eventId: 'evt-end-1', eventDate, workflowDetails, extra: 'passthrough'
|
|
186
|
-
} );
|
|
187
|
-
} );
|
|
175
|
+
deferred.resolve();
|
|
176
|
+
await callbackPromise;
|
|
177
|
+
expect( pendingHooks.size ).toBe( 0 );
|
|
188
178
|
} );
|
|
189
179
|
|
|
190
|
-
describe( '
|
|
191
|
-
|
|
180
|
+
describe( 'workflow lifecycle hooks', () => {
|
|
181
|
+
const cases = [
|
|
182
|
+
[ 'onWorkflowStart', onWorkflowStart, BusEventType.WORKFLOW_START, {} ],
|
|
183
|
+
[ 'onWorkflowEnd', onWorkflowEnd, BusEventType.WORKFLOW_END, {} ],
|
|
184
|
+
[ 'onWorkflowError', onWorkflowError, BusEventType.WORKFLOW_ERROR, { error: new Error( 'workflow failed' ) } ]
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
it.each( cases )( '%s forwards bus fields', async ( _name, registerHook, eventType, extraFields ) => {
|
|
192
188
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
193
|
-
const
|
|
194
|
-
|
|
189
|
+
const payload = {
|
|
190
|
+
eventId: 'evt-workflow-1',
|
|
191
|
+
eventDate,
|
|
192
|
+
workflowDetails,
|
|
193
|
+
extra: 'passthrough',
|
|
194
|
+
...extraFields
|
|
195
|
+
};
|
|
196
|
+
registerHook( handler );
|
|
195
197
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
} ) );
|
|
199
|
-
expect( handler ).not.toHaveBeenCalled();
|
|
198
|
+
expect( mainEventBusMock.on ).toHaveBeenCalledWith( eventType, expect.any( Function ) );
|
|
199
|
+
await mainOnHandlers[eventType]( payload );
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
eventId: 'evt-err-1', eventDate, workflowDetails, error: err, extra: 'passthrough'
|
|
203
|
-
} ) );
|
|
204
|
-
expect( handler ).toHaveBeenCalledWith( {
|
|
205
|
-
eventId: 'evt-err-1', eventDate, workflowDetails, error: err, extra: 'passthrough'
|
|
206
|
-
} );
|
|
201
|
+
expect( handler ).toHaveBeenCalledWith( payload );
|
|
207
202
|
} );
|
|
208
203
|
} );
|
|
209
204
|
|
|
@@ -214,53 +209,49 @@ describe( 'hooks/index', () => {
|
|
|
214
209
|
[ 'onActivityError', onActivityError, BusEventType.ACTIVITY_ERROR, { error: new Error( 'activity failed' ) } ]
|
|
215
210
|
];
|
|
216
211
|
|
|
217
|
-
it.each( cases )( '%s
|
|
212
|
+
it.each( cases )( '%s forwards internal activity bus fields', async ( _name, registerHook, eventType, extraFields = {} ) => {
|
|
218
213
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
expect( messageBusMock.on ).toHaveBeenCalledWith( eventType, expect.any( Function ) );
|
|
222
|
-
|
|
223
|
-
await Promise.resolve( onHandlers[eventType]( {
|
|
224
|
-
eventId: 'evt-ignored',
|
|
225
|
-
eventDate,
|
|
226
|
-
activityInfo,
|
|
227
|
-
workflowDetails,
|
|
228
|
-
outputActivityKind: ComponentType.INTERNAL_STEP,
|
|
229
|
-
...extraFields
|
|
230
|
-
} ) );
|
|
231
|
-
expect( handler ).not.toHaveBeenCalled();
|
|
232
|
-
|
|
233
|
-
await Promise.resolve( onHandlers[eventType]( {
|
|
214
|
+
const payload = {
|
|
234
215
|
eventId: 'evt-activity-1',
|
|
235
216
|
eventDate,
|
|
236
217
|
activityInfo,
|
|
237
218
|
workflowDetails,
|
|
238
|
-
outputActivityKind:
|
|
219
|
+
outputActivityKind: 'internal_step',
|
|
239
220
|
extra: 'passthrough',
|
|
240
221
|
...extraFields
|
|
241
|
-
}
|
|
222
|
+
};
|
|
223
|
+
registerHook( handler );
|
|
242
224
|
|
|
243
|
-
expect(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
workflowDetails,
|
|
248
|
-
outputActivityKind: ComponentType.STEP,
|
|
249
|
-
extra: 'passthrough',
|
|
250
|
-
...extraFields
|
|
251
|
-
} );
|
|
225
|
+
expect( mainEventBusMock.on ).toHaveBeenCalledWith( eventType, expect.any( Function ) );
|
|
226
|
+
await mainOnHandlers[eventType]( payload );
|
|
227
|
+
|
|
228
|
+
expect( handler ).toHaveBeenCalledWith( payload );
|
|
252
229
|
} );
|
|
253
230
|
} );
|
|
254
231
|
|
|
255
232
|
describe( 'on', () => {
|
|
256
|
-
it( 'subscribes to
|
|
233
|
+
it( 'subscribes to SDK and user event channels and forwards payloads', async () => {
|
|
257
234
|
const handler = vi.fn().mockResolvedValue( undefined );
|
|
258
235
|
on( 'myEvent', handler );
|
|
259
236
|
|
|
260
|
-
expect(
|
|
261
|
-
|
|
237
|
+
expect( stepEventBusMock.on ).toHaveBeenCalledWith( 'sdk:myEvent', expect.any( Function ) );
|
|
238
|
+
expect( stepEventBusMock.on ).toHaveBeenCalledWith( 'usr:myEvent', expect.any( Function ) );
|
|
239
|
+
await stepOnHandlers['sdk:myEvent']( { payload: { source: 'sdk' } } );
|
|
240
|
+
await stepOnHandlers['usr:myEvent']( { payload: { source: 'user' } } );
|
|
241
|
+
|
|
242
|
+
expect( handler ).toHaveBeenNthCalledWith( 1, { payload: { source: 'sdk' } } );
|
|
243
|
+
expect( handler ).toHaveBeenNthCalledWith( 2, { payload: { source: 'user' } } );
|
|
244
|
+
} );
|
|
245
|
+
} );
|
|
246
|
+
|
|
247
|
+
describe( 'emit', () => {
|
|
248
|
+
it( 'emits payloads on the user event channel', () => {
|
|
249
|
+
const payload = { foo: 1 };
|
|
250
|
+
|
|
251
|
+
const emitted = emit( 'myEvent', payload );
|
|
262
252
|
|
|
263
|
-
expect(
|
|
253
|
+
expect( stepEventBusMock.emit ).toHaveBeenCalledWith( 'usr:myEvent', payload );
|
|
254
|
+
expect( emitted ).toBe( true );
|
|
264
255
|
} );
|
|
265
256
|
} );
|
|
266
257
|
} );
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createChildLogger } from '#logger';
|
|
2
|
+
|
|
3
|
+
export const pendingHooks = new Set();
|
|
4
|
+
|
|
5
|
+
const log = createChildLogger( 'Hooks' );
|
|
6
|
+
const flushTimeoutMs = 30_000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Await all pending hooks to flush for a certain time
|
|
10
|
+
*/
|
|
11
|
+
export const flushPendingHooks = async () => {
|
|
12
|
+
const state = { timeout: null };
|
|
13
|
+
try {
|
|
14
|
+
const flushed = await Promise.race( [
|
|
15
|
+
Promise.allSettled( [ ...pendingHooks ] ).then( _ => true ),
|
|
16
|
+
new Promise( r => state.timeout = setTimeout( () => r( false ), flushTimeoutMs ) )
|
|
17
|
+
] );
|
|
18
|
+
if ( !flushed ) {
|
|
19
|
+
log.warn( 'Some hook callbacks exceeded the timeout and will not be awaited', { count: pendingHooks.size } );
|
|
20
|
+
}
|
|
21
|
+
} finally {
|
|
22
|
+
clearTimeout( state.timeout );
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const logMock = vi.hoisted( () => ( { warn: vi.fn() } ) );
|
|
4
|
+
|
|
5
|
+
vi.mock( '#logger', () => ( { createChildLogger: () => logMock } ) );
|
|
6
|
+
|
|
7
|
+
import { flushPendingHooks, pendingHooks } from './pending_hooks.js';
|
|
8
|
+
|
|
9
|
+
describe( 'pending hooks', () => {
|
|
10
|
+
beforeEach( () => {
|
|
11
|
+
pendingHooks.clear();
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
} );
|
|
14
|
+
|
|
15
|
+
afterEach( () => {
|
|
16
|
+
pendingHooks.clear();
|
|
17
|
+
vi.useRealTimers();
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
it( 'waits for pending hooks to settle', async () => {
|
|
21
|
+
const deferred = { resolve: null };
|
|
22
|
+
const hookPromise = new Promise( resolve => {
|
|
23
|
+
deferred.resolve = resolve;
|
|
24
|
+
} );
|
|
25
|
+
pendingHooks.add( hookPromise );
|
|
26
|
+
|
|
27
|
+
const flushPromise = flushPendingHooks();
|
|
28
|
+
const state = { flushed: false };
|
|
29
|
+
flushPromise.then( () => {
|
|
30
|
+
state.flushed = true;
|
|
31
|
+
} );
|
|
32
|
+
|
|
33
|
+
await Promise.resolve();
|
|
34
|
+
expect( state.flushed ).toBe( false );
|
|
35
|
+
|
|
36
|
+
deferred.resolve();
|
|
37
|
+
await flushPromise;
|
|
38
|
+
expect( state.flushed ).toBe( true );
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
it( 'stops waiting after the timeout', async () => {
|
|
42
|
+
vi.useFakeTimers();
|
|
43
|
+
pendingHooks.add( new Promise( () => {} ) );
|
|
44
|
+
|
|
45
|
+
const flushPromise = flushPendingHooks();
|
|
46
|
+
await vi.advanceTimersByTimeAsync( 30_000 );
|
|
47
|
+
|
|
48
|
+
await expect( flushPromise ).resolves.toBeUndefined();
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
it( 'logs the number of hooks that exceeded the timeout', async () => {
|
|
52
|
+
vi.useFakeTimers();
|
|
53
|
+
pendingHooks.add( new Promise( () => {} ) );
|
|
54
|
+
pendingHooks.add( new Promise( () => {} ) );
|
|
55
|
+
|
|
56
|
+
const flushPromise = flushPendingHooks();
|
|
57
|
+
await vi.advanceTimersByTimeAsync( 30_000 );
|
|
58
|
+
await flushPromise;
|
|
59
|
+
|
|
60
|
+
expect( logMock.warn ).toHaveBeenCalledWith( expect.any( String ), { count: 2 } );
|
|
61
|
+
} );
|
|
62
|
+
} );
|
package/src/interface/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { evaluator } from './evaluator.js';
|
|
9
9
|
import { step } from './step.js';
|
|
10
10
|
import { workflow } from './workflow.js';
|
|
11
|
-
import { executeInParallel } from './workflow_utils.js';
|
|
11
|
+
import { executeInParallel, hasErrorType } from './workflow_utils.js';
|
|
12
12
|
import { sendHttpRequest, sendPostRequestAndAwaitWebhook } from './webhook.js';
|
|
13
13
|
import { Logger } from './logger.js';
|
|
14
14
|
|
|
@@ -23,6 +23,7 @@ export {
|
|
|
23
23
|
EvaluationVerdictResult,
|
|
24
24
|
EvaluationFeedback,
|
|
25
25
|
executeInParallel,
|
|
26
|
+
hasErrorType,
|
|
26
27
|
sendHttpRequest,
|
|
27
28
|
sendPostRequestAndAwaitWebhook
|
|
28
29
|
};
|
|
@@ -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
|
+
} );
|