Files
2026-05-27 23:47:53 +01:00

3.0 KiB

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

import { withFsRetry } from "@suaveplan/storage-file";

const data = await withFsRetry(() => fs.readFile("/tmp/store/k.data"));

Usage: Advanced Example

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<T>(fn, options?): Promise<T> — 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.