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

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 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

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 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.