corestore 6.18.3 → 7.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 (4) hide show
  1. package/README.md +12 -38
  2. package/index.js +299 -459
  3. package/package.json +29 -30
  4. package/LICENSE +0 -21
package/README.md CHANGED
@@ -13,6 +13,11 @@ Corestore provides:
13
13
  ### Installation
14
14
  `npm install corestore`
15
15
 
16
+ > [!NOTE]
17
+ > This readme reflects Corestore 7, our latest major version that is backed by RocksDB for storage and atomicity.
18
+ > Whilst we are fully validating that, the npm dist-tag for latest is set to latest version of Corestore 7, the previous major, to avoid too much disruption.
19
+ > It will be updated to 11 in a few weeks.
20
+
16
21
  ### Usage
17
22
  A corestore instance can be constructed with a random-access-storage module, a function that returns a random-access-storage module given a path, or a string. If a string is specified, it will be assumed to be a path to a local storage directory:
18
23
  ```js
@@ -24,27 +29,17 @@ const core2 = store.get({ name: 'core-2' })
24
29
  ```
25
30
 
26
31
  ### API
27
- #### `const store = new Corestore(storage, opts = {})`
32
+ #### `const store = new Corestore(storage)`
28
33
  Create a new Corestore instance.
29
34
 
30
35
  `storage` can be either a random-access-storage module, a string, or a function that takes a path and returns an random-access-storage instance.
31
36
 
32
- Opts include:
33
- ```js
34
- {
35
- inflightRange: null // Advanced option. Forwarded to the Hypercores created by corestore.get(...)
36
- }
37
- ```
38
-
39
- #### `const core = store.get(key | { name: 'a-name', exclusive, ...hypercoreOpts})`
37
+ #### `const core = store.get(key | { name: 'a-name', ...hypercoreOpts})`
40
38
  Loads a Hypercore, either by name (if the `name` option is provided), or from the provided key (if the first argument is a Buffer or String with hex/z32 key, or if the `key` options is set).
41
39
 
42
40
  If that Hypercore has previously been loaded, subsequent calls to `get` will return a new Hypercore session on the existing core.
43
41
 
44
- If you set the `exclusive` option and you are opening a writable session it will wait for all other exclusive writable to close before
45
- opening the Hypercore effectively meaning any op on the core will wait until its exclusive.
46
-
47
- All other options besides `name` and `key` and `exclusive` will be forwarded to the Hypercore constructor.
42
+ All other options besides `name` and `key` will be forwarded to the Hypercore constructor.
48
43
 
49
44
  #### `const stream = store.replicate(optsOrStream)`
50
45
  Creates a replication stream that's capable of replicating all Hypercores that are managed by the Corestore, assuming the remote peer has the correct capabilities.
@@ -67,8 +62,11 @@ swarm.join(...)
67
62
  swarm.on('connection', (connection) => store.replicate(connection))
68
63
  ```
69
64
 
65
+ #### `const storeB = storeA.session()`
66
+ Create a new Corestore session. Closing a session will close all cores made from this session.
67
+
70
68
  #### `const store = store.namespace(name)`
71
- Create a new namespaced Corestore. Namespacing is useful if you're going to be sharing a single Corestore instance between many applications or components, as it prevents name collisions.
69
+ Create a new namespaced Corestore session. Namespacing is useful if you're going to be sharing a single Corestore instance between many applications or components, as it prevents name collisions.
72
70
 
73
71
  Namespaces can be chained:
74
72
  ```js
@@ -78,32 +76,8 @@ const core1 = ns1.get({ name: 'main' }) // These will load different Hypercores
78
76
  const core2 = ns2.get({ name: 'main' })
