dht-rpc 5.0.6 → 6.0.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.
Files changed (5) hide show
  1. package/README.md +13 -13
  2. package/index.js +14 -6
  3. package/lib/io.js +37 -29
  4. package/package.json +5 -4
  5. package/test.js +3 -22
package/README.md CHANGED
@@ -6,10 +6,6 @@ Make RPC calls over a [Kademlia](https://pdos.csail.mit.edu/~petar/papers/maymou
6
6
  npm install dht-rpc
7
7
  ```
8
8
 
9
- ## NOTE: v5
10
-
11
- Note that the latest release is v5. To see the v4 documentation/code go to https://github.com/mafintosh/dht-rpc/tree/v4
12
-
13
9
  ## Key Features
14
10
 
15
11
  * Remote IP / firewall detection
@@ -116,12 +112,12 @@ Options include:
116
112
  {
117
113
  // A list of bootstrap nodes
118
114
  bootstrap: [ 'bootstrap-node.com:24242', ... ],
119
- // Optionally pass in your own UDP socket to use.
120
- socket: udpSocket,
121
115
  // Optionally pass in array of { host, port } to add to the routing table if you know any peers
122
116
  nodes: [{ host, port }, ...],
123
117
  // Optionally pass a port you prefer to bind to instead of a random one
124
- bind: 0,
118
+ port: 0,
119
+ // Optionally pass a UDX instance on which sockets will be created.
120
+ udx,
125
121
  // dht-rpc will automatically detect if you are firewalled. If you know that you are not set this to false
126
122
  firewalled: true
127
123
  }
@@ -133,7 +129,7 @@ For the vast majority of use-cases you should always use adaptive mode to ensure
133
129
 
134
130
  Your DHT routing id is `hash(publicIp + publicPort)` and will be autoconfigured internally.
135
131
 
136
- #### `const node = DHT.boostrapper(bind, [options])`
132
+ #### `const node = DHT.bootrapper(port, [options])`
137
133
 
138
134
  Sugar for the options needed to run a bootstrap node, ie
139
135
 
@@ -144,7 +140,7 @@ Sugar for the options needed to run a bootstrap node, ie
144
140
  }
145
141
  ```
146
142
 
147
- Additionally since you'll want a known port for a bootstrap node it adds the bind option as a primary argument.
143
+ Additionally since you'll want a known port for a bootstrap node it adds the `port` option as a primary argument.
148
144
 
149
145
  #### `await node.ready()`
150
146
 
@@ -165,7 +161,7 @@ Emitted when the routing table is fully bootstrapped. Emitted as a conveinience.
165
161
 
166
162
  #### `node.on('listening')`
167
163
 
168
- Emitted when the underlying UDP socket is listening. Emitted as a conveinience.
164
+ Emitted when the underlying UDX socket is listening. Emitted as a conveinience.
169
165
 
170
166
  #### `node.on('persistent')`
171
167
 
@@ -179,6 +175,10 @@ you are on an open NAT.
179
175
  Emitted when the node has detected that the computer has gone to sleep. If this happens,
180
176
  it will switch from persistent mode to ephemeral again.
181
177
 
178
+ #### `node.on('network-change', interfaces)`
179
+
180
+ Emitted when the network interfaces of the computer change.
181
+
182
182
  #### `node.refresh()`
183
183
 
184
184
  Refresh the routing table by looking up a random node in the background.
@@ -201,12 +201,12 @@ Boolean indicated if your node is behind a firewall.
201
201
  This is auto detected by having other node's trying to do a PING to you
202
202
  without you contacting them first.
203
203
 
204
- #### `const udpAddr = node.address()`
204
+ #### `const addr = node.address()`
205
205
 
206
206
  Get the local address of the UDP socket bound.
207
207
 
208
208
  Note that if you are in ephemeral mode, this will return a different
209
- port than the one you provided in the constructor (under bind), as ephemeral
209
+ port than the one you provided in the constructor (under `port`), as ephemeral
210
210
  mode always uses a random port.
211
211
 
212
212
  #### `node.on('request', req)`
@@ -242,7 +242,7 @@ Options include:
242
242
  ```js
243
243
  {
244
244
  retry: true, // whether the request should retry on timeout
245
- socket: udpSocket // request on this specific socket
245
+ socket: udxSocket // request on this specific socket
246
246
  }
247
247
  ```
248
248
 
package/index.js CHANGED
@@ -2,6 +2,7 @@ const dns = require('dns')
2
2
  const { EventEmitter } = require('events')
3
3
  const Table = require('kademlia-routing-table')
4
4
  const TOS = require('time-ordered-set')
5
+ const UDX = require('udx-native')
5
6
  const sodium = require('sodium-universal')
6
7
  const c = require('compact-encoding')
7
8
  const NatSampler = require('nat-sampler')
@@ -28,7 +29,8 @@ class DHT extends EventEmitter {
28
29
  this.bootstrapNodes = opts.bootstrap === false ? [] : (opts.bootstrap || []).map(parseNode)
29
30
  this.table = new Table(opts.id || randomBytes(32))
30
31
  this.nodes = new TOS()
31
- this.io = new IO(this.table, {
32
+ this.udx = opts.udx || new UDX()
33
+ this.io = new IO(this.table, this.udx, {
32
34
  ...opts,
33
35
  onrequest: this._onrequest.bind(this),
34
36
  onresponse: this._onresponse.bind(this),
@@ -43,7 +45,7 @@ class DHT extends EventEmitter {
43
45
  this.destroyed = false
44
46
 
45
47
  this._nat = new NatSampler()
46
- this._bind = opts.bind || 0
48
+ this._port = opts.port || 0
47
49
  this._quickFirewall = opts.quickFirewall !== false
48
50
  this._forcePersistent = opts.ephemeral === false
49
51
  this._repinging = 0
@@ -60,13 +62,15 @@ class DHT extends EventEmitter {
60
62
 
61
63
  this.table.on('row', this._onrow)
62
64
 
65
+ this.io.networkInterfaces.on('change', (interfaces) => this._onnetworkchange(interfaces))
66
+
63
67
  if (opts.nodes) {
64
68
  for (const node of opts.nodes) this.addNode(node)
65
69
  }
66
70
  }
67
71
 
68
- static bootstrapper (bind, opts) {
69
- return new this({ bind, firewalled: false, bootstrap: [], ...opts })
72
+ static bootstrapper (port, opts) {
73
+ return new this({ port, firewalled: false, bootstrap: [], ...opts })
70
74
  }
71
75
 
72
76
  get id () {
@@ -329,6 +333,10 @@ class DHT extends EventEmitter {
329
333
  this._repingAndSwap(newNode, oldest)
330
334
  }
331
335
 
336
+ _onnetworkchange (interfaces) {
337
+ this.emit('network-change', interfaces)
338
+ }
339
+
332
340
  _repingAndSwap (newNode, oldNode) {
333
341
  const self = this
334
342
  const lastSeen = oldNode.seen
@@ -619,8 +627,8 @@ class DHT extends EventEmitter {
619
627
 
620
628
  return false
621
629
 
622
- function onmessage (_, rinfo) {
623
- hosts.push(rinfo.address)
630
+ function onmessage (_, { host }) {
631
+ hosts.push(host)
624
632
  }
625
633
  }
626
634
 
package/lib/io.js CHANGED
@@ -1,7 +1,6 @@
1
1
  const FIFO = require('fast-fifo')
2
2
  const sodium = require('sodium-universal')
3
3
  const c = require('compact-encoding')
4
- const bind = require('bind-easy')
5
4
  const b4a = require('b4a')
6
5
  const peer = require('./peer')
7
6
  const errors = require('./errors')
@@ -13,14 +12,16 @@ const TMP = b4a.alloc(32)
13
12
  const EMPTY_ARRAY = []
14
13
 
15
14
  module.exports = class IO {
16
- constructor (table, { maxWindow = 80, bind = 0, firewalled = true, onrequest, onresponse = noop, ontimeout = noop } = {}) {
15
+ constructor (table, udx, { maxWindow = 80, port = 0, firewalled = true, onrequest, onresponse = noop, ontimeout = noop } = {}) {
17
16
  this.table = table
17
+ this.udx = udx
18
18
  this.inflight = []
19
19
  this.clientSocket = null
20
20
  this.serverSocket = null
21
21
  this.firewalled = firewalled !== false
22
22
  this.ephemeral = true
23
23
  this.congestion = new CongestionWindow(maxWindow)
24
+ this.networkInterfaces = udx.watchNetworkInterfaces()
24
25
 
25
26
  this.onrequest = onrequest
26
27
  this.onresponse = onresponse
@@ -33,13 +34,13 @@ module.exports = class IO {
33
34
  this._drainInterval = null
34
35
  this._destroying = null
35
36
  this._binding = null
36
- this._bind = bind
37
+ this._port = port
37
38
  }
38
39
 
39
- onmessage (socket, buffer, rinfo) {
40
- if (buffer.byteLength < 2 || !(rinfo.port > 0 && rinfo.port < 65536)) return
40
+ onmessage (socket, buffer, { host, port }) {
41
+ if (buffer.byteLength < 2 || !(port > 0 && port < 65536)) return
41
42
 
42
- const from = { id: null, host: rinfo.address, port: rinfo.port }
43
+ const from = { id: null, host, port }
43
44
  const state = { start: 1, end: buffer.byteLength, buffer }
44
45
  const expectedSocket = this.firewalled ? this.clientSocket : this.serverSocket
45
46
  const external = socket !== expectedSocket
@@ -114,16 +115,11 @@ module.exports = class IO {
114
115
  req.onerror(errors.createDestroyedError(), req)
115
116
  }
116
117
 
117
- this._destroying = new Promise((resolve) => {
118
- let missing = 2
119
-
120
- this.serverSocket.close(done)
121
- this.clientSocket.close(done)
122
-
123
- function done () {
124
- if (--missing === 0) resolve()
125
- }
126
- })
118
+ this._destroying = Promise.allSettled([
119
+ this.serverSocket.close(),
120
+ this.clientSocket.close(),
121
+ this.networkInterfaces.destroy()
122
+ ])
127
123
 
128
124
  return this._destroying
129
125
  }
@@ -135,19 +131,32 @@ module.exports = class IO {
135
131
  }
136
132
 
137
133
  async _bindSockets () {
138
- const serverSocket = typeof this._bind === 'function' ? await this._bind() : await bind.udp(this._bind)
134
+ const serverSocket = this.udx.createSocket()
135
+
136
+ try {
137
+ serverSocket.bind(this._port)
138
+ } catch {
139
+ try {
140
+ serverSocket.bind()
141
+ } catch (err) {
142
+ await serverSocket.close()
143
+ throw err
144
+ }
145
+ }
146
+
147
+ const clientSocket = this.udx.createSocket()
139
148
 
140
149
  try {
141
- // TODO: we should reroll the socket is it's close to our preferred range of ports
142
- // to avoid it being accidentally opened
143
- // We'll prop need additional APIs for that
144
- this.clientSocket = await bind.udp()
145
- this.serverSocket = serverSocket
150
+ clientSocket.bind()
146
151
  } catch (err) {
147
- await new Promise((resolve) => serverSocket.close(resolve))
152
+ await serverSocket.close()
153
+ await clientSocket.close()
148
154
  throw err
149
155
  }
150
156
 
157
+ this.clientSocket = clientSocket
158
+ this.serverSocket = serverSocket
159
+
151
160
  this.serverSocket.on('message', this.onmessage.bind(this, this.serverSocket))
152
161
  this.clientSocket.on('message', this.onmessage.bind(this, this.clientSocket))
153
162
 
@@ -242,8 +251,7 @@ class Request {
242
251
  reply (value, opts = {}) {
243
252
  const socket = opts.socket || this.socket
244
253
  const to = opts.to || this.from
245
- const onflush = opts.onflush || null
246
- this._sendReply(0, value || null, opts.token !== false, opts.closerNodes !== false, to, socket, onflush)
254
+ this._sendReply(0, value || null, opts.token !== false, opts.closerNodes !== false, to, socket)
247
255
  }
248
256
 
249
257
  error (code, opts = {}) {
@@ -255,7 +263,7 @@ class Request {
255
263
  relay (value, to, opts) {
256
264
  const socket = (opts && opts.socket) || this.socket
257
265
  const buffer = this._encodeRequest(null, value, to, socket)
258
- socket.send(buffer, 0, buffer.byteLength, to.port, to.host)
266
+ socket.trySend(buffer, to.port, to.host)
259
267
  }
260
268
 
261
269
  send (force = false) {
@@ -280,7 +288,7 @@ class Request {
280
288
  if (this.destroyed) return
281
289
  this.sent++
282
290
  this._io.congestion.send()
283
- this.socket.send(this._buffer, 0, this._buffer.byteLength, this.to.port, this.to.host)
291
+ this.socket.trySend(this._buffer, this.to.port, this.to.host)
284
292
  if (this._timeout) clearTimeout(this._timeout)
285
293
  this._timeout = setTimeout(oncycle, 1000, this)
286
294
  }
@@ -298,7 +306,7 @@ class Request {
298
306
  this.onerror(err || errors.createDestroyedError(), this)
299
307
  }
300
308
 
301
- _sendReply (error, value, token, hasCloserNodes, from, socket, onflush) {
309
+ _sendReply (error, value, token, hasCloserNodes, from, socket) {
302
310
  if (socket === null || this.destroyed) return
303
311
 
304
312
  const id = this._io.ephemeral === false && socket === this._io.serverSocket
@@ -324,7 +332,7 @@ class Request {
324
332
  if (error > 0) c.uint.encode(state, error)
325
333
  if (value) c.buffer.encode(state, value)
326
334
 
327
- socket.send(state.buffer, 0, state.buffer.byteLength, from.port, from.host, onflush)
335
+ socket.trySend(state.buffer, from.port, from.host)
328
336
  }
329
337
 
330
338
  _encodeRequest (token, value, to, socket) {
package/package.json CHANGED
@@ -1,22 +1,23 @@
1
1
  {
2
2
  "name": "dht-rpc",
3
- "version": "5.0.6",
3
+ "version": "6.0.0",
4
4
  "description": "Make RPC calls over a Kademlia based DHT",
5
5
  "main": "index.js",
6
6
  "dependencies": {
7
7
  "b4a": "^1.3.1",
8
- "bind-easy": "^1.0.0",
9
8
  "compact-encoding": "^2.1.0",
10
9
  "compact-encoding-net": "^1.0.1",
10
+ "events": "^3.3.0",
11
11
  "fast-fifo": "^1.0.0",
12
12
  "kademlia-routing-table": "^1.0.0",
13
13
  "nat-sampler": "^1.0.1",
14
14
  "sodium-universal": "^3.0.4",
15
15
  "streamx": "^2.10.3",
16
- "time-ordered-set": "^1.0.2"
16
+ "time-ordered-set": "^1.0.2",
17
+ "udx-native": "^1.1.0"
17
18
  },
18
19
  "devDependencies": {
19
- "brittle": "^1.4.3",
20
+ "brittle": "^2.3.1",
20
21
  "standard": "^16.0.3"
21
22
  },
22
23
  "scripts": {
package/test.js CHANGED
@@ -2,14 +2,12 @@ const test = require('brittle')
2
2
  const dgram = require('dgram')
3
3
  const DHT = require('./')
4
4
 
5
- test.configure({ serial: true })
6
-
7
5
  test('make tiny swarm', async function (t) {
8
6
  await makeSwarm(2, t)
9
7
  t.pass('could make swarm')
10
8
  })
11
9
 
12
- test('make bigger swarm', async function (t) {
10
+ test('make bigger swarm', { timeout: 60000 }, async function (t) {
13
11
  const swarm = await makeSwarm(500, t)
14
12
 
15
13
  const targetNode = swarm[25]
@@ -165,23 +163,6 @@ test('request with/without retries', async function (t) {
165
163
  t.is(tries, 4)
166
164
  })
167
165
 
168
- test('reply onflush', async function (t) {
169
- const [, a, b] = await makeSwarm(3, t)
170
-
171
- let flushed = false
172
-
173
- b.on('request', function (req) {
174
- req.reply(null, {
175
- onflush () {
176
- flushed = true
177
- }
178
- })
179
- })
180
-
181
- await a.request({ command: 42 }, { host: '127.0.0.1', port: b.address().port })
182
- t.ok(flushed)
183
- })
184
-
185
166
  test('shorthand commit', async function (t) {
186
167
  const swarm = await makeSwarm(40, t)
187
168
 
@@ -290,12 +271,12 @@ test('addNode / nodes option', async function (t) {
290
271
  test('set bind', async function (t) {
291
272
  const port = await freePort()
292
273
 
293
- const a = new DHT({ bind: port, firewalled: false })
274
+ const a = new DHT({ port, firewalled: false })
294
275
  await a.ready()
295
276
 
296
277
  t.alike(a.address().port, port, 'bound to explicit port')
297
278
 
298
- const b = new DHT({ bind: port })
279
+ const b = new DHT({ port })
299
280
  await b.ready()
300
281
 
301
282
  t.not(b.address().port, port, 'bound to different port as explicit one is taken')