3.8 KiB
@suaveplan/packer/retry — Retry on IO-backed source reads
(no submodules)
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 optionalwithRetryenvelope. Generic over the chunk payload type.- Default policy:
maxAttempts: 1(no retries). Backwards-compatible. AbortSignal-aware: cancellation aborts the loop mid-backoff and surfacesRetryAbortedErrorfrom@suaveplan/retry.- Generic
RetryableReader<T>interface — works against anyread()-shaped object (realReadableStreamDefaultReader, test double, third-party polyfill).
Usage: Basic Example
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
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 peropts.retryConfig.RetryReadOptions.retryConfig?: RetryConfig— full@suaveplan/retryconfig bundle (maxAttempts,delay,shouldRetry,clock,timers).RetryReadOptions.signal?: AbortSignal— cancellation signal. When fired, the retry loop aborts immediately and rejects withRetryAbortedError.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 passretryConfig— the retry layer is a single-attempt passthrough. withRetrycalls the wrapped function with its ownsignalparameter that races againstsignal.aborted; the inner function recheckssignal.abortedbefore delegating toreader.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 matchingread()promise shape composes. RealReadableStreamDefaultReader<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 ashouldRetrypredicate on theRetryConfig.