@cero-base/core 1.3.0 → 1.5.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.
@@ -1,15 +1,18 @@
1
- import Hyperswarm from 'hyperswarm'
2
1
  import NoiseSecretStream from '@hyperswarm/secret-stream'
3
2
  import BlindPairing from 'blind-pairing'
3
+ import BlindPeering from 'blind-peering'
4
4
  import ProtomuxWakeup from 'protomux-wakeup'
5
5
  import ReadyResource from 'ready-resource'
6
6
  import safetyCatch from 'safety-catch'
7
+ import { decode as decodeKey } from 'hypercore-id-encoding'
7
8
  import b4a from 'b4a'
8
- import { hash } from 'hypercore-crypto'
9
9
 
10
10
  import { ACTIVE, PASSIVE } from '../lib/constants.js'
11
11
  import { CeroError } from '../lib/errors.js'
12
12
  import { Discovery } from './discovery.js'
13
+ import { DHTTransport, channelTopic } from './transports/dht.js'
14
+
15
+ export { channelTopic }
13
16
 
14
17
  /**
15
18
  * @typedef {object} NetworkOpts
@@ -18,6 +21,8 @@ import { Discovery } from './discovery.js'
18
21
  * @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
19
22
  * @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
20
23
  * @property {string} [channel] Optional network-isolation label; only same-channel peers meet.
24
+ * @property {any} [store] Corestore; required for mirrors (blind peers replicate its cores).
25
+ * @property {Array<string | Uint8Array>} [mirrors] Blind-peer public keys; each attached room/blob core is mirrored through them for offline sync.
21
26
  *
22
27
  * @typedef {{ replicate: (stream: any) => any }} Replicable
23
28
  */
@@ -28,21 +33,29 @@ import { Discovery } from './discovery.js'
28
33
  */
