@cero-base/core 1.12.0 → 1.14.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,18 +1,22 @@
1
1
  import NoiseSecretStream from '@hyperswarm/secret-stream'
2
+ import Autobee from 'autobee'
2
3
  import BlindPairing from 'blind-pairing'
3
4
  import BlindPeering from 'blind-peering'
4
5
  import ProtomuxWakeup from 'protomux-wakeup'
5
6
  import ReadyResource from 'ready-resource'
6
7
  import safetyCatch from 'safety-catch'
7
8
  import { decode as decodeKey } from 'hypercore-id-encoding'
9
+ import { hash } from 'hypercore-crypto'
10
+ import Hyperswarm from 'hyperswarm'
8
11
  import b4a from 'b4a'
9
12
 
10
13
  import { ACTIVE, PASSIVE } from '../lib/constants.js'
11
14
  import { CeroError } from '../lib/errors.js'
12
15
  import { Discovery } from './discovery.js'
13
- import { DHTTransport, channelTopic } from './transports/dht.js'
14
16
 
15
- export { channelTopic }
17
+ export function channelTopic(topic, channel) {
18
+ return channel ? hash([topic, b4a.from(channel)]) : topic
19
+ }
16
20
 
17
21
  /**
18
22
  * @typedef {object} NetworkOpts
@@ -43,7 +47,7 @@ export class Network extends ReadyResource {
43
47
  this.store = store || null
44
48
  this.mirrors = (mirrors || []).map((k) => (typeof k === 'string' ? decodeKey(k) : k))
45
49
 
46
- this._dht = null
50
+ this._swarm = null
47
51
  this.wakeup = new ProtomuxWakeup()
48
52
 
49
53
  this._replicateables = new Set()
@@ -55,7 +59,7 @@ export class Network extends ReadyResource {
55
59
 
56
60
  /** @returns {any} The underlying hyperswarm, or null before ready / after close. */
57
61
  get swarm() {
58
- return this._dht ? this._dht.swarm : null
62
+ return this._swarm
59
63
  }
60
64
 
61
65
  /**
@@ -108,6 +112,7 @@ export class Network extends ReadyResource {
108
112
  * @returns {Promise<any>}
109
113
  */
