3.5 KiB
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 oncause, so callers see a single structured failure shape. - Stateless — share one instance across many caches; both
TextEncoderandTextDecoderare module-level singletons.
Usage: Basic Example
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
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.