@zap-studio/retry 2.1.0 → 2.1.2
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 +60 -47
- package/README.md +17 -20
- package/dist/{base-policy-DkxBg9dh.js → base-policy-ROp4XL19.js} +8 -57
- package/dist/base-policy-ROp4XL19.js.map +1 -0
- package/dist/base-policy.d.ts +4 -44
- package/dist/base-policy.d.ts.map +1 -1
- package/dist/base-policy.js +2 -2
- package/dist/exponential-backoff.d.ts +2 -3
- package/dist/exponential-backoff.d.ts.map +1 -1
- package/dist/fixed-delay.d.ts +2 -3
- package/dist/fixed-delay.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/jitter.d.ts +3 -4
- package/dist/jitter.d.ts.map +1 -1
- package/dist/linear-backoff.d.ts +2 -3
- package/dist/linear-backoff.d.ts.map +1 -1
- package/dist/types.d.ts +7 -19
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -15
- package/dist/base-policy-DkxBg9dh.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,39 +4,52 @@ 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
|
-
|
|
7
|
+
|
|
8
|
+
## 2.1.2
|
|
9
|
+
<sub>2026-09-13</sub>
|
|
10
|
+
|
|
11
|
+
- [#613](https://github.com/zap-studio/monorepo/pull/613) *(patch)* Thanks [@alexandretrotel](https://github.com/alexandretrotel)!
|
|
12
|
+
Widen internal peer dependency ranges from an exact pin to a caret range, so consumers no longer resolve a duplicate copy of the peer on version skew.
|
|
13
|
+
|
|
14
|
+
## [2.1.1]
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
We removed the `@zap-studio/monads` dependency and the `runRetryPolicyResult` export from 2.1.0. They added a dependency and more bundle size, for something you can already do yourself: wrap `runRetryPolicy` with `@zap-studio/monads`'s `fromPromise`. See the README section "Using with `@zap-studio/monads`". 2.1.0 is deprecated on npm — use this release instead.
|
|
19
|
+
|
|
20
|
+
## [2.1.0] (deprecated — see 2.1.1)
|
|
8
21
|
|
|
9
22
|
### Added
|
|
10
23
|
|
|
11
|
-
- Added `runRetryPolicyResult(policy, execute, options?)
|
|
24
|
+
- Added `runRetryPolicyResult(policy, execute, options?)`. It is like `runRetryPolicy`, but returns a `ResultAsync` instead, using the new `@zap-studio/monads` dependency. This is new and optional — `runRetryPolicy` (both throw and non-throw modes) stays the same. `runRetryPolicyResult` has no `throwOnExhausted` option; it always returns a `Result`. The `Err` value is the same `RetryError`/`AbortError` object that `runRetryPolicy`'s throw mode would throw, so `RetryError.attempts`/`lastError`/`lastData` and `AbortError.cause` are kept.
|
|
12
25
|
|
|
13
26
|
## [2.0.0]
|
|
14
27
|
|
|
15
28
|
### Added
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
Added native OpenTelemetry support. `fetch`, `webhooks`, and `permit` each create their own span. This package does not, because a retry loop wraps someone else's work. Instead, 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).
|
|
18
31
|
|
|
19
32
|
### Changed
|
|
20
33
|
|
|
21
|
-
**Breaking:** `@opentelemetry/api` is now a required peer dependency. It
|
|
34
|
+
**Breaking:** `@opentelemetry/api` is now a required peer dependency. It is small, has no side effects, and does nothing until an app sets up a real SDK, so nothing changes at runtime if you don't set one up. But the package won't resolve without it installed: `npm install @opentelemetry/api`.
|
|
22
35
|
|
|
23
36
|
## [1.2.1]
|
|
24
37
|
|
|
25
38
|
### Changed
|
|
26
39
|
|
|
27
|
-
`@zap-studio/logger` is now an optional peer dependency
|
|
40
|
+
`@zap-studio/logger` is now an optional peer dependency, not a regular one. Every import from it is type-only (`import type { Logger }`), so it was never loaded at runtime — you can pass any object with the `Logger` shape (including `pino`), with no install needed. If you already use `logger?: Logger`, nothing changes for you.
|
|
28
41
|
|
|
29
42
|
## [1.2.0]
|
|
30
43
|
|
|
31
44
|
### Added
|
|
32
45
|
|
|
33
|
-
`runRetryPolicy(...)`
|
|
46
|
+
`runRetryPolicy(...)` now takes an optional `logger?: Logger` option (from `@zap-studio/logger`). If you pass it, it logs each retry decision at `debug` level (attempt, delay, reason), exhaustion at `warn`, and cancellation at `debug`. If you don't pass it, there is no extra logging cost. See [Logging](https://www.zapstudio.dev/retry/logging).
|
|
34
47
|
|
|
35
48
|
## [1.1.0]
|
|
36
49
|
|
|
37
50
|
### Added
|
|
38
51
|
|
|
39
|
-
`exponentialBackoff(...)` and `linearBackoff(...)`
|
|
52
|
+
`exponentialBackoff(...)` and `linearBackoff(...)` now take an optional `jitter?: "full" | "equal" | JitterOptions` option. It changes the delay after the delay is capped at `maxDelayMs`. `"full"` picks a random value in `[0, delayMs]`. `"equal"` keeps at least half the delay, and picks a random value in `[delayMs/2, delayMs]`. Pass `{ mode, random }` to use your own random source — useful for tests that need the same result every time.
|
|
40
53
|
|
|
41
54
|
New export: `applyJitter(delayMs, jitter?)`, also available from the `./jitter` subpath. See [Jitter](https://www.zapstudio.dev/retry/jitter).
|
|
42
55
|
|
|
@@ -44,44 +57,44 @@ New export: `applyJitter(delayMs, jitter?)`, also available from the `./jitter`
|
|
|
44
57
|
|
|
45
58
|
### Added
|
|
46
59
|
|
|
47
|
-
`RetryPolicy`
|
|
60
|
+
`RetryPolicy` now has an optional `isKnownError?: (error: unknown) => error is TError` hook. `BaseRetryPolicy.run(...)` calls it before it hands a caught value to `next(...)`/`onExhausted(...)`. By default, `BaseRetryPolicy.isKnownError` checks `error instanceof Error`.
|
|
48
61
|
|
|
49
|
-
- Override `isKnownError` when `TError` is a narrower subclass
|
|
62
|
+
- Override `isKnownError` when `TError` is a narrower subclass, like an HTTP error or a domain-specific error. This gives you real type narrowing, instead of just assuming `instanceof Error`, and stops unrelated `Error` types from being retried as if they belonged to your domain.
|
|
50
63
|
|
|
51
|
-
See [Narrow the Error Domain](https://www.zapstudio.dev/retry/custom-policies#narrow-the-error-domain) for
|
|
64
|
+
See [Narrow the Error Domain](https://www.zapstudio.dev/retry/custom-policies#narrow-the-error-domain) for how to override it.
|
|
52
65
|
|
|
53
|
-
New built-in policy: `linearBackoff(options)
|
|
66
|
+
New built-in policy: `linearBackoff(options)`. It adds a fixed `incrementMs` to the delay after each failed attempt, up to `maxDelayMs` — steadier growth than `exponentialBackoff`, bigger gaps than `fixedDelay`. See [linearBackoff](https://www.zapstudio.dev/retry/linear-backoff).
|
|
54
67
|
|
|
55
68
|
### Changed
|
|
56
69
|
|
|
57
|
-
- **Breaking:** `TError`
|
|
58
|
-
- **Breaking behavior change:** a rejection
|
|
59
|
-
- **Breaking:** Policies are plain objects
|
|
60
|
-
- **Breaking:** `RetryPolicy.onExhausted` is now optional (
|
|
70
|
+
- **Breaking:** `TError` must now extend `Error`, and defaults to `Error` (before, it was `TError = unknown`). This applies to `RetryPolicy`, `RetryDecisionInput`, `RetryExhaustedInput`, and `BaseRetryPolicy`.
|
|
71
|
+
- **Breaking behavior change:** if a rejection has a non-`Error` value (a thrown string, a plain object, `undefined`, and so on), that attempt now skips retry completely — it no longer reaches `next(...)`, and no delay or backoff is applied. Before, any thrown value went to the policy unchanged. In throw mode, `run(...)` now rethrows the value as-is. With `throwOnExhausted: false`, it wraps the value in a `RetryError` and returns it on `result.error` — `run(...)` never throws in that mode.
|
|
72
|
+
- **Breaking:** Policies are now plain objects, not classes. This helps tree-shaking: a bundler can drop an unused policy factory and its defaults completely, which it could not do with a shared class hierarchy. `ExponentialBackoff`/`FixedDelay` classes are gone — use the `exponentialBackoff(options)`/`fixedDelay(options)` factory functions instead, both returning a `RetryPolicy`. Change `new ExponentialBackoff(opts)` to `exponentialBackoff(opts)`, and `new FixedDelay(opts)` to `fixedDelay(opts)`.
|
|
73
|
+
- **Breaking:** `RetryPolicy.onExhausted` is now optional (before, it was required). Leave it out to get the same default `RetryError` that `BaseRetryPolicy.onExhausted` used to build.
|
|
61
74
|
|
|
62
75
|
### Removed
|
|
63
76
|
|
|
64
|
-
- **Breaking:** `BaseRetryPolicy` is removed. Retry
|
|
77
|
+
- **Breaking:** `BaseRetryPolicy` is removed. Retry runs now go through the standalone function `runRetryPolicy(policy, execute, options?)`, which accepts any object matching `RetryPolicy` — no subclass needed. Change `policy.run(execute, options)` to `runRetryPolicy(policy, execute, options)`. If your custom policy extended `BaseRetryPolicy` and overrode `next`/`onExhausted`/`isKnownError`, turn it into a plain object with the same members instead; see [Custom Policies](https://www.zapstudio.dev/retry/custom-policies).
|
|
65
78
|
|
|
66
79
|
### Removed
|
|
67
80
|
|
|
68
|
-
|
|
81
|
+
Moved abort/sleep internals out of the public API.
|
|
69
82
|
|
|
70
83
|
- Removed the `./abort` and `./sleep` subpath exports.
|
|
71
|
-
- Removed the public `sleepWithAbortSignal`, `throwIfAborted`, and `toAbortError` exports — they were
|
|
72
|
-
- `defaultSleep` is
|
|
84
|
+
- Removed the public `sleepWithAbortSignal`, `throwIfAborted`, and `toAbortError` exports — they were internal parts of the retry loop, not standalone tools, and nothing outside the retry loop used them.
|
|
85
|
+
- `defaultSleep` is not affected. It is still exported from `@zap-studio/retry` (no separate subpath).
|
|
73
86
|
|
|
74
87
|
## [0.3.2]
|
|
75
88
|
|
|
76
89
|
### Added
|
|
77
90
|
|
|
78
|
-
The package root now re-exports the full public API, so
|
|
91
|
+
The package root now re-exports the full public API, so you can import everything straight from `@zap-studio/retry` (`BaseRetryPolicy`, `ExponentialBackoff`, `FixedDelay`, `RetryError`, `AbortError`, abort helpers, `defaultSleep`, and all public types). Every export has no side effects and supports tree-shaking; the smaller subpath imports still work too.
|
|
79
92
|
|
|
80
|
-
- `BaseRetryPolicy` moved
|
|
93
|
+
- `BaseRetryPolicy` moved out of the main entrypoint. It now has its own module, at the new `./base-policy` subpath.
|
|
81
94
|
|
|
82
95
|
### Removed
|
|
83
96
|
|
|
84
|
-
- Removed the `./result-mode` and `./throw-mode` subpath exports. Both
|
|
97
|
+
- Removed the `./result-mode` and `./throw-mode` subpath exports. Both held internal code (`runResultMode`, `runThrowMode`) that is no longer part of the public API.
|
|
85
98
|
|
|
86
99
|
## [0.3.1]
|
|
87
100
|
|
|
@@ -93,32 +106,32 @@ Internal formatting and lint cleanup only. No public API or behavior change.
|
|
|
93
106
|
|
|
94
107
|
### Changed
|
|
95
108
|
|
|
96
|
-
- **Breaking:**
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
- In non-throw mode,
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
- Split the retry runner into
|
|
104
|
-
-
|
|
105
|
-
-
|
|
109
|
+
- **Breaking:** New subpath for error types: use `@zap-studio/retry/errors` (plural) for `RetryError`, `AbortError`, and related types. The old JSR `error` subpath pointed at an `error.ts` file that did not exist, so it is removed — change deep imports from `@zap-studio/retry/error` to `@zap-studio/retry/errors`.
|
|
110
|
+
- Added a dedicated `AbortError` type. Cancellation is now consistent: retry internals throw or return `RetryError` or `AbortError`, not a plain `Error`.
|
|
111
|
+
- `defaultSleep` is now only exposed from the `@zap-studio/retry/sleep` subpath — the main entry does not re-export it. `run` still uses it inside, when you don't pass your own `sleep`.
|
|
112
|
+
- Made non-throw exhaustion data consistent: `result.attempts` and `result.error.attempts` now always match for `RetryError` outcomes.
|
|
113
|
+
- In non-throw mode, cancellation now returns a normalized `AbortError` on `result.error`. `result.attempts` still shows the number of completed attempts.
|
|
114
|
+
- Split the result-mode internals into smaller helper functions, to make the code simpler and easier to keep up.
|
|
115
|
+
- Added more docs in the README and the package docs pages about `AbortError` behavior, in both throw and non-throw modes.
|
|
116
|
+
- Split the retry runner into separate modules: `throw-mode` (the throwing path), `result-mode` (the non-throw `RetryRunResult` path), and `sleep` (the default `defaultSleep` code). `BaseRetryPolicy` in `index` now calls these modules, with no change in public behavior.
|
|
117
|
+
- Added full TSDoc for `result-mode` and other `src` modules: private helpers, policy option and state fields, and every `RetryRunResult` union member.
|
|
118
|
+
- Reworked the test layout into `sleep`, `throw-mode`, `result-mode`, and `index` test files, with one shared `sequence-policy` fixture, replacing the old combined `index` and `abort` test files.
|
|
106
119
|
|
|
107
120
|
## [0.2.0]
|
|
108
121
|
|
|
109
122
|
### Changed
|
|
110
123
|
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
124
|
+
- Made the retry runner's hot paths faster: throw and non-throw modes now run separately, and a sleep call is skipped when the delay is zero or less.
|
|
125
|
+
- Added `AbortSignal` support to `run(...)`, so you can cancel a retry before or between attempts.
|
|
126
|
+
- Added retry benchmarks, with core and ecosystem scenarios, including real-world and fair-mode comparisons.
|
|
127
|
+
- Added abort-focused benchmarks that compare signal overhead and instant cancellation.
|
|
128
|
+
- Added more TSDoc for the new runner internals in this release.
|
|
116
129
|
|
|
117
130
|
## [0.1.2]
|
|
118
131
|
|
|
119
132
|
### Changed
|
|
120
133
|
|
|
121
|
-
-
|
|
134
|
+
- Added more TSDoc across retry modules and exported types, for more complete JSR docs.
|
|
122
135
|
|
|
123
136
|
## [0.1.1]
|
|
124
137
|
|
|
@@ -134,12 +147,12 @@ Internal formatting and lint cleanup only. No public API or behavior change.
|
|
|
134
147
|
|
|
135
148
|
### Added
|
|
136
149
|
|
|
137
|
-
-
|
|
138
|
-
- Added required `onExhausted` hook
|
|
139
|
-
- Added shared `BaseRetryPolicy` abstract class to
|
|
140
|
-
- Added `BaseRetryPolicy.run(execute, options)`
|
|
141
|
-
- Added `throwOnExhausted`
|
|
142
|
-
- Added `ExponentialBackoff` policy
|
|
143
|
-
- Added `FixedDelay` policy with
|
|
144
|
-
- Added `RetryError` for
|
|
145
|
-
- Documented
|
|
150
|
+
- Added a transport-agnostic `RetryPolicy` contract, with `RetryDecisionInput` and `RetryDecision`.
|
|
151
|
+
- Added a required `onExhausted` hook on `RetryPolicy`, so each policy can shape its own final error.
|
|
152
|
+
- Added a shared `BaseRetryPolicy` abstract class, to hold the default `onExhausted` behavior in one place.
|
|
153
|
+
- Added a `BaseRetryPolicy.run(execute, options)` method, to run a retry policy with little setup code.
|
|
154
|
+
- Added a `throwOnExhausted` option, plus a non-throw `RetryRunResult<T>` mode.
|
|
155
|
+
- Added an `ExponentialBackoff` policy. Its delay grows with `baseDelayMs`, up to `maxDelayMs`, and stops at `maxAttempts`.
|
|
156
|
+
- Added a `FixedDelay` policy, with a fixed delay and a limited number of attempts.
|
|
157
|
+
- Added `RetryError` for a failure after retries run out, holding the attempt count, the last error, and the last data.
|
|
158
|
+
- Documented when `RetryPolicy`, `BaseRetryPolicy.run`, and related types throw, with `@throws` tags for policy errors, exhaustion, and custom `sleep` failures.
|
package/README.md
CHANGED
|
@@ -27,7 +27,6 @@ npm install @zap-studio/retry
|
|
|
27
27
|
- **A shared runner** via `runRetryPolicy(policy, execute, options?)` with attempt-aware callbacks and custom sleep injection.
|
|
28
28
|
- **Structured terminal errors**: `RetryError` on exhaustion, `AbortError` on cancellation.
|
|
29
29
|
- **Non-throw mode** (`throwOnExhausted: false`) returns a `RetryRunResult` instead of throwing.
|
|
30
|
-
- **`Result`-returning variant** via `runRetryPolicyResult(policy, execute, options?)`, for explicit error handling with [`@zap-studio/monads`](https://www.npmjs.com/package/@zap-studio/monads) instead of throw/catch.
|
|
31
30
|
- **Cancellation** through `AbortSignal`, checked before, between, and during retries.
|
|
32
31
|
- **Custom policies** as plain objects implementing `RetryPolicy` — just a `next(...)` function, no subclassing.
|
|
33
32
|
- **Optional logging** via a `logger?: Logger` option ([`@zap-studio/logger`](https://www.npmjs.com/package/@zap-studio/logger)) — omit it and there's zero logging overhead.
|
|
@@ -135,25 +134,6 @@ if (!result.ok) {
|
|
|
135
134
|
}
|
|
136
135
|
```
|
|
137
136
|
|
|
138
|
-
## Result-Returning Variant
|
|
139
|
-
|
|
140
|
-
`runRetryPolicyResult(policy, execute, options?)` — additive alternative to `runRetryPolicy` for consumers who prefer explicit [`Result`](https://www.zapstudio.dev/monads/result)/[`ResultAsync`](https://www.zapstudio.dev/monads/result-async) values over throw/catch. There's no `throwOnExhausted` option — it always returns a `Result`.
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
import { isOk } from "@zap-studio/monads";
|
|
144
|
-
import { runRetryPolicyResult } from "@zap-studio/retry";
|
|
145
|
-
|
|
146
|
-
const result = await runRetryPolicyResult(policy, execute);
|
|
147
|
-
|
|
148
|
-
if (isOk(result)) {
|
|
149
|
-
console.log(result.value);
|
|
150
|
-
} else {
|
|
151
|
-
console.error(result.error); // RetryError | AbortError, same as runRetryPolicy's throw mode
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
For exhaustion and cancellation, `Err` contains the same `RetryError`/`AbortError` object `runRetryPolicy`'s throw mode would throw — `RetryError.attempts`/`lastError`/`lastData` and `AbortError.cause` are preserved. A value rejected by `policy.isKnownError` is instead wrapped in a new `RetryError`, because throw mode rethrows that value as-is.
|
|
156
|
-
|
|
157
137
|
## Cancellation
|
|
158
138
|
|
|
159
139
|
Through `AbortSignal`, checked before, between, and during retries.
|
|
@@ -228,6 +208,23 @@ const policy = exponentialBackoff({ maxAttempts: 5, baseDelayMs: 100 });
|
|
|
228
208
|
await runRetryPolicy(policy, execute);
|
|
229
209
|
```
|
|
230
210
|
|
|
211
|
+
## Using with `@zap-studio/monads`
|
|
212
|
+
|
|
213
|
+
This package has no dependency on `@zap-studio/monads` — nothing is added to your
|
|
214
|
+
bundle unless you install it yourself. If you want a `Result` instead of throw/catch,
|
|
215
|
+
wrap the call with `@zap-studio/monads`'s `fromPromise`, mapping the rejection into
|
|
216
|
+
your error type:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
import { fromPromise } from "@zap-studio/monads";
|
|
220
|
+
import { runRetryPolicy } from "@zap-studio/retry";
|
|
221
|
+
|
|
222
|
+
const result = fromPromise(
|
|
223
|
+
runRetryPolicy(policy, execute, { throwOnExhausted: true }),
|
|
224
|
+
(error) => error,
|
|
225
|
+
);
|
|
226
|
+
```
|
|
227
|
+
|
|
231
228
|
## Runtime Support
|
|
232
229
|
|
|
233
230
|
| Runtime | Minimum version |
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { AbortError, RetryError } from "./errors.js";
|
|
2
2
|
import { metrics, trace } from "@opentelemetry/api";
|
|
3
|
-
import { ResultAsync, err, ok } from "@zap-studio/monads";
|
|
4
3
|
//#region package.json
|
|
5
4
|
var name = "@zap-studio/retry";
|
|
6
|
-
var version = "2.1.
|
|
5
|
+
var version = "2.1.2";
|
|
7
6
|
//#endregion
|
|
8
7
|
//#region src/_otel.ts
|
|
9
8
|
/**
|
|
@@ -368,15 +367,6 @@ const defaultOnExhausted = (input) => new RetryError("Retry policy exhausted all
|
|
|
368
367
|
*/
|
|
369
368
|
const defaultIsKnownError = (error) => error instanceof Error;
|
|
370
369
|
/**
|
|
371
|
-
* Applies `runRetryPolicy`/`runRetryPolicyResult`'s shared defaults
|
|
372
|
-
* (`onExhausted`, `isKnownError`) to a caller-supplied `RetryPolicy`.
|
|
373
|
-
*/
|
|
374
|
-
const resolvePolicy = (policy) => ({
|
|
375
|
-
isKnownError: (error) => policy.isKnownError ? policy.isKnownError(error) : defaultIsKnownError(error),
|
|
376
|
-
next: (input) => policy.next(input),
|
|
377
|
-
onExhausted: (input) => policy.onExhausted ? policy.onExhausted(input) : defaultOnExhausted(input)
|
|
378
|
-
});
|
|
379
|
-
/**
|
|
380
370
|
* Runs retry orchestration in non-throw mode.
|
|
381
371
|
*
|
|
382
372
|
* When `throwOnExhausted` is `false`, returns a discriminated result union.
|
|
@@ -401,54 +391,15 @@ const resolvePolicy = (policy) => ({
|
|
|
401
391
|
async function runRetryPolicy(policy, execute, options = {}) {
|
|
402
392
|
const sleep = options.sleep ?? defaultSleep;
|
|
403
393
|
const { signal, logger } = options;
|
|
404
|
-
const resolvedPolicy =
|
|
394
|
+
const resolvedPolicy = {
|
|
395
|
+
isKnownError: (error) => policy.isKnownError ? policy.isKnownError(error) : defaultIsKnownError(error),
|
|
396
|
+
next: (input) => policy.next(input),
|
|
397
|
+
onExhausted: (input) => policy.onExhausted ? policy.onExhausted(input) : defaultOnExhausted(input)
|
|
398
|
+
};
|
|
405
399
|
if (options.throwOnExhausted === false) return await runResultMode(resolvedPolicy, execute, sleep, signal, logger);
|
|
406
400
|
return await runThrowMode(resolvedPolicy, execute, sleep, signal, logger);
|
|
407
401
|
}
|
|
408
|
-
/**
|
|
409
|
-
* Runs retry orchestration, returning a `ResultAsync` instead of throwing or
|
|
410
|
-
* returning the hand-rolled {@link RetryRunResult} union.
|
|
411
|
-
*
|
|
412
|
-
* Additive alternative to `runRetryPolicy` for consumers who prefer explicit
|
|
413
|
-
* `Result`/`ResultAsync` values (from `@zap-studio/monads`) over throw/catch.
|
|
414
|
-
* There's no `throwOnExhausted` option — this function always returns a
|
|
415
|
-
* `Result`, so the flag doesn't apply.
|
|
416
|
-
*
|
|
417
|
-
* @param policy - Retry policy: `next` is required, `onExhausted` and
|
|
418
|
-
* `isKnownError` fall back to their defaults when omitted.
|
|
419
|
-
* @param execute - Async function to execute per attempt.
|
|
420
|
-
* @param options - Runner settings, same as {@link RetryRunOptions} minus
|
|
421
|
-
* `throwOnExhausted`.
|
|
422
|
-
* @returns A `ResultAsync` resolving to `Ok` with the successful value, or `Err`
|
|
423
|
-
* with a `RetryError` (exhaustion) or `AbortError` (cancellation) — the same
|
|
424
|
-
* error object `runRetryPolicy`'s throw mode would throw. When
|
|
425
|
-
* `policy.isKnownError` rejects a caught value, it is wrapped in a new
|
|
426
|
-
* `RetryError` and returned on `Err` instead — in throw mode that same value
|
|
427
|
-
* is rethrown unchanged, not wrapped.
|
|
428
|
-
* @throws {Error} Any error thrown by `next`, `onExhausted`, or a custom `sleep`
|
|
429
|
-
* function.
|
|
430
|
-
*
|
|
431
|
-
* @example
|
|
432
|
-
* ```ts
|
|
433
|
-
* import { runRetryPolicyResult } from "@zap-studio/retry";
|
|
434
|
-
*
|
|
435
|
-
* const result = await runRetryPolicyResult(policy, async () => fetchFlakyResource());
|
|
436
|
-
*
|
|
437
|
-
* if (isOk(result)) {
|
|
438
|
-
* console.log(result.value);
|
|
439
|
-
* } else {
|
|
440
|
-
* console.error(result.error);
|
|
441
|
-
* }
|
|
442
|
-
* ```
|
|
443
|
-
*/
|
|
444
|
-
const runRetryPolicyResult = (policy, execute, options = {}) => new ResultAsync((async () => {
|
|
445
|
-
const sleep = options.sleep ?? defaultSleep;
|
|
446
|
-
const { signal, logger } = options;
|
|
447
|
-
const resolvedPolicy = resolvePolicy(policy);
|
|
448
|
-
const result = await runResultMode(resolvedPolicy, execute, sleep, signal, logger);
|
|
449
|
-
return result.ok ? ok(result.value) : err(result.error);
|
|
450
|
-
})());
|
|
451
402
|
//#endregion
|
|
452
|
-
export { runRetryPolicy as n,
|
|
403
|
+
export { runRetryPolicy as n, defaultSleep as t };
|
|
453
404
|
|
|
454
|
-
//# sourceMappingURL=base-policy-
|
|
405
|
+
//# sourceMappingURL=base-policy-ROp4XL19.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-policy-ROp4XL19.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 type { Logger } from \"@zap-studio/logger\";\n\nimport { trace } from \"@opentelemetry/api\";\n\nimport type {\n ResolvedRetryPolicy,\n RetryDecision,\n RetryExhaustedInput,\n RetryPolicy,\n RetryRunOptions,\n RetryRunResult,\n} from \"./types.ts\";\n\nimport { recordRetryAttempt } from \"./_otel.ts\";\nimport { AbortError, RetryError } from \"./errors.ts\";\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 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 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 return await execute(attempt);\n } catch (error) {\n throwIfAborted(signal, logger);\n\n if (!policy.isKnownError(error)) {\n throw error;\n }\n\n await handleThrowModeRetry(policy, { attempt, error, logger, signal, sleep });\n attempt += 1;\n }\n }\n};\n\n/**\n * After a failed, known-domain attempt in throw mode, applies the policy's\n * retry decision: throws the terminal error from `onExhausted` on\n * exhaustion, otherwise waits out the retry delay so the caller's loop can\n * continue.\n *\n * @param policy - Resolved retry policy providing `next` and `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, already known to the policy's domain.\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 `debug`\n * and exhaustion at `warn`.\n * @throws {RetryError} When retries are exhausted and `onExhausted` returns\n * the terminal error.\n * @throws {AbortError} When `signal` aborts while waiting for the retry delay.\n * @throws {Error} Any error thrown by `next`, `onExhausted`, or `sleep`.\n */\nconst handleThrowModeRetry = 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<void> => {\n const { attempt, error, sleep, signal, logger } = params;\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 await (signal === undefined\n ? sleep(decision.delayMs)\n : sleepWithAbortSignal(sleep, decision.delayMs, signal));\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(sleep, decision.delayMs, signal, attempt, logger);\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(signal, Math.max(0, attempt - 1), logger);\n if (abortResult !== undefined) {\n return abortResult;\n }\n\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 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 */\nconst defaultIsKnownError = <TError extends Error>(error: unknown): error is TError =>\n 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<T, TError extends Error = Error, TData = unknown>(\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<T, TError extends Error = Error, TData = unknown>(\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<T, TError extends Error = Error, TData = unknown>(\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 ? policy.isKnownError(error) : defaultIsKnownError(error),\n next: (input) => policy.next(input),\n onExhausted: (input) =>\n policy.onExhausted ? policy.onExhausted(input) : 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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"}
|
package/dist/base-policy.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { RetryPolicy, RetryRunOptions, RetryRunResult, RetryRunResultOptions } from "./types.js";
|
|
3
|
-
import { ResultAsync } from "@zap-studio/monads";
|
|
1
|
+
import { RetryPolicy, RetryRunOptions, RetryRunResult } from "./types.js";
|
|
4
2
|
//#region src/base-policy.d.ts
|
|
5
3
|
/**
|
|
6
4
|
* Awaits a timer-based delay, unless `delayMs` is non-positive.
|
|
@@ -15,7 +13,7 @@ import { ResultAsync } from "@zap-studio/monads";
|
|
|
15
13
|
* await defaultSleep(250); // waits 250ms
|
|
16
14
|
* ```
|
|
17
15
|
*/
|
|
18
|
-
declare const defaultSleep: (delayMs: number) => Promise<void>;
|
|
16
|
+
export declare const defaultSleep: (delayMs: number) => Promise<void>;
|
|
19
17
|
/**
|
|
20
18
|
* Runs retry orchestration in non-throw mode.
|
|
21
19
|
*
|
|
@@ -28,7 +26,7 @@ declare const defaultSleep: (delayMs: number) => Promise<void>;
|
|
|
28
26
|
* `RetryError` and returned as the terminal failure instead of thrown.
|
|
29
27
|
* @throws {Error} Any error thrown by `next`, `onExhausted`, or a custom `sleep`.
|
|
30
28
|
*/
|
|
31
|
-
declare function runRetryPolicy<T, TError extends Error = Error, TData = unknown>(policy: RetryPolicy<TError, TData>, execute: (attempt: number) => Promise<T>, options: RetryRunOptions & {
|
|
29
|
+
export declare function runRetryPolicy<T, TError extends Error = Error, TData = unknown>(policy: RetryPolicy<TError, TData>, execute: (attempt: number) => Promise<T>, options: RetryRunOptions & {
|
|
32
30
|
throwOnExhausted: false;
|
|
33
31
|
}): Promise<RetryRunResult<T>>;
|
|
34
32
|
/**
|
|
@@ -61,46 +59,8 @@ declare function runRetryPolicy<T, TError extends Error = Error, TData = unknown
|
|
|
61
59
|
* const data = await runRetryPolicy(linearBackoff, async () => fetchFlakyResource());
|
|
62
60
|
* ```
|
|
63
61
|
*/
|
|
64
|
-
declare function runRetryPolicy<T, TError extends Error = Error, TData = unknown>(policy: RetryPolicy<TError, TData>, execute: (attempt: number) => Promise<T>, options?: RetryRunOptions & {
|
|
62
|
+
export declare function runRetryPolicy<T, TError extends Error = Error, TData = unknown>(policy: RetryPolicy<TError, TData>, execute: (attempt: number) => Promise<T>, options?: RetryRunOptions & {
|
|
65
63
|
throwOnExhausted?: true;
|
|
66
64
|
}): Promise<T>;
|
|
67
|
-
/**
|
|
68
|
-
* Runs retry orchestration, returning a `ResultAsync` instead of throwing or
|
|
69
|
-
* returning the hand-rolled {@link RetryRunResult} union.
|
|
70
|
-
*
|
|
71
|
-
* Additive alternative to `runRetryPolicy` for consumers who prefer explicit
|
|
72
|
-
* `Result`/`ResultAsync` values (from `@zap-studio/monads`) over throw/catch.
|
|
73
|
-
* There's no `throwOnExhausted` option — this function always returns a
|
|
74
|
-
* `Result`, so the flag doesn't apply.
|
|
75
|
-
*
|
|
76
|
-
* @param policy - Retry policy: `next` is required, `onExhausted` and
|
|
77
|
-
* `isKnownError` fall back to their defaults when omitted.
|
|
78
|
-
* @param execute - Async function to execute per attempt.
|
|
79
|
-
* @param options - Runner settings, same as {@link RetryRunOptions} minus
|
|
80
|
-
* `throwOnExhausted`.
|
|
81
|
-
* @returns A `ResultAsync` resolving to `Ok` with the successful value, or `Err`
|
|
82
|
-
* with a `RetryError` (exhaustion) or `AbortError` (cancellation) — the same
|
|
83
|
-
* error object `runRetryPolicy`'s throw mode would throw. When
|
|
84
|
-
* `policy.isKnownError` rejects a caught value, it is wrapped in a new
|
|
85
|
-
* `RetryError` and returned on `Err` instead — in throw mode that same value
|
|
86
|
-
* is rethrown unchanged, not wrapped.
|
|
87
|
-
* @throws {Error} Any error thrown by `next`, `onExhausted`, or a custom `sleep`
|
|
88
|
-
* function.
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* ```ts
|
|
92
|
-
* import { runRetryPolicyResult } from "@zap-studio/retry";
|
|
93
|
-
*
|
|
94
|
-
* const result = await runRetryPolicyResult(policy, async () => fetchFlakyResource());
|
|
95
|
-
*
|
|
96
|
-
* if (isOk(result)) {
|
|
97
|
-
* console.log(result.value);
|
|
98
|
-
* } else {
|
|
99
|
-
* console.error(result.error);
|
|
100
|
-
* }
|
|
101
|
-
* ```
|
|
102
|
-
*/
|
|
103
|
-
declare const runRetryPolicyResult: <T, TError extends Error = Error, TData = unknown>(policy: RetryPolicy<TError, TData>, execute: (attempt: number) => Promise<T>, options?: RetryRunResultOptions) => ResultAsync<T, RetryError | AbortError>;
|
|
104
65
|
//#endregion
|
|
105
|
-
export { defaultSleep, runRetryPolicy, runRetryPolicyResult };
|
|
106
66
|
//# sourceMappingURL=base-policy.d.ts.map
|
|
@@ -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":";;;;;;;;;;;;;;;qBAmCa,eAAsB,oBAAkB;;;;;;;;;;;;;wBA0drC,eAAe,GAAG,eAAe,QAAQ,OAAO,iBAC9D,QAAQ,YAAY,QAAQ,QAC5B,UAAU,oBAAoB,QAAQ,IACtC,SAAS;EAAoB;IAC5B,QAAQ,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAgCV,eAAe,GAAG,eAAe,QAAQ,OAAO,iBAC9D,QAAQ,YAAY,QAAQ,QAC5B,UAAU,oBAAoB,QAAQ,IACtC,UAAU;EAAoB;IAC7B,QAAQ"}
|
package/dist/base-policy.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { n as runRetryPolicy,
|
|
1
|
+
import { n as runRetryPolicy, t as defaultSleep } from "./base-policy-ROp4XL19.js";
|
|
2
2
|
import "./errors.js";
|
|
3
|
-
export { defaultSleep, runRetryPolicy
|
|
3
|
+
export { defaultSleep, runRetryPolicy };
|
|
@@ -12,7 +12,7 @@ import { JitterMode, JitterOptions } from "./jitter.js";
|
|
|
12
12
|
* jitter: "full",
|
|
13
13
|
* };
|
|
14
14
|
*/
|
|
15
|
-
interface ExponentialBackoffOptions {
|
|
15
|
+
export interface ExponentialBackoffOptions {
|
|
16
16
|
/**
|
|
17
17
|
* Maximum number of attempts (including the first) before giving up.
|
|
18
18
|
*/
|
|
@@ -41,7 +41,6 @@ interface ExponentialBackoffOptions {
|
|
|
41
41
|
* maxDelayMs: 2_000,
|
|
42
42
|
* });
|
|
43
43
|
*/
|
|
44
|
-
declare const exponentialBackoff: (options: ExponentialBackoffOptions) => RetryPolicy;
|
|
44
|
+
export declare const exponentialBackoff: (options: ExponentialBackoffOptions) => RetryPolicy;
|
|
45
45
|
//#endregion
|
|
46
|
-
export { ExponentialBackoffOptions, exponentialBackoff };
|
|
47
46
|
//# sourceMappingURL=exponential-backoff.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exponential-backoff.d.ts","names":[],"sources":["../src/exponential-backoff.ts"],"mappings":";;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"exponential-backoff.d.ts","names":[],"sources":["../src/exponential-backoff.ts"],"mappings":";;;;;;;;;;;;;;iBAsBiB;;;;EAIf;;;;EAIA;;;;EAIA;;;;;EAKA,SAAS,aAAa;;;;;;;;;;;;qBAaX,qBAAsB,SAAS,8BAA4B"}
|
package/dist/fixed-delay.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { RetryPolicy } from "./types.js";
|
|
|
6
6
|
* @example
|
|
7
7
|
* const options: FixedDelayOptions = { maxAttempts: 3, delayMs: 250 };
|
|
8
8
|
*/
|
|
9
|
-
interface FixedDelayOptions {
|
|
9
|
+
export interface FixedDelayOptions {
|
|
10
10
|
/**
|
|
11
11
|
* Maximum number of attempts (including the first) before giving up.
|
|
12
12
|
*/
|
|
@@ -25,7 +25,6 @@ interface FixedDelayOptions {
|
|
|
25
25
|
* delayMs: 250,
|
|
26
26
|
* });
|
|
27
27
|
*/
|
|
28
|
-
declare const fixedDelay: (options: FixedDelayOptions) => RetryPolicy;
|
|
28
|
+
export declare const fixedDelay: (options: FixedDelayOptions) => RetryPolicy;
|
|
29
29
|
//#endregion
|
|
30
|
-
export { FixedDelayOptions, fixedDelay };
|
|
31
30
|
//# sourceMappingURL=fixed-delay.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixed-delay.d.ts","names":[],"sources":["../src/fixed-delay.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"fixed-delay.d.ts","names":[],"sources":["../src/fixed-delay.ts"],"mappings":";;;;;;;;iBAciB;;;;EAIf;;;;EAIA;;;;;;;;;;;qBAYW,aAAc,SAAS,sBAAoB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
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
|
|
3
|
-
import { defaultSleep, runRetryPolicy
|
|
2
|
+
import { RetryDecision, RetryDecisionInput, RetryExhaustedInput, RetryPolicy, RetryRunOptions, RetryRunResult } from "./types.js";
|
|
3
|
+
import { defaultSleep, runRetryPolicy } from "./base-policy.js";
|
|
4
4
|
import { JitterMode, JitterOptions, applyJitter } from "./jitter.js";
|
|
5
5
|
import { ExponentialBackoffOptions, exponentialBackoff } from "./exponential-backoff.js";
|
|
6
6
|
import { FixedDelayOptions, fixedDelay } from "./fixed-delay.js";
|
|
7
7
|
import { LinearBackoffOptions, linearBackoff } from "./linear-backoff.js";
|
|
8
|
-
export { AbortError, type AbortErrorContext, type ExponentialBackoffOptions, type FixedDelayOptions, type JitterMode, type JitterOptions, type LinearBackoffOptions, type RetryDecision, type RetryDecisionInput, RetryError, type RetryErrorContext, type RetryExhaustedInput, type RetryPolicy, type RetryRunOptions, type RetryRunResult,
|
|
8
|
+
export { AbortError, type AbortErrorContext, type ExponentialBackoffOptions, type FixedDelayOptions, type JitterMode, type JitterOptions, type LinearBackoffOptions, type RetryDecision, type RetryDecisionInput, RetryError, type RetryErrorContext, type RetryExhaustedInput, type RetryPolicy, type RetryRunOptions, type RetryRunResult, applyJitter, defaultSleep, exponentialBackoff, fixedDelay, linearBackoff, runRetryPolicy };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as runRetryPolicy,
|
|
1
|
+
import { n as runRetryPolicy, t as defaultSleep } from "./base-policy-ROp4XL19.js";
|
|
2
2
|
import { AbortError, RetryError } from "./errors.js";
|
|
3
3
|
import { applyJitter } from "./jitter.js";
|
|
4
4
|
import { exponentialBackoff } from "./exponential-backoff.js";
|
|
5
5
|
import { fixedDelay } from "./fixed-delay.js";
|
|
6
6
|
import { linearBackoff } from "./linear-backoff.js";
|
|
7
|
-
export { AbortError, RetryError, applyJitter, defaultSleep, exponentialBackoff, fixedDelay, linearBackoff, runRetryPolicy
|
|
7
|
+
export { AbortError, RetryError, applyJitter, defaultSleep, exponentialBackoff, fixedDelay, linearBackoff, runRetryPolicy };
|
package/dist/jitter.d.ts
CHANGED
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
* - `"equal"`: `delayMs/2 + random(0, delayMs/2)` — keeps a floor at half
|
|
13
13
|
* the computed delay, less spread than full jitter.
|
|
14
14
|
*/
|
|
15
|
-
type JitterMode = "equal" | "full";
|
|
15
|
+
export type JitterMode = "equal" | "full";
|
|
16
16
|
/**
|
|
17
17
|
* Configuration for jitter application.
|
|
18
18
|
*
|
|
19
19
|
* @example
|
|
20
20
|
* const jitter: JitterOptions = { mode: "full" };
|
|
21
21
|
*/
|
|
22
|
-
interface JitterOptions {
|
|
22
|
+
export interface JitterOptions {
|
|
23
23
|
/**
|
|
24
24
|
* Jitter strategy to apply.
|
|
25
25
|
*/
|
|
@@ -42,7 +42,6 @@ interface JitterOptions {
|
|
|
42
42
|
* @example
|
|
43
43
|
* const delayMs = applyJitter(1000, "full"); // 0-1000
|
|
44
44
|
*/
|
|
45
|
-
declare const applyJitter: (delayMs: number, jitter?: JitterMode | JitterOptions) => number;
|
|
45
|
+
export declare const applyJitter: (delayMs: number, jitter?: JitterMode | JitterOptions) => number;
|
|
46
46
|
//#endregion
|
|
47
|
-
export { JitterMode, JitterOptions, applyJitter };
|
|
48
47
|
//# sourceMappingURL=jitter.d.ts.map
|
package/dist/jitter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jitter.d.ts","names":[],"sources":["../src/jitter.ts"],"mappings":";;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"jitter.d.ts","names":[],"sources":["../src/jitter.ts"],"mappings":";;;;;;;;;;;;;;YAcY;;;;;;;iBAQK;;;;EAIf,MAAM;;;;;;EAMN;;;;;;;;;;;;;qBAcW,cAAe,iBAAiB,SAAS,aAAa"}
|
package/dist/linear-backoff.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { JitterMode, JitterOptions } from "./jitter.js";
|
|
|
13
13
|
* jitter: "equal",
|
|
14
14
|
* };
|
|
15
15
|
*/
|
|
16
|
-
interface LinearBackoffOptions {
|
|
16
|
+
export interface LinearBackoffOptions {
|
|
17
17
|
/**
|
|
18
18
|
* Maximum number of attempts (including the first) before giving up.
|
|
19
19
|
*/
|
|
@@ -47,7 +47,6 @@ interface LinearBackoffOptions {
|
|
|
47
47
|
* maxDelayMs: 2_000,
|
|
48
48
|
* });
|
|
49
49
|
*/
|
|
50
|
-
declare const linearBackoff: (options: LinearBackoffOptions) => RetryPolicy;
|
|
50
|
+
export declare const linearBackoff: (options: LinearBackoffOptions) => RetryPolicy;
|
|
51
51
|
//#endregion
|
|
52
|
-
export { LinearBackoffOptions, linearBackoff };
|
|
53
52
|
//# sourceMappingURL=linear-backoff.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"linear-backoff.d.ts","names":[],"sources":["../src/linear-backoff.ts"],"mappings":";;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"linear-backoff.d.ts","names":[],"sources":["../src/linear-backoff.ts"],"mappings":";;;;;;;;;;;;;;;iBAuBiB;;;;EAIf;;;;EAIA;;;;EAIA;;;;EAIA;;;;;EAKA,SAAS,aAAa;;;;;;;;;;;;;qBAcX,gBAAiB,SAAS,yBAAuB"}
|
package/dist/types.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { Logger } from "@zap-studio/logger";
|
|
|
14
14
|
* onExhausted: ({ attempts }) => new RetryError("done", { attempts }),
|
|
15
15
|
* };
|
|
16
16
|
*/
|
|
17
|
-
interface RetryPolicy<TError extends Error = Error, TData = unknown> {
|
|
17
|
+
export interface RetryPolicy<TError extends Error = Error, TData = unknown> {
|
|
18
18
|
/**
|
|
19
19
|
* Returns the retry decision for a failed attempt.
|
|
20
20
|
*
|
|
@@ -47,7 +47,7 @@ interface RetryPolicy<TError extends Error = Error, TData = unknown> {
|
|
|
47
47
|
* `RetryPolicy` with `onExhausted` and `isKnownError` resolved to concrete
|
|
48
48
|
* functions, used internally once `runRetryPolicy` has applied defaults.
|
|
49
49
|
*/
|
|
50
|
-
interface ResolvedRetryPolicy<TError extends Error, TData> {
|
|
50
|
+
export interface ResolvedRetryPolicy<TError extends Error, TData> {
|
|
51
51
|
/**
|
|
52
52
|
* Returns the retry decision for a failed attempt.
|
|
53
53
|
*/
|
|
@@ -69,7 +69,7 @@ interface ResolvedRetryPolicy<TError extends Error, TData> {
|
|
|
69
69
|
* @example
|
|
70
70
|
* const decision: RetryDecision = { shouldRetry: true, delayMs: 200, reason: "retry" };
|
|
71
71
|
*/
|
|
72
|
-
interface RetryDecision {
|
|
72
|
+
export interface RetryDecision {
|
|
73
73
|
/**
|
|
74
74
|
* When `true`, the runner may schedule another attempt (subject to
|
|
75
75
|
* `delayMs` and the runner's abort rules).
|
|
@@ -90,7 +90,7 @@ interface RetryDecision {
|
|
|
90
90
|
* @example
|
|
91
91
|
* const input: RetryDecisionInput = { attempt: 2, error: new Error("timeout") };
|
|
92
92
|
*/
|
|
93
|
-
interface RetryDecisionInput<TError extends Error = Error, TData = unknown> {
|
|
93
|
+
export interface RetryDecisionInput<TError extends Error = Error, TData = unknown> {
|
|
94
94
|
/**
|
|
95
95
|
* One-based attempt number for the current failure.
|
|
96
96
|
*/
|
|
@@ -117,7 +117,7 @@ interface RetryDecisionInput<TError extends Error = Error, TData = unknown> {
|
|
|
117
117
|
* @example
|
|
118
118
|
* const input: RetryExhaustedInput = { attempts: 5, error: new Error("timeout") };
|
|
119
119
|
*/
|
|
120
|
-
interface RetryExhaustedInput<TError extends Error = Error, TData = unknown> {
|
|
120
|
+
export interface RetryExhaustedInput<TError extends Error = Error, TData = unknown> {
|
|
121
121
|
/**
|
|
122
122
|
* Count of completed attempts that led to stopping retries.
|
|
123
123
|
*/
|
|
@@ -137,7 +137,7 @@ interface RetryExhaustedInput<TError extends Error = Error, TData = unknown> {
|
|
|
137
137
|
* @example
|
|
138
138
|
* const options: RetryRunOptions = { throwOnExhausted: false, signal: controller.signal };
|
|
139
139
|
*/
|
|
140
|
-
interface RetryRunOptions {
|
|
140
|
+
export interface RetryRunOptions {
|
|
141
141
|
/**
|
|
142
142
|
* Delay function used between retry attempts.
|
|
143
143
|
*
|
|
@@ -166,17 +166,6 @@ interface RetryRunOptions {
|
|
|
166
166
|
*/
|
|
167
167
|
readonly logger?: Logger;
|
|
168
168
|
}
|
|
169
|
-
/**
|
|
170
|
-
* Options for `runRetryPolicyResult(...)`.
|
|
171
|
-
*
|
|
172
|
-
* Same as {@link RetryRunOptions} minus `throwOnExhausted`, which doesn't
|
|
173
|
-
* apply — `runRetryPolicyResult` always returns a `Result`, never throws for
|
|
174
|
-
* exhaustion or abort.
|
|
175
|
-
*
|
|
176
|
-
* @example
|
|
177
|
-
* const options: RetryRunResultOptions = { signal: controller.signal };
|
|
178
|
-
*/
|
|
179
|
-
type RetryRunResultOptions = Omit<RetryRunOptions, "throwOnExhausted">;
|
|
180
169
|
/**
|
|
181
170
|
* Result union returned by non-throw runner mode.
|
|
182
171
|
*
|
|
@@ -188,7 +177,7 @@ type RetryRunResultOptions = Omit<RetryRunOptions, "throwOnExhausted">;
|
|
|
188
177
|
* const result: RetryRunResult<string> = await runRetryPolicy(policy, doWork, { throwOnExhausted: false });
|
|
189
178
|
* if (!result.ok) console.error(result.error);
|
|
190
179
|
*/
|
|
191
|
-
type RetryRunResult<T> = {
|
|
180
|
+
export type RetryRunResult<T> = {
|
|
192
181
|
/**
|
|
193
182
|
* Discriminator for a successful run.
|
|
194
183
|
*/
|
|
@@ -213,5 +202,4 @@ type RetryRunResult<T> = {
|
|
|
213
202
|
attempts: number;
|
|
214
203
|
};
|
|
215
204
|
//#endregion
|
|
216
|
-
export { ResolvedRetryPolicy, RetryDecision, RetryDecisionInput, RetryExhaustedInput, RetryPolicy, RetryRunOptions, RetryRunResult, RetryRunResultOptions };
|
|
217
205
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;;;;;iBAuBiB,YAAY,eAAe,QAAQ,OAAO;;;;;;EAMzD,OAAO,OAAO,mBAAmB,QAAQ,WAAW;;;;;;;;EAQpD,eAAe,OAAO,oBAAoB,QAAQ,WAAW;;;;;;;;;;;;;EAa7D,gBAAgB,mBAAmB,SAAS;;;;;;iBAO7B,oBAAoB,eAAe,OAAO;;;;EAIzD,MAAM,YAAY,QAAQ;;;;;EAK1B,cAAc,OAAO,oBAAoB,QAAQ,WAAW;;;;;EAK5D,eAAe,mBAAmB,SAAS;;;;;;;;iBAS5B;;;;;WAKN;;;;WAIA;;;;WAIA;;;;;;;;iBASM,mBAAmB,eAAe,QAAQ,OAAO;;;;WAIvD;;;;;WAKA;;;;;WAKA,QAAQ;;;;;WAKR,OAAO;;;;;;;;iBASD,oBAAoB,eAAe,QAAQ,OAAO;;;;WAIxD;;;;WAIA,QAAQ;;;;WAIR,OAAO;;;;;;;;iBASD;;;;;;WAMN,SAAS,oBAAoB;;;;;;WAM7B,SAAS;;;;;;;;WAQT;;;;;;;WAOA,SAAS;;;;;;;;;;;;;YAcR,eAAe;;;;EAKrB;;;;EAIA,OAAO;;;;;EAMP;;;;;EAKA,OAAO,aAAa;;;;EAIpB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/retry",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Composable, tree-shakeable retry policies for resilient async operations.",
|
|
6
6
|
"keywords": [
|
|
@@ -43,23 +43,20 @@
|
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"@zap-studio/monads": "1.0.0"
|
|
48
|
-
},
|
|
49
46
|
"devDependencies": {
|
|
50
|
-
"@opentelemetry/api": "^1.9.
|
|
51
|
-
"@opentelemetry/context-async-hooks": "^2.
|
|
52
|
-
"@opentelemetry/sdk-metrics": "^2.
|
|
53
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
54
|
-
"
|
|
55
|
-
"typescript": "^7.0.2",
|
|
56
|
-
"vitest": "^4.1.10",
|
|
47
|
+
"@opentelemetry/api": "^1.9.1",
|
|
48
|
+
"@opentelemetry/context-async-hooks": "^2.11.0",
|
|
49
|
+
"@opentelemetry/sdk-metrics": "^2.11.0",
|
|
50
|
+
"@opentelemetry/sdk-trace-base": "^2.11.0",
|
|
51
|
+
"@zap-studio/logger": "2.1.0",
|
|
57
52
|
"@zap-studio/typescript": "0.0.0",
|
|
58
|
-
"
|
|
53
|
+
"tsdown": "^0.23.0",
|
|
54
|
+
"typescript": "^7.0.2",
|
|
55
|
+
"vitest": "^5.0.0"
|
|
59
56
|
},
|
|
60
57
|
"peerDependencies": {
|
|
61
|
-
"@opentelemetry/api": "^1.9.
|
|
62
|
-
"@zap-studio/logger": "2.
|
|
58
|
+
"@opentelemetry/api": "^1.9.1",
|
|
59
|
+
"@zap-studio/logger": "^2.1.0"
|
|
63
60
|
},
|
|
64
61
|
"peerDependenciesMeta": {
|
|
65
62
|
"@zap-studio/logger": {
|
|
@@ -68,5 +65,6 @@
|
|
|
68
65
|
},
|
|
69
66
|
"engines": {
|
|
70
67
|
"node": ">=18.0.0"
|
|
71
|
-
}
|
|
68
|
+
},
|
|
69
|
+
"scripts": {}
|
|
72
70
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"base-policy-DkxBg9dh.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 type { Logger } from \"@zap-studio/logger\";\nimport type { Result } from \"@zap-studio/monads\";\n\nimport { trace } from \"@opentelemetry/api\";\nimport { err, ok, ResultAsync } from \"@zap-studio/monads\";\n\nimport type {\n ResolvedRetryPolicy,\n RetryDecision,\n RetryExhaustedInput,\n RetryPolicy,\n RetryRunOptions,\n RetryRunResult,\n RetryRunResultOptions,\n} from \"./types.ts\";\n\nimport { recordRetryAttempt } from \"./_otel.ts\";\nimport { AbortError, RetryError } from \"./errors.ts\";\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 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 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 return await execute(attempt);\n } catch (error) {\n throwIfAborted(signal, logger);\n\n if (!policy.isKnownError(error)) {\n throw error;\n }\n\n await handleThrowModeRetry(policy, { attempt, error, logger, signal, sleep });\n attempt += 1;\n }\n }\n};\n\n/**\n * After a failed, known-domain attempt in throw mode, applies the policy's\n * retry decision: throws the terminal error from `onExhausted` on\n * exhaustion, otherwise waits out the retry delay so the caller's loop can\n * continue.\n *\n * @param policy - Resolved retry policy providing `next` and `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, already known to the policy's domain.\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 `debug`\n * and exhaustion at `warn`.\n * @throws {RetryError} When retries are exhausted and `onExhausted` returns\n * the terminal error.\n * @throws {AbortError} When `signal` aborts while waiting for the retry delay.\n * @throws {Error} Any error thrown by `next`, `onExhausted`, or `sleep`.\n */\nconst handleThrowModeRetry = 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<void> => {\n const { attempt, error, sleep, signal, logger } = params;\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 await (signal === undefined\n ? sleep(decision.delayMs)\n : sleepWithAbortSignal(sleep, decision.delayMs, signal));\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(sleep, decision.delayMs, signal, attempt, logger);\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(signal, Math.max(0, attempt - 1), logger);\n if (abortResult !== undefined) {\n return abortResult;\n }\n\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 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 */\nconst defaultIsKnownError = <TError extends Error>(error: unknown): error is TError =>\n error instanceof Error;\n\n/**\n * Applies `runRetryPolicy`/`runRetryPolicyResult`'s shared defaults\n * (`onExhausted`, `isKnownError`) to a caller-supplied `RetryPolicy`.\n */\nconst resolvePolicy = <TError extends Error, TData>(\n policy: RetryPolicy<TError, TData>,\n): ResolvedRetryPolicy<TError, TData> => ({\n isKnownError: (error): error is TError =>\n policy.isKnownError ? policy.isKnownError(error) : defaultIsKnownError(error),\n next: (input) => policy.next(input),\n onExhausted: (input) =>\n policy.onExhausted ? policy.onExhausted(input) : defaultOnExhausted(input),\n});\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<T, TError extends Error = Error, TData = unknown>(\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<T, TError extends Error = Error, TData = unknown>(\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<T, TError extends Error = Error, TData = unknown>(\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 = resolvePolicy(policy);\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\n/**\n * Runs retry orchestration, returning a `ResultAsync` instead of throwing or\n * returning the hand-rolled {@link RetryRunResult} union.\n *\n * Additive alternative to `runRetryPolicy` for consumers who prefer explicit\n * `Result`/`ResultAsync` values (from `@zap-studio/monads`) over throw/catch.\n * There's no `throwOnExhausted` option — this function always returns a\n * `Result`, so the flag doesn't apply.\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, same as {@link RetryRunOptions} minus\n * `throwOnExhausted`.\n * @returns A `ResultAsync` resolving to `Ok` with the successful value, or `Err`\n * with a `RetryError` (exhaustion) or `AbortError` (cancellation) — the same\n * error object `runRetryPolicy`'s throw mode would throw. When\n * `policy.isKnownError` rejects a caught value, it is wrapped in a new\n * `RetryError` and returned on `Err` instead — in throw mode that same value\n * is rethrown unchanged, not wrapped.\n * @throws {Error} Any error thrown by `next`, `onExhausted`, or a custom `sleep`\n * function.\n *\n * @example\n * ```ts\n * import { runRetryPolicyResult } from \"@zap-studio/retry\";\n *\n * const result = await runRetryPolicyResult(policy, async () => fetchFlakyResource());\n *\n * if (isOk(result)) {\n * console.log(result.value);\n * } else {\n * console.error(result.error);\n * }\n * ```\n */\nexport const runRetryPolicyResult = <T, TError extends Error = Error, TData = unknown>(\n policy: RetryPolicy<TError, TData>,\n execute: (attempt: number) => Promise<T>,\n options: RetryRunResultOptions = {},\n): ResultAsync<T, RetryError | AbortError> =>\n new ResultAsync(\n (async (): Promise<Result<T, RetryError | AbortError>> => {\n const sleep = options.sleep ?? defaultSleep;\n const { signal, logger } = options;\n const resolvedPolicy = resolvePolicy(policy);\n\n const result = await runResultMode(resolvedPolicy, execute, sleep, signal, logger);\n\n return result.ok ? ok(result.value) : err(result.error);\n })(),\n );\n"],"mappings":"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"}
|