fix: index many-to-many query invalidation
This commit is contained in:
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user