Skip to content

Commit 04c000e

Browse files
authored
Merge pull request #3789 from rommapp/posthog-code/v2-gallery-per-card-fetch
revert(v2): hydrate gallery cards per-card via /roms/{id}/simple
2 parents 68c0f4c + 69c44d9 commit 04c000e

3 files changed

Lines changed: 151 additions & 259 deletions

File tree

frontend/src/v2/components/Gallery/GalleryShell.vue

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
573569
const FETCH_DEBOUNCE_MS = 80;
570+
const visiblePositions = new Set<number>();
574571
let fetchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
575572
let pendingRange: { first: number; last: number } | null = null;
576573
@@ -590,7 +587,27 @@ function collectVisiblePositions(range: {
590587
}
591588
592589
function 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
596613
function 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

frontend/src/v2/components/Gallery/GameListRow.vue

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
// GameListRow — single row of the list-mode gallery.
33
//
44
// Owns:
5-
// * Per-row lazy fetch: onMount fires `fetchWindowAt(position)` and
6-
// the store aligns the position to its 72-item window, deduping
7-
// against pending + loaded windows, so rows sharing a window share
8-
// one request. Mirror of the grid flow, with the same "row mount =
5+
// * Per-row lazy fetch: a watch fires `fetchRomAt(position)` whenever
6+
// the row is missing its rom but the store knows the id — covering
7+
// both mount ("entered the overscan window") and a context refresh
8+
// that clears `byPosition` under a still-mounted row. onUnmount aborts
9+
// the fetch via `cancelFetchAt(position)` so a fast scroll past
10+
// mid-flight doesn't keep server work queued for invisible rows.
11+
// Mirror of the per-card grid flow, with the same "row mount =
912
// entered viewport" contract via the shell's RVirtualScroller
1013
// overscan window.
1114
//
@@ -26,7 +29,7 @@ import {
2629
RSkeletonBlock,
2730
RTooltip,
2831
} from "@v2/lib";
29-
import { computed, onMounted } from "vue";
32+
import { computed, onBeforeUnmount, watch } from "vue";
3033
import { useI18n } from "vue-i18n";
3134
import { useRouter } from "vue-router";
3235
import storePlatforms from "@/stores/platforms";
@@ -261,13 +264,37 @@ function onRowPointerEnd() {
261264
selectionInput.handlePointerEnd();
262265
}
263266
264-
onMounted(() => {
265-
// Static mode (rom passed as prop) skips the gallery's per-row fetch
266-
// entirely — the consumer already owns the rom data.
267+
// Kick the per-row fetch whenever this position is missing its rom but
268+
// the store knows its id. This covers both mount ("entered the overscan
269+
// window") and a context refresh: a filter / search / sort / scan clears
270+
// `byPosition` and repopulates `romIdIndex`, but the row can stay mounted
271+
// at the same position — without this watch it would sit on skeletons
272+
// until it scrolled out and remounted. The store dedupes against
273+
// in-flight + already-loaded, so re-runs are cheap. Guarding on the id
274+
// (not just null rom) also re-fetches when a resort lands a different rom
275+
// at the same position.
276+
watch(
277+
() => {
278+
if (isStatic.value || props.position === undefined) return null;
279+
if (galleryRoms.byPosition.has(props.position)) return null;
280+
return galleryRoms.romIdIndex[props.position] ?? null;
281+
},
282+
(romId) => {
283+
if (romId != null && props.position !== undefined) {
284+
void galleryRoms.fetchRomAt(props.position);
285+
}
286+
},
287+
{ immediate: true },
288+
);
289+
290+
onBeforeUnmount(() => {
267291
if (isStatic.value || props.position === undefined) return;
268-
// Entered the overscan window, so kick the window fetch. Store aligns
269-
// to the window grid and dedupes against pending + loaded windows.
270-
void galleryRoms.fetchWindowAt(props.position);
292+
// Left the overscan window before the fetch resolved — abort so the
293+
// server doesn't keep building a row the user already scrolled past.
294+
// Idempotent if nothing was in flight.
295+
if (!galleryRoms.byPosition.has(props.position)) {
296+
galleryRoms.cancelFetchAt(props.position);
297+
}
271298
});
272299
</script>
273300

0 commit comments

Comments
 (0)