|
|
@ -1,4 +1,3 @@ |
|
|
|
const Table = require('kademlia-routing-table') |
|
|
|
const { Readable } = require('streamx') |
|
|
|
|
|
|
|
module.exports = class Query extends Readable { |
|
|
@ -6,25 +5,33 @@ module.exports = class Query extends Readable { |
|
|
|
super() |
|
|
|
|
|
|
|
this.dht = dht |
|
|
|
this.table = opts.table || new Table(target, { k: 20 }) |
|
|
|
this.k = this.dht.table.k |
|
|
|
this.target = target |
|
|
|
this.command = command |
|
|
|
this.value = value |
|
|
|
this.errors = 0 |
|
|
|
this.successes = 0 |
|
|
|
this.concurrency = opts.concurrency || 16 |
|
|
|
this.concurrency = opts.concurrency || this.k |
|
|
|
this.inflight = 0 |
|
|
|
this.map = opts.map || defaultMap |
|
|
|
this.closest = [] |
|
|
|
|
|
|
|
this._slowdown = false |
|
|
|
this._seen = new Set() |
|
|
|
this._pending = [] |
|
|
|
this._onresolve = this._onvisit.bind(this) |
|
|
|
this._onreject = this._onerror.bind(this) |
|
|
|
} |
|
|
|
this._fromTable = false |
|
|
|
|
|
|
|
get target () { |
|
|
|
return this.table.id |
|
|
|
} |
|
|
|
const nodes = opts.nodes || opts.closest |
|
|
|
|
|
|
|
closest () { |
|
|
|
return this.table.closest(this.table.id) |
|
|
|
if (nodes) { |
|
|
|
// add them reverse as we pop below
|
|
|
|
for (let i = nodes.length - 1; i >= 0; i--) { |
|
|
|
const node = nodes[i] |
|
|
|
this._addPending(node.id, node.host, node.port) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
finished () { |
|
|
@ -51,7 +58,7 @@ module.exports = class Query extends Readable { |
|
|
|
|
|
|
|
async commit (command = this.command, value = this.value, opts) { |
|
|
|
if (typeof command === 'object' && command) return this.commit(undefined, undefined, command) |
|
|
|
return this.dht.requestAll(this.table.id, command, value, this.closest(), opts) |
|
|
|
return this.dht.requestAll(this.target, command, value, this.closest, opts) |
|
|
|
} |
|
|
|
|
|
|
|
async toArray () { |
|
|
@ -61,45 +68,41 @@ module.exports = class Query extends Readable { |
|
|
|
return all |
|
|
|
} |
|
|
|
|
|
|
|
_open (cb) { |
|
|
|
let cnt = 0 |
|
|
|
|
|
|
|
// we need to do this in case of table reuse
|
|
|
|
for (const node of this.table.closest(this.table.id)) { |
|
|
|
node.visited = false |
|
|
|
cnt++ |
|
|
|
} |
|
|
|
_addFromTable () { |
|
|
|
if (this._pending.length >= this.k) return |
|
|
|
this._fromTable = true |
|
|
|
|
|
|
|
const closest = this.dht.table.closest(this.table.id) |
|
|
|
const closest = this.dht.table.closest(this.target, this.k - this._pending.length) |
|
|
|
|
|
|
|
for (const node of closest) { |
|
|
|
cnt++ |
|
|
|
this.table.add({ |
|
|
|
visited: false, |
|
|
|
id: node.id, |
|
|
|
token: null, |
|
|
|
port: node.port, |
|
|
|
host: node.host |
|
|
|
}) |
|
|
|
this._addPending(node.id, node.host, node.port) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (cnt >= this.concurrency) return cb(null) |
|
|
|
_open (cb) { |
|
|
|
this._addFromTable() |
|
|
|
if (this._pending.length >= this.k) return cb(null) |
|
|
|
|
|
|
|
this.dht._resolveBootstrapNodes((bootstrapNodes) => { |
|
|
|
for (const node of bootstrapNodes) { |
|
|
|
this._visit({ |
|
|
|
visited: false, |
|
|
|
id: node.id, |
|
|
|
token: null, |
|
|
|
port: node.port, |
|
|
|
host: node.host |
|
|
|
}) |
|
|
|
this._addPending(node.id, node.host, node.port) |
|
|
|
} |
|
|
|
|
|
|
|
cb(null) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
_isCloser (id) { |
|
|
|
return this.closest.length < this.k || this._compare(id, this.closest[this.closest.length - 1].id) < 0 |
|
|
|
} |
|
|
|
|
|
|
|
_addPending (id, host, port) { |
|
|
|
if (id && !this._isCloser(id)) return |
|
|
|
const addr = host + ':' + port |
|
|
|
if (this._seen.has(addr)) return |
|
|
|
this._seen.add(addr) |
|
|
|
this._pending.push({ id, host, port }) |
|
|
|
} |
|
|
|
|
|
|
|
_read (cb) { |
|
|
|
this._readMore() |
|
|
|
cb(null) |
|
|
@ -108,15 +111,27 @@ module.exports = class Query extends Readable { |
|
|
|
_readMore () { |
|
|
|
if (this.destroying) return |
|
|
|
|
|
|
|
const closest = this.table.closest(this.table.id) |
|
|
|
const concurrency = this._slowdown ? 3 : this.concurrency |
|
|
|
|
|
|
|
for (const node of closest) { |
|
|
|
if (node.visited) continue |
|
|
|
if (this.inflight >= this.concurrency) return |
|
|
|
this._visit(node) |
|
|
|
while (this.inflight < concurrency && this._pending.length > 0) { |
|
|
|
const next = this._pending.pop() |
|
|
|
if (next && next.id && !this._isCloser(next.id)) continue |
|
|
|
this._visit(next) |
|
|
|
} |
|
|
|
|
|
|
|
if (this.inflight === 0) { |
|
|
|
// if reusing closest nodes, slow down after the first readMore tick to allow
|
|
|
|
// the closests node a chance to reply before going broad to question more
|
|
|
|
if (!this._fromTable && this.successes === 0 && this.errors === 0) { |
|
|
|
this._slowdown = true |
|
|
|
} |
|
|
|
|
|
|
|
if (this.inflight === 0 && this._pending.length === 0) { |
|
|
|
// if more than 3/4 failed and we only used cached nodes, try again from the routing table
|
|
|
|
if (!this._fromTable && this.successes < this.k / 4) { |
|
|
|
this._addFromTable() |
|
|
|
return this._readMore() |
|
|
|
} |
|
|
|
|
|
|
|
this.push(null) |
|
|
|
} |
|
|
|
} |
|
|
@ -125,33 +140,59 @@ module.exports = class Query extends Readable { |
|
|
|
this.successes++ |
|
|
|
this.inflight-- |
|
|
|
|
|
|
|
if (m.nodeId !== null) { |
|
|
|
this.table.add({ |
|
|
|
visited: true, |
|
|
|
if (m.nodeId !== null && this._isCloser(m.nodeId)) { |
|
|
|
const node = { |
|
|
|
id: m.nodeId, |
|
|
|
token: m.token, |
|
|
|
port: m.from.port, |
|
|
|
host: m.from.host |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
this._pushClosest(node) |
|
|
|
} |
|
|
|
|
|
|
|
if (m.closerNodes !== null) { |
|
|
|
for (const node of m.closerNodes) { |
|
|
|
if (node.id.equals(this.dht.table.id)) continue |
|
|
|
if (this.table.get(node.id)) continue |
|
|
|
this.table.add({ |
|
|
|
visited: false, |
|
|
|
id: node.id, |
|
|
|
token: null, |
|
|
|
port: node.port, |
|
|
|
host: node.host |
|
|
|
}) |
|
|
|
this._addPending(node.id, node.host, node.port) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (!this._fromTable && this.successes + this.errors >= this.concurrency) { |
|
|
|
this._slowdown = false |
|
|
|
} |
|
|
|
|
|
|
|
if (this.push(this.map(m)) !== false) this._readMore() |
|
|
|
} |
|
|
|
|
|
|
|
_pushClosest (node) { |
|
|
|
this.closest.push(node) |
|
|
|
for (let i = this.closest.length - 2; i >= 0; i--) { |
|
|
|
const prev = this.closest[i] |
|
|
|
const cmp = this._compare(prev.id, node.id) |
|
|
|
// if sorted, done!
|
|
|
|
if (cmp < 0) break |
|
|
|
// if dup, splice it out (rare)
|
|
|
|
if (cmp === 0) { |
|
|
|
this.closest.splice(i + 1, 1) |
|
|
|
break |
|
|
|
} |
|
|
|
// swap and continue down
|
|
|
|
this.closest[i + 1] = prev |
|
|
|
this.closest[i] = node |
|
|
|
} |
|
|
|
if (this.closest.length > this.k) this.closest.pop() |
|
|
|
} |
|
|
|
|
|
|
|
_compare (a, b) { |
|
|
|
for (let i = 0; i < a.length; i++) { |
|
|
|
if (a[i] === b[i]) continue |
|
|
|
const t = this.target[i] |
|
|
|
return (t ^ a[i]) - (t ^ b[i]) |
|
|
|
} |
|
|
|
return 0 |
|
|
|
} |
|
|
|
|
|
|
|
_onerror () { |
|
|
|
this.errors++ |
|
|
|
this.inflight-- |
|
|
@ -159,10 +200,8 @@ module.exports = class Query extends Readable { |
|
|
|
} |
|
|
|
|
|
|
|
_visit (node) { |
|
|
|
node.visited = true |
|
|
|
|
|
|
|
this.inflight++ |
|
|
|
this.dht.request(this.table.id, this.command, this.value, node) |
|
|
|
this.dht.request(this.target, this.command, this.value, node) |
|
|
|
.then(this._onresolve, this._onreject) |
|
|
|
} |
|
|
|
} |
|
|
|