perf(library): patch what changed instead of reloading the list
A retag named the one file it rewrote and the store threw the whole list away anyway: 20.5 MB across the IPC at 26 138 tracks to pick up one new title. A soft rescan of an unchanged library did the same, though its metrics say zero added, zero updated and zero removed. - `TrackMetadataChanged` with a `filePath` re-reads that one row through `trackCache` and splices it in, replacing the array so the memoized filter and sort caches notice. The summaries are still refetched: a retag can move a track between albums. A failed patch falls back to the full reload, because the answer that cannot be wrong is the one to keep. - A batch write names no paths (it can be thousands) and still reloads. - `LibraryScanComplete` reads its own metrics and reloads only when the scan changed something; a payload of an unknown shape is treated as a change, so nothing can leave a stale list on screen. Closes #282
This commit is contained in:
1 parent
bce8d27370
commit
5b236ca5af
2 files changed
+205
-5
No files matched your search
@@ -21,6 +21,12 @@ import {
|
||||
} from '@test/support/harness';
|
||||
import { trackTable } from '@test/support/track-table';
|
||||
|
||||
const RETAGGED = {
|
||||
TrackName: 'Retagged',
|
||||
FilePath: '/a.mp3',
|
||||
PlayCount: 0,
|
||||
};
|
||||
|
||||
const TRACKS = [
|
||||
{ TrackName: 'One', FilePath: '/a.mp3', PlayCount: 0 },
|
||||
{ TrackName: 'Two', FilePath: '/b.mp3', PlayCount: 4 },
|
||||
@@ -112,10 +118,93 @@ describe('library store: caching', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('refetches when a track is retagged', async () => {
|
||||
/*
|
||||
* #282: a retag names the one file it rewrote, so the whole list does
|
||||
* not have to come back — 20.5 MB at 26 138 tracks to pick up one new
|
||||
* title. The summaries are still refetched, because a retag can move
|
||||
* a track between albums.
|
||||
*/
|
||||
it('re-reads the one file a retag names, not the whole list', async () => {
|
||||
stub('library.Library.GetTracksByPaths', [RETAGGED]);
|
||||
|
||||
emit(Events.TrackMetadataChanged, { filePath: '/a.mp3' });
|
||||
await flush();
|
||||
|
||||
expect(calls('library.Library.GetTrackTable')).toHaveLength(0);
|
||||
expect(lastArgs('library.Library.GetTracksByPaths')).toEqual([['/a.mp3']]);
|
||||
expect(calls().map((c) => c.path).sort()).toEqual([
|
||||
'library.Library.GetAlbums',
|
||||
'library.Library.GetArtists',
|
||||
'library.Library.GetGenres',
|
||||
'library.Library.GetTracksByPaths',
|
||||
]);
|
||||
});
|
||||
|
||||
it('splices the re-read row in, so consumers notice', async () => {
|
||||
stub('library.Library.GetTracksByPaths', [RETAGGED]);
|
||||
|
||||
const before = libraryStore.getCachedTracks();
|
||||
|
||||
emit(Events.TrackMetadataChanged, { filePath: '/a.mp3' });
|
||||
await flush();
|
||||
|
||||
const after = libraryStore.getCachedTracks();
|
||||
|
||||
expect(after?.map((t) => t.TrackName)).toEqual(['Retagged', 'Two']);
|
||||
expect(after, 'a new array, which is what memoized consumers key on').not.toBe(
|
||||
before,
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to a full reload when the patch cannot be read', async () => {
|
||||
stubFailure('library.Library.GetTracksByPaths', 'sql: database is locked');
|
||||
|
||||
emit(Events.TrackMetadataChanged, { filePath: '/a.mp3' });
|
||||
await flush();
|
||||
|
||||
expect(calls('library.Library.GetTrackTable')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('reloads everything for a batch write, which names no paths', async () => {
|
||||
emit(Events.TrackMetadataChanged, { batch: true, total: 40 });
|
||||
await flush();
|
||||
|
||||
expect(calls('library.Library.GetTrackTable')).toHaveLength(1);
|
||||
expect(calls('library.Library.GetTracksByPaths')).toHaveLength(0);
|
||||
});
|
||||
|
||||
/*
|
||||
* #282, the other half: a soft rescan of an unchanged library reports
|
||||
* zero added, updated and removed, and every loaded collection was
|
||||
* thrown away and refetched anyway.
|
||||
*/
|
||||
describe('a scan that changed nothing', () => {
|
||||
beforeEach(async () => {
|
||||
emit(Events.LibraryScanComplete, {
|
||||
added: 0,
|
||||
updated: 0,
|
||||
removed: 0,
|
||||
skipped: 31,
|
||||
});
|
||||
await flush();
|
||||
});
|
||||
|
||||
it('refetches nothing', () => {
|
||||
expect(calls()).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps the data it already had', () => {
|
||||
expect(libraryStore.getCachedTracks()?.map((t) => t.FilePath)).toEqual([
|
||||
'/a.mp3',
|
||||
'/b.mp3',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('reloads when a scan reports a change', async () => {
|
||||
emit(Events.LibraryScanComplete, { added: 1, updated: 0, removed: 0 });
|
||||
await flush();
|
||||
|
||||
expect(calls('library.Library.GetTrackTable')).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user