@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.
- package/README.md +62 -0
- package/core/bytes_codec.mjs +51 -0
- package/core/canonical_json.mjs +21 -0
- package/core/composite_key.mjs +91 -0
- package/core/constants.mjs +53 -0
- package/core/entity_id.mjs +53 -0
- package/core/entity_id_parse.mjs +40 -0
- package/core/evfs_logical_path.mjs +15 -0
- package/core/hexIds.mjs +73 -0
- package/core/hlc.mjs +114 -0
- package/core/random_id.mjs +9 -0
- package/crypto/channel.mjs +101 -0
- package/crypto/checkpoint_sign.mjs +67 -0
- package/crypto/crypto.mjs +158 -0
- package/crypto/key.mjs +431 -0
- package/dag/canonicalize_row.mjs +65 -0
- package/dag/event_query.mjs +15 -0
- package/dag/index.mjs +335 -0
- package/dag/storage.mjs +333 -0
- package/dag/strip_extensions.mjs +18 -0
- package/dag/tunables.json +6 -0
- package/discovery/bt.mjs +260 -0
- package/discovery/index.mjs +134 -0
- package/discovery/mdns.mjs +138 -0
- package/discovery/nostr.mjs +273 -0
- package/entity/files/acl.mjs +38 -0
- package/entity/files/evfs.mjs +189 -0
- package/entity/files/evfs_ref.mjs +37 -0
- package/entity/files/manifest_acl_registry.mjs +93 -0
- package/entity/files/replica_host_cache.mjs +46 -0
- package/entity/files/url.mjs +19 -0
- package/entity/hosting_registry.mjs +47 -0
- package/entity/localized_core.mjs +141 -0
- package/entity/logical_entity.mjs +25 -0
- package/entity/logical_entity_id_registry.mjs +40 -0
- package/entity/node_hash.mjs +15 -0
- package/entity/presentation_registry.mjs +44 -0
- package/entity/profile.mjs +256 -0
- package/entity/replica.mjs +20 -0
- package/entity/session_snapshot_registry.mjs +38 -0
- package/federation/chunk_fetch_pending.mjs +128 -0
- package/federation/chunk_fetch_scheduler.mjs +97 -0
- package/federation/dag_order_cache.mjs +124 -0
- package/federation/dedupe_slot.mjs +33 -0
- package/federation/message_rate_limit.mjs +23 -0
- package/federation/operator_key_chain.mjs +200 -0
- package/federation/topo_order_memo.mjs +44 -0
- package/federation/volatile_streams.mjs +52 -0
- package/federation/want_ids.mjs +158 -0
- package/files/assemble.mjs +220 -0
- package/files/assemble_stream.mjs +129 -0
- package/files/chunk_fetch.mjs +113 -0
- package/files/chunk_fetch_verify.mjs +23 -0
- package/files/chunk_provider_registry.mjs +70 -0
- package/files/chunk_responder.mjs +79 -0
- package/files/chunk_store.mjs +87 -0
- package/files/manifest.mjs +99 -0
- package/files/transfer_key.mjs +87 -0
- package/files/transfer_key_registry.mjs +108 -0
- package/governance/branch.mjs +230 -0
- package/governance/join_pow.mjs +165 -0
- package/governance/owner_succession_ballot.mjs +57 -0
- package/governance/tunables.json +7 -0
- package/index.mjs +39 -0
- package/link/channel_mux.mjs +222 -0
- package/link/frame.mjs +246 -0
- package/link/handshake.mjs +175 -0
- package/link/link.mjs +617 -0
- package/link/rtc.mjs +109 -0
- package/link/sdp_fingerprint.mjs +25 -0
- package/mailbox/consumer_registry.mjs +71 -0
- package/mailbox/deliver_or_store.mjs +190 -0
- package/mailbox/importance.mjs +72 -0
- package/mailbox/parse.mjs +73 -0
- package/mailbox/prune.mjs +106 -0
- package/mailbox/rate.mjs +86 -0
- package/mailbox/settings.mjs +53 -0
- package/mailbox/store.mjs +256 -0
- package/mailbox/tunables.json +12 -0
- package/mailbox/wire.mjs +40 -0
- package/node/denylist.mjs +309 -0
- package/node/entity_store.mjs +214 -0
- package/node/identity.mjs +99 -0
- package/node/instance.mjs +115 -0
- package/node/network.mjs +306 -0
- package/node/personal_block.mjs +286 -0
- package/node/reputation_store.mjs +326 -0
- package/node/retention_policy.mjs +82 -0
- package/node/signaling_config.mjs +32 -0
- package/node/storage.mjs +30 -0
- package/node/storage_plugins.mjs +71 -0
- package/node/user_paths.mjs +10 -0
- package/overlay/index.mjs +191 -0
- package/package.json +73 -0
- package/permissions/bitmask.mjs +35 -0
- package/permissions/evaluator.mjs +90 -0
- package/permissions/index.mjs +12 -0
- package/permissions/layered.mjs +39 -0
- package/registries/action.mjs +94 -0
- package/registries/event_type.mjs +75 -0
- package/registries/inbound.mjs +90 -0
- package/registries/p2p_viewer.mjs +35 -0
- package/registries/part_path.mjs +35 -0
- package/registries/room_provider.mjs +47 -0
- package/reputation/engine.mjs +458 -0
- package/reputation/math.mjs +73 -0
- package/reputation/pick_score.mjs +11 -0
- package/reputation/relay_dedupe.mjs +22 -0
- package/reputation/tunables.json +36 -0
- package/rooms/scoped_link.mjs +185 -0
- package/schemas/discovery.mjs +110 -0
- package/schemas/federation_pull.mjs +102 -0
- package/schemas/mailbox.mjs +37 -0
- package/schemas/remote_event.mjs +17 -0
- package/timeline/append_core.mjs +54 -0
- package/timeline/materialize_runner.mjs +26 -0
- package/timeline/prune.mjs +39 -0
- package/timeline/retention.mjs +54 -0
- package/timeline/retention_runner.mjs +35 -0
- package/timeline/verify_remote.mjs +25 -0
- package/transport/group_link_set.mjs +324 -0
- package/transport/ice_servers.mjs +72 -0
- package/transport/link_registry.mjs +749 -0
- package/transport/peer_identity_maps.mjs +94 -0
- package/transport/peer_pool.mjs +355 -0
- package/transport/remote_user_room.mjs +109 -0
- package/transport/room_scopes.mjs +2 -0
- package/transport/room_wire_action.mjs +19 -0
- package/transport/rtc_connection_budget.mjs +180 -0
- package/transport/rtc_mdns_filter.mjs +118 -0
- package/transport/stale_peer_log.mjs +50 -0
- package/transport/user_room.mjs +263 -0
- package/trust_graph/build.mjs +99 -0
- package/trust_graph/cache.mjs +44 -0
- package/trust_graph/engine.mjs +184 -0
- package/trust_graph/registry.mjs +38 -0
- package/trust_graph/resolve.mjs +105 -0
- package/trust_graph/send.mjs +88 -0
- package/trust_graph/tunables.json +13 -0
- package/utils/async_mutex.mjs +69 -0
- package/utils/debug_log.mjs +16 -0
- package/utils/duration.mjs +28 -0
- package/utils/json_io.mjs +57 -0
- package/utils/lru.mjs +22 -0
- package/utils/map_pool.mjs +27 -0
- package/wire/group_part.mjs +43 -0
- package/wire/ingress.mjs +58 -0
- package/wire/part_common.mjs +55 -0
- package/wire/part_fanout.mjs +71 -0
- package/wire/part_ingress.mjs +172 -0
- package/wire/part_invoke.mjs +81 -0
- package/wire/rate_bucket.mjs +49 -0
- package/wire/volatile_signature.mjs +76 -0
- 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
|
+
}
|