Files
2026-05-27 23:29:53 +01:00

14 KiB

Capability Spec: drive-plugin-cache

Package: @suaveplan/drive-plugin-cache

Every requirement carries a **Verified by:** line.

Requirements

Requirement: driveCachePlugin(options) factory returns a DrivePlugin

The package SHALL export a driveCachePlugin(options): DrivePlugin<DriveCacheHandle> factory. The returned plugin's install(drive) SHALL register a DriveMiddleware on the supplied DriveLike via drive.useMiddleware(...) and SHALL return a DriveCacheHandle exposing getStats(), invalidate(path), clear(), and the cache reference passed in options.

Scenario: install registers middleware on the drive

  • WHEN drive.usePlugin(driveCachePlugin({ cache })) is called against a real Drive instance
  • THEN drive.useMiddleware is invoked exactly once with an object exposing read, write, and delete middleware methods

Scenario: install returns a DriveCacheHandle with the same cache reference

  • WHEN drive.usePlugin(driveCachePlugin({ cache })) resolves to a handle
  • THEN handle.cache === cache (same reference), typeof handle.getStats === "function", typeof handle.invalidate === "function", typeof handle.clear === "function"

Verified by: src/plugin/plugin.test.ts::"install registers a DriveMiddleware on the drive" AND src/plugin/plugin.test.ts::"install returns a DriveCacheHandle with the same cache reference"


Requirement: Construction-time validation of required options

The driveCachePlugin(options) factory SHALL throw DRIVE_CACHE.INVALID_OPTIONS synchronously when options.cache is missing or is not a function-typed get/set/delete carrier. The factory MUST NOT defer this check to first use.

Scenario: missing cache throws INVALID_OPTIONS

  • WHEN driveCachePlugin({} as unknown as DriveCacheOptions) is called
  • THEN the call throws DriveCacheError with code === "DRIVE_CACHE.INVALID_OPTIONS"

Scenario: cache without get/set/delete methods throws INVALID_OPTIONS

  • WHEN driveCachePlugin({ cache: { name: "x" } as unknown as AbstractCache<Uint8Array> }) is called
  • THEN the call throws DriveCacheError with code === "DRIVE_CACHE.INVALID_OPTIONS"

Verified by: src/plugin/plugin.test.ts::"throws INVALID_OPTIONS when cache is missing" AND src/plugin/plugin.test.ts::"throws INVALID_OPTIONS when cache lacks get/set/delete"


Requirement: Read middleware returns cached bytes on hit

When the middleware's read(ctx, next) is invoked and cache.get(key) resolves to a Uint8Array, the middleware SHALL return those bytes WITHOUT invoking next(). The cache key SHALL be ${namespace}::${ctx.path} where namespace defaults to "drive".

Scenario: hit short-circuits next()

  • WHEN drive.read("clip.mov") is called and cache.get("drive::clip.mov") resolves to bytes
  • THEN the returned value equals those bytes AND the inner storage's readFile is NOT invoked AND stats.hits === 1

Scenario: custom namespace is used in the key

  • WHEN the plugin is installed with { cache, namespace: "media" } and drive.read("clip.mov") is called
  • THEN cache.get("media::clip.mov") is invoked exactly once

Verified by: src/middleware/middleware.test.ts::"hit short-circuits next() and returns cached bytes" AND src/middleware/middleware.test.ts::"uses the configured namespace in the cache key"


Requirement: Read middleware populates the cache on miss

When cache.get(key) returns undefined, the middleware SHALL invoke next() to obtain plaintext bytes from the next middleware (or the storage), SHALL call cache.set(key, bytes) if shouldCache(path, bytes) returns true, and SHALL return the bytes. The default shouldCache SHALL return true when bytes.byteLength <= 16 * 1024 * 1024 (16 MiB).

Scenario: miss invokes next() and populates the cache

  • WHEN drive.read("x.bin") is called and cache.get("drive::x.bin") returns undefined
  • THEN next() is invoked exactly once AND cache.set("drive::x.bin", returnedBytes) is invoked exactly once AND stats.misses === 1 AND stats.populations === 1

Scenario: shouldCache predicate gates population

  • WHEN the plugin is installed with { cache, shouldCache: () => false } and drive.read("x.bin") is called on a miss
  • THEN next() is invoked exactly once AND cache.set is NOT invoked AND stats.populations === 0

Scenario: default maxEntryBytes is 16 MiB

  • WHEN the plugin is installed without shouldCache or maxEntryBytes, and drive.read("big.bin") resolves to a Uint8Array of 16 * 1024 * 1024 + 1 bytes
  • THEN cache.set is NOT invoked AND stats.populations === 0

