M3: KVNode/KVStorage with arity dispatch, skip-scan keys, deleteTree, tree
Children are listed in subtree order (escaped segment + separator) so the skip-scan stays O(children) even when sibling keys extend each other with characters below the separator. Model-based property test vs a reference Map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
This commit is contained in:
@@ -397,6 +397,43 @@ Decisions taken where the brief was silent. Each is the simplest option consiste
|
||||
- **No fake `batch`**: `BaseKVDriver` deliberately does not provide a non-atomic `batch`, since stores
|
||||
use its presence to mean "atomic".
|
||||
|
||||
### M3 — KV node
|
||||
|
||||
- **Child order is "subtree order", not plain segment order.** The brief's skip-scan assumes a
|
||||
child's keys are contiguous, but they aren't when a sibling extends a segment with a character
|
||||
that sorts below the separator (for `/`: space and `!"#$%&'()*+,-.`). With children `a` and `a-1`
|
||||
the key space is `p/a`, `p/a-1`, `p/a-1/x`, `p/a/x`, so `a`'s descendants sort *after* `a-1`, and
|
||||
"yield if different from the previous segment" would emit `a` twice. `keys()`/`entries()` therefore
|
||||
order children by **escaped segment followed by the separator** (`a-1/` < `a/`), which is the order
|
||||
of their subtrees in key space. For keys without such characters this is plain lexicographic order.
|
||||
- Forward listing defers a child seen only through its own key on a small stack (bounded by
|
||||
segment length) until the scan passes `p/<seg>/`. It costs no extra driver calls beyond the
|
||||
brief's one seek per child with descendants.
|
||||
- Reverse listing needs one batched `getMany` probe per new child whose segment contains a
|
||||
below-separator character, to find shorter siblings that only have an own key.
|
||||
- `after` is exclusive in the direction of travel and follows the same order.
|
||||
- Implemented once in `src/kv/children.ts` (`scanChildren`) and reused by the event and document
|
||||
layers. A model-based property test checks forward, reverse, `after` and `limit` against
|
||||
brute force with keys like `a`, `a-1`, `a.b`, `a/x`, `%`, `~t`.
|
||||
- **Open question for the owner:** keep subtree order, or change segment escaping so the key
|
||||
layout nests exactly. That would mean escaping every below-separator character with an escape
|
||||
character above the separator, at the cost of the readable percent-encoding.
|
||||
- **Errors from async methods are rejections.** Every async method (`get`, `set`, …) is an `async`
|
||||
function, so a bad key yields a rejected promise rather than a synchronous throw. `child()` is sync
|
||||
and throws synchronously.
|
||||
- **`set(undefined)` is rejected** (`TypeError`: use `delete()`); `undefined` means "missing" in reads.
|
||||
- **`deleteTree()` on the root** is allowed and clears the whole store (chunked scan + delete, since
|
||||
the root range has no upper bound). Subtree deletion is `delete(selfKey)` plus
|
||||
`deleteRange(prefix, selfKey + succ(sep))`. It is not `[selfKey, …)`, which would also catch
|
||||
siblings such as `a!`.
|
||||
- **`tree({ depth })`** builds a plain object: leaves are values, interior nodes are objects. A node
|
||||
with both an own value and children exposes its own value under the `''` property (empty keys are
|
||||
invalid, so it can't collide). Reserved subtrees are skipped. Deeper levels are skipped by seeking
|
||||
rather than reading.
|
||||
- **`keys()` and `entries()` yield string keys** (number keys come back as strings).
|
||||
- **Exposed low-level properties** on every node: `path`, `selfKey`, `prefix`, `end` (subtree upper
|
||||
bound), `driver`, `separator`, `isRoot`. The event and document layers build on these.
|
||||
|
||||
<!-- decisions appended per milestone -->
|
||||
|
||||
## 13. Benchmark baselines
|
||||
|
||||
Reference in New Issue
Block a user