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
This commit is contained in:
1 parent
719370c8f7
commit
6081e3e325
1743 files changed
+205459
-131704
No files matched your search
+459
@@ -0,0 +1,459 @@
|
||||
// Entry 8: one shared binding per host and key; a hold implies the state of its variant (units: shape, then
|
||||
// snapshots); method-only references; origins of client holds; protocol version 4.
|
||||
import assert from "node:assert/strict";
|
||||
import { Type } from "typebox";
|
||||
import { describe, it } from "vitest";
|
||||
import { HostImpl } from "../src/host.ts";
|
||||
import {
|
||||
type Ctx,
|
||||
createHost,
|
||||
type Frame,
|
||||
type Host,
|
||||
hostFamily,
|
||||
hostNamed,
|
||||
isChordError,
|
||||
type Link,
|
||||
local,
|
||||
memoryLinkPair,
|
||||
method,
|
||||
PROTOCOL_VERSION,
|
||||
remote,
|
||||
} from "../src/index.ts";
|
||||
import { CountingLink, stateFrames } from "./fixtures/counting.ts";
|
||||
|
||||
const tick = () => new Promise<void>((resolve) => setImmediate(resolve));
|
||||
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 NoteSchema = Type.Array(Type.String());
|
||||
const Notes = remote("t.e8-notes", {
|
||||
version: 1,
|
||||
input: Type.Object({ list: Type.Optional(Type.String({ default: "main" })) }),
|
||||
host: hostNamed("server"),
|
||||
variants: {
|
||||
main: {
|
||||
state: { notes: NoteSchema, title: Type.String() },
|
||||
methods: { add: method(Type.String()), rename: method(Type.String()) },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function serveNotes(host: Host): void {
|
||||
host.serve(Notes, (_input, _principal, self) => {
|
||||
const notes = self.state("notes", NoteSchema, []);
|
||||
const title = self.state("title", Type.String(), "untitled");
|
||||
return {
|
||||
variant: "main",
|
||||
notes,
|
||||
title,
|
||||
add: (text: string, ctx: Ctx) => {
|
||||
notes.change(ctx, (draft) => {
|
||||
draft.push(text);
|
||||
});
|
||||
},
|
||||
rename: (text: string, ctx: Ctx) => {
|
||||
title.replace(ctx, text);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** A server serving `Notes`, and a browser whose end of the link counts what crosses it. */
|
||||
function setup(
|
||||
options: { readonly window?: number; readonly linger?: { types?: Record<string, number>; cap?: number } } = {},
|
||||
) {
|
||||
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
|
||||
serveNotes(server);
|
||||
const [down, up] = memoryLinkPair();
|
||||
server.accept(down, {
|
||||
principal: { user: "u" },
|
||||
...(options.window === undefined ? {} : { window: options.window }),
|
||||
});
|
||||
const link = new CountingLink(up);
|
||||
const errors: unknown[] = [];
|
||||
const browser = createHost({
|
||||
self: { kind: "host" },
|
||||
name: "browser",
|
||||
kind: "client",
|
||||
upstream: { link, principal: { kind: "server" } },
|
||||
onError: (error) => errors.push(error),
|
||||
...(options.linger === undefined ? {} : { linger: options.linger }),
|
||||
});
|
||||
return { server, browser, link, errors };
|
||||
}
|
||||
|
||||
describe("shared bindings: traffic", () => {
|
||||
it("N references to one object: one hold, one snapshot per member, one frame per change, one release", async () => {
|
||||
const { server, browser, link } = setup();
|
||||
const Panel = local("t.e8-panel", { version: 1, input: Type.Object({}), members: { count: Type.Number() } });
|
||||
browser.serve(Panel, (_input, _principal, self) => {
|
||||
const notes = self.get(Notes, {});
|
||||
return { count: self.derive([notes.notes], (list) => list.length) };
|
||||
});
|
||||
const a = browser.hold(Notes, {});
|
||||
const b = browser.hold(Notes, { list: "main" });
|
||||
const panel = browser.hold(Panel, {});
|
||||
await Promise.all([a.ready, b.ready, panel.ready]);
|
||||
const viaCtx = await browser.root(async (ctx) => {
|
||||
const ref = await ctx.get(Notes, {});
|
||||
return ref.title.value;
|
||||
});
|
||||
assert.equal(viaCtx, "untitled");
|
||||
assert.equal(link.count("sent", "hold"), 1, "one hold for every reference");
|
||||
const snaps = stateFrames(link.received).filter((frame) => frame.as === "snap");
|
||||
assert.deepEqual(snaps.map((frame) => frame.m).sort(), ["notes", "title"]);
|
||||
|
||||
const before = stateFrames(link.received).length;
|
||||
await a.add("milk");
|
||||
await until(() => a.notes.value.length === 1 && b.notes.value.length === 1 && panel.count.value === 1);
|
||||
await tick();
|
||||
assert.equal(stateFrames(link.received).length - before, 1, "one delta frame for one change, whatever N");
|
||||
|
||||
const sent = link.traffic("sent").length;
|
||||
a.release();
|
||||
panel.release();
|
||||
await sleep(5);
|
||||
assert.equal(link.traffic("sent").length, sent, "releasing all but one reference sends nothing");
|
||||
b.release();
|
||||
await until(() => link.count("sent", "release") === 1);
|
||||
await browser.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it("a held reference stays subscribed without listeners; values are readable at once after ready", async () => {
|
||||
const { server, browser } = setup();
|
||||
const notes = browser.hold(Notes, {});
|
||||
await notes.ready;
|
||||
assert.deepEqual(notes.notes.value, []);
|
||||
await notes.add("bread");
|
||||
await until(() => notes.notes.value.length === 1);
|
||||
assert.equal(notes.title.value, "untitled");
|
||||
await browser.close();
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shared bindings: linger", () => {
|
||||
it("a lingering binding keeps its hold and state; a reference taken meanwhile costs nothing on the link", async () => {
|
||||
const { server, browser, link } = setup({ linger: { types: { [Notes.name]: 40 } } });
|
||||
const first = await browser.get(Notes, {});
|
||||
const frames = link.traffic("received").length;
|
||||
first.release();
|
||||
await sleep(10);
|
||||
assert.equal(link.count("sent", "release"), 0, "not released before the linger");
|
||||
// A hook mounting and unmounting: no frames either way.
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const again = await browser.get(Notes, {});
|
||||
assert.deepEqual(again.notes.value, []);
|
||||
again.release();
|
||||
}
|
||||
assert.equal(link.count("sent", "hold"), 1);
|
||||
assert.equal(link.traffic("received").length, frames, "nothing new crossed the link");
|
||||
await until(() => link.count("sent", "release") === 1);
|
||||
await browser.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it("at most `cap` bindings linger: the least recently released goes first", async () => {
|
||||
const { server, browser, link } = setup({ linger: { types: { [Notes.name]: 10_000 }, cap: 1 } });
|
||||
const a = await browser.get(Notes, { list: "a" });
|
||||
const b = await browser.get(Notes, { list: "b" });
|
||||
a.release();
|
||||
await sleep(5);
|
||||
assert.equal(link.count("sent", "release"), 0);
|
||||
b.release();
|
||||
await until(() => link.count("sent", "release") === 1);
|
||||
const release = link.sent.find((frame) => frame.t === "release");
|
||||
const holdOfA = link.sent.find((frame) => frame.t === "hold" && (frame.input as { list: string }).list === "a");
|
||||
assert.equal(release?.t === "release" ? release.id : 0, holdOfA?.t === "hold" ? holdOfA.id : -1);
|
||||
await browser.close();
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shared bindings: identity", () => {
|
||||
it("normalized-equal inputs and equal principal keys share; other principals and fingerprints do not", async () => {
|
||||
const server = createHost({
|
||||
self: { kind: "host" },
|
||||
name: "server",
|
||||
onError: () => {},
|
||||
});
|
||||
const NotesV2 = remote("t.e8-notes", {
|
||||
version: 2,
|
||||
input: Type.Object({ list: Type.Optional(Type.String({ default: "main" })) }),
|
||||
host: hostNamed("server"),
|
||||
variants: {
|
||||
main: {
|
||||
state: { notes: NoteSchema, title: Type.String() },
|
||||
methods: { add: method(Type.String()), rename: method(Type.String()), clear: method() },
|
||||
},
|
||||
},
|
||||
});
|
||||
server.serve(
|
||||
NotesV2,
|
||||
(_input, _principal, self) => {
|
||||
const notes = self.state("notes", NoteSchema, []);
|
||||
const title = self.state("title", Type.String(), "untitled");
|
||||
return { variant: "main", notes, title, add: () => {}, rename: () => {}, clear: () => {} };
|
||||
},
|
||||
{ accepts: { 1: (current) => current } },
|
||||
);
|
||||
// A worker holds the server's objects; the principals it forwards key its bindings.
|
||||
const [down, up] = memoryLinkPair();
|
||||
server.accept(down, { principal: { worker: "w" }, kind: "host", name: "session:w" });
|
||||
const link = new CountingLink(up);
|
||||
const worker = createHost<unknown>({
|
||||
self: { kind: "host" },
|
||||
name: "session:w",
|
||||
upstream: { link, principal: { kind: "server" } },
|
||||
onError: () => {},
|
||||
});
|
||||
const refs = [
|
||||
worker.hold(Notes, {}),
|
||||
worker.hold(Notes, { list: "main" }),
|
||||
worker.hold(Notes, {}, { principal: { user: "a", grant: 1 } }),
|
||||
worker.hold(Notes, {}, { principal: { grant: 1, user: "a" } }),
|
||||
worker.hold(Notes, {}, { principal: { user: "a", grant: 2 } }),
|
||||
worker.hold(NotesV2, {}),
|
||||
];
|
||||
await Promise.all(refs.map((ref) => ref.ready));
|
||||
// host (shared by two inputs), user a grant 1 (shared by two key orders), user a grant 2, the v2 contract.
|
||||
assert.equal(link.count("sent", "hold"), 4);
|
||||
await worker.close();
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("hydration over a link", () => {
|
||||
const Matrix = remote("t.e8-matrix", {
|
||||
version: 1,
|
||||
input: Type.Object({ variant: Type.String() }),
|
||||
host: hostNamed("server"),
|
||||
variants: {
|
||||
values: { state: { none: Type.Null(), empty: Type.Array(Type.String()), maybe: Type.Unknown() } },
|
||||
bare: { methods: { ping: method(undefined, Type.String()) } },
|
||||
broken: { state: { n: Type.Number() } },
|
||||
},
|
||||
});
|
||||
|
||||
function serveMatrix(host: Host): void {
|
||||
host.serve(Matrix, ({ variant }, _principal, self) => {
|
||||
if (variant === "bare") return { variant: "bare", ping: () => "pong" };
|
||||
if (variant === "broken") {
|
||||
const source = self.state("source", Type.String(), "not a number");
|
||||
// A derivation exposed under a number schema: its snapshot fails validation.
|
||||
return { variant: "broken", n: self.derive([source], (text) => text as unknown as number) };
|
||||
}
|
||||
const none = self.state("none", Type.Null(), null);
|
||||
const empty = self.state("empty", Type.Array(Type.String()), []);
|
||||
return { variant: "values", none, empty, maybe: self.derive([none], () => undefined as unknown) };
|
||||
});
|
||||
}
|
||||
|
||||
it("null, empty and undefined values hydrate; a state-free variant is ready; a failed snapshot rejects", async () => {
|
||||
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
|
||||
serveMatrix(server);
|
||||
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 values = await browser.get(Matrix, { variant: "values" });
|
||||
assert.ok(values.variant === "values");
|
||||
assert.equal(values.none.value, null);
|
||||
assert.deepEqual(values.empty.value, []);
|
||||
assert.equal(values.maybe.value, undefined);
|
||||
const bare = await browser.get(Matrix, { variant: "bare" });
|
||||
assert.ok(bare.variant === "bare");
|
||||
assert.equal(await bare.ping(), "pong");
|
||||
assert.throws(
|
||||
// @ts-expect-error `none` is not a member of the bare variant (a narrowed reference does not show it).
|
||||
() => bare.none.value,
|
||||
(error) => isChordError(error, "not_in_variant"),
|
||||
);
|
||||
const broken = browser.hold(Matrix, { variant: "broken" });
|
||||
await assert.rejects(broken.ready, (error) => isChordError(error, "invalid"));
|
||||
assert.equal(broken.status, "gone");
|
||||
await browser.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it("reading before the first hydration throws not_ready", async () => {
|
||||
const { server, browser } = setup();
|
||||
const notes = browser.hold(Notes, {});
|
||||
assert.throws(
|
||||
() => notes.notes.value,
|
||||
(error) => isChordError(error, "not_ready"),
|
||||
);
|
||||
await notes.ready;
|
||||
assert.deepEqual(notes.notes.value, []);
|
||||
await browser.close();
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("method-only references (state: false)", () => {
|
||||
it("transfer no state alone; a full reference joining starts it once; its release stops it", async () => {
|
||||
const { server, browser, link } = setup();
|
||||
const caller = browser.hold(Notes, {}, { state: false });
|
||||
await caller.ready;
|
||||
await caller.add("x");
|
||||
const hold = link.sent.find((frame) => frame.t === "hold");
|
||||
assert.ok(hold?.t === "hold" && hold.state === false);
|
||||
assert.deepEqual(stateFrames(link.received), []);
|
||||
assert.throws(
|
||||
() => caller.notes.value,
|
||||
(error) => isChordError(error, "not_ready"),
|
||||
);
|
||||
|
||||
const viewer = browser.hold(Notes, {});
|
||||
await viewer.ready;
|
||||
assert.deepEqual(viewer.notes.value, ["x"]);
|
||||
assert.equal(link.count("sent", "sub"), 1);
|
||||
assert.equal(link.count("sent", "hold"), 1, "the same hold");
|
||||
viewer.release();
|
||||
await until(() => link.count("sent", "unsub") === 1);
|
||||
const after = stateFrames(link.received).length;
|
||||
await caller.add("y");
|
||||
await sleep(5);
|
||||
assert.equal(stateFrames(link.received).length, after, "no state once only method-only references remain");
|
||||
await browser.close();
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("origins of client holds", () => {
|
||||
const Counter = remote("t.e8-counter", {
|
||||
version: 1,
|
||||
input: Type.Object({ session: Type.String() }),
|
||||
host: hostFamily("session:", "session"),
|
||||
variants: { main: { state: { n: Type.Number() }, methods: { add: method(Type.Number()) } } },
|
||||
});
|
||||
|
||||
function cluster() {
|
||||
const server = createHost({ self: { kind: "host" }, name: "server", onError: () => {} });
|
||||
let worker: HostImpl | undefined;
|
||||
let toWorker: CountingLink | undefined;
|
||||
server.route("session:", {
|
||||
principal: (name) => ({ host: name }),
|
||||
authorize: () => true,
|
||||
resolve: async (name) => {
|
||||
if (toWorker !== undefined) return toWorker;
|
||||
const [down, up] = memoryLinkPair();
|
||||
const created = new HostImpl({
|
||||
self: { kind: "host" },
|
||||
name,
|
||||
upstream: { link: up, principal: { kind: "server" } },
|
||||
onError: () => {},
|
||||
});
|
||||
created.serve(Counter, (_input, _principal, self) => {
|
||||
const n = self.state("n", Type.Number(), 0);
|
||||
return {
|
||||
variant: "main",
|
||||
n,
|
||||
add: (delta: number, ctx: Ctx) => {
|
||||
n.replace(ctx, n.value + delta);
|
||||
},
|
||||
};
|
||||
});
|
||||
worker = created;
|
||||
toWorker = new CountingLink(down);
|
||||
return toWorker;
|
||||
},
|
||||
});
|
||||
const browser = () => {
|
||||
const [down, up] = memoryLinkPair();
|
||||
server.accept(down, { principal: { guest: "g" } });
|
||||
const host = createHost({
|
||||
self: { kind: "host" },
|
||||
name: "browser",
|
||||
kind: "client",
|
||||
upstream: { link: up, principal: { kind: "server" } },
|
||||
onError: () => {},
|
||||
});
|
||||
return { host, up };
|
||||
};
|
||||
const origins = () =>
|
||||
[...(worker?.upstream?.served.values() ?? [])].map((served) =>
|
||||
served.kind === "local" ? served.origin : undefined,
|
||||
);
|
||||
return { server, browser, origins, sentToWorker: () => toWorker?.sent ?? [] };
|
||||
}
|
||||
|
||||
it("a relayed hold carries its client connection's id; the server's own holds carry none", async () => {
|
||||
const c = cluster();
|
||||
const one = c.browser();
|
||||
const two = c.browser();
|
||||
await one.host.hold(Counter, { session: "S1" }).ready;
|
||||
await two.host.hold(Counter, { session: "S1" }).ready;
|
||||
const own = c.server.hold(Counter, { session: "S1" });
|
||||
await own.ready;
|
||||
const holds = c.sentToWorker().filter((frame): frame is Extract<Frame, { t: "hold" }> => frame.t === "hold");
|
||||
assert.equal(holds.length, 3);
|
||||
const relayed = holds.filter((frame) => frame.origin !== undefined);
|
||||
assert.equal(relayed.length, 2);
|
||||
assert.notEqual(relayed[0]!.origin, relayed[1]!.origin, "two tabs, two origins");
|
||||
assert.equal(holds.filter((frame) => frame.origin === undefined).length, 1);
|
||||
|
||||
// Disconnect is departure: the worker loses that origin at once; a reconnect holds under a new id.
|
||||
const before = new Set(c.origins());
|
||||
one.up.close();
|
||||
await until(() => c.origins().filter((origin) => origin !== undefined).length === 1);
|
||||
const three = c.browser();
|
||||
await three.host.hold(Counter, { session: "S1" }).ready;
|
||||
const after = c.origins().filter((origin) => origin !== undefined);
|
||||
assert.equal(after.length, 2);
|
||||
assert.ok(
|
||||
after.some((origin) => !before.has(origin)),
|
||||
"a new connection gets a new id",
|
||||
);
|
||||
await c.server.close();
|
||||
});
|
||||
|
||||
it("an origin a client sends is ignored: the server stamps its own", async () => {
|
||||
const c = cluster();
|
||||
const [down, raw] = memoryLinkPair();
|
||||
c.server.accept(down, { principal: { guest: "g" } });
|
||||
raw.receive(() => {});
|
||||
raw.send({ t: "hello", protocol: PROTOCOL_VERSION, name: "raw", kind: "client", codeVersion: 0 });
|
||||
raw.send({
|
||||
t: "hold",
|
||||
id: 1,
|
||||
at: "session:S1",
|
||||
token: Counter.description,
|
||||
input: { session: "S1" },
|
||||
origin: "spoofed",
|
||||
});
|
||||
await until(() => c.sentToWorker().some((frame) => frame.t === "hold"));
|
||||
const hold = c.sentToWorker().find((frame) => frame.t === "hold");
|
||||
assert.ok(hold?.t === "hold" && hold.origin !== undefined && hold.origin !== "spoofed");
|
||||
await c.server.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("protocol version 4", () => {
|
||||
it("a version 3 peer is refused; shapes carry no code version", async () => {
|
||||
const { server, browser, link } = setup();
|
||||
await browser.hold(Notes, {}).ready;
|
||||
const shape = link.received.find((frame) => frame.t === "shape");
|
||||
assert.ok(shape !== undefined && !("codeVersion" in shape));
|
||||
assert.deepEqual(shape.t === "shape" ? [...shape.state].sort() : [], ["notes", "title"]);
|
||||
|
||||
const [down, raw] = memoryLinkPair();
|
||||
server.accept(down, { principal: { user: "old" } });
|
||||
const closed = new Promise<string>((resolve) => (raw as Link).onClose(resolve));
|
||||
raw.receive(() => {});
|
||||
raw.send({ t: "hello", protocol: 3, name: "old", kind: "client", codeVersion: 0 });
|
||||
assert.equal(await closed, `protocol-version ${PROTOCOL_VERSION}`);
|
||||
assert.equal(PROTOCOL_VERSION, 4);
|
||||
await browser.close();
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user