mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-06 23:44:36 +00:00
The contract field placement is hostAddress, required on every remote() (type and declaration error); the fallback for remote types without one is gone. invalid/placement is invalid/host_address. host/placement.ts (Placement) is host/destination.ts (Destination); here is decided by address alone, and a host without an address refuses to serve a remote type. A hold always carries its address on the wire (the binary codec's ADDRESS flag is gone, the text codec requires it). Tests and examples declare their host addresses.
119 lines
5.5 KiB
TypeScript
119 lines
5.5 KiB
TypeScript
// Remote objects over one link: a server and a client host, a remote contract with two variants, a factory choosing
|
|
// the variant per principal over a shared local store, and the client's replicas following the server's commits.
|
|
// Run from packages/chord:
|
|
// node --conditions=source --experimental-strip-types test/next/examples/08-remote.ts
|
|
import { BACKGROUND_CONTEXT } from "../../../src/context/index.ts";
|
|
import { arr, hostKind, int, local, method, obj, remote, str } from "../../../src/next/contracts/index.ts";
|
|
import { defineState } from "../../../src/next/documents/index.ts";
|
|
import { createHost, defineFacet } from "../../../src/next/host/index.ts";
|
|
import { memoryLinkPair } from "../../../src/next/link/index.ts";
|
|
import { mapShared } from "../../../src/next/state/index.ts";
|
|
|
|
const context = BACKGROUND_CONTEXT;
|
|
|
|
// The app's principals: who calls. Links stamp them; a caller never sets its own.
|
|
type P = { kind: "owner" } | { kind: "guest"; session: string } | { kind: "server" };
|
|
|
|
const Note = obj({ text: str(), author: str() });
|
|
const PublicNote = obj({ text: str() });
|
|
|
|
// The server's host kind: every remote contract names the address of the host serving its objects.
|
|
const Server = hostKind("server");
|
|
|
|
// A remote contract: one object per (type, params, principal key), in the variant its factory chooses.
|
|
const Notes = remote("example.notes", {
|
|
version: 1,
|
|
params: { session: str() },
|
|
hostAddress: () => Server(),
|
|
variants: {
|
|
editor: { state: { notes: arr(Note), count: int() }, methods: { add: method(obj({ text: str() })) } },
|
|
reader: { state: { notes: arr(PublicNote), count: int() } },
|
|
},
|
|
});
|
|
|
|
// The shared store: the notes of a session once, behind per-principal views (design §6). Local types never cross a link.
|
|
const NoteStore = local("example.note-store", {
|
|
version: 1,
|
|
params: { session: str() },
|
|
members: { notes: arr(Note), add: method(Note) },
|
|
});
|
|
|
|
const NotesDoc = defineState("notes", { schema: obj({ list: arr(Note) }), version: 1, initial: () => ({ list: [] }) });
|
|
|
|
// A redaction computed once per store element, so a reader's derive keeps unchanged notes identical.
|
|
const redact = mapShared((note: { text: string; author: string }) => ({ text: note.text }));
|
|
|
|
const facet = defineFacet<P>((host) => {
|
|
host.serve(NoteStore, (_, self) => {
|
|
const doc = self.state(NotesDoc);
|
|
return {
|
|
notes: self.derive([doc], ([d]) => d.list),
|
|
add: async (note, ctx) => {
|
|
await doc.change((draft) => void draft.list.push(note), ctx);
|
|
return null;
|
|
},
|
|
};
|
|
});
|
|
// A remote factory gets the principal its object is for and returns one variant with exactly its members.
|
|
host.serve(Notes, ({ session }, principal, self) => {
|
|
const store = self.get(NoteStore, { session });
|
|
const count = self.derive([store.notes], ([n]) => n.length);
|
|
if (principal.kind === "guest") {
|
|
return { variant: "reader", members: { notes: self.derive([store.notes], ([n]) => redact(n)), count } };
|
|
}
|
|
return {
|
|
variant: "editor",
|
|
members: {
|
|
notes: store.notes,
|
|
count,
|
|
add: async ({ text }, ctx) => store.add({ text, author: principal.kind }, ctx),
|
|
},
|
|
};
|
|
});
|
|
});
|
|
|
|
// 1. The server, at its address: it acts as `server` for its own requests.
|
|
const server = createHost<P>({
|
|
address: Server(),
|
|
principal: { kind: "server" },
|
|
onError: (error) => console.error("server:", error),
|
|
});
|
|
await server.load([{ name: "@example/notes", facet }], context);
|
|
|
|
// 2. A client (a tab): no principal of its own. The entry point authenticated it as a guest of S1 and stamps that on
|
|
// the link; the client connects before it loads (its plugins may reference server types).
|
|
const [up, down] = memoryLinkPair();
|
|
server.accept(up, { principal: { kind: "guest", session: "S1" } }, context);
|
|
const client = createHost<P>({ onError: (error) => console.error("client:", error) });
|
|
client.connect(down, { principal: { kind: "server" } }, context);
|
|
await client.load([], context);
|
|
|
|
// 3. The server writes as an editor; the guest reads the redacted view over the link.
|
|
const own = await server.get(Notes, { session: "S1" }, context);
|
|
if (own.variant === "editor") await own.add({ text: "hello" }, context);
|
|
const notes = await client.get(Notes, { session: "S1" }, context);
|
|
console.log("guest variant:", notes.variant);
|
|
console.log("guest notes:", JSON.stringify(notes.notes.value), "count:", notes.count.value);
|
|
|
|
// 4. A commit on the server reaches the client as one state message: both members change in the same batch.
|
|
const watch = await notes.notes.watch(context);
|
|
watch.start(async (call) => {
|
|
if (call.kind === "value") console.log("frame:", JSON.stringify(call.value), "ops:", JSON.stringify(call.ops));
|
|
});
|
|
if (own.variant === "editor") await own.add({ text: "world" }, context);
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
console.log("count after the commit:", notes.count.value);
|
|
|
|
// 5. TypeScript narrows on `variant`: a reader has no `add` (a forged call would fail `invalid/member` at the server).
|
|
if (notes.variant === "reader") {
|
|
// @ts-expect-error: `add` is not a member of the reader variant
|
|
void notes.add;
|
|
}
|
|
|
|
// 6. Closing the server closes its links first: the client's reference goes loading and keeps its last values until
|
|
// a new link holds it again (10-drops.ts). Closing the client releases it and ends its watches.
|
|
await server.close(context);
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
console.log("after the server closed:", notes.state.value, "count:", notes.count.value);
|
|
await client.close(context);
|
|
console.log("after the client closed:", notes.state.value, await watch.closed);
|