110
114
  async blind() {
115
+ if (this.closing || this.closed) throw CeroError.CLOSED('Network')
111
116
  if (!this._blind) {
112
117
  const blind = new BlindPairing(this.swarm)
113
118
  this._blind = blind.ready().then(() => {
@@ -156,17 +161,15 @@ export class Network extends ReadyResource {
156
161
  }
157
162
 
158
163
  async _open() {
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
- })
164
+ const opts = {}
165
+ if (this.identity) {
166
+ opts.keyPair = { publicKey: this.identity.publicKey, secretKey: this.identity.secretKey }
167
+ }
168
+ if (this.bootstrap) opts.bootstrap = this.bootstrap
169
+ if (this.firewall) opts.firewall = this.firewall
170
+ if (this.relayThrough) opts.relayThrough = this.relayThrough
166
171
 
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
172
+ const swarm = (this._swarm = new Hyperswarm(opts))
170
173
  swarm.on('connection', (stream, info) => {
171
174
  if (this.closing || this.closed) {
172
175
  stream.destroy()
@@ -176,12 +179,10 @@ export class Network extends ReadyResource {
176
179
  for (const r of this._replicateables) replicateInto(r, stream)
177
180
  this.emit('connection', stream, info)
178
181
  })
179
- swarm.on('peer-add', (peer) => this.emit('peer-add', peer))
180
- swarm.on('peer-remove', (peer) => this.emit('peer-remove', peer))
181
182
 
182
183
  if (this.store && this.mirrors.length) {
183
184
  this._blindPeering = new BlindPeering(swarm.dht, this.store, {
184
- keys: this.mirrors,
185
+ blindPeers: this.mirrors.map((key) => ({ key })),
185
186
  wakeup: this.wakeup,
186
187
  pick: 2
187
188
  })
@@ -194,8 +195,9 @@ export class Network extends ReadyResource {
194
195
  * @param {{ timeout?: number }} [opts]
195
196
  * @returns {Promise<void>}
196
197
  */
197
- async flush(opts) {
198
- await this._dht?.flush(opts)
198
+ async flush({ timeout = 500 } = {}) {
199
+ if (!this._swarm) return
200
+ await Promise.race([this._swarm.flush(), new Promise((r) => setTimeout(r, timeout))])
199
201
  }
200
202
 
201
203
  /**
@@ -206,7 +208,11 @@ export class Network extends ReadyResource {
206
208
  async suspend() {
207
209
  if (this.closing || this.closed) return
208
210
  await this._blindPeering?.suspend()
209
- await this._dht?.suspend()
211
+ try {
212
+ await this._swarm?.suspend()
213
+ } catch (err) {
214
+ safetyCatch(err)
215
+ }
210
216
  }
211
217
 
212
218
  /**
@@ -216,7 +222,11 @@ export class Network extends ReadyResource {
216
222
  */
217
223
  async resume() {
218
224
  if (this.closing || this.closed) return
219
- await this._dht?.resume()
225
+ try {
226
+ await this._swarm?.resume()
227
+ } catch (err) {
228
+ safetyCatch(err)
229
+ }
220
230
  await this._blindPeering?.resume()
221
231
  }
222
232
 
@@ -256,13 +266,14 @@ export class Network extends ReadyResource {
256
266
  this._blind = null
257
267
  }
258
268
 
259
- if (this._dht) {
269
+ if (this._swarm) {
260
270
  try {
261
- await this._dht.destroy()
271
+ await this.flush()
272
+ await this._swarm.destroy()
262
273
  } catch (err) {
263
274
  safetyCatch(err)
264
275
  }
265
- this._dht = null
276
+ this._swarm = null
266
277
  }
267
278
 
268
279
  if (this.wakeup) {
@@ -289,10 +300,10 @@ export class Network extends ReadyResource {
289
300
  if (!this.swarm) throw CeroError.NOT_READY('Network', 'network')
290
301
  if (!isTopic(topic)) throw CeroError.INVALID('topic must be a 32-byte buffer')
291
302
 
292
- const session =
293
- mode === ACTIVE
294
- ? this.swarm.join(topic, { client: true, server: true })
295
- : this.swarm.join(topic, { client: false, server: true })
303
+ const session = this.swarm.join(channelTopic(topic, this.channel), {
304
+ client: mode === ACTIVE,
305
+ server: true
306
+ })
296
307
 
297
308
  const discovery = new Discovery(this, session, mode)
298
309
  this._discoveries.add(discovery)
@@ -313,7 +324,7 @@ export class Network extends ReadyResource {
313
324
  // mirror the core so it stays available when its writers are offline —
314
325
  // autobees announce their whole writer set, plain cores (blobs) just themselves
315
326
  if (this._blindPeering) {
316
- if (core.wakeupCapability) this._blindPeering.addAutobaseBackground(core)
327
+ if (Autobee.isAutobee(core)) this._blindPeering.addAutobaseBackground(core)
317
328
  else this._blindPeering.addCoreBackground(core)
318
329
  }
319
330
  }
@@ -6,6 +6,7 @@ import c from 'compact-encoding'
6
6
  import { discoveryKey, keyPair, sign } from 'hypercore-crypto'
7
7
 
8
8
  import { Invite } from './invite.js'
9
+ import { channelTopic } from '../network/index.js'
9
10
  import { getEncoding } from '../lib/spec/index.js'
10
11
  import { CeroError } from '../lib/errors.js'
11
12
 
@@ -103,7 +104,8 @@ export class Pairing extends ReadyResource {
103
104
  // identity throw 'Active member already exist'
104
105
  if (this.host) {
105
106
  this._member = this._blind.addMember({
106
- discoveryKey: discoveryKey(this.topic),
107
+ // channel-scoped; the candidate hashes identically (no channel → identity)
108
+ discoveryKey: channelTopic(discoveryKey(this.topic), this.network.channel),
107
109
  onadd: (req) => this._onCandidate(req).catch(this._onerror)
108
110
  })
109
111
  }
@@ -486,6 +488,8 @@ class Candidate {
486
488
  this._candidate = this.pairing._blind.addCandidate({
487
489
  invite: this.invite.blind,
488
490
  userData: this.userData,
491
+ // must hash exactly like the member side — see Pairing._open
492
+ discoveryKey: channelTopic(this.invite.discoveryKey, this.pairing.network.channel),
489
493
  onadd: (result) => this._done(result)
490
494
  })
491
495
  this._candidate.request.on('rejected', (err) => this._fail(fromBlindError(err)))
@@ -560,7 +564,9 @@ function decodeUserData(buf, encoding) {
560
564
  }
561
565
 
562
566
  function fromBlindError(err) {
563
- if (!err || !err.code) return CeroError.DENIED(err?.message || null)
567
+ // code-less rejections are protocol failures (undecodable reply, key
568
+ // mismatch, bad signature) — never a host decision, don't report DENIED
569
+ if (!err || !err.code) return CeroError.NETWORK_ERROR(err?.message || 'pairing failed')
564
570
  switch (err.code) {
565
571
  case 'INVITE_EXPIRED':
566
572
  return CeroError.EXPIRED(err.message)
package/src/rpc/peer.js CHANGED
@@ -25,8 +25,11 @@ export class RPCPeer extends ReadyResource {
25
25
  this.ipc = ipc
26
26
  this.spec = spec
27
27
  this.framed = new FramedStream(ipc)
28
- this.framed.pause()
29
28
  this.rpc = new spec.rpc(this.framed)
29
+ // pause AFTER hrpc attaches its 'data' listener — streamx re-resumes a
30
+ // paused stream the moment a listener attaches, so pausing first holds
31
+ // nothing and early frames would dispatch before the codec is bound
32
+ this.framed.pause()
30
33
  }
31
34
 
32
35
  async _open() {
@@ -5,11 +5,19 @@ import Corestore from 'corestore'
5
5
  import HyperDB from 'hyperdb'
6
6
  import ReadyResource from 'ready-resource'
7
7
 
8
- import { ROCKS, BEE, SINGLE, COLLECTION } from '../lib/constants.js'
9
- import { genId, subscribe } from '../lib/utils.js'
8
+ import { ROCKS, BEE, SINGLE, COLLECTION, QUERY_RESERVED } from '../lib/constants.js'
9
+ import { genId, subscribe, searchHit } from '../lib/utils.js'
10
10
  import { CeroError } from '../lib/errors.js'
11
11
 
12
- const RANGE_OPS = new Set(['gt', 'gte', 'lt', 'lte', 'reverse', 'limit'])
12
+ // The subset of the query grammar hyperdb itself honors (pushed down);
13
+ // the rest of QUERY_RESERVED is applied in memory here.
14
+ const RANGE_OPS = ['gt', 'gte', 'lt', 'lte', 'reverse', 'limit']
15
+
16
+ const pick = (query, keys) => {
17
+ const out = {}
18
+ if (query) for (const k of keys) if (query[k] !== undefined) out[k] = query[k]
19
+ return out
20
+ }
13
21
 
14
22
  /**
15
23
  * @typedef {object} StorageOpts
@@ -204,17 +212,18 @@ export class Storage extends ReadyResource {
204
212
  return { data: (await this.db.get(col, { id: query })) ?? null }
205
213
  }
206
214
 
207
- // hyperdb only honors range/limit operators — equality fields must be
208
- // filtered here or they are silently ignored and every row comes back
209
- const eq = Object.keys(query || {}).filter((k) => !RANGE_OPS.has(k))
210
- if (eq.length) {
215
+ // hyperdb only honors range/limit operators — equality fields and search
216
+ // must be applied here or they are silently ignored and every row comes back
217
+ const eq = Object.keys(query || {}).filter((k) => !QUERY_RESERVED.has(k))
218
+ if (eq.length || query?.search) {
211
219
  const rows = await this.db.find(col, {}).toArray()
212
- const matched = rows.filter((r) => eq.every((k) => r[k] === query[k]))
220
+ let matched = rows.filter((r) => eq.every((k) => r[k] === query[k]))
221
+ if (query.search) matched = matched.filter((r) => searchHit(r, query.search, query.fields))
213
222
  const data = query.limit != null ? matched.slice(0, query.limit) : matched
214
223
  return { data, total: matched.length, size: data.length }
215
224
  }
216
225
 
217
- const data = await this.db.find(col, query || {}).toArray()
226
+ const data = await this.db.find(col, pick(query, RANGE_OPS)).toArray()
218
227
  // a limit caps `data`, so `total` needs a full count; with no query
219
228
  // `data` is already everything — skip the second scan.
220
229
  const filtered = query && Object.keys(query).length > 0
@@ -233,9 +242,11 @@ export class Storage extends ReadyResource {
233
242
  this._guard()
234
243
  const ref = this._ref(name)
235
244
  const col = this._col(ref)
236
- const eq = Object.keys(query || {}).filter((k) => !RANGE_OPS.has(k))
237
- const rows = await this.db.find(col, eq.length ? {} : query || {}).toArray()
238
- const matched = eq.length ? rows.filter((r) => eq.every((k) => r[k] === query[k])) : rows
245
+ const eq = Object.keys(query || {}).filter((k) => !QUERY_RESERVED.has(k))
246
+ const filtered = eq.length || query?.search
247
+ const rows = await this.db.find(col, filtered ? {} : pick(query, RANGE_OPS)).toArray()
248
+ let matched = filtered ? rows.filter((r) => eq.every((k) => r[k] === query[k])) : rows
249
+ if (query?.search) matched = matched.filter((r) => searchHit(r, query.search, query.fields))
239
250
  return { data: matched.length }
240
251
  }
241
252
 
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Pull-driven delta stream over a collection: batches of `{ prev, next }`
3
- * pairs diffed between per-stream snapshot cursors. Update ticks only mark
4
- * the stream dirty — the diff runs when the reader demands, so backpressure
5
- * folds bursts into one bigger batch and nothing is ever dropped. The first
6
- * batch, and any batch after the view is swapped or fast-forwarded to a new
7
- * core, carries the full matching state as inserts with `reset: true`.
3
+ * pairs diffed between per-stream head cursors (`db.diff(col, { from })`).
4
+ * Update ticks only mark the stream dirty — the diff runs when the reader
5
+ * demands, so backpressure folds bursts into one bigger batch and nothing is
6
+ * ever dropped. The first batch, and any batch after the view is swapped or
7
+ * fast-forwarded to a new core, carries the full matching state as inserts
8
+ * with `reset: true`.
8
9
  *
9
10
  * @param {import('./index.js').Database} db
10
11
  * @param {string} name Ref name (scopes the update ticks).
@@ -7,6 +7,8 @@
7
7
  * @returns {Uint8Array}
8
8
  */
9
9
  export function blobEpochKey(entropy: Uint8Array): Uint8Array;
10
+ /** Prime a keyring from a local core's persisted epoch stash. */
11
+ export function loadEpochs(keyring: any, local: any): Promise<void>;
10
12
  /**
11
13
  * Wire codec for a rotation announcement's envelope list — one sealed box
12
14
  * per remaining member, addressed by member id.
@@ -46,7 +48,6 @@ export class Keyring {
46
48
  seq: number;
47
49
  /** bumped on every add/remove — cheap change detection for retries */
48
50
  version: number;
49
- primed: boolean;
50
51
  /** @returns {Array<{ epoch: number, stamp: number, entropy: Uint8Array }>} ascending by seq */
51
52
  all(): Array<{
52
53
  epoch: number;
@@ -86,12 +87,12 @@ export class EpochEncryption {
86
87
  export class EpochAutobee {
87
88
  constructor(store: any, key: any, handlers?: {});
88
89
  keyring: any;
90
+ _epochStalled: Set<any>;
89
91
  _epochRetry: any;
90
92
  _epochRetryDelay: number;
91
93
  _epochRetrySeen: number;
92
- _bootState(): Promise<void>;
93
94
  _bumpPendingWriters(): Promise<any>;
94
- _parkOnUnknownEpoch(w: any): void;
95
+ _applyWakeupHints(): Promise<any>;
95
96
  _scheduleEpochRetry(): void;
96
97
  _close(): Promise<any>;
97
98
  }
@@ -90,7 +90,7 @@ export class Database extends ReadyResource {
90
90
  onApplyHooks: Set<any>;
91
91
  _applySeq: number;
92
92
  _touched: Set<any>;
93
- txQueue: any[];
93
+ txQueue: any;
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>;
@@ -258,14 +258,19 @@ export class Database extends ReadyResource {
258
258
  */
259
259
  _healEpochs(): Promise<void>;
260
260
  /**
261
- * Batch every write performed inside `fn` into a single autobee append.
262
- * Nested calls reuse the outer queue.
261
+ * Batch every write made through the transaction handle passed to `fn`
262
+ * into a single autobee append. The handle is this database with its own
263
+ * write queue — same surface (`put`/`set`/`del`/`call`), but writes buffer
264
+ * until `fn` returns, and are discarded if it throws. Writes made on the
265
+ * database itself meanwhile append normally: an unrelated concurrent write
266
+ * can never be swallowed into (and rolled back with) a transaction.
267
+ * Nested `tx()` on the handle joins the same batch.
263
268
  *
264
269
  * @template T
265
- * @param {() => Promise<T> | T} fn
270
+ * @param {(tx: Database) => Promise<T> | T} fn
266
271
  * @returns {Promise<T>}
267
272
  */
268
- tx<T>(fn: () => Promise<T> | T): Promise<T>;
273
+ tx<T>(fn: (tx: Database) => Promise<T> | T): Promise<T>;
269
274
  _txChain: any;
270
275
  /**
271
276
  * Encode and append dispatch ops. Buffers into the active `tx` queue if one
@@ -33,5 +33,6 @@ export namespace RANK {
33
33
  export { reader_1 as reader };
34
34
  }
35
35
  export const NAMESPACE: "cero";
36
+ export const QUERY_RESERVED: Set<string>;
36
37
  export const COUNTERS: "counters";
37
38
  export const EPOCHS: "epochs";
@@ -27,6 +27,7 @@ export function subscribe<T>({ get, watch }: {
27
27
  get: () => Promise<T> | T;
28
28
  watch: (fn: () => void) => (() => void) | void;
29
29
  }): import("streamx").Readable<T>;
30
+ export function searchHit(row: any, term: any, fields: any): boolean;
30
31
  /**
31
32
  * z32-encode a 32-byte key into a canonical string id.
32
33
  *
@@ -1,4 +1,4 @@
1
- export { channelTopic };
1
+ export function channelTopic(topic: any, channel: any): any;
2
2
  /**
3
3
  * @typedef {object} NetworkOpts
4
4
  * @property {import('../identity/index.js').Identity} [identity] Long-lived keypair used as the swarm identity.
@@ -28,7 +28,7 @@ export class Network extends ReadyResource {
28
28
  channel: string;
29
29
  store: any;
30
30
  mirrors: any[];
31
- _dht: DHTTransport;
31
+ _swarm: any;
32
32
  wakeup: any;
33
33
  _replicateables: Set<any>;
34
34
  _discoveries: Set<any>;
@@ -82,7 +82,7 @@ export class Network extends ReadyResource {
82
82
  * @param {{ timeout?: number }} [opts]
83
83
  * @returns {Promise<void>}
84
84
  */
85
- flush(opts?: {
85
+ flush({ timeout }?: {
86
86
  timeout?: number;
87
87
  }): Promise<void>;
88
88
  /**
@@ -168,7 +168,5 @@ export type NetworkOpts = {
168
168
  export type Replicable = {
169
169
  replicate: (stream: any) => any;
170
170
  };
171
- import { channelTopic } from './transports/dht.js';
172
171
  import ReadyResource from 'ready-resource';
173
- import { DHTTransport } from './transports/dht.js';
174
172
  import { Discovery } from './discovery.js';