From 9908c431f0d9c850cc402e2bc7011d813a3bf859 Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Tue, 30 Dec 2025 18:02:08 -0500 Subject: [PATCH] Pause flow list updates while scrolled --- apps/web/app/page.tsx | 62 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 9a570a0..470c0a3 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -276,6 +276,7 @@ type ListScrollState = { isAtTop: boolean; isAtTopRef: React.MutableRefObject; missed: number; + resumeTick: number; onNewItems: (count: number) => void; jumpToTop: () => void; }; @@ -284,7 +285,9 @@ const useListScroll = (): ListScrollState => { const listRef = useRef(null); const [isAtTop, setIsAtTop] = useState(true); const [missed, setMissed] = useState(0); + const [resumeTick, setResumeTick] = useState(0); const isAtTopRef = useRef(true); + const prevAtTopRef = useRef(true); useEffect(() => { isAtTopRef.current = isAtTop; @@ -298,6 +301,11 @@ const useListScroll = (): ListScrollState => { const atTop = el.scrollTop <= 2; + if (atTop && !prevAtTopRef.current) { + setResumeTick((prev) => prev + 1); + } + + prevAtTopRef.current = atTop; isAtTopRef.current = atTop; setIsAtTop(atTop); @@ -353,6 +361,7 @@ const useListScroll = (): ListScrollState => { isAtTop, isAtTopRef, missed, + resumeTick, onNewItems, jumpToTop }; @@ -793,6 +802,8 @@ const useLiveStream = ( expectedType: MessageType; onNewItems?: (count: number) => void; captureScroll?: () => void; + shouldHold?: () => boolean; + resumeSignal?: number; } ): TapeState => { const [status, setStatus] = useState( @@ -810,6 +821,7 @@ const useLiveStream = ( const pendingRef = useRef([]); const pendingCountRef = useRef(0); const flushHandleRef = useRef(null); + const holdRef = useRef([]); useEffect(() => { pausedRef.current = paused; @@ -842,14 +854,25 @@ const useLiveStream = ( config.onNewItems(pendingCount); } - if (config.captureScroll) { + const shouldHold = config.shouldHold ? config.shouldHold() : false; + if (!shouldHold && config.captureScroll) { config.captureScroll(); } - setItems((prev) => mergeNewest(buffered, prev)); + if (shouldHold) { + holdRef.current = mergeNewest(buffered, holdRef.current); + setLastUpdate(Date.now()); + return; + } + + const nextBatch = + holdRef.current.length > 0 ? [...holdRef.current, ...buffered] : buffered; + holdRef.current = []; + + setItems((prev) => mergeNewest(nextBatch, prev)); setLastUpdate(Date.now()); }); - }, [config.captureScroll, config.onNewItems]); + }, [config.captureScroll, config.onNewItems, config.shouldHold]); const togglePause = useCallback(() => { setPaused((prev) => { @@ -868,6 +891,7 @@ const useLiveStream = ( setLastUpdate(null); pendingRef.current = []; pendingCountRef.current = 0; + holdRef.current = []; cancelFlush(); return; } @@ -951,6 +975,21 @@ const useLiveStream = ( }; }, [config.enabled, config.expectedType, config.wsPath, scheduleFlush, cancelFlush]); + useEffect(() => { + if (config.resumeSignal === undefined) { + return; + } + if (config.shouldHold && config.shouldHold()) { + return; + } + if (holdRef.current.length === 0) { + return; + } + setItems((prev) => mergeNewest(holdRef.current, prev)); + holdRef.current = []; + setLastUpdate(Date.now()); + }, [config.resumeSignal, config.shouldHold]); + return { status, items, @@ -966,14 +1005,18 @@ const useLiveStream = ( const useFlowStream = ( enabled: boolean, onNewItems?: (count: number) => void, - captureScroll?: () => void + captureScroll?: () => void, + shouldHold?: () => boolean, + resumeSignal?: number ): TapeState => { return useLiveStream({ enabled, wsPath: "/ws/flow", expectedType: "flow-packet", onNewItems, - captureScroll + captureScroll, + shouldHold, + resumeSignal }); }; @@ -1311,7 +1354,14 @@ export default function HomePage() { pollMs: mode === "replay" ? 200 : undefined }); - const flow = useFlowStream(mode === "live", flowScroll.onNewItems, flowAnchor.capture); + const flowHold = useCallback(() => !flowScroll.isAtTopRef.current, [flowScroll.isAtTopRef]); + const flow = useFlowStream( + mode === "live", + flowScroll.onNewItems, + flowAnchor.capture, + flowHold, + flowScroll.resumeTick + ); const alerts = useLiveStream({ enabled: mode === "live", wsPath: "/ws/alerts",