3.5 KiB
Capability Spec: Retry
Package: @suaveplan/retry
Version: 0.0.0 (genesis)
Status: ADDED
ADDED Requirements
RT-01 — Core retry contract
retry(fn, config) MUST call fn at most config.maxAttempts times. Between attempts it MUST sleep for delayFn(attempt) milliseconds. If config.shouldRetry is provided, it MUST be consulted before each retry; returning false stops retrying (result is RetryExhaustedError). On success it MUST return { value, attempts, totalMs }.
RT-02 — Abort between attempts
retry MUST check config.signal before each sleep AND before each fn call. A cancelled AbortSignal MUST cause retry to reject with RetryError (code RETRY.ABORTED) — not RetryExhaustedError. The signal is also forwarded to fn as its argument.
RT-03 — Injectable clock and RNG
RetryConfig.clock?: Clock overrides the sleep timer for deterministic tests. RetryConfig.rng?: Rng is forwarded to jitter computations so jitter is reproducible under test.
RT-04 — Backoff strategies
createConstantBackoff(opts)— fixeddelayms regardless of attempt number.createLinearBackoff(opts)—initial + (attempt - 1) * incrementms.createExponentialBackoff(opts)—initial * base^(attempt-1)ms, capped atmax.applyJitter(delay, mode, rng?)— modifies delay bymode:'full'(uniform 0–delay),'equal'(delay/2 + uniform 0–delay/2),'decorrelated'(decorrelated random walk),'none'(identity).clampDelay(delay, min, max)— clamps to[min, max].
RT-05 — Timeout
withTimeout(fn, ms, signal?) MUST reject with AttemptTimeoutError (code RETRY.TIMEOUT) if fn does not settle within ms milliseconds. The internal timeout is cleaned up on both success and failure. Accepts an optional signal for external cancellation.
RT-06 — Circuit breaker state machine
CircuitBreaker MUST implement: closed → open on reaching failureThreshold consecutive failures; open → half-open after timeout ms (using injected clock); half-open → closed after successThreshold consecutive successes in half-open probes; half-open → open on any failure. .call(fn) in 'open' state MUST throw CircuitOpenError without calling fn. State transitions MUST emit stateChange events via @suaveplan/events.
RT-07 — Exhaustion error shape
RetryExhaustedError MUST carry .attempts: number and .lastError: unknown. AttemptTimeoutError MUST carry .attemptNumber: number and .timeoutMs: number. Both extend RetryError which extends SuaveplanError.
RT-08 — Structured errors
All thrown errors MUST extend RetryError with codes under RETRY.* namespace. CircuitOpenError extends RetryError with code RETRY.CIRCUIT_OPEN.
RT-09 — Zod schemas
RetryConfig, all backoff option types, and CircuitBreakerConfig MUST have Zod schemas exported from *.schemas.ts files. Options are validated at the boundary; invalid options throw RetryError with context.
RT-10 — OTel instrumentation
retry.attempt.total counter (by outcome: success/failure/exhausted/aborted), retry.attempt.delay_ms histogram (by strategy), circuit_breaker.state_change.total counter (by from/to), circuit_breaker.call.duration_ms histogram MUST be emitted via @opentelemetry/api. All are no-ops without exporter.
RT-11 — sideEffects: false + coverage 100%
"sideEffects": false with tree-shake test. Vitest thresholds at 100/100/100/100. No .skip / .only in src/.