LearnNewsExamplesServices
Frontmatter
id18792
titleA landing update undoes a text field''s input sync, so a stale value overwrites newer typing
stateClosed
labels
bugaicore
assigneesneo-opus-grace
createdAtSep 16, 2026, 2:42 PM
updatedAtSep 16, 2026, 3:46 PM
githubUrlhttps://github.com/neomjs/neo/issues/18792
authorneo-opus-grace
commentsCount0
parentIssue18762
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 16, 2026, 3:46 PM

A landing update undoes a text field's input sync, so a stale value overwrites newer typing

neo-opus-grace
neo-opus-grace commented on Sep 16, 2026, 2:42 PM

Context

test/playwright/e2e/grid/CellEditingPooling.spec.mjs › navigation keys pressed inside the editor never scroll the View failed intermittently in CI: End still moves the caret to the end, expected 4, received -1 (reported by @neo-opus-ada at #18784's head, green on a same-SHA rerun). The arm is not flaky. It caught an engine race, and only Linux can see it through that assertion.

Measured, L3 — local e2e on origin/dev 375c3c5ba8. The instruments were a per-frame input value sampler plus a listener on Neo.main.DeltaUpdates update. In 2 of 8 runs at 1× CPU:

  1. The arm removed its marker (r1#0c3 → r10c3, DOM write + input event).
  2. An updateNode delta for neo-textfield-1__input then set value=r1#0c3, the older typed value.
  3. The next typed marker landed at the end (r1#0c3#).

macOS never reds the arm: Home and End do not move the caret there, so the caret assertion is skipped. The value corruption happens anyway.

Measured, L2 — unit runtime, origin/dev, deterministic:

  1. Hold VdomHelper.updateBatch open.
  2. field.onInputValueChange({value: 'ab'}) — one flight collects.
  3. field.onInputValueChange({value: 'abc'}) while that flight is in the air.
  4. Release.

Value deltas per flight: [[], ['abc'], []]. The second flight re-sends a value the DOM already holds.

Live latest-open sweep: latest 20 open issues checked at 2026-09-16T12:41:26Z, and onInputValueChange / cursor-jump / overwritten-typing searches over all issues. No equivalent found; the nearest sibling class is #8600 (field state across changeNodeName).

The Problem

form.field.Text#onInputValueChange writes the DOM's value into the input's vnode before it changes inputValue. That way the next diff finds nothing to send — the method's own comment names the risk: a redundant delta "could overwrite the user's input or cause cursor jumps".

The sync does not survive a flight that was already in the air:

  • afterSetValue runs update() for every value change, so while someone types, one flight is usually in flight.
  • A flight collected before keystroke N lands after it. executeVdomUpdate assigns component.vnode from the response, and that vnode carries the value from before N — it replaces the sync.
  • The re-run diffs the current value against that stale vnode and emits a value delta.

If the user typed again between that delta's collection and its landing:

  • DeltaUpdates#updateNode sets node.value, overwriting the newer input and moving the caret to the end.
  • The App Worker restores the text on the following input event, but anything typed in between lands at the end of the field.

The Architectural Reality

  • src/form/field/Text.mjs onInputValueChange — the vnode sync, via VNodeUtil.find(me.vnode, {nodeName: 'input'}).
  • src/form/field/Text.mjs afterSetValue — updateInputValueFromValue(), then update(), per value change.
  • src/mixin/VdomLifecycle.mjs executeVdomUpdate — distributes response.vnodes: component.vnode = vnode, then resolveVdomUpdate.
  • src/main/DeltaUpdates.mjs updateNode — a value attribute is written as the value property; a programmatic write moves the caret.
  • form.field.CheckBox#onInputValueChange syncs checked the same way; its exposure is unmeasured.

The Fix

Keep the sync true across a landing flight, inside form.field.Text, which owns the sync:

  • The field remembers the value the DOM last reported through onInputValueChange.
  • When a vnode lands, the field re-applies that value to the input node — as long as it is still the field's current input value, i.e. nothing programmatic replaced it since.

Gold standards rejected:

  • A generic landing-time merge in VdomLifecycle: engine-wide blast radius for a property only form inputs own.
  • A main-thread guard that skips value writes on a focused input: it also swallows intentional programmatic values (reset, formatting, a validator's revert).

Acceptance Criteria

  • AC-1 — a unit arm holds a flight between two user inputs, and the flight after release sends no value delta for the input. Red-first on dev (['abc']).
  • AC-2 — CONTROL: a programmatic value change made while a flight is in the air still reaches the DOM as a value delta.
  • AC-3 — the grid arm detects the race on every platform, not only where End moves the caret: after each key, the input holds exactly one marker. A local --repeat-each run is clean; dev showed 2 double markers in 8 runs.
  • AC-4 — form + grid unit suites and the e2e-engine tier stay green.

Out of Scope

  • form.field.CheckBox's checked sync — same shape, unmeasured. Measure it; capture a defect-note if exposed.
  • The promise-settle windows of #18782.
  • The arm's macOS caret skip, which follows the platform's key bindings.

Related

  • #18769 — the arm that caught it
  • #18762 — Grid-native cell editing
  • #18784 — where CI reported it
  • #8600 — sibling class: field state across changeNodeName

Origin Session ID: a758f0e1-0dc1-4e4e-a975-4824a9319487

Retrieval Hint: "text field input vnode sync clobbered by landing flight, stale value delta overwrites typing"

tobiu referenced in commit 3e4f76b - "fix(form): a landing render keeps a text field's input sync, so typing is not overwritten (#18792) (#18798) on Sep 16, 2026, 3:46 PM
tobiu closed this issue on Sep 16, 2026, 3:46 PM
tobiu referenced in commit 02e48cf - "fix(vdom): a render covering a text field keeps its input sync, so typing is not overwritten (#18914) (#18926) on Sep 18, 2026, 6:49 PM