@steve02081504/fount-p2p 0.0.12 → 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 +41 -10
- package/core/composite_key.mjs +5 -5
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- 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 +153 -112
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +2 -9
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +394 -59
- package/federation/chunk_fetch_pending.mjs +10 -13
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +29 -28
- 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/frame.mjs +9 -16
- package/link/handshake.mjs +14 -15
- package/link/pipe.mjs +8 -8
- package/link/providers/ble_gatt.mjs +6 -6
- package/link/rtc.mjs +3 -3
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +5 -5
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +37 -5
- 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 +22 -1
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -166
- package/transport/advert_ingest.mjs +11 -14
- package/transport/group_link_set.mjs +149 -99
- package/transport/link_registry.mjs +211 -60
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +45 -48
- package/transport/peer_pool.mjs +116 -14
- package/transport/remote_user_room.mjs +6 -9
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +170 -108
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +83 -158
- 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 +0 -3
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +3 -3
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/wire/part_ingress.mjs +32 -28
- package/wire/part_query.mjs +26 -20
- package/discovery/mdns.mjs +0 -197
- package/transport/signal_crypto.mjs +0 -104
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { attachNodeScopeFedChunkResponder } from '../files/chunk_responder.mjs'
|
|
2
|
+
import { attachMailboxWire } from '../mailbox/wire.mjs'
|
|
3
|
+
import { attachPartWire } from '../wire/part_ingress.mjs'
|
|
4
|
+
import { attachPartQueryWire } from '../wire/part_query.mjs'
|
|
5
|
+
|
|
6
|
+
import { sendToNodeLink, subscribeScope } from './link_registry.mjs'
|
|
7
|
+
|
|
8
|
+
/** @type {Map<string, Set<(payload: unknown, peerId: string) => void>>} */
|
|
9
|
+
const nodeActionHandlers = new Map()
|
|
10
|
+
|
|
11
|
+
/** @type {Set<(context: NodeScopeContext, wire: NodeScopeWire) => void>} */
|
|
12
|
+
const nodeScopeWireHooks = new Set()
|
|
13
|
+
|
|
14
|
+
/** @type {NodeScopeContext} */
|
|
15
|
+
const nodeScopeContext = { replicaUsername: '' }
|
|
16
|
+
|
|
17
|
+
/** @type {NodeScopeWire | null} */
|
|
18
|
+
let nodeScopeWire = null
|
|
19
|
+
|
|
20
|
+
/** @type {(() => void) | null} */
|
|
21
|
+
let nodeScopeSubscribeCleanup = null
|
|
22
|
+
|
|
23
|
+
/** @type {Set<() => void>} */
|
|
24
|
+
const nodeScopeFeatureDisposers = new Set()
|
|
25
|
+
|
|
26
|
+
/** @type {Map<string, { count: number, disposeCore: () => void }>} */
|
|
27
|
+
const featureAttachRefs = new Map()
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {{ replicaUsername: string }} NodeScopeContext
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {{
|
|
35
|
+
* on: (name: string, handler: (payload: unknown, peerId: string) => void) => () => void
|
|
36
|
+
* send: (name: string, payload: unknown, peerId: string | null) => void
|
|
37
|
+
* }} NodeScopeWire
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} peerId 对端 nodeHash
|
|
42
|
+
* @param {string} action 动作名
|
|
43
|
+
* @param {unknown} payload 载荷
|
|
44
|
+
* @returns {Promise<boolean>} 是否成功发出
|
|
45
|
+
*/
|
|
46
|
+
const sendNodeAction = (peerId, action, payload) =>
|
|
47
|
+
sendToNodeLink(peerId, { scope: 'node', action, payload })
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @returns {NodeScopeWire} node scope 的 wire 适配器
|
|
51
|
+
*/
|
|
52
|
+
function createNodeScopeWire() {
|
|
53
|
+
return {
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} name - action 名
|
|
56
|
+
* @param {(payload: unknown, peerId: string) => void} handler - 入站处理器
|
|
57
|
+
* @returns {() => void} 取消注册的 dispose
|
|
58
|
+
*/
|
|
59
|
+
on(name, handler) {
|
|
60
|
+
if (!nodeActionHandlers.has(name)) nodeActionHandlers.set(name, new Set())
|
|
61
|
+
nodeActionHandlers.get(name).add(handler)
|
|
62
|
+
return () => {
|
|
63
|
+
const set = nodeActionHandlers.get(name)
|
|
64
|
+
if (!set) return
|
|
65
|
+
set.delete(handler)
|
|
66
|
+
if (!set.size) nodeActionHandlers.delete(name)
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} name - action 名
|
|
71
|
+
* @param {unknown} payload - 出站载荷
|
|
72
|
+
* @param {string | null} peerId - 目标 peer,null 时忽略
|
|
73
|
+
* @returns {void}
|
|
74
|
+
*/
|
|
75
|
+
send(name, payload, peerId) {
|
|
76
|
+
if (!peerId) return
|
|
77
|
+
void sendNodeAction(peerId, name, payload).catch(() => { })
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @returns {boolean} 是否已订阅 node scope
|
|
84
|
+
*/
|
|
85
|
+
export function isNodeScopeSubscribed() {
|
|
86
|
+
return nodeScopeSubscribeCleanup != null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} action - action 名
|
|
91
|
+
* @returns {boolean} 是否已挂载处理器
|
|
92
|
+
*/
|
|
93
|
+
export function hasNodeScopeAction(action) {
|
|
94
|
+
return (nodeActionHandlers.get(action)?.size ?? 0) > 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string} action - action 名
|
|
99
|
+
* @returns {number} 已注册的处理器数量
|
|
100
|
+
*/
|
|
101
|
+
export function countNodeScopeActionHandlers(action) {
|
|
102
|
+
return nodeActionHandlers.get(action)?.size ?? 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 测试/调试:直接派发已挂载的 node action。
|
|
107
|
+
* @param {string} action - action 名
|
|
108
|
+
* @param {unknown} payload - 载荷
|
|
109
|
+
* @param {string} peerId - 发送方 nodeHash
|
|
110
|
+
* @returns {boolean} 是否有处理器被调用
|
|
111
|
+
*/
|
|
112
|
+
export function dispatchNodeScopeAction(action, payload, peerId) {
|
|
113
|
+
const handlers = nodeActionHandlers.get(action)
|
|
114
|
+
if (!handlers?.size) return false
|
|
115
|
+
for (const handler of handlers)
|
|
116
|
+
try { handler(payload, peerId) } catch { /* ignore */ }
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @returns {NodeScopeWire | null} 当前 wire,未 ensure 时为 null
|
|
122
|
+
*/
|
|
123
|
+
export function getNodeScopeWire() {
|
|
124
|
+
return nodeScopeWire
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @returns {NodeScopeContext} 可变 node scope 上下文
|
|
129
|
+
*/
|
|
130
|
+
export function getNodeScopeContext() {
|
|
131
|
+
return nodeScopeContext
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 只订阅 node scope 派发,不挂任何 feature。
|
|
136
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
137
|
+
* @returns {() => void} 取消订阅的 dispose
|
|
138
|
+
*/
|
|
139
|
+
export function ensureNodeScope(options = {}) {
|
|
140
|
+
if (options.replicaUsername != null)
|
|
141
|
+
nodeScopeContext.replicaUsername = String(options.replicaUsername)
|
|
142
|
+
if (nodeScopeSubscribeCleanup) return nodeScopeSubscribeCleanup
|
|
143
|
+
nodeScopeSubscribeCleanup = subscribeScope('node', (senderNodeHash, envelope) => {
|
|
144
|
+
const handlers = nodeActionHandlers.get(envelope.action)
|
|
145
|
+
if (!handlers?.size) return
|
|
146
|
+
for (const handler of handlers)
|
|
147
|
+
try { handler(envelope.payload, senderNodeHash) } catch { /* ignore */ }
|
|
148
|
+
})
|
|
149
|
+
if (!nodeScopeWire) {
|
|
150
|
+
nodeScopeWire = createNodeScopeWire()
|
|
151
|
+
for (const hook of nodeScopeWireHooks)
|
|
152
|
+
try { hook(nodeScopeContext, nodeScopeWire) } catch { /* ignore */ }
|
|
153
|
+
}
|
|
154
|
+
return nodeScopeSubscribeCleanup
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {(context: NodeScopeContext, wire: NodeScopeWire) => void} hook - wire 创建时回调
|
|
159
|
+
* @returns {() => void} 取消注册的 dispose
|
|
160
|
+
*/
|
|
161
|
+
export function registerNodeScopeWireHook(hook) {
|
|
162
|
+
nodeScopeWireHooks.add(hook)
|
|
163
|
+
if (nodeScopeWire)
|
|
164
|
+
try { hook(nodeScopeContext, nodeScopeWire) } catch { /* ignore */ }
|
|
165
|
+
return () => nodeScopeWireHooks.delete(hook)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {() => void} dispose - feature 卸载函数
|
|
170
|
+
* @returns {() => void} 包装后的 dispose(同时从跟踪集移除)
|
|
171
|
+
*/
|
|
172
|
+
function trackFeatureDisposer(dispose) {
|
|
173
|
+
nodeScopeFeatureDisposers.add(dispose)
|
|
174
|
+
return () => {
|
|
175
|
+
dispose()
|
|
176
|
+
nodeScopeFeatureDisposers.delete(dispose)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 同一 feature 多次 attach 共享一份 wire;dispose 引用计数归零才卸。
|
|
182
|
+
* @param {string} key - feature 去重键
|
|
183
|
+
* @param {() => () => void} attachCore - 首次 attach 时执行,返回核心 dispose
|
|
184
|
+
* @returns {() => void} 引用计数包装的 dispose
|
|
185
|
+
*/
|
|
186
|
+
function attachFeatureRefCounted(key, attachCore) {
|
|
187
|
+
let entry = featureAttachRefs.get(key)
|
|
188
|
+
if (!entry) {
|
|
189
|
+
entry = { count: 0, disposeCore: attachCore() }
|
|
190
|
+
featureAttachRefs.set(key, entry)
|
|
191
|
+
}
|
|
192
|
+
entry.count++
|
|
193
|
+
return trackFeatureDisposer(() => {
|
|
194
|
+
const cur = featureAttachRefs.get(key)
|
|
195
|
+
if (!cur) return
|
|
196
|
+
cur.count--
|
|
197
|
+
if (cur.count > 0) return
|
|
198
|
+
cur.disposeCore()
|
|
199
|
+
featureAttachRefs.delete(key)
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 自定义 node scope feature(与 mailbox/part 相同 refcount 语义)。
|
|
205
|
+
* @param {string} key - feature 去重键
|
|
206
|
+
* @param {(wire: NodeScopeWire, context: NodeScopeContext) => () => void} attachCore - 首次 attach 注册 handler,返回核心 dispose
|
|
207
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
208
|
+
* @returns {() => void} 引用计数包装的 dispose
|
|
209
|
+
*/
|
|
210
|
+
export function attachNodeScopeFeature(key, attachCore, options = {}) {
|
|
211
|
+
ensureNodeScope(options)
|
|
212
|
+
return attachFeatureRefCounted(key, () => attachCore(nodeScopeWire, nodeScopeContext))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* refcount 挂载 mailbox wire。
|
|
217
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
218
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
219
|
+
*/
|
|
220
|
+
export function attachNodeScopeMailbox(options = {}) {
|
|
221
|
+
ensureNodeScope(options)
|
|
222
|
+
return attachFeatureRefCounted('mailbox', () => attachMailboxWire(nodeScopeContext, nodeScopeWire))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* refcount 挂载 part ingress wire。
|
|
227
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
228
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
229
|
+
*/
|
|
230
|
+
export function attachNodeScopePart(options = {}) {
|
|
231
|
+
ensureNodeScope(options)
|
|
232
|
+
return attachFeatureRefCounted('part', () => attachPartWire(nodeScopeContext, nodeScopeWire))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* refcount 挂载 part_query wire。
|
|
237
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
238
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
239
|
+
*/
|
|
240
|
+
export function attachNodeScopePartQuery(options = {}) {
|
|
241
|
+
ensureNodeScope(options)
|
|
242
|
+
return attachFeatureRefCounted('partQuery', () => attachPartQueryWire(nodeScopeContext, nodeScopeWire))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* refcount 挂载 fed chunk responder。
|
|
247
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
248
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
249
|
+
*/
|
|
250
|
+
export function attachNodeScopeChunks(options = {}) {
|
|
251
|
+
ensureNodeScope(options)
|
|
252
|
+
return attachFeatureRefCounted('chunks', () =>
|
|
253
|
+
attachNodeScopeFedChunkResponder(() => nodeScopeContext.replicaUsername, nodeScopeWire))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 全业务 preset(等价旧 ensureNodeScopeRuntime 默认行为)。
|
|
258
|
+
* @param {{ replicaUsername?: string }} [options] - 可选副本用户名
|
|
259
|
+
* @returns {() => void} 取消全部 preset 挂载的 dispose
|
|
260
|
+
*/
|
|
261
|
+
export function attachUserRoomDefaultWires(options = {}) {
|
|
262
|
+
const disposers = [
|
|
263
|
+
attachNodeScopePart(options),
|
|
264
|
+
attachNodeScopePartQuery(options),
|
|
265
|
+
attachNodeScopeMailbox(options),
|
|
266
|
+
attachNodeScopeChunks(options),
|
|
267
|
+
]
|
|
268
|
+
return () => {
|
|
269
|
+
for (const dispose of disposers) dispose()
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 卸掉 feature 挂载;可选保留 node scope 订阅。
|
|
275
|
+
* @param {{ keepSubscribe?: boolean }} [options] - true 时保留 node scope 订阅
|
|
276
|
+
* @returns {void}
|
|
277
|
+
*/
|
|
278
|
+
export function stopNodeScopeRuntime(options = {}) {
|
|
279
|
+
for (const dispose of [...nodeScopeFeatureDisposers]) dispose()
|
|
280
|
+
nodeScopeFeatureDisposers.clear()
|
|
281
|
+
for (const entry of featureAttachRefs.values())
|
|
282
|
+
try { entry.disposeCore() } catch { /* ignore */ }
|
|
283
|
+
featureAttachRefs.clear()
|
|
284
|
+
if (options.keepSubscribe) return
|
|
285
|
+
nodeScopeSubscribeCleanup?.()
|
|
286
|
+
nodeScopeSubscribeCleanup = null
|
|
287
|
+
nodeScopeWire = null
|
|
288
|
+
nodeActionHandlers.clear()
|
|
289
|
+
}
|
|
@@ -1,18 +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
|
-
|
|
9
|
-
/** accept/dial 挂起期间 ICE 信令 backlog 上限,防无 handler 时无限堆积 */
|
|
8
|
+
/** accept/dial 挂起期间 ICE 信令 backlog 上限 */
|
|
10
9
|
const SIGNAL_BACKLOG_MAX = 64
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
|
-
* 创建带 backlog 的缓冲信令会话。
|
|
14
12
|
* @param {(message: unknown) => Promise<void>} sendRemote 远端发送回调
|
|
15
|
-
* @returns {
|
|
13
|
+
* @returns {object} 信令会话
|
|
16
14
|
*/
|
|
17
15
|
export function createBufferedSignalSession(sendRemote) {
|
|
18
16
|
/** @type {Set<(message: unknown) => void>} */
|
|
@@ -21,17 +19,15 @@ export function createBufferedSignalSession(sendRemote) {
|
|
|
21
19
|
const backlog = []
|
|
22
20
|
return {
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
25
|
-
* @param {unknown} message 信令消息
|
|
22
|
+
* @param {unknown} message 待发送信令消息
|
|
26
23
|
* @returns {Promise<void>}
|
|
27
24
|
*/
|
|
28
25
|
async send(message) {
|
|
29
26
|
await sendRemote(message)
|
|
30
27
|
},
|
|
31
28
|
/**
|
|
32
|
-
*
|
|
33
|
-
* @
|
|
34
|
-
* @returns {() => void} 取消订阅函数
|
|
29
|
+
* @param {(message: unknown) => void} handler 远端消息回调
|
|
30
|
+
* @returns {() => void} 取消订阅
|
|
35
31
|
*/
|
|
36
32
|
onRemote(handler) {
|
|
37
33
|
handlers.add(handler)
|
|
@@ -40,8 +36,7 @@ export function createBufferedSignalSession(sendRemote) {
|
|
|
40
36
|
return () => handlers.delete(handler)
|
|
41
37
|
},
|
|
42
38
|
/**
|
|
43
|
-
*
|
|
44
|
-
* @param {unknown} message 信令消息
|
|
39
|
+
* @param {unknown} message 入站信令消息
|
|
45
40
|
* @returns {void}
|
|
46
41
|
*/
|
|
47
42
|
deliver(message) {
|
|
@@ -54,7 +49,6 @@ export function createBufferedSignalSession(sendRemote) {
|
|
|
54
49
|
handler(message)
|
|
55
50
|
},
|
|
56
51
|
/**
|
|
57
|
-
* 清空 backlog 与 handler。
|
|
58
52
|
* @returns {void}
|
|
59
53
|
*/
|
|
60
54
|
clear() {
|
|
@@ -65,9 +59,8 @@ export function createBufferedSignalSession(sendRemote) {
|
|
|
65
59
|
}
|
|
66
60
|
|
|
67
61
|
/**
|
|
68
|
-
* 判断信令 body 是否为发起方 offer(据此决定入站是否新建应答 PC)。
|
|
69
62
|
* @param {unknown} body 信令 body
|
|
70
|
-
* @returns {boolean}
|
|
63
|
+
* @returns {boolean} 是否为 WebRTC offer description
|
|
71
64
|
*/
|
|
72
65
|
function isOfferSignalBody(body) {
|
|
73
66
|
return !!body && typeof body === 'object'
|
|
@@ -76,30 +69,19 @@ function isOfferSignalBody(body) {
|
|
|
76
69
|
}
|
|
77
70
|
|
|
78
71
|
/**
|
|
79
|
-
*
|
|
80
|
-
* @returns {import('../link/providers/index.mjs').LinkProvider | null} 命中的 provider;无则 null
|
|
72
|
+
* @returns {import('../link/providers/index.mjs').LinkProvider | null} 首个需要 offer/answer 的 provider
|
|
81
73
|
*/
|
|
82
74
|
export function findOfferAnswerProvider() {
|
|
83
75
|
return listLinkProviders().find(provider => provider.caps?.needsOfferAnswer) ?? null
|
|
84
76
|
}
|
|
85
77
|
|
|
86
78
|
/**
|
|
87
|
-
*
|
|
88
|
-
* @
|
|
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> }} 入站信令与主动拨号
|
|
79
|
+
* @param {object} deps 依赖
|
|
80
|
+
* @returns {object} offer/answer 拨号
|
|
97
81
|
*/
|
|
98
82
|
export function createOfferAnswerDial(deps) {
|
|
99
83
|
const {
|
|
100
84
|
localIdentity,
|
|
101
|
-
iceServers,
|
|
102
|
-
selfTopic,
|
|
103
85
|
signalSessions,
|
|
104
86
|
registerResolvedLink,
|
|
105
87
|
trimToBudget,
|
|
@@ -107,28 +89,30 @@ export function createOfferAnswerDial(deps) {
|
|
|
107
89
|
} = deps
|
|
108
90
|
|
|
109
91
|
/**
|
|
110
|
-
*
|
|
111
|
-
* @param {string}
|
|
112
|
-
* @
|
|
113
|
-
* @returns {ReturnType<typeof createBufferedSignalSession>} 信令会话
|
|
92
|
+
* @param {string} remoteNodeHash 远端
|
|
93
|
+
* @param {string} connId 连接 id
|
|
94
|
+
* @returns {ReturnType<typeof createBufferedSignalSession>} 该连接的缓冲信令会话
|
|
114
95
|
*/
|
|
115
96
|
function createConnSession(remoteNodeHash, connId) {
|
|
116
97
|
const normalized = normalizeHex64(remoteNodeHash)
|
|
117
|
-
const topic = nodeRendezvousTopic(normalized)
|
|
118
98
|
return createBufferedSignalSession(async message => {
|
|
119
|
-
await
|
|
99
|
+
await sendNodeSignalPacket(normalized, {
|
|
120
100
|
type: 'signal',
|
|
121
101
|
from: localIdentity.nodeHash,
|
|
122
102
|
connId,
|
|
123
103
|
body: message,
|
|
124
|
-
})
|
|
104
|
+
})
|
|
125
105
|
})
|
|
126
106
|
}
|
|
127
107
|
|
|
128
108
|
/**
|
|
129
|
-
*
|
|
130
|
-
* @param {
|
|
131
|
-
* @
|
|
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
|
|
132
116
|
*/
|
|
133
117
|
async function buildConnLink({ provider, remoteNodeHash, connId, session, initiator }) {
|
|
134
118
|
try {
|
|
@@ -136,11 +120,16 @@ export function createOfferAnswerDial(deps) {
|
|
|
136
120
|
const link = await (initiator ? provider.dial : provider.accept)({
|
|
137
121
|
nodeHash: remoteNodeHash,
|
|
138
122
|
signal: session,
|
|
139
|
-
iceServers,
|
|
123
|
+
iceServers: deps.iceServers,
|
|
140
124
|
localIdentity,
|
|
141
125
|
})
|
|
142
126
|
if (!link) {
|
|
143
127
|
signalSessions.delete(connId)
|
|
128
|
+
nodeDebug('p2p:webrtc null', {
|
|
129
|
+
peer: shortHash(remoteNodeHash),
|
|
130
|
+
provider: provider.id,
|
|
131
|
+
role: initiator ? 'dial' : 'accept',
|
|
132
|
+
})
|
|
144
133
|
return null
|
|
145
134
|
}
|
|
146
135
|
link.onDown(() => signalSessions.delete(connId))
|
|
@@ -148,19 +137,24 @@ export function createOfferAnswerDial(deps) {
|
|
|
148
137
|
await registerResolvedLink(remoteNodeHash, link)
|
|
149
138
|
return getCanonicalLink(remoteNodeHash) ?? null
|
|
150
139
|
}
|
|
151
|
-
catch {
|
|
140
|
+
catch (error) {
|
|
152
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
|
+
})
|
|
153
148
|
return null
|
|
154
149
|
}
|
|
155
150
|
}
|
|
156
151
|
|
|
157
152
|
/**
|
|
158
|
-
*
|
|
159
|
-
* @param {Uint8Array} bytes 加密信令字节
|
|
153
|
+
* @param {Uint8Array} bytes 加密信令
|
|
160
154
|
* @returns {Promise<void>}
|
|
161
155
|
*/
|
|
162
156
|
async function handleIncomingSignal(bytes) {
|
|
163
|
-
const packet =
|
|
157
|
+
const packet = decryptNodeSignalPacket(localIdentity.nodeHash, bytes)
|
|
164
158
|
if (packet?.type !== 'signal') return
|
|
165
159
|
const remoteNodeHash = normalizeHex64(packet.from)
|
|
166
160
|
const connId = String(packet.connId || '')
|
|
@@ -170,6 +164,10 @@ export function createOfferAnswerDial(deps) {
|
|
|
170
164
|
if (!isOfferSignalBody(packet.body)) return
|
|
171
165
|
const provider = findOfferAnswerProvider()
|
|
172
166
|
if (!provider) return
|
|
167
|
+
nodeDebug('p2p:webrtc inbound offer', {
|
|
168
|
+
peer: shortHash(remoteNodeHash),
|
|
169
|
+
provider: provider.id,
|
|
170
|
+
})
|
|
173
171
|
session = createConnSession(remoteNodeHash, connId)
|
|
174
172
|
signalSessions.set(connId, session)
|
|
175
173
|
void buildConnLink({ provider, remoteNodeHash, connId, session, initiator: false })
|
|
@@ -178,10 +176,9 @@ export function createOfferAnswerDial(deps) {
|
|
|
178
176
|
}
|
|
179
177
|
|
|
180
178
|
/**
|
|
181
|
-
* 经 offer/answer provider 拨号(discovery signal + glare)。
|
|
182
179
|
* @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
|
|
183
|
-
* @param {string} remoteNodeHash 远端
|
|
184
|
-
* @returns {Promise<object | null>}
|
|
180
|
+
* @param {string} remoteNodeHash 远端 nodeHash
|
|
181
|
+
* @returns {Promise<object | null>} 建链成功返回规范链路,否则 null
|
|
185
182
|
*/
|
|
186
183
|
async function dialOfferAnswer(provider, remoteNodeHash) {
|
|
187
184
|
const connId = randomBytes(16).toString('hex')
|
package/transport/peer_pool.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
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'
|
|
@@ -97,9 +99,9 @@ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxP
|
|
|
97
99
|
/** @type {Map<string, string[]>} */
|
|
98
100
|
const bySource = new Map()
|
|
99
101
|
for (const id of exploreIds) {
|
|
100
|
-
const
|
|
101
|
-
if (!bySource.has(
|
|
102
|
-
bySource.get(
|
|
102
|
+
const source = exploreSources.get(id) || 'unknown'
|
|
103
|
+
if (!bySource.has(source)) bySource.set(source, [])
|
|
104
|
+
bySource.get(source).push(id)
|
|
103
105
|
}
|
|
104
106
|
for (const ids of bySource.values()) shuffleInPlace(ids)
|
|
105
107
|
const out = []
|
|
@@ -107,12 +109,12 @@ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxP
|
|
|
107
109
|
const picked = new Map()
|
|
108
110
|
while (out.length < k) {
|
|
109
111
|
let progressed = false
|
|
110
|
-
for (const [
|
|
112
|
+
for (const [source, ids] of bySource) {
|
|
111
113
|
if (out.length >= k) break
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
out.push(ids[
|
|
115
|
-
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)
|
|
116
118
|
progressed = true
|
|
117
119
|
}
|
|
118
120
|
if (!progressed) break
|
|
@@ -133,7 +135,7 @@ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxP
|
|
|
133
135
|
* inRoomNodeHashes?: Set<string> | string[] 群内在线 node_id;有则优先,仅全不可达时用 explore 中非房内节点
|
|
134
136
|
* hintSources?: Map<string, string> explore 节点来源(用于配额)
|
|
135
137
|
* }} options 选取参数(roster、peers、rep、limits、selfNodeHash)
|
|
136
|
-
* @returns {string[]} 目标
|
|
138
|
+
* @returns {string[]} 目标 peerId 列表(去重,长度 ≤ maxPeers)
|
|
137
139
|
*/
|
|
138
140
|
export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash, inRoomNodeHashes, hintSources }) {
|
|
139
141
|
const blocked = new Set(peers.blockedPeers)
|
|
@@ -292,10 +294,10 @@ export function applyRosterToPeerPool(options) {
|
|
|
292
294
|
/**
|
|
293
295
|
* 稀疏连接池:优先 trusted,再 explore,再其余在线节点。
|
|
294
296
|
* @param {string} groupId 群
|
|
295
|
-
* @param {{ peerId: string, remoteNodeHash?: string }[]} roster
|
|
297
|
+
* @param {{ peerId: string, remoteNodeHash?: string }[]} roster 在线表
|
|
296
298
|
* @param {object} groupSettings 物化群设置
|
|
297
299
|
* @param {string} selfNodeHash 本机 node_id
|
|
298
|
-
* @returns {string[]} 目标
|
|
300
|
+
* @returns {string[]} 目标 peerId(去重)
|
|
299
301
|
*/
|
|
300
302
|
export function pickFederationTargetPeerIds(groupId, roster, groupSettings, selfNodeHash) {
|
|
301
303
|
const limits = resolveFederationPoolLimits(groupSettings)
|
|
@@ -342,3 +344,103 @@ export function reconcilePeerPoolFromRoster(groupId, roster, groupSettings) {
|
|
|
342
344
|
const { trustedPeers, explorePeers } = applyRosterToPeerPool({ peers, rep, roster, limits })
|
|
343
345
|
mergeNetworkPeerPools({ trustedPeers, explorePeers })
|
|
344
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
|
+
}
|