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, indexInvalidationRegistry, } from "../../src/contracts/query-invalidation.ts"; import { createRuntimeIdentityRegistry, defineQueryNamespaceIdentity, type QueryNamespaceIdentity, } from "../../src/contracts/query-keys.ts"; import { bindQuery } from "../../src/contracts/server-state.ts"; import type { CacheScopeSnapshot } from "../../src/contracts/server-state-scope.ts"; const TOPIC_A = defineQueryInvalidationTopic("qinv.topic-a"); const TOPIC_B = defineQueryInvalidationTopic("qinv.topic-b"); const NAMESPACE_A = defineQueryNamespaceIdentity("resource-a", 1); const NAMESPACE_B = defineQueryNamespaceIdentity("resource-b", 1); const NAMESPACE_C = defineQueryNamespaceIdentity("resource-c", 1); function invalidationIndex() { return indexInvalidationRegistry({ topics: [TOPIC_A, TOPIC_B], namespaces: [NAMESPACE_A, NAMESPACE_B, NAMESPACE_C], edges: [ { topicId: TOPIC_A, namespace: NAMESPACE_A }, { topicId: TOPIC_A, namespace: NAMESPACE_B }, { topicId: TOPIC_B, namespace: NAMESPACE_B }, { topicId: TOPIC_B, namespace: NAMESPACE_C }, ], }); } function topicVersions() { return new Map([ [TOPIC_A, 1], [TOPIC_B, 1], ]); } function realBoundQueryKey(namespace: QueryNamespaceIdentity) { const scope: CacheScopeSnapshot = { generation: 1, fingerprint: "scope-fingerprint-0001", identities: createRuntimeIdentityRegistry({ tokenFactory: () => `identity-token-${namespace.namespaceId}`, }), signal: new AbortController().signal, isCurrent: () => true, }; return bindQuery( { definitionId: `${namespace.namespaceId}-query-v1`, definitionVersion: 1, owner: "platform-test", namespace: namespace.namespaceId, namespaceVersion: namespace.namespaceVersion, operationId: `GET_${namespace.namespaceId.toUpperCase()}`, profileId: "DETAIL_STANDARD", measureResult: () => ({ itemCount: 1, estimatedBytes: 8 }), execute: async () => ({ ok: true as const, value: namespace.namespaceId }), }, { selected: namespace.namespaceId }, scope, ).queryKey; } function crossContextHarness() { let listener: | ((delivery: CrossContextInvalidationDelivery) => void) | undefined; const publish = vi.fn( (_event: { topic: string; topicVersion: number }) => ({ 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("invalidates every real V2 key connected to one local topic and publishes only topic identity", async () => { const client = createClient(); const harness = crossContextHarness(); const keyA = realBoundQueryKey(NAMESPACE_A); const keyB = realBoundQueryKey(NAMESPACE_B); const unrelatedKey = realBoundQueryKey(NAMESPACE_C); client.setQueryData(keyA, ["a"]); client.setQueryData(keyB, ["b"]); client.setQueryData(unrelatedKey, ["c"]); const dependencies = { queryClient: client, invalidationIndex: invalidationIndex(), topicVersions: topicVersions(), crossContext: harness.transport, }; const coordinator = createTanStackCacheCoordinator(dependencies); await coordinator.invalidate([TOPIC_A]); expect(client.getQueryState(keyA)?.isInvalidated).toBe(true); expect(client.getQueryState(keyB)?.isInvalidated).toBe(true); expect(client.getQueryState(unrelatedKey)?.isInvalidated).toBe(false); expect(harness.publish).toHaveBeenCalledOnce(); expect(harness.publish.mock.calls[0]?.[0]).toEqual({ topic: TOPIC_A, topicVersion: 1, }); }); it("invalidates every real V2 key connected to one remote topic without echoing it", async () => { const client = createClient(); const harness = crossContextHarness(); const keyA = realBoundQueryKey(NAMESPACE_A); const keyB = realBoundQueryKey(NAMESPACE_B); const unrelatedKey = realBoundQueryKey(NAMESPACE_C); client.setQueryData(keyA, ["a"]); client.setQueryData(keyB, ["b"]); client.setQueryData(unrelatedKey, ["c"]); const dependencies = { queryClient: client, invalidationIndex: invalidationIndex(), topicVersions: topicVersions(), crossContext: harness.transport, }; const coordinator = createTanStackCacheCoordinator(dependencies); harness.deliver(TOPIC_A); await vi.waitFor(() => { expect(client.getQueryState(keyA)?.isInvalidated).toBe(true); expect(client.getQueryState(keyB)?.isInvalidated).toBe(true); }); expect(client.getQueryState(unrelatedKey)?.isInvalidated).toBe(false); expect(harness.publish).not.toHaveBeenCalled(); }); it("invalidates each real V2 namespace once when a sequence gap spans overlapping topics", async () => { const client = createClient(); const harness = crossContextHarness(); const keyA = realBoundQueryKey(NAMESPACE_A); const keyB = realBoundQueryKey(NAMESPACE_B); const keyC = realBoundQueryKey(NAMESPACE_C); client.setQueryData(keyA, ["a"]); client.setQueryData(keyB, ["b"]); client.setQueryData(keyC, ["c"]); const invalidate = vi.spyOn(client, "invalidateQueries"); const dependencies = { queryClient: client, invalidationIndex: invalidationIndex(), topicVersions: topicVersions(), crossContext: harness.transport, }; createTanStackCacheCoordinator(dependencies); harness.deliver(TOPIC_A, "GAP"); await vi.waitFor(() => { expect(client.getQueryState(keyA)?.isInvalidated).toBe(true); expect(client.getQueryState(keyB)?.isInvalidated).toBe(true); expect(client.getQueryState(keyC)?.isInvalidated).toBe(true); }); expect(invalidate).toHaveBeenCalledTimes(3); }); 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, invalidationIndex: invalidationIndex(), topicVersions: topicVersions(), 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(2); expect(harness.publish).not.toHaveBeenCalled(); }); 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, invalidationIndex: invalidationIndex(), topicVersions: topicVersions(), 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).toHaveBeenCalledTimes(2)); 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, invalidationIndex: invalidationIndex(), topicVersions: topicVersions(), }); 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(), invalidationIndex: indexInvalidationRegistry({ topics: [], namespaces: [], edges: [], }), topicVersions: new Map(), 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(), invalidationIndex: invalidationIndex(), topicVersions: topicVersions(), }); expect(() => coordinator.beginMutation([ defineQueryInvalidationTopic("unknown-topic"), ]), ).toThrow( "Unregistered query invalidation topic", ); }); });