@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 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
- ## [2.1.0]
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?)`, a `ResultAsync`-returning alternative to `runRetryPolicy`, backed by the new `@zap-studio/monads` dependency. Additive and opt-in — `runRetryPolicy` (both throw and non-throw modes) is unchanged. No `throwOnExhausted` option; it always returns a `Result`. `Err`'s payload is the same `RetryError`/`AbortError` object `runRetryPolicy`'s throw mode would throw, preserving `RetryError.attempts`/`lastError`/`lastData` and `AbortError.cause`.
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
- 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).
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'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`.
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 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.
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(...)` gains an optional `logger?: Logger` option (from `@zap-studio/logger`). When provided, it logs each retry decision at `debug` (attempt, delay, reason), exhaustion at `warn`, and cancellation at `debug`. Omitting it keeps zero logging overhead. See [Logging](https://www.zapstudio.dev/retry/logging).
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(...)` gain an optional `jitter?: "full" | "equal" | JitterOptions` option, applied to the computed delay after it's capped at `maxDelayMs`. `"full"` randomizes across `[0, delayMs]`; `"equal"` keeps a floor at half the delay, randomizing across `[delayMs/2, delayMs]`. Pass `{ mode, random }` to override the random source, useful for deterministic tests.
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` gains an optional `isKnownError?: (error: unknown) => error is TError` hook that `BaseRetryPolicy.run(...)` now calls before handing a caught value to `next(...)`/`onExhausted(...)`. `BaseRetryPolicy.isKnownError` default checks `error instanceof Error`.
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 (an HTTP error, a domain-specific error) to get real narrowing instead of an `instanceof Error` assumption, and to stop unrelated `Error` types from being retried as if they belonged to your domain.
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 the override pattern.
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)` adds a fixed `incrementMs` to the delay after each failed attempt, capped at `maxDelayMs` — steadier growth than `exponentialBackoff`, more spacing than `fixedDelay`. See [linearBackoff](https://www.zapstudio.dev/retry/linear-backoff).
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` is now constrained to `TError extends Error` and defaults to `Error` (was `TError = unknown`) on `RetryPolicy`, `RetryDecisionInput`, `RetryExhaustedInput`, and `BaseRetryPolicy`.
58
- - **Breaking behavior change:** a rejection with a non-`Error` value (a thrown string, plain object, `undefined`, ...) now bypasses retry entirely on the attempt that produced it — it no longer reaches `next(...)`, and no delay/backoff is applied. Previously any thrown value was passed through to the policy unchanged. In throw mode, `run(...)` rethrows the value as-is; with `throwOnExhausted: false`, it's wrapped in a `RetryError` and returned on `result.error` — `run(...)` never throws in that mode.
59
- - **Breaking:** Policies are plain objects instead of classes, for tree-shaking bundlers can drop an unused policy factory and its defaults entirely, which isn't possible across a shared class hierarchy. `ExponentialBackoff`/`FixedDelay` classes are replaced by `exponentialBackoff(options)`/`fixedDelay(options)` factory functions that return a `RetryPolicy`. Migrate `new ExponentialBackoff(opts)` to `exponentialBackoff(opts)`, and `new FixedDelay(opts)` to `fixedDelay(opts)`.
60
- - **Breaking:** `RetryPolicy.onExhausted` is now optional (previously required); omit it to use the same default `RetryError` that `BaseRetryPolicy.onExhausted` used to build.
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 orchestration is now the standalone function `runRetryPolicy(policy, execute, options?)`, which accepts any object satisfying `RetryPolicy` — no subclassing required. Migrate `policy.run(execute, options)` to `runRetryPolicy(policy, execute, options)`. A custom policy that previously extended `BaseRetryPolicy` and overrode `next`/`onExhausted`/`isKnownError` becomes an object literal implementing the same members; see [Custom Policies](https://www.zapstudio.dev/retry/custom-policies).
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
- Collapsed abort/sleep orchestration internals out of the public API.
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 orchestration internals with no consumer outside the retry loop, not standalone utilities.
72
- - `defaultSleep` is unaffected and still exported from `@zap-studio/retry` (no dedicated subpath).
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 everything can be imported from `@zap-studio/retry` directly (`BaseRetryPolicy`, `ExponentialBackoff`, `FixedDelay`, `RetryError`, `AbortError`, abort helpers, `defaultSleep`, and all public types). All exports are side-effect free and tree-shakeable; granular subpath imports keep working.
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 from the entrypoint into its own module, available as the new `./base-policy` subpath.
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 were orchestration internals (`runResultMode`, `runThrowMode`) and are no longer part of the public API.
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:** Subpath for error types: use `@zap-studio/retry/errors` (plural) for `RetryError`, `AbortError`, and related types. A prior JSR `error` subpath that pointed at a non-existent `error.ts` entry is removed; update deep imports from `@zap-studio/retry/error` to `@zap-studio/retry/errors`.
97
- - Add dedicated `AbortError` and normalize cancellation paths so retry internals throw/return `RetryError` or `AbortError` instead of plain `Error`.
98
- - Expose `defaultSleep` from the `@zap-studio/retry/sleep` subpath only (the main entry does not re-export it; `run` still uses it internally when `sleep` is omitted).
99
- - Align non-throw exhaustion metadata so `result.attempts` and `result.error.attempts` stay consistent for `RetryError` outcomes.
100
- - In non-throw mode, return a normalized `AbortError` on `result.error` for cancellation; `result.attempts` still reports completed attempts.
101
- - Refactor result-mode internals into smaller helpers for lower complexity and cleaner maintainability.
102
- - Expand docs across README and package docs pages to explain `AbortError` behavior in throw and non-throw modes.
103
- - Split the retry runner into dedicated modules: `throw-mode` (throwing execution path), `result-mode` (non-throw `RetryRunResult` path), and `sleep` (the default `defaultSleep` implementation). `BaseRetryPolicy` in `index` now delegates to these modules without changing public behavior.
104
- - Add exhaustive TSDoc for `result-mode` and other `src` modules, including private helpers, policy option and state fields, and `RetryRunResult` union members.
105
- - Rework test layout into `sleep`, `throw-mode`, `result-mode`, and `index` test files with a shared `sequence-policy` fixture, replacing the prior combined `index` and `abort` test files.
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
- - Optimize retry runner hot paths by splitting throw/non-throw execution flows and skipping sleep calls when delay is non-positive.
112
- - Add `AbortSignal` support to `run(...)` so retry orchestration can be canceled before or between attempts.
113
- - Add retry benchmarking coverage with core and ecosystem scenarios, including real-world and fair-mode comparisons.
114
- - Add abort-focused ecosystem benchmarks comparing signal overhead and immediate cancellation behavior.
115
- - Expand TSDoc coverage for new runner internals added in this release.
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
- - Expand TSDoc coverage across retry modules and exported contracts for stronger JSR documentation completeness.
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
- - Introduced a transport-agnostic `RetryPolicy` contract with `RetryDecisionInput` and `RetryDecision`.
138
- - Added required `onExhausted` hook to `RetryPolicy` for policy-specific terminal error shaping.
139
- - Added shared `BaseRetryPolicy` abstract class to centralize default `onExhausted` behavior.
140
- - Added `BaseRetryPolicy.run(execute, options)` runner method to execute retry policies with minimal boilerplate.
141
- - Added `throwOnExhausted` runner option with non-throw `RetryRunResult<T>` mode.
142
- - Added `ExponentialBackoff` policy with bounded exponential delay via `baseDelayMs`, `maxDelayMs`, and `maxAttempts`.
143
- - Added `FixedDelay` policy with constant delay and bounded attempts.
144
- - Added `RetryError` for exhausted-retry failures with structured attempt/error/data context.
145
- - Documented throwable behavior on `RetryPolicy`, `BaseRetryPolicy.run`, and related contracts with explicit `@throws` tags for policy, exhaustion, and custom `sleep` failures.
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.0";
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 = resolvePolicy(policy);
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, runRetryPolicyResult as r, defaultSleep as t };
403
+ export { runRetryPolicy as n, defaultSleep as t };
453
404
 
454
- //# sourceMappingURL=base-policy-DkxBg9dh.js.map
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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"}
@@ -1,6 +1,4 @@
1
- import { r as RetryError, t as AbortError } from "./errors-CS5UPJWs.js";
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":";;;;;;;;;;;;;;;;;cAsCa,eAAsB,oBAAkB;;;;;;;;;;;;;iBAwerC,eAAe,GAAG,eAAe,QAAQ,OAAO,iBAC9D,QAAQ,YAAY,QAAQ,QAC5B,UAAU,oBAAoB,QAAQ,IACtC,SAAS;EAAoB;IAC5B,QAAQ,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgCV,eAAe,GAAG,eAAe,QAAQ,OAAO,iBAC9D,QAAQ,YAAY,QAAQ,QAC5B,UAAU,oBAAoB,QAAQ,IACtC,UAAU;EAAoB;IAC7B,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4EE,uBAAwB,GAAG,eAAe,QAAQ,OAAO,iBACpE,QAAQ,YAAY,QAAQ,QAC5B,UAAU,oBAAoB,QAAQ,IACtC,UAAS,0BACR,YAAY,GAAG,aAAa"}
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"}
@@ -1,3 +1,3 @@
1
- import { n as runRetryPolicy, r as runRetryPolicyResult, t as defaultSleep } from "./base-policy-DkxBg9dh.js";
1
+ import { n as runRetryPolicy, t as defaultSleep } from "./base-policy-ROp4XL19.js";
2
2
  import "./errors.js";
3
- export { defaultSleep, runRetryPolicy, runRetryPolicyResult };
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":";;;;;;;;;;;;;;UAsBiB;;;;EAIf;;;;EAIA;;;;EAIA;;;;;EAKA,SAAS,aAAa;;;;;;;;;;;;cAaX,qBAAsB,SAAS,8BAA4B"}
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"}
@@ -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":";;;;;;;;UAciB;;;;EAIf;;;;EAIA;;;;;;;;;;;cAYW,aAAc,SAAS,sBAAoB"}
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, RetryRunResultOptions } from "./types.js";
3
- import { defaultSleep, runRetryPolicy, runRetryPolicyResult } from "./base-policy.js";
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, type RetryRunResultOptions, applyJitter, defaultSleep, exponentialBackoff, fixedDelay, linearBackoff, runRetryPolicy, runRetryPolicyResult };
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, r as runRetryPolicyResult, t as defaultSleep } from "./base-policy-DkxBg9dh.js";
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, runRetryPolicyResult };
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
@@ -1 +1 @@
1
- {"version":3,"file":"jitter.d.ts","names":[],"sources":["../src/jitter.ts"],"mappings":";;;;;;;;;;;;;;KAcY;;;;;;;UAQK;;;;EAIf,MAAM;;;;;;EAMN;;;;;;;;;;;;;cAcW,cAAe,iBAAiB,SAAS,aAAa"}
1
+ {"version":3,"file":"jitter.d.ts","names":[],"sources":["../src/jitter.ts"],"mappings":";;;;;;;;;;;;;;YAcY;;;;;;;iBAQK;;;;EAIf,MAAM;;;;;;EAMN;;;;;;;;;;;;;qBAcW,cAAe,iBAAiB,SAAS,aAAa"}
@@ -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":";;;;;;;;;;;;;;;UAuBiB;;;;EAIf;;;;EAIA;;;;EAIA;;;;EAIA;;;;;EAKA,SAAS,aAAa;;;;;;;;;;;;;cAcX,gBAAiB,SAAS,yBAAuB"}
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
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;;;;;UAuBiB,YAAY,eAAe,QAAQ,OAAO;;;;;;EAMzD,OAAO,OAAO,mBAAmB,QAAQ,WAAW;;;;;;;;EAQpD,eAAe,OAAO,oBAAoB,QAAQ,WAAW;;;;;;;;;;;;;EAa7D,gBAAgB,mBAAmB,SAAS;;;;;;UAO7B,oBAAoB,eAAe,OAAO;;;;EAIzD,MAAM,YAAY,QAAQ;;;;;EAK1B,cAAc,OAAO,oBAAoB,QAAQ,WAAW;;;;;EAK5D,eAAe,mBAAmB,SAAS;;;;;;;;UAS5B;;;;;WAKN;;;;WAIA;;;;WAIA;;;;;;;;UASM,mBAAmB,eAAe,QAAQ,OAAO;;;;WAIvD;;;;;WAKA;;;;;WAKA,QAAQ;;;;;WAKR,OAAO;;;;;;;;UASD,oBAAoB,eAAe,QAAQ,OAAO;;;;WAIxD;;;;WAIA,QAAQ;;;;WAIR,OAAO;;;;;;;;UASD;;;;;;WAMN,SAAS,oBAAoB;;;;;;WAM7B,SAAS;;;;;;;;WAQT;;;;;;;WAOA,SAAS;;;;;;;;;;;;KAaR,wBAAwB,KAAK;;;;;;;;;;;;KAa7B,eAAe;;;;EAKrB;;;;EAIA,OAAO;;;;;EAMP;;;;;EAKA,OAAO,aAAa;;;;EAIpB"}
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.0",
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.0",
51
- "@opentelemetry/context-async-hooks": "^2.10.0",
52
- "@opentelemetry/sdk-metrics": "^2.10.0",
53
- "@opentelemetry/sdk-trace-base": "^2.10.0",
54
- "tsdown": "^0.22.14",
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
- "@zap-studio/logger": "2.0.0"
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.0",
62
- "@zap-studio/logger": "2.0.0"
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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"}