@steve02081504/fount-p2p 0.0.0

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 (154) hide show
  1. package/README.md +62 -0
  2. package/core/bytes_codec.mjs +51 -0
  3. package/core/canonical_json.mjs +21 -0
  4. package/core/composite_key.mjs +91 -0
  5. package/core/constants.mjs +53 -0
  6. package/core/entity_id.mjs +53 -0
  7. package/core/entity_id_parse.mjs +40 -0
  8. package/core/evfs_logical_path.mjs +15 -0
  9. package/core/hexIds.mjs +73 -0
  10. package/core/hlc.mjs +114 -0
  11. package/core/random_id.mjs +9 -0
  12. package/crypto/channel.mjs +101 -0
  13. package/crypto/checkpoint_sign.mjs +67 -0
  14. package/crypto/crypto.mjs +158 -0
  15. package/crypto/key.mjs +431 -0
  16. package/dag/canonicalize_row.mjs +65 -0
  17. package/dag/event_query.mjs +15 -0
  18. package/dag/index.mjs +335 -0
  19. package/dag/storage.mjs +333 -0
  20. package/dag/strip_extensions.mjs +18 -0
  21. package/dag/tunables.json +6 -0
  22. package/discovery/bt.mjs +260 -0
  23. package/discovery/index.mjs +134 -0
  24. package/discovery/mdns.mjs +138 -0
  25. package/discovery/nostr.mjs +273 -0
  26. package/entity/files/acl.mjs +38 -0
  27. package/entity/files/evfs.mjs +189 -0
  28. package/entity/files/evfs_ref.mjs +37 -0
  29. package/entity/files/manifest_acl_registry.mjs +93 -0
  30. package/entity/files/replica_host_cache.mjs +46 -0
  31. package/entity/files/url.mjs +19 -0
  32. package/entity/hosting_registry.mjs +47 -0
  33. package/entity/localized_core.mjs +141 -0
  34. package/entity/logical_entity.mjs +25 -0
  35. package/entity/logical_entity_id_registry.mjs +40 -0
  36. package/entity/node_hash.mjs +15 -0
  37. package/entity/presentation_registry.mjs +44 -0
  38. package/entity/profile.mjs +256 -0
  39. package/entity/replica.mjs +20 -0
  40. package/entity/session_snapshot_registry.mjs +38 -0
  41. package/federation/chunk_fetch_pending.mjs +128 -0
  42. package/federation/chunk_fetch_scheduler.mjs +97 -0
  43. package/federation/dag_order_cache.mjs +124 -0
  44. package/federation/dedupe_slot.mjs +33 -0
  45. package/federation/message_rate_limit.mjs +23 -0
  46. package/federation/operator_key_chain.mjs +200 -0
  47. package/federation/topo_order_memo.mjs +44 -0
  48. package/federation/volatile_streams.mjs +52 -0
  49. package/federation/want_ids.mjs +158 -0
  50. package/files/assemble.mjs +220 -0
  51. package/files/assemble_stream.mjs +129 -0
  52. package/files/chunk_fetch.mjs +113 -0
  53. package/files/chunk_fetch_verify.mjs +23 -0
  54. package/files/chunk_provider_registry.mjs +70 -0
  55. package/files/chunk_responder.mjs +79 -0
  56. package/files/chunk_store.mjs +87 -0
  57. package/files/manifest.mjs +99 -0
  58. package/files/transfer_key.mjs +87 -0
  59. package/files/transfer_key_registry.mjs +108 -0
  60. package/governance/branch.mjs +230 -0
  61. package/governance/join_pow.mjs +165 -0
  62. package/governance/owner_succession_ballot.mjs +57 -0
  63. package/governance/tunables.json +7 -0
  64. package/index.mjs +39 -0
  65. package/link/channel_mux.mjs +222 -0
  66. package/link/frame.mjs +246 -0
  67. package/link/handshake.mjs +175 -0
  68. package/link/link.mjs +617 -0
  69. package/link/rtc.mjs +109 -0
  70. package/link/sdp_fingerprint.mjs +25 -0
  71. package/mailbox/consumer_registry.mjs +71 -0
  72. package/mailbox/deliver_or_store.mjs +190 -0
  73. package/mailbox/importance.mjs +72 -0
  74. package/mailbox/parse.mjs +73 -0
  75. package/mailbox/prune.mjs +106 -0
  76. package/mailbox/rate.mjs +86 -0
  77. package/mailbox/settings.mjs +53 -0
  78. package/mailbox/store.mjs +256 -0
  79. package/mailbox/tunables.json +12 -0
  80. package/mailbox/wire.mjs +40 -0
  81. package/node/denylist.mjs +309 -0
  82. package/node/entity_store.mjs +214 -0
  83. package/node/identity.mjs +99 -0
  84. package/node/instance.mjs +115 -0
  85. package/node/network.mjs +306 -0
  86. package/node/personal_block.mjs +286 -0
  87. package/node/reputation_store.mjs +326 -0
  88. package/node/retention_policy.mjs +82 -0
  89. package/node/signaling_config.mjs +32 -0
  90. package/node/storage.mjs +30 -0
  91. package/node/storage_plugins.mjs +71 -0
  92. package/node/user_paths.mjs +10 -0
  93. package/overlay/index.mjs +191 -0
  94. package/package.json +73 -0
  95. package/permissions/bitmask.mjs +35 -0
  96. package/permissions/evaluator.mjs +90 -0
  97. package/permissions/index.mjs +12 -0
  98. package/permissions/layered.mjs +39 -0
  99. package/registries/action.mjs +94 -0
  100. package/registries/event_type.mjs +75 -0
  101. package/registries/inbound.mjs +90 -0
  102. package/registries/p2p_viewer.mjs +35 -0
  103. package/registries/part_path.mjs +35 -0
  104. package/registries/room_provider.mjs +47 -0
  105. package/reputation/engine.mjs +458 -0
  106. package/reputation/math.mjs +73 -0
  107. package/reputation/pick_score.mjs +11 -0
  108. package/reputation/relay_dedupe.mjs +22 -0
  109. package/reputation/tunables.json +36 -0
  110. package/rooms/scoped_link.mjs +185 -0
  111. package/schemas/discovery.mjs +110 -0
  112. package/schemas/federation_pull.mjs +102 -0
  113. package/schemas/mailbox.mjs +37 -0
  114. package/schemas/remote_event.mjs +17 -0
  115. package/timeline/append_core.mjs +54 -0
  116. package/timeline/materialize_runner.mjs +26 -0
  117. package/timeline/prune.mjs +39 -0
  118. package/timeline/retention.mjs +54 -0
  119. package/timeline/retention_runner.mjs +35 -0
  120. package/timeline/verify_remote.mjs +25 -0
  121. package/transport/group_link_set.mjs +324 -0
  122. package/transport/ice_servers.mjs +72 -0
  123. package/transport/link_registry.mjs +749 -0
  124. package/transport/peer_identity_maps.mjs +94 -0
  125. package/transport/peer_pool.mjs +355 -0
  126. package/transport/remote_user_room.mjs +109 -0
  127. package/transport/room_scopes.mjs +2 -0
  128. package/transport/room_wire_action.mjs +19 -0
  129. package/transport/rtc_connection_budget.mjs +180 -0
  130. package/transport/rtc_mdns_filter.mjs +118 -0
  131. package/transport/stale_peer_log.mjs +50 -0
  132. package/transport/user_room.mjs +263 -0
  133. package/trust_graph/build.mjs +99 -0
  134. package/trust_graph/cache.mjs +44 -0
  135. package/trust_graph/engine.mjs +184 -0
  136. package/trust_graph/registry.mjs +38 -0
  137. package/trust_graph/resolve.mjs +105 -0
  138. package/trust_graph/send.mjs +88 -0
  139. package/trust_graph/tunables.json +13 -0
  140. package/utils/async_mutex.mjs +69 -0
  141. package/utils/debug_log.mjs +16 -0
  142. package/utils/duration.mjs +28 -0
  143. package/utils/json_io.mjs +57 -0
  144. package/utils/lru.mjs +22 -0
  145. package/utils/map_pool.mjs +27 -0
  146. package/wire/group_part.mjs +43 -0
  147. package/wire/ingress.mjs +58 -0
  148. package/wire/part_common.mjs +55 -0
  149. package/wire/part_fanout.mjs +71 -0
  150. package/wire/part_ingress.mjs +172 -0
  151. package/wire/part_invoke.mjs +81 -0
  152. package/wire/rate_bucket.mjs +49 -0
  153. package/wire/volatile_signature.mjs +76 -0
  154. package/wire/wait.mjs +127 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * 按在线 peer 集合将 roster 分为 live 与 stale。
