@steve02081504/fount-p2p 0.0.5 → 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.
Files changed (47) hide show
  1. package/core/tcp_port.mjs +11 -0
  2. package/dag/canonicalize_row.mjs +5 -5
  3. package/discovery/advert_peer_hints.mjs +21 -0
  4. package/discovery/{bt.mjs → bt/index.mjs} +151 -55
  5. package/discovery/bt/peer_hints.mjs +41 -0
  6. package/discovery/bt/runtime.mjs +46 -0
  7. package/discovery/lan_peer_hints.mjs +43 -0
  8. package/discovery/mdns.mjs +10 -6
  9. package/discovery/nostr.mjs +3 -3
  10. package/federation/chunk_fetch_pending.mjs +3 -3
  11. package/federation/chunk_fetch_scheduler.mjs +3 -3
  12. package/federation/dag_order_cache.mjs +3 -3
  13. package/federation/topo_order_memo.mjs +3 -3
  14. package/files/chunk_responder.mjs +1 -1
  15. package/files/evfs.mjs +26 -26
  16. package/files/transfer_key.mjs +9 -9
  17. package/files/transfer_key_registry.mjs +9 -9
  18. package/governance/join_pow.mjs +17 -17
  19. package/index.mjs +3 -2
  20. package/link/channel_mux.mjs +7 -7
  21. package/link/frame.mjs +5 -5
  22. package/link/handshake.mjs +63 -36
  23. package/link/pipe.mjs +404 -0
  24. package/link/providers/ble_gatt.mjs +339 -0
  25. package/link/providers/index.mjs +90 -0
  26. package/link/providers/lan_tcp.mjs +342 -0
  27. package/link/providers/levels.mjs +9 -0
  28. package/link/providers/webrtc.mjs +385 -0
  29. package/mailbox/deliver_or_store.mjs +13 -13
  30. package/mailbox/wire.mjs +4 -4
  31. package/node/reputation_store.mjs +5 -5
  32. package/node/retention_policy.mjs +8 -8
  33. package/overlay/index.mjs +6 -6
  34. package/package.json +3 -2
  35. package/registries/inbound.mjs +8 -8
  36. package/rooms/scoped_link.mjs +23 -21
  37. package/timeline/append_core.mjs +3 -3
  38. package/transport/group_link_set.mjs +35 -33
  39. package/transport/link_registry.mjs +250 -63
  40. package/transport/peer_pool.mjs +4 -4
  41. package/transport/user_room.mjs +12 -14
  42. package/utils/ttl_map.mjs +41 -0
  43. package/wire/group_part.mjs +3 -3
  44. package/wire/part_ingress.mjs +14 -14
  45. package/wire/part_query.mjs +81 -82
  46. package/wire/part_query_cache.mjs +7 -0
  47. 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 { createLink } from '../link/link.mjs'
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'
@@ -187,24 +197,24 @@ function subscribeBucket(buckets, key, listener) {
187
197
 
188
198
  /**
189
199
  * 创建 P2P 链路注册表(discovery、信令、直连与 overlay relay)。
190
- * @param {object} [opts] 选项
191
- * @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array }} [opts.localIdentity] 本地身份
192
- * @param {typeof createLink} [opts.createLink] 链路工厂(可注入 mock)
193
- * @param {RTCConfiguration['iceServers']} [opts.iceServers] ICE 服务器列表
194
- * @param {number} [opts.maxActive] 最大并发活跃链路数
195
- * @param {boolean} [opts.autoRegisterDiscoveryProviders] 是否自动注册 discovery provider
196
- * @returns {object} link registry 接口
200
+ * @param {object} [options] 选项
201
+ * @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array }} [options.localIdentity] 本地身份
202
+ * @param {RTCConfiguration['iceServers']} [options.iceServers] ICE 服务器列表(包内 webrtc provider 使用)
203
+ * @param {number} [options.maxActive] 最大并发活跃链路数
204
+ * @param {boolean} [options.autoRegisterDiscoveryProviders] 是否自动注册 discovery provider
205
+ * @param {boolean} [options.autoRegisterLinkProviders] 是否自动注册内置 link provider
206
+ * @returns {object} link registry 接口(对上层即 fount 网络:ensure/send/subscribe,无传输类型)
197
207
  */
198
- export function createLinkRegistry(opts = {}) {
199
- const localIdentity = resolveLocalIdentity(opts.localIdentity)
200
- const createLinkImpl = opts.createLink ?? createLink
201
- const iceServers = opts.iceServers?.length ? opts.iceServers : DEFAULT_ICE_SERVERS
202
- const maxActive = Math.max(4, Number(opts.maxActive) || 32)
203
- const autoRegisterDiscoveryProviders = opts.autoRegisterDiscoveryProviders !== false
208
+ export function createLinkRegistry(options = {}) {
209
+ const localIdentity = resolveLocalIdentity(options.localIdentity)
210
+ const iceServers = options.iceServers?.length ? options.iceServers : DEFAULT_ICE_SERVERS
211
+ const maxActive = Math.max(4, Number(options.maxActive) || 32)
212
+ const autoRegisterDiscoveryProviders = options.autoRegisterDiscoveryProviders !== false
213
+ const autoRegisterLinkProviders = options.autoRegisterLinkProviders !== false
204
214
  const selfTopic = nodeRendezvousTopic(localIdentity.nodeHash)
205
- /** @type {Map<string, Awaited<ReturnType<typeof createLinkImpl>>>} */
215
+ /** @type {Map<string, object>} */
206
216
  const links = new Map()
207
- /** @type {Map<string, Promise<Awaited<ReturnType<typeof createLinkImpl>> | null>>} 按 nodeHash 去重的主动外拨 */
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(opts = {}) {
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
- * 启动 discovery runtimeadvert + signal listener)。
244
+ * 自动注册默认 LinkProviderlan_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(opts = {}) {
248
310
  ?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls),
249
311
  }))
