From a34444d6bd768ecee494ad17261daaa75a6ed808 Mon Sep 17 00:00:00 2001
From: Mario Zechner ({ 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);