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:
517
third_party/pi/packages/chord/test/tutorial.test.ts
vendored
Normal file
517
third_party/pi/packages/chord/test/tutorial.test.ts
vendored
Normal file
@@ -0,0 +1,517 @@
|
||||
// example:imports
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { it, onTestFinished } from "vitest";
|
||||
import {
|
||||
type Ctx,
|
||||
createHost,
|
||||
defineFacet,
|
||||
defineRegistry,
|
||||
type Facet,
|
||||
type FacetApi,
|
||||
type Factory,
|
||||
forbidden,
|
||||
type Host,
|
||||
hostNamed,
|
||||
invalid,
|
||||
isChordError,
|
||||
local,
|
||||
memoryLinkPair,
|
||||
method,
|
||||
opaque,
|
||||
remote,
|
||||
type StateOptions,
|
||||
} from "../src/index.ts";
|
||||
|
||||
// end-example:imports
|
||||
|
||||
// Compare every printed example with its runnable source, not a second handwritten copy.
|
||||
const tutorial = readFileSync(new URL("../docs/tutorial.md", import.meta.url), "utf8");
|
||||
const source = readFileSync(new URL(import.meta.url), "utf8");
|
||||
const examples = [...source.matchAll(/^[\t ]*\/\/ example:([\w-]+)\n([\s\S]*?)^[\t ]*\/\/ end-example:\1$/gm)];
|
||||
const printed = [...tutorial.matchAll(/<!-- example:([\w-]+) -->\n```ts\n([\s\S]*?)\n```/g)];
|
||||
function normalize(text: string): string {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
function checkExamples(section: string): void {
|
||||
assert.equal(printed.length, examples.length, "every tutorial example must have runnable source");
|
||||
assert.equal((tutorial.match(/```ts\n/g) ?? []).length, printed.length, "no untracked TypeScript examples");
|
||||
for (const match of examples.filter((entry) => entry[1] === "imports" || entry[1]?.startsWith(section))) {
|
||||
const copies = printed.filter((entry) => entry[1] === match[1]);
|
||||
assert.equal(copies.length, 1, `one printed copy of ${match[1]}`);
|
||||
assert.equal(normalize(copies[0]![2]!), normalize(match[2]!), `tutorial drift: ${match[1]}`);
|
||||
}
|
||||
}
|
||||
|
||||
// example:02-contract
|
||||
const TodoLabel = local("tutorial.todos", {
|
||||
version: 1,
|
||||
input: Type.Object({ list: Type.String() }),
|
||||
members: { label: method(undefined, Type.String()) },
|
||||
});
|
||||
// end-example:02-contract
|
||||
|
||||
// example:03-contract
|
||||
const Todo = Type.Object({ text: Type.String(), done: Type.Boolean() });
|
||||
const Rows = Type.Array(Todo);
|
||||
const TodoStore = local("tutorial.todos", {
|
||||
version: 1,
|
||||
input: Type.Object({ list: Type.String() }),
|
||||
members: {
|
||||
label: method(undefined, Type.String()),
|
||||
rows: Rows,
|
||||
openCount: Type.Number(),
|
||||
add: method(Type.Object({ text: Type.String() })),
|
||||
finish: method(Type.Object({ index: Type.Integer() })),
|
||||
reset: method(Rows),
|
||||
},
|
||||
});
|
||||
// end-example:03-contract
|
||||
|
||||
// example:03-factory
|
||||
/** The store's factory; `options` versions its rows (section 9). */
|
||||
const todoStore =
|
||||
(options?: StateOptions<Static<typeof Rows>>): Factory<typeof TodoStore> =>
|
||||
({ list }, _principal, self) => {
|
||||
const rows = self.state("rows", Rows, [], options);
|
||||
const openCount = self.derive([rows], (todos) => todos.filter((todo) => !todo.done).length);
|
||||
return {
|
||||
label: () => `Todos for ${list}`,
|
||||
rows,
|
||||
openCount,
|
||||
add: ({ text }, ctx) => {
|
||||
rows.change(ctx, (draft) => {
|
||||
draft.push({ text, done: false });
|
||||
});
|
||||
},
|
||||
finish: ({ index }, ctx) => {
|
||||
rows.change(ctx, (draft) => {
|
||||
const todo = draft[index];
|
||||
if (!todo) throw invalid("args", "No such todo");
|
||||
todo.done = true;
|
||||
});
|
||||
},
|
||||
reset: (next, ctx) => {
|
||||
rows.replace(ctx, next, { diff: true });
|
||||
},
|
||||
};
|
||||
};
|
||||
const todoFactory = todoStore();
|
||||
// end-example:03-factory
|
||||
|
||||
// example:05-views
|
||||
const Todos = remote("tutorial.todo-view", {
|
||||
version: 1,
|
||||
input: Type.Object({ list: Type.String() }),
|
||||
host: hostNamed("server"),
|
||||
variants: {
|
||||
viewer: { state: { rows: Rows } },
|
||||
editor: {
|
||||
state: { rows: Rows },
|
||||
methods: { add: method(Type.Object({ text: Type.String() })) },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/** Who may ask: the owner, a guest, or the server's own code. */
|
||||
type Principal = "owner" | "guest" | "server";
|
||||
|
||||
function serveViews(api: Pick<Host<Principal>, "serve">): void {
|
||||
api.serve(Todos, ({ list }, principal, self) => {
|
||||
if (list !== "family") throw forbidden("This list is not shared");
|
||||
const store = self.get(TodoStore, { list });
|
||||
if (principal === "guest") {
|
||||
return { variant: "viewer", rows: store.rows };
|
||||
}
|
||||
return {
|
||||
variant: "editor",
|
||||
rows: store.rows,
|
||||
add: ({ text }, ctx) => {
|
||||
if (text.trim().length === 0) throw invalid("args", "A todo needs text");
|
||||
return store.add({ text }, ctx);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
// end-example:05-views
|
||||
|
||||
it("01: objects live inside hosts", { timeout: 5000 }, async () => {
|
||||
checkExamples("01");
|
||||
// example:01
|
||||
const host = createHost({ self: "server", name: "todo-server" });
|
||||
assert.equal(host.name, "todo-server");
|
||||
await host.close();
|
||||
// end-example:01
|
||||
});
|
||||
|
||||
it("02: first contract, factory, reference and root", { timeout: 5000 }, async () => {
|
||||
checkExamples("02");
|
||||
onTestFinished(() => host.close());
|
||||
// example:02-call
|
||||
const host = createHost({ self: "server" });
|
||||
host.serve(TodoLabel, ({ list }) => ({ label: () => `Todos for ${list}` }));
|
||||
const result = await host.root(async (ctx) => {
|
||||
const todos = await ctx.get(TodoLabel, { list: "family" });
|
||||
return todos.label(ctx);
|
||||
});
|
||||
assert.equal(result, "Todos for family");
|
||||
// end-example:02-call
|
||||
});
|
||||
|
||||
it("03: cells publish values and derivations", { timeout: 5000 }, async () => {
|
||||
checkExamples("03");
|
||||
onTestFinished(() => host.close());
|
||||
// example:03-call
|
||||
const host = createHost({ self: "server", dev: true });
|
||||
host.serve(TodoStore, todoFactory);
|
||||
const todos = await host.get(TodoStore, { list: "family" });
|
||||
const counts: (number | undefined)[] = [];
|
||||
const stop = todos.openCount.subscribe((value) => counts.push(value));
|
||||
await host.root(async (ctx) => {
|
||||
await todos.add({ text: "Buy milk" }, ctx);
|
||||
await todos.finish({ index: 0 }, ctx);
|
||||
await todos.reset([], ctx);
|
||||
});
|
||||
assert.deepEqual(counts, [0, 1, 0]);
|
||||
assert.deepEqual(todos.rows.value, []);
|
||||
stop();
|
||||
todos.release();
|
||||
// end-example:03-call
|
||||
});
|
||||
|
||||
it("04: holds, asynchronous resources and release", { timeout: 5000 }, async () => {
|
||||
checkExamples("04");
|
||||
onTestFinished(() => host.close());
|
||||
// example:04-database
|
||||
const Database = local("tutorial.database", {
|
||||
version: 1,
|
||||
input: Type.Object({}),
|
||||
members: { db: opaque<{ saved: string[] }>("tutorial.database@1") },
|
||||
});
|
||||
const SavedTodos = local("tutorial.saved-todos", {
|
||||
version: 1,
|
||||
input: Type.Object({ list: Type.String() }),
|
||||
members: { add: method(Type.Object({ text: Type.String() })) },
|
||||
});
|
||||
const host = createHost({ self: "server" });
|
||||
let opens = 0;
|
||||
const closed = Promise.withResolvers<void>();
|
||||
host.serve(Database, (_input, _principal, self) => {
|
||||
const db = self.resource("db", {
|
||||
version: 1,
|
||||
open: async () => {
|
||||
opens++;
|
||||
return { saved: [] as string[] };
|
||||
},
|
||||
close: () => closed.resolve(),
|
||||
});
|
||||
return { db: self.expose(db) };
|
||||
});
|
||||
// end-example:04-database
|
||||
// example:04-saving
|
||||
host.serve(TodoStore, todoFactory);
|
||||
host.serve(
|
||||
SavedTodos,
|
||||
({ list }, _principal, self) => {
|
||||
const database = self.get(Database, {});
|
||||
const todos = self.get(TodoStore, { list });
|
||||
return {
|
||||
add: async ({ text }, ctx) => {
|
||||
database.db!.saved.push(text);
|
||||
await todos.add({ text }, ctx);
|
||||
},
|
||||
};
|
||||
},
|
||||
{ linger: 0 },
|
||||
);
|
||||
// end-example:04-saving
|
||||
// example:04-call
|
||||
const saved = await host.get(SavedTodos, { list: "family" });
|
||||
const database = await host.get(Database, {});
|
||||
await host.root((ctx) => saved.add({ text: "Buy milk" }, ctx));
|
||||
assert.deepEqual(database.db!.saved, ["Buy milk"]);
|
||||
assert.equal(opens, 1);
|
||||
const statuses: string[] = [];
|
||||
saved.onStatus((status) => statuses.push(status));
|
||||
database.release();
|
||||
saved.release();
|
||||
await closed.promise;
|
||||
assert.equal(saved.status, "released");
|
||||
assert.ok(statuses.includes("released"));
|
||||
// end-example:04-call
|
||||
});
|
||||
|
||||
it("05: principals select views of one shared store", { timeout: 5000 }, async () => {
|
||||
checkExamples("05");
|
||||
onTestFinished(() => host.close());
|
||||
// example:05-call
|
||||
const host = createHost<Principal>({ name: "server", self: "server", principalKey: (principal) => principal });
|
||||
host.serve(TodoStore, todoFactory);
|
||||
serveViews(host);
|
||||
const owner = await host.get(Todos, { list: "family" }, { principal: "owner" });
|
||||
const guest = await host.get(Todos, { list: "family" }, { principal: "guest" });
|
||||
assert.ok(owner.variant === "editor"); // narrows the reference to the editor variant
|
||||
assert.equal(guest.variant, "viewer");
|
||||
await host.root((ctx) => owner.add({ text: "Buy milk" }, ctx), { principal: "owner" });
|
||||
assert.deepEqual(guest.rows.value, [{ text: "Buy milk", done: false }]);
|
||||
// @ts-expect-error A viewer has no add: TypeScript refuses it, and so does the provider.
|
||||
await assert.rejects(guest.add({ text: "Not allowed" }), (error) => isChordError(error, "not_in_variant"));
|
||||
await assert.rejects(owner.add({ text: " " }), (error) => isChordError(error, "invalid"));
|
||||
// end-example:05-call
|
||||
});
|
||||
|
||||
it("06: remote methods and live state over a memory link", { timeout: 5000 }, async () => {
|
||||
checkExamples("06");
|
||||
onTestFinished(() => server.close());
|
||||
onTestFinished(() => browser.close());
|
||||
// example:06
|
||||
const server = createHost<Principal>({ self: "server", name: "server" });
|
||||
server.serve(TodoStore, todoFactory);
|
||||
serveViews(server);
|
||||
const [down, up] = memoryLinkPair();
|
||||
server.accept(down, { principal: "owner", kind: "client" });
|
||||
const browser = createHost({
|
||||
self: "browser",
|
||||
name: "browser",
|
||||
kind: "client",
|
||||
upstream: { link: up, principal: "server" },
|
||||
});
|
||||
const todos = await browser.get(Todos, { list: "family" });
|
||||
assert.ok(todos.variant === "editor");
|
||||
assert.deepEqual(todos.rows.value, []);
|
||||
const updated = Promise.withResolvers<void>();
|
||||
const stop = todos.rows.subscribe((rows) => {
|
||||
if (rows.length === 1) updated.resolve();
|
||||
});
|
||||
await browser.root((ctx) => todos.add({ text: "Buy milk" }, ctx));
|
||||
await updated.promise;
|
||||
assert.deepEqual(todos.rows.value, [{ text: "Buy milk", done: false }]);
|
||||
// Deliberately bypass TypeScript to simulate a malformed caller.
|
||||
await assert.rejects(todos.add({ text: 123 } as never), (error) => isChordError(error, "invalid"));
|
||||
stop();
|
||||
// A held reference keeps its state without listeners.
|
||||
assert.deepEqual(todos.rows.value, [{ text: "Buy milk", done: false }]);
|
||||
todos.release();
|
||||
// end-example:06
|
||||
});
|
||||
|
||||
it("07: listeners react and tasks start fresh calls", { timeout: 5000 }, async () => {
|
||||
checkExamples("07");
|
||||
onTestFinished(() => host.close());
|
||||
// example:07
|
||||
const Activity = local("tutorial.activity", {
|
||||
version: 1,
|
||||
input: Type.Object({}),
|
||||
members: { seen: Type.Number() },
|
||||
});
|
||||
const host = createHost({ self: "server" });
|
||||
host.serve(TodoStore, todoFactory);
|
||||
const observed = Promise.withResolvers<void>();
|
||||
const seeded = Promise.withResolvers<void>();
|
||||
const stopped = Promise.withResolvers<void>();
|
||||
host.serve(Activity, (_input, _principal, self) => {
|
||||
const todos = self.get(TodoStore, { list: "family" });
|
||||
const seen = self.state("seen", Type.Number(), 0);
|
||||
self.on("rows", todos.rows, (rows, ctx) => {
|
||||
seen.replace(ctx, rows?.length ?? 0);
|
||||
if (rows?.length === 1) observed.resolve();
|
||||
});
|
||||
self.task(async (ctx) => {
|
||||
await todos.add({ text: "Review the list" }, ctx);
|
||||
seeded.resolve();
|
||||
await new Promise<void>((resolve) => {
|
||||
if (ctx.signal.aborted) resolve();
|
||||
else ctx.signal.addEventListener("abort", () => resolve(), { once: true });
|
||||
});
|
||||
stopped.resolve();
|
||||
});
|
||||
return { seen };
|
||||
});
|
||||
const activity = await host.get(Activity, {});
|
||||
await Promise.all([seeded.promise, observed.promise]);
|
||||
assert.equal(activity.seen.value, 1);
|
||||
activity.release();
|
||||
await stopped.promise;
|
||||
// end-example:07
|
||||
});
|
||||
|
||||
it("08: plugins contribute registry entries and expose callbacks", { timeout: 5000 }, async () => {
|
||||
checkExamples("08");
|
||||
onTestFinished(() => host.close());
|
||||
// example:08
|
||||
const Actions = defineRegistry<(ctx: Ctx) => Promise<number>>("tutorial.actions");
|
||||
const Menu = local("tutorial.menu", { version: 1, input: Type.Object({}) });
|
||||
let click: (() => Promise<number>) | undefined;
|
||||
const storeFacet: Facet = {
|
||||
name: "todo-store",
|
||||
...defineFacet((api) => api.serve(TodoStore, todoFactory)),
|
||||
};
|
||||
const menuFacet: Facet = {
|
||||
name: "todo-menu",
|
||||
...defineFacet(
|
||||
(api) => {
|
||||
api.serve(
|
||||
Menu,
|
||||
(_input, _principal, self) => {
|
||||
const todos = self.get(TodoStore, { list: "family" });
|
||||
self.register(Actions, {
|
||||
addSample: async (ctx) => {
|
||||
await todos.add({ text: "Try plugins" }, ctx);
|
||||
return todos.openCount.value;
|
||||
},
|
||||
});
|
||||
return {};
|
||||
},
|
||||
{ lifetime: "host" },
|
||||
);
|
||||
click = () => api.root((ctx) => api.registry(Actions).get("addSample", ctx)!(ctx));
|
||||
},
|
||||
{ uses: [TodoStore] },
|
||||
),
|
||||
};
|
||||
const host = createHost({ self: "server" });
|
||||
await host.load([menuFacet, storeFacet], { awaitHostObjects: false });
|
||||
await host.ready();
|
||||
assert.equal(await click!(), 1);
|
||||
assert.equal(host.registry(Actions).table().size, 1);
|
||||
// end-example:08
|
||||
});
|
||||
|
||||
it("09: compatible reload, migration and an old-contract adapter", { timeout: 5000 }, async () => {
|
||||
checkExamples("09");
|
||||
onTestFinished(() => host.close());
|
||||
// example:09-compatible
|
||||
const host = createHost({ self: "server" });
|
||||
const facet = (label: string): Facet => ({
|
||||
name: "todos",
|
||||
setup: (api) =>
|
||||
api.serve(TodoStore, (input, principal, self) => ({
|
||||
...todoFactory(input, principal, self),
|
||||
label: () => label,
|
||||
})),
|
||||
});
|
||||
await host.load([facet("Original")]);
|
||||
const todos = await host.get(TodoStore, { list: "family" });
|
||||
await todos.add({ text: "Buy milk" });
|
||||
const entered = Promise.withResolvers<void>();
|
||||
const resume = Promise.withResolvers<void>();
|
||||
const oldRoot = host.root(async (ctx) => {
|
||||
const first = await todos.label(ctx);
|
||||
entered.resolve();
|
||||
await resume.promise; // Test-only pause: pin an old caller during reload.
|
||||
return [first, await todos.label(ctx)];
|
||||
});
|
||||
await entered.promise;
|
||||
const compatible = await host.reload([facet("Updated")]);
|
||||
assert.deepEqual(compatible.gated, []);
|
||||
assert.equal(await todos.label(), "Updated");
|
||||
resume.resolve();
|
||||
assert.deepEqual(await oldRoot, ["Original", "Original"]);
|
||||
assert.equal(todos.rows.value?.[0]?.text, "Buy milk");
|
||||
// end-example:09-compatible
|
||||
// example:09-migrate
|
||||
const events: string[] = [];
|
||||
const stopEvents = todos.rows.events((event) => events.push(event.type));
|
||||
const migrated = await host.reload([
|
||||
{
|
||||
name: "todos",
|
||||
setup: (api) =>
|
||||
api.serve(
|
||||
TodoStore,
|
||||
todoStore({
|
||||
version: 2,
|
||||
migrate: (old, from) => {
|
||||
assert.equal(from, 1);
|
||||
return (old as Static<typeof Rows>).map((todo) => ({ ...todo, text: todo.text.toUpperCase() }));
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
]);
|
||||
assert.equal(migrated.gated.length, 1);
|
||||
assert.deepEqual(todos.rows.value, [{ text: "BUY MILK", done: false }]);
|
||||
assert.deepEqual(events, ["snap", "reset"]);
|
||||
stopEvents();
|
||||
// end-example:09-migrate
|
||||
// example:09-contract
|
||||
// Remove label from the new provider's contract: it is now called heading.
|
||||
const { label: _label, ...members } = TodoStore.def.members;
|
||||
const RenamedStore = local("tutorial.todos", {
|
||||
version: 2,
|
||||
input: TodoStore.def.input,
|
||||
members: { ...members, heading: method(undefined, Type.String()) },
|
||||
});
|
||||
await host.reload([
|
||||
{
|
||||
name: "todos",
|
||||
setup: (api) =>
|
||||
api.serve(
|
||||
RenamedStore,
|
||||
(input, principal, self) => {
|
||||
const { label, ...implementation } = todoStore({ version: 2 })(input, principal, self);
|
||||
return { ...implementation, heading: label };
|
||||
},
|
||||
{
|
||||
accepts: {
|
||||
1: (current) => ({ ...current, label: current.heading }),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
]);
|
||||
assert.equal(await todos.label(), "Todos for family");
|
||||
const newer = await host.get(RenamedStore, { list: "family" });
|
||||
assert.equal(await newer.heading(), "Todos for family");
|
||||
assert.equal(todos.rows.value?.[0]?.text, "BUY MILK");
|
||||
// end-example:09-contract
|
||||
});
|
||||
|
||||
it("10: stub a provider with a fake facet and check error codes", { timeout: 5000 }, async () => {
|
||||
checkExamples("10");
|
||||
onTestFinished(() => host.close());
|
||||
// example:10
|
||||
const host = createHost<Principal>({ name: "server", self: "server", dev: true });
|
||||
const received: string[] = [];
|
||||
const fake: Facet = {
|
||||
name: "fake-store",
|
||||
...defineFacet((api: FacetApi) => {
|
||||
api.serve(TodoStore, (input, principal, self) => ({
|
||||
...todoFactory(input, principal, self),
|
||||
add: ({ text }) => {
|
||||
received.push(text);
|
||||
},
|
||||
}));
|
||||
}),
|
||||
};
|
||||
await host.load([fake]);
|
||||
serveViews(host);
|
||||
const owner = await host.get(Todos, { list: "family" }, { principal: "owner" });
|
||||
assert.ok(owner.variant === "editor");
|
||||
await owner.add({ text: "Test delegation" });
|
||||
assert.deepEqual(received, ["Test delegation"]);
|
||||
await assert.rejects(owner.add({ text: "" }), (error) => isChordError(error, "invalid"));
|
||||
owner.release();
|
||||
await assert.rejects(owner.add({ text: "Too late" }), (error) => isChordError(error, "released"));
|
||||
// end-example:10
|
||||
});
|
||||
|
||||
it("11: short roots release temporary holds", { timeout: 5000 }, async () => {
|
||||
checkExamples("11");
|
||||
onTestFinished(() => host.close());
|
||||
// example:11
|
||||
const host = createHost({ self: "server" });
|
||||
host.serve(TodoStore, todoFactory);
|
||||
const screen = await host.get(TodoStore, { list: "family" });
|
||||
await host.root(async (ctx) => {
|
||||
const temporary = await ctx.get(TodoStore, { list: "family" });
|
||||
await temporary.add({ text: "Keep roots short" }, ctx);
|
||||
}); // The temporary reference is released here; screen still holds the store.
|
||||
assert.equal(screen.openCount.value, 1);
|
||||
screen.release(); // No reference remains; without persistence, disposal loses these rows.
|
||||
// end-example:11
|
||||
});
|
||||
Reference in New Issue
Block a user