Verified by: src/middleware/middleware.test.ts::"miss invokes next() and populates the cache" AND src/middleware/middleware.test.ts::"shouldCache predicate gates population" AND src/middleware/middleware.test.ts::"default maxEntryBytes is 16 MiB"


Requirement: Cache failures never propagate

When cache.get, cache.set, or cache.delete throws or rejects, the middleware SHALL increment stats.errors, SHALL record the failure via the OTel drive_cache.errors_total{namespace, op} counter, and SHALL NOT propagate the failure to the consumer. On get failure, the middleware SHALL fall through to next(). On set / delete failure, the middleware SHALL still return the bytes / boolean from next().

Scenario: cache.get rejection falls through to next()

  • WHEN drive.read("x") is called and cache.get rejects with new Error("redis down")
  • THEN next() is invoked AND the returned bytes match next()'s result AND the consumer sees no error AND stats.errors === 1

Scenario: cache.set rejection does not propagate

  • WHEN drive.read("x") is called and cache.set rejects after next() succeeds
  • THEN drive.read resolves with next()'s bytes AND stats.errors === 1 AND stats.populations === 0

Scenario: cache.delete rejection on invalidation does not propagate

  • WHEN drive.write("x", bytes) succeeds and cache.delete rejects
  • THEN drive.write resolves successfully AND stats.errors === 1 AND stats.invalidations === 0

Verified by: src/middleware/middleware.test.ts::"cache.get rejection falls through to next()" AND src/middleware/middleware.test.ts::"cache.set rejection does not propagate" AND src/middleware/middleware.invalidation.test.ts::"cache.delete rejection on invalidation does not propagate"


Requirement: Write invalidates the cache post-success

When the middleware's write(ctx, next) is invoked and invalidateOnWrite !== false, the middleware SHALL invoke next() first; on successful resolution it SHALL call cache.delete(key). When next() throws or rejects, the middleware SHALL propagate the throw WITHOUT invalidating the cache.

Scenario: successful write invalidates the cache entry

  • WHEN drive.write("x.bin", bytes) succeeds
  • THEN cache.delete("drive::x.bin") is invoked exactly once AND stats.invalidations === 1

Scenario: failed write does not invalidate

  • WHEN drive.write("x.bin", bytes) rejects because the underlying storage threw
  • THEN cache.delete is NOT invoked AND stats.invalidations === 0

Scenario: invalidateOnWrite:false skips invalidation

  • WHEN the plugin is installed with { cache, invalidateOnWrite: false } and drive.write("x.bin", bytes) succeeds
  • THEN cache.delete is NOT invoked

Verified by: src/middleware/middleware.invalidation.test.ts::"successful write invalidates the cache entry" AND src/middleware/middleware.invalidation.test.ts::"failed write does not invalidate" AND src/middleware/middleware.invalidation.test.ts::"invalidateOnWrite:false skips invalidation"


Requirement: Delete invalidates the cache post-success

When the middleware's delete(ctx, next) is invoked and invalidateOnDelete !== false, the middleware SHALL invoke next() first; when next() resolves to true, the middleware SHALL call cache.delete(key). When next() resolves to false (the file did not exist), the middleware MAY still call cache.delete(key) to clean any stale entry. When next() throws, the middleware SHALL propagate the throw WITHOUT invalidating.

Scenario: successful delete invalidates the cache entry

  • WHEN drive.delete("x.bin") resolves to true
  • THEN cache.delete("drive::x.bin") is invoked exactly once AND stats.invalidations === 1

Scenario: failed delete does not invalidate

  • WHEN drive.delete("x.bin") rejects because the storage threw
  • THEN cache.delete is NOT invoked

Verified by: src/middleware/middleware.invalidation.test.ts::"successful delete invalidates the cache entry" AND src/middleware/middleware.invalidation.test.ts::"failed delete does not invalidate"


Requirement: AbortSignal short-circuit on pre-aborted signal

When ctx.signal?.aborted === true at the time the middleware is invoked, the middleware SHALL skip its own work (no cache.get / cache.set / cache.delete calls) and SHALL invoke next() immediately so the inner middleware or storage raises the canonical DRIVE.ABORTED. Live signals SHALL be forwarded into cache.get / cache.set / cache.delete calls when the underlying AbstractCache accepts a signal.

