5.4 KiB
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:observabilitytslib→catalog:build
Dev deps (via catalog):
vitest,@vitest/coverage-istanbul→catalog:testingfast-check→catalog:testingmitata→catalog:testing
Preserved public API surface (from main branch)
Core retry
retry(fn, config)→Promise<RetryResult<T>>— core retry primitivewithRetry(fn, config)→Promise<T>— throws on exhaustionwithCircuitBreaker(fn, breaker)→Promise<T>— wraps fn with circuit breakerwithTimeout(fn, ms, signal?)→Promise<T>— wraps fn with deadlinesleep(ms, signal?)→Promise<void>— cancellable sleepcreateDelayFn(strategy)→DelayFunction— adapts a strategy to a delay function
Backoff strategies
createConstantBackoff(opts)→RetryStrategy— fixed delaycreateLinearBackoff(opts)→RetryStrategy— linearly increasing delaycreateExponentialBackoff(opts)→RetryStrategy— exponentially increasing delayapplyJitter(delay, mode, rng?)→number— full / equal / decorrelated jitterclampDelay(delay, min, max)→number
Circuit breaker
CircuitBreakerclass —.call(fn),.reset(),.stategetter, event emittersCircuitOpenErrorextendsSuaveplanErrorCircuitBreakerConfig—{ 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 forRetryStrategyJitterMode—'full' | 'equal' | 'decorrelated' | 'none'ConstantBackoffOptions,LinearBackoffOptions,ExponentialBackoffOptions
Error types
RetryExhaustedErrorextendsSuaveplanError—.attempts,.lastErrorAttemptTimeoutErrorextendsSuaveplanError—.attemptNumber,.timeoutMs
New additions for genesis
Structured errors
RetryErrorbase extendsSuaveplanErrorwith codes underRETRY.*namespace- Codes:
RETRY.EXHAUSTED,RETRY.ABORTED,RETRY.TIMEOUT,RETRY.CIRCUIT_OPEN RetryExhaustedError,AttemptTimeoutError,CircuitOpenErrorextendRetryError
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 computationCircuitBreakerConfig.clock?: Clock
AbortSignal throughout
sleep(ms, signal?)— resolves early on abortretry(fn, { signal })— cancels between attempts on abortCircuitBreaker.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:
withRetrynever exceedsmaxAttemptscalls to the wrapped function - Circuit breaker: after
failureThresholdconsecutive failures, state is'open'
Benchmarks (mitata)
sleep(0)call overhead (baseline async cost)retrywith always-succeeding fn — 0 retry overheadcreateExponentialBackoffdelay computation — 1 000 iterationsCircuitBreaker.callclosed-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