@earendil-works/chord 1.0.4 → 1.2.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 (186) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +77 -194
  3. package/dist/cell.d.ts +101 -0
  4. package/dist/cell.d.ts.map +1 -0
  5. package/dist/cell.js +316 -0
  6. package/dist/cell.js.map +1 -0
  7. package/dist/cluster.d.ts +26 -0
  8. package/dist/cluster.d.ts.map +1 -0
  9. package/dist/cluster.js +214 -0
  10. package/dist/cluster.js.map +1 -0
  11. package/dist/connection.d.ts +157 -0
  12. package/dist/connection.d.ts.map +1 -0
  13. package/dist/connection.js +757 -0
  14. package/dist/connection.js.map +1 -0
  15. package/dist/context/index.d.ts +12 -1
  16. package/dist/context/index.d.ts.map +1 -1
  17. package/dist/context/index.js.map +1 -1
  18. package/dist/contract.d.ts +133 -0
  19. package/dist/contract.d.ts.map +1 -0
  20. package/dist/contract.js +157 -0
  21. package/dist/contract.js.map +1 -0
  22. package/dist/declare.d.ts +70 -0
  23. package/dist/declare.d.ts.map +1 -0
  24. package/dist/declare.js +22 -0
  25. package/dist/declare.js.map +1 -0
  26. package/dist/delta/apply-immutable-trusted.js.map +1 -1
  27. package/dist/delta/diff.d.ts +1 -1
  28. package/dist/delta/diff.d.ts.map +1 -1
  29. package/dist/delta/diff.js.map +1 -1
  30. package/dist/delta/index.d.ts +2 -2
  31. package/dist/delta/index.d.ts.map +1 -1
  32. package/dist/delta/index.js +41 -0
  33. package/dist/delta/index.js.map +1 -1
  34. package/dist/delta/revision-validator.d.ts +1 -1
  35. package/dist/delta/revision-validator.d.ts.map +1 -1
  36. package/dist/delta/revision-validator.js.map +1 -1
  37. package/dist/delta/tracker.d.ts +1 -1
  38. package/dist/delta/tracker.d.ts.map +1 -1
  39. package/dist/delta/tracker.js +40 -4
  40. package/dist/delta/tracker.js.map +1 -1
  41. package/dist/errors.d.ts +27 -0
  42. package/dist/errors.d.ts.map +1 -0
  43. package/dist/errors.js +44 -0
  44. package/dist/errors.js.map +1 -0
  45. package/dist/host.d.ts +254 -0
  46. package/dist/host.d.ts.map +1 -0
  47. package/dist/host.js +795 -0
  48. package/dist/host.js.map +1 -0
  49. package/dist/index.d.ts +17 -10
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +8 -8
  52. package/dist/index.js.map +1 -1
  53. package/dist/json.d.ts +9 -1
  54. package/dist/json.d.ts.map +1 -1
  55. package/dist/json.js.map +1 -1
  56. package/dist/link.d.ts +72 -0
  57. package/dist/link.d.ts.map +1 -0
  58. package/dist/link.js +169 -0
  59. package/dist/link.js.map +1 -0
  60. package/dist/loader.d.ts +4 -0
  61. package/dist/loader.d.ts.map +1 -0
  62. package/dist/loader.js +68 -0
  63. package/dist/loader.js.map +1 -0
  64. package/dist/manager.d.ts +115 -0
  65. package/dist/manager.d.ts.map +1 -0
  66. package/dist/manager.js +319 -0
  67. package/dist/manager.js.map +1 -0
  68. package/dist/node.d.ts +3 -4
  69. package/dist/node.d.ts.map +1 -1
  70. package/dist/node.js +3 -2
  71. package/dist/node.js.map +1 -1
  72. package/dist/publish.d.ts +5 -0
  73. package/dist/publish.d.ts.map +1 -0
  74. package/dist/publish.js +40 -0
  75. package/dist/publish.js.map +1 -0
  76. package/dist/record.d.ts +320 -0
  77. package/dist/record.d.ts.map +1 -0
  78. package/dist/record.js +1292 -0
  79. package/dist/record.js.map +1 -0
  80. package/dist/registry.d.ts +25 -0
  81. package/dist/registry.d.ts.map +1 -0
  82. package/dist/registry.js +6 -0
  83. package/dist/registry.js.map +1 -0
  84. package/dist/reload.d.ts +118 -0
  85. package/dist/reload.d.ts.map +1 -0
  86. package/dist/reload.js +589 -0
  87. package/dist/reload.js.map +1 -0
  88. package/dist/remote.d.ts +70 -0
  89. package/dist/remote.d.ts.map +1 -0
  90. package/dist/remote.js +308 -0
  91. package/dist/remote.js.map +1 -0
  92. package/dist/schema.d.ts +21 -0
  93. package/dist/schema.d.ts.map +1 -0
  94. package/dist/schema.js +228 -0
  95. package/dist/schema.js.map +1 -0
  96. package/dist/state.d.ts +92 -0
  97. package/dist/state.d.ts.map +1 -0
  98. package/dist/{services/state.js → state.js} +28 -138
  99. package/dist/state.js.map +1 -0
  100. package/dist/types.d.ts +62 -262
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/types.js.map +1 -1
  103. package/dist/websocket.d.ts +16 -0
  104. package/dist/websocket.d.ts.map +1 -0
  105. package/dist/websocket.js +15 -0
  106. package/dist/websocket.js.map +1 -0
  107. package/dist/wire.d.ts +135 -0
  108. package/dist/wire.d.ts.map +1 -0
  109. package/dist/wire.js +112 -0
  110. package/dist/wire.js.map +1 -0
  111. package/docs/guide.md +750 -0
  112. package/docs/spec.md +261 -0
  113. package/docs/tutorial.md +909 -0
  114. package/package.json +10 -11
  115. package/src/delta/README.md +5 -3
  116. package/dist/api.d.ts +0 -20
  117. package/dist/api.d.ts.map +0 -1
  118. package/dist/api.js +0 -77
  119. package/dist/api.js.map +0 -1
  120. package/dist/bundler.d.ts +0 -6
  121. package/dist/bundler.d.ts.map +0 -1
  122. package/dist/bundler.js +0 -3
  123. package/dist/bundler.js.map +0 -1
  124. package/dist/facets/host.d.ts +0 -12
  125. package/dist/facets/host.d.ts.map +0 -1
  126. package/dist/facets/host.js +0 -763
  127. package/dist/facets/host.js.map +0 -1
  128. package/dist/facets/loader.d.ts +0 -3
  129. package/dist/facets/loader.d.ts.map +0 -1
  130. package/dist/facets/loader.js +0 -5
  131. package/dist/facets/loader.js.map +0 -1
  132. package/dist/node/bundle-loader.d.ts +0 -30
  133. package/dist/node/bundle-loader.d.ts.map +0 -1
  134. package/dist/node/bundle-loader.js +0 -377
  135. package/dist/node/bundle-loader.js.map +0 -1
  136. package/dist/node/bundle.d.ts +0 -26
  137. package/dist/node/bundle.d.ts.map +0 -1
  138. package/dist/node/bundle.js +0 -183
  139. package/dist/node/bundle.js.map +0 -1
  140. package/dist/node/manifest.d.ts +0 -36
  141. package/dist/node/manifest.d.ts.map +0 -1
  142. package/dist/node/manifest.js +0 -6
  143. package/dist/node/manifest.js.map +0 -1
  144. package/dist/node/package.d.ts +0 -15
  145. package/dist/node/package.d.ts.map +0 -1
  146. package/dist/node/package.js +0 -199
  147. package/dist/node/package.js.map +0 -1
  148. package/dist/services/consumer.d.ts +0 -11
  149. package/dist/services/consumer.d.ts.map +0 -1
  150. package/dist/services/consumer.js +0 -585
  151. package/dist/services/consumer.js.map +0 -1
  152. package/dist/services/errors.d.ts +0 -8
  153. package/dist/services/errors.d.ts.map +0 -1
  154. package/dist/services/errors.js +0 -22
  155. package/dist/services/errors.js.map +0 -1
  156. package/dist/services/handle.d.ts +0 -15
  157. package/dist/services/handle.d.ts.map +0 -1
  158. package/dist/services/handle.js +0 -101
  159. package/dist/services/handle.js.map +0 -1
  160. package/dist/services/instances.d.ts +0 -26
  161. package/dist/services/instances.d.ts.map +0 -1
  162. package/dist/services/instances.js +0 -140
  163. package/dist/services/instances.js.map +0 -1
  164. package/dist/services/loopback.d.ts +0 -5
  165. package/dist/services/loopback.d.ts.map +0 -1
  166. package/dist/services/loopback.js +0 -15
  167. package/dist/services/loopback.js.map +0 -1
  168. package/dist/services/provider.d.ts +0 -37
  169. package/dist/services/provider.d.ts.map +0 -1
  170. package/dist/services/provider.js +0 -521
  171. package/dist/services/provider.js.map +0 -1
  172. package/dist/services/state-codec.d.ts +0 -15
  173. package/dist/services/state-codec.d.ts.map +0 -1
  174. package/dist/services/state-codec.js +0 -133
  175. package/dist/services/state-codec.js.map +0 -1
  176. package/dist/services/state-internals.d.ts +0 -13
  177. package/dist/services/state-internals.d.ts.map +0 -1
  178. package/dist/services/state-internals.js +0 -10
  179. package/dist/services/state-internals.js.map +0 -1
  180. package/dist/services/state.d.ts +0 -28
  181. package/dist/services/state.d.ts.map +0 -1
  182. package/dist/services/state.js.map +0 -1
  183. package/dist/services/wire.d.ts +0 -63
  184. package/dist/services/wire.d.ts.map +0 -1
  185. package/dist/services/wire.js +0 -186
  186. package/dist/services/wire.js.map +0 -1
