@steve02081504/fount-p2p 0.0.11 → 0.0.13

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 (102) hide show
  1. package/README.md +42 -11
  2. package/core/bytes_codec.mjs +65 -10
  3. package/core/composite_key.mjs +5 -5
  4. package/core/tcp_port.mjs +1 -1
  5. package/crypto/checkpoint_sign.mjs +2 -2
  6. package/crypto/crypto.mjs +3 -3
  7. package/crypto/key.mjs +12 -12
  8. package/dag/canonicalize_row.mjs +3 -3
  9. package/dag/index.mjs +2 -3
  10. package/dag/storage.mjs +36 -36
  11. package/discovery/advert_peer_hints.mjs +1 -1
  12. package/discovery/adverts.mjs +109 -0
  13. package/discovery/bt/index.mjs +154 -113
  14. package/discovery/bt/probe_child.mjs +2 -1
  15. package/discovery/bt/runtime.mjs +5 -12
  16. package/discovery/index.mjs +267 -62
  17. package/discovery/internal/signal_crypto.mjs +109 -0
  18. package/discovery/lan.mjs +228 -0
  19. package/discovery/nostr.mjs +430 -141
  20. package/federation/chunk_fetch_pending.mjs +14 -18
  21. package/federation/dag_order_cache.mjs +1 -1
  22. package/federation/entity_key_chain.mjs +3 -4
  23. package/federation/manifest_fetch_pending.mjs +1 -3
  24. package/federation/topo_order_memo.mjs +11 -4
  25. package/files/assemble.mjs +14 -14
  26. package/files/assemble_stream.mjs +6 -6
  27. package/files/chunk_fetch.mjs +2 -2
  28. package/files/chunk_responder.mjs +29 -20
  29. package/files/evfs.mjs +31 -30
  30. package/files/manifest_fetch.mjs +16 -3
  31. package/files/public_manifest.mjs +11 -11
  32. package/files/transfer_key_registry.mjs +2 -2
  33. package/index.mjs +86 -11
  34. package/infra/cli.mjs +62 -0
  35. package/infra/debug_log.mjs +56 -0
  36. package/infra/default_node_dir.mjs +22 -0
  37. package/infra/priority.mjs +56 -0
  38. package/infra/service.mjs +140 -0
  39. package/infra/tunables.json +5 -0
  40. package/link/channel_mux.mjs +10 -10
  41. package/link/frame.mjs +76 -131
  42. package/link/handshake.mjs +19 -20
  43. package/link/pipe.mjs +53 -79
  44. package/link/providers/ble_gatt.mjs +19 -31
  45. package/link/providers/lan_tcp.mjs +17 -32
  46. package/link/providers/link_id_pipe.mjs +46 -0
  47. package/link/providers/webrtc.mjs +42 -51
  48. package/link/rtc.mjs +24 -12
  49. package/mailbox/consumer_registry.mjs +2 -2
  50. package/mailbox/deliver_or_store.mjs +6 -6
  51. package/mailbox/wire.mjs +28 -24
  52. package/node/entity_store.mjs +6 -6
  53. package/node/identity.mjs +4 -4
  54. package/node/instance.mjs +46 -27
  55. package/node/local_data_revision.mjs +26 -0
  56. package/node/log.mjs +56 -0
  57. package/node/network.mjs +46 -14
  58. package/node/reputation_store.mjs +4 -4
  59. package/node/reputation_sync.mjs +318 -0
  60. package/node/routing_profile.mjs +21 -0
  61. package/node/signaling_config.mjs +30 -11
  62. package/overlay/index.mjs +35 -14
  63. package/package.json +13 -2
  64. package/rooms/scoped_link.mjs +17 -182
  65. package/schemas/discovery.mjs +1 -2
  66. package/schemas/federation_pull.mjs +2 -2
  67. package/schemas/part_query.mjs +1 -1
  68. package/schemas/remote_event.mjs +1 -1
  69. package/transport/advert_ingest.mjs +17 -0
  70. package/transport/group_link_set.mjs +165 -134
  71. package/transport/ice_servers.mjs +2 -2
  72. package/transport/link_registry.mjs +283 -124
  73. package/transport/mesh_keepalive.mjs +217 -0
  74. package/transport/node_scope.mjs +289 -0
  75. package/transport/offer_answer.mjs +48 -47
  76. package/transport/peer_pool.mjs +169 -78
  77. package/transport/remote_user_room.mjs +18 -23
  78. package/transport/rtc_connection_budget.mjs +18 -4
  79. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  80. package/transport/runtime_bootstrap.mjs +172 -104
  81. package/transport/tunables.json +11 -0
  82. package/transport/tunables.mjs +18 -0
  83. package/transport/user_room.mjs +93 -161
  84. package/trust_graph/build.mjs +0 -2
  85. package/trust_graph/cache.mjs +12 -3
  86. package/trust_graph/registry.mjs +6 -6
  87. package/trust_graph/send.mjs +1 -4
  88. package/utils/async_mutex.mjs +4 -4
  89. package/utils/emit_safe.mjs +11 -0
  90. package/utils/json_io.mjs +12 -12
  91. package/utils/map_pool.mjs +5 -5
  92. package/utils/shuffle.mjs +13 -0
  93. package/utils/ttl_map.mjs +29 -4
  94. package/wire/ingress.mjs +1 -1
  95. package/wire/part_ingress.mjs +38 -36
  96. package/wire/part_invoke.mjs +4 -4
  97. package/wire/part_query.mjs +28 -23
  98. package/wire/part_query.tunables.json +6 -1
  99. package/wire/part_query_cache.mjs +1 -1
  100. package/wire/volatile_signature.mjs +1 -1
  101. package/discovery/mdns.mjs +0 -191
  102. package/transport/signal_crypto.mjs +0 -82
