@yz-social/kdht 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
1
+ import { Node, KBucket, Contact, SimulatedContact, Helper } from '../index.js';
2
+ const { describe, it, expect, beforeAll, afterAll, BigInt} = globalThis; // For linters.
3
+
4
+ describe("DHT internals", function () {
5
+ beforeAll(function () {
6
+ //Node.distinguisher = 0;
7
+ });
8
+ afterAll(function () {
9
+ Node.stopRefresh();
10
+ });
11
+
12
+ describe("structure", function () {
13
+ let example;
14
+ beforeAll(async function () {
15
+ const contact = await SimulatedContact.create(0);
16
+ example = contact.node;
17
+ });
18
+ it("has key.", function () {
19
+ expect(typeof example.key).toBe('bigint');
20
+ });
21
+ describe("local storage", function () {
22
+ it("stores by Identifier key.", async function () {
23
+ let key = await Node.key("foo");
24
+ let value = 17;
25
+ example.storeLocally(key, value);
26
+ let retrieved = example.retrieveLocally(key);
27
+ expect(retrieved).toBe(value);
28
+ });
29
+ it("retrieves undefined if not set.", async function () {
30
+ let key = await Node.key("not seen");
31
+ let retrieved = example.retrieveLocally(key);
32
+ expect(retrieved).toBeUndefined();
33
+ });
34
+ });
35
+
36
+ describe("report", function () {
37
+ beforeAll(async function () { // Add some data for which we know the expected internal structure.
38
+ example.storeLocally(await Node.key("foo"), 17); // May or may not have already been set to same value, depending on test order.
39
+ example.storeLocally(await Node.key("bar"), "baz");
40
+ const contact = await SimulatedContact.create();
41
+ const node = contact.node;
42
+ let bucket = new KBucket(node, 90); // 90 isn't correct, but we're just looking at the structure.
43
+ bucket.contacts.push(await SimulatedContact.fromKey(1, node));
44
+ bucket.contacts.push(await SimulatedContact.fromKey(2, node));
45
+ example.routingTable.set(90, bucket);
46
+ });
47
+ afterAll(function () {
48
+ example.routingTable.delete(90);
49
+ });
50
+ it("includes name, routing names and stored items by bigInt key.", function () {
51
+ let report = example.report(string => string); // No op for what to do with the report. Just return it.
52
+ expect(report).toBe(`Node: 0, 0 transports
53
+ storing 2: 58686998438798322974467776505749455156n: 17, 336119020696479164089214630533760195420n: "baz"
54
+ 90: 1n, 2n`);
55
+ });
56
+ });
57
+
58
+ describe("constants", function () {
59
+ it("alpha >= 3.", function () {
60
+ expect(Node.alpha).toBeGreaterThanOrEqual(3);
61
+ });
62
+ it("k >= 10.", function () {
63
+ expect(Node.k).toBeGreaterThanOrEqual(10);
64
+ });
65
+ });
66
+ });
67
+
68
+ describe("operations", function () {
69
+ const one = 1n;
70
+ const two = 2n;
71
+ const three = 3n;
72
+ const max = Node.one << BigInt(Node.keySize);
73
+ describe("commonPrefixLength", function () {
74
+ it("is keySize for 0n.", function () {
75
+ expect(Node.commonPrefixLength(Node.zero)).toBe(Node.keySize);
76
+ });
77
+ it("is keySize - 1 for 1n.", function () {
78
+ expect(Node.commonPrefixLength(Node.one)).toBe(Node.keySize - 1);
79
+ });
80
+ it("is 1 for (keySize - 1) ones.", function () {
81
+ expect(Node.commonPrefixLength(BigInt("0b" + "1".repeat(Node.keySize-1)))).toBe(1);
82
+ });
83
+ it("is 0 for keySize ones.", function () {
84
+ expect(Node.commonPrefixLength(BigInt("0b" + "1".repeat(Node.keySize)))).toBe(0);
85
+ });
86
+ });
87
+ describe("getBucketIndex", function () {
88
+ let node;
89
+ beforeAll(function () {
90
+ node = Node.fromKey(Node.zero);
91
+ });
92
+ it("bucket keySize -1 is farthest.", function () {
93
+ const distance = max - Node.one; // max distance within nTagBits. All bits on.
94
+ expect(node.getBucketIndex(distance)).toBe(Node.keySize - 1);
95
+ });
96
+ it("bucket keySize - 2 is for middle distance.", function () {
97
+ const distance = (max / two) - Node.one;
98
+ expect(node.getBucketIndex(distance)).toBe(Node.keySize - 2);
99
+ });
100
+ it("bucket 1 is for a distance 2.", function () {
101
+ const distance = two;
102
+ expect(node.getBucketIndex(distance)).toBe(1);
103
+ });
104
+ it("bucket 0 is for a closest distance.", function () {
105
+ const distance = Node.one;
106
+ expect(node.getBucketIndex(distance)).toBe(0);
107
+ });
108
+ });
109
+ describe("randomTarget", function () {
110
+ let node;
111
+ beforeAll(async function () {
112
+ node = await Node.create();
113
+ });
114
+ function test(bucketIndex) {
115
+ it(`computes random of ${bucketIndex}.`, function () {
116
+ const random = node.ensureBucket(bucketIndex).randomTarget;
117
+ const computedBucket = node.getBucketIndex(random);
118
+ expect(computedBucket).toBe(bucketIndex);
119
+ });
120
+ }
121
+ for (let i = 0; i < Node.keySize; i++) test(i);
122
+ });
123
+
124
+ describe("examination", function () {
125
+ const keys = [];
126
+ let node;
127
+ beforeAll(async function () {
128
+ // Applications won't be hand-creating the routingTable, but this test does.
129
+ const contact = await SimulatedContact.create();
130
+ node = contact.node;
131
+ const bucket0 = new KBucket(node, 0);
132
+ const bucket10 = new KBucket(node, 10);
133
+ const bucket60 = new KBucket(node, 60);
134
+ const bucket90 = new KBucket(node, 90);
135
+ const addTo = async bucket => {
136
+ const key = bucket.randomTarget;
137
+ keys.push(key);
138
+ await bucket.addContact(SimulatedContact.fromKey(key, node));
139
+ };
140
+ await addTo(bucket0,);
141
+ await addTo(bucket10);
142
+ await addTo(bucket60);
143
+ await addTo(bucket90);
144
+ node.routingTable.set(0, bucket0);
145
+ node.routingTable.set(10, bucket10);
146
+ node.routingTable.set(60, bucket60);
147
+ node.routingTable.set(90, bucket90);
148
+ });
149
+ it("is initially empty.", async function () {
150
+ const node = await Node.create();
151
+ expect(node.contacts).toEqual([]);
152
+ });
153
+ it("collects from all buckets.", function () {
154
+ const contacts = node.contacts;
155
+ const asKeys = contacts.map(c => c.key);
156
+ expect(asKeys).toEqual(keys);
157
+ });
158
+ it("finds all ordered keys there are.", function () {
159
+ let target = node.key;
160
+ let all = [node.key, ...keys]; // Our findClosestHelpers includes ourself.
161
+ let keysAndDistances = all.map(key => ({key, distance: Node.distance(target, key)}));
162
+ keysAndDistances.sort(Helper.compare);
163
+ const closest = node.findClosestHelpers(target);
164
+ const mapped = closest.map(helper => ({key: helper.key, distance: helper.distance}));
165
+ expect(mapped).toEqual(keysAndDistances);
166
+ });
167
+ it("reports name and bucket contents.", function () {
168
+ let report = node.report(string => string);
169
+ let expected = `Node: ${node.name}, 0 transports
170
+ 0: ${node.routingTable.get(0).contacts.map(c => c.key.toString() + 'n').join(', ')}
171
+ 10: ${node.routingTable.get(10).contacts.map(c => c.key.toString() + 'n').join(', ')}
172
+ 60: ${node.routingTable.get(60).contacts.map(c => c.key.toString() + 'n').join(', ')}
173
+ 90: ${node.routingTable.get(90).contacts.map(c => c.key.toString() + 'n').join(', ')}`;
174
+ expect(report).toBe(expected);
175
+ });
176
+ });
177
+ describe("discovery", function () {
178
+ it("does not place self.", async function () {
179
+ let node = Node.fromKey(Node.one);
180
+ expect(await node.addToRoutingTable(SimulatedContact.fromKey(Node.one))).toBeFalsy();
181
+ expect(node.routingTable.size).toBe(0);
182
+ });
183
+ it("places in bucket if room.", async function () {
184
+ let contact = SimulatedContact.fromKey(Node.zero);
185
+ let node = contact.node;
186
+ let other = SimulatedContact.fromKey(Node.one, node); // Closest bucket
187
+ expect(await node.addToRoutingTable(other)).toBeTruthy();
188
+ expect(node.getBucketIndex(Node.one)).toBe(0);
189
+ const bucket = node.routingTable.get(0);
190
+ expect(bucket.contacts[0].key).toBe(Node.one);
191
+ });
192
+ describe("examples", function () {
193
+ const nOthers = Node.k + 40; // k+31 will not overflow. k+40 would overflow.
194
+ let node;
195
+ beforeAll(async function () {
196
+ let start = Date.now();
197
+ const host = SimulatedContact.fromKey(Node.zero);
198
+ node = host.node;
199
+ // These others are all constructed to have distances that increase by one from node.
200
+ for (let i = 1; i <= nOthers; i++) {
201
+ let other = SimulatedContact.fromKey(BigInt(i), host.host);
202
+ await node.addToRoutingTable(other);
203
+ }
204
+ //node.report();
205
+ }, 20e3);
206
+ it("places k in bucket.", function () {
207
+ // Checks the results of the discover() placement, each other should have filled in starting from the closest end.
208
+ // Working backwards from the last kBucket, these will all fill in 1, 2, 4, 8, 16 nodes in each bucket.
209
+ // The next bucket will then fill in k=20
210
+
211
+ // Iterate through the buckets, keeping track of the expectCount in each (1, 2, 4, ...)
212
+ for (let bucketIndex = 0, expectCount = 1, otherBigInt = Node.one, othersLast = BigInt(nOthers );
213
+ otherBigInt <= othersLast;
214
+ bucketIndex++, expectCount *= 2) {
215
+ const bucket = node.routingTable.get(bucketIndex);
216
+ // Now iterate through the entries in the bucket, up to expectCount or k.
217
+ let i = 0;
218
+
219
+ // Full bucket.contacts can be in a different order because each attempt to add to a full bucket
220
+ // causes the head of the bucket to be pinged and (if alive) rotated to the back.
221
+ // So, let's just collect the keys and the expected values, andnd sort the keys for comparison.
222
+ let keys = bucket.contacts.map(c => c.key);
223
+ let expecting = [];
224
+ for (; i < Math.min(expectCount, Node.k); i++) expecting.push(otherBigInt++);
225
+ const compare = (a, b) => {
226
+ if (a < b) return -1;
227
+ if (a > b) return 1;
228
+ return 0;
229
+ };
230
+ keys.sort(compare);
231
+ expect(keys).toEqual(expecting);
232
+
233
+ if (i >= Node.k) { // Now soak up those dropped, if any. (If we add a bucket replacement cache, it would be checked here.)
234
+ for (i = 0; otherBigInt <= othersLast; i++) {
235
+ otherBigInt++;
236
+ }
237
+ }
238
+ }
239
+ });
240
+ it('finds closest keys', function () {
241
+ const closest = node.findClosestHelpers(BigInt(40));
242
+ const keys = closest.map(helper => helper.key);
243
+ expect(keys).toEqual([
244
+ 40n, 41n, 42n, 43n, 44n,
245
+ 45n, 46n, 47n, 32n, 33n,
246
+ 34n, 35n, 36n, 37n, 38n,
247
+ 39n, 48n, 49n, 50n, 51n
248
+ ]);
249
+ });
250
+ });
251
+ });
252
+ });
253
+ });
@@ -0,0 +1,92 @@
1
+ import { Node } from '../index.js';
2
+ const { describe, it, expect, BigInt, afterAll } = globalThis; // For linters.
3
+
4
+ describe("DHT Keys", function () {
5
+ afterAll(function () {
6
+ Node.stopRefresh();
7
+ });
8
+
9
+ describe("Node creation", function () {
10
+
11
+ describe("from string", function () {
12
+ it("keeps given name.", async function () {
13
+ let name = "something";
14
+ let node= await Node.create(name);
15
+ expect(node.name).toBe(name);
16
+ expect(typeof node.key).toBe('bigint');
17
+ });
18
+
19
+ it("can ommit name.", async function () {
20
+ let node = await Node.create();
21
+ expect(typeof node.name).toBe('string');
22
+ expect(typeof node.key).toBe('bigint');
23
+ });
24
+
25
+ it("has non-negative bigInt.", async function () {
26
+ const key = Math.random();
27
+ const node = await Node.create(key);
28
+ const bigInt = node.key;
29
+ expect(bigInt).toBeGreaterThanOrEqual(0n);
30
+ });
31
+ });
32
+ });
33
+
34
+ describe("keySize", function () {
35
+ it("is >= 128.", function () {
36
+ expect(Node.keySize).toBeGreaterThanOrEqual(128);
37
+ });
38
+
39
+ it("is a multiple of 8.", function () {
40
+ expect(Node.keySize % 8).toBe(0);
41
+ });
42
+
43
+ it("is the max number of bits in the Identifier bigInt.", async function () {
44
+ const example = await Node.create("something else");
45
+ // Not every example will have a leading 1 in the most significant bit, but this one does.
46
+ const binaryString = example.key.toString(2);
47
+ expect(binaryString.length).toBe(Node.keySize);
48
+
49
+ const random = await Node.create(Math.random());
50
+ expect(random.key.toString(2).length).toBeLessThanOrEqual(Node.keySize);
51
+ });
52
+ });
53
+
54
+ describe("distance", function () {
55
+
56
+ it("is xor as a BigInt.", function () {
57
+ // Here we are just spot checking some of the bits.
58
+ let a = BigInt(0b1010);
59
+ let b = BigInt(0b1001);
60
+ let distance = Node.distance(a, b);
61
+ expect(distance).toBe(BigInt(0b11));
62
+ });
63
+
64
+ it("is zero from itself.", async function () {
65
+ let random = await Node.create(Math.random());
66
+ let distance = Node.distance(random.key, random.key);
67
+ expect(distance).toBe(0n);
68
+ });
69
+
70
+ it("is commutative.", async function () {
71
+ let a = await Node.create(Math.random());
72
+ let b = await Node.create(Math.random());
73
+ let distance1 = Node.distance(a.key, b.key);
74
+ let distance2 = Node.distance(b.key, a.key);
75
+ expect(distance1).toBe(distance2);
76
+ });
77
+
78
+ it("is maximum (all nTagBits on) for a complement.", async function () {
79
+ let random = await Node.create(Math.random());
80
+ let key = random.key;
81
+ let flipped = ~key;
82
+ let truncatedFlipped = BigInt.asUintN(Node.keySize, flipped);
83
+ let complementIdentifier = truncatedFlipped;
84
+ let distance = Node.distance(key, complementIdentifier);
85
+ let one = 1n;
86
+ let next = distance + one;
87
+ let max = one << BigInt(Node.keySize);
88
+ expect(next).toBe(max);
89
+ });
90
+ });
91
+ });
92
+
@@ -0,0 +1,56 @@
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
+ });
package/spec/node.html ADDED
@@ -0,0 +1,47 @@
1
+ <html>
2
+ <head>
3
+ <script type="importmap">
4
+ {
5
+ "imports": {
6
+ "uuid": "./uuid/index.js",
7
+ "@yz-social/kdht": "./kdht/index.js",
8
+ "@yz-social/webrtc": "./webrtc/index.js"
9
+ }
10
+ }
11
+ </script>
12
+ </head>
13
+ <body>
14
+ <button id="update">update node display</button><pre id="display"></pre><br/>
15
+ <input type="text" id="key" placeholder="key to read or write under"></input><br/>
16
+ <input type="text" id="writeValue" placeholder="value to write"></input> <button id="write">write</button><br/>
17
+ <input type="text" id="readValue" placeholder="read value" readonly></input> <button id="read">read</button><br/>
18
+ <script type="module">
19
+ import { v4 as uuidv4 } from 'uuid';
20
+ import { WebContact } from '@yz-social/kdht';
21
+ Object.assign(globalThis, {uuidv4, WebContact });
22
+
23
+ let name = localStorage.getItem('name');
24
+ if (!name) {
25
+ name = uuidv4();
26
+ localStorage.setItem('name', name);
27
+ }
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
+
36
+ update.onclick = () => {
37
+ const report = contact.node.report(null);
38
+ console.log({report, display});
39
+ display.textContent = report;
40
+ };
41
+ update.onclick();
42
+
43
+ write.onclick = () => contact.storeValue(key.value, writeValue.value);
44
+ read.onclick = () => contact.node.locateValue(key.value).then(value => readValue.value = value);
45
+ </script>
46
+ </body>
47
+ </html>
package/spec/portal.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import process from 'node:process';
3
+ import cluster from 'node:cluster';
4
+ import { spawn } from 'node:child_process'; // For optionally spawning bots.js
5
+ import { launchWriteRead } from './writes.js';
6
+ import express from 'express';
7
+ import logger from 'morgan';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import yargs from 'yargs';
11
+ import { hideBin } from 'yargs/helpers';
12
+ import { Node } from '../index.js';
13
+
14
+ // TODO: Allow a remote portal to be specified that this portal will hook with, forming one big network.
15
+ const argv = yargs(hideBin(process.argv))
16
+ .usage("Start an http post server through which nodes can connect to set of nPortals stable nodes.")
17
+ .option('nPortals', {
18
+ alias: 'nportals',
19
+ alias: 'p',
20
+ type: 'number',
21
+ default: 20,
22
+ description: "The number of steady nodes that handle initial connections."
23
+ })
24
+ .option('nBots', {
25
+ alias: 'nbots',
26
+ alias: 'n',
27
+ type: 'number',
28
+ default: 0,
29
+ description: "If non-zero, spawns bots.js with the given nBots."
30
+ })
31
+ .option('thrash', {
32
+ type: 'boolean',
33
+ default: false,
34
+ description: "Do the nBots randomly disconnect and reconnect with no memory of previous data?"
35
+ })
36
+ .option('nWrites', {
37
+ alias: 'w',
38
+ type: 'number',
39
+ default: 0,
40
+ description: "The number of test writes to pass to bots.js."
41
+ })
42
+ .option('baseURL', {
43
+ type: 'string',
44
+ default: 'http://localhost:3000/kdht',
45
+ description: "The base URL of the portal server through which to bootstrap."
46
+ })
47
+ .option('externalBaseURL', {
48
+ type: 'string',
49
+ default: '',
50
+ description: "The base URL of the some other portal server to which we should connect ours, if any."
51
+ })
52
+ .option('verbose', {
53
+ alias: 'v',
54
+ type: 'boolean',
55
+ description: "Run with verbose logging."
56
+ })
57
+ .option('fixedSpacing', {
58
+ type: 'number',
59
+ default: 2,
60
+ description: "Minimum seconds to add between each portal."
61
+ })
62
+ .options('variableSpacing', {
63
+ type: 'number',
64
+ default: 5,
65
+ description: "Additional variable seconds (+/- variableSpacing/2) to add to fixedSpacing between each portal."
66
+ })
67
+ .parse();
68
+
69
+ // NodeJS cluster forks a group of process that each communicate with the primary via send/message.
70
+ // Each fork is a stateful kdht node that can each handle multiple WebRTC connections. The parent runs
71
+ // a Web server that handles Post request by sending the data to the specific fork.
72
+ //
73
+ // (Some other applications using NodeJS cluster handle stateless requests, so each fork listens directly
74
+ // on a shared socket and picks up requests randomly. We do not make use of that shared socket feature.)
75
+
76
+ if (cluster.isPrimary) { // Parent process with portal webserver through which clienta can bootstrap
77
+ // Our job is to launch some kdht nodes to which clients can connect by signaling through
78
+ // a little web server operated here.
79
+ process.title = 'kdht-portal-server';
80
+ const __filename = fileURLToPath(import.meta.url);
81
+ const __dirname = path.dirname(__filename);
82
+ const app = express();
83
+ //fixme if (argv.verbose)
84
+ app.use(logger(':date[iso] :status :method :url :res[content-length] - :response-time ms'));
85
+
86
+ for (let i = 0; i < argv.nPortals; i++) cluster.fork();
87
+ const portalServer = await import('../portals/router.js');
88
+
89
+ // Portal server
90
+ app.set('port', parseInt((new URL(argv.baseURL)).port || '80'));
91
+ console.log(new Date(), process.title, 'startup on port', app.get('port'), 'in', __dirname);
92
+ app.use(express.json());
93
+
94
+ app.use('/kdht', portalServer.router);
95
+ app.use(express.static(path.resolve(__dirname, '..')));
96
+ app.listen(app.get('port'));
97
+ const startupSeconds = argv.fixedSpacing * argv.nPortals + 1.5 * argv.variableSpacing;
98
+ console.log(`Starting ${argv.nPortals} portals over ${startupSeconds} seconds.`);
99
+ if (argv.nBots || argv.nWrites) await Node.delay(startupSeconds * 1e3);
100
+ if (argv.nBots) {
101
+ const args = ['spec/bots.js', '--nBots', argv.nBots, '--baseURL', argv.baseURL, '--thrash', argv.thrash || false, '--verbose', argv.verbose || false];
102
+ if (argv.verbose) console.log('spawning node', args.join(' '));
103
+ const bots = spawn('node', args, {shell: process.platform === 'win32'});
104
+ // Slice off the trailing newline of data so that we don't have blank lines after our console adds one more.
105
+ function echo(data) { data = data.slice(0, -1); console.log(data.toString()); }
106
+ bots.stdout.on('data', echo);
107
+ bots.stderr.on('data', echo);
108
+ if (argv.nWrites) {
109
+ console.log(new Date(), 'Waiting a refresh interval while', argv.nBots, 'bots get randomly created before write/read test.');
110
+ await Node.delay(2 * Node.refreshTimeIntervalMS);
111
+ }
112
+ }
113
+ if (argv.nWrites) launchWriteRead(argv.nWrites, argv.baseURL, argv.nBots ? 2 * Node.refreshTimeIntervalMS : 0, argv.verbose);
114
+
115
+ } else { // A portal node through which client's can connect.
116
+ const portalNode = await import('../portals/node.js');
117
+ const {baseURL, externalBaseURL, fixedSpacing, variableSpacing, verbose} = argv;
118
+ portalNode.setup({baseURL, externalBaseURL, fixedSpacing, variableSpacing, verbose});
119
+ }
@@ -0,0 +1,14 @@
1
+ export default {
2
+ spec_dir: "spec",
3
+ spec_files: [
4
+ "**/*[sS]pec.?(m)js"
5
+ ],
6
+ helpers: [
7
+ "helpers/**/*.?(m)js"
8
+ ],
9
+ env: {
10
+ stopSpecOnExpectationFailure: false,
11
+ random: true,
12
+ forbidDuplicateNames: true
13
+ }
14
+ }
package/spec/writes.js ADDED
@@ -0,0 +1,11 @@
1
+ import process from 'node:process';
2
+ import { spawn } from 'node:child_process'; // For optionally spawning bots.js
3
+
4
+ export function launchWriteRead(nWrites, baseURL, waitBeforeRead, verbose = false) {
5
+ const args = ['jasmine', 'spec/dhtWriteRead.js', '--', '--nWrites', nWrites, '--baseURL', baseURL, '--waitBeforeRead', waitBeforeRead, '--verbose', verbose];
6
+ const bots = spawn('npx', args, {shell: process.platform === 'win32'});
7
+ console.log(new Date(), 'spawning npx', args.join(' '));
8
+ function echo(data) { data = data.slice(0, -1); console.log(data.toString()); }
9
+ bots.stdout.on('data', echo);
10
+ bots.stderr.on('data', echo);
11
+ }