@zap-studio/retry 1.2.0 → 2.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 +16 -0
- package/README.md +32 -0
- package/dist/base-policy-CiDqMVSD.js +381 -0
- package/dist/base-policy-CiDqMVSD.js.map +1 -0
- package/dist/base-policy.d.ts.map +1 -1
- package/dist/base-policy.js +2 -333
- package/dist/index.js +1 -1
- package/package.json +15 -4
- package/dist/base-policy.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [2.0.0]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
Native OpenTelemetry support. Unlike `fetch`, `webhooks`, and `permit`, this package never creates its own span — a retry loop wraps someone else's operation, so each decision is recorded as an event (`retry.scheduled`/`retry.exhausted`) on whatever span is already active, plus a `retry.attempts` counter tagged by outcome. See [OpenTelemetry](https://www.zapstudio.dev/retry/opentelemetry).
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
**Breaking:** `@opentelemetry/api` is now a required peer dependency. It's tiny, side-effect-free, and a no-op until an app registers a real SDK, so nothing changes at runtime for consumers who don't set one up — but the package won't resolve without it installed: `npm install @opentelemetry/api`.
|
|
16
|
+
|
|
17
|
+
## [1.2.1]
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
`@zap-studio/logger` is now an optional peer dependency instead of a regular dependency. Every import from it is type-only (`import type { Logger }`), so it was never pulled in at runtime — pass any object matching the `Logger` shape (including `pino`) with no install required. Existing consumers of `logger?: Logger` are unaffected.
|
|
22
|
+
|
|
7
23
|
## [1.2.0]
|
|
8
24
|
|
|
9
25
|
### Added
|
package/README.md
CHANGED
|
@@ -4,6 +4,16 @@ Composable retry policy primitives for HTTP clients and async workflows.
|
|
|
4
4
|
|
|
5
5
|
Full documentation: [zapstudio.dev/retry](https://www.zapstudio.dev/retry)
|
|
6
6
|
|
|
7
|
+
## Motivation
|
|
8
|
+
|
|
9
|
+
A hand-written retry loop is usually a `for` loop with `setTimeout`, and it is easy to get wrong in ways that only show up under load. Without jitter, every client that lost connection to a service retries at the exact same moment once it comes back, causing a new spike right when the service is trying to recover.
|
|
10
|
+
|
|
11
|
+
Without cancellation, a retry loop can keep running — and keep hitting the network — after the result is no longer needed, for example after a user navigates away.
|
|
12
|
+
|
|
13
|
+
`@zap-studio/retry` gives you this behavior as a built-in option, instead of something you write from scratch. `exponentialBackoff` and `linearBackoff` support jitter (`"full"` or `"equal"`, the same strategies AWS recommends) through the `jitter` option — turn it on and delays are randomized instead of fixed.
|
|
14
|
+
|
|
15
|
+
Every retry loop also accepts an `AbortSignal`, checked before each attempt and while waiting between attempts, so an abort stops the next attempt or delay from starting; it does not interrupt an attempt that is already running. You get retry policies as values you configure once and reuse, instead of logic you have to get right from scratch in every project.
|
|
16
|
+
|
|
7
17
|
## Installation
|
|
8
18
|
|
|
9
19
|
```bash
|
|
@@ -185,6 +195,28 @@ await runRetryPolicy(policy, execute, { logger });
|
|
|
185
195
|
|
|
186
196
|
Each retry decision logs at `debug` (attempt, delay, reason), exhaustion logs at `warn`, and cancellation logs at `debug`.
|
|
187
197
|
|
|
198
|
+
## OpenTelemetry
|
|
199
|
+
|
|
200
|
+
`@opentelemetry/api` is a required peer dependency — tiny, side-effect-free, and a no-op until an app registers a real SDK, so installing it costs nothing at runtime for consumers who never set one up.
|
|
201
|
+
|
|
202
|
+
Unlike `fetch`, `webhooks`, and `permit`, this package never creates its own span — a retry loop wraps someone else's operation, so each decision is recorded as an **event** on whatever span is already active (e.g. a caller's `fetch` span), plus a `retry.attempts` counter tagged by outcome:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
npm install @opentelemetry/api
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { exponentialBackoff, runRetryPolicy } from "@zap-studio/retry";
|
|
210
|
+
|
|
211
|
+
const policy = exponentialBackoff({ maxAttempts: 5, baseDelayMs: 100 });
|
|
212
|
+
|
|
213
|
+
// If a span is active when this runs (e.g. inside a caller's own span, or
|
|
214
|
+
// nested inside a @zap-studio/fetch call), each retry adds a
|
|
215
|
+
// "retry.scheduled" or "retry.exhausted" event to it. If not, it's a no-op
|
|
216
|
+
// — no wiring required either way.
|
|
217
|
+
await runRetryPolicy(policy, execute);
|
|
218
|
+
```
|
|
219
|
+
|
|
188
220
|
## Runtime Support
|
|
189
221
|
|
|
190
222
|
| Runtime | Minimum version |
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { AbortError, RetryError } from "./errors.js";
|
|
2
|
+
import { metrics, trace } from "@opentelemetry/api";
|
|
3
|
+
//#region package.json
|
|
4
|
+
var name = "@zap-studio/retry";
|
|
5
|
+
var version = "2.0.0";
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/_otel.ts
|
|
8
|
+
/**
|
|
9
|
+
* Internal OpenTelemetry wiring for the retry package: the attempts counter.
|
|
10
|
+
* Kept out of `base-policy.ts` so retry orchestration doesn't get tangled
|
|
11
|
+
* with metrics concerns. Span events are added directly to whatever span is
|
|
12
|
+
* already active (e.g. a caller's fetch span), so this package never
|
|
13
|
+
* creates its own tracer.
|
|
14
|
+
*
|
|
15
|
+
* @module @zap-studio/retry/otel
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Records one retry decision, tagged with `retry.decision: "retry" |
|
|
19
|
+
* "exhausted"`.
|
|
20
|
+
*
|
|
21
|
+
* Resolves the meter and counter fresh on every call instead of caching
|
|
22
|
+
* them at module scope: unlike `trace.getTracer()`, `metrics.getMeter()`
|
|
23
|
+
* has no proxy indirection — a reference grabbed before an app registers
|
|
24
|
+
* its `MeterProvider` (the common case, since ESM imports resolve before
|
|
25
|
+
* the importing module's own SDK-bootstrap code runs) would stay a no-op
|
|
26
|
+
* forever. Repeated `createCounter` calls with the same name are cheap and
|
|
27
|
+
* idempotent, so this costs nothing meaningful.
|
|
28
|
+
*/
|
|
29
|
+
const recordRetryAttempt = (decision) => {
|
|
30
|
+
metrics.getMeter(name, version).createCounter("retry.attempts", { description: "Number of retry decisions made, tagged by outcome." }).add(1, { "retry.decision": decision });
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/base-policy.ts
|
|
34
|
+
/**
|
|
35
|
+
* Retry runner base class and shared orchestration implementation.
|
|
36
|
+
*
|
|
37
|
+
* @module @zap-studio/retry/base-policy
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Awaits a timer-based delay, unless `delayMs` is non-positive.
|
|
41
|
+
*
|
|
42
|
+
* @param delayMs - Milliseconds to wait before resolving.
|
|
43
|
+
* @returns Promise that resolves when the delay completes.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { defaultSleep } from "@zap-studio/retry";
|
|
48
|
+
*
|
|
49
|
+
* await defaultSleep(250); // waits 250ms
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
const defaultSleep = async (delayMs) => {
|
|
53
|
+
if (delayMs <= 0) return;
|
|
54
|
+
await new Promise((resolve) => {
|
|
55
|
+
setTimeout(resolve, delayMs);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Normalizes an abort `reason` into an `AbortError`.
|
|
60
|
+
*/
|
|
61
|
+
const toAbortError = (reason) => {
|
|
62
|
+
if (reason instanceof AbortError) return reason;
|
|
63
|
+
if (reason instanceof Error) return new AbortError(reason.message, { cause: reason });
|
|
64
|
+
if (typeof reason === "string" && reason.length > 0) return new AbortError(reason);
|
|
65
|
+
if (reason === void 0) return new AbortError("Retry aborted.");
|
|
66
|
+
try {
|
|
67
|
+
return new AbortError(`Retry aborted: ${JSON.stringify(reason)}`);
|
|
68
|
+
} catch {
|
|
69
|
+
return new AbortError("Retry aborted.");
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Throws when the provided abort signal is already aborted.
|
|
74
|
+
*
|
|
75
|
+
* @param signal - Optional abort signal to inspect.
|
|
76
|
+
* @param logger - Optional logger; logs the abort at `debug` before throwing.
|
|
77
|
+
* @throws {AbortError} When the signal is aborted.
|
|
78
|
+
*/
|
|
79
|
+
const throwIfAborted = (signal, logger) => {
|
|
80
|
+
if (signal?.aborted !== true) return;
|
|
81
|
+
logger?.debug("retry aborted", { reason: signal.reason });
|
|
82
|
+
throw toAbortError(signal.reason);
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Waits for delay sleep while observing cancellation through an abort signal.
|
|
86
|
+
*
|
|
87
|
+
* @param sleep - Sleep function used to await `delayMs`.
|
|
88
|
+
* @param delayMs - Delay duration in milliseconds.
|
|
89
|
+
* @param signal - Abort signal to observe while waiting.
|
|
90
|
+
* @returns Promise that resolves when delay finishes.
|
|
91
|
+
* @throws {AbortError} When the signal aborts before or during wait.
|
|
92
|
+
*/
|
|
93
|
+
const sleepWithAbortSignal = async (sleep, delayMs, signal) => {
|
|
94
|
+
if (signal.aborted) throw toAbortError(signal.reason);
|
|
95
|
+
let onAbort;
|
|
96
|
+
try {
|
|
97
|
+
await Promise.race([sleep(delayMs), new Promise((_resolve, reject) => {
|
|
98
|
+
onAbort = () => {
|
|
99
|
+
reject(toAbortError(signal.reason));
|
|
100
|
+
};
|
|
101
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
102
|
+
})]);
|
|
103
|
+
} finally {
|
|
104
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Logs a `next(...)` decision: `debug` when retrying, `warn` when exhausted.
|
|
109
|
+
* Shared by both the throw-mode and non-throw retry loops.
|
|
110
|
+
*/
|
|
111
|
+
const logRetryDecision = (logger, attempt, decision, error) => {
|
|
112
|
+
if (decision.shouldRetry) {
|
|
113
|
+
logger?.debug("retry scheduled", {
|
|
114
|
+
attempt,
|
|
115
|
+
delayMs: decision.delayMs,
|
|
116
|
+
reason: decision.reason
|
|
117
|
+
});
|
|
118
|
+
trace.getActiveSpan()?.addEvent("retry.scheduled", {
|
|
119
|
+
attempt,
|
|
120
|
+
"retry.delay_ms": decision.delayMs,
|
|
121
|
+
"retry.reason": decision.reason ?? ""
|
|
122
|
+
});
|
|
123
|
+
recordRetryAttempt("retry");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
logger?.warn("retry policy exhausted", {
|
|
127
|
+
attempts: attempt,
|
|
128
|
+
error,
|
|
129
|
+
reason: decision.reason
|
|
130
|
+
});
|
|
131
|
+
trace.getActiveSpan()?.addEvent("retry.exhausted", {
|
|
132
|
+
attempt,
|
|
133
|
+
"retry.reason": decision.reason ?? ""
|
|
134
|
+
});
|
|
135
|
+
recordRetryAttempt("exhausted");
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Runs the throw-mode retry loop: throws `RetryError` on exhaustion and
|
|
139
|
+
* `AbortError` when `signal` aborts.
|
|
140
|
+
*
|
|
141
|
+
* @param policy - Resolved retry policy providing `next` and `onExhausted`.
|
|
142
|
+
* @param execute - Async work callback per attempt.
|
|
143
|
+
* @param sleep - Delay function between retries.
|
|
144
|
+
* @param signal - Optional cancel signal.
|
|
145
|
+
* @param logger - Optional logger; logs each retry decision at `debug` and
|
|
146
|
+
* exhaustion at `warn`.
|
|
147
|
+
* @returns Resolves to the first successful return value.
|
|
148
|
+
* @throws {RetryError} When retries are exhausted and `onExhausted` returns
|
|
149
|
+
* the terminal error.
|
|
150
|
+
* @throws {AbortError} When `signal` is already aborted or aborts while waiting.
|
|
151
|
+
* @throws {Error} Any error thrown by `next`, `onExhausted`, or `sleep`. Also
|
|
152
|
+
* rethrows the original caught value immediately, bypassing retry, when
|
|
153
|
+
* `policy.isKnownError` rejects it as outside this policy's error domain.
|
|
154
|
+
*/
|
|
155
|
+
const runThrowMode = async (policy, execute, sleep, signal, logger) => {
|
|
156
|
+
let attempt = 1;
|
|
157
|
+
while (true) {
|
|
158
|
+
throwIfAborted(signal, logger);
|
|
159
|
+
try {
|
|
160
|
+
return await execute(attempt);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throwIfAborted(signal, logger);
|
|
163
|
+
if (!policy.isKnownError(error)) throw error;
|
|
164
|
+
const decision = policy.next({
|
|
165
|
+
attempt,
|
|
166
|
+
error
|
|
167
|
+
});
|
|
168
|
+
logRetryDecision(logger, attempt, decision, error);
|
|
169
|
+
if (!decision.shouldRetry) throw policy.onExhausted({
|
|
170
|
+
attempts: attempt,
|
|
171
|
+
error
|
|
172
|
+
});
|
|
173
|
+
if (decision.delayMs > 0) await (signal === void 0 ? sleep(decision.delayMs) : sleepWithAbortSignal(sleep, decision.delayMs, signal));
|
|
174
|
+
attempt += 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* When `signal` is already aborted, builds the terminal `{ ok: false }` object
|
|
180
|
+
* with a normalized `AbortError` on `error`.
|
|
181
|
+
*
|
|
182
|
+
* @param signal - Optional abort signal; only acts when `aborted` is set.
|
|
183
|
+
* @param attempts - Number of finished attempts to report in the result.
|
|
184
|
+
* @param logger - Optional logger; logs the abort at `debug`.
|
|
185
|
+
* @returns Failure result or `undefined` if not aborted.
|
|
186
|
+
*/
|
|
187
|
+
const buildAbortResult = (signal, attempts, logger) => {
|
|
188
|
+
if (signal?.aborted !== true) return;
|
|
189
|
+
logger?.debug("retry aborted", { reason: signal.reason });
|
|
190
|
+
return {
|
|
191
|
+
attempts,
|
|
192
|
+
error: toAbortError(signal.reason),
|
|
193
|
+
ok: false
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Runs one `execute(attempt)` call and returns either a success value or a
|
|
198
|
+
* captured error without rethrowing.
|
|
199
|
+
*
|
|
200
|
+
* @param execute - User work callback.
|
|
201
|
+
* @param attempt - One-based attempt number passed to `execute`.
|
|
202
|
+
* @returns A tagged success with `value` or a tagged failure with `error`.
|
|
203
|
+
*/
|
|
204
|
+
const runAttempt = async (execute, attempt) => {
|
|
205
|
+
try {
|
|
206
|
+
return {
|
|
207
|
+
ok: true,
|
|
208
|
+
value: await execute(attempt)
|
|
209
|
+
};
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return {
|
|
212
|
+
error,
|
|
213
|
+
ok: false
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Awaits inter-attempt delay in result mode, mapping an abort during wait to
|
|
219
|
+
* a terminal result instead of throwing when `throwOnExhausted` is false.
|
|
220
|
+
*
|
|
221
|
+
* @param sleep - Custom or default sleep implementation.
|
|
222
|
+
* @param delayMs - Milliseconds to wait.
|
|
223
|
+
* @param signal - If set, `sleep` is raced with the abort signal.
|
|
224
|
+
* @param attempts - Attempt count to attach if the wait ends in abort.
|
|
225
|
+
* @param logger - Optional logger; logs an abort ending the wait at `debug`.
|
|
226
|
+
* @returns A terminal result when canceled during the wait, otherwise
|
|
227
|
+
* `undefined`.
|
|
228
|
+
* @throws {Error} The underlying `sleep` rejection when it is not an abort.
|
|
229
|
+
*/
|
|
230
|
+
const waitForDelay = async (sleep, delayMs, signal, attempts, logger) => {
|
|
231
|
+
if (signal === void 0) {
|
|
232
|
+
await sleep(delayMs);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
await sleepWithAbortSignal(sleep, delayMs, signal);
|
|
237
|
+
return;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const aborted = buildAbortResult(signal, attempts, logger);
|
|
240
|
+
if (aborted !== void 0) return aborted;
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* After a failed attempt, applies abort rules, `next`, optional delay, and
|
|
246
|
+
* either returns a terminal `RetryRunResult` or `undefined` to continue.
|
|
247
|
+
*
|
|
248
|
+
* @param policy - Resolved retry policy hooks (`next`, `onExhausted`).
|
|
249
|
+
* @param params - Failure context for the current attempt.
|
|
250
|
+
* @param params.attempt - Current attempt number.
|
|
251
|
+
* @param params.error - Error thrown by the attempt.
|
|
252
|
+
* @param params.sleep - Delay function between retries.
|
|
253
|
+
* @param params.signal - Optional abort signal.
|
|
254
|
+
* @param params.logger - Optional logger; logs each retry decision at
|
|
255
|
+
* `debug`, exhaustion at `warn`, and cancellation at `debug`.
|
|
256
|
+
* @returns Terminal non-throw result if the loop should stop, otherwise
|
|
257
|
+
* `undefined` to schedule another attempt.
|
|
258
|
+
* @throws {Error} Any error thrown by `next`, `onExhausted`, or a custom `sleep` when
|
|
259
|
+
* the error is not an abort.
|
|
260
|
+
*/
|
|
261
|
+
const handleFailure = async (policy, params) => {
|
|
262
|
+
const { attempt, error, sleep, signal, logger } = params;
|
|
263
|
+
const abortResult = buildAbortResult(signal, attempt, logger);
|
|
264
|
+
if (abortResult !== void 0) return abortResult;
|
|
265
|
+
const decision = policy.next({
|
|
266
|
+
attempt,
|
|
267
|
+
error
|
|
268
|
+
});
|
|
269
|
+
logRetryDecision(logger, attempt, decision, error);
|
|
270
|
+
if (!decision.shouldRetry) return {
|
|
271
|
+
attempts: attempt,
|
|
272
|
+
error: policy.onExhausted({
|
|
273
|
+
attempts: attempt,
|
|
274
|
+
error
|
|
275
|
+
}),
|
|
276
|
+
ok: false
|
|
277
|
+
};
|
|
278
|
+
if (decision.delayMs > 0) {
|
|
279
|
+
const delayAbortResult = await waitForDelay(sleep, decision.delayMs, signal, attempt, logger);
|
|
280
|
+
if (delayAbortResult !== void 0) return delayAbortResult;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Runs the non-throw retry loop, returning
|
|
285
|
+
* `RetryRunResult`.
|
|
286
|
+
*
|
|
287
|
+
* @param policy - Resolved retry policy providing `next` and `onExhausted`.
|
|
288
|
+
* @param execute - Async work callback per attempt.
|
|
289
|
+
* @param sleep - Delay function between retries.
|
|
290
|
+
* @param signal - Optional cancel signal.
|
|
291
|
+
* @param logger - Optional logger; logs each retry decision at `debug`,
|
|
292
|
+
* exhaustion at `warn`, and cancellation at `debug`.
|
|
293
|
+
* @returns Terminal success or failure object. When `policy.isKnownError`
|
|
294
|
+
* rejects a caught value as outside this policy's error domain, the
|
|
295
|
+
* original value is wrapped in a `RetryError` and returned on
|
|
296
|
+
* `result.error` immediately, bypassing retry — never thrown.
|
|
297
|
+
* @throws {Error} Any error thrown by `next`, `onExhausted`, or a non-abort `sleep`
|
|
298
|
+
* failure.
|
|
299
|
+
*/
|
|
300
|
+
const runResultMode = async (policy, execute, sleep, signal, logger) => {
|
|
301
|
+
let attempt = 1;
|
|
302
|
+
while (true) {
|
|
303
|
+
const abortResult = buildAbortResult(signal, Math.max(0, attempt - 1), logger);
|
|
304
|
+
if (abortResult !== void 0) return abortResult;
|
|
305
|
+
const execution = await runAttempt(execute, attempt);
|
|
306
|
+
if (execution.ok) return {
|
|
307
|
+
ok: true,
|
|
308
|
+
value: execution.value
|
|
309
|
+
};
|
|
310
|
+
const attemptAbortResult = buildAbortResult(signal, attempt, logger);
|
|
311
|
+
if (attemptAbortResult !== void 0) return attemptAbortResult;
|
|
312
|
+
if (!policy.isKnownError(execution.error)) return {
|
|
313
|
+
attempts: attempt,
|
|
314
|
+
error: new RetryError("Retry policy encountered an unknown error.", {
|
|
315
|
+
attempts: attempt,
|
|
316
|
+
lastError: execution.error
|
|
317
|
+
}),
|
|
318
|
+
ok: false
|
|
319
|
+
};
|
|
320
|
+
const failure = await handleFailure(policy, {
|
|
321
|
+
attempt,
|
|
322
|
+
error: execution.error,
|
|
323
|
+
logger,
|
|
324
|
+
signal,
|
|
325
|
+
sleep
|
|
326
|
+
});
|
|
327
|
+
if (failure !== void 0) return failure;
|
|
328
|
+
attempt += 1;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* Default `onExhausted` used when a policy omits it: wraps the exhaustion
|
|
333
|
+
* context in a generic `RetryError`.
|
|
334
|
+
*/
|
|
335
|
+
const defaultOnExhausted = (input) => new RetryError("Retry policy exhausted all attempts.", {
|
|
336
|
+
attempts: input.attempts,
|
|
337
|
+
lastData: input.data,
|
|
338
|
+
lastError: input.error
|
|
339
|
+
});
|
|
340
|
+
/**
|
|
341
|
+
* Default `isKnownError` used when a policy omits it: accepts any `Error`
|
|
342
|
+
* instance and rejects everything else.
|
|
343
|
+
*/
|
|
344
|
+
const defaultIsKnownError = (error) => error instanceof Error;
|
|
345
|
+
/**
|
|
346
|
+
* Runs retry orchestration in non-throw mode.
|
|
347
|
+
*
|
|
348
|
+
* When `throwOnExhausted` is `false`, returns a discriminated result union.
|
|
349
|
+
*
|
|
350
|
+
* @param policy - Retry policy: `next` is required, `onExhausted` and
|
|
351
|
+
* `isKnownError` fall back to their defaults when omitted.
|
|
352
|
+
* @param execute - Async function to execute per attempt.
|
|
353
|
+
* @param options - Runner settings.
|
|
354
|
+
* @returns Success value or terminal result object based on option mode.
|
|
355
|
+
* @throws {Error} Any error thrown by `next`, by `onExhausted`, or by a custom `sleep`
|
|
356
|
+
* function. When `throwOnExhausted` is `false`, exhaustion itself is returned
|
|
357
|
+
* as `{ ok: false }` instead of thrown.
|
|
358
|
+
* Cancellation is returned as `{ ok: false, error: AbortError }` in non-throw
|
|
359
|
+
* mode. A value rejected by `policy.isKnownError` is wrapped in a
|
|
360
|
+
* `RetryError` and returned the same way in non-throw mode; in throw mode
|
|
361
|
+
* it is rethrown as-is.
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* const result = await runRetryPolicy(policy, doWork, { throwOnExhausted: false });
|
|
365
|
+
* if (!result.ok) console.error(result.error);
|
|
366
|
+
*/
|
|
367
|
+
async function runRetryPolicy(policy, execute, options = {}) {
|
|
368
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
369
|
+
const { signal, logger } = options;
|
|
370
|
+
const resolvedPolicy = {
|
|
371
|
+
isKnownError: (error) => policy.isKnownError ? policy.isKnownError(error) : defaultIsKnownError(error),
|
|
372
|
+
next: (input) => policy.next(input),
|
|
373
|
+
onExhausted: (input) => policy.onExhausted ? policy.onExhausted(input) : defaultOnExhausted(input)
|
|
374
|
+
};
|
|
375
|
+
if (options.throwOnExhausted === false) return await runResultMode(resolvedPolicy, execute, sleep, signal, logger);
|
|
376
|
+
return await runThrowMode(resolvedPolicy, execute, sleep, signal, logger);
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
export { runRetryPolicy as n, defaultSleep as t };
|
|
380
|
+
|
|
381
|
+
//# sourceMappingURL=base-policy-CiDqMVSD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-policy-CiDqMVSD.js","names":["pkg.name","pkg.version"],"sources":["../package.json","../src/_otel.ts","../src/base-policy.ts"],"sourcesContent":["","/**\n * Internal OpenTelemetry wiring for the retry package: the attempts counter.\n * Kept out of `base-policy.ts` so retry orchestration doesn't get tangled\n * with metrics concerns. Span events are added directly to whatever span is\n * already active (e.g. a caller's fetch span), so this package never\n * creates its own tracer.\n *\n * @module @zap-studio/retry/otel\n */\n\nimport { metrics } from \"@opentelemetry/api\";\n\nimport pkg from \"../package.json\" with { type: \"json\" };\n\n/**\n * Records one retry decision, tagged with `retry.decision: \"retry\" |\n * \"exhausted\"`.\n *\n * Resolves the meter and counter fresh on every call instead of caching\n * them at module scope: unlike `trace.getTracer()`, `metrics.getMeter()`\n * has no proxy indirection — a reference grabbed before an app registers\n * its `MeterProvider` (the common case, since ESM imports resolve before\n * the importing module's own SDK-bootstrap code runs) would stay a no-op\n * forever. Repeated `createCounter` calls with the same name are cheap and\n * idempotent, so this costs nothing meaningful.\n */\nexport const recordRetryAttempt = (decision: \"exhausted\" | \"retry\"): void => {\n metrics\n .getMeter(pkg.name, pkg.version)\n .createCounter(\"retry.attempts\", {\n description: \"Number of retry decisions made, tagged by outcome.\",\n })\n .add(1, { \"retry.decision\": decision });\n};\n","/**\n * Retry runner base class and shared orchestration implementation.\n *\n * @module @zap-studio/retry/base-policy\n */\n\nimport { trace } from \"@opentelemetry/api\";\nimport type { Logger } from \"@zap-studio/logger\";\n\nimport { recordRetryAttempt } from \"./_otel.js\";\nimport { AbortError, RetryError } from \"./errors.js\";\nimport type {\n ResolvedRetryPolicy,\n RetryDecision,\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 * @param logger - Optional logger; logs the abort at `debug` before throwing.\n * @throws {AbortError} When the signal is aborted.\n */\nconst throwIfAborted = (signal?: AbortSignal, logger?: Logger): void => {\n if (signal?.aborted !== true) {\n return;\n }\n\n logger?.debug(\"retry aborted\", { reason: signal.reason });\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 * Logs a `next(...)` decision: `debug` when retrying, `warn` when exhausted.\n * Shared by both the throw-mode and non-throw retry loops.\n */\nconst logRetryDecision = (\n logger: Logger | undefined,\n attempt: number,\n decision: RetryDecision,\n error: unknown\n): void => {\n if (decision.shouldRetry) {\n logger?.debug(\"retry scheduled\", {\n attempt,\n delayMs: decision.delayMs,\n reason: decision.reason,\n });\n trace.getActiveSpan()?.addEvent(\"retry.scheduled\", {\n attempt,\n \"retry.delay_ms\": decision.delayMs,\n \"retry.reason\": decision.reason ?? \"\",\n });\n recordRetryAttempt(\"retry\");\n return;\n }\n\n logger?.warn(\"retry policy exhausted\", {\n attempts: attempt,\n error,\n reason: decision.reason,\n });\n trace.getActiveSpan()?.addEvent(\"retry.exhausted\", {\n attempt,\n \"retry.reason\": decision.reason ?? \"\",\n });\n recordRetryAttempt(\"exhausted\");\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 * @param logger - Optional logger; logs each retry decision at `debug` and\n * exhaustion at `warn`.\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 logger?: Logger\n): Promise<T> => {\n let attempt = 1;\n\n while (true) {\n throwIfAborted(signal, logger);\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, logger);\n\n if (!policy.isKnownError(error)) {\n throw error;\n }\n\n const decision = policy.next({\n attempt,\n error,\n });\n logRetryDecision(logger, attempt, decision, error);\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 * @param logger - Optional logger; logs the abort at `debug`.\n * @returns Failure result or `undefined` if not aborted.\n */\nconst buildAbortResult = (\n signal: AbortSignal | undefined,\n attempts: number,\n logger?: Logger\n): RetryRunResult<never> | undefined => {\n if (signal?.aborted !== true) {\n return undefined;\n }\n\n logger?.debug(\"retry aborted\", { reason: signal.reason });\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 * @param logger - Optional logger; logs an abort ending the wait at `debug`.\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 logger?: Logger\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, logger);\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 * @param params.logger - Optional logger; logs each retry decision at\n * `debug`, exhaustion at `warn`, and cancellation at `debug`.\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 logger: Logger | undefined;\n }\n): Promise<RetryRunResult<never> | undefined> => {\n const { attempt, error, sleep, signal, logger } = params;\n const abortResult = buildAbortResult(signal, attempt, logger);\n if (abortResult !== undefined) {\n return abortResult;\n }\n\n const decision = policy.next({\n attempt,\n error,\n });\n logRetryDecision(logger, attempt, decision, error);\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 logger\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 * @param logger - Optional logger; logs each retry decision at `debug`,\n * exhaustion at `warn`, and cancellation at `debug`.\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 logger?: Logger\n): Promise<RetryRunResult<T>> => {\n let attempt = 1;\n\n while (true) {\n const abortResult = buildAbortResult(\n signal,\n Math.max(0, attempt - 1),\n logger\n );\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, logger);\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 logger,\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, logger } = 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, logger);\n }\n\n return await runThrowMode(resolvedPolicy, execute, sleep, signal, logger);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-policy.d.ts","names":[],"sources":["../src/base-policy.ts"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"base-policy.d.ts","names":[],"sources":["../src/base-policy.ts"],"mappings":";;;;;;;;;;;;;;;cAiCa,eAAsB,oBAAkB;;;;;;;;;;;;;iBA4crC,eACd,GACA,eAAe,QAAQ,OACvB,iBAEA,QAAQ,YAAY,QAAQ,QAC5B,UAAU,oBAAoB,QAAQ,IACtC,SAAS;EAAoB;IAC5B,QAAQ,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgCV,eACd,GACA,eAAe,QAAQ,OACvB,iBAEA,QAAQ,YAAY,QAAQ,QAC5B,UAAU,oBAAoB,QAAQ,IACtC,UAAU;EAAoB;IAC7B,QAAQ"}
|
package/dist/base-policy.js
CHANGED
|
@@ -1,334 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Awaits a timer-based delay, unless `delayMs` is non-positive.
|
|
5
|
-
*
|
|
6
|
-
* @param delayMs - Milliseconds to wait before resolving.
|
|
7
|
-
* @returns Promise that resolves when the delay completes.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```ts
|
|
11
|
-
* import { defaultSleep } from "@zap-studio/retry";
|
|
12
|
-
*
|
|
13
|
-
* await defaultSleep(250); // waits 250ms
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
const defaultSleep = async (delayMs) => {
|
|
17
|
-
if (delayMs <= 0) return;
|
|
18
|
-
await new Promise((resolve) => {
|
|
19
|
-
setTimeout(resolve, delayMs);
|
|
20
|
-
});
|
|
21
|
-
};
|
|
22
|
-
/**
|
|
23
|
-
* Normalizes an abort `reason` into an `AbortError`.
|
|
24
|
-
*/
|
|
25
|
-
const toAbortError = (reason) => {
|
|
26
|
-
if (reason instanceof AbortError) return reason;
|
|
27
|
-
if (reason instanceof Error) return new AbortError(reason.message, { cause: reason });
|
|
28
|
-
if (typeof reason === "string" && reason.length > 0) return new AbortError(reason);
|
|
29
|
-
if (reason === void 0) return new AbortError("Retry aborted.");
|
|
30
|
-
try {
|
|
31
|
-
return new AbortError(`Retry aborted: ${JSON.stringify(reason)}`);
|
|
32
|
-
} catch {
|
|
33
|
-
return new AbortError("Retry aborted.");
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Throws when the provided abort signal is already aborted.
|
|
38
|
-
*
|
|
39
|
-
* @param signal - Optional abort signal to inspect.
|
|
40
|
-
* @param logger - Optional logger; logs the abort at `debug` before throwing.
|
|
41
|
-
* @throws {AbortError} When the signal is aborted.
|
|
42
|
-
*/
|
|
43
|
-
const throwIfAborted = (signal, logger) => {
|
|
44
|
-
if (signal?.aborted !== true) return;
|
|
45
|
-
logger?.debug("retry aborted", { reason: signal.reason });
|
|
46
|
-
throw toAbortError(signal.reason);
|
|
47
|
-
};
|
|
48
|
-
/**
|
|
49
|
-
* Waits for delay sleep while observing cancellation through an abort signal.
|
|
50
|
-
*
|
|
51
|
-
* @param sleep - Sleep function used to await `delayMs`.
|
|
52
|
-
* @param delayMs - Delay duration in milliseconds.
|
|
53
|
-
* @param signal - Abort signal to observe while waiting.
|
|
54
|
-
* @returns Promise that resolves when delay finishes.
|
|
55
|
-
* @throws {AbortError} When the signal aborts before or during wait.
|
|
56
|
-
*/
|
|
57
|
-
const sleepWithAbortSignal = async (sleep, delayMs, signal) => {
|
|
58
|
-
if (signal.aborted) throw toAbortError(signal.reason);
|
|
59
|
-
let onAbort;
|
|
60
|
-
try {
|
|
61
|
-
await Promise.race([sleep(delayMs), new Promise((_resolve, reject) => {
|
|
62
|
-
onAbort = () => {
|
|
63
|
-
reject(toAbortError(signal.reason));
|
|
64
|
-
};
|
|
65
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
66
|
-
})]);
|
|
67
|
-
} finally {
|
|
68
|
-
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
/**
|
|
72
|
-
* Logs a `next(...)` decision: `debug` when retrying, `warn` when exhausted.
|
|
73
|
-
* Shared by both the throw-mode and non-throw retry loops.
|
|
74
|
-
*/
|
|
75
|
-
const logRetryDecision = (logger, attempt, decision, error) => {
|
|
76
|
-
if (decision.shouldRetry) {
|
|
77
|
-
logger?.debug("retry scheduled", {
|
|
78
|
-
attempt,
|
|
79
|
-
delayMs: decision.delayMs,
|
|
80
|
-
reason: decision.reason
|
|
81
|
-
});
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
logger?.warn("retry policy exhausted", {
|
|
85
|
-
attempts: attempt,
|
|
86
|
-
error,
|
|
87
|
-
reason: decision.reason
|
|
88
|
-
});
|
|
89
|
-
};
|
|
90
|
-
/**
|
|
91
|
-
* Runs the throw-mode retry loop: throws `RetryError` on exhaustion and
|
|
92
|
-
* `AbortError` when `signal` aborts.
|
|
93
|
-
*
|
|
94
|
-
* @param policy - Resolved retry policy providing `next` and `onExhausted`.
|
|
95
|
-
* @param execute - Async work callback per attempt.
|
|
96
|
-
* @param sleep - Delay function between retries.
|
|
97
|
-
* @param signal - Optional cancel signal.
|
|
98
|
-
* @param logger - Optional logger; logs each retry decision at `debug` and
|
|
99
|
-
* exhaustion at `warn`.
|
|
100
|
-
* @returns Resolves to the first successful return value.
|
|
101
|
-
* @throws {RetryError} When retries are exhausted and `onExhausted` returns
|
|
102
|
-
* the terminal error.
|
|
103
|
-
* @throws {AbortError} When `signal` is already aborted or aborts while waiting.
|
|
104
|
-
* @throws {Error} Any error thrown by `next`, `onExhausted`, or `sleep`. Also
|
|
105
|
-
* rethrows the original caught value immediately, bypassing retry, when
|
|
106
|
-
* `policy.isKnownError` rejects it as outside this policy's error domain.
|
|
107
|
-
*/
|
|
108
|
-
const runThrowMode = async (policy, execute, sleep, signal, logger) => {
|
|
109
|
-
let attempt = 1;
|
|
110
|
-
while (true) {
|
|
111
|
-
throwIfAborted(signal, logger);
|
|
112
|
-
try {
|
|
113
|
-
return await execute(attempt);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
throwIfAborted(signal, logger);
|
|
116
|
-
if (!policy.isKnownError(error)) throw error;
|
|
117
|
-
const decision = policy.next({
|
|
118
|
-
attempt,
|
|
119
|
-
error
|
|
120
|
-
});
|
|
121
|
-
logRetryDecision(logger, attempt, decision, error);
|
|
122
|
-
if (!decision.shouldRetry) throw policy.onExhausted({
|
|
123
|
-
attempts: attempt,
|
|
124
|
-
error
|
|
125
|
-
});
|
|
126
|
-
if (decision.delayMs > 0) await (signal === void 0 ? sleep(decision.delayMs) : sleepWithAbortSignal(sleep, decision.delayMs, signal));
|
|
127
|
-
attempt += 1;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
/**
|
|
132
|
-
* When `signal` is already aborted, builds the terminal `{ ok: false }` object
|
|
133
|
-
* with a normalized `AbortError` on `error`.
|
|
134
|
-
*
|
|
135
|
-
* @param signal - Optional abort signal; only acts when `aborted` is set.
|
|
136
|
-
* @param attempts - Number of finished attempts to report in the result.
|
|
137
|
-
* @param logger - Optional logger; logs the abort at `debug`.
|
|
138
|
-
* @returns Failure result or `undefined` if not aborted.
|
|
139
|
-
*/
|
|
140
|
-
const buildAbortResult = (signal, attempts, logger) => {
|
|
141
|
-
if (signal?.aborted !== true) return;
|
|
142
|
-
logger?.debug("retry aborted", { reason: signal.reason });
|
|
143
|
-
return {
|
|
144
|
-
attempts,
|
|
145
|
-
error: toAbortError(signal.reason),
|
|
146
|
-
ok: false
|
|
147
|
-
};
|
|
148
|
-
};
|
|
149
|
-
/**
|
|
150
|
-
* Runs one `execute(attempt)` call and returns either a success value or a
|
|
151
|
-
* captured error without rethrowing.
|
|
152
|
-
*
|
|
153
|
-
* @param execute - User work callback.
|
|
154
|
-
* @param attempt - One-based attempt number passed to `execute`.
|
|
155
|
-
* @returns A tagged success with `value` or a tagged failure with `error`.
|
|
156
|
-
*/
|
|
157
|
-
const runAttempt = async (execute, attempt) => {
|
|
158
|
-
try {
|
|
159
|
-
return {
|
|
160
|
-
ok: true,
|
|
161
|
-
value: await execute(attempt)
|
|
162
|
-
};
|
|
163
|
-
} catch (error) {
|
|
164
|
-
return {
|
|
165
|
-
error,
|
|
166
|
-
ok: false
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
/**
|
|
171
|
-
* Awaits inter-attempt delay in result mode, mapping an abort during wait to
|
|
172
|
-
* a terminal result instead of throwing when `throwOnExhausted` is false.
|
|
173
|
-
*
|
|
174
|
-
* @param sleep - Custom or default sleep implementation.
|
|
175
|
-
* @param delayMs - Milliseconds to wait.
|
|
176
|
-
* @param signal - If set, `sleep` is raced with the abort signal.
|
|
177
|
-
* @param attempts - Attempt count to attach if the wait ends in abort.
|
|
178
|
-
* @param logger - Optional logger; logs an abort ending the wait at `debug`.
|
|
179
|
-
* @returns A terminal result when canceled during the wait, otherwise
|
|
180
|
-
* `undefined`.
|
|
181
|
-
* @throws {Error} The underlying `sleep` rejection when it is not an abort.
|
|
182
|
-
*/
|
|
183
|
-
const waitForDelay = async (sleep, delayMs, signal, attempts, logger) => {
|
|
184
|
-
if (signal === void 0) {
|
|
185
|
-
await sleep(delayMs);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
189
|
-
await sleepWithAbortSignal(sleep, delayMs, signal);
|
|
190
|
-
return;
|
|
191
|
-
} catch (error) {
|
|
192
|
-
const aborted = buildAbortResult(signal, attempts, logger);
|
|
193
|
-
if (aborted !== void 0) return aborted;
|
|
194
|
-
throw error;
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
/**
|
|
198
|
-
* After a failed attempt, applies abort rules, `next`, optional delay, and
|
|
199
|
-
* either returns a terminal `RetryRunResult` or `undefined` to continue.
|
|
200
|
-
*
|
|
201
|
-
* @param policy - Resolved retry policy hooks (`next`, `onExhausted`).
|
|
202
|
-
* @param params - Failure context for the current attempt.
|
|
203
|
-
* @param params.attempt - Current attempt number.
|
|
204
|
-
* @param params.error - Error thrown by the attempt.
|
|
205
|
-
* @param params.sleep - Delay function between retries.
|
|
206
|
-
* @param params.signal - Optional abort signal.
|
|
207
|
-
* @param params.logger - Optional logger; logs each retry decision at
|
|
208
|
-
* `debug`, exhaustion at `warn`, and cancellation at `debug`.
|
|
209
|
-
* @returns Terminal non-throw result if the loop should stop, otherwise
|
|
210
|
-
* `undefined` to schedule another attempt.
|
|
211
|
-
* @throws {Error} Any error thrown by `next`, `onExhausted`, or a custom `sleep` when
|
|
212
|
-
* the error is not an abort.
|
|
213
|
-
*/
|
|
214
|
-
const handleFailure = async (policy, params) => {
|
|
215
|
-
const { attempt, error, sleep, signal, logger } = params;
|
|
216
|
-
const abortResult = buildAbortResult(signal, attempt, logger);
|
|
217
|
-
if (abortResult !== void 0) return abortResult;
|
|
218
|
-
const decision = policy.next({
|
|
219
|
-
attempt,
|
|
220
|
-
error
|
|
221
|
-
});
|
|
222
|
-
logRetryDecision(logger, attempt, decision, error);
|
|
223
|
-
if (!decision.shouldRetry) return {
|
|
224
|
-
attempts: attempt,
|
|
225
|
-
error: policy.onExhausted({
|
|
226
|
-
attempts: attempt,
|
|
227
|
-
error
|
|
228
|
-
}),
|
|
229
|
-
ok: false
|
|
230
|
-
};
|
|
231
|
-
if (decision.delayMs > 0) {
|
|
232
|
-
const delayAbortResult = await waitForDelay(sleep, decision.delayMs, signal, attempt, logger);
|
|
233
|
-
if (delayAbortResult !== void 0) return delayAbortResult;
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
/**
|
|
237
|
-
* Runs the non-throw retry loop, returning
|
|
238
|
-
* `RetryRunResult`.
|
|
239
|
-
*
|
|
240
|
-
* @param policy - Resolved retry policy providing `next` and `onExhausted`.
|
|
241
|
-
* @param execute - Async work callback per attempt.
|
|
242
|
-
* @param sleep - Delay function between retries.
|
|
243
|
-
* @param signal - Optional cancel signal.
|
|
244
|
-
* @param logger - Optional logger; logs each retry decision at `debug`,
|
|
245
|
-
* exhaustion at `warn`, and cancellation at `debug`.
|
|
246
|
-
* @returns Terminal success or failure object. When `policy.isKnownError`
|
|
247
|
-
* rejects a caught value as outside this policy's error domain, the
|
|
248
|
-
* original value is wrapped in a `RetryError` and returned on
|
|
249
|
-
* `result.error` immediately, bypassing retry — never thrown.
|
|
250
|
-
* @throws {Error} Any error thrown by `next`, `onExhausted`, or a non-abort `sleep`
|
|
251
|
-
* failure.
|
|
252
|
-
*/
|
|
253
|
-
const runResultMode = async (policy, execute, sleep, signal, logger) => {
|
|
254
|
-
let attempt = 1;
|
|
255
|
-
while (true) {
|
|
256
|
-
const abortResult = buildAbortResult(signal, Math.max(0, attempt - 1), logger);
|
|
257
|
-
if (abortResult !== void 0) return abortResult;
|
|
258
|
-
const execution = await runAttempt(execute, attempt);
|
|
259
|
-
if (execution.ok) return {
|
|
260
|
-
ok: true,
|
|
261
|
-
value: execution.value
|
|
262
|
-
};
|
|
263
|
-
const attemptAbortResult = buildAbortResult(signal, attempt, logger);
|
|
264
|
-
if (attemptAbortResult !== void 0) return attemptAbortResult;
|
|
265
|
-
if (!policy.isKnownError(execution.error)) return {
|
|
266
|
-
attempts: attempt,
|
|
267
|
-
error: new RetryError("Retry policy encountered an unknown error.", {
|
|
268
|
-
attempts: attempt,
|
|
269
|
-
lastError: execution.error
|
|
270
|
-
}),
|
|
271
|
-
ok: false
|
|
272
|
-
};
|
|
273
|
-
const failure = await handleFailure(policy, {
|
|
274
|
-
attempt,
|
|
275
|
-
error: execution.error,
|
|
276
|
-
logger,
|
|
277
|
-
signal,
|
|
278
|
-
sleep
|
|
279
|
-
});
|
|
280
|
-
if (failure !== void 0) return failure;
|
|
281
|
-
attempt += 1;
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
/**
|
|
285
|
-
* Default `onExhausted` used when a policy omits it: wraps the exhaustion
|
|
286
|
-
* context in a generic `RetryError`.
|
|
287
|
-
*/
|
|
288
|
-
const defaultOnExhausted = (input) => new RetryError("Retry policy exhausted all attempts.", {
|
|
289
|
-
attempts: input.attempts,
|
|
290
|
-
lastData: input.data,
|
|
291
|
-
lastError: input.error
|
|
292
|
-
});
|
|
293
|
-
/**
|
|
294
|
-
* Default `isKnownError` used when a policy omits it: accepts any `Error`
|
|
295
|
-
* instance and rejects everything else.
|
|
296
|
-
*/
|
|
297
|
-
const defaultIsKnownError = (error) => error instanceof Error;
|
|
298
|
-
/**
|
|
299
|
-
* Runs retry orchestration in non-throw mode.
|
|
300
|
-
*
|
|
301
|
-
* When `throwOnExhausted` is `false`, returns a discriminated result union.
|
|
302
|
-
*
|
|
303
|
-
* @param policy - Retry policy: `next` is required, `onExhausted` and
|
|
304
|
-
* `isKnownError` fall back to their defaults when omitted.
|
|
305
|
-
* @param execute - Async function to execute per attempt.
|
|
306
|
-
* @param options - Runner settings.
|
|
307
|
-
* @returns Success value or terminal result object based on option mode.
|
|
308
|
-
* @throws {Error} Any error thrown by `next`, by `onExhausted`, or by a custom `sleep`
|
|
309
|
-
* function. When `throwOnExhausted` is `false`, exhaustion itself is returned
|
|
310
|
-
* as `{ ok: false }` instead of thrown.
|
|
311
|
-
* Cancellation is returned as `{ ok: false, error: AbortError }` in non-throw
|
|
312
|
-
* mode. A value rejected by `policy.isKnownError` is wrapped in a
|
|
313
|
-
* `RetryError` and returned the same way in non-throw mode; in throw mode
|
|
314
|
-
* it is rethrown as-is.
|
|
315
|
-
*
|
|
316
|
-
* @example
|
|
317
|
-
* const result = await runRetryPolicy(policy, doWork, { throwOnExhausted: false });
|
|
318
|
-
* if (!result.ok) console.error(result.error);
|
|
319
|
-
*/
|
|
320
|
-
async function runRetryPolicy(policy, execute, options = {}) {
|
|
321
|
-
const sleep = options.sleep ?? defaultSleep;
|
|
322
|
-
const { signal, logger } = options;
|
|
323
|
-
const resolvedPolicy = {
|
|
324
|
-
isKnownError: (error) => policy.isKnownError ? policy.isKnownError(error) : defaultIsKnownError(error),
|
|
325
|
-
next: (input) => policy.next(input),
|
|
326
|
-
onExhausted: (input) => policy.onExhausted ? policy.onExhausted(input) : defaultOnExhausted(input)
|
|
327
|
-
};
|
|
328
|
-
if (options.throwOnExhausted === false) return await runResultMode(resolvedPolicy, execute, sleep, signal, logger);
|
|
329
|
-
return await runThrowMode(resolvedPolicy, execute, sleep, signal, logger);
|
|
330
|
-
}
|
|
331
|
-
//#endregion
|
|
1
|
+
import { n as runRetryPolicy, t as defaultSleep } from "./base-policy-CiDqMVSD.js";
|
|
2
|
+
import "./errors.js";
|
|
332
3
|
export { defaultSleep, runRetryPolicy };
|
|
333
|
-
|
|
334
|
-
//# sourceMappingURL=base-policy.js.map
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { n as runRetryPolicy, t as defaultSleep } from "./base-policy-CiDqMVSD.js";
|
|
1
2
|
import { AbortError, RetryError } from "./errors.js";
|
|
2
|
-
import { defaultSleep, runRetryPolicy } from "./base-policy.js";
|
|
3
3
|
import { applyJitter } from "./jitter.js";
|
|
4
4
|
import { exponentialBackoff } from "./exponential-backoff.js";
|
|
5
5
|
import { fixedDelay } from "./fixed-delay.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/retry",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Composable, tree-shakeable retry policies for resilient async operations.",
|
|
6
6
|
"keywords": [
|
|
@@ -43,15 +43,26 @@
|
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"@zap-studio/logger": "1.0.0"
|
|
48
|
-
},
|
|
49
46
|
"devDependencies": {
|
|
47
|
+
"@opentelemetry/api": "^1.9.0",
|
|
48
|
+
"@opentelemetry/context-async-hooks": "^2.10.0",
|
|
49
|
+
"@opentelemetry/sdk-metrics": "^2.10.0",
|
|
50
|
+
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
50
51
|
"tsdown": "^0.22.14",
|
|
51
52
|
"typescript": "^7.0.2",
|
|
52
53
|
"vitest": "^4.1.10",
|
|
54
|
+
"@zap-studio/logger": "2.0.0",
|
|
53
55
|
"@zap-studio/typescript": "0.0.0"
|
|
54
56
|
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"@opentelemetry/api": "^1.9.0",
|
|
59
|
+
"@zap-studio/logger": "2.0.0"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"@zap-studio/logger": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
55
66
|
"engines": {
|
|
56
67
|
"node": ">=18.0.0"
|
|
57
68
|
}
|
package/dist/base-policy.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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 type { Logger } from \"@zap-studio/logger\";\n\nimport { AbortError, RetryError } from \"./errors.js\";\nimport type {\n ResolvedRetryPolicy,\n RetryDecision,\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 * @param logger - Optional logger; logs the abort at `debug` before throwing.\n * @throws {AbortError} When the signal is aborted.\n */\nconst throwIfAborted = (signal?: AbortSignal, logger?: Logger): void => {\n if (signal?.aborted !== true) {\n return;\n }\n\n logger?.debug(\"retry aborted\", { reason: signal.reason });\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 * Logs a `next(...)` decision: `debug` when retrying, `warn` when exhausted.\n * Shared by both the throw-mode and non-throw retry loops.\n */\nconst logRetryDecision = (\n logger: Logger | undefined,\n attempt: number,\n decision: RetryDecision,\n error: unknown\n): void => {\n if (decision.shouldRetry) {\n logger?.debug(\"retry scheduled\", {\n attempt,\n delayMs: decision.delayMs,\n reason: decision.reason,\n });\n return;\n }\n\n logger?.warn(\"retry policy exhausted\", {\n attempts: attempt,\n error,\n reason: decision.reason,\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 * @param logger - Optional logger; logs each retry decision at `debug` and\n * exhaustion at `warn`.\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 logger?: Logger\n): Promise<T> => {\n let attempt = 1;\n\n while (true) {\n throwIfAborted(signal, logger);\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, logger);\n\n if (!policy.isKnownError(error)) {\n throw error;\n }\n\n const decision = policy.next({\n attempt,\n error,\n });\n logRetryDecision(logger, attempt, decision, error);\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 * @param logger - Optional logger; logs the abort at `debug`.\n * @returns Failure result or `undefined` if not aborted.\n */\nconst buildAbortResult = (\n signal: AbortSignal | undefined,\n attempts: number,\n logger?: Logger\n): RetryRunResult<never> | undefined => {\n if (signal?.aborted !== true) {\n return undefined;\n }\n\n logger?.debug(\"retry aborted\", { reason: signal.reason });\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 * @param logger - Optional logger; logs an abort ending the wait at `debug`.\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 logger?: Logger\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, logger);\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 * @param params.logger - Optional logger; logs each retry decision at\n * `debug`, exhaustion at `warn`, and cancellation at `debug`.\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 logger: Logger | undefined;\n }\n): Promise<RetryRunResult<never> | undefined> => {\n const { attempt, error, sleep, signal, logger } = params;\n const abortResult = buildAbortResult(signal, attempt, logger);\n if (abortResult !== undefined) {\n return abortResult;\n }\n\n const decision = policy.next({\n attempt,\n error,\n });\n logRetryDecision(logger, attempt, decision, error);\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 logger\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 * @param logger - Optional logger; logs each retry decision at `debug`,\n * exhaustion at `warn`, and cancellation at `debug`.\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 logger?: Logger\n): Promise<RetryRunResult<T>> => {\n let attempt = 1;\n\n while (true) {\n const abortResult = buildAbortResult(\n signal,\n Math.max(0, attempt - 1),\n logger\n );\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, logger);\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 logger,\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, logger } = 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, logger);\n }\n\n return await runThrowMode(resolvedPolicy, execute, sleep, signal, logger);\n}\n"],"mappings":"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"}
|