audio streaming app plyr.fm
38
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: persist network artists across navigations with module-level cache (#974)

networkArtists was component-local $state — destroyed on every
navigation away from the homepage and re-fetched on return. Now uses
a module-level singleton cache (like tracksCache, statsCache) with a
5-minute TTL so the section appears instantly on subsequent visits.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

authored by

nate nowack
Claude Opus 4.6
and committed by
GitHub
372d7fe4 986b2b44

+54 -19
+50
frontend/src/lib/network-artists.svelte.ts
··· 1 + // network artists cache - prevents refetching on navigation 2 + import { browser } from '$app/environment'; 3 + import { API_URL } from '$lib/config'; 4 + 5 + export interface NetworkArtist { 6 + did: string; 7 + handle: string; 8 + display_name: string; 9 + avatar_url: string | null; 10 + track_count: number; 11 + } 12 + 13 + class NetworkArtistsCache { 14 + artists = $state<NetworkArtist[]>([]); 15 + loading = $state(false); 16 + private lastFetch = 0; 17 + private readonly CACHE_DURATION = 300_000; // 5 minutes 18 + 19 + get hasArtists(): boolean { 20 + return this.artists.length > 0; 21 + } 22 + 23 + async fetch(force = false): Promise<void> { 24 + if (!browser) return; 25 + 26 + const now = Date.now(); 27 + const isCacheValid = 28 + this.artists.length > 0 && now - this.lastFetch < this.CACHE_DURATION; 29 + 30 + if (!force && isCacheValid) return; 31 + if (this.loading) return; 32 + 33 + this.loading = true; 34 + try { 35 + const response = await fetch(`${API_URL}/discover/network`, { 36 + credentials: 'include' 37 + }); 38 + if (response.ok) { 39 + this.artists = await response.json(); 40 + this.lastFetch = now; 41 + } 42 + } catch (e) { 43 + console.error('failed to load network artists:', e); 44 + } finally { 45 + this.loading = false; 46 + } 47 + } 48 + } 49 + 50 + export const networkArtistsCache = new NetworkArtistsCache();
+4 -19
frontend/src/routes/+page.svelte
··· 9 9 import { player } from '$lib/player.svelte'; 10 10 import { queue } from '$lib/queue.svelte'; 11 11 import { tracksCache, fetchTopTracks } from '$lib/tracks.svelte'; 12 + import { networkArtistsCache } from '$lib/network-artists.svelte'; 12 13 import type { Track } from '$lib/types'; 13 14 import { auth } from '$lib/auth.svelte'; 14 15 import { fade } from 'svelte/transition'; 15 16 import { APP_NAME, APP_TAGLINE, APP_CANONICAL_URL } from '$lib/branding'; 16 - import { API_URL } from '$lib/config'; 17 17 import { 18 18 getRefreshedAvatar, 19 19 triggerAvatarRefresh, 20 20 hasAttemptedRefresh 21 21 } from '$lib/avatar-refresh.svelte'; 22 22 23 - interface NetworkArtist { 24 - did: string; 25 - handle: string; 26 - display_name: string; 27 - avatar_url: string | null; 28 - track_count: number; 29 - } 30 - 31 23 // use cached tracks 32 24 let tracks = $derived(tracksCache.tracks); 33 25 let loadingTracks = $derived(tracksCache.loading); ··· 42 34 let hasTopTracks = $derived(topTracks.length > 0); 43 35 44 36 // network artists (people you follow on bluesky who have music here) 45 - let networkArtists = $state<NetworkArtist[]>([]); 46 - let hasNetworkArtists = $derived(networkArtists.length > 0); 37 + let networkArtists = $derived(networkArtistsCache.artists); 38 + let hasNetworkArtists = $derived(networkArtistsCache.hasArtists); 47 39 48 40 // show loading during initial load or when actively loading with no cached data 49 41 let showLoading = $derived((initialLoad && !hasTracks) || (loadingTracks && !hasTracks)); ··· 55 47 let sentinelElement = $state<HTMLDivElement | null>(null); 56 48 57 49 onMount(async () => { 58 - const networkPromise = auth.isAuthenticated 59 - ? fetch(`${API_URL}/discover/network`, { credentials: 'include' }) 60 - .then((r) => (r.ok ? r.json() : [])) 61 - .then((artists: NetworkArtist[]) => (networkArtists = artists)) 62 - .catch(() => {}) 63 - : Promise.resolve(); 64 - 65 50 const [topResult] = await Promise.all([ 66 51 fetchTopTracks(10), 67 52 tracksCache.fetch(), 68 - networkPromise 53 + auth.isAuthenticated ? networkArtistsCache.fetch() : Promise.resolve() 69 54 ]); 70 55 topTracks = topResult; 71 56 loadingTopTracks = false;