@zap-studio/retry 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # @zap-studio/retry
2
+
3
+ ## 0.1.0
4
+
5
+ ### Added
6
+
7
+ - Introduced a transport-agnostic `RetryPolicy` contract with `RetryDecisionInput` and `RetryDecision`.
8
+ - Added required `onExhausted` hook to `RetryPolicy` for policy-specific terminal error shaping.
9
+ - Added shared `BaseRetryPolicy` abstract class to centralize default `onExhausted` behavior.
10
+ - Added `BaseRetryPolicy.run(execute, options)` runner method to execute retry policies with minimal boilerplate.
11
+ - Added `throwOnExhausted` runner option with non-throw `RetryRunResult<T>` mode.
12
+ - Added `ExponentialBackoff` policy with bounded exponential delay via `baseDelayMs`, `maxDelayMs`, and `maxAttempts`.
13
+ - Added `FixedDelay` policy with constant delay and bounded attempts.
14
+ - Added `RetryError` for exhausted-retry failures with structured attempt/error/data context.
15
+
16
+ ### Documentation
17
+
18
+ - Documented throwable behavior on `RetryPolicy`, `BaseRetryPolicy.run`, and related contracts with explicit `@throws` tags for policy, exhaustion, and custom `sleep` failures.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexandre Trotel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # @zap-studio/retry
2
+
3
+ Composable retry policy primitives for HTTP clients and async workflows.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @zap-studio/retry
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { ExponentialBackoff } from "@zap-studio/retry/exponential-backoff";
15
+ import { FixedDelay } from "@zap-studio/retry/fixed-delay";
16
+ import { $fetch } from "@zap-studio/fetch";
17
+
18
+ const exponential = new ExponentialBackoff({
19
+ maxAttempts: 5,
20
+ baseDelayMs: 100,
21
+ maxDelayMs: 2_000,
22
+ });
23
+
24
+ const data = await exponential.run(async () => {
25
+ const response = await $fetch("https://api.example.com/users", {
26
+ throwOnFetchError: true,
27
+ });
28
+ return await response.json();
29
+ });
30
+ ```
31
+
32
+ ## Handling Errors
33
+
34
+ `run(...)` throws when retries are exhausted.
35
+
36
+ By default, policies extending `BaseRetryPolicy` throw `RetryError`.
37
+
38
+ ```ts
39
+ import { RetryError } from "@zap-studio/retry/error";
40
+
41
+ try {
42
+ const data = await exponential.run(async () => {
43
+ const response = await $fetch("https://api.example.com/users", {
44
+ throwOnFetchError: true,
45
+ });
46
+ return await response.json();
47
+ });
48
+ console.log(data);
49
+ } catch (error) {
50
+ if (error instanceof RetryError) {
51
+ console.error("Retries exhausted:", error.attempts);
52
+ console.error("Last error:", error.lastError);
53
+ } else {
54
+ throw error;
55
+ }
56
+ }
57
+ ```
58
+
59
+ To handle exhaustion without throwing, pass `throwOnExhausted: false`:
60
+
61
+ ```ts
62
+ const result = await exponential.run(
63
+ async () => {
64
+ const response = await $fetch("https://api.example.com/users", {
65
+ throwOnFetchError: true,
66
+ });
67
+ return await response.json();
68
+ },
69
+ { throwOnExhausted: false },
70
+ );
71
+
72
+ if (!result.ok) {
73
+ console.error("Retries exhausted:", result.attempts);
74
+ console.error("Last error:", result.error.lastError);
75
+ } else {
76
+ console.log(result.value);
77
+ }
78
+ ```
79
+
80
+ ## Choosing The Right Policy
81
+
82
+ Use `ExponentialBackoff` for transient network instability and shared upstream services.
83
+
84
+ ```ts
85
+ const unstableNetworkPolicy = new ExponentialBackoff({
86
+ maxAttempts: 6,
87
+ baseDelayMs: 100,
88
+ maxDelayMs: 2_000,
89
+ });
90
+ ```
91
+
92
+ Use `FixedDelay` for stable, predictable retry intervals in controlled environments.
93
+
94
+ ```ts
95
+ const predictableIntervalPolicy = new FixedDelay({
96
+ maxAttempts: 4,
97
+ delayMs: 300,
98
+ });
99
+ ```
100
+
101
+ ## Custom Policies
102
+
103
+ Extend `BaseRetryPolicy` when the built-in policies do not match your retry rules.
104
+
105
+ You implement `next(...)` only; the base class supplies `onExhausted` with a default
106
+ `RetryError` and keeps the shared `run(...)` orchestration (override `onExhausted` when
107
+ you need a different terminal error).
108
+
109
+ ```ts
110
+ import { BaseRetryPolicy } from "@zap-studio/retry";
111
+ import type { RetryDecision, RetryDecisionInput } from "@zap-studio/retry/types";
112
+
113
+ class LinearBackoff extends BaseRetryPolicy {
114
+ constructor(
115
+ private readonly maxAttempts: number,
116
+ private readonly stepMs: number,
117
+ ) {
118
+ super();
119
+ }
120
+
121
+ public next(input: RetryDecisionInput): RetryDecision {
122
+ if (input.attempt >= this.maxAttempts) {
123
+ return {
124
+ shouldRetry: false,
125
+ delayMs: 0,
126
+ reason: "max-attempts-reached",
127
+ };
128
+ }
129
+
130
+ return {
131
+ shouldRetry: true,
132
+ delayMs: input.attempt * this.stepMs,
133
+ reason: "retry",
134
+ };
135
+ }
136
+ }
137
+
138
+ const policy = new LinearBackoff(5, 250);
139
+ const value = await policy.run(doWork);
140
+ ```
141
+
142
+ ## RetryError
143
+
144
+ Use `RetryError` when an orchestrator exhausts retries and needs to surface final context.
145
+
146
+ ```ts
147
+ import { RetryError } from "@zap-studio/retry/error";
148
+
149
+ throw new RetryError("Retry policy exhausted all attempts.", {
150
+ attempts: attempt,
151
+ lastError: error,
152
+ lastData: data,
153
+ });
154
+ ```
155
+
156
+ Policies implement `onExhausted(input)` to return the terminal error used by the built-in runner.
157
+
158
+ `ExponentialBackoff` and `FixedDelay` inherit the default implementation from `BaseRetryPolicy`.
159
+
160
+ ## License
161
+
162
+ MIT
@@ -0,0 +1,44 @@
1
+ //#region src/error.d.ts
2
+ /**
3
+ * Terminal error types used by retry policies and runners.
4
+ *
5
+ * @module
6
+ */
7
+ /**
8
+ * Context payload attached to `RetryError`.
9
+ */
10
+ interface RetryErrorContext {
11
+ readonly attempts: number;
12
+ readonly lastError?: unknown;
13
+ readonly lastData?: unknown;
14
+ }
15
+ /**
16
+ * Error thrown when retries are exhausted.
17
+ *
18
+ * @example
19
+ * throw new RetryError("Retry exhausted", {
20
+ * attempts: 3,
21
+ * lastError: new Error("network"),
22
+ * });
23
+ */
24
+ declare class RetryError extends Error {
25
+ /**
26
+ * Total attempts performed before exhaustion.
27
+ */
28
+ readonly attempts: number;
29
+ /**
30
+ * Last captured error from execution.
31
+ */
32
+ readonly lastError?: unknown;
33
+ /**
34
+ * Last captured data value, when available.
35
+ */
36
+ readonly lastData?: unknown;
37
+ /**
38
+ * Creates a RetryError with structured terminal context.
39
+ */
40
+ constructor(message: string, context: RetryErrorContext);
41
+ }
42
+ //#endregion
43
+ export { RetryErrorContext as n, RetryError as t };
44
+ //# sourceMappingURL=error-CWRADATN.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-CWRADATN.d.mts","names":[],"sources":["../src/error.ts"],"mappings":";;AASA;;;;;;;UAAiB,iBAAA;EAAA,SACN,QAAA;EAAA,SACA,SAAA;EAAA,SACA,QAAA;AAAA;;;;;;;;;;cAYE,UAAA,SAAmB,KAAA;EAiBQ;;;EAAA,SAbtB,QAAA;;;;WAIA,SAAA;;;;WAIA,QAAA;;;;EAKhB,WAAA,CAAY,OAAA,UAAiB,OAAA,EAAS,iBAAA;AAAA"}
@@ -0,0 +1,2 @@
1
+ import { n as RetryErrorContext, t as RetryError } from "./error-CWRADATN.mjs";
2
+ export { RetryError, RetryErrorContext };
package/dist/error.mjs ADDED
@@ -0,0 +1,38 @@
1
+ //#region src/error.ts
2
+ /**
3
+ * Error thrown when retries are exhausted.
4
+ *
5
+ * @example
6
+ * throw new RetryError("Retry exhausted", {
7
+ * attempts: 3,
8
+ * lastError: new Error("network"),
9
+ * });
10
+ */
11
+ var RetryError = class extends Error {
12
+ /**
13
+ * Total attempts performed before exhaustion.
14
+ */
15
+ attempts;
16
+ /**
17
+ * Last captured error from execution.
18
+ */
19
+ lastError;
20
+ /**
21
+ * Last captured data value, when available.
22
+ */
23
+ lastData;
24
+ /**
25
+ * Creates a RetryError with structured terminal context.
26
+ */
27
+ constructor(message, context) {
28
+ super(message);
29
+ this.name = "RetryError";
30
+ this.attempts = context.attempts;
31
+ this.lastError = context.lastError;
32
+ this.lastData = context.lastData;
33
+ }
34
+ };
35
+ //#endregion
36
+ export { RetryError };
37
+
38
+ //# sourceMappingURL=error.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.mjs","names":[],"sources":["../src/error.ts"],"sourcesContent":["/**\n * Terminal error types used by retry policies and runners.\n *\n * @module\n */\n\n/**\n * Context payload attached to `RetryError`.\n */\nexport interface RetryErrorContext {\n readonly attempts: number;\n readonly lastError?: unknown;\n readonly lastData?: unknown;\n}\n\n/**\n * Error thrown when retries are exhausted.\n *\n * @example\n * throw new RetryError(\"Retry exhausted\", {\n * attempts: 3,\n * lastError: new Error(\"network\"),\n * });\n */\nexport class RetryError extends Error {\n /**\n * Total attempts performed before exhaustion.\n */\n public readonly attempts: number;\n /**\n * Last captured error from execution.\n */\n public readonly lastError?: unknown;\n /**\n * Last captured data value, when available.\n */\n public readonly lastData?: unknown;\n\n /**\n * Creates a RetryError with structured terminal context.\n */\n constructor(message: string, context: RetryErrorContext) {\n super(message);\n this.name = \"RetryError\";\n this.attempts = context.attempts;\n this.lastError = context.lastError;\n this.lastData = context.lastData;\n }\n}\n"],"mappings":";;;;;;;;;;AAwBA,IAAa,aAAb,cAAgC,MAAM;;;;CAIpC;;;;CAIA;;;;CAIA;;;;CAKA,YAAY,SAAiB,SAA4B;AACvD,QAAM,QAAQ;AACd,OAAK,OAAO;AACZ,OAAK,WAAW,QAAQ;AACxB,OAAK,YAAY,QAAQ;AACzB,OAAK,WAAW,QAAQ"}
@@ -0,0 +1,38 @@
1
+ import { RetryDecision, RetryDecisionInput } from "./types.mjs";
2
+ import { BaseRetryPolicy } from "./index.mjs";
3
+
4
+ //#region src/exponential-backoff.d.ts
5
+ /**
6
+ * Configuration for `ExponentialBackoff`.
7
+ */
8
+ interface ExponentialBackoffOptions {
9
+ maxAttempts: number;
10
+ baseDelayMs: number;
11
+ maxDelayMs: number;
12
+ }
13
+ /**
14
+ * Retries with exponential delay growth up to a max cap.
15
+ *
16
+ * @example
17
+ * const policy = new ExponentialBackoff({
18
+ * maxAttempts: 5,
19
+ * baseDelayMs: 100,
20
+ * maxDelayMs: 2_000,
21
+ * });
22
+ */
23
+ declare class ExponentialBackoff extends BaseRetryPolicy {
24
+ private readonly maxAttempts;
25
+ private readonly baseDelayMs;
26
+ private readonly maxDelayMs;
27
+ /**
28
+ * Creates an exponential backoff retry policy.
29
+ */
30
+ constructor(options: ExponentialBackoffOptions);
31
+ /**
32
+ * Computes retry decision for the current attempt.
33
+ */
34
+ next(input: RetryDecisionInput): RetryDecision;
35
+ }
36
+ //#endregion
37
+ export { ExponentialBackoff, ExponentialBackoffOptions };
38
+ //# sourceMappingURL=exponential-backoff.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exponential-backoff.d.mts","names":[],"sources":["../src/exponential-backoff.ts"],"mappings":";;;;;;;UAYiB,yBAAA;EACf,WAAA;EACA,WAAA;EACA,UAAA;AAAA;;;;;;;;;;;cAaW,kBAAA,SAA2B,eAAA;EAAA,iBACrB,WAAA;EAAA,iBACA,WAAA;EAAA,iBACA,UAAA;;;;EAKjB,WAAA,CAAY,OAAA,EAAS,yBAAA;;;;EAUrB,IAAA,CAAY,KAAA,EAAO,kBAAA,GAAqB,aAAA;AAAA"}
@@ -0,0 +1,51 @@
1
+ import { BaseRetryPolicy } from "./index.mjs";
2
+ //#region src/exponential-backoff.ts
3
+ /**
4
+ * Exponential backoff retry strategy.
5
+ *
6
+ * @module
7
+ */
8
+ /**
9
+ * Retries with exponential delay growth up to a max cap.
10
+ *
11
+ * @example
12
+ * const policy = new ExponentialBackoff({
13
+ * maxAttempts: 5,
14
+ * baseDelayMs: 100,
15
+ * maxDelayMs: 2_000,
16
+ * });
17
+ */
18
+ var ExponentialBackoff = class extends BaseRetryPolicy {
19
+ maxAttempts;
20
+ baseDelayMs;
21
+ maxDelayMs;
22
+ /**
23
+ * Creates an exponential backoff retry policy.
24
+ */
25
+ constructor(options) {
26
+ super();
27
+ this.maxAttempts = options.maxAttempts;
28
+ this.baseDelayMs = options.baseDelayMs;
29
+ this.maxDelayMs = options.maxDelayMs;
30
+ }
31
+ /**
32
+ * Computes retry decision for the current attempt.
33
+ */
34
+ next(input) {
35
+ if (input.attempt >= this.maxAttempts) return {
36
+ shouldRetry: false,
37
+ delayMs: 0,
38
+ reason: "max-attempts-reached"
39
+ };
40
+ const exponent = Math.max(0, input.attempt - 1);
41
+ return {
42
+ shouldRetry: true,
43
+ delayMs: Math.min(this.maxDelayMs, this.baseDelayMs * 2 ** exponent),
44
+ reason: "retry"
45
+ };
46
+ }
47
+ };
48
+ //#endregion
49
+ export { ExponentialBackoff };
50
+
51
+ //# sourceMappingURL=exponential-backoff.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exponential-backoff.mjs","names":[],"sources":["../src/exponential-backoff.ts"],"sourcesContent":["/**\n * Exponential backoff retry strategy.\n *\n * @module\n */\n\nimport { BaseRetryPolicy } from \"./index.js\";\nimport type { RetryDecision, RetryDecisionInput } from \"./types.js\";\n\n/**\n * Configuration for `ExponentialBackoff`.\n */\nexport interface ExponentialBackoffOptions {\n maxAttempts: number;\n baseDelayMs: number;\n maxDelayMs: number;\n}\n\n/**\n * Retries with exponential delay growth up to a max cap.\n *\n * @example\n * const policy = new ExponentialBackoff({\n * maxAttempts: 5,\n * baseDelayMs: 100,\n * maxDelayMs: 2_000,\n * });\n */\nexport class ExponentialBackoff extends BaseRetryPolicy {\n private readonly maxAttempts: number;\n private readonly baseDelayMs: number;\n private readonly maxDelayMs: number;\n\n /**\n * Creates an exponential backoff retry policy.\n */\n constructor(options: ExponentialBackoffOptions) {\n super();\n this.maxAttempts = options.maxAttempts;\n this.baseDelayMs = options.baseDelayMs;\n this.maxDelayMs = options.maxDelayMs;\n }\n\n /**\n * Computes retry decision for the current attempt.\n */\n public next(input: RetryDecisionInput): RetryDecision {\n if (input.attempt >= this.maxAttempts) {\n return { shouldRetry: false, delayMs: 0, reason: \"max-attempts-reached\" };\n }\n\n const exponent = Math.max(0, input.attempt - 1);\n const delayMs = Math.min(this.maxDelayMs, this.baseDelayMs * 2 ** exponent);\n\n return { shouldRetry: true, delayMs, reason: \"retry\" };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA4BA,IAAa,qBAAb,cAAwC,gBAAgB;CACtD;CACA;CACA;;;;CAKA,YAAY,SAAoC;AAC9C,SAAO;AACP,OAAK,cAAc,QAAQ;AAC3B,OAAK,cAAc,QAAQ;AAC3B,OAAK,aAAa,QAAQ;;;;;CAM5B,KAAY,OAA0C;AACpD,MAAI,MAAM,WAAW,KAAK,YACxB,QAAO;GAAE,aAAa;GAAO,SAAS;GAAG,QAAQ;GAAwB;EAG3E,MAAM,WAAW,KAAK,IAAI,GAAG,MAAM,UAAU,EAAE;AAG/C,SAAO;GAAE,aAAa;GAAM,SAFZ,KAAK,IAAI,KAAK,YAAY,KAAK,cAAc,KAAK,SAAS;GAEtC,QAAQ;GAAS"}
@@ -0,0 +1,35 @@
1
+ import { RetryDecision, RetryDecisionInput } from "./types.mjs";
2
+ import { BaseRetryPolicy } from "./index.mjs";
3
+
4
+ //#region src/fixed-delay.d.ts
5
+ /**
6
+ * Configuration for `FixedDelay`.
7
+ */
8
+ interface FixedDelayOptions {
9
+ maxAttempts: number;
10
+ delayMs: number;
11
+ }
12
+ /**
13
+ * Retries with a constant delay between attempts.
14
+ *
15
+ * @example
16
+ * const policy = new FixedDelay({
17
+ * maxAttempts: 3,
18
+ * delayMs: 250,
19
+ * });
20
+ */
21
+ declare class FixedDelay extends BaseRetryPolicy {
22
+ private readonly maxAttempts;
23
+ private readonly delayMs;
24
+ /**
25
+ * Creates a fixed-delay retry policy.
26
+ */
27
+ constructor(options: FixedDelayOptions);
28
+ /**
29
+ * Computes retry decision for the current attempt.
30
+ */
31
+ next(input: RetryDecisionInput): RetryDecision;
32
+ }
33
+ //#endregion
34
+ export { FixedDelay, FixedDelayOptions };
35
+ //# sourceMappingURL=fixed-delay.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixed-delay.d.mts","names":[],"sources":["../src/fixed-delay.ts"],"mappings":";;;;;;AA0BA;UAdiB,iBAAA;EACf,WAAA;EACA,OAAA;AAAA;;;;;;;;;;cAYW,UAAA,SAAmB,eAAA;EAAA,iBACb,WAAA;EAAA,iBACA,OAAA;;;;EAKjB,WAAA,CAAY,OAAA,EAAS,iBAAA;;;;EASrB,IAAA,CAAY,KAAA,EAAO,kBAAA,GAAqB,aAAA;AAAA"}
@@ -0,0 +1,47 @@
1
+ import { BaseRetryPolicy } from "./index.mjs";
2
+ //#region src/fixed-delay.ts
3
+ /**
4
+ * Fixed-delay retry strategy.
5
+ *
6
+ * @module
7
+ */
8
+ /**
9
+ * Retries with a constant delay between attempts.
10
+ *
11
+ * @example
12
+ * const policy = new FixedDelay({
13
+ * maxAttempts: 3,
14
+ * delayMs: 250,
15
+ * });
16
+ */
17
+ var FixedDelay = class extends BaseRetryPolicy {
18
+ maxAttempts;
19
+ delayMs;
20
+ /**
21
+ * Creates a fixed-delay retry policy.
22
+ */
23
+ constructor(options) {
24
+ super();
25
+ this.maxAttempts = options.maxAttempts;
26
+ this.delayMs = options.delayMs;
27
+ }
28
+ /**
29
+ * Computes retry decision for the current attempt.
30
+ */
31
+ next(input) {
32
+ if (input.attempt >= this.maxAttempts) return {
33
+ shouldRetry: false,
34
+ delayMs: 0,
35
+ reason: "max-attempts-reached"
36
+ };
37
+ return {
38
+ shouldRetry: true,
39
+ delayMs: this.delayMs,
40
+ reason: "retry"
41
+ };
42
+ }
43
+ };
44
+ //#endregion
45
+ export { FixedDelay };
46
+
47
+ //# sourceMappingURL=fixed-delay.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixed-delay.mjs","names":[],"sources":["../src/fixed-delay.ts"],"sourcesContent":["/**\n * Fixed-delay retry strategy.\n *\n * @module\n */\n\nimport { BaseRetryPolicy } from \"./index.js\";\nimport type { RetryDecision, RetryDecisionInput } from \"./types.js\";\n\n/**\n * Configuration for `FixedDelay`.\n */\nexport interface FixedDelayOptions {\n maxAttempts: number;\n delayMs: number;\n}\n\n/**\n * Retries with a constant delay between attempts.\n *\n * @example\n * const policy = new FixedDelay({\n * maxAttempts: 3,\n * delayMs: 250,\n * });\n */\nexport class FixedDelay extends BaseRetryPolicy {\n private readonly maxAttempts: number;\n private readonly delayMs: number;\n\n /**\n * Creates a fixed-delay retry policy.\n */\n constructor(options: FixedDelayOptions) {\n super();\n this.maxAttempts = options.maxAttempts;\n this.delayMs = options.delayMs;\n }\n\n /**\n * Computes retry decision for the current attempt.\n */\n public next(input: RetryDecisionInput): RetryDecision {\n if (input.attempt >= this.maxAttempts) {\n return { shouldRetry: false, delayMs: 0, reason: \"max-attempts-reached\" };\n }\n\n return { shouldRetry: true, delayMs: this.delayMs, reason: \"retry\" };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA0BA,IAAa,aAAb,cAAgC,gBAAgB;CAC9C;CACA;;;;CAKA,YAAY,SAA4B;AACtC,SAAO;AACP,OAAK,cAAc,QAAQ;AAC3B,OAAK,UAAU,QAAQ;;;;;CAMzB,KAAY,OAA0C;AACpD,MAAI,MAAM,WAAW,KAAK,YACxB,QAAO;GAAE,aAAa;GAAO,SAAS;GAAG,QAAQ;GAAwB;AAG3E,SAAO;GAAE,aAAa;GAAM,SAAS,KAAK;GAAS,QAAQ;GAAS"}
@@ -0,0 +1,44 @@
1
+ import { t as RetryError } from "./error-CWRADATN.mjs";
2
+ import { RetryDecision, RetryDecisionInput, RetryExhaustedInput, RetryPolicy, RetryRunOptions, RetryRunResult } from "./types.mjs";
3
+
4
+ //#region src/index.d.ts
5
+ declare abstract class BaseRetryPolicy<TError = unknown, TData = unknown> implements RetryPolicy<TError, TData> {
6
+ /**
7
+ * Returns the retry decision for a failed attempt.
8
+ *
9
+ * @param input - Attempt context used to compute retry behavior.
10
+ * @throws Any error thrown by a concrete retry policy implementation.
11
+ */
12
+ abstract next(input: RetryDecisionInput<TError, TData>): RetryDecision;
13
+ /**
14
+ * Builds the terminal error thrown or returned when retries are exhausted.
15
+ *
16
+ * Override this when you need custom terminal error types.
17
+ *
18
+ * @param input - Exhaustion context.
19
+ * @returns `RetryError` by default.
20
+ * @throws Any error thrown by an overriding policy implementation.
21
+ */
22
+ onExhausted(input: RetryExhaustedInput<TError, TData>): RetryError;
23
+ run<T>(execute: (attempt: number) => Promise<T>, options: RetryRunOptions & {
24
+ throwOnExhausted: false;
25
+ }): Promise<RetryRunResult<T>>;
26
+ /**
27
+ * Runs retry orchestration and throws terminal error on exhaustion.
28
+ *
29
+ * @param execute - Async function to execute per attempt.
30
+ * @param options - Optional runner settings.
31
+ * @returns The successful execution value.
32
+ * @throws {RetryError} When retries are exhausted and `onExhausted` returns the
33
+ * terminal retry error. The default implementation returns `RetryError` with the last
34
+ * execution failure available on `RetryError.lastError`.
35
+ * @throws Any error thrown by `next`, by `onExhausted`, or by a custom `sleep`
36
+ * function.
37
+ */
38
+ run<T>(execute: (attempt: number) => Promise<T>, options?: RetryRunOptions & {
39
+ throwOnExhausted?: true | undefined;
40
+ }): Promise<T>;
41
+ }
42
+ //#endregion
43
+ export { BaseRetryPolicy };
44
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;uBAgBsB,eAAA,+CAA8D,WAAA,CAClF,MAAA,EACA,KAAA;;;;;;;WAQgB,IAAA,CAAK,KAAA,EAAO,kBAAA,CAAmB,MAAA,EAAQ,KAAA,IAAS,aAAA;;;;;;;;;;EAWhE,WAAA,CAAmB,KAAA,EAAO,mBAAA,CAAoB,MAAA,EAAQ,KAAA,IAAS,UAAA;EAQ/D,GAAA,GAAA,CACE,OAAA,GAAU,OAAA,aAAoB,OAAA,CAAQ,CAAA,GACtC,OAAA,EAAS,eAAA;IAAoB,gBAAA;EAAA,IAC5B,OAAA,CAAQ,cAAA,CAAe,CAAA;;;;;;;;;;;;;EAc1B,GAAA,GAAA,CACE,OAAA,GAAU,OAAA,aAAoB,OAAA,CAAQ,CAAA,GACtC,OAAA,GAAU,eAAA;IAAoB,gBAAA;EAAA,IAC7B,OAAA,CAAQ,CAAA;AAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,84 @@
1
+ import { RetryError } from "./error.mjs";
2
+ //#region src/index.ts
3
+ /**
4
+ * Retry runner base class and shared orchestration implementation.
5
+ *
6
+ * @module
7
+ */
8
+ var BaseRetryPolicy = class {
9
+ /**
10
+ * Builds the terminal error thrown or returned when retries are exhausted.
11
+ *
12
+ * Override this when you need custom terminal error types.
13
+ *
14
+ * @param input - Exhaustion context.
15
+ * @returns `RetryError` by default.
16
+ * @throws Any error thrown by an overriding policy implementation.
17
+ */
18
+ onExhausted(input) {
19
+ return new RetryError("Retry policy exhausted all attempts.", {
20
+ attempts: input.attempts,
21
+ lastError: input.error,
22
+ lastData: input.data
23
+ });
24
+ }
25
+ /**
26
+ * Runs retry orchestration in non-throw mode.
27
+ *
28
+ * When `throwOnExhausted` is `false`, returns a discriminated result union.
29
+ *
30
+ * @param execute - Async function to execute per attempt.
31
+ * @param options - Runner settings.
32
+ * @returns Success value or terminal result object based on option mode.
33
+ * @throws Any error thrown by `next`, by `onExhausted`, or by a custom `sleep`
34
+ * function. When `throwOnExhausted` is `false`, exhaustion itself is returned
35
+ * as `{ ok: false }` instead of thrown.
36
+ *
37
+ * @example
38
+ * const result = await policy.run(doWork, { throwOnExhausted: false });
39
+ * if (!result.ok) console.error(result.error);
40
+ */
41
+ async run(execute, options = {}) {
42
+ const sleep = options.sleep ?? defaultSleep;
43
+ let attempt = 1;
44
+ let lastError;
45
+ while (true) try {
46
+ const value = await execute(attempt);
47
+ if (options.throwOnExhausted === false) return {
48
+ ok: true,
49
+ value
50
+ };
51
+ return value;
52
+ } catch (error) {
53
+ lastError = error;
54
+ const decision = this.next({
55
+ attempt,
56
+ error
57
+ });
58
+ if (!decision.shouldRetry) {
59
+ const terminalError = this.onExhausted({
60
+ attempts: attempt,
61
+ error: lastError
62
+ });
63
+ if (options.throwOnExhausted === false) return {
64
+ ok: false,
65
+ error: terminalError,
66
+ attempts: attempt
67
+ };
68
+ throw terminalError;
69
+ }
70
+ await sleep(decision.delayMs);
71
+ attempt += 1;
72
+ }
73
+ }
74
+ };
75
+ /**
76
+ * Default delay implementation used by `run(...)` when no custom sleep function is provided.
77
+ */
78
+ async function defaultSleep(delayMs) {
79
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
80
+ }
81
+ //#endregion
82
+ export { BaseRetryPolicy };
83
+
84
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Retry runner base class and shared orchestration implementation.\n *\n * @module\n */\n\nimport { RetryError } from \"./error.js\";\nimport type {\n RetryDecision,\n RetryDecisionInput,\n RetryExhaustedInput,\n RetryPolicy,\n RetryRunOptions,\n RetryRunResult,\n} from \"./types.js\";\n\nexport abstract class BaseRetryPolicy<TError = unknown, TData = unknown> implements RetryPolicy<\n TError,\n TData\n> {\n /**\n * Returns the retry decision for a failed attempt.\n *\n * @param input - Attempt context used to compute retry behavior.\n * @throws Any error thrown by a concrete retry policy implementation.\n */\n public abstract next(input: RetryDecisionInput<TError, TData>): RetryDecision;\n\n /**\n * Builds the terminal error thrown or returned when retries are exhausted.\n *\n * Override this when you need custom terminal error types.\n *\n * @param input - Exhaustion context.\n * @returns `RetryError` by default.\n * @throws Any error thrown by an overriding policy implementation.\n */\n public onExhausted(input: RetryExhaustedInput<TError, TData>): RetryError {\n return new RetryError(\"Retry policy exhausted all attempts.\", {\n attempts: input.attempts,\n lastError: input.error,\n lastData: input.data,\n });\n }\n\n public async run<T>(\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 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 Any error thrown by `next`, by `onExhausted`, or by a custom `sleep`\n * function.\n */\n public async run<T>(\n execute: (attempt: number) => Promise<T>,\n options?: RetryRunOptions & { throwOnExhausted?: true | undefined },\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 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 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 *\n * @example\n * const result = await policy.run(doWork, { throwOnExhausted: false });\n * if (!result.ok) console.error(result.error);\n */\n public async run<T>(\n execute: (attempt: number) => Promise<T>,\n options: RetryRunOptions = {},\n ): Promise<T | RetryRunResult<T>> {\n const sleep = options.sleep ?? defaultSleep;\n let attempt = 1;\n let lastError: TError | undefined;\n\n while (true) {\n try {\n const value = await execute(attempt);\n if (options.throwOnExhausted === false) {\n return { ok: true, value };\n }\n return value;\n } catch (error) {\n lastError = error as TError;\n const decision = this.next({\n attempt,\n error: error as TError,\n });\n\n if (!decision.shouldRetry) {\n const terminalError = this.onExhausted({\n attempts: attempt,\n error: lastError,\n });\n if (options.throwOnExhausted === false) {\n return {\n ok: false,\n error: terminalError,\n attempts: attempt,\n };\n }\n throw terminalError;\n }\n\n await sleep(decision.delayMs);\n attempt += 1;\n }\n }\n }\n}\n\n/**\n * Default delay implementation used by `run(...)` when no custom sleep function is provided.\n */\nasync function defaultSleep(delayMs: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n}\n"],"mappings":";;;;;;;AAgBA,IAAsB,kBAAtB,MAGE;;;;;;;;;;CAkBA,YAAmB,OAAuD;AACxE,SAAO,IAAI,WAAW,wCAAwC;GAC5D,UAAU,MAAM;GAChB,WAAW,MAAM;GACjB,UAAU,MAAM;GACjB,CAAC;;;;;;;;;;;;;;;;;;CAyCJ,MAAa,IACX,SACA,UAA2B,EAAE,EACG;EAChC,MAAM,QAAQ,QAAQ,SAAS;EAC/B,IAAI,UAAU;EACd,IAAI;AAEJ,SAAO,KACL,KAAI;GACF,MAAM,QAAQ,MAAM,QAAQ,QAAQ;AACpC,OAAI,QAAQ,qBAAqB,MAC/B,QAAO;IAAE,IAAI;IAAM;IAAO;AAE5B,UAAO;WACA,OAAO;AACd,eAAY;GACZ,MAAM,WAAW,KAAK,KAAK;IACzB;IACO;IACR,CAAC;AAEF,OAAI,CAAC,SAAS,aAAa;IACzB,MAAM,gBAAgB,KAAK,YAAY;KACrC,UAAU;KACV,OAAO;KACR,CAAC;AACF,QAAI,QAAQ,qBAAqB,MAC/B,QAAO;KACL,IAAI;KACJ,OAAO;KACP,UAAU;KACX;AAEH,UAAM;;AAGR,SAAM,MAAM,SAAS,QAAQ;AAC7B,cAAW;;;;;;;AASnB,eAAe,aAAa,SAAgC;AAC1D,OAAM,IAAI,SAAS,YAAY,WAAW,SAAS,QAAQ,CAAC"}
@@ -0,0 +1,77 @@
1
+ import { t as RetryError } from "./error-CWRADATN.mjs";
2
+
3
+ //#region src/types.d.ts
4
+ /**
5
+ * Retry policy contract used by `BaseRetryPolicy`.
6
+ *
7
+ * @example
8
+ * const policy: RetryPolicy = {
9
+ * next: ({ attempt }) => ({ shouldRetry: attempt < 3, delayMs: 100 }),
10
+ * onExhausted: ({ attempts }) => new RetryError("done", { attempts }),
11
+ * };
12
+ */
13
+ interface RetryPolicy<TError = unknown, TData = unknown> {
14
+ /**
15
+ * Returns the retry decision for a failed attempt.
16
+ *
17
+ * @throws Any error thrown by the policy implementation.
18
+ */
19
+ next(input: RetryDecisionInput<TError, TData>): RetryDecision;
20
+ /**
21
+ * Builds the terminal error used when retries are exhausted.
22
+ *
23
+ * @throws Any error thrown by the policy implementation.
24
+ */
25
+ onExhausted(input: RetryExhaustedInput<TError, TData>): RetryError;
26
+ }
27
+ /**
28
+ * Decision returned by a retry policy for a specific attempt.
29
+ */
30
+ interface RetryDecision {
31
+ readonly shouldRetry: boolean;
32
+ readonly delayMs: number;
33
+ readonly reason?: "retry" | "max-attempts-reached" | "policy-declined";
34
+ }
35
+ /**
36
+ * Input passed to `RetryPolicy.next(...)` for each failed attempt.
37
+ */
38
+ interface RetryDecisionInput<TError = unknown, TData = unknown> {
39
+ readonly attempt: number;
40
+ readonly maxAttempts?: number | undefined;
41
+ readonly error?: TError;
42
+ readonly data?: TData;
43
+ }
44
+ /**
45
+ * Input passed to `RetryPolicy.onExhausted(...)` when retries stop.
46
+ */
47
+ interface RetryExhaustedInput<TError = unknown, TData = unknown> {
48
+ readonly attempts: number;
49
+ readonly error?: TError;
50
+ readonly data?: TData;
51
+ }
52
+ /**
53
+ * Options for `BaseRetryPolicy.run(...)`.
54
+ */
55
+ interface RetryRunOptions {
56
+ /**
57
+ * Delay function used between retry attempts.
58
+ *
59
+ * @throws Any error thrown or rejected by the custom delay implementation.
60
+ */
61
+ readonly sleep?: ((delayMs: number) => Promise<void>) | undefined;
62
+ readonly throwOnExhausted?: boolean | undefined;
63
+ }
64
+ /**
65
+ * Result union returned by non-throw runner mode.
66
+ */
67
+ type RetryRunResult<T> = {
68
+ ok: true;
69
+ value: T;
70
+ } | {
71
+ ok: false;
72
+ error: RetryError;
73
+ attempts: number;
74
+ };
75
+ //#endregion
76
+ export { RetryDecision, RetryDecisionInput, RetryExhaustedInput, RetryPolicy, RetryRunOptions, RetryRunResult };
77
+ //# sourceMappingURL=types.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;UAiBiB,WAAA;;;;;;EAMf,IAAA,CAAK,KAAA,EAAO,kBAAA,CAAmB,MAAA,EAAQ,KAAA,IAAS,aAAA;;;;;;EAMhD,WAAA,CAAY,KAAA,EAAO,mBAAA,CAAoB,MAAA,EAAQ,KAAA,IAAS,UAAA;AAAA;AAM1D;;;AAAA,UAAiB,aAAA;EAAA,SACN,WAAA;EAAA,SACA,OAAA;EAAA,SACA,MAAA;AAAA;;AAMX;;UAAiB,kBAAA;EAAA,SACN,OAAA;EAAA,SACA,WAAA;EAAA,SACA,KAAA,GAAQ,MAAA;EAAA,SACR,IAAA,GAAO,KAAA;AAAA;;;;UAMD,mBAAA;EAAA,SACN,QAAA;EAAA,SACA,KAAA,GAAQ,MAAA;EAAA,SACR,IAAA,GAAO,KAAA;AAAA;;;;UAMD,eAAA;EANC;AAMlB;;;;EANkB,SAYP,KAAA,KAAU,OAAA,aAAoB,OAAA;EAAA,SAC9B,gBAAA;AAAA;;;;KAMC,cAAA;EAEN,EAAA;EACA,KAAA,EAAO,CAAA;AAAA;EAGP,EAAA;EACA,KAAA,EAAO,UAAA;EACP,QAAA;AAAA"}
package/dist/types.mjs ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@zap-studio/retry",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Composable retry policies for resilient async operations.",
6
+ "keywords": [
7
+ "backoff",
8
+ "exponential backoff",
9
+ "fetch",
10
+ "http",
11
+ "resilience",
12
+ "retry",
13
+ "typescript"
14
+ ],
15
+ "homepage": "https://www.zapstudio.dev/packages/retry",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/zap-studio/monorepo.git",
20
+ "directory": "packages/retry"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "CHANGELOG.md",
25
+ "LICENSE",
26
+ "README.md"
27
+ ],
28
+ "type": "module",
29
+ "sideEffects": false,
30
+ "types": "./dist/index.d.mts",
31
+ "exports": {
32
+ ".": "./dist/index.mjs",
33
+ "./error": "./dist/error.mjs",
34
+ "./exponential-backoff": "./dist/exponential-backoff.mjs",
35
+ "./fixed-delay": "./dist/fixed-delay.mjs",
36
+ "./types": "./dist/types.mjs",
37
+ "./package.json": "./package.json"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "devDependencies": {
43
+ "typescript": "^6.0.3",
44
+ "vite-plus": "^0.1.19",
45
+ "@zap-studio/typescript": "0.0.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ },
50
+ "scripts": {
51
+ "build": "vp pack",
52
+ "test": "vp test run",
53
+ "test:watch": "vp test watch"
54
+ }
55
+ }