fix: index many-to-many query invalidation

This commit is contained in:
DongHyeonka
2026-08-01 23:17:09 +09:00
parent 853c2e3f30
commit 73a50426d6
18 changed files with 574 additions and 325 deletions
+88 -12
View File
@@ -1,24 +1,30 @@
import { describe, expect, it } from "vitest";
import { indexInvalidationRegistry } from "../../src/contracts/query-invalidation.ts";
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("reference-resource", 1);
const namespace = defineQueryNamespaceIdentity("orders", 1);
expect(queryNamespaceIdentityKey(namespace)).toBe(
'["reference-resource",1]',
'["orders",1]',
);
});
it("creates one namespace-first prefix for invalidation and bound keys", () => {
const namespace = defineQueryNamespaceIdentity("reference-resource", 1);
const namespace = defineQueryNamespaceIdentity("orders", 1);
const prefix = createQueryInvalidationPrefix(namespace);
const key = createBoundQueryKey(
namespace,
@@ -27,11 +33,11 @@ describe("query namespace identity", () => {
"identity-token-0001",
);
expect(prefix).toEqual(["query", 2, "reference-resource", 1]);
expect(prefix).toEqual(["query", 2, "orders", 1]);
expect(key).toEqual([
"query",
2,
"reference-resource",
"orders",
1,
"scope-fingerprint-0001",
3,
@@ -57,13 +63,13 @@ describe("query namespace identity", () => {
"rejects the non-positive-safe namespace version %s",
(namespaceVersion) => {
expect(() =>
defineQueryNamespaceIdentity("reference-resource", namespaceVersion),
defineQueryNamespaceIdentity("orders", namespaceVersion),
).toThrow(/namespace identity is invalid/u);
},
);
it("rejects a non-positive definition version before creating a bound key", () => {
const namespace = defineQueryNamespaceIdentity("reference-resource", 1);
const namespace = defineQueryNamespaceIdentity("orders", 1);
expect(() =>
createBoundQueryKey(
@@ -77,21 +83,91 @@ describe("query namespace identity", () => {
});
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("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" }],
namespaces: [orders],
edges: [{ topicId: "orders\u0000private", namespace: orders }],
},
},
{
label: "namespace",
registry: {
topics: ["orders"],
namespaces: ["orders\u001fprivate"],
edges: [{ topicId: "orders", namespace: "orders\u001fprivate" }],
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 }) => {