@earendil-works/chord 1.1.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 (190) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +79 -195
  3. package/dist/cell.d.ts +122 -0
  4. package/dist/cell.d.ts.map +1 -0
  5. package/dist/cell.js +356 -0
  6. package/dist/cell.js.map +1 -0
  7. package/dist/cluster.d.ts +27 -0
  8. package/dist/cluster.d.ts.map +1 -0
  9. package/dist/cluster.js +230 -0
  10. package/dist/cluster.js.map +1 -0
  11. package/dist/connection.d.ts +270 -0
  12. package/dist/connection.d.ts.map +1 -0
  13. package/dist/connection.js +1216 -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 +208 -0
  19. package/dist/contract.d.ts.map +1 -0
  20. package/dist/contract.js +246 -0
  21. package/dist/contract.js.map +1 -0
  22. package/dist/declare.d.ts +33 -0
  23. package/dist/declare.d.ts.map +1 -0
  24. package/dist/declare.js +2 -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 +4 -3
  31. package/dist/delta/index.d.ts.map +1 -1
  32. package/dist/delta/index.js +45 -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 +28 -0
  42. package/dist/errors.d.ts.map +1 -0
  43. package/dist/errors.js +59 -0
  44. package/dist/errors.js.map +1 -0
  45. package/dist/host.d.ts +348 -0
  46. package/dist/host.d.ts.map +1 -0
  47. package/dist/host.js +1000 -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 +107 -0
  57. package/dist/link.d.ts.map +1 -0
  58. package/dist/link.js +355 -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 +73 -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 +345 -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/object.d.ts +438 -0
  73. package/dist/object.d.ts.map +1 -0
  74. package/dist/object.js +1571 -0
  75. package/dist/object.js.map +1 -0
  76. package/dist/output.d.ts +85 -0
  77. package/dist/output.d.ts.map +1 -0
  78. package/dist/output.js +350 -0
  79. package/dist/output.js.map +1 -0
  80. package/dist/publish.d.ts +5 -0
  81. package/dist/publish.d.ts.map +1 -0
  82. package/dist/publish.js +40 -0
  83. package/dist/publish.js.map +1 -0
  84. package/dist/registry.d.ts +25 -0
  85. package/dist/registry.d.ts.map +1 -0
  86. package/dist/registry.js +6 -0
  87. package/dist/registry.js.map +1 -0
  88. package/dist/reload.d.ts +126 -0
  89. package/dist/reload.d.ts.map +1 -0
  90. package/dist/reload.js +653 -0
  91. package/dist/reload.js.map +1 -0
  92. package/dist/remote.d.ts +165 -0
  93. package/dist/remote.d.ts.map +1 -0
  94. package/dist/remote.js +612 -0
  95. package/dist/remote.js.map +1 -0
  96. package/dist/schema.d.ts +21 -0
  97. package/dist/schema.d.ts.map +1 -0
  98. package/dist/schema.js +228 -0
  99. package/dist/schema.js.map +1 -0
  100. package/dist/state.d.ts +92 -0
  101. package/dist/state.d.ts.map +1 -0
  102. package/dist/{services/state.js → state.js} +28 -138
  103. package/dist/state.js.map +1 -0
  104. package/dist/types.d.ts +69 -258
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/types.js.map +1 -1
  107. package/dist/websocket.d.ts +15 -0
  108. package/dist/websocket.d.ts.map +1 -0
  109. package/dist/websocket.js +16 -0
  110. package/dist/websocket.js.map +1 -0
  111. package/dist/wire.d.ts +216 -0
  112. package/dist/wire.d.ts.map +1 -0
  113. package/dist/wire.js +165 -0
  114. package/dist/wire.js.map +1 -0
  115. package/docs/guide.md +814 -0
  116. package/docs/spec.md +288 -0
  117. package/docs/tutorial.md +931 -0
  118. package/package.json +14 -10
  119. package/src/delta/README.md +5 -3
  120. package/dist/api.d.ts +0 -20
  121. package/dist/api.d.ts.map +0 -1
  122. package/dist/api.js +0 -77
  123. package/dist/api.js.map +0 -1
  124. package/dist/bundler.d.ts +0 -6
  125. package/dist/bundler.d.ts.map +0 -1
  126. package/dist/bundler.js +0 -3
  127. package/dist/bundler.js.map +0 -1
  128. package/dist/facets/host.d.ts +0 -12
  129. package/dist/facets/host.d.ts.map +0 -1
  130. package/dist/facets/host.js +0 -763
  131. package/dist/facets/host.js.map +0 -1
  132. package/dist/facets/loader.d.ts +0 -3
  133. package/dist/facets/loader.d.ts.map +0 -1
  134. package/dist/facets/loader.js +0 -5
  135. package/dist/facets/loader.js.map +0 -1
  136. package/dist/node/bundle-loader.d.ts +0 -30
  137. package/dist/node/bundle-loader.d.ts.map +0 -1
  138. package/dist/node/bundle-loader.js +0 -377
  139. package/dist/node/bundle-loader.js.map +0 -1
  140. package/dist/node/bundle.d.ts +0 -26
  141. package/dist/node/bundle.d.ts.map +0 -1
  142. package/dist/node/bundle.js +0 -183
  143. package/dist/node/bundle.js.map +0 -1
  144. package/dist/node/manifest.d.ts +0 -36
  145. package/dist/node/manifest.d.ts.map +0 -1
  146. package/dist/node/manifest.js +0 -6
  147. package/dist/node/manifest.js.map +0 -1
  148. package/dist/node/package.d.ts +0 -15
  149. package/dist/node/package.d.ts.map +0 -1
  150. package/dist/node/package.js +0 -199
  151. package/dist/node/package.js.map +0 -1
  152. package/dist/services/consumer.d.ts +0 -11
  153. package/dist/services/consumer.d.ts.map +0 -1
  154. package/dist/services/consumer.js +0 -585
  155. package/dist/services/consumer.js.map +0 -1
  156. package/dist/services/errors.d.ts +0 -8
  157. package/dist/services/errors.d.ts.map +0 -1
  158. package/dist/services/errors.js +0 -22
  159. package/dist/services/errors.js.map +0 -1
  160. package/dist/services/handle.d.ts +0 -15
  161. package/dist/services/handle.d.ts.map +0 -1
  162. package/dist/services/handle.js +0 -101
  163. package/dist/services/handle.js.map +0 -1
  164. package/dist/services/instances.d.ts +0 -26
  165. package/dist/services/instances.d.ts.map +0 -1
  166. package/dist/services/instances.js +0 -140
  167. package/dist/services/instances.js.map +0 -1
  168. package/dist/services/loopback.d.ts +0 -5
  169. package/dist/services/loopback.d.ts.map +0 -1
  170. package/dist/services/loopback.js +0 -15
  171. package/dist/services/loopback.js.map +0 -1
  172. package/dist/services/provider.d.ts +0 -37
  173. package/dist/services/provider.d.ts.map +0 -1
  174. package/dist/services/provider.js +0 -521
  175. package/dist/services/provider.js.map +0 -1
  176. package/dist/services/state-codec.d.ts +0 -15
  177. package/dist/services/state-codec.d.ts.map +0 -1
  178. package/dist/services/state-codec.js +0 -133
  179. package/dist/services/state-codec.js.map +0 -1
  180. package/dist/services/state-internals.d.ts +0 -13
  181. package/dist/services/state-internals.d.ts.map +0 -1
  182. package/dist/services/state-internals.js +0 -10
  183. package/dist/services/state-internals.js.map +0 -1
  184. package/dist/services/state.d.ts +0 -28
  185. package/dist/services/state.d.ts.map +0 -1
  186. package/dist/services/state.js.map +0 -1
  187. package/dist/services/wire.d.ts +0 -63
  188. package/dist/services/wire.d.ts.map +0 -1
  189. package/dist/services/wire.js +0 -186
  190. package/dist/services/wire.js.map +0 -1
