@steve02081504/fount-p2p 0.0.12 → 0.0.14

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.
Files changed (79) hide show
  1. package/README.md +41 -10
  2. package/core/composite_key.mjs +5 -5
  3. package/crypto/crypto.mjs +3 -3
  4. package/crypto/key.mjs +12 -12
  5. package/dag/storage.mjs +36 -36
  6. package/discovery/advert_peer_hints.mjs +9 -3
  7. package/discovery/adverts.mjs +115 -0
  8. package/discovery/bt/index.mjs +153 -112
  9. package/discovery/bt/probe_child.mjs +2 -1
  10. package/discovery/bt/runtime.mjs +2 -9
  11. package/discovery/index.mjs +267 -62
  12. package/discovery/internal/signal_crypto.mjs +109 -0
  13. package/discovery/lan.mjs +279 -0
  14. package/discovery/lan_interfaces.mjs +78 -0
  15. package/discovery/lan_peer_hints.mjs +22 -5
  16. package/discovery/nostr.mjs +474 -65
  17. package/federation/chunk_fetch_pending.mjs +10 -13
  18. package/federation/manifest_fetch_pending.mjs +1 -3
  19. package/files/assemble.mjs +14 -14
  20. package/files/assemble_stream.mjs +6 -6
  21. package/files/chunk_responder.mjs +29 -20
  22. package/files/evfs.mjs +29 -28
  23. package/files/manifest_fetch.mjs +16 -3
  24. package/files/public_manifest.mjs +11 -11
  25. package/files/transfer_key_registry.mjs +2 -2
  26. package/index.mjs +86 -11
  27. package/infra/cli.mjs +62 -0
  28. package/infra/debug_log.mjs +56 -0
  29. package/infra/default_node_dir.mjs +22 -0
  30. package/infra/priority.mjs +56 -0
  31. package/infra/service.mjs +140 -0
  32. package/infra/tunables.json +5 -0
  33. package/link/frame.mjs +9 -16
  34. package/link/handshake.mjs +25 -18
  35. package/link/pipe.mjs +8 -8
  36. package/link/providers/ble_gatt.mjs +6 -6
  37. package/link/providers/lan_tcp.mjs +62 -33
  38. package/link/providers/webrtc.mjs +3 -1
  39. package/link/rtc.mjs +3 -3
  40. package/mailbox/consumer_registry.mjs +2 -2
  41. package/mailbox/deliver_or_store.mjs +5 -5
  42. package/mailbox/wire.mjs +28 -24
  43. package/node/entity_store.mjs +6 -6
  44. package/node/instance.mjs +46 -27
  45. package/node/local_data_revision.mjs +26 -0
  46. package/node/log.mjs +56 -0
  47. package/node/network.mjs +37 -5
  48. package/node/reputation_store.mjs +4 -4
  49. package/node/reputation_sync.mjs +318 -0
  50. package/node/routing_profile.mjs +21 -0
  51. package/node/signaling_config.mjs +30 -11
  52. package/overlay/index.mjs +22 -1
  53. package/package.json +13 -2
  54. package/rooms/scoped_link.mjs +17 -166
  55. package/transport/advert_ingest.mjs +11 -14
  56. package/transport/group_link_set.mjs +149 -99
  57. package/transport/link_registry.mjs +211 -60
  58. package/transport/mesh_keepalive.mjs +217 -0
  59. package/transport/node_scope.mjs +289 -0
  60. package/transport/offer_answer.mjs +45 -48
  61. package/transport/peer_pool.mjs +116 -14
  62. package/transport/remote_user_room.mjs +6 -9
  63. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  64. package/transport/runtime_bootstrap.mjs +170 -108
  65. package/transport/tunables.json +11 -0
  66. package/transport/tunables.mjs +18 -0
  67. package/transport/user_room.mjs +83 -158
  68. package/trust_graph/build.mjs +0 -2
  69. package/trust_graph/cache.mjs +12 -3
  70. package/trust_graph/registry.mjs +6 -6
  71. package/trust_graph/send.mjs +0 -3
  72. package/utils/async_mutex.mjs +4 -4
  73. package/utils/emit_safe.mjs +3 -3
  74. package/utils/json_io.mjs +12 -12
  75. package/utils/map_pool.mjs +5 -5
  76. package/wire/part_ingress.mjs +32 -28
  77. package/wire/part_query.mjs +26 -20
  78. package/discovery/mdns.mjs +0 -197
  79. package/transport/signal_crypto.mjs +0 -104
@@ -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 MAX_ADVERT_BLOB_BYTES = 12 * 1024
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
- * advert 映射序列化为可读 characteristic blob。
27
- * @param {Map<string, Uint8Array>} adverts topic → payload 映射
28
- * @returns {Buffer} JSON 序列化后的 advert blob
63
+ * @param {Map<string, Uint8Array>} presence nodeHash → encrypted advert
64
+ * @returns {Buffer} JSON blob
29
65
  */
