Files
skills/docs/foundation/data/packer/src/retry/retryable-read.md
2026-05-29 08:20:56 +01:00

100 lines
3.8 KiB
Markdown

# `@suaveplan/packer/retry` — Retry on IO-backed source reads
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Wraps `ReadableStream` reads with the retry primitives from
`@suaveplan/retry` so transient IO failures (network blip, slow filesystem,
remote stream restart) don't fail an entire pack or unpack operation. This
module is the single funnel every production `reader.read()` call in
`@suaveplan/packer` flows through; `normalizeSource` and the abstract
`collect` helper both consume it. The audit line in `enterprise/packer.md`
(L47) mandates "**retry** primitives for IO-backed sources".
The helper is opt-in: when the caller does not pass a `retryConfig`, the
default policy is a single attempt with zero delay — wire behaviour
identical to a bare `reader.read()`. Callers who opt in get the configured
backoff / jitter / max-attempts strategy plus first-class `AbortSignal`
support: cancellation aborts the retry loop immediately.
## Features
- `retryRead(reader, opts)` — single-chunk read with optional `withRetry`
envelope. Generic over the chunk payload type.
- Default policy: `maxAttempts: 1` (no retries). Backwards-compatible.
- `AbortSignal`-aware: cancellation aborts the loop mid-backoff and
surfaces `RetryAbortedError` from `@suaveplan/retry`.
- Generic `RetryableReader<T>` interface — works against any
`read()`-shaped object (real `ReadableStreamDefaultReader`, test double,
third-party polyfill).
## Usage: Basic Example
```ts
import { retryRead } from "@suaveplan/packer/retry";
const stream = new ReadableStream<Uint8Array>(/* ... */);
const reader = stream.getReader();
const { value, done } = await retryRead(reader);
if (!done && value) {
/* consume the chunk */
}
```
## Usage: Advanced Example
```ts
import { retryRead } from "@suaveplan/packer/retry";
import { createExponentialBackoff } from "@suaveplan/retry";
const ac = new AbortController();
setTimeout(() => ac.abort(), 200);
const reader = remoteStream.getReader();
const result = await retryRead(reader, {
retryConfig: {
maxAttempts: 4,
delay: createExponentialBackoff({
initial: 25,
base: 2,
max: 1000,
jitter: "equal",
}),
},
signal: ac.signal,
});
```
## API Reference
- `retryRead<T = Uint8Array>(reader, opts?): Promise<{ value: T | undefined; done: boolean }>`
— performs one chunk read, optionally retrying per `opts.retryConfig`.
- `RetryReadOptions.retryConfig?: RetryConfig` — full `@suaveplan/retry`
config bundle (`maxAttempts`, `delay`, `shouldRetry`, `clock`, `timers`).
- `RetryReadOptions.signal?: AbortSignal` — cancellation signal. When
fired, the retry loop aborts immediately and rejects with
`RetryAbortedError`.
- `RetryableReader<T>` — `{ read(): Promise<{ value: T | undefined; done: boolean }> }`.
## Implementation Notes
- The default policy (`{ maxAttempts: 1, delay: () => 0 }`) keeps cluster
#1 byte-output identical for callers that don't pass `retryConfig` — the
retry layer is a single-attempt passthrough.
- `withRetry` calls the wrapped function with its own `signal` parameter
that races against `signal.aborted`; the inner function rechecks
`signal.aborted` before delegating to `reader.read()` so a signal that
fires between the retry loop's check and the next read still cancels
before any reader work happens.
- `RetryableReader<T>` is structural — anything with a matching `read()`
promise shape composes. Real `ReadableStreamDefaultReader<T>` satisfies
it directly; test doubles need only return the same `{ value, done }`
promise.
- The helper does not classify errors — every rejection is retried up to
`maxAttempts`. Callers needing fine-grained "retry only on network
error" classification supply a `shouldRetry` predicate on the
`RetryConfig`.