import { QueryClient } from "@tanstack/react-query"; import { describe, expect, it, vi } from "vitest"; import type { BrowserCrossContextInvalidation, CrossContextInvalidationDelivery, } from "../../src/adapters/cross-context-invalidation/index.ts"; import { createTanStackCacheCoordinator } from "../../src/adapters/query-cache/tanstack-cache-coordinator.ts"; import { defineQueryInvalidationTopic } from "../../src/contracts/query-invalidation.ts"; const TOPIC_A = defineQueryInvalidationTopic("qinv.topic-a"); const TOPIC_B = defineQueryInvalidationTopic("qinv.topic-b"); function queryRegistry() { return Object.freeze({ A: Object.freeze({ namespace: Object.freeze(["resource-a", 1] as const), invalidationTopic: TOPIC_A, crossContext: "invalidate-only" as const, version: 1, persistence: "disabled" as const, }), B: Object.freeze({ namespace: Object.freeze(["resource-b", 1] as const), invalidationTopic: TOPIC_B, crossContext: "invalidate-only" as const, version: 1, persistence: "disabled" as const, }), }); } function crossContextHarness() { let listener: | ((delivery: CrossContextInvalidationDelivery) => void) | undefined; const publish = vi.fn(() => ({ ok: true as const, transport: "BROADCAST" as const, })); const close = vi.fn(); const transport: BrowserCrossContextInvalidation = { getStatus: () => "ACTIVE_BROADCAST", publish, subscribe(next) { listener = next; return () => { listener = undefined; }; }, close, }; return Object.freeze({ transport, publish, close, deliver(topic: string, ordering: "NEXT" | "GAP" = "NEXT") { listener?.({ ordering, transport: "BROADCAST", event: { protocolVersion: 1, eventId: `event-${topic}-${ordering}`, sourceId: "remote-source", sourceEpoch: "remote-epoch", sequence: 1, cacheEpoch: "cache-epoch", topic, topicVersion: 1, emittedAt: 1, expiresAt: 60_001, }, }); }, }); } function createClient(): QueryClient { return new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity, gcTime: Infinity }, mutations: { retry: false }, }, }); } describe("TanStack cross-context cache coordinator", () => { it("maps a local opaque topic to one namespace and publishes no query key", async () => { const client = createClient(); const harness = crossContextHarness(); client.setQueryData(["resource-a", 1, "list"], ["a"]); client.setQueryData(["resource-b", 1, "list"], ["b"]); const coordinator = createTanStackCacheCoordinator({ queryClient: client, queryRegistry: queryRegistry(), crossContext: harness.transport, }); await coordinator.invalidate([TOPIC_A]); expect( client.getQueryState(["resource-a", 1, "list"])?.isInvalidated, ).toBe(true); expect( client.getQueryState(["resource-b", 1, "list"])?.isInvalidated, ).toBe(false); expect(harness.publish).toHaveBeenCalledWith({ topic: TOPIC_A, topicVersion: 1, }); expect(JSON.stringify(harness.publish.mock.calls)).not.toContain( "resource-a", ); }); it("applies a remote hint without publishing an echo", async () => { const client = createClient(); const harness = crossContextHarness(); client.setQueryData(["resource-a", 1, "detail", "opaque"], { value: true, }); const coordinator = createTanStackCacheCoordinator({ queryClient: client, queryRegistry: queryRegistry(), crossContext: harness.transport, }); harness.deliver(TOPIC_A); await vi.waitFor(() => expect( client.getQueryState([ "resource-a", 1, "detail", "opaque", ])?.isInvalidated, ).toBe(true), ); expect(harness.publish).not.toHaveBeenCalled(); coordinator.dispose(); expect(harness.close).toHaveBeenCalledOnce(); }); it("coalesces remote hints while a local mutation lease is held", async () => { const client = createClient(); const harness = crossContextHarness(); client.setQueryData(["resource-a", 1, "list"], ["a"]); const invalidate = vi.spyOn(client, "invalidateQueries"); const coordinator = createTanStackCacheCoordinator({ queryClient: client, queryRegistry: queryRegistry(), crossContext: harness.transport, }); const lease = coordinator.beginMutation([TOPIC_A]); harness.deliver(TOPIC_A); harness.deliver(TOPIC_A); await Promise.resolve(); expect(invalidate).not.toHaveBeenCalled(); await lease.release(); expect(invalidate).toHaveBeenCalledTimes(1); expect(harness.publish).not.toHaveBeenCalled(); }); it("reconciles every registered namespace when a source sequence has a gap", async () => { const client = createClient(); const harness = crossContextHarness(); client.setQueryData(["resource-a", 1, "list"], ["a"]); client.setQueryData(["resource-b", 1, "list"], ["b"]); const coordinator = createTanStackCacheCoordinator({ queryClient: client, queryRegistry: queryRegistry(), crossContext: harness.transport, }); harness.deliver(TOPIC_A, "GAP"); await vi.waitFor(() => { expect( client.getQueryState(["resource-a", 1, "list"])?.isInvalidated, ).toBe(true); expect( client.getQueryState(["resource-b", 1, "list"])?.isInvalidated, ).toBe(true); }); }); it("fences remote delivery until a local reset has cancelled and cleared the cache", async () => { const client = createClient(); const harness = crossContextHarness(); client.setQueryData(["resource-a", 1, "list"], ["private-old-scope"]); let finishCancellation: (() => void) | undefined; const cancelQueries = vi .spyOn(client, "cancelQueries") .mockImplementation( () => new Promise((resolve) => { finishCancellation = resolve; }), ); const invalidate = vi.spyOn(client, "invalidateQueries"); const coordinator = createTanStackCacheCoordinator({ queryClient: client, queryRegistry: queryRegistry(), crossContext: harness.transport, }); const reset = coordinator.resetLocal(); await vi.waitFor(() => expect(cancelQueries).toHaveBeenCalledOnce()); harness.deliver(TOPIC_A); await Promise.resolve(); expect(invalidate).not.toHaveBeenCalled(); finishCancellation?.(); await reset; expect(client.getQueryData(["resource-a", 1, "list"])).toBeUndefined(); await vi.waitFor(() => expect(invalidate).toHaveBeenCalledOnce()); expect(harness.publish).not.toHaveBeenCalled(); }); it("rejects a mandatory local reset when query cancellation fails", async () => { const client = createClient(); client.setQueryData(["resource-a", 1, "list"], ["private-old-scope"]); vi.spyOn(client, "cancelQueries").mockRejectedValue( new Error("cancellation failed"), ); const clear = vi.spyOn(client, "clear"); const coordinator = createTanStackCacheCoordinator({ queryClient: client, queryRegistry: queryRegistry(), }); await expect(coordinator.resetLocal()).rejects.toThrow( "mandatory query cancellation failed", ); expect(clear).toHaveBeenCalledOnce(); }); it("composes with no installed query topics", () => { // ยง24.12: removing the reference feature leaves the common runtime intact. // A template with no installed feature has zero invalidation topics, which // is a legitimate state, not a configuration defect. const harness = crossContextHarness(); const coordinator = createTanStackCacheCoordinator({ queryClient: createClient(), queryRegistry: Object.freeze({}), crossContext: harness.transport, }); // A remote hint for a topic this build does not install is ignored, not fatal. harness.deliver("qinv.topic-a"); expect(() => coordinator.beginMutation([])).not.toThrow(); coordinator.dispose(); }); it("rejects an unregistered topic before opening a mutation lease", () => { const coordinator = createTanStackCacheCoordinator({ queryClient: createClient(), queryRegistry: queryRegistry(), }); expect(() => coordinator.beginMutation([ defineQueryInvalidationTopic("unknown-topic"), ]), ).toThrow( "Unregistered query invalidation topic", ); }); });