@steve02081504/fount-p2p 0.0.11 → 0.0.12

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 (57) hide show
  1. package/README.md +1 -1
  2. package/core/bytes_codec.mjs +65 -10
  3. package/core/tcp_port.mjs +1 -1
  4. package/crypto/checkpoint_sign.mjs +2 -2
  5. package/dag/canonicalize_row.mjs +3 -3
  6. package/dag/index.mjs +2 -3
  7. package/discovery/bt/index.mjs +5 -5
  8. package/discovery/bt/runtime.mjs +3 -3
  9. package/discovery/mdns.mjs +7 -1
  10. package/discovery/nostr.mjs +53 -99
  11. package/federation/chunk_fetch_pending.mjs +4 -5
  12. package/federation/dag_order_cache.mjs +1 -1
  13. package/federation/entity_key_chain.mjs +3 -4
  14. package/federation/topo_order_memo.mjs +11 -4
  15. package/files/chunk_fetch.mjs +2 -2
  16. package/files/evfs.mjs +2 -2
  17. package/link/channel_mux.mjs +10 -10
  18. package/link/frame.mjs +76 -124
  19. package/link/handshake.mjs +5 -5
  20. package/link/pipe.mjs +49 -75
  21. package/link/providers/ble_gatt.mjs +14 -26
  22. package/link/providers/lan_tcp.mjs +17 -32
  23. package/link/providers/link_id_pipe.mjs +46 -0
  24. package/link/providers/webrtc.mjs +42 -51
  25. package/link/rtc.mjs +21 -9
  26. package/mailbox/deliver_or_store.mjs +1 -1
  27. package/node/identity.mjs +4 -4
  28. package/node/network.mjs +9 -9
  29. package/overlay/index.mjs +13 -13
  30. package/package.json +1 -1
  31. package/rooms/scoped_link.mjs +19 -35
  32. package/schemas/discovery.mjs +1 -2
  33. package/schemas/federation_pull.mjs +2 -2
  34. package/schemas/part_query.mjs +1 -1
  35. package/schemas/remote_event.mjs +1 -1
  36. package/transport/advert_ingest.mjs +20 -0
  37. package/transport/group_link_set.mjs +22 -41
  38. package/transport/ice_servers.mjs +2 -2
  39. package/transport/link_registry.mjs +113 -105
  40. package/transport/offer_answer.mjs +6 -2
  41. package/transport/peer_pool.mjs +53 -64
  42. package/transport/remote_user_room.mjs +13 -15
  43. package/transport/rtc_connection_budget.mjs +18 -4
  44. package/transport/runtime_bootstrap.mjs +8 -2
  45. package/transport/signal_crypto.mjs +25 -3
  46. package/transport/user_room.mjs +22 -15
  47. package/trust_graph/send.mjs +1 -1
  48. package/utils/emit_safe.mjs +11 -0
  49. package/utils/shuffle.mjs +13 -0
  50. package/utils/ttl_map.mjs +29 -4
  51. package/wire/ingress.mjs +1 -1
  52. package/wire/part_ingress.mjs +6 -8
  53. package/wire/part_invoke.mjs +4 -4
  54. package/wire/part_query.mjs +2 -3
  55. package/wire/part_query.tunables.json +6 -1
  56. package/wire/part_query_cache.mjs +1 -1
  57. package/wire/volatile_signature.mjs +1 -1
@@ -2,21 +2,18 @@ import { Buffer } from 'node:buffer'
2
2
 
3
3
  import { compareHex64Asc, normalizeHex64 } from '../core/hexIds.mjs'
4
4
  import { keyPairFromSeed } from '../crypto/crypto.mjs'
5
- import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
6
5
  import { subscribeTopic } from '../discovery/index.mjs'
7
- import { verifySignedAdvert } from '../link/handshake.mjs'
8
6
  import { listLinkProviders } from '../link/providers/index.mjs'
9
7
  import { ensureNodeSeed, getNodeHash } from '../node/identity.mjs'
10
8
  import { createOverlayRouter } from '../overlay/index.mjs'
9
+ import { emitSafe } from '../utils/emit_safe.mjs'
11
10
  import { createLruMap } from '../utils/lru.mjs'
12
11
 
12
+ import { ingestSignedAdvert } from './advert_ingest.mjs'
13
13
  import { DEFAULT_ICE_SERVERS } from './ice_servers.mjs'
14
14
  import { createOfferAnswerDial } from './offer_answer.mjs'
15
15
  import { createRuntimeBootstrap } from './runtime_bootstrap.mjs'
16
- import {
17
- decryptSignalPacket,
18
- nodeRendezvousTopic,
19
- } from './signal_crypto.mjs'
16
+ import { nodeRendezvousTopic } from './signal_crypto.mjs'
20
17
 
