hypercore 10.0.0-alpha.4 → 10.0.0-alpha.42
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/README.md +58 -3
- package/index.js +538 -172
- package/lib/bitfield.js +9 -5
- package/lib/block-encryption.js +68 -0
- package/lib/block-store.js +3 -1
- package/lib/caps.js +34 -0
- package/lib/core.js +84 -27
- package/lib/errors.js +42 -0
- package/lib/merkle-tree.js +186 -113
- package/lib/messages.js +249 -168
- package/lib/oplog.js +4 -3
- package/lib/replicator.js +1288 -548
- package/lib/streams.js +56 -0
- package/package.json +18 -9
- package/.github/workflows/test-node.yml +0 -23
- package/CHANGELOG.md +0 -37
- package/UPGRADE.md +0 -9
- package/examples/announce.js +0 -19
- package/examples/basic.js +0 -10
- package/examples/http.js +0 -123
- package/examples/lookup.js +0 -20
- package/lib/extensions.js +0 -76
- package/lib/protocol.js +0 -524
- package/lib/random-iterator.js +0 -46
- package/test/basic.js +0 -78
- package/test/bitfield.js +0 -71
- package/test/core.js +0 -290
- package/test/encodings.js +0 -18
- package/test/extension.js +0 -71
- package/test/helpers/index.js +0 -23
- package/test/merkle-tree.js +0 -518
- package/test/mutex.js +0 -137
- package/test/oplog.js +0 -399
- package/test/preload.js +0 -72
- package/test/replicate.js +0 -333
- package/test/sessions.js +0 -173
- package/test/user-data.js +0 -47
package/index.js
CHANGED
|
@@ -3,15 +3,19 @@ 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 b4a = require('b4a')
|
|
6
7
|
const Xache = require('xache')
|
|
7
8
|
const NoiseSecretStream = require('@hyperswarm/secret-stream')
|
|
9
|
+
const Protomux = require('protomux')
|
|
8
10
|
const codecs = require('codecs')
|
|
9
11
|
|
|
10
12
|
const fsctl = requireMaybe('fsctl') || { lock: noop, sparse: noop }
|
|
11
13
|
|
|
12
14
|
const Replicator = require('./lib/replicator')
|
|
13
|
-
const Extensions = require('./lib/extensions')
|
|
14
15
|
const Core = require('./lib/core')
|
|
16
|
+
const BlockEncryption = require('./lib/block-encryption')
|
|
17
|
+
const { ReadStream, WriteStream } = require('./lib/streams')
|
|
18
|
+
const { BAD_ARGUMENT, SESSION_CLOSED, SESSION_NOT_WRITABLE, SNAPSHOT_NOT_AVAILABLE } = require('./lib/errors')
|
|
15
19
|
|
|
16
20
|
const promises = Symbol.for('hypercore.promises')
|
|
17
21
|
const inspect = Symbol.for('nodejs.util.inspect.custom')
|
|
@@ -28,14 +32,17 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
28
32
|
opts = key
|
|
29
33
|
key = null
|
|
30
34
|
}
|
|
35
|
+
|
|
31
36
|
if (key && typeof key === 'string') {
|
|
32
|
-
key =
|
|
33
|
-
}
|
|
34
|
-
if (key && key.byteLength !== 32) {
|
|
35
|
-
throw new Error('Hypercore key should be 32 bytes')
|
|
37
|
+
key = b4a.from(key, 'hex')
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
if (!opts) opts = {}
|
|
41
|
+
|
|
42
|
+
if (!opts.crypto && key && key.byteLength !== 32) {
|
|
43
|
+
throw BAD_ARGUMENT('Hypercore key should be 32 bytes')
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
if (!storage) storage = opts.storage
|
|
40
47
|
|
|
41
48
|
this[promises] = true
|
|
@@ -44,23 +51,32 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
44
51
|
this.crypto = opts.crypto || hypercoreCrypto
|
|
45
52
|
this.core = null
|
|
46
53
|
this.replicator = null
|
|
47
|
-
this.
|
|
54
|
+
this.encryption = null
|
|
55
|
+
this.extensions = new Map()
|
|
48
56
|
this.cache = opts.cache === true ? new Xache({ maxSize: 65536, maxAge: 0 }) : (opts.cache || null)
|
|
49
57
|
|
|
50
58
|
this.valueEncoding = null
|
|
59
|
+
this.encodeBatch = null
|
|
60
|
+
this.activeRequests = []
|
|
61
|
+
|
|
51
62
|
this.key = key || null
|
|
52
|
-
this.
|
|
63
|
+
this.keyPair = null
|
|
53
64
|
this.readable = true
|
|
54
65
|
this.writable = false
|
|
55
66
|
this.opened = false
|
|
56
67
|
this.closed = false
|
|
68
|
+
this.snapshotted = !!opts.snapshot
|
|
57
69
|
this.sessions = opts._sessions || [this]
|
|
58
|
-
this.
|
|
70
|
+
this.auth = opts.auth || null
|
|
59
71
|
this.autoClose = !!opts.autoClose
|
|
60
72
|
|
|
61
73
|
this.closing = null
|
|
62
|
-
this.opening =
|
|
74
|
+
this.opening = this._openSession(key, storage, opts)
|
|
63
75
|
this.opening.catch(noop)
|
|
76
|
+
|
|
77
|
+
this._preappend = preappend.bind(this)
|
|
78
|
+
this._snapshot = null
|
|
79
|
+
this._findingPeers = 0
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
[inspect] (depth, opts) {
|
|
@@ -69,21 +85,78 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
69
85
|
while (indent.length < opts.indentationLvl) indent += ' '
|
|
70
86
|
}
|
|
71
87
|
|
|
88
|
+
let peers = ''
|
|
89
|
+
const min = Math.min(this.peers.length, 5)
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < min; i++) {
|
|
92
|
+
const peer = this.peers[i]
|
|
93
|
+
|
|
94
|
+
peers += indent + ' Peer(\n'
|
|
95
|
+
peers += indent + ' remotePublicKey: ' + opts.stylize(toHex(peer.remotePublicKey), 'string') + '\n'
|
|
96
|
+
peers += indent + ' remoteLength: ' + opts.stylize(peer.remoteLength, 'number') + '\n'
|
|
97
|
+
peers += indent + ' remoteFork: ' + opts.stylize(peer.remoteFork, 'number') + '\n'
|
|
98
|
+
peers += indent + ' remoteCanUpgrade: ' + opts.stylize(peer.remoteCanUpgrade, 'boolean') + '\n'
|
|
99
|
+
peers += indent + ' )' + '\n'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (this.peers.length > 5) {
|
|
103
|
+
peers += indent + ' ... and ' + (this.peers.length - 5) + ' more\n'
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (peers) peers = '[\n' + peers + indent + ' ]'
|
|
107
|
+
else peers = '[ ' + opts.stylize(0, 'number') + ' ]'
|
|
108
|
+
|
|
72
109
|
return this.constructor.name + '(\n' +
|
|
73
|
-
indent + ' key: ' + opts.stylize(
|
|
110
|
+
indent + ' key: ' + opts.stylize(toHex(this.key), 'string') + '\n' +
|
|
74
111
|
indent + ' discoveryKey: ' + opts.stylize(toHex(this.discoveryKey), 'string') + '\n' +
|
|
75
112
|
indent + ' opened: ' + opts.stylize(this.opened, 'boolean') + '\n' +
|
|
113
|
+
indent + ' closed: ' + opts.stylize(this.closed, 'boolean') + '\n' +
|
|
114
|
+
indent + ' snapshotted: ' + opts.stylize(this.snapshotted, 'boolean') + '\n' +
|
|
76
115
|
indent + ' writable: ' + opts.stylize(this.writable, 'boolean') + '\n' +
|
|
77
|
-
indent + ' sessions: ' + opts.stylize(this.sessions.length, 'number') + '\n' +
|
|
78
|
-
indent + ' peers: [ ' + opts.stylize(this.peers.length, 'number') + ' ]\n' +
|
|
79
116
|
indent + ' length: ' + opts.stylize(this.length, 'number') + '\n' +
|
|
80
117
|
indent + ' byteLength: ' + opts.stylize(this.byteLength, 'number') + '\n' +
|
|
118
|
+
indent + ' fork: ' + opts.stylize(this.fork, 'number') + '\n' +
|
|
119
|
+
indent + ' sessions: [ ' + opts.stylize(this.sessions.length, 'number') + ' ]\n' +
|
|
120
|
+
indent + ' activeRequests: [ ' + opts.stylize(this.activeRequests.length, 'number') + ' ]\n' +
|
|
121
|
+
indent + ' peers: ' + peers + '\n' +
|
|
81
122
|
indent + ')'
|
|
82
123
|
}
|
|
83
124
|
|
|
84
|
-
static
|
|
85
|
-
|
|
86
|
-
|
|
125
|
+
static getProtocolMuxer (stream) {
|
|
126
|
+
return stream.noiseStream.userData
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static createProtocolStream (isInitiator, opts = {}) {
|
|
130
|
+
let outerStream = Protomux.isProtomux(isInitiator)
|
|
131
|
+
? isInitiator.stream
|
|
132
|
+
: isStream(isInitiator)
|
|
133
|
+
? isInitiator
|
|
134
|
+
: opts.stream
|
|
135
|
+
|
|
136
|
+
let noiseStream = null
|
|
137
|
+
|
|
138
|
+
if (outerStream) {
|
|
139
|
+
noiseStream = outerStream.noiseStream
|
|
140
|
+
} else {
|
|
141
|
+
noiseStream = new NoiseSecretStream(isInitiator, null, opts)
|
|
142
|
+
outerStream = noiseStream.rawStream
|
|
143
|
+
}
|
|
144
|
+
if (!noiseStream) throw BAD_ARGUMENT('Invalid stream')
|
|
145
|
+
|
|
146
|
+
if (!noiseStream.userData) {
|
|
147
|
+
const protocol = new Protomux(noiseStream)
|
|
148
|
+
|
|
149
|
+
if (opts.ondiscoverykey) {
|
|
150
|
+
protocol.pair({ protocol: 'hypercore/alpha' }, opts.ondiscoverykey)
|
|
151
|
+
}
|
|
152
|
+
if (opts.keepAlive !== false) {
|
|
153
|
+
noiseStream.setKeepAlive(5000)
|
|
154
|
+
noiseStream.setTimeout(10000)
|
|
155
|
+
}
|
|
156
|
+
noiseStream.userData = protocol
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return outerStream
|
|
87
160
|
}
|
|
88
161
|
|
|
89
162
|
static defaultStorage (storage, opts = {}) {
|
|
@@ -98,45 +171,164 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
98
171
|
}
|
|
99
172
|
}
|
|
100
173
|
|
|
174
|
+
snapshot (opts) {
|
|
175
|
+
return this.session({ ...opts, snapshot: true })
|
|
176
|
+
}
|
|
177
|
+
|
|
101
178
|
session (opts = {}) {
|
|
102
179
|
if (this.closing) {
|
|
103
180
|
// This makes the closing logic alot easier. If this turns out to be a problem
|
|
104
181
|
// in practive, open an issue and we'll try to make a solution for it.
|
|
105
|
-
throw
|
|
182
|
+
throw SESSION_CLOSED('Cannot make sessions on a closing core')
|
|
106
183
|
}
|
|
107
184
|
|
|
108
185
|
const Clz = opts.class || Hypercore
|
|
109
|
-
const keyPair = opts.keyPair && opts.keyPair.secretKey && { ...opts.keyPair }
|
|
110
|
-
|
|
111
|
-
// This only works if the hypercore was fully loaded,
|
|
112
|
-
// but we only do this to validate the keypair to help catch bugs so yolo
|
|
113
|
-
if (this.key && keyPair) keyPair.publicKey = this.key
|
|
114
|
-
|
|
115
186
|
const s = new Clz(this.storage, this.key, {
|
|
116
187
|
...opts,
|
|
117
|
-
sign: opts.sign || (keyPair && keyPair.secretKey && Core.createSigner(this.crypto, keyPair)) || this.sign,
|
|
118
|
-
valueEncoding: this.valueEncoding,
|
|
119
|
-
extensions: this.extensions,
|
|
120
188
|
_opening: this.opening,
|
|
121
189
|
_sessions: this.sessions
|
|
122
190
|
})
|
|
123
191
|
|
|
124
|
-
s.
|
|
192
|
+
s._passCapabilities(this)
|
|
193
|
+
|
|
194
|
+
// Pass on the cache unless explicitly disabled.
|
|
195
|
+
if (opts.cache !== false) s.cache = this.cache
|
|
196
|
+
|
|
197
|
+
ensureEncryption(s, opts)
|
|
198
|
+
|
|
125
199
|
this.sessions.push(s)
|
|
126
200
|
|
|
127
201
|
return s
|
|
128
202
|
}
|
|
129
203
|
|
|
130
|
-
|
|
131
|
-
if (!this.
|
|
204
|
+
_passCapabilities (o) {
|
|
205
|
+
if (!this.auth) this.auth = o.auth
|
|
206
|
+
|
|
132
207
|
this.crypto = o.crypto
|
|
133
|
-
this.opened = o.opened
|
|
134
208
|
this.key = o.key
|
|
135
|
-
this.discoveryKey = o.discoveryKey
|
|
136
209
|
this.core = o.core
|
|
137
210
|
this.replicator = o.replicator
|
|
138
|
-
this.
|
|
211
|
+
this.encryption = o.encryption
|
|
212
|
+
this.writable = !!(this.auth && this.auth.sign)
|
|
139
213
|
this.autoClose = o.autoClose
|
|
214
|
+
|
|
215
|
+
if (this.snapshotted && this.core && !this._snapshot) this._updateSnapshot()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async _openFromExisting (from, opts) {
|
|
219
|
+
await from.opening
|
|
220
|
+
|
|
221
|
+
this._passCapabilities(from)
|
|
222
|
+
this.sessions = from.sessions
|
|
223
|
+
this.storage = from.storage
|
|
224
|
+
this.replicator.findingPeers += this._findingPeers
|
|
225
|
+
|
|
226
|
+
ensureEncryption(this, opts)
|
|
227
|
+
|
|
228
|
+
this.sessions.push(this)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async _openSession (key, storage, opts) {
|
|
232
|
+
const isFirst = !opts._opening
|
|
233
|
+
|
|
234
|
+
if (!isFirst) await opts._opening
|
|
235
|
+
if (opts.preload) opts = { ...opts, ...(await opts.preload()) }
|
|
236
|
+
|
|
237
|
+
const keyPair = (key && opts.keyPair)
|
|
238
|
+
? { ...opts.keyPair, publicKey: key }
|
|
239
|
+
: key
|
|
240
|
+
? { publicKey: key, secretKey: null }
|
|
241
|
+
: opts.keyPair
|
|
242
|
+
|
|
243
|
+
// This only works if the hypercore was fully loaded,
|
|
244
|
+
// but we only do this to validate the keypair to help catch bugs so yolo
|
|
245
|
+
if (this.key && keyPair) keyPair.publicKey = this.key
|
|
246
|
+
|
|
247
|
+
if (opts.auth) {
|
|
248
|
+
this.auth = opts.auth
|
|
249
|
+
} else if (opts.sign) {
|
|
250
|
+
this.auth = Core.createAuth(this.crypto, keyPair, opts)
|
|
251
|
+
} else if (keyPair && keyPair.secretKey) {
|
|
252
|
+
this.auth = Core.createAuth(this.crypto, keyPair)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (isFirst) {
|
|
256
|
+
await this._openCapabilities(keyPair, storage, opts)
|
|
257
|
+
// Only the root session should pass capabilities to other sessions.
|
|
258
|
+
for (let i = 0; i < this.sessions.length; i++) {
|
|
259
|
+
const s = this.sessions[i]
|
|
260
|
+
if (s !== this) s._passCapabilities(this)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!this.auth) this.auth = this.core.defaultAuth
|
|
265
|
+
this.writable = !!this.auth.sign
|
|
266
|
+
|
|
267
|
+
if (opts.valueEncoding) {
|
|
268
|
+
this.valueEncoding = c.from(codecs(opts.valueEncoding))
|
|
269
|
+
}
|
|
270
|
+
if (opts.encodeBatch) {
|
|
271
|
+
this.encodeBatch = opts.encodeBatch
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// This is a hidden option that's only used by Corestore.
|
|
275
|
+
// It's required so that corestore can load a name from userData before 'ready' is emitted.
|
|
276
|
+
if (opts._preready) await opts._preready(this)
|
|
277
|
+
|
|
278
|
+
this.opened = true
|
|
279
|
+
this.emit('ready')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async _openCapabilities (keyPair, storage, opts) {
|
|
283
|
+
if (opts.from) return this._openFromExisting(opts.from, opts)
|
|
284
|
+
|
|
285
|
+
this.storage = Hypercore.defaultStorage(opts.storage || storage)
|
|
286
|
+
|
|
287
|
+
this.core = await Core.open(this.storage, {
|
|
288
|
+
force: opts.force,
|
|
289
|
+
createIfMissing: opts.createIfMissing,
|
|
290
|
+
overwrite: opts.overwrite,
|
|
291
|
+
keyPair,
|
|
292
|
+
crypto: this.crypto,
|
|
293
|
+
legacy: opts.legacy,
|
|
294
|
+
auth: opts.auth,
|
|
295
|
+
onupdate: this._oncoreupdate.bind(this)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
if (opts.userData) {
|
|
299
|
+
for (const [key, value] of Object.entries(opts.userData)) {
|
|
300
|
+
await this.core.userData(key, value)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.key = this.core.header.signer.publicKey
|
|
305
|
+
this.keyPair = this.core.header.signer
|
|
306
|
+
|
|
307
|
+
this.replicator = new Replicator(this.core, this.key, {
|
|
308
|
+
eagerUpdate: true,
|
|
309
|
+
allowFork: opts.allowFork !== false,
|
|
310
|
+
onpeerupdate: this._onpeerupdate.bind(this),
|
|
311
|
+
onupload: this._onupload.bind(this)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
this.replicator.findingPeers += this._findingPeers
|
|
315
|
+
|
|
316
|
+
if (!this.encryption && opts.encryptionKey) {
|
|
317
|
+
this.encryption = new BlockEncryption(opts.encryptionKey, this.key)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
_updateSnapshot () {
|
|
322
|
+
const prev = this._snapshot
|
|
323
|
+
const next = this._snapshot = {
|
|
324
|
+
length: this.core.tree.length,
|
|
325
|
+
byteLength: this.core.tree.byteLength,
|
|
326
|
+
fork: this.core.tree.fork,
|
|
327
|
+
compatLength: this.core.tree.length
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!prev) return true
|
|
331
|
+
return prev.length !== next.length || prev.fork !== next.fork
|
|
140
332
|
}
|
|
141
333
|
|
|
142
334
|
close () {
|
|
@@ -155,6 +347,20 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
155
347
|
this.readable = false
|
|
156
348
|
this.writable = false
|
|
157
349
|
this.closed = true
|
|
350
|
+
this.opened = false
|
|
351
|
+
|
|
352
|
+
const gc = []
|
|
353
|
+
for (const ext of this.extensions.values()) {
|
|
354
|
+
if (ext.session === this) gc.push(ext)
|
|
355
|
+
}
|
|
356
|
+
for (const ext of gc) ext.destroy()
|
|
357
|
+
|
|
358
|
+
if (this.replicator !== null) {
|
|
359
|
+
this.replicator.findingPeers -= this._findingPeers
|
|
360
|
+
this.replicator.clearRequests(this.activeRequests)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this._findingPeers = 0
|
|
158
364
|
|
|
159
365
|
if (this.sessions.length) {
|
|
160
366
|
// if this is the last session and we are auto closing, trigger that first to enforce error handling
|
|
@@ -170,41 +376,54 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
170
376
|
}
|
|
171
377
|
|
|
172
378
|
replicate (isInitiator, opts = {}) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
let noiseStream = null
|
|
379
|
+
// Only limitation here is that ondiscoverykey doesn't work atm when passing a muxer directly,
|
|
380
|
+
// because it doesn't really make a lot of sense.
|
|
381
|
+
if (Protomux.isProtomux(isInitiator)) return this._attachToMuxer(isInitiator, opts)
|
|
177
382
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
outerStream = Hypercore.createProtocolStream(isInitiator, opts)
|
|
182
|
-
noiseStream = outerStream.noiseStream
|
|
183
|
-
}
|
|
184
|
-
if (!noiseStream) throw new Error('Invalid stream passed to replicate')
|
|
383
|
+
const protocolStream = Hypercore.createProtocolStream(isInitiator, opts)
|
|
384
|
+
const noiseStream = protocolStream.noiseStream
|
|
385
|
+
const protocol = noiseStream.userData
|
|
185
386
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
387
|
+
this._attachToMuxer(protocol, opts)
|
|
388
|
+
|
|
389
|
+
return protocolStream
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
_attachToMuxer (mux, opts) {
|
|
393
|
+
// If the user wants to, we can make this replication run in a session
|
|
394
|
+
// that way the core wont close "under them" during replication
|
|
395
|
+
if (opts.session) {
|
|
396
|
+
const s = this.session()
|
|
397
|
+
mux.stream.on('close', () => s.close().catch(noop))
|
|
190
398
|
}
|
|
191
399
|
|
|
192
|
-
const protocol = noiseStream.userData
|
|
193
400
|
if (this.opened) {
|
|
194
|
-
this.replicator.
|
|
401
|
+
this.replicator.attachTo(mux)
|
|
195
402
|
} else {
|
|
196
|
-
this.opening.then(() => this.replicator.
|
|
403
|
+
this.opening.then(() => this.replicator.attachTo(mux), mux.destroy.bind(mux))
|
|
197
404
|
}
|
|
198
405
|
|
|
199
|
-
return
|
|
406
|
+
return mux
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
get discoveryKey () {
|
|
410
|
+
return this.replicator === null ? null : this.replicator.discoveryKey
|
|
200
411
|
}
|
|
201
412
|
|
|
202
413
|
get length () {
|
|
203
|
-
return this.
|
|
414
|
+
return this._snapshot
|
|
415
|
+
? this._snapshot.length
|
|
416
|
+
: (this.core === null ? 0 : this.core.tree.length)
|
|
204
417
|
}
|
|
205
418
|
|
|
206
419
|
get byteLength () {
|
|
207
|
-
return this.
|
|
420
|
+
return this._snapshot
|
|
421
|
+
? this._snapshot.byteLength
|
|
422
|
+
: (this.core === null ? 0 : this.core.tree.byteLength - (this.core.tree.length * this.padding))
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
get contiguousLength () {
|
|
426
|
+
return this.core === null ? 0 : this.core.header.contiguousLength
|
|
208
427
|
}
|
|
209
428
|
|
|
210
429
|
get fork () {
|
|
@@ -215,105 +434,77 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
215
434
|
return this.replicator === null ? [] : this.replicator.peers
|
|
216
435
|
}
|
|
217
436
|
|
|
218
|
-
|
|
219
|
-
return this.
|
|
437
|
+
get encryptionKey () {
|
|
438
|
+
return this.encryption && this.encryption.key
|
|
220
439
|
}
|
|
221
440
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.valueEncoding = opts.valueEncoding ? c.from(codecs(opts.valueEncoding)) : null
|
|
226
|
-
|
|
227
|
-
const keyPair = (key && opts.keyPair)
|
|
228
|
-
? { ...opts.keyPair, publicKey: key }
|
|
229
|
-
: key
|
|
230
|
-
? { publicKey: key, secretKey: null }
|
|
231
|
-
: opts.keyPair
|
|
232
|
-
|
|
233
|
-
if (opts.from) {
|
|
234
|
-
const from = opts.from
|
|
235
|
-
await from.opening
|
|
236
|
-
for (const [name, ext] of this.extensions) from.extensions.register(name, null, ext)
|
|
237
|
-
this._initSession(from)
|
|
238
|
-
this.extensions = from.extensions
|
|
239
|
-
this.sessions = from.sessions
|
|
240
|
-
this.storage = from.storage
|
|
241
|
-
if (!this.sign) this.sign = opts.sign || ((keyPair && keyPair.secretKey) ? Core.createSigner(this.crypto, keyPair) : null)
|
|
242
|
-
this.writable = !!this.sign
|
|
243
|
-
this.sessions.push(this)
|
|
244
|
-
return
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (!this.storage) this.storage = Hypercore.defaultStorage(opts.storage || storage)
|
|
248
|
-
|
|
249
|
-
this.core = await Core.open(this.storage, {
|
|
250
|
-
keyPair,
|
|
251
|
-
crypto: this.crypto,
|
|
252
|
-
onupdate: this._oncoreupdate.bind(this)
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
if (opts.userData) {
|
|
256
|
-
for (const [key, value] of Object.entries(opts.userData)) {
|
|
257
|
-
await this.core.userData(key, value)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
this.replicator = new Replicator(this.core, {
|
|
262
|
-
onupdate: this._onpeerupdate.bind(this)
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
if (!this.sign) this.sign = opts.sign || this.core.defaultSign
|
|
266
|
-
|
|
267
|
-
this.discoveryKey = this.crypto.discoveryKey(this.core.header.signer.publicKey)
|
|
268
|
-
this.key = this.core.header.signer.publicKey
|
|
269
|
-
this.writable = !!this.sign
|
|
441
|
+
get padding () {
|
|
442
|
+
return this.encryption === null ? 0 : this.encryption.padding
|
|
443
|
+
}
|
|
270
444
|
|
|
271
|
-
|
|
272
|
-
this.
|
|
445
|
+
ready () {
|
|
446
|
+
return this.opening
|
|
447
|
+
}
|
|
273
448
|
|
|
274
|
-
|
|
449
|
+
_onupload (index, value, from) {
|
|
450
|
+
const byteLength = value.byteLength - this.padding
|
|
275
451
|
|
|
276
452
|
for (let i = 0; i < this.sessions.length; i++) {
|
|
277
|
-
|
|
278
|
-
if (s !== this) s._initSession(this)
|
|
279
|
-
s.emit('ready')
|
|
453
|
+
this.sessions[i].emit('upload', index, byteLength, from)
|
|
280
454
|
}
|
|
281
455
|
}
|
|
282
456
|
|
|
283
457
|
_oncoreupdate (status, bitfield, value, from) {
|
|
284
458
|
if (status !== 0) {
|
|
459
|
+
const truncated = (status & 0b10) !== 0
|
|
460
|
+
const appended = (status & 0b01) !== 0
|
|
461
|
+
|
|
462
|
+
if (truncated) {
|
|
463
|
+
this.replicator.ontruncate(bitfield.start)
|
|
464
|
+
}
|
|
465
|
+
|
|
285
466
|
for (let i = 0; i < this.sessions.length; i++) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
467
|
+
const s = this.sessions[i]
|
|
468
|
+
|
|
469
|
+
if (truncated) {
|
|
470
|
+
if (s.cache) s.cache.clear()
|
|
471
|
+
s.emit('truncate', bitfield.start, this.core.tree.fork)
|
|
472
|
+
// If snapshotted, make sure to update our compat so we can fail gets
|
|
473
|
+
if (s._snapshot && bitfield.start < s._snapshot.compatLength) s._snapshot.compatLength = bitfield.start
|
|
289
474
|
}
|
|
290
|
-
|
|
291
|
-
|
|
475
|
+
|
|
476
|
+
if (appended) {
|
|
477
|
+
s.emit('append')
|
|
292
478
|
}
|
|
293
479
|
}
|
|
294
480
|
|
|
295
|
-
this.replicator.
|
|
481
|
+
this.replicator.onupgrade()
|
|
296
482
|
}
|
|
297
483
|
|
|
298
|
-
if (bitfield
|
|
299
|
-
|
|
300
|
-
this.replicator.broadcastBlock(bitfield.start + i)
|
|
301
|
-
}
|
|
484
|
+
if (bitfield) {
|
|
485
|
+
this.replicator.onhave(bitfield.start, bitfield.length, bitfield.drop)
|
|
302
486
|
}
|
|
303
487
|
|
|
304
488
|
if (value) {
|
|
489
|
+
const byteLength = value.byteLength - this.padding
|
|
490
|
+
|
|
305
491
|
for (let i = 0; i < this.sessions.length; i++) {
|
|
306
|
-
this.sessions[i].emit('download', bitfield.start,
|
|
492
|
+
this.sessions[i].emit('download', bitfield.start, byteLength, from)
|
|
307
493
|
}
|
|
308
494
|
}
|
|
309
495
|
}
|
|
310
496
|
|
|
311
497
|
_onpeerupdate (added, peer) {
|
|
312
|
-
if (added) this.extensions.update(peer)
|
|
313
498
|
const name = added ? 'peer-add' : 'peer-remove'
|
|
314
499
|
|
|
315
500
|
for (let i = 0; i < this.sessions.length; i++) {
|
|
316
501
|
this.sessions[i].emit(name, peer)
|
|
502
|
+
|
|
503
|
+
if (added) {
|
|
504
|
+
for (const ext of this.sessions[i].extensions.values()) {
|
|
505
|
+
peer.extensions.set(ext.name, ext)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
317
508
|
}
|
|
318
509
|
}
|
|
319
510
|
|
|
@@ -330,19 +521,55 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
330
521
|
return null
|
|
331
522
|
}
|
|
332
523
|
|
|
333
|
-
|
|
524
|
+
findingPeers () {
|
|
525
|
+
this._findingPeers++
|
|
526
|
+
if (this.replicator !== null && !this.closing) this.replicator.findingPeers++
|
|
527
|
+
|
|
528
|
+
let once = true
|
|
529
|
+
|
|
530
|
+
return () => {
|
|
531
|
+
if (this.closing || !once) return
|
|
532
|
+
once = false
|
|
533
|
+
this._findingPeers--
|
|
534
|
+
if (this.replicator !== null && --this.replicator.findingPeers === 0) {
|
|
535
|
+
this.replicator.updateAll()
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async update (opts) {
|
|
334
541
|
if (this.opened === false) await this.opening
|
|
542
|
+
if (this.closing !== null) return false
|
|
543
|
+
|
|
335
544
|
// TODO: add an option where a writer can bootstrap it's state from the network also
|
|
336
|
-
if (this.writable)
|
|
337
|
-
|
|
545
|
+
if (this.writable) {
|
|
546
|
+
if (!this.snapshotted) return false
|
|
547
|
+
return this._updateSnapshot()
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const activeRequests = (opts && opts.activeRequests) || this.activeRequests
|
|
551
|
+
const req = this.replicator.addUpgrade(activeRequests)
|
|
552
|
+
|
|
553
|
+
if (!this.snapshotted) return req.promise
|
|
554
|
+
if (!(await req.promise)) return false
|
|
555
|
+
|
|
556
|
+
return this._updateSnapshot()
|
|
338
557
|
}
|
|
339
558
|
|
|
340
|
-
async seek (bytes) {
|
|
559
|
+
async seek (bytes, opts) {
|
|
341
560
|
if (this.opened === false) await this.opening
|
|
342
561
|
|
|
343
|
-
const s = this.core.tree.seek(bytes)
|
|
562
|
+
const s = this.core.tree.seek(bytes, this.padding)
|
|
563
|
+
|
|
564
|
+
const offset = await s.update()
|
|
565
|
+
if (offset) return offset
|
|
566
|
+
|
|
567
|
+
if (this.closing !== null) throw SESSION_CLOSED()
|
|
344
568
|
|
|
345
|
-
|
|
569
|
+
const activeRequests = (opts && opts.activeRequests) || this.activeRequests
|
|
570
|
+
const req = this.replicator.addSeek(activeRequests, s)
|
|
571
|
+
|
|
572
|
+
return req.promise
|
|
346
573
|
}
|
|
347
574
|
|
|
348
575
|
async has (index) {
|
|
@@ -353,41 +580,92 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
353
580
|
|
|
354
581
|
async get (index, opts) {
|
|
355
582
|
if (this.opened === false) await this.opening
|
|
356
|
-
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
583
|
+
if (this.closing !== null) throw SESSION_CLOSED()
|
|
584
|
+
if (this._snapshot !== null && index >= this._snapshot.compatLength) throw SNAPSHOT_NOT_AVAILABLE()
|
|
585
|
+
|
|
586
|
+
const encoding = (opts && opts.valueEncoding && c.from(codecs(opts.valueEncoding))) || this.valueEncoding
|
|
587
|
+
|
|
588
|
+
let req = this.cache && this.cache.get(index)
|
|
589
|
+
if (!req) req = this._get(index, opts)
|
|
590
|
+
|
|
591
|
+
let block = await req
|
|
592
|
+
if (!block) return null
|
|
593
|
+
|
|
594
|
+
if (this.encryption) {
|
|
595
|
+
// Copy the block as it might be shared with other sessions.
|
|
596
|
+
block = b4a.from(block)
|
|
597
|
+
|
|
598
|
+
this.encryption.decrypt(index, block)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return this._decode(encoding, block)
|
|
362
602
|
}
|
|
363
603
|
|
|
364
604
|
async _get (index, opts) {
|
|
365
|
-
|
|
605
|
+
let req
|
|
366
606
|
|
|
367
|
-
if (this.core.bitfield.get(index))
|
|
368
|
-
|
|
607
|
+
if (this.core.bitfield.get(index)) {
|
|
608
|
+
req = this.core.blocks.get(index)
|
|
609
|
+
|
|
610
|
+
if (this.cache) this.cache.set(index, req)
|
|
611
|
+
} else {
|
|
612
|
+
if (opts && opts.wait === false) return null
|
|
613
|
+
if (opts && opts.onwait) opts.onwait(index)
|
|
614
|
+
|
|
615
|
+
const activeRequests = (opts && opts.activeRequests) || this.activeRequests
|
|
616
|
+
|
|
617
|
+
req = this._cacheOnResolve(
|
|
618
|
+
index,
|
|
619
|
+
this.replicator
|
|
620
|
+
.addBlock(activeRequests, index)
|
|
621
|
+
.promise,
|
|
622
|
+
this.core.tree.fork
|
|
623
|
+
)
|
|
624
|
+
}
|
|
369
625
|
|
|
370
|
-
return
|
|
626
|
+
return req
|
|
371
627
|
}
|
|
372
628
|
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
const end = typeof (range && range.end) === 'number' ? range.end : -1 // download all
|
|
376
|
-
const linear = !!(range && range.linear)
|
|
629
|
+
async _cacheOnResolve (index, req, fork) {
|
|
630
|
+
const block = await req
|
|
377
631
|
|
|
378
|
-
|
|
632
|
+
if (this.cache && fork === this.core.tree.fork) {
|
|
633
|
+
this.cache.set(index, Promise.resolve(block))
|
|
634
|
+
}
|
|
379
635
|
|
|
380
|
-
|
|
636
|
+
return block
|
|
637
|
+
}
|
|
381
638
|
|
|
382
|
-
|
|
383
|
-
|
|
639
|
+
createReadStream (opts) {
|
|
640
|
+
return new ReadStream(this, opts)
|
|
641
|
+
}
|
|
384
642
|
|
|
385
|
-
|
|
643
|
+
createWriteStream (opts) {
|
|
644
|
+
return new WriteStream(this, opts)
|
|
386
645
|
}
|
|
387
646
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
647
|
+
download (range) {
|
|
648
|
+
const reqP = this._download(range)
|
|
649
|
+
|
|
650
|
+
// do not crash in the background...
|
|
651
|
+
reqP.catch(noop)
|
|
652
|
+
|
|
653
|
+
// TODO: turn this into an actual object...
|
|
654
|
+
return {
|
|
655
|
+
async downloaded () {
|
|
656
|
+
const req = await reqP
|
|
657
|
+
return req.promise
|
|
658
|
+
},
|
|
659
|
+
destroy () {
|
|
660
|
+
reqP.then(req => req.context && req.context.detach(req), noop)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async _download (range) {
|
|
666
|
+
if (this.opened === false) await this.opening
|
|
667
|
+
const activeRequests = (range && range.activeRequests) || this.activeRequests
|
|
668
|
+
return this.replicator.addRange(activeRequests, range)
|
|
391
669
|
}
|
|
392
670
|
|
|
393
671
|
// TODO: get rid of this / deprecate it?
|
|
@@ -395,12 +673,17 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
395
673
|
range.destroy(null)
|
|
396
674
|
}
|
|
397
675
|
|
|
676
|
+
// TODO: get rid of this / deprecate it?
|
|
677
|
+
cancel (request) {
|
|
678
|
+
// Do nothing for now
|
|
679
|
+
}
|
|
680
|
+
|
|
398
681
|
async truncate (newLength = 0, fork = -1) {
|
|
399
682
|
if (this.opened === false) await this.opening
|
|
400
|
-
if (this.writable === false) throw
|
|
683
|
+
if (this.writable === false) throw SESSION_NOT_WRITABLE()
|
|
401
684
|
|
|
402
685
|
if (fork === -1) fork = this.core.tree.fork + 1
|
|
403
|
-
await this.core.truncate(newLength, fork, this.
|
|
686
|
+
await this.core.truncate(newLength, fork, this.auth)
|
|
404
687
|
|
|
405
688
|
// TODO: Should propagate from an event triggered by the oplog
|
|
406
689
|
this.replicator.updateAll()
|
|
@@ -408,42 +691,108 @@ module.exports = class Hypercore extends EventEmitter {
|
|
|
408
691
|
|
|
409
692
|
async append (blocks) {
|
|
410
693
|
if (this.opened === false) await this.opening
|
|
411
|
-
if (this.writable === false) throw
|
|
694
|
+
if (this.writable === false) throw SESSION_NOT_WRITABLE()
|
|
412
695
|
|
|
413
|
-
|
|
414
|
-
const buffers = new Array(blks.length)
|
|
696
|
+
blocks = Array.isArray(blocks) ? blocks : [blocks]
|
|
415
697
|
|
|
416
|
-
|
|
417
|
-
const blk = blks[i]
|
|
698
|
+
const preappend = this.encryption && this._preappend
|
|
418
699
|
|
|
419
|
-
|
|
420
|
-
? blk
|
|
421
|
-
: this.valueEncoding
|
|
422
|
-
? c.encode(this.valueEncoding, blk)
|
|
423
|
-
: Buffer.from(blk)
|
|
700
|
+
const buffers = this.encodeBatch !== null ? this.encodeBatch(blocks) : new Array(blocks.length)
|
|
424
701
|
|
|
425
|
-
|
|
702
|
+
if (this.encodeBatch === null) {
|
|
703
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
704
|
+
buffers[i] = this._encode(this.valueEncoding, blocks[i])
|
|
705
|
+
}
|
|
426
706
|
}
|
|
427
707
|
|
|
428
|
-
return await this.core.append(buffers, this.
|
|
708
|
+
return await this.core.append(buffers, this.auth, { preappend })
|
|
429
709
|
}
|
|
430
710
|
|
|
431
|
-
|
|
432
|
-
|
|
711
|
+
async treeHash (length) {
|
|
712
|
+
if (length === undefined) {
|
|
713
|
+
await this.ready()
|
|
714
|
+
length = this.core.length
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const roots = await this.core.tree.getRoots(length)
|
|
718
|
+
return this.crypto.tree(roots)
|
|
433
719
|
}
|
|
434
720
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
721
|
+
registerExtension (name, handlers = {}) {
|
|
722
|
+
if (this.extensions.has(name)) {
|
|
723
|
+
const ext = this.extensions.get(name)
|
|
724
|
+
ext.handlers = handlers
|
|
725
|
+
ext.encoding = c.from(codecs(handlers.encoding) || c.buffer)
|
|
726
|
+
ext.session = this
|
|
727
|
+
return ext
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const ext = {
|
|
731
|
+
name,
|
|
732
|
+
handlers,
|
|
733
|
+
encoding: c.from(codecs(handlers.encoding) || c.buffer),
|
|
734
|
+
session: this,
|
|
735
|
+
send (message, peer) {
|
|
736
|
+
const buffer = c.encode(this.encoding, message)
|
|
737
|
+
peer.extension(name, buffer)
|
|
738
|
+
},
|
|
739
|
+
broadcast (message) {
|
|
740
|
+
const buffer = c.encode(this.encoding, message)
|
|
741
|
+
for (const peer of this.session.peers) {
|
|
742
|
+
peer.extension(name, buffer)
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
destroy () {
|
|
746
|
+
for (const peer of this.session.peers) {
|
|
747
|
+
if (peer.extensions.get(name) === ext) peer.extensions.delete(name)
|
|
748
|
+
}
|
|
749
|
+
this.session.extensions.delete(name)
|
|
750
|
+
},
|
|
751
|
+
_onmessage (state, peer) {
|
|
752
|
+
const m = this.encoding.decode(state)
|
|
753
|
+
if (this.handlers.onmessage) this.handlers.onmessage(m, peer)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
this.extensions.set(name, ext)
|
|
758
|
+
for (const peer of this.peers) {
|
|
759
|
+
peer.extensions.set(name, ext)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return ext
|
|
438
763
|
}
|
|
439
|
-
}
|
|
440
764
|
|
|
441
|
-
|
|
765
|
+
_encode (enc, val) {
|
|
766
|
+
const state = { start: this.padding, end: this.padding, buffer: null }
|
|
767
|
+
|
|
768
|
+
if (b4a.isBuffer(val)) {
|
|
769
|
+
if (state.start === 0) return val
|
|
770
|
+
state.end += val.byteLength
|
|
771
|
+
} else if (enc) {
|
|
772
|
+
enc.preencode(state, val)
|
|
773
|
+
} else {
|
|
774
|
+
val = b4a.from(val)
|
|
775
|
+
if (state.start === 0) return val
|
|
776
|
+
state.end += val.byteLength
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
state.buffer = b4a.allocUnsafe(state.end)
|
|
780
|
+
|
|
781
|
+
if (enc) enc.encode(state, val)
|
|
782
|
+
else state.buffer.set(val, state.start)
|
|
442
783
|
|
|
443
|
-
|
|
444
|
-
|
|
784
|
+
return state.buffer
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
_decode (enc, block) {
|
|
788
|
+
if (this.padding) block = block.subarray(this.padding)
|
|
789
|
+
if (enc) return c.decode(enc, block)
|
|
790
|
+
return block
|
|
791
|
+
}
|
|
445
792
|
}
|
|
446
793
|
|
|
794
|
+
function noop () {}
|
|
795
|
+
|
|
447
796
|
function isStream (s) {
|
|
448
797
|
return typeof s === 'object' && s && typeof s.pipe === 'function'
|
|
449
798
|
}
|
|
@@ -457,5 +806,22 @@ function requireMaybe (name) {
|
|
|
457
806
|
}
|
|
458
807
|
|
|
459
808
|
function toHex (buf) {
|
|
460
|
-
return buf &&
|
|
809
|
+
return buf && b4a.toString(buf, 'hex')
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function preappend (blocks) {
|
|
813
|
+
const offset = this.core.tree.length
|
|
814
|
+
const fork = this.core.tree.fork
|
|
815
|
+
|
|
816
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
817
|
+
this.encryption.encrypt(offset + i, blocks[i], fork)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function ensureEncryption (core, opts) {
|
|
822
|
+
if (!opts.encryptionKey) return
|
|
823
|
+
// Only override the block encryption if its either not already set or if
|
|
824
|
+
// the caller provided a different key.
|
|
825
|
+
if (core.encryption && b4a.equals(core.encryption.key, opts.encryptionKey)) return
|
|
826
|
+
core.encryption = new BlockEncryption(opts.encryptionKey, core.key)
|
|
461
827
|
}
|