pi-mono/packages/chord/test/examples/09-calls.ts
Mario Zechner e9c0ff4d84 feat(chord): Chord v2 is the package's root
src/next is src (the main export), src/next-node is src/node with short subpaths (/http, /workers, /plugins,
/plugins/runtime, /app, /surfaces, /web, /react, /tui, /tui/launcher); the foundations get /json and /watch;
Chord v1 moves to src/old (/old, /old/node) until p2 runs on v2; tests and docs move likewise (test/, docs/,
test/old, docs/old). Durable and env import the foundations from their subpaths.
2026-11-02 22:36:55 +00:00

115 lines
4.6 KiB
TypeScript

// Calls over a link: results, bytes as raw segments, ChordErrors and plugin errors, the caller's principal and
// incarnation in `ctx`, cancellation, and release rejecting the calls made through a reference.
// Run from packages/chord:
// node --conditions=source --experimental-strip-types test/examples/09-calls.ts
import { BACKGROUND_CONTEXT, withCancel } from "../../src/context/index.ts";
import { bytes, hostKind, int, method, obj, remote, str } from "../../src/contracts/index.ts";
import { isChordError, PluginError } from "../../src/data/index.ts";
import { createHost, defineFacet } from "../../src/host/index.ts";
import { memoryLinkPair } from "../../src/link/index.ts";
const context = BACKGROUND_CONTEXT;
type P = { kind: "owner" } | { kind: "server" };
// The server's host kind: a remote contract's `hostAddress` names the host serving its objects.
const Server = hostKind("server");
const Files = remote("example.files", {
version: 1,
params: {},
hostAddress: () => Server(),
variants: {
owner: {
methods: {
whoami: method(undefined, str()),
// A `bytes()` leaf: the argument and result travel as raw segments, never base64.
upload: method(obj({ name: str(), data: bytes() }), int()),
download: method(obj({ name: str() }), bytes()),
quota: method(),
slow: method(),
},
},
},
});
const stored = new Map<string, Uint8Array>();
const facet = defineFacet<P>((host) => {
host.serve(Files, () => ({
variant: "owner",
members: {
// ctx.principal is the caller as its link stamped it; ctx.incarnation the calling host's run (R20).
whoami: async (ctx) => `${ctx.principal?.kind} from ${ctx.incarnation.slice(0, 8)}…`,
upload: async ({ name, data }) => {
stored.set(name, data);
return data.byteLength;
},
download: async ({ name }) => stored.get(name) ?? new Uint8Array(),
// A PluginError crosses with its code, message and details; any other throw as code `internal`.
quota: async () => {
throw new PluginError("quota", "the store is full", { limit: 1 });
},
// A handler's context is aborted when its caller cancels or releases the reference.
slow: (ctx) =>
new Promise((resolve) =>
ctx.abortSignal?.addEventListener("abort", () => {
console.log("server: slow aborted:", (ctx.abortSignal?.reason as Error).message);
resolve(null);
}),
),
},
}));
});
const server = createHost<P>({
address: Server(),
principal: { kind: "server" },
onError: (error) => console.error("server:", error),
});
await server.load([{ name: "@example/files", facet }], context);
const [up, down] = memoryLinkPair();
server.accept(up, { principal: { kind: "owner" } }, context);
const client = createHost<P>({ onError: (error) => console.error("client:", error) });
client.connect(down, { principal: { kind: "server" } }, context);
await client.load([], context);
const files = await client.get(Files, {}, context);
// 1. A call resolves with its result; a method without arguments takes only the context.
console.log("whoami:", await files.whoami(context));
// 2. Bytes: 1 MiB goes out as one raw segment next to the JSON part.
const data = new Uint8Array(1024 * 1024).fill(7);
console.log("uploaded bytes:", await files.upload({ name: "chart.png", data }, context));
const back = await files.download({ name: "chart.png" }, context);
console.log("downloaded:", back.byteLength, "bytes, a", back.constructor.name);
// 3. Errors: arguments are checked at the provider (a ChordError); a plugin's own error keeps its code.
try {
// @ts-expect-error: `name` must be a string
await files.upload({ name: 1, data }, context);
} catch (error) {
if (isChordError(error, "invalid")) console.log("refused:", error.reason, error.message);
}
try {
await files.quota(context);
} catch (error) {
if (error instanceof PluginError) console.log("plugin error:", error.code, error.message, error.details);
}
// 4. Cancellation: the caller is rejected at once, the provider aborts the handler.
const { context: cancellable, cancel } = withCancel(context);
const slow = files.slow(cancellable);
setTimeout(() => cancel(new Error("the user gave up")), 5);
await slow.catch((error: unknown) => console.log("client: slow rejected:", (error as Error).message));
// 5. Release rejects the calls made through the reference; the provider aborts their handlers.
const pending = files.slow(context);
await new Promise((resolve) => setTimeout(resolve, 5));
files.release();
await pending.catch((error: unknown) => console.log("client: pending call:", (error as Error).message));
await new Promise((resolve) => setTimeout(resolve, 5));
await client.close(context);
await server.close(context);