first commit
This commit is contained in:
78
docs/foundation/flow/retry/README.md
Normal file
78
docs/foundation/flow/retry/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# @suaveplan/retry - Retry strategies with exponential/linear/constant backoff, jitter, circuit breaker with half-open probing, sleep and timeout helpers
|
||||
|
||||
## Summary
|
||||
|
||||
Retry strategies and circuit-breaker for async operations. The package provides constant, linear, and exponential backoff with four jitter modes (full, equal, decorrelated, none), a `sleep` helper with AbortSignal support, a per-attempt `withTimeout` wrapper, and a three-state circuit breaker (closed → open → half-open) with event emission and configurable failure/success thresholds. All entry points accept an injectable `Clock` and `AbortSignal` so they integrate cleanly with test harnesses and request cancellation.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [circuit-breaker](./src/circuit-breaker/circuit-breaker.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [circuit-breaker](./src/circuit-breaker/circuit-breaker.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [errors](./src/errors/retry-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [retry-error](./src/errors/retry-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [otel](./src/otel/retry-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [retry-otel](./src/otel/retry-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [retry](./src/retry/retry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [retry](./src/retry/retry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [constant](./src/strategies/constant/constant.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [constant](./src/strategies/constant/constant.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [exponential](./src/strategies/exponential/exponential.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [exponential](./src/strategies/exponential/exponential.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [linear](./src/strategies/linear/linear.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [linear](./src/strategies/linear/linear.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [circuit-breaker](./src/circuit-breaker/circuit-breaker.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/retry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry-error](./src/errors/retry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/retry-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry-otel](./src/otel/retry-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry](./src/retry/retry.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry](./src/retry/retry.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant](./src/strategies/constant/constant.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant](./src/strategies/constant/constant.md) — <!-- begin:auto-docs --> _(no submodules)_- [exponential](./src/strategies/exponential/exponential.md) — <!-- begin:auto-docs --> _(no submodules)_- [exponential](./src/strategies/exponential/exponential.md) — <!-- begin:auto-docs --> _(no submodules)_- [linear](./src/strategies/linear/linear.md) — <!-- begin:auto-docs --> _(no submodules)_- [linear](./src/strategies/linear/linear.md) — <!-- begin:auto-docs --> _(no submodules)_- [circuit-breaker](./src/circuit-breaker/circuit-breaker.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/retry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry-error](./src/errors/retry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/retry-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry-otel](./src/otel/retry-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry](./src/retry/retry.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry](./src/retry/retry.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant](./src/strategies/constant/constant.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant](./src/strategies/constant/constant.md) — <!-- begin:auto-docs --> _(no submodules)_- [exponential](./src/strategies/exponential/exponential.md) — <!-- begin:auto-docs --> _(no submodules)_- [exponential](./src/strategies/exponential/exponential.md) — <!-- begin:auto-docs --> _(no submodules)_- [linear](./src/strategies/linear/linear.md) — <!-- begin:auto-docs --> _(no submodules)_- [linear](./src/strategies/linear/linear.md) — <!-- begin:auto-docs --> _(no submodules)_- [circuit-breaker](./src/circuit-breaker/circuit-breaker.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/retry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry-error](./src/errors/retry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/retry-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry-otel](./src/otel/retry-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry](./src/retry/retry.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry](./src/retry/retry.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant](./src/strategies/constant/constant.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant](./src/strategies/constant/constant.md) — <!-- begin:auto-docs --> _(no submodules)_- [exponential](./src/strategies/exponential/exponential.md) — <!-- begin:auto-docs --> _(no submodules)_- [exponential](./src/strategies/exponential/exponential.md) — <!-- begin:auto-docs --> _(no submodules)_- [linear](./src/strategies/linear/linear.md) — <!-- begin:auto-docs --> _(no submodules)_- [linear](./src/strategies/linear/linear.md) — <!-- begin:auto-docs --> _(no submodules)_- [circuit-breaker](./src/circuit-breaker/circuit-breaker.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/retry-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [retry-error](./src/errors/retry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/retry-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [retry-otel](./src/otel/retry-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [retry](./src/retry/retry.md) — <!-- begin:auto-docs --> _(no submodules)_ - [retry](./src/retry/retry.md) — <!-- begin:auto-docs --> _(no submodules)_- [constant](./src/strategies/constant/constant.md) — <!-- begin:auto-docs --> _(no submodules)_ - [constant](./src/strategies/constant/constant.md) — <!-- begin:auto-docs --> _(no submodules)_- [exponential](./src/strategies/exponential/exponential.md) — <!-- begin:auto-docs --> _(no submodules)_ - [exponential](./src/strategies/exponential/exponential.md) — <!-- begin:auto-docs --> _(no submodules)_- [linear](./src/strategies/linear/linear.md) — <!-- begin:auto-docs --> _(no submodules)_ - [linear](./src/strategies/linear/linear.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Install
|
||||
|
||||
```sh
|
||||
bun add @suaveplan/retry
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import {
|
||||
withRetry,
|
||||
createExponentialBackoff,
|
||||
CircuitBreaker,
|
||||
} from "@suaveplan/retry";
|
||||
|
||||
// Retry with exponential backoff and full jitter — up to 5 attempts.
|
||||
const result = await withRetry(
|
||||
async (signal) => {
|
||||
const res = await fetch("https://api.example.com/data", { signal });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
},
|
||||
{
|
||||
maxAttempts: 5,
|
||||
delay: createExponentialBackoff({ initial: 100, max: 5000, jitter: "full" }),
|
||||
}
|
||||
);
|
||||
|
||||
// Wrap a sensitive downstream call in a circuit breaker.
|
||||
const breaker = new CircuitBreaker({
|
||||
failureThreshold: 3,
|
||||
successThreshold: 2,
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
breaker.on("open", ({ openedAt }) => console.warn("Circuit opened at", openedAt));
|
||||
|
||||
const value = await breaker.call(async (signal) => {
|
||||
return fetchDownstream(signal);
|
||||
});
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
| Module | Path |
|
||||
|--------|------|
|
||||
| `retry` — core loop, `retry`, `withRetry`, `sleep`, `withTimeout`, `createDelayFn` | [`src/retry/retry.md`](src/retry/retry.md) |
|
||||
| `constant` — `createConstantBackoff` | [`src/strategies/constant/constant.md`](src/strategies/constant/constant.md) |
|
||||
| `linear` — `createLinearBackoff` | [`src/strategies/linear/linear.md`](src/strategies/linear/linear.md) |
|
||||
| `exponential` — `createExponentialBackoff`, `applyJitter` | [`src/strategies/exponential/exponential.md`](src/strategies/exponential/exponential.md) |
|
||||
| `circuit-breaker` — `CircuitBreaker` class, `CircuitBreakerConfig`, event map | [`src/circuit-breaker/circuit-breaker.md`](src/circuit-breaker/circuit-breaker.md) |
|
||||
| `retry-error` — `RetryExhaustedError`, `RetryAbortedError`, `AttemptTimeoutError`, `CircuitOpenError` | [`src/errors/retry-error.md`](src/errors/retry-error.md) |
|
||||
| `retry-otel` — OpenTelemetry span/event helpers | [`src/otel/retry-otel.md`](src/otel/retry-otel.md) |
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# circuit-breaker
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `CircuitBreaker`, a state-machine implementation of the circuit-breaker pattern for protecting async functions against cascading failures. The circuit starts `closed` (normal operation) and opens after a configurable number of consecutive failures, temporarily blocking calls to give the underlying resource time to recover. After a timeout, the circuit enters `half-open` state to probe whether the resource has recovered, and closes again on sufficient consecutive successes.
|
||||
|
||||
State-change events are emitted via an internal `EventBus` from `@suaveplan/events`, enabling observability integrations, metrics, and alerting without polling.
|
||||
|
||||
## Features
|
||||
|
||||
- Three states: `closed`, `open`, `half-open` with deterministic transitions
|
||||
- Configurable `failureThreshold`, `successThreshold`, and `timeout` (open → half-open delay)
|
||||
- Optional `halfOpenProbes` limit to bound probe calls in half-open
|
||||
- Clock injection for deterministic testing and wall-time independence
|
||||
- `stateChange` event emitted on every transition via `EventBus`
|
||||
- `reset()` for forced closure (e.g., operator action or integration tests)
|
||||
- `AbortSignal` passed through to wrapped function
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { CircuitBreaker } from "@suaveplan/retry";
|
||||
|
||||
const breaker = new CircuitBreaker({
|
||||
failureThreshold: 5,
|
||||
successThreshold: 2,
|
||||
timeout: 30_000, // ms before half-open
|
||||
});
|
||||
|
||||
// Use breaker.call() instead of calling fn directly
|
||||
const data = await breaker.call((signal) => fetchData(url, { signal }));
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { CircuitBreaker, withRetry, createExponentialBackoff } from "@suaveplan/retry";
|
||||
|
||||
const breaker = new CircuitBreaker({
|
||||
failureThreshold: 3,
|
||||
successThreshold: 2,
|
||||
timeout: 10_000,
|
||||
clock: () => performance.now(),
|
||||
});
|
||||
|
||||
// Subscribe to state changes for metrics
|
||||
breaker.on("stateChange", ({ from, to }) => {
|
||||
metrics.increment("circuit_breaker.transition", { from, to });
|
||||
});
|
||||
|
||||
// Combine with retry — circuit rejects fast while open
|
||||
const delay = createExponentialBackoff({ initial: 200, max: 5_000 });
|
||||
|
||||
const result = await withRetry(
|
||||
(signal) => breaker.call(() => callDownstreamService(signal)),
|
||||
{
|
||||
maxAttempts: 6,
|
||||
delay,
|
||||
shouldRetry: (err) => !(err instanceof CircuitOpenError),
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new CircuitBreaker(config)`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `config.failureThreshold` | `number` | Consecutive failures to open the circuit. |
|
||||
| `config.successThreshold` | `number` | Consecutive successes in half-open to close. |
|
||||
| `config.timeout` | `number` | Ms before open transitions to half-open. |
|
||||
| `config.halfOpenProbes` | `number?` | Max probes in half-open (default: `successThreshold`). |
|
||||
| `config.clock` | `Clock?` | Time source. Defaults to `Date.now`. |
|
||||
|
||||
### `breaker.state`
|
||||
|
||||
`CircuitState` — one of `"closed"`, `"open"`, `"half-open"`.
|
||||
|
||||
### `breaker.on(event, handler)`
|
||||
|
||||
Subscribe to `"stateChange"` events. Returns an unsubscribe function.
|
||||
|
||||
### `breaker.call(fn, signal?)`
|
||||
|
||||
Execute `fn` through the circuit. Throws `CircuitOpenError` if open.
|
||||
|
||||
### `breaker.reset()`
|
||||
|
||||
Force circuit to `closed` and reset all counters. Emits `stateChange` unless already closed.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `checkHalfOpen` method is called lazily at the start of every `call()` — this avoids background timers and keeps the circuit breaker self-contained. The `openedAt` timestamp is set using the injected `clock` so deterministic tests can advance time by mutating the clock reference rather than using real timers. The `EventBus` is internal and not exposed as a public property to prevent external mutation. Listeners receive typed payloads without requiring any casting.
|
||||
115
docs/foundation/flow/retry/src/errors/retry-error.md
Normal file
115
docs/foundation/flow/retry/src/errors/retry-error.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# retry-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the error hierarchy for the `@suaveplan/retry` package. Every exception thrown by the retry engine, circuit breaker, or timeout wrapper is an instance of one of these classes, all of which extend `SuaveplanError` from `@suaveplan/error`. This gives callers a stable `code` property for programmatic branching without parsing error messages.
|
||||
|
||||
The `RETRY_CODES` registry is produced by `defineCodes("RETRY", [...])` and maps short reason keys to fully-qualified dot-separated strings (`RETRY.EXHAUSTED`, `RETRY.ABORTED`, `RETRY.TIMEOUT`, `RETRY.CIRCUIT_OPEN`). All codes are frozen and tree-shaken into the host bundle only when referenced.
|
||||
|
||||
## Features
|
||||
|
||||
- `RETRY_CODES` — frozen code registry with four stable codes
|
||||
- `RetryError` — abstract base; narrows `SuaveplanError<RetryCode>`
|
||||
- `RetryExhaustedError` — preserves `attempts` count and `lastError` reference
|
||||
- `RetryAbortedError` — signals graceful cancellation via `AbortSignal`
|
||||
- `AttemptTimeoutError` — carries `attemptNumber` and `timeoutMs` for observability
|
||||
- `CircuitOpenError` — carries optional `openedAt` timestamp
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { RetryExhaustedError, RETRY_CODES } from "@suaveplan/retry";
|
||||
|
||||
try {
|
||||
await withRetry(fn, { maxAttempts: 3, delay: () => 100 });
|
||||
} catch (err) {
|
||||
if (err instanceof RetryExhaustedError) {
|
||||
console.error(`Failed after ${err.attempts} attempts`, err.lastError);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import {
|
||||
RetryAbortedError,
|
||||
AttemptTimeoutError,
|
||||
CircuitOpenError,
|
||||
RETRY_CODES,
|
||||
} from "@suaveplan/retry";
|
||||
|
||||
function handleRetryError(err: unknown): never {
|
||||
if (err instanceof AttemptTimeoutError) {
|
||||
metrics.record("retry.timeout", { attempt: err.attemptNumber });
|
||||
} else if (err instanceof CircuitOpenError) {
|
||||
metrics.record("circuit.rejected");
|
||||
}
|
||||
// Use code for switch-based routing
|
||||
if (err instanceof RetryAbortedError) {
|
||||
// err.code === "RETRY.ABORTED"
|
||||
throw err; // re-throw for caller to handle
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `RETRY_CODES`
|
||||
|
||||
```typescript
|
||||
const RETRY_CODES: {
|
||||
readonly EXHAUSTED: "RETRY.EXHAUSTED";
|
||||
readonly ABORTED: "RETRY.ABORTED";
|
||||
readonly TIMEOUT: "RETRY.TIMEOUT";
|
||||
readonly CIRCUIT_OPEN: "RETRY.CIRCUIT_OPEN";
|
||||
}
|
||||
```
|
||||
|
||||
Frozen code registry. Use `err.code === RETRY_CODES.EXHAUSTED` for type-safe comparisons.
|
||||
|
||||
### `RetryError`
|
||||
|
||||
Abstract base. Extends `SuaveplanError<RetryCode>`. All retry-package errors are `instanceof RetryError`.
|
||||
|
||||
### `RetryExhaustedError`
|
||||
|
||||
```typescript
|
||||
constructor(opts: { attempts: number; lastError: unknown; message?: string })
|
||||
```
|
||||
|
||||
- `attempts` — total number of attempts made before giving up
|
||||
- `lastError` — the exception thrown on the final attempt
|
||||
|
||||
### `RetryAbortedError`
|
||||
|
||||
```typescript
|
||||
constructor(opts?: { cause?: unknown })
|
||||
```
|
||||
|
||||
Thrown when the `AbortSignal` fires between attempts.
|
||||
|
||||
### `AttemptTimeoutError`
|
||||
|
||||
```typescript
|
||||
constructor(opts: { attemptNumber: number; timeoutMs: number })
|
||||
```
|
||||
|
||||
- `attemptNumber` — which attempt timed out (1-based)
|
||||
- `timeoutMs` — the budget that was exceeded
|
||||
|
||||
### `CircuitOpenError`
|
||||
|
||||
```typescript
|
||||
constructor(opts?: { openedAt?: number })
|
||||
```
|
||||
|
||||
- `openedAt` — optional timestamp (ms) when the circuit opened
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`RetryError` is abstract so it cannot be constructed directly, forcing callers to use one of the concrete subclasses. Each concrete class hardcodes its own `RETRY_CODES.*` value, preventing mismatches between code and class type. The `cause` property of `RetryExhaustedError` is only set when `lastError` is an `Error` instance to avoid confusing the `cause` chain with non-Error values (which are stored separately on `lastError`). All classes pass `context` through to `SuaveplanError` so OTel span-event adapters can extract structured metadata without parsing messages.
|
||||
95
docs/foundation/flow/retry/src/otel/retry-otel.md
Normal file
95
docs/foundation/flow/retry/src/otel/retry-otel.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# retry-otel
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides OpenTelemetry instrumentation helpers for `@suaveplan/retry`. This module defines the `RetryMetrics` interface and the `createRetryMetrics` factory that creates all four instruments from a caller-supplied OTel `Meter`. Instrument creation is lazy — no metrics are registered until `createRetryMetrics` is called — and the instruments are decoupled from the circuit breaker and retry engine, allowing callers to integrate metrics at the call site without modifying core retry logic.
|
||||
|
||||
The design follows the OTel semantic conventions for reliability metrics: counters track event totals, histograms capture distributions that matter for latency and load analysis.
|
||||
|
||||
## Features
|
||||
|
||||
- `createRetryMetrics(meter)` — creates all four instruments at once
|
||||
- `recordAttempt` — increments `retry.attempt.total` with `outcome` and `strategy` attributes
|
||||
- `recordDelay` — records to `retry.attempt.delay_ms` histogram with `strategy` attribute
|
||||
- `recordCircuitStateChange` — increments `circuit_breaker.state_change.total` with `from`/`to` attributes
|
||||
- `recordCircuitCall` — records to `circuit_breaker.call.duration_ms` histogram with `outcome` attribute
|
||||
- Fully tree-shakeable — each helper is a standalone function
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import { createRetryMetrics, recordAttempt } from "@suaveplan/retry";
|
||||
|
||||
const meter = metrics.getMeter("my-service");
|
||||
const retryMetrics = createRetryMetrics(meter);
|
||||
|
||||
// After each attempt:
|
||||
recordAttempt(retryMetrics, { outcome: "failure", strategy: "exponential" });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import {
|
||||
createRetryMetrics,
|
||||
recordAttempt,
|
||||
recordDelay,
|
||||
recordCircuitStateChange,
|
||||
recordCircuitCall,
|
||||
CircuitBreaker,
|
||||
} from "@suaveplan/retry";
|
||||
|
||||
const meter = metrics.getMeter("payment-service");
|
||||
const m = createRetryMetrics(meter);
|
||||
|
||||
const breaker = new CircuitBreaker({ failureThreshold: 5, successThreshold: 2, timeout: 10_000 });
|
||||
breaker.on("stateChange", ({ from, to }) => recordCircuitStateChange(m, from, to));
|
||||
|
||||
// Instrument a call manually:
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result = await breaker.call(() => chargeCard(payload));
|
||||
recordCircuitCall(m, Date.now() - start, "success");
|
||||
recordAttempt(m, { outcome: "success", strategy: "circuit-breaker" });
|
||||
return result;
|
||||
} catch (err) {
|
||||
recordCircuitCall(m, Date.now() - start, "failure");
|
||||
recordAttempt(m, { outcome: "failure", strategy: "circuit-breaker" });
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createRetryMetrics(meter)`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `meter` | `Meter` | OTel Meter from `@opentelemetry/api`. |
|
||||
|
||||
**Returns:** `RetryMetrics` — `{ attemptTotal, attemptDelayMs, circuitStateChangeTotal, circuitCallDurationMs }`.
|
||||
|
||||
### `recordAttempt(metrics, opts)`
|
||||
|
||||
Increments `retry.attempt.total` by 1. Attributes: `outcome` (`"success"` \| `"failure"`), `strategy` (string, default `"unknown"`).
|
||||
|
||||
### `recordDelay(metrics, delayMs, strategy?)`
|
||||
|
||||
Records `delayMs` to `retry.attempt.delay_ms`. Attribute: `strategy`.
|
||||
|
||||
### `recordCircuitStateChange(metrics, from, to)`
|
||||
|
||||
Increments `circuit_breaker.state_change.total` by 1. Attributes: `from`, `to` (both `CircuitState`).
|
||||
|
||||
### `recordCircuitCall(metrics, durationMs, outcome)`
|
||||
|
||||
Records `durationMs` to `circuit_breaker.call.duration_ms`. Attribute: `outcome`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All helper functions accept `RetryMetrics` as their first parameter rather than using module-level singletons, making them safe for multi-tenant applications that may configure multiple meters. The functions do not set up any automatic instrumentation — callers instrument their retry loops explicitly. This is intentional: wrapping `retry()` internally with OTel would couple the core engine to the OTel API and add mandatory overhead even when metrics are not needed. The `strategy` attribute defaults to `"unknown"` when not provided to ensure all recorded events have a value for that dimension, preventing sparse attribute cardinality issues in OTel backends.
|
||||
119
docs/foundation/flow/retry/src/retry/retry.md
Normal file
119
docs/foundation/flow/retry/src/retry/retry.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# retry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides the core retry execution engine for `@suaveplan/retry`. This module exports `sleep`, `createDelayFn`, `withTimeout`, `retry`, and `withRetry` — the five primitives that power all retry-loop functionality in the package. The design prioritises correctness under cancellation: every function checks `AbortSignal` at the right moments to avoid wasted work after a caller has abandoned the operation.
|
||||
|
||||
The `retry` function is the heart of the package. It runs a user-supplied async function up to `maxAttempts` times, applies the configured delay strategy between failures, respects the abort signal, and optionally consults a `shouldRetry` predicate to stop early. It returns a rich `RetryResult` object rather than just the value, so callers can observe how many attempts were needed and how long the total operation took.
|
||||
|
||||
## Features
|
||||
|
||||
- `sleep(ms, signal?, timers?)` — cancellable delay with immediate rejection on pre-aborted signals
|
||||
- `createDelayFn(strategy)` — identity adapter for explicit typing in composition chains
|
||||
- `withTimeout(fn, ms, attempt, signal?, timers?)` — per-attempt deadline with `AttemptTimeoutError`
|
||||
- `retry(fn, config)` — full retry loop with abort, jitter, and predicate support
|
||||
- `withRetry(fn, config)` — simplified wrapper that returns the resolved value directly
|
||||
- Clock injection for deterministic elapsed-time measurement
|
||||
- `TimersLike` injection for deterministic sleep + timeout under `FakeClock`-driven tests
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { withRetry, createExponentialBackoff } from "@suaveplan/retry";
|
||||
|
||||
const delay = createExponentialBackoff({ initial: 100, max: 5_000, jitter: "full" });
|
||||
|
||||
const user = await withRetry(
|
||||
(signal) => fetchUser(userId, { signal }),
|
||||
{ maxAttempts: 5, delay },
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { retry, withTimeout, createLinearBackoff, RetryExhaustedError } from "@suaveplan/retry";
|
||||
|
||||
const controller = new AbortController();
|
||||
const delay = createLinearBackoff({ initial: 200, increment: 100, max: 2_000 });
|
||||
|
||||
const result = await retry(
|
||||
(signal) => withTimeout(processJob, 3_000, 1, signal),
|
||||
{
|
||||
maxAttempts: 8,
|
||||
delay,
|
||||
shouldRetry: (err) => !(err instanceof FatalError),
|
||||
signal: controller.signal,
|
||||
clock: () => performance.now(),
|
||||
},
|
||||
);
|
||||
|
||||
console.log(`Succeeded in ${result.attempts} attempt(s), ${result.totalMs}ms total`);
|
||||
```
|
||||
|
||||
### Usage: Deterministic tests with FakeClock
|
||||
|
||||
```typescript
|
||||
import type { TimersLike } from "@suaveplan/types";
|
||||
import { retry, createExponentialBackoff } from "@suaveplan/retry";
|
||||
|
||||
// FakeClock-backed timer pair — see @suaveplan/testing for a canned impl.
|
||||
const fakeTimers: TimersLike = makeFakeTimers(state);
|
||||
const delay = createExponentialBackoff({ initial: 1_000, base: 2 });
|
||||
|
||||
const promise = retry(handler, {
|
||||
maxAttempts: 5,
|
||||
delay,
|
||||
timers: fakeTimers,
|
||||
});
|
||||
// Advance the fake clock past every inter-attempt sleep. The retry loop
|
||||
// runs in zero real wall-clock time.
|
||||
advance(20_000);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `sleep(ms, signal?, timers?)`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `ms` | `number` | Duration to sleep (ms). |
|
||||
| `signal` | `AbortSignal?` | Cancellation signal. |
|
||||
| `timers` | `TimersLike?` | Injected timer pair. Defaults to platform `setTimeout`/`clearTimeout`. |
|
||||
|
||||
**Returns:** `Promise<void>`. Rejects with `DOMException("Aborted")` if signal fires.
|
||||
|
||||
### `createDelayFn(strategy)`
|
||||
|
||||
Returns `strategy` typed as `DelayFunction`. No-op wrapper for ergonomic composition.
|
||||
|
||||
### `withTimeout(fn, ms, attemptNumber, signal?, timers?)`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `fn` | `(signal?) => Promise<T>` | Function to race against deadline. |
|
||||
| `ms` | `number` | Time budget (ms). |
|
||||
| `attemptNumber` | `number` | 1-based attempt number (for error context). |
|
||||
| `signal` | `AbortSignal?` | Passed through to `fn`. |
|
||||
| `timers` | `TimersLike?` | Injected timer pair. Defaults to platform `setTimeout`/`clearTimeout`. |
|
||||
|
||||
**Throws:** `AttemptTimeoutError` if `ms` elapses before `fn` resolves.
|
||||
|
||||
### `retry(fn, config)`
|
||||
|
||||
Runs `fn` up to `config.maxAttempts` times. Returns `RetryResult<T>` on success.
|
||||
|
||||
`config.timers` (optional `TimersLike`) drives the inter-attempt sleep when supplied — pass a `FakeClock`-backed pair to make the entire loop deterministic in tests. When omitted, the loop falls back to `globalThis.setTimeout` / `clearTimeout`.
|
||||
|
||||
**Throws:** `RetryExhaustedError` on exhaustion, `RetryAbortedError` on abort.
|
||||
|
||||
### `withRetry(fn, config)`
|
||||
|
||||
Like `retry` but returns `T` directly. Throws the same errors.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The abort-signal check happens both before each attempt and before each sleep. This means a signal fired mid-sleep aborts the sleep immediately (via the `addEventListener` in `sleep`), while a signal fired after a successful `fn` call is noticed at the top of the next loop iteration. The `shouldRetry` predicate is consulted after each failure and can stop retrying by returning `false` — in this case `RetryExhaustedError` is thrown immediately with the current attempt count rather than waiting for `maxAttempts`. The clock injection in `RetryConfig` is used purely for elapsed-time measurement in `RetryResult.totalMs`; it has no effect on actual sleep durations.
|
||||
@@ -0,0 +1,63 @@
|
||||
# constant
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `createConstantBackoff`, a factory that creates a retry strategy returning a fixed, unchanging delay for every attempt. Constant backoff is the simplest possible strategy: every retry waits the same number of milliseconds before the next call. This is useful when the failure mode does not benefit from increasing delays (e.g., transient network hiccups with known recovery windows) and when predictable timing is more important than reducing thundering-herd pressure.
|
||||
|
||||
The returned function satisfies the `RetryStrategy` interface (`(attempt: number) => number`) and can be passed directly to `RetryConfig.delay` or any helper that accepts a strategy.
|
||||
|
||||
## Features
|
||||
|
||||
- Zero allocation per call — closure captures delay once at factory time
|
||||
- Deterministic: no randomness, no clock dependency
|
||||
- Composable with `applyJitter` for external jitter if needed
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { createConstantBackoff } from "@suaveplan/retry";
|
||||
import { withRetry } from "@suaveplan/retry";
|
||||
|
||||
const strategy = createConstantBackoff({ delay: 200 });
|
||||
|
||||
const result = await withRetry(fetchUser, {
|
||||
maxAttempts: 5,
|
||||
delay: strategy,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { createConstantBackoff, applyJitter } from "@suaveplan/retry";
|
||||
|
||||
const base = createConstantBackoff({ delay: 500 });
|
||||
|
||||
// Wrap with full jitter externally to spread retries under load
|
||||
const withJitter = (attempt: number) => applyJitter(base(attempt), "full");
|
||||
|
||||
await withRetry(sendEmail, {
|
||||
maxAttempts: 10,
|
||||
delay: withJitter,
|
||||
shouldRetry: (err) => isTransient(err),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createConstantBackoff(opts)`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `opts.delay` | `number` | Fixed delay in milliseconds returned for every attempt. |
|
||||
|
||||
**Returns:** `RetryStrategy` — `(attempt: number) => number`. The `attempt` argument is ignored.
|
||||
|
||||
**Throws:** Nothing.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The factory captures `delay` in a closure at construction time rather than reading from `opts` on each call, which avoids one property lookup per retry. The returned arrow function is a single expression with no branching, making it trivially inlineable by JIT compilers. For scenarios where the delay needs to vary per environment, the recommended pattern is to construct a new strategy at startup with the environment-configured value rather than mutating options at runtime.
|
||||
@@ -0,0 +1,92 @@
|
||||
# exponential
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `createExponentialBackoff`, `applyJitter`, and `clampDelay` — the three core building blocks for exponential backoff with optional jitter. Exponential backoff is the industry-standard strategy for retrying failed calls against overloaded or degraded services: each subsequent delay doubles (or grows by configurable base), reducing collision probability under high concurrency far better than constant or linear strategies.
|
||||
|
||||
Jitter breaks the synchronization problem that arises when many clients retry at identical intervals, which can cause "thundering herd" effects that worsen rather than relieve overloaded services. This module supports four jitter modes aligned with the AWS/Google SRE recommendations: `full`, `equal`, `decorrelated`, and `none`.
|
||||
|
||||
## Features
|
||||
|
||||
- Configurable `initial` delay, `base` exponent (default 2), and `max` cap
|
||||
- Four jitter modes: `full`, `equal`, `decorrelated`, `none`
|
||||
- Pluggable `Rng` for deterministic testing — falls back to `Math.random()`
|
||||
- `applyJitter` usable independently on any delay value
|
||||
- `clampDelay` utility for explicit range enforcement
|
||||
- All functions are pure (no side effects beyond rng call)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { createExponentialBackoff, withRetry } from "@suaveplan/retry";
|
||||
|
||||
const strategy = createExponentialBackoff({ initial: 100, max: 30_000, jitter: "full" });
|
||||
|
||||
// attempt 1 → ~0–100ms, attempt 2 → ~0–200ms, etc.
|
||||
await withRetry(fetchQuote, { maxAttempts: 6, delay: strategy });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { createExponentialBackoff, applyJitter, clampDelay } from "@suaveplan/retry";
|
||||
|
||||
// Seeded RNG for deterministic tests
|
||||
const rng = { nextUint32: () => 2147483648, nextFloat: () => 0.5 };
|
||||
|
||||
const strategy = createExponentialBackoff(
|
||||
{ initial: 50, base: 3, max: 10_000, jitter: "equal" },
|
||||
rng,
|
||||
);
|
||||
|
||||
// Manual composition: clamp after jitter for extra safety
|
||||
const bounded = (attempt: number) => clampDelay(strategy(attempt), 50, 5_000);
|
||||
|
||||
await withRetry(writeRecord, {
|
||||
maxAttempts: 8,
|
||||
delay: bounded,
|
||||
shouldRetry: (err) => isTransient(err),
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createExponentialBackoff(opts, rng?)`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `opts.initial` | `number` | Base delay for attempt 1 (ms). |
|
||||
| `opts.base` | `number?` | Exponent base. Default: `2`. |
|
||||
| `opts.max` | `number?` | Maximum delay cap (ms). |
|
||||
| `opts.jitter` | `JitterMode?` | Jitter mode. Default: `"none"`. |
|
||||
| `rng` | `Rng?` | Random source. Falls back to `Math.random()`. |
|
||||
|
||||
**Returns:** `RetryStrategy` — `(attempt: number) => number`.
|
||||
|
||||
### `applyJitter(delay, mode, rng?)`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `delay` | `number` | Base delay to apply jitter to (ms). |
|
||||
| `mode` | `JitterMode` | One of `"full"`, `"equal"`, `"decorrelated"`, `"none"`. |
|
||||
| `rng` | `Rng?` | Random source. Falls back to `Math.random()`. |
|
||||
|
||||
**Returns:** `number` — jitter-adjusted delay.
|
||||
|
||||
### `clampDelay(delay, min, max)`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `delay` | `number` | Unclamped delay (ms). |
|
||||
| `min` | `number` | Inclusive lower bound. |
|
||||
| `max` | `number` | Inclusive upper bound. |
|
||||
|
||||
**Returns:** `number` — `Math.max(min, Math.min(max, delay))`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `createExponentialBackoff` factory captures all configuration in a closure and calls `applyJitter` internally if a jitter mode other than `"none"` is configured. This ensures the correct `rng` is threaded through without callers needing to pass it on every call. The `"decorrelated"` jitter mode is a simplified variant that maps `delay` to `[delay, 2*delay]` — the classic AWS decorrelated formula uses `rand(delay/2, prev*3)` but requires external state; this simplified form achieves similar dispersion without statefulness. `clampDelay` is intentionally a standalone function rather than part of the strategy to keep concerns separated and allow explicit composition.
|
||||
67
docs/foundation/flow/retry/src/strategies/linear/linear.md
Normal file
67
docs/foundation/flow/retry/src/strategies/linear/linear.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# linear
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `createLinearBackoff`, a factory that creates a retry strategy with delays that increase linearly by a fixed increment for each subsequent attempt. Linear backoff is a good middle ground between constant backoff (no increase) and exponential backoff (rapid growth). It is particularly appropriate when the underlying resource is recovering at a predictable rate, such as a downstream service restarting after a deployment, where you expect recovery within a bounded window.
|
||||
|
||||
The formula is `delay(attempt) = initial + (attempt - 1) * increment`, optionally capped at `max`. Attempt numbers are 1-based: attempt 1 returns `initial`, attempt 2 returns `initial + increment`, and so on.
|
||||
|
||||
## Features
|
||||
|
||||
- Configurable `initial` delay (ms) — delay applied on attempt 1
|
||||
- Configurable `increment` (ms) — added per retry step
|
||||
- Optional `max` cap — prevents unbounded delay growth
|
||||
- No randomness — deterministic delays for predictable retry budgets
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { createLinearBackoff, withRetry } from "@suaveplan/retry";
|
||||
|
||||
const strategy = createLinearBackoff({ initial: 100, increment: 50, max: 500 });
|
||||
|
||||
// attempt 1 → 100ms, attempt 2 → 150ms, attempt 3 → 200ms, ... capped at 500ms
|
||||
const result = await withRetry(fetchUser, {
|
||||
maxAttempts: 5,
|
||||
delay: strategy,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { createLinearBackoff, applyJitter, withRetry } from "@suaveplan/retry";
|
||||
|
||||
const base = createLinearBackoff({ initial: 200, increment: 100 });
|
||||
|
||||
// Add equal jitter to spread load during recoveries
|
||||
const jittered = (attempt: number) => applyJitter(base(attempt), "equal");
|
||||
|
||||
await withRetry(processJob, {
|
||||
maxAttempts: 8,
|
||||
delay: jittered,
|
||||
shouldRetry: (err, attempt) => attempt < 4 || isRetriableError(err),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createLinearBackoff(opts)`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `opts.initial` | `number` | Delay for attempt 1 (ms). |
|
||||
| `opts.increment` | `number` | Amount added to delay per retry step (ms). |
|
||||
| `opts.max` | `number?` | Optional upper bound on delay (ms). |
|
||||
|
||||
**Returns:** `RetryStrategy` — `(attempt: number) => number`.
|
||||
|
||||
**Throws:** Nothing.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The closure captures `initial`, `increment`, and `max` at construction time for efficiency. The `max` cap uses `Math.min` so the comparison only runs when `max` is defined — the factory uses `opts.max !== undefined` rather than a truthiness check to correctly handle `max: 0`. For values where `increment` is 0, the strategy degenerates to constant backoff. Callers who need monotonically increasing delays should ensure `increment > 0`.
|
||||
75
docs/foundation/flow/retry/src/types/retry.types.md
Normal file
75
docs/foundation/flow/retry/src/types/retry.types.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# retry.types
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared type definitions for the `@suaveplan/retry` package. These interfaces describe the strategy function shape, the per-strategy options bags (constant, linear, exponential), and the top-level retry-loop configuration, plus the `RetryResult<T>` envelope returned by every successful run. Centralising the contracts here keeps the backoff builders, jitter helpers, and loop driver all speaking the same language.
|
||||
|
||||
## Features
|
||||
|
||||
- Zero runtime cost — all exports are pure TypeScript types
|
||||
- `RetryStrategy` — a pure function `(attempt: number) => number` that maps attempt count to delay; every backoff factory in the package returns one
|
||||
- `JitterMode` union covering the four supported jitter flavours: `full`, `equal`, `decorrelated`, `none`
|
||||
- Separate options interfaces per backoff flavour (`ConstantBackoffOptions`, `LinearBackoffOptions`, `ExponentialBackoffOptions`) so factories get named, checked parameters
|
||||
- `RetryConfig<T>` pulls together strategy, max-attempts, predicate, `AbortSignal`, injected `Clock`, and injected `Rng` (from `@suaveplan/types`)
|
||||
- `RetryResult<T>` carries the resolved value, total attempts, and cumulative wall-clock time for telemetry
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { RetryConfig, RetryStrategy } from "@suaveplan/retry/types/retry.types";
|
||||
|
||||
const constant: RetryStrategy = () => 100;
|
||||
|
||||
const config: RetryConfig<string> = {
|
||||
maxAttempts: 5,
|
||||
delay: constant,
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
ExponentialBackoffOptions,
|
||||
RetryConfig,
|
||||
RetryResult,
|
||||
} from "@suaveplan/retry/types/retry.types";
|
||||
|
||||
const options: ExponentialBackoffOptions = {
|
||||
initial: 200,
|
||||
base: 2,
|
||||
max: 10_000,
|
||||
jitter: "decorrelated",
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
const config: RetryConfig<number> = {
|
||||
maxAttempts: 7,
|
||||
delay: (attempt) => Math.min(options.max ?? Infinity, options.initial * 2 ** (attempt - 1)),
|
||||
shouldRetry: (err) => !(err instanceof TypeError),
|
||||
signal: controller.signal,
|
||||
clock: () => performance.now(),
|
||||
rng: () => Math.random(),
|
||||
};
|
||||
|
||||
declare function retryLoop<T>(fn: () => Promise<T>, cfg: RetryConfig<T>): Promise<RetryResult<T>>;
|
||||
|
||||
const result = await retryLoop(async () => 42, config);
|
||||
console.log(result.value, result.attempts, result.totalMs);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `JitterMode` — `"full" | "equal" | "decorrelated" | "none"`.
|
||||
- `RetryStrategy` — `(attempt: number) => number` delay-in-ms function.
|
||||
- `DelayFunction` — alias for `RetryStrategy`.
|
||||
- `ConstantBackoffOptions` — `{ delay }`.
|
||||
- `LinearBackoffOptions` — `{ initial, increment, max? }`.
|
||||
- `ExponentialBackoffOptions` — `{ initial, base?, max?, jitter? }`.
|
||||
- `RetryConfig<T>` — top-level loop config.
|
||||
- `RetryResult<T>` — `{ value, attempts, totalMs }` success envelope.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `RetryStrategy` shape is deliberately a plain function rather than an object with `next()` / `reset()` methods so callers can compose strategies with ordinary function composition, and so the factory functions (`constantBackoff`, `linearBackoff`, `exponentialBackoff`) can return closures rather than classes. The `Rng` injection from `@suaveplan/types` lets the decorrelated-jitter variant be driven by a deterministic seeded RNG in property-based tests while defaulting to `Math.random` in production code.
|
||||
Reference in New Issue
Block a user