@steve02081504/fount-p2p 0.0.10 → 0.0.12
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 +25 -7
- package/core/bytes_codec.mjs +65 -10
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- package/discovery/bt/index.mjs +5 -5
- package/discovery/bt/probe_child.mjs +20 -0
- package/discovery/bt/runtime.mjs +59 -15
- package/discovery/index.mjs +45 -48
- package/discovery/mdns.mjs +72 -17
- package/discovery/nostr.mjs +165 -129
- package/federation/chunk_fetch_pending.mjs +4 -5
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/topo_order_memo.mjs +11 -4
- package/files/chunk_fetch.mjs +2 -2
- package/files/evfs.mjs +2 -2
- package/link/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -124
- package/link/handshake.mjs +5 -5
- package/link/pipe.mjs +49 -75
- package/link/providers/ble_gatt.mjs +15 -27
- package/link/providers/index.mjs +7 -9
- package/link/providers/lan_tcp.mjs +18 -33
- package/link/providers/link_id_pipe.mjs +46 -0
- package/link/providers/webrtc.mjs +43 -52
- package/link/rtc.mjs +21 -9
- package/mailbox/deliver_or_store.mjs +1 -1
- package/node/identity.mjs +4 -4
- package/node/network.mjs +9 -9
- package/overlay/index.mjs +13 -13
- package/package.json +3 -2
- package/rooms/scoped_link.mjs +22 -38
- package/schemas/discovery.mjs +1 -2
- package/schemas/federation_pull.mjs +2 -2
- package/schemas/part_query.mjs +1 -1
- package/schemas/remote_event.mjs +1 -1
- package/transport/advert_ingest.mjs +20 -0
- package/transport/group_link_set.mjs +26 -45
- package/transport/ice_servers.mjs +12 -3
- package/transport/link_registry.mjs +181 -523
- package/transport/offer_answer.mjs +194 -0
- package/transport/peer_pool.mjs +53 -64
- package/transport/remote_user_room.mjs +13 -15
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/runtime_bootstrap.mjs +369 -0
- package/transport/signal_crypto.mjs +104 -0
- package/transport/user_room.mjs +22 -15
- package/trust_graph/send.mjs +1 -1
- package/utils/emit_safe.mjs +11 -0
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +6 -8
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +2 -3
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
4
|
+
import { sendSignal } from '../discovery/index.mjs'
|
|
5
|
+
import { listLinkProviders } from '../link/providers/index.mjs'
|
|
6
|
+
|
|
7
|
+
import { decryptSignalPacket, encryptSignalPacket, nodeRendezvousTopic } from './signal_crypto.mjs'
|
|
8
|
+
|
|
9
|
+
/** accept/dial 挂起期间 ICE 信令 backlog 上限,防无 handler 时无限堆积 */
|
|
10
|
+
const SIGNAL_BACKLOG_MAX = 64
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 创建带 backlog 的缓冲信令会话。
|
|
14
|
+
* @param {(message: unknown) => Promise<void>} sendRemote 远端发送回调
|
|
15
|
+
* @returns {{ send: (message: unknown) => Promise<void>, onRemote: (handler: (message: unknown) => void) => () => void, deliver: (message: unknown) => void, clear: () => void }} 信令会话
|
|
16
|
+
*/
|
|
17
|
+
export function createBufferedSignalSession(sendRemote) {
|
|
18
|
+
/** @type {Set<(message: unknown) => void>} */
|
|
19
|
+
const handlers = new Set()
|
|
20
|
+
/** @type {unknown[]} */
|
|
21
|
+
const backlog = []
|
|
22
|
+
return {
|
|
23
|
+
/**
|
|
24
|
+
* 发送信令消息到远端。
|
|
25
|
+
* @param {unknown} message 信令消息
|
|
26
|
+
* @returns {Promise<void>}
|
|
27
|
+
*/
|
|
28
|
+
async send(message) {
|
|
29
|
+
await sendRemote(message)
|
|
30
|
+
},
|
|
31
|
+
/**
|
|
32
|
+
* 注册远端信令 handler(含 backlog 回放)。
|
|
33
|
+
* @param {(message: unknown) => void} handler 入站回调
|
|
34
|
+
* @returns {() => void} 取消订阅函数
|
|
35
|
+
*/
|
|
36
|
+
onRemote(handler) {
|
|
37
|
+
handlers.add(handler)
|
|
38
|
+
for (const pending of backlog.splice(0))
|
|
39
|
+
handler(pending)
|
|
40
|
+
return () => handlers.delete(handler)
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* 投递信令消息;无 handler 时入 backlog(有界)。
|
|
44
|
+
* @param {unknown} message 信令消息
|
|
45
|
+
* @returns {void}
|
|
46
|
+
*/
|
|
47
|
+
deliver(message) {
|
|
48
|
+
if (!handlers.size) {
|
|
49
|
+
if (backlog.length >= SIGNAL_BACKLOG_MAX) backlog.shift()
|
|
50
|
+
backlog.push(message)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
for (const handler of handlers)
|
|
54
|
+
handler(message)
|
|
55
|
+
},
|
|
56
|
+
/**
|
|
57
|
+
* 清空 backlog 与 handler。
|
|
58
|
+
* @returns {void}
|
|
59
|
+
*/
|
|
60
|
+
clear() {
|
|
61
|
+
backlog.length = 0
|
|
62
|
+
handlers.clear()
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 判断信令 body 是否为发起方 offer(据此决定入站是否新建应答 PC)。
|
|
69
|
+
* @param {unknown} body 信令 body
|
|
70
|
+
* @returns {boolean} 是 offer 则 true
|
|
71
|
+
*/
|
|
72
|
+
function isOfferSignalBody(body) {
|
|
73
|
+
return !!body && typeof body === 'object'
|
|
74
|
+
&& body.type === 'description'
|
|
75
|
+
&& body.description?.type === 'offer'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 找已注册的 offer/answer provider(按 level 降序的第一个)。
|
|
80
|
+
* @returns {import('../link/providers/index.mjs').LinkProvider | null} 命中的 provider;无则 null
|
|
81
|
+
*/
|
|
82
|
+
export function findOfferAnswerProvider() {
|
|
83
|
+
return listLinkProviders().find(provider => provider.caps?.needsOfferAnswer) ?? null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Offer/answer glare 拨号控制器(WebRTC 等 needsOfferAnswer provider)。
|
|
88
|
+
* @param {object} deps 依赖注入
|
|
89
|
+
* @param {{ nodeHash: string, nodePubKey: string, secretKey: Uint8Array }} deps.localIdentity 本地身份
|
|
90
|
+
* @param {RTCConfiguration['iceServers']} deps.iceServers ICE 服务器列表
|
|
91
|
+
* @param {string} deps.selfTopic 本机 rendezvous topic
|
|
92
|
+
* @param {Map<string, ReturnType<typeof createBufferedSignalSession>>} deps.signalSessions 按 connId 索引的信令会话
|
|
93
|
+
* @param {(remoteNodeHash: string, link: object) => Promise<void>} deps.registerResolvedLink 注册规范链
|
|
94
|
+
* @param {() => Promise<void>} deps.trimToBudget 超预算驱逐
|
|
95
|
+
* @param {(remoteNodeHash: string) => object | null | undefined} deps.getCanonicalLink 读取当前规范链
|
|
96
|
+
* @returns {{ handleIncomingSignal: (bytes: Uint8Array) => Promise<void>, dialOfferAnswer: (provider: import('../link/providers/index.mjs').LinkProvider, remoteNodeHash: string) => Promise<object | null> }} 入站信令与主动拨号
|
|
97
|
+
*/
|
|
98
|
+
export function createOfferAnswerDial(deps) {
|
|
99
|
+
const {
|
|
100
|
+
localIdentity,
|
|
101
|
+
iceServers,
|
|
102
|
+
selfTopic,
|
|
103
|
+
signalSessions,
|
|
104
|
+
registerResolvedLink,
|
|
105
|
+
trimToBudget,
|
|
106
|
+
getCanonicalLink,
|
|
107
|
+
} = deps
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 为单条 PC 创建按 connId 标记的信令会话。
|
|
111
|
+
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
112
|
+
* @param {string} connId 连接标识
|
|
113
|
+
* @returns {ReturnType<typeof createBufferedSignalSession>} 信令会话
|
|
114
|
+
*/
|
|
115
|
+
function createConnSession(remoteNodeHash, connId) {
|
|
116
|
+
const normalized = normalizeHex64(remoteNodeHash)
|
|
117
|
+
const topic = nodeRendezvousTopic(normalized)
|
|
118
|
+
return createBufferedSignalSession(async message => {
|
|
119
|
+
await sendSignal(topic, normalized, encryptSignalPacket(topic, {
|
|
120
|
+
type: 'signal',
|
|
121
|
+
from: localIdentity.nodeHash,
|
|
122
|
+
connId,
|
|
123
|
+
body: message,
|
|
124
|
+
}))
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Offer/answer provider:为一条 connId 会话建链并走 glare 择一。
|
|
130
|
+
* @param {{ provider: import('../link/providers/index.mjs').LinkProvider, remoteNodeHash: string, connId: string, session: ReturnType<typeof createBufferedSignalSession>, initiator: boolean }} options 建链参数
|
|
131
|
+
* @returns {Promise<object | null>} 当前规范链;失败 null
|
|
132
|
+
*/
|
|
133
|
+
async function buildConnLink({ provider, remoteNodeHash, connId, session, initiator }) {
|
|
134
|
+
try {
|
|
135
|
+
if (initiator) await trimToBudget()
|
|
136
|
+
const link = await (initiator ? provider.dial : provider.accept)({
|
|
137
|
+
nodeHash: remoteNodeHash,
|
|
138
|
+
signal: session,
|
|
139
|
+
iceServers,
|
|
140
|
+
localIdentity,
|
|
141
|
+
})
|
|
142
|
+
if (!link) {
|
|
143
|
+
signalSessions.delete(connId)
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
link.onDown(() => signalSessions.delete(connId))
|
|
147
|
+
await link.ready
|
|
148
|
+
await registerResolvedLink(remoteNodeHash, link)
|
|
149
|
+
return getCanonicalLink(remoteNodeHash) ?? null
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
signalSessions.delete(connId)
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 处理入站加密信令并可能发起被动建链。
|
|
159
|
+
* @param {Uint8Array} bytes 加密信令字节
|
|
160
|
+
* @returns {Promise<void>}
|
|
161
|
+
*/
|
|
162
|
+
async function handleIncomingSignal(bytes) {
|
|
163
|
+
const packet = decryptSignalPacket(selfTopic, bytes)
|
|
164
|
+
if (packet?.type !== 'signal') return
|
|
165
|
+
const remoteNodeHash = normalizeHex64(packet.from)
|
|
166
|
+
const connId = String(packet.connId || '')
|
|
167
|
+
if (!remoteNodeHash || remoteNodeHash === localIdentity.nodeHash || !connId) return
|
|
168
|
+
let session = signalSessions.get(connId)
|
|
169
|
+
if (!session) {
|
|
170
|
+
if (!isOfferSignalBody(packet.body)) return
|
|
171
|
+
const provider = findOfferAnswerProvider()
|
|
172
|
+
if (!provider) return
|
|
173
|
+
session = createConnSession(remoteNodeHash, connId)
|
|
174
|
+
signalSessions.set(connId, session)
|
|
175
|
+
void buildConnLink({ provider, remoteNodeHash, connId, session, initiator: false })
|
|
176
|
+
}
|
|
177
|
+
session.deliver(packet.body)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 经 offer/answer provider 拨号(discovery signal + glare)。
|
|
182
|
+
* @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
|
|
183
|
+
* @param {string} remoteNodeHash 远端 64 hex
|
|
184
|
+
* @returns {Promise<object | null>} 当前规范链;失败 null
|
|
185
|
+
*/
|
|
186
|
+
async function dialOfferAnswer(provider, remoteNodeHash) {
|
|
187
|
+
const connId = randomBytes(16).toString('hex')
|
|
188
|
+
const session = createConnSession(remoteNodeHash, connId)
|
|
189
|
+
signalSessions.set(connId, session)
|
|
190
|
+
return await buildConnLink({ provider, remoteNodeHash, connId, session, initiator: true })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { handleIncomingSignal, dialOfferAnswer }
|
|
194
|
+
}
|
package/transport/peer_pool.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { loadPeerPoolView, mergeNetworkPeerPools } from '../node/network.mjs'
|
|
|
8
8
|
import { loadReputation } from '../node/reputation_store.mjs'
|
|
9
9
|
import { isQuarantinedPure } from '../reputation/engine.mjs'
|
|
10
10
|
import { clampReputationScore } from '../reputation/math.mjs'
|
|
11
|
+
import { shuffleInPlace } from '../utils/shuffle.mjs'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* 解析联邦池槽位参数(从 groupSettings 读取,含低功耗缩减)。
|
|
@@ -91,14 +92,8 @@ export function mergeTrustedWithAnchors(existingTrusted, rankedCandidates, limit
|
|
|
91
92
|
*/
|
|
92
93
|
export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxPerSource = EXPLORE_MAX_PER_SOURCE) {
|
|
93
94
|
if (k <= 0 || !exploreIds.length) return []
|
|
94
|
-
if (!exploreSources?.size)
|
|
95
|
-
|
|
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
|
-
}
|
|
95
|
+
if (!exploreSources?.size)
|
|
96
|
+
return shuffleInPlace([...exploreIds]).slice(0, k)
|
|
102
97
|
/** @type {Map<string, string[]>} */
|
|
103
98
|
const bySource = new Map()
|
|
104
99
|
for (const id of exploreIds) {
|
|
@@ -106,11 +101,7 @@ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxP
|
|
|
106
101
|
if (!bySource.has(src)) bySource.set(src, [])
|
|
107
102
|
bySource.get(src).push(id)
|
|
108
103
|
}
|
|
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
|
-
}
|
|
104
|
+
for (const ids of bySource.values()) shuffleInPlace(ids)
|
|
114
105
|
const out = []
|
|
115
106
|
/** @type {Map<string, number>} */
|
|
116
107
|
const picked = new Map()
|
|
@@ -148,7 +139,7 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
|
|
|
148
139
|
const blocked = new Set(peers.blockedPeers)
|
|
149
140
|
const roomSet = inRoomNodeHashes instanceof Set
|
|
150
141
|
? inRoomNodeHashes
|
|
151
|
-
: new Set(
|
|
142
|
+
: new Set(inRoomNodeHashes || [])
|
|
152
143
|
const onlineAll = roster.filter(
|
|
153
144
|
rosterEntry => rosterEntry.peerId
|
|
154
145
|
&& rosterEntry.remoteNodeHash
|
|
@@ -175,10 +166,11 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
|
|
|
175
166
|
}
|
|
176
167
|
|
|
177
168
|
const anchoredTrusted = peers.trustedPeers.filter(nodeHash => trustedSet.has(nodeHash))
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
.sort((a, b) => repScore(b, rep) - repScore(a, rep))
|
|
181
|
-
|
|
169
|
+
for (const nodeId of mergeTrustedWithAnchors(
|
|
170
|
+
anchoredTrusted,
|
|
171
|
+
[...trustedSet].sort((a, b) => repScore(b, rep) - repScore(a, rep)),
|
|
172
|
+
limits,
|
|
173
|
+
)) {
|
|
182
174
|
if (outPeerIds.size >= limits.maxPeers) break
|
|
183
175
|
pushNode(nodeId)
|
|
184
176
|
}
|
|
@@ -216,15 +208,14 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
|
|
|
216
208
|
* @returns {string[]} 应建链的 nodeHash 列表(去重)
|
|
217
209
|
*/
|
|
218
210
|
export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers, limits, anchors = [] }) {
|
|
219
|
-
const self = String(selfNodeHash || '')
|
|
220
211
|
const blocked = new Set(peers?.blockedPeers || [])
|
|
221
212
|
const now = Date.now()
|
|
222
|
-
const candidates = [...new Set(
|
|
223
|
-
.filter(id => id && id !==
|
|
213
|
+
const candidates = [...new Set(members)]
|
|
214
|
+
.filter(id => id && id !== selfNodeHash && !blocked.has(id) && !isQuarantinedPure(rep, id, now))
|
|
224
215
|
const ranked = candidates.slice().sort((a, b) => repScore(b, rep) - repScore(a, rep))
|
|
225
216
|
const candidateSet = new Set(candidates)
|
|
226
217
|
// 锚点(如 introducer/creator/seed)必连、且不占 trustedSlots——保证引导期连通。
|
|
227
|
-
const forced = [...new Set(
|
|
218
|
+
const forced = [...new Set(anchors)].filter(id => candidateSet.has(id))
|
|
228
219
|
const chosen = new Set(forced)
|
|
229
220
|
// trusted 槽只从非锚点候选填:既有 trusted 优先保留,再按信誉补至 trustedSlots。
|
|
230
221
|
const nonForced = ranked.filter(id => !chosen.has(id))
|
|
@@ -237,29 +228,24 @@ export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers
|
|
|
237
228
|
}
|
|
238
229
|
|
|
239
230
|
/**
|
|
240
|
-
*
|
|
241
|
-
* 纯计算版本,不含 I/O:接受 peers 对象,返回新对象。
|
|
242
|
-
*
|
|
231
|
+
* 将候选 id 并入 explore,并按信誉重填 trusted。
|
|
243
232
|
* @param {{
|
|
244
233
|
* peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
|
|
245
234
|
* rep: { byNodeHash?: Record<string, { score?: number }> },
|
|
246
|
-
*
|
|
235
|
+
* addIds: Iterable<string>,
|
|
247
236
|
* limits: ReturnType<typeof resolveFederationPoolLimits>,
|
|
248
|
-
* }} options
|
|
249
|
-
* @returns {{ trustedPeers: string[], explorePeers: string[] }}
|
|
237
|
+
* }} options 池状态与增量
|
|
238
|
+
* @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
|
|
250
239
|
*/
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
.map(id => String(id).trim())
|
|
255
|
-
.filter(Boolean),
|
|
256
|
-
)]
|
|
240
|
+
function rebuildExploreAndTrusted(options) {
|
|
241
|
+
const { peers, rep, addIds, limits } = options
|
|
242
|
+
const blocked = new Set(peers.blockedPeers)
|
|
257
243
|
const explore = new Set(peers.explorePeers)
|
|
258
|
-
for (const id of
|
|
259
|
-
if (!
|
|
260
|
-
const newExplorePeers = [...explore].slice(-500)
|
|
244
|
+
for (const id of addIds)
|
|
245
|
+
if (id && !blocked.has(id)) explore.add(id)
|
|
246
|
+
const newExplorePeers = [...explore].filter(id => !blocked.has(id)).slice(-500)
|
|
261
247
|
const ranked = [...new Set([...peers.trustedPeers, ...newExplorePeers])]
|
|
262
|
-
.filter(id => !
|
|
248
|
+
.filter(id => !blocked.has(id))
|
|
263
249
|
.sort((a, b) => repScore(b, rep) - repScore(a, rep))
|
|
264
250
|
return {
|
|
265
251
|
trustedPeers: mergeTrustedWithAnchors(peers.trustedPeers, ranked, limits, peers.blockedPeers),
|
|
@@ -268,32 +254,39 @@ export function applyPexHints({ peers, rep, hints, limits }) {
|
|
|
268
254
|
}
|
|
269
255
|
|
|
270
256
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
257
|
+
* PEX 线索并入 explore 并重填 trusted(纯计算)。
|
|
273
258
|
* @param {{
|
|
274
259
|
* peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
|
|
275
260
|
* rep: { byNodeHash?: Record<string, { score?: number }> },
|
|
276
|
-
*
|
|
261
|
+
* hints: string[],
|
|
277
262
|
* limits: ReturnType<typeof resolveFederationPoolLimits>,
|
|
278
|
-
* }} options
|
|
279
|
-
* @returns {{ trustedPeers: string[], explorePeers: string[] }}
|
|
263
|
+
* }} options 池状态与 PEX hints
|
|
264
|
+
* @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
|
|
280
265
|
*/
|
|
281
|
-
export function
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
266
|
+
export function applyPexHints(options) {
|
|
267
|
+
const { peers, rep, hints, limits } = options
|
|
268
|
+
return rebuildExploreAndTrusted({
|
|
269
|
+
peers, rep, limits,
|
|
270
|
+
addIds: hints || [],
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* roster 观测并入 explore 并重填 trusted(纯计算)。
|
|
276
|
+
* @param {{
|
|
277
|
+
* peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
|
|
278
|
+
* rep: { byNodeHash?: Record<string, { score?: number }> },
|
|
279
|
+
* roster: { remoteNodeHash?: string }[],
|
|
280
|
+
* limits: ReturnType<typeof resolveFederationPoolLimits>,
|
|
281
|
+
* }} options 池状态与 roster
|
|
282
|
+
* @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
|
|
283
|
+
*/
|
|
284
|
+
export function applyRosterToPeerPool(options) {
|
|
285
|
+
const { peers, rep, roster, limits } = options
|
|
286
|
+
return rebuildExploreAndTrusted({
|
|
287
|
+
peers, rep, limits,
|
|
288
|
+
addIds: roster.map(entry => entry.remoteNodeHash).filter(Boolean),
|
|
289
|
+
})
|
|
297
290
|
}
|
|
298
291
|
|
|
299
292
|
/**
|
|
@@ -308,17 +301,13 @@ export function pickFederationTargetPeerIds(groupId, roster, groupSettings, self
|
|
|
308
301
|
const limits = resolveFederationPoolLimits(groupSettings)
|
|
309
302
|
const peers = loadPeerPoolView(groupId)
|
|
310
303
|
const rep = loadReputation()
|
|
311
|
-
const inRoomNodeHashes = roster
|
|
312
|
-
.map(p => p.remoteNodeHash)
|
|
313
|
-
.map(id => String(id).trim())
|
|
314
|
-
.filter(Boolean)
|
|
315
304
|
return selectPeerIdsFromPool({
|
|
316
305
|
roster,
|
|
317
306
|
peers,
|
|
318
307
|
rep,
|
|
319
308
|
limits,
|
|
320
309
|
selfNodeHash,
|
|
321
|
-
inRoomNodeHashes,
|
|
310
|
+
inRoomNodeHashes: roster.map(entry => entry.remoteNodeHash).filter(Boolean),
|
|
322
311
|
hintSources: peers.hintSources,
|
|
323
312
|
})
|
|
324
313
|
}
|
|
@@ -12,15 +12,13 @@ import { USER_ROOM_SCOPE } from './room_scopes.mjs'
|
|
|
12
12
|
* }} RemoteUserRoomSlot
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
/** @type {Map<string, RemoteUserRoomSlot
|
|
15
|
+
/** @type {Map<string, RemoteUserRoomSlot>} nodeHash → slot */
|
|
16
16
|
const slots = new Map()
|
|
17
17
|
/** @type {Map<string, Promise<RemoteUserRoomSlot | null>>} nodeHash → 进行中的 promise */
|
|
18
18
|
const inflights = new Map()
|
|
19
19
|
|
|
20
20
|
registerFederationRoomProvider('remote-user-room', () => {
|
|
21
|
-
return [...slots.values()]
|
|
22
|
-
.filter(Boolean)
|
|
23
|
-
.map(s => s.roomSlot)
|
|
21
|
+
return [...slots.values()].map(s => s.roomSlot)
|
|
24
22
|
})
|
|
25
23
|
|
|
26
24
|
/**
|
|
@@ -32,16 +30,14 @@ registerFederationRoomProvider('remote-user-room', () => {
|
|
|
32
30
|
export async function ensureRemoteUserRoom(username, targetNodeHash) {
|
|
33
31
|
void username
|
|
34
32
|
const key = targetNodeHash.toLowerCase()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
const existing = slots.get(key)
|
|
34
|
+
if (existing) return existing
|
|
35
|
+
const inflight = inflights.get(key)
|
|
36
|
+
if (inflight) return await inflight
|
|
38
37
|
|
|
39
38
|
const task = (async () => {
|
|
40
39
|
try {
|
|
41
|
-
if (!await ensureLinkToNode(key))
|
|
42
|
-
slots.set(key, null)
|
|
43
|
-
return null
|
|
44
|
-
}
|
|
40
|
+
if (!await ensureLinkToNode(key)) return null
|
|
45
41
|
|
|
46
42
|
/** @type {import('../registries/room_provider.mjs').FederationRoomSlot} */
|
|
47
43
|
const roomSlot = {
|
|
@@ -66,17 +62,20 @@ export async function ensureRemoteUserRoom(username, targetNodeHash) {
|
|
|
66
62
|
* @returns {void}
|
|
67
63
|
*/
|
|
68
64
|
sendToPeer(peerId, actionName, payload) {
|
|
69
|
-
void getLink(key)?.send({ scope: 'node', action:
|
|
65
|
+
void getLink(key)?.send({ scope: 'node', action: actionName, payload }).catch(() => { })
|
|
70
66
|
},
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
const slot = {
|
|
74
70
|
roomSlot,
|
|
75
71
|
/**
|
|
76
|
-
*
|
|
72
|
+
* 关闭远端用户房间链路并释放槽位。
|
|
77
73
|
* @returns {Promise<void>} 关闭完成
|
|
78
74
|
*/
|
|
79
|
-
leave() {
|
|
75
|
+
leave() {
|
|
76
|
+
slots.delete(key)
|
|
77
|
+
return closeLink(key, 'remote-user-room-release')
|
|
78
|
+
},
|
|
80
79
|
}
|
|
81
80
|
slots.set(key, slot)
|
|
82
81
|
invalidateTrustGraphCache()
|
|
@@ -84,7 +83,6 @@ export async function ensureRemoteUserRoom(username, targetNodeHash) {
|
|
|
84
83
|
}
|
|
85
84
|
catch (error) {
|
|
86
85
|
console.error('p2p: failed to join remote user room', key, error)
|
|
87
|
-
slots.set(key, null)
|
|
88
86
|
return null
|
|
89
87
|
}
|
|
90
88
|
finally {
|
|
@@ -23,7 +23,7 @@ export function resolveRtcBudgetLimits(limits = {}) {
|
|
|
23
23
|
maxActive: Math.max(4, Math.min(128, Number(limits.maxActive) || 32)),
|
|
24
24
|
maxJoinsPerMin: Math.max(1, Math.min(120, Number(limits.maxJoinsPerMin) || 12)),
|
|
25
25
|
overloadCooldownMs: Math.max(1000, Number(limits.overloadCooldownMs) || 15_000),
|
|
26
|
-
trustedPeers:
|
|
26
|
+
trustedPeers: limits.trustedPeers || [],
|
|
27
27
|
trustedReserveFraction: Math.max(0.1, Math.min(0.5, Number(limits.trustedReserveFraction) || DEFAULT_TRUSTED_RESERVE_FRACTION)),
|
|
28
28
|
minTrustedReserved: Math.max(1, Math.floor(Number(limits.minTrustedReserved) || DEFAULT_MIN_TRUSTED_RESERVED)),
|
|
29
29
|
}
|
|
@@ -85,10 +85,14 @@ export function takeRtcJoinSlot(roomKey, peerId, limits = {}, sourceId = 'peer')
|
|
|
85
85
|
const isTrusted = peerId && bucket.trustedPeers.has(peerId)
|
|
86
86
|
const trustedReserved = Math.max(minTrustedReserved, Math.floor(maxActive * trustedReserveFraction))
|
|
87
87
|
const maxNonTrusted = Math.max(1, maxActive - trustedReserved)
|
|
88
|
-
|
|
88
|
+
let nonTrustedCount = 0
|
|
89
|
+
for (const id of bucket.active)
|
|
90
|
+
if (!bucket.trustedPeers.has(id)) nonTrustedCount++
|
|
89
91
|
if (!isTrusted) {
|
|
90
92
|
const source = String(sourceId || 'peer')
|
|
91
|
-
|
|
93
|
+
let sameSource = 0
|
|
94
|
+
for (const peerSource of bucket.sourceByPeer.values())
|
|
95
|
+
if (peerSource === source) sameSource++
|
|
92
96
|
const sourceCap = Math.max(1, Math.floor(maxActive * MAX_SOURCE_SLOT_FRACTION))
|
|
93
97
|
if (sameSource >= sourceCap) return false
|
|
94
98
|
if (nonTrustedCount >= maxNonTrusted && bucket.active.size >= maxActive) {
|
|
@@ -149,6 +153,16 @@ export function releaseRtcPeer(roomKey, peerId) {
|
|
|
149
153
|
bucket.active.delete(peerId)
|
|
150
154
|
bucket.sourceByPeer.delete(peerId)
|
|
151
155
|
bucket.peerNodeHash.delete(peerId)
|
|
156
|
+
// 房间空闲后丢掉桶,避免 leave 过的 roomKey 永久占内存
|
|
157
|
+
if (!bucket.active.size) budgets.delete(roomKey)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 当前预算桶数量(测试用)。
|
|
162
|
+
* @returns {number} 房间桶数
|
|
163
|
+
*/
|
|
164
|
+
export function rtcBudgetRoomCount() {
|
|
165
|
+
return budgets.size
|
|
152
166
|
}
|
|
153
167
|
|
|
154
168
|
/** RTC 过载时跳过的非关键联邦 action */
|
|
@@ -180,5 +194,5 @@ const NON_CRITICAL_FED_ACTIONS = new Set([
|
|
|
180
194
|
*/
|
|
181
195
|
export function isFederationActionAllowedUnderLoad(roomKey, actionName, limits = {}) {
|
|
182
196
|
if (!isRtcRoomOverloaded(roomKey, limits)) return true
|
|
183
|
-
return !NON_CRITICAL_FED_ACTIONS.has(
|
|
197
|
+
return !NON_CRITICAL_FED_ACTIONS.has(actionName)
|
|
184
198
|
}
|