@earendil-works/chord 1.2.0 → 1.3.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 (86) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +14 -13
  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/contract.d.ts +120 -45
  16. package/dist/contract.d.ts.map +1 -1
  17. package/dist/contract.js +104 -15
  18. package/dist/contract.js.map +1 -1
  19. package/dist/declare.d.ts +11 -48
  20. package/dist/declare.d.ts.map +1 -1
  21. package/dist/declare.js +1 -21
  22. package/dist/declare.js.map +1 -1
  23. package/dist/delta/index.d.ts +2 -1
  24. package/dist/delta/index.d.ts.map +1 -1
  25. package/dist/delta/index.js +4 -0
  26. package/dist/delta/index.js.map +1 -1
  27. package/dist/errors.d.ts +4 -3
  28. package/dist/errors.d.ts.map +1 -1
  29. package/dist/errors.js +15 -0
  30. package/dist/errors.js.map +1 -1
  31. package/dist/host.d.ts +214 -120
  32. package/dist/host.d.ts.map +1 -1
  33. package/dist/host.js +446 -241
  34. package/dist/host.js.map +1 -1
  35. package/dist/index.d.ts +8 -8
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +4 -4
  38. package/dist/index.js.map +1 -1
  39. package/dist/link.d.ts +47 -12
  40. package/dist/link.d.ts.map +1 -1
  41. package/dist/link.js +200 -14
  42. package/dist/link.js.map +1 -1
  43. package/dist/loader.d.ts.map +1 -1
  44. package/dist/loader.js +7 -2
  45. package/dist/loader.js.map +1 -1
  46. package/dist/manager.d.ts.map +1 -1
  47. package/dist/manager.js +35 -9
  48. package/dist/manager.js.map +1 -1
  49. package/dist/object.d.ts +438 -0
  50. package/dist/object.d.ts.map +1 -0
  51. package/dist/{record.js → object.js} +614 -335
  52. package/dist/object.js.map +1 -0
  53. package/dist/output.d.ts +85 -0
  54. package/dist/output.d.ts.map +1 -0
  55. package/dist/output.js +350 -0
  56. package/dist/output.js.map +1 -0
  57. package/dist/registry.d.ts +3 -3
  58. package/dist/registry.d.ts.map +1 -1
  59. package/dist/registry.js +1 -1
  60. package/dist/registry.js.map +1 -1
  61. package/dist/reload.d.ts +35 -27
  62. package/dist/reload.d.ts.map +1 -1
  63. package/dist/reload.js +256 -192
  64. package/dist/reload.js.map +1 -1
  65. package/dist/remote.d.ts +130 -35
  66. package/dist/remote.d.ts.map +1 -1
  67. package/dist/remote.js +493 -189
  68. package/dist/remote.js.map +1 -1
  69. package/dist/types.d.ts +39 -28
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/types.js.map +1 -1
  72. package/dist/websocket.d.ts +4 -5
  73. package/dist/websocket.d.ts.map +1 -1
  74. package/dist/websocket.js +3 -2
  75. package/dist/websocket.js.map +1 -1
  76. package/dist/wire.d.ts +210 -129
  77. package/dist/wire.d.ts.map +1 -1
  78. package/dist/wire.js +108 -55
  79. package/dist/wire.js.map +1 -1
  80. package/docs/guide.md +267 -203
  81. package/docs/spec.md +141 -114
  82. package/docs/tutorial.md +257 -235
  83. package/package.json +6 -1
  84. package/dist/record.d.ts +0 -320
  85. package/dist/record.d.ts.map +0 -1
  86. package/dist/record.js.map +0 -1
package/docs/tutorial.md CHANGED
@@ -7,15 +7,15 @@ You could hand-write object creation, subscription cleanup, messages between
7
7
  processes, and code replacement. Chord connects these pieces for you.
8
8
 
9
9
  A **host** is the runtime that owns objects. An **object** combines state with
10
- methods at an address, such as “the family todo list.” A **handle** is a caller's
11
- reference to that object, not the object itself. Several handles can reach the
12
- same object. Handles can also reach another host over a connection.
10
+ methods at an address, such as “the family todo list.” A **reference** is a caller's
11
+ reference to that object, not the object itself. Several references can reach the
12
+ same object. References can also reach another host over a connection.
13
13
 
14
14
  ```text
15
15
  browser host server host
16
- handle + subscribed rows -- link --> Todos view (owner or guest)
16
+ ref + subscribed rows -- link --> Todos view (owner or guest)
17
17
  |
18
- handle
18
+ ref
19
19
  v
20
20
  TodoStore: rows + methods
21
21
  ```
@@ -27,7 +27,7 @@ asking for it again.
27
27
 
28
28
  <!-- example:01 -->
29
29
  ```ts
30
- const host = createHost({ name: "todo-server" });
30
+ const host = createHost({ self: "server", name: "todo-server" });
31
31
  assert.equal(host.name, "todo-server");
32
32
  await host.close();
33
33
  ```
@@ -46,7 +46,10 @@ shuts down its objects and connections.
46
46
  Before callers can use an object, they need to agree on its name and members.
47
47
  That agreement is a **contract**: a runtime value that also gives TypeScript its
48
48
  types. A **schema** describes a value's shape. We use TypeBox's `Type` to write
49
- schemas; `Type.Null()` means this method takes no useful argument.
49
+ schemas. `method(args, result)` declares a method by its argument and result
50
+ schemas: `method()` takes no arguments and returns nothing, and
51
+ `method(undefined, Type.String())`, used below, takes no arguments and returns a
52
+ string.
50
53
 
51
54
  Start with a label for a named list. `local` means only code inside this host
52
55
  can reach it. The input `{ list: "family" }` identifies which list we want;
@@ -57,11 +60,11 @@ it is not an argument to `label`.
57
60
  const TodoLabel = local("tutorial.todos", {
58
61
  version: 1,
59
62
  input: Type.Object({ list: Type.String() }),
60
- members: { label: method(Type.Null(), Type.String()) },
63
+ members: { label: method(undefined, Type.String()) },
61
64
  });
62
65
  ```
63
66
 
64
- A **factory** is a synchronous function that creates the object's behavior:
67
+ A **factory** is a synchronous function that creates the object's implementation:
65
68
  the methods and state callers will see. `serve` registers a contract and its
66
69
  factory. Registration alone does not activate this lazy object; the first
67
70
  `get` does.
@@ -73,26 +76,27 @@ call from that root.
73
76
 
74
77
  <!-- example:02-call -->
75
78
  ```ts
76
- const host = createHost();
79
+ const host = createHost({ self: "server" });
77
80
  host.serve(TodoLabel, ({ list }) => ({ label: () => `Todos for ${list}` }));
