Initial Commit
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# `MemoryBackend`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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
|
||||
|
||||
```ts
|
||||
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.
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user