diff --git a/packages/chord/docs/next/guide-plugins-hosts/book.md b/packages/chord/docs/next/guide-plugins-hosts/book.md index 2ffe01d53..83fe5051a 100644 --- a/packages/chord/docs/next/guide-plugins-hosts/book.md +++ b/packages/chord/docs/next/guide-plugins-hosts/book.md @@ -33,7 +33,8 @@ This book builds the app in that order. Each chapter adds one concept and shows ## Seven things to know :::findings -1. **A host is one participant.** The server, each worker and each browser tab is a host; hosts connect over links. +1. **A host is one participant.** The server, each worker, each browser tab and each terminal UI is a host; hosts + connect over links. Tabs and terminal UIs are clients: no address, they connect to the server and ask. 2. **An object has a type, and a type is a contract.** The contract names the object's state and methods; any host can ask for an object by type and params, and Chord finds where it lives. 3. **A facet declares, an object acts.** A facet is a plugin's code for one host kind: it says which object types this @@ -48,6 +49,25 @@ This book builds the app in that order. Each chapter adds one concept and shows place; a broken edit keeps the old code running and reports why. ::: +:::flow wide animate | How it all fits: plugins on disk are built once on the server; each host evaluates the facets of its kind; tabs and the TUI are clients that reach workers' objects through the server. +row "On disk" muted +box disk "plugins/notes/" "`server.ts`, `worker.ts`, `web.tsx`, `tui.ts`, `contract.ts`" muted +box core "src/ (base code)" "`@app/core`: host kinds, principals" muted +row "Server process" accent +box comp "plugins component" "discover, build every facet, watch, roll out" accent +box server "server host" "server facets; routes Project → workers" +row "Clients and workers" +box tab "client: tab or TUI" "web or tui facets, local UI objects; holds `Notes { p1 }`" blue +box w1 "worker p1" "worker facets; serves `Notes { p1 }`" +arrow disk -> comp "read, build" +arrow core -> comp "host packages" +arrow comp -> server "server scripts" +arrow comp -> tab "web or tui scripts" +arrow tab -> server "hold Notes { p1 }" +arrow server -> w1 "start (worker scripts), relay" +note "Each host loads only its own kind's facets. Plugins meet only through object types and registries." +::: + ## Words used throughout A **host** is one participant of the app, made with `createHost`. A **host kind** (`Server`, `Session`) describes @@ -162,6 +182,49 @@ Starting workers, routing a host kind to them, the WebSocket entry and authentic this wiring for you: `startApp(...)` and `startWorker()`. ::: +# Client Hosts {#clients lede="Browser tabs and terminal UIs are hosts too: what they can do, and what they cannot."} + +A browser tab and a terminal UI are hosts like the server, with one difference: they have **no address**. Nobody can +send a request *to* a tab; a tab only connects to the server and asks for things. That is a deliberate rule: a tab +runs on a device the server does not control, so nothing on the server ever waits for, or trusts, a tab's state. + +```ts +// a browser tab (slice 11's page host will do this wiring for you) +const tab = createHost
({ onError }); // no address, no principal: a client +tab.connect(webSocketLink(new WebSocket("/chord")), { principal: { kind: "server" } }, ctx); +await tab.load([{ name: "@app/web", facet: webCore }, ...webFacets], ctx); + +const notes = tab.hold(Notes, { project: "p1" }, ctx); // a reference at once, live soon +(await notes.list.watch(ctx)).start(async (call) => { + if (call.kind === "value") render(call.value.items); // every change, as it commits on the worker +}); +await notes.add({ text: "from the tab" }, ctx); // a call: tab → server → worker +``` + +**Who the tab is** is not the tab's to say. The server authenticates the WebSocket when it connects (a cookie, the +owner on loopback, a guest's share link) and stamps that principal on everything the tab asks for. The worker's +`Notes` factory receives it and may give a guest a read-only variant. A tab cannot claim to be the owner. + +**What a tab serves** is only `local` types: state of this tab's UI, never shared. A draft that survives switching +panels, a selection, a tab-local cache: + +```ts +// plugins/notes/web.tsx: the web facet serves a local type, and contributes a panel +export default defineFacet((host, self) => { + host.serve(NoteDraft, (_params, self) => ({ text: self.state(DraftDoc) })); // lives in this tab only + self.contribute(Panels, { title: "Notes", Component: NotesPanel }); +}); +``` + +**When the connection drops** (a phone in a tunnel, a server restart), the tab keeps its references. Their values +stay readable, their `state` says `loading`, calls made meanwhile wait; on reconnect every reference is held again and +gets the current value in one frame. The UI shows a banner from `ref.state`, nothing more. + +**A terminal UI** is the same in Node: a client host connected to the server over a WebSocket, loading the plugins' +`tui` facets, rendering with whatever terminal library the app chose. **What a client never does**: serve a `remote` +type, be routed to, or keep data others rely on. Data that must outlive a tab belongs in an object on the server or a +worker. + # Plugins {#plugins lede="Move the code into a directory that can be added, edited and removed while the app runs."} So far the facets were part of the app's own code. A **plugin** is the same code in its own directory: diff --git a/packages/chord/docs/next/guide-plugins-hosts/index.html b/packages/chord/docs/next/guide-plugins-hosts/index.html index b026d7e8e..ec7ab3f9c 100644 --- a/packages/chord/docs/next/guide-plugins-hosts/index.html +++ b/packages/chord/docs/next/guide-plugins-hosts/index.html @@ -123,10 +123,11 @@ writes, a note says so.
This book builds the app in that order. Each chapter adds one concept and shows complete code.
A host is one participant of the app, made with createHost. A host kind (Server, Session) describes
addressable hosts; an address (Session({ id: "p1" })) names one. An object type is a contract: local (used
@@ -223,7 +226,7 @@ lives on its project's worker:
Project(p1), and sends the request to its parent, the server. The server has a route for Project: it starts (or
finds) the worker for p1 and passes the request on. The worker serves Notes. The tab gets a reference whose state
follows the worker's document, one frame per change.
-Each host is created the same way, with what it is:
// server.ts
const server = createHost<P>({ address: Server(), principal: { kind: "server" }, onError });
@@ -243,7 +246,40 @@ this wiring for you: startApp(...) and startWorker().<
-IIIPlugins
Move the code into a directory that can be added, edited and removed while the app runs.
+IIIClient Hosts
Browser tabs and terminal UIs are hosts too: what they can do, and what they cannot.
+A browser tab and a terminal UI are hosts like the server, with one difference: they have no address. Nobody can
+send a request to a tab; a tab only connects to the server and asks for things. That is a deliberate rule: a tab
+runs on a device the server does not control, so nothing on the server ever waits for, or trusts, a tab's state.
+// a browser tab (slice 11's page host will do this wiring for you)
+const tab = createHost<P>({ onError }); // no address, no principal: a client
+tab.connect(webSocketLink(new WebSocket("/chord")), { principal: { kind: "server" } }, ctx);
+await tab.load([{ name: "@app/web", facet: webCore }, ...webFacets], ctx);
+
+const notes = tab.hold(Notes, { project: "p1" }, ctx); // a reference at once, live soon
+(await notes.list.watch(ctx)).start(async (call) => {
+ if (call.kind === "value") render(call.value.items); // every change, as it commits on the worker
+});
+await notes.add({ text: "from the tab" }, ctx); // a call: tab → server → worker
+Who the tab is is not the tab's to say. The server authenticates the WebSocket when it connects (a cookie, the
+owner on loopback, a guest's share link) and stamps that principal on everything the tab asks for. The worker's
+Notes factory receives it and may give a guest a read-only variant. A tab cannot claim to be the owner.
+What a tab serves is only local types: state of this tab's UI, never shared. A draft that survives switching
+panels, a selection, a tab-local cache:
+// plugins/notes/web.tsx: the web facet serves a local type, and contributes a panel
+export default defineFacet((host, self) => {
+ host.serve(NoteDraft, (_params, self) => ({ text: self.state(DraftDoc) })); // lives in this tab only
+ self.contribute(Panels, { title: "Notes", Component: NotesPanel });
+});
+When the connection drops (a phone in a tunnel, a server restart), the tab keeps its references. Their values
+stay readable, their state says loading, calls made meanwhile wait; on reconnect every reference is held again and
+gets the current value in one frame. The UI shows a banner from ref.state, nothing more.
+A terminal UI is the same in Node: a client host connected to the server over a WebSocket, loading the plugins'
+tui facets, rendering with whatever terminal library the app chose. What a client never does: serve a remote
+type, be routed to, or keep data others rely on. Data that must outlive a tab belongs in an object on the server or a
+worker.
+
+
+IVPlugins
Move the code into a directory that can be added, edited and removed while the app runs.
So far the facets were part of the app's own code. A plugin is the same code in its own directory:
plugins/notes/
package.json { "name": "@app/notes" }
@@ -269,7 +305,7 @@ out; every host passes its own copy in). It holds what plugins share and never r
type, a defineFacet bound to it.
-IVLoading Plugins
Discover, build, evaluate, load: what happens between a directory and a running object.
+VLoading Plugins
Discover, build, evaluate, load: what happens between a directory and a running object.
The plugins component does the work. You tell it where plugins are and what each host kind needs:
const plugins = createPlugins({
roots: ["./plugins"],
@@ -300,7 +336,7 @@ fails is left out and listed in report.failed; the others load. Its
A worker never builds: the server builds the worker facets too, and each worker evaluates the scripts it is sent.
-VPlugins Using Plugins
Two ways one plugin uses another, and the one it may not.
+VIPlugins Using Plugins
Two ways one plugin uses another, and the one it may not.
A search plugin wants to search the notes. It imports the contract and asks for the object:
// plugins/search/worker.ts
import { Notes } from "../notes/contract.ts"; // copied into search's script: contracts only
@@ -320,7 +356,7 @@ defines Panels, and every plugin with a UI contributes one:
object's state. That rule is what lets the notes plugin change its storage without breaking search.
-VIChanging a Plugin
Edit, build, roll out: how new code takes over while every tab stays connected.
+VIIChanging a Plugin
Edit, build, roll out: how new code takes over while every tab stays connected.
You change the notes plugin: notes now have a timestamp. The watcher sees the saved file and marks the plugin dirty.
You (or an agent) build and roll out:
const built = await plugins.build(plugins.state.value.dirty, ctx);