@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.
Files changed (34) hide show
  1. package/README.md +92 -66
  2. package/dist/reqkit.mjs +2136 -2014
  3. package/package.json +9 -5
  4. package/src/{createFetch.js → http/createFetch.js} +130 -22
  5. package/src/index.js +18 -18
  6. package/src/{paginate.js → pagination/paginate.js} +2 -2
  7. package/src/{createRateLimiter.js → resilience/rateLimiter.js} +7 -4
  8. package/src/{withRetry.js → resilience/retry.js} +2 -2
  9. package/src/{retryAfterAware.js → resilience/retryAfterAware.js} +3 -3
  10. package/src/{downloadResumable.js → streams/download.js} +5 -5
  11. package/src/{streamSSE.js → streams/streamSSE.js} +5 -5
  12. package/src/{createMockFetch.js → testing/createMockFetch.js} +1 -1
  13. package/types/errors.d.ts +24 -0
  14. package/types/http.d.ts +180 -0
  15. package/types/index.d.ts +7 -538
  16. package/types/pagination.d.ts +62 -0
  17. package/types/resilience.d.ts +171 -0
  18. package/types/streams.d.ts +105 -0
  19. package/types/testing.d.ts +44 -0
  20. /package/{RECIPES.md → docs/recipes.md} +0 -0
  21. /package/src/{HTTPError.js → errors/HTTPError.js} +0 -0
  22. /package/src/{normalizeError.js → errors/normalizeError.js} +0 -0
  23. /package/src/{buildQuery.js → http/buildQuery.js} +0 -0
  24. /package/src/{_cache.js → http/cache/entry.js} +0 -0
  25. /package/src/{createMemoryCache.js → http/cache/memoryCache.js} +0 -0
  26. /package/src/{parseJsonSafe.js → http/parseJsonSafe.js} +0 -0
  27. /package/src/{_http.js → internal/http.js} +0 -0
  28. /package/src/{_sleep.js → internal/sleep.js} +0 -0
  29. /package/src/{createCircuitBreaker.js → resilience/circuitBreaker.js} +0 -0
  30. /package/src/{withDedupe.js → resilience/dedupe.js} +0 -0
  31. /package/src/{createLimiter.js → resilience/limiter.js} +0 -0
  32. /package/src/{withTimeout.js → resilience/timeout.js} +0 -0
  33. /package/src/{withProgress.js → streams/progress.js} +0 -0
  34. /package/src/{streams.js → streams/readers.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. Pull in the one function you need, or let [`createFetch`](#createfetchdefaults) wire them together for you — you always get a plain `Response` back.
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";
@@ -48,44 +47,19 @@ const pages = await limit.map(urls, (url) =>
48
47
  )
49
48
  );
50
49
  ```
51
- __
52
50
 
53
- ## Playground
54
-
55
- 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.
56
-
57
- 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.) The repo's `pages.yml` workflow publishes the same folder to GitHub Pages — enable it once under Settings → Pages → Source: "GitHub Actions".
58
-
59
- ## Works with what you already use
60
-
61
- 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).
62
-
63
- ## Runtime support
64
-
65
- | Runtime | Status |
66
- |---|---|
67
- | **Node 18, 20, 22, 24** | Primary target. Full test suite runs in CI on all four. |
68
- | **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. |
51
+ ## Install
69
52
 
70
- ```html
71
- <script type="module">
72
- import { createFetch } from "https://cdn.jsdelivr.net/npm/@boy-offi9-inc/reqkit@0.5.0/dist/reqkit.mjs";
73
- </script>
53
+ ```bash
54
+ npm install @boy-offi9-inc/reqkit
74
55
  ```
75
56
 
76
- ---
77
-
78
- ## Security
79
-
80
- reqkit handles data a server controls (headers, streams, redirects), so the defaults are conservative:
81
-
82
- - **Credentials stay home.** With a `baseUrl`, the default `auth` token and default `Authorization` / `Proxy-Authorization` / `Cookie` / `X-API-Key` / `X-Auth-Token` headers are only sent to that origin (plus `trustedOrigins`). Pass an absolute URL to another host and those headers are dropped — the classic "absolute URL leaks the token" bug (the one axios had) can't happen. Headers you pass *for a specific call* are deliberate and always sent. (With neither `baseUrl` nor `trustedOrigins` there's nothing to compare against, so set at least one.)
83
- - **A server can't steer pagination.** `paginateLinks` refuses a `Link: rel="next"` that points at another origin (`code: "PAGINATE_CROSS_ORIGIN"`) — otherwise a hostile API could send your requests, and any credentials you pass in `init`, to a third party. Allow extra origins with `origins: [...]`.
84
- - **Streams are bounded.** `readLines` / `readNDJSON` / `readSSE` run in linear time and throw `code: "LINE_TOO_LONG"` past `maxLineLength` (default 16 Mi characters) instead of buffering a never-ending line forever; `withProgress` and `downloadResumable` have `maxBytes`.
85
- - **Hostile headers are cheap.** `parseLinkHeader` is linear-time and ignores headers over 64 KB.
86
- - **Safe retries.** `createFetch` only retries idempotent methods (or requests with an `Idempotency-Key`), and a cross-origin redirect is left to `fetch`, which strips `Authorization` itself.
57
+ ```js
58
+ import { createFetch, withRetry } from "@boy-offi9-inc/reqkit"; // ESM
59
+ const { createFetch, withRetry } = require("@boy-offi9-inc/reqkit"); // CommonJS
60
+ ```
87
61
 
