Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@suaveplan/drive-plugin-cache - Read-through cache middleware for @suaveplan/drive. Accepts a caller-supplied AbstractCache 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. Accepts a caller-supplied AbstractCache<Uint8Array> (from @suaveplan/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.
Documentation tree
- error-codes —
error-codes.tsdefines the structured error surface for@suaveplan/drive-plugin-cache. It exports the `DRIVE_CACHE_…- error-codes —
error-codes.tsdefines the structured error surface for@suaveplan/drive-plugin-cache. It exports the `DRIVE_CACHE_…
- error-codes —
- handle —
handle.tsbuilds theDriveCacheHandlereturned bydriveCachePlugin.install(drive). The handle exposes a stats sna…- handle —
handle.tsbuilds theDriveCacheHandlereturned bydriveCachePlugin.install(drive). The handle exposes a stats sna…
- handle —
- middleware —
middleware.tsbuilds theDriveMiddlewareregistered on aDrivebydriveCachePlugin. The middleware intercepts `…- middleware.invalidation —
middleware.invalidation.tsprovides thewriteanddeletehooks of theDriveMiddlewarebuilt inmiddleware.ts.… - middleware —
middleware.tsbuilds theDriveMiddlewareregistered on aDrivebydriveCachePlugin. The middleware intercepts `… - middleware.signal —
middleware.signal.tsprovides two tinyAbortSignalhelpers used by the drive-cache middleware:getSignal(ctx)ext…
- middleware.invalidation —
- otel —
otel.tswires the@suaveplan/drive-cachemeter and tracer used by every middleware intercept in `@suaveplan/drive-p…- otel —
otel.tswires the@suaveplan/drive-cachemeter and tracer used by every middleware intercept in `@suaveplan/drive-p…
- otel —
- plugin —
plugin.tsis the public entry point of@suaveplan/drive-plugin-cache. It exports the `driveCachePlugin(options): Dr…- plugin —
plugin.tsis the public entry point of@suaveplan/drive-plugin-cache. It exports the `driveCachePlugin(options): Dr…
- plugin —
Modules
plugin—driveCachePlugin(options): DrivePlugin<DriveCacheHandle>factory.middleware— the read intercept (hit short-circuit + populate-on-miss).middleware.invalidation— write/delete invalidation hooks.middleware.signal—AbortSignalhelpers.handle—DriveCacheHandlebuilder.otel—@suaveplan/drive-cachemeter + tracer wiring.error-codes—DRIVE_CACHE_CODESregistry andDriveCacheError.types— public type surface.
Quickstart
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)
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)
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)
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)
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()
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.listresults. - Range / streaming reads (
Drive.createReadStream) — bypass middleware by design. - Per-tenant key partitioning (
keyFn(ctx)extension). - Built-in
wrap()integration — consumers compose viahandle.cache.wrap(...).
See the design proposal for the full design rationale.