@rohan9024/retrykit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -43
- package/dist/index.d.mts +51 -8
- package/dist/index.d.ts +51 -8
- package/dist/index.js +173 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +170 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -16
package/README.md
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
|
|
1
2
|
# @rohan9024/retrykit ⚡
|
|
2
3
|
|
|
3
|
-
>
|
|
4
|
+
> A zero-dependency, header-aware retry engine and circuit breaker built for reliable LLM API integrations.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@rohan9024/retrykit)
|
|
7
|
+
[](https://github.com/rohan9024/retrykit)
|
|
8
|
+
[](https://bundlephobia.com/package/@rohan9024/retrykit)
|
|
9
|
+
|
|
10
|
+
**RetryKit** helps developers build resilient applications powered by LLM APIs such as OpenAI, Anthropic, DeepSeek, Groq, and Mistral.
|
|
4
11
|
|
|
5
|
-
|
|
12
|
+
Unlike basic retry libraries such as `p-retry` or `tenacity`, RetryKit understands provider-specific rate-limit headers and uses that information to schedule smarter retries.
|
|
13
|
+
|
|
14
|
+
### Why RetryKit?
|
|
15
|
+
|
|
16
|
+
- **Header-aware retries:** Parses provider-specific rate-limit reset headers.
|
|
17
|
+
- **Exponential backoff with jitter:** Prevents synchronized retry storms.
|
|
18
|
+
- **SSE stream resilience:** Handles mid-stream connection interruptions.
|
|
19
|
+
- **Circuit breaker:** Prevents cascading failures during provider outages.
|
|
20
|
+
- **Timeout protection:** Supports per-attempt request timeouts.
|
|
21
|
+
- **Zero dependencies:** Lightweight and dependency-free.
|
|
6
22
|
|
|
7
23
|
---
|
|
8
24
|
|
|
@@ -14,40 +30,47 @@ npm install @rohan9024/retrykit
|
|
|
14
30
|
|
|
15
31
|
---
|
|
16
32
|
|
|
17
|
-
## 🚀
|
|
33
|
+
## 🚀 Quick Start
|
|
18
34
|
|
|
19
|
-
### 1. Drop-in Replacement
|
|
35
|
+
### 1. Drop-in Fetch Replacement (`retryFetch`)
|
|
20
36
|
|
|
21
|
-
`retryFetch`
|
|
37
|
+
Use `retryFetch` just like standard `fetch`, with automatic retries and rate-limit awareness.
|
|
22
38
|
|
|
23
39
|
```typescript
|
|
24
40
|
import { retryFetch } from '@rohan9024/retrykit';
|
|
25
41
|
|
|
26
|
-
const response = await retryFetch(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
'
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
const response = await retryFetch(
|
|
43
|
+
'https://api.openai.com/v1/chat/completions',
|
|
44
|
+
{
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: {
|
|
47
|
+
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
model: 'gpt-4o',
|
|
52
|
+
messages: [
|
|
53
|
+
{ role: 'user', content: 'Say hello!' }
|
|
54
|
+
],
|
|
55
|
+
}),
|
|
40
56
|
},
|
|
41
|
-
|
|
57
|
+
{
|
|
58
|
+
retries: 4,
|
|
59
|
+
timeoutMs: 15000,
|
|
60
|
+
onRetry: (error, attempt, delayMs) => {
|
|
61
|
+
console.warn(
|
|
62
|
+
`Retry #${attempt}: Waiting ${delayMs}ms...`
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
);
|
|
42
67
|
|
|
43
68
|
const data = await response.json();
|
|
44
69
|
```
|
|
45
70
|
|
|
46
|
-
---
|
|
47
|
-
|
|
48
71
|
### 2. Wrap Official SDKs (`retry`)
|
|
49
72
|
|
|
50
|
-
|
|
73
|
+
Use RetryKit with existing SDKs such as OpenAI, Anthropic, Mistral, or Google.
|
|
51
74
|
|
|
52
75
|
```typescript
|
|
53
76
|
import OpenAI from 'openai';
|
|
@@ -55,43 +78,113 @@ import { retry } from '@rohan9024/retrykit';
|
|
|
55
78
|
|
|
56
79
|
const openai = new OpenAI();
|
|
57
80
|
|
|
58
|
-
const completion = await retry(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
const completion = await retry(
|
|
82
|
+
() =>
|
|
83
|
+
openai.chat.completions.create({
|
|
84
|
+
model: 'gpt-4o',
|
|
85
|
+
messages: [
|
|
86
|
+
{
|
|
87
|
+
role: 'user',
|
|
88
|
+
content: 'Write a poem about resilience',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
}),
|
|
63
92
|
{
|
|
64
|
-
retries:
|
|
93
|
+
retries: 3,
|
|
65
94
|
baseDelayMs: 500,
|
|
66
95
|
}
|
|
67
96
|
);
|
|
68
97
|
```
|
|
69
98
|
|
|
99
|
+
### 3. Resilient Token Streaming (`retryStream`)
|
|
100
|
+
|
|
101
|
+
LLM streaming connections can fail midway through generation. `retryStream` helps recover from interrupted streams.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { retryStream } from '@rohan9024/retrykit';
|
|
105
|
+
|
|
106
|
+
async function* tokenGenerator() {
|
|
107
|
+
const stream = await fetchLLMStream();
|
|
108
|
+
|
|
109
|
+
for await (const chunk of stream) {
|
|
110
|
+
yield chunk;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for await (const chunk of retryStream(() => tokenGenerator())) {
|
|
115
|
+
process.stdout.write(chunk);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 4. Circuit Breaker
|
|
120
|
+
|
|
121
|
+
Protect your application from cascading failures and unnecessary requests when an AI provider experiences an outage.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import { CircuitBreaker } from '@rohan9024/retrykit';
|
|
125
|
+
|
|
126
|
+
const breaker = new CircuitBreaker({
|
|
127
|
+
failureThreshold: 3,
|
|
128
|
+
resetTimeoutMs: 30000,
|
|
129
|
+
|
|
130
|
+
onStateChange: (from, to) => {
|
|
131
|
+
console.log(`Circuit changed: ${from} -> ${to}`);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Fails fast when the circuit is open
|
|
136
|
+
const result = await breaker.execute(() => callUnstableAPI());
|
|
137
|
+
```
|
|
138
|
+
|
|
70
139
|
---
|
|
71
140
|
|
|
72
|
-
## 🧠 Provider
|
|
141
|
+
## 🧠 Supported Provider Headers
|
|
73
142
|
|
|
74
|
-
|
|
143
|
+
RetryKit automatically extracts retry timing information from commonly used API rate-limit headers.
|
|
75
144
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- `x-ratelimit-reset-tokens`
|
|
145
|
+
| Provider | Supported Headers |
|
|
146
|
+
|---|---|
|
|
147
|
+
| OpenAI | `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens`, `retry-after` |
|
|
148
|
+
| Anthropic | `anthropic-ratelimit-requests-reset`, `anthropic-ratelimit-tokens-reset`, `retry-after` |
|
|
149
|
+
| Groq / DeepSeek | `x-ratelimit-reset-total`, `retry-after-ms`, `retry-after` |
|
|
150
|
+
| Standard HTTP | `Retry-After` (integer seconds or RFC 1123 HTTP dates) |
|
|
79
151
|
|
|
80
152
|
---
|
|
81
153
|
|
|
82
|
-
## ⚙️ Options
|
|
154
|
+
## ⚙️ Configuration Options
|
|
155
|
+
|
|
156
|
+
RetryKit provides configurable retry behavior.
|
|
83
157
|
|
|
84
158
|
| Option | Type | Default | Description |
|
|
85
|
-
|
|
86
|
-
| `retries` | `number` | `3` |
|
|
87
|
-
| `baseDelayMs` | `number` | `500` |
|
|
88
|
-
| `maxDelayMs` | `number` | `30000` |
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
159
|
+
|---|---|---|---|
|
|
160
|
+
| `retries` | `number` | `3` | Maximum retry attempts before failing |
|
|
161
|
+
| `baseDelayMs` | `number` | `500` | Initial backoff delay in milliseconds |
|
|
162
|
+
| `maxDelayMs` | `number` | `30000` | Maximum backoff delay |
|
|
163
|
+
| `timeoutMs` | `number` | `undefined` | Per-attempt timeout in milliseconds |
|
|
164
|
+
| `retryOnStatus` | `number[]` | `[408, 429, 500, 502, 503, 504]` | HTTP status codes that trigger retries |
|
|
165
|
+
| `shouldRetry` | `(err, attempt) => boolean` | `undefined` | Custom retry decision logic |
|
|
166
|
+
| `onRetry` | `(err, attempt, delay) => void` | `undefined` | Callback executed before each retry |
|
|
167
|
+
| `signal` | `AbortSignal` | `undefined` | Abort signal for immediate cancellation |
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 🛡️ Designed For
|
|
172
|
+
|
|
173
|
+
- LLM-powered applications
|
|
174
|
+
- AI agents and autonomous workflows
|
|
175
|
+
- RAG pipelines
|
|
176
|
+
- High-volume API integrations
|
|
177
|
+
- Streaming chat applications
|
|
178
|
+
- Production AI infrastructure
|
|
92
179
|
|
|
93
180
|
---
|
|
94
181
|
|
|
95
182
|
## 📄 License
|
|
96
183
|
|
|
97
|
-
MIT
|
|
184
|
+
MIT © [rohan9024](https://github.com/rohan9024)
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
<p align="center">
|
|
189
|
+
Built for developers who want their AI applications to be resilient. ⚡
|
|
190
|
+
</p>
|
package/dist/index.d.mts
CHANGED
|
@@ -5,19 +5,61 @@ interface RetryOptions {
|
|
|
5
5
|
baseDelayMs?: number;
|
|
6
6
|
/** Maximum backoff ceiling delay in milliseconds. Default: 30000 */
|
|
7
7
|
maxDelayMs?: number;
|
|
8
|
+
/** Per-request timeout in milliseconds. Default: undefined */
|
|
9
|
+
timeoutMs?: number;
|
|
8
10
|
/** HTTP status codes that trigger a retry. Default: [408, 429, 500, 502, 503, 504] */
|
|
9
11
|
retryOnStatus?: number[];
|
|
10
|
-
/**
|
|
12
|
+
/** Custom condition to decide whether to retry */
|
|
13
|
+
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
14
|
+
/** Optional callback fired before every retry sleep */
|
|
11
15
|
onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
|
|
12
16
|
/** AbortSignal for immediate cancellation */
|
|
13
17
|
signal?: AbortSignal;
|
|
14
18
|
}
|
|
19
|
+
interface CircuitBreakerOptions {
|
|
20
|
+
/** Number of consecutive failures before opening the circuit. Default: 5 */
|
|
21
|
+
failureThreshold?: number;
|
|
22
|
+
/** Time in milliseconds to wait before attempting recovery (HALF_OPEN). Default: 30000 */
|
|
23
|
+
resetTimeoutMs?: number;
|
|
24
|
+
/** Optional callback triggered on circuit state changes */
|
|
25
|
+
onStateChange?: (from: CircuitState, to: CircuitState) => void;
|
|
26
|
+
}
|
|
27
|
+
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
15
28
|
type HeaderSource = Headers | Record<string, string | string[] | undefined> | [string, string][] | undefined;
|
|
16
29
|
|
|
17
30
|
declare function retryFetch(url: string | URL | Request, init?: RequestInit, options?: RetryOptions): Promise<Response>;
|
|
18
31
|
|
|
19
32
|
declare function retry<T>(fn: (attempt: number) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
20
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Resilient wrapper for Server-Sent Events / LLM chunk streams.
|
|
36
|
+
* If the streaming connection drops mid-generation, it retries and resumes.
|
|
37
|
+
*/
|
|
38
|
+
declare function retryStream<T>(streamFactory: (attempt: number) => Promise<AsyncIterable<T>>, options?: RetryOptions): AsyncGenerator<T, void, unknown>;
|
|
39
|
+
|
|
40
|
+
declare class CircuitBreakerOpenError extends Error {
|
|
41
|
+
constructor(message?: string);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Zero-dependency Circuit Breaker implementation to stop cascading failure
|
|
45
|
+
* cascades when downstream LLM APIs experience total outages.
|
|
46
|
+
*/
|
|
47
|
+
declare class CircuitBreaker {
|
|
48
|
+
private state;
|
|
49
|
+
private failureCount;
|
|
50
|
+
private lastFailureTime;
|
|
51
|
+
private readonly failureThreshold;
|
|
52
|
+
private readonly resetTimeoutMs;
|
|
53
|
+
private readonly onStateChange?;
|
|
54
|
+
constructor(options?: CircuitBreakerOptions);
|
|
55
|
+
getState(): CircuitState;
|
|
56
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
57
|
+
private recordSuccess;
|
|
58
|
+
private recordFailure;
|
|
59
|
+
private transitionTo;
|
|
60
|
+
reset(): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
21
63
|
declare const DEFAULT_RETRYABLE_STATUS_CODES: number[];
|
|
22
64
|
/**
|
|
23
65
|
* Calculates exponential backoff with full jitter:
|
|
@@ -29,19 +71,20 @@ declare function computeJitterDelay(attempt: number, baseDelayMs?: number, maxDe
|
|
|
29
71
|
*/
|
|
30
72
|
declare function parseDurationToMs(value: string): number | null;
|
|
31
73
|
/**
|
|
32
|
-
*
|
|
33
|
-
* -
|
|
34
|
-
* - x-ratelimit-reset-requests
|
|
35
|
-
* -
|
|
74
|
+
* Exhaustively checks provider headers across:
|
|
75
|
+
* - RFC 7231 / RFC 6585 standard `retry-after`
|
|
76
|
+
* - OpenAI: `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens`
|
|
77
|
+
* - Anthropic: `anthropic-ratelimit-requests-reset`, `anthropic-ratelimit-tokens-reset`
|
|
78
|
+
* - Groq / DeepSeek / Mistral: `x-ratelimit-reset-total`, `retry-after-ms`
|
|
36
79
|
*/
|
|
37
80
|
declare function parseRateLimitDelay(headers: HeaderSource): number | null;
|
|
38
81
|
/**
|
|
39
|
-
* Detects transient network drop errors.
|
|
82
|
+
* Detects transient network drop errors across Node, Bun, Deno, and browsers.
|
|
40
83
|
*/
|
|
41
84
|
declare function isTransientNetworkError(error: unknown): boolean;
|
|
42
85
|
/**
|
|
43
|
-
*
|
|
86
|
+
* Promisified sleep that respects AbortSignal cancellation.
|
|
44
87
|
*/
|
|
45
88
|
declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
46
89
|
|
|
47
|
-
export { DEFAULT_RETRYABLE_STATUS_CODES, type HeaderSource, type RetryOptions, computeJitterDelay, isTransientNetworkError, parseDurationToMs, parseRateLimitDelay, retry, retryFetch, sleep };
|
|
90
|
+
export { CircuitBreaker, CircuitBreakerOpenError, type CircuitBreakerOptions, type CircuitState, DEFAULT_RETRYABLE_STATUS_CODES, type HeaderSource, type RetryOptions, computeJitterDelay, isTransientNetworkError, parseDurationToMs, parseRateLimitDelay, retry, retryFetch, retryStream, sleep };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,19 +5,61 @@ interface RetryOptions {
|
|
|
5
5
|
baseDelayMs?: number;
|
|
6
6
|
/** Maximum backoff ceiling delay in milliseconds. Default: 30000 */
|
|
7
7
|
maxDelayMs?: number;
|
|
8
|
+
/** Per-request timeout in milliseconds. Default: undefined */
|
|
9
|
+
timeoutMs?: number;
|
|
8
10
|
/** HTTP status codes that trigger a retry. Default: [408, 429, 500, 502, 503, 504] */
|
|
9
11
|
retryOnStatus?: number[];
|
|
10
|
-
/**
|
|
12
|
+
/** Custom condition to decide whether to retry */
|
|
13
|
+
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
14
|
+
/** Optional callback fired before every retry sleep */
|
|
11
15
|
onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
|
|
12
16
|
/** AbortSignal for immediate cancellation */
|
|
13
17
|
signal?: AbortSignal;
|
|
14
18
|
}
|
|
19
|
+
interface CircuitBreakerOptions {
|
|
20
|
+
/** Number of consecutive failures before opening the circuit. Default: 5 */
|
|
21
|
+
failureThreshold?: number;
|
|
22
|
+
/** Time in milliseconds to wait before attempting recovery (HALF_OPEN). Default: 30000 */
|
|
23
|
+
resetTimeoutMs?: number;
|
|
24
|
+
/** Optional callback triggered on circuit state changes */
|
|
25
|
+
onStateChange?: (from: CircuitState, to: CircuitState) => void;
|
|
26
|
+
}
|
|
27
|
+
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
|
|
15
28
|
type HeaderSource = Headers | Record<string, string | string[] | undefined> | [string, string][] | undefined;
|
|
16
29
|
|
|
17
30
|
declare function retryFetch(url: string | URL | Request, init?: RequestInit, options?: RetryOptions): Promise<Response>;
|
|
18
31
|
|
|
19
32
|
declare function retry<T>(fn: (attempt: number) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
20
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Resilient wrapper for Server-Sent Events / LLM chunk streams.
|
|
36
|
+
* If the streaming connection drops mid-generation, it retries and resumes.
|
|
37
|
+
*/
|
|
38
|
+
declare function retryStream<T>(streamFactory: (attempt: number) => Promise<AsyncIterable<T>>, options?: RetryOptions): AsyncGenerator<T, void, unknown>;
|
|
39
|
+
|
|
40
|
+
declare class CircuitBreakerOpenError extends Error {
|
|
41
|
+
constructor(message?: string);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Zero-dependency Circuit Breaker implementation to stop cascading failure
|
|
45
|
+
* cascades when downstream LLM APIs experience total outages.
|
|
46
|
+
*/
|
|
47
|
+
declare class CircuitBreaker {
|
|
48
|
+
private state;
|
|
49
|
+
private failureCount;
|
|
50
|
+
private lastFailureTime;
|
|
51
|
+
private readonly failureThreshold;
|
|
52
|
+
private readonly resetTimeoutMs;
|
|
53
|
+
private readonly onStateChange?;
|
|
54
|
+
constructor(options?: CircuitBreakerOptions);
|
|
55
|
+
getState(): CircuitState;
|
|
56
|
+
execute<T>(fn: () => Promise<T>): Promise<T>;
|
|
57
|
+
private recordSuccess;
|
|
58
|
+
private recordFailure;
|
|
59
|
+
private transitionTo;
|
|
60
|
+
reset(): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
21
63
|
declare const DEFAULT_RETRYABLE_STATUS_CODES: number[];
|
|
22
64
|
/**
|
|
23
65
|
* Calculates exponential backoff with full jitter:
|
|
@@ -29,19 +71,20 @@ declare function computeJitterDelay(attempt: number, baseDelayMs?: number, maxDe
|
|
|
29
71
|
*/
|
|
30
72
|
declare function parseDurationToMs(value: string): number | null;
|
|
31
73
|
/**
|
|
32
|
-
*
|
|
33
|
-
* -
|
|
34
|
-
* - x-ratelimit-reset-requests
|
|
35
|
-
* -
|
|
74
|
+
* Exhaustively checks provider headers across:
|
|
75
|
+
* - RFC 7231 / RFC 6585 standard `retry-after`
|
|
76
|
+
* - OpenAI: `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens`
|
|
77
|
+
* - Anthropic: `anthropic-ratelimit-requests-reset`, `anthropic-ratelimit-tokens-reset`
|
|
78
|
+
* - Groq / DeepSeek / Mistral: `x-ratelimit-reset-total`, `retry-after-ms`
|
|
36
79
|
*/
|
|
37
80
|
declare function parseRateLimitDelay(headers: HeaderSource): number | null;
|
|
38
81
|
/**
|
|
39
|
-
* Detects transient network drop errors.
|
|
82
|
+
* Detects transient network drop errors across Node, Bun, Deno, and browsers.
|
|
40
83
|
*/
|
|
41
84
|
declare function isTransientNetworkError(error: unknown): boolean;
|
|
42
85
|
/**
|
|
43
|
-
*
|
|
86
|
+
* Promisified sleep that respects AbortSignal cancellation.
|
|
44
87
|
*/
|
|
45
88
|
declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
46
89
|
|
|
47
|
-
export { DEFAULT_RETRYABLE_STATUS_CODES, type HeaderSource, type RetryOptions, computeJitterDelay, isTransientNetworkError, parseDurationToMs, parseRateLimitDelay, retry, retryFetch, sleep };
|
|
90
|
+
export { CircuitBreaker, CircuitBreakerOpenError, type CircuitBreakerOptions, type CircuitState, DEFAULT_RETRYABLE_STATUS_CODES, type HeaderSource, type RetryOptions, computeJitterDelay, isTransientNetworkError, parseDurationToMs, parseRateLimitDelay, retry, retryFetch, retryStream, sleep };
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
CircuitBreaker: () => CircuitBreaker,
|
|
24
|
+
CircuitBreakerOpenError: () => CircuitBreakerOpenError,
|
|
23
25
|
DEFAULT_RETRYABLE_STATUS_CODES: () => DEFAULT_RETRYABLE_STATUS_CODES,
|
|
24
26
|
computeJitterDelay: () => computeJitterDelay,
|
|
25
27
|
isTransientNetworkError: () => isTransientNetworkError,
|
|
@@ -27,6 +29,7 @@ __export(index_exports, {
|
|
|
27
29
|
parseRateLimitDelay: () => parseRateLimitDelay,
|
|
28
30
|
retry: () => retry,
|
|
29
31
|
retryFetch: () => retryFetch,
|
|
32
|
+
retryStream: () => retryStream,
|
|
30
33
|
sleep: () => sleep
|
|
31
34
|
});
|
|
32
35
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -100,6 +103,11 @@ function parseDurationToMs(value) {
|
|
|
100
103
|
}
|
|
101
104
|
function parseRateLimitDelay(headers) {
|
|
102
105
|
if (!headers) return null;
|
|
106
|
+
const retryAfterMs = getHeader(headers, "retry-after-ms");
|
|
107
|
+
if (retryAfterMs) {
|
|
108
|
+
const parsed = parseFloat(retryAfterMs);
|
|
109
|
+
if (!isNaN(parsed)) return Math.max(0, Math.floor(parsed));
|
|
110
|
+
}
|
|
103
111
|
const retryAfter = getHeader(headers, "retry-after");
|
|
104
112
|
if (retryAfter) {
|
|
105
113
|
if (/^\d+(\.\d+)?$/.test(retryAfter.trim())) {
|
|
@@ -111,15 +119,23 @@ function parseRateLimitDelay(headers) {
|
|
|
111
119
|
return Math.max(0, parsedDate - Date.now());
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
const candidateKeys = [
|
|
123
|
+
"x-ratelimit-reset-requests",
|
|
124
|
+
"x-ratelimit-reset-tokens",
|
|
125
|
+
"anthropic-ratelimit-requests-reset",
|
|
126
|
+
"anthropic-ratelimit-tokens-reset",
|
|
127
|
+
"x-ratelimit-reset-total"
|
|
128
|
+
];
|
|
129
|
+
for (const key of candidateKeys) {
|
|
130
|
+
const raw = getHeader(headers, key);
|
|
131
|
+
if (raw) {
|
|
132
|
+
const ms = parseDurationToMs(raw);
|
|
133
|
+
if (ms !== null) return ms;
|
|
134
|
+
const epochSec = parseFloat(raw);
|
|
135
|
+
if (!isNaN(epochSec) && epochSec > 1e9) {
|
|
136
|
+
return Math.max(0, Math.floor(epochSec * 1e3 - Date.now()));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
123
139
|
}
|
|
124
140
|
return null;
|
|
125
141
|
}
|
|
@@ -128,9 +144,17 @@ function isTransientNetworkError(error) {
|
|
|
128
144
|
const msg = error.message.toLowerCase();
|
|
129
145
|
const code = error.code;
|
|
130
146
|
if (error.name === "TypeError" && msg.includes("fetch failed")) return true;
|
|
131
|
-
const transientCodes = [
|
|
147
|
+
const transientCodes = [
|
|
148
|
+
"ECONNRESET",
|
|
149
|
+
"ETIMEDOUT",
|
|
150
|
+
"ENOTFOUND",
|
|
151
|
+
"EAI_AGAIN",
|
|
152
|
+
"ECONNREFUSED",
|
|
153
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
154
|
+
"UND_ERR_SOCKET"
|
|
155
|
+
];
|
|
132
156
|
if (code && transientCodes.includes(code)) return true;
|
|
133
|
-
return msg.includes("network error") || msg.includes("socket hang up") || msg.includes("
|
|
157
|
+
return msg.includes("network error") || msg.includes("socket hang up") || msg.includes("terminated") || msg.includes("econnreset");
|
|
134
158
|
}
|
|
135
159
|
function sleep(ms, signal) {
|
|
136
160
|
return new Promise((resolve, reject) => {
|
|
@@ -155,7 +179,9 @@ async function retryFetch(url, init, options = {}) {
|
|
|
155
179
|
retries = 3,
|
|
156
180
|
baseDelayMs = 500,
|
|
157
181
|
maxDelayMs = 3e4,
|
|
182
|
+
timeoutMs,
|
|
158
183
|
retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
|
|
184
|
+
shouldRetry,
|
|
159
185
|
onRetry,
|
|
160
186
|
signal
|
|
161
187
|
} = options;
|
|
@@ -164,12 +190,25 @@ async function retryFetch(url, init, options = {}) {
|
|
|
164
190
|
if (signal?.aborted) {
|
|
165
191
|
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
166
192
|
}
|
|
193
|
+
const attemptController = new AbortController();
|
|
194
|
+
let timeoutId;
|
|
195
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
196
|
+
timeoutId = setTimeout(() => {
|
|
197
|
+
attemptController.abort(new DOMException(`Request timed out after ${timeoutMs}ms`, "TimeoutError"));
|
|
198
|
+
}, timeoutMs);
|
|
199
|
+
}
|
|
200
|
+
const abortHandler = () => attemptController.abort(signal?.reason);
|
|
201
|
+
signal?.addEventListener("abort", abortHandler, { once: true });
|
|
167
202
|
try {
|
|
168
203
|
const response = await fetch(url, {
|
|
169
204
|
...init,
|
|
170
|
-
signal: signal
|
|
205
|
+
signal: attemptController.signal
|
|
171
206
|
});
|
|
172
|
-
if (
|
|
207
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
208
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
209
|
+
const isStatusRetryable = retryOnStatus.includes(response.status);
|
|
210
|
+
const customRetry = shouldRetry ? shouldRetry(response, attempt) : isStatusRetryable;
|
|
211
|
+
if (customRetry && attempt < retries) {
|
|
173
212
|
const headerDelay = parseRateLimitDelay(response.headers);
|
|
174
213
|
const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
175
214
|
const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);
|
|
@@ -180,10 +219,15 @@ async function retryFetch(url, init, options = {}) {
|
|
|
180
219
|
}
|
|
181
220
|
return response;
|
|
182
221
|
} catch (error) {
|
|
183
|
-
if (
|
|
184
|
-
|
|
222
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
223
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
224
|
+
if (signal?.aborted) {
|
|
225
|
+
throw signal.reason ?? error;
|
|
185
226
|
}
|
|
186
|
-
|
|
227
|
+
const isTimeout = error instanceof Error && error.name === "TimeoutError";
|
|
228
|
+
const isNetworkDrop = isTransientNetworkError(error);
|
|
229
|
+
const customRetry = shouldRetry ? shouldRetry(error, attempt) : isNetworkDrop || isTimeout;
|
|
230
|
+
if (customRetry && attempt < retries) {
|
|
187
231
|
const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
188
232
|
onRetry?.(error, attempt + 1, chosenDelay);
|
|
189
233
|
await sleep(chosenDelay, signal);
|
|
@@ -202,6 +246,7 @@ async function retry(fn, options = {}) {
|
|
|
202
246
|
baseDelayMs = 500,
|
|
203
247
|
maxDelayMs = 3e4,
|
|
204
248
|
retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
|
|
249
|
+
shouldRetry,
|
|
205
250
|
onRetry,
|
|
206
251
|
signal
|
|
207
252
|
} = options;
|
|
@@ -224,7 +269,8 @@ async function retry(fn, options = {}) {
|
|
|
224
269
|
const headers = err?.headers ?? err?.response?.headers;
|
|
225
270
|
const isStatusRetryable = statusCode !== void 0 && retryOnStatus.includes(statusCode);
|
|
226
271
|
const isNetworkRetryable = isTransientNetworkError(error);
|
|
227
|
-
|
|
272
|
+
const customRetry = shouldRetry ? shouldRetry(error, attempt) : isStatusRetryable || isNetworkRetryable;
|
|
273
|
+
if (!customRetry) {
|
|
228
274
|
throw error;
|
|
229
275
|
}
|
|
230
276
|
let chosenDelay;
|
|
@@ -240,8 +286,117 @@ async function retry(fn, options = {}) {
|
|
|
240
286
|
}
|
|
241
287
|
}
|
|
242
288
|
}
|
|
289
|
+
|
|
290
|
+
// src/stream.ts
|
|
291
|
+
async function* retryStream(streamFactory, options = {}) {
|
|
292
|
+
const {
|
|
293
|
+
retries = 3,
|
|
294
|
+
baseDelayMs = 500,
|
|
295
|
+
maxDelayMs = 3e4,
|
|
296
|
+
onRetry,
|
|
297
|
+
signal
|
|
298
|
+
} = options;
|
|
299
|
+
let attempt = 0;
|
|
300
|
+
while (true) {
|
|
301
|
+
if (signal?.aborted) {
|
|
302
|
+
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
const stream = await streamFactory(attempt);
|
|
306
|
+
for await (const chunk of stream) {
|
|
307
|
+
if (signal?.aborted) {
|
|
308
|
+
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
309
|
+
}
|
|
310
|
+
yield chunk;
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (signal?.aborted || error instanceof Error && error.name === "AbortError") {
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
if (attempt >= retries) {
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
const delay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
321
|
+
onRetry?.(error, attempt + 1, delay);
|
|
322
|
+
await sleep(delay, signal);
|
|
323
|
+
attempt++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/circuit-breaker.ts
|
|
329
|
+
var CircuitBreakerOpenError = class extends Error {
|
|
330
|
+
constructor(message = "Circuit breaker is OPEN. Fast-failing requests.") {
|
|
331
|
+
super(message);
|
|
332
|
+
this.name = "CircuitBreakerOpenError";
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
var CircuitBreaker = class {
|
|
336
|
+
state = "CLOSED";
|
|
337
|
+
failureCount = 0;
|
|
338
|
+
lastFailureTime = 0;
|
|
339
|
+
failureThreshold;
|
|
340
|
+
resetTimeoutMs;
|
|
341
|
+
onStateChange;
|
|
342
|
+
constructor(options = {}) {
|
|
343
|
+
this.failureThreshold = options.failureThreshold ?? 5;
|
|
344
|
+
this.resetTimeoutMs = options.resetTimeoutMs ?? 3e4;
|
|
345
|
+
this.onStateChange = options.onStateChange;
|
|
346
|
+
}
|
|
347
|
+
getState() {
|
|
348
|
+
if (this.state === "OPEN") {
|
|
349
|
+
const elapsed = Date.now() - this.lastFailureTime;
|
|
350
|
+
if (elapsed >= this.resetTimeoutMs) {
|
|
351
|
+
this.transitionTo("HALF_OPEN");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return this.state;
|
|
355
|
+
}
|
|
356
|
+
async execute(fn) {
|
|
357
|
+
const currentState = this.getState();
|
|
358
|
+
if (currentState === "OPEN") {
|
|
359
|
+
throw new CircuitBreakerOpenError();
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const result = await fn();
|
|
363
|
+
this.recordSuccess();
|
|
364
|
+
return result;
|
|
365
|
+
} catch (error) {
|
|
366
|
+
this.recordFailure();
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
recordSuccess() {
|
|
371
|
+
if (this.state === "HALF_OPEN") {
|
|
372
|
+
this.transitionTo("CLOSED");
|
|
373
|
+
}
|
|
374
|
+
this.failureCount = 0;
|
|
375
|
+
}
|
|
376
|
+
recordFailure() {
|
|
377
|
+
this.failureCount++;
|
|
378
|
+
this.lastFailureTime = Date.now();
|
|
379
|
+
if (this.state === "HALF_OPEN" || this.failureCount >= this.failureThreshold) {
|
|
380
|
+
this.transitionTo("OPEN");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
transitionTo(nextState) {
|
|
384
|
+
const prevState = this.state;
|
|
385
|
+
if (prevState !== nextState) {
|
|
386
|
+
this.state = nextState;
|
|
387
|
+
this.onStateChange?.(prevState, nextState);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
reset() {
|
|
391
|
+
this.state = "CLOSED";
|
|
392
|
+
this.failureCount = 0;
|
|
393
|
+
this.lastFailureTime = 0;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
243
396
|
// Annotate the CommonJS export names for ESM import in node:
|
|
244
397
|
0 && (module.exports = {
|
|
398
|
+
CircuitBreaker,
|
|
399
|
+
CircuitBreakerOpenError,
|
|
245
400
|
DEFAULT_RETRYABLE_STATUS_CODES,
|
|
246
401
|
computeJitterDelay,
|
|
247
402
|
isTransientNetworkError,
|
|
@@ -249,6 +404,7 @@ async function retry(fn, options = {}) {
|
|
|
249
404
|
parseRateLimitDelay,
|
|
250
405
|
retry,
|
|
251
406
|
retryFetch,
|
|
407
|
+
retryStream,
|
|
252
408
|
sleep
|
|
253
409
|
});
|
|
254
410
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/engine.ts","../src/fetch.ts","../src/wrap.ts","../src/stream.ts","../src/circuit-breaker.ts"],"sourcesContent":["export { retryFetch } from './fetch.js';\r\nexport { retry } from './wrap.js';\r\nexport { retryStream } from './stream.js';\r\nexport { CircuitBreaker, CircuitBreakerOpenError } from './circuit-breaker.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 {\r\n RetryOptions,\r\n CircuitBreakerOptions,\r\n CircuitState,\r\n HeaderSource,\r\n} 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 instances, 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 if (typeof (headers as Headers).get === 'function') {\r\n return (headers as Headers).get(target);\r\n }\r\n\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 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 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 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 * Exhaustively checks provider headers across:\r\n * - RFC 7231 / RFC 6585 standard `retry-after`\r\n * - OpenAI: `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens`\r\n * - Anthropic: `anthropic-ratelimit-requests-reset`, `anthropic-ratelimit-tokens-reset`\r\n * - Groq / DeepSeek / Mistral: `x-ratelimit-reset-total`, `retry-after-ms`\r\n */\r\nexport function parseRateLimitDelay(headers: HeaderSource): number | null {\r\n if (!headers) return null;\r\n\r\n // 1. Direct milliseconds header (Groq, custom proxies)\r\n const retryAfterMs = getHeader(headers, 'retry-after-ms');\r\n if (retryAfterMs) {\r\n const parsed = parseFloat(retryAfterMs);\r\n if (!isNaN(parsed)) return Math.max(0, Math.floor(parsed));\r\n }\r\n\r\n // 2. Standard Retry-After (seconds or RFC 1123 HTTP Date)\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 // 3. Provider-specific reset headers\r\n const candidateKeys = [\r\n 'x-ratelimit-reset-requests',\r\n 'x-ratelimit-reset-tokens',\r\n 'anthropic-ratelimit-requests-reset',\r\n 'anthropic-ratelimit-tokens-reset',\r\n 'x-ratelimit-reset-total',\r\n ];\r\n\r\n for (const key of candidateKeys) {\r\n const raw = getHeader(headers, key);\r\n if (raw) {\r\n const ms = parseDurationToMs(raw);\r\n if (ms !== null) return ms;\r\n\r\n // Handle epoch timestamp formats\r\n const epochSec = parseFloat(raw);\r\n if (!isNaN(epochSec) && epochSec > 1000000000) {\r\n return Math.max(0, Math.floor(epochSec * 1000 - Date.now()));\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Detects transient network drop errors across Node, Bun, Deno, and browsers.\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 = [\r\n 'ECONNRESET',\r\n 'ETIMEDOUT',\r\n 'ENOTFOUND',\r\n 'EAI_AGAIN',\r\n 'ECONNREFUSED',\r\n 'UND_ERR_CONNECT_TIMEOUT',\r\n 'UND_ERR_SOCKET',\r\n ];\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('terminated') ||\r\n msg.includes('econnreset')\r\n );\r\n}\r\n\r\n/**\r\n * Promisified sleep that respects 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 timeoutMs,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n shouldRetry,\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 // Set up per-attempt timeout controller\r\n const attemptController = new AbortController();\r\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n\r\n if (timeoutMs && timeoutMs > 0) {\r\n timeoutId = setTimeout(() => {\r\n attemptController.abort(new DOMException(`Request timed out after ${timeoutMs}ms`, 'TimeoutError'));\r\n }, timeoutMs);\r\n }\r\n\r\n const abortHandler = () => attemptController.abort(signal?.reason);\r\n signal?.addEventListener('abort', abortHandler, { once: true });\r\n\r\n try {\r\n const response = await fetch(url, {\r\n ...init,\r\n signal: attemptController.signal,\r\n });\r\n\r\n if (timeoutId) clearTimeout(timeoutId);\r\n signal?.removeEventListener('abort', abortHandler);\r\n\r\n const isStatusRetryable = retryOnStatus.includes(response.status);\r\n const customRetry = shouldRetry ? shouldRetry(response, attempt) : isStatusRetryable;\r\n\r\n if (customRetry && 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 (timeoutId) clearTimeout(timeoutId);\r\n signal?.removeEventListener('abort', abortHandler);\r\n\r\n // Do not retry intentional outer aborts\r\n if (signal?.aborted) {\r\n throw signal.reason ?? error;\r\n }\r\n\r\n const isTimeout = error instanceof Error && error.name === 'TimeoutError';\r\n const isNetworkDrop = isTransientNetworkError(error);\r\n const customRetry = shouldRetry ? shouldRetry(error, attempt) : (isNetworkDrop || isTimeout);\r\n\r\n if (customRetry && 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 shouldRetry,\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 const customRetry = shouldRetry\r\n ? shouldRetry(error, attempt)\r\n : (isStatusRetryable || isNetworkRetryable);\r\n\r\n if (!customRetry) {\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}","import { computeJitterDelay, sleep } from './engine.js';\r\nimport { RetryOptions } from './types.js';\r\n\r\n/**\r\n * Resilient wrapper for Server-Sent Events / LLM chunk streams.\r\n * If the streaming connection drops mid-generation, it retries and resumes.\r\n */\r\nexport async function* retryStream<T>(\r\n streamFactory: (attempt: number) => Promise<AsyncIterable<T>>,\r\n options: RetryOptions = {}\r\n): AsyncGenerator<T, void, unknown> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\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 stream = await streamFactory(attempt);\r\n for await (const chunk of stream) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n yield chunk;\r\n }\r\n return;\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 delay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n onRetry?.(error, attempt + 1, delay);\r\n await sleep(delay, signal);\r\n attempt++;\r\n }\r\n }\r\n}","import { CircuitBreakerOptions, CircuitState } from './types.js';\r\n\r\nexport class CircuitBreakerOpenError extends Error {\r\n constructor(message: string = 'Circuit breaker is OPEN. Fast-failing requests.') {\r\n super(message);\r\n this.name = 'CircuitBreakerOpenError';\r\n }\r\n}\r\n\r\n/**\r\n * Zero-dependency Circuit Breaker implementation to stop cascading failure\r\n * cascades when downstream LLM APIs experience total outages.\r\n */\r\nexport class CircuitBreaker {\r\n private state: CircuitState = 'CLOSED';\r\n private failureCount: number = 0;\r\n private lastFailureTime: number = 0;\r\n private readonly failureThreshold: number;\r\n private readonly resetTimeoutMs: number;\r\n private readonly onStateChange?: (from: CircuitState, to: CircuitState) => void;\r\n\r\n constructor(options: CircuitBreakerOptions = {}) {\r\n this.failureThreshold = options.failureThreshold ?? 5;\r\n this.resetTimeoutMs = options.resetTimeoutMs ?? 30000;\r\n this.onStateChange = options.onStateChange;\r\n }\r\n\r\n public getState(): CircuitState {\r\n if (this.state === 'OPEN') {\r\n const elapsed = Date.now() - this.lastFailureTime;\r\n if (elapsed >= this.resetTimeoutMs) {\r\n this.transitionTo('HALF_OPEN');\r\n }\r\n }\r\n return this.state;\r\n }\r\n\r\n public async execute<T>(fn: () => Promise<T>): Promise<T> {\r\n const currentState = this.getState();\r\n\r\n if (currentState === 'OPEN') {\r\n throw new CircuitBreakerOpenError();\r\n }\r\n\r\n try {\r\n const result = await fn();\r\n this.recordSuccess();\r\n return result;\r\n } catch (error) {\r\n this.recordFailure();\r\n throw error;\r\n }\r\n }\r\n\r\n private recordSuccess(): void {\r\n if (this.state === 'HALF_OPEN') {\r\n this.transitionTo('CLOSED');\r\n }\r\n this.failureCount = 0;\r\n }\r\n\r\n private recordFailure(): void {\r\n this.failureCount++;\r\n this.lastFailureTime = Date.now();\r\n\r\n if (this.state === 'HALF_OPEN' || this.failureCount >= this.failureThreshold) {\r\n this.transitionTo('OPEN');\r\n }\r\n }\r\n\r\n private transitionTo(nextState: CircuitState): void {\r\n const prevState = this.state;\r\n if (prevState !== nextState) {\r\n this.state = nextState;\r\n this.onStateChange?.(prevState, nextState);\r\n }\r\n }\r\n\r\n public reset(): void {\r\n this.state = 'CLOSED';\r\n this.failureCount = 0;\r\n this.lastFailureTime = 0;\r\n }\r\n}"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -67,6 +67,11 @@ function parseDurationToMs(value) {
|
|
|
67
67
|
}
|
|
68
68
|
function parseRateLimitDelay(headers) {
|
|
69
69
|
if (!headers) return null;
|
|
70
|
+
const retryAfterMs = getHeader(headers, "retry-after-ms");
|
|
71
|
+
if (retryAfterMs) {
|
|
72
|
+
const parsed = parseFloat(retryAfterMs);
|
|
73
|
+
if (!isNaN(parsed)) return Math.max(0, Math.floor(parsed));
|
|
74
|
+
}
|
|
70
75
|
const retryAfter = getHeader(headers, "retry-after");
|
|
71
76
|
if (retryAfter) {
|
|
72
77
|
if (/^\d+(\.\d+)?$/.test(retryAfter.trim())) {
|
|
@@ -78,15 +83,23 @@ function parseRateLimitDelay(headers) {
|
|
|
78
83
|
return Math.max(0, parsedDate - Date.now());
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
const candidateKeys = [
|
|
87
|
+
"x-ratelimit-reset-requests",
|
|
88
|
+
"x-ratelimit-reset-tokens",
|
|
89
|
+
"anthropic-ratelimit-requests-reset",
|
|
90
|
+
"anthropic-ratelimit-tokens-reset",
|
|
91
|
+
"x-ratelimit-reset-total"
|
|
92
|
+
];
|
|
93
|
+
for (const key of candidateKeys) {
|
|
94
|
+
const raw = getHeader(headers, key);
|
|
95
|
+
if (raw) {
|
|
96
|
+
const ms = parseDurationToMs(raw);
|
|
97
|
+
if (ms !== null) return ms;
|
|
98
|
+
const epochSec = parseFloat(raw);
|
|
99
|
+
if (!isNaN(epochSec) && epochSec > 1e9) {
|
|
100
|
+
return Math.max(0, Math.floor(epochSec * 1e3 - Date.now()));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
90
103
|
}
|
|
91
104
|
return null;
|
|
92
105
|
}
|
|
@@ -95,9 +108,17 @@ function isTransientNetworkError(error) {
|
|
|
95
108
|
const msg = error.message.toLowerCase();
|
|
96
109
|
const code = error.code;
|
|
97
110
|
if (error.name === "TypeError" && msg.includes("fetch failed")) return true;
|
|
98
|
-
const transientCodes = [
|
|
111
|
+
const transientCodes = [
|
|
112
|
+
"ECONNRESET",
|
|
113
|
+
"ETIMEDOUT",
|
|
114
|
+
"ENOTFOUND",
|
|
115
|
+
"EAI_AGAIN",
|
|
116
|
+
"ECONNREFUSED",
|
|
117
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
118
|
+
"UND_ERR_SOCKET"
|
|
119
|
+
];
|
|
99
120
|
if (code && transientCodes.includes(code)) return true;
|
|
100
|
-
return msg.includes("network error") || msg.includes("socket hang up") || msg.includes("
|
|
121
|
+
return msg.includes("network error") || msg.includes("socket hang up") || msg.includes("terminated") || msg.includes("econnreset");
|
|
101
122
|
}
|
|
102
123
|
function sleep(ms, signal) {
|
|
103
124
|
return new Promise((resolve, reject) => {
|
|
@@ -122,7 +143,9 @@ async function retryFetch(url, init, options = {}) {
|
|
|
122
143
|
retries = 3,
|
|
123
144
|
baseDelayMs = 500,
|
|
124
145
|
maxDelayMs = 3e4,
|
|
146
|
+
timeoutMs,
|
|
125
147
|
retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
|
|
148
|
+
shouldRetry,
|
|
126
149
|
onRetry,
|
|
127
150
|
signal
|
|
128
151
|
} = options;
|
|
@@ -131,12 +154,25 @@ async function retryFetch(url, init, options = {}) {
|
|
|
131
154
|
if (signal?.aborted) {
|
|
132
155
|
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
133
156
|
}
|
|
157
|
+
const attemptController = new AbortController();
|
|
158
|
+
let timeoutId;
|
|
159
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
160
|
+
timeoutId = setTimeout(() => {
|
|
161
|
+
attemptController.abort(new DOMException(`Request timed out after ${timeoutMs}ms`, "TimeoutError"));
|
|
162
|
+
}, timeoutMs);
|
|
163
|
+
}
|
|
164
|
+
const abortHandler = () => attemptController.abort(signal?.reason);
|
|
165
|
+
signal?.addEventListener("abort", abortHandler, { once: true });
|
|
134
166
|
try {
|
|
135
167
|
const response = await fetch(url, {
|
|
136
168
|
...init,
|
|
137
|
-
signal: signal
|
|
169
|
+
signal: attemptController.signal
|
|
138
170
|
});
|
|
139
|
-
if (
|
|
171
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
172
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
173
|
+
const isStatusRetryable = retryOnStatus.includes(response.status);
|
|
174
|
+
const customRetry = shouldRetry ? shouldRetry(response, attempt) : isStatusRetryable;
|
|
175
|
+
if (customRetry && attempt < retries) {
|
|
140
176
|
const headerDelay = parseRateLimitDelay(response.headers);
|
|
141
177
|
const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
142
178
|
const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);
|
|
@@ -147,10 +183,15 @@ async function retryFetch(url, init, options = {}) {
|
|
|
147
183
|
}
|
|
148
184
|
return response;
|
|
149
185
|
} catch (error) {
|
|
150
|
-
if (
|
|
151
|
-
|
|
186
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
187
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
188
|
+
if (signal?.aborted) {
|
|
189
|
+
throw signal.reason ?? error;
|
|
152
190
|
}
|
|
153
|
-
|
|
191
|
+
const isTimeout = error instanceof Error && error.name === "TimeoutError";
|
|
192
|
+
const isNetworkDrop = isTransientNetworkError(error);
|
|
193
|
+
const customRetry = shouldRetry ? shouldRetry(error, attempt) : isNetworkDrop || isTimeout;
|
|
194
|
+
if (customRetry && attempt < retries) {
|
|
154
195
|
const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
155
196
|
onRetry?.(error, attempt + 1, chosenDelay);
|
|
156
197
|
await sleep(chosenDelay, signal);
|
|
@@ -169,6 +210,7 @@ async function retry(fn, options = {}) {
|
|
|
169
210
|
baseDelayMs = 500,
|
|
170
211
|
maxDelayMs = 3e4,
|
|
171
212
|
retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
|
|
213
|
+
shouldRetry,
|
|
172
214
|
onRetry,
|
|
173
215
|
signal
|
|
174
216
|
} = options;
|
|
@@ -191,7 +233,8 @@ async function retry(fn, options = {}) {
|
|
|
191
233
|
const headers = err?.headers ?? err?.response?.headers;
|
|
192
234
|
const isStatusRetryable = statusCode !== void 0 && retryOnStatus.includes(statusCode);
|
|
193
235
|
const isNetworkRetryable = isTransientNetworkError(error);
|
|
194
|
-
|
|
236
|
+
const customRetry = shouldRetry ? shouldRetry(error, attempt) : isStatusRetryable || isNetworkRetryable;
|
|
237
|
+
if (!customRetry) {
|
|
195
238
|
throw error;
|
|
196
239
|
}
|
|
197
240
|
let chosenDelay;
|
|
@@ -207,7 +250,116 @@ async function retry(fn, options = {}) {
|
|
|
207
250
|
}
|
|
208
251
|
}
|
|
209
252
|
}
|
|
253
|
+
|
|
254
|
+
// src/stream.ts
|
|
255
|
+
async function* retryStream(streamFactory, options = {}) {
|
|
256
|
+
const {
|
|
257
|
+
retries = 3,
|
|
258
|
+
baseDelayMs = 500,
|
|
259
|
+
maxDelayMs = 3e4,
|
|
260
|
+
onRetry,
|
|
261
|
+
signal
|
|
262
|
+
} = options;
|
|
263
|
+
let attempt = 0;
|
|
264
|
+
while (true) {
|
|
265
|
+
if (signal?.aborted) {
|
|
266
|
+
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const stream = await streamFactory(attempt);
|
|
270
|
+
for await (const chunk of stream) {
|
|
271
|
+
if (signal?.aborted) {
|
|
272
|
+
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
273
|
+
}
|
|
274
|
+
yield chunk;
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (signal?.aborted || error instanceof Error && error.name === "AbortError") {
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
if (attempt >= retries) {
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
const delay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
285
|
+
onRetry?.(error, attempt + 1, delay);
|
|
286
|
+
await sleep(delay, signal);
|
|
287
|
+
attempt++;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/circuit-breaker.ts
|
|
293
|
+
var CircuitBreakerOpenError = class extends Error {
|
|
294
|
+
constructor(message = "Circuit breaker is OPEN. Fast-failing requests.") {
|
|
295
|
+
super(message);
|
|
296
|
+
this.name = "CircuitBreakerOpenError";
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
var CircuitBreaker = class {
|
|
300
|
+
state = "CLOSED";
|
|
301
|
+
failureCount = 0;
|
|
302
|
+
lastFailureTime = 0;
|
|
303
|
+
failureThreshold;
|
|
304
|
+
resetTimeoutMs;
|
|
305
|
+
onStateChange;
|
|
306
|
+
constructor(options = {}) {
|
|
307
|
+
this.failureThreshold = options.failureThreshold ?? 5;
|
|
308
|
+
this.resetTimeoutMs = options.resetTimeoutMs ?? 3e4;
|
|
309
|
+
this.onStateChange = options.onStateChange;
|
|
310
|
+
}
|
|
311
|
+
getState() {
|
|
312
|
+
if (this.state === "OPEN") {
|
|
313
|
+
const elapsed = Date.now() - this.lastFailureTime;
|
|
314
|
+
if (elapsed >= this.resetTimeoutMs) {
|
|
315
|
+
this.transitionTo("HALF_OPEN");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return this.state;
|
|
319
|
+
}
|
|
320
|
+
async execute(fn) {
|
|
321
|
+
const currentState = this.getState();
|
|
322
|
+
if (currentState === "OPEN") {
|
|
323
|
+
throw new CircuitBreakerOpenError();
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const result = await fn();
|
|
327
|
+
this.recordSuccess();
|
|
328
|
+
return result;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
this.recordFailure();
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
recordSuccess() {
|
|
335
|
+
if (this.state === "HALF_OPEN") {
|
|
336
|
+
this.transitionTo("CLOSED");
|
|
337
|
+
}
|
|
338
|
+
this.failureCount = 0;
|
|
339
|
+
}
|
|
340
|
+
recordFailure() {
|
|
341
|
+
this.failureCount++;
|
|
342
|
+
this.lastFailureTime = Date.now();
|
|
343
|
+
if (this.state === "HALF_OPEN" || this.failureCount >= this.failureThreshold) {
|
|
344
|
+
this.transitionTo("OPEN");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
transitionTo(nextState) {
|
|
348
|
+
const prevState = this.state;
|
|
349
|
+
if (prevState !== nextState) {
|
|
350
|
+
this.state = nextState;
|
|
351
|
+
this.onStateChange?.(prevState, nextState);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
reset() {
|
|
355
|
+
this.state = "CLOSED";
|
|
356
|
+
this.failureCount = 0;
|
|
357
|
+
this.lastFailureTime = 0;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
210
360
|
export {
|
|
361
|
+
CircuitBreaker,
|
|
362
|
+
CircuitBreakerOpenError,
|
|
211
363
|
DEFAULT_RETRYABLE_STATUS_CODES,
|
|
212
364
|
computeJitterDelay,
|
|
213
365
|
isTransientNetworkError,
|
|
@@ -215,6 +367,7 @@ export {
|
|
|
215
367
|
parseRateLimitDelay,
|
|
216
368
|
retry,
|
|
217
369
|
retryFetch,
|
|
370
|
+
retryStream,
|
|
218
371
|
sleep
|
|
219
372
|
};
|
|
220
373
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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":[]}
|
|
1
|
+
{"version":3,"sources":["../src/engine.ts","../src/fetch.ts","../src/wrap.ts","../src/stream.ts","../src/circuit-breaker.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 instances, 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 if (typeof (headers as Headers).get === 'function') {\r\n return (headers as Headers).get(target);\r\n }\r\n\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 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 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 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 * Exhaustively checks provider headers across:\r\n * - RFC 7231 / RFC 6585 standard `retry-after`\r\n * - OpenAI: `x-ratelimit-reset-requests`, `x-ratelimit-reset-tokens`\r\n * - Anthropic: `anthropic-ratelimit-requests-reset`, `anthropic-ratelimit-tokens-reset`\r\n * - Groq / DeepSeek / Mistral: `x-ratelimit-reset-total`, `retry-after-ms`\r\n */\r\nexport function parseRateLimitDelay(headers: HeaderSource): number | null {\r\n if (!headers) return null;\r\n\r\n // 1. Direct milliseconds header (Groq, custom proxies)\r\n const retryAfterMs = getHeader(headers, 'retry-after-ms');\r\n if (retryAfterMs) {\r\n const parsed = parseFloat(retryAfterMs);\r\n if (!isNaN(parsed)) return Math.max(0, Math.floor(parsed));\r\n }\r\n\r\n // 2. Standard Retry-After (seconds or RFC 1123 HTTP Date)\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 // 3. Provider-specific reset headers\r\n const candidateKeys = [\r\n 'x-ratelimit-reset-requests',\r\n 'x-ratelimit-reset-tokens',\r\n 'anthropic-ratelimit-requests-reset',\r\n 'anthropic-ratelimit-tokens-reset',\r\n 'x-ratelimit-reset-total',\r\n ];\r\n\r\n for (const key of candidateKeys) {\r\n const raw = getHeader(headers, key);\r\n if (raw) {\r\n const ms = parseDurationToMs(raw);\r\n if (ms !== null) return ms;\r\n\r\n // Handle epoch timestamp formats\r\n const epochSec = parseFloat(raw);\r\n if (!isNaN(epochSec) && epochSec > 1000000000) {\r\n return Math.max(0, Math.floor(epochSec * 1000 - Date.now()));\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Detects transient network drop errors across Node, Bun, Deno, and browsers.\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 = [\r\n 'ECONNRESET',\r\n 'ETIMEDOUT',\r\n 'ENOTFOUND',\r\n 'EAI_AGAIN',\r\n 'ECONNREFUSED',\r\n 'UND_ERR_CONNECT_TIMEOUT',\r\n 'UND_ERR_SOCKET',\r\n ];\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('terminated') ||\r\n msg.includes('econnreset')\r\n );\r\n}\r\n\r\n/**\r\n * Promisified sleep that respects 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 timeoutMs,\r\n retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,\r\n shouldRetry,\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 // Set up per-attempt timeout controller\r\n const attemptController = new AbortController();\r\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\r\n\r\n if (timeoutMs && timeoutMs > 0) {\r\n timeoutId = setTimeout(() => {\r\n attemptController.abort(new DOMException(`Request timed out after ${timeoutMs}ms`, 'TimeoutError'));\r\n }, timeoutMs);\r\n }\r\n\r\n const abortHandler = () => attemptController.abort(signal?.reason);\r\n signal?.addEventListener('abort', abortHandler, { once: true });\r\n\r\n try {\r\n const response = await fetch(url, {\r\n ...init,\r\n signal: attemptController.signal,\r\n });\r\n\r\n if (timeoutId) clearTimeout(timeoutId);\r\n signal?.removeEventListener('abort', abortHandler);\r\n\r\n const isStatusRetryable = retryOnStatus.includes(response.status);\r\n const customRetry = shouldRetry ? shouldRetry(response, attempt) : isStatusRetryable;\r\n\r\n if (customRetry && 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 (timeoutId) clearTimeout(timeoutId);\r\n signal?.removeEventListener('abort', abortHandler);\r\n\r\n // Do not retry intentional outer aborts\r\n if (signal?.aborted) {\r\n throw signal.reason ?? error;\r\n }\r\n\r\n const isTimeout = error instanceof Error && error.name === 'TimeoutError';\r\n const isNetworkDrop = isTransientNetworkError(error);\r\n const customRetry = shouldRetry ? shouldRetry(error, attempt) : (isNetworkDrop || isTimeout);\r\n\r\n if (customRetry && 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 shouldRetry,\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 const customRetry = shouldRetry\r\n ? shouldRetry(error, attempt)\r\n : (isStatusRetryable || isNetworkRetryable);\r\n\r\n if (!customRetry) {\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}","import { computeJitterDelay, sleep } from './engine.js';\r\nimport { RetryOptions } from './types.js';\r\n\r\n/**\r\n * Resilient wrapper for Server-Sent Events / LLM chunk streams.\r\n * If the streaming connection drops mid-generation, it retries and resumes.\r\n */\r\nexport async function* retryStream<T>(\r\n streamFactory: (attempt: number) => Promise<AsyncIterable<T>>,\r\n options: RetryOptions = {}\r\n): AsyncGenerator<T, void, unknown> {\r\n const {\r\n retries = 3,\r\n baseDelayMs = 500,\r\n maxDelayMs = 30000,\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 stream = await streamFactory(attempt);\r\n for await (const chunk of stream) {\r\n if (signal?.aborted) {\r\n throw signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');\r\n }\r\n yield chunk;\r\n }\r\n return;\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 delay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);\r\n onRetry?.(error, attempt + 1, delay);\r\n await sleep(delay, signal);\r\n attempt++;\r\n }\r\n }\r\n}","import { CircuitBreakerOptions, CircuitState } from './types.js';\r\n\r\nexport class CircuitBreakerOpenError extends Error {\r\n constructor(message: string = 'Circuit breaker is OPEN. Fast-failing requests.') {\r\n super(message);\r\n this.name = 'CircuitBreakerOpenError';\r\n }\r\n}\r\n\r\n/**\r\n * Zero-dependency Circuit Breaker implementation to stop cascading failure\r\n * cascades when downstream LLM APIs experience total outages.\r\n */\r\nexport class CircuitBreaker {\r\n private state: CircuitState = 'CLOSED';\r\n private failureCount: number = 0;\r\n private lastFailureTime: number = 0;\r\n private readonly failureThreshold: number;\r\n private readonly resetTimeoutMs: number;\r\n private readonly onStateChange?: (from: CircuitState, to: CircuitState) => void;\r\n\r\n constructor(options: CircuitBreakerOptions = {}) {\r\n this.failureThreshold = options.failureThreshold ?? 5;\r\n this.resetTimeoutMs = options.resetTimeoutMs ?? 30000;\r\n this.onStateChange = options.onStateChange;\r\n }\r\n\r\n public getState(): CircuitState {\r\n if (this.state === 'OPEN') {\r\n const elapsed = Date.now() - this.lastFailureTime;\r\n if (elapsed >= this.resetTimeoutMs) {\r\n this.transitionTo('HALF_OPEN');\r\n }\r\n }\r\n return this.state;\r\n }\r\n\r\n public async execute<T>(fn: () => Promise<T>): Promise<T> {\r\n const currentState = this.getState();\r\n\r\n if (currentState === 'OPEN') {\r\n throw new CircuitBreakerOpenError();\r\n }\r\n\r\n try {\r\n const result = await fn();\r\n this.recordSuccess();\r\n return result;\r\n } catch (error) {\r\n this.recordFailure();\r\n throw error;\r\n }\r\n }\r\n\r\n private recordSuccess(): void {\r\n if (this.state === 'HALF_OPEN') {\r\n this.transitionTo('CLOSED');\r\n }\r\n this.failureCount = 0;\r\n }\r\n\r\n private recordFailure(): void {\r\n this.failureCount++;\r\n this.lastFailureTime = Date.now();\r\n\r\n if (this.state === 'HALF_OPEN' || this.failureCount >= this.failureThreshold) {\r\n this.transitionTo('OPEN');\r\n }\r\n }\r\n\r\n private transitionTo(nextState: CircuitState): void {\r\n const prevState = this.state;\r\n if (prevState !== nextState) {\r\n this.state = nextState;\r\n this.onStateChange?.(prevState, nextState);\r\n }\r\n }\r\n\r\n public reset(): void {\r\n this.state = 'CLOSED';\r\n this.failureCount = 0;\r\n this.lastFailureTime = 0;\r\n }\r\n}"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rohan9024/retrykit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "The ultimate zero-dependency, header-aware retry engine and circuit breaker for LLM APIs (OpenAI, Anthropic, DeepSeek, Groq).",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -27,28 +27,27 @@
|
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|
|
29
29
|
"retry",
|
|
30
|
-
"fetch",
|
|
31
30
|
"llm",
|
|
32
31
|
"openai",
|
|
33
32
|
"anthropic",
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
33
|
+
"deepseek",
|
|
34
|
+
"groq",
|
|
35
|
+
"fetch",
|
|
36
|
+
"circuit-breaker",
|
|
37
|
+
"streaming",
|
|
37
38
|
"rate-limit",
|
|
38
|
-
"
|
|
39
|
+
"backoff",
|
|
40
|
+
"jitter"
|
|
39
41
|
],
|
|
40
|
-
"author": "",
|
|
42
|
+
"author": "rohan9024",
|
|
41
43
|
"license": "MIT",
|
|
42
44
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^22.
|
|
44
|
-
"tsup": "^8.
|
|
45
|
-
"typescript": "^5.
|
|
46
|
-
"vitest": "^2.1.
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"tsup": "^8.3.0",
|
|
47
|
+
"typescript": "^5.6.0",
|
|
48
|
+
"vitest": "^2.1.0"
|
|
47
49
|
},
|
|
48
50
|
"engines": {
|
|
49
51
|
"node": ">=18.0.0"
|
|
50
|
-
},
|
|
51
|
-
"directories": {
|
|
52
|
-
"test": "test"
|
|
53
52
|
}
|
|
54
|
-
}
|
|
53
|
+
}
|