What Happens When Data Changes
This diagram follows a single data mutation from the call that triggers it all the way to the next committed render.
Where changes come from. Every write funnels into dataController.set (or .delete): direct API calls, asset actions via the binding parser, the set/delete expression functions, and data written by an ACTION state’s afterTransition.
Model update. The transaction is normalized — bindings are parsed and values are deformatted through the SchemaController’s formatters — then diffed against current state so no-op writes drop out. Surviving changes pass through the middleware chain, where ValidationMiddleware runs "change"-phase validators that may block or annotate the write before LocalModel commits it. The model returns every affected binding: both the ones written and any computed bindings that depend on them.
Batched render. The ViewController taps the model’s onUpdate/onDelete hooks and calls queueUpdate. Silent updates merge into the pending set without rendering; non-silent updates are coalesced into a single queueMicrotask, so a burst of synchronous writes produces just one render. When the microtask drains, the accumulated changed bindings and nodes are handed to currentView.update.
Incremental resolve. The Resolver walks the AST from the root and, at each node, asks the DependencyModel whether any binding that node tracks is in the changeset. Unaffected subtrees reuse their cached resolved value; affected ones re-run applicability/switch/template logic and the StringResolver, which re-reads the model and re-evaluates @[expr]@. The reassembled tree is emitted to the renderer, which commits the diff — and the next user input starts the loop again.
Alongside the main path, validation error/warning state updates in the "change" phase, computed-binding expressions re-evaluate, and the ValidationController may still gate the next transition via skipTransition.
%% Player: data change pipeline
%% Shows what happens from a dataController.set/delete call through the model
%% middleware, validation, ViewController microtask batching, and incremental
%% Resolver re-computation that produces the next rendered view.
flowchart TD
Sources(["change sources"]) --> S1["dataController.set(transaction)"]
Sources --> S2["dataController.delete(binding)"]
Sources --> S3["asset action via<br/>BindingParser.set"]
Sources --> S4["expression evaluator<br/>(set / delete fns)"]
Sources --> S5["transition afterTransition:<br/>ACTION result writes data"]
S1 --> Normalize
S2 --> DeletePath
S3 --> S1
S4 --> S1
S5 --> S1
Normalize["Normalize transaction:<br/>parse bindings, deformat<br/>via hooks.deformat<br/>(SchemaController formatters)"]
Normalize --> Diff["compare old vs new<br/>dequal -> drop no-ops"]
Diff --> ModelSet["model.set(normalizedTx)"]
ModelSet --> MW["Middleware chain<br/>(hooks.resolveDataStages)"]
MW --> VMW["ValidationMiddleware:<br/>run validators<br/>updateValidationsForBinding(b, 'change')<br/>may block / annotate"]
VMW --> LocalModel["LocalModel writes value"]
LocalModel --> Dependents["return affected bindings<br/>(direct + computed dependents)"]
Dependents --> OnSet["hooks.onSet"]
OnSet --> HasChange{"any actual change?"}
HasChange -- no --> Idle(["no-op"])
HasChange -- yes --> OnUpdate["hooks.onUpdate(updates, options)"]
DeletePath["validate parent binding<br/>model.delete<br/>hooks.onDelete"] --> DepDel["compute dependents"]
DepDel --> OnUpdate
OnUpdate --> VCTap["ViewController taps onUpdate/onDelete<br/>extract changed bindings"]
VCTap --> Queue["queueUpdate(bindings, nodes, silent)"]
Queue --> Silent{"silent?"}
Silent -- yes --> NoRender(["skip render<br/>(merge into pending)"])
Silent -- no --> Scheduled{"microtask<br/>already scheduled?"}
Scheduled -- yes --> MergePending["merge bindings<br/>into pending set"]
Scheduled -- no --> Schedule["queueMicrotask(...)"]
MergePending --> Wait
Schedule --> Wait["wait microtask"]
Wait --> Drain["drain pending<br/>changedBindings + changedNodes"]
Drain --> VIUp["currentView.update(<br/>changedBindings, changedNodes)"]
VIUp --> RUp["Resolver.update(dataChanges, nodeChanges)"]
RUp --> BeforeU["beforeUpdate hook"]
BeforeU --> Walk["walk AST from root"]
Walk --> Skip{"skipResolve?<br/>DependencyModel:<br/>node tracks bindings;<br/>any in changeset?"}
Skip -- no deps changed --> Cached["reuse cached resolved node"]
Skip -- deps changed --> Recompute["beforeResolve hook<br/>(applicability / switch / template)<br/>-> resolve hook<br/>(StringResolver re-reads model.get,<br/>re-runs @[expr]@)<br/>-> recurse children<br/>-> afterResolve hook"]
Cached --> Combine
Recompute --> Combine["assembled resolved subtree"]
Combine --> AfterU["afterUpdate hook"]
AfterU --> Emit["ViewInstance.hooks.onUpdate<br/>(new tree)"]
Emit --> Render(["renderer commits diff"])
Render -.next user input.-> Sources
subgraph Side["side effects fired alongside"]
direction LR
SE1["validation 'change' phase<br/>updates error/warning state"]
SE2["expressions in computed<br/>bindings re-evaluate"]
SE3["ValidationController may<br/>still gate next transition<br/>via skipTransition"]
end
VMW -.-> SE1
LocalModel -.-> SE2
SE1 -.-> SE3