Move docs/ package mirror into document-navigator plugin

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>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
# @suaveplan/cache-storage - AbstractCache backend that bridges any @suaveplan/storage AbstractStorage into a typed cache via a 32-byte fixed-width TTL header and pluggable Codec<T> (JSON default, identity for Uint8Array).
`AbstractCache<T>` backend that bridges any `@suaveplan/storage` `AbstractStorage` into a typed cache via a 32-byte fixed-width TTL header and pluggable `Codec<T>` (JSON default, identity for `Uint8Array`).
<!-- begin:auto-docs -->
## Documentation tree
- [identity](./src/codec/identity.md) — `IdentityCodec` is the `Codec<Uint8Array>` implementation that passes bytes through without encoding overhead. Use it w…
- [json](./src/codec/json.md) — `JsonCodec<T>` is the default codec for `StorageCache`. It serialises arbitrary JSON-compatible values to UTF-8 bytes v…
- [errors](./src/errors/errors.md) — Defines the structured error surface for `@suaveplan/cache-storage`. Every failure path inside the package — corrupt bl…
- [errors](./src/errors/errors.md) — Defines the structured error surface for `@suaveplan/cache-storage`. Every failure path inside the package — corrupt bl…
- [header](./src/header/header.md) — Defines the 32-byte fixed-width metadata header that `StorageCache` prepends to every cached value. The header carries…
- [header](./src/header/header.md) — Defines the 32-byte fixed-width metadata header that `StorageCache` prepends to every cached value. The header carries…
- [registry-augment](./src/registry-augment/registry-augment.md) — Augments the `CacheProviderRegistry` type exported by `@suaveplan/cache` with a `storage` entry, and side-effect-regist…
- [registry-augment](./src/registry-augment/registry-augment.md) — Augments the `CacheProviderRegistry` type exported by `@suaveplan/cache` with a `storage` entry, and side-effect-regist…
- [storage-cache](./src/storage-cache/storage-cache.md) — `StorageCache<T>` extends `AbstractCache<T>` from `@suaveplan/cache` and turns any `AbstractStorage` (from `@suaveplan/…
- [storage-cache](./src/storage-cache/storage-cache.md) — `StorageCache<T>` extends `AbstractCache<T>` from `@suaveplan/cache` and turns any `AbstractStorage` (from `@suaveplan/…
<!-- end:auto-docs -->
## Summary
`@suaveplan/cache-storage` is the universal cache backend for the cache family. It composes any byte-oriented storage adapter — `MemoryStorage`, `storage-browser-localstorage`, `storage-browser-indexeddb`, `storage-browser-opfs`, `storage-file`, `storage-s3`, `storage-cdn-*` — into a typed cache with TTL, lifecycle events, namespacing, and structured errors. The package subsumes the upstream `cache-browser` package by composition: pick the storage adapter that matches your environment, and the cache surface is identical to `MemoryCache`. Side-effect import auto-registers `StorageCache` as the `"storage"` provider on `CacheFactory`, so `createCache("storage", { storage })` works after a single import.
## Installation
```bash
bun add @suaveplan/cache-storage
```
## Quick Start
```ts
import { StorageCache } from "@suaveplan/cache-storage";
import { MemoryStorage } from "@suaveplan/storage/memory";
const cache = new StorageCache<{ id: string; email: string }>({
storage: new MemoryStorage(),
defaultTtl: 5 * 60_000,
});
await cache.set("user:1", { id: "1", email: "alice@example.com" });
const user = await cache.get("user:1");
```
## Modules
- [errors](./src/errors/errors.md) — `CACHE_STORAGE_CODES`, `StorageCacheError`.
- [header](./src/header/header.md) — 32-byte fixed-width metadata header codec.
- [codec/identity](./src/codec/identity.md) — `IdentityCodec` for `Uint8Array` values.
- [codec/json](./src/codec/json.md) — `JsonCodec<T>` default codec.
- [storage-cache](./src/storage-cache/storage-cache.md) — `StorageCache<T>` class.
- [registry-augment](./src/registry-augment/registry-augment.md) — `CacheProviderRegistry` augmentation + auto-registration.
## Browser-side caching (replaces `cache-browser`)
```ts
import { StorageCache } from "@suaveplan/cache-storage";
import { LocalStorageStorage } from "@suaveplan/storage-browser-localstorage";
const cache = new StorageCache<string>({
storage: new LocalStorageStorage(),
namespace: "ui:preferences",
defaultTtl: 7 * 24 * 60 * 60_000,
});
```
The same pattern works over `storage-browser-indexeddb` (larger payloads) and `storage-browser-opfs` (file-grade durability).
## Factory registration
```ts
import "@suaveplan/cache-storage"; // side-effect: register provider
import { createCache } from "@suaveplan/cache";
import { MemoryStorage } from "@suaveplan/storage/memory";
const cache = createCache("storage", { storage: new MemoryStorage() });
```
## Dependencies
| Dependency | Purpose |
| --- | --- |
| `@suaveplan/cache` | `AbstractCache<T>`, `CacheFactory`, `CacheProviderRegistry`, `CacheEvents`, `CacheSetOptions`. |
| `@suaveplan/error` | `defineCodes`, `SuaveplanError`. |
| `@suaveplan/storage` | `AbstractStorage` contract. |
| `@suaveplan/types` | `EventBusLike`. |
| `tslib` | TypeScript helpers. |

View File

@@ -0,0 +1,66 @@
# IdentityCodec
## Purpose
`IdentityCodec` is the `Codec<Uint8Array>` implementation that passes bytes through without encoding overhead. Use it when the cached `T` is already binary — chunked file data, pre-serialised protobufs, encrypted blobs, audio samples — and reserialising through JSON would be wasteful or lossy. The codec is paired with `StorageCache<Uint8Array>` to provide a byte-cache surface over any `AbstractStorage`.
## Features
- Zero-copy `encode` — returns the input `Uint8Array` by reference. Suitable for high-throughput paths where allocations matter.
- Defensive-copy `decode` — returns a fresh `Uint8Array` so callers mutating the result cannot corrupt the underlying storage buffer.
- No throws — every input is valid because the encode step is the identity function.
- Stateless — a single instance is safe to share across cache instances and concurrent operations.
## Usage: Basic Example
```ts
import { IdentityCodec } from "@suaveplan/cache-storage/codec/identity";
import { StorageCache } from "@suaveplan/cache-storage";
import { MemoryStorage } from "@suaveplan/storage/memory";
const cache = new StorageCache<Uint8Array>({
storage: new MemoryStorage(),
codec: new IdentityCodec(),
});
await cache.set("blob:1", new Uint8Array([1, 2, 3, 4]));
const out = await cache.get("blob:1");
```
## Usage: Advanced Example
```ts
import { IdentityCodec } from "@suaveplan/cache-storage/codec/identity";
import { StorageCache } from "@suaveplan/cache-storage";
// Cache pre-compressed payloads: avoid re-encoding on every cache hit.
const cache = new StorageCache<Uint8Array>({
storage: filesystemStorage,
codec: new IdentityCodec(),
defaultTtl: 60_000,
});
const compressed = await gzip(payload);
await cache.set(`gz:${id}`, compressed);
const cached = await cache.get(`gz:${id}`);
if (cached !== undefined) {
// Stream straight to the response without a decode step.
res.setHeader("content-encoding", "gzip");
res.end(cached);
}
```
## API Reference
| Member | Signature | Description |
| --- | --- | --- |
| `new IdentityCodec()` | — | Constructs a stateless codec. No arguments. |
| `encode(value)` | `(Uint8Array) → Uint8Array` | Returns `value` by reference. |
| `decode(bytes)` | `(Uint8Array) → Uint8Array` | Returns a fresh copy of `bytes`. |
Conforms to `Codec<Uint8Array>` from `@suaveplan/cache-storage/codec`.
## Implementation Notes
The `encode` step is intentionally non-copying. `StorageCache` immediately wraps the returned bytes in a fresh combined buffer (header + body), so referential sharing does not bleed into the on-disk representation. The `decode` step copies because the storage backend may hand out its internal buffer for performance — `MemoryStorage` and `storage-file` both do — and a caller that mutated the result would otherwise see arbitrary corruption on the next read.

View File

@@ -0,0 +1,79 @@
# JsonCodec
## Purpose
`JsonCodec<T>` is the default codec for `StorageCache`. It serialises arbitrary JSON-compatible values to UTF-8 bytes via `JSON.stringify` + `TextEncoder.encode`, and parses bytes back through `TextDecoder.decode` (in fatal mode) + `JSON.parse`. Use it for the typical cache payload — profile objects, configuration documents, API responses, denormalised projections — anywhere the value is already JSON-safe.
## Features
- Round-trips every JSON-safe TypeScript value: strings, numbers, booleans, `null`, plain objects, arrays, and nested combinations of these.
- Full multi-byte UTF-8 support — emoji, CJK, RTL scripts, mathematical operators all survive a round-trip.
- Fatal-mode UTF-8 decode — invalid leading sequences fail loud instead of silently producing replacement characters, so corruption surfaces immediately.
- Decode failures (malformed UTF-8 OR malformed JSON) are wrapped in `StorageCacheError("CACHE_STORAGE.CODEC_ERROR")` with the original throw on `cause`, so callers see a single structured failure shape.
- Stateless — share one instance across many caches; both `TextEncoder` and `TextDecoder` are module-level singletons.
## Usage: Basic Example
```ts
import { JsonCodec } from "@suaveplan/cache-storage/codec/json";
const codec = new JsonCodec<{ user: string; roles: string[] }>();
const bytes = codec.encode({ user: "alice", roles: ["admin"] });
const value = codec.decode(bytes);
```
## Usage: Advanced Example
```ts
import { JsonCodec } from "@suaveplan/cache-storage/codec/json";
import { StorageCache } from "@suaveplan/cache-storage";
import { MemoryStorage } from "@suaveplan/storage/memory";
import {
CACHE_STORAGE_CODES,
StorageCacheError,
} from "@suaveplan/cache-storage/errors";
interface UserSummary {
id: string;
email: string;
preferredLocale: string;
}
const cache = new StorageCache<UserSummary>({
storage: new MemoryStorage(),
codec: new JsonCodec<UserSummary>(),
defaultTtl: 5 * 60_000,
});
async function loadUser(id: string): Promise<UserSummary> {
try {
return await cache.wrap(`user:${id}`, () => fetchUser(id));
} catch (err) {
if (
err instanceof StorageCacheError &&
err.code === CACHE_STORAGE_CODES.CODEC_ERROR
) {
log.warn("Corrupted cache entry — refetching", { id, cause: err.cause });
await cache.delete(`user:${id}`);
return fetchUser(id);
}
throw err;
}
}
```
## API Reference
| Member | Signature | Description |
| --- | --- | --- |
| `new JsonCodec<T>()` | — | Constructs a stateless codec for `T`. |
| `encode(value)` | `(T) → Uint8Array` | `TextEncoder.encode(JSON.stringify(value))`. |
| `decode(bytes)` | `(Uint8Array) → T` | `JSON.parse(TextDecoder.decode(bytes))`. Throws `StorageCacheError("CACHE_STORAGE.CODEC_ERROR")` on malformed input. |
Conforms to `Codec<T>` from `@suaveplan/cache-storage/codec`.
## Implementation Notes
`TextDecoder` is constructed once at module load with `{ fatal: true }`. The fatal flag is critical — without it, invalid bytes would be replaced with `U+FFFD` and the cache would silently return decoded garbage. The split try/catch separates the two failure modes (`utf8` vs `json`) in the wrapped error's `messageKey` so observability pipelines can disambiguate them.
`encode` failures from `JSON.stringify` (circular references, `BigInt`, `Symbol`) propagate as-is — they originate in the application's data, not the cache, and `StorageCache.set` adapts them into `CACHE_STORAGE.CODEC_ERROR` at the cache boundary so the public surface stays uniform.

View File

@@ -0,0 +1,76 @@
# errors
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the structured error surface for `@suaveplan/cache-storage`. Every failure path inside the package — corrupt blob layouts, codec decode failures, oversized keys or values, propagated backend faults — is reported by throwing a `StorageCacheError` carrying one of the codes registered in `CACHE_STORAGE_CODES`. Consumers programmatically branch on `error.code` (a discriminated union literal like `"CACHE_STORAGE.CODEC_ERROR"`) instead of pattern-matching free-form message strings. The optional `key` discriminant identifies the cache key that produced the failure when one is known.
## Features
- 5-code registry built with `defineCodes("CACHE_STORAGE", [...])` so codes serialise identically across the OpenTelemetry span pipeline, the wire-format error envelope, and the i18n resolver.
- `StorageCacheError` extends `SuaveplanError<CacheStorageCode>` for cause-chain traversal and structured `traceId` / `context` propagation.
- Optional `key` discriminant carried on every instance — present on every code that originates at a key boundary (read, write, delete, validate).
- Every code is exercised by `errors.test.ts` against a real call site, satisfying the §13 step 0 reachability gate.
## Usage: Basic Example
```ts
import {
CACHE_STORAGE_CODES,
StorageCacheError,
} from "@suaveplan/cache-storage/errors";
try {
await cache.get("k");
} catch (err) {
if (
err instanceof StorageCacheError &&
err.code === CACHE_STORAGE_CODES.ENTRY_CORRUPT
) {
log.warn("Corrupted cache entry — deleting", { key: err.key });
}
}
```
## Usage: Advanced Example
```ts
import {
CACHE_STORAGE_CODES,
StorageCacheError,
} from "@suaveplan/cache-storage/errors";
function classify(err: unknown): "retry" | "fatal" | "ignore" {
if (!(err instanceof StorageCacheError)) return "fatal";
switch (err.code) {
case CACHE_STORAGE_CODES.BACKEND_ERROR:
return "retry";
case CACHE_STORAGE_CODES.ENTRY_CORRUPT:
case CACHE_STORAGE_CODES.CODEC_ERROR:
return "ignore"; // cache miss is safe
case CACHE_STORAGE_CODES.KEY_TOO_LONG:
case CACHE_STORAGE_CODES.VALUE_TOO_LARGE:
return "fatal";
default:
return "fatal";
}
}
```
## API Reference
| Export | Kind | Purpose |
| --- | --- | --- |
| `CACHE_STORAGE_CODES` | const | Frozen object mapping reason → `"CACHE_STORAGE.<REASON>"` value. |
| `CacheStorageCode` | type | Union of the registered code values. |
| `StorageCacheError` | class | `extends SuaveplanError<CacheStorageCode>`; constructor accepts `(code, messageKey, options?)` where options may include `key`, `cause`, `context`, `traceId`. |
| `StorageCacheErrorOptions` | type | `SuaveplanErrorOptions & { key?: string }`. |
Registered codes: `ENTRY_CORRUPT`, `CODEC_ERROR`, `BACKEND_ERROR`, `KEY_TOO_LONG`, `VALUE_TOO_LARGE`.
## Implementation Notes
Codes are intentionally minimal — five is enough to cover every failure mode `StorageCache` can produce. `BACKEND_ERROR` is the only code that wraps a foreign exception (the underlying `AbstractStorage.get` / `set`); the others originate inside the package and have well-defined call sites. Decode failures from the codec always surface as `CODEC_ERROR` even when the codec implementation throws something else — `StorageCache.#decodeBody` adapts arbitrary throwables into the canonical form.

View File

@@ -0,0 +1,81 @@
# header
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the 32-byte fixed-width metadata header that `StorageCache` prepends to every cached value. The header carries `createdAt`, `expiresAt`, `flags`, and `hitCount` so the cache can recover TTL semantics over any byte-oriented `AbstractStorage` without a side-channel metadata API. Fixed width keeps reads single-range and avoids a second round-trip when the underlying store supports byte-range fetches.
## Features
- 32 bytes, big-endian, deterministic layout — byte-identical between writers and readers across runtimes.
- Two `u64` fields (`createdAt`, `expiresAt`) accommodate millisecond timestamps well past the year 10000.
- One `u32` `flags` word carries codec selector bits (`FLAG_CODEC_JSON`) and a `FLAG_HAS_METADATA` presence bit reserved for downstream extensions.
- One `u32` `hitCount` saturates at 2³² − 1 — informational only; not rewritten on read in this version.
- 8 reserved bytes at the tail leave room for future extension without a version bump.
- Bounds-checked `encodeHeader` — out-of-range fields surface as `StorageCacheError("CACHE_STORAGE.ENTRY_CORRUPT")` rather than producing a silently truncated blob.
## Usage: Basic Example
```ts
import { decodeHeader, encodeHeader } from "@suaveplan/cache-storage/header";
const bytes = encodeHeader({
createdAt: Date.now(),
expiresAt: 0,
flags: 0,
hitCount: 0,
});
const meta = decodeHeader(bytes);
```
## Usage: Advanced Example
```ts
import {
decodeHeader,
encodeHeader,
FLAG_CODEC_JSON,
FLAG_HAS_METADATA,
HEADER_BYTES,
} from "@suaveplan/cache-storage/header";
function packEntry(value: Uint8Array, ttlMs: number): Uint8Array {
const now = Date.now();
const head = encodeHeader({
createdAt: now,
expiresAt: ttlMs > 0 ? now + ttlMs : 0,
flags: FLAG_HAS_METADATA | FLAG_CODEC_JSON,
hitCount: 0,
});
const out = new Uint8Array(HEADER_BYTES + value.length);
out.set(head, 0);
out.set(value, HEADER_BYTES);
return out;
}
function unpackEntry(blob: Uint8Array): {
meta: ReturnType<typeof decodeHeader>;
body: Uint8Array;
} {
return { meta: decodeHeader(blob), body: blob.subarray(HEADER_BYTES) };
}
```
## API Reference
| Export | Kind | Purpose |
| --- | --- | --- |
| `HEADER_BYTES` | const number `32` | Fixed length of the metadata header. |
| `HeaderMeta` | interface | `{ createdAt: number; expiresAt: number; flags: number; hitCount: number }`. |
| `encodeHeader(meta)` | function | Returns a fresh 32-byte `Uint8Array`. Throws `StorageCacheError("CACHE_STORAGE.ENTRY_CORRUPT")` when any field is out of range. |
| `decodeHeader(bytes)` | function | Parses the leading 32 bytes. Throws `StorageCacheError("CACHE_STORAGE.ENTRY_CORRUPT")` when the buffer is shorter. |
| `FLAG_HAS_METADATA` | const number | Bit 0 — set by `StorageCache` on every encode. |
| `FLAG_CODEC_JSON` | const number | Bit 1 — set when the body was produced by `JsonCodec` (for future routing). |
## Implementation Notes
`u64` values are split into two `u32` big-endian writes (`high * 2³² + low`) so the encoder runs without `BigInt` overhead. `decodeHeader` is the inverse. The reserved 8 bytes at offset 24..31 are zeroed at encode time to keep on-disk parity bytewise stable; future versions may stuff version markers there without breaking forward decoders that mask the field.

View File

@@ -0,0 +1,63 @@
# registry-augment
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Augments the `CacheProviderRegistry` type exported by `@suaveplan/cache` with a `storage` entry, and side-effect-registers `StorageCache` as the implementation behind that key on the shared `CacheFactory`. Importing this module — directly or transitively via the package root — is enough; the registration is idempotent and survives repeated imports or duplicate package versions resolving to the same singleton registry.
## Features
- TypeScript-side module augmentation makes `createCache("storage", config)` type-safe — the registry key is part of `CacheProviderType`, and the config parameter is inferred from `StorageCacheOptions<T>` (via the registry slot).
- Idempotent runtime registration — guarded by both an internal `registered` flag and a `hasCacheProvider("storage")` query against the live registry, so double-import never throws or overwrites.
- Side-effect import — the `registry-augment.ts` module is listed in `"sideEffects"` in `package.json` so bundlers preserve the registration call when the rest of the package is tree-shaken.
## Usage: Basic Example
```ts
// Side-effect import: registers the "storage" provider.
import "@suaveplan/cache-storage";
import { createCache } from "@suaveplan/cache";
import { MemoryStorage } from "@suaveplan/storage/memory";
const cache = createCache("storage", { storage: new MemoryStorage() });
```
## Usage: Advanced Example
```ts
// Manually drive registration — useful in tests that need to control the
// order of provider availability or want to confirm idempotency.
import {
hasCacheProvider,
unregisterCacheProvider,
} from "@suaveplan/cache";
import { registerStorageProvider } from "@suaveplan/cache-storage";
unregisterCacheProvider("storage");
registerStorageProvider();
console.log(hasCacheProvider("storage")); // true
```
## API Reference
| Export | Kind | Description |
| --- | --- | --- |
| `registerStorageProvider()` | function | Idempotently calls `registerCacheProvider("storage", StorageCache)`. Safe to invoke repeatedly. |
The module also performs module augmentation:
```ts
declare module "@suaveplan/cache" {
interface CacheProviderRegistry {
storage: typeof StorageCache;
}
}
```
## Implementation Notes
The `registered` module-local flag is set even when the registry already contained the provider (e.g. because another package version registered it first). That ensures the second branch is exercised at most once per module instance and that subsequent calls hit the fast path. The function is exported so test suites can drive it explicitly after a `clearCacheProviders()` call without re-importing the package.

View File

@@ -0,0 +1,108 @@
# StorageCache
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`StorageCache<T>` extends `AbstractCache<T>` from `@suaveplan/cache` and turns any `AbstractStorage` (from `@suaveplan/storage`) into a typed cache. Behind the type-level `T` API it stores each entry as a 32-byte fixed-width metadata header followed by codec-encoded value bytes — letting plain byte stores like `storage-browser-localstorage`, `storage-file`, or `storage-s3` participate in the cache provider registry without bespoke per-backend code. The class subsumes the upstream `cache-browser` package by composition: pick the storage adapter that matches your environment, hand it to `StorageCache`, and the cache surface is identical to `MemoryCache`.
## Features
- Drop-in `AbstractCache<T>` subclass — inherits `wrap`, `getMultiple`, `setMultiple` with `AbortSignal` and stale-while-revalidate semantics unchanged.
- 32-byte TTL header (`createdAt`, `expiresAt`, `flags`, `hitCount`) — lazy expiration on read, never expires when `expiresAt === 0`.
- Pluggable `Codec<T>` — `JsonCodec` by default; swap to `IdentityCodec` for binary payloads.
- Optional namespacing — prefixes every storage key with `${namespace}:`. Namespaced `clear()` lists + deletes prefix-matched keys via the storage backend's `list({ prefix })` cursor.
- Lifecycle events (`hit`, `miss`, `set`, `delete`, `clear`, `expire`) emitted through any `EventBusLike<CacheEvents>` — listeners optional, zero-cost when omitted.
- Structured `StorageCacheError` for all failure modes — `ENTRY_CORRUPT`, `CODEC_ERROR`, `BACKEND_ERROR`, `KEY_TOO_LONG`, `VALUE_TOO_LARGE`.
- Stats counters — `hits`, `misses`, `evictions`, `size`, `hitRate`. Size tracked client-side via insert/delete deltas (storage backends rarely expose a cheap count).
- Auto-registered with the `CacheFactory` registry as the `"storage"` provider on side-effect import.
## Usage: Basic Example
```ts
import { StorageCache } from "@suaveplan/cache-storage";
import { MemoryStorage } from "@suaveplan/storage/memory";
const cache = new StorageCache<{ id: string; name: string }>({
storage: new MemoryStorage(),
defaultTtl: 60_000,
});
await cache.set("user:1", { id: "1", name: "Alice" });
const user = await cache.get("user:1");
```
## Usage: Advanced Example
```ts
import "@suaveplan/cache-storage"; // side-effect: registers the "storage" provider
import { createCache } from "@suaveplan/cache";
import { StorageCache } from "@suaveplan/cache-storage";
import { IdentityCodec } from "@suaveplan/cache-storage/codec/identity";
import { EventBus } from "@suaveplan/events";
import { FileStorage } from "@suaveplan/storage-file";
const events = new EventBus();
events.on("evict", ({ key, reason }) => log.info("evicted", { key, reason }));
// Construct directly:
const direct = new StorageCache<Uint8Array>({
storage: new FileStorage({ root: "/var/cache/app" }),
codec: new IdentityCodec(),
namespace: "thumbnails",
defaultTtl: 24 * 60 * 60_000,
events,
});
// Or via the factory (same instance shape, registry-driven):
const indirect = createCache("storage" as never, {
storage: new FileStorage({ root: "/var/cache/app" }),
namespace: "responses",
} as never);
await direct.set("avatar:42", await renderAvatar(42));
// `wrap` with single-flight + SWR is inherited:
const summary = await direct.wrap(
`summary:${userId}`,
() => buildSummary(userId),
{ ttl: 60_000, staleWhileRevalidate: true, staleThreshold: 5_000 },
);
```
## API Reference
### Constructor
`new StorageCache<T>(options: StorageCacheOptions<T>)`
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `storage` | `AbstractStorage` | — (required) | Underlying byte-oriented storage adapter. |
| `codec` | `Codec<T>` | `new JsonCodec<T>()` | Value codec. |
| `defaultTtl` | `number` | `0` | TTL in ms when `set` omits one. `0` means no expiry. |
| `clock` | `Clock` | `() => Date.now()` | Injectable time source. |
| `events` | `EventBusLike<CacheEvents>` | `undefined` | Lifecycle event sink. |
| `namespace` | `string` | `""` | Key prefix; empty disables. |
### Methods
Every `AbstractCacheShape<T>` method: `get`, `set`, `delete`, `clear`, `has`, `getStats`, `getMultiple`, `setMultiple`, `wrap`. See the parent `AbstractCache` documentation for the inherited helpers.
### Thrown errors
- `StorageCacheError("CACHE_STORAGE.ENTRY_CORRUPT")` — stored blob shorter than `HEADER_BYTES`, or decoded header out of range.
- `StorageCacheError("CACHE_STORAGE.CODEC_ERROR")` — codec encode or decode failure.
- `StorageCacheError("CACHE_STORAGE.BACKEND_ERROR")` — underlying `AbstractStorage` threw.
- `StorageCacheError("CACHE_STORAGE.KEY_TOO_LONG")` — key longer than 1024 chars.
- `StorageCacheError("CACHE_STORAGE.VALUE_TOO_LARGE")` — encoded value exceeds 10 MiB.
## Implementation Notes
`get` performs a single `storage.get(prefixedKey)` and parses the header in place. Expired entries are deleted lazily — there is no background sweeper, which keeps the package universal (no `setInterval` dependency). The size counter is tracked through `set` and `delete` deltas; it is not authoritative when the underlying storage is shared with another writer, but cache stats are advisory in that case anyway.
`clear()` with no namespace defers to the storage backend's own `clear()` — fast and atomic where the backend supports it. With a namespace, it iterates `storage.list({ prefix })` in pages of 1000 and deletes each batch, which is O(n) in matching keys but bounded by namespace size.
`hitCount` is set to `0` on write and **not** rewritten on read. Bumping it would turn every `get` into a `getThenSet`, halving throughput; the field is reserved for a future `getPartial`-aware variant that updates the header in place when the backend supports byte-range writes.