Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
# 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

View File

@@ -0,0 +1,57 @@
# 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)` — fixed `delay` ms regardless of attempt number.
- `createLinearBackoff(opts)` — `initial + (attempt - 1) * increment` ms.
- `createExponentialBackoff(opts)` — `initial * base^(attempt-1)` ms, capped at `max`.
- `applyJitter(delay, mode, rng?)` — modifies delay by `mode`: `'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/`.

View File

@@ -0,0 +1,88 @@
> Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/control-flow/retry.
# Tasks: enterprise-retry
## 1. Scaffold
- [x] 1.1 Create `packages/universal/retry/` with `package.json`, `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md` skeleton
- [x] 1.2 Register coverage thresholds at 100/100/100/100 in `vitest.config.ts`; provider = istanbul
- [x] 1.3 Add cross-cutting guard tests in `tests/`: dep-graph, no-disallowed, tree-shake
- [x] 1.4 Add package to root `CLAUDE.md`, `README.md`, and `STRUCTURE.yml`
## 2. Types + errors
- [x] 2.1 Implement `src/types/retry.types.ts` — `RetryConfig`, `RetryResult`, `RetryStrategy`, `DelayFunction`, `JitterMode`, `ConstantBackoffOptions`, `LinearBackoffOptions`, `ExponentialBackoffOptions`
- [x] 2.2 Implement `src/errors/retry-error.ts` — `RetryError` base extends `SuaveplanError`; `RetryExhaustedError`, `AttemptTimeoutError` extend `RetryError`; `defineCodes` for `RETRY.EXHAUSTED`, `RETRY.ABORTED`, `RETRY.TIMEOUT`
- [x] 2.3 Implement `src/circuit-breaker/circuit-breaker.types.ts` — `CircuitBreakerConfig`, `CircuitBreakerEventMap`, `CircuitState`; `CircuitOpenError` extends `RetryError` with `RETRY.CIRCUIT_OPEN` code
- [x] 2.4 Zod schemas: `RetryConfigSchema`, `ConstantBackoffOptionsSchema`, `LinearBackoffOptionsSchema`, `ExponentialBackoffOptionsSchema`, `CircuitBreakerConfigSchema`
- [x] 2.5 Tests: error construction, code narrowing, `.attempts` / `.lastError` / `.attemptNumber` / `.timeoutMs` fields
- [x] 2.6 Implement `src/error-codes.ts` and auto-register via side-effect import in `src/index.ts`
## 3. Backoff strategies
- [x] 3.1 `src/strategies/constant/constant.ts` — `createConstantBackoff(opts)` → `RetryStrategy`; `ConstantBackoffOptions`
- [x] 3.2 `src/strategies/linear/linear.ts` — `createLinearBackoff(opts)` → `RetryStrategy`; `LinearBackoffOptions`
- [x] 3.3 `src/strategies/exponential/exponential.ts` — `createExponentialBackoff(opts)` → `RetryStrategy`; `applyJitter(delay, mode, rng?)`, `clampDelay(delay, min, max)`; `ExponentialBackoffOptions`
- [x] 3.4 All strategies accept injectable `rng?: Rng` for deterministic jitter
- [x] 3.5 Unit tests: delay values at each attempt, clamping, jitter modes (full/equal/decorrelated/none)
- [x] 3.6 Property-based tests: exponential `delay(n+1) >= delay(n)` with no jitter; jitter in `[0, d]`; clamp always in `[min, max]`
- [x] 3.7 Co-located `.md` for each strategy
## 4. Core retry
- [x] 4.1 Implement `src/retry/retry.ts`:
- `sleep(ms, signal?)` — cancellable; resolves early on abort
- `retry(fn, config)` → `Promise<RetryResult<T>>`
- `withRetry(fn, config)` → `Promise<T>` — throws `RetryExhaustedError` on exhaustion, `RetryError` on abort
- `withTimeout(fn, ms, signal?)` → `Promise<T>` — throws `AttemptTimeoutError`
- `createDelayFn(strategy)` → `DelayFunction`
- `withCircuitBreaker(fn, breaker)` → `Promise<T>`
- [x] 4.2 `retry` uses injected `clock?: Clock` for sleep duration; `rng?: Rng` from config forwarded to strategy
- [x] 4.3 `retry` honours `AbortSignal` between attempts (checks before each delay and after each call)
- [x] 4.4 Unit tests: all config options, abort between attempts, timeout, exhaustion, `shouldRetry` predicate
- [x] 4.5 Property-based tests: `withRetry` never exceeds `maxAttempts` calls to fn
- [x] 4.6 Co-located `src/retry/retry.md`
## 5. Circuit breaker
- [x] 5.1 Implement `src/circuit-breaker/circuit-breaker.ts` — `CircuitBreaker` class
- State machine: `closed → open → half-open → closed` (or back to open)
- `.call(fn, signal?)` — throws `CircuitOpenError` when open
- `.reset()` — force back to closed
- `.state` getter — `CircuitState`
- Event emitter via `@suaveplan/events`: `stateChange` event with `CircuitBreakerEventMap`
- `halfOpenProbes` attempts before promoting to closed
- `timeout` window after which open resets to half-open (uses `clock?: Clock`)
- [x] 5.2 Unit tests: failure threshold transitions, half-open probing, success promotion, manual reset, signal passing, clock injection
- [x] 5.3 Property-based tests: after `failureThreshold` consecutive failures → state is `'open'`
- [x] 5.4 Co-located `src/circuit-breaker/circuit-breaker.md`
## 6. OTel instrumentation
- [x] 6.1 Counter `retry.attempt.total` by outcome label (success / failure / exhausted / aborted)
- [x] 6.2 Histogram `retry.attempt.delay_ms` by strategy label
- [x] 6.3 Counter `circuit_breaker.state_change.total` by from/to label
- [x] 6.4 Histogram `circuit_breaker.call.duration_ms`
- [x] 6.5 No-op without exporter (tested)
## 7. Benchmarks
- [x] 7.1 `src/__benchmarks__/sleep.bench.ts` — `sleep(0)` call overhead baseline (mitata)
- [x] 7.2 `src/__benchmarks__/retry.bench.ts` — `withRetry` with always-succeeding fn (0-retry overhead)
- [x] 7.3 `src/__benchmarks__/backoff.bench.ts` — `createExponentialBackoff` delay computation 1 000 iterations
- [x] 7.4 `src/__benchmarks__/circuit-breaker.bench.ts` — `CircuitBreaker.call` closed-state hot path 10 000 iterations
- [x] 7.5 Publish baseline numbers in README
## 8. Docs
- [x] 8.1 Co-located `.md` for every implementation file (≥200 words, examples, API reference)
- [x] 8.2 Rewrite `README.md` with public API, circuit breaker state diagram, strategy comparison table, consumer guide
- [x] 8.3 `bun run docs:check --filter=@suaveplan/retry` ≥ 90%
## 9. Verify and publish
- [x] 9.1 `bunx turbo lint typecheck test --filter=@suaveplan/retry` green
- [x] 9.2 `bunx turbo test:coverage --filter=@suaveplan/retry` at 100/100/100/100
- [x] 9.3 `bunx turbo build --filter=@suaveplan/retry` green
- [x] 9.4 `bun scripts/publish.ts --package @suaveplan/retry` to Verdaccio at 0.0.0
- [x] 9.5 Archive OpenSpec change to `openspec/archive/2026-04-18-enterprise-retry/`