@cero-base/core 1.14.0 → 1.15.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.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "cero p2p primitives — identity, storage, network, database, blobs, rpc, pairing.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -136,17 +136,17 @@
136
136
  },
137
137
  "dependencies": {
138
138
  "@hyperswarm/secret-stream": "^6.9.1",
139
- "autobee": "2.0.0-rc.21",
139
+ "autobee": "2.0.0-rc.26",
140
140
  "autobee-encryption": "0.1.3",
141
141
  "b4a": "^1.8.1",
142
142
  "bare-crypto": "^1.15.3",
143
- "bare-fs": "^4.8.0",
143
+ "bare-fs": "^4.8.1",
144
144
  "bare-path": "^3.1.1",
145
145
  "bip39-mnemonic": "^2.5.0",
146
146
  "blind-pairing": "^2.3.1",
147
147
  "blind-peering": "^2.6.3",
148
148
  "compact-encoding": "^3.3.2",
149
- "corestore": "^7.12.0",
149
+ "corestore": "^7.12.2",
150
150
  "framed-stream": "^1.0.1",
151
151
  "hrpc": "^4.3.1",
152
152
  "hyperblobs": "^2.12.1",
@@ -164,17 +164,17 @@
164
164
  "ready-resource": "^1.2.0",
165
165
  "safety-catch": "^1.0.3",
166
166
  "sodium-universal": "^5.0.1",
167
- "streamx": "^2.28.0",
167
+ "streamx": "^2.28.1",
168
168
  "z32": "^1.1.0"
169
169
  },
