95 lines
4.4 KiB
JavaScript
95 lines
4.4 KiB
JavaScript
import { serializeHandle } from "#binding";
|
|
/** Rebuild count per handle: bumped whenever a command buffer lands and
|
|
* renumbers the arena, invalidating ids captured before it. */
|
|
const HANDLE_EPOCHS = new WeakMap();
|
|
/** Record that `handle`'s arena was rebuilt. Resolvers created before the bump
|
|
* refuse to take a fresh snapshot afterwards (their ids are stale). */
|
|
export function markHandleMutated(handle) {
|
|
HANDLE_EPOCHS.set(handle, (HANDLE_EPOCHS.get(handle) ?? 0) + 1);
|
|
}
|
|
/** Arena sentinel in the node struct's parent field: the node has no parent. */
|
|
const NO_PARENT = 0xffffffff;
|
|
/**
|
|
* Lazy node materializer for the walk paths: serializes the handle's arena
|
|
* once, on the first stub materialization, then materializes nodes from that
|
|
* snapshot. Subclasses supply reader construction and per-node materialization
|
|
* so the hot path stays free of per-node closures.
|
|
*/
|
|
export class LazyChildResolver {
|
|
#handle;
|
|
#reader = null;
|
|
#sealed = false;
|
|
#epoch;
|
|
constructor(handle) {
|
|
this.#handle = handle;
|
|
this.#epoch = HANDLE_EPOCHS.get(handle) ?? 0;
|
|
}
|
|
/**
|
|
* Mark the visitor pass over. Child ids were captured at match time; once
|
|
* the pass's mutations land the arena is rebuilt and ids renumbered, so a
|
|
* later snapshot would map those stale ids onto the wrong nodes (or out of
|
|
* range). Failing loudly here beats silently wrong children.
|
|
*/
|
|
seal() {
|
|
this.#sealed = true;
|
|
}
|
|
/** Guards the `.children` getters: a first read after the pass is refused
|
|
* outright, even when no mutation landed — match-time ids cannot be trusted
|
|
* once the plugin no longer controls the tree. */
|
|
assertUnsealed() {
|
|
if (this.#sealed) {
|
|
throw new Error("Cannot read node content: this node was retained past its visitor pass " +
|
|
"and the tree may have changed since; read it inside the visitor.");
|
|
}
|
|
}
|
|
#ensureReader() {
|
|
if (this.#reader === null) {
|
|
// A node id proves the tree was read in-pass, so a deferred snapshot is
|
|
// still faithful as long as no command buffer rebuilt the arena since
|
|
// match time (ids renumber on rebuild). An existing reader is always
|
|
// safe: the snapshot is an immutable pre-mutation copy.
|
|
if ((HANDLE_EPOCHS.get(this.#handle) ?? 0) !== this.#epoch) {
|
|
throw new Error("Cannot read node content: this node was retained past its visitor pass " +
|
|
"and the tree has changed since; read it inside the visitor.");
|
|
}
|
|
// The serialized buffer already carries each node's `data` blob (read
|
|
// eagerly by the materializer), and the arena isn't mutated mid-visit —
|
|
// so no separate lazy NAPI fetch is needed. This also keeps walk-path
|
|
// children consistent with the fully materialized tree (no `data` key
|
|
// when a node has none).
|
|
this.#reader = this.createReader(serializeHandle(this.#handle));
|
|
}
|
|
return this.#reader;
|
|
}
|
|
/** Materialize one node for a child stub's first real-field read. */
|
|
materializeOne(nodeId) {
|
|
return this.materializeNode(this.#ensureReader(), nodeId);
|
|
}
|
|
/** Arena id of `nodeId`'s parent in the pass snapshot, or undefined at the root. */
|
|
parentIdOf(nodeId) {
|
|
this.assertUnsealed();
|
|
const parentId = this.readParentId(this.#ensureReader(), nodeId);
|
|
return parentId === NO_PARENT ? undefined : parentId;
|
|
}
|
|
/** Per-parent child-id→index maps, built lazily: null until a plugin calls
|
|
* `indexInParent` (most never do). Cache-safe because the snapshot is immutable. */
|
|
#childIndexByParent = null;
|
|
/** Index of `nodeId` within its parent's children in the pass snapshot,
|
|
* or undefined at the root. */
|
|
indexInParent(nodeId) {
|
|
this.assertUnsealed();
|
|
const reader = this.#ensureReader();
|
|
const parentId = this.readParentId(reader, nodeId);
|
|
if (parentId === NO_PARENT)
|
|
return undefined;
|
|
const byParent = (this.#childIndexByParent ??= new Map());
|
|
let indexById = byParent.get(parentId);
|
|
if (indexById === undefined) {
|
|
const map = new Map();
|
|
this.readChildIds(reader, parentId).forEach((id, i) => map.set(id, i));
|
|
byParent.set(parentId, map);
|
|
indexById = map;
|
|
}
|
|
return indexById.get(nodeId);
|
|
}
|
|
}
|