@yz-social/kdht 0.1.0 → 0.1.2

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/dht/node.js CHANGED
@@ -98,7 +98,7 @@ export class Node extends NodeProbe {
98
98
  if (!contact.connection) { this.log('skipping unconnected', contact.sname, 'for message', requestTag); continue; }
99
99
  if (!(await contact.sendRPC('ping', contact.key))) {
100
100
  this.xlog('failed to get ping', contact.sname, 'for message', requestTag);
101
- this.removeContact(contact);
101
+ await this.removeContact(contact);
102
102
  continue;
103
103
  }
104
104
  let overlay = await contact.overlay;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yz-social/kdht",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Pure Kademlia base, for testing variations.",
5
5
  "exports": {
6
6
  ".": "./index.js",
package/spec/node.html CHANGED
@@ -19,29 +19,34 @@
19
19
  import { v4 as uuidv4 } from 'uuid';
20
20
  import { WebContact } from '@yz-social/kdht';
21
21
  Object.assign(globalThis, {uuidv4, WebContact });
22
+ const bootstrapBase = new URL('/kdht', origin).href; // Must be a string
23
+ let contact;
22
24
 
23
- let name = localStorage.getItem('name');
24
- if (!name) {
25
- name = uuidv4();
26
- localStorage.setItem('name', name);
25
+ async function connect() { // Connect or reconnect to the network.
26
+ const name = uuidv4();
27
+ contact = globalThis.contact = await WebContact.create({name});
28
+ const bootstrapName = await contact.fetchBootstrap(bootstrapBase);
29
+ console.log('connecting to', bootstrapName);
30
+ const bootstrapContact = globalThis.bootstrapContact = await contact.ensureRemoteContact(bootstrapName, bootstrapBase);
31
+ await contact.join(bootstrapContact);
32
+ update.onclick();
27
33
  }
28
- const contact = globalThis.contact = await WebContact.create({name});
29
- const bootstrapBase = new URL('/kdht', origin).href; // Must be a string
30
- const bootstrapName = await contact.fetchBootstrap(bootstrapBase);
31
- console.log('connecting to', bootstrapName);
32
- const bootstrapContact = globalThis.bootstrapContact = await contact.ensureRemoteContact(bootstrapName, bootstrapBase);
33
- await contact.join(bootstrapContact);
34
- console.log('connected');
35
34
 
36
35
  update.onclick = () => {
37
36
  const report = contact.node.report(null);
38
- console.log({report, display});
39
37
  display.textContent = report;
40
38
  };
41
- update.onclick();
39
+ await connect();
42
40
 
43
41
  write.onclick = () => contact.storeValue(key.value, writeValue.value);
44
42
  read.onclick = () => contact.node.locateValue(key.value).then(value => readValue.value = value);
43
+ document.addEventListener("visibilitychange", () => {
44
+ if (document.hidden) {
45
+ contact.disconnect();
46
+ } else {
47
+ connect();
48
+ }
49
+ });
45
50
  </script>
46
51
  </body>
47
52
  </html>
@@ -39,9 +39,9 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
39
39
  this.checkResponse(response);
40
40
  return this.checkSignals(await response.json());
41
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]}));
42
+ async messsageSignals(signals) {
43
+ return this.checkSignals(await this.host.message({targetKey: this.key, targetSname: this.sname,
44
+ payload: ['signal', this.host.contact.sname, ...signals]}));
45
45
  }