21
18
  /**
22
19
  * 解析或从节点种子推导本地身份。
@@ -104,8 +101,8 @@ export function createLinkRegistry(options = {}) {
104
101
  * @returns {boolean} 候选链应保留为规范链时 true
105
102
  */
106
103
  function linkIsPreferred(link, remoteNodeHash, against = null) {
107
- const level = Number(link.level) || 0
108
- const againstLevel = Number(against?.level) || 0
104
+ const level = link.level || 0
105
+ const againstLevel = against?.level || 0
109
106
  if (against && level !== againstLevel) return level > againstLevel
110
107
  const cmp = compareHex64Asc(localIdentity.nodeHash, remoteNodeHash)
111
108
  return link.initiator ? cmp < 0 : cmp > 0
@@ -125,8 +122,7 @@ export function createLinkRegistry(options = {}) {
125
122
  const wasCanonical = links.get(remoteNodeHash) === link
126
123
  if (!wasCanonical) return
127
124
  links.delete(remoteNodeHash)
128
- for (const listener of linkDownListeners)
129
- try { listener(remoteNodeHash, reason) } catch { /* ignore */ }
125
+ emitSafe(linkDownListeners, remoteNodeHash, reason)
130
126
  })
131
127
  }
132
128
 
@@ -140,15 +136,14 @@ export function createLinkRegistry(options = {}) {
140
136
  const normalized = normalizeHex64(remoteNodeHash)
141
137
  const existing = links.get(normalized)
142
138
  if (existing && existing !== candidate && !linkIsPreferred(candidate, normalized, existing)) {
143
- await candidate.close(Number(candidate.level) !== Number(existing.level) ? 'provider-loser' : 'glare-loser')
139
+ await candidate.close(candidate.level !== existing.level ? 'provider-loser' : 'glare-loser')
144
140
  return
145
141
  }
146
142
  links.set(normalized, candidate)
147
143
  wireLink(normalized, candidate)
148
144
  if (existing && existing !== candidate)
149
- await existing.close(Number(candidate.level) !== Number(existing.level) ? 'provider-replaced' : 'glare-replaced')
150
- for (const listener of linkUpListeners)
151
- try { listener(normalized, candidate) } catch { /* ignore */ }
145
+ await existing.close(candidate.level !== existing.level ? 'provider-replaced' : 'glare-replaced')
146
+ emitSafe(linkUpListeners, normalized, candidate)
152
147
  }
153
148
 
154
149
  /**
@@ -177,6 +172,10 @@ export function createLinkRegistry(options = {}) {
177
172
  autoRegisterDiscoveryProviders: options.autoRegisterDiscoveryProviders !== false,
178
173
  autoRegisterLinkProviders: options.autoRegisterLinkProviders !== false,
179
174
  onInboundLink,
175
+ /**
176
+ * @param {Uint8Array} bytes 入站信令字节
177
+ * @returns {void}
178
+ */
180
179
  handleIncomingSignal: bytes => handleIncomingSignal(bytes),
181
180
  })
182
181
 
