@boy-offi9-inc/reqkit 0.3.0 → 0.6.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.
@@ -0,0 +1,2507 @@
1
+ /*! @boy-offi9-inc/reqkit v0.6.0 — ESM bundle (generated by scripts/build-esm.js, do not edit) */
2
+ const __defs = {
3
+ "./HTTPError": function (module, exports, require) {
4
+ class HTTPError extends Error {
5
+ constructor(response, request) {
6
+ const statusText = response.statusText ? ` ${response.statusText}` : "";
7
+ super(`Request failed with status ${response.status}${statusText}: ${request.method} ${request.url}`);
8
+ this.name = "HTTPError";
9
+ this.status = response.status;
10
+ this.statusText = response.statusText || "";
11
+ this.response = response;
12
+ this.url = request.url;
13
+ this.method = request.method;
14
+ }
15
+ }
16
+
17
+ module.exports = { HTTPError };
18
+
19
+ },
20
+ "./_cache": function (module, exports, require) {
21
+ // Helpers for createFetch's response cache.
22
+
23
+ const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
24
+
25
+ function cacheKey(method, url, headers) {
26
+ // Every request header is part of the key, so responses fetched with one
27
+ // user's Authorization header can never be served to another.
28
+ return `${method} ${url} ${JSON.stringify([...headers.entries()].sort())}`;
29
+ }
30
+
31
+ /**
32
+ * Reads a response (via a clone — the original stays usable) into a plain,
33
+ * storable entry. Returns null when the response must not be cached.
34
+ */
35
+ async function snapshotResponse(response, maxBodyBytes = DEFAULT_MAX_BODY_BYTES) {
36
+ if (response.status !== 200) return null;
37
+
38
+ const cacheControl = (response.headers.get("cache-control") || "").toLowerCase();
39
+ if (/\bno-store\b/.test(cacheControl)) return null;
40
+ if ((response.headers.get("vary") || "").trim() === "*") return null;
41
+
42
+ const declared = Number(response.headers.get("content-length"));
43
+ if (response.headers.has("content-length") && declared > maxBodyBytes) return null;
44
+
45
+ const copy = response.clone();
46
+ const buffer = await copy.arrayBuffer();
47
+ if (buffer.byteLength > maxBodyBytes) return null;
48
+
49
+ const headers = [];
50
+ response.headers.forEach((value, key) => {
51
+ // The body we stored is already decoded; replaying these would corrupt it.
52
+ if (key === "content-encoding" || key === "content-length" || key === "transfer-encoding") return;
53
+ headers.push([key, value]);
54
+ });
55
+
56
+ return {
57
+ status: response.status,
58
+ statusText: response.statusText,
59
+ headers,
60
+ body: new Uint8Array(buffer),
61
+ storedAt: Date.now(),
62
+ etag: response.headers.get("etag"),
63
+ lastModified: response.headers.get("last-modified"),
64
+ };
65
+ }
66
+
67
+ function entryToResponse(entry) {
68
+ return new Response(entry.body.slice(), {
69
+ status: entry.status,
70
+ statusText: entry.statusText,
71
+ headers: entry.headers,
72
+ });
73
+ }
74
+
75
+ module.exports = { cacheKey, snapshotResponse, entryToResponse, DEFAULT_MAX_BODY_BYTES };
76
+
77
+ },
78
+ "./_http": function (module, exports, require) {
79
+ /**
80
+ * Parses a Retry-After header value into milliseconds. Per HTTP spec it's
81
+ * either an integer number of seconds, or an HTTP-date.
82
+ */
83
+ function parseRetryAfter(headerValue) {
84
+ if (headerValue === null || headerValue === undefined) return null;
85
+ const value = String(headerValue).trim();
86
+ if (!value) return null;
87
+
88
+ // delta-seconds (spec says integer; tolerate "1.5" from sloppy servers)
89
+ if (/^\d+(\.\d+)?$/.test(value)) {
90
+ return Number(value) * 1000;
91
+ }
92
+
93
+ const dateMs = Date.parse(value);
94
+ if (!Number.isNaN(dateMs)) {
95
+ return Math.max(0, dateMs - Date.now());
96
+ }
97
+
98
+ return null;
99
+ }
100
+
101
+ // Reset headers come in two flavors: a delta in seconds ("30", the IETF
102
+ // RateLimit-Reset draft, Discord's *-Reset-After) or an absolute epoch
103
+ // (GitHub/Twitter style, seconds or ms). Anything under ~31 years in seconds
104
+ // can't be a real epoch, so treat it as a delta.
105
+ function parseResetMs(value) {
106
+ if (value === null || value === undefined || String(value).trim() === "") return null;
107
+ const n = Number(value);
108
+ if (Number.isNaN(n) || n < 0) return null;
109
+ if (n < 1e9) return n * 1000;
110
+ const epochMs = n > 4102444800 ? n : n * 1000;
111
+ return Math.max(0, epochMs - Date.now());
112
+ }
113
+
114
+ function readHeader(headers, ...names) {
115
+ for (const name of names) {
116
+ const v = headers.get(name);
117
+ if (v !== null && v !== undefined && v !== "") return v;
118
+ }
119
+ return null;
120
+ }
121
+
122
+ /**
123
+ * Reads a wait time from common rate-limit headers, trying Retry-After first,
124
+ * then falling back to X-RateLimit-Reset(-After) if present.
125
+ */
126
+ function getWaitMsFromHeaders(headers) {
127
+ if (!headers || typeof headers.get !== "function") return null;
128
+
129
+ const retryAfter = parseRetryAfter(headers.get("retry-after"));
130
+ if (retryAfter !== null) return retryAfter;
131
+
132
+ return parseResetMs(
133
+ readHeader(headers, "x-ratelimit-reset-after", "x-ratelimit-reset", "x-rate-limit-reset", "ratelimit-reset")
134
+ );
135
+ }
136
+
137
+ /**
138
+ * Everything useful a response says about rate limits:
139
+ * { remaining: number|null, resetMs: number|null, retryAfterMs: number|null }
140
+ * Understands X-RateLimit-*, X-Rate-Limit-*, RateLimit-* and Retry-After.
141
+ */
142
+ function getRateLimitInfo(headers) {
143
+ const info = { remaining: null, resetMs: null, retryAfterMs: null };
144
+ if (!headers || typeof headers.get !== "function") return info;
145
+
146
+ const remaining = readHeader(headers, "x-ratelimit-remaining", "x-rate-limit-remaining", "ratelimit-remaining");
147
+ if (remaining !== null && !Number.isNaN(Number(remaining))) info.remaining = Number(remaining);
148
+
149
+ info.resetMs = parseResetMs(
150
+ readHeader(headers, "x-ratelimit-reset-after", "x-ratelimit-reset", "x-rate-limit-reset", "ratelimit-reset")
151
+ );
152
+ info.retryAfterMs = parseRetryAfter(headers.get("retry-after"));
153
+ return info;
154
+ }
155
+
156
+ async function discardBody(response) {
157
+ // An unread body keeps the underlying connection busy; cancel it before
158
+ // we sit through a backoff and fire the next request.
159
+ try {
160
+ if (response.body && typeof response.body.cancel === "function" && !response.bodyUsed) {
161
+ await response.body.cancel();
162
+ }
163
+ } catch {
164
+ // locked / already consumed / not a web stream — safe to ignore
165
+ }
166
+ }
167
+
168
+ module.exports = { parseRetryAfter, parseResetMs, getWaitMsFromHeaders, getRateLimitInfo, discardBody };
169
+
170
+ },
171
+ "./_sleep": function (module, exports, require) {
172
+ class RetryAbortedError extends Error {
173
+ constructor(cause) {
174
+ super("Retry loop was aborted");
175
+ this.name = "RetryAbortedError";
176
+ if (cause !== undefined) this.cause = cause;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Abortable sleep shared by withRetry and retryAfterAware. Rejects with
182
+ * RetryAbortedError (carrying `cause`, usually the last failure) if the
183
+ * signal aborts before or during the wait.
184
+ */
185
+ function sleep(ms, signal, cause) {
186
+ return new Promise((resolve, reject) => {
187
+ if (signal?.aborted) {
188
+ reject(new RetryAbortedError(cause));
189
+ return;
190
+ }
191
+ const onAbort = () => {
192
+ clearTimeout(timer);
193
+ reject(new RetryAbortedError(cause));
194
+ };
195
+ const timer = setTimeout(() => {
196
+ signal?.removeEventListener("abort", onAbort);
197
+ resolve();
198
+ }, ms);
199
+ signal?.addEventListener("abort", onAbort, { once: true });
200
+ });
201
+ }
202
+
203
+ /** Like sleep(), but rejects with the signal's own reason (an AbortError by default). */
204
+ function abortableSleep(ms, signal) {
205
+ return new Promise((resolve, reject) => {
206
+ if (signal?.aborted) {
207
+ reject(signal.reason);
208
+ return;
209
+ }
210
+ const onAbort = () => {
211
+ clearTimeout(timer);
212
+ reject(signal.reason);
213
+ };
214
+ const timer = setTimeout(() => {
215
+ signal?.removeEventListener("abort", onAbort);
216
+ resolve();
217
+ }, ms);
218
+ signal?.addEventListener("abort", onAbort, { once: true });
219
+ });
220
+ }
221
+
222
+ module.exports = { RetryAbortedError, sleep, abortableSleep };
223
+
224
+ },
225
+ "./buildQuery": function (module, exports, require) {
226
+ function toParam(key, value) {
227
+ if (value instanceof Date) {
228
+ if (Number.isNaN(value.getTime())) {
229
+ throw new TypeError(`buildQuery: value for "${key}" is an invalid Date`);
230
+ }
231
+ return value.toISOString();
232
+ }
233
+ if (typeof value === "object" || typeof value === "function" || typeof value === "symbol") {
234
+ throw new TypeError(
235
+ `buildQuery: value for "${key}" is not a primitive, Date or array of those — flatten it or JSON.stringify it first`
236
+ );
237
+ }
238
+ return String(value);
239
+ }
240
+
241
+ /**
242
+ * Builds a query string from a params object. Skips null/undefined values,
243
+ * repeats the key for arrays (`?tag=a&tag=b`), serializes Dates as ISO 8601,
244
+ * and encodes everything. Nested objects throw a TypeError instead of
245
+ * silently becoming "[object Object]".
246
+ *
247
+ * Deliberately minimal — for full query-string parsing/nested-object support,
248
+ * reach for `query-string` (sindresorhus) instead. This just covers the
249
+ * common "build a query string from a flat params object" case with zero deps.
250
+ *
251
+ * @param {Record<string, unknown>} params
252
+ */
253
+ function buildQuery(params) {
254
+ if (!params) return "";
255
+
256
+ const pairs = [];
257
+ for (const [key, value] of Object.entries(params)) {
258
+ if (value === null || value === undefined) continue;
259
+
260
+ const values = Array.isArray(value) ? value : [value];
261
+ for (const v of values) {
262
+ if (v === null || v === undefined) continue;
263
+ pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(toParam(key, v))}`);
264
+ }
265
+ }
266
+
267
+ return pairs.join("&");
268
+ }
269
+
270
+ module.exports = { buildQuery };
271
+
272
+ },
273
+ "./createCircuitBreaker": function (module, exports, require) {
274
+ class CircuitOpenError extends Error {
275
+ constructor(retryAfterMs) {
276
+ super("Circuit is open — failing fast instead of calling a service that keeps failing");
277
+ this.name = "CircuitOpenError";
278
+ // Deliberately not an E-code: this is *not* a network error and must not be retried as one.
279
+ this.code = "CIRCUIT_OPEN";
280
+ this.retryAfterMs = retryAfterMs;
281
+ }
282
+ }
283
+
284
+ function defaultIsFailure(valueOrError, isError) {
285
+ if (isError) return !(valueOrError && valueOrError.name === "AbortError");
286
+ return Boolean(valueOrError) && typeof valueOrError.status === "number" && valueOrError.status >= 500;
287
+ }
288
+
289
+ /**
290
+ * Circuit breaker: after `failureThreshold` consecutive failures it "opens"
291
+ * and fails fast with CircuitOpenError for `resetTimeoutMs`, so a dying
292
+ * service isn't hammered (and your callers don't wait on it). Then it goes
293
+ * "half-open" and lets `halfOpenMax` probe through: success closes the
294
+ * circuit, failure re-opens it.
295
+ *
296
+ * By default a thrown error (except AbortError) or a result with status >= 500
297
+ * counts as a failure; override with `isFailure(valueOrError, isError)`.
298
+ *
299
+ * @param {object} [opts]
300
+ * @param {number} [opts.failureThreshold=5]
301
+ * @param {number} [opts.resetTimeoutMs=30000]
302
+ * @param {number} [opts.halfOpenMax=1]
303
+ * @param {(valueOrError: any, isError: boolean) => boolean} [opts.isFailure]
304
+ * @param {(state: string, prev: string) => void} [opts.onStateChange]
305
+ */
306
+ function createCircuitBreaker(opts = {}) {
307
+ const {
308
+ failureThreshold = 5,
309
+ resetTimeoutMs = 30000,
310
+ halfOpenMax = 1,
311
+ isFailure = defaultIsFailure,
312
+ onStateChange,
313
+ } = opts;
314
+
315
+ let state = "closed";
316
+ let failures = 0;
317
+ let openedAt = 0;
318
+ let probes = 0;
319
+
320
+ function setState(next) {
321
+ if (next === state) return;
322
+ const prev = state;
323
+ state = next;
324
+ if (onStateChange) onStateChange(next, prev);
325
+ }
326
+
327
+ function open() {
328
+ openedAt = Date.now();
329
+ setState("open");
330
+ }
331
+
332
+ function current() {
333
+ if (state === "open" && Date.now() - openedAt >= resetTimeoutMs) {
334
+ probes = 0;
335
+ setState("half-open");
336
+ }
337
+ return state;
338
+ }
339
+
340
+ function recordFailure() {
341
+ if (state === "half-open") {
342
+ open();
343
+ return;
344
+ }
345
+ failures++;
346
+ if (failures >= failureThreshold) open();
347
+ }
348
+
349
+ function recordSuccess() {
350
+ failures = 0;
351
+ if (state === "half-open") setState("closed");
352
+ }
353
+
354
+ async function run(fn) {
355
+ const s = current();
356
+ if (s === "open") {
357
+ throw new CircuitOpenError(Math.max(0, resetTimeoutMs - (Date.now() - openedAt)));
358
+ }
359
+ let probing = false;
360
+ if (s === "half-open") {
361
+ if (probes >= halfOpenMax) throw new CircuitOpenError(0);
362
+ probes++;
363
+ probing = true;
364
+ }
365
+
366
+ let result;
367
+ try {
368
+ result = await fn();
369
+ } catch (err) {
370
+ if (isFailure(err, true)) recordFailure();
371
+ else if (probing) probes--; // e.g. an abort — free the probe slot
372
+ throw err;
373
+ }
374
+
375
+ if (isFailure(result, false)) recordFailure();
376
+ else recordSuccess();
377
+ return result;
378
+ }
379
+
380
+ return {
381
+ run,
382
+ get state() {
383
+ return current();
384
+ },
385
+ get failures() {
386
+ return failures;
387
+ },
388
+ /** Force the circuit closed and forget the failure count. */
389
+ reset() {
390
+ failures = 0;
391
+ probes = 0;
392
+ setState("closed");
393
+ },
394
+ };
395
+ }
396
+
397
+ module.exports = { createCircuitBreaker, CircuitOpenError };
398
+
399
+ },
400
+ "./createFetch": function (module, exports, require) {
401
+ const { withRetry, RetryAbortedError } = require("./withRetry");
402
+ const { withTimeout } = require("./withTimeout");
403
+ const { withDedupe } = require("./withDedupe");
404
+ const { buildQuery } = require("./buildQuery");
405
+ const { parseJsonSafe } = require("./parseJsonSafe");
406
+ const { normalizeError } = require("./normalizeError");
407
+ const { HTTPError } = require("./HTTPError");
408
+ const { createRateLimiter } = require("./createRateLimiter");
409
+ const { createCircuitBreaker } = require("./createCircuitBreaker");
410
+ const { createMemoryCache } = require("./createMemoryCache");
411
+ const { cacheKey, snapshotResponse, entryToResponse } = require("./_cache");
412
+ const { discardBody } = require("./_http");
413
+
414
+ const DEFAULT_RETRY_STATUS_CODES = [408, 425, 429, 500, 502, 503, 504];
415
+ // Safe to repeat without side effects. POST/PATCH are deliberately absent.
416
+ const DEFAULT_RETRY_METHODS = ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"];
417
+
418
+ function isAbsoluteUrl(url) {
419
+ return /^[a-z][a-z0-9+.-]*:/i.test(url);
420
+ }
421
+
422
+ function joinUrl(baseUrl, url, query) {
423
+ let full = String(url);
424
+ if (baseUrl && !isAbsoluteUrl(full)) {
425
+ full = `${String(baseUrl).replace(/\/+$/, "")}/${full.replace(/^\/+/, "")}`;
426
+ }
427
+ const qs = query ? buildQuery(query) : "";
428
+ if (qs) full += (full.includes("?") ? "&" : "?") + qs;
429
+ return full;
430
+ }
431
+
432
+ async function resolveHeaders(source) {
433
+ const value = typeof source === "function" ? await source() : source;
434
+ return new Headers(value || undefined);
435
+ }
436
+
437
+ function mergeHeaders(...headerSets) {
438
+ const out = new Headers();
439
+ for (const set of headerSets) {
440
+ set.forEach((value, key) => out.set(key, value));
441
+ }
442
+ return out;
443
+ }
444
+
445
+ function isStreamBody(body) {
446
+ return Boolean(body) && typeof body === "object" && typeof body.getReader === "function";
447
+ }
448
+
449
+ function originOf(url) {
450
+ try {
451
+ return new URL(url).origin;
452
+ } catch {
453
+ return url;
454
+ }
455
+ }
456
+
457
+ function mergeDefaults(base, extra) {
458
+ const merged = { ...base, ...extra };
459
+
460
+ if (base.headers || extra.headers) {
461
+ merged.headers = async () => {
462
+ const a = await resolveHeaders(base.headers);
463
+ const b = await resolveHeaders(extra.headers);
464
+ return mergeHeaders(a, b);
465
+ };
466
+ }
467
+ if (base.retry !== false && extra.retry !== false && (base.retry || extra.retry)) {
468
+ merged.retry = { ...base.retry, ...extra.retry };
469
+ }
470
+ return merged;
471
+ }
472
+
473
+ /**
474
+ * Builds a fetch-like function from the other reqkit pieces — only if you
475
+ * want that. It's still plain `fetch` underneath: you get a standard
476
+ * `Response` back, and `fetch` itself stays swappable.
477
+ *
478
+ * What you get on top of fetch:
479
+ * - `baseUrl`, `query` (via buildQuery), `json` request bodies
480
+ * - default headers (an object, or a function for tokens that change)
481
+ * - per-attempt `timeout` (withTimeout) and external `signal`
482
+ * - retries (withRetry) on network errors/timeouts and retryable statuses,
483
+ * honoring Retry-After; only for idempotent methods unless you opt in
484
+ * - `rateLimit`: stay under a budget and obey X-RateLimit-* / Retry-After
485
+ * *before* the server has to say 429
486
+ * - `circuitBreaker`: fail fast against a service that keeps failing
487
+ * - `cache`: in-memory GET cache with TTL, stale-while-revalidate and
488
+ * ETag / Last-Modified revalidation (304s)
489
+ * - concurrent GET de-duplication (withDedupe)
490
+ * - `HTTPError` on non-2xx (turn off with `throwHttpErrors: false`)
491
+ * - `onRequest` / `onResponse` hooks
492
+ *
493
+ * @param {object} [defaults]
494
+ * @param {string} [defaults.baseUrl]
495
+ * @param {HeadersInit | (() => HeadersInit | Promise<HeadersInit>)} [defaults.headers]
496
+ * @param {number | false} [defaults.timeout=30000] - per attempt, ms
497
+ * @param {false | object} [defaults.retry] - false disables. Options:
498
+ * `retries`(2), `statusCodes`, `methods`, `baseDelayMs`, `maxDelayMs`,
499
+ * `factor`, `jitter`, `maxRetryTime`, `onRetry`, `calculateDelay`
500
+ * @param {boolean | object} [defaults.rateLimit] - `true` just learns from
501
+ * response headers; `{ requests, perMs }` also enforces a budget; or pass a
502
+ * createRateLimiter() instance to share one budget
503
+ * @param {boolean | object | Function} [defaults.circuitBreaker] - options for
504
+ * a per-origin breaker, a createCircuitBreaker() instance, or `(url) => breaker`
505
+ * @param {boolean | object} [defaults.cache] - `{ ttl, staleWhileRevalidate,
506
+ * maxEntries, maxBodyBytes, store, onEvent }`; `true` = 60s TTL
507
+ * @param {boolean} [defaults.dedupe=false] - merge identical concurrent GET/HEAD calls
508
+ * @param {boolean} [defaults.throwHttpErrors=true]
509
+ * @param {typeof fetch} [defaults.fetch=globalThis.fetch]
510
+ * @param {(ctx: {url: string, init: object}) => void | Promise<void>} [defaults.onRequest] -
511
+ * runs once per call; may mutate ctx.url / ctx.init (e.g. add a header)
512
+ * @param {(ctx: {url: string, init: object, response: Response}) => void | Promise<void>} [defaults.onResponse]
513
+ */
514
+ function createFetch(defaults = {}) {
515
+ const {
516
+ baseUrl,
517
+ headers: defaultHeaders,
518
+ timeout: defaultTimeout = 30000,
519
+ retry: defaultRetry = {},
520
+ rateLimit,
521
+ circuitBreaker,
522
+ cache,
523
+ dedupe = false,
524
+ throwHttpErrors: defaultThrow = true,
525
+ fetch: customFetch,
526
+ onRequest,
527
+ onResponse,
528
+ } = defaults;
529
+
530
+ const doFetch = (...args) => (customFetch || globalThis.fetch)(...args);
531
+
532
+ // --- stateful pieces, built once so `extend()` children share them -------
533
+ const limiter = !rateLimit
534
+ ? null
535
+ : typeof rateLimit.acquire === "function"
536
+ ? rateLimit
537
+ : createRateLimiter(rateLimit === true ? {} : rateLimit);
538
+
539
+ const breakerRegistry = new Map();
540
+ const breakerFor = !circuitBreaker
541
+ ? null
542
+ : typeof circuitBreaker === "function"
543
+ ? circuitBreaker
544
+ : typeof circuitBreaker.run === "function"
545
+ ? () => circuitBreaker
546
+ : (url) => {
547
+ const origin = originOf(url);
548
+ if (!breakerRegistry.has(origin)) {
549
+ breakerRegistry.set(origin, createCircuitBreaker(circuitBreaker === true ? {} : circuitBreaker));
550
+ }
551
+ return breakerRegistry.get(origin);
552
+ };
553
+
554
+ const cacheCfg = !cache
555
+ ? null
556
+ : {
557
+ ttl: 60000,
558
+ staleWhileRevalidate: 0,
559
+ maxEntries: 100,
560
+ maxBodyBytes: undefined,
561
+ ...(cache === true ? {} : cache),
562
+ };
563
+ if (cacheCfg && !cacheCfg.store) cacheCfg.store = createMemoryCache({ maxEntries: cacheCfg.maxEntries });
564
+ // In-flight bookkeeping lives on the (shared) cache config so `extend()`
565
+ // children see the parent's pending writes and revalidations.
566
+ if (cacheCfg) {
567
+ cacheCfg.pending = cacheCfg.pending || new Map();
568
+ cacheCfg.revalidating = cacheCfg.revalidating || new Set();
569
+ }
570
+ const pendingStores = cacheCfg ? cacheCfg.pending : null;
571
+ const revalidating = cacheCfg ? cacheCfg.revalidating : null;
572
+ const emit = (type, url) => cacheCfg && cacheCfg.onEvent && cacheCfg.onEvent({ type, url });
573
+
574
+ // Shared in-flight GETs. Every caller gets its own clone of the response so
575
+ // body reads don't fight each other.
576
+ const sharedFetch = withDedupe((key, url, init) => doFetch(url, init), { keyFn: (key) => key });
577
+
578
+ function scheduleStore(key, response, url) {
579
+ const p = snapshotResponse(response, cacheCfg.maxBodyBytes)
580
+ .then(async (entry) => {
581
+ if (entry) {
582
+ await cacheCfg.store.set(key, entry);
583
+ emit("store", url);
584
+ }
585
+ })
586
+ .catch(() => {})
587
+ .finally(() => {
588
+ if (pendingStores.get(key) === p) pendingStores.delete(key);
589
+ });
590
+ pendingStores.set(key, p);
591
+ }
592
+
593
+ async function request(input, init = {}) {
594
+ const {
595
+ query,
596
+ json,
597
+ timeout = defaultTimeout,
598
+ retry: retryOpt,
599
+ throwHttpErrors = defaultThrow,
600
+ signal,
601
+ headers: initHeaders,
602
+ cache: cacheMode,
603
+ ...fetchInit
604
+ } = init;
605
+
606
+ const method = String(fetchInit.method || "GET").toUpperCase();
607
+ const headers = mergeHeaders(await resolveHeaders(defaultHeaders), new Headers(initHeaders || undefined));
608
+
609
+ let body = fetchInit.body;
610
+ if (json !== undefined) {
611
+ body = JSON.stringify(json);
612
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
613
+ }
614
+
615
+ const ctx = { url: joinUrl(baseUrl, input, query), init: { ...fetchInit, method, headers, body } };
616
+ if (onRequest) await onRequest(ctx);
617
+ const finalHeaders = ctx.init.headers instanceof Headers ? ctx.init.headers : new Headers(ctx.init.headers);
618
+ const finalMethod = String(ctx.init.method || method).toUpperCase();
619
+
620
+ // --- retry policy ------------------------------------------------------
621
+ const retryCfg = retryOpt === false || defaultRetry === false ? false : { ...defaultRetry, ...retryOpt };
622
+ const methods = (retryCfg && retryCfg.methods) || DEFAULT_RETRY_METHODS;
623
+ const hasIdempotencyKey = finalHeaders.has("idempotency-key");
624
+ const canRetry =
625
+ retryCfg !== false &&
626
+ !isStreamBody(ctx.init.body) && // a consumed stream can't be replayed
627
+ (methods.map((m) => m.toUpperCase()).includes(finalMethod) || hasIdempotencyKey);
628
+ const { statusCodes, methods: _methods, retries = 2, shouldRetry: customShouldRetry, ...retryRest } = retryCfg || {};
629
+
630
+ // One trip to the network: rate limit → circuit breaker → per-attempt
631
+ // timeout → fetch, retried per the policy above. `netHeaders` and
632
+ // `netSignal` are parameters so background revalidation can reuse this.
633
+ function network(netHeaders, netSignal) {
634
+ const attemptOnce = () => {
635
+ const attempt = async () => {
636
+ if (limiter) await limiter.acquire({ signal: netSignal });
637
+ const res = await withTimeout(
638
+ (attemptSignal) => {
639
+ const attemptInit = { ...ctx.init, method: finalMethod, headers: netHeaders, signal: attemptSignal };
640
+ const useDedupe =
641
+ dedupe && !netSignal && !ctx.init.body && (finalMethod === "GET" || finalMethod === "HEAD");
642
+ if (!useDedupe) return doFetch(ctx.url, attemptInit);
643
+ // No per-attempt signal on a shared request: aborting one caller
644
+ // must never cancel everyone else's. It still gets its own
645
+ // timeout so a hung server can't leak the request.
646
+ const sharedInit = {
647
+ ...attemptInit,
648
+ signal: timeout === false ? undefined : AbortSignal.timeout(timeout),
649
+ };
650
+ const key = `${finalMethod} ${ctx.url} ${JSON.stringify([...netHeaders.entries()].sort())}`;
651
+ return sharedFetch(key, ctx.url, sharedInit).then((res) => res.clone());
652
+ },
653
+ timeout === false ? Infinity : timeout,
654
+ { signal: netSignal }
655
+ );
656
+ if (limiter) limiter.update(res);
657
+ return res;
658
+ };
659
+ const breaker = breakerFor && breakerFor(ctx.url);
660
+ return breaker ? breaker.run(attempt) : attempt();
661
+ };
662
+
663
+ return canRetry
664
+ ? withRetry(attemptOnce, {
665
+ ...retryRest,
666
+ retries,
667
+ retryStatusCodes: statusCodes || DEFAULT_RETRY_STATUS_CODES,
668
+ shouldRetry:
669
+ customShouldRetry ||
670
+ ((err) => {
671
+ const n = normalizeError(err);
672
+ return n.isNetworkError || n.isTimeout;
673
+ }),
674
+ signal: netSignal,
675
+ })
676
+ : attemptOnce();
677
+ }
678
+
679
+ function conditionalHeaders(entry) {
680
+ const h = new Headers(finalHeaders);
681
+ if (entry) {
682
+ if (entry.etag && !h.has("if-none-match")) h.set("if-none-match", entry.etag);
683
+ if (entry.lastModified && !h.has("if-modified-since")) h.set("if-modified-since", entry.lastModified);
684
+ }
685
+ return h;
686
+ }
687
+
688
+ // --- cache lookup --------------------------------------------------------
689
+ const useCache =
690
+ Boolean(cacheCfg) &&
691
+ finalMethod === "GET" &&
692
+ !ctx.init.body &&
693
+ cacheMode !== false &&
694
+ cacheMode !== "no-store";
695
+ const key = useCache ? cacheKey(finalMethod, ctx.url, finalHeaders) : null;
696
+ let entry = null;
697
+
698
+ const finish = async (response) => {
699
+ if (onResponse) await onResponse({ url: ctx.url, init: ctx.init, response });
700
+ if (throwHttpErrors && !response.ok) {
701
+ throw new HTTPError(response, { method: finalMethod, url: ctx.url });
702
+ }
703
+ return response;
704
+ };
705
+
706
+ if (useCache && cacheMode !== "reload") {
707
+ if (pendingStores.has(key)) await pendingStores.get(key);
708
+ entry = (await cacheCfg.store.get(key)) || null;
709
+ if (entry) {
710
+ const age = Date.now() - entry.storedAt;
711
+ if (age < cacheCfg.ttl) {
712
+ emit("hit", ctx.url);
713
+ return finish(entryToResponse(entry));
714
+ }
715
+ if (age < cacheCfg.ttl + cacheCfg.staleWhileRevalidate) {
716
+ emit("stale", ctx.url);
717
+ if (!revalidating.has(key)) {
718
+ revalidating.add(key);
719
+ const staleEntry = entry;
720
+ (async () => {
721
+ try {
722
+ const res = await network(conditionalHeaders(staleEntry), undefined);
723
+ if (res.status === 304) {
724
+ staleEntry.storedAt = Date.now();
725
+ await cacheCfg.store.set(key, staleEntry);
726
+ emit("revalidated", ctx.url);
727
+ await discardBody(res);
728
+ } else if (res.status === 200) {
729
+ scheduleStore(key, res, ctx.url);
730
+ await pendingStores.get(key);
731
+ } else {
732
+ await discardBody(res);
733
+ }
734
+ } catch {
735
+ // keep serving the stale copy; try again next time
736
+ } finally {
737
+ revalidating.delete(key);
738
+ }
739
+ })();
740
+ }
741
+ return finish(entryToResponse(entry));
742
+ }
743
+ }
744
+ }
745
+
746
+ // --- network -------------------------------------------------------------
747
+ let response;
748
+ try {
749
+ response = await network(useCache ? conditionalHeaders(entry) : finalHeaders, signal);
750
+ } catch (err) {
751
+ // Present a user abort as the plain AbortError they caused, not as retry internals.
752
+ if (err instanceof RetryAbortedError && signal && signal.aborted) throw signal.reason ?? err;
753
+ throw err;
754
+ }
755
+
756
+ if (useCache) {
757
+ if (response.status === 304 && entry) {
758
+ entry.storedAt = Date.now();
759
+ await cacheCfg.store.set(key, entry);
760
+ emit("revalidated", ctx.url);
761
+ await discardBody(response);
762
+ response = entryToResponse(entry);
763
+ } else {
764
+ emit("miss", ctx.url);
765
+ if (response.status === 200) scheduleStore(key, response, ctx.url);
766
+ }
767
+ }
768
+
769
+ return finish(response);
770
+ }
771
+
772
+ /** Like `request`, but resolves to the parsed JSON body (null for an empty body). */
773
+ request.json = async (input, init) => {
774
+ const response = await request(input, init);
775
+ const { data, error } = await parseJsonSafe(response);
776
+ if (error) throw error;
777
+ return data;
778
+ };
779
+
780
+ /** A new fetch with these defaults merged over the current ones (headers and retry are merged, the rest replaced). */
781
+ request.extend = (overrides = {}) =>
782
+ createFetch(
783
+ mergeDefaults(
784
+ {
785
+ ...defaults,
786
+ // children share the parent's budget, breakers and cache unless they bring their own
787
+ rateLimit: limiter || undefined,
788
+ circuitBreaker: breakerFor || undefined,
789
+ cache: cacheCfg || undefined,
790
+ },
791
+ overrides
792
+ )
793
+ );
794
+
795
+ return request;
796
+ }
797
+
798
+ module.exports = { createFetch, HTTPError };
799
+
800
+ },
801
+ "./createLimiter": function (module, exports, require) {
802
+ /**
803
+ * Creates a concurrency limiter: at most `concurrency` calls run at once,
804
+ * the rest wait in a FIFO queue. Handy for "download these 200 files, 4 at a
805
+ * time" without pulling in a queue library. (Same idea as p-limit.)
806
+ *
807
+ * ```js
808
+ * const limit = createLimiter(4);
809
+ * const bytes = await Promise.all(urls.map((u) => limit(() => download(u))));
810
+ * // or: await limit.map(urls, (u) => download(u));
811
+ * ```
812
+ *
813
+ * @param {number} [concurrency=Infinity] - integer >= 1, or Infinity
814
+ */
815
+ function createLimiter(concurrency = Infinity) {
816
+ if (concurrency !== Infinity && !(Number.isInteger(concurrency) && concurrency >= 1)) {
817
+ throw new TypeError("createLimiter: `concurrency` must be an integer >= 1 (or Infinity)");
818
+ }
819
+
820
+ const queue = [];
821
+ let active = 0;
822
+
823
+ function next() {
824
+ while (active < concurrency && queue.length > 0) {
825
+ active++;
826
+ queue.shift()();
827
+ }
828
+ }
829
+
830
+ function limit(fn, ...args) {
831
+ return new Promise((resolve, reject) => {
832
+ queue.push(() => {
833
+ Promise.resolve()
834
+ .then(() => fn(...args))
835
+ .then(resolve, reject)
836
+ .finally(() => {
837
+ active--;
838
+ next();
839
+ });
840
+ });
841
+ next();
842
+ });
843
+ }
844
+
845
+ Object.defineProperties(limit, {
846
+ activeCount: { get: () => active },
847
+ pendingCount: { get: () => queue.length },
848
+ });
849
+
850
+ /** Runs `fn(item, index)` for every item through the limiter. Results keep input order. */
851
+ limit.map = (items, fn) => Promise.all(Array.from(items, (item, i) => limit(fn, item, i)));
852
+
853
+ return limit;
854
+ }
855
+
856
+ module.exports = { createLimiter };
857
+
858
+ },
859
+ "./createMemoryCache": function (module, exports, require) {
860
+ /**
861
+ * A small in-memory LRU store for createFetch's `cache` option. Anything with
862
+ * async-or-sync `get(key)`, `set(key, value)` and `delete(key)` can be used
863
+ * instead (Redis, IndexedDB, a Map...).
864
+ *
865
+ * @param {{ maxEntries?: number }} [opts]
866
+ */
867
+ function createMemoryCache(opts = {}) {
868
+ const { maxEntries = 100 } = opts;
869
+ if (!(Number.isInteger(maxEntries) && maxEntries >= 1)) {
870
+ throw new TypeError("createMemoryCache: `maxEntries` must be an integer >= 1");
871
+ }
872
+ const map = new Map();
873
+
874
+ return {
875
+ get(key) {
876
+ if (!map.has(key)) return undefined;
877
+ const value = map.get(key);
878
+ map.delete(key); // re-insert so it counts as most recently used
879
+ map.set(key, value);
880
+ return value;
881
+ },
882
+ set(key, value) {
883
+ map.delete(key);
884
+ map.set(key, value);
885
+ while (map.size > maxEntries) map.delete(map.keys().next().value);
886
+ },
887
+ delete(key) {
888
+ return map.delete(key);
889
+ },
890
+ clear() {
891
+ map.clear();
892
+ },
893
+ get size() {
894
+ return map.size;
895
+ },
896
+ };
897
+ }
898
+
899
+ module.exports = { createMemoryCache };
900
+
901
+ },
902
+ "./createMockFetch": function (module, exports, require) {
903
+ const { abortableSleep } = require("./_sleep");
904
+
905
+ const BODYLESS = new Set([101, 204, 205, 304]);
906
+
907
+ function toBody(body, headers) {
908
+ if (body === null || body === undefined) return null;
909
+ if (typeof body === "string") return body;
910
+ if (
911
+ body instanceof ArrayBuffer ||
912
+ ArrayBuffer.isView(body) ||
913
+ (typeof Blob !== "undefined" && body instanceof Blob) ||
914
+ (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) ||
915
+ body instanceof URLSearchParams
916
+ ) {
917
+ return body;
918
+ }
919
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
920
+ return JSON.stringify(body);
921
+ }
922
+
923
+ function buildResponse(status, body, extraHeaders) {
924
+ const headers = new Headers(extraHeaders || undefined);
925
+ const payload = BODYLESS.has(status) ? null : toBody(body, headers);
926
+ return new Response(payload, { status, headers });
927
+ }
928
+
929
+ function matchesUrl(matcher, url) {
930
+ if (typeof matcher === "function") return Boolean(matcher(url));
931
+ if (matcher instanceof RegExp) return matcher.test(url);
932
+ if (matcher === "*" || matcher === undefined) return true;
933
+ if (url === matcher) return true;
934
+ try {
935
+ const u = new URL(url);
936
+ if (matcher === u.pathname + u.search) return true;
937
+ if (!String(matcher).includes("?") && matcher === u.pathname) return true;
938
+ } catch {
939
+ // relative/odd URL — exact match only
940
+ }
941
+ return false;
942
+ }
943
+
944
+ /**
945
+ * A scriptable, fetch-compatible test double — for testing code that uses
946
+ * retries, timeouts, rate limits and so on without a network.
947
+ *
948
+ * ```js
949
+ * const mock = createMockFetch();
950
+ * mock.on("GET", "/users/1")
951
+ * .replyOnce(503, "busy", { headers: { "retry-after": "0" } })
952
+ * .reply(200, { id: 1 });
953
+ * mock.on("POST", /\/orders/).fail(mock.networkError());
954
+ *
955
+ * const api = createFetch({ fetch: mock.fetch, baseUrl: "https://api.test" });
956
+ * await api.json("/users/1"); // → { id: 1 } after one retry
957
+ * mock.calls.length; // 2
958
+ * ```
959
+ *
960
+ * String matchers match the full URL, `pathname + search`, or the bare
961
+ * `pathname`; you can also pass a RegExp or a `(url) => boolean`. `"*"` as the
962
+ * method matches any method. Unmatched requests reject with a clear error.
963
+ */
964
+ function createMockFetch() {
965
+ let routes = [];
966
+ const calls = [];
967
+
968
+ function on(method, matcher) {
969
+ const route = { method: String(method).toUpperCase(), matcher, once: [], fallback: null };
970
+ routes.push(route);
971
+
972
+ const chain = {
973
+ /** Always answer like this (once any `…Once` replies are used up). */
974
+ reply(status = 200, body, opts) {
975
+ route.fallback = replyHandler(status, body, opts);
976
+ return chain;
977
+ },
978
+ replyOnce(status = 200, body, opts) {
979
+ route.once.push(replyHandler(status, body, opts));
980
+ return chain;
981
+ },
982
+ /** `fn(call)` returns a Response, `{ status, body, headers }`, or throws/rejects. */
983
+ replyWith(fn) {
984
+ route.fallback = customHandler(fn);
985
+ return chain;
986
+ },
987
+ replyWithOnce(fn) {
988
+ route.once.push(customHandler(fn));
989
+ return chain;
990
+ },
991
+ /** Reject like fetch does on a network failure. */
992
+ fail(err = networkError()) {
993
+ route.fallback = async () => {
994
+ throw err;
995
+ };
996
+ return chain;
997
+ },
998
+ failOnce(err = networkError()) {
999
+ route.once.push(async () => {
1000
+ throw err;
1001
+ });
1002
+ return chain;
1003
+ },
1004
+ };
1005
+ return chain;
1006
+ }
1007
+
1008
+ function replyHandler(status, body, opts = {}) {
1009
+ return async (call) => {
1010
+ if (opts.delayMs) await abortableSleep(opts.delayMs, call.signal);
1011
+ return buildResponse(status, body, opts.headers);
1012
+ };
1013
+ }
1014
+
1015
+ function customHandler(fn) {
1016
+ return async (call) => {
1017
+ const out = await fn(call);
1018
+ if (out instanceof Response) return out;
1019
+ return buildResponse(out.status ?? 200, out.body, out.headers);
1020
+ };
1021
+ }
1022
+
1023
+ function networkError(code = "ECONNREFUSED") {
1024
+ const err = new TypeError("fetch failed");
1025
+ err.cause = Object.assign(new Error(`connect ${code}`), { code });
1026
+ return err;
1027
+ }
1028
+
1029
+ async function mockFetch(input, init = {}) {
1030
+ const url = input instanceof URL ? input.href : typeof input === "string" ? input : input.url;
1031
+ const method = String(init.method || (typeof input === "object" && input.method) || "GET").toUpperCase();
1032
+ const signal = init.signal || (typeof input === "object" ? input.signal : undefined);
1033
+ signal?.throwIfAborted?.();
1034
+
1035
+ const headers = {};
1036
+ new Headers(init.headers || (typeof input === "object" ? input.headers : undefined)).forEach((v, k) => {
1037
+ headers[k] = v;
1038
+ });
1039
+
1040
+ const call = {
1041
+ url,
1042
+ method,
1043
+ headers,
1044
+ body: init.body,
1045
+ signal,
1046
+ get json() {
1047
+ try {
1048
+ return typeof this.body === "string" ? JSON.parse(this.body) : undefined;
1049
+ } catch {
1050
+ return undefined;
1051
+ }
1052
+ },
1053
+ };
1054
+ calls.push(call);
1055
+
1056
+ const route = routes.find(
1057
+ (r) => (r.method === "*" || r.method === method) && matchesUrl(r.matcher, url) && (r.once.length > 0 || r.fallback)
1058
+ );
1059
+ if (!route) {
1060
+ throw new Error(`createMockFetch: no mock registered for ${method} ${url}`);
1061
+ }
1062
+ const handler = route.once.length > 0 ? route.once.shift() : route.fallback;
1063
+ return handler(call);
1064
+ }
1065
+
1066
+ return {
1067
+ fetch: mockFetch,
1068
+ on,
1069
+ get: (m) => on("GET", m),
1070
+ post: (m) => on("POST", m),
1071
+ put: (m) => on("PUT", m),
1072
+ patch: (m) => on("PATCH", m),
1073
+ delete: (m) => on("DELETE", m),
1074
+ /** A TypeError("fetch failed") with `cause.code`, like Node's fetch throws. */
1075
+ networkError,
1076
+ /** Every request made so far, in order. */
1077
+ calls,
1078
+ /** Remove all routes and forget recorded calls. */
1079
+ reset() {
1080
+ routes = [];
1081
+ calls.length = 0;
1082
+ },
1083
+ };
1084
+ }
1085
+
1086
+ module.exports = { createMockFetch };
1087
+
1088
+ },
1089
+ "./createRateLimiter": function (module, exports, require) {
1090
+ const { abortableSleep } = require("./_sleep");
1091
+ const { getRateLimitInfo } = require("./_http");
1092
+
1093
+ function abortable(promise, signal) {
1094
+ if (!signal) return promise;
1095
+ return new Promise((resolve, reject) => {
1096
+ if (signal.aborted) {
1097
+ reject(signal.reason);
1098
+ return;
1099
+ }
1100
+ const onAbort = () => reject(signal.reason);
1101
+ signal.addEventListener("abort", onAbort, { once: true });
1102
+ promise.then(
1103
+ (v) => (signal.removeEventListener("abort", onAbort), resolve(v)),
1104
+ (e) => (signal.removeEventListener("abort", onAbort), reject(e))
1105
+ );
1106
+ });
1107
+ }
1108
+
1109
+ /**
1110
+ * A *proactive* rate limiter: slows you down before the server has to say
1111
+ * 429. Two things feed it:
1112
+ * 1. your own budget — `requests` per `perMs` (sliding window), and
1113
+ * 2. what the server tells you — `update(response)` reads X-RateLimit-* /
1114
+ * RateLimit-* / Retry-After and pauses everything once `remaining` hits 0
1115
+ * (or the server sent Retry-After), until the reset time.
1116
+ *
1117
+ * Callers are served FIFO.
1118
+ *
1119
+ * ```js
1120
+ * const rl = createRateLimiter({ requests: 10, perMs: 1000 });
1121
+ * const res = await rl.run(() => fetch(url)); // acquire → call → learn from headers
1122
+ * ```
1123
+ *
1124
+ * @param {object} [opts]
1125
+ * @param {number} [opts.requests=Infinity] - max requests per window
1126
+ * @param {number} [opts.perMs=1000] - window length
1127
+ */
1128
+ function createRateLimiter(opts = {}) {
1129
+ const { requests = Infinity, perMs = 1000 } = opts;
1130
+ if (requests !== Infinity && !(Number.isInteger(requests) && requests >= 1)) {
1131
+ throw new TypeError("createRateLimiter: `requests` must be an integer >= 1 (or Infinity)");
1132
+ }
1133
+ if (!(typeof perMs === "number" && perMs > 0 && Number.isFinite(perMs))) {
1134
+ throw new TypeError("createRateLimiter: `perMs` must be a positive number");
1135
+ }
1136
+
1137
+ const stamps = [];
1138
+ let blockedUntil = 0;
1139
+ let tail = Promise.resolve();
1140
+
1141
+ async function take(signal) {
1142
+ for (;;) {
1143
+ signal?.throwIfAborted?.();
1144
+ const now = Date.now();
1145
+ while (stamps.length > 0 && stamps[0] <= now - perMs) stamps.shift();
1146
+
1147
+ let waitMs = 0;
1148
+ if (blockedUntil > now) waitMs = blockedUntil - now;
1149
+ else if (stamps.length >= requests) waitMs = stamps[0] + perMs - now;
1150
+
1151
+ if (waitMs <= 0) {
1152
+ stamps.push(now);
1153
+ return;
1154
+ }
1155
+ await abortableSleep(waitMs, signal);
1156
+ }
1157
+ }
1158
+
1159
+ /** Resolves when it's your turn. Rejects with the signal's reason if aborted while waiting. */
1160
+ function acquire({ signal } = {}) {
1161
+ const turn = tail.then(() => take(signal));
1162
+ tail = turn.catch(() => {});
1163
+ return abortable(turn, signal);
1164
+ }
1165
+
1166
+ /** Feed a response (anything with `.headers`/`.status`) so the limiter learns the server's limits. */
1167
+ function update(response) {
1168
+ if (!response || !response.headers) return;
1169
+ const { remaining, resetMs, retryAfterMs } = getRateLimitInfo(response.headers);
1170
+ let blockMs = null;
1171
+ if (retryAfterMs !== null && (response.status === 429 || response.status === 503)) blockMs = retryAfterMs;
1172
+ else if (remaining === 0 && resetMs !== null) blockMs = resetMs;
1173
+ if (blockMs !== null) blockedUntil = Math.max(blockedUntil, Date.now() + blockMs);
1174
+ }
1175
+
1176
+ /** acquire → fn() → update(result). Errors thrown by fn propagate (the slot stays spent). */
1177
+ async function run(fn, { signal } = {}) {
1178
+ await acquire({ signal });
1179
+ const result = await fn();
1180
+ update(result);
1181
+ return result;
1182
+ }
1183
+
1184
+ return {
1185
+ acquire,
1186
+ update,
1187
+ run,
1188
+ /** Ms until the server-imposed pause ends (0 if none). */
1189
+ get blockedForMs() {
1190
+ return Math.max(0, blockedUntil - Date.now());
1191
+ },
1192
+ /** Forget all history and server hints. */
1193
+ reset() {
1194
+ stamps.length = 0;
1195
+ blockedUntil = 0;
1196
+ },
1197
+ };
1198
+ }
1199
+
1200
+ module.exports = { createRateLimiter };
1201
+
1202
+ },
1203
+ "./downloadResumable": function (module, exports, require) {
1204
+ const { HTTPError } = require("./HTTPError");
1205
+ const { BodyTooLargeError } = require("./withProgress");
1206
+ const { normalizeError } = require("./normalizeError");
1207
+ const { getWaitMsFromHeaders } = require("./_http");
1208
+ const { abortableSleep } = require("./_sleep");
1209
+
1210
+ const RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
1211
+
1212
+ function parseContentRange(header) {
1213
+ // "bytes 100-199/1000" (total may be "*")
1214
+ const m = /^bytes\s+(\d+)-(\d+)\/(\d+|\*)$/i.exec(String(header || "").trim());
1215
+ if (!m) return null;
1216
+ return { start: Number(m[1]), end: Number(m[2]), total: m[3] === "*" ? null : Number(m[3]) };
1217
+ }
1218
+
1219
+ function isRetryable(err) {
1220
+ if (err instanceof BodyTooLargeError) return false;
1221
+ if (err instanceof HTTPError) return RETRYABLE_STATUS.has(err.status);
1222
+ const n = normalizeError(err);
1223
+ if (n.isAborted) return false;
1224
+ return n.isNetworkError || n.isTimeout;
1225
+ }
1226
+
1227
+ /**
1228
+ * Downloads a URL into memory and, if the connection drops mid-body, resumes
1229
+ * from where it stopped with an HTTP `Range` request instead of starting
1230
+ * over. Uses `If-Range` with the first response's strong ETag (or
1231
+ * Last-Modified) so a file that changed on the server is restarted rather
1232
+ * than stitched together wrongly. If the server ignores Range, it simply
1233
+ * restarts from zero.
1234
+ *
1235
+ * `retries` counts consecutive attempts *without progress* — a flaky link
1236
+ * that keeps making headway keeps going. Honors Retry-After on 429/503.
1237
+ *
1238
+ * ```js
1239
+ * const bytes = await downloadResumable(url, {
1240
+ * onProgress: ({ percent }) => console.log(percent),
1241
+ * maxBytes: 200 * 1024 * 1024,
1242
+ * });
1243
+ * ```
1244
+ *
1245
+ * @param {string} url
1246
+ * @param {object} [opts]
1247
+ * @param {typeof fetch} [opts.fetch=globalThis.fetch] - e.g. `createFetch(...)` or a mock
1248
+ * @param {HeadersInit} [opts.headers]
1249
+ * @param {number} [opts.retries=5]
1250
+ * @param {number} [opts.baseDelayMs=500]
1251
+ * @param {number} [opts.maxDelayMs=10000]
1252
+ * @param {(p: {loaded: number, total: number | null, percent: number | null}) => void} [opts.onProgress]
1253
+ * @param {number} [opts.maxBytes] - throws BodyTooLargeError past this
1254
+ * @param {AbortSignal} [opts.signal]
1255
+ * @param {(err: any, attempt: number, delayMs: number, info: {resumeFrom: number}) => void} [opts.onRetry]
1256
+ * @returns {Promise<Uint8Array>}
1257
+ */
1258
+ async function downloadResumable(url, opts = {}) {
1259
+ const {
1260
+ fetch: fetchFn = globalThis.fetch,
1261
+ headers,
1262
+ retries = 5,
1263
+ baseDelayMs = 500,
1264
+ maxDelayMs = 10000,
1265
+ onProgress,
1266
+ maxBytes,
1267
+ signal,
1268
+ onRetry,
1269
+ } = opts;
1270
+
1271
+ const chunks = [];
1272
+ let loaded = 0;
1273
+ let total = null;
1274
+ let validator = null;
1275
+ let attempt = 0; // consecutive attempts without progress
1276
+
1277
+ const report = () => {
1278
+ if (onProgress) {
1279
+ onProgress({ loaded, total, percent: total ? Math.min(100, Math.round((loaded / total) * 100)) : null });
1280
+ }
1281
+ };
1282
+
1283
+ for (;;) {
1284
+ signal?.throwIfAborted?.();
1285
+ const loadedBefore = loaded;
1286
+
1287
+ try {
1288
+ const reqHeaders = new Headers(headers || undefined);
1289
+ // Range offsets count encoded bytes, so ask for an uncompressed body.
1290
+ if (!reqHeaders.has("accept-encoding")) reqHeaders.set("accept-encoding", "identity");
1291
+ if (loaded > 0) {
1292
+ reqHeaders.set("range", `bytes=${loaded}-`);
1293
+ if (validator) reqHeaders.set("if-range", validator);
1294
+ }
1295
+
1296
+ const res = await fetchFn(url, { headers: reqHeaders, signal });
1297
+
1298
+ if (res.status === 416 && loaded > 0) {
1299
+ // Our offset no longer fits the file — start over, without burning a retry.
1300
+ await res.body?.cancel?.().catch(() => {});
1301
+ chunks.length = 0;
1302
+ loaded = 0;
1303
+ continue;
1304
+ }
1305
+ if (!res.ok) {
1306
+ await res.body?.cancel?.().catch(() => {});
1307
+ throw new HTTPError(res, { method: "GET", url });
1308
+ }
1309
+
1310
+ const encoding = res.headers.get("content-encoding");
1311
+ const encoded = Boolean(encoding) && encoding.trim().toLowerCase() !== "identity";
1312
+
1313
+ if (loaded > 0) {
1314
+ const range = res.status === 206 ? parseContentRange(res.headers.get("content-range")) : null;
1315
+ if (!range || range.start !== loaded || encoded) {
1316
+ // Server ignored Range, the file changed (If-Range failed), or the
1317
+ // bytes don't line up: discard and restart from the beginning.
1318
+ chunks.length = 0;
1319
+ loaded = 0;
1320
+ }
1321
+ }
1322
+
1323
+ if (loaded === 0) {
1324
+ const range = res.status === 206 ? parseContentRange(res.headers.get("content-range")) : null;
1325
+ const length = Number(res.headers.get("content-length"));
1326
+ total = encoded ? null : range ? range.total : Number.isFinite(length) && res.headers.has("content-length") ? length : null;
1327
+ const etag = res.headers.get("etag");
1328
+ validator = etag && !etag.startsWith("W/") ? etag : res.headers.get("last-modified");
1329
+ }
1330
+
1331
+ if (maxBytes !== undefined && total !== null && total > maxBytes) {
1332
+ await res.body?.cancel?.().catch(() => {});
1333
+ throw new BodyTooLargeError(maxBytes);
1334
+ }
1335
+
1336
+ if (!res.body || typeof res.body.getReader !== "function") {
1337
+ const buf = new Uint8Array(await res.arrayBuffer());
1338
+ if (maxBytes !== undefined && buf.byteLength > maxBytes) throw new BodyTooLargeError(maxBytes);
1339
+ chunks.push(buf);
1340
+ loaded += buf.byteLength;
1341
+ total = total ?? loaded;
1342
+ report();
1343
+ } else {
1344
+ const reader = res.body.getReader();
1345
+ const onAbort = () => reader.cancel(signal.reason).catch(() => {});
1346
+ signal?.addEventListener("abort", onAbort, { once: true });
1347
+ let finished = false;
1348
+ try {
1349
+ for (;;) {
1350
+ const { done, value } = await reader.read();
1351
+ signal?.throwIfAborted?.();
1352
+ if (done) break;
1353
+ chunks.push(value);
1354
+ loaded += value.byteLength;
1355
+ if (maxBytes !== undefined && loaded > maxBytes) throw new BodyTooLargeError(maxBytes);
1356
+ report();
1357
+ }
1358
+ finished = true;
1359
+ } finally {
1360
+ signal?.removeEventListener("abort", onAbort);
1361
+ if (!finished) reader.cancel().catch(() => {});
1362
+ }
1363
+ }
1364
+
1365
+ if (total !== null && loaded < total) {
1366
+ // Stream ended early without an error (some servers just close the socket).
1367
+ throw Object.assign(new Error(`Incomplete download: got ${loaded} of ${total} bytes`), { code: "ECONNRESET" });
1368
+ }
1369
+
1370
+ const out = new Uint8Array(loaded);
1371
+ let offset = 0;
1372
+ for (const chunk of chunks) {
1373
+ out.set(chunk, offset);
1374
+ offset += chunk.byteLength;
1375
+ }
1376
+ return out;
1377
+ } catch (err) {
1378
+ if (signal?.aborted) throw signal.reason ?? err;
1379
+ if (!isRetryable(err)) throw err;
1380
+
1381
+ if (loaded > loadedBefore) attempt = 0; // it made progress — doesn't count against the budget
1382
+ if (attempt >= retries) throw err;
1383
+
1384
+ let delayMs = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
1385
+ if (err instanceof HTTPError) {
1386
+ const headerMs = getWaitMsFromHeaders(err.response && err.response.headers);
1387
+ if (headerMs !== null) delayMs = Math.min(headerMs, 60000);
1388
+ }
1389
+ attempt++;
1390
+ if (onRetry) onRetry(err, attempt, delayMs, { resumeFrom: loaded });
1391
+ await abortableSleep(delayMs, signal);
1392
+ }
1393
+ }
1394
+ }
1395
+
1396
+ module.exports = { downloadResumable };
1397
+
1398
+ },
1399
+ "./index": function (module, exports, require) {
1400
+ const { withRetry, RetryAbortedError, StopRetry } = require("./withRetry");
1401
+ const { withTimeout, TimeoutError } = require("./withTimeout");
1402
+ const { normalizeError } = require("./normalizeError");
1403
+ const { parseJsonSafe } = require("./parseJsonSafe");
1404
+ const { buildQuery } = require("./buildQuery");
1405
+ const { retryAfterAware, parseRetryAfter, getWaitMsFromHeaders } = require("./retryAfterAware");
1406
+ const { withProgress, BodyTooLargeError } = require("./withProgress");
1407
+ const { withDedupe } = require("./withDedupe");
1408
+ const { createLimiter } = require("./createLimiter");
1409
+ const { createFetch, HTTPError } = require("./createFetch");
1410
+ const { createRateLimiter } = require("./createRateLimiter");
1411
+ const { createCircuitBreaker, CircuitOpenError } = require("./createCircuitBreaker");
1412
+ const { createMemoryCache } = require("./createMemoryCache");
1413
+ const { createMockFetch } = require("./createMockFetch");
1414
+ const { downloadResumable } = require("./downloadResumable");
1415
+ const { readLines, readNDJSON, readSSE } = require("./streams");
1416
+ const { streamSSE } = require("./streamSSE");
1417
+
1418
+ module.exports = {
1419
+ withRetry,
1420
+ RetryAbortedError,
1421
+ StopRetry,
1422
+ withTimeout,
1423
+ TimeoutError,
1424
+ normalizeError,
1425
+ parseJsonSafe,
1426
+ buildQuery,
1427
+ retryAfterAware,
1428
+ parseRetryAfter,
1429
+ getWaitMsFromHeaders,
1430
+ withProgress,
1431
+ BodyTooLargeError,
1432
+ withDedupe,
1433
+ createLimiter,
1434
+ createFetch,
1435
+ HTTPError,
1436
+ createRateLimiter,
1437
+ createCircuitBreaker,
1438
+ CircuitOpenError,
1439
+ createMemoryCache,
1440
+ createMockFetch,
1441
+ downloadResumable,
1442
+ readLines,
1443
+ readNDJSON,
1444
+ readSSE,
1445
+ streamSSE,
1446
+ };
1447
+
1448
+ },
1449
+ "./normalizeError": function (module, exports, require) {
1450
+ /**
1451
+ * Normalizes errors from fetch, axios, node's http/https, undici and generic
1452
+ * Error objects into one consistent shape:
1453
+ * { message, status, code, isNetworkError, isTimeout, isAborted, cause }
1454
+ *
1455
+ * Every field is always present (null/false when not applicable) so callers
1456
+ * can destructure without existence checks.
1457
+ *
1458
+ * - `isTimeout` is true only for real timeouts (reqkit's TimeoutError,
1459
+ * AbortSignal.timeout(), ETIMEDOUT, undici timeouts) — not for a plain
1460
+ * user-initiated abort.
1461
+ * - `isAborted` is true when the request was cancelled on purpose
1462
+ * (AbortController.abort(), axios cancel).
1463
+ */
1464
+
1465
+ function shape(err, overrides) {
1466
+ return {
1467
+ message: "Unknown error",
1468
+ status: null,
1469
+ code: null,
1470
+ isNetworkError: false,
1471
+ isTimeout: false,
1472
+ isAborted: false,
1473
+ cause: err,
1474
+ ...overrides,
1475
+ };
1476
+ }
1477
+
1478
+ // Node's fetch (undici) throws `TypeError: fetch failed` and puts the real
1479
+ // reason (ECONNREFUSED, ENOTFOUND, UND_ERR_*...) on `err.cause`, sometimes
1480
+ // inside an AggregateError. Walk the chain to find the first string code.
1481
+ function findCode(err) {
1482
+ const seen = new Set();
1483
+ const queue = [err];
1484
+ while (queue.length && seen.size < 10) {
1485
+ const cur = queue.shift();
1486
+ if (!cur || typeof cur !== "object" || seen.has(cur)) continue;
1487
+ seen.add(cur);
1488
+ if (typeof cur.code === "string") return cur.code;
1489
+ queue.push(cur.cause);
1490
+ if (Array.isArray(cur.errors)) queue.push(...cur.errors);
1491
+ }
1492
+ return null;
1493
+ }
1494
+
1495
+ function isTimeoutCode(code) {
1496
+ return code === "ETIMEDOUT" || code === "ESOCKETTIMEDOUT" || /^UND_ERR_.*TIMEOUT$/.test(code || "");
1497
+ }
1498
+
1499
+ function isNetworkCode(code) {
1500
+ if (!code) return false;
1501
+ return /^E[A-Z0-9]+$/.test(code) || code === "EAI_AGAIN" || code === "EAI_FAIL" || code.startsWith("UND_ERR_");
1502
+ }
1503
+
1504
+ // Browsers and Node throw a bare TypeError for network-level fetch failures.
1505
+ const FETCH_NETWORK_MESSAGE =
1506
+ /fetch failed|failed to fetch|network ?request failed|load failed|networkerror when attempting to fetch|^terminated$|^network error$/i;
1507
+
1508
+ function isFetchNetworkTypeError(err) {
1509
+ return err.name === "TypeError" && FETCH_NETWORK_MESSAGE.test(err.message || "");
1510
+ }
1511
+
1512
+ function normalizeError(err) {
1513
+ if (!err) return shape(err);
1514
+
1515
+ // axios error shape: err.response.{status,data}, err.request, err.code
1516
+ if (err.isAxiosError || (err.response && typeof err.response.status === "number" && err.config)) {
1517
+ return shape(err, {
1518
+ message: err.response?.data?.message || err.message || "Request failed",
1519
+ status: err.response?.status ?? null,
1520
+ code: err.code ?? null,
1521
+ isNetworkError: !err.response && Boolean(err.request),
1522
+ isTimeout: err.code === "ECONNABORTED" || err.code === "ETIMEDOUT" || /timeout/i.test(err.message || ""),
1523
+ isAborted: err.code === "ERR_CANCELED" || err.name === "CanceledError",
1524
+ });
1525
+ }
1526
+
1527
+ // fetch Response passed in directly (not thrown, but common enough to handle)
1528
+ if (typeof err.status === "number" && typeof err.ok === "boolean") {
1529
+ return shape(err, {
1530
+ message: err.statusText || `Request failed with status ${err.status}`,
1531
+ status: err.status,
1532
+ });
1533
+ }
1534
+
1535
+ const chainCode = findCode(err);
1536
+
1537
+ // reqkit's TimeoutError, AbortSignal.timeout() (DOMException TimeoutError),
1538
+ // node ETIMEDOUT, undici connect/headers/body timeouts
1539
+ if (err.name === "TimeoutError" || isTimeoutCode(chainCode)) {
1540
+ return shape(err, {
1541
+ message: err.message || "Request timed out",
1542
+ code: chainCode || "ETIMEDOUT",
1543
+ isTimeout: true,
1544
+ });
1545
+ }
1546
+
1547
+ // Deliberate cancellation via AbortController — not a timeout.
1548
+ if (err.name === "AbortError") {
1549
+ return shape(err, {
1550
+ message: err.message || "Request aborted",
1551
+ code: "ABORT_ERR",
1552
+ isAborted: true,
1553
+ });
1554
+ }
1555
+
1556
+ // node http/https / undici network-level failures, including
1557
+ // `TypeError: fetch failed` with the real code on err.cause
1558
+ if (isNetworkCode(chainCode) || isFetchNetworkTypeError(err)) {
1559
+ return shape(err, {
1560
+ message: err.message || `Network error (${chainCode})`,
1561
+ code: chainCode,
1562
+ isNetworkError: true,
1563
+ });
1564
+ }
1565
+
1566
+ return shape(err, {
1567
+ message: err.message || String(err),
1568
+ status: err.status ?? err.statusCode ?? err.response?.status ?? err.response?.statusCode ?? null,
1569
+ code: err.code ?? null,
1570
+ });
1571
+ }
1572
+
1573
+ module.exports = { normalizeError };
1574
+
1575
+ },
1576
+ "./parseJsonSafe": function (module, exports, require) {
1577
+ /**
1578
+ * Parses a fetch Response (or raw string) as JSON without throwing.
1579
+ * Returns { data, error } — exactly one is non-null.
1580
+ *
1581
+ * @param {Response | string} input
1582
+ */
1583
+ async function parseJsonSafe(input) {
1584
+ let text;
1585
+ try {
1586
+ text = typeof input === "string" ? input : await input.text();
1587
+ } catch (err) {
1588
+ return { data: null, error: err };
1589
+ }
1590
+
1591
+ if (!text) {
1592
+ return { data: null, error: null };
1593
+ }
1594
+
1595
+ try {
1596
+ return { data: JSON.parse(text), error: null };
1597
+ } catch (err) {
1598
+ return { data: null, error: err };
1599
+ }
1600
+ }
1601
+
1602
+ module.exports = { parseJsonSafe };
1603
+
1604
+ },
1605
+ "./retryAfterAware": function (module, exports, require) {
1606
+ const { withRetry } = require("./withRetry");
1607
+ const { normalizeError } = require("./normalizeError");
1608
+ const { parseRetryAfter, getWaitMsFromHeaders } = require("./_http");
1609
+
1610
+ function defaultShouldRetryError(err) {
1611
+ const normalized = normalizeError(err);
1612
+ return normalized.isNetworkError || normalized.isTimeout;
1613
+ }
1614
+
1615
+ /**
1616
+ * Retries a fetch-like call, honoring Retry-After / X-RateLimit-Reset
1617
+ * headers on 429/503 responses instead of blind exponential backoff — most
1618
+ * generic retry libraries treat rate-limit responses like any other failure
1619
+ * and ignore the header the server is explicitly giving you.
1620
+ *
1621
+ * This is a thin preset over `withRetry` (retryStatusCodes + Retry-After,
1622
+ * no jitter, network errors/timeouts only for thrown errors). Reach for
1623
+ * `withRetry` directly when you need jitter, `maxRetryTime`, `StopRetry`,
1624
+ * `calculateDelay`, ...
1625
+ *
1626
+ * Also retries thrown network errors / timeouts (see `shouldRetryError`),
1627
+ * and the whole loop — including any backoff wait — can be cancelled with
1628
+ * `signal`. The body of each response that gets retried is cancelled so the
1629
+ * connection is released.
1630
+ *
1631
+ * `fn` should return a fetch Response (or anything with `.status` and
1632
+ * `.headers.get()`).
1633
+ *
1634
+ * @param {(attempt: number) => Promise<Response>} fn
1635
+ * @param {object} [opts]
1636
+ * @param {number} [opts.retries=3]
1637
+ * @param {number[]} [opts.retryStatusCodes=[429, 503]]
1638
+ * @param {number} [opts.fallbackDelayMs=1000] - used when no usable header is present
1639
+ * @param {number} [opts.maxDelayMs=60000]
1640
+ * @param {(status: number, waitMs: number, attempt: number) => void} [opts.onRetry]
1641
+ * @param {(err: any, attempt: number) => boolean} [opts.shouldRetryError] -
1642
+ * decides whether a thrown error is retried. Default: network errors and
1643
+ * timeouts only.
1644
+ * @param {(err: any, waitMs: number, attempt: number) => void} [opts.onRetryError]
1645
+ * @param {AbortSignal} [opts.signal] - cancels the retry loop, throwing
1646
+ * RetryAbortedError
1647
+ */
1648
+ function retryAfterAware(fn, opts = {}) {
1649
+ const {
1650
+ retries = 3,
1651
+ retryStatusCodes = [429, 503],
1652
+ fallbackDelayMs = 1000,
1653
+ maxDelayMs = 60000,
1654
+ onRetry,
1655
+ shouldRetryError = defaultShouldRetryError,
1656
+ onRetryError,
1657
+ signal,
1658
+ } = opts;
1659
+
1660
+ return withRetry(fn, {
1661
+ retries,
1662
+ retryStatusCodes,
1663
+ baseDelayMs: fallbackDelayMs,
1664
+ maxDelayMs,
1665
+ maxRetryAfterMs: maxDelayMs,
1666
+ factor: 2,
1667
+ jitter: 0,
1668
+ shouldRetry: shouldRetryError,
1669
+ onRetry: (err, attempt, waitMs, ctx) => {
1670
+ if (ctx.reason === "result") {
1671
+ if (onRetry) onRetry(ctx.status, waitMs, attempt);
1672
+ } else if (onRetryError) {
1673
+ onRetryError(err, waitMs, attempt);
1674
+ }
1675
+ },
1676
+ signal,
1677
+ });
1678
+ }
1679
+
1680
+ module.exports = { retryAfterAware, parseRetryAfter, getWaitMsFromHeaders };
1681
+
1682
+ },
1683
+ "./streamSSE": function (module, exports, require) {
1684
+ const { readSSE } = require("./streams");
1685
+ const { HTTPError } = require("./HTTPError");
1686
+ const { normalizeError } = require("./normalizeError");
1687
+ const { getWaitMsFromHeaders, discardBody } = require("./_http");
1688
+ const { abortableSleep } = require("./_sleep");
1689
+
1690
+ function isRetryableStatus(status) {
1691
+ return status === 408 || status === 425 || status === 429 || status >= 500;
1692
+ }
1693
+
1694
+ function isRetryableError(err) {
1695
+ const n = normalizeError(err);
1696
+ return !n.isAborted && (n.isNetworkError || n.isTimeout);
1697
+ }
1698
+
1699
+ async function resolveHeaders(source) {
1700
+ const value = typeof source === "function" ? await source() : source;
1701
+ return new Headers(value || undefined);
1702
+ }
1703
+
1704
+ /**
1705
+ * Connects to a Server-Sent Events endpoint and yields its events — and when
1706
+ * the connection drops (or the server closes it), reconnects by itself,
1707
+ * sending `Last-Event-ID` so the server can resume where you left off. This is
1708
+ * what the browser's `EventSource` does, but with `fetch`: custom headers,
1709
+ * POST bodies, AbortSignal, your own retry/backoff rules.
1710
+ *
1711
+ * ```js
1712
+ * for await (const { data } of streamSSE("https://api.example.com/events", { signal })) {
1713
+ * console.log(data);
1714
+ * }
1715
+ * ```
1716
+ *
1717
+ * Reconnect behavior:
1718
+ * - `Last-Event-ID` is sent on every reconnect (the last `id:` seen).
1719
+ * - A server `retry: <ms>` field sets the base reconnect delay, like EventSource.
1720
+ * - Waits back off exponentially (capped at `maxDelayMs`) while connecting keeps
1721
+ * failing; 429/503 `Retry-After` is honored. A connection that delivered at
1722
+ * least one event resets the failure count (only no-progress attempts count
1723
+ * toward `retries`).
1724
+ * - HTTP 204 means "stop reconnecting" (iteration ends). A non-retryable
1725
+ * status (e.g. 401/404) or a wrong content-type throws immediately.
1726
+ * - Only GET reconnects by default. For other methods (e.g. a POST that starts
1727
+ * an LLM stream) re-sending the request could repeat side effects, so
1728
+ * nothing is retried unless you pass `reconnect: true`.
1729
+ * - Breaking out of the loop, or aborting `signal`, closes the connection.
1730
+ *
1731
+ * @param {string} url
1732
+ * @param {object} [opts]
1733
+ * @param {typeof fetch} [opts.fetch=globalThis.fetch] - e.g. a createFetch() instance
1734
+ * @param {string} [opts.method="GET"]
1735
+ * @param {HeadersInit | (() => HeadersInit | Promise<HeadersInit>)} [opts.headers] -
1736
+ * a function is called before every (re)connect — handy for refreshing a token
1737
+ * @param {BodyInit} [opts.body]
1738
+ * @param {AbortSignal} [opts.signal]
1739
+ * @param {string} [opts.lastEventId] - resume from here on the very first connect
1740
+ * @param {boolean} [opts.reconnect] - default: true for GET/HEAD, false otherwise
1741
+ * @param {number} [opts.retries=10] - consecutive failed connections before giving up (Infinity = never)
1742
+ * @param {number} [opts.reconnectDelayMs=1000] - base delay (the server's `retry:` overrides it)
1743
+ * @param {number} [opts.maxDelayMs=30000]
1744
+ * @param {(event: object) => boolean} [opts.stopWhen] - end the stream (no reconnect) after this event, e.g. `e => e.data === "[DONE]"`
1745
+ * @param {(info: {connections: number, lastEventId: string}) => void} [opts.onOpen]
1746
+ * @param {(info: {reason: "error"|"end"|"status", error?: any, status?: number, attempt: number, delayMs: number, lastEventId: string}) => void} [opts.onReconnect]
1747
+ * @returns {AsyncGenerator<{event: string, data: string, id: string, retry: number|undefined}>}
1748
+ */
1749
+ async function* streamSSE(url, opts = {}) {
1750
+ const {
1751
+ fetch: fetchFn = globalThis.fetch,
1752
+ method = "GET",
1753
+ headers,
1754
+ body,
1755
+ signal,
1756
+ reconnect = method === "GET" || method === "HEAD",
1757
+ retries = 10,
1758
+ reconnectDelayMs = 1000,
1759
+ maxDelayMs = 30000,
1760
+ stopWhen,
1761
+ onOpen,
1762
+ onReconnect,
1763
+ } = opts;
1764
+
1765
+ let lastId = opts.lastEventId || "";
1766
+ let baseDelay = reconnectDelayMs;
1767
+ let failures = 0; // consecutive connections that delivered nothing
1768
+ let connections = 0;
1769
+
1770
+ for (;;) {
1771
+ signal?.throwIfAborted?.();
1772
+ let failure = null; // { reason, error?, status?, response? }
1773
+ let res = null;
1774
+
1775
+ // ---- connect -----------------------------------------------------------
1776
+ try {
1777
+ const h = await resolveHeaders(headers);
1778
+ if (!h.has("accept")) h.set("accept", "text/event-stream");
1779
+ if (!h.has("cache-control")) h.set("cache-control", "no-cache");
1780
+ if (lastId) h.set("last-event-id", lastId);
1781
+ res = await fetchFn(url, { method, headers: h, body, signal });
1782
+ } catch (err) {
1783
+ if (signal?.aborted) throw signal.reason ?? err;
1784
+ if (err instanceof HTTPError) {
1785
+ if (!isRetryableStatus(err.status)) throw err;
1786
+ failure = { reason: "status", status: err.status, response: err.response, error: err };
1787
+ } else if (isRetryableError(err)) {
1788
+ failure = { reason: "error", error: err };
1789
+ } else {
1790
+ throw err;
1791
+ }
1792
+ }
1793
+
1794
+ // ---- inspect the response ---------------------------------------------
1795
+ if (!failure) {
1796
+ if (res.status === 204) {
1797
+ await discardBody(res);
1798
+ return; // the server's way of saying "don't reconnect"
1799
+ }
1800
+ if (!res.ok) {
1801
+ if (!isRetryableStatus(res.status)) {
1802
+ await discardBody(res);
1803
+ throw new HTTPError(res, { method, url });
1804
+ }
1805
+ failure = { reason: "status", status: res.status, response: res };
1806
+ } else {
1807
+ const type = res.headers.get("content-type") || "";
1808
+ if (!/text\/event-stream/i.test(type)) {
1809
+ await discardBody(res);
1810
+ throw Object.assign(new Error(`streamSSE: expected "text/event-stream" but the server sent "${type || "no content-type"}"`), {
1811
+ code: "SSE_BAD_CONTENT_TYPE",
1812
+ });
1813
+ }
1814
+ }
1815
+ }
1816
+
1817
+ // ---- stream ------------------------------------------------------------
1818
+ let gotEvent = false;
1819
+ if (!failure) {
1820
+ connections++;
1821
+ if (onOpen) onOpen({ connections, lastEventId: lastId });
1822
+ try {
1823
+ for await (const event of readSSE(res, { signal })) {
1824
+ gotEvent = true;
1825
+ lastId = event.id;
1826
+ if (event.retry !== undefined) baseDelay = event.retry;
1827
+ yield event;
1828
+ if (stopWhen && stopWhen(event)) return;
1829
+ }
1830
+ failure = { reason: "end" };
1831
+ } catch (err) {
1832
+ if (signal?.aborted) throw signal.reason ?? err;
1833
+ if (!isRetryableError(err)) throw err;
1834
+ failure = { reason: "error", error: err };
1835
+ }
1836
+ }
1837
+
1838
+ // ---- decide whether and when to reconnect ------------------------------
1839
+ if (!reconnect) {
1840
+ if (failure.reason === "end") return;
1841
+ if (failure.reason === "status") {
1842
+ throw failure.error || new HTTPError(failure.response, { method, url });
1843
+ }
1844
+ throw failure.error;
1845
+ }
1846
+
1847
+ // A connection that delivered events made progress: reconnect promptly and
1848
+ // start the failure count over. Only no-progress attempts count toward `retries`.
1849
+ failures = gotEvent ? 0 : failures + 1;
1850
+ if (failures > retries) {
1851
+ if (failure.reason === "status") throw failure.error || new HTTPError(failure.response, { method, url });
1852
+ if (failure.reason === "error") throw failure.error;
1853
+ throw Object.assign(new Error(`streamSSE: the server keeps closing the stream without sending events — gave up after ${retries} retries`), {
1854
+ code: "SSE_GAVE_UP",
1855
+ });
1856
+ }
1857
+
1858
+ let delayMs = Math.min(baseDelay * 2 ** Math.max(failures - 1, 0), maxDelayMs);
1859
+ if (failure.reason === "status" && failure.response) {
1860
+ const headerMs = getWaitMsFromHeaders(failure.response.headers);
1861
+ if (headerMs !== null) delayMs = Math.min(headerMs, 60000);
1862
+ await discardBody(failure.response);
1863
+ }
1864
+ delayMs += Math.random() * delayMs * 0.1; // a little jitter so clients don't reconnect in lockstep
1865
+
1866
+ if (onReconnect) {
1867
+ onReconnect({
1868
+ reason: failure.reason,
1869
+ error: failure.error,
1870
+ status: failure.status,
1871
+ attempt: Math.max(failures, 1),
1872
+ delayMs,
1873
+ lastEventId: lastId,
1874
+ });
1875
+ }
1876
+ await abortableSleep(delayMs, signal);
1877
+ }
1878
+ }
1879
+
1880
+ module.exports = { streamSSE };
1881
+
1882
+ },
1883
+ "./streams": function (module, exports, require) {
1884
+ const encoder = new TextEncoder();
1885
+
1886
+ // Yields Uint8Array chunks from a fetch Response, a web ReadableStream, or any
1887
+ // async iterable (e.g. a Node Readable). Cancels the underlying stream when
1888
+ // the consumer stops early or the signal aborts, so the connection is freed.
1889
+ async function* chunks(source, signal) {
1890
+ let body = source;
1891
+ if (body && typeof body.getReader !== "function" && "body" in body) body = body.body; // a Response
1892
+ if (body === null || body === undefined) return;
1893
+ signal?.throwIfAborted?.();
1894
+
1895
+ if (typeof body.getReader === "function") {
1896
+ const reader = body.getReader();
1897
+ const onAbort = () => reader.cancel(signal.reason).catch(() => {});
1898
+ signal?.addEventListener("abort", onAbort, { once: true });
1899
+ try {
1900
+ for (;;) {
1901
+ const { done, value } = await reader.read();
1902
+ signal?.throwIfAborted?.(); // cancel() makes read() resolve as done — don't mistake that for the end
1903
+ if (done) return;
1904
+ yield value;
1905
+ }
1906
+ } finally {
1907
+ signal?.removeEventListener("abort", onAbort);
1908
+ reader.cancel().catch(() => {});
1909
+ }
1910
+ } else if (typeof body[Symbol.asyncIterator] === "function") {
1911
+ for await (const chunk of body) {
1912
+ signal?.throwIfAborted?.();
1913
+ yield typeof chunk === "string" ? encoder.encode(chunk) : chunk;
1914
+ }
1915
+ } else {
1916
+ throw new TypeError("reqkit streams: expected a Response, a ReadableStream or an async iterable");
1917
+ }
1918
+ }
1919
+
1920
+ /**
1921
+ * Async-iterates the text lines of a streamed body. Handles \n, \r\n and \r,
1922
+ * multi-byte characters split across chunks, and a leading BOM.
1923
+ *
1924
+ * ```js
1925
+ * for await (const line of readLines(response)) console.log(line);
1926
+ * ```
1927
+ *
1928
+ * @param {Response | ReadableStream | AsyncIterable<Uint8Array>} source
1929
+ * @param {{ signal?: AbortSignal }} [opts]
1930
+ * @returns {AsyncGenerator<string>}
1931
+ */
1932
+ async function* readLines(source, opts = {}) {
1933
+ const decoder = new TextDecoder("utf-8");
1934
+ let buffer = "";
1935
+ let first = true;
1936
+
1937
+ for await (const chunk of chunks(source, opts.signal)) {
1938
+ buffer += decoder.decode(chunk, { stream: true });
1939
+ if (first && buffer.length > 0) {
1940
+ buffer = buffer.replace(/^\uFEFF/, "");
1941
+ first = false;
1942
+ }
1943
+
1944
+ let pos = 0;
1945
+ for (;;) {
1946
+ const rest = buffer.slice(pos);
1947
+ const i = rest.search(/\r\n|\n|\r/);
1948
+ if (i === -1) break;
1949
+ // A trailing "\r" might be the first half of "\r\n" — wait for more data.
1950
+ if (rest[i] === "\r" && i === rest.length - 1) break;
1951
+ yield rest.slice(0, i);
1952
+ pos += i + (rest.startsWith("\r\n", i) ? 2 : 1);
1953
+ }
1954
+ buffer = buffer.slice(pos);
1955
+ }
1956
+
1957
+ buffer += decoder.decode();
1958
+ if (buffer.endsWith("\r")) buffer = buffer.slice(0, -1);
1959
+ if (buffer.length > 0) yield buffer;
1960
+ }
1961
+
1962
+ /**
1963
+ * Async-iterates parsed JSON values from a newline-delimited JSON stream
1964
+ * (NDJSON / JSON Lines). Blank lines are skipped. A malformed line throws a
1965
+ * SyntaxError naming the line, unless `skipInvalid` is true.
1966
+ *
1967
+ * @param {Response | ReadableStream | AsyncIterable<Uint8Array>} source
1968
+ * @param {{ signal?: AbortSignal, skipInvalid?: boolean }} [opts]
1969
+ */
1970
+ async function* readNDJSON(source, opts = {}) {
1971
+ let lineNo = 0;
1972
+ for await (const line of readLines(source, opts)) {
1973
+ lineNo++;
1974
+ if (line.trim() === "") continue;
1975
+ try {
1976
+ yield JSON.parse(line);
1977
+ } catch (err) {
1978
+ if (opts.skipInvalid) continue;
1979
+ throw new SyntaxError(`readNDJSON: invalid JSON on line ${lineNo}: ${err.message}`);
1980
+ }
1981
+ }
1982
+ }
1983
+
1984
+ /**
1985
+ * Async-iterates Server-Sent Events (text/event-stream) as
1986
+ * `{ event, data, id, retry }`, following the spec: multi-line `data:` is
1987
+ * joined with "\n", comments (`:`) are ignored, `event` defaults to
1988
+ * "message", `id` carries over between events, and an event cut off by the end
1989
+ * of the stream is discarded. Great for LLM token streams.
1990
+ *
1991
+ * ```js
1992
+ * for await (const { data } of readSSE(response)) {
1993
+ * if (data === "[DONE]") break;
1994
+ * console.log(JSON.parse(data));
1995
+ * }
1996
+ * ```
1997
+ *
1998
+ * @param {Response | ReadableStream | AsyncIterable<Uint8Array>} source
1999
+ * @param {{ signal?: AbortSignal }} [opts]
2000
+ */
2001
+ async function* readSSE(source, opts = {}) {
2002
+ let event = "";
2003
+ let data = [];
2004
+ let retry;
2005
+ let lastId = "";
2006
+
2007
+ for await (const line of readLines(source, opts)) {
2008
+ if (line === "") {
2009
+ if (data.length > 0) yield { event: event || "message", data: data.join("\n"), id: lastId, retry };
2010
+ event = "";
2011
+ data = [];
2012
+ retry = undefined;
2013
+ continue;
2014
+ }
2015
+ if (line.startsWith(":")) continue;
2016
+
2017
+ const idx = line.indexOf(":");
2018
+ const field = idx === -1 ? line : line.slice(0, idx);
2019
+ let value = idx === -1 ? "" : line.slice(idx + 1);
2020
+ if (value.startsWith(" ")) value = value.slice(1);
2021
+
2022
+ if (field === "event") event = value;
2023
+ else if (field === "data") data.push(value);
2024
+ else if (field === "id") {
2025
+ if (!value.includes("\0")) lastId = value;
2026
+ } else if (field === "retry") {
2027
+ if (/^\d+$/.test(value)) retry = Number(value);
2028
+ }
2029
+ }
2030
+ }
2031
+
2032
+ module.exports = { readLines, readNDJSON, readSSE };
2033
+
2034
+ },
2035
+ "./withDedupe": function (module, exports, require) {
2036
+ // Non-plain objects (AbortSignal, Response, class instances, functions...)
2037
+ // can't be meaningfully JSON-serialized — JSON.stringify turns every
2038
+ // AbortSignal into "{}", which would make unrelated calls share one request.
2039
+ // Give them a stable identity-based id instead.
2040
+ const identityIds = new WeakMap();
2041
+ let nextIdentityId = 1;
2042
+
2043
+ function identityOf(value) {
2044
+ let id = identityIds.get(value);
2045
+ if (id === undefined) {
2046
+ id = nextIdentityId++;
2047
+ identityIds.set(value, id);
2048
+ }
2049
+ return id;
2050
+ }
2051
+
2052
+ function isPlainObject(value) {
2053
+ const proto = Object.getPrototypeOf(value);
2054
+ return proto === Object.prototype || proto === null;
2055
+ }
2056
+
2057
+ function keyReplacer(_key, value) {
2058
+ if (typeof value === "bigint") return { $bigint: String(value) };
2059
+ if (typeof value === "function") return { $ref: identityOf(value) };
2060
+ if (value === null || typeof value !== "object") return value;
2061
+ if (Array.isArray(value)) return value;
2062
+ if (!isPlainObject(value)) return { $ref: identityOf(value) };
2063
+
2064
+ // Sort plain-object keys so {a:1,b:2} and {b:2,a:1} dedupe together.
2065
+ const sorted = {};
2066
+ for (const k of Object.keys(value).sort()) sorted[k] = value[k];
2067
+ return sorted;
2068
+ }
2069
+
2070
+ function defaultKeyFn(...args) {
2071
+ try {
2072
+ return JSON.stringify(args, keyReplacer);
2073
+ } catch {
2074
+ // Circular structures etc. Fall back to identity for objects.
2075
+ return args
2076
+ .map((a) => ((a && typeof a === "object") || typeof a === "function" ? `#${identityOf(a)}` : String(a)))
2077
+ .join("|");
2078
+ }
2079
+ }
2080
+
2081
+ /**
2082
+ * Wraps an async function so that concurrent calls with the same key share
2083
+ * a single in-flight promise instead of firing duplicate requests. Common
2084
+ * use case: several parts of a UI ask for the same resource at once (e.g. a
2085
+ * user profile) — only one network request actually goes out, and every
2086
+ * caller resolves with the same result.
2087
+ *
2088
+ * The in-flight entry is cleared as soon as the call settles (success or
2089
+ * failure), so the next call — even with the same key — always starts a
2090
+ * fresh request. This is deduplication of concurrent calls, not a cache.
2091
+ *
2092
+ * @param {(...args: any[]) => Promise<any>} fn
2093
+ * @param {object} [opts]
2094
+ * @param {(...args: any[]) => string} [opts.keyFn] - derives a dedupe key
2095
+ * from the call arguments. By default plain data is compared by value
2096
+ * (object key order doesn't matter) and anything else — AbortSignals,
2097
+ * Responses, class instances, functions — by identity, so two calls with
2098
+ * different AbortSignals are never merged.
2099
+ * @returns {(...args: any[]) => Promise<any>}
2100
+ */
2101
+ function withDedupe(fn, opts = {}) {
2102
+ const { keyFn = defaultKeyFn } = opts;
2103
+ const inFlight = new Map();
2104
+
2105
+ return function deduped(...args) {
2106
+ const key = keyFn(...args);
2107
+
2108
+ if (inFlight.has(key)) {
2109
+ return inFlight.get(key);
2110
+ }
2111
+
2112
+ const promise = Promise.resolve()
2113
+ .then(() => fn(...args))
2114
+ .finally(() => {
2115
+ inFlight.delete(key);
2116
+ });
2117
+
2118
+ inFlight.set(key, promise);
2119
+ return promise;
2120
+ };
2121
+ }
2122
+
2123
+ module.exports = { withDedupe };
2124
+
2125
+ },
2126
+ "./withProgress": function (module, exports, require) {
2127
+ class BodyTooLargeError extends Error {
2128
+ constructor(maxBytes) {
2129
+ super(`Response body exceeded the ${maxBytes}-byte limit`);
2130
+ this.name = "BodyTooLargeError";
2131
+ this.code = "EBODYTOOLARGE";
2132
+ this.maxBytes = maxBytes;
2133
+ }
2134
+ }
2135
+
2136
+ function cancelQuietly(reader) {
2137
+ try {
2138
+ const p = reader.cancel();
2139
+ if (p && typeof p.catch === "function") p.catch(() => {});
2140
+ } catch {
2141
+ // already closed / released — nothing to do
2142
+ }
2143
+ }
2144
+
2145
+ /**
2146
+ * Reads a fetch Response body while reporting progress, and returns the
2147
+ * fully collected bytes as a Uint8Array. Works for any streamed response,
2148
+ * not just file downloads — same shape whether it's a media file, a large
2149
+ * JSON payload, or anything else worth showing progress for.
2150
+ *
2151
+ * `total` is null (and `percent` with it) when the size isn't trustworthy:
2152
+ * no Content-Length, or a Content-Encoding is applied (Content-Length is then
2153
+ * the *compressed* size while fetch hands you *decompressed* bytes, so
2154
+ * loaded could exceed total). `percent` is always clamped to 0–100.
2155
+ *
2156
+ * @param {Response} response
2157
+ * @param {(progress: { loaded: number, total: number | null, percent: number | null }) => void} [onProgress]
2158
+ * @param {object} [opts]
2159
+ * @param {number} [opts.maxBytes] - abort with BodyTooLargeError once more
2160
+ * than this many bytes have been received
2161
+ * @param {AbortSignal} [opts.signal] - cancels the read; rejects with the
2162
+ * signal's reason (an AbortError by default)
2163
+ * @returns {Promise<Uint8Array>}
2164
+ */
2165
+ async function withProgress(response, onProgress, opts = {}) {
2166
+ const { maxBytes, signal } = opts;
2167
+ signal?.throwIfAborted?.();
2168
+
2169
+ if (!response.body || typeof response.body.getReader !== "function") {
2170
+ // No streaming support available — fall back to a single-shot read,
2171
+ // still reporting one final progress event so callers don't need a branch.
2172
+ const buf = new Uint8Array(await response.arrayBuffer());
2173
+ signal?.throwIfAborted?.();
2174
+ if (maxBytes !== undefined && buf.byteLength > maxBytes) {
2175
+ throw new BodyTooLargeError(maxBytes);
2176
+ }
2177
+ if (onProgress) {
2178
+ onProgress({ loaded: buf.byteLength, total: buf.byteLength, percent: 100 });
2179
+ }
2180
+ return buf;
2181
+ }
2182
+
2183
+ const encoding = response.headers?.get?.("content-encoding");
2184
+ const encoded = Boolean(encoding) && encoding.trim().toLowerCase() !== "identity";
2185
+ const contentLength = response.headers?.get?.("content-length");
2186
+ const parsed = contentLength ? Number(contentLength) : NaN;
2187
+ const total = !encoded && Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
2188
+
2189
+ const reader = response.body.getReader();
2190
+
2191
+ // Fail fast when the declared size already exceeds the cap.
2192
+ if (maxBytes !== undefined && total !== null && total > maxBytes) {
2193
+ cancelQuietly(reader);
2194
+ throw new BodyTooLargeError(maxBytes);
2195
+ }
2196
+
2197
+ const onAbort = () => cancelQuietly(reader);
2198
+ signal?.addEventListener("abort", onAbort, { once: true });
2199
+
2200
+ const chunks = [];
2201
+ let loaded = 0;
2202
+ let finished = false;
2203
+
2204
+ try {
2205
+ for (;;) {
2206
+ const { done, value } = await reader.read();
2207
+ // reader.cancel() on abort makes read() resolve as done — check the
2208
+ // signal first so an aborted read never looks like a complete one.
2209
+ signal?.throwIfAborted?.();
2210
+ if (done) break;
2211
+
2212
+ chunks.push(value);
2213
+ loaded += value.byteLength;
2214
+
2215
+ if (maxBytes !== undefined && loaded > maxBytes) {
2216
+ throw new BodyTooLargeError(maxBytes);
2217
+ }
2218
+
2219
+ if (onProgress) {
2220
+ onProgress({
2221
+ loaded,
2222
+ total,
2223
+ percent: total ? Math.min(100, Math.round((loaded / total) * 100)) : null,
2224
+ });
2225
+ }
2226
+ }
2227
+ finished = true;
2228
+ } finally {
2229
+ signal?.removeEventListener("abort", onAbort);
2230
+ if (!finished) cancelQuietly(reader);
2231
+ }
2232
+
2233
+ const result = new Uint8Array(loaded);
2234
+ let offset = 0;
2235
+ for (const chunk of chunks) {
2236
+ result.set(chunk, offset);
2237
+ offset += chunk.byteLength;
2238
+ }
2239
+ return result;
2240
+ }
2241
+
2242
+ module.exports = { withProgress, BodyTooLargeError };
2243
+
2244
+ },
2245
+ "./withRetry": function (module, exports, require) {
2246
+ const { RetryAbortedError, sleep } = require("./_sleep");
2247
+ const { getWaitMsFromHeaders, discardBody } = require("./_http");
2248
+
2249
+ function defaultShouldRetry(err) {
2250
+ // Retry thrown errors except deliberate aborts. Narrow it with `shouldRetry`
2251
+ // (e.g. network errors only). HTTP *statuses* aren't errors with fetch, so
2252
+ // opt in with `retryStatusCodes` / `shouldRetryResult`.
2253
+ return Boolean(err) && err.name !== "AbortError";
2254
+ }
2255
+
2256
+ /**
2257
+ * Throw this from inside `fn` to stop retrying immediately. If you pass an
2258
+ * Error, that exact error is what withRetry rethrows; with a string, the
2259
+ * StopRetry itself is thrown. (Same idea as p-retry's AbortError.)
2260
+ */
2261
+ class StopRetry extends Error {
2262
+ constructor(reasonOrMessage) {
2263
+ const isError = reasonOrMessage instanceof Error;
2264
+ super(isError ? reasonOrMessage.message : String(reasonOrMessage ?? "Retrying was stopped"));
2265
+ this.name = "StopRetry";
2266
+ this.original = isError ? reasonOrMessage : null;
2267
+ }
2268
+ }
2269
+
2270
+ /**
2271
+ * Retries an async function with exponential backoff + jitter. Retries on
2272
+ * thrown errors, and — opt-in — on *results* too (HTTP status codes, or any
2273
+ * predicate), honoring Retry-After / X-RateLimit-Reset when the result has
2274
+ * headers.
2275
+ *
2276
+ * @param {(attempt: number) => Promise<any>} fn
2277
+ * @param {object} [opts]
2278
+ * @param {number} [opts.retries=3] - max retry attempts after the first try (Infinity is fine with maxRetryTime)
2279
+ * @param {number} [opts.baseDelayMs=300] - initial delay
2280
+ * @param {number} [opts.maxDelayMs=10000] - backoff ceiling
2281
+ * @param {number} [opts.factor=2] - backoff multiplier
2282
+ * @param {number|"full"} [opts.jitter=0.2] - fraction of extra random delay
2283
+ * added on top (0 disables); "full" picks uniformly in [0, delay] — the
2284
+ * AWS "full jitter" strategy, best when many clients retry at once
2285
+ * @param {number} [opts.maxRetryTime=Infinity] - total time budget in ms
2286
+ * across all attempts and waits; once the next wait would blow it, stop
2287
+ * and surface the last error/result
2288
+ * @param {(err: any, attempt: number) => boolean} [opts.shouldRetry] - for thrown errors
2289
+ * @param {number[]} [opts.retryStatusCodes=[]] - retry when the result's `.status` is in this list
2290
+ * @param {(result: any, attempt: number) => boolean} [opts.shouldRetryResult] - retry on any result predicate
2291
+ * @param {boolean} [opts.respectRetryAfter=true] - use Retry-After/X-RateLimit-Reset from `result.headers`
2292
+ * @param {number} [opts.maxRetryAfterMs=60000] - cap on a header-provided wait
2293
+ * @param {(info: {attempt: number, error: any, result: any, delayMs: number}) => number|false} [opts.calculateDelay]
2294
+ * - override the wait; return a number of ms, or false to stop retrying
2295
+ * @param {(err: any, attempt: number, delayMs: number, ctx: {reason: "error"|"result", result?: any, status?: number}) => void} [opts.onRetry]
2296
+ * - `err` is undefined when the retry was triggered by a result
2297
+ * @param {AbortSignal} [opts.signal] - cancels the whole retry loop,
2298
+ * including any pending backoff wait, throwing RetryAbortedError (with the
2299
+ * last failure as `cause`). Separate from a per-call signal you pass into
2300
+ * `fn` yourself — this one governs whether *another attempt starts at all*.
2301
+ */
2302
+ async function withRetry(fn, opts = {}) {
2303
+ const {
2304
+ retries = 3,
2305
+ baseDelayMs = 300,
2306
+ maxDelayMs = 10000,
2307
+ factor = 2,
2308
+ jitter = 0.2,
2309
+ maxRetryTime = Infinity,
2310
+ shouldRetry = defaultShouldRetry,
2311
+ retryStatusCodes = [],
2312
+ shouldRetryResult,
2313
+ respectRetryAfter = true,
2314
+ maxRetryAfterMs = 60000,
2315
+ calculateDelay,
2316
+ onRetry,
2317
+ signal,
2318
+ } = opts;
2319
+
2320
+ const startedAt = Date.now();
2321
+
2322
+ function backoff(attempt) {
2323
+ const exponential = Math.min(baseDelayMs * factor ** attempt, maxDelayMs);
2324
+ if (jitter === "full") return Math.round(Math.random() * exponential);
2325
+ const extra = Math.random() * exponential * (Number(jitter) || 0);
2326
+ return Math.round(exponential + extra);
2327
+ }
2328
+
2329
+ // Returns the wait in ms, or null if retrying should stop (custom
2330
+ // calculateDelay said so, or the time budget would be exceeded).
2331
+ function planWait(defaultMs, attempt, error, result) {
2332
+ let waitMs = defaultMs;
2333
+ if (calculateDelay) {
2334
+ const custom = calculateDelay({ attempt, error, result, delayMs: defaultMs });
2335
+ if (custom === false || custom === null || custom === undefined) return null;
2336
+ waitMs = Math.max(0, Number(custom) || 0);
2337
+ }
2338
+ if (Date.now() - startedAt + waitMs > maxRetryTime) return null;
2339
+ return waitMs;
2340
+ }
2341
+
2342
+ function wantsResultRetry(result, attempt) {
2343
+ if (
2344
+ retryStatusCodes.length > 0 &&
2345
+ result &&
2346
+ typeof result.status === "number" &&
2347
+ retryStatusCodes.includes(result.status)
2348
+ ) {
2349
+ return true;
2350
+ }
2351
+ return shouldRetryResult ? Boolean(shouldRetryResult(result, attempt)) : false;
2352
+ }
2353
+
2354
+ let lastErr;
2355
+ for (let attempt = 0; ; attempt++) {
2356
+ if (signal?.aborted) {
2357
+ throw new RetryAbortedError(lastErr);
2358
+ }
2359
+
2360
+ let result;
2361
+ try {
2362
+ result = await fn(attempt);
2363
+ } catch (err) {
2364
+ if (err instanceof StopRetry) throw err.original ?? err;
2365
+ lastErr = err;
2366
+ if (attempt >= retries || !shouldRetry(err, attempt)) throw err;
2367
+
2368
+ const waitMs = planWait(backoff(attempt), attempt, err, undefined);
2369
+ if (waitMs === null) throw err;
2370
+
2371
+ if (onRetry) onRetry(err, attempt, waitMs, { reason: "error" });
2372
+ await sleep(waitMs, signal, err);
2373
+ continue;
2374
+ }
2375
+
2376
+ if (attempt >= retries || !wantsResultRetry(result, attempt)) return result;
2377
+
2378
+ let defaultMs = backoff(attempt);
2379
+ if (respectRetryAfter) {
2380
+ const headerMs = getWaitMsFromHeaders(result && result.headers);
2381
+ if (headerMs !== null) defaultMs = Math.min(headerMs, maxRetryAfterMs);
2382
+ }
2383
+ const waitMs = planWait(defaultMs, attempt, undefined, result);
2384
+ if (waitMs === null) return result;
2385
+
2386
+ await discardBody(result);
2387
+ if (onRetry) onRetry(undefined, attempt, waitMs, { reason: "result", result, status: result && result.status });
2388
+ await sleep(waitMs, signal);
2389
+ }
2390
+ }
2391
+
2392
+ module.exports = { withRetry, RetryAbortedError, StopRetry };
2393
+
2394
+ },
2395
+ "./withTimeout": function (module, exports, require) {
2396
+ class TimeoutError extends Error {
2397
+ constructor(ms) {
2398
+ super(`Timed out after ${ms}ms`);
2399
+ this.name = "TimeoutError";
2400
+ this.code = "ETIMEDOUT";
2401
+ }
2402
+ }
2403
+
2404
+ /**
2405
+ * Races an async function against a timeout. `fn` is handed an AbortSignal
2406
+ * so the underlying request can actually be cancelled (not just abandoned) —
2407
+ * e.g. `withTimeout(signal => fetch(url, { signal }), 5000)`.
2408
+ *
2409
+ * Pass your own `signal` to also cancel from the outside (user navigated
2410
+ * away, shutdown...). That signal is linked into the one `fn` receives, and
2411
+ * aborting it rejects with its reason even if `fn` ignores the signal.
2412
+ * `ms: Infinity` disables the timer (handy to only link a signal).
2413
+ *
2414
+ * @param {(signal: AbortSignal) => Promise<any>} fn
2415
+ * @param {number} ms
2416
+ * @param {object} [opts]
2417
+ * @param {AbortSignal} [opts.signal] - external cancellation
2418
+ */
2419
+ async function withTimeout(fn, ms, opts = {}) {
2420
+ const { signal } = opts;
2421
+
2422
+ if (typeof ms !== "number" || Number.isNaN(ms) || ms < 0) {
2423
+ throw new TypeError("withTimeout: `ms` must be a non-negative number (or Infinity)");
2424
+ }
2425
+ signal?.throwIfAborted?.();
2426
+
2427
+ const controller = new AbortController();
2428
+ let timedOut = false;
2429
+
2430
+ const timer =
2431
+ ms === Infinity
2432
+ ? null
2433
+ : setTimeout(() => {
2434
+ timedOut = true;
2435
+ controller.abort(new TimeoutError(ms));
2436
+ }, ms);
2437
+
2438
+ const onExternalAbort = () => controller.abort(signal.reason);
2439
+ signal?.addEventListener("abort", onExternalAbort, { once: true });
2440
+
2441
+ const guard = new Promise((_, reject) => {
2442
+ controller.signal.addEventListener(
2443
+ "abort",
2444
+ () => reject(timedOut ? new TimeoutError(ms) : controller.signal.reason),
2445
+ { once: true }
2446
+ );
2447
+ });
2448
+
2449
+ try {
2450
+ return await Promise.race([fn(controller.signal), guard]);
2451
+ } catch (err) {
2452
+ // If our timeout is what triggered the abort, always surface TimeoutError —
2453
+ // regardless of whether fn's own rejection or our guard "won" the race
2454
+ // (that's a listener-registration-order detail callers shouldn't have to
2455
+ // think about).
2456
+ if (timedOut) throw new TimeoutError(ms);
2457
+ throw err;
2458
+ } finally {
2459
+ if (timer) clearTimeout(timer);
2460
+ signal?.removeEventListener("abort", onExternalAbort);
2461
+ }
2462
+ }
2463
+
2464
+ module.exports = { withTimeout, TimeoutError };
2465
+
2466
+ },
2467
+ };
2468
+ const __cache = {};
2469
+ function __require(id) {
2470
+ const key = id.replace(/\.js$/, "");
2471
+ if (!(key in __defs)) throw new Error(`reqkit bundle: cannot resolve ${id}`);
2472
+ if (__cache[key]) return __cache[key].exports;
2473
+ const module = (__cache[key] = { exports: {} });
2474
+ __defs[key].call(module.exports, module, module.exports, __require);
2475
+ return module.exports;
2476
+ }
2477
+ const __reqkit = __require("./index");
2478
+ export const {
2479
+ BodyTooLargeError,
2480
+ CircuitOpenError,
2481
+ HTTPError,
2482
+ RetryAbortedError,
2483
+ StopRetry,
2484
+ TimeoutError,
2485
+ buildQuery,
2486
+ createCircuitBreaker,
2487
+ createFetch,
2488
+ createLimiter,
2489
+ createMemoryCache,
2490
+ createMockFetch,
2491
+ createRateLimiter,
2492
+ downloadResumable,
2493
+ getWaitMsFromHeaders,
2494
+ normalizeError,
2495
+ parseJsonSafe,
2496
+ parseRetryAfter,
2497
+ readLines,
2498
+ readNDJSON,
2499
+ readSSE,
2500
+ retryAfterAware,
2501
+ streamSSE,
2502
+ withDedupe,
2503
+ withProgress,
2504
+ withRetry,
2505
+ withTimeout,
2506
+ } = __reqkit;
2507
+ export default __reqkit;