safe-fetch-iq 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Noteal Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,422 @@
1
+ # safe-fetch-iq – Smart, Safe HTTP Fetch for JS/TS
2
+
3
+ safe-fetch-iq is a small TypeScript library that makes `fetch` safe and ergonomic by default, powered internally by an intelligent retry engine.
4
+ - Drop-in `fetch` replacement (`import fetch from "safe-fetch-iq"`)
5
+ - Smart defaults: retry, timeout, auto JSON/text parsing
6
+ - Request deduplication and response caching
7
+ - Built on native `fetch` (Node 18+ and browsers)
8
+ - Zero runtime dependencies, tiny bundle size
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install safe-fetch-iq
14
+ ```
15
+
16
+ If you use TypeScript, `typescript` and `@types/node` should be available in your project.
17
+
18
+ ## Quick Start
19
+
20
+ ```ts
21
+ import fetch from "safe-fetch-iq";
22
+
23
+ const users = await fetch("https://api.example.com/users");
24
+
25
+ console.log(users);
26
+ ```
27
+
28
+ Default behavior:
29
+
30
+ - Retries network and transient errors up to 3 times
31
+ - Per-attempt timeout of 30 seconds
32
+ - Automatically parses JSON or text based on `Content-Type`
33
+ - Follows redirects using native fetch behavior
34
+ - Throws descriptive errors instead of silent failures
35
+
36
+ ## Smart Options
37
+
38
+ safe-fetch-iq extends the standard `RequestInit` with a few focused options:
39
+
40
+ ```ts
41
+ import fetch from "safe-fetch-iq";
42
+
43
+ const data = await fetch("https://api.example.com/data", {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify({ foo: "bar" }),
47
+
48
+ retry: 5,
49
+ timeout: 10_000,
50
+ retryOn: [429, 503],
51
+ retryDelay: attempt => attempt * 1000,
52
+ dedupe: true,
53
+ cache: "5m"
54
+ });
55
+ ```
56
+
57
+ Supported options:
58
+
59
+ - `retry?: number`
60
+ - Maximum retry attempts (default: `3`)
61
+ - `timeout?: number`
62
+ - Per-attempt timeout in milliseconds (default: `30000`)
63
+ - `retryOn?: number[]`
64
+ - HTTP status codes to retry (e.g. `[429, 503]`)
65
+ - `retryDelay?: (attempt: number) => number`
66
+ - Custom backoff function for delay in ms
67
+ - `dedupe?: boolean`
68
+ - Request deduplication for identical in-flight `GET` requests (default: `true`)
69
+ - `cache?: number | string`
70
+ - Response cache TTL for `GET` requests
71
+ - Number (ms) or string like `"10s"`, `"5m"`, `"1h"`
72
+
73
+ All regular `fetch` options still work. You can also pass an `AbortSignal` via `signal` as usual.
74
+
75
+ ## Request Deduplication
76
+
77
+ When `dedupe` is enabled (default), concurrent identical `GET` requests share the same promise:
78
+
79
+ ```ts
80
+ import fetch from "safe-fetch-iq";
81
+
82
+ const p1 = fetch("https://api.example.com/users");
83
+ const p2 = fetch("https://api.example.com/users");
84
+
85
+ // p1 and p2 resolve with the same underlying network response
86
+ const [a, b] = await Promise.all([p1, p2]);
87
+ ```
88
+
89
+ This avoids stampedes when multiple parts of your app request the same data at the same time.
90
+
91
+ ## Response Caching
92
+
93
+ For `GET` requests, you can enable simple in-memory caching:
94
+
95
+ ```ts
96
+ import fetch from "safe-fetch-iq";
97
+
98
+ // Cache for 5 minutes
99
+ const data = await fetch("https://api.example.com/data", {
100
+ cache: "5m"
101
+ });
102
+ ```
103
+
104
+ - The cache key is based on method+URL+body
105
+ - Calls within the TTL return the cached parsed value
106
+ - After TTL expires, a new request is made and cache is refreshed
107
+
108
+ ## Error Handling
109
+
110
+ safe-fetch-iq throws when:
111
+
112
+ - The response is not `ok` (non-2xx), after applying retries
113
+ - Retries are exhausted or cancelled by rules
114
+
115
+ Example:
116
+
117
+ ```ts
118
+ import fetch, { RetryIQError } from "safe-fetch-iq";
119
+
120
+ try {
121
+ const data = await fetch("https://api.example.com/data", {
122
+ retry: 5,
123
+ retryOn: [429, 503]
124
+ });
125
+ console.log(data);
126
+ } catch (err) {
127
+ if (err instanceof RetryIQError) {
128
+ console.error("Retry failed", err.metadata);
129
+ } else {
130
+ console.error("Unexpected error", err);
131
+ }
132
+ }
133
+ ```
134
+
135
+ `RetryIQError` includes metadata:
136
+
137
+ - `attempt` – last attempt index
138
+ - `totalAttempts`
139
+ - `errorType`
140
+ - `elapsedMs`
141
+
142
+ Non-OK HTTP responses are surfaced as errors that still carry the original `Response` for advanced handling.
143
+
144
+ ## Advanced Use: withRetry Engine
145
+
146
+ Under the hood, safe-fetch-iq is powered by a generic intelligent retry engine. You can use it directly for other clients like `axios`, custom HTTP clients, or even non-HTTP operations.
147
+
148
+ ### Core Idea
149
+
150
+ ```ts
151
+ import { withRetry } from "safe-fetch-iq";
152
+
153
+ const result = await withRetry(async (attempt, signal) => {
154
+ return doSomethingHttpLike(attempt, signal);
155
+ });
156
+ ```
157
+
158
+ The engine decides when to retry, how long to wait, and when to give up.
159
+
160
+ ### Smart Behavior
161
+
162
+ ### 1. Detects Retry-After
163
+
164
+ When the server returns `429` or `5xx` with a `Retry-After` header, the engine parses the header and waits at least that long before the next attempt.
165
+
166
+ - `Retry-After: 10` is interpreted as 10 seconds
167
+ - `Retry-After: Wed, 21 Oct 2015 07:28:00 GMT` is treated as an absolute time
168
+
169
+ If no `Retry-After` header exists, standard backoff is used.
170
+
171
+ ### 2. Error-Aware Backoff
172
+
173
+ Errors are classified into types:
174
+
175
+ - `rateLimit` – typically HTTP `429`
176
+ - `serverError` – HTTP `5xx`
177
+ - `networkError` – connection resets, timeouts, DNS errors
178
+ - `clientError` – `4xx` that should not be retried
179
+ - `unknown` – anything else
180
+
181
+ By default, only `rateLimit`, `serverError`, `networkError`, and `unknown` are retried. Client errors are treated as final.
182
+
183
+ ### 3. Budget-Aware Retries
184
+
185
+ You control how aggressive the retries can be.
186
+
187
+ ```ts
188
+ import { withRetry } from "safe-fetch-iq";
189
+
190
+ const response = await withRetry(
191
+ () => fetch("https://api.example.com/data"),
192
+ {
193
+ budget: {
194
+ maxRetries: 5,
195
+ maxRetryTimeMs: 60_000
196
+ }
197
+ }
198
+ );
199
+ ```
200
+
201
+ - `maxRetries` – maximum number of retry attempts
202
+ - `maxRetryTimeMs` – hard cap on total time spent retrying
203
+
204
+ If the server sends rate-limit headers:
205
+
206
+ - `X-RateLimit-Remaining`
207
+ - `X-Rate-Limit-Remaining`
208
+
209
+ and they indicate that you are out of quota, Retry IQ stops early instead of blindly continuing to hit the API.
210
+
211
+ ## Backoff Configuration
212
+
213
+ ```ts
214
+ import { withRetry } from "safe-fetch-iq";
215
+
216
+ await withRetry(
217
+ () => fetch("https://api.example.com/data"),
218
+ {
219
+ backoff: {
220
+ baseDelayMs: 250,
221
+ maxDelayMs: 30_000,
222
+ factor: 2,
223
+ jitter: "full",
224
+ perErrorType: {
225
+ rateLimit: {
226
+ baseDelayMs: 1000
227
+ }
228
+ }
229
+ }
230
+ }
231
+ );
232
+ ```
233
+
234
+ - `baseDelayMs` – first delay before backoff, default `250`
235
+ - `factor` – exponential factor, default `2`
236
+ - `maxDelayMs` – max per-attempt delay, default `30000`
237
+ - `jitter`
238
+ - `"none"` – deterministic backoff
239
+ - `"full"` – random between `0` and `delay`
240
+ - `"decorrelated"` – random between `delay / 2` and `delay`
241
+ - `perErrorType` – override backoff settings per error type
242
+
243
+ ## shouldRetry Hook
244
+
245
+ Use `shouldRetry` to inject custom rules.
246
+
247
+ ```ts
248
+ import { withRetry } from "safe-fetch-iq";
249
+
250
+ await withRetry(
251
+ () => fetch("https://api.example.com/data"),
252
+ {
253
+ shouldRetry: (error, context) => {
254
+ if (context.errorType === "clientError") return false;
255
+ if (context.attempt > 3) return false;
256
+ return true;
257
+ }
258
+ }
259
+ );
260
+ ```
261
+
262
+ You receive both the error and detailed context, including the calculated next delay.
263
+
264
+ ## Logging
265
+
266
+ Pass a logger to understand how retries behave in production.
267
+
268
+ ```ts
269
+ import { withRetry } from "safe-fetch-iq";
270
+
271
+ await withRetry(
272
+ () => fetch("https://api.example.com/data"),
273
+ {
274
+ logger: event => {
275
+ if (event.type === "retry") {
276
+ console.log(
277
+ `[RetryIQ] ${event.operationName ?? "operation"} attempt ${event.attempt} – waiting ${event.delayMs}ms`
278
+ );
279
+ }
280
+ if (event.type === "giveUp") {
281
+ console.warn(
282
+ `[RetryIQ] giving up after ${event.attempt} attempts due to ${event.reason} (${event.errorType})`
283
+ );
284
+ }
285
+ if (event.type === "succeeded") {
286
+ console.log(
287
+ `[RetryIQ] succeeded in ${event.elapsedMs}ms after ${event.attempt} attempts`
288
+ );
289
+ }
290
+ }
291
+ }
292
+ );
293
+ ```
294
+
295
+ ## Using AbortSignal
296
+
297
+ You can cancel the entire retry sequence with an `AbortSignal`.
298
+
299
+ ```ts
300
+ import { withRetry } from "safe-fetch-iq";
301
+
302
+ const controller = new AbortController();
303
+
304
+ const promise = withRetry(
305
+ (attempt, signal) => fetch("https://api.example.com/data", { signal }),
306
+ {
307
+ signal: controller.signal
308
+ }
309
+ );
310
+
311
+ setTimeout(() => controller.abort(), 5000);
312
+
313
+ await promise;
314
+ ```
315
+
316
+ If the signal is aborted, Retry IQ stops immediately.
317
+
318
+ ## Error Handling
319
+
320
+ When retries are exhausted or cancelled by rules, the engine throws a `RetryIQError`.
321
+
322
+ ```ts
323
+ import { withRetry, RetryIQError } from "safe-fetch-iq";
324
+
325
+ try {
326
+ await withRetry(() => fetch("https://api.example.com/data"));
327
+ } catch (err) {
328
+ if (err instanceof RetryIQError) {
329
+ console.error("Retry failed", err.metadata);
330
+ } else {
331
+ console.error("Unexpected error", err);
332
+ }
333
+ }
334
+ ```
335
+
336
+ `RetryIQError` includes metadata:
337
+
338
+ - `attempt` – last attempt index
339
+ - `totalAttempts`
340
+ - `errorType`
341
+ - `elapsedMs`
342
+
343
+ ## Adapters
344
+
345
+ ### fetch adapter
346
+
347
+ If you prefer a dedicated wrapper on top of native fetch:
348
+
349
+ ```ts
350
+ import { withRetryFetch } from "safe-fetch-iq";
351
+
352
+ const fetchWithRetry = withRetryFetch(fetch);
353
+
354
+ const response = await fetchWithRetry("https://api.example.com/data");
355
+ ```
356
+
357
+ You can pass options on each call:
358
+
359
+ ```ts
360
+ const response = await fetchWithRetry(
361
+ "https://api.example.com/data",
362
+ { method: "GET" },
363
+ {
364
+ budget: { maxRetries: 3 }
365
+ }
366
+ );
367
+ ```
368
+
369
+ ### axios adapter
370
+
371
+ ```ts
372
+ import axios from "axios";
373
+ import { withRetryAxios } from "safe-fetch-iq";
374
+
375
+ const axiosClient = axios.create({ baseURL: "https://api.example.com" });
376
+
377
+ const axiosWithRetryFactory = withRetryAxios(axiosClient);
378
+ const axiosWithRetry = axiosWithRetryFactory({
379
+ budget: { maxRetries: 4 }
380
+ });
381
+
382
+ const response = await axiosWithRetry.request({ method: "GET", url: "/users" });
383
+ console.log(response.data);
384
+ ```
385
+
386
+ ## API Reference
387
+
388
+ ### withRetry(operation, options?)
389
+
390
+ Wraps any async operation with intelligent retry behavior.
391
+
392
+ - `operation(attempt, signal)` – async function that performs the work
393
+ - `options` – `RetryIQOptions`
394
+
395
+ ### RetryIQOptions
396
+
397
+ - `operationName?: string`
398
+ - `classifyError?: (error) => RetryErrorType`
399
+ - `shouldRetry?: (error, context) => boolean | Promise<boolean>`
400
+ - `budget?: RetryBudgetConfig`
401
+ - `backoff?: RetryBackoffConfig`
402
+ - `logger?: (event: RetryEvent) => void`
403
+ - `respectRetryAfterHeader?: boolean` (default `true`)
404
+ - `signal?: AbortSignal`
405
+
406
+ ### Backoff and budget types
407
+
408
+ - `RetryBudgetConfig`
409
+ - `maxRetries: number`
410
+ - `maxRetryTimeMs?: number`
411
+ - `RetryBackoffConfig`
412
+ - `baseDelayMs?: number`
413
+ - `maxDelayMs?: number`
414
+ - `factor?: number`
415
+ - `jitter?: "none" | "full" | "decorrelated"`
416
+ - `perErrorType?: Partial<Record<RetryErrorType, Partial<Omit<RetryBackoffConfig, "perErrorType">>>>`
417
+
418
+ ## When To Use safe-fetch-iq
419
+
420
+ - You want a safer default `fetch` with retries and timeouts
421
+ - You need simple request deduplication and caching without extra infra
422
+ - You prefer zero-dependency utilities over heavyweight HTTP clients
@@ -0,0 +1,3 @@
1
+ export declare function delay(ms: number, signal?: AbortSignal): Promise<void>;
2
+ export declare function mergeAbortSignals(inner: AbortSignal, outer?: AbortSignal): AbortSignal;
3
+ //# sourceMappingURL=abort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../src/abort.ts"],"names":[],"mappings":"AAEA,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBrE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,WAAW,CAiBtF"}
package/dist/abort.js ADDED
@@ -0,0 +1,48 @@
1
+ import { RetryAbortError } from "./errors";
2
+ export function delay(ms, signal) {
3
+ if (ms <= 0)
4
+ return Promise.resolve();
5
+ return new Promise((resolve, reject) => {
6
+ const timeout = setTimeout(onDone, ms);
7
+ function onDone() {
8
+ if (signal) {
9
+ signal.removeEventListener("abort", onAbort);
10
+ }
11
+ resolve();
12
+ }
13
+ function onAbort() {
14
+ clearTimeout(timeout);
15
+ reject(new RetryAbortError());
16
+ }
17
+ if (signal) {
18
+ if (signal.aborted) {
19
+ clearTimeout(timeout);
20
+ reject(new RetryAbortError());
21
+ }
22
+ else {
23
+ signal.addEventListener("abort", onAbort);
24
+ }
25
+ }
26
+ });
27
+ }
28
+ export function mergeAbortSignals(inner, outer) {
29
+ if (!outer)
30
+ return inner;
31
+ if (outer.aborted)
32
+ return outer;
33
+ const controller = new AbortController();
34
+ const onOuterAbort = () => {
35
+ controller.abort();
36
+ inner.removeEventListener("abort", onInnerAbort);
37
+ outer.removeEventListener("abort", onOuterAbort);
38
+ };
39
+ const onInnerAbort = () => {
40
+ controller.abort();
41
+ inner.removeEventListener("abort", onInnerAbort);
42
+ outer.removeEventListener("abort", onOuterAbort);
43
+ };
44
+ inner.addEventListener("abort", onInnerAbort);
45
+ outer.addEventListener("abort", onOuterAbort);
46
+ return controller.signal;
47
+ }
48
+ //# sourceMappingURL=abort.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort.js","sourceRoot":"","sources":["../src/abort.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAE3C,MAAM,UAAU,KAAK,CAAC,EAAU,EAAE,MAAoB;IACpD,IAAI,EAAE,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IACtC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACvC,SAAS,MAAM;YACb,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC/C,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,SAAS,OAAO;YACd,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,MAAM,CAAC,IAAI,eAAe,EAAE,CAAC,CAAC;QAChC,CAAC;QACD,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,YAAY,CAAC,OAAO,CAAC,CAAC;gBACtB,MAAM,CAAC,IAAI,eAAe,EAAE,CAAC,CAAC;YAChC,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC5C,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAkB,EAAE,KAAmB;IACvE,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,KAAK,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAChC,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,UAAU,CAAC,KAAK,EAAE,CAAC;QACnB,KAAK,CAAC,mBAAmB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QACjD,KAAK,CAAC,mBAAmB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IACnD,CAAC,CAAC;IACF,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,UAAU,CAAC,KAAK,EAAE,CAAC;QACnB,KAAK,CAAC,mBAAmB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QACjD,KAAK,CAAC,mBAAmB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IACnD,CAAC,CAAC;IACF,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IAC9C,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IAC9C,OAAO,UAAU,CAAC,MAAM,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { WithRetryOptions } from "./types";
2
+ export type FetchLike = (input: unknown, init?: unknown) => Promise<unknown>;
3
+ export declare function withRetryFetch(fetchImpl: FetchLike): (input: unknown, init?: unknown, options?: WithRetryOptions) => Promise<unknown>;
4
+ export declare function withRetryAxios<TInstance extends {
5
+ request(config: any): Promise<any>;
6
+ }>(axiosInstance: TInstance): (defaultOptions?: WithRetryOptions) => {
7
+ request(config: any, options?: WithRetryOptions): Promise<any>;
8
+ };
9
+ //# sourceMappingURL=adapters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAE3C,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAE7E,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,IAE/C,OAAO,OAAO,EACd,OAAO,OAAO,EACd,UAAU,gBAAgB,KACzB,OAAO,CAAC,OAAO,CAAC,CAMpB;AAED,wBAAgB,cAAc,CAAC,SAAS,SAAS;IAAE,OAAO,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;CAAE,EACrF,aAAa,EAAE,SAAS,IAEa,iBAAiB,gBAAgB;oBAGlD,GAAG,YAAY,gBAAgB;EASpD"}
@@ -0,0 +1,18 @@
1
+ import { withRetry } from "./core";
2
+ export function withRetryFetch(fetchImpl) {
3
+ return async function fetchWithRetry(input, init, options) {
4
+ return withRetry(() => fetchImpl(input, init), options);
5
+ };
6
+ }
7
+ export function withRetryAxios(axiosInstance) {
8
+ return function createAxiosWithRetry(defaultOptions) {
9
+ const instance = axiosInstance;
10
+ return {
11
+ request(config, options) {
12
+ const mergedOptions = { ...(defaultOptions !== null && defaultOptions !== void 0 ? defaultOptions : {}), ...(options !== null && options !== void 0 ? options : {}) };
13
+ return withRetry(() => instance.request(config), mergedOptions);
14
+ }
15
+ };
16
+ };
17
+ }
18
+ //# sourceMappingURL=adapters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapters.js","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAKnC,MAAM,UAAU,cAAc,CAAC,SAAoB;IACjD,OAAO,KAAK,UAAU,cAAc,CAClC,KAAc,EACd,IAAc,EACd,OAA0B;QAE1B,OAAO,SAAS,CACd,GAAG,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,EAC5B,OAAO,CACR,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,aAAwB;IAExB,OAAO,SAAS,oBAAoB,CAAC,cAAiC;QACpE,MAAM,QAAQ,GAAG,aAAa,CAAC;QAC/B,OAAO;YACL,OAAO,CAAC,MAAW,EAAE,OAA0B;gBAC7C,MAAM,aAAa,GAAqB,EAAE,GAAG,CAAC,cAAc,aAAd,cAAc,cAAd,cAAc,GAAI,EAAE,CAAC,EAAE,GAAG,CAAC,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE,CAAC,EAAE,CAAC;gBAC1F,OAAO,SAAS,CACd,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,EAC9B,aAAa,CACd,CAAC;YACJ,CAAC;SACF,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { RetryBackoffConfig, RetryBudgetConfig, RetryErrorType } from "./types";
2
+ export declare const defaultBudget: RetryBudgetConfig;
3
+ export declare const defaultBackoff: RetryBackoffConfig;
4
+ export declare function defaultClassifyError(error: unknown): RetryErrorType;
5
+ export declare function isRetriableType(errorType: RetryErrorType): boolean;
6
+ export declare function mergeBackoffConfig(base: RetryBackoffConfig, errorType: RetryErrorType): Required<Omit<RetryBackoffConfig, "perErrorType">>;
7
+ export declare function computeBackoffDelayMs(baseConfig: RetryBackoffConfig, errorType: RetryErrorType, attempt: number): number;
8
+ export declare function selectClassifier(classifyError: ((error: unknown) => RetryErrorType) | undefined): (error: unknown) => RetryErrorType;
9
+ //# sourceMappingURL=backoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backoff.d.ts","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGhF,eAAO,MAAM,aAAa,EAAE,iBAG3B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,kBAK5B,CAAC;AAaF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAmBnE;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,cAAc,GAAG,OAAO,CAOlE;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,kBAAkB,EACxB,SAAS,EAAE,cAAc,GACxB,QAAQ,CAAC,IAAI,CAAC,kBAAkB,EAAE,cAAc,CAAC,CAAC,CAepD;AAED,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,kBAAkB,EAC9B,SAAS,EAAE,cAAc,EACzB,OAAO,EAAE,MAAM,GACd,MAAM,CAYR;AAED,wBAAgB,gBAAgB,CAC9B,aAAa,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,cAAc,CAAC,GAAG,SAAS,GAC9D,CAAC,KAAK,EAAE,OAAO,KAAK,cAAc,CAGpC"}
@@ -0,0 +1,83 @@
1
+ import { classifyHttpStatus, detectHttpResponse } from "./http";
2
+ export const defaultBudget = {
3
+ maxRetries: 5,
4
+ maxRetryTimeMs: 60000
5
+ };
6
+ export const defaultBackoff = {
7
+ baseDelayMs: 250,
8
+ maxDelayMs: 30000,
9
+ factor: 2,
10
+ jitter: "full"
11
+ };
12
+ function random() {
13
+ return Math.random();
14
+ }
15
+ function isAbortError(error) {
16
+ if (typeof error !== "object" || error === null)
17
+ return false;
18
+ const name = error.name;
19
+ const code = error.code;
20
+ return name === "AbortError" || code === "ABORT_ERR";
21
+ }
22
+ export function defaultClassifyError(error) {
23
+ if (isAbortError(error))
24
+ return "clientError";
25
+ const response = detectHttpResponse(error);
26
+ if (response) {
27
+ return classifyHttpStatus(response.status);
28
+ }
29
+ if (typeof error === "object" && error !== null) {
30
+ const code = error.code;
31
+ if (code === "ECONNRESET" ||
32
+ code === "ENOTFOUND" ||
33
+ code === "ETIMEDOUT" ||
34
+ code === "EAI_AGAIN" ||
35
+ code === "ECONNREFUSED") {
36
+ return "networkError";
37
+ }
38
+ }
39
+ return "unknown";
40
+ }
41
+ export function isRetriableType(errorType) {
42
+ return (errorType === "rateLimit" ||
43
+ errorType === "serverError" ||
44
+ errorType === "networkError" ||
45
+ errorType === "unknown");
46
+ }
47
+ export function mergeBackoffConfig(base, errorType) {
48
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
49
+ const perType = (_b = (_a = base.perErrorType) === null || _a === void 0 ? void 0 : _a[errorType]) !== null && _b !== void 0 ? _b : {};
50
+ const merged = {
51
+ baseDelayMs: (_c = base.baseDelayMs) !== null && _c !== void 0 ? _c : defaultBackoff.baseDelayMs,
52
+ maxDelayMs: (_d = base.maxDelayMs) !== null && _d !== void 0 ? _d : defaultBackoff.maxDelayMs,
53
+ factor: (_e = base.factor) !== null && _e !== void 0 ? _e : defaultBackoff.factor,
54
+ jitter: (_f = base.jitter) !== null && _f !== void 0 ? _f : defaultBackoff.jitter,
55
+ ...perType
56
+ };
57
+ return {
58
+ baseDelayMs: (_g = merged.baseDelayMs) !== null && _g !== void 0 ? _g : defaultBackoff.baseDelayMs,
59
+ maxDelayMs: (_h = merged.maxDelayMs) !== null && _h !== void 0 ? _h : defaultBackoff.maxDelayMs,
60
+ factor: (_j = merged.factor) !== null && _j !== void 0 ? _j : defaultBackoff.factor,
61
+ jitter: (_k = merged.jitter) !== null && _k !== void 0 ? _k : defaultBackoff.jitter
62
+ };
63
+ }
64
+ export function computeBackoffDelayMs(baseConfig, errorType, attempt) {
65
+ const cfg = mergeBackoffConfig(baseConfig, errorType);
66
+ const exp = Math.max(attempt - 1, 0);
67
+ const rawDelay = cfg.baseDelayMs * Math.pow(cfg.factor, exp);
68
+ const clamped = Math.min(rawDelay, cfg.maxDelayMs);
69
+ if (cfg.jitter === "none")
70
+ return clamped;
71
+ if (cfg.jitter === "full") {
72
+ return random() * clamped;
73
+ }
74
+ const min = clamped / 2;
75
+ const max = clamped;
76
+ return min + random() * (max - min);
77
+ }
78
+ export function selectClassifier(classifyError) {
79
+ if (classifyError)
80
+ return classifyError;
81
+ return defaultClassifyError;
82
+ }
83
+ //# sourceMappingURL=backoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backoff.js","sourceRoot":"","sources":["../src/backoff.ts"],"names":[],"mappings":"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"}
package/dist/core.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { RetryIQOptions } from "./types";
2
+ export declare function withRetry<T>(operation: (attempt: number, signal: AbortSignal) => Promise<T>, options?: RetryIQOptions): Promise<T>;
3
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,cAAc,EAEf,MAAM,SAAS,CAAC;AAgBjB,wBAAsB,SAAS,CAAC,CAAC,EAC/B,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAC/D,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,CAAC,CAAC,CA2LZ"}