fix: typing in a text field survives a slow connection #95
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/input-value-echo"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Reported from use: on a slow connection every text field becomes unusable — typing quickly
drops letters and the caret jumps to the end. Noticed on the setup wizard's step-3 composer,
but it was never that component: it was every input in Kagaku.UI that binds on
oninput, sothe bible editors, the metadata card, the region editor and the page summaries all had it.
The mechanism and its fix live in Kagaku.UI #8
(merged as
faf01da): a field used to send each keystroke to the server, which re-rendered and —seeing
valuediffer from the previous render — wrote it back. Over a fast link that echo landsbetween keystrokes and nobody sees it; over a slow one it arrives after the user has typed further
and resets the box to a value several characters behind.
LiveValuekeeps the typed text out ofthe render tree so there is nothing to write back.
What's in
c14bcfc→faf01da.input.jsjoins the loaded scripts inApp.razor. It carries the one write the render diffcannot express: a value equal to the string already rendered while the element has drifted away
from it. The common paths stay pure Blazor.
@key="sel.Id"). Selecting another region isa different subject, not new values for this one. Without the key the same field instances are
handed the next region's text, and a field whose new text happens to match what the previous one
was rendered with has to be resynced through interop; with it the element is recreated carrying
the right value from birth. This is the same reasoning the bible rows already follow.
Tests
803/803 green (unchanged count). Five adapter tests read typed text out of
GetAttribute("value"),which the fix deliberately empties — keystrokes are no longer in the render tree, so that is not a
seam any more. They now assert the mechanism that actually decides the outcome: emptying a box the
user typed into can only happen through a push, so a draft that must survive is one with no
push against it, and a box that must clear is one with exactly one push of
"".AdapterTestContext.ValuePushesis the shared seam.Worth calling out:
Reprocess_sends_the_page_back_and_clears_the_boxwas still green and hadstopped testing anything — it asserted an empty value attribute, which is now always empty. It
would have gone on passing over a box that never cleared.
One test lost an assertion rather than gaining one:
An_unsaved_draft_survives_the_reload_an_unrelated_operation_causeskeeps only its storeassertion. Adding a chapter legitimately clears the chapter-title box, and a push carries no clue
which element it went to, so a clear of the summary and a clear of the title are indistinguishable
from outside. The draft landing in the store still pins the merge-not-rebuild behaviour the test
exists for, but the element half of it is no longer covered here — the equivalent is covered in
Kagaku.UI's own suite against a single field.
Browser-verified
A/B under real latency, via a TCP proxy holding every byte 350 ms each way. CDP's network
emulation is not usable for this — it does not throttle frames on an already-open WebSocket,
and the pre-fix build passes cleanly through it, which is a false pass worth knowing about for
future circuit work.
46 characters typed at 45 ms intervals into a page summary:
c14bcfc(before)The ete.faf01da(after)The hero meets the stranger at the shrine gate.At the same latency, on the merged build with the key: clicking through regions swaps every field
correctly, including non-empty → empty (
English:...We meet again.→""), and typing intoNotesthen switching to a region whoseNotesis also empty clears it with zero interoppushes — the key removed the case that previously needed one.
Notes
exercises every field this touches.
ComboboxandLabeledEntriesTablein Kagaku.UI have the same defect and are not fixedyet — Jibril caught that my #8 body wrongly claimed the four shared inputs were all the
oninputbinders. Neither is used by Orihon today, so nothing here is affected; the follow-upbelongs in Kagaku.UI.
🤖 Generated with Claude Code
Summary
Summary
Coverage
Orihon.BlazorAdapter - 95.7%
Orihon.Domain - 100%
Orihon.Infrastructure - 96.3%
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlankLines_4
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__BlockBreaks_1
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__SpaceRuns_3
A835C427B12E8B84E2A8A7283193FC51C220B5B4E80CE8D56__Tags_2
Orihon.Kernel - 90.9%
Orihon.Server - 93.3%
Orihon.UseCases - 97.3%
🔮 fufu~ Jibril reviewed your code!
Oh? Oh~! A slow-connection echo fix that reaches all the way across the submodule boundary, into every
oninputfield in the app, and lands here as a three-line consumer change plus a honest test re-seam. This is the kind of fix I love to see — the diagnosis is surgical, the surface area is minimal, and nothing is smuggled in. ♪Verdict: ✅ Looks good to me~
The submodule bump
c14bcfc → faf01dabrings in exactly theLiveValuefix I already approved over in Kagaku.UI #8 (+535/-38, 14 files — byte-identical diff, I checked). The three Orihon-side changes are all correct:✅ What I liked~
The
@key="sel.Id"is textbook. fufu~ I went and checked every sibling:BiblePage.razorkeys all four row types onrow.Id/row.PageId(lines 65, 90, 113, 166),ProjectListPagekeys onproject.Id,PageOrganizerkeys onchapter.Chapter.Id. This follows the established pattern exactly. Selecting another region is a different subject — a fresh element carries the right value from birth, and the interop-resync gap closes by construction. The comment explaining why is the cherry on top. ♡The
ValuePushesseam is elegant.AdapterTestContext.ValuePushesprojectingJSInterop.Invocations["kagakuInput.setValue"]intoIReadOnlyList<string?>is exactly the right abstraction — it exposes the mechanism that actually decides the outcome (emptying a box can only happen through a push) rather than the symptom (GetAttribute("value")) that the fix deliberately empties. The doc comment explaining the LiveValue world-shift is thorough and honest.Every test adjustment is a genuine pivot, not a weakening. I traced all five:
Reprocess_sends_the_page_back_and_clears_the_box→Assert.Equal("", Assert.Single(ValuePushes))— pins exactly-one-empty-push. The old assertion (GetAttribute("value")empty) was a tautology under LiveValue (always empty now) and the PR body honestly calls this out — it would have stayed green over a box that never cleared. The new assertion actually proves the clear happened. ♪A_refused_reprocess_keeps_the_words→Assert.Empty(ValuePushes)— no push means nothing wrote over the element, which under LiveValue is the survival guarantee.The_translation_view_sends_the_page_back_through_translation+A_refused_translation_send_back_keeps_the_words— same correct pivot for the translation path.A_user_edited_draft_survives_the_agents_pass→Assert.DoesNotContain("The agent's version.", ValuePushes)— the agent's text was never pushed to overwrite the user's draft. This catches the exact regression (if LiveValue broke and the agent's version DID clobber the field, this string would appear in the pushes).The honesty about the lost assertion in
An_unsaved_draft_survives_the_reload_an_unrelated_operation_causesis exactly right: adding a chapter legitimately clears the chapter-title box, andValuePushescarries no clue which element a push went to, so the two clears are indistinguishable from outside. The store assertion still pins the merge-not-rebuild behaviour the test exists for. The equivalent coverage lives in Kagaku.UI's own suite against a single field — correct division of responsibility.input.jsload position (App.razor:27, second aftertheme.js) is fine — the script only defineswindow.kagakuInput, no auto-init, called via interop fromLiveValue.csonly when a push is needed.blazor.web.jsstill loads last. Graceful degradation if the script is missing.Local verification~
5bb67a0(PR just opened, 0 comments pre-review) — local checks used.No blockers. No non-blocking nits worth flagging — the
@keydoesn't have a dedicated region-switching test, but@keyis a declarative Blazor primitive (not custom logic), the browser A/B under 350ms real-latency is the right verification, and the PR body's note about CDP network emulation being a false-pass is a genuinely useful insight for future circuit work.Clean merge. fufu~ ♡
Automated review by Jibril · 2026-07-28
CI/CD: absent for head
5bb67a0· Local checks: 803/803 pass, 0 warnings