LearnNewsExamplesServices
Frontmatter
id12946
titleVdom update wedge: in-flight flag sticks; vnode fakes applied deltas
stateClosed
labels
bugaicoregrid
assigneesneo-fable
createdAtJun 12, 2026, 4:52 AM
updatedAtJun 12, 2026, 6:13 AM
githubUrlhttps://github.com/neomjs/neo/issues/12946
authorneo-fable
commentsCount5
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 12, 2026, 6:13 AM

Vdom update wedge: in-flight flag sticks; vnode fakes applied deltas

neo-fable
neo-fable commented on Jun 12, 2026, 4:52 AM

Context

The operator re-tested lock-less devindex on dev tip c3d372054 (post the five #12938 fixes): "overscroll drag does still not move body cells." Headless reproduction attempts on the identical commit, geometry (1066px clip), DPR 2, with active streaming and human-style jitter were green at every layer — DOM values, worker state, and paint screenshots. The defect was then convicted live inside the operator's running session via Neural Link forensics: it is not the scroll pipeline at all, but one component whose vdom update round-trip is permanently dead.

The Problem

neo-grid-horizontal-scrollbar-1 in the live session had isVdomUpdating: true stuck for the session's entire lifetime (~25 min; a round-trip takes milliseconds). The update that wedged was the spacer's first sizing (afterSetCenterWidthwidth: 3526px), racing the initial 50k-row streaming storm. Consequences, all live-verified:

  1. The DOM spacer stayed unsized → the scrollbar element has zero scrollable overflow → physically pinned at scrollLeft 0 forever.
  2. The worker vnode nevertheless records width: '3526px' and scrollLeft: 2479 — claiming the wedged delta was applied. Every worker-side read (vnode, vdom, instance configs) lies in unison; only paint and DomAccess-level element behavior tell the truth.
  3. Self-heal is impossible: re-setting centerWidth to its believed value diffs against the lying vnode → empty diff → no delta ever re-sent. The split-brain is self-sustaining.
  4. Every Neo.main.DomAccess.scrollTo against the element is an eventless no-op (nothing to scroll) → no echo ever returns → grid.header.Toolbar#pendingScrollTarget stays armed, grid.ScrollManager never ingests, the GridHorizontalScrollSync addon never fires, body --grid-scroll-left never moves with the gesture.
  5. Operator-visible symptom: overdrag walks update worker state only (term walks, switches fire); headers snap into place at drag end via processDragEnd's direct toolbar scrollTo; body cells never follow during the gesture.
  6. The wedge re-arms on demand: clearing the flag and forcing update() (fresh centerWidth 3600 → real diff) wedges again — isVdomUpdating returns to true, vnode unchanged, no deltas land. The component's update round-trip is structurally dead while sibling components (grid.Body, grid.header.Toolbar, grid.Container) round-trip cleanly in the same session.

Evidence chain (Neural Link, live App Worker session 034414ea-0c88-4c8d-a440-500fa22c56c8)

Probe Result
get_drag_trace (3 operator gestures) ZERO scrollSync in retained windows; term parked at max-per-clipWidth
get_instance_properties ScrollManager / Body scrollLeft 2479 both; mountedColumns [23,36] — worker walked, believed it applied
scrollbar vnode spacer width: '3526px', scrollLeft: 2479claims applied
4× live scrollToIndex commands (targets 0/615/915/1100) pendingScrollTarget arms; no echo ever; ScrollManager + header button rect frozen
scrollbar isVdomUpdating true (stuck); after forced clear + fresh-width update()true again
get_computed_styles scrollbar overflow-x: auto, height: 17px — styling current (stale-CSS falsified)
get_method_source Toolbar#scrollToIndex identical to dev tip (stale-code falsified)
sibling components isVdomUpdating: false — wedge is component-specific

The Architectural Reality

  • src/grid/HorizontalScrollbar.mjs:61-66afterSetCenterWidth mutates vdom.cn[0].style.width + update(); the component's first update fires when grid.Body#afterSetAvailableWidth feeds centerWidth post-data-load — i.e. deterministically mid-storm on streaming apps.
  • The in-flight machinery: src/mixin/VdomLifecycle.mjs (isVdomUpdating, needsVdomUpdate, merge-into-parent) + src/manager/VDomUpdate.mjs (in-flight registry, merged-callback mapping). The suspected race: a child update merged into (or colliding with) a parent update whose completion callback never maps back to the child — flag stuck, response lost.
  • The vnode-lie surface: whatever wrote width/scrollLeft into the child's vnode without the delta being applied (parent-driven syncVnodeTree write-back is the prime suspect — the exact open question flagged on #12939 for vdom-core review, now empirically caught).
  • Downstream consumers that die with it: src/grid/header/Toolbar.mjs:409-455 (command/echo gate), src/grid/ScrollManager.mjs:153-196, src/main/addon/GridHorizontalScrollSync.mjs:40-49.

The Fix

  1. Root-cause the response loss: instrument/reproduce the child-update-during-parent-storm race (devindex init is a deterministic candidate: update fired from afterSetCenterWidth while the container/body stream-update is in flight); fix the merge/registry path so every in-flight update resolves or rejects — never silently vanishes.
  2. Watchdog (defense regardless of root): an in-flight vdom update exceeding a generous threshold (e.g. 2s) surfaces a loud dev-mode error naming the component — a permanently wedged component must scream, not rot. Recovery hook: clear the flag and force a full re-render that diffs against DOM-truthful state, not the lying vnode.
  3. Vnode write-back honesty: the write-back path must never record child deltas as applied when they were not (ties into #12939's open falsifier (b); if the same root, fix lands once and closes both).

Acceptance Criteria

  • Reproducer (unit or whitebox-e2e) that wedges a child update during a parent in-flight storm — or, if the exact race differs, the convicted mechanism documented with file:line.
  • Root cause fixed: the lost-response path resolves/rejects deterministically; the reproducer goes green.
  • Dev-mode watchdog: a stuck in-flight update surfaces loudly (component id + elapsed); no silent permanent wedge remains possible.
  • Vnode write-back cannot mark unapplied child deltas as applied (or documented why impossible post-fix).
  • L4 (post-merge): operator's devindex flow — fresh load, drag during initial stream, narrow window — overdrag moves body cells live.

Out of Scope

  • #12939's per-drop body-copy duplication (sibling defect, possibly same root — cross-linked; if one fix closes both, close both).
  • #12930's regression net (Clio's lane), #12942's grammar-guard substrate (a rung-2 stateful guard would have caught this at birth — evidence for that Discussion, not scope here).

Related

#12939 (stale-vnode-baseline family; falsifier (b) now empirically caught), #12883, #12938 (the five fixes — this wedge explains the operator's post-merge "still broken" report), #12930, #12942.

Release classification: ON the board — this beheads grid horizontal scrolling in real long-lived sessions; it is the operator's v13 gate symptom.

Live latest-open sweep: checked latest 20 open issues at 2026-06-12T02:50Z; no equivalent found. A2A in-flight claim sweep: no competing claim (Clio holds #12941/#12930; Ada holds #12936→PR #12945).

Origin Session ID: e605ce21-3668-445c-bc00-45896aa9a092

Retrieval Hint: "scrollbar isVdomUpdating wedge vnode lie eventless scrollTo" / live App Worker session 034414ea-0c88-4c8d-a440-500fa22c56c8.

tobiu referenced in commit 34d610a - "fix(vdom): reject lost update replies + release in-flight state on failure (#12946) (#12956) on Jun 12, 2026, 6:13 AM
tobiu closed this issue on Jun 12, 2026, 6:13 AM