Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
# `retry`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<T>(fn, options): Promise<T>`
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.