@steve02081504/fount-p2p 0.0.11 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/core/bytes_codec.mjs +65 -10
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- package/discovery/bt/index.mjs +5 -5
- package/discovery/bt/runtime.mjs +3 -3
- package/discovery/mdns.mjs +7 -1
- package/discovery/nostr.mjs +53 -99
- package/federation/chunk_fetch_pending.mjs +4 -5
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/topo_order_memo.mjs +11 -4
- package/files/chunk_fetch.mjs +2 -2
- package/files/evfs.mjs +2 -2
- package/link/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -124
- package/link/handshake.mjs +5 -5
- package/link/pipe.mjs +49 -75
- package/link/providers/ble_gatt.mjs +14 -26
- 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 +21 -9
- package/mailbox/deliver_or_store.mjs +1 -1
- package/node/identity.mjs +4 -4
- package/node/network.mjs +9 -9
- package/overlay/index.mjs +13 -13
- package/package.json +1 -1
- package/rooms/scoped_link.mjs +19 -35
- package/schemas/discovery.mjs +1 -2
- package/schemas/federation_pull.mjs +2 -2
- package/schemas/part_query.mjs +1 -1
- package/schemas/remote_event.mjs +1 -1
- package/transport/advert_ingest.mjs +20 -0
- package/transport/group_link_set.mjs +22 -41
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +113 -105
- package/transport/offer_answer.mjs +6 -2
- package/transport/peer_pool.mjs +53 -64
- package/transport/remote_user_room.mjs +13 -15
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/runtime_bootstrap.mjs +8 -2
- package/transport/signal_crypto.mjs +25 -3
- package/transport/user_room.mjs +22 -15
- package/trust_graph/send.mjs +1 -1
- package/utils/emit_safe.mjs +11 -0
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +6 -8
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +2 -3
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
|
@@ -2,21 +2,18 @@ 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 { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
|
|
6
5
|
import { subscribeTopic } from '../discovery/index.mjs'
|
|
7
|
-
import { verifySignedAdvert } from '../link/handshake.mjs'
|
|
8
6
|
import { listLinkProviders } from '../link/providers/index.mjs'
|
|
9
7
|
import { ensureNodeSeed, getNodeHash } from '../node/identity.mjs'
|
|
10
8
|
import { createOverlayRouter } from '../overlay/index.mjs'
|
|
9
|
+
import { emitSafe } from '../utils/emit_safe.mjs'
|
|
11
10
|
import { createLruMap } from '../utils/lru.mjs'
|
|
12
11
|
|
|
12
|
+
import { ingestSignedAdvert } from './advert_ingest.mjs'
|
|
13
13
|
import { DEFAULT_ICE_SERVERS } from './ice_servers.mjs'
|
|
14
14
|
import { createOfferAnswerDial } from './offer_answer.mjs'
|
|
15
15
|
import { createRuntimeBootstrap } from './runtime_bootstrap.mjs'
|
|
16
|
-
import {
|
|
17
|
-
decryptSignalPacket,
|
|
18
|
-
nodeRendezvousTopic,
|
|
19
|
-
} from './signal_crypto.mjs'
|
|
16
|
+
import { nodeRendezvousTopic } from './signal_crypto.mjs'
|
|
20
17
|
|
|
21
18
|
/**
|
|
22
19
|
* 解析或从节点种子推导本地身份。
|
|
@@ -104,8 +101,8 @@ export function createLinkRegistry(options = {}) {
|
|
|
104
101
|
* @returns {boolean} 候选链应保留为规范链时 true
|
|
105
102
|
*/
|
|
106
103
|
function linkIsPreferred(link, remoteNodeHash, against = null) {
|
|
107
|
-
const level =
|
|
108
|
-
const againstLevel =
|
|
104
|
+
const level = link.level || 0
|
|
105
|
+
const againstLevel = against?.level || 0
|
|
109
106
|
if (against && level !== againstLevel) return level > againstLevel
|
|
110
107
|
const cmp = compareHex64Asc(localIdentity.nodeHash, remoteNodeHash)
|
|
111
108
|
return link.initiator ? cmp < 0 : cmp > 0
|
|
@@ -125,8 +122,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
125
122
|
const wasCanonical = links.get(remoteNodeHash) === link
|
|
126
123
|
if (!wasCanonical) return
|
|
127
124
|
links.delete(remoteNodeHash)
|
|
128
|
-
|
|
129
|
-
try { listener(remoteNodeHash, reason) } catch { /* ignore */ }
|
|
125
|
+
emitSafe(linkDownListeners, remoteNodeHash, reason)
|
|
130
126
|
})
|
|
131
127
|
}
|
|
132
128
|
|
|
@@ -140,15 +136,14 @@ export function createLinkRegistry(options = {}) {
|
|
|
140
136
|
const normalized = normalizeHex64(remoteNodeHash)
|
|
141
137
|
const existing = links.get(normalized)
|
|
142
138
|
if (existing && existing !== candidate && !linkIsPreferred(candidate, normalized, existing)) {
|
|
143
|
-
await candidate.close(
|
|
139
|
+
await candidate.close(candidate.level !== existing.level ? 'provider-loser' : 'glare-loser')
|
|
144
140
|
return
|
|
145
141
|
}
|
|
146
142
|
links.set(normalized, candidate)
|
|
147
143
|
wireLink(normalized, candidate)
|
|
148
144
|
if (existing && existing !== candidate)
|
|
149
|
-
await existing.close(
|
|
150
|
-
|
|
151
|
-
try { listener(normalized, candidate) } catch { /* ignore */ }
|
|
145
|
+
await existing.close(candidate.level !== existing.level ? 'provider-replaced' : 'glare-replaced')
|
|
146
|
+
emitSafe(linkUpListeners, normalized, candidate)
|
|
152
147
|
}
|
|
153
148
|
|
|
154
149
|
/**
|
|
@@ -177,6 +172,10 @@ export function createLinkRegistry(options = {}) {
|
|
|
177
172
|
autoRegisterDiscoveryProviders: options.autoRegisterDiscoveryProviders !== false,
|
|
178
173
|
autoRegisterLinkProviders: options.autoRegisterLinkProviders !== false,
|
|
179
174
|
onInboundLink,
|
|
175
|
+
/**
|
|
176
|
+
* @param {Uint8Array} bytes 入站信令字节
|
|
177
|
+
* @returns {void}
|
|
178
|
+
*/
|
|
180
179
|
handleIncomingSignal: bytes => handleIncomingSignal(bytes),
|
|
181
180
|
})
|
|
182
181
|
|
|
@@ -198,13 +197,25 @@ export function createLinkRegistry(options = {}) {
|
|
|
198
197
|
*/
|
|
199
198
|
async function trimToBudget() {
|
|
200
199
|
if (links.size < maxActive) return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
let victimHash = null
|
|
201
|
+
let victimLink = null
|
|
202
|
+
let victimWeight = Infinity
|
|
203
|
+
for (const [nodeHash, link] of links) {
|
|
204
|
+
const weight = scopeWeight(nodeHash)
|
|
205
|
+
if (
|
|
206
|
+
victimHash == null
|
|
207
|
+
|| weight < victimWeight
|
|
208
|
+
|| (weight === victimWeight && compareHex64Asc(nodeHash, victimHash) < 0)
|
|
209
|
+
) {
|
|
210
|
+
victimHash = nodeHash
|
|
211
|
+
victimLink = link
|
|
212
|
+
victimWeight = weight
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (victimLink) await victimLink.close('budget-evict')
|
|
205
216
|
}
|
|
206
217
|
|
|
207
|
-
;({ handleIncomingSignal, dialOfferAnswer } = createOfferAnswerDial({
|
|
218
|
+
; ({ handleIncomingSignal, dialOfferAnswer } = createOfferAnswerDial({
|
|
208
219
|
localIdentity,
|
|
209
220
|
iceServers,
|
|
210
221
|
selfTopic,
|
|
@@ -327,8 +338,9 @@ export function createLinkRegistry(options = {}) {
|
|
|
327
338
|
async function relayEnvelopeToNode(remoteNodeHash, envelope) {
|
|
328
339
|
if (!links.size || envelope?.scope === 'overlay') return false
|
|
329
340
|
try {
|
|
330
|
-
const
|
|
331
|
-
await
|
|
341
|
+
const overlay = getOverlayRouter()
|
|
342
|
+
const path = await overlay.discoverRoute(remoteNodeHash)
|
|
343
|
+
await overlay.relay(path, envelope)
|
|
332
344
|
return true
|
|
333
345
|
}
|
|
334
346
|
catch {
|
|
@@ -347,6 +359,17 @@ export function createLinkRegistry(options = {}) {
|
|
|
347
359
|
|| await relayEnvelopeToNode(remoteNodeHash, envelope)
|
|
348
360
|
}
|
|
349
361
|
|
|
362
|
+
/**
|
|
363
|
+
* 同步结果直接返回;thenable 才 await(避免每条 envelope 造 microtask)。
|
|
364
|
+
* @param {unknown} value 可能为 Promise 的返回值
|
|
365
|
+
* @returns {Promise<unknown>} 已 resolve 的值
|
|
366
|
+
*/
|
|
367
|
+
async function maybeAwait(value) {
|
|
368
|
+
if (value != null && typeof /** @type {{ then?: unknown }} */ value.then === 'function')
|
|
369
|
+
return await value
|
|
370
|
+
return value
|
|
371
|
+
}
|
|
372
|
+
|
|
350
373
|
/**
|
|
351
374
|
* 将入站 envelope 派发到 scope 监听器(经 authorizer 校验)。
|
|
352
375
|
* @param {string} senderNodeHash 发送方节点 64 hex
|
|
@@ -355,16 +378,16 @@ export function createLinkRegistry(options = {}) {
|
|
|
355
378
|
* @returns {Promise<void>}
|
|
356
379
|
*/
|
|
357
380
|
async function dispatchEnvelope(senderNodeHash, envelope, link) {
|
|
358
|
-
const scope =
|
|
381
|
+
const scope = envelope.scope || ''
|
|
359
382
|
for (const [prefix, authorizer] of scopeAuthorizers.entries())
|
|
360
383
|
if (scope.startsWith(prefix)) {
|
|
361
|
-
const allowed = await
|
|
384
|
+
const allowed = await maybeAwait(authorizer(scope, senderNodeHash, envelope, link))
|
|
362
385
|
if (!allowed) return
|
|
363
386
|
}
|
|
364
387
|
for (const [prefix, listeners] of scopeListeners.entries())
|
|
365
388
|
if (scope.startsWith(prefix))
|
|
366
389
|
for (const listener of listeners)
|
|
367
|
-
await
|
|
390
|
+
await maybeAwait(listener(senderNodeHash, envelope, link))
|
|
368
391
|
}
|
|
369
392
|
|
|
370
393
|
/**
|
|
@@ -374,7 +397,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
374
397
|
* @returns {() => void} 取消订阅函数
|
|
375
398
|
*/
|
|
376
399
|
function subscribeScope(prefix, listener) {
|
|
377
|
-
return subscribeBucket(scopeListeners,
|
|
400
|
+
return subscribeBucket(scopeListeners, prefix, listener)
|
|
378
401
|
}
|
|
379
402
|
|
|
380
403
|
/**
|
|
@@ -384,8 +407,8 @@ export function createLinkRegistry(options = {}) {
|
|
|
384
407
|
* @returns {() => void} 取消注册函数
|
|
385
408
|
*/
|
|
386
409
|
function registerScopeAuthorizer(prefix, authorizer) {
|
|
387
|
-
scopeAuthorizers.set(
|
|
388
|
-
return () => scopeAuthorizers.delete(
|
|
410
|
+
scopeAuthorizers.set(prefix, authorizer)
|
|
411
|
+
return () => scopeAuthorizers.delete(prefix)
|
|
389
412
|
}
|
|
390
413
|
|
|
391
414
|
return {
|
|
@@ -448,7 +471,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
448
471
|
* @returns {void}
|
|
449
472
|
*/
|
|
450
473
|
registerScopeInterest(scope, nodeHashes) {
|
|
451
|
-
scopeInterests.set(
|
|
474
|
+
scopeInterests.set(scope, new Set((nodeHashes || []).map(normalizeHex64).filter(Boolean)))
|
|
452
475
|
},
|
|
453
476
|
/**
|
|
454
477
|
* 释放 scope 兴趣。
|
|
@@ -456,7 +479,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
456
479
|
* @returns {void}
|
|
457
480
|
*/
|
|
458
481
|
releaseScopeInterest(scope) {
|
|
459
|
-
scopeInterests.delete(
|
|
482
|
+
scopeInterests.delete(scope)
|
|
460
483
|
},
|
|
461
484
|
registerScopeAuthorizer,
|
|
462
485
|
subscribeScope,
|
|
@@ -469,13 +492,10 @@ export function createLinkRegistry(options = {}) {
|
|
|
469
492
|
async subscribeNodeAdvert(nodeHash, onAdvert) {
|
|
470
493
|
const topic = nodeRendezvousTopic(nodeHash)
|
|
471
494
|
return await subscribeTopic(topic, async (bytes, meta) => {
|
|
472
|
-
const
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
noteAdvertPeerHints(verifiedNodeHash, packet.body, meta)
|
|
477
|
-
recentAdverts.touch(verifiedNodeHash, Date.now())
|
|
478
|
-
await Promise.resolve(onAdvert(verifiedNodeHash, packet.body))
|
|
495
|
+
const ingested = await ingestSignedAdvert(topic, bytes, meta)
|
|
496
|
+
if (!ingested) return
|
|
497
|
+
recentAdverts.touch(ingested.verifiedNodeHash, Date.now())
|
|
498
|
+
await Promise.resolve(onAdvert(ingested.verifiedNodeHash, ingested.body))
|
|
479
499
|
})
|
|
480
500
|
},
|
|
481
501
|
recentAdverts,
|
|
@@ -500,83 +520,71 @@ export function createLinkRegistry(options = {}) {
|
|
|
500
520
|
|
|
501
521
|
let defaultRegistry = null
|
|
502
522
|
|
|
523
|
+
/**
|
|
524
|
+
* 默认 registry 尚未创建时暂存的 scope authorizer 条目。
|
|
525
|
+
* 注册阶段不应触发 createLinkRegistry / resolveLocalIdentity;绑定在 getLinkRegistry flush 时完成。
|
|
526
|
+
* @type {Array<{ prefix: string, authorizer: Function, unregister: (() => void) | null }>}
|
|
527
|
+
*/
|
|
528
|
+
const pendingScopeAuthorizers = []
|
|
529
|
+
|
|
503
530
|
/**
|
|
504
531
|
* 获取进程级默认 link registry 单例。
|
|
505
532
|
* @returns {ReturnType<typeof createLinkRegistry>} 默认 registry
|
|
506
533
|
*/
|
|
507
534
|
export function getLinkRegistry() {
|
|
508
|
-
|
|
535
|
+
if (defaultRegistry) return defaultRegistry
|
|
536
|
+
defaultRegistry = createLinkRegistry()
|
|
537
|
+
for (const entry of pendingScopeAuthorizers)
|
|
538
|
+
entry.unregister = defaultRegistry.registerScopeAuthorizer(entry.prefix, entry.authorizer)
|
|
539
|
+
pendingScopeAuthorizers.length = 0
|
|
540
|
+
return defaultRegistry
|
|
509
541
|
}
|
|
510
542
|
|
|
511
543
|
/**
|
|
512
|
-
* 默认 registry
|
|
513
|
-
* @param {
|
|
514
|
-
* @returns {
|
|
515
|
-
*/
|
|
516
|
-
export const ensureLinkToNode = (...args) => getLinkRegistry().ensureLinkToNode(...args)
|
|
517
|
-
/**
|
|
518
|
-
* 默认 registry 的 getLink 代理。
|
|
519
|
-
* @param {...any} args 转发参数
|
|
520
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['getLink']>} 链路实例
|
|
521
|
-
*/
|
|
522
|
-
export const getLink = (...args) => getLinkRegistry().getLink(...args)
|
|
523
|
-
/**
|
|
524
|
-
* 默认 registry 的 listLinks 代理。
|
|
525
|
-
* @param {...any} args 转发参数
|
|
526
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['listLinks']>} 链路列表
|
|
527
|
-
*/
|
|
528
|
-
export const listLinks = (...args) => getLinkRegistry().listLinks(...args)
|
|
529
|
-
/**
|
|
530
|
-
* 默认 registry 的 closeLink 代理。
|
|
531
|
-
* @param {...any} args 转发参数
|
|
532
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['closeLink']>} 关闭完成
|
|
533
|
-
*/
|
|
534
|
-
export const closeLink = (...args) => getLinkRegistry().closeLink(...args)
|
|
535
|
-
/**
|
|
536
|
-
* 默认 registry 的 sendToNodeLink 代理。
|
|
537
|
-
* @param {...any} args 转发参数
|
|
538
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['sendToNodeLink']>} 是否成功
|
|
539
|
-
*/
|
|
540
|
-
export const sendToNodeLink = (...args) => getLinkRegistry().sendToNodeLink(...args)
|
|
541
|
-
/**
|
|
542
|
-
* 默认 registry 的 relayEnvelopeToNode 代理。
|
|
543
|
-
* @param {...any} args 转发参数
|
|
544
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['relayEnvelopeToNode']>} 是否成功
|
|
545
|
-
*/
|
|
546
|
-
export const relayEnvelopeToNode = (...args) => getLinkRegistry().relayEnvelopeToNode(...args)
|
|
547
|
-
/**
|
|
548
|
-
* 默认 registry 的 onLinkUp 代理。
|
|
549
|
-
* @param {...any} args 转发参数
|
|
550
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['onLinkUp']>} 取消订阅函数
|
|
544
|
+
* 默认 registry 方法代理。
|
|
545
|
+
* @param {string} name registry 方法名
|
|
546
|
+
* @returns {(...args: unknown[]) => unknown} 绑定到 getLinkRegistry()[name] 的函数
|
|
551
547
|
*/
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
548
|
+
const bindRegistryMethod = name => (...args) => getLinkRegistry()[name](...args)
|
|
549
|
+
|
|
550
|
+
/** @type {(...args: unknown[]) => unknown} */
|
|
551
|
+
export const ensureLinkToNode = bindRegistryMethod('ensureLinkToNode')
|
|
552
|
+
/** @type {(...args: unknown[]) => unknown} */
|
|
553
|
+
export const getLink = bindRegistryMethod('getLink')
|
|
554
|
+
/** @type {(...args: unknown[]) => unknown} */
|
|
555
|
+
export const listLinks = bindRegistryMethod('listLinks')
|
|
556
|
+
/** @type {(...args: unknown[]) => unknown} */
|
|
557
|
+
export const closeLink = bindRegistryMethod('closeLink')
|
|
558
|
+
/** @type {(...args: unknown[]) => unknown} */
|
|
559
|
+
export const sendToNodeLink = bindRegistryMethod('sendToNodeLink')
|
|
560
|
+
/** @type {(...args: unknown[]) => unknown} */
|
|
561
|
+
export const relayEnvelopeToNode = bindRegistryMethod('relayEnvelopeToNode')
|
|
562
|
+
/** @type {(...args: unknown[]) => unknown} */
|
|
563
|
+
export const onLinkUp = bindRegistryMethod('onLinkUp')
|
|
564
|
+
/** @type {(...args: unknown[]) => unknown} */
|
|
565
|
+
export const onLinkDown = bindRegistryMethod('onLinkDown')
|
|
566
|
+
/** @type {(...args: unknown[]) => unknown} */
|
|
567
|
+
export const registerScopeInterest = bindRegistryMethod('registerScopeInterest')
|
|
568
|
+
/** @type {(...args: unknown[]) => unknown} */
|
|
569
|
+
export const releaseScopeInterest = bindRegistryMethod('releaseScopeInterest')
|
|
570
|
+
/** @type {(...args: unknown[]) => unknown} */
|
|
571
|
+
export const subscribeScope = bindRegistryMethod('subscribeScope')
|
|
572
|
+
|
|
577
573
|
/**
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
* @
|
|
574
|
+
* 注册默认 registry 的 scope authorizer。
|
|
575
|
+
* 不急切创建 registry(不必 resolveLocalIdentity);首次 getLinkRegistry 时 flush。
|
|
576
|
+
* @param {string} prefix scope 前缀
|
|
577
|
+
* @param {Function} authorizer 校验函数
|
|
578
|
+
* @returns {() => void} 取消注册函数
|
|
581
579
|
*/
|
|
582
|
-
export
|
|
580
|
+
export function registerScopeAuthorizer(prefix, authorizer) {
|
|
581
|
+
if (defaultRegistry) return defaultRegistry.registerScopeAuthorizer(prefix, authorizer)
|
|
582
|
+
|
|
583
|
+
const entry = { prefix, authorizer, unregister: null }
|
|
584
|
+
pendingScopeAuthorizers.push(entry)
|
|
585
|
+
return () => {
|
|
586
|
+
const index = pendingScopeAuthorizers.indexOf(entry)
|
|
587
|
+
if (index !== -1) pendingScopeAuthorizers.splice(index, 1)
|
|
588
|
+
entry.unregister?.()
|
|
589
|
+
}
|
|
590
|
+
}
|
|
@@ -6,6 +6,9 @@ import { listLinkProviders } from '../link/providers/index.mjs'
|
|
|
6
6
|
|
|
7
7
|
import { decryptSignalPacket, encryptSignalPacket, nodeRendezvousTopic } from './signal_crypto.mjs'
|
|
8
8
|
|
|
9
|
+
/** accept/dial 挂起期间 ICE 信令 backlog 上限,防无 handler 时无限堆积 */
|
|
10
|
+
const SIGNAL_BACKLOG_MAX = 64
|
|
11
|
+
|
|
9
12
|
/**
|
|
10
13
|
* 创建带 backlog 的缓冲信令会话。
|
|
11
14
|
* @param {(message: unknown) => Promise<void>} sendRemote 远端发送回调
|
|
@@ -37,12 +40,13 @@ export function createBufferedSignalSession(sendRemote) {
|
|
|
37
40
|
return () => handlers.delete(handler)
|
|
38
41
|
},
|
|
39
42
|
/**
|
|
40
|
-
* 投递信令消息;无 handler 时入 backlog
|
|
43
|
+
* 投递信令消息;无 handler 时入 backlog(有界)。
|
|
41
44
|
* @param {unknown} message 信令消息
|
|
42
45
|
* @returns {void}
|
|
43
46
|
*/
|
|
44
47
|
deliver(message) {
|
|
45
48
|
if (!handlers.size) {
|
|
49
|
+
if (backlog.length >= SIGNAL_BACKLOG_MAX) backlog.shift()
|
|
46
50
|
backlog.push(message)
|
|
47
51
|
return
|
|
48
52
|
}
|
|
@@ -157,7 +161,7 @@ export function createOfferAnswerDial(deps) {
|
|
|
157
161
|
*/
|
|
158
162
|
async function handleIncomingSignal(bytes) {
|
|
159
163
|
const packet = decryptSignalPacket(selfTopic, bytes)
|
|
160
|
-
if (
|
|
164
|
+
if (packet?.type !== 'signal') return
|
|
161
165
|
const remoteNodeHash = normalizeHex64(packet.from)
|
|
162
166
|
const connId = String(packet.connId || '')
|
|
163
167
|
if (!remoteNodeHash || remoteNodeHash === localIdentity.nodeHash || !connId) return
|
package/transport/peer_pool.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { loadPeerPoolView, mergeNetworkPeerPools } from '../node/network.mjs'
|
|
|
8
8
|
import { loadReputation } from '../node/reputation_store.mjs'
|
|
9
9
|
import { isQuarantinedPure } from '../reputation/engine.mjs'
|
|
10
10
|
import { clampReputationScore } from '../reputation/math.mjs'
|
|
11
|
+
import { shuffleInPlace } from '../utils/shuffle.mjs'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* 解析联邦池槽位参数(从 groupSettings 读取,含低功耗缩减)。
|
|
@@ -91,14 +92,8 @@ export function mergeTrustedWithAnchors(existingTrusted, rankedCandidates, limit
|
|
|
91
92
|
*/
|
|
92
93
|
export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxPerSource = EXPLORE_MAX_PER_SOURCE) {
|
|
93
94
|
if (k <= 0 || !exploreIds.length) return []
|
|
94
|
-
if (!exploreSources?.size)
|
|
95
|
-
|
|
96
|
-
for (let i = copy.length - 1; i > 0; i--) {
|
|
97
|
-
const j = Math.floor(Math.random() * (i + 1))
|
|
98
|
-
;[copy[i], copy[j]] = [copy[j], copy[i]]
|
|
99
|
-
}
|
|
100
|
-
return copy.slice(0, k)
|
|
101
|
-
}
|
|
95
|
+
if (!exploreSources?.size)
|
|
96
|
+
return shuffleInPlace([...exploreIds]).slice(0, k)
|
|
102
97
|
/** @type {Map<string, string[]>} */
|
|
103
98
|
const bySource = new Map()
|
|
104
99
|
for (const id of exploreIds) {
|
|
@@ -106,11 +101,7 @@ export function selectExploreWithSourceQuota(exploreIds, exploreSources, k, maxP
|
|
|
106
101
|
if (!bySource.has(src)) bySource.set(src, [])
|
|
107
102
|
bySource.get(src).push(id)
|
|
108
103
|
}
|
|
109
|
-
for (const ids of bySource.values())
|
|
110
|
-
for (let i = ids.length - 1; i > 0; i--) {
|
|
111
|
-
const j = Math.floor(Math.random() * (i + 1))
|
|
112
|
-
;[ids[i], ids[j]] = [ids[j], ids[i]]
|
|
113
|
-
}
|
|
104
|
+
for (const ids of bySource.values()) shuffleInPlace(ids)
|
|
114
105
|
const out = []
|
|
115
106
|
/** @type {Map<string, number>} */
|
|
116
107
|
const picked = new Map()
|
|
@@ -148,7 +139,7 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
|
|
|
148
139
|
const blocked = new Set(peers.blockedPeers)
|
|
149
140
|
const roomSet = inRoomNodeHashes instanceof Set
|
|
150
141
|
? inRoomNodeHashes
|
|
151
|
-
: new Set(
|
|
142
|
+
: new Set(inRoomNodeHashes || [])
|
|
152
143
|
const onlineAll = roster.filter(
|
|
153
144
|
rosterEntry => rosterEntry.peerId
|
|
154
145
|
&& rosterEntry.remoteNodeHash
|
|
@@ -175,10 +166,11 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
|
|
|
175
166
|
}
|
|
176
167
|
|
|
177
168
|
const anchoredTrusted = peers.trustedPeers.filter(nodeHash => trustedSet.has(nodeHash))
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
.sort((a, b) => repScore(b, rep) - repScore(a, rep))
|
|
181
|
-
|
|
169
|
+
for (const nodeId of mergeTrustedWithAnchors(
|
|
170
|
+
anchoredTrusted,
|
|
171
|
+
[...trustedSet].sort((a, b) => repScore(b, rep) - repScore(a, rep)),
|
|
172
|
+
limits,
|
|
173
|
+
)) {
|
|
182
174
|
if (outPeerIds.size >= limits.maxPeers) break
|
|
183
175
|
pushNode(nodeId)
|
|
184
176
|
}
|
|
@@ -216,15 +208,14 @@ export function selectPeerIdsFromPool({ roster, peers, rep, limits, selfNodeHash
|
|
|
216
208
|
* @returns {string[]} 应建链的 nodeHash 列表(去重)
|
|
217
209
|
*/
|
|
218
210
|
export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers, limits, anchors = [] }) {
|
|
219
|
-
const self = String(selfNodeHash || '')
|
|
220
211
|
const blocked = new Set(peers?.blockedPeers || [])
|
|
221
212
|
const now = Date.now()
|
|
222
|
-
const candidates = [...new Set(
|
|
223
|
-
.filter(id => id && id !==
|
|
213
|
+
const candidates = [...new Set(members)]
|
|
214
|
+
.filter(id => id && id !== selfNodeHash && !blocked.has(id) && !isQuarantinedPure(rep, id, now))
|
|
224
215
|
const ranked = candidates.slice().sort((a, b) => repScore(b, rep) - repScore(a, rep))
|
|
225
216
|
const candidateSet = new Set(candidates)
|
|
226
217
|
// 锚点(如 introducer/creator/seed)必连、且不占 trustedSlots——保证引导期连通。
|
|
227
|
-
const forced = [...new Set(
|
|
218
|
+
const forced = [...new Set(anchors)].filter(id => candidateSet.has(id))
|
|
228
219
|
const chosen = new Set(forced)
|
|
229
220
|
// trusted 槽只从非锚点候选填:既有 trusted 优先保留,再按信誉补至 trustedSlots。
|
|
230
221
|
const nonForced = ranked.filter(id => !chosen.has(id))
|
|
@@ -237,29 +228,24 @@ export function selectLinkTargetsFromMembers({ members, selfNodeHash, rep, peers
|
|
|
237
228
|
}
|
|
238
229
|
|
|
239
230
|
/**
|
|
240
|
-
*
|
|
241
|
-
* 纯计算版本,不含 I/O:接受 peers 对象,返回新对象。
|
|
242
|
-
*
|
|
231
|
+
* 将候选 id 并入 explore,并按信誉重填 trusted。
|
|
243
232
|
* @param {{
|
|
244
233
|
* peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
|
|
245
234
|
* rep: { byNodeHash?: Record<string, { score?: number }> },
|
|
246
|
-
*
|
|
235
|
+
* addIds: Iterable<string>,
|
|
247
236
|
* limits: ReturnType<typeof resolveFederationPoolLimits>,
|
|
248
|
-
* }} options
|
|
249
|
-
* @returns {{ trustedPeers: string[], explorePeers: string[] }}
|
|
237
|
+
* }} options 池状态与增量
|
|
238
|
+
* @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
|
|
250
239
|
*/
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
.map(id => String(id).trim())
|
|
255
|
-
.filter(Boolean),
|
|
256
|
-
)]
|
|
240
|
+
function rebuildExploreAndTrusted(options) {
|
|
241
|
+
const { peers, rep, addIds, limits } = options
|
|
242
|
+
const blocked = new Set(peers.blockedPeers)
|
|
257
243
|
const explore = new Set(peers.explorePeers)
|
|
258
|
-
for (const id of
|
|
259
|
-
if (!
|
|
260
|
-
const newExplorePeers = [...explore].slice(-500)
|
|
244
|
+
for (const id of addIds)
|
|
245
|
+
if (id && !blocked.has(id)) explore.add(id)
|
|
246
|
+
const newExplorePeers = [...explore].filter(id => !blocked.has(id)).slice(-500)
|
|
261
247
|
const ranked = [...new Set([...peers.trustedPeers, ...newExplorePeers])]
|
|
262
|
-
.filter(id => !
|
|
248
|
+
.filter(id => !blocked.has(id))
|
|
263
249
|
.sort((a, b) => repScore(b, rep) - repScore(a, rep))
|
|
264
250
|
return {
|
|
265
251
|
trustedPeers: mergeTrustedWithAnchors(peers.trustedPeers, ranked, limits, peers.blockedPeers),
|
|
@@ -268,32 +254,39 @@ export function applyPexHints({ peers, rep, hints, limits }) {
|
|
|
268
254
|
}
|
|
269
255
|
|
|
270
256
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
257
|
+
* PEX 线索并入 explore 并重填 trusted(纯计算)。
|
|
273
258
|
* @param {{
|
|
274
259
|
* peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
|
|
275
260
|
* rep: { byNodeHash?: Record<string, { score?: number }> },
|
|
276
|
-
*
|
|
261
|
+
* hints: string[],
|
|
277
262
|
* limits: ReturnType<typeof resolveFederationPoolLimits>,
|
|
278
|
-
* }} options
|
|
279
|
-
* @returns {{ trustedPeers: string[], explorePeers: string[] }}
|
|
263
|
+
* }} options 池状态与 PEX hints
|
|
264
|
+
* @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
|
|
280
265
|
*/
|
|
281
|
-
export function
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
266
|
+
export function applyPexHints(options) {
|
|
267
|
+
const { peers, rep, hints, limits } = options
|
|
268
|
+
return rebuildExploreAndTrusted({
|
|
269
|
+
peers, rep, limits,
|
|
270
|
+
addIds: hints || [],
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* roster 观测并入 explore 并重填 trusted(纯计算)。
|
|
276
|
+
* @param {{
|
|
277
|
+
* peers: { trustedPeers: string[], explorePeers: string[], blockedPeers: string[] },
|
|
278
|
+
* rep: { byNodeHash?: Record<string, { score?: number }> },
|
|
279
|
+
* roster: { remoteNodeHash?: string }[],
|
|
280
|
+
* limits: ReturnType<typeof resolveFederationPoolLimits>,
|
|
281
|
+
* }} options 池状态与 roster
|
|
282
|
+
* @returns {{ trustedPeers: string[], explorePeers: string[] }} 重算后的 trusted/explore
|
|
283
|
+
*/
|
|
284
|
+
export function applyRosterToPeerPool(options) {
|
|
285
|
+
const { peers, rep, roster, limits } = options
|
|
286
|
+
return rebuildExploreAndTrusted({
|
|
287
|
+
peers, rep, limits,
|
|
288
|
+
addIds: roster.map(entry => entry.remoteNodeHash).filter(Boolean),
|
|
289
|
+
})
|
|
297
290
|
}
|
|
298
291
|
|
|
299
292
|
/**
|
|
@@ -308,17 +301,13 @@ export function pickFederationTargetPeerIds(groupId, roster, groupSettings, self
|
|
|
308
301
|
const limits = resolveFederationPoolLimits(groupSettings)
|
|
309
302
|
const peers = loadPeerPoolView(groupId)
|
|
310
303
|
const rep = loadReputation()
|
|
311
|
-
const inRoomNodeHashes = roster
|
|
312
|
-
.map(p => p.remoteNodeHash)
|
|
313
|
-
.map(id => String(id).trim())
|
|
314
|
-
.filter(Boolean)
|
|
315
304
|
return selectPeerIdsFromPool({
|
|
316
305
|
roster,
|
|
317
306
|
peers,
|
|
318
307
|
rep,
|
|
319
308
|
limits,
|
|
320
309
|
selfNodeHash,
|
|
321
|
-
inRoomNodeHashes,
|
|
310
|
+
inRoomNodeHashes: roster.map(entry => entry.remoteNodeHash).filter(Boolean),
|
|
322
311
|
hintSources: peers.hintSources,
|
|
323
312
|
})
|
|
324
313
|
}
|
|
@@ -12,15 +12,13 @@ import { USER_ROOM_SCOPE } from './room_scopes.mjs'
|
|
|
12
12
|
* }} RemoteUserRoomSlot
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
/** @type {Map<string, RemoteUserRoomSlot
|
|
15
|
+
/** @type {Map<string, RemoteUserRoomSlot>} nodeHash → slot */
|
|
16
16
|
const slots = new Map()
|
|
17
17
|
/** @type {Map<string, Promise<RemoteUserRoomSlot | null>>} nodeHash → 进行中的 promise */
|
|
18
18
|
const inflights = new Map()
|
|
19
19
|
|
|
20
20
|
registerFederationRoomProvider('remote-user-room', () => {
|
|
21
|
-
return [...slots.values()]
|
|
22
|
-
.filter(Boolean)
|
|
23
|
-
.map(s => s.roomSlot)
|
|
21
|
+
return [...slots.values()].map(s => s.roomSlot)
|
|
24
22
|
})
|
|
25
23
|
|
|
26
24
|
/**
|
|
@@ -32,16 +30,14 @@ registerFederationRoomProvider('remote-user-room', () => {
|
|
|
32
30
|
export async function ensureRemoteUserRoom(username, targetNodeHash) {
|
|
33
31
|
void username
|
|
34
32
|
const key = targetNodeHash.toLowerCase()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
const existing = slots.get(key)
|
|
34
|
+
if (existing) return existing
|
|
35
|
+
const inflight = inflights.get(key)
|
|
36
|
+
if (inflight) return await inflight
|
|
38
37
|
|
|
39
38
|
const task = (async () => {
|
|
40
39
|
try {
|
|
41
|
-
if (!await ensureLinkToNode(key))
|
|
42
|
-
slots.set(key, null)
|
|
43
|
-
return null
|
|
44
|
-
}
|
|
40
|
+
if (!await ensureLinkToNode(key)) return null
|
|
45
41
|
|
|
46
42
|
/** @type {import('../registries/room_provider.mjs').FederationRoomSlot} */
|
|
47
43
|
const roomSlot = {
|
|
@@ -66,17 +62,20 @@ export async function ensureRemoteUserRoom(username, targetNodeHash) {
|
|
|
66
62
|
* @returns {void}
|
|
67
63
|
*/
|
|
68
64
|
sendToPeer(peerId, actionName, payload) {
|
|
69
|
-
void getLink(key)?.send({ scope: 'node', action:
|
|
65
|
+
void getLink(key)?.send({ scope: 'node', action: actionName, payload }).catch(() => { })
|
|
70
66
|
},
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
const slot = {
|
|
74
70
|
roomSlot,
|
|
75
71
|
/**
|
|
76
|
-
*
|
|
72
|
+
* 关闭远端用户房间链路并释放槽位。
|
|
77
73
|
* @returns {Promise<void>} 关闭完成
|
|
78
74
|
*/
|
|
79
|
-
leave() {
|
|
75
|
+
leave() {
|
|
76
|
+
slots.delete(key)
|
|
77
|
+
return closeLink(key, 'remote-user-room-release')
|
|
78
|
+
},
|
|
80
79
|
}
|
|
81
80
|
slots.set(key, slot)
|
|
82
81
|
invalidateTrustGraphCache()
|
|
@@ -84,7 +83,6 @@ export async function ensureRemoteUserRoom(username, targetNodeHash) {
|
|
|
84
83
|
}
|
|
85
84
|
catch (error) {
|
|
86
85
|
console.error('p2p: failed to join remote user room', key, error)
|
|
87
|
-
slots.set(key, null)
|
|
88
86
|
return null
|
|
89
87
|
}
|
|
90
88
|
finally {
|