refactor: 리펙토링

This commit is contained in:
DongHyeonka
2026-08-01 19:39:59 +09:00
parent 9c959ea2a5
commit c6da03369c
171 changed files with 20329 additions and 782 deletions
+144
View File
@@ -19,6 +19,150 @@ export function defineQueryInvalidationTopic(
return value as QueryInvalidationTopic;
}
/**
* §12.2. Many-to-many topic/namespace registry.
*
* Topics stay opaque on the wire; the registry is what turns one received topic
* into the local namespaces that must revalidate. Bounds are checked at startup
* so a fan-out explosion cannot be introduced at runtime.
*/
export const INVALIDATION_REGISTRY_BOUNDS = Object.freeze({
maxTopics: 256,
maxNamespaces: 256,
maxEdges: 1_024,
maxTopicFanOut: 64,
maxNamespaceFanIn: 64,
maxIdBytes: 80,
});
export type InvalidationRegistryEdge = Readonly<{
topicId: string;
namespace: string;
}>;
export interface InvalidationRegistry {
readonly topics: readonly string[];
readonly namespaces: readonly string[];
readonly edges: readonly InvalidationRegistryEdge[];
}
export type InvalidationRegistryIndex = Readonly<{
namespacesForTopic: ReadonlyMap<string, readonly string[]>;
topicsForNamespace: ReadonlyMap<string, readonly string[]>;
}>;
function hasControlCharacter(value: string): boolean {
for (const character of value) {
const codePoint = character.codePointAt(0) ?? 0;
if (codePoint <= 0x1f || codePoint === 0x7f) return true;
}
return false;
}
/**
* Rejects duplicates, orphan topics and orphan namespaces at startup. An edge
* that points at an unregistered endpoint is a composition defect, not a
* runtime condition to be tolerated.
*/
export function indexInvalidationRegistry(
registry: InvalidationRegistry,
): InvalidationRegistryIndex {
const bounds = INVALIDATION_REGISTRY_BOUNDS;
const encoder = new TextEncoder();
const assertId = (value: string, label: string) => {
if (
typeof value !== "string" ||
value.length === 0 ||
hasControlCharacter(value) ||
encoder.encode(value).byteLength > bounds.maxIdBytes
) {
throw new TypeError(`Invalidation registry ${label} is invalid.`);
}
};
if (
registry.topics.length > bounds.maxTopics ||
registry.namespaces.length > bounds.maxNamespaces ||
registry.edges.length > bounds.maxEdges
) {
throw new TypeError("Invalidation registry exceeds its bounds.");
}
const topics = new Set<string>();
for (const topic of registry.topics) {
assertId(topic, "topic");
if (topics.has(topic)) {
throw new TypeError(`Duplicate invalidation topic: ${topic}`);
}
topics.add(topic);
}
const namespaces = new Set<string>();
for (const namespace of registry.namespaces) {
assertId(namespace, "namespace");
if (namespaces.has(namespace)) {
throw new TypeError(`Duplicate invalidation namespace: ${namespace}`);
}
namespaces.add(namespace);
}
const namespacesForTopic = new Map<string, string[]>();
const topicsForNamespace = new Map<string, string[]>();
const seenEdges = new Map<string, Set<string>>();
for (const edge of registry.edges) {
if (!topics.has(edge.topicId) || !namespaces.has(edge.namespace)) {
throw new TypeError("Invalidation edge references an unknown endpoint.");
}
const seenNamespaces = seenEdges.get(edge.topicId) ?? new Set<string>();
if (seenNamespaces.has(edge.namespace)) {
throw new TypeError("Duplicate invalidation edge.");
}
seenNamespaces.add(edge.namespace);
seenEdges.set(edge.topicId, seenNamespaces);
const fanOut = namespacesForTopic.get(edge.topicId) ?? [];
fanOut.push(edge.namespace);
if (fanOut.length > bounds.maxTopicFanOut) {
throw new TypeError(`Invalidation topic fan-out exceeded: ${edge.topicId}`);
}
namespacesForTopic.set(edge.topicId, fanOut);
const fanIn = topicsForNamespace.get(edge.namespace) ?? [];
fanIn.push(edge.topicId);
if (fanIn.length > bounds.maxNamespaceFanIn) {
throw new TypeError(
`Invalidation namespace fan-in exceeded: ${edge.namespace}`,
);
}
topicsForNamespace.set(edge.namespace, fanIn);
}
for (const topic of topics) {
if (!namespacesForTopic.has(topic)) {
throw new TypeError(`Orphan invalidation topic: ${topic}`);
}
}
for (const namespace of namespaces) {
if (!topicsForNamespace.has(namespace)) {
throw new TypeError(`Orphan invalidation namespace: ${namespace}`);
}
}
return Object.freeze({
namespacesForTopic: new Map(
[...namespacesForTopic].map(([key, value]) => [
key,
Object.freeze([...value]) as readonly string[],
]),
),
topicsForNamespace: new Map(
[...topicsForNamespace].map(([key, value]) => [
key,
Object.freeze([...value]) as readonly string[],
]),
),
});
}
export type QueryMutationLease = Readonly<{
/**
* Releases one local mutation fence. Remote hints coalesced while the fence