Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .tests/frontend/library-query-invalidation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,85 @@ test("artist batch lookup stays within the API batch limit", async (t) => {
queryClient.clear();
}
});

test("artist adds publish an immediate shared library lookup", async (t) => {
const vite = await createServer({
root: "frontend",
server: { middlewareMode: true, hmr: false },
appType: "custom",
optimizeDeps: { noDiscovery: true },
});
t.after(() => vite.close());

const { addArtistToLibrary } = await vite.ssrLoadModule(
"/src/utils/api/endpoints/library.js?artist-add-lookup-test",
);
const { queryClient, queryKeys } = await vite.ssrLoadModule("/src/queryClient.js");
const originalFetch = globalThis.fetch;
globalThis.fetch = async () => new Response(JSON.stringify({
foreignArtistId: "artist-1",
artist: { mbid: "artist-1", artistName: "Artist" },
}), {
status: 201,
headers: { "content-type": "application/json" },
});

try {
await addArtistToLibrary({ foreignArtistId: "artist-1", artistName: "Artist" });
assert.equal(queryClient.getQueryData(queryKeys.libraryLookup("artist-1")), true);
} finally {
globalThis.fetch = originalFetch;
queryClient.clear();
}
});

test("library refresh starts a new lookup after cancelling an active request", async (t) => {
const vite = await createServer({
root: "frontend",
server: { middlewareMode: true, hmr: false },
appType: "custom",
optimizeDeps: { noDiscovery: true },
});
t.after(() => vite.close());

const { lookupArtistInLibrary } = await vite.ssrLoadModule(
"/src/utils/api/endpoints/library.js?library-refresh-cancellation-test",
);
const { queryClient, queryKeys } = await vite.ssrLoadModule("/src/queryClient.js");
const originalFetch = globalThis.fetch;
let requestCount = 0;
let firstRequestSignal;
globalThis.fetch = (_url, init) => {
requestCount += 1;
if (requestCount === 1) {
firstRequestSignal = init.signal;
return new Promise((_resolve, reject) => {
init.signal.addEventListener("abort", () => reject(new Error("aborted")), {
once: true,
});
});
}
return Promise.resolve(new Response(JSON.stringify({ exists: true }), {
status: 200,
headers: { "content-type": "application/json" },
}));
};

try {
const initialRequest = lookupArtistInLibrary("artist-1", { bypassCache: true });
await waitForRequestSignal(() => firstRequestSignal);
await queryClient.cancelQueries({
queryKey: queryKeys.libraryLookupDetails("artist-1"),
exact: true,
});
await assert.rejects(initialRequest);

const refreshed = await lookupArtistInLibrary("artist-1", { bypassCache: true });
assert.equal(firstRequestSignal.aborted, true);
assert.equal(requestCount, 2);
assert.equal(refreshed.exists, true);
} finally {
globalThis.fetch = originalFetch;
queryClient.clear();
}
});
51 changes: 42 additions & 9 deletions frontend/src/pages/ArtistDetails/hooks/useArtistDetailsStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
} from "../../../utils/api/endpoints/library.js";
import { getAppSettings } from "../../../utils/api/endpoints/settings.js";
import { getStoredAuth } from "../../../utils/api/core.js";
import { queryClient, queryKeys } from "../../../queryClient.js";
import { useWebSocketChannel } from "../../../hooks/useWebSocket.js";
import { allReleaseTypes, emptyArtistShape } from "../constants";

