Skip to content
Shapemetry

@huukhanhnguyen/model / nodes/LaneNode

nodes/LaneNode

ChainNode

Base for the model's internal "lane" nodes — one instance per tables[]/patterns[]/components[]/views[]/sheets[] CHAIN, plus one shared node for the resolved 3D scene. These wrap the static/on-demand document lanes (schema/model.ts) in the SAME BaseNode dirty-gated pull contract the graph proper uses, so a hidden cross-lane read (Table.get, sampleTexture, resolvePlaced*, scene()) registers a REAL _dependents edge (via dependOn, see core/evaluationStack.ts) instead of the old isVolatile "never trust the cache" patch. (Attached files need no lane node of their own — a url-method parameter IS an ordinary graph node, so a reference to its key is an ordinary dependency edge. Materials and layers are both authored as material/layer NODES and resolved via SnapshotLaneNode → collectMaterialNodes / collectLayers.)

Never added to a NodeCollection, never model-key-addressable — Model.ts owns one Map per lane (keyed by the record/chain's own name/key, e.g. "rebarSchedule" for a table) as purely internal engine bookkeeping, not a user-facing model reference (never consulted by NodeCollection's keyedNodes()/generateKey()).

Naming (post 2026-07-21 convergence): a "node" is either a chain STEP (DataNode/ParameterNode) or a whole CHAIN viewed as one cached unit (ChainNode — dual-mode: plain resolve() OR per-args resolveWith()). There is no separate ArgsChainNode / RecordNode class anymore.

Extends

Type Parameters

C

C

V

V

Constructors

Constructor

new ChainNode<C, V>(scope, lane, key, getChain, resolveChain): ChainNode<C, V>

Parameters

scope

unknown

lane

string

key

string

getChain

() => C | undefined

resolveChain

(chain, argsOverride?) => V

Returns

ChainNode<C, V>

Overrides

LaneNode.constructor

_dependents

_dependents: Set<BaseNode>

Inherited from

LaneNode._dependents

_epoch

_epoch: number = 0

Bumped on every dirty (see setDirty/markDirty) — a stale async settle compares its captured epoch against the current one and discards itself on mismatch (a newer edit superseded it while it was in flight).

Inherited from

LaneNode._epoch

_hooks?

optional _hooks?: NodeHooks

Lifecycle observers attached to THIS node — see nodes/hooks.ts. Also carried by an DataDefinition, the declaration surface a node's hooks can be attached to before the node exists.

Inherited from

LaneNode._hooks

_reads

_reads: Set<BaseNode>

Reverse index of nodes this node currently reads (static expression refs + dynamic dependOn edges). Cleared at the start of every dirty recompute and on dispose so a dropped Table.get/sampleTexture edge stops cascading markDirty (and a disposed node is not retained).

Inherited from

LaneNode._reads

attributes

attributes: Record<string, unknown> = {}

Inherited from

LaneNode.attributes

collection

collection: BaseCollection

Inherited from

LaneNode.collection

error?

optional error?: string = ""

Last error message; empty string when valid, undefined after a successful eval.

Inherited from

LaneNode.error

id

id: string

Inherited from

LaneNode.id

isDirty

isDirty: boolean = true

True when the node needs re-evaluation before its value can be trusted.

Inherited from

LaneNode.isDirty

key

readonly key: string

parent?

optional parent?: any

Inherited from

LaneNode.parent

pending

pending: boolean = false

