@boy-offi9-inc/reqkit 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -66
- package/dist/reqkit.mjs +2136 -2014
- package/package.json +9 -5
- package/src/{createFetch.js → http/createFetch.js} +130 -22
- package/src/index.js +18 -18
- package/src/{paginate.js → pagination/paginate.js} +2 -2
- package/src/{createRateLimiter.js → resilience/rateLimiter.js} +7 -4
- package/src/{withRetry.js → resilience/retry.js} +2 -2
- package/src/{retryAfterAware.js → resilience/retryAfterAware.js} +3 -3
- package/src/{downloadResumable.js → streams/download.js} +5 -5
- package/src/{streamSSE.js → streams/streamSSE.js} +5 -5
- package/src/{createMockFetch.js → testing/createMockFetch.js} +1 -1
- package/types/errors.d.ts +24 -0
- package/types/http.d.ts +180 -0
- package/types/index.d.ts +7 -538
- package/types/pagination.d.ts +62 -0
- package/types/resilience.d.ts +171 -0
- package/types/streams.d.ts +105 -0
- package/types/testing.d.ts +44 -0
- /package/{RECIPES.md → docs/recipes.md} +0 -0
- /package/src/{HTTPError.js → errors/HTTPError.js} +0 -0
- /package/src/{normalizeError.js → errors/normalizeError.js} +0 -0
- /package/src/{buildQuery.js → http/buildQuery.js} +0 -0
- /package/src/{_cache.js → http/cache/entry.js} +0 -0
- /package/src/{createMemoryCache.js → http/cache/memoryCache.js} +0 -0
- /package/src/{parseJsonSafe.js → http/parseJsonSafe.js} +0 -0
- /package/src/{_http.js → internal/http.js} +0 -0
- /package/src/{_sleep.js → internal/sleep.js} +0 -0
- /package/src/{createCircuitBreaker.js → resilience/circuitBreaker.js} +0 -0
- /package/src/{withDedupe.js → resilience/dedupe.js} +0 -0
- /package/src/{createLimiter.js → resilience/limiter.js} +0 -0
- /package/src/{withTimeout.js → resilience/timeout.js} +0 -0
- /package/src/{withProgress.js → streams/progress.js} +0 -0
- /package/src/{streams.js → streams/readers.js} +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boy-offi9-inc/reqkit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Small, composable HTTP helpers
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Small, composable HTTP helpers \u2014 retry with backoff and Retry-After, timeouts, normalized errors, rate limiter, circuit breaker, response cache, resumable downloads, SSE/NDJSON streams, mock fetch, and an opt-in createFetch. Not a client; works alongside fetch/axios/anything.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"types": "./types/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"test": "node --test",
|
|
28
28
|
"typecheck": "tsc -p tsconfig.json",
|
|
29
29
|
"smoke": "node smoke/run.mjs",
|
|
30
|
-
"
|
|
30
|
+
"check": "npm run build && npm test && npm run typecheck && npm run smoke",
|
|
31
|
+
"prepublishOnly": "npm run check"
|
|
31
32
|
},
|
|
32
33
|
"keywords": [
|
|
33
34
|
"http",
|
|
@@ -61,7 +62,10 @@
|
|
|
61
62
|
"pagination",
|
|
62
63
|
"auth",
|
|
63
64
|
"token-refresh",
|
|
64
|
-
"link-header"
|
|
65
|
+
"link-header",
|
|
66
|
+
"stale-if-error",
|
|
67
|
+
"observability",
|
|
68
|
+
"logging"
|
|
65
69
|
],
|
|
66
70
|
"author": "Boy Offi9",
|
|
67
71
|
"license": "MIT",
|
|
@@ -83,8 +87,8 @@
|
|
|
83
87
|
"src",
|
|
84
88
|
"types",
|
|
85
89
|
"dist",
|
|
90
|
+
"docs/recipes.md",
|
|
86
91
|
"README.md",
|
|
87
|
-
"RECIPES.md",
|
|
88
92
|
"LICENSE"
|
|
89
93
|
],
|
|
90
94
|
"devDependencies": {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
const { withRetry, RetryAbortedError } = require("
|
|
2
|
-
const { withTimeout } = require("
|
|
3
|
-
const { withDedupe } = require("
|
|
1
|
+
const { withRetry, RetryAbortedError } = require("../resilience/retry");
|
|
2
|
+
const { withTimeout } = require("../resilience/timeout");
|
|
3
|
+
const { withDedupe } = require("../resilience/dedupe");
|
|
4
4
|
const { buildQuery } = require("./buildQuery");
|
|
5
5
|
const { parseJsonSafe } = require("./parseJsonSafe");
|
|
6
|
-
const { normalizeError } = require("
|
|
7
|
-
const { HTTPError } = require("
|
|
8
|
-
const { createRateLimiter } = require("
|
|
9
|
-
const { createCircuitBreaker } = require("
|
|
10
|
-
const { createMemoryCache } = require("./
|
|
11
|
-
const { cacheKey, snapshotResponse, entryToResponse } = require("./
|
|
12
|
-
const { discardBody, getOrigin } = require("
|
|
6
|
+
const { normalizeError } = require("../errors/normalizeError");
|
|
7
|
+
const { HTTPError } = require("../errors/HTTPError");
|
|
8
|
+
const { createRateLimiter } = require("../resilience/rateLimiter");
|
|
9
|
+
const { createCircuitBreaker } = require("../resilience/circuitBreaker");
|
|
10
|
+
const { createMemoryCache } = require("./cache/memoryCache");
|
|
11
|
+
const { cacheKey, snapshotResponse, entryToResponse } = require("./cache/entry");
|
|
12
|
+
const { discardBody, getOrigin } = require("../internal/http");
|
|
13
13
|
|
|
14
14
|
const DEFAULT_RETRY_STATUS_CODES = [408, 425, 429, 500, 502, 503, 504];
|
|
15
15
|
// Safe to repeat without side effects. POST/PATCH are deliberately absent.
|
|
@@ -94,8 +94,11 @@ function mergeDefaults(base, extra) {
|
|
|
94
94
|
* - `rateLimit`: stay under a budget and obey X-RateLimit-* / Retry-After
|
|
95
95
|
* *before* the server has to say 429
|
|
96
96
|
* - `circuitBreaker`: fail fast against a service that keeps failing
|
|
97
|
-
* - `cache`: in-memory GET cache with TTL, stale-while-revalidate
|
|
98
|
-
* ETag / Last-Modified revalidation (304s)
|
|
97
|
+
* - `cache`: in-memory GET cache with TTL, stale-while-revalidate,
|
|
98
|
+
* stale-if-error and ETag / Last-Modified revalidation (304s)
|
|
99
|
+
* - `onEvent`: one callback for everything that happens (request, attempt,
|
|
100
|
+
* retry, response, error, cache, rate-limit waits, circuit changes, token
|
|
101
|
+
* refreshes) — plug it into your logger or metrics
|
|
99
102
|
* - concurrent GET de-duplication (withDedupe)
|
|
100
103
|
* - `auth`: attach a token, and on a 401 refresh it ONCE (shared by all
|
|
101
104
|
* concurrent requests) and replay the request
|
|
@@ -117,8 +120,9 @@ function mergeDefaults(base, extra) {
|
|
|
117
120
|
* createRateLimiter() instance to share one budget
|
|
118
121
|
* @param {boolean | object | Function} [defaults.circuitBreaker] - options for
|
|
119
122
|
* a per-origin breaker, a createCircuitBreaker() instance, or `(url) => breaker`
|
|
120
|
-
* @param {boolean | object} [defaults.cache] - `{ ttl, staleWhileRevalidate,
|
|
121
|
-
* maxEntries, maxBodyBytes, store, onEvent }`; `true` = 60s TTL
|
|
123
|
+
* @param {boolean | object} [defaults.cache] - `{ ttl, staleWhileRevalidate, staleIfError,
|
|
124
|
+
* maxEntries, maxBodyBytes, store, onEvent }`; `true` = 60s TTL. `staleIfError` (ms) lets a
|
|
125
|
+
* cached copy be served for that long past its TTL when the network fails or the server answers 5xx.
|
|
122
126
|
* @param {object} [defaults.auth] - `{ getToken, refresh, header?, scheme?, shouldRefresh? }`.
|
|
123
127
|
* `getToken()` supplies the current token (sync or async); on a 401 (or
|
|
124
128
|
* whatever `shouldRefresh(response)` says) `refresh({ token, response })` is
|
|
@@ -133,6 +137,9 @@ function mergeDefaults(base, extra) {
|
|
|
133
137
|
* @param {(ctx: {url: string, init: object}) => void | Promise<void>} [defaults.onRequest] -
|
|
134
138
|
* runs once per call; may mutate ctx.url / ctx.init (e.g. add a header)
|
|
135
139
|
* @param {(ctx: {url: string, init: object, response: Response}) => void | Promise<void>} [defaults.onResponse]
|
|
140
|
+
* @param {(event: object) => void | Promise<void>} [defaults.onEvent] - receives `{ type, ... }` events:
|
|
141
|
+
* "request", "attempt", "retry", "response", "error", "cache", "ratelimit", "circuit", "auth".
|
|
142
|
+
* Errors thrown by the handler are swallowed — logging must never break a request.
|
|
136
143
|
*/
|
|
137
144
|
function createFetch(defaults = {}) {
|
|
138
145
|
const {
|
|
@@ -150,10 +157,22 @@ function createFetch(defaults = {}) {
|
|
|
150
157
|
fetch: customFetch,
|
|
151
158
|
onRequest,
|
|
152
159
|
onResponse,
|
|
160
|
+
onEvent,
|
|
153
161
|
} = defaults;
|
|
154
162
|
|
|
155
163
|
const doFetch = (...args) => (customFetch || globalThis.fetch)(...args);
|
|
156
164
|
|
|
165
|
+
// A throwing (or rejecting) handler must never break a request.
|
|
166
|
+
const emitEvent = (event) => {
|
|
167
|
+
if (!onEvent) return;
|
|
168
|
+
try {
|
|
169
|
+
const result = onEvent(event);
|
|
170
|
+
if (result && typeof result.catch === "function") result.catch(() => {});
|
|
171
|
+
} catch {
|
|
172
|
+
// ignore
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
157
176
|
// Origins allowed to receive default credentials. Empty = no baseUrl to compare against = unrestricted.
|
|
158
177
|
const trusted = new Set();
|
|
159
178
|
for (const o of [baseUrl, ...(trustedOrigins || [])]) {
|
|
@@ -183,7 +202,13 @@ function createFetch(defaults = {}) {
|
|
|
183
202
|
: (url) => {
|
|
184
203
|
const origin = originOf(url);
|
|
185
204
|
if (!breakerRegistry.has(origin)) {
|
|
186
|
-
|
|
205
|
+
const options = circuitBreaker === true ? {} : { ...circuitBreaker };
|
|
206
|
+
const userOnStateChange = options.onStateChange;
|
|
207
|
+
options.onStateChange = (state, previous) => {
|
|
208
|
+
if (userOnStateChange) userOnStateChange(state, previous);
|
|
209
|
+
emitEvent({ type: "circuit", origin, state, previous });
|
|
210
|
+
};
|
|
211
|
+
breakerRegistry.set(origin, createCircuitBreaker(options));
|
|
187
212
|
}
|
|
188
213
|
return breakerRegistry.get(origin);
|
|
189
214
|
};
|
|
@@ -193,6 +218,7 @@ function createFetch(defaults = {}) {
|
|
|
193
218
|
: {
|
|
194
219
|
ttl: 60000,
|
|
195
220
|
staleWhileRevalidate: 0,
|
|
221
|
+
staleIfError: 0,
|
|
196
222
|
maxEntries: 100,
|
|
197
223
|
maxBodyBytes: undefined,
|
|
198
224
|
...(cache === true ? {} : cache),
|
|
@@ -206,7 +232,10 @@ function createFetch(defaults = {}) {
|
|
|
206
232
|
}
|
|
207
233
|
const pendingStores = cacheCfg ? cacheCfg.pending : null;
|
|
208
234
|
const revalidating = cacheCfg ? cacheCfg.revalidating : null;
|
|
209
|
-
const emit = (type, url) =>
|
|
235
|
+
const emit = (type, url) => {
|
|
236
|
+
if (cacheCfg && cacheCfg.onEvent) cacheCfg.onEvent({ type, url });
|
|
237
|
+
emitEvent({ type: "cache", url, method: "GET", event: type });
|
|
238
|
+
};
|
|
210
239
|
|
|
211
240
|
// Shared in-flight GETs. Every caller gets its own clone of the response so
|
|
212
241
|
// body reads don't fight each other.
|
|
@@ -230,9 +259,12 @@ function createFetch(defaults = {}) {
|
|
|
230
259
|
// Returns a token newer than `usedToken`, refreshing at most once no matter
|
|
231
260
|
// how many requests were rejected at the same time. If someone else already
|
|
232
261
|
// refreshed (the current token isn't the one we used), just use that.
|
|
233
|
-
async function obtainFreshToken(authCfg, tokenUsed, response) {
|
|
262
|
+
async function obtainFreshToken(authCfg, tokenUsed, response, info) {
|
|
234
263
|
const current = await authCfg.getToken();
|
|
235
|
-
if (current && current !== tokenUsed)
|
|
264
|
+
if (current && current !== tokenUsed) {
|
|
265
|
+
emitEvent({ type: "auth", ...info, outcome: "reused" });
|
|
266
|
+
return current;
|
|
267
|
+
}
|
|
236
268
|
|
|
237
269
|
const state = refreshStateFor(authCfg);
|
|
238
270
|
if (!state.inflight) {
|
|
@@ -245,10 +277,21 @@ function createFetch(defaults = {}) {
|
|
|
245
277
|
}
|
|
246
278
|
})();
|
|
247
279
|
}
|
|
248
|
-
|
|
280
|
+
try {
|
|
281
|
+
const token = await state.inflight;
|
|
282
|
+
emitEvent({ type: "auth", ...info, outcome: "refreshed" });
|
|
283
|
+
return token;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
emitEvent({ type: "auth", ...info, outcome: "failed", error });
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
249
288
|
}
|
|
250
289
|
|
|
251
|
-
|
|
290
|
+
// May a cached copy be served because the origin is failing?
|
|
291
|
+
const withinStaleIfError = (entry) =>
|
|
292
|
+
cacheCfg && cacheCfg.staleIfError > 0 && Date.now() - entry.storedAt < cacheCfg.ttl + cacheCfg.staleIfError;
|
|
293
|
+
|
|
294
|
+
async function execute(input, init, state) {
|
|
252
295
|
const {
|
|
253
296
|
query,
|
|
254
297
|
json,
|
|
@@ -263,6 +306,8 @@ function createFetch(defaults = {}) {
|
|
|
263
306
|
|
|
264
307
|
const method = String(fetchInit.method || "GET").toUpperCase();
|
|
265
308
|
const url = joinUrl(baseUrl, input, query);
|
|
309
|
+
state.url = url;
|
|
310
|
+
state.method = method;
|
|
266
311
|
const credentialsAllowed = isTrustedTarget(url);
|
|
267
312
|
|
|
268
313
|
const defaultHeaderSet = await resolveHeaders(defaultHeaders);
|
|
@@ -285,6 +330,9 @@ function createFetch(defaults = {}) {
|
|
|
285
330
|
if (onRequest) await onRequest(ctx);
|
|
286
331
|
const finalHeaders = ctx.init.headers instanceof Headers ? ctx.init.headers : new Headers(ctx.init.headers);
|
|
287
332
|
const finalMethod = String(ctx.init.method || method).toUpperCase();
|
|
333
|
+
state.url = ctx.url;
|
|
334
|
+
state.method = finalMethod;
|
|
335
|
+
emitEvent({ type: "request", url: ctx.url, method: finalMethod });
|
|
288
336
|
|
|
289
337
|
// --- auth: attach the current token (unless the caller set the header) ----
|
|
290
338
|
const authHeader = auth ? String(auth.header || "authorization").toLowerCase() : null;
|
|
@@ -310,8 +358,13 @@ function createFetch(defaults = {}) {
|
|
|
310
358
|
// `netSignal` are parameters so background revalidation can reuse this.
|
|
311
359
|
function network(netHeaders, netSignal) {
|
|
312
360
|
const attemptOnce = () => {
|
|
361
|
+
state.attempts++;
|
|
362
|
+
emitEvent({ type: "attempt", url: ctx.url, method: finalMethod, attempt: state.attempts });
|
|
313
363
|
const attempt = async () => {
|
|
314
|
-
if (limiter)
|
|
364
|
+
if (limiter) {
|
|
365
|
+
const waitedMs = await limiter.acquire({ signal: netSignal });
|
|
366
|
+
if (waitedMs > 0) emitEvent({ type: "ratelimit", url: ctx.url, method: finalMethod, waitedMs });
|
|
367
|
+
}
|
|
315
368
|
const res = await withTimeout(
|
|
316
369
|
(attemptSignal) => {
|
|
317
370
|
const attemptInit = { ...ctx.init, method: finalMethod, headers: netHeaders, signal: attemptSignal };
|
|
@@ -343,6 +396,19 @@ function createFetch(defaults = {}) {
|
|
|
343
396
|
...retryRest,
|
|
344
397
|
retries,
|
|
345
398
|
retryStatusCodes: statusCodes || DEFAULT_RETRY_STATUS_CODES,
|
|
399
|
+
onRetry: (err, attempt, delayMs, rctx) => {
|
|
400
|
+
if (retryRest.onRetry) retryRest.onRetry(err, attempt, delayMs, rctx);
|
|
401
|
+
emitEvent({
|
|
402
|
+
type: "retry",
|
|
403
|
+
url: ctx.url,
|
|
404
|
+
method: finalMethod,
|
|
405
|
+
attempt: attempt + 1,
|
|
406
|
+
delayMs,
|
|
407
|
+
reason: rctx.reason === "result" ? "status" : "error",
|
|
408
|
+
status: rctx.status,
|
|
409
|
+
error: err,
|
|
410
|
+
});
|
|
411
|
+
},
|
|
346
412
|
shouldRetry:
|
|
347
413
|
customShouldRetry ||
|
|
348
414
|
((err) => {
|
|
@@ -374,6 +440,17 @@ function createFetch(defaults = {}) {
|
|
|
374
440
|
let entry = null;
|
|
375
441
|
|
|
376
442
|
const finish = async (response) => {
|
|
443
|
+
state.responded = true;
|
|
444
|
+
emitEvent({
|
|
445
|
+
type: "response",
|
|
446
|
+
url: ctx.url,
|
|
447
|
+
method: finalMethod,
|
|
448
|
+
status: response.status,
|
|
449
|
+
ok: response.ok,
|
|
450
|
+
durationMs: Date.now() - state.startedAt,
|
|
451
|
+
attempts: state.attempts,
|
|
452
|
+
cache: state.cache,
|
|
453
|
+
});
|
|
377
454
|
if (onResponse) await onResponse({ url: ctx.url, init: ctx.init, response });
|
|
378
455
|
if (throwHttpErrors && !response.ok) {
|
|
379
456
|
throw new HTTPError(response, { method: finalMethod, url: ctx.url });
|
|
@@ -388,10 +465,12 @@ function createFetch(defaults = {}) {
|
|
|
388
465
|
const age = Date.now() - entry.storedAt;
|
|
389
466
|
if (age < cacheCfg.ttl) {
|
|
390
467
|
emit("hit", ctx.url);
|
|
468
|
+
state.cache = "hit";
|
|
391
469
|
return finish(entryToResponse(entry));
|
|
392
470
|
}
|
|
393
471
|
if (age < cacheCfg.ttl + cacheCfg.staleWhileRevalidate) {
|
|
394
472
|
emit("stale", ctx.url);
|
|
473
|
+
state.cache = "stale";
|
|
395
474
|
if (!revalidating.has(key)) {
|
|
396
475
|
revalidating.add(key);
|
|
397
476
|
const staleEntry = entry;
|
|
@@ -428,13 +507,19 @@ function createFetch(defaults = {}) {
|
|
|
428
507
|
} catch (err) {
|
|
429
508
|
// Present a user abort as the plain AbortError they caused, not as retry internals.
|
|
430
509
|
if (err instanceof RetryAbortedError && signal && signal.aborted) throw signal.reason ?? err;
|
|
510
|
+
// The network failed but we still hold a usable copy: serve it (never for a deliberate abort).
|
|
511
|
+
if (entry && withinStaleIfError(entry) && !(signal && signal.aborted) && !(err && err.name === "AbortError")) {
|
|
512
|
+
emit("stale-if-error", ctx.url);
|
|
513
|
+
state.cache = "stale-if-error";
|
|
514
|
+
return finish(entryToResponse(entry));
|
|
515
|
+
}
|
|
431
516
|
throw err;
|
|
432
517
|
}
|
|
433
518
|
|
|
434
519
|
// --- auth: a rejected token is refreshed once, then the request replays ----
|
|
435
520
|
const needsRefresh = auth && (auth.shouldRefresh ? auth.shouldRefresh(response) : response.status === 401);
|
|
436
521
|
if (usedToken && needsRefresh && !isStreamBody(ctx.init.body)) {
|
|
437
|
-
const fresh = await obtainFreshToken(auth, usedToken, response);
|
|
522
|
+
const fresh = await obtainFreshToken(auth, usedToken, response, { url: ctx.url, method: finalMethod });
|
|
438
523
|
if (fresh) {
|
|
439
524
|
await discardBody(response);
|
|
440
525
|
finalHeaders.set(authHeader, formatToken(fresh));
|
|
@@ -449,15 +534,25 @@ function createFetch(defaults = {}) {
|
|
|
449
534
|
}
|
|
450
535
|
}
|
|
451
536
|
|
|
537
|
+
// The server answered, but with an error, and we still hold a usable copy.
|
|
538
|
+
if (useCache && entry && response.status >= 500 && withinStaleIfError(entry)) {
|
|
539
|
+
await discardBody(response);
|
|
540
|
+
emit("stale-if-error", ctx.url);
|
|
541
|
+
state.cache = "stale-if-error";
|
|
542
|
+
return finish(entryToResponse(entry));
|
|
543
|
+
}
|
|
544
|
+
|
|
452
545
|
if (useCache) {
|
|
453
546
|
if (response.status === 304 && entry) {
|
|
454
547
|
entry.storedAt = Date.now();
|
|
455
548
|
await cacheCfg.store.set(key, entry);
|
|
456
549
|
emit("revalidated", ctx.url);
|
|
550
|
+
state.cache = "revalidated";
|
|
457
551
|
await discardBody(response);
|
|
458
552
|
response = entryToResponse(entry);
|
|
459
553
|
} else {
|
|
460
554
|
emit("miss", ctx.url);
|
|
555
|
+
state.cache = "miss";
|
|
461
556
|
if (response.status === 200) scheduleStore(key, response, ctx.url);
|
|
462
557
|
}
|
|
463
558
|
}
|
|
@@ -465,6 +560,19 @@ function createFetch(defaults = {}) {
|
|
|
465
560
|
return finish(response);
|
|
466
561
|
}
|
|
467
562
|
|
|
563
|
+
async function request(input, init = {}) {
|
|
564
|
+
const state = { startedAt: Date.now(), attempts: 0, responded: false, cache: undefined, url: undefined, method: undefined };
|
|
565
|
+
try {
|
|
566
|
+
return await execute(input, init, state);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
// A non-2xx HTTPError was already reported as a "response" event.
|
|
569
|
+
if (!state.responded) {
|
|
570
|
+
emitEvent({ type: "error", url: state.url, method: state.method, error, durationMs: Date.now() - state.startedAt, attempts: state.attempts });
|
|
571
|
+
}
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
468
576
|
/** The `baseUrl` this instance was created with (paginateLinks uses it to stay on-origin). */
|
|
469
577
|
request.baseUrl = baseUrl;
|
|
470
578
|
|
package/src/index.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
const { withRetry, RetryAbortedError, StopRetry } = require("./
|
|
2
|
-
const { withTimeout, TimeoutError } = require("./
|
|
3
|
-
const { normalizeError } = require("./normalizeError");
|
|
4
|
-
const { parseJsonSafe } = require("./parseJsonSafe");
|
|
5
|
-
const { buildQuery } = require("./buildQuery");
|
|
6
|
-
const { retryAfterAware, parseRetryAfter, getWaitMsFromHeaders } = require("./retryAfterAware");
|
|
7
|
-
const { withProgress, BodyTooLargeError } = require("./
|
|
8
|
-
const { withDedupe } = require("./
|
|
9
|
-
const { createLimiter } = require("./
|
|
10
|
-
const { createFetch, HTTPError } = require("./createFetch");
|
|
11
|
-
const { createRateLimiter } = require("./
|
|
12
|
-
const { createCircuitBreaker, CircuitOpenError } = require("./
|
|
13
|
-
const { createMemoryCache } = require("./
|
|
14
|
-
const { createMockFetch } = require("./createMockFetch");
|
|
15
|
-
const { downloadResumable } = require("./
|
|
16
|
-
const { readLines, readNDJSON, readSSE } = require("./streams");
|
|
17
|
-
const { streamSSE } = require("./streamSSE");
|
|
18
|
-
const { paginate, paginatePages, paginateLinks, parseLinkHeader } = require("./paginate");
|
|
1
|
+
const { withRetry, RetryAbortedError, StopRetry } = require("./resilience/retry");
|
|
2
|
+
const { withTimeout, TimeoutError } = require("./resilience/timeout");
|
|
3
|
+
const { normalizeError } = require("./errors/normalizeError");
|
|
4
|
+
const { parseJsonSafe } = require("./http/parseJsonSafe");
|
|
5
|
+
const { buildQuery } = require("./http/buildQuery");
|
|
6
|
+
const { retryAfterAware, parseRetryAfter, getWaitMsFromHeaders } = require("./resilience/retryAfterAware");
|
|
7
|
+
const { withProgress, BodyTooLargeError } = require("./streams/progress");
|
|
8
|
+
const { withDedupe } = require("./resilience/dedupe");
|
|
9
|
+
const { createLimiter } = require("./resilience/limiter");
|
|
10
|
+
const { createFetch, HTTPError } = require("./http/createFetch");
|
|
11
|
+
const { createRateLimiter } = require("./resilience/rateLimiter");
|
|
12
|
+
const { createCircuitBreaker, CircuitOpenError } = require("./resilience/circuitBreaker");
|
|
13
|
+
const { createMemoryCache } = require("./http/cache/memoryCache");
|
|
14
|
+
const { createMockFetch } = require("./testing/createMockFetch");
|
|
15
|
+
const { downloadResumable } = require("./streams/download");
|
|
16
|
+
const { readLines, readNDJSON, readSSE } = require("./streams/readers");
|
|
17
|
+
const { streamSSE } = require("./streams/streamSSE");
|
|
18
|
+
const { paginate, paginatePages, paginateLinks, parseLinkHeader } = require("./pagination/paginate");
|
|
19
19
|
|
|
20
20
|
module.exports = {
|
|
21
21
|
withRetry,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const { abortableSleep } = require("
|
|
2
|
-
const { getRateLimitInfo } = require("
|
|
1
|
+
const { abortableSleep } = require("../internal/sleep");
|
|
2
|
+
const { getRateLimitInfo } = require("../internal/http");
|
|
3
3
|
|
|
4
4
|
function abortable(promise, signal) {
|
|
5
5
|
if (!signal) return promise;
|
|
@@ -50,6 +50,7 @@ function createRateLimiter(opts = {}) {
|
|
|
50
50
|
let tail = Promise.resolve();
|
|
51
51
|
|
|
52
52
|
async function take(signal) {
|
|
53
|
+
let waitedMs = 0;
|
|
53
54
|
for (;;) {
|
|
54
55
|
signal?.throwIfAborted?.();
|
|
55
56
|
const now = Date.now();
|
|
@@ -61,13 +62,15 @@ function createRateLimiter(opts = {}) {
|
|
|
61
62
|
|
|
62
63
|
if (waitMs <= 0) {
|
|
63
64
|
stamps.push(now);
|
|
64
|
-
return;
|
|
65
|
+
return waitedMs;
|
|
65
66
|
}
|
|
67
|
+
const waitStartedAt = Date.now();
|
|
66
68
|
await abortableSleep(waitMs, signal);
|
|
69
|
+
waitedMs += Date.now() - waitStartedAt;
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
/** Resolves
|
|
73
|
+
/** Resolves with the time spent waiting. Rejects if aborted while waiting. */
|
|
71
74
|
function acquire({ signal } = {}) {
|
|
72
75
|
const turn = tail.then(() => take(signal));
|
|
73
76
|
tail = turn.catch(() => {});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const { RetryAbortedError, sleep } = require("
|
|
2
|
-
const { getWaitMsFromHeaders, discardBody } = require("
|
|
1
|
+
const { RetryAbortedError, sleep } = require("../internal/sleep");
|
|
2
|
+
const { getWaitMsFromHeaders, discardBody } = require("../internal/http");
|
|
3
3
|
|
|
4
4
|
function defaultShouldRetry(err) {
|
|
5
5
|
// Retry thrown errors except deliberate aborts. Narrow it with `shouldRetry`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const { withRetry } = require("./
|
|
2
|
-
const { normalizeError } = require("
|
|
3
|
-
const { parseRetryAfter, getWaitMsFromHeaders } = require("
|
|
1
|
+
const { withRetry } = require("./retry");
|
|
2
|
+
const { normalizeError } = require("../errors/normalizeError");
|
|
3
|
+
const { parseRetryAfter, getWaitMsFromHeaders } = require("../internal/http");
|
|
4
4
|
|
|
5
5
|
function defaultShouldRetryError(err) {
|
|
6
6
|
const normalized = normalizeError(err);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const { HTTPError } = require("
|
|
2
|
-
const { BodyTooLargeError } = require("./
|
|
3
|
-
const { normalizeError } = require("
|
|
4
|
-
const { getWaitMsFromHeaders } = require("
|
|
5
|
-
const { abortableSleep } = require("
|
|
1
|
+
const { HTTPError } = require("../errors/HTTPError");
|
|
2
|
+
const { BodyTooLargeError } = require("./progress");
|
|
3
|
+
const { normalizeError } = require("../errors/normalizeError");
|
|
4
|
+
const { getWaitMsFromHeaders } = require("../internal/http");
|
|
5
|
+
const { abortableSleep } = require("../internal/sleep");
|
|
6
6
|
|
|
7
7
|
const RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
8
8
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const { readSSE } = require("./
|
|
2
|
-
const { HTTPError } = require("
|
|
3
|
-
const { normalizeError } = require("
|
|
4
|
-
const { getWaitMsFromHeaders, discardBody } = require("
|
|
5
|
-
const { abortableSleep } = require("
|
|
1
|
+
const { readSSE } = require("./readers");
|
|
2
|
+
const { HTTPError } = require("../errors/HTTPError");
|
|
3
|
+
const { normalizeError } = require("../errors/normalizeError");
|
|
4
|
+
const { getWaitMsFromHeaders, discardBody } = require("../internal/http");
|
|
5
|
+
const { abortableSleep } = require("../internal/sleep");
|
|
6
6
|
|
|
7
7
|
function isRetryableStatus(status) {
|
|
8
8
|
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Error types and classification — src/errors/
|
|
2
|
+
|
|
3
|
+
export interface NormalizedError {
|
|
4
|
+
message: string;
|
|
5
|
+
status: number | null;
|
|
6
|
+
code: string | null;
|
|
7
|
+
isNetworkError: boolean;
|
|
8
|
+
isTimeout: boolean;
|
|
9
|
+
/** True when the request was cancelled on purpose (AbortController, axios cancel). */
|
|
10
|
+
isAborted: boolean;
|
|
11
|
+
cause: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function normalizeError(err: unknown): NormalizedError;
|
|
15
|
+
|
|
16
|
+
export class HTTPError extends Error {
|
|
17
|
+
name: "HTTPError";
|
|
18
|
+
status: number;
|
|
19
|
+
statusText: string;
|
|
20
|
+
response: Response;
|
|
21
|
+
url: string;
|
|
22
|
+
method: string;
|
|
23
|
+
constructor(response: Response, request: { method: string; url: string });
|
|
24
|
+
}
|