Files
PotentialStorage/src/kv-storage/kv-storage-http-server.ts
T
Claude 08fb78721f
CI / Node 20 (pull_request) Successful in 10s
CI / Node 22 (pull_request) Successful in 11s
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
2026-10-23 08:05:03 +00:00

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 })
}
}