pi-mono/packages/chord/test/examples/05-errors.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

94 lines
4.6 KiB
TypeScript

// Errors: what Chord refuses and how a caller tells the cases apart (`ChordError` with `code` and `reason`).
// Run from packages/chord:
// node --conditions=source --experimental-strip-types test/examples/05-errors.ts
import { BACKGROUND_CONTEXT } from "../../src/context/index.ts";
import { int, local, method, obj, str } from "../../src/contracts/index.ts";
import { isChordError } from "../../src/data/index.ts";
import { createHost, defineFacet, type HostError } from "../../src/host/index.ts";
import { constant } from "../../src/state/index.ts";
const context = BACKGROUND_CONTEXT;
/** Prints how `work` failed: a ChordError's code, reason and message, or a plain error's message. */
async function attempt(label: string, work: () => unknown): Promise<void> {
try {
await work();
console.log(`${label}: ok`);
} catch (error) {
if (isChordError(error))
console.log(`${label}: ${error.code}${error.reason ? `/${error.reason}` : ""} - ${error.message}`);
else console.log(`${label}: ${error instanceof Error ? error.message : String(error)}`);
}
}
const Echo = local("example.echo", {
version: 1,
params: { id: int() },
members: { id: int(), echo: method(obj({ text: str() }), str()) },
});
// Two types that get each other while they activate: a cycle.
const Ping = local("example.ping", { version: 1, params: {}, members: { n: int() } });
const Pong = local("example.pong", { version: 1, params: {}, members: { n: int() } });
const Broken = local("example.broken", { version: 1, params: { id: int() }, members: { n: int() } });
const Sloppy = local("example.sloppy", { version: 1, params: { id: int() }, members: { n: int() } });
const facet = defineFacet((host) => {
host.serve(Echo, ({ id }) => ({ id: constant(id), echo: async ({ text }) => text }));
host.serve(Broken, () => {
throw new Error("broken: missing configuration");
});
// Plugins are bundled without type checks; Chord checks the implementation's members when the object activates.
host.serve(Sloppy, () => ({ n: constant(1), extra: constant(2) }) as never);
});
// Ping and Pong are served by a second plugin: types with empty params activate at load.
const cycle = defineFacet((host) => {
host.serve(Ping, (_, self) => ({ n: self.get(Pong, {}).n }));
host.serve(Pong, (_, self) => ({ n: self.get(Ping, {}).n }));
});
// Failed activations are reported to onError; 07-failures.ts shows the other reports.
const onError = (error: HostError): void => {
if (error.kind === "activation_failed") console.log(`reported: ${error.object} failed ${error.failure.kind}`);
else console.log("reported:", error.kind);
};
// A dependency cycle fails every object on it broken, with invalid/cycle naming the chain. Load still resolves: the
// start barrier waits for each object's first outcome, live or failed.
const host = createHost({ onError });
await host.load(
[
{ name: "@example/errors", facet },
{ name: "@example/cycle", facet: cycle },
],
context,
);
await attempt("get in a cycle", () => host.get(Ping, {}, context));
const echo = await host.get(Echo, { id: 1 }, context);
// Arguments are checked against the contract at the object: wrong types and unknown fields are invalid/args.
// @ts-expect-error: text must be a string
await attempt("wrong argument", () => echo.echo({ text: 1 }, context));
// @ts-expect-error: an unknown field
await attempt("unknown field", () => echo.echo({ text: "hi", extra: true }, context));
// Params select the object and are checked on every get: invalid/params.
// @ts-expect-error: id must be an integer
await attempt("bad params", () => host.get(Echo, { id: "one" }, context));
// An implementation with a member its contract does not declare fails activation.
await attempt("unknown member", () => host.get(Sloppy, { id: 1 }, context));
// A get needs the provider's version and fingerprint; another declaration of the type does not fit. That is not the
// caller's fault (a reload may bring the version): the get waits up to its readyTimeout, then rejects with the cause.
const EchoV2 = local("example.echo", { version: 2, params: { id: int() }, members: { id: int() } });
await attempt("contract mismatch", () => host.get(EchoV2, { id: 1 }, context, { readyTimeout: 100 }));
// A factory that throws fails broken: the get rejects at once with its error, and no object is left behind.
await attempt("throwing factory", () => host.get(Broken, { id: 1 }, context));
// After release, every use of a reference throws released.
echo.release();
await attempt("released reference", () => echo.echo({ text: "hi" }, context));
await host.close(context);
await attempt("get after close", () => host.get(Echo, { id: 1 }, context));