marko-virtual: scrollToEnd during an in-flight prepend strands the view one prepend above the bottom (chat-pretext e2e fails on CI)
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 55/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- playwright, typescript
調査の方向性
virtualizer タグの onUpdate フローと packages/marko-virtual の Marko アダプターから始め、次に examples/marko/chat-pretext/src/routes/+page.marko と失敗している e2e/chat-pretext.spec.ts テストを調べます。#1237 における react-virtual の applyContainerSize 変更と順序を比較し、latest-race の決定論的な再現を実行します。scrollToEnd 後の遅延 prepend でもビューが下端に留まり、e2e テストが fixme なしでパスすれば完了です。
索引モデルが issue の本文から書いたものです。
説明
Describe the bug
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'
import type { Page } from '@playwright/test'
const dist = (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 (let i = 0; i < 4; i++) {
test(`race ${i}: Latest then late prepend keeps the bottom`, async ({ page }) => {
await page.goto('/chat-pretext')
await expect.poll(() => dist(page), { timeout: 5000 }).toBeLessThanOrEqual(80)
await page.waitForTimeout(400) // autoHistoryEnabled arms after 250ms
await page.locator('.messages').evaluate((el) => { el.scrollTop = 0 })
await page.waitForTimeout(30)
await page.locator('[data-testid="latest"]').click()
await page.waitForTimeout(1500)
expect(await dist(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).
Possible fixes
- Marko adapter: write the sizer height to the DOM synchronously before calling
_willUpdate(), mirroring react-virtual'sapplyContainerSizeordering from #1237. - Core: extend the clamp-detect-and-retry introduced in #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.
- 主要言語
- TypeScript
- スター
- 7.1k
- フォーク
- 466
- 平均マージ
- 2日 31分
- マージ済み PR(30日)
- 13
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
TanStack/virtual のほかの issue
-
難易度 4/5 3〜5日 初心者へのやさしさ 55/100
-
難易度 5/5 1週間以上 初心者へのやさしさ 38/100
-
難易度 4/5 3〜5日 初心者へのやさしさ 62/100
-
難易度 3/5 1〜2日 初心者へのやさしさ 68/100
-
難易度 4/5 3〜5日 初心者へのやさしさ 42/100
TanStack/virtual の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
vercel-labs/just-bash#464 ·
-
looksLikeSlug() is ASCII-only, so non-Latin entity slugs (e.g. Korean) skip exact match and collapse オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
-
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
TanStack/tanstack.com#1293 ·