@steve02081504/fount-p2p 0.0.8 → 0.0.10

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 CHANGED
@@ -54,7 +54,7 @@ During development: `node scripts/check-imports.mjs` validates relative imports.
54
54
 
55
55
  ## Optional dependencies
56
56
 
57
- - `@stoprocent/noble` / `@stoprocent/bleno` — Bluetooth discovery (attempted by default; skipped when not installed)
57
+ - `@stoprocent/noble` / `@stoprocent/bleno` — Bluetooth (optionalDependencies). Unavailable when there is no adapter, load fails, or the radio never reaches poweredOn — other discovery/link paths continue.
58
58
 
59
59
  Group chunk remote storage (S3, etc.) is implemented by the shell as `GroupStoragePlugin` and injected; see `node/storage_plugins.mjs` for the local reference implementation.
60
60
 
@@ -1,7 +1,7 @@
1
1
  import { Buffer } from 'node:buffer'
2
2
 
3
3
  import { getBtPeerHint } from './peer_hints.mjs'
4
- import { loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from './runtime.mjs'
4
+ import { canUseBluetoothRuntime, loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from './runtime.mjs'
5
5
 
6
6
  /** 重导出 waitPoweredOn,供 discovery 调用方使用。 */
7
7
  export { waitPoweredOn } from './runtime.mjs'
@@ -15,19 +15,11 @@ const MAX_SIGNAL_BLOB_BYTES = 8 * 1024
15
15
  const PERIPHERAL_RESCAN_MS = 15_000
16
16
 
17
17
  /**
18
- * 探测本机 noble 运行时是否具备 BT 扫描所需 API。
19
- * @returns {Promise<boolean>} noble 可加载且具备 startScanningAsync 与 waitForPoweredOn(Async) 时为 true
18
+ * 探测本机 BT discovery 是否可用;失败则回落其它 discovery,不抛错。
19
+ * @returns {Promise<boolean>} 可用为 true
20
20
  */
21
21
  export async function canUseBluetoothDiscovery() {
22
- try {
23
- const noble = await loadNoble()
24
- if (typeof noble.startScanningAsync !== 'function') return false
25
- const wait = noble.waitForPoweredOnAsync ?? noble.waitForPoweredOn
26
- return typeof wait === 'function'
27
- }
28
- catch {
29
- return false
30
- }
22
+ return canUseBluetoothRuntime()
31
23
  }
32
24
 
33
25
  /**
@@ -252,14 +244,15 @@ export function createBluetoothDiscoveryProvider() {
252
244
 
253
245
  /**
254
246
  * Central:按 peer hint 连接并对端 signal characteristic 写短包。
247
+ * 无 hint 时返回 false(正常降级,不算错误)。
255
248
  * @param {string} topic 信令 topic
256
249
  * @param {string} to 目标 nodeHash
257
250
  * @param {Uint8Array} bytes 载荷
258
- * @returns {Promise<void>}
251
+ * @returns {Promise<boolean>} 是否投递
259
252
  */
260
253
  async function sendSignalViaGatt(topic, to, bytes) {
261
254
  const hint = getBtPeerHint(to)
262
- if (!hint) throw new Error('p2p: bt signal missing peer hint')
255
+ if (!hint) return false
263
256
  const blob = Buffer.from(JSON.stringify({
264
257
  topic: String(topic),
265
258
  to: String(to),
@@ -324,20 +317,13 @@ export function createBluetoothDiscoveryProvider() {
324
317
  finally {
325
318
  try { await peripheral.disconnectAsync() } catch { /* ignore */ }
326
319
  }
320
+ return true
327
321
  }
328
322
 
329
323
  return {
330
324
  id: 'bt',
331
325
  priority: 20,
332
326
  caps: { canDiscover: true, canSignal: true, canRelay: false },
333
- /**
334
- * 是否具备对该 peer 的 GATT 信令能力(需未过期 BT peer hint)。
335
- * @param {string} to 目标 nodeHash
336
- * @returns {boolean} 有 hint 则可尝试
337
- */
338
- canSignalTo(to) {
339
- return !!getBtPeerHint(to)
340
- },
341
327
  /**
342
328
  * 广播指定 topic 的 advert。
343
329
  * @param {string} topic advert 主题
@@ -364,14 +350,14 @@ export function createBluetoothDiscoveryProvider() {
364
350
  return addListener(advertListeners, String(topic), onAdvert)
365
351
  },
366
352
  /**
367
- * 经 GATT 向近场 peer 发送信令。
353
+ * 经 GATT 向近场 peer 发送信令;无 peer hint 时返回 false。
368
354
  * @param {string} topic 信令 topic
369
355
  * @param {string} to 目标 nodeHash
370
356
  * @param {Uint8Array} bytes 载荷
371
- * @returns {Promise<void>}
357
+ * @returns {Promise<boolean>} 是否投递
372
358
  */
373
- async sendSignal(topic, to, bytes) {
374
- await sendSignalViaGatt(topic, to, bytes)
359
+ sendSignal(topic, to, bytes) {
360
+ return sendSignalViaGatt(topic, to, bytes)
375
361
  },
376
362
  /**
377
363
  * 监听经本机 peripheral signal characteristic 写入的信令。
@@ -1,3 +1,4 @@
1
+ import { existsSync, readdirSync } from 'node:fs'
1
2
  import process from 'node:process'
2
3
 
3
4
  /**
@@ -13,20 +14,69 @@ export function resolveBtRole() {
13
14
  }
14
15
 
15
16
  /**
16
- * 加载 Noble BLE central。
17
+ * 廉价硬件探测:明确无适配器时跳过 noble/bleno import(加载本身会拉起 native,无适配器时常在 teardown SIGSEGV)。
18
+ * @returns {boolean | null} true=有迹象,false=明确无,null=未知(继续尝试加载,失败则回落)
19
+ */
20
+ export function probeBluetoothHardware() {
21
+ if (process.platform === 'linux') try {
22
+ const dir = '/sys/class/bluetooth'
23
+ if (!existsSync(dir)) return false
24
+ return readdirSync(dir).some(name => name && !name.startsWith('.'))
25
+ }
26
+ catch {
27
+ return false
28
+ }
29
+ return null
30
+ }
31
+
32
+ /** @type {boolean | null} canUseBluetoothRuntime 缓存 */
33
+ let cachedRuntimeOk = null
34
+
35
+ /**
36
+ * 探测 BT 栈是否真正可用(有适配器迹象 → 能 load → poweredOn)。
37
+ * 任一步失败返回 false,调用方回落其它 discovery/link;不抛错。
38
+ * @param {number} [timeoutMs=3000] waitPoweredOn 超时
39
+ * @returns {Promise<boolean>} 可用为 true
40
+ */
41
+ export async function canUseBluetoothRuntime(timeoutMs = 3000) {
42
+ if (cachedRuntimeOk !== null) return cachedRuntimeOk
43
+ if (probeBluetoothHardware() === false) {
44
+ cachedRuntimeOk = false
45
+ return false
46
+ }
47
+ try {
48
+ const noble = await loadNoble()
49
+ if (!noble.startScanningAsync) {
50
+ cachedRuntimeOk = false
51
+ return false
52
+ }
53
+ await waitPoweredOn(noble, timeoutMs)
54
+ cachedRuntimeOk = true
55
+ }
56
+ catch {
57
+ cachedRuntimeOk = false
58
+ }
59
+ return cachedRuntimeOk
60
+ }
61
+
62
+ /**
63
+ * 加载 Noble BLE central。明确无适配器时直接抛错,避免无意义的 native import。
17
64
  * @returns {Promise<any>} noble 运行时
18
65
  */
19
66
  export async function loadNoble() {
67
+ if (probeBluetoothHardware() === false)
68
+ throw new Error('p2p: no bluetooth adapter')
20
69
  const mod = await import('@stoprocent/noble')
21
- if (typeof mod.withBindings === 'function') return mod.withBindings('default')
22
- return mod.default ?? mod
70
+ return mod?.withBindings?.('default') || mod?.default || mod
23
71
  }
24
72
 
25
73
  /**
26
- * 加载 Bleno BLE peripheral。
74
+ * 加载 Bleno BLE peripheral。明确无适配器时直接抛错,避免无意义的 native import
27
75
  * @returns {Promise<any>} bleno 运行时
28
76
  */
29
77
  export async function loadBleno() {
78
+ if (probeBluetoothHardware() === false)
79
+ throw new Error('p2p: no bluetooth adapter')
30
80
  const mod = await import('@stoprocent/bleno')
31
81
  if (typeof mod.withBindings === 'function') return mod.withBindings('default')
32
82
  return mod.default ?? mod
@@ -40,7 +90,7 @@ export async function loadBleno() {
40
90
  */
41
91
  export async function waitPoweredOn(runtime, timeout) {
42
92
  const wait = runtime.waitForPoweredOnAsync ?? runtime.waitForPoweredOn
43
- if (typeof wait !== 'function')
93
+ if (!wait)
44
94
  throw new Error('p2p: bluetooth runtime missing waitForPoweredOn(Async)')
45
95
  return wait.call(runtime, timeout)
46
96
  }
@@ -1,4 +1,4 @@
1
- /** @typedef {{ id: string, priority: number, caps: { canDiscover?: boolean, canSignal?: boolean, canRelay?: boolean }, advertise?: (topic: string, bytes: Uint8Array) => (() => void) | Promise<() => void>, subscribe?: (topic: string, onAdvert: (bytes: Uint8Array, meta?: object) => void) => (() => void) | Promise<() => void>, canSignalTo?: (to: string) => boolean, sendSignal?: (topic: string, to: string, bytes: Uint8Array) => void | Promise<void>, onSignal?: (topic: string, onSignal: (bytes: Uint8Array, meta?: object) => void) => (() => void) | Promise<() => void> }} DiscoveryProvider */
1
+ /** @typedef {{ id: string, priority: number, caps: { canDiscover?: boolean, canSignal?: boolean, canRelay?: boolean }, advertise?: (topic: string, bytes: Uint8Array) => (() => void) | Promise<() => void>, subscribe?: (topic: string, onAdvert: (bytes: Uint8Array, meta?: object) => void) => (() => void) | Promise<() => void>, sendSignal?: (topic: string, to: string, bytes: Uint8Array) => boolean | void | Promise<boolean | void>, onSignal?: (topic: string, onSignal: (bytes: Uint8Array, meta?: object) => void) => (() => void) | Promise<() => void> }} DiscoveryProvider */
2
2
 
3
3
  /** @type {Map<string, DiscoveryProvider>} */
4
4
  const providers = new Map()
@@ -33,6 +33,7 @@ export function listDiscoveryProviders() {
33
33
 
34
34
  /**
35
35
  * 通过所有可用 provider 广播 topic advert。
36
+ * 单路失败静默(正常降级);其它 provider 仍可成功。
36
37
  * @param {string} topic advert 主题
37
38
  * @param {Uint8Array} bytes advert 载荷
38
39
  * @returns {Promise<() => void>} 统一取消函数
@@ -45,10 +46,7 @@ export async function advertiseTopic(topic, bytes) {
45
46
  try {
46
47
  cleanup = await provider.advertise(topic, bytes)
47
48
  }
48
- catch (error) {
49
- console.warn(`p2p: discovery advertise failed for ${provider.id}`, error)
50
- continue
51
- }
49
+ catch { continue }
52
50
  if (typeof cleanup === 'function') cleanups.push(cleanup)
53
51
  }
54
52
  return () => {
@@ -59,6 +57,7 @@ export async function advertiseTopic(topic, bytes) {
59
57
 
60
58
  /**
61
59
  * 通过所有可用 provider 订阅 topic advert。
60
+ * 单路失败静默(正常降级)。
62
61
  * @param {string} topic advert 主题
63
62
  * @param {(bytes: Uint8Array, meta?: object) => void} onAdvert advert 回调
64
63
  * @returns {Promise<() => void>} 统一取消函数
@@ -71,10 +70,7 @@ export async function subscribeTopic(topic, onAdvert) {
71
70
  try {
72
71
  cleanup = await provider.subscribe(topic, onAdvert)
73
72
  }
74
- catch (error) {
75
- console.warn(`p2p: discovery subscribe failed for ${provider.id}`, error)
76
- continue
77
- }
73
+ catch { continue }
78
74
  if (typeof cleanup === 'function') cleanups.push(cleanup)
79
75
  }
80
76
  return () => {
@@ -85,6 +81,8 @@ export async function subscribeTopic(topic, onAdvert) {
85
81
 
86
82
  /**
87
83
  * 通过 discovery provider 发送信令。
84
+ * provider 返回 `false` = 本路不可达(正常降级,不计成功);其它返回值 / void = 已投递。
85
+ * 单路 throw 静默;全部未投递才 throw。
88
86
  * @param {string} topic 信令 topic
89
87
  * @param {string} to 目标节点标识
90
88
  * @param {Uint8Array} bytes 信令载荷
@@ -95,19 +93,19 @@ export async function sendSignal(topic, to, bytes) {
95
93
  if (!capable.length) throw new Error('p2p: no discovery provider can signal')
96
94
  let sent = false
97
95
  let lastError = null
98
- for (const provider of capable) if (provider?.canSignalTo?.(to)) try {
99
- await Promise.resolve(provider.sendSignal(topic, to, bytes))
100
- sent = true
96
+ for (const provider of capable) try {
97
+ if (await Promise.resolve(provider.sendSignal(topic, to, bytes)) !== false)
98
+ sent = true
101
99
  }
102
100
  catch (error) {
103
101
  lastError = error
104
- console.warn(`p2p: discovery signal failed for ${provider.id}`, error)
105
102
  }
106
103
  if (!sent) throw lastError || new Error('p2p: no discovery provider delivered signal')
107
104
  }
108
105
 
109
106
  /**
110
107
  * 通过所有可用 provider 监听信令。
108
+ * 单路失败静默(正常降级)。
111
109
  * @param {string} topic 信令 topic
112
110
  * @param {(bytes: Uint8Array, meta?: object) => void} onSignal 信令回调
113
111
  * @returns {Promise<() => void>} 统一取消函数
@@ -120,10 +118,7 @@ export async function listenSignals(topic, onSignal) {
120
118
  try {
121
119
  cleanup = await provider.onSignal(topic, onSignal)
122
120
  }
123
- catch (error) {
124
- console.warn(`p2p: discovery signal listener failed for ${provider.id}`, error)
125
- continue
126
- }
121
+ catch { continue }
127
122
  if (typeof cleanup === 'function') cleanups.push(cleanup)
128
123
  }
129
124
  return () => {
@@ -94,14 +94,12 @@ export function attachTrustGraphFedChunkResponder(username, room, fedOut, guardG
94
94
  */
95
95
  const send = () => {
96
96
  try { sendChunkData(resp, pid) }
97
- catch (error) {
98
- console.warn('federation: trust-graph chunk response failed', error)
99
- }
97
+ catch { /* peer gone — normal */ }
100
98
  }
101
99
  if (fedOut) fedOut.enqueue(6, send)
102
100
  else send()
103
101
  })
104
- })().catch(error => console.warn('federation: trust-graph chunk handler failed', error))
102
+ })().catch(() => { /* peer/handler race — normal degrade */ })
105
103
  })
106
104
 
107
105
  getChunkData(data => {
@@ -120,14 +118,12 @@ export function attachTrustGraphFedChunkResponder(username, room, fedOut, guardG
120
118
  */
121
119
  const send = () => {
122
120
  try { sendManifestData(resp, pid) }
123
- catch (error) {
124
- console.warn('federation: trust-graph manifest response failed', error)
125
- }
121
+ catch { /* peer gone — normal */ }
126
122
  }
127
123
  if (fedOut) fedOut.enqueue(6, send)
128
124
  else send()
129
125
  })
130
- })().catch(error => console.warn('federation: trust-graph manifest handler failed', error))
126
+ })().catch(() => { /* peer/handler race — normal degrade */ })
131
127
  })
