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.

  • Before we begin
  • I. One Host
  • II. Several Hosts
  • -
  • III. Plugins
  • -
  • IV. Loading Plugins
  • -
  • V. Plugins Using Plugins
  • -
  • VI. Changing a Plugin
  • +
  • III. Client Hosts
  • +
  • IV. Plugins
  • +
  • V. Loading Plugins
  • +
  • VI. Plugins Using Plugins
  • +
  • VII. Changing a Plugin
  • Base Code and Plugins
  • Appendix A: Reference
  • @@ -144,7 +145,8 @@ reloaded while the app runs.

    This book builds the app in that order. Each chapter adds one concept and shows complete code.

    Seven things to know

      -
    1. A host is one participant. The server, each worker and each browser tab is a host; hosts connect over links.
    2. +
    3. 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.
    4. 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.
    5. A facet declares, an object acts. A facet is a plugin's code for one host kind: it says which object types this @@ -158,6 +160,7 @@ contributes to a registry another defines. Nothing else crosses between plugins.
    6. An edit is a rollout. Change a file, build, roll out: the server, then every worker, swap the plugin's objects in place; a broken edit keeps the old code running and reports why.
    +
    ON DISKSERVER PROCESSCLIENTS AND WORKERSplugins/notes/server.ts`, `worker.ts`,`web.tsx`, `tui.ts`, `contract.tssrc/ (base code)`@app/core`: host kinds, principalsplugins componentdiscover, build every facet, watch,roll outserver hostserver facets; routes Project →workersclient: tab or TUIweb or tui facets, local UI objects;holds `Notes { p1 }`worker p1worker facets; serves `Notes { p1 }`read, buildhost packagesserver scriptsweb or tui scriptshold Notes { p1 }start (worker scripts), relayEach host loads only its own kind's facets. Plugins meet only through object types and registries.
    Figure 1. 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.

    Words used throughout

    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.

    -
    CLIENTSSERVERWORKERStab Aholds Notes { project: p1 }tab Bholds Notes { project: p1 }serverroute Project → workerworker p1serves Notes { p1 }worker p2serves Notes { p2 }holdholdrelay
    Figure 1. A tab holds a project's notes: the server routes by address to the project's worker and relays, without reading the data.
    +
    CLIENTSSERVERWORKERStab Aholds Notes { project: p1 }tab Bholds Notes { project: p1 }serverroute Project → workerworker p1serves Notes { p1 }worker p2serves Notes { p2 }holdholdrelay
    Figure 2. A tab holds a project's notes: the server routes by address to the project's worker and relays, without reading the data.

    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().<
     
     
     
    -
    III

    Plugins

    Move the code into a directory that can be added, edited and removed while the app runs.

    +
    III

    Client 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.

    +
    + +
    IV

    Plugins

    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.

    -
    IV

    Loading Plugins

    Discover, build, evaluate, load: what happens between a directory and a running object.

    +
    V

    Loading 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.

    -
    V

    Plugins Using Plugins

    Two ways one plugin uses another, and the one it may not.

    +
    VI

    Plugins 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.

    -
    VI

    Changing a Plugin

    Edit, build, roll out: how new code takes over while every tab stays connected.

    +
    VII

    Changing 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);