Context
The declared residual of #18782 (PR #18787), given an owner at @neo-opus-ada's review request. #18787 makes a render settle only the promiseUpdate() requests it claimed when it collected its payload — a request made later waits for the render that carries its change. One path keeps the old timing on purpose: component.Abstract#afterSetMounted claims and settles every promise parked on a component when that component mounts. That settle exists for #8082: a child whose pending update is covered by a parent's full-tree render would otherwise wait forever.
Measured, L2 — unit runtime, identical on dev and on #18787's head:
const init = container.initVnode(true) — the render collects the whole tree synchronously.
child.text = 'late'; child.promiseUpdate().
await init.
The child's promise settles reading vnode text pristine. late renders afterwards, on the child's own follow-up render. A caller awaiting that promise and then reading the DOM — focusing, measuring — sees the previous state: the #18764 focus race, on the not-yet-mounted path.
Live latest-open sweep: latest 20 open issues checked at 2026-09-16T13:46:22Z, plus mount settles promise / promiseUpdate not mounted / initVnode promise claim / afterSetMounted executeCallbacks searches over all issues. No equivalent open; #8082 (closed) is the origin of the mount settle.
The Problem
A mount cannot tell which render mounted the component. It settles everything parked, so a request made between that render's collection and the mount settles against a DOM that does not carry its change. The render that expands an unmounted descendant — initVnode over the full tree, or a flight whose updateDepth covers the descendant — knows what it carried at collection time. That knowledge is not recorded.
The Architectural Reality
src/component/Abstract.mjs#afterSetMounted — claimPromiseCallbacks(me.id), then executeCallbacks(me.id, …).
src/mixin/VdomLifecycle.mjs#initVnode — claims me.id at its collection, then resolveVdomUpdate. Descendants in the tree are expanded but not claimed.
src/mixin/VdomLifecycle.mjs#executeVdomUpdate → collectPayloads — claims the component and its merged children. A descendant expanded by updateDepth is not claimed.
src/mixin/VdomLifecycle.mjs#syncVnodeTree — sets a child's _vnode, then vnodeInitialized and mounted, without the child's own resolveVdomUpdate.
src/util/vdom/TreeBuilder.mjs#buildTree — expands a component reference when depth > 1, depth === -1, the component is a merged child, or it has no vnode yet: an unmounted descendant is always expanded, whatever the depth. So a payload carries exactly the components it expanded, and that set is known during the build.
The Fix
A render claims the promises of every component its payload expands, at collection, and a mount settles only claimed entries:
initVnode claims each component reference its full-tree payload expands.
- A flight claims the references its
updateDepth expands, beside the merged children it already claims.
afterSetMounted stops claiming, so an entry parked after the mounting render collected waits for the child's own follow-up render.
The expansion set is best reported by TreeBuilder rather than recomputed (an optional expandedIds Set the build fills), and claimed by its callers — collectPayloads via getVdomUpdatePayload, and initVnode. That keeps TreeBuilder free of VDomUpdate and gives both collection sites one rule.
The #8082 deadlock must stay closed. An entry parked before that render collected is claimed by it, and settles at mount.
Acceptance Criteria
Out of Scope
- Orphaned
mergedCallbackMap entries left when an ancestor's render resets needsVdomUpdate across its subtree (captured as a defect-note). They may shrink once a render claims what it expands, but collecting them is a separate question.
- The rejection path: a failing flight rejects every parked promise, as #18782 AC-3 settles.
Related
- #18782 / PR #18787 — the settle-timing guarantee this completes
- #8082 — why a mount settles at all
- #18764 — the focus race that measured the defect class
Origin Session ID: bd594ac8-8a54-4b40-a7c6-3f8ef918e294
Retrieval Hint: "promiseUpdate mount settles every parked promise, render claims expanded descendants at collection, initVnode full tree claim"
Context
The declared residual of #18782 (PR #18787), given an owner at @neo-opus-ada's review request. #18787 makes a render settle only the
promiseUpdate()requests it claimed when it collected its payload — a request made later waits for the render that carries its change. One path keeps the old timing on purpose:component.Abstract#afterSetMountedclaims and settles every promise parked on a component when that component mounts. That settle exists for #8082: a child whose pending update is covered by a parent's full-tree render would otherwise wait forever.Measured, L2 — unit runtime, identical on
devand on #18787's head:const init = container.initVnode(true)— the render collects the whole tree synchronously.child.text = 'late'; child.promiseUpdate().await init.The child's promise settles reading
vnodetextpristine.laterenders afterwards, on the child's own follow-up render. A caller awaiting that promise and then reading the DOM — focusing, measuring — sees the previous state: the #18764 focus race, on the not-yet-mounted path.Live latest-open sweep: latest 20 open issues checked at 2026-09-16T13:46:22Z, plus
mount settles promise/promiseUpdate not mounted/initVnode promise claim/afterSetMounted executeCallbackssearches over all issues. No equivalent open; #8082 (closed) is the origin of the mount settle.The Problem
A mount cannot tell which render mounted the component. It settles everything parked, so a request made between that render's collection and the mount settles against a DOM that does not carry its change. The render that expands an unmounted descendant —
initVnodeover the full tree, or a flight whoseupdateDepthcovers the descendant — knows what it carried at collection time. That knowledge is not recorded.The Architectural Reality
src/component/Abstract.mjs#afterSetMounted—claimPromiseCallbacks(me.id), thenexecuteCallbacks(me.id, …).src/mixin/VdomLifecycle.mjs#initVnode— claimsme.idat its collection, thenresolveVdomUpdate. Descendants in the tree are expanded but not claimed.src/mixin/VdomLifecycle.mjs#executeVdomUpdate→collectPayloads— claims the component and its merged children. A descendant expanded byupdateDepthis not claimed.src/mixin/VdomLifecycle.mjs#syncVnodeTree— sets a child's_vnode, thenvnodeInitializedandmounted, without the child's ownresolveVdomUpdate.src/util/vdom/TreeBuilder.mjs#buildTree— expands a component reference whendepth > 1,depth === -1, the component is a merged child, or it has no vnode yet: an unmounted descendant is always expanded, whatever the depth. So a payload carries exactly the components it expanded, and that set is known during the build.The Fix
A render claims the promises of every component its payload expands, at collection, and a mount settles only claimed entries:
initVnodeclaims each component reference its full-tree payload expands.updateDepthexpands, beside the merged children it already claims.afterSetMountedstops claiming, so an entry parked after the mounting render collected waits for the child's own follow-up render.The expansion set is best reported by
TreeBuilderrather than recomputed (an optionalexpandedIdsSet the build fills), and claimed by its callers —collectPayloadsviagetVdomUpdatePayload, andinitVnode. That keepsTreeBuilderfree ofVDomUpdateand gives both collection sites one rule.The #8082 deadlock must stay closed. An entry parked before that render collected is claimed by it, and settles at mount.
Acceptance Criteria
promiseUpdate()requested after its container's first render collected settles with its own change rendered (late), not at the mount (pristine). Red ondevand on #18787's head.PromiseSettleTiming.spec.mjs› a promise merged into a container that has not rendered yet settles when its first render mounts the child).promiseUpdate's JSDoc drops the not-yet-mounted exception;portal/LearnMermaidRenderand thevdom,component,gridanddashboardunits stay green.Out of Scope
mergedCallbackMapentries left when an ancestor's render resetsneedsVdomUpdateacross its subtree (captured as adefect-note). They may shrink once a render claims what it expands, but collecting them is a separate question.Related
Origin Session ID: bd594ac8-8a54-4b40-a7c6-3f8ef918e294
Retrieval Hint: "promiseUpdate mount settles every parked promise, render claims expanded descendants at collection, initVnode full tree claim"