46
46
  get webrtcLabel() {
47
47
  return `@${this.host.contact.sname} ==> ${this.sname}`;
@@ -209,6 +209,7 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
209
209
  // If we find that in our inFlight tags, then the message is a response.
210
210
  if (dataString === '"bye"') { // Special messsage that the other side is disconnecting, so we can clean up early.
211
211
  this.webrtc.close();
212
+ this.host.xlog('removing disconnected contact');
212
213
  await this.host.removeContact(this); // TODO: Make sure we're not invoking this in maxTransports cases.
213
214
  return;
214
215
  }
package/junx.js DELETED
@@ -1,17 +0,0 @@
1
- async function bar() {
2
- throw new Error("synchronously thrown from bar (asynchronous from foo)");
3
- }
4
- async function foo() {
5
- try {
6
- bar();
7
- } catch (e) {
8
- console.log('foo caught', e);
9
- }
10
- throw new Error('synchronously thrown from foo');
11
- }
12
-
13
- try {
14
- await foo();
15
- } catch (e) {
16
- console.log('outer caught', e);
17
- }
package/routes/index.js DELETED
@@ -1,39 +0,0 @@
1
- import express from 'express';
2
- import { TrickleWebRTC } from '@yz-social/webrtc';
3
- import { WebContact as Contact } from '../index.js';
4
- export const router = express.Router();
5
-
6
-
7
-
8
- router.post('/join/:key', async (req, res, next) => {
9
-
10
- const {params, body} = req;
11
- const tag = params.tag;
12
- const incomingSignals = body;
13
- let connection = testConnections[tag];
14
- console.log('post connection:', !!connection, 'incoming:', incomingSignals.map(s => s[0]));
15
- if (!connection) {
16
- connection = testConnections[tag] = TrickleWebRTC.ensure({serviceLabel: tag});
17
- const timer = setTimeout(() => connection.close(), 15e3);
18
- console.log('created connection and applying', incomingSignals.map(s => s[0]));
19
- connection.next = connection.signals;
20
- const dataPromise = connection.dataChannelPromise = connection.ensureDataChannel('echo', {}, incomingSignals);
21
- dataPromise.then(dataChannel => {
22
- connection.reportConnection(true);
23
- dataChannel.onclose = () => {
24
- clearTimeout(timer);
25
- connection.close();
26
- delete testConnections[tag];
27
- };
28
- dataChannel.onmessage = event => dataChannel.send(event.data); // Just echo what we are given.
29
- });
30
- } else {
31
- console.log('applying incoming signals');
32
- connection.signals = incomingSignals;
33
- }
34
- console.log('awaiting response');
35
- const responseSignals = await Promise.race([connection.next, connection.dataChannelPromise]);
36
- connection.next = connection.signals;
37
- console.log('responding', responseSignals.map(s => s[0]));
38
- res.send(responseSignals); // Send back our signalling answer+ice.
39
- });
@@ -1,48 +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 verboseIndex = process.argv.indexOf('--verbose');
12
- const shutdownIndex = process.argv.indexOf('--shutdown');
13
- const nWrites = nWritesIndex >= 0 ? JSON.parse(process.argv[nWritesIndex + 1]) : 10;
14
- const verbose = verboseIndex >= 0 ? JSON.parse( process.argv[verboseIndex + 1] || 'true' ) : false;
15
- const shutdown = shutdownIndex >= 0 ? JSON.parse( process.argv[shutdownIndex + 1] || 'true' ) : true;
16
-
17
- describe("DHT write/read", function () {
18
- let contact;
19
- beforeAll(async function () {
20
- contact = await WebContact.create({name: uuidv4(), debug: verbose});
21
- const bootstrapName = await contact.fetchBootstrap();
22
- const bootstrapContact = await contact.ensureRemoteContact(bootstrapName, 'http://localhost:3000/kdht');
23
- console.log(new Date(), contact.sname, 'joining', bootstrapContact.sname);
24
- await contact.join(bootstrapContact);
25
- console.log('joined');
26
- for (let index = 0; index < nWrites; index++) {
27
- const wrote = await contact.store(index, index);
28
- console.log('Wrote', index);
29
- }
30
- console.log(new Date(), 'Written. Waiting two refresh periods before reading.', 2 * Node.refreshTimeIntervalMS / 1e3, 'seconds');
31
- await Node.delay(2 * Node.refreshTimeIntervalMS);
32
- console.log(new Date(), 'Reading');
33
- }, 5e3 * nWrites + 2 * Node.refreshTimeIntervalMS);
34
- afterAll(function () {
35
- if (shutdown) {
36
- exec('pkill kdht-bot');
37
- Node.delay(2e3);
38
- exec('pkill kdht-portal-server');
39
- }
40
- });
41
- for (let index = 0; index < nWrites; index++) {
42
- it(`reads ${index}.`, async function () {
43
- const read = await contact.node.locateValue(index);
44
- console.log('read', read);
45
- expect(read).toBe(index);
46
- });
47
- }
48
- });
package/save/webrtc.js DELETED
@@ -1,212 +0,0 @@
1
- import { v4 as uuidv4 } from 'uuid';
2
- import { Node } from '../dht/node.js';
3
- import { Helper } from '../dht/helper.js';
4
- import { Contact } from './contact.js';
5
- import { WebRTC } from '@yz-social/webrtc';
6
- const { BigInt } = globalThis; // For linters.
7
-
8
-
9
- export class WebContact extends Contact {
10
- // Can this set all be done more simply?
11
- get name() { return this.node.name; }
12
- get key() { return this.node.key; }
13
- get isServerNode() { return this.node.isServerNode; }
14
- get isRunning() { return this.node.isRunning; }
15
-
16
- checkResponse(response) {
17
- if (!response.ok) throw new Error(`${this.sname} fetch ${response.url} failed ${response.status}: ${response.statusText}.`);
18
- }
19
- async fetchSignals(url, signalsToSend) {
20
- const response = await fetch(url, {
21
- method: 'POST',
22
- headers: { 'Content-Type': 'application/json' },
23
- body: JSON.stringify(signalsToSend)
24
- });
25
- this.checkResponse(response);
26
- return await response.json();
27
- }
28
- async fetchBootstrap(label = 'random') {
29
- const url = `http://localhost:3000/kdht/name/${label}`;
30
- const response = await fetch(url);
31
- this.checkResponse(response);
32
- return await response.json();
33
- }
34
- get webrtcLabel() {
35
- return `@${this.host.contact.sname} ==> ${this.sname}`;
36
- }
37
- // Within a process, there is only one WebContact.serializer, so each process can only complete one connection at a time.
38
- // (It can maintain Node.maxTransports open connections. The serialization is a limit on the connection/signal process.)
39
- static serializer = Promise.resolve();
40
- channelName = 'kdht';
41
-
42
- ensureWebRTC(initialSignals) { // If not already configured, sets up contact to have properties:
43
- // - connection - a promise for an open webrtc data channel:
44
- // send(string) puts data on the channel
45
- // incomming messages are dispatched to receiveWebRTC(string)
46
- // - closed - resolves when webrtc closes.
47
- // - webrtc - an instance of WebRTC (which may be used for webrtc.respond()
48
- // initialSignals should be a list, or null for the creator of the offer.
49
- if (this.webrtc) return this.webrtc;
50
-
51
- const webrtc = this.webrtc = new WebRTC({label: this.webrtcLabel});
52
- let {promise, resolve} = Promise.withResolvers();
53
- this.closed = promise;
54
- const dataChannelPromise = webrtc.ensureDataChannel(this.channelName, {}, initialSignals);
55
- dataChannelPromise.then(dataChannel => {
56
- webrtc.reportConnection(true);
57
- dataChannel.addEventListener('close', () => {
58
- console.log(webrtc.label, 'connection closed');
59
- this.connection = this.webrtc = null;
60
- resolve(null);
61
- });
62
- dataChannel.addEventListener('message', event => this.receiveWebRTC(event.data));
63
- });
64
- return dataChannelPromise;
65
- }
66
- connect() { // Connect from host to node, promising a possibly cloned contact that has been noted.
67
- // Creates a WebRTC instance and uses it's connectVia to to signal.
68
- const contact = this.host.noteContactForTransport(this);
69
- //this.host.log('ensuring connection for', contact.sname, contact.connection ? 'exists' : 'must create');
70
- if (contact.connection) return contact.connection;
71
- contact.initiating = true;
72
- //console.log('setting client contact connection promise');
73
- return contact.connection = contact.constructor.serializer = contact.constructor.serializer.then(async () => { // TODO: do we need this serialization?
74
- const { host, node, isServerNode, bootstrapHost } = contact;
75
- // Anyone can connect to a server node using the server's connect endpoint.
76
- // Anyone in the DHT can connect to another DHT node through a sponsor.
77
- const dataChannelPromise = contact.ensureWebRTC(null);
78
- await this.webrtc.signalsReady;
79
- let sponsor = contact.findSponsor(candidate => candidate.connection) ||
80
- contact.findSponsor(candidate => candidate.isServerNode);
81
- try {
82
- if (sponsor) {
83
- await contact.webrtc.connectVia(async signals => sponsor.sendRPC('forwardSignals', contact.key, host.contact.sname, ...signals));
84
- } else if (bootstrapHost || !contact.isServerNode) {
85
- const url = `${bootstrapHost || 'http://localhost:3000/kdht'}/join/${this.host.contact.sname}/${this.sname}`;
86
- await contact.webrtc.connectVia(signals => this.fetchSignals(url, signals));
87
- } else {
88
- console.error(`Cannot reach unsponsored contact ${contact.sname}.`);
89
- await contact.host.removeContact(contact);
90
- return contact.connection = contact.webrtc = null;
91
- }
92
- } catch (error) {
93
- console.error(`${contact.webrtc.label} failed to connect through ${sponsor ? `sponsor ${sponsor.sname}` : `bootstrap ${bootstrapHost || contact.sname}`}`);
94
- console.error(error);
95
- return null;
96
- }
97
- return await dataChannelPromise;
98
- });
99
- }
100
- async signals(senderSname, ...signals) { // Accept directed WebRTC signals from a sender sname, creating if necessary the new contact on
101
- // host to receive them, and promising a response.
102
- let contact = await this.ensureRemoteContact(senderSname);
103
- if (contact.initiating) return []; // TODO: is this the right response?
104
- if (contact.webrtc) return await contact.webrtc.respond(signals); // TODO: move this and its counterpart on connect to ensureWebRTC?
105
- this.host.noteContactForTransport(contact);
106
- contact.connection = contact.ensureWebRTC(signals);
107
- return await contact.webrtc.respond([]);
108
- }
109
- async send(message) { // Promise to send through previously opened connection promise.
110
- return this.connection // Connection could be reset to null
111
- ?.then(channel => channel?.send(JSON.stringify(message)))
112
- .catch(error => console.error(this.host.contact.sname, error, error.stack));
113
- }
114
- getName(sname) { // Answer name from sname.
115
- if (sname.startsWith(this.constructor.serverSignifier)) return sname.slice(1);
116
- return sname;
117
- }
118
- async ensureRemoteContact(sname, sponsor = null) {
119
- let contact;
120
- if (sname === this.host.contact.sname) {
121
- contact = this.host.contact; // ok, not remote, but contacts can send back us in a list of closest nodes.
122
- }
123
- const name = this.getName(sname);
124
- if (!contact) {
125
- // Not the final answer. Just an optimization to avoid hashing name.
126
- contact = this.host.existingContact(name);
127
- }
128
- if (!contact) {
129
- const isServerNode = name !== sname;
130
- contact = await this.constructor.create({name, isServerNode}, this.host); // checks for existence AFTER creating Node.
131
- }
132
- if (sponsor instanceof Contact) contact.noteSponsor(sponsor);
133
- else if (typeof(sponsor) === 'string') contact.bootstrapHost = sponsor;
134
- return contact;
135
- }
136
- receiveRPC(method, sender, key, ...rest) { // Receive an RPC from sender, dispatch, and return that value, which will be awaited and sent back to sender.
137
- if (method === 'forwardSignals') { // Can't be handled by Node, because 'forwardSignals' is specific to WebContact.
138
- const [sendingSname, ...signals] = rest;
139
- //console.log(this.host.contact.sname, this.host.key, 'received forwardingSignals from', sender.sname, 'on contact', this.sname, this.key, 'concerning', key);
140
- if (key === this.host.key) { // for us!
141
- //console.log(this.host.contact.sname, 'processing signals from', sendingSname, signals.map(s => s[0]));
142
- return this.signals(...rest);
143
- } else { // Forard to the target.
144
- const target = this.host.findContactByKey(key);
145
- if (!target) {
146
- //console.log(this.host.contact.same, 'does not have contact to', sendingSname);
147
- return null;
148
- }
149
- //if (this.debugCounter++ > 3) process.exit(1);
150
- //console.log(this.host.contact.sname, 'forwarding signals from', sendingSname, signals.map(s => s[0]), 'to target', target.sname, key);
151
- return target.sendRPC('forwardSignals', key, ...rest);
152
- }
153
- }
154
- return super.receiveRPC(method, sender, key, ...rest);
155
- }
156
- //debugCounter = 0;
157
- inFlight = new Map();
158
- async transmitRPC(method, key, ...rest) { // Must return a promise.
159
- // this.host.log('transmit to', this.sname, this.connection ? 'with connection' : 'WITHOUT connection');
160
- if (!await this.connect()) return null;
161
- const sender = this.host.contact.sname;
162
- // uuid so that the two sides don't send a request with the same id to each other.
163
- // Alternatively, we could concatenate a counter to our host.name.
164
- const messageTag = uuidv4();
165
- const responsePromise = new Promise(resolve => this.inFlight.set(messageTag, resolve));
166
- // this.host.log('sending to', this.sname);
167
- //console.log(this.host.contact.sname, 'sending', method, key, rest.map(s => Array.isArray(s) ? s[0] : s), 'to', this.sname);
168
- this.send([messageTag, method, sender, key.toString(), ...rest]);
169
- const response = await Promise.race([responsePromise, this.closed]);
170
- // this.host.log('got response from', this.sname);
171
- return response;
172
- }
173
- async receiveWebRTC(dataString) { // Handle receipt of a WebRTC data channel message that was sent to this contact.
174
- // The message could the start of an RPC sent from the peer, or it could be a response to an RPC that we made.
175
- // As we do the latter, we generate and note (in transmitRPC) a message tag included in the message.
176
- // If we find that in our inFlight tags, then the message is a response.
177
- const [messageTag, ...data] = JSON.parse(dataString);
178
- const responder = this.inFlight.get(messageTag);
179
- if (responder) { // A response to something we sent and are waiting for.
180
- let [result] = data;
181
- this.inFlight.delete(messageTag);
182
- //this.host.log('received response', result);
183
- if (Array.isArray(result)) {
184
- if (!result.length) responder(result);
185
- const first = result[0];
186
- const isSignal = Array.isArray(first) && ['offer', 'answer', 'icecandidate'].includes(first[0]);
187
- //console.log(this.host.contact.sname, 'got rpc response on contact', this.sname, isSignal, result.map(s => Array.isArray(s) ? s[0] : s));
188
- if (isSignal) {
189
- responder(result); // This could be the sponsor or the original sender. Either way, it will know what to do.
190
- } else {
191
- responder(await Promise.all(result.map(async ([sname, distance]) =>
192
- new Helper(await this.ensureRemoteContact(sname, this), BigInt(distance)))));
193
- }
194
- } else {
195
- responder(result);
196
- }
197
- } else { // An incoming request.
198
- const [method, senderLabel, key, ...rest] = data;
199
- const sender = await this.ensureRemoteContact(senderLabel);
200
- //this.host.log('dispatched', method, 'from', sender.sname);
201
- let response = await this.receiveRPC(method, sender, BigInt(key), ...rest);
202
- //if (method === 'forwardSignals') console.log(this.host.contact.sname, 'incoming signals through', this.sname, 'originating from', rest[0]);
203
- if ((method !== 'forwardSignals') && this.host.constructor.isContactsResult(response)) {
204
- response = response.map(helper => [helper.contact.sname, helper.distance.toString()]);
205
- }
206
- this.send([messageTag, response]);
207
- }
208
- }
209
- disconnectTransport() {
210
- this.webrtc?.close();
211
- }
212
- }