9.9 KiB
Capability Spec: i18n-backend-memory (i18n-backend-memory)
Package: @suaveplan/i18n-backend-memory
Status: PROPOSED — first delta; becomes canonical spec on archive.
Every requirement below carries a
**Verified by:**line at authoring time. (Seeopenspec/AGENTS.mdSection 13.)
ADDED Requirements
Requirement: MemoryBackend implements the Backend interface
The package SHALL export a MemoryBackend class that implements the Backend interface from @suaveplan/types/i18n (universal-tier, browser + server). The class SHALL declare a static capabilities matrix of { canRead: true, canReadMulti: true, canSave: true, canTrackModification: true, canWatch: false } and SHALL accept an optional MemoryBackendOptions constructor argument carrying { seed?, name?, now? }. Default name SHALL equal "memory". Default now SHALL equal Date.now.
Scenario: capabilities matrix matches the contract
- WHEN
new MemoryBackend().capabilitiesis read - THEN the result equals
{ canRead: true, canReadMulti: true, canSave: true, canTrackModification: true, canWatch: false }
Scenario: default name is "memory"
- WHEN
new MemoryBackend().nameis read - THEN the value equals
"memory"
Scenario: custom name override
- WHEN
new MemoryBackend({ name: "cache" }).nameis read - THEN the value equals
"cache"
Verified by: packages/i18n/i18n-backend-memory/src/backend.test.ts::"capabilities matrix matches the contract" AND packages/i18n/i18n-backend-memory/src/backend.test.ts::"default name is memory" AND packages/i18n/i18n-backend-memory/src/backend.test.ts::"custom name override"
Requirement: read returns the seeded record on hit
The read(locale, namespace, signal?) method SHALL return the TranslationRecord previously seeded for the (locale, namespace) pair. Lookup SHALL be O(1) via the underlying Map<locale, Map<namespace, TranslationRecord>> data structure.
Scenario: read hit returns seeded record
- GIVEN a
MemoryBackendconstructed with{ seed: { en: { common: { hello: "Hello" } } } } - WHEN
backend.read("en", "common")is awaited - THEN the result equals
{ hello: "Hello" }
Verified by: packages/i18n/i18n-backend-memory/src/backend.test.ts::"read hit returns seeded record"
Requirement: read miss throws BACKEND_LOCALE_NOT_AVAILABLE
The read method SHALL throw I18nError with code I18N.BACKEND_LOCALE_NOT_AVAILABLE when the (locale, namespace) pair has no record. The error context SHALL include { locale, namespace, backend } so chained-backend orchestration can log the missed lookup.
Scenario: read miss throws BACKEND_LOCALE_NOT_AVAILABLE
- GIVEN an empty
MemoryBackend - WHEN
backend.read("en", "common")is awaited - THEN the call throws
I18nErrorwith code"I18N.BACKEND_LOCALE_NOT_AVAILABLE"
Verified by: packages/i18n/i18n-backend-memory/src/backend.test.ts::"read miss throws BACKEND_LOCALE_NOT_AVAILABLE"
Requirement: readMulti returns partial records without throwing
The readMulti(locale, namespaces, signal?) method SHALL return a Record<namespace, TranslationRecord> map containing only the namespaces that exist for the locale. Missing namespaces SHALL be omitted from the map (NOT undefined values, NOT thrown errors). When the locale is entirely absent, the method SHALL return {}.
Scenario: readMulti omits missing namespaces
- GIVEN a
MemoryBackendseeded with{ en: { common: {...}, errors: {...} } } - WHEN
backend.readMulti("en", ["common", "missing"])is awaited - THEN the result is
{ common: {...} }and does NOT contain amissingkey
Scenario: readMulti returns empty for absent locale
- WHEN
backend.readMulti("xx", ["any"])is awaited - THEN the result is
{}
Verified by: packages/i18n/i18n-backend-memory/src/backend.test.ts::"readMulti omits missing namespaces" AND packages/i18n/i18n-backend-memory/src/backend.test.ts::"readMulti returns empty for absent locale"
Requirement: save round-trip with deterministic lastModified
The save(locale, namespace, record, signal?) method SHALL persist the record into the in-memory store and SHALL update the modification ledger to the value of the injected now() function. A subsequent lastModified(locale, namespace) call SHALL return that exact timestamp.
Scenario: save then read round-trips
- WHEN
backend.save("en", "common", { hello: "Hi" })is awaited and thenbackend.read("en", "common")is awaited - THEN the read result equals
{ hello: "Hi" }
Scenario: lastModified returns the injected timestamp
- GIVEN a
MemoryBackendconstructed with{ now: () => 1234 } - WHEN
backend.save("en", "common", { x: "y" })is awaited and thenbackend.lastModified("en", "common")is awaited - THEN the result equals
1234
Verified by: packages/i18n/i18n-backend-memory/src/backend.test.ts::"save then read round-trips" AND packages/i18n/i18n-backend-memory/src/backend.test.ts::"lastModified returns the injected timestamp"
Requirement: AbortSignal cancellation throws BACKEND_TIMEOUT
Every async method (read, readMulti, save) SHALL accept an optional signal?: AbortSignal parameter. When the signal is already aborted at call time, the method SHALL throw I18nError with code I18N.BACKEND_TIMEOUT and SHALL NOT mutate state.
Scenario: pre-aborted signal aborts read
- GIVEN an
AbortControllerwhose signal is already aborted - WHEN
backend.read("en", "common", { signal })is awaited - THEN the call throws
I18nErrorwith code"I18N.BACKEND_TIMEOUT"
Verified by: packages/i18n/i18n-backend-memory/src/backend.test.ts::"pre-aborted signal aborts read with BACKEND_TIMEOUT"
Requirement: memoryBackend factory function
The package SHALL export a memoryBackend(options?: MemoryBackendOptions): MemoryBackend factory function that returns new MemoryBackend(options). The factory exists so consumers can compose backends fluently without new.
Scenario: factory returns MemoryBackend instance
- WHEN
memoryBackend()is called - THEN the result satisfies
result instanceof MemoryBackend
Scenario: factory passes seed through
- WHEN
memoryBackend({ seed: { en: { common: { x: "y" } } } }).read("en", "common")is awaited - THEN the result equals
{ x: "y" }
Verified by: packages/i18n/i18n-backend-memory/src/factory.test.ts::"factory returns MemoryBackend instance" AND packages/i18n/i18n-backend-memory/src/factory.test.ts::"factory passes seed through"
Requirement: Boundary validation via Zod
The constructor SHALL validate MemoryBackendOptions via MemoryBackendOptionsSchema (Zod) and SHALL throw I18nError(I18N.INVALID_OPTIONS) on invalid input.
Scenario: invalid options throw INVALID_OPTIONS
- WHEN
new MemoryBackend({ seed: "not an object" as unknown as MemorySeed })is called - THEN the call throws
I18nErrorwith code"I18N.INVALID_OPTIONS"
Verified by: packages/i18n/i18n-backend-memory/src/backend.test.ts::"invalid options throw INVALID_OPTIONS"
Requirement: OpenTelemetry instrumentation
The backend SHALL emit i18n.backend.memory.read_duration_ms (histogram, milliseconds), i18n.backend.memory.read_total{outcome="hit"|"miss"|"error"} (counter), i18n.backend.memory.read_errors_total{code} (counter), i18n.backend.memory.write_duration_ms (histogram), and i18n.backend.memory.write_total{outcome="ok"|"error"} (counter). The histograms SHALL be observed via performance.now() deltas around each call.
Scenario: read emits read_total counter on hit
- WHEN
backend.read("en", "common")succeeds - THEN
i18n.backend.memory.read_totalincrements withoutcome="hit"
Scenario: read emits read_total counter on miss
- WHEN
backend.read("en", "missing")throwsBACKEND_LOCALE_NOT_AVAILABLE - THEN
i18n.backend.memory.read_totalincrements withoutcome="miss"
Scenario: save emits write_total counter
- WHEN
backend.save("en", "common", {...})succeeds - THEN
i18n.backend.memory.write_totalincrements withoutcome="ok"
Verified by: packages/i18n/i18n-backend-memory/src/backend.spec-conformance.test.ts::"read emits read_total counter on hit" AND packages/i18n/i18n-backend-memory/src/backend.spec-conformance.test.ts::"read emits read_total counter on miss" AND packages/i18n/i18n-backend-memory/src/backend.spec-conformance.test.ts::"save emits write_total counter"
Requirement: Subpath exports
The package SHALL declare subpath exports so consumers can import only the surfaces they need:
@suaveplan/i18n-backend-memory— root barrel (class + factory + types)@suaveplan/i18n-backend-memory/backend—MemoryBackendclass only@suaveplan/i18n-backend-memory/factory—memoryBackendfactory function only
Scenario: every subpath resolves
- WHEN every documented subpath is imported in a consumer test fixture
- THEN every import resolves with no
Module not founderrors
Verified by: packages/i18n/i18n-backend-memory/src/index.test.ts::"every documented subpath resolves"
Requirement: Side-effect-free package
The package SHALL declare "sideEffects": false in package.json so bundlers can tree-shake unused exports, and SHALL NOT depend on @suaveplan/i18n core nor on any other backend package.
Scenario: package.json declares sideEffects false
- WHEN
package.jsonis parsed - THEN the
sideEffectsfield equalsfalse
Scenario: package.json does not depend on @suaveplan/i18n core
- WHEN
package.jsondependenciesis parsed - THEN the result does NOT contain
@suaveplan/i18n
Verified by: packages/i18n/i18n-backend-memory/src/index.test.ts::"package.json declares sideEffects false" AND packages/i18n/i18n-backend-memory/src/index.test.ts::"package.json does not depend on i18n core"