@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 +1 -1
- package/discovery/bt/index.mjs +12 -26
- package/discovery/bt/runtime.mjs +55 -5
- package/discovery/index.mjs +12 -17
- package/files/chunk_responder.mjs +4 -8
- package/link/providers/ble_gatt.mjs +3 -14
- package/link/rtc.mjs +1 -1
- package/package.json +1 -1
- package/transport/link_registry.mjs +5 -9
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
|
|
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
|
|
package/discovery/bt/index.mjs
CHANGED
|
@@ -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
|
-
* 探测本机
|
|
19
|
-
* @returns {Promise<boolean>}
|
|
18
|
+
* 探测本机 BT discovery 是否可用;失败则回落其它 discovery,不抛错。
|
|
19
|
+
* @returns {Promise<boolean>} 可用为 true
|
|
20
20
|
*/
|
|
21
21
|
export async function canUseBluetoothDiscovery() {
|
|
22
|
-
|
|
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<
|
|
251
|
+
* @returns {Promise<boolean>} 是否投递
|
|
259
252
|
*/
|
|
260
253
|
async function sendSignalViaGatt(topic, to, bytes) {
|
|
261
254
|
const hint = getBtPeerHint(to)
|
|
262
|
-
if (!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<
|
|
357
|
+
* @returns {Promise<boolean>} 是否投递
|
|
372
358
|
*/
|
|
373
|
-
|
|
374
|
-
|
|
359
|
+
sendSignal(topic, to, bytes) {
|
|
360
|
+
return sendSignalViaGatt(topic, to, bytes)
|
|
375
361
|
},
|
|
376
362
|
/**
|
|
377
363
|
* 监听经本机 peripheral signal characteristic 写入的信令。
|
package/discovery/bt/runtime.mjs
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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 (
|
|
93
|
+
if (!wait)
|
|
44
94
|
throw new Error('p2p: bluetooth runtime missing waitForPoweredOn(Async)')
|
|
45
95
|
return wait.call(runtime, timeout)
|
|
46
96
|
}
|
package/discovery/index.mjs
CHANGED
|
@@ -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>,
|
|
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
|
|
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
|
|
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)
|
|
99
|
-
await Promise.resolve(provider.sendSignal(topic, to, bytes))
|
|
100
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
18
|
+
* 探测 BLE GATT 数据链路是否可用;失败则回落其它 link provider。
|
|
21
19
|
* @returns {Promise<boolean>} 可用为 true
|
|
22
20
|
*/
|
|
23
21
|
export async function canUseBleGattLink() {
|
|
24
|
-
|
|
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(
|
|
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
|
@@ -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
|
-
|
|
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
|
|
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))
|