Plain operations: last write wins, hash-keyed events, simpler composite
CI / Node 22 (pull_request) Successful in 12s
CI / Node 26 (pull_request) Successful in 13s
CI / Node 24 (pull_request) Successful in 14s

- 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
This commit is contained in:
Claude
2026-10-23 13:33:07 +00:00
parent 1f82ec9110
commit 631c75428b
34 changed files with 814 additions and 1387 deletions
+109 -59
View File
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { KVStorageComposite } from '../../src/kv-storage/kv-storage-composite.js'
import { KVStorageInMemory } from '../../src/kv-storage/kv-storage-in-memory.js'
import type { BaseKVStorage } from '../../src/kv-storage/base-kv-storage.js'
import type { BaseKVStorage, KVChangeEvent } from '../../src/kv-storage/base-kv-storage.js'
import { DocumentStorageKV } from '../../src/document-storage/document-storage-kv.js'
import { collect, KVStorageCounting } from '../helpers.js'
import { runKVStorageConformance } from './kv-storage.conformance.js'
@@ -23,21 +23,21 @@ afterEach(async () => {
await network?.close()
})
function composite(persistent: BaseKVStorage = new KVStorageInMemory()) {
const c = new KVStorageComposite({ source: network.client(), replicas: [new KVStorageInMemory(), persistent], retry: { baseDelay: 5, maxDelay: 20 } })
function composite(persistent: BaseKVStorage = new KVStorageInMemory(), queue?: BaseKVStorage) {
const c = new KVStorageComposite({ source: network.client(), replicas: [new KVStorageInMemory(), persistent], queue, retryMs: 10 })
composites.push(c)
return c
}
describe('KVStorageComposite sync', () => {
it('loads existing data with one full sync, then serves reads locally', async () => {
it('loads existing data with one sync, then serves reads locally', async () => {
network = new TestNetwork()
await network.store.writeMany([
['users/1', { name: 'Ada' }],
['users/2', { name: 'Bob' }],
{ key: 'users/1', value: { name: 'Ada' } },
{ key: 'users/2', value: { name: 'Bob' } },
])
const db = composite()
await db.synced()
await db.sync()
network.store.reset()
expect(await db.child('users').get('1')).toEqual({ name: 'Ada' })
expect(await collect(db.child('users').keys())).toEqual(['1', '2'])
@@ -48,7 +48,7 @@ describe('KVStorageComposite sync', () => {
network = new TestNetwork()
const memory = new KVStorageInMemory()
const db = composite(memory)
await db.synced()
await db.sync()
const seen = db.waitFor('change', (e) => e.origin === 'remote')
await network.client().child('users').set('9', { name: 'Remote' })
expect((await seen).changes).toEqual([{ key: 'users/9', value: { name: 'Remote' } }])
@@ -56,10 +56,29 @@ describe('KVStorageComposite sync', () => {
expect(await memory.readMany(['users/9'])).toEqual([{ name: 'Remote' }])
})
it('announces only remote changes that differ from local values', async () => {
network = new TestNetwork()
const db = composite()
await db.sync()
const events: KVChangeEvent[] = []
db.on('change', (e) => events.push(e))
await db.set('mine', 1) // relayed back by the server: same value, so no remote event
const other = db.waitFor('change', (e) => e.origin === 'remote')
await network.client().writeMany([
{ key: 'mine', value: 1 },
{ key: 'theirs', value: 2 },
])
await other
expect(events).toEqual([
{ changes: [{ key: 'mine', value: 1 }], origin: 'local' },
{ changes: [{ key: 'theirs', value: 2 }], origin: 'remote' },
])
})
it('queues writes while offline and replays them in order when back online', async () => {
network = new TestNetwork()
const db = composite()
await db.synced()
await db.sync()
network.online = false
await db.child('users').set('1', { v: 1 })
await db.child('users').set('1', { v: 2 })
@@ -67,68 +86,97 @@ describe('KVStorageComposite sync', () => {
await db.child('users').set('2', { v: 3 })
expect(db.queued).toBe(4)
expect(await db.child('users').get('2')).toEqual({ v: 3 }) // served locally
await expect(db.writeIfAbsent([['x', 1]])).rejects.toThrow(/Cannot reach/)
network.online = true
await vi.waitFor(() => expect(db.queued).toBe(0))
expect(await network.store.readMany(['users/1', 'users/2'])).toEqual([undefined, { v: 3 }])
})
it('keeps queued local values when the server pushes an older value for the same key', async () => {
it('the last write wins when the server pushes a change while a local write is queued', async () => {
network = new TestNetwork()
const db = composite()
await db.synced()
await db.sync()
network.online = false
await db.child('k').set({ from: 'local' })
await network.store.writeMany([['k', { from: 'remote' }]]) // pushed over the still-open stream
await new Promise((r) => setTimeout(r, 20))
expect(await db.child('k').get()).toEqual({ from: 'local' })
await db.set('k', { from: 'local' })
await network.store.writeMany([{ key: 'k', value: { from: 'remote' } }]) // pushed over the still-open stream
await vi.waitFor(async () => expect(await db.get('k')).toEqual({ from: 'remote' }))
network.online = true
await vi.waitFor(() => expect(db.queued).toBe(0))
await vi.waitFor(() => expect(db.queued).toBe(0)) // the queued write is sent last, so it wins
expect(await network.store.readMany(['k'])).toEqual([{ from: 'local' }])
expect(await db.get('k')).toEqual({ from: 'local' })
})
it('restarts from the persistent replica and resumes without a full sync', async () => {
it('restarts from the persistent replica, then catches up', async () => {
network = new TestNetwork()
const persistent = new KVStorageInMemory()
const first = composite(persistent)
await first.child('a').set(1)
await first.synced()
const queue = new KVStorageInMemory()
const first = composite(persistent, queue)
await first.set('a', 1)
await first.set('gone', 1)
await first.sync()
await first.close()
await network.client().writeMany([['b', 2]]) // missed while "closed"
await network.client().writeMany([{ key: 'b', value: 2 }, { key: 'gone' }]) // missed while closed
network.store.reset()
const second = composite(persistent)
expect(await second.child('a').get()).toBe(1) // available before contacting the server
await vi.waitFor(async () => expect(await second.child('b').get()).toBe(2))
expect(network.store.calls.scan).toBeUndefined() // resumed via Last-Event-ID, no full scan
})
it('falls back to a full sync when the server cannot resume', async () => {
network = new TestNetwork({ historySize: 1 })
const persistent = new KVStorageInMemory()
const first = composite(persistent)
await first.child('a').set(1)
await first.synced()
await first.close()
await network.client().writeMany([['b', 2]])
await network.client().writeMany([['c', 3]])
await network.client().removeMany(['a'])
const second = composite(persistent)
await vi.waitFor(async () => expect(await collect(second.keys())).toEqual(['b', 'c']))
})
it('replays an outbox left by a previous session', async () => {
network = new TestNetwork()
const persistent = new KVStorageInMemory()
const first = composite(persistent)
await first.synced()
network.online = false
await first.child('offline').set('queued')
const second = composite(persistent, queue)
expect(await second.get('a')).toBe(1) // available without the server
network.online = true
await second.sync()
expect(await collect(second.scan({ values: true }))).toEqual([
['a', 1],
['b', 2],
])
})
it('replays writes queued by a previous session from a persistent queue', async () => {
network = new TestNetwork()
const persistent = new KVStorageInMemory()
const queue = persistent.child('~sync')
const first = new KVStorageComposite({ source: network.client(), replicas: [persistent], queue, prefix: 'data' })
composites.push(first)
await first.sync()
network.online = false
await first.child('data').set('offline', 'queued')
await first.close()
network.online = true
const second = composite(persistent)
await second.start()
await vi.waitFor(async () => expect(await network.store.readMany(['offline'])).toEqual(['queued']))
const second = new KVStorageComposite({ source: network.client(), replicas: [persistent], queue, prefix: 'data' })
composites.push(second)
expect(second.queued).toBe(0)
await second.sync()
expect(second.queued).toBe(0)
expect(await network.store.readMany(['data/offline'])).toEqual(['queued'])
})
it('catches up on changes missed while the stream was down', async () => {
network = new TestNetwork()
const db = composite()
await db.sync()
const resynced = vi.fn()
db.on('change', resynced)
network.online = false
network.dropStreams()
await network.store.writeMany([{ key: 'missed', value: 1 }])
network.online = true
await vi.waitFor(async () => expect(await db.get('missed')).toBe(1), { timeout: 5000 })
expect(resynced).toHaveBeenCalledWith({ changes: [{ key: 'missed', value: 1 }], origin: 'remote' })
})
it('syncs only its prefix and rejects keys outside it', async () => {
network = new TestNetwork()
await network.store.writeMany([
{ key: 'alice/a', value: 1 },
{ key: 'bob/b', value: 2 },
])
const db = new KVStorageComposite({ source: network.client(), replicas: [new KVStorageInMemory()], prefix: 'alice' })
composites.push(db)
await db.sync()
expect(await collect(db.scan({ values: true }))).toEqual([['alice/a', 1]])
await expect(db.child('bob').get('b')).rejects.toBeInstanceOf(RangeError)
})
it('refuses a queue inside the synced range of a replica', () => {
const replica = new KVStorageInMemory()
expect(() => new KVStorageComposite({ source: new KVStorageInMemory(), replicas: [replica], queue: replica.child('q') })).toThrow(/outside/)
expect(() => new KVStorageComposite({ source: new KVStorageInMemory(), replicas: [replica], queue: replica.child('q'), prefix: 'data' })).not.toThrow()
})
it('serves document queries from the local replica, offline too', async () => {
@@ -137,12 +185,12 @@ describe('KVStorageComposite sync', () => {
const users = new DocumentStorageKV(db).child('users')
await users.set('1', { age: 30 })
await users.set('2', { age: 40 })
await db.synced()
await db.sync()
network.online = false
expect((await collect(users.find({ age: { $gt: 35 } }))).map((r) => r.key)).toEqual(['2'])
})
it('does not undo writes made while a full sync is scanning', async () => {
it('does not undo writes made while a sync is scanning', async () => {
// A source whose scan reads ahead (like the HTTP stream) and pauses after its first entry.
let release!: () => void
let reached!: () => void
@@ -150,8 +198,8 @@ describe('KVStorageComposite sync', () => {
const paused = new Promise<void>((r) => (reached = r))
const inner = new KVStorageInMemory()
await inner.writeMany([
['a', 1],
['c', 3],
{ key: 'a', value: 1 },
{ key: 'c', value: 3 },
])
const source = new KVStorageCounting(inner)
const scan = source.scan.bind(source)
@@ -163,13 +211,15 @@ describe('KVStorageComposite sync', () => {
}
}
const persistent = new KVStorageInMemory()
await persistent.writeMany([['a0', 'stale']]) // a replica copy from before, without sync metadata
await persistent.writeMany([{ key: 'a0', value: 'stale' }])
const db = new KVStorageComposite({ source, replicas: [persistent] })
composites.push(db)
void db.start()
await paused // the diff has passed 'b' in the source but not in the replica
await db.writeMany([['b', 2]])
await db.set('b', 2)
release()
await db.synced()
await vi.waitFor(() => expect(db.queued).toBe(0))
await db.sync()
expect(await collect(db.scan({ values: true }))).toEqual([
['a', 1],
['b', 2],