perf(frontend): patch the stores instead of invalidating them
An event carries what a consumer needs so it never has to invalidate. - `library-store` answers `TrackPlayCountChanged` by patching one track, replacing the tracks array (consumers key memoized caches on its identity) while sharing every unchanged Track — instead of discarding four collections and refetching 25 MB per song. - `playlist-store` answers `PlaylistTracksChanged` by refetching the one playlist the event names, plus the summaries, since `UpdatedAt` is a sort key. 2 668 kB and 172 ms for one heart, against 2.0 kB. It falls back to a full invalidate only where a patch cannot be shown to be equivalent: no id, a cold cache, an unknown id, or a fetch already in flight. And a store with no subscriber fetches nothing — the singleton's constructor used to put every track of every playlist on the path to first paint for a view the user might never open. - `library-store` guards every fetch with a cache generation and holds the request itself instead of deriving a promise from subscriber notifications, which fixes the library-filter race and the never-settling waiter together: they are the same bug seen from either end. - `explore-cache`'s two art caches are bounded, sharing one exported cap constant — the artist photo's data URL is held by both, so capping either alone frees nothing at all and reads as a fix that did not work. - `search-store` deliberately does *not* coalesce its notify: deferring makes a subscriber that unsubscribes synchronously after a `setTerm` miss the notification entirely, which is a semantic change rather than an optimisation, and this is the store on the keystroke path. - `selection-controller` retains its keys across a refetch rather than clearing them, since they are file paths and those survive one, and `getSelectedKeysOrdered()` gains an early exit. It stays a walk of the list: an index goes stale on any re-sort, re-filter or refetch while a file path survives all three, and 3 ms does not buy a silently mis-ordered queue insert.
This commit is contained in:
@@ -47,6 +47,16 @@ interface TracksModified {
|
||||
|
||||
type Subscriber = () => void;
|
||||
|
||||
/**
|
||||
* Queue bindings are fire-and-forget by design: what happened is
|
||||
* reported by events (QueueChanged, PlaybackFailed), not by a return
|
||||
* value. A rejected bridge call still needs somewhere to land other
|
||||
* than an unhandled rejection (errors.m1).
|
||||
*/
|
||||
function reportBindingFailure(name: string): (err: unknown) => void {
|
||||
return (err: unknown) => console.error(`${name} failed`, err);
|
||||
}
|
||||
|
||||
class QueueStore {
|
||||
private state: QueueState = {
|
||||
tracks: [],
|
||||
@@ -190,15 +200,21 @@ class QueueStore {
|
||||
// ===================================================================
|
||||
|
||||
play(): void {
|
||||
Queue.Play();
|
||||
void Queue.Play().catch(
|
||||
reportBindingFailure('Queue.Play'),
|
||||
);
|
||||
}
|
||||
|
||||
next(): void {
|
||||
Queue.Next();
|
||||
void Queue.Next().catch(
|
||||
reportBindingFailure('Queue.Next'),
|
||||
);
|
||||
}
|
||||
|
||||
previous(): void {
|
||||
Queue.Previous();
|
||||
void Queue.Previous().catch(
|
||||
reportBindingFailure('Queue.Previous'),
|
||||
);
|
||||
}
|
||||
|
||||
setQueue(
|
||||
@@ -206,61 +222,87 @@ class QueueStore {
|
||||
startIndex: number,
|
||||
shuffleStart = false,
|
||||
): void {
|
||||
Queue.SetQueue(filePaths, startIndex, shuffleStart);
|
||||
void Queue.SetQueue(filePaths, startIndex, shuffleStart).catch(
|
||||
reportBindingFailure('Queue.SetQueue'),
|
||||
);
|
||||
}
|
||||
|
||||
addToQueue(filePath: string): void {
|
||||
Queue.AddTrack(filePath);
|
||||
void Queue.AddTrack(filePath).catch(
|
||||
reportBindingFailure('Queue.AddTrack'),
|
||||
);
|
||||
}
|
||||
|
||||
playNext(filePath: string): void {
|
||||
Queue.InsertNext(filePath);
|
||||
void Queue.InsertNext(filePath).catch(
|
||||
reportBindingFailure('Queue.InsertNext'),
|
||||
);
|
||||
}
|
||||
|
||||
removeFromQueue(position: number): void {
|
||||
Queue.RemoveTrack(position);
|
||||
void Queue.RemoveTrack(position).catch(
|
||||
reportBindingFailure('Queue.RemoveTrack'),
|
||||
);
|
||||
}
|
||||
|
||||
removeTracksFromQueue(positions: number[]): void {
|
||||
Queue.RemoveTracks(positions);
|
||||
void Queue.RemoveTracks(positions).catch(
|
||||
reportBindingFailure('Queue.RemoveTracks'),
|
||||
);
|
||||
}
|
||||
|
||||
addTracksToQueue(filePaths: string[]): void {
|
||||
Queue.AddTracks(filePaths);
|
||||
void Queue.AddTracks(filePaths).catch(
|
||||
reportBindingFailure('Queue.AddTracks'),
|
||||
);
|
||||
}
|
||||
|
||||
playTracksNext(filePaths: string[]): void {
|
||||
Queue.InsertNextTracks(filePaths);
|
||||
void Queue.InsertNextTracks(filePaths).catch(
|
||||
reportBindingFailure('Queue.InsertNextTracks'),
|
||||
);
|
||||
}
|
||||
|
||||
toggleShuffle(): void {
|
||||
Queue.ToggleShuffle();
|
||||
void Queue.ToggleShuffle().catch(
|
||||
reportBindingFailure('Queue.ToggleShuffle'),
|
||||
);
|
||||
}
|
||||
|
||||
cycleRepeat(): void {
|
||||
Queue.CycleRepeat();
|
||||
void Queue.CycleRepeat().catch(
|
||||
reportBindingFailure('Queue.CycleRepeat'),
|
||||
);
|
||||
}
|
||||
|
||||
playAtIndex(index: number): void {
|
||||
Queue.PlayIndex(index);
|
||||
void Queue.PlayIndex(index).catch(
|
||||
reportBindingFailure('Queue.PlayIndex'),
|
||||
);
|
||||
}
|
||||
|
||||
insertTracksAtIndex(
|
||||
filePaths: string[],
|
||||
index: number,
|
||||
): void {
|
||||
Queue.InsertTracksAt(filePaths, index);
|
||||
void Queue.InsertTracksAt(filePaths, index).catch(
|
||||
reportBindingFailure('Queue.InsertTracksAt'),
|
||||
);
|
||||
}
|
||||
|
||||
moveTracksInQueue(
|
||||
fromIndices: number[],
|
||||
toIndex: number,
|
||||
): void {
|
||||
Queue.MoveQueueTracks(fromIndices, toIndex);
|
||||
void Queue.MoveQueueTracks(fromIndices, toIndex).catch(
|
||||
reportBindingFailure('Queue.MoveQueueTracks'),
|
||||
);
|
||||
}
|
||||
|
||||
clearQueue(): void {
|
||||
Queue.Clear();
|
||||
void Queue.Clear().catch(
|
||||
reportBindingFailure('Queue.Clear'),
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
Reference in New Issue
Block a user