diff --git a/site/src/components/ActivityFeed.svelte b/site/src/components/ActivityFeed.svelte index 0dac192..5ccda62 100644 --- a/site/src/components/ActivityFeed.svelte +++ b/site/src/components/ActivityFeed.svelte @@ -132,57 +132,83 @@ $: if (sport || datePre || query || customFrom || customTo) shown = PAGE_SIZE; // reset pagination on filter change - // When a search query or any date filter is active, eagerly load all - // remaining shards so results aren't limited to the initially-loaded year. - // Use only primary let-variables here — Svelte 5 doesn't reliably track - // derived $: variables as dependencies of side-effect $: blocks. - let loadingAllShards = false; + // Eager-load shards / feed-pages when a filter needs data not yet in memory. + let loadingAllShards = false; let loadingAllFeedPages = false; - $: if ((query.trim() || customFrom || customTo || datePre !== 'all') && pendingShards.length > 0 && !loadingAllShards) { - loadingAllShards = true; - (async () => { - while (pendingShards.length > 0) { - const url = pendingShards[0]; - pendingShards = pendingShards.slice(1); - try { - const fresh = await loadShardActivities(url); - const existing = new Map(all.map(a => [a.id, a])); - for (const a of fresh) if (!existing.has(a.id)) existing.set(a.id, a); - all = [...existing.values()].sort((a, b) => - (b.started_at ?? '').localeCompare(a.started_at ?? ''), - ); - } catch { /* ignore — partial results still useful */ } - } - loadingAllShards = false; - })(); + function _yearFromShard(url: string): number | null { + const m = url.match(/index-(\d{4})\.json$/); + return m ? parseInt(m[1], 10) : null; } - $: if ((query.trim() || customFrom || customTo || datePre !== 'all') && feedNextPage > 0 && !loadingAllFeedPages) { - loadingAllFeedPages = true; - (async () => { - // Snapshot at loop start — customFrom may change while we're fetching. - const fromFilter = customFrom; - while (feedNextPage > 0) { - const page = feedNextPage; - feedNextPage = page < feedTotalPages ? page + 1 : 0; - try { - const fresh = await loadCombinedFeedPage(base, page); - const existing = new Map(all.map(a => [a.id, a])); - for (const a of fresh) if (!existing.has(a.id)) existing.set(a.id, a); - all = [...existing.values()].sort((a, b) => - (b.started_at ?? '').localeCompare(a.started_at ?? ''), - ); - // Feed is sorted newest-first. Once the oldest activity in this page - // predates our from-filter, everything needed is already loaded. - if (fromFilter && fresh.length > 0) { - const oldest = fresh.reduce((m, a) => (a.started_at ?? '') < (m.started_at ?? '') ? a : m); - if ((oldest.started_at ?? '') < fromFilter) { feedNextPage = 0; break; } - } - } catch { /* ignore — partial results still useful */ } - } - loadingAllFeedPages = false; - })(); + // Returns [minYear, maxYear] for year-specific filters (year preset or custom + // date range). Returns null for open-ended filters (all, 7d, 30d, 6mo). + function _neededYearRange(pre: string, from: string, to: string): [number, number] | null { + if (from || to) { + const fy = from ? parseInt(from.slice(0, 4), 10) : 0; + const ty = to ? parseInt(to.slice(0, 4), 10) : 9999; + return [fy, ty]; + } + if (/^\d{4}$/.test(pre)) { const y = parseInt(pre, 10); return [y, y]; } + return null; + } + + $: { + const yr = _neededYearRange(datePre, customFrom, customTo); + const needEager = !!query.trim() || yr !== null; + if (needEager && pendingShards.length > 0 && !loadingAllShards) { + loadingAllShards = true; + // When year-specific filter (no search), load only shards that cover + // the needed range; unneeded shards stay in pendingShards for "Load more". + // When search is active, load everything so full-text search works. + const toLoad = (yr && !query.trim()) + ? pendingShards.filter(url => { const y = _yearFromShard(url); return y !== null && y >= yr[0] && y <= yr[1]; }) + : [...pendingShards]; + (async () => { + for (const url of toLoad) { + pendingShards = pendingShards.filter(u => u !== url); + try { + const fresh = await loadShardActivities(url); + const existing = new Map(all.map(a => [a.id, a])); + for (const a of fresh) if (!existing.has(a.id)) existing.set(a.id, a); + all = [...existing.values()].sort((a, b) => + (b.started_at ?? '').localeCompare(a.started_at ?? ''), + ); + } catch { /* ignore — partial results still useful */ } + } + loadingAllShards = false; + })(); + } + } + + $: { + const yr = _neededYearRange(datePre, customFrom, customTo); + if ((!!query.trim() || yr !== null) && feedNextPage > 0 && !loadingAllFeedPages) { + loadingAllFeedPages = true; + // Capture at loop start — dateFrom is reactive and may change mid-fetch. + const effectiveFrom = dateFrom; + (async () => { + while (feedNextPage > 0) { + const page = feedNextPage; + feedNextPage = page < feedTotalPages ? page + 1 : 0; + try { + const fresh = await loadCombinedFeedPage(base, page); + const existing = new Map(all.map(a => [a.id, a])); + for (const a of fresh) if (!existing.has(a.id)) existing.set(a.id, a); + all = [...existing.values()].sort((a, b) => + (b.started_at ?? '').localeCompare(a.started_at ?? ''), + ); + // Feed is sorted newest-first. Once the oldest activity in this page + // predates our from-filter, everything needed is already loaded. + if (effectiveFrom && fresh.length > 0) { + const oldest = fresh.reduce((m, a) => (a.started_at ?? '') < (m.started_at ?? '') ? a : m); + if ((oldest.started_at ?? '') < effectiveFrom) { feedNextPage = 0; break; } + } + } catch { /* ignore — partial results still useful */ } + } + loadingAllFeedPages = false; + })(); + } } $: if (mounted) {