@yz-social/kdht 0.1.7 → 0.1.9

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
@@ -59,7 +59,7 @@ export class KBucket {
59
59
  return false;
60
60
  }
61
61
 
62
- async addContact(contact) { // Returns 'present' or 'added' if it was added to end within capacity, else false.
62
+ addContact(contact) { // Returns 'present' or 'added' if it was added to end within capacity, else false.
63
63
  // Resets refresh timer.
64
64
  this.node.constructor.assert(contact.node.key !== this.node.key, 'attempt to add self contact to bucket');
65
65
  let added = this.removeKey(contact.key, false) || 'added';
@@ -67,7 +67,7 @@ export class KBucket {
67
67
  if (this.isFull) {
68
68
  if (added === 'present') this.node.looseTransports.push(contact); // So no findContact will fail during ping. Should we instead serialize findContact?
69
69
  const head = this.contacts[0];
70
- if (await head.sendRPC('ping', head.key)) { // still alive
70
+ if (head.connection) { // still alive
71
71
  added = false; // New contact will not be added.
72
72
  contact = head; // Add head back, below.
73
73
  }
package/dht/node.js CHANGED
@@ -109,12 +109,11 @@ export class Node extends NodeProbe {
109
109
  this.ilog('joining', contact.sname);
110
110
  contact = this.ensureContact(contact);
111
111
  await contact.connect();
112
- await this.addToRoutingTable(contact);
112
+ this.addToRoutingTable(contact);
113
113
  await this.locateNodes(this.key); // Discovers between us and otherNode.
114
114
 
115
115
  // Refresh every bucket farther out than our closest neighbor.
116
116
  // I think(?) that this can be done by refreshing "just" the farthest bucket:
117
- //this.ensureBucket(this.constructor.keySize - 1).resetRefresh();
118
117
  await this.ensureBucket(this.constructor.keySize - 1).refresh();
119
118
  // But if it turns out to be necessary to explicitly refresh each next bucket in turn, this is how:
120
119
  // let started = false;
@@ -56,6 +56,10 @@ export class NodeContacts extends NodeTransports {
56
56
  });
57
57
  return contacts;
58
58
  }
59
+ get connections() {
60
+ return this.contacts.filter(contact => contact.connection)
61
+ .concat(this.looseTransports.filter(contact => contact.connection));
62
+ }
59
63
  contactDictionary = {}; // maps name => contact for lifetime of Node instance until removeContact.
60
64
  existingContact(name) { // Returns contact with the given name for this node, without searching buckets or looseTransports.
61
65
  return this.contactDictionary[name];
@@ -80,20 +84,14 @@ export class NodeContacts extends NodeTransports {
80
84
  if (sponsor) contact.noteSponsor(sponsor);
81
85
  return contact;
82
86
  }
83
- routingTableSerializer = Promise.resolve();
84
- queueRoutingTableChange(thunk) { // Promise to resolve thunk() -- after all previous queued thunks have resolved.
85
- return this.routingTableSerializer = this.routingTableSerializer.then(thunk);
86
- }
87
87
  removeContact(contact) { // Removes from node entirely if present, from looseTransports or bucket as necessary, returning bucket if that's where it was, else null.
88
- return this.queueRoutingTableChange(() => {
89
- delete this.contactDictionary[contact.name];
90
- const key = contact.key;
91
- if (this.removeLooseTransport(key)) return null;
92
- const bucketIndex = this.getBucketIndex(key);
93
- const bucket = this.routingTable.get(bucketIndex);
94
- // Host might not yet have added node or anyone else as contact for that bucket yet, so maybe no bucket.
95
- return bucket?.removeKey(key) ? bucket : null;
96
- });
88
+ delete this.contactDictionary[contact.name];
89
+ const key = contact.key;
90
+ if (this.removeLooseTransport(key)) return null;
91
+ const bucketIndex = this.getBucketIndex(key);
92
+ const bucket = this.routingTable.get(bucketIndex);
93
+ // Host might not yet have added node or anyone else as contact for that bucket yet, so maybe no bucket.
94
+ return bucket?.removeKey(key) ? bucket : null;
97
95
  }
98
96
  addToRoutingTable(contact) { // Promise contact, and add it to the routing table if room.
99
97
  if (contact.key === this.key) return null; // Do not add self.
@@ -102,18 +100,16 @@ export class NodeContacts extends NodeTransports {
102
100
  // we have already dropped the connection.
103
101
  //this.constructor.assert(contact.connection, 'Adding contact without connection', contact.report, 'in', this.contact.report);
104
102
 
105
- return this.queueRoutingTableChange(async () => {
106
- const bucketIndex = this.getBucketIndex(contact.key);
107
- const bucket = this.ensureBucket(bucketIndex);
103
+ const bucketIndex = this.getBucketIndex(contact.key);
104
+ const bucket = this.ensureBucket(bucketIndex);
108
105
 
109
- // Try to add to bucket
110
- const added = await bucket.addContact(contact);
111
- if (added !== 'present') { // Not already tracked in bucket.
112
- this.removeLooseTransport(contact.key); // Can't be in two places.
113
- this.queueWork(() => this.replicateCloserStorage(contact));
114
- }
115
- return added;
116
- });
106
+ // Try to add to bucket
107
+ const added = bucket.addContact(contact);
108
+ if (added !== 'present') { // Not already tracked in bucket.
109
+ this.removeLooseTransport(contact.key); // Can't be in two places.
110
+ this.replicateCloserStorage(contact); // Asynchronous, but don't wait for it here.
111
+ }
112
+ return added;
117
113
  }
118
114
  findClosestHelpers(targetKey, count = this.constructor.k) { // Answer count closest Helpers to targetKey, including ourself.
119
115
  if (!this.contact) return []; // Can happen while we are shutting down during a probe.
@@ -25,30 +25,65 @@ export class NodeMessages extends NodeContacts {
25
25
  if (value !== undefined) return {value};
26
26
  return this.findClosestHelpers(key);
27
27
  }
28
- async signals(key, signals, forwardingExclusions = false) {
29
- const origin = signals[0];
30
- //this.xlog(this.key, 'handling signals request for', {origin, key, signals, forwardingExclusions});
31
- //await this.constructor.delay(100); // fixme remove
32
- if (!this.isRunning) return null;
33
- if (this.key === key) return await this.contact.signals(...signals); // Yay, us!
28
+ async signals(key, signals, forwardingExclusions = null, targetNameForDebugging) {
29
+ // Handle an exchange of signals, with a response that may include {result, forwardingExclusions}. See code.
30
+
31
+ if (!this.isRunning) { // In case it happens in simulations.
32
+ //this.xlog('\n*** not running ***');
33
+ return null; //{forwardingExclusions}; // FIXME
34
+ }
35
+
36
+ // If the key is us, pass the signals to our home contact and respond with the WebRTC signals from the contact.
37
+ // (Subtle: the signals will contain the sender name. The handler in our home contact will create
38
+ // a new specific contact if necessary, and set up the WebRTC through that.)
39
+ // The forwardingExclusions are passed back, in case the sender wants to see the steps involved.
40
+ if (this.key === key) return {result: await this.contact.signals(...signals), forwardingExclusions};
41
+
42
+ // If we have a direct connection to the key, pass it on and answer what it tells us.
43
+ // (E.g., if we sponsored target for sender, we will have a direct connection that will answer as above.)
44
+ let contact = this.findContactByKey(key);
45
+ if (contact && contact.connection) {
46
+ forwardingExclusions?.push(this.name); // Keeps stats accurate if sender is examining paths.
47
+ const response = await contact.sendRPC('signals', key, signals, forwardingExclusions, targetNameForDebugging);
48
+ if (response) return response;
49
+ return {forwardingExclusions}; // Subtle: If it fails, return a definitive failure instead of just null.
50
+ }
34
51
 
35
- let contact = this.findContactByKey(key); // If we have the target as a contact, use it directly.
36
- if (!contact && !forwardingExclusions) return null;
37
- if (contact) return await contact.sendRPC('signals', key, signals, forwardingExclusions);
38
52
  // Forward recursively.
39
- const contacts = this.findClosestHelpers(key).map(helper => helper.contact);
53
+ if (forwardingExclusions) return await this.recursiveSignals(key, signals, forwardingExclusions, Contact.forwardingTimeoutMS, targetNameForDebugging);
54
+
55
+ // We were a sponsor but for a contact has since disconnected. We do not know if they are still connected to others.
56
+ //this.xlog('\n*** sponsored disconnected ***');
57
+ return {forwardingExclusions}; // FIXME: Is this definitively right, or should we answer null here?
58
+ }
59
+ static maxTries = Math.pow(this.alpha, 3); // alpha tries at each of three deep, or equivalent.
60
+ async recursiveSignals(key, signals, forwardingExclusions, expiration, targetNameForDebugging) { // Forward recursively.
61
+ // The target key may not be reachable from here (and might not even still be running).
62
+ // So bound our branching.
63
+ if (Date.now() > expiration) return null;
64
+ let remainingThisNode = this.constructor.alpha; // If it's good enough for probing, then it's good enough here.
65
+ if (forwardingExclusions.length > this.constructor.maxTries) {
66
+ this.xlog('abandoning wandering path towards', targetNameForDebugging, 'through', forwardingExclusions.join(', '));
67
+ return {forwardingExclusions};
68
+ }
69
+ const helpers = this.findClosestHelpers(key);
70
+ const contacts = helpers.map(helper => helper.contact);
40
71
  forwardingExclusions.push(this.name);
41
- //this.xlog('forwarding signals', {key, forwardingExclusions, contacts: contacts.map(c => c.sname)});
72
+
42
73
  for (const contact of contacts) {
43
- // this.xlog('contact', contact.sname, contact.isRunning ? 'running' : 'dead',
44
- // contact.connection ? 'connected': 'unconnected',
45
- // forwardingExclusions.includes(contact.name) ? 'excluded' : 'allowed');
74
+ if (!remainingThisNode--) break;
46
75
  if (!contact.isRunning) continue;
47
76
  if (!contact.connection) continue;
48
77
  if (forwardingExclusions.includes(contact.name)) continue;
49
- const response = await contact.sendRPC('signals', key, signals, forwardingExclusions);
50
- //this.xlog('got response from', contact.sname);
51
- if (response) return response;
78
+ this.constructor.assert(contact.key !== this.key, 'forwarding through self');
79
+ //this.xlog('forwarding through', contact.sname);
80
+ const response = await contact.sendRPC('signals', key, signals, forwardingExclusions, targetNameForDebugging);
81
+ if (response) {
82
+ return response;
83
+ } else { // No response at all: continue with further calls that exclude contact.
84
+ //this.xlog('No forwarding response from', contact.sname, );
85
+ forwardingExclusions.push(contact.name);
86
+ }
52
87
  }
53
88
  return null;
54
89
  }
@@ -58,8 +93,7 @@ export class NodeMessages extends NodeContacts {
58
93
  this.constructor.assert(typeof(method)==='string', 'no method', method, sender, rest);
59
94
  this.constructor.assert(sender instanceof Contact, 'no sender', method, sender, rest);
60
95
  this.constructor.assert(sender.host.key === this.key, 'sender', sender.host.name, 'not on receiver', this.name);
61
- // The sender exists, so add it to the routing table, but asynchronously so as to allow it to finish joining.
62
- this.addToRoutingTable(sender);
96
+ this.addToRoutingTable(sender); // sender exists, so add it to the routing table.
63
97
  if (!(method in this)) {
64
98
  this.xlog('Does not handle method', method);
65
99
  return null;
package/dht/nodeProbe.js CHANGED
@@ -18,10 +18,10 @@ export class NodeProbe extends NodeMessages {
18
18
  if (!results) { // disconnected
19
19
  if (trace) this.log(helper.name, '=> disconnected');
20
20
  this.log('removing unconnected contact', contact.sname);
21
- await this.removeContact(contact);
21
+ this.removeContact(contact);
22
22
  return null; // signal that there is *no* response from this contact - to distinguish from a response that confirms that the contact is alive, even if there are (after filtering) no new contacts to try.
23
23
  }
24
- await this.addToRoutingTable(contact); // Live node, so update bucket.
24
+ this.addToRoutingTable(contact); // Live node, so update bucket.
25
25
  // this.log('step added contact', contact.sname);
26
26
  if (this.constructor.isContactsResult(results)) { // Keep only those that we have not seen, and note the new ones we have.
27
27
  const rawResults = results;
@@ -55,6 +55,7 @@ export class NodeProbe extends NodeMessages {
55
55
  if (trace) this.log(`iterate: key=${targetKey}, finder=${finder}, k=${k}`);
56
56
 
57
57
  if (targetKey !== this.key) {
58
+ // Schedule a refresh for the targetKey's bucket.
58
59
  const bucketIndex = this.getBucketIndex(targetKey);
59
60
  const bucket = this.routingTable.get(bucketIndex);
60
61
  // Subtle: if we don't have one now, but will after, refreshes will be rescheduled by KBucket constructor.
@@ -118,7 +119,7 @@ export class NodeProbe extends NodeMessages {
118
119
  return null;
119
120
  };
120
121
 
121
- // Handler for when a request completes. result is only expected if status='responded'.
122
+ // Handler for when a request completes. a non-null result is only expected if status='responded'.
122
123
  const handleCompletion = (helper, status, result) => {
123
124
  if (iterationFinished) return; // too late
124
125
 
@@ -166,7 +167,7 @@ export class NodeProbe extends NodeMessages {
166
167
 
167
168
  // Result is array of Helpers (may be empty if node had no new contacts)
168
169
  // Merge new helpers into allNodesSeen and track progress
169
- if (result?.length > 0) {
170
+ if (result.length > 0) {
170
171
  allNodesSeen.push(...result);
171
172
  allNodesSeen.sort(Helper.compare); // Keep sorted by distance (best-first).
172
173
  responsesWithoutNewNodes = 0; // reset counter
@@ -226,7 +227,7 @@ export class NodeProbe extends NodeMessages {
226
227
  }
227
228
 
228
229
  this.step(targetKey, finder, helper, keysSeen, trace)
229
- .then(result => handleCompletion(helper, 'responded', result))
230
+ .then(result => handleCompletion(helper, result ? 'responded' : 'disconnected', result))
230
231
  .catch(err => {
231
232
  // Handle errors - treat as disconnected
232
233
  handleCompletion(helper, 'disconnected');
@@ -30,10 +30,6 @@ export class NodeRefresh extends NodeKeys {
30
30
  const adjustment = this.randomInteger(margin);
31
31
  return Math.floor(target + margin/2 - adjustment);
32
32
  }
33
- workQueue = Promise.resolve();
34
- queueWork(thunk) { // Promise to resolve thunk() -- after all previous queued thunks have resolved.
35
- return this.workQueue = this.workQueue.then(thunk);
36
- }
37
33
  timers = new Map();
38
34
  schedule(timerKey, statisticsKey, thunk, timeout = this.fuzzyInterval()) {
39
35
  // Schedule thunk() to occur at a fuzzyInterval from now, cancelling any
@@ -46,16 +42,14 @@ export class NodeRefresh extends NodeKeys {
46
42
  const start = Date.now();
47
43
  clearInterval(this.timers.get(timerKey));
48
44
  this.timers.set(timerKey, setTimeout(async () => {
49
- const lag = Date.now() - start - timeout;
45
+ const now = Date.now();
46
+ const elapsed = now - start;
47
+ const lag = elapsed - timeout;
50
48
  this.timers.delete(timerKey);
51
49
  if (this.isStopped()) return;
50
+ this.ilog('refresh', statisticsKey, timerKey, 'last/lag ms:', elapsed.toLocaleString(), lag.toLocaleString());
52
51
  if (lag > 250) console.log(`** System is overloaded by ${lag.toLocaleString()} ms. **`);
53
- // Each actual thunk execution is serialized: Each Node executes its OWN various refreshes and probes
54
- // one at a time. This prevents a node from self-DoS'ing, but of course it does not coordinate across
55
- // nodes. If the system is bogged down for any reason, then the timeout spacing will get smaller
56
- // until finally the node is just running flat out.
57
- // this.log('queue', statisticsKey, timerKey, timeout);
58
- await this.queueWork(thunk);
52
+ await thunk();
59
53
  }, timeout));
60
54
  }
61
55
  }
@@ -14,6 +14,7 @@ export class NodeStorage extends NodeRefresh {
14
14
  // Claude.ai suggests just writing to the next in line, but that doesn't work.
15
15
  this.schedule(key, 'storage', () => {
16
16
  this.ilog('refresh value', value, 'at key', key);
17
+ // IF storeValue determines we are one of the nodes to store, then it will get scheduled again.
17
18
  this.storeValue(key, value);
18
19
  });
19
20
  }
@@ -22,7 +23,7 @@ export class NodeStorage extends NodeRefresh {
22
23
  }
23
24
  async replicateCloserStorage(contact) { // Replicate to new contact any of our data for which contact is closer than us.
24
25
  for (const key in this.storage.keys()) {
25
- if (contact.distance(key) <= this.distance(key)) { //fixme define distance on contact
26
+ if (contact.distance(key) <= this.distance(key)) {
26
27
  await contact.store(key, this.retrieveLocally(key));
27
28
  }
28
29
  }
@@ -1,4 +1,5 @@
1
1
  import { NodeStorage } from './nodeStorage.js';
2
+ import { WebRTC } from '@yz-social/webrtc';
2
3
 
3
4
  // Management of Contacts that have a limited number of connections that can transport messages.
4
5
  export class NodeTransports extends NodeStorage {
@@ -16,7 +17,7 @@ export class NodeTransports extends NodeStorage {
16
17
  }
17
18
  return false;
18
19
  }
19
- static maxTransports = 62; // FIXME: try 124
20
+ static maxTransports = WebRTC.suggestedInstancesLimit;
20
21
  noteContactForTransport(contact) { // We're about to use this contact for a message, so keep track of it.
21
22
  // Requires: if we later addToRoutingTable successfully, it should be removed from looseTransports.
22
23
  // Requires: if we later remove contact because of a failed send, it should be removed from looseTransports.
@@ -36,7 +37,7 @@ export class NodeTransports extends NodeStorage {
36
37
  }
37
38
  let dropped = removeLast(this.looseTransports);
38
39
  if (dropped) {
39
- //console.log(this.name, 'dropping loose transport', dropped.name);
40
+ this.xlog('dropping loose transport', dropped.name);
40
41
  } else { // Find the bucket with the most connections.
41
42
  let bestBucket = null, bestCount = 0;
42
43
  this.forEachBucket(bucket => {
@@ -47,8 +48,8 @@ export class NodeTransports extends NodeStorage {
47
48
  return true;
48
49
  });
49
50
  dropped = removeLast(bestBucket.contacts);
50
- if (!dropped) console.log('Unable to find something to drop in', this.report(null));
51
- //else console.log(this.name, 'dropping transport', dropped.name, 'in bucket', bestBucket.index, 'among', bestCount);
51
+ if (!dropped) this.xlog('Unable to find something to drop in', this.report(null));
52
+ else this.xlog('dropping transport', dropped.name, 'in bucket', bestBucket.index, 'among', bestCount, 'contacts.');
52
53
  }
53
54
  dropped.disconnectTransport();
54
55
  }
@@ -72,7 +72,7 @@ export class NodeUtilities {
72
72
  for (let index = 0; index < this.constructor.keySize; index++) {
73
73
  const bucket = this.routingTable.get(index);
74
74
  if (!bucket) continue;
75
- report += `\n ${index}: ` + (contactsString(bucket.contacts) || '-');
75
+ report += `\n ${index} (${bucket.contacts.length}): ` + (contactsString(bucket.contacts) || '-');
76
76
  }
77
77
  return logger ? logger(report) : report;
78
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yz-social/kdht",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Pure Kademlia base, for testing variations.",
5
5
  "exports": {
6
6
  ".": "./index.js",
@@ -14,10 +14,10 @@
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 && echo '-- SUCCESS --' || echo '**** FAIL ****'"
17
+ "test": "npx jasmine && npx jasmine spec/webrtcTests.js && echo '-- SUCCESS --' || echo '**** FAIL ****'"
18
18
  },
19
19
  "dependencies": {
20
- "@yz-social/webrtc": "^0.1.1",
20
+ "@yz-social/webrtc": "^0.1.2",
21
21
  "uuid": "^13.0.0"
22
22
  },
23
23
  "devDependencies": {
package/portals/node.js CHANGED
@@ -10,7 +10,7 @@ export async function setup({baseURL, externalBaseURL = '', verbose, fixedSpacin
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, debug: verbose});
13
+ const contact = await WebContact.create({name: hostName, isServerNode: true, info: false, debug: verbose});
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/spec/bots.js CHANGED
@@ -59,9 +59,10 @@ if (cluster.isPrimary) {
59
59
  }
60
60
  }
61
61
 
62
+ const info = false;
62
63
  await Node.delay(Node.randomInteger(Node.refreshTimeIntervalMS));
63
64
  console.log(cluster.worker?.id || 0, host);
64
- let contact = await WebContact.create({name: host, debug: argv.verbose});
65
+ let contact = await WebContact.create({name: host, info, debug: argv.verbose});
65
66
  let bootstrapName = await contact.fetchBootstrap(argv.baseURL);
66
67
  let bootstrapContact = await contact.ensureRemoteContact(bootstrapName, argv.baseURL);
67
68
  await contact.join(bootstrapContact);
@@ -79,7 +80,7 @@ while (argv.thrash) {
79
80
  await contact.disconnect();
80
81
  await Node.delay(1e3); // TODO: remove?
81
82
 
82
- contact = await WebContact.create({name: next, debug: argv.verbose});
83
+ contact = await WebContact.create({name: next, info, debug: argv.verbose});
83
84
  bootstrapName = await contact.fetchBootstrap(argv.baseURL);
84
85
  bootstrapContact = await contact.ensureRemoteContact(bootstrapName, argv.baseURL);
85
86
  await contact.join(bootstrapContact);
@@ -9,7 +9,7 @@ const { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach} = glob
9
9
  // implementation changes for different DHTs.
10
10
  import { setupServerNodes, shutdownServerNodes,
11
11
  start1, setupClientsByTime, shutdownClientNodes,
12
- getContacts, getRandomLiveContact,
12
+ getContacts, readThroughRandom,
13
13
  startThrashing, write1, read1, Node, Contact } from './dhtImplementation.js';
14
14
 
15
15
  // Some definitions:
@@ -48,7 +48,7 @@ async function timed(operation, logString) {
48
48
  await operation(startTime);
49
49
  const endTime = Date.now();
50
50
  const elapsed = endTime - startTime;
51
- console.log(await logString(elapsed/1e3));
51
+ console.log(new Date(), await logString(elapsed/1e3));
52
52
  return elapsed;
53
53
  }
54
54
 
@@ -74,41 +74,17 @@ function delay(ms, label = '') {
74
74
  }, ms));
75
75
  }
76
76
 
77
- export async function startWrite1(name, bootstrapContact, refreshTimeIntervalMS, index) {
78
- return await start1(name, bootstrapContact, refreshTimeIntervalMS)
79
- .then(contact => write1(contact, name, name));
80
- }
81
-
82
- async function awaitNonNullContact(contacts, i) { // Kind of stupid...
83
- // When a contact thrashes, its contact[i] is null for a moment.
84
- // TODO Alt: When thrashing, set slot to a promise and await it in all references, rather than this.
85
- let contact = contacts[i];
86
- if (contact) return contact;
87
- await delay(50);
88
- return await awaitNonNullContact(contacts, i);
89
- }
90
-
91
77
  async function parallelWriteAll() {
92
78
  // Persist a unique string through each contact all at once, but not resolving until all are ready.
93
79
  const contacts = await getContacts();
94
80
  // The key and value are the same, to facilitate read confirmation.
95
- const writes = await Promise.all(contacts.map(async (contact, index) => {
96
- let ok;
97
- for (let i = 0; i < 10; i++) {
98
- contact ||= await awaitNonNullContact(contacts, index);
99
- ok = await write1(contact, index, index);
100
- if (ok) break;
101
- contact = contacts[index];
102
- }
103
- // It's ok if the contact disconnect right after, as long the write succeeded.
104
- expect(ok).toBeTruthy();
105
- }));
81
+ const writes = await Promise.all(contacts.map(async (contact, index) => write1(contact, index, index)));
106
82
  return writes.length;
107
83
  }
108
84
  async function serialWriteAll() { // One-at-atime alternative to above, useful for debugging.
109
85
  const contacts = await getContacts();
110
86
  for (let index = 0; index < contacts.length; index++) {
111
- const ok = await write1(await awaitNonNullContact(contacts, index), index, index);
87
+ const ok = await write1(contacts[index], index, index);
112
88
  expect(ok).toBeTruthy();
113
89
  }
114
90
  return contacts.length;
@@ -117,14 +93,7 @@ async function parallelReadAll(start = 0) {
117
93
  // Reads from a random contact, confirming the value, for each key written by writeAll.
118
94
  const contacts = await getContacts();
119
95
  const readPromises = await Promise.all(contacts.map(async (_, index) => {
120
- if (index < start) return;
121
- let value;
122
- for (let liveTry = 0; liveTry < 4; liveTry++) {
123
- const contact = await getRandomLiveContact();
124
- value = await read1(contact, index);
125
- if (contact.isRunning) break;
126
- console.log('\n\n*** read from dead contact. retrying', liveTry, '***\n');
127
- }
96
+ const value = await readThroughRandom(index);
128
97
  expect(value).toBe(index);
129
98
  }));
130
99
  return readPromises.length - start;
@@ -132,7 +101,7 @@ async function parallelReadAll(start = 0) {
132
101
  async function serialReadAll() { // One-at-a-time alternative of above, useful for debugging.
133
102
  const contacts = await getContacts();
134
103
  for (let index = 0; index < contacts.length; index++) {
135
- const value = await read1(await getRandomLiveContact(), index);
104
+ const value = await readThroughRandom(index);
136
105
  expect(value).toBe(index);
137
106
  }
138
107
  return contacts.length;
@@ -156,25 +125,25 @@ describe("DHT", function () {
156
125
 
157
126
  describe(notes || suiteLabel, function () {
158
127
  beforeAll(async function () {
159
- console.log('\n' + suiteLabel);
128
+ console.log('\n', new Date(), suiteLabel);
160
129
  if (notes) console.log(notes);
161
130
  await delay(3e3); // For gc
162
131
  await timed(_ => setupServerNodes(nServerNodes, refreshTimeIntervalMS, pingTimeMS, maxTransports),
163
132
  elapsed => `Server setup ${nServerNodes} / ${elapsed} = ${Math.round(nServerNodes/elapsed)} nodes/second.`);
164
133
  expect(await getContactsLength()).toBe(nServerNodes); // sanity check
165
- //console.log('end server setup');
166
- });
134
+ console.log(new Date(), 'end server setup');
135
+ }, 10e3);
167
136
  afterAll(async function () {
168
- //console.log('start server shutdown');
137
+ console.log(new Date(), 'start server shutdown');
169
138
  await shutdownServerNodes(nServerNodes);
170
139
  expect(await getContactsLength()).toBe(0); // sanity check
171
- //console.log('end server shutdown');
140
+ console.log(new Date(), 'end server shutdown');
172
141
  }, 20e3);
173
142
 
174
143
  describe("joins within a refresh interval", function () {
175
144
  let nJoined = 0, nWritten = 0;
176
145
  beforeAll(async function () {
177
- //console.log('start client setup');
146
+ console.log(new Date(), 'start client setup');
178
147
  if (startThrashingBefore === 'creation') await startThrashing(nServerNodes, refreshTimeIntervalMS);
179
148
  let elapsed = await timed(async _ => nJoined = await setupClientsByTime(refreshTimeIntervalMS, nServerNodes, maxClientNodes, setupTimeMS),
180
149
  elapsed => `Created ${nJoined} / ${elapsed} = ${(elapsed/nJoined).toFixed(3)} client nodes/second.`);
@@ -182,16 +151,16 @@ describe("DHT", function () {
182
151
  if (maxClientNodes < Infinity) expect(nJoined).toBe(maxClientNodes); // Sanity check
183
152
  if (startThrashingBefore === 'writing') await startThrashing(nServerNodes, refreshTimeIntervalMS);
184
153
  await delay(runtimeBeforeWriteMS, 'pause before writing');
185
- elapsed = await timed(async _ => nWritten = await parallelWriteAll(), // Alt: serialWriteAll
154
+ console.log(new Date(), 'writing');
155
+ elapsed = await timed(async _ => nWritten = await serialWriteAll(), // Alt: serial/parallelWriteAll
186
156
  elapsed => `Wrote ${nWritten} / ${elapsed} = ${Math.round(nWritten/elapsed)} nodes/second.`);
187
- //console.log('end client setup');
188
- }, setupTimeMS + runtimeBeforeWriteMS + runtimeBeforeWriteMS + 3 * setupTimeMS);
157
+ }, setupTimeMS + runtimeBeforeWriteMS + runtimeBeforeWriteMS + 5 * setupTimeMS);
189
158
  afterAll(async function () {
190
- //console.log('start client shutdown');
159
+ console.log(new Date(), 'start client shutdown');
191
160
  //await Node.reportAll();
192
161
  await shutdownClientNodes(nServerNodes, nJoined);
193
162
  expect(await getContactsLength()).toBe(nServerNodes); // Sanity check.
194
- //console.log('end client shutdown');
163
+ console.log(new Date(), 'end client shutdown');
195
164
  }, 20e3);
196
165
  it("produces.", async function () {
197
166
  const total = await getContactsLength();
@@ -201,8 +170,9 @@ describe("DHT", function () {
201
170
  it("can be read.", async function () {
202
171
  if (startThrashingBefore === 'reading') await startThrashing(nServerNodes, refreshTimeIntervalMS);
203
172
  await delay(runtimeBeforeReadMS, 'pause before reading');
173
+ console.log(new Date(), 'reading');
204
174
  let nRead = 0;
205
- await timed(async _ => nRead = await parallelReadAll(), // alt: serialReadAll
175
+ await timed(async _ => nRead = await serialReadAll(), // alt: serial/parallelReadAll
206
176
  elapsed => `Read ${nRead} / ${elapsed} = ${Math.round(nRead/elapsed)} values/second.`);
207
177
  expect(nRead).toBe(nWritten);
208
178
  }, 10 * setupTimeMS + 5 * runtimeBeforeReadMS);
@@ -217,6 +187,7 @@ describe("DHT", function () {
217
187
  test({setupTimeMS: 1e3, pingTimeMS: 0, startThrashingBefore: 'never', notes: "Probing on, but no disconnects or network delay."});
218
188
  test({pingTimeMS: 0, refreshTimeIntervalMS: 5e3, notes: "Small networks allow faster thrash smoke-testing."});
219
189
  test({notes: "Normal ops"});
190
+ test({setupTimeMS: 40e3, notes: "Bigger network overflowing bucket."});
220
191
 
221
192
  // test({maxClientNodes: 55, setupTimeMS: 240e3, pingTimeMS: 40, maxTransports: 62,
222
193
  // //startThrashingBefore: 'never', runtimeBeforeWriteMS: 0, runtimeBeforeReadMS: 0,