Files
skills/docs/communication/messaging/sms/src/retry/retry.md
2026-05-29 08:20:56 +01:00

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 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

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 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.