Fix scoped catalog refresh invalidation (#51)
* Fix scoped catalog refresh invalidation * Remove duplicate catalog refetch * fix(web): keep library lists fresh on scoped catalog events --------- Co-authored-by: RXWatcher <14085001+RXWatcher@users.noreply.github.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
Quick
parent
480ca44306
commit
6b1554a86c
@@ -1,6 +1,8 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { act, cleanup, render } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { adminKeys, catalogKeys, libraryKeys, sectionKeys } from "@/hooks/queries/keys";
|
||||
import { invalidateCatalogState } from "./realtimeCatalogInvalidation";
|
||||
import { buildEventsUrl, RealtimeEventsProvider } from "./RealtimeEventsProvider";
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
@@ -84,6 +86,84 @@ describe("buildEventsUrl", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalidateCatalogState", () => {
|
||||
it("invalidates library lists for a scoped library change", async () => {
|
||||
const queryClient = new QueryClient();
|
||||
const otherCatalogKey = catalogKeys.list({
|
||||
source: "section",
|
||||
scope: "library",
|
||||
section_id: "all",
|
||||
library_id: 1,
|
||||
limit: 60,
|
||||
offset: 0,
|
||||
});
|
||||
const changedCatalogKey = catalogKeys.list({
|
||||
source: "section",
|
||||
scope: "library",
|
||||
section_id: "all",
|
||||
library_id: 3,
|
||||
limit: 60,
|
||||
offset: 0,
|
||||
});
|
||||
const otherSectionKey = sectionKeys.libraryLayout(1);
|
||||
const changedSectionKey = sectionKeys.libraryLayout(3);
|
||||
const userLibrariesKey = libraryKeys.user("profile-1");
|
||||
|
||||
queryClient.setQueryData(adminKeys.libraries(), []);
|
||||
queryClient.setQueryData(adminKeys.libraryMatchQueueStatuses(), []);
|
||||
queryClient.setQueryData(userLibrariesKey, []);
|
||||
queryClient.setQueryData(otherCatalogKey, { items: [] });
|
||||
queryClient.setQueryData(changedCatalogKey, { items: [] });
|
||||
queryClient.setQueryData(otherSectionKey, { sections: [] });
|
||||
queryClient.setQueryData(changedSectionKey, { sections: [] });
|
||||
|
||||
invalidateCatalogState(queryClient, { libraryId: 3, allowDashboardRefetch: false });
|
||||
await Promise.resolve();
|
||||
|
||||
expect(queryClient.getQueryState(adminKeys.libraries())?.isInvalidated).toBe(true);
|
||||
expect(queryClient.getQueryState(adminKeys.libraryMatchQueueStatuses())?.isInvalidated).toBe(
|
||||
true,
|
||||
);
|
||||
expect(queryClient.getQueryState(userLibrariesKey)?.isInvalidated).toBe(true);
|
||||
expect(queryClient.getQueryState(otherCatalogKey)?.isInvalidated).toBe(false);
|
||||
expect(queryClient.getQueryState(changedCatalogKey)?.isInvalidated).toBe(true);
|
||||
expect(queryClient.getQueryState(otherSectionKey)?.isInvalidated).toBe(false);
|
||||
expect(queryClient.getQueryState(changedSectionKey)?.isInvalidated).toBe(true);
|
||||
});
|
||||
|
||||
it("can skip library lists for item-scoped catalog changes", async () => {
|
||||
const queryClient = new QueryClient();
|
||||
const changedCatalogKey = catalogKeys.list({
|
||||
source: "section",
|
||||
scope: "library",
|
||||
section_id: "all",
|
||||
library_id: 3,
|
||||
limit: 60,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
queryClient.setQueryData(adminKeys.libraries(), []);
|
||||
queryClient.setQueryData(adminKeys.libraryMatchQueueStatuses(), []);
|
||||
queryClient.setQueryData(libraryKeys.all, []);
|
||||
queryClient.setQueryData(changedCatalogKey, { items: [] });
|
||||
|
||||
invalidateCatalogState(queryClient, {
|
||||
itemId: "item-1",
|
||||
libraryId: 3,
|
||||
allowDashboardRefetch: false,
|
||||
includeLibraryLists: false,
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
expect(queryClient.getQueryState(adminKeys.libraries())?.isInvalidated).toBe(false);
|
||||
expect(queryClient.getQueryState(adminKeys.libraryMatchQueueStatuses())?.isInvalidated).toBe(
|
||||
false,
|
||||
);
|
||||
expect(queryClient.getQueryState(libraryKeys.all)?.isInvalidated).toBe(false);
|
||||
expect(queryClient.getQueryState(changedCatalogKey)?.isInvalidated).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("RealtimeEventsProvider", () => {
|
||||
beforeEach(() => {
|
||||
FakeWebSocket.instances = [];
|
||||
|
||||
@@ -18,9 +18,10 @@ import {
|
||||
type RealtimeConnectionState,
|
||||
type RealtimeEventsContextValue,
|
||||
} from "@/components/realtimeEventsContext";
|
||||
import { invalidateCatalogState } from "@/components/realtimeCatalogInvalidation";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { usePageActivity } from "@/hooks/usePageActivity";
|
||||
import { adminKeys, catalogKeys, historyImportKeys, libraryKeys } from "@/hooks/queries/keys";
|
||||
import { adminKeys, historyImportKeys, libraryKeys } from "@/hooks/queries/keys";
|
||||
import {
|
||||
invalidateMediaSurfaceQueries,
|
||||
updateCatalogItemDetail,
|
||||
@@ -46,7 +47,12 @@ interface UserStatePayload {
|
||||
in_watchlist?: boolean;
|
||||
}
|
||||
|
||||
const CATALOG_ITEM_CHANGED_EVENTS = new Set(["metadata.updated", "catalog.item.changed"]);
|
||||
const CATALOG_ITEM_CHANGED_EVENTS = new Set([
|
||||
"catalog.item.changed",
|
||||
"library.item_added",
|
||||
"metadata.updated",
|
||||
]);
|
||||
const SCOPED_CATALOG_LIBRARY_EVENTS = new Set(["catalog.library.changed", "library.changed"]);
|
||||
const DASHBOARD_QUERY_KEYS = [
|
||||
adminKeys.stats(),
|
||||
adminKeys.sessions(),
|
||||
@@ -178,25 +184,12 @@ function invalidateDashboardQueries(queryClient: QueryClient, allowRefetch: bool
|
||||
}
|
||||
}
|
||||
|
||||
function invalidateCatalogState(
|
||||
queryClient: QueryClient,
|
||||
options: { itemId?: string; allowDashboardRefetch: boolean },
|
||||
) {
|
||||
const { itemId, allowDashboardRefetch } = options;
|
||||
void invalidateMediaSurfaceQueries(queryClient, itemId ? { itemId } : {}).then(() => {
|
||||
bumpHomeRefreshSignal(queryClient);
|
||||
});
|
||||
void queryClient.refetchQueries({ queryKey: catalogKeys.all, type: "active" });
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: adminKeys.libraries(),
|
||||
refetchType: allowDashboardRefetch ? "active" : "none",
|
||||
});
|
||||
void queryClient.invalidateQueries({ queryKey: adminKeys.libraryMatchQueueStatuses() });
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: adminKeys.stats(),
|
||||
refetchType: allowDashboardRefetch ? "active" : "none",
|
||||
});
|
||||
void queryClient.invalidateQueries({ queryKey: libraryKeys.all });
|
||||
function catalogEventLibraryID(data: unknown) {
|
||||
if (!data || typeof data !== "object" || !("library_id" in data)) {
|
||||
return undefined;
|
||||
}
|
||||
const value = (data as { library_id?: unknown }).library_id;
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function handleJobSideEffects(
|
||||
@@ -473,18 +466,29 @@ export function RealtimeEventsProvider({ children }: { children: ReactNode }) {
|
||||
function handleEvent(message: EventsEventMessage) {
|
||||
switch (message.channel) {
|
||||
case "catalog":
|
||||
if (CATALOG_ITEM_CHANGED_EVENTS.has(message.event)) {
|
||||
invalidateCatalogState(queryClient, {
|
||||
itemId:
|
||||
typeof message.data === "object" && message.data && "content_id" in message.data
|
||||
? (message.data as { content_id?: string }).content_id
|
||||
: undefined,
|
||||
allowDashboardRefetch: allowDashboardRealtimeUpdatesRef.current,
|
||||
});
|
||||
} else {
|
||||
invalidateCatalogState(queryClient, {
|
||||
allowDashboardRefetch: allowDashboardRealtimeUpdatesRef.current,
|
||||
});
|
||||
{
|
||||
const eventLibraryID = catalogEventLibraryID(message.data);
|
||||
if (CATALOG_ITEM_CHANGED_EVENTS.has(message.event)) {
|
||||
invalidateCatalogState(queryClient, {
|
||||
itemId:
|
||||
typeof message.data === "object" && message.data && "content_id" in message.data
|
||||
? (message.data as { content_id?: string }).content_id
|
||||
: undefined,
|
||||
libraryId: eventLibraryID,
|
||||
allowDashboardRefetch: allowDashboardRealtimeUpdatesRef.current,
|
||||
includeLibraryLists: false,
|
||||
});
|
||||
} else if (SCOPED_CATALOG_LIBRARY_EVENTS.has(message.event) && eventLibraryID) {
|
||||
invalidateCatalogState(queryClient, {
|
||||
libraryId: eventLibraryID,
|
||||
allowDashboardRefetch: allowDashboardRealtimeUpdatesRef.current,
|
||||
});
|
||||
} else {
|
||||
invalidateCatalogState(queryClient, {
|
||||
libraryId: eventLibraryID,
|
||||
allowDashboardRefetch: allowDashboardRealtimeUpdatesRef.current,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "jobs":
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { adminKeys, libraryKeys } from "@/hooks/queries/keys";
|
||||
import { invalidateMediaSurfaceQueries } from "@/hooks/queries/mediaSurfaceRefresh";
|
||||
import { bumpHomeRefreshSignal } from "@/pages/homeSurfaceRefresh";
|
||||
|
||||
export function invalidateCatalogState(
|
||||
queryClient: QueryClient,
|
||||
options: {
|
||||
itemId?: string;
|
||||
libraryId?: number;
|
||||
allowDashboardRefetch: boolean;
|
||||
includeLibraryLists?: boolean;
|
||||
},
|
||||
) {
|
||||
const { itemId, libraryId, allowDashboardRefetch, includeLibraryLists = true } = options;
|
||||
void invalidateMediaSurfaceQueries(queryClient, { itemId, libraryId }).then(() => {
|
||||
bumpHomeRefreshSignal(queryClient);
|
||||
});
|
||||
if (includeLibraryLists) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: adminKeys.libraries(),
|
||||
refetchType: allowDashboardRefetch ? "active" : "none",
|
||||
});
|
||||
void queryClient.invalidateQueries({ queryKey: adminKeys.libraryMatchQueueStatuses() });
|
||||
void queryClient.invalidateQueries({ queryKey: libraryKeys.all });
|
||||
}
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: adminKeys.stats(),
|
||||
refetchType: allowDashboardRefetch ? "active" : "none",
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user