@earendil-works/chord 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +69 -14
  3. package/dist/cell.d.ts +30 -9
  4. package/dist/cell.d.ts.map +1 -1
  5. package/dist/cell.js +52 -12
  6. package/dist/cell.js.map +1 -1
  7. package/dist/cluster.d.ts +4 -3
  8. package/dist/cluster.d.ts.map +1 -1
  9. package/dist/cluster.js +58 -42
  10. package/dist/cluster.js.map +1 -1
  11. package/dist/connection.d.ts +171 -58
  12. package/dist/connection.d.ts.map +1 -1
  13. package/dist/connection.js +676 -217
  14. package/dist/connection.js.map +1 -1
  15. package/dist/context/index.d.ts +8 -0
  16. package/dist/context/index.d.ts.map +1 -1
  17. package/dist/context/index.js +15 -0
  18. package/dist/context/index.js.map +1 -1
  19. package/dist/contract.d.ts +120 -45
  20. package/dist/contract.d.ts.map +1 -1
  21. package/dist/contract.js +104 -15
  22. package/dist/contract.js.map +1 -1
  23. package/dist/declare.d.ts +11 -48
  24. package/dist/declare.d.ts.map +1 -1
  25. package/dist/declare.js +1 -21
  26. package/dist/declare.js.map +1 -1
  27. package/dist/delta/index.d.ts +2 -1
  28. package/dist/delta/index.d.ts.map +1 -1
  29. package/dist/delta/index.js +4 -0
  30. package/dist/delta/index.js.map +1 -1
  31. package/dist/errors.d.ts +4 -3
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +15 -0
  34. package/dist/errors.js.map +1 -1
  35. package/dist/host.d.ts +214 -120
  36. package/dist/host.d.ts.map +1 -1
  37. package/dist/host.js +446 -241
  38. package/dist/host.js.map +1 -1
  39. package/dist/index.d.ts +8 -8
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +4 -4
  42. package/dist/index.js.map +1 -1
  43. package/dist/link.d.ts +47 -12
  44. package/dist/link.d.ts.map +1 -1
  45. package/dist/link.js +200 -14
  46. package/dist/link.js.map +1 -1
  47. package/dist/loader.d.ts.map +1 -1
  48. package/dist/loader.js +7 -2
  49. package/dist/loader.js.map +1 -1
  50. package/dist/manager.d.ts.map +1 -1
  51. package/dist/manager.js +35 -9
  52. package/dist/manager.js.map +1 -1
  53. package/dist/object.d.ts +438 -0
  54. package/dist/object.d.ts.map +1 -0
  55. package/dist/{record.js → object.js} +614 -335
  56. package/dist/object.js.map +1 -0
  57. package/dist/output.d.ts +91 -0
  58. package/dist/output.d.ts.map +1 -0
  59. package/dist/output.js +360 -0
  60. package/dist/output.js.map +1 -0
  61. package/dist/registry.d.ts +3 -3
  62. package/dist/registry.d.ts.map +1 -1
  63. package/dist/registry.js +1 -1
  64. package/dist/registry.js.map +1 -1
  65. package/dist/reload.d.ts +35 -27
  66. package/dist/reload.d.ts.map +1 -1
  67. package/dist/reload.js +256 -192
  68. package/dist/reload.js.map +1 -1
  69. package/dist/remote.d.ts +130 -35
  70. package/dist/remote.d.ts.map +1 -1
  71. package/dist/remote.js +493 -189
  72. package/dist/remote.js.map +1 -1
  73. package/dist/sqlite/cloudflare.d.ts +52 -0
  74. package/dist/sqlite/cloudflare.d.ts.map +1 -0
  75. package/dist/sqlite/cloudflare.js +134 -0
  76. package/dist/sqlite/cloudflare.js.map +1 -0
  77. package/dist/sqlite/database.d.ts +37 -0
  78. package/dist/sqlite/database.d.ts.map +1 -0
  79. package/dist/sqlite/database.js +2 -0
  80. package/dist/sqlite/database.js.map +1 -0
  81. package/dist/sqlite/index.d.ts +4 -0
  82. package/dist/sqlite/index.d.ts.map +1 -0
  83. package/dist/sqlite/index.js +3 -0
  84. package/dist/sqlite/index.js.map +1 -0
  85. package/dist/sqlite/migrations.d.ts +10 -0
  86. package/dist/sqlite/migrations.d.ts.map +1 -0
  87. package/dist/sqlite/migrations.js +33 -0
  88. package/dist/sqlite/migrations.js.map +1 -0
  89. package/dist/sqlite/node.d.ts +38 -0
  90. package/dist/sqlite/node.d.ts.map +1 -0
  91. package/dist/sqlite/node.js +174 -0
  92. package/dist/sqlite/node.js.map +1 -0
  93. package/dist/sqlite/schema.d.ts +3 -0
  94. package/dist/sqlite/schema.d.ts.map +1 -0
  95. package/dist/sqlite/schema.js +10 -0
  96. package/dist/sqlite/schema.js.map +1 -0
  97. package/dist/telemetry-sqlite/index.d.ts +24 -0
  98. package/dist/telemetry-sqlite/index.d.ts.map +1 -0
  99. package/dist/telemetry-sqlite/index.js +273 -0
  100. package/dist/telemetry-sqlite/index.js.map +1 -0
  101. package/dist/telemetry-sqlite/store.d.ts +9 -0
  102. package/dist/telemetry-sqlite/store.d.ts.map +1 -0
  103. package/dist/telemetry-sqlite/store.js +110 -0
  104. package/dist/telemetry-sqlite/store.js.map +1 -0
  105. package/dist/types.d.ts +39 -28
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js.map +1 -1
  108. package/dist/websocket.d.ts +4 -5
  109. package/dist/websocket.d.ts.map +1 -1
  110. package/dist/websocket.js +3 -2
  111. package/dist/websocket.js.map +1 -1
  112. package/dist/wire.d.ts +210 -129
  113. package/dist/wire.d.ts.map +1 -1
  114. package/dist/wire.js +108 -55
  115. package/dist/wire.js.map +1 -1
  116. package/docs/guide.md +267 -203
  117. package/docs/spec.md +141 -114
  118. package/docs/tutorial.md +257 -235
  119. package/package.json +27 -1
  120. package/dist/record.d.ts +0 -320
  121. package/dist/record.d.ts.map +0 -1
  122. package/dist/record.js.map +0 -1
package/docs/guide.md CHANGED
@@ -4,8 +4,11 @@
4
4
 
5
5
  Chord builds applications out of **objects** that live in several processes (a server, workers, browser tabs) and can be **hot reloaded** while they run.
6
6
 
