Files
suaveplan/packages/localization/i18n/i18n-backend-memory/src/backend/backend.md
2026-05-27 23:47:53 +01:00

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: () => number clock for deterministic lastModified testing.
  • Optional signal?: AbortSignal parameter on read, readMulti, and save; pre-aborted signals throw I18nError(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) — validates options via MemoryBackendOptionsSchema; throws I18nError(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; throws I18nError(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 injected now() function.
  • lastModified(locale, namespace): Promise<number | undefined> — returns the timestamp of the last save for the slot, or undefined if 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 flat Map<string, number> keyed by "${locale} ${namespace}" with a single space separator (BCP-47 tags do not contain spaces, so the key is unambiguous).
  • signal?.aborted is checked at the entry of every async method and the throw surfaces as I18nError(I18N.BACKEND_TIMEOUT). The aborted flag is sampled exactly once at the top — the backend performs no further async work, so post-yield abort is not meaningful.
  • OTel recordRead / recordWrite calls emit zero overhead unless a MeterProvider is registered. Test suites use the in-memory metrics state ring exposed by ./observability to assert counter/histogram payloads without standing up the full SDK.
  • Seeded options call the same private #put path as save, so the modification ledger is populated for every seeded slot using the injected now() function. The seed is consumed synchronously at construction; consumers needing async seeding call save() post-construction.