fix(ifr): replay background history and reset list state on hydration fallback - #358
Merged
Conversation
… fallback A structural hydration mismatch tore down the whole IFR tree but applied only the mismatching background batch. Batches consumed earlier — skipped as identical, or value-patched — had only ever been painted as the main-thread render, so everything they described vanished from the page. Buffer the consumed background batches and replay them onto the clean page before the mismatching one. Teardown also left `list-apply`'s registries populated. A native <list> does not own its rows: they live in those registries and reach native only through the closures __CreateList captured. After a fallback the ids in the abandoned stream get reused by a structurally different background render, so a stale `listElementIds` entry routed a background INSERT into the dead list instead of the element tree (the child silently never appears), and `update-list-info` was committed onto whatever element inherited the id. Clearing the state makes the abandoned list's callbacks inert and lets the replay rebuild each list. Adds IFR × <list> coverage, which the suite had none of: a hydrated first screen whose rows native materializes after the background thread has mutated the list, plus both fallback paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuaHSwJh1PvJHUcwrH95pi
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Huxpro
marked this pull request as ready for review
August 6, 2026 17:06
Contributor
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Answers the
<list>half of #357 and fixes what the check turned up.What the self-check found
Steady-state hydration is fine, by construction. Unlike ReactLynx, vue-lynx never has to carry list callbacks across the thread boundary:
__CreateListis called fromops-apply.tson the main thread, and both the IFR render and every post-hydration background batch run through that same executor and the samelist-apply.tsmodule state. There is only ever one set of closures and onelistItemsarray per list, keyed by the element id both threads agree on. When an identical structural frame is skipped, nothing is re-created and nothing is orphaned; a later background insert mutates the very arraycomponentAtIndexreads.flushListUpdates()runs at the end of everyapplyOps, including the deferred-flush IFR batches, solastFlushedbaselines to a state the background thread agrees with by definition (the frame was byte-identical). A test now pins this end to end.The fallback path was broken, in two ways:
teardownIfrTree()removed everything the recorded stream created, but only the mismatching background batch was applied afterwards. Batches consumed earlier (skipped as identical, or value-patched) had only ever been painted as the main-thread render, so every element they described disappeared. Not list-specific — any multi-batch first screen loses its earlier batches.list-apply's registries. A native<list>does not own its rows: they live in those registries and reach native only through the closures__CreateListcaptured. After a fallback the element ids are reused by a structurally different background render, so a stalelistElementIdsentry routed a backgroundINSERTinto the dead list instead of the element tree — the child silently never appears — andupdate-list-infowas committed onto whatever element inherited the id.Changes
ifr.tsbuffers the consumed background batches and replays the complete history onto the clean page before applying the mismatching batch.teardownIfrTree()callsresetListState(), which also makes the abandoned list's callbacks inert (they resolve nothing) instead of appending live rows into a detached list.packages/testing-library/src/__tests__/ifr-list.test.ts— the suite had no IFR ×<list>coverage at all:vaporalready has both fixes — it buffersbackgroundHistoryand its teardown goes throughresetMainThreadState(). This bringsmainin line.Testing
pnpm --filter vue-lynx-testing-library test— 234 passed, including the 3 new tests.mainwithout theifr.tschange.pnpm --filter vue-lynx buildclean; biome clean.Not in this PR
__FlushElementTree()in main-thread handlers on Web (Self-check: <list> ownership across IFR hydration, and __FlushElementTree() in main-thread handlers on Web #357 check 2): no vue-lynx code path calls an&mut selfPAPI during main-thread event dispatch — every flush site is driven byrenderPageorvuePatchUpdate— and the bundled worklet-runtime already defersElement.setStyleProperty/setAttribute's flush to a coalesced microtask, so the documentedMainThreadRefpattern is safe. Details in the issue comment.enqueueComponentis still a no-op (list: framework-side cell recycling (enqueueComponent / recycle pool) #302); that is unchanged by IFR and not IFR-specific.Closes the
<list>/IFR question in #357.Generated by Claude Code