@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.
@@ -0,0 +1,110 @@
1
+ import Hyperswarm from 'hyperswarm'
2
+ import b4a from 'b4a'
3
+ import { hash } from 'hypercore-crypto'
4
+ import safetyCatch from 'safety-catch'
5
+
6
+ /**
7
+ * @typedef {object} DHTTransportOpts
8
+ * @property {import('../../identity/index.js').Identity} [identity] Long-lived keypair used as the swarm identity.
9
+ * @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap nodes.
10
+ * @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
11
+ * @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
12
+ * @property {string} [channel] Optional network-isolation label; only same-channel peers meet.
13
+ */
14
+
15
+ /**
16
+ * The internet transport: a Hyperswarm that finds peers over the DHT. Owns the
17
+ * swarm's whole lifecycle — construction (identity keyPair, bootstrap, firewall,
18
+ * relay), the channel-topic join/leave wrapping, flush, suspend/resume, and
19
+ * teardown. {@link Network} subscribes to `this.swarm`'s connection/peer events
20
+ * and drives topic joins; the swarm-specific wiring lives here.
21
+ */
22
+ export class DHTTransport {
23
+ /** @param {DHTTransportOpts} [opts] */
24
+ constructor({ identity, bootstrap, firewall, relayThrough, channel } = {}) {
25
+ const opts = {}
26
+ if (identity) opts.keyPair = { publicKey: identity.publicKey, secretKey: identity.secretKey }
27
+ if (bootstrap) opts.bootstrap = bootstrap
28
+ if (firewall) opts.firewall = firewall
29
+ if (relayThrough) opts.relayThrough = relayThrough
30
+
31
+ this.swarm = new Hyperswarm(opts)
32
+
33
+ if (channel) {
34
+ const join = this.swarm.join.bind(this.swarm)
35
+ const leave = this.swarm.leave.bind(this.swarm)
36
+ this.swarm.join = (topic, opts) => join(channelTopic(topic, channel), opts)
37
+ this.swarm.leave = (topic) => leave(channelTopic(topic, channel))
38
+ }
39
+ }
40
+
41
+ /** @returns {boolean} */
42
+ get suspended() {
43
+ return this.swarm?.suspended === true
44
+ }
45
+
46
+ /**
47
+ * Wait for pending DHT announces and lookups to settle, bounded by timeout.
48
+ *
49
+ * @param {{ timeout?: number }} [opts]
50
+ * @returns {Promise<void>}
51
+ */
52
+ async flush({ timeout = 500 } = {}) {
53
+ if (!this.swarm) return
54
+ await Promise.race([this.swarm.flush(), new Promise((r) => setTimeout(r, timeout))])
55
+ }
56
+
57
+ /**
58
+ * Pause the swarm — keeps state, drops sockets. Idempotent.
59
+ *
60
+ * @returns {Promise<void>}
61
+ */
62
+ async suspend() {
63
+ if (!this.swarm || this.swarm.suspended) return
64
+ try {
65
+ await this.swarm.suspend()
66
+ } catch (err) {
67
+ safetyCatch(err)
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Resume a suspended swarm. Idempotent.
73
+ *
74
+ * @returns {Promise<void>}
75
+ */
76
+ async resume() {
77
+ if (!this.swarm || !this.swarm.suspended) return
78
+ try {
79
+ await this.swarm.resume()
80
+ } catch (err) {
81
+ safetyCatch(err)
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Flush pending discovery, then tear down the swarm. Idempotent.
87
+ *
88
+ * @returns {Promise<void>}
89
+ */
90
+ async destroy() {
91
+ if (!this.swarm) return
92
+ try {
93
+ await this.flush()
94
+ } catch (err) {
95
+ safetyCatch(err)
96
+ }
97
+ try {
98
+ await this.swarm.destroy()
99
+ } catch (err) {
100
+ safetyCatch(err)
101
+ }
102
+ this.swarm = null
103
+ }
104
+ }
105
+
106
+ // A channel re-namespaces every swarm topic so only same-channel peers meet.
107
+ // No channel → identity (unchanged, back-compat).
108
+ export function channelTopic(topic, channel) {
109
+ return channel ? hash([topic, b4a.from(channel)]) : topic
110
+ }
@@ -1,15 +1,13 @@
1
1
  import { Duplex } from 'streamx'
2
2
 
3
- // iOS caps a single GATT write (and a notify payload) at ATT_MTU − 3 ≈ 182
4
- // bytes. 150 stays safely under that without negotiating an MTU. Raise later
5
- // via the peripheral's maximumWriteValueLength + write-without-response.
3
+ // A single GATT write/notify caps at ATT_MTU − 3 ≈ 182 bytes; 150 stays under
4
+ // that without negotiating an MTU.
6
5
  const PAYLOAD = 150
7
6
 
8
7
  /**
9
8
  * A dumb byte-carrying duplex for the GATT transport. Framing and session logic
10
- * live in BluetoothTransport; this only fragments outbound writes to fit a GATT
11
- * write and pushes inbound payload bytes. NoiseSecretStream wraps it as a raw
12
- * duplex, exactly like the old L2CAP channel.
9
+ * live in BLETransport; this only fragments outbound writes to fit a GATT
10
+ * write and pushes inbound payload bytes.
13
11
  *
14
12
  * @extends Duplex
15
13
  */
package/src/rpc/index.js CHANGED
@@ -12,6 +12,7 @@ const EMPTY = b4a.alloc(0)
12
12
  // Default envelope encodings for the @cero namespace, used when the supplied
13
13
  // spec does not provide its own rows/query/create types in its schema.
14
14
  const DEFAULT_ROWS = getEncoding('@cero/rows')
15
+ const DEFAULT_CHANGES = getEncoding('@cero/changes')
15
16
  const DEFAULT_QUERY = getEncoding('@cero/query')
16
17
  const DEFAULT_CREATE = getEncoding('@cero/create')
17
18
 
@@ -26,6 +27,8 @@ const DEFAULT_CREATE = getEncoding('@cero/create')
26
27
  * @property {(type: string, row: any) => Uint8Array} encodeRow
27
28
  * @property {(type: string, buf: Uint8Array) => any} decodeRow
28
29
  * @property {(type: string, rows: any[]) => Uint8Array} encodeRows
30
+ * @property {(type: string, changes: Array<{ prev: any, next: any }>) => Uint8Array} encodeChanges
31
+ * @property {(type: string, buf: Uint8Array) => Array<{ prev: any, next: any }>} decodeChanges
29
32
  * @property {(type: string, buf: Uint8Array) => any[]} decodeRows
30
33
  * @property {(q: any) => Uint8Array} encodeQuery
31
34
  * @property {(buf: Uint8Array) => any} decodeQuery
@@ -51,6 +54,7 @@ export function bindCodec(spec) {
51
54
 
52
55
  const ns = spec.meta?.ns
53
56
  const ROWS = ns ? `@${ns}/rows` : null
57
+ const CHANGES = ns ? `@${ns}/changes` : null
54
58
  const QUERY = ns ? `@${ns}/query` : null
55
59
  const CREATE = ns ? `@${ns}/create` : null
56
60
 
@@ -73,6 +77,25 @@ export function bindCodec(spec) {
73
77
  const data = env?.data || []
74
78
  return data.map((b) => schema.decode(type, b))
75
79
  },
80
+ encodeChanges(type, changes) {
81
+ const prev = changes.map((x) => (x.prev ? schema.encode(type, x.prev) : EMPTY))
82
+ const next = changes.map((x) => (x.next ? schema.encode(type, x.next) : EMPTY))
83
+ return encodeEnvelope(schema, CHANGES, DEFAULT_CHANGES, { prev, next })
84
+ },
85
+ decodeChanges(type, buf) {
86
+ if (!buf || buf.length === 0) return []
87
+ const env = decodeEnvelope(schema, CHANGES, DEFAULT_CHANGES, buf)
88
+ const prev = env?.prev || []
89
+ const next = env?.next || []
90
+ const out = []
91
+ for (let i = 0; i < Math.max(prev.length, next.length); i++) {
92
+ out.push({
93
+ prev: prev[i]?.length ? schema.decode(type, prev[i]) : null,
94
+ next: next[i]?.length ? schema.decode(type, next[i]) : null
95
+ })
96
+ }
97
+ return out
98
+ },
76
99
  encodeQuery(q) {
77
100
  if (q == null) return encodeEnvelope(schema, QUERY, DEFAULT_QUERY, {})
78
101
  const { gt, gte, lt, lte, limit, reverse, ...rest } = q
@@ -0,0 +1,15 @@
1
+ /**
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`.
8
+ *
9
+ * @param {import('./index.js').Database} db
10
+ * @param {string} name Ref name (scopes the update ticks).
11
+ * @param {string} col Collection path (`@ns/name`).
12
+ * @param {(row: any) => boolean} matches
13
+ * @returns {import('streamx').Readable}
14
+ */
15
+ export function makeChanges(db: import("./index.js").Database, name: string, col: string, matches: (row: any) => boolean): import("streamx").Readable;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Prefix an encoded op with the app's contract version.
3
+ *
4
+ * @param {number} version
5
+ * @param {Uint8Array} body
6
+ * @returns {Uint8Array}
7
+ */
8
+ export function wrap(version: number, body: Uint8Array): Uint8Array;
9
+ /**
10
+ * Split an op into contract version and payload. Ops written before the
11
+ * envelope existed carry no sentinel and read as version 0.
12
+ *
13
+ * @param {Uint8Array} buf
14
+ * @returns {{ version: number, body: Uint8Array }}
15
+ */
16
+ export function unwrap(buf: Uint8Array): {
17
+ version: number;
18
+ body: Uint8Array;
19
+ };
@@ -54,6 +54,8 @@ export class Database extends ReadyResource {
54
54
  kind?: string;
55
55
  verb?: string;
56
56
  }>;
57
+ version: any;
58
+ behind: any;
57
59
  routes: Record<string, Function>;
58
60
  namespace: string;
59
61
  encryptionKey: Uint8Array<ArrayBufferLike>;
@@ -216,6 +218,24 @@ export class Database extends ReadyResource {
216
218
  * @returns {Promise<void>}
217
219
  */
218
220
  write(ops: Array<[string, any]>): Promise<void>;
221
+ /**
222
+ * Record that the log contains ops from a newer app version than this
223
+ * peer understands. Fires `'behind'` once per version so apps can prompt
224
+ * an upgrade; the marker survives restarts via local core userData.
225
+ *
226
+ * @param {number} version
227
+ */
228
+ _onFuture(version: number): void;
229
+ /**
230
+ * Validate ops against the committed view before appending: each op runs in
231
+ * a throwaway transaction with host effects stubbed, so a throwing handler
232
+ * rejects the write and nothing enters the permanent log. Permission gates
233
+ * that `return` are not rejections — apply stays the authority at
234
+ * linearization time.
235
+ *
236
+ * @param {Uint8Array[]} encoded
237
+ */
238
+ _dryRun(encoded: Uint8Array[]): Promise<void>;
219
239
  /**
220
240
  * Read a row. With no `query`: list all (collection) or fetch the one
221
241
  * record (single). With a string id: fetch that specific row.
@@ -247,6 +267,18 @@ export class Database extends ReadyResource {
247
267
  * @returns {import('streamx').Readable}
248
268
  */
249
269
  watch(name: string, query?: Query): import("streamx").Readable;
270
+ /**
271
+ * Delta subscription: batches of `{ prev, next }` row pairs instead of
272
+ * full snapshots. The first batch (and any batch after a view swap) carries
273
+ * the current matching rows as inserts with `reset: true` — replaying every
274
+ * batch into a Map keyed by row id always reconstructs current state.
275
+ * `limit`/`reverse` are not applied; deltas are unwindowed by design.
276
+ *
277
+ * @param {string} name
278
+ * @param {Query} [query]
279
+ * @returns {import('streamx').Readable}
280
+ */
281
+ changes(name: string, query?: Query): import("streamx").Readable;
250
282
  /**
251
283
  * First-run bootstrap: create the device writer, save it, and swap into it.
252
284
  *
@@ -2,12 +2,8 @@ export function resolveStruct(name: any, v?: number): {
2
2
  preencode(state: any, m: any): void;
3
3
  encode(state: any, m: any): void;
4
4
  decode(state: any): {
5
- coreKey: any;
6
- blockOffset: any;
7
- blockLength: any;
8
- byteOffset: any;
9
- byteLength: any;
10
- type: any;
5
+ prev: any;
6
+ next: any;
11
7
  } | {
12
8
  status: any;
13
9
  reason: any;
@@ -21,18 +17,21 @@ export function resolveStruct(name: any, v?: number): {
21
17
  name: any;
22
18
  role: any;
23
19
  noAccept: boolean;
24
- };
25
- };
26
- export function getStruct(name: any, v?: number): {
27
- preencode(state: any, m: any): void;
28
- encode(state: any, m: any): void;
29
- decode(state: any): {
20
+ } | {
30
21
  coreKey: any;
31
22
  blockOffset: any;
32
23
  blockLength: any;
33
24
  byteOffset: any;
34
25
  byteLength: any;
35
26
  type: any;
27
+ };
28
+ };
29
+ export function getStruct(name: any, v?: number): {
30
+ preencode(state: any, m: any): void;
31
+ encode(state: any, m: any): void;
32
+ decode(state: any): {
33
+ prev: any;
34
+ next: any;
36
35
  } | {
37
36
  status: any;
38
37
  reason: any;
@@ -46,6 +45,13 @@ export function getStruct(name: any, v?: number): {
46
45
  name: any;
47
46
  role: any;
48
47
  noAccept: boolean;
48
+ } | {
49
+ coreKey: any;
50
+ blockOffset: any;
51
+ blockLength: any;
52
+ byteOffset: any;
53
+ byteLength: any;
54
+ type: any;
49
55
  };
50
56
  };
51
57
  export function getEnum(name: any): void;
@@ -85,6 +91,13 @@ export function getEncoding(name: any): {
85
91
  byteLength: any;
86
92
  type: any;
87
93
  };
94
+ } | {
95
+ preencode(state: any, m: any): void;
96
+ encode(state: any, m: any): void;
97
+ decode(state: any): {
98
+ prev: any;
99
+ next: any;
100
+ };
88
101
  };
89
102
  export function encode(name: any, value: any, v?: number): any;
90
103
  export function decode(name: any, buffer: any, v?: number): any;
@@ -1,4 +1,4 @@
1
- export function channelTopic(topic: any, channel: any): any;
1
+ export { channelTopic };
2
2
  /**
3
3
  * @typedef {object} NetworkOpts
4
4
  * @property {import('../identity/index.js').Identity} [identity] Long-lived keypair used as the swarm identity.
@@ -6,6 +6,8 @@ export function channelTopic(topic: any, channel: any): any;
6
6
  * @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
7
7
  * @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
8
8
  * @property {string} [channel] Optional network-isolation label; only same-channel peers meet.
9
+ * @property {any} [store] Corestore; required for mirrors (blind peers replicate its cores).
10
+ * @property {Array<string | Uint8Array>} [mirrors] Blind-peer public keys; each attached room/blob core is mirrored through them for offline sync.
9
11
  *
10
12
  * @typedef {{ replicate: (stream: any) => any }} Replicable
11
13
  */
@@ -15,7 +17,7 @@ export function channelTopic(topic: any, channel: any): any;
15
17
  */
16
18
  export class Network extends ReadyResource {
17
19
  /** @param {NetworkOpts} [opts] */
18
- constructor({ identity, bootstrap, firewall, relayThrough, channel }?: NetworkOpts);
20
+ constructor({ identity, bootstrap, firewall, relayThrough, channel, store, mirrors }?: NetworkOpts);
19
21
  identity: import("../index.js").Identity;
20
22
  bootstrap: {
21
23
  host: string;
@@ -24,12 +26,17 @@ export class Network extends ReadyResource {
24
26
  firewall: (remotePublicKey: Uint8Array, payload: any) => boolean;
25
27
  relayThrough: Uint8Array<ArrayBufferLike>[];
26
28
  channel: string;
27
- swarm: any;
29
+ store: any;
30
+ mirrors: any[];
31
+ _dht: DHTTransport;
28
32
  wakeup: any;
29
33
  _replicateables: Set<any>;
30
34
  _discoveries: Set<any>;
31
35
  _injected: Set<any>;
32
36
  _blind: any;
37
+ _blindPeering: any;
38
+ /** @returns {any} The underlying hyperswarm, or null before ready / after close. */
39
+ get swarm(): any;
33
40
  /**
34
41
  * Feed an externally-established connection — a Bluetooth L2CAP channel, a
35
42
  * serial link, an in-process pair, any duplex — into the network. A raw
@@ -75,7 +82,7 @@ export class Network extends ReadyResource {
75
82
  * @param {{ timeout?: number }} [opts]
76
83
  * @returns {Promise<void>}
77
84
  */
78
- flush({ timeout }?: {
85
+ flush(opts?: {
79
86
  timeout?: number;
80
87
  }): Promise<void>;
81
88
  /**
@@ -149,9 +156,19 @@ export type NetworkOpts = {
149
156
  * Optional network-isolation label; only same-channel peers meet.
150
157
  */
151
158
  channel?: string;
159
+ /**
160
+ * Corestore; required for mirrors (blind peers replicate its cores).
161
+ */
162
+ store?: any;
163
+ /**
164
+ * Blind-peer public keys; each attached room/blob core is mirrored through them for offline sync.
165
+ */
166
+ mirrors?: Array<string | Uint8Array>;
152
167
  };
153
168
  export type Replicable = {
154
169
  replicate: (stream: any) => any;
155
170
  };
171
+ import { channelTopic } from './transports/dht.js';
156
172
  import ReadyResource from 'ready-resource';
173
+ import { DHTTransport } from './transports/dht.js';
157
174
  import { Discovery } from './discovery.js';
@@ -12,37 +12,36 @@ export function toServiceUUID(topic: Uint8Array, tag?: string): string;
12
12
  * byte-stream to each discovered peer, and feeds it into `network.inject`. From
13
13
  * there replication and pairing are transport-agnostic (see Network.inject).
14
14
  *
15
- * Choreography: the server adds one data characteristic (write + notify) and
16
- * advertises. The central connects, discovers the characteristic, subscribes,
17
- * then framed bytes flow both ways — central→server as GATT writes, server→
18
- * central as notifications — each tagged with an 8-byte session id (bitchat
19
- * model). `backend` is bare-bluetooth in production and a mock in tests.
20
- *
21
- * ponytail: capability-handshake DoS link-scoring is deferred — it needs a
22
- * replication-progress signal (design §4b). v1 caps links + times out dials.
15
+ * The server adds one data characteristic (write + notify) and advertises. The
16
+ * central connects, discovers the characteristic, subscribes, then framed bytes
17
+ * flow both ways — central→server as GATT writes, server→central as
18
+ * notifications — each tagged with an 8-byte session id. `backend` is
19
+ * bare-bluetooth in production and a mock in tests.
23
20
  *
24
21
  * @extends ReadyResource
25
22
  */
26
- export class BluetoothTransport extends ReadyResource {
23
+ export class BLETransport extends ReadyResource {
27
24
  /**
28
25
  * @param {object} opts
29
26
  * @param {any} opts.backend bare-bluetooth-shaped module (Central, Server, Service, Characteristic).
30
- * @param {import('./index.js').Network} opts.network
27
+ * @param {import('../index.js').Network} opts.network
31
28
  * @param {Uint8Array} opts.uuid The 32-byte topic the service UUID derives from.
32
29
  * @param {Uint8Array} opts.nodeId Stable local id (identity/device key) for the initiate tie-break.
33
30
  * @param {string} [opts.tag] UUID namespace (channel mesh vs invite mesh).
34
- * @param {number} [opts.cap] Max concurrent links; gossip covers the rest.
31
+ * @param {number} [opts.maxOutbound] Max concurrent outbound dials/links; gossip covers the rest.
32
+ * @param {number} [opts.maxInbound] Max concurrent inbound sessions; newcomers past this are refused.
35
33
  * @param {{ scanMode?: any }} [opts.scanOptions] Platform scan options (e.g. Android low-power).
36
34
  * @param {boolean} [opts.keepLinks] On close, stop the radio but leave established links alive (invite rendezvous: the link outlives the QR and carries the initial replication).
37
35
  * @param {string} [opts.name] Local app-user display name, sent to peers over a hello frame.
38
36
  */
39
- constructor({ backend, network, uuid, nodeId, tag, cap, scanOptions, keepLinks, name }: {
37
+ constructor({ backend, network, uuid, nodeId, tag, maxOutbound, maxInbound, scanOptions, keepLinks, name }: {
40
38
  backend: any;
41
- network: import("./index.js").Network;
39
+ network: import("../index.js").Network;
42
40
  uuid: Uint8Array;
43
41
  nodeId: Uint8Array;
44
42
  tag?: string;
45
- cap?: number;
43
+ maxOutbound?: number;
44
+ maxInbound?: number;
46
45
  scanOptions?: {
47
46
  scanMode?: any;
48
47
  };
@@ -50,12 +49,13 @@ export class BluetoothTransport extends ReadyResource {
50
49
  name?: string;
51
50
  });
52
51
  backend: any;
53
- network: import("./index.js").Network;
52
+ network: import("../index.js").Network;
54
53
  name: string;
55
54
  nodeId: Uint8Array<ArrayBufferLike>;
56
55
  nodeHex: any;
57
56
  serviceUUID: string;
58
- cap: number;
57
+ maxOutbound: number;
58
+ maxInbound: number;
59
59
  scanOptions: {
60
60
  scanMode?: any;
61
61
  };
@@ -71,19 +71,8 @@ export class BluetoothTransport extends ReadyResource {
71
71
  _scanning: boolean;
72
72
  _advertising: boolean;
73
73
  _serviceAdded: boolean;
74
- /** peripheral id being dialed its connect-timeout timer */
75
- _dialing: Map<any, any>;
76
- /** peripheral ids that carry a live channel — never re-dialed (a second
77
- * dial's failure would disconnect the peripheral and kill the good link) */
78
- _linked: Set<any>;
79
- /** peripheral id → retry-after timestamp; failed dials back off */
80
- _coolUntil: Map<any, any>;
81
- /** peripheral id → consecutive failure count; drives exponential backoff */
82
- _failures: Map<any, any>;
83
- /** peripheral id → remote peer key, learned at handshake — dial guard */
84
- _peerByPeripheral: Map<any, any>;
85
- /** live central-side peripheral wrappers — for goodbye + physical hang-up on suspend */
86
- _connectedPeripherals: Set<any>;
74
+ /** peripheral id per-peer dial state { timer, linked, coolUntil, failures, peerKey, peripheral } */
75
+ _devices: Map<any, any>;
87
76
  /** last central.connect timestamp — global inter-dial rate limit */
88
77
  _lastDial: number;
89
78
  _scanTimer: any;
@@ -100,21 +89,15 @@ export class BluetoothTransport extends ReadyResource {
100
89
  */
101
90
  shouldInitiate(peerNodeId: Uint8Array): boolean;
102
91
  get linkCount(): number;
92
+ _device(id: any): any;
93
+ _prune(id: any): void;
103
94
  _startServer(Service: any, Characteristic: any): void;
104
95
  _maybeAdvertise(): void;
105
96
  _onWriteRequests(requests: any): void;
106
97
  _onServerFrame(data: any): void;
107
98
  _closeServerSession(sidHex: any, sid: any): void;
108
- /** Our hello payload: the local app-user name the peer labels this link with. */
109
99
  _helloPayload(): any;
110
- /**
111
- * Parse a hello payload. Malformed → null (the caller ignores it).
112
- *
113
- * @param {Uint8Array} payload
114
- * @returns {string | null}
115
- */
116
- _parseHello(payload: Uint8Array): string | null;
117
- /** Stash a peer's name onto a server session + its conn, then refresh mirrors. */
100
+ _parseHello(payload: any): string;
118
101
  _applyPeerName(session: any, payload: any): void;
119
102
  _enqueueNotify(f: any): Promise<any>;
120
103
  _drainNotify(): void;
@@ -130,26 +113,17 @@ export class BluetoothTransport extends ReadyResource {
130
113
  _writeOnce(peripheral: any, char: any, f: any): Promise<any>;
131
114
  _abortDial(peripheral: any, _reason: any): void;
132
115
  _clearDial(id: any): void;
116
+ _isDialing(): boolean;
133
117
  _onCentralError(err: any): void;
134
- _onChannel(l2cap: any, isInitiator: any, peripheralId: any): any;
118
+ _onChannel(stream: any, isInitiator: any, peripheralId: any): any;
135
119
  _track(conn: any, peripheralId: any, isInitiator: any): void;
136
120
  _untrack(conn: any): void;
137
- /**
138
- * Best-effort TYPE_CLOSE to every live session — server sessions over the
139
- * notify path, central sessions over the write path — reusing the same helpers
140
- * a normal stream close uses. Waits up to DRAIN_MS for the frames to flush,
141
- * then resolves regardless: suspend must never hang on a wedged radio.
142
- *
143
- * @returns {Promise<void>}
144
- */
145
121
  _sayGoodbye(): Promise<void>;
146
122
  /**
147
123
  * Pause radio activity but KEEP the Server/Central instances and the
148
124
  * registered GATT service alive — the toggle-friendly counterpart to _close.
149
- * iOS CoreBluetooth managers can't be destroy()ed (native double-free), so a
150
- * fresh transport per toggle leaks a manager whose stale peripheral-manager
151
- * keeps a duplicate GATT service registered; remote centrals then subscribe to
152
- * the dead service and hear silence. Reuse one instance instead. Idempotent.
125
+ * CoreBluetooth managers can't be destroy()ed (native double-free), so one
126
+ * transport is reused across toggles rather than recreated. Idempotent.
153
127
  */
154
128
  suspend(): Promise<void>;
155
129
  /**
@@ -0,0 +1,75 @@
1
+ export function channelTopic(topic: any, channel: any): any;
2
+ /**
3
+ * @typedef {object} DHTTransportOpts
4
+ * @property {import('../../identity/index.js').Identity} [identity] Long-lived keypair used as the swarm identity.
5
+ * @property {Array<{ host: string, port: number }>} [bootstrap] Custom DHT bootstrap nodes.
6
+ * @property {(remotePublicKey: Uint8Array, payload: any) => boolean} [firewall] Incoming-connection filter.
7
+ * @property {Uint8Array[]} [relayThrough] Relay public keys to tunnel through.
8
+ * @property {string} [channel] Optional network-isolation label; only same-channel peers meet.
9
+ */
10
+ /**
11
+ * The internet transport: a Hyperswarm that finds peers over the DHT. Owns the
12
+ * swarm's whole lifecycle — construction (identity keyPair, bootstrap, firewall,
13
+ * relay), the channel-topic join/leave wrapping, flush, suspend/resume, and
14
+ * teardown. {@link Network} subscribes to `this.swarm`'s connection/peer events
15
+ * and drives topic joins; the swarm-specific wiring lives here.
16
+ */
17
+ export class DHTTransport {
18
+ /** @param {DHTTransportOpts} [opts] */
19
+ constructor({ identity, bootstrap, firewall, relayThrough, channel }?: DHTTransportOpts);
20
+ swarm: any;
21
+ /** @returns {boolean} */
22
+ get suspended(): boolean;
23
+ /**
24
+ * Wait for pending DHT announces and lookups to settle, bounded by timeout.
25
+ *
26
+ * @param {{ timeout?: number }} [opts]
27
+ * @returns {Promise<void>}
28
+ */
29
+ flush({ timeout }?: {
30
+ timeout?: number;
31
+ }): Promise<void>;
32
+ /**
33
+ * Pause the swarm — keeps state, drops sockets. Idempotent.
34
+ *
35
+ * @returns {Promise<void>}
36
+ */
37
+ suspend(): Promise<void>;
38
+ /**
39
+ * Resume a suspended swarm. Idempotent.
40
+ *
41
+ * @returns {Promise<void>}
42
+ */
43
+ resume(): Promise<void>;
44
+ /**
45
+ * Flush pending discovery, then tear down the swarm. Idempotent.
46
+ *
47
+ * @returns {Promise<void>}
48
+ */
49
+ destroy(): Promise<void>;
50
+ }
51
+ export type DHTTransportOpts = {
52
+ /**
53
+ * Long-lived keypair used as the swarm identity.
54
+ */
55
+ identity?: import("../../identity/index.js").Identity;
56
+ /**
57
+ * Custom DHT bootstrap nodes.
58
+ */
59
+ bootstrap?: Array<{
60
+ host: string;
61
+ port: number;
62
+ }>;
63
+ /**
64
+ * Incoming-connection filter.
65
+ */
66
+ firewall?: (remotePublicKey: Uint8Array, payload: any) => boolean;
67
+ /**
68
+ * Relay public keys to tunnel through.
69
+ */
70
+ relayThrough?: Uint8Array[];
71
+ /**
72
+ * Optional network-isolation label; only same-channel peers meet.
73
+ */
74
+ channel?: string;
75
+ };
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * A dumb byte-carrying duplex for the GATT transport. Framing and session logic
3
- * live in BluetoothTransport; this only fragments outbound writes to fit a GATT
4
- * write and pushes inbound payload bytes. NoiseSecretStream wraps it as a raw
5
- * duplex, exactly like the old L2CAP channel.
3
+ * live in BLETransport; this only fragments outbound writes to fit a GATT
4
+ * write and pushes inbound payload bytes.
6
5
  *
7
6
  * @extends Duplex
8
7
  */
@@ -9,6 +9,8 @@
9
9
  * @property {(type: string, row: any) => Uint8Array} encodeRow
10
10
  * @property {(type: string, buf: Uint8Array) => any} decodeRow
11
11
  * @property {(type: string, rows: any[]) => Uint8Array} encodeRows
12
+ * @property {(type: string, changes: Array<{ prev: any, next: any }>) => Uint8Array} encodeChanges
13
+ * @property {(type: string, buf: Uint8Array) => Array<{ prev: any, next: any }>} decodeChanges
12
14
  * @property {(type: string, buf: Uint8Array) => any[]} decodeRows
13
15
  * @property {(q: any) => Uint8Array} encodeQuery
14
16
  * @property {(buf: Uint8Array) => any} decodeQuery
@@ -57,6 +59,14 @@ export type Codec = {
57
59
  encodeRow: (type: string, row: any) => Uint8Array;
58
60
  decodeRow: (type: string, buf: Uint8Array) => any;
59
61
  encodeRows: (type: string, rows: any[]) => Uint8Array;
62
+ encodeChanges: (type: string, changes: Array<{
63
+ prev: any;
64
+ next: any;
65
+ }>) => Uint8Array;
66
+ decodeChanges: (type: string, buf: Uint8Array) => Array<{
67
+ prev: any;
68
+ next: any;
69
+ }>;
60
70
  decodeRows: (type: string, buf: Uint8Array) => any[];
61
71
  encodeQuery: (q: any) => Uint8Array;
62
72
  decodeQuery: (buf: Uint8Array) => any;