first commit
This commit is contained in:
129
docs/networking/drive/drive-plugin-cache/README.md
Normal file
129
docs/networking/drive/drive-plugin-cache/README.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# @suaveplan/drive-plugin-cache - Read-through cache middleware for @suaveplan/drive. Accepts a caller-supplied AbstractCache<Uint8Array> and intercepts Drive.read to short-circuit hot paths; write/delete invalidate post-success. Cache backend, eviction policy, TTL, and namespacing are entirely caller-owned.
|
||||
|
||||
Read-through cache middleware for [`@suaveplan/drive`](../drive). Accepts a caller-supplied `AbstractCache<Uint8Array>` (from [`@suaveplan/cache`](../../cache/cache)) and intercepts every `drive.read(path)` to return cached plaintext bytes instead of re-running the storage / mesh / decryption stack. `write` and `delete` invalidate the corresponding entry post-success.
|
||||
|
||||
The cache itself is opaque to the plugin — consumers pick the backend (`MemoryCache`, `StorageCache` over OPFS / file / S3 / CDN, `RedisCache`), the eviction policy, the TTL, the namespacing, the codec, the redis/storage instance. The plugin just talks `AbstractCache`.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [error-codes](./src/error-codes/error-codes.md) — `error-codes.ts` defines the structured error surface for `@suaveplan/drive-plugin-cache`. It exports the `DRIVE_CACHE_…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — `error-codes.ts` defines the structured error surface for `@suaveplan/drive-plugin-cache`. It exports the `DRIVE_CACHE_…
|
||||
- [handle](./src/handle/handle.md) — `handle.ts` builds the `DriveCacheHandle` returned by `driveCachePlugin.install(drive)`. The handle exposes a stats sna…
|
||||
- [handle](./src/handle/handle.md) — `handle.ts` builds the `DriveCacheHandle` returned by `driveCachePlugin.install(drive)`. The handle exposes a stats sna…
|
||||
- [middleware](./src/middleware/middleware.md) — `middleware.ts` builds the `DriveMiddleware` registered on a `Drive` by `driveCachePlugin`. The middleware intercepts `…
|
||||
- [middleware.invalidation](./src/middleware/middleware.invalidation.md) — `middleware.invalidation.ts` provides the `write` and `delete` hooks of the `DriveMiddleware` built in `middleware.ts`.…
|
||||
- [middleware](./src/middleware/middleware.md) — `middleware.ts` builds the `DriveMiddleware` registered on a `Drive` by `driveCachePlugin`. The middleware intercepts `…
|
||||
- [middleware.signal](./src/middleware/middleware.signal.md) — `middleware.signal.ts` provides two tiny `AbortSignal` helpers used by the drive-cache middleware: `getSignal(ctx)` ext…
|
||||
- [otel](./src/otel/otel.md) — `otel.ts` wires the `@suaveplan/drive-cache` meter and tracer used by every middleware intercept in `@suaveplan/drive-p…
|
||||
- [otel](./src/otel/otel.md) — `otel.ts` wires the `@suaveplan/drive-cache` meter and tracer used by every middleware intercept in `@suaveplan/drive-p…
|
||||
- [plugin](./src/plugin/plugin.md) — `plugin.ts` is the public entry point of `@suaveplan/drive-plugin-cache`. It exports the `driveCachePlugin(options): Dr…
|
||||
- [plugin](./src/plugin/plugin.md) — `plugin.ts` is the public entry point of `@suaveplan/drive-plugin-cache`. It exports the `driveCachePlugin(options): Dr…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Modules
|
||||
|
||||
- [`plugin`](./src/plugin/plugin.md) — `driveCachePlugin(options): DrivePlugin<DriveCacheHandle>` factory.
|
||||
- [`middleware`](./src/middleware/middleware.md) — the read intercept (hit short-circuit + populate-on-miss).
|
||||
- [`middleware.invalidation`](./src/middleware/middleware.invalidation.md) — write/delete invalidation hooks.
|
||||
- [`middleware.signal`](./src/middleware/middleware.signal.md) — `AbortSignal` helpers.
|
||||
- [`handle`](./src/handle/handle.md) — `DriveCacheHandle` builder.
|
||||
- [`otel`](./src/otel/otel.md) — `@suaveplan/drive-cache` meter + tracer wiring.
|
||||
- [`error-codes`](./src/error-codes/error-codes.md) — `DRIVE_CACHE_CODES` registry and `DriveCacheError`.
|
||||
- [`types`](./src/types/options.types.md) — public type surface.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```ts
|
||||
import { Drive } from "@suaveplan/drive";
|
||||
import { MemoryCache } from "@suaveplan/cache";
|
||||
import { driveCachePlugin } from "@suaveplan/drive-plugin-cache";
|
||||
|
||||
const cache = new MemoryCache<Uint8Array>();
|
||||
const drive = new Drive(storage);
|
||||
await drive.initialize();
|
||||
const handle = await drive.usePlugin(driveCachePlugin({ cache }));
|
||||
|
||||
await drive.write("clip.mov", payload);
|
||||
const bytes1 = await drive.read("clip.mov"); // miss → populate
|
||||
const bytes2 = await drive.read("clip.mov"); // hit → cached
|
||||
console.log(handle.getStats()); // { hits: 1, misses: 1, ... }
|
||||
```
|
||||
|
||||
## Composition recipes
|
||||
|
||||
### 1. Memory cache (zero deps, ephemeral, fastest)
|
||||
|
||||
```ts
|
||||
import { MemoryCache } from "@suaveplan/cache";
|
||||
const cache = new MemoryCache<Uint8Array>({ maxSize: 10_000 });
|
||||
await drive.usePlugin(driveCachePlugin({ cache }));
|
||||
```
|
||||
|
||||
### 2. OPFS-backed cache (browser, survives reloads)
|
||||
|
||||
```ts
|
||||
import { createOpfsStorage } from "@suaveplan/storage-browser-opfs";
|
||||
import { StorageCache, IdentityCodec } from "@suaveplan/cache-storage";
|
||||
|
||||
const cache = new StorageCache<Uint8Array>({
|
||||
storage: await createOpfsStorage("drive-cache"),
|
||||
codec: new IdentityCodec(),
|
||||
});
|
||||
await drive.usePlugin(driveCachePlugin({ cache, namespace: "media" }));
|
||||
```
|
||||
|
||||
### 3. File-backed cache (server, survives process restart)
|
||||
|
||||
```ts
|
||||
import { createNodeFsStorage } from "@suaveplan/storage-file";
|
||||
import { StorageCache, IdentityCodec } from "@suaveplan/cache-storage";
|
||||
|
||||
const cache = new StorageCache<Uint8Array>({
|
||||
storage: createNodeFsStorage({ rootDir: "/var/cache/drive", mkdirp: true }),
|
||||
codec: new IdentityCodec(),
|
||||
});
|
||||
await drive.usePlugin(driveCachePlugin({ cache }));
|
||||
```
|
||||
|
||||
### 4. Redis-backed cache (server, cross-process shared)
|
||||
|
||||
```ts
|
||||
import { createRedisCache } from "@suaveplan/cache-redis";
|
||||
|
||||
const cache = createRedisCache({ client, prefix: "drive-cache:" });
|
||||
await drive.usePlugin(driveCachePlugin({ cache, namespace: "media" }));
|
||||
```
|
||||
|
||||
### 5. Stale-while-revalidate via `cache.wrap()`
|
||||
|
||||
```ts
|
||||
const handle = await drive.usePlugin(driveCachePlugin({ cache }));
|
||||
const bytes = await handle.cache.wrap(
|
||||
"drive::clip.mov",
|
||||
() => drive.read("clip.mov"),
|
||||
{ staleAge: 60_000 }
|
||||
);
|
||||
```
|
||||
|
||||
Recipe 5 illustrates why the plugin exposes `handle.cache`: stale-while-revalidate, lock-and-load, refresh-ahead, and every other policy `cache.wrap()` already supports stays one composition step away — without the plugin growing options for each.
|
||||
|
||||
## Defaults
|
||||
|
||||
| Option | Default | Rationale |
|
||||
|---|---|---|
|
||||
| `namespace` | `"drive"` | Single-drive consumers don't need to pick. Multi-drive consumers pass distinct strings and share one cache. |
|
||||
| `invalidateOnWrite` | `true` | Cached bytes go stale the moment storage accepts new bytes. Post-success deletion is the safe default. |
|
||||
| `invalidateOnDelete` | `true` | Symmetric with `invalidateOnWrite`. |
|
||||
| `maxEntryBytes` | `16 * 1024 * 1024` (16 MiB) | Matches `drive-mesh-protocol`'s `MAX_FRAME_BYTES`. Empirically safe in every consumer already running drive. Bypass via a custom `shouldCache` for large-blob workflows. |
|
||||
| `shouldCache` | `bytes.byteLength <= maxEntryBytes` | The byte-ceiling check is the only failure mode for the default predicate. |
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Caching `Drive.stat` / `Drive.list` results.
|
||||
- Range / streaming reads (`Drive.createReadStream`) — bypass middleware by design.
|
||||
- Per-tenant key partitioning (`keyFn(ctx)` extension).
|
||||
- Built-in `wrap()` integration — consumers compose via `handle.cache.wrap(...)`.
|
||||
|
||||
See the design proposal for the full design rationale.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# `error-codes`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`error-codes.ts` defines the structured error surface for `@suaveplan/drive-plugin-cache`. It exports the `DRIVE_CACHE_CODES` registry produced by `defineCodes("DRIVE_CACHE", [...] as const)` and the `DriveCacheError` class that carries optional `path`, `namespace`, `reason`, `byteLength`, and `maxEntryBytes` discriminants for operator correlation.
|
||||
|
||||
The plugin deliberately owns a narrow registry — three codes — because the interesting failures are upstream (`CACHE.*`, `DRIVE.*`) and pass through unchanged. Allocating a wide error registry here would bloat the surface and mask the real diagnostic value already carried by the cache and drive packages.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `DRIVE_CACHE_CODES` registry with three reasons: `INVALID_OPTIONS`, `CACHE_TOO_LARGE`, `INSTALL_FAILED`.
|
||||
- `DriveCacheError extends SuaveplanError<DriveCacheErrorCode>` with five optional discriminants.
|
||||
- Every code is reachable from a real call site exercised by the test suite.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { DRIVE_CACHE_CODES, DriveCacheError } from "@suaveplan/drive-plugin-cache/error-codes";
|
||||
|
||||
throw new DriveCacheError(
|
||||
DRIVE_CACHE_CODES.INVALID_OPTIONS,
|
||||
"driveCachePlugin requires options.cache",
|
||||
{ reason: "cache" }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { DriveCacheError, DRIVE_CACHE_CODES } from "@suaveplan/drive-plugin-cache/error-codes";
|
||||
|
||||
try {
|
||||
drive.useMiddleware(myMiddleware);
|
||||
} catch (cause) {
|
||||
throw new DriveCacheError(
|
||||
DRIVE_CACHE_CODES.INSTALL_FAILED,
|
||||
"Drive.useMiddleware rejected the cache middleware",
|
||||
{ cause, namespace: "drive" }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Catch by `instanceof DriveCacheError` to access the discriminants; the original underlying throwable is preserved on `cause` via the `SuaveplanError` base class so operators can walk the chain with `walkCauses`.
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Description |
|
||||
|---|---|---|
|
||||
| `DRIVE_CACHE_CODES` | `{ readonly INVALID_OPTIONS: "DRIVE_CACHE.INVALID_OPTIONS"; ... }` | Frozen record of every code string. |
|
||||
| `DriveCacheErrorCode` | union of the three code string literals | Discriminant for switch/match. |
|
||||
| `DriveCacheErrorOptions` | `{ path?: string; namespace?: string; reason?: string; byteLength?: number; maxEntryBytes?: number; cause?: unknown; ... }` | Constructor options bag. |
|
||||
| `DriveCacheError` | `extends SuaveplanError<DriveCacheErrorCode>` | Structured error with discriminants and preserved cause chain. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry uses `as const` on the reasons array so `defineCodes` can infer the literal-typed return shape. The class assigns each discriminant from the options bag in the constructor, defaulting to `undefined` when absent — the field is declared `readonly` to make the populated state immutable after construction. `this.name = "DriveCacheError"` aligns runtime `name` with the class name for stack traces in environments that read it (e.g. Sentry, V8 stack frames).
|
||||
@@ -0,0 +1,59 @@
|
||||
# `handle`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`handle.ts` builds the `DriveCacheHandle` returned by `driveCachePlugin.install(drive)`. The handle exposes a stats snapshot, a single-path invalidator, a cache clear shortcut, and the underlying `AbstractCache` reference for advanced wiring (e.g. `cache.wrap(key, loader, { staleAge })`).
|
||||
|
||||
It also defines the `MutableStats` counter bag shared between the middleware (which increments inline as `read`/`write`/`delete` interceptions resolve) and the handle (whose `getStats()` returns a shallow snapshot so consumers cannot mutate the live counters).
|
||||
|
||||
## Features
|
||||
|
||||
- `createMutableStats()` builds a fresh zeroed counter bag per plugin instance.
|
||||
- `createHandle({ cache, stats, namespace })` returns a `DriveCacheHandle` whose methods read/mutate the shared `stats` reference without ever exposing it.
|
||||
- `getStats()` always returns a fresh shallow snapshot, so callers can cache the value without risk of mutation.
|
||||
- `invalidate(path)` swallows underlying cache errors, increments `stats.errors`, and records via OTel.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createHandle, createMutableStats } from "@suaveplan/drive-plugin-cache/handle";
|
||||
|
||||
const stats = createMutableStats();
|
||||
const handle = createHandle({ cache, stats, namespace: "drive" });
|
||||
|
||||
await handle.invalidate("clip.mov");
|
||||
const snapshot = handle.getStats();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
const handle = createHandle({ cache, stats, namespace: "media" });
|
||||
|
||||
// Compose with cache.wrap for stale-while-revalidate
|
||||
const bytes = await handle.cache.wrap(
|
||||
"media::clip.mov",
|
||||
() => drive.read("clip.mov"),
|
||||
{ staleAge: 60_000 }
|
||||
);
|
||||
|
||||
// Or clear when switching drives
|
||||
await handle.clear();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Description |
|
||||
|---|---|---|
|
||||
| `MutableStats` | `{ hits: number; misses: number; populations: number; invalidations: number; errors: number }` | Mutable counter bag — middleware-internal, never exposed. |
|
||||
| `createMutableStats()` | `() => MutableStats` | Fresh zeroed instance per plugin. |
|
||||
| `CreateHandleArgs` | `{ cache; stats; namespace }` | Constructor bag. |
|
||||
| `createHandle(args)` | `(CreateHandleArgs) => DriveCacheHandle` | Returns the handle whose `getStats()` snapshots the shared counter. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`getStats()` returns a fresh plain object every call rather than spreading the underlying `stats` reference. This produces an identical observable behaviour but lets the live counter bag stay untouched even if a consumer mutates the returned object (e.g. `Object.assign(snap, { hits: 0 })` after caching the snapshot somewhere). The `invalidate(path)` method silently swallows underlying cache errors — failure to delete a stale cache entry is a non-fatal optimisation slip, not a contract breach. The error is captured via `stats.errors += 1` and the OTel `errors_total{namespace, op: "delete"}` counter so operators can still see it.
|
||||
@@ -0,0 +1,53 @@
|
||||
# `middleware.invalidation`
|
||||
|
||||
## Purpose
|
||||
|
||||
`middleware.invalidation.ts` provides the `write` and `delete` hooks of the `DriveMiddleware` built in `middleware.ts`. Each hook runs `next()` first; on successful resolution it deletes the corresponding cache entry. When `next()` throws, the throw propagates and the cache is left untouched — invariably the bytes in the cache are still the bytes the consumer last wrote.
|
||||
|
||||
This split keeps `middleware.ts` focused on the read path (the hot path) and makes the invalidation logic independently testable. The two hooks share a single `invalidate(path)` closure that wraps `cache.delete` with stats accounting, OTel recording, and error swallowing.
|
||||
|
||||
## Features
|
||||
|
||||
- Post-success invalidation — no race window where a concurrent read can repopulate stale bytes between the cache delete and the storage write.
|
||||
- `invalidateOnWrite` / `invalidateOnDelete` per-side toggles default to `true` but accept `false` for callers wanting append-only or write-through behaviour.
|
||||
- Underlying `cache.delete` rejection is swallowed, counted on `stats.errors`, and recorded via OTel as `errors_total{namespace, op: "delete"}`.
|
||||
- The `delete` hook returns the original `next()` boolean unchanged — semantics match `Drive.delete`'s "did the file exist" contract.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { buildInvalidationMiddleware } from "@suaveplan/drive-plugin-cache/middleware";
|
||||
|
||||
const hooks = buildInvalidationMiddleware({
|
||||
cache,
|
||||
stats,
|
||||
namespace: "drive",
|
||||
invalidateOnWrite: true,
|
||||
invalidateOnDelete: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
const hooks = buildInvalidationMiddleware({
|
||||
cache,
|
||||
stats,
|
||||
namespace: "audit",
|
||||
invalidateOnWrite: false, // append-only: writes never displace cached bytes
|
||||
invalidateOnDelete: true,
|
||||
logger,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Description |
|
||||
|---|---|---|
|
||||
| `BuildInvalidationArgs` | `{ cache; stats; namespace; invalidateOnWrite; invalidateOnDelete; logger? }` | Constructor bag. |
|
||||
| `InvalidationHooks` | `{ write(ctx, next); delete(ctx, next) }` | Hook pair compatible with `DriveMiddleware`. |
|
||||
| `buildInvalidationMiddleware(args)` | `(BuildInvalidationArgs) => InvalidationHooks` | Returns the two hooks. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The shared `invalidate(path)` helper is a closure over the per-build state — `cache`, `stats`, `namespace`, optional `logger`. This avoids passing those four parameters through every invocation while keeping the function pure modulo the explicit mutable state. The `delete` hook still calls `invalidate` when `next()` returns `false` (the file did not exist on disk). This is safe — `cache.delete` is idempotent on missing keys — and it cleans any stale cache entry produced by an earlier reader that observed the file before a concurrent removal.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,46 @@
|
||||
# `middleware.signal`
|
||||
|
||||
## Purpose
|
||||
|
||||
`middleware.signal.ts` provides two tiny `AbortSignal` helpers used by the drive-cache middleware: `getSignal(ctx)` extracts a signal from the open-shaped `MiddlewareContext`, and `isPreAborted(ctx)` returns true when the extracted signal is already aborted at the moment the middleware runs.
|
||||
|
||||
Drive's core does not currently thread a signal into `ctx`. The `MiddlewareContext` shape is an open `[key: string]: unknown` carrier, so downstream extensions and future Drive versions can. Centralising the lookup here means the read/write/delete middleware code stays a single line, and updating to a future canonical `ctx.signal` field is a one-file change.
|
||||
|
||||
## Features
|
||||
|
||||
- Type-safe extraction — returns `AbortSignal | undefined`, never throws.
|
||||
- `isPreAborted` collapses the "have a signal and it is aborted" check into a single boolean.
|
||||
- No runtime dependency — purely structural over the `MiddlewareContext` open shape.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { isPreAborted, getSignal } from "@suaveplan/drive-plugin-cache/middleware";
|
||||
|
||||
if (isPreAborted(ctx)) return next();
|
||||
const signal = getSignal(ctx);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { getSignal } from "@suaveplan/drive-plugin-cache/middleware";
|
||||
|
||||
const signal = getSignal(ctx);
|
||||
if (signal !== undefined) {
|
||||
// Pass the signal into the underlying cache.get when supported.
|
||||
await (cache as { get(k: string, s?: AbortSignal): Promise<Uint8Array | undefined> })
|
||||
.get(key, signal);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Description |
|
||||
|---|---|---|
|
||||
| `getSignal(ctx)` | `(MiddlewareContext) => AbortSignal \| undefined` | Returns the signal carried on `ctx.signal` when it is an `AbortSignal`, else `undefined`. |
|
||||
| `isPreAborted(ctx)` | `(MiddlewareContext) => boolean` | True when `getSignal(ctx)?.aborted === true`. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `instanceof AbortSignal` guard rejects loose duck-typed shapes that happen to carry an `aborted` boolean but lack the full signal contract (no `addEventListener`, no `reason`). This matters because forwarding such a shape into `cache.get` could cause the cache to call `signal.addEventListener` on a non-EventTarget object. The instanceof check is cheap and correct for every standards-conformant runtime; consumers building their own signal-shaped object should construct a real `AbortController` and surface its `signal`.
|
||||
64
docs/networking/drive/drive-plugin-cache/src/otel/otel.md
Normal file
64
docs/networking/drive/drive-plugin-cache/src/otel/otel.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# `otel`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`otel.ts` wires the `@suaveplan/drive-cache` meter and tracer used by every middleware intercept in `@suaveplan/drive-plugin-cache`. It exposes lazily-built `getInstruments()`, a `setDriveCacheMeterProvider` override (so tests can inject a stub provider), `resetInstruments()` for inter-test isolation, and five `recordX` helpers that pair a counter increment with a single short-lived span carrying the canonical `drive.cache.*` attributes.
|
||||
|
||||
The instruments are cached after the first call and rebuilt only when the provider override changes or `resetInstruments()` is called. This avoids re-creating instrument handles on the hot read path while still letting tests drive their own provider.
|
||||
|
||||
## Features
|
||||
|
||||
- `drive_cache.hits_total{namespace}` / `misses_total{namespace}` / `populations_total{namespace}` / `invalidations_total{namespace}` counters.
|
||||
- `drive_cache.errors_total{namespace, op}` counter — `op ∈ {"get", "set", "delete"}`.
|
||||
- `drive_cache.entry_bytes{namespace}` histogram observed on every population.
|
||||
- One-span-per-event spans with `drive.cache.namespace`, `drive.cache.path`, and `drive.cache.outcome ∈ {"hit", "miss", "populated", "invalidated", "error"}` attributes.
|
||||
- `setDriveCacheMeterProvider(provider)` resets the cached instruments so tests can swap providers between cases.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { recordHit, recordMiss, recordPopulate } from "@suaveplan/drive-plugin-cache/otel";
|
||||
|
||||
recordMiss("drive", "clip.mov");
|
||||
recordPopulate("drive", "clip.mov", 4096);
|
||||
recordHit("drive", "clip.mov");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
setDriveCacheMeterProvider,
|
||||
resetInstruments,
|
||||
recordError,
|
||||
} from "@suaveplan/drive-plugin-cache/otel";
|
||||
import { MeterProvider } from "@opentelemetry/sdk-metrics";
|
||||
|
||||
const provider = new MeterProvider();
|
||||
setDriveCacheMeterProvider(provider);
|
||||
recordError("drive", "set", "clip.mov");
|
||||
// run assertions on provider's metric readers
|
||||
setDriveCacheMeterProvider(undefined);
|
||||
resetInstruments();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Description |
|
||||
|---|---|---|
|
||||
| `getInstruments(force?)` | `() => DriveCacheInstruments` | Lazy accessor; pass `true` to force-rebuild. |
|
||||
| `resetInstruments()` | `() => void` | Clear the cached bag (test-only). |
|
||||
| `setDriveCacheMeterProvider(provider)` | `(p?: MeterProvider) => void` | Swap the meter provider; auto-resets the cache. |
|
||||
| `recordHit(namespace, path)` | `(string, string) => void` | Increment hits + emit `outcome="hit"` span. |
|
||||
| `recordMiss(namespace, path)` | `(string, string) => void` | Increment misses + emit `outcome="miss"` span. |
|
||||
| `recordPopulate(namespace, path, byteLength)` | `(string, string, number) => void` | Increment populations + observe entry size + emit `outcome="populated"` span. |
|
||||
| `recordInvalidate(namespace, path)` | `(string, string) => void` | Increment invalidations + emit `outcome="invalidated"` span. |
|
||||
| `recordError(namespace, op, path)` | `(string, "get" \| "set" \| "delete", string) => void` | Increment errors + emit `outcome="error"` span with `op` attribute. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Spans are created via `tracer.startSpan(...).end()` rather than `tracer.startActiveSpan(...)`. The middleware does its own work inside the calling async flow; the only purpose of these spans is to carry the outcome attribute for trace-backed analysis. Long-lived active spans would force every middleware intercept to thread an `OpenTelemetry.Context`, which is overkill for a cache event. Counter attribute bags are constructed inline as plain objects — V8 inlines this allocation on the hot path so the impact is undetectable.
|
||||
@@ -0,0 +1,66 @@
|
||||
# `plugin`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`plugin.ts` is the public entry point of `@suaveplan/drive-plugin-cache`. It exports the `driveCachePlugin(options): DrivePlugin<DriveCacheHandle>` factory — the only function consumers normally need. The factory validates the options synchronously (so misuse fails fast at construction, not on first read), then assembles the cache middleware, stats bag, and handle.
|
||||
|
||||
`install(drive)` registers the middleware on the supplied `DriveLike` via `drive.useMiddleware` and returns the `DriveCacheHandle`. If `drive.useMiddleware` itself throws, the failure is wrapped as `DRIVE_CACHE.INSTALL_FAILED` so operators can correlate the install-time error chain.
|
||||
|
||||
## Features
|
||||
|
||||
- Synchronous `DRIVE_CACHE.INVALID_OPTIONS` throw when `options.cache` is missing or lacks function-typed `get`/`set`/`delete` methods.
|
||||
- `INSTALL_FAILED` wraps any throw from `drive.useMiddleware`, preserving the original cause.
|
||||
- Default namespace `"drive"`, default invalidation `true` for both write and delete, default `shouldCache` enforces the 16-MiB ceiling.
|
||||
- Returns `DriveCacheHandle` whose `cache` reference is identical (`===`) to the cache passed in `options`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { driveCachePlugin } from "@suaveplan/drive-plugin-cache";
|
||||
import { MemoryCache } from "@suaveplan/cache";
|
||||
|
||||
const cache = new MemoryCache<Uint8Array>();
|
||||
const handle = await drive.usePlugin(driveCachePlugin({ cache }));
|
||||
|
||||
const bytes1 = await drive.read("clip.mov"); // miss → next() → populate
|
||||
const bytes2 = await drive.read("clip.mov"); // hit → return cached
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { driveCachePlugin } from "@suaveplan/drive-plugin-cache";
|
||||
|
||||
const handleA = await driveA.usePlugin(
|
||||
driveCachePlugin({
|
||||
cache, // shared underlying AbstractCache
|
||||
namespace: "a", // distinct namespace per drive
|
||||
invalidateOnWrite: true,
|
||||
invalidateOnDelete: true,
|
||||
shouldCache: (path, bytes) =>
|
||||
bytes.byteLength <= 8 * 1024 * 1024,
|
||||
maxEntryBytes: 8 * 1024 * 1024,
|
||||
logger,
|
||||
})
|
||||
);
|
||||
const handleB = await driveB.usePlugin(
|
||||
driveCachePlugin({ cache, namespace: "b" })
|
||||
);
|
||||
// handleA and handleB share `cache` but their entries never overlap.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Export | Shape | Description |
|
||||
|---|---|---|
|
||||
| `driveCachePlugin(options)` | `(DriveCacheOptions) => DrivePlugin<DriveCacheHandle>` | Factory. Throws `DRIVE_CACHE.INVALID_OPTIONS` synchronously on misuse. |
|
||||
|
||||
The returned plugin object exposes `name === "@suaveplan/drive-plugin-cache"` and an `install(drive)` method that registers the middleware and returns the handle.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Option validation runs once at factory time. The factory snapshots the namespace into the middleware closure and into the handle closure separately — both stay in sync because the namespace is immutable after construction. The single mutable state shared between the middleware and the handle is the `MutableStats` counter bag; the handle's `getStats()` returns shallow snapshots, so consumers cannot reach back into the live counters.
|
||||
@@ -0,0 +1,52 @@
|
||||
# `options.types`
|
||||
|
||||
## Purpose
|
||||
|
||||
`options.types.ts` declares the three public interfaces consumed across `@suaveplan/drive-plugin-cache`: `DriveCacheOptions` (the factory input), `DriveCacheHandle` (the install return value), and `DriveCacheStats` (the snapshot shape exposed by `handle.getStats()`). These types are the public contract — everything else in the package is implementation detail.
|
||||
|
||||
The shapes are deliberately small. `DriveCacheOptions` carries one required field (`cache`) and five optional knobs (invalidation toggles, namespace, predicate, byte ceiling, logger). `DriveCacheHandle` carries one readonly reference (`cache`) and three methods (`getStats`, `invalidate`, `clear`). `DriveCacheStats` is five readonly counters — `hits`, `misses`, `populations`, `invalidations`, `errors`.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict `readonly` on every field so consumers cannot mutate snapshots.
|
||||
- `cache: AbstractCache<Uint8Array>` — single injectable; backend is fully caller-owned.
|
||||
- Defaults documented inline so the consumer can predict the runtime behaviour without reading source.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import type { DriveCacheOptions } from "@suaveplan/drive-plugin-cache/types";
|
||||
|
||||
const options: DriveCacheOptions = {
|
||||
cache: new MemoryCache<Uint8Array>(),
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import type { DriveCacheOptions } from "@suaveplan/drive-plugin-cache/types";
|
||||
|
||||
const options: DriveCacheOptions = {
|
||||
cache,
|
||||
namespace: "media",
|
||||
invalidateOnWrite: true,
|
||||
invalidateOnDelete: false,
|
||||
shouldCache: (path, bytes) =>
|
||||
path.endsWith(".mov") && bytes.byteLength < 8 * 1024 * 1024,
|
||||
maxEntryBytes: 8 * 1024 * 1024,
|
||||
logger,
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Type | Fields | Notes |
|
||||
|---|---|---|
|
||||
| `DriveCacheOptions` | `cache; invalidateOnWrite?; invalidateOnDelete?; namespace?; shouldCache?; maxEntryBytes?; logger?` | Factory input. |
|
||||
| `DriveCacheHandle` | `cache; getStats(); invalidate(path); clear()` | Install return value. |
|
||||
| `DriveCacheStats` | `hits; misses; populations; invalidations; errors` | Snapshot returned by `getStats()`. All readonly. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `readonly` modifier on `DriveCacheOptions` fields prevents accidental in-place mutation of options after they cross the factory boundary. The factory snapshots required values into local closure state at construction time so post-install mutation of the options object never affects runtime behaviour anyway. `AbstractCache<Uint8Array>` is the canonical surface — concrete cache implementations (`MemoryCache`, `StorageCache`, `RedisCache`) extend it without runtime cost.
|
||||
Reference in New Issue
Block a user