7
- - An **object** is state plus code at an address. It is created on demand and lives while someone holds it.
8
- - Everyone gets a **handle**, never the object. Handles work the same locally and across processes, and survive reloads.
7
+ - An **object type** is declared by a contract: a name, an input (the object's address) and members. A plugin's
8
+ **facet** (its code for one host: server, worker, browser) serves object types.
9
+ - An **object** is one instance of an object type: state plus code at an address. It is created on demand and lives
10
+ while someone references it.
11
+ - Everyone gets a **reference**, never the object. References work the same locally and across processes, and survive reloads.
9
12
  - Every object knows its **principal**: an owner and a guest get different objects, with different methods.
10
13
  - **State** is observable JSON. Remote viewers get a snapshot followed by deltas.
11
14
  - **Hot reload** swaps code while it runs. Work in flight finishes on the code it started with, state is kept, and incompatible changes migrate at a safe point.
@@ -18,7 +21,7 @@ Chord builds applications out of **objects** that live in several processes (a s
18
21
  - [Declarations](#declarations)
19
22
  - [Activation](#activation)
20
23
  - [Principals and variants](#principals-and-variants)
21
- - [Handles](#handles)
24
+ - [References](#references)
22
25
  - [State](#state)
23
26
  - [Calls, ctx and roots](#calls-ctx-and-roots)
24
27
  - [Lifetime](#lifetime)
@@ -34,7 +37,7 @@ Chord builds applications out of **objects** that live in several processes (a s
34
37
 
35
38
  ```ts
36
39
  import { Type } from "typebox";
37
- import { cell, createHost, method, remote } from "@earendil-works/chord";
40
+ import { createHost, method, remote } from "@earendil-works/chord";
38
41
 
39
42
  // contract.ts: shared by provider and consumers
40
43
  export const Counter = remote("demo.counter", {
@@ -43,27 +46,26 @@ export const Counter = remote("demo.counter", {
43
46
  variants: {
44
47
  default: {
45
48
  state: { count: Type.Number() },
46
- methods: { increment: method(Type.Object({ by: Type.Number() }), Type.Null()) },
49
+ methods: { increment: method(Type.Object({ by: Type.Number() })) },
47
50
  },
48
51
  },
49
52
  });
50
53
 
51
- const host = createHost({ name: "server", principalKey: (p) => p.kind });
54
+ const host = createHost({ name: "server", self: { kind: "server" } });
52
55
 
53
56
  host.serve(Counter, ({ name }, _principal, self) => {
54
- const state = self.state({ count: cell(Type.Number(), 0) });
57
+ const count = self.state("count", Type.Number(), 0);
55
58
  return {
56
- kind: "default",
57
- count: state.count,
59
+ variant: "default",
60
+ count,
58
61
  increment: async ({ by }, ctx) => {
59
- state.count.change(ctx, (n) => n + by);
60
- return null;
62
+ count.change(ctx, (n) => n + by);
61
63
  },
62
64
  };
63
65
  });
64
66
 
65
67
  await host.root(async (ctx) => {
66
- const clicks = ctx.get(Counter, { name: "clicks" }); // a handle; held until this root ends
68
+ const clicks = await ctx.get(Counter, { name: "clicks" }); // ready, with its state; released when this root ends
67
69
  clicks.count.subscribe((n) => console.log("count", n));
68
70
  await clicks.increment({ by: 2 }, ctx); // logs 0, then 2
69
71
  });
@@ -81,6 +83,7 @@ Remote types can be reached from other hosts. Everything that crosses the wire h
81
83
  export const Chat = remote("pi.chat", {
82
84
  version: 1,
83
85
  input: Type.Object({ session: Type.String(), conversation: Type.Number() }),
86
+ host: hostFamily("session:", "session"), // lives on `session:S1` for `{ session: "S1", … }`
84
87
  variants: {
85
88
  viewer: {
86
89
  state: { view: ConversationViewSchema },
@@ -89,13 +92,26 @@ export const Chat = remote("pi.chat", {
89
92
  state: { view: ConversationViewSchema },
90
93
  methods: {
91
94
  send: method(SendInputSchema, Type.Object({ submissionId: Type.Number() })),
92
- stop: method(Type.Null(), Type.Null()),
95
+ stop: method(), // no arguments, no result: `chat.stop(ctx)`
93
96
  },
94
97
  },
95
98
  },
96
99
  });
97
100
  ```
98
101
 
102
+ **Where a remote type lives** is part of its contract, so no caller names a host:
103
+
104
+ - `host: hostNamed("server")`: every object lives on the host named `server`.
105
+ - `host: hostFamily("session:", "session")`: an object lives on `session:` + its input's `session` field, a required
106
+ string. TypeScript checks the field name against the input.
107
+
108
+ A `get` normalizes the input (defaults count), resolves the host, and acquires the object here if that is this host,
109
+ otherwise over the link to it. `serve` refuses a type whose placement does not include the serving host, so a misplaced
110
+ type fails at startup or at the reload that adds it. The host that gets a hold checks that its own contract places the
111
+ input on itself, and a relay checks the hold's target against its contract token: a client never chooses where an
112
+ object lives. The placement is part of the description and the fingerprint: moving a type is a breaking change, and
113
+ references to it get `upgrade_required`.
114
+
99
115
  ### Local types
100
116
 
101
117
  Local types are reachable only inside their host. Their members may be anything: functions, callbacks, native objects. Non-JSON members get opaque or function descriptors.
@@ -147,66 +163,60 @@ host.serve(Index, indexFactory, {
147
163
  ## Serving objects
148
164
 
149
165
  ```ts
150
- host.serve(Type, (input, principal, self) => behavior, options);
166
+ host.serve(Type, (input, principal, self) => implementation, options);
151
167
  ```
152
168
 
153
- - The **factory** runs synchronously. It declares what the object needs and returns its behavior: methods and state members, plus `kind` for remote types. Returning a promise throws.
154
- - There is one **record** per `(type, input, principal)`. It holds the object's state, resources and holds, and survives reloads. Only the behavior is replaced.
169
+ - The **factory** runs synchronously. It declares what the object needs and returns its implementation: methods and state members, plus `variant` for remote types. Returning a promise throws.
170
+ - There is one **object** per `(type, input, principal)`. It holds the object's state, resources and references, and survives reloads. Only the implementation is replaced.
155
171
  - **Options:**
156
- - `state: { version, migrate }`: the state version and its migrations (see [Hot reload](#hot-reload));
157
172
  - `accepts`: adapters for consumers built against older contract versions;
158
- - `linger`: how long to keep the object after the last hold is released;
159
- - `eager`: activate when the plugin loads, and keep it for the host's lifetime.
173
+ - `lifetime`: `"referenced"` (default) creates the object at its first `get` and disposes it after the last reference
174
+ is released; `"host"` creates it when the plugin loads and keeps it until the host closes (see [Lifetime](#lifetime));
175
+ - `linger`: how long to keep a referenced object after the last reference is released.
160
176
 
161
177
  ## Declarations
162
178
 
163
- A reload runs the factory again over the same record. Declarations are matched by **property name**, so the new code's `status` cell is the old code's `status` cell, with its value.
179
+ A reload runs the factory again over the same object. Declarations that keep something across a reload have a **name**: the new code's `status` cell is the old code's `status` cell, with its value. Derivations and tasks hold nothing across a reload and have no name.
164
180
 
165
181
  ```ts
166
182
  host.serve(StatusStore, ({ session }, _host, self) => {
167
- const state = self.state({
168
- status: cell(StatusSchema, { running: false, tasks: 0 }),
169
- errors: cell(Type.Array(Type.String()), []),
170
- });
183
+ const status = self.state("status", StatusSchema, { running: false, tasks: 0 });
184
+ const errors = self.state("errors", Type.Array(Type.String()), []);
171
185
  const catalog = self.get(Catalog, {});
172
- const res = self.resources({
173
- db: resource({
174
- rev: 1,
175
- open: async ({ signal }) => openSqlite(statusPath(session), { signal }),
176
- close: async (db) => db.close(),
177
- }),
178
- });
179
- self.listen({
180
- catalogChanges: listener(catalog.rows, (rows, ctx) => state.errors.replace(ctx, checkRows(rows))),
181
- });
182
- self.tasks({
183
- flush: async (ctx) => {
184
- while (!ctx.signal.aborted) {
185
- await sleep(10_000, ctx.signal);
186
- await res.db.value.run("INSERT INTO history VALUES (?)", [JSON.stringify(state.status.value)]);
187
- }
188
- },
186
+ const db = self.resource("db", {
187
+ version: 1,
188
+ open: async ({ signal }) => openSqlite(statusPath(session), { signal }),
189
+ close: async (db) => db.close(),
189
190
  });
190
- const derived = self.derive({
191
- summary: derivation([state.status, state.errors], (s, e) => ({ ...s, failing: e.length > 0 })),
191
+ self.on("catalogChanges", catalog.rows, (rows, ctx) => errors.replace(ctx, checkRows(rows)));
192
+ self.task(async (ctx) => {
193
+ while (!ctx.signal.aborted) {
194
+ await sleep(10_000, ctx.signal);
195
+ await db.value.run("INSERT INTO history VALUES (?)", [JSON.stringify(status.value)]);
196
+ }
192
197
  });
193
- return { summary: derived.summary, report: reportMethod(state) };
198
+ const summary = self.derive([status, errors], (s, e) => ({ ...s, failing: e.length > 0 }));
199
+ return { summary, report: reportMethod(status) }; // exposed state is what the implementation returns
194
200
  });
195
201
  ```
196
202
 
197
203
  | Declaration | What it is | On reload |
198
204
  | --- | --- | --- |
199
- | `self.state({ name: cell(schema, initial) })` | an observable JSON cell; the schema is required | same cell and value; migrated when the state version is bumped |
200
- | `self.get(Type, input, { at? })` | a hold on another object; its identity is the address | kept if declared again, otherwise released |
201
- | `self.resources({ name: resource({ rev, deps?, exclusive?, open, close }) })` | a connection, watch, socket, port | kept; reopened only if `rev` or `deps` change |
202
- | `self.listen({ name: listener(source, fn) })` or `listener(source, select, fn)` | reacts to a state or resource event | the same subscription, with the new `fn`, as long as the source (resource) and `select` are unchanged |
203
- | `self.tasks({ name: async (ctx) => ... })` | long-running background work | the old run is aborted through `ctx.signal`, and the new code starts |
204
- | `self.derive({ name: derivation(sources, fn) })` | live projection of state; `fn` is synchronous | kept; `fn` replaced |
205
+ | `self.state(name, schema, initial \| { init }, { version?, migrate? })` | an observable JSON cell; the schema is required | same cell and value (`init` does not run); migrated when its version rises |
206
+ | `self.get(Type, input, { at? })` | a reference to another object; its identity is the address | kept if declared again, otherwise released |
207
+ | `self.resource(name, { version, deps?, exclusive?, open, close? })` | a connection, watch, socket, port | kept; reopened only if `version`, `deps` or `exclusive` change |
208
+ | `self.on(name, source, handler)` or `self.on(name, source, select, handler)` | reacts to state or to an event source | the same subscription, with the new handler, as long as the source and `select` are unchanged |
209
+ | `self.task(async (ctx) => ...)` | long-running background work | the old run is aborted through `ctx.signal`; the new code starts once it finished (at most `taskStopDeadline`, default 5 s) |
210
+ | `self.derive(sources, fn)` | live projection of state; `fn` is synchronous | a new derivation, computed when the reload commits |
205
211
  | `self.register(Registry, { key: value })` | an entry in a registry | replaced at commit |
206
- | `self.expose(res.name)` | a member backed by a resource (local types) | follows the resource |
212
+ | `self.expose(resource)` | a member backed by a resource (local types) | follows the resource |
213
+
214
+ Names are strings: declaring one twice in a run fails (`invalid/duplicate`). Each kind has its own names, so a cell and a resource may share one.
207
215
 
208
216
  A listener keeps its subscription across reloads when it is declared on the same resource (or source) with the same `select`: the same function, or one with the same source text. `select` is not called again, so a source that accepts a single subscriber (a Durable watch) survives reloads, and `select` may wrap the value in a new adapter object. A `select` whose result depends on captured values that change between reloads is not noticed; give such a listener a new name.
209
217
 
218
+ **A listener on state reconciles.** A listener on state (a cell, a derivation, a reference's state member) gets the current value when it starts, then every new value. When a reload replaces its source (a derivation is new at every reload), it subscribes again and gets the current value again. Write its handler to bring things in line with the value it gets (set, compare, start or stop what the value says), not to count or append per call. An event source (a resource's events) delivers no current value.
219
+
210
220
  A name you no longer declare is released, closed or stopped, after the old code has finished what it was doing.
211
221
 
212
222
  **State that must survive a reload lives in cells,** never in closure variables. A closure variable lasts as long as one version of the code.
@@ -215,21 +225,23 @@ A name you no longer declare is released, closed or stopped, after the old code
215
225
  // wrong: reset on every reload
216
226
  host.serve(Visits, (_, _p, self) => {
217
227
  let visits = 0;
218
- return { hit: async (_, ctx) => ++visits };
228
+ return { hit: async (ctx) => ++visits };
219
229
  });
220
230
 
221
231
  // right
222
232
  host.serve(Visits, (_, _p, self) => {
223
- const state = self.state({ visits: cell(Type.Number(), 0) });
233
+ const visits = self.state("visits", Type.Number(), 0);
224
234
  return {
225
- hit: async (_, ctx) => {
226
- state.visits.change(ctx, (n) => n + 1);
227
- return state.visits.value;
235
+ hit: async (ctx) => {
236
+ visits.change(ctx, (n) => n + 1);
237
+ return visits.value;
228
238
  },
229
239
  };
230
240
  });
231
241
  ```
232
242
 
243
+ **Methods** are declared with `method(args?, result?)` in the contract. Without `args` a method takes none: callers write `ref.hit(ctx)` (or `ref.hit()`), and its implementation is `(ctx) => …`. Without `result` it returns nothing: the implementation returns nothing, and callers get `undefined`. `method(undefined, Type.Number())` takes no arguments and returns a number.
244
+
233
245
  ## Activation
234
246
 
235
247
  The factory is synchronous, but activation isn't. `self.get` and `self.resources` only *declare*; the slow work happens afterwards:
@@ -238,24 +250,23 @@ The factory is synchronous, but activation isn't. `self.get` and `self.resources
238
250
  get(Index, { session: "S1" })
239
251
  1. run the factory → declarations
240
252
  2. open declared resources → e.g. connect to SQLite
241
- 3. wait for declared holds → dependencies activate first
242
- 4. live → handles become "live"
253
+ 3. wait for declared refs → dependencies activate first
254
+ 4. run the cells' init → e.g. read the rows into a cell
255
+ 5. live → refs become "live"; listeners and tasks start
243
256
  ```
244
257
 
245
258
  ```ts
246
259
  host.serve(Index, ({ session }, _host, self) => {
247
- const res = self.resources({
248
- db: resource({ rev: 1, open: () => openSqlite(indexPath(session)), close: (db) => db.close() }),
249
- });
260
+ const db = self.resource("db", { version: 1, open: () => openSqlite(indexPath(session)), close: (db) => db.close() });
250
261
  return {
251
- search: async ({ query }, ctx) => res.db.value.all("SELECT entryId, text FROM fts WHERE fts MATCH ?", [query]),
262
+ search: async ({ query }, ctx) => db.value.all("SELECT entryId, text FROM fts WHERE fts MATCH ?", [query]),
252
263
  };
253
264
  });
254
265
 
255
266
  host.serve(Chat, ({ session, conversation }, principal, self) => {
256
267
  const index = self.get(Index, { session }); // a placeholder until Index is live
257
268
  return {
258
- kind: "participant",
269
+ variant: "participant",
259
270
  find: async ({ query }, ctx) => index.search({ query }, ctx), // only ever runs once Chat (and Index) are live
260
271
  // ...
261
272
  };
@@ -263,79 +274,92 @@ host.serve(Chat, ({ session, conversation }, principal, self) => {
263
274
  ```
264
275
 
265
276
  **Rules:**
266
- - Inside the factory, never use what you declared: `res.db.value` throws `not_ready`, and calling a handle throws. Use them in methods, listeners and tasks.
277
+ - Inside the factory, never use what you declared: `db.value` and `index.someState.value` throw `not_ready`, and calling a reference throws. Use them in methods, listeners, tasks and `init`.
278
+ - **The object goes live only with its references ready**: live, and (for a remote reference) every state member of its variant hydrated. The same holds for state it forwards or derives from a remote reference it holds some other way: it goes live once that state has its value. A reference that is not ready after the host's `readyTimeout` (default 60 s) fails the activation with `unavailable/ready_timeout`; a reference cycle across hosts ends that way instead of hanging.
267
279
  - **Concurrent `get`s** of one address share one activation, so the database opens once.
268
280
  - **Failure propagates:** if `openSqlite` throws, `Index` fails, `Chat` fails with that cause, and every waiting `get` rejects. The next `get` tries again.
269
281
  - **Abandoned activations:** if everyone releases before activation finishes, the result is disposed.
270
282
 
271
283
  ### Startup readiness and parent admission
272
284
 
273
- `await host.load(facets)` and `await host.install(facet)` wait for the eager objects they install to become live:
274
- resources are open, declared holds are live, and registrations are published. All facets in a load are installed
275
- before activation is awaited, so an eager consumer can use a provider in a later facet. Ordinary lazy objects stay
276
- lazy unless an eager object holds them. Factory, dependency and resource errors reject startup with the handle's
277
- activation error; eager failures also reach `onError`. Installation is not rolled back on failure: close the host
285
+ `await host.load(facets)` and `await host.install(facet)` wait for the `lifetime: "host"` objects they install to
286
+ become live: resources are open, declared references are live, and registrations are published. All facets in a load
287
+ are installed before activation is awaited, so a host object can use a provider in a later facet. Referenced objects
288
+ are created only when something references them. Factory, dependency and resource errors reject startup with the
289
+ reference's activation error; host object failures also reach `onError`. Installation is not rolled back on failure: close the host
278
290
  when abandoning startup. `install` registers synchronously, so setup/registration errors still throw synchronously.
279
291
 
280
- `await host.ready()` waits for the eager holds present when called, including direct `host.serve(..., { eager })`
281
- and previously deferred loads. It is an activation barrier, not a continuous health check, a wait for tasks to
282
- finish, or a remote-state hydration barrier. Later installations require another readiness wait.
292
+ `await host.ready()` waits for the host objects present when called, including direct
293
+ `host.serve(..., { lifetime: "host" })` and previously deferred loads. It is an activation barrier, not a continuous health check, a wait for tasks to
294
+ finish. Later installations require another readiness wait.
283
295
 
284
296
  A worker may need to open its local Harness and announce control readiness **before** the parent accepts its link.
285
- An eager reporter waiting for the server's SessionStatus cannot become live before that acceptance. Awaiting the
286
- whole load before announcing control readiness would deadlock. Defer the whole eager barrier explicitly:
297
+ A host object reporting to the server's SessionStatus cannot become live before that acceptance. Awaiting the
298
+ whole load before announcing control readiness would deadlock. Defer the whole barrier explicitly:
287
299
 
288
300
  ```ts
289
- // Connect the upstream link first; its outgoing holds can queue until the parent accepts it.
290
- await worker.load(workerFacets, { awaitEager: false }); // also supported by install
291
- const harness = worker.get(Harness, {});
292
- await harness.ready; // local bootstrap only; no upstream dependency
301
+ // Connect the upstream link first; its outgoing references can queue until the parent accepts it.
302
+ await worker.load(workerFacets, { awaitHostObjects: false }); // also supported by install
303
+ const harness = await worker.get(Harness, {}); // local bootstrap only; no upstream dependency
293
304
  announceControlReady(); // parent can now accept the worker link
294
- await worker.ready(); // full eager readiness, including upstream holds
305
+ await worker.ready(); // every host object live, including upstream references
295
306
  harness.release();
296
307
  ```
297
308
 
298
- `awaitEager: false` still starts and host-holds every eager object; it skips waiting, not activation. There is no
299
- automatic classification of upstream dependencies. Choose a local bootstrap handle with no upstream dependency,
309
+ `awaitHostObjects: false` still starts every host object; it skips waiting, not activation. There is no
310
+ automatic classification of upstream dependencies. Choose a local bootstrap reference with no upstream dependency,
300
311
  connect before loading upstream consumers, and let the parent admit the link before the full barrier. Control
301
- readiness means admission is possible, not that every eager object is live. Keep worker bootstrap counted as busy
302
- until the full barrier settles, and report a later failure through the worker's fatal/startup-error path.
312
+ readiness means admission is possible, not that every host object is live. Keep worker bootstrap counted as busy
313
+ until the full barrier settles, and report a later failure through the worker's fatal/startup-error path. Never await a
314
+ reference whose object needs the parent (a remote one, or a local one that holds one) before the parent admits the link.
303
315
 
304
- To hand out a native object a resource opens (a local type's `opaque` member), expose the resource. Handles read its value once the object is live, and `undefined` before:
316
+ To hand out a native object a resource opens (a local type's `opaque` member), expose the resource. References read its value once the object is live, and `undefined` before:
305
317
 
306
318
  ```ts
307
319
  host.serve(Harness, (_, _host, self) => {
308
- const res = self.resources({ durable: resource({ rev: 1, open: () => DurableHarness.open(storage), close: (h) => h.close() }) });
309
- return { harness: self.expose(res.durable) }; // contract member: harness: opaque<DurableHarness>("pi.durable-harness@1")
320
+ const durable = self.resource("durable", { version: 1, open: () => DurableHarness.open(storage), close: (h) => h.close() });
321
+ return { harness: self.expose(durable) }; // contract member: harness: opaque<DurableHarness>("pi.durable-harness@1")
310
322
  });
311
323
 
312
- const harness = ctx.get(Harness, {});
313
- await harness.ready;
324
+ const harness = await ctx.get(Harness, {});
314
325
  harness.harness.resume(); // the DurableHarness itself
315
326
  ```
316
327
 
317
328
  ## Principals and variants
318
329
 
319
- The application defines its principals and how they're keyed:
330
+ The application defines its principals and how they're keyed. Chord invents none: the app gives every principal a
331
+ request can act as.
320
332
 
321
333
  ```ts
322
334
  type Principal =
323
- | { kind: "owner"; via: "device" | "tunnel" | "relay" }
335
+ | { kind: "owner"; access: "local" | "relay" }
324
336
  | { kind: "guest"; id: string; name: string; session: string; mode: "view" | "full"; revision: number }
325
- | { kind: "worker"; session: string; generation: number };
337
+ | { kind: "worker"; session: string; generation: number }
338
+ | { kind: "server" } // the parent's own requests, as a worker sees them
339
+ | { kind: "host" }; // a host's own code
326
340
 
327
- const host = createHost({
341
+ const host = createHost<Principal>({
328
342
  name: "server",
329
- principalKey: (p: Principal) =>
330
- p.kind === "owner" ? `owner@${p.via}` :
343
+ self: { kind: "host" },
344
+ principalKey: (p) =>
345
+ p.kind === "owner" ? `owner:${p.access}` :
331
346
  p.kind === "guest" ? `guest:${p.id}:r${p.revision}` :
332
- `worker:${p.session}:${p.generation}`,
347
+ p.kind === "worker" ? `worker:${p.session}:${p.generation}` :
348
+ p.kind,
333
349
  });
334
350
  ```
335
351
 
336
- - There is one record per principal key: two tabs of the owner share an object, and a guest gets its own.
337
- - Code inside a host acts as the built-in `host` principal, so internal objects are shared.
352
+ - `createHost<Principal>` types every factory's `principal`, `ctx.principal`, routes, `accept` and
353
+ `invalidatePrincipal` patterns, so code narrows on `kind` without casts. Facet modules say it too:
354
+ `defineFacet<Principal>((api) => …)` (an app usually exports its own `defineFacet` and `Ctx` aliases). Without a type
355
+ argument, principals are `unknown`.
356
+ - There is one object per principal key: two tabs of the owner share an object, and a guest gets its own.
357
+ - Code inside a host acts as `self`, so internal objects are shared. A principal with the same key as `self` is the
358
+ host's own, whichever object carries it.
338
359
  - A link stamps the principal of whatever connects to it. Callers can never supply their own.
360
+ - A child host names what its parent's own requests act as: `connect(link, { principal })` (or
361
+ `upstream: { link, principal }`). Requests the parent relays act as the caller it forwards.
362
+ - A route names what the hosts it resolves act as when they call back: `route(prefix, { …, principal: (name) => … })`.
339
363
 
340
364
  **A remote factory picks a variant:**
341
365
 
@@ -343,101 +367,126 @@ const host = createHost({
343
367
  host.serve(Chat, ({ session, conversation }, principal, self) => {
344
368
  if (!canView(principal, session)) throw forbidden();
345
369
  const c = self.get(Conversation, { session, conversation }); // as host: one shared instance
346
- if (!canSend(principal)) return { kind: "viewer", view: c.view };
370
+ if (!canSend(principal)) return { variant: "viewer", view: c.view };
347
371
  const author = authorOf(principal);
348
372
  return {
349
- kind: "participant",
373
+ variant: "participant",
350
374
  view: c.view,
351
375
  send: async (input, ctx) => c.submit(input, author, ctx),
352
- stop: async (_, ctx) => {
376
+ stop: async (ctx) => {
353
377
  await c.stop(author, ctx);
354
- return null;
355
378
  },
356
379
  };
357
380
  });
358
381
  ```
359
382
 
360
383
  ```ts
361
- const chat = ctx.get(Chat, { session: "S1", conversation: 1 }, { at: "session:S1" });
362
- await chat.ready;
363
- if (chat.kind === "participant") showComposer(chat.send);
384
+ const chat = await ctx.get(Chat, { session: "S1", conversation: 1 }); // on session:S1
385
+ if (chat.variant === "participant") showComposer(chat.send); // narrows: `send` exists only there
364
386
  else showReadOnly();
365
387
  ```
366
388
 
367
389
  **What a variant doesn't do:**
368
390
  - **It isn't complete authorization.** Methods still check their arguments: that a submission belongs to this conversation, or a session ID to this principal.
369
391
  - **It doesn't cover flags that change while you hold the object.** Those are state, for example `you: { canDraw: boolean }`.
370
- - **Revocation** is `host.invalidatePrincipal(match)`. It force-disposes matching records, here and over links, and their handles become `gone`. A new `get` runs the factory with the new grants.
392
+ - **Revocation** is `host.invalidatePrincipal(match)`. It force-disposes matching objects, here and over links, and their references become `gone`. A new `get` runs the factory with the new grants.
371
393
 
372
- Local types have no variants. Their only callers are inside the host, acting as `host`. If a local object needs to know who's acting, pass that explicitly, like `author` above.
394
+ Local types have no variants. Their only callers are inside the host, acting as its `self`. If a local object needs to know who's acting, pass that explicitly, like `author` above.
373
395
 
374
- ## Handles
396
+ ## References
397
+
398
+ | Where | API | Released |
399
+ | --- | --- | --- |
400
+ | a factory | `self.get(Type, input)`: synchronous; the object goes live after it is ready | with the object (or its replaced declaration) |
401
+ | a call | `await ctx.get(Type, input, { state? })` | when the call settles |
402
+ | outside Chord | `await host.get(Type, input, { signal?, state? })` | `ref.release()` |
403
+ | a UI runtime | `host.hold(Type, input)`: synchronous, possibly `loading` | `ref.release()` |
375
404
 
376
405
  ```ts
377
- const chat = ctx.get(Chat, input, { at: "session:S1" });
406
+ const chat = await ctx.get(Chat, input); // ready: live, every state member of its variant hydrated
378
407
  chat.status; // "loading" | "live" | "released" | "gone"
379
- await chat.ready; // live, or rejects with the activation error
380
- chat.kind; // the current variant
381
- chat.shape; // { kind, members, contract }
408
+ chat.variant; // the current variant; a multi-variant reference narrows on it
409
+ chat.shape; // { variant, members, contract }
382
410
  const send = chat.send; // a stable function: always dispatches to the current code
383
- chat.view.value; // undefined until hydrated (remote: while someone subscribes)
411
+ chat.view.value; // the current value, typed T
384
412
  chat.view.subscribe((v) => render(v));
385
413
  chat.onStatus((status, error) => { if (status === "gone") reconnect(error); });
386
414
  chat.release();
387
415
  ```
388
416
 
389
- - **Calls on a `loading` handle wait** until it's live, respecting `ctx` cancellation and a deadline.
417
+ - **A reference you hold is usable:** an awaited `get` resolves once the object is live and every state member of its variant has its first snapshot, local or remote. It rejects with the activation error (`forbidden`, unreachable, …), with `aborted` (a `ctx.get` whose ctx is aborted or whose call settles first; `host.get`'s `signal`), or with `unavailable/ready_timeout` after the host's `readyTimeout`. Only that reference is released then.
418
+ - **A held reference stays subscribed** to the state of its variant, with or without listeners, until it is released. After a link loss the last values stay readable while it re-holds (status `loading`).
419
+ - **Reading state before the first hydration throws `not_ready`** (a `hold` that is still loading, a member read inside a factory). A member outside the current variant throws `not_in_variant`; an ended reference throws its end (`released`, or the `gone` error). A listener added before the hydration gets its snapshot then.
420
+ - **`state: false`** gives a method-only reference: it receives no state (unless another reference to the same object wants it) and is ready once its object is live.
421
+ - **Calls on a `loading` reference wait** until it's live, respecting `ctx` cancellation and a deadline.
390
422
  - A member missing from the current variant rejects with `not_in_variant`.
391
423
  - After `release()`, every member rejects with `released`.
392
- - **`gone`** means the object was forced away while held: revocation, archive, its host dying. Call `get` again, or show "unavailable". `onStatus` reports every status change (`live`, `loading` while a link reconnects, `released`, `gone` with the error), so a UI needn't poll.
393
- - **A remote state member is hydrated only while subscribed.** When its last subscriber leaves, its `value` goes back to `undefined`, so a later subscriber never sees a stale value: it gets `undefined`, then the current snapshot.
424
+ - **`gone`** means the object was forced away while referenced: revocation, archive, its host dying. Call `get` again, or show "unavailable". `onStatus` reports every status change (`live`, `loading` while a link reconnects, `released`, `gone` with the error), so a UI needn't poll.
425
+ - **References to one remote object share one binding** per host and key (type, normalized input, principal key, contract fingerprint): one hold over the link, one stream per state member, calls over the same hold. Each reference keeps its own status, observers and `release`. With `createHost({ linger: { types: { [typeName]: ms }, cap } })`, a binding whose last reference is released stays held (and hydrated) for `ms`, at most `cap` (default 4) at a time, so taking the reference again costs nothing on the link.
394
426
  - **Return values are values.** Anything long-lived you want to share is an object you reach with `get`.
395
427
 
396
428
  ## State
397
429
 
398
430
  ```ts
399
- const state = self.state({ todos: cell(Type.Array(TodoSchema), []) });
431
+ const todos = self.state("todos", Type.Array(TodoSchema), []);
400
432
 
401
- state.todos.change(ctx, (draft) => {
433
+ todos.change(ctx, (draft) => {
402
434
  draft.push({ id: "t1", text: "Write docs", done: false });
403
435
  });
404
- state.todos.change(ctx, (draft) => {
436
+ todos.change(ctx, (draft) => {
405
437
  draft[0].done = true;
406
438
  });
407
- state.todos.replace(ctx, []);
439
+ todos.replace(ctx, []);
408
440
 
409
- state.todos.value; // immutable
410
- state.todos.subscribe((value) => render(value));
441
+ todos.value; // immutable
442
+ todos.subscribe((value) => render(value));
411
443
 
412
444
  // mirroring an external immutable store: subscribers get a diff, not the whole value
413
- state.view.replace(ctx, conversation.view); // diffs when the new value reuses old containers
414
- state.view.replace(ctx, rebuilt, { diff: true }); // always diff
445
+ view.replace(ctx, conversation.view); // diffs when the new value reuses old containers
446
+ view.replace(ctx, rebuilt, { diff: true }); // always diff
415
447
  ```
416
448
 
417
449
  - A `change` is atomic: one revision, or nothing if the callback throws.
418
450
  - `change` only touches the paths you mutate, so fields your code doesn't know about survive. Use `replace` only where your code owns the whole value.
419
451
  - Values are immutable. Never mutate what you read or receive.
420
- - Remote subscribers get a snapshot, then deltas. After a reconnect or a reload they get a `reset` snapshot, and nothing is replayed.
452
+ - Remote references get a snapshot of each state member of their variant, then deltas. After a reconnect or a reload they get a `reset` snapshot, and nothing is replayed.
421
453
  - `replace` sends a structural diff when the new value shares containers with the old one (a new revision of the same state), and a single replacement otherwise. `{ diff: true | false }` forces either. The value is the same either way.
422
- - A behavior may expose another object's state member as its own: `return { kind: "viewer", view: conversation.view }`. It reads `undefined` only while that object is loading, which never happens once this object is live.
454
+ - An implementation may expose another object's state member as its own: `return { variant: "viewer", view: conversation.view }`. The object goes live only once that member has its value, local or remote.
455
+
456
+ **A cell whose first value comes from a source** declares `{ init }` instead of an initial value:
423
457
 
424
458
  ```ts
425
- const derived = self.derive({
426
- visible: derivation([catalog.rows], (rows) => rows.filter((r) => visibleTo(principal, r))),
427
- openCount: derivation([state.todos], (t) => t.filter((x) => !x.done).length),
428
- });
459
+ const db = self.resource("db", { version: 1, open: () => openSqlite(path), close: (db) => db.close() });
460
+ const notes = self.state("notes", Type.Array(NoteSchema), { init: () => db.value.all("SELECT * FROM notes") });
461
+ ```
462
+
463
+ `init(ctx)` runs once the resources are open and the references live, before the object goes live, so no method,
464
+ listener, task or subscriber sees a value that did not come from the source. A task that loads the value instead
465
+ starts only after the object is live: a method called in between answers from the placeholder (a note that exists
466
+ reads as unknown), and an async load can overwrite a write made during its `await`. `init` may be async and gets a
467
+ `ctx` to call other objects. The `init`s of one object run in parallel, so one must not read another's cell. A throw or
468
+ an invalid value fails the activation. A reload keeps the cell and its value and does not run `init` again; a cell the
469
+ reload adds runs its `init` while the reload prepares, and a failure refuses the reload.
470
+
471
+ `init` reads a source the object has anyway. It saves nothing: what must outlive the process is written to that source
472
+ (a database, a file, a Durable document) by the code that changes it.
473
+
474
+ ```ts
475
+ const visible = self.derive([catalog.rows], (rows) => rows.filter((r) => visibleTo(principal, r)));
476
+ const openCount = self.derive([todos], (t) => t.filter((x) => !x.done).length);
429
477
  ```
430
478
 
431
- `derivation` functions are synchronous.
479
+ Derivation functions are synchronous. A derivation has no name: every reload makes a new one, computed when the
480
+ reload commits.
432
481
 
433
482
  ## Calls, ctx and roots
434
483
 
435
- Every method gets `(args, ctx)`. Pass `ctx` on to everything you call:
484
+ Every method gets `(args, ctx)`, or `(ctx)` when it takes no arguments. Pass `ctx` on to everything you call:
436
485
 
437
486
  ```ts
438
487
  send: async (input, ctx) => {
439
488
  ctx.signal.throwIfAborted();
440
- const uploads = ctx.get(Uploads, {}); // a hold owned by this call; released when it settles
489
+ const uploads = await ctx.get(Uploads, {}); // a reference owned by this call; released when it settles
441
490
  const files = await uploads.resolve({ ids: input.uploads }, ctx);
442
491
  return c.submit({ ...input, files }, author, ctx);
443
492
  },
@@ -446,57 +495,62 @@ send: async (input, ctx) => {
446
495
  A **root** is a short tree of calls, started at ingress:
447
496
  - by a link, for a request that arrives without one;
448
497
  - by `host.root(fn)`, for timers and scripts;
449
- - by each event delivered to a `listener`;
498
+ - by each event delivered to a listener (`self.on`);
450
499
  - by each Chord call made from a `task` or a Durable phase.
451
500
 
452
- A root runs on the code that was current when it started (its **epoch**), across hosts, until it ends. Keep roots short. Long-running work is a `task`, and long waits are state.
501
+ A root runs on the code that was current when it started (its **code version**), across hosts, until it ends. Keep roots short. Long-running work is a `task`, and long waits are state.
453
502
 
454
503
  ```ts
455
504
  // wrong: holds a root (and its version of the code) open for minutes
456
- approve: async (_, ctx) => waitForHuman(ctx),
505
+ approve: async (ctx) => waitForHuman(ctx),
457
506
 
458
507
  // right: record the request as state; the answer is a separate call
459
508
  request: async (req, ctx) => {
460
- state.pending.change(ctx, (d) => {
509
+ pending.change(ctx, (d) => {
461
510
  d.push(req);
462
511
  });
463
- return null;
464
512
  },
465
513
  answer: async ({ id, value }, ctx) => {
466
- state.pending.change(ctx, (d) => {
514
+ pending.change(ctx, (d) => {
467
515
  settle(d, id, value);
468
516
  });
469
- return null;
470
517
  },
471
518
  ```
472
519
 
473
520
  ## Lifetime
474
521
 
475
- An object lives while it's **held**, and every hold has an owner:
522
+ An object lives while it's **referenced**, and every reference has an owner:
476
523
 
477
524
  | Owner | Acquired with | Released when |
478
525
  | --- | --- | --- |
479
526
  | another object | `self.get` | that object is disposed, or no longer declares it |
480
527
  | a call | `ctx.get` | the call settles |
481
528
  | a connection | a remote host's `get` | release, disconnect, revocation |
482
- | the host | `eager: true` | host shutdown |
529
+ | the host | `lifetime: "host"` | host shutdown (or a reload removing its facet) |
483
530
  | UI | `useObject` | unmount |
484
531
 
485
- - **Disposal** runs after the last hold, plus `linger`. Tasks are aborted, listeners removed, resources closed in reverse order (every close runs, and errors are collected), then the object's own holds are released, which can dispose further objects.
486
- - **A resource's `close` may still use the objects its record holds.** That holds for host shutdown and revocation too: `host.close()` and `invalidatePrincipal` dispose holders before what they hold, and close links last. Bound remote calls in `close` with a signal: a call over a link that is already down waits for a reconnect.
487
- - **Forced disposal** (revocation, archive) makes handles `gone` at once and aborts in-flight calls.
488
- - **Holds must not form cycles.** Inside one host this is checked at `get`.
532
+ - **Disposal** runs after the last reference is released, plus `linger`. Tasks are aborted, listeners removed, resources closed in reverse order (every close runs, and errors are collected), then the object's own references are released, which can dispose further objects.
533
+ - **A resource's `close` may still use the objects its object references.** That holds for host shutdown and revocation too: `host.close()` and `invalidatePrincipal` dispose referrers before what they reference, and close links last. Bound remote calls in `close` with a signal: a call over a link that is already down waits for a reconnect.
534
+ - **Forced disposal** (revocation, archive) makes references `gone` at once and aborts in-flight calls.
535
+ - **References must not form cycles.** Inside one host this is checked at `get`.
489
536
 
490
537
  ```ts
491
538
  host.serve(Conversation, conversationFactory, { linger: 30_000 }); // switching back quickly doesn't reopen its watch
539
+ host.serve(Index, indexFactory, { lifetime: "host" }); // one object from load to host shutdown
492
540
  ```
493
541
 
542
+ **`lifetime: "host"`** is for local types with an empty input (or only defaults): the type has exactly one object,
543
+ created for the host principal when its facet loads. Every `get` reaches that object, whoever asks; a `get` with
544
+ another input is invalid. References others hold do not change its lifetime, and it cannot `linger`. Only such
545
+ objects may register into registries. TypeScript refuses `lifetime: "host"` for a remote type, for an input with a
546
+ required field and together with `linger`; `serve` checks the same at runtime, for callers without types.
547
+
494
548
  ## Registries
495
549
 
496
550
  ```ts
497
551
  export const Routes = defineRegistry<RouteHandler>("http.routes", { conflict: "error" });
498
552
 
499
- // contribute (only from eager objects)
553
+ // contribute (only from `lifetime: "host"` objects)
500
554
  host.serve(Downloads, (_, _host, self) => {
501
555
  const blobs = self.get(Blobs, {});
502
556
  self.register(Routes, {
@@ -507,9 +561,9 @@ host.serve(Downloads, (_, _host, self) => {
507
561
  },
508
562
  });
509
563
  return {};
510
- }, { eager: true });
564
+ }, { lifetime: "host" });
511
565
 
512
- // consume: a request sees the table of its own epoch
566
+ // consume: a request sees the table of its own code version
513
567
  server.on("request", (req, res) =>
514
568
  host.root(async (ctx) => {
515
569
  const route = host.registry(Routes).get(`${req.method} ${pathOf(req)}`, ctx);
@@ -518,7 +572,7 @@ server.on("request", (req, res) =>
518
572
  );
519
573
  ```
520
574
 
521
- - Each epoch has one complete table. A reload never leaves a moment where an entry is missing.
575
+ - Each code version has one complete table. A reload never leaves a moment where an entry is missing.
522
576
  - A request that started before a reload keeps using the old table.
523
577
  - Order is first-registration order and survives replacement.
524
578
  - `conflict: "error"` makes a duplicate key fail the registration (or the reload).
@@ -556,13 +610,13 @@ await host.load([
556
610
 
557
611
  Built-in plugins are trusted code from this repository and load directly from source.
558
612
 
559
- The facet's `setup` argument is its host, scoped to what a plugin needs: `serve`, `name`, `root(fn)` and `registry(R)`. Code that is not a Chord method, listener or task (Durable tools and hooks, callbacks of native libraries) keeps it to start roots and read registries, instead of reaching for a process-global host:
613
+ The facet's `setup` argument is its host, scoped to what a plugin needs: `serve`, `name`, `root(fn)`, `registry(R)` and `signal`, which aborts once this run of the facet is no longer installed (a reload replaced or removed it, or the reload that loaded it aborted). Code that is not a Chord method, listener or task (Durable tools and hooks, callbacks of native libraries) keeps it to start roots and read registries, instead of reaching for a process-global host:
560
614
 
561
615
  ```ts
562
616
  export default defineFacet((host) => {
563
617
  host.serve(Sessions, sessionsFactory);
564
618
  tools.register("rename_session", (args) =>
565
- host.root((ctx) => ctx.get(Sessions, {}, { at: "server" }).rename(args, ctx)),
619
+ host.root(async (ctx) => (await ctx.get(Sessions, {})).rename(args, ctx)),
566
620
  );
567
621
  });
568
622
  ```
@@ -576,36 +630,31 @@ await host.reload([new URL("../plugins/chat/worker.ts", import.meta.url)]); //
576
630
  ### What happens
577
631
 
578
632
  1. **Load** the new modules.
579
- 2. **Prepare:** run the new factories over the existing records, without side effects. Cell writes throw, listeners and tasks don't start, registrations are collected, and changed resources are opened next to the old ones. Contracts are checked against every live consumer. Any failure leaves everything as it was.
580
- 3. **Gate:** only if a state version was bumped or an exclusive resource changes.
633
+ 2. **Prepare:** run the new factories over the existing objects, without side effects. Cell writes throw, listeners and tasks don't start, registrations are collected, and changed resources are opened next to the old ones. Contracts are checked against every live consumer. Any failure leaves everything as it was.
634
+ 3. **Gate:** only if a cell's version was raised or an exclusive resource changes.
581
635
  4. **Commit:** in one synchronous step, install the new code, swap registry tables, restart tasks, rebind listeners, and send `reset` and shape updates to subscribers.
582
- 5. **Retire:** old code keeps running the roots it started. Once they end, it's dropped, along with whatever it alone declared.
636
+ 5. **Retire:** old code keeps running the roots it started. Once they end, it's dropped, along with whatever it alone declared. If they are still running when the next reload starts, that reload waits up to `retireDeadline`, then aborts them (their `ctx.signal`) before it closes what only the old code used.
583
637
 
584
638
  ### Compatible changes: no waiting
585
639
 
586
640
  Bug fixes, new methods, new cells, new resources and registration changes all switch immediately. A root that started on the old code finishes on it, and new roots get the new code.
587
641
 
588
- ### Changing what state looks like or means: bump the state version
642
+ ### Changing what state looks like or means: bump the cell's version
589
643
 
590
644
  ```ts
591
645
  // v1: { running, tasks } where tasks counted phases
592
646
  // v2: tasks counts tasks, and usage is new
593
647
  host.serve(StatusStore, ({ session }, _host, self) => {
594
- const state = self.state({ status: cell(StatusV2, { running: false, tasks: 0, usage: emptyUsage() }) });
648
+ const status = self.state("status", StatusV2, { running: false, tasks: 0, usage: emptyUsage() }, {
649
+ version: 2,
650
+ migrate: (old, from) => (from < 2 ? { running: old.running, tasks: 0, usage: emptyUsage() } : old),
651
+ });
595
652
  return {
596
- status: state.status,
653
+ status,
597
654
  report: async (s, ctx) => {
598
- state.status.replace(ctx, s);
599
- return null;
655
+ status.replace(ctx, s);
600
656
  },
601
657
  };
602
- }, {
603
- state: {
604
- version: 2,
605
- migrate: {
606
- status: (old, from) => (from < 2 ? { running: old.running, tasks: 0, usage: emptyUsage() } : old),
607
- },
608
- },
609
658
  });
610
659
  ```
611
660
 
@@ -617,24 +666,26 @@ At the gate, for each live `StatusStore`:
617
666
 
618
667
  If the roots inside don't finish within the deadline (5 s), the reload is **aborted** and nothing changes. The error names the blocking roots.
619
668
 
620
- The runtime compares each cell's schema between old and new code:
669
+ The version belongs to the cell (default 1); other cells of the object are not migrated. The runtime compares each
670
+ cell's schema and version between old and new code:
621
671
  - unchanged: compatible;
622
- - changed with a bump: migrate;
623
- - changed **without** a bump: the reload is refused.
672
+ - version raised: migrate (without `migrate`, the value is kept and must fit the new schema);
673
+ - schema changed **without** a raised version, or a lowered version: the reload is refused.
624
674
 
625
675
  A change of meaning with an identical schema needs a bump too. Nothing can detect that one for you.
626
676
 
627
677
  ### Resources
628
678
 
629
679
  ```ts
630
- const res = self.resources({
631
- db: resource({ rev: 2, open: () => openSqlite(path, { wal: true }), close: (db) => db.close() }), // rev 1 → 2: reopen
632
- port: resource({ rev: 1, exclusive: true, open: () => listen(8787), close: (s) => s.close() }),
633
- });
680
+ const db = self.resource("db", { version: 2, open: () => openSqlite(path, { wal: true }), close: (db) => db.close() }); // 1 → 2: reopen
681
+ const port = self.resource("port", { version: 1, exclusive: true, open: () => listen(8787), close: (s) => s.close() });
634
682
  ```
635
683
 
636
- - A reload doesn't touch a resource unless its `rev` or `deps` change.
637
- - A changed resource opens next to the old one during prepare.
684
+ - A reload keeps a resource's open value unless its `version`, `deps` or `exclusive` change: `open` doesn't run
685
+ again. The version is required: a native value has no schema to detect a change. The kept resource still takes the
686
+ new code's declaration at the commit, so a later `close` is the new one.
687
+ - A changed resource opens next to the old one during prepare. The old one is closed with its own `close` once the
688
+ old code retires.
638
689
  - An `exclusive` resource can't exist twice, so it's swapped at the gate: close the old one, open the new one, and reopen the old one if that fails.
639
690
 
640
691
  ### Contract changes
@@ -646,10 +697,10 @@ const res = self.resources({
646
697
  | Breaking, without an adapter | refused, unless the consumers reload in the same batch |
647
698
  | Breaking, version not bumped | refused: "consumer needs member …" |
648
699
 
649
- The host's own **eager bootstrap hold** is a lifecycle pin, not an old-version consumer. When a reload replaces
650
- its provider, that hold adopts the new contract at commit; it does not require an `accepts` adapter for itself.
651
- The record, cells and unchanged resources stay alive without reactivation. A refused reload keeps the original
652
- hold and contract. Other holds (`self.get`, `ctx.get`, `host.get`, peers) still obey the compatibility rules above;
700
+ The host's own **reference to a `lifetime: "host"` object** is a lifecycle pin, not an old-version consumer. When a reload replaces
701
+ its provider, that reference adopts the new contract at commit; it does not require an `accepts` adapter for itself.
702
+ The object, cells and unchanged resources stay alive without reactivation. A refused reload keeps the original
703
+ reference and contract. Other references (`self.get`, `ctx.get`, `host.get`, peers) still obey the compatibility rules above;
653
704
  state-version bumps and migrations are still required when stored state changes.
654
705
 
655
706
  ### Several plugins at once
@@ -662,46 +713,59 @@ On the server, `host.reload(serverFacets, { remote: { "session:": workerFacets }
662
713
 
663
714
  ```ts
664
715
  // server
665
- const server = createHost({ name: "server", principalKey });
716
+ const server = createHost<Principal>({ name: "server", self: { kind: "host" }, principalKey });
666
717
  server.accept(webSocketLink(wss, { principal: (req) => authenticate(req) })); // stamps owner or guest from cookies
667
718
  server.route("session:", {
668
719
  authorize: (principal, host) => canReach(principal, host), // before resolving or spawning
669
720
  resolve: (host) => workers.ensure(host), // a link to the worker, spawned if needed
721
+ principal: (host) => workers.principal(host), // what the worker acts as when it calls the server
670
722
  });
671
723
 
672
724
  // worker
673
- const worker = createHost({ name: `session:${id}`, principalKey, upstream: ipcLink(process) });
725
+ const worker = createHost<Principal>({
726
+ name: `session:${id}`,
727
+ self: { kind: "host" },
728
+ principalKey,
729
+ upstream: { link: ipcLink(process), principal: { kind: "server" } }, // the server's own requests act as this
730
+ });
674
731
 
675
732
  // anywhere
676
- const chat = ctx.get(Chat, input, { at: "session:S1" }); // browser → server → worker
677
- const sessions = self.get(Sessions, {}, { at: "server" }); // worker → server, acting as worker{S1}
733
+ const chat = await ctx.get(Chat, { session: "S1", conversation: 1 }); // browser → server → worker session:S1
734
+ const sessions = self.get(Sessions, {}); // `hostNamed("server")`: worker → server, acting as worker{S1}
678
735
  ```
679
736
 
680
737
  - **The server relays** between its children: worker to worker, and browser to worker.
681
- - **Disconnects:** when a browser disconnects, exactly the holds it had in workers are released.
738
+ - **Disconnects:** when a browser disconnects, exactly the references it had in workers are released.
682
739
  - **Principals across links:**
683
740
  - a forwarded principal is accepted only from the parent link;
684
741
  - calls an object makes itself act as its host;
685
742
  - links ping, and a dead link releases everything held through it.
743
+ - **Client origins:** a hold that comes from a browser carries an opaque id of its connection (`origin`), forwarded by the server when it relays the hold, so a worker can tell its clients apart. A client cannot set it. A disconnect releases that client's holds at once.
744
+ - **Slow peers:** a peer acknowledges what it processed. While a window (`window`, default 256 KiB) of frames is unacknowledged, state waits and is coalesced per member's stream; calls, results and the first snapshots of a new reference are never held back, so a `get` does not wait behind other references' state. Waiting ops are capped per link (`pendingCap`, default four windows): beyond, the oldest streams are replaced by their current value. So under load a result can arrive before the state change it caused: wait for the state you need instead of assuming it is there when the call returns.
686
745
  - **Reloads across hosts:** the server coordinates prepare and commit for every host.
687
- - **Workers that start behind** (spawned after a reload) are caught up to the server's epoch before anything is routed to them. Calls their roots make to the server meanwhile run in the server's current epoch instead of failing `superseded`.
746
+ - **Workers that start behind** (spawned after a reload) are caught up to the server's code version before anything is routed to them. Calls their roots make to the server meanwhile run in the server's current code version instead of failing `superseded`.
747
+ - **Protocol version:** both ends announce the link protocol version in their hello. A peer of another version is not
748
+ talked to: the link closes, and `onProtocolMismatch` (a `LinkOptions` field of `connect` and `accept`) tells the
749
+ side that has to load new code. A browser reloads its page instead of reconnecting:
750
+
751
+ ```ts
752
+ host.connect(webSocketLink(socket), { onProtocolMismatch: () => location.reload() });
753
+ ```
688
754
  - **Browsers** don't take part in commits. They learn about new code through shape and `reset` messages, and an incompatible contract makes their calls fail with `upgrade_required`.
689
755
 
690
756
  ## Durable
691
757
 
692
- A worker's host provides the Durable Harness as an eager local object. A Durable phase isn't a root: it's pinned to its registry snapshot by Durable itself. Each Chord call a hook or tool makes starts a fresh short root.
758
+ A worker's host provides the Durable Harness as a local object of lifetime `"host"`. A Durable phase isn't a root: it's pinned to its registry snapshot by Durable itself. Each Chord call a hook or tool makes starts a fresh short root.
693
759
 
694
760
  ```ts
695
761
  host.serve(Harness, (_, _host, self) => {
696
- const res = self.resources({
697
- durable: resource({
698
- rev: 1,
699
- open: () => DurableHarness.open(storage, { models, registry: durableRegistry(host, Extensions) }), // a reader over the epoch tables
700
- close: (h) => h.close(),
701
- }),
762
+ const durable = self.resource("durable", {
763
+ version: 1,
764
+ open: () => DurableHarness.open(storage, { models, registry: durableRegistry(host, Extensions) }), // a reader over the code version tables
765
+ close: (h) => h.close(),
702
766
  });
703
- return { conversation: (id: number) => res.durable.value.conversation(id) };
704
- }, { eager: true });
767
+ return { conversation: (id: number) => durable.value.conversation(id) };
768
+ }, { lifetime: "host" });
705
769
 
706
770
  // a plugin contributes a Durable extension like any registration
707
771
  host.serve(Guard, (_, _host, self) => {
@@ -712,7 +776,7 @@ host.serve(Guard, (_, _host, self) => {
712
776
  }),
713
777
  });
714
778
  return {};
715
- }, { eager: true });
779
+ }, { lifetime: "host" });
716
780
  ```
717
781
 
718
782
  A reload changing the guard: Durable phases already running keep the old snapshot, the next phase uses the new one, and there's never a phase without a guard.
@@ -723,15 +787,15 @@ A reload changing the guard: Durable phases already running keep the old snapsho
723
787
  | --- | --- |
724
788
  | `forbidden` | The factory or a method refused this principal |
725
789
  | `invalid` | Input, arguments or a result failed validation |
726
- | `not_ready` | A declared resource or handle was used inside a factory |
727
- | `not_in_variant` | The member doesn't exist in the variant you hold |
728
- | `released` | The handle was released |
729
- | `gone` | The object was forced away while held. Call `get` again. |
790
+ | `not_ready` | A declared resource or reference was used inside a factory, or state read before its reference's first hydration |
791
+ | `not_in_variant` | The member doesn't exist in the variant you have |
792
+ | `released` | The reference was released |
793
+ | `gone` | The object was forced away while referenced. Call `get` again. |
730
794
  | `contract_mismatch` | The provider's contract doesn't cover the consumer's, and no adapter exists |
731
795
  | `upgrade_conflict` | An old root reached an object migrated meanwhile. Retry as a new root. |
732
- | `superseded` | The root's epoch is no longer available on this host. Retry. |
796
+ | `superseded` | The root's code version is no longer available on this host. Retry. |
733
797
  | `upgrade_required` | A browser's contract is outdated. Reload its web facets. |
734
- | `unavailable` | The target host can't be reached or started |
798
+ | `unavailable` | The target host can't be reached or started; reason `ready_timeout`: a reference was not ready within `readyTimeout` |
735
799
  | `aborted` | The call's `ctx` was cancelled |
736
800
 
737
801
  ## Rules of thumb
@@ -739,11 +803,11 @@ A reload changing the guard: Durable phases already running keep the old snapsho
739
803
  | Do | Not |
740
804
  | --- | --- |
741
805
  | Keep reloadable state in cells | Closure variables (lost on reload) |
742
- | Bump the state version, with `migrate`, when a cell's shape or meaning changes | Changing a schema without a bump (refused) or a meaning without a bump (silent) |
806
+ | Bump the cell's version, with `migrate`, when a cell's shape or meaning changes | Changing a schema without a bump (refused) or a meaning without a bump (silent) |
743
807
  | Bump the contract version on breaking changes; add `accepts` for old consumers | Renaming or removing members at the same version (refused) |
744
808
  | Do slow setup in a resource's `open` | Awaiting inside the factory (it's synchronous) |
745
809
  | Do side effects in resources, listeners, tasks and registrations | Effects in the factory body (they run during prepare) |
746
- | Use `ctx.get` inside methods | `self.get` inside methods (a new hold per call) |
810
+ | Use `ctx.get` inside methods | `self.get` inside methods (a new reference per call) |
747
811
  | Keep roots short; long waits are state | Awaiting a human inside a method |
748
812
  | Check arguments in methods | Treating the variant as all of authorization |
749
813
  | Pass `ctx` everywhere | Starting background work with the caller's `ctx` (use a task) |