hypercore 10.0.0-alpha.2 → 10.0.0-alpha.6
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/.github/workflows/test-node.yml +1 -2
- package/README.md +7 -0
- package/index.js +55 -9
- package/lib/protocol.js +3 -1
- package/lib/replicator.js +57 -13
- package/package.json +3 -2
- package/test/basic.js +12 -0
- package/test/replicate.js +76 -0
package/README.md
CHANGED
|
@@ -113,6 +113,7 @@ A range can have the following properties:
|
|
|
113
113
|
{
|
|
114
114
|
start: startIndex,
|
|
115
115
|
end: nonInclusiveEndIndex,
|
|
116
|
+
blocks: [index1, index2, ...],
|
|
116
117
|
linear: false // download range linearly and not randomly
|
|
117
118
|
}
|
|
118
119
|
```
|
|
@@ -125,6 +126,12 @@ To download the full core continously (often referred to as non sparse mode) do
|
|
|
125
126
|
core.download({ start: 0, end: -1 })
|
|
126
127
|
```
|
|
127
128
|
|
|
129
|
+
To downloaded a discrete range of blocks pass a list of indices.
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
core.download({ blocks: [4, 9, 7] });
|
|
133
|
+
```
|
|
134
|
+
|
|
128
135
|
To cancel downloading a range simply destroy the range instance.
|
|
129
136
|
|
|
130
137
|
``` js
|
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@ const raf = require('random-access-file')
|
|
|
3
3
|
const isOptions = require('is-options')
|
|
4
4
|
const hypercoreCrypto = require('hypercore-crypto')
|
|
5
5
|
const c = require('compact-encoding')
|
|
6
|
+
const Xache = require('xache')
|
|
6
7
|
const NoiseSecretStream = require('@hyperswarm/secret-stream')
|
|
7
8
|
const codecs = require('codecs')
|
|
8
9
|
|
|
@@ -27,14 +28,17 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
27
28
|
opts = key
|
|
28
29
|
key = null
|
|
29
30
|
}
|
|
31
|
+
|
|
30
32
|
if (key && typeof key === 'string') {
|
|
31
33
|
key = Buffer.from(key, 'hex')
|
|
32
34
|
}
|
|
33
|
-
|
|
35
|
+
|
|
36
|
+
if (!opts) opts = {}
|
|
37
|
+
|
|
38
|
+
if (!opts.crypto && key && key.byteLength !== 32) {
|
|
34
39
|
throw new Error('Hypercore key should be 32 bytes')
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
if (!opts) opts = {}
|
|
38
42
|
if (!storage) storage = opts.storage
|
|
39
43
|
|
|
40
44
|
this[promises] = true
|
|
@@ -44,6 +48,7 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
44
48
|
this.core = null
|
|
45
49
|
this.replicator = null
|
|
46
50
|
this.extensions = opts.extensions || new Extensions()
|
|
51
|
+
this.cache = opts.cache === true ? new Xache({ maxSize: 65536, maxAge: 0 }) : (opts.cache || null)
|
|
47
52
|
|
|
48
53
|
this.valueEncoding = null
|
|
49
54
|
this.key = key || null
|
|
@@ -91,7 +96,7 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
91
96
|
return function createFile (name) {
|
|
92
97
|
const locked = name === toLock || name.endsWith('/' + toLock)
|
|
93
98
|
const lock = locked ? fsctl.lock : null
|
|
94
|
-
const sparse = locked ? null : fsctl.sparse
|
|
99
|
+
const sparse = locked ? null : null // fsctl.sparse, disable sparse on windows - seems to fail for some people. TODO: investigate
|
|
95
100
|
return raf(name, { directory, lock, sparse })
|
|
96
101
|
}
|
|
97
102
|
}
|
|
@@ -281,8 +286,13 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
281
286
|
_oncoreupdate (status, bitfield, value, from) {
|
|
282
287
|
if (status !== 0) {
|
|
283
288
|
for (let i = 0; i < this.sessions.length; i++) {
|
|
284
|
-
if ((status & 0b10) !== 0)
|
|
285
|
-
|
|
289
|
+
if ((status & 0b10) !== 0) {
|
|
290
|
+
if (this.cache) this.cache.clear()
|
|
291
|
+
this.sessions[i].emit('truncate', this.core.tree.fork)
|
|
292
|
+
}
|
|
293
|
+
if ((status & 0b01) !== 0) {
|
|
294
|
+
this.sessions[i].emit('append')
|
|
295
|
+
}
|
|
286
296
|
}
|
|
287
297
|
|
|
288
298
|
this.replicator.broadcastInfo()
|
|
@@ -346,6 +356,15 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
346
356
|
|
|
347
357
|
async get (index, opts) {
|
|
348
358
|
if (this.opened === false) await this.opening
|
|
359
|
+
const c = this.cache && this.cache.get(index)
|
|
360
|
+
if (c) return c
|
|
361
|
+
const fork = this.core.tree.fork
|
|
362
|
+
const b = await this._get(index, opts)
|
|
363
|
+
if (this.cache && fork === this.core.tree.fork && b) this.cache.set(index, b)
|
|
364
|
+
return b
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async _get (index, opts) {
|
|
349
368
|
const encoding = (opts && opts.valueEncoding && c.from(codecs(opts.valueEncoding))) || this.valueEncoding
|
|
350
369
|
|
|
351
370
|
if (this.core.bitfield.get(index)) return decode(encoding, await this.core.blocks.get(index))
|
|
@@ -355,13 +374,27 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
355
374
|
}
|
|
356
375
|
|
|
357
376
|
download (range) {
|
|
358
|
-
const start = (range && range.start) || 0
|
|
359
|
-
const end = typeof (range && range.end) === 'number' ? range.end : -1 // download all
|
|
360
377
|
const linear = !!(range && range.linear)
|
|
361
378
|
|
|
362
|
-
|
|
379
|
+
let start
|
|
380
|
+
let end
|
|
381
|
+
let filter
|
|
382
|
+
|
|
383
|
+
if (range && range.blocks) {
|
|
384
|
+
const blocks = range.blocks instanceof Set
|
|
385
|
+
? range.blocks
|
|
386
|
+
: new Set(range.blocks)
|
|
387
|
+
|
|
388
|
+
start = range.start || (blocks.size ? min(range.blocks) : 0)
|
|
389
|
+
end = range.end || (blocks.size ? max(range.blocks) + 1 : 0)
|
|
363
390
|
|
|
364
|
-
|
|
391
|
+
filter = (i) => blocks.has(i)
|
|
392
|
+
} else {
|
|
393
|
+
start = (range && range.start) || 0
|
|
394
|
+
end = typeof (range && range.end) === 'number' ? range.end : -1 // download all
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const r = Replicator.createRange(start, end, filter, linear)
|
|
365
398
|
|
|
366
399
|
if (this.opened) this.replicator.addRange(r)
|
|
367
400
|
else this.opening.then(() => this.replicator.addRange(r), noop)
|
|
@@ -443,3 +476,16 @@ function requireMaybe (name) {
|
|
|
443
476
|
function toHex (buf) {
|
|
444
477
|
return buf && buf.toString('hex')
|
|
445
478
|
}
|
|
479
|
+
|
|
480
|
+
function reduce (iter, fn, acc) {
|
|
481
|
+
for (const item of iter) acc = fn(acc, item)
|
|
482
|
+
return acc
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function min (arr) {
|
|
486
|
+
return reduce(arr, (a, b) => Math.min(a, b), Infinity)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function max (arr) {
|
|
490
|
+
return reduce(arr, (a, b) => Math.max(a, b), -Infinity)
|
|
491
|
+
}
|
package/lib/protocol.js
CHANGED
|
@@ -125,6 +125,7 @@ class Peer {
|
|
|
125
125
|
this.resend = false
|
|
126
126
|
this.state = state
|
|
127
127
|
this.extensions = new Map()
|
|
128
|
+
this.destroyed = false
|
|
128
129
|
|
|
129
130
|
this._destroyer = this._safeDestroy.bind(this)
|
|
130
131
|
}
|
|
@@ -229,6 +230,7 @@ class Peer {
|
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
destroy (err) {
|
|
233
|
+
this.destroyed = true
|
|
232
234
|
return this.protocol.unregisterPeer(this, err)
|
|
233
235
|
}
|
|
234
236
|
}
|
|
@@ -296,7 +298,7 @@ module.exports = class Protocol {
|
|
|
296
298
|
peer.remoteAlias = -1
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
peer.handlers.onunregister(
|
|
301
|
+
peer.handlers.onunregister(peer, err)
|
|
300
302
|
|
|
301
303
|
if (err) this.noiseStream.destroy(err)
|
|
302
304
|
}
|
package/lib/replicator.js
CHANGED
|
@@ -25,11 +25,12 @@ class InvertedPromise {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
class Request {
|
|
28
|
-
constructor (index, seek) {
|
|
28
|
+
constructor (index, seek, nodes) {
|
|
29
29
|
this.peer = null
|
|
30
30
|
this.index = index
|
|
31
31
|
this.seek = seek
|
|
32
32
|
this.value = seek === 0
|
|
33
|
+
this.nodes = nodes
|
|
33
34
|
this.promises = []
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -119,9 +120,10 @@ class Seek {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
class Range {
|
|
122
|
-
constructor (start, end, linear) {
|
|
123
|
+
constructor (start, end, filter, linear) {
|
|
123
124
|
this.start = start
|
|
124
125
|
this.end = end
|
|
126
|
+
this.filter = filter
|
|
125
127
|
this.linear = !!linear
|
|
126
128
|
this.promise = null
|
|
127
129
|
this.done = false
|
|
@@ -144,7 +146,7 @@ class Range {
|
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
for (; this._start < this.end; this._start++) {
|
|
147
|
-
if (!bitfield.get(this._start)) return false
|
|
149
|
+
if (this.filter(this._start) && !bitfield.get(this._start)) return false
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
return true
|
|
@@ -278,7 +280,9 @@ class RequestPool {
|
|
|
278
280
|
|
|
279
281
|
_updateSeek (peer, seek) {
|
|
280
282
|
if (seek.request) return false
|
|
281
|
-
|
|
283
|
+
// We have to snapshot the nodes here now, due to the caching of the request...
|
|
284
|
+
const nodes = log2(seek.seeker.end - seek.seeker.start)
|
|
285
|
+
seek.request = this._requestRange(peer, seek.seeker.start, seek.seeker.end, seek.seeker.bytes, nodes)
|
|
282
286
|
return seek.request !== null
|
|
283
287
|
}
|
|
284
288
|
|
|
@@ -296,10 +300,13 @@ class RequestPool {
|
|
|
296
300
|
const end = range.end === -1 ? peer.state.length : range.end
|
|
297
301
|
if (end <= range._start) return false
|
|
298
302
|
|
|
299
|
-
if (range.linear) return !!this._requestRange(peer, range._start, end, 0)
|
|
303
|
+
if (range.linear) return !!this._requestRange(peer, range._start, end, 0, 0, range.filter)
|
|
300
304
|
|
|
301
305
|
const r = range._start + Math.floor(Math.random() * (end - range._start))
|
|
302
|
-
return !!(
|
|
306
|
+
return !!(
|
|
307
|
+
this._requestRange(peer, r, end, 0, 0, range.filter) ||
|
|
308
|
+
this._requestRange(peer, range._start, r, 0, 0, range.filter)
|
|
309
|
+
)
|
|
303
310
|
}
|
|
304
311
|
|
|
305
312
|
_updateUpgrade (peer) {
|
|
@@ -328,7 +335,7 @@ class RequestPool {
|
|
|
328
335
|
this.pendingUpgrade = null
|
|
329
336
|
}
|
|
330
337
|
|
|
331
|
-
_requestRange (peer, start, end, seek) {
|
|
338
|
+
_requestRange (peer, start, end, seek, nodes, filter = tautology) {
|
|
332
339
|
const remote = peer.state.bitfield
|
|
333
340
|
const local = this.core.bitfield
|
|
334
341
|
|
|
@@ -336,12 +343,12 @@ class RequestPool {
|
|
|
336
343
|
if (end === -1) end = peer.state.length
|
|
337
344
|
|
|
338
345
|
for (let i = start; i < end; i++) {
|
|
339
|
-
if (!remote.get(i) || local.get(i)) continue
|
|
346
|
+
if (!filter(i) || !remote.get(i) || local.get(i)) continue
|
|
340
347
|
// TODO: if this was a NO_VALUE request, retry if no blocks can be found elsewhere
|
|
341
348
|
if (this.requests.has(i)) continue
|
|
342
349
|
|
|
343
350
|
// TODO: if seeking and i >= core.length, let that takes precendance in the upgrade req
|
|
344
|
-
const req = new Request(i, i < this.core.tree.length ? seek : 0)
|
|
351
|
+
const req = new Request(i, i < this.core.tree.length ? seek : 0, nodes)
|
|
345
352
|
this.requests.set(i, req)
|
|
346
353
|
this.send(peer, req)
|
|
347
354
|
return req
|
|
@@ -390,16 +397,28 @@ class RequestPool {
|
|
|
390
397
|
|
|
391
398
|
if (data.block.index < this.core.tree.length || this.core.truncating > 0) {
|
|
392
399
|
try {
|
|
393
|
-
data.block.nodes = await this.core.tree.nodes(data.block.index * 2)
|
|
400
|
+
data.block.nodes = Math.max(req.nodes, await this.core.tree.nodes(data.block.index * 2))
|
|
394
401
|
} catch (err) {
|
|
395
402
|
console.error('TODO handle me:', err.stack)
|
|
396
403
|
}
|
|
397
404
|
}
|
|
398
405
|
|
|
406
|
+
if (peer.destroyed) {
|
|
407
|
+
req.peer = null
|
|
408
|
+
this.pending.push(req)
|
|
409
|
+
if (upgrading) {
|
|
410
|
+
this.upgrading.resolve()
|
|
411
|
+
this.upgrading = null
|
|
412
|
+
}
|
|
413
|
+
this.replicator.updateAll()
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
|
|
399
417
|
if (fork !== this.core.tree.fork || paused(peer, this.core.tree.fork) || this.core.truncating > 0) {
|
|
400
418
|
if (peer.state.inflight > 0) peer.state.inflight--
|
|
401
419
|
if (req.promises.length) { // someone is eagerly waiting for this request
|
|
402
420
|
req.peer = null // resend on some other peer
|
|
421
|
+
this.pending.push(req)
|
|
403
422
|
} else { // otherwise delete the request
|
|
404
423
|
this.requests.delete(req.index)
|
|
405
424
|
}
|
|
@@ -594,7 +613,7 @@ class RequestPool {
|
|
|
594
613
|
return e.createPromise()
|
|
595
614
|
}
|
|
596
615
|
|
|
597
|
-
const r = new Request(index, 0)
|
|
616
|
+
const r = new Request(index, 0, 0)
|
|
598
617
|
|
|
599
618
|
this.requests.set(index, r)
|
|
600
619
|
this.pending.push(r)
|
|
@@ -662,8 +681,18 @@ module.exports = class Replicator {
|
|
|
662
681
|
return promise
|
|
663
682
|
}
|
|
664
683
|
|
|
665
|
-
static createRange (start, end, linear) {
|
|
666
|
-
|
|
684
|
+
static createRange (start, end, filter, linear) {
|
|
685
|
+
// createRange(start, end)
|
|
686
|
+
if (filter === undefined) {
|
|
687
|
+
filter = tautology
|
|
688
|
+
|
|
689
|
+
// createRange(start, end, linear)
|
|
690
|
+
} else if (typeof filter === 'boolean') {
|
|
691
|
+
linear = filter
|
|
692
|
+
filter = tautology
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return new Range(start, end, filter, linear)
|
|
667
696
|
}
|
|
668
697
|
|
|
669
698
|
addRange (range) {
|
|
@@ -810,3 +839,18 @@ function pages (core) {
|
|
|
810
839
|
}
|
|
811
840
|
|
|
812
841
|
function noop () {}
|
|
842
|
+
|
|
843
|
+
function tautology () {
|
|
844
|
+
return true
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function log2 (n) {
|
|
848
|
+
let res = 1
|
|
849
|
+
|
|
850
|
+
while (n > 2) {
|
|
851
|
+
n /= 2
|
|
852
|
+
res++
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return res
|
|
856
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypercore",
|
|
3
|
-
"version": "10.0.0-alpha.
|
|
3
|
+
"version": "10.0.0-alpha.6",
|
|
4
4
|
"description": "Hypercore 10",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"random-access-file": "^2.1.4",
|
|
40
40
|
"random-array-iterator": "^1.0.0",
|
|
41
41
|
"safety-catch": "^1.0.1",
|
|
42
|
-
"uint64le": "^1.0.0"
|
|
42
|
+
"uint64le": "^1.0.0",
|
|
43
|
+
"xache": "^1.0.0"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"brittle": "^1.4.1",
|
package/test/basic.js
CHANGED
|
@@ -76,3 +76,15 @@ test('storage options', async function (t) {
|
|
|
76
76
|
t.alike(await core.get(0), Buffer.from('hello'))
|
|
77
77
|
t.end()
|
|
78
78
|
})
|
|
79
|
+
|
|
80
|
+
test(
|
|
81
|
+
'allow publicKeys with different byteLength that 32, if opts.crypto were passed',
|
|
82
|
+
function (t) {
|
|
83
|
+
const key = Buffer.alloc(33).fill('a')
|
|
84
|
+
|
|
85
|
+
const core = new Hypercore(ram, key, { crypto: {} })
|
|
86
|
+
|
|
87
|
+
t.is(core.key, key)
|
|
88
|
+
t.pass('creating a core with more than 32 byteLength key did not throw')
|
|
89
|
+
}
|
|
90
|
+
)
|
package/test/replicate.js
CHANGED
|
@@ -294,3 +294,79 @@ test('multiplexing multiple times over the same stream', async function (t) {
|
|
|
294
294
|
t.is(b1.length, a1.length, 'same length')
|
|
295
295
|
t.end()
|
|
296
296
|
})
|
|
297
|
+
|
|
298
|
+
test('destroying a stream and re-replicating works', async function (t) {
|
|
299
|
+
const core = await create()
|
|
300
|
+
|
|
301
|
+
while (core.length < 33) await core.append(Buffer.from('#' + core.length))
|
|
302
|
+
|
|
303
|
+
const clone = await create(core.key)
|
|
304
|
+
|
|
305
|
+
let s1 = core.replicate(true)
|
|
306
|
+
let s2 = clone.replicate(false)
|
|
307
|
+
|
|
308
|
+
s1.pipe(s2).pipe(s1)
|
|
309
|
+
|
|
310
|
+
await s2.opened
|
|
311
|
+
|
|
312
|
+
const all = []
|
|
313
|
+
for (let i = 0; i < 33; i++) {
|
|
314
|
+
all.push(clone.get(i))
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
clone.once('download', function () {
|
|
318
|
+
// simulate stream failure in the middle of bulk downloading
|
|
319
|
+
s1.destroy()
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
await new Promise((resolve) => s1.once('close', resolve))
|
|
323
|
+
|
|
324
|
+
// retry
|
|
325
|
+
s1 = core.replicate(true)
|
|
326
|
+
s2 = clone.replicate(false)
|
|
327
|
+
|
|
328
|
+
s1.pipe(s2).pipe(s1)
|
|
329
|
+
|
|
330
|
+
const blocks = await Promise.all(all)
|
|
331
|
+
|
|
332
|
+
t.is(blocks.length, 33, 'downloaded 33 blocks')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('replicate discrete range', async function (t) {
|
|
336
|
+
const a = await create()
|
|
337
|
+
|
|
338
|
+
await a.append(['a', 'b', 'c', 'd', 'e'])
|
|
339
|
+
|
|
340
|
+
const b = await create(a.key)
|
|
341
|
+
|
|
342
|
+
let d = 0
|
|
343
|
+
b.on('download', () => d++)
|
|
344
|
+
|
|
345
|
+
replicate(a, b, t)
|
|
346
|
+
|
|
347
|
+
const r = b.download({ blocks: [0, 2, 3] })
|
|
348
|
+
await r.downloaded()
|
|
349
|
+
|
|
350
|
+
t.is(d, 3)
|
|
351
|
+
t.alike(await b.get(0), Buffer.from('a'))
|
|
352
|
+
t.alike(await b.get(2), Buffer.from('c'))
|
|
353
|
+
t.alike(await b.get(3), Buffer.from('d'))
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
test('replicate discrete empty range', async function (t) {
|
|
357
|
+
const a = await create()
|
|
358
|
+
|
|
359
|
+
await a.append(['a', 'b', 'c', 'd', 'e'])
|
|
360
|
+
|
|
361
|
+
const b = await create(a.key)
|
|
362
|
+
|
|
363
|
+
let d = 0
|
|
364
|
+
b.on('download', () => d++)
|
|
365
|
+
|
|
366
|
+
replicate(a, b, t)
|
|
367
|
+
|
|
368
|
+
const r = b.download({ blocks: [] })
|
|
369
|
+
await r.downloaded()
|
|
370
|
+
|
|
371
|
+
t.is(d, 0)
|
|
372
|
+
})
|