enum SceneNodeIdentity
Swift
enum SceneNodeIdentity
Hands each view node the compact id the scene graph addresses it by.
The renderer keeps state — a scroll offset, a tint's fade, an animation's
target — against a uint32 the producer chooses, and it survives the
producer republishing its draw list. That only works if the number means
the same thing next frame, so what the producer needs is an id that is
stable while a node lives, unique while it lives, and safe to reuse
after it dies. This is that.
The first two come free: NodeID is already the identity reconciliation
preserves, so a keyed ForEach row keeps its NodeID across an insert
above it and a plain child keeps its across a body recompute. All this adds
is a dense uint32 alongside — NodeID is a process-wide counter that only
climbs, and truncating it to 32 bits would eventually wrap onto a live node.
The third is the whole difficulty, and it is why this is not a dictionary.
Reuse is the hazard
An id released and immediately reissued would hand the new node whatever the renderer still remembers about the old one: its scroll position, a half-finished fade, a target it was still travelling toward. The symptom is a fresh list that opens already scrolled, and it would be intermittent — which is the kind of bug that costs a week.
So an id is not reissued until the renderer has certainly forgotten it.
RenderWindow::sweepSceneState drops a node's state after
kRetainReplays replays without drawing it; the numbers here are that
constant plus margin, and the two are a pair. Changing one without the
other reintroduces the hazard, which is why the relationship is written
down here rather than left to be noticed.
Emit passes, not seconds and not loop iterations
Ages are counted in emit passes, because that is the only clock on which "unseen" means anything here: a node is refreshed by being asked about, and it is only asked about while a draw list is being built.
Wall-clock would be wrong because the renderer counts replays, not seconds, and the two sides must agree on how long "a while" is. Loop iterations would be wrong for a sharper reason, and were: the loop wakes for input, animation, agent traffic and D-Bus pumps, and most of those wakes emit nothing at all. Ageing on them makes a window that is on screen and simply unchanged look identical to one whose nodes have all been unmounted — so every live id expires, is re-minted on the next emit, and the renderer's retained state (a scroll offset, a fade, an animation target) is orphaned against an id nothing refers to any more. The visible symptom is a panel that silently loses its scroll position, and a list that then cannot be scrolled back because the producer and the renderer no longer agree on where it is.
That is why the counter is not advanced by its caller alone:
advanceFrame() is inert unless noteEmitPass() has recorded that
something was actually drawn. ImageStore.endFrame withholds its own clock
on idle iterations for the same reason — an unused-looking cache entry and
an unseen-looking node are the same mistake about the same silence.
Sources/LavaUI/SceneNodeIdentity.swift:59
Properties
static var census: (assigned: Int, quarantined: Int, free: Int) { get }
Live mappings, quarantined ids, and ids ready for reuse. For tests and for anyone wondering whether this grows without bound.
Sources/LavaUI/SceneNodeIdentity.swift:185
Methods
static func advanceFrame()
Advances the frame counter and ages the tables, if anything has been drawn since the last call. Call once per loop iteration, before anything asks for an id.
Safe to call on every iteration precisely because it is inert without an emit — see the note on emit passes above. Callers do not have to know which wakes drew and which did not.
Sources/LavaUI/SceneNodeIdentity.swift:157
static func id(for node: NodeID) -> UInt32
The scene id for node, minting one if it has none.
Also marks the node as drawn this frame, which is what keeps its id. A node stops being asked about when it stops being emitted — whether it was unmounted or merely scrolled out of a virtualized list — and that is the only signal needed. Nothing has to notice an unmount.
Sources/LavaUI/SceneNodeIdentity.swift:109
static func node(for sceneID: UInt32) -> NodeID?
Resolves renderer read-back to the view-node identity used by LavaUI.
Sources/LavaUI/SceneNodeIdentity.swift:132
static func resetForTesting()
Forgets everything. Tests only — a process that did this while a renderer held state would reissue every id it had just given out.
Sources/LavaUI/SceneNodeIdentity.swift:193