@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,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,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
|
+
}
|