5.0 KiB
MemoryBackend
(no submodules)
Purpose
MemoryBackend is the universal in-memory translation backend
for the @suaveplan/i18n family. It implements the full Backend
contract from @suaveplan/types (read, readMulti, save,
lastModified) backed by Map<locale, Map<namespace, TranslationRecord>> plus a modification ledger keyed by ${locale} ${namespace}. It is the canonical seed source for tests, the
default cache layer for ChainedBackend, and the reference
implementation every other backend's conformance suite is matched
against.
The backend has zero runtime dependencies on @suaveplan/i18n core,
so it can be used standalone — for example, a test suite needing
catalog-shaped data can import MemoryBackend without paying the
cost of pulling in the full catalog stack.
Features
- Full 5-flag capability matrix:
canRead: true,canReadMulti: true,canSave: true,canTrackModification: true,canWatch: false. - Optional constructor seed (
{ <locale>: { <namespace>: TranslationRecord } }) for pre-populating before first read. - Injectable
now: () => numberclock for deterministiclastModifiedtesting. - Optional
signal?: AbortSignalparameter onread,readMulti, andsave; pre-aborted signals throwI18nError(I18N.BACKEND_TIMEOUT). - OTel histograms + counters emitted on every read/save:
i18n.backend.memory.read_duration_ms,read_total{outcome},read_errors_total{code},write_duration_ms,write_total{outcome}. - Zod boundary validation: invalid options throw
I18nError(I18N.INVALID_OPTIONS)at construction time.
Usage: Basic Example
import { MemoryBackend } from "@suaveplan/i18n-backend-memory";
const backend = new MemoryBackend({
seed: {
en: { common: { hello: "Hello" } },
fr: { common: { hello: "Bonjour" } },
},
});
const enCommon = await backend.read("en", "common");
// { hello: "Hello" }
Usage: Advanced Example
The injectable clock makes lastModified deterministic so chained-
backend tests can assert exact timestamps without fighting wall-clock
non-determinism.
import { MemoryBackend } from "@suaveplan/i18n-backend-memory";
let tick = 1000;
const backend = new MemoryBackend({
name: "cache",
now: () => tick++,
});
await backend.save("en", "common", { hello: "Hello" });
await backend.save("en", "auth", { login: "Sign in" });
const tCommon = await backend.lastModified("en", "common"); // 1000
const tAuth = await backend.lastModified("en", "auth"); // 1001
// Cancellable reads play nicely with multi-backend orchestration.
const controller = new AbortController();
controller.abort();
await backend.read("en", "common", controller.signal); // throws BACKEND_TIMEOUT
API Reference
class MemoryBackend implements Backend
constructor(options?: MemoryBackendOptions)— validatesoptionsviaMemoryBackendOptionsSchema; throwsI18nError(I18N.INVALID_OPTIONS)on schema mismatch.name: string— backend label (default"memory").capabilities: BackendCapabilities— frozen 5-flag matrix.read(locale, namespace, signal?): Promise<TranslationRecord>— resolves with the seeded record; throwsI18nError(I18N.BACKEND_LOCALE_NOT_AVAILABLE)on miss.readMulti(locale, namespaces, signal?): Promise<Record<string, TranslationRecord>>— returns only the namespaces that exist; missing namespaces are omitted (not undefined).save(locale, namespace, record, signal?): Promise<void>— persists the record and stamps the modification ledger via the injectednow()function.lastModified(locale, namespace): Promise<number | undefined>— returns the timestamp of the lastsavefor the slot, orundefinedif never written.clear(): void— test-only helper, drops all in-memory state.snapshot(): MemorySeed— returns a fresh deep-copy snapshot of the current store keyed by locale → namespace.
Implementation Notes
- The
Map<locale, Map<namespace, ...>>two-level data structure gives O(1) lookups and O(1) inserts. The modification ledger is a flatMap<string, number>keyed by"${locale} ${namespace}"with a single space separator (BCP-47 tags do not contain spaces, so the key is unambiguous). signal?.abortedis checked at the entry of every async method and the throw surfaces asI18nError(I18N.BACKEND_TIMEOUT). Theabortedflag is sampled exactly once at the top — the backend performs no further async work, so post-yield abort is not meaningful.- OTel
recordRead/recordWritecalls emit zero overhead unless aMeterProvideris registered. Test suites use the in-memory metrics state ring exposed by./observabilityto assert counter/histogram payloads without standing up the full SDK. - Seeded options call the same private
#putpath assave, so the modification ledger is populated for every seeded slot using the injectednow()function. The seed is consumed synchronously at construction; consumers needing async seeding callsave()post-construction.