@zap-studio/retry 0.1.2 → 0.2.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 +10 -0
- package/README.md +45 -0
- package/dist/{error-CWRADATN.d.mts → error-CVW4I654.d.mts} +2 -2
- package/dist/{error-CWRADATN.d.mts.map → error-CVW4I654.d.mts.map} +1 -1
- package/dist/error.d.mts +1 -1
- package/dist/error.mjs.map +1 -1
- package/dist/exponential-backoff.mjs +1 -1
- package/dist/exponential-backoff.mjs.map +1 -1
- package/dist/fixed-delay.mjs +1 -1
- package/dist/fixed-delay.mjs.map +1 -1
- package/dist/index.d.mts +47 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +166 -24
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +14 -1
- package/dist/types.d.mts.map +1 -1
- package/package.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @zap-studio/retry
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Optimize retry runner hot paths by splitting throw/non-throw execution flows and skipping sleep calls when delay is non-positive.
|
|
8
|
+
- Add `AbortSignal` support to `run(...)` so retry orchestration can be canceled before or between attempts.
|
|
9
|
+
- Add retry benchmarking coverage with core and ecosystem scenarios, including real-world and fair-mode comparisons.
|
|
10
|
+
- Add abort-focused ecosystem benchmarks comparing signal overhead and immediate cancellation behavior.
|
|
11
|
+
- Expand TSDoc coverage for new runner internals added in this release.
|
|
12
|
+
|
|
3
13
|
## 0.1.2
|
|
4
14
|
|
|
5
15
|
### Changed
|
package/README.md
CHANGED
|
@@ -77,6 +77,51 @@ if (!result.ok) {
|
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
## Cancellation With AbortSignal
|
|
81
|
+
|
|
82
|
+
Use `signal` in `run(...)` options to stop retrying early.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
|
|
87
|
+
const promise = exponential.run(
|
|
88
|
+
async () => {
|
|
89
|
+
const response = await $fetch("https://api.example.com/users", {
|
|
90
|
+
throwOnFetchError: true,
|
|
91
|
+
});
|
|
92
|
+
return await response.json();
|
|
93
|
+
},
|
|
94
|
+
{ signal: controller.signal },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
controller.abort(new Error("Request canceled"));
|
|
98
|
+
|
|
99
|
+
await promise;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
In non-throw mode, abort is returned as `{ ok: false }`:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
|
|
107
|
+
const result = await exponential.run(
|
|
108
|
+
async () => {
|
|
109
|
+
const response = await $fetch("https://api.example.com/users", {
|
|
110
|
+
throwOnFetchError: true,
|
|
111
|
+
});
|
|
112
|
+
return await response.json();
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
throwOnExhausted: false,
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
console.error("Retry stopped:", result.error);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
80
125
|
## Choosing The Right Policy
|
|
81
126
|
|
|
82
127
|
Use `ExponentialBackoff` for transient network instability and shared upstream services.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Terminal error types used by retry policies and runners.
|
|
4
4
|
*
|
|
5
|
-
* @module
|
|
5
|
+
* @module @zap-studio/retry/error
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
8
|
* Context payload attached to `RetryError`.
|
|
@@ -41,4 +41,4 @@ declare class RetryError extends Error {
|
|
|
41
41
|
}
|
|
42
42
|
//#endregion
|
|
43
43
|
export { RetryErrorContext as n, RetryError as t };
|
|
44
|
-
//# sourceMappingURL=error-
|
|
44
|
+
//# sourceMappingURL=error-CVW4I654.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-
|
|
1
|
+
{"version":3,"file":"error-CVW4I654.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"}
|
package/dist/error.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as RetryErrorContext, t as RetryError } from "./error-
|
|
1
|
+
import { n as RetryErrorContext, t as RetryError } from "./error-CVW4I654.mjs";
|
|
2
2
|
export { RetryError, RetryErrorContext };
|
package/dist/error.mjs.map
CHANGED
|
@@ -1 +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"}
|
|
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 @zap-studio/retry/error\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"}
|
|
@@ -1 +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"}
|
|
1
|
+
{"version":3,"file":"exponential-backoff.mjs","names":[],"sources":["../src/exponential-backoff.ts"],"sourcesContent":["/**\n * Exponential backoff retry strategy.\n *\n * @module @zap-studio/retry/exponential-backoff\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"}
|
package/dist/fixed-delay.mjs
CHANGED
package/dist/fixed-delay.mjs.map
CHANGED
|
@@ -1 +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"}
|
|
1
|
+
{"version":3,"file":"fixed-delay.mjs","names":[],"sources":["../src/fixed-delay.ts"],"sourcesContent":["/**\n * Fixed-delay retry strategy.\n *\n * @module @zap-studio/retry/fixed-delay\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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { t as RetryError } from "./error-
|
|
1
|
+
import { t as RetryError } from "./error-CVW4I654.mjs";
|
|
2
2
|
import { RetryDecision, RetryDecisionInput, RetryExhaustedInput, RetryPolicy, RetryRunOptions, RetryRunResult } from "./types.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/index.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Base class for implementing retry policies and running retry orchestration.
|
|
7
|
+
*
|
|
8
|
+
* Extend this class and implement {@link BaseRetryPolicy.next} to define retry
|
|
9
|
+
* behavior, then call {@link BaseRetryPolicy.run} to execute operations with that
|
|
10
|
+
* policy.
|
|
11
|
+
*/
|
|
5
12
|
declare abstract class BaseRetryPolicy<TError = unknown, TData = unknown> implements RetryPolicy<TError, TData> {
|
|
6
13
|
/**
|
|
7
14
|
* Returns the retry decision for a failed attempt.
|
|
@@ -20,6 +27,14 @@ declare abstract class BaseRetryPolicy<TError = unknown, TData = unknown> implem
|
|
|
20
27
|
* @throws Any error thrown by an overriding policy implementation.
|
|
21
28
|
*/
|
|
22
29
|
onExhausted(input: RetryExhaustedInput<TError, TData>): RetryError;
|
|
30
|
+
/**
|
|
31
|
+
* Runs retry orchestration in non-throw mode.
|
|
32
|
+
*
|
|
33
|
+
* @param execute - Async function to execute per attempt.
|
|
34
|
+
* @param options - Runner settings with `throwOnExhausted: false`.
|
|
35
|
+
* @returns A discriminated result union containing success value or terminal error.
|
|
36
|
+
* @throws Any error thrown by `next`, `onExhausted`, or a custom `sleep`.
|
|
37
|
+
*/
|
|
23
38
|
run<T>(execute: (attempt: number) => Promise<T>, options: RetryRunOptions & {
|
|
24
39
|
throwOnExhausted: false;
|
|
25
40
|
}): Promise<RetryRunResult<T>>;
|
|
@@ -32,13 +47,43 @@ declare abstract class BaseRetryPolicy<TError = unknown, TData = unknown> implem
|
|
|
32
47
|
* @throws {RetryError} When retries are exhausted and `onExhausted` returns the
|
|
33
48
|
* terminal retry error. The default implementation returns `RetryError` with the last
|
|
34
49
|
* execution failure available on `RetryError.lastError`.
|
|
50
|
+
* @throws {Error} When `options.signal` is already aborted or aborts while retrying.
|
|
35
51
|
* @throws Any error thrown by `next`, by `onExhausted`, or by a custom `sleep`
|
|
36
52
|
* function.
|
|
37
53
|
*/
|
|
38
54
|
run<T>(execute: (attempt: number) => Promise<T>, options?: RetryRunOptions & {
|
|
39
55
|
throwOnExhausted?: true;
|
|
40
56
|
}): Promise<T>;
|
|
57
|
+
/**
|
|
58
|
+
* Runs retry orchestration in throwing mode.
|
|
59
|
+
*
|
|
60
|
+
* This path is selected when `throwOnExhausted` is not `false`.
|
|
61
|
+
*
|
|
62
|
+
* @param execute - Async function to execute per attempt.
|
|
63
|
+
* @param sleep - Delay function used between retry attempts.
|
|
64
|
+
* @returns The successful execution value.
|
|
65
|
+
* @throws {RetryError} Terminal error returned by `onExhausted(...)`.
|
|
66
|
+
* @throws Any error thrown by `next`, by `onExhausted`, or by `sleep`.
|
|
67
|
+
*/
|
|
68
|
+
private runThrowMode;
|
|
69
|
+
/**
|
|
70
|
+
* Runs retry orchestration in non-throw mode.
|
|
71
|
+
*
|
|
72
|
+
* This path is selected when `throwOnExhausted` is `false`.
|
|
73
|
+
*
|
|
74
|
+
* @param execute - Async function to execute per attempt.
|
|
75
|
+
* @param sleep - Delay function used between retry attempts.
|
|
76
|
+
* @returns A discriminated result union containing success value or terminal error.
|
|
77
|
+
* @throws Any error thrown by `next`, by `onExhausted`, or by `sleep`.
|
|
78
|
+
*/
|
|
79
|
+
private runResultMode;
|
|
41
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Default delay implementation used by `run(...)` when no custom sleep function is provided.
|
|
83
|
+
*
|
|
84
|
+
* Returns immediately when `delayMs` is non-positive.
|
|
85
|
+
*/
|
|
86
|
+
declare function defaultSleep(delayMs: number): Promise<void>;
|
|
42
87
|
//#endregion
|
|
43
|
-
export { BaseRetryPolicy };
|
|
88
|
+
export { BaseRetryPolicy, defaultSleep };
|
|
44
89
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;;uBAuBsB,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;;;;;;;;;EAgB/D,GAAA,GAAA,CACE,OAAA,GAAU,OAAA,aAAoB,OAAA,CAAQ,CAAA,GACtC,OAAA,EAAS,eAAA;IAAoB,gBAAA;EAAA,IAC5B,OAAA,CAAQ,cAAA,CAAe,CAAA;;;;;;;;;;;;;;EAe1B,GAAA,GAAA,CACE,OAAA,GAAU,OAAA,aAAoB,OAAA,CAAQ,CAAA,GACtC,OAAA,GAAU,eAAA;IAAoB,gBAAA;EAAA,IAC7B,OAAA,CAAQ,CAAA;;;;;;;;;;;;UA2CG,YAAA;EAmDA;AA+EhB;;;;;;;;;EA/EgB,QAAA,aAAA;AAAA;;;;;;iBA+EM,YAAA,CAAa,OAAA,WAAkB,OAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,14 @@ import { RetryError } from "./error.mjs";
|
|
|
3
3
|
/**
|
|
4
4
|
* Retry runner base class and shared orchestration implementation.
|
|
5
5
|
*
|
|
6
|
-
* @module
|
|
6
|
+
* @module @zap-studio/retry
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Base class for implementing retry policies and running retry orchestration.
|
|
10
|
+
*
|
|
11
|
+
* Extend this class and implement {@link BaseRetryPolicy.next} to define retry
|
|
12
|
+
* behavior, then call {@link BaseRetryPolicy.run} to execute operations with that
|
|
13
|
+
* policy.
|
|
7
14
|
*/
|
|
8
15
|
var BaseRetryPolicy = class {
|
|
9
16
|
/**
|
|
@@ -33,6 +40,7 @@ var BaseRetryPolicy = class {
|
|
|
33
40
|
* @throws Any error thrown by `next`, by `onExhausted`, or by a custom `sleep`
|
|
34
41
|
* function. When `throwOnExhausted` is `false`, exhaustion itself is returned
|
|
35
42
|
* as `{ ok: false }` instead of thrown.
|
|
43
|
+
* Abort errors are also returned as `{ ok: false }` in non-throw mode.
|
|
36
44
|
*
|
|
37
45
|
* @example
|
|
38
46
|
* const result = await policy.run(doWork, { throwOnExhausted: false });
|
|
@@ -40,45 +48,179 @@ var BaseRetryPolicy = class {
|
|
|
40
48
|
*/
|
|
41
49
|
async run(execute, options = {}) {
|
|
42
50
|
const sleep = options.sleep ?? defaultSleep;
|
|
51
|
+
const signal = options.signal;
|
|
52
|
+
if (options.throwOnExhausted === false) return this.runResultMode(execute, sleep, signal);
|
|
53
|
+
return this.runThrowMode(execute, sleep, signal);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Runs retry orchestration in throwing mode.
|
|
57
|
+
*
|
|
58
|
+
* This path is selected when `throwOnExhausted` is not `false`.
|
|
59
|
+
*
|
|
60
|
+
* @param execute - Async function to execute per attempt.
|
|
61
|
+
* @param sleep - Delay function used between retry attempts.
|
|
62
|
+
* @returns The successful execution value.
|
|
63
|
+
* @throws {RetryError} Terminal error returned by `onExhausted(...)`.
|
|
64
|
+
* @throws Any error thrown by `next`, by `onExhausted`, or by `sleep`.
|
|
65
|
+
*/
|
|
66
|
+
async runThrowMode(execute, sleep, signal) {
|
|
43
67
|
let attempt = 1;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
error
|
|
57
|
-
});
|
|
58
|
-
if (!decision.shouldRetry) {
|
|
59
|
-
const terminalError = this.onExhausted({
|
|
68
|
+
while (true) {
|
|
69
|
+
throwIfAborted(signal);
|
|
70
|
+
try {
|
|
71
|
+
return await execute(attempt);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throwIfAborted(signal);
|
|
74
|
+
const typedError = error;
|
|
75
|
+
const decision = this.next({
|
|
76
|
+
attempt,
|
|
77
|
+
error: typedError
|
|
78
|
+
});
|
|
79
|
+
if (!decision.shouldRetry) throw this.onExhausted({
|
|
60
80
|
attempts: attempt,
|
|
61
|
-
error:
|
|
81
|
+
error: typedError
|
|
82
|
+
});
|
|
83
|
+
if (decision.delayMs > 0) if (signal) await sleepWithAbortSignal(sleep, decision.delayMs, signal);
|
|
84
|
+
else await sleep(decision.delayMs);
|
|
85
|
+
attempt += 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Runs retry orchestration in non-throw mode.
|
|
91
|
+
*
|
|
92
|
+
* This path is selected when `throwOnExhausted` is `false`.
|
|
93
|
+
*
|
|
94
|
+
* @param execute - Async function to execute per attempt.
|
|
95
|
+
* @param sleep - Delay function used between retry attempts.
|
|
96
|
+
* @returns A discriminated result union containing success value or terminal error.
|
|
97
|
+
* @throws Any error thrown by `next`, by `onExhausted`, or by `sleep`.
|
|
98
|
+
*/
|
|
99
|
+
async runResultMode(execute, sleep, signal) {
|
|
100
|
+
let attempt = 1;
|
|
101
|
+
while (true) {
|
|
102
|
+
if (signal?.aborted) {
|
|
103
|
+
const attempts = Math.max(0, attempt - 1);
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
error: toRetryError(signal.reason, attempts),
|
|
107
|
+
attempts
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
value: await execute(attempt)
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (signal?.aborted) {
|
|
117
|
+
const attempts = Math.max(0, attempt - 1);
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: toRetryError(signal.reason, attempts),
|
|
121
|
+
attempts
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const typedError = error;
|
|
125
|
+
const decision = this.next({
|
|
126
|
+
attempt,
|
|
127
|
+
error: typedError
|
|
62
128
|
});
|
|
63
|
-
if (
|
|
129
|
+
if (!decision.shouldRetry) return {
|
|
64
130
|
ok: false,
|
|
65
|
-
error:
|
|
131
|
+
error: this.onExhausted({
|
|
132
|
+
attempts: attempt,
|
|
133
|
+
error: typedError
|
|
134
|
+
}),
|
|
66
135
|
attempts: attempt
|
|
67
136
|
};
|
|
68
|
-
|
|
137
|
+
if (decision.delayMs > 0) if (signal) try {
|
|
138
|
+
await sleepWithAbortSignal(sleep, decision.delayMs, signal);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (signal.aborted) return {
|
|
141
|
+
ok: false,
|
|
142
|
+
error: toRetryError(signal.reason, attempt),
|
|
143
|
+
attempts: attempt
|
|
144
|
+
};
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
else await sleep(decision.delayMs);
|
|
148
|
+
attempt += 1;
|
|
69
149
|
}
|
|
70
|
-
await sleep(decision.delayMs);
|
|
71
|
-
attempt += 1;
|
|
72
150
|
}
|
|
73
151
|
}
|
|
74
152
|
};
|
|
75
153
|
/**
|
|
76
154
|
* Default delay implementation used by `run(...)` when no custom sleep function is provided.
|
|
155
|
+
*
|
|
156
|
+
* Returns immediately when `delayMs` is non-positive.
|
|
77
157
|
*/
|
|
78
158
|
async function defaultSleep(delayMs) {
|
|
159
|
+
if (delayMs <= 0) return;
|
|
79
160
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
80
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Throws an abort error when the provided signal is already aborted.
|
|
164
|
+
*
|
|
165
|
+
* @param signal - Optional cancellation signal.
|
|
166
|
+
* @throws {Error} Abort reason converted to an `Error`.
|
|
167
|
+
*/
|
|
168
|
+
function throwIfAborted(signal) {
|
|
169
|
+
if (!signal?.aborted) return;
|
|
170
|
+
throw toAbortError(signal.reason);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Normalizes an abort reason value into an `Error` instance.
|
|
174
|
+
*
|
|
175
|
+
* @param reason - Abort reason from `AbortSignal.reason`.
|
|
176
|
+
* @returns Normalized error instance.
|
|
177
|
+
*/
|
|
178
|
+
function toAbortError(reason) {
|
|
179
|
+
if (reason instanceof Error) return reason;
|
|
180
|
+
if (typeof reason === "string" && reason.length > 0) return new Error(reason);
|
|
181
|
+
if (reason === void 0) return /* @__PURE__ */ new Error("Retry aborted.");
|
|
182
|
+
try {
|
|
183
|
+
return /* @__PURE__ */ new Error(`Retry aborted: ${JSON.stringify(reason)}`);
|
|
184
|
+
} catch {
|
|
185
|
+
return /* @__PURE__ */ new Error("Retry aborted.");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Converts an abort reason into a `RetryError` for non-throw runner mode.
|
|
190
|
+
*
|
|
191
|
+
* @param reason - Abort reason from `AbortSignal.reason`.
|
|
192
|
+
* @returns Retry terminal error value.
|
|
193
|
+
*/
|
|
194
|
+
function toRetryError(reason, attempts) {
|
|
195
|
+
const abortError = toAbortError(reason);
|
|
196
|
+
return new RetryError(abortError.message, {
|
|
197
|
+
attempts,
|
|
198
|
+
lastError: abortError
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Awaits delay sleep while also observing cancellation via `AbortSignal`.
|
|
203
|
+
*
|
|
204
|
+
* @param sleep - Delay function.
|
|
205
|
+
* @param delayMs - Delay duration in milliseconds.
|
|
206
|
+
* @param signal - Cancellation signal.
|
|
207
|
+
* @throws {Error} Abort reason converted to an `Error` when canceled.
|
|
208
|
+
*/
|
|
209
|
+
async function sleepWithAbortSignal(sleep, delayMs, signal) {
|
|
210
|
+
if (signal.aborted) throw toAbortError(signal.reason);
|
|
211
|
+
let onAbort;
|
|
212
|
+
try {
|
|
213
|
+
await Promise.race([sleep(delayMs), new Promise((_, reject) => {
|
|
214
|
+
onAbort = () => {
|
|
215
|
+
reject(toAbortError(signal.reason));
|
|
216
|
+
};
|
|
217
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
218
|
+
})]);
|
|
219
|
+
} finally {
|
|
220
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
81
223
|
//#endregion
|
|
82
|
-
export { BaseRetryPolicy };
|
|
224
|
+
export { BaseRetryPolicy, defaultSleep };
|
|
83
225
|
|
|
84
226
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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 },\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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Retry runner base class and shared orchestration implementation.\n *\n * @module @zap-studio/retry\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\n/**\n * Base class for implementing retry policies and running retry orchestration.\n *\n * Extend this class and implement {@link BaseRetryPolicy.next} to define retry\n * behavior, then call {@link BaseRetryPolicy.run} to execute operations with that\n * policy.\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 /**\n * Runs retry orchestration in non-throw mode.\n *\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 * @throws Any error thrown by `next`, `onExhausted`, or a custom `sleep`.\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 {Error} When `options.signal` is already aborted or aborts while retrying.\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 },\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 * Abort errors are also returned as `{ ok: false }` in non-throw mode.\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 const signal = options.signal;\n if (options.throwOnExhausted === false) {\n return this.runResultMode(execute, sleep, signal);\n }\n\n return this.runThrowMode(execute, sleep, signal);\n }\n\n /**\n * Runs retry orchestration in throwing mode.\n *\n * This path is selected when `throwOnExhausted` is not `false`.\n *\n * @param execute - Async function to execute per attempt.\n * @param sleep - Delay function used between retry attempts.\n * @returns The successful execution value.\n * @throws {RetryError} Terminal error returned by `onExhausted(...)`.\n * @throws Any error thrown by `next`, by `onExhausted`, or by `sleep`.\n */\n private async runThrowMode<T>(\n execute: (attempt: number) => Promise<T>,\n sleep: (delayMs: number) => Promise<void>,\n signal?: AbortSignal,\n ): Promise<T> {\n let attempt = 1;\n\n while (true) {\n throwIfAborted(signal);\n\n try {\n return await execute(attempt);\n } catch (error) {\n throwIfAborted(signal);\n\n const typedError = error as TError;\n const decision = this.next({\n attempt,\n error: typedError,\n });\n\n if (!decision.shouldRetry) {\n throw this.onExhausted({\n attempts: attempt,\n error: typedError,\n });\n }\n\n if (decision.delayMs > 0) {\n if (signal) {\n await sleepWithAbortSignal(sleep, decision.delayMs, signal);\n } else {\n await sleep(decision.delayMs);\n }\n }\n\n attempt += 1;\n }\n }\n }\n\n /**\n * Runs retry orchestration in non-throw mode.\n *\n * This path is selected when `throwOnExhausted` is `false`.\n *\n * @param execute - Async function to execute per attempt.\n * @param sleep - Delay function used between retry attempts.\n * @returns A discriminated result union containing success value or terminal error.\n * @throws Any error thrown by `next`, by `onExhausted`, or by `sleep`.\n */\n private async runResultMode<T>(\n execute: (attempt: number) => Promise<T>,\n sleep: (delayMs: number) => Promise<void>,\n signal?: AbortSignal,\n ): Promise<RetryRunResult<T>> {\n let attempt = 1;\n\n while (true) {\n if (signal?.aborted) {\n const attempts = Math.max(0, attempt - 1);\n return {\n ok: false,\n error: toRetryError(signal.reason, attempts),\n attempts,\n };\n }\n\n try {\n const value = await execute(attempt);\n return { ok: true, value };\n } catch (error) {\n if (signal?.aborted) {\n const attempts = Math.max(0, attempt - 1);\n return {\n ok: false,\n error: toRetryError(signal.reason, attempts),\n attempts,\n };\n }\n\n const typedError = error as TError;\n const decision = this.next({\n attempt,\n error: typedError,\n });\n\n if (!decision.shouldRetry) {\n const terminalError = this.onExhausted({\n attempts: attempt,\n error: typedError,\n });\n\n return {\n ok: false,\n error: terminalError,\n attempts: attempt,\n };\n }\n\n if (decision.delayMs > 0) {\n if (signal) {\n try {\n await sleepWithAbortSignal(sleep, decision.delayMs, signal);\n } catch (error) {\n if (signal.aborted) {\n return {\n ok: false,\n error: toRetryError(signal.reason, attempt),\n attempts: attempt,\n };\n }\n throw error;\n }\n } else {\n await sleep(decision.delayMs);\n }\n }\n\n attempt += 1;\n }\n }\n }\n}\n\n/**\n * Default delay implementation used by `run(...)` when no custom sleep function is provided.\n *\n * Returns immediately when `delayMs` is non-positive.\n */\nexport async function defaultSleep(delayMs: number): Promise<void> {\n if (delayMs <= 0) {\n return;\n }\n\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n}\n\n/**\n * Throws an abort error when the provided signal is already aborted.\n *\n * @param signal - Optional cancellation signal.\n * @throws {Error} Abort reason converted to an `Error`.\n */\nfunction throwIfAborted(signal?: AbortSignal): void {\n if (!signal?.aborted) {\n return;\n }\n\n throw toAbortError(signal.reason);\n}\n\n/**\n * Normalizes an abort reason value into an `Error` instance.\n *\n * @param reason - Abort reason from `AbortSignal.reason`.\n * @returns Normalized error instance.\n */\nfunction toAbortError(reason: unknown): Error {\n if (reason instanceof Error) {\n return reason;\n }\n\n if (typeof reason === \"string\" && reason.length > 0) {\n return new Error(reason);\n }\n\n if (reason === undefined) {\n return new Error(\"Retry aborted.\");\n }\n\n try {\n return new Error(`Retry aborted: ${JSON.stringify(reason)}`);\n } catch {\n return new Error(\"Retry aborted.\");\n }\n}\n\n/**\n * Converts an abort reason into a `RetryError` for non-throw runner mode.\n *\n * @param reason - Abort reason from `AbortSignal.reason`.\n * @returns Retry terminal error value.\n */\nfunction toRetryError(reason: unknown, attempts: number): RetryError {\n const abortError = toAbortError(reason);\n return new RetryError(abortError.message, {\n attempts,\n lastError: abortError,\n });\n}\n\n/**\n * Awaits delay sleep while also observing cancellation via `AbortSignal`.\n *\n * @param sleep - Delay function.\n * @param delayMs - Delay duration in milliseconds.\n * @param signal - Cancellation signal.\n * @throws {Error} Abort reason converted to an `Error` when canceled.\n */\nasync function sleepWithAbortSignal(\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>((_, 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"],"mappings":"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"}
|
package/dist/types.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as RetryError } from "./error-
|
|
1
|
+
import { t as RetryError } from "./error-CVW4I654.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/types.d.ts
|
|
4
4
|
/**
|
|
@@ -59,6 +59,19 @@ interface RetryRunOptions {
|
|
|
59
59
|
* @throws Any error thrown or rejected by the custom delay implementation.
|
|
60
60
|
*/
|
|
61
61
|
readonly sleep?: (delayMs: number) => Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Abort signal used to cancel retry orchestration.
|
|
64
|
+
*
|
|
65
|
+
* When aborted, the runner stops retrying and terminates immediately.
|
|
66
|
+
*/
|
|
67
|
+
readonly signal?: AbortSignal;
|
|
68
|
+
/**
|
|
69
|
+
* When `true`, the runner throws a `RetryError` when retries are exhausted.
|
|
70
|
+
*
|
|
71
|
+
* When `false`, the runner returns a `RetryRunResult` discriminated union.
|
|
72
|
+
*
|
|
73
|
+
* @default false
|
|
74
|
+
*/
|
|
62
75
|
readonly throwOnExhausted?: boolean;
|
|
63
76
|
}
|
|
64
77
|
/**
|
package/dist/types.d.mts.map
CHANGED
|
@@ -1 +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,IAAS,OAAA,aAAoB,OAAA;
|
|
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,IAAS,OAAA,aAAoB,OAAA;;;;;;WAM7B,MAAA,GAAS,WAAA;EAQT;AAMX;;;;;;EANW,SAAA,gBAAA;AAAA;;;;KAMC,cAAA;EAEN,EAAA;EACA,KAAA,EAAO,CAAA;AAAA;EAGP,EAAA;EACA,KAAA,EAAO,UAAA;EACP,QAAA;AAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/retry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Composable retry policies for resilient async operations.",
|
|
6
6
|
"keywords": [
|
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
+
"@types/async-retry": "^1.4.9",
|
|
44
|
+
"@types/promise-retry": "^1.1.6",
|
|
45
|
+
"async-retry": "^1.3.3",
|
|
46
|
+
"exponential-backoff": "^3.1.3",
|
|
47
|
+
"p-retry": "^8.0.0",
|
|
48
|
+
"promise-retry": "^2.0.1",
|
|
43
49
|
"typescript": "^6.0.3",
|
|
44
50
|
"vite-plus": "^0.1.19",
|
|
45
51
|
"@zap-studio/typescript": "0.0.0"
|