# `retry-helper` _(no submodules)_ ## Purpose Wraps `@suaveplan/retry.retry` with the transient-error classifier specific to filesystem operations and the error-mapping policy specific to this package. Every public `NodeFsStorage` operation that performs IO routes through `withFsRetry` so the retry policy lives in one place. Tests can drive the retry loop deterministically by injecting a `TimersLike` pair (typically the FakeClock-backed pair from `@suaveplan/testing`). ## Features - Classifies `EBUSY`/`EMFILE`/`ENFILE`/`EAGAIN` as transient; every other errno is fatal. - Maps `EACCES`/`EPERM` to `STORAGE_FILE.PERMISSION_DENIED` after exhausting retries. - Maps any other terminal error to `STORAGE_FILE.IO_FAILED` and preserves `cause`. - Lets a previously-thrown `StorageFileError` bubble through unwrapped so error codes don't get clobbered. - Defaults to `globalThis.setTimeout`/`clearTimeout` but accepts an injectable `TimersLike` pair for deterministic timing. ## Usage: Basic Example ```ts import { withFsRetry } from "@suaveplan/storage-file"; const data = await withFsRetry(() => fs.readFile("/tmp/store/k.data")); ``` ## Usage: Advanced Example ```ts import { withFsRetry, type WithFsRetryOptions } from "@suaveplan/storage-file"; import { fakeClock } from "@suaveplan/testing/runner"; const fc = fakeClock(); const options: WithFsRetryOptions = { retry: { maxAttempts: 5, baseDelayMs: 10, maxDelayMs: 200 }, timers: { setTimer: fc.setTimeout, clearTimer: fc.clearTimeout }, signal: controller.signal, key: "k", }; await withFsRetry(async () => { throw Object.assign(new Error("EBUSY"), { code: "EBUSY" }); }, options).catch((err) => { // → StorageFileError(STORAGE_FILE.IO_FAILED, cause: { code: "EBUSY" }) }); ``` ## API Reference - `withFsRetry(fn, options?): Promise` — runs `fn` with retry on transient fs errors. Throws `StorageFileError` with `IO_FAILED` or `PERMISSION_DENIED` after exhaustion. - `WithFsRetryOptions` — `{ retry?, timers?, signal?, key? }`. `retry.maxAttempts` default 3, `retry.baseDelayMs` default 50, `retry.maxDelayMs` default 5000. - `isTransientFsError(err): boolean` — true when `err.code` is one of `EBUSY`/`EMFILE`/`ENFILE`/`EAGAIN`. - `isPermissionFsError(err): boolean` — true when `err.code` is `EACCES` or `EPERM`. ## Implementation Notes - The exponential backoff is built via `createExponentialBackoff` from `@suaveplan/retry`. The jitter policy is the package default (equal jitter). - `signal` is forwarded to the inner `retry` call so an abort surfaces as a `RetryAbortedError` from the retry package; the outer `catch` rewraps it via the IO_FAILED path. - `key` (when provided) is propagated into the rethrown `StorageFileError` so OTel attribute extraction works downstream without additional context plumbing. - Pre-existing `StorageFileError` instances are NOT rewrapped — this keeps a `STORAGE_FILE.INVALID_KEY` raised inside `fn` visible as `INVALID_KEY` at the call site, not as a meaningless `IO_FAILED` wrapper.