first commit
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# retry-helper
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Coordinates retry logic for HTTP requests against BunnyCDN Storage Zones and the Bunny.net management API. Distinguishes transient outcomes (`408`/`425`/`429`/`5xx`) from fatal ones, honours `Retry-After` when the server provides it, falls back to exponential backoff with a configurable cap, and surfaces exhausted retries as `STORAGE_CDN_BUNNYCDN.UPSTREAM_ERROR` with `zone`/`region` context.
|
||||
|
||||
Structurally similar to the sibling `storage-cdn-rackspace` retry helper but lives in this package so error codes carry BunnyCDN-specific diagnostics.
|
||||
|
||||
## Features
|
||||
|
||||
- `isTransientStatus(status)` — `408`, `425`, `429`, or any `5xx`.
|
||||
- `parseRetryAfter(value, now)` — delta-seconds and HTTP-date forms; capped at `RETRY_AFTER_MAX_DELAY_MS` (60s).
|
||||
- `retryWithBackoff(fn, options?)` — runs `fn` up to `maxAttempts`, sleeps between transient outcomes, supports cancellation.
|
||||
- Injectable `TimersLike` so tests pin behaviour under a `FakeClock`.
|
||||
- Exhausted retries surface as `STORAGE_CDN_BUNNYCDN.UPSTREAM_ERROR`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { retryWithBackoff } from "@suaveplan/storage-cdn-bunnycdn";
|
||||
|
||||
const response = await retryWithBackoff(() => fetch("https://storage.bunnycdn.com/myz/k"));
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { retryWithBackoff } from "@suaveplan/storage-cdn-bunnycdn";
|
||||
|
||||
const response = await retryWithBackoff(
|
||||
() => fetch(url, { signal, headers: { AccessKey: process.env["BUNNY_KEY"]! } }),
|
||||
{
|
||||
retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 10_000, honourRetryAfter: true },
|
||||
timers: fakeTimers,
|
||||
signal,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `retryWithBackoff(fn, options?): Promise<Response>`
|
||||
|
||||
- `fn: () => Promise<Response>` — attempts the request.
|
||||
- `options.retry?` — `maxAttempts`, `baseDelayMs`, `maxDelayMs`, `honourRetryAfter`.
|
||||
- `options.timers?` — defaults to `globalThis.setTimeout`/`clearTimeout`.
|
||||
- `options.signal?` — cancels the wait between attempts; pre-aborted signal short-circuits with `STORAGE_CDN_BUNNYCDN.UPSTREAM_ERROR`.
|
||||
|
||||
Returns the last response after exhaustion or throws on synthesised exhaustion / abort.
|
||||
|
||||
### `isTransientStatus(status: number): boolean`
|
||||
|
||||
True for `408`, `425`, `429`, or any value in `[500, 599]`.
|
||||
|
||||
### `parseRetryAfter(value: string | null, now: number): number | undefined`
|
||||
|
||||
- Numeric value → delta-seconds × 1000 (capped at `RETRY_AFTER_MAX_DELAY_MS`).
|
||||
- HTTP-date → `parseDate − now` (clamped to 0 if in the past, capped on the high side).
|
||||
- Missing / empty / unparseable → `undefined`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The Retry-After ceiling is **distinct from** `maxDelayMs`; the latter governs exponential backoff, the former specifically defends against the "stall an hour" abuse pattern.
|
||||
- `sleep` listens to the abort event with `{ once: true }` and tears down both the abort listener and the underlying timer on either resolution path — no listener leaks.
|
||||
- The helper is `fetch`-shaped (returns a `Response`) rather than generic — HTTP semantics drive every retry decision.
|
||||
- Synthesised exhaustion (no response, no error) is only reachable when `maxAttempts === 0`; the schema validates `> 0` at the boundary so the path is defensive.
|
||||
- Mid-wait abort throws `UPSTREAM_ERROR` (with `cause = signal.reason`) rather than a separate `ABORTED` code because the BunnyCDN error registry deliberately keeps abort under the upstream umbrella — operators monitoring "request canceled" already filter on `cause`-shape, not a separate code.
|
||||
Reference in New Issue
Block a user