- KV: one write primitive, write(changes) with { key, value? }; writeIfAbsent removed.
The server executes exactly what clients send and never reads values, so they can be
encrypted end to end. Adds setFields/getFields on the base class.
- HTTP: no server history or event ids; SSE relays every change (own writes included) and
the client emits resync when a dropped stream reconnects.
- Composite: writes apply locally, then go to the source (undone locally if refused);
pushed changes are applied and announced only where they differ from local values;
offline writes go to a queue store (any KV storage); sync() = replay, diff, replay.
Removes KVReplicaSet, KVOutbox, KeyCounter, SerialQueue and RetryTimer.
- Events: id = <time>_<sha256(time + canonical data)>, so concurrent appends never
collide and retries can't duplicate; no versions or ConcurrencyError.
- Event-sourced documents: read, resolve, append, apply; catchUp folds in foreign events;
getAt takes an event id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
55 lines
2.8 KiB
TypeScript
55 lines
2.8 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
import { KVStorageHttp } from '../../src/kv-storage/kv-storage-http.js'
|
|
import { KVStorageHttpServer } from '../../src/kv-storage/kv-storage-http-server.js'
|
|
import { KVStorageInMemory } from '../../src/kv-storage/kv-storage-in-memory.js'
|
|
import { EventStorageKV } from '../../src/event-storage/event-storage-kv.js'
|
|
import { DocumentStorageKV } from '../../src/document-storage/document-storage-kv.js'
|
|
import { EventSourcedDocumentStorage } from '../../src/document-storage/event-sourced-document-storage.js'
|
|
import { runKVStorageConformance } from './kv-storage.conformance.js'
|
|
import { serve } from './node-server.js'
|
|
|
|
runKVStorageConformance('KVStorageHttp (in-process)', () => {
|
|
const server = new KVStorageHttpServer(new KVStorageInMemory(), { auth: 'none', basePath: '/kv' })
|
|
return new KVStorageHttp({ url: 'http://test/kv', fetch: async (input, init) => server.handle(new Request(input, init)) })
|
|
})
|
|
|
|
describe('over a real socket', () => {
|
|
let server: Awaited<ReturnType<typeof serve>>
|
|
const stores: KVStorageHttpServer[] = []
|
|
beforeAll(async () => {
|
|
// One socket; each test storage gets its own backend under /<n>.
|
|
server = await serve((request) => stores[Number(new URL(request.url).pathname.split('/')[1])]!.handle(request))
|
|
})
|
|
afterAll(() => server.close())
|
|
const fresh = () => {
|
|
stores.push(new KVStorageHttpServer(new KVStorageInMemory(), { auth: 'none', basePath: `/${stores.length}` }))
|
|
return new KVStorageHttp({ url: `${server.url}/${stores.length - 1}` })
|
|
}
|
|
|
|
runKVStorageConformance('KVStorageHttp (node:http)', fresh)
|
|
|
|
it('stops a long scan early', async () => {
|
|
const db = fresh()
|
|
await db.writeMany(Array.from({ length: 5000 }, (_, i) => ({ key: `k${String(i).padStart(5, '0')}`, value: i })))
|
|
const seen: string[] = []
|
|
for await (const [key] of db.scan()) if (seen.push(key) === 3) break
|
|
expect(seen).toEqual(['k00000', 'k00001', 'k00002'])
|
|
})
|
|
|
|
it('runs events and event-sourced documents over HTTP', async () => {
|
|
const kv = fresh()
|
|
const docs = new EventSourcedDocumentStorage(new EventStorageKV(kv), new DocumentStorageKV(kv.child('projections')))
|
|
const id = await docs.child('people').insert({ email: 'a@x', n: 1 })
|
|
await docs.child('people').update(id, { $inc: { n: 1 } })
|
|
expect(await docs.child('people').findOne({ email: 'a@x' })).toEqual({ key: id, data: { email: 'a@x', n: 2 } })
|
|
})
|
|
})
|
|
|
|
describe('server', () => {
|
|
it('maps errors to 404 / 500', async () => {
|
|
const server = new KVStorageHttpServer(new KVStorageInMemory(), { auth: 'none' })
|
|
expect((await server.handle(new Request('http://x/nope', { method: 'POST', body: '{}' }))).status).toBe(404)
|
|
expect((await server.handle(new Request('http://x/readMany', { method: 'POST', body: '{"keys": 5}' }))).status).toBe(500)
|
|
})
|
|
})
|