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,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.

View File

@@ -0,0 +1,127 @@
{
"name": "@suaveplan/drive-plugin-cache",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "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.",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./error-codes": {
"import": {
"types": "./dist/error-codes/index.d.ts",
"default": "./dist/error-codes/error-codes.js"
},
"require": {
"types": "./dist/error-codes/index.d.ts",
"default": "./dist/error-codes/error-codes.js"
}
},
"./types": {
"import": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
},
"require": {
"types": "./dist/types/index.d.ts",
"default": "./dist/types/index.js"
}
},
"./handle": {
"import": {
"types": "./dist/handle/index.d.ts",
"default": "./dist/handle/handle.js"
},
"require": {
"types": "./dist/handle/index.d.ts",
"default": "./dist/handle/handle.js"
}
},
"./middleware": {
"import": {
"types": "./dist/middleware/index.d.ts",
"default": "./dist/middleware/middleware.js"
},
"require": {
"types": "./dist/middleware/index.d.ts",
"default": "./dist/middleware/middleware.js"
}
},
"./plugin": {
"import": {
"types": "./dist/plugin/index.d.ts",
"default": "./dist/plugin/plugin.js"
},
"require": {
"types": "./dist/plugin/index.d.ts",
"default": "./dist/plugin/plugin.js"
}
},
"./otel": {
"import": {
"types": "./dist/otel/index.d.ts",
"default": "./dist/otel/otel.js"
},
"require": {
"types": "./dist/otel/index.d.ts",
"default": "./dist/otel/otel.js"
}
}
},
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/",
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
"format": "bunx biome format --write --max-diagnostics 500 src/",
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@opentelemetry/api": "catalog:observability",
"@suaveplan/cache": "workspace:^",
"@suaveplan/drive": "workspace:^",
"@suaveplan/error": "workspace:^",
"@suaveplan/types": "workspace:^",
"tslib": "catalog:build"
},
"devDependencies": {
"@suaveplan/cache-storage": "workspace:^",
"@suaveplan/storage": "workspace:^",
"@suaveplan/storage-file": "workspace:^",
"@suaveplan/testing": "workspace:^",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:testing",
"fast-check": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
},
"files": [
"dist",
"src",
"README.md"
]
}

View File

@@ -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).

View File

@@ -0,0 +1,80 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
DRIVE_CACHE_CODES,
DriveCacheError,
type DriveCacheErrorCode,
} from "./error-codes.js";
describe("DRIVE_CACHE_CODES registry", () => {
it("exposes all three namespaced codes", { timeout: 5000 }, () => {
const codes: DriveCacheErrorCode[] = [
DRIVE_CACHE_CODES.INVALID_OPTIONS,
DRIVE_CACHE_CODES.CACHE_TOO_LARGE,
DRIVE_CACHE_CODES.INSTALL_FAILED,
];
expect(codes).toHaveLength(3);
for (const code of codes) {
expect(code.startsWith("DRIVE_CACHE.")).toBe(true);
}
});
});
describe("DriveCacheError", () => {
it("populates every discriminant from the options bag", {
timeout: 5000,
}, () => {
const cause = new Error("underlying");
const err = new DriveCacheError(
DRIVE_CACHE_CODES.CACHE_TOO_LARGE,
"too big",
{
path: "clip.mov",
namespace: "media",
reason: "byte-ceiling",
byteLength: 32_000_000,
maxEntryBytes: 16_777_216,
cause,
}
);
expect(err.code).toBe(DRIVE_CACHE_CODES.CACHE_TOO_LARGE);
expect(err.path).toBe("clip.mov");
expect(err.namespace).toBe("media");
expect(err.reason).toBe("byte-ceiling");
expect(err.byteLength).toBe(32_000_000);
expect(err.maxEntryBytes).toBe(16_777_216);
expect(err.cause).toBe(cause);
expect(err.name).toBe("DriveCacheError");
});
it("leaves unspecified discriminants undefined", { timeout: 5000 }, () => {
const err = new DriveCacheError(
DRIVE_CACHE_CODES.INVALID_OPTIONS,
"no options at all"
);
expect(err.path).toBeUndefined();
expect(err.namespace).toBeUndefined();
expect(err.reason).toBeUndefined();
expect(err.byteLength).toBeUndefined();
expect(err.maxEntryBytes).toBeUndefined();
});
it("is reachable for every registered code via a real throw", {
timeout: 5000,
}, () => {
const allCodes: DriveCacheErrorCode[] = [
DRIVE_CACHE_CODES.INVALID_OPTIONS,
DRIVE_CACHE_CODES.CACHE_TOO_LARGE,
DRIVE_CACHE_CODES.INSTALL_FAILED,
];
for (const code of allCodes) {
let caught: DriveCacheError | undefined;
try {
throw new DriveCacheError(code, `test for ${code}`);
} catch (err) {
caught = err as DriveCacheError;
}
expect(caught).toBeInstanceOf(DriveCacheError);
expect(caught?.code).toBe(code);
}
});
});

View File

@@ -0,0 +1,65 @@
import {
defineCodes,
SuaveplanError,
type SuaveplanErrorOptions,
} from "@suaveplan/error";
/**
* Registry of every error code raised by `@suaveplan/drive-plugin-cache`.
*
* Three codes total — `INVALID_OPTIONS` (synchronous misuse at factory time),
* `CACHE_TOO_LARGE` (defensive belt-and-braces when a custom `maxEntryBytes`
* is exceeded yet `shouldCache` still returned `true`), and `INSTALL_FAILED`
* (the supplied `DriveLike.useMiddleware` itself threw). The vast majority
* of failure modes come from the underlying `AbstractCache` and `Drive`
* and carry their own `CACHE_*` / `DRIVE_*` codes that pass through this
* middleware unchanged.
*/
export const DRIVE_CACHE_CODES = defineCodes("DRIVE_CACHE", [
"INVALID_OPTIONS",
"CACHE_TOO_LARGE",
"INSTALL_FAILED",
] as const);
export type DriveCacheErrorCode =
(typeof DRIVE_CACHE_CODES)[keyof typeof DRIVE_CACHE_CODES];
export interface DriveCacheErrorOptions extends SuaveplanErrorOptions {
path?: string;
namespace?: string;
reason?: string;
byteLength?: number;
maxEntryBytes?: number;
}
/**
* Structured error for drive-cache plugin operations.
*
* Carries optional discriminants — `path` and `namespace` correlate the
* failure with a specific cache entry; `byteLength`/`maxEntryBytes` flag the
* `CACHE_TOO_LARGE` ratio; `reason` describes the offending option in
* `INVALID_OPTIONS`. The original underlying cause (when re-wrapping a
* `Drive.useMiddleware` throwable for `INSTALL_FAILED`) is preserved on
* `cause` via the base class.
*/
export class DriveCacheError extends SuaveplanError<DriveCacheErrorCode> {
public readonly path: string | undefined;
public readonly namespace: string | undefined;
public readonly reason: string | undefined;
public readonly byteLength: number | undefined;
public readonly maxEntryBytes: number | undefined;
public constructor(
code: DriveCacheErrorCode,
message: string,
options?: DriveCacheErrorOptions
) {
super(code, message, options);
this.path = options?.path;
this.namespace = options?.namespace;
this.reason = options?.reason;
this.byteLength = options?.byteLength;
this.maxEntryBytes = options?.maxEntryBytes;
this.name = "DriveCacheError";
}
}

View File

@@ -0,0 +1,8 @@
export type {
DriveCacheErrorCode,
DriveCacheErrorOptions,
} from "./error-codes.js";
export {
DRIVE_CACHE_CODES,
DriveCacheError,
} from "./error-codes.js";

View File

@@ -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.

View File

