@steve02081504/fount-p2p 0.0.11 → 0.0.13
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 +42 -11
- package/core/bytes_codec.mjs +65 -10
- package/core/composite_key.mjs +5 -5
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +109 -0
- package/discovery/bt/index.mjs +154 -113
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +5 -12
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +430 -141
- package/federation/chunk_fetch_pending.mjs +14 -18
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/federation/topo_order_memo.mjs +11 -4
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_fetch.mjs +2 -2
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +31 -30
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -131
- package/link/handshake.mjs +19 -20
- package/link/pipe.mjs +53 -79
- package/link/providers/ble_gatt.mjs +19 -31
- package/link/providers/lan_tcp.mjs +17 -32
- package/link/providers/link_id_pipe.mjs +46 -0
- package/link/providers/webrtc.mjs +42 -51
- package/link/rtc.mjs +24 -12
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +6 -6
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/identity.mjs +4 -4
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +46 -14
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +35 -14
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -182
- 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 +17 -0
- package/transport/group_link_set.mjs +165 -134
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +283 -124
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +48 -47
- package/transport/peer_pool.mjs +169 -78
- package/transport/remote_user_room.mjs +18 -23
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +172 -104
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +93 -161
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +1 -4
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +11 -0
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +38 -36
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +28 -23
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
- package/discovery/mdns.mjs +0 -191
- package/transport/signal_crypto.mjs +0 -82
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto'
|
|
2
2
|
|
|
3
3
|
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
4
|
-
import {
|
|
4
|
+
import { decryptNodeSignalPacket, sendNodeSignalPacket } from '../discovery/index.mjs'
|
|
5
5
|
import { listLinkProviders } from '../link/providers/index.mjs'
|
|
6
|
+
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
/** accept/dial 挂起期间 ICE 信令 backlog 上限 */
|
|
9
|
+
const SIGNAL_BACKLOG_MAX = 64
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
|
-
* 创建带 backlog 的缓冲信令会话。
|
|
11
12
|
* @param {(message: unknown) => Promise<void>} sendRemote 远端发送回调
|
|
12
|
-
* @returns {
|
|
13
|
+
* @returns {object} 信令会话
|
|
13
14
|
*/
|
|
14
15
|
export function createBufferedSignalSession(sendRemote) {
|
|
15
16
|
/** @type {Set<(message: unknown) => void>} */
|
|
@@ -18,17 +19,15 @@ export function createBufferedSignalSession(sendRemote) {
|
|
|
18
19
|
const backlog = []
|
|
19
20
|
return {
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
-
* @param {unknown} message 信令消息
|
|
22
|
+
* @param {unknown} message 待发送信令消息
|
|
23
23
|
* @returns {Promise<void>}
|
|
24
24
|
*/
|
|
25
25
|
async send(message) {
|
|
26
26
|
await sendRemote(message)
|
|
27
27
|
},
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
* @
|
|
31
|
-
* @returns {() => void} 取消订阅函数
|
|
29
|
+
* @param {(message: unknown) => void} handler 远端消息回调
|
|
30
|
+
* @returns {() => void} 取消订阅
|
|
32
31
|
*/
|
|
33
32
|
onRemote(handler) {
|
|
34
33
|
handlers.add(handler)
|
|
@@ -37,12 +36,12 @@ export function createBufferedSignalSession(sendRemote) {
|
|
|
37
36
|
return () => handlers.delete(handler)
|
|
38
37
|
},
|
|
39
38
|
/**
|
|
40
|
-
*
|
|
41
|
-
* @param {unknown} message 信令消息
|
|
39
|
+
* @param {unknown} message 入站信令消息
|
|
42
40
|
* @returns {void}
|
|
43
41
|
*/
|
|
44
42
|
deliver(message) {
|
|
45
43
|
if (!handlers.size) {
|
|
44
|
+
if (backlog.length >= SIGNAL_BACKLOG_MAX) backlog.shift()
|
|
46
45
|
backlog.push(message)
|
|
47
46
|
return
|
|
48
47
|
}
|
|
@@ -50,7 +49,6 @@ export function createBufferedSignalSession(sendRemote) {
|
|
|
50
49
|
handler(message)
|
|
51
50
|
},
|
|
52
51
|
/**
|
|
53
|
-
* 清空 backlog 与 handler。
|
|
54
52
|
* @returns {void}
|
|
55
53
|
*/
|
|
56
54
|
clear() {
|
|
@@ -61,9 +59,8 @@ export function createBufferedSignalSession(sendRemote) {
|
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
/**
|
|
64
|
-
* 判断信令 body 是否为发起方 offer(据此决定入站是否新建应答 PC)。
|
|
65
62
|
* @param {unknown} body 信令 body
|
|
66
|
-
* @returns {boolean}
|
|
63
|
+
* @returns {boolean} 是否为 WebRTC offer description
|
|
67
64
|
*/
|
|
68
65
|
function isOfferSignalBody(body) {
|
|
69
66
|
return !!body && typeof body === 'object'
|
|
@@ -72,30 +69,19 @@ function isOfferSignalBody(body) {
|
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
/**
|
|
75
|
-
*
|
|
76
|
-
* @returns {import('../link/providers/index.mjs').LinkProvider | null} 命中的 provider;无则 null
|
|
72
|
+
* @returns {import('../link/providers/index.mjs').LinkProvider | null} 首个需要 offer/answer 的 provider
|
|
77
73
|
*/
|
|
78
74
|
export function findOfferAnswerProvider() {
|
|
79
75
|
return listLinkProviders().find(provider => provider.caps?.needsOfferAnswer) ?? null
|
|
80
76
|
}
|
|
81
77
|
|
|
82
78
|
/**
|
|
83
|
-
*
|
|
84
|
-
* @
|
|
85
|
-
* @param {{ nodeHash: string, nodePubKey: string, secretKey: Uint8Array }} deps.localIdentity 本地身份
|
|
86
|
-
* @param {RTCConfiguration['iceServers']} deps.iceServers ICE 服务器列表
|
|
87
|
-
* @param {string} deps.selfTopic 本机 rendezvous topic
|
|
88
|
-
* @param {Map<string, ReturnType<typeof createBufferedSignalSession>>} deps.signalSessions 按 connId 索引的信令会话
|
|
89
|
-
* @param {(remoteNodeHash: string, link: object) => Promise<void>} deps.registerResolvedLink 注册规范链
|
|
90
|
-
* @param {() => Promise<void>} deps.trimToBudget 超预算驱逐
|
|
91
|
-
* @param {(remoteNodeHash: string) => object | null | undefined} deps.getCanonicalLink 读取当前规范链
|
|
92
|
-
* @returns {{ handleIncomingSignal: (bytes: Uint8Array) => Promise<void>, dialOfferAnswer: (provider: import('../link/providers/index.mjs').LinkProvider, remoteNodeHash: string) => Promise<object | null> }} 入站信令与主动拨号
|
|
79
|
+
* @param {object} deps 依赖
|
|
80
|
+
* @returns {object} offer/answer 拨号
|
|
93
81
|
*/
|
|
94
82
|
export function createOfferAnswerDial(deps) {
|
|
95
83
|
const {
|
|
96
84
|
localIdentity,
|
|
97
|
-
iceServers,
|
|
98
|
-
selfTopic,
|
|
99
85
|
signalSessions,
|
|
100
86
|
registerResolvedLink,
|
|
101
87
|
trimToBudget,
|
|
@@ -103,28 +89,30 @@ export function createOfferAnswerDial(deps) {
|
|
|
103
89
|
} = deps
|
|
104
90
|
|
|
105
91
|
/**
|
|
106
|
-
*
|
|
107
|
-
* @param {string}
|
|
108
|
-
* @
|
|
109
|
-
* @returns {ReturnType<typeof createBufferedSignalSession>} 信令会话
|
|
92
|
+
* @param {string} remoteNodeHash 远端
|
|
93
|
+
* @param {string} connId 连接 id
|
|
94
|
+
* @returns {ReturnType<typeof createBufferedSignalSession>} 该连接的缓冲信令会话
|
|
110
95
|
*/
|
|
111
96
|
function createConnSession(remoteNodeHash, connId) {
|
|
112
97
|
const normalized = normalizeHex64(remoteNodeHash)
|
|
113
|
-
const topic = nodeRendezvousTopic(normalized)
|
|
114
98
|
return createBufferedSignalSession(async message => {
|
|
115
|
-
await
|
|
99
|
+
await sendNodeSignalPacket(normalized, {
|
|
116
100
|
type: 'signal',
|
|
117
101
|
from: localIdentity.nodeHash,
|
|
118
102
|
connId,
|
|
119
103
|
body: message,
|
|
120
|
-
})
|
|
104
|
+
})
|
|
121
105
|
})
|
|
122
106
|
}
|
|
123
107
|
|
|
124
108
|
/**
|
|
125
|
-
*
|
|
126
|
-
* @param {
|
|
127
|
-
* @
|
|
109
|
+
* @param {object} options 建链参数
|
|
110
|
+
* @param {import('../link/providers/index.mjs').LinkProvider} options.provider 链路提供者
|
|
111
|
+
* @param {string} options.remoteNodeHash 远端 nodeHash
|
|
112
|
+
* @param {string} options.connId 连接 id
|
|
113
|
+
* @param {ReturnType<typeof createBufferedSignalSession>} options.session 信令会话
|
|
114
|
+
* @param {boolean} options.initiator 是否主动拨号
|
|
115
|
+
* @returns {Promise<object | null>} 建链成功返回规范链路,否则 null
|
|
128
116
|
*/
|
|
129
117
|
async function buildConnLink({ provider, remoteNodeHash, connId, session, initiator }) {
|
|
130
118
|
try {
|
|
@@ -132,11 +120,16 @@ export function createOfferAnswerDial(deps) {
|
|
|
132
120
|
const link = await (initiator ? provider.dial : provider.accept)({
|
|
133
121
|
nodeHash: remoteNodeHash,
|
|
134
122
|
signal: session,
|
|
135
|
-
iceServers,
|
|
123
|
+
iceServers: deps.iceServers,
|
|
136
124
|
localIdentity,
|
|
137
125
|
})
|
|
138
126
|
if (!link) {
|
|
139
127
|
signalSessions.delete(connId)
|
|
128
|
+
nodeDebug('p2p:webrtc null', {
|
|
129
|
+
peer: shortHash(remoteNodeHash),
|
|
130
|
+
provider: provider.id,
|
|
131
|
+
role: initiator ? 'dial' : 'accept',
|
|
132
|
+
})
|
|
140
133
|
return null
|
|
141
134
|
}
|
|
142
135
|
link.onDown(() => signalSessions.delete(connId))
|
|
@@ -144,20 +137,25 @@ export function createOfferAnswerDial(deps) {
|
|
|
144
137
|
await registerResolvedLink(remoteNodeHash, link)
|
|
145
138
|
return getCanonicalLink(remoteNodeHash) ?? null
|
|
146
139
|
}
|
|
147
|
-
catch {
|
|
140
|
+
catch (error) {
|
|
148
141
|
signalSessions.delete(connId)
|
|
142
|
+
nodeDebug('p2p:webrtc fail', {
|
|
143
|
+
peer: shortHash(remoteNodeHash),
|
|
144
|
+
provider: provider.id,
|
|
145
|
+
role: initiator ? 'dial' : 'accept',
|
|
146
|
+
err: String(error?.message || error),
|
|
147
|
+
})
|
|
149
148
|
return null
|
|
150
149
|
}
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
/**
|
|
154
|
-
*
|
|
155
|
-
* @param {Uint8Array} bytes 加密信令字节
|
|
153
|
+
* @param {Uint8Array} bytes 加密信令
|
|
156
154
|
* @returns {Promise<void>}
|
|
157
155
|
*/
|
|
158
156
|
async function handleIncomingSignal(bytes) {
|
|
159
|
-
const packet =
|
|
160
|
-
if (
|
|
157
|
+
const packet = decryptNodeSignalPacket(localIdentity.nodeHash, bytes)
|
|
158
|
+
if (packet?.type !== 'signal') return
|
|
161
159
|
const remoteNodeHash = normalizeHex64(packet.from)
|
|
162
160
|
const connId = String(packet.connId || '')
|
|
163
161
|
if (!remoteNodeHash || remoteNodeHash === localIdentity.nodeHash || !connId) return
|
|
@@ -166,6 +164,10 @@ export function createOfferAnswerDial(deps) {
|
|
|
166
164
|
if (!isOfferSignalBody(packet.body)) return
|
|
167
165
|
const provider = findOfferAnswerProvider()
|
|
168
166
|
if (!provider) return
|
|
167
|
+
nodeDebug('p2p:webrtc inbound offer', {
|
|
168
|
+
peer: shortHash(remoteNodeHash),
|
|
169
|
+
provider: provider.id,
|
|
170
|
+
})
|
|
169
171
|
session = createConnSession(remoteNodeHash, connId)
|
|
170
172
|
signalSessions.set(connId, session)
|
|
171
173
|
void buildConnLink({ provider, remoteNodeHash, connId, session, initiator: false })
|
|
@@ -174,10 +176,9 @@ export function createOfferAnswerDial(deps) {
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
/**
|
|
177
|
-
* 经 offer/answer provider 拨号(discovery signal + glare)。
|
|
178
179
|
* @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
|
|
179
|
-
* @param {string} remoteNodeHash 远端
|
|
180
|
-
* @returns {Promise<object | null>}
|
|
180
|
+
* @param {string} remoteNodeHash 远端 nodeHash
|
|
181
|
+
* @returns {Promise<object | null>} 建链成功返回规范链路,否则 null
|
|
181
182
|
*/
|
|
182
183
|
async function dialOfferAnswer(provider, remoteNodeHash) {
|
|
183
184
|
const connId = randomBytes(16).toString('hex')
|
package/transport/peer_pool.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* 连接池纯计算:
|
|
3
|
+
* - 群联邦稀疏拨号:trustedSlots + exploreSlots(groupSettings)
|
|
4
|
+
* - 节点 mesh 保活:N / K_max(routing profile + transport tunables)
|
|
5
|
+
* 不含文件 I/O;I/O 由调用方注入。
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
import { compareHex64Asc } from '../core/hexIds.mjs'
|
|
7
9
|
import { loadPeerPoolView, mergeNetworkPeerPools } from '../node/network.mjs'
|
|
8
10
|
import { loadReputation } from '../node/reputation_store.mjs'
|
|
9
11
|
import { isQuarantinedPure } from '../reputation/engine.mjs'
|
|
10
12
|
import { clampReputationScore } from '../reputation/math.mjs'
|
|
13
|
+
import { shuffleInPlace } from '../utils/shuffle.mjs'
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* 解析联邦池槽位参数(从 groupSettings 读取,含低功耗缩减)。
|
|
@@ -91,37 +94,27 @@ export function mergeTrustedWithAnchors(existingTrusted, rankedCandidates, limit
|
|
|
91
94
|
*/
|
|
92
95
|
export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxPerSource = EXPLORE_MAX_PER_SOURCE) {
|
|
93
96
|
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
|
-
}
|
|
97
|
+
if (!exploreSources?.size)
|
|
98
|
+
return shuffleInPlace([...exploreIds]).slice(0, k)
|
|
102
99
|
/** @type {Map<string, string[]>} */
|
|
103
100
|
const bySource = new Map()
|
|
104
101
|
for (const id of exploreIds) {
|
|
105
|
-
const
|
|
106
|
-
if (!bySource.has(
|
|
107
|
-
bySource.get(
|
|
102
|
+
const source = exploreSources.get(id) || 'unknown'
|
|
103
|
+
if (!bySource.has(source)) bySource.set(source, [])
|
|
104
|
+
bySource.get(source).push(id)
|
|
108
105
|
}
|
|
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
|
-
}
|
|
106
|
+
for (const ids of bySource.values()) shuffleInPlace(ids)
|
|
114
107
|
const out = []
|
|
115
108
|
/** @type {Map<string, number>} */
|
|
116
109
|
const picked = new Map()
|
|
117
110
|
while (out.length < k) {
|
|
118
111
|
let progressed = false
|
|
119
|
-
for (const [
|
|
112
|
+
for (const [source, ids] of bySource) {
|
|
120
113
|
if (out.length >= k) break
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
out.push(ids[
|
|
124
|
-
picked.set(
|
|
114
|
+
const index = picked.get(source) ?? 0
|
|
115
|
+
if (index >= maxPerSource || index >= ids.length) continue
|
|
116
|
+
out.push(ids[index])
|
|
117
|
+
picked.set(source, index + 1)
|
|
125
118
|
progressed = true
|
|
126
119
|
}
|
|
127
120
|
if (!progressed) break
|
|
@@ -142,13 +135,13 @@ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxP
|
|
|
142
135
|
* inRoomNodeHashes?: Set<string> | string[] 群内在线 node_id;有则优先,仅全不可达时用 explore 中非房内节点
|
|
143
136
|
* hintSources?: Map<string, string> explore 节点来源(用于配额)
|
|
144
137
|
* }} options 选取参数(roster、peers、rep、limits、selfNodeHash)
|
|
145
|
-
* @returns {string[]} 目标
|
|
138
|
+
* @returns {string[]} 目标 peerId 列表(去重,长度 ≤ maxPeers)
|
|
146
139
|
*/
|
|
147
140
|
export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash, inRoomNodeHashes, hintSources }) {
|
|
148
141
|
const blocked = new Set(peers.blockedPeers)
|
|
149
142
|
const roomSet = inRoomNodeHashes instanceof Set
|
|
150
143
|
? inRoomNodeHashes
|
|
151
|
-
: new Set(
|
|
144
|
+
: new Set(inRoomNodeHashes || [])
|
|
152
145
|
const onlineAll = roster.filter(
|
|
153
146
|
rosterEntry => rosterEntry.peerId
|
|
154
147
|
&& rosterEntry.remoteNodeHash
|
|
@@ -175,10 +168,11 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
|
|
|
175
168
|
}
|
|
176
169
|
|
|
177
170
|
const anchoredTrusted = peers.trustedPeers.filter(nodeHash => trustedSet.has(nodeHash))
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
.sort((a, b) => repScore(b, rep) - repScore(a, rep))
|
|
181
|
-
|
|
171
|
+
for (const nodeId of mergeTrustedWithAnchors(
|
|
172
|
+
anchoredTrusted,
|
|
173
|
+
[...trustedSet].sort((a, b) => repScore(b, rep) - repScore(a, rep)),
|
|
174
|
+
limits,
|
|
175
|
+
)) {
|
|
182
176
|
if (outPeerIds.size >= limits.maxPeers) break
|
|
183
177
|
pushNode(nodeId)
|
|
184
178
|
}
|
|
@@ -216,15 +210,14 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
|
|
|
216
210
|
* @returns {string[]} 应建链的 nodeHash 列表(去重)
|
|
217
211
|
*/
|
|
218
212
|
export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers, limits, anchors = [] }) {
|
|
219
|
-
const self = String(selfNodeHash || '')
|
|
220
213
|
const blocked = new Set(peers?.blockedPeers || [])
|
|
221
214
|
const now = Date.now()
|
|
222
|
-
const candidates = [...new Set(
|
|
223
|
-
.filter(id => id && id !==
|
|
215
|
+
const candidates = [...new Set(members)]
|
|
216
|
+
.filter(id => id && id !== selfNodeHash && !blocked.has(id) && !isQuarantinedPure(rep, id, now))
|
|
224
217
|
const ranked = candidates.slice().sort((a, b) => repScore(b, rep) - repScore(a, rep))
|
|
225
218
|
const candidateSet = new Set(candidates)
|
|
226
219
|
// 锚点(如 introducer/creator/seed)必连、且不占 trustedSlots——保证引导期连通。
|
|
227
|
-
const forced = [...new Set(
|
|
220
|
+
const forced = [...new Set(anchors)].filter(id => candidateSet.has(id))
|
|
228
221
|
const chosen = new Set(forced)
|
|
229
222
|
// trusted 槽只从非锚点候选填:既有 trusted 优先保留,再按信誉补至 trustedSlots。
|
|
230
223
|
const nonForced = ranked.filter(id => !chosen.has(id))
|
|
@@ -237,29 +230,24 @@ export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers
|
|
|
237
230
|
}
|
|
238
231
|
|
|
239
232
|
/**
|
|
240
|
-
*
|
|
241
|
-
* 纯计算版本,不含 I/O:接受 peers 对象,返回新对象。
|
|
242
|
-
*
|
|
233
|
+
* 将候选 id 并入 explore,并按信誉重填 trusted。
|
|
243
234
|
* @param {{
|
|
244
235
|
* peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
|
|
245
236
|
* rep: { byNodeHash?: Record<string, { score?: number }> },
|
|
246
|
-
*
|
|
237
|
+
* addIds: Iterable<string>,
|
|
247
238
|
* limits: ReturnType<typeof resolveFederationPoolLimits>,
|
|
248
|
-
* }} options
|
|
249
|
-
* @returns {{ trustedPeers: string[], explorePeers: string[] }}
|
|
239
|
+
* }} options 池状态与增量
|
|
240
|
+
* @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
|
|
250
241
|
*/
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
.map(id => String(id).trim())
|
|
255
|
-
.filter(Boolean),
|
|
256
|
-
)]
|
|
242
|
+
function rebuildExploreAndTrusted(options) {
|
|
243
|
+
const { peers, rep, addIds, limits } = options
|
|
244
|
+
const blocked = new Set(peers.blockedPeers)
|
|
257
245
|
const explore = new Set(peers.explorePeers)
|
|
258
|
-
for (const id of
|
|
259
|
-
if (!
|
|
260
|
-
const newExplorePeers = [...explore].slice(-500)
|
|
246
|
+
for (const id of addIds)
|
|
247
|
+
if (id && !blocked.has(id)) explore.add(id)
|
|
248
|
+
const newExplorePeers = [...explore].filter(id => !blocked.has(id)).slice(-500)
|
|
261
249
|
const ranked = [...new Set([...peers.trustedPeers, ...newExplorePeers])]
|
|
262
|
-
.filter(id => !
|
|
250
|
+
.filter(id => !blocked.has(id))
|
|
263
251
|
.sort((a, b) => repScore(b, rep) - repScore(a, rep))
|
|
264
252
|
return {
|
|
265
253
|
trustedPeers: mergeTrustedWithAnchors(peers.trustedPeers, ranked, limits, peers.blockedPeers),
|
|
@@ -268,57 +256,60 @@ export function applyPexHints({ peers, rep, hints, limits }) {
|
|
|
268
256
|
}
|
|
269
257
|
|
|
270
258
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
259
|
+
* PEX 线索并入 explore 并重填 trusted(纯计算)。
|
|
273
260
|
* @param {{
|
|
274
261
|
* peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
|
|
275
262
|
* rep: { byNodeHash?: Record<string, { score?: number }> },
|
|
276
|
-
*
|
|
263
|
+
* hints: string[],
|
|
277
264
|
* limits: ReturnType<typeof resolveFederationPoolLimits>,
|
|
278
|
-
* }} options
|
|
279
|
-
* @returns {{ trustedPeers: string[], explorePeers: string[] }}
|
|
265
|
+
* }} options 池状态与 PEX hints
|
|
266
|
+
* @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
|
|
280
267
|
*/
|
|
281
|
-
export function
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
268
|
+
export function applyPexHints(options) {
|
|
269
|
+
const { peers, rep, hints, limits } = options
|
|
270
|
+
return rebuildExploreAndTrusted({
|
|
271
|
+
peers, rep, limits,
|
|
272
|
+
addIds: hints || [],
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* roster 观测并入 explore 并重填 trusted(纯计算)。
|
|
278
|
+
* @param {{
|
|
279
|
+
* peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
|
|
280
|
+
* rep: { byNodeHash?: Record<string, { score?: number }> },
|
|
281
|
+
* roster: { remoteNodeHash?: string }[],
|
|
282
|
+
* limits: ReturnType<typeof resolveFederationPoolLimits>,
|
|
283
|
+
* }} options 池状态与 roster
|
|
284
|
+
* @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
|
|
285
|
+
*/
|
|
286
|
+
export function applyRosterToPeerPool(options) {
|
|
287
|
+
const { peers, rep, roster, limits } = options
|
|
288
|
+
return rebuildExploreAndTrusted({
|
|
289
|
+
peers, rep, limits,
|
|
290
|
+
addIds: roster.map(entry => entry.remoteNodeHash).filter(Boolean),
|
|
291
|
+
})
|
|
297
292
|
}
|
|
298
293
|
|
|
299
294
|
/**
|
|
300
295
|
* 稀疏连接池:优先 trusted,再 explore,再其余在线节点。
|
|
301
296
|
* @param {string} groupId 群
|
|
302
|
-
* @param {{ peerId: string, remoteNodeHash?: string }[]} roster
|
|
297
|
+
* @param {{ peerId: string, remoteNodeHash?: string }[]} roster 在线表
|
|
303
298
|
* @param {object} groupSettings 物化群设置
|
|
304
299
|
* @param {string} selfNodeHash 本机 node_id
|
|
305
|
-
* @returns {string[]} 目标
|
|
300
|
+
* @returns {string[]} 目标 peerId(去重)
|
|
306
301
|
*/
|
|
307
302
|
export function pickFederationTargetPeerIds(groupId, roster, groupSettings, selfNodeHash) {
|
|
308
303
|
const limits = resolveFederationPoolLimits(groupSettings)
|
|
309
304
|
const peers = loadPeerPoolView(groupId)
|
|
310
305
|
const rep = loadReputation()
|
|
311
|
-
const inRoomNodeHashes = roster
|
|
312
|
-
.map(p => p.remoteNodeHash)
|
|
313
|
-
.map(id => String(id).trim())
|
|
314
|
-
.filter(Boolean)
|
|
315
306
|
return selectPeerIdsFromPool({
|
|
316
307
|
roster,
|
|
317
308
|
peers,
|
|
318
309
|
rep,
|
|
319
310
|
limits,
|
|
320
311
|
selfNodeHash,
|
|
321
|
-
inRoomNodeHashes,
|
|
312
|
+
inRoomNodeHashes: roster.map(entry => entry.remoteNodeHash).filter(Boolean),
|
|
322
313
|
hintSources: peers.hintSources,
|
|
323
314
|
})
|
|
324
315
|
}
|
|
@@ -353,3 +344,103 @@ export function reconcilePeerPoolFromRoster(groupId, roster, groupSettings) {
|
|
|
353
344
|
const { trustedPeers, explorePeers } = applyRosterToPeerPool({ peers, rep, roster, limits })
|
|
354
345
|
mergeNetworkPeerPools({ trustedPeers, explorePeers })
|
|
355
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Mesh 保活 N/K 槽位(routing profile 可缩 N 与 K_max)。
|
|
350
|
+
* @param {'default' | 'low'} [routingProfile='default'] 路由 profile
|
|
351
|
+
* @param {object} [tunables={}] transport tunables
|
|
352
|
+
* @returns {{ N: number, K_max: number }} mesh 槽位上限 N 与熟人槽 K_max
|
|
353
|
+
*/
|
|
354
|
+
export function resolveMeshPoolLimits(routingProfile = 'default', tunables = {}) {
|
|
355
|
+
const low = routingProfile === 'low'
|
|
356
|
+
const N = Math.max(1, Math.min(32, Number(low ? tunables.meshNLow : tunables.meshN) || (low ? 4 : 8)))
|
|
357
|
+
const K_max = Math.max(0, Math.min(N, Number(low ? tunables.meshKMaxLow : tunables.meshKMax) || (low ? 2 : 5)))
|
|
358
|
+
return { N, K_max }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 选取 mesh 拨号目标:补齐 K 熟人(可超出当前空位,由调用方先踢探索),再按 N−K 探索配额填空。
|
|
363
|
+
* @param {{
|
|
364
|
+
* selfNodeHash: string,
|
|
365
|
+
* trustedPeers: string[],
|
|
366
|
+
* exploreCandidates: string[],
|
|
367
|
+
* hintSources?: Map<string, string>,
|
|
368
|
+
* limits: ReturnType<typeof resolveMeshPoolLimits>,
|
|
369
|
+
* connectedHashes: Set<string>,
|
|
370
|
+
* rep: { byNodeHash?: Record<string, { score?: number, quarantinedUntil?: number }> },
|
|
371
|
+
* blockedPeers?: string[],
|
|
372
|
+
* now?: number,
|
|
373
|
+
* }} options 选取参数
|
|
374
|
+
* @returns {string[]} 应拨号的 nodeHash(熟人可导致需先驱逐探索)
|
|
375
|
+
*/
|
|
376
|
+
export function selectMeshLinkTargets(options) {
|
|
377
|
+
const {
|
|
378
|
+
selfNodeHash,
|
|
379
|
+
trustedPeers = [],
|
|
380
|
+
exploreCandidates = [],
|
|
381
|
+
hintSources,
|
|
382
|
+
limits,
|
|
383
|
+
connectedHashes,
|
|
384
|
+
rep,
|
|
385
|
+
blockedPeers = [],
|
|
386
|
+
now = Date.now(),
|
|
387
|
+
} = options
|
|
388
|
+
const blocked = new Set(blockedPeers)
|
|
389
|
+
const connected = connectedHashes
|
|
390
|
+
|
|
391
|
+
const eligibleTrusted = [...new Set(trustedPeers)]
|
|
392
|
+
.filter(id => id && id !== selfNodeHash && !blocked.has(id))
|
|
393
|
+
.filter(id => !isQuarantinedPure(rep, id, now))
|
|
394
|
+
const trustedSet = new Set(eligibleTrusted)
|
|
395
|
+
/** 目标组成:K 熟人槽 + (N−K) 探索槽 */
|
|
396
|
+
const K = Math.min(limits.K_max, eligibleTrusted.length)
|
|
397
|
+
let connectedTrusted = 0
|
|
398
|
+
let connectedExplore = 0
|
|
399
|
+
for (const id of connected)
|
|
400
|
+
if (trustedSet.has(id)) connectedTrusted++
|
|
401
|
+
else connectedExplore++
|
|
402
|
+
const trustedSlotsLeft = Math.max(0, K - connectedTrusted)
|
|
403
|
+
const trustedRanked = eligibleTrusted
|
|
404
|
+
.filter(id => !connected.has(id))
|
|
405
|
+
.sort((a, b) => repScore(b, rep) - repScore(a, rep))
|
|
406
|
+
// 熟人缺口优先补齐,即使当前已满 N(调用方应先踢探索腾位)
|
|
407
|
+
const trustedPick = trustedRanked.slice(0, trustedSlotsLeft)
|
|
408
|
+
|
|
409
|
+
const exploreQuota = Math.max(0, limits.N - K)
|
|
410
|
+
const exploreSlotsLeft = Math.max(0, exploreQuota - connectedExplore)
|
|
411
|
+
const freeAfterTrustedDial = Math.max(0, limits.N - connected.size - trustedPick.length)
|
|
412
|
+
const exploreNeed = Math.min(exploreSlotsLeft, freeAfterTrustedDial)
|
|
413
|
+
const explorePool = [...new Set(exploreCandidates)]
|
|
414
|
+
.filter(id => id && id !== selfNodeHash && !blocked.has(id) && !connected.has(id) && !trustedSet.has(id))
|
|
415
|
+
.filter(id => !isQuarantinedPure(rep, id, now))
|
|
416
|
+
const explorePick = selectExploreWithSourceQuota(explorePool, hintSources, exploreNeed)
|
|
417
|
+
return [...trustedPick, ...explorePick]
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* mesh trim:探索链优先驱逐,同档取 scope 权重低、nodeHash 小。
|
|
422
|
+
* @param {string[]} linkHashes 当前链路 nodeHash
|
|
423
|
+
* @param {Set<string>} exploreLinkHashes 探索链集合
|
|
424
|
+
* @param {string[]} trustedPeers 熟人池
|
|
425
|
+
* @param {(nodeHash: string) => number} scopeWeightFn scope 权重
|
|
426
|
+
* @returns {string | null} 应驱逐的 nodeHash
|
|
427
|
+
*/
|
|
428
|
+
export function pickMeshEvictionVictim(linkHashes, exploreLinkHashes, trustedPeers, scopeWeightFn) {
|
|
429
|
+
const trustedSet = new Set(trustedPeers)
|
|
430
|
+
let victimHash = null
|
|
431
|
+
let victimScore = Infinity
|
|
432
|
+
for (const nodeHash of linkHashes) {
|
|
433
|
+
const isExplore = exploreLinkHashes.has(nodeHash) && !trustedSet.has(nodeHash)
|
|
434
|
+
const weight = scopeWeightFn(nodeHash)
|
|
435
|
+
const score = (isExplore ? 0 : 1000) + weight
|
|
436
|
+
if (
|
|
437
|
+
victimHash == null
|
|
438
|
+
|| score < victimScore
|
|
439
|
+
|| (score === victimScore && compareHex64Asc(nodeHash, victimHash) < 0)
|
|
440
|
+
) {
|
|
441
|
+
victimHash = nodeHash
|
|
442
|
+
victimScore = score
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return victimHash
|
|
446
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { bumpLocalDataRevision } from '../node/local_data_revision.mjs'
|
|
1
2
|
import { registerFederationRoomProvider } from '../registries/room_provider.mjs'
|
|
2
|
-
import { invalidateTrustGraphCache } from '../trust_graph/cache.mjs'
|
|
3
3
|
|
|
4
4
|
import { closeLink, ensureLinkToNode, getLink } from './link_registry.mjs'
|
|
5
5
|
import { USER_ROOM_SCOPE } from './room_scopes.mjs'
|
|
@@ -12,36 +12,33 @@ 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
|
-
/** @type {Map<string, Promise<RemoteUserRoomSlot
|
|
17
|
+
/** @type {Map<string, Promise<RemoteUserRoomSlot>>} 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
|
/**
|
|
27
25
|
* 加入目标节点的用户房间(幂等)。
|
|
28
26
|
* @param {string} username 本地 replica 用户名
|
|
29
27
|
* @param {string} targetNodeHash 目标节点 64 hex
|
|
30
|
-
* @returns {Promise<RemoteUserRoomSlot
|
|
28
|
+
* @returns {Promise<RemoteUserRoomSlot>} 房间槽
|
|
31
29
|
*/
|
|
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
|
-
|
|
43
|
-
return null
|
|
44
|
-
}
|
|
40
|
+
if (!await ensureLinkToNode(key))
|
|
41
|
+
throw new Error(`p2p: ensureRemoteUserRoom link failed for ${key}`)
|
|
45
42
|
|
|
46
43
|
/** @type {import('../registries/room_provider.mjs').FederationRoomSlot} */
|
|
47
44
|
const roomSlot = {
|
|
@@ -66,27 +63,25 @@ export async function ensureRemoteUserRoom(username, targetNodeHash) {
|
|
|
66
63
|
* @returns {void}
|
|
67
64
|
*/
|
|
68
65
|
sendToPeer(peerId, actionName, payload) {
|
|
69
|
-
void getLink(key)?.send({ scope: 'node', action:
|
|
66
|
+
void getLink(key)?.send({ scope: 'node', action: actionName, payload }).catch(() => { })
|
|
70
67
|
},
|
|
71
68
|
}
|
|
72
69
|
|
|
73
70
|
const slot = {
|
|
74
71
|
roomSlot,
|
|
75
72
|
/**
|
|
76
|
-
*
|
|
73
|
+
* 关闭远端用户房间链路并释放槽位。
|
|
77
74
|
* @returns {Promise<void>} 关闭完成
|
|
78
75
|
*/
|
|
79
|
-
leave() {
|
|
76
|
+
leave() {
|
|
77
|
+
slots.delete(key)
|
|
78
|
+
return closeLink(key, 'remote-user-room-release')
|
|
79
|
+
},
|
|
80
80
|
}
|
|
81
81
|
slots.set(key, slot)
|
|
82
|
-
|
|
82
|
+
bumpLocalDataRevision()
|
|
83
83
|
return slot
|
|
84
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
85
|
finally {
|
|
91
86
|
inflights.delete(key)
|
|
92
87
|
}
|