diff --git a/package.json b/package.json index ee9bb861..29c50f93 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@hookform/resolvers": "^5.4.0", "@mui/icons-material": "^9.2.0", "@mui/material": "^9.2.0", + "@tanstack/query-broadcast-client-experimental": "^5.101.2", "@tanstack/react-query": "^5.101.2", "@tanstack/react-query-devtools": "^5.101.2", "@testing-library/dom": "^10.4.1", diff --git a/src/lib/query/query-client.ts b/src/lib/query/query-client.ts index 1223fc2b..bef74cd1 100644 --- a/src/lib/query/query-client.ts +++ b/src/lib/query/query-client.ts @@ -1,5 +1,6 @@ import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query"; import { toast } from "react-toastify"; +import { setupQueryBroadcast } from "@/lib/query/setup-query-broadcast"; export const queryClient = new QueryClient({ queryCache: new QueryCache({ @@ -30,4 +31,6 @@ export const queryClient = new QueryClient({ }, }); +setupQueryBroadcast(queryClient); + export default queryClient; diff --git a/src/lib/query/setup-query-broadcast.ts b/src/lib/query/setup-query-broadcast.ts new file mode 100644 index 00000000..3b5f24d6 --- /dev/null +++ b/src/lib/query/setup-query-broadcast.ts @@ -0,0 +1,19 @@ +import { broadcastQueryClient } from "@tanstack/query-broadcast-client-experimental"; +import type { QueryClient } from "@tanstack/react-query"; + +/** Same-origin channel name for cross-tab QueryClient sync. */ +export const QUERY_BROADCAST_CHANNEL = "seahaven-admin-query"; + +/** + * Syncs QueryClient cache (invalidations + data) across browser tabs via BroadcastChannel. + * No-op outside the browser and during Vitest runs. + */ +export function setupQueryBroadcast(queryClient: QueryClient): void { + if (typeof window === "undefined") return; + if (import.meta.env.MODE === "test") return; + + broadcastQueryClient({ + queryClient, + broadcastChannel: QUERY_BROADCAST_CHANNEL, + }); +} diff --git a/src/test/lib/query/setup-query-broadcast.test.ts b/src/test/lib/query/setup-query-broadcast.test.ts new file mode 100644 index 00000000..bc203093 --- /dev/null +++ b/src/test/lib/query/setup-query-broadcast.test.ts @@ -0,0 +1,47 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { QueryClient } from "@tanstack/react-query"; + +const broadcastQueryClient = vi.fn(); + +vi.mock("@tanstack/query-broadcast-client-experimental", () => ({ + broadcastQueryClient: (...args: unknown[]) => broadcastQueryClient(...args), +})); + +describe("setupQueryBroadcast", () => { + beforeEach(() => { + broadcastQueryClient.mockClear(); + vi.unstubAllEnvs(); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("does not call broadcastQueryClient when MODE is test", async () => { + vi.stubEnv("MODE", "test"); + const { setupQueryBroadcast } = await import("@/lib/query/setup-query-broadcast"); + const client = new QueryClient(); + + setupQueryBroadcast(client); + + expect(broadcastQueryClient).not.toHaveBeenCalled(); + }); + + it("calls broadcastQueryClient with the app channel when MODE is not test", async () => { + vi.stubEnv("MODE", "development"); + vi.resetModules(); + + const { setupQueryBroadcast, QUERY_BROADCAST_CHANNEL } = + await import("@/lib/query/setup-query-broadcast"); + const client = new QueryClient(); + + setupQueryBroadcast(client); + + expect(broadcastQueryClient).toHaveBeenCalledTimes(1); + expect(broadcastQueryClient).toHaveBeenCalledWith({ + queryClient: client, + broadcastChannel: QUERY_BROADCAST_CHANNEL, + }); + expect(QUERY_BROADCAST_CHANNEL).toBe("seahaven-admin-query"); + }); +});