@earendil-works/chord 1.2.0 → 1.4.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 +71 -0
- package/README.md +69 -14
- 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/context/index.d.ts +8 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +15 -0
- package/dist/context/index.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 +91 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +360 -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/sqlite/cloudflare.d.ts +52 -0
- package/dist/sqlite/cloudflare.d.ts.map +1 -0
- package/dist/sqlite/cloudflare.js +134 -0
- package/dist/sqlite/cloudflare.js.map +1 -0
- package/dist/sqlite/database.d.ts +37 -0
- package/dist/sqlite/database.d.ts.map +1 -0
- package/dist/sqlite/database.js +2 -0
- package/dist/sqlite/database.js.map +1 -0
- package/dist/sqlite/index.d.ts +4 -0
- package/dist/sqlite/index.d.ts.map +1 -0
- package/dist/sqlite/index.js +3 -0
- package/dist/sqlite/index.js.map +1 -0
- package/dist/sqlite/migrations.d.ts +10 -0
- package/dist/sqlite/migrations.d.ts.map +1 -0
- package/dist/sqlite/migrations.js +33 -0
- package/dist/sqlite/migrations.js.map +1 -0
- package/dist/sqlite/node.d.ts +38 -0
- package/dist/sqlite/node.d.ts.map +1 -0
- package/dist/sqlite/node.js +174 -0
- package/dist/sqlite/node.js.map +1 -0
- package/dist/sqlite/schema.d.ts +3 -0
- package/dist/sqlite/schema.d.ts.map +1 -0
- package/dist/sqlite/schema.js +10 -0
- package/dist/sqlite/schema.js.map +1 -0
- package/dist/telemetry-sqlite/index.d.ts +24 -0
- package/dist/telemetry-sqlite/index.d.ts.map +1 -0
- package/dist/telemetry-sqlite/index.js +273 -0
- package/dist/telemetry-sqlite/index.js.map +1 -0
- package/dist/telemetry-sqlite/store.d.ts +9 -0
- package/dist/telemetry-sqlite/store.d.ts.map +1 -0
- package/dist/telemetry-sqlite/store.js +110 -0
- package/dist/telemetry-sqlite/store.js.map +1 -0
- 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 +27 -1
- package/dist/record.d.ts +0 -320
- package/dist/record.d.ts.map +0 -1
- package/dist/record.js.map +0 -1
package/dist/connection.js
CHANGED
|
@@ -1,39 +1,54 @@
|
|
|
1
|
-
import { contractFromDescription, LOCAL_VARIANT } from "./contract.js";
|
|
1
|
+
import { contractFromDescription, LOCAL_VARIANT, resolveHost } from "./contract.js";
|
|
2
2
|
import { encoder } from "./delta/index.js";
|
|
3
3
|
import { ChordError, invalid } from "./errors.js";
|
|
4
|
-
import {
|
|
4
|
+
import { Packed, unpack } from "./link.js";
|
|
5
|
+
import { RefImpl, variantOf } from "./object.js";
|
|
5
6
|
import { assertValid } from "./schema.js";
|
|
6
|
-
import { fromWireError, isFrame, toWireError } from "./wire.js";
|
|
7
|
+
import { fromWireError, isFrame, PROTOCOL_VERSION, toWireError } from "./wire.js";
|
|
8
|
+
const DEFAULT_WINDOW = 256 * 1024;
|
|
9
|
+
/** The default cap of waiting state ops per link, in windows. */
|
|
10
|
+
const PENDING_WINDOWS = 4;
|
|
7
11
|
const unavailable = (message) => new ChordError("unavailable", message);
|
|
8
12
|
// ─── relay ───────────────────────────────────────────────────────────────────
|
|
9
13
|
/**
|
|
10
|
-
* A hold relayed from an origin link to a downstream link. It owns everything done through it:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
14
|
+
* A hold relayed from an origin link to a downstream link. It owns everything done through it: state and calls queued
|
|
15
|
+
* while the route resolves, and the downstream twins once forwarded. `close` undoes all of it exactly once (cancels
|
|
16
|
+
* calls downstream, drops what waits for the origin, releases the downstream hold, rejects waiters).
|
|
13
17
|
*/
|
|
14
|
-
export class
|
|
18
|
+
export class RelayRef {
|
|
15
19
|
origin;
|
|
16
20
|
frame;
|
|
17
21
|
principal;
|
|
18
|
-
client;
|
|
22
|
+
/** The client connection the hold comes from (forwarded downstream); undefined for a host's own holds. */
|
|
23
|
+
originTag;
|
|
19
24
|
/** The origin's authorization generation when the hold arrived; a revocation bumps it. */
|
|
20
25
|
generation;
|
|
21
26
|
down;
|
|
22
27
|
downId = 0;
|
|
23
28
|
closed = false;
|
|
29
|
+
/** The origin wants state (forwarded as `state: false`, `sub`, `unsub`). */
|
|
30
|
+
stateWanted;
|
|
31
|
+
/** The members of the first unit, still to forward past the origin's window. */
|
|
32
|
+
urgent;
|
|
33
|
+
#shaped = false;
|
|
34
|
+
/** Members whose ops this relay dropped: downstream ops are ignored until their reset arrives. */
|
|
35
|
+
awaiting = new Set();
|
|
24
36
|
calls = new Map();
|
|
25
|
-
subs = new Map();
|
|
26
37
|
#waiters = new Set();
|
|
27
|
-
constructor(origin, frame, principal,
|
|
38
|
+
constructor(origin, frame, principal, originTag) {
|
|
28
39
|
this.origin = origin;
|
|
29
40
|
this.frame = frame;
|
|
30
41
|
this.principal = principal;
|
|
31
|
-
this.
|
|
42
|
+
this.originTag = originTag;
|
|
43
|
+
this.stateWanted = frame.state !== false;
|
|
32
44
|
this.generation = origin.generation;
|
|
33
45
|
}
|
|
34
46
|
get originId() {
|
|
35
47
|
return this.frame.id;
|
|
36
48
|
}
|
|
49
|
+
get client() {
|
|
50
|
+
return this.originTag !== undefined;
|
|
51
|
+
}
|
|
37
52
|
/** Still authorized as it was when it arrived. */
|
|
38
53
|
get current() {
|
|
39
54
|
return !this.closed && this.origin.open && this.origin.generation === this.generation;
|
|
@@ -48,50 +63,77 @@ export class RelayHold {
|
|
|
48
63
|
this.#waiters.add({ resolve: resolve, reject });
|
|
49
64
|
});
|
|
50
65
|
}
|
|
51
|
-
/** Forwards the hold
|
|
66
|
+
/** Forwards the hold to `down`. */
|
|
52
67
|
open(down) {
|
|
53
68
|
if (this.closed)
|
|
54
69
|
return;
|
|
55
70
|
this.down = down;
|
|
56
71
|
this.downId = down.nextId();
|
|
57
|
-
down.
|
|
72
|
+
down.relayedRefs.set(this.downId, this);
|
|
73
|
+
const { at, token, input } = this.frame;
|
|
58
74
|
down.send({
|
|
59
|
-
|
|
75
|
+
t: "hold",
|
|
60
76
|
id: this.downId,
|
|
77
|
+
at,
|
|
78
|
+
token,
|
|
79
|
+
input,
|
|
61
80
|
principal: this.principal,
|
|
62
|
-
...(this.
|
|
81
|
+
...(this.originTag === undefined ? {} : { origin: this.originTag }),
|
|
82
|
+
...(this.stateWanted ? {} : { state: false }),
|
|
83
|
+
...(this.frame.again === true ? { again: true } : {}),
|
|
63
84
|
});
|
|
64
|
-
for (const [originSubId, entry] of this.subs)
|
|
65
|
-
if (entry.downId === undefined)
|
|
66
|
-
this.#forwardSub(originSubId, entry);
|
|
67
85
|
for (const waiter of this.#waiters)
|
|
68
86
|
waiter.resolve(down);
|
|
69
87
|
this.#waiters.clear();
|
|
70
88
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
/** The origin starts or stops the hold's state. */
|
|
90
|
+
wantState(wanted) {
|
|
91
|
+
if (this.closed || wanted === this.stateWanted)
|
|
92
|
+
return;
|
|
93
|
+
this.stateWanted = wanted;
|
|
94
|
+
if (!wanted) {
|
|
95
|
+
this.origin.dropHoldState(this.originId);
|
|
96
|
+
this.awaiting.clear();
|
|
97
|
+
}
|
|
98
|
+
this.down?.send({ t: wanted ? "sub" : "unsub", hold: this.downId });
|
|
77
99
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
/**
|
|
101
|
+
* The origin could not keep up and dropped what waited for member `m` (or asked for it again): ask downstream for a
|
|
102
|
+
* reset, and ignore its ops until the reset arrives.
|
|
103
|
+
*/
|
|
104
|
+
resync(m) {
|
|
105
|
+
if (this.closed)
|
|
106
|
+
return;
|
|
107
|
+
this.awaiting.add(m);
|
|
108
|
+
this.down?.send({ t: "resync", hold: this.downId, m });
|
|
83
109
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
110
|
+
/** A shape from downstream: forwarded at once; the members of the first unit then skip the origin's window. */
|
|
111
|
+
shape(frame) {
|
|
112
|
+
if (this.closed)
|
|
87
113
|
return;
|
|
88
|
-
this
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (forward)
|
|
93
|
-
this.down.send({ t: "unsub", id: entry.downId });
|
|
114
|
+
if (!this.#shaped) {
|
|
115
|
+
this.#shaped = true;
|
|
116
|
+
if (this.frame.again !== true)
|
|
117
|
+
this.urgent = new Set(frame.state);
|
|
94
118
|
}
|
|
119
|
+
this.origin.send({ ...frame, id: this.originId });
|
|
120
|
+
}
|
|
121
|
+
/** A state frame from downstream. */
|
|
122
|
+
state(frame) {
|
|
123
|
+
if (this.closed)
|
|
124
|
+
return;
|
|
125
|
+
const m = frame.m;
|
|
126
|
+
if (this.awaiting.has(m)) {
|
|
127
|
+
if (frame.as === "ops")
|
|
128
|
+
return;
|
|
129
|
+
this.awaiting.delete(m);
|
|
130
|
+
}
|
|
131
|
+
const out = { ...frame, hold: this.originId };
|
|
132
|
+
if (this.urgent?.delete(m) === true) {
|
|
133
|
+
this.origin.send(out);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.origin.sendState(out, { resync: () => this.resync(m) });
|
|
95
137
|
}
|
|
96
138
|
/**
|
|
97
139
|
* Closes the relay. `releaseDown`: the downstream hold is still there (origin released, disconnected, or was
|
|
@@ -103,12 +145,11 @@ export class RelayHold {
|
|
|
103
145
|
this.closed = true;
|
|
104
146
|
for (const call of [...this.calls.values()])
|
|
105
147
|
call.cancel(error, options.releaseDown);
|
|
106
|
-
|
|
107
|
-
this.unsubscribe(originSubId, options.releaseDown);
|
|
148
|
+
this.origin.dropHoldState(this.originId);
|
|
108
149
|
if (this.origin.served.get(this.originId)?.kind === "relay")
|
|
109
150
|
this.origin.served.delete(this.originId);
|
|
110
151
|
if (this.down !== undefined) {
|
|
111
|
-
this.down.
|
|
152
|
+
this.down.relayedRefs.delete(this.downId);
|
|
112
153
|
if (options.releaseDown)
|
|
113
154
|
this.down.send({ t: "release", id: this.downId });
|
|
114
155
|
}
|
|
@@ -155,44 +196,97 @@ export class RelayCall {
|
|
|
155
196
|
}
|
|
156
197
|
}
|
|
157
198
|
// ─── connection ──────────────────────────────────────────────────────────────
|
|
158
|
-
/**
|
|
199
|
+
/**
|
|
200
|
+
* One end of a link on a host: consumer of remote references, provider for the peer's holds, and relay.
|
|
201
|
+
*
|
|
202
|
+
* State
|
|
203
|
+
* -----
|
|
204
|
+
*
|
|
205
|
+
* A hold implies the state of its variant (unless it says `state: false`). The provider answers with a unit: the shape,
|
|
206
|
+
* then a snapshot of each state member the consumer does not have yet (`shape.state` lists them), then the members'
|
|
207
|
+
* ops and resets. A reload or a variant change sends another unit with the members that are new; members that left the
|
|
208
|
+
* variant stop. The consumer applies a shape once its snapshots are here (`RemoteBinding`).
|
|
209
|
+
*
|
|
210
|
+
* Flow control
|
|
211
|
+
* ------------
|
|
212
|
+
*
|
|
213
|
+
* Problem: state is pushed as fast as it changes, but a peer may read slowly (a phone behind a relay), and a relay may
|
|
214
|
+
* hide that: Cloudflare forwards to browsers without backpressure. Everything sent then queues in front of the next
|
|
215
|
+
* call result. Example: a browser opens three long transcripts (500 KiB of snapshots each) over a 5 Mbit/s path; a
|
|
216
|
+
* result sent next waits 2.4 s, and the snapshot of a transcript the user already left is still sent in full.
|
|
217
|
+
*
|
|
218
|
+
* Solution: the peer acknowledges what it processed (`ack`, a cumulative frame count), and the sender counts the sizes
|
|
219
|
+
* of the frames not yet acknowledged. While they exceed the window, state waits here in slots, flushed in creation
|
|
220
|
+
* order as acknowledgements come:
|
|
221
|
+
* - a member's stream has one slot: ops are appended (encoded batches of one stream may be concatenated, see
|
|
222
|
+
* `encoder`), a snapshot, reset or error replaces it;
|
|
223
|
+
* - a hold's unit has one slot, built lazily from the current values when it is sent, so a newer unit merges into it;
|
|
224
|
+
* while it waits, the members it introduces have no stream yet.
|
|
225
|
+
*
|
|
226
|
+
* The first unit of a hold that is not a re-hold (`again`) is sent at once, past the window: a `get` waits for it, and
|
|
227
|
+
* must not wait behind other holds' state. Results, shapes, gone, reload control and pings are sent at once too: they
|
|
228
|
+
* are bounded by the requests the peer makes. Hence the one ordering exception of a link: while state waits, a result
|
|
229
|
+
* can arrive before state changes made before it. Frames of one stream stay in order.
|
|
230
|
+
*
|
|
231
|
+
* Waiting ops are capped per link (`pendingCap`): beyond, the oldest streams are replaced by their current value (a
|
|
232
|
+
* reset built at flush), or, on a relay, by a reset asked of the provider (`resync`), so memory stays bounded however
|
|
233
|
+
* long the peer stalls. The window bounds what is unacknowledged when a frame is sent, so one frame (a large snapshot)
|
|
234
|
+
* may exceed it. Both hellos must announce `acks`; with an older peer nothing is acknowledged and nothing waits. Links
|
|
235
|
+
* that do not report sizes (IPC) never fill the window.
|
|
236
|
+
*/
|
|
159
237
|
export class Connection {
|
|
160
238
|
host;
|
|
161
239
|
link;
|
|
162
240
|
/** `upstream`: the link to this host's parent. `downstream`: a link this host accepted. */
|
|
163
241
|
role;
|
|
164
242
|
peer;
|
|
243
|
+
/** A client connection's opaque id: holds that come over it (directly or relayed) carry it as their `origin`. */
|
|
244
|
+
originId;
|
|
165
245
|
principal;
|
|
166
246
|
restamp;
|
|
167
247
|
/** Bumped when the stamped principal is revoked or re-stamped: relays from before are refused. */
|
|
168
248
|
generation = 0;
|
|
169
|
-
/** The
|
|
170
|
-
|
|
171
|
-
/** A downstream host peer is usable once it runs this host's current
|
|
249
|
+
/** The code version a downstream host peer runs (from its hello, then from commits). */
|
|
250
|
+
peerCodeVersion;
|
|
251
|
+
/** A downstream host peer is usable once it runs this host's current code version (see `HostImpl.peerHello`). */
|
|
172
252
|
synced;
|
|
173
|
-
/**
|
|
174
|
-
|
|
175
|
-
/** Roots of fenced
|
|
253
|
+
/** Code versions this child announced while behind this host (fenced): this host never ran them. */
|
|
254
|
+
fencedCodeVersions = new Set();
|
|
255
|
+
/** Roots of fenced code versions, by the code version of this host they were admitted in. */
|
|
176
256
|
#admitted = new Map();
|
|
177
257
|
#parentName;
|
|
178
258
|
#syncWaiters = new Set();
|
|
179
259
|
#nextId = 1;
|
|
180
260
|
#lastSeen = Date.now();
|
|
181
261
|
#pinger;
|
|
262
|
+
#maxCalls;
|
|
263
|
+
// flow control (see the class comment)
|
|
264
|
+
#window;
|
|
265
|
+
#pendingCap;
|
|
266
|
+
/** Whether the peer acknowledges frames; until its hello is here, frames are counted in case it does. */
|
|
267
|
+
#peerAcks;
|
|
268
|
+
#sent = 0;
|
|
269
|
+
#acked = 0;
|
|
270
|
+
/** Sizes of the frames sent after the last acknowledged one. */
|
|
271
|
+
#unacked = [];
|
|
272
|
+
#unackedBytes = 0;
|
|
273
|
+
#pending = new Map();
|
|
274
|
+
/** Bytes of ops waiting in `#pending`. */
|
|
275
|
+
#pendingBytes = 0;
|
|
276
|
+
#received = 0;
|
|
277
|
+
#ackTimer;
|
|
278
|
+
#onProtocolMismatch;
|
|
279
|
+
/** The peer speaks another protocol version: nothing it sends is processed any more. */
|
|
280
|
+
#mismatched = false;
|
|
182
281
|
// consumer side: what this host asked of the peer
|
|
183
282
|
remotes = new Map();
|
|
184
283
|
calls = new Map();
|
|
185
|
-
subs = new Map();
|
|
186
284
|
// provider side: what the peer asked of this host
|
|
187
285
|
served = new Map();
|
|
188
286
|
incoming = new Map();
|
|
189
|
-
feeds = new Map();
|
|
190
|
-
/** Origin side of relays: the peer's sub ids that are relayed. */
|
|
191
|
-
relaySubs = new Map();
|
|
192
287
|
// downstream side of relays: ids this host used on this link
|
|
193
|
-
|
|
288
|
+
relayedRefs = new Map();
|
|
194
289
|
relayedCalls = new Map();
|
|
195
|
-
relayedSubs = new Map();
|
|
196
290
|
/** Reload votes awaited from this (child) peer. */
|
|
197
291
|
votes = new Map();
|
|
198
292
|
constructor(host, link, config) {
|
|
@@ -200,14 +294,29 @@ export class Connection {
|
|
|
200
294
|
this.link = link;
|
|
201
295
|
this.role = config.role;
|
|
202
296
|
this.peer = config.peer;
|
|
297
|
+
this.originId = config.originId;
|
|
203
298
|
this.principal = config.principal;
|
|
204
299
|
this.restamp = config.restamp;
|
|
205
300
|
this.synced = !(this.role === "downstream" && this.peer.kind === "host");
|
|
301
|
+
this.#window = config.window ?? DEFAULT_WINDOW;
|
|
302
|
+
this.#pendingCap = config.pendingCap ?? this.#window * PENDING_WINDOWS;
|
|
303
|
+
this.#maxCalls = config.maxCalls ?? Number.POSITIVE_INFINITY;
|
|
304
|
+
this.#onProtocolMismatch = config.onProtocolMismatch;
|
|
206
305
|
link.onClose((reason) => this.#closed(reason));
|
|
207
306
|
link.receive((frame) => this.#receive(frame));
|
|
208
|
-
this.send({
|
|
307
|
+
this.send({
|
|
308
|
+
t: "hello",
|
|
309
|
+
protocol: PROTOCOL_VERSION,
|
|
310
|
+
name: host.name,
|
|
311
|
+
kind: host.kind,
|
|
312
|
+
codeVersion: host.current.id,
|
|
313
|
+
acks: true,
|
|
314
|
+
});
|
|
209
315
|
const interval = config.pingInterval ?? 15_000;
|
|
210
316
|
const timeout = config.pingTimeout ?? 45_000;
|
|
317
|
+
// A link that was already closed closed this connection above; it needs no keepalive.
|
|
318
|
+
if (link.closed)
|
|
319
|
+
return;
|
|
211
320
|
let n = 0;
|
|
212
321
|
this.#pinger = setInterval(() => {
|
|
213
322
|
if (Date.now() - this.#lastSeen > timeout)
|
|
@@ -225,12 +334,178 @@ export class Connection {
|
|
|
225
334
|
return this.role === "upstream" ? this.#parentName : this.peer.name;
|
|
226
335
|
}
|
|
227
336
|
send(frame) {
|
|
228
|
-
this.link.send(frame);
|
|
337
|
+
const size = this.link.send(frame) ?? 0;
|
|
338
|
+
if (this.#peerAcks === false)
|
|
339
|
+
return;
|
|
340
|
+
this.#sent++;
|
|
341
|
+
this.#unacked.push(size);
|
|
342
|
+
this.#unackedBytes += size;
|
|
343
|
+
}
|
|
344
|
+
/** Sends a state frame, or keeps it waiting (see the class comment). */
|
|
345
|
+
sendState(frame, overflow) {
|
|
346
|
+
const key = stateKey(frame.hold, frame.m);
|
|
347
|
+
let slot = this.#pending.get(key);
|
|
348
|
+
if (slot === undefined && !this.#full) {
|
|
349
|
+
this.send(frame);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (slot?.kind === "unit")
|
|
353
|
+
return;
|
|
354
|
+
if (slot === undefined) {
|
|
355
|
+
slot = {
|
|
356
|
+
kind: "state",
|
|
357
|
+
hold: frame.hold,
|
|
358
|
+
m: frame.m,
|
|
359
|
+
base: undefined,
|
|
360
|
+
ops: [],
|
|
361
|
+
size: 0,
|
|
362
|
+
lazy: false,
|
|
363
|
+
overflow,
|
|
364
|
+
};
|
|
365
|
+
this.#pending.set(key, slot);
|
|
366
|
+
}
|
|
367
|
+
if (frame.as !== "ops") {
|
|
368
|
+
this.#pendingBytes -= slot.size;
|
|
369
|
+
slot.base = frame;
|
|
370
|
+
slot.ops = [];
|
|
371
|
+
slot.size = 0;
|
|
372
|
+
slot.lazy = false;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
// A reset built at flush already covers these ops.
|
|
376
|
+
if (slot.lazy)
|
|
377
|
+
return;
|
|
378
|
+
const size = JSON.stringify(frame.ops).length;
|
|
379
|
+
if (this.#pendingBytes + size > this.#pendingCap)
|
|
380
|
+
this.#makeRoom(size, slot);
|
|
381
|
+
if (slot.lazy || this.#pending.get(key) !== slot)
|
|
382
|
+
return;
|
|
383
|
+
for (const op of frame.ops ?? [])
|
|
384
|
+
slot.ops.push(op);
|
|
385
|
+
slot.size += size;
|
|
386
|
+
this.#pendingBytes += size;
|
|
387
|
+
}
|
|
388
|
+
/** Replaces the oldest waiting streams by resets until `size` more bytes fit; `current` last, if needed. */
|
|
389
|
+
#makeRoom(size, current) {
|
|
390
|
+
for (const slot of [...this.#pending.values()]) {
|
|
391
|
+
if (this.#pendingBytes + size <= this.#pendingCap)
|
|
392
|
+
return;
|
|
393
|
+
if (slot.kind === "state" && slot !== current && slot.size > 0)
|
|
394
|
+
this.#drop(slot);
|
|
395
|
+
}
|
|
396
|
+
if (this.#pendingBytes + size > this.#pendingCap)
|
|
397
|
+
this.#drop(current);
|
|
398
|
+
}
|
|
399
|
+
/** Drops what waits for a stream: a lazy reset replaces it (provider), or the relay asks downstream for one. */
|
|
400
|
+
#drop(slot) {
|
|
401
|
+
this.#pendingBytes -= slot.size;
|
|
402
|
+
slot.base = undefined;
|
|
403
|
+
slot.ops = [];
|
|
404
|
+
slot.size = 0;
|
|
405
|
+
if ("resend" in slot.overflow) {
|
|
406
|
+
slot.lazy = true;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
this.#pending.delete(stateKey(slot.hold, slot.m));
|
|
410
|
+
slot.overflow.resync();
|
|
411
|
+
}
|
|
412
|
+
/** Sends member `m` of a served hold again as a reset, through the window (`resync`). */
|
|
413
|
+
#resetLater(hold, m, resend) {
|
|
414
|
+
const key = stateKey(hold, m);
|
|
415
|
+
let slot = this.#pending.get(key);
|
|
416
|
+
if (slot?.kind === "unit")
|
|
417
|
+
return;
|
|
418
|
+
if (slot === undefined) {
|
|
419
|
+
slot = { kind: "state", hold, m, base: undefined, ops: [], size: 0, lazy: true, overflow: { resend } };
|
|
420
|
+
this.#pending.set(key, slot);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
this.#pendingBytes -= slot.size;
|
|
424
|
+
slot.base = undefined;
|
|
425
|
+
slot.ops = [];
|
|
426
|
+
slot.size = 0;
|
|
427
|
+
slot.lazy = true;
|
|
428
|
+
}
|
|
429
|
+
this.#flush();
|
|
430
|
+
}
|
|
431
|
+
/** Queues a hold's lazy unit (a waiting one already covers this one), and sends what the window allows. */
|
|
432
|
+
#queueUnit(hold, flush) {
|
|
433
|
+
const key = unitKey(hold);
|
|
434
|
+
if (!this.#pending.has(key))
|
|
435
|
+
this.#pending.set(key, { kind: "unit", hold, flush });
|
|
436
|
+
this.#flush();
|
|
437
|
+
}
|
|
438
|
+
/** Forgets what waits for a stream that ended. */
|
|
439
|
+
dropState(hold, m) {
|
|
440
|
+
const slot = this.#pending.get(stateKey(hold, m));
|
|
441
|
+
if (slot?.kind !== "state")
|
|
442
|
+
return;
|
|
443
|
+
this.#pendingBytes -= slot.size;
|
|
444
|
+
this.#pending.delete(stateKey(hold, m));
|
|
445
|
+
}
|
|
446
|
+
/** Forgets everything that waits for a hold (released, closed, or its state stopped). */
|
|
447
|
+
dropHoldState(hold) {
|
|
448
|
+
for (const [key, slot] of this.#pending) {
|
|
449
|
+
if (slot.hold !== hold)
|
|
450
|
+
continue;
|
|
451
|
+
if (slot.kind === "state")
|
|
452
|
+
this.#pendingBytes -= slot.size;
|
|
453
|
+
this.#pending.delete(key);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/** What waits for the peer: slots and bytes of ops (for tests and diagnostics). */
|
|
457
|
+
get waiting() {
|
|
458
|
+
return { slots: this.#pending.size, bytes: this.#pendingBytes };
|
|
459
|
+
}
|
|
460
|
+
get #full() {
|
|
461
|
+
return this.#peerAcks === true && this.#unackedBytes >= this.#window;
|
|
462
|
+
}
|
|
463
|
+
#onAck(n) {
|
|
464
|
+
if (this.#peerAcks !== true || n < this.#acked || n > this.#sent) {
|
|
465
|
+
this.#violation(`ack ${n} outside ${this.#acked}..${this.#sent}`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
for (const size of this.#unacked.splice(0, n - this.#acked))
|
|
469
|
+
this.#unackedBytes -= size;
|
|
470
|
+
this.#acked = n;
|
|
471
|
+
this.#flush();
|
|
472
|
+
}
|
|
473
|
+
/** Sends waiting slots in creation order while the window allows. */
|
|
474
|
+
#flush() {
|
|
475
|
+
for (const [key, slot] of this.#pending) {
|
|
476
|
+
if (this.#full)
|
|
477
|
+
return;
|
|
478
|
+
this.#pending.delete(key);
|
|
479
|
+
if (slot.kind === "unit") {
|
|
480
|
+
slot.flush();
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
this.#pendingBytes -= slot.size;
|
|
484
|
+
if (slot.lazy) {
|
|
485
|
+
if ("resend" in slot.overflow)
|
|
486
|
+
slot.overflow.resend();
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (slot.base !== undefined)
|
|
490
|
+
this.send(slot.base);
|
|
491
|
+
if (slot.ops.length > 0)
|
|
492
|
+
this.send({ t: "state", hold: slot.hold, m: slot.m, as: "ops", ops: slot.ops });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
/** Acknowledges once the frames that arrived together are processed: one ack per burst, not per frame. */
|
|
496
|
+
#scheduleAck() {
|
|
497
|
+
if (this.#peerAcks !== true || this.#ackTimer !== undefined || !this.open)
|
|
498
|
+
return;
|
|
499
|
+
this.#ackTimer = setTimeout(() => {
|
|
500
|
+
this.#ackTimer = undefined;
|
|
501
|
+
this.link.send({ t: "ack", n: this.#received });
|
|
502
|
+
}, 0);
|
|
503
|
+
this.#ackTimer.unref?.();
|
|
229
504
|
}
|
|
230
505
|
nextId() {
|
|
231
506
|
return this.#nextId++;
|
|
232
507
|
}
|
|
233
|
-
/** Resolves once the peer is usable (a host peer runs this host's
|
|
508
|
+
/** Resolves once the peer is usable (a host peer runs this host's code version); rejects if the link closes first. */
|
|
234
509
|
whenSynced() {
|
|
235
510
|
if (!this.open)
|
|
236
511
|
return Promise.reject(unavailable(`link to ${this.name ?? "peer"} closed`));
|
|
@@ -238,8 +513,8 @@ export class Connection {
|
|
|
238
513
|
return Promise.resolve();
|
|
239
514
|
return new Promise((resolve, reject) => this.#syncWaiters.add({ resolve: resolve, reject }));
|
|
240
515
|
}
|
|
241
|
-
markSynced(
|
|
242
|
-
this.
|
|
516
|
+
markSynced(codeVersion) {
|
|
517
|
+
this.peerCodeVersion = codeVersion;
|
|
243
518
|
this.synced = true;
|
|
244
519
|
for (const waiter of this.#syncWaiters)
|
|
245
520
|
waiter.resolve(undefined);
|
|
@@ -258,25 +533,43 @@ export class Connection {
|
|
|
258
533
|
else
|
|
259
534
|
this.principal = next;
|
|
260
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Stops talking to a peer of another protocol version: reports it and closes the link. The close reason tells the
|
|
538
|
+
* peer this side's version (`protocol-version <n>`); a peer that noticed first told us the same way (`#closed`).
|
|
539
|
+
*/
|
|
540
|
+
#protocolMismatch(peer, close = true) {
|
|
541
|
+
if (this.#mismatched)
|
|
542
|
+
return;
|
|
543
|
+
this.#mismatched = true;
|
|
544
|
+
try {
|
|
545
|
+
this.#onProtocolMismatch?.({ local: PROTOCOL_VERSION, peer });
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
this.host.report(error);
|
|
549
|
+
}
|
|
550
|
+
if (close)
|
|
551
|
+
this.link.close(`protocol-version ${PROTOCOL_VERSION}`);
|
|
552
|
+
}
|
|
261
553
|
/** A protocol violation: the peer cannot be trusted with anything on this link any more. */
|
|
262
554
|
#violation(reason) {
|
|
263
555
|
this.host.report(new ChordError("invalid", `link to ${this.name ?? "peer"}: ${reason}`, { reason: "protocol" }));
|
|
264
556
|
this.link.close(`protocol error: ${reason}`);
|
|
265
557
|
}
|
|
266
558
|
// ─── consumer side (RemoteChannel) ───
|
|
267
|
-
hold(
|
|
559
|
+
hold(binding, again) {
|
|
268
560
|
const id = this.nextId();
|
|
269
|
-
this.remotes.set(id,
|
|
270
|
-
|
|
271
|
-
const principal =
|
|
561
|
+
this.remotes.set(id, binding);
|
|
562
|
+
binding.bound(this, id);
|
|
563
|
+
const principal = binding.principal;
|
|
272
564
|
this.send({
|
|
273
565
|
t: "hold",
|
|
274
566
|
id,
|
|
275
|
-
at:
|
|
276
|
-
token:
|
|
277
|
-
input:
|
|
567
|
+
at: binding.at,
|
|
568
|
+
token: binding.contract.description,
|
|
569
|
+
input: binding.input,
|
|
278
570
|
...(principal === undefined ? {} : { principal }),
|
|
279
|
-
...(
|
|
571
|
+
...(binding.stateOn ? {} : { state: false }),
|
|
572
|
+
...(again ? { again: true } : {}),
|
|
280
573
|
});
|
|
281
574
|
return id;
|
|
282
575
|
}
|
|
@@ -284,6 +577,10 @@ export class Connection {
|
|
|
284
577
|
this.remotes.delete(id);
|
|
285
578
|
this.send({ t: "release", id });
|
|
286
579
|
}
|
|
580
|
+
wantState(hold, wanted) {
|
|
581
|
+
if (this.remotes.has(hold))
|
|
582
|
+
this.send({ t: wanted ? "sub" : "unsub", hold });
|
|
583
|
+
}
|
|
287
584
|
call(hold, member, args, root, signal) {
|
|
288
585
|
const id = this.nextId();
|
|
289
586
|
return new Promise((resolve, reject) => {
|
|
@@ -308,36 +605,34 @@ export class Connection {
|
|
|
308
605
|
reject(error);
|
|
309
606
|
},
|
|
310
607
|
});
|
|
311
|
-
// Clients never carry roots or
|
|
608
|
+
// Clients never carry roots or code versions: the server starts roots for them.
|
|
312
609
|
const carried = root !== undefined && this.host.kind === "host";
|
|
313
610
|
if (carried)
|
|
314
611
|
this.host.rootSent(root.rootId, this);
|
|
315
612
|
this.send({ t: "call", id, hold, member, args, ...(carried ? { root } : {}) });
|
|
316
613
|
});
|
|
317
614
|
}
|
|
318
|
-
sub(hold, member, sink) {
|
|
319
|
-
const id = this.nextId();
|
|
320
|
-
this.subs.set(id, sink);
|
|
321
|
-
this.send({ t: "sub", id, hold, member });
|
|
322
|
-
return id;
|
|
323
|
-
}
|
|
324
|
-
unsub(id) {
|
|
325
|
-
if (this.subs.delete(id))
|
|
326
|
-
this.send({ t: "unsub", id });
|
|
327
|
-
}
|
|
328
615
|
// ─── receiving ───
|
|
329
616
|
#receive(frame) {
|
|
617
|
+
if (this.#mismatched)
|
|
618
|
+
return;
|
|
330
619
|
this.#lastSeen = Date.now();
|
|
331
620
|
if (!isFrame(frame)) {
|
|
332
621
|
this.#violation("malformed frame");
|
|
333
622
|
return;
|
|
334
623
|
}
|
|
624
|
+
if (frame.t === "ack") {
|
|
625
|
+
this.#onAck(frame.n);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
this.#received++;
|
|
335
629
|
try {
|
|
336
630
|
this.#dispatch(frame);
|
|
337
631
|
}
|
|
338
632
|
catch (error) {
|
|
339
633
|
this.host.report(error);
|
|
340
634
|
}
|
|
635
|
+
this.#scheduleAck();
|
|
341
636
|
}
|
|
342
637
|
#dispatch(frame) {
|
|
343
638
|
switch (frame.t) {
|
|
@@ -349,6 +644,7 @@ export class Connection {
|
|
|
349
644
|
return;
|
|
350
645
|
case "pong":
|
|
351
646
|
case "bye":
|
|
647
|
+
case "ack":
|
|
352
648
|
return;
|
|
353
649
|
case "hold":
|
|
354
650
|
this.#onHold(frame);
|
|
@@ -363,10 +659,11 @@ export class Connection {
|
|
|
363
659
|
this.incoming.get(frame.id)?.abort(new ChordError("aborted", "cancelled by caller"));
|
|
364
660
|
return;
|
|
365
661
|
case "sub":
|
|
366
|
-
this.#onSub(frame);
|
|
367
|
-
return;
|
|
368
662
|
case "unsub":
|
|
369
|
-
this.#
|
|
663
|
+
this.#onWantState(frame.hold, frame.t === "sub");
|
|
664
|
+
return;
|
|
665
|
+
case "resync":
|
|
666
|
+
this.#onResync(frame.hold, frame.m);
|
|
370
667
|
return;
|
|
371
668
|
case "shape":
|
|
372
669
|
case "gone":
|
|
@@ -397,7 +694,7 @@ export class Connection {
|
|
|
397
694
|
this.host.invalidatePrincipal(frame.pattern).catch((error) => this.host.report(error));
|
|
398
695
|
}
|
|
399
696
|
else if (frame.t === "retire")
|
|
400
|
-
this.host.allowRetire(frame.
|
|
697
|
+
this.host.allowRetire(frame.codeVersion);
|
|
401
698
|
else
|
|
402
699
|
this.host.onReloadFrame(this, frame);
|
|
403
700
|
return;
|
|
@@ -417,10 +714,21 @@ export class Connection {
|
|
|
417
714
|
}
|
|
418
715
|
}
|
|
419
716
|
/**
|
|
420
|
-
* The peer's hello must agree with what this side decided about it. A downstream host announces its
|
|
421
|
-
* host either finds it in step or fences and reconciles it (never renumbers).
|
|
717
|
+
* The peer's hello must agree with what this side decided about it. A downstream host announces its code version:
|
|
718
|
+
* the host either finds it in step or fences and reconciles it (never renumbers).
|
|
422
719
|
*/
|
|
423
720
|
#onHello(frame) {
|
|
721
|
+
if ((frame.protocol ?? 1) !== PROTOCOL_VERSION) {
|
|
722
|
+
this.#protocolMismatch(frame.protocol ?? 1);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (this.#peerAcks === undefined) {
|
|
726
|
+
this.#peerAcks = frame.acks === true;
|
|
727
|
+
if (!this.#peerAcks) {
|
|
728
|
+
this.#unacked = [];
|
|
729
|
+
this.#unackedBytes = 0;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
424
732
|
if (this.role === "upstream") {
|
|
425
733
|
if (frame.kind !== "host") {
|
|
426
734
|
this.#violation("parent is not a host");
|
|
@@ -434,19 +742,23 @@ export class Connection {
|
|
|
434
742
|
return;
|
|
435
743
|
}
|
|
436
744
|
if (this.peer.kind === "host")
|
|
437
|
-
this.host.peerHello(this, frame.
|
|
745
|
+
this.host.peerHello(this, frame.codeVersion);
|
|
438
746
|
}
|
|
439
747
|
/**
|
|
440
748
|
* Principal stamping: a downstream link acts as the principal it was accepted with, whatever the frame says. Only
|
|
441
|
-
* the parent may forward a principal (relayed requests); its own requests act as
|
|
749
|
+
* the parent may forward a principal (relayed requests); its own requests act as the principal given to `connect`.
|
|
442
750
|
*/
|
|
443
751
|
#principalOf(frame) {
|
|
444
752
|
if (this.role === "downstream")
|
|
445
753
|
return this.principal;
|
|
446
|
-
return frame.principal ??
|
|
754
|
+
return frame.principal ?? this.principal;
|
|
447
755
|
}
|
|
448
|
-
|
|
449
|
-
|
|
756
|
+
/**
|
|
757
|
+
* The client connection a hold comes from: this link's own id for a client link (whatever the frame says), the id
|
|
758
|
+
* the parent forwarded for a relayed hold, none for a host's own holds.
|
|
759
|
+
*/
|
|
760
|
+
#originOf(frame) {
|
|
761
|
+
return this.role === "downstream" ? this.originId : frame.origin;
|
|
450
762
|
}
|
|
451
763
|
#onHold(frame) {
|
|
452
764
|
if (this.served.has(frame.id)) {
|
|
@@ -454,55 +766,214 @@ export class Connection {
|
|
|
454
766
|
return;
|
|
455
767
|
}
|
|
456
768
|
const principal = this.#principalOf(frame);
|
|
457
|
-
const
|
|
769
|
+
const origin = this.#originOf(frame);
|
|
770
|
+
const client = origin !== undefined;
|
|
458
771
|
if (frame.at !== this.host.name) {
|
|
459
772
|
if (this.role === "upstream") {
|
|
460
773
|
this.send({ t: "gone", id: frame.id, error: toWireError(unavailable(`${this.host.name} does not relay`)) });
|
|
461
774
|
return;
|
|
462
775
|
}
|
|
463
|
-
const relay = new
|
|
776
|
+
const relay = new RelayRef(this, frame, principal, origin);
|
|
464
777
|
this.served.set(frame.id, { kind: "relay", relay });
|
|
465
778
|
this.host.relay(relay).catch((error) => relay.close(error, { releaseDown: true, notify: true }));
|
|
466
779
|
return;
|
|
467
780
|
}
|
|
468
|
-
let
|
|
781
|
+
let ref;
|
|
469
782
|
try {
|
|
470
783
|
const token = contractFromDescription(frame.token);
|
|
471
784
|
if (token.scope !== "remote")
|
|
472
785
|
throw invalid("token", `${token.name} is local; only remote types cross links`);
|
|
786
|
+
// Addressed here, so the input must place it here (also by this host's contract, checked when acquiring).
|
|
787
|
+
if (resolveHost(token.name, token.description.host, frame.input) !== this.host.name)
|
|
788
|
+
throw invalid("placement", `${token.name} with this input does not live on ${this.host.name}`);
|
|
473
789
|
const acquired = this.host.acquire(token, frame.input, { principal }, client ? "client" : "connection");
|
|
474
|
-
if (!(acquired instanceof
|
|
475
|
-
throw unavailable("not a local
|
|
476
|
-
|
|
790
|
+
if (!(acquired instanceof RefImpl))
|
|
791
|
+
throw unavailable("not a local reference");
|
|
792
|
+
ref = acquired;
|
|
477
793
|
}
|
|
478
794
|
catch (error) {
|
|
479
795
|
this.send({ t: "gone", id: frame.id, error: toWireError(client ? clientError(error) : error) });
|
|
480
796
|
return;
|
|
481
797
|
}
|
|
482
|
-
const served = {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
members: current.members,
|
|
493
|
-
contract: current.contract,
|
|
494
|
-
epoch: current.epoch,
|
|
495
|
-
});
|
|
798
|
+
const served = {
|
|
799
|
+
kind: "local",
|
|
800
|
+
id: frame.id,
|
|
801
|
+
ref,
|
|
802
|
+
principal,
|
|
803
|
+
origin,
|
|
804
|
+
wantsState: frame.state !== false,
|
|
805
|
+
again: frame.again === true,
|
|
806
|
+
unitSent: false,
|
|
807
|
+
feeds: new Map(),
|
|
496
808
|
};
|
|
497
|
-
|
|
498
|
-
|
|
809
|
+
this.served.set(frame.id, served);
|
|
810
|
+
ref.onShape(() => this.#unit(served));
|
|
811
|
+
ref.goneListeners.add((error) => {
|
|
499
812
|
if (this.served.get(frame.id) !== served)
|
|
500
813
|
return;
|
|
501
814
|
this.#dropServed(frame.id, false);
|
|
502
815
|
this.send({ t: "gone", id: frame.id, error: toWireError(error) });
|
|
503
816
|
});
|
|
504
|
-
if (
|
|
505
|
-
|
|
817
|
+
if (ref.status === "live")
|
|
818
|
+
this.#unit(served);
|
|
819
|
+
}
|
|
820
|
+
/** The state members of `shape` the consumer's contract knows. */
|
|
821
|
+
#stateMembers(ref, shape) {
|
|
822
|
+
return shape.members.filter((name) => ref.describe(shape.variant, name)?.kind === "state");
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* The shape changed (live, reload, variant) or state was asked for: members that left lose their stream, and a unit
|
|
826
|
+
* goes out with the shape and the members the consumer lacks. The first unit of a hold (not a re-hold) is urgent.
|
|
827
|
+
*/
|
|
828
|
+
#unit(served) {
|
|
829
|
+
if (this.served.get(served.id) !== served)
|
|
830
|
+
return;
|
|
831
|
+
const shape = served.ref.shape;
|
|
832
|
+
if (shape === undefined)
|
|
833
|
+
return;
|
|
834
|
+
const members = new Set(served.wantsState ? this.#stateMembers(served.ref, shape) : []);
|
|
835
|
+
for (const [name, feed] of served.feeds) {
|
|
836
|
+
if (members.has(name))
|
|
837
|
+
continue;
|
|
838
|
+
feed.unsubscribe();
|
|
839
|
+
served.feeds.delete(name);
|
|
840
|
+
this.dropState(served.id, name);
|
|
841
|
+
}
|
|
842
|
+
const urgent = served.feeds.size === 0 && !(served.again && !served.unitSent);
|
|
843
|
+
if (urgent)
|
|
844
|
+
this.#sendUnit(served);
|
|
845
|
+
else
|
|
846
|
+
this.#queueUnit(served.id, () => this.#sendUnit(served));
|
|
847
|
+
}
|
|
848
|
+
/** Builds and sends a unit from the current shape and values. */
|
|
849
|
+
#sendUnit(served) {
|
|
850
|
+
this.#pending.delete(unitKey(served.id));
|
|
851
|
+
if (this.served.get(served.id) !== served)
|
|
852
|
+
return;
|
|
853
|
+
const shape = served.ref.shape;
|
|
854
|
+
if (shape === undefined)
|
|
855
|
+
return;
|
|
856
|
+
served.unitSent = true;
|
|
857
|
+
const members = served.wantsState ? this.#stateMembers(served.ref, shape) : [];
|
|
858
|
+
const listed = members.filter((name) => !served.feeds.has(name));
|
|
859
|
+
const snaps = [];
|
|
860
|
+
const attached = [];
|
|
861
|
+
for (const name of listed) {
|
|
862
|
+
const attachment = this.#attach(served, name);
|
|
863
|
+
if (attachment === undefined)
|
|
864
|
+
continue;
|
|
865
|
+
snaps.push(attachment.frame);
|
|
866
|
+
attached.push([name, attachment.feed]);
|
|
867
|
+
}
|
|
868
|
+
this.send({
|
|
869
|
+
t: "shape",
|
|
870
|
+
id: served.id,
|
|
871
|
+
...(shape.variant === undefined ? {} : { variant: shape.variant }),
|
|
872
|
+
members: shape.members,
|
|
873
|
+
contract: shape.contract,
|
|
874
|
+
state: snaps.map((frame) => frame.m),
|
|
875
|
+
});
|
|
876
|
+
for (const frame of snaps)
|
|
877
|
+
this.send(frame);
|
|
878
|
+
for (const [name, feed] of attached)
|
|
879
|
+
this.#replay(served, name, feed);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Subscribes member `name` for the consumer: its snapshot (as a frame of type `as`), and a feed whose later events
|
|
883
|
+
* wait in its buffer until the snapshot is sent (`#replay`). Undefined if the member is not readable state.
|
|
884
|
+
*/
|
|
885
|
+
#attach(served, name, as = "snap") {
|
|
886
|
+
const readable = served.ref.member(name);
|
|
887
|
+
if (readable === undefined || typeof readable.events !== "function")
|
|
888
|
+
return undefined;
|
|
889
|
+
const feed = { unsubscribe: () => { }, encoder: encoder(), buffer: [] };
|
|
890
|
+
let first;
|
|
891
|
+
feed.unsubscribe = readable.events((event) => {
|
|
892
|
+
if (first === undefined)
|
|
893
|
+
first = event;
|
|
894
|
+
else if (feed.buffer !== undefined)
|
|
895
|
+
feed.buffer.push(event);
|
|
896
|
+
else
|
|
897
|
+
this.#feedEvent(served, name, feed, event);
|
|
898
|
+
});
|
|
899
|
+
served.feeds.set(name, feed);
|
|
900
|
+
return { frame: this.#snapshot(served, name, as, first?.value), feed };
|
|
901
|
+
}
|
|
902
|
+
/** Sends the events that arrived while the snapshot was built, now that it is sent. */
|
|
903
|
+
#replay(served, name, feed) {
|
|
904
|
+
const buffered = feed.buffer ?? [];
|
|
905
|
+
feed.buffer = undefined;
|
|
906
|
+
for (const event of buffered)
|
|
907
|
+
this.#feedEvent(served, name, feed, event);
|
|
908
|
+
}
|
|
909
|
+
/** A member's state frame for a snapshot or reset: validated against the schema the consumer is promised. */
|
|
910
|
+
#snapshot(served, name, as, value) {
|
|
911
|
+
try {
|
|
912
|
+
validateSnapshot(served.ref, name, value);
|
|
913
|
+
}
|
|
914
|
+
catch (error) {
|
|
915
|
+
return { t: "state", hold: served.id, m: name, as: "error", error: toWireError(error) };
|
|
916
|
+
}
|
|
917
|
+
return { t: "state", hold: served.id, m: name, as, ...(value === undefined ? {} : { value }) };
|
|
918
|
+
}
|
|
919
|
+
/** A later event of a member's stream: ops are encoded, a reset starts a new stream. */
|
|
920
|
+
#feedEvent(served, name, feed, event) {
|
|
921
|
+
if (served.feeds.get(name) !== feed)
|
|
922
|
+
return;
|
|
923
|
+
const overflow = { resend: () => this.#resend(served, name) };
|
|
924
|
+
if (event.type === "ops") {
|
|
925
|
+
this.sendState({ t: "state", hold: served.id, m: name, as: "ops", ops: feed.encoder.encode(event.ops) }, overflow);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
feed.encoder = encoder();
|
|
929
|
+
this.sendState(this.#snapshot(served, name, "reset", event.value), overflow);
|
|
930
|
+
}
|
|
931
|
+
/** Sends member `name` again, now, as a reset from its current value (a lazy slot is flushed, or `resync`). */
|
|
932
|
+
#resend(served, name) {
|
|
933
|
+
const feed = served.feeds.get(name);
|
|
934
|
+
if (feed === undefined || this.served.get(served.id) !== served)
|
|
935
|
+
return;
|
|
936
|
+
feed.unsubscribe();
|
|
937
|
+
served.feeds.delete(name);
|
|
938
|
+
const attachment = this.#attach(served, name, "reset");
|
|
939
|
+
if (attachment === undefined)
|
|
940
|
+
return;
|
|
941
|
+
this.send(attachment.frame);
|
|
942
|
+
this.#replay(served, name, attachment.feed);
|
|
943
|
+
}
|
|
944
|
+
/** The consumer starts (`sub`) or stops (`unsub`) the state of a hold. */
|
|
945
|
+
#onWantState(hold, wanted) {
|
|
946
|
+
const served = this.served.get(hold);
|
|
947
|
+
if (served === undefined)
|
|
948
|
+
return;
|
|
949
|
+
if (served.kind === "relay") {
|
|
950
|
+
served.relay.wantState(wanted);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (served.wantsState === wanted)
|
|
954
|
+
return;
|
|
955
|
+
served.wantsState = wanted;
|
|
956
|
+
if (wanted) {
|
|
957
|
+
this.#unit(served);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
for (const feed of served.feeds.values())
|
|
961
|
+
feed.unsubscribe();
|
|
962
|
+
served.feeds.clear();
|
|
963
|
+
this.dropHoldState(hold);
|
|
964
|
+
}
|
|
965
|
+
/** The consumer (a relay that dropped ops) asks for member `m` again as a reset. */
|
|
966
|
+
#onResync(hold, m) {
|
|
967
|
+
const served = this.served.get(hold);
|
|
968
|
+
if (served === undefined)
|
|
969
|
+
return;
|
|
970
|
+
if (served.kind === "relay") {
|
|
971
|
+
served.relay.resync(m);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (!served.feeds.has(m))
|
|
975
|
+
return;
|
|
976
|
+
this.#resetLater(hold, m, () => this.#resend(served, m));
|
|
506
977
|
}
|
|
507
978
|
/** Forgets a served hold: unsubscribes its feeds and releases it (locally or downstream). */
|
|
508
979
|
#dropServed(id, release) {
|
|
@@ -517,16 +988,23 @@ export class Connection {
|
|
|
517
988
|
});
|
|
518
989
|
return;
|
|
519
990
|
}
|
|
520
|
-
for (const feed of served.feeds)
|
|
521
|
-
|
|
991
|
+
for (const feed of served.feeds.values())
|
|
992
|
+
feed.unsubscribe();
|
|
993
|
+
served.feeds.clear();
|
|
994
|
+
this.dropHoldState(id);
|
|
522
995
|
if (release)
|
|
523
|
-
served.
|
|
996
|
+
served.ref.release();
|
|
524
997
|
}
|
|
525
998
|
#onCall(frame) {
|
|
526
999
|
if (this.incoming.has(frame.id)) {
|
|
527
1000
|
this.#violation(`duplicate call id ${frame.id}`);
|
|
528
1001
|
return;
|
|
529
1002
|
}
|
|
1003
|
+
if (this.incoming.size >= this.#maxCalls) {
|
|
1004
|
+
const error = new ChordError("unavailable", `Too many calls in progress on this connection (${this.#maxCalls}); wait for earlier ones to finish`, { reason: "too_many_calls" });
|
|
1005
|
+
this.send({ t: "result", id: frame.id, error: toWireError(error) });
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
530
1008
|
const served = this.served.get(frame.hold);
|
|
531
1009
|
if (served === undefined) {
|
|
532
1010
|
this.send({ t: "result", id: frame.id, error: toWireError(new ChordError("gone", "unknown hold")) });
|
|
@@ -539,107 +1017,53 @@ export class Connection {
|
|
|
539
1017
|
const principal = served.kind === "local" ? served.principal : served.relay.principal;
|
|
540
1018
|
const options = { principal, signal: controller.signal };
|
|
541
1019
|
const run = served.kind === "local"
|
|
542
|
-
? (ctx) => served.
|
|
543
|
-
: (ctx) => relayCall(served.relay, frame, { rootId: ctx.rootId,
|
|
1020
|
+
? (ctx) => served.ref.object.call(served.ref, frame.member, unpack(frame.args), ctx)
|
|
1021
|
+
: (ctx) => relayCall(served.relay, frame, { rootId: ctx.rootId, codeVersion: ctx.codeVersion },
|
|
1022
|
+
// The root's signal: a cancel from the origin and the root's code version retiring both reach it.
|
|
1023
|
+
ctx.signal, this.host);
|
|
544
1024
|
const result = root === undefined
|
|
545
1025
|
? this.host.root(run, options)
|
|
546
|
-
: this.host.joinRoot(root.rootId, root.
|
|
1026
|
+
: this.host.joinRoot(root.rootId, root.codeVersion, run, { ...options, from: this });
|
|
547
1027
|
result
|
|
548
1028
|
.then((value) => this.send({ t: "result", id: frame.id, value }), (error) => this.send({ t: "result", id: frame.id, error: toWireError(error) }))
|
|
549
1029
|
.finally(() => this.incoming.delete(frame.id));
|
|
550
1030
|
}
|
|
551
1031
|
/**
|
|
552
|
-
* A child that connected behind this host runs roots of
|
|
553
|
-
* or was, reconciled). Such a root is admitted in this host's current
|
|
554
|
-
*
|
|
1032
|
+
* A child that connected behind this host runs roots of a code version this host never had (it was fenced and is
|
|
1033
|
+
* being, or was, reconciled). Such a root is admitted in this host's current code version at its first call here,
|
|
1034
|
+
* and keeps that code version for its later calls (`superseded` once this host retired it). Other unknown code
|
|
1035
|
+
* versions stay `superseded`.
|
|
555
1036
|
*/
|
|
556
1037
|
#admitRoot(root) {
|
|
557
|
-
if (root === undefined || this.role !== "downstream" || !this.
|
|
1038
|
+
if (root === undefined || this.role !== "downstream" || !this.fencedCodeVersions.has(root.codeVersion))
|
|
558
1039
|
return root;
|
|
559
|
-
if (this.host.
|
|
1040
|
+
if (this.host.hasCodeVersion(root.codeVersion))
|
|
560
1041
|
return root;
|
|
561
|
-
let
|
|
562
|
-
if (
|
|
563
|
-
|
|
564
|
-
this.#admitted.set(root.rootId,
|
|
1042
|
+
let codeVersion = this.#admitted.get(root.rootId);
|
|
1043
|
+
if (codeVersion === undefined) {
|
|
1044
|
+
codeVersion = this.host.current.id;
|
|
1045
|
+
this.#admitted.set(root.rootId, codeVersion);
|
|
565
1046
|
}
|
|
566
|
-
return { rootId: root.rootId,
|
|
567
|
-
}
|
|
568
|
-
#onSub(frame) {
|
|
569
|
-
if (this.feeds.has(frame.id) || this.relaySubs.has(frame.id)) {
|
|
570
|
-
this.#violation(`duplicate sub id ${frame.id}`);
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
const served = this.served.get(frame.hold);
|
|
574
|
-
if (served === undefined) {
|
|
575
|
-
this.send({ t: "state", id: frame.id, error: toWireError(new ChordError("gone", "unknown hold")) });
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
if (served.kind === "relay") {
|
|
579
|
-
served.relay.subscribe(frame.id, frame.member);
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
const readable = served.handle.member(frame.member);
|
|
583
|
-
if (readable === undefined || typeof readable.events !== "function") {
|
|
584
|
-
this.send({
|
|
585
|
-
t: "state",
|
|
586
|
-
id: frame.id,
|
|
587
|
-
error: toWireError(new ChordError("not_in_variant", `${frame.member} is not state`)),
|
|
588
|
-
});
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
let stream = encoder();
|
|
592
|
-
const send = (event) => {
|
|
593
|
-
if (event.type === "ops") {
|
|
594
|
-
this.send({ t: "state", id: frame.id, ops: stream.encode(event.ops) });
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
// A loading provider handle has nothing yet: the hydrated value follows as a reset.
|
|
598
|
-
if (event.value === undefined)
|
|
599
|
-
return;
|
|
600
|
-
// Snapshots are validated against the effective schema once per (re)subscription; ops only in dev mode.
|
|
601
|
-
try {
|
|
602
|
-
validateSnapshot(served.handle, frame.member, event.value);
|
|
603
|
-
}
|
|
604
|
-
catch (error) {
|
|
605
|
-
this.send({ t: "state", id: frame.id, error: toWireError(error) });
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
stream = encoder();
|
|
609
|
-
this.send(event.type === "snap"
|
|
610
|
-
? { t: "state", id: frame.id, snap: event.value }
|
|
611
|
-
: { t: "state", id: frame.id, reset: event.value });
|
|
612
|
-
};
|
|
613
|
-
const unsubscribe = readable.events(send);
|
|
614
|
-
this.feeds.set(frame.id, unsubscribe);
|
|
615
|
-
served.feeds.add(frame.id);
|
|
616
|
-
}
|
|
617
|
-
#onUnsub(id) {
|
|
618
|
-
const unsubscribe = this.feeds.get(id);
|
|
619
|
-
if (unsubscribe !== undefined) {
|
|
620
|
-
this.feeds.delete(id);
|
|
621
|
-
unsubscribe();
|
|
622
|
-
}
|
|
623
|
-
this.relaySubs.get(id)?.unsubscribe(id);
|
|
1047
|
+
return { rootId: root.rootId, codeVersion };
|
|
624
1048
|
}
|
|
625
1049
|
#onHoldReply(frame) {
|
|
626
|
-
const relay = this.
|
|
1050
|
+
const relay = this.relayedRefs.get(frame.id);
|
|
627
1051
|
if (relay !== undefined) {
|
|
628
1052
|
if (frame.t === "gone")
|
|
629
1053
|
relay.close(fromWireError(frame.error), { releaseDown: false, notify: true });
|
|
630
|
-
else
|
|
631
|
-
relay.
|
|
1054
|
+
else
|
|
1055
|
+
relay.shape(frame);
|
|
632
1056
|
return;
|
|
633
1057
|
}
|
|
634
|
-
const
|
|
635
|
-
if (
|
|
1058
|
+
const binding = this.remotes.get(frame.id);
|
|
1059
|
+
if (binding === undefined)
|
|
636
1060
|
return;
|
|
637
1061
|
if (frame.t === "gone") {
|
|
638
1062
|
this.remotes.delete(frame.id);
|
|
639
|
-
|
|
1063
|
+
binding.gone(fromWireError(frame.error));
|
|
640
1064
|
return;
|
|
641
1065
|
}
|
|
642
|
-
|
|
1066
|
+
binding.shaped({ variant: frame.variant, members: frame.members, contract: frame.contract }, frame.state);
|
|
643
1067
|
}
|
|
644
1068
|
#onResult(frame) {
|
|
645
1069
|
const relayed = this.relayedCalls.get(frame.id);
|
|
@@ -649,23 +1073,54 @@ export class Connection {
|
|
|
649
1073
|
}
|
|
650
1074
|
const pending = this.calls.get(frame.id);
|
|
651
1075
|
this.calls.delete(frame.id);
|
|
652
|
-
if (frame.error
|
|
653
|
-
pending?.resolve(frame.value);
|
|
654
|
-
else
|
|
1076
|
+
if (frame.error !== undefined) {
|
|
655
1077
|
pending?.reject(fromWireError(frame.error));
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
let value;
|
|
1081
|
+
try {
|
|
1082
|
+
value = unpack(frame.value);
|
|
1083
|
+
}
|
|
1084
|
+
catch (error) {
|
|
1085
|
+
pending?.reject(error);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
pending?.resolve(value);
|
|
656
1089
|
}
|
|
657
1090
|
#onState(frame) {
|
|
658
|
-
const
|
|
659
|
-
if (
|
|
660
|
-
|
|
1091
|
+
const relay = this.relayedRefs.get(frame.hold);
|
|
1092
|
+
if (relay !== undefined) {
|
|
1093
|
+
relay.state(frame);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
const binding = this.remotes.get(frame.hold);
|
|
1097
|
+
if (binding === undefined)
|
|
661
1098
|
return;
|
|
1099
|
+
if (!(frame.value instanceof Packed)) {
|
|
1100
|
+
binding.state(frame.m, frame);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
let value;
|
|
1104
|
+
try {
|
|
1105
|
+
value = unpack(frame.value);
|
|
662
1106
|
}
|
|
663
|
-
|
|
1107
|
+
catch (error) {
|
|
1108
|
+
binding.state(frame.m, { as: "error", error: toWireError(error) });
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
binding.state(frame.m, { ...frame, value });
|
|
664
1112
|
}
|
|
665
1113
|
// ─── lifetime ───
|
|
666
1114
|
#closed(reason) {
|
|
1115
|
+
const told = /^protocol-version (\d+)$/.exec(reason);
|
|
1116
|
+
if (told !== null && Number(told[1]) !== PROTOCOL_VERSION)
|
|
1117
|
+
this.#protocolMismatch(Number(told[1]), false);
|
|
667
1118
|
if (this.#pinger !== undefined)
|
|
668
1119
|
clearInterval(this.#pinger);
|
|
1120
|
+
if (this.#ackTimer !== undefined)
|
|
1121
|
+
clearTimeout(this.#ackTimer);
|
|
1122
|
+
this.#pending.clear();
|
|
1123
|
+
this.#pendingBytes = 0;
|
|
669
1124
|
const error = unavailable(`link to ${this.name ?? "peer"} closed: ${reason}`);
|
|
670
1125
|
// As origin: everything the peer held here, relayed or not, is released (downstream calls cancelled).
|
|
671
1126
|
for (const id of [...this.served.keys()])
|
|
@@ -676,7 +1131,6 @@ export class Connection {
|
|
|
676
1131
|
for (const pending of this.calls.values())
|
|
677
1132
|
pending.reject(error);
|
|
678
1133
|
this.calls.clear();
|
|
679
|
-
this.subs.clear();
|
|
680
1134
|
for (const vote of this.votes.values())
|
|
681
1135
|
vote.reject(error);
|
|
682
1136
|
this.votes.clear();
|
|
@@ -684,22 +1138,24 @@ export class Connection {
|
|
|
684
1138
|
waiter.reject(error);
|
|
685
1139
|
this.#syncWaiters.clear();
|
|
686
1140
|
// As downstream of relays: their origins lose the hold.
|
|
687
|
-
for (const relay of [...this.
|
|
1141
|
+
for (const relay of [...this.relayedRefs.values()])
|
|
688
1142
|
relay.close(error, { releaseDown: false, notify: true });
|
|
689
|
-
this.
|
|
1143
|
+
this.relayedRefs.clear();
|
|
690
1144
|
this.relayedCalls.clear();
|
|
691
|
-
this.
|
|
692
|
-
|
|
1145
|
+
const bindings = [...this.remotes.values()];
|
|
1146
|
+
this.remotes.clear();
|
|
1147
|
+
for (const binding of bindings) {
|
|
693
1148
|
// An upstream link may be replaced (reconnect); a downstream host that went away is gone.
|
|
694
1149
|
if (this.role === "upstream")
|
|
695
|
-
|
|
1150
|
+
binding.detached();
|
|
696
1151
|
else
|
|
697
|
-
|
|
1152
|
+
binding.gone(error);
|
|
698
1153
|
}
|
|
699
|
-
this.remotes.clear();
|
|
700
1154
|
this.host.connectionClosed(this);
|
|
701
1155
|
}
|
|
702
1156
|
}
|
|
1157
|
+
const stateKey = (hold, m) => `s${hold}\u0000${m}`;
|
|
1158
|
+
const unitKey = (hold) => `u${hold}`;
|
|
703
1159
|
/**
|
|
704
1160
|
* A call through a relay: waits for the route, then forwards with the root it runs in. It settles exactly once; a
|
|
705
1161
|
* cancel (or the origin going away) is forwarded downstream and removes both mappings.
|
|
@@ -709,6 +1165,7 @@ async function relayCall(relay, frame, root, signal, host) {
|
|
|
709
1165
|
if (signal.aborted)
|
|
710
1166
|
throw abortError();
|
|
711
1167
|
let onAbort = () => { };
|
|
1168
|
+
let onCancel = () => { };
|
|
712
1169
|
const aborted = new Promise((_, reject) => {
|
|
713
1170
|
onAbort = () => reject(abortError());
|
|
714
1171
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -720,32 +1177,34 @@ async function relayCall(relay, frame, root, signal, host) {
|
|
|
720
1177
|
throw new ChordError("gone", "relayed hold closed");
|
|
721
1178
|
return await new Promise((resolve, reject) => {
|
|
722
1179
|
const call = new RelayCall(relay, frame.id, down.nextId(), { resolve, reject });
|
|
723
|
-
|
|
1180
|
+
onCancel = () => call.cancel(abortError(), true);
|
|
1181
|
+
signal.addEventListener("abort", onCancel, { once: true });
|
|
724
1182
|
host.rootSent(root.rootId, down);
|
|
725
1183
|
down.send({ t: "call", id: call.downId, hold: relay.downId, member: frame.member, args: frame.args, root });
|
|
726
1184
|
});
|
|
727
1185
|
}
|
|
728
1186
|
finally {
|
|
729
1187
|
signal.removeEventListener("abort", onAbort);
|
|
1188
|
+
signal.removeEventListener("abort", onCancel);
|
|
730
1189
|
}
|
|
731
1190
|
}
|
|
732
1191
|
/**
|
|
733
1192
|
* Validates a snapshot against the schema the consumer is promised: the provider's own schema for a direct binding,
|
|
734
1193
|
* the consumer's contract for one through an `accepts` adapter (the adapter's view is what the consumer gets).
|
|
735
1194
|
*/
|
|
736
|
-
function validateSnapshot(
|
|
1195
|
+
function validateSnapshot(ref, member, value) {
|
|
737
1196
|
if (value === undefined)
|
|
738
1197
|
return;
|
|
739
|
-
const installed =
|
|
1198
|
+
const installed = ref.object.currentInstalled();
|
|
740
1199
|
if (installed === undefined)
|
|
741
1200
|
return;
|
|
742
|
-
const view =
|
|
743
|
-
const
|
|
744
|
-
const description = view !== undefined && view !== installed.
|
|
745
|
-
?
|
|
746
|
-
: installed.service.contract.description.variants[
|
|
1201
|
+
const view = ref.currentView();
|
|
1202
|
+
const variant = view === undefined ? variantOf(installed.implementation) : variantOf(view);
|
|
1203
|
+
const description = view !== undefined && view !== installed.implementation
|
|
1204
|
+
? ref.contract.description.variants[variant ?? LOCAL_VARIANT]?.[member]
|
|
1205
|
+
: installed.service.contract.description.variants[variantOf(installed.implementation) ?? LOCAL_VARIANT]?.[member];
|
|
747
1206
|
if (description?.kind === "state")
|
|
748
|
-
assertValid("state", `${
|
|
1207
|
+
assertValid("state", `${ref.address}.${member}`, description.schema, value);
|
|
749
1208
|
}
|
|
750
1209
|
/** Clients (browsers) holding an outdated contract are told to reload their code. */
|
|
751
1210
|
function clientError(error) {
|