docs(chord): slice 8 review 2 fixes; design §17 and slices folded

This commit is contained in:
Mario Zechner 2026-10-28 17:51:28 +00:00
parent 02a2b2532d
commit eb9d443462
3 changed files with 24 additions and 19 deletions

View file

@ -1497,7 +1497,7 @@ export default defineFacet((host, self) => { // `self` (contributions) a
export const Http = defineRegistry<HttpContribution, Router>("chord.http", { version: 2 }); // shared: identity only
host.serve(Http, { // the defining plugin's facet: the one provider (slice 8 D1)
compose: (table, { report }) => buildRouter(table, report), // pure; always delivers a value; reports conflicts
accepts: { 1: (old: HttpContributionV1): HttpContribution => upgrade(old) },
accepts: { 1: { contribution: (old: HttpContributionV1) => upgrade(old), value: (router) => routerV1(router) } },
});
self.contribute(Http, { routes: { … }, wraps: [ … ] });
const router = self.registry(Http).value; // live (host.registry(Http) outside objects)
@ -1535,6 +1535,12 @@ const router = self.registry(Http).value; // live (host.registry(Http) outsi
use site catches synchronous throws and rejections when it calls an entry, reports naming the contributor (the
composed table keeps each entry's plugin name), and continues with the other entries. Durable already does this for
extensions (`extension_error`; a throwing tool becomes an error result); `compose` reports through `report`.
- **Slice 8's rules** (decisions "Slice 8 D1" to "Slice 8 S3", `slice-8.md`): the token is shared, the provider serves it
(`host.serve(R, { compose, accepts })`); `self.registry(R)` in a factory is a declared dependency (activation waits
for a value; with none after `load`, the reader fails `transient` and is retried when one arrives); contributions
never fail a registry (a failed one is left out and reported); a throwing `compose` keeps the last good composition;
older readers get the value mapped back (`accepts[old].value`); `retryOn` retries on value calls only; `host.root(fn,
context, { principal? })` seals its context at settle, abort and `host.close`.
- **Registries may carry natives.** An entry lives exactly as long as its contributor; consumers look up at each use
or follow the composed value and keep nothing from an entry beyond a use.
- **Contributions that change (owner, 2026-10-19; W4):** registries are host-local, so contributions are usually

View file

@ -62,8 +62,8 @@ await host.root(async (ctx) => (await ctx.get(Notes, { session })).add(note, ctx
order); a contribution carries its token's version: an older one is adapted once with the declaration's `accepts`
when the table is built, an older one without an adapter or a newer one is left out and reported (`HostError`
`contribution_refused { registry, plugin, version }`).
- **Contributions:** a plain value, or a Chord watchable (a `writable`, a derive, a document member, another
registry's value), followed as a dependent of the table (its change recomposes); a foreign watchable (a Durable watch)
- **Contributions:** a plain value, or a Chord watchable (a `writable`, a derive, a document member; another
registry's value too, a cycle of registries reading each other refused when the second one serves or contributes), followed as a dependent of the table (its change recomposes); a foreign watchable (a Durable watch)
is refused at `contribute` (declaration policy). The table's entry is `{ plugin, type?, version, value }` (`type` for
an object contributor; served types carry their plugin's name).
- **`compose(table, { report })`:** pure, lazy as a derive: the registry's value is marked when its table changes (a
@ -83,9 +83,11 @@ await host.root(async (ctx) => (await ctx.get(Notes, { session })).add(note, ctx
table; its serve composes it.
- **Reading is a dependency (S3, owner, 2026-10-27: "isn't self.registry meant to wait like self.get?"):**
`self.registry(R)` in a factory declares the registry as a dependency of the attempt, as `self.get`: activation waits
until it has a value (bounded by `readyTimeout`); without one in time the object fails `transient` with
`unavailable/undeclared` as the dependency's cause and is retried the moment the registry gets its first value
(R18). It returns a `RegistryValue<V>` (a native source: a derive source, an `on` source, a `retryOn` source; never a
until it has a value; once `load`'s setups ran, a registry with no value cannot get one before a reload (a provider
comes only with a plugin), so a reader then fails at once `transient` with `unavailable/undeclared` as the
dependency's cause (as `self.get` of a type not served here fails at once, and the start barrier waits for no
contribution) and is retried the moment the registry gets its first value (R18); a first `compose` that throws fails
its readers the same way. It returns a `RegistryValue<V>` (a native source: a derive source, an `on` source, a `retryOn` source; never a
state member); methods and routines read `.value` at use. Once the object is live, a later failure of the registry
(a throwing `compose`) changes nothing for it: `.value` keeps the last good composition. Code outside objects:
`await host.registry(R, ctx)` waits the same way.
@ -145,7 +147,8 @@ after `host.close` rejects `released`. No span of its own until slice 9.
- Durable's `HarnessOptions.registry` takes a `RegistryReader` (`snapshot()`, `subscribe(listener)`, the listener
called synchronously after every publication). A `RegistryValue` offers a synchronous observer of its compositions
(`observe(observer)`, as Durable's `observeXXX`: `advance` inside the flush that composed, record only); Durable's
(`observe(observer, ctx)`, as Durable's `observeXXX`: `advance` inside the flush that composed, record only; an
attached observer counts as watching, so the lazy compose runs for it); Durable's
`registryReader(value)` (in its `src`) reads `.value` in `snapshot()` and forwards `observe` to `subscribe`. The
harness object declares the extensions registry with `self.registry(...)`, so it activates (and opens Durable) only
once the registry has a value. p3's `compose` builds Durable's snapshot without throwing (a duplicate task name is
@ -155,15 +158,17 @@ after `host.close` rejects `released`. No span of its own until slice 9.
```ts
const harness = self.registry(HarnessRegistry); // a dependency: activation waits for it
self.retryOn(harness); // the new harness's contribution wakes the failed follower
const view = self.follow((observer, ctx) => harness.value.observeConversation(id, observer, ctx));
```
When that harness closes, Durable ends the observer `failed` (S1, a Durable change in this slice: a Session close
ends observers `failed`, `ended` only when the observed owner ended), so the follower fails `transient` and its retry
follows the new harness.
- Example `29-registries.ts` (a router registry with two contributing plugins, a conflict reported, a `writable`
contribution, a missing declaration healing); example `30-host-root.ts`; example `31-harness-registry.ts` (a
Durable harness on the store, its extensions from a registry, its views followed by a second object).
- Examples: Chord's `29-registries.ts` (a router registry with two contributing plugins, a conflict reported, a
`writable` contribution, a missing provider healing) and `30-host-root.ts`; Durable's `test/examples/37-harness-
registry.ts` (a Durable harness on the store, its extensions from a registry, its views followed by a second object)
and the `registryReader` tests in Durable's tree.
## 6. Carry-over

View file

@ -49,7 +49,7 @@ Order (owner, 2026-10-22: routines and jobs before the store and reload).
| 6a | **The store** | the store (backends, tables, queries, writes, commit queue, outbox, documents engine, migrations, export), persisted documents, `self.table`, keyed watches and retirement |
| 6b | **Durable and jobs on the store** | persisted jobs, Durable's `StoreStorage`, the Chord source over Durable's views (`self.follow`) |
| 7 | **Reload and rollout** | plugin loading, staging, swaps and restarts, revisions, `accepts` in use |
| 8 | **Registries and `host.root`** | registries, `self.writable`, `retryOn`, a `RegistryReader` for Durable, `host.root` and `chord(fn, context)` (the harness object is p3's base code) |
| 8 | **Registries and `host.root`** | registries, `self.writable`, `retryOn`, a `RegistryReader` for Durable, `host.root` (`chord(fn, context)` and the harness object are p3's base code) |
| 9 | **Telemetry** | spans, events, metrics across hosts |
The playground (`apps/chord-playground`) moves to the new code once slice 4 exists. Round-3 cuts (C3-C16) and
@ -227,14 +227,8 @@ Specified in `slice-6b.md` (decided 2026-10-27, D1-D9, R1-R4, "owner names and c
- **Durable's views from other objects** (from slice 6b): the harness carried in a registry entry, so an object other
than the one that opened Durable can `self.follow` its views.
- **`host.root(fn, context, { principal? })`**: a call scope for code outside objects, built with its first users
(Durable hooks, `chord(fn, context)`); decide what an aborted, uncooperative `fn` means for its references (match
the handler and `CallContext` rules). (Slice 3b, owner.)
- **`retryOn`** retries a broken object as `ref.retry()` does. (Slice 2.)
- **`host.activity` on the facet's host** (`FacetHost`), if the harness or a core facet needs it; today only
`references` is there. (Slice 4b.)
- Registries recompose inside the owner's flush, never inside another owner's commit step; changing contributions are
native watchables (`self.writable`). (W4.)
- Specified in `slice-8.md` (decided 2026-10-27): `host.root` (D3), `retryOn` (D2), registries recomposing in the
owner's flush (W4); `host.activity` on the facet's host dropped (nothing needs it).
### Slice 9: telemetry