audio streaming app plyr.fm
38
fork

Configure Feed

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

1# state management 2 3## svelte 5 runes mode 4 5plyr.fm uses svelte 5 runes mode throughout the frontend. **all reactive state must use the `$state()` rune**. 6 7### component-local state 8 9**critical**: in svelte 5 runes mode, plain `let` variables are NOT reactive. you must explicitly opt into reactivity. 10 11```typescript 12// ❌ WRONG - no reactivity, UI won't update 13let loading = true; 14let tracks = []; 15let selectedId = null; 16 17// assignments won't trigger UI updates 18loading = false; // template still shows "loading..." 19tracks = newTracks; // template won't re-render 20``` 21 22```typescript 23// ✅ CORRECT - reactive state 24let loading = $state(true); 25let tracks = $state<Track[]>([]); 26let selectedId = $state<number | null>(null); 27 28// assignments trigger UI updates 29loading = false; // template updates immediately 30tracks = newTracks; // template re-renders with new data 31``` 32 33### when to use `$state()` 34 35use `$state()` for any variable that: 36- is used in the template (`{#if loading}`, `{#each tracks}`, etc.) 37- needs to trigger UI updates when changed 38- is bound to form inputs (`bind:value={title}`) 39- is checked in reactive blocks (`$effect(() => { ... })`) 40 41### overridable `$derived` for optimistic UI (Svelte 5.25+) 42 43as of Svelte 5.25, `$derived` values can be temporarily overridden by reassignment. this is the recommended pattern for optimistic UI where you want to: 441. sync with a prop value (derived behavior) 452. temporarily override for immediate feedback (state behavior) 463. auto-reset when the prop updates 47 48```typescript 49// ✅ RECOMMENDED for optimistic UI (Svelte 5.25+) 50let liked = $derived(initialLiked); 51 52async function toggleLike() { 53 const previous = liked; 54 liked = !liked; // optimistic update - works in 5.25+! 55 56 try { 57 await saveLike(liked); 58 } catch { 59 liked = previous; // revert on failure 60 } 61} 62``` 63 64this replaces the older pattern of `$state` + `$effect` to sync with props: 65 66```typescript 67// ❌ OLD pattern - still works but more verbose 68let liked = $state(initialLiked); 69 70$effect(() => { 71 liked = initialLiked; // sync with prop 72}); 73``` 74 75use plain `let` for: 76- constants that never change 77- variables only used in functions/callbacks (not template) 78- intermediate calculations that don't need reactivity 79 80### common mistakes 81 82**1. mixing reactive and non-reactive state** 83 84```typescript 85// ❌ creates confusing bugs - some state updates, some doesn't 86let loading = true; // non-reactive 87let tracks = $state<Track[]>([]); // reactive 88let selectedId = $state<number | null>(null); // reactive 89``` 90 91**2. forgetting `$state()` after copy-pasting** 92 93```typescript 94// ❌ copied from svelte 4 code 95let editing = false; 96let editValue = ''; 97 98// ✅ updated for svelte 5 99let editing = $state(false); 100let editValue = $state(''); 101``` 102 103**3. assuming reactivity from svelte 4 habits** 104 105svelte 4 made all component `let` variables reactive by default. svelte 5 requires explicit `$state()` opt-in for finer control and better performance. 106 107### debugging reactivity issues 108 109**symptom**: template shows stale data even though console.log shows variable updated 110 111```typescript 112async function loadData() { 113 loading = true; // variable updates... 114 const data = await fetch(...); 115 loading = false; // variable updates... 116 console.log('loading:', loading); // logs "false" 117} 118// but UI still shows "loading..." spinner 119``` 120 121**diagnosis**: missing `$state()` wrapper 122 123```typescript 124// check variable declaration 125let loading = true; // ❌ missing $state() 126 127// fix 128let loading = $state(true); // ✅ now reactive 129``` 130 131**verification**: after adding `$state()`, check: 1321. variable assignments trigger template updates 1332. no console errors about "Cannot access X before initialization" 1343. UI reflects current variable value 135 136## global state management 137 138### overview 139 140plyr.fm uses global state managers following the Svelte 5 runes pattern for cross-component reactive state. 141 142## managers 143 144### player (`frontend/src/lib/player.svelte.ts`) 145- manages audio playback state globally 146- tracks: current track, play/pause, volume, progress 147- persists across navigation 148- integrates with queue for track advancement 149- media session integration in `Player.svelte` (see below) 150 151### uploader (`frontend/src/lib/uploader.svelte.ts`) 152- manages file uploads in background 153- fire-and-forget pattern - returns immediately 154- shows toast notifications for progress/completion 155- triggers cache refresh on success 156- server-sent events for real-time progress 157 158### tracks cache (`frontend/src/lib/tracks.svelte.ts`) 159- caches track list globally in localStorage 160- provides instant navigation by serving cached data 161- cursor-based pagination with `fetchMore()` for infinite scroll 162- pagination state (`nextCursor`, `hasMore`) persisted alongside tracks 163- invalidates on new uploads (resets to first page) 164- includes like status for each track (when authenticated) 165- simple invalidation model - no time-based expiry 166 167### queue (`frontend/src/lib/queue.svelte.ts`) 168- manages playback queue with server sync 169- optimistic updates with 250ms debounce 170- handles shuffle and repeat modes 171- conflict resolution for multi-device scenarios 172- see [`queue.md`](./queue.md) for details 173 174### liked tracks (`frontend/src/lib/tracks.svelte.ts`) 175- like/unlike functions exported from tracks module 176- invalidates cache on like/unlike 177- fetch liked tracks via `/tracks/liked` endpoint 178- integrates with main tracks cache for like status 179 180### preferences 181- user preferences managed through `ProfileMenu.svelte` (mobile) and `SettingsMenu.svelte` (desktop) 182- theme selection: dark / light / system (follows OS preference) 183- accent color customization 184- auto-play next track setting 185- hidden tags for discovery feed filtering 186- persisted to backend via `/preferences/` API 187- localStorage fallback for offline access 188- no dedicated state file - integrated into settings components 189 190### toast (`frontend/src/lib/toast.svelte.ts`) 191- global notification system 192- types: success, error, info, warning 193- auto-dismiss with configurable duration 194- in-place updates for progress changes 195 196## pattern 197 198```typescript 199class GlobalState { 200 // reactive state using $state rune 201 data = $state<Type>(initialValue); 202 203 // methods that mutate state 204 updateData(newValue: Type) { 205 this.data = newValue; 206 } 207} 208 209export const globalState = new GlobalState(); 210``` 211 212## benefits 213 214- survives navigation (state persists across route changes) 215- single source of truth 216- reactive updates automatically propagate to components 217- no prop drilling 218 219## flow examples 220 221### upload flow 222 2231. user clicks upload on dedicated `/upload` page (linked from portal or ProfileMenu) 2242. `uploader.upload()` called - returns immediately 2253. user navigates to homepage 2264. homepage renders cached tracks instantly (no blocking) 2275. upload completes in background 2286. success toast appears with "view track" link 2297. cache refreshes, homepage updates with new track 230 231this avoids HTTP/1.1 connection pooling issues by using cached data instead of blocking on fresh fetches during long-running uploads. 232 233### like flow 234 2351. user clicks like button on track 2362. `POST /tracks/{id}/like` sent to backend 2373. ATProto record created on user's PDS 2384. database updated 2395. tracks cache invalidated 2406. UI reflects updated like status on next cache fetch 2417. if error occurs, error logged to console 242 243### queue flow 244 2451. user clicks "play now" on a track 2462. queue state updates locally (instant feedback) 2473. debounced PUT to `/queue/` (250ms) 2484. server hydrates and returns full queue 2495. client merges server state 2506. other devices receive updates via periodic polling 251 252### preferences flow 253 2541. user changes accent color in settings 2552. UI updates immediately 2563. `PATCH /preferences/` sent in background 2574. server updates and returns new preferences 2585. changes persist across sessions 2596. localStorage provides offline access 260 261## state persistence 262 263### server-side persistence 264 265- **queue**: stored in `queue_state` table, synced via LISTEN/NOTIFY 266- **liked tracks**: stored in `track_likes` table 267- **preferences**: stored in `user_preferences` table 268 269### client-side persistence 270 271- **player volume**: localStorage (`playerVolume`) 272- **queue position**: synced with server, cached locally 273- **preferences**: server-authoritative, localStorage fallback 274 275## benefits 276 277- **cross-device sync**: queue and likes work across devices 278- **offline resilience**: localStorage provides graceful degradation 279- **instant feedback**: optimistic updates keep UI responsive 280- **server authority**: conflicts resolved by server state 281- **efficient updates**: debouncing and batching reduce API calls 282 283## media session API 284 285the player component (`frontend/src/lib/components/player/Player.svelte`) integrates with the [Media Session API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API) to provide metadata and controls to external interfaces like: 286 287- **lock screen controls** (iOS/Android) 288- **CarPlay / Android Auto** 289- **bluetooth devices** 290- **macOS control center** 291 292### metadata 293 294when a track plays, we update `navigator.mediaSession.metadata` with: 295- title, artist, album 296- artwork (track image or artist avatar fallback) 297 298### action handlers 299 300set up in `onMount`, these respond to system media controls: 301- `play` / `pause` - toggle playback 302- `previoustrack` / `nexttrack` - navigate queue 303- `seekto` - jump to position 304- `seekbackward` / `seekforward` - skip 10 seconds 305 306### position state 307 308reactive effects keep `navigator.mediaSession.setPositionState()` synced with playback progress, enabling scrubbers on lock screens. 309 310### implementation notes 311 312- handlers are registered once in `onMount` 313- metadata updates reactively via `$effect` when `player.currentTrack` changes 314- playback state syncs reactively with `player.paused` 315- position updates on every `player.currentTime` / `player.duration` change 316- gracefully no-ops if `navigator.mediaSession` is unavailable