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
196 lines
6.4 KiB
TypeScript
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();
|
|
});
|
|
});
|