diff --git a/packages/chord/docs/next/design.md b/packages/chord/docs/next/design.md index de9324a66..a474aa829 100644 --- a/packages/chord/docs/next/design.md +++ b/packages/chord/docs/next/design.md @@ -1497,7 +1497,7 @@ export default defineFacet((host, self) => { // `self` (contributions) a export const Http = defineRegistry("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 diff --git a/packages/chord/docs/next/slice-8.md b/packages/chord/docs/next/slice-8.md index bc99fa89b..090ee3bbf 100644 --- a/packages/chord/docs/next/slice-8.md +++ b/packages/chord/docs/next/slice-8.md @@ -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` (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` (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 diff --git a/packages/chord/docs/next/slices.md b/packages/chord/docs/next/slices.md index d0498da76..c2498ecba 100644 --- a/packages/chord/docs/next/slices.md +++ b/packages/chord/docs/next/slices.md @@ -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