Files
dev/third_party/pi/packages/chord/test/hydration.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

196 lines
6.4 KiB
TypeScript

// Entry 8: a local object goes live only with its state, also where it forwards or derives from a remote reference's
// member; activation waits at most `readyTimeout` (a cycle across hosts fails with a hint instead of hanging).
import assert from "node:assert/strict";
import { Type } from "typebox";
import { describe, it } from "vitest";
import {
type Ctx,
createHost,
type Host,
hostNamed,
isChordError,
local,
memoryLinkPair,
method,
remote,
} from "../src/index.ts";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
/** Lives on the worker `w`; its value is set by `set`. */
const Source = remote("t.e8-source", {
version: 1,
input: Type.Object({}),
host: hostNamed("w"),
variants: { main: { state: { v: Type.String() }, methods: { set: method(Type.String()) } } },
});
/** Lives on the server; forwards (and derives from) the worker's `Source`. */
const Forward = remote("t.e8-forward", {
version: 1,
input: Type.Object({ how: Type.String() }),
host: hostNamed("server"),
variants: { main: { state: { v: Type.String(), size: Type.Number() } } },
});
function serveSource(host: Host): void {
host.serve(Source, (_input, _principal, self) => {
const v = self.state("v", Type.String(), "from the worker");
return {
variant: "main",
v,
set: (text: string, ctx: Ctx) => {
v.replace(ctx, text);
},
};
});
}
/** A server with a worker child `w` (accepted after `delay` ms, as p2's server admits a worker after it is ready). */
function cluster(options: { readonly delay?: number; readonly readyTimeout?: number } = {}) {
const server = createHost({
self: { kind: "host" },
name: "server",
onError: () => {},
...(options.readyTimeout === undefined ? {} : { readyTimeout: options.readyTimeout }),
});
const [down, up] = memoryLinkPair();
const worker = createHost({
self: { kind: "host" },
name: "w",
upstream: { link: up, principal: { kind: "server" } },
onError: () => {},
...(options.readyTimeout === undefined ? {} : { readyTimeout: options.readyTimeout }),
});
const admit = () => server.accept(down, { principal: { kind: "worker" }, kind: "host", name: "w" });
if (options.delay === undefined) admit();
else setTimeout(admit, options.delay);
const browser = () => {
const [down2, up2] = memoryLinkPair();
server.accept(down2, { principal: { user: "u" } });
return createHost({
self: { kind: "host" },
name: "browser",
kind: "client",
upstream: { link: up2, principal: { kind: "server" } },
onError: () => {},
});
};
return { server, worker, browser };
}
describe("local hydration", () => {
it("an object that forwards and derives from a remote member goes live with its value", async () => {
const c = cluster();
serveSource(c.worker);
// Held outside the factory (no `self.get`): activation still waits for its snapshot.
const held = c.server.hold(Source, {});
c.server.serve(Forward, ({ how }, _principal, self) => {
const source = how === "declared" ? self.get(Source, {}) : held;
return { variant: "main", v: source.v, size: self.derive([source.v], (text) => text.length) };
});
const browser = c.browser();
for (const how of ["held", "declared"]) {
const forward = browser.hold(Forward, { how });
await forward.ready;
assert.equal(forward.v.value, "from the worker");
assert.equal(forward.size.value, "from the worker".length);
const local = c.server.hold(Forward, { how });
await local.ready;
assert.equal(local.v.value, "from the worker");
}
await c.server.close();
await c.worker.close();
});
it("a member read inside a factory throws not_ready; it is a plain value once live", async () => {
const host = createHost({ self: { kind: "host" }, onError: () => {} });
const Inner = local("t.e8-inner", { version: 1, input: Type.Object({}), members: { n: Type.Number() } });
const Outer = local("t.e8-outer", { version: 1, input: Type.Object({}), members: { m: Type.Number() } });
host.serve(Inner, (_input, _principal, self) => ({ n: self.state("n", Type.Number(), 7) }));
const seen: unknown[] = [];
host.serve(Outer, (_input, _principal, self) => {
const inner = self.get(Inner, {});
try {
seen.push(inner.n.value);
} catch (error) {
seen.push(isChordError(error, "not_ready") ? "not_ready" : error);
}
return { m: self.derive([inner.n], (n) => n * 2) };
});
const outer = host.hold(Outer, {});
await outer.ready;
assert.deepEqual(seen, ["not_ready"]);
assert.equal(outer.m.value, 14);
});
it("a host object of a child holding the parent's object goes live once the parent admits the child", async () => {
const Settings = remote("t.e8-settings", {
version: 1,
input: Type.Object({}),
host: hostNamed("server"),
variants: { main: { state: { theme: Type.String() } } },
});
const Local = local("t.e8-boot", { version: 1, input: Type.Object({}), members: { theme: Type.String() } });
const c = cluster({ delay: 20 });
c.server.serve(Settings, (_input, _principal, self) => ({
variant: "main",
theme: self.state("theme", Type.String(), "dark"),
}));
await c.worker.install(
{
name: "boot",
setup: (api) =>
api.serve(
Local,
(_input, _principal, self) => {
const settings = self.get(Settings, {});
return { theme: settings.theme };
},
{ lifetime: "host" },
),
},
{ awaitHostObjects: false },
);
await c.worker.ready();
const boot = c.worker.hold(Local, {});
await boot.ready;
assert.equal(boot.theme.value, "dark");
await c.server.close();
await c.worker.close();
});
it("a reference cycle across hosts fails at readyTimeout with a cycle hint", async () => {
const A = remote("t.e8-cycle-a", {
version: 1,
input: Type.Object({}),
host: hostNamed("server"),
variants: { main: {} },
});
const B = remote("t.e8-cycle-b", {
version: 1,
input: Type.Object({}),
host: hostNamed("w"),
variants: { main: {} },
});
const c = cluster({ readyTimeout: 50 });
c.server.serve(A, (_input, _principal, self) => {
self.get(B, {});
return { variant: "main" };
});
c.worker.serve(B, (_input, _principal, self) => {
self.get(A, {});
return { variant: "main" };
});
const a = c.server.hold(A, {});
await assert.rejects(
a.ready,
(error) =>
isChordError(error, "unavailable") && error.reason === "ready_timeout" && /cycle/.test(error.message),
);
await sleep(10);
await c.server.close();
await c.worker.close();
});
});