@zap-studio/retry 0.3.1 → 1.0.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/CHANGELOG.md +55 -14
- package/README.md +87 -141
- package/dist/base-policy.d.ts +67 -0
- package/dist/base-policy.d.ts.map +1 -0
- package/dist/base-policy.js +306 -0
- package/dist/base-policy.js.map +1 -0
- package/dist/{errors-BVZjP1Q5.d.mts → errors-CS5UPJWs.d.ts} +25 -1
- package/dist/errors-CS5UPJWs.d.ts.map +1 -0
- package/dist/{errors.d.mts → errors.d.ts} +1 -1
- package/dist/{errors.mjs → errors.js} +19 -1
- package/dist/errors.js.map +1 -0
- package/dist/exponential-backoff.d.ts +40 -0
- package/dist/exponential-backoff.d.ts.map +1 -0
- package/dist/exponential-backoff.js +35 -0
- package/dist/exponential-backoff.js.map +1 -0
- package/dist/fixed-delay.d.ts +31 -0
- package/dist/fixed-delay.d.ts.map +1 -0
- package/dist/fixed-delay.js +33 -0
- package/dist/fixed-delay.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/linear-backoff.d.ts +46 -0
- package/dist/linear-backoff.d.ts.map +1 -0
- package/dist/linear-backoff.js +35 -0
- package/dist/linear-backoff.js.map +1 -0
- package/dist/{types.d.mts → types.d.ts} +52 -8
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/package.json +12 -22
- package/dist/abort.d.mts +0 -29
- package/dist/abort.d.mts.map +0 -1
- package/dist/abort.mjs +0 -61
- package/dist/abort.mjs.map +0 -1
- package/dist/errors-BVZjP1Q5.d.mts.map +0 -1
- package/dist/errors.mjs.map +0 -1
- package/dist/exponential-backoff.d.mts +0 -55
- package/dist/exponential-backoff.d.mts.map +0 -1
- package/dist/exponential-backoff.mjs +0 -60
- package/dist/exponential-backoff.mjs.map +0 -1
- package/dist/fixed-delay.d.mts +0 -46
- package/dist/fixed-delay.d.mts.map +0 -1
- package/dist/fixed-delay.mjs +0 -53
- package/dist/fixed-delay.mjs.map +0 -1
- package/dist/index.d.mts +0 -59
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -63
- package/dist/index.mjs.map +0 -1
- package/dist/result-mode.d.mts +0 -19
- package/dist/result-mode.d.mts.map +0 -1
- package/dist/result-mode.mjs +0 -145
- package/dist/result-mode.mjs.map +0 -1
- package/dist/sleep.d.mts +0 -17
- package/dist/sleep.d.mts.map +0 -1
- package/dist/sleep.mjs +0 -23
- package/dist/sleep.mjs.map +0 -1
- package/dist/throw-mode.d.mts +0 -21
- package/dist/throw-mode.d.mts.map +0 -1
- package/dist/throw-mode.mjs +0 -49
- package/dist/throw-mode.mjs.map +0 -1
- package/dist/types.d.mts.map +0 -1
- package/dist/types.mjs +0 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { AbortError, RetryError } from "./errors.js";
|
|
2
|
+
//#region src/base-policy.ts
|
|
3
|
+
/**
|
|
4
|
+
* Retry runner base class and shared orchestration implementation.
|
|
5
|
+
*
|
|
6
|
+
* @module @zap-studio/retry/base-policy
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Awaits a timer-based delay, unless `delayMs` is non-positive.
|
|
10
|
+
*
|
|
11
|
+
* @param delayMs - Milliseconds to wait before resolving.
|
|
12
|
+
* @returns Promise that resolves when the delay completes.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { defaultSleep } from "@zap-studio/retry";
|
|
17
|
+
*
|
|
18
|
+
* await defaultSleep(250); // waits 250ms
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
const defaultSleep = async (delayMs) => {
|
|
22
|
+
if (delayMs <= 0) return;
|
|
23
|
+
await new Promise((resolve) => {
|
|
24
|
+
setTimeout(resolve, delayMs);
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Normalizes an abort `reason` into an `AbortError`.
|
|
29
|
+
*/
|
|
30
|
+
const toAbortError = (reason) => {
|
|
31
|
+
if (reason instanceof AbortError) return reason;
|
|
32
|
+
if (reason instanceof Error) return new AbortError(reason.message, { cause: reason });
|
|
33
|
+
if (typeof reason === "string" && reason.length > 0) return new AbortError(reason);
|
|
34
|
+
if (reason === void 0) return new AbortError("Retry aborted.");
|
|
35
|
+
try {
|
|
36
|
+
return new AbortError(`Retry aborted: ${JSON.stringify(reason)}`);
|
|
37
|
+
} catch {
|
|
38
|
+
return new AbortError("Retry aborted.");
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Throws when the provided abort signal is already aborted.
|
|
43
|
+
*
|
|
44
|
+
* @param signal - Optional abort signal to inspect.
|
|
45
|
+
* @throws {AbortError} When the signal is aborted.
|
|
46
|
+
*/
|
|
47
|
+
const throwIfAborted = (signal) => {
|
|
48
|
+
if (signal?.aborted !== true) return;
|
|
49
|
+
throw toAbortError(signal.reason);
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Waits for delay sleep while observing cancellation through an abort signal.
|
|
53
|
+
*
|
|
54
|
+
* @param sleep - Sleep function used to await `delayMs`.
|
|
55
|
+
* @param delayMs - Delay duration in milliseconds.
|
|
56
|
+
* @param signal - Abort signal to observe while waiting.
|
|
57
|
+
* @returns Promise that resolves when delay finishes.
|
|
58
|
+
* @throws {AbortError} When the signal aborts before or during wait.
|
|
59
|
+
*/
|
|
60
|
+
const sleepWithAbortSignal = async (sleep, delayMs, signal) => {
|
|
61
|
+
if (signal.aborted) throw toAbortError(signal.reason);
|
|
62
|
+
let onAbort;
|
|
63
|
+
try {
|
|
64
|
+
await Promise.race([sleep(delayMs), new Promise((_resolve, reject) => {
|
|
65
|
+
onAbort = () => {
|
|
66
|
+
reject(toAbortError(signal.reason));
|
|
67
|
+
};
|
|
68
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
69
|
+
})]);
|
|
70
|
+
} finally {
|
|
71
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Runs the throw-mode retry loop: throws `RetryError` on exhaustion and
|
|
76
|
+
* `AbortError` when `signal` aborts.
|
|
77
|
+
*
|
|
78
|
+
* @param policy - Resolved retry policy providing `next` and `onExhausted`.
|
|
79
|
+
* @param execute - Async work callback per attempt.
|
|
80
|
+
* @param sleep - Delay function between retries.
|
|
81
|
+
* @param signal - Optional cancel signal.
|
|
82
|
+
* @returns Resolves to the first successful return value.
|
|
83
|
+
* @throws {RetryError} When retries are exhausted and `onExhausted` returns
|
|
84
|
+
* the terminal error.
|
|
85
|
+
* @throws {AbortError} When `signal` is already aborted or aborts while waiting.
|
|
86
|
+
* @throws {Error} Any error thrown by `next`, `onExhausted`, or `sleep`. Also
|
|
87
|
+
* rethrows the original caught value immediately, bypassing retry, when
|
|
88
|
+
* `policy.isKnownError` rejects it as outside this policy's error domain.
|
|
89
|
+
*/
|
|
90
|
+
const runThrowMode = async (policy, execute, sleep, signal) => {
|
|
91
|
+
let attempt = 1;
|
|
92
|
+
while (true) {
|
|
93
|
+
throwIfAborted(signal);
|
|
94
|
+
try {
|
|
95
|
+
return await execute(attempt);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
throwIfAborted(signal);
|
|
98
|
+
if (!policy.isKnownError(error)) throw error;
|
|
99
|
+
const decision = policy.next({
|
|
100
|
+
attempt,
|
|
101
|
+
error
|
|
102
|
+
});
|
|
103
|
+
if (!decision.shouldRetry) throw policy.onExhausted({
|
|
104
|
+
attempts: attempt,
|
|
105
|
+
error
|
|
106
|
+
});
|
|
107
|
+
if (decision.delayMs > 0) await (signal === void 0 ? sleep(decision.delayMs) : sleepWithAbortSignal(sleep, decision.delayMs, signal));
|
|
108
|
+
attempt += 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* When `signal` is already aborted, builds the terminal `{ ok: false }` object
|
|
114
|
+
* with a normalized `AbortError` on `error`.
|
|
115
|
+
*
|
|
116
|
+
* @param signal - Optional abort signal; only acts when `aborted` is set.
|
|
117
|
+
* @param attempts - Number of finished attempts to report in the result.
|
|
118
|
+
* @returns Failure result or `undefined` if not aborted.
|
|
119
|
+
*/
|
|
120
|
+
const buildAbortResult = (signal, attempts) => {
|
|
121
|
+
if (signal?.aborted !== true) return;
|
|
122
|
+
return {
|
|
123
|
+
attempts,
|
|
124
|
+
error: toAbortError(signal.reason),
|
|
125
|
+
ok: false
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Runs one `execute(attempt)` call and returns either a success value or a
|
|
130
|
+
* captured error without rethrowing.
|
|
131
|
+
*
|
|
132
|
+
* @param execute - User work callback.
|
|
133
|
+
* @param attempt - One-based attempt number passed to `execute`.
|
|
134
|
+
* @returns A tagged success with `value` or a tagged failure with `error`.
|
|
135
|
+
*/
|
|
136
|
+
const runAttempt = async (execute, attempt) => {
|
|
137
|
+
try {
|
|
138
|
+
return {
|
|
139
|
+
ok: true,
|
|
140
|
+
value: await execute(attempt)
|
|
141
|
+
};
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return {
|
|
144
|
+
error,
|
|
145
|
+
ok: false
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Awaits inter-attempt delay in result mode, mapping an abort during wait to
|
|
151
|
+
* a terminal result instead of throwing when `throwOnExhausted` is false.
|
|
152
|
+
*
|
|
153
|
+
* @param sleep - Custom or default sleep implementation.
|
|
154
|
+
* @param delayMs - Milliseconds to wait.
|
|
155
|
+
* @param signal - If set, `sleep` is raced with the abort signal.
|
|
156
|
+
* @param attempts - Attempt count to attach if the wait ends in abort.
|
|
157
|
+
* @returns A terminal result when canceled during the wait, otherwise
|
|
158
|
+
* `undefined`.
|
|
159
|
+
* @throws {Error} The underlying `sleep` rejection when it is not an abort.
|
|
160
|
+
*/
|
|
161
|
+
const waitForDelay = async (sleep, delayMs, signal, attempts) => {
|
|
162
|
+
if (signal === void 0) {
|
|
163
|
+
await sleep(delayMs);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
await sleepWithAbortSignal(sleep, delayMs, signal);
|
|
168
|
+
return;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const aborted = buildAbortResult(signal, attempts);
|
|
171
|
+
if (aborted !== void 0) return aborted;
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* After a failed attempt, applies abort rules, `next`, optional delay, and
|
|
177
|
+
* either returns a terminal `RetryRunResult` or `undefined` to continue.
|
|
178
|
+
*
|
|
179
|
+
* @param policy - Resolved retry policy hooks (`next`, `onExhausted`).
|
|
180
|
+
* @param params - Failure context for the current attempt.
|
|
181
|
+
* @param params.attempt - Current attempt number.
|
|
182
|
+
* @param params.error - Error thrown by the attempt.
|
|
183
|
+
* @param params.sleep - Delay function between retries.
|
|
184
|
+
* @param params.signal - Optional abort signal.
|
|
185
|
+
* @returns Terminal non-throw result if the loop should stop, otherwise
|
|
186
|
+
* `undefined` to schedule another attempt.
|
|
187
|
+
* @throws {Error} Any error thrown by `next`, `onExhausted`, or a custom `sleep` when
|
|
188
|
+
* the error is not an abort.
|
|
189
|
+
*/
|
|
190
|
+
const handleFailure = async (policy, params) => {
|
|
191
|
+
const { attempt, error, sleep, signal } = params;
|
|
192
|
+
const abortResult = buildAbortResult(signal, attempt);
|
|
193
|
+
if (abortResult !== void 0) return abortResult;
|
|
194
|
+
const decision = policy.next({
|
|
195
|
+
attempt,
|
|
196
|
+
error
|
|
197
|
+
});
|
|
198
|
+
if (!decision.shouldRetry) return {
|
|
199
|
+
attempts: attempt,
|
|
200
|
+
error: policy.onExhausted({
|
|
201
|
+
attempts: attempt,
|
|
202
|
+
error
|
|
203
|
+
}),
|
|
204
|
+
ok: false
|
|
205
|
+
};
|
|
206
|
+
if (decision.delayMs > 0) {
|
|
207
|
+
const delayAbortResult = await waitForDelay(sleep, decision.delayMs, signal, attempt);
|
|
208
|
+
if (delayAbortResult !== void 0) return delayAbortResult;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Runs the non-throw retry loop, returning
|
|
213
|
+
* `RetryRunResult`.
|
|
214
|
+
*
|
|
215
|
+
* @param policy - Resolved retry policy providing `next` and `onExhausted`.
|
|
216
|
+
* @param execute - Async work callback per attempt.
|
|
217
|
+
* @param sleep - Delay function between retries.
|
|
218
|
+
* @param signal - Optional cancel signal.
|
|
219
|
+
* @returns Terminal success or failure object. When `policy.isKnownError`
|
|
220
|
+
* rejects a caught value as outside this policy's error domain, the
|
|
221
|
+
* original value is wrapped in a `RetryError` and returned on
|
|
222
|
+
* `result.error` immediately, bypassing retry — never thrown.
|
|
223
|
+
* @throws {Error} Any error thrown by `next`, `onExhausted`, or a non-abort `sleep`
|
|
224
|
+
* failure.
|
|
225
|
+
*/
|
|
226
|
+
const runResultMode = async (policy, execute, sleep, signal) => {
|
|
227
|
+
let attempt = 1;
|
|
228
|
+
while (true) {
|
|
229
|
+
const abortResult = buildAbortResult(signal, Math.max(0, attempt - 1));
|
|
230
|
+
if (abortResult !== void 0) return abortResult;
|
|
231
|
+
const execution = await runAttempt(execute, attempt);
|
|
232
|
+
if (execution.ok) return {
|
|
233
|
+
ok: true,
|
|
234
|
+
value: execution.value
|
|
235
|
+
};
|
|
236
|
+
const attemptAbortResult = buildAbortResult(signal, attempt);
|
|
237
|
+
if (attemptAbortResult !== void 0) return attemptAbortResult;
|
|
238
|
+
if (!policy.isKnownError(execution.error)) return {
|
|
239
|
+
attempts: attempt,
|
|
240
|
+
error: new RetryError("Retry policy encountered an unknown error.", {
|
|
241
|
+
attempts: attempt,
|
|
242
|
+
lastError: execution.error
|
|
243
|
+
}),
|
|
244
|
+
ok: false
|
|
245
|
+
};
|
|
246
|
+
const failure = await handleFailure(policy, {
|
|
247
|
+
attempt,
|
|
248
|
+
error: execution.error,
|
|
249
|
+
signal,
|
|
250
|
+
sleep
|
|
251
|
+
});
|
|
252
|
+
if (failure !== void 0) return failure;
|
|
253
|
+
attempt += 1;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* Default `onExhausted` used when a policy omits it: wraps the exhaustion
|
|
258
|
+
* context in a generic `RetryError`.
|
|
259
|
+
*/
|
|
260
|
+
const defaultOnExhausted = (input) => new RetryError("Retry policy exhausted all attempts.", {
|
|
261
|
+
attempts: input.attempts,
|
|
262
|
+
lastData: input.data,
|
|
263
|
+
lastError: input.error
|
|
264
|
+
});
|
|
265
|
+
/**
|
|
266
|
+
* Default `isKnownError` used when a policy omits it: accepts any `Error`
|
|
267
|
+
* instance and rejects everything else.
|
|
268
|
+
*/
|
|
269
|
+
const defaultIsKnownError = (error) => error instanceof Error;
|
|
270
|
+
/**
|
|
271
|
+
* Runs retry orchestration in non-throw mode.
|
|
272
|
+
*
|
|
273
|
+
* When `throwOnExhausted` is `false`, returns a discriminated result union.
|
|
274
|
+
*
|
|
275
|
+
* @param policy - Retry policy: `next` is required, `onExhausted` and
|
|
276
|
+
* `isKnownError` fall back to their defaults when omitted.
|
|
277
|
+
* @param execute - Async function to execute per attempt.
|
|
278
|
+
* @param options - Runner settings.
|
|
279
|
+
* @returns Success value or terminal result object based on option mode.
|
|
280
|
+
* @throws {Error} Any error thrown by `next`, by `onExhausted`, or by a custom `sleep`
|
|
281
|
+
* function. When `throwOnExhausted` is `false`, exhaustion itself is returned
|
|
282
|
+
* as `{ ok: false }` instead of thrown.
|
|
283
|
+
* Cancellation is returned as `{ ok: false, error: AbortError }` in non-throw
|
|
284
|
+
* mode. A value rejected by `policy.isKnownError` is wrapped in a
|
|
285
|
+
* `RetryError` and returned the same way in non-throw mode; in throw mode
|
|
286
|
+
* it is rethrown as-is.
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* const result = await runRetryPolicy(policy, doWork, { throwOnExhausted: false });
|
|
290
|
+
* if (!result.ok) console.error(result.error);
|
|
291
|
+
*/
|
|
292
|
+
async function runRetryPolicy(policy, execute, options = {}) {
|
|
293
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
294
|
+
const { signal } = options;
|
|
295
|
+
const resolvedPolicy = {
|
|
296
|
+
isKnownError: (error) => policy.isKnownError ? policy.isKnownError(error) : defaultIsKnownError(error),
|
|
297
|
+
next: (input) => policy.next(input),
|
|
298
|
+
onExhausted: (input) => policy.onExhausted ? policy.onExhausted(input) : defaultOnExhausted(input)
|
|
299
|
+
};
|
|
300
|
+
if (options.throwOnExhausted === false) return await runResultMode(resolvedPolicy, execute, sleep, signal);
|
|
301
|
+
return await runThrowMode(resolvedPolicy, execute, sleep, signal);
|
|
302
|
+
}
|
|
303
|
+
//#endregion
|
|
304
|
+
export { defaultSleep, runRetryPolicy };
|
|
305
|
+
|
|
306
|
+
//# sourceMappingURL=base-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-policy.js","names":[],"sources":["../src/base-policy.ts"],"sourcesContent":["/**\n * Retry runner base class and shared orchestration implementation.\n *\n * @module @zap-studio/retry/base-policy\n */\n\nimport { AbortError, RetryError } from \"./errors.js\";\nimport type {\n ResolvedRetryPolicy,\n RetryExhaustedInput,\n RetryPolicy,\n RetryRunOptions,\n RetryRunResult,\n} from \"./types.js\";\n\n/**\n * Awaits a timer-based delay, unless `delayMs` is non-positive.\n *\n * @param delayMs - Milliseconds to wait before resolving.\n * @returns Promise that resolves when the delay completes.\n *\n * @example\n * ```ts\n * import { defaultSleep } from \"@zap-studio/retry\";\n *\n * await defaultSleep(250); // waits 250ms\n * ```\n */\nexport const defaultSleep = async (delayMs: number): Promise<void> => {\n if (delayMs <= 0) {\n return;\n }\n\n // oxlint-disable-next-line promise/avoid-new -- Timer sleep requires adapting callback API to a promise.\n await new Promise<void>((resolve) => {\n setTimeout(resolve, delayMs);\n });\n};\n\n/**\n * Normalizes an abort `reason` into an `AbortError`.\n */\nconst toAbortError = (reason: unknown): AbortError => {\n if (reason instanceof AbortError) {\n return reason;\n }\n\n if (reason instanceof Error) {\n return new AbortError(reason.message, { cause: reason });\n }\n\n if (typeof reason === \"string\" && reason.length > 0) {\n return new AbortError(reason);\n }\n\n if (reason === undefined) {\n return new AbortError(\"Retry aborted.\");\n }\n\n try {\n return new AbortError(`Retry aborted: ${JSON.stringify(reason)}`);\n } catch {\n return new AbortError(\"Retry aborted.\");\n }\n};\n\n/**\n * Throws when the provided abort signal is already aborted.\n *\n * @param signal - Optional abort signal to inspect.\n * @throws {AbortError} When the signal is aborted.\n */\nconst throwIfAborted = (signal?: AbortSignal): void => {\n if (signal?.aborted !== true) {\n return;\n }\n\n throw toAbortError(signal.reason);\n};\n\n/**\n * Waits for delay sleep while observing cancellation through an abort signal.\n *\n * @param sleep - Sleep function used to await `delayMs`.\n * @param delayMs - Delay duration in milliseconds.\n * @param signal - Abort signal to observe while waiting.\n * @returns Promise that resolves when delay finishes.\n * @throws {AbortError} When the signal aborts before or during wait.\n */\nconst sleepWithAbortSignal = async (\n sleep: (delayMs: number) => Promise<void>,\n delayMs: number,\n signal: AbortSignal\n): Promise<void> => {\n if (signal.aborted) {\n throw toAbortError(signal.reason);\n }\n\n let onAbort: (() => void) | undefined;\n\n try {\n await Promise.race([\n sleep(delayMs),\n // oxlint-disable-next-line promise/avoid-new -- AbortSignal callback is adapted into the race promise.\n new Promise<never>((_resolve, reject) => {\n onAbort = (): void => {\n reject(toAbortError(signal.reason));\n };\n\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }),\n ]);\n } finally {\n if (onAbort) {\n signal.removeEventListener(\"abort\", onAbort);\n }\n }\n};\n\n/**\n * Runs the throw-mode retry loop: throws `RetryError` on exhaustion and\n * `AbortError` when `signal` aborts.\n *\n * @param policy - Resolved retry policy providing `next` and `onExhausted`.\n * @param execute - Async work callback per attempt.\n * @param sleep - Delay function between retries.\n * @param signal - Optional cancel signal.\n * @returns Resolves to the first successful return value.\n * @throws {RetryError} When retries are exhausted and `onExhausted` returns\n * the terminal error.\n * @throws {AbortError} When `signal` is already aborted or aborts while waiting.\n * @throws {Error} Any error thrown by `next`, `onExhausted`, or `sleep`. Also\n * rethrows the original caught value immediately, bypassing retry, when\n * `policy.isKnownError` rejects it as outside this policy's error domain.\n */\nconst runThrowMode = async <T, TError extends Error, TData>(\n policy: ResolvedRetryPolicy<TError, TData>,\n execute: (attempt: number) => Promise<T>,\n sleep: (delayMs: number) => Promise<void>,\n signal?: AbortSignal\n): Promise<T> => {\n let attempt = 1;\n\n while (true) {\n throwIfAborted(signal);\n\n try {\n // oxlint-disable-next-line no-await-in-loop -- Retry attempts must run sequentially.\n return await execute(attempt);\n } catch (error) {\n throwIfAborted(signal);\n\n if (!policy.isKnownError(error)) {\n throw error;\n }\n\n const decision = policy.next({\n attempt,\n error,\n });\n\n if (!decision.shouldRetry) {\n throw policy.onExhausted({\n attempts: attempt,\n error,\n });\n }\n\n if (decision.delayMs > 0) {\n // oxlint-disable-next-line no-await-in-loop -- Delay belongs between sequential retry attempts.\n await (signal === undefined\n ? sleep(decision.delayMs)\n : sleepWithAbortSignal(sleep, decision.delayMs, signal));\n }\n\n attempt += 1;\n }\n }\n};\n\n/**\n * When `signal` is already aborted, builds the terminal `{ ok: false }` object\n * with a normalized `AbortError` on `error`.\n *\n * @param signal - Optional abort signal; only acts when `aborted` is set.\n * @param attempts - Number of finished attempts to report in the result.\n * @returns Failure result or `undefined` if not aborted.\n */\nconst buildAbortResult = (\n signal: AbortSignal | undefined,\n attempts: number\n): RetryRunResult<never> | undefined => {\n if (signal?.aborted !== true) {\n return undefined;\n }\n\n return {\n attempts,\n error: toAbortError(signal.reason),\n ok: false,\n };\n};\n\n/**\n * Runs one `execute(attempt)` call and returns either a success value or a\n * captured error without rethrowing.\n *\n * @param execute - User work callback.\n * @param attempt - One-based attempt number passed to `execute`.\n * @returns A tagged success with `value` or a tagged failure with `error`.\n */\nconst runAttempt = async <T>(\n execute: (attempt: number) => Promise<T>,\n attempt: number\n): Promise<{ ok: true; value: T } | { ok: false; error: unknown }> => {\n try {\n return {\n ok: true,\n value: await execute(attempt),\n };\n } catch (error) {\n return {\n error,\n ok: false,\n };\n }\n};\n\n/**\n * Awaits inter-attempt delay in result mode, mapping an abort during wait to\n * a terminal result instead of throwing when `throwOnExhausted` is false.\n *\n * @param sleep - Custom or default sleep implementation.\n * @param delayMs - Milliseconds to wait.\n * @param signal - If set, `sleep` is raced with the abort signal.\n * @param attempts - Attempt count to attach if the wait ends in abort.\n * @returns A terminal result when canceled during the wait, otherwise\n * `undefined`.\n * @throws {Error} The underlying `sleep` rejection when it is not an abort.\n */\nconst waitForDelay = async (\n sleep: (delayMs: number) => Promise<void>,\n delayMs: number,\n signal: AbortSignal | undefined,\n attempts: number\n): Promise<RetryRunResult<never> | undefined> => {\n if (signal === undefined) {\n await sleep(delayMs);\n return undefined;\n }\n\n try {\n await sleepWithAbortSignal(sleep, delayMs, signal);\n return undefined;\n } catch (error) {\n const aborted = buildAbortResult(signal, attempts);\n if (aborted !== undefined) {\n return aborted;\n }\n throw error;\n }\n};\n\n/**\n * After a failed attempt, applies abort rules, `next`, optional delay, and\n * either returns a terminal `RetryRunResult` or `undefined` to continue.\n *\n * @param policy - Resolved retry policy hooks (`next`, `onExhausted`).\n * @param params - Failure context for the current attempt.\n * @param params.attempt - Current attempt number.\n * @param params.error - Error thrown by the attempt.\n * @param params.sleep - Delay function between retries.\n * @param params.signal - Optional abort signal.\n * @returns Terminal non-throw result if the loop should stop, otherwise\n * `undefined` to schedule another attempt.\n * @throws {Error} Any error thrown by `next`, `onExhausted`, or a custom `sleep` when\n * the error is not an abort.\n */\nconst handleFailure = async <TError extends Error, TData>(\n policy: ResolvedRetryPolicy<TError, TData>,\n params: {\n attempt: number;\n error: TError;\n sleep: (delayMs: number) => Promise<void>;\n signal: AbortSignal | undefined;\n }\n): Promise<RetryRunResult<never> | undefined> => {\n const { attempt, error, sleep, signal } = params;\n const abortResult = buildAbortResult(signal, attempt);\n if (abortResult !== undefined) {\n return abortResult;\n }\n\n const decision = policy.next({\n attempt,\n error,\n });\n\n if (!decision.shouldRetry) {\n const terminalError = policy.onExhausted({\n attempts: attempt,\n error,\n });\n\n return {\n attempts: attempt,\n error: terminalError,\n ok: false,\n };\n }\n\n if (decision.delayMs > 0) {\n const delayAbortResult = await waitForDelay(\n sleep,\n decision.delayMs,\n signal,\n attempt\n );\n if (delayAbortResult !== undefined) {\n return delayAbortResult;\n }\n }\n\n return undefined;\n};\n\n/**\n * Runs the non-throw retry loop, returning\n * `RetryRunResult`.\n *\n * @param policy - Resolved retry policy providing `next` and `onExhausted`.\n * @param execute - Async work callback per attempt.\n * @param sleep - Delay function between retries.\n * @param signal - Optional cancel signal.\n * @returns Terminal success or failure object. When `policy.isKnownError`\n * rejects a caught value as outside this policy's error domain, the\n * original value is wrapped in a `RetryError` and returned on\n * `result.error` immediately, bypassing retry — never thrown.\n * @throws {Error} Any error thrown by `next`, `onExhausted`, or a non-abort `sleep`\n * failure.\n */\nconst runResultMode = async <T, TError extends Error, TData>(\n policy: ResolvedRetryPolicy<TError, TData>,\n execute: (attempt: number) => Promise<T>,\n sleep: (delayMs: number) => Promise<void>,\n signal?: AbortSignal\n): Promise<RetryRunResult<T>> => {\n let attempt = 1;\n\n while (true) {\n const abortResult = buildAbortResult(signal, Math.max(0, attempt - 1));\n if (abortResult !== undefined) {\n return abortResult;\n }\n\n // oxlint-disable-next-line no-await-in-loop -- Retry attempts must run sequentially.\n const execution = await runAttempt(execute, attempt);\n if (execution.ok) {\n return { ok: true, value: execution.value };\n }\n\n const attemptAbortResult = buildAbortResult(signal, attempt);\n if (attemptAbortResult !== undefined) {\n return attemptAbortResult;\n }\n\n if (!policy.isKnownError(execution.error)) {\n return {\n attempts: attempt,\n error: new RetryError(\"Retry policy encountered an unknown error.\", {\n attempts: attempt,\n lastError: execution.error,\n }),\n ok: false,\n };\n }\n\n // oxlint-disable-next-line no-await-in-loop -- Failure handling belongs to the current sequential attempt.\n const failure = await handleFailure(policy, {\n attempt,\n error: execution.error,\n signal,\n sleep,\n });\n if (failure !== undefined) {\n return failure;\n }\n\n attempt += 1;\n }\n};\n\n/**\n * Default `onExhausted` used when a policy omits it: wraps the exhaustion\n * context in a generic `RetryError`.\n */\nconst defaultOnExhausted = <TError extends Error, TData>(\n input: RetryExhaustedInput<TError, TData>\n): RetryError =>\n new RetryError(\"Retry policy exhausted all attempts.\", {\n attempts: input.attempts,\n lastData: input.data,\n lastError: input.error,\n });\n\n/**\n * Default `isKnownError` used when a policy omits it: accepts any `Error`\n * instance and rejects everything else.\n */\n// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- TError only appears in the return predicate; needed so callers infer the right narrowed type.\nconst defaultIsKnownError = <TError extends Error>(\n error: unknown\n): error is TError => error instanceof Error;\n\n/**\n * Runs retry orchestration in non-throw mode.\n *\n * @param policy - Retry policy: `next` is required, `onExhausted` and\n * `isKnownError` fall back to their defaults when omitted.\n * @param execute - Async function to execute per attempt.\n * @param options - Runner settings with `throwOnExhausted: false`.\n * @returns A discriminated result union containing success value or terminal error.\n * When `policy.isKnownError` rejects a caught value, it is wrapped in a\n * `RetryError` and returned as the terminal failure instead of thrown.\n * @throws {Error} Any error thrown by `next`, `onExhausted`, or a custom `sleep`.\n */\nexport function runRetryPolicy<\n T,\n TError extends Error = Error,\n TData = unknown,\n>(\n policy: RetryPolicy<TError, TData>,\n execute: (attempt: number) => Promise<T>,\n options: RetryRunOptions & { throwOnExhausted: false }\n): Promise<RetryRunResult<T>>;\n\n/**\n * Runs retry orchestration and throws terminal error on exhaustion.\n *\n * @param policy - Retry policy: `next` is required, `onExhausted` and\n * `isKnownError` fall back to their defaults when omitted.\n * @param execute - Async function to execute per attempt.\n * @param options - Optional runner settings.\n * @returns The successful execution value.\n * @throws {RetryError} When retries are exhausted and `onExhausted` returns the\n * terminal retry error. The default implementation returns `RetryError` with the last\n * execution failure available on `RetryError.lastError`.\n * @throws {AbortError} When `options.signal` is already aborted or aborts while retrying.\n * @throws {Error} Any error thrown by `next`, by `onExhausted`, or by a custom `sleep`\n * function.\n *\n * @example\n * ```ts\n * import { runRetryPolicy } from \"@zap-studio/retry\";\n * import type { RetryPolicy } from \"@zap-studio/retry\";\n *\n * const linearBackoff: RetryPolicy = {\n * next: ({ attempt }) =>\n * attempt < 3\n * ? { shouldRetry: true, delayMs: attempt * 100, reason: \"retry\" }\n * : { shouldRetry: false, delayMs: 0, reason: \"max-attempts-reached\" },\n * };\n *\n * const data = await runRetryPolicy(linearBackoff, async () => fetchFlakyResource());\n * ```\n */\nexport function runRetryPolicy<\n T,\n TError extends Error = Error,\n TData = unknown,\n>(\n policy: RetryPolicy<TError, TData>,\n execute: (attempt: number) => Promise<T>,\n options?: RetryRunOptions & { throwOnExhausted?: true }\n): Promise<T>;\n\n/**\n * Runs retry orchestration in non-throw mode.\n *\n * When `throwOnExhausted` is `false`, returns a discriminated result union.\n *\n * @param policy - Retry policy: `next` is required, `onExhausted` and\n * `isKnownError` fall back to their defaults when omitted.\n * @param execute - Async function to execute per attempt.\n * @param options - Runner settings.\n * @returns Success value or terminal result object based on option mode.\n * @throws {Error} Any error thrown by `next`, by `onExhausted`, or by a custom `sleep`\n * function. When `throwOnExhausted` is `false`, exhaustion itself is returned\n * as `{ ok: false }` instead of thrown.\n * Cancellation is returned as `{ ok: false, error: AbortError }` in non-throw\n * mode. A value rejected by `policy.isKnownError` is wrapped in a\n * `RetryError` and returned the same way in non-throw mode; in throw mode\n * it is rethrown as-is.\n *\n * @example\n * const result = await runRetryPolicy(policy, doWork, { throwOnExhausted: false });\n * if (!result.ok) console.error(result.error);\n */\nexport async function runRetryPolicy<\n T,\n TError extends Error = Error,\n TData = unknown,\n>(\n policy: RetryPolicy<TError, TData>,\n execute: (attempt: number) => Promise<T>,\n options: RetryRunOptions = {}\n): Promise<T | RetryRunResult<T>> {\n const sleep = options.sleep ?? defaultSleep;\n const { signal } = options;\n const resolvedPolicy: ResolvedRetryPolicy<TError, TData> = {\n isKnownError: (error): error is TError =>\n policy.isKnownError\n ? policy.isKnownError(error)\n : defaultIsKnownError(error),\n next: (input) => policy.next(input),\n onExhausted: (input) =>\n policy.onExhausted\n ? policy.onExhausted(input)\n : defaultOnExhausted(input),\n };\n\n if (options.throwOnExhausted === false) {\n return await runResultMode(resolvedPolicy, execute, sleep, signal);\n }\n\n return await runThrowMode(resolvedPolicy, execute, sleep, signal);\n}\n"],"mappings":"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"}
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
8
|
* Context payload attached to `RetryError`.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const context: RetryErrorContext = { attempts: 3, lastError: new Error("network") };
|
|
9
12
|
*/
|
|
10
13
|
interface RetryErrorContext {
|
|
11
14
|
/**
|
|
@@ -23,6 +26,9 @@ interface RetryErrorContext {
|
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
28
|
* Context payload attached to `AbortError`.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const context: AbortErrorContext = { cause: new Error("shutting down") };
|
|
26
32
|
*/
|
|
27
33
|
interface AbortErrorContext {
|
|
28
34
|
/**
|
|
@@ -59,6 +65,22 @@ declare class RetryError extends Error {
|
|
|
59
65
|
}
|
|
60
66
|
/**
|
|
61
67
|
* Error thrown when retry orchestration is canceled through `AbortSignal`.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { AbortError, runRetryPolicy } from "@zap-studio/retry";
|
|
72
|
+
*
|
|
73
|
+
* const controller = new AbortController();
|
|
74
|
+
* controller.abort("shutting down");
|
|
75
|
+
*
|
|
76
|
+
* try {
|
|
77
|
+
* await runRetryPolicy(policy, doWork, { signal: controller.signal });
|
|
78
|
+
* } catch (error) {
|
|
79
|
+
* if (error instanceof AbortError) {
|
|
80
|
+
* console.error("Retry canceled:", error.message);
|
|
81
|
+
* }
|
|
82
|
+
* }
|
|
83
|
+
* ```
|
|
62
84
|
*/
|
|
63
85
|
declare class AbortError extends Error {
|
|
64
86
|
/**
|
|
@@ -66,6 +88,8 @@ declare class AbortError extends Error {
|
|
|
66
88
|
*/
|
|
67
89
|
override readonly cause?: unknown;
|
|
68
90
|
/**
|
|
91
|
+
* Creates an AbortError with an optional diagnostic cause.
|
|
92
|
+
*
|
|
69
93
|
* @param message - Human-readable abort description.
|
|
70
94
|
* @param context - Optional `cause` link for diagnostic chaining.
|
|
71
95
|
*/
|
|
@@ -73,4 +97,4 @@ declare class AbortError extends Error {
|
|
|
73
97
|
}
|
|
74
98
|
//#endregion
|
|
75
99
|
export { RetryErrorContext as i, AbortErrorContext as n, RetryError as r, AbortError as t };
|
|
76
|
-
//# sourceMappingURL=errors-
|
|
100
|
+
//# sourceMappingURL=errors-CS5UPJWs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors-CS5UPJWs.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;UAciB;;;;WAIN;;;;WAIA;;;;WAIA;;;;;;;;UASM;;;;WAIN;;;;;;;;;;;cAYE,mBAAmB;;;;WAId;;;;WAIA;;;;WAIA;;;;EAKhB,YAAY,iBAAiB,SAAS;;;;;;;;;;;;;;;;;;;;;cA4B3B,mBAAmB;;;;oBAIL;;;;;;;EAQzB,YAAY,iBAAiB,UAAS"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as RetryErrorContext, n as AbortErrorContext, r as RetryError, t as AbortError } from "./errors-
|
|
1
|
+
import { i as RetryErrorContext, n as AbortErrorContext, r as RetryError, t as AbortError } from "./errors-CS5UPJWs.js";
|
|
2
2
|
export { AbortError, AbortErrorContext, RetryError, RetryErrorContext };
|
|
@@ -34,6 +34,22 @@ var RetryError = class extends Error {
|
|
|
34
34
|
};
|
|
35
35
|
/**
|
|
36
36
|
* Error thrown when retry orchestration is canceled through `AbortSignal`.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { AbortError, runRetryPolicy } from "@zap-studio/retry";
|
|
41
|
+
*
|
|
42
|
+
* const controller = new AbortController();
|
|
43
|
+
* controller.abort("shutting down");
|
|
44
|
+
*
|
|
45
|
+
* try {
|
|
46
|
+
* await runRetryPolicy(policy, doWork, { signal: controller.signal });
|
|
47
|
+
* } catch (error) {
|
|
48
|
+
* if (error instanceof AbortError) {
|
|
49
|
+
* console.error("Retry canceled:", error.message);
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
37
53
|
*/
|
|
38
54
|
var AbortError = class extends Error {
|
|
39
55
|
/**
|
|
@@ -41,6 +57,8 @@ var AbortError = class extends Error {
|
|
|
41
57
|
*/
|
|
42
58
|
cause;
|
|
43
59
|
/**
|
|
60
|
+
* Creates an AbortError with an optional diagnostic cause.
|
|
61
|
+
*
|
|
44
62
|
* @param message - Human-readable abort description.
|
|
45
63
|
* @param context - Optional `cause` link for diagnostic chaining.
|
|
46
64
|
*/
|
|
@@ -53,4 +71,4 @@ var AbortError = class extends Error {
|
|
|
53
71
|
//#endregion
|
|
54
72
|
export { AbortError, RetryError };
|
|
55
73
|
|
|
56
|
-
//# sourceMappingURL=errors.
|
|
74
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["// oxlint-disable max-classes-per-file -- Public retry error types are intentionally colocated.\n\n/**\n * Terminal error types used by retry policies and runners.\n *\n * @module @zap-studio/retry/errors\n */\n\n/**\n * Context payload attached to `RetryError`.\n *\n * @example\n * const context: RetryErrorContext = { attempts: 3, lastError: new Error(\"network\") };\n */\nexport interface RetryErrorContext {\n /**\n * Count of completed attempts at exhaustion.\n */\n readonly attempts: number;\n /**\n * The last error object raised by a failed `execute` attempt.\n */\n readonly lastError?: unknown;\n /**\n * Optional data captured from the last attempt when provided by a policy.\n */\n readonly lastData?: unknown;\n}\n\n/**\n * Context payload attached to `AbortError`.\n *\n * @example\n * const context: AbortErrorContext = { cause: new Error(\"shutting down\") };\n */\nexport interface AbortErrorContext {\n /**\n * When the abort `reason` was an `Error`, the optional wrapped cause.\n */\n readonly cause?: unknown;\n}\n\n/**\n * Error thrown when retries are exhausted.\n *\n * @example\n * throw new RetryError(\"Retry exhausted\", {\n * attempts: 3,\n * lastError: new Error(\"network\"),\n * });\n */\nexport class RetryError extends Error {\n /**\n * Total attempts performed before exhaustion.\n */\n public readonly attempts: number;\n /**\n * Last captured error from execution.\n */\n public readonly lastError?: unknown;\n /**\n * Last captured data value, when available.\n */\n public readonly lastData?: unknown;\n\n /**\n * Creates a RetryError with structured terminal context.\n */\n constructor(message: string, context: RetryErrorContext) {\n super(message);\n this.name = \"RetryError\";\n this.attempts = context.attempts;\n this.lastError = context.lastError;\n this.lastData = context.lastData;\n }\n}\n\n/**\n * Error thrown when retry orchestration is canceled through `AbortSignal`.\n *\n * @example\n * ```ts\n * import { AbortError, runRetryPolicy } from \"@zap-studio/retry\";\n *\n * const controller = new AbortController();\n * controller.abort(\"shutting down\");\n *\n * try {\n * await runRetryPolicy(policy, doWork, { signal: controller.signal });\n * } catch (error) {\n * if (error instanceof AbortError) {\n * console.error(\"Retry canceled:\", error.message);\n * }\n * }\n * ```\n */\nexport class AbortError extends Error {\n /**\n * Optional wrapped cause when the native abort `reason` was an `Error`.\n */\n public override readonly cause?: unknown;\n\n /**\n * Creates an AbortError with an optional diagnostic cause.\n *\n * @param message - Human-readable abort description.\n * @param context - Optional `cause` link for diagnostic chaining.\n */\n constructor(message: string, context: AbortErrorContext = {}) {\n super(message);\n this.name = \"AbortError\";\n this.cause = context.cause;\n }\n}\n"],"mappings":";;;;;;;;;;AAmDA,IAAa,aAAb,cAAgC,MAAM;;;;CAIpC;;;;CAIA;;;;CAIA;;;;CAKA,YAAY,SAAiB,SAA4B;EACvD,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,WAAW,QAAQ;EACxB,KAAK,YAAY,QAAQ;EACzB,KAAK,WAAW,QAAQ;CAC1B;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,IAAa,aAAb,cAAgC,MAAM;;;;CAIpC;;;;;;;CAQA,YAAY,SAAiB,UAA6B,CAAC,GAAG;EAC5D,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,QAAQ,QAAQ;CACvB;AACF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { RetryPolicy } from "./types.js";
|
|
2
|
+
//#region src/exponential-backoff.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for `exponentialBackoff(...)`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const options: ExponentialBackoffOptions = {
|
|
8
|
+
* maxAttempts: 5,
|
|
9
|
+
* baseDelayMs: 100,
|
|
10
|
+
* maxDelayMs: 2_000,
|
|
11
|
+
* };
|
|
12
|
+
*/
|
|
13
|
+
interface ExponentialBackoffOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Maximum number of attempts (including the first) before giving up.
|
|
16
|
+
*/
|
|
17
|
+
maxAttempts: number;
|
|
18
|
+
/**
|
|
19
|
+
* Initial delay in milliseconds, doubled each retry until capped.
|
|
20
|
+
*/
|
|
21
|
+
baseDelayMs: number;
|
|
22
|
+
/**
|
|
23
|
+
* Hard upper bound in milliseconds for computed exponential delay.
|
|
24
|
+
*/
|
|
25
|
+
maxDelayMs: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates a retry policy with exponential delay growth up to a max cap.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const policy = exponentialBackoff({
|
|
32
|
+
* maxAttempts: 5,
|
|
33
|
+
* baseDelayMs: 100,
|
|
34
|
+
* maxDelayMs: 2_000,
|
|
35
|
+
* });
|
|
36
|
+
*/
|
|
37
|
+
declare const exponentialBackoff: (options: ExponentialBackoffOptions) => RetryPolicy;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { ExponentialBackoffOptions, exponentialBackoff };
|
|
40
|
+
//# sourceMappingURL=exponential-backoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exponential-backoff.d.ts","names":[],"sources":["../src/exponential-backoff.ts"],"mappings":";;;;;;;;;;;;UAsBiB;;;;EAIf;;;;EAIA;;;;EAIA;;;;;;;;;;;;cAaW,qBACX,SAAS,8BACR"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/exponential-backoff.ts
|
|
2
|
+
/**
|
|
3
|
+
* Creates a retry policy with exponential delay growth up to a max cap.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const policy = exponentialBackoff({
|
|
7
|
+
* maxAttempts: 5,
|
|
8
|
+
* baseDelayMs: 100,
|
|
9
|
+
* maxDelayMs: 2_000,
|
|
10
|
+
* });
|
|
11
|
+
*/
|
|
12
|
+
const exponentialBackoff = (options) => {
|
|
13
|
+
const { maxAttempts, baseDelayMs, maxDelayMs } = options;
|
|
14
|
+
return {
|
|
15
|
+
/**
|
|
16
|
+
* Computes retry decision for the current attempt.
|
|
17
|
+
*/
|
|
18
|
+
next(input) {
|
|
19
|
+
if (input.attempt >= maxAttempts) return {
|
|
20
|
+
delayMs: 0,
|
|
21
|
+
reason: "max-attempts-reached",
|
|
22
|
+
shouldRetry: false
|
|
23
|
+
};
|
|
24
|
+
const exponent = Math.max(0, input.attempt - 1);
|
|
25
|
+
return {
|
|
26
|
+
delayMs: Math.min(maxDelayMs, baseDelayMs * 2 ** exponent),
|
|
27
|
+
reason: "retry",
|
|
28
|
+
shouldRetry: true
|
|
29
|
+
};
|
|
30
|
+
} };
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { exponentialBackoff };
|
|
34
|
+
|
|
35
|
+
//# sourceMappingURL=exponential-backoff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exponential-backoff.js","names":[],"sources":["../src/exponential-backoff.ts"],"sourcesContent":["/**\n * Exponential backoff retry strategy.\n *\n * @module @zap-studio/retry/exponential-backoff\n */\n\nimport type {\n RetryDecision,\n RetryDecisionInput,\n RetryPolicy,\n} from \"./types.js\";\n\n/**\n * Configuration for `exponentialBackoff(...)`.\n *\n * @example\n * const options: ExponentialBackoffOptions = {\n * maxAttempts: 5,\n * baseDelayMs: 100,\n * maxDelayMs: 2_000,\n * };\n */\nexport interface ExponentialBackoffOptions {\n /**\n * Maximum number of attempts (including the first) before giving up.\n */\n maxAttempts: number;\n /**\n * Initial delay in milliseconds, doubled each retry until capped.\n */\n baseDelayMs: number;\n /**\n * Hard upper bound in milliseconds for computed exponential delay.\n */\n maxDelayMs: number;\n}\n\n/**\n * Creates a retry policy with exponential delay growth up to a max cap.\n *\n * @example\n * const policy = exponentialBackoff({\n * maxAttempts: 5,\n * baseDelayMs: 100,\n * maxDelayMs: 2_000,\n * });\n */\nexport const exponentialBackoff = (\n options: ExponentialBackoffOptions\n): RetryPolicy => {\n const { maxAttempts, baseDelayMs, maxDelayMs } = options;\n\n return {\n /**\n * Computes retry decision for the current attempt.\n */\n next(input: RetryDecisionInput): RetryDecision {\n if (input.attempt >= maxAttempts) {\n return {\n delayMs: 0,\n reason: \"max-attempts-reached\",\n shouldRetry: false,\n };\n }\n\n const exponent = Math.max(0, input.attempt - 1);\n const delayMs = Math.min(maxDelayMs, baseDelayMs * 2 ** exponent);\n\n return { delayMs, reason: \"retry\", shouldRetry: true };\n },\n };\n};\n"],"mappings":";;;;;;;;;;;AA+CA,MAAa,sBACX,YACgB;CAChB,MAAM,EAAE,aAAa,aAAa,eAAe;CAEjD,OAAO;;;;AAIL,KAAK,OAA0C;EAC7C,IAAI,MAAM,WAAW,aACnB,OAAO;GACL,SAAS;GACT,QAAQ;GACR,aAAa;EACf;EAGF,MAAM,WAAW,KAAK,IAAI,GAAG,MAAM,UAAU,CAAC;EAG9C,OAAO;GAAE,SAFO,KAAK,IAAI,YAAY,cAAc,KAAK,QAEzC;GAAG,QAAQ;GAAS,aAAa;EAAK;CACvD,EACF;AACF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { RetryPolicy } from "./types.js";
|
|
2
|
+
//#region src/fixed-delay.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for `fixedDelay(...)`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const options: FixedDelayOptions = { maxAttempts: 3, delayMs: 250 };
|
|
8
|
+
*/
|
|
9
|
+
interface FixedDelayOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Maximum number of attempts (including the first) before giving up.
|
|
12
|
+
*/
|
|
13
|
+
maxAttempts: number;
|
|
14
|
+
/**
|
|
15
|
+
* Constant delay in milliseconds before each retry after a failure.
|
|
16
|
+
*/
|
|
17
|
+
delayMs: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a retry policy with a constant delay between attempts.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const policy = fixedDelay({
|
|
24
|
+
* maxAttempts: 3,
|
|
25
|
+
* delayMs: 250,
|
|
26
|
+
* });
|
|
27
|
+
*/
|
|
28
|
+
declare const fixedDelay: (options: FixedDelayOptions) => RetryPolicy;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { FixedDelayOptions, fixedDelay };
|
|
31
|
+
//# sourceMappingURL=fixed-delay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixed-delay.d.ts","names":[],"sources":["../src/fixed-delay.ts"],"mappings":";;;;;;;;UAkBiB;;;;EAIf;;;;EAIA;;;;;;;;;;;cAYW,aAAc,SAAS,sBAAoB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/fixed-delay.ts
|
|
2
|
+
/**
|
|
3
|
+
* Creates a retry policy with a constant delay between attempts.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const policy = fixedDelay({
|
|
7
|
+
* maxAttempts: 3,
|
|
8
|
+
* delayMs: 250,
|
|
9
|
+
* });
|
|
10
|
+
*/
|
|
11
|
+
const fixedDelay = (options) => {
|
|
12
|
+
const { maxAttempts, delayMs } = options;
|
|
13
|
+
return {
|
|
14
|
+
/**
|
|
15
|
+
* Computes retry decision for the current attempt.
|
|
16
|
+
*/
|
|
17
|
+
next(input) {
|
|
18
|
+
if (input.attempt >= maxAttempts) return {
|
|
19
|
+
delayMs: 0,
|
|
20
|
+
reason: "max-attempts-reached",
|
|
21
|
+
shouldRetry: false
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
delayMs,
|
|
25
|
+
reason: "retry",
|
|
26
|
+
shouldRetry: true
|
|
27
|
+
};
|
|
28
|
+
} };
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { fixedDelay };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=fixed-delay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixed-delay.js","names":[],"sources":["../src/fixed-delay.ts"],"sourcesContent":["/**\n * Fixed-delay retry strategy.\n *\n * @module @zap-studio/retry/fixed-delay\n */\n\nimport type {\n RetryDecision,\n RetryDecisionInput,\n RetryPolicy,\n} from \"./types.js\";\n\n/**\n * Configuration for `fixedDelay(...)`.\n *\n * @example\n * const options: FixedDelayOptions = { maxAttempts: 3, delayMs: 250 };\n */\nexport interface FixedDelayOptions {\n /**\n * Maximum number of attempts (including the first) before giving up.\n */\n maxAttempts: number;\n /**\n * Constant delay in milliseconds before each retry after a failure.\n */\n delayMs: number;\n}\n\n/**\n * Creates a retry policy with a constant delay between attempts.\n *\n * @example\n * const policy = fixedDelay({\n * maxAttempts: 3,\n * delayMs: 250,\n * });\n */\nexport const fixedDelay = (options: FixedDelayOptions): RetryPolicy => {\n const { maxAttempts, delayMs } = options;\n\n return {\n /**\n * Computes retry decision for the current attempt.\n */\n next(input: RetryDecisionInput): RetryDecision {\n if (input.attempt >= maxAttempts) {\n return {\n delayMs: 0,\n reason: \"max-attempts-reached\",\n shouldRetry: false,\n };\n }\n\n return { delayMs, reason: \"retry\", shouldRetry: true };\n },\n };\n};\n"],"mappings":";;;;;;;;;;AAsCA,MAAa,cAAc,YAA4C;CACrE,MAAM,EAAE,aAAa,YAAY;CAEjC,OAAO;;;;AAIL,KAAK,OAA0C;EAC7C,IAAI,MAAM,WAAW,aACnB,OAAO;GACL,SAAS;GACT,QAAQ;GACR,aAAa;EACf;EAGF,OAAO;GAAE;GAAS,QAAQ;GAAS,aAAa;EAAK;CACvD,EACF;AACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { i as RetryErrorContext, n as AbortErrorContext, r as RetryError, t as AbortError } from "./errors-CS5UPJWs.js";
|
|
2
|
+
import { RetryDecision, RetryDecisionInput, RetryExhaustedInput, RetryPolicy, RetryRunOptions, RetryRunResult } from "./types.js";
|
|
3
|
+
import { defaultSleep, runRetryPolicy } from "./base-policy.js";
|
|
4
|
+
import { ExponentialBackoffOptions, exponentialBackoff } from "./exponential-backoff.js";
|
|
5
|
+
import { FixedDelayOptions, fixedDelay } from "./fixed-delay.js";
|
|
6
|
+
import { LinearBackoffOptions, linearBackoff } from "./linear-backoff.js";
|
|
7
|
+
export { AbortError, type AbortErrorContext, type ExponentialBackoffOptions, type FixedDelayOptions, type LinearBackoffOptions, type RetryDecision, type RetryDecisionInput, RetryError, type RetryErrorContext, type RetryExhaustedInput, type RetryPolicy, type RetryRunOptions, type RetryRunResult, defaultSleep, exponentialBackoff, fixedDelay, linearBackoff, runRetryPolicy };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AbortError, RetryError } from "./errors.js";
|
|
2
|
+
import { defaultSleep, runRetryPolicy } from "./base-policy.js";
|
|
3
|
+
import { exponentialBackoff } from "./exponential-backoff.js";
|
|
4
|
+
import { fixedDelay } from "./fixed-delay.js";
|
|
5
|
+
import { linearBackoff } from "./linear-backoff.js";
|
|
6
|
+
export { AbortError, RetryError, defaultSleep, exponentialBackoff, fixedDelay, linearBackoff, runRetryPolicy };
|