Persistence¶
All frontend persistence uses localStorage. There are no IndexedDB, file system, or network stores on the frontend side. Rust handles disk persistence for auth sessions and debug logs.
Sources: src/persistence/
Storage key map¶
| Key | Shape | Purpose |
|---|---|---|
neegde.trackCache.v1 |
Record<id, TrackData> |
Full track data for every track ever seen |
neegde.albumCache.v1 |
Record<id, AlbumData> |
Full album data for liked albums |
neegde.queue.v2 |
{ trackIds: string[], pos: number } |
Current playback queue and position |
neegde.likes.v2 |
{ trackIds, albumIds, likedAt } |
Liked track/album IDs + timestamps |
neegde.playlists.v2 |
{ playlists: PlaylistSnapshot[] } |
User playlists (name + trackIds) |
neegde.player.repeatMode |
"off" \| "all" \| "one" |
Repeat mode |
neegde.player.shuffle |
"0" \| "1" |
Shuffle on/off |
torrent_file_b64_v1_<topicId> |
base64 string | Cached .torrent file per topic |
trackCache.ts — TrackData store¶
The trackCache is the authoritative on-disk store for all TrackData. Every other persistence key (likes, playlists, queue) stores only IDs; data is resolved through this cache.
class TrackCache {
private readonly _data: Map<string, TrackData> = reactive(new Map());
put(t: Track | TrackData): void { ... } // idempotent, debounced write
putMany(ts: Array<Track | TrackData>): void { ... }
get(id: string): TrackData | undefined { ... }
hydrateTrack(id: string): Track | null { ... } // buildTrack(data)
}
Write path: put() compares the new data against the cached version with a shallow equality check. If nothing changed, the write is skipped. Saves are debounced at 250 ms — a burst of registerEntities calls from search results triggers one localStorage write, not N.
Read path: hydrateTrack(id) reads from the in-memory Map (loaded from localStorage on startup) and calls buildTrack. This is used by the queue and likes to restore Track instances on app start.
Eviction: no eviction. Track JSON is ~500 B. The cache is bounded by human-scale usage (liked tracks + playlist entries + recent queue). Cache growth is not a practical problem.
Cover URL merging: mergePersistedTrackFields(td) in trackCache.ts copies coverUrl and albumTitle from the persisted record into a newly registered entity. This prevents catalog enrichment (cover fetched in a previous session) from vanishing when the user runs a new search and the same track is re-emitted with coverUrl: null.
queue.ts — queue snapshot¶
export interface QueueSnapshot {
trackIds: string[];
pos: number;
}
const QUEUE_STORAGE_KEY = "neegde.queue.v2";
saveQueueSnapshot writes on every queue mutation. loadQueueSnapshot is called on app start by PlaybackQueue.seedFromSnapshot. The pos value is bounds-clamped to [0, trackIds.length - 1] on load.
likes.ts — liked IDs¶
export interface LikesSnapshot {
trackIds: string[];
albumIds: string[];
likedAt: Record<string, number>; // id → epoch ms
}
const LIKES_STORAGE_KEY = "neegde.likes.v2";
likedAt stores the like timestamp in milliseconds since epoch. likedTracks and likedAlbums computeds sort by this timestamp (most recent first).
The likes store does not write track/album data here — only IDs. On app start, bootstrap.ts loads trackCache + albumCache, hydrates entities, then loads LikesSnapshot. The sequence matters: entities must be in the registry before likedTracks computed runs.
Bootstrap sequence¶
src/persistence/bootstrap.ts runs once on app start:
// 1. Load caches from localStorage
const trackCacheData = loadTrackCache(); // Map<id, TrackData>
const albumCacheData = loadAlbumCache(); // Map<id, AlbumData>
// 2. Hydrate all cached tracks/albums into the entity registry
registerEntities([...Object.values(trackCacheData), ...Object.values(albumCacheData)]);
// 3. Load likes snapshot → IDs resolve through the registry
const likes = loadLikesSnapshot();
setLikesFromSnapshot(likes);
// 4. Load queue snapshot → IDs resolve through the registry
const queue = loadQueueSnapshot();
playbackQueue.seedFromSnapshot(queue);
If a liked track's data is not in trackCache (user cleared cache, or it was never stored), hydrateTrack(id) returns null and the track is silently skipped in the likedTracks computed. The ID is still in likedAt, so if the track is later re-found via search and registered, it reappears in the Liked view.
generalList.ts — debug registry¶
general-list.json is a Rust-side debug file that records every TrackData the app has ever registered. It is not a user feature — it exists for debugging session history.
The frontend calls recordGeneralList(trackData, event) in registerEntity. This invokes app_debug_push with category "general_list" and the track data as detail. On the Rust side, general_list_write appends to general-list.json.
The file can grow large on heavy-use installs. factory_reset (Settings → Reset app data) deletes it along with the other data files.