LearnNewsExamplesServices
Frontmatter
id19687
titleA cross-window insert leaves a stale mountedPromise, and updateVdom loops
stateClosed
labels
bugaicore
assigneesneo-opus-grace
createdAtOct 13, 2026, 12:16 AM
updatedAtOct 13, 2026, 1:01 AM
githubUrlhttps://github.com/neomjs/neo/issues/19687
authorneo-opus-grace
commentsCount0
parentIssue19655
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtOct 13, 2026, 1:01 AM

A cross-window insert leaves a stale mountedPromise, and updateVdom loops

neo-opus-grace
neo-opus-grace commented on Oct 13, 2026, 12:16 AM

Context

LegacyPopupReturnNL.spec.mjs:12 is red in every ten-shard dev run of the Brain-gated burn-in, and on both attempts of the retry (run 38313618683). It fails with "Request timed out". Hosted probes, from an unmerged branch:

  • Run 38313590362: after the drag's popup connects, the shared App worker answers neither the Neural Link nor its own main thread (Neo.worker.App.getWorkerId(), 3 s). The same call answers before the drag.
  • Run 38314276213: Chrome's debugger paused the App worker 12 times. Every stack runs updateVdom ← afterSetVdom ← update ← the mountedPromise.then() callback in src/mixin/VdomLifecycle.mjs (line 1337). That is a microtask loop, so no message is ever handled again.
  • Run 38315013009: this in the paused frame is legacy-return-widget, the dragged widget, in all six samples. It reads mounted: false, vnodeInitialized: true, has a vnode, hasBeenMounted: true, holds a _mountedPromise, and has no mountedPromiseResolve, so that promise is already resolved. Its parentId is the popup's viewport.

The Problem

updateVdom() on an unmounted component waits on mountedPromise.then(() => me.update()). Given a promise that an earlier mount resolved, the callback runs at once and finds the component still unmounted. It chains another then, and the loop never ends. The worker starves, and every later request to it times out.

The Architectural Reality

  • src/container/Base.mjs insert(), on a NeoInstance from a parent in another window, with removeFromPreviousParent: false: item._mounted = false. This raw write skips afterSetMounted() on purpose, so the copy's later mount still triggers it. It also skips the unmount branch, whose first act is delete me._mountedPromise.
  • src/dashboard/Container.mjs onWindowConnect() takes that branch during a window drag: mainView.add(detachedItem.widget, false, !me.#isWindowDragging).
  • src/component/Abstract.mjs get mountedPromise() returns any cached promise, whatever mounted reads now.

Design authority: the getter's JSDoc — "It also handles unmounting by resetting the promise, so it can be safely awaited again if the component is remounted". A raw unmount gets no reset.

The Fix

The getter owns the cache, so it checks it. A cached promise with no pending resolver is resolved. While the component is unmounted, the getter discards it and creates a pending one. This covers every path that clears mounted without the hook. The container's raw write stays, because skipping the unmount hooks for a copy is intended.

Acceptance Criteria

  • A unit arm runs the real insert into another window that keeps the item in its previous parent. The item's mountedPromise must stay pending until the next mount, and settle on it.
  • A unit arm pins the loop: one update() on an unmounted component holding a promise from an earlier mount runs update() once. Red before the fix (with the count capped), green after.
  • Post-merge: LegacyPopupReturnNL:12 passes in the next ten-shard dev run.

Out of Scope

  • Euclid's defect note on a staged value surviving a throwing beforeSet. It is a separate mechanism in Neo.createConfig.
  • The other popup intermittents in #19657, for example #19684.

Related

Live latest-open sweep: the latest open issues at 2026-10-12T22:15Z, plus gh search issues for "mountedPromise". No equivalent found, and no competing claim in A2A.

Origin Session ID: 02040656-932a-48c8-8894-6f43d0368663

tobiu referenced in commit c37f76c - "fix(component): an unmounted component never receives a settled mountedPromise, so updateVdom cannot loop on it (#19687) (#19689) on Oct 13, 2026, 1:01 AM
tobiu closed this issue on Oct 13, 2026, 1:01 AM