78
81
  const result = await host.root(async (ctx) => {
79
- const todos = ctx.get(TodoLabel, { list: "family" });
80
- return todos.label(null, ctx);
82
+ const todos = await ctx.get(TodoLabel, { list: "family" });
83
+ return todos.label(ctx);
81
84
  });
82
85
  assert.equal(result, "Todos for family");
83
86
  ```
84
87
 
85
- The result is `"Todos for family"`. `ctx.get` returns a handle immediately;
86
- method calls wait for the object to become live. The handle acquired through
88
+ The result is `"Todos for family"`. `await ctx.get` resolves once the object is
89
+ live (and, as we will see, has its state). The reference acquired through
87
90
  `ctx.get` is released when this root ends. We never receive the factory's
88
91
  returned object directly. A standalone call without `ctx`, such as
89
- `todos.label(null)`, starts its own root. Inside a root or method, pass the
92
+ `todos.label()`, starts its own root. A method without arguments takes only
93
+ the context: `todos.label(ctx)`; its implementation is `(ctx) => …`. Inside a root or method, pass the
90
94
  existing `ctx` so nested calls stay in the same call tree.
91
95
 
92
96
  The factory may also receive `principal` (who is asking) and `self` (the
93
97
  object's declaration API). We did not need them yet.
94
98
 
95
- > **Why:** The handle separates callers from implementations. Later, replacing
99
+ > **Why:** The reference separates callers from implementations. Later, replacing
96
100
  > code will not require replacing every caller's reference.
97
101
 
98
102
  ## 3. Give the list live state
@@ -105,8 +109,10 @@ Extend the contract with `rows`, `openCount`, and editing methods. State
105
109
  members describe values, not methods. This adds members at contract version 1;
106
110
  older callers that only need `label` still have everything they need.
107
111
 
108
- `self.state` declares named cells. The name `rows` is important: on reload,
109
- Chord matches that name to the existing cell and keeps its value.
112
+ `self.state(name, schema, initial)` declares a cell and returns it. The name
113
+ `rows` is important: on reload, Chord matches that name to the existing cell and
114
+ keeps its value. `self.derive(sources, fn)` declares a derivation; it has no
115
+ name, because it holds nothing a reload must keep.
110
116
 
111
117
  <!-- example:03-contract -->
112
118
  ```ts
@@ -116,51 +122,52 @@ const TodoStore = local("tutorial.todos", {
116
122
  version: 1,
117
123
  input: Type.Object({ list: Type.String() }),
118
124
  members: {
119
- label: method(Type.Null(), Type.String()),
125
+ label: method(undefined, Type.String()),
120
126
  rows: Rows,
121
127
  openCount: Type.Number(),
122
- add: method(Type.Object({ text: Type.String() }), Type.Null()),
123
- finish: method(Type.Object({ index: Type.Integer() }), Type.Null()),
124
- reset: method(Rows, Type.Null()),
128
+ add: method(Type.Object({ text: Type.String() })),
129
+ finish: method(Type.Object({ index: Type.Integer() })),
130
+ reset: method(Rows),
125
131
  },
126
132
  });
127
133
  ```
128
134
 
129
- The factory now declares a cell and a derivation. Each method receives its
130
- arguments and a context; changing the cell updates its readers and its derived
131
- count together.
135
+ The factory now declares a cell and a derivation. It is wrapped in `todoStore`,
136
+ which takes the cell's options (section 9 raises its version); for now
137
+ `todoFactory` uses the defaults. Each method receives its arguments and a
138
+ context; changing the cell updates its readers and its derived count together.
139
+ `add`, `finish` and `reset` declare no result, so they return nothing.
132
140
 
133
141
  <!-- example:03-factory -->
134
142
  ```ts
