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,104 @@
|
||||
import { BaseKVDriver, type BatchOp, type ScanOptions } from './driver.js'
|
||||
import { SortedArrayMap, type SortedMap } from './sorted.js'
|
||||
|
||||
export interface InMemoryKVDriverOptions {
|
||||
/**
|
||||
* Copy values on write and read so callers can't mutate stored state through a shared
|
||||
* reference (matching serialising drivers). Default `true`. Disable for speed when values
|
||||
* are treated as immutable.
|
||||
*/
|
||||
clone?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory driver over a sorted map. Implements every optional capability:
|
||||
* `setIfAbsent`, atomic `batch` and `deleteRange`.
|
||||
*/
|
||||
export class InMemoryKVDriver extends BaseKVDriver {
|
||||
readonly #data: SortedMap<unknown> = new SortedArrayMap()
|
||||
readonly #clone: (v: unknown) => unknown
|
||||
|
||||
constructor(opts: InMemoryKVDriverOptions = {}) {
|
||||
super()
|
||||
this.#clone = opts.clone === false ? identity : cloneValue
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.#data.size
|
||||
}
|
||||
|
||||
async getMany(keys: string[]): Promise<unknown[]> {
|
||||
const out = new Array<unknown>(keys.length)
|
||||
for (let i = 0; i < keys.length; i++) out[i] = this.#clone(this.#data.get(keys[i]!))
|
||||
return out
|
||||
}
|
||||
|
||||
async setMany(entries: [string, unknown][]): Promise<void> {
|
||||
const clone = this.#clone
|
||||
this.#data.setMany(entries.map(([k, v]) => [k, clone(v)]))
|
||||
}
|
||||
|
||||
async deleteMany(keys: string[]): Promise<void> {
|
||||
for (const key of keys) this.#data.delete(key)
|
||||
}
|
||||
|
||||
async setIfAbsent(entries: [string, unknown][]): Promise<boolean[]> {
|
||||
return entries.map(([key, value]) => {
|
||||
if (this.#data.has(key)) return false
|
||||
this.#data.set(key, this.#clone(value))
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
async batch(ops: BatchOp[]): Promise<void> {
|
||||
// Applied synchronously within one microtask, so no other operation can observe a partial batch.
|
||||
for (const op of ops) {
|
||||
if (op.type === 'set') this.#data.set(op.key, this.#clone(op.value))
|
||||
else this.#data.delete(op.key)
|
||||
}
|
||||
}
|
||||
|
||||
override async deleteRange(gte: string, lt: string): Promise<void> {
|
||||
const data = this.#data
|
||||
const start = data.lowerBound(gte)
|
||||
const doomed: string[] = []
|
||||
for (let i = start; ; i++) {
|
||||
const key = data.keyAt(i)
|
||||
if (key === undefined || key >= lt) break
|
||||
doomed.push(key)
|
||||
}
|
||||
for (const key of doomed) data.delete(key)
|
||||
}
|
||||
|
||||
async *scan(opts: ScanOptions): AsyncGenerator<[string, unknown]> {
|
||||
const data = this.#data
|
||||
const { gte, lt, reverse = false, values = false } = opts
|
||||
let remaining = opts.limit ?? Infinity
|
||||
if (remaining <= 0) return
|
||||
// `last` is the last key yielded; if the map changes while we're suspended, re-seek from it.
|
||||
let last: string | undefined
|
||||
let version = data.version
|
||||
let i = reverse ? (lt === undefined ? data.size : data.lowerBound(lt)) - 1 : gte === undefined ? 0 : data.lowerBound(gte)
|
||||
for (;;) {
|
||||
if (data.version !== version) {
|
||||
version = data.version
|
||||
if (last !== undefined) i = reverse ? data.lowerBound(last) - 1 : data.lowerBound(last, true)
|
||||
}
|
||||
const key = data.keyAt(i)
|
||||
if (key === undefined || i < 0) return
|
||||
if (reverse ? gte !== undefined && key < gte : lt !== undefined && key >= lt) return
|
||||
last = key
|
||||
i += reverse ? -1 : 1
|
||||
yield [key, values ? this.#clone(data.get(key)) : undefined]
|
||||
if (--remaining <= 0) return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function identity(v: unknown): unknown {
|
||||
return v
|
||||
}
|
||||
|
||||
function cloneValue(v: unknown): unknown {
|
||||
return v === null || typeof v !== 'object' ? v : structuredClone(v)
|
||||
}
|
||||
Reference in New Issue
Block a user