diff --git a/frontend/src/stores/showcase.js b/frontend/src/stores/showcase.js index 72e5967..78da18b 100644 --- a/frontend/src/stores/showcase.js +++ b/frontend/src/stores/showcase.js @@ -19,6 +19,16 @@ import { useApi } from '../composables/useApi.js' const PAGE = 3 const INITIAL_BATCHES = 20 const APPEND_DELAY_MS = 80 // ≈ the MasonryGrid stagger animation (70 ms) +// Operator-flagged 2026-06-01: scrolling the showcase eventually hit a +// premature "End." because /api/showcase returns a *random sample* and +// after enough scrolling the `seen` Set accumulated enough to fully +// collide with a 3-item batch. The showcase is supposed to be endless; +// only a genuinely empty API response (library has zero images) should +// mark it exhausted. Retry up to FETCH_RETRY_CAP times on all-dupe +// batches; only flip `exhausted` when the API returns 0 items OR every +// retry came back dupe-only (graceful fallback for tiny libraries +// where retries will keep returning the same handful of items). +const FETCH_RETRY_CAP = 8 function _sleep(ms) { return new Promise(r => setTimeout(r, ms)) } @@ -48,17 +58,32 @@ export const useShowcaseStore = defineStore('showcase', () => { } } - // Single batch — used by infinite-scroll appends. Trickles its 5 items - // in for the same one-at-a-time cadence as the initial load. + // Single batch — used by infinite-scroll appends. Trickles its items + // in for the same one-at-a-time cadence as the initial load. Retries + // up to FETCH_RETRY_CAP times when the API's random sample comes back + // all-duplicates (the showcase is endless by design; only a genuinely + // empty API response should mark it exhausted, not an unlucky sample). async function fetchPage() { if (loading.value) return loading.value = true error.value = null try { - const body = await api.get('/api/showcase', { params: { limit: PAGE } }) - const fresh = (body.images || []).filter(i => !seen.has(i.id)) - if (fresh.length === 0) { exhausted.value = true; return } - await _trickleAppend(fresh, _seq) + for (let attempt = 0; attempt < FETCH_RETRY_CAP; attempt++) { + const body = await api.get('/api/showcase', { params: { limit: PAGE } }) + const items = body.images || [] + // API genuinely empty → library is empty / endpoint exhausted. + if (items.length === 0) { exhausted.value = true; return } + const fresh = items.filter(i => !seen.has(i.id)) + if (fresh.length > 0) { + await _trickleAppend(fresh, _seq) + return + } + // All-dupes batch — keep trying. Showcase is endless by intent. + } + // Retry cap hit with zero fresh items: library is probably much + // smaller than the running `seen` set, fall back to exhausted so + // the UI stops trying. Operator can shuffle to reset `seen`. + exhausted.value = true } catch (e) { error.value = e.message || String(e) } finally {