132
128
 
133
129
  getManifestData(data => {
@@ -3,7 +3,7 @@ import { randomBytes } from 'node:crypto'
3
3
 
4
4
  import { normalizeHex64 } from '../../core/hexIds.mjs'
5
5
  import { getBtPeerHint } from '../../discovery/bt/peer_hints.mjs'
6
- import { loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from '../../discovery/bt/runtime.mjs'
6
+ import { canUseBluetoothRuntime, loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from '../../discovery/bt/runtime.mjs'
7
7
  import { asLinkHandle, coercePipeInbound, createLinkPipe } from '../pipe.mjs'
8
8
 
9
9
  import { LINK_LEVEL_BLE_GATT } from './levels.mjs'
@@ -14,23 +14,12 @@ export const BLE_DATA_SERVICE_UUID = 'f017f017f017f017f017f017f017f019'
14
14
  export const BLE_DATA_CHAR_UUID = 'f017f017f017f017f017f017f017f01a'
15
15
  const BT_DEVICE_NAME = 'fount-bt'
16
16
 
17
- let cachedAvailable = null
18
-
19
17
  /**
20
- * 探测 BLE GATT 数据链路是否可用(至少 noble central)。
18
+ * 探测 BLE GATT 数据链路是否可用;失败则回落其它 link provider。
21
19
  * @returns {Promise<boolean>} 可用为 true
22
20
  */
23
21
  export async function canUseBleGattLink() {
24
- if (cachedAvailable !== null) return cachedAvailable
25
- try {
26
- const noble = await loadNoble()
27
- const wait = noble.waitForPoweredOnAsync ?? noble.waitForPoweredOn
28
- cachedAvailable = typeof noble.startScanningAsync === 'function' && typeof wait === 'function'
29
- }
30
- catch {
31
- cachedAvailable = false
32
- }
33
- return cachedAvailable
22
+ return canUseBluetoothRuntime()
34
23
  }
35
24
 
36
25
  /**
package/link/rtc.mjs CHANGED
@@ -8,7 +8,7 @@ import { wrapRtcPeerConnectionForMdns } from '../transport/rtc_mdns_filter.mjs'
8
8
  * libdatachannel 的原生线程在 pc.close() 后仍需时间回收;进程退出时若原生资源未同步销毁,
9
9
  * Windows 上会触发堆损坏(退出码 0xC0000374)。
10
10
  */
11
- const { cleanup = undefined } = await import('node-datachannel').catch(console.error)
11
+ const { cleanup = undefined } = await import('node-datachannel').catch(() => ({}))
12
12
  process.on('exit', () => {
13
13
  try { cleanup?.() } catch { /* already torn down */ }
14
14
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",
@@ -315,7 +315,9 @@ export function createLinkRegistry(options = {}) {
315
315
  /** @type {import('../link/providers/index.mjs').LinkProvider[]} */
316
316
  const listenProviders = []
317
317
  if (ownedLanTcp) listenProviders.push(ownedLanTcp)
318
- if (ownedBleGatt) listenProviders.push(ownedBleGatt)
318
+ // 无适配器时 isAvailable 应在 import native 前返回 false;勿无条件 ensureListening(会 loadBleno)。
319
+ if (ownedBleGatt && await Promise.resolve(ownedBleGatt.isAvailable()))
320
+ listenProviders.push(ownedBleGatt)
319
321
  for (const provider of await listAvailableLinkProviders()) {
320
322
  const id = String(provider.id)
321
323
  if (id.startsWith('lan_tcp') || id.startsWith('ble_gatt')) continue
@@ -347,9 +349,7 @@ export function createLinkRegistry(options = {}) {
347
349
  })
348
350
  if (typeof stop === 'function') stopLinkListeners.push(stop)
349
351
  }
350
- catch (error) {
351
- console.warn(`p2p: link provider listening failed for ${provider.id}`, error)
352
- }
352
+ catch { /* provider listen unavailable — normal degrade */ }
353
353
 
354
354
  if (listDiscoveryProviders().length) {
355
355
  stopSignalListener = await listenSignals(selfTopic, bytes => {
@@ -591,16 +591,12 @@ export function createLinkRegistry(options = {}) {
591
591
  // soft-fail 返回 null(不 throw);必须 continue 才能落到更低 level。
592
592
  const link = await dialOfferAnswer(provider, normalized)
593
593
  if (link) return link
594
- console.warn(`p2p: link dial failed for ${provider.id}: soft-fail`)
595
594
  continue
596
595
  }
597
596
  const link = await dialProvider(provider, normalized)
598
597
  if (link) return link
599
- console.warn(`p2p: link dial failed for ${provider.id}: soft-fail`)
600
- }
601
- catch (error) {
602
- console.warn(`p2p: link dial failed for ${provider.id}: ${error?.message ?? error}`)
603
598
  }
599
+ catch { /* dial failed — try next provider */ }
604
600
 
605
601
  return null
606
602
  })().finally(() => inflights.delete(normalized))