{"id":50834967,"url":"https://github.com/aashahin/bunbreaker","last_synced_at":"2026-06-14T02:32:40.244Z","repository":{"id":359898436,"uuid":"1228719783","full_name":"aashahin/bunbreaker","owner":"aashahin","description":"Bun-native circuit breaker with built-in retry, abort-aware fetch, error mapping, and diagnostics. Zero dependencies — Redis + SQLite + Memory tiered storage, Bun.cron health probes, ElysiaJS \u0026 Hono adapters.","archived":false,"fork":false,"pushed_at":"2026-05-05T10:37:58.000Z","size":68,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-24T03:07:57.441Z","etag":null,"topics":["breaker","bun","bunjs","circuit-breaker","elysia","elysiajs","hono","honojs","redis","sqlite"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/bunbreaker","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aashahin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-04T09:57:11.000Z","updated_at":"2026-05-05T09:52:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aashahin/bunbreaker","commit_stats":null,"previous_names":["aashahin/bunbreaker"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/aashahin/bunbreaker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aashahin%2Fbunbreaker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aashahin%2Fbunbreaker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aashahin%2Fbunbreaker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aashahin%2Fbunbreaker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aashahin","download_url":"https://codeload.github.com/aashahin/bunbreaker/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aashahin%2Fbunbreaker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34307683,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["breaker","bun","bunjs","circuit-breaker","elysia","elysiajs","hono","honojs","redis","sqlite"],"created_at":"2026-06-14T02:32:40.174Z","updated_at":"2026-06-14T02:32:40.236Z","avatar_url":"https://github.com/aashahin.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# bunbreaker\n\n\u003e Bun-native circuit breaker with built-in retry, abort-aware fetch, error mapping, and diagnostics. Zero dependencies — Redis + SQLite + Memory tiered storage, Bun.cron health probes, ElysiaJS \u0026 Hono adapters.\n\n**Minimum runtime: Bun \u003e= 1.3.12**\n\n## Features\n\n- **Circuit Breaker** — CLOSED → OPEN → HALF_OPEN state machine with configurable thresholds\n- **Capacity Limiter** — Per-breaker concurrent execution semaphore with dedicated `CapacityExceededError`\n- **Built-in Retry** — Exponential backoff with jitter, per-error retryability, total time budgets\n- **Abort-Aware Fetch** — `fetchWithBreaker()` cancels TCP connections on timeout via `AbortController`\n- **Dual Threshold Modes** — Absolute failure count or percentage-based (like Opossum's `errorThresholdPercentage`)\n- **Error Classification** — Built-in classifier for HTTP status, network errors, timeouts. Fully overridable\n- **Error Mapping** — Transform `CircuitOpenError` → your domain errors before they leave the breaker\n- **Three-Tier Storage** — Redis (primary) → SQLite (fallback + audit) → Memory (last resort)\n- **Auto-Failover** — Redis meta-breaker detects failures, switches to SQLite, and startup failures retry in the background\n- **Sliding Window** — True sliding window via Redis sorted sets + atomic Lua scripts\n- **Health Probes** — `Bun.cron` in-process scheduler probes OPEN circuits automatically\n- **Fallback Queue** — SQLite-backed bounded outbox replays events when services recover\n- **Diagnostics** — Per-breaker stats + aggregate snapshot for health endpoints\n- **Alert Adapters** — Resend, Telegram, Webhook (pure functions, zero coupling)\n- **Framework Adapters** — ElysiaJS and Hono (optional, thin wrappers)\n- **Disposable** — `await using cb = await createBreaker(...)` with `Symbol.asyncDispose`\n- **Zero npm dependencies** — Built entirely on Bun primitives\n\n## Quick Start\n\n```ts\nimport { createBreaker, telegramAlert } from \"bunbreaker\";\n\nconst cb = await createBreaker({\n  redisUrl: process.env.REDIS_URL,\n  sqlite: { path: \"./bunbreaker.db\" },\n});\n\n// Create a named circuit breaker\nconst paymentBreaker = cb.for(\"payment-api\", {\n  failureThreshold: 5,\n  windowSecs: 60,\n  resetTimeoutSecs: 30,\n  timeoutMs: 8000,\n  fallback: async () =\u003e ({ status: \"queued\" }),\n});\n\n// Execute a protected call\nconst result = await paymentBreaker.execute(\n  () =\u003e fetch(\"https://payments.example.com/charge\", {\n    method: \"POST\",\n    body: JSON.stringify(payload),\n  }),\n  payload // optional — enqueued when circuit is OPEN\n);\n\n// Subscribe to events\ncb.events\n  .on(\"opened\", telegramAlert(process.env.TG_TOKEN!, process.env.TG_CHAT!))\n  .on(\"closed\", (e) =\u003e console.log(`${e.name} recovered`))\n  .on(\"*\", (e) =\u003e metrics.increment(`breaker.${e.type}`));\n\n// Health status\nconst health = cb.health();\n// → { currentLayer: \"redis\", redis: { open: false, failures: 0, recoversAt: null } }\n\n// Diagnostics\nconst snap = await cb.diagnostics();\n// → { summary: { openBreakers: 0, totalRequests: 42, ... }, breakers: [...] }\n\n// Graceful shutdown\nawait cb.shutdown();\n```\n\n## Retry\n\nBuilt-in retry with exponential backoff, jitter, and a hard total time budget. Each retry attempt gets its own timeout, capped by any remaining `maxRetryTimeMs` budget.\n\n`execute()` uses a promise race for timeouts, so it cannot cancel the underlying work. The caller is released at `timeoutMs`, but the original promise may still run in the background. For production mutations, prefer `executeWithAbort()` or `fetchWithBreaker()`. To avoid duplicate side effects, `execute()` does not retry `BreakerTimeoutError` by default; provide `retry.shouldRetry` if you explicitly want that behavior for idempotent work.\n\n```ts\nconst breaker = cb.for(\"flaky-api\", {\n  failureThreshold: 5,\n  windowSecs: 60,\n  resetTimeoutSecs: 30,\n  timeoutMs: 3000,\n  retry: {\n    retries: 3,\n    factor: 2,             // exponential backoff factor (default: 2)\n    minTimeoutMs: 250,     // minimum delay between retries\n    maxTimeoutMs: 5000,    // maximum delay between retries\n    maxRetryTimeMs: 10000, // hard total wall-clock budget for all retries\n    shouldRetry: (err) =\u003e {\n      // Override per-error retryability (default: uses error classifier)\n      return !(err instanceof PaymentError);\n    },\n    onRetry: (err, attempt, retriesLeft) =\u003e {\n      logger.warn(`Retry ${attempt}, ${retriesLeft} left`, err);\n    },\n  },\n});\n\n// Only the FINAL error (after all retries) counts toward the breaker\nconst result = await breaker.execute(() =\u003e callFlakyService());\n```\n\n## Abort-Aware Fetch\n\n`fetchWithBreaker()` creates a per-attempt `AbortController` that actually cancels the TCP connection on timeout — unlike `execute(() =\u003e fetch(...))` which just races the promise. Caller-provided abort signals are treated as caller cancellation, so they do not count as upstream failures or trigger retries.\n\n```ts\nimport { fetchWithBreaker } from \"bunbreaker\";\n\nconst breaker = cb.for(\"external-api\", {\n  failureThreshold: 5,\n  windowSecs: 60,\n  resetTimeoutSecs: 30,\n  timeoutMs: 5000,\n});\n\n// Basic usage — abort on timeout, classify 5xx responses automatically\nconst response = await fetchWithBreaker(breaker, \"https://api.example.com/data\");\n\n// With per-fetch retry (independent from breaker's retry config)\nconst response = await fetchWithBreaker(\n  breaker,\n  \"https://api.example.com/data\",\n  { method: \"POST\", body: JSON.stringify(data) },\n  {\n    timeoutMs: 3000,  // override breaker's timeout for this call\n    retry: { retries: 2, minTimeoutMs: 100 },\n  }\n);\n```\n\nYou can also use `executeWithAbort()` directly for non-fetch workloads that support cancellation:\n\n```ts\nconst result = await breaker.executeWithAbort(async (signal) =\u003e {\n  const response = await fetch(\"https://api.example.com/stream\", { signal });\n  return response.json();\n});\n```\n\n## Percentage-Based Thresholding\n\nInstead of a fixed failure count, trip the circuit when the error rate exceeds a percentage. Requires a minimum request volume to prevent false positives on low traffic.\n\n```ts\nconst breaker = cb.for(\"high-traffic-api\", {\n  percentageThreshold: 50, // trip at 50% error rate\n  volumeThreshold: 20,     // need at least 20 requests before evaluating\n  windowSecs: 60,\n  resetTimeoutSecs: 30,\n  timeoutMs: 5000,\n});\n```\n\n\u003e **Note**: Use `failureThreshold` OR `percentageThreshold`, not both.\n\n## Capacity Limiter\n\nLimit the number of concurrent in-flight executions per breaker. When the limit is reached, new requests are rejected immediately (via fallback or `CapacityExceededError`) — even if the circuit is CLOSED.\n\nThis prevents overwhelming a slow or degraded upstream service with unbounded concurrency.\n\n```ts\nconst breaker = cb.for(\"payment-api\", {\n  failureThreshold: 5,\n  windowSecs: 60,\n  resetTimeoutSecs: 30,\n  timeoutMs: 8000,\n  capacity: 40, // max 40 concurrent requests\n});\n\n// If 40 requests are already in-flight, this rejects immediately\nconst result = await breaker.execute(() =\u003e paymentService.charge(body));\n```\n\nCapacity rejections emit `capacity_rejected` and do not enqueue payloads, because the upstream circuit is not OPEN. Timed-out calls release capacity at the breaker timeout, even if non-abortable work continues in the background.\n\n## Enabled Kill-Switch\n\nDisable a circuit breaker at runtime without removing it. When `enabled` is `false`, all calls pass straight through to the wrapped function with no circuit breaker logic — no state checks, no failure counting, no timeout racing. Stats are still tracked for observability.\n\n```ts\nconst breaker = cb.for(\"payment-api\", {\n  failureThreshold: 5,\n  windowSecs: 60,\n  resetTimeoutSecs: 30,\n  timeoutMs: 8000,\n  enabled: process.env.PAYMENT_BREAKER_ENABLED !== \"false\", // runtime kill-switch\n});\n\nbreaker.setEnabled(false); // runtime toggle for an existing breaker\nbreaker.setEnabled(true);\n```\n\n## Error Classification\n\nThe built-in classifier decides which errors count toward the threshold and which are retryable:\n\n| Error Type | Counts? | Retries? | Trips? |\n|-----------|---------|----------|--------|\n| 5xx Server | ✅ | ✅ | ✅ |\n| 429 Rate Limited | ✅ | ✅ | ✅ |\n| 4xx Client | ❌ | ❌ | — |\n| Network failure | ✅ | ✅ | ✅ |\n| Timeout | ✅ | ✅ | ✅ |\n| Validation/Business | ❌ | ❌ | — |\n\n### Custom Error Classifier\n\n```ts\nconst breaker = cb.for(\"service\", {\n  failureThreshold: 5,\n  windowSecs: 60,\n  resetTimeoutSecs: 30,\n  timeoutMs: 5000,\n  errorClassifier: (err) =\u003e ({\n    shouldCount: true,        // count toward failure threshold\n    shouldRetry: true,        // eligible for retry\n    shouldTrip: false,        // count for health metrics, but don't trip the circuit\n  }),\n});\n```\n\nThe `shouldTrip` field lets you separate \"count for health metrics\" from \"trigger OPEN\". For example, you might want to track 429s in failure stats but not trip the circuit for rate limiting.\n\n## Error Mapping\n\nMap breaker errors to your application's domain errors before they leave the library:\n\n```ts\nimport { CircuitOpenError, BreakerTimeoutError } from \"bunbreaker\";\n\nconst breaker = cb.for(\"payment-api\", {\n  failureThreshold: 5,\n  windowSecs: 60,\n  resetTimeoutSecs: 30,\n  timeoutMs: 5000,\n  errorMapper: (err, ctx) =\u003e {\n    if (err instanceof CircuitOpenError) {\n      return new ThirdPartyCircuitOpenError(ctx.name);\n    }\n    if (err instanceof BreakerTimeoutError) {\n      return new ThirdPartyTimeoutError(ctx.name);\n    }\n    return err instanceof Error ? err : new Error(String(err));\n  },\n});\n```\n\nThe `ctx` parameter includes `{ name, state, config }` for context-aware mapping.\n\n## Diagnostics\n\nGet runtime stats for all registered breakers:\n\n```ts\nconst snapshot = await cb.diagnostics();\n// {\n//   generatedAt: \"2024-01-15T10:30:00.000Z\",\n//   storeHealth: { currentLayer: \"redis\", ... },\n//   summary: {\n//     registeredBreakers: 3,\n//     openBreakers: 1,\n//     halfOpenBreakers: 0,\n//     closedBreakers: 2,\n//     totalRequests: 1542,\n//     totalFailures: 23,\n//     totalTimeouts: 5,\n//     totalRejects: 12,\n//   },\n//   breakers: [\n//     {\n//       name: \"payment-api\",\n//       state: \"CLOSED\",\n//       config: { ... },\n//       stats: {\n//         createdAt: 1705312200000,\n//         useCount: 500,\n//         successCount: 487,\n//         failureCount: 13,\n//         rejectCount: 0,\n//         timeoutCount: 3,\n//         retryCount: 8,\n//         lastUsedAt: 1705312500000,\n//         lastOpenedAt: 1705312100000,\n//         lastClosedAt: 1705312150000,\n//       },\n//     },\n//     ...\n//   ],\n// }\n```\n\nPer-breaker stats are also available directly:\n\n```ts\nconst stats = breaker.getStats();\n```\n\n## Production Behavior\n\n### Redis startup recovery\n\nIf Redis initialization fails during `createBreaker()`, bunbreaker starts with SQLite/Memory and retries Redis initialization every `redisReconnectIntervalMs` milliseconds. Once Redis connects, it becomes the active distributed store again. Set `redisReconnectIntervalMs: 0` to disable this startup retry loop.\n\nUse `redisKeyPrefix` when multiple applications, environments, or tenants share one Redis deployment:\n\n```ts\nconst cb = await createBreaker({\n  redisUrl: process.env.REDIS_URL,\n  redisKeyPrefix: \"prod:checkout\",\n});\n```\n\n### SQLite fallback\n\nIf the configured SQLite path cannot be opened, bunbreaker throws during `createBreaker()` by default so production deployments do not silently lose queue and audit durability. For tests or emergency degraded mode, opt in explicitly:\n\n```ts\nconst cb = await createBreaker({\n  sqlite: {\n    path: \"/var/lib/app/bunbreaker.db\",\n    allowInMemoryFallback: true,\n  },\n});\n```\n\n### Health probe gating\n\nRegistering a probe gates recovery by default. An OPEN circuit with a registered probe stays OPEN after `resetTimeoutSecs` until the probe succeeds, then moves to HALF_OPEN for the next real trial request.\n\n```ts\ncb.probe(\"payment-api\", {\n  url: \"https://payments.example.com/health\",\n  timeoutMs: 1000,\n  gateHalfOpen: true, // default\n});\n```\n\nSet `gateHalfOpen: false` if you want probes for observability only and prefer timer-based OPEN -\u003e HALF_OPEN recovery.\n\n### Queue bounds\n\nFallback queue writes are bounded by pending count, serialized payload size, and pending TTL. Queue write failures emit `queue_error` but do not block the fallback response.\n\nReplay handlers have a per-event timeout (`handlerTimeoutMs`, default 30s) so one stuck handler cannot hold the replay lock forever. Set it to `0` only when your handler already has its own hard timeout.\n\n## Framework Adapters\n\nFramework adapters inject the breaker helper only by default. Health and diagnostics routes are opt-in because diagnostics can expose breaker names and configuration. Enable them only behind internal/admin routing.\n\n### ElysiaJS\n\n```ts\nimport { Elysia } from \"elysia\";\nimport { createBreaker } from \"bunbreaker\";\nimport { elysiaBreaker } from \"bunbreaker/elysia\";\n\nconst cb = await createBreaker({ redisUrl: process.env.REDIS_URL });\n\nconst app = new Elysia()\n  .use(elysiaBreaker(cb, {\n    healthRoutes: {\n      healthPath: \"/internal/health/circuits\",\n      diagnosticsPath: \"/internal/health/circuits/diagnostics\",\n    },\n  }))\n  .post(\"/checkout\", async ({ breaker, body }) =\u003e {\n    return await breaker\n      .for(\"payment-api\", {\n        failureThreshold: 5,\n        windowSecs: 60,\n        resetTimeoutSecs: 30,\n        timeoutMs: 8000,\n      })\n      .execute(() =\u003e paymentService.charge(body));\n  });\n```\n\n### Hono\n\n```ts\nimport { Hono } from \"hono\";\nimport { createBreaker } from \"bunbreaker\";\nimport { honoBreaker } from \"bunbreaker/hono\";\n\nconst cb = await createBreaker({ redisUrl: process.env.REDIS_URL });\nconst app = new Hono();\n\napp.use(\"*\", honoBreaker(cb, {\n  healthRoutes: {\n    healthPath: \"/internal/health/circuits\",\n    diagnosticsPath: \"/internal/health/circuits/diagnostics\",\n  },\n}));\n\napp.get(\"/resource\", async (c) =\u003e {\n  const result = await c.var.breaker\n    .for(\"upstream\", {\n      failureThreshold: 5,\n      windowSecs: 60,\n      resetTimeoutSecs: 30,\n      timeoutMs: 5000,\n    })\n    .execute(() =\u003e fetchUpstream());\n  return c.json(result);\n});\n```\n\n### Bun.serve (Standalone)\n\n```ts\nimport { createBreaker } from \"bunbreaker\";\n\nawait using cb = await createBreaker({\n  sqlite: { path: \"./bunbreaker.db\" },\n});\n\nconst apiBreaker = cb.for(\"external-api\", {\n  failureThreshold: 3,\n  windowSecs: 30,\n  resetTimeoutSecs: 15,\n  timeoutMs: 5000,\n});\n\nBun.serve({\n  async fetch(req) {\n    const url = new URL(req.url);\n\n    if (url.pathname === \"/health\") {\n      return Response.json(cb.health());\n    }\n\n    if (url.pathname === \"/diagnostics\") {\n      return Response.json(await cb.diagnostics());\n    }\n\n    if (url.pathname === \"/api/data\") {\n      try {\n        const data = await apiBreaker.execute(() =\u003e\n          fetch(\"https://api.example.com/data\").then((r) =\u003e r.json())\n        );\n        return Response.json(data);\n      } catch (err) {\n        return Response.json({ error: \"Service unavailable\" }, { status: 503 });\n      }\n    }\n\n    return new Response(\"Not found\", { status: 404 });\n  },\n  port: 3000,\n});\n```\n\n## Configuration\n\n### `createBreaker(config)`\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `redisUrl` | `string?` | — | Redis connection URL. Omit for SQLite/Memory only |\n| `redisKeyPrefix` | `string` | `bunbreaker` | Redis key/channel namespace for shared Redis deployments |\n| `redisReconnectIntervalMs` | `number` | `30000` | Retry Redis startup initialization after failure. Set `0` to disable |\n| `sqlite.path` | `string` | `./bunbreaker.db` | SQLite database file path |\n| `sqlite.allowInMemoryFallback` | `boolean` | `false` | Continue with non-durable in-memory SQLite if the configured path fails |\n| `sqlite.auditRetentionSecs` | `number` | `2592000` (30d) | Retain audit transition events. Set `0` to retain forever |\n| `sqlite.deliveredRetentionSecs` | `number` | `604800` (7d) | Retain delivered events. Set `0` to retain forever |\n| `sqlite.deadRetentionSecs` | `number` | `2592000` (30d) | Retain dead and stale pending events. Set `0` to retain forever |\n| `sqlite.autoPurge` | `boolean` | `true` | Auto-purge old events |\n| `sqlite.purgeSchedule` | `string` | `0 3 * * *` | Purge cron (UTC) |\n| `probeSchedule` | `string` | `* * * * *` | Health probe cron (UTC) |\n| `memoryCacheTtlMs` | `number` | `7000` | Memory cache TTL in ms |\n\n### `.for(name, config)` — Breaker Config\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `failureThreshold` | `number?` | — | Absolute failure count to trigger OPEN |\n| `percentageThreshold` | `number?` | — | Error % (0–100) to trigger OPEN |\n| `volumeThreshold` | `number?` | — | Minimum requests before percentage check |\n| `windowSecs` | `number` | — | Sliding window duration in seconds |\n| `resetTimeoutSecs` | `number` | — | Seconds in OPEN before HALF_OPEN |\n| `timeoutMs` | `number` | — | Max ms to wait for fn() |\n| `capacity` | `number?` | — | Max concurrent in-flight executions |\n| `enabled` | `boolean?` | `true` | Set `false` to bypass all breaker logic |\n| `retry` | `RetryConfig?` | — | Retry configuration (see below) |\n| `errorMapper` | `ErrorMapper?` | — | Map errors to domain types |\n| `errorClassifier` | `function?` | — | Override default classification |\n| `fallback` | `function?` | — | Called when OPEN instead of throwing |\n| `queueOnOpen` | `boolean?` | `true` | Enqueue payloads when OPEN |\n| `queueMaxPending` | `number` | `10000` | Max pending queued payloads per breaker. Set `0` to disable |\n| `queueMaxPayloadBytes` | `number` | `262144` | Max serialized queued payload size. Set `0` to disable |\n| `queuePendingTtlSecs` | `number` | `604800` | Purge stale pending rows before enqueue. Set `0` to disable |\n\n### `RetryConfig`\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `retries` | `number` | — | Number of retry attempts |\n| `factor` | `number` | `2` | Exponential backoff factor |\n| `minTimeoutMs` | `number` | `250` | Minimum delay between retries |\n| `maxTimeoutMs` | `number` | `5000` | Maximum delay between retries |\n| `maxRetryTimeMs` | `number` | `Infinity` | Hard total wall-clock budget |\n| `shouldRetry` | `function?` | — | Override per-error retryability |\n| `onRetry` | `function?` | — | Called on each retry attempt |\n\n### `ProbeConfig`\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `url` | `string` | — | GET endpoint to probe |\n| `expectedStatus` | `number` | `200` | Status required for a successful probe |\n| `timeoutMs` | `number` | `3000` | Probe request timeout |\n| `gateHalfOpen` | `boolean` | `true` | Require probe success before OPEN -\u003e HALF_OPEN |\n\n### `ReplayerConfig`\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `batchSize` | `number` | `50` | Events to process per batch |\n| `batchDelayMs` | `number` | `1000` | Delay between batches |\n| `maxJitterMs` | `number` | `5000` | Random startup delay before replay |\n| `lockTtlSecs` | `number` | `30` | Distributed replay lock TTL |\n| `handlerTimeoutMs` | `number` | `30000` | Per-event handler timeout. Set `0` to disable |\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────────┐\n│                  BunbreakerInstance                  │\n│  .for()  .events  .health()  .diagnostics()         │\n├─────────────────────────────────────────────────────┤\n│                   CircuitBreaker                    │\n│  execute() → raceWithTimeout → classify → threshold │\n│  executeWithAbort() → AbortController → classify    │\n│  retry integration → only final error counts        │\n├─────────────────────────────────────────────────────┤\n│                    StoreManager                     │\n│         Redis → SQLite → Memory (fallback)          │\n│         Meta-breaker on Redis itself                │\n├──────────┬──────────────┬───────────────────────────┤\n│ RedisStore│  SQLiteStore  │     MemoryStore          │\n│ Sorted set│  WAL mode     │     Map + TTL            │\n│ Lua atomic│  Audit log    │     Last resort          │\n│ Pub/Sub   │  Event queue  │                          │\n└──────────┴──────────────┴───────────────────────────┘\n```\n\n## API Reference\n\n### `CircuitBreaker`\n\n| Method | Description |\n|--------|-------------|\n| `execute(fn, payload?)` | Execute with timeout race + optional retry |\n| `executeWithAbort(fn, payload?)` | Execute with `AbortSignal` on timeout |\n| `executeSelfTimed(fn, payload?)` | Execute without timeout (caller manages timeout) |\n| `getState()` | Get current circuit state |\n| `getStats()` | Get diagnostics stats snapshot |\n| `setEnabled(enabled)` | Toggle breaker logic at runtime |\n\n### `BunbreakerInstance`\n\n| Method | Description |\n|--------|-------------|\n| `for(name, config)` | Create or retrieve a named breaker |\n| `events` | Typed event emitter |\n| `probe(name, config)` | Register a health probe |\n| `health()` | Get store health status |\n| `queue` | Access the local event queue |\n| `replayer(config?)` | Create an event replayer |\n| `diagnostics()` | Get full diagnostics snapshot |\n| `maintenance()` | Manual SQLite VACUUM |\n| `shutdown()` | Graceful shutdown |\n\n### Standalone Functions\n\n| Function | Description |\n|----------|-------------|\n| `fetchWithBreaker(breaker, input, init?, options?)` | Abort-aware fetch with circuit breaker |\n| `executeWithRetry(fn, ctx)` | Pure retry engine (no breaker dependency) |\n| `classifyError(err)` | Default error classifier |\n\n### Alert Adapters\n\nAlert adapters default to a 5000ms network timeout and swallow/log delivery failures so alerts never crash protected application code.\n\n```ts\ncb.events\n  .on(\"opened\", telegramAlert(token, chatId, { timeoutMs: 3000 }))\n  .on(\"opened\", webhookAlert(url, { Authorization: \"Bearer ...\" }, { timeoutMs: 3000 }))\n  .on(\"opened\", resendAlert(apiKey, [\"ops@example.com\"], \"alerts@example.com\", { timeoutMs: 3000 }));\n```\n\n## Events\n\n```ts\ncb.events\n  .on(\"opened\", (e) =\u003e { /* e.name, e.failures, e.ts */ })\n  .on(\"closed\", (e) =\u003e { /* e.name, e.ts */ })\n  .on(\"half_open\", (e) =\u003e { /* e.name, e.ts */ })\n  .on(\"rejected\", (e) =\u003e { /* e.name, e.ts */ })\n  .on(\"capacity_rejected\", (e) =\u003e { /* e.name, e.capacity, e.ts */ })\n  .on(\"fallback\", (e) =\u003e { /* e.name, e.ts */ })\n  .on(\"ignored_error\", (e) =\u003e { /* e.name, e.reason, e.ts */ })\n  .on(\"queue_error\", (e) =\u003e { /* e.name, e.reason, e.ts */ })\n  .on(\"queue_purge_warning\", (e) =\u003e { /* e.name, e.deadCount, e.ts */ })\n  .on(\"*\", (e) =\u003e { /* wildcard — all events */ });\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faashahin%2Fbunbreaker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faashahin%2Fbunbreaker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faashahin%2Fbunbreaker/lists"}