@@ -0,0 +1,85 @@
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
import { FakeCache } from "../../tests/setup/fake-cache.js";
import {
createHandle,
createMutableStats,
type MutableStats,
} from "./handle.js";
describe("DriveCacheHandle", () => {
let cache: FakeCache;
let stats: MutableStats;
beforeEach(() => {
cache = new FakeCache();
stats = createMutableStats();
});
it("getStats returns monotonically increasing counters", {
timeout: 5000,
}, () => {
const handle = createHandle({ cache, stats, namespace: "drive" });
stats.hits = 2;
stats.misses = 1;
stats.populations = 1;
const snap = handle.getStats();
expect(snap).toEqual({
hits: 2,
misses: 1,
populations: 1,
invalidations: 0,
errors: 0,
});
});
it("getStats returns a fresh snapshot, not the live stats reference", {
timeout: 5000,
}, () => {
const handle = createHandle({ cache, stats, namespace: "drive" });
stats.hits = 1;
const a = handle.getStats();
stats.hits = 2;
const b = handle.getStats();
expect(a.hits).toBe(1);
expect(b.hits).toBe(2);
// Mutating the snapshot must not affect the live stats.
(a as unknown as { hits: number }).hits = 999;
expect(handle.getStats().hits).toBe(2);
});
it("invalidate deletes one cache entry", { timeout: 5000 }, async () => {
cache.store.set("drive::x.bin", new Uint8Array([1]));
const handle = createHandle({ cache, stats, namespace: "drive" });
await handle.invalidate("x.bin");
expect(cache.deleteCalls).toEqual(["drive::x.bin"]);
expect(stats.invalidations).toBe(1);
});
it("invalidate swallows underlying cache.delete errors", {
timeout: 5000,
}, async () => {
cache.throwOnDelete = new Error("redis down");
const handle = createHandle({ cache, stats, namespace: "drive" });
await handle.invalidate("x.bin");
expect(stats.errors).toBe(1);
expect(stats.invalidations).toBe(0);
});
it("clear empties the underlying cache", {
timeout: 5000,
}, async () => {
cache.store.set("drive::x.bin", new Uint8Array([1]));
const handle = createHandle({ cache, stats, namespace: "drive" });
await handle.clear();
expect(cache.clearCalls).toBe(1);
expect(cache.store.size).toBe(0);
});
it("uses the configured namespace in the invalidate key", {
timeout: 5000,
}, async () => {
const handle = createHandle({ cache, stats, namespace: "media" });
await handle.invalidate("clip.mov");
expect(cache.deleteCalls).toEqual(["media::clip.mov"]);
});
});

View File

@@ -0,0 +1,66 @@
import type { AbstractCache } from "@suaveplan/cache";
import { recordError, recordInvalidate } from "../otel/otel.js";
import type {
DriveCacheHandle,
DriveCacheStats,
} from "../types/options.types.js";
/**
* Mutable counter bag shared between the middleware and the handle. The
* handle never exposes this object directly — `getStats()` returns a shallow
* snapshot.
*/
export interface MutableStats {
hits: number;
misses: number;
populations: number;
invalidations: number;
errors: number;
}
export function createMutableStats(): MutableStats {
return {
hits: 0,
misses: 0,
populations: 0,
invalidations: 0,
errors: 0,
};
}
export interface CreateHandleArgs {
readonly cache: AbstractCache<Uint8Array>;
readonly stats: MutableStats;
readonly namespace: string;
}
/** Build the `DriveCacheHandle` returned from `driveCachePlugin.install`. */
export function createHandle(args: CreateHandleArgs): DriveCacheHandle {
const { cache, stats, namespace } = args;
return {
cache,
getStats(): DriveCacheStats {
return {
hits: stats.hits,
misses: stats.misses,
populations: stats.populations,
invalidations: stats.invalidations,
errors: stats.errors,
};
},
async invalidate(path: string): Promise<void> {
const key = `${namespace}::${path}`;
try {
await cache.delete(key);
stats.invalidations += 1;
recordInvalidate(namespace, path);
} catch {
stats.errors += 1;
recordError(namespace, "delete", path);
}
},
async clear(): Promise<void> {
await cache.clear();
},
};
}

View File

@@ -0,0 +1,8 @@
export type {
CreateHandleArgs,
MutableStats,
} from "./handle.js";
export {
createHandle,
createMutableStats,
} from "./handle.js";

View File

@@ -0,0 +1,45 @@
// Side-effect import: ensures the DRIVE_CACHE.* code registry registers even
// when subpath consumers never reach this barrel.
import "./error-codes/error-codes.js";
export {
DRIVE_CACHE_CODES,
DriveCacheError,
type DriveCacheErrorCode,
type DriveCacheErrorOptions,
} from "./error-codes/error-codes.js";
export {
type CreateHandleArgs,
createHandle,
createMutableStats,
type MutableStats,
} from "./handle/handle.js";
export {
type BuildInvalidationArgs,
buildInvalidationMiddleware,
type InvalidationHooks,
} from "./middleware/middleware.invalidation.js";
export {
type BuildMiddlewareArgs,
buildMiddleware,
} from "./middleware/middleware.js";
export { getSignal, isPreAborted } from "./middleware/middleware.signal.js";
export {
type DriveCacheInstruments,
type DriveCacheOp,
type DriveCacheOutcome,
getInstruments,
recordError,
recordHit,
recordInvalidate,
recordMiss,
recordPopulate,
resetInstruments,
setDriveCacheMeterProvider,
} from "./otel/otel.js";
export { driveCachePlugin } from "./plugin/plugin.js";
export type {
DriveCacheHandle,
DriveCacheOptions,
DriveCacheStats,
} from "./types/options.types.js";

View File

@@ -0,0 +1,10 @@
export {
type BuildInvalidationArgs,
buildInvalidationMiddleware,
type InvalidationHooks,
} from "./middleware.invalidation.js";
export {
type BuildMiddlewareArgs,
buildMiddleware,
} from "./middleware.js";
export { getSignal, isPreAborted } from "./middleware.signal.js";

View File

@@ -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.

View File

@@ -0,0 +1,146 @@
import type { MiddlewareContext } from "@suaveplan/drive";
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
import { FakeCache } from "../../tests/setup/fake-cache.js";
import { createMutableStats, type MutableStats } from "../handle/handle.js";
import { buildMiddleware } from "./middleware.js";
function makeCtx(path: string, op = "write"): MiddlewareContext {
return {
operation: op,
path,
drive: {} as MiddlewareContext["drive"],
};
}
describe("middleware: invalidation", () => {
let cache: FakeCache;
let stats: MutableStats;
beforeEach(() => {
cache = new FakeCache();
stats = createMutableStats();
});
it("successful write invalidates the cache entry", {
timeout: 5000,
}, async () => {
cache.store.set("drive::x.bin", new Uint8Array([1]));
const mw = buildMiddleware({ cache, stats });
await mw.write?.(makeCtx("x.bin"), async () => {});
expect(cache.deleteCalls).toEqual(["drive::x.bin"]);
expect(stats.invalidations).toBe(1);
});
it("failed write does not invalidate", {
timeout: 5000,
}, async () => {
const mw = buildMiddleware({ cache, stats });
let threw = false;
try {
await mw.write?.(makeCtx("x.bin"), async () => {
throw new Error("storage failed");
});
} catch {
threw = true;
}
expect(threw).toBe(true);
expect(cache.deleteCalls).toEqual([]);
expect(stats.invalidations).toBe(0);
});
it("invalidateOnWrite:false skips invalidation", {
timeout: 5000,
}, async () => {
const mw = buildMiddleware({
cache,
stats,
invalidateOnWrite: false,
});
await mw.write?.(makeCtx("x.bin"), async () => {});
expect(cache.deleteCalls).toEqual([]);
});
it("successful delete invalidates the cache entry", {
timeout: 5000,
}, async () => {
cache.store.set("drive::x.bin", new Uint8Array([1]));
const mw = buildMiddleware({ cache, stats });
const removed = await mw.delete?.(
makeCtx("x.bin", "delete"),
async () => true
);
expect(removed).toBe(true);
expect(cache.deleteCalls).toEqual(["drive::x.bin"]);
expect(stats.invalidations).toBe(1);
});
it("delete invalidates even when next() returned false", {
timeout: 5000,
}, async () => {
const mw = buildMiddleware({ cache, stats });
const removed = await mw.delete?.(
makeCtx("x.bin", "delete"),
async () => false
);
expect(removed).toBe(false);
// Per spec: "MAY still call cache.delete(key) to clean any stale entry"
expect(cache.deleteCalls).toEqual(["drive::x.bin"]);
});
it("invalidateOnDelete:false skips invalidation", {
timeout: 5000,
}, async () => {
const mw = buildMiddleware({
cache,
stats,
invalidateOnDelete: false,
});
await mw.delete?.(makeCtx("x.bin", "delete"), async () => true);
expect(cache.deleteCalls).toEqual([]);
});
it("failed delete does not invalidate", {
timeout: 5000,
}, async () => {
const mw = buildMiddleware({ cache, stats });
let threw = false;
try {
await mw.delete?.(makeCtx("x.bin", "delete"), async () => {
throw new Error("storage gone");
});
} catch {
threw = true;
}
expect(threw).toBe(true);
expect(cache.deleteCalls).toEqual([]);
});
it("cache.delete rejection on invalidation does not propagate", {
timeout: 5000,
}, async () => {
cache.throwOnDelete = new Error("redis down");
const mw = buildMiddleware({ cache, stats });
// Should NOT throw
await mw.write?.(makeCtx("x.bin"), async () => {});
expect(stats.errors).toBe(1);
expect(stats.invalidations).toBe(0);
});
it("invalidation logger captures suppressed cache.delete errors", {
timeout: 5000,
}, async () => {
cache.throwOnDelete = new Error("redis down");
const logs: string[] = [];
const logger = {
debug: () => {},
info: () => {},
error: () => {},
warn: (msg: string) => {
logs.push(msg);
},
};
const mw = buildMiddleware({ cache, stats, logger });
await mw.write?.(makeCtx("x.bin"), async () => {});
expect(logs.some((m) => m.includes("cache.delete failed"))).toBe(true);
});
});