135
- const todoFactory: Factory<typeof TodoStore> = ({ list }, _principal, self) => {
136
- const state = self.state({ rows: cell(Rows, []) });
137
- const derived = self.derive({
138
- openCount: derivation([state.rows], (rows) => rows.filter((todo) => !todo.done).length),
139
- });
140
- return {
141
- label: () => `Todos for ${list}`,
142
- rows: state.rows,
143
- openCount: derived.openCount,
144
- add: ({ text }, ctx) => {
145
- state.rows.change(ctx, (draft) => {
146
- draft.push({ text, done: false });
147
- });
148
- return null;
149
- },
150
- finish: ({ index }, ctx) => {
151
- state.rows.change(ctx, (draft) => {
152
- const todo = draft[index];
153
- if (!todo) throw invalid("args", "No such todo");
154
- todo.done = true;
155
- });
156
- return null;
157
- },
158
- reset: (rows, ctx) => {
159
- state.rows.replace(ctx, rows, { diff: true });
160
- return null;
161
- },
143
+ /** The store's factory; `options` versions its rows (section 9). */
144
+ const todoStore =
145
+ (options?: StateOptions<Static<typeof Rows>>): Factory<typeof TodoStore> =>
146
+ ({ list }, _principal, self) => {
147
+ const rows = self.state("rows", Rows, [], options);
148
+ const openCount = self.derive([rows], (todos) => todos.filter((todo) => !todo.done).length);
149
+ return {
150
+ label: () => `Todos for ${list}`,
151
+ rows,
152
+ openCount,
153
+ add: ({ text }, ctx) => {
154
+ rows.change(ctx, (draft) => {
155
+ draft.push({ text, done: false });
156
+ });
157
+ },
158
+ finish: ({ index }, ctx) => {
159
+ rows.change(ctx, (draft) => {
160
+ const todo = draft[index];
161
+ if (!todo) throw invalid("args", "No such todo");
162
+ todo.done = true;
163
+ });
164
+ },
165
+ reset: (next, ctx) => {
166
+ rows.replace(ctx, next, { diff: true });
167
+ },
168
+ };
162
169
  };
163
- };
170
+ const todoFactory = todoStore();
164
171
  ```
165
172
 
166
173
  `change(ctx, fn)` gives `fn` a temporary mutable **draft**. Here `add`
@@ -176,10 +183,9 @@ receive the same resulting value.
176
183
 
177
184
  <!-- example:03-call -->
178
185
  ```ts
179
- const host = createHost({ dev: true });
186
+ const host = createHost({ self: "server", dev: true });
180
187
  host.serve(TodoStore, todoFactory);
181
- const todos = host.get(TodoStore, { list: "family" });
182
- await todos.ready;
188
+ const todos = await host.get(TodoStore, { list: "family" });
183
189
  const counts: (number | undefined)[] = [];
184
190
  const stop = todos.openCount.subscribe((value) => counts.push(value));
185
191
  await host.root(async (ctx) => {
@@ -197,19 +203,19 @@ The count subscriber sees `0, 1, 0`: initially empty, one unfinished todo,
197
203
  then no unfinished todos. Resetting to empty does not add another count
198
204
  notification because the derived count is still zero. The final rows are `[]`.
199
205
  `subscribe` calls back immediately with the current value and returns an
200
- unsubscribe function. We await `ready` first so that value is not a loading
201
- placeholder.
206
+ unsubscribe function. The awaited `get` makes sure there is a value: a reference
207
+ you hold is live, with its state.
202
208
 
203
209
  > **Why:** Store reloadable state in cells, not closure variables. Treat
204
- > `cell.value`, `handle.rows.value`, and subscriber values as immutable.
210
+ > `cell.value`, `ref.rows.value`, and subscriber values as immutable.
205
211
  > Only mutate drafts inside `change`; do not keep a draft for later use.
206
212
  > A derivation runs synchronously, so use it for projections, not database work.
207
213
 
208
214
  ## 4. Let objects use objects and resources
209
215
 
210
- Saving a todo may require both the list and a database connection. A **hold**
211
- is a handle's claim that keeps an object alive. `self.get` declares a hold
212
- owned by this object; it lasts longer than a single method call.
216
+ Saving a todo may require both the list and a database connection. A
217
+ **reference** keeps the object it refers to alive. `self.get` declares a
218
+ reference owned by this object; it lasts longer than a single method call.
213
219
 
214
220
  A **resource** is something the runtime opens and later closes, such as a
215
221
  database connection. Its `open` may be asynchronous even though the factory
@@ -230,23 +236,21 @@ const Database = local("tutorial.database", {
230
236
  const SavedTodos = local("tutorial.saved-todos", {
231
237
  version: 1,
232
238
  input: Type.Object({ list: Type.String() }),
233
- members: { add: method(Type.Object({ text: Type.String() }), Type.Null()) },
239
+ members: { add: method(Type.Object({ text: Type.String() })) },
234
240
  });
235
- const host = createHost();
241
+ const host = createHost({ self: "server" });
236
242
  let opens = 0;
237
243
  const closed = Promise.withResolvers<void>();
238
244
  host.serve(Database, (_input, _principal, self) => {
239
- const res = self.resources({
240
- db: resource({
241
- rev: 1,
242
- open: async () => {
243
- opens++;
244
- return { saved: [] as string[] };
245
- },
246
- close: () => closed.resolve(),
247
- }),
245
+ const db = self.resource("db", {
246
+ version: 1,
247
+ open: async () => {
248
+ opens++;
249
+ return { saved: [] as string[] };
250
+ },
251
+ close: () => closed.resolve(),
248
252
  });
249
- return { db: self.expose(res.db) };
253
+ return { db: self.expose(db) };
250
254
  });
251
255
  ```
252
256
 
@@ -269,7 +273,6 @@ host.serve(
269
273
  add: async ({ text }, ctx) => {
270
274
  database.db!.saved.push(text);
271
275
  await todos.add({ text }, ctx);
272
- return null;
273
276
  },
274
277
  };
275
278
  },
@@ -277,14 +280,13 @@ host.serve(
277
280
  );
278
281
  ```
279
282
 
280
- An outside consumer holds a `SavedTodos` handle. We also hold `Database`
283
+ An outside consumer holds a `SavedTodos` reference. We also hold `Database`
281
284
  directly so we can inspect what was saved.
282
285
 
283
286
  <!-- example:04-call -->
284
287
  ```ts
285
- const saved = host.get(SavedTodos, { list: "family" });
286
- const database = host.get(Database, {});
287
- await saved.ready;
288
+ const saved = await host.get(SavedTodos, { list: "family" });
289
+ const database = await host.get(Database, {});
288
290
  await host.root((ctx) => saved.add({ text: "Buy milk" }, ctx));
289
291
  assert.deepEqual(database.db!.saved, ["Buy milk"]);
290
292
  assert.equal(opens, 1);
@@ -298,25 +300,25 @@ assert.ok(statuses.includes("released"));
298
300
  ```
299
301
 
300
302
  After the method, the fake database contains `["Buy milk"]`. It opened once,
301
- even though two objects held it. `SavedTodos` and `Database` become live
303
+ even though two objects referenced it. `SavedTodos` and `Database` become live
302
304
  only after their resources and declared dependencies are ready. That is why
303
305
  `database.db!` is safe inside this live object's method: activation has
304
- already established that dependency. An outside caller must await readiness.
306
+ already established that dependency. An outside caller awaits its `get`.
305
307
 
306
- When both external handles are released, `SavedTodos` is disposed and
307
- releases its own database hold. With no holds left, the database closes; we
308
- wait for `closed.promise` to prove it. The released handle stays
308
+ When both external references are released, `SavedTodos` is disposed and
309
+ releases its own database reference. With no references left, the database closes; we
310
+ wait for `closed.promise` to prove it. The released reference stays
309
311
  `released`, and `onStatus` reports that transition. `onStatus` watches
310
312
  changes; unlike state subscription, it does not deliver an initial status.
311
313
 
312
- `linger` is a delay in milliseconds after the last hold leaves. Zero disposes
314
+ `linger` is a delay in milliseconds after the last reference is released. Zero disposes
313
315
  without a grace period; a positive value can avoid reopening an expensive
314
316
  resource when a screen is briefly hidden. Linger is not persistence.
315
317
 
316
- > **Tip:** A factory only declares dependencies. Never call a held method or
318
+ > **Tip:** A factory only declares dependencies. Never call a referenced object's method or
317
319
  > read `resource.value` there, including during reload. Activation happens
318
- > afterward. Bump a resource's `rev` when its configuration should be reopened.
319
- > Disposal stops tasks and listeners, closes resources, then releases holds.
320
+ > afterward. Bump a resource's `version` when its configuration should be reopened.
321
+ > Disposal stops tasks and listeners, closes resources, then releases references.
320
322
 
321
323
  ## 5. Who is asking? One store, two views
322
324
 
@@ -327,32 +329,38 @@ whether an editing method is available.
327
329
 
328
330
  Do not create a separate data cell for each viewer: their lists would diverge.
329
331
  Instead, each view holds the same host-local `TodoStore` and exposes its rows.
330
- `self.get` defaults to the host principal, so owner and guest views share
331
- that store. Remote view records themselves are separate per principal key.
332
+ `self.get` acts as the host's own principal, so owner and guest views share
333
+ that store. Remote view objects themselves are separate per principal key.
334
+ A remote contract also says where its objects live: `host: hostNamed("server")`
335
+ puts every `Todos` object on the host named `server`. Section 6 uses that.
332
336
 
333
337
  <!-- example:05-views -->
334
338
  ```ts
335
339
  const Todos = remote("tutorial.todo-view", {
336
340
  version: 1,
337
341
  input: Type.Object({ list: Type.String() }),
342
+ host: hostNamed("server"),
338
343
  variants: {
339
344
  viewer: { state: { rows: Rows } },
340
345
  editor: {
341
346
  state: { rows: Rows },
342
- methods: { add: method(Type.Object({ text: Type.String() }), Type.Null()) },
347
+ methods: { add: method(Type.Object({ text: Type.String() })) },
343
348
  },
344
349
  },
345
350
  });
346
351
 
347
- function serveViews(api: Pick<Host, "serve">): void {
352
+ /** Who may ask: the owner, a guest, or the server's own code. */
353
+ type Principal = "owner" | "guest" | "server";
354
+
355
+ function serveViews(api: Pick<Host<Principal>, "serve">): void {
348
356
  api.serve(Todos, ({ list }, principal, self) => {
349
357
  if (list !== "family") throw forbidden("This list is not shared");
350
358
  const store = self.get(TodoStore, { list });
351
- if (principal !== "owner" && principal !== hostPrincipal) {
352
- return { kind: "viewer", rows: store.rows };
359
+ if (principal === "guest") {
360
+ return { variant: "viewer", rows: store.rows };
353
361
  }
354
362
  return {
355
- kind: "editor",
363
+ variant: "editor",
356
364
  rows: store.rows,
357
365
  add: ({ text }, ctx) => {
358
366
  if (text.trim().length === 0) throw invalid("args", "A todo needs text");
@@ -364,29 +372,35 @@ function serveViews(api: Pick<Host, "serve">): void {
364
372
  ```
365
373
 
366
374
  Here principals are the strings `"owner"` and `"guest"`, assigned by
367
- trusted application code. The built-in `hostPrincipal` can also edit.
368
- `principalKey` maps identity to record identity. Real applications should use
369
- stable identity, connection provenance, and grant revision, not display names.
375
+ trusted application code, and `"server"`, the host's own code: every host is
376
+ created with `self`, the principal its own roots and objects act as. Chord
377
+ invents no principals. The `Principal` type parameter of `createHost` and
378
+ `Host` makes `principal` in the factory that type, so TypeScript checks the
379
+ comparisons. `principalKey` maps identity to object identity. Real
380
+ applications should use stable identity, connection provenance, and grant
381
+ revision, not display names.
370
382
 
371
383
  <!-- example:05-call -->
372
384
  ```ts
373
- const host = createHost({ principalKey: (principal) => String(principal) });
385
+ const host = createHost<Principal>({ name: "server", self: "server", principalKey: (principal) => principal });
374
386
  host.serve(TodoStore, todoFactory);
375
387
  serveViews(host);
376
- const owner = host.get(Todos, { list: "family" }, { principal: "owner" });
377
- const guest = host.get(Todos, { list: "family" }, { principal: "guest" });
378
- await Promise.all([owner.ready, guest.ready]);
379
- assert.equal(owner.kind, "editor");
380
- assert.equal(guest.kind, "viewer");
388
+ const owner = await host.get(Todos, { list: "family" }, { principal: "owner" });
389
+ const guest = await host.get(Todos, { list: "family" }, { principal: "guest" });
390
+ assert.ok(owner.variant === "editor"); // narrows the reference to the editor variant
391
+ assert.equal(guest.variant, "viewer");
381
392
  await host.root((ctx) => owner.add({ text: "Buy milk" }, ctx), { principal: "owner" });
382
393
  assert.deepEqual(guest.rows.value, [{ text: "Buy milk", done: false }]);
394
+ // @ts-expect-error A viewer has no add: TypeScript refuses it, and so does the provider.
383
395
  await assert.rejects(guest.add({ text: "Not allowed" }), (error) => isChordError(error, "not_in_variant"));
384
396
  await assert.rejects(owner.add({ text: " " }), (error) => isChordError(error, "invalid"));
385
397
  ```
386
398
 
387
399
  The owner gets `editor`; the guest gets `viewer`. Both see the same milk
388
- todo. Calling the guest's `add` rejects with `not_in_variant`, even though
389
- TypeScript exposes methods from all variants on a handle. Whitespace-only text
400
+ todo. A reference is typed as the union of its variants: `add` is reachable only
401
+ after narrowing on `variant` (`assert.ok(owner.variant === "editor")` does that).
402
+ The guest's `add` is refused by TypeScript, and, called anyway, rejects with
403
+ `not_in_variant`. Whitespace-only text
390
404
  rejects with `invalid`: it is a string, as the schema demands, but it breaks a
391
405
  rule only the method knows. Use `invalid` for bad input and `forbidden` only
392
406
  for callers who may not do something.
@@ -394,58 +408,67 @@ for callers who may not do something.
394
408
  > **Why:** A variant is not all of authorization. The factory checks which
395
409
  > list may be opened; the method checks the operation's arguments. Production
396
410
  > code must also enforce grants that can change. Use
397
- > `host.invalidatePrincipal(match)` to revoke matching records; held handles
398
- > become `gone`. A UI watches `onStatus` and gets a new handle when appropriate.
411
+ > `host.invalidatePrincipal(match)` to revoke matching objects; held references
412
+ > become `gone`. A UI watches `onStatus` and gets a new reference when appropriate.
399
413
  > Guest-provided names or message text must never establish identity.
400
414
 
401
415
  ## 6. Reach the list from another process
402
416
 
403
417
  A browser cannot call a host-only contract. `remote` contracts describe only
404
418
  schema-checked state and methods that can travel as JSON. Our `Todos` view is
405
- remote; the underlying `TodoStore` and database stay local.
419
+ remote; the underlying `TodoStore` and database stay local. A caller never
420
+ names a host: `browser.get(Todos, …)` goes to `server` because the contract
421
+ says so, and the server refuses to serve, or to be asked for, a type it is not
422
+ the place of. A type that lives on one host per session says
423
+ `host: hostFamily("session:", "session")`: its input's `session` field picks
424
+ the host (`{ session: "S1" }` lives on `session:S1`).
406
425
 
407
426
  A **link** carries ordered messages between hosts. A memory link runs both ends
408
427
  in one test process, but uses the remote protocol. The accepting host stamps
409
428
  the caller's principal from its authenticator, not from method arguments or a
410
- client's claimed identity.
429
+ client's claimed identity. The connecting side names the principal its parent's
430
+ own requests act as (`upstream: { link, principal }`, or `connect(link, {
431
+ principal })`).
411
432
 
412
433
  <!-- example:06 -->
413
434
  ```ts
414
- const server = createHost({ name: "server" });
435
+ const server = createHost<Principal>({ self: "server", name: "server" });
415
436
  server.serve(TodoStore, todoFactory);
416
437
  serveViews(server);
417
438
  const [down, up] = memoryLinkPair();
418
439
  server.accept(down, { principal: "owner", kind: "client" });
419
- const browser = createHost({ name: "browser", kind: "client", upstream: up });
420
- const todos = browser.get(Todos, { list: "family" }, { at: "server" });
421
- const hydrated = Promise.withResolvers<void>();
440
+ const browser = createHost({
441
+ self: "browser",
442
+ name: "browser",
443
+ kind: "client",
444
+ upstream: { link: up, principal: "server" },
445
+ });
446
+ const todos = await browser.get(Todos, { list: "family" });
447
+ assert.ok(todos.variant === "editor");
448
+ assert.deepEqual(todos.rows.value, []);
422
449
  const updated = Promise.withResolvers<void>();
423
450
  const stop = todos.rows.subscribe((rows) => {
424
- if (rows !== undefined) hydrated.resolve();
425
- if (rows?.length === 1) updated.resolve();
451
+ if (rows.length === 1) updated.resolve();
426
452
  });
427
- await todos.ready;
428
- await hydrated.promise;
429
- assert.deepEqual(todos.rows.value, []);
430
453
  await browser.root((ctx) => todos.add({ text: "Buy milk" }, ctx));
431
454
  await updated.promise;
432
455
  assert.deepEqual(todos.rows.value, [{ text: "Buy milk", done: false }]);
433
456
  // Deliberately bypass TypeScript to simulate a malformed caller.
434
457
  await assert.rejects(todos.add({ text: 123 } as never), (error) => isChordError(error, "invalid"));
435
458
  stop();
436
- assert.equal(todos.rows.value, undefined);
459
+ // A held reference keeps its state without listeners.
460
+ assert.deepEqual(todos.rows.value, [{ text: "Buy milk", done: false }]);
437
461
  todos.release();
438
462
  ```
439
463
 
440
464
  The browser sees an empty snapshot, then the new milk todo. `ready` means
441
- the remote handle has bound to a live object; it does **not** guarantee that
442
- subscribed state has arrived. That is why we separately await `hydrated` and
443
- `updated`. A remote readable starts at `undefined` and returns to
444
- `undefined` when its last subscriber leaves.
465
+ the remote reference has bound to a live object **and** has its state: every
466
+ state member of its variant has a snapshot. A held reference stays subscribed,
467
+ with or without listeners, until it is released.
445
468
 
446
469
  What crosses the wire: the input and consumer's contract description, the
447
- provider's shape (variant and members), method arguments and results, and
448
- subscribed state. State starts with a **snapshot**, the whole current value;
470
+ provider's shape (variant and members), method arguments and results, and the
471
+ state of the reference's variant. State starts with a **snapshot**, the whole current value;
449
472
  subsequent **deltas** describe changes. Rebinds and reloads can send a
450
473
  `reset`, a fresh whole value. Functions, resource connections, and cell
451
474
  instances do not cross the wire.
@@ -479,35 +502,30 @@ const Activity = local("tutorial.activity", {
479
502
  input: Type.Object({}),
480
503
  members: { seen: Type.Number() },
481
504
  });
482
- const host = createHost();
505
+ const host = createHost({ self: "server" });
483
506
  host.serve(TodoStore, todoFactory);
484
507
  const observed = Promise.withResolvers<void>();
485
508
  const seeded = Promise.withResolvers<void>();
486
509
  const stopped = Promise.withResolvers<void>();
487
510
  host.serve(Activity, (_input, _principal, self) => {
488
511
  const todos = self.get(TodoStore, { list: "family" });
489
- const state = self.state({ seen: cell(Type.Number(), 0) });
490
- self.listen({
491
- rows: listener(todos.rows, (rows, ctx) => {
492
- state.seen.replace(ctx, rows?.length ?? 0);
493
- if (rows?.length === 1) observed.resolve();
494
- }),
512
+ const seen = self.state("seen", Type.Number(), 0);
513
+ self.on("rows", todos.rows, (rows, ctx) => {
514
+ seen.replace(ctx, rows?.length ?? 0);
515
+ if (rows?.length === 1) observed.resolve();
495
516
  });
496
- self.tasks({
497
- seed: async (ctx) => {
498
- await todos.add({ text: "Review the list" }, ctx);
499
- seeded.resolve();
500
- await new Promise<void>((resolve) => {
501
- if (ctx.signal.aborted) resolve();
502
- else ctx.signal.addEventListener("abort", () => resolve(), { once: true });
503
- });
504
- stopped.resolve();
505
- },
517
+ self.task(async (ctx) => {
518
+ await todos.add({ text: "Review the list" }, ctx);
519
+ seeded.resolve();
520
+ await new Promise<void>((resolve) => {
521
+ if (ctx.signal.aborted) resolve();
522
+ else ctx.signal.addEventListener("abort", () => resolve(), { once: true });
523
+ });
524
+ stopped.resolve();
506
525
  });
507
- return { seen: state.seen };
526
+ return { seen };
508
527
  });
509
- const activity = host.get(Activity, {});
510
- await activity.ready;
528
+ const activity = await host.get(Activity, {});
511
529
  await Promise.all([seeded.promise, observed.promise]);
512
530
  assert.equal(activity.seen.value, 1);
513
531
  activity.release();
@@ -518,13 +536,19 @@ The final `seen` is `1`. Each listener event receives a fresh root's
518
536
  `ctx`; its write belongs to that short call tree. A task is not one enormous
519
537
  root: Chord method calls made with its task context start fresh short roots.
520
538
  We pass that context into `todos.add` so cancellation follows the task.
521
- Releasing the activity handle aborts its task and resolves `stopped`.
522
-
523
- Listeners can also subscribe to a resource's event stream with
524
- `listener(resource, select, fn)`: `select` gets the opened resource and
525
- returns something with `subscribe`. Kept subscriptions survive reload while
526
- the resource and selector remain unchanged; the callback switches to new code.
527
- A selector that captures changing configuration should get a new listener name.
539
+ Releasing the activity reference aborts its task and resolves `stopped`.
540
+
541
+ `self.on(name, source, handler)` declares a listener; `self.task(fn)` a task.
542
+ A listener on state, like `rows` here, gets the current value when it starts,
543
+ then every change. It gets the current value again whenever a reload replaced
544
+ its source (a derivation is new at every reload), so its handler must
545
+ reconcile (`seen` is set from the rows, not counted up). Listeners can also
546
+ subscribe to a resource's event stream with `self.on(name, resource, select,
547
+ handler)`: `select` gets the opened resource and returns something with
548
+ `subscribe`; an event stream delivers no current value. Kept subscriptions
549
+ survive reload while the resource and selector remain unchanged; the handler
550
+ switches to new code. A selector that captures changing configuration should
551
+ get a new listener name. Tasks have no names: a reload restarts them all.
528
552
 
529
553
  > **Why:** Do not wait for a person, sleep for minutes, or run an endless loop
530
554
  > inside a method. That pins a root and can block state migration. Store a
@@ -534,13 +558,14 @@ A selector that captures changing configuration should get a new listener name.
534
558
  ## 8. Package features as plugins
535
559
 
536
560
  Now a plugin can contribute a menu action without editing a central switch.
537
- A **registry** is a named table of contributions. A **facet** declares what a
538
- plugin serves on one kind of host. A plugin can have server, worker, and browser
561
+ A **registry** is a named table of contributions. A **facet** is a plugin's code
562
+ for one kind of host: it serves object types. A plugin can have server, worker, and browser
539
563
  facets, each installing that part of the feature.
540
564
 
541
- Registrations belong to eager objects: **eager** means the host creates and
542
- holds the object at startup, even if no screen asks for it. That keeps our menu
543
- contribution present. Lazy objects still activate only when held.
565
+ Registrations belong to objects of **lifetime `"host"`**: the host creates such
566
+ an object when its facet loads and keeps it until the host closes, even if no
567
+ screen asks for it. That keeps our menu contribution present. Objects of the
568
+ default lifetime, `"referenced"`, are still created only when referenced.
544
569
 
545
570
  <!-- example:08 -->
546
571
  ```ts
@@ -562,20 +587,20 @@ const menuFacet: Facet = {
562
587
  self.register(Actions, {
563
588
  addSample: async (ctx) => {
564
589
  await todos.add({ text: "Try plugins" }, ctx);
565
- return todos.openCount.value!;
590
+ return todos.openCount.value;
566
591
  },
567
592
  });
568
593
  return {};
569
594
  },
570
- { eager: true },
595
+ { lifetime: "host" },
571
596
  );
572
597
  click = () => api.root((ctx) => api.registry(Actions).get("addSample", ctx)!(ctx));
573
598
  },
574
599
  { uses: [TodoStore] },
575
600
  ),
576
601
  };
577
- const host = createHost();
578
- await host.load([menuFacet, storeFacet], { awaitEager: false });
602
+ const host = createHost({ self: "server" });
603
+ await host.load([menuFacet, storeFacet], { awaitHostObjects: false });
579
604
  await host.ready();
580
605
  assert.equal(await click!(), 1);
581
606
  assert.equal(host.registry(Actions).table().size, 1);
@@ -583,7 +608,7 @@ assert.equal(host.registry(Actions).table().size, 1);
583
608
 
584
609
  Clicking returns `1`, and the registry has one entry. The menu's dependency
585
610
  can come from a later facet in the load batch: `load` installs the whole
586
- batch before waiting for eager activation. The facet declares `uses` so
611
+ batch before waiting for host objects to activate. The facet declares `uses` so
587
612
  reload can also check the contract it consumes.
588
613
 
589
614
  The facet API supplies `serve`, `name`, `root`, and `registry`.
@@ -593,16 +618,16 @@ root's code version. Without `ctx`, lookup uses the current table. Duplicate
593
618
  keys fail by default; `subscribe` can bridge tables to an imperative consumer.
594
619
 
595
620
  Normally use `await host.load(facets)` (or `await host.install(facet)`),
596
- which waits for those eager objects. This example explicitly defers that barrier
597
- with `awaitEager: false`, then awaits `host.ready()`. That waits for a
598
- snapshot of the host's eager holds, not for task completion or later loads.
621
+ which waits for those host objects. This example explicitly defers that barrier
622
+ with `awaitHostObjects: false`, then awaits `host.ready()`. That waits for a
623
+ snapshot of the host's objects, not for task completion or later loads.
599
624
  Failures reject the readiness promise; abandoning startup requires closing the
600
625
  host, because installation is not a rollback transaction.
601
626
 
602
627
  > **Tip:** Use deferred readiness only when needed. A worker may have to await
603
- > an independent local bootstrap handle, announce control readiness so its
628
+ > an independent local bootstrap reference, announce control readiness so its
604
629
  > parent admits its link, and only then await `host.ready()`. Waiting for
605
- > upstream-dependent eager objects before admission would deadlock.
630
+ > upstream-dependent host objects before admission would deadlock.
606
631
 
607
632
  For real plugin files, default-export `defineFacet(...)`. Node hosts configure
608
633
  `createHost({ loader: nodeLoader })`, with `nodeLoader` imported from
@@ -613,19 +638,19 @@ copies in memory, so repeated loading has that limitation.
613
638
 
614
639
  ## 9. Change running code without losing the list
615
640
 
616
- A handle is useful precisely because callers keep it while implementations
617
- change. An **epoch** identifies one committed set of code and registry tables.
618
- A root keeps the epoch it started with. New roots use the latest one.
641
+ A reference is useful precisely because callers keep it while implementations
642
+ change. An **code version** identifies one committed set of code and registry tables.
643
+ A root keeps the code version it started with. New roots use the latest one.
619
644
 
620
645
  ### Compatible code change: no waiting
621
646
 
622
647
  Give the same named facet a new factory. It changes the label but keeps the
623
- cell declarations. This test deliberately pauses one caller to show what an
648
+ cell declaration. This test deliberately pauses one caller to show what an
624
649
  old root sees; application methods should not pause like this.
625
650
 
626
651
  <!-- example:09-compatible -->
627
652
  ```ts
628
- const host = createHost();
653
+ const host = createHost({ self: "server" });
629
654
  const facet = (label: string): Facet => ({
630
655
  name: "todos",
631
656
  setup: (api) =>
@@ -635,20 +660,20 @@ const facet = (label: string): Facet => ({
635
660
  })),
636
661
  });
637
662
  await host.load([facet("Original")]);
638
- const todos = host.get(TodoStore, { list: "family" });
663
+ const todos = await host.get(TodoStore, { list: "family" });
639
664
  await todos.add({ text: "Buy milk" });
640
665
  const entered = Promise.withResolvers<void>();
641
666
  const resume = Promise.withResolvers<void>();
642
667
  const oldRoot = host.root(async (ctx) => {
643
- const first = await todos.label(null, ctx);
668
+ const first = await todos.label(ctx);
644
669
  entered.resolve();
645
670
  await resume.promise; // Test-only pause: pin an old caller during reload.
646
- return [first, await todos.label(null, ctx)];
671
+ return [first, await todos.label(ctx)];
647
672
  });
648
673
  await entered.promise;
649
674
  const compatible = await host.reload([facet("Updated")]);
650
675
  assert.deepEqual(compatible.gated, []);
651
- assert.equal(await todos.label(null), "Updated");
676
+ assert.equal(await todos.label(), "Updated");
652
677
  resume.resolve();
653
678
  assert.deepEqual(await oldRoot, ["Original", "Original"]);
654
679
  assert.equal(todos.rows.value?.[0]?.text, "Buy milk");
@@ -661,9 +686,10 @@ survives. Compatible code changes do not wait for that old root to finish.
661
686
  ### State change: bump its version and migrate
662
687
 
663
688
  Suppose a new policy converts existing todo text to uppercase. The schema is
664
- unchanged, but the data's meaning changes. We still bump the **state version**.
665
- This version belongs to stored cells, independently of the contract version.
666
- A **migration** converts an old cell value to the new representation.
689
+ unchanged, but the data's meaning changes. We still bump the cell's **state
690
+ version** (`self.state`'s `version` option, default 1). It belongs to one cell,
691
+ independently of the contract version. A **migration** (`migrate`) converts an
692
+ old value of that cell to the new representation.
667
693
 
668
694
  <!-- example:09-migrate -->
669
695
  ```ts
@@ -673,17 +699,16 @@ const migrated = await host.reload([
673
699
  {
674
700
  name: "todos",
675
701
  setup: (api) =>
676
- api.serve(TodoStore, todoFactory, {
677
- state: {
702
+ api.serve(
703
+ TodoStore,
704
+ todoStore({
678
705
  version: 2,
679
- migrate: {
680
- rows: (old, from) => {
681
- assert.equal(from, 1);
682
- return (old as Static<typeof Rows>).map((todo) => ({ ...todo, text: todo.text.toUpperCase() }));
683
- },
706
+ migrate: (old, from) => {
707
+ assert.equal(from, 1);
708
+ return (old as Static<typeof Rows>).map((todo) => ({ ...todo, text: todo.text.toUpperCase() }));
684
709
  },
685
- },
686
- }),
710
+ }),
711
+ ),
687
712
  },
688
713
  ]);
689
714
  assert.equal(migrated.gated.length, 1);
@@ -693,24 +718,24 @@ stopEvents();
693
718
  ```
694
719
 
695
720
  The migration receives the old rows and their state version, `1`. It returns
696
- new rows; the handle now reads `[{ text: "BUY MILK", done: false }]`.
721
+ new rows; the reference now reads `[{ text: "BUY MILK", done: false }]`.
697
722
  Subscribers get a `reset` event containing the migrated value. The event
698
723
  assertion sees an initial `snap`, then that `reset`.
699
- One record is gated. A **gate** waits for calls already inside that record to
724
+ One object is gated. A **gate** waits for calls already inside that object to
700
725
  finish before migrating a private copy, validating it, and committing. Old
701
726
  roots that had not entered it cannot later use old code over migrated data:
702
727
  they get `upgrade_conflict` and should retry in a new root.
703
728
 
704
- Changing a cell's schema also requires a state-version bump and a migration
705
- whose result fits the new schema. A change of meaning with the same schema,
729
+ Changing a cell's schema also requires a version bump, and a migration unless
730
+ the old value already fits the new schema. A change of meaning with the same schema,
706
731
  like this example, cannot be detected automatically. Real migrations must
707
- handle each stored version they support, not assume every installation is
732
+ reference each stored version they support, not assume every installation is
708
733
  exactly one version behind.
709
734
 
710
735
  ### Contract change: keep an adapter for old callers
711
736
 
712
737
  Rename `label` to `heading`. That is a breaking contract change, so use
713
- contract version 2. The provider's `accepts[1]` adapter builds the behavior
738
+ contract version 2. The provider's `accepts[1]` adapter builds the implementation
714
739
  that version-1 consumers still expect.
715
740
 
716
741
  <!-- example:09-contract -->
@@ -720,7 +745,7 @@ const { label: _label, ...members } = TodoStore.def.members;
720
745
  const RenamedStore = local("tutorial.todos", {
721
746
  version: 2,
722
747
  input: TodoStore.def.input,
723
- members: { ...members, heading: method(Type.Null(), Type.String()) },
748
+ members: { ...members, heading: method(undefined, Type.String()) },
724
749
  });
725
750
  await host.reload([
726
751
  {
@@ -729,11 +754,10 @@ await host.reload([
729
754
  api.serve(
730
755
  RenamedStore,
731
756
  (input, principal, self) => {
732
- const { label, ...behavior } = todoFactory(input, principal, self);
733
- return { ...behavior, heading: label };
757
+ const { label, ...implementation } = todoStore({ version: 2 })(input, principal, self);
758
+ return { ...implementation, heading: label };
734
759
  },
735
760
  {
736
- state: { version: 2 },
737
761
  accepts: {
738
762
  1: (current) => ({ ...current, label: current.heading }),
739
763
  },
@@ -741,19 +765,20 @@ await host.reload([
741
765
  ),
742
766
  },
743
767
  ]);
744
- assert.equal(await todos.label(null), "Todos for family");
745
- const newer = host.get(RenamedStore, { list: "family" });
746
- assert.equal(await newer.heading(null), "Todos for family");
768
+ assert.equal(await todos.label(), "Todos for family");
769
+ const newer = await host.get(RenamedStore, { list: "family" });
770
+ assert.equal(await newer.heading(), "Todos for family");
747
771
  assert.equal(todos.rows.value?.[0]?.text, "BUY MILK");
748
772
  ```
749
773
 
750
- The old handle's `label` and the new handle's `heading` both return
774
+ The old reference's `label` and the new reference's `heading` both return
751
775
  `"Todos for family"`. Both still reach the migrated rows. The adapter maps
752
- behaviors, not handles; forwarding the existing method preserves its context.
753
- We keep state version 2 here so this code-only change does not repeat migration.
776
+ implementations, not references; forwarding the existing method preserves its context.
777
+ We keep the cell at version 2 here so this code-only change does not repeat
778
+ migration.
754
779
 
755
780
  > **Why:** Contract versions protect callers; state versions protect stored
756
- > data; resource revisions control reopening. Do not use one number for all
781
+ > data; resource versions control reopening. Do not use one number for all
757
782
  > three. Additive compatible contracts can keep their version; breaking shape
758
783
  > or meaning changes need a contract bump and adapters for old consumers.
759
784
  > Reload prepares candidates first; preparation failure leaves the old code
@@ -762,13 +787,13 @@ We keep state version 2 here so this code-only change does not repeat migration.
762
787
 
763
788
  ## 10. Test the objects, not a whole deployment
764
789
 
765
- Most behavior needs only a bare host. Use real factories for state tests, and a
790
+ Most implementation needs only a bare host. Use real factories for state tests, and a
766
791
  fake facet when you want to test delegation without a real database or service.
767
792
  Here the fake records what the editing view sends to the store.
768
793
 
769
794
  <!-- example:10 -->
770
795
  ```ts
771
- const host = createHost({ dev: true });
796
+ const host = createHost<Principal>({ name: "server", self: "server", dev: true });
772
797
  const received: string[] = [];
773
798
  const fake: Facet = {
774
799
  name: "fake-store",
@@ -777,14 +802,14 @@ const fake: Facet = {
777
802
  ...todoFactory(input, principal, self),
778
803
  add: ({ text }) => {
779
804
  received.push(text);
780
- return null;
781
805
  },
782
806
  }));
783
807
  }),
784
808
  };
785
809
  await host.load([fake]);
786
810
  serveViews(host);
787
- const owner = host.get(Todos, { list: "family" }, { principal: "owner" });
811
+ const owner = await host.get(Todos, { list: "family" }, { principal: "owner" });
812
+ assert.ok(owner.variant === "editor");
788
813
  await owner.add({ text: "Test delegation" });
789
814
  assert.deepEqual(received, ["Test delegation"]);
790
815
  await assert.rejects(owner.add({ text: "" }), (error) => isChordError(error, "invalid"));
@@ -793,43 +818,42 @@ await assert.rejects(owner.add({ text: "Too late" }), (error) => isChordError(er
793
818
  ```
794
819
 
795
820
  The fake received `["Test delegation"]`. Empty text failed before reaching
796
- the fake. The released handle rejects further calls with `released`.
821
+ the fake. The released reference rejects further calls with `released`.
797
822
  `isChordError(error, code)` checks a stable error code; avoid testing only a
798
823
  message's wording.
799
824
 
800
825
  Our other tests exercise the real cells (section 3), resource cleanup (4),
801
- principal variants (5), a client over a memory link (6), and callers held through
802
- reload (9). For remote state tests, await the value you need, not just
803
- `handle.ready`. Always unsubscribe and close hosts; leaked holds can hide
826
+ principal variants (5), a client over a memory link (6), and callers kept through
827
+ reload (9). After an awaited `get` the state is there; for later changes, await
828
+ the value you need. Always unsubscribe and close hosts; leaked references can hide
804
829
  lifetime bugs or leave background tasks running.
805
830
 
806
831
  > **Tip:** A fake must honor the contract it serves. This fake delegates all
807
832
  > ordinary members to `todoFactory` and replaces only `add`; it deliberately
808
- > records calls rather than updating rows. Do not confuse that with persistence.
833
+ > objects calls rather than updating rows. Do not confuse that with persistence.
809
834
 
810
835
  ## 11. Keep these habits
811
836
 
812
- A temporary call handle should not accidentally become a permanent hold. A
837
+ A temporary call reference should not accidentally become a permanent one. A
813
838
  screen should not lose its data merely because a button click finishes. Use
814
839
  `ctx.get` for the first lifetime and `host.get` for the second.
815
840
 
816
841
  <!-- example:11 -->
817
842
  ```ts
818
- const host = createHost();
843
+ const host = createHost({ self: "server" });
819
844
  host.serve(TodoStore, todoFactory);
820
- const screen = host.get(TodoStore, { list: "family" });
821
- await screen.ready;
845
+ const screen = await host.get(TodoStore, { list: "family" });
822
846
  await host.root(async (ctx) => {
823
- const temporary = ctx.get(TodoStore, { list: "family" });
847
+ const temporary = await ctx.get(TodoStore, { list: "family" });
824
848
  await temporary.add({ text: "Keep roots short" }, ctx);
825
- }); // The temporary handle is released here; screen still holds the store.
849
+ }); // The temporary reference is released here; screen still holds the store.
826
850
  assert.equal(screen.openCount.value, 1);
827
- screen.release(); // No hold remains; without persistence, disposal loses these rows.
851
+ screen.release(); // No reference remains; without persistence, disposal loses these rows.
828
852
  ```
829
853
 
830
854
  The screen still sees count `1` after the root finishes, because it owns a
831
- separate hold. When the screen releases, no hold remains. This tutorial's cells
832
- are in memory: reload preserves a live record, but disposal or process restart
855
+ separate reference. When the screen releases it, no reference remains. This tutorial's cells
856
+ are in memory: reload preserves a live object, but disposal or process restart
833
857
  loses its rows. Real persistence belongs in a database resource, with an
834
858
  explicit load/save policy.
835
859
 
@@ -841,17 +865,18 @@ explicit load/save policy.
841
865
  can erase fields your code does not know about.
842
866
  - Declare dependencies in a synchronous factory, not during methods: activation
843
867
  and reload need a complete declaration list.
844
- - Use `ctx.get` for temporary method dependencies: their holds end with the call.
868
+ - Use `ctx.get` for temporary method dependencies: their references end with the call.
845
869
  - Pass `ctx` into nested calls: cancellation and code versions must stay together.
846
870
  - Keep roots short and tasks cancellable: migration and shutdown need work to settle.
847
871
  - Check grants and arguments, not just variants: available methods are not a full policy.
848
872
  - Stamp principals at trusted ingress, not from client payloads: names are not identities.
849
- - Subscribe before expecting remote state: readiness is not snapshot hydration.
850
- - Release handles and unsubscribe: otherwise unused objects remain held or observed.
851
- - Bump state versions, contract versions, and resource revisions for their own changes:
873
+ - Await `get` before reading state: a reference you hold is live and hydrated, a
874
+ loading one (`host.hold`) throws `not_ready`.
875
+ - Release references and unsubscribe: otherwise unused objects remain referenced or observed.
876
+ - Bump state versions, contract versions, and resource versions for their own changes:
852
877
  each protects a different boundary.
853
- - Await eager readiness before reporting startup success: declared is not yet live.
854
- - Keep hold dependencies acyclic: an object cannot wait for itself through another object.
878
+ - Await host object readiness before reporting startup success: declared is not yet live.
879
+ - Keep reference dependencies acyclic: an object cannot wait for itself through another object.
855
880
 
856
881
  ### Imports and running the examples
857
882
 
@@ -869,26 +894,23 @@ import { type Static, Type } from "typebox";
869
894
  import { it, onTestFinished } from "vitest";
870
895
  import {
871
896
  type Ctx,
872
- cell,
873
897
  createHost,
874
898
  defineFacet,
875
899
  defineRegistry,
876
- derivation,
877
900
  type Facet,
878
901
  type FacetApi,
879
902
  type Factory,
880
903
  forbidden,
881
904
  type Host,
882
- hostPrincipal,
905
+ hostNamed,
883
906
  invalid,
884
907
  isChordError,
885
- listener,
886
908
  local,
887
909
  memoryLinkPair,
888
910
  method,
889
911
  opaque,
890
912
  remote,
891
- resource,
913
+ type StateOptions,
892
914
  } from "../src/index.ts";
893
915
  ```
894
916