feat(storage): json backend — one human-readable file per unit
Atomic whole-file replacement (same-dir temp + fsync + rename + parent fsync); the in-memory unit state is authoritative and the file is always the current net state, pretty-printed. Missing files open as empty units and materialize on first write; foreign or unparsable files reject with malformed-medium, stored-version drift with version-mismatch.
This commit is contained in:
80
packages/storage/storage-json/tests/json-backend.spec.ts
Normal file
80
packages/storage/storage-json/tests/json-backend.spec.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterAll, describe, expect, it } from 'vitest'
|
||||
import { runKvBackendContract } from '../../storage/tests/contract.ts'
|
||||
import { JsonStorageBackend } from '../src/index.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
|
||||
async function freshRoot(): Promise<string> {
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-storage-json-'))
|
||||
roots.push(root)
|
||||
return root
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
for (const root of roots) await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
runKvBackendContract('json', async () => {
|
||||
const root = await freshRoot()
|
||||
return {
|
||||
backend: new JsonStorageBackend(root),
|
||||
reopen: async () => new JsonStorageBackend(root),
|
||||
}
|
||||
})
|
||||
|
||||
describe('json backend specifics', () => {
|
||||
const descriptor = { name: 'shape', version: 1, tables: ['t'], hasGlobal: true }
|
||||
|
||||
it('publishes a human-readable pretty-printed file', async () => {
|
||||
const root = await freshRoot()
|
||||
const backend = new JsonStorageBackend(root)
|
||||
const unit = await backend.kv.open(descriptor)
|
||||
await unit.putRecord('t', 'k', { hello: 'world' })
|
||||
const text = await readFile(join(root, 'shape.json'), 'utf8')
|
||||
expect(text).toBe(`${JSON.stringify(
|
||||
{ unit: { name: 'shape', version: 1 }, global: null, tables: { t: { k: { hello: 'world' } } } },
|
||||
null,
|
||||
2,
|
||||
)}\n`)
|
||||
await backend.close()
|
||||
})
|
||||
|
||||
it('defers materialization until the first write', async () => {
|
||||
const root = await freshRoot()
|
||||
const backend = new JsonStorageBackend(root)
|
||||
await backend.kv.open(descriptor)
|
||||
await expect(readFile(join(root, 'shape.json'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
await backend.close()
|
||||
})
|
||||
|
||||
it('rejects a malformed medium', async () => {
|
||||
const root = await freshRoot()
|
||||
await writeFile(join(root, 'shape.json'), 'not json at all', 'utf8')
|
||||
const backend = new JsonStorageBackend(root)
|
||||
await expect(backend.kv.open(descriptor)).rejects.toMatchObject({ code: 'malformed-medium' })
|
||||
await backend.close()
|
||||
})
|
||||
|
||||
it('rejects a foreign unit header', async () => {
|
||||
const root = await freshRoot()
|
||||
await writeFile(
|
||||
join(root, 'shape.json'),
|
||||
JSON.stringify({ unit: { name: 'other', version: 1 }, global: null, tables: {} }),
|
||||
'utf8',
|
||||
)
|
||||
const backend = new JsonStorageBackend(root)
|
||||
await expect(backend.kv.open(descriptor)).rejects.toMatchObject({ code: 'malformed-medium' })
|
||||
await backend.close()
|
||||
})
|
||||
|
||||
it('rejects double-open of one unit', async () => {
|
||||
const root = await freshRoot()
|
||||
const backend = new JsonStorageBackend(root)
|
||||
await backend.kv.open(descriptor)
|
||||
await expect(backend.kv.open(descriptor)).rejects.toMatchObject({ code: 'malformed-medium' })
|
||||
await backend.close()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user