@rohan9024/retrykit 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -3
- package/dist/index.d.mts +47 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +220 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -6
package/README.md
CHANGED
|
@@ -1,3 +1,97 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @rohan9024/retrykit ⚡
|
|
2
|
+
|
|
3
|
+
> Zero-dependency, header-aware retry wrapper tailored for LLM APIs and fetch.
|
|
4
|
+
|
|
5
|
+
Automatically handles `429 Too Many Requests` by parsing rate-limit headers from OpenAI, Anthropic, and Groq before falling back to exponential backoff with full jitter.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 📦 Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @rohan9024/retrykit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 🚀 Usage
|
|
18
|
+
|
|
19
|
+
### 1. Drop-in Replacement for `fetch` (`retryFetch`)
|
|
20
|
+
|
|
21
|
+
`retryFetch` works exactly like standard `fetch`, but automatically handles retries and honors rate-limit headers:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { retryFetch } from '@rohan9024/retrykit';
|
|
25
|
+
|
|
26
|
+
const response = await retryFetch('https://api.openai.com/v1/chat/completions', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
model: 'gpt-4o',
|
|
34
|
+
messages: [{ role: 'user', content: 'Say hello!' }],
|
|
35
|
+
}),
|
|
36
|
+
}, {
|
|
37
|
+
retries: 3,
|
|
38
|
+
onRetry: (error, attempt, delayMs) => {
|
|
39
|
+
console.warn(`Retry attempt #${attempt} after ${delayMs}ms delay...`);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### 2. Wrap Official SDKs (`retry`)
|
|
49
|
+
|
|
50
|
+
Wrap calls from the official OpenAI, Anthropic, or Mistral client libraries:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import OpenAI from 'openai';
|
|
54
|
+
import { retry } from '@rohan9024/retrykit';
|
|
55
|
+
|
|
56
|
+
const openai = new OpenAI();
|
|
57
|
+
|
|
58
|
+
const completion = await retry(() =>
|
|
59
|
+
openai.chat.completions.create({
|
|
60
|
+
model: 'gpt-4o',
|
|
61
|
+
messages: [{ role: 'user', content: 'Write a haiku' }],
|
|
62
|
+
}),
|
|
63
|
+
{
|
|
64
|
+
retries: 4,
|
|
65
|
+
baseDelayMs: 500,
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 🧠 Provider Header Awareness
|
|
73
|
+
|
|
74
|
+
`retrykit` inspects and obeys the following provider headers before falling back to exponential backoff:
|
|
75
|
+
|
|
76
|
+
- `retry-after` (RFC 1123 HTTP Dates or relative seconds)
|
|
77
|
+
- `x-ratelimit-reset-requests` (OpenAI format, e.g., `"15s"`, `"600ms"`)
|
|
78
|
+
- `x-ratelimit-reset-tokens` (Anthropic format)
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## ⚙️ Options Reference
|
|
83
|
+
|
|
84
|
+
| Option | Type | Default | Description |
|
|
85
|
+
| :--- | :--- | :--- | :--- |
|
|
86
|
+
| `retries` | `number` | `3` | Max retry attempts before failing |
|
|
87
|
+
| `baseDelayMs` | `number` | `500` | Starting backoff delay in ms |
|
|
88
|
+
| `maxDelayMs` | `number` | `30000` | Ceiling delay cap in ms |
|
|
89
|
+
| `retryOnStatus`| `number[]` | `[408, 429, 500, 502, 503, 504]` | HTTP status codes that trigger a retry |
|
|
90
|
+
| `onRetry` | `(err, attempt, delay) => void` | `undefined` | Hook called before every sleep |
|
|
91
|
+
| `signal` | `AbortSignal` | `undefined` | Immediate cancellation controller |
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 📄 License
|
|
96
|
+
|
|
97
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface RetryOptions {
|
|
2
|
+
/** Maximum number of retry attempts. Default: 3 */
|
|
3
|
+
retries?: number;
|
|
4
|
+
/** Base delay in milliseconds for exponential backoff. Default: 500 */
|
|
5
|
+
baseDelayMs?: number;
|
|
6
|
+
/** Maximum backoff ceiling delay in milliseconds. Default: 30000 */
|
|
7
|
+
maxDelayMs?: number;
|
|
8
|
+
/** HTTP status codes that trigger a retry. Default: [408, 429, 500, 502, 503, 504] */
|
|
9
|
+
retryOnStatus?: number[];
|
|
10
|
+
/** Optional callback fired on every retry attempt before sleep */
|
|
11
|
+
onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
|
|
12
|
+
/** AbortSignal for immediate cancellation */
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
type HeaderSource = Headers | Record<string, string | string[] | undefined> | [string, string][] | undefined;
|
|
16
|
+
|
|
17
|
+
declare function retryFetch(url: string | URL | Request, init?: RequestInit, options?: RetryOptions): Promise<Response>;
|
|
18
|
+
|
|
19
|
+
declare function retry<T>(fn: (attempt: number) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
20
|
+
|
|
21
|
+
declare const DEFAULT_RETRYABLE_STATUS_CODES: number[];
|
|
22
|
+
/**
|
|
23
|
+
* Calculates exponential backoff with full jitter:
|
|
24
|
+
* sleepMs = min(maxDelayMs, baseDelayMs * 2 ** attempt) * Math.random()
|
|
25
|
+
*/
|
|
26
|
+
declare function computeJitterDelay(attempt: number, baseDelayMs?: number, maxDelayMs?: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* Parses duration strings returned by LLM APIs (e.g. "600ms", "10s", "2m", "1m30s", "1.5").
|
|
29
|
+
*/
|
|
30
|
+
declare function parseDurationToMs(value: string): number | null;
|
|
31
|
+
/**
|
|
32
|
+
* Inspects rate-limiting headers and extracts sleep delay in milliseconds:
|
|
33
|
+
* - retry-after (seconds or HTTP date)
|
|
34
|
+
* - x-ratelimit-reset-requests (OpenAI format)
|
|
35
|
+
* - x-ratelimit-reset-tokens (Anthropic format)
|
|
36
|
+
*/
|
|
37
|
+
declare function parseRateLimitDelay(headers: HeaderSource): number | null;
|
|
38
|
+
/**
|
|
39
|
+
* Detects transient network drop errors.
|
|
40
|
+
*/
|
|
41
|
+
declare function isTransientNetworkError(error: unknown): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Sleep helper respecting AbortSignal cancellation.
|
|
44
|
+
*/
|
|
45
|
+
declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
46
|
+
|
|
47
|
+
export { DEFAULT_RETRYABLE_STATUS_CODES, type HeaderSource, type RetryOptions, computeJitterDelay, isTransientNetworkError, parseDurationToMs, parseRateLimitDelay, retry, retryFetch, sleep };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface RetryOptions {
|
|
2
|
+
/** Maximum number of retry attempts. Default: 3 */
|
|
3
|
+
retries?: number;
|
|
4
|
+
/** Base delay in milliseconds for exponential backoff. Default: 500 */
|
|
5
|
+
baseDelayMs?: number;
|
|
6
|
+
/** Maximum backoff ceiling delay in milliseconds. Default: 30000 */
|
|
7
|
+
maxDelayMs?: number;
|
|
8
|
+
/** HTTP status codes that trigger a retry. Default: [408, 429, 500, 502, 503, 504] */
|
|
9
|
+
retryOnStatus?: number[];
|
|
10
|
+
/** Optional callback fired on every retry attempt before sleep */
|
|
11
|
+
onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
|
|
12
|
+
/** AbortSignal for immediate cancellation */
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
type HeaderSource = Headers | Record<string, string | string[] | undefined> | [string, string][] | undefined;
|
|
16
|
+
|
|
17
|
+
declare function retryFetch(url: string | URL | Request, init?: RequestInit, options?: RetryOptions): Promise<Response>;
|
|
18
|
+
|
|
19
|
+
declare function retry<T>(fn: (attempt: number) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
20
|
+
|
|
21
|
+
declare const DEFAULT_RETRYABLE_STATUS_CODES: number[];
|
|
22
|
+
/**
|
|
23
|
+
* Calculates exponential backoff with full jitter:
|
|
24
|
+
* sleepMs = min(maxDelayMs, baseDelayMs * 2 ** attempt) * Math.random()
|
|
25
|
+
*/
|
|
26
|
+
declare function computeJitterDelay(attempt: number, baseDelayMs?: number, maxDelayMs?: number): number;
|
|
27
|
+
/**
|
|
28
|
+
* Parses duration strings returned by LLM APIs (e.g. "600ms", "10s", "2m", "1m30s", "1.5").
|
|
29
|
+
*/
|
|
30
|
+
declare function parseDurationToMs(value: string): number | null;
|
|
31
|
+
/**
|
|
32
|
+
* Inspects rate-limiting headers and extracts sleep delay in milliseconds:
|
|
33
|
+
* - retry-after (seconds or HTTP date)
|
|
34
|
+
* - x-ratelimit-reset-requests (OpenAI format)
|
|
35
|
+
* - x-ratelimit-reset-tokens (Anthropic format)
|
|
36
|
+
*/
|
|
37
|
+
declare function parseRateLimitDelay(headers: HeaderSource): number | null;
|
|
38
|
+
/**
|
|
39
|
+
* Detects transient network drop errors.
|
|
40
|
+
*/
|
|
41
|
+
declare function isTransientNetworkError(error: unknown): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Sleep helper respecting AbortSignal cancellation.
|
|
44
|
+
*/
|
|
45
|
+
declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
46
|
+
|
|
47
|
+
export { DEFAULT_RETRYABLE_STATUS_CODES, type HeaderSource, type RetryOptions, computeJitterDelay, isTransientNetworkError, parseDurationToMs, parseRateLimitDelay, retry, retryFetch, sleep };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DEFAULT_RETRYABLE_STATUS_CODES: () => DEFAULT_RETRYABLE_STATUS_CODES,
|
|
24
|
+
computeJitterDelay: () => computeJitterDelay,
|
|
25
|
+
isTransientNetworkError: () => isTransientNetworkError,
|
|
26
|
+
parseDurationToMs: () => parseDurationToMs,
|
|
27
|
+
parseRateLimitDelay: () => parseRateLimitDelay,
|
|
28
|
+
retry: () => retry,
|
|
29
|
+
retryFetch: () => retryFetch,
|
|
30
|
+
sleep: () => sleep
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/engine.ts
|
|
35
|
+
var DEFAULT_RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];
|
|
36
|
+
function computeJitterDelay(attempt, baseDelayMs = 500, maxDelayMs = 3e4) {
|
|
37
|
+
const exponential = baseDelayMs * Math.pow(2, attempt);
|
|
38
|
+
const ceiling = Math.min(maxDelayMs, exponential);
|
|
39
|
+
return Math.floor(ceiling * Math.random());
|
|
40
|
+
}
|
|
41
|
+
function getHeader(headers, name) {
|
|
42
|
+
if (!headers) return null;
|
|
43
|
+
const target = name.toLowerCase();
|
|
44
|
+
if (typeof headers.get === "function") {
|
|
45
|
+
return headers.get(target);
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(headers)) {
|
|
48
|
+
for (const [key, value] of headers) {
|
|
49
|
+
if (key.toLowerCase() === target) {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (typeof headers === "object") {
|
|
56
|
+
for (const key of Object.keys(headers)) {
|
|
57
|
+
if (key.toLowerCase() === target) {
|
|
58
|
+
const val = headers[key];
|
|
59
|
+
if (Array.isArray(val)) return val[0] ?? null;
|
|
60
|
+
return typeof val === "string" ? val : String(val);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function parseDurationToMs(value) {
|
|
67
|
+
const trimmed = value.trim();
|
|
68
|
+
if (!trimmed) return null;
|
|
69
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
70
|
+
const sec = parseFloat(trimmed);
|
|
71
|
+
return isNaN(sec) ? null : Math.max(0, Math.floor(sec * 1e3));
|
|
72
|
+
}
|
|
73
|
+
const regex = /(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)/gi;
|
|
74
|
+
let match;
|
|
75
|
+
let totalMs = 0;
|
|
76
|
+
let matched = false;
|
|
77
|
+
while ((match = regex.exec(trimmed)) !== null) {
|
|
78
|
+
matched = true;
|
|
79
|
+
const amount = parseFloat(match[1]);
|
|
80
|
+
const unit = match[2].toLowerCase();
|
|
81
|
+
switch (unit) {
|
|
82
|
+
case "ms":
|
|
83
|
+
totalMs += amount;
|
|
84
|
+
break;
|
|
85
|
+
case "s":
|
|
86
|
+
totalMs += amount * 1e3;
|
|
87
|
+
break;
|
|
88
|
+
case "m":
|
|
89
|
+
totalMs += amount * 60 * 1e3;
|
|
90
|
+
break;
|
|
91
|
+
case "h":
|
|
92
|
+
totalMs += amount * 60 * 60 * 1e3;
|
|
93
|
+
break;
|
|
94
|
+
case "d":
|
|
95
|
+
totalMs += amount * 24 * 60 * 60 * 1e3;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return matched ? Math.max(0, Math.floor(totalMs)) : null;
|
|
100
|
+
}
|
|
101
|
+
function parseRateLimitDelay(headers) {
|
|
102
|
+
if (!headers) return null;
|
|
103
|
+
const retryAfter = getHeader(headers, "retry-after");
|
|
104
|
+
if (retryAfter) {
|
|
105
|
+
if (/^\d+(\.\d+)?$/.test(retryAfter.trim())) {
|
|
106
|
+
const sec = parseFloat(retryAfter.trim());
|
|
107
|
+
if (!isNaN(sec)) return Math.max(0, Math.floor(sec * 1e3));
|
|
108
|
+
}
|
|
109
|
+
const parsedDate = Date.parse(retryAfter);
|
|
110
|
+
if (!isNaN(parsedDate)) {
|
|
111
|
+
return Math.max(0, parsedDate - Date.now());
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const openaiReset = getHeader(headers, "x-ratelimit-reset-requests");
|
|
115
|
+
if (openaiReset) {
|
|
116
|
+
const durationMs = parseDurationToMs(openaiReset);
|
|
117
|
+
if (durationMs !== null) return durationMs;
|
|
118
|
+
}
|
|
119
|
+
const anthropicReset = getHeader(headers, "x-ratelimit-reset-tokens");
|
|
120
|
+
if (anthropicReset) {
|
|
121
|
+
const durationMs = parseDurationToMs(anthropicReset);
|
|
122
|
+
if (durationMs !== null) return durationMs;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
function isTransientNetworkError(error) {
|
|
127
|
+
if (!(error instanceof Error)) return false;
|
|
128
|
+
const msg = error.message.toLowerCase();
|
|
129
|
+
const code = error.code;
|
|
130
|
+
if (error.name === "TypeError" && msg.includes("fetch failed")) return true;
|
|
131
|
+
const transientCodes = ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED"];
|
|
132
|
+
if (code && transientCodes.includes(code)) return true;
|
|
133
|
+
return msg.includes("network error") || msg.includes("socket hang up") || msg.includes("timeout");
|
|
134
|
+
}
|
|
135
|
+
function sleep(ms, signal) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
if (signal?.aborted) {
|
|
138
|
+
return reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
139
|
+
}
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
signal?.removeEventListener("abort", onAbort);
|
|
142
|
+
resolve();
|
|
143
|
+
}, ms);
|
|
144
|
+
function onAbort() {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
reject(signal?.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
147
|
+
}
|
|
148
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/fetch.ts
|
|
153
|
+
async function retryFetch(url, init, options = {}) {
|
|
154
|
+
const {
|
|
155
|
+
retries = 3,
|
|
156
|
+
baseDelayMs = 500,
|
|
157
|
+
maxDelayMs = 3e4,
|
|
158
|
+
retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
|
|
159
|
+
onRetry,
|
|
160
|
+
signal
|
|
161
|
+
} = options;
|
|
162
|
+
let attempt = 0;
|
|
163
|
+
while (true) {
|
|
164
|
+
if (signal?.aborted) {
|
|
165
|
+
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const response = await fetch(url, {
|
|
169
|
+
...init,
|
|
170
|
+
signal: signal ?? init?.signal
|
|
171
|
+
});
|
|
172
|
+
if (retryOnStatus.includes(response.status) && attempt < retries) {
|
|
173
|
+
const headerDelay = parseRateLimitDelay(response.headers);
|
|
174
|
+
const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
175
|
+
const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);
|
|
176
|
+
onRetry?.(response, attempt + 1, chosenDelay);
|
|
177
|
+
await sleep(chosenDelay, signal);
|
|
178
|
+
attempt++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
return response;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (signal?.aborted || error instanceof Error && error.name === "AbortError") {
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
if (isTransientNetworkError(error) && attempt < retries) {
|
|
187
|
+
const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
188
|
+
onRetry?.(error, attempt + 1, chosenDelay);
|
|
189
|
+
await sleep(chosenDelay, signal);
|
|
190
|
+
attempt++;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/wrap.ts
|
|
199
|
+
async function retry(fn, options = {}) {
|
|
200
|
+
const {
|
|
201
|
+
retries = 3,
|
|
202
|
+
baseDelayMs = 500,
|
|
203
|
+
maxDelayMs = 3e4,
|
|
204
|
+
retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
|
|
205
|
+
onRetry,
|
|
206
|
+
signal
|
|
207
|
+
} = options;
|
|
208
|
+
let attempt = 0;
|
|
209
|
+
while (true) {
|
|
210
|
+
if (signal?.aborted) {
|
|
211
|
+
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
return await fn(attempt);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (signal?.aborted || error instanceof Error && error.name === "AbortError") {
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
if (attempt >= retries) {
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
const err = error;
|
|
223
|
+
const statusCode = err?.status ?? err?.statusCode ?? err?.response?.status;
|
|
224
|
+
const headers = err?.headers ?? err?.response?.headers;
|
|
225
|
+
const isStatusRetryable = statusCode !== void 0 && retryOnStatus.includes(statusCode);
|
|
226
|
+
const isNetworkRetryable = isTransientNetworkError(error);
|
|
227
|
+
if (!isStatusRetryable && !isNetworkRetryable) {
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
let chosenDelay;
|
|
231
|
+
const headerDelay = headers ? parseRateLimitDelay(headers) : null;
|
|
232
|
+
if (headerDelay !== null) {
|
|
233
|
+
chosenDelay = Math.min(maxDelayMs, headerDelay);
|
|
234
|
+
} else {
|
|
235
|
+
chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
236
|
+
}
|
|
237
|
+
onRetry?.(error, attempt + 1, chosenDelay);
|
|
238
|
+
await sleep(chosenDelay, signal);
|
|
239
|
+
attempt++;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
244
|
+
0 && (module.exports = {
|
|
245
|
+
DEFAULT_RETRYABLE_STATUS_CODES,
|
|
246
|
+
computeJitterDelay,
|
|
247
|
+
isTransientNetworkError,
|
|
248
|
+
parseDurationToMs,
|
|
249
|
+
parseRateLimitDelay,
|
|
250
|
+
retry,
|
|
251
|
+
retryFetch,
|
|
252
|
+
sleep
|
|
253
|
+
});
|
|
254
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/engine.ts","../src/fetch.ts","../src/wrap.ts"],"sourcesContent":["export { retryFetch } from './fetch.js';\r\nexport { retry } from './wrap.js';\r\nexport {\r\n computeJitterDelay,\r\n parseDurationToMs,\r\n parseRateLimitDelay,\r\n isTransientNetworkError,\r\n sleep,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n} from './engine.js';\r\nexport type { RetryOptions, HeaderSource } from './types.js';","import { HeaderSource } from './types.js';\r\n\r\nexport const DEFAULT_RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];\r\n\r\n/**\r\n * Calculates exponential backoff with full jitter:\r\n * sleepMs = min(maxDelayMs, baseDelayMs * 2 ** attempt) * Math.random()\r\n */\r\nexport function computeJitterDelay(\r\n attempt: number,\r\n baseDelayMs: number = 500,\r\n maxDelayMs: number = 30000\r\n): number {\r\n const exponential = baseDelayMs * Math.pow(2, attempt);\r\n const ceiling = Math.min(maxDelayMs, exponential);\r\n return Math.floor(ceiling * Math.random());\r\n}\r\n\r\n/**\r\n * Safely extracts a header value case-insensitively across Headers, records, or tuples.\r\n */\r\nexport function getHeader(headers: HeaderSource, name: string): string | null {\r\n if (!headers) return null;\r\n\r\n const target = name.toLowerCase();\r\n\r\n // Native Web API Headers\r\n if (typeof (headers as Headers).get === 'function') {\r\n return (headers as Headers).get(target);\r\n }\r\n\r\n // Array of tuples\r\n if (Array.isArray(headers)) {\r\n for (const [key, value] of headers) {\r\n if (key.toLowerCase() === target) {\r\n return value;\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n // Plain object\r\n if (typeof headers === 'object') {\r\n for (const key of Object.keys(headers)) {\r\n if (key.toLowerCase() === target) {\r\n const val = (headers as Record<string, unknown>)[key];\r\n if (Array.isArray(val)) return val[0] ?? null;\r\n return typeof val === 'string' ? val : String(val);\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Parses duration strings returned by LLM APIs (e.g. \"600ms\", \"10s\", \"2m\", \"1m30s\", \"1.5\").\r\n */\r\nexport function parseDurationToMs(value: string): number | null {\r\n const trimmed = value.trim();\r\n if (!trimmed) return null;\r\n\r\n // Pure numeric string represents seconds\r\n if (/^\\d+(\\.\\d+)?$/.test(trimmed)) {\r\n const sec = parseFloat(trimmed);\r\n return isNaN(sec) ? null : Math.max(0, Math.floor(sec * 1000));\r\n }\r\n\r\n // Units match\r\n const regex = /(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)/gi;\r\n let match: RegExpExecArray | null;\r\n let totalMs = 0;\r\n let matched = false;\r\n\r\n while ((match = regex.exec(trimmed)) !== null) {\r\n matched = true;\r\n const amount = parseFloat(match[1]);\r\n const unit = match[2].toLowerCase();\r\n\r\n switch (unit) {\r\n case 'ms':\r\n totalMs += amount;\r\n break;\r\n case 's':\r\n totalMs += amount * 1000;\r\n break;\r\n case 'm':\r\n totalMs += amount * 60 * 1000;\r\n break;\r\n case 'h':\r\n totalMs += amount * 60 * 60 * 1000;\r\n break;\r\n case 'd':\r\n totalMs += amount * 24 * 60 * 60 * 1000;\r\n break;\r\n }\r\n }\r\n\r\n return matched ? Math.max(0, Math.floor(totalMs)) : null;\r\n}\r\n\r\n/**\r\n * Inspects rate-limiting headers and extracts sleep delay in milliseconds:\r\n * - retry-after (seconds or HTTP date)\r\n * - x-ratelimit-reset-requests (OpenAI format)\r\n * - x-ratelimit-reset-tokens (Anthropic format)\r\n */\r\nexport function parseRateLimitDelay(headers: HeaderSource): number | null {\r\n if (!headers) return null;\r\n\r\n const retryAfter = getHeader(headers, 'retry-after');\r\n if (retryAfter) {\r\n if (/^\\d+(\\.\\d+)?$/.test(retryAfter.trim())) {\r\n const sec = parseFloat(retryAfter.trim());\r\n if (!isNaN(sec)) return Math.max(0, Math.floor(sec * 1000));\r\n }\r\n\r\n const parsedDate = Date.parse(retryAfter);\r\n if (!isNaN(parsedDate)) {\r\n return Math.max(0, parsedDate - Date.now());\r\n }\r\n }\r\n\r\n const openaiReset = getHeader(headers, 'x-ratelimit-reset-requests');\r\n if (openaiReset) {\r\n const durationMs = parseDurationToMs(openaiReset);\r\n if (durationMs !== null) return durationMs;\r\n }\r\n\r\n const anthropicReset = getHeader(headers, 'x-ratelimit-reset-tokens');\r\n if (anthropicReset) {\r\n const durationMs = parseDurationToMs(anthropicReset);\r\n if (durationMs !== null) return durationMs;\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Detects transient network drop errors.\r\n */\r\nexport function isTransientNetworkError(error: unknown): boolean {\r\n if (!(error instanceof Error)) return false;\r\n\r\n const msg = error.message.toLowerCase();\r\n const code = (error as { code?: string }).code;\r\n\r\n if (error.name === 'TypeError' && msg.includes('fetch failed')) return true;\r\n\r\n const transientCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED'];\r\n if (code && transientCodes.includes(code)) return true;\r\n\r\n return (\r\n msg.includes('network error') ||\r\n msg.includes('socket hang up') ||\r\n msg.includes('timeout')\r\n );\r\n}\r\n\r\n/**\r\n * Sleep helper respecting AbortSignal cancellation.\r\n */\r\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n if (signal?.aborted) {\r\n return reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));\r\n }\r\n\r\n const timer = setTimeout(() => {\r\n signal?.removeEventListener('abort', onAbort);\r\n resolve();\r\n }, ms);\r\n\r\n function onAbort() {\r\n clearTimeout(timer);\r\n reject(signal?.reason ?? new DOMException('The operation was aborted.', 'AbortError'));\r\n }\r\n\r\n signal?.addEventListener('abort', onAbort, { once: true });\r\n });\r\n}","import {\r\n computeJitterDelay,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n isTransientNetworkError,\r\n parseRateLimitDelay,\r\n sleep,\r\n} from './engine.js';\r\nimport { RetryOptions } from './types.js';\r\n\r\nexport async function retryFetch(\r\n url: string | URL | Request,\r\n init?: RequestInit,\r\n options: RetryOptions = {}\r\n): Promise<Response> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n try {\r\n const response = await fetch(url, {\r\n ...init,\r\n signal: signal ?? init?.signal,\r\n });\r\n\r\n if (retryOnStatus.includes(response.status) && attempt < retries) {\r\n const headerDelay = parseRateLimitDelay(response.headers);\r\n const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);\r\n\r\n onRetry?.(response, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n continue;\r\n }\r\n\r\n return response;\r\n } catch (error) {\r\n if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {\r\n throw error;\r\n }\r\n\r\n if (isTransientNetworkError(error) && attempt < retries) {\r\n const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n onRetry?.(error, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n continue;\r\n }\r\n\r\n throw error;\r\n }\r\n }\r\n}","import {\r\n computeJitterDelay,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n isTransientNetworkError,\r\n parseRateLimitDelay,\r\n sleep,\r\n} from './engine.js';\r\nimport { HeaderSource, RetryOptions } from './types.js';\r\n\r\ninterface ErrorWithStatus extends Error {\r\n status?: number;\r\n statusCode?: number;\r\n headers?: HeaderSource;\r\n response?: {\r\n status?: number;\r\n headers?: HeaderSource;\r\n };\r\n}\r\n\r\nexport async function retry<T>(\r\n fn: (attempt: number) => Promise<T>,\r\n options: RetryOptions = {}\r\n): Promise<T> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n try {\r\n return await fn(attempt);\r\n } catch (error) {\r\n if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {\r\n throw error;\r\n }\r\n\r\n if (attempt >= retries) {\r\n throw error;\r\n }\r\n\r\n const err = error as ErrorWithStatus;\r\n const statusCode = err?.status ?? err?.statusCode ?? err?.response?.status;\r\n const headers = err?.headers ?? err?.response?.headers;\r\n\r\n const isStatusRetryable = statusCode !== undefined && retryOnStatus.includes(statusCode);\r\n const isNetworkRetryable = isTransientNetworkError(error);\r\n\r\n if (!isStatusRetryable && !isNetworkRetryable) {\r\n throw error;\r\n }\r\n\r\n let chosenDelay: number;\r\n const headerDelay = headers ? parseRateLimitDelay(headers) : null;\r\n\r\n if (headerDelay !== null) {\r\n chosenDelay = Math.min(maxDelayMs, headerDelay);\r\n } else {\r\n chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n }\r\n\r\n onRetry?.(error, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n }\r\n }\r\n}"],"mappings":"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","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// src/engine.ts
|
|
2
|
+
var DEFAULT_RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];
|
|
3
|
+
function computeJitterDelay(attempt, baseDelayMs = 500, maxDelayMs = 3e4) {
|
|
4
|
+
const exponential = baseDelayMs * Math.pow(2, attempt);
|
|
5
|
+
const ceiling = Math.min(maxDelayMs, exponential);
|
|
6
|
+
return Math.floor(ceiling * Math.random());
|
|
7
|
+
}
|
|
8
|
+
function getHeader(headers, name) {
|
|
9
|
+
if (!headers) return null;
|
|
10
|
+
const target = name.toLowerCase();
|
|
11
|
+
if (typeof headers.get === "function") {
|
|
12
|
+
return headers.get(target);
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(headers)) {
|
|
15
|
+
for (const [key, value] of headers) {
|
|
16
|
+
if (key.toLowerCase() === target) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (typeof headers === "object") {
|
|
23
|
+
for (const key of Object.keys(headers)) {
|
|
24
|
+
if (key.toLowerCase() === target) {
|
|
25
|
+
const val = headers[key];
|
|
26
|
+
if (Array.isArray(val)) return val[0] ?? null;
|
|
27
|
+
return typeof val === "string" ? val : String(val);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function parseDurationToMs(value) {
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
if (!trimmed) return null;
|
|
36
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
37
|
+
const sec = parseFloat(trimmed);
|
|
38
|
+
return isNaN(sec) ? null : Math.max(0, Math.floor(sec * 1e3));
|
|
39
|
+
}
|
|
40
|
+
const regex = /(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)/gi;
|
|
41
|
+
let match;
|
|
42
|
+
let totalMs = 0;
|
|
43
|
+
let matched = false;
|
|
44
|
+
while ((match = regex.exec(trimmed)) !== null) {
|
|
45
|
+
matched = true;
|
|
46
|
+
const amount = parseFloat(match[1]);
|
|
47
|
+
const unit = match[2].toLowerCase();
|
|
48
|
+
switch (unit) {
|
|
49
|
+
case "ms":
|
|
50
|
+
totalMs += amount;
|
|
51
|
+
break;
|
|
52
|
+
case "s":
|
|
53
|
+
totalMs += amount * 1e3;
|
|
54
|
+
break;
|
|
55
|
+
case "m":
|
|
56
|
+
totalMs += amount * 60 * 1e3;
|
|
57
|
+
break;
|
|
58
|
+
case "h":
|
|
59
|
+
totalMs += amount * 60 * 60 * 1e3;
|
|
60
|
+
break;
|
|
61
|
+
case "d":
|
|
62
|
+
totalMs += amount * 24 * 60 * 60 * 1e3;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return matched ? Math.max(0, Math.floor(totalMs)) : null;
|
|
67
|
+
}
|
|
68
|
+
function parseRateLimitDelay(headers) {
|
|
69
|
+
if (!headers) return null;
|
|
70
|
+
const retryAfter = getHeader(headers, "retry-after");
|
|
71
|
+
if (retryAfter) {
|
|
72
|
+
if (/^\d+(\.\d+)?$/.test(retryAfter.trim())) {
|
|
73
|
+
const sec = parseFloat(retryAfter.trim());
|
|
74
|
+
if (!isNaN(sec)) return Math.max(0, Math.floor(sec * 1e3));
|
|
75
|
+
}
|
|
76
|
+
const parsedDate = Date.parse(retryAfter);
|
|
77
|
+
if (!isNaN(parsedDate)) {
|
|
78
|
+
return Math.max(0, parsedDate - Date.now());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const openaiReset = getHeader(headers, "x-ratelimit-reset-requests");
|
|
82
|
+
if (openaiReset) {
|
|
83
|
+
const durationMs = parseDurationToMs(openaiReset);
|
|
84
|
+
if (durationMs !== null) return durationMs;
|
|
85
|
+
}
|
|
86
|
+
const anthropicReset = getHeader(headers, "x-ratelimit-reset-tokens");
|
|
87
|
+
if (anthropicReset) {
|
|
88
|
+
const durationMs = parseDurationToMs(anthropicReset);
|
|
89
|
+
if (durationMs !== null) return durationMs;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function isTransientNetworkError(error) {
|
|
94
|
+
if (!(error instanceof Error)) return false;
|
|
95
|
+
const msg = error.message.toLowerCase();
|
|
96
|
+
const code = error.code;
|
|
97
|
+
if (error.name === "TypeError" && msg.includes("fetch failed")) return true;
|
|
98
|
+
const transientCodes = ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED"];
|
|
99
|
+
if (code && transientCodes.includes(code)) return true;
|
|
100
|
+
return msg.includes("network error") || msg.includes("socket hang up") || msg.includes("timeout");
|
|
101
|
+
}
|
|
102
|
+
function sleep(ms, signal) {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
if (signal?.aborted) {
|
|
105
|
+
return reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
106
|
+
}
|
|
107
|
+
const timer = setTimeout(() => {
|
|
108
|
+
signal?.removeEventListener("abort", onAbort);
|
|
109
|
+
resolve();
|
|
110
|
+
}, ms);
|
|
111
|
+
function onAbort() {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
reject(signal?.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
114
|
+
}
|
|
115
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/fetch.ts
|
|
120
|
+
async function retryFetch(url, init, options = {}) {
|
|
121
|
+
const {
|
|
122
|
+
retries = 3,
|
|
123
|
+
baseDelayMs = 500,
|
|
124
|
+
maxDelayMs = 3e4,
|
|
125
|
+
retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
|
|
126
|
+
onRetry,
|
|
127
|
+
signal
|
|
128
|
+
} = options;
|
|
129
|
+
let attempt = 0;
|
|
130
|
+
while (true) {
|
|
131
|
+
if (signal?.aborted) {
|
|
132
|
+
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const response = await fetch(url, {
|
|
136
|
+
...init,
|
|
137
|
+
signal: signal ?? init?.signal
|
|
138
|
+
});
|
|
139
|
+
if (retryOnStatus.includes(response.status) && attempt < retries) {
|
|
140
|
+
const headerDelay = parseRateLimitDelay(response.headers);
|
|
141
|
+
const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
142
|
+
const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);
|
|
143
|
+
onRetry?.(response, attempt + 1, chosenDelay);
|
|
144
|
+
await sleep(chosenDelay, signal);
|
|
145
|
+
attempt++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
return response;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (signal?.aborted || error instanceof Error && error.name === "AbortError") {
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
if (isTransientNetworkError(error) && attempt < retries) {
|
|
154
|
+
const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
155
|
+
onRetry?.(error, attempt + 1, chosenDelay);
|
|
156
|
+
await sleep(chosenDelay, signal);
|
|
157
|
+
attempt++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/wrap.ts
|
|
166
|
+
async function retry(fn, options = {}) {
|
|
167
|
+
const {
|
|
168
|
+
retries = 3,
|
|
169
|
+
baseDelayMs = 500,
|
|
170
|
+
maxDelayMs = 3e4,
|
|
171
|
+
retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
|
|
172
|
+
onRetry,
|
|
173
|
+
signal
|
|
174
|
+
} = options;
|
|
175
|
+
let attempt = 0;
|
|
176
|
+
while (true) {
|
|
177
|
+
if (signal?.aborted) {
|
|
178
|
+
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
return await fn(attempt);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (signal?.aborted || error instanceof Error && error.name === "AbortError") {
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
if (attempt >= retries) {
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
const err = error;
|
|
190
|
+
const statusCode = err?.status ?? err?.statusCode ?? err?.response?.status;
|
|
191
|
+
const headers = err?.headers ?? err?.response?.headers;
|
|
192
|
+
const isStatusRetryable = statusCode !== void 0 && retryOnStatus.includes(statusCode);
|
|
193
|
+
const isNetworkRetryable = isTransientNetworkError(error);
|
|
194
|
+
if (!isStatusRetryable && !isNetworkRetryable) {
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
let chosenDelay;
|
|
198
|
+
const headerDelay = headers ? parseRateLimitDelay(headers) : null;
|
|
199
|
+
if (headerDelay !== null) {
|
|
200
|
+
chosenDelay = Math.min(maxDelayMs, headerDelay);
|
|
201
|
+
} else {
|
|
202
|
+
chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
203
|
+
}
|
|
204
|
+
onRetry?.(error, attempt + 1, chosenDelay);
|
|
205
|
+
await sleep(chosenDelay, signal);
|
|
206
|
+
attempt++;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
DEFAULT_RETRYABLE_STATUS_CODES,
|
|
212
|
+
computeJitterDelay,
|
|
213
|
+
isTransientNetworkError,
|
|
214
|
+
parseDurationToMs,
|
|
215
|
+
parseRateLimitDelay,
|
|
216
|
+
retry,
|
|
217
|
+
retryFetch,
|
|
218
|
+
sleep
|
|
219
|
+
};
|
|
220
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/engine.ts","../src/fetch.ts","../src/wrap.ts"],"sourcesContent":["import { HeaderSource } from './types.js';\r\n\r\nexport const DEFAULT_RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];\r\n\r\n/**\r\n * Calculates exponential backoff with full jitter:\r\n * sleepMs = min(maxDelayMs, baseDelayMs * 2 ** attempt) * Math.random()\r\n */\r\nexport function computeJitterDelay(\r\n attempt: number,\r\n baseDelayMs: number = 500,\r\n maxDelayMs: number = 30000\r\n): number {\r\n const exponential = baseDelayMs * Math.pow(2, attempt);\r\n const ceiling = Math.min(maxDelayMs, exponential);\r\n return Math.floor(ceiling * Math.random());\r\n}\r\n\r\n/**\r\n * Safely extracts a header value case-insensitively across Headers, records, or tuples.\r\n */\r\nexport function getHeader(headers: HeaderSource, name: string): string | null {\r\n if (!headers) return null;\r\n\r\n const target = name.toLowerCase();\r\n\r\n // Native Web API Headers\r\n if (typeof (headers as Headers).get === 'function') {\r\n return (headers as Headers).get(target);\r\n }\r\n\r\n // Array of tuples\r\n if (Array.isArray(headers)) {\r\n for (const [key, value] of headers) {\r\n if (key.toLowerCase() === target) {\r\n return value;\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n // Plain object\r\n if (typeof headers === 'object') {\r\n for (const key of Object.keys(headers)) {\r\n if (key.toLowerCase() === target) {\r\n const val = (headers as Record<string, unknown>)[key];\r\n if (Array.isArray(val)) return val[0] ?? null;\r\n return typeof val === 'string' ? val : String(val);\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Parses duration strings returned by LLM APIs (e.g. \"600ms\", \"10s\", \"2m\", \"1m30s\", \"1.5\").\r\n */\r\nexport function parseDurationToMs(value: string): number | null {\r\n const trimmed = value.trim();\r\n if (!trimmed) return null;\r\n\r\n // Pure numeric string represents seconds\r\n if (/^\\d+(\\.\\d+)?$/.test(trimmed)) {\r\n const sec = parseFloat(trimmed);\r\n return isNaN(sec) ? null : Math.max(0, Math.floor(sec * 1000));\r\n }\r\n\r\n // Units match\r\n const regex = /(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)/gi;\r\n let match: RegExpExecArray | null;\r\n let totalMs = 0;\r\n let matched = false;\r\n\r\n while ((match = regex.exec(trimmed)) !== null) {\r\n matched = true;\r\n const amount = parseFloat(match[1]);\r\n const unit = match[2].toLowerCase();\r\n\r\n switch (unit) {\r\n case 'ms':\r\n totalMs += amount;\r\n break;\r\n case 's':\r\n totalMs += amount * 1000;\r\n break;\r\n case 'm':\r\n totalMs += amount * 60 * 1000;\r\n break;\r\n case 'h':\r\n totalMs += amount * 60 * 60 * 1000;\r\n break;\r\n case 'd':\r\n totalMs += amount * 24 * 60 * 60 * 1000;\r\n break;\r\n }\r\n }\r\n\r\n return matched ? Math.max(0, Math.floor(totalMs)) : null;\r\n}\r\n\r\n/**\r\n * Inspects rate-limiting headers and extracts sleep delay in milliseconds:\r\n * - retry-after (seconds or HTTP date)\r\n * - x-ratelimit-reset-requests (OpenAI format)\r\n * - x-ratelimit-reset-tokens (Anthropic format)\r\n */\r\nexport function parseRateLimitDelay(headers: HeaderSource): number | null {\r\n if (!headers) return null;\r\n\r\n const retryAfter = getHeader(headers, 'retry-after');\r\n if (retryAfter) {\r\n if (/^\\d+(\\.\\d+)?$/.test(retryAfter.trim())) {\r\n const sec = parseFloat(retryAfter.trim());\r\n if (!isNaN(sec)) return Math.max(0, Math.floor(sec * 1000));\r\n }\r\n\r\n const parsedDate = Date.parse(retryAfter);\r\n if (!isNaN(parsedDate)) {\r\n return Math.max(0, parsedDate - Date.now());\r\n }\r\n }\r\n\r\n const openaiReset = getHeader(headers, 'x-ratelimit-reset-requests');\r\n if (openaiReset) {\r\n const durationMs = parseDurationToMs(openaiReset);\r\n if (durationMs !== null) return durationMs;\r\n }\r\n\r\n const anthropicReset = getHeader(headers, 'x-ratelimit-reset-tokens');\r\n if (anthropicReset) {\r\n const durationMs = parseDurationToMs(anthropicReset);\r\n if (durationMs !== null) return durationMs;\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Detects transient network drop errors.\r\n */\r\nexport function isTransientNetworkError(error: unknown): boolean {\r\n if (!(error instanceof Error)) return false;\r\n\r\n const msg = error.message.toLowerCase();\r\n const code = (error as { code?: string }).code;\r\n\r\n if (error.name === 'TypeError' && msg.includes('fetch failed')) return true;\r\n\r\n const transientCodes = ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED'];\r\n if (code && transientCodes.includes(code)) return true;\r\n\r\n return (\r\n msg.includes('network error') ||\r\n msg.includes('socket hang up') ||\r\n msg.includes('timeout')\r\n );\r\n}\r\n\r\n/**\r\n * Sleep helper respecting AbortSignal cancellation.\r\n */\r\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\r\n return new Promise((resolve, reject) => {\r\n if (signal?.aborted) {\r\n return reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));\r\n }\r\n\r\n const timer = setTimeout(() => {\r\n signal?.removeEventListener('abort', onAbort);\r\n resolve();\r\n }, ms);\r\n\r\n function onAbort() {\r\n clearTimeout(timer);\r\n reject(signal?.reason ?? new DOMException('The operation was aborted.', 'AbortError'));\r\n }\r\n\r\n signal?.addEventListener('abort', onAbort, { once: true });\r\n });\r\n}","import {\r\n computeJitterDelay,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n isTransientNetworkError,\r\n parseRateLimitDelay,\r\n sleep,\r\n} from './engine.js';\r\nimport { RetryOptions } from './types.js';\r\n\r\nexport async function retryFetch(\r\n url: string | URL | Request,\r\n init?: RequestInit,\r\n options: RetryOptions = {}\r\n): Promise<Response> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n try {\r\n const response = await fetch(url, {\r\n ...init,\r\n signal: signal ?? init?.signal,\r\n });\r\n\r\n if (retryOnStatus.includes(response.status) && attempt < retries) {\r\n const headerDelay = parseRateLimitDelay(response.headers);\r\n const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);\r\n\r\n onRetry?.(response, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n continue;\r\n }\r\n\r\n return response;\r\n } catch (error) {\r\n if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {\r\n throw error;\r\n }\r\n\r\n if (isTransientNetworkError(error) && attempt < retries) {\r\n const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n onRetry?.(error, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n continue;\r\n }\r\n\r\n throw error;\r\n }\r\n }\r\n}","import {\r\n computeJitterDelay,\r\n DEFAULT_RETRYABLE_STATUS_CODES,\r\n isTransientNetworkError,\r\n parseRateLimitDelay,\r\n sleep,\r\n} from './engine.js';\r\nimport { HeaderSource, RetryOptions } from './types.js';\r\n\r\ninterface ErrorWithStatus extends Error {\r\n status?: number;\r\n statusCode?: number;\r\n headers?: HeaderSource;\r\n response?: {\r\n status?: number;\r\n headers?: HeaderSource;\r\n };\r\n}\r\n\r\nexport async function retry<T>(\r\n fn: (attempt: number) => Promise<T>,\r\n options: RetryOptions = {}\r\n): Promise<T> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n onRetry,\r\n signal,\r\n } = options;\r\n\r\n let attempt = 0;\r\n\r\n while (true) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n\r\n try {\r\n return await fn(attempt);\r\n } catch (error) {\r\n if (signal?.aborted || (error instanceof Error && error.name === 'AbortError')) {\r\n throw error;\r\n }\r\n\r\n if (attempt >= retries) {\r\n throw error;\r\n }\r\n\r\n const err = error as ErrorWithStatus;\r\n const statusCode = err?.status ?? err?.statusCode ?? err?.response?.status;\r\n const headers = err?.headers ?? err?.response?.headers;\r\n\r\n const isStatusRetryable = statusCode !== undefined && retryOnStatus.includes(statusCode);\r\n const isNetworkRetryable = isTransientNetworkError(error);\r\n\r\n if (!isStatusRetryable && !isNetworkRetryable) {\r\n throw error;\r\n }\r\n\r\n let chosenDelay: number;\r\n const headerDelay = headers ? parseRateLimitDelay(headers) : null;\r\n\r\n if (headerDelay !== null) {\r\n chosenDelay = Math.min(maxDelayMs, headerDelay);\r\n } else {\r\n chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n }\r\n\r\n onRetry?.(error, attempt + 1, chosenDelay);\r\n await sleep(chosenDelay, signal);\r\n attempt++;\r\n }\r\n }\r\n}"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@rohan9024/retrykit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@rohan9024/retrykit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-dependency, header-aware network retry library tailored for LLM APIs and fetch.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"llms.txt",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"prepublishOnly": "npm run test && npm run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"retry",
|
|
30
|
+
"fetch",
|
|
31
|
+
"llm",
|
|
32
|
+
"openai",
|
|
33
|
+
"anthropic",
|
|
34
|
+
"backoff",
|
|
35
|
+
"jitter",
|
|
36
|
+
"exponential-backoff",
|
|
37
|
+
"rate-limit",
|
|
38
|
+
"retry-after"
|
|
39
|
+
],
|
|
40
|
+
"author": "",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.20.5",
|
|
44
|
+
"tsup": "^8.5.1",
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vitest": "^2.1.9"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
|
+
},
|
|
51
|
+
"directories": {
|
|
52
|
+
"test": "test"
|
|
53
|
+
}
|
|
54
|
+
}
|