{"_id":"@billdaddy/retryfn","_rev":"2-b2954b6809dc57c14e7f10aceab75f5a","name":"@billdaddy/retryfn","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@billdaddy/retryfn","version":"0.1.0","keywords":["retry","backoff","exponential-backoff","jitter","abortsignal","timeout","retry-after","rate-limit","fetch","resilience","async","zero-dependency"],"author":{"url":"https://github.com/trananhtung","name":"Tung Tran"},"license":"MIT","_id":"@billdaddy/retryfn@0.1.0","maintainers":[{"name":"billdaddy","email":"tunganhtran94@gmail.com"}],"homepage":"https://github.com/trananhtung/retryfn#readme","bugs":{"url":"https://github.com/trananhtung/retryfn/issues"},"bin":{"retryfn":"dist/cli.js"},"dist":{"shasum":"f3c4c877966765ae9e3f899b058f12f32e3b46b9","tarball":"https://registry.npmjs.org/@billdaddy/retryfn/-/retryfn-0.1.0.tgz","fileCount":17,"integrity":"sha512-PZwigKI4jRX2E1m7iMrhPV+WYi3UUrnc4dM4YAoh+XAGrnvcKP57PZTkU9vq4ljzDMMEauKJVZXMIK4NorRBSQ==","signatures":[{"sig":"MEUCIEExnu25nNfd/U64W9cEUcoc29HnF/2t1RF3DQ5TOYZOAiEAqOoIXbu71EvtTIGAu7uTuD9jvOrWO24AM7QEMskkJq0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":82480},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"a49c71fb9b8b6da15b1c564aec5274b406e26ca9","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run build"},"_npmUser":{"name":"billdaddy","email":"tunganhtran94@gmail.com"},"repository":{"url":"git+https://github.com/trananhtung/retryfn.git","type":"git"},"_npmVersion":"11.5.2","description":"Retry async functions with exponential backoff, jitter, AbortSignal, per-attempt timeouts, and Retry-After awareness. Zero dependencies.","directories":{},"sideEffects":false,"_nodeVersion":"20.18.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.7.2","@types/node":"^20.17.10"},"_npmOperationalInternal":{"tmp":"tmp/retryfn_0.1.0_1782029736540_0.41128917600436377","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@billdaddy/retryfn","version":"0.1.1","description":"Retry async functions with exponential backoff, jitter, AbortSignal, per-attempt timeouts, and Retry-After awareness. Zero dependencies.","keywords":["retry","backoff","exponential-backoff","jitter","abortsignal","timeout","retry-after","rate-limit","fetch","resilience","async","zero-dependency"],"license":"MIT","author":{"name":"Tung Tran","url":"https://github.com/trananhtung"},"homepage":"https://github.com/trananhtung/retryfn#readme","repository":{"type":"git","url":"git+https://github.com/trananhtung/retryfn.git"},"bugs":{"url":"https://github.com/trananhtung/retryfn/issues"},"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"bin":{"retryfn":"dist/cli.js"},"engines":{"node":">=18"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","prepublishOnly":"npm run build"},"devDependencies":{"@types/node":"^20.17.10","tsup":"^8.3.5","typescript":"^5.7.2","vitest":"^2.1.8"},"sideEffects":false,"publishConfig":{"access":"public"},"_id":"@billdaddy/retryfn@0.1.1","gitHead":"aebc1ec37b2f6705e65828be0e7985ba73e3fc57","_nodeVersion":"20.18.2","_npmVersion":"11.5.2","dist":{"integrity":"sha512-TcxTNwm3a9KmSCFV1VIZN2ACeKGldqXjv9yURqE+V3NFrO1CAfa2z218eByu0Vf6lc/o4z9Uob5phAsE33RCAA==","shasum":"968e6fd1e29ba120aa3f27ea3a1aba5a76746d19","tarball":"https://registry.npmjs.org/@billdaddy/retryfn/-/retryfn-0.1.1.tgz","fileCount":17,"unpackedSize":83700,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDbfG4HGD4iQTuvOcrhJfr2Oxr3FcEV0tse0Ep9hd6DzAiEAvTHCK+f4WBWv1tTAOLBvgU6ladMkJihjeLNR6FOvzqk="}]},"_npmUser":{"name":"billdaddy","email":"tunganhtran94@gmail.com"},"directories":{},"maintainers":[{"name":"billdaddy","email":"tunganhtran94@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/retryfn_0.1.1_1782140221088_0.6316123046253832"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-21T08:15:36.397Z","modified":"2026-06-22T14:57:01.388Z","0.1.0":"2026-06-21T08:15:36.675Z","0.1.1":"2026-06-22T14:57:01.256Z"},"bugs":{"url":"https://github.com/trananhtung/retryfn/issues"},"author":{"name":"Tung Tran","url":"https://github.com/trananhtung"},"license":"MIT","homepage":"https://github.com/trananhtung/retryfn#readme","keywords":["retry","backoff","exponential-backoff","jitter","abortsignal","timeout","retry-after","rate-limit","fetch","resilience","async","zero-dependency"],"repository":{"type":"git","url":"git+https://github.com/trananhtung/retryfn.git"},"description":"Retry async functions with exponential backoff, jitter, AbortSignal, per-attempt timeouts, and Retry-After awareness. Zero dependencies.","maintainers":[{"name":"billdaddy","email":"tunganhtran94@gmail.com"}],"readme":"# retryfn\n\n[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)\n\n> Retry async functions with **exponential backoff, jitter, `AbortSignal`, per-attempt timeouts**, and **`Retry-After` awareness**. **Zero dependencies**.\n\n[![CI](https://github.com/trananhtung/retryfn/actions/workflows/ci.yml/badge.svg)](https://github.com/trananhtung/retryfn/actions/workflows/ci.yml)\n[![npm version](https://img.shields.io/npm/v/@billdaddy/retryfn.svg)](https://www.npmjs.com/package/@billdaddy/retryfn)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/@billdaddy/retryfn)](https://bundlephobia.com/package/@billdaddy/retryfn)\n[![types](https://img.shields.io/npm/types/@billdaddy/retryfn.svg)](https://www.npmjs.com/package/@billdaddy/retryfn)\n[![license](https://img.shields.io/npm/l/@billdaddy/retryfn.svg)](./LICENSE)\n\nTransient failures are a fact of life: a dropped connection, a `429 Too Many\nRequests` from an LLM API, a service that's briefly `503`. `retryfn` retries the\noperation the right way — exponential backoff with jitter so you don't stampede,\n**honoring the server's `Retry-After` header** when it sends one, with real\n`AbortSignal` and per-attempt timeout support.\n\n```ts\nimport { retry } from \"@billdaddy/retryfn\";\n\nconst data = await retry(\n  async ({ signal }) => {\n    const res = await fetch(url, { signal });\n    if (res.status === 429 || res.status >= 500) {\n      throw Object.assign(new Error(`HTTP ${res.status}`), { response: res });\n    }\n    return res.json();\n  },\n  { retries: 5, timeout: 10_000 },\n);\n```\n\nIf that `429` carried `Retry-After: 2`, the next attempt waits exactly 2 seconds —\nnot a guessed backoff.\n\n## Why retryfn?\n\n- **Server-aware.** Reads a `Retry-After` hint (seconds *or* HTTP-date, from a\n  `Headers` object or a plain record) and waits exactly that long.\n- **Cancellable.** Each attempt gets an `AbortSignal` driven by your `timeout` and\n  your own external `signal` — forward it straight to `fetch`.\n- **Good backoff by default.** Exponential growth with **full jitter**, capped by\n  `maxDelay` and an optional total `maxElapsed` budget.\n- **Precise control.** `shouldRetry(error)` to decide per-error, `onRetry` to\n  observe, deterministic via an injectable `rng`.\n- **Zero dependencies**, ESM + CJS + types, and a CLI to retry shell commands.\n\n## Install\n\n```bash\nnpm install @billdaddy/retryfn\n# or: pnpm add @billdaddy/retryfn  /  yarn add @billdaddy/retryfn  /  bun add @billdaddy/retryfn\n```\n\n## API\n\n### `retry(fn, options?) → Promise<T>`\n\n`fn` receives `{ attempt, signal }`. Throw to trigger a retry; return to resolve.\n\n| Option            | Type                                   | Default  | Description                                       |\n| ----------------- | -------------------------------------- | -------- | ------------------------------------------------- |\n| `retries`         | `number`                               | `3`      | Retries after the first try (4 attempts total).   |\n| `minDelay`        | `number` (ms)                          | `200`    | Base delay for the first retry.                   |\n| `maxDelay`        | `number` (ms)                          | `30000`  | Cap on a single computed delay.                   |\n| `factor`          | `number`                               | `2`      | Exponential multiplier.                           |\n| `jitter`          | `\"full\" \\| \"equal\" \\| \"none\"`          | `\"full\"` | Randomisation strategy.                           |\n| `maxElapsed`      | `number` (ms)                          | —        | Total time budget across all attempts/waits.      |\n| `timeout`         | `number` (ms)                          | —        | Per-attempt timeout (aborts the attempt signal).  |\n| `signal`          | `AbortSignal`                          | —        | Cancel the whole operation.                       |\n| `honorRetryAfter` | `boolean`                              | `true`   | Prefer a `Retry-After` hint over backoff.         |\n| `shouldRetry`     | `(error, attempt) => boolean \\| Promise` | retry all | Decide whether an error is retryable.           |\n| `onRetry`         | `({error, attempt, delay}) => void`    | —        | Observe each scheduled retry.                     |\n\nThrows the last error when retries are exhausted, `shouldRetry` returns `false`,\nor the external `signal` aborts.\n\n### `calcBackoff(attempt, options?, rng?) → number`\n\nThe backoff math on its own (zero-based `attempt`), exported for reuse and testing.\n\n### `getRetryAfterMs(error, now?) → number | undefined`\n\nExtract a `Retry-After` wait (ms) from an error: `error.retryAfterMs`,\n`error.retryAfter` (seconds), or a `Retry-After` header on\n`error.response.headers` / `error.headers`.\n\n### `isAbortError(err) → boolean`\n\n`true` for `AbortError` / `TimeoutError` thrown via an `AbortSignal`.\n\n## Recipes\n\n**Only retry network/5xx, never 4xx (except 429):**\n\n```ts\nawait retry(call, {\n  shouldRetry: (err) => {\n    const s = (err as any).response?.status;\n    return s == null || s === 429 || s >= 500;\n  },\n});\n```\n\n**Hard ceiling on total time:**\n\n```ts\nawait retry(call, { retries: 20, maxElapsed: 15_000 });\n```\n\n## CLI\n\nRetry a shell command until it succeeds:\n\n```bash\nretryfn -r 5 -- curl -fsS https://flaky.example.com/health\nretryfn --min 1000 --factor 3 -- ./deploy.sh\n```\n\n## Contributors ✨\n\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome — code, docs, bug reports, ideas, reviews! See the [emoji key](https://allcontributors.org/docs/en/emoji-key) for how each contribution is recognized, and open a PR or issue to get involved.\n\nThanks goes to these wonderful people:\n\n<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->\n<!-- prettier-ignore-start -->\n<!-- markdownlint-disable -->\n<table>\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/trananhtung\"><img src=\"https://avatars.githubusercontent.com/u/30992229?v=4?s=100\" width=\"100px;\" alt=\"Tung Tran\"/><br /><sub><b>Tung Tran</b></sub></a><br /><a href=\"https://github.com/trananhtung/retryfn/commits?author=trananhtung\" title=\"Code\">💻</a> <a href=\"#maintenance-trananhtung\" title=\"Maintenance\">🚧</a></td>\n    </tr>\n  </tbody>\n</table>\n\n<!-- markdownlint-restore -->\n<!-- prettier-ignore-end -->\n\n<!-- ALL-CONTRIBUTORS-LIST:END -->\n\n## License\n\n[MIT](./LICENSE) © Tung Tran\n","readmeFilename":"README.md"}