30
- function serializeAdvertBlob(adverts) {
31
- const entries = [...adverts.entries()].map(([topic, bytes]) => ({
32
- topic,
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 > MAX_ADVERT_BLOB_BYTES)
37
- throw new Error(`p2p: bluetooth advert blob exceeds ${MAX_ADVERT_BLOB_BYTES} bytes`)
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
- * characteristic blob 解析 advert 列表。
43
- * @param {Uint8Array | Buffer} raw 原始 blob 字节
44
- * @returns {Array<{ topic: string, bytes: Uint8Array }>} 解析出的 advert 条目
78
+ * 外层 JSON nodeHash 不可信;只抽出加密 advert bytes,验签后再记可见池 / hint。
79
+ * @param {Uint8Array | Buffer} raw 原始 blob
80
+ * @returns {Uint8Array[]} 加密 advert 列表
45
81
  */
46
- function parseAdvertBlob(raw) {
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
- topic: entry.topic || '',
52
- bytes: Uint8Array.from(Buffer.from(entry.data || '', 'base64')),
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
- * topic bucket 注册监听器。
62
- * @param {Map<string, Set<Function>>} bucket topic 监听器集合
63
- * @param {string} topic 订阅 topic
64
- * @param {Function} listener advert 回调
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 addListener(bucket, topic, listener) {
68
- if (!bucket.has(topic)) bucket.set(topic, new Set())
69
- bucket.get(topic).add(listener)
70
- return () => {
71
- const set = bucket.get(topic)
72
- if (!set) return
73
- set.delete(listener)
74
- if (!set.size) bucket.delete(topic)
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
- * - 默认在 Windows 上只启用 scan 侧发现(单适配器 central+peripheral 常冲突)
81
- * - 其他平台默认 dual:advertise + scan
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 adverts = new Map()
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
- * 初始化 peripheral(Bleno)运行时。
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 advertCharacteristic = new bleno.Characteristic({
144
+ const presenceCharacteristic = new bleno.Characteristic({
111
145
  uuid: BT_CHARACTERISTIC_UUID,
112
146
  properties: ['read'],
113
147
  /**
114
- * stoprocent/bleno onReadRequest(connection, offset, callback)
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 = serializeAdvertBlob(adverts)
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
- * stoprocent/bleno onWriteRequest(connection, data, offset, withoutResponse, callback)
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 topic = String(parsed?.topic || '')
181
+ const to = normalizeHex64(parsed?.to)
150
182
  const bytes = Uint8Array.from(Buffer.from(String(parsed?.data || ''), 'base64'))
151
- if (topic && bytes.byteLength)
152
- for (const listener of signalListeners.get(topic) || [])
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: [advertCharacteristic, signalCharacteristic],
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 (!adverts.size && !signalListeners.size) {
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 (adverts.size) serializeAdvertBlob(adverts)
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
- * 连接并读取远端 peripheral advert characteristic。
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 { topic, bytes } of parseAdvertBlob(raw)) {
215
- const listeners = advertListeners.get(topic)
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
- * Central:按 peer hint 连接并对端 signal characteristic 写短包。
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 sendSignalViaGatt(topic, to, bytes) {
254
- const hint = getBtPeerHint(to)
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
- topic: String(topic),
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
- * 先查 noble 已缓存的 peripheral,避免只等下一次 advertise 漏报。
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
- * 广播指定 topic advert。
329
- * @param {string} topic advert 主题
330
- * @param {Uint8Array} bytes advert 载荷
331
- * @returns {Promise<() => void>} 取消广播函数
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 advertise(topic, bytes) {
369
+ async startPresence(getBeacon) {
334
370
  if (role === 'scan') return () => { }
335
- adverts.set(String(topic), Uint8Array.from(bytes))
336
- await refreshAdvertising()
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
- adverts.delete(String(topic))
387
+ clearInterval(timer)
388
+ if (localNodeHash) localPresence.delete(localNodeHash)
339
389
  void refreshAdvertising().catch(() => { })
340
390
  }
341
391
  },
342
392
  /**
343
- * 订阅指定 topic 的远端 advert。
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<boolean>} 是否投递
395
+ * @returns {Promise<void>}
358
396
  */
359
- sendSignal(topic, to, bytes) {
360
- return sendSignalViaGatt(topic, to, bytes)
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
- * 监听经本机 peripheral signal characteristic 写入的信令。
364
- * @param {string} topic 信令 topic
365
- * @param {Function} onSignal 回调
366
- * @returns {Promise<() => void>} 取消订阅
402
+ * @param {string} localNodeHash 本机 hash
403
+ * @param {(bytes: Uint8Array) => void} onSignal 回调
404
+ * @returns {Promise<() => void>} 取消信令监听
367
405
  */
368
- async onSignal(topic, onSignal) {
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
- stop()
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.env.FOUNT_BT_PROBE_MS || 3000)
10
+ const timeoutMs = Number(process.argv[2] || 3000)
10
11
 
11
12
  try {
12
13
  const noble = await loadNoble()
@@ -6,13 +6,10 @@ import { fileURLToPath } from 'node:url'
6
6
 
7
7
  /**
8
8
  * 解析 Bluetooth 角色(scan / dual)。
9
- * Win32 默认 scan(单适配器 central+peripheral 常冲突);可用 FOUNT_BT_DISCOVERY_ROLE 覆盖。
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 退出。