Plain operations: last write wins, hash-keyed events, simpler composite
- 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:
@@ -15,14 +15,14 @@ export function runKVStorageConformance(name: string, make: () => BaseKVStorage
|
||||
describe('primitives', () => {
|
||||
beforeEach(async () => {
|
||||
await db.writeMany([
|
||||
['b', 2],
|
||||
['a/2', { n: 'a2' }],
|
||||
['c', [3]],
|
||||
['a', 1],
|
||||
['a/1', 'a1'],
|
||||
['a0', null],
|
||||
['10', 10],
|
||||
['9', 9],
|
||||
{ key: 'b', value: 2 },
|
||||
{ key: 'a/2', value: { n: 'a2' } },
|
||||
{ key: 'c', value: [3] },
|
||||
{ key: 'a', value: 1 },
|
||||
{ key: 'a/1', value: 'a1' },
|
||||
{ key: 'a0', value: null },
|
||||
{ key: '10', value: 10 },
|
||||
{ key: '9', value: 9 },
|
||||
])
|
||||
})
|
||||
|
||||
@@ -48,16 +48,9 @@ export function runKVStorageConformance(name: string, make: () => BaseKVStorage
|
||||
expect(await db.readMany(['b', 'c'])).toEqual([undefined, [3]])
|
||||
})
|
||||
|
||||
it('writeIfAbsent writes only absent keys, and only one concurrent claim wins', async () => {
|
||||
expect(
|
||||
await db.writeIfAbsent([
|
||||
['a', 'x'],
|
||||
['new', 'y'],
|
||||
]),
|
||||
).toEqual([false, true])
|
||||
expect(await db.readMany(['a', 'new'])).toEqual([1, 'y'])
|
||||
const results = await Promise.all([1, 2, 3].map((n) => db.writeIfAbsent([['claim', n]])))
|
||||
expect(results.flat().filter(Boolean)).toHaveLength(1)
|
||||
it('writes and removes in one batch', async () => {
|
||||
await db.writeMany([{ key: 'b' }, { key: 'new', value: 'y' }, { key: 'a', value: 'x' }])
|
||||
expect(await db.readMany(['a', 'b', 'new'])).toEqual(['x', undefined, 'y'])
|
||||
})
|
||||
|
||||
it('keeps scanning correctly when keys change mid-scan', async () => {
|
||||
@@ -74,22 +67,18 @@ export function runKVStorageConformance(name: string, make: () => BaseKVStorage
|
||||
it('emits change events for writes and removals', async () => {
|
||||
const events: unknown[] = []
|
||||
const off = db.on('change', (event) => events.push(event))
|
||||
await db.writeMany([['x', 1]])
|
||||
await db.writeMany([{ key: 'x', value: 1 }])
|
||||
await db.removeMany(['x'])
|
||||
await db.writeIfAbsent([
|
||||
['a', 'no'],
|
||||
['y', 2],
|
||||
])
|
||||
await db.writeMany([])
|
||||
off()
|
||||
expect(events).toEqual([
|
||||
{ changes: [{ key: 'x', value: 1 }], origin: 'local' },
|
||||
{ changes: [{ key: 'x' }], origin: 'local' },
|
||||
{ changes: [{ key: 'y', value: 2 }], origin: 'local' },
|
||||
])
|
||||
})
|
||||
|
||||
it('stores bigint and Uint8Array values', async () => {
|
||||
await db.writeMany([['bin', { n: 10n ** 20n, bytes: new Uint8Array([0, 255]) }]])
|
||||
await db.writeMany([{ key: 'bin', value: { n: 10n ** 20n, bytes: new Uint8Array([0, 255]) } }])
|
||||
expect(await db.readMany(['bin'])).toEqual([{ n: 10n ** 20n, bytes: new Uint8Array([0, 255]) }])
|
||||
})
|
||||
|
||||
@@ -123,6 +112,14 @@ export function runKVStorageConformance(name: string, make: () => BaseKVStorage
|
||||
expect(await n.getMany(['a', 'b'])).toEqual([undefined, 2])
|
||||
})
|
||||
|
||||
it('sets and gets direct children as fields', async () => {
|
||||
const user = db.child('users').child('1')
|
||||
await user.setFields({ name: 'Ada', hobby: 'chess', skipped: undefined })
|
||||
await user.child('address').set('city', 'Paris')
|
||||
expect(await user.get('hobby')).toBe('chess')
|
||||
expect(await user.getFields()).toEqual({ hobby: 'chess', name: 'Ada' })
|
||||
})
|
||||
|
||||
it('keeps own values and children independent', async () => {
|
||||
const user = db.child('users').child('1')
|
||||
await user.set('own')
|
||||
@@ -157,7 +154,7 @@ export function runKVStorageConformance(name: string, make: () => BaseKVStorage
|
||||
await n.set('c', 3)
|
||||
await n.child('c').child('x').set('y', 1)
|
||||
await n.set('a-1', 4)
|
||||
await db.writeMany([['n/~e/000000000001', {}]])
|
||||
await db.writeMany([{ key: 'n/~e/000000000001', value: {} }])
|
||||
expect(await collect(n.keys())).toEqual(['a', 'a-1', 'b', 'c'])
|
||||
expect(await collect(n.keys({ reverse: true }))).toEqual(['c', 'b', 'a-1', 'a'])
|
||||
expect(await collect(n.keys({ after: 'a', limit: 2 }))).toEqual(['a-1', 'b'])
|
||||
|
||||
Reference in New Issue
Block a user