Skip to content

Stop stale model echoes from overwriting newer text in text inputs - #110

Draft
simonhamp wants to merge 1 commit into
mainfrom
fix/text-input-stale-echo
Draft

simonhamp wants to merge 1 commit into
mainfrom
fix/text-input-stale-echo

Conversation

@simonhamp

Copy link
Copy Markdown
Member

Fixes #95.

Typing fast into a text input bound with native:model loses letters. "Buy milk" gets saved as "Buyilk" or "Byu". It happens on iOS and Android, and debounce makes it rarer without fixing it.

Each keystroke sends a change event and PHP re-renders once per event. The tree from each render carries the value from that event. When keys arrive faster than the round trip, several renders are in flight at once. The renderers only remembered the last value they sent, so the echo of an earlier keystroke didn't match. It was treated as a real server change and replaced everything typed since. On iOS it gets worse, because the replaced text fires the text change handler and is sent back to PHP. Old letters then keep bouncing for a while and bleed into the next thing you type.

The fix keeps a small queue of values each field has sent and not yet seen come back (NativeUITextEchoTracker in Swift, TextEchoTracker in Kotlin). PHP handles events one at a time and in order, so when a published value matches something in the queue it's an echo. We drop it and everything before it and leave the field alone. That also works when the renderer skips some trees. A value that isn't in the queue is something PHP chose to set, like clearing the draft after Add, and it's applied the same way as before. The queue is capped at 64 so a field whose model is never published back can't grow it forever.

Changed in the shared iOS core (so outlined, filled and bare) and in all three Android renderers.

This overlaps with #96. That PR drops every server value while the field is focused, apart from the keep-focus-on-submit clear on iOS. I think that would stop "Add" from clearing a focused field in the usual todo pattern, since tapping a button doesn't take focus away from the field. It also only covers the outlined variant on Android. I haven't run #96 to confirm the Add case. This PR works without looking at focus, so it doesn't have that trade-off.

Testing

I tested in a copy of super-native on the iPhone 17 simulator, on a screen with native:model="draft", submit to add, and a 30ms render. AXe and simdeck taps and key events don't reach the app on this Xcode 27 setup. So a test-only hook in the local copy (not in this PR) appended characters to the field's text every 25ms, then submitted. That runs the same onChange(of: text) and commit path a keystroke does.

  • mobile-ui main as it is: 0 of 8 words came through. "Buy milk" became "By" or "Bmi", and later words were full of letters from earlier ones.
  • With this change: 8 of 8 correct, in both the model and the field, and the field cleared after every submit. I also ran it again with a 100ms render and got 8 of 8.

Swift typechecks (swiftc -typecheck -wmo against mobile-air main, with the same baseline errors as before and a canary to prove the file was checked). Kotlin compiles (:app:compileDebugKotlin in super-native's Android project with these files swapped in). I didn't run it on an Android device or emulator.

Things to look at

  • The one case this can't tell apart: you type a value, delete it and retype it within a single round trip, and the renderer skips the tree for the middle step. Then a later push from PHP of that exact earlier value could be read as an echo. That's much narrower than the current bug, but it's there. A real fix would need a sequence number on the wire from mobile-air.
  • If PHP rewrites the value on every change (say uppercasing it) while keystrokes are in flight, the rewrite still wins over newer local text. That's the same as today.
  • iOS still sends the value back to PHP after a server push, as before. The tracker skips recording that one, since its echo would never show up.

🤖 Generated with Claude Code

With native:model every keystroke sends a change event and PHP re-renders
once per event. When typing outpaces the round trip, several renders are in
flight. The field only remembered the newest value it sent, so the echo of an
older keystroke looked like a real server change and replaced whatever had
been typed since. On iOS that replaced text was then sent back to PHP, so old
letters kept bouncing around.

Each field now keeps a short queue of values it has sent and not yet seen come
back. PHP handles events in order, so an incoming value that matches the queue
is an echo: drop it and everything older. A value that isn't in the queue is
something PHP set on purpose, like clearing the field after Add, and is applied
as before. Covers the shared iOS core and all three Android variants.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Text input: a late echo of an earlier keystroke overwrites newer text (characters vanish while typing)

1 participant