mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(chord): live queries over an object's own tables (self.query)
self.query(table, q, limit) runs before the object goes live and again in the commit step of every commit that touched the table, publishing only the diff; a reload swaps the object. The tutorial and 59-chat use it.
This commit is contained in:
parent
828bcadcba
commit
2439933cf2
25 changed files with 1113 additions and 167 deletions
|
|
@ -972,3 +972,25 @@ Fix round 1: the bench itself now builds with the app's API and the kinds (its p
|
|||
L5), and every package file's real path is checked against the plugin's `node_modules`. CPU 8, three runs: cold
|
||||
build 261.1-270.2 ms, rebuild 13.6-14.4 ms (the first two runs as before, the third with a slower cold build on a busy
|
||||
machine).
|
||||
|
||||
## Live queries (2026-11-03)
|
||||
|
||||
`test/bench/live-query.ts`, Node 24.4.1, CPU 10, a SQLite `:memory:` store, medians and p95 of seven bursts of 2,000
|
||||
commits, three runs (equal to 0.5 µs). Each commit appends one row of a chat table (an int key, a short author, an
|
||||
80-character text, a number) through a method call; the live query is the newest 100 rows, so every commit shifts the
|
||||
window by one row.
|
||||
|
||||
| One-row commit through a call | Median | p95 |
|
||||
| --- | --- | --- |
|
||||
| no live query | 8.5-8.7 µs | 10.3-10.6 µs |
|
||||
| a live query of 100 rows, unwatched | 66.9-67.3 µs | 71.5-72.0 µs |
|
||||
| a live query of 100 rows, watched (one insert and one removal published) | 86.3-86.5 µs | 94.4-95.1 µs |
|
||||
| a commit of another table beside the watched query | 7.4-7.5 µs | 7.9-8.0 µs |
|
||||
|
||||
A touching commit costs about 58 µs more for the run (the `SELECT` of 100 rows in the step, most of it node:sqlite
|
||||
building the row objects; rows whose stored columns did not change are not decoded again but reused) and about 19 µs
|
||||
more for the diff when watched. A commit of another table costs nothing extra (one map lookup per written table).
|
||||
The diff depends on the reuse: with every row decoded anew, `diffRevisions` compared 100 fresh rows against the
|
||||
previous 100 by content and the watched commit took 885 µs; with unchanged rows the same objects it compares them by
|
||||
identity. "There may be dragons": at 100 rows a table written more than a few thousand times a second spends most of
|
||||
its time in the query.
|
||||
|
|
|
|||
|
|
@ -2476,17 +2476,19 @@ validation and copies the v2 kernel added around Delta.
|
|||
or SWC transform in every web build, fragile when hooks change). Open: a live view of table rows (the tutorial's
|
||||
and 59-chat's `self.open` read at activation, which makes every reload a restart): to explain first.
|
||||
- **Live queries over an object's own tables (owner, 2026-11-03: "if self.query is easy enough, and we document it as
|
||||
'there may be dragons' then i guess it's ok"; resolves the tutorial's gap 2):** `self.query(table, q)` in a factory
|
||||
returns a `Watchable` of the query's rows, usable as a state member and a derive source. Chord runs the query before
|
||||
the object goes live (like loading its documents: no `self.open`, so a reload swaps the object), and again in the
|
||||
publication step of every commit of the object that touched that table, diffing the new rows against the previous
|
||||
ones (ops to watchers and holds, as for any member). Only the object's own tables (only it writes them, so its
|
||||
commits are the exact invalidation); the query is fixed for the attempt (params decide it); it must have a `limit`.
|
||||
Cost: one query per touching commit, bounded by the limit; documented with a warning ("there may be dragons": not
|
||||
for tables written hundreds of times a second). Incremental maintenance of the result (an ordered, limited query
|
||||
with row-level changes kept in memory, re-running only when a removal empties the window or a bulk write hides
|
||||
which rows changed) is carry-over for when a case needs it. Rejected: a window document written beside the table
|
||||
(rows stored twice; app code mirroring every update and delete).
|
||||
'there may be dragons' then i guess it's ok"; resolves the tutorial's gap 2):** `self.query(table, q, limit)` in a
|
||||
factory returns a `Watchable` of the query's rows, usable as a state member and a derive source. Chord runs the
|
||||
query before the object goes live (like loading its documents: no `self.open`, so a reload swaps the object), and
|
||||
again in the publication step of every commit of the object that touched that table, diffing the new rows against
|
||||
the previous ones (ops to watchers and holds, as for any member). Only the object's own tables (only it writes them,
|
||||
so its commits are the exact invalidation); the query is fixed for the attempt (params decide it); a limit is
|
||||
required (the third argument, as `page` takes it: the query language keeps the limit an argument of the runner;
|
||||
refused `invalid/args` at declaration; build, 2026-11-03). Cost: one query per touching commit, bounded by the
|
||||
limit; documented with a warning ("there may be dragons": not for tables written hundreds of times a second).
|
||||
Incremental maintenance of the result (an ordered, limited query with row-level changes kept in memory, re-running
|
||||
only when a removal empties the window or a bulk write hides which rows changed) is carry-over for when a case
|
||||
needs it. Rejected: a window document written beside the table (rows stored twice; app code mirroring every update
|
||||
and delete).
|
||||
- **A plugin's npm dependencies are bundled; external only by opt-in (owner, 2026-10-29: "explicit opt in + (a)"):**
|
||||
a plugin's `dependencies` are installed into its own `node_modules` (`npm install --ignore-scripts`) and bundled into
|
||||
each facet that imports them (host packages, its `peerDependencies`, stay external and are passed in). On Node a
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ versions and adapters · 28 building: slices · 29 telemetry · 30 cost model ·
|
|||
| reference | what `get` returns; keeps its object alive; live and hydrated when awaited |
|
||||
| document | a named JSON value an object owns, written in transactions |
|
||||
| derive | a value computed from watchables |
|
||||
| live query | a bounded query of an object's own table, kept current by its commits (`self.query`, section 11) |
|
||||
| native | an OS-level value opened with `self.open`; only in objects |
|
||||
| routine | standing behaviour of an object: `self.on(source, fn)`, `self.onLive(fn)` |
|
||||
| job | owned work a call starts that outlives it: intent, run, finish; optional state document per run |
|
||||
|
|
@ -1195,6 +1196,13 @@ host.serve(DraftStore, (_, self) => { // local, empty params: may
|
|||
}, ctx);
|
||||
await edits.get({ id }, ctx); // outside: committed state, ctx last
|
||||
```
|
||||
- **Live queries in the commit step (owner, 2026-11-03; section 11):** a transaction that writes a table with live
|
||||
queries (`self.query`) stages one more step write after its own writes: it reads each such query on the step's
|
||||
backend transaction (so it sees the commit's rows, one `SELECT` counted in `chord.store.statements` and
|
||||
`chord.query.runs`), writes nothing and records nothing in the outbox; the rows are adopted with the documents in the
|
||||
commit's batch. A refused step adopts nothing. At activation the queries run right after the tables are prepared,
|
||||
before the references are bound. A transaction writing no table with a live query stages nothing (one map lookup per
|
||||
written table).
|
||||
- **The outbox (owner, 2026-10-26):** optional, `openStore(backend, ctx, { outbox: true })` (an omitted option keeps the
|
||||
persisted setting; switching needs an explicit `true` or `false`), persisted in the store's
|
||||
metadata (turning it on starts sync from an archive at the current `lastSeq`); a table in the store's backend,
|
||||
|
|
@ -1251,11 +1259,28 @@ host.serve(DraftStore, (_, self) => { // local, empty params: may
|
|||
| Durable's views | the conversation view, task graph and Durable documents | `Watchable`s through Durable's watches; feed derives and state members |
|
||||
| paged method | large or queryable collections | a method returning pages (chat history) |
|
||||
| table | a plugin's own records with queries (owner, 2026-10-21) | `self.table`, the store's query DSL |
|
||||
| live query | a bounded window of the object's own table, live (owner, 2026-11-03) | `self.query(table, q, limit)` |
|
||||
| plugin database | derived data only: full-text search, caches (rebuilt, never synced) | a native opened by its owner |
|
||||
|
||||
App state lives in Chord documents; only transcript-bound data (entries, documents that fork and rewind with a
|
||||
conversation) lives in Durable.
|
||||
|
||||
**Live queries** (decision "Live queries over an object's own tables", owner, 2026-11-03): `self.query(table, q,
|
||||
limit)` in the factory of the table's owner returns a `Watchable` of `q`'s rows (at most `limit`, in its order): a
|
||||
state member, a derive source, an `on` source. Chord runs it before the object goes live (with its documents and
|
||||
tables, section 10) and again in the commit step of every commit of the object whose staged writes touch that table;
|
||||
the rows are adopted in the commit's batch and published as `diffRevisions` ops (W1), unchanged rows kept the same
|
||||
objects so the diff compares them by identity. Rules: only the object's own tables (only it writes them, so its
|
||||
commits are the exact invalidation: a commit not writing the table runs nothing); the query is fixed for the attempt
|
||||
(params decide it); **a limit is required** (`invalid/args` at declaration; the store's query language keeps the limit
|
||||
an argument of the runner, as `page` has it). A run that fails (a row the code cannot read) is a failed member, as a
|
||||
throwing derive (one failure call, `query_failed` once per episode, `[["r", rows]]` at the next good run); the object
|
||||
stays live. It is no native, so an object with live queries swaps on reload (section 21). Cost: one query per
|
||||
touching commit, bounded by the limit (100 rows: about 60 µs unwatched, 80 µs watched, `benchmarks.md`); **there may
|
||||
be dragons** for tables written hundreds of times a second. Incremental maintenance of the result (row-level changes
|
||||
kept in memory, a re-run only when a removal empties the window or a bulk write hides which rows changed) is left for
|
||||
when a case needs it.
|
||||
|
||||
### Watchables (owner, 2026-10-21; log "Watchable and one watch API", W1-W4)
|
||||
|
||||
There is no `replicatedState` in v2. Everything observable is one of two kinds, sharing one lifecycle:
|
||||
|
|
@ -1921,7 +1946,7 @@ type ReloadReport = {
|
|||
| --- | --- |
|
||||
| its type is removed | **ended**: as an end (natives close `end`, callers of running calls rejected `released`, handlers drained, in-memory documents gone, persisted ones kept); its holders' requests are not ended: they wait unbound `not_served` (§7) |
|
||||
| failed, parked, activating or waiting for its backoff | **retried** on the new code (`ref.retry()`'s rule, whatever broke it); an activating attempt is sealed and its natives close `reload` |
|
||||
| live, neither its live attempt nor the new factory opens natives, and no table of the new factory is newer than the store's | **swapped** in place (below) |
|
||||
| live, neither its live attempt nor the new factory opens natives, and no table of the new factory is newer than the store's | **swapped** in place (below); live queries are no natives (section 11): an object showing table rows through `self.query` swaps, one reading them in `self.open` restarts |
|
||||
| live, otherwise | **restarted**: new calls park (it shows `loading`), its contributions leave, running calls drain (bounded by `closeTimeout`; a call outliving the drain is cut off by the seal fence), the attempt is sealed (in-memory job runs end `aborted { reload }`, persisted ones are stopped and resumed, R3), natives close `reload`, the new attempt activates after that teardown (documents stay; eager table migrations run as at activation) |
|
||||
| any of the above whose new code waits for a dependency | **parked** (decision 4): no overlap of old and new code; reported with the cause |
|
||||
|
||||
|
|
@ -1942,7 +1967,8 @@ type ReloadReport = {
|
|||
variant lacks fails `invalid/member`; §7); exposed derives are matched by their member names, internal ones are
|
||||
rebuilt; in-memory documents migrate through `migrate` when the new code declares a higher version; the
|
||||
contributions are replaced in one batch; `self.writable` starts at its new initial value (decision 2); `retryOn`
|
||||
sources are re-declared. **Routines hand over per object:** the old attempt's routines are aborted; the new ones
|
||||
sources are re-declared; the new attempt's live queries run at its activation (the old attempt's are re-run by the
|
||||
commits of its remaining calls until it is sealed, then dropped). **Routines hand over per object:** the old attempt's routines are aborted; the new ones
|
||||
start once the old routine calls finished, bounded by `closeTimeout` (a stuck one is reported `drain_timeout` and
|
||||
the whole old attempt is sealed then, audit A12: one seal token per attempt); calls never wait.
|
||||
- **Natives are not kept across reloads by name:** natives have no line, transactions or schema, so old and new code
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ addressable hosts; an **address** (`Session({ id: "p1" })`) names one. An **obje
|
|||
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 **document** is data an object
|
||||
stores, changed in **transactions**; a **table** holds many records with queries, in the host's **store**; a **derive** is a value computed from others; a **native** is an outside
|
||||
stores, changed in **transactions**; a **table** holds many records with queries, in the host's **store**, and a **live query** keeps some of its rows current; 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.
|
||||
|
||||
|
|
@ -180,6 +180,20 @@ Persisted data lives only in `local` objects with empty params (one per host and
|
|||
on who asked or with which params; an object with params (a view per project) reads from such a store. Writes go
|
||||
through the same transactions as documents: a note row and a document can change in one commit.
|
||||
|
||||
A table has no watch of its own. To show rows live (the newest notes in a tab), the object declares a **live query**
|
||||
over its own table, with a limit:
|
||||
|
||||
```ts
|
||||
const newest = self.query(rows, recent, 50); // the 50 newest rows, a value like a document's
|
||||
return { newest, add, page }; // a member: tabs watch it and get only the changes
|
||||
```
|
||||
|
||||
Chord runs the query before the object goes live, and again in every commit of the object that writes `rows`, and
|
||||
sends what changed. Only the object's own tables: it is their only writer, so its commits are exactly when the rows
|
||||
can change, and a commit that writes no such table costs nothing. Each writing commit runs the query once more, at
|
||||
most the limit's rows: right for data people edit, wrong for a table written hundreds of times a second (there may be
|
||||
dragons; keep what the tab shows in a document the object writes with each change instead). The limit is required.
|
||||
|
||||
A **derive** is a value computed from other values, recomputed whenever they change. It is how an object offers
|
||||
something it does not store:
|
||||
|
||||
|
|
@ -212,7 +226,8 @@ self.onLive(async (ctx) => { // a routine start
|
|||
```
|
||||
|
||||
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.
|
||||
Chord closes it and opens it again. Objects without natives are swapped in place. A first read of a table is no
|
||||
native: declare it as a live query, not with `self.open`, or the object restarts at every change of its plugin.
|
||||
|
||||
**Await what you start in a root or a handler.** What is started with a context ends with it. Code outside objects
|
||||
gets its `ctx` from `host.root(fn, ctx)`, and that scope seals when `fn` returns; a handler's context ends when its
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ addressable hosts; an <strong>address</strong> (<code>Session({ id: "p1&quo
|
|||
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>document</strong> is data an object
|
||||
stores, changed in <strong>transactions</strong>; a <strong>table</strong> holds many records with queries, in the host's <strong>store</strong>; a <strong>derive</strong> is a value computed from others; a <strong>native</strong> is an outside
|
||||
stores, changed in <strong>transactions</strong>; a <strong>table</strong> holds many records with queries, in the host's <strong>store</strong>, and a <strong>live query</strong> keeps some of its rows current; 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>
|
||||
|
|
@ -258,6 +258,15 @@ host.<span class="tk-f">serve</span>(<span class="tk-t">NoteStore</span>, (_para
|
|||
<p>Persisted data lives only in <code>local</code> objects with empty params (one per host and type), so its address never depends
|
||||
on who asked or with which params; an object with params (a view per project) reads from such a store. Writes go
|
||||
through the same transactions as documents: a note row and a document can change in one commit.</p>
|
||||
<p>A table has no watch of its own. To show rows live (the newest notes in a tab), the object declares a <strong>live query</strong>
|
||||
over its own table, with a limit:</p>
|
||||
<pre><code><span class="tk-k">const</span> newest = self.<span class="tk-f">query</span>(rows, recent, <span class="tk-n">50</span>); <span class="tk-c">// the 50 newest rows, a value like a document's</span>
|
||||
<span class="tk-k">return</span> { newest, add, page }; <span class="tk-c">// a member: tabs watch it and get only the changes</span></code></pre>
|
||||
<p>Chord runs the query before the object goes live, and again in every commit of the object that writes <code>rows</code>, and
|
||||
sends what changed. Only the object's own tables: it is their only writer, so its commits are exactly when the rows
|
||||
can change, and a commit that writes no such table costs nothing. Each writing commit runs the query once more, at
|
||||
most the limit's rows: right for data people edit, wrong for a table written hundreds of times a second (there may be
|
||||
dragons; keep what the tab shows in a document the object writes with each change instead). The limit is required.</p>
|
||||
<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>
|
||||
|
|
@ -277,7 +286,8 @@ self.<span class="tk-f">onLive</span>(<span class="tk-k">async</span> (ctx) =>
|
|||
<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>
|
||||
Chord closes it and opens it again. Objects without natives are swapped in place. A first read of a table is no
|
||||
native: declare it as a live query, not with <code>self.open</code>, or the object restarts at every change of its plugin.</p>
|
||||
<p><strong>Await what you start in a root or a handler.</strong> What is started with a context ends with it. Code outside objects
|
||||
gets its <code>ctx</code> from <code>host.root(fn, ctx)</code>, and that scope seals when <code>fn</code> returns; a handler's context ends when its
|
||||
caller cancels or its object ends. A change still waiting when its context ends is cancelled before it commits:</p>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,27 @@ A facet only declares (`host.serve`, `self.contribute`, `host.references`), sync
|
|||
happens in objects: state in documents and tables, standing behaviour in a `local` object with empty params (it lives
|
||||
as long as the host). Names are prefixed by the plugin (`bob.stats`).
|
||||
|
||||
## Tables shown live
|
||||
|
||||
A table has no watch. To show its rows live, the object owning it declares a live query with a limit:
|
||||
|
||||
```ts
|
||||
host.serve(Store, (_params, self) => { // a local type with empty params: it may have tables
|
||||
const items = self.table(Items);
|
||||
const newest = self.query(items, query(Items).orderBy((i) => i.id, "desc"), 50);
|
||||
return { items: newest, add: async (row, ctx) => self.tx(async (tx) => tx.put(items, row), ctx) };
|
||||
});
|
||||
```
|
||||
|
||||
- Chord runs it before the object goes live and again in each commit of the object that writes the table; watchers get
|
||||
only what changed. A commit writing no such table runs nothing.
|
||||
- Only the object's own tables, the query fixed for the object's attempt (decide it from params), the limit required
|
||||
(`invalid/args` without one).
|
||||
- One query per writing commit, at most `limit` rows (for 100 rows about 60 µs more per commit, 80 µs when watched;
|
||||
`benchmarks.md`): there may be dragons for a table written hundreds of times a second. Keep a document for those.
|
||||
- Never read a table in `self.open` to fill a window: that makes a native, and the object restarts at every reload of
|
||||
its plugin instead of swapping in place. A live query is no native.
|
||||
|
||||
## What a plugin may import
|
||||
|
||||
| Import | Resolves to |
|
||||
|
|
@ -209,5 +230,6 @@ of plugins it has no files of, it declares again, as `Notes` above.
|
|||
3. Imports: the app's API at one version, own files, same-root contract files by relative path, other repositories'
|
||||
by package name pinned in `dependencies`.
|
||||
4. Contracts prefixed with the plugin's name; a version bump for every change of shape.
|
||||
5. Files of one kind named `*.<kind>.ts(x)`.
|
||||
6. A test with `testPlugin` and stubs for every name `t.unserved()` lists.
|
||||
5. Live rows of a table through `self.query(table, q, limit)`, never a read in `self.open`.
|
||||
6. Files of one kind named `*.<kind>.ts(x)`.
|
||||
7. A test with `testPlugin` and stubs for every name `t.unserved()` lists.
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ slice spec's section citations are for orientation, not a reading limit (owner,
|
|||
- **Slice 11** (committed 2026-11-03, `slice-11.md`): `src/surfaces/` (scripts objects, registered scripts, the
|
||||
connection loop, clients, themes), `src/web/`, `src/react/`, `src/tui/`, `src/node/surfaces/`, `src/node/tui/` (the
|
||||
launcher and its child-process sandbox); examples 56-59.
|
||||
- **Live queries** (built 2026-11-03, decision "Live queries over an object's own tables"): `self.query` in
|
||||
`src/store/live-query.ts`; examples 59 and 61 use it; `test/bench/live-query.ts`.
|
||||
- **Chord's shared code** (upstream pi): `src/watch.ts` (`Watch`, `NativeWatch`, `WatchHandle`, `WatchEnd`,
|
||||
`NativeWatchable`), Delta (`track`, `diffRevisions`, `applyImmutable`, encoders, `PendingOps`, `appendText`),
|
||||
context, JSON, output, SQLite drivers.
|
||||
|
|
@ -136,6 +138,7 @@ pattern, no code. Decided cuts are noted where they apply.
|
|||
| 11 | `Watchable`, watches (Chord's `Watch`), the batch (W1), reentrancy | 1 |
|
||||
| 11 | `NativeWatchable`, `self.writable` | 8 |
|
||||
| 11 | Durable's views as sources (`self.follow` over Durable's `observeXXX`) | 6b |
|
||||
| 11 | live queries (`self.query(table, q, limit)`: run before live, re-run per touching commit, diffed) | after 11 |
|
||||
| 12 | `self.derive`, failures per member, lazy when unwatched, `mapShared` | 1 |
|
||||
| 12 | container-width benchmark (R24) | 9 |
|
||||
| 13 | `self.open`, close reasons, order, restart patterns (R16) | 2 (restarts on reload: 7b) |
|
||||
|
|
@ -193,12 +196,14 @@ deleted (the slice's spec and code are the record). Moved here from `carry-over.
|
|||
- **`rebuild` of tables** (key and column-type changes): `evolveTable` has no edit for them; build one only when a
|
||||
real case needs it. (Slice 6a's second spec review; out of slice 7b's scope, `slice-7b.md` §12.)
|
||||
|
||||
### Later: live queries (when a case needs it)
|
||||
|
||||
- **Incremental maintenance of a live query's result** (decision "Live queries over an object's own tables"): an
|
||||
ordered, limited result kept from row-level changes in memory, re-running only when a removal empties the window
|
||||
or a bulk write hides which rows changed; today every touching commit re-runs the query. (Live queries build.)
|
||||
|
||||
### After slice 11
|
||||
|
||||
- **Live queries** (decision "Live queries over an object's own tables"): `self.query(table, q)`, re-run per
|
||||
touching commit and diffed; tests (activation before live, swap on reload, a commit not touching the table costs
|
||||
nothing, the limit required, cost guard); the tutorial and 59-chat use it instead of `self.open`; the guide's
|
||||
section with the warning. Later, when a case needs it: incremental maintenance of an ordered, limited result.
|
||||
- **Messages and languages in the app kit** (decision "Messages and languages in the app kit", proposal
|
||||
`docs/i18n/`): `defineMessages`, `t.message`/`t.render`, `MessagesOf`, the `Translations` registry with
|
||||
fallbacks and conflicts, the locale preference per principal (default from the device, live switch, `lang`/`dir` on
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ without one (a **client**: a page, a terminal). A **link** connects two hosts. A
|
|||
the app and stamped by links. An **object** is state and methods at one identity; its **contract** (`local` for its own
|
||||
host only, `remote` for any host) names its **params** (which instance) and members. A remote contract's **variants**
|
||||
are the member sets different principals get. A **factory** declares an object's parts: **documents** and **tables**
|
||||
(stored data, changed in **transactions**), **derives** (values computed from others), **natives** (things opened and
|
||||
closed, such as a file or a database cursor). A **reference** is what asking for an object returns. A **facet** is a
|
||||
(stored data, changed in **transactions**), **live queries** (a table's rows Chord keeps current), **derives**
|
||||
(values computed from others), **natives** (things opened and closed, such as a file or a database cursor). A **reference** is what asking for an object returns. A **facet** is a
|
||||
plugin's code for one host kind; a **registry** is an extension point plugins **contribute** to. A **rollout** applies
|
||||
new plugin code to the hosts in order.
|
||||
|
||||
|
|
@ -285,7 +285,7 @@ each start. It sits in the URL's fragment, which browsers never send, so it neve
|
|||
it and receives a session cookie. A command-line client on the same machine reads `owner-token` instead (mode 0600,
|
||||
chapter III). Nothing is the owner because it comes from loopback: any local process could.
|
||||
|
||||
# A List on Its Own Worker {#worker lede="A host kind, a remote contract, a worker facet with a table and a live window, and the principal that adds a todo."}
|
||||
# A List on Its Own Worker {#worker lede="A host kind, a remote contract, a worker facet with a table and a live query, and the principal that adds a todo."}
|
||||
|
||||
Each list gets its own process, a **worker**, with its own store: a list that misbehaves cannot take the others down,
|
||||
and an idle list costs nothing. This chapter declares where lists live, what a list offers, and the worker code that
|
||||
|
|
@ -376,7 +376,7 @@ const Items = defineTable("todo.items", {
|
|||
/** The newest todos first. */
|
||||
const NEWEST = query(Items).orderBy((item) => item.id, "desc");
|
||||
|
||||
/** How many todos the live window holds. */
|
||||
/** How many todos the live query holds. */
|
||||
const WINDOW = 50;
|
||||
```
|
||||
|
||||
|
|
@ -406,40 +406,38 @@ of references. Its factory declares its parts and returns its members:
|
|||
```ts
|
||||
host.serve(Store, (_params, self) => {
|
||||
const items = self.table(Items);
|
||||
// The newest todos, oldest first, and the next todo's number: read once when the store goes live, then kept here.
|
||||
const window = self.writable<Item[]>([]);
|
||||
let next = 1;
|
||||
self.open(
|
||||
async (ctx) => {
|
||||
const { items: newest } = await items.page(NEWEST, WINDOW, ctx);
|
||||
window.set([...newest].reverse());
|
||||
next = (newest[0]?.id ?? 0) + 1;
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
// The newest todos, live: read before the store goes live, read again after every commit writing the table.
|
||||
const newest = self.query(items, NEWEST, WINDOW);
|
||||
return {
|
||||
items: self.derive([window], ([value]) => value),
|
||||
// Oldest first, as the list shows them.
|
||||
items: self.derive([newest], ([rows]) => [...rows].reverse()),
|
||||
```
|
||||
|
||||
A **factory** is synchronous and only declares; Chord activates the object afterwards. `self.table` declares the
|
||||
table. A table has no watch, so the store keeps a **live window** itself: `self.writable` holds the newest 50 todos,
|
||||
filled by `self.open`, which runs during activation, and exposed through `self.derive` (a writable is an in-process
|
||||
value, not a state member; a derive over it is). `self.open` is meant for **natives**, things opened and closed; the
|
||||
window's first read is one, and chapter VIII shows its price.
|
||||
table. `self.query(items, NEWEST, WINDOW)` declares a **live query** over it: the rows of `NEWEST`, at most 50, as a
|
||||
value the object can expose or derive from. Chord runs it before the store goes live, and again after every commit
|
||||
of the store that writes `items`, and sends watchers only what changed. The store's derive turns the rows around,
|
||||
oldest first, for the list.
|
||||
|
||||
A live query reads only its own object's tables: only the store writes them, so its own commits are exactly the
|
||||
moments the rows can change, and a commit that writes no table with a query costs nothing. The limit is required:
|
||||
each such commit runs the query once more, at most `WINDOW` rows. That is cheap for a list a person edits and wrong
|
||||
for a table written hundreds of times a second (design §11, "there may be dragons").
|
||||
|
||||
Writes go through **transactions**: `self.tx(fn, ctx)` runs `fn` with `tx`, and what it stages is committed at once
|
||||
when `fn` returns, or not at all if it throws. The window follows after the commit; the store is the table's only
|
||||
writer, so it knows every change:
|
||||
when `fn` returns, or not at all if it throws. The live query follows in the same commit; the store keeps nothing
|
||||
else in memory, not even the next todo's number, which it reads in the transaction:
|
||||
|
||||
<!-- quote: plugins/lists/worker.ts -->
|
||||
```ts
|
||||
add: async ({ text, by }, ctx) => {
|
||||
// Refused as Chord refuses arguments that fail their schema: the caller gets `invalid/args`.
|
||||
if (text === "") throw invalid("args", "a todo needs text");
|
||||
const item = { id: next++, text, done: false, by, at: Date.now() };
|
||||
await self.tx(async (tx) => tx.put(items, item), ctx);
|
||||
// In number order: two adds may commit in either order.
|
||||
window.set([...window.value, item].sort((a, b) => a.id - b.id).slice(-WINDOW));
|
||||
await self.tx(async (tx) => {
|
||||
// The number after the newest todo's: the store's transactions run one after another.
|
||||
const { items: last } = await tx.page(items, NEWEST, 1);
|
||||
tx.put(items, { id: (last[0]?.id ?? 0) + 1, text, done: false, by, at: Date.now() });
|
||||
}, ctx);
|
||||
return null;
|
||||
```
|
||||
|
||||
|
|
@ -1057,7 +1055,7 @@ console.log(" ada adds ' ':", refused);
|
|||
<!-- output: main -->
|
||||
```text
|
||||
8. lists changed, rolled out to: server, list, web, tui
|
||||
on list|groceries: todo.store restarted, todo.todos(groceries) parked
|
||||
on list|groceries: todo.store swapped, todo.todos(groceries) parked
|
||||
ada adds ' tea ': [ ] tea (ada)
|
||||
ada adds ' ': { code: 'invalid', reason: 'args' }
|
||||
```
|
||||
|
|
@ -1074,9 +1072,21 @@ and rolled it out host by host. On each host Chord stages the new code (it runs
|
|||
| its new code waits for a dependency | **parked** until the dependency is live |
|
||||
| its type was removed | **ended**; its holders wait until something serves it again |
|
||||
|
||||
The store opens a native (the window's first read), so it restarted and read its window again from the table; the
|
||||
views waited for it (parked), then went on with the new code. Ada's reference never noticed more than a moment of
|
||||
`loading`, and her next add was trimmed. Her blank one is now empty after trimming, so the store refused it with
|
||||
The store opens no natives, and neither does its new factory: its live query is not a native, Chord runs it. So the
|
||||
store was **swapped**, not restarted. The sequence on the list's worker:
|
||||
|
||||
1. The store's new attempt starts beside the old one: the same object, instance, table and line. Calls arriving now
|
||||
wait at a gate; calls already running finish on the old code.
|
||||
2. The new attempt runs its live query (the same read as at the first activation) and goes live; the gate opens,
|
||||
and references move to its members by name, each with one frame from its last value.
|
||||
3. The old attempt is sealed once its calls settled; its query leaves with it, so later commits run only the new one.
|
||||
|
||||
The views' new attempts need the store, which was still between steps 1 and 2 when they checked it, so they
|
||||
**parked** for that moment and went live with the new code right after. Ada's reference waited a moment, and her next add was trimmed.
|
||||
Had the store kept its window as a one-time read in `self.open`, it would count as holding a native and **restart**
|
||||
at every reload of its plugin: running calls drained, natives closed and opened again, a new instance.
|
||||
|
||||
Ada's blank todo is now empty after trimming, so the store refused it with
|
||||
`invalid("args", …)` (chapter II): her client got `{ code: "invalid", reason: "args" }`, Chord's own error, which
|
||||
crossed the worker's link and the server's as it was. Had the edit broken the plugin, that plugin alone would have kept its old code
|
||||
and the report would name the error.
|
||||
|
|
@ -1364,14 +1374,11 @@ a tutorial.
|
|||
`"~kind": "Integer"`, which pi-schema's `int()` lacks, so the key of an `int` key column is typed `string`:
|
||||
`tx.get(items, { id })` with a number fails `tsc`. The store here uses `updateWhere` with `eq` instead of
|
||||
`tx.update`.
|
||||
- **A one-time read at activation is a native.** The live window over a table needs a first read before the object is
|
||||
live; `self.open` is the only place, so the store counts as holding a native and restarts at every reload of its
|
||||
plugin, parking its views, though it holds nothing open. Tables have no watch, so every table-backed live view
|
||||
repeats this pattern (example 59's chat does too).
|
||||
|
||||
Three more, found the same way, are fixed: a reloaded web plugin lost its own React state (now `usePageState`,
|
||||
Four more, found the same way, are fixed: a reloaded web plugin lost its own React state (now `usePageState`,
|
||||
chapter VIII), a plugin's `contract.ts` could not be imported outside builds (the API is now a workspace package,
|
||||
chapter I), and plugins could not throw Chord's `invalid` (Chord exports `invalid` and `forbidden`, chapter II).
|
||||
chapter I), plugins could not throw Chord's `invalid` (Chord exports `invalid` and `forbidden`, chapter II), and a
|
||||
live window over a table needed a one-time read in `self.open`, which made the store restart at every reload of its
|
||||
plugin (now `self.query`, a live query Chord runs, chapters II and VIII).
|
||||
|
||||
# The App's Files {.appendix}
|
||||
|
||||
|
|
@ -1399,7 +1406,7 @@ chapter I), and plugins could not throw Chord's `invalid` (Chord exports `invali
|
|||
| `startWorker({ api, kinds, onError })` | I | a worker's entry |
|
||||
| `app.ownerLink()`, `app.ownerTokenFile` | I, III | the owner's sign-in, for a browser and for local clients |
|
||||
| `hostKind(name, params)`, `remote(name, { version, params, hostAddress, variants })`, `local(name, { version, params, members })` | II | host kinds and contracts |
|
||||
| `host.serve(Type, factory)`; factory `self`: `table`, `writable`, `open`, `derive`, `tx`, `get`, `contribute` | II | an object's parts |
|
||||
| `host.serve(Type, factory)`; factory `self`: `table`, `query`, `derive`, `tx`, `get`, `contribute` | II | an object's parts |
|
||||
| `host.root(fn, ctx, { principal })`, `ctx.get(Type, params)` | II | code outside objects, as a principal |
|
||||
| `invalid(reason, message, details?)`, `forbidden(message, details?)` | II, VIII | Chord's errors, thrown by a plugin |
|
||||
| `self.contribute(Http, { routes })` | III | routes, denied by default |
|
||||
|
|
|
|||
|
|
@ -181,8 +181,8 @@ without one (a <strong>client</strong>: a page, a terminal). A <strong>link</str
|
|||
the app and stamped by links. An <strong>object</strong> is state and methods at one identity; its <strong>contract</strong> (<code>local</code> for its own
|
||||
host only, <code>remote</code> for any host) names its <strong>params</strong> (which instance) and members. A remote contract's <strong>variants</strong>
|
||||
are the member sets different principals get. A <strong>factory</strong> declares an object's parts: <strong>documents</strong> and <strong>tables</strong>
|
||||
(stored data, changed in <strong>transactions</strong>), <strong>derives</strong> (values computed from others), <strong>natives</strong> (things opened and
|
||||
closed, such as a file or a database cursor). A <strong>reference</strong> is what asking for an object returns. A <strong>facet</strong> is a
|
||||
(stored data, changed in <strong>transactions</strong>), <strong>live queries</strong> (a table's rows Chord keeps current), <strong>derives</strong>
|
||||
(values computed from others), <strong>natives</strong> (things opened and closed, such as a file or a database cursor). A <strong>reference</strong> is what asking for an object returns. A <strong>facet</strong> is a
|
||||
plugin's code for one host kind; a <strong>registry</strong> is an extension point plugins <strong>contribute</strong> to. A <strong>rollout</strong> applies
|
||||
new plugin code to the hosts in order.</p>
|
||||
</section>
|
||||
|
|
@ -353,7 +353,7 @@ it and receives a session cookie. A command-line client on the same machine read
|
|||
chapter III). Nothing is the owner because it comes from loopback: any local process could.</p>
|
||||
</section>
|
||||
|
||||
<section class="chapter" id="worker"><header class="opener"><div class="numeral">II</div><h2>A List on Its Own Worker</h2><p class="lede">A host kind, a remote contract, a worker facet with a table and a live window, and the principal that adds a todo.</p></header>
|
||||
<section class="chapter" id="worker"><header class="opener"><div class="numeral">II</div><h2>A List on Its Own Worker</h2><p class="lede">A host kind, a remote contract, a worker facet with a table and a live query, and the principal that adds a todo.</p></header>
|
||||
<p class="dropcap">Each list gets its own process, a <strong>worker</strong>, with its own store: a list that misbehaves cannot take the others down,
|
||||
and an idle list costs nothing. This chapter declares where lists live, what a list offers, and the worker code that
|
||||
keeps its todos.</p>
|
||||
|
|
@ -425,7 +425,7 @@ worker's store:</p>
|
|||
<span class="tk-c">/** The newest todos first. */</span>
|
||||
<span class="tk-k">const</span> NEWEST = <span class="tk-f">query</span>(<span class="tk-t">Items</span>).<span class="tk-f">orderBy</span>((item) => item.id, <span class="tk-s">"desc"</span>);
|
||||
|
||||
<span class="tk-c">/** How many todos the live window holds. */</span>
|
||||
<span class="tk-c">/** How many todos the live query holds. */</span>
|
||||
<span class="tk-k">const</span> WINDOW = <span class="tk-n">50</span>;</code></pre>
|
||||
<p>A table holds many records with queries and pages; it lives in an object, never in a facet. Persisted data lives only
|
||||
in <code>local</code> objects with empty params (one per host and type), so its address never depends on who asked (design §9,
|
||||
|
|
@ -447,35 +447,32 @@ of references. Its factory declares its parts and returns its members:</p>
|
|||
<!-- quote: plugins/lists/worker.ts -->
|
||||
<pre><code> host.<span class="tk-f">serve</span>(<span class="tk-t">Store</span>, (_params, self) => {
|
||||
<span class="tk-k">const</span> items = self.<span class="tk-f">table</span>(<span class="tk-t">Items</span>);
|
||||
<span class="tk-c">// The newest todos, oldest first, and the next todo's number: read once when the store goes live, then kept here.</span>
|
||||
<span class="tk-k">const</span> window = self.<span class="tk-f">writable</span><<span class="tk-t">Item</span>[]>([]);
|
||||
<span class="tk-k">let</span> next = <span class="tk-n">1</span>;
|
||||
self.<span class="tk-f">open</span>(
|
||||
<span class="tk-k">async</span> (ctx) => {
|
||||
<span class="tk-k">const</span> { items: newest } = <span class="tk-k">await</span> items.<span class="tk-f">page</span>(NEWEST, WINDOW, ctx);
|
||||
window.<span class="tk-f">set</span>([...newest].<span class="tk-f">reverse</span>());
|
||||
next = (newest[<span class="tk-n">0</span>]?.id ?? <span class="tk-n">0</span>) + <span class="tk-n">1</span>;
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
<span class="tk-c">// The newest todos, live: read before the store goes live, read again after every commit writing the table.</span>
|
||||
<span class="tk-k">const</span> newest = self.<span class="tk-f">query</span>(items, NEWEST, WINDOW);
|
||||
<span class="tk-k">return</span> {
|
||||
items: self.<span class="tk-f">derive</span>([window], ([value]) => value),</code></pre>
|
||||
<span class="tk-c">// Oldest first, as the list shows them.</span>
|
||||
items: self.<span class="tk-f">derive</span>([newest], ([rows]) => [...rows].<span class="tk-f">reverse</span>()),</code></pre>
|
||||
<p>A <strong>factory</strong> is synchronous and only declares; Chord activates the object afterwards. <code>self.table</code> declares the
|
||||
table. A table has no watch, so the store keeps a <strong>live window</strong> itself: <code>self.writable</code> holds the newest 50 todos,
|
||||
filled by <code>self.open</code>, which runs during activation, and exposed through <code>self.derive</code> (a writable is an in-process
|
||||
value, not a state member; a derive over it is). <code>self.open</code> is meant for <strong>natives</strong>, things opened and closed; the
|
||||
window's first read is one, and chapter VIII shows its price.</p>
|
||||
table. <code>self.query(items, NEWEST, WINDOW)</code> declares a <strong>live query</strong> over it: the rows of <code>NEWEST</code>, at most 50, as a
|
||||
value the object can expose or derive from. Chord runs it before the store goes live, and again after every commit
|
||||
of the store that writes <code>items</code>, and sends watchers only what changed. The store's derive turns the rows around,
|
||||
oldest first, for the list.</p>
|
||||
<p>A live query reads only its own object's tables: only the store writes them, so its own commits are exactly the
|
||||
moments the rows can change, and a commit that writes no table with a query costs nothing. The limit is required:
|
||||
each such commit runs the query once more, at most <code>WINDOW</code> rows. That is cheap for a list a person edits and wrong
|
||||
for a table written hundreds of times a second (design §11, "there may be dragons").</p>
|
||||
<p>Writes go through <strong>transactions</strong>: <code>self.tx(fn, ctx)</code> runs <code>fn</code> with <code>tx</code>, and what it stages is committed at once
|
||||
when <code>fn</code> returns, or not at all if it throws. The window follows after the commit; the store is the table's only
|
||||
writer, so it knows every change:</p>
|
||||
when <code>fn</code> returns, or not at all if it throws. The live query follows in the same commit; the store keeps nothing
|
||||
else in memory, not even the next todo's number, which it reads in the transaction:</p>
|
||||
<!-- quote: plugins/lists/worker.ts -->
|
||||
<pre><code> add: <span class="tk-k">async</span> ({ text, by }, ctx) => {
|
||||
<span class="tk-c">// Refused as Chord refuses arguments that fail their schema: the caller gets `invalid/args`.</span>
|
||||
<span class="tk-k">if</span> (text === <span class="tk-s">""</span>) <span class="tk-k">throw</span> <span class="tk-f">invalid</span>(<span class="tk-s">"args"</span>, <span class="tk-s">"a todo needs text"</span>);
|
||||
<span class="tk-k">const</span> item = { id: next++, text, done: <span class="tk-k">false</span>, by, at: <span class="tk-t">Date</span>.<span class="tk-f">now</span>() };
|
||||
<span class="tk-k">await</span> self.<span class="tk-f">tx</span>(<span class="tk-k">async</span> (tx) => tx.<span class="tk-f">put</span>(items, item), ctx);
|
||||
<span class="tk-c">// In number order: two adds may commit in either order.</span>
|
||||
window.<span class="tk-f">set</span>([...window.value, item].<span class="tk-f">sort</span>((a, b) => a.id - b.id).<span class="tk-f">slice</span>(-WINDOW));
|
||||
<span class="tk-k">await</span> self.<span class="tk-f">tx</span>(<span class="tk-k">async</span> (tx) => {
|
||||
<span class="tk-c">// The number after the newest todo's: the store's transactions run one after another.</span>
|
||||
<span class="tk-k">const</span> { items: last } = <span class="tk-k">await</span> tx.<span class="tk-f">page</span>(items, NEWEST, <span class="tk-n">1</span>);
|
||||
tx.<span class="tk-f">put</span>(items, { id: (last[<span class="tk-n">0</span>]?.id ?? <span class="tk-n">0</span>) + <span class="tk-n">1</span>, text, done: <span class="tk-k">false</span>, by, at: <span class="tk-t">Date</span>.<span class="tk-f">now</span>() });
|
||||
}, ctx);
|
||||
<span class="tk-k">return</span> <span class="tk-k">null</span>;</code></pre>
|
||||
<p>A todo without text is refused with <code>invalid("args", …)</code>, from the API: the error crosses every link as it is, so a
|
||||
caller on a page or in a terminal gets <code>{ code: "invalid", reason: "args" }</code>, as for arguments that fail the
|
||||
|
|
@ -959,7 +956,7 @@ console.<span class="tk-f">log</span>(<span class="tk-s">" ada adds ' tea ':
|
|||
console.<span class="tk-f">log</span>(<span class="tk-s">" ada adds ' ':"</span>, refused);</code></pre>
|
||||
<!-- output: main -->
|
||||
<pre class="plain"><code>8. lists changed, rolled out to: server, list, web, tui
|
||||
on list|groceries: todo.store restarted, todo.todos(groceries) parked
|
||||
on list|groceries: todo.store swapped, todo.todos(groceries) parked
|
||||
ada adds ' tea ': [ ] tea (ada)
|
||||
ada adds ' ': { code: 'invalid', reason: 'args' }</code></pre>
|
||||
<p>The watcher marked <code>@todo/lists</code> dirty when the file changed; <code>applyChanges</code> rebuilt it (one esbuild call per facet)
|
||||
|
|
@ -990,9 +987,20 @@ and rolled it out host by host. On each host Chord stages the new code (it runs
|
|||
<td><strong>ended</strong>; its holders wait until something serves it again</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
<p>The store opens a native (the window's first read), so it restarted and read its window again from the table; the
|
||||
views waited for it (parked), then went on with the new code. Ada's reference never noticed more than a moment of
|
||||
<code>loading</code>, and her next add was trimmed. Her blank one is now empty after trimming, so the store refused it with
|
||||
<p>The store opens no natives, and neither does its new factory: its live query is not a native, Chord runs it. So the
|
||||
store was <strong>swapped</strong>, not restarted. The sequence on the list's worker:</p>
|
||||
<ol>
|
||||
<li>The store's new attempt starts beside the old one: the same object, instance, table and line. Calls arriving now
|
||||
wait at a gate; calls already running finish on the old code.</li>
|
||||
<li>The new attempt runs its live query (the same read as at the first activation) and goes live; the gate opens,
|
||||
and references move to its members by name, each with one frame from its last value.</li>
|
||||
<li>The old attempt is sealed once its calls settled; its query leaves with it, so later commits run only the new one.</li>
|
||||
</ol>
|
||||
<p>The views' new attempts need the store, which was still between steps 1 and 2 when they checked it, so they
|
||||
<strong>parked</strong> for that moment and went live with the new code right after. Ada's reference waited a moment, and her next add was trimmed.
|
||||
Had the store kept its window as a one-time read in <code>self.open</code>, it would count as holding a native and <strong>restart</strong>
|
||||
at every reload of its plugin: running calls drained, natives closed and opened again, a new instance.</p>
|
||||
<p>Ada's blank todo is now empty after trimming, so the store refused it with
|
||||
<code>invalid("args", …)</code> (chapter II): her client got <code>{ code: "invalid", reason: "args" }</code>, Chord's own error, which
|
||||
crossed the worker's link and the server's as it was. Had the edit broken the plugin, that plugin alone would have kept its old code
|
||||
and the report would name the error.</p>
|
||||
|
|
@ -1254,15 +1262,13 @@ a tutorial.</p>
|
|||
<li><strong>Integer table keys do not type-check.</strong> <code>KeyOf</code> in <code>src/store/table.ts</code> still tests TypeBox's
|
||||
<code>"~kind": "Integer"</code>, which pi-schema's <code>int()</code> lacks, so the key of an <code>int</code> key column is typed <code>string</code>:
|
||||
<code>tx.get(items, { id })</code> with a number fails <code>tsc</code>. The store here uses <code>updateWhere</code> with <code>eq</code> instead of
|
||||
<code>tx.update</code>.</li>
|
||||
<li><strong>A one-time read at activation is a native.</strong> The live window over a table needs a first read before the object is
|
||||
live; <code>self.open</code> is the only place, so the store counts as holding a native and restarts at every reload of its
|
||||
plugin, parking its views, though it holds nothing open. Tables have no watch, so every table-backed live view
|
||||
repeats this pattern (example 59's chat does too).</li>
|
||||
</ul>
|
||||
<p>Three more, found the same way, are fixed: a reloaded web plugin lost its own React state (now <code>usePageState</code>,
|
||||
<code>tx.update</code>.
|
||||
Four more, found the same way, are fixed: a reloaded web plugin lost its own React state (now <code>usePageState</code>,
|
||||
chapter VIII), a plugin's <code>contract.ts</code> could not be imported outside builds (the API is now a workspace package,
|
||||
chapter I), and plugins could not throw Chord's <code>invalid</code> (Chord exports <code>invalid</code> and <code>forbidden</code>, chapter II).</p>
|
||||
chapter I), plugins could not throw Chord's <code>invalid</code> (Chord exports <code>invalid</code> and <code>forbidden</code>, chapter II), and a
|
||||
live window over a table needed a one-time read in <code>self.open</code>, which made the store restart at every reload of its
|
||||
plugin (now <code>self.query</code>, a live query Chord runs, chapters II and VIII).</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="chapter appendix" id="the-app-s-files"><header class="opener"><div class="numeral">A</div><h2>The App's Files</h2></header>
|
||||
|
|
@ -1372,7 +1378,7 @@ chapter I), and plugins could not throw Chord's <code>invalid</code> (Chord
|
|||
<td>host kinds and contracts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>host.serve(Type, factory)</code>; factory <code>self</code>: <code>table</code>, <code>writable</code>, <code>open</code>, <code>derive</code>, <code>tx</code>, <code>get</code>, <code>contribute</code></td>
|
||||
<td><code>host.serve(Type, factory)</code>; factory <code>self</code>: <code>table</code>, <code>query</code>, <code>derive</code>, <code>tx</code>, <code>get</code>, <code>contribute</code></td>
|
||||
<td>II</td>
|
||||
<td>an object's parts</td>
|
||||
</tr>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue