mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
fix(frontend): wait for mounted extent rebound
Problem: WebView2 can restore the native transcript extent before Virtuoso has remounted visible rows, so reader-stability correction can expose a blank frame. Root cause: the correction was eligible on the first rebound sample and did not require mounted viewport coverage or stable restored geometry. Fix: gate the single correction on a nonblank viewport and one unchanged-height interval, with a regression test covering the rebound race. Verification: pnpm test:transcript; pnpm test:transcript-browser; five consecutive transcript scroll stability replays; pnpm test:remote; pnpm test:performance; pnpm build; repolint.
This commit is contained in:
1 parent
79d612b601
commit
444b80284f
2 files changed
+27
-2
No files matched your search
@@ -69,8 +69,12 @@ const rectAt = (top: number) => ({
|
||||
});
|
||||
const scrollElement = dom.window.document.getElementById("scroll") as HTMLDivElement;
|
||||
const rowElement = scrollElement.querySelector<HTMLElement>(".transcript__row")!;
|
||||
const reboundCoverageRow = dom.window.document.createElement("div");
|
||||
reboundCoverageRow.className = "transcript__row";
|
||||
reboundCoverageRow.dataset.rowKey = "row-ready";
|
||||
scrollElement.getBoundingClientRect = () => rectAt(0);
|
||||
rowElement.getBoundingClientRect = () => rectAt(20);
|
||||
reboundCoverageRow.getBoundingClientRect = () => rectAt(20);
|
||||
Object.defineProperty(scrollElement, "clientHeight", { configurable: true, value: 725 });
|
||||
let scrollExtent = 15_829;
|
||||
Object.defineProperty(scrollElement, "scrollHeight", { configurable: true, get: () => scrollExtent });
|
||||
@@ -145,13 +149,18 @@ await act(async () => arbiter?.followGrowingTail());
|
||||
await flushFrames();
|
||||
check(scrollByCalls === 0, "the transaction waits while the native extent remains collapsed");
|
||||
scrollExtent = 15_829;
|
||||
scrollElement.append(reboundCoverageRow);
|
||||
await act(async () => arbiter?.followGrowingTail());
|
||||
await flushFrames();
|
||||
check(scrollByCalls === 0,
|
||||
"the rebound waits for mounted coverage and stable restored geometry");
|
||||
await flushFrames();
|
||||
check(scrollByCalls === 1 && lastScrollByTop > 1_900,
|
||||
`the rebound restores the logical anchor exactly once (${lastScrollByTop}px)`);
|
||||
check(scrollWrites.length === 1 && scrollWrites[0].owner === "reader-stability",
|
||||
"the correction is owned by reader stability rather than recovery or tail-follow");
|
||||
check(arbiter?.modeRef.current === "manual", "the correction preserves manual reader ownership");
|
||||
reboundCoverageRow.remove();
|
||||
|
||||
// Touch movement is incremental: the second touchmove protects only its own
|
||||
// segment rather than replaying the distance from the original touchstart.
|
||||
@@ -173,13 +182,16 @@ await act(async () => arbiter?.onTouchMoveIntent({
|
||||
scrollExtent = 4_000;
|
||||
scrollElement.scrollTop = 1_000;
|
||||
rowElement.remove();
|
||||
scrollElement.append(reboundCoverageRow);
|
||||
await act(async () => arbiter?.deliverScroll());
|
||||
scrollExtent = 5_000;
|
||||
scrollByCalls = 0;
|
||||
lastScrollByTop = 0;
|
||||
await flushFrames();
|
||||
await flushFrames();
|
||||
check(scrollByCalls === 1 && lastScrollByTop === 1_020,
|
||||
`consecutive touch segments use incremental geometry (${lastScrollByTop}px)`);
|
||||
reboundCoverageRow.remove();
|
||||
scrollElement.append(rowElement);
|
||||
|
||||
// Ordinary sub-viewport measurement jitter stays browser-owned, and a higher
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
import type { TranscriptScrollMode } from "./transcriptScrollArbiter";
|
||||
import { nativeTranscriptDistanceFromBottom } from "./transcriptScrollGeometry";
|
||||
import type { TranscriptScrollWriteRecord } from "./transcriptScrollProbe";
|
||||
import { captureTranscriptLayoutAnchor } from "./transcriptVirtuosoRecovery";
|
||||
import {
|
||||
captureTranscriptLayoutAnchor,
|
||||
transcriptElementViewportIsBlank,
|
||||
} from "./transcriptVirtuosoRecovery";
|
||||
|
||||
const READER_EXTENT_STABILITY_MS = 180;
|
||||
|
||||
@@ -68,6 +71,7 @@ export function useTranscriptReaderExtentStability({
|
||||
deadline: Date.now() + READER_EXTENT_STABILITY_MS,
|
||||
frame: null,
|
||||
};
|
||||
let reboundHeight: number | undefined;
|
||||
const tick = () => {
|
||||
active.frame = null;
|
||||
if (
|
||||
@@ -85,7 +89,15 @@ export function useTranscriptReaderExtentStability({
|
||||
clientHeight: element.clientHeight,
|
||||
};
|
||||
observeTranscriptReaderExtent(active, snapshot);
|
||||
if (transcriptReaderExtentCanCorrect(active, snapshot)) {
|
||||
// WebView2 can publish the restored native extent one or two paints
|
||||
// before Virtuoso mounts rows for it. Writing during that gap moves the
|
||||
// viewport into an empty size tree. Wait for mounted coverage and one
|
||||
// unchanged-height interval, then spend the single correction budget.
|
||||
if (
|
||||
transcriptReaderExtentCanCorrect(active, snapshot)
|
||||
&& !transcriptElementViewportIsBlank(element)
|
||||
&& reboundHeight === snapshot.scrollHeight
|
||||
) {
|
||||
const row = active.anchor
|
||||
? Array.from(element.querySelectorAll<HTMLElement>(".transcript__row[data-row-key]"))
|
||||
.find((candidate) => candidate.dataset.rowKey === active.anchor?.rowKey)
|
||||
@@ -109,6 +121,7 @@ export function useTranscriptReaderExtentStability({
|
||||
return;
|
||||
}
|
||||
}
|
||||
reboundHeight = snapshot.scrollHeight;
|
||||
if (Date.now() >= active.deadline) {
|
||||
guardRef.current = null;
|
||||
return;
|
||||
|
||||
Reference in new issue
Block a user