2.9 KiB
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
TimersLikeseam — defaults to aglobalThis.setTimeoutadapter; tests inject a controlled timer. - Injectable
randomsource — defaults toMath.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
onRetryhook receives aSmsRetryEventso callers can record per-attempt OTel counters / bus events.
Usage: Basic
import { RetryPolicy } from "@suaveplan/sms/retry";
const policy = new RetryPolicy({ maxAttempts: 3 });
const value = await policy.execute(async () => callRemoteService());
Usage: Advanced
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 RetryPolicyconstructor(options?: RetryPolicyOptions)execute<T>(fn: (attempt: number) => Promise<T>, options?: { signal?: AbortSignal; onRetry?: RetryHook; to?: string; }): Promise<T>
type RetryHook = (event: SmsRetryEvent) => void.interface RetryPolicyOptionsmaxAttempts?: number(default 3)initialDelayMs?: number(default 50)maxDelayMs?: number(default 5000)timers?: TimersLikerandom?: () => 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.