@@ -1,15 +1,16 @@
1
1
  import { randomBytes } from 'node:crypto'
2
2
 
3
3
  import { normalizeHex64 } from '../core/hexIds.mjs'
4
- import { sendSignal } from '../discovery/index.mjs'
4
+ import { decryptNodeSignalPacket, sendNodeSignalPacket } from '../discovery/index.mjs'
5
5
  import { listLinkProviders } from '../link/providers/index.mjs'
6
+ import { nodeDebug, shortHash } from '../node/log.mjs'
6
7
 
7
- import { decryptSignalPacket, encryptSignalPacket, nodeRendezvousTopic } from './signal_crypto.mjs'
8
+ /** accept/dial 挂起期间 ICE 信令 backlog 上限 */
9
+ const SIGNAL_BACKLOG_MAX = 64
8
10
 
9
11
  /**
10
- * 创建带 backlog 的缓冲信令会话。
11
12
  * @param {(message: unknown) => Promise<void>} sendRemote 远端发送回调
12
- * @returns {{ send: (message: unknown) => Promise<void>, onRemote: (handler: (message: unknown) => void) => () => void, deliver: (message: unknown) => void, clear: () => void }} 信令会话
13
+ * @returns {object} 信令会话
13
14
  */
14
15
  export function createBufferedSignalSession(sendRemote) {
15
16
  /** @type {Set<(message: unknown) => void>} */
@@ -18,17 +19,15 @@ export function createBufferedSignalSession(sendRemote) {
18
19
  const backlog = []
19
20
  return {
20
21
  /**
21
- * 发送信令消息到远端。
22
- * @param {unknown} message 信令消息
22
+ * @param {unknown} message 待发送信令消息
23
23
  * @returns {Promise<void>}
24
24
  */
25
25
  async send(message) {
26
26
  await sendRemote(message)
27
27
  },
28
28
  /**
29
- * 注册远端信令 handler(含 backlog 回放)。
30
- * @param {(message: unknown) => void} handler 入站回调
31
- * @returns {() => void} 取消订阅函数
29
+ * @param {(message: unknown) => void} handler 远端消息回调
30
+ * @returns {() => void} 取消订阅
32
31
  */
33
32
  onRemote(handler) {
34
33
  handlers.add(handler)
@@ -37,12 +36,12 @@ export function createBufferedSignalSession(sendRemote) {
37
36
  return () => handlers.delete(handler)
38
37
  },
39
38
  /**
40
- * 投递信令消息;无 handler 时入 backlog。
41
- * @param {unknown} message 信令消息
39
+ * @param {unknown} message 入站信令消息
42
40
  * @returns {void}
43
41
  */
44
42
  deliver(message) {
45
43
  if (!handlers.size) {
44
+ if (backlog.length >= SIGNAL_BACKLOG_MAX) backlog.shift()
46
45
  backlog.push(message)
47
46
  return
48
47
  }
@@ -50,7 +49,6 @@ export function createBufferedSignalSession(sendRemote) {
50
49
  handler(message)
51
50
  },
52
51
  /**
53
- * 清空 backlog 与 handler。
54
52
  * @returns {void}
55
53
  */
56
54
  clear() {
@@ -61,9 +59,8 @@ export function createBufferedSignalSession(sendRemote) {
61
59
  }
62
60
 
63
61
  /**
64
- * 判断信令 body 是否为发起方 offer(据此决定入站是否新建应答 PC)。
65
62
  * @param {unknown} body 信令 body
66
- * @returns {boolean} offer 则 true
63
+ * @returns {boolean} 是否为 WebRTC offer description
67
64
  */
68
65
  function isOfferSignalBody(body) {
69
66
  return !!body && typeof body === 'object'
@@ -72,30 +69,19 @@ function isOfferSignalBody(body) {
72
69
  }
73
70
 
74
71
  /**
75
- * 找已注册的 offer/answer provider(按 level 降序的第一个)。
76
- * @returns {import('../link/providers/index.mjs').LinkProvider | null} 命中的 provider;无则 null
72
+ * @returns {import('../link/providers/index.mjs').LinkProvider | null} 首个需要 offer/answer provider
77
73
  */
78
74
  export function findOfferAnswerProvider() {
79
75
  return listLinkProviders().find(provider => provider.caps?.needsOfferAnswer) ?? null
80
76
  }
81
77
 
82
78
  /**
83
- * Offer/answer glare 拨号控制器(WebRTC 等 needsOfferAnswer provider)。
84
- * @param {object} deps 依赖注入
85
- * @param {{ nodeHash: string, nodePubKey: string, secretKey: Uint8Array }} deps.localIdentity 本地身份
86
- * @param {RTCConfiguration['iceServers']} deps.iceServers ICE 服务器列表
87
- * @param {string} deps.selfTopic 本机 rendezvous topic
88
- * @param {Map<string, ReturnType<typeof createBufferedSignalSession>>} deps.signalSessions 按 connId 索引的信令会话
89
- * @param {(remoteNodeHash: string, link: object) => Promise<void>} deps.registerResolvedLink 注册规范链
90
- * @param {() => Promise<void>} deps.trimToBudget 超预算驱逐
91
- * @param {(remoteNodeHash: string) => object | null | undefined} deps.getCanonicalLink 读取当前规范链
92
- * @returns {{ handleIncomingSignal: (bytes: Uint8Array) => Promise<void>, dialOfferAnswer: (provider: import('../link/providers/index.mjs').LinkProvider, remoteNodeHash: string) => Promise<object | null> }} 入站信令与主动拨号
79
+ * @param {object} deps 依赖
80
+ * @returns {object} offer/answer 拨号
93
81
  */
94
82
  export function createOfferAnswerDial(deps) {
95
83
  const {
96
84
  localIdentity,
97
- iceServers,
98
- selfTopic,
99
85
  signalSessions,
100
86
  registerResolvedLink,
101
87
  trimToBudget,
@@ -103,28 +89,30 @@ export function createOfferAnswerDial(deps) {
103
89
  } = deps
104
90
 
105
91
  /**
106
- * 为单条 PC 创建按 connId 标记的信令会话。
107
- * @param {string} remoteNodeHash 远端节点 64 hex
108
- * @param {string} connId 连接标识
109
- * @returns {ReturnType<typeof createBufferedSignalSession>} 信令会话
92
+ * @param {string} remoteNodeHash 远端
93
+ * @param {string} connId 连接 id
94
+ * @returns {ReturnType<typeof createBufferedSignalSession>} 该连接的缓冲信令会话
110
95
  */
111
96
  function createConnSession(remoteNodeHash, connId) {
112
97
  const normalized = normalizeHex64(remoteNodeHash)
113
- const topic = nodeRendezvousTopic(normalized)
114
98
  return createBufferedSignalSession(async message => {
115
- await sendSignal(topic, normalized, encryptSignalPacket(topic, {
99
+ await sendNodeSignalPacket(normalized, {
116
100
  type: 'signal',
117
101
  from: localIdentity.nodeHash,
118
102
  connId,
119
103
  body: message,
120
- }))
104
+ })
121
105
  })
122
106
  }
123
107
 
124
108
  /**
125
- * Offer/answer provider:为一条 connId 会话建链并走 glare 择一。
126
- * @param {{ provider: import('../link/providers/index.mjs').LinkProvider, remoteNodeHash: string, connId: string, session: ReturnType<typeof createBufferedSignalSession>, initiator: boolean }} options 建链参数
127
- * @returns {Promise<object | null>} 当前规范链;失败 null
109
+ * @param {object} options 建链参数
110
+ * @param {import('../link/providers/index.mjs').LinkProvider} options.provider 链路提供者
111
+ * @param {string} options.remoteNodeHash 远端 nodeHash
112
+ * @param {string} options.connId 连接 id
113
+ * @param {ReturnType<typeof createBufferedSignalSession>} options.session 信令会话
114
+ * @param {boolean} options.initiator 是否主动拨号
115
+ * @returns {Promise<object | null>} 建链成功返回规范链路,否则 null
128
116
  */
129
117
  async function buildConnLink({ provider, remoteNodeHash, connId, session, initiator }) {
130
118
  try {
@@ -132,11 +120,16 @@ export function createOfferAnswerDial(deps) {
132
120
  const link = await (initiator ? provider.dial : provider.accept)({
133
121
  nodeHash: remoteNodeHash,
134
122
  signal: session,
135
- iceServers,
123
+ iceServers: deps.iceServers,
136
124
  localIdentity,
137
125
  })
138
126
  if (!link) {
139
127
  signalSessions.delete(connId)
128
+ nodeDebug('p2p:webrtc null', {
129
+ peer: shortHash(remoteNodeHash),
130
+ provider: provider.id,
131
+ role: initiator ? 'dial' : 'accept',
132
+ })
140
133
  return null
141
134
  }
142
135
  link.onDown(() => signalSessions.delete(connId))
@@ -144,20 +137,25 @@ export function createOfferAnswerDial(deps) {
144
137
  await registerResolvedLink(remoteNodeHash, link)
145
138
  return getCanonicalLink(remoteNodeHash) ?? null
146
139
  }
147
- catch {
140
+ catch (error) {
148
141
  signalSessions.delete(connId)
142
+ nodeDebug('p2p:webrtc fail', {
143
+ peer: shortHash(remoteNodeHash),
144
+ provider: provider.id,
145
+ role: initiator ? 'dial' : 'accept',
146
+ err: String(error?.message || error),
147
+ })
149
148
  return null
150
149
  }
151
150
  }
152
151
 
153
152
  /**
154
- * 处理入站加密信令并可能发起被动建链。
155
- * @param {Uint8Array} bytes 加密信令字节
153
+ * @param {Uint8Array} bytes 加密信令
156
154
  * @returns {Promise<void>}
157
155
  */
158
156
  async function handleIncomingSignal(bytes) {
159
- const packet = decryptSignalPacket(selfTopic, bytes)
160
- if (!packet || packet.type !== 'signal') return
157
+ const packet = decryptNodeSignalPacket(localIdentity.nodeHash, bytes)
158
+ if (packet?.type !== 'signal') return
161
159
  const remoteNodeHash = normalizeHex64(packet.from)
162
160
  const connId = String(packet.connId || '')
163
161
  if (!remoteNodeHash || remoteNodeHash === localIdentity.nodeHash || !connId) return
@@ -166,6 +164,10 @@ export function createOfferAnswerDial(deps) {
166
164
  if (!isOfferSignalBody(packet.body)) return
167
165
  const provider = findOfferAnswerProvider()
168
166
  if (!provider) return
167
+ nodeDebug('p2p:webrtc inbound offer', {
168
+ peer: shortHash(remoteNodeHash),
169
+ provider: provider.id,
170
+ })
169
171
  session = createConnSession(remoteNodeHash, connId)
170
172
  signalSessions.set(connId, session)
171
173
  void buildConnLink({ provider, remoteNodeHash, connId, session, initiator: false })
@@ -174,10 +176,9 @@ export function createOfferAnswerDial(deps) {
174
176
  }
175
177
 
176
178
  /**
177
- * 经 offer/answer provider 拨号(discovery signal + glare)。
178
179
  * @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
179
- * @param {string} remoteNodeHash 远端 64 hex
180
- * @returns {Promise<object | null>} 当前规范链;失败 null
180
+ * @param {string} remoteNodeHash 远端 nodeHash
181
+ * @returns {Promise<object | null>} 建链成功返回规范链路,否则 null
181
182
  */
182
183
  async function dialOfferAnswer(provider, remoteNodeHash) {
183
184
  const connId = randomBytes(16).toString('hex')
@@ -1,13 +1,16 @@
1
1
  /**
2
- * 稀疏连接池纯计算逻辑(§0、§4):
3
- * Top-K 信任连接 + M 随机探索,可被 chat shell 与 subfounts 共用。
4
- * 不含文件 I/O;I/O 层由调用方注入。
2
+ * 连接池纯计算:
3
+ * - 群联邦稀疏拨号:trustedSlots + exploreSlots(groupSettings)
4
+ * - 节点 mesh 保活:N / K_max(routing profile + transport tunables)
5
+ * 不含文件 I/O;I/O 由调用方注入。
5
6
  */
6
7
 
8
+ import { compareHex64Asc } from '../core/hexIds.mjs'
7
9
  import { loadPeerPoolView, mergeNetworkPeerPools } from '../node/network.mjs'
8
10
  import { loadReputation } from '../node/reputation_store.mjs'
9
11
  import { isQuarantinedPure } from '../reputation/engine.mjs'
10
12
  import { clampReputationScore } from '../reputation/math.mjs'
13
+ import { shuffleInPlace } from '../utils/shuffle.mjs'
11
14
 
12
15
  /**
13
16
  * 解析联邦池槽位参数(从 groupSettings 读取,含低功耗缩减)。
@@ -91,37 +94,27 @@ export function mergeTrustedWithAnchors(existingTrusted, rankedCandidates, limit
91
94
  */
92
95
  export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxPerSource = EXPLORE_MAX_PER_SOURCE) {
93
96
  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
- }
97
+ if (!exploreSources?.size)
98
+ return shuffleInPlace([...exploreIds]).slice(0, k)
102
99
  /** @type {Map<string, string[]>} */
103
100
  const bySource = new Map()
104
101
  for (const id of exploreIds) {
105
- const src = exploreSources.get(id) || 'unknown'
106
- if (!bySource.has(src)) bySource.set(src, [])
107
- bySource.get(src).push(id)
102
+ const source = exploreSources.get(id) || 'unknown'
103
+ if (!bySource.has(source)) bySource.set(source, [])
104
+ bySource.get(source).push(id)
108
105
  }
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
- }
106
+ for (const ids of bySource.values()) shuffleInPlace(ids)
114
107
  const out = []
115
108
  /** @type {Map<string, number>} */
116
109
  const picked = new Map()
117
110
  while (out.length < k) {
118
111
  let progressed = false
119
- for (const [src, ids] of bySource) {
112
+ for (const [source, ids] of bySource) {
120
113
  if (out.length >= k) break
121
- const idx = picked.get(src) ?? 0
122
- if (idx >= maxPerSource || idx >= ids.length) continue
123
- out.push(ids[idx])
124
- picked.set(src, idx + 1)
114
+ const index = picked.get(source) ?? 0
115
+ if (index >= maxPerSource || index >= ids.length) continue
116
+ out.push(ids[index])
117
+ picked.set(source, index + 1)
125
118
  progressed = true
126
119
  }
127
120
  if (!progressed) break
@@ -142,13 +135,13 @@ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxP
142
135
  * inRoomNodeHashes?: Set<string> | string[] 群内在线 node_id;有则优先,仅全不可达时用 explore 中非房内节点
143
136
  * hintSources?: Map<string, string> explore 节点来源(用于配额)
144
137
  * }} options 选取参数(roster、peers、rep、limits、selfNodeHash)
145
- * @returns {string[]} 目标 Trystero peerId 列表(去重,长度 ≤ maxPeers)
138
+ * @returns {string[]} 目标 peerId 列表(去重,长度 ≤ maxPeers)
146
139
  */
147
140
  export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash, inRoomNodeHashes, hintSources }) {
148
141
  const blocked = new Set(peers.blockedPeers)
149
142
  const roomSet = inRoomNodeHashes instanceof Set
150
143
  ? inRoomNodeHashes
151
- : new Set(Array.isArray(inRoomNodeHashes) ? inRoomNodeHashes : [])
144
+ : new Set(inRoomNodeHashes || [])
152
145
  const onlineAll = roster.filter(
153
146
  rosterEntry => rosterEntry.peerId
154
147
  && rosterEntry.remoteNodeHash
@@ -175,10 +168,11 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
175
168
  }
176
169
 
177
170
  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)) {
171
+ for (const nodeId of mergeTrustedWithAnchors(
172
+ anchoredTrusted,
173
+ [...trustedSet].sort((a, b) => repScore(b, rep) - repScore(a, rep)),
174
+ limits,
175
+ )) {
182
176
  if (outPeerIds.size >= limits.maxPeers) break
183
177
  pushNode(nodeId)
184
178
  }
@@ -216,15 +210,14 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
216
210
  * @returns {string[]} 应建链的 nodeHash 列表(去重)
217
211
  */
218
212
  export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers, limits, anchors = [] }) {
219
- const self = String(selfNodeHash || '')
220
213
  const blocked = new Set(peers?.blockedPeers || [])
221
214
  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))
215
+ const candidates = [...new Set(members)]
216
+ .filter(id => id && id !== selfNodeHash && !blocked.has(id) && !isQuarantinedPure(rep, id, now))
224
217
  const ranked = candidates.slice().sort((a, b) => repScore(b, rep) - repScore(a, rep))
225
218
  const candidateSet = new Set(candidates)
226
219
  // 锚点(如 introducer/creator/seed)必连、且不占 trustedSlots——保证引导期连通。
227
- const forced = [...new Set([...anchors].map(id => String(id)))].filter(id => candidateSet.has(id))
220
+ const forced = [...new Set(anchors)].filter(id => candidateSet.has(id))
228
221
  const chosen = new Set(forced)
229
222
  // trusted 槽只从非锚点候选填:既有 trusted 优先保留,再按信誉补至 trustedSlots。
230
223
  const nonForced = ranked.filter(id => !chosen.has(id))
@@ -237,29 +230,24 @@ export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers
237
230
  }
238
231
 
239
232
  /**
240
- * 将新 PEX 节点线索合并进 explore 池,并按信誉重新填充 trusted 槽。
241
- * 纯计算版本,不含 I/O:接受 peers 对象,返回新对象。
242
- *
233
+ * 将候选 id 并入 explore,并按信誉重填 trusted
243
234
  * @param {{
244
235
  * peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
245
236
  * rep: { byNodeHash?: Record<string, { score?: number }> },
246
- * hints: string[],
237
+ * addIds: Iterable<string>,
247
238
  * limits: ReturnType<typeof resolveFederationPoolLimits>,
248
- * }} options 合并参数(peers、rep、hints、limits)
249
- * @returns {{ trustedPeers: string[], explorePeers: string[] }} 更新后的 trusted/explore 列表
239
+ * }} options 池状态与增量
240
+ * @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
250
241
  */
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
- )]
242
+ function rebuildExploreAndTrusted(options) {
243
+ const { peers, rep, addIds, limits } = options
244
+ const blocked = new Set(peers.blockedPeers)
257
245
  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)
246
+ for (const id of addIds)
247
+ if (id && !blocked.has(id)) explore.add(id)
248
+ const newExplorePeers = [...explore].filter(id => !blocked.has(id)).slice(-500)
261
249
  const ranked = [...new Set([...peers.trustedPeers, ...newExplorePeers])]
262
- .filter(id => !peers.blockedPeers.includes(id))
250
+ .filter(id => !blocked.has(id))
263
251
  .sort((a, b) => repScore(b, rep) - repScore(a, rep))
264
252
  return {
265
253
  trustedPeers: mergeTrustedWithAnchors(peers.trustedPeers, ranked, limits, peers.blockedPeers),
@@ -268,57 +256,60 @@ export function applyPexHints({ peers, rep, hints, limits }) {
268
256
  }
269
257
 
270
258
  /**
271
- * roster 观测中更新 explore 池并重新填充 trusted 槽(纯计算版本)。
272
- *
259
+ * PEX 线索并入 explore 并重填 trusted(纯计算)。
273
260
  * @param {{
274
261
  * peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
275
262
  * rep: { byNodeHash?: Record<string, { score?: number }> },
276
- * roster: Array<{ remoteNodeHash?: string }>,
263
+ * hints: string[],
277
264
  * limits: ReturnType<typeof resolveFederationPoolLimits>,
278
- * }} options roster 更新参数(peers、rep、roster、limits)
279
- * @returns {{ trustedPeers: string[], explorePeers: string[] }} 更新后的 trusted/explore 列表
265
+ * }} options 池状态与 PEX hints
266
+ * @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
280
267
  */
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
- }
268
+ export function applyPexHints(options) {
269
+ const { peers, rep, hints, limits } = options
270
+ return rebuildExploreAndTrusted({
271
+ peers, rep, limits,
272
+ addIds: hints || [],
273
+ })
274
+ }
275
+
276
+ /**
277
+ * roster 观测并入 explore 并重填 trusted(纯计算)。
278
+ * @param {{
279
+ * peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
280
+ * rep: { byNodeHash?: Record<string, { score?: number }> },
281
+ * roster: { remoteNodeHash?: string }[],
282
+ * limits: ReturnType<typeof resolveFederationPoolLimits>,
283
+ * }} options 池状态与 roster
284
+ * @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
285
+ */
286
+ export function applyRosterToPeerPool(options) {
287
+ const { peers, rep, roster, limits } = options
288
+ return rebuildExploreAndTrusted({
289
+ peers, rep, limits,
290
+ addIds: roster.map(entry => entry.remoteNodeHash).filter(Boolean),
291
+ })
297
292
  }
298
293
 
299
294
  /**
300
295
  * 稀疏连接池:优先 trusted,再 explore,再其余在线节点。
301
296
  * @param {string} groupId 群
302
- * @param {{ peerId: string, remoteNodeHash?: string }[]} roster Trystero 在线表
297
+ * @param {{ peerId: string, remoteNodeHash?: string }[]} roster 在线表
303
298
  * @param {object} groupSettings 物化群设置
304
299
  * @param {string} selfNodeHash 本机 node_id
305
- * @returns {string[]} 目标 Trystero peerId(去重)
300
+ * @returns {string[]} 目标 peerId(去重)
306
301
  */
307
302
  export function pickFederationTargetPeerIds(groupId, roster, groupSettings, selfNodeHash) {
308
303
  const limits = resolveFederationPoolLimits(groupSettings)
309
304
  const peers = loadPeerPoolView(groupId)
310
305
  const rep = loadReputation()
311
- const inRoomNodeHashes = roster
312
- .map(p => p.remoteNodeHash)
313
- .map(id => String(id).trim())
314
- .filter(Boolean)
315
306
  return selectPeerIdsFromPool({
316
307
  roster,
317
308
  peers,
318
309
  rep,
319
310
  limits,
320
311
  selfNodeHash,
321
- inRoomNodeHashes,
312
+ inRoomNodeHashes: roster.map(entry => entry.remoteNodeHash).filter(Boolean),
322
313
  hintSources: peers.hintSources,
323
314
  })
324
315
  }
@@ -353,3 +344,103 @@ export function reconcilePeerPoolFromRoster(groupId, roster, groupSettings) {
353
344
  const { trustedPeers, explorePeers } = applyRosterToPeerPool({ peers, rep, roster, limits })
354
345
  mergeNetworkPeerPools({ trustedPeers, explorePeers })
355
346
  }
347
+
348
+ /**
349
+ * Mesh 保活 N/K 槽位(routing profile 可缩 N 与 K_max)。
350
+ * @param {'default' | 'low'} [routingProfile='default'] 路由 profile
351
+ * @param {object} [tunables={}] transport tunables
352
+ * @returns {{ N: number, K_max: number }} mesh 槽位上限 N 与熟人槽 K_max
353
+ */
354
+ export function resolveMeshPoolLimits(routingProfile = 'default', tunables = {}) {
355
+ const low = routingProfile === 'low'
356
+ const N = Math.max(1, Math.min(32, Number(low ? tunables.meshNLow : tunables.meshN) || (low ? 4 : 8)))
357
+ const K_max = Math.max(0, Math.min(N, Number(low ? tunables.meshKMaxLow : tunables.meshKMax) || (low ? 2 : 5)))
358
+ return { N, K_max }
359
+ }
360
+
361
+ /**
362
+ * 选取 mesh 拨号目标:补齐 K 熟人(可超出当前空位,由调用方先踢探索),再按 N−K 探索配额填空。
363
+ * @param {{
364
+ * selfNodeHash: string,
365
+ * trustedPeers: string[],
366
+ * exploreCandidates: string[],
367
+ * hintSources?: Map<string, string>,
368
+ * limits: ReturnType<typeof resolveMeshPoolLimits>,
369
+ * connectedHashes: Set<string>,
370
+ * rep: { byNodeHash?: Record<string, { score?: number, quarantinedUntil?: number }> },
371
+ * blockedPeers?: string[],
372
+ * now?: number,
373
+ * }} options 选取参数
374
+ * @returns {string[]} 应拨号的 nodeHash(熟人可导致需先驱逐探索)
375
+ */
376
+ export function selectMeshLinkTargets(options) {
377
+ const {
378
+ selfNodeHash,
379
+ trustedPeers = [],
380
+ exploreCandidates = [],
381
+ hintSources,
382
+ limits,
383
+ connectedHashes,
384
+ rep,
385
+ blockedPeers = [],
386
+ now = Date.now(),
387
+ } = options
388
+ const blocked = new Set(blockedPeers)
389
+ const connected = connectedHashes
390
+
391
+ const eligibleTrusted = [...new Set(trustedPeers)]
392
+ .filter(id => id && id !== selfNodeHash && !blocked.has(id))
393
+ .filter(id => !isQuarantinedPure(rep, id, now))
394
+ const trustedSet = new Set(eligibleTrusted)
395
+ /** 目标组成:K 熟人槽 + (N−K) 探索槽 */
396
+ const K = Math.min(limits.K_max, eligibleTrusted.length)
397
+ let connectedTrusted = 0
398
+ let connectedExplore = 0
399
+ for (const id of connected)
400
+ if (trustedSet.has(id)) connectedTrusted++
401
+ else connectedExplore++
402
+ const trustedSlotsLeft = Math.max(0, K - connectedTrusted)
403
+ const trustedRanked = eligibleTrusted
404
+ .filter(id => !connected.has(id))
405
+ .sort((a, b) => repScore(b, rep) - repScore(a, rep))
406
+ // 熟人缺口优先补齐,即使当前已满 N(调用方应先踢探索腾位)
407
+ const trustedPick = trustedRanked.slice(0, trustedSlotsLeft)
408
+
409
+ const exploreQuota = Math.max(0, limits.N - K)
410
+ const exploreSlotsLeft = Math.max(0, exploreQuota - connectedExplore)
411
+ const freeAfterTrustedDial = Math.max(0, limits.N - connected.size - trustedPick.length)
412
+ const exploreNeed = Math.min(exploreSlotsLeft, freeAfterTrustedDial)
413
+ const explorePool = [...new Set(exploreCandidates)]
414
+ .filter(id => id && id !== selfNodeHash && !blocked.has(id) && !connected.has(id) && !trustedSet.has(id))
415
+ .filter(id => !isQuarantinedPure(rep, id, now))
416
+ const explorePick = selectExploreWithSourceQuota(explorePool, hintSources, exploreNeed)
417
+ return [...trustedPick, ...explorePick]
418
+ }
419
+
420
+ /**
421
+ * mesh trim:探索链优先驱逐,同档取 scope 权重低、nodeHash 小。
422
+ * @param {string[]} linkHashes 当前链路 nodeHash
423
+ * @param {Set<string>} exploreLinkHashes 探索链集合
424
+ * @param {string[]} trustedPeers 熟人池
425
+ * @param {(nodeHash: string) => number} scopeWeightFn scope 权重
426
+ * @returns {string | null} 应驱逐的 nodeHash
427
+ */
428
+ export function pickMeshEvictionVictim(linkHashes, exploreLinkHashes, trustedPeers, scopeWeightFn) {
429
+ const trustedSet = new Set(trustedPeers)
430
+ let victimHash = null
431
+ let victimScore = Infinity
432
+ for (const nodeHash of linkHashes) {
433
+ const isExplore = exploreLinkHashes.has(nodeHash) && !trustedSet.has(nodeHash)
434
+ const weight = scopeWeightFn(nodeHash)
435
+ const score = (isExplore ? 0 : 1000) + weight
436
+ if (
437
+ victimHash == null
438
+ || score < victimScore
439
+ || (score === victimScore && compareHex64Asc(nodeHash, victimHash) < 0)
440
+ ) {
441
+ victimHash = nodeHash
442
+ victimScore = score
443
+ }
444
+ }
445
+ return victimHash
446
+ }
@@ -1,5 +1,5 @@
1
+ import { bumpLocalDataRevision } from '../node/local_data_revision.mjs'
1
2
  import { registerFederationRoomProvider } from '../registries/room_provider.mjs'
2
- import { invalidateTrustGraphCache } from '../trust_graph/cache.mjs'
3
3
 
4
4
  import { closeLink, ensureLinkToNode, getLink } from './link_registry.mjs'
5
5
  import { USER_ROOM_SCOPE } from './room_scopes.mjs'
@@ -12,36 +12,33 @@ 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
- /** @type {Map<string, Promise<RemoteUserRoomSlot | null>>} nodeHash → 进行中的 promise */
17
+ /** @type {Map<string, Promise<RemoteUserRoomSlot>>} 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
  /**
27
25
  * 加入目标节点的用户房间(幂等)。
28
26
  * @param {string} username 本地 replica 用户名
29
27
  * @param {string} targetNodeHash 目标节点 64 hex
30
- * @returns {Promise<RemoteUserRoomSlot | null>} 房间槽
28
+ * @returns {Promise<RemoteUserRoomSlot>} 房间槽
31
29
  */
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))
41
+ throw new Error(`p2p: ensureRemoteUserRoom link failed for ${key}`)
45
42
 
46
43
  /** @type {import('../registries/room_provider.mjs').FederationRoomSlot} */
47
44
  const roomSlot = {
@@ -66,27 +63,25 @@ export async function ensureRemoteUserRoom(username, targetNodeHash) {
66
63
  * @returns {void}
67
64
  */
68
65
  sendToPeer(peerId, actionName, payload) {
69
- void getLink(key)?.send({ scope: 'node', action: String(actionName), payload }).catch(() => { })
66
+ void getLink(key)?.send({ scope: 'node', action: actionName, payload }).catch(() => { })
70
67
  },
71
68
  }
72
69
 
73
70
  const slot = {
74
71
  roomSlot,
75
72
  /**
76
- * 关闭远端用户房间链路。
73
+ * 关闭远端用户房间链路并释放槽位。
77
74
  * @returns {Promise<void>} 关闭完成
78
75
  */
79
- leave() { return closeLink(key, 'remote-user-room-release') },
76
+ leave() {
77
+ slots.delete(key)
78
+ return closeLink(key, 'remote-user-room-release')
79
+ },
80
80
  }
81
81
  slots.set(key, slot)
82
- invalidateTrustGraphCache()
82
+ bumpLocalDataRevision()
83
83
  return slot
84
84
  }
85
- catch (error) {
86
- console.error('p2p: failed to join remote user room', key, error)
87
- slots.set(key, null)
88
- return null
89
- }
90
85
  finally {
91
86
  inflights.delete(key)
92
87
  }