pi-mono/packages/chord/test/examples/10-drops.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.9 KiB
TypeScript

// Surviving drops: a reference held before any link exists, `ref.state` as it follows the link, calls parked while
// loading and sent in order once live, a call in flight when the link drops, and the re-hold after `connect` again.
// Run from packages/chord:
// node --conditions=source --experimental-strip-types test/examples/10-drops.ts
import { BACKGROUND_CONTEXT, type Context } from "../../src/context/index.ts";
import { arr, hostKind, int, method, obj, remote, str } from "../../src/contracts/index.ts";
import { defineState } from "../../src/documents/index.ts";
import { createHost, defineFacet, type Host } from "../../src/host/index.ts";
import { type Link, 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 Notes = remote("example.notes", {
version: 1,
params: { session: str() },
hostAddress: () => Server(),
variants: {
editor: {
state: { notes: arr(str()) },
methods: { add: method(obj({ text: str() }), int()), slow: method() },
},
},
});
const NotesDoc = defineState("notes", { schema: obj({ list: arr(str()) }), version: 1, initial: () => ({ list: [] }) });
const facet = defineFacet<P>((host) => {
// Lingers for a minute after its last reference: a client that reconnects within it finds its notes again.
host.serve(
Notes,
(_, _principal, self) => {
const doc = self.state(NotesDoc);
return {
variant: "editor",
members: {
notes: self.derive([doc], ([d]) => d.list),
add: async ({ text }, ctx) => {
await doc.change((draft) => void draft.list.push(text), ctx);
return doc.value.list.length;
},
slow: (ctx) => new Promise((resolve) => ctx.abortSignal?.addEventListener("abort", () => resolve(null))),
},
};
},
{ linger: 60_000 },
);
});
const server = createHost<P>({
address: Server(),
principal: { kind: "server" },
onError: (error) => console.error("server:", error),
});
await server.load([{ name: "@example/notes", facet }], context);
const client = createHost<P>({ onError: (error) => console.error("client:", error) });
await client.load([], context);
/** A new link between the hosts: the server stamps the client as its owner. Returns the server's end. */
function connect(server: Host<P>, client: Host<P>, context: Context): Link {
const [up, down] = memoryLinkPair();
server.accept(up, { principal: { kind: "owner" } }, context);
client.connect(down, { principal: { kind: "server" } }, context);
return up;
}
const settle = () => new Promise((resolve) => setTimeout(resolve, 10));
// 1. A hold before any link: a reference at once, `loading`, its variant unknown. Calls on it park.
const notes = client.hold(Notes, { session: "S1" }, context);
console.log("held without a link:", notes.state.value, "variant:", notes.variant);
const watch = await notes.state.watch(context);
watch.start(async (call) => {
if (call.kind === "value") console.log(" ref.state →", call.value.kind);
});
const parked = notes.add({ text: "written while loading" }, context);
// 2. Connect: the reference is held over the link, goes live, and the parked call is sent.
let link = connect(server, client, context);
console.log("parked call's result once live:", await parked);
console.log("notes:", notes.notes.value, "variant:", notes.variant);
// 3. The link drops (the server's end closes it). A call in flight fails `unavailable` and is never resent; the
// reference goes loading and keeps its last values.
const inFlight = notes.slow(context).catch((error: Error) => `rejected: ${error.message}`);
await settle();
link.close();
console.log("in-flight call:", await inFlight);
await settle();
console.log("after the drop:", notes.state.value, "last notes:", notes.notes.value);
// 4. Calls made while the link is down park; they wait at most the reference's readyTimeout (60 s here).
const first = notes.add({ text: "first while down" }, context);
const second = notes.add({ text: "second while down" }, context);
// 5. The app connects again (its link adapter would): every reference is held again with fresh ids, its members get
// the new snapshot as one frame `[["r", value]]`, then the parked calls are sent in the order they were made.
const frames = await notes.notes.watch(context);
frames.start(async (call) => {
if (call.kind === "value") console.log(" notes frame:", JSON.stringify(call.ops));
});
link = connect(server, client, context);
console.log("parked results, in order:", await first, await second);
await settle();
console.log("notes after the re-hold:", notes.notes.value);
// 6. Release: `ref.state` is `ended` (its error says the holder released it) and its watch ends.
notes.release();
console.log("watch ended:", await watch.closed, "state:", notes.state.value);
await client.close(context);
await server.close(context);