diff --git a/src/renderer/features/shared/mutations/favorite-optimistic-updates.ts b/src/renderer/features/shared/mutations/favorite-optimistic-updates.ts index 05a48c556..8f4e031b1 100644 --- a/src/renderer/features/shared/mutations/favorite-optimistic-updates.ts +++ b/src/renderer/features/shared/mutations/favorite-optimistic-updates.ts @@ -23,10 +23,532 @@ export interface PreviousQueryData { queryKey: readonly unknown[]; } +interface PendingUpdate { + previousData: unknown; + queryKey: readonly unknown[]; + updater: (prev: any) => any; +} + +function collectAndApplyUpdates( + queryClient: QueryClient, + pendingUpdates: PendingUpdate[], +): PreviousQueryData[] { + const previousQueries: PreviousQueryData[] = []; + + pendingUpdates.forEach(({ previousData, queryKey, updater }) => { + previousQueries.push({ data: previousData, queryKey }); + queryClient.setQueryData(queryKey, updater); + }); + + return previousQueries; +} + +function updateItemInArray( + items: T[], + itemIdSet: Set, + updater: (item: T) => T, +): null | T[] { + let hasChanges = false; + const updatedItems = items.map((item) => { + if (itemIdSet.has(item.id)) { + hasChanges = true; + return updater(item); + } + return item; + }); + + return hasChanges ? updatedItems : null; +} + +function updateItemsInPages( + pages: P[], + itemIdSet: Set, + updater: (item: T) => T, +): null | P[] { + let hasChanges = false; + const updatedPages = pages.map((page) => { + if (!page) return page; + const updatedItems = updateItemInArray(page.items, itemIdSet, updater); + if (updatedItems) { + hasChanges = true; + return { ...page, items: updatedItems }; + } + return page; + }); + + return hasChanges ? updatedPages : null; +} + export const applyFavoriteOptimisticUpdates = ( queryClient: QueryClient, variables: FavoriteArgs, isFavorite: boolean, +): PreviousQueryData[] => { + const pendingUpdates: PendingUpdate[] = []; + const itemIdSet = new Set(); + + if (Array.isArray(variables.query.id)) { + variables.query.id.forEach((id) => { + itemIdSet.add(id); + }); + } else { + itemIdSet.add(variables.query.id); + } + + const createFavoriteUpdater = (item: T): T => ({ + ...item, + userFavorite: isFavorite, + }); + + switch (variables.query.type) { + case LibraryItem.ALBUM: { + const detailQueryKey = queryKeys.albums.detail(variables.apiClientProps.serverId); + const detailQueries = queryClient.getQueriesData({ + exact: false, + queryKey: detailQueryKey, + }); + + detailQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: AlbumDetailResponse | undefined) => { + if (prev && itemIdSet.has(prev.id)) { + return { ...prev, userFavorite: isFavorite }; + } + return prev; + }, + }); + } + }); + + const listQueryKey = queryKeys.albums.list(variables.apiClientProps.serverId); + const listQueries = queryClient.getQueriesData({ + exact: false, + queryKey: listQueryKey, + }); + + listQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (current: AlbumListResponse | undefined) => { + if (!current) return current; + const updatedItems = updateItemInArray( + current.items, + itemIdSet, + (item) => createFavoriteUpdater(item), + ); + return updatedItems ? { ...current, items: updatedItems } : current; + }, + }); + } + }); + + const infiniteListQueryKey = queryKeys.albums.infiniteList( + variables.apiClientProps.serverId, + ); + const infiniteListQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infiniteListQueryKey, + }); + + infiniteListQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: ( + current: + | undefined + | { pageParams: string[]; pages: AlbumListResponse[] }, + ) => { + if (!current) return current; + const updatedPages = updateItemsInPages( + current.pages.filter((p): p is AlbumListResponse => !!p), + itemIdSet, + (item) => createFavoriteUpdater(item), + ); + return updatedPages ? { ...current, pages: updatedPages } : current; + }, + }); + } + }); + + const infiniteLoaderQueryKey = infiniteLoaderDataQueryKey( + variables.apiClientProps.serverId, + LibraryItem.ALBUM, + ); + const infiniteLoaderQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infiniteLoaderQueryKey, + }); + + infiniteLoaderQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: ( + prev: + | undefined + | { + data: unknown[]; + pagesLoaded: Record; + }, + ) => { + if (prev && prev.data) { + const updatedData = updateItemInArray( + prev.data as Array<{ id: string; userFavorite?: boolean }>, + itemIdSet, + (item) => createFavoriteUpdater(item), + ); + return updatedData ? { ...prev, data: updatedData } : prev; + } + return prev; + }, + }); + } + }); + + break; + } + case LibraryItem.ALBUM_ARTIST: { + const detailQueryKey = queryKeys.albumArtists.detail(variables.apiClientProps.serverId); + const detailQueries = queryClient.getQueriesData({ + exact: false, + queryKey: detailQueryKey, + }); + + detailQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: AlbumArtistDetailResponse | undefined) => { + if (!prev) return prev; + + if (itemIdSet.has(prev.id)) { + return { ...prev, userFavorite: isFavorite }; + } + + if (prev.similarArtists && prev.similarArtists.length > 0) { + const hasMatchingSimilarArtist = prev.similarArtists.some( + (artist) => itemIdSet.has(artist.id), + ); + + if (hasMatchingSimilarArtist) { + return { + ...prev, + similarArtists: prev.similarArtists.map((artist) => + itemIdSet.has(artist.id) + ? { ...artist, userFavorite: isFavorite } + : artist, + ), + }; + } + } + + return prev; + }, + }); + } + }); + + const listQueryKey = queryKeys.albumArtists.list(variables.apiClientProps.serverId); + const listQueries = queryClient.getQueriesData({ + exact: false, + queryKey: listQueryKey, + }); + + listQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: AlbumArtistListResponse | undefined) => { + if (!prev) return prev; + const updatedItems = updateItemInArray( + prev.items.filter((item): item is AlbumArtist => !!item), + itemIdSet, + (item) => createFavoriteUpdater(item), + ); + return updatedItems ? { ...prev, items: updatedItems } : prev; + }, + }); + } + }); + + const infiniteListQueryKey = queryKeys.albumArtists.infiniteList( + variables.apiClientProps.serverId, + ); + const infiniteListQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infiniteListQueryKey, + }); + + infiniteListQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: ( + prev: + | undefined + | { pageParams: string[]; pages: AlbumArtistListResponse[] }, + ) => { + if (!prev) return prev; + const updatedPages = updateItemsInPages< + AlbumArtist, + AlbumArtistListResponse + >( + prev.pages.filter((p): p is AlbumArtistListResponse => !!p), + itemIdSet, + (item) => createFavoriteUpdater(item), + ); + return updatedPages ? { ...prev, pages: updatedPages } : prev; + }, + }); + } + }); + + const infiniteLoaderQueryKey = infiniteLoaderDataQueryKey( + variables.apiClientProps.serverId, + LibraryItem.ALBUM_ARTIST, + ); + const infiniteLoaderQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infiniteLoaderQueryKey, + }); + + infiniteLoaderQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: ( + prev: + | undefined + | { + data: unknown[]; + pagesLoaded: Record; + }, + ) => { + if (prev && prev.data) { + const updatedData = updateItemInArray( + prev.data as Array<{ id: string; userFavorite?: boolean }>, + itemIdSet, + (item) => createFavoriteUpdater(item), + ); + return updatedData ? { ...prev, data: updatedData } : prev; + } + return prev; + }, + }); + } + }); + + break; + } + case LibraryItem.ARTIST: { + const listQueryKey = queryKeys.artists.list(variables.apiClientProps.serverId); + const listQueries = queryClient.getQueriesData({ + exact: false, + queryKey: listQueryKey, + }); + + listQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: ArtistListResponse | undefined) => { + if (!prev) return prev; + const updatedItems = updateItemInArray(prev.items, itemIdSet, (item) => + createFavoriteUpdater(item), + ); + return updatedItems ? { ...prev, items: updatedItems } : prev; + }, + }); + } + }); + + const infiniteListQueryKey = queryKeys.artists.infiniteList( + variables.apiClientProps.serverId, + ); + const infiniteListQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infiniteListQueryKey, + }); + + infiniteListQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: ( + prev: undefined | { pageParams: string[]; pages: ArtistListResponse[] }, + ) => { + if (!prev) return prev; + const updatedPages = updateItemsInPages< + AlbumArtist, + AlbumArtistListResponse + >( + prev.pages.filter((p): p is AlbumArtistListResponse => !!p), + itemIdSet, + (item) => createFavoriteUpdater(item), + ); + return updatedPages ? { ...prev, pages: updatedPages } : prev; + }, + }); + } + }); + + const infiniteLoaderQueryKey = infiniteLoaderDataQueryKey( + variables.apiClientProps.serverId, + LibraryItem.ARTIST, + ); + const infiniteLoaderQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infiniteLoaderQueryKey, + }); + + infiniteLoaderQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: ( + prev: + | undefined + | { + data: unknown[]; + pagesLoaded: Record; + }, + ) => { + if (prev && prev.data) { + const updatedData = updateItemInArray( + prev.data as Array<{ id: string; userFavorite?: boolean }>, + itemIdSet, + (item) => createFavoriteUpdater(item), + ); + return updatedData ? { ...prev, data: updatedData } : prev; + } + return prev; + }, + }); + } + }); + + break; + } + case LibraryItem.PLAYLIST_SONG: + case LibraryItem.QUEUE_SONG: + case LibraryItem.SONG: { + const albumDetailQueryKey = queryKeys.albums.detail(variables.apiClientProps.serverId); + const albumDetailQueries = queryClient.getQueriesData({ + exact: false, + queryKey: albumDetailQueryKey, + }); + + albumDetailQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: AlbumDetailResponse | undefined) => { + if (!prev || !prev.songs) return prev; + const updatedSongs = updateItemInArray(prev.songs, itemIdSet, (item) => + createFavoriteUpdater(item), + ); + return updatedSongs ? { ...prev, songs: updatedSongs } : prev; + }, + }); + } + }); + + const detailQueryKey = queryKeys.songs.detail(variables.apiClientProps.serverId); + const detailQueries = queryClient.getQueriesData({ + exact: false, + queryKey: detailQueryKey, + }); + + detailQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: SongDetailResponse | undefined) => { + if (prev && itemIdSet.has(prev.id)) { + return { ...prev, userFavorite: isFavorite }; + } + return prev; + }, + }); + } + }); + + const playlistSongListQueryKey = queryKeys.playlists.songList( + variables.apiClientProps.serverId, + ); + const playlistSongListQueries = queryClient.getQueriesData({ + exact: false, + queryKey: playlistSongListQueryKey, + }); + + playlistSongListQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: PlaylistSongListResponse | undefined) => { + if (!prev) return prev; + const updatedItems = updateItemInArray(prev.items, itemIdSet, (item) => + createFavoriteUpdater(item), + ); + return updatedItems ? { ...prev, items: updatedItems } : prev; + }, + }); + } + }); + + const topSongsQueryKey = queryKeys.albumArtists.topSongs( + variables.apiClientProps.serverId, + ); + const topSongsQueries = queryClient.getQueriesData({ + exact: false, + queryKey: topSongsQueryKey, + }); + + topSongsQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: TopSongListResponse | undefined) => { + if (!prev) return prev; + const updatedItems = updateItemInArray(prev.items, itemIdSet, (item) => + createFavoriteUpdater(item), + ); + return updatedItems ? { ...prev, items: updatedItems } : prev; + }, + }); + } + }); + + break; + } + } + + return collectAndApplyUpdates(queryClient, pendingUpdates); +}; + +export const applyFavoriteOptimisticUpdatesDeferred = ( + queryClient: QueryClient, + variables: FavoriteArgs, + isFavorite: boolean, ): PreviousQueryData[] => { const previousQueries: PreviousQueryData[] = []; const itemIdSet = new Set(); @@ -39,618 +561,172 @@ export const applyFavoriteOptimisticUpdates = ( itemIdSet.add(variables.query.id); } + const queryKeysToUpdate: Array<{ + data: unknown; + queryKey: readonly unknown[]; + type: string; + }> = []; + + const collectQueries = (baseKey: readonly unknown[], type: string) => { + const queries = queryClient.getQueriesData({ exact: false, queryKey: baseKey }); + queries.forEach(([queryKey, data]) => { + if (data) { + previousQueries.push({ data, queryKey }); + queryKeysToUpdate.push({ data, queryKey, type }); + } + }); + }; + switch (variables.query.type) { case LibraryItem.ALBUM: { - const detailQueryKey = queryKeys.albums.detail(variables.apiClientProps.serverId); - - const detailQueries = queryClient.getQueriesData({ - exact: false, - queryKey: detailQueryKey, - }); - - if (detailQueries.length) { - detailQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: AlbumDetailResponse | undefined) => { - if (prev && itemIdSet.has(prev.id)) { - return { - ...prev, - userFavorite: isFavorite, - }; - } - - return prev; - }, - ); - } - }); - } - - const listQueryKey = queryKeys.albums.list(variables.apiClientProps.serverId); - - const listQueries = queryClient.getQueriesData({ - exact: false, - queryKey: listQueryKey, - }); - - if (listQueries.length) { - listQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: AlbumListResponse | undefined) => { - if (prev) { - return { - ...prev, - items: prev.items.map((item: Album) => { - return itemIdSet.has(item.id) - ? { ...item, userFavorite: isFavorite } - : item; - }), - }; - } - - return prev; - }, - ); - } - }); - } - - const infiniteListQueryKey = queryKeys.albums.infiniteList( - variables.apiClientProps.serverId, + collectQueries( + queryKeys.albums.detail(variables.apiClientProps.serverId), + 'album-detail', ); - - const infiniteListQueries = queryClient.getQueriesData({ - exact: false, - queryKey: infiniteListQueryKey, - }); - - if (infiniteListQueries.length) { - infiniteListQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - ( - prev: - | undefined - | { pageParams: string[]; pages: AlbumListResponse[] }, - ) => { - if (prev) { - return { - ...prev, - pages: prev.pages.map( - (page: AlbumListResponse | undefined) => { - if (page) { - return { - ...page, - items: page.items.map((item: Album) => { - return itemIdSet.has(item.id) - ? { - ...item, - userFavorite: isFavorite, - } - : item; - }), - }; - } - - return page; - }, - ), - }; - } - - return prev; - }, - ); - } - }); - } - - const infiniteLoaderQueryKey = infiniteLoaderDataQueryKey( - variables.apiClientProps.serverId, - LibraryItem.ALBUM, + collectQueries(queryKeys.albums.list(variables.apiClientProps.serverId), 'album-list'); + collectQueries( + queryKeys.albums.infiniteList(variables.apiClientProps.serverId), + 'album-infinite-list', + ); + collectQueries( + infiniteLoaderDataQueryKey(variables.apiClientProps.serverId, LibraryItem.ALBUM), + 'album-infinite-loader', ); - - const infiniteLoaderQueries = queryClient.getQueriesData({ - exact: false, - queryKey: infiniteLoaderQueryKey, - }); - - if (infiniteLoaderQueries.length) { - infiniteLoaderQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - ( - prev: - | undefined - | { - data: unknown[]; - pagesLoaded: Record; - }, - ) => { - if (prev && prev.data) { - return { - ...prev, - data: prev.data.map((item: any) => { - if (!item || !item.id) { - return item; - } - - return itemIdSet.has(item.id) - ? { ...item, userFavorite: isFavorite } - : item; - }), - }; - } - - return prev; - }, - ); - } - }); - } - break; } case LibraryItem.ALBUM_ARTIST: { - const detailQueryKey = queryKeys.albumArtists.detail(variables.apiClientProps.serverId); - - const detailQueries = queryClient.getQueriesData({ - exact: false, - queryKey: detailQueryKey, - }); - - if (detailQueries.length) { - detailQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: AlbumArtistDetailResponse | undefined) => { - if (!prev) { - return prev; - } - - // Update the main artist if it matches - if (itemIdSet.has(prev.id)) { - return { - ...prev, - userFavorite: isFavorite, - }; - } - - // Update similar artists if any match - if (prev.similarArtists && prev.similarArtists.length > 0) { - const hasMatchingSimilarArtist = prev.similarArtists.some( - (artist) => itemIdSet.has(artist.id), - ); - - if (hasMatchingSimilarArtist) { - return { - ...prev, - similarArtists: prev.similarArtists.map((artist) => - itemIdSet.has(artist.id) - ? { ...artist, userFavorite: isFavorite } - : artist, - ), - }; - } - } - - return prev; - }, - ); - } - }); - } - const listQueryKey = queryKeys.albumArtists.list(variables.apiClientProps.serverId); - - const listQueries = queryClient.getQueriesData({ - exact: false, - queryKey: listQueryKey, - }); - - if (listQueries.length) { - listQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: AlbumArtistListResponse | undefined) => { - if (prev) { - return { - ...prev, - items: prev.items.map((item: AlbumArtist | undefined) => { - if (item) { - return itemIdSet.has(item.id) - ? { ...item, userFavorite: isFavorite } - : item; - } - - return item; - }), - }; - } - - return prev; - }, - ); - } - }); - } - - const infiniteListQueryKey = queryKeys.albumArtists.infiniteList( - variables.apiClientProps.serverId, + collectQueries( + queryKeys.albumArtists.detail(variables.apiClientProps.serverId), + 'album-artist-detail', ); - - const infiniteListQueries = queryClient.getQueriesData({ - exact: false, - queryKey: infiniteListQueryKey, - }); - - if (infiniteListQueries.length) { - infiniteListQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - ( - prev: - | undefined - | { pageParams: string[]; pages: AlbumArtistListResponse[] }, - ) => { - if (prev) { - return { - ...prev, - pages: prev.pages.map( - (page: AlbumArtistListResponse | undefined) => { - if (page) { - return { - ...page, - items: page.items.map( - (item: AlbumArtist) => { - return itemIdSet.has(item.id) - ? { - ...item, - userFavorite: isFavorite, - } - : item; - }, - ), - }; - } - - return page; - }, - ), - }; - } - - return prev; - }, - ); - } - }); - } - - const infiniteLoaderQueryKey = infiniteLoaderDataQueryKey( - variables.apiClientProps.serverId, - LibraryItem.ALBUM_ARTIST, + collectQueries( + queryKeys.albumArtists.list(variables.apiClientProps.serverId), + 'album-artist-list', + ); + collectQueries( + queryKeys.albumArtists.infiniteList(variables.apiClientProps.serverId), + 'album-artist-infinite-list', + ); + collectQueries( + infiniteLoaderDataQueryKey( + variables.apiClientProps.serverId, + LibraryItem.ALBUM_ARTIST, + ), + 'album-artist-infinite-loader', ); - - const infiniteLoaderQueries = queryClient.getQueriesData({ - exact: false, - queryKey: infiniteLoaderQueryKey, - }); - - if (infiniteLoaderQueries.length) { - infiniteLoaderQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - ( - prev: - | undefined - | { - data: unknown[]; - pagesLoaded: Record; - }, - ) => { - if (prev && prev.data) { - return { - ...prev, - data: prev.data.map((item: any) => { - if (!item || !item.id) { - return item; - } - - return itemIdSet.has(item.id) - ? { ...item, userFavorite: isFavorite } - : item; - }), - }; - } - - return prev; - }, - ); - } - }); - } - break; } case LibraryItem.ARTIST: { - const listQueryKey = queryKeys.artists.list(variables.apiClientProps.serverId); - - const listQueries = queryClient.getQueriesData({ - exact: false, - queryKey: listQueryKey, - }); - - if (listQueries.length) { - listQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: ArtistListResponse | undefined) => { - if (prev) { - return { - ...prev, - items: prev.items.map((item: AlbumArtist) => { - return itemIdSet.has(item.id) - ? { ...item, userFavorite: isFavorite } - : item; - }), - }; - } - - return prev; - }, - ); - } - }); - } - - const infiniteListQueryKey = queryKeys.artists.infiniteList( - variables.apiClientProps.serverId, + collectQueries( + queryKeys.artists.list(variables.apiClientProps.serverId), + 'artist-list', ); - - const infiniteListQueries = queryClient.getQueriesData({ - exact: false, - queryKey: infiniteListQueryKey, - }); - - if (infiniteListQueries.length) { - infiniteListQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - ( - prev: - | undefined - | { pageParams: string[]; pages: ArtistListResponse[] }, - ) => { - if (prev) { - return { - ...prev, - pages: prev.pages.map((page: ArtistListResponse) => { - return { - ...page, - items: page.items.map((item: AlbumArtist) => { - return itemIdSet.has(item.id) - ? { ...item, userFavorite: isFavorite } - : item; - }), - }; - }), - }; - } - - return prev; - }, - ); - } - }); - } - - const infiniteLoaderQueryKey = infiniteLoaderDataQueryKey( - variables.apiClientProps.serverId, - LibraryItem.ARTIST, + collectQueries( + queryKeys.artists.infiniteList(variables.apiClientProps.serverId), + 'artist-infinite-list', + ); + collectQueries( + infiniteLoaderDataQueryKey(variables.apiClientProps.serverId, LibraryItem.ARTIST), + 'artist-infinite-loader', ); - - const infiniteLoaderQueries = queryClient.getQueriesData({ - exact: false, - queryKey: infiniteLoaderQueryKey, - }); - - if (infiniteLoaderQueries.length) { - infiniteLoaderQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - ( - prev: - | undefined - | { - data: unknown[]; - pagesLoaded: Record; - }, - ) => { - if (prev && prev.data) { - return { - ...prev, - data: prev.data.map((item: any) => { - if (!item || !item.id) { - return item; - } - - return itemIdSet.has(item.id) - ? { ...item, userFavorite: isFavorite } - : item; - }), - }; - } - - return prev; - }, - ); - } - }); - } - break; } case LibraryItem.PLAYLIST_SONG: case LibraryItem.QUEUE_SONG: case LibraryItem.SONG: { - const albumDetailQueryKey = queryKeys.albums.detail(variables.apiClientProps.serverId); - - const albumDetailQueries = queryClient.getQueriesData({ - exact: false, - queryKey: albumDetailQueryKey, - }); - - if (albumDetailQueries.length) { - albumDetailQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: AlbumDetailResponse | undefined) => { - if (prev) { - return { - ...prev, - songs: prev.songs?.map((song: Song) => { - return itemIdSet.has(song.id) - ? { ...song, userFavorite: isFavorite } - : song; - }), - }; - } - - return prev; - }, - ); - } - }); - } - - const detailQueryKey = queryKeys.songs.detail(variables.apiClientProps.serverId); - - const detailQueries = queryClient.getQueriesData({ - exact: false, - queryKey: detailQueryKey, - }); - - if (detailQueries.length) { - detailQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: SongDetailResponse | undefined) => { - if (prev && itemIdSet.has(prev.id)) { - return { - ...prev, - userFavorite: isFavorite, - }; - } - - return prev; - }, - ); - } - }); - } - - const playlistSongListQueryKey = queryKeys.playlists.songList( - variables.apiClientProps.serverId, + collectQueries( + queryKeys.albums.detail(variables.apiClientProps.serverId), + 'album-detail', ); - - const playlistSongListQueries = queryClient.getQueriesData({ - exact: false, - queryKey: playlistSongListQueryKey, - }); - - if (playlistSongListQueries.length) { - playlistSongListQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: PlaylistSongListResponse | undefined) => { - if (prev) { - return { - ...prev, - items: prev.items.map((item: Song) => - itemIdSet.has(item.id) - ? { ...item, userFavorite: isFavorite } - : item, - ), - }; - } - - return prev; - }, - ); - } - }); - } - - const topSongsQueryKey = queryKeys.albumArtists.topSongs( - variables.apiClientProps.serverId, + collectQueries( + queryKeys.songs.detail(variables.apiClientProps.serverId), + 'song-detail', + ); + collectQueries( + queryKeys.playlists.songList(variables.apiClientProps.serverId), + 'playlist-song-list', + ); + collectQueries( + queryKeys.albumArtists.topSongs(variables.apiClientProps.serverId), + 'top-songs', ); - - const topSongsQueries = queryClient.getQueriesData({ - exact: false, - queryKey: topSongsQueryKey, - }); - - if (topSongsQueries.length) { - topSongsQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: TopSongListResponse | undefined) => { - if (prev) { - return { - ...prev, - items: prev.items.map((item: Song) => - itemIdSet.has(item.id) - ? { ...item, userFavorite: isFavorite } - : item, - ), - }; - } - - return prev; - }, - ); - } - }); - } - break; } } + queueMicrotask(() => { + queryKeysToUpdate.forEach(({ queryKey, type }) => { + queryClient.setQueryData(queryKey, (prev: any) => { + if (!prev) return prev; + + switch (type) { + case 'album-artist-detail': + case 'album-detail': + case 'song-detail': { + if (itemIdSet.has(prev.id)) { + return { ...prev, userFavorite: isFavorite }; + } + if (prev.similarArtists) { + const hasMatch = prev.similarArtists.some((a: any) => + itemIdSet.has(a.id), + ); + if (hasMatch) { + return { + ...prev, + similarArtists: prev.similarArtists.map((a: any) => + itemIdSet.has(a.id) + ? { ...a, userFavorite: isFavorite } + : a, + ), + }; + } + } + return prev; + } + case 'album-artist-infinite-list': + case 'album-infinite-list': + case 'artist-infinite-list': { + const updatedPages = updateItemsInPages( + prev.pages || [], + itemIdSet, + (item) => ({ ...item, userFavorite: isFavorite }), + ); + return updatedPages ? { ...prev, pages: updatedPages } : prev; + } + case 'album-artist-infinite-loader': + case 'album-infinite-loader': + case 'artist-infinite-loader': { + if (prev.data) { + const updatedData = updateItemInArray(prev.data, itemIdSet, (item) => ({ + ...item, + userFavorite: isFavorite, + })); + return updatedData ? { ...prev, data: updatedData } : prev; + } + return prev; + } + case 'album-artist-list': + case 'album-list': + case 'artist-list': + case 'playlist-song-list': + case 'top-songs': { + const updatedItems = updateItemInArray( + prev.items || [], + itemIdSet, + (item) => ({ ...item, userFavorite: isFavorite }), + ); + return updatedItems ? { ...prev, items: updatedItems } : prev; + } + default: + return prev; + } + }); + }); + }); + return previousQueries; }; + export const restoreFavoriteQueryData = ( queryClient: QueryClient, previousQueries: PreviousQueryData[], diff --git a/src/renderer/features/shared/mutations/rating-optimistic-updates.ts b/src/renderer/features/shared/mutations/rating-optimistic-updates.ts index 3585c2d9b..22e895822 100644 --- a/src/renderer/features/shared/mutations/rating-optimistic-updates.ts +++ b/src/renderer/features/shared/mutations/rating-optimistic-updates.ts @@ -18,10 +18,512 @@ import { TopSongListResponse, } from '/@/shared/types/domain-types'; +interface PendingUpdate { + previousData: unknown; + queryKey: readonly unknown[]; + updater: (prev: any) => any; +} + +function collectAndApplyUpdates( + queryClient: QueryClient, + pendingUpdates: PendingUpdate[], +): PreviousQueryData[] { + const previousQueries: PreviousQueryData[] = []; + + // Batch all updates together - React Query will batch these internally + pendingUpdates.forEach(({ previousData, queryKey, updater }) => { + previousQueries.push({ data: previousData, queryKey }); + queryClient.setQueryData(queryKey, updater); + }); + + return previousQueries; +} + +function updateItemInArray( + items: T[], + itemIdSet: Set, + updater: (item: T) => T, +): null | T[] { + let hasChanges = false; + const updatedItems = items.map((item) => { + if (itemIdSet.has(item.id)) { + hasChanges = true; + return updater(item); + } + return item; + }); + + return hasChanges ? updatedItems : null; +} + +function updateItemsInPages( + pages: P[], + itemIdSet: Set, + updater: (item: T) => T, +): null | P[] { + let hasChanges = false; + const updatedPages = pages.map((page) => { + if (!page) return page; + const updatedItems = updateItemInArray(page.items, itemIdSet, updater); + if (updatedItems) { + hasChanges = true; + return { ...page, items: updatedItems }; + } + return page; + }); + + return hasChanges ? updatedPages : null; +} + export const applyRatingOptimisticUpdates = ( queryClient: QueryClient, variables: SetRatingArgs, rating: number, +): PreviousQueryData[] => { + const pendingUpdates: PendingUpdate[] = []; + const itemIdSet = new Set(); + + if (Array.isArray(variables.query.id)) { + variables.query.id.forEach((id) => { + itemIdSet.add(id); + }); + } else { + itemIdSet.add(variables.query.id); + } + + const createRatingUpdater = (item: T): T => ({ + ...item, + userRating: rating, + }); + + switch (variables.query.type) { + case LibraryItem.ALBUM: { + const detailQueryKey = queryKeys.albums.detail(variables.apiClientProps.serverId); + const detailQueries = queryClient.getQueriesData({ + exact: false, + queryKey: detailQueryKey, + }); + + detailQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: AlbumDetailResponse | undefined) => { + if (prev && itemIdSet.has(prev.id)) { + return { ...prev, userRating: rating }; + } + return prev; + }, + }); + } + }); + + const listQueryKey = queryKeys.albums.list(variables.apiClientProps.serverId); + const listQueries = queryClient.getQueriesData({ + exact: false, + queryKey: listQueryKey, + }); + + listQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: AlbumListResponse | undefined) => { + if (!prev) return prev; + const updatedItems = updateItemInArray(prev.items, itemIdSet, (item) => + createRatingUpdater(item), + ); + return updatedItems ? { ...prev, items: updatedItems } : prev; + }, + }); + } + }); + + const infiniteListQueryKey = queryKeys.albums.infiniteList( + variables.apiClientProps.serverId, + ); + const infiniteListQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infiniteListQueryKey, + }); + + infiniteListQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: ( + prev: undefined | { pageParams: string[]; pages: AlbumListResponse[] }, + ) => { + if (!prev) return prev; + const updatedPages = updateItemsInPages( + prev.pages.filter((p): p is AlbumListResponse => !!p), + itemIdSet, + (item) => createRatingUpdater(item), + ); + return updatedPages ? { ...prev, pages: updatedPages } : prev; + }, + }); + } + }); + + // Update infinite loader custom query keys + const infiniteLoaderQueryKey = [ + variables.apiClientProps.serverId, + 'item-list-infinite-loader', + LibraryItem.ALBUM, + ]; + + const infiniteLoaderQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infiniteLoaderQueryKey, + }); + + infiniteLoaderQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: ( + prev: + | undefined + | { + data: unknown[]; + pagesLoaded: Record; + }, + ) => { + if (prev && prev.data) { + const updatedData = updateItemInArray( + prev.data as Array<{ id: string; userRating?: null | number }>, + itemIdSet, + (item) => createRatingUpdater(item), + ); + return updatedData ? { ...prev, data: updatedData } : prev; + } + return prev; + }, + }); + } + }); + + break; + } + case LibraryItem.ALBUM_ARTIST: { + const detailQueryKey = queryKeys.albumArtists.detail(variables.apiClientProps.serverId); + const detailQueries = queryClient.getQueriesData({ + exact: false, + queryKey: detailQueryKey, + }); + + detailQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: AlbumArtistDetailResponse | undefined) => { + if (!prev) return prev; + + if (itemIdSet.has(prev.id)) { + return { ...prev, userRating: rating }; + } + + if (prev.similarArtists && prev.similarArtists.length > 0) { + const hasMatchingSimilarArtist = prev.similarArtists.some( + (artist) => itemIdSet.has(artist.id), + ); + + if (hasMatchingSimilarArtist) { + return { + ...prev, + similarArtists: prev.similarArtists.map((artist) => + itemIdSet.has(artist.id) + ? { ...artist, userRating: rating } + : artist, + ), + }; + } + } + + return prev; + }, + }); + } + }); + + const listQueryKey = queryKeys.albumArtists.list(variables.apiClientProps.serverId); + const listQueries = queryClient.getQueriesData({ + exact: false, + queryKey: listQueryKey, + }); + + listQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: AlbumArtistListResponse | undefined) => { + if (!prev) return prev; + const updatedItems = updateItemInArray(prev.items, itemIdSet, (item) => + createRatingUpdater(item), + ); + return updatedItems ? { ...prev, items: updatedItems } : prev; + }, + }); + } + }); + + const infiniteListQueryKey = queryKeys.albumArtists.infiniteList( + variables.apiClientProps.serverId, + ); + const infiniteListQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infiniteListQueryKey, + }); + + infiniteListQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: ( + prev: + | undefined + | { pageParams: string[]; pages: AlbumArtistListResponse[] }, + ) => { + if (!prev) return prev; + const updatedPages = updateItemsInPages< + AlbumArtist, + AlbumArtistListResponse + >( + prev.pages.filter((p): p is AlbumArtistListResponse => !!p), + itemIdSet, + (item) => createRatingUpdater(item), + ); + return updatedPages ? { ...prev, pages: updatedPages } : prev; + }, + }); + } + }); + + // Update infinite loader custom query keys + const infiniteLoaderQueryKey = [ + variables.apiClientProps.serverId, + 'item-list-infinite-loader', + LibraryItem.ALBUM_ARTIST, + ]; + + const infiniteLoaderQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infiniteLoaderQueryKey, + }); + + infiniteLoaderQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: ( + prev: + | undefined + | { + data: unknown[]; + pagesLoaded: Record; + }, + ) => { + if (prev && prev.data) { + const updatedData = updateItemInArray( + prev.data as Array<{ id: string; userRating?: null | number }>, + itemIdSet, + (item) => createRatingUpdater(item), + ); + return updatedData ? { ...prev, data: updatedData } : prev; + } + return prev; + }, + }); + } + }); + + break; + } + case LibraryItem.ARTIST: { + const listQueryKey = queryKeys.artists.list(variables.apiClientProps.serverId); + const listQueries = queryClient.getQueriesData({ + exact: false, + queryKey: listQueryKey, + }); + + listQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: ArtistListResponse | undefined) => { + if (!prev) return prev; + const updatedItems = updateItemInArray(prev.items, itemIdSet, (item) => + createRatingUpdater(item), + ); + return updatedItems ? { ...prev, items: updatedItems } : prev; + }, + }); + } + }); + + const infiniteListQueryKey = queryKeys.artists.infiniteList( + variables.apiClientProps.serverId, + ); + const infiniteListQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infiniteListQueryKey, + }); + + infiniteListQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: ( + prev: undefined | { pageParams: string[]; pages: ArtistListResponse[] }, + ) => { + if (!prev) return prev; + const updatedPages = updateItemsInPages< + AlbumArtist, + AlbumArtistListResponse + >( + prev.pages.filter((p): p is AlbumArtistListResponse => !!p), + itemIdSet, + (item) => createRatingUpdater(item), + ); + return updatedPages ? { ...prev, pages: updatedPages } : prev; + }, + }); + } + }); + + // Update infinite loader custom query keys + const infiniteLoaderQueryKey = [ + variables.apiClientProps.serverId, + 'item-list-infinite-loader', + LibraryItem.ARTIST, + ]; + + const infiniteLoaderQueries = queryClient.getQueriesData({ + exact: false, + queryKey: infiniteLoaderQueryKey, + }); + + infiniteLoaderQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: ( + prev: + | undefined + | { + data: unknown[]; + pagesLoaded: Record; + }, + ) => { + if (prev && prev.data) { + const updatedData = updateItemInArray( + prev.data as Array<{ id: string; userRating?: null | number }>, + itemIdSet, + (item) => createRatingUpdater(item), + ); + return updatedData ? { ...prev, data: updatedData } : prev; + } + return prev; + }, + }); + } + }); + + break; + } + case LibraryItem.PLAYLIST_SONG: + case LibraryItem.QUEUE_SONG: + case LibraryItem.SONG: { + const albumDetailQueryKey = queryKeys.albums.detail(variables.apiClientProps.serverId); + const albumDetailQueries = queryClient.getQueriesData({ + exact: false, + queryKey: albumDetailQueryKey, + }); + + albumDetailQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: AlbumDetailResponse | undefined) => { + if (!prev || !prev.songs) return prev; + const updatedSongs = updateItemInArray(prev.songs, itemIdSet, (item) => + createRatingUpdater(item), + ); + return updatedSongs ? { ...prev, songs: updatedSongs } : prev; + }, + }); + } + }); + + const detailQueryKey = queryKeys.songs.detail(variables.apiClientProps.serverId); + const detailQueries = queryClient.getQueriesData({ + exact: false, + queryKey: detailQueryKey, + }); + + detailQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: SongDetailResponse | undefined) => { + if (prev && itemIdSet.has(prev.id)) { + return { ...prev, userRating: rating }; + } + return prev; + }, + }); + } + }); + + const topSongsQueryKey = queryKeys.albumArtists.topSongs( + variables.apiClientProps.serverId, + ); + const topSongsQueries = queryClient.getQueriesData({ + exact: false, + queryKey: topSongsQueryKey, + }); + + topSongsQueries.forEach(([queryKey, data]) => { + if (data) { + pendingUpdates.push({ + previousData: data, + queryKey, + updater: (prev: TopSongListResponse | undefined) => { + if (!prev) return prev; + const updatedItems = updateItemInArray(prev.items, itemIdSet, (item) => + createRatingUpdater(item), + ); + return updatedItems ? { ...prev, items: updatedItems } : prev; + }, + }); + } + }); + + break; + } + } + + return collectAndApplyUpdates(queryClient, pendingUpdates); +}; + +export const applyRatingOptimisticUpdatesDeferred = ( + queryClient: QueryClient, + variables: SetRatingArgs, + rating: number, ): PreviousQueryData[] => { const previousQueries: PreviousQueryData[] = []; const itemIdSet = new Set(); @@ -34,505 +536,167 @@ export const applyRatingOptimisticUpdates = ( itemIdSet.add(variables.query.id); } + const queryKeysToUpdate: Array<{ + data: unknown; + queryKey: readonly unknown[]; + type: string; + }> = []; + + const collectQueries = (baseKey: readonly unknown[], type: string) => { + const queries = queryClient.getQueriesData({ exact: false, queryKey: baseKey }); + queries.forEach(([queryKey, data]) => { + if (data) { + previousQueries.push({ data, queryKey }); + queryKeysToUpdate.push({ data, queryKey, type }); + } + }); + }; + switch (variables.query.type) { case LibraryItem.ALBUM: { - const detailQueryKey = queryKeys.albums.detail(variables.apiClientProps.serverId); - - const detailQueries = queryClient.getQueriesData({ - exact: false, - queryKey: detailQueryKey, - }); - - if (detailQueries.length) { - detailQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: AlbumDetailResponse | undefined) => { - if (prev && itemIdSet.has(prev.id)) { - return { ...prev, userRating: rating }; - } - return prev; - }, - ); - } - }); - } - - const listQueryKey = queryKeys.albums.list(variables.apiClientProps.serverId); - - const listQueries = queryClient.getQueriesData({ - exact: false, - queryKey: listQueryKey, - }); - - if (listQueries.length) { - listQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData(queryKey, (prev: AlbumListResponse) => { - return { - ...prev, - items: prev.items.map((item: Album) => { - return itemIdSet.has(item.id) - ? { ...item, userRating: rating } - : item; - }), - }; - }); - } - }); - } - - const infiniteListQueryKey = queryKeys.albums.infiniteList( - variables.apiClientProps.serverId, + collectQueries( + queryKeys.albums.detail(variables.apiClientProps.serverId), + 'album-detail', + ); + collectQueries(queryKeys.albums.list(variables.apiClientProps.serverId), 'album-list'); + collectQueries( + queryKeys.albums.infiniteList(variables.apiClientProps.serverId), + 'album-infinite-list', + ); + collectQueries( + [variables.apiClientProps.serverId, 'item-list-infinite-loader', LibraryItem.ALBUM], + 'album-infinite-loader', ); - - const infiniteListQueries = queryClient.getQueriesData({ - exact: false, - queryKey: infiniteListQueryKey, - }); - - if (infiniteListQueries.length) { - infiniteListQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: { pageParams: string[]; pages: AlbumListResponse[] }) => { - return { - ...prev, - pages: prev.pages.map((page: AlbumListResponse) => { - return { - ...page, - items: page.items.map((item: Album) => { - return itemIdSet.has(item.id) - ? { ...item, userRating: rating } - : item; - }), - }; - }), - }; - }, - ); - } - }); - } - - // Update infinite loader custom query keys - const infiniteLoaderQueryKey = [ - variables.apiClientProps.serverId, - 'item-list-infinite-loader', - LibraryItem.ALBUM, - ]; - - const infiniteLoaderQueries = queryClient.getQueriesData({ - exact: false, - queryKey: infiniteLoaderQueryKey, - }); - - if (infiniteLoaderQueries.length) { - infiniteLoaderQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - ( - prev: - | undefined - | { - data: unknown[]; - pagesLoaded: Record; - }, - ) => { - if (prev && prev.data) { - return { - ...prev, - data: prev.data.map((item: any) => { - if (!item || !item.id) { - return item; - } - - return itemIdSet.has(item.id) - ? { ...item, userRating: rating } - : item; - }), - }; - } - - return prev; - }, - ); - } - }); - } - break; } case LibraryItem.ALBUM_ARTIST: { - const detailQueryKey = queryKeys.albumArtists.detail(variables.apiClientProps.serverId); - - const detailQueries = queryClient.getQueriesData({ - exact: false, - queryKey: detailQueryKey, - }); - - if (detailQueries.length) { - detailQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: AlbumArtistDetailResponse | undefined) => { - if (!prev) { - return prev; - } - - // Update the main artist if it matches - if (itemIdSet.has(prev.id)) { - return { ...prev, userRating: rating }; - } - - // Update similar artists if any match - if (prev.similarArtists && prev.similarArtists.length > 0) { - const hasMatchingSimilarArtist = prev.similarArtists.some( - (artist) => itemIdSet.has(artist.id), - ); - - if (hasMatchingSimilarArtist) { - return { - ...prev, - similarArtists: prev.similarArtists.map((artist) => - itemIdSet.has(artist.id) - ? { ...artist, userRating: rating } - : artist, - ), - }; - } - } - - return prev; - }, - ); - } - }); - } - - const listQueryKey = queryKeys.albumArtists.list(variables.apiClientProps.serverId); - - const listQueries = queryClient.getQueriesData({ - exact: false, - queryKey: listQueryKey, - }); - - if (listQueries.length) { - listQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData(queryKey, (prev: AlbumArtistListResponse) => { - return { - ...prev, - items: prev.items.map((item: AlbumArtist) => { - return itemIdSet.has(item.id) - ? { ...item, userRating: rating } - : item; - }), - }; - }); - } - }); - } - - const infiniteListQueryKey = queryKeys.albumArtists.infiniteList( - variables.apiClientProps.serverId, + collectQueries( + queryKeys.albumArtists.detail(variables.apiClientProps.serverId), + 'album-artist-detail', + ); + collectQueries( + queryKeys.albumArtists.list(variables.apiClientProps.serverId), + 'album-artist-list', + ); + collectQueries( + queryKeys.albumArtists.infiniteList(variables.apiClientProps.serverId), + 'album-artist-infinite-list', + ); + collectQueries( + [ + variables.apiClientProps.serverId, + 'item-list-infinite-loader', + LibraryItem.ALBUM_ARTIST, + ], + 'album-artist-infinite-loader', ); - const infiniteListQueries = queryClient.getQueriesData({ - exact: false, - queryKey: infiniteListQueryKey, - }); - - if (infiniteListQueries.length) { - infiniteListQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: { pageParams: string[]; pages: AlbumArtistListResponse[] }) => { - return { - ...prev, - pages: prev.pages.map((page: AlbumArtistListResponse) => { - return { - ...page, - items: page.items.map((item: AlbumArtist) => { - return itemIdSet.has(item.id) - ? { ...item, userRating: rating } - : item; - }), - }; - }), - }; - }, - ); - } - }); - } - - // Update infinite loader custom query keys - const infiniteLoaderQueryKey = [ - variables.apiClientProps.serverId, - 'item-list-infinite-loader', - LibraryItem.ALBUM_ARTIST, - ]; - - const infiniteLoaderQueries = queryClient.getQueriesData({ - exact: false, - queryKey: infiniteLoaderQueryKey, - }); - - if (infiniteLoaderQueries.length) { - infiniteLoaderQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - ( - prev: - | undefined - | { - data: unknown[]; - pagesLoaded: Record; - }, - ) => { - if (prev && prev.data) { - return { - ...prev, - data: prev.data.map((item: any) => { - if (!item || !item.id) { - return item; - } - - return itemIdSet.has(item.id) - ? { ...item, userRating: rating } - : item; - }), - }; - } - - return prev; - }, - ); - } - }); - } - break; } case LibraryItem.ARTIST: { - const listQueryKey = queryKeys.artists.list(variables.apiClientProps.serverId); - - const listQueries = queryClient.getQueriesData({ - exact: false, - queryKey: listQueryKey, - }); - - if (listQueries.length) { - listQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData(queryKey, (prev: ArtistListResponse) => { - return { - ...prev, - items: prev.items.map((item: AlbumArtist) => { - return itemIdSet.has(item.id) - ? { ...item, userRating: rating } - : item; - }), - }; - }); - } - }); - } - - const infiniteListQueryKey = queryKeys.artists.infiniteList( - variables.apiClientProps.serverId, + collectQueries( + queryKeys.artists.list(variables.apiClientProps.serverId), + 'artist-list', + ); + collectQueries( + queryKeys.artists.infiniteList(variables.apiClientProps.serverId), + 'artist-infinite-list', + ); + collectQueries( + [ + variables.apiClientProps.serverId, + 'item-list-infinite-loader', + LibraryItem.ARTIST, + ], + 'artist-infinite-loader', ); - const infiniteListQueries = queryClient.getQueriesData({ - exact: false, - queryKey: infiniteListQueryKey, - }); - - if (infiniteListQueries.length) { - infiniteListQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: { pageParams: string[]; pages: ArtistListResponse[] }) => { - return { - ...prev, - pages: prev.pages.map((page: ArtistListResponse) => { - return { - ...page, - items: page.items.map((item: AlbumArtist) => { - return itemIdSet.has(item.id) - ? { ...item, userRating: rating } - : item; - }), - }; - }), - }; - }, - ); - } - }); - } - - // Update infinite loader custom query keys - const infiniteLoaderQueryKey = [ - variables.apiClientProps.serverId, - 'item-list-infinite-loader', - LibraryItem.ARTIST, - ]; - - const infiniteLoaderQueries = queryClient.getQueriesData({ - exact: false, - queryKey: infiniteLoaderQueryKey, - }); - - if (infiniteLoaderQueries.length) { - infiniteLoaderQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - ( - prev: - | undefined - | { - data: unknown[]; - pagesLoaded: Record; - }, - ) => { - if (prev && prev.data) { - return { - ...prev, - data: prev.data.map((item: any) => { - if (!item || !item.id) { - return item; - } - - return itemIdSet.has(item.id) - ? { ...item, userRating: rating } - : item; - }), - }; - } - - return prev; - }, - ); - } - }); - } - break; } case LibraryItem.PLAYLIST_SONG: case LibraryItem.QUEUE_SONG: case LibraryItem.SONG: { - const albumDetailQueryKey = queryKeys.albums.detail(variables.apiClientProps.serverId); - - const albumDetailQueries = queryClient.getQueriesData({ - exact: false, - queryKey: albumDetailQueryKey, - }); - - if (albumDetailQueries.length) { - albumDetailQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: AlbumDetailResponse | undefined) => { - if (prev) { - return { - ...prev, - songs: prev.songs?.map((song: Song) => { - return itemIdSet.has(song.id) - ? { ...song, userRating: rating } - : song; - }), - }; - } - return prev; - }, - ); - } - }); - } - - const detailQueryKey = queryKeys.songs.detail(variables.apiClientProps.serverId); - - const detailQueries = queryClient.getQueriesData({ - exact: false, - queryKey: detailQueryKey, - }); - - if (detailQueries.length) { - detailQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: SongDetailResponse | undefined) => { - if (prev && itemIdSet.has(prev.id)) { - return { ...prev, userRating: rating }; - } - return prev; - }, - ); - } - }); - } - - const topSongsQueryKey = queryKeys.albumArtists.topSongs( - variables.apiClientProps.serverId, + collectQueries( + queryKeys.albums.detail(variables.apiClientProps.serverId), + 'album-detail', + ); + collectQueries( + queryKeys.songs.detail(variables.apiClientProps.serverId), + 'song-detail', + ); + collectQueries( + queryKeys.albumArtists.topSongs(variables.apiClientProps.serverId), + 'top-songs', ); - - const topSongsQueries = queryClient.getQueriesData({ - exact: false, - queryKey: topSongsQueryKey, - }); - - if (topSongsQueries.length) { - topSongsQueries.forEach(([queryKey, data]) => { - if (data) { - previousQueries.push({ data, queryKey }); - queryClient.setQueryData( - queryKey, - (prev: TopSongListResponse | undefined) => { - if (prev) { - return { - ...prev, - items: prev.items.map((item: Song) => - itemIdSet.has(item.id) - ? { ...item, userRating: rating } - : item, - ), - }; - } - return prev; - }, - ); - } - }); - } - break; } } + queueMicrotask(() => { + queryKeysToUpdate.forEach(({ queryKey, type }) => { + queryClient.setQueryData(queryKey, (prev: any) => { + if (!prev) return prev; + + switch (type) { + case 'album-artist-detail': + case 'album-detail': + case 'song-detail': { + if (itemIdSet.has(prev.id)) { + return { ...prev, userRating: rating }; + } + if (prev.similarArtists) { + const hasMatch = prev.similarArtists.some((a: any) => + itemIdSet.has(a.id), + ); + if (hasMatch) { + return { + ...prev, + similarArtists: prev.similarArtists.map((a: any) => + itemIdSet.has(a.id) ? { ...a, userRating: rating } : a, + ), + }; + } + } + return prev; + } + case 'album-artist-infinite-list': + case 'album-infinite-list': + case 'artist-infinite-list': { + const updatedPages = updateItemsInPages( + prev.pages || [], + itemIdSet, + (item) => ({ ...item, userRating: rating }), + ); + return updatedPages ? { ...prev, pages: updatedPages } : prev; + } + case 'album-artist-infinite-loader': + case 'album-infinite-loader': + case 'artist-infinite-loader': { + if (prev.data) { + const updatedData = updateItemInArray(prev.data, itemIdSet, (item) => ({ + ...item, + userRating: rating, + })); + return updatedData ? { ...prev, data: updatedData } : prev; + } + return prev; + } + case 'album-artist-list': + case 'album-list': + case 'artist-list': + case 'top-songs': { + const updatedItems = updateItemInArray( + prev.items || [], + itemIdSet, + (item) => ({ ...item, userRating: rating }), + ); + return updatedItems ? { ...prev, items: updatedItems } : prev; + } + default: + return prev; + } + }); + }); + }); + return previousQueries; };