3.0 KiB
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/EAGAINas transient; every other errno is fatal. - Maps
EACCES/EPERMtoSTORAGE_FILE.PERMISSION_DENIEDafter exhausting retries. - Maps any other terminal error to
STORAGE_FILE.IO_FAILEDand preservescause. - Lets a previously-thrown
StorageFileErrorbubble through unwrapped so error codes don't get clobbered. - Defaults to
globalThis.setTimeout/clearTimeoutbut accepts an injectableTimersLikepair 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>— runsfnwith retry on transient fs errors. ThrowsStorageFileErrorwithIO_FAILEDorPERMISSION_DENIEDafter exhaustion.WithFsRetryOptions—{ retry?, timers?, signal?, key? }.retry.maxAttemptsdefault 3,retry.baseDelayMsdefault 50,retry.maxDelayMsdefault 5000.isTransientFsError(err): boolean— true whenerr.codeis one ofEBUSY/EMFILE/ENFILE/EAGAIN.isPermissionFsError(err): boolean— true whenerr.codeisEACCESorEPERM.
Implementation Notes
- The exponential backoff is built via
createExponentialBackofffrom@suaveplan/retry. The jitter policy is the package default (equal jitter). signalis forwarded to the innerretrycall so an abort surfaces as aRetryAbortedErrorfrom the retry package; the outercatchrewraps it via the IO_FAILED path.key(when provided) is propagated into the rethrownStorageFileErrorso OTel attribute extraction works downstream without additional context plumbing.- Pre-existing
StorageFileErrorinstances are NOT rewrapped — this keeps aSTORAGE_FILE.INVALID_KEYraised insidefnvisible asINVALID_KEYat the call site, not as a meaninglessIO_FAILEDwrapper.