Files
dev/third_party/pi/packages/chord/test/variants.test.ts
alexion 6081e3e325 chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
2026-10-15 17:58:48 -04:00

169 lines
6.0 KiB
TypeScript

// Entry 8: a binding applies a shape only once its new state members have snapshots; members outside the variant
// throw `not_in_variant`; a re-hold after a link loss keeps the last values readable and sends one fresh unit.
import assert from "node:assert/strict";
import { Type } from "typebox";
import { describe, it } from "vitest";
import {
type Ctx,
createHost,
type Facet,
hostNamed,
isChordError,
memoryLinkPair,
method,
type Readable,
remote,
} from "../src/index.ts";
import { CountingLink, stateFrames } from "./fixtures/counting.ts";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function until(check: () => boolean, attempts = 200): Promise<void> {
for (let i = 0; i < attempts && !check(); i++) await sleep(2);
assert.ok(check(), "condition not reached");
}
const Doc = remote("t.e8-doc", {
version: 1,
input: Type.Object({}),
host: hostNamed("server"),
variants: {
a: { state: { x: Type.Number() }, methods: { bump: method() } },
b: { state: { x: Type.Number(), y: Type.String() }, methods: { bump: method() } },
c: { state: { y: Type.String() }, methods: { bump: method() } },
},
});
/** The server's facet: `Doc` in variant `variant` (the cells survive reloads by name). */
function docFacet(variant: "a" | "b" | "c"): Facet {
return {
name: "doc",
setup: (api) =>
api.serve(Doc, (_input, _principal, self) => {
const x = self.state("x", Type.Number(), 1);
const y = self.state("y", Type.String(), "why");
const bump = (ctx: Ctx) => {
x.replace(ctx, x.value + 1);
};
if (variant === "a") return { variant, x, bump };
if (variant === "b") return { variant, x, y, bump };
return { variant, y, bump };
}),
};
}
/** A reference whose variant changes under the test: its members, typed loosely (TypeScript narrows only once). */
type View = { readonly variant: string | undefined; readonly x: Readable<number>; readonly y: Readable<string> };
const view = (ref: object): View => ref as unknown as View;
function setup() {
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
const [down, up] = memoryLinkPair();
server.accept(down, { principal: { user: "u" } });
const link = new CountingLink(up);
const browser = createHost({
self: { kind: "host" },
name: "browser",
kind: "client",
upstream: { link, principal: { kind: "server" } },
onError: () => {},
});
return { server, browser, link };
}
describe("variants", () => {
it("a new variant's members arrive with its shape; a member outside the variant throws not_in_variant", async () => {
const { server, browser, link } = setup();
await server.install(docFacet("a"));
const doc = browser.hold(Doc, {});
await doc.ready;
assert.equal(view(doc).variant, "a");
assert.equal(view(doc).x.value, 1);
assert.throws(
() => view(doc).y.value,
(error) => isChordError(error, "not_in_variant"),
);
// Every shape the reference shows is readable in full, at the moment it is shown.
const failures: unknown[] = [];
doc.onShape((shape) => {
for (const name of ["x", "y"]) {
const member = Doc.description.variants[shape.variant ?? ""]?.[name];
if (member?.kind !== "state") continue;
try {
void (view(doc) as unknown as Record<string, Readable<unknown>>)[name]!.value;
} catch (error) {
failures.push(error);
}
}
});
const before = stateFrames(link.received).length;
await server.reload([docFacet("b")]);
await until(() => view(doc).variant === "b");
assert.equal(view(doc).y.value, "why");
assert.equal(view(doc).x.value, 1);
const unit = stateFrames(link.received).slice(before);
assert.deepEqual(
unit.filter((frame) => frame.as === "snap").map((frame) => frame.m),
["y"],
"only the new member's snapshot",
);
await server.reload([docFacet("c")]);
await until(() => view(doc).variant === "c");
assert.throws(
() => view(doc).x.value,
(error) => isChordError(error, "not_in_variant"),
);
assert.equal(view(doc).y.value, "why");
assert.deepEqual(failures, []);
await browser.close();
await server.close();
});
});
describe("re-hold after a link loss", () => {
it("keeps the last values readable, sends one fresh unit, and a reference joining meanwhile waits for it", async () => {
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
await server.install(docFacet("b"));
const [down, up] = memoryLinkPair();
server.accept(down, { principal: { user: "u" } });
const browser = createHost({
self: { kind: "host" },
name: "browser",
kind: "client",
upstream: { link: up, principal: { kind: "server" } },
onError: () => {},
});
const doc = browser.hold(Doc, {});
await doc.ready;
await doc.bump();
await until(() => view(doc).x.value === 2);
up.close();
await until(() => doc.status === "loading");
// The last values stay readable while the binding re-holds.
assert.equal(view(doc).x.value, 2);
const joining = browser.hold(Doc, {});
assert.equal(joining.status, "loading");
let joined = false;
void joining.ready.then(() => {
joined = true;
});
await sleep(5);
assert.equal(joined, false, "a reference joining during the re-hold waits for the fresh unit");
const [down2, up2] = memoryLinkPair();
server.accept(down2, { principal: { user: "u" } });
const link = new CountingLink(up2);
browser.connect(link, { principal: { kind: "server" } });
await joining.ready;
assert.equal(doc.status, "live");
const hold = link.sent.find((frame) => frame.t === "hold");
assert.ok(hold?.t === "hold" && hold.again === true, "a re-hold says again");
assert.equal(link.count("sent", "hold"), 1, "one hold for both references");
const snaps = stateFrames(link.received).filter((frame) => frame.as === "snap");
assert.deepEqual(snaps.map((frame) => frame.m).sort(), ["x", "y"], "one snapshot per member");
// The old hold was released with the link: the server made a fresh object (its cells start over).
assert.equal(view(doc).x.value, 1);
await browser.close();
await server.close();
});
});