chore: initialize from frontend template 4dc033c

This commit is contained in:
DongHyeonka
2026-08-13 18:23:26 +09:00
commit 40107eec84
897 changed files with 234824 additions and 0 deletions
@@ -0,0 +1,123 @@
import type { CacheScopeSnapshot } from "../../contracts/server-state-scope.ts";
export type ConditionalValidatorBinding = Readonly<{
definitionId: string;
identityToken: string;
representationVersion: number;
scope: CacheScopeSnapshot;
}>;
export type ConditionalValidatorStore = Readonly<{
install(
binding: ConditionalValidatorBinding,
validator: string,
cacheRevision: number,
): boolean;
prepare(
binding: ConditionalValidatorBinding,
cacheRevision: number,
): string | null;
acceptNotModified(
binding: ConditionalValidatorBinding,
cacheRevision: number,
hasMappedValue: boolean,
): boolean;
remove(binding: ConditionalValidatorBinding): void;
clear(): void;
}>;
type ValidatorRow = {
validator: string;
cacheRevision: number;
generation: number;
};
export function createConditionalValidatorStore(
maxEntries = 1_024,
): ConditionalValidatorStore {
if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {
throw new TypeError("Invalid conditional validator capacity.");
}
const rows = new Map<string, ValidatorRow>();
function key(binding: ConditionalValidatorBinding): string | null {
if (
!binding.scope.isCurrent() ||
!binding.definitionId ||
!/^[A-Za-z0-9._:-]{16,128}$/.test(binding.identityToken) ||
!Number.isSafeInteger(binding.representationVersion) ||
binding.representationVersion < 1
) {
return null;
}
return [
binding.scope.fingerprint,
binding.definitionId,
binding.identityToken,
binding.representationVersion,
].join(":");
}
return Object.freeze({
install(binding, validator, cacheRevision) {
const selectedKey = key(binding);
if (
!selectedKey ||
!isSafeEntityTag(validator) ||
!Number.isSafeInteger(cacheRevision) ||
cacheRevision < 0
) {
return false;
}
if (!rows.has(selectedKey) && rows.size >= maxEntries) return false;
rows.set(selectedKey, {
validator,
cacheRevision,
generation: binding.scope.generation,
});
return true;
},
prepare(binding, cacheRevision) {
const selectedKey = key(binding);
if (!selectedKey) return null;
const row = rows.get(selectedKey);
return row &&
row.generation === binding.scope.generation &&
row.cacheRevision === cacheRevision
? row.validator
: null;
},
acceptNotModified(binding, cacheRevision, hasMappedValue) {
const selectedKey = key(binding);
if (!selectedKey || !hasMappedValue) return false;
const row = rows.get(selectedKey);
return Boolean(
row &&
row.generation === binding.scope.generation &&
row.cacheRevision === cacheRevision,
);
},
remove(binding) {
const selectedKey = key(binding);
if (selectedKey) rows.delete(selectedKey);
},
clear() {
rows.clear();
},
});
}
function isSafeEntityTag(value: string): boolean {
if (value.length < 3 || value.length > 256) return false;
const opaque = value.startsWith('W/"')
? value.slice(3, -1)
: value.startsWith('"')
? value.slice(1, -1)
: null;
if (opaque === null || !value.endsWith('"')) return false;
return [...opaque].every((character) => {
const code = character.codePointAt(0) ?? 0;
return code === 0x21 || (code >= 0x23 && code <= 0x7e) ||
(code >= 0x80 && code <= 0xff);
});
}
@@ -0,0 +1,139 @@
import type { Result } from "../../application/result.ts";
import type {
CursorPage,
CursorPaginationProfile,
CursorPaginationRuntime,
} from "../../contracts/cursor-pagination.ts";
import { createFailure } from "../../contracts/errors.ts";
export function createCursorPaginationRuntime<Value>(dependencies: Readonly<{
definitionId: string;
profile: CursorPaginationProfile;
loadPage(
cursor: string | null,
context: Readonly<{ signal?: AbortSignal }>,
): Promise<Result<CursorPage<Value>>>;
}>): CursorPaginationRuntime<Value> {
validateProfile(dependencies.profile);
return Object.freeze({
async loadAll(context) {
const items: Value[] = [];
const cursors = new Set<string>();
let cursor: string | null = null;
let snapshot: string | null | undefined;
for (
let pageIndex = 0;
pageIndex < dependencies.profile.maxPages;
pageIndex += 1
) {
if (context.signal?.aborted) {
return failure("REQUEST_ABORTED", "PAGINATION_ABORTED");
}
const result = await dependencies.loadPage(cursor, context);
if (!result.ok) return result;
const page = result.value;
if (!isValidPage(page, dependencies.profile)) {
return failure(
"PAGINATION_CONTRACT_VIOLATION",
"PAGINATION_PAGE_INVALID",
);
}
if (snapshot === undefined) {
snapshot = page.snapshotToken;
} else if (snapshot !== page.snapshotToken) {
return failure(
"PAGINATION_CONTRACT_VIOLATION",
"PAGINATION_SNAPSHOT_CHANGED",
);
}
items.push(...page.items);
if (
items.length > dependencies.profile.maxTotalItems ||
estimatedBytes(items) > dependencies.profile.maxEstimatedBytes
) {
return failure(
"RESULT_LIMIT_EXCEEDED",
"PAGINATION_RESULT_LIMIT",
);
}
if (!page.hasMore) return { ok: true, value: Object.freeze(items) };
const nextCursor = page.nextCursor;
if (!nextCursor || cursors.has(nextCursor)) {
return failure(
"PAGINATION_CONTRACT_VIOLATION",
"PAGINATION_CURSOR_LOOP",
);
}
cursors.add(nextCursor);
cursor = nextCursor;
}
return failure(
"RESULT_LIMIT_EXCEEDED",
"PAGINATION_PAGE_LIMIT",
);
},
});
function failure(
kind:
| "PAGINATION_CONTRACT_VIOLATION"
| "RESULT_LIMIT_EXCEEDED"
| "REQUEST_ABORTED",
code: string,
) {
return {
ok: false as const,
error: createFailure(kind, dependencies.definitionId, 0, { code }),
};
}
}
function validateProfile(profile: CursorPaginationProfile): void {
if (
!profile.profileId ||
!Number.isSafeInteger(profile.maxPages) ||
profile.maxPages < 1 ||
profile.maxPages > 100 ||
!Number.isSafeInteger(profile.maxTotalItems) ||
profile.maxTotalItems < 1 ||
!Number.isSafeInteger(profile.maxEstimatedBytes) ||
profile.maxEstimatedBytes < 1 ||
!Number.isSafeInteger(profile.maxCursorBytes) ||
profile.maxCursorBytes < 1 ||
profile.maxCursorBytes > 4_096
) {
throw new TypeError("Invalid cursor pagination profile.");
}
}
function isValidPage<Value>(
page: CursorPage<Value>,
profile: CursorPaginationProfile,
): boolean {
const encoder = new TextEncoder();
return (
Boolean(page) &&
Array.isArray(page.items) &&
typeof page.hasMore === "boolean" &&
page.hasMore === (page.nextCursor !== null) &&
(page.nextCursor === null ||
(typeof page.nextCursor === "string" &&
page.nextCursor.length > 0 &&
encoder.encode(page.nextCursor).byteLength <=
profile.maxCursorBytes)) &&
(page.snapshotToken === null ||
(typeof page.snapshotToken === "string" &&
page.snapshotToken.length > 0 &&
encoder.encode(page.snapshotToken).byteLength <=
profile.maxCursorBytes)) &&
(profile.allowSparsePage || !page.hasMore || page.items.length > 0)
);
}
function estimatedBytes(value: unknown): number {
try {
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
} catch {
return Number.POSITIVE_INFINITY;
}
}
@@ -0,0 +1,202 @@
import type { AuthSessionPort } from "../../application/ports/auth-session-port.ts";
import type { QueryInvalidationCoordinator } from "../../contracts/query-invalidation.ts";
import {
createRuntimeIdentityRegistry,
type RuntimeIdentityRegistry,
} from "../../contracts/query-keys.ts";
import type {
CacheScopeSnapshot,
ClientScopeLifecycleEvent,
ClientScopePhase,
ServerStateScopeRuntime,
} from "../../contracts/server-state-scope.ts";
/**
* Steps 4-11 of §10.6 that this runtime does not own directly. Each optional
* capability registers its own closer so the ordering lives in one place rather
* than being re-derived by every subsystem.
*/
export type ScopeResetParticipant = Readonly<{
/** Lower runs earlier; the §10.6 step number is used as the rank. */
order: number;
label: string;
close(): void | Promise<void>;
}>;
export type ServerStateScopeDependencies = Readonly<{
session: Pick<AuthSessionPort, "subscribe">;
queryInvalidation: Pick<QueryInvalidationCoordinator, "resetLocal">;
tokenFactory?: () => string;
participants?: readonly ScopeResetParticipant[];
activateNextGeneration?: () => void | Promise<void>;
}>;
export function createServerStateScopeRuntime(
dependencies: ServerStateScopeDependencies,
): ServerStateScopeRuntime {
const listeners = new Set<() => void>();
const lifecycleListeners = new Set<
(event: ClientScopeLifecycleEvent) => void
>();
const participants = [...(dependencies.participants ?? [])].sort(
(left, right) => left.order - right.order,
);
let generation = 1;
let identities = newIdentityRegistry(dependencies.tokenFactory);
let fingerprint = scopeFingerprint(dependencies.tokenFactory);
let generationLifetime = new AbortController();
let phase: ClientScopePhase = "READY";
let disposed = false;
let resetChain = Promise.resolve();
function createSnapshot(): CacheScopeSnapshot {
const capturedGeneration = generation;
const capturedIdentities = identities;
return Object.freeze({
generation: capturedGeneration,
fingerprint,
identities: capturedIdentities,
signal: generationLifetime.signal,
isCurrent: () =>
!disposed &&
phase === "READY" &&
generation === capturedGeneration &&
identities === capturedIdentities,
});
}
let currentSnapshot = createSnapshot();
function publishLifecycle(event: ClientScopeLifecycleEvent): void {
for (const listener of [...lifecycleListeners]) {
try {
listener(event);
} catch {
// One subscriber defect cannot stop the fence from propagating.
}
}
}
function publishSnapshot(): void {
for (const listener of [...listeners]) {
try {
listener();
} catch {
// Subscriber defects are isolated from the mandatory reset sequence.
}
}
}
const unsubscribe = dependencies.session.subscribe(() => {
if (disposed) return;
const previousIdentities = identities;
const previousGeneration = generation;
// §10.6 steps 1-3 are synchronous: increment the generation, invalidate the
// old snapshot, publish FENCED. Nothing between here and READY may render a
// value that belonged to the previous identity.
generationLifetime.abort();
const targetGeneration = ++generation;
phase = "FENCED";
currentSnapshot = createSnapshot();
publishLifecycle(
Object.freeze({ kind: "FENCED" as const, previousGeneration }),
);
publishSnapshot();
resetChain = resetChain
.catch(() => {})
.then(async () => {
let failed = false;
// Steps 4-11: close admission, cancel and clear, release leases.
for (const participant of participants) {
try {
await participant.close();
} catch {
failed = true;
}
}
try {
await dependencies.queryInvalidation.resetLocal();
} catch {
failed = true;
}
previousIdentities.close();
if (disposed || generation !== targetGeneration) return;
if (!failed) {
try {
await dependencies.activateNextGeneration?.();
} catch {
failed = true;
}
}
if (disposed || generation !== targetGeneration) return;
if (failed) {
phase = "FAILED";
currentSnapshot = createSnapshot();
publishLifecycle(
Object.freeze({
kind: "FAILED" as const,
generation: targetGeneration,
}),
);
publishSnapshot();
return;
}
// Steps 12-15: new identity registry, READY, notify, reopen admission.
identities = newIdentityRegistry(dependencies.tokenFactory);
fingerprint = scopeFingerprint(dependencies.tokenFactory);
generationLifetime = new AbortController();
phase = "READY";
currentSnapshot = createSnapshot();
publishLifecycle(
Object.freeze({ kind: "READY" as const, snapshot: currentSnapshot }),
);
publishSnapshot();
});
});
return Object.freeze({
getSnapshot: () => currentSnapshot,
getPhase: () => phase,
subscribe(listener: () => void) {
listeners.add(listener);
return () => listeners.delete(listener);
},
subscribeLifecycle(listener: (event: ClientScopeLifecycleEvent) => void) {
lifecycleListeners.add(listener);
return () => lifecycleListeners.delete(listener);
},
dispose() {
if (disposed) return;
disposed = true;
phase = "DISPOSED";
generationLifetime.abort();
unsubscribe();
publishLifecycle(Object.freeze({ kind: "DISPOSED" as const }));
listeners.clear();
lifecycleListeners.clear();
identities.close();
},
});
}
function newIdentityRegistry(
tokenFactory: (() => string) | undefined,
): RuntimeIdentityRegistry {
return createRuntimeIdentityRegistry({
...(tokenFactory ? { tokenFactory } : {}),
});
}
function scopeFingerprint(tokenFactory: (() => string) | undefined): string {
const candidate = tokenFactory?.() ?? crypto.randomUUID();
if (!/^[A-Za-z0-9._:-]{16,128}$/.test(candidate)) {
throw new TypeError("Invalid cache scope fingerprint.");
}
return candidate;
}
@@ -0,0 +1,319 @@
import type { QueryClient } from "@tanstack/react-query";
import type { DiagnosticsPort } from "../../application/ports/diagnostics-port.ts";
import type {
InvalidationRegistryIndex,
QueryInvalidationCoordinator,
QueryInvalidationTopic,
QueryMutationLease,
} from "../../contracts/query-invalidation.ts";
import { INVALIDATION_REGISTRY_BOUNDS } from "../../contracts/query-invalidation.ts";
import {
createQueryInvalidationPrefix,
queryNamespaceIdentityKey,
type QueryNamespaceIdentity,
} from "../../contracts/query-keys.ts";
import type {
BrowserCrossContextInvalidation,
CrossContextInvalidationDelivery,
} from "../cross-context-invalidation/index.ts";
export type TanStackCacheCoordinatorDependencies = Readonly<{
queryClient: QueryClient;
invalidationIndex: InvalidationRegistryIndex;
topicVersions: ReadonlyMap<string, number>;
crossContext?: BrowserCrossContextInvalidation;
diagnostics?: DiagnosticsPort;
}>;
/**
* Joins registry-owned invalidation topics to TanStack Query without putting a
* query key or cached value on the cross-context wire.
*/
export function createTanStackCacheCoordinator(
dependencies: TanStackCacheCoordinatorDependencies,
): QueryInvalidationCoordinator {
validateConfiguration(
dependencies.invalidationIndex,
dependencies.topicVersions,
);
const mutationLeases = new Map<QueryInvalidationTopic, number>();
const pendingRemote = new Set<QueryInvalidationTopic>();
let disposed = false;
let resetting = false;
let lifecycleGeneration = 0;
let flushPromise: Promise<void> | null = null;
let resetPromise: Promise<void> | null = null;
const unsubscribe = dependencies.crossContext?.subscribe((delivery) => {
receiveRemote(delivery);
});
function namespacesFor(
topic: QueryInvalidationTopic,
): readonly QueryNamespaceIdentity[] {
const selected = dependencies.invalidationIndex.namespacesForTopic.get(topic);
if (!selected) {
throw new TypeError("Unregistered query invalidation topic.");
}
return selected;
}
async function invalidateLocal(
topics: readonly QueryInvalidationTopic[],
expectedGeneration = lifecycleGeneration,
): Promise<void> {
if (
disposed ||
resetting ||
expectedGeneration !== lifecycleGeneration
) {
return;
}
const namespaces = new Map<string, QueryNamespaceIdentity>();
for (const topic of topics) {
for (const namespace of namespacesFor(topic)) {
namespaces.set(queryNamespaceIdentityKey(namespace), namespace);
}
}
for (const namespace of namespaces.values()) {
try {
await dependencies.queryClient.invalidateQueries({
queryKey: createQueryInvalidationPrefix(namespace),
exact: false,
refetchType: "active",
});
} catch {
report("invalidate");
}
}
}
function receiveRemote(
delivery: CrossContextInvalidationDelivery,
): void {
if (disposed) return;
if (
!dependencies.invalidationIndex.namespacesForTopic.has(
delivery.event.topic,
)
) {
report("unknown-topic");
return;
}
const selectedTopic = delivery.event.topic as QueryInvalidationTopic;
if (delivery.ordering === "GAP") {
for (const topic of dependencies.invalidationIndex.namespacesForTopic.keys()) {
pendingRemote.add(topic as QueryInvalidationTopic);
}
report("sequence-gap");
} else {
pendingRemote.add(selectedTopic);
}
if (!resetting) void flushRemote();
}
function flushRemote(): Promise<void> {
if (disposed || resetting) return Promise.resolve();
if (flushPromise) return flushPromise;
const expectedGeneration = lifecycleGeneration;
flushPromise = Promise.resolve()
.then(async () => {
while (
!disposed &&
!resetting &&
expectedGeneration === lifecycleGeneration
) {
const ready = [...pendingRemote].filter(
(topic) => (mutationLeases.get(topic) ?? 0) === 0,
);
if (ready.length === 0) return;
for (const topic of ready) {
pendingRemote.delete(topic);
}
await invalidateLocal(ready, expectedGeneration);
}
})
.catch(() => {
report("remote-flush");
})
.finally(() => {
flushPromise = null;
if (
!disposed &&
!resetting &&
[...pendingRemote].some(
(topic) => (mutationLeases.get(topic) ?? 0) === 0,
)
) {
void flushRemote();
}
});
return flushPromise;
}
function uniqueTopics(
topics: readonly QueryInvalidationTopic[],
): readonly QueryInvalidationTopic[] {
const unique = [...new Set(topics)];
for (const topic of unique) namespacesFor(topic);
return unique;
}
function report(operation: string): void {
try {
dependencies.diagnostics?.record({
level: "warn",
eventId: "cache.operation.failed",
context: {
operation,
error_kind: "QUERY_CACHE_FAILURE",
},
});
} catch {
// Cache correctness and cleanup do not depend on diagnostics.
}
}
return Object.freeze({
async invalidate(
topics: readonly QueryInvalidationTopic[],
): Promise<void> {
if (disposed) return;
const selectedTopics = uniqueTopics(topics);
await invalidateLocal(selectedTopics);
for (const topic of selectedTopics) {
const topicVersion = dependencies.topicVersions.get(topic);
if (topicVersion === undefined) {
throw new TypeError("Unregistered query invalidation topic.");
}
const published = dependencies.crossContext?.publish({
topic,
topicVersion,
});
if (published && !published.ok) {
report("cross-context-publish");
}
}
},
beginMutation(
topics: readonly QueryInvalidationTopic[],
): QueryMutationLease {
if (disposed) {
throw new TypeError("Query invalidation coordinator is disposed.");
}
const selectedTopics = uniqueTopics(topics);
for (const topic of selectedTopics) {
mutationLeases.set(
topic,
(mutationLeases.get(topic) ?? 0) + 1,
);
}
let released = false;
return Object.freeze({
async release() {
if (released) return;
released = true;
for (const topic of selectedTopics) {
const remaining = (mutationLeases.get(topic) ?? 1) - 1;
if (remaining <= 0) {
mutationLeases.delete(topic);
} else {
mutationLeases.set(topic, remaining);
}
}
await flushRemote();
},
});
},
async resetLocal() {
if (disposed) return;
if (resetPromise) return resetPromise;
resetting = true;
lifecycleGeneration += 1;
pendingRemote.clear();
mutationLeases.clear();
const activeFlush = flushPromise;
resetPromise = (async () => {
try {
await activeFlush;
} catch {
report("reset-flush");
}
let cancellationFailed = false;
try {
await dependencies.queryClient.cancelQueries();
} catch {
report("reset-cancel");
cancellationFailed = true;
}
dependencies.queryClient.clear();
if (cancellationFailed) {
throw new TypeError("mandatory query cancellation failed");
}
})().finally(() => {
resetting = false;
resetPromise = null;
if (
!disposed &&
[...pendingRemote].some(
(topic) => (mutationLeases.get(topic) ?? 0) === 0,
)
) {
void flushRemote();
}
});
return resetPromise;
},
dispose() {
if (disposed) return;
disposed = true;
unsubscribe?.();
dependencies.crossContext?.close();
pendingRemote.clear();
mutationLeases.clear();
flushPromise = null;
resetPromise = null;
},
});
}
function validateConfiguration(
index: InvalidationRegistryIndex,
topicVersions: ReadonlyMap<string, number>,
): void {
if (
index.namespacesForTopic.size > INVALIDATION_REGISTRY_BOUNDS.maxTopics ||
topicVersions.size !== index.namespacesForTopic.size
) {
throw new TypeError("Query invalidation registry is invalid.");
}
for (const [topic, namespaces] of index.namespacesForTopic) {
const version = topicVersions.get(topic);
if (
namespaces.length < 1 ||
!Number.isSafeInteger(version) ||
(version ?? 0) < 1
) {
throw new TypeError("Query invalidation registry is invalid.");
}
const namespaceKeys = new Set<string>();
for (const namespace of namespaces) {
const key = queryNamespaceIdentityKey(namespace);
if (namespaceKeys.has(key)) {
throw new TypeError("Query invalidation registry is invalid.");
}
namespaceKeys.add(key);
}
}
for (const topic of topicVersions.keys()) {
if (!index.namespacesForTopic.has(topic)) {
throw new TypeError("Query invalidation registry is invalid.");
}
}
}
@@ -0,0 +1,115 @@
import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query";
import { createFailure } from "../../contracts/errors.ts";
import { safeErrorKind } from "../../contracts/diagnostics.ts";
import type { DiagnosticsPort } from "../../application/ports/diagnostics-port.ts";
import type { QueryCachePort } from "../../application/ports/query-cache-port.ts";
export type QueryCacheDependencies = Readonly<{
diagnostics?: DiagnosticsPort;
}>;
export const QUERY_CACHE_DEFAULTS = Object.freeze({
staleTime: 30_000,
gcTime: 300_000,
refetchOnWindowFocus: true,
retry: false,
mutationRetry: false,
persistence: false,
});
export function createQueryClient(
dependencies: QueryCacheDependencies = {},
): QueryClient {
function report(operation: string, error: unknown): void {
try {
dependencies.diagnostics?.record({
level: "warn",
eventId: "cache.operation.failed",
context: {
operation,
error_kind: safeErrorKind(error),
},
});
} catch {
// Query behavior remains independent from diagnostics.
}
}
return new QueryClient({
queryCache: new QueryCache({
onError: (error) => report("query", error),
}),
mutationCache: new MutationCache({
onError: (error) => report("mutation", error),
}),
defaultOptions: {
queries: {
staleTime: QUERY_CACHE_DEFAULTS.staleTime,
gcTime: QUERY_CACHE_DEFAULTS.gcTime,
refetchOnWindowFocus: QUERY_CACHE_DEFAULTS.refetchOnWindowFocus,
retry: QUERY_CACHE_DEFAULTS.retry,
},
mutations: {
retry: QUERY_CACHE_DEFAULTS.mutationRetry,
},
},
});
}
export function createQueryCacheAdapter(
queryClient: QueryClient,
dependencies: QueryCacheDependencies = {},
): QueryCachePort {
return Object.freeze({
read(key) {
try {
return { ok: true, value: queryClient.getQueryData(key) };
} catch {
return cacheFailure("read", key, dependencies.diagnostics);
}
},
write(key, value) {
try {
queryClient.setQueryData(key, structuredClone(value));
return { ok: true };
} catch {
return cacheFailure("write", key, dependencies.diagnostics);
}
},
async invalidate(namespace) {
try {
await queryClient.invalidateQueries({ queryKey: namespace, exact: false });
return { ok: true };
} catch {
return cacheFailure("invalidate", namespace, dependencies.diagnostics);
}
},
});
}
function cacheFailure(
phase: string,
key: readonly unknown[],
diagnostics: DiagnosticsPort | undefined,
): Readonly<{ ok: false; error: ReturnType<typeof createFailure> }> {
const namespace = typeof key[0] === "string" ? key[0] : "unknown";
try {
diagnostics?.record({
level: "warn",
eventId: "cache.operation.failed",
context: {
operation: phase,
error_kind: "QUERY_CACHE_FAILURE",
},
});
} catch {
// Cache behavior remains independent from diagnostics.
}
return {
ok: false,
error: createFailure("QUERY_CACHE_FAILURE", "QUERY_CACHE", 0, {
code: `QUERY_CACHE_${phase.toUpperCase()}_FAILED`,
causeClass: `namespace:${namespace}`,
}),
};
}