Initial Commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user