You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In the Marko chat-pretext example (and the e2e test built on it), calling scrollToEnd() while a history prepend is in flight leaves the view stranded exactly one prepend above the bottom, with no scroll event to recover it. The e2e/chat-pretext.spec.ts › Latest returns to the bottom and status flips back to At latest test fails deterministically on CI because of this (see #1260, two consecutive runs), while passing locally where the timing differs.
Root cause
The <virtualizer> tag's onUpdate runs setOptions(...), then _willUpdate(), then notify():
current.setOptions({ ...current.options, ...buildOptions(input, notify) })current._willUpdate()notify()// ← sets the reactive `size`; the sizer DOM grows in a later batch
On an end-anchored prepend, setOptions bumps the tracked scrollOffset by the prepended height and _willUpdate writes that offset to scrollTop. At that moment the sizer still has the old height, so the browser clamps the write to the old maximum. The sizer grows afterwards, scrollTop never moves, and the DOM is left prependedHeight short of the end. Core's tracked offset already holds the new value, so isAtEnd() reports true and the example's pinPending re-issue (which exists for exactly this strand, see the comment in examples/marko/chat-pretext/src/routes/+page.marko) disarms without correcting anything.
This is the same ordering problem react-virtual fixed in #1237 by growing the container before _willUpdate syncs the scroll position.
Reproduction (deterministic)
Drop this into packages/marko-virtual/e2e/app/e2e/ and run CI=1 npx playwright test latest-race. Fails 4/4 on main (and on #1260) with distance 870 (12 rows × ~72px):
import{expect,test}from'@playwright/test'importtype{Page}from'@playwright/test'constdist=(page: Page)=>page.locator('.messages').evaluate((el)=>el.scrollHeight-el.scrollTop-el.clientHeight)// Same as "Latest returns to the bottom" but clicks Latest right after arming the// auto history load, so the 180ms prepend lands AFTER the jump — the ordering CI hits.for(leti=0;i<4;i++){test(`race ${i}: Latest then late prepend keeps the bottom`,async({ page })=>{awaitpage.goto('/chat-pretext')awaitexpect.poll(()=>dist(page),{timeout: 5000}).toBeLessThanOrEqual(80)awaitpage.waitForTimeout(400)// autoHistoryEnabled arms after 250msawaitpage.locator('.messages').evaluate((el)=>{el.scrollTop=0})awaitpage.waitForTimeout(30)awaitpage.locator('[data-testid="latest"]').click()awaitpage.waitForTimeout(1500)expect(awaitdist(page)).toBeLessThanOrEqual(80)})}
Expected behavior
scrollToEnd() followed by a late prepend keeps the viewport pinned to the bottom (or, at minimum, the prepend keeps the visible content anchored, which for a bottom-pinned view means staying at the bottom).
Core: extend the clamp-detect-and-retry introduced in fix(virtual-core): Fix #1258 #1260 for compensation writes to the anchor-sync write in _willUpdate, so every adapter recovers regardless of DOM update ordering.
Until one of those lands, the affected e2e test is marked test.fixme referencing this issue so unrelated PRs can go green.
Platform
Chromium (Playwright), Linux CI runner; reproducible on macOS with the forced-timing spec above.
tanstack-virtual version
@tanstack/virtual-core@3.17.8, @tanstack/marko-virtual current main.
Describe the bug
In the Marko
chat-pretextexample (and the e2e test built on it), callingscrollToEnd()while a history prepend is in flight leaves the view stranded exactly one prepend above the bottom, with no scroll event to recover it. Thee2e/chat-pretext.spec.ts › Latest returns to the bottom and status flips back to At latesttest fails deterministically on CI because of this (see #1260, two consecutive runs), while passing locally where the timing differs.Root cause
The
<virtualizer>tag'sonUpdaterunssetOptions(...), then_willUpdate(), thennotify():On an end-anchored prepend,
setOptionsbumps the trackedscrollOffsetby the prepended height and_willUpdatewrites that offset toscrollTop. At that moment the sizer still has the old height, so the browser clamps the write to the old maximum. The sizer grows afterwards,scrollTopnever moves, and the DOM is leftprependedHeightshort of the end. Core's tracked offset already holds the new value, soisAtEnd()reports true and the example'spinPendingre-issue (which exists for exactly this strand, see the comment inexamples/marko/chat-pretext/src/routes/+page.marko) disarms without correcting anything.This is the same ordering problem react-virtual fixed in #1237 by growing the container before
_willUpdatesyncs the scroll position.Reproduction (deterministic)
Drop this into
packages/marko-virtual/e2e/app/e2e/and runCI=1 npx playwright test latest-race. Fails 4/4 onmain(and on #1260) withdistance 870(12 rows × ~72px):Expected behavior
scrollToEnd()followed by a late prepend keeps the viewport pinned to the bottom (or, at minimum, the prepend keeps the visible content anchored, which for a bottom-pinned view means staying at the bottom).Possible fixes
_willUpdate(), mirroring react-virtual'sapplyContainerSizeordering from fix(react-virtual): grow size container before scroll sync on end-anchored prepend #1237._willUpdate, so every adapter recovers regardless of DOM update ordering.Until one of those lands, the affected e2e test is marked
test.fixmereferencing this issue so unrelated PRs can go green.Platform
Chromium (Playwright), Linux CI runner; reproducible on macOS with the forced-timing spec above.
tanstack-virtual version
@tanstack/virtual-core@3.17.8,@tanstack/marko-virtualcurrentmain.