250
312
  }
251
- if (!listDiscoveryProviders().length) return
252
- stopSignalListener = await listenSignals(selfTopic, bytes => {
253
- void handleIncomingSignal(bytes).catch(() => { })
254
- })
255
- stopAdvert = await advertiseTopic(selfTopic, encryptSignalPacket(selfTopic, {
256
- type: 'advert',
257
- body: await buildSignedAdvert(selfTopic, Date.now(), localIdentity),
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(opts = {}) {
290
394
  }
291
395
 
292
396
  /**
293
- * 保留由较小 nodeHash 发起的那条链(两端对任意一条链算出一致结论),用于双 PC glare 择一。
294
- * @param {Awaited<ReturnType<typeof createLinkImpl>>} link 链路实例
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(opts = {}) {
303
411
  /**
304
412
  * 为链路绑定 envelope 派发与 down 回调。
305
413
  * @param {string} remoteNodeHash 远端节点 64 hex
306
- * @param {Awaited<ReturnType<typeof createLinkImpl>>} link 链路实例
414
+ * @param {object} link 链路实例
307
415
  * @returns {void}
308
416
  */
309
417
  function wireLink(remoteNodeHash, link) {
@@ -321,40 +429,52 @@ export function createLinkRegistry(opts = {}) {
321
429
  }
322
430
 
323
431
  /**
324
- * 注册已建立的链路,并做双 PC glare 择一:同一对节点若因双向同时发起而建成两条 PC,
325
- * 保留由较小 nodeHash 发起的那条(两端一致),关闭另一条。单条链路(常态)直接安装。
432
+ * 注册已建立的链路:level 优先,同 level 再按 initiator/nodeHash glare 规则择一。
326
433
  * @param {string} remoteNodeHash 远端节点 64 hex
327
- * @param {Awaited<ReturnType<typeof createLinkImpl>>} candidate 候选链路
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
- if (existing && existing !== candidate && !linkIsPreferred(candidate, normalized)) {
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
- * 为一条 connId 会话建 PC 并走 glare 择一:建 link → 绑 onDown 清 session → ready → registerResolvedLink。
350
- * initiator 侧建链前先 trimToBudget 腾预算。出错则清理该 connId 会话。
351
- * @param {{ remoteNodeHash: string, connId: string, session: ReturnType<typeof createBufferedSignalSession>, initiator: boolean }} opts 建链参数
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
- async function buildConnLink({ remoteNodeHash, connId, session, initiator }) {
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 createLinkImpl({ nodeHash: remoteNodeHash, initiator, signal: session, iceServers, localIdentity })
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(opts = {}) {
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(opts = {}) {
414
537
  }
415
538
 
416
539
  /**
417
- * 主动建链到远端节点(initiator)。
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<Awaited<ReturnType<typeof createLinkImpl>> | null>} 链路实例;失败时 null
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(opts = {}) {
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
- // 想连就直拨(单向零额外成本);若对端也在同时拨,双方各建一条 PC,由 registerResolvedLink 择一。
428
- // buildConnLink 成功时返回的是规范链而非本条 candidate——glare 双 PC 择一时本条可能是被关闭的败者。
429
- const connId = randomBytes(16).toString('hex')
430
- const session = createConnSession(normalized, connId)
431
- signalSessions.set(connId, session)
432
- const task = buildConnLink({ remoteNodeHash: normalized, connId, session, initiator: true })
433
- .finally(() => inflights.delete(normalized))
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(opts = {}) {
465
638
  sendToNodeLink: sendDirectToNodeLink,
466
639
  /**
467
640
  * 列出当前所有活跃链路。
468
- * @returns {Array<{ nodeHash: string, link: Awaited<ReturnType<typeof createLinkImpl>> }>} 链路列表
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(opts = {}) {
499
672
  /**
500
673
  * 确保到远端节点的链路(直连或被动应答)。
501
674
  * @param {string} remoteNodeHash 远端节点 64 hex
502
- * @returns {Promise<Awaited<ReturnType<typeof createLinkImpl>> | null>} 链路实例
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(opts = {}) {
520
693
  * 将入站 envelope 派发到 scope 监听器(经 authorizer 校验)。
521
694
  * @param {string} senderNodeHash 发送方节点 64 hex
522
695
  * @param {{ scope: string, action: string, payload: unknown }} envelope 信封
523
- * @param {Awaited<ReturnType<typeof createLinkImpl>>} link 来源链路
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(opts = {}) {
539
712
  /**
540
713
  * 订阅指定 scope 前缀的 envelope。
541
714
  * @param {string} prefix scope 前缀
542
- * @param {(senderNodeHash: string, envelope: { scope: string, action: string, payload: unknown }, link: Awaited<ReturnType<typeof createLinkImpl>>) => void | Promise<void>} listener 监听器
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(opts = {}) {
549
722
  /**
550
723
  * 注册 scope 前缀的 authorizer(入站校验)。
551
724
  * @param {string} prefix scope 前缀
552
- * @param {(scope: string, senderNodeHash: string, envelope: object, link: Awaited<ReturnType<typeof createLinkImpl>>) => boolean | Promise<boolean>} authorizer 校验函数
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(opts = {}) {
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 {Awaited<ReturnType<typeof createLinkImpl>> | null} 链路实例;不存在时 null
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: Awaited<ReturnType<typeof createLinkImpl>> }>} 链路列表
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(opts = {}) {
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(opts = {}) {
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(opts = {}) {
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
  }
@@ -141,7 +141,7 @@ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxP
141
141
  * selfNodeHash: string,
142
142
  * inRoomNodeHashes?: Set<string> | string[] 群内在线 node_id;有则优先,仅全不可达时用 explore 中非房内节点
143
143
  * hintSources?: Map<string, string> explore 节点来源(用于配额)
144
- * }} opts 选取参数(roster、peers、rep、limits、selfNodeHash)
144
+ * }} options 选取参数(roster、peers、rep、limits、selfNodeHash)
145
145
  * @returns {string[]} 目标 Trystero peerId 列表(去重,长度 ≤ maxPeers)
146
146
  */
147
147
  export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash, inRoomNodeHashes, hintSources }) {
@@ -212,7 +212,7 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
212
212
  * peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[], hintSources?: Map<string, string> },
213
213
  * limits: ReturnType<typeof resolveFederationPoolLimits>,
214
214
  * anchors?: Iterable<string>,
215
- * }} opts 选取参数(members、selfNodeHash、rep、peers、limits、anchors)
215
+ * }} options 选取参数(members、selfNodeHash、rep、peers、limits、anchors)
216
216
  * @returns {string[]} 应建链的 nodeHash 列表(去重)
