- 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
70 lines
2.4 KiB
TypeScript
70 lines
2.4 KiB
TypeScript
import { KVStorageHttp } from '../../src/kv-storage/kv-storage-http.js'
|
|
import { KVStorageHttpServer, type KVStorageHttpServerOptions } from '../../src/kv-storage/kv-storage-http-server.js'
|
|
import { KVStorageInMemory } from '../../src/kv-storage/kv-storage-in-memory.js'
|
|
import type { RequestSigner } from '../../src/utils/request-auth.js'
|
|
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())
|
|
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 })
|
|
}
|
|
|
|
client(signer?: RequestSigner): KVStorageHttp {
|
|
const client = new KVStorageHttp({
|
|
url: 'http://server',
|
|
signer,
|
|
fetch: async (url, init) => {
|
|
if (!this.online) throw new TypeError('fetch failed')
|
|
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()))
|
|
}
|
|
}
|