Files
2026-05-27 23:29:53 +01:00

5.4 KiB

OpenSpec Change: enterprise-retry

  • Change ID: enterprise-retry
  • Package: @suaveplan/retry
  • Category: universal
  • Target path: packages/universal/retry
  • Status: IN PROGRESS
  • Date: 2026-04-18
  • Reference spec: /mnt/s/web/suaveplan/enterprise/retry.md

Summary

Greenfield implementation of @suaveplan/retry for the genesis branch. The existing main-branch retry package (100% test coverage) provides the baseline public-API surface to preserve; this change delivers enterprise-grade retry-with-backoff strategies (constant, linear, exponential + jitter), circuit breaker with half-open probing, sleep/timeout helpers, injectable clock and RNG, AbortSignal support throughout, structured errors, Zod schemas, property-based tests, OTel instrumentation, and benchmarks.

Current-state audit (genesis)

The package does not exist on genesis yet — this is a fresh build.

Available workspace deps on genesis

Package Available
@suaveplan/error ✅
@suaveplan/types ✅
@suaveplan/events ✅
@suaveplan/opentelemetry ❌ not yet built — use @opentelemetry/api directly
@suaveplan/feature-flags ❌ not yet built — skip
@suaveplan/schema-registry ❌ not yet built — export Zod schemas directly

External deps (via catalog):

  • zod → catalog:forms
  • @opentelemetry/api → catalog:observability
  • tslib → catalog:build

Dev deps (via catalog):

  • vitest, @vitest/coverage-istanbul → catalog:testing
  • fast-check → catalog:testing
  • mitata → catalog:testing

Preserved public API surface (from main branch)

Core retry

  • retry(fn, config) → Promise<RetryResult<T>> — core retry primitive
  • withRetry(fn, config) → Promise<T> — throws on exhaustion
  • withCircuitBreaker(fn, breaker) → Promise<T> — wraps fn with circuit breaker
  • withTimeout(fn, ms, signal?) → Promise<T> — wraps fn with deadline
  • sleep(ms, signal?) → Promise<void> — cancellable sleep
  • createDelayFn(strategy) → DelayFunction — adapts a strategy to a delay function

Backoff strategies

  • createConstantBackoff(opts) → RetryStrategy — fixed delay
  • createLinearBackoff(opts) → RetryStrategy — linearly increasing delay
  • createExponentialBackoff(opts) → RetryStrategy — exponentially increasing delay
  • applyJitter(delay, mode, rng?) → number — full / equal / decorrelated jitter
  • clampDelay(delay, min, max) → number

Circuit breaker

  • CircuitBreaker class — .call(fn), .reset(), .state getter, event emitters
  • CircuitOpenError extends SuaveplanError
  • CircuitBreakerConfig — { failureThreshold, successThreshold, timeout, halfOpenProbes }
  • CircuitBreakerEventMap — { stateChange: CircuitState }
  • CircuitState — 'closed' | 'open' | 'half-open'

Types

  • RetryConfig — { maxAttempts, delay, shouldRetry?, signal?, clock?, rng? }
  • RetryResult<T> — { value: T; attempts: number; totalMs: number }
  • RetryStrategy — (attempt: number) => number (delay in ms)
  • DelayFunction — alias for RetryStrategy
  • JitterMode — 'full' | 'equal' | 'decorrelated' | 'none'
  • ConstantBackoffOptions, LinearBackoffOptions, ExponentialBackoffOptions

Error types

  • RetryExhaustedError extends SuaveplanError — .attempts, .lastError
  • AttemptTimeoutError extends SuaveplanError — .attemptNumber, .timeoutMs

New additions for genesis

Structured errors

  • RetryError base extends SuaveplanError with codes under RETRY.* namespace
  • Codes: RETRY.EXHAUSTED, RETRY.ABORTED, RETRY.TIMEOUT, RETRY.CIRCUIT_OPEN
  • RetryExhaustedError, AttemptTimeoutError, CircuitOpenError extend RetryError

Zod schemas

  • RetryConfigSchema, ConstantBackoffOptionsSchema, LinearBackoffOptionsSchema, ExponentialBackoffOptionsSchema, CircuitBreakerConfigSchema

Injectable clock + RNG

  • RetryConfig.clock?: Clock — for deterministic sleep (FakeClock in tests)
  • RetryConfig.rng?: Rng — for deterministic jitter computation
  • CircuitBreakerConfig.clock?: Clock

AbortSignal throughout

  • sleep(ms, signal?) — resolves early on abort
  • retry(fn, { signal }) — cancels between attempts on abort
  • CircuitBreaker.call(fn, signal?) — passes signal through to fn

OTel instrumentation

  • Counter: retry.attempt.total (by outcome label: success / failure / exhausted / aborted)
  • Histogram: retry.attempt.delay_ms (per strategy label)
  • Counter: circuit_breaker.state_change.total (by from/to label)
  • Histogram: circuit_breaker.call.duration_ms

Property-based tests (fast-check)

  • Exponential backoff: delay(n+1) >= delay(n) for all n (monotone with no jitter)
  • Jitter bounds: applyJitter(d, 'full', rng) always in [0, d]
  • Clamp: clampDelay(d, min, max) always in [min, max]
  • Retry count: withRetry never exceeds maxAttempts calls to the wrapped function
  • Circuit breaker: after failureThreshold consecutive failures, state is 'open'

Benchmarks (mitata)

  • sleep(0) call overhead (baseline async cost)
  • retry with always-succeeding fn — 0 retry overhead
  • createExponentialBackoff delay computation — 1 000 iterations
  • CircuitBreaker.call closed-state hot path — 10 000 iterations

Impact

  • Adds one new package: packages/universal/retry
  • Adds one new spec capability: retry
  • No existing package consumers on genesis yet