217
217
  */
218
218
  export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers, limits, anchors = [] }) {
@@ -245,7 +245,7 @@ export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers
245
245
  * rep: { byNodeHash?: Record<string, { score?: number }> },
246
246
  * hints: string[],
247
247
  * limits: ReturnType<typeof resolveFederationPoolLimits>,
248
- * }} opts 合并参数(peers、rep、hints、limits)
248
+ * }} options 合并参数(peers、rep、hints、limits)
249
249
  * @returns {{ trustedPeers: string[], explorePeers: string[] }} 更新后的 trusted/explore 列表
250
250
  */
251
251
  export function applyPexHints({ peers, rep, hints, limits }) {
@@ -275,7 +275,7 @@ export function applyPexHints({ peers, rep, hints, limits }) {
275
275
  * rep: { byNodeHash?: Record<string, { score?: number }> },
276
276
  * roster: Array<{ remoteNodeHash?: string }>,
277
277
  * limits: ReturnType<typeof resolveFederationPoolLimits>,
278
- * }} opts roster 更新参数(peers、rep、roster、limits)
278
+ * }} options roster 更新参数(peers、rep、roster、limits)
279
279
  * @returns {{ trustedPeers: string[], explorePeers: string[] }} 更新后的 trusted/explore 列表
280
280
  */