29
34
  export class Network extends ReadyResource {
30
35
  /** @param {NetworkOpts} [opts] */
31
- constructor({ identity, bootstrap, firewall, relayThrough, channel } = {}) {
36
+ constructor({ identity, bootstrap, firewall, relayThrough, channel, store, mirrors } = {}) {
32
37
  super()
33
38
  this.identity = identity || null
34
39
  this.bootstrap = bootstrap || null
35
40
  this.firewall = firewall || null
36
41
  this.relayThrough = relayThrough || null
37
42
  this.channel = channel || null
43
+ this.store = store || null
44
+ this.mirrors = (mirrors || []).map((k) => (typeof k === 'string' ? decodeKey(k) : k))
38
45
 
39
- this.swarm = null
46
+ this._dht = null
40
47
  this.wakeup = new ProtomuxWakeup()
41
48
 
42
49
  this._replicateables = new Set()
43
50
  this._discoveries = new Set()
44
51
  this._injected = new Set()
45
52
  this._blind = null
53
+ this._blindPeering = null
54
+ }
55
+
56
+ /** @returns {any} The underlying hyperswarm, or null before ready / after close. */
57
+ get swarm() {
58
+ return this._dht ? this._dht.swarm : null
46
59
  }
47
60
 
48
61
  /**
@@ -143,24 +156,18 @@ export class Network extends ReadyResource {
143
156
  }
144
157
 
145
158
  async _open() {
146
- const opts = {}
147
- if (this.identity)
148
- opts.keyPair = { publicKey: this.identity.publicKey, secretKey: this.identity.secretKey }
149
- if (this.bootstrap) opts.bootstrap = this.bootstrap
150
- if (this.firewall) opts.firewall = this.firewall
151
- if (this.relayThrough) opts.relayThrough = this.relayThrough
152
-
153
- this.swarm = new Hyperswarm(opts)
154
-
155
- if (this.channel) {
156
- const channel = this.channel
157
- const join = this.swarm.join.bind(this.swarm)
158
- const leave = this.swarm.leave.bind(this.swarm)
159
- this.swarm.join = (topic, opts) => join(channelTopic(topic, channel), opts)
160
- this.swarm.leave = (topic) => leave(channelTopic(topic, channel))
161
- }
159
+ this._dht = new DHTTransport({
160
+ identity: this.identity,
161
+ bootstrap: this.bootstrap,
162
+ firewall: this.firewall,
163
+ relayThrough: this.relayThrough,
164
+ channel: this.channel
165
+ })
162
166
 
163
- this.swarm.on('connection', (stream, info) => {
167
+ // DHTTransport owns the swarm's lifecycle; Network subscribes to its events
168
+ // because the handlers touch Network state (wakeup, replicateables, emit).
169
+ const swarm = this._dht.swarm
170
+ swarm.on('connection', (stream, info) => {
164
171
  if (this.closing || this.closed) {
165
172
  stream.destroy()
166
173
  return
@@ -169,8 +176,16 @@ export class Network extends ReadyResource {
169
176
  for (const r of this._replicateables) replicateInto(r, stream)
170
177
  this.emit('connection', stream, info)
171
178
  })
172
- this.swarm.on('peer-add', (peer) => this.emit('peer-add', peer))
173
- this.swarm.on('peer-remove', (peer) => this.emit('peer-remove', peer))
179
+ swarm.on('peer-add', (peer) => this.emit('peer-add', peer))
180
+ swarm.on('peer-remove', (peer) => this.emit('peer-remove', peer))
181
+
182
+ if (this.store && this.mirrors.length) {
183
+ this._blindPeering = new BlindPeering(swarm.dht, this.store, {
184
+ keys: this.mirrors,
185
+ wakeup: this.wakeup,
186
+ pick: 2
187
+ })
188
+ }
174
189
  }
175
190
 
176
191
  /**
@@ -179,9 +194,8 @@ export class Network extends ReadyResource {
179
194
  * @param {{ timeout?: number }} [opts]
180
195
  * @returns {Promise<void>}
181
196
  */
182
- async flush({ timeout = 500 } = {}) {
183
- if (!this.swarm) return
184
- await Promise.race([this.swarm.flush(), new Promise((r) => setTimeout(r, timeout))])
197
+ async flush(opts) {
198
+ await this._dht?.flush(opts)
185
199
  }
186
200
 
187
201
  /**
@@ -190,13 +204,9 @@ export class Network extends ReadyResource {
190
204
  * @returns {Promise<void>}
191
205
  */
192
206
  async suspend() {
193
- if (!this.swarm || this.closing || this.closed) return
194
- if (this.swarm.suspended) return
195
- try {
196
- await this.swarm.suspend()
197
- } catch (err) {
198
- safetyCatch(err)
199
- }
207
+ if (this.closing || this.closed) return
208
+ await this._blindPeering?.suspend()
209
+ await this._dht?.suspend()
200
210
  }
201
211
 
202
212
  /**
@@ -205,13 +215,9 @@ export class Network extends ReadyResource {
205
215
  * @returns {Promise<void>}
206
216
  */
207
217
  async resume() {
208
- if (!this.swarm || this.closing || this.closed) return
209
- if (!this.swarm.suspended) return
210
- try {
211
- await this.swarm.resume()
212
- } catch (err) {
213
- safetyCatch(err)
214
- }
218
+ if (this.closing || this.closed) return
219
+ await this._dht?.resume()
220
+ await this._blindPeering?.resume()
215
221
  }
216
222
 
217
223
  async _close() {
@@ -232,27 +238,31 @@ export class Network extends ReadyResource {
232
238
  }
233
239
  }
234
240
 
235
- if (this._blind) {
241
+ if (this._blindPeering) {
236
242
  try {
237
- await (await this._blind).close()
243
+ await this._blindPeering.close()
238
244
  } catch (err) {
239
245
  safetyCatch(err)
240
246
  }
241
- this._blind = null
247
+ this._blindPeering = null
242
248
  }
243
249
 
244
- if (this.swarm) {
250
+ if (this._blind) {
245
251
  try {
246
- await this.flush()
252
+ await (await this._blind).close()
247
253
  } catch (err) {
248
254
  safetyCatch(err)
249
255
  }
256
+ this._blind = null
257
+ }
258
+
259
+ if (this._dht) {
250
260
  try {
251
- await this.swarm.destroy()
261
+ await this._dht.destroy()
252
262
  } catch (err) {
253
263
  safetyCatch(err)
254
264
  }
255
- this.swarm = null
265
+ this._dht = null
256
266
  }
257
267
 
258
268
  if (this.wakeup) {
@@ -300,6 +310,12 @@ export class Network extends ReadyResource {
300
310
  if (!core) throw CeroError.REQUIRED('core')
301
311
  this._replicateables.add(core)
302
312
  for (const stream of this.connections) replicateInto(core, stream)
313
+ // mirror the core so it stays available when its writers are offline —
314
+ // autobees announce their whole writer set, plain cores (blobs) just themselves
315
+ if (this._blindPeering) {
316
+ if (core.wakeupCapability) this._blindPeering.addAutobaseBackground(core)
317
+ else this._blindPeering.addCoreBackground(core)
318
+ }
303
319
  }
304
320
 
305
321
  /**
@@ -350,12 +366,6 @@ function replicateInto(core, stream) {
350
366
  }
351
367
  }
352
368
 
353
- // A channel re-namespaces every swarm topic so only same-channel peers meet.
354
- // No channel → identity (unchanged, back-compat).
355
- export function channelTopic(topic, channel) {
356
- return channel ? hash([topic, b4a.from(channel)]) : topic
357
- }
358
-
359
369
  function isTopic(x) {
360
370
  return b4a.isBuffer(x) && x.length === 32
361
371
  }