M9: benchmarks, chunked sorted map and JSON clone for the in-memory driver

Profiling showed document updates dominated by O(n) sorted-array splices; the
chunked map makes inserts O(log n + chunk). Baselines recorded in DESIGN.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
This commit is contained in:
Claude
2026-10-23 07:25:04 +00:00
parent 76f791f54b
commit 5a370f8c78
10 changed files with 376 additions and 68 deletions
+20 -31
View File
@@ -1,11 +1,11 @@
import { BaseKVDriver, type BatchOp, type ScanOptions } from './driver.js'
import { SortedArrayMap, type SortedMap } from './sorted.js'
import { ChunkedSortedMap, 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.
* Deep-copy values on write and read so callers can't mutate stored state through a shared
* reference (matching serialising drivers). Values must be JSON-compatible. Default `true`.
* Disable for speed when values are treated as immutable.
*/
clone?: boolean
}
@@ -15,7 +15,7 @@ export interface InMemoryKVDriverOptions {
* `setIfAbsent`, atomic `batch` and `deleteRange`.
*/
export class InMemoryKVDriver extends BaseKVDriver {
readonly #data: SortedMap<unknown> = new SortedArrayMap()
readonly #data: SortedMap<unknown> = new ChunkedSortedMap()
readonly #clone: (v: unknown) => unknown
constructor(opts: InMemoryKVDriverOptions = {}) {
@@ -59,37 +59,17 @@ export class InMemoryKVDriver extends BaseKVDriver {
}
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)
const doomed = [...this.#data.keys({ gte, lt })]
for (const key of doomed) this.#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]
// The key iterator re-seeks by itself if the map changes while we're suspended.
for (const key of this.#data.keys({ gte, lt, reverse })) {
yield [key, values ? this.#clone(this.#data.get(key)) : undefined]
if (--remaining <= 0) return
}
}
@@ -99,6 +79,15 @@ function identity(v: unknown): unknown {
return v
}
/** Deep copy of a JSON-compatible value (much faster than `structuredClone`). */
function cloneValue(v: unknown): unknown {
return v === null || typeof v !== 'object' ? v : structuredClone(v)
if (v === null || typeof v !== 'object') return v
if (Array.isArray(v)) {
const out = new Array<unknown>(v.length)
for (let i = 0; i < v.length; i++) out[i] = cloneValue(v[i])
return out
}
const out: Record<string, unknown> = {}
for (const k in v) out[k] = cloneValue((v as Record<string, unknown>)[k])
return out
}