@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
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
import { contractFromDescription, LOCAL_VARIANT } from "./contract.js";
|
|
2
|
+
import { encoder } from "./delta/index.js";
|
|
3
|
+
import { ChordError, invalid } from "./errors.js";
|
|
4
|
+
import { HandleImpl, kindOf } from "./record.js";
|
|
5
|
+
import { assertValid } from "./schema.js";
|
|
6
|
+
import { fromWireError, isFrame, toWireError } from "./wire.js";
|
|
7
|
+
const unavailable = (message) => new ChordError("unavailable", message);
|
|
8
|
+
// ─── relay ───────────────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* A hold relayed from an origin link to a downstream link. It owns everything done through it: subscriptions and
|
|
11
|
+
* calls queued while the route resolves, and the downstream twins once forwarded. `close` undoes all of it exactly
|
|
12
|
+
* once (cancels calls downstream, drops subscriptions, releases the downstream hold, rejects waiters).
|
|
13
|
+
*/
|
|
14
|
+
export class RelayHold {
|
|
15
|
+
origin;
|
|
16
|
+
frame;
|
|
17
|
+
principal;
|
|
18
|
+
client;
|
|
19
|
+
/** The origin's authorization generation when the hold arrived; a revocation bumps it. */
|
|
20
|
+
generation;
|
|
21
|
+
down;
|
|
22
|
+
downId = 0;
|
|
23
|
+
closed = false;
|
|
24
|
+
calls = new Map();
|
|
25
|
+
subs = new Map();
|
|
26
|
+
#waiters = new Set();
|
|
27
|
+
constructor(origin, frame, principal, client) {
|
|
28
|
+
this.origin = origin;
|
|
29
|
+
this.frame = frame;
|
|
30
|
+
this.principal = principal;
|
|
31
|
+
this.client = client;
|
|
32
|
+
this.generation = origin.generation;
|
|
33
|
+
}
|
|
34
|
+
get originId() {
|
|
35
|
+
return this.frame.id;
|
|
36
|
+
}
|
|
37
|
+
/** Still authorized as it was when it arrived. */
|
|
38
|
+
get current() {
|
|
39
|
+
return !this.closed && this.origin.open && this.origin.generation === this.generation;
|
|
40
|
+
}
|
|
41
|
+
/** The downstream connection once the route resolved; rejects if the relay closes first. */
|
|
42
|
+
ready() {
|
|
43
|
+
if (this.closed)
|
|
44
|
+
return Promise.reject(new ChordError("gone", "relayed hold closed"));
|
|
45
|
+
if (this.down !== undefined)
|
|
46
|
+
return Promise.resolve(this.down);
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
this.#waiters.add({ resolve: resolve, reject });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Forwards the hold (and what queued meanwhile) to `down`. */
|
|
52
|
+
open(down) {
|
|
53
|
+
if (this.closed)
|
|
54
|
+
return;
|
|
55
|
+
this.down = down;
|
|
56
|
+
this.downId = down.nextId();
|
|
57
|
+
down.relayedHolds.set(this.downId, this);
|
|
58
|
+
down.send({
|
|
59
|
+
...this.frame,
|
|
60
|
+
id: this.downId,
|
|
61
|
+
principal: this.principal,
|
|
62
|
+
...(this.client ? { client: true } : {}),
|
|
63
|
+
});
|
|
64
|
+
for (const [originSubId, entry] of this.subs)
|
|
65
|
+
if (entry.downId === undefined)
|
|
66
|
+
this.#forwardSub(originSubId, entry);
|
|
67
|
+
for (const waiter of this.#waiters)
|
|
68
|
+
waiter.resolve(down);
|
|
69
|
+
this.#waiters.clear();
|
|
70
|
+
}
|
|
71
|
+
subscribe(originSubId, member) {
|
|
72
|
+
const entry = { member, downId: undefined };
|
|
73
|
+
this.subs.set(originSubId, entry);
|
|
74
|
+
this.origin.relaySubs.set(originSubId, this);
|
|
75
|
+
if (this.down !== undefined)
|
|
76
|
+
this.#forwardSub(originSubId, entry);
|
|
77
|
+
}
|
|
78
|
+
#forwardSub(originSubId, entry) {
|
|
79
|
+
const down = this.down;
|
|
80
|
+
entry.downId = down.nextId();
|
|
81
|
+
down.relayedSubs.set(entry.downId, { relay: this, originSubId });
|
|
82
|
+
down.send({ t: "sub", id: entry.downId, hold: this.downId, member: entry.member });
|
|
83
|
+
}
|
|
84
|
+
unsubscribe(originSubId, forward = true) {
|
|
85
|
+
const entry = this.subs.get(originSubId);
|
|
86
|
+
if (entry === undefined)
|
|
87
|
+
return;
|
|
88
|
+
this.subs.delete(originSubId);
|
|
89
|
+
this.origin.relaySubs.delete(originSubId);
|
|
90
|
+
if (entry.downId !== undefined && this.down !== undefined) {
|
|
91
|
+
this.down.relayedSubs.delete(entry.downId);
|
|
92
|
+
if (forward)
|
|
93
|
+
this.down.send({ t: "unsub", id: entry.downId });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Closes the relay. `releaseDown`: the downstream hold is still there (origin released, disconnected, or was
|
|
98
|
+
* revoked): cancel its calls and release it. `notify`: tell the origin the hold is gone.
|
|
99
|
+
*/
|
|
100
|
+
close(error, options) {
|
|
101
|
+
if (this.closed)
|
|
102
|
+
return;
|
|
103
|
+
this.closed = true;
|
|
104
|
+
for (const call of [...this.calls.values()])
|
|
105
|
+
call.cancel(error, options.releaseDown);
|
|
106
|
+
for (const originSubId of [...this.subs.keys()])
|
|
107
|
+
this.unsubscribe(originSubId, options.releaseDown);
|
|
108
|
+
if (this.origin.served.get(this.originId)?.kind === "relay")
|
|
109
|
+
this.origin.served.delete(this.originId);
|
|
110
|
+
if (this.down !== undefined) {
|
|
111
|
+
this.down.relayedHolds.delete(this.downId);
|
|
112
|
+
if (options.releaseDown)
|
|
113
|
+
this.down.send({ t: "release", id: this.downId });
|
|
114
|
+
}
|
|
115
|
+
for (const waiter of this.#waiters)
|
|
116
|
+
waiter.reject(error);
|
|
117
|
+
this.#waiters.clear();
|
|
118
|
+
if (options.notify)
|
|
119
|
+
this.origin.send({ t: "gone", id: this.originId, error: toWireError(error) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** One relayed call: settles once, by result, cancellation or the relay closing; removes both mappings. */
|
|
123
|
+
export class RelayCall {
|
|
124
|
+
relay;
|
|
125
|
+
originId;
|
|
126
|
+
downId;
|
|
127
|
+
#pending;
|
|
128
|
+
#settled = false;
|
|
129
|
+
constructor(relay, originId, downId, pending) {
|
|
130
|
+
this.relay = relay;
|
|
131
|
+
this.originId = originId;
|
|
132
|
+
this.downId = downId;
|
|
133
|
+
this.#pending = pending;
|
|
134
|
+
relay.calls.set(originId, this);
|
|
135
|
+
relay.down?.relayedCalls.set(downId, this);
|
|
136
|
+
}
|
|
137
|
+
finish(error, value) {
|
|
138
|
+
if (this.#settled)
|
|
139
|
+
return;
|
|
140
|
+
this.#settled = true;
|
|
141
|
+
this.relay.calls.delete(this.originId);
|
|
142
|
+
this.relay.down?.relayedCalls.delete(this.downId);
|
|
143
|
+
if (error === undefined)
|
|
144
|
+
this.#pending.resolve(value);
|
|
145
|
+
else
|
|
146
|
+
this.#pending.reject(error);
|
|
147
|
+
}
|
|
148
|
+
/** Cancels downstream (when the downstream hold still exists) and settles with `error`. */
|
|
149
|
+
cancel(error, forward) {
|
|
150
|
+
if (this.#settled)
|
|
151
|
+
return;
|
|
152
|
+
if (forward)
|
|
153
|
+
this.relay.down?.send({ t: "cancel", id: this.downId });
|
|
154
|
+
this.finish(error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ─── connection ──────────────────────────────────────────────────────────────
|
|
158
|
+
/** One end of a link on a host: consumer of remote handles, provider for the peer's holds, and relay. */
|
|
159
|
+
export class Connection {
|
|
160
|
+
host;
|
|
161
|
+
link;
|
|
162
|
+
/** `upstream`: the link to this host's parent. `downstream`: a link this host accepted. */
|
|
163
|
+
role;
|
|
164
|
+
peer;
|
|
165
|
+
principal;
|
|
166
|
+
restamp;
|
|
167
|
+
/** Bumped when the stamped principal is revoked or re-stamped: relays from before are refused. */
|
|
168
|
+
generation = 0;
|
|
169
|
+
/** The epoch a downstream host peer runs (from its hello, then from commits). */
|
|
170
|
+
peerEpoch;
|
|
171
|
+
/** A downstream host peer is usable once it runs this host's current epoch (see `HostImpl.peerHello`). */
|
|
172
|
+
synced;
|
|
173
|
+
/** Epochs this child announced while behind this host (fenced): this host never ran them. */
|
|
174
|
+
fencedEpochs = new Set();
|
|
175
|
+
/** Roots of fenced epochs, by the epoch of this host they were admitted in. */
|
|
176
|
+
#admitted = new Map();
|
|
177
|
+
#parentName;
|
|
178
|
+
#syncWaiters = new Set();
|
|
179
|
+
#nextId = 1;
|
|
180
|
+
#lastSeen = Date.now();
|
|
181
|
+
#pinger;
|
|
182
|
+
// consumer side: what this host asked of the peer
|
|
183
|
+
remotes = new Map();
|
|
184
|
+
calls = new Map();
|
|
185
|
+
subs = new Map();
|
|
186
|
+
// provider side: what the peer asked of this host
|
|
187
|
+
served = new Map();
|
|
188
|
+
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
|
+
// downstream side of relays: ids this host used on this link
|
|
193
|
+
relayedHolds = new Map();
|
|
194
|
+
relayedCalls = new Map();
|
|
195
|
+
relayedSubs = new Map();
|
|
196
|
+
/** Reload votes awaited from this (child) peer. */
|
|
197
|
+
votes = new Map();
|
|
198
|
+
constructor(host, link, config) {
|
|
199
|
+
this.host = host;
|
|
200
|
+
this.link = link;
|
|
201
|
+
this.role = config.role;
|
|
202
|
+
this.peer = config.peer;
|
|
203
|
+
this.principal = config.principal;
|
|
204
|
+
this.restamp = config.restamp;
|
|
205
|
+
this.synced = !(this.role === "downstream" && this.peer.kind === "host");
|
|
206
|
+
link.onClose((reason) => this.#closed(reason));
|
|
207
|
+
link.receive((frame) => this.#receive(frame));
|
|
208
|
+
this.send({ t: "hello", name: host.name, kind: host.kind, epoch: host.current.id });
|
|
209
|
+
const interval = config.pingInterval ?? 15_000;
|
|
210
|
+
const timeout = config.pingTimeout ?? 45_000;
|
|
211
|
+
let n = 0;
|
|
212
|
+
this.#pinger = setInterval(() => {
|
|
213
|
+
if (Date.now() - this.#lastSeen > timeout)
|
|
214
|
+
link.close("ping timeout");
|
|
215
|
+
else
|
|
216
|
+
this.send({ t: "ping", n: ++n });
|
|
217
|
+
}, interval);
|
|
218
|
+
this.#pinger.unref?.();
|
|
219
|
+
}
|
|
220
|
+
get open() {
|
|
221
|
+
return !this.link.closed;
|
|
222
|
+
}
|
|
223
|
+
/** The peer's name: configured for downstream hosts, announced by the parent upstream. */
|
|
224
|
+
get name() {
|
|
225
|
+
return this.role === "upstream" ? this.#parentName : this.peer.name;
|
|
226
|
+
}
|
|
227
|
+
send(frame) {
|
|
228
|
+
this.link.send(frame);
|
|
229
|
+
}
|
|
230
|
+
nextId() {
|
|
231
|
+
return this.#nextId++;
|
|
232
|
+
}
|
|
233
|
+
/** Resolves once the peer is usable (a host peer runs this host's epoch); rejects if the link closes first. */
|
|
234
|
+
whenSynced() {
|
|
235
|
+
if (!this.open)
|
|
236
|
+
return Promise.reject(unavailable(`link to ${this.name ?? "peer"} closed`));
|
|
237
|
+
if (this.synced)
|
|
238
|
+
return Promise.resolve();
|
|
239
|
+
return new Promise((resolve, reject) => this.#syncWaiters.add({ resolve: resolve, reject }));
|
|
240
|
+
}
|
|
241
|
+
markSynced(epoch) {
|
|
242
|
+
this.peerEpoch = epoch;
|
|
243
|
+
this.synced = true;
|
|
244
|
+
for (const waiter of this.#syncWaiters)
|
|
245
|
+
waiter.resolve(undefined);
|
|
246
|
+
this.#syncWaiters.clear();
|
|
247
|
+
}
|
|
248
|
+
/** Revokes the stamped principal: relays made under it close at once (also those still resolving a route). */
|
|
249
|
+
revoke(next) {
|
|
250
|
+
this.generation++;
|
|
251
|
+
const error = new ChordError("gone", "principal invalidated");
|
|
252
|
+
for (const served of [...this.served.values()]) {
|
|
253
|
+
if (served.kind === "relay")
|
|
254
|
+
served.relay.close(error, { releaseDown: true, notify: true });
|
|
255
|
+
}
|
|
256
|
+
if (next === undefined)
|
|
257
|
+
this.link.close("principal invalidated");
|
|
258
|
+
else
|
|
259
|
+
this.principal = next;
|
|
260
|
+
}
|
|
261
|
+
/** A protocol violation: the peer cannot be trusted with anything on this link any more. */
|
|
262
|
+
#violation(reason) {
|
|
263
|
+
this.host.report(new ChordError("invalid", `link to ${this.name ?? "peer"}: ${reason}`, { reason: "protocol" }));
|
|
264
|
+
this.link.close(`protocol error: ${reason}`);
|
|
265
|
+
}
|
|
266
|
+
// ─── consumer side (RemoteChannel) ───
|
|
267
|
+
hold(handle) {
|
|
268
|
+
const id = this.nextId();
|
|
269
|
+
this.remotes.set(id, handle);
|
|
270
|
+
handle.bound(this, id);
|
|
271
|
+
const principal = handle.principal;
|
|
272
|
+
this.send({
|
|
273
|
+
t: "hold",
|
|
274
|
+
id,
|
|
275
|
+
at: handle.at,
|
|
276
|
+
token: handle.contract.description,
|
|
277
|
+
input: handle.input,
|
|
278
|
+
...(principal === undefined ? {} : { principal }),
|
|
279
|
+
...(handle.owner === "client" ? { client: true } : {}),
|
|
280
|
+
});
|
|
281
|
+
return id;
|
|
282
|
+
}
|
|
283
|
+
release(id) {
|
|
284
|
+
this.remotes.delete(id);
|
|
285
|
+
this.send({ t: "release", id });
|
|
286
|
+
}
|
|
287
|
+
call(hold, member, args, root, signal) {
|
|
288
|
+
const id = this.nextId();
|
|
289
|
+
return new Promise((resolve, reject) => {
|
|
290
|
+
if (signal.aborted) {
|
|
291
|
+
reject(new ChordError("aborted", "call aborted"));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const onAbort = () => {
|
|
295
|
+
this.calls.delete(id);
|
|
296
|
+
this.send({ t: "cancel", id });
|
|
297
|
+
reject(new ChordError("aborted", "call aborted", { cause: signal.reason }));
|
|
298
|
+
};
|
|
299
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
300
|
+
const done = () => signal.removeEventListener("abort", onAbort);
|
|
301
|
+
this.calls.set(id, {
|
|
302
|
+
resolve: (value) => {
|
|
303
|
+
done();
|
|
304
|
+
resolve(value);
|
|
305
|
+
},
|
|
306
|
+
reject: (error) => {
|
|
307
|
+
done();
|
|
308
|
+
reject(error);
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
// Clients never carry roots or epochs: the server starts roots for them.
|
|
312
|
+
const carried = root !== undefined && this.host.kind === "host";
|
|
313
|
+
if (carried)
|
|
314
|
+
this.host.rootSent(root.rootId, this);
|
|
315
|
+
this.send({ t: "call", id, hold, member, args, ...(carried ? { root } : {}) });
|
|
316
|
+
});
|
|
317
|
+
}
|
|
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
|
+
// ─── receiving ───
|
|
329
|
+
#receive(frame) {
|
|
330
|
+
this.#lastSeen = Date.now();
|
|
331
|
+
if (!isFrame(frame)) {
|
|
332
|
+
this.#violation("malformed frame");
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
this.#dispatch(frame);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
this.host.report(error);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
#dispatch(frame) {
|
|
343
|
+
switch (frame.t) {
|
|
344
|
+
case "hello":
|
|
345
|
+
this.#onHello(frame);
|
|
346
|
+
return;
|
|
347
|
+
case "ping":
|
|
348
|
+
this.send({ t: "pong", n: frame.n });
|
|
349
|
+
return;
|
|
350
|
+
case "pong":
|
|
351
|
+
case "bye":
|
|
352
|
+
return;
|
|
353
|
+
case "hold":
|
|
354
|
+
this.#onHold(frame);
|
|
355
|
+
return;
|
|
356
|
+
case "release":
|
|
357
|
+
this.#dropServed(frame.id, true);
|
|
358
|
+
return;
|
|
359
|
+
case "call":
|
|
360
|
+
this.#onCall(frame);
|
|
361
|
+
return;
|
|
362
|
+
case "cancel":
|
|
363
|
+
this.incoming.get(frame.id)?.abort(new ChordError("aborted", "cancelled by caller"));
|
|
364
|
+
return;
|
|
365
|
+
case "sub":
|
|
366
|
+
this.#onSub(frame);
|
|
367
|
+
return;
|
|
368
|
+
case "unsub":
|
|
369
|
+
this.#onUnsub(frame.id);
|
|
370
|
+
return;
|
|
371
|
+
case "shape":
|
|
372
|
+
case "gone":
|
|
373
|
+
this.#onHoldReply(frame);
|
|
374
|
+
return;
|
|
375
|
+
case "result":
|
|
376
|
+
this.#onResult(frame);
|
|
377
|
+
return;
|
|
378
|
+
case "state":
|
|
379
|
+
this.#onState(frame);
|
|
380
|
+
return;
|
|
381
|
+
case "root_end":
|
|
382
|
+
// Honored only from the link the root came over (see HostImpl.foreignRootEnded).
|
|
383
|
+
this.#admitted.delete(frame.rootId);
|
|
384
|
+
this.host.foreignRootEnded(frame.rootId, this);
|
|
385
|
+
return;
|
|
386
|
+
case "invalidate":
|
|
387
|
+
case "prepare":
|
|
388
|
+
case "commit":
|
|
389
|
+
case "abort":
|
|
390
|
+
case "retire":
|
|
391
|
+
// Control frames only from the parent.
|
|
392
|
+
if (this.role !== "upstream") {
|
|
393
|
+
this.#violation(`${frame.t} from a child`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (frame.t === "invalidate") {
|
|
397
|
+
this.host.invalidatePrincipal(frame.pattern).catch((error) => this.host.report(error));
|
|
398
|
+
}
|
|
399
|
+
else if (frame.t === "retire")
|
|
400
|
+
this.host.allowRetire(frame.epoch);
|
|
401
|
+
else
|
|
402
|
+
this.host.onReloadFrame(this, frame);
|
|
403
|
+
return;
|
|
404
|
+
case "vote": {
|
|
405
|
+
if (this.role !== "downstream" || this.peer.kind !== "host") {
|
|
406
|
+
this.#violation("vote from a non-participant");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const vote = this.votes.get(frame.reload);
|
|
410
|
+
this.votes.delete(frame.reload);
|
|
411
|
+
if (frame.error === undefined)
|
|
412
|
+
vote?.resolve(frame.report);
|
|
413
|
+
else
|
|
414
|
+
vote?.reject(fromWireError(frame.error));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* The peer's hello must agree with what this side decided about it. A downstream host announces its epoch: the
|
|
421
|
+
* host either finds it in step or fences and reconciles it (never renumbers).
|
|
422
|
+
*/
|
|
423
|
+
#onHello(frame) {
|
|
424
|
+
if (this.role === "upstream") {
|
|
425
|
+
if (frame.kind !== "host") {
|
|
426
|
+
this.#violation("parent is not a host");
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
this.#parentName ??= frame.name;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (frame.kind !== this.peer.kind || (this.peer.name !== undefined && frame.name !== this.peer.name)) {
|
|
433
|
+
this.#violation(`hello ${frame.kind} ${frame.name} does not match the accepted ${this.peer.kind} ${this.peer.name ?? ""}`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (this.peer.kind === "host")
|
|
437
|
+
this.host.peerHello(this, frame.epoch);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* 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 `{ host: parentName }`.
|
|
442
|
+
*/
|
|
443
|
+
#principalOf(frame) {
|
|
444
|
+
if (this.role === "downstream")
|
|
445
|
+
return this.principal;
|
|
446
|
+
return frame.principal ?? { host: this.#parentName ?? "parent" };
|
|
447
|
+
}
|
|
448
|
+
#isClient(frame) {
|
|
449
|
+
return this.role === "downstream" ? this.peer.kind === "client" : frame.client === true;
|
|
450
|
+
}
|
|
451
|
+
#onHold(frame) {
|
|
452
|
+
if (this.served.has(frame.id)) {
|
|
453
|
+
this.#violation(`duplicate hold id ${frame.id}`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const principal = this.#principalOf(frame);
|
|
457
|
+
const client = this.#isClient(frame);
|
|
458
|
+
if (frame.at !== this.host.name) {
|
|
459
|
+
if (this.role === "upstream") {
|
|
460
|
+
this.send({ t: "gone", id: frame.id, error: toWireError(unavailable(`${this.host.name} does not relay`)) });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const relay = new RelayHold(this, frame, principal, client);
|
|
464
|
+
this.served.set(frame.id, { kind: "relay", relay });
|
|
465
|
+
this.host.relay(relay).catch((error) => relay.close(error, { releaseDown: true, notify: true }));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
let handle;
|
|
469
|
+
try {
|
|
470
|
+
const token = contractFromDescription(frame.token);
|
|
471
|
+
if (token.scope !== "remote")
|
|
472
|
+
throw invalid("token", `${token.name} is local; only remote types cross links`);
|
|
473
|
+
const acquired = this.host.acquire(token, frame.input, { principal }, client ? "client" : "connection");
|
|
474
|
+
if (!(acquired instanceof HandleImpl))
|
|
475
|
+
throw unavailable("not a local handle");
|
|
476
|
+
handle = acquired;
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
this.send({ t: "gone", id: frame.id, error: toWireError(client ? clientError(error) : error) });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const served = { kind: "local", handle, principal, feeds: new Set() };
|
|
483
|
+
this.served.set(frame.id, served);
|
|
484
|
+
const shape = () => {
|
|
485
|
+
const current = handle.shape;
|
|
486
|
+
if (current === undefined || this.served.get(frame.id) !== served)
|
|
487
|
+
return;
|
|
488
|
+
this.send({
|
|
489
|
+
t: "shape",
|
|
490
|
+
id: frame.id,
|
|
491
|
+
kind: current.kind,
|
|
492
|
+
members: current.members,
|
|
493
|
+
contract: current.contract,
|
|
494
|
+
epoch: current.epoch,
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
handle.onShape(shape);
|
|
498
|
+
handle.goneListeners.add((error) => {
|
|
499
|
+
if (this.served.get(frame.id) !== served)
|
|
500
|
+
return;
|
|
501
|
+
this.#dropServed(frame.id, false);
|
|
502
|
+
this.send({ t: "gone", id: frame.id, error: toWireError(error) });
|
|
503
|
+
});
|
|
504
|
+
if (handle.status === "live")
|
|
505
|
+
shape();
|
|
506
|
+
}
|
|
507
|
+
/** Forgets a served hold: unsubscribes its feeds and releases it (locally or downstream). */
|
|
508
|
+
#dropServed(id, release) {
|
|
509
|
+
const served = this.served.get(id);
|
|
510
|
+
if (served === undefined)
|
|
511
|
+
return;
|
|
512
|
+
this.served.delete(id);
|
|
513
|
+
if (served.kind === "relay") {
|
|
514
|
+
served.relay.close(new ChordError("released", "relayed hold released"), {
|
|
515
|
+
releaseDown: release,
|
|
516
|
+
notify: false,
|
|
517
|
+
});
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
for (const feed of served.feeds)
|
|
521
|
+
this.#onUnsub(feed);
|
|
522
|
+
if (release)
|
|
523
|
+
served.handle.release();
|
|
524
|
+
}
|
|
525
|
+
#onCall(frame) {
|
|
526
|
+
if (this.incoming.has(frame.id)) {
|
|
527
|
+
this.#violation(`duplicate call id ${frame.id}`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const served = this.served.get(frame.hold);
|
|
531
|
+
if (served === undefined) {
|
|
532
|
+
this.send({ t: "result", id: frame.id, error: toWireError(new ChordError("gone", "unknown hold")) });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const controller = new AbortController();
|
|
536
|
+
this.incoming.set(frame.id, controller);
|
|
537
|
+
// Roots are honored from hosts only; a request without one (or from a client) starts one here.
|
|
538
|
+
const root = this.peer.kind === "host" ? this.#admitRoot(frame.root) : undefined;
|
|
539
|
+
const principal = served.kind === "local" ? served.principal : served.relay.principal;
|
|
540
|
+
const options = { principal, signal: controller.signal };
|
|
541
|
+
const run = served.kind === "local"
|
|
542
|
+
? (ctx) => served.handle.record.call(served.handle, frame.member, frame.args, ctx)
|
|
543
|
+
: (ctx) => relayCall(served.relay, frame, { rootId: ctx.rootId, epoch: ctx.epoch }, controller.signal, this.host);
|
|
544
|
+
const result = root === undefined
|
|
545
|
+
? this.host.root(run, options)
|
|
546
|
+
: this.host.joinRoot(root.rootId, root.epoch, run, { ...options, from: this });
|
|
547
|
+
result
|
|
548
|
+
.then((value) => this.send({ t: "result", id: frame.id, value }), (error) => this.send({ t: "result", id: frame.id, error: toWireError(error) }))
|
|
549
|
+
.finally(() => this.incoming.delete(frame.id));
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* A child that connected behind this host runs roots of an epoch this host never had (it was fenced and is being,
|
|
553
|
+
* or was, reconciled). Such a root is admitted in this host's current epoch at its first call here, and keeps that
|
|
554
|
+
* epoch for its later calls (`superseded` once this host retired it). Other unknown epochs stay `superseded`.
|
|
555
|
+
*/
|
|
556
|
+
#admitRoot(root) {
|
|
557
|
+
if (root === undefined || this.role !== "downstream" || !this.fencedEpochs.has(root.epoch))
|
|
558
|
+
return root;
|
|
559
|
+
if (this.host.hasEpoch(root.epoch))
|
|
560
|
+
return root;
|
|
561
|
+
let epoch = this.#admitted.get(root.rootId);
|
|
562
|
+
if (epoch === undefined) {
|
|
563
|
+
epoch = this.host.current.id;
|
|
564
|
+
this.#admitted.set(root.rootId, epoch);
|
|
565
|
+
}
|
|
566
|
+
return { rootId: root.rootId, epoch };
|
|
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);
|
|
624
|
+
}
|
|
625
|
+
#onHoldReply(frame) {
|
|
626
|
+
const relay = this.relayedHolds.get(frame.id);
|
|
627
|
+
if (relay !== undefined) {
|
|
628
|
+
if (frame.t === "gone")
|
|
629
|
+
relay.close(fromWireError(frame.error), { releaseDown: false, notify: true });
|
|
630
|
+
else if (!relay.closed)
|
|
631
|
+
relay.origin.send({ ...frame, id: relay.originId });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const handle = this.remotes.get(frame.id);
|
|
635
|
+
if (handle === undefined)
|
|
636
|
+
return;
|
|
637
|
+
if (frame.t === "gone") {
|
|
638
|
+
this.remotes.delete(frame.id);
|
|
639
|
+
handle.gone(fromWireError(frame.error));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
handle.shaped({ kind: frame.kind, members: frame.members, contract: frame.contract, epoch: frame.epoch });
|
|
643
|
+
}
|
|
644
|
+
#onResult(frame) {
|
|
645
|
+
const relayed = this.relayedCalls.get(frame.id);
|
|
646
|
+
if (relayed !== undefined) {
|
|
647
|
+
relayed.finish(frame.error === undefined ? undefined : fromWireError(frame.error), frame.value);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const pending = this.calls.get(frame.id);
|
|
651
|
+
this.calls.delete(frame.id);
|
|
652
|
+
if (frame.error === undefined)
|
|
653
|
+
pending?.resolve(frame.value);
|
|
654
|
+
else
|
|
655
|
+
pending?.reject(fromWireError(frame.error));
|
|
656
|
+
}
|
|
657
|
+
#onState(frame) {
|
|
658
|
+
const relayed = this.relayedSubs.get(frame.id);
|
|
659
|
+
if (relayed !== undefined) {
|
|
660
|
+
relayed.relay.origin.send({ ...frame, id: relayed.originSubId });
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
this.subs.get(frame.id)?.(frame);
|
|
664
|
+
}
|
|
665
|
+
// ─── lifetime ───
|
|
666
|
+
#closed(reason) {
|
|
667
|
+
if (this.#pinger !== undefined)
|
|
668
|
+
clearInterval(this.#pinger);
|
|
669
|
+
const error = unavailable(`link to ${this.name ?? "peer"} closed: ${reason}`);
|
|
670
|
+
// As origin: everything the peer held here, relayed or not, is released (downstream calls cancelled).
|
|
671
|
+
for (const id of [...this.served.keys()])
|
|
672
|
+
this.#dropServed(id, true);
|
|
673
|
+
for (const controller of this.incoming.values())
|
|
674
|
+
controller.abort(error);
|
|
675
|
+
this.incoming.clear();
|
|
676
|
+
for (const pending of this.calls.values())
|
|
677
|
+
pending.reject(error);
|
|
678
|
+
this.calls.clear();
|
|
679
|
+
this.subs.clear();
|
|
680
|
+
for (const vote of this.votes.values())
|
|
681
|
+
vote.reject(error);
|
|
682
|
+
this.votes.clear();
|
|
683
|
+
for (const waiter of this.#syncWaiters)
|
|
684
|
+
waiter.reject(error);
|
|
685
|
+
this.#syncWaiters.clear();
|
|
686
|
+
// As downstream of relays: their origins lose the hold.
|
|
687
|
+
for (const relay of [...this.relayedHolds.values()])
|
|
688
|
+
relay.close(error, { releaseDown: false, notify: true });
|
|
689
|
+
this.relayedHolds.clear();
|
|
690
|
+
this.relayedCalls.clear();
|
|
691
|
+
this.relayedSubs.clear();
|
|
692
|
+
for (const handle of this.remotes.values()) {
|
|
693
|
+
// An upstream link may be replaced (reconnect); a downstream host that went away is gone.
|
|
694
|
+
if (this.role === "upstream")
|
|
695
|
+
handle.detached();
|
|
696
|
+
else
|
|
697
|
+
handle.gone(error);
|
|
698
|
+
}
|
|
699
|
+
this.remotes.clear();
|
|
700
|
+
this.host.connectionClosed(this);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* A call through a relay: waits for the route, then forwards with the root it runs in. It settles exactly once; a
|
|
705
|
+
* cancel (or the origin going away) is forwarded downstream and removes both mappings.
|
|
706
|
+
*/
|
|
707
|
+
async function relayCall(relay, frame, root, signal, host) {
|
|
708
|
+
const abortError = () => new ChordError("aborted", "call cancelled", { cause: signal.reason });
|
|
709
|
+
if (signal.aborted)
|
|
710
|
+
throw abortError();
|
|
711
|
+
let onAbort = () => { };
|
|
712
|
+
const aborted = new Promise((_, reject) => {
|
|
713
|
+
onAbort = () => reject(abortError());
|
|
714
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
715
|
+
});
|
|
716
|
+
aborted.catch(() => { });
|
|
717
|
+
try {
|
|
718
|
+
const down = await Promise.race([relay.ready(), aborted]);
|
|
719
|
+
if (relay.closed)
|
|
720
|
+
throw new ChordError("gone", "relayed hold closed");
|
|
721
|
+
return await new Promise((resolve, reject) => {
|
|
722
|
+
const call = new RelayCall(relay, frame.id, down.nextId(), { resolve, reject });
|
|
723
|
+
signal.addEventListener("abort", () => call.cancel(abortError(), true), { once: true });
|
|
724
|
+
host.rootSent(root.rootId, down);
|
|
725
|
+
down.send({ t: "call", id: call.downId, hold: relay.downId, member: frame.member, args: frame.args, root });
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
signal.removeEventListener("abort", onAbort);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Validates a snapshot against the schema the consumer is promised: the provider's own schema for a direct binding,
|
|
734
|
+
* the consumer's contract for one through an `accepts` adapter (the adapter's view is what the consumer gets).
|
|
735
|
+
*/
|
|
736
|
+
function validateSnapshot(handle, member, value) {
|
|
737
|
+
if (value === undefined)
|
|
738
|
+
return;
|
|
739
|
+
const installed = handle.record.currentInstalled();
|
|
740
|
+
if (installed === undefined)
|
|
741
|
+
return;
|
|
742
|
+
const view = handle.currentView();
|
|
743
|
+
const kind = view === undefined ? kindOf(installed.behavior) : kindOf(view);
|
|
744
|
+
const description = view !== undefined && view !== installed.behavior
|
|
745
|
+
? handle.contract.description.variants[kind ?? LOCAL_VARIANT]?.[member]
|
|
746
|
+
: installed.service.contract.description.variants[kindOf(installed.behavior) ?? LOCAL_VARIANT]?.[member];
|
|
747
|
+
if (description?.kind === "state")
|
|
748
|
+
assertValid("state", `${handle.address}.${member}`, description.schema, value);
|
|
749
|
+
}
|
|
750
|
+
/** Clients (browsers) holding an outdated contract are told to reload their code. */
|
|
751
|
+
function clientError(error) {
|
|
752
|
+
if (error instanceof ChordError && error.code === "contract_mismatch") {
|
|
753
|
+
return new ChordError("upgrade_required", error.message, { cause: error });
|
|
754
|
+
}
|
|
755
|
+
return error;
|
|
756
|
+
}
|
|
757
|
+
//# sourceMappingURL=connection.js.map
|