# `@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 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` 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(/* ... */); 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(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` — `{ 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` is structural — anything with a matching `read()` promise shape composes. Real `ReadableStreamDefaultReader` 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`.