package/dist/object.js ADDED
@@ -0,0 +1,1571 @@
1
+ import { Value } from "typebox/value";
2
+ import { Cell, Derived, HYDRATION, hydrationOf, isSource, notReady, unhydrated, } from "./cell.js";
3
+ import { bind, LOCAL_VARIANT, takesNoArgs, } from "./contract.js";
4
+ import { asChordError, ChordError, invalid, runAll } from "./errors.js";
5
+ import { enqueue } from "./publish.js";
6
+ import { assertValid, normalize } from "./schema.js";
7
+ const noop = () => { };
8
+ export const makeService = (contract, factory, options, facet) => {
9
+ checkLifetime(contract, options);
10
+ return {
11
+ contract,
12
+ factory: factory,
13
+ options: options,
14
+ facet,
15
+ };
16
+ };
17
+ /** Whether objects of `service` have `lifetime: "host"`. */
18
+ export const hostLifetime = (service) => service.options.lifetime === "host";
19
+ /** Refuses lifetimes the type cannot have, when it is served. */
20
+ function checkLifetime(contract, options) {
21
+ const lifetime = options.lifetime ?? "referenced";
22
+ if (lifetime !== "referenced" && lifetime !== "host")
23
+ throw new TypeError(`${contract.name}: unknown lifetime ${String(lifetime)}`);
24
+ if (lifetime !== "host")
25
+ return;
26
+ if (contract.scope !== "local")
27
+ throw new TypeError(`${contract.name}: lifetime "host" needs a local type (remote types are referenced)`);
28
+ if (options.linger !== undefined)
29
+ throw new TypeError(`${contract.name}: an object of lifetime "host" does not linger (it lives as long as the host)`);
30
+ try {
31
+ normalize(contract.input, {}, `${contract.name} input`);
32
+ }
33
+ catch {
34
+ throw new TypeError(`${contract.name}: lifetime "host" needs an input that accepts {} (the object's one address)`);
35
+ }
36
+ }
37
+ /** Runtime form of `Exposed<T>`. */
38
+ class ExposedImpl {
39
+ resource;
40
+ constructor(resource) {
41
+ this.resource = resource;
42
+ }
43
+ }
44
+ export class ResourceImpl {
45
+ key;
46
+ /** The declaration of the factory run whose code is current: a kept resource adopts the new one at commit. */
47
+ spec;
48
+ deps;
49
+ /** `unavailable`: a close/reopen failed during a reload rollback; the value is gone for good. */
50
+ state = "closed";
51
+ #value;
52
+ constructor(key, spec, deps) {
53
+ this.key = key;
54
+ this.spec = spec;
55
+ this.deps = deps;
56
+ }
57
+ get value() {
58
+ if (this.state === "unavailable")
59
+ throw new ChordError("unavailable", `resource ${this.key} could not be restored`);
60
+ if (this.state !== "open")
61
+ throw notReady(`resource ${this.key}`);
62
+ return this.#value;
63
+ }
64
+ get exclusive() {
65
+ return this.spec.exclusive === true;
66
+ }
67
+ /**
68
+ * Opens the resource. An open that completes after `signal` aborted (rollback, abandonment) closes the value it
69
+ * produced at once: nobody owns it any more.
70
+ */
71
+ async open(signal, order) {
72
+ if (this.state !== "closed")
73
+ return;
74
+ this.state = "opening";
75
+ let value;
76
+ try {
77
+ value = await this.spec.open({ signal });
78
+ }
79
+ catch (error) {
80
+ this.state = "closed";
81
+ throw error;
82
+ }
83
+ if (signal.aborted) {
84
+ this.state = "closed";
85
+ await this.spec.close?.(value);
86
+ throw new ChordError("aborted", `resource ${this.key} opened after abort`);
87
+ }
88
+ this.#value = value;
89
+ this.state = "open";
90
+ order.push(this);
91
+ }
92
+ async close(order) {
93
+ if (this.state !== "open")
94
+ return;
95
+ this.state = "closed";
96
+ const index = order.indexOf(this);
97
+ if (index !== -1)
98
+ order.splice(index, 1);
99
+ const value = this.#value;
100
+ this.#value = undefined;
101
+ await this.spec.close?.(value);
102
+ }
103
+ markUnavailable() {
104
+ this.state = "unavailable";
105
+ this.#value = undefined;
106
+ }
107
+ /**
108
+ * A reload kept this resource (same version, deps and exclusivity): its later `close` is the new declaration's, and
109
+ * the old declaration (with the old code it closes over) is no longer referenced.
110
+ */
111
+ adopt(spec) {
112
+ this.spec = spec;
113
+ }
114
+ }
115
+ /**
116
+ * What `self.resources` returns: the resource, except that `.value` throws `not_ready` while any factory runs, even
117
+ * if the resource is open (a reload's prepare rerun must not touch the native resource the old code uses).
118
+ */
119
+ class ResourceFacade {
120
+ #resource;
121
+ #host;
122
+ constructor(resource, host) {
123
+ this.#resource = resource;
124
+ this.#host = host;
125
+ facades.set(this, resource);
126
+ }
127
+ get key() {
128
+ return this.#resource.key;
129
+ }
130
+ get value() {
131
+ if (this.#host.inFactory > 0)
132
+ throw notReady(`resource ${this.key} (inside a factory)`);
133
+ return this.#resource.value;
134
+ }
135
+ }
136
+ const facades = new WeakMap();
137
+ const resourceOf = (value) => value instanceof ResourceImpl ? value : value !== null && typeof value === "object" ? facades.get(value) : undefined;
138
+ /** Everything one factory run declared, by kind and key. */
139
+ export class Decls {
140
+ cells = new Map();
141
+ cellSchemas = new Map();
142
+ cellVersions = new Map();
143
+ cellMigrations = new Map();
144
+ refs = new Map();
145
+ resources = new Map();
146
+ /** Resources this run kept from the previous one, with this run's declarations (adopted at commit). */
147
+ adopted = new Map();
148
+ listeners = new Map();
149
+ /** By position (`#0`, `#1`, …): tasks have no names. */
150
+ tasks = new Map();
151
+ /** By position: derivations have no names and are new at every run. */
152
+ derives = new Map();
153
+ registrations = new Map();
154
+ }
155
+ const isStateInit = (value) => value !== null && typeof value === "object" && typeof value.init === "function";
156
+ // ─── Contexts ────────────────────────────────────────────────────────────────
157
+ export class CtxImpl {
158
+ host;
159
+ signal;
160
+ rootId;
161
+ codeVersion;
162
+ principal;
163
+ /** Code holders (tasks): every call they make starts a fresh root. */
164
+ holder;
165
+ #refs = [];
166
+ #release;
167
+ /** Aborted when the ctx closes: awaited `get`s give up. */
168
+ #closing = new AbortController();
169
+ #closed = false;
170
+ /** True after the call or root that received this ctx settled: it may no longer start work. */
171
+ get closed() {
172
+ return this.#closed;
173
+ }
174
+ /** Throws unless this ctx may start work on `host` (the called reference's host) now: not expired, aborted or foreign. */
175
+ check(host) {
176
+ if (this.host !== host)
177
+ throw invalid("foreign_ctx", "ctx belongs to another host");
178
+ if (this.#closed)
179
+ throw new ChordError("aborted", "ctx expired: its call already settled", { reason: "expired" });
180
+ if (this.signal.aborted) {
181
+ const reason = this.signal.reason;
182
+ // A forced retirement aborted the root: its code version is gone, so the next call is `superseded` (retryable).
183
+ if (reason instanceof ChordError && reason.code === "superseded")
184
+ throw new ChordError("superseded", reason.message, { cause: reason });
185
+ throw new ChordError("aborted", "ctx aborted", { cause: reason });
186
+ }
187
+ }
188
+ /** `signals`: the ctx is aborted with the first of them; it stops following them when it closes. */
189
+ constructor(host, fields) {
190
+ this.host = host;
191
+ const linked = linkedSignal(fields.signals);
192
+ this.signal = linked.signal;
193
+ this.#release = linked.release;
194
+ this.rootId = fields.rootId;
195
+ this.codeVersion = fields.codeVersion;
196
+ this.principal = fields.principal;
197
+ this.holder = fields.holder ?? false;
198
+ }
199
+ async get(type, input, options) {
200
+ this.check(this.host);
201
+ const ref = this.host.acquire(type, input, { principal: this.principal, ...options }, "call");
202
+ this.#refs.push(ref);
203
+ await awaitReady(ref, [this.signal, this.#closing.signal], this.host.readyTimeout);
204
+ return ref.proxy;
205
+ }
206
+ /**
207
+ * A context for a call into `object`: same root and code version, its own call-owned references, aborted on forced
208
+ * disposal. Code inside an object acts as the host principal.
209
+ */
210
+ child(object) {
211
+ return new CtxImpl(this.host, {
212
+ signals: [this.signal, object.callAbort.signal],
213
+ rootId: this.rootId,
214
+ codeVersion: this.codeVersion,
215
+ principal: this.host.principal,
216
+ });
217
+ }
218
+ close() {
219
+ this.#closed = true;
220
+ this.#closing.abort(new ChordError("aborted", "ctx expired: its call already settled", { reason: "expired" }));
221
+ this.#release();
222
+ for (const ref of this.#refs.splice(0))
223
+ ref.release();
224
+ }
225
+ }
226
+ /**
227
+ * A signal aborted with the first of `sources` to abort, until `release()`. Not `AbortSignal.any`: Node keeps a
228
+ * dependant of a long-lived source (an object's or a code version's) reachable while it has abort listeners, so every
229
+ * call or task context would keep its listeners, and the code they close over, alive as long as the source.
230
+ */
231
+ export function linkedSignal(sources) {
232
+ const controller = new AbortController();
233
+ const detach = [];
234
+ const release = () => {
235
+ for (const remove of detach.splice(0))
236
+ remove();
237
+ };
238
+ for (const source of sources) {
239
+ if (source.aborted) {
240
+ release();
241
+ controller.abort(source.reason);
242
+ break;
243
+ }
244
+ const forward = () => {
245
+ release();
246
+ controller.abort(source.reason);
247
+ };
248
+ source.addEventListener("abort", forward, { once: true });
249
+ detach.push(() => source.removeEventListener("abort", forward));
250
+ }
251
+ return { signal: controller.signal, release };
252
+ }
253
+ /**
254
+ * Waits until `ref` is ready, at most `ms` and until a signal aborts; on failure the reference is released (only it: a
255
+ * binding other references share stays) and the error is rethrown: the activation error, `aborted`, or
256
+ * `unavailable/ready_timeout`.
257
+ */
258
+ export async function awaitReady(ref, signals, ms) {
259
+ const timeout = new AbortController();
260
+ const timer = Number.isFinite(ms)
261
+ ? setTimeout(() => timeout.abort(new ChordError("unavailable", `${ref.address} was not ready after ${ms} ms`, {
262
+ reason: "ready_timeout",
263
+ })), ms)
264
+ : undefined;
265
+ timer?.unref?.();
266
+ const linked = linkedSignal([...signals, timeout.signal]);
267
+ try {
268
+ await raceSignal(ref.ready, linked.signal);
269
+ }
270
+ catch (error) {
271
+ ref.release();
272
+ const reason = linked.signal.reason;
273
+ throw reason instanceof ChordError && reason.reason === "ready_timeout" ? reason : error;
274
+ }
275
+ finally {
276
+ clearTimeout(timer);
277
+ linked.release();
278
+ }
279
+ }
280
+ export async function raceSignal(promise, signal) {
281
+ if (signal.aborted)
282
+ throw new ChordError("aborted", "call aborted", { cause: signal.reason });
283
+ let onAbort = noop;
284
+ const aborted = new Promise((_, reject) => {
285
+ onAbort = () => reject(new ChordError("aborted", "call aborted", { cause: signal.reason }));
286
+ signal.addEventListener("abort", onAbort, { once: true });
287
+ });
288
+ try {
289
+ return await Promise.race([promise, aborted]);
290
+ }
291
+ finally {
292
+ signal.removeEventListener("abort", onAbort);
293
+ }
294
+ }
295
+ export const REF_BASE = new Set([
296
+ "contract",
297
+ "status",
298
+ "ready",
299
+ "variant",
300
+ "shape",
301
+ "error",
302
+ "onShape",
303
+ "onStatus",
304
+ "release",
305
+ ]);
306
+ /** Queues a status notification (after the change that caused it is complete, like every publication). */
307
+ export function notifyStatus(listeners, status, error, report) {
308
+ if (listeners.size === 0)
309
+ return;
310
+ const current = [...listeners];
311
+ enqueue(() => {
312
+ for (const listener of current) {
313
+ try {
314
+ listener(status, error);
315
+ }
316
+ catch (thrown) {
317
+ report(thrown);
318
+ }
319
+ }
320
+ });
321
+ }
322
+ /** Proxy → reference, for `deps: [ref]` and links. */
323
+ export const refsByProxy = new WeakMap();
324
+ export const refOf = (value) => value !== null && typeof value === "object" ? refsByProxy.get(value) : undefined;
325
+ /**
326
+ * A reference's state member: a stable readable that follows whatever source the current implementation exposes under
327
+ * that name. Reading throws `not_ready` until the reference is live, `not_in_variant` while the variant lacks the
328
+ * member. A listener added before the member is readable gets its snapshot then. Rebinding (reload, variant) delivers a
329
+ * reset. Upstream is subscribed only while someone listens.
330
+ */
331
+ class MemberReadable {
332
+ #ref;
333
+ #name;
334
+ #values = new Set();
335
+ #events = new Set();
336
+ /** Event listeners that have not had their snapshot yet (added while the member was not readable). */
337
+ #unseen = new Set();
338
+ #source;
339
+ #upstream = noop;
340
+ /** The value listeners last saw; a first upstream snapshot equal to it is not re-announced. */
341
+ #seen;
342
+ constructor(ref, name) {
343
+ this.#ref = ref;
344
+ this.#name = name;
345
+ }
346
+ get value() {
347
+ const ref = this.#ref;
348
+ if (ref.status === "released" || ref.status === "gone")
349
+ throw ref.terminalError();
350
+ if (ref.status !== "live")
351
+ throw notReady(`${ref.address}.${this.#name}`);
352
+ const view = ref.currentView();
353
+ if (view === undefined)
354
+ throw notReady(`${ref.address}.${this.#name}`);
355
+ const source = this.#current();
356
+ if (source === undefined) {
357
+ throw new ChordError("not_in_variant", `${ref.address}: ${this.#name} is not a state member of variant ${variantOf(view) ?? "(local)"}`);
358
+ }
359
+ return source.value;
360
+ }
361
+ /** Not hydrated while the reference is not live, or while its source (a forwarded remote member) is not. */
362
+ get [HYDRATION]() {
363
+ const ref = this.#ref;
364
+ if (ref.status === "live")
365
+ return hydrationOf(this.#current());
366
+ return {
367
+ hydrated: false,
368
+ whenHydrated: async () => {
369
+ await ref.ready;
370
+ await hydrationOf(this.#current())?.whenHydrated();
371
+ },
372
+ };
373
+ }
374
+ subscribe(listener) {
375
+ this.#values.add(listener);
376
+ const source = this.#readable();
377
+ if (source !== undefined) {
378
+ this.#seen = { value: source.value };
379
+ this.#safe(() => listener(source.value));
380
+ }
381
+ this.bind();
382
+ return () => this.#remove(this.#values, listener);
383
+ }
384
+ events(listener) {
385
+ this.#events.add(listener);
386
+ const source = this.#readable();
387
+ if (source !== undefined) {
388
+ const value = source.value;
389
+ this.#seen = { value };
390
+ this.#safe(() => listener({ type: "snap", value }));
391
+ }
392
+ else
393
+ this.#unseen.add(listener);
394
+ this.bind();
395
+ return () => {
396
+ this.#unseen.delete(listener);
397
+ this.#remove(this.#events, listener);
398
+ };
399
+ }
400
+ /** (Re)subscribes upstream to the current source; a changed source is announced as a reset. */
401
+ bind() {
402
+ const source = this.#values.size + this.#events.size > 0 ? this.#current() : undefined;
403
+ if (source === this.#source)
404
+ return;
405
+ this.#upstream();
406
+ this.#upstream = noop;
407
+ this.#source = source;
408
+ if (source === undefined)
409
+ return;
410
+ let first = true;
411
+ this.#upstream = source.events((event) => {
412
+ const isFirst = first;
413
+ first = false;
414
+ if (isFirst && this.#unseen.size === 0 && this.#seen !== undefined && event.value === this.#seen.value)
415
+ return;
416
+ this.#seen = { value: event.value };
417
+ const translated = isFirst ? { type: "reset", value: event.value } : event;
418
+ const events = [...this.#events];
419
+ const values = [...this.#values];
420
+ const unseen = new Set(this.#unseen);
421
+ this.#unseen.clear();
422
+ // Queued: a rebind during a commit must not run observers before the commit completes.
423
+ enqueue(() => {
424
+ for (const listener of events) {
425
+ if (!this.#events.has(listener))
426
+ continue;
427
+ const delivered = unseen.has(listener)
428
+ ? { type: "snap", value: event.value }
429
+ : translated;
430
+ this.#safe(() => listener(delivered));
431
+ }
432
+ for (const listener of values)
433
+ if (this.#values.has(listener))
434
+ this.#safe(() => listener(event.value));
435
+ });
436
+ });
437
+ }
438
+ stop() {
439
+ this.#values.clear();
440
+ this.#events.clear();
441
+ this.#unseen.clear();
442
+ this.bind();
443
+ }
444
+ #current() {
445
+ const view = this.#ref.currentView();
446
+ if (view === undefined || !this.#ref.inVariant(view, this.#name))
447
+ return undefined;
448
+ const value = view[this.#name];
449
+ // Cells and derivations, or any readable an `accepts` adapter maps them to.
450
+ return isSource(value) || isReadableLike(value) ? value : undefined;
451
+ }
452
+ /** The current source if its value can be read now (live, in the variant, hydrated). */
453
+ #readable() {
454
+ const source = this.#current();
455
+ return source === undefined || hydrationOf(source)?.hydrated === false ? undefined : source;
456
+ }
457
+ #remove(set, listener) {
458
+ set.delete(listener);
459
+ this.bind();
460
+ }
461
+ #safe(fn) {
462
+ try {
463
+ fn();
464
+ }
465
+ catch (error) {
466
+ this.#ref.object.host.report(error);
467
+ }
468
+ }
469
+ }
470
+ /** One reference on an object. Exposed through a Proxy so members bind late to the current implementation. */
471
+ export class RefImpl {
472
+ object;
473
+ contract;
474
+ owner;
475
+ address;
476
+ proxy;
477
+ ready;
478
+ status;
479
+ error;
480
+ shapeListeners = new Set();
481
+ /** Internal: notified when the reference becomes gone (links forward it). */
482
+ goneListeners = new Set();
483
+ readables = new Map();
484
+ /** Weak: a retired code version's service (and the factory code it holds) must not be kept alive by its consumers. */
485
+ bindings = new WeakMap();
486
+ views = new WeakMap();
487
+ callers = new Map();
488
+ constructor(object, contract, owner) {
489
+ this.object = object;
490
+ this.contract = contract;
491
+ this.owner = owner;
492
+ this.address = object.address;
493
+ this.status = object.status === "live" ? "live" : "loading";
494
+ this.ready = object.ready.then(() => {
495
+ if (this.status === "gone")
496
+ throw this.error;
497
+ if (this.status === "released")
498
+ throw new ChordError("released", `reference ${this.address} released`);
499
+ });
500
+ this.ready.catch(noop);
501
+ this.proxy = new Proxy(this, {
502
+ get: (target, property) => {
503
+ if (REF_BASE.has(property)) {
504
+ const value = Reflect.get(target, property, target);
505
+ return typeof value === "function" ? value.bind(target) : value;
506
+ }
507
+ if (typeof property !== "string" || property === "then")
508
+ return undefined;
509
+ return target.member(property);
510
+ },
511
+ has: (target, property) => REF_BASE.has(property) || target.member(String(property)) !== undefined,
512
+ });
513
+ refsByProxy.set(this.proxy, this);
514
+ }
515
+ get variant() {
516
+ const view = this.currentView();
517
+ return view === undefined ? undefined : variantOf(view);
518
+ }
519
+ get shape() {
520
+ const installed = this.status === "live" ? this.object.currentInstalled() : undefined;
521
+ const view = installed === undefined ? undefined : this.#safeView(installed);
522
+ if (installed === undefined || view === undefined)
523
+ return undefined;
524
+ return {
525
+ variant: variantOf(view),
526
+ members: Object.keys(view).filter((key) => key !== "variant"),
527
+ contract: installed.service.contract.fingerprint,
528
+ };
529
+ }
530
+ onShape(listener) {
531
+ this.shapeListeners.add(listener);
532
+ return () => this.shapeListeners.delete(listener);
533
+ }
534
+ onStatus(listener) {
535
+ this.#statusListeners.add(listener);
536
+ return () => this.#statusListeners.delete(listener);
537
+ }
538
+ #statusListeners = new Set();
539
+ #statusChanged() {
540
+ notifyStatus(this.#statusListeners, this.status, this.error, (error) => this.object.host.report(error));
541
+ }
542
+ release() {
543
+ if (this.status === "released" || this.status === "gone")
544
+ return;
545
+ this.status = "released";
546
+ this.#stopReadables();
547
+ this.#statusChanged();
548
+ this.object.release(this);
549
+ }
550
+ // ─── internal ───
551
+ live() {
552
+ if (this.status !== "loading")
553
+ return;
554
+ this.status = "live";
555
+ this.#statusChanged();
556
+ this.shapeChanged();
557
+ }
558
+ gone(error) {
559
+ if (this.status === "released" || this.status === "gone")
560
+ return;
561
+ this.status = "gone";
562
+ this.error = error;
563
+ this.#stopReadables();
564
+ this.#statusChanged();
565
+ for (const listener of [...this.goneListeners])
566
+ listener(error);
567
+ }
568
+ shapeChanged() {
569
+ for (const readable of this.readables.values())
570
+ readable.bind();
571
+ const shape = this.shape;
572
+ if (shape === undefined)
573
+ return;
574
+ const listeners = [...this.shapeListeners];
575
+ enqueue(() => {
576
+ for (const listener of listeners) {
577
+ try {
578
+ listener(shape);
579
+ }
580
+ catch (error) {
581
+ this.object.host.report(error);
582
+ }
583
+ }
584
+ });
585
+ }
586
+ terminalError() {
587
+ return this.status === "gone" ? this.error : new ChordError("released", `reference ${this.address} released`);
588
+ }
589
+ /** The implementation as this consumer sees it (through an accepts adapter when bound to another version). */
590
+ viewOf(installed) {
591
+ let adapter = this.bindings.get(installed.service);
592
+ if (!this.bindings.has(installed.service)) {
593
+ adapter = bind(installed.service.contract, installed.service.options.accepts, this.contract);
594
+ this.bindings.set(installed.service, adapter);
595
+ }
596
+ if (adapter === undefined)
597
+ return installed.implementation;
598
+ let view = this.views.get(installed.implementation);
599
+ if (view === undefined) {
600
+ view = adapter(installed.implementation);
601
+ this.views.set(installed.implementation, view);
602
+ }
603
+ return view;
604
+ }
605
+ currentView() {
606
+ if (this.status !== "live")
607
+ return undefined;
608
+ const installed = this.object.currentInstalled();
609
+ return installed === undefined ? undefined : this.#safeView(installed);
610
+ }
611
+ /** The view, or undefined when this consumer no longer binds (reported; calls reject with the bind error). */
612
+ #safeView(installed) {
613
+ try {
614
+ return this.viewOf(installed);
615
+ }
616
+ catch (error) {
617
+ this.object.host.report(error);
618
+ return undefined;
619
+ }
620
+ }
621
+ describe(variant, member) {
622
+ const variants = this.contract.description.variants;
623
+ if (variant !== undefined)
624
+ return variants[variant]?.[member];
625
+ for (const members of Object.values(variants))
626
+ if (members[member] !== undefined)
627
+ return members[member];
628
+ return undefined;
629
+ }
630
+ /** Remote consumers see only the members their contract lists for the current variant. */
631
+ inVariant(view, name) {
632
+ return this.contract.scope !== "remote" || this.describe(variantOf(view), name) !== undefined;
633
+ }
634
+ member(name) {
635
+ const declared = this.describe(undefined, name);
636
+ const view = this.currentView();
637
+ const value = view?.[name];
638
+ if (declared?.kind === "method" || (declared === undefined && typeof value === "function")) {
639
+ return this.caller(name);
640
+ }
641
+ if (declared?.kind === "state" || (declared === undefined && (isSource(value) || isReadableLike(value)))) {
642
+ let readable = this.readables.get(name);
643
+ if (readable === undefined) {
644
+ readable = new MemberReadable(this, name);
645
+ this.readables.set(name, readable);
646
+ }
647
+ return readable;
648
+ }
649
+ if (view === undefined || !this.inVariant(view, name))
650
+ return undefined;
651
+ return value instanceof ExposedImpl ? value.resource.value : value;
652
+ }
653
+ caller(name) {
654
+ let caller = this.callers.get(name);
655
+ if (caller === undefined) {
656
+ const noArgs = takesNoArgs(this.contract, name);
657
+ caller = (...given) => {
658
+ // A method without arguments is called as `ref.name(ctx)`.
659
+ const [args, ctx] = (noArgs ? [undefined, given[0]] : given);
660
+ if (this.object.host.inFactory > 0) {
661
+ throw new ChordError("not_ready", `${this.address}.${name} called inside a factory`);
662
+ }
663
+ if (ctx !== undefined && !(ctx instanceof CtxImpl)) {
664
+ return Promise.reject(invalid("foreign_ctx", "ctx must come from a Chord host"));
665
+ }
666
+ if (ctx === undefined || ctx.holder) {
667
+ // A code holder (task) starts a fresh root per call, cancelled with the holder.
668
+ try {
669
+ ctx?.check(this.object.host);
670
+ }
671
+ catch (error) {
672
+ return Promise.reject(error);
673
+ }
674
+ return this.object.host.root((root) => this.object.call(this, name, args, root), {
675
+ signal: ctx?.signal,
676
+ });
677
+ }
678
+ return this.object.call(this, name, args, ctx);
679
+ };
680
+ this.callers.set(name, caller);
681
+ }
682
+ return caller;
683
+ }
684
+ #stopReadables() {
685
+ for (const readable of this.readables.values())
686
+ readable.stop();
687
+ }
688
+ }
689
+ export const variantOf = (implementation) => typeof implementation.variant === "string" ? implementation.variant : undefined;
690
+ /**
691
+ * Two `select` functions are the same if they are identical or have the same source text: an inline arrow is a new
692
+ * object on every factory run, but its code is what matters. (A select whose implementation depends on captured values
693
+ * that change between runs is not detected; such a listener keeps its old subscription until its source changes.)
694
+ */
695
+ const sameSelect = (a, b) => a === b || (a !== undefined && b !== undefined && String(a) === String(b));
696
+ export class ObjectImpl {
697
+ host;
698
+ address;
699
+ typeName;
700
+ input;
701
+ principal;
702
+ /** The one object of a `lifetime: "host"` type: only such objects may register. */
703
+ hostLifetime;
704
+ status = "activating";
705
+ ready = Promise.resolve();
706
+ service;
707
+ /** Calls carrying an older code version are rejected upgrade_conflict (state bump or exclusive resource replaced). */
708
+ minCodeVersion = 0;
709
+ decls = new Decls();
710
+ /** Declarations of a factory run that has not been installed yet (activation or reload candidate). */
711
+ pending = new Set();
712
+ implementations = new Map();
713
+ refs = new Set();
714
+ leases = new Map();
715
+ openOrder = [];
716
+ callAbort = new AbortController();
717
+ activationAbort = new AbortController();
718
+ gate;
719
+ /** Served only by a running reload's pending code version: listeners, tasks and registrations start at its commit. */
720
+ deferredRuntime = false;
721
+ #subscriptions = new Map();
722
+ #invocations = new Set();
723
+ #tasks = new Map();
724
+ /** Aborted task runs that have not finished yet. */
725
+ #stopped = new Set();
726
+ /** Bumped by every start and stop of the tasks: a delayed start that is no longer the latest is dropped. */
727
+ #taskRuns = 0;
728
+ #linger;
729
+ #disposal;
730
+ constructor(host, identity) {
731
+ this.host = host;
732
+ this.address = identity.address;
733
+ this.typeName = identity.typeName;
734
+ this.input = identity.input;
735
+ this.principal = identity.principal;
736
+ this.hostLifetime = identity.hostLifetime;
737
+ }
738
+ start(after) {
739
+ this.ready = this.#activate(after).then(() => {
740
+ for (const ref of this.refs)
741
+ ref.live();
742
+ this.#maybeIdle();
743
+ }, (cause) => {
744
+ if (this.status === "activating")
745
+ this.status = "failed";
746
+ this.host.forget(this);
747
+ // A ChordError (forbidden, a dependency's error, ...) keeps its code; anything else is `unavailable`.
748
+ const error = asChordError(cause, `activating ${this.address}`);
749
+ for (const ref of this.refs)
750
+ ref.gone(error);
751
+ throw error;
752
+ });
753
+ this.ready.catch(noop);
754
+ }
755
+ /**
756
+ * Activation as one transaction: factory, opens, then a synchronous finalization (registry check and publication,
757
+ * implementations, runtime). Every failure, including one during finalization or a release while listeners start,
758
+ * unwinds everything this activation did.
759
+ */
760
+ async #activate(after) {
761
+ await after?.catch(noop);
762
+ const signal = this.activationAbort.signal;
763
+ const abandoned = () => new ChordError("aborted", `${this.address} abandoned`);
764
+ if (signal.aborted)
765
+ throw abandoned();
766
+ const service = this.host.service(this.typeName);
767
+ if (service === undefined)
768
+ throw new ChordError("unavailable", `${this.typeName} is not served`, { reason: "unknown_type" });
769
+ this.service = service;
770
+ const run = this.run(service, undefined);
771
+ try {
772
+ await this.open(run.created.resources, run.created.refs, signal);
773
+ if (signal.aborted)
774
+ throw abandoned();
775
+ await this.waitHydrated(run);
776
+ if (signal.aborted)
777
+ throw abandoned();
778
+ await this.initCells(run, signal);
779
+ if (signal.aborted)
780
+ throw abandoned();
781
+ this.host.checkRegistrations(this, run.decls);
782
+ this.decls = run.decls;
783
+ for (const [retained, services] of this.host.retainedCodeVersions()) {
784
+ if (services.get(this.typeName) === service) {
785
+ this.implementations.set(retained, {
786
+ codeVersion: retained,
787
+ service,
788
+ implementation: run.implementation,
789
+ decls: run.decls,
790
+ });
791
+ }
792
+ }
793
+ const pending = this.host.pendingCodeVersion();
794
+ if (!this.implementations.has(this.host.currentCodeVersion()) &&
795
+ (pending === undefined || !this.implementations.has(pending))) {
796
+ throw new ChordError("superseded", `${this.address}: code changed during activation`);
797
+ }
798
+ for (const derived of run.created.derives)
799
+ derived.attach();
800
+ this.refreshSchemas();
801
+ // Served only by a running reload's pending code version: new code must not run (listeners, tasks,
802
+ // registrations) before that reload commits.
803
+ this.deferredRuntime = !this.implementations.has(this.host.currentCodeVersion());
804
+ if (!this.deferredRuntime) {
805
+ if (run.decls.registrations.size > 0)
806
+ this.host.registriesChanged(this);
807
+ this.startRuntime();
808
+ }
809
+ // A listener starting up may have released the last reference synchronously: no resurrection.
810
+ if (signal.aborted)
811
+ throw abandoned();
812
+ this.status = "live";
813
+ }
814
+ catch (error) {
815
+ // Invocations started during finalization (listener roots) end before their resources close.
816
+ this.callAbort.abort(error);
817
+ this.stopListeners();
818
+ this.stopTasks();
819
+ const registered = this.decls.registrations.size > 0 && !this.deferredRuntime;
820
+ this.implementations.clear();
821
+ this.decls = new Decls();
822
+ if (registered)
823
+ this.host.registriesChanged();
824
+ await Promise.allSettled([...this.#invocations, ...this.#stopped]);
825
+ await this.unwind(run).catch((cleanup) => this.host.report(cleanup));
826
+ throw error;
827
+ }
828
+ finally {
829
+ this.pending.delete(run.decls);
830
+ }
831
+ }
832
+ /**
833
+ * Runs the factory synchronously. With `previous`, runs in recording mode for a reload prepare. The run's
834
+ * declarations stay in `pending` (visible to the cycle check) until the caller installs or discards them.
835
+ */
836
+ run(service, previous) {
837
+ const self = new SelfImpl(this, previous);
838
+ const discard = (run) => {
839
+ this.pending.delete(run.decls);
840
+ void this.unwind(run).catch((cleanup) => this.host.report(cleanup));
841
+ };
842
+ this.pending.add(self.run(undefined).decls);
843
+ this.host.inFactory++;
844
+ let implementation;
845
+ try {
846
+ implementation = service.factory(this.input, this.principal, self);
847
+ }
848
+ catch (error) {
849
+ this.host.inFactory--;
850
+ discard(self.run(undefined));
851
+ throw error;
852
+ }
853
+ this.host.inFactory--;
854
+ const run = self.run(implementation);
855
+ try {
856
+ if (implementation instanceof Promise)
857
+ throw invalid("implementation", `${this.typeName}: factories must be synchronous`);
858
+ validateImplementation(service, run.implementation);
859
+ }
860
+ catch (error) {
861
+ discard(run);
862
+ throw error;
863
+ }
864
+ return run;
865
+ }
866
+ /** Opens resources (dependencies first) and waits for references; settles all before failing. */
867
+ async open(resources, refs, signal) {
868
+ const opening = new Map();
869
+ const openOne = (resource) => {
870
+ let promise = opening.get(resource);
871
+ if (promise === undefined) {
872
+ promise = (async () => {
873
+ await Promise.all(resource.deps.map((dep) => !(dep instanceof ResourceImpl) ? dep.ready : resources.includes(dep) ? openOne(dep) : undefined));
874
+ await resource.open(signal, this.openOrder);
875
+ })();
876
+ opening.set(resource, promise);
877
+ }
878
+ return promise;
879
+ };
880
+ const ready = this.withinReadyTimeout(Promise.all(refs.map((ref) => ref.ready)), () => refs.filter((ref) => ref.status === "loading").map((ref) => ref.address));
881
+ const results = await Promise.allSettled([...resources.map(openOne), ready]);
882
+ const failure = results.find((result) => result.status === "rejected");
883
+ if (failure !== undefined)
884
+ throw failure.reason;
885
+ }
886
+ /**
887
+ * Waits until the state sources a run exposes or derives from have their values: a local object goes live only with
888
+ * its state, also where it forwards or derives from a remote reference's member.
889
+ */
890
+ async waitHydrated(run) {
891
+ const exposed = Object.entries(run.implementation ?? {});
892
+ const pending = unhydrated([...run.created.derives, ...exposed.map(([, value]) => value)]);
893
+ if (pending.length === 0)
894
+ return;
895
+ await this.withinReadyTimeout(Promise.all(pending.map((hydration) => hydration.whenHydrated())), () => [
896
+ ...exposed
897
+ .filter(([, value]) => hydrationOf(value)?.hydrated === false)
898
+ .map(([name]) => `${this.address}.${name}`),
899
+ ...run.created.derives
900
+ .filter((derived) => hydrationOf(derived) !== undefined)
901
+ .map((derived) => `derive ${derived.key}`),
902
+ ]);
903
+ }
904
+ /** `work` within the host's `readyTimeout`; a timeout names what is still awaited. */
905
+ async withinReadyTimeout(work, waiting) {
906
+ const ms = this.host.readyTimeout;
907
+ if (!Number.isFinite(ms))
908
+ return work;
909
+ let timer;
910
+ const timeout = new Promise((_, reject) => {
911
+ timer = setTimeout(() => {
912
+ const list = waiting();
913
+ reject(new ChordError("unavailable", `${this.address} was not ready after ${ms} ms${list.length === 0 ? "" : `, waiting on ${list.join(", ")}`} (a reference cycle across hosts?)`, { reason: "ready_timeout" }));
914
+ }, ms);
915
+ timer.unref?.();
916
+ });
917
+ timeout.catch(noop);
918
+ try {
919
+ return await Promise.race([work, timeout]);
920
+ }
921
+ finally {
922
+ clearTimeout(timer);
923
+ }
924
+ }
925
+ /**
926
+ * Fills the cells `run` created with `init`, each in a root of its own, in parallel; settles all before failing. Runs
927
+ * after the run's resources are open and its references live, before anything reads the cells.
928
+ */
929
+ async initCells(run, signal) {
930
+ if (run.created.inits.length === 0)
931
+ return;
932
+ const results = await Promise.allSettled(run.created.inits.map(async ([cell, init]) => {
933
+ const value = await this.host.root((ctx) => init(ctx), { signal });
934
+ if (!signal.aborted)
935
+ cell.initialize(value);
936
+ }));
937
+ const failure = results.find((result) => result.status === "rejected");
938
+ if (failure !== undefined)
939
+ throw failure.reason;
940
+ }
941
+ /** Undoes the objects a run created: closes its resources (reverse order) and releases its references. */
942
+ async unwind(run) {
943
+ for (const derived of run.created.derives)
944
+ derived.detach();
945
+ const resources = [...run.created.resources].sort((a, b) => this.openOrder.indexOf(b) - this.openOrder.indexOf(a));
946
+ await runAll([
947
+ ...resources.map((resource) => () => resource.close(this.openOrder)),
948
+ ...run.created.refs.map((ref) => () => ref.release()),
949
+ ], `${this.address}: cleanup failed`);
950
+ }
951
+ currentInstalled() {
952
+ return this.implementations.get(this.host.currentCodeVersion()) ?? [...this.implementations.values()].at(-1);
953
+ }
954
+ /** Every state source exposed as a remote state member gets that member's schema (checked per write in dev mode). */
955
+ refreshSchemas() {
956
+ const schemas = new Map();
957
+ for (const installed of this.implementations.values()) {
958
+ for (const [source, list] of memberSchemas(installed.service, installed.implementation)) {
959
+ schemas.set(source, [...(schemas.get(source) ?? []), ...list]);
960
+ }
961
+ }
962
+ for (const decls of this.allDecls()) {
963
+ for (const source of [...decls.cells.values(), ...decls.derives.values()])
964
+ source.schemas = schemas.get(source) ?? [];
965
+ }
966
+ }
967
+ allDecls() {
968
+ const result = new Set([this.decls, ...this.pending]);
969
+ for (const installed of this.implementations.values())
970
+ result.add(installed.decls);
971
+ return result;
972
+ }
973
+ // ─── runtime: listeners and tasks of the current declarations ───
974
+ /** The resource a listener is declared on, or its source itself. */
975
+ #origin(decl) {
976
+ return resourceOf(decl.source) ?? decl.source;
977
+ }
978
+ /** Resolves a listener's subscribable: `select(resource value)`, `select(source)` or the source itself. */
979
+ #listenSource(key, decl) {
980
+ const resource = resourceOf(decl.source);
981
+ const value = resource === undefined ? decl.source : resource.value;
982
+ const source = (decl.select === undefined ? value : decl.select(value));
983
+ if (typeof source?.subscribe !== "function")
984
+ throw invalid("implementation", `listener ${key}: source has no subscribe()`);
985
+ return source;
986
+ }
987
+ /**
988
+ * Subscribes the listeners of `decls` without delivering: events are buffered until `activate`. A listener declared
989
+ * on the same resource (or source) with the same `select` keeps its subscription without calling `select` again
990
+ * (only its function is swapped, by name), so sources that accept a single subscriber survive reloads. Throws, with
991
+ * nothing left subscribed, if any source fails.
992
+ */
993
+ stageListeners(decls) {
994
+ const fresh = new Map();
995
+ const kept = new Set();
996
+ try {
997
+ for (const [key, decl] of decls.listeners) {
998
+ const origin = this.#origin(decl);
999
+ const previous = this.#subscriptions.get(key);
1000
+ if (previous !== undefined && previous.origin === origin && sameSelect(previous.select, decl.select)) {
1001
+ kept.add(key);
1002
+ continue;
1003
+ }
1004
+ const source = this.#listenSource(key, decl);
1005
+ const entry = {
1006
+ origin,
1007
+ select: decl.select,
1008
+ unsubscribe: noop,
1009
+ buffer: [],
1010
+ };
1011
+ fresh.set(key, entry);
1012
+ // State (a readable) is subscribed when the listener activates: it delivers the value current then, not one
1013
+ // of the candidate before the commit (a new derivation computes its value at the commit).
1014
+ if (isSource(source) || isReadableLike(source)) {
1015
+ entry.buffer = undefined;
1016
+ entry.subscribe = () => {
1017
+ entry.unsubscribe = source.subscribe((event) => this.#deliver(key, event));
1018
+ };
1019
+ continue;
1020
+ }
1021
+ entry.unsubscribe = source.subscribe((event) => {
1022
+ if (entry.buffer !== undefined)
1023
+ entry.buffer.push(event);
1024
+ else
1025
+ this.#deliver(key, event);
1026
+ });
1027
+ }
1028
+ }
1029
+ catch (error) {
1030
+ for (const entry of fresh.values())
1031
+ entry.unsubscribe();
1032
+ throw error;
1033
+ }
1034
+ return {
1035
+ activate: () => {
1036
+ for (const [key, subscription] of this.#subscriptions) {
1037
+ if (kept.has(key))
1038
+ continue;
1039
+ subscription.unsubscribe();
1040
+ this.#subscriptions.delete(key);
1041
+ }
1042
+ for (const [key, entry] of fresh) {
1043
+ this.#subscriptions.set(key, entry);
1044
+ entry.subscribe?.();
1045
+ const buffered = entry.buffer ?? [];
1046
+ entry.buffer = undefined;
1047
+ for (const event of buffered)
1048
+ this.#deliver(key, event);
1049
+ }
1050
+ },
1051
+ discard: () => {
1052
+ for (const entry of fresh.values())
1053
+ entry.unsubscribe();
1054
+ },
1055
+ };
1056
+ }
1057
+ startRuntime() {
1058
+ this.stageListeners(this.decls).activate();
1059
+ this.startTasks();
1060
+ }
1061
+ /**
1062
+ * Starts the tasks of the current declarations. Runs aborted by `stopTasks` (a reload, a gate) may still be finishing:
1063
+ * their replacements start once they all finished, or after `taskStopDeadline` (reported), so the old and the new run
1064
+ * of a task never work side by side. A later start or stop, or the object leaving `live`, cancels a delayed start.
1065
+ */
1066
+ startTasks() {
1067
+ const run = ++this.#taskRuns;
1068
+ const start = () => {
1069
+ for (const key of this.decls.tasks.keys())
1070
+ this.#startTask(key, this.host.currentCodeVersion());
1071
+ };
1072
+ if (this.#stopped.size === 0) {
1073
+ start();
1074
+ return;
1075
+ }
1076
+ const deadline = this.host.taskStopDeadline;
1077
+ let timer;
1078
+ const late = new Promise((resolve) => {
1079
+ timer = setTimeout(() => resolve(true), deadline);
1080
+ });
1081
+ const stopped = Promise.allSettled([...this.#stopped]).then(() => false);
1082
+ void Promise.race([stopped, late]).then((missed) => {
1083
+ clearTimeout(timer);
1084
+ if (run !== this.#taskRuns || this.status !== "live")
1085
+ return;
1086
+ if (missed) {
1087
+ this.host.report(new ChordError("aborted", `${this.address}: aborted tasks still running after ${deadline} ms; starting their replacements anyway`));
1088
+ }
1089
+ start();
1090
+ });
1091
+ }
1092
+ stopListeners() {
1093
+ for (const subscription of this.#subscriptions.values())
1094
+ subscription.unsubscribe();
1095
+ this.#subscriptions.clear();
1096
+ }
1097
+ #startTask(key, codeVersion) {
1098
+ const fn = this.decls.tasks.get(key);
1099
+ if (fn === undefined)
1100
+ return;
1101
+ const controller = new AbortController();
1102
+ const ctx = new CtxImpl(this.host, {
1103
+ signals: [controller.signal, this.callAbort.signal],
1104
+ rootId: `task:${this.address}:${key}`,
1105
+ codeVersion,
1106
+ principal: this.host.principal,
1107
+ holder: true,
1108
+ });
1109
+ const done = (async () => {
1110
+ try {
1111
+ await fn(ctx);
1112
+ }
1113
+ catch (error) {
1114
+ if (!ctx.signal.aborted)
1115
+ this.host.report(error);
1116
+ }
1117
+ finally {
1118
+ ctx.close();
1119
+ if (this.#tasks.get(key)?.controller === controller)
1120
+ this.#tasks.delete(key);
1121
+ }
1122
+ })();
1123
+ this.#tasks.set(key, { controller, done });
1124
+ }
1125
+ /**
1126
+ * Aborts running tasks (and cancels a delayed start). Their completion is awaited by the next `startTasks` and at
1127
+ * disposal; each leaves `#stopped` when it finishes.
1128
+ */
1129
+ stopTasks() {
1130
+ this.#taskRuns++;
1131
+ for (const task of this.#tasks.values()) {
1132
+ task.controller.abort(new ChordError("aborted", "task stopped"));
1133
+ const done = task.done;
1134
+ this.#stopped.add(done);
1135
+ void done.finally(() => this.#stopped.delete(done));
1136
+ }
1137
+ this.#tasks.clear();
1138
+ }
1139
+ /**
1140
+ * Delivers one listener event, in publication order. The invocation is a root that references a lease on this object
1141
+ * (a reload gate waits for it like for any other root that touched the object), counts as an invocation (disposal
1142
+ * waits for it) and is cancelled by forced disposal.
1143
+ */
1144
+ #deliver(key, event) {
1145
+ enqueue(() => {
1146
+ if (this.gate !== undefined) {
1147
+ this.gate.queue.push({ key, event });
1148
+ return;
1149
+ }
1150
+ const decl = this.decls.listeners.get(key);
1151
+ if (decl === undefined || (this.status !== "live" && this.status !== "activating"))
1152
+ return;
1153
+ const invocation = this.host.root((root) => {
1154
+ this.lease(root);
1155
+ const child = root.child(this);
1156
+ return this.#invoke(async () => {
1157
+ try {
1158
+ await decl.fn(event, child);
1159
+ }
1160
+ finally {
1161
+ child.close();
1162
+ }
1163
+ });
1164
+ });
1165
+ invocation.catch((error) => this.host.report(error));
1166
+ });
1167
+ }
1168
+ /** Opens the object's gate and replays queued listener events through the current listeners. */
1169
+ openGate() {
1170
+ const gate = this.gate;
1171
+ this.gate = undefined;
1172
+ for (const { key, event } of gate?.queue ?? [])
1173
+ this.#deliver(key, event);
1174
+ }
1175
+ // ─── calls ───
1176
+ /** Admission for a call or write carrying `ctx`. */
1177
+ admit(ctx) {
1178
+ if (this.host.failed !== undefined) {
1179
+ throw new ChordError("unavailable", `${this.host.name} failed and admits no work`, { reason: "host_failed" });
1180
+ }
1181
+ if (!this.host.hasCodeVersion(ctx.codeVersion))
1182
+ throw new ChordError("superseded", `code version ${ctx.codeVersion} is retired`);
1183
+ if (ctx.codeVersion < this.minCodeVersion) {
1184
+ throw new ChordError("upgrade_conflict", `${this.address} was upgraded in code version ${this.minCodeVersion}`);
1185
+ }
1186
+ if (this.gate !== undefined && !this.gate.grandfathered.has(ctx.rootId)) {
1187
+ throw new ChordError("upgrade_conflict", `${this.address} is being upgraded`);
1188
+ }
1189
+ }
1190
+ /** Records that the root of `ctx` touched this object (a reload gate grandfathers it). */
1191
+ lease(ctx) {
1192
+ if (this.leases.has(ctx.rootId))
1193
+ return;
1194
+ this.leases.set(ctx.rootId, ctx.codeVersion);
1195
+ this.host.touch(ctx.rootId, this);
1196
+ }
1197
+ guard = (ctx) => {
1198
+ if (this.host.inFactory > 0)
1199
+ throw invalid("read_only", "cells cannot be written while a factory runs");
1200
+ if (ctx instanceof CtxImpl)
1201
+ ctx.check(this.host);
1202
+ if (ctx.signal.aborted)
1203
+ throw new ChordError("aborted", "write with an aborted ctx");
1204
+ if (this.status === "disposed")
1205
+ throw new ChordError("gone", `${this.address} is disposed`);
1206
+ if (ctx.rootId.startsWith("task:"))
1207
+ return;
1208
+ this.admit(ctx);
1209
+ };
1210
+ async call(ref, name, args, ctx) {
1211
+ ctx.check(this.host);
1212
+ if (ref.status === "released" || ref.status === "gone")
1213
+ throw ref.terminalError();
1214
+ if (ref.status === "loading")
1215
+ await raceSignal(ref.ready, ctx.signal);
1216
+ if (ref.status !== "live" || this.status !== "live") {
1217
+ throw ref.status === "live"
1218
+ ? new ChordError("gone", `${this.address} is ${this.status}`)
1219
+ : ref.terminalError();
1220
+ }
1221
+ this.admit(ctx);
1222
+ const installed = this.implementations.get(ctx.codeVersion);
1223
+ if (installed === undefined)
1224
+ throw new ChordError("superseded", `${this.address} has no implementation for code version ${ctx.codeVersion}`);
1225
+ const view = ref.viewOf(installed);
1226
+ const variant = variantOf(view);
1227
+ const wanted = ref.describe(ref.contract.scope === "remote" ? variant : LOCAL_VARIANT, name);
1228
+ const fn = view[name];
1229
+ if (typeof fn !== "function" || (ref.contract.scope === "remote" && wanted?.kind !== "method")) {
1230
+ throw new ChordError("not_in_variant", `${this.address}: ${name} is not a method of variant ${variant ?? "(local)"}`);
1231
+ }
1232
+ const direct = view === installed.implementation;
1233
+ const schema = direct
1234
+ ? installed.service.contract.description.variants[variant ?? LOCAL_VARIANT]?.[name]
1235
+ : wanted;
1236
+ const method = schema?.kind === "method" ? schema : undefined;
1237
+ const validated = ref.contract.scope === "remote" ? method : undefined;
1238
+ // No arguments: the handler takes only ctx. No result (void): the caller gets undefined, whatever it returned.
1239
+ const takesArgs = method === undefined || method.args !== undefined;
1240
+ const returns = method === undefined || method.result !== undefined;
1241
+ let input;
1242
+ if (takesArgs) {
1243
+ // The additive rule assumes a consumer sends only what its own schema knows, and a provider sees only what its
1244
+ // schema knows: strip with both before validating.
1245
+ const own = ref.contract.scope === "remote" && wanted?.kind === "method" ? wanted.args : undefined;
1246
+ const sent = own === undefined ? args : Value.Clean(own, Value.Clone(args));
1247
+ input = validated?.args ? Value.Clean(validated.args, Value.Clone(sent)) : sent;
1248
+ if (validated?.args)
1249
+ assertValid("args", `${this.address}.${name} args`, validated.args, input);
1250
+ }
1251
+ this.lease(ctx);
1252
+ const child = ctx.child(this);
1253
+ let result;
1254
+ try {
1255
+ result = await this.#invoke(() => takesArgs
1256
+ ? fn(input, child)
1257
+ : fn(child));
1258
+ }
1259
+ finally {
1260
+ child.close();
1261
+ }
1262
+ if (!returns)
1263
+ return undefined;
1264
+ if (validated?.result)
1265
+ assertValid("result", `${this.address}.${name} result`, validated.result, result);
1266
+ return result;
1267
+ }
1268
+ /** Runs an invocation (method or listener) that keeps the object alive until it settles. */
1269
+ #invoke(fn) {
1270
+ const invocation = (async () => fn())();
1271
+ this.#invocations.add(invocation);
1272
+ const done = () => {
1273
+ this.#invocations.delete(invocation);
1274
+ if (this.#invocations.size === 0)
1275
+ this.#maybeIdle();
1276
+ };
1277
+ invocation.then(done, done);
1278
+ return invocation;
1279
+ }
1280
+ rootEnded(rootId) {
1281
+ this.leases.delete(rootId);
1282
+ const gate = this.gate;
1283
+ if (gate?.grandfathered.delete(rootId) && gate.grandfathered.size === 0)
1284
+ gate.drained();
1285
+ }
1286
+ // ─── lifetime ───
1287
+ addRef(ref) {
1288
+ this.refs.add(ref);
1289
+ if (this.#linger !== undefined)
1290
+ clearTimeout(this.#linger);
1291
+ this.#linger = undefined;
1292
+ }
1293
+ release(ref) {
1294
+ if (this.refs.delete(ref))
1295
+ this.#maybeIdle();
1296
+ }
1297
+ /** No references and no invocations: dispose (after `linger` for a live object). */
1298
+ #maybeIdle() {
1299
+ if (this.refs.size > 0 || this.#invocations.size > 0 || this.#linger !== undefined)
1300
+ return;
1301
+ if (this.status === "activating") {
1302
+ this.#background(this.dispose(false));
1303
+ return;
1304
+ }
1305
+ if (this.status !== "live")
1306
+ return;
1307
+ const linger = this.service?.options.linger ?? 0;
1308
+ if (linger <= 0) {
1309
+ this.#background(this.dispose(false));
1310
+ return;
1311
+ }
1312
+ this.#linger = setTimeout(() => {
1313
+ this.#linger = undefined;
1314
+ if (this.refs.size === 0 && this.#invocations.size === 0 && this.status === "live") {
1315
+ this.#background(this.dispose(false));
1316
+ }
1317
+ }, linger);
1318
+ this.#linger.unref?.();
1319
+ }
1320
+ #background(disposal) {
1321
+ disposal.catch((error) => this.host.report(error));
1322
+ }
1323
+ /**
1324
+ * Disposes the object. Forced: references become gone at once (new calls are rejected) and running invocations are
1325
+ * cancelled. Either way, resources close only after every invocation settled.
1326
+ */
1327
+ dispose(forced, reason) {
1328
+ this.#disposal ??= this.#dispose(forced, reason);
1329
+ return this.#disposal;
1330
+ }
1331
+ async #dispose(forced, reason) {
1332
+ const wasActivating = this.status === "activating";
1333
+ this.status = "disposing";
1334
+ this.host.forget(this);
1335
+ if (this.#linger !== undefined)
1336
+ clearTimeout(this.#linger);
1337
+ this.#linger = undefined;
1338
+ const error = reason ?? new ChordError("gone", `${this.address} was disposed`);
1339
+ if (forced) {
1340
+ for (const ref of this.refs)
1341
+ ref.gone(error);
1342
+ this.callAbort.abort(error);
1343
+ }
1344
+ this.activationAbort.abort(error);
1345
+ if (wasActivating)
1346
+ await this.ready.catch(noop);
1347
+ for (const ref of this.refs)
1348
+ ref.gone(error);
1349
+ const decls = this.allDecls();
1350
+ this.stopTasks();
1351
+ this.stopListeners();
1352
+ await Promise.allSettled([...this.#invocations]);
1353
+ const registered = [...decls].some((set) => set.registrations.size > 0) && !this.deferredRuntime;
1354
+ this.implementations.clear();
1355
+ this.decls = new Decls();
1356
+ if (registered)
1357
+ this.host.registriesChanged();
1358
+ const refs = new Set();
1359
+ for (const set of decls) {
1360
+ for (const derived of set.derives.values())
1361
+ derived.detach();
1362
+ for (const ref of set.refs.values())
1363
+ refs.add(ref);
1364
+ }
1365
+ try {
1366
+ await runAll([
1367
+ () => Promise.allSettled([...this.#stopped]),
1368
+ ...[...this.openOrder].reverse().map((resource) => () => resource.close(this.openOrder)),
1369
+ ...[...refs].map((ref) => () => ref.release()),
1370
+ ], `${this.address}: disposal failed`);
1371
+ }
1372
+ finally {
1373
+ this.status = "disposed";
1374
+ this.host.disposed(this);
1375
+ }
1376
+ }
1377
+ }
1378
+ const isReadableLike = (value) => value !== null &&
1379
+ typeof value === "object" &&
1380
+ typeof value.events === "function" &&
1381
+ typeof value.subscribe === "function";
1382
+ /** The remote state-member schemas an implementation exposes, by source. */
1383
+ export function memberSchemas(service, implementation) {
1384
+ const schemas = new Map();
1385
+ const contract = service.contract;
1386
+ if (contract.scope !== "remote")
1387
+ return schemas;
1388
+ const members = contract.description.variants[variantOf(implementation) ?? ""] ?? {};
1389
+ for (const [name, description] of Object.entries(members)) {
1390
+ const source = implementation[name];
1391
+ if (description.kind !== "state" || !isSource(source))
1392
+ continue;
1393
+ schemas.set(source, [...(schemas.get(source) ?? []), description.schema]);
1394
+ }
1395
+ return schemas;
1396
+ }
1397
+ function validateImplementation(service, implementation) {
1398
+ const contract = service.contract;
1399
+ const fail = (message) => {
1400
+ throw invalid("implementation", `${contract.name}: ${message}`);
1401
+ };
1402
+ if (implementation === null || typeof implementation !== "object")
1403
+ fail("factory must return an object");
1404
+ let members;
1405
+ if (contract.scope === "remote") {
1406
+ const variant = implementation.variant;
1407
+ if (typeof variant !== "string" || contract.description.variants[variant] === undefined)
1408
+ fail(`unknown variant ${String(variant)}`);
1409
+ members = contract.description.variants[variant];
1410
+ for (const key of Object.keys(implementation))
1411
+ if (key !== "variant" && members[key] === undefined)
1412
+ fail(`${key} is not in variant ${String(variant)}`);
1413
+ }
1414
+ else {
1415
+ members = contract.description.variants[LOCAL_VARIANT] ?? {};
1416
+ }
1417
+ for (const [name, description] of Object.entries(members)) {
1418
+ const value = implementation[name];
1419
+ if (description.kind === "method" && typeof value !== "function")
1420
+ fail(`${name} must be a method`);
1421
+ if (description.kind === "state" && !isSource(value) && !isReadableLike(value)) {
1422
+ fail(`${name} must be a cell, a derivation or a held object's state member`);
1423
+ }
1424
+ if (description.kind === "opaque" && value === undefined)
1425
+ fail(`${name} is missing`);
1426
+ }
1427
+ }
1428
+ // ─── The factory's `self` ────────────────────────────────────────────────────
1429
+ class SelfImpl {
1430
+ #object;
1431
+ #previous;
1432
+ #decls = new Decls();
1433
+ #created = { refs: [], resources: [], derives: [], inits: [] };
1434
+ constructor(object, previous) {
1435
+ this.#object = object;
1436
+ this.#previous = previous;
1437
+ }
1438
+ run(implementation) {
1439
+ return { implementation: implementation, decls: this.#decls, created: this.#created };
1440
+ }
1441
+ /** Refuses a second declaration of `name` of one kind in this run (TypeScript cannot see string names). */
1442
+ #unique(map, kind, name) {
1443
+ if (typeof name !== "string" || name === "")
1444
+ throw new TypeError(`${kind} needs a name`);
1445
+ if (map.has(name))
1446
+ throw invalid("duplicate", `${this.#object.typeName}: duplicate ${kind} name ${name}`);
1447
+ }
1448
+ state(name, schema, initial, options = {}) {
1449
+ this.#unique(this.#decls.cells, "state", name);
1450
+ const version = options.version ?? 1;
1451
+ if (!Number.isInteger(version) || version < 1)
1452
+ throw new TypeError(`state ${name}: version must be a positive integer`);
1453
+ const host = this.#object.host;
1454
+ // A JSON value never holds a function: an object with a function `init` is the initializer, not a value.
1455
+ const init = isStateInit(initial) ? initial.init : undefined;
1456
+ let cell = this.#previous?.cells.get(name);
1457
+ if (cell === undefined) {
1458
+ cell = new Cell(name, schema, init === undefined ? { value: initial } : undefined, this.#object.guard, {
1459
+ dev: host.dev,
1460
+ report: (error) => host.report(error),
1461
+ });
1462
+ if (init !== undefined)
1463
+ this.#created.inits.push([cell, init]);
1464
+ }
1465
+ this.#decls.cells.set(name, cell);
1466
+ this.#decls.cellSchemas.set(name, schema);
1467
+ this.#decls.cellVersions.set(name, version);
1468
+ if (options.migrate !== undefined)
1469
+ this.#decls.cellMigrations.set(name, options.migrate);
1470
+ return cell;
1471
+ }
1472
+ get(type, input, options) {
1473
+ const host = this.#object.host;
1474
+ // A prepare rerun (recording mode) binds new references against the reload's pending contracts: a breaking provider
1475
+ // change and its updated consumer reload together.
1476
+ const candidate = this.#previous !== undefined;
1477
+ const address = host.addressOf(type, input, options?.principal ?? host.principal, candidate);
1478
+ const declared = this.#decls.refs.get(address);
1479
+ if (declared !== undefined) {
1480
+ if (declared.contract.fingerprint !== type.fingerprint) {
1481
+ throw invalid("duplicate", `${this.#object.typeName}: ${type.name} held twice with different contracts`);
1482
+ }
1483
+ return declared.proxy;
1484
+ }
1485
+ const existing = this.#previous?.refs.get(address);
1486
+ if (existing !== undefined &&
1487
+ existing.contract.fingerprint === type.fingerprint &&
1488
+ existing.status !== "gone" &&
1489
+ existing.status !== "released") {
1490
+ this.#decls.refs.set(address, existing);
1491
+ return existing.proxy;
1492
+ }
1493
+ const ref = host.acquire(type, input, options, "object", this.#object, candidate);
1494
+ this.#decls.refs.set(address, ref);
1495
+ this.#created.refs.push(ref);
1496
+ return ref.proxy;
1497
+ }
1498
+ resource(name, declaration) {
1499
+ this.#unique(this.#decls.resources, "resource", name);
1500
+ if (!Number.isInteger(declaration.version))
1501
+ throw new TypeError(`resource ${name}: version must be an integer`);
1502
+ const deps = (declaration.deps ?? []).map((dep) => {
1503
+ const resolved = refOf(dep) ?? resourceOf(dep);
1504
+ if (resolved === undefined) {
1505
+ throw new TypeError(`resource ${name}: deps must be references or resources of this object`);
1506
+ }
1507
+ return resolved;
1508
+ });
1509
+ const existing = this.#previous?.resources.get(name);
1510
+ const kept = existing !== undefined &&
1511
+ existing.spec.version === declaration.version &&
1512
+ existing.exclusive === (declaration.exclusive === true) &&
1513
+ existing.deps.length === deps.length &&
1514
+ existing.deps.every((dep, index) => dep === deps[index]);
1515
+ const resource = kept ? existing : new ResourceImpl(name, declaration, deps);
1516
+ if (kept)
1517
+ this.#decls.adopted.set(existing, declaration);
1518
+ else
1519
+ this.#created.resources.push(resource);
1520
+ this.#decls.resources.set(name, resource);
1521
+ return new ResourceFacade(resource, this.#object.host);
1522
+ }
1523
+ on(name, source, ...rest) {
1524
+ this.#unique(this.#decls.listeners, "listener", name);
1525
+ const [select, fn] = rest.length === 1 ? [undefined, rest[0]] : [rest[0], rest[1]];
1526
+ if (typeof fn !== "function")
1527
+ throw new TypeError(`listener ${name}: the handler must be a function`);
1528
+ if (select !== undefined && typeof select !== "function")
1529
+ throw new TypeError(`listener ${name}: select must be a function`);
1530
+ this.#decls.listeners.set(name, {
1531
+ source,
1532
+ select: select,
1533
+ fn: fn,
1534
+ });
1535
+ }
1536
+ task(fn) {
1537
+ if (typeof fn !== "function")
1538
+ throw new TypeError("a task must be a function");
1539
+ this.#decls.tasks.set(`#${this.#decls.tasks.size}`, fn);
1540
+ }
1541
+ derive(sources, fn) {
1542
+ if (typeof fn !== "function")
1543
+ throw new TypeError("a derivation needs a function");
1544
+ const host = this.#object.host;
1545
+ const key = `#${this.#decls.derives.size}`;
1546
+ const derived = new Derived(key, sources, fn, {
1547
+ dev: host.dev,
1548
+ report: (error) => host.report(error),
1549
+ });
1550
+ this.#created.derives.push(derived);
1551
+ this.#decls.derives.set(key, derived);
1552
+ return derived;
1553
+ }
1554
+ expose(resource) {
1555
+ const resolved = resourceOf(resource);
1556
+ if (resolved === undefined || ![...this.#decls.resources.values()].includes(resolved)) {
1557
+ throw new TypeError("expose takes a resource declared by this object");
1558
+ }
1559
+ return new ExposedImpl(resolved);
1560
+ }
1561
+ register(registry, entries) {
1562
+ if (!this.#object.hostLifetime)
1563
+ throw invalid("not_host_lifetime", `${this.#object.typeName}: only objects of lifetime "host" may register`);
1564
+ for (const [key, value] of Object.entries(entries)) {
1565
+ const id = `${registry.name}\u0000${key}`;
1566
+ this.#unique(this.#decls.registrations, "register", id);
1567
+ this.#decls.registrations.set(id, { registry: registry, key, value });
1568
+ }
1569
+ }
1570
+ }
1571
+ //# sourceMappingURL=object.js.map