@cero-base/cero 1.11.1 → 1.13.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 +10 -18
- package/src/bluetooth.js +130 -147
- package/src/handle/index.js +2 -0
- package/src/index.js +2 -1
- package/types/bluetooth.d.ts +49 -31
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cero-base/cero",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "The ideal p2p API
|
|
3
|
+
"version": "1.13.0",
|
|
4
|
+
"description": "The ideal p2p API — everything is a handle, handles contain refs, refs contain rows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"engines": {
|
|
@@ -95,19 +95,20 @@
|
|
|
95
95
|
"test:node": "ls test/*.test.js | xargs -P1 -n1 brittle-node"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
-
"@cero-base/core": "^1.
|
|
98
|
+
"@cero-base/core": "^1.13.0",
|
|
99
99
|
"b4a": "^1.8.1",
|
|
100
100
|
"bare-abort-controller": "^1.1.2",
|
|
101
101
|
"bare-crypto": "^1.15.3",
|
|
102
102
|
"bare-fs": "^4.8.0",
|
|
103
103
|
"bare-path": "^3.1.1",
|
|
104
|
-
"
|
|
104
|
+
"ble-swarm": "github:holepunchto/ble-swarm#63566f02d4b3a1f3849b9fb009e18666411592a5",
|
|
105
|
+
"compact-encoding": "^3.3.2",
|
|
105
106
|
"corestore": "^7.12.0",
|
|
106
|
-
"hrpc": "^4.3.
|
|
107
|
-
"hypercore": "^11.35.
|
|
107
|
+
"hrpc": "^4.3.1",
|
|
108
|
+
"hypercore": "^11.35.2",
|
|
108
109
|
"hypercore-crypto": "^3.7.0",
|
|
109
|
-
"hypercore-storage": "^3.2.
|
|
110
|
-
"hyperdb": "^6.
|
|
110
|
+
"hypercore-storage": "^3.2.1",
|
|
111
|
+
"hyperdb": "^6.9.0",
|
|
111
112
|
"hyperdispatch": "^1.6.0",
|
|
112
113
|
"hyperschema": "^1.22.0",
|
|
113
114
|
"ready-resource": "^1.2.0",
|
|
@@ -124,14 +125,5 @@
|
|
|
124
125
|
"brittle": "^4.1.0",
|
|
125
126
|
"typescript": "^5.9.3"
|
|
126
127
|
},
|
|
127
|
-
"license": "Apache-2.0"
|
|
128
|
-
"x": 1,
|
|
129
|
-
"peerDependencies": {
|
|
130
|
-
"bare-bluetooth": ">=0.2.0"
|
|
131
|
-
},
|
|
132
|
-
"peerDependenciesMeta": {
|
|
133
|
-
"bare-bluetooth": {
|
|
134
|
-
"optional": true
|
|
135
|
-
}
|
|
136
|
-
}
|
|
128
|
+
"license": "Apache-2.0"
|
|
137
129
|
}
|
package/src/bluetooth.js
CHANGED
|
@@ -1,33 +1,22 @@
|
|
|
1
1
|
import ReadyResource from 'ready-resource'
|
|
2
|
+
import BluetoothSwarm from 'ble-swarm'
|
|
3
|
+
import crypto from 'hypercore-crypto'
|
|
2
4
|
import safetyCatch from 'safety-catch'
|
|
5
|
+
import b4a from 'b4a'
|
|
3
6
|
|
|
4
|
-
import { BLETransport } from '@cero-base/core/network/transports/ble'
|
|
5
7
|
import { Pairing } from '@cero-base/core/pairing'
|
|
6
8
|
import { Invite } from '@cero-base/core/invite'
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*/
|
|
15
|
-
async function loadBackend() {
|
|
16
|
-
try {
|
|
17
|
-
return await import('bare-bluetooth')
|
|
18
|
-
} catch {
|
|
19
|
-
return null
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* `me.bluetooth` — the whole app-facing surface for nearby (Bluetooth) sync.
|
|
25
|
-
* Bluetooth is a cero feature, not a parallel API: it only changes how peers
|
|
26
|
-
* meet and carry bytes; capability-gated replication still decides what syncs.
|
|
11
|
+
* `me.bluetooth` — the app-facing surface for nearby (Bluetooth) sync, a thin
|
|
12
|
+
* facade over ble-swarm. Bluetooth only changes how peers meet and carry
|
|
13
|
+
* bytes; capability-gated replication still decides what syncs. Discovery is
|
|
14
|
+
* one topic-derived service UUID at a time (tag `cero-ble`) — the data service
|
|
15
|
+
* sits on a fixed per-tag UUID, so switching topics only retunes the radio.
|
|
27
16
|
*
|
|
28
17
|
* ```js
|
|
29
18
|
* const me = await cero(dir, spec, { channel, bluetooth: true })
|
|
30
|
-
* me.bluetooth.state // 'unsupported' | 'unauthorized' | 'off' | 'waiting' | 'on'
|
|
19
|
+
* me.bluetooth.state // 'unsupported' | 'unauthorized' | 'off' | 'waiting' | 'starting' | 'on'
|
|
31
20
|
* await me.bluetooth.start()
|
|
32
21
|
* me.bluetooth.peers // Map of live BLE links
|
|
33
22
|
* me.bluetooth.on('update', () => {})
|
|
@@ -39,7 +28,7 @@ export class Bluetooth extends ReadyResource {
|
|
|
39
28
|
/**
|
|
40
29
|
* @param {object} handle Root cero Handle (network + identity + channel).
|
|
41
30
|
* @param {object} [opts]
|
|
42
|
-
* @param {any} [opts.backend] Injected bare-bluetooth-shaped backend (tests);
|
|
31
|
+
* @param {any} [opts.backend] Injected bare-bluetooth-shaped backend (tests); omitted → ble-swarm loads its own, null/false → unsupported.
|
|
43
32
|
* @param {boolean} [opts.autoStart] Start on handle open (from `cero({ bluetooth: true })`).
|
|
44
33
|
* @param {number} [opts.maxOutbound] Max concurrent outbound links; gossip covers the rest.
|
|
45
34
|
* @param {number} [opts.maxInbound] Max concurrent inbound sessions; newcomers past this are refused.
|
|
@@ -48,28 +37,90 @@ export class Bluetooth extends ReadyResource {
|
|
|
48
37
|
constructor(handle, { backend, autoStart, maxOutbound, maxInbound, pipe } = {}) {
|
|
49
38
|
super()
|
|
50
39
|
this._handle = handle
|
|
51
|
-
// explicit null/false disables bluetooth; only an omitted backend lazy-loads
|
|
52
|
-
this._backend = backend || null
|
|
53
|
-
this._lazy = backend === undefined
|
|
54
40
|
this._autoStart = autoStart === true
|
|
55
|
-
|
|
56
|
-
this.
|
|
57
|
-
this.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
41
|
+
/** @type {{ hex: string, count: number, timer: any } | null} active invite rendezvous (single topic — one at a time) */
|
|
42
|
+
this._announce = null
|
|
43
|
+
this._restorePending = false
|
|
44
|
+
|
|
45
|
+
const identity = handle.identity
|
|
46
|
+
// the mesh topic: one per channel, or a fixed global topic when
|
|
47
|
+
// channelless — any nearby cero device links (global nearby; strangers
|
|
48
|
+
// still sync zero bytes, replication is capability-gated)
|
|
49
|
+
this._topic = crypto.hash(b4a.from(handle.network.channel || 'cero-ble'))
|
|
50
|
+
this.swarm = new BluetoothSwarm({
|
|
51
|
+
backend,
|
|
52
|
+
// injected links authenticate with the same long-lived identity as the
|
|
53
|
+
// swarm, so one person reached over Wi-Fi and BLE dedupes to one peer
|
|
54
|
+
keyPair: { publicKey: identity.publicKey, secretKey: identity.secretKey },
|
|
55
|
+
topic: this._topic,
|
|
56
|
+
tag: 'cero-ble',
|
|
57
|
+
pipe,
|
|
58
|
+
maxOutbound,
|
|
59
|
+
maxInbound
|
|
60
|
+
})
|
|
61
|
+
this.swarm.on('update', () => {
|
|
62
|
+
// an ended rendezvous retunes back to the mesh only once its links
|
|
63
|
+
// drain — retuning drops links, and the pairing link still carries
|
|
64
|
+
// the joiner's initial replication
|
|
65
|
+
if (this._restorePending && this.swarm.peers.size === 0) {
|
|
66
|
+
this._restorePending = false
|
|
67
|
+
this.swarm.setTopic(this._topic).catch(safetyCatch)
|
|
68
|
+
}
|
|
69
|
+
this.emit('update')
|
|
70
|
+
})
|
|
71
|
+
this.swarm.on('connection', (conn) => {
|
|
72
|
+
// the same treatment as any transport: wakeup, replication, pairing
|
|
73
|
+
this._handle.network.inject(conn)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @returns {'unsupported'|'unauthorized'|'off'|'waiting'|'starting'|'on'} */
|
|
78
|
+
get state() {
|
|
79
|
+
return this.swarm.state
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @returns {Map<string, any>} Live BLE links, keyed by peer public key. */
|
|
83
|
+
get peers() {
|
|
84
|
+
return this.swarm.peers
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async _open() {
|
|
88
|
+
if (this._autoStart) await this.start()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Begin advertising + scanning. Idempotent; no-op when unsupported.
|
|
93
|
+
*
|
|
94
|
+
* @returns {Promise<void>}
|
|
95
|
+
*/
|
|
96
|
+
async start() {
|
|
97
|
+
await this.swarm.start()
|
|
63
98
|
}
|
|
64
99
|
|
|
65
100
|
/**
|
|
66
|
-
*
|
|
67
|
-
* the
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
|
|
101
|
+
* Stop advertising/scanning and drop links; open invite rendezvous end with
|
|
102
|
+
* the radio. Idempotent. Local data and the rest of the network (DHT) are
|
|
103
|
+
* untouched.
|
|
104
|
+
*
|
|
105
|
+
* @returns {Promise<void>}
|
|
106
|
+
*/
|
|
107
|
+
async stop() {
|
|
108
|
+
this._clearAnnounce()
|
|
109
|
+
await this.swarm.stop()
|
|
110
|
+
// while stopped setTopic just sticks — the next start() is back on the mesh
|
|
111
|
+
await this.swarm.setTopic(this._topic)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Offline join rendezvous: retune the radio to the invite-derived topic so
|
|
116
|
+
* holder and joiner find each other with zero DHT. One topic at a time —
|
|
117
|
+
* announcing a new invite replaces the previous rendezvous. Returns a stop
|
|
118
|
+
* function — closing the QR must stop the rendezvous so a photographed
|
|
119
|
+
* invite doesn't stay an ambient discovery beacon (admission itself is
|
|
120
|
+
* always gated by blind-pairing verifying the invite). The retune back to
|
|
121
|
+
* the mesh topic waits for live links to drain: the link a join just
|
|
122
|
+
* established survives and carries the joiner's initial replication.
|
|
123
|
+
* Auto-stops at the invite's expiry, on `stop()`, and on close.
|
|
73
124
|
*
|
|
74
125
|
* Only active while nearby sync is on: before `start()` (and after `stop()`)
|
|
75
126
|
* this is a no-op — the user controls the radio, and a join must not touch
|
|
@@ -79,138 +130,70 @@ export class Bluetooth extends ReadyResource {
|
|
|
79
130
|
* @returns {() => void}
|
|
80
131
|
*/
|
|
81
132
|
announce(invite) {
|
|
82
|
-
if (!this.
|
|
83
|
-
if (
|
|
133
|
+
if (!this.swarm.supported) return () => {}
|
|
134
|
+
if (this.state !== 'on' && this.state !== 'waiting' && this.state !== 'starting') {
|
|
84
135
|
return () => {}
|
|
136
|
+
}
|
|
85
137
|
const topic = Pairing.inviteTopic(invite)
|
|
86
138
|
if (!topic) return () => {}
|
|
87
139
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.
|
|
101
|
-
|
|
102
|
-
const { expires } = Invite.parse(invite)
|
|
103
|
-
const timer = expires > 0 ? setTimeout(() => stop(), Math.max(0, expires - Date.now())) : null
|
|
140
|
+
const hex = b4a.toString(topic, 'hex')
|
|
141
|
+
if (this._announce && this._announce.hex !== hex) this._stopAnnounce()
|
|
142
|
+
if (!this._announce) {
|
|
143
|
+
const entry = { hex, count: 0, timer: null }
|
|
144
|
+
const { expires } = Invite.parse(invite)
|
|
145
|
+
if (expires > 0) {
|
|
146
|
+
entry.timer = setTimeout(() => this._stopAnnounce(), Math.max(0, expires - Date.now()))
|
|
147
|
+
}
|
|
148
|
+
this._announce = entry
|
|
149
|
+
this._restorePending = false
|
|
150
|
+
this.swarm.setTopic(topic).catch(safetyCatch)
|
|
151
|
+
}
|
|
152
|
+
const entry = this._announce
|
|
153
|
+
entry.count++
|
|
104
154
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
155
|
+
let stopped = false
|
|
156
|
+
return () => {
|
|
157
|
+
if (stopped) return
|
|
158
|
+
stopped = true
|
|
159
|
+
if (this._announce === entry && --entry.count <= 0) this._stopAnnounce()
|
|
109
160
|
}
|
|
110
|
-
return stop
|
|
111
161
|
}
|
|
112
162
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
163
|
+
_stopAnnounce() {
|
|
164
|
+
this._clearAnnounce()
|
|
165
|
+
if (this.swarm.peers.size > 0) this._restorePending = true
|
|
166
|
+
else this.swarm.setTopic(this._topic).catch(safetyCatch)
|
|
116
167
|
}
|
|
117
168
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (!
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (this._autoStart) await this.start()
|
|
169
|
+
_clearAnnounce() {
|
|
170
|
+
const e = this._announce
|
|
171
|
+
if (!e) return
|
|
172
|
+
if (e.timer) clearTimeout(e.timer)
|
|
173
|
+
this._announce = null
|
|
174
|
+
this._restorePending = false
|
|
125
175
|
}
|
|
126
176
|
|
|
127
177
|
/**
|
|
128
|
-
*
|
|
129
|
-
* No-op (stays `unsupported`) when no backend is present.
|
|
178
|
+
* Host-lifecycle pause (app backgrounded): radio down, user intent kept.
|
|
130
179
|
*
|
|
131
180
|
* @returns {Promise<void>}
|
|
132
181
|
*/
|
|
133
|
-
async
|
|
134
|
-
|
|
135
|
-
if (this.state === 'unsupported') return
|
|
136
|
-
if (this._transport) {
|
|
137
|
-
// reuse one transport across toggles — recreating leaks iOS CoreBluetooth
|
|
138
|
-
// managers (can't destroy()) and their stale GATT service
|
|
139
|
-
this._transport.name = this._name || ''
|
|
140
|
-
this._transport.resume()
|
|
141
|
-
this.state = this._transport.state
|
|
142
|
-
this.emit('update')
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
const handle = this._handle
|
|
146
|
-
|
|
147
|
-
this._transport = new BLETransport({
|
|
148
|
-
backend: this._backend,
|
|
149
|
-
network: handle.network,
|
|
150
|
-
// channel isolation for free: same channel → same UUID, like the swarm.
|
|
151
|
-
// No channel → the identity topic (unchanged global-mesh semantics).
|
|
152
|
-
uuid: handle.network.channel
|
|
153
|
-
? Buffer.from(handle.network.channel)
|
|
154
|
-
: handle.identity.publicKey,
|
|
155
|
-
nodeId: handle.identity.publicKey,
|
|
156
|
-
name: this._name || '',
|
|
157
|
-
pipe: this._pipe,
|
|
158
|
-
maxOutbound: this._maxOutbound,
|
|
159
|
-
maxInbound: this._maxInbound,
|
|
160
|
-
// Android scans in a battery-saver mode by default — too slow for a mesh
|
|
161
|
-
scanOptions:
|
|
162
|
-
typeof Bare !== 'undefined' && Bare.platform === 'android'
|
|
163
|
-
? { scanMode: this._backend.Central.SCAN_MODE_LOW_LATENCY }
|
|
164
|
-
: undefined
|
|
165
|
-
})
|
|
166
|
-
const transport = this._transport
|
|
167
|
-
transport.on('update', () => {
|
|
168
|
-
// a torn-down or replaced transport can still emit late adapter events
|
|
169
|
-
if (this._transport !== transport) return
|
|
170
|
-
this.state = transport.state
|
|
171
|
-
this.emit('update')
|
|
172
|
-
})
|
|
173
|
-
try {
|
|
174
|
-
await this._transport.ready()
|
|
175
|
-
this.state = this._transport.state
|
|
176
|
-
} catch (err) {
|
|
177
|
-
this._transport = null
|
|
178
|
-
this.state = 'off'
|
|
179
|
-
throw err
|
|
180
|
-
}
|
|
181
|
-
this.emit('update')
|
|
182
|
+
async suspend() {
|
|
183
|
+
await this.swarm.suspend()
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
/**
|
|
185
|
-
*
|
|
186
|
-
* data and the rest of the network (DHT) are untouched.
|
|
187
|
+
* Resume after a host-lifecycle pause.
|
|
187
188
|
*
|
|
188
189
|
* @returns {Promise<void>}
|
|
189
190
|
*/
|
|
190
|
-
async
|
|
191
|
-
|
|
192
|
-
this._announces.delete(t)
|
|
193
|
-
await t.close().catch(safetyCatch)
|
|
194
|
-
}
|
|
195
|
-
if (!this._transport) return
|
|
196
|
-
// suspend, don't close: keep the transport (and its GATT service) so a later
|
|
197
|
-
// start() resumes the same instance instead of leaking a new one. suspend()
|
|
198
|
-
// is async (it says goodbye + drains before hanging up); await it.
|
|
199
|
-
await this._transport.suspend()
|
|
200
|
-
this.state = 'off'
|
|
201
|
-
this.emit('update')
|
|
191
|
+
async resume() {
|
|
192
|
+
await this.swarm.resume()
|
|
202
193
|
}
|
|
203
194
|
|
|
204
195
|
async _close() {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
await t.close().catch(safetyCatch)
|
|
208
|
-
}
|
|
209
|
-
if (this._transport) {
|
|
210
|
-
await this._transport.close().catch(safetyCatch)
|
|
211
|
-
this._transport = null
|
|
212
|
-
}
|
|
213
|
-
this.state = 'off'
|
|
214
|
-
this.emit('update')
|
|
196
|
+
this._clearAnnounce()
|
|
197
|
+
await this.swarm.destroy()
|
|
215
198
|
}
|
|
216
199
|
}
|
package/src/handle/index.js
CHANGED
|
@@ -837,6 +837,7 @@ export class Handle extends ReadyResource {
|
|
|
837
837
|
if (this.parent || this.closing || this.closed || this._suspended) return
|
|
838
838
|
this._suspended = true
|
|
839
839
|
await Promise.all([...this.children].map((c) => c.pair?.suspend()))
|
|
840
|
+
await this.bluetooth?.suspend()
|
|
840
841
|
await this.network.suspend()
|
|
841
842
|
try {
|
|
842
843
|
await this.store.store.suspend()
|
|
@@ -859,6 +860,7 @@ export class Handle extends ReadyResource {
|
|
|
859
860
|
this._onerror(err)
|
|
860
861
|
}
|
|
861
862
|
await this.network.resume()
|
|
863
|
+
await this.bluetooth?.resume()
|
|
862
864
|
await Promise.all([...this.children].map((child) => child.pair?.resume()))
|
|
863
865
|
}
|
|
864
866
|
|
package/src/index.js
CHANGED
|
@@ -202,7 +202,8 @@ export async function cero(dir, spec, opts = {}) {
|
|
|
202
202
|
backend: bt.backend,
|
|
203
203
|
autoStart: bt.autoStart !== false,
|
|
204
204
|
maxOutbound: bt.maxOutbound,
|
|
205
|
-
maxInbound: bt.maxInbound
|
|
205
|
+
maxInbound: bt.maxInbound,
|
|
206
|
+
pipe: bt.pipe
|
|
206
207
|
})
|
|
207
208
|
me.once('close', () => me.bluetooth.close().catch(safetyCatch))
|
|
208
209
|
await me.bluetooth.ready()
|
package/types/bluetooth.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `me.bluetooth` — the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* `me.bluetooth` — the app-facing surface for nearby (Bluetooth) sync, a thin
|
|
3
|
+
* facade over ble-swarm. Bluetooth only changes how peers meet and carry
|
|
4
|
+
* bytes; capability-gated replication still decides what syncs. Discovery is
|
|
5
|
+
* one topic-derived service UUID at a time (tag `cero-ble`) — the data service
|
|
6
|
+
* sits on a fixed per-tag UUID, so switching topics only retunes the radio.
|
|
5
7
|
*
|
|
6
8
|
* ```js
|
|
7
9
|
* const me = await cero(dir, spec, { channel, bluetooth: true })
|
|
8
|
-
* me.bluetooth.state // 'unsupported' | 'unauthorized' | 'off' | 'waiting' | 'on'
|
|
10
|
+
* me.bluetooth.state // 'unsupported' | 'unauthorized' | 'off' | 'waiting' | 'starting' | 'on'
|
|
9
11
|
* await me.bluetooth.start()
|
|
10
12
|
* me.bluetooth.peers // Map of live BLE links
|
|
11
13
|
* me.bluetooth.on('update', () => {})
|
|
@@ -17,7 +19,7 @@ export class Bluetooth extends ReadyResource {
|
|
|
17
19
|
/**
|
|
18
20
|
* @param {object} handle Root cero Handle (network + identity + channel).
|
|
19
21
|
* @param {object} [opts]
|
|
20
|
-
* @param {any} [opts.backend] Injected bare-bluetooth-shaped backend (tests);
|
|
22
|
+
* @param {any} [opts.backend] Injected bare-bluetooth-shaped backend (tests); omitted → ble-swarm loads its own, null/false → unsupported.
|
|
21
23
|
* @param {boolean} [opts.autoStart] Start on handle open (from `cero({ bluetooth: true })`).
|
|
22
24
|
* @param {number} [opts.maxOutbound] Max concurrent outbound links; gossip covers the rest.
|
|
23
25
|
* @param {number} [opts.maxInbound] Max concurrent inbound sessions; newcomers past this are refused.
|
|
@@ -31,25 +33,44 @@ export class Bluetooth extends ReadyResource {
|
|
|
31
33
|
pipe?: "l2cap" | "gatt";
|
|
32
34
|
});
|
|
33
35
|
_handle: any;
|
|
34
|
-
_backend: any;
|
|
35
|
-
_lazy: boolean;
|
|
36
36
|
_autoStart: boolean;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
/** @type {{ hex: string, count: number, timer: any } | null} active invite rendezvous (single topic — one at a time) */
|
|
38
|
+
_announce: {
|
|
39
|
+
hex: string;
|
|
40
|
+
count: number;
|
|
41
|
+
timer: any;
|
|
42
|
+
} | null;
|
|
43
|
+
_restorePending: boolean;
|
|
44
|
+
_topic: any;
|
|
45
|
+
swarm: any;
|
|
46
|
+
/** @returns {'unsupported'|'unauthorized'|'off'|'waiting'|'starting'|'on'} */
|
|
47
|
+
get state(): "unsupported" | "unauthorized" | "off" | "waiting" | "starting" | "on";
|
|
48
|
+
/** @returns {Map<string, any>} Live BLE links, keyed by peer public key. */
|
|
49
|
+
get peers(): Map<string, any>;
|
|
50
|
+
/**
|
|
51
|
+
* Begin advertising + scanning. Idempotent; no-op when unsupported.
|
|
52
|
+
*
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
*/
|
|
55
|
+
start(): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Stop advertising/scanning and drop links; open invite rendezvous end with
|
|
58
|
+
* the radio. Idempotent. Local data and the rest of the network (DHT) are
|
|
59
|
+
* untouched.
|
|
60
|
+
*
|
|
61
|
+
* @returns {Promise<void>}
|
|
62
|
+
*/
|
|
63
|
+
stop(): Promise<void>;
|
|
45
64
|
/**
|
|
46
|
-
* Offline join rendezvous
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
65
|
+
* Offline join rendezvous: retune the radio to the invite-derived topic so
|
|
66
|
+
* holder and joiner find each other with zero DHT. One topic at a time —
|
|
67
|
+
* announcing a new invite replaces the previous rendezvous. Returns a stop
|
|
68
|
+
* function — closing the QR must stop the rendezvous so a photographed
|
|
69
|
+
* invite doesn't stay an ambient discovery beacon (admission itself is
|
|
70
|
+
* always gated by blind-pairing verifying the invite). The retune back to
|
|
71
|
+
* the mesh topic waits for live links to drain: the link a join just
|
|
72
|
+
* established survives and carries the joiner's initial replication.
|
|
73
|
+
* Auto-stops at the invite's expiry, on `stop()`, and on close.
|
|
53
74
|
*
|
|
54
75
|
* Only active while nearby sync is on: before `start()` (and after `stop()`)
|
|
55
76
|
* this is a no-op — the user controls the radio, and a join must not touch
|
|
@@ -59,22 +80,19 @@ export class Bluetooth extends ReadyResource {
|
|
|
59
80
|
* @returns {() => void}
|
|
60
81
|
*/
|
|
61
82
|
announce(invite: string): () => void;
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
_stopAnnounce(): void;
|
|
84
|
+
_clearAnnounce(): void;
|
|
64
85
|
/**
|
|
65
|
-
*
|
|
66
|
-
* No-op (stays `unsupported`) when no backend is present.
|
|
86
|
+
* Host-lifecycle pause (app backgrounded): radio down, user intent kept.
|
|
67
87
|
*
|
|
68
88
|
* @returns {Promise<void>}
|
|
69
89
|
*/
|
|
70
|
-
|
|
90
|
+
suspend(): Promise<void>;
|
|
71
91
|
/**
|
|
72
|
-
*
|
|
73
|
-
* data and the rest of the network (DHT) are untouched.
|
|
92
|
+
* Resume after a host-lifecycle pause.
|
|
74
93
|
*
|
|
75
94
|
* @returns {Promise<void>}
|
|
76
95
|
*/
|
|
77
|
-
|
|
96
|
+
resume(): Promise<void>;
|
|
78
97
|
}
|
|
79
98
|
import ReadyResource from 'ready-resource';
|
|
80
|
-
import { BLETransport } from '@cero-base/core/network/transports/ble';
|