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
// ===================================================================
/** Last observed changeGeneration from the store. */
private lastChangeGen = libraryStore.changeGeneration;
hostConnected(): void {
this.lastChangeGen = libraryStore.changeGeneration;
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 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() {
EventsOn(Events.LibraryScanComplete, () => {
this.invalidate();
@@ -105,6 +112,7 @@ class LibraryStore {
try {
const tracks = await GetAllTracks();
this.tracks = tracks;
this.changeGen++;
return tracks;
} finally {
@@ -128,6 +136,7 @@ class LibraryStore {
try {
const albums = await GetAllAlbums();
this.albums = albums;
this.changeGen++;
return albums;
} finally {
@@ -151,6 +160,7 @@ class LibraryStore {
try {
const artists = await GetAllArtists();
this.artists = artists;
this.changeGen++;
return artists;
} finally {
@@ -174,6 +184,7 @@ class LibraryStore {
try {
const genres = await GetAllGenresWithCounts();
this.genres = genres;
this.changeGen++;
return genres;
} finally {
@@ -243,6 +254,17 @@ class LibraryStore {
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
// ===================================================================
@@ -271,6 +293,7 @@ class LibraryStore {
if (clamped === this.coverSizeValue) return;
this.coverSizeValue = clamped;
this.changeGen++;
this.saveCoverSize();
this.notify();
}
@@ -314,6 +337,7 @@ class LibraryStore {
this.albums = null;
this.artists = null;
this.genres = null;
this.changeGen++;
this.scrollPositions = { tracks: 0, albums: 0, artists: 0, genres: 0 };
this.notify();
this.eagerFetch();
+9 -1
View File
@@ -52,6 +52,7 @@ class QueueStore {
};
private subscribers = new Set<Subscriber>();
private notifyScheduled = false;
constructor() {
this.initializeEventListeners();
@@ -268,7 +269,14 @@ class QueueStore {
}
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();
}
});
}
}