10 KiB
@suaveplan/storage-plugin-cache - Read-through cache decorator for AbstractStorage. Wraps any @suaveplan/storage with an injectable AbstractCache; hit/miss/populate on get, invalidate-on-write/setPartial/delete/clear, namespaced isolation, full OTel surface, cache failures swallow with stats.
Read-through cache decorator for AbstractStorage. Wraps any
@suaveplan/storage instance with an injectable
AbstractCache<Uint8Array>; hit/miss/populate on get, invalidate on
every set / setPartial / delete / clear. Namespaced isolation so
two wrappers can share one cache. Full OpenTelemetry surface
(storage_cache.* counters, entry_bytes histogram, per-op spans).
Cache failures swallow with stats so a flaky cache never breaks a read.
This is the structural inverse of @suaveplan/cache-storage, which
turns any AbstractStorage into a cache backend. Here we turn any
AbstractCache into a front of a storage.
Documentation tree
- cached-storage —
CachedStorageis a read-through cache decorator that extendsAbstractStoragefrom@suaveplan/storage. It wraps an…- cached-storage.clear — This module owns the
CachedStorage.clearpath. The wrapper SHALL first delegate toinner.clear(signal)(so the sour… - cached-storage.context — This module defines the
CachedStorageContextshape that the per-method helper files (cached-storage.get.ts, `cached… - cached-storage.delete — This module owns the delete-through path of
CachedStorage.delete. The wrapper forwards the deletion to the inner stor… - cached-storage.get — This module owns the read-through path of
CachedStorage.get(key, signal?). The implementation first consults the inje… - cached-storage —
CachedStorageis a read-through cache decorator that extendsAbstractStoragefrom@suaveplan/storage. It wraps an… - cached-storage.passthrough — This module owns the passthrough surface of
CachedStorage— every abstract method onAbstractStoragethat does NOT… - cached-storage.signal — This helper centralises the wrapper's policy for a pre-aborted
AbortSignal: every public method that takes a signal M… - cached-storage.write — This module owns the write-through path of
CachedStorage.setandCachedStorage.setPartial. Both ops invoke the inne…
- cached-storage.clear — This module owns the
- error-codes — This module owns the canonical registry of error codes raised by
@suaveplan/storage-plugin-cache. The package deliber…- error-codes — This module owns the canonical registry of error codes raised by
@suaveplan/storage-plugin-cache. The package deliber…
- error-codes — This module owns the canonical registry of error codes raised by
- factory — This module exports the
cachedStorage(inner, options)factory — a thin convenience over `new CachedStorage(inner, opt…- factory — This module exports the
cachedStorage(inner, options)factory — a thin convenience over `new CachedStorage(inner, opt…
- factory — This module exports the
- otel — This module wires the OpenTelemetry meter and tracer used by every
CachedStorageinstance. The wrapper emits five cou…- otel — This module wires the OpenTelemetry meter and tracer used by every
CachedStorageinstance. The wrapper emits five cou…
- otel — This module wires the OpenTelemetry meter and tracer used by every
Modules
./error-codes—STORAGE_CACHE_CODES(INVALID_OPTIONS,INNER_MISSING),CachedStorageError../types—CachedStorageOptions,CachedStorageStats,ResolvedCachedStorageOptions,CachedStorageErrorOp../cached-storage—CachedStorage extends AbstractStorage, per-method helpers split across.get.ts,.write.ts,.delete.ts,.clear.ts,.passthrough.ts,.signal.ts../factory—cachedStorage(inner, options): CachedStoragefactory../otel— meter + tracer wiring,recordHit/recordMiss/recordPopulate/recordInvalidate/recordError,setStorageCacheMeterProvider,resetInstruments.
Quick start
import { cachedStorage } from "@suaveplan/storage-plugin-cache";
import { MemoryCache } from "@suaveplan/cache";
import { createHttpStorage } from "@suaveplan/storage-http";
const inner = createHttpStorage({ baseUrl: "https://cdn.example.com/" });
const cache = new MemoryCache<Uint8Array>();
const storage = cachedStorage(inner, { cache, namespace: "media" });
const bytes = await storage.get("clip.mov"); // miss → inner → populate
const again = await storage.get("clip.mov"); // hit (no inner call)
await storage.set("clip.mov", new Uint8Array([1])); // inner.set + invalidate
Composition recipes
1. Memory cache in front of HTTPS (hot keys)
import { cachedStorage } from "@suaveplan/storage-plugin-cache";
import { MemoryCache } from "@suaveplan/cache";
import { createHttpStorage } from "@suaveplan/storage-http";
const inner = createHttpStorage({ baseUrl: "https://cdn.example.com/" });
const cache = new MemoryCache<Uint8Array>();
const storage = cachedStorage(inner, { cache, namespace: "media" });
2. File-backed cache in front of HTTPS (cross-process durable)
import { cachedStorage } from "@suaveplan/storage-plugin-cache";
import { createCache } from "@suaveplan/cache";
import { createNodeFsStorage } from "@suaveplan/storage-file";
import { createHttpStorage } from "@suaveplan/storage-http";
const inner = createHttpStorage({ baseUrl: "https://cdn.example.com/" });
const fileBackend = createNodeFsStorage({ rootDir: "/var/cache/cdn" });
const cache = createCache("storage", { storage: fileBackend });
const storage = cachedStorage(inner, { cache, namespace: "cdn-prod" });
3. CDN with namespace isolation
// Two wrappers sharing one cache instance, distinct namespaces,
// distinct inner storages — neither wrapper sees the other's entries
// on get, and `wrapperA.clear()` does not evict wrapperB's entries.
const cache = new MemoryCache<Uint8Array>();
const wrapperA = cachedStorage(innerA, { cache, namespace: "a" });
const wrapperB = cachedStorage(innerB, { cache, namespace: "b" });
4. Redis cache in front of S3 (cross-process shared)
import { createRedisCache } from "@suaveplan/cache-redis";
const inner = new S3Storage({ /* … */ });
const cache = createRedisCache({ client, prefix: "media-cache:" });
const storage = cachedStorage(inner, { cache, namespace: "media" });
5. Stale-while-revalidate via cache.wrap()
const storage = cachedStorage(inner, { cache });
// Expose the underlying cache for direct wrap()-based SWR:
const bytes = await storage.cache.wrap(
`storage::clip.mov`,
() => storage.inner.get("clip.mov"),
{ staleAge: 60_000 },
);
6. Composed with storage-uri / drive — the wrapper is just an AbstractStorage
import { createStorageUriResolver, fileUriPlugin } from "@suaveplan/storage-uri";
const cachedFile = cachedStorage(
createNodeFsStorage({ rootDir: "/var/media" }),
{ cache },
);
const resolver = createStorageUriResolver([
fileUriPlugin({ rootDir: "/var/media", storage: cachedFile }),
]);
Options
| Option | Default | Notes |
|---|---|---|
cache |
(required) | An AbstractCache<Uint8Array> instance — MemoryCache, StorageCache, createRedisCache, etc. |
namespace |
"storage" |
Prefix applied to every cache key (${namespace}::${key}). Distinct namespaces isolate two wrappers on a shared cache. |
invalidateOnWrite |
true |
When false, set/setPartial do NOT evict the cache entry — application owns the invalidation. |
invalidateOnDelete |
true |
When false, delete does NOT evict. |
invalidateOnClear |
true |
When false, clear does NOT walk the cache's namespace. |
shouldCache(key, bytes) |
bytes.byteLength <= maxEntryBytes |
Predicate gating cache population on miss. |
maxEntryBytes |
16 * 1024 * 1024 (16 MiB) |
Used by the default shouldCache. Matches drive-mesh-protocol's frame ceiling and drive-plugin-cache's entry ceiling. |
logger |
undefined | LoggerLike used to warn when clear() can't enumerate the cache. |
Relationship to siblings
@suaveplan/cache-storage— the structural inverse: turns anyAbstractStorageinto a cache backend. Use that one when you want to plug a file/S3/redis storage into thecreateCache("storage")factory. Use this one when you want to put a cache in front of a storage to accelerateget.add-drive-plugin-cache— the drive-tier twin: caches reassembled plaintext at the drive middleware layer. Same option shape, same OTel naming, same error-swallow contract. Use the drive wrapper for plaintext-after-chunk-reassembly, use this one for the raw storage bytes underneath.- Future
cacheUriPlugin— a planned URI-tier wrapper that composes with@suaveplan/storage-urito cache across URI schemes. This package's invalidation surface intentionally stops at the storage tier; URI-level caching belongs there.
Errors
| Code | When |
|---|---|
STORAGE_CACHE.INNER_MISSING |
Constructor / factory called with nullish inner. |
STORAGE_CACHE.INVALID_OPTIONS |
Constructor / factory called without a valid cache (missing, wrong shape, missing get/set/delete). |
Every other failure mode flows through with its original namespaced
code (STORAGE.*, STORAGE_HTTP.*, CACHE.*).
Cache failures never propagate
A cache.get / cache.set / cache.delete rejection is logged
through the OTel storage_cache.errors_total{namespace, op} counter
and bumped on getCacheStats().errors, but the wrapper's own surface
NEVER throws because the cache misbehaved. A failed cache.get falls
through to inner.get; a failed cache.set still returns the
inner-read bytes; a failed cache.delete on invalidation still
returns the inner-write/delete result.