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 /` 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 { 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({ 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 }) } }