Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
# `retry-helper`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<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.