import { describe, expect, it } from "vitest"; import { defineQueryInvalidationTopic, indexInvalidationRegistry, indexInvalidationTopicVersions, type InvalidationRegistry, } from "../../src/contracts/query-invalidation.ts"; import { createBoundQueryKey, createQueryInvalidationPrefix, defineQueryNamespaceIdentity, queryNamespaceIdentityKey, type QueryNamespaceIdentity, } from "../../src/contracts/query-keys.ts"; describe("query namespace identity", () => { it("uses a canonical JSON tuple as the namespace identity key", () => { const namespace = defineQueryNamespaceIdentity("orders", 1); expect(queryNamespaceIdentityKey(namespace)).toBe( '["orders",1]', ); }); it("creates one namespace-first prefix for invalidation and bound keys", () => { const namespace = defineQueryNamespaceIdentity("orders", 1); const prefix = createQueryInvalidationPrefix(namespace); const key = createBoundQueryKey( namespace, "scope-fingerprint-0001", 3, "identity-token-0001", ); expect(prefix).toEqual(["query", 2, "orders", 1]); expect(key).toEqual([ "query", 2, "orders", 1, "scope-fingerprint-0001", 3, "identity-token-0001", ]); expect(key.slice(0, 4)).toEqual(prefix); expect(Object.isFrozen(prefix)).toBe(true); expect(Object.isFrozen(key)).toBe(true); }); it.each([ ["empty", ""], ["C0 control", "orders\u0000private"], ["DEL control", "orders\u007fprivate"], ["81 UTF-8 bytes", "가".repeat(27)], ])("rejects a $0 namespace ID", (_label, namespaceId) => { expect(() => defineQueryNamespaceIdentity(namespaceId, 1)).toThrow( /namespace identity is invalid/u, ); }); it.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1])( "rejects the non-positive-safe namespace version %s", (namespaceVersion) => { expect(() => defineQueryNamespaceIdentity("orders", namespaceVersion), ).toThrow(/namespace identity is invalid/u); }, ); it("rejects a non-positive definition version before creating a bound key", () => { const namespace = defineQueryNamespaceIdentity("orders", 1); expect(() => createBoundQueryKey( namespace, "scope-fingerprint-0001", 0, "identity-token-0001", ), ).toThrow(/definition version is invalid/u); }); }); describe("query invalidation registry", () => { const topic = defineQueryInvalidationTopic("qinv.orders.changed"); const orders = defineQueryNamespaceIdentity("orders", 1); const summaries = defineQueryNamespaceIdentity("order-summaries", 2); it("indexes every namespace identity connected to one topic", () => { const registry: InvalidationRegistry = { topics: [topic], namespaces: [orders, summaries], edges: [ { topicId: topic, namespace: orders }, { topicId: topic, namespace: summaries }, ], }; const index = indexInvalidationRegistry(registry); expect(index.namespacesForTopic.get(topic)).toEqual([orders, summaries]); expect(index.topicsForNamespace.get('["orders",1]')).toEqual([topic]); expect(index.topicsForNamespace.get('["order-summaries",2]')).toEqual([ topic, ]); }); it("snapshots a caller-owned namespace identity before indexing it", () => { const mutableNamespace = { namespaceId: "orders", namespaceVersion: 1, }; const index = indexInvalidationRegistry({ topics: [topic], namespaces: [mutableNamespace], edges: [{ topicId: topic, namespace: mutableNamespace }], }); mutableNamespace.namespaceId = "mutated-orders"; mutableNamespace.namespaceVersion = 2; const indexedNamespace = index.namespacesForTopic.get(topic)?.[0]; expect(indexedNamespace).toEqual({ namespaceId: "orders", namespaceVersion: 1, }); expect(Object.isFrozen(indexedNamespace)).toBe(true); expect( indexedNamespace && createQueryInvalidationPrefix(indexedNamespace), ).toEqual(["query", 2, "orders", 1]); }); it("rejects duplicate namespace identities even when they are separate objects", () => { const duplicate = defineQueryNamespaceIdentity("orders", 1); expect(() => indexInvalidationRegistry({ topics: [topic], namespaces: [orders, duplicate], edges: [{ topicId: topic, namespace: orders }], }), ).toThrow(/Duplicate invalidation namespace/u); }); it("projects one bounded transport version for every registered topic", () => { const registry: InvalidationRegistry = { topics: [topic], namespaces: [orders], edges: [{ topicId: topic, namespace: orders }], }; const versions = indexInvalidationTopicVersions(registry, [ { topicId: topic, topicVersion: 1 }, ]); expect([...versions]).toEqual([[topic, 1]]); expect(() => indexInvalidationTopicVersions(registry, [ { topicId: topic, topicVersion: 1 }, { topicId: topic, topicVersion: 2 }, ]), ).toThrow(/topic version registry/u); expect(() => indexInvalidationTopicVersions(registry, [])).toThrow( /topic version registry/u, ); }); it.each([ { label: "topic", registry: { topics: ["orders\u0000private"], namespaces: [orders], edges: [{ topicId: "orders\u0000private", namespace: orders }], }, }, { label: "namespace", registry: { topics: ["orders"], namespaces: [ { namespaceId: "orders\u001fprivate", namespaceVersion: 1, } as QueryNamespaceIdentity, ], edges: [ { topicId: "orders", namespace: { namespaceId: "orders\u001fprivate", namespaceVersion: 1, } as QueryNamespaceIdentity, }, ], }, }, ])("rejects control characters in a registry $label", ({ registry }) => { expect(() => indexInvalidationRegistry(registry)).toThrow(/is invalid/u); }); });