{"_id":"@boy-offi9-inc/reqkit","_rev":"6-8e8fa7d54c4c3dca7b5fc37a8cb00c28","name":"@boy-offi9-inc/reqkit","dist-tags":{"latest":"0.1.7"},"versions":{"0.1.0":{"name":"@boy-offi9-inc/reqkit","version":"0.1.0","keywords":["http","fetch","retry","backoff","timeout","rate-limit","retry-after","download-progress","error-normalization","utility"],"author":{"name":"Boy Offi9"},"license":"MIT","_id":"@boy-offi9-inc/reqkit@0.1.0","maintainers":[{"name":"boy-offi9-inc","email":"boyoffiinc@gmail.com"}],"dist":{"shasum":"2950633e9da719fe2f9994a7af61dfe674f4ee8a","tarball":"https://registry.npmjs.org/@boy-offi9-inc/reqkit/-/reqkit-0.1.0.tgz","fileCount":14,"integrity":"sha512-dE2ZevOoOyAszN6Py5K5imW0qqfA3y5r0CmIi0meiTwgLxnlO8ZibdNBMZPOxa8K2sGRamYkHINjBpM1IOA01g==","signatures":[{"sig":"MEYCIQC8waOFYbaFO/2SQt4+lm4CqIxBHZhDXTzXTTlUyuHgZwIhAIqMUoZ7EMHo77KZBGllFHfmkwr+Jjv94zk1cse0FL9y","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":23643},"main":"./src/index.js","types":"./types/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./types/index.d.ts","import":"./src/index.mjs","require":"./src/index.js"},"./package.json":"./package.json"},"gitHead":"8a24ff832c53c3d8b5285afc9e9d3e2015e47e0c","scripts":{"test":"node --test"},"_npmUser":{"name":"boy-offi9-inc","email":"boyoffiinc@gmail.com"},"_npmVersion":"11.6.2","description":"Small, composable HTTP helper functions — retry with backoff, timeouts, normalized errors, safe JSON parsing, rate-limit-aware retry, and download progress. Not a client, works alongside fetch/axios/anything.","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/reqkit_0.1.0_1786834241869_0.9163734753046331","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@boy-offi9-inc/reqkit","version":"0.1.2","keywords":["http","fetch","retry","backoff","timeout","rate-limit","retry-after","download-progress","error-normalization","utility"],"author":{"name":"Boy Offi9"},"license":"MIT","_id":"@boy-offi9-inc/reqkit@0.1.2","maintainers":[{"name":"boy-offi9-inc","email":"boyoffiinc@gmail.com"}],"dist":{"shasum":"fcb77b66c262b330ca8c591b579d7ba563d1ea02","tarball":"https://registry.npmjs.org/@boy-offi9-inc/reqkit/-/reqkit-0.1.2.tgz","fileCount":13,"integrity":"sha512-mFIDGhd0IYZTsv0rBnghW6kWeiNxGBvYM6Mbu7OFMuDGlmBuOOQuY+9rPam5a58DicvpTsivOG73Kp4JpR20qw==","signatures":[{"sig":"MEUCIA5hC8ZIsocQnFKywRu4NU4pr7gtZLri4RHWrecK7/1cAiEAiLKfrW/fPPCGJKDS/VdAsjwLpWYxlkZMTEBBGMlZHms=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":21984},"main":"./src/index.js","types":"./types/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./types/index.d.ts","import":"./src/index.mjs","require":"./src/index.js"},"./package.json":"./package.json"},"gitHead":"eec32fbd5ed5315141edcf23c27bb10ee6eb09c1","scripts":{"test":"node --test"},"_npmUser":{"name":"boy-offi9-inc","email":"boyoffiinc@gmail.com"},"_npmVersion":"11.6.2","description":"Small, composable HTTP helper functions — retry with backoff, timeouts, normalized errors, safe JSON parsing, rate-limit-aware retry, and download progress. Not a client, works alongside fetch/axios/anything.","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/reqkit_0.1.2_1786961503670_0.2494018613247868","host":"s3://npm-registry-packages-npm-production"}},"0.1.7":{"name":"@boy-offi9-inc/reqkit","version":"0.1.7","description":"Small, composable HTTP helper functions — retry with backoff, timeouts, normalized errors, safe JSON parsing, rate-limit-aware retry, and download progress. Not a client, works alongside fetch/axios/anything.","main":"./src/index.js","types":"./types/index.d.ts","exports":{".":{"types":"./types/index.d.ts","require":"./src/index.js","import":"./src/index.mjs"},"./package.json":"./package.json"},"sideEffects":false,"scripts":{"test":"node --test"},"keywords":["http","fetch","retry","backoff","timeout","rate-limit","retry-after","download-progress","error-normalization","utility"],"author":{"name":"Boy Offi9"},"license":"MIT","publishConfig":{"access":"public"},"engines":{"node":">=18"},"gitHead":"d80370abbc4e918541f40440fa47ed22c519d2c8","_id":"@boy-offi9-inc/reqkit@0.1.7","_nodeVersion":"24.13.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-5pbi3toiZJzjS0teiJ8Z4M8ABFAV+aL/ePtCd0v69rYRhTLfSlIzfwut2QfVg9eQYmCYk4Mqo7XH5+bh+/SpqQ==","shasum":"3ab4500337e772bfc40719c861a4abf25f96615c","tarball":"https://registry.npmjs.org/@boy-offi9-inc/reqkit/-/reqkit-0.1.7.tgz","fileCount":14,"unpackedSize":26393,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCIfhU62XTaNZOx+XcYbKAWPxKRMwFJO5ZJ6XXSJyIRcgIhAPnEthgmvU/fwuvzdqqCR8DCBRSL1nXEKDH/bEH51ktE"}]},"_npmUser":{"name":"boy-offi9-inc","email":"boyoffiinc@gmail.com"},"directories":{},"maintainers":[{"name":"boy-offi9-inc","email":"boyoffiinc@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/reqkit_0.1.7_1787955182205_0.018853209031699958"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-15T22:50:41.642Z","modified":"2026-08-28T22:13:02.530Z","0.1.1":"2026-08-15T12:07:32.803Z","0.1.0":"2026-08-15T22:50:42.017Z","0.1.2":"2026-08-17T10:11:43.807Z","0.1.7":"2026-08-28T22:13:02.368Z"},"author":{"name":"Boy Offi9"},"license":"MIT","keywords":["http","fetch","retry","backoff","timeout","rate-limit","retry-after","download-progress","error-normalization","utility"],"description":"Small, composable HTTP helper functions — retry with backoff, timeouts, normalized errors, safe JSON parsing, rate-limit-aware retry, and download progress. Not a client, works alongside fetch/axios/anything.","maintainers":[{"name":"boy-offi9-inc","email":"boyoffiinc@gmail.com"}],"readme":"<p align=\"center\">\n  <a href=\"https://www.npmjs.com/package/@boy-offi9-inc/reqkit\" aria-label=\"npm\">\n    <img src=\"https://i.ibb.co/1f63mDTg/logo-wordmark.png\" alt=\"reqkit on npm\" width=\"480\" />\n  </a>\n</p>\n\n<p align=\"center\">\n  <a href=\"./LICENSE\"><img src=\"https://img.shields.io/badge/License-MIT-yellow.svg\" alt=\"License: MIT\"></a>\n  <img src=\"https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white\" alt=\"Node >=18\">\n  <img src=\"https://img.shields.io/badge/npm-%40boy--offi9--inc%2Freqkit-CB3837?logo=npm&logoColor=white\" alt=\"npm package\">\n  <img src=\"https://img.shields.io/badge/dependencies-zero-brightgreen\" alt=\"Zero dependencies\">\n  <img src=\"https://img.shields.io/badge/module-CJS%20%2B%20ESM-blue\" alt=\"CJS + ESM\">\n  <img src=\"https://img.shields.io/badge/types-included-3178C6?logo=typescript&logoColor=white\" alt=\"TypeScript types included\">\n</p>\n\n# reqkit\n\nSmall, composable HTTP helper functions. Not a client, not an axios replacement — works alongside `fetch`, `axios`, or anything else you're already using. Pull in the one function you need; ignore the rest.\n\n```js\nconst { withRetry, normalizeError } = require(\"@boy-offi9-inc/reqkit\");\n\nconst data = await withRetry(() => fetch(url).then(r => r.json()));\n```\n\n---\n\n## Why this exists\n\nMost 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, and a couple of small helpers that you can pick up independently.\n\n**Honest note:** `withRetry`, `withTimeout`, and `normalizeError` solve a well-covered problem — there are other solid packages doing similar things (`fetchy`, `fetchpilot`, `p-retry`, to name a few).\n\n- **`retryAfterAware`** — actually reads `Retry-After`/`X-RateLimit-Reset` headers instead of blind backoff on 429s. Most retry libraries treat rate-limit responses like any other failure and ignore the server's suggested retry time.\n- **`withProgress`** — progress callback for any streamed response body (downloads, large payloads, anything), not tied to a specific HTTP client.\n- **`withDedupe`** — coalesces concurrent calls with the same key into one in-flight call, so simultaneous requests for the same resource don't each hit the network.\n\nZero dependencies. CJS + ESM. TypeScript types included.\n\n---\n\n## Install\n\n```bash\nnpm install @boy-offi9-inc/reqkit\n```\n\n---\n\n## API\n\n### `withRetry(fn, opts?)`\n\nRetries an async function with exponential backoff + jitter.\n\n```js\nconst data = await withRetry(\n  () => fetch(url).then(r => r.json()),\n  { retries: 3, baseDelayMs: 300, onRetry: (err, attempt, delayMs) => console.log(`retry ${attempt} in ${delayMs}ms`) }\n);\n```\n\n| Option | Default | Description |\n|---|---|---|\n| `retries` | `3` | max retry attempts after the first try |\n| `baseDelayMs` | `300` | initial delay |\n| `maxDelayMs` | `10000` | delay ceiling |\n| `shouldRetry` | retries anything but AbortError | `(err, attempt) => boolean` |\n| `onRetry` | — | `(err, attempt, delayMs) => void` |\n| `signal` | — | `AbortSignal` — cancels the whole retry loop, including any pending backoff wait, throwing `RetryAbortedError` |\n\n```js\n// Cancel a long retry/backoff sequence externally — e.g. the user navigated away\nconst controller = new AbortController();\nconst promise = withRetry(() => fetch(url), { retries: 5, signal: controller.signal });\ncontroller.abort(); // rejects with RetryAbortedError, even mid-backoff\n```\n\n### `withTimeout(fn, ms)`\n\nRaces an async function against a timeout, passing it an `AbortSignal` so the underlying request can actually be cancelled.\n\n```js\nconst data = await withTimeout(\n  signal => fetch(url, { signal }).then(r => r.json()),\n  5000\n);\n// throws TimeoutError on timeout, regardless of whether fn cooperates with the signal\n```\n\n### `normalizeError(err)`\n\nOne consistent error shape regardless of source (fetch, axios, node http, generic):\n\n```js\nconst { message, status, code, isNetworkError, isTimeout } = normalizeError(err);\n```\n\nEvery field is always present (`null`/`false` when not applicable) — no existence checks needed.\n\n### `parseJsonSafe(input)`\n\nNever throws on malformed JSON. Accepts a `Response` or a raw string.\n\n```js\nconst { data, error } = await parseJsonSafe(response);\nif (error) { /* handle malformed JSON without a try/catch */ }\n```\n\n### `buildQuery(params)`\n\nMinimal query string builder — skips `null`/`undefined`, repeats the key for arrays, encodes everything.\n\n```js\nbuildQuery({ q: \"hello world\", tag: [\"a\", \"b\"] });\n// \"q=hello%20world&tag=a&tag=b\"\n```\n\nFor full parsing/nested-object support, use [`query-string`](https://www.npmjs.com/package/query-string) instead — this only covers the common flat-object case with zero dependencies.\n\n### `retryAfterAware(fn, opts?)`\n\nRetries a fetch-like call, honoring `Retry-After`/`X-RateLimit-Reset` headers on 429/503 instead of blind backoff.\n\n```js\nconst response = await retryAfterAware(\n  () => fetch(url),\n  { retries: 3, retryStatusCodes: [429, 503] }\n);\n```\n\n| Option | Default | Description |\n|---|---|---|\n| `retries` | `3` | |\n| `retryStatusCodes` | `[429, 503]` | |\n| `fallbackDelayMs` | `1000` | used when no usable header is present |\n| `maxDelayMs` | `60000` | |\n| `onRetry` | — | `(status, waitMs, attempt) => void` |\n\n### `withProgress(response, onProgress?)`\n\nReads a streamed `Response` body while reporting progress, returns the collected bytes as a `Uint8Array`.\n\n```js\nconst bytes = await withProgress(response, ({ loaded, total, percent }) => {\n  console.log(`${percent ?? \"?\"}% (${loaded}/${total ?? \"?\"} bytes)`);\n});\n```\n\nWorks without `Content-Length` too — `total`/`percent` are just `null` in that case, `loaded` is still accurate. Falls back to a single-shot read (still calling `onProgress` once) if the response body is not a stream.\n\n### `withDedupe(fn, opts?)`\n\nCoalesces concurrent calls with the same arguments into a single in-flight call — every caller gets the same result, but the underlying request only fires once. Useful when several parts of a UI ask for the same resource around the same time.\n\n```js\nconst getUser = withDedupe((id) => fetch(`/api/users/${id}`).then(r => r.json()));\n\n// Only one network request goes out — both callers share it.\nconst [a, b] = await Promise.all([getUser(1), getUser(1)]);\n```\n\n| Option | Default | Notes |\n| --- | --- | --- |\n| `keyFn` | `(...args) => JSON.stringify(args)` | derives the dedupe key from call args |\n\nThis is deduplication of *concurrent* calls, not a cache — once a call settles (success or failure), the next call with the same key starts fresh.\n\n```js\n// Custom key when the default arg-based key isn't quite right\nconst fn = withDedupe((req) => doFetch(req), { keyFn: (req) => req.url });\n```\n\n---\n\n## Design notes\n\n- **Zero dependencies.** Nothing to audit, nothing to break underneath you.\n- **CJS + ESM.** `require()` and `import` both work.\n- **Composable, not a client.** Every function takes and returns plain values (`Response`, plain objects, `Uint8Array`) — nothing proprietary to learn.\n- **TypeScript types included**, hand-written (no build step to go wrong).\n","readmeFilename":"README.md"}