View File

@@ -0,0 +1,88 @@
import type { AbstractCache } from "@suaveplan/cache";
import type { LoggerLike } from "@suaveplan/types";
import type { MutableStats } from "../handle/handle.js";
import { recordError, recordInvalidate } from "../otel/otel.js";
interface MiddlewareContext {
readonly operation: string;
readonly path: string;
readonly drive: unknown;
readonly [key: string]: unknown;
}
export interface BuildInvalidationArgs {
readonly cache: AbstractCache<Uint8Array>;
readonly stats: MutableStats;
readonly namespace: string;
readonly invalidateOnWrite: boolean;
readonly invalidateOnDelete: boolean;
readonly logger?: LoggerLike | undefined;
}
export interface InvalidationHooks {
write(ctx: MiddlewareContext, next: () => Promise<void>): Promise<void>;
delete(
ctx: MiddlewareContext,
next: () => Promise<boolean>
): Promise<boolean>;
}
/**
* Build the `write` and `delete` `DriveMiddleware` hooks responsible for
* post-success cache invalidation. 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.
*/
export function buildInvalidationMiddleware(
args: BuildInvalidationArgs
): InvalidationHooks {
const {
cache,
stats,
namespace,
invalidateOnWrite,
invalidateOnDelete,
logger,
} = args;
async function invalidate(path: string): Promise<void> {
const key = `${namespace}::${path}`;
try {
await cache.delete(key);
stats.invalidations += 1;
recordInvalidate(namespace, path);
} catch (cause) {
stats.errors += 1;
recordError(namespace, "delete", path);
if (logger !== undefined) {
logger.warn("drive-plugin-cache: cache.delete failed", {
path,
namespace,
cause: String(cause),
});
}
}
}
return {
async write(
ctx: MiddlewareContext,
next: () => Promise<void>
): Promise<void> {
await next();
if (invalidateOnWrite) {
await invalidate(ctx.path);
}
},
async delete(
ctx: MiddlewareContext,
next: () => Promise<boolean>
): Promise<boolean> {
const removed = await next();
if (invalidateOnDelete) {
await invalidate(ctx.path);
}
return removed;
},
};
}

View File

@@ -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.

View File

