fix(library): open track details by path, not from the whole library

Track details from the queue, a playlist or a smart playlist looked
the track up in libraryStore's whole-library array, and returned
silently when it had not loaded, so the menu item did nothing. That
dependency is also why every track was fetched eagerly at startup
(20.5 MB at 26k tracks).

GetTracksByPaths answers for the paths in hand, and trackCache holds
the answers: bounded, coalesced into one call per task, and forgetting
what the retag, removal and scan events name. Every opener, and
track-details' own refresh after a save, go through it. A source sweep
pins the whole-library array to the store and the Tracks view.

Closes #279
This commit is contained in:
yonlu committed 2026-10-05 23:01:37 -04:00
1 parent 3cddf70c4d
commit 4a9fe1d207
14 files changed
+747 -270

No files matched your search

@@ -28,7 +28,7 @@ import type { TrackDetails } from '@components/track-details/track-details';
const ALBUM_TRACKS = 'library.Library.GetAlbumTracks';
const COMPLETENESS = 'library.Library.GetAlbumCompleteness';
const FILE_PATHS = 'library.Library.GetFilePathsByRecordingMBIDs';
const ALL_TRACKS = 'library.Library.GetTracks';
const TRACKS_BY_PATH = 'library.Library.GetTracksByPaths';
const LOOKUP_RG = 'explore.Service.LookupReleaseGroup';
const BROWSE_RELEASES = 'explore.Service.BrowseReleases';
@@ -127,14 +127,13 @@ describe('Explore track details', () => {
stub(ALBUM_TRACKS, [albumTrack]);
stub(COMPLETENESS, { known: true, complete: true, owned: 1, expected: 1 });
stub(FILE_PATHS, { [MBID]: [PATH] });
stub(ALL_TRACKS, [libraryTrack]);
stub(TRACKS_BY_PATH, [libraryTrack]);
});
/**
* `libraryStore` fetches at import and caches the empty list the
* shared setup stubs, for the life of the browser session — so a test
* that wants tracks in it has to say so. A scan-complete event is how
* the app itself invalidates that cache.
* `trackCache` keeps what it was answered for the life of the browser
* session, so a test that changes the answer has to drop it. A
* scan-complete event is how the app itself invalidates that cache.
*/
async function primeLibrary() {
emit(Events.LibraryScanComplete);
@@ -204,7 +203,7 @@ describe('Explore track details', () => {
items[details]!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
// Polled rather than counted: the opener is three awaits deep — the
// path lookup, the store's tracks, and the dynamic `import()` of
// path lookup, the track by path, and the dynamic `import()` of
// the dialog chunk — and a chunk fetch is the one of the three
// whose cost depends on what else the suite is doing.
const shown = await dialogTrack(el);
@@ -230,7 +229,7 @@ describe('Explore track details', () => {
});
it('reports a path with no library track rather than opening empty', async () => {
stub(ALL_TRACKS, []);
stub(TRACKS_BY_PATH, []);
await primeLibrary();
const outcome = await showTrackDetailsForPath(