| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 |
- /**
- * On-disk JSON unit format: the file is always the current net state, kept
- * human-readable (pretty-printed, stable key order from insertion) — that
- * legibility is this backend's reason to exist. `single`-layout units are
- * one document with a unit header; `per-record`-layout units are a directory
- * with one version-stamped document per record (`<table>/<key>.json`) plus a
- * `global.json` for the global slot, so a write rewrites one record instead
- * of the whole unit.
- * @module @deepseek-ai/dsh-storage-json/src/format
- */
- import { StorageError } from '@deepseek-ai/dsh-storage'
- import type { KvUnitDescriptor } from '@deepseek-ai/dsh-storage'
- /** In-memory authoritative state of one unit; the file is its projection. `global` is `null` until first written. */
- export interface UnitState {
- version: number
- global: unknown
- tables: Map<string, Map<string, unknown>>
- }
- /**
- * Serialize a unit state to file content.
- * @param name - Unit name, stamped into the header.
- * @param state - Authoritative in-memory state.
- * @returns pretty-printed JSON document with a trailing newline.
- */
- export function serialize(name: string, state: UnitState): string {
- const tables: Record<string, Record<string, unknown>> = {}
- for (const [table, records] of state.tables) {
- tables[table] = Object.fromEntries(records)
- }
- const document = {
- unit: { name, version: state.version },
- global: state.global,
- tables,
- }
- return `${JSON.stringify(document, null, 2)}\n`
- }
- /**
- * Parse file content into unit state, validating shape and version.
- * @param text - Raw file content.
- * @param descriptor - Expected identity; version mismatch rejects.
- * @returns the parsed state.
- */
- export function parse(text: string, descriptor: KvUnitDescriptor): UnitState {
- let document: unknown
- try {
- document = JSON.parse(text)
- } catch (error) {
- throw new StorageError('malformed-medium', `unit '${descriptor.name}': file is not valid JSON`, { cause: error })
- }
- if (typeof document !== 'object' || document === null) {
- throw new StorageError('malformed-medium', `unit '${descriptor.name}': file is not a JSON object`)
- }
- const { unit, global: globalValue, tables } = document as Record<string, unknown>
- if (
- typeof unit !== 'object' || unit === null ||
- (unit as Record<string, unknown>)['name'] !== descriptor.name ||
- typeof (unit as Record<string, unknown>)['version'] !== 'number'
- ) {
- throw new StorageError('malformed-medium', `unit '${descriptor.name}': missing or foreign unit header`)
- }
- const version = (unit as Record<string, unknown>)['version'] as number
- if (version !== descriptor.version) {
- throw new StorageError(
- 'version-mismatch',
- `unit '${descriptor.name}': stored version ${version} != expected ${descriptor.version}`,
- )
- }
- if (typeof tables !== 'object' || tables === null) {
- throw new StorageError('malformed-medium', `unit '${descriptor.name}': tables is not an object`)
- }
- const state: UnitState = { version, global: globalValue ?? null, tables: new Map() }
- for (const table of descriptor.tables) {
- const records = (tables as Record<string, unknown>)[table]
- if (records === undefined) {
- state.tables.set(table, new Map())
- continue
- }
- if (typeof records !== 'object' || records === null || Array.isArray(records)) {
- throw new StorageError('malformed-medium', `unit '${descriptor.name}': table '${table}' is not an object`)
- }
- state.tables.set(table, new Map(Object.entries(records as Record<string, unknown>)))
- }
- return state
- }
- /**
- * Serialize one per-record document: the unit's version stamp plus the
- * record value, pretty-printed like the whole-unit document.
- * @param version - Unit format version, stamped into the header.
- * @param value - The record value (or the global singleton value).
- * @returns pretty-printed JSON document with a trailing newline.
- */
- export function serializeRecord(version: number, value: unknown): string {
- return `${JSON.stringify({ version, record: value }, null, 2)}\n`
- }
- /**
- * Parse one per-record document, validating its version stamp. A document
- * that is malformed or stamped with an unaccepted version is FOREIGN and
- * reads as absent — the per-record contract: one bad or stale record file
- * must not brick the whole unit, and an unaccepted version stamp discards the
- * record instead of migrating it (the whole-unit format rejects instead,
- * because there is exactly one document).
- * @param text - Raw per-record document content.
- * @param versions - Accepted unit versions (the current one plus the
- * descriptor's compatibleVersions); any other stamp discards the
- * document.
- * @returns the record value, or `undefined` for a foreign document.
- */
- export function parseRecord(text: string, versions: readonly number[]): unknown {
- let document: unknown
- try {
- document = JSON.parse(text)
- } catch {
- return undefined
- }
- if (typeof document !== 'object' || document === null) return undefined
- const { version: stamped, record } = document as Record<string, unknown>
- if (typeof stamped !== 'number' || !versions.includes(stamped)) return undefined
- return record
- }
|