170
170
  "devDependencies": {
171
171
  "@hyperswarm/testnet": "^3.1.4",
172
172
  "bare-abort-controller": "^1.1.2",
173
- "bare-events": "^2.9.1",
173
+ "bare-events": "^2.9.2",
174
174
  "bare-fetch": "^3.2.0",
175
175
  "bare-process": "^4.5.1",
176
176
  "bare-url": "^2.5.2",
177
- "blind-peer": "^3.13.3",
177
+ "blind-peer": "^3.14.0",
178
178
  "brittle": "^4.1.0",
179
179
  "typescript": "^5.9.3",
180
180
  "which-runtime": "^1.4.0"
@@ -129,7 +129,9 @@ export class Database extends ReadyResource {
129
129
  this._healTimer = null
130
130
  }
131
131
  if (this._discovery) {
132
- await this._discovery.destroy()
132
+ // unannounce in the background — awaiting the DHT round-trip here
133
+ // blocks every close (and the writer-swap reboot) for seconds
134
+ this._discovery.destroy().catch(safetyCatch)
133
135
  this._discovery = null
134
136
  }
135
137
  if (this.bee) {
@@ -184,6 +186,26 @@ export class Database extends ReadyResource {
184
186
  optimistic: true,
185
187
  wakeup: wakeup || undefined,
186
188
  open: (b) => HyperDB.bee2(b, this.spec.database, { autoUpdate: true }),
189
+ // without this hook autobee trusts EVERY fast-forward candidate, so a
190
+ // removed device could serve a stale or forked head and we would boot
191
+ // onto it. A head is only worth following if its writer is still a
192
+ // device — decided against the reference view autobee hands us (its own
193
+ // state at the point of the decision), never the live view. Untrusted
194
+ // heads are then skipped outright: mostRecentTrusted stays unset because
195
+ // no cero row records another writer's oplog length to fall back to.
196
+ isTrusted: async (writer, view) => {
197
+ try {
198
+ if (await view.get(`@${this.ns}/devices`, { id: toId(writer) })) return true
199
+ // before any device record exists (a boot's genesis-era reference)
200
+ // only the genesis writer may vouch — its core IS the db key, and
201
+ // its signatures are unforgeable. Anyone else stays refused: an
202
+ // empty view is exactly the window an ex-member would target.
203
+ const any = await view.find(`@${this.ns}/devices`, { limit: 1 }).toArray()
204
+ return any.length === 0 && !!this.key && b4a.equals(writer, this.key)
205
+ } catch {
206
+ return false
207
+ }
208
+ },
187
209
  apply: async (nodes, view, host) => {
188
210
  // envelope handling lives here, once: unwrap every node and skip ops
189
211
  // from a newer app version (deterministic — they stay in the log), so
@@ -216,6 +238,12 @@ export class Database extends ReadyResource {
216
238
  }
217
239
  })
218
240
 
241
+ // swarm BEFORE boot when the key is known: replication is live while the
242
+ // bee opens, so the boot (and a future fast-forward boot) has peers to
243
+ // pull from instead of opening cold and waiting for discovery
244
+ const known = !!this.key
245
+ if (this.network && known) this._joinSwarm(bee, crypto.discoveryKey(this.key))
246
+
219
247
  await bee.ready()
220
248
  // assigned before the first update: apply-time hooks (_onEpoch's save,
221
249
  // _onFuture) dereference this.bee and must never see it null mid-drain
@@ -250,15 +278,19 @@ export class Database extends ReadyResource {
250
278
  // without a listener autobee escalates apply/view errors to a process crash
251
279
  bee.on('error', this._onerror)
252
280
 
253
- if (this.network) {
254
- this.network.attach(bee)
255
- // a writer swap re-opens the bee — tear down the prior discovery session
256
- // first so it isn't orphaned by the reassignment below
257
- if (this._discovery) await this._discovery.destroy()
258
- this._discovery = this.network.join(bee.discoveryKey, {
259
- mode: this.passive ? PASSIVE : ACTIVE
260
- })
261
- }
281
+ // a fresh db has no key until boot mints it — join once it exists
282
+ if (this.network && !known) this._joinSwarm(bee, bee.discoveryKey)
283
+ }
284
+
285
+ // a writer swap re-opens the bee on the SAME topic — join first so the old
286
+ // session's destroy is a refcounted detach, not a DHT unannounce round-trip
287
+ _joinSwarm(bee, discoveryKey) {
288
+ this.network.attach(bee)
289
+ const prev = this._discovery
290
+ this._discovery = this.network.join(discoveryKey, {
291
+ mode: this.passive ? PASSIVE : ACTIVE
292
+ })
293
+ if (prev) prev.destroy().catch(safetyCatch)
262
294
  }
263
295
 
264
296
  /**
@@ -24,6 +24,7 @@ export function channelTopic(topic, channel) {
24
24
  * @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap nodes.
25
25
  * @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
26
26
  * @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
27
+ * @property {number[]} [backoffs] Reconnect backoff tiers in ms; the default escalates to ~10min, far too slow for local nets.
27
28
  * @property {string} [channel] Optional network-isolation label; only same-channel peers meet.
28
29
  * @property {any} [store] Corestore; required for mirrors (blind peers replicate its cores).
29
30
  * @property {Array<string | Uint8Array>} [mirrors] Blind-peer public keys; each attached room/blob core is mirrored through them for offline sync.
@@ -37,12 +38,22 @@ export function channelTopic(topic, channel) {
37
38
  */
38
39
  export class Network extends ReadyResource {
39
40
  /** @param {NetworkOpts} [opts] */
40
- constructor({ identity, bootstrap, firewall, relayThrough, channel, store, mirrors } = {}) {
41
+ constructor({
42
+ identity,
43
+ bootstrap,
44
+ firewall,
45
+ relayThrough,
46
+ backoffs,
47
+ channel,
48
+ store,
49
+ mirrors
50
+ } = {}) {
41
51
  super()
42
52
  this.identity = identity || null
43
53
  this.bootstrap = bootstrap || null
44
54
  this.firewall = firewall || null
45
55
  this.relayThrough = relayThrough || null
56
+ this.backoffs = backoffs || null
46
57
  this.channel = channel || null
47
58
  this.store = store || null
48
59
  this.mirrors = (mirrors || []).map((k) => (typeof k === 'string' ? decodeKey(k) : k))
@@ -168,6 +179,7 @@ export class Network extends ReadyResource {
168
179
  if (this.bootstrap) opts.bootstrap = this.bootstrap
169
180
  if (this.firewall) opts.firewall = this.firewall
170
181
  if (this.relayThrough) opts.relayThrough = this.relayThrough
182
+ if (this.backoffs) opts.backoffs = this.backoffs
171
183
 
172
184
  const swarm = (this._swarm = new Hyperswarm(opts))
173
185
  swarm.on('connection', (stream, info) => {
@@ -240,12 +252,24 @@ export class Network extends ReadyResource {
240
252
  }
241
253
  this._injected.clear()
242
254
 
243
- for (const d of [...this._discoveries]) {
244
- try {
245
- await d.destroy()
246
- } catch (err) {
247
- safetyCatch(err)
248
- }
255
+ // stop every discovery session NOW (timers die synchronously) but never
256
+ // await the DHT unannounce round-trips — the force destroy below skips
257
+ // upstream's own clear(), so un-destroyed sessions would leak live timers
258
+ for (const d of [...this._discoveries]) d.destroy().catch(safetyCatch)
259
+ this._discoveries.clear()
260
+
261
+ // the swarm dies FIRST, with force: every polite per-topic DHT unannounce
262
+ // (blind-pairing's included) becomes an instant no-op instead of a network
263
+ // round-trip — a vanished node ages out of the DHT regardless
264
+ const swarm = this._swarm
265
+ this._swarm = null
266
+ if (swarm) {
267
+ // even force destroy awaits a graceful server.close DHT round-trip
268
+ // upstream — cap the wait and let the teardown finish in the background
269
+ await Promise.race([
270
+ swarm.destroy({ force: true }).catch(safetyCatch),
271
+ new Promise((r) => setTimeout(r, 750))
272
+ ])
249
273
  }
250
274
 
251
275
  if (this._blindPeering) {
@@ -266,16 +290,6 @@ export class Network extends ReadyResource {
266
290
  this._blind = null
267
291
  }
268
292
 
269
- if (this._swarm) {
270
- try {
271
- await this.flush()
272
- await this._swarm.destroy()
273
- } catch (err) {
274
- safetyCatch(err)
275
- }
276
- this._swarm = null
277
- }
278
-
279
293
  if (this.wakeup) {
280
294
  try {
281
295
  await this.wakeup.destroy()
@@ -94,6 +94,7 @@ export class Database extends ReadyResource {
94
94
  _discovery: import("../network/discovery.js").Discovery;
95
95
  /** Open the underlying autobee, wire dispatcher + apply, attach to network. */
96
96
  openBee(): Promise<void>;
97
+ _joinSwarm(bee: any, discoveryKey: any): void;
97
98
  /**
98
99
  * Flip announce mode at runtime — passive stays reachable (server) but
99
100
  * stops actively looking (client). Cheap; use it to demote idle rooms.
@@ -5,6 +5,7 @@ export function channelTopic(topic: any, channel: any): any;
5
5
  * @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap nodes.
6
6
  * @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
7
7
  * @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
8
+ * @property {number[]} [backoffs] Reconnect backoff tiers in ms; the default escalates to ~10min, far too slow for local nets.
8
9
  * @property {string} [channel] Optional network-isolation label; only same-channel peers meet.
9
10
  * @property {any} [store] Corestore; required for mirrors (blind peers replicate its cores).
10
11
  * @property {Array<string | Uint8Array>} [mirrors] Blind-peer public keys; each attached room/blob core is mirrored through them for offline sync.
@@ -17,7 +18,7 @@ export function channelTopic(topic: any, channel: any): any;
17
18
  */
18
19
  export class Network extends ReadyResource {
19
20
  /** @param {NetworkOpts} [opts] */
20
- constructor({ identity, bootstrap, firewall, relayThrough, channel, store, mirrors }?: NetworkOpts);
21
+ constructor({ identity, bootstrap, firewall, relayThrough, backoffs, channel, store, mirrors }?: NetworkOpts);
21
22
  identity: import("../index.js").Identity;
22
23
  bootstrap: {
23
24
  host: string;
@@ -25,6 +26,7 @@ export class Network extends ReadyResource {
25
26
  }[];
26
27
  firewall: (remotePublicKey: Uint8Array, payload: any) => boolean;
27
28
  relayThrough: Uint8Array<ArrayBufferLike>[];
29
+ backoffs: number[];
28
30
  channel: string;
29
31
  store: any;
30
32
  mirrors: any[];
@@ -152,6 +154,10 @@ export type NetworkOpts = {
152
154
  * Relay public keys to tunnel through.
153
155
  */
154
156
  relayThrough?: Uint8Array[];
157
+ /**
158
+ * Reconnect backoff tiers in ms; the default escalates to ~10min, far too slow for local nets.
159
+ */
160
+ backoffs?: number[];
155
161
  /**
156
162
  * Optional network-isolation label; only same-channel peers meet.
157
163
  */