export interface ScanOptions { gte?: string lt?: string limit?: number reverse?: boolean /** When false/omitted, yielded values are `undefined`. */ values?: boolean } export type BatchOp = { type: 'set'; key: string; value: unknown } | { type: 'delete'; key: string } /** * A flat, string-keyed backend. Values are JSON-serialisable; byte-oriented backends serialise * internally with a {@link Codec}. All path, index and typing logic lives above this interface. */ export interface KVDriver { /** Same order as `keys`; `undefined` for missing keys. */ getMany(keys: string[]): Promise<(unknown | undefined)[]> setMany(entries: [key: string, value: unknown][]): Promise deleteMany(keys: string[]): Promise /** MUST yield in key order (descending when `reverse`). `gte` is inclusive, `lt` exclusive. */ scan(opts: ScanOptions): AsyncIterable<[key: string, value: unknown]> // Optional capabilities. Stores feature-detect them and fall back where possible. /** Writes each entry only if its key is absent. `true` = written. */ setIfAbsent?(entries: [string, unknown][]): Promise /** Applies all ops atomically. */ batch?(ops: BatchOp[]): Promise deleteRange?(gte: string, lt: string): Promise } /** Serialises values for byte/string-oriented backends. */ export interface Codec { encode(value: unknown): string decode(encoded: string): unknown } export const JsonCodec: Codec = { encode: (value) => JSON.stringify(value), decode: (encoded) => JSON.parse(encoded), } /** * Base class for drivers: implement the four required methods and get `deleteRange` * (scan + deleteMany in chunks) and single-key helpers for free. */ export abstract class BaseKVDriver implements KVDriver { abstract getMany(keys: string[]): Promise<(unknown | undefined)[]> abstract setMany(entries: [string, unknown][]): Promise abstract deleteMany(keys: string[]): Promise abstract scan(opts: ScanOptions): AsyncIterable<[string, unknown]> /** Chunk size used by the default `deleteRange`. */ protected deleteRangeChunk = 1000 async deleteRange(gte: string, lt: string): Promise { for (;;) { const keys: string[] = [] for await (const [key] of this.scan({ gte, lt, limit: this.deleteRangeChunk })) keys.push(key) if (keys.length === 0) return await this.deleteMany(keys) if (keys.length < this.deleteRangeChunk) return } } async get(key: string): Promise { return (await this.getMany([key]))[0] } set(key: string, value: unknown): Promise { return this.setMany([[key, value]]) } delete(key: string): Promise { return this.deleteMany([key]) } }