{"_id":"@backendkit-labs/retry","_rev":"3-f833974ab605aa6d062fcfcaecbaf1b5","name":"@backendkit-labs/retry","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.1":{"name":"@backendkit-labs/retry","version":"0.1.1","keywords":["retry","again","resilience","backoff","fault-tolerance","circuit-breaker","bulkhead","nestjs","node","backend"],"author":{"name":"BackendKit Labs","email":"hello@backendkitlabs.dev"},"license":"Apache-2.0","_id":"@backendkit-labs/retry@0.1.1","maintainers":[{"name":"backendkit.dev","email":"backendkit.dev@gmail.com"}],"homepage":"https://backendkitlabs.dev/docs/retry/","bugs":{"url":"https://github.com/BackendKit-labs/backendkit-monorepo/issues"},"dist":{"shasum":"71ba1f1eca633696d101ed13b1244846cb6c9c4b","tarball":"https://registry.npmjs.org/@backendkit-labs/retry/-/retry-0.1.1.tgz","fileCount":20,"integrity":"sha512-PtBWaTNVUh65SzQ2Wnh9w+CeMe3mebIk77e4/rThItQNvUaoQcTjLgEn6ExDwaaN5jrM2ZJRLQ365GGBZkzjAA==","signatures":[{"sig":"MEUCIHGhirX7di6Jl5wy9MI7AJt6hqMW7bxgVNU7+psLsIe3AiEAxO+5+xqcd5fv3K1m3L5Iiy+QjEd5rPWRfLTsfm5JejM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":256086},"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"},"./nestjs":{"types":"./dist/nestjs/index.d.ts","import":"./dist/nestjs/index.js","require":"./dist/nestjs/index.cjs"}},"gitHead":"ef9c9f6f2ef9d8c17aef7d082c9e9179c8ece74c","scripts":{"dev":"tsup --watch","lint":"eslint src/","test":"vitest run","build":"tsup","format":"prettier --write src/","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build && npm run test && npm run lint"},"_npmUser":{"name":"backendkit.dev","email":"backendkit.dev@gmail.com"},"repository":{"url":"git+https://github.com/BackendKit-labs/backendkit-monorepo.git","type":"git","directory":"packages/retry"},"_npmVersion":"11.8.0","description":"Enterprise-grade retry library for Node.js -- exponential backoff, sliding window budget, error classification, duck-typed integrations, and optional NestJS support","directories":{},"sideEffects":false,"_nodeVersion":"22.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rxjs":"^7.8.0","tsup":"^8.0.0","eslint":"^9.0.0","vitest":"^2.0.0","prettier":"^3.0.0","@eslint/js":"^9.39.4","typescript":"^5.5.0","@types/node":"^22.0.0","@nestjs/core":"^10.0.0","@nestjs/common":"^10.0.0","reflect-metadata":"^0.2.0","typescript-eslint":"^8.59.3"},"peerDependencies":{"rxjs":">=7.0.0","@nestjs/core":">=10.0.0","@nestjs/common":">=10.0.0"},"peerDependenciesMeta":{"rxjs":{"optional":true},"@nestjs/core":{"optional":true},"@nestjs/common":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/retry_0.1.1_1779229942446_0.451660725144827","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@backendkit-labs/retry","version":"0.1.2","keywords":["retry","again","resilience","backoff","fault-tolerance","circuit-breaker","bulkhead","nestjs","node","backend"],"author":{"name":"BackendKit Labs","email":"hello@backendkitlabs.dev"},"license":"Apache-2.0","_id":"@backendkit-labs/retry@0.1.2","maintainers":[{"name":"backendkit.dev","email":"backendkit.dev@gmail.com"}],"homepage":"https://backendkitlabs.dev/docs/retry/","bugs":{"url":"https://github.com/BackendKit-labs/backendkit-monorepo/issues"},"dist":{"shasum":"bc311daff222fab7c1eee127498beea4ea286c89","tarball":"https://registry.npmjs.org/@backendkit-labs/retry/-/retry-0.1.2.tgz","fileCount":20,"integrity":"sha512-0ZbD6DEW+yKMmWbR9NUb2a2UhB3OZ/RU885+pHvSuGZmQr05SuGNOyrfuXAIzxlLsGl64hltt0BXg3796Zf1Ew==","signatures":[{"sig":"MEQCIDju6uzjv5AsacwFMjZxn4KWdVo1xgmIbw1fWYkv6vtrAiBRy+CIaMBnKo0cLinUS2e2iqcHwGgFThLuGf1DeASaMg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":258664},"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"},"./nestjs":{"types":"./dist/nestjs/index.d.ts","import":"./dist/nestjs/index.js","require":"./dist/nestjs/index.cjs"}},"gitHead":"8c37c800d4be422307496e95836ecb5ebb7b76df","scripts":{"dev":"tsup --watch","lint":"eslint src/","test":"vitest run","build":"tsup","format":"prettier --write src/","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build && npm run test && npm run lint"},"_npmUser":{"name":"backendkit.dev","email":"backendkit.dev@gmail.com"},"repository":{"url":"git+https://github.com/BackendKit-labs/backendkit-monorepo.git","type":"git","directory":"packages/retry"},"_npmVersion":"11.8.0","description":"Enterprise-grade retry library for Node.js -- exponential backoff, sliding window budget, error classification, duck-typed integrations, and optional NestJS support","directories":{},"sideEffects":false,"_nodeVersion":"22.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rxjs":"^7.8.0","tsup":"^8.0.0","eslint":"^9.0.0","vitest":"^2.0.0","prettier":"^3.0.0","@eslint/js":"^9.39.4","typescript":"^5.5.0","@types/node":"^22.0.0","@nestjs/core":"^10.0.0","@nestjs/common":"^10.0.0","reflect-metadata":"^0.2.0","typescript-eslint":"^8.59.3"},"peerDependencies":{"rxjs":">=7.0.0","@nestjs/core":">=10.0.0","@nestjs/common":">=10.0.0"},"peerDependenciesMeta":{"rxjs":{"optional":true},"@nestjs/core":{"optional":true},"@nestjs/common":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/retry_0.1.2_1779232214044_0.44068432445424843","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@backendkit-labs/retry","version":"0.2.0","license":"Apache-2.0","author":{"name":"BackendKit Labs","email":"hello@backendkitlabs.dev"},"description":"Enterprise-grade retry library for Node.js -- exponential backoff, sliding window budget, error classification, duck-typed integrations, and optional NestJS support","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"},"./nestjs":{"types":"./dist/nestjs/index.d.ts","import":"./dist/nestjs/index.js","require":"./dist/nestjs/index.cjs"}},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","typecheck":"tsc --noEmit","lint":"eslint src/","format":"prettier --write src/","prepublishOnly":"npm run build && npm run test && npm run lint"},"keywords":["retry","again","resilience","backoff","fault-tolerance","circuit-breaker","bulkhead","nestjs","node","backend"],"homepage":"https://backendkitlabs.dev/docs/retry/","repository":{"type":"git","url":"git+https://github.com/BackendKit-labs/backendkit-monorepo.git","directory":"packages/retry"},"bugs":{"url":"https://github.com/BackendKit-labs/backendkit-monorepo/issues"},"publishConfig":{"access":"public"},"sideEffects":false,"engines":{"node":">=18"},"peerDependencies":{"@nestjs/common":">=10.0.0","@nestjs/core":">=10.0.0","rxjs":">=7.0.0"},"peerDependenciesMeta":{"@nestjs/common":{"optional":true},"@nestjs/core":{"optional":true},"rxjs":{"optional":true}},"devDependencies":{"@eslint/js":"^9.39.4","@nestjs/common":"^10.0.0","@nestjs/core":"^10.0.0","@nestjs/testing":"^10.4.22","@types/node":"^22.0.0","eslint":"^9.0.0","prettier":"^3.0.0","reflect-metadata":"^0.2.0","rxjs":"^7.8.0","tsup":"^8.0.0","typescript":"^5.5.0","typescript-eslint":"^8.59.3","vitest":"^2.0.0"},"gitHead":"8f45501f784ed112b72b9fd2199a42f70cd251e5","_id":"@backendkit-labs/retry@0.2.0","_nodeVersion":"24.16.0","_npmVersion":"11.8.0","dist":{"integrity":"sha512-6g2qkrDhC43rkWopdotQ38LUzXx4guwwUA+KpwCdnw8Ol/vak1Tr9+m2inDGsRkpp9FJS0OfycA4mrwlpasHDA==","shasum":"9785f8ef77f46a8064cbe313b47710f207778a02","tarball":"https://registry.npmjs.org/@backendkit-labs/retry/-/retry-0.2.0.tgz","fileCount":20,"unpackedSize":297843,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCjWib6BdlfyC2T9RIwCcVPl7BKex9DjLG58XRLX+uI8wIhAKP42APm2GjTu6Ui/jd6q/cIY3tHCJ/zN2LIORWx5vlF"}]},"_npmUser":{"name":"backendkit.dev","email":"backendkit.dev@gmail.com"},"directories":{},"maintainers":[{"name":"backendkit.dev","email":"backendkit.dev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/retry_0.2.0_1789405087206_0.3383345117458205"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-19T22:32:22.250Z","modified":"2026-09-14T16:58:07.527Z","0.1.1":"2026-05-19T22:32:22.636Z","0.1.2":"2026-05-19T23:10:14.212Z","0.2.0":"2026-09-14T16:58:07.349Z"},"bugs":{"url":"https://github.com/BackendKit-labs/backendkit-monorepo/issues"},"author":{"name":"BackendKit Labs","email":"hello@backendkitlabs.dev"},"license":"Apache-2.0","homepage":"https://backendkitlabs.dev/docs/retry/","keywords":["retry","again","resilience","backoff","fault-tolerance","circuit-breaker","bulkhead","nestjs","node","backend"],"repository":{"type":"git","url":"git+https://github.com/BackendKit-labs/backendkit-monorepo.git","directory":"packages/retry"},"description":"Enterprise-grade retry library for Node.js -- exponential backoff, sliding window budget, error classification, duck-typed integrations, and optional NestJS support","maintainers":[{"name":"backendkit.dev","email":"backendkit.dev@gmail.com"}],"readme":"# @backendkit-labs/retry\n\nEnterprise-grade retry library for Node.js — exponential backoff, sliding-window budget, error classification, circuit-breaker and bulkhead integration, and optional NestJS support. Returns `Result<T, RetryError>`, never throws.\n\n[![npm](https://img.shields.io/npm/v/@backendkit-labs/retry)](https://www.npmjs.com/package/@backendkit-labs/retry)\n[![license](https://img.shields.io/npm/l/@backendkit-labs/retry)](LICENSE)\n[![node](https://img.shields.io/node/v/@backendkit-labs/retry)](package.json)\n\n---\n\n## Minimal Example\n\nSelf-contained runnable example — no NestJS, one file, realistic scenario.\n\n```bash\ngit clone https://github.com/BackendKit-labs/backendkit-monorepo.git\ncd backendkit-monorepo/examples/minimal-retry\nnpm install && npm start\n```\n\nShows a payment gateway that fails 60% of the time retried with exponential backoff + jitter. Lifecycle hooks log each attempt in real time. → [full source](https://github.com/BackendKit-labs/backendkit-monorepo/tree/master/examples/minimal-retry)\n\n---\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [RetryEngine — full control](#RetryEngine--full-control)\n- [Configuration Reference](#configuration-reference)\n  - [maxAttempts](#maxattempts)\n  - [backoff](#backoff)\n  - [retryIf / abortIf](#retryif--abortif)\n  - [timeout](#timeout)\n  - [budget](#budget)\n  - [hooks](#hooks)\n  - [fallback](#fallback)\n  - [dynamicDelay](#dynamicdelay)\n  - [classifiers](#classifiers)\n- [Backoff Strategies](#backoff-strategies)\n- [Error Types](#error-types)\n- [BackendKit Integrations](#backendkit-integrations)\n  - [circuit-breaker](#backendkit-labscircuit-breaker)\n  - [bulkhead](#backendkit-labsbulkhead)\n  - [result](#backendkit-labsresult)\n  - [observability](#backendkit-labsobservability)\n  - [All three layers together](#all-three-layers-together)\n- [NestJS Integration](#nestjs-integration)\n- [RetryRegistry](#RetryRegistry)\n- [API Reference](#api-reference)\n\n---\n\n## Installation\n\n```bash\nnpm install @backendkit-labs/retry @backendkit-labs/result\n```\n\nNestJS peer dependencies (optional — only if using `RetryModule`):\n\n```bash\nnpm install @nestjs/common @nestjs/core rxjs\n```\n\n---\n\n## Quick Start\n\n`retry()` is a standalone function backed by a global registry. It covers 90% of use cases in two lines:\n\n```typescript\nimport { retry } from '@backendkit-labs/retry';\nimport { match } from '@backendkit-labs/result';\n\nconst result = await retry(() => fetchUser(userId), {\n  maxAttempts: 3,\n  backoff: { type: 'exponential', baseDelay: 200 },\n});\n\nmatch(result, {\n  ok:   (user)  => res.json(user),\n  fail: (error) => res.status(502).json({ error: error.message }),\n});\n```\n\n`retry()` returns `Result<T, RetryError>` — a plain `{ ok, value } | { ok, error }` object — and **never throws**. Check `result.ok`, or use `match()` from `@backendkit-labs/result` to handle both paths.\n\n### Minimal retry with default config\n\n```typescript\n// 3 attempts, fixed 200ms delay\nconst result = await retry(() => callExternalApi());\n```\n\n### Handle specific error types\n\n```typescript\nconst result = await retry(() => chargePayment(order), {\n  maxAttempts: 4,\n  backoff: { type: 'exponential', baseDelay: 300, maxDelay: 5_000 },\n});\n\nif (!result.ok) {\n  const { type, status, metadata } = result.error;\n  if (type === 'http' && status === 429) {\n    // rate-limited — already retried, still failed\n  }\n  console.log(`Failed after ${metadata.attempts} attempt(s)`);\n}\n```\n\n---\n\n## RetryEngine — full control\n\n`RetryEngine` is the stateful core. Use it when you need shared configuration, per-engine metrics, or external integrations (circuit breaker, bulkhead, observability).\n\n```typescript\nimport { RetryEngine } from '@backendkit-labs/retry';\nimport { circuitBreaker } from '@backendkit-labs/circuit-breaker';\nimport { bulkhead } from '@backendkit-labs/bulkhead';\n\nconst engine = new RetryEngine({\n  name: 'payment-gateway',\n  defaultConfig: {\n    maxAttempts: 3,\n    backoff: { type: 'exponential', baseDelay: 200, maxDelay: 8_000, jitter: 'full' },\n    timeout: { attemptTimeoutMs: 5_000, globalTimeoutMs: 20_000 },\n  },\n  integrations: {\n    circuitBreaker: circuitBreaker({ failureThreshold: 5, cooldownMs: 10_000 }),\n    bulkhead: bulkhead({ maxConcurrent: 10, maxQueue: 20 }),\n    observability: {\n      logger: myLogger,\n      metrics: myMetricsEmitter,\n    },\n  },\n});\n\nconst result = await engine.execute(() => chargePayment(order));\n\n// Per-execution overrides\nconst result2 = await engine.execute(\n  () => refundPayment(order),\n  { maxAttempts: 5 },\n);\n\n// With correlationId for distributed tracing\nconst result3 = await engine.executeWithContext(\n  () => fetchInventory(productId),\n  { correlationId: req.headers['x-request-id'] },\n);\n```\n\n---\n\n## Configuration Reference\n\nAll options are in `RetryConfig`. Every field except `maxAttempts` and `backoff` is optional.\n\n```typescript\ninterface RetryConfig {\n  maxAttempts: number;\n  backoff: BackoffConfig | BackoffStrategy;\n  retryIf?: RetryCondition | RetryConditionFn;\n  abortIf?: AbortCondition | AbortConditionFn;\n  timeout?: Partial<TimeoutConfig>;\n  budget?: Partial<RetryBudgetConfig>;\n  idempotency?: Partial<IdempotencyConfig>;\n  classifiers?: ClassifierRule[];\n  dynamicDelay?: (error: RetryErrorPayload, attempt: number) => number;\n  hooks?: RetryHooks;\n  fallback?: (error: RetryErrorPayload) => unknown | Promise<unknown>;\n  correlationId?: string;\n}\n```\n\n### maxAttempts\n\nTotal number of attempts including the first one. `maxAttempts: 3` means one initial call plus two retries.\n\n```typescript\nawait retry(task, { maxAttempts: 5 });\n```\n\n### backoff\n\nControls the delay between retries. Accepts a config object (shorthand) or a `BackoffStrategy` instance (composable). See [Backoff Strategies](#backoff-strategies) for all options.\n\n```typescript\n// Shorthand (most common)\n{ backoff: { type: 'exponential', baseDelay: 200, maxDelay: 10_000, jitter: 'full' } }\n\n// Strategy instance (composable)\nimport { ExponentialBackoff, JitterDecorator } from '@backendkit-labs/retry';\nconst strategy = new JitterDecorator(new ExponentialBackoff({ baseDelay: 200 }), 'full');\n{ backoff: strategy }\n```\n\n### retryIf / abortIf\n\nFine-grained control over which errors trigger a retry and which abort immediately.\n\n```typescript\nawait retry(task, {\n  // Only retry on network errors and 5xx responses\n  retryIf: (error) => error.type === 'network' || (error.type === 'http' && (error.status ?? 0) >= 500),\n\n  // Abort immediately on 401/403 — retrying is pointless\n  abortIf: (error) => error.type === 'http' && [401, 403].includes(error.status ?? 0),\n});\n```\n\nBoth accept a plain function `(error: RetryErrorPayload) => boolean | Promise<boolean>` or an object implementing `RetryCondition` / `AbortCondition`.\n\n**Default behavior is driven by the [classifier](#classifiers):** an error classified `'transient'` retries, `'permanent'` aborts (5xx/network/timeout → transient; 4xx except 429 → permanent, by default). Supplying custom `classifiers` changes this default without needing to also set `retryIf`/`abortIf`. `retryIf`/`abortIf` are independent overrides on top of that -- `abortIf` is checked first, so if you only override `retryIf` to force a retry, also set `abortIf: () => false` or the classifier-driven default abort will still win.\n\n### timeout\n\nPer-attempt and global timeouts are independent:\n\n```typescript\nawait retry(task, {\n  timeout: {\n    attemptTimeoutMs: 5_000,  // each individual call is capped at 5s\n    globalTimeoutMs: 30_000,  // entire retry operation (including delays) capped at 30s\n  },\n});\n```\n\n- `attemptTimeoutMs` expiration → error classified as `type: 'timeout'` → triggers retry (if retryable)\n- `globalTimeoutMs` expiration → operation aborted immediately regardless of attempt count\n- `0` means unlimited (default)\n\nYour task receives an `AbortSignal` that fires on either timeout -- pass it through to anything that supports cancellation (`fetch`, database drivers, etc.) so a timed-out attempt actually stops instead of just being abandoned:\n\n```typescript\nawait retry(\n  (signal) => fetch('https://api.example.com/data', { signal }).then(r => r.json()),\n  { timeout: { attemptTimeoutMs: 5_000 } },\n);\n```\n\nA task that ignores the signal (most existing code) keeps running in the background after the timeout fires -- `retry()` stops waiting on it and moves to the next attempt, but the abandoned call isn't force-killed. The signal is optional to use; omitting the parameter is fine and behaves exactly as before.\n\n### budget\n\nSliding-window budget prevents retry storms. If the ratio of retries to total calls exceeds `maxRetryRatio` in the last `windowMs` milliseconds, further retries are rejected.\n\n```typescript\nawait retry(task, {\n  budget: {\n    windowMs: 60_000,       // 1-minute sliding window\n    maxRetryRatio: 0.1,     // max 10% of calls may be retries\n    minRequestCount: 20,    // budget not enforced until at least 20 calls\n  },\n});\n```\n\nBudget exhaustion produces `type: 'unknown'` with `metadata.budgetExhausted: true`. Share a budget across calls by reusing the same `RetryEngine` instance.\n\n### hooks\n\nLifecycle hooks for observability, logging, and debugging. Hook errors are swallowed and never affect retry state.\n\n```typescript\nawait retry(task, {\n  hooks: {\n    beforeRetry: ({ attempt, delayMs, error }) => {\n      logger.warn(`Retry #${attempt} in ${delayMs}ms — ${error.message}`);\n    },\n    afterRetry: ({ attempt, error }) => {\n      logger.debug(`Attempt ${attempt} finished with error: ${error.type}`);\n    },\n    onRetrySuccess: ({ attempt, totalAttempts, totalElapsedMs }) => {\n      logger.info(`Succeeded on attempt ${attempt}/${totalAttempts} (${totalElapsedMs}ms total)`);\n    },\n    onExhausted: ({ lastError, totalAttempts, totalElapsedMs }) => {\n      logger.error(`All ${totalAttempts} attempts failed in ${totalElapsedMs}ms`, lastError);\n    },\n    onBudgetExhausted: () => {\n      logger.warn('Retry budget exhausted — skipping retry');\n    },\n  },\n});\n```\n\n### fallback\n\nReturn a default value when all retries are exhausted instead of returning `err(...)`:\n\n```typescript\nconst result = await retry(() => fetchConfig(), {\n  maxAttempts: 3,\n  fallback: () => DEFAULT_CONFIG,\n});\n\n// result.ok === true, result.value === DEFAULT_CONFIG\n```\n\n### dynamicDelay\n\nOverride backoff with a delay computed from the error — useful for respecting `Retry-After` headers:\n\n```typescript\nawait retry(task, {\n  dynamicDelay: (error, attempt) => {\n    if (error.type === 'http' && error.status === 429) {\n      // Use Retry-After header value if available in cause\n      const retryAfterMs = (error.cause as any)?.retryAfterMs ?? 5_000;\n      return retryAfterMs;\n    }\n    return 0; // 0 = fall back to backoff strategy\n  },\n});\n```\n\n### classifiers\n\nAdd custom rules that classify errors as `'transient'` (retryable) or `'permanent'` (abort). This classification **is** the default retry/abort decision (see [retryIf / abortIf](#retryif--abortif)) -- adding a rule here changes what actually gets retried, not just how it's logged:\n\n```typescript\nawait retry(task, {\n  classifiers: [\n    {\n      name: 'business-validation',\n      priority: 1,                    // evaluated before built-in rules\n      match: (error) => error.type === 'http' && error.status === 422,\n      classification: 'permanent',\n    },\n    {\n      name: 'gateway-timeout',\n      priority: 50,\n      match: (error) => error.type === 'http' && error.status === 504,\n      classification: 'transient',\n    },\n  ],\n});\n```\n\nBuilt-in rules (lower priority number = evaluated first):\n\n| Status / Type | Classification |\n|---|---|\n| 400, 401, 403, 404, 413, 422 | permanent |\n| 429, 500–599 | transient |\n| `network`, `timeout` | transient |\n| `circuit-open`, `bulkhead-rejected` | transient |\n| everything else | permanent |\n\n---\n\n## Backoff Strategies\n\nThree built-in strategies, all composable with `JitterDecorator`:\n\n### Fixed\n\nSame delay every time:\n\n```typescript\nimport { FixedBackoff } from '@backendkit-labs/retry';\n\nnew FixedBackoff({ baseDelay: 500 });\n// attempt 1→2: 500ms, 2→3: 500ms, ...\n```\n\nShorthand: `{ type: 'fixed', baseDelay: 500 }`\n\n### Linear\n\nDelay grows linearly:\n\n```typescript\nimport { LinearBackoff } from '@backendkit-labs/retry';\n\nnew LinearBackoff({ baseDelay: 200, multiplier: 1.5, maxDelay: 5_000 });\n// attempt 1→2: 200ms, 2→3: 300ms, 3→4: 450ms, ...\n```\n\nShorthand: `{ type: 'linear', baseDelay: 200, multiplier: 1.5, maxDelay: 5_000 }`\n\n### Exponential\n\nDelay doubles (or scales by `multiplier`) each attempt:\n\n```typescript\nimport { ExponentialBackoff } from '@backendkit-labs/retry';\n\nnew ExponentialBackoff({ baseDelay: 200, multiplier: 2, maxDelay: 30_000, jitter: 'full' });\n// attempt 1→2: ~200ms, 2→3: ~400ms, 3→4: ~800ms, ...\n```\n\nShorthand: `{ type: 'exponential', baseDelay: 200, maxDelay: 30_000, jitter: 'full' }`\n\nJitter types: `'full'` (uniform random in [0, delay]), `'equal'` (delay/2 + random in [0, delay/2]), `'decorrelated'` (delay based on previous delay × random).\n\n### JitterDecorator\n\nWrap any strategy with jitter:\n\n```typescript\nimport { LinearBackoff, JitterDecorator } from '@backendkit-labs/retry';\n\nconst strategy = new JitterDecorator(\n  new LinearBackoff({ baseDelay: 300 }),\n  'full',\n);\n```\n\n---\n\n## Error Types\n\n`RetryError` is the union of `RetryErrorPayload` and `RetryMetadata`:\n\n```typescript\ntype RetryError = RetryErrorPayload & { metadata: RetryMetadata };\n\ninterface RetryErrorPayload {\n  type: ErrorType;     // 'http' | 'network' | 'timeout' | 'circuit-open' | 'bulkhead-rejected' | 'business' | 'unknown'\n  message: string;\n  status?: number;     // HTTP status code (only when type === 'http')\n  cause?: unknown;     // original thrown error\n  attempt: number;     // attempt number when this error occurred\n  elapsedMs: number;   // elapsed ms at this point\n}\n\ninterface RetryMetadata {\n  attempts: number;          // total attempts made\n  totalElapsedMs: number;    // total operation duration\n  lastError?: RetryErrorPayload;\n  budgetExhausted?: boolean; // true if stopped by budget\n  circuitOpen?: boolean;     // true if stopped by circuit breaker\n}\n```\n\nHandling different error types:\n\n```typescript\nconst result = await retry(() => callApi());\n\nif (!result.ok) {\n  const error = result.error;\n\n  switch (error.type) {\n    case 'http':\n      console.log(`HTTP ${error.status} after ${error.metadata.attempts} attempts`);\n      break;\n    case 'timeout':\n      console.log(`Timed out after ${error.metadata.totalElapsedMs}ms`);\n      break;\n    case 'circuit-open':\n      console.log('Circuit breaker is OPEN — fast-failed without retrying');\n      break;\n    case 'network':\n      console.log('Network error:', error.cause);\n      break;\n  }\n}\n```\n\n---\n\n## BackendKit Integrations\n\nAll integrations are **duck-typed** — `Retry` never imports any other BackendKit library at compile time. You connect them by passing the instance directly to `RetryEngine`. Any object that satisfies the minimal interface works, including mocks in tests.\n\n---\n\n### `@backendkit-labs/circuit-breaker`\n\nThe circuit breaker controls whether to attempt a call. `Retry` checks it before each attempt:\n\n```typescript\nimport { CircuitBreaker } from '@backendkit-labs/circuit-breaker';\nimport { RetryEngine } from '@backendkit-labs/retry';\n\nconst cb = new CircuitBreaker({ name: 'payments', failureThreshold: 50, minimumCalls: 5 });\n\nconst engine = new RetryEngine({\n  name: 'payments',\n  integrations: {\n    circuitBreaker: cb,  // duck-typed: canAttempt() / onSuccess() / onError()\n  },\n});\n\nconst result = await engine.execute(() => chargePayment(order));\n\nif (!result.ok && result.error.type === 'circuit-open') {\n  // Breaker was OPEN — Retry returned immediately without calling the task\n}\n```\n\n**Execution flow:**\n1. `Retry` calls `cb.canAttempt()` before every attempt. If `false` → returns `{ type: 'circuit-open' }` immediately.\n2. On success → `cb.onSuccess(durationMs)` — registers the healthy call.\n3. On transient failure → `cb.onError(err)` — updates the breaker's failure counter.\n4. On permanent/business failure (e.g. 422) → CB is **not** notified — this is not an infrastructure problem.\n\n**The real value:** the circuit breaker stops calls when the service is known to be down. `Retry` acts as the gradual recovery mechanism — it waits with backoff and retries when the breaker transitions to half-open.\n\n---\n\n### `@backendkit-labs/bulkhead`\n\nLimits the concurrency of attempts. Every attempt — including retries — passes through the bulkhead:\n\n```typescript\nimport { Bulkhead } from '@backendkit-labs/bulkhead';\nimport { RetryEngine } from '@backendkit-labs/retry';\n\nconst bulkhead = new Bulkhead({ maxConcurrent: 10, maxQueue: 20 });\n\nconst engine = new RetryEngine({\n  name: 'orders',\n  integrations: {\n    bulkhead,  // duck-typed: execute(fn) / isFull()\n  },\n});\n```\n\n**Execution flow:**\n- Each attempt (including retries) is wrapped in `bulkhead.execute(fn)`.\n- If the bulkhead is full → it rejects → `Retry` classifies as `type: 'bulkhead-rejected'` (transient by default) → waits with backoff and re-queues.\n\n---\n\n### `@backendkit-labs/result`\n\n`retry()` already returns `Result<T, RetryError>` — direct integration, no adapter needed:\n\n```typescript\nimport { retry } from '@backendkit-labs/retry';\nimport { match } from '@backendkit-labs/result';\n\nconst result = await retry(() => fetchOrder(id), {\n  maxAttempts: 3,\n  backoff: { type: 'exponential', baseDelay: 200 },\n});\n\nmatch(result, {\n  ok:   (order) => res.json(order),\n  fail: (err)   => res.status(502).json({ message: err.message, attempts: err.metadata.attempts }),\n});\n```\n\nWhen combining with `@backendkit-labs/http-client` (which also returns `Result`), unwrap between layers so `Retry` sees a thrown error instead of a nested `Result`:\n\n```typescript\nconst result = await retry(\n  async () => {\n    const r = await httpClient.get<Order>('/orders/1');\n    if (!r.ok) throw Object.assign(new Error(r.error.message), { status: r.error.status });\n    return r.value;\n  },\n  { maxAttempts: 3, backoff: { type: 'exponential', baseDelay: 100 } },\n);\n```\n\nThe thrown error with a `.status` property is detected as `type: 'http'` and classified correctly by the built-in rules.\n\n---\n\n### `@backendkit-labs/observability`\n\nPlug in any logger and metrics emitter that satisfy the minimal duck-typed interfaces:\n\n```typescript\nimport { Logger } from '@backendkit-labs/observability';\n\nconst logger = new Logger({ service: 'payments' });\n\nconst engine = new RetryEngine({\n  name: 'payments',\n  integrations: {\n    observability: {\n      logger,                         // info / warn / error\n      metrics: metricsRegistry,       // emit(event)\n    },\n  },\n});\n```\n\n**Logs emitted automatically:**\n\n```\nWARN  \"Retry: attempt failed\"  { attempt: 2, type: 'http', classification: 'transient' }\nERROR \"Retry: exhausted\"       { attempts: 3, type: 'http', totalElapsedMs: 1842 }\n```\n\n**Metrics emitted automatically:**\n\n| Metric | When | Tags |\n|---|---|---|\n| `Retry.attempt_failed` | After each failed attempt | `attempt`, `type`, `classification` |\n| `Retry.success` | On eventual success | `attempt` (attempt number that succeeded) |\n| `Retry.exhausted` | All retries exhausted | `type` |\n| `Retry.budget_exhausted` | Budget refused a retry | — |\n\n---\n\n### All three layers together\n\nThe most complete pattern for a production service — circuit breaker, bulkhead, budget, timeout, and observability in a single engine:\n\n```typescript\nimport { CircuitBreaker } from '@backendkit-labs/circuit-breaker';\nimport { Bulkhead }        from '@backendkit-labs/bulkhead';\nimport { Logger }          from '@backendkit-labs/observability';\nimport { RetryEngine }     from '@backendkit-labs/retry';\n\nconst cb       = new CircuitBreaker({ name: 'payments', failureThreshold: 50, minimumCalls: 5 });\nconst bulkhead = new Bulkhead({ maxConcurrent: 10, maxQueue: 20 });\nconst logger   = new Logger({ service: 'payments-client' });\n\nconst engine = new RetryEngine({\n  name: 'payments-client',\n  defaultConfig: {\n    maxAttempts: 4,\n    backoff:  { type: 'exponential', baseDelay: 200, maxDelay: 5_000, jitter: 'full' },\n    budget:   { windowMs: 60_000, maxRetryRatio: 0.15 },  // max 15% retries per minute\n    timeout:  { attemptTimeoutMs: 3_000, globalTimeoutMs: 12_000 },\n  },\n  integrations: {\n    circuitBreaker: cb,\n    bulkhead,\n    observability: { logger, metrics: metricsRegistry },\n  },\n});\n\nconst result = await engine.execute(() => chargePayment(order));\n```\n\n**What happens per attempt:**\n\n```\nbudget.recordCall()\n→ checkGlobalTimeout()              ← abort if 12s total exceeded\n→ cb.canAttempt()                   ← fast-fail if circuit is OPEN\n→ bulkhead.execute(...)             ← limit concurrency\n→ executeWithAttemptTimeout(3_000)  ← cap each call at 3s\n→ success:  cb.onSuccess() / budget.recordSuccess()\n→ failure:  cb.onError()  / budget.recordFailure()\n            → abort? retry? budget exhausted? → backoff and loop\n```\n\n---\n\n## NestJS Integration\n\nImport `RetryModule` once at the application root. It registers `RetryService` and, only if `globalInterceptor: true` is set (default: `false`), a global `RetryInterceptor`.\n\n### Module setup\n\n```typescript\nimport { RetryModule } from '@backendkit-labs/retry/nestjs';\n\n@Module({\n  imports: [\n    RetryModule.forRoot({\n      engineConfig: {\n        name: 'default',\n        defaultConfig: {\n          maxAttempts: 3,\n          backoff: { type: 'exponential', baseDelay: 200, jitter: 'full' },\n        },\n      },\n      globalInterceptor: false, // set true to apply @Retry to all controllers globally\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n### RetryService — inject and execute\n\n```typescript\nimport { Injectable } from '@nestjs/common';\nimport { RetryService } from '@backendkit-labs/retry/nestjs';\n\n@Injectable()\nexport class PaymentsService {\n  constructor(private readonly Retry: RetryService) {}\n\n  async charge(order: Order) {\n    const result = await this.Retry.execute(\n      () => this.gateway.charge(order),\n      { maxAttempts: 4, backoff: { type: 'exponential', baseDelay: 300 } },\n    );\n\n    if (!result.ok) throw new ServiceUnavailableException(result.error.message);\n    return result.value;\n  }\n}\n```\n\n### @Retry decorator\n\nWraps the method directly -- works on any `@Injectable()` (or plain) class, with or without NestJS DI, no `RetryModule` import required:\n\n```typescript\nimport { Retry } from '@backendkit-labs/retry/nestjs';\n\n@Injectable()\nexport class InventoryService {\n  @Retry({\n    maxAttempts: 3,\n    backoff: { type: 'exponential', baseDelay: 150 },\n  })\n  async reserveStock(productId: string, quantity: number) {\n    return this.http.post('/inventory/reserve', { productId, quantity });\n  }\n}\n```\n\nEach `@Retry`-decorated method gets its own named `RetryEngine`, so `budget`/`idempotency` config actually accumulates across calls. By default that engine lives in an internal registry private to `@Retry`; inject `public readonly retryRegistry: RetryRegistry` on the class to use your own instead (so you can read its metrics, or share it with other code):\n\n```typescript\nimport { RetryRegistry } from '@backendkit-labs/retry';\n\n@Injectable()\nexport class InventoryService {\n  constructor(public readonly retryRegistry: RetryRegistry) {}\n\n  @Retry({ maxAttempts: 3 })\n  async reserveStock(productId: string, quantity: number) { ... }\n}\n```\n\n`@Retry` also sets metadata for `RetryInterceptor` to read, but you don't need the interceptor for this to work -- the decorator retries the method itself. Only turn on `RetryModule`'s `globalInterceptor` if you specifically want pipeline-level retry (re-running `next.handle()`, including other interceptors) instead of method-level retry; enabling both for the same method retries it twice.\n\n### DI tokens\n\nInject the underlying engine or registry directly:\n\n```typescript\nimport { Inject } from '@nestjs/common';\nimport { RETRY_ENGINE_TOKEN, RETRY_REGISTRY_TOKEN } from '@backendkit-labs/retry/nestjs';\nimport type { RetryEngine, RetryRegistry } from '@backendkit-labs/retry';\n\n@Injectable()\nexport class MyService {\n  constructor(\n    @Inject(RETRY_ENGINE_TOKEN) private engine: RetryEngine,\n    @Inject(RETRY_REGISTRY_TOKEN) private registry: RetryRegistry,\n  ) {}\n}\n```\n\n---\n\n## RetryRegistry\n\n`RetryRegistry` manages named `RetryEngine` instances — useful when different services need different retry configurations in the same process.\n\n```typescript\nimport { RetryRegistry } from '@backendkit-labs/retry';\n\nconst registry = new RetryRegistry();\n\nconst paymentEngine = registry.getOrCreate('payments', {\n  defaultConfig: { maxAttempts: 3, backoff: { type: 'exponential', baseDelay: 300 } },\n});\n\nconst emailEngine = registry.getOrCreate('email', {\n  defaultConfig: { maxAttempts: 5, backoff: { type: 'fixed', baseDelay: 1_000 } },\n});\n\n// Retrieve later by name\nconst engine = registry.get('payments');\n\n// Metrics snapshot for all engines\nconst allMetrics = registry.getAllMetrics();\n\n// Reset a specific engine's state\nregistry.reset('payments');\n```\n\n---\n\n## API Reference\n\n### `retry(task, options?)`\n\nStandalone function using a global default registry.\n\n| Param | Type | Description |\n|---|---|---|\n| `task` | `() => Promise<T>` | The async operation to retry |\n| `options` | `Partial<RetryConfig>` | Optional config overrides |\n| Returns | `Promise<Result<T, RetryError>>` | Never throws |\n\n### `RetryEngine`\n\n| Method | Signature | Description |\n|---|---|---|\n| `execute` | `<T>(task, options?) => Promise<Result<T, RetryError>>` | Execute with retry |\n| `executeWithContext` | `<T>(task, { correlationId? }, options?) => Promise<Result<T, RetryError>>` | Execute with correlationId |\n| `updateDefaults` | `(partial: Partial<RetryConfig>) => void` | Update engine defaults at runtime |\n| `getMetrics` | `() => RetryMetricsSnapshot` | Get current metrics |\n| `resetMetrics` | `() => void` | Reset metrics counters |\n\n### `RetryRegistry`\n\n| Method | Signature | Description |\n|---|---|---|\n| `getOrCreate` | `(name, config?) => RetryEngine` | Get or create a named engine |\n| `get` | `(name) => RetryEngine \\| undefined` | Get engine by name |\n| `reset` | `(name) => void` | Remove a named engine |\n| `resetAll` | `() => void` | Remove all engines |\n| `getAllMetrics` | `() => Record<string, RetryMetricsSnapshot>` | Metrics for all engines |\n\n### NestJS exports (`@backendkit-labs/retry/nestjs`)\n\n| Export | Type | Description |\n|---|---|---|\n| `RetryModule` | `DynamicModule` | `RetryModule.forRoot(options?)` |\n| `RetryService` | `Injectable` | `.execute(task, options?)` |\n| `Retry` | `MethodDecorator` | `@Retry(config)` |\n| `RetryInterceptor` | `NestInterceptor` | Intercepts methods decorated with `@Retry` |\n| `RETRY_ENGINE_TOKEN` | `string` | DI token for `RetryEngine` |\n| `RETRY_REGISTRY_TOKEN` | `string` | DI token for `RetryRegistry` |\n\n---\n\n## License\n\nApache-2.0 — see [LICENSE](LICENSE) for details.\n\nPart of the [BackendKit Labs](https://backendkitlabs.dev) ecosystem.\n","readmeFilename":"README.md"}