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:
- The arm removed its marker (
r1#0c3 → r10c3, DOM write + input event).
- An
updateNode delta for neo-textfield-1__input then set value=r1#0c3, the older typed value.
- 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:
- Hold
VdomHelper.updateBatch open.
field.onInputValueChange({value: 'ab'}) — one flight collects.
field.onInputValueChange({value: 'abc'}) while that flight is in the air.
- 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
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"
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/dev375c3c5ba8. The instruments were a per-frame input value sampler plus a listener onNeo.main.DeltaUpdatesupdate. In 2 of 8 runs at 1× CPU:r1#0c3→r10c3, DOM write +inputevent).updateNodedelta forneo-textfield-1__inputthen setvalue=r1#0c3, the older typed value.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:VdomHelper.updateBatchopen.field.onInputValueChange({value: 'ab'})— one flight collects.field.onInputValueChange({value: 'abc'})while that flight is in the air.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 acrosschangeNodeName).The Problem
form.field.Text#onInputValueChangewrites the DOM's value into the input's vnode before it changesinputValue. 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:
afterSetValuerunsupdate()for every value change, so while someone types, one flight is usually in flight.executeVdomUpdateassignscomponent.vnodefrom the response, and that vnode carries the value from before N — it replaces the sync.valuedelta.If the user typed again between that delta's collection and its landing:
DeltaUpdates#updateNodesetsnode.value, overwriting the newer input and moving the caret to the end.inputevent, but anything typed in between lands at the end of the field.The Architectural Reality
src/form/field/Text.mjsonInputValueChange— the vnode sync, viaVNodeUtil.find(me.vnode, {nodeName: 'input'}).src/form/field/Text.mjsafterSetValue—updateInputValueFromValue(), thenupdate(), per value change.src/mixin/VdomLifecycle.mjsexecuteVdomUpdate— distributesresponse.vnodes:component.vnode = vnode, thenresolveVdomUpdate.src/main/DeltaUpdates.mjsupdateNode— avalueattribute is written as thevalueproperty; a programmatic write moves the caret.form.field.CheckBox#onInputValueChangesyncscheckedthe same way; its exposure is unmeasured.The Fix
Keep the sync true across a landing flight, inside
form.field.Text, which owns the sync:onInputValueChange.Gold standards rejected:
VdomLifecycle: engine-wide blast radius for a property only form inputs own.valuewrites on a focused input: it also swallows intentional programmatic values (reset, formatting, a validator's revert).Acceptance Criteria
valuedelta for the input. Red-first ondev(['abc']).valuedelta.--repeat-eachrun is clean;devshowed 2 double markers in 8 runs.form+gridunit suites and thee2e-enginetier stay green.Out of Scope
form.field.CheckBox'scheckedsync — same shape, unmeasured. Measure it; capture adefect-noteif exposed.Related
changeNodeNameOrigin Session ID: a758f0e1-0dc1-4e4e-a975-4824a9319487
Retrieval Hint: "text field input vnode sync clobbered by landing flight, stale value delta overwrites typing"