- BaseKVStorage / BaseEventStorage / BaseDocumentStorage define the API once; implementations provide only raw primitives (client and server share an API) - Drop KV indexes, query planner, unique indexes; document stores take index fields - Escape every char <= '/' so child keys stay contiguous; skip-scan is now simple - Event-sourced documents = event storage + any document store for projections - New folder layout (kv-storage, event-storage, document-storage, utils); tests only in test/ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
71 lines
2.8 KiB
TypeScript
71 lines
2.8 KiB
TypeScript
import type { BaseKVStorage, ScanOptions } from './base-kv-storage.js'
|
|
|
|
/**
|
|
* Serves any KV storage over HTTP for `KVStorageHttp`. Framework-agnostic: `handle(Request)`
|
|
* returns a `Response`. All routes are `POST /<primitive>` with a JSON body; `scan` streams NDJSON.
|
|
* The raw primitives use absolute keys, so this exposes the whole store behind `storage`.
|
|
*/
|
|
export class KVStorageHttpServer {
|
|
constructor(
|
|
private readonly storage: BaseKVStorage,
|
|
private readonly basePath = '',
|
|
) {}
|
|
|
|
async handle(request: Request): Promise<Response> {
|
|
const path = new URL(request.url).pathname
|
|
if (request.method !== 'POST' || !path.startsWith(this.basePath)) return KVStorageHttpServer.error(404, 'Not found')
|
|
try {
|
|
const body = (await request.json()) as { keys: string[]; entries: [string, unknown][] } & ScanOptions
|
|
switch (path.slice(this.basePath.length)) {
|
|
case '/readMany': {
|
|
const values = await this.storage.readMany(body.keys)
|
|
const missing = values.flatMap((v, i) => (v === undefined ? [i] : []))
|
|
return Response.json({ values: values.map((v) => v ?? null), missing })
|
|
}
|
|
case '/writeMany':
|
|
await this.storage.writeMany(body.entries)
|
|
return new Response(null, { status: 204 })
|
|
case '/removeMany':
|
|
await this.storage.removeMany(body.keys)
|
|
return new Response(null, { status: 204 })
|
|
case '/writeIfAbsent':
|
|
return Response.json(await this.storage.writeIfAbsent(body.entries))
|
|
case '/scan':
|
|
return this.scan(body)
|
|
default:
|
|
return KVStorageHttpServer.error(404, 'Not found')
|
|
}
|
|
} catch (error) {
|
|
return KVStorageHttpServer.error(500, error instanceof Error ? error.message : String(error))
|
|
}
|
|
}
|
|
|
|
private scan(options: ScanOptions): Response {
|
|
const iterator = this.storage.scan(options)[Symbol.asyncIterator]()
|
|
const encoder = new TextEncoder()
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
async pull(controller) {
|
|
// Send in small batches so short reads (skip-scan seeks) don't make the server run far ahead.
|
|
let chunk = ''
|
|
for (let i = 0; i < 32; i++) {
|
|
const next = await iterator.next()
|
|
if (next.done) {
|
|
if (chunk) controller.enqueue(encoder.encode(chunk))
|
|
return controller.close()
|
|
}
|
|
chunk += JSON.stringify(options.values ? next.value : [next.value[0]]) + '\n'
|
|
}
|
|
controller.enqueue(encoder.encode(chunk))
|
|
},
|
|
async cancel() {
|
|
await iterator.return?.()
|
|
},
|
|
})
|
|
return new Response(stream, { headers: { 'content-type': 'application/x-ndjson' } })
|
|
}
|
|
|
|
private static error(status: number, message: string): Response {
|
|
return Response.json({ error: message }, { status })
|
|
}
|
|
}
|