@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
|
@@ -2,18 +2,21 @@ import { Buffer } from 'node:buffer'
|
|
|
2
2
|
|
|
3
3
|
import { compareHex64Asc, normalizeHex64 } from '../core/hexIds.mjs'
|
|
4
4
|
import { keyPairFromSeed } from '../crypto/crypto.mjs'
|
|
5
|
-
import {
|
|
5
|
+
import { watchVerifiedNodeAdvert, setDiscoveryLinkDialer, prepareConnectToNode } from '../discovery/index.mjs'
|
|
6
6
|
import { listLinkProviders } from '../link/providers/index.mjs'
|
|
7
7
|
import { ensureNodeSeed, getNodeHash } from '../node/identity.mjs'
|
|
8
|
+
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
9
|
+
import { loadPeerPoolView } from '../node/network.mjs'
|
|
8
10
|
import { createOverlayRouter } from '../overlay/index.mjs'
|
|
9
11
|
import { emitSafe } from '../utils/emit_safe.mjs'
|
|
10
12
|
import { createLruMap } from '../utils/lru.mjs'
|
|
11
13
|
|
|
12
|
-
import {
|
|
14
|
+
import { applyAdvertPeerHints } from './advert_ingest.mjs'
|
|
13
15
|
import { DEFAULT_ICE_SERVERS } from './ice_servers.mjs'
|
|
16
|
+
import { createMeshKeepalive } from './mesh_keepalive.mjs'
|
|
14
17
|
import { createOfferAnswerDial } from './offer_answer.mjs'
|
|
18
|
+
import { pickMeshEvictionVictim } from './peer_pool.mjs'
|
|
15
19
|
import { createRuntimeBootstrap } from './runtime_bootstrap.mjs'
|
|
16
|
-
import { nodeRendezvousTopic } from './signal_crypto.mjs'
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* 解析或从节点种子推导本地身份。
|
|
@@ -60,15 +63,23 @@ function subscribeBucket(buckets, key, listener) {
|
|
|
60
63
|
* @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array }} [options.localIdentity] 本地身份
|
|
61
64
|
* @param {RTCConfiguration['iceServers']} [options.iceServers] ICE 服务器列表(包内 webrtc provider 使用)
|
|
62
65
|
* @param {number} [options.maxActive] 最大并发活跃链路数
|
|
66
|
+
* @param {boolean} [options.meshKeepalive=true] 是否启用 mesh 保活(N/K 扫描拨号)
|
|
63
67
|
* @param {boolean} [options.autoRegisterDiscoveryProviders] 是否自动注册 discovery provider
|
|
64
68
|
* @param {boolean} [options.autoRegisterLinkProviders] 是否自动注册内置 link provider
|
|
65
69
|
* @returns {object} link registry 接口(对上层即 fount 网络:ensure/send/subscribe,无传输类型)
|
|
66
70
|
*/
|
|
67
71
|
export function createLinkRegistry(options = {}) {
|
|
68
72
|
const localIdentity = resolveLocalIdentity(options.localIdentity)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
73
|
+
let iceServers = options.iceServers?.length ? options.iceServers : DEFAULT_ICE_SERVERS
|
|
74
|
+
let maxActive = Math.max(4, Number(options.maxActive) || 32)
|
|
75
|
+
const savedMaxActive = maxActive
|
|
76
|
+
const meshKeepaliveEnabled = options.meshKeepalive !== false
|
|
77
|
+
/** @type {((nodeHash: string) => number) | null} */
|
|
78
|
+
let priorityWeightFunction = null
|
|
79
|
+
/** @type {Set<string>} */
|
|
80
|
+
let exploreLinkHashes = new Set()
|
|
81
|
+
/** @type {ReturnType<typeof createMeshKeepalive> | null} */
|
|
82
|
+
let meshKeepalive = null
|
|
72
83
|
/** @type {Map<string, object>} */
|
|
73
84
|
const links = new Map()
|
|
74
85
|
/** @type {Map<string, Promise<object | null>>} */
|
|
@@ -168,13 +179,12 @@ export function createLinkRegistry(options = {}) {
|
|
|
168
179
|
|
|
169
180
|
const bootstrap = createRuntimeBootstrap({
|
|
170
181
|
localIdentity,
|
|
171
|
-
selfTopic,
|
|
172
182
|
autoRegisterDiscoveryProviders: options.autoRegisterDiscoveryProviders !== false,
|
|
173
183
|
autoRegisterLinkProviders: options.autoRegisterLinkProviders !== false,
|
|
174
184
|
onInboundLink,
|
|
175
185
|
/**
|
|
176
|
-
* @param {Uint8Array} bytes
|
|
177
|
-
* @returns {void}
|
|
186
|
+
* @param {Uint8Array} bytes 入站加密信令
|
|
187
|
+
* @returns {Promise<void>}
|
|
178
188
|
*/
|
|
179
189
|
handleIncomingSignal: bytes => handleIncomingSignal(bytes),
|
|
180
190
|
})
|
|
@@ -185,47 +195,41 @@ export function createLinkRegistry(options = {}) {
|
|
|
185
195
|
* @returns {number} 权重值
|
|
186
196
|
*/
|
|
187
197
|
function scopeWeight(remoteNodeHash) {
|
|
188
|
-
let weight = 0
|
|
198
|
+
let weight = priorityWeightFunction?.(remoteNodeHash) ?? 0
|
|
189
199
|
for (const hashes of scopeInterests.values())
|
|
190
200
|
if (hashes.has(remoteNodeHash)) weight++
|
|
191
201
|
return weight
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
/**
|
|
195
|
-
* 超出 maxActive
|
|
205
|
+
* 超出 maxActive 时驱逐:探索链优先于熟人/scope 权重。
|
|
196
206
|
* @returns {Promise<void>}
|
|
197
207
|
*/
|
|
198
208
|
async function trimToBudget() {
|
|
199
209
|
if (links.size < maxActive) return
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|| (weight === victimWeight && compareHex64Asc(nodeHash, victimHash) < 0)
|
|
209
|
-
) {
|
|
210
|
-
victimHash = nodeHash
|
|
211
|
-
victimLink = link
|
|
212
|
-
victimWeight = weight
|
|
213
|
-
}
|
|
214
|
-
}
|
|
210
|
+
const peers = loadPeerPoolView()
|
|
211
|
+
const victimHash = pickMeshEvictionVictim(
|
|
212
|
+
[...links.keys()],
|
|
213
|
+
exploreLinkHashes,
|
|
214
|
+
peers.trustedPeers,
|
|
215
|
+
scopeWeight,
|
|
216
|
+
)
|
|
217
|
+
const victimLink = victimHash ? links.get(victimHash) : null
|
|
215
218
|
if (victimLink) await victimLink.close('budget-evict')
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
; ({ handleIncomingSignal, dialOfferAnswer } = createOfferAnswerDial({
|
|
219
222
|
localIdentity,
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
/**
|
|
224
|
+
* @returns {RTCConfiguration['iceServers']} 当前 ICE 服务器列表
|
|
225
|
+
*/
|
|
226
|
+
get iceServers() { return iceServers },
|
|
222
227
|
signalSessions,
|
|
223
228
|
registerResolvedLink,
|
|
224
229
|
trimToBudget,
|
|
225
230
|
/**
|
|
226
|
-
*
|
|
227
|
-
* @
|
|
228
|
-
* @returns {object | null | undefined} 规范链实例
|
|
231
|
+
* @param {string} remoteNodeHash 远端 nodeHash
|
|
232
|
+
* @returns {object | null} 已有规范链路
|
|
229
233
|
*/
|
|
230
234
|
getCanonicalLink: remoteNodeHash => links.get(normalizeHex64(remoteNodeHash)),
|
|
231
235
|
}))
|
|
@@ -262,33 +266,100 @@ export function createLinkRegistry(options = {}) {
|
|
|
262
266
|
if (links.has(normalized)) return links.get(normalized)
|
|
263
267
|
if (inflights.has(normalized)) return await inflights.get(normalized)
|
|
264
268
|
const task = (async () => {
|
|
269
|
+
nodeDebug('p2p:dial start', { peer: shortHash(normalized) })
|
|
270
|
+
await prepareConnectToNode(normalized)
|
|
265
271
|
const providers = listLinkProviders()
|
|
266
272
|
for (const provider of providers)
|
|
267
273
|
try {
|
|
268
274
|
if (typeof provider.canReach === 'function') {
|
|
269
275
|
const reachable = await Promise.resolve(provider.canReach({ nodeHash: normalized }))
|
|
270
|
-
if (!reachable)
|
|
276
|
+
if (!reachable) {
|
|
277
|
+
nodeDebug('p2p:dial skip', { peer: shortHash(normalized), provider: provider.id, reason: 'canReach=false' })
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
271
280
|
}
|
|
272
281
|
if (typeof provider.isAvailable === 'function') {
|
|
273
282
|
const available = await Promise.resolve(provider.isAvailable())
|
|
274
|
-
if (!available)
|
|
283
|
+
if (!available) {
|
|
284
|
+
nodeDebug('p2p:dial skip', { peer: shortHash(normalized), provider: provider.id, reason: 'isAvailable=false' })
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
275
287
|
}
|
|
288
|
+
nodeDebug('p2p:dial try', { peer: shortHash(normalized), provider: provider.id })
|
|
276
289
|
if (provider.caps?.needsOfferAnswer) {
|
|
277
290
|
const link = await dialOfferAnswer(provider, normalized)
|
|
278
|
-
if (link)
|
|
291
|
+
if (link) {
|
|
292
|
+
nodeDebug('p2p:dial ok', { peer: shortHash(normalized), provider: provider.id })
|
|
293
|
+
return link
|
|
294
|
+
}
|
|
295
|
+
nodeDebug('p2p:dial miss', { peer: shortHash(normalized), provider: provider.id })
|
|
279
296
|
continue
|
|
280
297
|
}
|
|
281
298
|
const link = await dialProvider(provider, normalized)
|
|
282
|
-
if (link)
|
|
299
|
+
if (link) {
|
|
300
|
+
nodeDebug('p2p:dial ok', { peer: shortHash(normalized), provider: provider.id })
|
|
301
|
+
return link
|
|
302
|
+
}
|
|
303
|
+
nodeDebug('p2p:dial miss', { peer: shortHash(normalized), provider: provider.id })
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
nodeDebug('p2p:dial fail', {
|
|
307
|
+
peer: shortHash(normalized),
|
|
308
|
+
provider: provider.id,
|
|
309
|
+
err: String(error?.message || error),
|
|
310
|
+
})
|
|
283
311
|
}
|
|
284
|
-
catch { /* dial failed — try next provider */ }
|
|
285
312
|
|
|
313
|
+
nodeDebug('p2p:dial exhausted', { peer: shortHash(normalized) })
|
|
286
314
|
return null
|
|
287
315
|
})().finally(() => inflights.delete(normalized))
|
|
288
316
|
inflights.set(normalized, task)
|
|
289
317
|
return await task
|
|
290
318
|
}
|
|
291
319
|
|
|
320
|
+
meshKeepalive = createMeshKeepalive({
|
|
321
|
+
registry: {
|
|
322
|
+
localIdentity,
|
|
323
|
+
/**
|
|
324
|
+
* @returns {Array<{ nodeHash: string, link: object }>} 当前链路列表
|
|
325
|
+
*/
|
|
326
|
+
listLinks: () => [...links.entries()].map(([nodeHash, link]) => ({ nodeHash, link })),
|
|
327
|
+
/**
|
|
328
|
+
* @param {string} nodeHash 目标 nodeHash
|
|
329
|
+
* @returns {object | null} 已有链路或 null
|
|
330
|
+
*/
|
|
331
|
+
getLink: nodeHash => links.get(normalizeHex64(nodeHash)) || null,
|
|
332
|
+
ensureLinkToNode: ensureDirectLinkToNode,
|
|
333
|
+
/**
|
|
334
|
+
* @param {(nodeHash: string) => void} listener link up 回调
|
|
335
|
+
* @returns {() => void} 取消订阅
|
|
336
|
+
*/
|
|
337
|
+
onLinkUp: listener => {
|
|
338
|
+
linkUpListeners.add(listener)
|
|
339
|
+
return () => linkUpListeners.delete(listener)
|
|
340
|
+
},
|
|
341
|
+
/**
|
|
342
|
+
* @param {(nodeHash: string, reason: string) => void} listener link down 回调
|
|
343
|
+
* @returns {() => void} 取消订阅
|
|
344
|
+
*/
|
|
345
|
+
onLinkDown: listener => {
|
|
346
|
+
linkDownListeners.add(listener)
|
|
347
|
+
return () => linkDownListeners.delete(listener)
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
enabled: meshKeepaliveEnabled,
|
|
351
|
+
})
|
|
352
|
+
exploreLinkHashes = meshKeepalive.exploreLinkHashes
|
|
353
|
+
setDiscoveryLinkDialer(ensureDirectLinkToNode)
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
*
|
|
357
|
+
*/
|
|
358
|
+
const ensureRuntimeWithMesh = async () => {
|
|
359
|
+
await bootstrap.ensureRuntime()
|
|
360
|
+
meshKeepalive?.start()
|
|
361
|
+
}
|
|
362
|
+
|
|
292
363
|
/**
|
|
293
364
|
* 经已有直连发送 envelope。
|
|
294
365
|
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
@@ -329,6 +400,13 @@ export function createLinkRegistry(options = {}) {
|
|
|
329
400
|
return overlayRouter
|
|
330
401
|
}
|
|
331
402
|
|
|
403
|
+
/**
|
|
404
|
+
* @returns {import('../overlay/index.mjs').OverlayRouter} overlay 路由器单例
|
|
405
|
+
*/
|
|
406
|
+
function ensureOverlayRouter() {
|
|
407
|
+
return getOverlayRouter()
|
|
408
|
+
}
|
|
409
|
+
|
|
332
410
|
/**
|
|
333
411
|
* 经 overlay 多跳 relay envelope 到无直连的节点。
|
|
334
412
|
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
@@ -416,8 +494,55 @@ export function createLinkRegistry(options = {}) {
|
|
|
416
494
|
buildLocalAdvert: bootstrap.buildLocalAdvert,
|
|
417
495
|
lanTcpPort: bootstrap.lanTcpPort,
|
|
418
496
|
whenListening: bootstrap.whenListening,
|
|
419
|
-
ensureRuntime:
|
|
497
|
+
ensureRuntime: ensureRuntimeWithMesh,
|
|
498
|
+
reloadDiscoveryRelays: bootstrap.reloadDiscoveryRelays,
|
|
499
|
+
ensureOverlayRouter,
|
|
500
|
+
getOverlayRouter,
|
|
420
501
|
ensureLinkToNode: ensureDirectLinkToNode,
|
|
502
|
+
/**
|
|
503
|
+
* 设置最大并发活跃链路数。
|
|
504
|
+
* @param {number} value 最大并发活跃链路数
|
|
505
|
+
* @returns {Promise<void>}
|
|
506
|
+
*/
|
|
507
|
+
async setMaxActive(value) {
|
|
508
|
+
maxActive = Math.max(4, Math.min(128, Math.floor(Number(value) || savedMaxActive)))
|
|
509
|
+
await trimToBudget()
|
|
510
|
+
},
|
|
511
|
+
/**
|
|
512
|
+
* @returns {number} 当前 maxActive
|
|
513
|
+
*/
|
|
514
|
+
getMaxActive() {
|
|
515
|
+
return maxActive
|
|
516
|
+
},
|
|
517
|
+
/**
|
|
518
|
+
* 设置 ICE 服务器列表。
|
|
519
|
+
* @param {RTCConfiguration['iceServers']} servers ICE 列表
|
|
520
|
+
* @returns {void}
|
|
521
|
+
*/
|
|
522
|
+
setIceServers(servers) {
|
|
523
|
+
iceServers = servers?.length ? servers : DEFAULT_ICE_SERVERS
|
|
524
|
+
},
|
|
525
|
+
/**
|
|
526
|
+
* @returns {RTCConfiguration['iceServers']} 当前 ICE 服务器列表
|
|
527
|
+
*/
|
|
528
|
+
getIceServers() {
|
|
529
|
+
return iceServers
|
|
530
|
+
},
|
|
531
|
+
/**
|
|
532
|
+
* infra/trim:额外优先级权重;null 清除。
|
|
533
|
+
* @param {((nodeHash: string) => number) | null} weightFunction 额外 trim 权重;null 清除
|
|
534
|
+
* @returns {void}
|
|
535
|
+
*/
|
|
536
|
+
setPriorityWeightFunction(weightFunction) {
|
|
537
|
+
priorityWeightFunction = typeof weightFunction === 'function' ? weightFunction : null
|
|
538
|
+
},
|
|
539
|
+
/**
|
|
540
|
+
* @param {string} nodeHash - 节点 64-hex hash
|
|
541
|
+
* @returns {number} 额外路由 trim 权重
|
|
542
|
+
*/
|
|
543
|
+
getPriorityWeight(nodeHash) {
|
|
544
|
+
return priorityWeightFunction?.(nodeHash) ?? 0
|
|
545
|
+
},
|
|
421
546
|
/**
|
|
422
547
|
* 获取到指定节点的活跃链路。
|
|
423
548
|
* @param {string} nodeHash 节点 64 hex
|
|
@@ -484,18 +609,17 @@ export function createLinkRegistry(options = {}) {
|
|
|
484
609
|
registerScopeAuthorizer,
|
|
485
610
|
subscribeScope,
|
|
486
611
|
/**
|
|
487
|
-
*
|
|
612
|
+
* 监听指定节点的 advert(per-hash,无 topic)。
|
|
488
613
|
* @param {string} nodeHash 目标节点 64 hex
|
|
489
614
|
* @param {(verifiedNodeHash: string, body: object) => void | Promise<void>} onAdvert advert 回调
|
|
490
|
-
* @returns {Promise<() => void>}
|
|
615
|
+
* @returns {Promise<() => void>} 取消函数
|
|
491
616
|
*/
|
|
492
|
-
async
|
|
493
|
-
const
|
|
494
|
-
return await
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
await Promise.resolve(onAdvert(ingested.verifiedNodeHash, ingested.body))
|
|
617
|
+
async watchNodeAdvert(nodeHash, onAdvert) {
|
|
618
|
+
const hash = normalizeHex64(nodeHash)
|
|
619
|
+
return await watchVerifiedNodeAdvert(hash, async (verifiedNodeHash, body, meta) => {
|
|
620
|
+
applyAdvertPeerHints(verifiedNodeHash, body, meta)
|
|
621
|
+
recentAdverts.touch(verifiedNodeHash, Date.now())
|
|
622
|
+
await Promise.resolve(onAdvert(verifiedNodeHash, body))
|
|
499
623
|
})
|
|
500
624
|
},
|
|
501
625
|
recentAdverts,
|
|
@@ -505,6 +629,8 @@ export function createLinkRegistry(options = {}) {
|
|
|
505
629
|
* @returns {Promise<void>}
|
|
506
630
|
*/
|
|
507
631
|
async shutdown() {
|
|
632
|
+
await meshKeepalive?.stop()
|
|
633
|
+
setDiscoveryLinkDialer(null)
|
|
508
634
|
await bootstrap.shutdown()
|
|
509
635
|
overlayRouter?.close()
|
|
510
636
|
overlayRouter = null
|
|
@@ -519,6 +645,26 @@ export function createLinkRegistry(options = {}) {
|
|
|
519
645
|
}
|
|
520
646
|
|
|
521
647
|
let defaultRegistry = null
|
|
648
|
+
/** @type {object | null} */
|
|
649
|
+
let pendingRegistryOptions = null
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* 在首次 getLinkRegistry 前配置默认 registry 选项。
|
|
653
|
+
* @param {object} options createLinkRegistry 选项
|
|
654
|
+
* @returns {void}
|
|
655
|
+
*/
|
|
656
|
+
export function configureLinkRegistry(options = {}) {
|
|
657
|
+
if (defaultRegistry) throw new Error('p2p: configureLinkRegistry must run before getLinkRegistry')
|
|
658
|
+
pendingRegistryOptions = { ...pendingRegistryOptions, ...options }
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* @returns {void}
|
|
663
|
+
*/
|
|
664
|
+
export function resetLinkRegistryForTests() {
|
|
665
|
+
defaultRegistry = null
|
|
666
|
+
pendingRegistryOptions = null
|
|
667
|
+
}
|
|
522
668
|
|
|
523
669
|
/**
|
|
524
670
|
* 默认 registry 尚未创建时暂存的 scope authorizer 条目。
|
|
@@ -533,7 +679,8 @@ const pendingScopeAuthorizers = []
|
|
|
533
679
|
*/
|
|
534
680
|
export function getLinkRegistry() {
|
|
535
681
|
if (defaultRegistry) return defaultRegistry
|
|
536
|
-
defaultRegistry = createLinkRegistry()
|
|
682
|
+
defaultRegistry = createLinkRegistry(pendingRegistryOptions || {})
|
|
683
|
+
pendingRegistryOptions = null
|
|
537
684
|
for (const entry of pendingScopeAuthorizers)
|
|
538
685
|
entry.unregister = defaultRegistry.registerScopeAuthorizer(entry.prefix, entry.authorizer)
|
|
539
686
|
pendingScopeAuthorizers.length = 0
|
|
@@ -543,31 +690,35 @@ export function getLinkRegistry() {
|
|
|
543
690
|
/**
|
|
544
691
|
* 默认 registry 方法代理。
|
|
545
692
|
* @param {string} name registry 方法名
|
|
546
|
-
* @returns {(...
|
|
693
|
+
* @returns {(...methodArguments: unknown[]) => unknown} 绑定到 getLinkRegistry()[name] 的函数
|
|
547
694
|
*/
|
|
548
|
-
const bindRegistryMethod = name => (...
|
|
695
|
+
const bindRegistryMethod = name => (...methodArguments) => getLinkRegistry()[name](...methodArguments)
|
|
549
696
|
|
|
550
|
-
/** @type {(...
|
|
697
|
+
/** 确保 overlay 路由器已创建。 @type {(...methodArguments: unknown[]) => unknown} */
|
|
698
|
+
export const ensureOverlayRouter = bindRegistryMethod('ensureOverlayRouter')
|
|
699
|
+
/** 热重载 discovery relay 配置。 @type {(...methodArguments: unknown[]) => unknown} */
|
|
700
|
+
export const reloadDiscoveryRelays = bindRegistryMethod('reloadDiscoveryRelays')
|
|
701
|
+
/** 确保到 nodeHash 的活跃链路。 @type {(...methodArguments: unknown[]) => unknown} */
|
|
551
702
|
export const ensureLinkToNode = bindRegistryMethod('ensureLinkToNode')
|
|
552
|
-
/** @type {(...
|
|
703
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
553
704
|
export const getLink = bindRegistryMethod('getLink')
|
|
554
|
-
/** @type {(...
|
|
705
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
555
706
|
export const listLinks = bindRegistryMethod('listLinks')
|
|
556
|
-
/** @type {(...
|
|
707
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
557
708
|
export const closeLink = bindRegistryMethod('closeLink')
|
|
558
|
-
/** @type {(...
|
|
709
|
+
/** 经活跃链路向节点发送 envelope。 @type {(...methodArguments: unknown[]) => unknown} */
|
|
559
710
|
export const sendToNodeLink = bindRegistryMethod('sendToNodeLink')
|
|
560
|
-
/** @type {(...
|
|
711
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
561
712
|
export const relayEnvelopeToNode = bindRegistryMethod('relayEnvelopeToNode')
|
|
562
|
-
/** @type {(...
|
|
713
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
563
714
|
export const onLinkUp = bindRegistryMethod('onLinkUp')
|
|
564
|
-
/** @type {(...
|
|
715
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
565
716
|
export const onLinkDown = bindRegistryMethod('onLinkDown')
|
|
566
|
-
/** @type {(...
|
|
717
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
567
718
|
export const registerScopeInterest = bindRegistryMethod('registerScopeInterest')
|
|
568
|
-
/** @type {(...
|
|
719
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
569
720
|
export const releaseScopeInterest = bindRegistryMethod('releaseScopeInterest')
|
|
570
|
-
/** @type {(...
|
|
721
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
571
722
|
export const subscribeScope = bindRegistryMethod('subscribeScope')
|
|
572
723
|
|
|
573
724
|
/**
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
|
+
import { listVisibleNodeHashes as discoveryListVisible } from '../discovery/index.mjs'
|
|
3
|
+
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
4
|
+
import { applyNetworkHint, loadPeerPoolView, promoteExplorePeer } from '../node/network.mjs'
|
|
5
|
+
import { loadReputation } from '../node/reputation_store.mjs'
|
|
6
|
+
import { getRoutingProfile } from '../node/routing_profile.mjs'
|
|
7
|
+
|
|
8
|
+
import { pickMeshEvictionVictim, resolveMeshPoolLimits, selectMeshLinkTargets } from './peer_pool.mjs'
|
|
9
|
+
import { loadTransportTunables } from './tunables.mjs'
|
|
10
|
+
|
|
11
|
+
/** 本机主动关链:不清槽重拨,由下次 tick / 调用方决定。 */
|
|
12
|
+
const INTENTIONAL_CLOSE = new Set([
|
|
13
|
+
'budget-evict',
|
|
14
|
+
'manual-close',
|
|
15
|
+
'registry-shutdown',
|
|
16
|
+
'inbound-no-nodehash',
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string | undefined} reason 关链原因
|
|
21
|
+
* @returns {boolean} 是否为本机主动关链(不重拨)
|
|
22
|
+
*/
|
|
23
|
+
export function isMeshIntentionalClose(reason) {
|
|
24
|
+
return INTENTIONAL_CLOSE.has(String(reason || ''))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 创建 mesh 保活控制器:扫描可见节点、按 N/K 拨号、稳定探索晋升熟人。
|
|
29
|
+
* @param {object} deps 依赖
|
|
30
|
+
* @param {object} deps.registry link registry(ensureLinkToNode / listLinks / onLinkUp / onLinkDown)
|
|
31
|
+
* @param {boolean} [deps.enabled=true] 是否启用
|
|
32
|
+
* @returns {{ exploreLinkHashes: Set<string>, start: () => void, stop: () => Promise<void> }} mesh 保活控制器
|
|
33
|
+
*/
|
|
34
|
+
export function createMeshKeepalive(deps) {
|
|
35
|
+
const { registry, enabled = true } = deps
|
|
36
|
+
const tunables = loadTransportTunables()
|
|
37
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
38
|
+
let timer = null
|
|
39
|
+
/** @type {Set<string>} */
|
|
40
|
+
const exploreLinks = new Set()
|
|
41
|
+
/** @type {Map<string, number>} */
|
|
42
|
+
const exploreStableSince = new Map()
|
|
43
|
+
/** @type {(() => void) | null} */
|
|
44
|
+
let stopLinkDown = null
|
|
45
|
+
/** @type {(() => void) | null} */
|
|
46
|
+
let stopLinkUp = null
|
|
47
|
+
/** @type {Promise<void> | null} */
|
|
48
|
+
let tickInflight = null
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 非熟人活跃链记入探索槽(含入站);熟人则清掉探索标记。
|
|
52
|
+
* @param {string} nodeHash 对端
|
|
53
|
+
* @param {string[]} [trustedPeers] 熟人表;省略则读盘
|
|
54
|
+
* @returns {void}
|
|
55
|
+
*/
|
|
56
|
+
function syncExploreMark(nodeHash, trustedPeers) {
|
|
57
|
+
const hash = normalizeHex64(nodeHash)
|
|
58
|
+
if (!hash) return
|
|
59
|
+
const trusted = trustedPeers ?? loadPeerPoolView().trustedPeers
|
|
60
|
+
if (trusted.includes(hash)) {
|
|
61
|
+
exploreLinks.delete(hash)
|
|
62
|
+
exploreStableSince.delete(hash)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
exploreLinks.add(hash)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 为拨号腾出空位:优先踢探索。
|
|
70
|
+
* @param {number} needSlots 需要空位
|
|
71
|
+
* @param {string[]} trustedPeers 熟人表
|
|
72
|
+
* @returns {Promise<void>}
|
|
73
|
+
*/
|
|
74
|
+
async function evictExploreForRoom(needSlots, trustedPeers) {
|
|
75
|
+
for (let i = 0; i < needSlots; i++) {
|
|
76
|
+
const connected = registry.listLinks().map(entry => entry.nodeHash)
|
|
77
|
+
const victimHash = pickMeshEvictionVictim(connected, exploreLinks, trustedPeers, () => 0)
|
|
78
|
+
if (!victimHash || trustedPeers.includes(victimHash)) break
|
|
79
|
+
const entry = registry.listLinks().find(item => item.nodeHash === victimHash)
|
|
80
|
+
if (!entry?.link?.close) break
|
|
81
|
+
await entry.link.close('budget-evict')
|
|
82
|
+
exploreLinks.delete(victimHash)
|
|
83
|
+
exploreStableSince.delete(victimHash)
|
|
84
|
+
nodeDebug('p2p:mesh evict', { peer: shortHash(victimHash), reason: 'budget-evict' })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @returns {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
async function tick() {
|
|
92
|
+
if (!enabled) return
|
|
93
|
+
const limits = resolveMeshPoolLimits(getRoutingProfile(), tunables)
|
|
94
|
+
const promoteMs = Math.max(60_000, Number(tunables.meshPromoteStableMs) || 30 * 60_000)
|
|
95
|
+
const scanLimit = Math.max(8, Number(tunables.meshScanLimit) || 64)
|
|
96
|
+
const visible = await discoveryListVisible({ limit: scanLimit })
|
|
97
|
+
for (const hash of visible)
|
|
98
|
+
applyNetworkHint({ nodeHash: hash, source: 'mesh:scan', kind: 'visible', weight: 0.15 })
|
|
99
|
+
|
|
100
|
+
const peers = loadPeerPoolView()
|
|
101
|
+
const rep = loadReputation()
|
|
102
|
+
const now = Date.now()
|
|
103
|
+
const connected = new Set(registry.listLinks().map(entry => entry.nodeHash))
|
|
104
|
+
for (const nodeHash of connected)
|
|
105
|
+
syncExploreMark(nodeHash, peers.trustedPeers)
|
|
106
|
+
|
|
107
|
+
const targets = selectMeshLinkTargets({
|
|
108
|
+
selfNodeHash: registry.localIdentity.nodeHash,
|
|
109
|
+
trustedPeers: peers.trustedPeers,
|
|
110
|
+
exploreCandidates: [...new Set([...peers.explorePeers, ...visible])],
|
|
111
|
+
hintSources: peers.hintSources,
|
|
112
|
+
limits,
|
|
113
|
+
connectedHashes: connected,
|
|
114
|
+
rep,
|
|
115
|
+
blockedPeers: peers.blockedPeers,
|
|
116
|
+
now,
|
|
117
|
+
})
|
|
118
|
+
const toDial = targets.filter(nodeHash => !connected.has(nodeHash))
|
|
119
|
+
nodeDebug('p2p:mesh tick', {
|
|
120
|
+
N: limits.N,
|
|
121
|
+
K_max: limits.K_max,
|
|
122
|
+
visible: visible.map(hash => shortHash(hash)),
|
|
123
|
+
connected: [...connected].map(hash => shortHash(hash)),
|
|
124
|
+
dial: toDial.map(hash => shortHash(hash)),
|
|
125
|
+
})
|
|
126
|
+
const overflow = connected.size + toDial.length - limits.N
|
|
127
|
+
if (overflow > 0)
|
|
128
|
+
await evictExploreForRoom(overflow, peers.trustedPeers)
|
|
129
|
+
|
|
130
|
+
for (const nodeHash of toDial)
|
|
131
|
+
void registry.ensureLinkToNode(nodeHash).then(link => {
|
|
132
|
+
if (link) {
|
|
133
|
+
syncExploreMark(nodeHash, peers.trustedPeers)
|
|
134
|
+
nodeDebug('p2p:mesh dial ok', { peer: shortHash(nodeHash), provider: link.providerId })
|
|
135
|
+
}
|
|
136
|
+
else
|
|
137
|
+
nodeDebug('p2p:mesh dial miss', { peer: shortHash(nodeHash) })
|
|
138
|
+
}).catch(error => {
|
|
139
|
+
nodeDebug('p2p:mesh dial fail', {
|
|
140
|
+
peer: shortHash(nodeHash),
|
|
141
|
+
err: String(error?.message || error),
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const connectedAfter = new Set(registry.listLinks().map(entry => entry.nodeHash))
|
|
146
|
+
for (const nodeHash of connectedAfter) {
|
|
147
|
+
if (peers.trustedPeers.includes(nodeHash)) {
|
|
148
|
+
exploreStableSince.delete(nodeHash)
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
if (!exploreLinks.has(nodeHash)) continue
|
|
152
|
+
const since = exploreStableSince.get(nodeHash) ?? now
|
|
153
|
+
if (!exploreStableSince.has(nodeHash)) exploreStableSince.set(nodeHash, since)
|
|
154
|
+
else if (now - since >= promoteMs) {
|
|
155
|
+
promoteExplorePeer(nodeHash)
|
|
156
|
+
exploreLinks.delete(nodeHash)
|
|
157
|
+
exploreStableSince.delete(nodeHash)
|
|
158
|
+
nodeDebug('p2p:mesh promote', { peer: shortHash(nodeHash) })
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const hash of [...exploreStableSince.keys()])
|
|
162
|
+
if (!connectedAfter.has(hash)) exploreStableSince.delete(hash)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @returns {Promise<void>}
|
|
167
|
+
*/
|
|
168
|
+
function runTick() {
|
|
169
|
+
if (tickInflight) return tickInflight
|
|
170
|
+
tickInflight = tick().catch(error => {
|
|
171
|
+
nodeDebug('p2p:mesh tick fail', { err: String(error?.message || error) })
|
|
172
|
+
}).finally(() => { tickInflight = null })
|
|
173
|
+
return tickInflight
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
/** @returns {Set<string>} 探索链路集合(trim 时优先驱逐) */
|
|
178
|
+
exploreLinkHashes: exploreLinks,
|
|
179
|
+
/**
|
|
180
|
+
* 启动 mesh 扫描 / 拨号 / 晋升循环。
|
|
181
|
+
* @returns {void}
|
|
182
|
+
*/
|
|
183
|
+
start() {
|
|
184
|
+
if (!enabled || timer) return
|
|
185
|
+
nodeDebug('p2p:mesh start', { self: shortHash(registry.localIdentity?.nodeHash) })
|
|
186
|
+
stopLinkUp = registry.onLinkUp?.(nodeHash => {
|
|
187
|
+
syncExploreMark(nodeHash)
|
|
188
|
+
}) ?? null
|
|
189
|
+
stopLinkDown = registry.onLinkDown((nodeHash, reason) => {
|
|
190
|
+
const hash = normalizeHex64(nodeHash)
|
|
191
|
+
exploreLinks.delete(hash)
|
|
192
|
+
exploreStableSince.delete(hash)
|
|
193
|
+
nodeDebug('p2p:mesh link down', { peer: shortHash(hash), reason })
|
|
194
|
+
// 主动关链不立刻补洞(避免 budget-evict 刚踢又连);意外断链用 tick 按 N/K 重选,而非粘住原对端。
|
|
195
|
+
if (isMeshIntentionalClose(reason)) return
|
|
196
|
+
void runTick()
|
|
197
|
+
})
|
|
198
|
+
void runTick()
|
|
199
|
+
timer = setInterval(() => { void runTick() }, Math.max(15_000, Number(tunables.meshKeepaliveIntervalMs) || 60_000))
|
|
200
|
+
timer.unref?.()
|
|
201
|
+
},
|
|
202
|
+
/**
|
|
203
|
+
* 停止保活并清空探索标记。
|
|
204
|
+
* @returns {Promise<void>}
|
|
205
|
+
*/
|
|
206
|
+
async stop() {
|
|
207
|
+
if (timer) { clearInterval(timer); timer = null }
|
|
208
|
+
stopLinkUp?.()
|
|
209
|
+
stopLinkUp = null
|
|
210
|
+
stopLinkDown?.()
|
|
211
|
+
stopLinkDown = null
|
|
212
|
+
exploreLinks.clear()
|
|
213
|
+
exploreStableSince.clear()
|
|
214
|
+
if (tickInflight) await tickInflight.catch(() => { })
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
}
|