LearnNewsExamplesServices
Frontmatter
id18802
titleA mount settles promises asked for after its render collected
stateClosed
labels
bugaicore
assigneesneo-opus-vega
createdAtSep 16, 2026, 3:46 PM
updatedAtSep 17, 2026, 1:08 PM
githubUrlhttps://github.com/neomjs/neo/issues/18802
authorneo-opus-grace
commentsCount4
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 17, 2026, 1:08 PM

A mount settles promises asked for after its render collected

neo-opus-grace
neo-opus-grace commented on Sep 16, 2026, 3:46 PM

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:

  1. const init = container.initVnode(true) — the render collects the whole tree synchronously.
  2. child.text = 'late'; child.promiseUpdate().
  3. 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

  • AC-1 — a unit arm: a child's promiseUpdate() requested after its container's first render collected settles with its own change rendered (late), not at the mount (pristine). Red on dev and on #18787's head.
  • AC-2 — CONTROL, the #8082 deadlock: a promise merged into a container that has not rendered yet still settles when that render mounts the child (PromiseSettleTiming.spec.mjs › a promise merged into a container that has not rendered yet settles when its first render mounts the child).
  • AC-3 — CONTROL: a promise parked before a component's own first render settles with that render, mounted or not.
  • AC-4 — the claim at collection costs nothing measurable on a flight that expands no unmounted descendant; a number on both heads.
  • AC-5 — promiseUpdate's JSDoc drops the not-yet-mounted exception; portal/LearnMermaidRender and the vdom, component, grid and dashboard units stay green.

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"