pi-mono/packages/chord/test/next/host/ref-state.test.ts
Mario Zechner 4b535d57ea feat(chord): slice 7b, reload in the kernel
host.reload(facets, ctx): staging into a staged table with a recording self (all or
nothing), one commit (types, registries, contributions; per-name wakes; installing
holds), and each object's fate: pure swap in place, native restart after its drain,
retry on the new code, or its end by its type's removal, with a report. Old calls
finish on old handles (R15 for documents and tables), routines hand over per object,
jobs keep their versions (accepts, blocked { version }, persisted runs per R3),
registries swap providers and contributions in one batch, holds are kept,
re-subscribed or unbound with failed { waiting } (model §5), accepts maps older
consumers' state, arguments, results and keyed members.

References are the only indirection (owner, 2026-10-29): a consumer's watches and
derives attach through the reference, which passes them straight to the object
instance's own document or derive, moves them to a new instance (one frame) and keeps
its last values while unbound; MemberSource, keyed-members and the per-version
hand-over are removed. Older consumers read through the instance's shared mapping
per version. Examples 33-34, reload benchmark.
2026-10-30 08:09:06 +00:00

190 lines
7.2 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { BACKGROUND_CONTEXT as context, withCancel } from "../../../src/context/index.ts";
import { int, local, method } from "../../../src/next/contracts/index.ts";
import { broken } from "../../../src/next/data/index.ts";
import { constant } from "../../../src/next/state/index.ts";
import {
bindHold,
bindNext,
fakeTimers,
flushed,
forEachCodec,
loaded,
Notes,
rawParent,
rawProvider,
rejection,
statesOf,
transported,
} from "../support.ts";
const Flaky = local("app.flaky", {
version: 1,
params: { id: int() },
members: { n: int(), get: method(undefined, int()) },
});
/** Serves `Flaky`, whose open throws while `failing.left` is above 0 (broken while `failing.broken`). */
async function flaky(failing: { left: number; broken: boolean }) {
return loaded((h) =>
h.serve(Flaky, ({ id }, self) => {
self.open(
async () => {
if (failing.left-- <= 0) return;
if (failing.broken) throw broken(new Error("no configuration"));
throw new Error("offline");
},
() => {},
);
return { n: constant(id), get: async () => id };
}),
);
}
const offline = { code: "unavailable", message: "app.flaky(1): offline" };
describe("ref.state of a local reference", () => {
fakeTimers();
it("is watched from its current state: failed with retryAt, loading on the next attempt, live; release ends it", async () => {
const { host } = await flaky({ left: 1, broken: false });
const ref = host.hold(Flaky, { id: 1 }, context);
expect(ref.state.value).toEqual({ kind: "loading" });
const watch = await ref.state.watch(context);
expect(watch.value).toEqual({ kind: "loading" });
const states = statesOf(watch);
await flushed();
expect(ref.state.value).toMatchObject({
kind: "failed",
cause: { kind: "transient", error: offline },
retryAt: 1_000,
});
await vi.advanceTimersByTimeAsync(1_000);
await flushed();
expect(ref.state.value).toEqual({ kind: "live" });
ref.release();
expect(await watch.closed).toEqual({ kind: "ended" });
expect(states).toEqual(["failed: app.flaky(1): offline", "loading", "live"]);
expect(ref.state.value).toMatchObject({ kind: "ended", error: { code: "released" } });
});
it("reads and calls follow the state: not_ready before live, the cause's error while failed, released after", async () => {
const { host } = await flaky({ left: 1, broken: true });
const ref = host.hold(Flaky, { id: 1 }, context);
expect(() => ref.n.value).toThrow("app.flaky(1): not live yet");
await flushed();
const cause = { code: "unavailable", message: "app.flaky(1): no configuration" };
expect(ref.state.value).toMatchObject({
kind: "failed",
cause: { kind: "broken", error: cause },
retryAt: undefined,
});
expect(() => ref.n.value).toThrow("no configuration");
expect(await rejection(ref.get(context))).toEqual(cause);
ref.release();
expect(() => ref.n.value).toThrow("app.flaky: released");
expect(await rejection(ref.get(context))).toEqual({ code: "released", message: "app.flaky: released" });
});
it("retry renews a broken object: loading, then live", async () => {
const { host } = await flaky({ left: 1, broken: true });
const ref = host.hold(Flaky, { id: 1 }, context);
await flushed();
expect(ref.state.value.kind).toBe("failed");
ref.retry();
expect(ref.state.value).toEqual({ kind: "loading" });
await flushed();
expect(ref.state.value).toEqual({ kind: "live" });
expect(await ref.get(context)).toBe(1);
});
it("is made on first access, once; a watch taken after release has ended", async () => {
const { host } = await flaky({ left: 0, broken: false });
const ref = await host.get(Flaky, { id: 1 }, context);
expect(ref.state).toBe(ref.state);
expect(ref.state.value).toEqual({ kind: "live" });
ref.release();
const late = await ref.state.watch(context);
expect(late.value).toMatchObject({ kind: "ended", error: { code: "released" } });
expect(await late.closed).toEqual({ kind: "ended" });
});
it("a watch ends cancelled with its context", async () => {
const { host } = await flaky({ left: 0, broken: false });
const ref = host.hold(Flaky, { id: 1 }, context);
const { context: cancellable, cancel } = withCancel(context);
const watch = await ref.state.watch(cancellable);
cancel();
expect(await watch.closed).toEqual({ kind: "aborted" });
});
});
forEachCodec(() => {
describe("ref.state of a remote reference", () => {
fakeTimers();
it("loading until bound, loading while its link is down, failed with retryAt from retryIn, live again", async () => {
const { client, peer } = await rawProvider();
const notes = client.hold(Notes, { session: "S1" }, context);
const states = statesOf(await notes.state.watch(context));
await bindNext(peer);
await transported();
expect(notes.state.value).toEqual({ kind: "live" });
peer.close();
await transported();
expect(notes.state.value).toEqual({ kind: "loading" });
const next = await rawParent(client);
const { hold } = await next.next("hold");
const error = { code: "unavailable", message: "offline" } as const;
next.send({
kind: "failed",
hold,
cause: { kind: "transient", error: { kind: "chord", error } },
retryIn: 500,
});
await transported();
expect(notes.state.value).toMatchObject({ kind: "failed", cause: { kind: "transient", error }, retryAt: 500 });
next.send({ kind: "loading", hold });
await transported();
expect(notes.state.value).toEqual({ kind: "loading" });
bindHold(next, hold);
await transported();
expect(notes.state.value).toEqual({ kind: "live" });
expect(states).toEqual(["live", "loading", "failed: offline", "loading", "live"]);
});
it("a failed with retryIn 0 retries now: retryAt is the current time", async () => {
const { client, peer } = await rawProvider();
vi.setSystemTime(2_000);
const notes = client.hold(Notes, { session: "S1" }, context);
const { hold } = await peer.next("hold");
const error = { code: "unavailable", message: "offline" } as const;
peer.send({ kind: "failed", hold, cause: { kind: "transient", error: { kind: "chord", error } }, retryIn: 0 });
await transported();
expect(notes.state.value).toMatchObject({ kind: "failed", retryAt: 2_000 });
});
it("variant: undefined before the first binding, the last bound one while loading or failed", async () => {
const { client, peer } = await rawProvider();
const notes = client.hold(Notes, { session: "S1" }, context);
expect(notes.variant).toBeUndefined();
await bindNext(peer, [{ text: "a" }]);
await transported();
expect(notes.variant).toBe("editor");
peer.close();
await transported();
expect([notes.variant, notes.count.value]).toEqual(["editor", 1]);
const next = await rawParent(client);
const { hold } = await next.next("hold");
const error = { code: "unavailable", message: "offline" } as const;
next.send({ kind: "failed", hold, cause: { kind: "broken", error: { kind: "chord", error } } });
await transported();
expect(notes.state.value).toMatchObject({ kind: "failed", retryAt: undefined });
expect(notes.variant).toBe("editor");
// Its replica's last value stays readable while the hold is failed (rule 5).
expect(notes.count.value).toBe(1);
notes.release();
expect(() => notes.variant).toThrow("app.notes: released");
});
});
});