@yz-social/kdht 0.1.9 → 0.1.10

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/kbucket.js CHANGED
@@ -36,7 +36,6 @@ export class KBucket {
36
36
  }
37
37
  async refresh() { // Refresh specified bucket using LocateNodes for a random key in the specified bucket's range.
38
38
  if (this.node.isStopped() || !this.contacts.length) return false; // fixme skip isStopped?
39
- this.node.ilog('refresh bucket', this.index);
40
39
  const targetKey = this.randomTarget;
41
40
  await this.node.locateNodes(targetKey); // Side-effect is to update this bucket.
42
41
  return true;
@@ -23,7 +23,7 @@ export class NodeStorage extends NodeRefresh {
23
23
  }
24
24
  async replicateCloserStorage(contact) { // Replicate to new contact any of our data for which contact is closer than us.
25
25
  for (const key in this.storage.keys()) {
26
- if (contact.distance(key) <= this.distance(key)) {
26
+ if (contact.connection && (contact.distance(key) <= this.distance(key))) {
27
27
  await contact.store(key, this.retrieveLocally(key));
28
28
  }
29
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yz-social/kdht",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Pure Kademlia base, for testing variations.",
5
5
  "exports": {
6
6
  ".": "./index.js",
@@ -14,10 +14,11 @@
14
14
  "background": "npm stop; (npm start 1>server.log 2>&1 &); sleep 1",
15
15
  "bots": "node spec/bots.js",
16
16
  "thrashbots": "node spec/bots.js --thrash",
17
- "test": "npx jasmine && npx jasmine spec/webrtcTests.js && echo '-- SUCCESS --' || echo '**** FAIL ****'"
17
+ "testWebrtc": "npx jasmine spec/testWebrtc.js",
18
+ "test": "npx jasmine && npm run testWebrtc && echo '-- SUCCESS --' || echo '**** FAIL ****'"
18
19
  },
19
20
  "dependencies": {
20
- "@yz-social/webrtc": "^0.1.2",
21
+ "@yz-social/webrtc": "^0.1.3",
21
22
  "uuid": "^13.0.0"
22
23
  },
23
24
  "devDependencies": {
package/portals/node.js CHANGED
@@ -3,14 +3,14 @@ import cluster from 'node:cluster';
3
3
  import { v4 as uuidv4 } from 'uuid';
4
4
  import { WebContact, Node } from '../index.js';
5
5
 
6
- export async function setup({baseURL, externalBaseURL = '', verbose, fixedSpacing, variableSpacing}) {
6
+ export async function setup({baseURL, externalBaseURL = '', debug, fixedSpacing, info, variableSpacing}) {
7
7
  const hostName = uuidv4();
8
8
  process.title = 'kdht-portal-' + hostName;
9
9
  // For debugging:
10
10
  // process.on('uncaughtException', error => console.error(hostName, 'Global uncaught exception:', error));
11
11
  // process.on('unhandledRejection', error => console.error(hostName, 'Global unhandled promise rejection:', error));
12
12
 
13
- const contact = await WebContact.create({name: hostName, isServerNode: true, info: false, debug: verbose});
13
+ const contact = await WebContact.create({name: hostName, isServerNode: true, info, debug});
14
14
  // Handle signaling that comes as a message from the server.
15
15
  process.on('message', async ([senderSname, ...incomingSignals]) => { // Signals from a sender through the server.
16
16
  const response = await contact.signals(senderSname, ...incomingSignals);
package/portals/router.js CHANGED
@@ -5,8 +5,7 @@ import { Node } from '../index.js';
5
5
  export const router = express.Router();
6
6
 
7
7
  const portals = {}; // Maps worker sname => worker, for the full lifetime of the program. NOTE: MAY get filed in out of order from workers.
8
- const workers = Object.values(cluster.workers);
9
- for (const worker of workers) {
8
+ function initWorker(worker) {
10
9
  worker.on('message', message => { // Message from a worker, in response to a POST.
11
10
  if (!worker.tag) { // The very first message from a worker (during setup) will identify its tag.
12
11
  portals[message] = worker;
@@ -23,8 +22,11 @@ for (const worker of workers) {
23
22
  }
24
23
  });
25
24
  }
26
- cluster.on('exit', (worker, code, signal) => { // Tell us about dead workers.
25
+ Object.values(cluster.workers).forEach(initWorker);
26
+ cluster.on('exit', (worker, code, signal) => { // Tell us about dead workers and restart them.
27
27
  console.error(`\n\n*** Crashed worker ${worker.id}:${worker.tag} received code: ${code} signal: ${signal}. ***\n`);
28
+ delete worker.tag;
29
+ initWorker(cluster.fork());
28
30
  });
29
31
 
30
32
  router.get('/name/:label', (req, res, next) => { // Answer the actual sname corresponding to label.
package/spec/bots.js CHANGED
@@ -17,7 +17,7 @@ const argv = yargs(hideBin(process.argv))
17
17
  alias: 'n',
18
18
  alias: 'nbots',
19
19
  type: 'number',
20
- default: Math.max(logicalCores / 2, 2),
20
+ default: logicalCores,
21
21
  description: "The number of bots, which can only be reached through the network."
22
22
  })
23
23
  .option('baseURL', {
@@ -37,9 +37,16 @@ const argv = yargs(hideBin(process.argv))
37
37
  default: 0,
38
38
  description: "The number of test writes to check."
39
39
  })
40
+ .option('info', {
41
+ alias: 'i',
42
+ type: 'boolean',
43
+ default: true,
44
+ description: "Run with info logging."
45
+ })
40
46
  .option('verbose', {
41
47
  alias: 'v',
42
48
  type: 'boolean',
49
+ default: false,
43
50
  description: "Run with verbose logging."
44
51
  })
45
52
  .parse();
@@ -48,21 +55,24 @@ const host = uuidv4();
48
55
  process.title = 'kdht-bot-' + host;
49
56
 
50
57
  if (cluster.isPrimary) {
51
- console.log(`${cpus()[0].model}, ${logicalCores} logical cores. Starting ${argv.nBots} over ${Node.refreshTimeIntervalMS/1000} seconds.`);
58
+ console.log(`${cpus()[0].model}, ${logicalCores} logical cores. Starting ${argv.nBots} ${argv.thrash ? 'thrashbots' : 'bots'} over ${Node.refreshTimeIntervalMS/1000} seconds.`);
52
59
  for (let i = 1; i < argv.nBots; i++) { // The cluster primary becomes bot 0.
53
60
  cluster.fork();
54
61
  }
62
+ cluster.on('exit', (worker, code, signal) => { // Tell us about dead workers and restart them.
63
+ console.error(`\n\n*** Crashed worker ${worker.id}:${worker.tag} received code: ${code} signal: ${signal}. ***\n`);
64
+ cluster.fork();
65
+ });
55
66
  if (argv.nWrites) {
56
67
  console.log(new Date(), 'Waiting a refresh interval while bots get randomly created before write/read test');
57
68
  await Node.delay(2 * Node.refreshTimeIntervalMS);
58
69
  launchWriteRead(argv.nWrites, argv.baseURL, Node.refreshTimeIntervalMS, argv.verbose);
59
- }
70
+ }
60
71
  }
61
72
 
62
- const info = false;
63
73
  await Node.delay(Node.randomInteger(Node.refreshTimeIntervalMS));
64
74
  console.log(cluster.worker?.id || 0, host);
65
- let contact = await WebContact.create({name: host, info, debug: argv.verbose});
75
+ let contact = await WebContact.create({name: host, info: argv.info, debug: argv.verbose});
66
76
  let bootstrapName = await contact.fetchBootstrap(argv.baseURL);
67
77
  let bootstrapContact = await contact.ensureRemoteContact(bootstrapName, argv.baseURL);
68
78
  await contact.join(bootstrapContact);
@@ -80,7 +90,7 @@ while (argv.thrash) {
80
90
  await contact.disconnect();
81
91
  await Node.delay(1e3); // TODO: remove?
82
92
 
83
- contact = await WebContact.create({name: next, info, debug: argv.verbose});
93
+ contact = await WebContact.create({name: next, info: argv.info, debug: argv.verbose});
84
94
  bootstrapName = await contact.fetchBootstrap(argv.baseURL);
85
95
  bootstrapContact = await contact.ensureRemoteContact(bootstrapName, argv.baseURL);
86
96
  await contact.join(bootstrapContact);
package/spec/portal.js CHANGED
@@ -21,7 +21,7 @@ const argv = yargs(hideBin(process.argv))
21
21
  alias: 'nportals',
22
22
  alias: 'p',
23
23
  type: 'number',
24
- default: Math.max(logicalCores / 2, 2),
24
+ default: Math.max(2, logicalCores - 1),
25
25
  description: "The number of steady nodes that handle initial connections."
26
26
  })
27
27
  .option('nBots', {
@@ -52,9 +52,16 @@ const argv = yargs(hideBin(process.argv))
52
52
  default: '',
53
53
  description: "The base URL of the some other portal server to which we should connect ours, if any."
54
54
  })
55
+ .option('info', {
56
+ alias: 'i',
57
+ type: 'boolean',
58
+ default: true,
59
+ description: "Run with info logging."
60
+ })
55
61
  .option('verbose', {
56
62
  alias: 'v',
57
63
  type: 'boolean',
64
+ default: false,
58
65
  description: "Run with verbose logging."
59
66
  })
60
67
  .option('fixedSpacing', {
@@ -118,6 +125,6 @@ if (cluster.isPrimary) { // Parent process with portal webserver through which c
118
125
 
119
126
  } else { // A portal node through which client's can connect.
120
127
  const portalNode = await import('../portals/node.js');
121
- const {baseURL, externalBaseURL, fixedSpacing, variableSpacing, verbose} = argv;
122
- await portalNode.setup({baseURL, externalBaseURL, fixedSpacing, variableSpacing, verbose});
128
+ const {baseURL, externalBaseURL, fixedSpacing, variableSpacing, info, verbose} = argv;
129
+ await portalNode.setup({baseURL, externalBaseURL, fixedSpacing, variableSpacing, info, debug: verbose});
123
130
  }
@@ -10,18 +10,19 @@ import path from 'path';
10
10
 
11
11
  describe("DHT write/read", function () {
12
12
  let contact, portalProcess, botProcess;
13
+ const botInfo = false;
13
14
  const verbose = false;
15
+ const testNodeVerbose = verbose;
14
16
  const baseURL = 'http://localhost:3000/kdht';
15
17
  const logicalCores = availableParallelism();
16
18
  console.log(`Model description "${cpus()[0].model}", ${logicalCores} logical cores.`);
17
- const maxPerCluster = logicalCores / 2; // Why half? Because we have at least two processes, and clusters of logicalCores size tend to go... off.
18
- const nPortals = maxPerCluster;
19
- const nBots = maxPerCluster;
19
+ const nPortals = Math.max(2, logicalCores - 1);
20
+ const thrash = false;
21
+ const nBots = Math.max(2, (thrash ? 0.5 : 1) * logicalCores);
20
22
  const fixedSpacing = 2; // Between portals.
21
23
  const variableSpacing = 5; // Additional random between portals.
22
24
  const nWrites = 40;
23
25
  const waitBeforeRead = 15e3;
24
- const thrash = true;
25
26
  const showPortals = true;
26
27
  const showBots = true;
27
28
 
@@ -35,7 +36,7 @@ describe("DHT write/read", function () {
35
36
  function echo(data) { data = data.slice(0, -1); console.log(data.toString()); }
36
37
 
37
38
  console.log(new Date(), 'starting', nPortals, 'portals over', portalSeconds, 'seconds');
38
- portalProcess = spawn('node', [path.resolve(__dirname, 'portal.js'), '--nPortals', nPortals, '--verbose', verbose.toString()]);
39
+ portalProcess = spawn('node', [path.resolve(__dirname, 'portal.js'), '--nPortals', nPortals, '--info', botInfo, '--verbose', verbose.toString()]);
39
40
  if (showPortals) {
40
41
  portalProcess.stdout.on('data', echo);
41
42
  portalProcess.stderr.on('data', echo);
@@ -43,18 +44,16 @@ describe("DHT write/read", function () {
43
44
  await Node.delay(portalSeconds * 1e3);
44
45
 
45
46
  if (nBots) {
46
- for (let launched = 0, round = Math.min(nBots, maxPerCluster); launched < nBots; round = Math.min(nBots - launched, maxPerCluster), launched += round) {
47
- console.log(new Date(), 'starting', round, 'bots over', botsMilliseconds/1e3, 'seconds');
48
- botProcess = spawn('node', [path.resolve(__dirname, 'bots.js'), '--nBots', round, '--thrash', thrash.toString(), '--verbose', verbose.toString()]);
49
- if (showBots) {
50
- botProcess.stdout.on('data', echo);
51
- botProcess.stderr.on('data', echo);
52
- }
53
- await Node.delay(botsMilliseconds);
47
+ console.log(new Date(), 'starting', nBots, thrash ? 'thrashbots' : 'bots', 'over', botsMilliseconds/1e3, 'seconds');
48
+ botProcess = spawn('node', [path.resolve(__dirname, 'bots.js'), '--nBots', nBots, '--thrash', thrash.toString(), '--info', botInfo, '--verbose', verbose.toString()]);
49
+ if (showBots) {
50
+ botProcess.stdout.on('data', echo);
51
+ botProcess.stderr.on('data', echo);
54
52
  }
53
+ await Node.delay(botsMilliseconds);
55
54
  }
56
55
 
57
- contact = await WebContact.create({name: uuidv4(), debug: verbose});
56
+ contact = await WebContact.create({name: uuidv4(), debug: testNodeVerbose});
58
57
  const bootstrapName = await contact.fetchBootstrap(baseURL);
59
58
  const bootstrapContact = await contact.ensureRemoteContact(bootstrapName, baseURL);
60
59
  console.log(new Date(), 'client node', contact.sname, 'joining', bootstrapContact.sname);
@@ -69,7 +68,7 @@ describe("DHT write/read", function () {
69
68
  await Node.delay(waitBeforeRead);
70
69
  }
71
70
  console.log(new Date(), 'Reading');
72
- }, 5e3 * nWrites + (1 + Math.ceil(nBots / maxPerCluster)) * Node.refreshTimeIntervalMS);
71
+ }, 5e3 * nWrites + (1 + nBots) * Node.refreshTimeIntervalMS);
73
72
  afterAll(async function () {
74
73
  contact.disconnect();
75
74
  console.log(new Date(), 'killing portals and bots');
@@ -123,7 +123,7 @@ export class Contact {
123
123
  distance(key) { return this.host.constructor.distance(this.key, key); }
124
124
 
125
125
  // RPC
126
- static maxPingMs = 330; // Not including connect time. These are single-hop WebRTC data channels.
126
+ static maxPingMS = 330; // Not including connect time. These are single-hop WebRTC data channels.
127
127
  serializeRequest(...rest) { // Return the composite datum suitable for transport over the wire.
128
128
  return rest; // Non-simulation subclases must override.
129
129
  }
@@ -140,7 +140,7 @@ export class Contact {
140
140
  let hops = 15; // recursive calls
141
141
  if (method === 'signals') hops = 2;
142
142
  else if (['ping', 'findNodes', 'findValue', 'store'].includes(method)) hops = 1;
143
- return Node.delay(hops * this.constructor.maxPingMs, null);
143
+ return Node.delay(hops * this.constructor.maxPingMS, null);
144
144
  }
145
145
  async sendRPC(method, ...rest) { // Promise the result of a network call to node, or null if not possible.
146
146
  const sender = this.host.contact;
@@ -208,6 +208,7 @@ export class Contact {
208
208
  }
209
209
 
210
210
  // Signaling
211
+ static forwardingTimeoutMS = 3/2 * this.maxPingMS - 0.2 * this.maxPingMS;
211
212
  async messageSignals(signals) { // send signals through the network, promising the response signals.
212
213
  // If contact cannot be reached, remove it and promise [].
213
214
  if (this.host.isStopped()) return [];
@@ -235,14 +236,15 @@ export class Contact {
235
236
 
236
237
  if (this.host.isStopped()) return [];
237
238
  if (this.node.isRunning)
238
- this.host.ilog('Using recursive signal routing to', this.sname, 'after trying', sponsors.length, 'sponsors.');
239
+ this.host.log('Using recursive signal routing to', this.sname, 'after trying', sponsors.length, 'sponsors.');
239
240
 
240
241
  const start = Date.now();
241
- const {forwardingExclusions, result} = (await this.host.recursiveSignals(this.key, payload, [], Date.now + this.constructor.forwardingTimeoutMS, this.name)) || {};
242
+ const response = await this.host.recursiveSignals(this.key, payload, [], Date.now + this.constructor.forwardingTimeoutMS, this.name);
243
+ const {forwardingExclusions, result} = response || {};
242
244
  const elapsed = Date.now() - start;
243
245
  if (!!this.isRunning !== !!result) // Of course, only simulations can really know isRunning to be false.
244
- this.host.ilog('Recursive response', !!result, 'to', this.isRunning ? 'running' : 'disconnected', this.sname,
245
- 'in', forwardingExclusions?.length, 'steps over',
246
+ this.host.ilog('Recursive', response ? 'data from' : 'failure from', this.sname,
247
+ 'in', forwardingExclusions?.length || 'unknown', 'steps over',
246
248
  elapsed, 'ms, after trying',
247
249
  sponsors.length, 'sponsors.',
248
250
  );
@@ -263,8 +265,7 @@ export class Contact {
263
265
  //return `${this.connection ? '_' : ''}${this.sname}v${this.counter}${this.isRunning ? '' : '*'}`;
264
266
  return `${this.connection ? '_' : ''}${this.sname}${this.isRunning ? '' : '*'}`; // simpler version
265
267
  }
266
- static forwardingTimeoutMS = 3 * this.maxPingMS / 2 - 0.2 * this.maxPingMS;
267
- static pingTimeMS = 40; // ms
268
+ static pingTimeMS = 40; // ms to consume each RPC in simulations
268
269
  static async ensureTime(thunk, ms = this.pingTimeMS) { // Promise that thunk takes at least ms to execute.
269
270
  const start = Date.now();
270
271
  const result = await thunk();
@@ -12,8 +12,9 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
12
12
  get isServerNode() { return this.node.isServerNode; } // It it reachable through a server.
13
13
 
14
14
  checkResponse(response) { // Return a fetch response, or throw error if response is not a 200 series.
15
- if (!response) return;
16
- if (!response.ok) throw new Error(`fetch ${response.url} failed ${response.status}: ${response.statusText}.`);
15
+ if (response?.ok) return true;
16
+ this.host.xlog(`*** Unable to reach portal ${response?.url || this.sname}, ${response?.status || 'failed fetch'}: ${response?.statusText || 'Unknown reason'}. ***`);
17
+ return false;
17
18
  }
18
19
  // connection:close is far more robust against pooling issues common to some implementations (e.g., NodeJS).
19
20
  // https://github.com/nodejs/undici/issues/3492
@@ -21,7 +22,7 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
21
22
  // worker index or the string 'random' to an available sname to which we can connect().
22
23
  const url = `${baseURL}/name/${label}`;
23
24
  const response = await fetch(url, {headers: { 'Connection': 'close' } }).catch(e => this.host.xlog(e));
24
- this.checkResponse(response);
25
+ if (!this.checkResponse(response)) return this.fetchBootstrap(baseURL, label);
25
26
  return await response.json();
26
27
  }
27
28
  async fetchSignals(url, signalsToSend) {
@@ -30,7 +31,7 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
30
31
  headers: { 'Content-Type': 'application/json', 'Connection': 'close' },
31
32
  body: JSON.stringify(signalsToSend)
32
33
  }).catch(e => this.host.xlog(e));
33
- this.checkResponse(response);
34
+ if (!this.checkResponse(response)) return this.fetchSignals(url, signalsToSend);
34
35
  return this.checkSignals(await response?.json());
35
36
  }
36
37
  async signals(senderSname, ...signals) { // Accept directed WebRTC signals from a sender sname, creating if necessary the
@@ -38,18 +39,18 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
38
39
  //this.host.xlog('contact signals', senderSname, signals);
39
40
  let contact = await this.ensureRemoteContact(senderSname);
40
41
 
41
- if (contact.webrtc) return await contact.webrtc.respond(signals);
42
+ if (contact.webrtc?.pc) return await contact.webrtc.respond(signals);
42
43
 
43
44
  this.host.noteContactForTransport(contact);
44
- contact.ensureWebRTC();
45
+ contact.createWebRTC(false);
45
46
  return await contact.webrtc.respond(signals);
46
47
  }
47
48
  get webrtcLabel() {
48
49
  return `@${this.host.contact.sname} ==> ${this.sname}`;
49
50
  }
50
51
 
51
- ensureWebRTC(initiate = false, timeoutMS = this.host.timeoutMS || 30e3) { // Ensure we are connected, if possible.
52
- // If not already configured, sets up contact to have properties:
52
+ createWebRTC(initiate = false, timeoutMS = this.host.timeoutMS || 30e3) { // Ensure we are connected, if possible.
53
+ // Sets up contact to have properties:
53
54
  // - connection - a promise for an open webrtc data channel:
54
55
  // this.send(string) puts data on the channel
55
56
  // incomming messages are dispatched to receiveWebRTC(string)
@@ -73,7 +74,7 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
73
74
  this.webrtc = this.connection = this.unsafeData = null;
74
75
  };
75
76
  if (initiate) {
76
- if (bootstrapHost/* || isServerNode*/) {
77
+ if (bootstrapHost && !host.connections.length) {
77
78
  const url = `${bootstrapHost || 'http://localhost:3000/kdht'}/join/${host.contact.sname}/${this.sname}`;
78
79
  this.webrtc.transferSignals = signals => this.fetchSignals(url, signals);
79
80
  } else {
@@ -101,6 +102,7 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
101
102
  }
102
103
  const timerPromise = new Promise(expired => {
103
104
  timeout = setTimeout(async () => {
105
+ if (this.host.isStopped()) return;
104
106
  const now = Date.now();
105
107
  this.host.ilog('Unable to connect to', this.sname);
106
108
  // this.host.xlog('**** connection timeout', this.sname, now - start,
@@ -125,9 +127,8 @@ export class WebContact extends Contact { // Our wrapper for the means of contac
125
127
  // Anyone can connect to a server node using the server's connect endpoint.
126
128
  // Anyone in the DHT can connect to another DHT node through a sponsor.
127
129
  if (contact.connection) return contact.connection;
128
- contact.ensureWebRTC(true);
129
- await this.connection;
130
- return this.connection;
130
+ contact.createWebRTC(true);
131
+ return await this.connection;
131
132
  }
132
133
 
133
134
  async send(message) { // Promise to send through previously opened connection promise.