perf(14-03): add notification batching to queue store and granular change tracking to library store

- Queue store now uses queueMicrotask batching (matching library store pattern)
- Library store adds changeGeneration counter incremented only on actual data changes
- LibraryController checks changeGeneration before requestUpdate, skipping loading-only transitions
- Reduces unnecessary component re-renders during data loading cycles
This commit is contained in:
2026-03-14 13:53:59 -04:00
parent 1ec8f82ef8
commit d0c05dc1d4
3 changed files with 46 additions and 2 deletions
@@ -29,9 +29,21 @@ export class LibraryController implements ReactiveController {
// LIFECYCLE HOOKS // LIFECYCLE HOOKS
// =================================================================== // ===================================================================
/** Last observed changeGeneration from the store. */
private lastChangeGen = libraryStore.changeGeneration;
hostConnected(): void { hostConnected(): void {
this.lastChangeGen = libraryStore.changeGeneration;
this.unsubscribe = libraryStore.subscribe(() => { this.unsubscribe = libraryStore.subscribe(() => {
this.host.requestUpdate(); const gen = libraryStore.changeGeneration;
// Skip requestUpdate when only loading flags toggled
// (changeGeneration unchanged means no actual data changed).
if (gen !== this.lastChangeGen) {
this.lastChangeGen = gen;
this.host.requestUpdate();
}
}); });
} }
+24
View File
@@ -48,6 +48,13 @@ class LibraryStore {
private subscribers = new Set<Subscriber>(); private subscribers = new Set<Subscriber>();
private notifyScheduled = false; private notifyScheduled = false;
/**
* Monotonic counter incremented only when actual data changes
* (not loading flag transitions). Subscribers can compare against
* a saved value to skip requestUpdate when only loading state toggled.
*/
private changeGen = 0;
constructor() { constructor() {
EventsOn(Events.LibraryScanComplete, () => { EventsOn(Events.LibraryScanComplete, () => {
this.invalidate(); this.invalidate();
@@ -105,6 +112,7 @@ class LibraryStore {
try { try {
const tracks = await GetAllTracks(); const tracks = await GetAllTracks();
this.tracks = tracks; this.tracks = tracks;
this.changeGen++;
return tracks; return tracks;
} finally { } finally {
@@ -128,6 +136,7 @@ class LibraryStore {
try { try {
const albums = await GetAllAlbums(); const albums = await GetAllAlbums();
this.albums = albums; this.albums = albums;
this.changeGen++;
return albums; return albums;
} finally { } finally {
@@ -151,6 +160,7 @@ class LibraryStore {
try { try {
const artists = await GetAllArtists(); const artists = await GetAllArtists();
this.artists = artists; this.artists = artists;
this.changeGen++;
return artists; return artists;
} finally { } finally {
@@ -174,6 +184,7 @@ class LibraryStore {
try { try {
const genres = await GetAllGenresWithCounts(); const genres = await GetAllGenresWithCounts();
this.genres = genres; this.genres = genres;
this.changeGen++;
return genres; return genres;
} finally { } finally {
@@ -243,6 +254,17 @@ class LibraryStore {
return this.genresLoading; return this.genresLoading;
} }
/**
* Monotonic counter that increments only when cached data
* actually changes (tracks, albums, artists, genres, coverSize,
* or invalidation). Loading flag transitions do NOT increment.
* Controllers can compare against a saved value to skip
* unnecessary requestUpdate() calls.
*/
get changeGeneration(): number {
return this.changeGen;
}
// =================================================================== // ===================================================================
// SCROLL POSITION // SCROLL POSITION
// =================================================================== // ===================================================================
@@ -271,6 +293,7 @@ class LibraryStore {
if (clamped === this.coverSizeValue) return; if (clamped === this.coverSizeValue) return;
this.coverSizeValue = clamped; this.coverSizeValue = clamped;
this.changeGen++;
this.saveCoverSize(); this.saveCoverSize();
this.notify(); this.notify();
} }
@@ -314,6 +337,7 @@ class LibraryStore {
this.albums = null; this.albums = null;
this.artists = null; this.artists = null;
this.genres = null; this.genres = null;
this.changeGen++;
this.scrollPositions = { tracks: 0, albums: 0, artists: 0, genres: 0 }; this.scrollPositions = { tracks: 0, albums: 0, artists: 0, genres: 0 };
this.notify(); this.notify();
this.eagerFetch(); this.eagerFetch();
+9 -1
View File
@@ -52,6 +52,7 @@ class QueueStore {
}; };
private subscribers = new Set<Subscriber>(); private subscribers = new Set<Subscriber>();
private notifyScheduled = false;
constructor() { constructor() {
this.initializeEventListeners(); this.initializeEventListeners();
@@ -268,7 +269,14 @@ class QueueStore {
} }
private notify(): void { private notify(): void {
this.subscribers.forEach((callback) => callback()); if (this.notifyScheduled) return;
this.notifyScheduled = true;
queueMicrotask(() => {
this.notifyScheduled = false;
for (const sub of this.subscribers) {
sub();
}
});
} }
} }