Files
2026-05-27 23:47:53 +01:00
..
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00

@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 — CachedStorage is a read-through cache decorator that extends AbstractStorage from @suaveplan/storage. It wraps an…
    • cached-storage.clear — This module owns the CachedStorage.clear path. The wrapper SHALL first delegate to inner.clear(signal) (so the sour…
    • cached-storage.context — This module defines the CachedStorageContext shape 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 — CachedStorage is a read-through cache decorator that extends AbstractStorage from @suaveplan/storage. It wraps an…
    • cached-storage.passthrough — This module owns the passthrough surface of CachedStorage — every abstract method on AbstractStorage that 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.set and CachedStorage.setPartial. Both ops invoke the inne…
  • 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…
  • 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…
  • otel — This module wires the OpenTelemetry meter and tracer used by every CachedStorage instance. The wrapper emits five cou…
    • otel — This module wires the OpenTelemetry meter and tracer used by every CachedStorage instance. The wrapper emits five cou…

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): CachedStorage factory.
  • ./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 any AbstractStorage into a cache backend. Use that one when you want to plug a file/S3/redis storage into the createCache("storage") factory. Use this one when you want to put a cache in front of a storage to accelerate get.
  • 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-uri to 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.