first commit
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# `middleware`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`middleware.ts` builds the `DriveMiddleware` registered on a `Drive` by `driveCachePlugin`. The middleware intercepts `read(ctx, next)` to short-circuit hot paths from the cache and to populate fresh entries on a miss. `write` and `delete` interceptions are delegated to `middleware.invalidation`. `stat` and `list` are pure passthroughs in this iteration.
|
||||
|
||||
A cache is a perf optimisation, not a correctness boundary. Every underlying cache failure (`get` rejection, `set` rejection, `delete` rejection) is captured into `stats.errors` and the OTel `errors_total{namespace, op}` counter — and is never propagated to the consumer.
|
||||
|
||||
## Features
|
||||
|
||||
- Pre-aborted `AbortSignal` short-circuits the cache work and defers to `next()` so the inner storage surfaces canonical `DRIVE.ABORTED`.
|
||||
- Live `AbortSignal` is forwarded to `cache.get` for caches whose `get` accepts a signal.
|
||||
- `shouldCache(path, bytes)` predicate gates population — defaults to a 16-MiB byte ceiling matching `drive-mesh-protocol`'s `MAX_FRAME_BYTES`.
|
||||
- Population enforces the byte ceiling defensively; an oversize entry raises a structured `DRIVE_CACHE.CACHE_TOO_LARGE` internally and is suppressed (counted on `stats.errors`).
|
||||
- `stat` and `list` pass straight to `next()` without touching the cache.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { buildMiddleware } from "@suaveplan/drive-plugin-cache/middleware";
|
||||
import { createMutableStats } from "@suaveplan/drive-plugin-cache/handle";
|
||||
|
||||
const stats = createMutableStats();
|
||||
const mw = buildMiddleware({ cache, stats, namespace: "drive" });
|
||||
drive.useMiddleware(mw);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
const mw = buildMiddleware({
|
||||
cache,
|
||||
stats,
|
||||
namespace: "media",
|
||||
invalidateOnWrite: true,
|
||||
invalidateOnDelete: false,
|
||||
shouldCache: (path, bytes) =>
|
||||
path.endsWith(".mov") && bytes.byteLength <= 8 * 1024 * 1024,
|
||||
maxEntryBytes: 8 * 1024 * 1024,
|
||||
logger,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Description |
|
||||
|---|---|---|
|
||||
| `BuildMiddlewareArgs` | `{ cache; stats; namespace?; invalidateOnWrite?; invalidateOnDelete?; shouldCache?; maxEntryBytes?; logger? }` | Constructor bag. |
|
||||
| `buildMiddleware(args)` | `(BuildMiddlewareArgs) => DriveMiddleware` | Returns the full `DriveMiddleware` (read + write + delete + stat + list). |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The cache key is computed as `${namespace}::${ctx.path}`. The `::` separator avoids accidental collision with paths that already contain `/`. Population happens **after** `next()` resolves — i.e. only when the inner storage actually produced bytes — so a thrown `next()` cannot poison the cache. Symmetrically, invalidation happens **after** a successful `write`/`delete` `next()`. The optimistic alternative (delete before next()) opens a race window where a concurrent read could repopulate stale bytes between the cache delete and the storage write.
|
||||
Reference in New Issue
Block a user