88
- What reqkit does **not** do: it doesn't block requests to private/internal addresses (SSRF protection for server-side use needs DNS-level control). If you fetch user-supplied URLs on a server, validate them first.
62
+ Requires Node 18+. TypeScript types are included.
89
63
 
90
64
  ## Pick what you need
91
65
 
@@ -101,7 +75,8 @@ What reqkit does **not** do: it doesn't block requests to private/internal addre
101
75
  | Run N things at a time | [`createLimiter`](#createlimiterconcurrency) |
102
76
  | Slow down *before* the server says 429 (obey `X-RateLimit-*`) | [`createRateLimiter`](#createratelimiteropts) |
103
77
  | Stop hammering a service that keeps failing | [`createCircuitBreaker`](#createcircuitbreakeropts) |
104
- | 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) |
105
80
  | Attach a token and refresh it on 401 — once, even with many requests in flight | [`createFetch` → `auth`](#auth-token-refresh) |
106
81
  | Walk a paginated API (cursor, offset, `Link` headers) | [`paginate` / `paginateLinks`](#pagination-paginate-paginatepages-paginatelinks) |
107
82
  | Resume a download after the connection drops | [`downloadResumable`](#downloadresumableurl-opts) |
@@ -110,26 +85,48 @@ What reqkit does **not** do: it doesn't block requests to private/internal addre
110
85
  | Test retry/timeout code without a network | [`createMockFetch`](#createmockfetch) |
111
86
  | All of the above, wired up | [`createFetch`](#createfetchdefaults) |
112
87
 
113
- ---
88
+ ## Why reqkit
114
89
 
115
- ## Why this exists
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.
116
95
 
117
- Most HTTP retry/timeout wrappers bundle everything into a client you have to fully adopt. This is the opposite: standalone functions that compose with whatever you're already using. Zero dependencies, CJS + ESM, TypeScript types included.
96
+ ## Runtime support
118
97
 
119
- **Honest note:** retries, timeouts and error normalization are a well-covered problem — there are other solid packages doing similar things (`fetchy`, `fetchpilot`, `p-retry`, to name a few). What reqkit leans on:
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. |
120
102
 
121
- - **It reads the server.** Retries wait for `Retry-After` / `X-RateLimit-Reset` instead of blind backoff on 429s — most retry libraries treat rate-limit responses like any other failure.
122
- - **It's safe by default.** `createFetch` only retries idempotent methods, so a timed-out `POST` can't double-charge anyone.
123
- - **It understands real-world errors.** Node's `TypeError: fetch failed` and the browser's `Failed to fetch` are classified as network errors, and a deliberate abort is never reported as a timeout.
124
- - **It's not tied to a client.** `withProgress`, `withDedupe` and `createLimiter` work with any streamed body or async function.
103
+ ```js
104
+ // Deno
105
+ import { createFetch } from "npm:@boy-offi9-inc/reqkit";
106
+ ```
125
107
 
126
- ---
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
+ ```
127
114
 
128
- ## Install
115
+ ## Playground and recipes
129
116
 
130
- ```bash
131
- npm install @boy-offi9-inc/reqkit
132
- ```
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).
133
130
 
134
131
  ---
135
132
 
@@ -145,7 +142,7 @@ const data = await withRetry(
145
142
  { retries: 3, baseDelayMs: 300, onRetry: (err, attempt, delayMs) => console.log(`retry ${attempt} in ${delayMs}ms`) }
146
143
  );
147
144
 
148
- // fetch doesn't throw on 4xx/5xx — retry on statuses by returning the Response:
145
+ // fetch doesn't throw on 4xx/5xx, so list the statuses to retry:
149
146
  const res = await withRetry(() => fetch(url), { retryStatusCodes: [429, 500, 502, 503, 504] });
150
147
  ```
151
148
 
@@ -169,16 +166,15 @@ const res = await withRetry(() => fetch(url), { retryStatusCodes: [429, 500, 502
169
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.
170
167
 
171
168
  ```js
172
- // Cancel a long retry/backoff sequence externally — e.g. the user navigated away
173
169
  const controller = new AbortController();
174
170
  const promise = withRetry(() => fetch(url), { retries: 5, signal: controller.signal });
175
171
  controller.abort(); // rejects with RetryAbortedError, even mid-backoff
176
172
  ```
177
173
 
178
- **Stop early with `StopRetry`** — throw it from inside `fn` when an error is not worth retrying (bad credentials, a 400...):
174
+ **`StopRetry`** — throw it from inside `fn` to stop retrying immediately:
179
175
 
180
176
  ```js
181
- const { withRetry, StopRetry } = require("@boy-offi9-inc/reqkit");
177
+ import { withRetry, StopRetry } from "@boy-offi9-inc/reqkit";
182
178
 
183
179
  await withRetry(async () => {
184
180
  const res = await fetch(url);
@@ -289,7 +285,6 @@ Coalesces concurrent calls with the same arguments into a single in-flight call
289
285
  ```js
290
286
  const getUser = withDedupe((id) => fetch(`/api/users/${id}`).then(r => r.json()));
291
287
 
292
- // Only one network request goes out — both callers share it.
293
288
  const [a, b] = await Promise.all([getUser(1), getUser(1)]);
294
289
  ```
295
290
 
@@ -302,7 +297,6 @@ Plain arguments are compared by value (object key order doesn't matter); anythin
302
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.
303
298
 
304
299
  ```js
305
- // Custom key when the default arg-based key isn't quite right
306
300
  const fn = withDedupe((req) => doFetch(req), { keyFn: (req) => req.url });
307
301
  ```
308
302
 
@@ -479,10 +473,10 @@ String matchers match the full URL, `pathname + search`, or the bare `pathname`;
479
473
 
480
474
  ### `createFetch(defaults)`
481
475
 
482
- Opt-in assembly of the pieces above into a fetch-like function. It's still plain `fetch` underneath: you get a standard `Response`, and you can pass your own `fetch`.
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`.
483
477
 
484
478
  ```js
485
- const { createFetch } = require("@boy-offi9-inc/reqkit");
479
+ import { createFetch, HTTPError } from "@boy-offi9-inc/reqkit";
486
480
 
487
481
  const api = createFetch({
488
482
  baseUrl: "https://api.example.com/v1",
@@ -523,6 +517,7 @@ const admin = api.extend({ headers: { "x-role": "admin" } }); // merges over the
523
517
  | `fetch` | `globalThis.fetch` | swap in undici, node-fetch, a test double... |
524
518
  | `onRequest(ctx)` | — | once per call; may mutate `ctx.url` / `ctx.init` (e.g. add a header) |
525
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) |
526
521
 
527
522
  #### Auth token refresh
528
523
 
@@ -546,6 +541,35 @@ await Promise.all(urls.map((u) => api.json(u))); // 20 requests, all 401 → one
546
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.
547
542
  - `extend()` children that share the same `auth` object share one refresh; the cache keys entries per token.
548
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
+
549
573
  #### Caching
550
574
 
551
575
  ```js
@@ -564,10 +588,13 @@ await api.json("/config"); // fresh → served from memory, no request
564
588
  |---|---|---|
565
589
  | `ttl` | `60000` | serve without asking the server for this long |
566
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 |
567
592
  | `maxEntries` | `100` | size of the built-in LRU store |
568
593
  | `maxBodyBytes` | `1 MiB` | bigger responses aren't cached |
569
594
  | `store` | built-in LRU | bring your own (`get`/`set`/`delete`, sync or async) — Redis, IndexedDB, a `Map`… |
570
- | `onEvent` | — | `({ type: "hit" \| "stale" \| "revalidated" \| "miss" \| "store", url })` for logging/metrics |
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.
571
598
 
572
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.
573
600
 
@@ -575,15 +602,14 @@ Only `GET` `200` responses are cached; `Cache-Control: no-store` and `Vary: *` a
575
602
 
576
603
  ---
577
604
 
578
- ## Design notes
605
+ ## Prior art
579
606
 
580
- - **Zero dependencies.** Nothing to audit, nothing to break underneath you.
581
- - **CJS + ESM.** `require()` and `import` both work.
582
- - **Composable, not a client.** Every function takes and returns plain values (`Response`, plain objects, `Uint8Array`) — nothing proprietary to learn.
583
- - **TypeScript types included**, hand-written (no build step to go wrong), with separate entries for CJS and ESM.
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.
584
608
 
585
- ---
609
+ ## Contributing
586
610
 
587
- ## Prior art
611
+ Issues and pull requests are welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md) and the [architecture overview](./docs/architecture.md).
612
+
613
+ ## License
588
614
 
589
- reqkit's building blocks are small, from-scratch implementations of ideas that proved themselves in popular libraries — go look at them, they're great: [`p-retry`](https://github.com/sindresorhus/p-retry) (`StopRetry`, time budgets), [`ky`](https://github.com/sindresorhus/ky) and [`ofetch`](https://github.com/unjs/ofetch) (`HTTPError`, `json`/`query`/`baseUrl`, `extend`, retry statuses), [`got`](https://github.com/sindresorhus/got) and [`axios-retry`](https://github.com/softonic/axios-retry) (retry only idempotent methods, `calculateDelay`), [`p-limit`](https://github.com/sindresorhus/p-limit) (concurrency limiter), [`opossum`](https://github.com/nodeshift/opossum) (circuit breaker), [`bottleneck`](https://github.com/SGrondin/bottleneck) / [`p-throttle`](https://github.com/sindresorhus/p-throttle) (rate limiting), [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) (SSE), [`nock`](https://github.com/nock/nock) / undici's `MockAgent` (mocking), the HTTP caching RFCs (`stale-while-revalidate`, conditional requests), and the AWS Architecture Blog's "full jitter" backoff.
615
+ [MIT](./LICENSE) © Boy Offi9