@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/core",
3
- "version": "0.13.1-next.357174a.0",
3
+ "version": "0.13.1-next.38c895c.0",
4
4
  "description": "The core module of the output framework",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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' );
@@ -1,4 +1,4 @@
1
- import { Worker, NativeConnection, GracefulShutdownPeriodExpiredError } from '@temporalio/worker';
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
- // An expired drain is the bound working as intended, not a worker failure, so it keeps exit code 0.
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
  } );
@@ -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();