True while an async compute() is in flight (its result hasn't settled yet).

Inherited from

LaneNode.pending

sourceId

sourceId: string = ""

Inherited from

LaneNode.sourceId

value

value: unknown = undefined

Cached evaluated value; undefined when the node is in an error state.

Inherited from

LaneNode.value

Accessors

dependents

Get Signature

get dependents(): Set<BaseNode>

Returns

Set<BaseNode>

Inherited from

LaneNode.dependents

errorLogPrefix

Get Signature

get protected errorLogPrefix(): string

console.error prefix on a compute failure — override to match a subclass's existing log message (includes node-specific context).

Returns

string

Inherited from

LaneNode.errorLogPrefix

model

Get Signature

get model(): any

Returns

any

Inherited from

LaneNode.model

owner

Get Signature

get owner(): any

Walk up to the nearest owner that has a nodes collection (ModelScope).

Returns

any

Inherited from

LaneNode.owner

resolving

Get Signature

get resolving(): boolean

True while THIS instance is mid-track/pull — Model.ts's root-fold re-entrancy guard reads this on the shared SceneNode so a whole-scene consumer re-entering the fold mid-walk fails with a named cycle.

Returns

boolean

Inherited from

LaneNode.resolving

symbol

Get Signature

get symbol(): string

Returns

string

Inherited from

LaneNode.symbol

valueType

Get Signature

get valueType(): string

Returns

string

Inherited from

LaneNode.valueType

Methods

_commit()

protected _commit(raw, isAsync, settle?): void

Protected (not private): ExpressionNode/ContainerNode's evaluate() overrides call this directly to reuse the exact same commit path for a volatile step that bypasses the isDirty gate.

Parameters

raw

unknown

isAsync

boolean

settle?

EvaluateSettle

Returns

void

Inherited from

LaneNode._commit

_dispose()

_dispose(): void

Returns

void

Inherited from

LaneNode._dispose

_fail()

protected _fail(err, isAsync, settle?): void

Protected (not private) — see _commit's doc.

Parameters

err

unknown

isAsync

boolean

settle?

EvaluateSettle

Returns

void

Inherited from

LaneNode._fail

addListener()

addListener(event, listener): () => void

Parameters

event

string

listener

Listener

Returns

() => void

Inherited from

LaneNode.addListener

compute()

compute(): unknown

Compute the raw value for this node; override in subclasses to produce actual output.

Returns

unknown

Inherited from

LaneNode.compute

copy()

copy(): any

Duplicate this node in its collection, inserting it right after the original.

Returns

any

Inherited from

LaneNode.copy

display()

display(listener?): string

Return the human-readable display string for this node's value (or its error message).

Parameters

listener?

() => void

Returns

string

Inherited from

LaneNode.display

errorValue()

protected errorValue(): unknown

Value to fall back to when compute()/postCompute() throws. Override for a node whose "empty" isn't undefined (ContainerNode: []).

Returns

unknown

Inherited from

LaneNode.errorValue

evaluate()

evaluate(listener?): unknown

Evaluate this node, cache the result, and notify dependents; returns the cached value. When compute() returns a thenable, this returns the prior (stale) value synchronously and settles later — see core/maybeAsync.ts.

Parameters

listener?

() => void

Returns

unknown

Inherited from

LaneNode.evaluate

finishInit()

finishInit(): void

Assembly is complete: this node has its full structure and its children. Every assembly path calls it at the point wiring is genuinely done, which is NOT one line:

  • the classes that take everything as constructor parameters (the LaneNode family, ExpressionNode, OptionNode, ArgumentNode) call it at the end of their own constructor;
  • DataNode calls it after its args are pushed — fromRegistry and copy, never its constructor, which sets only method+definition;
  • ContainerNode after its children are attached, ParameterNode after its args are wired (NodeCollection: createFromJSON, addContainer, addEntry).

Guarded to fire at most once. That is what makes NESTED assembly safe — createFromJSON delegates to fromRegistry, and one node must see one init, not two — and makes a redundant call harmless rather than wrong.

Returns

void

Inherited from

LaneNode.finishInit

getError()

getError(listener?): string | undefined

Parameters

listener?

() => void

Returns

string | undefined

Inherited from

LaneNode.getError

hasChanged()

hasChanged(): boolean

See LaneNode.hasSourceChanged's doc — Model's blanket touch handler calls this (not resolveWith's hot path) to decide whether to bump _epoch, which resolveWith's memo-clear check then picks up on its next call.

Returns

boolean

hasSourceChanged()

protected hasSourceChanged(source): boolean

True when source differs (by content) from what was last recorded via snapshotSource. Model's blanket "some lane was touched" handler (_invalidateLaneNodes) calls this (through each subclass's own hasChanged()) to dirty PRECISELY instead of coarsely: diffing one record/chain's own (small) JSON is cheap, unlike the whole-lane JSON.stringify hash the old _texturesSignature paid on every resolve of every chain. A node that's never resolved (fresh, isDirty already true) doesn't need this at all — it recomputes on its own regardless.

Parameters

source

unknown

Returns

boolean

Inherited from

LaneNode.hasSourceChanged

isPending()

isPending(listener?): boolean

True while an async compute() is still settling (see pending).

Parameters

listener?

() => void

Returns

boolean

Inherited from

LaneNode.isPending

isValid()

isValid(listener?): boolean

Parameters

listener?

() => void

Returns

boolean

Inherited from

LaneNode.isValid

notify()

notify(event, ...args): void

Parameters

event

string

args

...unknown[]

Returns

void

Inherited from

LaneNode.notify

postCompute()

protected postCompute(raw): unknown

Post-process compute()'s raw result into this.value; throw to signal a failure (caught by evaluate()'s template, same as a compute() throw). Override for a node whose value isn't a sanitizer-typed scalar: ContainerNode returns the data list as-is; ArgumentNode routes through the domain coercion pipeline (nodeMethods/parameter.ts + data/ asset resolution) instead of convert/validate.

Parameters

raw

unknown

Returns

unknown

Inherited from

LaneNode.postCompute

pull()

protected pull(compute): V

Single cached-value convenience over track — the common case (one value per node, dirtied wholesale on setDirty/markDirty). A failed compute leaves isDirty untouched (still true, or whatever it was) so the NEXT resolve retries fresh — matching the pre-existing "throws every time until fixed" contract these lane nodes replace.

Parameters

compute

() => V

Returns

V

Inherited from

LaneNode.pull

remove()

remove(): void

The removal door. BaseObject.remove() detaches from the collection and notifies "remove"; the pair brackets that, so beforeRemove still sees the node in its collection with its read edges intact.

afterRemove means "AFTER THE NODE LEFT ITS COLLECTION" — it does NOT promise the node's read edges are gone. Deliberately does not retractReads(): removing a node does not retract its edges today, only the disposal call sites do (tables.ts), and adding it here would be a behaviour change rather than an observation point.

Not anchored at retractReads() — that runs on EVERY dirty recompute (3 of its 6 call sites: evaluate, LaneNode.pull, evaluateStep), so hooks there would report a live node as removed on every evaluate.

Returns

void

Inherited from

LaneNode.remove

removeListener()

removeListener(event, listener): void

Parameters

event

string

listener

Listener

Returns

void

Inherited from

LaneNode.removeListener

resolve()

resolve(): V

Returns

V

resolveWith()

resolveWith(argsOverride?): V

Parameters

argsOverride?

Record<string, unknown>

Returns

V

retractReads()

retractReads(): void

Drop every reverse read edge recorded in _reads.

Returns

void

Inherited from

LaneNode.retractReads

setError()

setError(message): void