@@ -0,0 +1,909 @@
1
+ # Build a shared todo list with Chord
2
+
3
+ ## 1. What problem does Chord solve?
4
+
5
+ A todo list has data, operations on that data, and several people looking at it.
6
+ You could hand-write object creation, subscription cleanup, messages between
7
+ processes, and code replacement. Chord connects these pieces for you.
8
+
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.
13
+
14
+ ```text
15
+ browser host server host
16
+ handle + subscribed rows -- link --> Todos view (owner or guest)
17
+ |
18
+ handle
19
+ v
20
+ TodoStore: rows + methods
21
+ ```
22
+
23
+ We will start inside one host, add observable state, then share it with a
24
+ browser. Finally we will add background work, plugins, and hot reload.
25
+ **Live state** means subscribers receive new values when data changes, without
26
+ asking for it again.
27
+
28
+ <!-- example:01 -->
29
+ ```ts
30
+ const host = createHost({ name: "todo-server" });
31
+ assert.equal(host.name, "todo-server");
32
+ await host.close();
33
+ ```
34
+
35
+ The assertion passes: the host's name is `todo-server`. Creating a host does
36
+ not create a todo object, start a server socket, or persist data. `close()`
37
+ shuts down its objects and connections.
38
+
39
+ > **Tip:** These are executable TypeScript examples, using assertions to show
40
+ > results. Shared imports are at the end. Later sections reuse earlier contracts
41
+ > and functions, but each section's test starts with fresh hosts. Test-only
42
+ > cleanup closes hosts even when an assertion fails; snippets omit that cleanup.
43
+
44
+ ## 2. Your first object
45
+
46
+ Before callers can use an object, they need to agree on its name and members.
47
+ That agreement is a **contract**: a runtime value that also gives TypeScript its
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.
50
+
51
+ Start with a label for a named list. `local` means only code inside this host
52
+ can reach it. The input `{ list: "family" }` identifies which list we want;
53
+ it is not an argument to `label`.
54
+
55
+ <!-- example:02-contract -->
56
+ ```ts
57
+ const TodoLabel = local("tutorial.todos", {
58
+ version: 1,
59
+ input: Type.Object({ list: Type.String() }),
60
+ members: { label: method(Type.Null(), Type.String()) },
61
+ });
62
+ ```
63
+
64
+ A **factory** is a synchronous function that creates the object's behavior:
65
+ the methods and state callers will see. `serve` registers a contract and its
66
+ factory. Registration alone does not activate this lazy object; the first
67
+ `get` does.
68
+
69
+ A **root** is one short tree of calls, such as the work for a button click.
70
+ `host.root` starts it. Its **context**, called `ctx`, carries cancellation,
71
+ caller identity, and the code version for nested calls. Pass it to methods you
72
+ call from that root.
73
+
74
+ <!-- example:02-call -->
75
+ ```ts
76
+ const host = createHost();
77
+ host.serve(TodoLabel, ({ list }) => ({ label: () => `Todos for ${list}` }));
78
+ const result = await host.root(async (ctx) => {
79
+ const todos = ctx.get(TodoLabel, { list: "family" });
80
+ return todos.label(null, ctx);
81
+ });
82
+ assert.equal(result, "Todos for family");
83
+ ```
84
+
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
87
+ `ctx.get` is released when this root ends. We never receive the factory's
88
+ 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
90
+ existing `ctx` so nested calls stay in the same call tree.
91
+
92
+ The factory may also receive `principal` (who is asking) and `self` (the
93
+ object's declaration API). We did not need them yet.
94
+
95
+ > **Why:** The handle separates callers from implementations. Later, replacing
96
+ > code will not require replacing every caller's reference.
97
+
98
+ ## 3. Give the list live state
99
+
100
+ A label is not a todo list. We need rows that can change, and a count that always
101
+ agrees with them. A **cell** owns an observable JSON value. A **derivation**
102
+ computes another observable value from cells or other readable sources.
103
+
104
+ Extend the contract with `rows`, `openCount`, and editing methods. State
105
+ members describe values, not methods. This adds members at contract version 1;
106
+ older callers that only need `label` still have everything they need.
107
+
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.
110
+
111
+ <!-- example:03-contract -->
112
+ ```ts
113
+ const Todo = Type.Object({ text: Type.String(), done: Type.Boolean() });
114
+ const Rows = Type.Array(Todo);
115
+ const TodoStore = local("tutorial.todos", {
116
+ version: 1,
117
+ input: Type.Object({ list: Type.String() }),
118
+ members: {
119
+ label: method(Type.Null(), Type.String()),
120
+ rows: Rows,
121
+ 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()),
125
+ },
126
+ });
127
+ ```
128
+
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.
132
+
133
+ <!-- example:03-factory -->
134
+ ```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
+ },
162
+ };
163
+ };
164
+ ```
165
+
166
+ `change(ctx, fn)` gives `fn` a temporary mutable **draft**. Here `add`
167
+ pushes a row and `finish` changes one field. The draft is discarded if the
168
+ callback throws. The callback must be synchronous.
169
+
170
+ `replace(ctx, value)` replaces the whole value. We use it for resetting a
171
+ list we own completely. `{ diff: true }` asks Chord to publish just the
172
+ structural changes. Without that option, replacement diffs automatically when
173
+ the new value shares containers at the same keys with the old one; unrelated
174
+ values normally publish a whole-value replacement. Either way, subscribers
175
+ receive the same resulting value.
176
+
177
+ <!-- example:03-call -->
178
+ ```ts
179
+ const host = createHost({ dev: true });
180
+ host.serve(TodoStore, todoFactory);
181
+ const todos = host.get(TodoStore, { list: "family" });
182
+ await todos.ready;
183
+ const counts: (number | undefined)[] = [];
184
+ const stop = todos.openCount.subscribe((value) => counts.push(value));
185
+ await host.root(async (ctx) => {
186
+ await todos.add({ text: "Buy milk" }, ctx);
187
+ await todos.finish({ index: 0 }, ctx);
188
+ await todos.reset([], ctx);
189
+ });
190
+ assert.deepEqual(counts, [0, 1, 0]);
191
+ assert.deepEqual(todos.rows.value, []);
192
+ stop();
193
+ todos.release();
194
+ ```
195
+
196
+ The count subscriber sees `0, 1, 0`: initially empty, one unfinished todo,
197
+ then no unfinished todos. Resetting to empty does not add another count
198
+ notification because the derived count is still zero. The final rows are `[]`.
199
+ `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.
202
+
203
+ > **Why:** Store reloadable state in cells, not closure variables. Treat
204
+ > `cell.value`, `handle.rows.value`, and subscriber values as immutable.
205
+ > Only mutate drafts inside `change`; do not keep a draft for later use.
206
+ > A derivation runs synchronously, so use it for projections, not database work.
207
+
208
+ ## 4. Let objects use objects and resources
209
+
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.
213
+
214
+ A **resource** is something the runtime opens and later closes, such as a
215
+ database connection. Its `open` may be asynchronous even though the factory
216
+ must be synchronous. The example database is deliberately fake: it just saves
217
+ strings in memory.
218
+
219
+ `opaque<T>("name@version")` describes a native, host-only member. It is not
220
+ JSON state and cannot be sent over a link. `self.expose` connects that member
221
+ to a declared resource, without reading the resource during the factory.
222
+
223
+ <!-- example:04-database -->
224
+ ```ts
225
+ const Database = local("tutorial.database", {
226
+ version: 1,
227
+ input: Type.Object({}),
228
+ members: { db: opaque<{ saved: string[] }>("tutorial.database@1") },
229
+ });
230
+ const SavedTodos = local("tutorial.saved-todos", {
231
+ version: 1,
232
+ input: Type.Object({ list: Type.String() }),
233
+ members: { add: method(Type.Object({ text: Type.String() }), Type.Null()) },
234
+ });
235
+ const host = createHost();
236
+ let opens = 0;
237
+ const closed = Promise.withResolvers<void>();
238
+ 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
+ }),
248
+ });
249
+ return { db: self.expose(res.db) };
250
+ });
251
+ ```
252
+
253
+ The factory returns an exposed marker, not an opened connection. Chord runs
254
+ `open` after declarations finish. The promise named `closed` lets the test
255
+ observe eventual cleanup.
256
+
257
+ Now declare `SavedTodos` as an object that holds both the store and the database.
258
+ Its editing method uses these dependencies only after activation.
259
+
260
+ <!-- example:04-saving -->
261
+ ```ts
262
+ host.serve(TodoStore, todoFactory);
263
+ host.serve(
264
+ SavedTodos,
265
+ ({ list }, _principal, self) => {
266
+ const database = self.get(Database, {});
267
+ const todos = self.get(TodoStore, { list });
268
+ return {
269
+ add: async ({ text }, ctx) => {
270
+ database.db!.saved.push(text);
271
+ await todos.add({ text }, ctx);
272
+ return null;
273
+ },
274
+ };
275
+ },
276
+ { linger: 0 },
277
+ );
278
+ ```
279
+
280
+ An outside consumer holds a `SavedTodos` handle. We also hold `Database`
281
+ directly so we can inspect what was saved.
282
+
283
+ <!-- example:04-call -->
284
+ ```ts
285
+ const saved = host.get(SavedTodos, { list: "family" });
286
+ const database = host.get(Database, {});
287
+ await saved.ready;
288
+ await host.root((ctx) => saved.add({ text: "Buy milk" }, ctx));
289
+ assert.deepEqual(database.db!.saved, ["Buy milk"]);
290
+ assert.equal(opens, 1);
291
+ const statuses: string[] = [];
292
+ saved.onStatus((status) => statuses.push(status));
293
+ database.release();
294
+ saved.release();
295
+ await closed.promise;
296
+ assert.equal(saved.status, "released");
297
+ assert.ok(statuses.includes("released"));
298
+ ```
299
+
300
+ After the method, the fake database contains `["Buy milk"]`. It opened once,
301
+ even though two objects held it. `SavedTodos` and `Database` become live
302
+ only after their resources and declared dependencies are ready. That is why
303
+ `database.db!` is safe inside this live object's method: activation has
304
+ already established that dependency. An outside caller must await readiness.
305
+
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
309
+ `released`, and `onStatus` reports that transition. `onStatus` watches
310
+ changes; unlike state subscription, it does not deliver an initial status.
311
+
312
+ `linger` is a delay in milliseconds after the last hold leaves. Zero disposes
313
+ without a grace period; a positive value can avoid reopening an expensive
314
+ resource when a screen is briefly hidden. Linger is not persistence.
315
+
316
+ > **Tip:** A factory only declares dependencies. Never call a held method or
317
+ > 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
+
321
+ ## 5. Who is asking? One store, two views
322
+
323
+ Owners may add todos; guests may only read them. A **principal** is the
324
+ application-defined identity of a caller. A **variant** is a named set of
325
+ members a remote object offers to that principal. It lets the UI discover
326
+ whether an editing method is available.
327
+
328
+ Do not create a separate data cell for each viewer: their lists would diverge.
329
+ 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
+
333
+ <!-- example:05-views -->
334
+ ```ts
335
+ const Todos = remote("tutorial.todo-view", {
336
+ version: 1,
337
+ input: Type.Object({ list: Type.String() }),
338
+ variants: {
339
+ viewer: { state: { rows: Rows } },
340
+ editor: {
341
+ state: { rows: Rows },
342
+ methods: { add: method(Type.Object({ text: Type.String() }), Type.Null()) },
343
+ },
344
+ },
345
+ });
346
+
347
+ function serveViews(api: Pick<Host, "serve">): void {
348
+ api.serve(Todos, ({ list }, principal, self) => {
349
+ if (list !== "family") throw forbidden("This list is not shared");
350
+ const store = self.get(TodoStore, { list });
351
+ if (principal !== "owner" && principal !== hostPrincipal) {
352
+ return { kind: "viewer", rows: store.rows };
353
+ }
354
+ return {
355
+ kind: "editor",
356
+ rows: store.rows,
357
+ add: ({ text }, ctx) => {
358
+ if (text.trim().length === 0) throw invalid("args", "A todo needs text");
359
+ return store.add({ text }, ctx);
360
+ },
361
+ };
362
+ });
363
+ }
364
+ ```
365
+
366
+ 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.
370
+
371
+ <!-- example:05-call -->
372
+ ```ts
373
+ const host = createHost({ principalKey: (principal) => String(principal) });
374
+ host.serve(TodoStore, todoFactory);
375
+ 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");
381
+ await host.root((ctx) => owner.add({ text: "Buy milk" }, ctx), { principal: "owner" });
382
+ assert.deepEqual(guest.rows.value, [{ text: "Buy milk", done: false }]);
383
+ await assert.rejects(guest.add({ text: "Not allowed" }), (error) => isChordError(error, "not_in_variant"));
384
+ await assert.rejects(owner.add({ text: " " }), (error) => isChordError(error, "invalid"));
385
+ ```
386
+
387
+ 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
390
+ rejects with `invalid`: it is a string, as the schema demands, but it breaks a
391
+ rule only the method knows. Use `invalid` for bad input and `forbidden` only
392
+ for callers who may not do something.
393
+
394
+ > **Why:** A variant is not all of authorization. The factory checks which
395
+ > list may be opened; the method checks the operation's arguments. Production
396
+ > 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.
399
+ > Guest-provided names or message text must never establish identity.
400
+
401
+ ## 6. Reach the list from another process
402
+
403
+ A browser cannot call a host-only contract. `remote` contracts describe only
404
+ schema-checked state and methods that can travel as JSON. Our `Todos` view is
405
+ remote; the underlying `TodoStore` and database stay local.
406
+
407
+ A **link** carries ordered messages between hosts. A memory link runs both ends
408
+ in one test process, but uses the remote protocol. The accepting host stamps
409
+ the caller's principal from its authenticator, not from method arguments or a
410
+ client's claimed identity.
411
+
412
+ <!-- example:06 -->
413
+ ```ts
414
+ const server = createHost({ name: "server" });
415
+ server.serve(TodoStore, todoFactory);
416
+ serveViews(server);
417
+ const [down, up] = memoryLinkPair();
418
+ 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>();
422
+ const updated = Promise.withResolvers<void>();
423
+ const stop = todos.rows.subscribe((rows) => {
424
+ if (rows !== undefined) hydrated.resolve();
425
+ if (rows?.length === 1) updated.resolve();
426
+ });
427
+ await todos.ready;
428
+ await hydrated.promise;
429
+ assert.deepEqual(todos.rows.value, []);
430
+ await browser.root((ctx) => todos.add({ text: "Buy milk" }, ctx));
431
+ await updated.promise;
432
+ assert.deepEqual(todos.rows.value, [{ text: "Buy milk", done: false }]);
433
+ // Deliberately bypass TypeScript to simulate a malformed caller.
434
+ await assert.rejects(todos.add({ text: 123 } as never), (error) => isChordError(error, "invalid"));
435
+ stop();
436
+ assert.equal(todos.rows.value, undefined);
437
+ todos.release();
438
+ ```
439
+
440
+ 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.
445
+
446
+ 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;
449
+ subsequent **deltas** describe changes. Rebinds and reloads can send a
450
+ `reset`, a fresh whole value. Functions, resource connections, and cell
451
+ instances do not cross the wire.
452
+
453
+ The deliberate bad argument rejects with `invalid`. Remote arguments,
454
+ results, and outgoing state are validated at the provider boundary. Local
455
+ methods do not get per-call schema validation; they still need app-level checks.
456
+ `dev: true` additionally checks cell writes while developing.
457
+
458
+ > **Tip:** Replace `memoryLinkPair` with `webSocketLink` for a WebSocket or
459
+ > `ipcLink` for a child-process IPC channel; object contracts stay the same.
460
+ > Those adapters wrap an existing connection, not an authenticator. Configure
461
+ > `accept` with an authenticated principal. A browser is `kind: "client"`;
462
+ > a trusted child host is accepted with `kind: "host"` and its configured name.
463
+
464
+ ## 7. React to changes and run background work
465
+
466
+ Not all work comes from method calls. The app may update an activity indicator
467
+ when rows change, or run a background reminder. A **listener** reacts to an
468
+ observable source. A **task** is background execution owned by an object,
469
+ which is aborted when the object is disposed or its code is reloaded.
470
+
471
+ Our activity object mirrors the row count. Its task inserts a sample todo,
472
+ then waits for shutdown. The promises make those events deterministic in a
473
+ test; they are not stored application state.
474
+
475
+ <!-- example:07 -->
476
+ ```ts
477
+ const Activity = local("tutorial.activity", {
478
+ version: 1,
479
+ input: Type.Object({}),
480
+ members: { seen: Type.Number() },
481
+ });
482
+ const host = createHost();
483
+ host.serve(TodoStore, todoFactory);
484
+ const observed = Promise.withResolvers<void>();
485
+ const seeded = Promise.withResolvers<void>();
486
+ const stopped = Promise.withResolvers<void>();
487
+ host.serve(Activity, (_input, _principal, self) => {
488
+ 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
+ }),
495
+ });
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
+ },
506
+ });
507
+ return { seen: state.seen };
508
+ });
509
+ const activity = host.get(Activity, {});
510
+ await activity.ready;
511
+ await Promise.all([seeded.promise, observed.promise]);
512
+ assert.equal(activity.seen.value, 1);
513
+ activity.release();
514
+ await stopped.promise;
515
+ ```
516
+
517
+ The final `seen` is `1`. Each listener event receives a fresh root's
518
+ `ctx`; its write belongs to that short call tree. A task is not one enormous
519
+ root: Chord method calls made with its task context start fresh short roots.
520
+ 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.
528
+
529
+ > **Why:** Do not wait for a person, sleep for minutes, or run an endless loop
530
+ > inside a method. That pins a root and can block state migration. Store a
531
+ > pending request in a cell and answer in another call, or use a task that obeys
532
+ > `ctx.signal`. Never retain a completed method's `ctx` for background work.
533
+
534
+ ## 8. Package features as plugins
535
+
536
+ 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
539
+ facets, each installing that part of the feature.
540
+
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.
544
+
545
+ <!-- example:08 -->
546
+ ```ts
547
+ const Actions = defineRegistry<(ctx: Ctx) => Promise<number>>("tutorial.actions");
548
+ const Menu = local("tutorial.menu", { version: 1, input: Type.Object({}) });
549
+ let click: (() => Promise<number>) | undefined;
550
+ const storeFacet: Facet = {
551
+ name: "todo-store",
552
+ ...defineFacet((api) => api.serve(TodoStore, todoFactory)),
553
+ };
554
+ const menuFacet: Facet = {
555
+ name: "todo-menu",
556
+ ...defineFacet(
557
+ (api) => {
558
+ api.serve(
559
+ Menu,
560
+ (_input, _principal, self) => {
561
+ const todos = self.get(TodoStore, { list: "family" });
562
+ self.register(Actions, {
563
+ addSample: async (ctx) => {
564
+ await todos.add({ text: "Try plugins" }, ctx);
565
+ return todos.openCount.value!;
566
+ },
567
+ });
568
+ return {};
569
+ },
570
+ { eager: true },
571
+ );
572
+ click = () => api.root((ctx) => api.registry(Actions).get("addSample", ctx)!(ctx));
573
+ },
574
+ { uses: [TodoStore] },
575
+ ),
576
+ };
577
+ const host = createHost();
578
+ await host.load([menuFacet, storeFacet], { awaitEager: false });
579
+ await host.ready();
580
+ assert.equal(await click!(), 1);
581
+ assert.equal(host.registry(Actions).table().size, 1);
582
+ ```
583
+
584
+ Clicking returns `1`, and the registry has one entry. The menu's dependency
585
+ 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
587
+ reload can also check the contract it consumes.
588
+
589
+ The facet API supplies `serve`, `name`, `root`, and `registry`.
590
+ Our ordinary UI callback uses its saved `api.root`, not a process-global
591
+ host. Looking up a registry entry with `ctx` uses the table belonging to that
592
+ root's code version. Without `ctx`, lookup uses the current table. Duplicate
593
+ keys fail by default; `subscribe` can bridge tables to an imperative consumer.
594
+
595
+ 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.
599
+ Failures reject the readiness promise; abandoning startup requires closing the
600
+ host, because installation is not a rollback transaction.
601
+
602
+ > **Tip:** Use deferred readiness only when needed. A worker may have to await
603
+ > an independent local bootstrap handle, announce control readiness so its
604
+ > parent admits its link, and only then await `host.ready()`. Waiting for
605
+ > upstream-dependent eager objects before admission would deadlock.
606
+
607
+ For real plugin files, default-export `defineFacet(...)`. Node hosts configure
608
+ `createHost({ loader: nodeLoader })`, with `nodeLoader` imported from
609
+ `@earendil-works/chord/node`, then pass module URLs to `load` and `reload`.
610
+ The in-memory facet objects here avoid filesystem setup. Built-in facets are
611
+ trusted in-process code, not a sandbox; the current Node loader keeps old module
612
+ copies in memory, so repeated loading has that limitation.
613
+
614
+ ## 9. Change running code without losing the list
615
+
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.
619
+
620
+ ### Compatible code change: no waiting
621
+
622
+ 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
624
+ old root sees; application methods should not pause like this.
625
+
626
+ <!-- example:09-compatible -->
627
+ ```ts
628
+ const host = createHost();
629
+ const facet = (label: string): Facet => ({
630
+ name: "todos",
631
+ setup: (api) =>
632
+ api.serve(TodoStore, (input, principal, self) => ({
633
+ ...todoFactory(input, principal, self),
634
+ label: () => label,
635
+ })),
636
+ });
637
+ await host.load([facet("Original")]);
638
+ const todos = host.get(TodoStore, { list: "family" });
639
+ await todos.add({ text: "Buy milk" });
640
+ const entered = Promise.withResolvers<void>();
641
+ const resume = Promise.withResolvers<void>();
642
+ const oldRoot = host.root(async (ctx) => {
643
+ const first = await todos.label(null, ctx);
644
+ entered.resolve();
645
+ await resume.promise; // Test-only pause: pin an old caller during reload.
646
+ return [first, await todos.label(null, ctx)];
647
+ });
648
+ await entered.promise;
649
+ const compatible = await host.reload([facet("Updated")]);
650
+ assert.deepEqual(compatible.gated, []);
651
+ assert.equal(await todos.label(null), "Updated");
652
+ resume.resolve();
653
+ assert.deepEqual(await oldRoot, ["Original", "Original"]);
654
+ assert.equal(todos.rows.value?.[0]?.text, "Buy milk");
655
+ ```
656
+
657
+ Reload commits while the old root is paused: `gated` is empty. New calls see
658
+ `"Updated"`; the resumed old root sees `"Original"` twice. The milk row
659
+ survives. Compatible code changes do not wait for that old root to finish.
660
+
661
+ ### State change: bump its version and migrate
662
+
663
+ 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.
667
+
668
+ <!-- example:09-migrate -->
669
+ ```ts
670
+ const events: string[] = [];
671
+ const stopEvents = todos.rows.events((event) => events.push(event.type));
672
+ const migrated = await host.reload([
673
+ {
674
+ name: "todos",
675
+ setup: (api) =>
676
+ api.serve(TodoStore, todoFactory, {
677
+ state: {
678
+ 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
+ },
684
+ },
685
+ },
686
+ }),
687
+ },
688
+ ]);
689
+ assert.equal(migrated.gated.length, 1);
690
+ assert.deepEqual(todos.rows.value, [{ text: "BUY MILK", done: false }]);
691
+ assert.deepEqual(events, ["snap", "reset"]);
692
+ stopEvents();
693
+ ```
694
+
695
+ 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 }]`.
697
+ Subscribers get a `reset` event containing the migrated value. The event
698
+ assertion sees an initial `snap`, then that `reset`.
699
+ One record is gated. A **gate** waits for calls already inside that record to
700
+ finish before migrating a private copy, validating it, and committing. Old
701
+ roots that had not entered it cannot later use old code over migrated data:
702
+ they get `upgrade_conflict` and should retry in a new root.
703
+
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,
706
+ like this example, cannot be detected automatically. Real migrations must
707
+ handle each stored version they support, not assume every installation is
708
+ exactly one version behind.
709
+
710
+ ### Contract change: keep an adapter for old callers
711
+
712
+ 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
714
+ that version-1 consumers still expect.
715
+
716
+ <!-- example:09-contract -->
717
+ ```ts
718
+ // Remove label from the new provider's contract: it is now called heading.
719
+ const { label: _label, ...members } = TodoStore.def.members;
720
+ const RenamedStore = local("tutorial.todos", {
721
+ version: 2,
722
+ input: TodoStore.def.input,
723
+ members: { ...members, heading: method(Type.Null(), Type.String()) },
724
+ });
725
+ await host.reload([
726
+ {
727
+ name: "todos",
728
+ setup: (api) =>
729
+ api.serve(
730
+ RenamedStore,
731
+ (input, principal, self) => {
732
+ const { label, ...behavior } = todoFactory(input, principal, self);
733
+ return { ...behavior, heading: label };
734
+ },
735
+ {
736
+ state: { version: 2 },
737
+ accepts: {
738
+ 1: (current) => ({ ...current, label: current.heading }),
739
+ },
740
+ },
741
+ ),
742
+ },
743
+ ]);
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");
747
+ assert.equal(todos.rows.value?.[0]?.text, "BUY MILK");
748
+ ```
749
+
750
+ The old handle's `label` and the new handle's `heading` both return
751
+ `"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.
754
+
755
+ > **Why:** Contract versions protect callers; state versions protect stored
756
+ > data; resource revisions control reopening. Do not use one number for all
757
+ > three. Additive compatible contracts can keep their version; breaking shape
758
+ > or meaning changes need a contract bump and adapters for old consumers.
759
+ > Reload prepares candidates first; preparation failure leaves the old code
760
+ > running. A migration gate that misses its deadline aborts rather than
761
+ > switching data underneath active old code.
762
+
763
+ ## 10. Test the objects, not a whole deployment
764
+
765
+ Most behavior needs only a bare host. Use real factories for state tests, and a
766
+ fake facet when you want to test delegation without a real database or service.
767
+ Here the fake records what the editing view sends to the store.
768
+
769
+ <!-- example:10 -->
770
+ ```ts
771
+ const host = createHost({ dev: true });
772
+ const received: string[] = [];
773
+ const fake: Facet = {
774
+ name: "fake-store",
775
+ ...defineFacet((api: FacetApi) => {
776
+ api.serve(TodoStore, (input, principal, self) => ({
777
+ ...todoFactory(input, principal, self),
778
+ add: ({ text }) => {
779
+ received.push(text);
780
+ return null;
781
+ },
782
+ }));
783
+ }),
784
+ };
785
+ await host.load([fake]);
786
+ serveViews(host);
787
+ const owner = host.get(Todos, { list: "family" }, { principal: "owner" });
788
+ await owner.add({ text: "Test delegation" });
789
+ assert.deepEqual(received, ["Test delegation"]);
790
+ await assert.rejects(owner.add({ text: "" }), (error) => isChordError(error, "invalid"));
791
+ owner.release();
792
+ await assert.rejects(owner.add({ text: "Too late" }), (error) => isChordError(error, "released"));
793
+ ```
794
+
795
+ The fake received `["Test delegation"]`. Empty text failed before reaching
796
+ the fake. The released handle rejects further calls with `released`.
797
+ `isChordError(error, code)` checks a stable error code; avoid testing only a
798
+ message's wording.
799
+
800
+ 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
804
+ lifetime bugs or leave background tasks running.
805
+
806
+ > **Tip:** A fake must honor the contract it serves. This fake delegates all
807
+ > ordinary members to `todoFactory` and replaces only `add`; it deliberately
808
+ > records calls rather than updating rows. Do not confuse that with persistence.
809
+
810
+ ## 11. Keep these habits
811
+
812
+ A temporary call handle should not accidentally become a permanent hold. A
813
+ screen should not lose its data merely because a button click finishes. Use
814
+ `ctx.get` for the first lifetime and `host.get` for the second.
815
+
816
+ <!-- example:11 -->
817
+ ```ts
818
+ const host = createHost();
819
+ host.serve(TodoStore, todoFactory);
820
+ const screen = host.get(TodoStore, { list: "family" });
821
+ await screen.ready;
822
+ await host.root(async (ctx) => {
823
+ const temporary = ctx.get(TodoStore, { list: "family" });
824
+ await temporary.add({ text: "Keep roots short" }, ctx);
825
+ }); // The temporary handle is released here; screen still holds the store.
826
+ assert.equal(screen.openCount.value, 1);
827
+ screen.release(); // No hold remains; without persistence, disposal loses these rows.
828
+ ```
829
+
830
+ 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
833
+ loses its rows. Real persistence belongs in a database resource, with an
834
+ explicit load/save policy.
835
+
836
+ ### Checklist and common mistakes
837
+
838
+ - Keep reloadable state in cells, not closure variables: factories rerun.
839
+ - Change drafts, not published values: mutation outside cells bypasses updates.
840
+ - Use `change` for field edits and `replace` for owned snapshots: replacing
841
+ can erase fields your code does not know about.
842
+ - Declare dependencies in a synchronous factory, not during methods: activation
843
+ and reload need a complete declaration list.
844
+ - Use `ctx.get` for temporary method dependencies: their holds end with the call.
845
+ - Pass `ctx` into nested calls: cancellation and code versions must stay together.
846
+ - Keep roots short and tasks cancellable: migration and shutdown need work to settle.
847
+ - Check grants and arguments, not just variants: available methods are not a full policy.
848
+ - 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:
852
+ 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.
855
+
856
+ ### Imports and running the examples
857
+
858
+ These are the shared imports from `test/tutorial.test.ts`. The relative
859
+ path is correct from that test directory; applications import the same names
860
+ from `@earendil-works/chord`. `assert` expresses expected values; `it` and
861
+ `onTestFinished` come from the test runner; `readFileSync` is used by the
862
+ tutorial's source-matching check, not by the application.
863
+
864
+ <!-- example:imports -->
865
+ ```ts
866
+ import assert from "node:assert/strict";
867
+ import { readFileSync } from "node:fs";
868
+ import { type Static, Type } from "typebox";
869
+ import { it, onTestFinished } from "vitest";
870
+ import {
871
+ type Ctx,
872
+ cell,
873
+ createHost,
874
+ defineFacet,
875
+ defineRegistry,
876
+ derivation,
877
+ type Facet,
878
+ type FacetApi,
879
+ type Factory,
880
+ forbidden,
881
+ type Host,
882
+ hostPrincipal,
883
+ invalid,
884
+ isChordError,
885
+ listener,
886
+ local,
887
+ memoryLinkPair,
888
+ method,
889
+ opaque,
890
+ remote,
891
+ resource,
892
+ } from "../src/index.ts";
893
+ ```
894
+
895
+ From `packages/chord`, run:
896
+
897
+ ```sh
898
+ node ../../node_modules/vitest/dist/cli.js --run test/tutorial.test.ts
899
+ ```
900
+
901
+ There is one test per numbered section. Every TypeScript block is matched
902
+ against its runnable source, so changing either copy without the other fails a
903
+ test. The test harness adds teardown and drift checks; it never uses a real
904
+ database, network service, or provider API.
905
+
906
+ Next read [the usage guide](guide.md) for the API reference and
907
+ [the specification](spec.md) for compatibility, lifetime, and reload
908
+ rules, including its known limitations. If a reference disagrees with
909
+ `src`, the current implementation wins.