@cero-base/core 1.2.0 → 1.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cero-base/core",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "cero p2p primitives — identity, storage, network, database, blobs, rpc, pairing.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -41,9 +41,13 @@
41
41
  "types": "./types/network/index.d.ts",
42
42
  "default": "./src/network/index.js"
43
43
  },
44
- "./network/bluetooth": {
45
- "types": "./types/network/bluetooth.d.ts",
46
- "default": "./src/network/bluetooth.js"
44
+ "./network/transports/ble": {
45
+ "types": "./types/network/transports/ble.d.ts",
46
+ "default": "./src/network/transports/ble.js"
47
+ },
48
+ "./network/transports/dht": {
49
+ "types": "./types/network/transports/dht.d.ts",
50
+ "default": "./src/network/transports/dht.js"
47
51
  },
48
52
  "./database": {
49
53
  "types": "./types/database/index.d.ts",
@@ -1,15 +1,16 @@
1
- import Hyperswarm from 'hyperswarm'
2
1
  import NoiseSecretStream from '@hyperswarm/secret-stream'
3
2
  import BlindPairing from 'blind-pairing'
4
3
  import ProtomuxWakeup from 'protomux-wakeup'
5
4
  import ReadyResource from 'ready-resource'
6
5
  import safetyCatch from 'safety-catch'
7
6
  import b4a from 'b4a'
8
- import { hash } from 'hypercore-crypto'
9
7
 
10
8
  import { ACTIVE, PASSIVE } from '../lib/constants.js'
11
9
  import { CeroError } from '../lib/errors.js'
12
10
  import { Discovery } from './discovery.js'
11
+ import { DHTTransport, channelTopic } from './transports/dht.js'
12
+
13
+ export { channelTopic }
13
14
 
14
15
  /**
15
16
  * @typedef {object} NetworkOpts
@@ -36,7 +37,7 @@ export class Network extends ReadyResource {
36
37
  this.relayThrough = relayThrough || null
37
38
  this.channel = channel || null
38
39
 
39
- this.swarm = null
40
+ this._dht = null
40
41
  this.wakeup = new ProtomuxWakeup()
41
42
 
42
43
  this._replicateables = new Set()
@@ -45,6 +46,11 @@ export class Network extends ReadyResource {
45
46
  this._blind = null
46
47
  }
47
48
 
49
+ /** @returns {any} The underlying hyperswarm, or null before ready / after close. */
50
+ get swarm() {
51
+ return this._dht ? this._dht.swarm : null
52
+ }
53
+
48
54
  /**
49
55
  * Feed an externally-established connection — a Bluetooth L2CAP channel, a
50
56
  * serial link, an in-process pair, any duplex — into the network. A raw
@@ -60,8 +66,16 @@ export class Network extends ReadyResource {
60
66
  inject(stream, { isInitiator } = {}) {
61
67
  if (this.closing || this.closed) throw CeroError.CLOSED('Network')
62
68
  if (!stream) throw CeroError.REQUIRED('stream')
69
+ // Injected links authenticate with the same long-lived identity as the
70
+ // swarm — an ephemeral key per link would defeat duplicate-peer detection
71
+ // (two radios between one pair would look like two different peers).
72
+ const keyPair = this.identity
73
+ ? { publicKey: this.identity.publicKey, secretKey: this.identity.secretKey }
74
+ : (this.swarm?.keyPair ?? undefined)
63
75
  const conn =
64
- stream.noiseStream === stream ? stream : new NoiseSecretStream(isInitiator === true, stream)
76
+ stream.noiseStream === stream
77
+ ? stream
78
+ : new NoiseSecretStream(isInitiator === true, stream, keyPair ? { keyPair } : undefined)
65
79
 
66
80
  // blind-pairing picks the lowest-`rtt` unvisited channel to send on; that
67
81
  // field only exists on real udx sockets. A raw injected duplex has none,
@@ -135,24 +149,18 @@ export class Network extends ReadyResource {
135
149
  }
136
150
 
137
151
  async _open() {
138
- const opts = {}
139
- if (this.identity)
140
- opts.keyPair = { publicKey: this.identity.publicKey, secretKey: this.identity.secretKey }
141
- if (this.bootstrap) opts.bootstrap = this.bootstrap
142
- if (this.firewall) opts.firewall = this.firewall
143
- if (this.relayThrough) opts.relayThrough = this.relayThrough
144
-
145
- this.swarm = new Hyperswarm(opts)
146
-
147
- if (this.channel) {
148
- const channel = this.channel
149
- const join = this.swarm.join.bind(this.swarm)
150
- const leave = this.swarm.leave.bind(this.swarm)
151
- this.swarm.join = (topic, opts) => join(channelTopic(topic, channel), opts)
152
- this.swarm.leave = (topic) => leave(channelTopic(topic, channel))
153
- }
152
+ this._dht = new DHTTransport({
153
+ identity: this.identity,
154
+ bootstrap: this.bootstrap,
155
+ firewall: this.firewall,
156
+ relayThrough: this.relayThrough,
157
+ channel: this.channel
158
+ })
154
159
 
155
- this.swarm.on('connection', (stream, info) => {
160
+ // DHTTransport owns the swarm's lifecycle; Network subscribes to its events
161
+ // because the handlers touch Network state (wakeup, replicateables, emit).
162
+ const swarm = this._dht.swarm
163
+ swarm.on('connection', (stream, info) => {
156
164
  if (this.closing || this.closed) {
157
165
  stream.destroy()
158
166
  return
@@ -161,8 +169,8 @@ export class Network extends ReadyResource {
161
169
  for (const r of this._replicateables) replicateInto(r, stream)
162
170
  this.emit('connection', stream, info)
163
171
  })
164
- this.swarm.on('peer-add', (peer) => this.emit('peer-add', peer))
165
- this.swarm.on('peer-remove', (peer) => this.emit('peer-remove', peer))
172
+ swarm.on('peer-add', (peer) => this.emit('peer-add', peer))
173
+ swarm.on('peer-remove', (peer) => this.emit('peer-remove', peer))
166
174
  }
167
175
 
168
176
  /**
@@ -171,9 +179,8 @@ export class Network extends ReadyResource {
171
179
  * @param {{ timeout?: number }} [opts]
172
180
  * @returns {Promise<void>}
173
181
  */
174
- async flush({ timeout = 500 } = {}) {
175
- if (!this.swarm) return
176
- await Promise.race([this.swarm.flush(), new Promise((r) => setTimeout(r, timeout))])
182
+ async flush(opts) {
183
+ await this._dht?.flush(opts)
177
184
  }
178
185
 
179
186
  /**
@@ -182,13 +189,8 @@ export class Network extends ReadyResource {
182
189
  * @returns {Promise<void>}
183
190
  */
184
191
  async suspend() {
185
- if (!this.swarm || this.closing || this.closed) return
186
- if (this.swarm.suspended) return
187
- try {
188
- await this.swarm.suspend()
189
- } catch (err) {
190
- safetyCatch(err)
191
- }
192
+ if (this.closing || this.closed) return
193
+ await this._dht?.suspend()
192
194
  }
193
195
 
194
196
  /**
@@ -197,13 +199,8 @@ export class Network extends ReadyResource {
197
199
  * @returns {Promise<void>}
198
200
  */
199
201
  async resume() {
200
- if (!this.swarm || this.closing || this.closed) return
201
- if (!this.swarm.suspended) return
202
- try {
203
- await this.swarm.resume()
204
- } catch (err) {
205
- safetyCatch(err)
206
- }
202
+ if (this.closing || this.closed) return
203
+ await this._dht?.resume()
207
204
  }
208
205
 
209
206
  async _close() {
@@ -233,18 +230,13 @@ export class Network extends ReadyResource {
233
230
  this._blind = null
234
231
  }
235
232
 
236
- if (this.swarm) {
233
+ if (this._dht) {
237
234
  try {
238
- await this.flush()
235
+ await this._dht.destroy()
239
236
  } catch (err) {
240
237
  safetyCatch(err)
241
238
  }
242
- try {
243
- await this.swarm.destroy()
244
- } catch (err) {
245
- safetyCatch(err)
246
- }
247
- this.swarm = null
239
+ this._dht = null
248
240
  }
249
241
 
250
242
  if (this.wakeup) {
@@ -342,12 +334,6 @@ function replicateInto(core, stream) {
342
334
  }
343
335
  }
344
336
 
345
- // A channel re-namespaces every swarm topic so only same-channel peers meet.
346
- // No channel → identity (unchanged, back-compat).
347
- export function channelTopic(topic, channel) {
348
- return channel ? hash([topic, b4a.from(channel)]) : topic
349
- }
350
-
351
337
  function isTopic(x) {
352
338
  return b4a.isBuffer(x) && x.length === 32
353
339
  }