From 436d99e04b02d5ed784e09dba8601383e7fc8e76 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 31 Oct 2026 14:01:40 +0000 Subject: [PATCH] docs(chord): tutorial: one schema for contract and document --- .../docs/next/guide-plugins-hosts/book.md | 18 ++++++++++++------ .../docs/next/guide-plugins-hosts/index.html | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/chord/docs/next/guide-plugins-hosts/book.md b/packages/chord/docs/next/guide-plugins-hosts/book.md index 83fe5051a..ab04e2119 100644 --- a/packages/chord/docs/next/guide-plugins-hosts/book.md +++ b/packages/chord/docs/next/guide-plugins-hosts/book.md @@ -84,18 +84,20 @@ Start without plugins. One host, one object that keeps notes: ```ts import { createHost, defineFacet, defineState, local, method, obj, arr, str } from "@earendil-works/chord/next"; -// the contract: what a Notes object looks like from outside +const NoteList = obj({ items: arr(str()) }); // one schema, used by the contract and the document + +// the contract: what a Notes object offers const Notes = local("notes.store", { version: 1, params: {}, // one object per host: no params members: { - list: obj({ items: arr(str()) }), // state anyone can read and watch + list: NoteList, // state anyone can read and watch add: method(obj({ text: str() })), // a method: arguments checked by Chord }, }); -// the document the object keeps -const NotesDoc = defineState("notes", { schema: obj({ items: arr(str()) }), version: 1, initial: () => ({ items: [] }) }); +// the document the object stores (its own version: storage can change without changing the contract) +const NotesDoc = defineState("notes", { schema: NoteList, version: 1, initial: () => ({ items: [] }) }); // the facet: which types this host serves, and how to make each object const facet = defineFacet((host) => { @@ -115,7 +117,11 @@ await notes.add({ text: "buy milk" }, ctx); console.log(notes.list.value); // { items: ["buy milk"] } ``` -Four things happened. The **contract** `Notes` says what the object offers. The **facet** says this host serves +Four things happened. The **contract** `Notes` says what the object offers; the **document** says what it stores. +Here they have the same shape, so one schema serves both; they are separate because they change separately (a +derive or a guest's redacted view offers something other than storage, and storage can migrate without users +noticing). +The **facet** says this host serves `Notes`, with a **factory** that declares the object's parts: a document, and a method writing it in a transaction. `load` publishes the facet. `get` returns a **reference** once the object is live; its members are the contract's. @@ -135,7 +141,7 @@ export const Notes = remote("notes", { version: 1, params: { project: str() }, hostAddress: ({ project }) => Project({ id: project }), // where an object of these params lives - variants: { editor: { state: { list: NotesSchema }, methods: { add: method(AddArgs) } } }, + variants: { editor: { state: { list: NoteList }, methods: { add: method(obj({ text: str() })) } } }, }); ``` diff --git a/packages/chord/docs/next/guide-plugins-hosts/index.html b/packages/chord/docs/next/guide-plugins-hosts/index.html index ec7ab3f9c..677e54ab6 100644 --- a/packages/chord/docs/next/guide-plugins-hosts/index.html +++ b/packages/chord/docs/next/guide-plugins-hosts/index.html @@ -174,18 +174,20 @@ extension point: one plugin defines it, others contribute. A rolloutStart without plugins. One host, one object that keeps notes:

import { createHost, defineFacet, defineState, local, method, obj, arr, str } from "@earendil-works/chord/next";
 
-// the contract: what a Notes object looks like from outside
+const NoteList = obj({ items: arr(str()) });          // one schema, used by the contract and the document
+
+// the contract: what a Notes object offers
 const Notes = local("notes.store", {
 	version: 1,
 	params: {},                                        // one object per host: no params
 	members: {
-		list: obj({ items: arr(str()) }),             // state anyone can read and watch
+		list: NoteList,                               // state anyone can read and watch
 		add: method(obj({ text: str() })),            // a method: arguments checked by Chord
 	},
 });
 
-// the document the object keeps
-const NotesDoc = defineState("notes", { schema: obj({ items: arr(str()) }), version: 1, initial: () => ({ items: [] }) });
+// the document the object stores (its own version: storage can change without changing the contract)
+const NotesDoc = defineState("notes", { schema: NoteList, version: 1, initial: () => ({ items: [] }) });
 
 // the facet: which types this host serves, and how to make each object
 const facet = defineFacet((host) => {
@@ -203,7 +205,11 @@ extension point: one plugin defines it, others contribute. A rolloutconst notes = await server.get(Notes, {}, ctx);        // a reference: live, with its state
 await notes.add({ text: "buy milk" }, ctx);
 console.log(notes.list.value);                          // { items: ["buy milk"] }
-

Four things happened. The contract Notes says what the object offers. The facet says this host serves +

Four things happened. The contract Notes says what the object offers; the document says what it stores. +Here they have the same shape, so one schema serves both; they are separate because they change separately (a +derive or a guest's redacted view offers something other than storage, and storage can migrate without users +noticing). +The facet says this host serves Notes, with a factory that declares the object's parts: a document, and a method writing it in a transaction. load publishes the facet. get returns a reference once the object is live; its members are the contract's.

The factory only declares: it is synchronous and opens nothing. Chord activates the object (loads its documents, opens @@ -220,7 +226,7 @@ lives on its project's worker:

version: 1, params: { project: str() }, hostAddress: ({ project }) => Project({ id: project }), // where an object of these params lives - variants: { editor: { state: { list: NotesSchema }, methods: { add: method(AddArgs) } } }, + variants: { editor: { state: { list: NoteList }, methods: { add: method(obj({ text: str() })) } } }, });

hostAddress is the whole routing story. A tab asks for Notes { project: "p1" }. Its host computes the address, Project(p1), and sends the request to its parent, the server. The server has a route for Project: it starts (or