@steve02081504/fount-p2p 0.0.12 → 0.0.13
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/README.md +41 -10
- package/core/composite_key.mjs +5 -5
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +109 -0
- package/discovery/bt/index.mjs +153 -112
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +2 -9
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +394 -59
- package/federation/chunk_fetch_pending.mjs +10 -13
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +29 -28
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/frame.mjs +9 -16
- package/link/handshake.mjs +14 -15
- package/link/pipe.mjs +8 -8
- package/link/providers/ble_gatt.mjs +6 -6
- package/link/rtc.mjs +3 -3
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +5 -5
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +37 -5
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +22 -1
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -166
- package/transport/advert_ingest.mjs +11 -14
- package/transport/group_link_set.mjs +149 -99
- package/transport/link_registry.mjs +211 -60
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +45 -48
- package/transport/peer_pool.mjs +116 -14
- package/transport/remote_user_room.mjs +6 -9
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +170 -108
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +83 -158
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +0 -3
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +3 -3
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/wire/part_ingress.mjs +32 -28
- package/wire/part_query.mjs +26 -20
- package/discovery/mdns.mjs +0 -197
- package/transport/signal_crypto.mjs +0 -104
package/discovery/bt/index.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
|
|
3
|
+
import { isHex64, normalizeHex64 } from '../../core/hexIds.mjs'
|
|
4
|
+
import { nodeDebug, shortHash } from '../../node/log.mjs'
|
|
5
|
+
import { noteAdvertPeerHints } from '../advert_peer_hints.mjs'
|
|
6
|
+
import { ingestNetworkAdvert } from '../adverts.mjs'
|
|
7
|
+
|
|
3
8
|
import { getBtPeerHint } from './peer_hints.mjs'
|
|
4
9
|
import { canUseBluetoothRuntime, loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from './runtime.mjs'
|
|
5
10
|
|
|
@@ -10,12 +15,44 @@ const BT_SERVICE_UUID = 'f017f017f017f017f017f017f017f017'
|
|
|
10
15
|
const BT_CHARACTERISTIC_UUID = 'f017f017f017f017f017f017f017f018'
|
|
11
16
|
const BT_SIGNAL_CHAR_UUID = 'f017f017f017f017f017f017f017f01b'
|
|
12
17
|
const BT_DEVICE_NAME = 'fount-bt'
|
|
13
|
-
const
|
|
18
|
+
const MAX_PRESENCE_BLOB_BYTES = 12 * 1024
|
|
14
19
|
const MAX_SIGNAL_BLOB_BYTES = 8 * 1024
|
|
15
20
|
const PERIPHERAL_RESCAN_MS = 15_000
|
|
16
21
|
|
|
22
|
+
/** @type {Map<string, number>} nodeHash → lastSeenAt */
|
|
23
|
+
const visibleByHash = new Map()
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} nodeHash 节点 hash
|
|
27
|
+
* @param {number} [now=Date.now()] 当前时间
|
|
28
|
+
* @returns {void}
|
|
29
|
+
*/
|
|
30
|
+
export function noteBtVisibleNode(nodeHash, now = Date.now()) {
|
|
31
|
+
const hash = normalizeHex64(nodeHash)
|
|
32
|
+
if (!isHex64(hash)) return
|
|
33
|
+
visibleByHash.set(hash, now)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {number} [now=Date.now()] 当前时间
|
|
38
|
+
* @param {number} [ttlMs=PERIPHERAL_RESCAN_MS * 4] TTL
|
|
39
|
+
* @returns {string[]} 可见 nodeHash
|
|
40
|
+
*/
|
|
41
|
+
export function listBtVisibleNodeHashes(now = Date.now(), ttlMs = PERIPHERAL_RESCAN_MS * 4) {
|
|
42
|
+
/** @type {string[]} */
|
|
43
|
+
const out = []
|
|
44
|
+
for (const [hash, seenAt] of visibleByHash)
|
|
45
|
+
if (now - seenAt <= ttlMs) out.push(hash)
|
|
46
|
+
else visibleByHash.delete(hash)
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @returns {void} 测试用 */
|
|
51
|
+
export function clearBtVisibleNodes() {
|
|
52
|
+
visibleByHash.clear()
|
|
53
|
+
}
|
|
54
|
+
|
|
17
55
|
/**
|
|
18
|
-
* 探测本机 BT discovery 是否可用;失败则回落其它 discovery,不抛错。
|
|
19
56
|
* @returns {Promise<boolean>} 可用为 true
|
|
20
57
|
*/
|
|
21
58
|
export async function canUseBluetoothDiscovery() {
|
|
@@ -23,34 +60,32 @@ export async function canUseBluetoothDiscovery() {
|
|
|
23
60
|
}
|
|
24
61
|
|
|
25
62
|
/**
|
|
26
|
-
*
|
|
27
|
-
* @
|
|
28
|
-
* @returns {Buffer} JSON 序列化后的 advert blob
|
|
63
|
+
* @param {Map<string, Uint8Array>} presence nodeHash → encrypted advert
|
|
64
|
+
* @returns {Buffer} JSON blob
|
|
29
65
|
*/
|
|
30
|
-
function
|
|
31
|
-
const entries = [...
|
|
32
|
-
|
|
66
|
+
function serializePresenceBlob(presence) {
|
|
67
|
+
const entries = [...presence.entries()].map(([nodeHash, bytes]) => ({
|
|
68
|
+
nodeHash,
|
|
33
69
|
data: Buffer.from(bytes).toString('base64'),
|
|
34
70
|
}))
|
|
35
71
|
const blob = Buffer.from(JSON.stringify({ entries }), 'utf8')
|
|
36
|
-
if (blob.byteLength >
|
|
37
|
-
throw new Error(`p2p: bluetooth
|
|
72
|
+
if (blob.byteLength > MAX_PRESENCE_BLOB_BYTES)
|
|
73
|
+
throw new Error(`p2p: bluetooth presence blob exceeds ${MAX_PRESENCE_BLOB_BYTES} bytes`)
|
|
38
74
|
return blob
|
|
39
75
|
}
|
|
40
76
|
|
|
41
77
|
/**
|
|
42
|
-
*
|
|
43
|
-
* @param {Uint8Array | Buffer} raw 原始 blob
|
|
44
|
-
* @returns {
|
|
78
|
+
* 外层 JSON 的 nodeHash 不可信;只抽出加密 advert bytes,验签后再记可见池 / hint。
|
|
79
|
+
* @param {Uint8Array | Buffer} raw 原始 blob
|
|
80
|
+
* @returns {Uint8Array[]} 加密 advert 列表
|
|
45
81
|
*/
|
|
46
|
-
function
|
|
82
|
+
function parsePresenceBlob(raw) {
|
|
47
83
|
try {
|
|
48
84
|
const parsed = JSON.parse(Buffer.from(raw).toString('utf8'))
|
|
49
85
|
if (!parsed?.entries?.length) return []
|
|
50
|
-
return parsed.entries.map(entry =>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
})).filter(entry => entry.topic && entry.bytes.byteLength)
|
|
86
|
+
return parsed.entries.map(entry =>
|
|
87
|
+
Uint8Array.from(Buffer.from(entry.data || '', 'base64')),
|
|
88
|
+
).filter(bytes => bytes.byteLength)
|
|
54
89
|
}
|
|
55
90
|
catch {
|
|
56
91
|
return []
|
|
@@ -58,38 +93,36 @@ function parseAdvertBlob(raw) {
|
|
|
58
93
|
}
|
|
59
94
|
|
|
60
95
|
/**
|
|
61
|
-
*
|
|
62
|
-
* @param {
|
|
63
|
-
* @param {string}
|
|
64
|
-
* @
|
|
65
|
-
* @returns {() => void} 取消订阅函数
|
|
96
|
+
* 扫描到的 BT presence:验签 network advert 后写入可见池与 peer hint。
|
|
97
|
+
* @param {Uint8Array} bytes 加密 network advert
|
|
98
|
+
* @param {{ peripheralId: string }} meta 扫描 meta(至少 peripheralId)
|
|
99
|
+
* @returns {Promise<{ verifiedNodeHash: string, body: object } | null>} 验签结果
|
|
66
100
|
*/
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
101
|
+
export async function acceptBtScannedPresence(bytes, meta) {
|
|
102
|
+
const peripheralId = String(meta?.peripheralId || '').trim()
|
|
103
|
+
if (!peripheralId || !bytes?.byteLength) return null
|
|
104
|
+
const ingested = await ingestNetworkAdvert(bytes, meta)
|
|
105
|
+
if (!ingested) return null
|
|
106
|
+
const firstSeen = !visibleByHash.has(ingested.verifiedNodeHash)
|
|
107
|
+
noteBtVisibleNode(ingested.verifiedNodeHash)
|
|
108
|
+
noteAdvertPeerHints(ingested.verifiedNodeHash, ingested.body, meta)
|
|
109
|
+
if (firstSeen)
|
|
110
|
+
nodeDebug('p2p:bt peer visible', {
|
|
111
|
+
peer: shortHash(ingested.verifiedNodeHash),
|
|
112
|
+
peripheralId,
|
|
113
|
+
})
|
|
114
|
+
return ingested
|
|
76
115
|
}
|
|
77
116
|
|
|
78
117
|
/**
|
|
79
|
-
*
|
|
80
|
-
* -
|
|
81
|
-
*
|
|
82
|
-
* - 通过固定 BLE service + read characteristic 传输完整 advert 列表,避免 31-byte 广告包限制
|
|
83
|
-
* - dual 下额外暴露 write characteristic 传短信令;central 可按 peer hint 写信令
|
|
84
|
-
*
|
|
85
|
-
* @returns {import('./index.mjs').DiscoveryProvider} Bluetooth 发现提供者
|
|
118
|
+
* Bluetooth 发现提供者:固定 GATT service 传 presence / node signal,无 topic。
|
|
119
|
+
* Win 默认 scan-only;非 Win 默认 dual(scan+advertise)。
|
|
120
|
+
* @returns {import('../index.mjs').DiscoveryProvider} Bluetooth 发现提供者
|
|
86
121
|
*/
|
|
87
122
|
export function createBluetoothDiscoveryProvider() {
|
|
88
123
|
const role = resolveBtRole()
|
|
89
124
|
/** @type {Map<string, Uint8Array>} */
|
|
90
|
-
const
|
|
91
|
-
/** @type {Map<string, Set<Function>>} */
|
|
92
|
-
const advertListeners = new Map()
|
|
125
|
+
const localPresence = new Map()
|
|
93
126
|
/** @type {Map<string, Set<Function>>} */
|
|
94
127
|
const signalListeners = new Map()
|
|
95
128
|
/** @type {Map<string, number>} */
|
|
@@ -98,28 +131,28 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
98
131
|
let blenoRuntime = null
|
|
99
132
|
let scanningStarted = false
|
|
100
133
|
let advertisingStarted = false
|
|
134
|
+
/** @type {string | null} */
|
|
135
|
+
let localNodeHash = null
|
|
101
136
|
|
|
102
137
|
/**
|
|
103
|
-
*
|
|
104
|
-
* @returns {Promise<any|null>} Bleno 实例;scan 模式下为 null
|
|
138
|
+
* @returns {Promise<any|null>} Bleno 实例
|
|
105
139
|
*/
|
|
106
140
|
async function ensurePeripheralRuntime() {
|
|
107
141
|
if (role === 'scan') return null
|
|
108
142
|
if (blenoRuntime) return blenoRuntime
|
|
109
143
|
const bleno = await loadBleno()
|
|
110
|
-
const
|
|
144
|
+
const presenceCharacteristic = new bleno.Characteristic({
|
|
111
145
|
uuid: BT_CHARACTERISTIC_UUID,
|
|
112
146
|
properties: ['read'],
|
|
113
147
|
/**
|
|
114
|
-
*
|
|
115
|
-
* @param {*} _connection 连接句柄
|
|
148
|
+
* @param {*} _connection 连接
|
|
116
149
|
* @param {number} offset 偏移
|
|
117
150
|
* @param {Function} callback 结果
|
|
118
151
|
* @returns {void}
|
|
119
152
|
*/
|
|
120
153
|
onReadRequest(_connection, offset, callback) {
|
|
121
154
|
try {
|
|
122
|
-
const blob =
|
|
155
|
+
const blob = serializePresenceBlob(localPresence)
|
|
123
156
|
if (offset > blob.length) {
|
|
124
157
|
callback(bleno.Characteristic.RESULT_INVALID_OFFSET)
|
|
125
158
|
return
|
|
@@ -135,8 +168,7 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
135
168
|
uuid: BT_SIGNAL_CHAR_UUID,
|
|
136
169
|
properties: ['write', 'writeWithoutResponse'],
|
|
137
170
|
/**
|
|
138
|
-
*
|
|
139
|
-
* @param {*} _connection 连接句柄
|
|
171
|
+
* @param {*} _connection 连接
|
|
140
172
|
* @param {Buffer} data 信令 blob
|
|
141
173
|
* @param {number} _offset 偏移
|
|
142
174
|
* @param {boolean} _withoutResponse 无响应写
|
|
@@ -146,12 +178,11 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
146
178
|
onWriteRequest(_connection, data, _offset, _withoutResponse, callback) {
|
|
147
179
|
try {
|
|
148
180
|
const parsed = JSON.parse(Buffer.from(data).toString('utf8'))
|
|
149
|
-
const
|
|
181
|
+
const to = normalizeHex64(parsed?.to)
|
|
150
182
|
const bytes = Uint8Array.from(Buffer.from(String(parsed?.data || ''), 'base64'))
|
|
151
|
-
if (
|
|
152
|
-
for (const listener of signalListeners.get(
|
|
183
|
+
if (isHex64(to) && bytes.byteLength)
|
|
184
|
+
for (const listener of signalListeners.get(to) || [])
|
|
153
185
|
listener(bytes, { provider: 'bt' })
|
|
154
|
-
|
|
155
186
|
callback(bleno.Characteristic.RESULT_SUCCESS)
|
|
156
187
|
}
|
|
157
188
|
catch {
|
|
@@ -163,7 +194,7 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
163
194
|
await bleno.setServicesAsync([
|
|
164
195
|
new bleno.PrimaryService({
|
|
165
196
|
uuid: BT_SERVICE_UUID,
|
|
166
|
-
characteristics: [
|
|
197
|
+
characteristics: [presenceCharacteristic, signalCharacteristic],
|
|
167
198
|
}),
|
|
168
199
|
])
|
|
169
200
|
blenoRuntime = bleno
|
|
@@ -171,21 +202,20 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
171
202
|
}
|
|
172
203
|
|
|
173
204
|
/**
|
|
174
|
-
* 刷新 BLE 广播状态。
|
|
175
205
|
* @returns {Promise<void>}
|
|
176
206
|
*/
|
|
177
207
|
async function refreshAdvertising() {
|
|
178
208
|
if (role === 'scan') return
|
|
179
209
|
const bleno = await ensurePeripheralRuntime()
|
|
180
210
|
if (!bleno) return
|
|
181
|
-
if (!
|
|
211
|
+
if (!localPresence.size && !signalListeners.size) {
|
|
182
212
|
if (advertisingStarted) {
|
|
183
213
|
await bleno.stopAdvertisingAsync().catch(() => { })
|
|
184
214
|
advertisingStarted = false
|
|
185
215
|
}
|
|
186
216
|
return
|
|
187
217
|
}
|
|
188
|
-
if (
|
|
218
|
+
if (localPresence.size) serializePresenceBlob(localPresence)
|
|
189
219
|
if (!advertisingStarted) {
|
|
190
220
|
await bleno.startAdvertisingAsync(BT_DEVICE_NAME, [BT_SERVICE_UUID])
|
|
191
221
|
advertisingStarted = true
|
|
@@ -193,8 +223,7 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
193
223
|
}
|
|
194
224
|
|
|
195
225
|
/**
|
|
196
|
-
*
|
|
197
|
-
* @param {*} peripheral Noble peripheral 对象
|
|
226
|
+
* @param {*} peripheral Noble peripheral
|
|
198
227
|
* @returns {Promise<void>}
|
|
199
228
|
*/
|
|
200
229
|
async function inspectPeripheral(peripheral) {
|
|
@@ -211,23 +240,16 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
211
240
|
)
|
|
212
241
|
if (!characteristics?.length) return
|
|
213
242
|
const raw = await characteristics[0].readAsync()
|
|
214
|
-
for (const
|
|
215
|
-
|
|
216
|
-
if (!listeners?.size) continue
|
|
217
|
-
for (const listener of listeners)
|
|
218
|
-
listener(bytes, { provider: 'bt', peripheralId: inspectKey })
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
/* ignore transient bluetooth failures */
|
|
243
|
+
for (const bytes of parsePresenceBlob(raw))
|
|
244
|
+
await acceptBtScannedPresence(bytes, { provider: 'bt', peripheralId: inspectKey })
|
|
223
245
|
}
|
|
246
|
+
catch { /* ignore */ }
|
|
224
247
|
finally {
|
|
225
248
|
try { await peripheral.disconnectAsync() } catch { /* ignore */ }
|
|
226
249
|
}
|
|
227
250
|
}
|
|
228
251
|
|
|
229
252
|
/**
|
|
230
|
-
* 启动 Noble 扫描运行时。
|
|
231
253
|
* @returns {Promise<void>}
|
|
232
254
|
*/
|
|
233
255
|
async function ensureScanRuntime() {
|
|
@@ -243,19 +265,16 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
243
265
|
}
|
|
244
266
|
|
|
245
267
|
/**
|
|
246
|
-
*
|
|
247
|
-
* 无 hint 时返回 false(正常降级,不算错误)。
|
|
248
|
-
* @param {string} topic 信令 topic
|
|
249
|
-
* @param {string} to 目标 nodeHash
|
|
268
|
+
* @param {string} toNodeHash 目标
|
|
250
269
|
* @param {Uint8Array} bytes 载荷
|
|
251
|
-
* @returns {Promise<boolean>}
|
|
270
|
+
* @returns {Promise<boolean>} 是否经 GATT 发出
|
|
252
271
|
*/
|
|
253
|
-
async function
|
|
254
|
-
const
|
|
272
|
+
async function sendNodeSignalViaGatt(toNodeHash, bytes) {
|
|
273
|
+
const hash = normalizeHex64(toNodeHash)
|
|
274
|
+
const hint = getBtPeerHint(hash)
|
|
255
275
|
if (!hint) return false
|
|
256
276
|
const blob = Buffer.from(JSON.stringify({
|
|
257
|
-
|
|
258
|
-
to: String(to),
|
|
277
|
+
to: hash,
|
|
259
278
|
data: Buffer.from(bytes).toString('base64'),
|
|
260
279
|
}), 'utf8')
|
|
261
280
|
if (blob.byteLength > MAX_SIGNAL_BLOB_BYTES)
|
|
@@ -264,8 +283,7 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
264
283
|
const noble = nobleRuntime
|
|
265
284
|
const wantId = hint.peripheralId
|
|
266
285
|
/**
|
|
267
|
-
*
|
|
268
|
-
* @returns {*|null} 已缓存的 peripheral,未命中为 null
|
|
286
|
+
* @returns {*|null} Noble 缓存中的 peripheral,未找到为 null
|
|
269
287
|
*/
|
|
270
288
|
function cachedPeripheral() {
|
|
271
289
|
const table = noble?._peripherals
|
|
@@ -325,53 +343,76 @@ export function createBluetoothDiscoveryProvider() {
|
|
|
325
343
|
priority: 20,
|
|
326
344
|
caps: { canDiscover: true, canSignal: true, canRelay: false },
|
|
327
345
|
/**
|
|
328
|
-
*
|
|
329
|
-
* @
|
|
330
|
-
|
|
331
|
-
|
|
346
|
+
* @param {{ limit?: number, roomSecret?: string }} [options] 扫描选项
|
|
347
|
+
* @returns {Promise<string[]>} 群扫描时 BT 无群语义,返回空
|
|
348
|
+
*/
|
|
349
|
+
async listVisibleNodeHashes(options = {}) {
|
|
350
|
+
if (options.roomSecret) return []
|
|
351
|
+
const limit = Math.max(1, Number(options.limit) || 64)
|
|
352
|
+
if (role !== 'scan') await ensureScanRuntime().catch(() => { })
|
|
353
|
+
return listBtVisibleNodeHashes().slice(0, limit)
|
|
354
|
+
},
|
|
355
|
+
/**
|
|
356
|
+
* @param {string} nodeHash 目标
|
|
357
|
+
* @returns {Promise<boolean>} 有 BT hint 且 GATT 可达时为 true
|
|
358
|
+
*/
|
|
359
|
+
async connectToNode(nodeHash) {
|
|
360
|
+
const hash = normalizeHex64(nodeHash)
|
|
361
|
+
if (!isHex64(hash)) return false
|
|
362
|
+
if (!getBtPeerHint(hash)) return false
|
|
363
|
+
return await sendNodeSignalViaGatt(hash, new Uint8Array([0])).catch(() => false)
|
|
364
|
+
},
|
|
365
|
+
/**
|
|
366
|
+
* @param {() => Promise<{ nodeHash: string, advertBytes?: Uint8Array } | null>} getBeacon beacon
|
|
367
|
+
* @returns {Promise<() => void>} 停止 presence 广播
|
|
332
368
|
*/
|
|
333
|
-
async
|
|
369
|
+
async startPresence(getBeacon) {
|
|
334
370
|
if (role === 'scan') return () => { }
|
|
335
|
-
|
|
336
|
-
|
|
371
|
+
/**
|
|
372
|
+
* @returns {Promise<void>}
|
|
373
|
+
*/
|
|
374
|
+
const refresh = async () => {
|
|
375
|
+
const body = await getBeacon?.()
|
|
376
|
+
if (!body?.nodeHash || !body.advertBytes?.byteLength) return
|
|
377
|
+
const hash = normalizeHex64(body.nodeHash)
|
|
378
|
+
if (!isHex64(hash)) return
|
|
379
|
+
localNodeHash = hash
|
|
380
|
+
localPresence.set(hash, Uint8Array.from(body.advertBytes))
|
|
381
|
+
noteBtVisibleNode(hash)
|
|
382
|
+
await refreshAdvertising()
|
|
383
|
+
}
|
|
384
|
+
await refresh().catch(() => { })
|
|
385
|
+
const timer = setInterval(() => { void refresh().catch(() => { }) }, 30_000)
|
|
337
386
|
return () => {
|
|
338
|
-
|
|
387
|
+
clearInterval(timer)
|
|
388
|
+
if (localNodeHash) localPresence.delete(localNodeHash)
|
|
339
389
|
void refreshAdvertising().catch(() => { })
|
|
340
390
|
}
|
|
341
391
|
},
|
|
342
392
|
/**
|
|
343
|
-
*
|
|
344
|
-
* @param {string} topic advert 主题
|
|
345
|
-
* @param {Function} onAdvert advert 回调
|
|
346
|
-
* @returns {Promise<() => void>} 取消订阅函数
|
|
347
|
-
*/
|
|
348
|
-
async subscribe(topic, onAdvert) {
|
|
349
|
-
await ensureScanRuntime()
|
|
350
|
-
return addListener(advertListeners, String(topic), onAdvert)
|
|
351
|
-
},
|
|
352
|
-
/**
|
|
353
|
-
* 经 GATT 向近场 peer 发送信令;无 peer hint 时返回 false。
|
|
354
|
-
* @param {string} topic 信令 topic
|
|
355
|
-
* @param {string} to 目标 nodeHash
|
|
393
|
+
* @param {string} toNodeHash 目标
|
|
356
394
|
* @param {Uint8Array} bytes 载荷
|
|
357
|
-
* @returns {Promise<
|
|
395
|
+
* @returns {Promise<void>}
|
|
358
396
|
*/
|
|
359
|
-
|
|
360
|
-
|
|
397
|
+
async sendNodeSignal(toNodeHash, bytes) {
|
|
398
|
+
const ok = await sendNodeSignalViaGatt(toNodeHash, bytes)
|
|
399
|
+
if (!ok) throw new Error('p2p: bt signal unavailable')
|
|
361
400
|
},
|
|
362
401
|
/**
|
|
363
|
-
*
|
|
364
|
-
* @param {
|
|
365
|
-
* @
|
|
366
|
-
* @returns {Promise<() => void>} 取消订阅
|
|
402
|
+
* @param {string} localNodeHash 本机 hash
|
|
403
|
+
* @param {(bytes: Uint8Array) => void} onSignal 回调
|
|
404
|
+
* @returns {Promise<() => void>} 取消信令监听
|
|
367
405
|
*/
|
|
368
|
-
async
|
|
406
|
+
async listenNodeSignals(localNodeHash, onSignal) {
|
|
369
407
|
if (role === 'scan') return () => { }
|
|
408
|
+
const hash = normalizeHex64(localNodeHash)
|
|
409
|
+
if (!isHex64(hash)) throw new Error('p2p: invalid nodeHash')
|
|
370
410
|
await ensurePeripheralRuntime()
|
|
411
|
+
if (!signalListeners.has(hash)) signalListeners.set(hash, new Set())
|
|
412
|
+
signalListeners.get(hash).add(onSignal)
|
|
371
413
|
await refreshAdvertising()
|
|
372
|
-
const stop = addListener(signalListeners, String(topic), onSignal)
|
|
373
414
|
return () => {
|
|
374
|
-
|
|
415
|
+
signalListeners.get(hash)?.delete(onSignal)
|
|
375
416
|
void refreshAdvertising().catch(() => { })
|
|
376
417
|
}
|
|
377
418
|
},
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 子进程 BT 可用性探测:load → poweredOn → stop → exit。
|
|
3
3
|
* 由 `canUseBluetoothRuntime` spawn;勿在父进程直接跑 waitPoweredOn(会拖住事件循环)。
|
|
4
|
+
* argv[2]:waitPoweredOn 超时毫秒(默认 3000)。
|
|
4
5
|
*/
|
|
5
6
|
import process from 'node:process'
|
|
6
7
|
|
|
7
8
|
import { loadNoble, waitPoweredOn } from './runtime.mjs'
|
|
8
9
|
|
|
9
|
-
const timeoutMs = Number(process.
|
|
10
|
+
const timeoutMs = Number(process.argv[2] || 3000)
|
|
10
11
|
|
|
11
12
|
try {
|
|
12
13
|
const noble = await loadNoble()
|
package/discovery/bt/runtime.mjs
CHANGED
|
@@ -6,13 +6,10 @@ import { fileURLToPath } from 'node:url'
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 解析 Bluetooth 角色(scan / dual)。
|
|
9
|
-
* Win32 默认 scan(单适配器 central+peripheral
|
|
9
|
+
* Win32 默认 scan(单适配器 central+peripheral 常冲突);其他平台 dual。
|
|
10
10
|
* @returns {'scan' | 'dual'} 生效角色
|
|
11
11
|
*/
|
|
12
12
|
export function resolveBtRole() {
|
|
13
|
-
const override = String(process.env.FOUNT_BT_DISCOVERY_ROLE || '').trim().toLowerCase()
|
|
14
|
-
if (override === 'dual') return 'dual'
|
|
15
|
-
if (override === 'scan') return 'scan'
|
|
16
13
|
return process.platform === 'win32' ? 'scan' : 'dual'
|
|
17
14
|
}
|
|
18
15
|
|
|
@@ -46,12 +43,8 @@ const PROBE_CHILD = join(dirname(fileURLToPath(import.meta.url)), 'probe_child.m
|
|
|
46
43
|
*/
|
|
47
44
|
function probeBluetoothInSubprocess(timeoutMs) {
|
|
48
45
|
return new Promise(resolve => {
|
|
49
|
-
const child = spawn(process.execPath, [PROBE_CHILD], {
|
|
46
|
+
const child = spawn(process.execPath, [PROBE_CHILD, String(timeoutMs)], {
|
|
50
47
|
stdio: 'ignore',
|
|
51
|
-
env: {
|
|
52
|
-
...process.env,
|
|
53
|
-
FOUNT_BT_PROBE_MS: String(timeoutMs),
|
|
54
|
-
},
|
|
55
48
|
windowsHide: true,
|
|
56
49
|
})
|
|
57
50
|
// 探测是旁路缓存填充;勿拖住父进程事件循环 / shutdown 退出。
|