@earendil-works/chord 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +77 -194
- package/dist/cell.d.ts +101 -0
- package/dist/cell.d.ts.map +1 -0
- package/dist/cell.js +316 -0
- package/dist/cell.js.map +1 -0
- package/dist/cluster.d.ts +26 -0
- package/dist/cluster.d.ts.map +1 -0
- package/dist/cluster.js +214 -0
- package/dist/cluster.js.map +1 -0
- package/dist/connection.d.ts +157 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +757 -0
- package/dist/connection.js.map +1 -0
- package/dist/context/index.d.ts +12 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js.map +1 -1
- package/dist/contract.d.ts +133 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +157 -0
- package/dist/contract.js.map +1 -0
- package/dist/declare.d.ts +70 -0
- package/dist/declare.d.ts.map +1 -0
- package/dist/declare.js +22 -0
- package/dist/declare.js.map +1 -0
- package/dist/delta/apply-immutable-trusted.js.map +1 -1
- package/dist/delta/diff.d.ts +1 -1
- package/dist/delta/diff.d.ts.map +1 -1
- package/dist/delta/diff.js.map +1 -1
- package/dist/delta/index.d.ts +2 -2
- package/dist/delta/index.d.ts.map +1 -1
- package/dist/delta/index.js +41 -0
- package/dist/delta/index.js.map +1 -1
- package/dist/delta/revision-validator.d.ts +1 -1
- package/dist/delta/revision-validator.d.ts.map +1 -1
- package/dist/delta/revision-validator.js.map +1 -1
- package/dist/delta/tracker.d.ts +1 -1
- package/dist/delta/tracker.d.ts.map +1 -1
- package/dist/delta/tracker.js +40 -4
- package/dist/delta/tracker.js.map +1 -1
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +44 -0
- package/dist/errors.js.map +1 -0
- package/dist/host.d.ts +254 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +795 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +17 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +9 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js.map +1 -1
- package/dist/link.d.ts +72 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +169 -0
- package/dist/link.js.map +1 -0
- package/dist/loader.d.ts +4 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +68 -0
- package/dist/loader.js.map +1 -0
- package/dist/manager.d.ts +115 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +319 -0
- package/dist/manager.js.map +1 -0
- package/dist/node.d.ts +3 -4
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +3 -2
- package/dist/node.js.map +1 -1
- package/dist/publish.d.ts +5 -0
- package/dist/publish.d.ts.map +1 -0
- package/dist/publish.js +40 -0
- package/dist/publish.js.map +1 -0
- package/dist/record.d.ts +320 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +1292 -0
- package/dist/record.js.map +1 -0
- package/dist/registry.d.ts +25 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +6 -0
- package/dist/registry.js.map +1 -0
- package/dist/reload.d.ts +118 -0
- package/dist/reload.d.ts.map +1 -0
- package/dist/reload.js +589 -0
- package/dist/reload.js.map +1 -0
- package/dist/remote.d.ts +70 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +308 -0
- package/dist/remote.js.map +1 -0
- package/dist/schema.d.ts +21 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +228 -0
- package/dist/schema.js.map +1 -0
- package/dist/state.d.ts +92 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/{services/state.js → state.js} +28 -138
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +62 -262
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/websocket.d.ts +16 -0
- package/dist/websocket.d.ts.map +1 -0
- package/dist/websocket.js +15 -0
- package/dist/websocket.js.map +1 -0
- package/dist/wire.d.ts +135 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +112 -0
- package/dist/wire.js.map +1 -0
- package/docs/guide.md +750 -0
- package/docs/spec.md +261 -0
- package/docs/tutorial.md +909 -0
- package/package.json +9 -10
- package/src/delta/README.md +5 -3
- package/dist/api.d.ts +0 -20
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js +0 -77
- package/dist/api.js.map +0 -1
- package/dist/bundler.d.ts +0 -6
- package/dist/bundler.d.ts.map +0 -1
- package/dist/bundler.js +0 -3
- package/dist/bundler.js.map +0 -1
- package/dist/facets/host.d.ts +0 -12
- package/dist/facets/host.d.ts.map +0 -1
- package/dist/facets/host.js +0 -763
- package/dist/facets/host.js.map +0 -1
- package/dist/facets/loader.d.ts +0 -3
- package/dist/facets/loader.d.ts.map +0 -1
- package/dist/facets/loader.js +0 -5
- package/dist/facets/loader.js.map +0 -1
- package/dist/node/bundle-loader.d.ts +0 -30
- package/dist/node/bundle-loader.d.ts.map +0 -1
- package/dist/node/bundle-loader.js +0 -377
- package/dist/node/bundle-loader.js.map +0 -1
- package/dist/node/bundle.d.ts +0 -26
- package/dist/node/bundle.d.ts.map +0 -1
- package/dist/node/bundle.js +0 -183
- package/dist/node/bundle.js.map +0 -1
- package/dist/node/manifest.d.ts +0 -36
- package/dist/node/manifest.d.ts.map +0 -1
- package/dist/node/manifest.js +0 -6
- package/dist/node/manifest.js.map +0 -1
- package/dist/node/package.d.ts +0 -15
- package/dist/node/package.d.ts.map +0 -1
- package/dist/node/package.js +0 -199
- package/dist/node/package.js.map +0 -1
- package/dist/services/consumer.d.ts +0 -11
- package/dist/services/consumer.d.ts.map +0 -1
- package/dist/services/consumer.js +0 -585
- package/dist/services/consumer.js.map +0 -1
- package/dist/services/errors.d.ts +0 -8
- package/dist/services/errors.d.ts.map +0 -1
- package/dist/services/errors.js +0 -22
- package/dist/services/errors.js.map +0 -1
- package/dist/services/handle.d.ts +0 -15
- package/dist/services/handle.d.ts.map +0 -1
- package/dist/services/handle.js +0 -101
- package/dist/services/handle.js.map +0 -1
- package/dist/services/instances.d.ts +0 -26
- package/dist/services/instances.d.ts.map +0 -1
- package/dist/services/instances.js +0 -140
- package/dist/services/instances.js.map +0 -1
- package/dist/services/loopback.d.ts +0 -5
- package/dist/services/loopback.d.ts.map +0 -1
- package/dist/services/loopback.js +0 -15
- package/dist/services/loopback.js.map +0 -1
- package/dist/services/provider.d.ts +0 -37
- package/dist/services/provider.d.ts.map +0 -1
- package/dist/services/provider.js +0 -521
- package/dist/services/provider.js.map +0 -1
- package/dist/services/state-codec.d.ts +0 -15
- package/dist/services/state-codec.d.ts.map +0 -1
- package/dist/services/state-codec.js +0 -133
- package/dist/services/state-codec.js.map +0 -1
- package/dist/services/state-internals.d.ts +0 -13
- package/dist/services/state-internals.d.ts.map +0 -1
- package/dist/services/state-internals.js +0 -10
- package/dist/services/state-internals.js.map +0 -1
- package/dist/services/state.d.ts +0 -28
- package/dist/services/state.d.ts.map +0 -1
- package/dist/services/state.js.map +0 -1
- package/dist/services/wire.d.ts +0 -63
- package/dist/services/wire.d.ts.map +0 -1
- package/dist/services/wire.js +0 -186
- package/dist/services/wire.js.map +0 -1
package/dist/record.js
ADDED
|
@@ -0,0 +1,1292 @@
|
|
|
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";
|
|
5
|
+
import { asChordError, ChordError, invalid, runAll } from "./errors.js";
|
|
6
|
+
import { enqueue } from "./publish.js";
|
|
7
|
+
import { assertValid } from "./schema.js";
|
|
8
|
+
const noop = () => { };
|
|
9
|
+
export const makeService = (contract, factory, options, facet) => ({
|
|
10
|
+
contract,
|
|
11
|
+
factory: factory,
|
|
12
|
+
options: options,
|
|
13
|
+
facet,
|
|
14
|
+
stateVersion: options.state?.version ?? 1,
|
|
15
|
+
});
|
|
16
|
+
/** Runtime form of `Exposed<T>`. */
|
|
17
|
+
class ExposedImpl {
|
|
18
|
+
resource;
|
|
19
|
+
constructor(resource) {
|
|
20
|
+
this.resource = resource;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class ResourceImpl {
|
|
24
|
+
key;
|
|
25
|
+
spec;
|
|
26
|
+
deps;
|
|
27
|
+
/** `unavailable`: a close/reopen failed during a reload rollback; the value is gone for good. */
|
|
28
|
+
state = "closed";
|
|
29
|
+
#value;
|
|
30
|
+
constructor(key, spec, deps) {
|
|
31
|
+
this.key = key;
|
|
32
|
+
this.spec = spec;
|
|
33
|
+
this.deps = deps;
|
|
34
|
+
}
|
|
35
|
+
get value() {
|
|
36
|
+
if (this.state === "unavailable")
|
|
37
|
+
throw new ChordError("unavailable", `resource ${this.key} could not be restored`);
|
|
38
|
+
if (this.state !== "open")
|
|
39
|
+
throw notReady(`resource ${this.key}`);
|
|
40
|
+
return this.#value;
|
|
41
|
+
}
|
|
42
|
+
get exclusive() {
|
|
43
|
+
return this.spec.exclusive === true;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Opens the resource. An open that completes after `signal` aborted (rollback, abandonment) closes the value it
|
|
47
|
+
* produced at once: nobody owns it any more.
|
|
48
|
+
*/
|
|
49
|
+
async open(signal, order) {
|
|
50
|
+
if (this.state !== "closed")
|
|
51
|
+
return;
|
|
52
|
+
this.state = "opening";
|
|
53
|
+
let value;
|
|
54
|
+
try {
|
|
55
|
+
value = await this.spec.open({ signal });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
this.state = "closed";
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
if (signal.aborted) {
|
|
62
|
+
this.state = "closed";
|
|
63
|
+
await this.spec.close?.(value);
|
|
64
|
+
throw new ChordError("aborted", `resource ${this.key} opened after abort`);
|
|
65
|
+
}
|
|
66
|
+
this.#value = value;
|
|
67
|
+
this.state = "open";
|
|
68
|
+
order.push(this);
|
|
69
|
+
}
|
|
70
|
+
async close(order) {
|
|
71
|
+
if (this.state !== "open")
|
|
72
|
+
return;
|
|
73
|
+
this.state = "closed";
|
|
74
|
+
const index = order.indexOf(this);
|
|
75
|
+
if (index !== -1)
|
|
76
|
+
order.splice(index, 1);
|
|
77
|
+
const value = this.#value;
|
|
78
|
+
this.#value = undefined;
|
|
79
|
+
await this.spec.close?.(value);
|
|
80
|
+
}
|
|
81
|
+
markUnavailable() {
|
|
82
|
+
this.state = "unavailable";
|
|
83
|
+
this.#value = undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* What `self.resources` returns: the resource, except that `.value` throws `not_ready` while any factory runs, even
|
|
88
|
+
* if the resource is open (a reload's prepare rerun must not touch the native resource the old code uses).
|
|
89
|
+
*/
|
|
90
|
+
class ResourceFacade {
|
|
91
|
+
#resource;
|
|
92
|
+
#host;
|
|
93
|
+
constructor(resource, host) {
|
|
94
|
+
this.#resource = resource;
|
|
95
|
+
this.#host = host;
|
|
96
|
+
facades.set(this, resource);
|
|
97
|
+
}
|
|
98
|
+
get key() {
|
|
99
|
+
return this.#resource.key;
|
|
100
|
+
}
|
|
101
|
+
get value() {
|
|
102
|
+
if (this.#host.inFactory > 0)
|
|
103
|
+
throw notReady(`resource ${this.key} (inside a factory)`);
|
|
104
|
+
return this.#resource.value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const facades = new WeakMap();
|
|
108
|
+
const resourceOf = (value) => value instanceof ResourceImpl ? value : value !== null && typeof value === "object" ? facades.get(value) : undefined;
|
|
109
|
+
/** Everything one factory run declared, by kind and key. */
|
|
110
|
+
export class Decls {
|
|
111
|
+
cells = new Map();
|
|
112
|
+
cellSchemas = new Map();
|
|
113
|
+
holds = new Map();
|
|
114
|
+
resources = new Map();
|
|
115
|
+
listeners = new Map();
|
|
116
|
+
tasks = new Map();
|
|
117
|
+
derives = new Map();
|
|
118
|
+
deriveFns = new Map();
|
|
119
|
+
registrations = new Map();
|
|
120
|
+
}
|
|
121
|
+
// ─── Contexts ────────────────────────────────────────────────────────────────
|
|
122
|
+
export class CtxImpl {
|
|
123
|
+
host;
|
|
124
|
+
signal;
|
|
125
|
+
rootId;
|
|
126
|
+
epoch;
|
|
127
|
+
principal;
|
|
128
|
+
/** Code holders (tasks): every call they make starts a fresh root. */
|
|
129
|
+
holder;
|
|
130
|
+
#holds = [];
|
|
131
|
+
#closed = false;
|
|
132
|
+
/** True after the call or root that received this ctx settled: it may no longer start work. */
|
|
133
|
+
get closed() {
|
|
134
|
+
return this.#closed;
|
|
135
|
+
}
|
|
136
|
+
/** Throws unless this ctx may start work on `host` now (not expired, not aborted, not from another host). */
|
|
137
|
+
check(host) {
|
|
138
|
+
if (this.host !== host)
|
|
139
|
+
throw invalid("foreign_ctx", "ctx belongs to another host");
|
|
140
|
+
if (this.#closed)
|
|
141
|
+
throw new ChordError("aborted", "ctx expired: its call already settled", { reason: "expired" });
|
|
142
|
+
if (this.signal.aborted)
|
|
143
|
+
throw new ChordError("aborted", "ctx aborted", { cause: this.signal.reason });
|
|
144
|
+
}
|
|
145
|
+
constructor(host, fields) {
|
|
146
|
+
this.host = host;
|
|
147
|
+
this.signal = fields.signal;
|
|
148
|
+
this.rootId = fields.rootId;
|
|
149
|
+
this.epoch = fields.epoch;
|
|
150
|
+
this.principal = fields.principal;
|
|
151
|
+
this.holder = fields.holder ?? false;
|
|
152
|
+
}
|
|
153
|
+
get(type, input, options) {
|
|
154
|
+
this.check(this.host);
|
|
155
|
+
const handle = this.host.acquire(type, input, { principal: this.principal, ...options }, "call");
|
|
156
|
+
this.#holds.push(handle);
|
|
157
|
+
return handle.proxy;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* A context for a call into `record`: same root and epoch, its own call-owned holds, aborted on forced disposal.
|
|
161
|
+
* Code inside an object acts as the host principal.
|
|
162
|
+
*/
|
|
163
|
+
child(record) {
|
|
164
|
+
return new CtxImpl(this.host, {
|
|
165
|
+
signal: AbortSignal.any([this.signal, record.callAbort.signal]),
|
|
166
|
+
rootId: this.rootId,
|
|
167
|
+
epoch: this.epoch,
|
|
168
|
+
principal: this.host.principal,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
close() {
|
|
172
|
+
this.#closed = true;
|
|
173
|
+
for (const handle of this.#holds.splice(0))
|
|
174
|
+
handle.release();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export async function raceSignal(promise, signal) {
|
|
178
|
+
if (signal.aborted)
|
|
179
|
+
throw new ChordError("aborted", "call aborted", { cause: signal.reason });
|
|
180
|
+
let onAbort = noop;
|
|
181
|
+
const aborted = new Promise((_, reject) => {
|
|
182
|
+
onAbort = () => reject(new ChordError("aborted", "call aborted", { cause: signal.reason }));
|
|
183
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
184
|
+
});
|
|
185
|
+
try {
|
|
186
|
+
return await Promise.race([promise, aborted]);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
signal.removeEventListener("abort", onAbort);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export const HANDLE_BASE = new Set([
|
|
193
|
+
"contract",
|
|
194
|
+
"status",
|
|
195
|
+
"ready",
|
|
196
|
+
"kind",
|
|
197
|
+
"shape",
|
|
198
|
+
"error",
|
|
199
|
+
"onShape",
|
|
200
|
+
"onStatus",
|
|
201
|
+
"release",
|
|
202
|
+
]);
|
|
203
|
+
/** Queues a status notification (after the change that caused it is complete, like every publication). */
|
|
204
|
+
export function notifyStatus(listeners, status, error, report) {
|
|
205
|
+
if (listeners.size === 0)
|
|
206
|
+
return;
|
|
207
|
+
const current = [...listeners];
|
|
208
|
+
enqueue(() => {
|
|
209
|
+
for (const listener of current) {
|
|
210
|
+
try {
|
|
211
|
+
listener(status, error);
|
|
212
|
+
}
|
|
213
|
+
catch (thrown) {
|
|
214
|
+
report(thrown);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/** Proxy → hold, for `deps: [handle]` and links. */
|
|
220
|
+
export const holdsByProxy = new WeakMap();
|
|
221
|
+
export const handleOf = (value) => value !== null && typeof value === "object" ? holdsByProxy.get(value) : undefined;
|
|
222
|
+
/**
|
|
223
|
+
* A handle's state member: a stable readable that follows whatever source the current behavior exposes under that
|
|
224
|
+
* name. `undefined` until the handle is live (or while the variant lacks the member). Rebinding (reload, variant)
|
|
225
|
+
* delivers a reset. Upstream is subscribed only while someone listens.
|
|
226
|
+
*/
|
|
227
|
+
class MemberReadable {
|
|
228
|
+
#handle;
|
|
229
|
+
#name;
|
|
230
|
+
#values = new Set();
|
|
231
|
+
#events = new Set();
|
|
232
|
+
#source;
|
|
233
|
+
#upstream = noop;
|
|
234
|
+
/** The value listeners last saw; a first upstream snapshot equal to it is not re-announced. */
|
|
235
|
+
#seen;
|
|
236
|
+
constructor(handle, name) {
|
|
237
|
+
this.#handle = handle;
|
|
238
|
+
this.#name = name;
|
|
239
|
+
}
|
|
240
|
+
get value() {
|
|
241
|
+
return this.#current()?.value;
|
|
242
|
+
}
|
|
243
|
+
subscribe(listener) {
|
|
244
|
+
this.#values.add(listener);
|
|
245
|
+
this.#seen = this.value;
|
|
246
|
+
this.#safe(() => listener(this.#seen));
|
|
247
|
+
this.bind();
|
|
248
|
+
return () => this.#remove(this.#values, listener);
|
|
249
|
+
}
|
|
250
|
+
events(listener) {
|
|
251
|
+
this.#events.add(listener);
|
|
252
|
+
this.#seen = this.value;
|
|
253
|
+
this.#safe(() => listener({ type: "snap", value: this.#seen }));
|
|
254
|
+
this.bind();
|
|
255
|
+
return () => this.#remove(this.#events, listener);
|
|
256
|
+
}
|
|
257
|
+
/** (Re)subscribes upstream to the current source; a changed source is announced as a reset. */
|
|
258
|
+
bind() {
|
|
259
|
+
const source = this.#values.size + this.#events.size > 0 ? this.#current() : undefined;
|
|
260
|
+
if (source === this.#source)
|
|
261
|
+
return;
|
|
262
|
+
this.#upstream();
|
|
263
|
+
this.#upstream = noop;
|
|
264
|
+
this.#source = source;
|
|
265
|
+
if (source === undefined)
|
|
266
|
+
return;
|
|
267
|
+
let first = true;
|
|
268
|
+
this.#upstream = source.events((event) => {
|
|
269
|
+
const isFirst = first;
|
|
270
|
+
first = false;
|
|
271
|
+
if (isFirst && event.value === this.#seen)
|
|
272
|
+
return;
|
|
273
|
+
this.#seen = event.value;
|
|
274
|
+
const translated = isFirst ? { type: "reset", value: event.value } : event;
|
|
275
|
+
const events = [...this.#events];
|
|
276
|
+
const values = [...this.#values];
|
|
277
|
+
// Queued: a rebind during a commit must not run observers before the commit completes.
|
|
278
|
+
enqueue(() => {
|
|
279
|
+
for (const listener of events)
|
|
280
|
+
if (this.#events.has(listener))
|
|
281
|
+
this.#safe(() => listener(translated));
|
|
282
|
+
for (const listener of values)
|
|
283
|
+
if (this.#values.has(listener))
|
|
284
|
+
this.#safe(() => listener(event.value));
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
stop() {
|
|
289
|
+
this.#values.clear();
|
|
290
|
+
this.#events.clear();
|
|
291
|
+
this.bind();
|
|
292
|
+
}
|
|
293
|
+
#current() {
|
|
294
|
+
const view = this.#handle.currentView();
|
|
295
|
+
if (view === undefined || !this.#handle.inVariant(view, this.#name))
|
|
296
|
+
return undefined;
|
|
297
|
+
const value = view[this.#name];
|
|
298
|
+
// Cells and derivations, or any readable an `accepts` adapter maps them to.
|
|
299
|
+
return isSource(value) || isReadableLike(value) ? value : undefined;
|
|
300
|
+
}
|
|
301
|
+
#remove(set, listener) {
|
|
302
|
+
set.delete(listener);
|
|
303
|
+
this.bind();
|
|
304
|
+
}
|
|
305
|
+
#safe(fn) {
|
|
306
|
+
try {
|
|
307
|
+
fn();
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
this.#handle.record.host.report(error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** One hold on a record. Exposed through a Proxy so members bind late to the current behavior. */
|
|
315
|
+
export class HandleImpl {
|
|
316
|
+
record;
|
|
317
|
+
contract;
|
|
318
|
+
owner;
|
|
319
|
+
address;
|
|
320
|
+
proxy;
|
|
321
|
+
ready;
|
|
322
|
+
status;
|
|
323
|
+
error;
|
|
324
|
+
shapeListeners = new Set();
|
|
325
|
+
/** Internal: notified when the handle becomes gone (links forward it). */
|
|
326
|
+
goneListeners = new Set();
|
|
327
|
+
readables = new Map();
|
|
328
|
+
bindings = new Map();
|
|
329
|
+
views = new WeakMap();
|
|
330
|
+
callers = new Map();
|
|
331
|
+
constructor(record, contract, owner) {
|
|
332
|
+
this.record = record;
|
|
333
|
+
this.contract = contract;
|
|
334
|
+
this.owner = owner;
|
|
335
|
+
this.address = record.address;
|
|
336
|
+
this.status = record.status === "live" ? "live" : "loading";
|
|
337
|
+
this.ready = record.ready.then(() => {
|
|
338
|
+
if (this.status === "gone")
|
|
339
|
+
throw this.error;
|
|
340
|
+
if (this.status === "released")
|
|
341
|
+
throw new ChordError("released", `handle ${this.address} released`);
|
|
342
|
+
});
|
|
343
|
+
this.ready.catch(noop);
|
|
344
|
+
this.proxy = new Proxy(this, {
|
|
345
|
+
get: (target, property) => {
|
|
346
|
+
if (HANDLE_BASE.has(property)) {
|
|
347
|
+
const value = Reflect.get(target, property, target);
|
|
348
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
349
|
+
}
|
|
350
|
+
if (typeof property !== "string" || property === "then")
|
|
351
|
+
return undefined;
|
|
352
|
+
return target.member(property);
|
|
353
|
+
},
|
|
354
|
+
has: (target, property) => HANDLE_BASE.has(property) || target.member(String(property)) !== undefined,
|
|
355
|
+
});
|
|
356
|
+
holdsByProxy.set(this.proxy, this);
|
|
357
|
+
}
|
|
358
|
+
get kind() {
|
|
359
|
+
const view = this.currentView();
|
|
360
|
+
return view === undefined ? undefined : kindOf(view);
|
|
361
|
+
}
|
|
362
|
+
get shape() {
|
|
363
|
+
const installed = this.status === "live" ? this.record.currentInstalled() : undefined;
|
|
364
|
+
const view = installed === undefined ? undefined : this.#safeView(installed);
|
|
365
|
+
if (installed === undefined || view === undefined)
|
|
366
|
+
return undefined;
|
|
367
|
+
return {
|
|
368
|
+
kind: kindOf(view),
|
|
369
|
+
members: Object.keys(view).filter((key) => key !== "kind"),
|
|
370
|
+
contract: installed.service.contract.fingerprint,
|
|
371
|
+
epoch: installed.epoch,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
onShape(listener) {
|
|
375
|
+
this.shapeListeners.add(listener);
|
|
376
|
+
return () => this.shapeListeners.delete(listener);
|
|
377
|
+
}
|
|
378
|
+
onStatus(listener) {
|
|
379
|
+
this.#statusListeners.add(listener);
|
|
380
|
+
return () => this.#statusListeners.delete(listener);
|
|
381
|
+
}
|
|
382
|
+
#statusListeners = new Set();
|
|
383
|
+
#statusChanged() {
|
|
384
|
+
notifyStatus(this.#statusListeners, this.status, this.error, (error) => this.record.host.report(error));
|
|
385
|
+
}
|
|
386
|
+
release() {
|
|
387
|
+
if (this.status === "released" || this.status === "gone")
|
|
388
|
+
return;
|
|
389
|
+
this.status = "released";
|
|
390
|
+
this.#stopReadables();
|
|
391
|
+
this.#statusChanged();
|
|
392
|
+
this.record.release(this);
|
|
393
|
+
}
|
|
394
|
+
// ─── internal ───
|
|
395
|
+
live() {
|
|
396
|
+
if (this.status !== "loading")
|
|
397
|
+
return;
|
|
398
|
+
this.status = "live";
|
|
399
|
+
this.#statusChanged();
|
|
400
|
+
this.shapeChanged();
|
|
401
|
+
}
|
|
402
|
+
gone(error) {
|
|
403
|
+
if (this.status === "released" || this.status === "gone")
|
|
404
|
+
return;
|
|
405
|
+
this.status = "gone";
|
|
406
|
+
this.error = error;
|
|
407
|
+
this.#stopReadables();
|
|
408
|
+
this.#statusChanged();
|
|
409
|
+
for (const listener of [...this.goneListeners])
|
|
410
|
+
listener(error);
|
|
411
|
+
}
|
|
412
|
+
shapeChanged() {
|
|
413
|
+
for (const readable of this.readables.values())
|
|
414
|
+
readable.bind();
|
|
415
|
+
const shape = this.shape;
|
|
416
|
+
if (shape === undefined)
|
|
417
|
+
return;
|
|
418
|
+
const listeners = [...this.shapeListeners];
|
|
419
|
+
enqueue(() => {
|
|
420
|
+
for (const listener of listeners) {
|
|
421
|
+
try {
|
|
422
|
+
listener(shape);
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
this.record.host.report(error);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
terminalError() {
|
|
431
|
+
return this.status === "gone" ? this.error : new ChordError("released", `handle ${this.address} released`);
|
|
432
|
+
}
|
|
433
|
+
/** The behavior as this consumer sees it (through an accepts adapter when bound to another version). */
|
|
434
|
+
viewOf(installed) {
|
|
435
|
+
let adapter = this.bindings.get(installed.service);
|
|
436
|
+
if (!this.bindings.has(installed.service)) {
|
|
437
|
+
adapter = bind(installed.service.contract, installed.service.options.accepts, this.contract);
|
|
438
|
+
this.bindings.set(installed.service, adapter);
|
|
439
|
+
}
|
|
440
|
+
if (adapter === undefined)
|
|
441
|
+
return installed.behavior;
|
|
442
|
+
let view = this.views.get(installed.behavior);
|
|
443
|
+
if (view === undefined) {
|
|
444
|
+
view = adapter(installed.behavior);
|
|
445
|
+
this.views.set(installed.behavior, view);
|
|
446
|
+
}
|
|
447
|
+
return view;
|
|
448
|
+
}
|
|
449
|
+
currentView() {
|
|
450
|
+
if (this.status !== "live")
|
|
451
|
+
return undefined;
|
|
452
|
+
const installed = this.record.currentInstalled();
|
|
453
|
+
return installed === undefined ? undefined : this.#safeView(installed);
|
|
454
|
+
}
|
|
455
|
+
/** The view, or undefined when this consumer no longer binds (reported; calls reject with the bind error). */
|
|
456
|
+
#safeView(installed) {
|
|
457
|
+
try {
|
|
458
|
+
return this.viewOf(installed);
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
this.record.host.report(error);
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
describe(kind, member) {
|
|
466
|
+
const variants = this.contract.description.variants;
|
|
467
|
+
if (kind !== undefined)
|
|
468
|
+
return variants[kind]?.[member];
|
|
469
|
+
for (const members of Object.values(variants))
|
|
470
|
+
if (members[member] !== undefined)
|
|
471
|
+
return members[member];
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
/** Remote consumers see only the members their contract lists for the current variant. */
|
|
475
|
+
inVariant(view, name) {
|
|
476
|
+
return this.contract.scope !== "remote" || this.describe(kindOf(view), name) !== undefined;
|
|
477
|
+
}
|
|
478
|
+
member(name) {
|
|
479
|
+
const declared = this.describe(undefined, name);
|
|
480
|
+
const view = this.currentView();
|
|
481
|
+
const value = view?.[name];
|
|
482
|
+
if (declared?.kind === "method" || (declared === undefined && typeof value === "function")) {
|
|
483
|
+
return this.caller(name);
|
|
484
|
+
}
|
|
485
|
+
if (declared?.kind === "state" || (declared === undefined && (isSource(value) || isReadableLike(value)))) {
|
|
486
|
+
let readable = this.readables.get(name);
|
|
487
|
+
if (readable === undefined) {
|
|
488
|
+
readable = new MemberReadable(this, name);
|
|
489
|
+
this.readables.set(name, readable);
|
|
490
|
+
}
|
|
491
|
+
return readable;
|
|
492
|
+
}
|
|
493
|
+
if (view === undefined || !this.inVariant(view, name))
|
|
494
|
+
return undefined;
|
|
495
|
+
return value instanceof ExposedImpl ? value.resource.value : value;
|
|
496
|
+
}
|
|
497
|
+
caller(name) {
|
|
498
|
+
let caller = this.callers.get(name);
|
|
499
|
+
if (caller === undefined) {
|
|
500
|
+
caller = (args, ctx) => {
|
|
501
|
+
if (this.record.host.inFactory > 0) {
|
|
502
|
+
throw new ChordError("not_ready", `${this.address}.${name} called inside a factory`);
|
|
503
|
+
}
|
|
504
|
+
if (ctx !== undefined && !(ctx instanceof CtxImpl)) {
|
|
505
|
+
return Promise.reject(invalid("foreign_ctx", "ctx must come from a Chord host"));
|
|
506
|
+
}
|
|
507
|
+
if (ctx === undefined || ctx.holder) {
|
|
508
|
+
// A code holder (task) starts a fresh root per call, cancelled with the holder.
|
|
509
|
+
try {
|
|
510
|
+
ctx?.check(this.record.host);
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
return Promise.reject(error);
|
|
514
|
+
}
|
|
515
|
+
return this.record.host.root((root) => this.record.call(this, name, args, root), {
|
|
516
|
+
signal: ctx?.signal,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return this.record.call(this, name, args, ctx);
|
|
520
|
+
};
|
|
521
|
+
this.callers.set(name, caller);
|
|
522
|
+
}
|
|
523
|
+
return caller;
|
|
524
|
+
}
|
|
525
|
+
#stopReadables() {
|
|
526
|
+
for (const readable of this.readables.values())
|
|
527
|
+
readable.stop();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
export const kindOf = (behavior) => typeof behavior.kind === "string" ? behavior.kind : undefined;
|
|
531
|
+
/**
|
|
532
|
+
* 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 behavior depends on captured values
|
|
534
|
+
* that change between runs is not detected; such a listener keeps its old subscription until its source changes.)
|
|
535
|
+
*/
|
|
536
|
+
const sameSelect = (a, b) => a === b || (a !== undefined && b !== undefined && String(a) === String(b));
|
|
537
|
+
export class RecordImpl {
|
|
538
|
+
host;
|
|
539
|
+
address;
|
|
540
|
+
typeName;
|
|
541
|
+
input;
|
|
542
|
+
principal;
|
|
543
|
+
eager;
|
|
544
|
+
status = "activating";
|
|
545
|
+
ready = Promise.resolve();
|
|
546
|
+
service;
|
|
547
|
+
stateVersion = 0;
|
|
548
|
+
/** Calls carrying an older epoch are rejected upgrade_conflict (state bump or exclusive resource replaced). */
|
|
549
|
+
minEpoch = 0;
|
|
550
|
+
decls = new Decls();
|
|
551
|
+
/** Declarations of a factory run that has not been installed yet (activation or reload candidate). */
|
|
552
|
+
pending = new Set();
|
|
553
|
+
behaviors = new Map();
|
|
554
|
+
holds = new Set();
|
|
555
|
+
leases = new Map();
|
|
556
|
+
openOrder = [];
|
|
557
|
+
callAbort = new AbortController();
|
|
558
|
+
activationAbort = new AbortController();
|
|
559
|
+
gate;
|
|
560
|
+
/** Served only by a running reload's pending epoch: listeners, tasks and registrations start at its commit. */
|
|
561
|
+
deferredRuntime = false;
|
|
562
|
+
#subscriptions = new Map();
|
|
563
|
+
#invocations = new Set();
|
|
564
|
+
#tasks = new Map();
|
|
565
|
+
#stopped = [];
|
|
566
|
+
#linger;
|
|
567
|
+
#disposal;
|
|
568
|
+
constructor(host, identity) {
|
|
569
|
+
this.host = host;
|
|
570
|
+
this.address = identity.address;
|
|
571
|
+
this.typeName = identity.typeName;
|
|
572
|
+
this.input = identity.input;
|
|
573
|
+
this.principal = identity.principal;
|
|
574
|
+
this.eager = identity.eager;
|
|
575
|
+
}
|
|
576
|
+
start(after) {
|
|
577
|
+
this.ready = this.#activate(after).then(() => {
|
|
578
|
+
for (const handle of this.holds)
|
|
579
|
+
handle.live();
|
|
580
|
+
this.#maybeIdle();
|
|
581
|
+
}, (cause) => {
|
|
582
|
+
if (this.status === "activating")
|
|
583
|
+
this.status = "failed";
|
|
584
|
+
this.host.forget(this);
|
|
585
|
+
// A ChordError (forbidden, a dependency's error, ...) keeps its code; anything else is `unavailable`.
|
|
586
|
+
const error = asChordError(cause, `activating ${this.address}`);
|
|
587
|
+
for (const handle of this.holds)
|
|
588
|
+
handle.gone(error);
|
|
589
|
+
throw error;
|
|
590
|
+
});
|
|
591
|
+
this.ready.catch(noop);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Activation as one transaction: factory, opens, then a synchronous finalization (registry check and publication,
|
|
595
|
+
* behaviors, runtime). Every failure, including one during finalization or a release while listeners start,
|
|
596
|
+
* unwinds everything this activation did.
|
|
597
|
+
*/
|
|
598
|
+
async #activate(after) {
|
|
599
|
+
await after?.catch(noop);
|
|
600
|
+
const signal = this.activationAbort.signal;
|
|
601
|
+
const abandoned = () => new ChordError("aborted", `${this.address} abandoned`);
|
|
602
|
+
if (signal.aborted)
|
|
603
|
+
throw abandoned();
|
|
604
|
+
const service = this.host.service(this.typeName);
|
|
605
|
+
if (service === undefined)
|
|
606
|
+
throw new ChordError("unavailable", `${this.typeName} is not served`, { reason: "unknown_type" });
|
|
607
|
+
this.service = service;
|
|
608
|
+
this.stateVersion = service.stateVersion;
|
|
609
|
+
const run = this.run(service, undefined);
|
|
610
|
+
try {
|
|
611
|
+
await this.open(run.created.resources, run.created.holds, signal);
|
|
612
|
+
if (signal.aborted)
|
|
613
|
+
throw abandoned();
|
|
614
|
+
this.host.checkRegistrations(this, run.decls);
|
|
615
|
+
this.decls = run.decls;
|
|
616
|
+
for (const [retained, services] of this.host.retainedEpochs()) {
|
|
617
|
+
if (services.get(this.typeName) === service) {
|
|
618
|
+
this.behaviors.set(retained, { epoch: retained, service, behavior: run.behavior, decls: run.decls });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const pending = this.host.pendingEpoch();
|
|
622
|
+
if (!this.behaviors.has(this.host.currentEpoch()) && (pending === undefined || !this.behaviors.has(pending))) {
|
|
623
|
+
throw new ChordError("superseded", `${this.address}: code changed during activation`);
|
|
624
|
+
}
|
|
625
|
+
for (const derived of run.created.derives)
|
|
626
|
+
derived.attach();
|
|
627
|
+
this.refreshSchemas();
|
|
628
|
+
// Served only by a running reload's pending epoch: new code must not run (listeners, tasks, registrations)
|
|
629
|
+
// before that reload commits.
|
|
630
|
+
this.deferredRuntime = !this.behaviors.has(this.host.currentEpoch());
|
|
631
|
+
if (!this.deferredRuntime) {
|
|
632
|
+
if (run.decls.registrations.size > 0)
|
|
633
|
+
this.host.registriesChanged(this);
|
|
634
|
+
this.startRuntime();
|
|
635
|
+
}
|
|
636
|
+
// A listener starting up may have released the last hold synchronously: no resurrection.
|
|
637
|
+
if (signal.aborted)
|
|
638
|
+
throw abandoned();
|
|
639
|
+
this.status = "live";
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
// Invocations started during finalization (listener roots) end before their resources close.
|
|
643
|
+
this.callAbort.abort(error);
|
|
644
|
+
this.stopListeners();
|
|
645
|
+
this.stopTasks();
|
|
646
|
+
const registered = this.decls.registrations.size > 0 && !this.deferredRuntime;
|
|
647
|
+
this.behaviors.clear();
|
|
648
|
+
this.decls = new Decls();
|
|
649
|
+
if (registered)
|
|
650
|
+
this.host.registriesChanged();
|
|
651
|
+
await Promise.allSettled([...this.#invocations, ...this.#stopped.splice(0)]);
|
|
652
|
+
await this.unwind(run).catch((cleanup) => this.host.report(cleanup));
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
finally {
|
|
656
|
+
this.pending.delete(run.decls);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Runs the factory synchronously. With `previous`, runs in recording mode for a reload prepare. The run's
|
|
661
|
+
* declarations stay in `pending` (visible to the cycle check) until the caller installs or discards them.
|
|
662
|
+
*/
|
|
663
|
+
run(service, previous) {
|
|
664
|
+
const self = new SelfImpl(this, previous);
|
|
665
|
+
const discard = (run) => {
|
|
666
|
+
this.pending.delete(run.decls);
|
|
667
|
+
void this.unwind(run).catch((cleanup) => this.host.report(cleanup));
|
|
668
|
+
};
|
|
669
|
+
this.pending.add(self.run(undefined).decls);
|
|
670
|
+
this.host.inFactory++;
|
|
671
|
+
let behavior;
|
|
672
|
+
try {
|
|
673
|
+
behavior = service.factory(this.input, this.principal, self);
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
this.host.inFactory--;
|
|
677
|
+
discard(self.run(undefined));
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
this.host.inFactory--;
|
|
681
|
+
const run = self.run(behavior);
|
|
682
|
+
try {
|
|
683
|
+
if (behavior instanceof Promise)
|
|
684
|
+
throw invalid("behavior", `${this.typeName}: factories must be synchronous`);
|
|
685
|
+
validateBehavior(service, run.behavior);
|
|
686
|
+
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
discard(run);
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
691
|
+
return run;
|
|
692
|
+
}
|
|
693
|
+
/** Opens resources (dependencies first) and waits for holds; settles all before failing. */
|
|
694
|
+
async open(resources, holds, signal) {
|
|
695
|
+
const opening = new Map();
|
|
696
|
+
const openOne = (resource) => {
|
|
697
|
+
let promise = opening.get(resource);
|
|
698
|
+
if (promise === undefined) {
|
|
699
|
+
promise = (async () => {
|
|
700
|
+
await Promise.all(resource.deps.map((dep) => !(dep instanceof ResourceImpl) ? dep.ready : resources.includes(dep) ? openOne(dep) : undefined));
|
|
701
|
+
await resource.open(signal, this.openOrder);
|
|
702
|
+
})();
|
|
703
|
+
opening.set(resource, promise);
|
|
704
|
+
}
|
|
705
|
+
return promise;
|
|
706
|
+
};
|
|
707
|
+
const results = await Promise.allSettled([...resources.map(openOne), ...holds.map((hold) => hold.ready)]);
|
|
708
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
709
|
+
if (failure !== undefined)
|
|
710
|
+
throw failure.reason;
|
|
711
|
+
}
|
|
712
|
+
/** Undoes the objects a run created: closes its resources (reverse order) and releases its holds. */
|
|
713
|
+
async unwind(run) {
|
|
714
|
+
for (const derived of run.created.derives)
|
|
715
|
+
derived.detach();
|
|
716
|
+
const resources = [...run.created.resources].sort((a, b) => this.openOrder.indexOf(b) - this.openOrder.indexOf(a));
|
|
717
|
+
await runAll([
|
|
718
|
+
...resources.map((resource) => () => resource.close(this.openOrder)),
|
|
719
|
+
...run.created.holds.map((hold) => () => hold.release()),
|
|
720
|
+
], `${this.address}: cleanup failed`);
|
|
721
|
+
}
|
|
722
|
+
currentInstalled() {
|
|
723
|
+
return this.behaviors.get(this.host.currentEpoch()) ?? [...this.behaviors.values()].at(-1);
|
|
724
|
+
}
|
|
725
|
+
/** Every state source exposed as a remote state member gets that member's schema (checked per write in dev mode). */
|
|
726
|
+
refreshSchemas() {
|
|
727
|
+
const schemas = new Map();
|
|
728
|
+
for (const installed of this.behaviors.values()) {
|
|
729
|
+
for (const [source, list] of memberSchemas(installed.service, installed.behavior)) {
|
|
730
|
+
schemas.set(source, [...(schemas.get(source) ?? []), ...list]);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
for (const decls of this.allDecls()) {
|
|
734
|
+
for (const source of [...decls.cells.values(), ...decls.derives.values()])
|
|
735
|
+
source.schemas = schemas.get(source) ?? [];
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
allDecls() {
|
|
739
|
+
const result = new Set([this.decls, ...this.pending]);
|
|
740
|
+
for (const installed of this.behaviors.values())
|
|
741
|
+
result.add(installed.decls);
|
|
742
|
+
return result;
|
|
743
|
+
}
|
|
744
|
+
// ─── runtime: listeners and tasks of the current declarations ───
|
|
745
|
+
/** The resource a listener is declared on, or its source itself. */
|
|
746
|
+
#origin(decl) {
|
|
747
|
+
return resourceOf(decl.source) ?? decl.source;
|
|
748
|
+
}
|
|
749
|
+
/** Resolves a listener's subscribable: `select(resource value)`, `select(source)` or the source itself. */
|
|
750
|
+
#listenSource(key, decl) {
|
|
751
|
+
const resource = resourceOf(decl.source);
|
|
752
|
+
const value = resource === undefined ? decl.source : resource.value;
|
|
753
|
+
const source = (decl.select === undefined ? value : decl.select(value));
|
|
754
|
+
if (typeof source?.subscribe !== "function")
|
|
755
|
+
throw invalid("behavior", `listener ${key}: source has no subscribe()`);
|
|
756
|
+
return source;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Subscribes the listeners of `decls` without delivering: events are buffered until `activate`. A listener declared
|
|
760
|
+
* on the same resource (or source) with the same `select` keeps its subscription without calling `select` again
|
|
761
|
+
* (only its function is swapped, by name), so sources that accept a single subscriber survive reloads. Throws, with
|
|
762
|
+
* nothing left subscribed, if any source fails.
|
|
763
|
+
*/
|
|
764
|
+
stageListeners(decls) {
|
|
765
|
+
const fresh = new Map();
|
|
766
|
+
const kept = new Set();
|
|
767
|
+
try {
|
|
768
|
+
for (const [key, decl] of decls.listeners) {
|
|
769
|
+
const origin = this.#origin(decl);
|
|
770
|
+
const previous = this.#subscriptions.get(key);
|
|
771
|
+
if (previous !== undefined && previous.origin === origin && sameSelect(previous.select, decl.select)) {
|
|
772
|
+
kept.add(key);
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const source = this.#listenSource(key, decl);
|
|
776
|
+
const entry = {
|
|
777
|
+
origin,
|
|
778
|
+
select: decl.select,
|
|
779
|
+
unsubscribe: noop,
|
|
780
|
+
buffer: [],
|
|
781
|
+
};
|
|
782
|
+
fresh.set(key, entry);
|
|
783
|
+
entry.unsubscribe = source.subscribe((event) => {
|
|
784
|
+
if (entry.buffer !== undefined)
|
|
785
|
+
entry.buffer.push(event);
|
|
786
|
+
else
|
|
787
|
+
this.#deliver(key, event);
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
for (const entry of fresh.values())
|
|
793
|
+
entry.unsubscribe();
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
activate: () => {
|
|
798
|
+
for (const [key, subscription] of this.#subscriptions) {
|
|
799
|
+
if (kept.has(key))
|
|
800
|
+
continue;
|
|
801
|
+
subscription.unsubscribe();
|
|
802
|
+
this.#subscriptions.delete(key);
|
|
803
|
+
}
|
|
804
|
+
for (const [key, entry] of fresh) {
|
|
805
|
+
this.#subscriptions.set(key, entry);
|
|
806
|
+
const buffered = entry.buffer ?? [];
|
|
807
|
+
entry.buffer = undefined;
|
|
808
|
+
for (const event of buffered)
|
|
809
|
+
this.#deliver(key, event);
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
discard: () => {
|
|
813
|
+
for (const entry of fresh.values())
|
|
814
|
+
entry.unsubscribe();
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
startRuntime() {
|
|
819
|
+
this.stageListeners(this.decls).activate();
|
|
820
|
+
this.startTasks();
|
|
821
|
+
}
|
|
822
|
+
startTasks() {
|
|
823
|
+
for (const key of this.decls.tasks.keys())
|
|
824
|
+
this.startTask(key, this.host.currentEpoch());
|
|
825
|
+
}
|
|
826
|
+
stopListeners() {
|
|
827
|
+
for (const subscription of this.#subscriptions.values())
|
|
828
|
+
subscription.unsubscribe();
|
|
829
|
+
this.#subscriptions.clear();
|
|
830
|
+
}
|
|
831
|
+
startTask(key, epoch) {
|
|
832
|
+
const fn = this.decls.tasks.get(key);
|
|
833
|
+
if (fn === undefined)
|
|
834
|
+
return;
|
|
835
|
+
const controller = new AbortController();
|
|
836
|
+
const ctx = new CtxImpl(this.host, {
|
|
837
|
+
signal: AbortSignal.any([controller.signal, this.callAbort.signal]),
|
|
838
|
+
rootId: `task:${this.address}:${key}`,
|
|
839
|
+
epoch,
|
|
840
|
+
principal: this.host.principal,
|
|
841
|
+
holder: true,
|
|
842
|
+
});
|
|
843
|
+
const done = (async () => {
|
|
844
|
+
try {
|
|
845
|
+
await fn(ctx);
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
if (!ctx.signal.aborted)
|
|
849
|
+
this.host.report(error);
|
|
850
|
+
}
|
|
851
|
+
finally {
|
|
852
|
+
ctx.close();
|
|
853
|
+
if (this.#tasks.get(key)?.controller === controller)
|
|
854
|
+
this.#tasks.delete(key);
|
|
855
|
+
}
|
|
856
|
+
})();
|
|
857
|
+
this.#tasks.set(key, { controller, done });
|
|
858
|
+
}
|
|
859
|
+
/** Aborts running tasks; their completion is awaited at disposal. */
|
|
860
|
+
stopTasks() {
|
|
861
|
+
for (const task of this.#tasks.values()) {
|
|
862
|
+
task.controller.abort(new ChordError("aborted", "task stopped"));
|
|
863
|
+
this.#stopped.push(task.done);
|
|
864
|
+
}
|
|
865
|
+
this.#tasks.clear();
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Delivers one listener event, in publication order. The invocation is a root that holds a lease on this record
|
|
869
|
+
* (a reload gate waits for it like for any other root that touched the record), counts as an invocation (disposal
|
|
870
|
+
* waits for it) and is cancelled by forced disposal.
|
|
871
|
+
*/
|
|
872
|
+
#deliver(key, event) {
|
|
873
|
+
enqueue(() => {
|
|
874
|
+
if (this.gate !== undefined) {
|
|
875
|
+
this.gate.queue.push({ key, event });
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const decl = this.decls.listeners.get(key);
|
|
879
|
+
if (decl === undefined || (this.status !== "live" && this.status !== "activating"))
|
|
880
|
+
return;
|
|
881
|
+
const invocation = this.host.root((root) => {
|
|
882
|
+
this.lease(root);
|
|
883
|
+
const child = root.child(this);
|
|
884
|
+
return this.#invoke(async () => {
|
|
885
|
+
try {
|
|
886
|
+
await decl.fn(event, child);
|
|
887
|
+
}
|
|
888
|
+
finally {
|
|
889
|
+
child.close();
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
invocation.catch((error) => this.host.report(error));
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
/** Opens the record's gate and replays queued listener events through the current listeners. */
|
|
897
|
+
openGate() {
|
|
898
|
+
const gate = this.gate;
|
|
899
|
+
this.gate = undefined;
|
|
900
|
+
for (const { key, event } of gate?.queue ?? [])
|
|
901
|
+
this.#deliver(key, event);
|
|
902
|
+
}
|
|
903
|
+
// ─── calls ───
|
|
904
|
+
/** Admission for a call or write carrying `ctx`. */
|
|
905
|
+
admit(ctx) {
|
|
906
|
+
if (this.host.failed !== undefined) {
|
|
907
|
+
throw new ChordError("unavailable", `${this.host.name} failed and admits no work`, { reason: "host_failed" });
|
|
908
|
+
}
|
|
909
|
+
if (!this.host.hasEpoch(ctx.epoch))
|
|
910
|
+
throw new ChordError("superseded", `epoch ${ctx.epoch} is retired`);
|
|
911
|
+
if (ctx.epoch < this.minEpoch) {
|
|
912
|
+
throw new ChordError("upgrade_conflict", `${this.address} was upgraded in epoch ${this.minEpoch}`);
|
|
913
|
+
}
|
|
914
|
+
if (this.gate !== undefined && !this.gate.grandfathered.has(ctx.rootId)) {
|
|
915
|
+
throw new ChordError("upgrade_conflict", `${this.address} is being upgraded`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/** Records that the root of `ctx` touched this record (a reload gate grandfathers it). */
|
|
919
|
+
lease(ctx) {
|
|
920
|
+
if (this.leases.has(ctx.rootId))
|
|
921
|
+
return;
|
|
922
|
+
this.leases.set(ctx.rootId, ctx.epoch);
|
|
923
|
+
this.host.touch(ctx.rootId, this);
|
|
924
|
+
}
|
|
925
|
+
guard = (ctx) => {
|
|
926
|
+
if (this.host.inFactory > 0)
|
|
927
|
+
throw invalid("read_only", "cells cannot be written while a factory runs");
|
|
928
|
+
if (ctx instanceof CtxImpl)
|
|
929
|
+
ctx.check(this.host);
|
|
930
|
+
if (ctx.signal.aborted)
|
|
931
|
+
throw new ChordError("aborted", "write with an aborted ctx");
|
|
932
|
+
if (this.status === "disposed")
|
|
933
|
+
throw new ChordError("gone", `${this.address} is disposed`);
|
|
934
|
+
if (ctx.rootId.startsWith("task:"))
|
|
935
|
+
return;
|
|
936
|
+
this.admit(ctx);
|
|
937
|
+
};
|
|
938
|
+
async call(handle, name, args, ctx) {
|
|
939
|
+
ctx.check(this.host);
|
|
940
|
+
if (handle.status === "released" || handle.status === "gone")
|
|
941
|
+
throw handle.terminalError();
|
|
942
|
+
if (handle.status === "loading")
|
|
943
|
+
await raceSignal(handle.ready, ctx.signal);
|
|
944
|
+
if (handle.status !== "live" || this.status !== "live") {
|
|
945
|
+
throw handle.status === "live"
|
|
946
|
+
? new ChordError("gone", `${this.address} is ${this.status}`)
|
|
947
|
+
: handle.terminalError();
|
|
948
|
+
}
|
|
949
|
+
this.admit(ctx);
|
|
950
|
+
const installed = this.behaviors.get(ctx.epoch);
|
|
951
|
+
if (installed === undefined)
|
|
952
|
+
throw new ChordError("superseded", `${this.address} has no behavior for epoch ${ctx.epoch}`);
|
|
953
|
+
const view = handle.viewOf(installed);
|
|
954
|
+
const kind = kindOf(view);
|
|
955
|
+
const wanted = handle.describe(handle.contract.scope === "remote" ? kind : LOCAL_VARIANT, name);
|
|
956
|
+
const fn = view[name];
|
|
957
|
+
if (typeof fn !== "function" || (handle.contract.scope === "remote" && wanted?.kind !== "method")) {
|
|
958
|
+
throw new ChordError("not_in_variant", `${this.address}: ${name} is not a method of variant ${kind ?? "(local)"}`);
|
|
959
|
+
}
|
|
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
|
+
this.lease(ctx);
|
|
971
|
+
const child = ctx.child(this);
|
|
972
|
+
let result;
|
|
973
|
+
try {
|
|
974
|
+
result = await this.#invoke(() => fn(input, child));
|
|
975
|
+
}
|
|
976
|
+
finally {
|
|
977
|
+
child.close();
|
|
978
|
+
}
|
|
979
|
+
if (validated)
|
|
980
|
+
assertValid("result", `${this.address}.${name} result`, validated.result, result);
|
|
981
|
+
return result;
|
|
982
|
+
}
|
|
983
|
+
/** Runs an invocation (method or listener) that keeps the record alive until it settles. */
|
|
984
|
+
#invoke(fn) {
|
|
985
|
+
const invocation = (async () => fn())();
|
|
986
|
+
this.#invocations.add(invocation);
|
|
987
|
+
const done = () => {
|
|
988
|
+
this.#invocations.delete(invocation);
|
|
989
|
+
if (this.#invocations.size === 0)
|
|
990
|
+
this.#maybeIdle();
|
|
991
|
+
};
|
|
992
|
+
invocation.then(done, done);
|
|
993
|
+
return invocation;
|
|
994
|
+
}
|
|
995
|
+
rootEnded(rootId) {
|
|
996
|
+
this.leases.delete(rootId);
|
|
997
|
+
const gate = this.gate;
|
|
998
|
+
if (gate?.grandfathered.delete(rootId) && gate.grandfathered.size === 0)
|
|
999
|
+
gate.drained();
|
|
1000
|
+
}
|
|
1001
|
+
// ─── lifetime ───
|
|
1002
|
+
addHold(handle) {
|
|
1003
|
+
this.holds.add(handle);
|
|
1004
|
+
if (this.#linger !== undefined)
|
|
1005
|
+
clearTimeout(this.#linger);
|
|
1006
|
+
this.#linger = undefined;
|
|
1007
|
+
}
|
|
1008
|
+
release(handle) {
|
|
1009
|
+
if (this.holds.delete(handle))
|
|
1010
|
+
this.#maybeIdle();
|
|
1011
|
+
}
|
|
1012
|
+
/** No holds and no invocations: dispose (after `linger` for a live record). */
|
|
1013
|
+
#maybeIdle() {
|
|
1014
|
+
if (this.holds.size > 0 || this.#invocations.size > 0 || this.#linger !== undefined)
|
|
1015
|
+
return;
|
|
1016
|
+
if (this.status === "activating") {
|
|
1017
|
+
this.#background(this.dispose(false));
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (this.status !== "live")
|
|
1021
|
+
return;
|
|
1022
|
+
const linger = this.service?.options.linger ?? 0;
|
|
1023
|
+
if (linger <= 0) {
|
|
1024
|
+
this.#background(this.dispose(false));
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
this.#linger = setTimeout(() => {
|
|
1028
|
+
this.#linger = undefined;
|
|
1029
|
+
if (this.holds.size === 0 && this.#invocations.size === 0 && this.status === "live") {
|
|
1030
|
+
this.#background(this.dispose(false));
|
|
1031
|
+
}
|
|
1032
|
+
}, linger);
|
|
1033
|
+
this.#linger.unref?.();
|
|
1034
|
+
}
|
|
1035
|
+
#background(disposal) {
|
|
1036
|
+
disposal.catch((error) => this.host.report(error));
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Disposes the record. Forced: handles become gone at once (new calls are rejected) and running invocations are
|
|
1040
|
+
* cancelled. Either way, resources close only after every invocation settled.
|
|
1041
|
+
*/
|
|
1042
|
+
dispose(forced, reason) {
|
|
1043
|
+
this.#disposal ??= this.#dispose(forced, reason);
|
|
1044
|
+
return this.#disposal;
|
|
1045
|
+
}
|
|
1046
|
+
async #dispose(forced, reason) {
|
|
1047
|
+
const wasActivating = this.status === "activating";
|
|
1048
|
+
this.status = "disposing";
|
|
1049
|
+
this.host.forget(this);
|
|
1050
|
+
if (this.#linger !== undefined)
|
|
1051
|
+
clearTimeout(this.#linger);
|
|
1052
|
+
this.#linger = undefined;
|
|
1053
|
+
const error = reason ?? new ChordError("gone", `${this.address} was disposed`);
|
|
1054
|
+
if (forced) {
|
|
1055
|
+
for (const handle of this.holds)
|
|
1056
|
+
handle.gone(error);
|
|
1057
|
+
this.callAbort.abort(error);
|
|
1058
|
+
}
|
|
1059
|
+
this.activationAbort.abort(error);
|
|
1060
|
+
if (wasActivating)
|
|
1061
|
+
await this.ready.catch(noop);
|
|
1062
|
+
for (const handle of this.holds)
|
|
1063
|
+
handle.gone(error);
|
|
1064
|
+
const decls = this.allDecls();
|
|
1065
|
+
this.stopTasks();
|
|
1066
|
+
this.stopListeners();
|
|
1067
|
+
await Promise.allSettled([...this.#invocations]);
|
|
1068
|
+
const registered = [...decls].some((set) => set.registrations.size > 0) && !this.deferredRuntime;
|
|
1069
|
+
this.behaviors.clear();
|
|
1070
|
+
this.decls = new Decls();
|
|
1071
|
+
if (registered)
|
|
1072
|
+
this.host.registriesChanged();
|
|
1073
|
+
const holds = new Set();
|
|
1074
|
+
for (const set of decls) {
|
|
1075
|
+
for (const derived of set.derives.values())
|
|
1076
|
+
derived.detach();
|
|
1077
|
+
for (const hold of set.holds.values())
|
|
1078
|
+
holds.add(hold);
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
await runAll([
|
|
1082
|
+
() => Promise.allSettled(this.#stopped.splice(0)),
|
|
1083
|
+
...[...this.openOrder].reverse().map((resource) => () => resource.close(this.openOrder)),
|
|
1084
|
+
...[...holds].map((hold) => () => hold.release()),
|
|
1085
|
+
], `${this.address}: disposal failed`);
|
|
1086
|
+
}
|
|
1087
|
+
finally {
|
|
1088
|
+
this.status = "disposed";
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const isReadableLike = (value) => value !== null &&
|
|
1093
|
+
typeof value === "object" &&
|
|
1094
|
+
typeof value.events === "function" &&
|
|
1095
|
+
typeof value.subscribe === "function";
|
|
1096
|
+
/** The remote state-member schemas a behavior exposes, by source. */
|
|
1097
|
+
export function memberSchemas(service, behavior) {
|
|
1098
|
+
const schemas = new Map();
|
|
1099
|
+
const contract = service.contract;
|
|
1100
|
+
if (contract.scope !== "remote")
|
|
1101
|
+
return schemas;
|
|
1102
|
+
const members = contract.description.variants[kindOf(behavior) ?? ""] ?? {};
|
|
1103
|
+
for (const [name, description] of Object.entries(members)) {
|
|
1104
|
+
const source = behavior[name];
|
|
1105
|
+
if (description.kind !== "state" || !isSource(source))
|
|
1106
|
+
continue;
|
|
1107
|
+
schemas.set(source, [...(schemas.get(source) ?? []), description.schema]);
|
|
1108
|
+
}
|
|
1109
|
+
return schemas;
|
|
1110
|
+
}
|
|
1111
|
+
function validateBehavior(service, behavior) {
|
|
1112
|
+
const contract = service.contract;
|
|
1113
|
+
const fail = (message) => {
|
|
1114
|
+
throw invalid("behavior", `${contract.name}: ${message}`);
|
|
1115
|
+
};
|
|
1116
|
+
if (behavior === null || typeof behavior !== "object")
|
|
1117
|
+
fail("factory must return an object");
|
|
1118
|
+
let members;
|
|
1119
|
+
if (contract.scope === "remote") {
|
|
1120
|
+
const kind = behavior.kind;
|
|
1121
|
+
if (typeof kind !== "string" || contract.description.variants[kind] === undefined)
|
|
1122
|
+
fail(`unknown variant ${String(kind)}`);
|
|
1123
|
+
members = contract.description.variants[kind];
|
|
1124
|
+
for (const key of Object.keys(behavior))
|
|
1125
|
+
if (key !== "kind" && members[key] === undefined)
|
|
1126
|
+
fail(`${key} is not in variant ${String(kind)}`);
|
|
1127
|
+
}
|
|
1128
|
+
else {
|
|
1129
|
+
members = contract.description.variants[LOCAL_VARIANT] ?? {};
|
|
1130
|
+
}
|
|
1131
|
+
for (const [name, description] of Object.entries(members)) {
|
|
1132
|
+
const value = behavior[name];
|
|
1133
|
+
if (description.kind === "method" && typeof value !== "function")
|
|
1134
|
+
fail(`${name} must be a method`);
|
|
1135
|
+
if (description.kind === "state" && !isSource(value) && !isReadableLike(value)) {
|
|
1136
|
+
fail(`${name} must be a cell, a derivation or a held object's state member`);
|
|
1137
|
+
}
|
|
1138
|
+
if (description.kind === "opaque" && value === undefined)
|
|
1139
|
+
fail(`${name} is missing`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
// ─── The factory's `self` ────────────────────────────────────────────────────
|
|
1143
|
+
class SelfImpl {
|
|
1144
|
+
#record;
|
|
1145
|
+
#previous;
|
|
1146
|
+
#decls = new Decls();
|
|
1147
|
+
#created = {
|
|
1148
|
+
holds: [],
|
|
1149
|
+
resources: [],
|
|
1150
|
+
derives: [],
|
|
1151
|
+
};
|
|
1152
|
+
constructor(record, previous) {
|
|
1153
|
+
this.#record = record;
|
|
1154
|
+
this.#previous = previous;
|
|
1155
|
+
}
|
|
1156
|
+
run(behavior) {
|
|
1157
|
+
return { behavior: behavior, decls: this.#decls, created: this.#created };
|
|
1158
|
+
}
|
|
1159
|
+
/** Iterates named declarations of one kind, rejecting repeated names and wrong descriptors. */
|
|
1160
|
+
*#entries(map, kind, entries, descriptor) {
|
|
1161
|
+
if (entries === null || typeof entries !== "object")
|
|
1162
|
+
throw new TypeError(`${kind} takes an object of named declarations`);
|
|
1163
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
1164
|
+
if (map.has(name))
|
|
1165
|
+
throw invalid("duplicate", `${this.#record.typeName}: duplicate ${kind} name ${name}`);
|
|
1166
|
+
if (descriptor !== undefined && declKind(value) !== descriptor) {
|
|
1167
|
+
throw new TypeError(`${kind}.${name} must be declared with ${descriptor}()`);
|
|
1168
|
+
}
|
|
1169
|
+
yield [name, value];
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
state(cells) {
|
|
1173
|
+
const result = {};
|
|
1174
|
+
const host = this.#record.host;
|
|
1175
|
+
for (const [name, decl] of this.#entries(this.#decls.cells, "state", cells, "cell")) {
|
|
1176
|
+
const cell = this.#previous?.cells.get(name) ??
|
|
1177
|
+
new Cell(name, decl.schema, decl.initial, this.#record.guard, {
|
|
1178
|
+
dev: host.dev,
|
|
1179
|
+
report: (error) => host.report(error),
|
|
1180
|
+
});
|
|
1181
|
+
this.#decls.cells.set(name, cell);
|
|
1182
|
+
this.#decls.cellSchemas.set(name, decl.schema);
|
|
1183
|
+
result[name] = cell;
|
|
1184
|
+
}
|
|
1185
|
+
return result;
|
|
1186
|
+
}
|
|
1187
|
+
get(type, input, options) {
|
|
1188
|
+
const host = this.#record.host;
|
|
1189
|
+
// A prepare rerun (recording mode) binds new holds against the reload's pending contracts: a breaking provider
|
|
1190
|
+
// change and its updated consumer reload together.
|
|
1191
|
+
const candidate = this.#previous !== undefined;
|
|
1192
|
+
const address = host.addressOf(type, input, options?.principal ?? host.principal, options?.at, candidate);
|
|
1193
|
+
const declared = this.#decls.holds.get(address);
|
|
1194
|
+
if (declared !== undefined) {
|
|
1195
|
+
if (declared.contract.fingerprint !== type.fingerprint) {
|
|
1196
|
+
throw invalid("duplicate", `${this.#record.typeName}: ${type.name} held twice with different contracts`);
|
|
1197
|
+
}
|
|
1198
|
+
return declared.proxy;
|
|
1199
|
+
}
|
|
1200
|
+
const existing = this.#previous?.holds.get(address);
|
|
1201
|
+
if (existing !== undefined &&
|
|
1202
|
+
existing.contract.fingerprint === type.fingerprint &&
|
|
1203
|
+
existing.status !== "gone" &&
|
|
1204
|
+
existing.status !== "released") {
|
|
1205
|
+
this.#decls.holds.set(address, existing);
|
|
1206
|
+
return existing.proxy;
|
|
1207
|
+
}
|
|
1208
|
+
const handle = host.acquire(type, input, options, "record", this.#record, candidate);
|
|
1209
|
+
this.#decls.holds.set(address, handle);
|
|
1210
|
+
this.#created.holds.push(handle);
|
|
1211
|
+
return handle.proxy;
|
|
1212
|
+
}
|
|
1213
|
+
resources(resources) {
|
|
1214
|
+
const result = {};
|
|
1215
|
+
for (const [name, { spec }] of this.#entries(this.#decls.resources, "resources", resources, "resource")) {
|
|
1216
|
+
const deps = (spec.deps ?? []).map((dep) => {
|
|
1217
|
+
const resolved = handleOf(dep) ?? resourceOf(dep);
|
|
1218
|
+
if (resolved === undefined) {
|
|
1219
|
+
throw new TypeError(`resource ${name}: deps must be handles or resources of this record`);
|
|
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);
|
|
1263
|
+
}
|
|
1264
|
+
this.#decls.derives.set(name, derived);
|
|
1265
|
+
this.#decls.deriveFns.set(name, fn);
|
|
1266
|
+
result[name] = derived;
|
|
1267
|
+
}
|
|
1268
|
+
return result;
|
|
1269
|
+
}
|
|
1270
|
+
expose(resource) {
|
|
1271
|
+
const resolved = resourceOf(resource);
|
|
1272
|
+
if (resolved === undefined || ![...this.#decls.resources.values()].includes(resolved)) {
|
|
1273
|
+
throw new TypeError("expose takes a resource declared by this record");
|
|
1274
|
+
}
|
|
1275
|
+
return new ExposedImpl(resolved);
|
|
1276
|
+
}
|
|
1277
|
+
register(registry, entries) {
|
|
1278
|
+
if (!this.#record.eager)
|
|
1279
|
+
throw invalid("not_eager", `${this.#record.typeName}: only eager records may register`);
|
|
1280
|
+
const prefixed = {};
|
|
1281
|
+
for (const [key, value] of Object.entries(entries))
|
|
1282
|
+
prefixed[`${registry.name}\u0000${key}`] = value;
|
|
1283
|
+
for (const [id, value] of this.#entries(this.#decls.registrations, "register", prefixed)) {
|
|
1284
|
+
this.#decls.registrations.set(id, {
|
|
1285
|
+
registry: registry,
|
|
1286
|
+
key: id.slice(registry.name.length + 1),
|
|
1287
|
+
value,
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
//# sourceMappingURL=record.js.map
|