@yz-social/kdht 0.1.3 → 0.1.4
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/.github/workflows/ci.yml +27 -0
- package/dht/kbucket.js +2 -2
- package/dht/node.js +67 -90
- package/dht/nodeContacts.js +7 -5
- package/dht/nodeMessages.js +44 -4
- package/dht/nodeProbe.js +240 -47
- package/dht/nodeRefresh.js +6 -4
- package/dht/nodeStorage.js +4 -0
- package/dht/nodeTransports.js +3 -3
- package/dht/nodeUtilities.js +1 -1
- package/package.json +8 -18
- package/portals/node.js +1 -0
- package/spec/bots.js +12 -8
- package/spec/dhtAcceptanceSpec.js +23 -9
- package/spec/dhtImplementation.js +3 -3
- package/spec/dhtInternalsSpec.js +304 -15
- package/spec/dhtKeySpec.js +0 -3
- package/spec/dhtWriteReadSpec.js +85 -0
- package/spec/portal.js +5 -2
- package/transports/contact.js +88 -20
- package/transports/simulations.js +87 -20
- package/transports/webrtc.js +98 -96
- package/spec/dhtWriteRead.js +0 -56
|
@@ -6,48 +6,99 @@ export class SimulatedContact extends Contact {
|
|
|
6
6
|
get key() { return this.node.key; }
|
|
7
7
|
get isServerNode() { return this.node.isServerNode; }
|
|
8
8
|
|
|
9
|
-
get isRunning() { //
|
|
9
|
+
get isRunning() { // Is the far node running.
|
|
10
10
|
return this.node.isRunning;
|
|
11
11
|
}
|
|
12
12
|
connection = null;
|
|
13
13
|
async connect() { return this; }
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Dispatch directly on the node, returning the response. This is different than the send to and from with messageTag used by
|
|
15
|
+
// SimulatedConnectionContact and WebContact.
|
|
16
|
+
async transmitRPC(messageTag, method, sender, ...rest) {
|
|
17
|
+
// Use delay from the destination node if set, representing a laggy VM/connection
|
|
18
|
+
const delayMs = this.node.delayMs;
|
|
16
19
|
return await this.constructor.ensureTime(async () => {
|
|
17
20
|
if (!this.isRunning) return null; // Receiver closed.
|
|
18
|
-
return await this.node.
|
|
19
|
-
});
|
|
21
|
+
return await this.node.receiveRPC(method, this.node.ensureContact(this.host.contact), ...rest);
|
|
22
|
+
}, delayMs);
|
|
20
23
|
}
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export class SimulatedConnectionContact extends SimulatedContact {
|
|
24
27
|
connection = null; // The cached connection (to another node's connected contact back to us) over which messages can be directly sent, if any.
|
|
25
|
-
|
|
28
|
+
disconnect() {
|
|
29
|
+
// Report if we are the last node to hold a value.
|
|
30
|
+
if (Node.refreshTimeIntervalMS && Node.contacts?.length) { // i.e., not shutting down and in simulation where we track all Contacts.
|
|
31
|
+
for (const key of this.host.storage.keys()) {
|
|
32
|
+
const found = Node.contacts.find(contact => contact?.host.storage.has(key) && (contact?.key !== this.key));
|
|
33
|
+
if (!found) console.log('\n\n*** removing last storer for ', key, this.host.storage.get(key), 'among', Node.contacts.filter(e => e).length, 'contacts ***\n');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return super.disconnect();
|
|
37
|
+
}
|
|
38
|
+
disconnectTransport(andNotify = true) {
|
|
26
39
|
const farContactForUs = this.connection;
|
|
27
40
|
if (!farContactForUs) return;
|
|
28
41
|
Node.assert(farContactForUs.key === this.host.key, 'Far contact backpointer', farContactForUs.node.name, 'does not point to us', this.host.name);
|
|
29
42
|
Node.assert(farContactForUs.host.key === this.key, 'Far contact host', farContactForUs.host.name, 'is not hosted at contact', this.name);
|
|
30
|
-
|
|
31
|
-
this.connection = null;
|
|
43
|
+
super.disconnectTransport(andNotify);
|
|
44
|
+
this.connection = farContactForUs.connection = null;
|
|
32
45
|
}
|
|
33
46
|
async connect(forMethod = 'findNodes') { // Connect from host to node, promising a possibly cloned contact that has been noted.
|
|
34
47
|
// Simulates the setup of a bilateral transport between this host and node, including bookkeeping.
|
|
35
48
|
// TODO: Simulate webrtc signaling.
|
|
36
49
|
const contact = this;
|
|
37
|
-
let { host, node, isServerNode } = contact;
|
|
50
|
+
let { host, node, isServerNode, connection } = contact;
|
|
51
|
+
if (connection) return connection;
|
|
38
52
|
|
|
39
53
|
// Anyone can connect to a server node using the server's connect endpoint.
|
|
40
54
|
// Anyone in the DHT can connect to another DHT node through a sponsor.
|
|
41
|
-
if (
|
|
55
|
+
if (isServerNode) {
|
|
56
|
+
// No point in slowing the tests down to actually wait for this. It doesn't change the outcome.
|
|
57
|
+
//await Node.delay(250); // Connect through portal.
|
|
58
|
+
} else {
|
|
59
|
+
//this.host.xlog('connecting', this.sname);
|
|
42
60
|
let mutualSponsor = null;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
const isConnected = (contact) => { // Is contact already connected to us?
|
|
62
|
+
return contact.connection && contact.node.existingContact(this.node.name)?.connection;
|
|
63
|
+
};
|
|
64
|
+
await Node.delay(100);
|
|
65
|
+
const sponsors = Array.from(this._sponsors.values());
|
|
66
|
+
const target = this.node, targetKey = target.key;
|
|
67
|
+
const findSponsor = () => {
|
|
68
|
+
for (const sponsor of sponsors) {
|
|
69
|
+
//if (isConnected(sponsor)) return sponsor;
|
|
70
|
+
if (sponsor.sendRPC('signals', this.key, [])) {
|
|
71
|
+
return sponsor;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
function findPath(contact, excluded) {
|
|
77
|
+
if (contact.key === targetKey) return true;
|
|
78
|
+
if (!isConnected(contact)) return false;
|
|
79
|
+
const closest = contact.node.findClosestHelpers(targetKey)
|
|
80
|
+
.map(helper => helper.contact)
|
|
81
|
+
.filter(contact => !excluded.includes(contact.key));
|
|
82
|
+
for (const sub of closest) {
|
|
83
|
+
if (findPath(sub, [sub.node.key, ...excluded])) return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (! findSponsor()) {
|
|
88
|
+
await Node.delay(100);
|
|
89
|
+
if ( findSponsor()) console.log('*** found sponsor after delay ***');
|
|
90
|
+
else if (findPath(this.host.contact, [this.host.key])) console.log('*** found path ***');
|
|
91
|
+
else {
|
|
92
|
+
// console.log('No connection path from', this.host.contact.report, 'to', this.report, 'sponsors:', sponsors.map(c => c.report)
|
|
93
|
+
// //, 'contacts:', this.node.findClosestHelpers(targetKey).map(helper => helper.contact.report)
|
|
94
|
+
// );
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
46
97
|
}
|
|
47
|
-
if (!mutualSponsor) return null;
|
|
48
98
|
}
|
|
49
|
-
|
|
50
|
-
|
|
99
|
+
|
|
100
|
+
// our sponsors are not transferred to the other side.
|
|
101
|
+
const farContactForUs = node.ensureContact(host.contact);
|
|
51
102
|
|
|
52
103
|
contact.connection = farContactForUs;
|
|
53
104
|
host.noteContactForTransport(contact);
|
|
@@ -57,10 +108,26 @@ export class SimulatedConnectionContact extends SimulatedContact {
|
|
|
57
108
|
|
|
58
109
|
return contact;
|
|
59
110
|
}
|
|
60
|
-
|
|
111
|
+
signals(...rest) {
|
|
112
|
+
return [this.name]; // Just a simulation
|
|
113
|
+
}
|
|
114
|
+
async send(message) {
|
|
115
|
+
await Node.delay(10);
|
|
116
|
+
this.connection?.receiveRPC(...message);
|
|
117
|
+
}
|
|
118
|
+
async synchronousSend(message) {
|
|
119
|
+
const other = this.connection;
|
|
120
|
+
await Node.delay(1);
|
|
121
|
+
other?.receiveRPC(...message);
|
|
122
|
+
}
|
|
123
|
+
async transmitRPC(messageTag, method, sender, ...rest) { // "transmit" the call (with sending contact added).
|
|
61
124
|
if (!this.isRunning) return null; // Receiver closed.
|
|
62
|
-
const farContactForUs = this.connection
|
|
63
|
-
if (!farContactForUs) return null;
|
|
64
|
-
|
|
125
|
+
const farContactForUs = this.connection;
|
|
126
|
+
if (!farContactForUs) return await Node.delay(this.constructor.maxPingMs, null);
|
|
127
|
+
// Use delay from the destination node if set, representing a laggy VM/connection
|
|
128
|
+
const delayMs = this.node.delayMs;
|
|
129
|
+
const responsePromise = Promise.race([this.getResponsePromise(messageTag), this.rpcTimeout(method)]);
|
|
130
|
+
this.constructor.ensureTime(() => farContactForUs.receiveRPC(messageTag, method, farContactForUs, ...rest), delayMs);
|
|
131
|
+
return await responsePromise;
|
|
65
132
|
}
|
|
66
133
|
}
|
package/transports/webrtc.js
CHANGED
|
@@ -5,7 +5,6 @@ import { Helper } from '../dht/helper.js';
|
|
|
5
5
|
import { Contact } from './contact.js';
|
|
6
6
|
import { WebRTC } from '@yz-social/webrtc';
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
export class WebContact extends Contact { // Our wrapper for the means of contacting a remote node.
|
|
10
9
|
// Can this set all be done more simply?
|
|
11
10
|
get name() { return this.node.name; } // Key of remote node as a string (e.g., as a guid).
|
|
@@ -14,12 +13,15 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
|
|
|
14
13
|
get isRunning() { return this.node.isRunning; } // Have we marked at is no longer running.
|
|
15
14
|
|
|
16
15
|
checkResponse(response) { // Return a fetch response, or throw error if response is not a 200 series.
|
|
16
|
+
if (!response) return;
|
|
17
17
|
if (!response.ok) throw new Error(`fetch ${response.url} failed ${response.status}: ${response.statusText}.`);
|
|
18
18
|
}
|
|
19
|
+
// connection:close is far more robust against pooling issues common to some implementations (e.g., NodeJS).
|
|
20
|
+
// https://github.com/nodejs/undici/issues/3492
|
|
19
21
|
async fetchBootstrap(baseURL, label = 'random') { // Promise to ask portal (over http(s)) to convert a portal
|
|
20
22
|
// worker index or the string 'random' to an available sname to which we can connect().
|
|
21
23
|
const url = `${baseURL}/name/${label}`;
|
|
22
|
-
const response = await fetch(url);
|
|
24
|
+
const response = await fetch(url, {headers: { 'Connection': 'close' } }).catch(e => this.host.xlog(e));
|
|
23
25
|
this.checkResponse(response);
|
|
24
26
|
return await response.json();
|
|
25
27
|
}
|
|
@@ -33,21 +35,49 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
|
|
|
33
35
|
async fetchSignals(url, signalsToSend) {
|
|
34
36
|
const response = await fetch(url, {
|
|
35
37
|
method: 'POST',
|
|
36
|
-
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
headers: { 'Content-Type': 'application/json', 'Connection': 'close' },
|
|
37
39
|
body: JSON.stringify(signalsToSend)
|
|
38
|
-
});
|
|
40
|
+
}).catch(e => this.host.xlog(e));
|
|
39
41
|
this.checkResponse(response);
|
|
40
|
-
return this.checkSignals(await response
|
|
42
|
+
return this.checkSignals(await response?.json());
|
|
41
43
|
}
|
|
42
44
|
async messsageSignals(signals) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
// Try sponsors first (two hops if connected).
|
|
46
|
+
const payload = [this.host.contact.sname, ...signals];
|
|
47
|
+
//this.host.xlog('contact messageSignals', payload);
|
|
48
|
+
const sponsors = Array.from(this._sponsors.values());
|
|
49
|
+
for (const sponsor of sponsors) {
|
|
50
|
+
const response = await sponsor.sendRPC('signals', this.key, payload);
|
|
51
|
+
if (response) return response;
|
|
52
|
+
this._sponsors.delete(sponsor.key);
|
|
53
|
+
}
|
|
54
|
+
if (!this.host.sRunning) return [];
|
|
55
|
+
this.host.xlog('Unable to signal through sponsor. Using recursive message to', this.sname, this.key);
|
|
56
|
+
const recursive = await this.host.contact.sendRPC('signals', this.key, payload, []);
|
|
57
|
+
//this.host.xlog('got recursive response', recursive);
|
|
58
|
+
if (!this.host.isRunning) return [];
|
|
59
|
+
if (!recursive) this.host.xlog('Unable to deliver signals to', this.sname);
|
|
60
|
+
return this.checkSignals(recursive);
|
|
61
|
+
// return this.checkSignals(await this.host.message({targetKey: this.key, targetSname: this.sname,
|
|
62
|
+
// payload: ['signal', this.host.contact.sname, ...signals]}));
|
|
63
|
+
}
|
|
64
|
+
async signals(senderSname, ...signals) { // Accept directed WebRTC signals from a sender sname, creating if necessary the
|
|
65
|
+
// new contact on host to receive them, and promising a response.
|
|
66
|
+
//this.host.xlog('contact signals', senderSname, signals);
|
|
67
|
+
let contact = await this.ensureRemoteContact(senderSname);
|
|
68
|
+
|
|
69
|
+
if (contact.webrtc) return await contact.webrtc.respond(signals);
|
|
70
|
+
|
|
71
|
+
this.host.noteContactForTransport(contact);
|
|
72
|
+
contact.ensureWebRTC();
|
|
73
|
+
return await contact.webrtc.respond(signals);
|
|
45
74
|
}
|
|
46
75
|
get webrtcLabel() {
|
|
47
76
|
return `@${this.host.contact.sname} ==> ${this.sname}`;
|
|
48
77
|
}
|
|
49
78
|
|
|
50
|
-
ensureWebRTC(initiate = false, timeoutMS = this.host.timeoutMS || 5e3) { //
|
|
79
|
+
ensureWebRTC(initiate = false, timeoutMS = this.host.timeoutMS || 5e3) { // Ensure we are connected, if possible.
|
|
80
|
+
// If not already configured, sets up contact to have properties:
|
|
51
81
|
// - connection - a promise for an open webrtc data channel:
|
|
52
82
|
// this.send(string) puts data on the channel
|
|
53
83
|
// incomming messages are dispatched to receiveWebRTC(string)
|
|
@@ -68,7 +98,7 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
|
|
|
68
98
|
const onclose = () => { // Does NOT mean that the far side has gone away. It could just be over maxTransports.
|
|
69
99
|
this.host.log('connection closed');
|
|
70
100
|
resolve(null); // closed promise
|
|
71
|
-
this.webrtc = this.connection = this.
|
|
101
|
+
this.webrtc = this.connection = this.unsafeData = null;
|
|
72
102
|
};
|
|
73
103
|
if (initiate) {
|
|
74
104
|
if (bootstrapHost/* || isServerNode*/) {
|
|
@@ -90,39 +120,32 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
|
|
|
90
120
|
dataChannel.addEventListener('message', event => this.receiveWebRTC(event.data));
|
|
91
121
|
if (this.host.debug) await webrtc.reportConnection(true); // TODO: make this asymchronous?
|
|
92
122
|
if (webrtc.statsElapsed > 500) this.host.xlog(`** slow connection to ${this.sname} took ${webrtc.statsElapsed.toLocaleString()} ms. **`);
|
|
123
|
+
this.unsafeData = dataChannel;
|
|
93
124
|
return dataChannel;
|
|
94
125
|
});
|
|
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
126
|
if (!timeoutMS) {
|
|
103
127
|
this.connection = channelPromise;
|
|
104
|
-
this.overlay = overlayPromise;
|
|
105
128
|
return;
|
|
106
129
|
}
|
|
107
130
|
const timerPromise = new Promise(expired => {
|
|
108
131
|
timeout = setTimeout(async () => {
|
|
109
132
|
const now = Date.now();
|
|
110
|
-
this.host.xlog('
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
this.host.xlog('Unable to connect to', this.sname);
|
|
134
|
+
// this.host.xlog('**** connection timeout', this.sname, now - start,
|
|
135
|
+
// 'status:', webrtc.pc.connectionState, 'signaling:', webrtc.pc.signalingState,
|
|
136
|
+
// 'last signal:', now - webrtc.lastOutboundSignal,
|
|
137
|
+
// 'last send:', now - webrtc.lastOutboundSend,
|
|
138
|
+
// 'last response:', now - webrtc.lastResponse,
|
|
139
|
+
// '****');
|
|
116
140
|
onclose();
|
|
117
141
|
await this.host.removeContact(this); // fixme?
|
|
118
142
|
expired(null);
|
|
119
143
|
}, timeoutMS);
|
|
120
144
|
});
|
|
121
145
|
this.connection = Promise.race([channelPromise, timerPromise]);
|
|
122
|
-
this.overlay = Promise.race([overlayPromise, timerPromise]);
|
|
123
146
|
}
|
|
124
147
|
async connect() { // Connect from host to node, promising a possibly cloned contact that has been noted.
|
|
125
|
-
// Creates a WebRTC instance
|
|
148
|
+
// Creates a connected WebRTC instance.
|
|
126
149
|
const contact = this.host.noteContactForTransport(this);
|
|
127
150
|
///if (contact.connection) contact.host.xlog('connect existing', contact.sname, contact.counter);
|
|
128
151
|
|
|
@@ -131,25 +154,24 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
|
|
|
131
154
|
// Anyone in the DHT can connect to another DHT node through a sponsor.
|
|
132
155
|
if (contact.connection) return contact.connection;
|
|
133
156
|
contact.ensureWebRTC(true);
|
|
134
|
-
await
|
|
157
|
+
await this.connection;
|
|
135
158
|
return this.connection;
|
|
136
159
|
}
|
|
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
160
|
|
|
148
161
|
async send(message) { // Promise to send through previously opened connection promise.
|
|
149
162
|
let channel = await this.connection;
|
|
150
163
|
if (channel?.readyState === 'open') channel.send(JSON.stringify(message));
|
|
151
164
|
else this.host.xlog('Unable to open channel');
|
|
152
165
|
}
|
|
166
|
+
synchronousSend(message) { // this.send awaits channel open promise. This is if we know it has been opened.
|
|
167
|
+
if (this.unsafeData?.readyState !== 'open') return; // But it may have since been closed.
|
|
168
|
+
this.host.log('sending', message, 'to', this.sname);
|
|
169
|
+
try {
|
|
170
|
+
this.unsafeData.send(JSON.stringify(message));
|
|
171
|
+
} catch (e) { // Some webrtc can change readyState in background.
|
|
172
|
+
this.host.log(e);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
153
175
|
getName(sname) { // Answer name from sname.
|
|
154
176
|
if (sname.startsWith(this.constructor.serverSignifier)) return sname.slice(1);
|
|
155
177
|
return sname;
|
|
@@ -172,41 +194,44 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
|
|
|
172
194
|
else if (typeof(sponsor) === 'string') contact.bootstrapHost = sponsor;
|
|
173
195
|
return contact;
|
|
174
196
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
197
|
+
serializeRequest(messageTag, method, sender, targetKey, ...rest) { // Stringify sender and targetKey.
|
|
198
|
+
Node.assert(sender instanceof Contact, 'no sender', sender);
|
|
199
|
+
return [messageTag, method, sender.sname, targetKey.toString(), ...rest];
|
|
200
|
+
}
|
|
201
|
+
async deserializeRequest(method, sender, targetKey, ...rest) { // Inverse of serializeRequest. Response object will be spread for Node receiveRPC.
|
|
202
|
+
// TODO: Currently, parameters do NOT include messageTag! (Because of how receiveRPC is called without it.)
|
|
203
|
+
return [method, await this.ensureRemoteContact(sender), BigInt(targetKey), ...rest];
|
|
204
|
+
}
|
|
205
|
+
isSignalResponse(response) {
|
|
206
|
+
const first = response[0];
|
|
207
|
+
if (!first) return false;
|
|
208
|
+
if (('description' in first) || ('candidate' in first)) return true;
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
serializeResponse(response) {
|
|
212
|
+
if (!this.host.constructor.isContactsResult(response)) return response;
|
|
213
|
+
if (this.isSignalResponse(response)) return response;
|
|
214
|
+
return response.map(helper => [helper.contact.sname, helper.distance.toString()]);
|
|
215
|
+
}
|
|
216
|
+
async deserializeResponse(result) {
|
|
217
|
+
let response;
|
|
218
|
+
if (!Node.isContactsResult(result)) return result;
|
|
219
|
+
if (!result.length) return result;
|
|
220
|
+
if (this.isSignalResponse(result)) return result;
|
|
221
|
+
return await Promise.all(result.map(async ([sname, distance]) =>
|
|
222
|
+
new Helper(await this.ensureRemoteContact(sname, this), BigInt(distance))));
|
|
223
|
+
}
|
|
224
|
+
async transmitRPC(messageTag, method, ...rest) { // Must return a promise.
|
|
194
225
|
// this.host.log('transmit to', this.sname, this.connection ? 'with connection' : 'WITHOUT connection');
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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]);
|
|
226
|
+
const responsePromise = this.getResponsePromise(messageTag);
|
|
227
|
+
await this.send([messageTag, method, ...rest]);
|
|
228
|
+
return await Promise.race([responsePromise, this.rpcTimeout(method), this.closed]);
|
|
205
229
|
}
|
|
230
|
+
|
|
206
231
|
async receiveWebRTC(dataString) { // Handle receipt of a WebRTC data channel message that was sent to this contact.
|
|
207
232
|
// 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
233
|
// 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
|
|
234
|
+
// If we find that in our messageResolvers tags, then the message is a response.
|
|
210
235
|
if (dataString === '"bye"') { // Special messsage that the other side is disconnecting, so we can clean up early.
|
|
211
236
|
this.webrtc.close();
|
|
212
237
|
this.host.xlog('removing disconnected contact', this.sname);
|
|
@@ -214,37 +239,14 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
|
|
|
214
239
|
return;
|
|
215
240
|
}
|
|
216
241
|
const [messageTag, ...data] = JSON.parse(dataString);
|
|
217
|
-
|
|
218
|
-
if (responder) { // A response to something we sent and are waiting for.
|
|
219
|
-
let [result] = data;
|
|
220
|
-
this.inFlight.delete(messageTag);
|
|
221
|
-
if (Array.isArray(result)) {
|
|
222
|
-
if (!result.length) responder(result);
|
|
223
|
-
const first = result[0];
|
|
224
|
-
const isSignal = Array.isArray(first) && ['offer', 'answer', 'icecandidate'].includes(first[0]);
|
|
225
|
-
if (isSignal) {
|
|
226
|
-
responder(result); // This could be the sponsor or the original sender. Either way, it will know what to do.
|
|
227
|
-
} else {
|
|
228
|
-
responder(await Promise.all(result.map(async ([sname, distance]) =>
|
|
229
|
-
new Helper(await this.ensureRemoteContact(sname, this), BigInt(distance)))));
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
responder(result);
|
|
233
|
-
}
|
|
234
|
-
} else { // An incoming request.
|
|
235
|
-
const [method, senderLabel, key, ...rest] = data;
|
|
236
|
-
const sender = await this.ensureRemoteContact(senderLabel);
|
|
237
|
-
let response = await this.receiveRPC(method, sender, BigInt(key), ...rest);
|
|
238
|
-
if ((method !== 'forwardSignals') && this.host.constructor.isContactsResult(response)) {
|
|
239
|
-
response = response.map(helper => [helper.contact.sname, helper.distance.toString()]);
|
|
240
|
-
}
|
|
241
|
-
this.send([messageTag, response]);
|
|
242
|
-
}
|
|
242
|
+
await this.receiveRPC(messageTag, ...data);
|
|
243
243
|
}
|
|
244
244
|
async disconnectTransport() {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
if (!this.connection) return;
|
|
246
|
+
super.disconnectTransport();
|
|
247
|
+
Node.delay(100).then(() => { // Allow time for super to send close/bye message.
|
|
248
|
+
this.webrtc?.close();
|
|
249
|
+
this.connection = this.webrtc = this.initiating = null;
|
|
250
|
+
});
|
|
249
251
|
}
|
|
250
252
|
}
|
package/spec/dhtWriteRead.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env npx jasmine
|
|
2
|
-
const { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } = globalThis; // For linters.
|
|
3
|
-
import process from 'node:process';
|
|
4
|
-
import { exec } from 'node:child_process';
|
|
5
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
-
import { WebContact, Node } from '../index.js';
|
|
7
|
-
|
|
8
|
-
// I cannot get yargs to work properly within jasmine. Get args by hand.
|
|
9
|
-
// Note: jasmine will treat --options as arguments to itself. To pass them to the script, you need to separate with '--'.
|
|
10
|
-
const nWritesIndex = process.argv.indexOf('--nWrites');
|
|
11
|
-
const baseURLIndex = process.argv.indexOf('--baseURL');
|
|
12
|
-
const waitBeforeReadIndex = process.argv.indexOf('--waitBeforeRead');
|
|
13
|
-
const verboseIndex = process.argv.indexOf('--verbose');
|
|
14
|
-
const shutdownIndex = process.argv.indexOf('--shutdown');
|
|
15
|
-
|
|
16
|
-
const nWrites = nWritesIndex >= 0 ? JSON.parse(process.argv[nWritesIndex + 1]) : 10;
|
|
17
|
-
const baseURL = baseURLIndex >= 0 ? process.argv[baseURLIndex + 1] : 'http://localhost:3000/kdht';
|
|
18
|
-
const waitBeforeRead = waitBeforeReadIndex >= 0 ? JSON.parse(process.argv[waitBeforeReadIndex + 1]) : 10;
|
|
19
|
-
const verbose = verboseIndex >= 0 ? JSON.parse( process.argv[verboseIndex + 1] || 'true' ) : false;
|
|
20
|
-
const shutdown = shutdownIndex >= 0 ? JSON.parse( process.argv[shutdownIndex + 1] || 'true' ) : true;
|
|
21
|
-
|
|
22
|
-
describe("DHT write/read", function () {
|
|
23
|
-
let contact;
|
|
24
|
-
beforeAll(async function () {
|
|
25
|
-
contact = await WebContact.create({name: uuidv4(), debug: verbose});
|
|
26
|
-
const bootstrapName = await contact.fetchBootstrap(baseURL);
|
|
27
|
-
const bootstrapContact = await contact.ensureRemoteContact(bootstrapName, baseURL);
|
|
28
|
-
console.log(new Date(), contact.sname, 'joining', bootstrapContact.sname);
|
|
29
|
-
await contact.join(bootstrapContact);
|
|
30
|
-
console.log(new Date(), contact.sname, 'joined');
|
|
31
|
-
for (let index = 0; index < nWrites; index++) {
|
|
32
|
-
const wrote = await contact.storeValue(index, index);
|
|
33
|
-
console.log('Wrote', index);
|
|
34
|
-
}
|
|
35
|
-
if (waitBeforeRead) {
|
|
36
|
-
console.log(new Date(), `Written. Waiting ${waitBeforeRead.toLocaleString()} ms before reading.`);
|
|
37
|
-
await Node.delay(waitBeforeRead);
|
|
38
|
-
}
|
|
39
|
-
console.log(new Date(), 'Reading');
|
|
40
|
-
}, 5e3 * nWrites + 2 * Node.refreshTimeIntervalMS);
|
|
41
|
-
afterAll(async function () {
|
|
42
|
-
if (shutdown) {
|
|
43
|
-
contact.disconnect();
|
|
44
|
-
exec('pkill kdht-portal-server');
|
|
45
|
-
} else {
|
|
46
|
-
contact.disconnect();
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
for (let index = 0; index < nWrites; index++) {
|
|
50
|
-
it(`reads ${index}.`, async function () {
|
|
51
|
-
const read = await contact.node.locateValue(index);
|
|
52
|
-
console.log('read', read);
|
|
53
|
-
expect(read).toBe(index);
|
|
54
|
-
}, 10e3); // Can take longer to re-establish multiple connections.
|
|
55
|
-
}
|
|
56
|
-
});
|