@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,180 @@
1
+ /**
2
+ * Trystero/WebRTC 连接预算(进程内)+ 单源槽位配额与 trusted 保留。
3
+ */
4
+
5
+ /** @type {Map<string, { active: Set<string>, joinTimestamps: number[], overloadUntil: number, sourceByPeer: Map<string, string>, trustedPeers: Set<string>, peerNodeHash: Map<string, string> }>} */
6
+ const budgets = new Map()
7
+
8
+ /** 单来源最多占用的非 trusted 槽位比例 */
9
+ export const MAX_SOURCE_SLOT_FRACTION = 0.25
10
+
11
+ /** 默认 trusted 保留比例 */
12
+ const DEFAULT_TRUSTED_RESERVE_FRACTION = 0.25
13
+
14
+ /** 默认 trusted 绝对保留槽位 */
15
+ const DEFAULT_MIN_TRUSTED_RESERVED = 3
16
+
17
+ /**
18
+ * @param {object} [limits] 限额
19
+ * @returns {{ maxActive: number, maxJoinsPerMin: number, overloadCooldownMs: number, trustedPeers?: string[], trustedReserveFraction: number, minTrustedReserved: number }} 生效限额
20
+ */
21
+ export function resolveRtcBudgetLimits(limits = {}) {
22
+ return {
23
+ maxActive: Math.max(4, Math.min(128, Number(limits.maxActive) || 32)),
24
+ maxJoinsPerMin: Math.max(1, Math.min(120, Number(limits.maxJoinsPerMin) || 12)),
25
+ overloadCooldownMs: Math.max(1000, Number(limits.overloadCooldownMs) || 15_000),
26
+ trustedPeers: Array.isArray(limits.trustedPeers) ? limits.trustedPeers.map(String) : [],
27
+ trustedReserveFraction: Math.max(0.1, Math.min(0.5, Number(limits.trustedReserveFraction) || DEFAULT_TRUSTED_RESERVE_FRACTION)),
28
+ minTrustedReserved: Math.max(1, Math.floor(Number(limits.minTrustedReserved) || DEFAULT_MIN_TRUSTED_RESERVED)),
29
+ }
30
+ }
31
+
32
+ /**
33
+ * @param {string} roomKey 房间键
34
+ * @param {object} [limits] RTC 限额
35
+ * @returns {{ active: Set<string>, joinTimestamps: number[], overloadUntil: number, sourceByPeer: Map<string, string>, trustedPeers: Set<string> }} 桶
36
+ */
37
+ function bucketFor(roomKey, limits = {}) {
38
+ let bucket = budgets.get(roomKey)
39
+ if (!bucket) {
40
+ bucket = {
41
+ active: new Set(),
42
+ joinTimestamps: [],
43
+ overloadUntil: 0,
44
+ sourceByPeer: new Map(),
45
+ trustedPeers: new Set(resolveRtcBudgetLimits(limits).trustedPeers || []),
46
+ peerNodeHash: new Map(),
47
+ }
48
+ budgets.set(roomKey, bucket)
49
+ }
50
+ for (const id of resolveRtcBudgetLimits(limits).trustedPeers || [])
51
+ bucket.trustedPeers.add(String(id))
52
+ return bucket
53
+ }
54
+
55
+ /**
56
+ * @param {string} roomKey 房间键
57
+ * @param {object} [limits] 限额
58
+ * @returns {boolean} 是否处于过载冷却
59
+ */
60
+ export function isRtcRoomOverloaded(roomKey, limits = {}) {
61
+ const { overloadCooldownMs } = resolveRtcBudgetLimits(limits)
62
+ const bucket = bucketFor(roomKey, limits)
63
+ return Date.now() < bucket.overloadUntil
64
+ }
65
+
66
+ /**
67
+ * @param {string} roomKey 房间键
68
+ * @param {string} peerId 对等端 id
69
+ * @param {object} [limits] 限额
70
+ * @param {string} [sourceId='peer'] 来源标识(用于单源配额)
71
+ * @returns {boolean} 是否允许新 join/握手
72
+ */
73
+ export function takeRtcJoinSlot(roomKey, peerId, limits = {}, sourceId = 'peer') {
74
+ const { maxActive, maxJoinsPerMin, overloadCooldownMs, trustedReserveFraction, minTrustedReserved } = resolveRtcBudgetLimits(limits)
75
+ const bucket = bucketFor(roomKey, limits)
76
+ const now = Date.now()
77
+ if (now < bucket.overloadUntil) return false
78
+ bucket.joinTimestamps = bucket.joinTimestamps.filter(t => now - t < 60_000)
79
+ if (bucket.joinTimestamps.length >= maxJoinsPerMin) {
80
+ bucket.overloadUntil = now + overloadCooldownMs
81
+ return false
82
+ }
83
+ if (peerId && bucket.active.has(peerId)) return true
84
+
85
+ const isTrusted = peerId && bucket.trustedPeers.has(peerId)
86
+ const trustedReserved = Math.max(minTrustedReserved, Math.floor(maxActive * trustedReserveFraction))
87
+ const maxNonTrusted = Math.max(1, maxActive - trustedReserved)
88
+ const nonTrustedCount = [...bucket.active].filter(id => !bucket.trustedPeers.has(id)).length
89
+ if (!isTrusted) {
90
+ const source = String(sourceId || 'peer')
91
+ const sameSource = [...bucket.sourceByPeer.entries()].filter(([, s]) => s === source).length
92
+ const sourceCap = Math.max(1, Math.floor(maxActive * MAX_SOURCE_SLOT_FRACTION))
93
+ if (sameSource >= sourceCap) return false
94
+ if (nonTrustedCount >= maxNonTrusted && bucket.active.size >= maxActive) {
95
+ bucket.overloadUntil = now + overloadCooldownMs
96
+ return false
97
+ }
98
+ }
99
+
100
+ if (bucket.active.size >= maxActive && !isTrusted) {
101
+ bucket.overloadUntil = now + overloadCooldownMs
102
+ return false
103
+ }
104
+ bucket.joinTimestamps.push(now)
105
+ if (peerId) {
106
+ bucket.active.add(peerId)
107
+ bucket.sourceByPeer.set(peerId, String(sourceId || 'peer'))
108
+ }
109
+ return true
110
+ }
111
+
112
+ /**
113
+ * @param {string} roomKey 房间键
114
+ * @param {string} peerId Trystero 对端 id
115
+ * @param {string} nodeHash 对等 nodeHash
116
+ * @param {object} [limits] 限额(含 trustedPeers nodeHash 列表)
117
+ * @returns {void}
118
+ */
119
+ export function annotateRtcPeerNodeHash(roomKey, peerId, nodeHash, limits = {}) {
120
+ const bucket = budgets.get(roomKey)
121
+ if (!bucket || !peerId || !nodeHash) return
122
+ bucket.peerNodeHash.set(peerId, String(nodeHash).trim())
123
+ for (const trusted of resolveRtcBudgetLimits(limits).trustedPeers || [])
124
+ if (String(trusted).trim() === String(nodeHash).trim())
125
+ bucket.trustedPeers.add(peerId)
126
+
127
+ }
128
+
129
+ /**
130
+ * @param {string} roomKey 房间键
131
+ * @param {string} peerId Trystero 对端 id
132
+ * @param {string} sourceId 来源标识(PEX hint source / explore 源)
133
+ * @returns {void}
134
+ */
135
+ export function setRtcPeerSource(roomKey, peerId, sourceId) {
136
+ const bucket = budgets.get(roomKey)
137
+ if (!bucket || !peerId) return
138
+ bucket.sourceByPeer.set(peerId, String(sourceId || 'peer'))
139
+ }
140
+
141
+ /**
142
+ * @param {string} roomKey 房间键
143
+ * @param {string} peerId 对等端 id
144
+ * @returns {void}
145
+ */
146
+ export function releaseRtcPeer(roomKey, peerId) {
147
+ const bucket = budgets.get(roomKey)
148
+ if (!bucket || !peerId) return
149
+ bucket.active.delete(peerId)
150
+ bucket.sourceByPeer.delete(peerId)
151
+ bucket.peerNodeHash.delete(peerId)
152
+ }
153
+
154
+ /** RTC 过载时跳过的非关键联邦 action */
155
+ const NON_CRITICAL_FED_ACTIONS = new Set([
156
+ 'fed_pex',
157
+ 'fed_partition_bridge',
158
+ 'fed_chunk_put',
159
+ 'fed_chunk_get',
160
+ 'fed_chunk_data',
161
+ 'fed_chunk_ack',
162
+ 'part_invoke',
163
+ 'discovery_announce',
164
+ 'discovery_query',
165
+ 'char_rpc',
166
+ 'fed_volatile',
167
+ 'fed_tip_ping',
168
+ 'fed_archive_digest_obs',
169
+ ])
170
+
171
+ /**
172
+ * @param {string} roomKey 房间键
173
+ * @param {string} actionName Trystero 动作
174
+ * @param {object} [limits] 限额
175
+ * @returns {boolean} 是否允许处理/发送该 action
176
+ */
177
+ export function isFederationActionAllowedUnderLoad(roomKey, actionName, limits = {}) {
178
+ if (!isRtcRoomOverloaded(roomKey, limits)) return true
179
+ return !NON_CRITICAL_FED_ACTIONS.has(String(actionName || '').trim())
180
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * 服务端 WebRTC polyfill(werift / node-datachannel)在 Windows 等环境常产出 `.local` mDNS host candidate,
3
+ * 远端无法解析。按策略改写为 loopback 或丢弃。
4
+ */
5
+
6
+ /** @typedef {'none' | 'rewrite-loopback' | 'drop'} MdnsCandidatePolicy */
7
+
8
+ /**
9
+ * @param {string | null | undefined} candidateSdp ICE candidate SDP 行
10
+ * @param {MdnsCandidatePolicy} policy 处理策略
11
+ * @returns {string | null} 处理后的 SDP;drop 策略下不可用时返回 null
12
+ */
13
+ export function applyMdnsHostCandidatePolicy(candidateSdp, policy) {
14
+ const sdp = String(candidateSdp || '').trim()
15
+ if (!sdp || !/\.local/i.test(sdp)) return sdp || null
16
+ if (!/\btyp host\b/i.test(sdp)) return sdp
17
+ if (policy === 'drop') return null
18
+ if (policy === 'rewrite-loopback') {
19
+ const rewritten = sdp.replace(/(\s)[\w-]+\.local(\s|$)/gi, '$1127.0.0.1$2')
20
+ return rewritten === sdp ? null : rewritten
21
+ }
22
+ return sdp
23
+ }
24
+
25
+ /**
26
+ * @param {RTCIceCandidate | { candidate?: string } | null | undefined} candidate ICE candidate
27
+ * @param {typeof RTCIceCandidate} RTCIceCandidateCtor 构造函数
28
+ * @param {MdnsCandidatePolicy} policy 处理策略
29
+ * @returns {RTCIceCandidate | { candidate?: string } | null | undefined} 过滤/改写后的 candidate
30
+ */
31
+ export function filterMdnsIceCandidate(candidate, RTCIceCandidateCtor, policy) {
32
+ if (!candidate || policy === 'none') return candidate
33
+ const raw = typeof candidate === 'string'
34
+ ? candidate
35
+ : candidate.candidate ?? candidate.toJSON?.()?.candidate ?? ''
36
+ const rewritten = applyMdnsHostCandidatePolicy(raw, policy)
37
+ if (!rewritten) return null
38
+ if (rewritten === raw) return candidate
39
+ try {
40
+ const init = typeof candidate.toJSON === 'function'
41
+ ? { ...candidate.toJSON(), candidate: rewritten }
42
+ : { candidate: rewritten, sdpMid: candidate.sdpMid, sdpMLineIndex: candidate.sdpMLineIndex }
43
+ return new RTCIceCandidateCtor(init)
44
+ }
45
+ catch {
46
+ return { ...candidate, candidate: rewritten }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * @param {typeof RTCPeerConnection} BaseRTC 原始 polyfill 类
52
+ * @param {typeof RTCIceCandidate} [RTCIceCandidate] ICE candidate 构造函数
53
+ * @param {MdnsCandidatePolicy} [policy='drop'] mDNS 策略
54
+ * @returns {typeof RTCPeerConnection} 包装后的 RTCPeerConnection 类(none 时原样返回)
55
+ */
56
+ export function wrapRtcPeerConnectionForMdns(BaseRTC, RTCIceCandidate = globalThis.RTCIceCandidate, policy = 'drop') {
57
+ if (policy === 'none') return BaseRTC
58
+
59
+ /**
60
+ * @param {RTCPeerConnectionIceEvent} event ICE 候选事件
61
+ * @param {(event: RTCPeerConnectionIceEvent) => void} handler 用户 handler
62
+ * @returns {void}
63
+ */
64
+ function invokeFilteredIceHandler(event, handler) {
65
+ if (!event?.candidate) {
66
+ handler(event)
67
+ return
68
+ }
69
+ const filtered = filterMdnsIceCandidate(event.candidate, RTCIceCandidate, policy)
70
+ if (!filtered) return
71
+ if (filtered === event.candidate) {
72
+ handler(event)
73
+ return
74
+ }
75
+ handler({ ...event, candidate: filtered })
76
+ }
77
+
78
+ return class MdnsFilteredRTCPeerConnection extends BaseRTC {
79
+ /** @type {((event: RTCPeerConnectionIceEvent) => void) | null} */
80
+ #userIceHandler = null
81
+
82
+ /**
83
+ * @param {RTCConfiguration} [config] RTC 配置
84
+ */
85
+ constructor(config) {
86
+ super(config)
87
+ /** @param {RTCPeerConnectionIceEvent} event ICE 候选事件 */
88
+ const relayIce = event => {
89
+ if (this.#userIceHandler)
90
+ invokeFilteredIceHandler(event, this.#userIceHandler)
91
+ }
92
+ super.onicecandidate = relayIce
93
+ const iceObs = this.onIceCandidate
94
+ if (iceObs && typeof iceObs.subscribe === 'function')
95
+ iceObs.subscribe(candidate => {
96
+ if (!this.#userIceHandler) return
97
+ if (!candidate) {
98
+ this.#userIceHandler({ candidate: null })
99
+ return
100
+ }
101
+ const filtered = filterMdnsIceCandidate(candidate, RTCIceCandidate, policy)
102
+ if (filtered)
103
+ this.#userIceHandler({ candidate: filtered })
104
+ })
105
+
106
+ }
107
+
108
+ /** @returns {((event: RTCPeerConnectionIceEvent) => void) | null} 用户 ICE 处理器 */
109
+ get onicecandidate() {
110
+ return this.#userIceHandler
111
+ }
112
+
113
+ /** @param {((event: RTCPeerConnectionIceEvent) => void) | null} handler ICE 处理器 */
114
+ set onicecandidate(handler) {
115
+ this.#userIceHandler = handler
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 联邦/房间陈旧 peer 自愈修剪可观测性:进程内计数器 + 近期记录 + debug_logs。
3
+ *
4
+ * 无回退行为——仅在发生处记录异常「身份映射滞后于活跃连接」
5
+ * (群/房间 + peerId + nodeHash),以便追踪 onPeerLeave 遗漏。
6
+ * 计数通过 catchup stats 暴露给测试;磁盘记录可在 debug_logs/ 下 grep。
7
+ */
8
+ import { debugLog } from '../utils/debug_log.mjs'
9
+
10
+ /** @type {Map<string, number>} scopeId → 累计修剪次数 */
11
+ const pruneCounts = new Map()
12
+ /** @type {Array<{ ts: number, scope: string, peerId: string, nodeHash: string | null, meta?: object }>} */
13
+ const recent = []
14
+ const RECENT_CAP = 200
15
+
16
+ /**
17
+ * 记录一批陈旧 peer 修剪。
18
+ * @param {string} scope 计数器 scope(群 id / 房间标签)
19
+ * @param {Array<{ peerId: string, remoteNodeHash?: string }>} staleEntries 被修剪条目
20
+ * @param {object} [meta] 写入磁盘记录的额外上下文(partitionId / room)
21
+ * @returns {void}
22
+ */
23
+ export function recordStalePeerPrune(scope, staleEntries, meta = {}) {
24
+ if (!staleEntries?.length) return
25
+ pruneCounts.set(scope, (pruneCounts.get(scope) || 0) + staleEntries.length)
26
+ const ts = Date.now()
27
+ const lines = []
28
+ for (const { peerId, remoteNodeHash } of staleEntries) {
29
+ const record = { ts, scope, peerId, nodeHash: remoteNodeHash || null, ...meta }
30
+ recent.push(record)
31
+ lines.push(JSON.stringify(record))
32
+ }
33
+ while (recent.length > RECENT_CAP) recent.shift()
34
+ void debugLog('federation_stale_peer', `${lines.join('\n')}\n`).catch(() => { /* best-effort observability */ })
35
+ }
36
+
37
+ /**
38
+ * @param {string} scope 计数器 scope
39
+ * @returns {number} 该 scope 累计修剪次数
40
+ */
41
+ export function getStalePeerPruneCount(scope) {
42
+ return pruneCounts.get(scope) || 0
43
+ }
44
+
45
+ /**
46
+ * @returns {Array<{ ts: number, scope: string, peerId: string, nodeHash: string | null, meta?: object }>} 近期修剪记录
47
+ */
48
+ export function getRecentStalePeerPrunes() {
49
+ return [...recent]
50
+ }
@@ -0,0 +1,263 @@
1
+ import { createHash } from 'node:crypto'
2
+
3
+ import { attachMailboxWire } from '../mailbox/wire.mjs'
4
+ import { ensureNodeDefaults, getNodeHash } from '../node/identity.mjs'
5
+ import { registerFederationRoomProvider } from '../registries/room_provider.mjs'
6
+ import { attachPartWire } from '../wire/part_ingress.mjs'
7
+
8
+ import { listLinks, sendToNodeLink, subscribeScope, getLinkRegistry } from './link_registry.mjs'
9
+ import { USER_ROOM_SCOPE } from './room_scopes.mjs'
10
+
11
+ /**
12
+ * 在 node scope action 表上注册 fed_chunk_get / fed_chunk_data handler,
13
+ * 供用户房间(本地 + 远端)双向 chunk 传输使用。
14
+ * @param {string} username 副本用户名 用户名
15
+ * @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire action 表
16
+ * @returns {void}
17
+ */
18
+ function attachUserRoomChunkHandlers(username, wire) {
19
+ import('../files/chunk_responder.mjs').then(({ attachNodeScopeFedChunkResponder }) => {
20
+ attachNodeScopeFedChunkResponder(username, wire)
21
+ }).catch(error => console.error('p2p: failed to attach chunk handlers to node scope', error))
22
+ }
23
+
24
+ /** @type {Promise<UserRoomSlot | null> | null} */
25
+ let userRoomInflight = null
26
+
27
+ /** @type {UserRoomSlot | null} */
28
+ export let userRoomSlot = null
29
+
30
+ /** @type {Map<string, Set<(payload: unknown, peerId: string) => void>>} */
31
+ const nodeActionHandlers = new Map()
32
+ /** @type {Set<(username: string, wire: { on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }) => void>} */
33
+ const nodeScopeWireHooks = new Set()
34
+ /** @type {string} */
35
+ let nodeScopeReplicaUsername = ''
36
+ /** @type {ReturnType<typeof createNodeScopeWire> | null} */
37
+ let nodeScopeWire = null
38
+ let nodeScopeCleanup = null
39
+
40
+ /**
41
+ * Chat 等非 P2P 模块可向 node scope 注册 fed_emoji 等 handler(避免 p2p→shell 硬依赖)。
42
+ * @param {(username: string, wire: { on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }) => void} hook 注册回调
43
+ * @returns {() => void} 取消注册
44
+ */
45
+ export function registerUserRoomNodeScopeHook(hook) {
46
+ nodeScopeWireHooks.add(hook)
47
+ if (nodeScopeWire)
48
+ try { hook(nodeScopeReplicaUsername, nodeScopeWire) } catch { /* ignore */ }
49
+ return () => nodeScopeWireHooks.delete(hook)
50
+ }
51
+
52
+ /**
53
+ * 创建 node scope 的 on/send wire 表。
54
+ * @returns {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire 接口
55
+ */
56
+ function createNodeScopeWire() {
57
+ return {
58
+ /**
59
+ * 注册 node scope action handler。
60
+ * @param {string} name action 名称
61
+ * @param {(payload: unknown, peerId: string) => void} handler 入站回调
62
+ * @returns {void}
63
+ */
64
+ on(name, handler) {
65
+ const key = String(name)
66
+ if (!nodeActionHandlers.has(key)) nodeActionHandlers.set(key, new Set())
67
+ nodeActionHandlers.get(key).add(handler)
68
+ },
69
+ /**
70
+ * 向指定 peer 发送 node scope action。
71
+ * @param {string} name action 名称
72
+ * @param {unknown} payload 载荷
73
+ * @param {string | null} peerId 目标 peer id
74
+ * @returns {void}
75
+ */
76
+ send(name, payload, peerId) {
77
+ if (!peerId) return
78
+ void sendToNodeLink(peerId, { scope: 'node', action: String(name), payload }).catch(() => { })
79
+ },
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 返回当前所有活跃链路的 roster。
85
+ * @returns {Array<{ peerId: string, remoteNodeHash: string }>} 在线 peer 列表
86
+ */
87
+ function activeLinkRoster() {
88
+ return listLinks().map(({ nodeHash }) => ({ peerId: nodeHash, remoteNodeHash: nodeHash }))
89
+ }
90
+
91
+ /**
92
+ * 初始化 node scope 订阅与 wire 派发(幂等)。
93
+ * @param {{ replicaUsername?: string }} ctx 入站上下文
94
+ * @returns {Promise<void>}
95
+ */
96
+ async function ensureNodeScopeRuntime(ctx) {
97
+ if (nodeScopeCleanup) return
98
+ nodeScopeReplicaUsername = String(ctx.replicaUsername || nodeScopeReplicaUsername || '')
99
+ nodeScopeCleanup = subscribeScope('node', (senderNodeHash, envelope) => {
100
+ const handlers = nodeActionHandlers.get(String(envelope?.action || ''))
101
+ if (!handlers?.size) return
102
+ for (const handler of handlers)
103
+ try { handler(envelope.payload, senderNodeHash) } catch { /* ignore */ }
104
+ })
105
+ const wire = createNodeScopeWire()
106
+ nodeScopeWire = wire
107
+ attachPartWire({ replicaUsername: ctx.replicaUsername }, wire)
108
+ attachMailboxWire({ replicaUsername: ctx.replicaUsername }, wire)
109
+ attachUserRoomChunkHandlers(ctx.replicaUsername || '', wire)
110
+ for (const hook of nodeScopeWireHooks)
111
+ try { hook(ctx.replicaUsername || '', wire) } catch { /* ignore */ }
112
+ }
113
+
114
+ /**
115
+ * 用户级 node scope 房间(`fount-node-{nodeHash}`),单节点单实例。
116
+ *
117
+ * @typedef {{
118
+ * roomId: string
119
+ * roomSecret: string
120
+ * room: object
121
+ * sendToPeer: (peerId: string, actionName: string, payload: unknown) => void
122
+ * getRoster: () => Array<{ peerId: string, remoteNodeHash: string | undefined }>
123
+ * getPeerIdByNodeHash: (nodeHash: string) => string | null
124
+ * }} UserRoomSlot
125
+ */
126
+
127
+ registerFederationRoomProvider('user-room', () => {
128
+ if (!userRoomSlot) return []
129
+ return [{
130
+ groupId: USER_ROOM_SCOPE,
131
+ /** @returns {Array<{ peerId: string, remoteNodeHash: string | undefined }>} 房间 roster 列表 */
132
+ getRoster: () => userRoomSlot.getRoster(),
133
+ /**
134
+ * @param {string} nodeHash 64 位十六进制
135
+ * @returns {string | null} 对端 id
136
+ */
137
+ getPeerIdByNodeHash: nodeHash => userRoomSlot.getPeerIdByNodeHash(nodeHash),
138
+ /**
139
+ * @param {string} peerId 目标 peer
140
+ * @param {string} actionName 节点 scope action
141
+ * @param {unknown} payload 载荷
142
+ * @returns {void}
143
+ */
144
+ sendToPeer: (peerId, actionName, payload) => userRoomSlot.sendToPeer(peerId, actionName, payload),
145
+ }]
146
+ })
147
+
148
+ /**
149
+ * @returns {{ appId: string, password: string, roomId: string, nodeHash: string }} 用户房间参数
150
+ */
151
+ export function resolveUserRoomCredentials() {
152
+ const nodeHash = getNodeHash()
153
+ const password = createHash('sha256').update(`fount-user-room:${nodeHash}`).digest('hex')
154
+ return {
155
+ appId: 'fount-user-fed',
156
+ password,
157
+ roomId: `fount-node-${nodeHash}`,
158
+ nodeHash,
159
+ }
160
+ }
161
+
162
+ /**
163
+ * @param {{ replicaUsername?: string }} [ctx] 入站上下文(part/mailbox 派发用)
164
+ * @returns {Promise<UserRoomSlot | null>} 用户级联邦房间槽
165
+ */
166
+ export async function ensureUserRoom(ctx = {}) {
167
+ if (userRoomSlot) return userRoomSlot
168
+ if (userRoomInflight) return await userRoomInflight
169
+
170
+ userRoomInflight = (async () => {
171
+ ensureNodeDefaults()
172
+ try {
173
+ await getLinkRegistry().ensureRuntime()
174
+ await ensureNodeScopeRuntime(ctx)
175
+ const creds = resolveUserRoomCredentials()
176
+ /** @type {UserRoomSlot} */
177
+ userRoomSlot = {
178
+ roomId: creds.roomId,
179
+ roomSecret: creds.password,
180
+ room: null,
181
+ /**
182
+ * 经 node scope 向 peer 发送 action。
183
+ * @param {string} peerId 目标 peer id
184
+ * @param {string} actionName action 名称
185
+ * @param {unknown} payload 载荷
186
+ * @returns {void}
187
+ */
188
+ sendToPeer(peerId, actionName, payload) {
189
+ void sendToNodeLink(peerId, { scope: 'node', action: String(actionName), payload }).catch(() => { })
190
+ },
191
+ /**
192
+ * 返回当前活跃链路 roster。
193
+ * @returns {Array<{ peerId: string, remoteNodeHash: string }>} 在线 peer 列表
194
+ */
195
+ getRoster: () => activeLinkRoster(),
196
+ /**
197
+ * 按 nodeHash 查找 peer id。
198
+ * @param {string} nodeHash 目标节点 64 hex
199
+ * @returns {string | null} 对端 id;无链路时为 null
200
+ */
201
+ getPeerIdByNodeHash(nodeHash) {
202
+ return getLinkRegistry().getLink(nodeHash) ? String(nodeHash) : null
203
+ },
204
+ }
205
+ return userRoomSlot
206
+ }
207
+ catch (error) {
208
+ console.error('p2p: node scope init failed', error)
209
+ userRoomSlot = null
210
+ return null
211
+ }
212
+ finally {
213
+ userRoomInflight = null
214
+ }
215
+ })()
216
+
217
+ return await userRoomInflight
218
+ }
219
+
220
+ /**
221
+ * @returns {void}
222
+ */
223
+ export function invalidateUserRoom() {
224
+ userRoomSlot = null
225
+ userRoomInflight = null
226
+ nodeScopeWire = null
227
+ nodeScopeReplicaUsername = ''
228
+ nodeScopeCleanup = null
229
+ }
230
+
231
+ /**
232
+ * @param {string} username 副本用户名
233
+ * @param {string} actionName 节点 scope action
234
+ * @param {unknown} payload 载荷
235
+ * @param {string | null} [exceptPeerId] 跳过的 peer
236
+ * @param {number} [limit] 最多转发 peer 数
237
+ * @returns {Promise<number>} 实际转发的 peer 数
238
+ */
239
+ export async function deliverToUserRoomPeers(username, actionName, payload, exceptPeerId = null, limit) {
240
+ const { USER_ROOM_PEER_FANOUT_DEFAULT } = await import('../wire/part_common.mjs')
241
+ const fanoutLimit = limit ?? USER_ROOM_PEER_FANOUT_DEFAULT
242
+ const slot = await ensureUserRoom({ replicaUsername: username })
243
+ if (!slot) return 0
244
+ const body = { ...payload, nodeHash: getNodeHash() }
245
+ let sent = 0
246
+ const peers = [...slot.getRoster()
247
+ .filter(({ peerId }) => peerId && peerId !== exceptPeerId)]
248
+ for (let swapIndex = peers.length - 1; swapIndex > 0; swapIndex--) {
249
+ const pickIndex = Math.floor(Math.random() * (swapIndex + 1))
250
+ const tmp = peers[swapIndex]
251
+ peers[swapIndex] = peers[pickIndex]
252
+ peers[pickIndex] = tmp
253
+ }
254
+ for (const { peerId } of peers)
255
+ try {
256
+ if (await sendToNodeLink(peerId, { scope: 'node', action: String(actionName), payload: body }))
257
+ sent++
258
+ if (sent >= fanoutLimit) break
259
+ }
260
+ catch { /* disconnected */ }
261
+
262
+ return sent
263
+ }