# `retry` _(no submodules)_ ## 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(fn: (attempt: number) => Promise, options?: { signal?: AbortSignal; onRetry?: RetryHook; to?: string; }): Promise` - `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.