Files
tech-log-frontend/src/contracts/query-keys.ts
T

316 lines
9.1 KiB
TypeScript

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
| number
| string
| readonly CanonicalValue[]
| Readonly<{ [key: string]: CanonicalValue }>;
const DEFAULT_LIMITS = Object.freeze({
maxDepth: 12,
maxNodes: 512,
maxStringBytes: 2_048,
maxEncodedBytes: 16_384,
});
export function canonicalize(value: unknown): CanonicalValue {
const seen = new WeakSet<object>();
let nodes = 0;
const encoder = new TextEncoder();
function visit(candidate: unknown, depth: number): CanonicalValue {
nodes += 1;
if (nodes > DEFAULT_LIMITS.maxNodes || depth > DEFAULT_LIMITS.maxDepth) {
throw new TypeError("Query identity exceeds its structural budget.");
}
if (
candidate === null ||
typeof candidate === "boolean" ||
(typeof candidate === "number" &&
Number.isFinite(candidate) &&
!Object.is(candidate, -0))
) {
return candidate;
}
if (typeof candidate === "string") {
if (encoder.encode(candidate).byteLength > DEFAULT_LIMITS.maxStringBytes) {
throw new TypeError("Query identity string exceeds its byte budget.");
}
return candidate;
}
if (!candidate || typeof candidate !== "object") {
throw new TypeError("Query identity contains a non-canonical value.");
}
if (seen.has(candidate)) {
throw new TypeError("Query identity contains a cycle or shared reference.");
}
seen.add(candidate);
if (Array.isArray(candidate)) {
for (let index = 0; index < candidate.length; index += 1) {
if (!Object.hasOwn(candidate, index)) {
throw new TypeError("Query identity contains a sparse array.");
}
}
return Object.freeze(candidate.map((item) => visit(item, depth + 1)));
}
const prototype = Object.getPrototypeOf(candidate);
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError("Query identity requires plain objects.");
}
const descriptors = Object.getOwnPropertyDescriptors(candidate);
const output: Record<string, CanonicalValue> = Object.create(null);
for (const key of Object.keys(descriptors).sort()) {
if (key === "__proto__" || key === "prototype" || key === "constructor") {
throw new TypeError("Query identity contains a forbidden key.");
}
const descriptor = descriptors[key];
if (!descriptor || !("value" in descriptor)) {
throw new TypeError("Query identity contains an accessor.");
}
output[key] = visit(descriptor.value, depth + 1);
}
return Object.freeze(output);
}
const result = visit(value, 0);
if (encoder.encode(JSON.stringify(result)).byteLength > DEFAULT_LIMITS.maxEncodedBytes) {
throw new TypeError("Query identity exceeds its encoded byte budget.");
}
return result;
}
export type RuntimeIdentityBinding = Readonly<{
token: string;
acquire(): void;
release(): void;
}>;
export type RuntimeIdentityRegistry = Readonly<{
intern(value: unknown): RuntimeIdentityBinding;
close(): void;
inspect(): Readonly<{
entries: number;
canonicalBytes: number;
activeLeases: number;
closed: boolean;
}>;
}>;
type IdentityRow = {
canonical: string;
canonicalBytes: number;
token: string;
refCount: number;
touched: number;
};
export function createRuntimeIdentityRegistry(
options: Readonly<{
maxEntries?: number;
maxCanonicalBytes?: number;
tokenFactory?: () => string;
}> = {},
): RuntimeIdentityRegistry {
const maxEntries = options.maxEntries ?? 4_096;
const maxCanonicalBytes = options.maxCanonicalBytes ?? 4 * 1024 * 1024;
const tokenFactory =
options.tokenFactory ??
(() => {
if (
typeof crypto === "undefined" ||
typeof crypto.randomUUID !== "function"
) {
throw new TypeError("Secure runtime identity generation is unavailable.");
}
return crypto.randomUUID();
});
const byCanonical = new Map<string, IdentityRow>();
const byToken = new Map<string, IdentityRow>();
let totalCanonicalBytes = 0;
let sequence = 0;
let closed = false;
function evictAvailable(requiredBytes: number): void {
const candidates = [...byCanonical.values()]
.filter((row) => row.refCount === 0)
.sort((left, right) => left.touched - right.touched);
for (const row of candidates) {
if (
byCanonical.size < maxEntries &&
totalCanonicalBytes + requiredBytes <= maxCanonicalBytes
) {
return;
}
byCanonical.delete(row.canonical);
byToken.delete(row.token);
totalCanonicalBytes -= row.canonicalBytes;
}
}
return Object.freeze({
intern(value): RuntimeIdentityBinding {
if (closed) throw new TypeError("Runtime identity registry is closed.");
const canonical = JSON.stringify(canonicalize(value));
const canonicalBytes = new TextEncoder().encode(canonical).byteLength;
let row = byCanonical.get(canonical);
if (!row) {
evictAvailable(canonicalBytes);
if (
byCanonical.size >= maxEntries ||
totalCanonicalBytes + canonicalBytes > maxCanonicalBytes
) {
throw new TypeError("Runtime identity capacity exceeded.");
}
let token = "";
for (let attempt = 0; attempt < 8; attempt += 1) {
const candidate = tokenFactory();
if (
/^[A-Za-z0-9._:-]{16,128}$/.test(candidate) &&
!byToken.has(candidate)
) {
token = candidate;
break;
}
}
if (!token) {
throw new TypeError("Runtime identity token collision.");
}
row = {
canonical,
canonicalBytes,
token,
refCount: 0,
touched: sequence++,
};
byCanonical.set(canonical, row);
byToken.set(token, row);
totalCanonicalBytes += canonicalBytes;
}
row.touched = sequence++;
let leaseCount = 0;
return Object.freeze({
token: row.token,
acquire() {
if (closed) return;
leaseCount += 1;
row.refCount += 1;
row.touched = sequence++;
},
release() {
if (leaseCount === 0) return;
leaseCount -= 1;
row.refCount = Math.max(0, row.refCount - 1);
row.touched = sequence++;
},
});
},
close() {
closed = true;
byCanonical.clear();
byToken.clear();
totalCanonicalBytes = 0;
},
inspect() {
return Object.freeze({
entries: byCanonical.size,
canonicalBytes: totalCanonicalBytes,
activeLeases: [...byCanonical.values()].reduce(
(total, row) => total + row.refCount,
0,
),
closed,
});
},
});
}
const defaultIdentityRegistry = createRuntimeIdentityRegistry();
export function runtimeIdentityToken(value: unknown): string {
return defaultIdentityRegistry.intern(value).token;
}