@@ -551,26 +551,23 @@ const currentLetter = computed<string>(() => {
551551 return " " ;
552552});
553553
554- // Viewport-driven fetch. The shell collects the positions currently in
555- // view (rows in `viewportRange`) and hands them to the store, which keeps
556- // `byPosition` in sync by fetching the 72-item windows those positions
557- // fall into. Windows are shared across cards and cached, so a full
558- // viewport resolves in a handful of paginated requests rather than one
559- // per card. (Fetching per-card instead issued one request plus one DB
560- // lookup per visible card, so ~100 simultaneous round-trips on a full
561- // grid, which the browser's per-host connection cap then serialized into
562- // slow waves on low-power devices.)
563- //
564- // The store also aborts windows that scrolled out of view, so paging
565- // through a large library doesn't leave departed windows downloading.
554+ // Per-card viewport-driven fetch. The shell tracks which positions are
555+ // currently visible (from the rows in `viewportRange`) and keeps
556+ // `byPosition` in sync via per-card `GET /roms/{id}/simple` calls — pure
557+ // by-id lookups on the backend, much faster than the paginated
558+ // `getRoms(limit/offset)` pipeline. Each fetch is independent, so covers
559+ // stream in as their individual responses land.
566560//
567561// No idle-time prefetch: when the user stops scrolling, no new requests
568- // are fired for off-screen positions.
562+ // are fired. Cards already in the viewport that are still missing keep
563+ // loading; everything off-screen waits until the user scrolls there.
569564//
570- // A small debounce on viewport changes prevents request storms during
571- // smooth scrolling: only when the viewport settles for
572- // `FETCH_DEBOUNCE_MS` do we sync.
565+ // Cancellation: positions that leave the viewport while their fetch is in
566+ // flight are aborted via `cancelFetchAt`. A small debounce on viewport
567+ // changes prevents fire-and-cancel storms during smooth scrolling — only
568+ // when the viewport settles for `FETCH_DEBOUNCE_MS` do we sync.
573569const FETCH_DEBOUNCE_MS = 80 ;
570+ const visiblePositions = new Set <number >();
574571let fetchDebounceTimer: ReturnType <typeof setTimeout > | null = null ;
575572let pendingRange: { first: number ; last: number } | null = null ;
576573
@@ -590,7 +587,27 @@ function collectVisiblePositions(range: {
590587}
591588
592589function syncFetches(range : { first: number ; last: number }) {
593- galleryRoms .syncVisibleWindows (collectVisiblePositions (range ));
590+ const next = collectVisiblePositions (range );
591+
592+ // Cancel positions that left the viewport before their fetch resolved.
593+ // The store's per-position controller handles the network abort;
594+ // idempotent if nothing was in flight.
595+ for (const p of visiblePositions ) {
596+ if (! next .has (p ) && ! galleryRoms .byPosition .has (p )) {
597+ galleryRoms .cancelFetchAt (p );
598+ }
599+ }
600+
601+ // Fire per-card fetches for positions that just entered. The store
602+ // dedupes against in-flight + already-loaded internally.
603+ for (const p of next ) {
604+ if (! galleryRoms .byPosition .has (p )) {
605+ void galleryRoms .fetchRomAt (p );
606+ }
607+ }
608+
609+ visiblePositions .clear ();
610+ for (const p of next ) visiblePositions .add (p );
594611}
595612
596613function scheduleFetchSync(range : { first: number ; last: number }) {
@@ -772,10 +789,12 @@ onBeforeUnmount(() => {
772789 gallerySelection .clear ();
773790 if (searchDebounce ) clearTimeout (searchDebounce );
774791 if (fetchDebounceTimer ) clearTimeout (fetchDebounceTimer );
775- // When leaving the gallery entirely, stop any window fetches still
776- // downloading so navigating away mid-scroll doesn't keep the network /
777- // backend busy. Keeps the loaded cache so returning to the same gallery is instant.
792+ // When leaving the gallery entirely, stop any per-card fetches still in
793+ // flight so navigating away mid-scroll doesn't keep the network /
794+ // backend busy. Keeps the hydrated cache so returning to the same
795+ // gallery is instant.
778796 galleryRoms .abortInFlight ();
797+ visiblePositions .clear ();
779798 inflowResizeObserver ?.disconnect ();
780799 inflowResizeObserver = null ;
781800 // Drop the debug stats so the overlay doesn't show stale gallery numbers
0 commit comments