first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

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.