{"id":44881013,"url":"https://github.com/calummacc/nest-failover","last_synced_at":"2026-02-17T16:06:03.649Z","repository":{"id":308916544,"uuid":"1034549606","full_name":"calummacc/nest-failover","owner":"calummacc","description":"Generic multi-provider orchestrator for NestJS with priority, fallback, parallel, and retries.","archived":false,"fork":false,"pushed_at":"2025-08-14T16:15:40.000Z","size":91,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-22T02:23:21.102Z","etag":null,"topics":["failover","fallback","multi-provider","nestjs","nestjs-library","nestjs-module","nodejs","orchestration","parallel-execution","priority-execution","resilience","retries","typescript"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@calumma/nest-failover","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/calummacc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2025-08-08T15:12:06.000Z","updated_at":"2025-08-14T16:15:14.000Z","dependencies_parsed_at":"2025-08-08T17:46:11.484Z","dependency_job_id":"97402a0d-b052-4550-b9b3-6657f78918fe","html_url":"https://github.com/calummacc/nest-failover","commit_stats":null,"previous_names":["calummacc/nest-failover"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/calummacc/nest-failover","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/calummacc%2Fnest-failover","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/calummacc%2Fnest-failover/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/calummacc%2Fnest-failover/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/calummacc%2Fnest-failover/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/calummacc","download_url":"https://codeload.github.com/calummacc/nest-failover/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/calummacc%2Fnest-failover/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29549274,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-17T14:33:00.708Z","status":"ssl_error","status_checked_at":"2026-02-17T14:32:58.657Z","response_time":100,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["failover","fallback","multi-provider","nestjs","nestjs-library","nestjs-module","nodejs","orchestration","parallel-execution","priority-execution","resilience","retries","typescript"],"created_at":"2026-02-17T16:06:02.767Z","updated_at":"2026-02-17T16:06:03.643Z","avatar_url":"https://github.com/calummacc.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"### @calumma/nest-failover — Multi‑provider failover for NestJS \n\n[![npm version](https://img.shields.io/npm/v/%40calumma%2Fnest-failover.svg)](https://www.npmjs.com/package/@calumma/nest-failover)\n[![npm downloads](https://img.shields.io/npm/dm/%40calumma%2Fnest-failover.svg)](https://www.npmjs.com/package/@calumma/nest-failover)\n[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)\n[![GitHub](https://img.shields.io/badge/github-calummacc%2Fnest--failover-24292e?logo=github\u0026logoColor=white)](https://github.com/calummacc/nest-failover)\n\n\nA tiny, type-safe **failover \u0026 multi-provider orchestration** module for **NestJS**.  \nWith v2, you can define **multi-operation providers** (e.g., `upload`, `download`, `presign`) and call them via:\n\n- `executeOp` — **sequential** failover by priority\n- `executeAnyOp` — **parallel-any**; returns the first success\n- `executeAllOp` — **parallel-all**; collects all outcomes\n\nIncludes **retry with backoff** (classic algorithms + jitter), **per-op/per-provider policy**, **provider filtering**, and **observable hooks** for metrics.\n\n\u003e v1 single-operation API remains available but is **deprecated**. See **[Migration from v1](#migration-from-v1)**.\n\n---\n\n## Table of Contents\n\n- [Why this module?](#why-this-module)\n- [Install](#install)\n- [Quick Start (MultiOp)](#quick-start-multiop)\n- [Core Concepts](#core-concepts)\n  - [Operation Shapes](#operation-shapes)\n  - [MultiOpProvider Interface](#multiopprovider-interface)\n  - [FallbackCoreModule Options](#fallbackcoremodule-options)\n  - [Policy Resolution Precedence](#policy-resolution-precedence)\n- [API Reference](#api-reference)\n  - [`executeOp`](#executeop)\n  - [`executeAnyOp`](#executeanyop)\n  - [`executeAllOp`](#executeallop)\n  - [Legacy APIs (Deprecated)](#legacy-apis-deprecated)\n- [Retry \u0026 Backoff](#retry--backoff)\n  - [Algorithms](#algorithms)\n  - [Respecting Retry-After](#respecting-retry-after)\n  - [Choosing a Strategy](#choosing-a-strategy)\n- [Hooks \u0026 Telemetry](#hooks--telemetry)\n- [Examples](#examples)\n  - [StorageOps: upload, download, presign](#storageops-upload-download-presign)\n  - [Sequential with Priority \u0026 Retry](#sequential-with-priority--retry)\n  - [Parallel Any (Fastest Success)](#parallel-any-fastest-success)\n  - [Parallel All (Health Fanout)](#parallel-all-health-fanout)\n  - [Filtering Providers](#filtering-providers)\n- [Migration from v1](#migration-from-v1)\n- [Error Model](#error-model)\n- [Performance Tips](#performance-tips)\n- [Troubleshooting \u0026 FAQ](#troubleshooting--faq)\n- [TypeScript Notes](#typescript-notes)\n- [Versioning](#versioning)\n- [Contributing](#contributing)\n- [License](#license)\n\n---\n\n## Why this module?\n\nWhen you must call the **same capability** across multiple backends/providers (e.g., S3, R2, GCS), you often want:\n\n- **Failover**: try providers in order until one succeeds\n- **Parallel-any**: return the **first** provider that completes successfully\n- **Parallel-all**: **fan out** to all providers and inspect outcomes\n- **Typed input/output** per operation (not just `any`)\n- **Retry with backoff** and **jitter** to avoid thundering herds\n- **Per-op/per-provider policy** tuning (different SLA/behavior)\n- **Hooks** for logging/metrics\n\nThis module gives you these primitives with a tiny surface and solid type-safety.\n\n---\n\n## Install\n\n```bash\nnpm install @calumma/nest-failover\n# or\nyarn add @calumma/nest-failover\n# or\npnpm add @calumma/nest-failover\n```\n\nPeer dep: `@nestjs/common` v9+. Works with ESM or CJS TypeScript targets.\n\n### Named Exports\n\n```ts\nimport {\n  FallbackCoreModule,\n  FallbackCoreService,\n  OpShape,\n  MultiOpProvider,\n  AllProvidersFailedError,\n  wrapLegacyAsMultiOp,\n  // types\n  RetryPolicy,\n  PolicyConfig,\n} from '@calumma/nest-failover';\n```\n\n---\n\n## Quick Start (MultiOp)\n\nDefine your **operations** and a **provider**:\n\n```ts\n// types.ts\nimport { OpShape, MultiOpProvider } from '@calumma/nest-failover';\n\nexport type StorageOps = {\n  upload:   OpShape\u003c{ key: string; data: Buffer }, { key: string; url?: string }\u003e;\n  download: OpShape\u003c{ key: string }, { stream: NodeJS.ReadableStream }\u003e;\n  presign:  OpShape\u003c{ key: string; expiresIn?: number }, { url: string }\u003e;\n};\n\n// s3.provider.ts\nexport class S3Provider implements MultiOpProvider\u003cStorageOps\u003e {\n  name = 's3';\n  capabilities = {\n    upload:   async (i) =\u003e ({ key: i.key, url: await this.putObject(i) }),\n    download: async (i) =\u003e ({ stream: await this.getStream(i.key) }),\n    presign:  async (i) =\u003e ({ url: await this.signedUrl(i.key, i.expiresIn) }),\n  };\n  // optional per-provider hooks\n  async beforeExecuteOp(op, input) { /* custom logging */ }\n  async afterExecuteOp(op, input, output) { /* metrics */ }\n\n  // ... private methods to talk to S3 SDK ...\n}\n```\n\n### forRootAsync example\n\n```ts\n// app.module.ts\n@Module({\n  imports: [\n    FallbackCoreModule.forRootAsync\u003cStorageOps\u003e({\n      useFactory: async () =\u003e {\n        // e.g. load secrets/SDK clients here\n        return {\n          providers: [\n            { provider: new S3Provider(),  policy: { maxRetry: 2, baseDelayMs: 200 } },\n            { provider: new R2Provider(),  policy: { maxRetry: 1 } },\n            { provider: new GCSProvider(), policy: { maxRetry: 1 } },\n          ],\n          policy: {\n            default: { maxRetry: 1, baseDelayMs: 150, maxDelayMs: 5000, backoff: 'fullJitter' },\n            perOp: { upload: { maxRetry: 3 } },\n            perProvider: { r2: { baseDelayMs: 250 } },\n          },\n        };\n      },\n      inject: [], // add ConfigService/etc. if needed\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\nWire it into your module:\n\n```ts\n// app.module.ts\nimport { Module } from '@nestjs/common';\nimport { FallbackCoreModule, OpShape } from '@calumma/nest-failover';\nimport { S3Provider } from './s3.provider';\nimport { R2Provider } from './r2.provider';\nimport { GCSProvider } from './gcs.provider';\nimport { StorageOps } from './types';\n\n@Module({\n  imports: [\n    FallbackCoreModule.forRoot\u003cStorageOps\u003e({\n      providers: [\n        { provider: new S3Provider(),  policy: { maxRetry: 2, baseDelayMs: 200 } },\n        { provider: new R2Provider(),  policy: { maxRetry: 1 } },\n        { provider: new GCSProvider(), policy: { maxRetry: 1 } },\n      ],\n      policy: {\n        default: { maxRetry: 1, baseDelayMs: 150, maxDelayMs: 5000, backoff: 'fullJitter' },\n        perOp: { upload: { maxRetry: 3 } },                 // heavier retry for upload\n        perProvider: { r2: { baseDelayMs: 250 } },          // tune per provider\n      },\n      hooks: {\n        onProviderSuccess: (ctx) =\u003e {/* log/metrics */},\n        onProviderFail:    (ctx) =\u003e {/* warn/metrics */},\n        onAllFailed:       (ctx) =\u003e {/* alert */},\n      },\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\nUse it in a service:\n\n```ts\nimport { Injectable } from '@nestjs/common';\nimport { FallbackCoreService } from '@calumma/nest-failover';\nimport { StorageOps } from './types';\n\n@Injectable()\nexport class FileService {\n  constructor(private readonly failover: FallbackCoreService\u003cStorageOps\u003e) {}\n\n  async upload(key: string, data: Buffer) {\n    return this.failover.executeOp('upload', { key, data });\n  }\n\n  async presign(key: string) {\n    return this.failover.executeOp('presign', { key, expiresIn: 3600 }, { providerNames: ['s3', 'gcs'] });\n  }\n}\n```\n\n---\n\n## Core Concepts\n\n### Operation Shapes\n\n```ts\nexport type OpShape\u003cI = unknown, O = unknown\u003e = { in: I; out: O };\n```\n\nDefine a **map** of operation names to `{ in, out }` to get precise typing per operation.\n\n### MultiOpProvider Interface\n\n```ts\nexport interface MultiOpProvider\u003cOps extends Record\u003cstring, OpShape\u003e\u003e {\n  name: string;\n  capabilities: {\n    [K in keyof Ops]: (input: Ops[K]['in']) =\u003e Promise\u003cOps[K]['out']\u003e;\n  };\n  beforeExecuteOp?\u003cK extends keyof Ops\u003e(op: K, input: Ops[K]['in']): void | Promise\u003cvoid\u003e;\n  afterExecuteOp?\u003cK extends keyof Ops\u003e(op: K, input: Ops[K]['in'], output: Ops[K]['out']): void | Promise\u003cvoid\u003e;\n}\n```\n\n\u003e Note: Each provider’s `name` must be unique. It’s used for filtering, policy resolution (`perProvider`), logs, and error aggregation. Duplicate names may cause confusing behavior.\n\n### FallbackCoreModule Options\n\n```ts\nexport type BackoffKind =\n  | 'none'\n  | 'linear'\n  | 'exp'\n  | 'fullJitter'\n  | 'equalJitter'\n  | 'decorrelatedJitter'\n  | 'fibonacci';\n\nexport type RetryPolicy = {\n  maxRetry?: number;     // default 0\n  baseDelayMs?: number;  // default 200\n  maxDelayMs?: number;   // default 5000\n  backoff?: BackoffKind; // default 'fullJitter'\n};\n\nexport type PolicyConfig\u003cOpNames extends string = string\u003e = {\n  default?: RetryPolicy;\n  perOp?: Partial\u003cRecord\u003cOpNames, RetryPolicy\u003e\u003e;\n  perProvider?: Record\u003cstring, RetryPolicy\u003e;\n};\n\nexport type FallbackCoreOptions\u003cOps extends Record\u003cstring, OpShape\u003e = any\u003e = {\n  providers: Array\u003c\n    | { provider: MultiOpProvider\u003cOps\u003e; policy?: RetryPolicy }   // v2\n    | { provider: IProvider\u003cany, any\u003e; policy?: RetryPolicy }    // legacy (v1)\n  \u003e;\n  policy?: PolicyConfig\u003ckeyof Ops \u0026 string\u003e;\n  hooks?: {\n    onProviderSuccess?: (ctx: { provider: string; op?: string; attempt: number; durationMs: number; delayMs?: number }, input: unknown, output: unknown) =\u003e void | Promise\u003cvoid\u003e;\n    onProviderFail?:    (ctx: { provider: string; op?: string; attempt: number; durationMs: number; delayMs?: number }, input: unknown, error: unknown) =\u003e void | Promise\u003cvoid\u003e;\n    onAllFailed?:       (ctx: { op?: string }, input: unknown, errors: ProviderAttemptError[]) =\u003e void | Promise\u003cvoid\u003e;\n  };\n};\n```\n\n### Policy Resolution Precedence\n\nEffective retry policy is computed with priority:\n\n```\nperProvider[providerName] \u003e perOp[opName] \u003e provider.inlinePolicy \u003e policy.default\n```\n\nMissing fields cascade to lower priority and finally to defaults:\n`maxRetry=0`, `baseDelayMs=200`, `maxDelayMs=5000`, `backoff='fullJitter'`.\n\n---\n\n## API Reference\n\n### `executeOp`\n\n```ts\nexecuteOp\u003cK extends keyof Ops\u003e(\n  op: K,\n  input: Ops[K]['in'],\n  options?: { providerNames?: string[] }\n): Promise\u003cOps[K]['out']\u003e;\n```\n\n* **Sequential**: tries providers in the configured order.\n* Applies per-provider retry with backoff.\n* Skips providers that **don’t implement** `op`.\n* Stops on first success; throws `AllProvidersFailedError` if all failed.\n\n### `executeAnyOp`\n\n```ts\nexecuteAnyOp\u003cK extends keyof Ops\u003e(\n  op: K,\n  input: Ops[K]['in'],\n  options?: { providerNames?: string[] }\n): Promise\u003cOps[K]['out']\u003e;\n```\n\n* **Parallel-any**: runs all eligible providers concurrently (each with its retry loop).\n* Resolves with the **first** success; rejects with `AllProvidersFailedError` if none succeed.\n\n### `executeAllOp`\n\n```ts\nexecuteAllOp\u003cK extends keyof Ops\u003e(\n  op: K,\n  input: Ops[K]['in'],\n  options?: { providerNames?: string[] }\n): Promise\u003cArray\u003c\n  { provider: string; ok: true; value: Ops[K]['out'] } |\n  { provider: string; ok: false; error: unknown }\n\u003e\u003e;\n```\n\n* **Parallel-all**: runs all eligible providers concurrently.\n* Returns **all** outcomes (no throw).\n\n### Legacy APIs (Deprecated)\n\nThese remain for backward compatibility and internally route via a `'default'` operation using a legacy adapter:\n\n* `execute(input)`\n* `executeAny(input)`\n* `executeAll(input)`\n* `executeWithFilter(input, providerNames, mode)`\n\nPrefer using **`executeOp` / `executeAnyOp` / `executeAllOp`**.\n\n---\n\n## Retry \u0026 Backoff\n\n### Algorithms\n\nSupported `backoff` kinds:\n\n| Kind                 | Formula (cap by `maxDelayMs`)      | Notes                                |\n| -------------------- | ---------------------------------- | ------------------------------------ |\n| `none`               | `0`                                | No delay between retries             |\n| `linear`             | `base * attempt`                   | Simple, predictable                  |\n| `exp`                | `base * 2^(attempt-1)`             | Classic exponential                  |\n| `fullJitter`         | `random(0, base * 2^(attempt-1))`  | Recommended default; avoids herds    |\n| `equalJitter`        | `baseExp/2 + random(0, baseExp/2)` | Softer jitter                        |\n| `decorrelatedJitter` | `random(base, prevDelay * 3)`      | Great for flaky networks             |\n| `fibonacci`          | `base * Fib(attempt)`              | Middle ground between linear and exp |\n\n### Respecting Retry-After\n\nIf a provider error includes `retryAfterMs` **or** HTTP `Retry-After` header, the next delay **overrides** the computed backoff.\n\nServers may send `Retry-After` as either seconds or an HTTP-date. This library first tries to parse a number (seconds); if it’s a date, you should convert it to milliseconds and attach as `error.retryAfterMs` on your error before rethrowing.\n\n```ts\nfunction retryAfterToMs(value: string): number | undefined {\n  const secs = Number(value);\n  if (!Number.isNaN(secs)) return secs * 1000;\n  const asDate = Date.parse(value);\n  if (!Number.isNaN(asDate)) return Math.max(0, asDate - Date.now());\n  return undefined;\n}\n```\n\n### Choosing a Strategy\n\n* Default: **`fullJitter`** with `baseDelayMs=200`, `maxDelayMs=5000`, `maxRetry=3`.\n* Network-heavy ops (upload/download): `decorrelatedJitter` or `fullJitter`.\n* Lightweight ops (presign/metadata): `linear` with small `maxRetry`.\n\n```ts\n// Tune upload heavier than presign, and tweak a specific provider\npolicy: {\n  default: { maxRetry: 2, baseDelayMs: 200, maxDelayMs: 5000, backoff: 'fullJitter' },\n  perOp: {\n    upload:  { maxRetry: 4, baseDelayMs: 250, backoff: 'decorrelatedJitter' },\n    presign: { maxRetry: 1, baseDelayMs: 100, backoff: 'linear' },\n  },\n  perProvider: {\n    gcs: { maxRetry: 3, baseDelayMs: 300 }, // overrides above for GCS\n  },\n}\n```\n\n---\n\n## Hooks \u0026 Telemetry\n\nGlobal hooks receive context including provider, op, attempt, duration, and `delayMs` (if retrying):\n\n```ts\nhooks: {\n  onProviderSuccess: ({ provider, op, attempt, durationMs }) =\u003e {},\n  onProviderFail:    ({ provider, op, attempt, durationMs, delayMs }) =\u003e {},\n  onAllFailed:       ({ op }, input, attempts) =\u003e {},\n}\n```\n\nUse these to export metrics (e.g., Prometheus/OpenTelemetry) or attach structured logs.\n\n---\n\n## Examples\n\n### StorageOps: upload, download, presign\n\n```ts\nexport type StorageOps = {\n  upload:   OpShape\u003c{ key: string; data: Buffer }, { key: string; url?: string }\u003e;\n  download: OpShape\u003c{ key: string }, { stream: NodeJS.ReadableStream }\u003e;\n  presign:  OpShape\u003c{ key: string; expiresIn?: number }, { url: string }\u003e;\n};\n```\n\nThree providers implementing different cloud SDKs (`S3Provider`, `R2Provider`, `GCSProvider`) expose the same capabilities.\n\n### Sequential with Priority \u0026 Retry\n\n```ts\nconst out = await failover.executeOp('upload', { key: 'a.txt', data: buf });\n// Tries S3 -\u003e R2 -\u003e GCS, with per-provider retry and backoff\n```\n\n### Parallel Any (Fastest Success)\n\n```ts\nconst stream = await failover.executeAnyOp('download', { key: 'a.txt' });\n// Resolves with the first provider that returns successfully\n```\n\n\u003e Cancellation: When the first provider succeeds, other in-flight attempts are ignored best-effort. Depending on your SDK, you can wire an `AbortController` inside your provider to cancel underlying requests.\n\n```ts\n// Inside a provider method:\nconst ac = new AbortController();\ntry {\n  const res = await fetch(url, { signal: ac.signal });\n  return await res.json();\n} finally {\n  // expose a cancel hook if your runtime supports it\n}\n```\n\n### Parallel All (Health Fanout)\n\n```ts\nconst res = await failover.executeAllOp('presign', { key: 'a.txt', expiresIn: 3600 });\n// Inspect success/failure of every provider\n```\n\n### Filtering Providers\n\n```ts\nawait failover.executeOp('presign', { key: 'a.txt' }, { providerNames: ['s3', 'gcs'] });\n```\n\n```ts\n// Without filter; all capable providers are considered automatically\nawait failover.executeOp('presign', { key: 'a.txt' });\n```\n\n\u003e Tip: Filtering by `providerNames` narrows candidates before capability checks. If you pass a name that doesn’t implement the `op`, it will be skipped. If all filtered providers are incompatible, you’ll get `AllProvidersFailedError` quickly.\n\n---\n\n## Migration from v1\n\nv1 exposed a single-operation `IProvider\u003cInput, Output\u003e` with methods like `execute`, `executeAny`, `executeAll`.\n\nIn v2:\n\n* Prefer **MultiOpProvider** and **`executeOp/AnyOp/AllOp`**.\n* Legacy usage continues to work, but is **deprecated**.\n\n### Adapting a v1 Provider\n\nWrap a legacy provider to a `'default'` op:\n\n```ts\nimport { wrapLegacyAsMultiOp } from '@calumma/nest-failover';\n\nconst legacy = { name: 'old', execute: async (input: In): Promise\u003cOut\u003e =\u003e {/*...*/} };\nconst v2provider = wrapLegacyAsMultiOp(legacy, 'default');\n```\n\nThen call:\n\n```ts\nawait failover.executeOp('default' as any, input);\n```\n\nOr convert to a proper MultiOpProvider by defining explicit ops.\n\n```ts\n// If you want type safety without 'as any':\ntype LegacyOps = { default: OpShape\u003cIn, Out\u003e };\nconst wrapped = wrapLegacyAsMultiOp\u003cIn, Out\u003e(legacy, 'default');\n// register `wrapped` in FallbackCoreModule.forRoot\u003cLegacyOps\u003e(...)\nawait failover.executeOp\u003c'default'\u003e('default', input);\n```\n\n\u003e You can also keep calling `execute`/`executeAny`/`executeAll`; they route through a `'default'` op internally. Prefer `executeOp` for new code.\n\n---\n\n## Error Model\n\nWhen all providers fail:\n\n```ts\nexport class AllProvidersFailedError extends Error {\n  constructor(\n    public readonly op: string | undefined,\n    public readonly attempts: ProviderAttemptError[]\n  ) { super(`All providers failed${op ? ` for op \"${op}\"` : ''}`); }\n}\n\nexport type ProviderAttemptError = {\n  provider: string;\n  op?: string;\n  attempt: number;\n  error: unknown;\n};\n```\n\n* `executeOp` / `executeAnyOp` throw `AllProvidersFailedError`.\n* `executeAllOp` **never throws**; returns `{ ok: false, error }` entries.\n\n---\n\n## Performance Tips\n\n* Tune **per-op** and **per-provider** policy: uploads can retry more than presign.\n* Use **parallel-any** for latency-sensitive reads (e.g., nearest region/CDN).\n* Add a lightweight **circuit-breaker** outside (e.g., mark provider unhealthy after repeated failures) if needed.\n* Use hooks to track **p50/p95** and success rates per provider/op.\n\n---\n\n## Testing\n\nCreate fake providers that deterministically fail/succeed to validate sequencing and backoff:\n\n```ts\nclass FlakyProvider implements MultiOpProvider\u003cStorageOps\u003e {\n  name = 'flaky';\n  private count = 0;\n  capabilities = {\n    upload: async (i) =\u003e {\n      this.count++;\n      if (this.count \u003c 3) throw Object.assign(new Error('ETEMP'), { code: 'ETEMP' });\n      return { key: i.key };\n    },\n    download: async () =\u003e { throw new Error('not-impl'); },\n    presign: async () =\u003e ({ url: 'https://example.com' }),\n  };\n}\n```\n\nUse `executeOp('upload', ...)` and assert number of attempts/hook calls. For backoff tests, stub timers or inject a time provider.\n\n---\n\n## Troubleshooting \u0026 FAQ\n\n**Q: How do I skip providers that don’t support an operation?**\nA: You don’t need to. The service automatically filters to providers that define the capability for that `op`.\n\n**Q: Can I honor `Retry-After` from HTTP 429/503?**\nA: Yes. If an error includes `retryAfterMs` or an HTTP `Retry-After` header, that delay overrides backoff.\n\n**Q: How do I run only a subset of providers?**\nA: Use `{ providerNames: [...] }` option.\n\n**Q: Does parallel-any cancel other in-flight providers?**\nA: The first success **wins**; other results are ignored best-effort. Depending on your SDKs, you may optionally cancel requests.\n\n**Q: What Node/Nest versions are supported?**\nA: Node 16+ and NestJS 9+. TypeScript is recommended with `strict` mode.\n\n---\n\n## TypeScript Notes\n\n* Prefer defining ops via `OpShape` map to get precise inference.\n* `executeOp('upload', ...)` infers output type specific to `upload`.\n* For legacy code, consider migration to MultiOpProvider for better types.\n\n---\n\n## Versioning\n\n* v2 introduces MultiOpProvider and per-op APIs.\n* v1 APIs are deprecated but still supported through adapters.\n* See releases for detailed changelogs.\n\n## Environment Support\n\n- Node.js: 16+ (tested on 16/18/20)\n- NestJS: 9+\n- TypeScript: 5+ (`strict` recommended)\n- Module formats: ESM \u0026 CJS\n\n---\n\n## Contributing\n\nIssues and PRs are welcome. Please include tests for new features and maintain 100% type coverage in public APIs.\n\n---\n\n## License\n\nMIT © [Calumma](https://github.com/calummacc)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcalummacc%2Fnest-failover","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcalummacc%2Fnest-failover","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcalummacc%2Fnest-failover/lists"}