first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
# `retry`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`retry` is the exponential-backoff retry policy used by
`SmsService.send` to absorb transient provider failures. The
policy retries a thrown attempt up to `maxAttempts` times with
exponential backoff (`initialDelayMs * 2^attempt`, capped at
`maxDelayMs`) and full jitter, awaits each backoff against an
injectable `Timers` seam so tests can drive the policy without
real wall-clock waits, and honours an `AbortSignal` so cancelled
operations don't sit through a multi-second backoff.
## Features
- Exponential backoff with full-jitter delay computation.
- Injectable `TimersLike` seam — defaults to a
`globalThis.setTimeout` adapter; tests inject a controlled
timer.
- Injectable `random` source — defaults to `Math.random`; tests
pin it to a fixed value for deterministic jitter.
- AbortSignal-aware — a pre-aborted signal halts before the
first attempt; an abort that lands during a backoff cancels
the timer and rejects with `SmsAbortedError`.
- Per-attempt `onRetry` hook receives a `SmsRetryEvent` so
callers can record per-attempt OTel counters / bus events.
## Usage: Basic
```ts
import { RetryPolicy } from "@suaveplan/sms/retry";
const policy = new RetryPolicy({ maxAttempts: 3 });
const value = await policy.execute(async () => callRemoteService());
```
## Usage: Advanced
```ts
import { RetryPolicy } from "@suaveplan/sms/retry";
const ctrl = new AbortController();
const policy = new RetryPolicy({
maxAttempts: 5,
initialDelayMs: 100,
maxDelayMs: 2_000,
});
const result = await policy.execute(
async (attempt) => {
if (attempt < 2) throw new Error("flaky");
return "ok";
},
{
signal: ctrl.signal,
onRetry: (event) => {
console.log("retry", event.attempt, event.error.message);
},
to: "+15551234567",
}
);
```
## API Reference
- `class RetryPolicy`
- `constructor(options?: RetryPolicyOptions)`
- `execute<T>(fn: (attempt: number) => Promise<T>, options?: {
signal?: AbortSignal; onRetry?: RetryHook; to?: string;
}): Promise<T>`
- `type RetryHook = (event: SmsRetryEvent) => void`.
- `interface RetryPolicyOptions`
- `maxAttempts?: number` (default 3)
- `initialDelayMs?: number` (default 50)
- `maxDelayMs?: number` (default 5000)
- `timers?: TimersLike`
- `random?: () => number`
## Implementation Notes
The backoff uses full jitter (`floor(random() * capped)`) which
empirically produces lower contention than equal jitter for SMS
provider rate-limit scenarios. The `onRetry` hook receives the
ZERO-based attempt index for the attempt that just failed, so
the first retry fires `attempt = 0`. The delay loop guards
against abort via an `AbortSignal.addEventListener("abort", …,
{ once: true })` listener that clears the pending timer and
rejects synchronously, ensuring an aborted retry never wakes up
to perform the next attempt.