@@ -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`.

View File

@@ -0,0 +1,95 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { FakeCache } from "../../tests/setup/fake-cache.js";
import { createMutableStats } from "../handle/handle.js";
import { buildMiddleware } from "./middleware.js";
import { getSignal, isPreAborted } from "./middleware.signal.js";
interface TestCtx {
operation: string;
path: string;
drive: unknown;
[key: string]: unknown;
}
function makeCtx(path: string, signal?: AbortSignal): TestCtx {
const base: TestCtx = {
operation: "read",
path,
drive: {},
};
if (signal !== undefined) {
(base as { signal?: AbortSignal }).signal = signal;
}
return base;
}
describe("middleware.signal helpers", () => {
it("getSignal returns undefined when ctx has no signal", {
timeout: 5000,
}, () => {
expect(getSignal(makeCtx("x"))).toBeUndefined();
});
it("getSignal returns the signal when ctx carries a real AbortSignal", {
timeout: 5000,
}, () => {
const ctrl = new AbortController();
expect(getSignal(makeCtx("x", ctrl.signal))).toBe(ctrl.signal);
});
it("getSignal rejects non-AbortSignal duck shapes", {
timeout: 5000,
}, () => {
const ctx: TestCtx = {
operation: "read",
path: "x",
drive: {},
signal: { aborted: true },
};
expect(getSignal(ctx)).toBeUndefined();
});
it("isPreAborted reflects the signal state", {
timeout: 5000,
}, () => {
const ctrl = new AbortController();
expect(isPreAborted(makeCtx("x", ctrl.signal))).toBe(false);
ctrl.abort();
expect(isPreAborted(makeCtx("x", ctrl.signal))).toBe(true);
});
});
describe("middleware: AbortSignal threading", () => {
it("pre-aborted signal skips cache work", {
timeout: 5000,
}, async () => {
const cache = new FakeCache();
const stats = createMutableStats();
const mw = buildMiddleware({ cache, stats });
const ctrl = new AbortController();
ctrl.abort();
let nextCalls = 0;
await mw.read?.(makeCtx("x", ctrl.signal), async () => {
nextCalls += 1;
return new Uint8Array();
});
expect(cache.getCalls).toEqual([]);
expect(cache.setCalls).toEqual([]);
expect(nextCalls).toBe(1);
});
it("live signal is forwarded to cache operations", {
timeout: 5000,
}, async () => {
const cache = new FakeCache();
const stats = createMutableStats();
const mw = buildMiddleware({ cache, stats });
const ctrl = new AbortController();
await mw.read?.(
makeCtx("x", ctrl.signal),
async () => new Uint8Array([1])
);
expect(cache.getCalls).toHaveLength(1);
expect(cache.getCalls[0].signal).toBe(ctrl.signal);
});
});

View File

@@ -0,0 +1,35 @@
/**
* Structural view of the middleware-context fields this helper reads. Mirrors
* `MiddlewareContext` from `@suaveplan/drive` (which is not re-exported from
* the package root) without binding to the concrete shape — `path`/`drive`/
* `operation` are irrelevant for signal extraction.
*/
export interface SignalContext {
readonly [key: string]: unknown;
}
/**
* Extract the `AbortSignal` (if any) attached to a middleware context.
*
* Drive's core does not currently thread a signal into `ctx`, but the
* underlying `MiddlewareContext` shape is an open `[key: string]: unknown`
* carrier so downstream extensions / future Drive versions can. This helper
* centralises the lookup so the read/write/delete middleware code stays a
* single line.
*/
export function getSignal(ctx: SignalContext): AbortSignal | undefined {
const candidate = ctx["signal"];
if (candidate instanceof AbortSignal) return candidate;
return undefined;
}
/**
* `true` when the given context carries a signal that is already aborted at
* the moment the middleware runs. Used to short-circuit the cache work and
* defer to `next()` so the inner storage layer surfaces `DRIVE.ABORTED`
* canonically.
*/
export function isPreAborted(ctx: SignalContext): boolean {
const signal = getSignal(ctx);
return signal?.aborted ?? false;
}

View File

@@ -0,0 +1,210 @@
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
import { FakeCache } from "../../tests/setup/fake-cache.js";
import { createMutableStats, type MutableStats } from "../handle/handle.js";
import { buildMiddleware } from "./middleware.js";
interface TestCtx {
operation: string;
path: string;
drive: unknown;
[key: string]: unknown;
}
function makeCtx(path: string, extra?: Record<string, unknown>): TestCtx {
return {
operation: "read",
path,
drive: {},
...extra,
};
}
describe("middleware: read", () => {
let cache: FakeCache;
let stats: MutableStats;
beforeEach(() => {
cache = new FakeCache();
stats = createMutableStats();
});
it("hit short-circuits next() and returns cached bytes", {
timeout: 5000,
}, async () => {
const cached = new Uint8Array([10, 20, 30]);
cache.store.set("drive::clip.mov", cached);
const mw = buildMiddleware({ cache, stats });
let nextCalls = 0;
const out = await mw.read?.(makeCtx("clip.mov"), async () => {
nextCalls += 1;
return new Uint8Array([99]);
});
expect(out).toBe(cached);
expect(nextCalls).toBe(0);
expect(stats.hits).toBe(1);
expect(stats.misses).toBe(0);
});
it("uses the configured namespace in the cache key", {
timeout: 5000,
}, async () => {
const cached = new Uint8Array([1]);
cache.store.set("media::clip.mov", cached);
const mw = buildMiddleware({ cache, stats, namespace: "media" });
await mw.read?.(makeCtx("clip.mov"), async () => new Uint8Array());
expect(cache.getCalls).toEqual([
{ key: "media::clip.mov", signal: undefined },
]);
});
it("miss invokes next() and populates the cache", {
timeout: 5000,
}, async () => {
const bytes = new Uint8Array([5, 6, 7]);
const mw = buildMiddleware({ cache, stats });
const out = await mw.read?.(makeCtx("x.bin"), async () => bytes);
expect(out).toBe(bytes);
expect(stats.misses).toBe(1);
expect(stats.populations).toBe(1);
expect(cache.setCalls).toEqual([{ key: "drive::x.bin", value: bytes }]);
});
it("shouldCache predicate gates population", {
timeout: 5000,
}, async () => {
const mw = buildMiddleware({
cache,
stats,
shouldCache: () => false,
});
await mw.read?.(makeCtx("x.bin"), async () => new Uint8Array([1, 2]));
expect(cache.setCalls).toEqual([]);
expect(stats.populations).toBe(0);
expect(stats.misses).toBe(1);
});
it("default maxEntryBytes is 16 MiB", { timeout: 30000 }, async () => {
const big = new Uint8Array(16 * 1024 * 1024 + 1);
const mw = buildMiddleware({ cache, stats });
await mw.read?.(makeCtx("big.bin"), async () => big);
expect(cache.setCalls).toEqual([]);
expect(stats.populations).toBe(0);
// Default shouldCache short-circuits before the defensive
// CACHE_TOO_LARGE branch — so stats.errors stays 0.
expect(stats.errors).toBe(0);
});
it("populates entries exactly at the 16 MiB boundary", {
timeout: 30000,
}, async () => {
const boundary = new Uint8Array(16 * 1024 * 1024);
const mw = buildMiddleware({ cache, stats });
await mw.read?.(makeCtx("boundary.bin"), async () => boundary);
expect(stats.populations).toBe(1);
});
it("custom shouldCache enforcing maxEntryBytes triggers CACHE_TOO_LARGE swallow", {
timeout: 5000,
}, async () => {
// shouldCache returns true even though bytes exceed maxEntryBytes —
// middleware defensively suppresses and counts an error.
const mw = buildMiddleware({
cache,
stats,
shouldCache: () => true,
maxEntryBytes: 4,
});
await mw.read?.(
makeCtx("x.bin"),
async () => new Uint8Array([1, 2, 3, 4, 5])
);
expect(cache.setCalls).toEqual([]);
expect(stats.errors).toBe(1);
});
it("cache.get rejection falls through to next()", {
timeout: 5000,
}, async () => {
cache.throwOnGet = new Error("redis down");
const bytes = new Uint8Array([42]);
const mw = buildMiddleware({ cache, stats });
const out = await mw.read?.(makeCtx("x"), async () => bytes);
expect(out).toBe(bytes);
expect(stats.errors).toBe(1);
expect(stats.misses).toBe(1);
});
it("cache.set rejection does not propagate", {
timeout: 5000,
}, async () => {
cache.throwOnSet = new Error("disk full");
const bytes = new Uint8Array([1, 2]);
const mw = buildMiddleware({ cache, stats });
const out = await mw.read?.(makeCtx("x"), async () => bytes);
expect(out).toBe(bytes);
expect(stats.errors).toBe(1);
expect(stats.populations).toBe(0);
});
it("logger receives suppressed cache.get error context", {
timeout: 5000,
}, async () => {
cache.throwOnGet = new Error("redis down");
const logs: Array<{
msg: string;
ctx: Record<string, unknown> | undefined;
}> = [];
const logger = {
debug: () => {},
info: () => {},
error: () => {},
warn: (msg: string, ctx?: Record<string, unknown>) => {
logs.push({ msg, ctx });
},
};
const mw = buildMiddleware({ cache, stats, logger });
await mw.read?.(makeCtx("x"), async () => new Uint8Array());
expect(logs.length).toBeGreaterThanOrEqual(1);
expect(logs[0].msg).toContain("cache.get failed");
});
it("logger receives suppressed cache.set error context", {
timeout: 5000,
}, async () => {
cache.throwOnSet = new Error("disk full");
const logs: Array<{ msg: string }> = [];
const logger = {
debug: () => {},
info: () => {},
error: () => {},
warn: (msg: string) => {
logs.push({ msg });
},
};
const mw = buildMiddleware({ cache, stats, logger });
await mw.read?.(makeCtx("x"), async () => new Uint8Array([1]));
expect(logs.some((l) => l.msg.includes("cache.set failed"))).toBe(true);
});
it("stat passes through without touching the cache", {
timeout: 5000,
}, async () => {
const mw = buildMiddleware({ cache, stats });
const stat = { size: 1 };
const out = await mw.stat?.(makeCtx("x"), async () => stat);
expect(out).toBe(stat);
expect(cache.getCalls).toEqual([]);
expect(cache.setCalls).toEqual([]);
});
it("list passes through without touching the cache", {
timeout: 5000,
}, async () => {
const mw = buildMiddleware({ cache, stats });
const listing = [{ path: "a" }];
const out = await mw.list?.(makeCtx("folder/"), async () => listing);
expect(out).toBe(listing);
expect(cache.getCalls).toEqual([]);
expect(cache.setCalls).toEqual([]);
});
});

View File

@@ -0,0 +1,180 @@
import type { AbstractCache } from "@suaveplan/cache";
import type { DriveMiddleware } from "@suaveplan/drive";
import type { LoggerLike } from "@suaveplan/types";
import {
DRIVE_CACHE_CODES,
DriveCacheError,
} from "../error-codes/error-codes.js";
import type { MutableStats } from "../handle/handle.js";
import {
recordError,
recordHit,
recordMiss,
recordPopulate,
} from "../otel/otel.js";
import { buildInvalidationMiddleware } from "./middleware.invalidation.js";
import { getSignal, isPreAborted } from "./middleware.signal.js";
/**
* Structural copy of `MiddlewareContext` from `@suaveplan/drive`. The drive
* package does not currently re-export this type from its root barrel, so we
* mirror the shape here. It is a structural match — any object compatible
* with drive's `MiddlewareContext` is compatible with this one.
*/
interface MiddlewareContext {
readonly operation: string;
readonly path: string;
readonly drive: unknown;
readonly [key: string]: unknown;
}
const DEFAULT_NAMESPACE = "drive";
const DEFAULT_MAX_ENTRY_BYTES = 16 * 1024 * 1024;
export interface BuildMiddlewareArgs {
readonly cache: AbstractCache<Uint8Array>;
readonly stats: MutableStats;
readonly namespace?: string | undefined;
readonly invalidateOnWrite?: boolean | undefined;
readonly invalidateOnDelete?: boolean | undefined;
readonly shouldCache?:
| ((path: string, bytes: Uint8Array) => boolean)
| undefined;
readonly maxEntryBytes?: number | undefined;
readonly logger?: LoggerLike | undefined;
}
/**
* Build the `DriveMiddleware` registered on a `Drive` by `driveCachePlugin`.
*
* Wires the read short-circuit / populate-on-miss behaviour with the
* invalidation hooks for write and delete. `stat` and `list` are pure
* passthroughs in this iteration.
*/
export function buildMiddleware(args: BuildMiddlewareArgs): DriveMiddleware {
const namespace = args.namespace ?? DEFAULT_NAMESPACE;
const maxEntryBytes = args.maxEntryBytes ?? DEFAULT_MAX_ENTRY_BYTES;
const shouldCachePredicate =
args.shouldCache ??
((_path: string, bytes: Uint8Array): boolean =>
bytes.byteLength <= maxEntryBytes);
const cache = args.cache;
const stats = args.stats;
const logger = args.logger;
const invalidation = buildInvalidationMiddleware({
cache,
stats,
namespace,
invalidateOnWrite: args.invalidateOnWrite ?? true,
invalidateOnDelete: args.invalidateOnDelete ?? true,
logger,
});
return {
async read(
ctx: MiddlewareContext,
next: () => Promise<Uint8Array>
): Promise<Uint8Array> {
if (isPreAborted(ctx)) {
return next();
}
const key = `${namespace}::${ctx.path}`;
const signal = getSignal(ctx);
let cached: Uint8Array | undefined;
try {
cached = await invokeCacheGet(cache, key, signal);
} catch (cause) {
stats.errors += 1;
recordError(namespace, "get", ctx.path);
logCacheError(logger, "get", ctx.path, namespace, cause);
cached = undefined;
}
if (cached !== undefined) {
stats.hits += 1;
recordHit(namespace, ctx.path);
return cached;
}
stats.misses += 1;
recordMiss(namespace, ctx.path);
const bytes = await next();
if (shouldCachePredicate(ctx.path, bytes)) {
try {
if (bytes.byteLength > maxEntryBytes) {
throw new DriveCacheError(
DRIVE_CACHE_CODES.CACHE_TOO_LARGE,
"cached entry exceeds configured maxEntryBytes",
{
path: ctx.path,
namespace,
byteLength: bytes.byteLength,
maxEntryBytes,
}
);
}
await cache.set(key, bytes);
stats.populations += 1;
recordPopulate(namespace, ctx.path, bytes.byteLength);
} catch (cause) {
stats.errors += 1;
recordError(namespace, "set", ctx.path);
logCacheError(logger, "set", ctx.path, namespace, cause);
}
}
return bytes;
},
write: invalidation.write,
delete: invalidation.delete,
async stat(
_ctx: MiddlewareContext,
next: () => Promise<unknown | null>
): Promise<unknown | null> {
return next();
},
async list(
_ctx: MiddlewareContext,
next: () => Promise<unknown>
): Promise<unknown> {
return next();
},
};
}
/**
* Call `cache.get(key)`, optionally forwarding an `AbortSignal` to caches
* whose `get` accepts a second argument. The `AbstractCache` base contract
* is single-arg, but concrete implementations may extend it; the cast keeps
* the call site type-safe while still threading the signal where supported.
*/
async function invokeCacheGet(
cache: AbstractCache<Uint8Array>,
key: string,
signal: AbortSignal | undefined
): Promise<Uint8Array | undefined> {
if (signal === undefined) return cache.get(key);
const extended = cache as unknown as {
get(k: string, s?: AbortSignal): Promise<Uint8Array | undefined>;
};
return extended.get(key, signal);
}
function logCacheError(
logger: LoggerLike | undefined,
op: "get" | "set",
path: string,
namespace: string,
cause: unknown
): void {
if (logger === undefined) return;
logger.warn(`drive-plugin-cache: cache.${op} failed`, {
path,
namespace,
cause: String(cause),
});
}

View File

@@ -0,0 +1,15 @@
export type {
DriveCacheInstruments,
DriveCacheOp,
DriveCacheOutcome,
} from "./otel.js";
export {
getInstruments,
recordError,
recordHit,
recordInvalidate,
recordMiss,
recordPopulate,
resetInstruments,
setDriveCacheMeterProvider,
} from "./otel.js";

View 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.

View File

@@ -0,0 +1,171 @@
import type {
Counter,
Histogram,
Meter,
MeterProvider,
} from "@opentelemetry/api";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import {
getInstruments,
recordError,
recordHit,
recordInvalidate,
recordMiss,
recordPopulate,
resetInstruments,
setDriveCacheMeterProvider,
} from "./otel.js";
interface Sample {
value: number;
attrs: Record<string, unknown> | undefined;
}
class StubCounter implements Counter {
public adds: Sample[] = [];
public add(v: number, attrs?: Record<string, unknown>): void {
this.adds.push({ value: v, attrs });
}
}
class StubHistogram implements Histogram {
public samples: Sample[] = [];
public record(v: number, attrs?: Record<string, unknown>): void {
this.samples.push({ value: v, attrs });
}
}
class StubMeter implements Meter {
public counters = new Map<string, StubCounter>();
public histograms = new Map<string, StubHistogram>();
public createCounter(name: string): Counter {
const c = new StubCounter();
this.counters.set(name, c);
return c;
}
public createHistogram(name: string): Histogram {
const h = new StubHistogram();
this.histograms.set(name, h);
return h;
}
public createUpDownCounter(): never {
throw new Error("unused");
}
public createObservableCounter(): never {
throw new Error("unused");
}
public createObservableUpDownCounter(): never {
throw new Error("unused");
}
public createObservableGauge(): never {
throw new Error("unused");
}
public addBatchObservableCallback(): void {}
public removeBatchObservableCallback(): void {}
public createGauge(): never {
throw new Error("unused");
}
}
class StubMeterProvider implements MeterProvider {
public meter = new StubMeter();
public getMeter(): Meter {
return this.meter;
}
}
describe("drive-plugin-cache OTel", () => {
let provider: StubMeterProvider;
beforeEach(() => {
provider = new StubMeterProvider();
setDriveCacheMeterProvider(provider);
});
afterEach(() => {
setDriveCacheMeterProvider(undefined);
resetInstruments();
});
it("hit increments hits_total and records hit-outcome span", {
timeout: 5000,
}, () => {
recordHit("drive", "clip.mov");
const counter = provider.meter.counters.get("drive_cache.hits_total");
expect(counter?.adds).toEqual([
{ value: 1, attrs: { namespace: "drive" } },
]);
});
it("miss increments misses_total", { timeout: 5000 }, () => {
recordMiss("drive", "x.bin");
const counter = provider.meter.counters.get("drive_cache.misses_total");
expect(counter?.adds[0]).toEqual({
value: 1,
attrs: { namespace: "drive" },
});
});
it("population records entry_bytes histogram", {
timeout: 5000,
}, () => {
recordPopulate("drive", "x.bin", 4096);
const counter = provider.meter.counters.get(
"drive_cache.populations_total"
);
expect(counter?.adds[0].value).toBe(1);
const hist = provider.meter.histograms.get("drive_cache.entry_bytes");
expect(hist?.samples).toEqual([
{ value: 4096, attrs: { namespace: "drive" } },
]);
});
it("invalidate increments invalidations_total", {
timeout: 5000,
}, () => {
recordInvalidate("media", "clip.mov");
const counter = provider.meter.counters.get(
"drive_cache.invalidations_total"
);
expect(counter?.adds[0]).toEqual({
value: 1,
attrs: { namespace: "media" },
});
});
it("cache error increments errors_total with op label", {
timeout: 5000,
}, () => {
recordError("drive", "set", "x.bin");
const counter = provider.meter.counters.get("drive_cache.errors_total");
expect(counter?.adds).toEqual([
{ value: 1, attrs: { namespace: "drive", op: "set" } },
]);
});
it("getInstruments caches across calls and rebuilds when forced", {
timeout: 5000,
}, () => {
const a = getInstruments();
const b = getInstruments();
expect(a).toBe(b);
const c = getInstruments(true);
expect(c).not.toBe(a);
});
it("setDriveCacheMeterProvider(undefined) reverts to global metrics", {
timeout: 5000,
}, () => {
setDriveCacheMeterProvider(undefined);
resetInstruments();
// Should not throw — global metrics returns a real (noop) meter.
const inst = getInstruments();
expect(typeof inst.hitsTotal.add).toBe("function");
});
});

View File

@@ -0,0 +1,180 @@
import {
type Counter,
type Histogram,
type Meter,
type MeterProvider,
metrics,
type Tracer,
trace,
} from "@opentelemetry/api";
const METER_NAME = "@suaveplan/drive-cache";
const TRACER_NAME = "@suaveplan/drive-cache";
let overrideMeterProvider: MeterProvider | undefined;
/**
* Override the `MeterProvider` used to acquire the drive-cache meter. Pass
* `undefined` to revert to the global provider. Resets the cached instrument
* bag so the next access binds to the new provider.
*/
export function setDriveCacheMeterProvider(
provider: MeterProvider | undefined
): void {
overrideMeterProvider = provider;
cachedInstruments = undefined;
}
export type DriveCacheOp = "get" | "set" | "delete";
export type DriveCacheOutcome =
| "hit"
| "miss"
| "populated"
| "invalidated"
| "error";
export interface DriveCacheInstruments {
readonly tracer: Tracer;
readonly meter: Meter;
readonly hitsTotal: Counter;
readonly missesTotal: Counter;
readonly populationsTotal: Counter;
readonly invalidationsTotal: Counter;
readonly errorsTotal: Counter;
readonly entryBytes: Histogram;
}
let cachedInstruments: DriveCacheInstruments | undefined;
/** Lazily build (or return cached) instruments bound to the active provider. */
export function getInstruments(force = false): DriveCacheInstruments {
if (cachedInstruments !== undefined && !force) return cachedInstruments;
const tracer = trace.getTracer(TRACER_NAME);
const meter = (overrideMeterProvider ?? metrics).getMeter(METER_NAME);
cachedInstruments = {
tracer,
meter,
hitsTotal: meter.createCounter("drive_cache.hits_total", {
description: "Cache hits served by drive-plugin-cache middleware",
}),
missesTotal: meter.createCounter("drive_cache.misses_total", {
description:
"Cache misses observed by drive-plugin-cache middleware",
}),
populationsTotal: meter.createCounter("drive_cache.populations_total", {
description: "Cache populations performed after a successful miss",
}),
invalidationsTotal: meter.createCounter(
"drive_cache.invalidations_total",
{
description:
"Cache invalidations performed after a successful write/delete or explicit handle.invalidate(path)",
}
),
errorsTotal: meter.createCounter("drive_cache.errors_total", {
description:
"Suppressed underlying cache errors attributed by op (get/set/delete)",
}),
entryBytes: meter.createHistogram("drive_cache.entry_bytes", {
description:
"Distribution of cached entry sizes observed on populations",
unit: "By",
}),
};
return cachedInstruments;
}
/** Reset cached instruments — used by tests between provider swaps. */
export function resetInstruments(): void {
cachedInstruments = undefined;
}
/** Increment `drive_cache.hits_total{namespace}` and start an outcome=hit span. */
export function recordHit(namespace: string, path: string): void {
const inst = getInstruments();
inst.hitsTotal.add(1, { namespace });
inst.tracer
.startSpan("drive_cache.read", {
attributes: {
"drive.cache.namespace": namespace,
"drive.cache.path": path,
"drive.cache.outcome": "hit",
},
})
.end();
}
/** Increment `drive_cache.misses_total{namespace}` and emit an outcome=miss span. */
export function recordMiss(namespace: string, path: string): void {
const inst = getInstruments();
inst.missesTotal.add(1, { namespace });
inst.tracer
.startSpan("drive_cache.read", {
attributes: {
"drive.cache.namespace": namespace,
"drive.cache.path": path,
"drive.cache.outcome": "miss",
},
})
.end();
}
/**
* Increment `drive_cache.populations_total{namespace}`, record the entry size
* in `drive_cache.entry_bytes{namespace}`, and emit an outcome=populated span.
*/
export function recordPopulate(
namespace: string,
path: string,
byteLength: number
): void {
const inst = getInstruments();
inst.populationsTotal.add(1, { namespace });
inst.entryBytes.record(byteLength, { namespace });
inst.tracer
.startSpan("drive_cache.populate", {
attributes: {
"drive.cache.namespace": namespace,
"drive.cache.path": path,
"drive.cache.outcome": "populated",
"drive.cache.bytes": byteLength,
},
})
.end();
}
/** Increment `drive_cache.invalidations_total{namespace}` and emit an outcome=invalidated span. */
export function recordInvalidate(namespace: string, path: string): void {
const inst = getInstruments();
inst.invalidationsTotal.add(1, { namespace });
inst.tracer
.startSpan("drive_cache.invalidate", {
attributes: {
"drive.cache.namespace": namespace,
"drive.cache.path": path,
"drive.cache.outcome": "invalidated",
},
})
.end();
}
/** Increment `drive_cache.errors_total{namespace, op}` and emit an outcome=error span. */
export function recordError(
namespace: string,
op: DriveCacheOp,
path: string
): void {
const inst = getInstruments();
inst.errorsTotal.add(1, { namespace, op });
inst.tracer
.startSpan("drive_cache.error", {
attributes: {
"drive.cache.namespace": namespace,
"drive.cache.path": path,
"drive.cache.outcome": "error",
"drive.cache.op": op,
},
})
.end();
}

View File

@@ -0,0 +1 @@
export { driveCachePlugin } from "./plugin.js";

View File

@@ -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.

View File

@@ -0,0 +1,126 @@
import type { DriveLike, DriveMiddleware, DrivePlugin } from "@suaveplan/drive";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { FakeCache } from "../../tests/setup/fake-cache.js";
import {
DRIVE_CACHE_CODES,
DriveCacheError,
} from "../error-codes/error-codes.js";
import type {
DriveCacheHandle,
DriveCacheOptions,
} from "../types/options.types.js";
import { driveCachePlugin } from "./plugin.js";
class FakeDrive implements Pick<DriveLike, "useMiddleware"> {
public middlewares: DriveMiddleware[] = [];
public throwOnUseMiddleware: Error | undefined;
public useMiddleware(mw: DriveMiddleware): void {
if (this.throwOnUseMiddleware !== undefined)
throw this.throwOnUseMiddleware;
this.middlewares.push(mw);
}
}
function asDrive(d: FakeDrive): DriveLike {
return d as unknown as DriveLike;
}
async function installPlugin(
plugin: DrivePlugin<DriveCacheHandle>,
drive: FakeDrive
): Promise<DriveCacheHandle> {
return await plugin.install(asDrive(drive));
}
describe("driveCachePlugin", () => {
it("install registers a DriveMiddleware on the drive", {
timeout: 5000,
}, async () => {
const cache = new FakeCache();
const drive = new FakeDrive();
const plugin = driveCachePlugin({ cache });
await installPlugin(plugin, drive);
expect(drive.middlewares).toHaveLength(1);
const mw = drive.middlewares[0];
expect(typeof mw.read).toBe("function");
expect(typeof mw.write).toBe("function");
expect(typeof mw.delete).toBe("function");
});
it("install returns a DriveCacheHandle with the same cache reference", {
timeout: 5000,
}, async () => {
const cache = new FakeCache();
const drive = new FakeDrive();
const plugin = driveCachePlugin({ cache });
const handle = await installPlugin(plugin, drive);
expect(handle.cache).toBe(cache);
expect(typeof handle.getStats).toBe("function");
expect(typeof handle.invalidate).toBe("function");
expect(typeof handle.clear).toBe("function");
});
it("exposes a stable plugin name", { timeout: 5000 }, () => {
const cache = new FakeCache();
const plugin = driveCachePlugin({ cache });
expect(plugin.name).toBe("@suaveplan/drive-plugin-cache");
});
it("throws INVALID_OPTIONS when cache is missing", {
timeout: 5000,
}, () => {
expect(() =>
driveCachePlugin({} as unknown as DriveCacheOptions)
).toThrow(DriveCacheError);
try {
driveCachePlugin({} as unknown as DriveCacheOptions);
} catch (err) {
expect((err as DriveCacheError).code).toBe(
DRIVE_CACHE_CODES.INVALID_OPTIONS
);
}
});
it("throws INVALID_OPTIONS when options is null", { timeout: 5000 }, () => {
expect(() =>
driveCachePlugin(null as unknown as DriveCacheOptions)
).toThrow(DriveCacheError);
});
it("throws INVALID_OPTIONS when cache lacks get/set/delete", {
timeout: 5000,
}, () => {
expect(() =>
driveCachePlugin({
cache: { name: "x" } as unknown as FakeCache,
})
).toThrow(DriveCacheError);
try {
driveCachePlugin({
cache: { name: "x" } as unknown as FakeCache,
});
} catch (err) {
expect((err as DriveCacheError).code).toBe(
DRIVE_CACHE_CODES.INVALID_OPTIONS
);
}
});
it("wraps drive.useMiddleware throws as INSTALL_FAILED", {
timeout: 5000,
}, async () => {
const cache = new FakeCache();
const drive = new FakeDrive();
drive.throwOnUseMiddleware = new Error("registry refused");
const plugin = driveCachePlugin({ cache });
let caught: DriveCacheError | undefined;
try {
await installPlugin(plugin, drive);
} catch (err) {
caught = err as DriveCacheError;
}
expect(caught).toBeInstanceOf(DriveCacheError);
expect(caught?.code).toBe(DRIVE_CACHE_CODES.INSTALL_FAILED);
expect((caught?.cause as Error).message).toBe("registry refused");
});
});

View File

@@ -0,0 +1,88 @@
import type { DriveLike, DrivePlugin } from "@suaveplan/drive";
import {
DRIVE_CACHE_CODES,
DriveCacheError,
} from "../error-codes/error-codes.js";
import { createHandle, createMutableStats } from "../handle/handle.js";
import { buildMiddleware } from "../middleware/middleware.js";
import type {
DriveCacheHandle,
DriveCacheOptions,
} from "../types/options.types.js";
const PLUGIN_NAME = "@suaveplan/drive-plugin-cache";
const DEFAULT_NAMESPACE = "drive";
/**
* Build a `DrivePlugin` that registers a read-through cache middleware on the
* supplied `DriveLike`. The plugin owns the middleware lifecycle and surfaces
* a `DriveCacheHandle` for stats / explicit invalidation / clear.
*/
export function driveCachePlugin(
options: DriveCacheOptions
): DrivePlugin<DriveCacheHandle> {
validateOptions(options);
const namespace = options.namespace ?? DEFAULT_NAMESPACE;
const stats = createMutableStats();
const middleware = buildMiddleware({
cache: options.cache,
stats,
namespace,
invalidateOnWrite: options.invalidateOnWrite,
invalidateOnDelete: options.invalidateOnDelete,
shouldCache: options.shouldCache,
maxEntryBytes: options.maxEntryBytes,
logger: options.logger,
});
const handle = createHandle({ cache: options.cache, stats, namespace });
return {
name: PLUGIN_NAME,
install(drive: DriveLike): DriveCacheHandle {
try {
drive.useMiddleware(middleware);
} catch (cause) {
throw new DriveCacheError(
DRIVE_CACHE_CODES.INSTALL_FAILED,
"Drive.useMiddleware rejected the cache middleware",
{ namespace, cause }
);
}
return handle;
},
};
}
function validateOptions(options: DriveCacheOptions): void {
if (options === null || typeof options !== "object") {
throw new DriveCacheError(
DRIVE_CACHE_CODES.INVALID_OPTIONS,
"driveCachePlugin requires an options object with a cache",
{ reason: "options" }
);
}
const cache = (options as { cache?: unknown }).cache;
if (cache === undefined || cache === null) {
throw new DriveCacheError(
DRIVE_CACHE_CODES.INVALID_OPTIONS,
"driveCachePlugin requires options.cache",
{ reason: "cache" }
);
}
const candidate = cache as {
get?: unknown;
set?: unknown;
delete?: unknown;
};
if (
typeof candidate.get !== "function" ||
typeof candidate.set !== "function" ||
typeof candidate.delete !== "function"
) {
throw new DriveCacheError(
DRIVE_CACHE_CODES.INVALID_OPTIONS,
"options.cache must expose function-typed get/set/delete methods",
{ reason: "cache" }
);
}
}

View File

@@ -0,0 +1,5 @@
export type {
DriveCacheHandle,
DriveCacheOptions,
DriveCacheStats,
} from "./options.types.js";

View File

@@ -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.

View File

@@ -0,0 +1,54 @@
import type { AbstractCache } from "@suaveplan/cache";
import type { LoggerLike } from "@suaveplan/types";
/**
* Options accepted by `driveCachePlugin(options)`.
*
* The `cache` field is the only required field — everything else has a
* conservative default. The cache instance is opaque to the plugin: the
* caller picks the backend (memory / storage / redis / custom), the
* eviction policy, the TTL, the namespacing, and the lifecycle events.
*/
export interface DriveCacheOptions {
readonly cache: AbstractCache<Uint8Array>;
/** Invalidate the cache entry after a successful `drive.write` (default: `true`). */
readonly invalidateOnWrite?: boolean;
/** Invalidate the cache entry after a successful `drive.delete` (default: `true`). */
readonly invalidateOnDelete?: boolean;
/** Namespace prefix combined into the cache key as `${namespace}::${path}` (default: `"drive"`). */
readonly namespace?: string;
/** Predicate gating cache population on a read-miss. Defaults to a 16-MiB ceiling. */
readonly shouldCache?: (path: string, bytes: Uint8Array) => boolean;
/** Hard byte ceiling used by the default `shouldCache` predicate (default: `16 * 1024 * 1024`). */
readonly maxEntryBytes?: number;
/** Optional logger receiving suppressed cache errors. */
readonly logger?: LoggerLike;
}
/**
* Monotonic per-handle counters exposed by `DriveCacheHandle.getStats()`.
*
* Snapshots are immutable — `getStats()` returns a shallow copy so callers
* cannot mutate the live counter object.
*/
export interface DriveCacheStats {
readonly hits: number;
readonly misses: number;
readonly populations: number;
readonly invalidations: number;
readonly errors: number;
}
/**
* Handle returned from `drive.usePlugin(driveCachePlugin(options))`.
*
* Exposes the underlying cache reference for advanced wiring (e.g.
* `cache.wrap(key, loader, { staleAge })`), a stats snapshot, a single-path
* invalidator, and a `clear()` shortcut over the underlying cache.
*/
export interface DriveCacheHandle {
readonly cache: AbstractCache<Uint8Array>;
getStats(): DriveCacheStats;
invalidate(path: string): Promise<void>;
clear(): Promise<void>;
}

View File

@@ -0,0 +1,65 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { MemoryCache } from "@suaveplan/cache";
import { Drive } from "@suaveplan/drive";
import { createNodeFsStorage } from "@suaveplan/storage-file";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import { driveCachePlugin } from "../../src/plugin/plugin.js";
describe("drive-plugin-cache integration: MemoryCache", () => {
let rootDir: string;
let drive: Drive;
beforeEach(async () => {
rootDir = mkdtempSync(join(tmpdir(), "drive-plugin-cache-mem-"));
const fileStorage = createNodeFsStorage({ rootDir, mkdirp: true });
const storage = Object.assign(fileStorage, { id: "default" });
drive = new Drive(storage);
await drive.initialize();
});
afterEach(async () => {
await drive.dispose();
rmSync(rootDir, { recursive: true, force: true });
});
it("second read of the same path is served from the cache", {
timeout: 30000,
}, async () => {
const cache = new MemoryCache<Uint8Array>({ maxSize: 100 });
const handle = await drive.usePlugin(driveCachePlugin({ cache }));
const payload = new Uint8Array([0x4d, 0x4f, 0x56, 0x21]);
await drive.write("clip.mov", payload);
const bytes1 = await drive.read("clip.mov");
const bytes2 = await drive.read("clip.mov");
expect(Array.from(bytes1)).toEqual(Array.from(payload));
expect(Array.from(bytes2)).toEqual(Array.from(payload));
const stats = handle.getStats();
expect(stats.hits).toBe(1);
expect(stats.misses).toBe(1);
expect(stats.populations).toBe(1);
});
it("write invalidates a previously populated entry", {
timeout: 30000,
}, async () => {
const cache = new MemoryCache<Uint8Array>({ maxSize: 100 });
const handle = await drive.usePlugin(driveCachePlugin({ cache }));
await drive.write("x.bin", new Uint8Array([1, 2, 3]));
await drive.read("x.bin"); // populates
await drive.write("x.bin", new Uint8Array([9, 9, 9])); // invalidates
const fresh = await drive.read("x.bin");
expect(Array.from(fresh)).toEqual([9, 9, 9]);
// Two writes → two post-success invalidations. The first targets an
// unpopulated key (idempotent on the cache); the second actually
// evicts the populated entry produced by the intermediate read.
expect(handle.getStats().invalidations).toBe(2);
});
});

View File

@@ -0,0 +1,75 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { MemoryCache } from "@suaveplan/cache";
import { Drive } from "@suaveplan/drive";
import { createNodeFsStorage } from "@suaveplan/storage-file";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import { driveCachePlugin } from "../../src/plugin/plugin.js";
describe("drive-plugin-cache integration: shared cache, distinct namespaces", () => {
let rootA: string;
let rootB: string;
let driveA: Drive;
let driveB: Drive;
beforeEach(async () => {
rootA = mkdtempSync(join(tmpdir(), "drive-plugin-cache-a-"));
rootB = mkdtempSync(join(tmpdir(), "drive-plugin-cache-b-"));
const storageA = Object.assign(
createNodeFsStorage({ rootDir: rootA, mkdirp: true }),
{ id: "default" }
);
const storageB = Object.assign(
createNodeFsStorage({ rootDir: rootB, mkdirp: true }),
{ id: "default" }
);
driveA = new Drive(storageA);
driveB = new Drive(storageB);
await driveA.initialize();
await driveB.initialize();
});
afterEach(async () => {
await driveA.dispose();
await driveB.dispose();
rmSync(rootA, { recursive: true, force: true });
rmSync(rootB, { recursive: true, force: true });
});
it("distinct namespaces isolate entries on a shared cache", {
timeout: 30000,
}, async () => {
const cache = new MemoryCache<Uint8Array>({ maxSize: 100 });
const handleA = await driveA.usePlugin(
driveCachePlugin({ cache, namespace: "a" })
);
const handleB = await driveB.usePlugin(
driveCachePlugin({ cache, namespace: "b" })
);
await driveA.write("clip.mov", new Uint8Array([1]));
await driveB.write("clip.mov", new Uint8Array([2]));
// Populate A's entry under namespace "a"
await driveA.read("clip.mov");
// B's read for the same path under namespace "b" must miss
await driveB.read("clip.mov");
expect(handleA.getStats().populations).toBe(1);
expect(handleB.getStats().populations).toBe(1);
expect(handleA.getStats().hits).toBe(0);
expect(handleB.getStats().hits).toBe(0);
// Verify keys are distinct on the shared cache
expect(await cache.get("a::clip.mov")).toBeDefined();
expect(await cache.get("b::clip.mov")).toBeDefined();
expect(Array.from((await cache.get("a::clip.mov")) ?? [])).toEqual([1]);
expect(Array.from((await cache.get("b::clip.mov")) ?? [])).toEqual([2]);
});
});

View File

@@ -0,0 +1,70 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { IdentityCodec, StorageCache } from "@suaveplan/cache-storage";
import { Drive } from "@suaveplan/drive";
import { createNodeFsStorage } from "@suaveplan/storage-file";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import { driveCachePlugin } from "../../src/plugin/plugin.js";
describe("drive-plugin-cache integration: StorageCache over storage-file", () => {
let driveRoot: string;
let cacheRoot: string;
beforeEach(() => {
driveRoot = mkdtempSync(join(tmpdir(), "drive-plugin-cache-drive-"));
cacheRoot = mkdtempSync(join(tmpdir(), "drive-plugin-cache-disk-"));
});
afterEach(() => {
rmSync(driveRoot, { recursive: true, force: true });
rmSync(cacheRoot, { recursive: true, force: true });
});
it("disk-backed cache survives plugin reinstall", {
timeout: 30000,
}, async () => {
const fileStorage = createNodeFsStorage({
rootDir: driveRoot,
mkdirp: true,
});
const storage = Object.assign(fileStorage, { id: "default" });
const drive = new Drive(storage);
await drive.initialize();
const cacheStorage = createNodeFsStorage({
rootDir: cacheRoot,
mkdirp: true,
});
const cache1 = new StorageCache<Uint8Array>({
storage: cacheStorage,
codec: new IdentityCodec(),
});
const handle1 = await drive.usePlugin(
driveCachePlugin({ cache: cache1 })
);
const payload = new Uint8Array([10, 20, 30, 40]);
await drive.write("clip.bin", payload);
await drive.read("clip.bin"); // miss + populate
expect(handle1.getStats().populations).toBe(1);
// Build a fresh plugin/cache instance pointing at the same on-disk
// cache directory. The previously-populated entry must rehydrate.
const cache2 = new StorageCache<Uint8Array>({
storage: cacheStorage,
codec: new IdentityCodec(),
});
const fresh = await cache2.get("drive::clip.bin");
expect(fresh).toBeDefined();
expect(Array.from(fresh ?? new Uint8Array())).toEqual([10, 20, 30, 40]);
await drive.dispose();
});
});

View File

@@ -0,0 +1,64 @@
import type { CacheSetOptions, CacheStats } from "@suaveplan/cache";
import { AbstractCache } from "@suaveplan/cache";
/**
* In-memory `AbstractCache<Uint8Array>` used by every middleware/handle/plugin
* unit test. Tracks every call shape (key, value, signal) so tests can assert
* the exact arguments the middleware forwarded into the cache surface.
*
* This is a real class, not a mock — no `vi.mock`/`jest.mock` involved.
*/
export class FakeCache extends AbstractCache<Uint8Array> {
public store = new Map<string, Uint8Array>();
public getCalls: { key: string; signal?: AbortSignal }[] = [];
public setCalls: { key: string; value: Uint8Array }[] = [];
public deleteCalls: string[] = [];
public clearCalls = 0;
public throwOnGet: Error | undefined;
public throwOnSet: Error | undefined;
public throwOnDelete: Error | undefined;
public async get(
key: string,
signal?: AbortSignal
): Promise<Uint8Array | undefined> {
this.getCalls.push({ key, signal });
if (this.throwOnGet !== undefined) throw this.throwOnGet;
return this.store.get(key);
}
public async set(
key: string,
value: Uint8Array,
_options?: CacheSetOptions
): Promise<void> {
this.setCalls.push({ key, value });
if (this.throwOnSet !== undefined) throw this.throwOnSet;
this.store.set(key, value);
}
public async delete(key: string): Promise<boolean> {
this.deleteCalls.push(key);
if (this.throwOnDelete !== undefined) throw this.throwOnDelete;
return this.store.delete(key);
}
public async clear(): Promise<void> {
this.clearCalls += 1;
this.store.clear();
}
public async has(key: string): Promise<boolean> {
return this.store.has(key);
}
public getStats(): CacheStats {
return {
hits: 0,
misses: 0,
size: this.store.size,
hitRate: 0,
evictions: 0,
};
}
}

View File

@@ -0,0 +1,5 @@
// Polyfills for the drive-plugin-cache test environment. Node 18+ provides
// `URL`, `AbortController`, `ReadableStream`, and `Response` natively, so no
// runtime polyfills are required. File exists so vitest has a stable setup
// entry mirroring sibling drive packages.
export {};

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts", "tests/**/*.ts"],
"exclude": ["dist", "node_modules"]
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,37 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir: "../../../node_modules/.vite/packages/drive-plugin-cache",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: { entry: "src/index.ts", formats: ["es"] },
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [
"@opentelemetry/api",
"@suaveplan/cache",
"@suaveplan/drive",
"@suaveplan/error",
"@suaveplan/types",
"tslib",
],
},
},
});

View File

@@ -0,0 +1,32 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: false,
environment: "node",
pool: "forks",
setupFiles: ["./tests/setup/polyfills.ts"],
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
exclude: ["dist/**", "node_modules/**"],
coverage: {
provider: "istanbul",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.ts"],
exclude: [
"src/**/*.test.ts",
"src/**/*.types.ts",
"src/**/index.ts",
"src/**/*.bench.ts",
"src/__benchmarks__/**",
"tests/**",
],
thresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
},
testTimeout: 30000,
},
});