@outputai/core 0.10.1-next.fc0a41f.0 → 0.11.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 +12 -7
- package/src/consts.js +5 -2
- package/src/errors.d.ts +12 -2
- package/src/errors.js +17 -3
- package/src/errors.spec.js +19 -0
- package/src/helpers/activity_options.js +21 -0
- package/src/helpers/activity_options.spec.js +55 -0
- package/src/helpers/error_serializer.js +236 -0
- package/src/helpers/error_serializer.spec.js +413 -0
- package/src/helpers/errors.js +55 -13
- package/src/helpers/errors.spec.js +166 -66
- package/src/helpers/fetch.js +1 -38
- package/src/helpers/fetch.spec.js +1 -39
- package/src/helpers/function.js +13 -0
- package/src/helpers/function.spec.js +23 -1
- package/src/helpers/redact.js +51 -0
- package/src/helpers/redact.spec.js +66 -0
- package/src/helpers/string.js +10 -0
- package/src/helpers/string.spec.js +36 -1
- package/src/hooks/index.d.ts +35 -44
- package/src/hooks/index.js +2 -1
- package/src/hooks/index.spec.js +15 -7
- package/src/interface/evaluator.d.ts +10 -9
- package/src/interface/evaluator.js +1 -6
- package/src/interface/evaluator.spec.js +10 -10
- package/src/interface/step.d.ts +16 -15
- package/src/interface/step.js +1 -6
- package/src/interface/step.spec.js +10 -10
- package/src/interface/validations/index.js +33 -53
- package/src/interface/validations/index.spec.js +25 -17
- package/src/interface/webhook.js +1 -2
- package/src/interface/webhook.spec.js +25 -19
- package/src/interface/workflow.d.ts +25 -18
- package/src/interface/workflow.js +24 -52
- package/src/interface/workflow.spec.js +109 -108
- package/src/interface/workflow_activity_options.js +2 -5
- package/src/temporal/client.js +47 -0
- package/src/temporal/client.spec.js +124 -0
- package/src/temporal/index.d.ts +32 -0
- package/src/temporal/index.js +1 -0
- package/src/tracing/trace_engine.js +9 -11
- package/src/tracing/trace_engine.spec.js +67 -28
- package/src/worker/catalog_workflow/catalog_job.js +3 -3
- package/src/worker/catalog_workflow/catalog_job.spec.js +1 -1
- package/src/worker/index.js +10 -1
- package/src/worker/index.spec.js +23 -8
- package/src/worker/interceptors/activity.js +32 -19
- package/src/worker/interceptors/activity.spec.js +71 -166
- package/src/worker/interceptors/workflow.js +53 -11
- package/src/worker/interceptors/workflow.spec.js +176 -49
- package/src/worker/loader/workflows.js +16 -3
- package/src/worker/loader/workflows.spec.js +37 -5
- package/src/worker/log_hooks.js +13 -10
- package/src/worker/log_hooks.spec.js +12 -4
- package/src/worker/sinks.js +3 -1
- package/src/worker/sinks.spec.js +21 -4
- package/src/worker/telemetry.js +2 -1
- package/src/worker/temporal_logger.js +33 -0
- package/src/worker/temporal_logger.spec.js +90 -0
- package/src/helpers/aggregations.js +0 -44
- package/src/helpers/aggregations.spec.js +0 -92
- package/src/tracing/tools/utils.js +0 -42
- package/src/tracing/tools/utils.spec.js +0 -83
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
flattenPrototypeChain,
|
|
4
|
+
resolveErrorName,
|
|
5
|
+
serializeError,
|
|
6
|
+
truncateArray,
|
|
7
|
+
truncateString
|
|
8
|
+
} from './error_serializer.js';
|
|
9
|
+
|
|
10
|
+
const redactUrlMock = vi.hoisted( () => vi.fn().mockReturnValue( '[Redacted URL]' ) );
|
|
11
|
+
const isUrlMock = vi.hoisted( () => vi.fn().mockReturnValue( false ) );
|
|
12
|
+
|
|
13
|
+
vi.mock( './redact.js', () => ( { redactUrl: redactUrlMock } ) );
|
|
14
|
+
vi.mock( './string.js', () => ( { isUrl: isUrlMock } ) );
|
|
15
|
+
|
|
16
|
+
describe( 'error serializer helpers', () => {
|
|
17
|
+
describe( 'truncateString', () => {
|
|
18
|
+
it( 'preserves values within the limit and marks omitted characters', () => {
|
|
19
|
+
expect( truncateString( 'exact', 5 ) ).toBe( 'exact' );
|
|
20
|
+
expect( truncateString( 'longer', 5 ) ).toBe( 'longe... [1 more characters omitted]' );
|
|
21
|
+
} );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
describe( 'truncateArray', () => {
|
|
25
|
+
it( 'preserves values within the limit and marks omitted items without mutating the source', () => {
|
|
26
|
+
const exact = [ 1, 2 ];
|
|
27
|
+
const long = [ 1, 2, 3 ];
|
|
28
|
+
|
|
29
|
+
expect( truncateArray( exact, 2 ) ).toBe( exact );
|
|
30
|
+
expect( truncateArray( long, 2 ) ).toEqual( [ 1, 2, '... [1 more items omitted]' ] );
|
|
31
|
+
expect( long ).toEqual( [ 1, 2, 3 ] );
|
|
32
|
+
} );
|
|
33
|
+
} );
|
|
34
|
+
|
|
35
|
+
describe( 'flattenPrototypeChain', () => {
|
|
36
|
+
it( 'returns child-first prototypes and honors the depth limit', () => {
|
|
37
|
+
const grandparent = { grandparent: true };
|
|
38
|
+
const parent = Object.create( grandparent );
|
|
39
|
+
const child = Object.create( parent );
|
|
40
|
+
|
|
41
|
+
expect( flattenPrototypeChain( child, 0, [], 2 ) ).toEqual( [ child, parent ] );
|
|
42
|
+
expect( flattenPrototypeChain( child, 0, [], 3 ) ).toEqual( [ child, parent, grandparent ] );
|
|
43
|
+
} );
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
describe( 'resolveErrorName', () => {
|
|
47
|
+
it( 'uses explicit, inherited, and constructor names in precedence order', () => {
|
|
48
|
+
class ConstructorNamedError extends Error {}
|
|
49
|
+
class InheritedNamedError extends Error {}
|
|
50
|
+
InheritedNamedError.prototype.name = 'InheritedName';
|
|
51
|
+
class ExplicitlyNamedError extends Error {
|
|
52
|
+
name = 'ExplicitName';
|
|
53
|
+
}
|
|
54
|
+
class ExplicitlyGenericError extends Error {
|
|
55
|
+
name = 'Error';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
expect( resolveErrorName( new ConstructorNamedError() ) ).toBe( 'ConstructorNamedError' );
|
|
59
|
+
expect( resolveErrorName( new InheritedNamedError() ) ).toBe( 'InheritedName' );
|
|
60
|
+
expect( resolveErrorName( new ExplicitlyNamedError() ) ).toBe( 'ExplicitName' );
|
|
61
|
+
expect( resolveErrorName( new ExplicitlyGenericError() ) ).toBe( 'Error' );
|
|
62
|
+
expect( resolveErrorName( new Error() ) ).toBe( 'Error' );
|
|
63
|
+
expect( resolveErrorName( { name: 'ObjectName' } ) ).toBeUndefined();
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
it( 'returns undefined when an explicit name getter throws', () => {
|
|
67
|
+
const error = new Error();
|
|
68
|
+
Object.defineProperty( error, 'name', {
|
|
69
|
+
get: () => {
|
|
70
|
+
throw new Error( 'name unavailable' );
|
|
71
|
+
}
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
expect( resolveErrorName( error ) ).toBeUndefined();
|
|
75
|
+
} );
|
|
76
|
+
} );
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
describe( 'serializeError', () => {
|
|
80
|
+
it( 'serializes descriptors and skips unusable properties', () => {
|
|
81
|
+
const error = new Error( 'step failed' );
|
|
82
|
+
Object.defineProperties( error, {
|
|
83
|
+
code: { value: 'ESTEP' },
|
|
84
|
+
hidden: { value: 'available for debugging' },
|
|
85
|
+
computed: { get: () => `computed from ${error.message}` },
|
|
86
|
+
broken: {
|
|
87
|
+
get: () => {
|
|
88
|
+
throw new Error( 'getter failed' );
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
callback: { value: () => 'not serializable' },
|
|
92
|
+
skipped: { value: undefined },
|
|
93
|
+
symbol: { value: Symbol( 'debug' ) },
|
|
94
|
+
__proto__: { value: { unsafe: true } }
|
|
95
|
+
} );
|
|
96
|
+
error.nullable = null;
|
|
97
|
+
|
|
98
|
+
const serialized = serializeError( error, { dropKeys: [ 'stack' ] } );
|
|
99
|
+
|
|
100
|
+
expect( serialized ).toEqual( {
|
|
101
|
+
name: 'Error',
|
|
102
|
+
message: 'step failed',
|
|
103
|
+
code: 'ESTEP',
|
|
104
|
+
hidden: 'available for debugging',
|
|
105
|
+
computed: 'computed from step failed',
|
|
106
|
+
nullable: null,
|
|
107
|
+
symbol: 'Symbol(debug)'
|
|
108
|
+
} );
|
|
109
|
+
expect( Object.hasOwn( serialized, '__proto__' ) ).toBe( false );
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
it( 'applies custom dropped keys recursively and across prototypes', () => {
|
|
113
|
+
class DebugError extends Error {}
|
|
114
|
+
DebugError.prototype.internal = 'prototype value';
|
|
115
|
+
|
|
116
|
+
const cause = new DebugError( 'cause failed' );
|
|
117
|
+
cause.context = { internal: 'nested cause value', visible: 'cause context' };
|
|
118
|
+
const error = new DebugError( 'request failed', { cause } );
|
|
119
|
+
error.internal = 'instance value';
|
|
120
|
+
error.context = { internal: 'nested root value', visible: 'root context' };
|
|
121
|
+
|
|
122
|
+
const serialized = serializeError( error, { dropKeys: [ 'internal', 'stack' ] } );
|
|
123
|
+
|
|
124
|
+
expect( serialized ).not.toHaveProperty( 'internal' );
|
|
125
|
+
expect( serialized.context ).toEqual( { visible: 'root context' } );
|
|
126
|
+
expect( serialized.cause ).not.toHaveProperty( 'internal' );
|
|
127
|
+
expect( serialized.cause.context ).toEqual( { visible: 'cause context' } );
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
it( 'merges prototype properties child-first without invoking overridden getters', () => {
|
|
131
|
+
const overriddenGetter = vi.fn( () => 'base' );
|
|
132
|
+
class BaseFailure extends Error {}
|
|
133
|
+
Object.defineProperties( BaseFailure.prototype, {
|
|
134
|
+
baseProperty: { value: 'base' },
|
|
135
|
+
overridden: { get: overriddenGetter }
|
|
136
|
+
} );
|
|
137
|
+
class RequestFailure extends BaseFailure {}
|
|
138
|
+
RequestFailure.prototype.requestProperty = 'request';
|
|
139
|
+
|
|
140
|
+
const error = new RequestFailure( 'request failed' );
|
|
141
|
+
Object.defineProperty( error, 'overridden', { value: 'instance' } );
|
|
142
|
+
|
|
143
|
+
expect( serializeError( error, { dropKeys: [ 'stack' ] } ) ).toEqual( {
|
|
144
|
+
name: 'RequestFailure',
|
|
145
|
+
message: 'request failed',
|
|
146
|
+
overridden: 'instance',
|
|
147
|
+
requestProperty: 'request',
|
|
148
|
+
baseProperty: 'base'
|
|
149
|
+
} );
|
|
150
|
+
expect( overriddenGetter ).not.toHaveBeenCalled();
|
|
151
|
+
} );
|
|
152
|
+
|
|
153
|
+
it( 'recursively serializes causes, nested objects, and arrays', () => {
|
|
154
|
+
const databaseError = new Error( 'database unavailable' );
|
|
155
|
+
databaseError.code = 'ECONNREFUSED';
|
|
156
|
+
const serviceError = new Error( 'service failed', { cause: databaseError } );
|
|
157
|
+
serviceError.context = {
|
|
158
|
+
attempts: [
|
|
159
|
+
{ number: 1, metadata: { retryable: true } },
|
|
160
|
+
{ number: 2, metadata: null }
|
|
161
|
+
]
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
expect( serializeError( serviceError, { dropKeys: [ 'stack' ] } ) ).toEqual( {
|
|
165
|
+
name: 'Error',
|
|
166
|
+
message: 'service failed',
|
|
167
|
+
cause: {
|
|
168
|
+
name: 'Error',
|
|
169
|
+
message: 'database unavailable',
|
|
170
|
+
code: 'ECONNREFUSED'
|
|
171
|
+
},
|
|
172
|
+
context: {
|
|
173
|
+
attempts: [
|
|
174
|
+
{ number: 1, metadata: { retryable: true } },
|
|
175
|
+
{ number: 2, metadata: null }
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
} );
|
|
179
|
+
} );
|
|
180
|
+
|
|
181
|
+
it( 'returns primitives as-is', () => {
|
|
182
|
+
expect( serializeError( undefined ) ).toBeUndefined();
|
|
183
|
+
expect( serializeError( null ) ).toBeNull();
|
|
184
|
+
expect( serializeError( 42 ) ).toBe( 42 );
|
|
185
|
+
expect( serializeError( true ) ).toBe( true );
|
|
186
|
+
} );
|
|
187
|
+
|
|
188
|
+
it( 'serializes Date values as ISO strings and marks invalid dates', () => {
|
|
189
|
+
expect( serializeError( new Date( '2026-07-22T16:30:00.000Z' ) ) ).toBe( '2026-07-22T16:30:00.000Z' );
|
|
190
|
+
expect( serializeError( new Date( 'foo' ) ) ).toBe( '[Invalid Date]' );
|
|
191
|
+
} );
|
|
192
|
+
|
|
193
|
+
it( 'serializes RegExp values with toString', () => {
|
|
194
|
+
expect( serializeError( /request-\d+/gi ) ).toBe( '/request-\\d+/gi' );
|
|
195
|
+
} );
|
|
196
|
+
|
|
197
|
+
it( 'serializes bigint values with an n suffix', () => {
|
|
198
|
+
expect( serializeError( 42n ) ).toBe( '42n' );
|
|
199
|
+
expect( serializeError( -42n ) ).toBe( '-42n' );
|
|
200
|
+
} );
|
|
201
|
+
|
|
202
|
+
it( 'marks bigints that would exceed the string length budget', () => {
|
|
203
|
+
const maxDigits = 16_383; // MAX_STRING_LEN - 1
|
|
204
|
+
const tooLarge = 10n ** BigInt( maxDigits );
|
|
205
|
+
|
|
206
|
+
expect( serializeError( tooLarge - 1n ) ).toBe( `${tooLarge - 1n}n` );
|
|
207
|
+
expect( serializeError( tooLarge ) ).toBe( '[BigInt too large]' );
|
|
208
|
+
expect( serializeError( -tooLarge ) ).toBe( '[BigInt too large]' );
|
|
209
|
+
expect( serializeError( 10n ** 20_000n ) ).toBe( '[BigInt too large]' );
|
|
210
|
+
} );
|
|
211
|
+
|
|
212
|
+
it( 'serializes symbol values', () => {
|
|
213
|
+
expect( serializeError( Symbol( 'request' ) ) ).toBe( 'Symbol(request)' );
|
|
214
|
+
} );
|
|
215
|
+
|
|
216
|
+
it( 'serializes functions by name', () => {
|
|
217
|
+
function namedFunction() {}
|
|
218
|
+
expect( serializeError( namedFunction ) ).toBe( '[Function: namedFunction]' );
|
|
219
|
+
expect( serializeError( () => undefined ) ).toBe( '[Function: (anonymous)]' );
|
|
220
|
+
} );
|
|
221
|
+
|
|
222
|
+
it( 'serializes ArrayBuffers and views as type and size without dumping contents', () => {
|
|
223
|
+
expect( serializeError( new Uint16Array( [ 1, 2 ] ) ) ).toBe( 'Uint16Array(2)' );
|
|
224
|
+
expect( serializeError( Buffer.from( [ 1, 2 ] ) ) ).toBe( 'Buffer(2)' );
|
|
225
|
+
expect( serializeError( new DataView( new ArrayBuffer( 8 ) ) ) ).toBe( 'DataView(8)' );
|
|
226
|
+
expect( serializeError( new ArrayBuffer( 16 ) ) ).toBe( 'ArrayBuffer(16)' );
|
|
227
|
+
} );
|
|
228
|
+
|
|
229
|
+
it( 'redacts URL strings and URL instances with redactUrl', () => {
|
|
230
|
+
const stringUrl = 'https://user:password@example.com/models?model=gpt-5&token=secret#debug';
|
|
231
|
+
const url = new URL( 'ftp://user:password@example.com/file.txt?download=true#details' );
|
|
232
|
+
redactUrlMock.mockClear();
|
|
233
|
+
isUrlMock.mockClear();
|
|
234
|
+
isUrlMock.mockReturnValueOnce( true );
|
|
235
|
+
|
|
236
|
+
expect( serializeError( {
|
|
237
|
+
stringUrl,
|
|
238
|
+
url
|
|
239
|
+
} ) ).toEqual( {
|
|
240
|
+
stringUrl: '[Redacted URL]',
|
|
241
|
+
url: '[Redacted URL]'
|
|
242
|
+
} );
|
|
243
|
+
expect( isUrlMock ).toHaveBeenCalledOnce();
|
|
244
|
+
expect( isUrlMock ).toHaveBeenCalledWith( stringUrl );
|
|
245
|
+
expect( redactUrlMock ).toHaveBeenCalledTimes( 2 );
|
|
246
|
+
expect( redactUrlMock ).toHaveBeenNthCalledWith( 1, stringUrl );
|
|
247
|
+
expect( redactUrlMock ).toHaveBeenNthCalledWith( 2, url );
|
|
248
|
+
} );
|
|
249
|
+
|
|
250
|
+
it( 'truncates string values and property names', () => {
|
|
251
|
+
const longValue = 'v'.repeat( 16_385 );
|
|
252
|
+
const sharedKeyPrefix = 'k'.repeat( 256 );
|
|
253
|
+
|
|
254
|
+
expect( serializeError( {
|
|
255
|
+
longValue,
|
|
256
|
+
[`${sharedKeyPrefix}a`]: 'first',
|
|
257
|
+
[`${sharedKeyPrefix}b`]: 'second'
|
|
258
|
+
} ) ).toEqual( {
|
|
259
|
+
longValue: `${'v'.repeat( 16_384 )}... [1 more characters omitted]`,
|
|
260
|
+
[`${sharedKeyPrefix}... [1 more characters omitted]`]: 'first'
|
|
261
|
+
} );
|
|
262
|
+
} );
|
|
263
|
+
|
|
264
|
+
it( 'limits arrays, maps, and sets while recursively serializing their values', () => {
|
|
265
|
+
const exactArray = Array.from( { length: 50 }, ( _, index ) => index );
|
|
266
|
+
const longArray = Array.from( { length: 52 }, ( _, index ) => index );
|
|
267
|
+
const longMap = new Map( Array.from( { length: 51 }, ( _, index ) => [ `key-${index}`, BigInt( index ) ] ) );
|
|
268
|
+
const longSet = new Set( Array.from( { length: 52 }, ( _, index ) => BigInt( index ) ) );
|
|
269
|
+
|
|
270
|
+
expect( serializeError( exactArray ) ).toEqual( exactArray );
|
|
271
|
+
expect( serializeError( longArray ) ).toEqual( [ ...exactArray, '... [2 more items omitted]' ] );
|
|
272
|
+
|
|
273
|
+
const serializedMap = serializeError( longMap );
|
|
274
|
+
expect( serializedMap ).toHaveLength( 51 );
|
|
275
|
+
expect( serializedMap[0] ).toEqual( [ 'key-0', '0n' ] );
|
|
276
|
+
expect( serializedMap.at( -1 ) ).toBe( '... [1 more items omitted]' );
|
|
277
|
+
|
|
278
|
+
const serializedSet = serializeError( longSet );
|
|
279
|
+
expect( serializedSet ).toHaveLength( 51 );
|
|
280
|
+
expect( serializedSet[0] ).toBe( '0n' );
|
|
281
|
+
expect( serializedSet.at( -1 ) ).toBe( '... [2 more items omitted]' );
|
|
282
|
+
} );
|
|
283
|
+
|
|
284
|
+
it( 'globally limits object properties while prioritizing own properties', () => {
|
|
285
|
+
const exactProperties = Object.fromEntries( Array.from( { length: 50 }, ( _, index ) => [ `property-${index}`, index ] ) );
|
|
286
|
+
expect( serializeError( exactProperties ) ).toEqual( exactProperties );
|
|
287
|
+
|
|
288
|
+
const prototype = { inherited: 'omitted' };
|
|
289
|
+
const instance = Object.assign(
|
|
290
|
+
Object.create( prototype ),
|
|
291
|
+
Object.fromEntries( Array.from( { length: 50 }, ( _, index ) => [ `own-${index}`, index ] ) )
|
|
292
|
+
);
|
|
293
|
+
const serialized = serializeError( instance );
|
|
294
|
+
|
|
295
|
+
expect( Object.keys( serialized ) ).toHaveLength( 51 );
|
|
296
|
+
expect( serialized['own-49'] ).toBe( 49 );
|
|
297
|
+
expect( serialized ).not.toHaveProperty( 'inherited' );
|
|
298
|
+
expect( serialized['...'] ).toBe( '[Maximum Object Properties Reached]' );
|
|
299
|
+
} );
|
|
300
|
+
|
|
301
|
+
it( 'retains error identity when properties reach the object limit', () => {
|
|
302
|
+
class DetailedError extends Error {}
|
|
303
|
+
const error = new DetailedError( 'failed' );
|
|
304
|
+
Object.assign( error, Object.fromEntries( Array.from( { length: 50 }, ( _, index ) => [ `field-${index}`, index ] ) ) );
|
|
305
|
+
|
|
306
|
+
const serialized = serializeError( error, { dropKeys: [ 'stack' ] } );
|
|
307
|
+
|
|
308
|
+
expect( serialized.name ).toBe( 'DetailedError' );
|
|
309
|
+
expect( serialized.message ).toBe( 'failed' );
|
|
310
|
+
expect( serialized['...'] ).toBe( '[Maximum Object Properties Reached]' );
|
|
311
|
+
} );
|
|
312
|
+
|
|
313
|
+
it( 'recursively excludes sensitive and internal properties while retaining similarly named diagnostics', () => {
|
|
314
|
+
const value = {
|
|
315
|
+
failure: { internal: 'Temporal protobuf' },
|
|
316
|
+
originalFailure: { details: 'Nexus failure' },
|
|
317
|
+
$type: { parent: 'protobuf reflection graph' },
|
|
318
|
+
request: { body: 'private request' },
|
|
319
|
+
response: { body: 'private response' },
|
|
320
|
+
headers: { Authorization: 'Bearer secret' },
|
|
321
|
+
Authorization: 'Bearer secret',
|
|
322
|
+
RequestBodyValues: { prompt: 'private prompt' },
|
|
323
|
+
RESPONSE_HEADERS: { Cookie: 'private cookie' },
|
|
324
|
+
APIKey: 'private key',
|
|
325
|
+
AccessToken: 'private token',
|
|
326
|
+
PASSWORD: 'private password',
|
|
327
|
+
Credential: 'private credential',
|
|
328
|
+
keyboard: 'preserved',
|
|
329
|
+
tokenCount: 42,
|
|
330
|
+
passwordPolicy: 'preserved',
|
|
331
|
+
secretCount: 1,
|
|
332
|
+
nested: {
|
|
333
|
+
Cookie: 'private cookie',
|
|
334
|
+
code: 'EFAILED'
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
expect( serializeError( value ) ).toEqual( {
|
|
339
|
+
keyboard: 'preserved',
|
|
340
|
+
tokenCount: 42,
|
|
341
|
+
passwordPolicy: 'preserved',
|
|
342
|
+
secretCount: 1,
|
|
343
|
+
nested: { code: 'EFAILED' }
|
|
344
|
+
} );
|
|
345
|
+
} );
|
|
346
|
+
|
|
347
|
+
it( 'includes stacks by default and supports dropping them recursively', () => {
|
|
348
|
+
const cause = new Error( 'database unavailable' );
|
|
349
|
+
const error = new Error( 'service failed', { cause } );
|
|
350
|
+
|
|
351
|
+
const withStacks = serializeError( error );
|
|
352
|
+
const withoutStacks = serializeError( error, { dropKeys: [ 'stack' ] } );
|
|
353
|
+
|
|
354
|
+
expect( withStacks.stack ).toContain( 'Error: service failed' );
|
|
355
|
+
expect( withStacks.cause.stack ).toContain( 'Error: database unavailable' );
|
|
356
|
+
expect( withoutStacks ).not.toHaveProperty( 'stack' );
|
|
357
|
+
expect( withoutStacks.cause ).not.toHaveProperty( 'stack' );
|
|
358
|
+
} );
|
|
359
|
+
|
|
360
|
+
it( 'marks circular references but serializes shared sibling values independently', () => {
|
|
361
|
+
const shared = { code: 'ESHARED' };
|
|
362
|
+
const objectCycle = {};
|
|
363
|
+
objectCycle.self = objectCycle;
|
|
364
|
+
const arrayCycle = [];
|
|
365
|
+
arrayCycle.push( arrayCycle );
|
|
366
|
+
const mapCycle = new Map();
|
|
367
|
+
mapCycle.set( 'self', mapCycle );
|
|
368
|
+
const setCycle = new Set();
|
|
369
|
+
setCycle.add( setCycle );
|
|
370
|
+
|
|
371
|
+
expect( serializeError( {
|
|
372
|
+
objectCycle,
|
|
373
|
+
arrayCycle,
|
|
374
|
+
mapCycle,
|
|
375
|
+
setCycle,
|
|
376
|
+
first: shared,
|
|
377
|
+
second: shared
|
|
378
|
+
} ) ).toEqual( {
|
|
379
|
+
objectCycle: { self: '[Circular Reference]' },
|
|
380
|
+
arrayCycle: [ '[Circular Reference]' ],
|
|
381
|
+
mapCycle: [ [ 'self', '[Circular Reference]' ] ],
|
|
382
|
+
setCycle: [ '[Circular Reference]' ],
|
|
383
|
+
first: { code: 'ESHARED' },
|
|
384
|
+
second: { code: 'ESHARED' }
|
|
385
|
+
} );
|
|
386
|
+
} );
|
|
387
|
+
|
|
388
|
+
it( 'marks values beyond the maximum recursive depth', () => {
|
|
389
|
+
const createNestedValue = depth => depth === 0 ? {} : { child: createNestedValue( depth - 1 ) };
|
|
390
|
+
const root = createNestedValue( 10 );
|
|
391
|
+
|
|
392
|
+
expect(
|
|
393
|
+
Array.from( { length: 9 } ).reduce( value => value.child, serializeError( root ) ).child
|
|
394
|
+
).toBe( '[Maximum Depth Reached]' );
|
|
395
|
+
} );
|
|
396
|
+
|
|
397
|
+
it( 'marks values after the total serialization limit is reached', () => {
|
|
398
|
+
const value = Array.from(
|
|
399
|
+
{ length: 50 },
|
|
400
|
+
() => Array.from( { length: 50 }, ( _, index ) => index )
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect( JSON.stringify( serializeError( value ) ) ).toContain( '[Serialization Limit Reached]' );
|
|
404
|
+
} );
|
|
405
|
+
|
|
406
|
+
it( 'returns an unserializable marker for exotic values that throw during reflection', () => {
|
|
407
|
+
const { proxy, revoke } = Proxy.revocable( {}, {} );
|
|
408
|
+
revoke();
|
|
409
|
+
|
|
410
|
+
expect( serializeError( proxy ) ).toBe( '[Unserializable]' );
|
|
411
|
+
expect( serializeError( { nested: proxy } ) ).toEqual( { nested: '[Unserializable]' } );
|
|
412
|
+
} );
|
|
413
|
+
} );
|
package/src/helpers/errors.js
CHANGED
|
@@ -1,16 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isPlainObject } from './object.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* @param {
|
|
6
|
-
* @param {
|
|
7
|
-
* @returns {
|
|
4
|
+
* Checks whether a value inherits from a constructor matching any supplied class name.
|
|
5
|
+
* @param {*} obj - Value whose prototype chain to inspect
|
|
6
|
+
* @param {string[]} classNames - Constructor names to match
|
|
7
|
+
* @returns {boolean} Whether a matching constructor exists
|
|
8
8
|
*/
|
|
9
|
-
export const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
export const inheritsFromAnyNamedType = ( obj, classNames ) => {
|
|
10
|
+
if ( classNames.length === 0 ) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if ( obj === null || ( typeof obj !== 'object' && typeof obj !== 'function' ) ) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const prototype = Object.getPrototypeOf( obj );
|
|
18
|
+
if ( !prototype ) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const constructor = Object.getOwnPropertyDescriptor( prototype, 'constructor' )?.value;
|
|
23
|
+
if ( constructor?.name && classNames.includes( constructor.name ) ) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return inheritsFromAnyNamedType( prototype, classNames );
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Builds an error attributing all enumerable properties from a given object.
|
|
31
|
+
* Property `cause` is rehydrated recursively when it is a plain object.
|
|
32
|
+
*
|
|
33
|
+
* If object is already an error, it is returned as it is.
|
|
34
|
+
*
|
|
35
|
+
* If is a primitive, it will only set message to its string value.
|
|
36
|
+
*
|
|
37
|
+
* @param {unknown} target
|
|
38
|
+
* @returns {Error}
|
|
39
|
+
*/
|
|
40
|
+
export const rehydrateError = target => {
|
|
41
|
+
if ( target instanceof Error ) {
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
const error = new Error();
|
|
45
|
+
error.stack = undefined;
|
|
46
|
+
|
|
47
|
+
if ( target === null || target === undefined ) {
|
|
48
|
+
return error;
|
|
49
|
+
}
|
|
50
|
+
if ( typeof target === 'object' ) {
|
|
51
|
+
for ( const [ p, v ] of Object.entries( target ) ) {
|
|
52
|
+
error[p] = ( p === 'cause' && isPlainObject( v ) ) ? rehydrateError( v ) : v;
|
|
53
|
+
}
|
|
54
|
+
return error;
|
|
55
|
+
}
|
|
56
|
+
error.message = String( target );
|
|
57
|
+
return error;
|
|
58
|
+
};
|