Initial Commit
This commit is contained in:
99
packages/foundation/data/packer/src/retry/retryable-read.md
Normal file
99
packages/foundation/data/packer/src/retry/retryable-read.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# `@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`.
|
||||
Reference in New Issue
Block a user