281
281
  export function applyRosterToPeerPool({ peers, rep, roster, limits }) {
@@ -91,12 +91,12 @@ function activeLinkRoster() {
91
91
 
92
92
  /**
93
93
  * 初始化 node scope 订阅与 wire 派发(幂等)。
94
- * @param {{ replicaUsername?: string }} ctx 入站上下文
94
+ * @param {{ replicaUsername?: string }} wireContext 入站上下文
95
95
  * @returns {Promise<void>}
96
96
  */
97
- async function ensureNodeScopeRuntime(ctx) {
97
+ async function ensureNodeScopeRuntime(wireContext) {
98
98
  if (nodeScopeCleanup) return
99
- nodeScopeReplicaUsername = String(ctx.replicaUsername || nodeScopeReplicaUsername || '')
99
+ nodeScopeReplicaUsername = String(wireContext.replicaUsername || nodeScopeReplicaUsername || '')
100
100
  nodeScopeCleanup = subscribeScope('node', (senderNodeHash, envelope) => {
101
101
  const handlers = nodeActionHandlers.get(String(envelope?.action || ''))
102
102
  if (!handlers?.size) return
@@ -105,12 +105,12 @@ async function ensureNodeScopeRuntime(ctx) {
105
105
  })
106
106
  const wire = createNodeScopeWire()
107
107
  nodeScopeWire = wire
108
- attachPartWire({ replicaUsername: ctx.replicaUsername }, wire)
109
- attachPartQueryWire({ replicaUsername: ctx.replicaUsername }, wire)
110
- attachMailboxWire({ replicaUsername: ctx.replicaUsername }, wire)
111
- attachUserRoomChunkHandlers(ctx.replicaUsername || '', wire)
108
+ attachPartWire({ replicaUsername: wireContext.replicaUsername }, wire)
109
+ attachPartQueryWire({ replicaUsername: wireContext.replicaUsername }, wire)
110
+ attachMailboxWire({ replicaUsername: wireContext.replicaUsername }, wire)
111
+ attachUserRoomChunkHandlers(wireContext.replicaUsername || '', wire)
112
112
  for (const hook of nodeScopeWireHooks)
113
- try { hook(ctx.replicaUsername || '', wire) } catch { /* ignore */ }
113
+ try { hook(wireContext.replicaUsername || '', wire) } catch { /* ignore */ }
114
114
  }
115
115
 
116
116
  /**
@@ -162,10 +162,10 @@ export function resolveUserRoomCredentials() {
162
162
  }
163
163
 
164
164
  /**
165
- * @param {{ replicaUsername?: string }} [ctx] 入站上下文(part/mailbox 派发用)
165
+ * @param {{ replicaUsername?: string }} [wireContext] 入站上下文(part/mailbox 派发用)
166
166
  * @returns {Promise<UserRoomSlot | null>} 用户级联邦房间槽
167
167
  */
168
- export async function ensureUserRoom(ctx = {}) {
168
+ export async function ensureUserRoom(wireContext = {}) {
169
169
  if (userRoomSlot) return userRoomSlot
170
170
  if (userRoomInflight) return await userRoomInflight
171
171
 
@@ -173,7 +173,7 @@ export async function ensureUserRoom(ctx = {}) {
173
173
  ensureNodeDefaults()
174
174
  try {
175
175
  await getLinkRegistry().ensureRuntime()
176
- await ensureNodeScopeRuntime(ctx)
176
+ await ensureNodeScopeRuntime(wireContext)
177
177
  const creds = resolveUserRoomCredentials()
178
178
  /** @type {UserRoomSlot} */
179
179
  userRoomSlot = {
@@ -238,9 +238,7 @@ export async function deliverToUserRoomPeers(username, actionName, payload, exce
238
238
  .filter(({ peerId }) => peerId && peerId !== exceptPeerId)]
239
239
  for (let swapIndex = peers.length - 1; swapIndex > 0; swapIndex--) {
240
240
  const pickIndex = Math.floor(Math.random() * (swapIndex + 1))
241
- const tmp = peers[swapIndex]
242
- peers[swapIndex] = peers[pickIndex]
243
- peers[pickIndex] = tmp
241
+ ;[peers[swapIndex], peers[pickIndex]] = [peers[pickIndex], peers[swapIndex]]
244
242
  }
245
243
  for (const { peerId } of peers)
246
244
  try {
@@ -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
+ }