3
+ * @param {Array<{ peerId: string, remoteNodeHash?: string }>} rosterEntries roster 条目
4
+ * @param {Iterable<string>} livePeerIds 当前在线 peer id 集合
5
+ * @returns {{ live: Array<{ peerId: string, remoteNodeHash?: string }>, stale: Array<{ peerId: string, remoteNodeHash?: string }> }} 分组结果
6
+ */
7
+ export function partitionRosterByLiveness(rosterEntries, livePeerIds) {
8
+ const liveSet = livePeerIds instanceof Set ? livePeerIds : new Set(livePeerIds)
9
+ /** @type {Array<{ peerId: string, remoteNodeHash?: string }>} */
10
+ const live = []
11
+ /** @type {Array<{ peerId: string, remoteNodeHash?: string }>} */
12
+ const stale = []
13
+ for (const entry of rosterEntries) {
14
+ if (!entry?.peerId) continue
15
+ const bucket = liveSet.has(entry.peerId) ? live : stale
16
+ bucket.push(entry)
17
+ }
18
+ return { live, stale }
19
+ }
20
+
21
+ /**
22
+ * 从 peer↔node 映射中移除离线条目。
23
+ * @param {Map<string, string>} peerToNode 对端 id → nodeHash
24
+ * @param {Map<string, string>} nodeToPeer nodeHash → 对端 id
25
+ * @param {Iterable<string>} livePeerIds 当前在线 peer id 集合
26
+ * @returns {Array<{ peerId: string, remoteNodeHash?: string }>} 被移除的 stale 条目
27
+ */
28
+ export function pruneStaleRosterEntries(peerToNode, nodeToPeer, livePeerIds) {
29
+ const entries = [...peerToNode.entries()].map(([peerId, remoteNodeHash]) => ({ peerId, remoteNodeHash }))
30
+ const { stale } = partitionRosterByLiveness(entries, livePeerIds)
31
+ for (const { peerId, remoteNodeHash } of stale) {
32
+ peerToNode.delete(peerId)
33
+ if (remoteNodeHash && nodeToPeer.get(remoteNodeHash) === peerId) nodeToPeer.delete(remoteNodeHash)
34
+ }
35
+ return stale
36
+ }
37
+
38
+ /**
39
+ * 创建 peer id 与 nodeHash 双向映射及 roster 查询辅助。
40
+ * @param {object} [opts] 选项
41
+ * @param {() => Iterable<string>} [opts.getLivePeerIds] 获取当前在线 peer id 的回调
42
+ * @param {(stale: Array<{ peerId: string, remoteNodeHash?: string }>) => void} [opts.onStalePruned] stale 条目被清理时的回调
43
+ * @returns {{ peerToNode: Map<string, string>, nodeToPeer: Map<string, string>, getRoster: () => Array<{ peerId: string, remoteNodeHash: string | undefined }>, getPeerIdByNodeHash: (nodeHash: string) => string | null, onPeerLeave: (peerId: string) => void }} 映射与查询接口
44
+ */
45
+ export function createPeerIdentityMaps(opts = {}) {
46
+ /** @type {Map<string, string>} */
47
+ const peerToNode = new Map()
48
+ /** @type {Map<string, string>} */
49
+ const nodeToPeer = new Map()
50
+ const getLivePeerIds = typeof opts.getLivePeerIds === 'function' ? opts.getLivePeerIds : null
51
+ const onStalePruned = typeof opts.onStalePruned === 'function' ? opts.onStalePruned : null
52
+
53
+ /**
54
+ * 根据 live peer 集合清理 stale 映射。
55
+ * @returns {void}
56
+ */
57
+ const reconcile = () => {
58
+ if (!getLivePeerIds) return
59
+ const stale = pruneStaleRosterEntries(peerToNode, nodeToPeer, getLivePeerIds())
60
+ if (stale.length && onStalePruned) onStalePruned(stale)
61
+ }
62
+
63
+ return {
64
+ peerToNode,
65
+ nodeToPeer,
66
+ /**
67
+ * 返回当前 roster(会先 reconcile stale 条目)。
68
+ * @returns {Array<{ peerId: string, remoteNodeHash: string | undefined }>} roster 列表
69
+ */
70
+ getRoster() {
71
+ reconcile()
72
+ return [...peerToNode.entries()].map(([peerId, remoteNodeHash]) => ({ peerId, remoteNodeHash }))
73
+ },
74
+ /**
75
+ * 按 nodeHash 查找对应 peer id。
76
+ * @param {string} targetNodeHash 目标节点 64 hex
77
+ * @returns {string | null} 对端 id;无映射时为 null
78
+ */
79
+ getPeerIdByNodeHash(targetNodeHash) {
80
+ reconcile()
81
+ return nodeToPeer.get(String(targetNodeHash).trim().toLowerCase()) || null
82
+ },
83
+ /**
84
+ * peer 离线时从映射中移除。
85
+ * @param {string} peerId 离线的 peer id
86
+ * @returns {void}
87
+ */
88
+ onPeerLeave(peerId) {
89
+ const remote = peerToNode.get(peerId)
90
+ if (remote) nodeToPeer.delete(remote)
91
+ peerToNode.delete(peerId)
92
+ },
93
+ }
94
+ }
@@ -0,0 +1,355 @@
1
+ /**
2
+ * 稀疏连接池纯计算逻辑(§0、§4):
3
+ * Top-K 信任连接 + M 随机探索,可被 chat shell 与 subfounts 共用。
4
+ * 不含文件 I/O;I/O 层由调用方注入。
5
+ */
6
+
7
+ import { loadPeerPoolView, mergeNetworkPeerPools } from '../node/network.mjs'
8
+ import { loadReputation } from '../node/reputation_store.mjs'
9
+ import { isQuarantinedPure } from '../reputation/engine.mjs'
10
+ import { clampReputationScore } from '../reputation/math.mjs'
11
+
12
+ /**
13
+ * 解析联邦池槽位参数(从 groupSettings 读取,含低功耗缩减)。
14
+ * @param {object | undefined} groupSettings 群设置
15
+ * @returns {{
16
+ * trustedSlots: number,
17
+ * exploreSlots: number,
18
+ * maxPeers: number,
19
+ * gossipTtl: number,
20
+ * wantIdsBudget: number,
21
+ * batterySaver: boolean,
22
+ * }} 解析后的联邦池参数
23
+ */
24
+ export function resolveFederationPoolLimits(groupSettings = {}) {
25
+ const battery = !!groupSettings.batterySaver
26
+ const trustedSlots = battery
27
+ ? 2
28
+ : Math.max(1, Math.min(32, Number(groupSettings.trustedPeerSlots) || 8))
29
+ const exploreSlots = battery
30
+ ? 1
31
+ : Math.max(0, Math.min(16, Number(groupSettings.explorePeerSlots) || 4))
32
+ const maxPeersRaw = Number(groupSettings.maxPeers)
33
+ const maxPeers = Number.isFinite(maxPeersRaw) && maxPeersRaw > 0
34
+ ? Math.min(64, Math.floor(maxPeersRaw))
35
+ : Math.min(64, Math.max(trustedSlots + exploreSlots, 24))
36
+ let trustedOut = trustedSlots
37
+ let exploreOut = exploreSlots
38
+ if (trustedOut + exploreOut > maxPeers) {
39
+ trustedOut = Math.min(trustedOut, maxPeers)
40
+ exploreOut = Math.min(exploreOut, Math.max(0, maxPeers - trustedOut))
41
+ }
42
+ const gossipTtl = Math.max(0, Math.min(8, Number.isFinite(Number(groupSettings.gossipTtl)) ? Number(groupSettings.gossipTtl) : 2))
43
+ const wantIdsBudget = Math.max(4, Math.min(128, Number(groupSettings.wantIdsBudget) || 16))
44
+ return {
45
+ trustedSlots: trustedOut,
46
+ exploreSlots: exploreOut,
47
+ maxPeers,
48
+ gossipTtl,
49
+ wantIdsBudget,
50
+ batterySaver: battery,
51
+ }
52
+ }
53
+
54
+ /**
55
+ * @param {string} nodeId 节点 id
56
+ * @param {{ byNodeHash?: Record<string, { score?: number }> }} rep 信誉表
57
+ * @returns {number} 排序分
58
+ */
59
+ function repScore(nodeId, rep) {
60
+ const score = Number(rep.byNodeHash?.[nodeId]?.score ?? 0)
61
+ return clampReputationScore(Number.isFinite(score) ? score : 0)
62
+ }
63
+
64
+ /** explore 选取时单 source 上限 */
65
+ export const EXPLORE_MAX_PER_SOURCE = 3
66
+
67
+ /**
68
+ * trusted 锚点优先保留,再按信誉填充剩余槽位。
69
+ * @param {string[]} existingTrusted 既有 trusted
70
+ * @param {string[]} rankedCandidates 信誉排序候选
71
+ * @param {ReturnType<typeof resolveFederationPoolLimits>} limits 槽位
72
+ * @param {string[]} [blockedPeers] 拉黑列表
73
+ * @returns {string[]} 新 trusted 列表
74
+ */
75
+ export function mergeTrustedWithAnchors(existingTrusted, rankedCandidates, limits, blockedPeers = []) {
76
+ const blocked = new Set(blockedPeers)
77
+ const candidateSet = new Set(rankedCandidates.filter(id => id && !blocked.has(id)))
78
+ const anchored = existingTrusted.filter(id => id && !blocked.has(id) && candidateSet.has(id))
79
+ const anchoredSet = new Set(anchored)
80
+ const fill = rankedCandidates.filter(id => id && !blocked.has(id) && !anchoredSet.has(id))
81
+ return [...anchored, ...fill].slice(0, limits.trustedSlots)
82
+ }
83
+
84
+ /**
85
+ * 按 source 轮询选取 explore,限制单源占比。
86
+ * @param {string[]} exploreIds 候选 nodeHash
87
+ * @param {Map<string, string> | undefined} exploreSources nodeHash → source
88
+ * @param {number} k 选取数量
89
+ * @param {number} [maxPerSource=EXPLORE_MAX_PER_SOURCE] 每源上限
90
+ * @returns {string[]} 选取结果
91
+ */
92
+ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxPerSource = EXPLORE_MAX_PER_SOURCE) {
93
+ 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
+ }
102
+ /** @type {Map<string, string[]>} */
103
+ const bySource = new Map()
104
+ 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)
108
+ }
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
+ }
114
+ const out = []
115
+ /** @type {Map<string, number>} */
116
+ const picked = new Map()
117
+ while (out.length < k) {
118
+ let progressed = false
119
+ for (const [src, ids] of bySource) {
120
+ 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)
125
+ progressed = true
126
+ }
127
+ if (!progressed) break
128
+ }
129
+ return out
130
+ }
131
+
132
+ /**
133
+ * 稀疏连接池纯选取:给定在线列表、已持久化 peers 状态与信誉表,
134
+ * 输出按 Top-K trusted + M random explore + 剩余按信誉补至 maxPeers 的 peerId 列表。
135
+ *
136
+ * @param {{
137
+ * roster: Array<{ peerId: string, remoteNodeHash?: string }>,
138
+ * peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
139
+ * rep: { byNodeHash?: Record<string, { score?: number }> },
140
+ * limits: ReturnType<typeof resolveFederationPoolLimits>,
141
+ * selfNodeHash: string,
142
+ * inRoomNodeHashes?: Set<string> | string[] 群内在线 node_id;有则优先,仅全不可达时用 explore 中非房内节点
143
+ * hintSources?: Map<string, string> explore 节点来源(用于配额)
144
+ * }} opts 选取参数(roster、peers、rep、limits、selfNodeHash)
145
+ * @returns {string[]} 目标 Trystero peerId 列表(去重,长度 ≤ maxPeers)
146
+ */
147
+ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash, inRoomNodeHashes, hintSources }) {
148
+ const blocked = new Set(peers.blockedPeers)
149
+ const roomSet = inRoomNodeHashes instanceof Set
150
+ ? inRoomNodeHashes
151
+ : new Set(Array.isArray(inRoomNodeHashes) ? inRoomNodeHashes : [])
152
+ const onlineAll = roster.filter(
153
+ rosterEntry => rosterEntry.peerId
154
+ && rosterEntry.remoteNodeHash
155
+ && rosterEntry.remoteNodeHash !== selfNodeHash
156
+ && !blocked.has(rosterEntry.remoteNodeHash),
157
+ )
158
+ const onlineInRoom = roomSet.size
159
+ ? onlineAll.filter(rosterEntry => roomSet.has(rosterEntry.remoteNodeHash))
160
+ : onlineAll
161
+ const online = onlineInRoom.length ? onlineInRoom : onlineAll
162
+ if (!online.length) return []
163
+
164
+ const peerIdByNodeHash = new Map(online.map(rosterEntry => [rosterEntry.remoteNodeHash, rosterEntry.peerId]))
165
+ const trustedSet = new Set(peers.trustedPeers.filter(nodeHash => peerIdByNodeHash.has(nodeHash)))
166
+ const exploreSet = new Set(peers.explorePeers.filter(nodeHash => peerIdByNodeHash.has(nodeHash) && !trustedSet.has(nodeHash)))
167
+
168
+ const outPeerIds = new Set()
169
+ /**
170
+ * @param {string} nodeHash 远端节点 hash
171
+ */
172
+ const pushNode = nodeHash => {
173
+ const peerId = peerIdByNodeHash.get(nodeHash)
174
+ if (peerId) outPeerIds.add(peerId)
175
+ }
176
+
177
+ 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)) {
182
+ if (outPeerIds.size >= limits.maxPeers) break
183
+ pushNode(nodeId)
184
+ }
185
+
186
+ const exploreArray = [...exploreSet]
187
+ for (const nodeId of selectExploreWithSourceQuota(exploreArray, hintSources, limits.exploreSlots)) {
188
+ if (outPeerIds.size >= limits.maxPeers) break
189
+ pushNode(nodeId)
190
+ }
191
+
192
+ const remainingNodeHashes = [...peerIdByNodeHash.keys()]
193
+ .filter(nodeHash => !trustedSet.has(nodeHash) && !exploreSet.has(nodeHash))
194
+ .sort((a, b) => repScore(b, rep) - repScore(a, rep))
195
+ for (const nodeHash of remainingNodeHashes) {
196
+ if (outPeerIds.size >= limits.maxPeers) break
197
+ pushNode(nodeHash)
198
+ }
199
+
200
+ return [...outPeerIds].slice(0, limits.maxPeers)
201
+ }
202
+
203
+ /**
204
+ * 从群成员集合选出应主动建链的 nodeHash(top-K 信任 + M 随机 explore + 强制锚点必连)。
205
+ * 与 selectPeerIdsFromPool 不同:候选是"已知成员 nodeHash"(未必在线),输出用于 ensureLinkToNode 的 nodeHash。
206
+ * 这让大群不再全网状 autoconnect,而是每节点只连少数信任节点 + 随机若干条以保图连通。
207
+ *
208
+ * @param {{
209
+ * members: Iterable<string>,
210
+ * selfNodeHash: string,
211
+ * rep: { byNodeHash?: Record<string, { score?: number, quarantinedUntil?: number }> },
212
+ * peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[], hintSources?: Map<string, string> },
213
+ * limits: ReturnType<typeof resolveFederationPoolLimits>,
214
+ * anchors?: Iterable<string>,
215
+ * }} opts 选取参数(members、selfNodeHash、rep、peers、limits、anchors)
216
+ * @returns {string[]} 应建链的 nodeHash 列表(去重)
217
+ */
218
+ export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers, limits, anchors = [] }) {
219
+ const self = String(selfNodeHash || '')
220
+ const blocked = new Set(peers?.blockedPeers || [])
221
+ 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))
224
+ const ranked = candidates.slice().sort((a, b) => repScore(b, rep) - repScore(a, rep))
225
+ const candidateSet = new Set(candidates)
226
+ // 锚点(如 introducer/creator/seed)必连、且不占 trustedSlots——保证引导期连通。
227
+ const forced = [...new Set([...anchors].map(id => String(id)))].filter(id => candidateSet.has(id))
228
+ const chosen = new Set(forced)
229
+ // trusted 槽只从非锚点候选填:既有 trusted 优先保留,再按信誉补至 trustedSlots。
230
+ const nonForced = ranked.filter(id => !chosen.has(id))
231
+ for (const id of mergeTrustedWithAnchors(peers?.trustedPeers || [], nonForced, limits))
232
+ chosen.add(id)
233
+ const remaining = ranked.filter(id => !chosen.has(id))
234
+ for (const id of selectExploreWithSourceQuota(remaining, peers?.hintSources, limits.exploreSlots))
235
+ chosen.add(id)
236
+ return [...chosen]
237
+ }
238
+
239
+ /**
240
+ * 将新 PEX 节点线索合并进 explore 池,并按信誉重新填充 trusted 槽。
241
+ * 纯计算版本,不含 I/O:接受 peers 对象,返回新对象。
242
+ *
243
+ * @param {{
244
+ * peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
245
+ * rep: { byNodeHash?: Record<string, { score?: number }> },
246
+ * hints: string[],
247
+ * limits: ReturnType<typeof resolveFederationPoolLimits>,
248
+ * }} opts 合并参数(peers、rep、hints、limits)
249
+ * @returns {{ trustedPeers: string[], explorePeers: string[] }} 更新后的 trusted/explore 列表
250
+ */
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
+ )]
257
+ 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)
261
+ const ranked = [...new Set([...peers.trustedPeers, ...newExplorePeers])]
262
+ .filter(id => !peers.blockedPeers.includes(id))
263
+ .sort((a, b) => repScore(b, rep) - repScore(a, rep))
264
+ return {
265
+ trustedPeers: mergeTrustedWithAnchors(peers.trustedPeers, ranked, limits, peers.blockedPeers),
266
+ explorePeers: newExplorePeers,
267
+ }
268
+ }
269
+
270
+ /**
271
+ * 从 roster 观测中更新 explore 池并重新填充 trusted 槽(纯计算版本)。
272
+ *
273
+ * @param {{
274
+ * peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
275
+ * rep: { byNodeHash?: Record<string, { score?: number }> },
276
+ * roster: Array<{ remoteNodeHash?: string }>,
277
+ * limits: ReturnType<typeof resolveFederationPoolLimits>,
278
+ * }} opts roster 更新参数(peers、rep、roster、limits)
279
+ * @returns {{ trustedPeers: string[], explorePeers: string[] }} 更新后的 trusted/explore 列表
280
+ */
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
+ }
297
+ }
298
+
299
+ /**
300
+ * 稀疏连接池:优先 trusted,再 explore,再其余在线节点。
301
+ * @param {string} groupId 群
302
+ * @param {{ peerId: string, remoteNodeHash?: string }[]} roster Trystero 在线表
303
+ * @param {object} groupSettings 物化群设置
304
+ * @param {string} selfNodeHash 本机 node_id
305
+ * @returns {string[]} 目标 Trystero peerId(去重)
306
+ */
307
+ export function pickFederationTargetPeerIds(groupId, roster, groupSettings, selfNodeHash) {
308
+ const limits = resolveFederationPoolLimits(groupSettings)
309
+ const peers = loadPeerPoolView(groupId)
310
+ const rep = loadReputation()
311
+ const inRoomNodeHashes = roster
312
+ .map(p => p.remoteNodeHash)
313
+ .map(id => String(id).trim())
314
+ .filter(Boolean)
315
+ return selectPeerIdsFromPool({
316
+ roster,
317
+ peers,
318
+ rep,
319
+ limits,
320
+ selfNodeHash,
321
+ inRoomNodeHashes,
322
+ hintSources: peers.hintSources,
323
+ })
324
+ }
325
+
326
+ /**
327
+ * 合并 PEX 提示并提升长期高信誉节点为 trusted。
328
+ * @param {string} groupId 群
329
+ * @param {string[]} hints 节点 id 列表
330
+ * @param {object} groupSettings 群设置
331
+ * @returns {void}
332
+ */
333
+ export function mergePexNodeHints(groupId, hints, groupSettings) {
334
+ const limits = resolveFederationPoolLimits(groupSettings)
335
+ const peers = loadPeerPoolView(groupId)
336
+ const rep = loadReputation()
337
+ const { trustedPeers, explorePeers } = applyPexHints({ peers, rep, hints, limits })
338
+ mergeNetworkPeerPools({ trustedPeers, explorePeers })
339
+ }
340
+
341
+ /**
342
+ * roster 观测:将在线节点并入 explore,并按信誉填充 trusted 槽位。
343
+ * @param {string} groupId 群
344
+ * @param {{ remoteNodeHash?: string }[]} roster 在线表
345
+ * @param {object} groupSettings 群设置
346
+ * @returns {void}
347
+ */
348
+ export function reconcilePeerPoolFromRoster(groupId, roster, groupSettings) {
349
+ if (!roster.length) return
350
+ const limits = resolveFederationPoolLimits(groupSettings)
351
+ const peers = loadPeerPoolView(groupId)
352
+ const rep = loadReputation()
353
+ const { trustedPeers, explorePeers } = applyRosterToPeerPool({ peers, rep, roster, limits })
354
+ mergeNetworkPeerPools({ trustedPeers, explorePeers })
355
+ }
@@ -0,0 +1,109 @@
1
+ import { registerFederationRoomProvider } from '../registries/room_provider.mjs'
2
+ import { invalidateTrustGraphCache } from '../trust_graph/cache.mjs'
3
+
4
+ import { closeLink, ensureLinkToNode, getLink } from './link_registry.mjs'
5
+ import { USER_ROOM_SCOPE } from './room_scopes.mjs'
6
+
7
+
8
+ /**
9
+ * @typedef {{
10
+ * roomSlot: import('../registries/room_provider.mjs').FederationRoomSlot
11
+ * leave: () => void | Promise<void>
12
+ * }} RemoteUserRoomSlot
13
+ */
14
+
15
+ /** @type {Map<string, RemoteUserRoomSlot | null>} nodeHash → slot(null 表示加入失败/进行中) */
16
+ const slots = new Map()
17
+ /** @type {Map<string, Promise<RemoteUserRoomSlot | null>>} nodeHash → 进行中的 promise */
18
+ const inflights = new Map()
19
+
20
+ registerFederationRoomProvider('remote-user-room', () => {
21
+ return [...slots.values()]
22
+ .filter(Boolean)
23
+ .map(s => s.roomSlot)
24
+ })
25
+
26
+ /**
27
+ * 加入目标节点的用户房间(幂等)。
28
+ * @param {string} username 本地 replica 用户名
29
+ * @param {string} targetNodeHash 目标节点 64 hex
30
+ * @returns {Promise<RemoteUserRoomSlot | null>} 房间槽
31
+ */
32
+ export async function ensureRemoteUserRoom(username, targetNodeHash) {
33
+ void username
34
+ 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
38
+
39
+ const task = (async () => {
40
+ try {
41
+ if (!await ensureLinkToNode(key)) {
42
+ slots.set(key, null)
43
+ return null
44
+ }
45
+
46
+ /** @type {import('../registries/room_provider.mjs').FederationRoomSlot} */
47
+ const roomSlot = {
48
+ groupId: USER_ROOM_SCOPE,
49
+ /**
50
+ * 返回远端用户房间 roster(链路存在时含目标节点)。
51
+ * @returns {Array<{ peerId: string, remoteNodeHash: string }>} roster 列表
52
+ */
53
+ getRoster: () => getLink(key) ? [{ peerId: key, remoteNodeHash: key }] : [],
54
+ /**
55
+ * 按 nodeHash 查找 peer id。
56
+ * @param {string} nh 目标节点 64 hex
57
+ * @returns {string | null} 对端 id;无链路时为 null
58
+ */
59
+ getPeerIdByNodeHash: nh => getLink(nh) ? String(nh) : null,
60
+ /**
61
+ * 经 node scope 向远端 peer 发送 action。始终走当前规范链路(`getLink`),
62
+ * 因为 glare 双 PC 择一后最初返回的链路可能已被关闭,规范链在 registry 内。
63
+ * @param {string} peerId 目标 peer id
64
+ * @param {string} actionName action 名称
65
+ * @param {unknown} payload 载荷
66
+ * @returns {void}
67
+ */
68
+ sendToPeer(peerId, actionName, payload) {
69
+ void getLink(key)?.send({ scope: 'node', action: String(actionName), payload }).catch(() => { })
70
+ },
71
+ }
72
+
73
+ const slot = {
74
+ roomSlot,
75
+ /**
76
+ * 关闭远端用户房间链路。
77
+ * @returns {Promise<void>} 关闭完成
78
+ */
79
+ leave() { return closeLink(key, 'remote-user-room-release') },
80
+ }
81
+ slots.set(key, slot)
82
+ invalidateTrustGraphCache()
83
+ return slot
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
+ finally {
91
+ inflights.delete(key)
92
+ }
93
+ })()
94
+
95
+ inflights.set(key, task)
96
+ return await task
97
+ }
98
+
99
+ /**
100
+ * 释放目标节点的远端用户房间连接。
101
+ * @param {string} targetNodeHash 目标节点 64 hex
102
+ * @returns {void}
103
+ */
104
+ export function releaseRemoteUserRoom(targetNodeHash) {
105
+ const key = targetNodeHash.toLowerCase()
106
+ const slot = slots.get(key)
107
+ if (slot) void Promise.resolve(slot.leave()).catch(() => { })
108
+ slots.delete(key)
109
+ }
@@ -0,0 +1,2 @@
1
+ /** user room 在 trust graph / room provider 中的 scope id */
2
+ export const USER_ROOM_SCOPE = 'user-room'
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 将 room 的 wire action 绑定到 senderRegistry。
3
+ * @param {object} roomContext 房间上下文(含 wireActions 与 senderRegistry)
4
+ * @param {string} name action 名称
5
+ * @returns {{ send: Function, on: (handler: Function) => void }} 发送函数与注册回调
6
+ */
7
+ export function wireAction(roomContext, name) {
8
+ const send = roomContext.wireActions.sender(name)
9
+ roomContext.senderRegistry.set(name, send)
10
+ return {
11
+ send,
12
+ /**
13
+ * 注册该 action 的入站 handler。
14
+ * @param {Function} handler 入站回调
15
+ * @returns {void}
16
+ */
17
+ on: handler => { roomContext.wireActions.on(name, handler) },
18
+ }
19
+ }