79
77
  ```
80
78
 
81
- #### `const storeB = storeA.session(opts)`
82
- Create a new Corestore that shares resources with the original, like cache, cores, replication streams, and storage, while optionally resetting the namespace, overriding `primaryKey`.
83
- Useful when an application wants to accept an optional Corestore, but needs to maintain a predictable key derivation.
84
-
85
- `opts` are the same as the constructor options:
86
-
87
- ```js
88
- {
89
- primaryKey, // Overrides the primaryKey for this session
90
- namespace, // If set to null it will reset to the DEFAULT_NAMESPACE
91
- detach: true, // By disabling this, closing the session will also close the store that created the session
92
- inflightRange: null // Advanced option. Forwarded to the Hypercores created by `session.get(...)
93
- }
94
- ```
95
-
96
79
  #### `await store.close()`
97
80
  Fully close this Corestore instance.
98
81
 
99
- #### `store.on('core-open', core)`
100
- Emitted when the first session for a core is opened.
101
-
102
- *Note: This core may close at any time, so treat it as a weak reference*
103
-
104
- #### `store.on('core-close', core)`
105
- Emitted when the last session for a core is closed.
106
-
107
82
  ### License
108
83
  MIT
109
-
package/index.js CHANGED
@@ -1,567 +1,407 @@
1
- const safetyCatch = require('safety-catch')
2
- const crypto = require('hypercore-crypto')
3
- const sodium = require('sodium-universal')
4
- const Hypercore = require('hypercore')
5
- const hypercoreId = require('hypercore-id-encoding')
6
- const Xache = require('xache')
7
1
  const b4a = require('b4a')
2
+ const Hypercore = require('hypercore')
8
3
  const ReadyResource = require('ready-resource')
9
- const RW = require('read-write-mutexify')
4
+ const EventEmitter = require('events')
5
+ const sodium = require('sodium-universal')
6
+ const crypto = require('hypercore-crypto')
7
+ const ID = require('hypercore-id-encoding')
10
8
 
11
9
  const [NS] = crypto.namespace('corestore', 1)
12
10
  const DEFAULT_NAMESPACE = b4a.alloc(32) // This is meant to be 32 0-bytes
13
11
 
14
- const CORES_DIR = 'cores'
15
- const PRIMARY_KEY_FILE_NAME = 'primary-key'
16
- const USERDATA_NAME_KEY = 'corestore/name'
17
- const USERDATA_NAMESPACE_KEY = 'corestore/namespace'
18
- const POOL_SIZE = 512 // how many open fds to aim for before cycling them
19
- const DEFAULT_MANIFEST = 0 // bump to 1 when this is more widely deployed
20
- const DEFAULT_COMPAT = true
21
-
22
- module.exports = class Corestore extends ReadyResource {
23
- constructor (storage, opts = {}) {
24
- super()
25
-
26
- const root = opts._root
27
-
28
- this.storage = Hypercore.defaultStorage(storage, { lock: PRIMARY_KEY_FILE_NAME, poolSize: opts.poolSize || POOL_SIZE, rmdir: true })
29
- this.cores = root ? root.cores : new Map()
30
- this.cache = !!opts.cache
31
- this.primaryKey = opts.primaryKey || null
32
- this.passive = !!opts.passive
33
- this.manifestVersion = typeof opts.manifestVersion === 'number' ? opts.manifestVersion : (root ? root.manifestVersion : DEFAULT_MANIFEST)
34
- this.compat = typeof opts.compat === 'boolean' ? opts.compat : (root ? root.compat : DEFAULT_COMPAT)
35
- this.inflightRange = opts.inflightRange || null
36
- this.globalCache = opts.globalCache || null
37
-
38
- this._keyStorage = null
39
- this._bootstrap = opts._bootstrap || null
40
- this._namespace = opts.namespace || DEFAULT_NAMESPACE
41
- this._noCoreCache = root ? root._noCoreCache : new Xache({ maxSize: 65536 })
42
-
43
- this._root = root || this
44
- this._replicationStreams = root ? root._replicationStreams : []
45
- this._overwrite = opts.overwrite === true
46
- this._readonly = opts.writable === false
47
- this._attached = opts._attached || null
48
- this._notDownloadingLinger = opts.notDownloadingLinger
49
-
50
- this._sessions = new Set() // sessions for THIS namespace
51
- this._rootStoreSessions = new Set()
52
- this._locks = root ? root._locks : new Map()
53
-
54
- this._findingPeersCount = 0
55
- this._findingPeers = []
56
- this._isCorestore = true
57
-
58
- if (this._namespace.byteLength !== 32) throw new Error('Namespace must be a 32-byte Buffer or Uint8Array')
59
- this.ready().catch(safetyCatch)
12
+ class StreamTracker {
13
+ constructor () {
14
+ this.records = []
60
15
  }
61
16
 
62
- static isCorestore (obj) {
63
- return !!(typeof obj === 'object' && obj && obj._isCorestore)
17
+ add (stream, isExternal) {
18
+ const record = { index: 0, stream, isExternal }
19
+ this.records.push(record)
20
+ return record
64
21
  }
65
22
 
66
- static from (storage, opts) {
67
- return this.isCorestore(storage) ? storage : new this(storage, opts)
23
+ remove (record) {
24
+ const popped = this.records.pop()
25
+ if (popped === record) return
26
+ this.records[popped.index = record.index] = popped
68
27
  }
69
28
 
70
- // for now just release the lock...
71
- async suspend () {
72
- if (this._root !== this) return this._root.suspend()
73
-
74
- await this.ready()
75
-
76
- if (this._keyStorage !== null) {
77
- await new Promise((resolve, reject) => {
78
- this._keyStorage.suspend((err) => {
79
- if (err) return reject(err)
80
- resolve()
81
- })
82
- })
29
+ attachAll (core) {
30
+ for (let i = 0; i < this.records.length; i++) {
31
+ const record = this.records[i]
32
+ const muxer = record.stream.noiseStream.userData
33
+ if (!core.replicator.attached(muxer)) core.replicator.attachTo(muxer)
83
34
  }
84
35
  }
85
36
 
86
- async resume () {
87
- if (this._root !== this) return this._root.resume()
88
-
89
- await this.ready()
90
-
91
- if (this._keyStorage !== null) {
92
- await new Promise((resolve, reject) => {
93
- this._keyStorage.open((err) => {
94
- if (err) return reject(err)
95
- resolve()
96
- })
97
- })
37
+ destroy () {
38
+ // reverse is safer cause we delete mb
39
+ for (let i = this.records.length - 1; i >= 0; i--) {
40
+ const record = this.records[i]
41
+ if (!record.isExternal) record.stream.destroy()
98
42
  }
99
43
  }
44
+ }
100
45
 
101
- findingPeers () {
102
- let done = false
103
- this._incFindingPeers()
104
-
105
- return () => {
106
- if (done) return
107
- done = true
108
- this._decFindingPeers()
109
- }
46
+ class SessionTracker {
47
+ constructor () {
48
+ this.map = new Map()
110
49
  }
111
50
 
112
- _emitCore (name, core) {
113
- this.emit(name, core)
114
- for (const session of this._root._rootStoreSessions) {
115
- if (session !== this) {
116
- session.emit(name, core)
117
- }
118
- }
119
- if (this !== this._root) this._root.emit(name, core)
51
+ get size () {
52
+ return this.map.size
120
53
  }
121
54
 
122
- _incFindingPeers () {
123
- if (++this._findingPeersCount !== 1) return
124
-
125
- for (const core of this._sessions) {
126
- this._findingPeers.push(core.findingPeers())
127
- }
55
+ get (id) {
56
+ const existing = this.map.get(id)
57
+ if (existing !== undefined) return existing
58
+ const fresh = []
59
+ this.map.set(id, fresh)
60
+ return fresh
128
61
  }
129
62
 
130
- _decFindingPeers () {
131
- if (--this._findingPeersCount !== 0) return
132
-
133
- while (this._findingPeers.length > 0) {
134
- this._findingPeers.pop()()
135
- }
63
+ gc (id) {
64
+ this.map.delete(id)
136
65
  }
137
66
 
138
- async _openNamespaceFromBootstrap () {
139
- const ns = await this._bootstrap.getUserData(USERDATA_NAMESPACE_KEY)
140
- if (ns) {
141
- this._namespace = ns
142
- }
67
+ list (id) {
68
+ return id ? (this.map.get(id) || []) : [...this]
143
69
  }
144
70
 
145
- async _open () {
146
- if (this._root !== this) {
147
- await this._root.ready()
148
- if (!this.primaryKey) this.primaryKey = this._root.primaryKey
149
- if (this._bootstrap) await this._openNamespaceFromBootstrap()
150
- return
71
+ * [Symbol.iterator] () {
72
+ for (const sessions of this.map.values()) {
73
+ yield * sessions[Symbol.iterator]()
151
74
  }
75
+ }
76
+ }
152
77
 
153
- this._keyStorage = this.storage(PRIMARY_KEY_FILE_NAME)
154
-
155
- this.primaryKey = await new Promise((resolve, reject) => {
156
- this._keyStorage.stat((err, st) => {
157
- if (err && err.code !== 'ENOENT') return reject(err)
158
- if (err || st.size < 32 || this._overwrite) {
159
- const key = this.primaryKey || crypto.randomBytes(32)
160
- return this._keyStorage.write(0, key, err => {
161
- if (err) return reject(err)
162
- return resolve(key)
163
- })
164
- }
165
- this._keyStorage.read(0, 32, (err, key) => {
166
- if (err) return reject(err)
167
- if (this.primaryKey) return resolve(this.primaryKey)
168
- return resolve(key)
169
- })
170
- })
171
- })
78
+ class CoreTracker extends EventEmitter {
79
+ constructor () {
80
+ super()
81
+ this.map = new Map()
82
+ this.watching = []
83
+ }
172
84
 
173
- if (this._bootstrap) await this._openNamespaceFromBootstrap()
85
+ get size () {
86
+ return this.map.size
174
87
  }
175
88
 
176
- async _exists (discoveryKey) {
177
- const id = b4a.toString(discoveryKey, 'hex')
178
- const storageRoot = getStorageRoot(id)
89
+ watch (store) {
90
+ if (store.watchIndex !== -1) return
91
+ store.watchIndex = this.watching.push(store) - 1
92
+ }
179
93
 
180
- const st = this.storage(storageRoot + '/oplog')
94
+ unwatch (store) {
95
+ if (store.watchIndex === -1) return
96
+ const head = this.watching.pop()
97
+ if (head !== store) this.watching[(head.watchIndex = store.watchIndex)] = head
98
+ store.watchIndex = -1
99
+ }
181
100
 
182
- const exists = await new Promise((resolve) => st.stat((err, st) => resolve(!err && st.size > 0)))
183
- await new Promise(resolve => st.close(resolve))
101
+ get (id) {
102
+ // we allow you do call this from the outside, so support normal buffers also
103
+ if (b4a.isBuffer(id)) id = b4a.toString(id, 'hex')
104
+ return this.map.get(id) || null
105
+ }
184
106
 
185
- return exists
107
+ set (id, core) {
108
+ this.map.set(id, core)
109
+ this.emit('add', core) // TODO: will be removed
110
+ if (this.watching.length > 0) this._emit(core)
186
111
  }
187
112
 
188
- async _generateKeys (opts) {
189
- if (opts._discoveryKey) {
190
- return {
191
- manifest: null,
192
- keyPair: null,
193
- key: null,
194
- discoveryKey: opts._discoveryKey
195
- }
113
+ _emit (core) {
114
+ for (let i = this.watching.length - 1; i >= 0; i--) {
115
+ const store = this.watching[i]
116
+ for (const fn of store.watchers) fn(core)
196
117
  }
118
+ }
197
119
 
198
- const keyPair = opts.name
199
- ? await this.createKeyPair(opts.name)
200
- : (opts.secretKey)
201
- ? { secretKey: opts.secretKey, publicKey: opts.publicKey }
202
- : null
120
+ delete (id, core) {
121
+ this.map.delete(id)
122
+ this.emit('remove', core) // TODO: will be removed
123
+ }
203
124
 
204
- if (opts.manifest) {
205
- const key = Hypercore.key(opts.manifest)
125
+ [Symbol.iterator] () {
126
+ return this.map.values()
127
+ }
128
+ }
206
129
 
207
- return {
208
- manifest: opts.manifest,
209
- keyPair,
210
- key,
211
- discoveryKey: crypto.discoveryKey(key)
212
- }
213
- }
130
+ class Corestore extends ReadyResource {
131
+ constructor (storage, opts = {}) {
132
+ super()
214
133
 
215
- if (opts.key) {
216
- return {
217
- manifest: null,
218
- keyPair,
219
- key: opts.key,
220
- discoveryKey: crypto.discoveryKey(opts.key)
221
- }
222
- }
134
+ this.root = opts.root || null
135
+ this.storage = this.root ? this.root.storage : Hypercore.defaultStorage(storage)
136
+ this.streamTracker = this.root ? this.root.streamTracker : new StreamTracker()
137
+ this.cores = this.root ? this.root.cores : new CoreTracker()
138
+ this.sessions = new SessionTracker()
139
+ this.globalCache = this.root ? this.root.globalCache : (opts.globalCache || null)
140
+ this.primaryKey = this.root ? this.root.primaryKey : (opts.primaryKey || null)
141
+ this.ns = opts.namespace || DEFAULT_NAMESPACE
223
142
 
224
- const publicKey = opts.publicKey || keyPair.publicKey
143
+ this.watchers = null
144
+ this.watchIndex = -1
225
145
 
226
- if (opts.compat === false || (opts.compat !== true && !this.compat)) {
227
- let manifest = { version: this.manifestVersion, signers: [{ publicKey }] } // default manifest
228
- let key = Hypercore.key(manifest)
229
- let discoveryKey = crypto.discoveryKey(key)
146
+ this.manifestVersion = 1 // just compat
230
147
 
231
- if (!(await this._exists(discoveryKey)) && manifest.version !== 0) {
232
- const manifestV0 = { version: 0, signers: [{ publicKey }] }
233
- const keyV0 = Hypercore.key(manifestV0)
234
- const discoveryKeyV0 = crypto.discoveryKey(keyV0)
148
+ this._ongcBound = this._ongc.bind(this)
235
149
 
236
- if (await this._exists(discoveryKeyV0)) {
237
- manifest = manifestV0
238
- key = keyV0
239
- discoveryKey = discoveryKeyV0
240
- }
241
- }
150
+ this.ready().catch(noop)
151
+ }
242
152
 
243
- return {
244
- manifest,
245
- keyPair,
246
- key,
247
- discoveryKey
248
- }
153
+ watch (fn) {
154
+ if (this.watchers === null) {
155
+ this.watchers = new Set()
156
+ this.cores.watch(this)
249
157
  }
250
158
 
251
- return {
252
- manifest: null,
253
- keyPair,
254
- key: publicKey,
255
- discoveryKey: crypto.discoveryKey(publicKey)
256
- }
159
+ this.watchers.add(fn)
257
160
  }
258
161
 
259
- _getPrereadyUserData (core, key) {
260
- // Need to manually read the header values before the Hypercore is ready, hence the ugliness.
261
- for (const { key: savedKey, value } of core.core.header.userData) {
262
- if (key === savedKey) return value
263
- }
264
- return null
265
- }
162
+ unwatch (fn) {
163
+ if (this.watchers === null) return
266
164
 
267
- async _preready (core) {
268
- const name = this._getPrereadyUserData(core, USERDATA_NAME_KEY)
269
- if (!name) return
165
+ this.watchers.delete(fn)
270
166
 
271
- const namespace = this._getPrereadyUserData(core, USERDATA_NAMESPACE_KEY)
272
- const keyPair = await this.createKeyPair(b4a.toString(name), namespace)
273
- core.setKeyPair(keyPair)
167
+ if (this.watchers.size === 0) {
168
+ this.watchers = null
169
+ this.cores.unwatch(this)
170
+ }
274
171
  }
275
172
 
276
- _getLock (id) {
277
- let rw = this._locks.get(id)
278
-
279
- if (!rw) {
280
- rw = new RW()
281
- this._locks.set(id, rw)
282
- }
173
+ session (opts) {
174
+ const root = this.root || this
175
+ return new Corestore(null, { ...opts, root })
176
+ }
283
177
 
284
- return rw
178
+ namespace (name, opts) {
179
+ return this.session({ ...opts, namespace: generateNamespace(this.ns, name) })
285
180
  }
286
181
 
287
- async _preload (id, keys, opts) {
288
- const { manifest, keyPair, key } = keys
182
+ _ongc (session) {
183
+ if (session.sessions.length === 0) this.sessions.gc(session.id)
184
+ }
289
185
 
290
- while (this.cores.has(id)) {
291
- const existing = this.cores.get(id)
292
- if (existing.opened && !existing.closing) return { from: existing, keyPair, manifest, cache: !!opts.cache }
293
- if (existing.closing) {
294
- await existing.close()
295
- } else {
296
- await existing.ready().catch(safetyCatch)
297
- }
298
- }
186
+ async _getOrSetSeed () {
187
+ const seed = await this.storage.getSeed()
188
+ if (seed !== null) return seed
189
+ return await this.storage.setSeed(this.primaryKey || crypto.randomBytes(32))
190
+ }
299
191
 
300
- const hasKeyPair = !!(keyPair && keyPair.secretKey)
301
- const userData = {}
302
- if (opts.name) {
303
- userData[USERDATA_NAME_KEY] = b4a.from(opts.name)
304
- userData[USERDATA_NAMESPACE_KEY] = this._namespace
192
+ async _open () {
193
+ if (this.root !== null) {
194
+ if (this.root.opened === false) await this.root.ready()
195
+ this.primaryKey = this.root.primaryKey
196
+ return
305
197
  }
306
198
 
307
- // No more async ticks allowed after this point -- necessary for caching
308
-
309
- const storageRoot = getStorageRoot(id)
310
- const core = new Hypercore(p => this.storage(storageRoot + '/' + p), {
311
- _preready: this._preready.bind(this),
312
- notDownloadingLinger: this._notDownloadingLinger,
313
- inflightRange: this.inflightRange,
314
- autoClose: true,
315
- active: false,
316
- encryptionKey: opts.encryptionKey || null,
317
- isBlockKey: !!opts.isBlockKey,
318
- userData,
319
- manifest,
320
- key,
321
- compat: opts.compat,
322
- cache: opts.cache,
323
- globalCache: this.globalCache,
324
- createIfMissing: opts.createIfMissing === false ? false : !opts._discoveryKey,
325
- keyPair: hasKeyPair ? keyPair : null
326
- })
199
+ const primaryKey = await this._getOrSetSeed()
327
200
 
328
- if (this._root.closing) {
329
- try {
330
- await core.close()
331
- } catch {}
332
- throw new Error('The corestore is closed')
201
+ if (this.primaryKey === null) {
202
+ this.primaryKey = primaryKey
203
+ return
333
204
  }
334
205
 
335
- this.cores.set(id, core)
336
- this._noCoreCache.delete(id)
337
- core.ready().then(() => {
338
- if (core.closing) return // extra safety here as ready is a tick after open
339
- if (hasKeyPair) core.setKeyPair(keyPair)
340
- this._emitCore('core-open', core)
341
- if (this.passive) return
342
-
343
- const ondownloading = () => {
344
- for (const { stream } of this._replicationStreams) {
345
- core.replicate(stream, { session: true })
346
- }
347
- }
348
- // when the replicator says we are downloading, answer the call
349
- core.replicator.ondownloading = ondownloading
350
- // trigger once if the condition is already true
351
- if (core.replicator.downloading) ondownloading()
352
- }, () => {
353
- this._noCoreCache.set(id, true)
354
- this.cores.delete(id)
355
- })
356
- core.once('close', () => {
357
- this._emitCore('core-close', core)
358
- this.cores.delete(id)
359
- })
360
- core.on('conflict', (len, fork, proof) => {
361
- this.emit('conflict', core, len, fork, proof)
362
- })
363
-
364
- return { from: core, keyPair, manifest, cache: !!opts.cache }
365
- }
366
-
367
- async createKeyPair (name, namespace = this._namespace) {
368
- if (!this.opened) await this.ready()
369
-
370
- const keyPair = {
371
- publicKey: b4a.allocUnsafe(sodium.crypto_sign_PUBLICKEYBYTES),
372
- secretKey: b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)
206
+ if (!b4a.equals(primaryKey, this.primaryKey)) {
207
+ throw new Error('Another corestore is stored here')
373
208
  }
374
-
375
- const seed = deriveSeed(this.primaryKey, namespace, name)
376
- sodium.crypto_sign_seed_keypair(keyPair.publicKey, keyPair.secretKey, seed)
377
-
378
- return keyPair
379
209
  }
380
210
 
381
- get (opts = {}) {
382
- if (this.closing || this._root.closing) throw new Error('The corestore is closed')
383
- opts = validateGetOptions(opts)
384
-
385
- if (opts.cache !== false) {
386
- opts.cache = opts.cache === true || (this.cache && !opts.cache) ? defaultCache() : opts.cache
387
- }
388
- if (this._readonly && opts.writable !== false) {
389
- opts.writable = false
390
- }
391
-
392
- let rw = null
393
- let id = null
211
+ async _close () {
212
+ const sessions = []
213
+ const hanging = [...this.sessions]
214
+ for (const sess of hanging) sessions.push(sess.close())
394
215
 
395
- const core = new Hypercore(null, {
396
- ...opts,
397
- globalCache: this.globalCache,
398
- name: null,
399
- preload: async () => {
400
- if (opts.preload) opts = { ...opts, ...(await opts.preload()) }
401
- if (!this.opened) await this.ready()
216
+ if (this.watchers !== null) this.cores.unwatch(this)
402
217
 
403
- const keys = await this._generateKeys(opts)
218
+ await Promise.all(sessions)
219
+ if (this.root !== null) return
404
220
 
405
- id = b4a.toString(keys.discoveryKey, 'hex')
406
- rw = (opts.exclusive && opts.writable !== false) ? this._getLock(id) : null
221
+ const cores = []
222
+ for (const core of this.cores) cores.push(core.close())
223
+ await Promise.all(cores)
224
+ await this.storage.close()
225
+ }
407
226
 
408
- if (rw) await rw.write.lock()
409
- return await this._preload(id, keys, opts)
410
- }
411
- })
227
+ async _attachMaybe (muxer, discoveryKey) {
228
+ if (this.opened === false) await this.ready()
229
+ if (this.cores.get(toHex(discoveryKey)) === null && !(await this.storage.has(discoveryKey))) return
412
230
 
413
- this._sessions.add(core)
414
- if (this._findingPeersCount > 0) {
415
- this._findingPeers.push(core.findingPeers())
416
- }
231
+ const core = this._getCore(discoveryKey, { createIfMissing: false })
417
232
 
418
- const gc = () => {
419
- // technically better to also clear _findingPeers if we added it,
420
- // but the lifecycle for those are pretty short so prob not worth the complexity
421
- // as _decFindingPeers clear them all.
422
- this._sessions.delete(core)
233
+ if (!core) return
234
+ if (!core.opened) await core.ready()
423
235
 
424
- if (!rw) return
425
- rw.write.unlock()
426
- if (!rw.write.locked) this._locks.delete(id)
236
+ if (!core.replicator.attached(muxer)) {
237
+ core.replicator.attachTo(muxer)
427
238
  }
428
-
429
- core.ready().catch(gc)
430
- core.once('close', gc)
431
-
432
- return core
433
239
  }
434
240
 
435
241
  replicate (isInitiator, opts) {
436
- const isExternal = isStream(isInitiator) || !!(opts && opts.stream)
242
+ const isExternal = isStream(isInitiator)
437
243
  const stream = Hypercore.createProtocolStream(isInitiator, {
438
244
  ...opts,
439
- ondiscoverykey: async discoveryKey => {
245
+ ondiscoverykey: discoveryKey => {
440
246
  if (this.closing) return
441
-
442
- const id = b4a.toString(discoveryKey, 'hex')
443
- if (this._noCoreCache.get(id)) return
444
-
445
- const core = this.get({ _discoveryKey: discoveryKey, active: false })
446
-
447
- try {
448
- await core.ready()
449
- } catch {
450
- return
451
- }
452
-
453
- // remote is asking for the core so we HAVE to answer even if not downloading
454
- if (!core.closing) core.replicate(stream, { session: true })
455
- await core.close()
247
+ const muxer = stream.noiseStream.userData
248
+ return this._attachMaybe(muxer, discoveryKey)
456
249
  }
457
250
  })
458
251
 
459
- if (!this.passive) {
252
+ if (this.cores.size > 0) {
460
253
  const muxer = stream.noiseStream.userData
254
+ const uncork = muxer.uncork.bind(muxer)
461
255
  muxer.cork()
462
- for (const core of this.cores.values()) {
463
- // If the core is not opened, it will be replicated in preload.
464
- if (!core.opened || core.closing || !core.replicator.downloading) continue
465
- core.replicate(stream, { session: true })
466
- }
467
- stream.noiseStream.opened.then(() => muxer.uncork())
468
- }
469
256
 
470
- const streamRecord = { stream, isExternal }
471
- this._replicationStreams.push(streamRecord)
257
+ for (const core of this.cores) {
258
+ if (!core.replicator.downloading || core.replicator.attached(muxer) || !core.opened) continue
259
+ core.replicator.attachTo(muxer)
260
+ }
472
261
 
473
- stream.once('close', () => {
474
- this._replicationStreams.splice(this._replicationStreams.indexOf(streamRecord), 1)
475
- })
262
+ stream.noiseStream.opened.then(uncork)
263
+ }
476
264
 
265
+ const record = this.streamTracker.add(stream, isExternal)
266
+ stream.once('close', () => this.streamTracker.remove(record))
477
267
  return stream
478
268
  }
479
269
 
480
- namespace (name, opts) {
481
- if (name instanceof Hypercore) {
482
- return this.session({ ...opts, _bootstrap: name })
270
+ get (opts) {
271
+ if (b4a.isBuffer(opts) || typeof opts === 'string') opts = { key: opts }
272
+ if (!opts) opts = {}
273
+
274
+ const conf = {
275
+ preload: null,
276
+ parent: null,
277
+ sessions: null,
278
+ ongc: null,
279
+ core: null,
280
+ active: opts.active !== false,
281
+ encryptionKey: opts.encryptionKey || null,
282
+ isBlockKey: false,
283
+ valueEncoding: opts.valueEncoding || null,
284
+ exclusive: !!opts.exclusive,
285
+ manifest: opts.manifest || null,
286
+ keyPair: opts.keyPair || null,
287
+ onwait: opts.onwait || null,
288
+ wait: opts.wait !== false,
289
+ timeout: opts.timeout || 0,
290
+ draft: !!opts.draft,
291
+ writable: opts.writable
483
292
  }
484
- return this.session({ ...opts, namespace: generateNamespace(this._namespace, name) })
293
+
294
+ conf.preload = this._preload(opts)
295
+
296
+ return new Hypercore(null, null, conf)
485
297
  }
486
298
 
487
- session (opts) {
488
- const session = new Corestore(this.storage, {
489
- namespace: this._namespace,
490
- cache: this.cache,
491
- writable: !this._readonly,
492
- _attached: opts && opts.detach === false ? this : null,
493
- _root: this._root,
494
- inflightRange: this.inflightRange,
495
- globalCache: this.globalCache,
496
- ...opts
497
- })
498
- if (this === this._root) this._rootStoreSessions.add(session)
499
- return session
299
+ async createKeyPair (name, ns = this.ns) {
300
+ if (this.opened === false) await this.ready()
301
+ return createKeyPair(this.primaryKey, ns, name)
500
302
  }
501
303
 
502
- _closeNamespace () {
503
- const closePromises = []
504
- for (const session of this._sessions) {
505
- closePromises.push(session.close())
304
+ async _preload (opts) {
305
+ if (opts.preload) opts = { ...opts, ...(await opts.preload) }
306
+ if (this.opened === false) await this.ready()
307
+
308
+ const discoveryKey = opts.name ? await this.storage.getAlias({ name: opts.name, namespace: this.ns }) : null
309
+ const core = this._getCore(discoveryKey, opts)
310
+
311
+ return {
312
+ parent: opts.parent || null,
313
+ sessions: this.sessions.get(core.id),
314
+ ongc: this._ongcBound,
315
+ core,
316
+ encryptionKey: opts.encryptionKey || null,
317
+ isBlockKey: !!opts.isBlockKey
506
318
  }
507
- return Promise.allSettled(closePromises)
508
319
  }
509
320
 
510
- async _closePrimaryNamespace () {
511
- const closePromises = []
512
- // At this point, the primary namespace is closing.
513
- for (const { stream, isExternal } of this._replicationStreams) {
514
- // Only close streams that were created by the Corestore
515
- if (!isExternal) stream.destroy()
321
+ _auth (discoveryKey, opts) {
322
+ const result = {
323
+ keyPair: null,
324
+ key: null,
325
+ discoveryKey,
326
+ manifest: null
516
327
  }
517
- for (const core of this.cores.values()) {
518
- closePromises.push(forceClose(core))
328
+
329
+ if (opts.name) {
330
+ result.keyPair = createKeyPair(this.primaryKey, this.ns, opts.name)
331
+ } else if (opts.keyPair) {
332
+ result.keyPair = opts.keyPair
519
333
  }
520
- await Promise.allSettled(closePromises)
521
- await new Promise((resolve, reject) => {
522
- this._keyStorage.close(err => {
523
- if (err) return reject(err)
524
- return resolve(null)
525
- })
526
- })
334
+
335
+ if (opts.manifest) {
336
+ result.manifest = opts.manifest
337
+ } else if (result.keyPair && !result.discoveryKey) {
338
+ result.manifest = { version: 1, signers: [{ publicKey: result.keyPair.publicKey }] }
339
+ }
340
+
341
+ if (opts.key) result.key = ID.decode(opts.key)
342
+ else if (result.manifest) result.key = Hypercore.key(result.manifest)
343
+
344
+ if (result.discoveryKey) return result
345
+
346
+ if (opts.discoveryKey) result.discoveryKey = ID.decode(opts.discoveryKey)
347
+ else if (result.key) result.discoveryKey = crypto.discoveryKey(result.key)
348
+ else throw new Error('Could not derive discovery from input')
349
+
350
+ return result
527
351
  }
528
352
 
529
- async _close () {
530
- this._root._rootStoreSessions.delete(this)
353
+ _hasCore (discoveryKey) {
354
+ return this.cores.get(toHex(discoveryKey)) !== null
355
+ }
531
356
 
532
- await this._closeNamespace()
357
+ _getCore (discoveryKey, opts) {
358
+ const auth = this._auth(discoveryKey, opts)
359
+
360
+ const id = toHex(auth.discoveryKey)
361
+ const existing = this.cores.get(id)
362
+ if (existing) return existing
363
+
364
+ const core = Hypercore.createCore(this.storage, {
365
+ eagerUpgrade: true,
366
+ notDownloadingLinger: opts.notDownloadingLinger,
367
+ allowFork: opts.allowFork !== false,
368
+ inflightRange: opts.inflightRange,
369
+ compat: false, // no compat for now :)
370
+ force: opts.force,
371
+ createIfMissing: opts.createIfMissing,
372
+ discoveryKey: auth.discoveryKey,
373
+ overwrite: opts.overwrite,
374
+ key: auth.key,
375
+ keyPair: auth.keyPair,
376
+ legacy: opts.legacy,
377
+ manifest: auth.manifest,
378
+ globalCache: opts.globalCache || this.globalCache || null,
379
+ alias: opts.name ? { name: opts.name, namespace: this.ns } : null
380
+ })
533
381
 
534
- if (this._root === this) {
535
- await this._closePrimaryNamespace()
536
- } else if (this._attached) {
537
- await this._attached.close()
382
+ core.onidle = () => {
383
+ core.destroy()
384
+ this.cores.delete(id, core)
538
385
  }
539
- }
540
- }
541
386
 
542
- function validateGetOptions (opts) {
543
- const key = (b4a.isBuffer(opts) || typeof opts === 'string') ? hypercoreId.decode(opts) : null
544
- if (key) return { key }
387
+ core.replicator.ondownloading = () => {
388
+ this.streamTracker.attachAll(core)
389
+ }
545
390
 
546
- if (opts.key) {
547
- opts.key = hypercoreId.decode(opts.key)
548
- }
549
- if (opts.keyPair) {
550
- opts.publicKey = opts.keyPair.publicKey
551
- opts.secretKey = opts.keyPair.secretKey
391
+ this.cores.set(id, core)
392
+ return core
552
393
  }
394
+ }
553
395
 
554
- if (opts.name && typeof opts.name !== 'string') throw new Error('name option must be a String')
555
- if (opts.name && opts.secretKey) throw new Error('Cannot provide both a name and a secret key')
556
- if (opts.publicKey && !b4a.isBuffer(opts.publicKey)) throw new Error('publicKey option must be a Buffer or Uint8Array')
557
- if (opts.secretKey && !b4a.isBuffer(opts.secretKey)) throw new Error('secretKey option must be a Buffer or Uint8Array')
558
- if (!opts._discoveryKey && (!opts.name && !opts.publicKey && !opts.manifest && !opts.key && !opts.preload)) throw new Error('Must provide either a name or a publicKey')
559
- return opts
396
+ module.exports = Corestore
397
+
398
+ function isStream (s) {
399
+ return typeof s === 'object' && s && typeof s.pipe === 'function'
560
400
  }
561
401
 
562
402
  function generateNamespace (namespace, name) {
563
403
  if (!b4a.isBuffer(name)) name = b4a.from(name)
564
- const out = b4a.allocUnsafe(32)
404
+ const out = b4a.allocUnsafeSlow(32)
565
405
  sodium.crypto_generichash_batch(out, [namespace, name])
566
406
  return out
567
407
  }
@@ -573,19 +413,19 @@ function deriveSeed (primaryKey, namespace, name) {
573
413
  return out
574
414
  }
575
415
 
576
- function defaultCache () {
577
- return new Xache({ maxSize: 65536, maxAge: 0 })
578
- }
579
-
580
- function isStream (s) {
581
- return typeof s === 'object' && s && typeof s.pipe === 'function'
416
+ function createKeyPair (primaryKey, namespace, name) {
417
+ const seed = deriveSeed(primaryKey, namespace, name)
418
+ const buf = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES + sodium.crypto_sign_SECRETKEYBYTES)
419
+ const keyPair = {
420
+ publicKey: buf.subarray(0, sodium.crypto_sign_PUBLICKEYBYTES),
421
+ secretKey: buf.subarray(sodium.crypto_sign_PUBLICKEYBYTES)
422
+ }
423
+ sodium.crypto_sign_seed_keypair(keyPair.publicKey, keyPair.secretKey, seed)
424
+ return keyPair
582
425
  }
583
426
 
584
- async function forceClose (core) {
585
- await core.ready()
586
- return Promise.all(core.sessions.map(s => s.close()))
587
- }
427
+ function noop () {}
588
428
 
589
- function getStorageRoot (id) {
590
- return CORES_DIR + '/' + id.slice(0, 2) + '/' + id.slice(2, 4) + '/' + id
429
+ function toHex (discoveryKey) {
430
+ return b4a.toString(discoveryKey, 'hex')
591
431
  }
package/package.json CHANGED
@@ -1,44 +1,43 @@
1
1
  {
2
2
  "name": "corestore",
3
- "version": "6.18.3",
3
+ "version": "7.0.0",
4
4
  "description": "A Hypercore factory that simplifies managing collections of cores.",
5
5
  "main": "index.js",
6
+ "files": [
7
+ "index.js"
8
+ ],
9
+ "dependencies": {
10
+ "b4a": "^1.6.7",
11
+ "bare-events": "^2.5.0",
12
+ "hypercore": "^11.0.0",
13
+ "hypercore-crypto": "^3.4.2",
14
+ "hypercore-id-encoding": "^1.3.0",
15
+ "ready-resource": "^1.1.1",
16
+ "sodium-universal": "^4.0.1"
17
+ },
18
+ "devDependencies": {
19
+ "brittle": "^3.7.0",
20
+ "rache": "^1.0.0",
21
+ "standard": "^17.1.2",
22
+ "test-tmp": "^1.3.0"
23
+ },
6
24
  "scripts": {
7
25
  "test": "standard && brittle test/*.js"
8
26
  },
27
+ "imports": {
28
+ "events": {
29
+ "bare": "bare-events",
30
+ "default": "events"
31
+ }
32
+ },
9
33
  "repository": {
10
34
  "type": "git",
11
- "url": "git+https://github.com/holepunchto/corestore.git"
35
+ "url": "https://github.com/holepunchto/corestore2.git"
12
36
  },
13
- "keywords": [
14
- "corestore"
15
- ],
16
- "author": "Andrew Osheroff <andrewosh@gmail.com>",
37
+ "author": "Holepunch Inc",
17
38
  "license": "MIT",
18
39
  "bugs": {
19
- "url": "https://github.com/holepunchto/corestore/issues"
20
- },
21
- "homepage": "https://github.com/holepunchto/corestore#readme",
22
- "files": [
23
- "index.js",
24
- "lib/**.js"
25
- ],
26
- "devDependencies": {
27
- "brittle": "^3.2.2",
28
- "random-access-memory": "^6.2.0",
29
- "standard": "^17.1.0",
30
- "test-tmp": "^1.0.2",
31
- "rache": "^1.0.0"
40
+ "url": "https://github.com/holepunchto/corestore2/issues"
32
41
  },
33
- "dependencies": {
34
- "b4a": "^1.6.4",
35
- "hypercore": "^10.37.10",
36
- "hypercore-crypto": "^3.4.0",
37
- "hypercore-id-encoding": "^1.2.0",
38
- "read-write-mutexify": "^2.1.0",
39
- "ready-resource": "^1.0.0",
40
- "safety-catch": "^1.0.1",
41
- "sodium-universal": "^4.0.0",
42
- "xache": "^1.1.0"
43
- }
42
+ "homepage": "https://github.com/holepunchto/corestore2"
44
43
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) 2023 Holepunch Inc
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.