Simplify: one abstract base class per layer
CI / Node 20 (pull_request) Successful in 10s
CI / Node 22 (pull_request) Successful in 11s

- BaseKVStorage / BaseEventStorage / BaseDocumentStorage define the API once;
  implementations provide only raw primitives (client and server share an API)
- Drop KV indexes, query planner, unique indexes; document stores take index fields
- Escape every char <= '/' so child keys stay contiguous; skip-scan is now simple
- Event-sourced documents = event storage + any document store for projections
- New folder layout (kv-storage, event-storage, document-storage, utils); tests only in test/

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 08:05:03 +00:00
parent e27541c46a
commit 08fb78721f
80 changed files with 2518 additions and 6337 deletions
+75
View File
@@ -0,0 +1,75 @@
import { DocumentPath, type Doc } from './document-path.js'
import { DocumentQuery } from './document-query.js'
import { Json } from './json.js'
/** Update operators. Paths may use dots. `$push`/`$addToSet` accept `{ $each: [...] }`. */
export interface UpdateSpec {
$set?: Record<string, unknown>
$unset?: Record<string, unknown>
$inc?: Record<string, number>
$push?: Record<string, unknown>
$pull?: Record<string, unknown>
$addToSet?: Record<string, unknown>
}
/**
* A resolved change to one document. Operators are resolved to final values (`$inc` becomes
* `set: { n: 6 }`), so replaying changes is deterministic.
*/
export type DocChange = { op: 'set'; doc: Doc } | { op: 'patch'; set: Record<string, unknown>; unset: string[] } | { op: 'delete' }
export class DocumentUpdate {
/** Resolves update operators against the current state into a patch. */
static resolve(state: Doc, spec: UpdateSpec): DocChange {
const set: Record<string, unknown> = {}
const unset: string[] = []
const array = (path: string) => {
const current = DocumentPath.get(state, path) ?? []
if (!Array.isArray(current)) throw new TypeError(`"${path}" is not an array`)
return Json.clone(current)
}
const items = (value: unknown) => (Json.isObject(value) && Array.isArray(value.$each) ? value.$each : [value])
for (const [op, fields] of Object.entries(spec)) {
for (const [path, value] of Object.entries(fields as Record<string, unknown>)) {
if (path in set || unset.includes(path)) throw new TypeError(`"${path}" is updated twice`)
switch (op) {
case '$set':
set[path] = Json.clone(value)
break
case '$unset':
unset.push(path)
break
case '$inc': {
const current = DocumentPath.get(state, path) ?? 0
if (typeof current !== 'number' || typeof value !== 'number') throw new TypeError(`$inc needs numbers at "${path}"`)
set[path] = current + value
break
}
case '$push':
set[path] = [...array(path), ...Json.clone(items(value))]
break
case '$addToSet':
set[path] = items(value).reduce<unknown[]>((acc, item) => (acc.some((x) => Json.equal(x, item)) ? acc : [...acc, Json.clone(item)]), array(path))
break
case '$pull':
set[path] = array(path).filter((item) => !DocumentQuery.matchesValue(item, value))
break
default:
throw new TypeError(`Unsupported update operator ${op}`)
}
}
}
return { op: 'patch', set, unset }
}
/** Applies a change to a state (`undefined` = no document). */
static apply(state: Doc | undefined, change: DocChange): Doc | undefined {
if (change.op === 'delete') return undefined
if (change.op === 'set') return Json.clone(change.doc)
const next = Json.clone(state ?? {})
for (const [path, value] of Object.entries(change.set)) DocumentPath.set(next, path, Json.clone(value))
for (const path of change.unset) DocumentPath.unset(next, path)
return next
}
}