mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
docs(chord): tutorial: an object's parts before they are used
This commit is contained in:
parent
436d99e04b
commit
dae7bd8fcb
2 changed files with 118 additions and 22 deletions
|
|
@ -74,7 +74,9 @@ A **host** is one participant of the app, made with `createHost`. A **host kind*
|
|||
addressable hosts; an **address** (`Session({ id: "p1" })`) names one. An **object type** is a contract: `local` (used
|
||||
on its own host only) or `remote` (used from any host). An **object** is one instance of a type, per params. A
|
||||
**facet** is a plugin's declarations for one host kind. A **plugin** is a directory with a `package.json`. A
|
||||
**reference** is what you get when you ask for an object (`host.get`, `self.get`, `ctx.get`). A **registry** is an
|
||||
**reference** is what you get when you ask for an object (`host.get`, `self.get`, `ctx.get`). A **document** is data an object
|
||||
stores, changed in **transactions**; a **derive** is a value computed from others; a **native** is an outside
|
||||
resource an object opens and closes. A **registry** is an
|
||||
extension point: one plugin defines it, others contribute. A **rollout** applies a build to the hosts in order.
|
||||
|
||||
# One Host {#host lede="The smallest app: one process, one object type, one call."}
|
||||
|
|
@ -118,9 +120,9 @@ console.log(notes.list.value); // { items: ["buy milk"]
|
|||
```
|
||||
|
||||
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).
|
||||
Here they have the same shape, so one schema serves both; they are separate because they change separately (an
|
||||
object may offer a computed value or a shortened view instead of what it stores, and storage can change its shape
|
||||
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.
|
||||
|
|
@ -128,6 +130,57 @@ The **facet** says this host serves
|
|||
The factory only declares: it is synchronous and opens nothing. Chord activates the object (loads its documents, opens
|
||||
what it asked to open) and then serves calls. That is why the same factory can run again when the code changes.
|
||||
|
||||
# An Object's Parts {#parts lede="Documents, transactions, computed values, watching, and outside resources: the vocabulary of every factory."}
|
||||
|
||||
A factory builds an object from a handful of parts, each declared with one call on `self`. The notes object grows to
|
||||
show all of them.
|
||||
|
||||
**Documents** hold the object's data: `self.state(NotesDoc)` gives a handle whose `.value` is the current data. Data
|
||||
changes only in a **transaction**: `self.tx(fn, ctx)` runs `fn` with `tx`, and `await tx.draft(handle)` gives a
|
||||
mutable copy of the document's data; whatever you change on it is committed at once when `fn` returns, or not at all
|
||||
if it throws.
|
||||
|
||||
```ts
|
||||
add: async ({ text }, ctx) => self.tx(async (tx) => {
|
||||
const draft = await tx.draft(doc); // the document's data, writable inside this transaction only
|
||||
draft.items.push(text); // recorded as a change, committed when the function returns
|
||||
}, ctx),
|
||||
```
|
||||
|
||||
A **derive** is a value computed from other values, recomputed whenever they change. It is how an object offers
|
||||
something it does not store:
|
||||
|
||||
```ts
|
||||
const count = self.derive([doc], ([notes]) => notes.items.length); // 0, 1, 2, … as notes are added
|
||||
return { list: doc, count, add }; // `count` can be a member like `list`
|
||||
```
|
||||
|
||||
**Watching** is how anyone follows a value as it changes: a document handle, a derive, a reference's member. `watch`
|
||||
gives the current value, then one call per change:
|
||||
|
||||
```ts
|
||||
const watch = await notes.count.watch(ctx);
|
||||
watch.start(async (call) => {
|
||||
if (call.kind === "value") console.log("notes:", call.value); // "failed" calls say a value cannot be computed now
|
||||
});
|
||||
```
|
||||
|
||||
A **routine** is the object watching on its own behalf: `self.on(source, fn)` runs `fn` with each new value, for as
|
||||
long as the object lives (a reconciler: "whenever the notes change, rebuild the export file").
|
||||
|
||||
A **native** is something outside Chord the object opens and must close: a file watcher, a database connection, a
|
||||
child process. `self.open(open, close)` opens it while the object activates and closes it when the object ends:
|
||||
|
||||
```ts
|
||||
const inbox = self.open(async (ctx) => watchDirectory(inboxPath, ctx.abortSignal), (watcher) => watcher.close());
|
||||
self.onLive(async (ctx) => { // a routine started once the object is live
|
||||
for await (const file of inbox.value.files(ctx.abortSignal)) await importNote(file, ctx);
|
||||
});
|
||||
```
|
||||
|
||||
Natives are why some objects restart when their plugin changes: an open connection cannot be moved to new code, so
|
||||
Chord closes it and opens it again. Objects without natives are swapped in place.
|
||||
|
||||
# Several Hosts {#hosts lede="A server, a worker per project, browser tabs: how they find each other's objects."}
|
||||
|
||||
Now give each project its own process. Declare the kinds of addressable host, and make notes a `remote` type that
|
||||
|
|
@ -145,7 +198,11 @@ export const Notes = remote("notes", {
|
|||
});
|
||||
```
|
||||
|
||||
`hostAddress` is the whole routing story. A tab asks for `Notes { project: "p1" }`. Its host computes the address,
|
||||
Two things are new. A `remote` type can be used from any host, and it has **variants**: different sets of members
|
||||
for different callers, chosen by the factory (an `editor` variant with `add`, a `reader` variant for guests without
|
||||
it). And `hostAddress` says where its objects live; it 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
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -122,12 +122,13 @@ writes, a note says so.</p>
|
|||
<nav class="toc"><h2>Contents</h2><ol>
|
||||
<li><a href="#before-we-begin">Before we begin</a></li>
|
||||
<li><a href="#host">I. One Host</a></li>
|
||||
<li><a href="#hosts">II. Several Hosts</a></li>
|
||||
<li><a href="#clients">III. Client Hosts</a></li>
|
||||
<li><a href="#plugins">IV. Plugins</a></li>
|
||||
<li><a href="#loading">V. Loading Plugins</a></li>
|
||||
<li><a href="#together">VI. Plugins Using Plugins</a></li>
|
||||
<li><a href="#reload">VII. Changing a Plugin</a></li>
|
||||
<li><a href="#parts">II. An Object's Parts</a></li>
|
||||
<li><a href="#hosts">III. Several Hosts</a></li>
|
||||
<li><a href="#clients">IV. Client Hosts</a></li>
|
||||
<li><a href="#plugins">V. Plugins</a></li>
|
||||
<li><a href="#loading">VI. Loading Plugins</a></li>
|
||||
<li><a href="#together">VII. Plugins Using Plugins</a></li>
|
||||
<li><a href="#reload">VIII. Changing a Plugin</a></li>
|
||||
<li><a href="#base-code-and-plugins">Base Code and Plugins</a></li>
|
||||
<li><a href="#reference">Appendix A: Reference</a></li>
|
||||
</ol></nav>
|
||||
|
|
@ -166,7 +167,9 @@ place; a broken edit keeps the old code running and reports why.</li>
|
|||
addressable hosts; an <strong>address</strong> (<code>Session({ id: "p1" })</code>) names one. An <strong>object type</strong> is a contract: <code>local</code> (used
|
||||
on its own host only) or <code>remote</code> (used from any host). An <strong>object</strong> is one instance of a type, per params. A
|
||||
<strong>facet</strong> is a plugin's declarations for one host kind. A <strong>plugin</strong> is a directory with a <code>package.json</code>. A
|
||||
<strong>reference</strong> is what you get when you ask for an object (<code>host.get</code>, <code>self.get</code>, <code>ctx.get</code>). A <strong>registry</strong> is an
|
||||
<strong>reference</strong> is what you get when you ask for an object (<code>host.get</code>, <code>self.get</code>, <code>ctx.get</code>). A <strong>document</strong> is data an object
|
||||
stores, changed in <strong>transactions</strong>; a <strong>derive</strong> is a value computed from others; a <strong>native</strong> is an outside
|
||||
resource an object opens and closes. A <strong>registry</strong> is an
|
||||
extension point: one plugin defines it, others contribute. A <strong>rollout</strong> applies a build to the hosts in order.</p>
|
||||
</section>
|
||||
|
||||
|
|
@ -206,9 +209,9 @@ extension point: one plugin defines it, others contribute. A <strong>rollout</st
|
|||
<span class="tk-k">await</span> notes.<span class="tk-f">add</span>({ text: <span class="tk-s">"buy milk"</span> }, ctx);
|
||||
console.<span class="tk-f">log</span>(notes.list.value); <span class="tk-c">// { items: ["buy milk"] }</span></code></pre>
|
||||
<p>Four things happened. The <strong>contract</strong> <code>Notes</code> says what the object offers; the <strong>document</strong> 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).
|
||||
Here they have the same shape, so one schema serves both; they are separate because they change separately (an
|
||||
object may offer a computed value or a shortened view instead of what it stores, and storage can change its shape
|
||||
without users noticing).
|
||||
The <strong>facet</strong> says this host serves
|
||||
<code>Notes</code>, with a <strong>factory</strong> that declares the object's parts: a document, and a method writing it in a transaction.
|
||||
<code>load</code> publishes the facet. <code>get</code> returns a <strong>reference</strong> once the object is live; its members are the contract's.</p>
|
||||
|
|
@ -216,7 +219,40 @@ The <strong>facet</strong> says this host serves
|
|||
what it asked to open) and then serves calls. That is why the same factory can run again when the code changes.</p>
|
||||
</section>
|
||||
|
||||
<section class="chapter" id="hosts"><header class="opener"><div class="numeral">II</div><h2>Several Hosts</h2><p class="lede">A server, a worker per project, browser tabs: how they find each other's objects.</p></header>
|
||||
<section class="chapter" id="parts"><header class="opener"><div class="numeral">II</div><h2>An Object's Parts</h2><p class="lede">Documents, transactions, computed values, watching, and outside resources: the vocabulary of every factory.</p></header>
|
||||
<p class="dropcap">A factory builds an object from a handful of parts, each declared with one call on <code>self</code>. The notes object grows to
|
||||
show all of them.</p>
|
||||
<p><strong>Documents</strong> hold the object's data: <code>self.state(NotesDoc)</code> gives a handle whose <code>.value</code> is the current data. Data
|
||||
changes only in a <strong>transaction</strong>: <code>self.tx(fn, ctx)</code> runs <code>fn</code> with <code>tx</code>, and <code>await tx.draft(handle)</code> gives a
|
||||
mutable copy of the document's data; whatever you change on it is committed at once when <code>fn</code> returns, or not at all
|
||||
if it throws.</p>
|
||||
<pre><code>add: <span class="tk-k">async</span> ({ text }, ctx) => self.<span class="tk-f">tx</span>(<span class="tk-k">async</span> (tx) => {
|
||||
<span class="tk-k">const</span> draft = <span class="tk-k">await</span> tx.<span class="tk-f">draft</span>(doc); <span class="tk-c">// the document's data, writable inside this transaction only</span>
|
||||
draft.items.<span class="tk-f">push</span>(text); <span class="tk-c">// recorded as a change, committed when the function returns</span>
|
||||
}, ctx),</code></pre>
|
||||
<p>A <strong>derive</strong> is a value computed from other values, recomputed whenever they change. It is how an object offers
|
||||
something it does not store:</p>
|
||||
<pre><code><span class="tk-k">const</span> count = self.<span class="tk-f">derive</span>([doc], ([notes]) => notes.items.length); <span class="tk-c">// 0, 1, 2, … as notes are added</span>
|
||||
<span class="tk-k">return</span> { list: doc, count, add }; <span class="tk-c">// `count` can be a member like `list`</span></code></pre>
|
||||
<p><strong>Watching</strong> is how anyone follows a value as it changes: a document handle, a derive, a reference's member. <code>watch</code>
|
||||
gives the current value, then one call per change:</p>
|
||||
<pre><code><span class="tk-k">const</span> watch = <span class="tk-k">await</span> notes.count.<span class="tk-f">watch</span>(ctx);
|
||||
watch.<span class="tk-f">start</span>(<span class="tk-k">async</span> (call) => {
|
||||
<span class="tk-k">if</span> (call.kind === <span class="tk-s">"value"</span>) console.<span class="tk-f">log</span>(<span class="tk-s">"notes:"</span>, call.value); <span class="tk-c">// "failed" calls say a value cannot be computed now</span>
|
||||
});</code></pre>
|
||||
<p>A <strong>routine</strong> is the object watching on its own behalf: <code>self.on(source, fn)</code> runs <code>fn</code> with each new value, for as
|
||||
long as the object lives (a reconciler: "whenever the notes change, rebuild the export file").</p>
|
||||
<p>A <strong>native</strong> is something outside Chord the object opens and must close: a file watcher, a database connection, a
|
||||
child process. <code>self.open(open, close)</code> opens it while the object activates and closes it when the object ends:</p>
|
||||
<pre><code><span class="tk-k">const</span> inbox = self.<span class="tk-f">open</span>(<span class="tk-k">async</span> (ctx) => <span class="tk-f">watchDirectory</span>(inboxPath, ctx.abortSignal), (watcher) => watcher.<span class="tk-f">close</span>());
|
||||
self.<span class="tk-f">onLive</span>(<span class="tk-k">async</span> (ctx) => { <span class="tk-c">// a routine started once the object is live</span>
|
||||
<span class="tk-k">for</span> <span class="tk-k">await</span> (<span class="tk-k">const</span> file <span class="tk-k">of</span> inbox.value.<span class="tk-f">files</span>(ctx.abortSignal)) <span class="tk-k">await</span> <span class="tk-f">importNote</span>(file, ctx);
|
||||
});</code></pre>
|
||||
<p>Natives are why some objects restart when their plugin changes: an open connection cannot be moved to new code, so
|
||||
Chord closes it and opens it again. Objects without natives are swapped in place.</p>
|
||||
</section>
|
||||
|
||||
<section class="chapter" id="hosts"><header class="opener"><div class="numeral">III</div><h2>Several Hosts</h2><p class="lede">A server, a worker per project, browser tabs: how they find each other's objects.</p></header>
|
||||
<p class="dropcap">Now give each project its own process. Declare the kinds of addressable host, and make notes a <code>remote</code> type that
|
||||
lives on its project's worker:</p>
|
||||
<pre><code><span class="tk-k">export</span> <span class="tk-k">const</span> <span class="tk-t">Server</span> = <span class="tk-f">hostKind</span>(<span class="tk-s">"server"</span>);
|
||||
|
|
@ -228,7 +264,10 @@ lives on its project's worker:</p>
|
|||
hostAddress: ({ project }) => <span class="tk-t">Project</span>({ id: project }), <span class="tk-c">// where an object of these params lives</span>
|
||||
variants: { editor: { state: { list: <span class="tk-t">NoteList</span> }, methods: { add: <span class="tk-f">method</span>(<span class="tk-f">obj</span>({ text: <span class="tk-f">str</span>() })) } } },
|
||||
});</code></pre>
|
||||
<p><code>hostAddress</code> is the whole routing story. A tab asks for <code>Notes { project: "p1" }</code>. Its host computes the address,
|
||||
<p>Two things are new. A <code>remote</code> type can be used from any host, and it has <strong>variants</strong>: different sets of members
|
||||
for different callers, chosen by the factory (an <code>editor</code> variant with <code>add</code>, a <code>reader</code> variant for guests without
|
||||
it). And <code>hostAddress</code> says where its objects live; it is the whole routing story.</p>
|
||||
<p>A tab asks for <code>Notes { project: "p1" }</code>. Its host computes the address,
|
||||
<code>Project(p1)</code>, and sends the request to its parent, the server. The server has a <strong>route</strong> for <code>Project</code>: it starts (or
|
||||
finds) the worker for <code>p1</code> and passes the request on. The worker serves <code>Notes</code>. The tab gets a reference whose state
|
||||
follows the worker's document, one frame per change.</p>
|
||||
|
|
@ -252,7 +291,7 @@ this wiring for you: <code>startApp(...)</code> and <code>startWorker()</code>.<
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="chapter" id="clients"><header class="opener"><div class="numeral">III</div><h2>Client Hosts</h2><p class="lede">Browser tabs and terminal UIs are hosts too: what they can do, and what they cannot.</p></header>
|
||||
<section class="chapter" id="clients"><header class="opener"><div class="numeral">IV</div><h2>Client Hosts</h2><p class="lede">Browser tabs and terminal UIs are hosts too: what they can do, and what they cannot.</p></header>
|
||||
<p class="dropcap">A browser tab and a terminal UI are hosts like the server, with one difference: they have <strong>no address</strong>. Nobody can
|
||||
send a request <em>to</em> 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.</p>
|
||||
|
|
@ -285,7 +324,7 @@ type, be routed to, or keep data others rely on. Data that must outlive a tab be
|
|||
worker.</p>
|
||||
</section>
|
||||
|
||||
<section class="chapter" id="plugins"><header class="opener"><div class="numeral">IV</div><h2>Plugins</h2><p class="lede">Move the code into a directory that can be added, edited and removed while the app runs.</p></header>
|
||||
<section class="chapter" id="plugins"><header class="opener"><div class="numeral">V</div><h2>Plugins</h2><p class="lede">Move the code into a directory that can be added, edited and removed while the app runs.</p></header>
|
||||
<p class="dropcap">So far the facets were part of the app's own code. A <strong>plugin</strong> is the same code in its own directory:</p>
|
||||
<pre class="plain"><code>plugins/notes/
|
||||
package.json { "name": "@app/notes" }
|
||||
|
|
@ -311,7 +350,7 @@ out; every host passes its own copy in). It holds what plugins share and never r
|
|||
type, a <code>defineFacet</code> bound to it.</p>
|
||||
</section>
|
||||
|
||||
<section class="chapter" id="loading"><header class="opener"><div class="numeral">V</div><h2>Loading Plugins</h2><p class="lede">Discover, build, evaluate, load: what happens between a directory and a running object.</p></header>
|
||||
<section class="chapter" id="loading"><header class="opener"><div class="numeral">VI</div><h2>Loading Plugins</h2><p class="lede">Discover, build, evaluate, load: what happens between a directory and a running object.</p></header>
|
||||
<p class="dropcap">The <strong>plugins component</strong> does the work. You tell it where plugins are and what each host kind needs:</p>
|
||||
<pre><code><span class="tk-k">const</span> plugins = <span class="tk-f">createPlugins</span>({
|
||||
roots: [<span class="tk-s">"./plugins"</span>],
|
||||
|
|
@ -342,7 +381,7 @@ fails is left out and listed in <code>report.failed</code>; the others load. Its
|
|||
<p>A worker never builds: the server builds the worker facets too, and each worker evaluates the scripts it is sent.</p>
|
||||
</section>
|
||||
|
||||
<section class="chapter" id="together"><header class="opener"><div class="numeral">VI</div><h2>Plugins Using Plugins</h2><p class="lede">Two ways one plugin uses another, and the one it may not.</p></header>
|
||||
<section class="chapter" id="together"><header class="opener"><div class="numeral">VII</div><h2>Plugins Using Plugins</h2><p class="lede">Two ways one plugin uses another, and the one it may not.</p></header>
|
||||
<p class="dropcap">A search plugin wants to search the notes. It <strong>imports the contract</strong> and asks for the object:</p>
|
||||
<pre><code><span class="tk-c">// plugins/search/worker.ts</span>
|
||||
<span class="tk-k">import</span> { <span class="tk-t">Notes</span> } <span class="tk-k">from</span> <span class="tk-s">"../notes/contract.ts"</span>; <span class="tk-c">// copied into search's script: contracts only</span>
|
||||
|
|
@ -362,7 +401,7 @@ defines <code>Panels</code>, and every plugin with a UI contributes one:</p>
|
|||
object's state. That rule is what lets the notes plugin change its storage without breaking search.</p>
|
||||
</section>
|
||||
|
||||
<section class="chapter" id="reload"><header class="opener"><div class="numeral">VII</div><h2>Changing a Plugin</h2><p class="lede">Edit, build, roll out: how new code takes over while every tab stays connected.</p></header>
|
||||
<section class="chapter" id="reload"><header class="opener"><div class="numeral">VIII</div><h2>Changing a Plugin</h2><p class="lede">Edit, build, roll out: how new code takes over while every tab stays connected.</p></header>
|
||||
<p class="dropcap">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:</p>
|
||||
<pre><code><span class="tk-k">const</span> built = <span class="tk-k">await</span> plugins.<span class="tk-f">build</span>(plugins.state.value.dirty, ctx);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue