@yz-social/kdht 0.1.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.
@@ -0,0 +1,127 @@
1
+ import { Node } from '../dht/node.js';
2
+
3
+ export class Contact {
4
+ // Represents an abstract contact from a host (a Node) to another node.
5
+ // The host calls aContact.sendRpc(...messageParameters) to send the message to node and promises the response.
6
+ // This could be by wire, by passing the message through some overlay network, or for just calling a method directly on node in a simulation.
7
+
8
+ // Creation
9
+ // host should be a dht Node.
10
+ // node is the far end of the contact, and could be Node (for in-process simulation) or a serialization of a key.
11
+ static counter = 0;
12
+ static fromNode(node, host = node) {
13
+ let contact = host.existingContact(node.name);
14
+ if (contact) Node.assert(contact.host === host, 'Existing contact host', contact.host.name, 'does not match specified host', host.name, 'for', node.name);
15
+ //if (!contact) host.log('Creating contact', node.name);
16
+ contact ||= new this();
17
+ // Every Contact is unique to a host Node, from which it sends messages to a specific "far" node.
18
+ // Every Node caches a contact property for that Node as it's own host, and from which Contacts for other hosts may be cloned.
19
+ node.contact ||= contact;
20
+ contact.node = node;
21
+ contact.host = host; // In whose buckets (or looseTransports) does this contact live?
22
+ contact.counter = this.counter++;
23
+ host.addExistingContact(contact); // After contact.node (and thus contact.namem) is set.
24
+ return contact;
25
+ }
26
+ static async create(properties, host = undefined) {
27
+ return this.fromNode(await Node.create(properties), host);
28
+ }
29
+ static fromKey(key, host) {
30
+ const node = Node.fromKey(key);
31
+ return this.fromNode(node, host || node);
32
+ }
33
+ clone(hostNode, searchHost = true) { // Answer a Contact that is set up for hostNode - either this instance or a new one.
34
+ // Unless searchHost is null, any existing contact on hostNode will be returned.
35
+ if (this.host === hostNode) return this; // All good.
36
+
37
+ // Reuse existing contact in hostNode -- if still running.
38
+ let existing = searchHost && hostNode.existingContact(this.name);
39
+ if (existing?.isRunning) return existing;
40
+
41
+ // Make one.
42
+ Node.assert(this.key !== hostNode.key, 'Cloning self-contact', this, hostNode);
43
+ const clone = this.constructor.fromNode(this.node, hostNode);
44
+ return clone;
45
+ }
46
+ static serverSignifier = 'S';
47
+ get sname() { // Serialized name, indicating whether it is a server node.
48
+ if (this._sname) return this._sname;
49
+ if (this.name.length < 36) return this._sname = this.name; // Kluge: index of portal node.
50
+ if (this.isServerNode) return this._sname = this.constructor.serverSignifier + this.name;
51
+ return this._sname = this.name;
52
+ }
53
+
54
+ // Operations
55
+ join(other) { return this.host.join(other); }
56
+ storeValue(key, value) { return this.host.storeValue(key, value); }
57
+ store(key, value) {
58
+ return this.sendRPC('store', key, value);
59
+ }
60
+ disconnect() { // Simulate a disconnection of node, marking as such and rejecting any RPCs in flight.
61
+ Node.assert(this.host === this.node, "Disconnect", this.name, "not invoked on home contact", this.host.name);
62
+ this.host.isRunning = false;
63
+ this.host.stopRefresh();
64
+ this.host.contacts.forEach(async contact => {
65
+ const far = contact.connection;
66
+ if (!far) return;
67
+ contact.disconnectTransport();
68
+ });
69
+ }
70
+ distance(key) { return this.host.constructor.distance(this.key, key); }
71
+
72
+ // RPC
73
+ sendRPC(method, ...rest) { // Promise the result of a nework call to node. Rejects if we get disconnected along the way.
74
+ const sender = this.host.contact;
75
+ //this.host.log('sendRPC', method, rest, sender.isRunning ? 'running' : 'stopped', 'sender key:', sender.key, 'to node:', this.sname, this.key);
76
+ if (!sender.isRunning) {this.host.log('not running'); return null; }// sender closed before call.
77
+ if (sender.key === this.key) {
78
+ const result = this.receiveRPC(method, sender, ...rest);
79
+ if (!result) this.host.xlog('no local result');
80
+ return result;
81
+ }
82
+
83
+ const start = Date.now();
84
+ return this.transmitRPC(method, ...rest) // The main event.
85
+ .then(result => {
86
+ if (!sender.isRunning) {this.host.log('sender closed'); return null; } // Sender closed after call.
87
+ return result;
88
+ })
89
+ .finally(() => Node.noteStatistic(start, 'rpc'));
90
+ }
91
+ async receiveRPC(method, sender, ...rest) { // Call the message method to act on the 'to' node side.
92
+ Node.assert(typeof(method)==='string', 'no method', method);
93
+ Node.assert(sender instanceof Contact, 'no sender', sender);
94
+ return this.host.receiveRPC(method, sender, ...rest);
95
+ }
96
+ // Sponsorship
97
+ _sponsors = new Map(); // maps key => contact
98
+ noteSponsor(contact) {
99
+ if (!contact) return;
100
+ this._sponsors.set(contact.key, contact);
101
+ }
102
+ hasSponsor(key) {
103
+ return this._sponsors.get(key);
104
+ }
105
+ async findSponsor(predicate) { // Answer the sponsor contact for which await predicate(contact) is true, else falsy.
106
+ for (const candidate of this._sponsors.values()) {
107
+ if (await predicate(candidate)) return candidate;
108
+ }
109
+ return null;
110
+ }
111
+
112
+ // Utilities
113
+ get report() { // Answer string of name, followed by * if disconnected
114
+ //return `${this.connection ? '_' : ''}${this.sname}${this.isRunning ? '' : '*'}@${this.host.contact.sname}v${this.counter}`; // verbose version
115
+ //return `${this.connection ? '_' : ''}${this.sname}v${this.counter}${this.isRunning ? '' : '*'}`;
116
+ return `${this.connection ? '_' : ''}${this.sname}${this.isRunning ? '' : '*'}`; // simpler version
117
+ }
118
+ static pingTimeMS = 30; // ms
119
+ static async ensureTime(thunk, ms = this.pingTimeMS) { // Promise that thunk takes at least ms to execute.
120
+ const start = Date.now();
121
+ const result = await thunk();
122
+ const elapsed = Date.now() - start;
123
+ if (elapsed > ms) return result;
124
+ await new Promise(resolve => setTimeout(resolve, ms - elapsed));
125
+ return result;
126
+ }
127
+ }
@@ -0,0 +1,70 @@
1
+ import { Node } from '../dht/node.js';
2
+ import { Contact, SimulatedContact, SimulatedConnectionContact } from './contact.js';
3
+ import { WebRTC } from '@yz-social/webrtc';
4
+
5
+ export class InProcessWebContact extends SimulatedConnectionContact {
6
+ // Still a SimulatedContacConnection, but does create the real bidirectional webrtc connection.
7
+ // Of course, testing requires that there be very few nodes, because:
8
+ // - Each node is running in one instance, so the limited number of WebRTC instances gets used up per process instead of per node.
9
+ // - Each connection takes two in-process WebRTC instances - one for each end.
10
+ static count = 0;
11
+ static serializer = Promise.resolve();
12
+ async connect(forMethod = 'findNodes') { // Connect from host to node, promising a possibly cloned contact that has been noted.
13
+ // Simulates the setup of a bilateral transport between this host and node, including bookkeeping.
14
+ // TODO: Simulate webrtc signaling.
15
+ const contact = this;
16
+ let { host, node, isServerNode } = contact;
17
+ if (contact.webrtc) throw Error('wtf');
18
+
19
+ // Anyone can connect to a server node using the server's connect endpoint.
20
+ // Anyone in the DHT can connect to another DHT node through a sponsor.
21
+ if (!isServerNode) {
22
+ let mutualSponsor = null;
23
+ for (const sponsor of this._sponsors.values()) {
24
+ if (!sponsor.hasConnection || !sponsor.node.existingContact(this.node.namem)?.hasConnection) continue;
25
+ mutualSponsor = sponsor;
26
+ }
27
+ if (!mutualSponsor) return null;
28
+ }
29
+ const farContactForUs = node.ensureContact(host.contact, contact.sponsor);
30
+
31
+ // FIXME
32
+ // Just makes the connection and bashes it into place at both ends.
33
+ // Only works because the contact has access to both ends.
34
+ await (this.constructor.serializer = this.constructor.serializer.then(async () => {
35
+ const a = new WebRTC({label: 'initiator' + host.name + '/' + node.name});
36
+ const b = new WebRTC({label: 'contacted' + node.name + '/' + host.name});
37
+ const channelName = 'kdht';
38
+ this.constructor.count += 2;
39
+ const aDataChannelPromise = a.ensureDataChannel(channelName);
40
+ await a.signalsReady;
41
+ const bDataChannelPromise = b.ensureDataChannel(channelName, {}, a.signals);
42
+ await b.signalsReady;
43
+ await a.connectVia(signals => b.respond(signals));
44
+ const aDataChannel = await aDataChannelPromise;
45
+ const bDataChannel = await bDataChannelPromise;
46
+ let onclose = contact => {
47
+ this.constructor.count--;
48
+ //console.log('disconnect', contact.host.name, 'to', contact.node.name, this.constructor.count);
49
+ contact.webrtc = null;
50
+ };
51
+ aDataChannel.addEventListener('close', () => onclose(contact));
52
+ bDataChannel.addEventListener('close', () => onclose(farContactForUs));
53
+ contact.webrtc = a;
54
+ farContactForUs.webrtc = b;
55
+ //console.log(`${host.name}->${node.name}, ${this.constructor.count} webrtc peers`);
56
+ }));
57
+
58
+ contact.hasTransport = farContactForUs;
59
+ host.noteContactForTransport(contact);
60
+
61
+ farContactForUs.hasTransport = contact;
62
+ node.noteContactForTransport(farContactForUs);
63
+
64
+ return contact;
65
+ }
66
+ disconnectTransport() {
67
+ this.webrtc?.close();
68
+ super.disconnectTransport();
69
+ }
70
+ }
@@ -0,0 +1,66 @@
1
+ import { Node } from '../dht/node.js';
2
+ import { Contact } from './contact.js';
3
+
4
+ export class SimulatedContact extends Contact {
5
+ get name() { return this.node.name; }
6
+ get key() { return this.node.key; }
7
+ get isServerNode() { return this.node.isServerNode; }
8
+
9
+ get isRunning() { // Ask our canonical home contact.
10
+ return this.node.isRunning;
11
+ }
12
+ connection = null;
13
+ async connect() { return this; }
14
+ disconnectTransport() { }
15
+ async transmitRPC(method, ...rest) { // Transmit the call to the receiving node's contact.
16
+ return await this.constructor.ensureTime(async () => {
17
+ if (!this.isRunning) return null; // Receiver closed.
18
+ return await this.node.contact.receiveRPC(method, this.node.ensureContact(this.host.contact), ...rest);
19
+ });
20
+ }
21
+ }
22
+
23
+ export class SimulatedConnectionContact extends SimulatedContact {
24
+ connection = null; // The cached connection (to another node's connected contact back to us) over which messages can be directly sent, if any.
25
+ disconnectTransport() {
26
+ const farContactForUs = this.connection;
27
+ if (!farContactForUs) return;
28
+ Node.assert(farContactForUs.key === this.host.key, 'Far contact backpointer', farContactForUs.node.name, 'does not point to us', this.host.name);
29
+ Node.assert(farContactForUs.host.key === this.key, 'Far contact host', farContactForUs.host.name, 'is not hosted at contact', this.name);
30
+ farContactForUs.connection = null;
31
+ this.connection = null;
32
+ }
33
+ async connect(forMethod = 'findNodes') { // Connect from host to node, promising a possibly cloned contact that has been noted.
34
+ // Simulates the setup of a bilateral transport between this host and node, including bookkeeping.
35
+ // TODO: Simulate webrtc signaling.
36
+ const contact = this;
37
+ let { host, node, isServerNode } = contact;
38
+
39
+ // Anyone can connect to a server node using the server's connect endpoint.
40
+ // Anyone in the DHT can connect to another DHT node through a sponsor.
41
+ if (!isServerNode) {
42
+ let mutualSponsor = null;
43
+ for (const sponsor of this._sponsors.values()) {
44
+ if (!sponsor.connection || !sponsor.node.existingContact(this.node.name)?.connection) continue;
45
+ mutualSponsor = sponsor;
46
+ }
47
+ if (!mutualSponsor) return null;
48
+ }
49
+
50
+ const farContactForUs = node.ensureContact(host.contact, contact.sponsor);
51
+
52
+ contact.connection = farContactForUs;
53
+ host.noteContactForTransport(contact);
54
+
55
+ farContactForUs.connection = contact;
56
+ node.noteContactForTransport(farContactForUs);
57
+
58
+ return contact;
59
+ }
60
+ async transmitRPC(method, ...rest) { // A message from this.host to this.node. Forward to this.node through overlay connection for bucket.
61
+ if (!this.isRunning) return null; // Receiver closed.
62
+ const farContactForUs = this.connection || (await this.connect(method))?.connection;
63
+ if (!farContactForUs) return null; // receiver no longer reachable.
64
+ return await this.constructor.ensureTime(() => farContactForUs.receiveRPC(method, farContactForUs, ...rest));
65
+ }
66
+ }
@@ -0,0 +1,249 @@
1
+ const { BigInt } = globalThis; // For linters.
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { Node } from '../dht/node.js';
4
+ import { Helper } from '../dht/helper.js';
5
+ import { Contact } from './contact.js';
6
+ import { WebRTC } from '@yz-social/webrtc';
7
+
8
+
9
+ export class WebContact extends Contact { // Our wrapper for the means of contacting a remote node.
10
+ // Can this set all be done more simply?
11
+ get name() { return this.node.name; } // Key of remote node as a string (e.g., as a guid).
12
+ get key() { return this.node.key; } // Key of remote node as a BigInt.
13
+ get isServerNode() { return this.node.isServerNode; } // It it reachable through a server.
14
+ get isRunning() { return this.node.isRunning; } // Have we marked at is no longer running.
15
+
16
+ checkResponse(response) { // Return a fetch response, or throw error if response is not a 200 series.
17
+ if (!response.ok) throw new Error(`fetch ${response.url} failed ${response.status}: ${response.statusText}.`);
18
+ }
19
+ async fetchBootstrap(baseURL, label = 'random') { // Promise to ask portal (over http(s)) to convert a portal
20
+ // worker index or the string 'random' to an available sname to which we can connect().
21
+ const url = `${baseURL}/name/${label}`;
22
+ const response = await fetch(url);
23
+ this.checkResponse(response);
24
+ return await response.json();
25
+ }
26
+ async checkSignals(signals) {
27
+ if (!signals) {
28
+ await this.host.removeContact(this);
29
+ return [];
30
+ }
31
+ return signals;
32
+ }
33
+ async fetchSignals(url, signalsToSend) {
34
+ const response = await fetch(url, {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify(signalsToSend)
38
+ });
39
+ this.checkResponse(response);
40
+ return this.checkSignals(await response.json());
41
+ }
42
+ messsageSignals(signals) {
43
+ return this.checkSignals(this.host.message({targetKey: this.key, targetSname: this.sname,
44
+ payload: ['signal', this.host.contact.sname, ...signals]}));
45
+ }
46
+ get webrtcLabel() {
47
+ return `@${this.host.contact.sname} ==> ${this.sname}`;
48
+ }
49
+
50
+ ensureWebRTC(initiate = false, timeoutMS = this.host.timeoutMS || 5e3) { // If not already configured, sets up contact to have properties:
51
+ // - connection - a promise for an open webrtc data channel:
52
+ // this.send(string) puts data on the channel
53
+ // incomming messages are dispatched to receiveWebRTC(string)
54
+ // - closed - resolves when webrtc closes.
55
+ // - webrtc - an instance of WebRTC (which may be used for webrtc.respond()
56
+ //
57
+ // If timeoutMS is non-zero and a connection is not established within that time, connection and closed resolve to null.
58
+ //
59
+ // This is synchronous: all side-effects (assignments to this) happen immediately.
60
+ const start = Date.now();
61
+ const { host, node, isServerNode, bootstrapHost } = this;
62
+ this.host.log('starting connection', this.sname, this.connection ? 'exists!!!' : 'fresh', this.counter);
63
+ let {promise, resolve} = Promise.withResolvers();
64
+ this.closed = promise;
65
+ const webrtc = this.webrtc = new WebRTC({name: this.webrtcLabel,
66
+ debug: host.debug,
67
+ polite: this.host.key < this.node.key});
68
+ const onclose = () => { // Does NOT mean that the far side has gone away. It could just be over maxTransports.
69
+ this.host.log('connection closed');
70
+ resolve(null); // closed promise
71
+ this.webrtc = this.connection = this.overlay = null;
72
+ };
73
+ if (initiate) {
74
+ if (bootstrapHost/* || isServerNode*/) {
75
+ const url = `${bootstrapHost || 'http://localhost:3000/kdht'}/join/${host.contact.sname}/${this.sname}`;
76
+ this.webrtc.transferSignals = signals => this.fetchSignals(url, signals);
77
+ } else {
78
+ this.webrtc.transferSignals = signals => this.messsageSignals(signals);
79
+ }
80
+ } // Otherwise, we just hang on to signals until we're asked to respond().
81
+
82
+ let timeout;
83
+ const kdhtChannelName = 'kdht';
84
+ const channelPromise = webrtc.getDataChannelPromise(kdhtChannelName);
85
+ webrtc.createChannel(kdhtChannelName, {negotiated: true});
86
+ channelPromise.then(async dataChannel => {
87
+ this.host.log('data channel open', this.sname, Date.now() - start, this.counter);
88
+ clearTimeout(timeout);
89
+ dataChannel.addEventListener('close', onclose);
90
+ dataChannel.addEventListener('message', event => this.receiveWebRTC(event.data));
91
+ if (this.host.debug) await webrtc.reportConnection(true); // TODO: make this asymchronous?
92
+ if (webrtc.statsElapsed > 500) this.host.xlog(`** slow connection to ${this.sname} took ${webrtc.statsElapsed.toLocaleString()} ms. **`);
93
+ return dataChannel;
94
+ });
95
+ const overlayChannelName = 'overlay';
96
+ const overlayPromise = webrtc.getDataChannelPromise(overlayChannelName);
97
+ webrtc.createChannel(overlayChannelName, {negotiated: true});
98
+ overlayPromise.then(async overlay => {
99
+ overlay.addEventListener('message', event => host.messageHandler(event.data));
100
+ return overlay;
101
+ });
102
+ if (!timeoutMS) {
103
+ this.connection = channelPromise;
104
+ this.overlay = overlayPromise;
105
+ return;
106
+ }
107
+ const timerPromise = new Promise(expired => {
108
+ timeout = setTimeout(async () => {
109
+ const now = Date.now();
110
+ this.host.xlog('**** connection timeout', this.sname, now - start,
111
+ 'status:', webrtc.pc.connectionState, 'signaling:', webrtc.pc.signalingState,
112
+ 'last signal:', now - webrtc.lastOutboundSignal,
113
+ 'last send:', now - webrtc.lastOutboundSend,
114
+ 'last response:', now - webrtc.lastResponse,
115
+ '****');
116
+ onclose();
117
+ await this.host.removeContact(this); // fixme?
118
+ expired(null);
119
+ }, timeoutMS);
120
+ });
121
+ this.connection = Promise.race([channelPromise, timerPromise]);
122
+ this.overlay = Promise.race([overlayPromise, timerPromise]);
123
+ }
124
+ async connect() { // Connect from host to node, promising a possibly cloned contact that has been noted.
125
+ // Creates a WebRTC instance and uses it's connectVia to to signal.
126
+ const contact = this.host.noteContactForTransport(this);
127
+ ///if (contact.connection) contact.host.xlog('connect existing', contact.sname, contact.counter);
128
+
129
+ const { host, node, isServerNode, bootstrapHost } = contact;
130
+ // Anyone can connect to a server node using the server's connect endpoint.
131
+ // Anyone in the DHT can connect to another DHT node through a sponsor.
132
+ if (contact.connection) return contact.connection;
133
+ contact.ensureWebRTC(true);
134
+ await Promise.all([this.connection, this.overlay]);
135
+ return this.connection;
136
+ }
137
+ async signals(senderSname, ...signals) { // Accept directed WebRTC signals from a sender sname, creating if necessary the
138
+ // new contact on host to receive them, and promising a response.
139
+ let contact = await this.ensureRemoteContact(senderSname);
140
+
141
+ if (contact.webrtc) return await contact.webrtc.respond(signals);
142
+
143
+ this.host.noteContactForTransport(contact);
144
+ contact.ensureWebRTC();
145
+ return await contact.webrtc.respond(signals);
146
+ }
147
+
148
+ async send(message) { // Promise to send through previously opened connection promise.
149
+ let channel = await this.connection;
150
+ if (channel?.readyState === 'open') channel.send(JSON.stringify(message));
151
+ else this.host.xlog('Unable to open channel');
152
+ }
153
+ getName(sname) { // Answer name from sname.
154
+ if (sname.startsWith(this.constructor.serverSignifier)) return sname.slice(1);
155
+ return sname;
156
+ }
157
+ async ensureRemoteContact(sname, sponsor = null) {
158
+ let contact;
159
+ if (sname === this.host.contact.sname) {
160
+ contact = this.host.contact; // ok, not remote, but contacts can send back us in a list of closest nodes.
161
+ }
162
+ const name = this.getName(sname);
163
+ if (!contact) {
164
+ // Not the final answer. Just an optimization to avoid hashing name.
165
+ contact = this.host.existingContact(name);
166
+ }
167
+ if (!contact) {
168
+ const isServerNode = name !== sname;
169
+ contact = await this.constructor.create({name, isServerNode}, this.host); // checks for existence AFTER creating Node.
170
+ }
171
+ if (sponsor instanceof Contact) contact.noteSponsor(sponsor);
172
+ else if (typeof(sponsor) === 'string') contact.bootstrapHost = sponsor;
173
+ return contact;
174
+ }
175
+ receiveRPC(method, sender, key, ...rest) { // Receive an RPC from sender, dispatch, and return that value, which will be awaited and sent back to sender.
176
+ if (method === 'forwardSignals') { // Can't be handled by Node, because 'forwardSignals' is specific to WebContact.
177
+ const [sendingSname, ...signals] = rest;
178
+ if (key === this.host.key) { // for us!
179
+ return this.signals(...rest);
180
+ } else { // Forward to the target.
181
+ const target = this.host.findContactByKey(key);
182
+ if (!target) {
183
+ console.log(this.host.contact.sname, 'does not have contact to', sendingSname);
184
+ return null;
185
+ }
186
+ return target.sendRPC('forwardSignals', key, ...rest);
187
+ }
188
+ }
189
+ return super.receiveRPC(method, sender, key, ...rest);
190
+ }
191
+ inFlight = new Map();
192
+ async transmitRPC(method, key, ...rest) { // Must return a promise.
193
+ const MAX_PING_MS = 250; // No including connect time. These are single-hop WebRTC data channels.
194
+ // this.host.log('transmit to', this.sname, this.connection ? 'with connection' : 'WITHOUT connection');
195
+ if (!await this.connect()) return null;
196
+ const sender = this.host.contact.sname;
197
+ // uuid so that the two sides don't send a request with the same id to each other.
198
+ // Alternatively, we could concatenate a counter to our host.name.
199
+ const messageTag = uuidv4();
200
+ const responsePromise = new Promise(resolve => this.inFlight.set(messageTag, resolve));
201
+ const message = [messageTag, method, sender, key.toString(), ...rest];
202
+ this.send(message);
203
+ const timeout = Node.delay(MAX_PING_MS, null); // Faster than waiting for webrtc to observe a close
204
+ return await Promise.race([responsePromise, timeout, this.closed]);
205
+ }
206
+ async receiveWebRTC(dataString) { // Handle receipt of a WebRTC data channel message that was sent to this contact.
207
+ // The message could the start of an RPC sent from the peer, or it could be a response to an RPC that we made.
208
+ // As we do the latter, we generate and note (in transmitRPC) a message tag included in the message.
209
+ // If we find that in our inFlight tags, then the message is a response.
210
+ if (dataString === '"bye"') { // Special messsage that the other side is disconnecting, so we can clean up early.
211
+ this.webrtc.close();
212
+ await this.host.removeContact(this); // TODO: Make sure we're not invoking this in maxTransports cases.
213
+ return;
214
+ }
215
+ const [messageTag, ...data] = JSON.parse(dataString);
216
+ const responder = this.inFlight.get(messageTag);
217
+ if (responder) { // A response to something we sent and are waiting for.
218
+ let [result] = data;
219
+ this.inFlight.delete(messageTag);
220
+ if (Array.isArray(result)) {
221
+ if (!result.length) responder(result);
222
+ const first = result[0];
223
+ const isSignal = Array.isArray(first) && ['offer', 'answer', 'icecandidate'].includes(first[0]);
224
+ if (isSignal) {
225
+ responder(result); // This could be the sponsor or the original sender. Either way, it will know what to do.
226
+ } else {
227
+ responder(await Promise.all(result.map(async ([sname, distance]) =>
228
+ new Helper(await this.ensureRemoteContact(sname, this), BigInt(distance)))));
229
+ }
230
+ } else {
231
+ responder(result);
232
+ }
233
+ } else { // An incoming request.
234
+ const [method, senderLabel, key, ...rest] = data;
235
+ const sender = await this.ensureRemoteContact(senderLabel);
236
+ let response = await this.receiveRPC(method, sender, BigInt(key), ...rest);
237
+ if ((method !== 'forwardSignals') && this.host.constructor.isContactsResult(response)) {
238
+ response = response.map(helper => [helper.contact.sname, helper.distance.toString()]);
239
+ }
240
+ this.send([messageTag, response]);
241
+ }
242
+ }
243
+ async disconnectTransport() {
244
+ await this.send('bye'); await Node.delay(50); // FIXME: Hack: We'll need to be able to pass tests without this, too.
245
+
246
+ this.webrtc?.close();
247
+ this.connection = this.webrtc = this.initiating = null;
248
+ }
249
+ }