@@ -198,13 +197,25 @@ export function createLinkRegistry(options = {}) {
198
197
  */
199
198
  async function trimToBudget() {
200
199
  if (links.size < maxActive) return
201
- const candidates = [...links.entries()]
202
- .sort((left, right) => scopeWeight(left[0]) - scopeWeight(right[0]) || compareHex64Asc(left[0], right[0]))
203
- const victim = candidates[0]
204
- if (victim) await victim[1].close('budget-evict')
200
+ let victimHash = null
201
+ let victimLink = null
202
+ let victimWeight = Infinity
203
+ for (const [nodeHash, link] of links) {
204
+ const weight = scopeWeight(nodeHash)
205
+ if (
206
+ victimHash == null
207
+ || weight < victimWeight
208
+ || (weight === victimWeight && compareHex64Asc(nodeHash, victimHash) < 0)
209
+ ) {
210
+ victimHash = nodeHash
211
+ victimLink = link
212
+ victimWeight = weight
213
+ }
214
+ }
215
+ if (victimLink) await victimLink.close('budget-evict')
205
216
  }
206
217
 
207
- ;({ handleIncomingSignal, dialOfferAnswer } = createOfferAnswerDial({
218
+ ; ({ handleIncomingSignal, dialOfferAnswer } = createOfferAnswerDial({
208
219
  localIdentity,
209
220
  iceServers,
210
221
  selfTopic,
@@ -327,8 +338,9 @@ export function createLinkRegistry(options = {}) {
327
338
  async function relayEnvelopeToNode(remoteNodeHash, envelope) {
328
339
  if (!links.size || envelope?.scope === 'overlay') return false
329
340
  try {
330
- const path = await getOverlayRouter().discoverRoute(remoteNodeHash)
331
- await getOverlayRouter().relay(path, envelope)
341
+ const overlay = getOverlayRouter()
342
+ const path = await overlay.discoverRoute(remoteNodeHash)
343
+ await overlay.relay(path, envelope)
332
344
  return true
333
345
  }
334
346
  catch {
@@ -347,6 +359,17 @@ export function createLinkRegistry(options = {}) {
347
359
  || await relayEnvelopeToNode(remoteNodeHash, envelope)
348
360
  }
349
361
 
362
+ /**
363
+ * 同步结果直接返回;thenable 才 await(避免每条 envelope 造 microtask)。
364
+ * @param {unknown} value 可能为 Promise 的返回值
365
+ * @returns {Promise<unknown>} 已 resolve 的值
366
+ */
367
+ async function maybeAwait(value) {
368
+ if (value != null && typeof /** @type {{ then?: unknown }} */ value.then === 'function')
369
+ return await value
370
+ return value
371
+ }
372
+
350
373
  /**
351
374
  * 将入站 envelope 派发到 scope 监听器(经 authorizer 校验)。
352
375
  * @param {string} senderNodeHash 发送方节点 64 hex
@@ -355,16 +378,16 @@ export function createLinkRegistry(options = {}) {
355
378
  * @returns {Promise<void>}
356
379
  */
357
380
  async function dispatchEnvelope(senderNodeHash, envelope, link) {
358
- const scope = String(envelope?.scope || '')
381
+ const scope = envelope.scope || ''
359
382
  for (const [prefix, authorizer] of scopeAuthorizers.entries())
360
383
  if (scope.startsWith(prefix)) {
361
- const allowed = await Promise.resolve(authorizer(scope, senderNodeHash, envelope, link))
384
+ const allowed = await maybeAwait(authorizer(scope, senderNodeHash, envelope, link))
362
385
  if (!allowed) return
363
386
  }
364
387
  for (const [prefix, listeners] of scopeListeners.entries())
365
388
  if (scope.startsWith(prefix))
366
389
  for (const listener of listeners)
367
- await Promise.resolve(listener(senderNodeHash, envelope, link))
390
+ await maybeAwait(listener(senderNodeHash, envelope, link))
368
391
  }
369
392
 
370
393
  /**
@@ -374,7 +397,7 @@ export function createLinkRegistry(options = {}) {
374
397
  * @returns {() => void} 取消订阅函数
375
398
  */
376
399
  function subscribeScope(prefix, listener) {
377
- return subscribeBucket(scopeListeners, String(prefix), listener)
400
+ return subscribeBucket(scopeListeners, prefix, listener)
378
401
  }
379
402
 
380
403
  /**
@@ -384,8 +407,8 @@ export function createLinkRegistry(options = {}) {
384
407
  * @returns {() => void} 取消注册函数
385
408
  */
386
409
  function registerScopeAuthorizer(prefix, authorizer) {
387
- scopeAuthorizers.set(String(prefix), authorizer)
388
- return () => scopeAuthorizers.delete(String(prefix))
410
+ scopeAuthorizers.set(prefix, authorizer)
411
+ return () => scopeAuthorizers.delete(prefix)
389
412
  }
390
413
 
391
414
  return {
@@ -448,7 +471,7 @@ export function createLinkRegistry(options = {}) {
448
471
  * @returns {void}
449
472
  */
450
473
  registerScopeInterest(scope, nodeHashes) {
451
- scopeInterests.set(String(scope), new Set((Array.isArray(nodeHashes) ? nodeHashes : []).map(normalizeHex64).filter(Boolean)))
474
+ scopeInterests.set(scope, new Set((nodeHashes || []).map(normalizeHex64).filter(Boolean)))
452
475
  },
453
476
  /**
454
477
  * 释放 scope 兴趣。
@@ -456,7 +479,7 @@ export function createLinkRegistry(options = {}) {
456
479
  * @returns {void}
457
480
  */
458
481
  releaseScopeInterest(scope) {
459
- scopeInterests.delete(String(scope))
482
+ scopeInterests.delete(scope)
460
483
  },
461
484
  registerScopeAuthorizer,
462
485
  subscribeScope,
@@ -469,13 +492,10 @@ export function createLinkRegistry(options = {}) {
469
492
  async subscribeNodeAdvert(nodeHash, onAdvert) {
470
493
  const topic = nodeRendezvousTopic(nodeHash)
471
494
  return await subscribeTopic(topic, async (bytes, meta) => {
472
- const packet = decryptSignalPacket(topic, bytes)
473
- if (packet?.type !== 'advert') return
474
- const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
475
- if (!verifiedNodeHash) return
476
- noteAdvertPeerHints(verifiedNodeHash, packet.body, meta)
477
- recentAdverts.touch(verifiedNodeHash, Date.now())
478
- await Promise.resolve(onAdvert(verifiedNodeHash, packet.body))
495
+ const ingested = await ingestSignedAdvert(topic, bytes, meta)
496
+ if (!ingested) return
497
+ recentAdverts.touch(ingested.verifiedNodeHash, Date.now())
498
+ await Promise.resolve(onAdvert(ingested.verifiedNodeHash, ingested.body))
479
499
  })
480
500
  },
481
501
  recentAdverts,
@@ -500,83 +520,71 @@ export function createLinkRegistry(options = {}) {
500
520
 
501
521
  let defaultRegistry = null
502
522
 
523
+ /**
524
+ * 默认 registry 尚未创建时暂存的 scope authorizer 条目。
525
+ * 注册阶段不应触发 createLinkRegistry / resolveLocalIdentity;绑定在 getLinkRegistry flush 时完成。
526
+ * @type {Array<{ prefix: string, authorizer: Function, unregister: (() => void) | null }>}
527
+ */
528
+ const pendingScopeAuthorizers = []
529
+
503
530
  /**
504
531
  * 获取进程级默认 link registry 单例。
505
532
  * @returns {ReturnType<typeof createLinkRegistry>} 默认 registry
506
533
  */
507
534
  export function getLinkRegistry() {
508
- return defaultRegistry ||= createLinkRegistry()
535
+ if (defaultRegistry) return defaultRegistry
536
+ defaultRegistry = createLinkRegistry()
537
+ for (const entry of pendingScopeAuthorizers)
538
+ entry.unregister = defaultRegistry.registerScopeAuthorizer(entry.prefix, entry.authorizer)
539
+ pendingScopeAuthorizers.length = 0
540
+ return defaultRegistry
509
541
  }
510
542
 
511
543
  /**
512
- * 默认 registry 的 ensureLinkToNode 代理。
513
- * @param {...any} args 转发参数
514
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['ensureLinkToNode']>} 链路实例
515
- */
516
- export const ensureLinkToNode = (...args) => getLinkRegistry().ensureLinkToNode(...args)
517
- /**
518
- * 默认 registry 的 getLink 代理。
519
- * @param {...any} args 转发参数
520
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['getLink']>} 链路实例
521
- */
522
- export const getLink = (...args) => getLinkRegistry().getLink(...args)
523
- /**
524
- * 默认 registry 的 listLinks 代理。
525
- * @param {...any} args 转发参数
526
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['listLinks']>} 链路列表
527
- */
528
- export const listLinks = (...args) => getLinkRegistry().listLinks(...args)
529
- /**
530
- * 默认 registry 的 closeLink 代理。
531
- * @param {...any} args 转发参数
532
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['closeLink']>} 关闭完成
533
- */
534
- export const closeLink = (...args) => getLinkRegistry().closeLink(...args)
535
- /**
536
- * 默认 registry 的 sendToNodeLink 代理。
537
- * @param {...any} args 转发参数
538
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['sendToNodeLink']>} 是否成功
539
- */
540
- export const sendToNodeLink = (...args) => getLinkRegistry().sendToNodeLink(...args)
541
- /**
542
- * 默认 registry 的 relayEnvelopeToNode 代理。
543
- * @param {...any} args 转发参数
544
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['relayEnvelopeToNode']>} 是否成功
545
- */
546
- export const relayEnvelopeToNode = (...args) => getLinkRegistry().relayEnvelopeToNode(...args)
547
- /**
548
- * 默认 registry 的 onLinkUp 代理。
549
- * @param {...any} args 转发参数
550
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['onLinkUp']>} 取消订阅函数
544
+ * 默认 registry 方法代理。
545
+ * @param {string} name registry 方法名
546
+ * @returns {(...args: unknown[]) => unknown} 绑定到 getLinkRegistry()[name] 的函数
551
547
  */
552
- export const onLinkUp = (...args) => getLinkRegistry().onLinkUp(...args)
553
- /**
554
- * 默认 registry onLinkDown 代理。
555
- * @param {...any} args 转发参数
556
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['onLinkDown']>} 取消订阅函数
557
- */
558
- export const onLinkDown = (...args) => getLinkRegistry().onLinkDown(...args)
559
- /**
560
- * 默认 registry registerScopeInterest 代理。
561
- * @param {...any} args 转发参数
562
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['registerScopeInterest']>} 无返回值
563
- */
564
- export const registerScopeInterest = (...args) => getLinkRegistry().registerScopeInterest(...args)
565
- /**
566
- * 默认 registry releaseScopeInterest 代理。
567
- * @param {...any} args 转发参数
568
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['releaseScopeInterest']>} 无返回值
569
- */
570
- export const releaseScopeInterest = (...args) => getLinkRegistry().releaseScopeInterest(...args)
571
- /**
572
- * 默认 registry registerScopeAuthorizer 代理。
573
- * @param {...any} args 转发参数
574
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['registerScopeAuthorizer']>} 取消注册函数
575
- */
576
- export const registerScopeAuthorizer = (...args) => getLinkRegistry().registerScopeAuthorizer(...args)
548
+ const bindRegistryMethod = name => (...args) => getLinkRegistry()[name](...args)
549
+
550
+ /** @type {(...args: unknown[]) => unknown} */
551
+ export const ensureLinkToNode = bindRegistryMethod('ensureLinkToNode')
552
+ /** @type {(...args: unknown[]) => unknown} */
553
+ export const getLink = bindRegistryMethod('getLink')
554
+ /** @type {(...args: unknown[]) => unknown} */
555
+ export const listLinks = bindRegistryMethod('listLinks')
556
+ /** @type {(...args: unknown[]) => unknown} */
557
+ export const closeLink = bindRegistryMethod('closeLink')
558
+ /** @type {(...args: unknown[]) => unknown} */
559
+ export const sendToNodeLink = bindRegistryMethod('sendToNodeLink')
560
+ /** @type {(...args: unknown[]) => unknown} */
561
+ export const relayEnvelopeToNode = bindRegistryMethod('relayEnvelopeToNode')
562
+ /** @type {(...args: unknown[]) => unknown} */
563
+ export const onLinkUp = bindRegistryMethod('onLinkUp')
564
+ /** @type {(...args: unknown[]) => unknown} */
565
+ export const onLinkDown = bindRegistryMethod('onLinkDown')
566
+ /** @type {(...args: unknown[]) => unknown} */
567
+ export const registerScopeInterest = bindRegistryMethod('registerScopeInterest')
568
+ /** @type {(...args: unknown[]) => unknown} */
569
+ export const releaseScopeInterest = bindRegistryMethod('releaseScopeInterest')
570
+ /** @type {(...args: unknown[]) => unknown} */
571
+ export const subscribeScope = bindRegistryMethod('subscribeScope')
572
+
577
573
  /**
578
- * 默认 registry 的 subscribeScope 代理。
579
- * @param {...any} args 转发参数
580
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['subscribeScope']>} 取消订阅函数
574
+ * 注册默认 registry 的 scope authorizer。
575
+ * 不急切创建 registry(不必 resolveLocalIdentity);首次 getLinkRegistry 时 flush。
576
+ * @param {string} prefix scope 前缀
577
+ * @param {Function} authorizer 校验函数
578
+ * @returns {() => void} 取消注册函数
581
579
  */
582
- export const subscribeScope = (...args) => getLinkRegistry().subscribeScope(...args)
580
+ export function registerScopeAuthorizer(prefix, authorizer) {
581
+ if (defaultRegistry) return defaultRegistry.registerScopeAuthorizer(prefix, authorizer)
582
+
583
+ const entry = { prefix, authorizer, unregister: null }
584
+ pendingScopeAuthorizers.push(entry)
585
+ return () => {
586
+ const index = pendingScopeAuthorizers.indexOf(entry)
587
+ if (index !== -1) pendingScopeAuthorizers.splice(index, 1)
588
+ entry.unregister?.()
589
+ }
590
+ }
@@ -6,6 +6,9 @@ import { listLinkProviders } from '../link/providers/index.mjs'
6
6
 
7
7
  import { decryptSignalPacket, encryptSignalPacket, nodeRendezvousTopic } from './signal_crypto.mjs'
8
8
 
9
+ /** accept/dial 挂起期间 ICE 信令 backlog 上限,防无 handler 时无限堆积 */
10
+ const SIGNAL_BACKLOG_MAX = 64
11
+
9
12
  /**
10
13
  * 创建带 backlog 的缓冲信令会话。
11
14
  * @param {(message: unknown) => Promise<void>} sendRemote 远端发送回调
@@ -37,12 +40,13 @@ export function createBufferedSignalSession(sendRemote) {
37
40
  return () => handlers.delete(handler)
38
41
  },
39
42
  /**
40
- * 投递信令消息;无 handler 时入 backlog
43
+ * 投递信令消息;无 handler 时入 backlog(有界)。
41
44
  * @param {unknown} message 信令消息
42
45
  * @returns {void}
43
46
  */
44
47
  deliver(message) {
45
48
  if (!handlers.size) {
49
+ if (backlog.length >= SIGNAL_BACKLOG_MAX) backlog.shift()
46
50
  backlog.push(message)
47
51
  return
48
52
  }
@@ -157,7 +161,7 @@ export function createOfferAnswerDial(deps) {
157
161
  */
158
162
  async function handleIncomingSignal(bytes) {
159
163
  const packet = decryptSignalPacket(selfTopic, bytes)
160
- if (!packet || packet.type !== 'signal') return
164
+ if (packet?.type !== 'signal') return
161
165
  const remoteNodeHash = normalizeHex64(packet.from)
162
166
  const connId = String(packet.connId || '')
163
167
  if (!remoteNodeHash || remoteNodeHash === localIdentity.nodeHash || !connId) return
@@ -8,6 +8,7 @@ import { loadPeerPoolView, mergeNetworkPeerPools } from '../node/network.mjs'
8
8
  import { loadReputation } from '../node/reputation_store.mjs'
9
9
  import { isQuarantinedPure } from '../reputation/engine.mjs'
10
10
  import { clampReputationScore } from '../reputation/math.mjs'
11
+ import { shuffleInPlace } from '../utils/shuffle.mjs'
11
12
 
12
13
  /**
13
14
  * 解析联邦池槽位参数(从 groupSettings 读取,含低功耗缩减)。
@@ -91,14 +92,8 @@ export function mergeTrustedWithAnchors(existingTrusted, rankedCandidates, limit
91
92
  */
92
93
  export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxPerSource = EXPLORE_MAX_PER_SOURCE) {
93
94
  if (k <= 0 || !exploreIds.length) return []
94
- if (!exploreSources?.size) {
95
- const copy = [...exploreIds]
96
- for (let i = copy.length - 1; i > 0; i--) {
97
- const j = Math.floor(Math.random() * (i + 1))
98
- ;[copy[i], copy[j]] = [copy[j], copy[i]]
99
- }
100
- return copy.slice(0, k)
101
- }
95
+ if (!exploreSources?.size)
96
+ return shuffleInPlace([...exploreIds]).slice(0, k)
102
97
  /** @type {Map<string, string[]>} */
103
98
  const bySource = new Map()
104
99
  for (const id of exploreIds) {
@@ -106,11 +101,7 @@ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxP
106
101
  if (!bySource.has(src)) bySource.set(src, [])
107
102
  bySource.get(src).push(id)
108
103
  }
109
- for (const ids of bySource.values())
110
- for (let i = ids.length - 1; i > 0; i--) {
111
- const j = Math.floor(Math.random() * (i + 1))
112
- ;[ids[i], ids[j]] = [ids[j], ids[i]]
113
- }
104
+ for (const ids of bySource.values()) shuffleInPlace(ids)
114
105
  const out = []
115
106
  /** @type {Map<string, number>} */
116
107
  const picked = new Map()
@@ -148,7 +139,7 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
148
139
  const blocked = new Set(peers.blockedPeers)
149
140
  const roomSet = inRoomNodeHashes instanceof Set
150
141
  ? inRoomNodeHashes
151
- : new Set(Array.isArray(inRoomNodeHashes) ? inRoomNodeHashes : [])
142
+ : new Set(inRoomNodeHashes || [])
152
143
  const onlineAll = roster.filter(
153
144
  rosterEntry => rosterEntry.peerId
154
145
  && rosterEntry.remoteNodeHash
@@ -175,10 +166,11 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
175
166
  }
176
167
 
177
168
  const anchoredTrusted = peers.trustedPeers.filter(nodeHash => trustedSet.has(nodeHash))
178
- const extraTrusted = [...trustedSet]
179
- .filter(nodeHash => !anchoredTrusted.includes(nodeHash))
180
- .sort((a, b) => repScore(b, rep) - repScore(a, rep))
181
- for (const nodeId of [...anchoredTrusted, ...extraTrusted].slice(0, limits.trustedSlots)) {
169
+ for (const nodeId of mergeTrustedWithAnchors(
170
+ anchoredTrusted,
171
+ [...trustedSet].sort((a, b) => repScore(b, rep) - repScore(a, rep)),
172
+ limits,
173
+ )) {
182
174
  if (outPeerIds.size >= limits.maxPeers) break
183
175
  pushNode(nodeId)
184
176
  }
@@ -216,15 +208,14 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
216
208
  * @returns {string[]} 应建链的 nodeHash 列表(去重)
217
209
  */
218
210
  export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers, limits, anchors = [] }) {
219
- const self = String(selfNodeHash || '')
220
211
  const blocked = new Set(peers?.blockedPeers || [])
221
212
  const now = Date.now()
222
- const candidates = [...new Set([...members].map(id => String(id)))]
223
- .filter(id => id && id !== self && !blocked.has(id) && !isQuarantinedPure(rep, id, now))
213
+ const candidates = [...new Set(members)]
214
+ .filter(id => id && id !== selfNodeHash && !blocked.has(id) && !isQuarantinedPure(rep, id, now))
224
215
  const ranked = candidates.slice().sort((a, b) => repScore(b, rep) - repScore(a, rep))
225
216
  const candidateSet = new Set(candidates)
226
217
  // 锚点(如 introducer/creator/seed)必连、且不占 trustedSlots——保证引导期连通。
227
- const forced = [...new Set([...anchors].map(id => String(id)))].filter(id => candidateSet.has(id))
218
+ const forced = [...new Set(anchors)].filter(id => candidateSet.has(id))
228
219
  const chosen = new Set(forced)
229
220
  // trusted 槽只从非锚点候选填:既有 trusted 优先保留,再按信誉补至 trustedSlots。
230
221
  const nonForced = ranked.filter(id => !chosen.has(id))
@@ -237,29 +228,24 @@ export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers
237
228
  }
238
229
 
239
230
  /**
240
- * 将新 PEX 节点线索合并进 explore 池,并按信誉重新填充 trusted 槽。
241
- * 纯计算版本,不含 I/O:接受 peers 对象,返回新对象。
242
- *
231
+ * 将候选 id 并入 explore,并按信誉重填 trusted
243
232
  * @param {{
244
233
  * peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
245
234
  * rep: { byNodeHash?: Record<string, { score?: number }> },
246
- * hints: string[],
235
+ * addIds: Iterable<string>,
247
236
  * limits: ReturnType<typeof resolveFederationPoolLimits>,
248
- * }} options 合并参数(peers、rep、hints、limits)
249
- * @returns {{ trustedPeers: string[], explorePeers: string[] }} 更新后的 trusted/explore 列表
237
+ * }} options 池状态与增量
238
+ * @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
250
239
  */
251
- export function applyPexHints({ peers, rep, hints, limits }) {
252
- const ids = [...new Set(
253
- (Array.isArray(hints) ? hints : [])
254
- .map(id => String(id).trim())
255
- .filter(Boolean),
256
- )]
240
+ function rebuildExploreAndTrusted(options) {
241
+ const { peers, rep, addIds, limits } = options
242
+ const blocked = new Set(peers.blockedPeers)
257
243
  const explore = new Set(peers.explorePeers)
258
- for (const id of ids)
259
- if (!peers.blockedPeers.includes(id)) explore.add(id)
260
- const newExplorePeers = [...explore].slice(-500)
244
+ for (const id of addIds)
245
+ if (id && !blocked.has(id)) explore.add(id)
246
+ const newExplorePeers = [...explore].filter(id => !blocked.has(id)).slice(-500)
261
247
  const ranked = [...new Set([...peers.trustedPeers, ...newExplorePeers])]
262
- .filter(id => !peers.blockedPeers.includes(id))
248
+ .filter(id => !blocked.has(id))
263
249
  .sort((a, b) => repScore(b, rep) - repScore(a, rep))
264
250
  return {
265
251
  trustedPeers: mergeTrustedWithAnchors(peers.trustedPeers, ranked, limits, peers.blockedPeers),
@@ -268,32 +254,39 @@ export function applyPexHints({ peers, rep, hints, limits }) {
268
254
  }
269
255
 
270
256
  /**
271
- * roster 观测中更新 explore 池并重新填充 trusted 槽(纯计算版本)。
272
- *
257
+ * PEX 线索并入 explore 并重填 trusted(纯计算)。
273
258
  * @param {{
274
259
  * peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
275
260
  * rep: { byNodeHash?: Record<string, { score?: number }> },
276
- * roster: Array<{ remoteNodeHash?: string }>,
261
+ * hints: string[],
277
262
  * limits: ReturnType<typeof resolveFederationPoolLimits>,
278
- * }} options roster 更新参数(peers、rep、roster、limits)
279
- * @returns {{ trustedPeers: string[], explorePeers: string[] }} 更新后的 trusted/explore 列表
263
+ * }} options 池状态与 PEX hints
264
+ * @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
280
265
  */
281
- export function applyRosterToPeerPool({ peers, rep, roster, limits }) {
282
- const explore = new Set(peers.explorePeers)
283
- for (const rosterEntry of roster) {
284
- const nodeId = rosterEntry.remoteNodeHash?.trim()
285
- if (nodeId && !peers.blockedPeers.includes(nodeId)) explore.add(nodeId)
286
- }
287
- const newExplorePeers = [...explore]
288
- .filter(id => !peers.blockedPeers.includes(id))
289
- .slice(-500)
290
- const candidates = [...new Set([...peers.trustedPeers, ...newExplorePeers])]
291
- .filter(id => !peers.blockedPeers.includes(id))
292
- .sort((a, b) => repScore(b, rep) - repScore(a, rep))
293
- return {
294
- trustedPeers: mergeTrustedWithAnchors(peers.trustedPeers, candidates, limits, peers.blockedPeers),
295
- explorePeers: newExplorePeers,
296
- }
266
+ export function applyPexHints(options) {
267
+ const { peers, rep, hints, limits } = options
268
+ return rebuildExploreAndTrusted({
269
+ peers, rep, limits,
270
+ addIds: hints || [],
271
+ })
272
+ }
273
+
274
+ /**
275
+ * roster 观测并入 explore 并重填 trusted(纯计算)。
276
+ * @param {{
277
+ * peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
278
+ * rep: { byNodeHash?: Record<string, { score?: number }> },
279
+ * roster: { remoteNodeHash?: string }[],
280
+ * limits: ReturnType<typeof resolveFederationPoolLimits>,
281
+ * }} options 池状态与 roster
282
+ * @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
283
+ */
284
+ export function applyRosterToPeerPool(options) {
285
+ const { peers, rep, roster, limits } = options
286
+ return rebuildExploreAndTrusted({
287
+ peers, rep, limits,
288
+ addIds: roster.map(entry => entry.remoteNodeHash).filter(Boolean),
289
+ })
297
290
  }
298
291
 
299
292
  /**
@@ -308,17 +301,13 @@ export function pickFederationTargetPeerIds(groupId, roster, groupSettings, self
308
301
  const limits = resolveFederationPoolLimits(groupSettings)
309
302
  const peers = loadPeerPoolView(groupId)
310
303
  const rep = loadReputation()
311
- const inRoomNodeHashes = roster
312
- .map(p => p.remoteNodeHash)
313
- .map(id => String(id).trim())
314
- .filter(Boolean)
315
304
  return selectPeerIdsFromPool({
316
305
  roster,
317
306
  peers,
318
307
  rep,
319
308
  limits,
320
309
  selfNodeHash,
321
- inRoomNodeHashes,
310
+ inRoomNodeHashes: roster.map(entry => entry.remoteNodeHash).filter(Boolean),
322
311
  hintSources: peers.hintSources,
323
312
  })
324
313
  }
@@ -12,15 +12,13 @@ import { USER_ROOM_SCOPE } from './room_scopes.mjs'
12
12
  * }} RemoteUserRoomSlot
13
13
  */
14
14
 
15
- /** @type {Map<string, RemoteUserRoomSlot | null>} nodeHash → slot(null 表示加入失败/进行中) */
15
+ /** @type {Map<string, RemoteUserRoomSlot>} nodeHash → slot */
16
16
  const slots = new Map()
17
17
  /** @type {Map<string, Promise<RemoteUserRoomSlot | null>>} nodeHash → 进行中的 promise */
18
18
  const inflights = new Map()
19
19
 
20
20
  registerFederationRoomProvider('remote-user-room', () => {
21
- return [...slots.values()]
22
- .filter(Boolean)
23
- .map(s => s.roomSlot)
21
+ return [...slots.values()].map(s => s.roomSlot)
24
22
  })
25
23
 
26
24
  /**
@@ -32,16 +30,14 @@ registerFederationRoomProvider('remote-user-room', () => {
32
30
  export async function ensureRemoteUserRoom(username, targetNodeHash) {
33
31
  void username
34
32
  const key = targetNodeHash.toLowerCase()
35
- if (slots.has(key)) return slots.get(key) || null
36
- const existing = inflights.get(key)
37
- if (existing) return await existing
33
+ const existing = slots.get(key)
34
+ if (existing) return existing
35
+ const inflight = inflights.get(key)
36
+ if (inflight) return await inflight
38
37
 
39
38
  const task = (async () => {
40
39
  try {
41
- if (!await ensureLinkToNode(key)) {
42
- slots.set(key, null)
43
- return null
44
- }
40
+ if (!await ensureLinkToNode(key)) return null
45
41
 
46
42
  /** @type {import('../registries/room_provider.mjs').FederationRoomSlot} */
47
43
  const roomSlot = {
@@ -66,17 +62,20 @@ export async function ensureRemoteUserRoom(username, targetNodeHash) {
66
62
  * @returns {void}
67
63
  */
68
64
  sendToPeer(peerId, actionName, payload) {
69
- void getLink(key)?.send({ scope: 'node', action: String(actionName), payload }).catch(() => { })
65
+ void getLink(key)?.send({ scope: 'node', action: actionName, payload }).catch(() => { })
70
66
  },
71
67
  }
72
68
 
73
69
  const slot = {
74
70
  roomSlot,
75
71
  /**
76
- * 关闭远端用户房间链路。
72
+ * 关闭远端用户房间链路并释放槽位。
77
73
  * @returns {Promise<void>} 关闭完成
78
74
  */
79
- leave() { return closeLink(key, 'remote-user-room-release') },
75
+ leave() {
76
+ slots.delete(key)
77
+ return closeLink(key, 'remote-user-room-release')
78
+ },
80
79
  }
81
80
  slots.set(key, slot)
82
81
  invalidateTrustGraphCache()
@@ -84,7 +83,6 @@ export async function ensureRemoteUserRoom(username, targetNodeHash) {
84
83
  }
85
84
  catch (error) {
86
85
  console.error('p2p: failed to join remote user room', key, error)
87
- slots.set(key, null)
88
86
  return null
89
87
  }
90
88
  finally {