refactor: 리펙토링
This commit is contained in:
@@ -6,18 +6,47 @@ import {
|
||||
} from "../../contracts/query-keys.ts";
|
||||
import type {
|
||||
CacheScopeSnapshot,
|
||||
ClientScopeLifecycleEvent,
|
||||
ClientScopePhase,
|
||||
ServerStateScopeRuntime,
|
||||
} from "../../contracts/server-state-scope.ts";
|
||||
|
||||
export function createServerStateScopeRuntime(dependencies: Readonly<{
|
||||
/**
|
||||
* 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: QueryInvalidationCoordinator;
|
||||
queryInvalidation: Pick<QueryInvalidationCoordinator, "resetLocal">;
|
||||
tokenFactory?: () => string;
|
||||
}>): ServerStateScopeRuntime {
|
||||
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();
|
||||
|
||||
@@ -28,42 +57,129 @@ export function createServerStateScopeRuntime(dependencies: Readonly<{
|
||||
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
|
||||
.then(() => dependencies.queryInvalidation.resetLocal())
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
.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();
|
||||
for (const listener of listeners) listener();
|
||||
publishLifecycle(
|
||||
Object.freeze({ kind: "READY" as const, snapshot: currentSnapshot }),
|
||||
);
|
||||
publishSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
return Object.freeze({
|
||||
getSnapshot: () => currentSnapshot,
|
||||
subscribe(listener) {
|
||||
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();
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user