@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 CHANGED
@@ -1,3 +1,190 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+
2
+ # @rohan9024/retrykit ⚡
3
+
4
+ > A zero-dependency, header-aware retry engine and circuit breaker built for reliable LLM API integrations.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/@rohan9024/retrykit.svg)](https://www.npmjs.com/package/@rohan9024/retrykit)
7
+ [![license](https://img.shields.io/npm/l/@rohan9024/retrykit.svg)](https://github.com/rohan9024/retrykit)
8
+ [![bundle size](https://img.shields.io/bundlephobia/min/@rohan9024/retrykit)](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>
@@ -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 };
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
  }