Files
deepseek-harness/packages/storage/storage-json/src/format.ts
T
_Kerman 501f387b46 feat(storage): add the per-record layout to the json backend
The json backend now serves two layouts. single (the default) keeps the
whole unit as one document at <root>/<name>.json; per-record keeps one
version-stamped document per record at <root>/<name>/<table>/<key>.json
(plus global.json), so one write rewrites one record instead of the whole
unit. The per-record unit is stateless — the directory is the state,
loadAll re-reads the tree, and every write is a single durable file
operation — while single keeps its authoritative in-memory state and
whole-file publish. Records keys must be path-safe ([a-zA-Z0-9_-]+);
an unsafe key rejects. A record document that is malformed or stamped
with another version reads as an absent record: one bad or stale file
never bricks the unit, and a version bump discards stale records instead
of migrating them. DomainSpec and KvUnitDescriptor gain the optional
layout field (defineDomain validates it, descriptorOf projects it).
2026-08-20 13:47:39 +08:00

124 lines
5.0 KiB
TypeScript

/**
* 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 a different version is FOREIGN and reads
* as absent — the per-record contract: one bad or stale record file must not
* brick the whole unit, and a version bump discards stale records instead of
* migrating them (the whole-unit format rejects instead, because there is
* exactly one document).
* @param text - Raw per-record document content.
* @param version - Expected unit version; a mismatch discards the document.
* @returns the record value, or `undefined` for a foreign document.
*/
export function parseRecord(text: string, version: 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 (stamped !== version) return undefined
return record
}