chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createServerStateScopeRuntime } from "../../src/adapters/query-cache/server-state-scope-runtime.ts";
|
||||
import type { ClientScopeLifecycleEvent } from "../../src/contracts/server-state-scope.ts";
|
||||
|
||||
describe("server-state session generation runtime", () => {
|
||||
it("fences the old generation synchronously and publishes READY after the reset order", async () => {
|
||||
let sessionListener: () => void = () => {};
|
||||
let completeReset: () => void = () => {};
|
||||
const reset = new Promise<void>((resolve) => {
|
||||
completeReset = resolve;
|
||||
});
|
||||
const resetLocal = vi.fn(() => reset);
|
||||
const participantOrder: string[] = [];
|
||||
let tokenSequence = 0;
|
||||
const runtime = createServerStateScopeRuntime({
|
||||
session: {
|
||||
subscribe(listener) {
|
||||
sessionListener = listener;
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
queryInvalidation: {
|
||||
resetLocal,
|
||||
},
|
||||
participants: [
|
||||
{
|
||||
order: 9,
|
||||
label: "realtime",
|
||||
close: () => void participantOrder.push("realtime"),
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
label: "admission",
|
||||
close: () => void participantOrder.push("admission"),
|
||||
},
|
||||
],
|
||||
tokenFactory: () =>
|
||||
`scope-token-${String(tokenSequence++).padStart(8, "0")}`,
|
||||
});
|
||||
const changed = vi.fn();
|
||||
const lifecycle: ClientScopeLifecycleEvent[] = [];
|
||||
runtime.subscribe(changed);
|
||||
runtime.subscribeLifecycle((event) => lifecycle.push(event));
|
||||
const before = runtime.getSnapshot();
|
||||
const identity = before.identities.intern({ id: "private" });
|
||||
identity.acquire();
|
||||
|
||||
sessionListener();
|
||||
|
||||
// §10.6 steps 1-3 are synchronous: the old snapshot is immediately stale,
|
||||
// FENCED is published, and subscribers are notified before any await.
|
||||
expect(before.isCurrent()).toBe(false);
|
||||
expect(before.signal.aborted).toBe(true);
|
||||
expect(runtime.getPhase()).toBe("FENCED");
|
||||
expect(lifecycle[0]).toEqual({ kind: "FENCED", previousGeneration: 1 });
|
||||
expect(changed).toHaveBeenCalledOnce();
|
||||
// Nothing may read as current while the scope is fenced.
|
||||
expect(runtime.getSnapshot().isCurrent()).toBe(false);
|
||||
|
||||
await vi.waitFor(() => expect(resetLocal).toHaveBeenCalledOnce());
|
||||
// Participants close in §10.6 step order, before the local cache reset.
|
||||
expect(participantOrder).toEqual(["admission", "realtime"]);
|
||||
|
||||
completeReset();
|
||||
await vi.waitFor(() => expect(runtime.getPhase()).toBe("READY"));
|
||||
const after = runtime.getSnapshot();
|
||||
expect(after.generation).toBe(before.generation + 1);
|
||||
expect(after.fingerprint).not.toBe(before.fingerprint);
|
||||
expect(after.isCurrent()).toBe(true);
|
||||
expect(after.signal.aborted).toBe(false);
|
||||
expect(after.signal).not.toBe(before.signal);
|
||||
expect(before.identities.inspect().closed).toBe(true);
|
||||
expect(lifecycle.at(-1)).toMatchObject({ kind: "READY" });
|
||||
expect(changed).toHaveBeenCalledTimes(2);
|
||||
|
||||
runtime.dispose();
|
||||
expect(runtime.getPhase()).toBe("DISPOSED");
|
||||
expect(after.identities.inspect().closed).toBe(true);
|
||||
expect(after.isCurrent()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not let a throwing snapshot subscriber prevent reset", async () => {
|
||||
let sessionListener: () => void = () => {};
|
||||
const resetLocal = vi.fn(async () => {});
|
||||
const runtime = createServerStateScopeRuntime({
|
||||
session: {
|
||||
subscribe(listener) {
|
||||
sessionListener = listener;
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
queryInvalidation: {
|
||||
resetLocal,
|
||||
},
|
||||
tokenFactory: () => "scope-listener-token-0001",
|
||||
});
|
||||
runtime.subscribe(() => {
|
||||
throw new Error("subscriber defect");
|
||||
});
|
||||
|
||||
expect(() => sessionListener()).not.toThrow();
|
||||
await vi.waitFor(() => expect(resetLocal).toHaveBeenCalledOnce());
|
||||
await vi.waitFor(() => expect(runtime.getPhase()).toBe("READY"));
|
||||
});
|
||||
|
||||
it("remains failed when a mandatory participant cannot close", async () => {
|
||||
let sessionListener: () => void = () => {};
|
||||
const resetLocal = vi.fn(async () => {});
|
||||
const runtime = createServerStateScopeRuntime({
|
||||
session: {
|
||||
subscribe(listener) {
|
||||
sessionListener = listener;
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
queryInvalidation: {
|
||||
resetLocal,
|
||||
},
|
||||
participants: [
|
||||
{
|
||||
order: 4,
|
||||
label: "mandatory-admission",
|
||||
close: async () => {
|
||||
throw new Error("close failed");
|
||||
},
|
||||
},
|
||||
],
|
||||
tokenFactory: () => "scope-participant-token-0001",
|
||||
});
|
||||
|
||||
sessionListener();
|
||||
|
||||
await vi.waitFor(() => expect(resetLocal).toHaveBeenCalledOnce());
|
||||
await vi.waitFor(() =>
|
||||
expect(runtime.getPhase() as string).toBe("FAILED"),
|
||||
);
|
||||
expect(runtime.getSnapshot().isCurrent()).toBe(false);
|
||||
});
|
||||
|
||||
it("remains failed when local cache reset rejects", async () => {
|
||||
let sessionListener: () => void = () => {};
|
||||
const runtime = createServerStateScopeRuntime({
|
||||
session: {
|
||||
subscribe(listener) {
|
||||
sessionListener = listener;
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
queryInvalidation: {
|
||||
resetLocal: async () => {
|
||||
throw new Error("reset failed");
|
||||
},
|
||||
},
|
||||
tokenFactory: () => "scope-reset-token-0000001",
|
||||
});
|
||||
|
||||
sessionListener();
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(runtime.getPhase() as string).toBe("FAILED"),
|
||||
);
|
||||
expect(runtime.getSnapshot().isCurrent()).toBe(false);
|
||||
});
|
||||
|
||||
it("activates the next generation after reset and fails closed on activation error", async () => {
|
||||
let sessionListener: () => void = () => {};
|
||||
const steps: string[] = [];
|
||||
const runtime = createServerStateScopeRuntime({
|
||||
session: {
|
||||
subscribe(listener) {
|
||||
sessionListener = listener;
|
||||
return () => {};
|
||||
},
|
||||
},
|
||||
queryInvalidation: {
|
||||
resetLocal: async () => {
|
||||
steps.push("reset");
|
||||
},
|
||||
},
|
||||
activateNextGeneration: async () => {
|
||||
steps.push("activate");
|
||||
throw new Error("activation failed");
|
||||
},
|
||||
tokenFactory: () => "scope-activation-token-001",
|
||||
} as Parameters<typeof createServerStateScopeRuntime>[0] & {
|
||||
activateNextGeneration(): Promise<void>;
|
||||
});
|
||||
|
||||
sessionListener();
|
||||
|
||||
await vi.waitFor(() => expect(steps).toEqual(["reset", "activate"]));
|
||||
await vi.waitFor(() =>
|
||||
expect(runtime.getPhase() as string).toBe("FAILED"),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user