@nimbus-cqrs/core 2.2.0 → 2.3.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/esm/_dnt.polyfills.d.ts +7 -0
- package/esm/_dnt.polyfills.d.ts.map +1 -0
- package/esm/_dnt.polyfills.js +1 -0
- package/esm/index.d.ts +4 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +4 -0
- package/esm/lib/env/getEnv.d.ts +15 -0
- package/esm/lib/env/getEnv.d.ts.map +1 -0
- package/esm/lib/env/getEnv.js +35 -0
- package/esm/lib/eventBus/eventBus.d.ts +0 -1
- package/esm/lib/eventBus/eventBus.d.ts.map +1 -1
- package/esm/lib/eventBus/eventBus.js +31 -32
- package/esm/lib/log/logTruncator.d.ts +96 -0
- package/esm/lib/log/logTruncator.d.ts.map +1 -0
- package/esm/lib/log/logTruncator.js +254 -0
- package/esm/lib/log/logger.d.ts +20 -0
- package/esm/lib/log/logger.d.ts.map +1 -1
- package/esm/lib/log/logger.js +28 -1
- package/esm/lib/log/options.d.ts +13 -1
- package/esm/lib/log/options.d.ts.map +1 -1
- package/esm/lib/retry/withRetry.d.ts +175 -0
- package/esm/lib/retry/withRetry.d.ts.map +1 -0
- package/esm/lib/retry/withRetry.js +207 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dnt.polyfills.d.ts","sourceRoot":"","sources":["../src/_dnt.polyfills.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,KAAK;QACb,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CACF;AAED,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/esm/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import "./_dnt.polyfills.js";
|
|
2
|
+
export * from './lib/env/getEnv.js';
|
|
1
3
|
export * from './lib/eventBus/eventBus.js';
|
|
2
4
|
export * from './lib/exception/exception.js';
|
|
3
5
|
export * from './lib/exception/forbiddenException.js';
|
|
@@ -6,6 +8,7 @@ export * from './lib/exception/invalidInputException.js';
|
|
|
6
8
|
export * from './lib/exception/notFoundException.js';
|
|
7
9
|
export * from './lib/exception/unauthorizedException.js';
|
|
8
10
|
export * from './lib/log/logFormatter.js';
|
|
11
|
+
export * from './lib/log/logTruncator.js';
|
|
9
12
|
export * from './lib/log/logger.js';
|
|
10
13
|
export * from './lib/log/logLevel.js';
|
|
11
14
|
export * from './lib/log/options.js';
|
|
@@ -14,5 +17,6 @@ export * from './lib/message/event.js';
|
|
|
14
17
|
export * from './lib/message/message.js';
|
|
15
18
|
export * from './lib/message/query.js';
|
|
16
19
|
export * from './lib/message/router.js';
|
|
20
|
+
export * from './lib/retry/withRetry.js';
|
|
17
21
|
export * from './lib/tracing/withSpan.js';
|
|
18
22
|
//# sourceMappingURL=index.d.ts.map
|
package/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,0CAA0C,CAAC;AACzD,cAAc,sCAAsC,CAAC;AACrD,cAAc,0CAA0C,CAAC;AACzD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,0CAA0C,CAAC;AACzD,cAAc,sCAAsC,CAAC;AACrD,cAAc,0CAA0C,CAAC;AACzD,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC"}
|
package/esm/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import "./_dnt.polyfills.js";
|
|
2
|
+
export * from './lib/env/getEnv.js';
|
|
1
3
|
export * from './lib/eventBus/eventBus.js';
|
|
2
4
|
export * from './lib/exception/exception.js';
|
|
3
5
|
export * from './lib/exception/forbiddenException.js';
|
|
@@ -6,6 +8,7 @@ export * from './lib/exception/invalidInputException.js';
|
|
|
6
8
|
export * from './lib/exception/notFoundException.js';
|
|
7
9
|
export * from './lib/exception/unauthorizedException.js';
|
|
8
10
|
export * from './lib/log/logFormatter.js';
|
|
11
|
+
export * from './lib/log/logTruncator.js';
|
|
9
12
|
export * from './lib/log/logger.js';
|
|
10
13
|
export * from './lib/log/logLevel.js';
|
|
11
14
|
export * from './lib/log/options.js';
|
|
@@ -14,4 +17,5 @@ export * from './lib/message/event.js';
|
|
|
14
17
|
export * from './lib/message/message.js';
|
|
15
18
|
export * from './lib/message/query.js';
|
|
16
19
|
export * from './lib/message/router.js';
|
|
20
|
+
export * from './lib/retry/withRetry.js';
|
|
17
21
|
export * from './lib/tracing/withSpan.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type GetEnvInput = {
|
|
2
|
+
variables: string[];
|
|
3
|
+
};
|
|
4
|
+
/**
|
|
5
|
+
* Get environment variables from the process.env object.
|
|
6
|
+
* Throws an exception if any of the variables are not defined
|
|
7
|
+
* and logs the missing variable names.
|
|
8
|
+
*
|
|
9
|
+
* @param variables - The list of environment variables to get
|
|
10
|
+
* @returns {Record<string, string>} Object of the environment variables
|
|
11
|
+
* @throws {GenericException} Thrown if any of the requested variables are not defined
|
|
12
|
+
*/
|
|
13
|
+
export declare const getEnv: ({ variables, }: GetEnvInput) => Record<string, string>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=getEnv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getEnv.d.ts","sourceRoot":"","sources":["../../../src/lib/env/getEnv.ts"],"names":[],"mappings":"AAIA,KAAK,WAAW,GAAG;IACf,SAAS,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM,GAAI,gBAEpB,WAAW,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CA2BrC,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { GenericException } from '../exception/genericException.js';
|
|
3
|
+
import { getLogger } from '../log/logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Get environment variables from the process.env object.
|
|
6
|
+
* Throws an exception if any of the variables are not defined
|
|
7
|
+
* and logs the missing variable names.
|
|
8
|
+
*
|
|
9
|
+
* @param variables - The list of environment variables to get
|
|
10
|
+
* @returns {Record<string, string>} Object of the environment variables
|
|
11
|
+
* @throws {GenericException} Thrown if any of the requested variables are not defined
|
|
12
|
+
*/
|
|
13
|
+
export const getEnv = ({ variables, }) => {
|
|
14
|
+
const envVars = {};
|
|
15
|
+
const missingEnvVars = [];
|
|
16
|
+
for (const variable of variables) {
|
|
17
|
+
if (!process.env[variable]) {
|
|
18
|
+
missingEnvVars.push(variable);
|
|
19
|
+
}
|
|
20
|
+
envVars[variable] = process.env[variable];
|
|
21
|
+
}
|
|
22
|
+
if (missingEnvVars.length > 0) {
|
|
23
|
+
getLogger().error({
|
|
24
|
+
category: 'Nimbus',
|
|
25
|
+
message: 'Undefined environment variables',
|
|
26
|
+
data: {
|
|
27
|
+
undefinedVariables: missingEnvVars,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
throw new GenericException('Undefined environment variables', {
|
|
31
|
+
undefinedVariables: missingEnvVars,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return envVars;
|
|
35
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eventBus.d.ts","sourceRoot":"","sources":["../../../src/lib/eventBus/eventBus.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"eventBus.d.ts","sourceRoot":"","sources":["../../../src/lib/eventBus/eventBus.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AA2CjD;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAChC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACvC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,mBAAmB,CAAC,MAAM,SAAS,KAAK,IAAI;IACpD;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;OAEG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD;;OAEG;IACH,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;CACjD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,qBAAa,cAAc;IACvB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAyB;IAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;gBACS,OAAO,CAAC,EAAE,qBAAqB;IAU3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACI,QAAQ,CAAC,MAAM,SAAS,KAAK,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAqD1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiDG;IACI,cAAc,CAAC,MAAM,SAAS,KAAK,EAAE,EACxC,IAAI,EACJ,OAAO,EACP,OAAO,EACP,OAAO,GACV,EAAE,mBAAmB,CAAC,MAAM,CAAC,GAAG,IAAI;IAqCrC,OAAO,CAAC,aAAa;IA4ErB,OAAO,CAAC,sBAAsB;IAY9B,OAAO,CAAC,mBAAmB;IAwC3B;;;;;;;;OAQG;IACH,OAAO,CAAC,kBAAkB;CAmB7B;AAOD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,aAAa,GACtB,MAAM,MAAM,EACZ,UAAU,IAAI,CAAC,qBAAqB,EAAE,MAAM,CAAC,KAC9C,IAEF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,WAAW,GAAI,OAAM,MAAkB,KAAG,cAStD,CAAC"}
|
|
@@ -2,6 +2,7 @@ import { metrics, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
|
|
|
2
2
|
import EventEmitter from 'node:events';
|
|
3
3
|
import { GenericException } from '../exception/genericException.js';
|
|
4
4
|
import { getLogger } from '../log/logger.js';
|
|
5
|
+
import { withRetry } from '../retry/withRetry.js';
|
|
5
6
|
const tracer = trace.getTracer('nimbus');
|
|
6
7
|
const meter = metrics.getMeter('nimbus');
|
|
7
8
|
const eventsPublishedCounter = meter.createCounter('eventbus_events_published_total', {
|
|
@@ -301,35 +302,38 @@ export class NimbusEventBus {
|
|
|
301
302
|
}),
|
|
302
303
|
},
|
|
303
304
|
}, async (span) => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
try {
|
|
305
|
+
try {
|
|
306
|
+
await withRetry(async () => {
|
|
307
307
|
await handler(event);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
attempt
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
span,
|
|
319
|
-
metricLabels,
|
|
320
|
-
startTime,
|
|
321
|
-
retryConfig: {
|
|
322
|
-
maxRetries,
|
|
323
|
-
baseDelay,
|
|
324
|
-
maxDelay,
|
|
325
|
-
},
|
|
308
|
+
}, {
|
|
309
|
+
maxRetries,
|
|
310
|
+
initialDelayMs: baseDelay,
|
|
311
|
+
maxDelayMs: maxDelay,
|
|
312
|
+
jitterFactor: useJitter ? 0.1 : 0,
|
|
313
|
+
onRetry: ({ attempt, delayMs }) => {
|
|
314
|
+
retryAttemptsCounter.add(1, metricLabels);
|
|
315
|
+
span.addEvent('retry', {
|
|
316
|
+
attempt,
|
|
317
|
+
delay_ms: delayMs,
|
|
326
318
|
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
this._recordDeliveryMetrics(metricLabels, 'success', startTime);
|
|
322
|
+
span.end();
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
this._handleFinalFailure({
|
|
326
|
+
error,
|
|
327
|
+
event,
|
|
328
|
+
span,
|
|
329
|
+
metricLabels,
|
|
330
|
+
startTime,
|
|
331
|
+
retryConfig: {
|
|
332
|
+
maxRetries,
|
|
333
|
+
baseDelay,
|
|
334
|
+
maxDelay,
|
|
335
|
+
},
|
|
336
|
+
});
|
|
333
337
|
}
|
|
334
338
|
});
|
|
335
339
|
}
|
|
@@ -355,11 +359,6 @@ export class NimbusEventBus {
|
|
|
355
359
|
}
|
|
356
360
|
throw exception;
|
|
357
361
|
}
|
|
358
|
-
_calculateRetryDelay(attempt, baseDelay, maxDelay, useJitter) {
|
|
359
|
-
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
360
|
-
const jitter = useJitter ? Math.random() * delay * 0.1 : 0;
|
|
361
|
-
return delay + jitter;
|
|
362
|
-
}
|
|
363
362
|
/**
|
|
364
363
|
* Validate the size of the event and return the size in bytes.
|
|
365
364
|
*
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { LogInput } from './logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Truncates a {@link LogInput} before it is turned into a log record.
|
|
4
|
+
*
|
|
5
|
+
* Use with {@link setupLogger} via the `truncator` option, or provide a custom
|
|
6
|
+
* implementation. The built-in factory is {@link createLogTruncator}.
|
|
7
|
+
*/
|
|
8
|
+
export type LogTruncator = (logInput: LogInput) => LogInput;
|
|
9
|
+
/**
|
|
10
|
+
* Options for {@link createLogTruncator}.
|
|
11
|
+
*
|
|
12
|
+
* All fields are optional; omitted fields use the built-in defaults.
|
|
13
|
+
*/
|
|
14
|
+
export type LogTruncatorOptions = {
|
|
15
|
+
/**
|
|
16
|
+
* Maximum serialized byte size of `data` after structural truncation.
|
|
17
|
+
* If still over budget (or not JSON-serializable), `data` is replaced
|
|
18
|
+
* with a size marker. Defaults to 16_384 (16 KB).
|
|
19
|
+
*/
|
|
20
|
+
maxBytes?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Maximum number of array items kept inside `data` (also applies when
|
|
23
|
+
* converting `Map` / `Set` to arrays). Defaults to 50.
|
|
24
|
+
*/
|
|
25
|
+
maxArrayItems?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Maximum number of own enumerable keys kept on plain objects inside
|
|
28
|
+
* `data`. Defaults to 50.
|
|
29
|
+
*/
|
|
30
|
+
maxObjectKeys?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Maximum object nesting depth inside `data`, and maximum depth when
|
|
33
|
+
* walking `error.cause` / aggregate error chains.
|
|
34
|
+
* Defaults to 8. Values below 1 are clamped to 1 so root `data` stays
|
|
35
|
+
* an object.
|
|
36
|
+
*/
|
|
37
|
+
maxDepth?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Maximum length of `category`.
|
|
40
|
+
* Defaults to 50.
|
|
41
|
+
*/
|
|
42
|
+
maxCategoryLength?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Maximum length of `message` and `error.message`.
|
|
45
|
+
* Defaults to 200.
|
|
46
|
+
*/
|
|
47
|
+
maxMessageLength?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Maximum length of `error.stack`.
|
|
50
|
+
* Defaults to 200.
|
|
51
|
+
*/
|
|
52
|
+
maxStackLength?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Maximum length of string values inside `data`.
|
|
55
|
+
* Defaults to 200.
|
|
56
|
+
*/
|
|
57
|
+
maxDataStringLength?: number;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Creates a {@link LogTruncator} that truncates each {@link LogInput} field
|
|
61
|
+
* separately (except `correlationId`, which is left untouched).
|
|
62
|
+
*
|
|
63
|
+
* - `message` / `category`: string length caps
|
|
64
|
+
* - `data`: structural truncation (arrays, objects, strings, depth, circular
|
|
65
|
+
* refs, common JS types) plus a byte-size cliff that replaces oversized or
|
|
66
|
+
* unserializable payloads with a size marker
|
|
67
|
+
* - `error`: stays `Error`-shaped; truncates `message` / `stack`, copies
|
|
68
|
+
* enumerable custom fields, and walks `cause` / aggregate errors up to
|
|
69
|
+
* `maxDepth`
|
|
70
|
+
*
|
|
71
|
+
* @param options - Optional limits; omitted fields use built-in defaults.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { createLogTruncator, setupLogger } from '@nimbus-cqrs/core';
|
|
76
|
+
*
|
|
77
|
+
* setupLogger({
|
|
78
|
+
* truncator: createLogTruncator(),
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* setupLogger({
|
|
82
|
+
* truncator: createLogTruncator({
|
|
83
|
+
* maxBytes: 8_192,
|
|
84
|
+
* maxArrayItems: 50,
|
|
85
|
+
* maxObjectKeys: 50,
|
|
86
|
+
* maxDepth: 8,
|
|
87
|
+
* maxCategoryLength: 50,
|
|
88
|
+
* maxMessageLength: 100,
|
|
89
|
+
* maxStackLength: 200,
|
|
90
|
+
* maxDataStringLength: 200,
|
|
91
|
+
* }),
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare const createLogTruncator: (options?: LogTruncatorOptions) => LogTruncator;
|
|
96
|
+
//# sourceMappingURL=logTruncator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logTruncator.d.ts","sourceRoot":"","sources":["../../../src/lib/log/logTruncator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAE5D;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAsDF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,kBAAkB,GAC3B,UAAS,mBAAwB,KAClC,YA+TF,CAAC"}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
const DEFAULT_MAX_BYTES = 16_384;
|
|
2
|
+
const DEFAULT_MAX_ARRAY_ITEMS = 50;
|
|
3
|
+
const DEFAULT_MAX_OBJECT_KEYS = 50;
|
|
4
|
+
const DEFAULT_MAX_DEPTH = 8;
|
|
5
|
+
const DEFAULT_MAX_CATEGORY_LENGTH = 50;
|
|
6
|
+
const DEFAULT_MAX_MESSAGE_LENGTH = 200;
|
|
7
|
+
const DEFAULT_MAX_STACK_LENGTH = 200;
|
|
8
|
+
const DEFAULT_MAX_DATA_STRING_LENGTH = 200;
|
|
9
|
+
const serializedByteSize = (value) => {
|
|
10
|
+
try {
|
|
11
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const TRUNCATED_STRING_SUFFIX = ' [... __truncated]';
|
|
18
|
+
const truncateString = (value, maxLength) => {
|
|
19
|
+
if (value.length <= maxLength) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
return `${value.slice(0, maxLength)}${TRUNCATED_STRING_SUFFIX}`;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Creates a {@link LogTruncator} that truncates each {@link LogInput} field
|
|
26
|
+
* separately (except `correlationId`, which is left untouched).
|
|
27
|
+
*
|
|
28
|
+
* - `message` / `category`: string length caps
|
|
29
|
+
* - `data`: structural truncation (arrays, objects, strings, depth, circular
|
|
30
|
+
* refs, common JS types) plus a byte-size cliff that replaces oversized or
|
|
31
|
+
* unserializable payloads with a size marker
|
|
32
|
+
* - `error`: stays `Error`-shaped; truncates `message` / `stack`, copies
|
|
33
|
+
* enumerable custom fields, and walks `cause` / aggregate errors up to
|
|
34
|
+
* `maxDepth`
|
|
35
|
+
*
|
|
36
|
+
* @param options - Optional limits; omitted fields use built-in defaults.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { createLogTruncator, setupLogger } from '@nimbus-cqrs/core';
|
|
41
|
+
*
|
|
42
|
+
* setupLogger({
|
|
43
|
+
* truncator: createLogTruncator(),
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* setupLogger({
|
|
47
|
+
* truncator: createLogTruncator({
|
|
48
|
+
* maxBytes: 8_192,
|
|
49
|
+
* maxArrayItems: 50,
|
|
50
|
+
* maxObjectKeys: 50,
|
|
51
|
+
* maxDepth: 8,
|
|
52
|
+
* maxCategoryLength: 50,
|
|
53
|
+
* maxMessageLength: 100,
|
|
54
|
+
* maxStackLength: 200,
|
|
55
|
+
* maxDataStringLength: 200,
|
|
56
|
+
* }),
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export const createLogTruncator = (options = {}) => {
|
|
61
|
+
// Clamp so root `data` stays an object and string/array limits stay usable.
|
|
62
|
+
const maxBytes = Math.max(0, options.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
63
|
+
const maxArrayItems = Math.max(0, options.maxArrayItems ?? DEFAULT_MAX_ARRAY_ITEMS);
|
|
64
|
+
const maxObjectKeys = Math.max(0, options.maxObjectKeys ?? DEFAULT_MAX_OBJECT_KEYS);
|
|
65
|
+
const maxDepth = Math.max(1, options.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
66
|
+
const maxCategoryLength = Math.max(0, options.maxCategoryLength ?? DEFAULT_MAX_CATEGORY_LENGTH);
|
|
67
|
+
const maxMessageLength = Math.max(0, options.maxMessageLength ?? DEFAULT_MAX_MESSAGE_LENGTH);
|
|
68
|
+
const maxStackLength = Math.max(0, options.maxStackLength ?? DEFAULT_MAX_STACK_LENGTH);
|
|
69
|
+
const maxDataStringLength = Math.max(0, options.maxDataStringLength ?? DEFAULT_MAX_DATA_STRING_LENGTH);
|
|
70
|
+
const truncateDataValue = (value, depth, seen) => {
|
|
71
|
+
if (typeof value === 'bigint') {
|
|
72
|
+
return truncateString(`${value}n`, maxDataStringLength);
|
|
73
|
+
}
|
|
74
|
+
if (value === null || typeof value !== 'object') {
|
|
75
|
+
if (typeof value === 'string' &&
|
|
76
|
+
value.length > maxDataStringLength) {
|
|
77
|
+
return truncateString(value, maxDataStringLength);
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
if (seen.has(value)) {
|
|
82
|
+
return '[Circular]';
|
|
83
|
+
}
|
|
84
|
+
if (depth >= maxDepth) {
|
|
85
|
+
return '[Max depth]';
|
|
86
|
+
}
|
|
87
|
+
seen.add(value);
|
|
88
|
+
try {
|
|
89
|
+
if (value instanceof Date) {
|
|
90
|
+
return truncateString(value.toISOString(), maxDataStringLength);
|
|
91
|
+
}
|
|
92
|
+
if (value instanceof RegExp) {
|
|
93
|
+
return truncateString(value.toString(), maxDataStringLength);
|
|
94
|
+
}
|
|
95
|
+
if (value instanceof ArrayBuffer) {
|
|
96
|
+
return {
|
|
97
|
+
__truncated: true,
|
|
98
|
+
reason: 'arrayBuffer',
|
|
99
|
+
byteLength: value.byteLength,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (ArrayBuffer.isView(value)) {
|
|
103
|
+
return {
|
|
104
|
+
__truncated: true,
|
|
105
|
+
reason: 'typedArray',
|
|
106
|
+
type: value.constructor.name,
|
|
107
|
+
byteLength: value.byteLength,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (value instanceof Error) {
|
|
111
|
+
return truncateErrorAsData(value, depth, seen);
|
|
112
|
+
}
|
|
113
|
+
if (value instanceof Map) {
|
|
114
|
+
return truncateDataValue(Array.from(value.entries()), depth + 1, seen);
|
|
115
|
+
}
|
|
116
|
+
if (value instanceof Set) {
|
|
117
|
+
return truncateDataValue(Array.from(value.values()), depth + 1, seen);
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
if (value.length > maxArrayItems) {
|
|
121
|
+
return [
|
|
122
|
+
...value
|
|
123
|
+
.slice(0, maxArrayItems)
|
|
124
|
+
.map((item) => truncateDataValue(item, depth + 1, seen)),
|
|
125
|
+
{
|
|
126
|
+
__truncated: true,
|
|
127
|
+
omittedCount: value.length - maxArrayItems,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
return value.map((item) => truncateDataValue(item, depth + 1, seen));
|
|
132
|
+
}
|
|
133
|
+
const entries = Object.entries(value);
|
|
134
|
+
const limited = entries.length > maxObjectKeys
|
|
135
|
+
? entries.slice(0, maxObjectKeys)
|
|
136
|
+
: entries;
|
|
137
|
+
const result = {};
|
|
138
|
+
for (const [key, entry] of limited) {
|
|
139
|
+
result[key] = truncateDataValue(entry, depth + 1, seen);
|
|
140
|
+
}
|
|
141
|
+
if (entries.length > maxObjectKeys) {
|
|
142
|
+
result.__truncated = true;
|
|
143
|
+
result.omittedKeyCount = entries.length - maxObjectKeys;
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
// Recursion stack: allow shared refs, still detect true cycles.
|
|
149
|
+
seen.delete(value);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const truncateErrorAsData = (error, depth, seen) => {
|
|
153
|
+
const result = {
|
|
154
|
+
name: error.name,
|
|
155
|
+
message: truncateString(error.message, maxMessageLength),
|
|
156
|
+
};
|
|
157
|
+
if (typeof error.stack === 'string') {
|
|
158
|
+
result.stack = truncateString(error.stack, maxStackLength);
|
|
159
|
+
}
|
|
160
|
+
for (const [key, entry] of Object.entries(error)) {
|
|
161
|
+
if (key === 'name' ||
|
|
162
|
+
key === 'message' ||
|
|
163
|
+
key === 'stack' ||
|
|
164
|
+
key === 'cause' ||
|
|
165
|
+
key === 'errors') {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
result[key] = truncateDataValue(entry, depth + 1, seen);
|
|
169
|
+
}
|
|
170
|
+
if ('cause' in error && error.cause !== undefined) {
|
|
171
|
+
result.cause = truncateDataValue(error.cause, depth + 1, seen);
|
|
172
|
+
}
|
|
173
|
+
if (typeof AggregateError !== 'undefined' &&
|
|
174
|
+
error instanceof AggregateError) {
|
|
175
|
+
result.errors = truncateDataValue(error.errors, depth + 1, seen);
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
};
|
|
179
|
+
const truncateData = (data) => {
|
|
180
|
+
const truncated = truncateDataValue(data, 0, new WeakSet());
|
|
181
|
+
const byteSize = serializedByteSize(truncated);
|
|
182
|
+
if (byteSize !== undefined && byteSize <= maxBytes) {
|
|
183
|
+
return truncated;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
__truncated: true,
|
|
187
|
+
reason: byteSize === undefined ? 'unserializable' : 'size',
|
|
188
|
+
...(byteSize !== undefined ? { serializedByteSize: byteSize } : {}),
|
|
189
|
+
maxBytes,
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
const copyCustomErrorFields = (source, target, depth) => {
|
|
193
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
194
|
+
if (key === 'name' ||
|
|
195
|
+
key === 'message' ||
|
|
196
|
+
key === 'stack' ||
|
|
197
|
+
key === 'cause' ||
|
|
198
|
+
key === 'errors') {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
target[key] =
|
|
202
|
+
entry instanceof Error
|
|
203
|
+
? truncateError(entry, depth + 1)
|
|
204
|
+
: truncateDataValue(entry, depth + 1, new WeakSet());
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const truncateError = (error, depth) => {
|
|
208
|
+
if (depth >= maxDepth) {
|
|
209
|
+
const maxDepthError = new Error('[Max depth]');
|
|
210
|
+
maxDepthError.name = error.name;
|
|
211
|
+
return maxDepthError;
|
|
212
|
+
}
|
|
213
|
+
const message = truncateString(error.message, maxMessageLength);
|
|
214
|
+
let truncated;
|
|
215
|
+
if (typeof AggregateError !== 'undefined' &&
|
|
216
|
+
error instanceof AggregateError) {
|
|
217
|
+
truncated = new AggregateError(error.errors.map((item) => item instanceof Error
|
|
218
|
+
? truncateError(item, depth + 1)
|
|
219
|
+
: truncateDataValue(item, depth + 1, new WeakSet())), message);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
truncated = new Error(message);
|
|
223
|
+
}
|
|
224
|
+
truncated.name = error.name;
|
|
225
|
+
if (typeof error.stack === 'string') {
|
|
226
|
+
truncated.stack = truncateString(error.stack, maxStackLength);
|
|
227
|
+
}
|
|
228
|
+
if ('cause' in error && error.cause !== undefined) {
|
|
229
|
+
truncated.cause = error.cause instanceof Error
|
|
230
|
+
? truncateError(error.cause, depth + 1)
|
|
231
|
+
: truncateDataValue(error.cause, depth + 1, new WeakSet());
|
|
232
|
+
}
|
|
233
|
+
copyCustomErrorFields(error, truncated, depth);
|
|
234
|
+
return truncated;
|
|
235
|
+
};
|
|
236
|
+
return (logInput) => {
|
|
237
|
+
const truncated = {
|
|
238
|
+
message: truncateString(logInput.message, maxMessageLength),
|
|
239
|
+
};
|
|
240
|
+
if (logInput.category !== undefined) {
|
|
241
|
+
truncated.category = truncateString(logInput.category, maxCategoryLength);
|
|
242
|
+
}
|
|
243
|
+
if (logInput.data !== undefined) {
|
|
244
|
+
truncated.data = truncateData(logInput.data);
|
|
245
|
+
}
|
|
246
|
+
if (logInput.error !== undefined) {
|
|
247
|
+
truncated.error = truncateError(logInput.error, 0);
|
|
248
|
+
}
|
|
249
|
+
if (logInput.correlationId !== undefined) {
|
|
250
|
+
truncated.correlationId = logInput.correlationId;
|
|
251
|
+
}
|
|
252
|
+
return truncated;
|
|
253
|
+
};
|
|
254
|
+
};
|
package/esm/lib/log/logger.d.ts
CHANGED
|
@@ -32,6 +32,12 @@ export type LogInput = {
|
|
|
32
32
|
* Useful for tracking a request through multiple services or handlers.
|
|
33
33
|
*/
|
|
34
34
|
correlationId?: string;
|
|
35
|
+
/**
|
|
36
|
+
* When `true`, skips the configured truncator for this log call.
|
|
37
|
+
* Useful for intentionally logging full payloads. Defaults to `false`.
|
|
38
|
+
* This flag is not included in the emitted log record.
|
|
39
|
+
*/
|
|
40
|
+
skipTruncation?: boolean;
|
|
35
41
|
};
|
|
36
42
|
/**
|
|
37
43
|
* A full log record with the log input and additional metadata attached.
|
|
@@ -80,6 +86,7 @@ export type LogRecord = {
|
|
|
80
86
|
* @example
|
|
81
87
|
* ```ts
|
|
82
88
|
* import {
|
|
89
|
+
* createLogTruncator,
|
|
83
90
|
* getLogger,
|
|
84
91
|
* jsonLogFormatter,
|
|
85
92
|
* parseLogLevel,
|
|
@@ -94,6 +101,7 @@ export type LogRecord = {
|
|
|
94
101
|
* ? jsonLogFormatter
|
|
95
102
|
* : prettyLogFormatter,
|
|
96
103
|
* useConsoleColors: process.env.NODE_ENV !== 'production',
|
|
104
|
+
* truncator: createLogTruncator(),
|
|
97
105
|
* });
|
|
98
106
|
*
|
|
99
107
|
* // Use the logger throughout your application
|
|
@@ -118,6 +126,7 @@ export declare class Logger {
|
|
|
118
126
|
private readonly _logLevel;
|
|
119
127
|
private readonly _formatter;
|
|
120
128
|
private readonly _useConsoleColors;
|
|
129
|
+
private readonly _truncator?;
|
|
121
130
|
constructor(options: LogOptions);
|
|
122
131
|
/**
|
|
123
132
|
* Configure the Logger.
|
|
@@ -125,6 +134,7 @@ export declare class Logger {
|
|
|
125
134
|
* @param {LogOptions} options - The options for the Logger
|
|
126
135
|
* @param {LogLevel} options.logLevel - The log level to use for the Logger
|
|
127
136
|
* @param {LogFormatter} options.formatter - The formatter to use for the Logger
|
|
137
|
+
* @param {LogTruncator} options.truncator - Optional truncator for log inputs
|
|
128
138
|
*/
|
|
129
139
|
static configure(options: LogOptions): void;
|
|
130
140
|
/**
|
|
@@ -289,6 +299,14 @@ export declare class Logger {
|
|
|
289
299
|
* @returns {string} The colorized string
|
|
290
300
|
*/
|
|
291
301
|
private _colorizeString;
|
|
302
|
+
/**
|
|
303
|
+
* Apply the configured truncator, failing open if it throws.
|
|
304
|
+
*
|
|
305
|
+
* @param {LogInput} logInput - The original log input
|
|
306
|
+
*
|
|
307
|
+
* @returns {LogInput} The truncated log input, or the original on failure
|
|
308
|
+
*/
|
|
309
|
+
private _applyTruncator;
|
|
292
310
|
/**
|
|
293
311
|
* Log a message.
|
|
294
312
|
*
|
|
@@ -306,6 +324,7 @@ export declare class Logger {
|
|
|
306
324
|
* @example
|
|
307
325
|
* ```ts
|
|
308
326
|
* import {
|
|
327
|
+
* createLogTruncator,
|
|
309
328
|
* jsonLogFormatter,
|
|
310
329
|
* parseLogLevel,
|
|
311
330
|
* prettyLogFormatter,
|
|
@@ -319,6 +338,7 @@ export declare class Logger {
|
|
|
319
338
|
* ? prettyLogFormatter
|
|
320
339
|
* : jsonLogFormatter,
|
|
321
340
|
* useConsoleColors: process.env.NODE_ENV === "development",
|
|
341
|
+
* truncator: createLogTruncator(),
|
|
322
342
|
* });
|
|
323
343
|
* ```
|
|
324
344
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/lib/log/logger.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAE3D,OAAO,EAAE,KAAK,QAAQ,EAAmB,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/lib/log/logger.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAE3D,OAAO,EAAE,KAAK,QAAQ,EAAmB,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAGlE;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAAG;IACnB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC;IACd;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG;IACpB;;OAEG;IACH,SAAS,EAAE,IAAI,CAAC;IAChB;;OAEG;IACH,KAAK,EAAE,QAAQ,CAAC;IAChB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B;;OAEG;IACH,KAAK,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IAC1B;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,qBAAa,MAAM;IACf,OAAO,CAAC,MAAM,CAAC,SAAS,CAAS;IAEjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAW;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAe;IAC1C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAe;gBAE/B,OAAO,EAAE,UAAU;IAQ/B;;;;;;;OAOG;WACW,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAIlD;;;;OAIG;WACW,WAAW,IAAI,MAAM;IAQnC;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACI,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAMtC;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACI,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAMrC;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACI,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAMrC;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACI,KAAK,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAMtC;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACI,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAMzC;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB;IAgBzB;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAMxB;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAwBvB;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAgBvB;;;;;;OAMG;IACH,OAAO,CAAC,IAAI;CAyBf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,UAAU,KAAG,IAEjD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,eAAO,MAAM,SAAS,QAAO,MAE5B,CAAC"}
|
package/esm/lib/log/logger.js
CHANGED
|
@@ -13,6 +13,7 @@ import { defaultLogOptions } from './options.js';
|
|
|
13
13
|
* @example
|
|
14
14
|
* ```ts
|
|
15
15
|
* import {
|
|
16
|
+
* createLogTruncator,
|
|
16
17
|
* getLogger,
|
|
17
18
|
* jsonLogFormatter,
|
|
18
19
|
* parseLogLevel,
|
|
@@ -27,6 +28,7 @@ import { defaultLogOptions } from './options.js';
|
|
|
27
28
|
* ? jsonLogFormatter
|
|
28
29
|
* : prettyLogFormatter,
|
|
29
30
|
* useConsoleColors: process.env.NODE_ENV !== 'production',
|
|
31
|
+
* truncator: createLogTruncator(),
|
|
30
32
|
* });
|
|
31
33
|
*
|
|
32
34
|
* // Use the logger throughout your application
|
|
@@ -51,11 +53,13 @@ export class Logger {
|
|
|
51
53
|
_logLevel;
|
|
52
54
|
_formatter;
|
|
53
55
|
_useConsoleColors;
|
|
56
|
+
_truncator;
|
|
54
57
|
constructor(options) {
|
|
55
58
|
this._logLevel = options.logLevel ?? defaultLogOptions.logLevel;
|
|
56
59
|
this._formatter = options.formatter ?? defaultLogOptions.formatter;
|
|
57
60
|
this._useConsoleColors = options.useConsoleColors ??
|
|
58
61
|
defaultLogOptions.useConsoleColors;
|
|
62
|
+
this._truncator = options.truncator ?? defaultLogOptions.truncator;
|
|
59
63
|
}
|
|
60
64
|
/**
|
|
61
65
|
* Configure the Logger.
|
|
@@ -63,6 +67,7 @@ export class Logger {
|
|
|
63
67
|
* @param {LogOptions} options - The options for the Logger
|
|
64
68
|
* @param {LogLevel} options.logLevel - The log level to use for the Logger
|
|
65
69
|
* @param {LogFormatter} options.formatter - The formatter to use for the Logger
|
|
70
|
+
* @param {LogTruncator} options.truncator - Optional truncator for log inputs
|
|
66
71
|
*/
|
|
67
72
|
static configure(options) {
|
|
68
73
|
Logger._instance = new Logger(options);
|
|
@@ -285,6 +290,25 @@ export class Logger {
|
|
|
285
290
|
}
|
|
286
291
|
return string;
|
|
287
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Apply the configured truncator, failing open if it throws.
|
|
295
|
+
*
|
|
296
|
+
* @param {LogInput} logInput - The original log input
|
|
297
|
+
*
|
|
298
|
+
* @returns {LogInput} The truncated log input, or the original on failure
|
|
299
|
+
*/
|
|
300
|
+
_applyTruncator(logInput) {
|
|
301
|
+
if (!this._truncator || logInput.skipTruncation) {
|
|
302
|
+
return logInput;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
return this._truncator(logInput);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
console.warn('Log truncator failed; logging original input.', error);
|
|
309
|
+
return logInput;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
288
312
|
/**
|
|
289
313
|
* Log a message.
|
|
290
314
|
*
|
|
@@ -293,7 +317,8 @@ export class Logger {
|
|
|
293
317
|
* @param {Function} logFunction - The log function
|
|
294
318
|
*/
|
|
295
319
|
_log(logInput, logLevel, logFunction) {
|
|
296
|
-
const
|
|
320
|
+
const truncatedInput = this._applyTruncator(logInput);
|
|
321
|
+
const logRecord = this._produceLogRecord(truncatedInput, logLevel);
|
|
297
322
|
const formattedLogRecord = this._formatLogRecord(logRecord);
|
|
298
323
|
if (Array.isArray(formattedLogRecord)) {
|
|
299
324
|
if (this._useConsoleColors) {
|
|
@@ -320,6 +345,7 @@ export class Logger {
|
|
|
320
345
|
* @example
|
|
321
346
|
* ```ts
|
|
322
347
|
* import {
|
|
348
|
+
* createLogTruncator,
|
|
323
349
|
* jsonLogFormatter,
|
|
324
350
|
* parseLogLevel,
|
|
325
351
|
* prettyLogFormatter,
|
|
@@ -333,6 +359,7 @@ export class Logger {
|
|
|
333
359
|
* ? prettyLogFormatter
|
|
334
360
|
* : jsonLogFormatter,
|
|
335
361
|
* useConsoleColors: process.env.NODE_ENV === "development",
|
|
362
|
+
* truncator: createLogTruncator(),
|
|
336
363
|
* });
|
|
337
364
|
* ```
|
|
338
365
|
*/
|
package/esm/lib/log/options.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type LogFormatter } from './logFormatter.js';
|
|
2
2
|
import type { LogLevel } from './logLevel.js';
|
|
3
|
+
import type { LogTruncator } from './logTruncator.js';
|
|
3
4
|
/**
|
|
4
5
|
* Configuration options for the Logger.
|
|
5
6
|
*
|
|
@@ -26,9 +27,20 @@ export type LogOptions = {
|
|
|
26
27
|
* Defaults to false.
|
|
27
28
|
*/
|
|
28
29
|
useConsoleColors?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Optional truncator applied to each {@link LogInput} before formatting.
|
|
32
|
+
* Use {@link createLogTruncator} for the built-in implementation, or supply a custom one.
|
|
33
|
+
* Defaults to undefined (no truncation).
|
|
34
|
+
*/
|
|
35
|
+
truncator?: LogTruncator;
|
|
29
36
|
};
|
|
30
37
|
/**
|
|
31
38
|
* The default log options.
|
|
32
39
|
*/
|
|
33
|
-
export declare const defaultLogOptions:
|
|
40
|
+
export declare const defaultLogOptions: {
|
|
41
|
+
logLevel: LogLevel;
|
|
42
|
+
formatter: LogFormatter;
|
|
43
|
+
useConsoleColors: boolean;
|
|
44
|
+
truncator?: LogTruncator;
|
|
45
|
+
};
|
|
34
46
|
//# sourceMappingURL=options.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/log/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/lib/log/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IACrB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,SAAS,CAAC,EAAE,YAAY,CAAC;CAC5B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE;IAC5B,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,YAAY,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,YAAY,CAAC;CAK5B,CAAC"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context passed to {@link WithRetryOptions.shouldRetry} and
|
|
3
|
+
* {@link WithRetryOptions.onRetry} after a failed attempt.
|
|
4
|
+
*/
|
|
5
|
+
export type RetryContext = {
|
|
6
|
+
/**
|
|
7
|
+
* The error thrown by the failed attempt.
|
|
8
|
+
*/
|
|
9
|
+
error: Error;
|
|
10
|
+
/**
|
|
11
|
+
* The 1-based attempt number that failed.
|
|
12
|
+
*/
|
|
13
|
+
attempt: number;
|
|
14
|
+
/**
|
|
15
|
+
* How many retries remain after this failure (not counting the
|
|
16
|
+
* attempt that just failed).
|
|
17
|
+
*/
|
|
18
|
+
retriesLeft: number;
|
|
19
|
+
/**
|
|
20
|
+
* The delay in milliseconds before the next retry.
|
|
21
|
+
*/
|
|
22
|
+
delayMs: number;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Options for {@link withRetry}.
|
|
26
|
+
*/
|
|
27
|
+
export type WithRetryOptions = {
|
|
28
|
+
/**
|
|
29
|
+
* Maximum number of retries after the initial attempt.
|
|
30
|
+
* Total attempts = `maxRetries + 1`. Defaults to `2`.
|
|
31
|
+
*/
|
|
32
|
+
maxRetries?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Base delay in milliseconds before the first retry. Subsequent
|
|
35
|
+
* retries scale exponentially by {@link factor}. Defaults to `1000`.
|
|
36
|
+
*/
|
|
37
|
+
initialDelayMs?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Cap for the exponential backoff delay in milliseconds (applied
|
|
40
|
+
* before jitter). Defaults to `30000`.
|
|
41
|
+
*/
|
|
42
|
+
maxDelayMs?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Exponential growth factor. Defaults to `2`.
|
|
45
|
+
*/
|
|
46
|
+
factor?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Fraction of the capped base delay added as random jitter
|
|
49
|
+
* (`0` disables jitter). Defaults to `0.1` (up to 10%).
|
|
50
|
+
*/
|
|
51
|
+
jitterFactor?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Optional wall-clock budget for the entire retry sequence,
|
|
54
|
+
* measured with `performance.now()`. When exceeded, the last
|
|
55
|
+
* error is thrown without further attempts.
|
|
56
|
+
*/
|
|
57
|
+
maxRetryTimeMs?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Abort signal that cancels waiting between retries and prevents
|
|
60
|
+
* further attempts.
|
|
61
|
+
*/
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
/**
|
|
64
|
+
* Decide whether a failed attempt should be retried. Returning
|
|
65
|
+
* `false` rejects with the failure error immediately.
|
|
66
|
+
*/
|
|
67
|
+
shouldRetry?: (context: RetryContext) => boolean | Promise<boolean>;
|
|
68
|
+
/**
|
|
69
|
+
* Called after a failed attempt when another retry will occur,
|
|
70
|
+
* before waiting for {@link RetryContext.delayMs}.
|
|
71
|
+
*/
|
|
72
|
+
onRetry?: (context: RetryContext) => void | Promise<void>;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Options for {@link calculateBackoffDelay}.
|
|
76
|
+
*/
|
|
77
|
+
export type CalculateBackoffDelayOptions = {
|
|
78
|
+
/**
|
|
79
|
+
* Cap for the exponential base delay in milliseconds. Defaults to
|
|
80
|
+
* `Infinity` (no cap).
|
|
81
|
+
*/
|
|
82
|
+
maxDelayMs?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Exponential growth factor. Defaults to `2`.
|
|
85
|
+
*/
|
|
86
|
+
factor?: number;
|
|
87
|
+
/**
|
|
88
|
+
* Fraction of the capped base delay added as random jitter
|
|
89
|
+
* (`0` disables jitter). Defaults to `0.1`.
|
|
90
|
+
*/
|
|
91
|
+
jitterFactor?: number;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Error thrown from inside a {@link withRetry} operation to abort
|
|
95
|
+
* further retries immediately. The promise rejects with this error
|
|
96
|
+
* (or its cause message when constructed from another error).
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* import { RetryAbortedError, withRetry } from '@nimbus-cqrs/core';
|
|
101
|
+
*
|
|
102
|
+
* await withRetry(async () => {
|
|
103
|
+
* const response = await fetch(url);
|
|
104
|
+
* if (response.status === 404) {
|
|
105
|
+
* throw new RetryAbortedError('Resource not found');
|
|
106
|
+
* }
|
|
107
|
+
* return response.json();
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export declare class RetryAbortedError extends Error {
|
|
112
|
+
/**
|
|
113
|
+
* Optional underlying error when constructed from an `Error`.
|
|
114
|
+
*/
|
|
115
|
+
readonly cause?: Error;
|
|
116
|
+
constructor(messageOrError?: string | Error);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Calculates an exponential backoff delay with optional jitter and
|
|
120
|
+
* max-delay cap for a given retry attempt.
|
|
121
|
+
*
|
|
122
|
+
* @param initialDelayMs - Base delay in milliseconds before exponential
|
|
123
|
+
* scaling.
|
|
124
|
+
* @param attempt - Zero-based retry attempt number (`0` for the first
|
|
125
|
+
* retry after the initial failure).
|
|
126
|
+
* @param options - Optional factor, max delay, and jitter settings.
|
|
127
|
+
* @returns The backoff delay in milliseconds (integer).
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* import { calculateBackoffDelay } from '@nimbus-cqrs/core';
|
|
132
|
+
*
|
|
133
|
+
* const delayMs = calculateBackoffDelay(1000, 0, {
|
|
134
|
+
* maxDelayMs: 30_000,
|
|
135
|
+
* jitterFactor: 0.1,
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export declare const calculateBackoffDelay: (initialDelayMs: number, attempt: number, options?: CalculateBackoffDelayOptions) => number;
|
|
140
|
+
/**
|
|
141
|
+
* Retries an async (or sync) function with exponential backoff until
|
|
142
|
+
* it succeeds, retries are exhausted, or retries are aborted.
|
|
143
|
+
*
|
|
144
|
+
* On exhaustion the last error is rethrown. Throw
|
|
145
|
+
* {@link RetryAbortedError} inside `fn` to stop retrying immediately.
|
|
146
|
+
* Pass an {@link AbortSignal} to cancel waiting between attempts.
|
|
147
|
+
*
|
|
148
|
+
* @param fn - Function to execute. Receives the 1-based attempt number.
|
|
149
|
+
* @param options - Retry, backoff, and callback options.
|
|
150
|
+
* @returns The fulfilled value of `fn`.
|
|
151
|
+
* @throws The last error after retries are exhausted, a
|
|
152
|
+
* {@link RetryAbortedError}, or the abort signal reason.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* import { withRetry } from '@nimbus-cqrs/core';
|
|
157
|
+
*
|
|
158
|
+
* const data = await withRetry(
|
|
159
|
+
* async (attempt) => {
|
|
160
|
+
* console.log(`Attempt ${attempt}`);
|
|
161
|
+
* return await fetch(url).then((r) => r.json());
|
|
162
|
+
* },
|
|
163
|
+
* {
|
|
164
|
+
* maxRetries: 3,
|
|
165
|
+
* initialDelayMs: 500,
|
|
166
|
+
* maxDelayMs: 10_000,
|
|
167
|
+
* onRetry: ({ attempt, delayMs, error }) => {
|
|
168
|
+
* console.warn(`Retry ${attempt} in ${delayMs}ms`, error);
|
|
169
|
+
* },
|
|
170
|
+
* },
|
|
171
|
+
* );
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export declare const withRetry: <T>(fn: (attempt: number) => Promise<T> | T, options?: WithRetryOptions) => Promise<T>;
|
|
175
|
+
//# sourceMappingURL=withRetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withRetry.d.ts","sourceRoot":"","sources":["../../../src/lib/retry/withRetry.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACvB;;OAEG;IACH,KAAK,EAAE,KAAK,CAAC;IACb;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,CACV,OAAO,EAAE,YAAY,KACpB,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChC;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACvC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAQF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IACxC;;OAEG;IACH,SAAkB,KAAK,CAAC,EAAE,KAAK,CAAC;gBAEpB,cAAc,CAAC,EAAE,MAAM,GAAG,KAAK;CAc9C;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,qBAAqB,GAC9B,gBAAgB,MAAM,EACtB,SAAS,MAAM,EACf,UAAS,4BAAiC,KAC3C,MAcF,CAAC;AA+CF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,SAAS,GAAU,CAAC,EAC7B,IAAI,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EACvC,UAAS,gBAAqB,KAC/B,OAAO,CAAC,CAAC,CAgFX,CAAC"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
2
|
+
const DEFAULT_INITIAL_DELAY_MS = 1000;
|
|
3
|
+
const DEFAULT_MAX_DELAY_MS = 30_000;
|
|
4
|
+
const DEFAULT_FACTOR = 2;
|
|
5
|
+
const DEFAULT_JITTER_FACTOR = 0.1;
|
|
6
|
+
/**
|
|
7
|
+
* Error thrown from inside a {@link withRetry} operation to abort
|
|
8
|
+
* further retries immediately. The promise rejects with this error
|
|
9
|
+
* (or its cause message when constructed from another error).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { RetryAbortedError, withRetry } from '@nimbus-cqrs/core';
|
|
14
|
+
*
|
|
15
|
+
* await withRetry(async () => {
|
|
16
|
+
* const response = await fetch(url);
|
|
17
|
+
* if (response.status === 404) {
|
|
18
|
+
* throw new RetryAbortedError('Resource not found');
|
|
19
|
+
* }
|
|
20
|
+
* return response.json();
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class RetryAbortedError extends Error {
|
|
25
|
+
/**
|
|
26
|
+
* Optional underlying error when constructed from an `Error`.
|
|
27
|
+
*/
|
|
28
|
+
cause;
|
|
29
|
+
constructor(messageOrError) {
|
|
30
|
+
const message = messageOrError instanceof Error
|
|
31
|
+
? messageOrError.message
|
|
32
|
+
: (messageOrError ?? 'Retry aborted');
|
|
33
|
+
const cause = messageOrError instanceof Error
|
|
34
|
+
? messageOrError
|
|
35
|
+
: undefined;
|
|
36
|
+
super(message, cause ? { cause } : undefined);
|
|
37
|
+
this.name = 'RetryAbortedError';
|
|
38
|
+
if (cause) {
|
|
39
|
+
this.cause = cause;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Calculates an exponential backoff delay with optional jitter and
|
|
45
|
+
* max-delay cap for a given retry attempt.
|
|
46
|
+
*
|
|
47
|
+
* @param initialDelayMs - Base delay in milliseconds before exponential
|
|
48
|
+
* scaling.
|
|
49
|
+
* @param attempt - Zero-based retry attempt number (`0` for the first
|
|
50
|
+
* retry after the initial failure).
|
|
51
|
+
* @param options - Optional factor, max delay, and jitter settings.
|
|
52
|
+
* @returns The backoff delay in milliseconds (integer).
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { calculateBackoffDelay } from '@nimbus-cqrs/core';
|
|
57
|
+
*
|
|
58
|
+
* const delayMs = calculateBackoffDelay(1000, 0, {
|
|
59
|
+
* maxDelayMs: 30_000,
|
|
60
|
+
* jitterFactor: 0.1,
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export const calculateBackoffDelay = (initialDelayMs, attempt, options = {}) => {
|
|
65
|
+
const factor = options.factor ?? DEFAULT_FACTOR;
|
|
66
|
+
const maxDelayMs = options.maxDelayMs ?? Infinity;
|
|
67
|
+
const jitterFactor = options.jitterFactor ?? DEFAULT_JITTER_FACTOR;
|
|
68
|
+
const baseDelay = Math.min(initialDelayMs * Math.pow(factor, attempt), maxDelayMs);
|
|
69
|
+
const jitter = jitterFactor > 0
|
|
70
|
+
? Math.random() * baseDelay * jitterFactor
|
|
71
|
+
: 0;
|
|
72
|
+
return Math.floor(baseDelay + jitter);
|
|
73
|
+
};
|
|
74
|
+
const toError = (error) => error instanceof Error ? error : new Error(String(error));
|
|
75
|
+
const getAbortReason = (signal) => {
|
|
76
|
+
if (signal.reason instanceof Error) {
|
|
77
|
+
return signal.reason;
|
|
78
|
+
}
|
|
79
|
+
if (typeof signal.reason === 'string' && signal.reason.length > 0) {
|
|
80
|
+
return new Error(signal.reason);
|
|
81
|
+
}
|
|
82
|
+
return new DOMException('The operation was aborted.', 'AbortError');
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Returns a promise that resolves after `ms` milliseconds, or rejects
|
|
86
|
+
* early when {@link signal} is aborted.
|
|
87
|
+
*/
|
|
88
|
+
const delay = (ms, signal) => {
|
|
89
|
+
if (ms <= 0) {
|
|
90
|
+
if (signal?.aborted) {
|
|
91
|
+
return Promise.reject(getAbortReason(signal));
|
|
92
|
+
}
|
|
93
|
+
return Promise.resolve();
|
|
94
|
+
}
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
if (signal?.aborted) {
|
|
97
|
+
reject(getAbortReason(signal));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
signal?.removeEventListener('abort', onAbort);
|
|
102
|
+
resolve();
|
|
103
|
+
}, ms);
|
|
104
|
+
const onAbort = () => {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
reject(getAbortReason(signal));
|
|
107
|
+
};
|
|
108
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Retries an async (or sync) function with exponential backoff until
|
|
113
|
+
* it succeeds, retries are exhausted, or retries are aborted.
|
|
114
|
+
*
|
|
115
|
+
* On exhaustion the last error is rethrown. Throw
|
|
116
|
+
* {@link RetryAbortedError} inside `fn` to stop retrying immediately.
|
|
117
|
+
* Pass an {@link AbortSignal} to cancel waiting between attempts.
|
|
118
|
+
*
|
|
119
|
+
* @param fn - Function to execute. Receives the 1-based attempt number.
|
|
120
|
+
* @param options - Retry, backoff, and callback options.
|
|
121
|
+
* @returns The fulfilled value of `fn`.
|
|
122
|
+
* @throws The last error after retries are exhausted, a
|
|
123
|
+
* {@link RetryAbortedError}, or the abort signal reason.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* import { withRetry } from '@nimbus-cqrs/core';
|
|
128
|
+
*
|
|
129
|
+
* const data = await withRetry(
|
|
130
|
+
* async (attempt) => {
|
|
131
|
+
* console.log(`Attempt ${attempt}`);
|
|
132
|
+
* return await fetch(url).then((r) => r.json());
|
|
133
|
+
* },
|
|
134
|
+
* {
|
|
135
|
+
* maxRetries: 3,
|
|
136
|
+
* initialDelayMs: 500,
|
|
137
|
+
* maxDelayMs: 10_000,
|
|
138
|
+
* onRetry: ({ attempt, delayMs, error }) => {
|
|
139
|
+
* console.warn(`Retry ${attempt} in ${delayMs}ms`, error);
|
|
140
|
+
* },
|
|
141
|
+
* },
|
|
142
|
+
* );
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export const withRetry = async (fn, options = {}) => {
|
|
146
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
147
|
+
const initialDelayMs = options.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
|
|
148
|
+
const maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
149
|
+
const factor = options.factor ?? DEFAULT_FACTOR;
|
|
150
|
+
const jitterFactor = options.jitterFactor ?? DEFAULT_JITTER_FACTOR;
|
|
151
|
+
const maxRetryTimeMs = options.maxRetryTimeMs;
|
|
152
|
+
const signal = options.signal;
|
|
153
|
+
const startedAt = performance.now();
|
|
154
|
+
let attempt = 0;
|
|
155
|
+
while (true) {
|
|
156
|
+
if (signal?.aborted) {
|
|
157
|
+
throw getAbortReason(signal);
|
|
158
|
+
}
|
|
159
|
+
attempt++;
|
|
160
|
+
try {
|
|
161
|
+
return await fn(attempt);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (error instanceof RetryAbortedError) {
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
const err = toError(error);
|
|
168
|
+
const retriesLeft = maxRetries + 1 - attempt;
|
|
169
|
+
if (retriesLeft <= 0) {
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
if (maxRetryTimeMs !== undefined &&
|
|
173
|
+
performance.now() - startedAt >= maxRetryTimeMs) {
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
let delayMs = calculateBackoffDelay(initialDelayMs, attempt - 1, { maxDelayMs, factor, jitterFactor });
|
|
177
|
+
if (maxRetryTimeMs !== undefined) {
|
|
178
|
+
const remainingMs = maxRetryTimeMs -
|
|
179
|
+
(performance.now() - startedAt);
|
|
180
|
+
// floor() can turn a sub-millisecond remainder into 0.
|
|
181
|
+
// delay(0) is a no-op, so sync failures would otherwise
|
|
182
|
+
// burn through every remaining retry without ever
|
|
183
|
+
// advancing past maxRetryTimeMs.
|
|
184
|
+
if (remainingMs < 1) {
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
delayMs = Math.min(delayMs, Math.floor(remainingMs));
|
|
188
|
+
}
|
|
189
|
+
const context = {
|
|
190
|
+
error: err,
|
|
191
|
+
attempt,
|
|
192
|
+
retriesLeft,
|
|
193
|
+
delayMs,
|
|
194
|
+
};
|
|
195
|
+
if (options.shouldRetry) {
|
|
196
|
+
const shouldRetry = await options.shouldRetry(context);
|
|
197
|
+
if (!shouldRetry) {
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (options.onRetry) {
|
|
202
|
+
await options.onRetry(context);
|
|
203
|
+
}
|
|
204
|
+
await delay(delayMs, signal);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|