@boy-offi9-inc/reqkit 0.7.0 → 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 +95 -55
- package/dist/reqkit.mjs +2143 -1907
- package/package.json +9 -5
- package/src/{createFetch.js → http/createFetch.js} +169 -30
- package/src/index.js +18 -18
- package/src/{_http.js → internal/http.js} +11 -1
- package/src/{paginate.js → pagination/paginate.js} +33 -4
- 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/{streams.js → streams/readers.js} +64 -20
- 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 -518
- 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/{_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/README.md
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
<a href="https://github.com/boy-offi9-inc/reqkit/actions/workflows/ci.yml"><img src="https://github.com/boy-offi9-inc/reqkit/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
10
10
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
11
11
|
<img src="https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white" alt="Node >=18">
|
|
12
|
-
<img src="https://img.shields.io/badge/npm-%40boy--offi9--inc%2Freqkit-CB3837?logo=npm&logoColor=white" alt="npm package">
|
|
13
12
|
<img src="https://img.shields.io/badge/dependencies-zero-brightgreen" alt="Zero dependencies">
|
|
14
13
|
<img src="https://img.shields.io/badge/module-CJS%20%2B%20ESM-blue" alt="CJS + ESM">
|
|
15
14
|
<img src="https://img.shields.io/badge/types-included-3178C6?logo=typescript&logoColor=white" alt="TypeScript types included">
|
|
@@ -19,7 +18,7 @@
|
|
|
19
18
|
|
|
20
19
|
**Tiny, zero-dependency HTTP helpers for `fetch`, axios, or anything else — retries that respect `Retry-After`, timeouts that really cancel, and errors that make sense.**
|
|
21
20
|
|
|
22
|
-
Not a client, not an axios replacement.
|
|
21
|
+
Not a client, not an axios replacement. Use one function on its own, or let [`createFetch`](#createfetchdefaults) wire them together. You always get a plain `Response` back.
|
|
23
22
|
|
|
24
23
|
```js
|
|
25
24
|
import { createFetch } from "@boy-offi9-inc/reqkit";
|
|
@@ -49,32 +48,18 @@ const pages = await limit.map(urls, (url) =>
|
|
|
49
48
|
);
|
|
50
49
|
```
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
## Playground
|
|
55
|
-
|
|
56
|
-
Try everything in your browser — retries obeying `Retry-After`, rate limiting, the circuit breaker, caching, SSE streaming, an SSE stream that reconnects itself, and a resumable download that survives a dropped connection. It runs against a fake in-memory server, so there's nothing to install and no network involved.
|
|
57
|
-
|
|
58
|
-
Run it locally: `npm run build && python3 -m http.server -d docs` → open `http://localhost:8000`. (ES modules don't load from `file://`, so it needs a server.)
|
|
59
|
-
|
|
60
|
-
## Works with what you already use
|
|
61
|
-
|
|
62
|
-
Recipes for **axios, ky, got, undici/node-fetch**, testing, and LLM streaming live in [RECIPES.md](./RECIPES.md): keep your client, bolt on just the piece you're missing (`Retry-After`-aware retries, a rate limiter, a circuit breaker).
|
|
63
|
-
|
|
64
|
-
## Runtime support
|
|
51
|
+
## Install
|
|
65
52
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
| **Deno, Bun, browsers, Cloudflare Workers** | Use the dependency-free ESM bundle (`@boy-offi9-inc/reqkit/browser`, or `import … from "npm:@boy-offi9-inc/reqkit"` in Deno). Only web-standard APIs are used. CI smoke-tests the bundle on Deno, Bun and headless Chromium — **experimental** until those jobs have a track record; please open an issue if one misbehaves. |
|
|
53
|
+
```bash
|
|
54
|
+
npm install @boy-offi9-inc/reqkit
|
|
55
|
+
```
|
|
70
56
|
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
</script>
|
|
57
|
+
```js
|
|
58
|
+
import { createFetch, withRetry } from "@boy-offi9-inc/reqkit"; // ESM
|
|
59
|
+
const { createFetch, withRetry } = require("@boy-offi9-inc/reqkit"); // CommonJS
|
|
75
60
|
```
|
|
76
61
|
|
|
77
|
-
|
|
62
|
+
Requires Node 18+. TypeScript types are included.
|
|
78
63
|
|
|
79
64
|
## Pick what you need
|
|
80
65
|
|
|
@@ -90,7 +75,8 @@ Recipes for **axios, ky, got, undici/node-fetch**, testing, and LLM streaming li
|
|
|
90
75
|
| Run N things at a time | [`createLimiter`](#createlimiterconcurrency) |
|
|
91
76
|
| Slow down *before* the server says 429 (obey `X-RateLimit-*`) | [`createRateLimiter`](#createratelimiteropts) |
|
|
92
77
|
| Stop hammering a service that keeps failing | [`createCircuitBreaker`](#createcircuitbreakeropts) |
|
|
93
|
-
| Cache GETs (TTL, stale-while-revalidate, ETag/304) | [`createFetch` → `cache`](#caching) |
|
|
78
|
+
| Cache GETs (TTL, stale-while-revalidate, stale-if-error, ETag/304) | [`createFetch` → `cache`](#caching) |
|
|
79
|
+
| Log or measure every request, retry and cache hit | [`createFetch` → `onEvent`](#events-and-logging) |
|
|
94
80
|
| Attach a token and refresh it on 401 — once, even with many requests in flight | [`createFetch` → `auth`](#auth-token-refresh) |
|
|
95
81
|
| Walk a paginated API (cursor, offset, `Link` headers) | [`paginate` / `paginateLinks`](#pagination-paginate-paginatepages-paginatelinks) |
|
|
96
82
|
| Resume a download after the connection drops | [`downloadResumable`](#downloadresumableurl-opts) |
|
|
@@ -99,26 +85,48 @@ Recipes for **axios, ky, got, undici/node-fetch**, testing, and LLM streaming li
|
|
|
99
85
|
| Test retry/timeout code without a network | [`createMockFetch`](#createmockfetch) |
|
|
100
86
|
| All of the above, wired up | [`createFetch`](#createfetchdefaults) |
|
|
101
87
|
|
|
102
|
-
|
|
88
|
+
## Why reqkit
|
|
103
89
|
|
|
104
|
-
|
|
90
|
+
- **No client to adopt.** Standalone functions that compose with whatever you already use — fetch, axios, ky, got, undici.
|
|
91
|
+
- **It reads the server.** Retries wait for `Retry-After` / `X-RateLimit-Reset` instead of backing off blindly, and a rate limiter can slow down *before* the server says 429.
|
|
92
|
+
- **Safe by default.** Retries only touch idempotent methods, credentials never leave your origin, and parsers bound their memory and time on hostile input.
|
|
93
|
+
- **Errors that make sense.** Node's `TypeError: fetch failed`, the browser's `Failed to fetch`, axios and undici errors all normalize to one shape, and a deliberate abort is never reported as a timeout.
|
|
94
|
+
- **Zero dependencies**, CommonJS and ESM, hand-written TypeScript types.
|
|
105
95
|
|
|
106
|
-
|
|
96
|
+
## Runtime support
|
|
107
97
|
|
|
108
|
-
|
|
98
|
+
| Runtime | |
|
|
99
|
+
|---|---|
|
|
100
|
+
| **Node 18, 20, 22, 24** | Supported. |
|
|
101
|
+
| **Deno, Bun, Cloudflare Workers, browsers** | Use the dependency-free ESM bundle, `@boy-offi9-inc/reqkit/browser`. Only web-standard APIs are used. |
|
|
109
102
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
103
|
+
```js
|
|
104
|
+
// Deno
|
|
105
|
+
import { createFetch } from "npm:@boy-offi9-inc/reqkit";
|
|
106
|
+
```
|
|
114
107
|
|
|
115
|
-
|
|
108
|
+
```html
|
|
109
|
+
<!-- Browser, no build step (pin a version in production) -->
|
|
110
|
+
<script type="module">
|
|
111
|
+
import { createFetch } from "https://cdn.jsdelivr.net/npm/@boy-offi9-inc/reqkit/dist/reqkit.mjs";
|
|
112
|
+
</script>
|
|
113
|
+
```
|
|
116
114
|
|
|
117
|
-
##
|
|
115
|
+
## Playground and recipes
|
|
118
116
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
- **[Interactive playground](https://boy-offi9-inc.github.io/reqkit/)** — every feature running in your browser against a fake server; nothing to install.
|
|
118
|
+
- **[Recipes](./docs/recipes.md)** — using reqkit alongside axios, ky, got and undici; testing; LLM streaming.
|
|
119
|
+
|
|
120
|
+
## Security
|
|
121
|
+
|
|
122
|
+
reqkit handles data a server controls (headers, streams, redirects), so the defaults are conservative:
|
|
123
|
+
|
|
124
|
+
- **Credentials stay on your origin.** With a `baseUrl`, the default `auth` token and the default `Authorization`, `Proxy-Authorization`, `Cookie`, `X-API-Key` and `X-Auth-Token` headers are only sent to that origin (plus any `trustedOrigins`). An absolute URL to another host doesn't receive them. Headers you pass for a specific call are always sent. Without a `baseUrl` or `trustedOrigins` there is no origin to compare against, so set at least one.
|
|
125
|
+
- **Pagination stays on origin.** `paginateLinks` refuses a `Link: rel="next"` that points to another origin (`code: "PAGINATE_CROSS_ORIGIN"`); allow more with `origins: [...]`.
|
|
126
|
+
- **Bounded input.** `readLines`, `readNDJSON` and `readSSE` run in linear time and throw `LINE_TOO_LONG` past `maxLineLength` (default 16 Mi characters); `parseLinkHeader` ignores headers over 64 KB; `withProgress` and `downloadResumable` accept `maxBytes`.
|
|
127
|
+
- **Safe retries.** `createFetch` retries only idempotent methods (or requests carrying an `Idempotency-Key`). Cross-origin redirects are left to `fetch`, which strips `Authorization`.
|
|
128
|
+
|
|
129
|
+
reqkit does not block requests to private or internal addresses. If you fetch user-supplied URLs on a server, validate them first. To report a vulnerability, see [SECURITY.md](./SECURITY.md).
|
|
122
130
|
|
|
123
131
|
---
|
|
124
132
|
|
|
@@ -134,7 +142,7 @@ const data = await withRetry(
|
|
|
134
142
|
{ retries: 3, baseDelayMs: 300, onRetry: (err, attempt, delayMs) => console.log(`retry ${attempt} in ${delayMs}ms`) }
|
|
135
143
|
);
|
|
136
144
|
|
|
137
|
-
// fetch doesn't throw on 4xx/5xx
|
|
145
|
+
// fetch doesn't throw on 4xx/5xx, so list the statuses to retry:
|
|
138
146
|
const res = await withRetry(() => fetch(url), { retryStatusCodes: [429, 500, 502, 503, 504] });
|
|
139
147
|
```
|
|
140
148
|
|
|
@@ -158,16 +166,15 @@ const res = await withRetry(() => fetch(url), { retryStatusCodes: [429, 500, 502
|
|
|
158
166
|
When retries run out on a *result* (e.g. a 503), the last result is returned rather than thrown — you decide what a bad status means. Bodies of retried responses are cancelled so connections are freed.
|
|
159
167
|
|
|
160
168
|
```js
|
|
161
|
-
// Cancel a long retry/backoff sequence externally — e.g. the user navigated away
|
|
162
169
|
const controller = new AbortController();
|
|
163
170
|
const promise = withRetry(() => fetch(url), { retries: 5, signal: controller.signal });
|
|
164
171
|
controller.abort(); // rejects with RetryAbortedError, even mid-backoff
|
|
165
172
|
```
|
|
166
173
|
|
|
167
|
-
|
|
174
|
+
**`StopRetry`** — throw it from inside `fn` to stop retrying immediately:
|
|
168
175
|
|
|
169
176
|
```js
|
|
170
|
-
|
|
177
|
+
import { withRetry, StopRetry } from "@boy-offi9-inc/reqkit";
|
|
171
178
|
|
|
172
179
|
await withRetry(async () => {
|
|
173
180
|
const res = await fetch(url);
|
|
@@ -278,7 +285,6 @@ Coalesces concurrent calls with the same arguments into a single in-flight call
|
|
|
278
285
|
```js
|
|
279
286
|
const getUser = withDedupe((id) => fetch(`/api/users/${id}`).then(r => r.json()));
|
|
280
287
|
|
|
281
|
-
// Only one network request goes out — both callers share it.
|
|
282
288
|
const [a, b] = await Promise.all([getUser(1), getUser(1)]);
|
|
283
289
|
```
|
|
284
290
|
|
|
@@ -291,7 +297,6 @@ Plain arguments are compared by value (object key order doesn't matter); anythin
|
|
|
291
297
|
This is deduplication of *concurrent* calls, not a cache — once a call settles (success or failure), the next call with the same key starts fresh.
|
|
292
298
|
|
|
293
299
|
```js
|
|
294
|
-
// Custom key when the default arg-based key isn't quite right
|
|
295
300
|
const fn = withDedupe((req) => doFetch(req), { keyFn: (req) => req.url });
|
|
296
301
|
```
|
|
297
302
|
|
|
@@ -362,6 +367,8 @@ for await (const row of readNDJSON(response)) console.log(row); // newline-delim
|
|
|
362
367
|
for await (const line of readLines(response)) console.log(line);
|
|
363
368
|
```
|
|
364
369
|
|
|
370
|
+
All three take `{ signal, maxLineLength }` — a line (or one SSE event's data) longer than `maxLineLength` characters (default 16 Mi) throws `code: "LINE_TOO_LONG"` rather than buffering forever.
|
|
371
|
+
|
|
365
372
|
`readSSE` follows the spec: multi-line `data:` is joined with `\n`, comments are ignored, `event` defaults to `"message"`, `id` carries over, `retry` is parsed, and an event cut off by the end of the stream is dropped. `readNDJSON` skips blank lines and names the bad line on a parse error (or pass `skipInvalid: true`). Line splitting handles `\n`, `\r\n`, `\r`, a BOM, and multi-byte characters split across chunks.
|
|
366
373
|
|
|
367
374
|
### Pagination: `paginate`, `paginatePages`, `paginateLinks`
|
|
@@ -466,10 +473,10 @@ String matchers match the full URL, `pathname + search`, or the bare `pathname`;
|
|
|
466
473
|
|
|
467
474
|
### `createFetch(defaults)`
|
|
468
475
|
|
|
469
|
-
|
|
476
|
+
Assembles the pieces above into one fetch-like function. It's still plain `fetch` underneath: you get a standard `Response` back, and you can supply your own `fetch`.
|
|
470
477
|
|
|
471
478
|
```js
|
|
472
|
-
|
|
479
|
+
import { createFetch, HTTPError } from "@boy-offi9-inc/reqkit";
|
|
473
480
|
|
|
474
481
|
const api = createFetch({
|
|
475
482
|
baseUrl: "https://api.example.com/v1",
|
|
@@ -502,6 +509,7 @@ const admin = api.extend({ headers: { "x-role": "admin" } }); // merges over the
|
|
|
502
509
|
| `retry` | see below | `false` disables; otherwise `{ retries, statusCodes, methods, baseDelayMs, maxDelayMs, factor, jitter, maxRetryTime, shouldRetry, calculateDelay, onRetry }` |
|
|
503
510
|
| `rateLimit` | — | `true` just learns from `X-RateLimit-*` / `Retry-After`; `{ requests, perMs }` also enforces a budget; or pass a shared [`createRateLimiter`](#createratelimiteropts) |
|
|
504
511
|
| `circuitBreaker` | — | `true` / options → one breaker **per origin**; or pass a shared [`createCircuitBreaker`](#createcircuitbreakeropts), or `(url) => breaker`. An open circuit stops retries immediately. |
|
|
512
|
+
| `trustedOrigins` | — | extra origins that may receive the default credentials — see [Security](#security) |
|
|
505
513
|
| `auth` | — | attach a token and refresh it on 401 — see [Auth token refresh](#auth-token-refresh) |
|
|
506
514
|
| `cache` | — | in-memory GET cache — see [Caching](#caching). `true` = 60s TTL. Per request: `cache: false` bypasses, `"reload"` refreshes |
|
|
507
515
|
| `dedupe` | `false` | merge identical concurrent `GET`/`HEAD` calls; every caller gets its own readable clone. Requests with their own `signal` are never merged. |
|
|
@@ -509,6 +517,7 @@ const admin = api.extend({ headers: { "x-role": "admin" } }); // merges over the
|
|
|
509
517
|
| `fetch` | `globalThis.fetch` | swap in undici, node-fetch, a test double... |
|
|
510
518
|
| `onRequest(ctx)` | — | once per call; may mutate `ctx.url` / `ctx.init` (e.g. add a header) |
|
|
511
519
|
| `onResponse(ctx)` | — | once per call, with the final response, before `HTTPError` is thrown |
|
|
520
|
+
| `onEvent(event)` | — | one callback for everything that happens — see [Events and logging](#events-and-logging) |
|
|
512
521
|
|
|
513
522
|
#### Auth token refresh
|
|
514
523
|
|
|
@@ -532,6 +541,35 @@ await Promise.all(urls.map((u) => api.json(u))); // 20 requests, all 401 → one
|
|
|
532
541
|
- **Options:** `header` (default `"authorization"`), `scheme` (default `"Bearer"`, `""` for a bare token such as an `X-API-Key`), `shouldRefresh(response)` (default: status 401). `refresh({ token, response })` may return the new token or just update your store. If `refresh` throws, every waiting request rejects with that error — and the next request tries again.
|
|
533
542
|
- `extend()` children that share the same `auth` object share one refresh; the cache keys entries per token.
|
|
534
543
|
|
|
544
|
+
#### Events and logging
|
|
545
|
+
|
|
546
|
+
One callback receives everything that happens, so you can plug reqkit into your logger or metrics without wrapping anything.
|
|
547
|
+
|
|
548
|
+
```js
|
|
549
|
+
const api = createFetch({
|
|
550
|
+
baseUrl: "https://api.example.com",
|
|
551
|
+
onEvent: (e) => {
|
|
552
|
+
if (e.type === "response") metrics.timing("http.request", e.durationMs, { status: e.status, cache: e.cache });
|
|
553
|
+
if (e.type === "retry") log.warn(`retry #${e.attempt} in ${e.delayMs}ms (${e.reason})`);
|
|
554
|
+
if (e.type === "error") log.error(e.error, { url: e.url });
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
| `type` | Emitted when | Fields |
|
|
560
|
+
|---|---|---|
|
|
561
|
+
| `request` | a call starts | `url`, `method` |
|
|
562
|
+
| `attempt` | a network attempt starts | `url`, `method`, `attempt` (1-based) |
|
|
563
|
+
| `retry` | an attempt failed and another will follow | `url`, `method`, `attempt`, `delayMs`, `reason` (`"error"` \| `"status"`), `status?`, `error?` |
|
|
564
|
+
| `response` | the final response arrives (including non-2xx, before `HTTPError` is thrown) | `url`, `method`, `status`, `ok`, `durationMs`, `attempts`, `cache?` (`"hit"` \| `"stale"` \| `"revalidated"` \| `"miss"` \| `"stale-if-error"`) |
|
|
565
|
+
| `error` | the call fails without a response — network error, timeout, abort, open circuit | `url?`, `method?`, `error`, `durationMs`, `attempts` |
|
|
566
|
+
| `cache` | cache activity | `url`, `method`, `event` (`hit`, `stale`, `revalidated`, `miss`, `store`, `stale-if-error`) |
|
|
567
|
+
| `ratelimit` | a call waited on the rate limiter | `url`, `method`, `waitedMs` |
|
|
568
|
+
| `circuit` | a circuit breaker changed state (breakers created from options — one per origin) | `origin`, `state`, `previous` |
|
|
569
|
+
| `auth` | a 401 triggered token handling | `url`, `method`, `outcome` (`"refreshed"` \| `"reused"` \| `"failed"`), `error?` |
|
|
570
|
+
|
|
571
|
+
Handlers can be sync or async, and anything they throw is ignored — logging never breaks a request. `extend()` children inherit the handler or replace it. A cache hit makes no network attempt, so its `response` event has `attempts: 0`.
|
|
572
|
+
|
|
535
573
|
#### Caching
|
|
536
574
|
|
|
537
575
|
```js
|
|
@@ -550,10 +588,13 @@ await api.json("/config"); // fresh → served from memory, no request
|
|
|
550
588
|
|---|---|---|
|
|
551
589
|
| `ttl` | `60000` | serve without asking the server for this long |
|
|
552
590
|
| `staleWhileRevalidate` | `0` | after the TTL, keep serving the stale copy this long while refreshing in the background |
|
|
591
|
+
| `staleIfError` | `0` | after the TTL, serve the stale copy for this long **when the origin is failing** (network error, timeout, open circuit, 5xx) instead of surfacing the error |
|
|
553
592
|
| `maxEntries` | `100` | size of the built-in LRU store |
|
|
554
593
|
| `maxBodyBytes` | `1 MiB` | bigger responses aren't cached |
|
|
555
594
|
| `store` | built-in LRU | bring your own (`get`/`set`/`delete`, sync or async) — Redis, IndexedDB, a `Map`… |
|
|
556
|
-
| `onEvent` | — | `({ type: "hit" \| "stale" \| "revalidated" \| "miss" \| "store", url })`
|
|
595
|
+
| `onEvent` | — | `({ type: "hit" \| "stale" \| "revalidated" \| "miss" \| "store" \| "stale-if-error", url })` — cache-only; see also [`onEvent`](#events-and-logging) on `createFetch` |
|
|
596
|
+
|
|
597
|
+
With `staleIfError: 10 * 60_000`, a cached copy keeps being served for up to 10 minutes past its TTL while the origin is down, instead of your callers seeing the failure. 4xx responses and deliberate aborts are never masked, and `cache: "reload"` always goes to the origin.
|
|
557
598
|
|
|
558
599
|
Only `GET` `200` responses are cached; `Cache-Control: no-store` and `Vary: *` are respected. **Every request header is part of the cache key**, so a response fetched with one user's `Authorization` can never be served to another. Each hit is a fresh `Response`, so bodies can be read as many times as you like. `extend()` children share the parent's cache, limiter and breakers.
|
|
559
600
|
|
|
@@ -561,15 +602,14 @@ Only `GET` `200` responses are cached; `Cache-Control: no-store` and `Vary: *` a
|
|
|
561
602
|
|
|
562
603
|
---
|
|
563
604
|
|
|
564
|
-
##
|
|
605
|
+
## Prior art
|
|
606
|
+
|
|
607
|
+
reqkit's building blocks are small independent implementations of ideas from [`p-retry`](https://github.com/sindresorhus/p-retry), [`ky`](https://github.com/sindresorhus/ky), [`ofetch`](https://github.com/unjs/ofetch), [`got`](https://github.com/sindresorhus/got), [`axios-retry`](https://github.com/softonic/axios-retry), [`p-limit`](https://github.com/sindresorhus/p-limit), [`opossum`](https://github.com/nodeshift/opossum), [`bottleneck`](https://github.com/SGrondin/bottleneck), [`p-throttle`](https://github.com/sindresorhus/p-throttle), [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) and [`nock`](https://github.com/nock/nock), plus the HTTP caching RFCs and the AWS "full jitter" backoff.
|
|
565
608
|
|
|
566
|
-
|
|
567
|
-
- **CJS + ESM.** `require()` and `import` both work.
|
|
568
|
-
- **Composable, not a client.** Every function takes and returns plain values (`Response`, plain objects, `Uint8Array`) — nothing proprietary to learn.
|
|
569
|
-
- **TypeScript types included**, hand-written (no build step to go wrong), with separate entries for CJS and ESM.
|
|
609
|
+
## Contributing
|
|
570
610
|
|
|
571
|
-
|
|
611
|
+
Issues and pull requests are welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md) and the [architecture overview](./docs/architecture.md).
|
|
572
612
|
|
|
573
|
-
##
|
|
613
|
+
## License
|
|
574
614
|
|
|
575
|
-
|
|
615
|
+
[MIT](./LICENSE) © Boy Offi9
|