@async-kit/retryx 0.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 ADDED
@@ -0,0 +1,9 @@
1
+ ## 0.1.2 (2026-03-11)
2
+
3
+ ### 🩹 Fixes
4
+
5
+ - **release:** add publishConfig access public to all packages ([82c12ca](https://github.com/NexaLeaf/async-kit/commit/82c12ca))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Palanisamy Muthusamy
package/README.md ADDED
@@ -0,0 +1,348 @@
1
+ <div align="center">
2
+
3
+ <img src="https://capsule-render.vercel.app/api?type=rect&color=gradient&customColorList=12&height=120&section=header&text=retryx&fontSize=60&fontColor=fff&animation=fadeIn&desc=%40async-kit%2Fretryx&descAlignY=75&descAlign=50" width="100%"/>
4
+
5
+ <br/>
6
+
7
+ [![npm](https://img.shields.io/npm/v/@async-kit/retryx?style=for-the-badge&logo=npm&color=4ECDC4)](https://www.npmjs.com/package/@async-kit/retryx)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](../../LICENSE)
10
+ [![Bundle size](https://img.shields.io/bundlephobia/minzip/@async-kit/retryx?style=for-the-badge&color=4ECDC4)](https://bundlephobia.com/package/@async-kit/retryx)
11
+ [![Node](https://img.shields.io/badge/Node-%3E%3D18-339933?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/)
12
+ [![Browser](https://img.shields.io/badge/Browser-Supported-4285F4?style=for-the-badge&logo=googlechrome&logoColor=white)](#compatibility)
13
+
14
+ **Smart async retry with exponential backoff, jitter strategies, circuit breaker, and AbortSignal support.**
15
+
16
+ *Make any async operation resilient in one line.*
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install @async-kit/retryx
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```typescript
31
+ import { retry, createRetry, withRetry, CircuitBreaker } from '@async-kit/retryx';
32
+
33
+ // One-shot retry
34
+ const data = await retry(() => fetch('/api').then(r => r.json()), {
35
+ maxAttempts: 5,
36
+ jitter: 'full',
37
+ });
38
+
39
+ // Reusable retry function
40
+ const resilient = createRetry({ maxAttempts: 3, initialDelay: 200 });
41
+ await resilient(() => callApi());
42
+
43
+ // Wrap any async function
44
+ const safeFetch = withRetry(fetch, { maxAttempts: 3 });
45
+ const resp = await safeFetch('/api/users', { method: 'GET' });
46
+
47
+ // Circuit breaker
48
+ const cb = new CircuitBreaker({ failureThreshold: 5, successThreshold: 2, openDurationMs: 10_000 });
49
+ const result = await cb.run(() => callExternalService());
50
+ ```
51
+
52
+ ## API
53
+
54
+ ### `retry(task, options?)`
55
+
56
+ | Option | Type | Default | Description |
57
+ |---|---|---|---|
58
+ | `maxAttempts` | `number` | `3` | Total attempts including the first |
59
+ | `initialDelay` | `number` | `200` | Base delay in ms before the first retry |
60
+ | `maxDelay` | `number` | `30_000` | Maximum delay cap in ms |
61
+ | `factor` | `number` | `2` | Exponential backoff multiplier |
62
+ | `jitter` | `JitterStrategy` | `'equal'` | Randomization strategy (see below) |
63
+ | `retryIf` | `(err, ctx) => bool` | `() => true` | Return `false` to stop retrying; may be async |
64
+ | `onRetry` | `(n, err, ms, ctx) => void` | — | Hook called before each retry delay |
65
+ | `signal` | `AbortSignal` | — | Cancels pending retry delays |
66
+ | `timeoutMs` | `number` | — | Per-attempt timeout; throws `RetryxTimeoutError` |
67
+
68
+ ### Jitter Strategies
69
+
70
+ | Strategy | Formula | Best For |
71
+ |---|---|---|
72
+ | `'equal'` (default) | `cap/2 + random(0, cap/2)` | Preserves mean delay |
73
+ | `'full'` | `random(0, cap)` | AWS-recommended; highest spread |
74
+ | `'decorrelated'` | `min(cap, random(base, prev*3))` | Aggressive thundering-herd prevention |
75
+ | `'none'` | `cap` | Deterministic testing |
76
+
77
+ ### `createRetry(defaults)`
78
+
79
+ Creates a reusable retry function. Per-call `overrides` are merged with defaults.
80
+
81
+ ```typescript
82
+ const resilient = createRetry({ maxAttempts: 5, jitter: 'full' });
83
+ await resilient(() => fetchData(), { maxAttempts: 3 }); // override for this call
84
+ ```
85
+
86
+ ### `withRetry(fn, options)`
87
+
88
+ Wraps an existing async function so every invocation is automatically retried.
89
+
90
+ ```typescript
91
+ const safeFetch = withRetry(fetch, { maxAttempts: 3 });
92
+ const resp = await safeFetch('/api/users', { method: 'GET' }); // retried transparently
93
+ ```
94
+
95
+ ### `RetryContext`
96
+
97
+ Passed to `retryIf` and `onRetry`:
98
+
99
+ ```typescript
100
+ interface RetryContext {
101
+ attemptNumber: number; // 1-based attempt that just failed
102
+ totalAttempts: number;
103
+ elapsedMs: number; // wall-clock ms since first attempt
104
+ errors: unknown[]; // all errors so far, in order
105
+ }
106
+ ```
107
+
108
+ ## Circuit Breaker
109
+
110
+ Prevents cascading failures by fast-failing calls when a service is degraded.
111
+
112
+ ```typescript
113
+ const cb = new CircuitBreaker({
114
+ failureThreshold: 5, // open after 5 consecutive failures
115
+ successThreshold: 2, // close after 2 successes in HALF_OPEN
116
+ openDurationMs: 10_000, // stay open 10 s before probing
117
+ volumeThreshold: 10, // require ≥ 10 calls before tripping
118
+ onStateChange: (from, to) => console.log(`${from} → ${to}`),
119
+ });
120
+ ```
121
+
122
+ #### States
123
+
124
+ ```
125
+ CLOSED ──(failures >= threshold)──► OPEN ──(after openDurationMs)──► HALF_OPEN
126
+ ▲ │
127
+ └──────────(successes >= successThreshold)────────────────────────────┘
128
+ ```
129
+
130
+ | Method | Description |
131
+ |---|---|
132
+ | `.run(task)` | Execute; throws `CircuitOpenError` when OPEN |
133
+ | `.reset()` | Force to CLOSED state |
134
+ | `.stats()` | Returns `{ failures, successes, calls, state }` |
135
+ | `.currentState` | Current `CircuitState` |
136
+
137
+ ## Error Types
138
+
139
+ | Class | When |
140
+ |---|---|
141
+ | `RetryxError` | All attempts exhausted — has `.attempts`, `.lastError`, `.allErrors` |
142
+ | `RetryxTimeoutError` | Per-attempt `timeoutMs` exceeded — has `.attempt`, `.timeoutMs` |
143
+ | `CircuitOpenError` | Call blocked by open circuit — has `.retryAfterMs` |
144
+
145
+ ## Examples
146
+
147
+ ### Retry only transient HTTP errors
148
+
149
+ ```typescript
150
+ import { retry } from '@async-kit/retryx';
151
+
152
+ const data = await retry(
153
+ () => fetch('/api/orders').then(async r => {
154
+ if (!r.ok) throw Object.assign(new Error(r.statusText), { status: r.status });
155
+ return r.json();
156
+ }),
157
+ {
158
+ maxAttempts: 5,
159
+ jitter: 'full',
160
+ retryIf: (err: any) => err.status == null || err.status >= 500,
161
+ onRetry: (attempt, err: any, delayMs) => {
162
+ console.warn(`[attempt ${attempt}] ${err.message} — retrying in ${delayMs}ms`);
163
+ },
164
+ }
165
+ );
166
+ ```
167
+
168
+ ### App-wide resilience factory
169
+
170
+ ```typescript
171
+ import { createRetry } from '@async-kit/retryx';
172
+
173
+ // Define once, use everywhere
174
+ export const resilient = createRetry({
175
+ maxAttempts: 4,
176
+ initialDelay: 300,
177
+ maxDelay: 15_000,
178
+ jitter: 'equal',
179
+ });
180
+
181
+ // In your service layer
182
+ export const ordersApi = {
183
+ create: (payload: OrderPayload) =>
184
+ resilient(() => httpClient.post('/orders', payload)),
185
+ get: (id: string) =>
186
+ resilient(() => httpClient.get(`/orders/${id}`)),
187
+ };
188
+ ```
189
+
190
+ ### `withRetry` — wrap third-party SDKs
191
+
192
+ ```typescript
193
+ import { withRetry } from '@async-kit/retryx';
194
+ import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
195
+
196
+ const s3 = new S3Client({});
197
+
198
+ // Wrap the entire send method — every call is retried automatically
199
+ const resilientSend = withRetry(
200
+ s3.send.bind(s3),
201
+ { maxAttempts: 4, jitter: 'decorrelated' }
202
+ );
203
+
204
+ const response = await resilientSend(
205
+ new GetObjectCommand({ Bucket: 'my-bucket', Key: 'data.json' })
206
+ );
207
+ ```
208
+
209
+ ### Per-attempt timeout to bound total latency
210
+
211
+ ```typescript
212
+ import { retry, RetryxTimeoutError } from '@async-kit/retryx';
213
+
214
+ const result = await retry(
215
+ () => slowExternalService.query(params),
216
+ {
217
+ maxAttempts: 3,
218
+ timeoutMs: 2_000, // each attempt must finish within 2 s
219
+ initialDelay: 500,
220
+ }
221
+ ).catch((err) => {
222
+ if (err instanceof RetryxTimeoutError)
223
+ console.error(`Attempt ${err.attempt} timed out after ${err.timeoutMs}ms`);
224
+ throw err;
225
+ });
226
+ ```
227
+
228
+ ### Cancellable polling with AbortSignal
229
+
230
+ ```typescript
231
+ import { retry } from '@async-kit/retryx';
232
+
233
+ const controller = new AbortController();
234
+
235
+ // Cancel from the UI
236
+ document.getElementById('cancel')!.onclick = () => controller.abort();
237
+
238
+ const result = await retry(
239
+ () => pollJobStatus(jobId).then(s => {
240
+ if (s.status !== 'done') throw new Error('not ready');
241
+ return s;
242
+ }),
243
+ {
244
+ maxAttempts: 120,
245
+ initialDelay: 1_000,
246
+ maxDelay: 10_000,
247
+ signal: controller.signal,
248
+ }
249
+ );
250
+ ```
251
+
252
+ ### Circuit Breaker — protect a downstream service
253
+
254
+ ```typescript
255
+ import { CircuitBreaker, CircuitOpenError } from '@async-kit/retryx';
256
+
257
+ const inventoryCb = new CircuitBreaker({
258
+ failureThreshold: 5,
259
+ successThreshold: 2,
260
+ openDurationMs: 30_000,
261
+ onStateChange: (from, to) => {
262
+ logger.warn(`inventory circuit: ${from} → ${to}`);
263
+ metrics.increment(`circuit.inventory.${to.toLowerCase()}`);
264
+ },
265
+ });
266
+
267
+ async function getInventory(sku: string) {
268
+ try {
269
+ return await inventoryCb.run(() => inventoryService.get(sku));
270
+ } catch (err) {
271
+ if (err instanceof CircuitOpenError) {
272
+ // Serve from cache while circuit is open
273
+ return cache.get(`inventory:${sku}`) ?? { qty: 0 };
274
+ }
275
+ throw err;
276
+ }
277
+ }
278
+ ```
279
+
280
+ ### Circuit Breaker + retry together
281
+
282
+ ```typescript
283
+ import { retry, CircuitBreaker, CircuitOpenError } from '@async-kit/retryx';
284
+
285
+ const cb = new CircuitBreaker({ failureThreshold: 3, successThreshold: 1, openDurationMs: 10_000 });
286
+
287
+ const data = await retry(
288
+ () => cb.run(() => externalService.fetch(id)),
289
+ {
290
+ maxAttempts: 5,
291
+ retryIf: (err) => !(err instanceof CircuitOpenError), // don't retry open-circuit errors
292
+ onRetry: (n, err) => console.log(`retry ${n}: ${err}`),
293
+ }
294
+ );
295
+ ```
296
+
297
+ ### Inspect all errors across attempts
298
+
299
+ ```typescript
300
+ import { retry, RetryxError } from '@async-kit/retryx';
301
+
302
+ try {
303
+ await retry(() => unstableService.call(), { maxAttempts: 3 });
304
+ } catch (err) {
305
+ if (err instanceof RetryxError) {
306
+ console.error(`Failed after ${err.attempts} attempts`);
307
+ err.allErrors.forEach((e, i) =>
308
+ console.error(` Attempt ${i + 1}:`, e)
309
+ );
310
+ }
311
+ }
312
+ ```
313
+
314
+ ## Types
315
+
316
+ ```typescript
317
+ import type {
318
+ JitterStrategy,
319
+ RetryContext,
320
+ RetryxOptions,
321
+ CircuitState,
322
+ CircuitBreakerOptions,
323
+ CircuitBreakerStats,
324
+ } from '@async-kit/retryx';
325
+ ```
326
+
327
+ ## Compatibility
328
+
329
+ | Environment | Support | Notes |
330
+ |---|---|---|
331
+ | **Node.js** | ≥ 18 | Recommended ≥ 24 for best performance |
332
+ | **Deno** | ✅ | Via npm specifier (`npm:@async-kit/retryx`) |
333
+ | **Bun** | ✅ | Full support |
334
+ | **Chrome** | ≥ 80 | ESM via bundler or native import |
335
+ | **Firefox** | ≥ 75 | ESM via bundler or native import |
336
+ | **Safari** | ≥ 13.1 | ESM via bundler or native import |
337
+ | **Edge** | ≥ 80 | ESM via bundler or native import |
338
+ | **React Native** | ✅ | Via Metro bundler |
339
+ | **Cloudflare Workers** | ✅ | ESM, `AbortSignal` natively supported |
340
+ | **Vercel Edge Runtime** | ✅ | ESM, no `process` / `fs` dependencies |
341
+
342
+ **No Node.js built-ins are used.** The package relies only on standard JavaScript (`Promise`, `setTimeout`, `clearTimeout`, `AbortSignal`, `DOMException`) — all available in any modern runtime.
343
+
344
+ > **`AbortSignal` / `DOMException`** are part of the Web Platform API. In Node.js they are globals since v15. In older environments (Node 14 or old browsers) you may need to polyfill `AbortController` — e.g. [`abortcontroller-polyfill`](https://www.npmjs.com/package/abortcontroller-polyfill).
345
+
346
+ ## License
347
+
348
+ MIT © async-kit contributors · Part of the [async-kit](../../README.md) ecosystem
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Jitter strategy for exponential backoff.
3
+ * - `'equal'` — `cap/2 + random(0, cap/2)` (default, preserves mean delay)
4
+ * - `'full'` — `random(0, cap)` (AWS recommended for highest spread)
5
+ * - `'decorrelated'` — `min(cap, random(base, prev * 3))` (aggressive spread)
6
+ * - `'none'` — no randomization
7
+ */
8
+ type JitterStrategy = 'equal' | 'full' | 'decorrelated' | 'none';
9
+ /** Context object passed to `retryIf` and `onRetry` callbacks. */
10
+ interface RetryContext {
11
+ /** 1-based attempt number of the attempt that just failed. */
12
+ attemptNumber: number;
13
+ /** Total configured max attempts. */
14
+ totalAttempts: number;
15
+ /** Wall-clock ms elapsed since the first attempt started. */
16
+ elapsedMs: number;
17
+ /** All errors collected so far, in order. */
18
+ errors: unknown[];
19
+ }
20
+ /** Options for `retry()` and `createRetry()`. */
21
+ interface RetryxOptions {
22
+ /** Total attempts including the first. Default: `3`. */
23
+ maxAttempts?: number;
24
+ /** Base delay in ms before the first retry. Default: `200`. */
25
+ initialDelay?: number;
26
+ /** Maximum delay cap in ms. Default: `30_000`. */
27
+ maxDelay?: number;
28
+ /** Backoff multiplier. Default: `2`. */
29
+ factor?: number;
30
+ /** Jitter strategy. Default: `'equal'`. */
31
+ jitter?: JitterStrategy;
32
+ /**
33
+ * Return `false` to stop retrying immediately.
34
+ * Receives the error and full context. May be async.
35
+ */
36
+ retryIf?: (error: unknown, context: RetryContext) => boolean | Promise<boolean>;
37
+ /** Called before each retry delay. */
38
+ onRetry?: (attemptNumber: number, error: unknown, delayMs: number, context: RetryContext) => void;
39
+ /** AbortSignal — cancels pending retry delays. */
40
+ signal?: AbortSignal;
41
+ /** Per-attempt timeout in ms. Throws `RetryxTimeoutError` if exceeded. */
42
+ timeoutMs?: number;
43
+ }
44
+ /** All possible circuit breaker states. */
45
+ type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
46
+ /** Constructor options for `CircuitBreaker`. */
47
+ interface CircuitBreakerOptions {
48
+ /** Number of failures before opening the circuit. */
49
+ failureThreshold: number;
50
+ /** Successes in HALF_OPEN state needed to re-close. */
51
+ successThreshold: number;
52
+ /** How long (ms) to stay OPEN before moving to HALF_OPEN. */
53
+ openDurationMs: number;
54
+ /** Minimum calls in the window before the circuit can trip. Default: `1`. */
55
+ volumeThreshold?: number;
56
+ /** Called whenever the circuit transitions between states. */
57
+ onStateChange?: (from: CircuitState, to: CircuitState) => void;
58
+ }
59
+ /** Snapshot of `CircuitBreaker` counters and state. */
60
+ interface CircuitBreakerStats {
61
+ failures: number;
62
+ successes: number;
63
+ calls: number;
64
+ state: CircuitState;
65
+ }
66
+
67
+ declare class RetryxError extends Error {
68
+ readonly attempts: number;
69
+ readonly lastError: unknown;
70
+ /** Every error thrown across all attempts, in order. */
71
+ readonly allErrors: unknown[];
72
+ constructor(message: string, attempts: number, lastError: unknown,
73
+ /** Every error thrown across all attempts, in order. */
74
+ allErrors: unknown[]);
75
+ }
76
+ declare class RetryxTimeoutError extends Error {
77
+ readonly attempt: number;
78
+ readonly timeoutMs: number;
79
+ constructor(attempt: number, timeoutMs: number);
80
+ }
81
+ /**
82
+ * Retry an async operation with exponential backoff, jitter, and context-aware hooks.
83
+ *
84
+ * @example
85
+ * const data = await retry(() => fetch('/api').then(r => r.json()), { maxAttempts: 5 });
86
+ */
87
+ declare function retry<T>(task: () => Promise<T>, options?: RetryxOptions): Promise<T>;
88
+ /**
89
+ * Create a reusable retry function with pre-configured options.
90
+ *
91
+ * @example
92
+ * const resilient = createRetry({ maxAttempts: 5 });
93
+ * await resilient(() => callApi());
94
+ */
95
+ declare function createRetry(defaults: RetryxOptions): <T>(task: () => Promise<T>, overrides?: RetryxOptions) => Promise<T>;
96
+ /**
97
+ * Wraps an async function so that every call is automatically retried.
98
+ *
99
+ * @example
100
+ * const resilientFetch = withRetry(fetch, { maxAttempts: 3 });
101
+ * const resp = await resilientFetch('/api/users'); // retried on failure
102
+ */
103
+ declare function withRetry<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => Promise<TReturn>, options: RetryxOptions): (...args: TArgs) => Promise<TReturn>;
104
+ declare class CircuitOpenError extends Error {
105
+ readonly retryAfterMs: number;
106
+ constructor(retryAfterMs: number);
107
+ }
108
+ /**
109
+ * Circuit breaker wrapping any async operation.
110
+ *
111
+ * States:
112
+ * - **CLOSED** — calls pass through normally.
113
+ * - **OPEN** — calls fail fast with `CircuitOpenError`.
114
+ * - **HALF_OPEN** — one probe call is allowed; success closes, failure re-opens.
115
+ *
116
+ * @example
117
+ * const cb = new CircuitBreaker({ failureThreshold: 5, successThreshold: 2, openDurationMs: 10_000 });
118
+ * const result = await cb.run(() => callExternalService());
119
+ */
120
+ declare class CircuitBreaker {
121
+ private state;
122
+ private failures;
123
+ private successes;
124
+ private calls;
125
+ private openedAt;
126
+ private readonly failureThreshold;
127
+ private readonly successThreshold;
128
+ private readonly openDurationMs;
129
+ private readonly volumeThreshold;
130
+ private readonly onStateChange?;
131
+ constructor(options: CircuitBreakerOptions);
132
+ get currentState(): CircuitState;
133
+ run<T>(task: () => Promise<T>): Promise<T>;
134
+ /** Manually reset the circuit to CLOSED state. */
135
+ reset(): void;
136
+ stats(): CircuitBreakerStats;
137
+ private checkHalfOpen;
138
+ private onSuccess;
139
+ private onFailure;
140
+ private transition;
141
+ }
142
+
143
+ export { CircuitBreaker, type CircuitBreakerOptions, type CircuitBreakerStats, CircuitOpenError, type CircuitState, type JitterStrategy, type RetryContext, RetryxError, type RetryxOptions, RetryxTimeoutError, createRetry, retry, withRetry };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Jitter strategy for exponential backoff.
3
+ * - `'equal'` — `cap/2 + random(0, cap/2)` (default, preserves mean delay)
4
+ * - `'full'` — `random(0, cap)` (AWS recommended for highest spread)
5
+ * - `'decorrelated'` — `min(cap, random(base, prev * 3))` (aggressive spread)
6
+ * - `'none'` — no randomization
7
+ */
8
+ type JitterStrategy = 'equal' | 'full' | 'decorrelated' | 'none';
9
+ /** Context object passed to `retryIf` and `onRetry` callbacks. */
10
+ interface RetryContext {
11
+ /** 1-based attempt number of the attempt that just failed. */
12
+ attemptNumber: number;
13
+ /** Total configured max attempts. */
14
+ totalAttempts: number;
15
+ /** Wall-clock ms elapsed since the first attempt started. */
16
+ elapsedMs: number;
17
+ /** All errors collected so far, in order. */
18
+ errors: unknown[];
19
+ }
20
+ /** Options for `retry()` and `createRetry()`. */
21
+ interface RetryxOptions {
22
+ /** Total attempts including the first. Default: `3`. */
23
+ maxAttempts?: number;
24
+ /** Base delay in ms before the first retry. Default: `200`. */
25
+ initialDelay?: number;
26
+ /** Maximum delay cap in ms. Default: `30_000`. */
27
+ maxDelay?: number;
28
+ /** Backoff multiplier. Default: `2`. */
29
+ factor?: number;
30
+ /** Jitter strategy. Default: `'equal'`. */
31
+ jitter?: JitterStrategy;
32
+ /**
33
+ * Return `false` to stop retrying immediately.
34
+ * Receives the error and full context. May be async.
35
+ */
36
+ retryIf?: (error: unknown, context: RetryContext) => boolean | Promise<boolean>;
37
+ /** Called before each retry delay. */
38
+ onRetry?: (attemptNumber: number, error: unknown, delayMs: number, context: RetryContext) => void;
39
+ /** AbortSignal — cancels pending retry delays. */
40
+ signal?: AbortSignal;
41
+ /** Per-attempt timeout in ms. Throws `RetryxTimeoutError` if exceeded. */
42
+ timeoutMs?: number;
43
+ }
44
+ /** All possible circuit breaker states. */
45
+ type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
46
+ /** Constructor options for `CircuitBreaker`. */
47
+ interface CircuitBreakerOptions {
48
+ /** Number of failures before opening the circuit. */
49
+ failureThreshold: number;
50
+ /** Successes in HALF_OPEN state needed to re-close. */
51
+ successThreshold: number;
52
+ /** How long (ms) to stay OPEN before moving to HALF_OPEN. */
53
+ openDurationMs: number;
54
+ /** Minimum calls in the window before the circuit can trip. Default: `1`. */
55
+ volumeThreshold?: number;
56
+ /** Called whenever the circuit transitions between states. */
57
+ onStateChange?: (from: CircuitState, to: CircuitState) => void;
58
+ }
59
+ /** Snapshot of `CircuitBreaker` counters and state. */
60
+ interface CircuitBreakerStats {
61
+ failures: number;
62
+ successes: number;
63
+ calls: number;
64
+ state: CircuitState;
65
+ }
66
+
67
+ declare class RetryxError extends Error {
68
+ readonly attempts: number;
69
+ readonly lastError: unknown;
70
+ /** Every error thrown across all attempts, in order. */
71
+ readonly allErrors: unknown[];
72
+ constructor(message: string, attempts: number, lastError: unknown,
73
+ /** Every error thrown across all attempts, in order. */
74
+ allErrors: unknown[]);
75
+ }
76
+ declare class RetryxTimeoutError extends Error {
77
+ readonly attempt: number;
78
+ readonly timeoutMs: number;
79
+ constructor(attempt: number, timeoutMs: number);
80
+ }
81
+ /**
82
+ * Retry an async operation with exponential backoff, jitter, and context-aware hooks.
83
+ *
84
+ * @example
85
+ * const data = await retry(() => fetch('/api').then(r => r.json()), { maxAttempts: 5 });
86
+ */
87
+ declare function retry<T>(task: () => Promise<T>, options?: RetryxOptions): Promise<T>;
88
+ /**
89
+ * Create a reusable retry function with pre-configured options.
90
+ *
91
+ * @example
92
+ * const resilient = createRetry({ maxAttempts: 5 });
93
+ * await resilient(() => callApi());
94
+ */
95
+ declare function createRetry(defaults: RetryxOptions): <T>(task: () => Promise<T>, overrides?: RetryxOptions) => Promise<T>;
96
+ /**
97
+ * Wraps an async function so that every call is automatically retried.
98
+ *
99
+ * @example
100
+ * const resilientFetch = withRetry(fetch, { maxAttempts: 3 });
101
+ * const resp = await resilientFetch('/api/users'); // retried on failure
102
+ */
103
+ declare function withRetry<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => Promise<TReturn>, options: RetryxOptions): (...args: TArgs) => Promise<TReturn>;
104
+ declare class CircuitOpenError extends Error {
105
+ readonly retryAfterMs: number;
106
+ constructor(retryAfterMs: number);
107
+ }
108
+ /**
109
+ * Circuit breaker wrapping any async operation.
110
+ *
111
+ * States:
112
+ * - **CLOSED** — calls pass through normally.
113
+ * - **OPEN** — calls fail fast with `CircuitOpenError`.
114
+ * - **HALF_OPEN** — one probe call is allowed; success closes, failure re-opens.
115
+ *
116
+ * @example
117
+ * const cb = new CircuitBreaker({ failureThreshold: 5, successThreshold: 2, openDurationMs: 10_000 });
118
+ * const result = await cb.run(() => callExternalService());
119
+ */
120
+ declare class CircuitBreaker {
121
+ private state;
122
+ private failures;
123
+ private successes;
124
+ private calls;
125
+ private openedAt;
126
+ private readonly failureThreshold;
127
+ private readonly successThreshold;
128
+ private readonly openDurationMs;
129
+ private readonly volumeThreshold;
130
+ private readonly onStateChange?;
131
+ constructor(options: CircuitBreakerOptions);
132
+ get currentState(): CircuitState;
133
+ run<T>(task: () => Promise<T>): Promise<T>;
134
+ /** Manually reset the circuit to CLOSED state. */
135
+ reset(): void;
136
+ stats(): CircuitBreakerStats;
137
+ private checkHalfOpen;
138
+ private onSuccess;
139
+ private onFailure;
140
+ private transition;
141
+ }
142
+
143
+ export { CircuitBreaker, type CircuitBreakerOptions, type CircuitBreakerStats, CircuitOpenError, type CircuitState, type JitterStrategy, type RetryContext, RetryxError, type RetryxOptions, RetryxTimeoutError, createRetry, retry, withRetry };
package/dist/index.js ADDED
@@ -0,0 +1,224 @@
1
+ 'use strict';
2
+
3
+ // src/retryx.ts
4
+ var RetryxError = class extends Error {
5
+ constructor(message, attempts, lastError, allErrors) {
6
+ super(message);
7
+ this.attempts = attempts;
8
+ this.lastError = lastError;
9
+ this.allErrors = allErrors;
10
+ this.name = "RetryxError";
11
+ }
12
+ };
13
+ var RetryxTimeoutError = class extends Error {
14
+ constructor(attempt, timeoutMs) {
15
+ super(`Attempt ${attempt} timed out after ${timeoutMs}ms`);
16
+ this.attempt = attempt;
17
+ this.timeoutMs = timeoutMs;
18
+ this.name = "RetryxTimeoutError";
19
+ }
20
+ };
21
+ function abortableDelay(ms, signal) {
22
+ return new Promise((resolve, reject) => {
23
+ if (signal?.aborted) {
24
+ reject(new DOMException("Aborted", "AbortError"));
25
+ return;
26
+ }
27
+ const timer = setTimeout(resolve, ms);
28
+ signal?.addEventListener(
29
+ "abort",
30
+ () => {
31
+ clearTimeout(timer);
32
+ reject(new DOMException("Aborted", "AbortError"));
33
+ },
34
+ { once: true }
35
+ );
36
+ });
37
+ }
38
+ function raceTimeout(promise, ms, attempt) {
39
+ return new Promise((resolve, reject) => {
40
+ const timer = setTimeout(() => reject(new RetryxTimeoutError(attempt, ms)), ms);
41
+ promise.then(
42
+ (v) => {
43
+ clearTimeout(timer);
44
+ resolve(v);
45
+ },
46
+ (e) => {
47
+ clearTimeout(timer);
48
+ reject(e);
49
+ }
50
+ );
51
+ });
52
+ }
53
+ function computeDelay(attempt, prevDelay, opts) {
54
+ const cap = Math.min(opts.initialDelay * Math.pow(opts.factor, attempt), opts.maxDelay);
55
+ switch (opts.jitter) {
56
+ case "none":
57
+ return cap;
58
+ case "full":
59
+ return Math.floor(Math.random() * cap);
60
+ case "decorrelated":
61
+ return Math.min(opts.maxDelay, Math.floor(opts.initialDelay + Math.random() * (prevDelay * 3 - opts.initialDelay)));
62
+ case "equal":
63
+ default:
64
+ return Math.floor(cap / 2 + Math.random() * (cap / 2));
65
+ }
66
+ }
67
+ async function retry(task, options = {}) {
68
+ const {
69
+ maxAttempts = 3,
70
+ initialDelay = 200,
71
+ maxDelay = 3e4,
72
+ factor = 2,
73
+ jitter = "equal",
74
+ retryIf = () => true,
75
+ onRetry,
76
+ signal,
77
+ timeoutMs
78
+ } = options;
79
+ const allErrors = [];
80
+ const startTime = Date.now();
81
+ let prevDelay = initialDelay;
82
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
83
+ try {
84
+ return timeoutMs != null ? await raceTimeout(task(), timeoutMs, attempt + 1) : await task();
85
+ } catch (err) {
86
+ allErrors.push(err);
87
+ if (attempt === maxAttempts - 1) break;
88
+ const context = {
89
+ attemptNumber: attempt + 1,
90
+ totalAttempts: maxAttempts,
91
+ elapsedMs: Date.now() - startTime,
92
+ errors: [...allErrors]
93
+ };
94
+ const shouldRetry = await retryIf(err, context);
95
+ if (!shouldRetry) break;
96
+ const delayMs = computeDelay(attempt, prevDelay, { initialDelay, maxDelay, factor, jitter });
97
+ prevDelay = delayMs;
98
+ onRetry?.(attempt + 1, err, delayMs, context);
99
+ await abortableDelay(delayMs, signal);
100
+ }
101
+ }
102
+ throw allErrors.length === 1 && allErrors[0] instanceof RetryxTimeoutError ? allErrors[0] : new RetryxError(
103
+ `All ${maxAttempts} attempts failed`,
104
+ maxAttempts,
105
+ allErrors[allErrors.length - 1],
106
+ allErrors
107
+ );
108
+ }
109
+ function createRetry(defaults) {
110
+ return (task, overrides) => retry(task, { ...defaults, ...overrides });
111
+ }
112
+ function withRetry(fn, options) {
113
+ return (...args) => retry(() => fn(...args), options);
114
+ }
115
+ var CircuitOpenError = class extends Error {
116
+ constructor(retryAfterMs) {
117
+ super(`Circuit is OPEN. Retry after ${retryAfterMs}ms`);
118
+ this.retryAfterMs = retryAfterMs;
119
+ this.name = "CircuitOpenError";
120
+ }
121
+ };
122
+ var CircuitBreaker = class {
123
+ state = "CLOSED";
124
+ failures = 0;
125
+ successes = 0;
126
+ calls = 0;
127
+ openedAt = 0;
128
+ failureThreshold;
129
+ successThreshold;
130
+ openDurationMs;
131
+ volumeThreshold;
132
+ onStateChange;
133
+ constructor(options) {
134
+ this.failureThreshold = options.failureThreshold;
135
+ this.successThreshold = options.successThreshold;
136
+ this.openDurationMs = options.openDurationMs;
137
+ this.volumeThreshold = options.volumeThreshold ?? 1;
138
+ this.onStateChange = options.onStateChange;
139
+ }
140
+ get currentState() {
141
+ this.checkHalfOpen();
142
+ return this.state;
143
+ }
144
+ async run(task) {
145
+ this.checkHalfOpen();
146
+ if (this.state === "OPEN") {
147
+ const retryAfterMs = Math.max(0, this.openedAt + this.openDurationMs - Date.now());
148
+ throw new CircuitOpenError(retryAfterMs);
149
+ }
150
+ this.calls++;
151
+ try {
152
+ const result = await task();
153
+ this.onSuccess();
154
+ return result;
155
+ } catch (err) {
156
+ this.onFailure();
157
+ throw err;
158
+ }
159
+ }
160
+ /** Manually reset the circuit to CLOSED state. */
161
+ reset() {
162
+ const prev = this.state;
163
+ this.state = "CLOSED";
164
+ this.failures = 0;
165
+ this.successes = 0;
166
+ this.calls = 0;
167
+ this.openedAt = 0;
168
+ if (prev !== "CLOSED") this.onStateChange?.(prev, "CLOSED");
169
+ }
170
+ stats() {
171
+ return {
172
+ failures: this.failures,
173
+ successes: this.successes,
174
+ calls: this.calls,
175
+ state: this.state
176
+ };
177
+ }
178
+ checkHalfOpen() {
179
+ if (this.state === "OPEN" && Date.now() >= this.openedAt + this.openDurationMs) {
180
+ this.transition("HALF_OPEN");
181
+ }
182
+ }
183
+ onSuccess() {
184
+ this.failures = 0;
185
+ if (this.state === "HALF_OPEN") {
186
+ this.successes++;
187
+ if (this.successes >= this.successThreshold) {
188
+ this.transition("CLOSED");
189
+ }
190
+ }
191
+ }
192
+ onFailure() {
193
+ this.failures++;
194
+ if (this.state === "HALF_OPEN") {
195
+ this.openedAt = Date.now();
196
+ this.transition("OPEN");
197
+ } else if (this.state === "CLOSED" && this.calls >= this.volumeThreshold && this.failures >= this.failureThreshold) {
198
+ this.openedAt = Date.now();
199
+ this.transition("OPEN");
200
+ }
201
+ }
202
+ transition(next) {
203
+ const prev = this.state;
204
+ this.state = next;
205
+ if (next === "CLOSED") {
206
+ this.failures = 0;
207
+ this.successes = 0;
208
+ }
209
+ if (next === "HALF_OPEN") {
210
+ this.successes = 0;
211
+ }
212
+ this.onStateChange?.(prev, next);
213
+ }
214
+ };
215
+
216
+ exports.CircuitBreaker = CircuitBreaker;
217
+ exports.CircuitOpenError = CircuitOpenError;
218
+ exports.RetryxError = RetryxError;
219
+ exports.RetryxTimeoutError = RetryxTimeoutError;
220
+ exports.createRetry = createRetry;
221
+ exports.retry = retry;
222
+ exports.withRetry = withRetry;
223
+ //# sourceMappingURL=index.js.map
224
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/retryx.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export type { JitterStrategy, RetryContext, RetryxOptions, CircuitState, CircuitBreakerOptions, CircuitBreakerStats } from './types.js';\nimport type { JitterStrategy, RetryContext, RetryxOptions, CircuitState, CircuitBreakerOptions, CircuitBreakerStats } from './types.js';\n\n// ─── Error Types ─────────────────────────────────────────────────────────────\n\nexport class RetryxError extends Error {\n constructor(\n message: string,\n public readonly attempts: number,\n public readonly lastError: unknown,\n /** Every error thrown across all attempts, in order. */\n public readonly allErrors: unknown[]\n ) {\n super(message);\n this.name = 'RetryxError';\n }\n}\n\nexport class RetryxTimeoutError extends Error {\n constructor(\n public readonly attempt: number,\n public readonly timeoutMs: number\n ) {\n super(`Attempt ${attempt} timed out after ${timeoutMs}ms`);\n this.name = 'RetryxTimeoutError';\n }\n}\n\n// ─── Internal Helpers ────────────────────────────────────────────────────────\n\nfunction abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException('Aborted', 'AbortError'));\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n 'abort',\n () => { clearTimeout(timer); reject(new DOMException('Aborted', 'AbortError')); },\n { once: true }\n );\n });\n}\n\nfunction raceTimeout<T>(promise: Promise<T>, ms: number, attempt: number): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new RetryxTimeoutError(attempt, ms)), ms);\n promise.then(\n (v) => { clearTimeout(timer); resolve(v); },\n (e) => { clearTimeout(timer); reject(e); }\n );\n });\n}\n\nfunction computeDelay(\n attempt: number,\n prevDelay: number,\n opts: Required<Pick<RetryxOptions, 'initialDelay' | 'maxDelay' | 'factor' | 'jitter'>>\n): number {\n const cap = Math.min(opts.initialDelay * Math.pow(opts.factor, attempt), opts.maxDelay);\n\n switch (opts.jitter) {\n case 'none':\n return cap;\n case 'full':\n return Math.floor(Math.random() * cap);\n case 'decorrelated':\n return Math.min(opts.maxDelay, Math.floor(opts.initialDelay + Math.random() * (prevDelay * 3 - opts.initialDelay)));\n case 'equal':\n default:\n return Math.floor(cap / 2 + Math.random() * (cap / 2));\n }\n}\n\n// ─── retry ───────────────────────────────────────────────────────────────────\n\n/**\n * Retry an async operation with exponential backoff, jitter, and context-aware hooks.\n *\n * @example\n * const data = await retry(() => fetch('/api').then(r => r.json()), { maxAttempts: 5 });\n */\nexport async function retry<T>(\n task: () => Promise<T>,\n options: RetryxOptions = {}\n): Promise<T> {\n const {\n maxAttempts = 3,\n initialDelay = 200,\n maxDelay = 30_000,\n factor = 2,\n jitter = 'equal',\n retryIf = () => true,\n onRetry,\n signal,\n timeoutMs,\n } = options;\n\n const allErrors: unknown[] = [];\n const startTime = Date.now();\n let prevDelay = initialDelay;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return timeoutMs != null\n ? await raceTimeout(task(), timeoutMs, attempt + 1)\n : await task();\n } catch (err) {\n // Timeout errors always propagate — don't retry by default but respect retryIf\n allErrors.push(err);\n\n if (attempt === maxAttempts - 1) break;\n\n const context: RetryContext = {\n attemptNumber: attempt + 1,\n totalAttempts: maxAttempts,\n elapsedMs: Date.now() - startTime,\n errors: [...allErrors],\n };\n\n const shouldRetry = await retryIf(err, context);\n if (!shouldRetry) break;\n\n const delayMs = computeDelay(attempt, prevDelay, { initialDelay, maxDelay, factor, jitter });\n prevDelay = delayMs;\n\n onRetry?.(attempt + 1, err, delayMs, context);\n await abortableDelay(delayMs, signal);\n }\n }\n\n throw allErrors.length === 1 && allErrors[0] instanceof RetryxTimeoutError\n ? allErrors[0]\n : new RetryxError(\n `All ${maxAttempts} attempts failed`,\n maxAttempts,\n allErrors[allErrors.length - 1],\n allErrors\n );\n}\n\n// ─── createRetry ─────────────────────────────────────────────────────────────\n\n/**\n * Create a reusable retry function with pre-configured options.\n *\n * @example\n * const resilient = createRetry({ maxAttempts: 5 });\n * await resilient(() => callApi());\n */\nexport function createRetry(\n defaults: RetryxOptions\n): <T>(task: () => Promise<T>, overrides?: RetryxOptions) => Promise<T> {\n return (task, overrides) => retry(task, { ...defaults, ...overrides });\n}\n\n// ─── withRetry ───────────────────────────────────────────────────────────────\n\n/**\n * Wraps an async function so that every call is automatically retried.\n *\n * @example\n * const resilientFetch = withRetry(fetch, { maxAttempts: 3 });\n * const resp = await resilientFetch('/api/users'); // retried on failure\n */\nexport function withRetry<TArgs extends unknown[], TReturn>(\n fn: (...args: TArgs) => Promise<TReturn>,\n options: RetryxOptions\n): (...args: TArgs) => Promise<TReturn> {\n return (...args: TArgs) => retry(() => fn(...args), options);\n}\n\n// ─── CircuitBreaker ──────────────────────────────────────────────────────────\n\nexport class CircuitOpenError extends Error {\n constructor(public readonly retryAfterMs: number) {\n super(`Circuit is OPEN. Retry after ${retryAfterMs}ms`);\n this.name = 'CircuitOpenError';\n }\n}\n\n/**\n * Circuit breaker wrapping any async operation.\n *\n * States:\n * - **CLOSED** — calls pass through normally.\n * - **OPEN** — calls fail fast with `CircuitOpenError`.\n * - **HALF_OPEN** — one probe call is allowed; success closes, failure re-opens.\n *\n * @example\n * const cb = new CircuitBreaker({ failureThreshold: 5, successThreshold: 2, openDurationMs: 10_000 });\n * const result = await cb.run(() => callExternalService());\n */\nexport class CircuitBreaker {\n private state: CircuitState = 'CLOSED';\n private failures = 0;\n private successes = 0;\n private calls = 0;\n private openedAt = 0;\n\n private readonly failureThreshold: number;\n private readonly successThreshold: number;\n private readonly openDurationMs: number;\n private readonly volumeThreshold: number;\n private readonly onStateChange?: (from: CircuitState, to: CircuitState) => void;\n\n constructor(options: CircuitBreakerOptions) {\n this.failureThreshold = options.failureThreshold;\n this.successThreshold = options.successThreshold;\n this.openDurationMs = options.openDurationMs;\n this.volumeThreshold = options.volumeThreshold ?? 1;\n this.onStateChange = options.onStateChange;\n }\n\n get currentState(): CircuitState {\n this.checkHalfOpen();\n return this.state;\n }\n\n async run<T>(task: () => Promise<T>): Promise<T> {\n this.checkHalfOpen();\n\n if (this.state === 'OPEN') {\n const retryAfterMs = Math.max(0, this.openedAt + this.openDurationMs - Date.now());\n throw new CircuitOpenError(retryAfterMs);\n }\n\n this.calls++;\n\n try {\n const result = await task();\n this.onSuccess();\n return result;\n } catch (err) {\n this.onFailure();\n throw err;\n }\n }\n\n /** Manually reset the circuit to CLOSED state. */\n reset(): void {\n const prev = this.state;\n this.state = 'CLOSED';\n this.failures = 0;\n this.successes = 0;\n this.calls = 0;\n this.openedAt = 0;\n if (prev !== 'CLOSED') this.onStateChange?.(prev, 'CLOSED');\n }\n\n stats(): CircuitBreakerStats {\n return {\n failures: this.failures,\n successes: this.successes,\n calls: this.calls,\n state: this.state,\n };\n }\n\n private checkHalfOpen(): void {\n if (this.state === 'OPEN' && Date.now() >= this.openedAt + this.openDurationMs) {\n this.transition('HALF_OPEN');\n }\n }\n\n private onSuccess(): void {\n this.failures = 0;\n if (this.state === 'HALF_OPEN') {\n this.successes++;\n if (this.successes >= this.successThreshold) {\n this.transition('CLOSED');\n }\n }\n }\n\n private onFailure(): void {\n this.failures++;\n if (this.state === 'HALF_OPEN') {\n this.openedAt = Date.now();\n this.transition('OPEN');\n } else if (this.state === 'CLOSED' && this.calls >= this.volumeThreshold && this.failures >= this.failureThreshold) {\n this.openedAt = Date.now();\n this.transition('OPEN');\n }\n }\n\n private transition(next: CircuitState): void {\n const prev = this.state;\n this.state = next;\n if (next === 'CLOSED') { this.failures = 0; this.successes = 0; }\n if (next === 'HALF_OPEN') { this.successes = 0; }\n this.onStateChange?.(prev, next);\n }\n}\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,216 @@
1
+ // src/retryx.ts
2
+ var RetryxError = class extends Error {
3
+ constructor(message, attempts, lastError, allErrors) {
4
+ super(message);
5
+ this.attempts = attempts;
6
+ this.lastError = lastError;
7
+ this.allErrors = allErrors;
8
+ this.name = "RetryxError";
9
+ }
10
+ };
11
+ var RetryxTimeoutError = class extends Error {
12
+ constructor(attempt, timeoutMs) {
13
+ super(`Attempt ${attempt} timed out after ${timeoutMs}ms`);
14
+ this.attempt = attempt;
15
+ this.timeoutMs = timeoutMs;
16
+ this.name = "RetryxTimeoutError";
17
+ }
18
+ };
19
+ function abortableDelay(ms, signal) {
20
+ return new Promise((resolve, reject) => {
21
+ if (signal?.aborted) {
22
+ reject(new DOMException("Aborted", "AbortError"));
23
+ return;
24
+ }
25
+ const timer = setTimeout(resolve, ms);
26
+ signal?.addEventListener(
27
+ "abort",
28
+ () => {
29
+ clearTimeout(timer);
30
+ reject(new DOMException("Aborted", "AbortError"));
31
+ },
32
+ { once: true }
33
+ );
34
+ });
35
+ }
36
+ function raceTimeout(promise, ms, attempt) {
37
+ return new Promise((resolve, reject) => {
38
+ const timer = setTimeout(() => reject(new RetryxTimeoutError(attempt, ms)), ms);
39
+ promise.then(
40
+ (v) => {
41
+ clearTimeout(timer);
42
+ resolve(v);
43
+ },
44
+ (e) => {
45
+ clearTimeout(timer);
46
+ reject(e);
47
+ }
48
+ );
49
+ });
50
+ }
51
+ function computeDelay(attempt, prevDelay, opts) {
52
+ const cap = Math.min(opts.initialDelay * Math.pow(opts.factor, attempt), opts.maxDelay);
53
+ switch (opts.jitter) {
54
+ case "none":
55
+ return cap;
56
+ case "full":
57
+ return Math.floor(Math.random() * cap);
58
+ case "decorrelated":
59
+ return Math.min(opts.maxDelay, Math.floor(opts.initialDelay + Math.random() * (prevDelay * 3 - opts.initialDelay)));
60
+ case "equal":
61
+ default:
62
+ return Math.floor(cap / 2 + Math.random() * (cap / 2));
63
+ }
64
+ }
65
+ async function retry(task, options = {}) {
66
+ const {
67
+ maxAttempts = 3,
68
+ initialDelay = 200,
69
+ maxDelay = 3e4,
70
+ factor = 2,
71
+ jitter = "equal",
72
+ retryIf = () => true,
73
+ onRetry,
74
+ signal,
75
+ timeoutMs
76
+ } = options;
77
+ const allErrors = [];
78
+ const startTime = Date.now();
79
+ let prevDelay = initialDelay;
80
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
81
+ try {
82
+ return timeoutMs != null ? await raceTimeout(task(), timeoutMs, attempt + 1) : await task();
83
+ } catch (err) {
84
+ allErrors.push(err);
85
+ if (attempt === maxAttempts - 1) break;
86
+ const context = {
87
+ attemptNumber: attempt + 1,
88
+ totalAttempts: maxAttempts,
89
+ elapsedMs: Date.now() - startTime,
90
+ errors: [...allErrors]
91
+ };
92
+ const shouldRetry = await retryIf(err, context);
93
+ if (!shouldRetry) break;
94
+ const delayMs = computeDelay(attempt, prevDelay, { initialDelay, maxDelay, factor, jitter });
95
+ prevDelay = delayMs;
96
+ onRetry?.(attempt + 1, err, delayMs, context);
97
+ await abortableDelay(delayMs, signal);
98
+ }
99
+ }
100
+ throw allErrors.length === 1 && allErrors[0] instanceof RetryxTimeoutError ? allErrors[0] : new RetryxError(
101
+ `All ${maxAttempts} attempts failed`,
102
+ maxAttempts,
103
+ allErrors[allErrors.length - 1],
104
+ allErrors
105
+ );
106
+ }
107
+ function createRetry(defaults) {
108
+ return (task, overrides) => retry(task, { ...defaults, ...overrides });
109
+ }
110
+ function withRetry(fn, options) {
111
+ return (...args) => retry(() => fn(...args), options);
112
+ }
113
+ var CircuitOpenError = class extends Error {
114
+ constructor(retryAfterMs) {
115
+ super(`Circuit is OPEN. Retry after ${retryAfterMs}ms`);
116
+ this.retryAfterMs = retryAfterMs;
117
+ this.name = "CircuitOpenError";
118
+ }
119
+ };
120
+ var CircuitBreaker = class {
121
+ state = "CLOSED";
122
+ failures = 0;
123
+ successes = 0;
124
+ calls = 0;
125
+ openedAt = 0;
126
+ failureThreshold;
127
+ successThreshold;
128
+ openDurationMs;
129
+ volumeThreshold;
130
+ onStateChange;
131
+ constructor(options) {
132
+ this.failureThreshold = options.failureThreshold;
133
+ this.successThreshold = options.successThreshold;
134
+ this.openDurationMs = options.openDurationMs;
135
+ this.volumeThreshold = options.volumeThreshold ?? 1;
136
+ this.onStateChange = options.onStateChange;
137
+ }
138
+ get currentState() {
139
+ this.checkHalfOpen();
140
+ return this.state;
141
+ }
142
+ async run(task) {
143
+ this.checkHalfOpen();
144
+ if (this.state === "OPEN") {
145
+ const retryAfterMs = Math.max(0, this.openedAt + this.openDurationMs - Date.now());
146
+ throw new CircuitOpenError(retryAfterMs);
147
+ }
148
+ this.calls++;
149
+ try {
150
+ const result = await task();
151
+ this.onSuccess();
152
+ return result;
153
+ } catch (err) {
154
+ this.onFailure();
155
+ throw err;
156
+ }
157
+ }
158
+ /** Manually reset the circuit to CLOSED state. */
159
+ reset() {
160
+ const prev = this.state;
161
+ this.state = "CLOSED";
162
+ this.failures = 0;
163
+ this.successes = 0;
164
+ this.calls = 0;
165
+ this.openedAt = 0;
166
+ if (prev !== "CLOSED") this.onStateChange?.(prev, "CLOSED");
167
+ }
168
+ stats() {
169
+ return {
170
+ failures: this.failures,
171
+ successes: this.successes,
172
+ calls: this.calls,
173
+ state: this.state
174
+ };
175
+ }
176
+ checkHalfOpen() {
177
+ if (this.state === "OPEN" && Date.now() >= this.openedAt + this.openDurationMs) {
178
+ this.transition("HALF_OPEN");
179
+ }
180
+ }
181
+ onSuccess() {
182
+ this.failures = 0;
183
+ if (this.state === "HALF_OPEN") {
184
+ this.successes++;
185
+ if (this.successes >= this.successThreshold) {
186
+ this.transition("CLOSED");
187
+ }
188
+ }
189
+ }
190
+ onFailure() {
191
+ this.failures++;
192
+ if (this.state === "HALF_OPEN") {
193
+ this.openedAt = Date.now();
194
+ this.transition("OPEN");
195
+ } else if (this.state === "CLOSED" && this.calls >= this.volumeThreshold && this.failures >= this.failureThreshold) {
196
+ this.openedAt = Date.now();
197
+ this.transition("OPEN");
198
+ }
199
+ }
200
+ transition(next) {
201
+ const prev = this.state;
202
+ this.state = next;
203
+ if (next === "CLOSED") {
204
+ this.failures = 0;
205
+ this.successes = 0;
206
+ }
207
+ if (next === "HALF_OPEN") {
208
+ this.successes = 0;
209
+ }
210
+ this.onStateChange?.(prev, next);
211
+ }
212
+ };
213
+
214
+ export { CircuitBreaker, CircuitOpenError, RetryxError, RetryxTimeoutError, createRetry, retry, withRetry };
215
+ //# sourceMappingURL=index.mjs.map
216
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/retryx.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["export type { JitterStrategy, RetryContext, RetryxOptions, CircuitState, CircuitBreakerOptions, CircuitBreakerStats } from './types.js';\nimport type { JitterStrategy, RetryContext, RetryxOptions, CircuitState, CircuitBreakerOptions, CircuitBreakerStats } from './types.js';\n\n// ─── Error Types ─────────────────────────────────────────────────────────────\n\nexport class RetryxError extends Error {\n constructor(\n message: string,\n public readonly attempts: number,\n public readonly lastError: unknown,\n /** Every error thrown across all attempts, in order. */\n public readonly allErrors: unknown[]\n ) {\n super(message);\n this.name = 'RetryxError';\n }\n}\n\nexport class RetryxTimeoutError extends Error {\n constructor(\n public readonly attempt: number,\n public readonly timeoutMs: number\n ) {\n super(`Attempt ${attempt} timed out after ${timeoutMs}ms`);\n this.name = 'RetryxTimeoutError';\n }\n}\n\n// ─── Internal Helpers ────────────────────────────────────────────────────────\n\nfunction abortableDelay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException('Aborted', 'AbortError'));\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n 'abort',\n () => { clearTimeout(timer); reject(new DOMException('Aborted', 'AbortError')); },\n { once: true }\n );\n });\n}\n\nfunction raceTimeout<T>(promise: Promise<T>, ms: number, attempt: number): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new RetryxTimeoutError(attempt, ms)), ms);\n promise.then(\n (v) => { clearTimeout(timer); resolve(v); },\n (e) => { clearTimeout(timer); reject(e); }\n );\n });\n}\n\nfunction computeDelay(\n attempt: number,\n prevDelay: number,\n opts: Required<Pick<RetryxOptions, 'initialDelay' | 'maxDelay' | 'factor' | 'jitter'>>\n): number {\n const cap = Math.min(opts.initialDelay * Math.pow(opts.factor, attempt), opts.maxDelay);\n\n switch (opts.jitter) {\n case 'none':\n return cap;\n case 'full':\n return Math.floor(Math.random() * cap);\n case 'decorrelated':\n return Math.min(opts.maxDelay, Math.floor(opts.initialDelay + Math.random() * (prevDelay * 3 - opts.initialDelay)));\n case 'equal':\n default:\n return Math.floor(cap / 2 + Math.random() * (cap / 2));\n }\n}\n\n// ─── retry ───────────────────────────────────────────────────────────────────\n\n/**\n * Retry an async operation with exponential backoff, jitter, and context-aware hooks.\n *\n * @example\n * const data = await retry(() => fetch('/api').then(r => r.json()), { maxAttempts: 5 });\n */\nexport async function retry<T>(\n task: () => Promise<T>,\n options: RetryxOptions = {}\n): Promise<T> {\n const {\n maxAttempts = 3,\n initialDelay = 200,\n maxDelay = 30_000,\n factor = 2,\n jitter = 'equal',\n retryIf = () => true,\n onRetry,\n signal,\n timeoutMs,\n } = options;\n\n const allErrors: unknown[] = [];\n const startTime = Date.now();\n let prevDelay = initialDelay;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return timeoutMs != null\n ? await raceTimeout(task(), timeoutMs, attempt + 1)\n : await task();\n } catch (err) {\n // Timeout errors always propagate — don't retry by default but respect retryIf\n allErrors.push(err);\n\n if (attempt === maxAttempts - 1) break;\n\n const context: RetryContext = {\n attemptNumber: attempt + 1,\n totalAttempts: maxAttempts,\n elapsedMs: Date.now() - startTime,\n errors: [...allErrors],\n };\n\n const shouldRetry = await retryIf(err, context);\n if (!shouldRetry) break;\n\n const delayMs = computeDelay(attempt, prevDelay, { initialDelay, maxDelay, factor, jitter });\n prevDelay = delayMs;\n\n onRetry?.(attempt + 1, err, delayMs, context);\n await abortableDelay(delayMs, signal);\n }\n }\n\n throw allErrors.length === 1 && allErrors[0] instanceof RetryxTimeoutError\n ? allErrors[0]\n : new RetryxError(\n `All ${maxAttempts} attempts failed`,\n maxAttempts,\n allErrors[allErrors.length - 1],\n allErrors\n );\n}\n\n// ─── createRetry ─────────────────────────────────────────────────────────────\n\n/**\n * Create a reusable retry function with pre-configured options.\n *\n * @example\n * const resilient = createRetry({ maxAttempts: 5 });\n * await resilient(() => callApi());\n */\nexport function createRetry(\n defaults: RetryxOptions\n): <T>(task: () => Promise<T>, overrides?: RetryxOptions) => Promise<T> {\n return (task, overrides) => retry(task, { ...defaults, ...overrides });\n}\n\n// ─── withRetry ───────────────────────────────────────────────────────────────\n\n/**\n * Wraps an async function so that every call is automatically retried.\n *\n * @example\n * const resilientFetch = withRetry(fetch, { maxAttempts: 3 });\n * const resp = await resilientFetch('/api/users'); // retried on failure\n */\nexport function withRetry<TArgs extends unknown[], TReturn>(\n fn: (...args: TArgs) => Promise<TReturn>,\n options: RetryxOptions\n): (...args: TArgs) => Promise<TReturn> {\n return (...args: TArgs) => retry(() => fn(...args), options);\n}\n\n// ─── CircuitBreaker ──────────────────────────────────────────────────────────\n\nexport class CircuitOpenError extends Error {\n constructor(public readonly retryAfterMs: number) {\n super(`Circuit is OPEN. Retry after ${retryAfterMs}ms`);\n this.name = 'CircuitOpenError';\n }\n}\n\n/**\n * Circuit breaker wrapping any async operation.\n *\n * States:\n * - **CLOSED** — calls pass through normally.\n * - **OPEN** — calls fail fast with `CircuitOpenError`.\n * - **HALF_OPEN** — one probe call is allowed; success closes, failure re-opens.\n *\n * @example\n * const cb = new CircuitBreaker({ failureThreshold: 5, successThreshold: 2, openDurationMs: 10_000 });\n * const result = await cb.run(() => callExternalService());\n */\nexport class CircuitBreaker {\n private state: CircuitState = 'CLOSED';\n private failures = 0;\n private successes = 0;\n private calls = 0;\n private openedAt = 0;\n\n private readonly failureThreshold: number;\n private readonly successThreshold: number;\n private readonly openDurationMs: number;\n private readonly volumeThreshold: number;\n private readonly onStateChange?: (from: CircuitState, to: CircuitState) => void;\n\n constructor(options: CircuitBreakerOptions) {\n this.failureThreshold = options.failureThreshold;\n this.successThreshold = options.successThreshold;\n this.openDurationMs = options.openDurationMs;\n this.volumeThreshold = options.volumeThreshold ?? 1;\n this.onStateChange = options.onStateChange;\n }\n\n get currentState(): CircuitState {\n this.checkHalfOpen();\n return this.state;\n }\n\n async run<T>(task: () => Promise<T>): Promise<T> {\n this.checkHalfOpen();\n\n if (this.state === 'OPEN') {\n const retryAfterMs = Math.max(0, this.openedAt + this.openDurationMs - Date.now());\n throw new CircuitOpenError(retryAfterMs);\n }\n\n this.calls++;\n\n try {\n const result = await task();\n this.onSuccess();\n return result;\n } catch (err) {\n this.onFailure();\n throw err;\n }\n }\n\n /** Manually reset the circuit to CLOSED state. */\n reset(): void {\n const prev = this.state;\n this.state = 'CLOSED';\n this.failures = 0;\n this.successes = 0;\n this.calls = 0;\n this.openedAt = 0;\n if (prev !== 'CLOSED') this.onStateChange?.(prev, 'CLOSED');\n }\n\n stats(): CircuitBreakerStats {\n return {\n failures: this.failures,\n successes: this.successes,\n calls: this.calls,\n state: this.state,\n };\n }\n\n private checkHalfOpen(): void {\n if (this.state === 'OPEN' && Date.now() >= this.openedAt + this.openDurationMs) {\n this.transition('HALF_OPEN');\n }\n }\n\n private onSuccess(): void {\n this.failures = 0;\n if (this.state === 'HALF_OPEN') {\n this.successes++;\n if (this.successes >= this.successThreshold) {\n this.transition('CLOSED');\n }\n }\n }\n\n private onFailure(): void {\n this.failures++;\n if (this.state === 'HALF_OPEN') {\n this.openedAt = Date.now();\n this.transition('OPEN');\n } else if (this.state === 'CLOSED' && this.calls >= this.volumeThreshold && this.failures >= this.failureThreshold) {\n this.openedAt = Date.now();\n this.transition('OPEN');\n }\n }\n\n private transition(next: CircuitState): void {\n const prev = this.state;\n this.state = next;\n if (next === 'CLOSED') { this.failures = 0; this.successes = 0; }\n if (next === 'HALF_OPEN') { this.successes = 0; }\n this.onStateChange?.(prev, next);\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@async-kit/retryx",
3
+ "version": "0.1.2",
4
+ "description": "Smart async retry system with exponential backoff, jitter, and circuit breaker for JavaScript/TypeScript",
5
+ "keywords": [
6
+ "async",
7
+ "retry",
8
+ "backoff",
9
+ "circuit-breaker",
10
+ "resilience",
11
+ "fault-tolerance"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/NexaLeaf/async-kit",
17
+ "directory": "packages/retryx"
18
+ },
19
+ "homepage": "https://github.com/NexaLeaf/async-kit/tree/main/packages/retryx#readme",
20
+ "main": "./dist/index.cjs",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "import": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ },
29
+ "require": {
30
+ "types": "./dist/index.d.cts",
31
+ "default": "./dist/index.cjs"
32
+ }
33
+ }
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "README.md",
41
+ "CHANGELOG.md"
42
+ ],
43
+ "sideEffects": false,
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "typecheck": "tsc -p tsconfig.lib.json --noEmit"
47
+ },
48
+ "devDependencies": {}
49
+ }