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,121 @@
# @suaveplan/i18n-backend-memory - Universal in-memory translation backend for the @suaveplan/i18n family. Backed by Map<locale, Map<namespace, TranslationRecord>> plus a modification ledger; implements the full Backend contract (read, readMulti, save, lastModified). Designed for tests, seeded defaults, and the cache layer of a ChainedBackend. Zero runtime dependencies on @suaveplan/i18n core.
## Summary
Universal in-memory translation backend for the
`@suaveplan/i18n` family. Backed by `Map<locale, Map<namespace,
TranslationRecord>>` plus a modification ledger; implements the full
`Backend` contract (`read`, `readMulti`, `save`, `lastModified`).
Designed for tests, seeded defaults, and the cache layer of a
`ChainedBackend`. Zero runtime dependencies on `@suaveplan/i18n`
core.
<!-- begin:auto-docs -->
## Documentation tree
- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_ - [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_ - [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_
## Why this exists
The Wave 5 modular split lifts each backend out of the
`@suaveplan/i18n` monolith into its own sibling so consumers can
install only the backends they need (matching the `crypto-*` /
`drive-*` / `network-plugin-*` granularity established elsewhere in
the project). The in-memory backend is the simplest and most
universal of the five — it has zero external runtime deps, runs
identically in browser and server, and is the canonical seed source
for every other backend's tests.
## Install
```bash
bun add @suaveplan/i18n-backend-memory
```
## Quick Start
```ts
import { memoryBackend } from "@suaveplan/i18n-backend-memory";
const backend = memoryBackend({
seed: {
en: { common: { hello: "Hello" } },
fr: { common: { hello: "Bonjour" } },
},
});
const enCommon = await backend.read("en", "common");
// { hello: "Hello" }
await backend.save("en", "auth", { login: { title: "Sign in" } });
const t = await backend.lastModified("en", "auth"); // number (ms)
```
## Modules
| Subpath | Description |
|---|---|
| `@suaveplan/i18n-backend-memory` | Root barrel — class + factory + types + schemas + observability |
| `@suaveplan/i18n-backend-memory/backend` | `MemoryBackend` class only |
| `@suaveplan/i18n-backend-memory/factory` | `memoryBackend` factory function only |
| `@suaveplan/i18n-backend-memory/schemas` | Zod boundary schemas |
| `@suaveplan/i18n-backend-memory/observability` | OTel instruments + in-memory state ring |
## Modules Documentation
- [backend.md](src/backend/backend.md) — `MemoryBackend` class + capability matrix
- [factory.md](src/factory/factory.md) — `memoryBackend` factory function
- [observability.md](src/observability/observability.md) — OTel instruments + metric names + cardinality
## Capabilities
| Flag | Value | Notes |
|---|---|---|
| `canRead` | `true` | always — every backend reads |
| `canReadMulti` | `true` | bulk-load O(N) over the namespace map |
| `canSave` | `true` | persists into the in-memory store |
| `canTrackModification` | `true` | injectable `now()` clock |
| `canWatch` | `false` | no reactive `loaded` events |
## Testing
The package's own tests use the shared `runBackendConformance`
harness from `@suaveplan/i18n-shared/testing` so the universal
`Backend` contract is exercised against a fresh in-memory backend
per assertion:
```ts
import { runBackendConformance } from "@suaveplan/i18n-shared/testing";
import { MemoryBackend } from "@suaveplan/i18n-backend-memory";
runBackendConformance("MemoryBackend", async () => {
const backend = new MemoryBackend();
return {
backend,
async seed(locale, namespace, record) {
await backend.save(locale, namespace, record as never);
},
};
});
```
## Versioning
This package is part of the i18n modular-split rollout. Stays at
`0.1.0` until the slim core (Wave 6) cuts the family `1.0.0`
release. The metric names under `i18n.backend.memory.*` are part of
the public API contract and will not change without a major bump.
## Wave 5 Scope
This package is Wave 5a of the 14-package i18n modular split. It
ships pure additions only — `packages/i18n/i18n/src/core/backend/memory/`
retains its copy of the backend until Wave 6 deletes it. Both copies
coexist during Wave 5; downstream consumers continue to import from
the monolith and observe no behaviour change.

View File

@@ -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.

View File

@@ -0,0 +1,84 @@
# `memoryBackend(options?)`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Factory wrapper around `new MemoryBackend(options)` for callers that
prefer fluent builder-style composition over `new`. Returns a fresh
backend per call — no internal singleton, no shared state, no
caching. The factory exists primarily so chained-backend
configurations can be expressed declaratively without the visual
weight of repeated `new` keywords.
The factory is the recommended public entry point for routine use
because it composes naturally with the rest of the i18n family
(every backend ships a `<name>Backend` factory mirroring its class
constructor). Direct `new MemoryBackend(...)` remains supported and
is exactly equivalent.
## Features
- Returns a fresh `MemoryBackend` per call.
- Forwards every constructor option (`seed`, `name`, `now`)
unchanged.
- Same Zod boundary validation as the class constructor — invalid
options throw `I18nError(I18N.INVALID_OPTIONS)`.
## Usage: Basic Example
```ts
import { memoryBackend } from "@suaveplan/i18n-backend-memory";
const backend = memoryBackend();
await backend.save("en", "common", { hello: "Hello" });
const record = await backend.read("en", "common");
// { hello: "Hello" }
```
## Usage: Advanced Example
The factory shines in chained-backend configurations where multiple
in-memory backends (e.g. a cache layer + a write-through layer)
need to be expressed inline.
```ts
import { memoryBackend } from "@suaveplan/i18n-backend-memory";
const cache = memoryBackend({ name: "cache" });
const seed = memoryBackend({
name: "seed",
seed: {
en: { common: { hello: "Hello" } },
},
});
// Hand both to a ChainedBackend (Wave 5e).
// const chain = chainedBackend({ backends: [cache, seed] });
```
## API Reference
### `memoryBackend(options?: MemoryBackendOptions): MemoryBackend`
- `options.seed?: MemorySeed` — optional pre-population (`{ <locale>:
{ <namespace>: TranslationRecord } }`).
- `options.name?: string` — optional name override (default
`"memory"`).
- `options.now?: () => number` — injectable clock for deterministic
`lastModified` testing.
- Returns: a fresh `MemoryBackend` instance.
## Implementation Notes
- The factory is a one-liner: `return new MemoryBackend(options)`.
It exists for ergonomic parity with the rest of the
`<name>Backend` factory pattern across the i18n backend family
and adds no behaviour beyond instantiation.
- The factory does NOT memoise — every call returns a distinct
backend instance. Callers needing shared state pass the same
backend reference to multiple call sites; do not call the
factory multiple times expecting the same store.
- For consumers who need polymorphism over `BackendFactory<TOptions>`
from `@suaveplan/types`, this factory satisfies that signature.

View File

@@ -0,0 +1,113 @@
# `MemoryBackend` Observability
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the OpenTelemetry instrumentation for
`@suaveplan/i18n-backend-memory`. It ships per-call metric emitters
(`recordRead`, `recordWrite`) that the backend invokes around every
`read` / `save` operation, plus a small in-memory state ring so spec-
conformance tests can assert exact attribute payloads without
standing up a full SDK + InMemoryMetricExporter pipeline. Every
instrument is built lazily and is a no-op when no MeterProvider is
registered — consumers pay zero overhead unless they wire up the
SDK.
The metric naming follows the per-backend prefix convention agreed
in the Wave 5 design: `i18n.backend.memory.*`. Sibling backend
packages (`i18n-backend-http`, `i18n-backend-storage`, etc.) emit
under their own prefixes so consumers can group dashboard panels
per backend without shared-namespace collisions.
## Features
- Five instruments — two histograms, three counters — covering both
read and write paths.
- In-memory state ring for test assertions (no exporter required).
- Lazy meter construction; idempotent `getMemoryBackendMetrics()`.
- Test-only `resetMemoryBackendMetricsCache()` so suites can
rebuild the meter under a fresh provider.
## Usage: Basic Example
```ts
import {
getMemoryBackendMetricsState,
resetMemoryBackendMetricsState,
} from "@suaveplan/i18n-backend-memory/observability";
import { MemoryBackend } from "@suaveplan/i18n-backend-memory";
resetMemoryBackendMetricsState();
const backend = new MemoryBackend({ seed: { en: { common: { hi: "Hi" } } } });
await backend.read("en", "common");
const state = getMemoryBackendMetricsState();
expect(state.readTotal).toEqual([{ attributes: { outcome: "hit" } }]);
```
## Usage: Advanced Example
Mismatched reads emit BOTH an outcome counter AND an
`read_errors_total` counter labelled by error code. Tests can assert
the full payload to detect regressions in error classification.
```ts
import {
getMemoryBackendMetricsState,
resetMemoryBackendMetricsState,
} from "@suaveplan/i18n-backend-memory/observability";
import { MemoryBackend } from "@suaveplan/i18n-backend-memory";
resetMemoryBackendMetricsState();
const backend = new MemoryBackend();
await backend.read("en", "missing").catch(() => undefined);
const state = getMemoryBackendMetricsState();
expect(state.readTotal).toEqual([{ attributes: { outcome: "miss" } }]);
expect(state.readErrorsTotal).toEqual([
{ attributes: { code: "I18N.BACKEND_LOCALE_NOT_AVAILABLE" } },
]);
```
## API Reference
### Emitted instruments
| Instrument | Kind | Labels | Unit |
|---|---|---|---|
| `i18n.backend.memory.read_duration_ms` | Histogram | `locale`, `namespace`, `outcome` | ms |
| `i18n.backend.memory.read_total` | Counter | `outcome` (hit \| miss) | count |
| `i18n.backend.memory.read_errors_total` | Counter | `code` (I18N error code) | count |
| `i18n.backend.memory.write_duration_ms` | Histogram | `locale`, `namespace`, `outcome` | ms |
| `i18n.backend.memory.write_total` | Counter | `outcome` (ok \| error) | count |
### Functions
- `getMemoryBackendMetrics()` — returns cached instruments; builds
them on first call.
- `recordRead(locale, namespace, outcome, durationMs, errorCode?)`
— emit a read sample.
- `recordWrite(locale, namespace, outcome, durationMs)` — emit a
write sample.
- `getMemoryBackendMetricsState()` — frozen-shape snapshot of every
emitted sample since last reset.
- `resetMemoryBackendMetricsState()` — clear the in-memory state
ring (test fixture).
- `resetMemoryBackendMetricsCache()` — drop the cached instruments
so a fresh `MeterProvider` can be wired (test fixture).
## Implementation Notes
- The state ring is intentionally cardinality-bounded by the
caller's test cases. It exists for assertion ergonomics, not for
production telemetry — production telemetry flows through the
registered OTel `MeterProvider`.
- Cardinality safety: aggregate counters use only `outcome` (hit /
miss / ok / error); per-error-code attribution lives on a
separate `read_errors_total` counter so consumers can drop the
high-cardinality counter without losing aggregate visibility.
- Metric names are stable: every `i18n.backend.memory.*` name is
part of the package's public API contract and will not change
without a major bump.