Scenario: pre-aborted signal skips cache work

  • WHEN drive.read("x", { signal: AbortSignal.abort() }) is called
  • THEN cache.get is NOT invoked AND next() is invoked exactly once

Scenario: live signal is forwarded to cache operations

  • WHEN drive.read("x", { signal: liveSignal }) is called and the underlying cache records the signal it received
  • THEN cache.get receives liveSignal as the second argument (when AbstractCache.get accepts a signal)

Verified by: src/middleware/middleware.signal.test.ts::"pre-aborted signal skips cache work" AND src/middleware/middleware.signal.test.ts::"live signal is forwarded to cache operations"


Requirement: DriveCacheHandle exposes stats, invalidate, clear, and the cache reference

The handle returned from install() SHALL expose getStats() returning a monotonically-non-decreasing DriveCacheStats snapshot, invalidate(path) deleting the single cache entry for that path, clear() clearing the underlying cache, and a readonly cache reference equal (by ===) to the cache passed in options.cache.

Scenario: getStats returns monotonically increasing counters

  • WHEN two hits, one miss, and one population are exercised
  • THEN getStats() returns { hits: 2, misses: 1, populations: 1, invalidations: 0, errors: 0 }

Scenario: invalidate deletes one cache entry

  • WHEN handle.invalidate("x.bin") is called
  • THEN cache.delete("drive::x.bin") is invoked exactly once AND stats.invalidations === 1

Scenario: clear empties the underlying cache

  • WHEN handle.clear() is called
  • THEN cache.clear is invoked exactly once

Verified by: src/handle/handle.test.ts::"getStats returns monotonically increasing counters" AND src/handle/handle.test.ts::"invalidate deletes one cache entry" AND src/handle/handle.test.ts::"clear empties the underlying cache"


Requirement: stat and list middleware passthrough

The middleware SHALL define stat(ctx, next) and list(ctx, next) methods that immediately return await next() without consulting the cache. The cache SHALL NOT be populated or invalidated by stat or list calls.

Scenario: stat passes through unchanged

  • WHEN drive.stat("x.bin") is called
  • THEN cache.get is NOT invoked AND cache.set is NOT invoked AND the returned value equals next()'s result

Scenario: list passes through unchanged

  • WHEN drive.list("folder/") is called
  • THEN cache.get is NOT invoked AND cache.set is NOT invoked AND the returned value equals next()'s result

Verified by: src/middleware/middleware.test.ts::"stat passes through without touching the cache" AND src/middleware/middleware.test.ts::"list passes through without touching the cache"


Requirement: OpenTelemetry instrumentation

Every middleware operation SHALL emit a span on the @suaveplan/drive-cache tracer with drive.cache.namespace and drive.cache.path attributes and a drive.cache.outcome attribute drawn from {"hit", "miss", "populated", "invalidated", "error"}. The plugin SHALL emit counters drive_cache.{hits, misses, populations, invalidations, errors}_total{namespace} and a drive_cache.entry_bytes{namespace} histogram observing each populations_total increment with the cached entry's byte length.

Scenario: cache hit increments hits_total and records a hit-outcome span

  • WHEN a cache hit is exercised
  • THEN drive_cache.hits_total{namespace:"drive"} increments by 1 AND the emitted span has drive.cache.outcome === "hit"

Scenario: population records entry_bytes histogram

  • WHEN a 4096-byte payload is cached on a miss
  • THEN drive_cache.entry_bytes records one observation of 4096

Scenario: cache error increments errors_total with op label

  • WHEN cache.set rejects during population
  • THEN drive_cache.errors_total{namespace:"drive", op:"set"} increments by 1

Verified by: src/otel/otel.test.ts::"hit increments hits_total and records hit-outcome span" AND src/otel/otel.test.ts::"population records entry_bytes histogram" AND src/otel/otel.test.ts::"cache error increments errors_total with op label"


Requirement: Multi-namespace isolation on a shared cache

Two driveCachePlugin instances installed against two Drive instances with the SAME AbstractCache reference but DISTINCT namespace strings SHALL NOT observe each other's entries. A read on drive A with namespace "a" and a read on drive B with namespace "b" for the same path SHALL produce independent cache hits / misses.

Scenario: distinct namespaces isolate entries on a shared cache

  • WHEN plugin A (namespace "a") populates "clip.mov" and plugin B (namespace "b") then reads "clip.mov"
  • THEN plugin B observes a miss AND cache.get("a::clip.mov") and cache.get("b::clip.mov") are distinct calls

Verified by: tests/integration/with-namespaced-caches.test.ts::"distinct namespaces isolate entries on a shared cache"