@earendil-works/chord 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +14 -13
- package/dist/cell.d.ts +30 -9
- package/dist/cell.d.ts.map +1 -1
- package/dist/cell.js +52 -12
- package/dist/cell.js.map +1 -1
- package/dist/cluster.d.ts +4 -3
- package/dist/cluster.d.ts.map +1 -1
- package/dist/cluster.js +58 -42
- package/dist/cluster.js.map +1 -1
- package/dist/connection.d.ts +171 -58
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +676 -217
- package/dist/connection.js.map +1 -1
- package/dist/contract.d.ts +120 -45
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +104 -15
- package/dist/contract.js.map +1 -1
- package/dist/declare.d.ts +11 -48
- package/dist/declare.d.ts.map +1 -1
- package/dist/declare.js +1 -21
- package/dist/declare.js.map +1 -1
- package/dist/delta/index.d.ts +2 -1
- package/dist/delta/index.d.ts.map +1 -1
- package/dist/delta/index.js +4 -0
- package/dist/delta/index.js.map +1 -1
- package/dist/errors.d.ts +4 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -1
- package/dist/host.d.ts +214 -120
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +446 -241
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/link.d.ts +47 -12
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +200 -14
- package/dist/link.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +7 -2
- package/dist/loader.js.map +1 -1
- package/dist/manager.d.ts.map +1 -1
- package/dist/manager.js +35 -9
- package/dist/manager.js.map +1 -1
- package/dist/object.d.ts +438 -0
- package/dist/object.d.ts.map +1 -0
- package/dist/{record.js → object.js} +614 -335
- package/dist/object.js.map +1 -0
- package/dist/output.d.ts +85 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +350 -0
- package/dist/output.js.map +1 -0
- package/dist/registry.d.ts +3 -3
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/registry.js.map +1 -1
- package/dist/reload.d.ts +35 -27
- package/dist/reload.d.ts.map +1 -1
- package/dist/reload.js +256 -192
- package/dist/reload.js.map +1 -1
- package/dist/remote.d.ts +130 -35
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +493 -189
- package/dist/remote.js.map +1 -1
- package/dist/types.d.ts +39 -28
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/websocket.d.ts +4 -5
- package/dist/websocket.d.ts.map +1 -1
- package/dist/websocket.js +3 -2
- package/dist/websocket.js.map +1 -1
- package/dist/wire.d.ts +210 -129
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +108 -55
- package/dist/wire.js.map +1 -1
- package/docs/guide.md +267 -203
- package/docs/spec.md +141 -114
- package/docs/tutorial.md +257 -235
- package/package.json +6 -1
- package/dist/record.d.ts +0 -320
- package/dist/record.d.ts.map +0 -1
- package/dist/record.js.map +0 -1
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
import { Value } from "typebox/value";
|
|
2
|
-
import { Cell, Derived, isSource, notReady } from "./cell.js";
|
|
3
|
-
import { bind, LOCAL_VARIANT, } from "./contract.js";
|
|
4
|
-
import { declKind, } from "./declare.js";
|
|
2
|
+
import { Cell, Derived, HYDRATION, hydrationOf, isSource, notReady, unhydrated, } from "./cell.js";
|
|
3
|
+
import { bind, LOCAL_VARIANT, takesNoArgs, } from "./contract.js";
|
|
5
4
|
import { asChordError, ChordError, invalid, runAll } from "./errors.js";
|
|
6
5
|
import { enqueue } from "./publish.js";
|
|
7
|
-
import { assertValid } from "./schema.js";
|
|
6
|
+
import { assertValid, normalize } from "./schema.js";
|
|
8
7
|
const noop = () => { };
|
|
9
|
-
export const makeService = (contract, factory, options, facet) =>
|
|
10
|
-
contract,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+
}
|
|
16
37
|
/** Runtime form of `Exposed<T>`. */
|
|
17
38
|
class ExposedImpl {
|
|
18
39
|
resource;
|
|
@@ -22,6 +43,7 @@ class ExposedImpl {
|
|
|
22
43
|
}
|
|
23
44
|
export class ResourceImpl {
|
|
24
45
|
key;
|
|
46
|
+
/** The declaration of the factory run whose code is current: a kept resource adopts the new one at commit. */
|
|
25
47
|
spec;
|
|
26
48
|
deps;
|
|
27
49
|
/** `unavailable`: a close/reopen failed during a reload rollback; the value is gone for good. */
|
|
@@ -82,6 +104,13 @@ export class ResourceImpl {
|
|
|
82
104
|
this.state = "unavailable";
|
|
83
105
|
this.#value = undefined;
|
|
84
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
|
+
}
|
|
85
114
|
}
|
|
86
115
|
/**
|
|
87
116
|
* What `self.resources` returns: the resource, except that `.value` throws `not_ready` while any factory runs, even
|
|
@@ -110,68 +139,142 @@ const resourceOf = (value) => value instanceof ResourceImpl ? value : value !==
|
|
|
110
139
|
export class Decls {
|
|
111
140
|
cells = new Map();
|
|
112
141
|
cellSchemas = new Map();
|
|
113
|
-
|
|
142
|
+
cellVersions = new Map();
|
|
143
|
+
cellMigrations = new Map();
|
|
144
|
+
refs = new Map();
|
|
114
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();
|
|
115
148
|
listeners = new Map();
|
|
149
|
+
/** By position (`#0`, `#1`, …): tasks have no names. */
|
|
116
150
|
tasks = new Map();
|
|
151
|
+
/** By position: derivations have no names and are new at every run. */
|
|
117
152
|
derives = new Map();
|
|
118
|
-
deriveFns = new Map();
|
|
119
153
|
registrations = new Map();
|
|
120
154
|
}
|
|
155
|
+
const isStateInit = (value) => value !== null && typeof value === "object" && typeof value.init === "function";
|
|
121
156
|
// ─── Contexts ────────────────────────────────────────────────────────────────
|
|
122
157
|
export class CtxImpl {
|
|
123
158
|
host;
|
|
124
159
|
signal;
|
|
125
160
|
rootId;
|
|
126
|
-
|
|
161
|
+
codeVersion;
|
|
127
162
|
principal;
|
|
128
163
|
/** Code holders (tasks): every call they make starts a fresh root. */
|
|
129
164
|
holder;
|
|
130
|
-
#
|
|
165
|
+
#refs = [];
|
|
166
|
+
#release;
|
|
167
|
+
/** Aborted when the ctx closes: awaited `get`s give up. */
|
|
168
|
+
#closing = new AbortController();
|
|
131
169
|
#closed = false;
|
|
132
170
|
/** True after the call or root that received this ctx settled: it may no longer start work. */
|
|
133
171
|
get closed() {
|
|
134
172
|
return this.#closed;
|
|
135
173
|
}
|
|
136
|
-
/** Throws unless this ctx may start work on `host` now
|
|
174
|
+
/** Throws unless this ctx may start work on `host` (the called reference's host) now: not expired, aborted or foreign. */
|
|
137
175
|
check(host) {
|
|
138
176
|
if (this.host !== host)
|
|
139
177
|
throw invalid("foreign_ctx", "ctx belongs to another host");
|
|
140
178
|
if (this.#closed)
|
|
141
179
|
throw new ChordError("aborted", "ctx expired: its call already settled", { reason: "expired" });
|
|
142
|
-
if (this.signal.aborted)
|
|
143
|
-
|
|
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
|
+
}
|
|
144
187
|
}
|
|
188
|
+
/** `signals`: the ctx is aborted with the first of them; it stops following them when it closes. */
|
|
145
189
|
constructor(host, fields) {
|
|
146
190
|
this.host = host;
|
|
147
|
-
|
|
191
|
+
const linked = linkedSignal(fields.signals);
|
|
192
|
+
this.signal = linked.signal;
|
|
193
|
+
this.#release = linked.release;
|
|
148
194
|
this.rootId = fields.rootId;
|
|
149
|
-
this.
|
|
195
|
+
this.codeVersion = fields.codeVersion;
|
|
150
196
|
this.principal = fields.principal;
|
|
151
197
|
this.holder = fields.holder ?? false;
|
|
152
198
|
}
|
|
153
|
-
get(type, input, options) {
|
|
199
|
+
async get(type, input, options) {
|
|
154
200
|
this.check(this.host);
|
|
155
|
-
const
|
|
156
|
-
this.#
|
|
157
|
-
|
|
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;
|
|
158
205
|
}
|
|
159
206
|
/**
|
|
160
|
-
* A context for a call into `
|
|
161
|
-
* Code inside an object acts as the host principal.
|
|
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.
|
|
162
209
|
*/
|
|
163
|
-
child(
|
|
210
|
+
child(object) {
|
|
164
211
|
return new CtxImpl(this.host, {
|
|
165
|
-
|
|
212
|
+
signals: [this.signal, object.callAbort.signal],
|
|
166
213
|
rootId: this.rootId,
|
|
167
|
-
|
|
214
|
+
codeVersion: this.codeVersion,
|
|
168
215
|
principal: this.host.principal,
|
|
169
216
|
});
|
|
170
217
|
}
|
|
171
218
|
close() {
|
|
172
219
|
this.#closed = true;
|
|
173
|
-
|
|
174
|
-
|
|
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();
|
|
175
278
|
}
|
|
176
279
|
}
|
|
177
280
|
export async function raceSignal(promise, signal) {
|
|
@@ -189,11 +292,11 @@ export async function raceSignal(promise, signal) {
|
|
|
189
292
|
signal.removeEventListener("abort", onAbort);
|
|
190
293
|
}
|
|
191
294
|
}
|
|
192
|
-
export const
|
|
295
|
+
export const REF_BASE = new Set([
|
|
193
296
|
"contract",
|
|
194
297
|
"status",
|
|
195
298
|
"ready",
|
|
196
|
-
"
|
|
299
|
+
"variant",
|
|
197
300
|
"shape",
|
|
198
301
|
"error",
|
|
199
302
|
"onShape",
|
|
@@ -216,43 +319,83 @@ export function notifyStatus(listeners, status, error, report) {
|
|
|
216
319
|
}
|
|
217
320
|
});
|
|
218
321
|
}
|
|
219
|
-
/** Proxy →
|
|
220
|
-
export const
|
|
221
|
-
export const
|
|
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;
|
|
222
325
|
/**
|
|
223
|
-
* A
|
|
224
|
-
* name. `
|
|
225
|
-
*
|
|
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.
|
|
226
330
|
*/
|
|
227
331
|
class MemberReadable {
|
|
228
|
-
#
|
|
332
|
+
#ref;
|
|
229
333
|
#name;
|
|
230
334
|
#values = new Set();
|
|
231
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();
|
|
232
338
|
#source;
|
|
233
339
|
#upstream = noop;
|
|
234
340
|
/** The value listeners last saw; a first upstream snapshot equal to it is not re-announced. */
|
|
235
341
|
#seen;
|
|
236
|
-
constructor(
|
|
237
|
-
this.#
|
|
342
|
+
constructor(ref, name) {
|
|
343
|
+
this.#ref = ref;
|
|
238
344
|
this.#name = name;
|
|
239
345
|
}
|
|
240
346
|
get value() {
|
|
241
|
-
|
|
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
|
+
};
|
|
242
373
|
}
|
|
243
374
|
subscribe(listener) {
|
|
244
375
|
this.#values.add(listener);
|
|
245
|
-
|
|
246
|
-
|
|
376
|
+
const source = this.#readable();
|
|
377
|
+
if (source !== undefined) {
|
|
378
|
+
this.#seen = { value: source.value };
|
|
379
|
+
this.#safe(() => listener(source.value));
|
|
380
|
+
}
|
|
247
381
|
this.bind();
|
|
248
382
|
return () => this.#remove(this.#values, listener);
|
|
249
383
|
}
|
|
250
384
|
events(listener) {
|
|
251
385
|
this.#events.add(listener);
|
|
252
|
-
|
|
253
|
-
|
|
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);
|
|
254
394
|
this.bind();
|
|
255
|
-
return () =>
|
|
395
|
+
return () => {
|
|
396
|
+
this.#unseen.delete(listener);
|
|
397
|
+
this.#remove(this.#events, listener);
|
|
398
|
+
};
|
|
256
399
|
}
|
|
257
400
|
/** (Re)subscribes upstream to the current source; a changed source is announced as a reset. */
|
|
258
401
|
bind() {
|
|
@@ -268,17 +411,24 @@ class MemberReadable {
|
|
|
268
411
|
this.#upstream = source.events((event) => {
|
|
269
412
|
const isFirst = first;
|
|
270
413
|
first = false;
|
|
271
|
-
if (isFirst && event.value === this.#seen)
|
|
414
|
+
if (isFirst && this.#unseen.size === 0 && this.#seen !== undefined && event.value === this.#seen.value)
|
|
272
415
|
return;
|
|
273
|
-
this.#seen = event.value;
|
|
416
|
+
this.#seen = { value: event.value };
|
|
274
417
|
const translated = isFirst ? { type: "reset", value: event.value } : event;
|
|
275
418
|
const events = [...this.#events];
|
|
276
419
|
const values = [...this.#values];
|
|
420
|
+
const unseen = new Set(this.#unseen);
|
|
421
|
+
this.#unseen.clear();
|
|
277
422
|
// Queued: a rebind during a commit must not run observers before the commit completes.
|
|
278
423
|
enqueue(() => {
|
|
279
|
-
for (const listener of events)
|
|
280
|
-
if (this.#events.has(listener))
|
|
281
|
-
|
|
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
|
+
}
|
|
282
432
|
for (const listener of values)
|
|
283
433
|
if (this.#values.has(listener))
|
|
284
434
|
this.#safe(() => listener(event.value));
|
|
@@ -288,16 +438,22 @@ class MemberReadable {
|
|
|
288
438
|
stop() {
|
|
289
439
|
this.#values.clear();
|
|
290
440
|
this.#events.clear();
|
|
441
|
+
this.#unseen.clear();
|
|
291
442
|
this.bind();
|
|
292
443
|
}
|
|
293
444
|
#current() {
|
|
294
|
-
const view = this.#
|
|
295
|
-
if (view === undefined || !this.#
|
|
445
|
+
const view = this.#ref.currentView();
|
|
446
|
+
if (view === undefined || !this.#ref.inVariant(view, this.#name))
|
|
296
447
|
return undefined;
|
|
297
448
|
const value = view[this.#name];
|
|
298
449
|
// Cells and derivations, or any readable an `accepts` adapter maps them to.
|
|
299
450
|
return isSource(value) || isReadableLike(value) ? value : undefined;
|
|
300
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
|
+
}
|
|
301
457
|
#remove(set, listener) {
|
|
302
458
|
set.delete(listener);
|
|
303
459
|
this.bind();
|
|
@@ -307,13 +463,13 @@ class MemberReadable {
|
|
|
307
463
|
fn();
|
|
308
464
|
}
|
|
309
465
|
catch (error) {
|
|
310
|
-
this.#
|
|
466
|
+
this.#ref.object.host.report(error);
|
|
311
467
|
}
|
|
312
468
|
}
|
|
313
469
|
}
|
|
314
|
-
/** One
|
|
315
|
-
export class
|
|
316
|
-
|
|
470
|
+
/** One reference on an object. Exposed through a Proxy so members bind late to the current implementation. */
|
|
471
|
+
export class RefImpl {
|
|
472
|
+
object;
|
|
317
473
|
contract;
|
|
318
474
|
owner;
|
|
319
475
|
address;
|
|
@@ -322,28 +478,29 @@ export class HandleImpl {
|
|
|
322
478
|
status;
|
|
323
479
|
error;
|
|
324
480
|
shapeListeners = new Set();
|
|
325
|
-
/** Internal: notified when the
|
|
481
|
+
/** Internal: notified when the reference becomes gone (links forward it). */
|
|
326
482
|
goneListeners = new Set();
|
|
327
483
|
readables = new Map();
|
|
328
|
-
|
|
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();
|
|
329
486
|
views = new WeakMap();
|
|
330
487
|
callers = new Map();
|
|
331
|
-
constructor(
|
|
332
|
-
this.
|
|
488
|
+
constructor(object, contract, owner) {
|
|
489
|
+
this.object = object;
|
|
333
490
|
this.contract = contract;
|
|
334
491
|
this.owner = owner;
|
|
335
|
-
this.address =
|
|
336
|
-
this.status =
|
|
337
|
-
this.ready =
|
|
492
|
+
this.address = object.address;
|
|
493
|
+
this.status = object.status === "live" ? "live" : "loading";
|
|
494
|
+
this.ready = object.ready.then(() => {
|
|
338
495
|
if (this.status === "gone")
|
|
339
496
|
throw this.error;
|
|
340
497
|
if (this.status === "released")
|
|
341
|
-
throw new ChordError("released", `
|
|
498
|
+
throw new ChordError("released", `reference ${this.address} released`);
|
|
342
499
|
});
|
|
343
500
|
this.ready.catch(noop);
|
|
344
501
|
this.proxy = new Proxy(this, {
|
|
345
502
|
get: (target, property) => {
|
|
346
|
-
if (
|
|
503
|
+
if (REF_BASE.has(property)) {
|
|
347
504
|
const value = Reflect.get(target, property, target);
|
|
348
505
|
return typeof value === "function" ? value.bind(target) : value;
|
|
349
506
|
}
|
|
@@ -351,24 +508,23 @@ export class HandleImpl {
|
|
|
351
508
|
return undefined;
|
|
352
509
|
return target.member(property);
|
|
353
510
|
},
|
|
354
|
-
has: (target, property) =>
|
|
511
|
+
has: (target, property) => REF_BASE.has(property) || target.member(String(property)) !== undefined,
|
|
355
512
|
});
|
|
356
|
-
|
|
513
|
+
refsByProxy.set(this.proxy, this);
|
|
357
514
|
}
|
|
358
|
-
get
|
|
515
|
+
get variant() {
|
|
359
516
|
const view = this.currentView();
|
|
360
|
-
return view === undefined ? undefined :
|
|
517
|
+
return view === undefined ? undefined : variantOf(view);
|
|
361
518
|
}
|
|
362
519
|
get shape() {
|
|
363
|
-
const installed = this.status === "live" ? this.
|
|
520
|
+
const installed = this.status === "live" ? this.object.currentInstalled() : undefined;
|
|
364
521
|
const view = installed === undefined ? undefined : this.#safeView(installed);
|
|
365
522
|
if (installed === undefined || view === undefined)
|
|
366
523
|
return undefined;
|
|
367
524
|
return {
|
|
368
|
-
|
|
369
|
-
members: Object.keys(view).filter((key) => key !== "
|
|
525
|
+
variant: variantOf(view),
|
|
526
|
+
members: Object.keys(view).filter((key) => key !== "variant"),
|
|
370
527
|
contract: installed.service.contract.fingerprint,
|
|
371
|
-
epoch: installed.epoch,
|
|
372
528
|
};
|
|
373
529
|
}
|
|
374
530
|
onShape(listener) {
|
|
@@ -381,7 +537,7 @@ export class HandleImpl {
|
|
|
381
537
|
}
|
|
382
538
|
#statusListeners = new Set();
|
|
383
539
|
#statusChanged() {
|
|
384
|
-
notifyStatus(this.#statusListeners, this.status, this.error, (error) => this.
|
|
540
|
+
notifyStatus(this.#statusListeners, this.status, this.error, (error) => this.object.host.report(error));
|
|
385
541
|
}
|
|
386
542
|
release() {
|
|
387
543
|
if (this.status === "released" || this.status === "gone")
|
|
@@ -389,7 +545,7 @@ export class HandleImpl {
|
|
|
389
545
|
this.status = "released";
|
|
390
546
|
this.#stopReadables();
|
|
391
547
|
this.#statusChanged();
|
|
392
|
-
this.
|
|
548
|
+
this.object.release(this);
|
|
393
549
|
}
|
|
394
550
|
// ─── internal ───
|
|
395
551
|
live() {
|
|
@@ -422,15 +578,15 @@ export class HandleImpl {
|
|
|
422
578
|
listener(shape);
|
|
423
579
|
}
|
|
424
580
|
catch (error) {
|
|
425
|
-
this.
|
|
581
|
+
this.object.host.report(error);
|
|
426
582
|
}
|
|
427
583
|
}
|
|
428
584
|
});
|
|
429
585
|
}
|
|
430
586
|
terminalError() {
|
|
431
|
-
return this.status === "gone" ? this.error : new ChordError("released", `
|
|
587
|
+
return this.status === "gone" ? this.error : new ChordError("released", `reference ${this.address} released`);
|
|
432
588
|
}
|
|
433
|
-
/** The
|
|
589
|
+
/** The implementation as this consumer sees it (through an accepts adapter when bound to another version). */
|
|
434
590
|
viewOf(installed) {
|
|
435
591
|
let adapter = this.bindings.get(installed.service);
|
|
436
592
|
if (!this.bindings.has(installed.service)) {
|
|
@@ -438,18 +594,18 @@ export class HandleImpl {
|
|
|
438
594
|
this.bindings.set(installed.service, adapter);
|
|
439
595
|
}
|
|
440
596
|
if (adapter === undefined)
|
|
441
|
-
return installed.
|
|
442
|
-
let view = this.views.get(installed.
|
|
597
|
+
return installed.implementation;
|
|
598
|
+
let view = this.views.get(installed.implementation);
|
|
443
599
|
if (view === undefined) {
|
|
444
|
-
view = adapter(installed.
|
|
445
|
-
this.views.set(installed.
|
|
600
|
+
view = adapter(installed.implementation);
|
|
601
|
+
this.views.set(installed.implementation, view);
|
|
446
602
|
}
|
|
447
603
|
return view;
|
|
448
604
|
}
|
|
449
605
|
currentView() {
|
|
450
606
|
if (this.status !== "live")
|
|
451
607
|
return undefined;
|
|
452
|
-
const installed = this.
|
|
608
|
+
const installed = this.object.currentInstalled();
|
|
453
609
|
return installed === undefined ? undefined : this.#safeView(installed);
|
|
454
610
|
}
|
|
455
611
|
/** The view, or undefined when this consumer no longer binds (reported; calls reject with the bind error). */
|
|
@@ -458,14 +614,14 @@ export class HandleImpl {
|
|
|
458
614
|
return this.viewOf(installed);
|
|
459
615
|
}
|
|
460
616
|
catch (error) {
|
|
461
|
-
this.
|
|
617
|
+
this.object.host.report(error);
|
|
462
618
|
return undefined;
|
|
463
619
|
}
|
|
464
620
|
}
|
|
465
|
-
describe(
|
|
621
|
+
describe(variant, member) {
|
|
466
622
|
const variants = this.contract.description.variants;
|
|
467
|
-
if (
|
|
468
|
-
return variants[
|
|
623
|
+
if (variant !== undefined)
|
|
624
|
+
return variants[variant]?.[member];
|
|
469
625
|
for (const members of Object.values(variants))
|
|
470
626
|
if (members[member] !== undefined)
|
|
471
627
|
return members[member];
|
|
@@ -473,7 +629,7 @@ export class HandleImpl {
|
|
|
473
629
|
}
|
|
474
630
|
/** Remote consumers see only the members their contract lists for the current variant. */
|
|
475
631
|
inVariant(view, name) {
|
|
476
|
-
return this.contract.scope !== "remote" || this.describe(
|
|
632
|
+
return this.contract.scope !== "remote" || this.describe(variantOf(view), name) !== undefined;
|
|
477
633
|
}
|
|
478
634
|
member(name) {
|
|
479
635
|
const declared = this.describe(undefined, name);
|
|
@@ -497,8 +653,11 @@ export class HandleImpl {
|
|
|
497
653
|
caller(name) {
|
|
498
654
|
let caller = this.callers.get(name);
|
|
499
655
|
if (caller === undefined) {
|
|
500
|
-
|
|
501
|
-
|
|
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) {
|
|
502
661
|
throw new ChordError("not_ready", `${this.address}.${name} called inside a factory`);
|
|
503
662
|
}
|
|
504
663
|
if (ctx !== undefined && !(ctx instanceof CtxImpl)) {
|
|
@@ -507,16 +666,16 @@ export class HandleImpl {
|
|
|
507
666
|
if (ctx === undefined || ctx.holder) {
|
|
508
667
|
// A code holder (task) starts a fresh root per call, cancelled with the holder.
|
|
509
668
|
try {
|
|
510
|
-
ctx?.check(this.
|
|
669
|
+
ctx?.check(this.object.host);
|
|
511
670
|
}
|
|
512
671
|
catch (error) {
|
|
513
672
|
return Promise.reject(error);
|
|
514
673
|
}
|
|
515
|
-
return this.
|
|
674
|
+
return this.object.host.root((root) => this.object.call(this, name, args, root), {
|
|
516
675
|
signal: ctx?.signal,
|
|
517
676
|
});
|
|
518
677
|
}
|
|
519
|
-
return this.
|
|
678
|
+
return this.object.call(this, name, args, ctx);
|
|
520
679
|
};
|
|
521
680
|
this.callers.set(name, caller);
|
|
522
681
|
}
|
|
@@ -527,42 +686,45 @@ export class HandleImpl {
|
|
|
527
686
|
readable.stop();
|
|
528
687
|
}
|
|
529
688
|
}
|
|
530
|
-
export const
|
|
689
|
+
export const variantOf = (implementation) => typeof implementation.variant === "string" ? implementation.variant : undefined;
|
|
531
690
|
/**
|
|
532
691
|
* Two `select` functions are the same if they are identical or have the same source text: an inline arrow is a new
|
|
533
|
-
* object on every factory run, but its code is what matters. (A select whose
|
|
692
|
+
* object on every factory run, but its code is what matters. (A select whose implementation depends on captured values
|
|
534
693
|
* that change between runs is not detected; such a listener keeps its old subscription until its source changes.)
|
|
535
694
|
*/
|
|
536
695
|
const sameSelect = (a, b) => a === b || (a !== undefined && b !== undefined && String(a) === String(b));
|
|
537
|
-
export class
|
|
696
|
+
export class ObjectImpl {
|
|
538
697
|
host;
|
|
539
698
|
address;
|
|
540
699
|
typeName;
|
|
541
700
|
input;
|
|
542
701
|
principal;
|
|
543
|
-
|
|
702
|
+
/** The one object of a `lifetime: "host"` type: only such objects may register. */
|
|
703
|
+
hostLifetime;
|
|
544
704
|
status = "activating";
|
|
545
705
|
ready = Promise.resolve();
|
|
546
706
|
service;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
minEpoch = 0;
|
|
707
|
+
/** Calls carrying an older code version are rejected upgrade_conflict (state bump or exclusive resource replaced). */
|
|
708
|
+
minCodeVersion = 0;
|
|
550
709
|
decls = new Decls();
|
|
551
710
|
/** Declarations of a factory run that has not been installed yet (activation or reload candidate). */
|
|
552
711
|
pending = new Set();
|
|
553
|
-
|
|
554
|
-
|
|
712
|
+
implementations = new Map();
|
|
713
|
+
refs = new Set();
|
|
555
714
|
leases = new Map();
|
|
556
715
|
openOrder = [];
|
|
557
716
|
callAbort = new AbortController();
|
|
558
717
|
activationAbort = new AbortController();
|
|
559
718
|
gate;
|
|
560
|
-
/** Served only by a running reload's pending
|
|
719
|
+
/** Served only by a running reload's pending code version: listeners, tasks and registrations start at its commit. */
|
|
561
720
|
deferredRuntime = false;
|
|
562
721
|
#subscriptions = new Map();
|
|
563
722
|
#invocations = new Set();
|
|
564
723
|
#tasks = new Map();
|
|
565
|
-
|
|
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;
|
|
566
728
|
#linger;
|
|
567
729
|
#disposal;
|
|
568
730
|
constructor(host, identity) {
|
|
@@ -571,12 +733,12 @@ export class RecordImpl {
|
|
|
571
733
|
this.typeName = identity.typeName;
|
|
572
734
|
this.input = identity.input;
|
|
573
735
|
this.principal = identity.principal;
|
|
574
|
-
this.
|
|
736
|
+
this.hostLifetime = identity.hostLifetime;
|
|
575
737
|
}
|
|
576
738
|
start(after) {
|
|
577
739
|
this.ready = this.#activate(after).then(() => {
|
|
578
|
-
for (const
|
|
579
|
-
|
|
740
|
+
for (const ref of this.refs)
|
|
741
|
+
ref.live();
|
|
580
742
|
this.#maybeIdle();
|
|
581
743
|
}, (cause) => {
|
|
582
744
|
if (this.status === "activating")
|
|
@@ -584,15 +746,15 @@ export class RecordImpl {
|
|
|
584
746
|
this.host.forget(this);
|
|
585
747
|
// A ChordError (forbidden, a dependency's error, ...) keeps its code; anything else is `unavailable`.
|
|
586
748
|
const error = asChordError(cause, `activating ${this.address}`);
|
|
587
|
-
for (const
|
|
588
|
-
|
|
749
|
+
for (const ref of this.refs)
|
|
750
|
+
ref.gone(error);
|
|
589
751
|
throw error;
|
|
590
752
|
});
|
|
591
753
|
this.ready.catch(noop);
|
|
592
754
|
}
|
|
593
755
|
/**
|
|
594
756
|
* Activation as one transaction: factory, opens, then a synchronous finalization (registry check and publication,
|
|
595
|
-
*
|
|
757
|
+
* implementations, runtime). Every failure, including one during finalization or a release while listeners start,
|
|
596
758
|
* unwinds everything this activation did.
|
|
597
759
|
*/
|
|
598
760
|
async #activate(after) {
|
|
@@ -605,35 +767,46 @@ export class RecordImpl {
|
|
|
605
767
|
if (service === undefined)
|
|
606
768
|
throw new ChordError("unavailable", `${this.typeName} is not served`, { reason: "unknown_type" });
|
|
607
769
|
this.service = service;
|
|
608
|
-
this.stateVersion = service.stateVersion;
|
|
609
770
|
const run = this.run(service, undefined);
|
|
610
771
|
try {
|
|
611
|
-
await this.open(run.created.resources, run.created.
|
|
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);
|
|
612
779
|
if (signal.aborted)
|
|
613
780
|
throw abandoned();
|
|
614
781
|
this.host.checkRegistrations(this, run.decls);
|
|
615
782
|
this.decls = run.decls;
|
|
616
|
-
for (const [retained, services] of this.host.
|
|
783
|
+
for (const [retained, services] of this.host.retainedCodeVersions()) {
|
|
617
784
|
if (services.get(this.typeName) === service) {
|
|
618
|
-
this.
|
|
785
|
+
this.implementations.set(retained, {
|
|
786
|
+
codeVersion: retained,
|
|
787
|
+
service,
|
|
788
|
+
implementation: run.implementation,
|
|
789
|
+
decls: run.decls,
|
|
790
|
+
});
|
|
619
791
|
}
|
|
620
792
|
}
|
|
621
|
-
const pending = this.host.
|
|
622
|
-
if (!this.
|
|
793
|
+
const pending = this.host.pendingCodeVersion();
|
|
794
|
+
if (!this.implementations.has(this.host.currentCodeVersion()) &&
|
|
795
|
+
(pending === undefined || !this.implementations.has(pending))) {
|
|
623
796
|
throw new ChordError("superseded", `${this.address}: code changed during activation`);
|
|
624
797
|
}
|
|
625
798
|
for (const derived of run.created.derives)
|
|
626
799
|
derived.attach();
|
|
627
800
|
this.refreshSchemas();
|
|
628
|
-
// Served only by a running reload's pending
|
|
629
|
-
// before that reload commits.
|
|
630
|
-
this.deferredRuntime = !this.
|
|
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());
|
|
631
804
|
if (!this.deferredRuntime) {
|
|
632
805
|
if (run.decls.registrations.size > 0)
|
|
633
806
|
this.host.registriesChanged(this);
|
|
634
807
|
this.startRuntime();
|
|
635
808
|
}
|
|
636
|
-
// A listener starting up may have released the last
|
|
809
|
+
// A listener starting up may have released the last reference synchronously: no resurrection.
|
|
637
810
|
if (signal.aborted)
|
|
638
811
|
throw abandoned();
|
|
639
812
|
this.status = "live";
|
|
@@ -644,11 +817,11 @@ export class RecordImpl {
|
|
|
644
817
|
this.stopListeners();
|
|
645
818
|
this.stopTasks();
|
|
646
819
|
const registered = this.decls.registrations.size > 0 && !this.deferredRuntime;
|
|
647
|
-
this.
|
|
820
|
+
this.implementations.clear();
|
|
648
821
|
this.decls = new Decls();
|
|
649
822
|
if (registered)
|
|
650
823
|
this.host.registriesChanged();
|
|
651
|
-
await Promise.allSettled([...this.#invocations, ...this.#stopped
|
|
824
|
+
await Promise.allSettled([...this.#invocations, ...this.#stopped]);
|
|
652
825
|
await this.unwind(run).catch((cleanup) => this.host.report(cleanup));
|
|
653
826
|
throw error;
|
|
654
827
|
}
|
|
@@ -668,9 +841,9 @@ export class RecordImpl {
|
|
|
668
841
|
};
|
|
669
842
|
this.pending.add(self.run(undefined).decls);
|
|
670
843
|
this.host.inFactory++;
|
|
671
|
-
let
|
|
844
|
+
let implementation;
|
|
672
845
|
try {
|
|
673
|
-
|
|
846
|
+
implementation = service.factory(this.input, this.principal, self);
|
|
674
847
|
}
|
|
675
848
|
catch (error) {
|
|
676
849
|
this.host.inFactory--;
|
|
@@ -678,11 +851,11 @@ export class RecordImpl {
|
|
|
678
851
|
throw error;
|
|
679
852
|
}
|
|
680
853
|
this.host.inFactory--;
|
|
681
|
-
const run = self.run(
|
|
854
|
+
const run = self.run(implementation);
|
|
682
855
|
try {
|
|
683
|
-
if (
|
|
684
|
-
throw invalid("
|
|
685
|
-
|
|
856
|
+
if (implementation instanceof Promise)
|
|
857
|
+
throw invalid("implementation", `${this.typeName}: factories must be synchronous`);
|
|
858
|
+
validateImplementation(service, run.implementation);
|
|
686
859
|
}
|
|
687
860
|
catch (error) {
|
|
688
861
|
discard(run);
|
|
@@ -690,8 +863,8 @@ export class RecordImpl {
|
|
|
690
863
|
}
|
|
691
864
|
return run;
|
|
692
865
|
}
|
|
693
|
-
/** Opens resources (dependencies first) and waits for
|
|
694
|
-
async open(resources,
|
|
866
|
+
/** Opens resources (dependencies first) and waits for references; settles all before failing. */
|
|
867
|
+
async open(resources, refs, signal) {
|
|
695
868
|
const opening = new Map();
|
|
696
869
|
const openOne = (resource) => {
|
|
697
870
|
let promise = opening.get(resource);
|
|
@@ -704,29 +877,85 @@ export class RecordImpl {
|
|
|
704
877
|
}
|
|
705
878
|
return promise;
|
|
706
879
|
};
|
|
707
|
-
const
|
|
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]);
|
|
708
882
|
const failure = results.find((result) => result.status === "rejected");
|
|
709
883
|
if (failure !== undefined)
|
|
710
884
|
throw failure.reason;
|
|
711
885
|
}
|
|
712
|
-
/**
|
|
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. */
|
|
713
942
|
async unwind(run) {
|
|
714
943
|
for (const derived of run.created.derives)
|
|
715
944
|
derived.detach();
|
|
716
945
|
const resources = [...run.created.resources].sort((a, b) => this.openOrder.indexOf(b) - this.openOrder.indexOf(a));
|
|
717
946
|
await runAll([
|
|
718
947
|
...resources.map((resource) => () => resource.close(this.openOrder)),
|
|
719
|
-
...run.created.
|
|
948
|
+
...run.created.refs.map((ref) => () => ref.release()),
|
|
720
949
|
], `${this.address}: cleanup failed`);
|
|
721
950
|
}
|
|
722
951
|
currentInstalled() {
|
|
723
|
-
return this.
|
|
952
|
+
return this.implementations.get(this.host.currentCodeVersion()) ?? [...this.implementations.values()].at(-1);
|
|
724
953
|
}
|
|
725
954
|
/** Every state source exposed as a remote state member gets that member's schema (checked per write in dev mode). */
|
|
726
955
|
refreshSchemas() {
|
|
727
956
|
const schemas = new Map();
|
|
728
|
-
for (const installed of this.
|
|
729
|
-
for (const [source, list] of memberSchemas(installed.service, installed.
|
|
957
|
+
for (const installed of this.implementations.values()) {
|
|
958
|
+
for (const [source, list] of memberSchemas(installed.service, installed.implementation)) {
|
|
730
959
|
schemas.set(source, [...(schemas.get(source) ?? []), ...list]);
|
|
731
960
|
}
|
|
732
961
|
}
|
|
@@ -737,7 +966,7 @@ export class RecordImpl {
|
|
|
737
966
|
}
|
|
738
967
|
allDecls() {
|
|
739
968
|
const result = new Set([this.decls, ...this.pending]);
|
|
740
|
-
for (const installed of this.
|
|
969
|
+
for (const installed of this.implementations.values())
|
|
741
970
|
result.add(installed.decls);
|
|
742
971
|
return result;
|
|
743
972
|
}
|
|
@@ -752,7 +981,7 @@ export class RecordImpl {
|
|
|
752
981
|
const value = resource === undefined ? decl.source : resource.value;
|
|
753
982
|
const source = (decl.select === undefined ? value : decl.select(value));
|
|
754
983
|
if (typeof source?.subscribe !== "function")
|
|
755
|
-
throw invalid("
|
|
984
|
+
throw invalid("implementation", `listener ${key}: source has no subscribe()`);
|
|
756
985
|
return source;
|
|
757
986
|
}
|
|
758
987
|
/**
|
|
@@ -780,6 +1009,15 @@ export class RecordImpl {
|
|
|
780
1009
|
buffer: [],
|
|
781
1010
|
};
|
|
782
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
|
+
}
|
|
783
1021
|
entry.unsubscribe = source.subscribe((event) => {
|
|
784
1022
|
if (entry.buffer !== undefined)
|
|
785
1023
|
entry.buffer.push(event);
|
|
@@ -803,6 +1041,7 @@ export class RecordImpl {
|
|
|
803
1041
|
}
|
|
804
1042
|
for (const [key, entry] of fresh) {
|
|
805
1043
|
this.#subscriptions.set(key, entry);
|
|
1044
|
+
entry.subscribe?.();
|
|
806
1045
|
const buffered = entry.buffer ?? [];
|
|
807
1046
|
entry.buffer = undefined;
|
|
808
1047
|
for (const event of buffered)
|
|
@@ -819,24 +1058,51 @@ export class RecordImpl {
|
|
|
819
1058
|
this.stageListeners(this.decls).activate();
|
|
820
1059
|
this.startTasks();
|
|
821
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
|
+
*/
|
|
822
1066
|
startTasks() {
|
|
823
|
-
|
|
824
|
-
|
|
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
|
+
});
|
|
825
1091
|
}
|
|
826
1092
|
stopListeners() {
|
|
827
1093
|
for (const subscription of this.#subscriptions.values())
|
|
828
1094
|
subscription.unsubscribe();
|
|
829
1095
|
this.#subscriptions.clear();
|
|
830
1096
|
}
|
|
831
|
-
startTask(key,
|
|
1097
|
+
#startTask(key, codeVersion) {
|
|
832
1098
|
const fn = this.decls.tasks.get(key);
|
|
833
1099
|
if (fn === undefined)
|
|
834
1100
|
return;
|
|
835
1101
|
const controller = new AbortController();
|
|
836
1102
|
const ctx = new CtxImpl(this.host, {
|
|
837
|
-
|
|
1103
|
+
signals: [controller.signal, this.callAbort.signal],
|
|
838
1104
|
rootId: `task:${this.address}:${key}`,
|
|
839
|
-
|
|
1105
|
+
codeVersion,
|
|
840
1106
|
principal: this.host.principal,
|
|
841
1107
|
holder: true,
|
|
842
1108
|
});
|
|
@@ -856,17 +1122,23 @@ export class RecordImpl {
|
|
|
856
1122
|
})();
|
|
857
1123
|
this.#tasks.set(key, { controller, done });
|
|
858
1124
|
}
|
|
859
|
-
/**
|
|
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
|
+
*/
|
|
860
1129
|
stopTasks() {
|
|
1130
|
+
this.#taskRuns++;
|
|
861
1131
|
for (const task of this.#tasks.values()) {
|
|
862
1132
|
task.controller.abort(new ChordError("aborted", "task stopped"));
|
|
863
|
-
|
|
1133
|
+
const done = task.done;
|
|
1134
|
+
this.#stopped.add(done);
|
|
1135
|
+
void done.finally(() => this.#stopped.delete(done));
|
|
864
1136
|
}
|
|
865
1137
|
this.#tasks.clear();
|
|
866
1138
|
}
|
|
867
1139
|
/**
|
|
868
|
-
* Delivers one listener event, in publication order. The invocation is a root that
|
|
869
|
-
* (a reload gate waits for it like for any other root that touched the
|
|
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
|
|
870
1142
|
* waits for it) and is cancelled by forced disposal.
|
|
871
1143
|
*/
|
|
872
1144
|
#deliver(key, event) {
|
|
@@ -893,7 +1165,7 @@ export class RecordImpl {
|
|
|
893
1165
|
invocation.catch((error) => this.host.report(error));
|
|
894
1166
|
});
|
|
895
1167
|
}
|
|
896
|
-
/** Opens the
|
|
1168
|
+
/** Opens the object's gate and replays queued listener events through the current listeners. */
|
|
897
1169
|
openGate() {
|
|
898
1170
|
const gate = this.gate;
|
|
899
1171
|
this.gate = undefined;
|
|
@@ -906,20 +1178,20 @@ export class RecordImpl {
|
|
|
906
1178
|
if (this.host.failed !== undefined) {
|
|
907
1179
|
throw new ChordError("unavailable", `${this.host.name} failed and admits no work`, { reason: "host_failed" });
|
|
908
1180
|
}
|
|
909
|
-
if (!this.host.
|
|
910
|
-
throw new ChordError("superseded", `
|
|
911
|
-
if (ctx.
|
|
912
|
-
throw new ChordError("upgrade_conflict", `${this.address} was upgraded in
|
|
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}`);
|
|
913
1185
|
}
|
|
914
1186
|
if (this.gate !== undefined && !this.gate.grandfathered.has(ctx.rootId)) {
|
|
915
1187
|
throw new ChordError("upgrade_conflict", `${this.address} is being upgraded`);
|
|
916
1188
|
}
|
|
917
1189
|
}
|
|
918
|
-
/** Records that the root of `ctx` touched this
|
|
1190
|
+
/** Records that the root of `ctx` touched this object (a reload gate grandfathers it). */
|
|
919
1191
|
lease(ctx) {
|
|
920
1192
|
if (this.leases.has(ctx.rootId))
|
|
921
1193
|
return;
|
|
922
|
-
this.leases.set(ctx.rootId, ctx.
|
|
1194
|
+
this.leases.set(ctx.rootId, ctx.codeVersion);
|
|
923
1195
|
this.host.touch(ctx.rootId, this);
|
|
924
1196
|
}
|
|
925
1197
|
guard = (ctx) => {
|
|
@@ -935,52 +1207,65 @@ export class RecordImpl {
|
|
|
935
1207
|
return;
|
|
936
1208
|
this.admit(ctx);
|
|
937
1209
|
};
|
|
938
|
-
async call(
|
|
1210
|
+
async call(ref, name, args, ctx) {
|
|
939
1211
|
ctx.check(this.host);
|
|
940
|
-
if (
|
|
941
|
-
throw
|
|
942
|
-
if (
|
|
943
|
-
await raceSignal(
|
|
944
|
-
if (
|
|
945
|
-
throw
|
|
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"
|
|
946
1218
|
? new ChordError("gone", `${this.address} is ${this.status}`)
|
|
947
|
-
:
|
|
1219
|
+
: ref.terminalError();
|
|
948
1220
|
}
|
|
949
1221
|
this.admit(ctx);
|
|
950
|
-
const installed = this.
|
|
1222
|
+
const installed = this.implementations.get(ctx.codeVersion);
|
|
951
1223
|
if (installed === undefined)
|
|
952
|
-
throw new ChordError("superseded", `${this.address} has no
|
|
953
|
-
const view =
|
|
954
|
-
const
|
|
955
|
-
const wanted =
|
|
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);
|
|
956
1228
|
const fn = view[name];
|
|
957
|
-
if (typeof fn !== "function" || (
|
|
958
|
-
throw new ChordError("not_in_variant", `${this.address}: ${name} is not a method of variant ${
|
|
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);
|
|
959
1250
|
}
|
|
960
|
-
const direct = view === installed.behavior;
|
|
961
|
-
const schema = direct ? installed.service.contract.description.variants[kind ?? LOCAL_VARIANT]?.[name] : wanted;
|
|
962
|
-
const validated = handle.contract.scope === "remote" && schema?.kind === "method" ? schema : undefined;
|
|
963
|
-
// The additive rule assumes a consumer sends only what its own schema knows, and a provider sees only what its
|
|
964
|
-
// schema knows: strip with both before validating.
|
|
965
|
-
const own = handle.contract.scope === "remote" && wanted?.kind === "method" ? wanted.args : undefined;
|
|
966
|
-
const sent = own === undefined ? args : Value.Clean(own, Value.Clone(args));
|
|
967
|
-
const input = validated ? Value.Clean(validated.args, Value.Clone(sent)) : sent;
|
|
968
|
-
if (validated)
|
|
969
|
-
assertValid("args", `${this.address}.${name} args`, validated.args, input);
|
|
970
1251
|
this.lease(ctx);
|
|
971
1252
|
const child = ctx.child(this);
|
|
972
1253
|
let result;
|
|
973
1254
|
try {
|
|
974
|
-
result = await this.#invoke(() =>
|
|
1255
|
+
result = await this.#invoke(() => takesArgs
|
|
1256
|
+
? fn(input, child)
|
|
1257
|
+
: fn(child));
|
|
975
1258
|
}
|
|
976
1259
|
finally {
|
|
977
1260
|
child.close();
|
|
978
1261
|
}
|
|
979
|
-
if (
|
|
1262
|
+
if (!returns)
|
|
1263
|
+
return undefined;
|
|
1264
|
+
if (validated?.result)
|
|
980
1265
|
assertValid("result", `${this.address}.${name} result`, validated.result, result);
|
|
981
1266
|
return result;
|
|
982
1267
|
}
|
|
983
|
-
/** Runs an invocation (method or listener) that keeps the
|
|
1268
|
+
/** Runs an invocation (method or listener) that keeps the object alive until it settles. */
|
|
984
1269
|
#invoke(fn) {
|
|
985
1270
|
const invocation = (async () => fn())();
|
|
986
1271
|
this.#invocations.add(invocation);
|
|
@@ -999,19 +1284,19 @@ export class RecordImpl {
|
|
|
999
1284
|
gate.drained();
|
|
1000
1285
|
}
|
|
1001
1286
|
// ─── lifetime ───
|
|
1002
|
-
|
|
1003
|
-
this.
|
|
1287
|
+
addRef(ref) {
|
|
1288
|
+
this.refs.add(ref);
|
|
1004
1289
|
if (this.#linger !== undefined)
|
|
1005
1290
|
clearTimeout(this.#linger);
|
|
1006
1291
|
this.#linger = undefined;
|
|
1007
1292
|
}
|
|
1008
|
-
release(
|
|
1009
|
-
if (this.
|
|
1293
|
+
release(ref) {
|
|
1294
|
+
if (this.refs.delete(ref))
|
|
1010
1295
|
this.#maybeIdle();
|
|
1011
1296
|
}
|
|
1012
|
-
/** No
|
|
1297
|
+
/** No references and no invocations: dispose (after `linger` for a live object). */
|
|
1013
1298
|
#maybeIdle() {
|
|
1014
|
-
if (this.
|
|
1299
|
+
if (this.refs.size > 0 || this.#invocations.size > 0 || this.#linger !== undefined)
|
|
1015
1300
|
return;
|
|
1016
1301
|
if (this.status === "activating") {
|
|
1017
1302
|
this.#background(this.dispose(false));
|
|
@@ -1026,7 +1311,7 @@ export class RecordImpl {
|
|
|
1026
1311
|
}
|
|
1027
1312
|
this.#linger = setTimeout(() => {
|
|
1028
1313
|
this.#linger = undefined;
|
|
1029
|
-
if (this.
|
|
1314
|
+
if (this.refs.size === 0 && this.#invocations.size === 0 && this.status === "live") {
|
|
1030
1315
|
this.#background(this.dispose(false));
|
|
1031
1316
|
}
|
|
1032
1317
|
}, linger);
|
|
@@ -1036,7 +1321,7 @@ export class RecordImpl {
|
|
|
1036
1321
|
disposal.catch((error) => this.host.report(error));
|
|
1037
1322
|
}
|
|
1038
1323
|
/**
|
|
1039
|
-
* Disposes the
|
|
1324
|
+
* Disposes the object. Forced: references become gone at once (new calls are rejected) and running invocations are
|
|
1040
1325
|
* cancelled. Either way, resources close only after every invocation settled.
|
|
1041
1326
|
*/
|
|
1042
1327
|
dispose(forced, reason) {
|
|
@@ -1052,40 +1337,41 @@ export class RecordImpl {
|
|
|
1052
1337
|
this.#linger = undefined;
|
|
1053
1338
|
const error = reason ?? new ChordError("gone", `${this.address} was disposed`);
|
|
1054
1339
|
if (forced) {
|
|
1055
|
-
for (const
|
|
1056
|
-
|
|
1340
|
+
for (const ref of this.refs)
|
|
1341
|
+
ref.gone(error);
|
|
1057
1342
|
this.callAbort.abort(error);
|
|
1058
1343
|
}
|
|
1059
1344
|
this.activationAbort.abort(error);
|
|
1060
1345
|
if (wasActivating)
|
|
1061
1346
|
await this.ready.catch(noop);
|
|
1062
|
-
for (const
|
|
1063
|
-
|
|
1347
|
+
for (const ref of this.refs)
|
|
1348
|
+
ref.gone(error);
|
|
1064
1349
|
const decls = this.allDecls();
|
|
1065
1350
|
this.stopTasks();
|
|
1066
1351
|
this.stopListeners();
|
|
1067
1352
|
await Promise.allSettled([...this.#invocations]);
|
|
1068
1353
|
const registered = [...decls].some((set) => set.registrations.size > 0) && !this.deferredRuntime;
|
|
1069
|
-
this.
|
|
1354
|
+
this.implementations.clear();
|
|
1070
1355
|
this.decls = new Decls();
|
|
1071
1356
|
if (registered)
|
|
1072
1357
|
this.host.registriesChanged();
|
|
1073
|
-
const
|
|
1358
|
+
const refs = new Set();
|
|
1074
1359
|
for (const set of decls) {
|
|
1075
1360
|
for (const derived of set.derives.values())
|
|
1076
1361
|
derived.detach();
|
|
1077
|
-
for (const
|
|
1078
|
-
|
|
1362
|
+
for (const ref of set.refs.values())
|
|
1363
|
+
refs.add(ref);
|
|
1079
1364
|
}
|
|
1080
1365
|
try {
|
|
1081
1366
|
await runAll([
|
|
1082
|
-
() => Promise.allSettled(this.#stopped
|
|
1367
|
+
() => Promise.allSettled([...this.#stopped]),
|
|
1083
1368
|
...[...this.openOrder].reverse().map((resource) => () => resource.close(this.openOrder)),
|
|
1084
|
-
...[...
|
|
1369
|
+
...[...refs].map((ref) => () => ref.release()),
|
|
1085
1370
|
], `${this.address}: disposal failed`);
|
|
1086
1371
|
}
|
|
1087
1372
|
finally {
|
|
1088
1373
|
this.status = "disposed";
|
|
1374
|
+
this.host.disposed(this);
|
|
1089
1375
|
}
|
|
1090
1376
|
}
|
|
1091
1377
|
}
|
|
@@ -1093,43 +1379,43 @@ const isReadableLike = (value) => value !== null &&
|
|
|
1093
1379
|
typeof value === "object" &&
|
|
1094
1380
|
typeof value.events === "function" &&
|
|
1095
1381
|
typeof value.subscribe === "function";
|
|
1096
|
-
/** The remote state-member schemas
|
|
1097
|
-
export function memberSchemas(service,
|
|
1382
|
+
/** The remote state-member schemas an implementation exposes, by source. */
|
|
1383
|
+
export function memberSchemas(service, implementation) {
|
|
1098
1384
|
const schemas = new Map();
|
|
1099
1385
|
const contract = service.contract;
|
|
1100
1386
|
if (contract.scope !== "remote")
|
|
1101
1387
|
return schemas;
|
|
1102
|
-
const members = contract.description.variants[
|
|
1388
|
+
const members = contract.description.variants[variantOf(implementation) ?? ""] ?? {};
|
|
1103
1389
|
for (const [name, description] of Object.entries(members)) {
|
|
1104
|
-
const source =
|
|
1390
|
+
const source = implementation[name];
|
|
1105
1391
|
if (description.kind !== "state" || !isSource(source))
|
|
1106
1392
|
continue;
|
|
1107
1393
|
schemas.set(source, [...(schemas.get(source) ?? []), description.schema]);
|
|
1108
1394
|
}
|
|
1109
1395
|
return schemas;
|
|
1110
1396
|
}
|
|
1111
|
-
function
|
|
1397
|
+
function validateImplementation(service, implementation) {
|
|
1112
1398
|
const contract = service.contract;
|
|
1113
1399
|
const fail = (message) => {
|
|
1114
|
-
throw invalid("
|
|
1400
|
+
throw invalid("implementation", `${contract.name}: ${message}`);
|
|
1115
1401
|
};
|
|
1116
|
-
if (
|
|
1402
|
+
if (implementation === null || typeof implementation !== "object")
|
|
1117
1403
|
fail("factory must return an object");
|
|
1118
1404
|
let members;
|
|
1119
1405
|
if (contract.scope === "remote") {
|
|
1120
|
-
const
|
|
1121
|
-
if (typeof
|
|
1122
|
-
fail(`unknown variant ${String(
|
|
1123
|
-
members = contract.description.variants[
|
|
1124
|
-
for (const key of Object.keys(
|
|
1125
|
-
if (key !== "
|
|
1126
|
-
fail(`${key} is not in variant ${String(
|
|
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)}`);
|
|
1127
1413
|
}
|
|
1128
1414
|
else {
|
|
1129
1415
|
members = contract.description.variants[LOCAL_VARIANT] ?? {};
|
|
1130
1416
|
}
|
|
1131
1417
|
for (const [name, description] of Object.entries(members)) {
|
|
1132
|
-
const value =
|
|
1418
|
+
const value = implementation[name];
|
|
1133
1419
|
if (description.kind === "method" && typeof value !== "function")
|
|
1134
1420
|
fail(`${name} must be a method`);
|
|
1135
1421
|
if (description.kind === "state" && !isSource(value) && !isReadableLike(value)) {
|
|
@@ -1141,152 +1427,145 @@ function validateBehavior(service, behavior) {
|
|
|
1141
1427
|
}
|
|
1142
1428
|
// ─── The factory's `self` ────────────────────────────────────────────────────
|
|
1143
1429
|
class SelfImpl {
|
|
1144
|
-
#
|
|
1430
|
+
#object;
|
|
1145
1431
|
#previous;
|
|
1146
1432
|
#decls = new Decls();
|
|
1147
|
-
#created = {
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
derives: [],
|
|
1151
|
-
};
|
|
1152
|
-
constructor(record, previous) {
|
|
1153
|
-
this.#record = record;
|
|
1433
|
+
#created = { refs: [], resources: [], derives: [], inits: [] };
|
|
1434
|
+
constructor(object, previous) {
|
|
1435
|
+
this.#object = object;
|
|
1154
1436
|
this.#previous = previous;
|
|
1155
1437
|
}
|
|
1156
|
-
run(
|
|
1157
|
-
return {
|
|
1158
|
-
}
|
|
1159
|
-
/**
|
|
1160
|
-
|
|
1161
|
-
if (
|
|
1162
|
-
throw new TypeError(`${kind}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
this.#decls.cellSchemas.set(name, decl.schema);
|
|
1183
|
-
result[name] = cell;
|
|
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]);
|
|
1184
1464
|
}
|
|
1185
|
-
|
|
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;
|
|
1186
1471
|
}
|
|
1187
1472
|
get(type, input, options) {
|
|
1188
|
-
const host = this.#
|
|
1189
|
-
// A prepare rerun (recording mode) binds new
|
|
1473
|
+
const host = this.#object.host;
|
|
1474
|
+
// A prepare rerun (recording mode) binds new references against the reload's pending contracts: a breaking provider
|
|
1190
1475
|
// change and its updated consumer reload together.
|
|
1191
1476
|
const candidate = this.#previous !== undefined;
|
|
1192
|
-
const address = host.addressOf(type, input, options?.principal ?? host.principal,
|
|
1193
|
-
const declared = this.#decls.
|
|
1477
|
+
const address = host.addressOf(type, input, options?.principal ?? host.principal, candidate);
|
|
1478
|
+
const declared = this.#decls.refs.get(address);
|
|
1194
1479
|
if (declared !== undefined) {
|
|
1195
1480
|
if (declared.contract.fingerprint !== type.fingerprint) {
|
|
1196
|
-
throw invalid("duplicate", `${this.#
|
|
1481
|
+
throw invalid("duplicate", `${this.#object.typeName}: ${type.name} held twice with different contracts`);
|
|
1197
1482
|
}
|
|
1198
1483
|
return declared.proxy;
|
|
1199
1484
|
}
|
|
1200
|
-
const existing = this.#previous?.
|
|
1485
|
+
const existing = this.#previous?.refs.get(address);
|
|
1201
1486
|
if (existing !== undefined &&
|
|
1202
1487
|
existing.contract.fingerprint === type.fingerprint &&
|
|
1203
1488
|
existing.status !== "gone" &&
|
|
1204
1489
|
existing.status !== "released") {
|
|
1205
|
-
this.#decls.
|
|
1490
|
+
this.#decls.refs.set(address, existing);
|
|
1206
1491
|
return existing.proxy;
|
|
1207
1492
|
}
|
|
1208
|
-
const
|
|
1209
|
-
this.#decls.
|
|
1210
|
-
this.#created.
|
|
1211
|
-
return
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
}
|
|
1221
|
-
return resolved;
|
|
1222
|
-
});
|
|
1223
|
-
const existing = this.#previous?.resources.get(name);
|
|
1224
|
-
const kept = existing !== undefined &&
|
|
1225
|
-
existing.spec.rev === spec.rev &&
|
|
1226
|
-
existing.exclusive === (spec.exclusive === true) &&
|
|
1227
|
-
existing.deps.length === deps.length &&
|
|
1228
|
-
existing.deps.every((dep, index) => dep === deps[index]);
|
|
1229
|
-
const resource = kept ? existing : new ResourceImpl(name, spec, deps);
|
|
1230
|
-
if (!kept)
|
|
1231
|
-
this.#created.resources.push(resource);
|
|
1232
|
-
this.#decls.resources.set(name, resource);
|
|
1233
|
-
result[name] = new ResourceFacade(resource, this.#record.host);
|
|
1234
|
-
}
|
|
1235
|
-
return result;
|
|
1236
|
-
}
|
|
1237
|
-
listen(listeners) {
|
|
1238
|
-
for (const [name, decl] of this.#entries(this.#decls.listeners, "listen", listeners, "listener")) {
|
|
1239
|
-
this.#decls.listeners.set(name, decl);
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
tasks(tasks) {
|
|
1243
|
-
for (const [name, fn] of this.#entries(this.#decls.tasks, "tasks", tasks)) {
|
|
1244
|
-
if (typeof fn !== "function")
|
|
1245
|
-
throw new TypeError(`tasks.${name} must be a function`);
|
|
1246
|
-
this.#decls.tasks.set(name, fn);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
derive(derivations) {
|
|
1250
|
-
const result = {};
|
|
1251
|
-
const host = this.#record.host;
|
|
1252
|
-
for (const [name, { sources, fn }] of this.#entries(this.#decls.derives, "derive", derivations, "derivation")) {
|
|
1253
|
-
const existing = this.#previous?.derives.get(name);
|
|
1254
|
-
let derived;
|
|
1255
|
-
if (existing !== undefined &&
|
|
1256
|
-
existing.sources.length === sources.length &&
|
|
1257
|
-
existing.sources.every((source, index) => source === sources[index])) {
|
|
1258
|
-
derived = existing;
|
|
1259
|
-
}
|
|
1260
|
-
else {
|
|
1261
|
-
derived = new Derived(name, sources, fn, { dev: host.dev, report: (error) => host.report(error) });
|
|
1262
|
-
this.#created.derives.push(derived);
|
|
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`);
|
|
1263
1506
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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;
|
|
1269
1553
|
}
|
|
1270
1554
|
expose(resource) {
|
|
1271
1555
|
const resolved = resourceOf(resource);
|
|
1272
1556
|
if (resolved === undefined || ![...this.#decls.resources.values()].includes(resolved)) {
|
|
1273
|
-
throw new TypeError("expose takes a resource declared by this
|
|
1557
|
+
throw new TypeError("expose takes a resource declared by this object");
|
|
1274
1558
|
}
|
|
1275
1559
|
return new ExposedImpl(resolved);
|
|
1276
1560
|
}
|
|
1277
1561
|
register(registry, entries) {
|
|
1278
|
-
if (!this.#
|
|
1279
|
-
throw invalid("
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
this.#decls.registrations.set(id, {
|
|
1285
|
-
registry: registry,
|
|
1286
|
-
key: id.slice(registry.name.length + 1),
|
|
1287
|
-
value,
|
|
1288
|
-
});
|
|
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 });
|
|
1289
1568
|
}
|
|
1290
1569
|
}
|
|
1291
1570
|
}
|
|
1292
|
-
//# sourceMappingURL=
|
|
1571
|
+
//# sourceMappingURL=object.js.map
|