mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
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.
190 lines
7.2 KiB
TypeScript
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");
|
|
});
|
|
});
|
|
});
|