# `retry` _(no submodules)_ ## Purpose Provides `withRetry`, a small exponential-backoff helper used by the connection pool, the migration runner, and any caller-supplied wrapping of adapter calls. The helper is intentionally minimal: it owns the loop, the backoff calculation, the optional jitter, and the abort plumbing. Higher-level concerns (circuit breakers, dead-letter routing, OTel spans) are layered on top by the caller — keeping retry stateless makes it composable. Backoff follows the canonical formula `min(baseBackoffMs * 2^(attempt-1), maxBackoffMs)`. Jittered mode multiplies the result by a `[0, 1)` random factor so concurrent callers don't synchronise their retry windows. Both the RNG and the `setTimeout` setter are injectable so tests run deterministically without time travel. ## Features - Exponential backoff capped at `maxBackoffMs`. - Optional full-jitter randomisation through an injectable `Rng`. - `isRetryable(err, attempt)` predicate for bail-out on permanent failures. - `AbortSignal` propagation cancels pending sleeps and the loop. - Pure async — no global state, no side effects beyond the optional sleep. ## Usage: Basic ```ts import { withRetry } from "@suaveplan/api-plugin-database/retry/retry"; const result = await withRetry(() => db.query("SELECT 1"), { maxAttempts: 3, baseBackoffMs: 50, maxBackoffMs: 1_000, jitter: true, }); ``` ## Usage: Advanced ```ts import { withRetry } from "@suaveplan/api-plugin-database"; const controller = new AbortController(); setTimeout(() => controller.abort(), 30_000); const data = await withRetry( async (attempt) => { const adapter = await pool.acquire(); try { return await adapter.query("SELECT * FROM users WHERE active"); } finally { pool.release(adapter); } }, { maxAttempts: 5, baseBackoffMs: 100, maxBackoffMs: 5_000, jitter: true, signal: controller.signal, isRetryable: (err) => err instanceof Error && /timeout|reset/i.test(err.message), } ); ``` ## API Reference ### `withRetry(fn, options): Promise` Invokes `fn(attempt)` (1-indexed) until success or budget exhaustion. Returns the resolved value or rethrows the final error. ### `interface RetryOptions` - `maxAttempts` (number, >=1) — total attempt budget. - `baseBackoffMs` (number, >=1) — base interval. - `maxBackoffMs` (number, >=1) — interval ceiling. - `jitter` (boolean) — multiply by `random()` for full jitter. - `isRetryable?` (`(err, attempt) => boolean`) — bail predicate. - `rng?` (`Rng`) — injectable random source. - `signal?` (`AbortSignal`) — cancels in-flight sleep + loop. - `schedule?` (`(cb, ms) => void`) — injectable `setTimeout` for tests. ## Implementation Notes - Sleeps are interrupted promptly by `AbortSignal`; the inner promise rejects with the signal's `reason` (when an `Error`) or a generic `Error("withRetry aborted")`. - Aborts at the top of the loop (before `fn(attempt)`) short-circuit the retry budget immediately. - The helper does not catch or rewrap thrown values — callers see the underlying error. This keeps the helper independent of the package's error registry, which composes downstream. - Helper functions are intentionally inline-sized; the file stays under the 300-line hard limit.