@rohan9024/retrykit 0.0.0-stage → 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 +190 -3
- package/dist/index.d.mts +90 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.js +410 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +373 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +52 -5
package/README.md
CHANGED
|
@@ -1,3 +1,190 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
|
|
2
|
+
# @rohan9024/retrykit ⚡
|
|
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.
|
|
11
|
+
|
|
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.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 📦 Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @rohan9024/retrykit
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🚀 Quick Start
|
|
34
|
+
|
|
35
|
+
### 1. Drop-in Fetch Replacement (`retryFetch`)
|
|
36
|
+
|
|
37
|
+
Use `retryFetch` just like standard `fetch`, with automatic retries and rate-limit awareness.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { retryFetch } from '@rohan9024/retrykit';
|
|
41
|
+
|
|
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
|
+
}),
|
|
56
|
+
},
|
|
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
|
+
);
|
|
67
|
+
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Wrap Official SDKs (`retry`)
|
|
72
|
+
|
|
73
|
+
Use RetryKit with existing SDKs such as OpenAI, Anthropic, Mistral, or Google.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import OpenAI from 'openai';
|
|
77
|
+
import { retry } from '@rohan9024/retrykit';
|
|
78
|
+
|
|
79
|
+
const openai = new OpenAI();
|
|
80
|
+
|
|
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
|
+
}),
|
|
92
|
+
{
|
|
93
|
+
retries: 3,
|
|
94
|
+
baseDelayMs: 500,
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
```
|
|
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
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 🧠 Supported Provider Headers
|
|
142
|
+
|
|
143
|
+
RetryKit automatically extracts retry timing information from commonly used API rate-limit headers.
|
|
144
|
+
|
|
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) |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## ⚙️ Configuration Options
|
|
155
|
+
|
|
156
|
+
RetryKit provides configurable retry behavior.
|
|
157
|
+
|
|
158
|
+
| Option | Type | Default | Description |
|
|
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
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 📄 License
|
|
183
|
+
|
|
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
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
/** Per-request timeout in milliseconds. Default: undefined */
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
/** HTTP status codes that trigger a retry. Default: [408, 429, 500, 502, 503, 504] */
|
|
11
|
+
retryOnStatus?: number[];
|
|
12
|
+
/** Custom condition to decide whether to retry */
|
|
13
|
+
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
14
|
+
/** Optional callback fired before every retry sleep */
|
|
15
|
+
onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
|
|
16
|
+
/** AbortSignal for immediate cancellation */
|
|
17
|
+
signal?: AbortSignal;
|
|
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';
|
|
28
|
+
type HeaderSource = Headers | Record<string, string | string[] | undefined> | [string, string][] | undefined;
|
|
29
|
+
|
|
30
|
+
declare function retryFetch(url: string | URL | Request, init?: RequestInit, options?: RetryOptions): Promise<Response>;
|
|
31
|
+
|
|
32
|
+
declare function retry<T>(fn: (attempt: number) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
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
|
+
|
|
63
|
+
declare const DEFAULT_RETRYABLE_STATUS_CODES: number[];
|
|
64
|
+
/**
|
|
65
|
+
* Calculates exponential backoff with full jitter:
|
|
66
|
+
* sleepMs = min(maxDelayMs, baseDelayMs * 2 ** attempt) * Math.random()
|
|
67
|
+
*/
|
|
68
|
+
declare function computeJitterDelay(attempt: number, baseDelayMs?: number, maxDelayMs?: number): number;
|
|
69
|
+
/**
|
|
70
|
+
* Parses duration strings returned by LLM APIs (e.g. "600ms", "10s", "2m", "1m30s", "1.5").
|
|
71
|
+
*/
|
|
72
|
+
declare function parseDurationToMs(value: string): number | null;
|
|
73
|
+
/**
|
|
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`
|
|
79
|
+
*/
|
|
80
|
+
declare function parseRateLimitDelay(headers: HeaderSource): number | null;
|
|
81
|
+
/**
|
|
82
|
+
* Detects transient network drop errors across Node, Bun, Deno, and browsers.
|
|
83
|
+
*/
|
|
84
|
+
declare function isTransientNetworkError(error: unknown): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Promisified sleep that respects AbortSignal cancellation.
|
|
87
|
+
*/
|
|
88
|
+
declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
89
|
+
|
|
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
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
/** Per-request timeout in milliseconds. Default: undefined */
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
/** HTTP status codes that trigger a retry. Default: [408, 429, 500, 502, 503, 504] */
|
|
11
|
+
retryOnStatus?: number[];
|
|
12
|
+
/** Custom condition to decide whether to retry */
|
|
13
|
+
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
14
|
+
/** Optional callback fired before every retry sleep */
|
|
15
|
+
onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
|
|
16
|
+
/** AbortSignal for immediate cancellation */
|
|
17
|
+
signal?: AbortSignal;
|
|
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';
|
|
28
|
+
type HeaderSource = Headers | Record<string, string | string[] | undefined> | [string, string][] | undefined;
|
|
29
|
+
|
|
30
|
+
declare function retryFetch(url: string | URL | Request, init?: RequestInit, options?: RetryOptions): Promise<Response>;
|
|
31
|
+
|
|
32
|
+
declare function retry<T>(fn: (attempt: number) => Promise<T>, options?: RetryOptions): Promise<T>;
|
|
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
|
+
|
|
63
|
+
declare const DEFAULT_RETRYABLE_STATUS_CODES: number[];
|
|
64
|
+
/**
|
|
65
|
+
* Calculates exponential backoff with full jitter:
|
|
66
|
+
* sleepMs = min(maxDelayMs, baseDelayMs * 2 ** attempt) * Math.random()
|
|
67
|
+
*/
|
|
68
|
+
declare function computeJitterDelay(attempt: number, baseDelayMs?: number, maxDelayMs?: number): number;
|
|
69
|
+
/**
|
|
70
|
+
* Parses duration strings returned by LLM APIs (e.g. "600ms", "10s", "2m", "1m30s", "1.5").
|
|
71
|
+
*/
|
|
72
|
+
declare function parseDurationToMs(value: string): number | null;
|
|
73
|
+
/**
|
|
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`
|
|
79
|
+
*/
|
|
80
|
+
declare function parseRateLimitDelay(headers: HeaderSource): number | null;
|
|
81
|
+
/**
|
|
82
|
+
* Detects transient network drop errors across Node, Bun, Deno, and browsers.
|
|
83
|
+
*/
|
|
84
|
+
declare function isTransientNetworkError(error: unknown): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Promisified sleep that respects AbortSignal cancellation.
|
|
87
|
+
*/
|
|
88
|
+
declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
|
|
89
|
+
|
|
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
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
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
|
+
CircuitBreaker: () => CircuitBreaker,
|
|
24
|
+
CircuitBreakerOpenError: () => CircuitBreakerOpenError,
|
|
25
|
+
DEFAULT_RETRYABLE_STATUS_CODES: () => DEFAULT_RETRYABLE_STATUS_CODES,
|
|
26
|
+
computeJitterDelay: () => computeJitterDelay,
|
|
27
|
+
isTransientNetworkError: () => isTransientNetworkError,
|
|
28
|
+
parseDurationToMs: () => parseDurationToMs,
|
|
29
|
+
parseRateLimitDelay: () => parseRateLimitDelay,
|
|
30
|
+
retry: () => retry,
|
|
31
|
+
retryFetch: () => retryFetch,
|
|
32
|
+
retryStream: () => retryStream,
|
|
33
|
+
sleep: () => sleep
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/engine.ts
|
|
38
|
+
var DEFAULT_RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504];
|
|
39
|
+
function computeJitterDelay(attempt, baseDelayMs = 500, maxDelayMs = 3e4) {
|
|
40
|
+
const exponential = baseDelayMs * Math.pow(2, attempt);
|
|
41
|
+
const ceiling = Math.min(maxDelayMs, exponential);
|
|
42
|
+
return Math.floor(ceiling * Math.random());
|
|
43
|
+
}
|
|
44
|
+
function getHeader(headers, name) {
|
|
45
|
+
if (!headers) return null;
|
|
46
|
+
const target = name.toLowerCase();
|
|
47
|
+
if (typeof headers.get === "function") {
|
|
48
|
+
return headers.get(target);
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(headers)) {
|
|
51
|
+
for (const [key, value] of headers) {
|
|
52
|
+
if (key.toLowerCase() === target) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (typeof headers === "object") {
|
|
59
|
+
for (const key of Object.keys(headers)) {
|
|
60
|
+
if (key.toLowerCase() === target) {
|
|
61
|
+
const val = headers[key];
|
|
62
|
+
if (Array.isArray(val)) return val[0] ?? null;
|
|
63
|
+
return typeof val === "string" ? val : String(val);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function parseDurationToMs(value) {
|
|
70
|
+
const trimmed = value.trim();
|
|
71
|
+
if (!trimmed) return null;
|
|
72
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
73
|
+
const sec = parseFloat(trimmed);
|
|
74
|
+
return isNaN(sec) ? null : Math.max(0, Math.floor(sec * 1e3));
|
|
75
|
+
}
|
|
76
|
+
const regex = /(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)/gi;
|
|
77
|
+
let match;
|
|
78
|
+
let totalMs = 0;
|
|
79
|
+
let matched = false;
|
|
80
|
+
while ((match = regex.exec(trimmed)) !== null) {
|
|
81
|
+
matched = true;
|
|
82
|
+
const amount = parseFloat(match[1]);
|
|
83
|
+
const unit = match[2].toLowerCase();
|
|
84
|
+
switch (unit) {
|
|
85
|
+
case "ms":
|
|
86
|
+
totalMs += amount;
|
|
87
|
+
break;
|
|
88
|
+
case "s":
|
|
89
|
+
totalMs += amount * 1e3;
|
|
90
|
+
break;
|
|
91
|
+
case "m":
|
|
92
|
+
totalMs += amount * 60 * 1e3;
|
|
93
|
+
break;
|
|
94
|
+
case "h":
|
|
95
|
+
totalMs += amount * 60 * 60 * 1e3;
|
|
96
|
+
break;
|
|
97
|
+
case "d":
|
|
98
|
+
totalMs += amount * 24 * 60 * 60 * 1e3;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return matched ? Math.max(0, Math.floor(totalMs)) : null;
|
|
103
|
+
}
|
|
104
|
+
function parseRateLimitDelay(headers) {
|
|
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
|
+
}
|
|
111
|
+
const retryAfter = getHeader(headers, "retry-after");
|
|
112
|
+
if (retryAfter) {
|
|
113
|
+
if (/^\d+(\.\d+)?$/.test(retryAfter.trim())) {
|
|
114
|
+
const sec = parseFloat(retryAfter.trim());
|
|
115
|
+
if (!isNaN(sec)) return Math.max(0, Math.floor(sec * 1e3));
|
|
116
|
+
}
|
|
117
|
+
const parsedDate = Date.parse(retryAfter);
|
|
118
|
+
if (!isNaN(parsedDate)) {
|
|
119
|
+
return Math.max(0, parsedDate - Date.now());
|
|
120
|
+
}
|
|
121
|
+
}
|
|
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
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function isTransientNetworkError(error) {
|
|
143
|
+
if (!(error instanceof Error)) return false;
|
|
144
|
+
const msg = error.message.toLowerCase();
|
|
145
|
+
const code = error.code;
|
|
146
|
+
if (error.name === "TypeError" && msg.includes("fetch failed")) return true;
|
|
147
|
+
const transientCodes = [
|
|
148
|
+
"ECONNRESET",
|
|
149
|
+
"ETIMEDOUT",
|
|
150
|
+
"ENOTFOUND",
|
|
151
|
+
"EAI_AGAIN",
|
|
152
|
+
"ECONNREFUSED",
|
|
153
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
154
|
+
"UND_ERR_SOCKET"
|
|
155
|
+
];
|
|
156
|
+
if (code && transientCodes.includes(code)) return true;
|
|
157
|
+
return msg.includes("network error") || msg.includes("socket hang up") || msg.includes("terminated") || msg.includes("econnreset");
|
|
158
|
+
}
|
|
159
|
+
function sleep(ms, signal) {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
if (signal?.aborted) {
|
|
162
|
+
return reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
163
|
+
}
|
|
164
|
+
const timer = setTimeout(() => {
|
|
165
|
+
signal?.removeEventListener("abort", onAbort);
|
|
166
|
+
resolve();
|
|
167
|
+
}, ms);
|
|
168
|
+
function onAbort() {
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
reject(signal?.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
171
|
+
}
|
|
172
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/fetch.ts
|
|
177
|
+
async function retryFetch(url, init, options = {}) {
|
|
178
|
+
const {
|
|
179
|
+
retries = 3,
|
|
180
|
+
baseDelayMs = 500,
|
|
181
|
+
maxDelayMs = 3e4,
|
|
182
|
+
timeoutMs,
|
|
183
|
+
retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
|
|
184
|
+
shouldRetry,
|
|
185
|
+
onRetry,
|
|
186
|
+
signal
|
|
187
|
+
} = options;
|
|
188
|
+
let attempt = 0;
|
|
189
|
+
while (true) {
|
|
190
|
+
if (signal?.aborted) {
|
|
191
|
+
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
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 });
|
|
202
|
+
try {
|
|
203
|
+
const response = await fetch(url, {
|
|
204
|
+
...init,
|
|
205
|
+
signal: attemptController.signal
|
|
206
|
+
});
|
|
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) {
|
|
212
|
+
const headerDelay = parseRateLimitDelay(response.headers);
|
|
213
|
+
const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
214
|
+
const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);
|
|
215
|
+
onRetry?.(response, attempt + 1, chosenDelay);
|
|
216
|
+
await sleep(chosenDelay, signal);
|
|
217
|
+
attempt++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
return response;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
223
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
224
|
+
if (signal?.aborted) {
|
|
225
|
+
throw signal.reason ?? error;
|
|
226
|
+
}
|
|
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) {
|
|
231
|
+
const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
232
|
+
onRetry?.(error, attempt + 1, chosenDelay);
|
|
233
|
+
await sleep(chosenDelay, signal);
|
|
234
|
+
attempt++;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/wrap.ts
|
|
243
|
+
async function retry(fn, options = {}) {
|
|
244
|
+
const {
|
|
245
|
+
retries = 3,
|
|
246
|
+
baseDelayMs = 500,
|
|
247
|
+
maxDelayMs = 3e4,
|
|
248
|
+
retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
|
|
249
|
+
shouldRetry,
|
|
250
|
+
onRetry,
|
|
251
|
+
signal
|
|
252
|
+
} = options;
|
|
253
|
+
let attempt = 0;
|
|
254
|
+
while (true) {
|
|
255
|
+
if (signal?.aborted) {
|
|
256
|
+
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
return await fn(attempt);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (signal?.aborted || error instanceof Error && error.name === "AbortError") {
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
if (attempt >= retries) {
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
const err = error;
|
|
268
|
+
const statusCode = err?.status ?? err?.statusCode ?? err?.response?.status;
|
|
269
|
+
const headers = err?.headers ?? err?.response?.headers;
|
|
270
|
+
const isStatusRetryable = statusCode !== void 0 && retryOnStatus.includes(statusCode);
|
|
271
|
+
const isNetworkRetryable = isTransientNetworkError(error);
|
|
272
|
+
const customRetry = shouldRetry ? shouldRetry(error, attempt) : isStatusRetryable || isNetworkRetryable;
|
|
273
|
+
if (!customRetry) {
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
let chosenDelay;
|
|
277
|
+
const headerDelay = headers ? parseRateLimitDelay(headers) : null;
|
|
278
|
+
if (headerDelay !== null) {
|
|
279
|
+
chosenDelay = Math.min(maxDelayMs, headerDelay);
|
|
280
|
+
} else {
|
|
281
|
+
chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
282
|
+
}
|
|
283
|
+
onRetry?.(error, attempt + 1, chosenDelay);
|
|
284
|
+
await sleep(chosenDelay, signal);
|
|
285
|
+
attempt++;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
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
|
+
};
|
|
396
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
397
|
+
0 && (module.exports = {
|
|
398
|
+
CircuitBreaker,
|
|
399
|
+
CircuitBreakerOpenError,
|
|
400
|
+
DEFAULT_RETRYABLE_STATUS_CODES,
|
|
401
|
+
computeJitterDelay,
|
|
402
|
+
isTransientNetworkError,
|
|
403
|
+
parseDurationToMs,
|
|
404
|
+
parseRateLimitDelay,
|
|
405
|
+
retry,
|
|
406
|
+
retryFetch,
|
|
407
|
+
retryStream,
|
|
408
|
+
sleep
|
|
409
|
+
});
|
|
410
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
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 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
|
+
}
|
|
75
|
+
const retryAfter = getHeader(headers, "retry-after");
|
|
76
|
+
if (retryAfter) {
|
|
77
|
+
if (/^\d+(\.\d+)?$/.test(retryAfter.trim())) {
|
|
78
|
+
const sec = parseFloat(retryAfter.trim());
|
|
79
|
+
if (!isNaN(sec)) return Math.max(0, Math.floor(sec * 1e3));
|
|
80
|
+
}
|
|
81
|
+
const parsedDate = Date.parse(retryAfter);
|
|
82
|
+
if (!isNaN(parsedDate)) {
|
|
83
|
+
return Math.max(0, parsedDate - Date.now());
|
|
84
|
+
}
|
|
85
|
+
}
|
|
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
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function isTransientNetworkError(error) {
|
|
107
|
+
if (!(error instanceof Error)) return false;
|
|
108
|
+
const msg = error.message.toLowerCase();
|
|
109
|
+
const code = error.code;
|
|
110
|
+
if (error.name === "TypeError" && msg.includes("fetch failed")) return true;
|
|
111
|
+
const transientCodes = [
|
|
112
|
+
"ECONNRESET",
|
|
113
|
+
"ETIMEDOUT",
|
|
114
|
+
"ENOTFOUND",
|
|
115
|
+
"EAI_AGAIN",
|
|
116
|
+
"ECONNREFUSED",
|
|
117
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
118
|
+
"UND_ERR_SOCKET"
|
|
119
|
+
];
|
|
120
|
+
if (code && transientCodes.includes(code)) return true;
|
|
121
|
+
return msg.includes("network error") || msg.includes("socket hang up") || msg.includes("terminated") || msg.includes("econnreset");
|
|
122
|
+
}
|
|
123
|
+
function sleep(ms, signal) {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
if (signal?.aborted) {
|
|
126
|
+
return reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
127
|
+
}
|
|
128
|
+
const timer = setTimeout(() => {
|
|
129
|
+
signal?.removeEventListener("abort", onAbort);
|
|
130
|
+
resolve();
|
|
131
|
+
}, ms);
|
|
132
|
+
function onAbort() {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
reject(signal?.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
135
|
+
}
|
|
136
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/fetch.ts
|
|
141
|
+
async function retryFetch(url, init, options = {}) {
|
|
142
|
+
const {
|
|
143
|
+
retries = 3,
|
|
144
|
+
baseDelayMs = 500,
|
|
145
|
+
maxDelayMs = 3e4,
|
|
146
|
+
timeoutMs,
|
|
147
|
+
retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
|
|
148
|
+
shouldRetry,
|
|
149
|
+
onRetry,
|
|
150
|
+
signal
|
|
151
|
+
} = options;
|
|
152
|
+
let attempt = 0;
|
|
153
|
+
while (true) {
|
|
154
|
+
if (signal?.aborted) {
|
|
155
|
+
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
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 });
|
|
166
|
+
try {
|
|
167
|
+
const response = await fetch(url, {
|
|
168
|
+
...init,
|
|
169
|
+
signal: attemptController.signal
|
|
170
|
+
});
|
|
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) {
|
|
176
|
+
const headerDelay = parseRateLimitDelay(response.headers);
|
|
177
|
+
const backoffDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
178
|
+
const chosenDelay = Math.min(maxDelayMs, headerDelay ?? backoffDelay);
|
|
179
|
+
onRetry?.(response, attempt + 1, chosenDelay);
|
|
180
|
+
await sleep(chosenDelay, signal);
|
|
181
|
+
attempt++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
return response;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
187
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
188
|
+
if (signal?.aborted) {
|
|
189
|
+
throw signal.reason ?? error;
|
|
190
|
+
}
|
|
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) {
|
|
195
|
+
const chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
196
|
+
onRetry?.(error, attempt + 1, chosenDelay);
|
|
197
|
+
await sleep(chosenDelay, signal);
|
|
198
|
+
attempt++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/wrap.ts
|
|
207
|
+
async function retry(fn, options = {}) {
|
|
208
|
+
const {
|
|
209
|
+
retries = 3,
|
|
210
|
+
baseDelayMs = 500,
|
|
211
|
+
maxDelayMs = 3e4,
|
|
212
|
+
retryOnStatus = DEFAULT_RETRYABLE_STATUS_CODES,
|
|
213
|
+
shouldRetry,
|
|
214
|
+
onRetry,
|
|
215
|
+
signal
|
|
216
|
+
} = options;
|
|
217
|
+
let attempt = 0;
|
|
218
|
+
while (true) {
|
|
219
|
+
if (signal?.aborted) {
|
|
220
|
+
throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
return await fn(attempt);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (signal?.aborted || error instanceof Error && error.name === "AbortError") {
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
if (attempt >= retries) {
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
const err = error;
|
|
232
|
+
const statusCode = err?.status ?? err?.statusCode ?? err?.response?.status;
|
|
233
|
+
const headers = err?.headers ?? err?.response?.headers;
|
|
234
|
+
const isStatusRetryable = statusCode !== void 0 && retryOnStatus.includes(statusCode);
|
|
235
|
+
const isNetworkRetryable = isTransientNetworkError(error);
|
|
236
|
+
const customRetry = shouldRetry ? shouldRetry(error, attempt) : isStatusRetryable || isNetworkRetryable;
|
|
237
|
+
if (!customRetry) {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
let chosenDelay;
|
|
241
|
+
const headerDelay = headers ? parseRateLimitDelay(headers) : null;
|
|
242
|
+
if (headerDelay !== null) {
|
|
243
|
+
chosenDelay = Math.min(maxDelayMs, headerDelay);
|
|
244
|
+
} else {
|
|
245
|
+
chosenDelay = computeJitterDelay(attempt, baseDelayMs, maxDelayMs);
|
|
246
|
+
}
|
|
247
|
+
onRetry?.(error, attempt + 1, chosenDelay);
|
|
248
|
+
await sleep(chosenDelay, signal);
|
|
249
|
+
attempt++;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
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
|
+
};
|
|
360
|
+
export {
|
|
361
|
+
CircuitBreaker,
|
|
362
|
+
CircuitBreakerOpenError,
|
|
363
|
+
DEFAULT_RETRYABLE_STATUS_CODES,
|
|
364
|
+
computeJitterDelay,
|
|
365
|
+
isTransientNetworkError,
|
|
366
|
+
parseDurationToMs,
|
|
367
|
+
parseRateLimitDelay,
|
|
368
|
+
retry,
|
|
369
|
+
retryFetch,
|
|
370
|
+
retryStream,
|
|
371
|
+
sleep
|
|
372
|
+
};
|
|
373
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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,6 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@rohan9024/retrykit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
1
|
+
{
|
|
2
|
+
"name": "@rohan9024/retrykit",
|
|
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
|
+
"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
|
+
"llm",
|
|
31
|
+
"openai",
|
|
32
|
+
"anthropic",
|
|
33
|
+
"deepseek",
|
|
34
|
+
"groq",
|
|
35
|
+
"fetch",
|
|
36
|
+
"circuit-breaker",
|
|
37
|
+
"streaming",
|
|
38
|
+
"rate-limit",
|
|
39
|
+
"backoff",
|
|
40
|
+
"jitter"
|
|
41
|
+
],
|
|
42
|
+
"author": "rohan9024",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.0.0",
|
|
46
|
+
"tsup": "^8.3.0",
|
|
47
|
+
"typescript": "^5.6.0",
|
|
48
|
+
"vitest": "^2.1.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18.0.0"
|
|
52
|
+
}
|
|
6
53
|
}
|