@nimbus-cqrs/core 2.1.2 → 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.
Files changed (40) hide show
  1. package/esm/_dnt.polyfills.d.ts +7 -0
  2. package/esm/_dnt.polyfills.d.ts.map +1 -0
  3. package/esm/_dnt.polyfills.js +1 -0
  4. package/esm/index.d.ts +4 -0
  5. package/esm/index.d.ts.map +1 -1
  6. package/esm/index.js +4 -0
  7. package/esm/lib/env/getEnv.d.ts +15 -0
  8. package/esm/lib/env/getEnv.d.ts.map +1 -0
  9. package/esm/lib/env/getEnv.js +35 -0
  10. package/esm/lib/eventBus/eventBus.d.ts +0 -1
  11. package/esm/lib/eventBus/eventBus.d.ts.map +1 -1
  12. package/esm/lib/eventBus/eventBus.js +31 -32
  13. package/esm/lib/exception/exception.d.ts +1 -1
  14. package/esm/lib/exception/exception.d.ts.map +1 -1
  15. package/esm/lib/exception/invalidInputException.d.ts +1 -1
  16. package/esm/lib/exception/invalidInputException.d.ts.map +1 -1
  17. package/esm/lib/log/logTruncator.d.ts +96 -0
  18. package/esm/lib/log/logTruncator.d.ts.map +1 -0
  19. package/esm/lib/log/logTruncator.js +254 -0
  20. package/esm/lib/log/logger.d.ts +20 -0
  21. package/esm/lib/log/logger.d.ts.map +1 -1
  22. package/esm/lib/log/logger.js +28 -1
  23. package/esm/lib/log/options.d.ts +13 -1
  24. package/esm/lib/log/options.d.ts.map +1 -1
  25. package/esm/lib/message/command.d.ts +11 -1
  26. package/esm/lib/message/command.d.ts.map +1 -1
  27. package/esm/lib/message/command.js +17 -10
  28. package/esm/lib/message/event.d.ts +11 -1
  29. package/esm/lib/message/event.d.ts.map +1 -1
  30. package/esm/lib/message/event.js +17 -10
  31. package/esm/lib/message/message.d.ts +14 -0
  32. package/esm/lib/message/message.d.ts.map +1 -1
  33. package/esm/lib/message/message.js +47 -1
  34. package/esm/lib/message/query.d.ts +11 -1
  35. package/esm/lib/message/query.d.ts.map +1 -1
  36. package/esm/lib/message/query.js +16 -9
  37. package/esm/lib/retry/withRetry.d.ts +175 -0
  38. package/esm/lib/retry/withRetry.d.ts.map +1 -0
  39. package/esm/lib/retry/withRetry.js +207 -0
  40. package/package.json +2 -2
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbus-cqrs/core",
3
- "version": "2.1.2",
3
+ "version": "2.3.0",
4
4
  "description": "Simplify Event-Driven Applications - Core building blocks of the Nimbus framework.",
5
5
  "keywords": [
6
6
  "nimbus",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@opentelemetry/api": "^1.9.1",
37
- "zod": "^4.3.6"
37
+ "zod": "^4.4.3"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.0.0"