const buildReleaseGroupCoverRequest = (rgId, artist, libraryAlbums, pageArtistName) => {
Expand Down Expand Up @@ -126,6 +128,17 @@ export function useArtistDetailsStream(
const artistNameRef = useRef(artistNameFromNav || "");
const visibleCoverIdsRef = useRef(visibleCoverIds);
const streamRequestRef = useRef(0);
const libraryRefreshRef = useRef(null);
const optimisticLibraryLookupRef = useRef(false);

useWebSocketChannel(
"library",
(message) => {
if (message?.type !== "library_scan_completed") return;
libraryRefreshRef.current?.();
},
{ enabled: Boolean(mbid) },
);

if (artistMbidRef.current !== mbid) {
artistMbidRef.current = mbid;
Expand Down Expand Up @@ -154,6 +167,7 @@ export function useArtistDetailsStream(
const requestId = ++streamRequestRef.current;
const isCurrentRequest = () => streamRequestRef.current === requestId;
const nextCachedLookup = readLibraryLookupCache([mbid])?.[mbid];
optimisticLibraryLookupRef.current = nextCachedLookup === true;
const nextSeededExistsInLibrary =
stableInitialLibraryHint.existsInLibrary === true || nextCachedLookup === true
? true
Expand Down Expand Up @@ -253,22 +267,24 @@ export function useArtistDetailsStream(
await Promise.allSettled(requests);
};

const loadLibraryFallback = async () => {
const loadLibraryFallback = async ({ bypassCache = false } = {}) => {
setLoadingLibrary(true);
try {
const lookup = await lookupArtistInLibrary(mbid);
const lookup = await lookupArtistInLibrary(mbid, { bypassCache });
if (!isCurrentRequest()) return;
queryClient.setQueryData(queryKeys.libraryLookup(mbid), lookup.exists);
setExistsInLibrary(lookup.exists);
if (!lookup.exists || !lookup.artist) return;

const fullArtist = lookup.canonical
? lookup.artist
: await getLibraryArtist(lookup.artist.mbid || lookup.artist.foreignArtistId).catch(
(err) => {
console.error("Failed to fetch full artist details:", err);
return lookup.artist;
},
);
: await getLibraryArtist(
lookup.artist.mbid || lookup.artist.foreignArtistId,
{ bypassCache },
).catch((err) => {
console.error("Failed to fetch full artist details:", err);
return lookup.artist;
});
if (!isCurrentRequest()) return;

setLibraryArtist(fullArtist);
Expand All @@ -281,7 +297,7 @@ export function useArtistDetailsStream(
const artistId = fullArtist.id || lookup.artist.id;
if (!artistId) return;

const albums = await getLibraryAlbums(artistId).catch((err) => {
const albums = await getLibraryAlbums(artistId, { bypassCache }).catch((err) => {
console.error("Failed to fetch library albums:", err);
return [];
});
Expand All @@ -295,6 +311,15 @@ export function useArtistDetailsStream(
}
};

libraryRefreshRef.current = async () => {
optimisticLibraryLookupRef.current = false;
await queryClient.cancelQueries({
queryKey: queryKeys.libraryLookupDetails(mbid),
exact: true,
});
return loadLibraryFallback({ bypassCache: true });
Comment thread
lklynet marked this conversation as resolved.
Resolved
};

const loadRestFallback = async () => {
if (restFallbackStarted) return;
restFallbackStarted = true;
Expand Down Expand Up @@ -434,10 +459,17 @@ export function useArtistDetailsStream(
if (!isCurrentRequest()) return;
libraryReceived = true;
if (data.exists && data.artist) {
optimisticLibraryLookupRef.current = false;
queryClient.setQueryData(queryKeys.libraryLookup(mbid), true);
setExistsInLibrary(true);
setLibraryArtist(data.artist);
setLibraryAlbums(data.albums || []);
} else if (!data.exists) {
if (optimisticLibraryLookupRef.current) {
setLoadingLibrary(false);
return;
}
queryClient.setQueryData(queryKeys.libraryLookup(mbid), false);
setExistsInLibrary(false);
setLibraryArtist(null);
setLibraryAlbums([]);
Expand Down Expand Up @@ -490,6 +522,7 @@ export function useArtistDetailsStream(
return () => {
clearTimeout(fallbackTimeout);
eventSource.close();
libraryRefreshRef.current = null;
};
}, [
mbid,
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/utils/api/endpoints/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,16 @@ export const lookupAlbumsInLibraryBatch = (mbids, { signal, bypassCache = false
});
};

export const addArtistToLibrary = (artistData) =>
postData("/library/artists", artistData);
export const addArtistToLibrary = async (artistData) => {
const result = await postData("/library/artists", artistData);
const mbid =
result?.artist?.mbid ||
result?.artist?.foreignArtistId ||
result?.foreignArtistId ||
artistData?.foreignArtistId;
if (mbid) queryClient.setQueryData(queryKeys.libraryLookup(mbid), true);
return result;
};

export const deleteArtistFromLibrary = (mbid, deleteFiles = false) =>
deleteData(`/library/artists/${mbid}`, {
Expand Down
Loading