@steve02081504/fount-p2p 0.0.6 → 0.0.7
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/core/tcp_port.mjs +11 -0
- package/discovery/advert_peer_hints.mjs +21 -0
- package/discovery/{bt.mjs → bt/index.mjs} +151 -55
- package/discovery/bt/peer_hints.mjs +41 -0
- package/discovery/bt/runtime.mjs +46 -0
- package/discovery/lan_peer_hints.mjs +43 -0
- package/discovery/mdns.mjs +6 -2
- package/index.mjs +3 -2
- package/link/handshake.mjs +48 -21
- package/link/pipe.mjs +404 -0
- package/link/providers/ble_gatt.mjs +339 -0
- package/link/providers/index.mjs +90 -0
- package/link/providers/lan_tcp.mjs +342 -0
- package/link/providers/levels.mjs +9 -0
- package/link/providers/webrtc.mjs +385 -0
- package/package.json +3 -2
- package/rooms/scoped_link.mjs +5 -3
- package/transport/group_link_set.mjs +5 -3
- package/transport/link_registry.mjs +241 -54
- package/utils/ttl_map.mjs +41 -0
- package/link/link.mjs +0 -617
|
@@ -2,12 +2,22 @@ import { Buffer } from 'node:buffer'
|
|
|
2
2
|
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'
|
|
3
3
|
|
|
4
4
|
import { compareHex64Asc, normalizeHex64 } from '../core/hexIds.mjs'
|
|
5
|
+
import { normalizeTcpPort } from '../core/tcp_port.mjs'
|
|
5
6
|
import { sha256Hex, keyPairFromSeed } from '../crypto/crypto.mjs'
|
|
7
|
+
import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
|
|
6
8
|
import { advertiseTopic, listenSignals, listDiscoveryProviders, registerDiscoveryProvider, sendSignal, subscribeTopic } from '../discovery/index.mjs'
|
|
7
9
|
import { createMdnsDiscoveryProvider } from '../discovery/mdns.mjs'
|
|
8
10
|
import { mergeSignalingRelayUrls, createNostrDiscoveryProvider } from '../discovery/nostr.mjs'
|
|
9
11
|
import { buildSignedAdvert, verifySignedAdvert } from '../link/handshake.mjs'
|
|
10
|
-
import {
|
|
12
|
+
import { createBleGattLinkProvider } from '../link/providers/ble_gatt.mjs'
|
|
13
|
+
import {
|
|
14
|
+
listAvailableLinkProviders,
|
|
15
|
+
listLinkProviders,
|
|
16
|
+
registerLinkProvider,
|
|
17
|
+
unregisterLinkProvider,
|
|
18
|
+
} from '../link/providers/index.mjs'
|
|
19
|
+
import { createLanTcpLinkProvider } from '../link/providers/lan_tcp.mjs'
|
|
20
|
+
import { createWebRtcLinkProvider } from '../link/providers/webrtc.mjs'
|
|
11
21
|
import { ensureNodeSeed, getNodeHash, getNodeTransportSettings } from '../node/identity.mjs'
|
|
12
22
|
import { getSignalingRuntimeConfig } from '../node/instance.mjs'
|
|
13
23
|
import { createOverlayRouter } from '../overlay/index.mjs'
|
|
@@ -189,22 +199,22 @@ function subscribeBucket(buckets, key, listener) {
|
|
|
189
199
|
* 创建 P2P 链路注册表(discovery、信令、直连与 overlay relay)。
|
|
190
200
|
* @param {object} [options] 选项
|
|
191
201
|
* @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array }} [options.localIdentity] 本地身份
|
|
192
|
-
* @param {
|
|
193
|
-
* @param {RTCConfiguration['iceServers']} [options.iceServers] ICE 服务器列表
|
|
202
|
+
* @param {RTCConfiguration['iceServers']} [options.iceServers] ICE 服务器列表(包内 webrtc provider 使用)
|
|
194
203
|
* @param {number} [options.maxActive] 最大并发活跃链路数
|
|
195
204
|
* @param {boolean} [options.autoRegisterDiscoveryProviders] 是否自动注册 discovery provider
|
|
196
|
-
* @
|
|
205
|
+
* @param {boolean} [options.autoRegisterLinkProviders] 是否自动注册内置 link provider
|
|
206
|
+
* @returns {object} link registry 接口(对上层即 fount 网络:ensure/send/subscribe,无传输类型)
|
|
197
207
|
*/
|
|
198
208
|
export function createLinkRegistry(options = {}) {
|
|
199
209
|
const localIdentity = resolveLocalIdentity(options.localIdentity)
|
|
200
|
-
const createLinkImpl = options.createLink ?? createLink
|
|
201
210
|
const iceServers = options.iceServers?.length ? options.iceServers : DEFAULT_ICE_SERVERS
|
|
202
211
|
const maxActive = Math.max(4, Number(options.maxActive) || 32)
|
|
203
212
|
const autoRegisterDiscoveryProviders = options.autoRegisterDiscoveryProviders !== false
|
|
213
|
+
const autoRegisterLinkProviders = options.autoRegisterLinkProviders !== false
|
|
204
214
|
const selfTopic = nodeRendezvousTopic(localIdentity.nodeHash)
|
|
205
|
-
/** @type {Map<string,
|
|
215
|
+
/** @type {Map<string, object>} */
|
|
206
216
|
const links = new Map()
|
|
207
|
-
/** @type {Map<string, Promise<
|
|
217
|
+
/** @type {Map<string, Promise<object | null>>} 按 nodeHash 去重的主动外拨 */
|
|
208
218
|
const inflights = new Map()
|
|
209
219
|
/** @type {Map<string, ReturnType<typeof createBufferedSignalSession>>} 按 connId 索引的信令会话(每条 PC 一个方向) */
|
|
210
220
|
const signalSessions = new Map()
|
|
@@ -222,21 +232,73 @@ export function createLinkRegistry(options = {}) {
|
|
|
222
232
|
let runtimeStarted = false
|
|
223
233
|
let stopAdvert = null
|
|
224
234
|
let stopSignalListener = null
|
|
235
|
+
/** @type {Array<() => void>} */
|
|
236
|
+
const stopLinkListeners = []
|
|
225
237
|
let overlayRouter = null
|
|
238
|
+
/** @type {ReturnType<typeof createLanTcpLinkProvider> | null} 本 registry 持有的 lan_tcp 实例 */
|
|
239
|
+
let ownedLanTcp = null
|
|
240
|
+
/** @type {ReturnType<typeof createBleGattLinkProvider> | null} 本 registry 持有的 ble_gatt 实例 */
|
|
241
|
+
let ownedBleGatt = null
|
|
226
242
|
|
|
227
243
|
/**
|
|
228
|
-
*
|
|
244
|
+
* 自动注册默认 LinkProvider(lan_tcp / webrtc / ble_gatt)。
|
|
245
|
+
* lan_tcp / ble_gatt 每 registry 一个实例(避免 ensureListening 覆盖他人的 localIdentity);
|
|
246
|
+
* webrtc 按 id 单例注册。拨号时再经 isAvailable / canReach 跳过。
|
|
247
|
+
* @returns {Promise<void>}
|
|
248
|
+
*/
|
|
249
|
+
async function ensureLinkProviders() {
|
|
250
|
+
if (!autoRegisterLinkProviders) return
|
|
251
|
+
if (!ownedLanTcp) {
|
|
252
|
+
ownedLanTcp = createLanTcpLinkProvider()
|
|
253
|
+
registerLinkProvider(ownedLanTcp)
|
|
254
|
+
}
|
|
255
|
+
if (!ownedBleGatt) {
|
|
256
|
+
ownedBleGatt = createBleGattLinkProvider()
|
|
257
|
+
registerLinkProvider(ownedBleGatt)
|
|
258
|
+
}
|
|
259
|
+
const ids = new Set(listLinkProviders().map(provider => provider.id))
|
|
260
|
+
if (!ids.has('webrtc'))
|
|
261
|
+
registerLinkProvider(createWebRtcLinkProvider())
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 本 registry 的 LAN TCP 监听端口。
|
|
266
|
+
* @returns {number | null} listen 端口;未 listen 为 null
|
|
267
|
+
*/
|
|
268
|
+
function resolveLanTcpPort() {
|
|
269
|
+
const endpoint = typeof ownedLanTcp?.localEndpoint === 'function' ? ownedLanTcp.localEndpoint() : null
|
|
270
|
+
return normalizeTcpPort(endpoint?.port)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 构造带本机身份与(若已 listen)tcpPort 的签名 advert。
|
|
275
|
+
* node / group / scoped 广播共用,保证 LAN hint 可从任意 topic 学到。
|
|
276
|
+
* @param {string} topic 广播主题
|
|
277
|
+
* @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string, tcpPort?: number }>} 签名 advert
|
|
278
|
+
*/
|
|
279
|
+
async function buildLocalAdvert(topic) {
|
|
280
|
+
const tcpPort = resolveLanTcpPort()
|
|
281
|
+
return await buildSignedAdvert(topic, Date.now(), {
|
|
282
|
+
...localIdentity,
|
|
283
|
+
...tcpPort != null ? { tcpPort } : {},
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 启动 discovery + link provider listening。
|
|
289
|
+
* 先 ensureListening(拿到 lan_tcp 端口),再广播带 tcpPort 的 advert。
|
|
229
290
|
* @returns {Promise<void>}
|
|
230
291
|
*/
|
|
231
292
|
async function ensureDiscoveryRuntime() {
|
|
232
293
|
if (runtimeStarted) return
|
|
233
294
|
runtimeStarted = true
|
|
295
|
+
await ensureLinkProviders()
|
|
234
296
|
if (autoRegisterDiscoveryProviders) {
|
|
235
297
|
const providerIds = new Set(listDiscoveryProviders().map(provider => provider.id))
|
|
236
298
|
if (!providerIds.has('mdns'))
|
|
237
299
|
registerDiscoveryProvider(createMdnsDiscoveryProvider())
|
|
238
300
|
if (!providerIds.has('bt')) {
|
|
239
|
-
const bt = await import('../discovery/bt.mjs').catch(() => null)
|
|
301
|
+
const bt = await import('../discovery/bt/index.mjs').catch(() => null)
|
|
240
302
|
if (await bt?.canUseBluetoothDiscovery?.())
|
|
241
303
|
registerDiscoveryProvider(bt.createBluetoothDiscoveryProvider())
|
|
242
304
|
}
|
|
@@ -248,14 +310,56 @@ export function createLinkRegistry(options = {}) {
|
|
|
248
310
|
?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls),
|
|
249
311
|
}))
|
|
250
312
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
313
|
+
// 只对本 registry 持有的 lan_tcp / ble_gatt 调用 ensureListening。
|
|
314
|
+
// 切勿遍历其它 registry 的 lan_tcp:* / ble_gatt:* ——会覆盖其 localIdentity/onInbound。
|
|
315
|
+
/** @type {import('../link/providers/index.mjs').LinkProvider[]} */
|
|
316
|
+
const listenProviders = []
|
|
317
|
+
if (ownedLanTcp) listenProviders.push(ownedLanTcp)
|
|
318
|
+
if (ownedBleGatt) listenProviders.push(ownedBleGatt)
|
|
319
|
+
for (const provider of await listAvailableLinkProviders()) {
|
|
320
|
+
const id = String(provider.id)
|
|
321
|
+
if (id.startsWith('lan_tcp') || id.startsWith('ble_gatt')) continue
|
|
322
|
+
if (typeof provider.ensureListening === 'function')
|
|
323
|
+
listenProviders.push(provider)
|
|
324
|
+
}
|
|
325
|
+
for (const provider of listenProviders)
|
|
326
|
+
try {
|
|
327
|
+
const stop = await provider.ensureListening({
|
|
328
|
+
localIdentity,
|
|
329
|
+
/**
|
|
330
|
+
* @param {object} link 入站链路
|
|
331
|
+
* @returns {void}
|
|
332
|
+
*/
|
|
333
|
+
onInbound(link) {
|
|
334
|
+
void (async () => {
|
|
335
|
+
try {
|
|
336
|
+
await link.ready
|
|
337
|
+
const remote = normalizeHex64(link.nodeHash)
|
|
338
|
+
if (!remote) {
|
|
339
|
+
await link.close('inbound-no-nodehash')
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
await registerResolvedLink(remote, link)
|
|
343
|
+
}
|
|
344
|
+
catch { /* ignore failed inbound */ }
|
|
345
|
+
})()
|
|
346
|
+
},
|
|
347
|
+
})
|
|
348
|
+
if (typeof stop === 'function') stopLinkListeners.push(stop)
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
console.warn(`p2p: link provider listening failed for ${provider.id}`, error)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (listDiscoveryProviders().length) {
|
|
355
|
+
stopSignalListener = await listenSignals(selfTopic, bytes => {
|
|
356
|
+
void handleIncomingSignal(bytes).catch(() => { })
|
|
357
|
+
})
|
|
358
|
+
stopAdvert = await advertiseTopic(selfTopic, encryptSignalPacket(selfTopic, {
|
|
359
|
+
type: 'advert',
|
|
360
|
+
body: await buildLocalAdvert(selfTopic),
|
|
361
|
+
}))
|
|
362
|
+
}
|
|
259
363
|
}
|
|
260
364
|
|
|
261
365
|
/**
|
|
@@ -290,12 +394,16 @@ export function createLinkRegistry(options = {}) {
|
|
|
290
394
|
}
|
|
291
395
|
|
|
292
396
|
/**
|
|
293
|
-
*
|
|
294
|
-
* @param {
|
|
397
|
+
* 择链:更高 level 优先;同 level 时保留由较小 nodeHash 发起的那条(glare 两端一致)。
|
|
398
|
+
* @param {object} link 链路实例
|
|
295
399
|
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
400
|
+
* @param {object | null} [against] 现有规范链
|
|
296
401
|
* @returns {boolean} 本条链应保留则 true
|
|
297
402
|
*/
|
|
298
|
-
function linkIsPreferred(link, remoteNodeHash) {
|
|
403
|
+
function linkIsPreferred(link, remoteNodeHash, against = null) {
|
|
404
|
+
const level = Number(link.level) || 0
|
|
405
|
+
const againstLevel = Number(against?.level) || 0
|
|
406
|
+
if (against && level !== againstLevel) return level > againstLevel
|
|
299
407
|
const cmp = compareHex64Asc(localIdentity.nodeHash, remoteNodeHash)
|
|
300
408
|
return link.initiator ? cmp < 0 : cmp > 0
|
|
301
409
|
}
|
|
@@ -303,7 +411,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
303
411
|
/**
|
|
304
412
|
* 为链路绑定 envelope 派发与 down 回调。
|
|
305
413
|
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
306
|
-
* @param {
|
|
414
|
+
* @param {object} link 链路实例
|
|
307
415
|
* @returns {void}
|
|
308
416
|
*/
|
|
309
417
|
function wireLink(remoteNodeHash, link) {
|
|
@@ -321,40 +429,52 @@ export function createLinkRegistry(options = {}) {
|
|
|
321
429
|
}
|
|
322
430
|
|
|
323
431
|
/**
|
|
324
|
-
*
|
|
325
|
-
* 保留由较小 nodeHash 发起的那条(两端一致),关闭另一条。单条链路(常态)直接安装。
|
|
432
|
+
* 注册已建立的链路:level 优先,同 level 再按 initiator/nodeHash glare 规则择一。
|
|
326
433
|
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
327
|
-
* @param {
|
|
434
|
+
* @param {object} candidate 候选链路
|
|
328
435
|
* @returns {Promise<void>}
|
|
329
436
|
*/
|
|
330
437
|
async function registerResolvedLink(remoteNodeHash, candidate) {
|
|
331
438
|
const normalized = normalizeHex64(remoteNodeHash)
|
|
332
439
|
const existing = links.get(normalized)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
await candidate.close('glare-loser')
|
|
440
|
+
if (existing && existing !== candidate && !linkIsPreferred(candidate, normalized, existing)) {
|
|
441
|
+
await candidate.close(Number(candidate.level) !== Number(existing.level) ? 'provider-loser' : 'glare-loser')
|
|
336
442
|
return
|
|
337
443
|
}
|
|
338
|
-
// 候选胜出(或无现有):先把 candidate 设为规范链,再关旧链——
|
|
339
|
-
// 这样旧链 onDown 时 links.get 已指向 candidate,不会被误判为规范链而对外报 linkDown。
|
|
340
444
|
links.set(normalized, candidate)
|
|
341
445
|
wireLink(normalized, candidate)
|
|
342
446
|
if (existing && existing !== candidate)
|
|
343
|
-
await existing.close('glare-replaced')
|
|
447
|
+
await existing.close(Number(candidate.level) !== Number(existing.level) ? 'provider-replaced' : 'glare-replaced')
|
|
344
448
|
for (const listener of linkUpListeners)
|
|
345
449
|
try { listener(normalized, candidate) } catch { /* ignore */ }
|
|
346
450
|
}
|
|
347
451
|
|
|
348
452
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
* @param {{ remoteNodeHash: string, connId: string, session: ReturnType<typeof createBufferedSignalSession>, initiator: boolean }} options 建链参数
|
|
352
|
-
* @returns {Promise<Awaited<ReturnType<typeof createLinkImpl>> | null>} 当前规范链(本条可能因 glare 被关,取 links 现值);失败 null
|
|
453
|
+
* 找已注册的 offer/answer provider(按 level 降序的第一个)。
|
|
454
|
+
* @returns {import('../link/providers/index.mjs').LinkProvider | null} 命中的 provider
|
|
353
455
|
*/
|
|
354
|
-
|
|
456
|
+
function findOfferAnswerProvider() {
|
|
457
|
+
return listLinkProviders().find(provider => provider.caps?.needsOfferAnswer) ?? null
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Offer/answer provider:为一条 connId 会话建链并走 glare 择一。
|
|
462
|
+
* @param {{ provider: import('../link/providers/index.mjs').LinkProvider, remoteNodeHash: string, connId: string, session: ReturnType<typeof createBufferedSignalSession>, initiator: boolean }} options 建链参数
|
|
463
|
+
* @returns {Promise<object | null>} 当前规范链;失败 null
|
|
464
|
+
*/
|
|
465
|
+
async function buildConnLink({ provider, remoteNodeHash, connId, session, initiator }) {
|
|
355
466
|
try {
|
|
356
467
|
if (initiator) await trimToBudget()
|
|
357
|
-
const link = await
|
|
468
|
+
const link = await (initiator ? provider.dial : provider.accept)({
|
|
469
|
+
nodeHash: remoteNodeHash,
|
|
470
|
+
signal: session,
|
|
471
|
+
iceServers,
|
|
472
|
+
localIdentity,
|
|
473
|
+
})
|
|
474
|
+
if (!link) {
|
|
475
|
+
signalSessions.delete(connId)
|
|
476
|
+
return null
|
|
477
|
+
}
|
|
358
478
|
link.onDown(() => signalSessions.delete(connId))
|
|
359
479
|
await link.ready
|
|
360
480
|
await registerResolvedLink(remoteNodeHash, link)
|
|
@@ -377,14 +497,17 @@ export function createLinkRegistry(options = {}) {
|
|
|
377
497
|
const remoteNodeHash = normalizeHex64(packet.from)
|
|
378
498
|
const connId = String(packet.connId || '')
|
|
379
499
|
if (!remoteNodeHash || remoteNodeHash === localIdentity.nodeHash || !connId) return
|
|
500
|
+
// 仅 needsOfferAnswer 走双 PC glare;其它 provider 不吃 SDP 信令。
|
|
380
501
|
let session = signalSessions.get(connId)
|
|
381
502
|
if (!session) {
|
|
382
503
|
// 只有全新的 offer 才开一条独立应答 PC;answer/ice 若无对应 connId 会话则是迟到/无效帧,丢弃。
|
|
383
504
|
// 应答 PC 按 connId 独立创建,不受按 nodeHash 去重的 inflight 阻挡——这是支持双向同时建链的关键。
|
|
384
505
|
if (!isOfferSignalBody(packet.body)) return
|
|
506
|
+
const provider = findOfferAnswerProvider()
|
|
507
|
+
if (!provider) return
|
|
385
508
|
session = createConnSession(remoteNodeHash, connId)
|
|
386
509
|
signalSessions.set(connId, session)
|
|
387
|
-
void buildConnLink({ remoteNodeHash, connId, session, initiator: false })
|
|
510
|
+
void buildConnLink({ provider, remoteNodeHash, connId, session, initiator: false })
|
|
388
511
|
}
|
|
389
512
|
session.deliver(packet.body)
|
|
390
513
|
}
|
|
@@ -414,9 +537,41 @@ export function createLinkRegistry(options = {}) {
|
|
|
414
537
|
}
|
|
415
538
|
|
|
416
539
|
/**
|
|
417
|
-
*
|
|
540
|
+
* 经 offer/answer provider 拨号(discovery signal + glare)。
|
|
541
|
+
* @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
|
|
542
|
+
* @param {string} remoteNodeHash 远端 64 hex
|
|
543
|
+
* @returns {Promise<object | null>} 当前规范链;失败 null
|
|
544
|
+
*/
|
|
545
|
+
async function dialOfferAnswer(provider, remoteNodeHash) {
|
|
546
|
+
const connId = randomBytes(16).toString('hex')
|
|
547
|
+
const session = createConnSession(remoteNodeHash, connId)
|
|
548
|
+
signalSessions.set(connId, session)
|
|
549
|
+
return await buildConnLink({ provider, remoteNodeHash, connId, session, initiator: true })
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* 经非 offer/answer provider 拨号。
|
|
554
|
+
* @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
|
|
555
|
+
* @param {string} remoteNodeHash 远端 64 hex
|
|
556
|
+
* @returns {Promise<object | null>} 当前规范链;失败 null
|
|
557
|
+
*/
|
|
558
|
+
async function dialProvider(provider, remoteNodeHash) {
|
|
559
|
+
await trimToBudget()
|
|
560
|
+
const link = await provider.dial({
|
|
561
|
+
nodeHash: remoteNodeHash,
|
|
562
|
+
localIdentity,
|
|
563
|
+
iceServers,
|
|
564
|
+
})
|
|
565
|
+
if (!link) return null
|
|
566
|
+
await link.ready
|
|
567
|
+
await registerResolvedLink(remoteNodeHash, link)
|
|
568
|
+
return links.get(remoteNodeHash) ?? null
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* 按 level 降序尝试各 LinkProvider,不可用/不可达/失败则回落。
|
|
418
573
|
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
419
|
-
* @returns {Promise<
|
|
574
|
+
* @returns {Promise<object | null>} 链路实例;失败时 null
|
|
420
575
|
*/
|
|
421
576
|
async function ensureDirectLinkToNode(remoteNodeHash) {
|
|
422
577
|
await ensureDiscoveryRuntime()
|
|
@@ -424,13 +579,31 @@ export function createLinkRegistry(options = {}) {
|
|
|
424
579
|
if (!normalized || normalized === localIdentity.nodeHash) return null
|
|
425
580
|
if (links.has(normalized)) return links.get(normalized)
|
|
426
581
|
if (inflights.has(normalized)) return await inflights.get(normalized)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
582
|
+
const task = (async () => {
|
|
583
|
+
const providers = await listAvailableLinkProviders()
|
|
584
|
+
for (const provider of providers)
|
|
585
|
+
try {
|
|
586
|
+
if (typeof provider.canReach === 'function') {
|
|
587
|
+
const reachable = await Promise.resolve(provider.canReach({ nodeHash: normalized }))
|
|
588
|
+
if (!reachable) continue
|
|
589
|
+
}
|
|
590
|
+
if (provider.caps?.needsOfferAnswer) {
|
|
591
|
+
// soft-fail 返回 null(不 throw);必须 continue 才能落到更低 level。
|
|
592
|
+
const link = await dialOfferAnswer(provider, normalized)
|
|
593
|
+
if (link) return link
|
|
594
|
+
console.warn(`p2p: link dial failed for ${provider.id}: soft-fail`)
|
|
595
|
+
continue
|
|
596
|
+
}
|
|
597
|
+
const link = await dialProvider(provider, normalized)
|
|
598
|
+
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
|
+
}
|
|
604
|
+
|
|
605
|
+
return null
|
|
606
|
+
})().finally(() => inflights.delete(normalized))
|
|
434
607
|
inflights.set(normalized, task)
|
|
435
608
|
return await task
|
|
436
609
|
}
|
|
@@ -465,7 +638,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
465
638
|
sendToNodeLink: sendDirectToNodeLink,
|
|
466
639
|
/**
|
|
467
640
|
* 列出当前所有活跃链路。
|
|
468
|
-
* @returns {Array<{ nodeHash: string, link:
|
|
641
|
+
* @returns {Array<{ nodeHash: string, link: object }>} 链路列表
|
|
469
642
|
*/
|
|
470
643
|
listLinks() {
|
|
471
644
|
return [...links.entries()].map(([nodeHash, link]) => ({ nodeHash, link }))
|
|
@@ -499,7 +672,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
499
672
|
/**
|
|
500
673
|
* 确保到远端节点的链路(直连或被动应答)。
|
|
501
674
|
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
502
|
-
* @returns {Promise<
|
|
675
|
+
* @returns {Promise<object | null>} 链路实例
|
|
503
676
|
*/
|
|
504
677
|
async function ensureLinkToNode(remoteNodeHash) {
|
|
505
678
|
return await ensureDirectLinkToNode(remoteNodeHash)
|
|
@@ -520,7 +693,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
520
693
|
* 将入站 envelope 派发到 scope 监听器(经 authorizer 校验)。
|
|
521
694
|
* @param {string} senderNodeHash 发送方节点 64 hex
|
|
522
695
|
* @param {{ scope: string, action: string, payload: unknown }} envelope 信封
|
|
523
|
-
* @param {
|
|
696
|
+
* @param {object} link 来源链路
|
|
524
697
|
* @returns {Promise<void>}
|
|
525
698
|
*/
|
|
526
699
|
async function dispatchEnvelope(senderNodeHash, envelope, link) {
|
|
@@ -539,7 +712,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
539
712
|
/**
|
|
540
713
|
* 订阅指定 scope 前缀的 envelope。
|
|
541
714
|
* @param {string} prefix scope 前缀
|
|
542
|
-
* @param {(senderNodeHash: string, envelope: { scope: string, action: string, payload: unknown }, link:
|
|
715
|
+
* @param {(senderNodeHash: string, envelope: { scope: string, action: string, payload: unknown }, link: object) => void | Promise<void>} listener 监听器
|
|
543
716
|
* @returns {() => void} 取消订阅函数
|
|
544
717
|
*/
|
|
545
718
|
function subscribeScope(prefix, listener) {
|
|
@@ -549,7 +722,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
549
722
|
/**
|
|
550
723
|
* 注册 scope 前缀的 authorizer(入站校验)。
|
|
551
724
|
* @param {string} prefix scope 前缀
|
|
552
|
-
* @param {(scope: string, senderNodeHash: string, envelope: object, link:
|
|
725
|
+
* @param {(scope: string, senderNodeHash: string, envelope: object, link: object) => boolean | Promise<boolean>} authorizer 校验函数
|
|
553
726
|
* @returns {() => void} 取消注册函数
|
|
554
727
|
*/
|
|
555
728
|
function registerScopeAuthorizer(prefix, authorizer) {
|
|
@@ -559,19 +732,21 @@ export function createLinkRegistry(options = {}) {
|
|
|
559
732
|
|
|
560
733
|
return {
|
|
561
734
|
localIdentity,
|
|
735
|
+
buildLocalAdvert,
|
|
736
|
+
lanTcpPort: resolveLanTcpPort,
|
|
562
737
|
ensureRuntime: ensureDiscoveryRuntime,
|
|
563
738
|
ensureLinkToNode,
|
|
564
739
|
/**
|
|
565
740
|
* 获取到指定节点的活跃链路。
|
|
566
741
|
* @param {string} nodeHash 节点 64 hex
|
|
567
|
-
* @returns {
|
|
742
|
+
* @returns {object | null} 链路实例;不存在时 null
|
|
568
743
|
*/
|
|
569
744
|
getLink(nodeHash) {
|
|
570
745
|
return links.get(normalizeHex64(nodeHash)) || null
|
|
571
746
|
},
|
|
572
747
|
/**
|
|
573
748
|
* 列出所有活跃链路。
|
|
574
|
-
* @returns {Array<{ nodeHash: string, link:
|
|
749
|
+
* @returns {Array<{ nodeHash: string, link: object }>} 链路列表
|
|
575
750
|
*/
|
|
576
751
|
listLinks() {
|
|
577
752
|
return [...links.entries()].map(([nodeHash, link]) => ({ nodeHash, link }))
|
|
@@ -634,11 +809,12 @@ export function createLinkRegistry(options = {}) {
|
|
|
634
809
|
*/
|
|
635
810
|
async subscribeNodeAdvert(nodeHash, onAdvert) {
|
|
636
811
|
const topic = nodeRendezvousTopic(nodeHash)
|
|
637
|
-
return await subscribeTopic(topic, async bytes => {
|
|
812
|
+
return await subscribeTopic(topic, async (bytes, meta) => {
|
|
638
813
|
const packet = decryptSignalPacket(topic, bytes)
|
|
639
814
|
if (packet?.type !== 'advert') return
|
|
640
815
|
const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
|
|
641
816
|
if (!verifiedNodeHash) return
|
|
817
|
+
noteAdvertPeerHints(verifiedNodeHash, packet.body, meta)
|
|
642
818
|
recentAdverts.touch(verifiedNodeHash, Date.now())
|
|
643
819
|
await Promise.resolve(onAdvert(verifiedNodeHash, packet.body))
|
|
644
820
|
})
|
|
@@ -652,6 +828,8 @@ export function createLinkRegistry(options = {}) {
|
|
|
652
828
|
async shutdown() {
|
|
653
829
|
stopAdvert?.()
|
|
654
830
|
stopSignalListener?.()
|
|
831
|
+
for (const stop of stopLinkListeners.splice(0))
|
|
832
|
+
try { stop() } catch { /* ignore */ }
|
|
655
833
|
overlayRouter?.close()
|
|
656
834
|
overlayRouter = null
|
|
657
835
|
for (const link of links.values())
|
|
@@ -660,6 +838,15 @@ export function createLinkRegistry(options = {}) {
|
|
|
660
838
|
inflights.clear()
|
|
661
839
|
for (const session of signalSessions.values()) session.clear()
|
|
662
840
|
signalSessions.clear()
|
|
841
|
+
if (ownedLanTcp) {
|
|
842
|
+
unregisterLinkProvider(ownedLanTcp.id)
|
|
843
|
+
ownedLanTcp = null
|
|
844
|
+
}
|
|
845
|
+
if (ownedBleGatt) {
|
|
846
|
+
unregisterLinkProvider(ownedBleGatt.id)
|
|
847
|
+
ownedBleGatt = null
|
|
848
|
+
}
|
|
849
|
+
runtimeStarted = false
|
|
663
850
|
},
|
|
664
851
|
}
|
|
665
852
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 带 TTL 的 Map:get 时惰性过期删除。
|
|
3
|
+
* @template T
|
|
4
|
+
* @param {number} ttlMs 存活毫秒
|
|
5
|
+
* @returns {{ ttlMs: number, set: (key: string, value: T) => void, get: (key: string, now?: number) => T | null, clear: () => void }} TTL Map 句柄
|
|
6
|
+
*/
|
|
7
|
+
export function createTtlMap(ttlMs) {
|
|
8
|
+
/** @type {Map<string, { value: T, seenAt: number }>} */
|
|
9
|
+
const map = new Map()
|
|
10
|
+
return {
|
|
11
|
+
ttlMs,
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} key 键
|
|
14
|
+
* @param {T} value 值
|
|
15
|
+
* @returns {void}
|
|
16
|
+
*/
|
|
17
|
+
set(key, value) {
|
|
18
|
+
map.set(key, { value, seenAt: Date.now() })
|
|
19
|
+
},
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} key 键
|
|
22
|
+
* @param {number} [now=Date.now()] 当前时间(测试可注入)
|
|
23
|
+
* @returns {T | null} 未过期值,否则 null
|
|
24
|
+
*/
|
|
25
|
+
get(key, now = Date.now()) {
|
|
26
|
+
const entry = map.get(key)
|
|
27
|
+
if (!entry) return null
|
|
28
|
+
if (now - entry.seenAt > ttlMs) {
|
|
29
|
+
map.delete(key)
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
return entry.value
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* @returns {void}
|
|
36
|
+
*/
|
|
37
|
+
clear() {
|
|
38
|
+
map.clear()
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|