@@ -554,23 +554,20 @@ const currentLetter = computed<string>(() => {
554554 return " " ;
555555});
556556
557- // Per-card viewport-driven fetch. The shell tracks which positions are
558- // currently visible (from the rows in `viewportRange`) and keeps
559- // `byPosition` in sync via per-card `GET /roms/{id}/simple` calls — pure
560- // by-id lookups on the backend, much faster than the paginated
561- // `getRoms(limit/offset)` pipeline. Each fetch is independent, so covers
562- // stream in as their individual responses land.
557+ // Viewport-driven windowed fetch. The shell collects which positions are
558+ // currently visible (from the rows in `viewportRange`, for both grid and
559+ // list layouts) and hands them to the store's `syncVisibleWindows`, which
560+ // aligns each to its shared 72-item window, dedupes, starts the windows
561+ // covering the viewport, and cancels any that scrolled out of view.
562+ // Batching visible cards into a handful of paginated `getRoms` requests
563+ // (instead of one request per card) is what keeps a fast scroll — or two
564+ // users scrolling at once — from flooding the single-worker backend.
563565//
564- // No idle-time prefetch: when the user stops scrolling, no new requests
565- // are fired. Cards already in the viewport that are still missing keep
566- // loading; everything off-screen waits until the user scrolls there.
567- //
568- // Cancellation: positions that leave the viewport while their fetch is in
569- // flight are aborted via `cancelFetchAt`. A small debounce on viewport
570- // changes prevents fire-and-cancel storms during smooth scrolling — only
571- // when the viewport settles for `FETCH_DEBOUNCE_MS` do we sync.
566+ // A small debounce on viewport changes prevents fire-and-cancel storms
567+ // during smooth scrolling — only when the viewport settles for
568+ // `FETCH_DEBOUNCE_MS` do we sync. Both layouts share this one path, so the
569+ // list is debounced too (list rows no longer self-fetch on mount).
572570const FETCH_DEBOUNCE_MS = 80 ;
573- const visiblePositions = new Set <number >();
574571let fetchDebounceTimer: ReturnType <typeof setTimeout > | null = null ;
575572let pendingRange: { first: number ; last: number } | null = null ;
576573
@@ -583,34 +580,20 @@ function collectVisiblePositions(range: {
583580 const items = virtualItems .value ;
584581 for (let i = range .first ; i <= range .last ; i ++ ) {
585582 const it = items [i ];
586- if (! it || it .kind !== " row" ) continue ;
587- for (let p = it .startPosition ; p < it .endPosition ; p ++ ) out .add (p );
583+ if (! it ) continue ;
584+ // Grid rows fan out into a contiguous run of card positions; list rows
585+ // carry a single position each.
586+ if (it .kind === " row" ) {
587+ for (let p = it .startPosition ; p < it .endPosition ; p ++ ) out .add (p );
588+ } else if (it .kind === " list-row" ) {
589+ out .add (it .position );
590+ }
588591 }
589592 return out ;
590593}
591594
592595function syncFetches(range : { first: number ; last: number }) {
593- const next = collectVisiblePositions (range );
594-
595- // Cancel positions that left the viewport before their fetch resolved.
596- // The store's per-position controller handles the network abort;
597- // idempotent if nothing was in flight.
598- for (const p of visiblePositions ) {
599- if (! next .has (p ) && ! galleryRoms .byPosition .has (p )) {
600- galleryRoms .cancelFetchAt (p );
601- }
602- }
603-
604- // Fire per-card fetches for positions that just entered. The store
605- // dedupes against in-flight + already-loaded internally.
606- for (const p of next ) {
607- if (! galleryRoms .byPosition .has (p )) {
608- void galleryRoms .fetchRomAt (p );
609- }
610- }
611-
612- visiblePositions .clear ();
613- for (const p of next ) visiblePositions .add (p );
596+ galleryRoms .syncVisibleWindows (collectVisiblePositions (range ));
614597}
615598
616599function scheduleFetchSync(range : { first: number ; last: number }) {
@@ -663,9 +646,9 @@ function scrollToLetter(letter: string) {
663646 toolbarHeight .value + (layout .value === " list" ? LIST_HEADER_HEIGHT : 0 );
664647 scrollerRef .value ?.scrollToIndex (idx , { smooth: true , stickyOffset });
665648 // The viewport-driven fetch sync handles the destination — once the
666- // smooth scroll settles, `update:viewportRange` fires and the cards
667- // at the landing zone start loading via `syncFetches` (grid) or via
668- // each `GameListRow`'s onMounted (list). No manual prefetch needed.
649+ // smooth scroll settles, `update:viewportRange` fires and the windows at
650+ // the landing zone start loading via `syncFetches` (both layouts). No
651+ // manual prefetch needed.
669652}
670653
671654// ── Search filter (debounced) ───────────────────────────────────────
@@ -792,12 +775,10 @@ onBeforeUnmount(() => {
792775 gallerySelection .clear ();
793776 if (searchDebounce ) clearTimeout (searchDebounce );
794777 if (fetchDebounceTimer ) clearTimeout (fetchDebounceTimer );
795- // When leaving the gallery entirely, stop any per-card fetches still in
796- // flight so navigating away mid-scroll doesn't keep the network /
797- // backend busy. Keeps the hydrated cache so returning to the same
798- // gallery is instant.
778+ // When leaving the gallery entirely, stop any in-flight window fetches so
779+ // navigating away mid-scroll doesn't keep the network / backend busy.
780+ // Keeps the hydrated cache so returning to the same gallery is instant.
799781 galleryRoms .abortInFlight ();
800- visiblePositions .clear ();
801782 inflowResizeObserver ?.disconnect ();
802783 inflowResizeObserver = null ;
803784 // Drop the debug stats so the overlay doesn't show stale gallery numbers
0 commit comments