M2: KV driver interface, BaseKVDriver, InMemoryKVDriver, conformance suite
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,77 @@
|
||||
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<void>
|
||||
deleteMany(keys: string[]): Promise<void>
|
||||
/** 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<boolean[]>
|
||||
/** Applies all ops atomically. */
|
||||
batch?(ops: BatchOp[]): Promise<void>
|
||||
deleteRange?(gte: string, lt: string): Promise<void>
|
||||
}
|
||||
|
||||
/** 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<void>
|
||||
abstract deleteMany(keys: string[]): Promise<void>
|
||||
abstract scan(opts: ScanOptions): AsyncIterable<[string, unknown]>
|
||||
|
||||
/** Chunk size used by the default `deleteRange`. */
|
||||
protected deleteRangeChunk = 1000
|
||||
|
||||
async deleteRange(gte: string, lt: string): Promise<void> {
|
||||
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<unknown | undefined> {
|
||||
return (await this.getMany([key]))[0]
|
||||
}
|
||||
|
||||
set(key: string, value: unknown): Promise<void> {
|
||||
return this.setMany([[key, value]])
|
||||
}
|
||||
|
||||
delete(key: string): Promise<void> {
|
||||
return this.deleteMany([key])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user