fix: align bound query keys with invalidation prefixes

This commit is contained in:
DongHyeonka
2026-08-01 22:11:04 +09:00
parent 92c3d438ab
commit 853c2e3f30
4 changed files with 207 additions and 8 deletions
+84
View File
@@ -1,6 +1,90 @@
export const QUERY_REGISTRY: Readonly<Record<string, readonly unknown[]>> =
Object.freeze({});
export const QUERY_KEY_SCHEMA_VERSION = 2 as const;
export const QUERY_NAMESPACE_ID_MAX_BYTES = 80 as const;
export type QueryNamespaceIdentity = Readonly<{
namespaceId: string;
namespaceVersion: number;
}>;
function hasControlCharacter(value: string): boolean {
for (const character of value) {
const codePoint = character.codePointAt(0) ?? 0;
if (
codePoint <= 0x1f ||
(codePoint >= 0x7f && codePoint <= 0x9f)
) {
return true;
}
}
return false;
}
function assertQueryNamespaceIdentity(
namespace: QueryNamespaceIdentity,
): void {
if (
!namespace ||
typeof namespace !== "object" ||
typeof namespace.namespaceId !== "string" ||
namespace.namespaceId.length === 0 ||
hasControlCharacter(namespace.namespaceId) ||
new TextEncoder().encode(namespace.namespaceId).byteLength >
QUERY_NAMESPACE_ID_MAX_BYTES ||
!Number.isSafeInteger(namespace.namespaceVersion) ||
namespace.namespaceVersion < 1
) {
throw new TypeError("Query namespace identity is invalid.");
}
}
export function defineQueryNamespaceIdentity(
namespaceId: string,
namespaceVersion: number,
): QueryNamespaceIdentity {
const namespace = { namespaceId, namespaceVersion };
assertQueryNamespaceIdentity(namespace);
return Object.freeze(namespace);
}
export function queryNamespaceIdentityKey(
namespace: QueryNamespaceIdentity,
): string {
assertQueryNamespaceIdentity(namespace);
return JSON.stringify([namespace.namespaceId, namespace.namespaceVersion]);
}
export function createQueryInvalidationPrefix(
namespace: QueryNamespaceIdentity,
) {
assertQueryNamespaceIdentity(namespace);
return Object.freeze([
"query",
QUERY_KEY_SCHEMA_VERSION,
namespace.namespaceId,
namespace.namespaceVersion,
] as const);
}
export function createBoundQueryKey(
namespace: QueryNamespaceIdentity,
scopeFingerprint: string,
definitionVersion: number,
identityToken: string,
) {
if (!Number.isSafeInteger(definitionVersion) || definitionVersion < 1) {
throw new TypeError("Query definition version is invalid.");
}
return Object.freeze([
...createQueryInvalidationPrefix(namespace),
scopeFingerprint,
definitionVersion,
identityToken,
] as const);
}
export type CanonicalValue =
| null
| boolean
+12 -7
View File
@@ -1,6 +1,10 @@
import type { Result } from "../application/result.ts";
import type { QueryInvalidationTopic } from "./query-invalidation.ts";
import type { RuntimeIdentityBinding } from "./query-keys.ts";
import {
createBoundQueryKey,
defineQueryNamespaceIdentity,
type RuntimeIdentityBinding,
} from "./query-keys.ts";
import type { CacheScopeSnapshot } from "./server-state-scope.ts";
/**
@@ -176,20 +180,21 @@ export function bindQuery<Input, Value>(
`Query definition requires measureResult: ${definition.definitionId}`,
);
}
const namespace = defineQueryNamespaceIdentity(
definition.namespace,
definition.namespaceVersion,
);
const identity = scope.identities.intern(input);
return Object.freeze({
definitionId: definition.definitionId,
// §10.7. Opaque runtime identity only. No raw account/resource ID, URL,
// filter object, document or cursor ever enters a query key.
queryKey: Object.freeze([
"query",
1,
queryKey: createBoundQueryKey(
namespace,
scope.fingerprint,
definition.namespace,
definition.namespaceVersion,
definition.definitionVersion,
identity.token,
]),
),
profile: getServerStateProfile(definition.profileId),
identity,
scope,