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:
@@ -7,12 +7,14 @@ import { KVStorageCounting } from '../helpers.js'
|
||||
/**
|
||||
* An in-process server with a network switch. While `online` is false, new requests fail like a
|
||||
* dropped connection (open SSE streams stay up, so pushes can still be simulated).
|
||||
* `dropStreams()` ends every open SSE stream, as a proxy or server restart would.
|
||||
*/
|
||||
export class TestNetwork {
|
||||
online = true
|
||||
readonly store = new KVStorageCounting(new KVStorageInMemory())
|
||||
readonly server: KVStorageHttpServer
|
||||
server: KVStorageHttpServer
|
||||
private readonly clients: KVStorageHttp[] = []
|
||||
private readonly streams = new Set<() => void>()
|
||||
|
||||
constructor(options: Partial<KVStorageHttpServerOptions> = {}) {
|
||||
this.server = new KVStorageHttpServer(this.store, { auth: 'none', ...options })
|
||||
@@ -24,13 +26,43 @@ export class TestNetwork {
|
||||
signer,
|
||||
fetch: async (url, init) => {
|
||||
if (!this.online) throw new TypeError('fetch failed')
|
||||
return this.server.handle(new Request(url, init))
|
||||
const response = await this.server.handle(new Request(url, init))
|
||||
return url.endsWith('/events') && response.body ? this.droppable(response) : response
|
||||
},
|
||||
})
|
||||
this.clients.push(client)
|
||||
return client
|
||||
}
|
||||
|
||||
dropStreams(): void {
|
||||
for (const drop of this.streams) drop()
|
||||
this.streams.clear()
|
||||
}
|
||||
|
||||
private droppable(response: Response): Response {
|
||||
const reader = response.body!.getReader()
|
||||
let drop!: () => void
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start: (controller) => {
|
||||
drop = () => {
|
||||
controller.close()
|
||||
void reader.cancel()
|
||||
}
|
||||
this.streams.add(drop)
|
||||
},
|
||||
pull: async (controller) => {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) controller.close()
|
||||
else controller.enqueue(value)
|
||||
},
|
||||
cancel: async () => {
|
||||
this.streams.delete(drop)
|
||||
await reader.cancel()
|
||||
},
|
||||
})
|
||||
return new Response(body, { status: response.status, headers: response.headers })
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await Promise.all(this.clients.map((c) => c.close()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user