diff --git a/packages/chord/CHANGELOG.md b/packages/chord/CHANGELOG.md
index 5e17d46e6..de47a4729 100644
--- a/packages/chord/CHANGELOG.md
+++ b/packages/chord/CHANGELOG.md
@@ -18,6 +18,7 @@
- Chord v2's root exports `invalid(reason, message, details?)` and the new `forbidden(message, details?)`, the `ChordError`s a plugin throws for arguments or params it refuses (`invalid/args`, `invalid/params`) and for a principal it refuses (`forbidden`); an app's API re-exports them, and the caller gets the same `{ code, reason }` as from Chord's own checks.
- Chord v2's React bindings gain `usePageState(key, initial)`, `useState`'s pair with the value kept by the page's host under `key` instead of the component: a component of a reloaded web plugin is a new function and remounts (its `useState` starts over), and page state is what survives (a half-typed message, a scroll position); any value, never on the wire, gone with the page. `TuiScope` gains `pageState(key, initial)` on the same per-host store, its changes calling the scope's `changed`.
- Chord v2 reports mistakes that waited silently through `onError` (decision "Mistakes that wait are reported at once"): `unbound_long { waitingFor, error, since }`, a request waiting unbound (a type or registry not served here, not at its version, a registry whose first `compose` threw) once after 5 s, on one timer per host (it keeps waiting until its `readyTimeout`); `registry_undeclared { registry, plugins }`, a registry with contributions and no provider at a publication (`load`, a reload's commit) or when a host-lifetime object's contributions enter, naming the contributing plugins, again only once they change; `transaction_cancelled { object, error }`, a document change or transaction that did not commit because the context it was started with ended first (a root whose `fn` returned, a caller that cancelled, its object's end), besides rejecting.
+- Chord v2 gains live queries: `self.query(table, q, limit)` in the factory of a table's owner is a watchable of the query's rows (at most `limit`, in its order), usable as a state member and a derive source. Chord runs it before the object goes live and again inside the commit step of every commit of the object that writes the table, publishing only the diff in that commit's batch; a commit writing no table with a live query runs nothing (`chord.query.runs` counts the runs). Only the object's own tables; the limit is required (`invalid/args` at declaration). A run that fails (a row the code cannot read) is a failed member, reported once as the new `HostError` `query_failed { object, table, error }`, and heals at the next good run. A live query is no native, so an object showing table rows live swaps on reload instead of restarting, as reading them in `self.open` made it. Each touching commit runs the query once (100 rows: about 60 µs, 80 µs watched): not for tables written hundreds of times a second.
### Changed
diff --git a/packages/chord/docs/benchmarks.md b/packages/chord/docs/benchmarks.md
index 1a18a3bb2..4f5003322 100644
--- a/packages/chord/docs/benchmarks.md
+++ b/packages/chord/docs/benchmarks.md
@@ -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.
diff --git a/packages/chord/docs/decisions.md b/packages/chord/docs/decisions.md
index f8e5dabb7..919814eb6 100644
--- a/packages/chord/docs/decisions.md
+++ b/packages/chord/docs/decisions.md
@@ -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
diff --git a/packages/chord/docs/design.md b/packages/chord/docs/design.md
index e327e2ff0..254784587 100644
--- a/packages/chord/docs/design.md
+++ b/packages/chord/docs/design.md
@@ -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
diff --git a/packages/chord/docs/guide-plugins-hosts/book.md b/packages/chord/docs/guide-plugins-hosts/book.md
index 1a6802c0a..10c73df42 100644
--- a/packages/chord/docs/guide-plugins-hosts/book.md
+++ b/packages/chord/docs/guide-plugins-hosts/book.md
@@ -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
diff --git a/packages/chord/docs/guide-plugins-hosts/index.html b/packages/chord/docs/guide-plugins-hosts/index.html
index 349975217..a69f6570c 100644
--- a/packages/chord/docs/guide-plugins-hosts/index.html
+++ b/packages/chord/docs/guide-plugins-hosts/index.html
@@ -169,7 +169,7 @@ addressable hosts; an address (Session({ id: "p1&quo
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.
Persisted data lives only in local 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.
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:
+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:
const count = self.derive([doc], ([notes]) => notes.items.length); // 0, 1, 2, … as notes are added
@@ -277,7 +286,8 @@ self.onLive(async (ctx) =>
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.
+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 withself.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
caller cancels or its object ends. A change still waiting when its context ends is cancelled before it commits:
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.
@@ -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.
-A host kind, a remote contract, a worker facet with a table and a live window, and the principal that adds a todo.
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 keeps its todos.
@@ -425,7 +425,7 @@ worker's store: /** 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;A table holds many records with queries and pages; it lives in an object, never in a facet. Persisted data lives only
in local 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:
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.
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:
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:
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;
A todo without text is refused with The watcher marked invalid("args", …), from the API: the error crosses every link as it is, so a
caller on a page or in a terminal gets { code: "invalid", reason: "args" }, as for arguments that fail the
@@ -959,7 +956,7 @@ console.log(" ada adds ' tea ':
console.log(" ada adds ' ':", refused);
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' }@todo/lists dirty when the file changed; applyChanges 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
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:
+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.
KeyOf in src/store/table.ts still tests TypeBox's
"~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.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,
+tx.update.
+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).
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).
+
invalid (Chord
host.serve(Type, factory); factory self: table, writable, open, derive, tx, get, contributehost.serve(Type, factory); factory self: table, query, derive, tx, get, contributeextends RoutineDeclarations
{ /** This object's documents of `definition`, in memory for its life (across its attempts). */ @@ -146,6 +148,18 @@ export interface StoreSelf
extends Self
{
state