@outputai/core 0.13.1-next.357174a.0 → 0.13.1-next.38c895c.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
|
@@ -61,6 +61,18 @@ export const flattenPrototypeChain = ( target, depth = 0, result = [], maxDepth
|
|
|
61
61
|
!target || depth >= maxDepth ? result :
|
|
62
62
|
flattenPrototypeChain( Object.getPrototypeOf( target ), depth + 1, result.concat( target ), maxDepth );
|
|
63
63
|
|
|
64
|
+
// Realm agnostic check for DOMException, works for AbortController errors even on environments without DOMExceptions
|
|
65
|
+
export const isDomException = target => Object.prototype.toString.call( target ) === '[object DOMException]';
|
|
66
|
+
|
|
67
|
+
const evaluateSpecialObjectsKeysList = target => {
|
|
68
|
+
// DOMException has a special serialization because it carries a bunch of legacy constant fields
|
|
69
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException
|
|
70
|
+
if ( isDomException( target ) ) {
|
|
71
|
+
return [ 'name', 'message', 'code', 'cause', 'stack' ];
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
};
|
|
75
|
+
|
|
64
76
|
/**
|
|
65
77
|
* Define the best "name" for an Error object
|
|
66
78
|
* Rules: Assigned .name property > inherited .name if not "Error" > constructor.name
|
|
@@ -176,6 +188,8 @@ const serializeValue = ( target, options, state = { depth: 0, seen: new GlobalCo
|
|
|
176
188
|
const prototypes = flattenPrototypeChain( target );
|
|
177
189
|
const receiver = target;
|
|
178
190
|
|
|
191
|
+
const strictKeysList = evaluateSpecialObjectsKeysList( target );
|
|
192
|
+
|
|
179
193
|
const props = prototypes.reduce( ( projection, proto ) => {
|
|
180
194
|
if ( Object.keys( projection ).length > MAX_OBJECT_KEYS ) {
|
|
181
195
|
return projection;
|
|
@@ -187,6 +201,10 @@ const serializeValue = ( target, options, state = { depth: 0, seen: new GlobalCo
|
|
|
187
201
|
continue;
|
|
188
202
|
}
|
|
189
203
|
|
|
204
|
+
if ( Array.isArray( strictKeysList ) && !strictKeysList.includes( key ) ) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
190
208
|
const sanitizedKey = truncateString( key, MAX_OBJECT_KEY_LEN );
|
|
191
209
|
if ( Object.hasOwn( projection, sanitizedKey ) ) { // do not overwrite similar keys
|
|
192
210
|
continue;
|
|
@@ -212,7 +230,7 @@ const serializeValue = ( target, options, state = { depth: 0, seen: new GlobalCo
|
|
|
212
230
|
if ( !options.ignoredKeys.includes( 'name' ) && target instanceof Error ) {
|
|
213
231
|
const name = resolveErrorName( target );
|
|
214
232
|
if ( name !== undefined ) {
|
|
215
|
-
props.name = name;
|
|
233
|
+
props.name = typeof name === 'string' ? truncateString( name ) : serializeValue( name, options, nextState );
|
|
216
234
|
}
|
|
217
235
|
}
|
|
218
236
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
flattenPrototypeChain,
|
|
4
|
+
isDomException,
|
|
4
5
|
resolveErrorName,
|
|
5
6
|
serializeError,
|
|
6
7
|
truncateArray,
|
|
@@ -43,6 +44,33 @@ describe( 'error serializer helpers', () => {
|
|
|
43
44
|
} );
|
|
44
45
|
} );
|
|
45
46
|
|
|
47
|
+
describe( 'isDomException', () => {
|
|
48
|
+
afterEach( () => {
|
|
49
|
+
vi.unstubAllGlobals();
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
it( 'detects DOMExceptions, including abort reasons, and rejects lookalikes', () => {
|
|
53
|
+
expect( isDomException( new DOMException( 'aborted', 'AbortError' ) ) ).toBe( true );
|
|
54
|
+
expect( isDomException( AbortSignal.abort().reason ) ).toBe( true );
|
|
55
|
+
expect( isDomException( new Error( 'aborted' ) ) ).toBe( false );
|
|
56
|
+
expect( isDomException( { name: 'AbortError', code: 20 } ) ).toBe( false );
|
|
57
|
+
expect( isDomException( undefined ) ).toBe( false );
|
|
58
|
+
expect( isDomException( null ) ).toBe( false );
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
it( 'detects DOMExceptions where the global is unavailable, as in the workflow sandbox', () => {
|
|
62
|
+
const error = new DOMException( 'aborted', 'AbortError' );
|
|
63
|
+
vi.stubGlobal( 'DOMException', undefined );
|
|
64
|
+
|
|
65
|
+
expect( isDomException( error ) ).toBe( true );
|
|
66
|
+
expect( serializeError( error, { dropKeys: [ 'stack' ] } ) ).toEqual( {
|
|
67
|
+
name: 'AbortError',
|
|
68
|
+
message: 'aborted',
|
|
69
|
+
code: 20
|
|
70
|
+
} );
|
|
71
|
+
} );
|
|
72
|
+
} );
|
|
73
|
+
|
|
46
74
|
describe( 'resolveErrorName', () => {
|
|
47
75
|
it( 'uses explicit, inherited, and constructor names in precedence order', () => {
|
|
48
76
|
class ConstructorNamedError extends Error {}
|
|
@@ -194,6 +222,33 @@ describe( 'serializeError', () => {
|
|
|
194
222
|
expect( serializeError( /request-\d+/gi ) ).toBe( '/request-\\d+/gi' );
|
|
195
223
|
} );
|
|
196
224
|
|
|
225
|
+
it( 'serializes DOMException values without the legacy prototype constants', () => {
|
|
226
|
+
const error = new DOMException( 'Cancelled by caller', 'AbortError' );
|
|
227
|
+
|
|
228
|
+
expect( serializeError( error ) ).toEqual( {
|
|
229
|
+
name: 'AbortError',
|
|
230
|
+
message: 'Cancelled by caller',
|
|
231
|
+
code: 20,
|
|
232
|
+
stack: expect.stringContaining( 'AbortError: Cancelled by caller' )
|
|
233
|
+
} );
|
|
234
|
+
expect( serializeError( error, { dropKeys: [ /stack/ ] } ) ).toEqual( {
|
|
235
|
+
name: 'AbortError',
|
|
236
|
+
message: 'Cancelled by caller',
|
|
237
|
+
code: 20
|
|
238
|
+
} );
|
|
239
|
+
} );
|
|
240
|
+
|
|
241
|
+
it( 'marks circular DOMException fields and drops properties outside the allowed list', () => {
|
|
242
|
+
const error = new DOMException( 'aborted', 'AbortError' );
|
|
243
|
+
Object.defineProperty( error, 'message', { value: error, enumerable: true } );
|
|
244
|
+
error.attempt = 2;
|
|
245
|
+
|
|
246
|
+
expect( serializeError( { reason: error, sibling: 'preserved' }, { dropKeys: [ 'stack' ] } ) ).toEqual( {
|
|
247
|
+
reason: { name: 'AbortError', message: '[Circular Reference]', code: 20 },
|
|
248
|
+
sibling: 'preserved'
|
|
249
|
+
} );
|
|
250
|
+
} );
|
|
251
|
+
|
|
197
252
|
it( 'serializes bigint values with an n suffix', () => {
|
|
198
253
|
expect( serializeError( 42n ) ).toBe( '42n' );
|
|
199
254
|
expect( serializeError( -42n ) ).toBe( '-42n' );
|
|
@@ -298,6 +353,17 @@ describe( 'serializeError', () => {
|
|
|
298
353
|
expect( serialized['...'] ).toBe( '[Maximum Object Properties Reached]' );
|
|
299
354
|
} );
|
|
300
355
|
|
|
356
|
+
it( 'serializes the resolved error name instead of returning it raw', () => {
|
|
357
|
+
const circular = new Error( 'boom' );
|
|
358
|
+
circular.name = circular;
|
|
359
|
+
const oversized = new Error( 'boom' );
|
|
360
|
+
oversized.name = 'n'.repeat( 16_385 );
|
|
361
|
+
|
|
362
|
+
expect( serializeError( circular, { dropKeys: [ 'stack' ] } ).name ).toBe( '[Circular Reference]' );
|
|
363
|
+
expect( serializeError( oversized, { dropKeys: [ 'stack' ] } ).name )
|
|
364
|
+
.toBe( `${'n'.repeat( 16_384 )}... [1 more characters omitted]` );
|
|
365
|
+
} );
|
|
366
|
+
|
|
301
367
|
it( 'retains error identity when properties reach the object limit', () => {
|
|
302
368
|
class DetailedError extends Error {}
|
|
303
369
|
const error = new DetailedError( 'failed' );
|
package/src/worker/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Worker, NativeConnection
|
|
1
|
+
import { Worker, NativeConnection } from '@temporalio/worker';
|
|
2
2
|
import * as configs from './configs.js';
|
|
3
3
|
import { loadActivities } from './loader/activities.js';
|
|
4
4
|
import { loadWorkflows } from './loader/workflows.js';
|
|
@@ -174,11 +174,10 @@ execute()
|
|
|
174
174
|
log.info( 'Flushing hook callbacks...' );
|
|
175
175
|
await flushPendingHooks( hookFlushTimeoutMs );
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
const workerFailedToStop = shutdownFailures.some( v => v.service === 'worker' && !( v.error instanceof GracefulShutdownPeriodExpiredError ) );
|
|
177
|
+
const workerFailedToStop = shutdownFailures.some( v => v.service === 'worker' );
|
|
179
178
|
const exitCode = workerFailedToStop || hasError ? 1 : 0;
|
|
180
179
|
setTimeout( () => {
|
|
181
|
-
log.info( 'Bye' );
|
|
180
|
+
log.info( 'Bye', { exitCode } );
|
|
182
181
|
process.exit( exitCode );
|
|
183
182
|
} );
|
|
184
183
|
} );
|
package/src/worker/index.spec.js
CHANGED
|
@@ -8,7 +8,6 @@ const {
|
|
|
8
8
|
connectionMonitorInstance,
|
|
9
9
|
createCatalogMock,
|
|
10
10
|
flushPendingHooksMock,
|
|
11
|
-
GracefulShutdownPeriodExpiredError,
|
|
12
11
|
hashSourceCodeMock,
|
|
13
12
|
initInterceptorsMock,
|
|
14
13
|
interruption,
|
|
@@ -118,11 +117,6 @@ const {
|
|
|
118
117
|
name = 'UncaughtError';
|
|
119
118
|
};
|
|
120
119
|
|
|
121
|
-
/** Stands in for the Temporal error thrown when the drain outlives the force time */
|
|
122
|
-
class GracefulShutdownPeriodExpiredError extends Error {
|
|
123
|
-
name = 'GracefulShutdownPeriodExpiredError';
|
|
124
|
-
};
|
|
125
|
-
|
|
126
120
|
const interruption = { controller: null, KillSignError, UncaughtError };
|
|
127
121
|
|
|
128
122
|
return {
|
|
@@ -132,7 +126,6 @@ const {
|
|
|
132
126
|
connectionMonitorInstance,
|
|
133
127
|
createCatalogMock: vi.fn().mockReturnValue( { workflowNames: [ 'demo' ] } ),
|
|
134
128
|
flushPendingHooksMock: vi.fn().mockResolvedValue( undefined ),
|
|
135
|
-
GracefulShutdownPeriodExpiredError,
|
|
136
129
|
hashSourceCodeMock: vi.fn().mockResolvedValue( 'catalog-hash' ),
|
|
137
130
|
initInterceptorsMock: vi.fn().mockReturnValue( [] ),
|
|
138
131
|
interruption,
|
|
@@ -204,8 +197,7 @@ vi.mock( './catalog_workflow/catalog_publisher.js', () => ( {
|
|
|
204
197
|
vi.mock( './shutdown.js', () => ( { shutdownServices: shutdownServicesMock } ) );
|
|
205
198
|
vi.mock( '@temporalio/worker', () => ( {
|
|
206
199
|
NativeConnection: { connect: vi.fn().mockResolvedValue( mockConnection ) },
|
|
207
|
-
Worker: { create: vi.fn().mockResolvedValue( mockWorker ) }
|
|
208
|
-
GracefulShutdownPeriodExpiredError
|
|
200
|
+
Worker: { create: vi.fn().mockResolvedValue( mockWorker ) }
|
|
209
201
|
} ) );
|
|
210
202
|
|
|
211
203
|
const importWorker = async () => {
|
|
@@ -397,7 +389,7 @@ describe( 'worker/index', () => {
|
|
|
397
389
|
} );
|
|
398
390
|
expect( mockLog.error ).not.toHaveBeenCalled();
|
|
399
391
|
expect( mainEventBusMock.emit ).not.toHaveBeenCalledWith( BusEventType.RUNTIME_ERROR, expect.anything() );
|
|
400
|
-
expect( mockLog.info ).toHaveBeenCalledWith( 'Bye' );
|
|
392
|
+
expect( mockLog.info ).toHaveBeenCalledWith( 'Bye', { exitCode: 0 } );
|
|
401
393
|
expect( process.exit ).toHaveBeenCalledWith( 0 );
|
|
402
394
|
} );
|
|
403
395
|
|
|
@@ -412,18 +404,6 @@ describe( 'worker/index', () => {
|
|
|
412
404
|
expect( process.exit ).toHaveBeenCalledWith( 0 );
|
|
413
405
|
} );
|
|
414
406
|
|
|
415
|
-
it( 'still exits cleanly when the drain outlived the force time', async () => {
|
|
416
|
-
await bootWorker();
|
|
417
|
-
const error = new GracefulShutdownPeriodExpiredError( 'Timed out while waiting for worker to shutdown gracefully' );
|
|
418
|
-
shutdownServicesMock.mockResolvedValueOnce( [ { service: 'worker', error } ] );
|
|
419
|
-
|
|
420
|
-
interruption.controller.abort( new interruption.KillSignError( 'SIGTERM' ) );
|
|
421
|
-
await waitForExit();
|
|
422
|
-
|
|
423
|
-
expect( mockLog.error ).not.toHaveBeenCalled();
|
|
424
|
-
expect( process.exit ).toHaveBeenCalledWith( 0 );
|
|
425
|
-
} );
|
|
426
|
-
|
|
427
407
|
it( 'does not report the worker as terminated when a signal ended the run', async () => {
|
|
428
408
|
await bootWorker();
|
|
429
409
|
|
|
@@ -482,7 +462,6 @@ describe( 'worker/index', () => {
|
|
|
482
462
|
|
|
483
463
|
expect( process.exit ).toHaveBeenCalledWith( 1 );
|
|
484
464
|
} );
|
|
485
|
-
|
|
486
465
|
it( 'reports a lost connection and shuts down', async () => {
|
|
487
466
|
const error = new Error( 'connection lost' );
|
|
488
467
|
await bootWorker();
|