Simplify: one abstract base class per layer
- 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
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user