Set an error message on this node and notify listeners. Fires "error" (specific) and "change" (the universal event — see ParameterNode.setKey's doc).

Parameters

message

string

Returns

void

Inherited from

LaneNode.setError

snapshotSource()

protected snapshotSource(source): void

Record a content snapshot of source (the record/chain JSON THIS node was built from — never the computed value, which can be large/ non-JSON, e.g. a texture's pixel buffer) — call once per successful resolve, right after reading the source.

Parameters

source

unknown

Returns

void

Inherited from

LaneNode.snapshotSource

toJSON()

toJSON(): BaseObjectJSON

Returns

BaseObjectJSON

Inherited from

LaneNode.toJSON

track()

protected track<R>(compute): R

Cycle + read-tracking wrapper around compute — always registers dependOn(this) first (harmless when nothing is currently evaluating) and throws instead of recursing when THIS instance is already mid-resolve on the current call stack (A reads B reads A, any number of hops) — the guard replacing the old per-lane bespoke ones (Model._resolvingTables and friends) for the graph-integrated path. Does NOT itself decide caching — a subclass with one cached value uses pull; one that caches per call-arguments (ChainNode's resolveWith) calls this directly and manages its own memo.

Type Parameters

R

R

Parameters

compute

() => R

Returns

R

Inherited from

LaneNode.track


abstract LaneNode

Base for the model's internal "lane" nodes — one instance per tables[]/patterns[]/components[]/views[]/sheets[] CHAIN, plus one shared node for the resolved 3D scene. These wrap the static/on-demand document lanes (schema/model.ts) in the SAME BaseNode dirty-gated pull contract the graph proper uses, so a hidden cross-lane read (Table.get, sampleTexture, resolvePlaced*, scene()) registers a REAL _dependents edge (via dependOn, see core/evaluationStack.ts) instead of the old isVolatile "never trust the cache" patch. (Attached files need no lane node of their own — a url-method parameter IS an ordinary graph node, so a reference to its key is an ordinary dependency edge. Materials and layers are both authored as material/layer NODES and resolved via SnapshotLaneNode → collectMaterialNodes / collectLayers.)

Never added to a NodeCollection, never model-key-addressable — Model.ts owns one Map per lane (keyed by the record/chain's own name/key, e.g. "rebarSchedule" for a table) as purely internal engine bookkeeping, not a user-facing model reference (never consulted by NodeCollection's keyedNodes()/generateKey()).

Naming (post 2026-07-21 convergence): a "node" is either a chain STEP (DataNode/ParameterNode) or a whole CHAIN viewed as one cached unit (ChainNode — dual-mode: plain resolve() OR per-args resolveWith()). There is no separate ArgsChainNode / RecordNode class anymore.

Extends

Extended by

Type Parameters

T

T

Constructors

Constructor

new LaneNode<T>(scope): LaneNode<T>

Parameters

scope

unknown

Returns

LaneNode<T>

Overrides

BaseNode.constructor

_dependents

_dependents: Set<BaseNode>

Inherited from

BaseNode._dependents

_epoch

_epoch: number = 0

Bumped on every dirty (see setDirty/markDirty) — a stale async settle compares its captured epoch against the current one and discards itself on mismatch (a newer edit superseded it while it was in flight).

Inherited from

BaseNode._epoch

_hooks?

optional _hooks?: NodeHooks

Lifecycle observers attached to THIS node — see nodes/hooks.ts. Also carried by an DataDefinition, the declaration surface a node's hooks can be attached to before the node exists.

Inherited from

BaseNode._hooks

_reads

_reads: Set<BaseNode>

Reverse index of nodes this node currently reads (static expression refs + dynamic dependOn edges). Cleared at the start of every dirty recompute and on dispose so a dropped Table.get/sampleTexture edge stops cascading markDirty (and a disposed node is not retained).

Inherited from

BaseNode._reads

attributes

attributes: Record<string, unknown> = {}

Inherited from

BaseNode.attributes

collection

collection: BaseCollection

Inherited from

BaseNode.collection

error?

optional error?: string = ""

Last error message; empty string when valid, undefined after a successful eval.

Inherited from

BaseNode.error

id

id: string

Inherited from

BaseNode.id

isDirty

isDirty: boolean = true

True when the node needs re-evaluation before its value can be trusted.

Inherited from

BaseNode.isDirty

parent?

optional parent?: any

Inherited from

BaseNode.parent

pending

pending: boolean = false

True while an async compute() is in flight (its result hasn't settled yet).

Inherited from

BaseNode.pending

sourceId

sourceId: string = ""

Inherited from

BaseNode.sourceId

value

value: unknown = undefined

Cached evaluated value; undefined when the node is in an error state.

Inherited from

BaseNode.value

Accessors

dependents

Get Signature

get dependents(): Set<BaseNode>

Returns

Set<BaseNode>

Inherited from

BaseNode.dependents

errorLogPrefix

Get Signature

get protected errorLogPrefix(): string

console.error prefix on a compute failure — override to match a subclass's existing log message (includes node-specific context).

Returns

string

Inherited from

BaseNode.errorLogPrefix

model

Get Signature

get model(): any

Returns

any

Inherited from

BaseNode.model

owner

Get Signature

get owner(): any

Walk up to the nearest owner that has a nodes collection (ModelScope).

Returns

any

Inherited from

BaseNode.owner

resolving

Get Signature

get resolving(): boolean

True while THIS instance is mid-track/pull — Model.ts's root-fold re-entrancy guard reads this on the shared SceneNode so a whole-scene consumer re-entering the fold mid-walk fails with a named cycle.

Returns

boolean

symbol

Get Signature

get symbol(): string

Returns

string

Inherited from

BaseNode.symbol

valueType

Get Signature

get valueType(): string

Returns

string

Inherited from

BaseNode.valueType

Methods

_commit()

protected _commit(raw, isAsync, settle?): void

Protected (not private): ExpressionNode/ContainerNode's evaluate() overrides call this directly to reuse the exact same commit path for a volatile step that bypasses the isDirty gate.

Parameters

raw

unknown

isAsync

boolean

settle?

EvaluateSettle

Returns

void

Inherited from

BaseNode._commit

_dispose()

_dispose(): void

Returns

void

Inherited from

BaseNode._dispose

_fail()

protected _fail(err, isAsync, settle?): void

Protected (not private) — see _commit's doc.

Parameters

err

unknown

isAsync

boolean

settle?

EvaluateSettle

Returns

void

Inherited from

BaseNode._fail

addListener()

addListener(event, listener): () => void

Parameters

event

string

listener

Listener

Returns

() => void

Inherited from

BaseNode.addListener

compute()

compute(): unknown

Compute the raw value for this node; override in subclasses to produce actual output.

Returns

unknown

Inherited from

BaseNode.compute

copy()

copy(): any

Duplicate this node in its collection, inserting it right after the original.

Returns

any

Inherited from

BaseNode.copy

display()

display(listener?): string

Return the human-readable display string for this node's value (or its error message).

Parameters

listener?

() => void

Returns

string

Inherited from

BaseNode.display

errorValue()

protected errorValue(): unknown

Value to fall back to when compute()/postCompute() throws. Override for a node whose "empty" isn't undefined (ContainerNode: []).

Returns

unknown

Inherited from

BaseNode.errorValue

evaluate()

evaluate(listener?): unknown

Evaluate this node, cache the result, and notify dependents; returns the cached value. When compute() returns a thenable, this returns the prior (stale) value synchronously and settles later — see core/maybeAsync.ts.

Parameters

listener?

() => void

Returns

unknown

Inherited from

BaseNode.evaluate

finishInit()

finishInit(): void

Assembly is complete: this node has its full structure and its children. Every assembly path calls it at the point wiring is genuinely done, which is NOT one line:

  • the classes that take everything as constructor parameters (the LaneNode family, ExpressionNode, OptionNode, ArgumentNode) call it at the end of their own constructor;
  • DataNode calls it after its args are pushed — fromRegistry and copy, never its constructor, which sets only method+definition;
  • ContainerNode after its children are attached, ParameterNode after its args are wired (NodeCollection: createFromJSON, addContainer, addEntry).

Guarded to fire at most once. That is what makes NESTED assembly safe — createFromJSON delegates to fromRegistry, and one node must see one init, not two — and makes a redundant call harmless rather than wrong.

Returns

void

Inherited from

BaseNode.finishInit

getError()

getError(listener?): string | undefined

Parameters

listener?

() => void

Returns

string | undefined

Inherited from

BaseNode.getError

hasSourceChanged()

protected hasSourceChanged(source): boolean

True when source differs (by content) from what was last recorded via snapshotSource. Model's blanket "some lane was touched" handler (_invalidateLaneNodes) calls this (through each subclass's own hasChanged()) to dirty PRECISELY instead of coarsely: diffing one record/chain's own (small) JSON is cheap, unlike the whole-lane JSON.stringify hash the old _texturesSignature paid on every resolve of every chain. A node that's never resolved (fresh, isDirty already true) doesn't need this at all — it recomputes on its own regardless.

Parameters

source

unknown

Returns

boolean

isPending()

isPending(listener?): boolean

True while an async compute() is still settling (see pending).

Parameters

listener?

() => void

Returns

boolean

Inherited from

BaseNode.isPending

isValid()

isValid(listener?): boolean

Parameters

listener?

() => void

Returns

boolean

Inherited from

BaseNode.isValid

notify()

notify(event, ...args): void

Parameters

event

string

args

...unknown[]

Returns

void

Inherited from

BaseNode.notify

postCompute()

protected postCompute(raw): unknown

Post-process compute()'s raw result into this.value; throw to signal a failure (caught by evaluate()'s template, same as a compute() throw). Override for a node whose value isn't a sanitizer-typed scalar: ContainerNode returns the data list as-is; ArgumentNode routes through the domain coercion pipeline (nodeMethods/parameter.ts + data/ asset resolution) instead of convert/validate.

Parameters

raw

unknown

Returns

unknown

Inherited from

BaseNode.postCompute

pull()

protected pull(compute): T

Single cached-value convenience over track — the common case (one value per node, dirtied wholesale on setDirty/markDirty). A failed compute leaves isDirty untouched (still true, or whatever it was) so the NEXT resolve retries fresh — matching the pre-existing "throws every time until fixed" contract these lane nodes replace.

Parameters

compute

() => T

Returns

T

remove()

remove(): void

The removal door. BaseObject.remove() detaches from the collection and notifies "remove"; the pair brackets that, so beforeRemove still sees the node in its collection with its read edges intact.

afterRemove means "AFTER THE NODE LEFT ITS COLLECTION" — it does NOT promise the node's read edges are gone. Deliberately does not retractReads(): removing a node does not retract its edges today, only the disposal call sites do (tables.ts), and adding it here would be a behaviour change rather than an observation point.

Not anchored at retractReads() — that runs on EVERY dirty recompute (3 of its 6 call sites: evaluate, LaneNode.pull, evaluateStep), so hooks there would report a live node as removed on every evaluate.

Returns

void

Inherited from

BaseNode.remove

removeListener()

removeListener(event, listener): void

Parameters

event

string

listener

Listener

Returns

void

Inherited from

BaseNode.removeListener

retractReads()

retractReads(): void

Drop every reverse read edge recorded in _reads.

Returns

void

Inherited from

BaseNode.retractReads

setError()

setError(message): void

Set an error message on this node and notify listeners. Fires "error" (specific) and "change" (the universal event — see ParameterNode.setKey's doc).

Parameters

message

string

Returns

void

Inherited from

BaseNode.setError

snapshotSource()

protected snapshotSource(source): void

Record a content snapshot of source (the record/chain JSON THIS node was built from — never the computed value, which can be large/ non-JSON, e.g. a texture's pixel buffer) — call once per successful resolve, right after reading the source.

Parameters

source

unknown

Returns

void

toJSON()

toJSON(): BaseObjectJSON

Returns

BaseObjectJSON

Inherited from

BaseNode.toJSON

track()

protected track<R>(compute): R

Cycle + read-tracking wrapper around compute — always registers dependOn(this) first (harmless when nothing is currently evaluating) and throws instead of recursing when THIS instance is already mid-resolve on the current call stack (A reads B reads A, any number of hops) — the guard replacing the old per-lane bespoke ones (Model._resolvingTables and friends) for the graph-integrated path. Does NOT itself decide caching — a subclass with one cached value uses pull; one that caches per call-arguments (ChainNode's resolveWith) calls this directly and manages its own memo.

Type Parameters

R

R

Parameters

compute

() => R

Returns

R


SceneNode

One shared node representing the resolved 3D scene (the root scene[] fold

  • render-frame bake — Model.ts's collectRenderFrame / sceneFrame()).

THE whole-scene pull choke point: every consumer that needs the aggregate (tableSource's default scan, nesting's part-count scan, view/sheet resolution, the expression scene() helper) goes through Model.sceneFrame(reader?) → this node. Pull-style root binding: while resolving, dependOn every root body node so an edit ANYWHERE in the root fold dirties this node (and every whole-scene consumer that depended on it). Structural root changes (add/remove/reorder) still dirty this node directly from Model.ts — a _dependents edge can't precede a brand-new node's own existence.

Cycle contract: a re-entrant resolve(reader) while already resolving throws naming reader (e.g. viewport reading view "plan"). Root-fold re-entry that bypasses this node (tessellateScene → rootFoldSlices mid- fold) is caught by Model.ts's fold-depth counter, which reads activeReader for the same message shape. Never touch()-invalidated (see Model._invalidateLaneNodes's doc) — a lane-JSON edit never changes the 3D scene on its own.

Extends

Type Parameters

F

F

Constructors

Constructor

new SceneNode<F>(scope, collect, bindRoots?): SceneNode<F>

Parameters

scope

unknown

collect

() => F

bindRoots?

() => void

Called inside withEvaluating(this) so dependOn(rootBody) wires each root body → this SceneNode.

Returns

SceneNode<F>

Overrides

LaneNode.constructor

_dependents

_dependents: Set<BaseNode>

Inherited from

LaneNode._dependents

_epoch

_epoch: number = 0

Bumped on every dirty (see setDirty/markDirty) — a stale async settle compares its captured epoch against the current one and discards itself on mismatch (a newer edit superseded it while it was in flight).

Inherited from

LaneNode._epoch

_hooks?

optional _hooks?: NodeHooks

Lifecycle observers attached to THIS node — see nodes/hooks.ts. Also carried by an DataDefinition, the declaration surface a node's hooks can be attached to before the node exists.

Inherited from

LaneNode._hooks

_reads

_reads: Set<BaseNode>

Reverse index of nodes this node currently reads (static expression refs + dynamic dependOn edges). Cleared at the start of every dirty recompute and on dispose so a dropped Table.get/sampleTexture edge stops cascading markDirty (and a disposed node is not retained).

Inherited from

LaneNode._reads

attributes

attributes: Record<string, unknown> = {}

Inherited from

LaneNode.attributes

collection

collection: BaseCollection

Inherited from

LaneNode.collection

cycleGate

cycleGate: string | undefined

Set ONLY by Model._withSceneReader for self-referential embeds (viewport / tableEntity). rootFoldSlices throws while this is set — ordinary tableSource / scene() / host tessellate leave it unset so mid-fold Table.get re-entry stays legal.

error?

optional error?: string = ""

Last error message; empty string when valid, undefined after a successful eval.

Inherited from

LaneNode.error

id

id: string

Inherited from

LaneNode.id

isDirty

isDirty: boolean = true

True when the node needs re-evaluation before its value can be trusted.

Inherited from

LaneNode.isDirty

parent?

optional parent?: any

Inherited from

LaneNode.parent

pending

pending: boolean = false

True while an async compute() is in flight (its result hasn't settled yet).

Inherited from

LaneNode.pending

sourceId

sourceId: string = ""

Inherited from

LaneNode.sourceId

value

value: unknown = undefined

Cached evaluated value; undefined when the node is in an error state.

Inherited from

LaneNode.value

Accessors

dependents

Get Signature

get dependents(): Set<BaseNode>

Returns

Set<BaseNode>

Inherited from

LaneNode.dependents

errorLogPrefix

Get Signature

get protected errorLogPrefix(): string

console.error prefix on a compute failure — override to match a subclass's existing log message (includes node-specific context).

Returns

string

Inherited from

LaneNode.errorLogPrefix

model

Get Signature

get model(): any

Returns

any

Inherited from

LaneNode.model

owner

Get Signature

get owner(): any

Walk up to the nearest owner that has a nodes collection (ModelScope).

Returns

any

Inherited from

LaneNode.owner

resolving

Get Signature

get resolving(): boolean

True while THIS instance is mid-track/pull — Model.ts's root-fold re-entrancy guard reads this on the shared SceneNode so a whole-scene consumer re-entering the fold mid-walk fails with a named cycle.

Returns

boolean

Inherited from

LaneNode.resolving

symbol

Get Signature

get symbol(): string

Returns

string

Inherited from

LaneNode.symbol

valueType

Get Signature

get valueType(): string

Returns

string

Inherited from

LaneNode.valueType

Methods

_commit()

protected _commit(raw, isAsync, settle?): void

Protected (not private): ExpressionNode/ContainerNode's evaluate() overrides call this directly to reuse the exact same commit path for a volatile step that bypasses the isDirty gate.

Parameters

raw

unknown

isAsync

boolean

settle?

EvaluateSettle

Returns

void

Inherited from

LaneNode._commit

_dispose()

_dispose(): void

Returns

void

Inherited from

LaneNode._dispose

_fail()

protected _fail(err, isAsync, settle?): void

Protected (not private) — see _commit's doc.

Parameters

err

unknown

isAsync

boolean

settle?

EvaluateSettle

Returns

void

Inherited from

LaneNode._fail

addListener()

addListener(event, listener): () => void

Parameters

event

string

listener

Listener

Returns

() => void

Inherited from

LaneNode.addListener

compute()

compute(): unknown

Compute the raw value for this node; override in subclasses to produce actual output.

Returns

unknown

Inherited from

LaneNode.compute

copy()

copy(): any

Duplicate this node in its collection, inserting it right after the original.

Returns

any

Inherited from

LaneNode.copy

display()

display(listener?): string

Return the human-readable display string for this node's value (or its error message).

Parameters

listener?

() => void

Returns

string

Inherited from

LaneNode.display

errorValue()

protected errorValue(): unknown

Value to fall back to when compute()/postCompute() throws. Override for a node whose "empty" isn't undefined (ContainerNode: []).

Returns

unknown

Inherited from

LaneNode.errorValue

evaluate()

evaluate(listener?): unknown

Evaluate this node, cache the result, and notify dependents; returns the cached value. When compute() returns a thenable, this returns the prior (stale) value synchronously and settles later — see core/maybeAsync.ts.

Parameters

listener?

() => void

Returns

unknown

Inherited from

LaneNode.evaluate

finishInit()

finishInit(): void

Assembly is complete: this node has its full structure and its children. Every assembly path calls it at the point wiring is genuinely done, which is NOT one line:

  • the classes that take everything as constructor parameters (the LaneNode family, ExpressionNode, OptionNode, ArgumentNode) call it at the end of their own constructor;
  • DataNode calls it after its args are pushed — fromRegistry and copy, never its constructor, which sets only method+definition;
  • ContainerNode after its children are attached, ParameterNode after its args are wired (NodeCollection: createFromJSON, addContainer, addEntry).

Guarded to fire at most once. That is what makes NESTED assembly safe — createFromJSON delegates to fromRegistry, and one node must see one init, not two — and makes a redundant call harmless rather than wrong.

Returns

void

Inherited from

LaneNode.finishInit

getError()

getError(listener?): string | undefined

Parameters

listener?

() => void

Returns

string | undefined

Inherited from

LaneNode.getError

hasSourceChanged()

protected hasSourceChanged(source): boolean

True when source differs (by content) from what was last recorded via snapshotSource. Model's blanket "some lane was touched" handler (_invalidateLaneNodes) calls this (through each subclass's own hasChanged()) to dirty PRECISELY instead of coarsely: diffing one record/chain's own (small) JSON is cheap, unlike the whole-lane JSON.stringify hash the old _texturesSignature paid on every resolve of every chain. A node that's never resolved (fresh, isDirty already true) doesn't need this at all — it recomputes on its own regardless.

Parameters

source

unknown

Returns

boolean

Inherited from

LaneNode.hasSourceChanged

isPending()

isPending(listener?): boolean

True while an async compute() is still settling (see pending).

Parameters

listener?

() => void

Returns

boolean

Inherited from

LaneNode.isPending

isValid()

isValid(listener?): boolean

Parameters

listener?

() => void

Returns

boolean

Inherited from

LaneNode.isValid

notify()

notify(event, ...args): void

Parameters

event

string

args

...unknown[]

Returns

void

Inherited from

LaneNode.notify

postCompute()

protected postCompute(raw): unknown

Post-process compute()'s raw result into this.value; throw to signal a failure (caught by evaluate()'s template, same as a compute() throw). Override for a node whose value isn't a sanitizer-typed scalar: ContainerNode returns the data list as-is; ArgumentNode routes through the domain coercion pipeline (nodeMethods/parameter.ts + data/ asset resolution) instead of convert/validate.

Parameters

raw

unknown

Returns

unknown

Inherited from

LaneNode.postCompute

pull()

protected pull(compute): F

Single cached-value convenience over track — the common case (one value per node, dirtied wholesale on setDirty/markDirty). A failed compute leaves isDirty untouched (still true, or whatever it was) so the NEXT resolve retries fresh — matching the pre-existing "throws every time until fixed" contract these lane nodes replace.

Parameters

compute

() => F

Returns

F

Inherited from

LaneNode.pull

remove()

remove(): void

The removal door. BaseObject.remove() detaches from the collection and notifies "remove"; the pair brackets that, so beforeRemove still sees the node in its collection with its read edges intact.

afterRemove means "AFTER THE NODE LEFT ITS COLLECTION" — it does NOT promise the node's read edges are gone. Deliberately does not retractReads(): removing a node does not retract its edges today, only the disposal call sites do (tables.ts), and adding it here would be a behaviour change rather than an observation point.

Not anchored at retractReads() — that runs on EVERY dirty recompute (3 of its 6 call sites: evaluate, LaneNode.pull, evaluateStep), so hooks there would report a live node as removed on every evaluate.

Returns

void

Inherited from

LaneNode.remove

removeListener()

removeListener(event, listener): void

Parameters

event

string

listener

Listener

Returns

void

Inherited from

LaneNode.removeListener

resolve()

resolve(_reader?): F

Pull the cached scene. Self-ref embeds stamp cycleGate (viewport/ tableEntity): a nested resolve while collecting then throws naming the gate. Ordinary mid-fold Table.get nested pulls run a nested collect without re-entering track (same dual-fold pattern the old collectGeometry→rootFoldSlices path allowed while SceneNode was idle).

Parameters

_reader?

string

Returns

F

retractReads()

retractReads(): void

Drop every reverse read edge recorded in _reads.

Returns

void

Inherited from

LaneNode.retractReads

setError()

setError(message): void

Set an error message on this node and notify listeners. Fires "error" (specific) and "change" (the universal event — see ParameterNode.setKey's doc).

Parameters

message

string

Returns

void

Inherited from

LaneNode.setError

snapshotSource()

protected snapshotSource(source): void

Record a content snapshot of source (the record/chain JSON THIS node was built from — never the computed value, which can be large/ non-JSON, e.g. a texture's pixel buffer) — call once per successful resolve, right after reading the source.

Parameters

source

unknown

Returns

void

Inherited from

LaneNode.snapshotSource

toJSON()

toJSON(): BaseObjectJSON

Returns

BaseObjectJSON

Inherited from

LaneNode.toJSON

track()

protected track<R>(compute): R

Cycle + read-tracking wrapper around compute — always registers dependOn(this) first (harmless when nothing is currently evaluating) and throws instead of recursing when THIS instance is already mid-resolve on the current call stack (A reads B reads A, any number of hops) — the guard replacing the old per-lane bespoke ones (Model._resolvingTables and friends) for the graph-integrated path. Does NOT itself decide caching — a subclass with one cached value uses pull; one that caches per call-arguments (ChainNode's resolveWith) calls this directly and manages its own memo.

Type Parameters

R

R

Parameters

compute

() => R

Returns

R

Inherited from

LaneNode.track


SnapshotLaneNode

One cached value for a whole lane (cameras[] / materials[] / layers[]) whose source is a JSON array on the ModelScope. hasChanged() diffs the lane JSON; resolve() re-runs produce under withEvaluating(this) so every param/pattern read inside wires a real edge back onto this node — a param or pattern edit dirties the lane without a signature WeakMap. Lane entries are { key, method, args } nodes; resolving them produces the records.

NOTE: the producer callback is named produce, never compute — BaseNode already has a public compute() method; a private field of that name breaks assignability to BaseNode (TS private-name clash).

Extends

Type Parameters

S

S

V

V

Constructors

Constructor

new SnapshotLaneNode<S, V>(scope, getSource, produce): SnapshotLaneNode<S, V>

Parameters

scope

unknown

getSource

() => S

produce

() => V

Returns

SnapshotLaneNode<S, V>

Overrides

LaneNode.constructor

_dependents

_dependents: Set<BaseNode>

Inherited from

LaneNode._dependents

_epoch

_epoch: number = 0

Bumped on every dirty (see setDirty/markDirty) — a stale async settle compares its captured epoch against the current one and discards itself on mismatch (a newer edit superseded it while it was in flight).

Inherited from

LaneNode._epoch

_hooks?

optional _hooks?: NodeHooks

Lifecycle observers attached to THIS node — see nodes/hooks.ts. Also carried by an DataDefinition, the declaration surface a node's hooks can be attached to before the node exists.

Inherited from

LaneNode._hooks

_reads

_reads: Set<BaseNode>

Reverse index of nodes this node currently reads (static expression refs + dynamic dependOn edges). Cleared at the start of every dirty recompute and on dispose so a dropped Table.get/sampleTexture edge stops cascading markDirty (and a disposed node is not retained).

Inherited from

LaneNode._reads

attributes

attributes: Record<string, unknown> = {}

Inherited from

LaneNode.attributes

collection

collection: BaseCollection

Inherited from

LaneNode.collection

error?

optional error?: string = ""

Last error message; empty string when valid, undefined after a successful eval.

Inherited from

LaneNode.error

id

id: string

Inherited from

LaneNode.id

isDirty

isDirty: boolean = true

True when the node needs re-evaluation before its value can be trusted.

Inherited from

LaneNode.isDirty

parent?

optional parent?: any

Inherited from

LaneNode.parent

pending

pending: boolean = false

True while an async compute() is in flight (its result hasn't settled yet).

Inherited from

LaneNode.pending

sourceId

sourceId: string = ""

Inherited from

LaneNode.sourceId

value

value: unknown = undefined

Cached evaluated value; undefined when the node is in an error state.

Inherited from

LaneNode.value

Accessors

dependents

Get Signature

get dependents(): Set<BaseNode>

Returns

Set<BaseNode>

Inherited from

LaneNode.dependents

errorLogPrefix

Get Signature

get protected errorLogPrefix(): string

console.error prefix on a compute failure — override to match a subclass's existing log message (includes node-specific context).

Returns

string

Inherited from

LaneNode.errorLogPrefix

model

Get Signature

get model(): any

Returns

any

Inherited from

LaneNode.model

owner

Get Signature

get owner(): any

Walk up to the nearest owner that has a nodes collection (ModelScope).

Returns

any

Inherited from

LaneNode.owner

resolving

Get Signature

get resolving(): boolean

True while THIS instance is mid-track/pull — Model.ts's root-fold re-entrancy guard reads this on the shared SceneNode so a whole-scene consumer re-entering the fold mid-walk fails with a named cycle.

Returns

boolean

Inherited from

LaneNode.resolving

symbol

Get Signature

get symbol(): string

Returns

string

Inherited from

LaneNode.symbol

valueType

Get Signature

get valueType(): string

Returns

string

Inherited from

LaneNode.valueType

Methods

_commit()

protected _commit(raw, isAsync, settle?): void

Protected (not private): ExpressionNode/ContainerNode's evaluate() overrides call this directly to reuse the exact same commit path for a volatile step that bypasses the isDirty gate.

Parameters

raw

unknown

isAsync

boolean

settle?

EvaluateSettle

Returns

void

Inherited from

LaneNode._commit

_dispose()

_dispose(): void

Returns

void

Inherited from

LaneNode._dispose

_fail()

protected _fail(err, isAsync, settle?): void

Protected (not private) — see _commit's doc.

Parameters

err

unknown

isAsync

boolean

settle?

EvaluateSettle

Returns

void

Inherited from

LaneNode._fail

addListener()

addListener(event, listener): () => void

Parameters

event

string

listener

Listener

Returns

() => void

Inherited from

LaneNode.addListener

compute()

compute(): unknown

Compute the raw value for this node; override in subclasses to produce actual output.

Returns

unknown

Inherited from

LaneNode.compute

copy()

copy(): any

Duplicate this node in its collection, inserting it right after the original.

Returns

any

Inherited from

LaneNode.copy

display()

display(listener?): string

Return the human-readable display string for this node's value (or its error message).

Parameters

listener?

() => void

Returns

string

Inherited from

LaneNode.display

errorValue()

protected errorValue(): unknown

Value to fall back to when compute()/postCompute() throws. Override for a node whose "empty" isn't undefined (ContainerNode: []).

Returns

unknown

Inherited from

LaneNode.errorValue

evaluate()

evaluate(listener?): unknown

Evaluate this node, cache the result, and notify dependents; returns the cached value. When compute() returns a thenable, this returns the prior (stale) value synchronously and settles later — see core/maybeAsync.ts.

Parameters

listener?

() => void

Returns

unknown

Inherited from

LaneNode.evaluate

finishInit()

finishInit(): void

Assembly is complete: this node has its full structure and its children. Every assembly path calls it at the point wiring is genuinely done, which is NOT one line:

  • the classes that take everything as constructor parameters (the LaneNode family, ExpressionNode, OptionNode, ArgumentNode) call it at the end of their own constructor;
  • DataNode calls it after its args are pushed — fromRegistry and copy, never its constructor, which sets only method+definition;
  • ContainerNode after its children are attached, ParameterNode after its args are wired (NodeCollection: createFromJSON, addContainer, addEntry).

Guarded to fire at most once. That is what makes NESTED assembly safe — createFromJSON delegates to fromRegistry, and one node must see one init, not two — and makes a redundant call harmless rather than wrong.

Returns

void

Inherited from

LaneNode.finishInit

getError()

getError(listener?): string | undefined

Parameters

listener?

() => void

Returns

string | undefined

Inherited from

LaneNode.getError

hasChanged()

hasChanged(): boolean

Returns

boolean

hasSourceChanged()

protected hasSourceChanged(source): boolean

True when source differs (by content) from what was last recorded via snapshotSource. Model's blanket "some lane was touched" handler (_invalidateLaneNodes) calls this (through each subclass's own hasChanged()) to dirty PRECISELY instead of coarsely: diffing one record/chain's own (small) JSON is cheap, unlike the whole-lane JSON.stringify hash the old _texturesSignature paid on every resolve of every chain. A node that's never resolved (fresh, isDirty already true) doesn't need this at all — it recomputes on its own regardless.

Parameters

source

unknown

Returns

boolean

Inherited from

LaneNode.hasSourceChanged

isPending()

isPending(listener?): boolean

True while an async compute() is still settling (see pending).

Parameters

listener?

() => void

Returns

boolean

Inherited from

LaneNode.isPending

isValid()

isValid(listener?): boolean

Parameters

listener?

() => void

Returns

boolean

Inherited from

LaneNode.isValid

notify()

notify(event, ...args): void

Parameters

event

string

args

...unknown[]

Returns

void

Inherited from

LaneNode.notify

postCompute()

protected postCompute(raw): unknown

Post-process compute()'s raw result into this.value; throw to signal a failure (caught by evaluate()'s template, same as a compute() throw). Override for a node whose value isn't a sanitizer-typed scalar: ContainerNode returns the data list as-is; ArgumentNode routes through the domain coercion pipeline (nodeMethods/parameter.ts + data/ asset resolution) instead of convert/validate.

Parameters

raw

unknown

Returns

unknown

Inherited from

LaneNode.postCompute

pull()

protected pull(compute): V

Single cached-value convenience over track — the common case (one value per node, dirtied wholesale on setDirty/markDirty). A failed compute leaves isDirty untouched (still true, or whatever it was) so the NEXT resolve retries fresh — matching the pre-existing "throws every time until fixed" contract these lane nodes replace.

Parameters

compute

() => V

Returns

V

Inherited from

LaneNode.pull

remove()

remove(): void

The removal door. BaseObject.remove() detaches from the collection and notifies "remove"; the pair brackets that, so beforeRemove still sees the node in its collection with its read edges intact.

afterRemove means "AFTER THE NODE LEFT ITS COLLECTION" — it does NOT promise the node's read edges are gone. Deliberately does not retractReads(): removing a node does not retract its edges today, only the disposal call sites do (tables.ts), and adding it here would be a behaviour change rather than an observation point.

Not anchored at retractReads() — that runs on EVERY dirty recompute (3 of its 6 call sites: evaluate, LaneNode.pull, evaluateStep), so hooks there would report a live node as removed on every evaluate.

Returns

void

Inherited from

LaneNode.remove

removeListener()

removeListener(event, listener): void

Parameters

event

string

listener

Listener

Returns

void

Inherited from

LaneNode.removeListener

resolve()

resolve(): V

Returns

V

retractReads()

retractReads(): void

Drop every reverse read edge recorded in _reads.

Returns

void

Inherited from

LaneNode.retractReads

setError()

setError(message): void

Set an error message on this node and notify listeners. Fires "error" (specific) and "change" (the universal event — see ParameterNode.setKey's doc).

Parameters

message

string

Returns

void

Inherited from

LaneNode.setError

snapshotSource()

protected snapshotSource(source): void

Record a content snapshot of source (the record/chain JSON THIS node was built from — never the computed value, which can be large/ non-JSON, e.g. a texture's pixel buffer) — call once per successful resolve, right after reading the source.

Parameters

source

unknown

Returns

void

Inherited from

LaneNode.snapshotSource

toJSON()

toJSON(): BaseObjectJSON

Returns

BaseObjectJSON

Inherited from

LaneNode.toJSON

track()

protected track<R>(compute): R

Cycle + read-tracking wrapper around compute — always registers dependOn(this) first (harmless when nothing is currently evaluating) and throws instead of recursing when THIS instance is already mid-resolve on the current call stack (A reads B reads A, any number of hops) — the guard replacing the old per-lane bespoke ones (Model._resolvingTables and friends) for the graph-integrated path. Does NOT itself decide caching — a subclass with one cached value uses pull; one that caches per call-arguments (ChainNode's resolveWith) calls this directly and manages its own memo.

Type Parameters

R

R

Parameters

compute

() => R

Returns

R

Inherited from

LaneNode.track

Last updated: 📖 44 min readEdit on GitHub