# retry-helper _(no submodules)_ ## Purpose Coordinates retry logic for HTTP requests against Rackspace Cloud Files and the Rackspace CDN management API. Distinguishes transient outcomes (`408`/`425`/`429`/`5xx`) from fatal ones, honours `Retry-After` when the server provides it, and falls back to exponential backoff with a configurable cap. AbortSignal is plumbed through every wait so cancellation lands within milliseconds. This is structurally similar to `@suaveplan/storage-http`'s retry helper but lives in this package so the adapter can use Rackspace-specific error codes for exhausted retries — a `STORAGE_CDN_RACKSPACE.UPSTREAM_ERROR` surfaces with `container` and `region` context that the generic helper cannot carry. ## 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 without real timers. - Exhausted retries surface as `STORAGE_CDN_RACKSPACE.UPSTREAM_ERROR`. ## Usage: Basic ```ts import { retryWithBackoff } from "@suaveplan/storage-cdn-rackspace"; const response = await retryWithBackoff(() => fetch("https://cdn.example.com/file.bin")); ``` ## Usage: Advanced ```ts import { retryWithBackoff } from "@suaveplan/storage-cdn-rackspace"; const response = await retryWithBackoff( () => fetch(url, { signal, headers: { "x-auth-token": token } }), { retry: { maxAttempts: 5, baseDelayMs: 200, maxDelayMs: 10_000, honourRetryAfter: true }, timers: fakeTimers, signal, }, ); ``` ## API Reference ### `retryWithBackoff(fn, options?): Promise` - `fn: () => Promise` — 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_RACKSPACE.UPSTREAM_ERROR`. Returns the last response after exhaustion (caller decides how to surface a final transient status) or throws on synthesised exhaustion (no response, no error captured) / 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 1 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, and a generic helper would duplicate the classifier. - 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 Rackspace error registry deliberately keeps abort under the upstream umbrella — operators monitoring "request canceled" already filter on `cause`-shape, not a separate code.