@steve02081504/fount-p2p 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +42 -11
  2. package/core/bytes_codec.mjs +65 -10
  3. package/core/composite_key.mjs +5 -5
  4. package/core/tcp_port.mjs +1 -1
  5. package/crypto/checkpoint_sign.mjs +2 -2
  6. package/crypto/crypto.mjs +3 -3
  7. package/crypto/key.mjs +12 -12
  8. package/dag/canonicalize_row.mjs +3 -3
  9. package/dag/index.mjs +2 -3
  10. package/dag/storage.mjs +36 -36
  11. package/discovery/advert_peer_hints.mjs +1 -1
  12. package/discovery/adverts.mjs +109 -0
  13. package/discovery/bt/index.mjs +154 -113
  14. package/discovery/bt/probe_child.mjs +2 -1
  15. package/discovery/bt/runtime.mjs +5 -12
  16. package/discovery/index.mjs +267 -62
  17. package/discovery/internal/signal_crypto.mjs +109 -0
  18. package/discovery/lan.mjs +228 -0
  19. package/discovery/nostr.mjs +430 -141
  20. package/federation/chunk_fetch_pending.mjs +14 -18
  21. package/federation/dag_order_cache.mjs +1 -1
  22. package/federation/entity_key_chain.mjs +3 -4
  23. package/federation/manifest_fetch_pending.mjs +1 -3
  24. package/federation/topo_order_memo.mjs +11 -4
  25. package/files/assemble.mjs +14 -14
  26. package/files/assemble_stream.mjs +6 -6
  27. package/files/chunk_fetch.mjs +2 -2
  28. package/files/chunk_responder.mjs +29 -20
  29. package/files/evfs.mjs +31 -30
  30. package/files/manifest_fetch.mjs +16 -3
  31. package/files/public_manifest.mjs +11 -11
  32. package/files/transfer_key_registry.mjs +2 -2
  33. package/index.mjs +86 -11
  34. package/infra/cli.mjs +62 -0
  35. package/infra/debug_log.mjs +56 -0
  36. package/infra/default_node_dir.mjs +22 -0
  37. package/infra/priority.mjs +56 -0
  38. package/infra/service.mjs +140 -0
  39. package/infra/tunables.json +5 -0
  40. package/link/channel_mux.mjs +10 -10
  41. package/link/frame.mjs +76 -131
  42. package/link/handshake.mjs +19 -20
  43. package/link/pipe.mjs +53 -79
  44. package/link/providers/ble_gatt.mjs +19 -31
  45. package/link/providers/lan_tcp.mjs +17 -32
  46. package/link/providers/link_id_pipe.mjs +46 -0
  47. package/link/providers/webrtc.mjs +42 -51
  48. package/link/rtc.mjs +24 -12
  49. package/mailbox/consumer_registry.mjs +2 -2
  50. package/mailbox/deliver_or_store.mjs +6 -6
  51. package/mailbox/wire.mjs +28 -24
  52. package/node/entity_store.mjs +6 -6
  53. package/node/identity.mjs +4 -4
  54. package/node/instance.mjs +46 -27
  55. package/node/local_data_revision.mjs +26 -0
  56. package/node/log.mjs +56 -0
  57. package/node/network.mjs +46 -14
  58. package/node/reputation_store.mjs +4 -4
  59. package/node/reputation_sync.mjs +318 -0
  60. package/node/routing_profile.mjs +21 -0
  61. package/node/signaling_config.mjs +30 -11
  62. package/overlay/index.mjs +35 -14
  63. package/package.json +13 -2
  64. package/rooms/scoped_link.mjs +17 -182
  65. package/schemas/discovery.mjs +1 -2
  66. package/schemas/federation_pull.mjs +2 -2
  67. package/schemas/part_query.mjs +1 -1
  68. package/schemas/remote_event.mjs +1 -1
  69. package/transport/advert_ingest.mjs +17 -0
  70. package/transport/group_link_set.mjs +165 -134
  71. package/transport/ice_servers.mjs +2 -2
  72. package/transport/link_registry.mjs +283 -124
  73. package/transport/mesh_keepalive.mjs +217 -0
  74. package/transport/node_scope.mjs +289 -0
  75. package/transport/offer_answer.mjs +48 -47
  76. package/transport/peer_pool.mjs +169 -78
  77. package/transport/remote_user_room.mjs +18 -23
  78. package/transport/rtc_connection_budget.mjs +18 -4
  79. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  80. package/transport/runtime_bootstrap.mjs +172 -104
  81. package/transport/tunables.json +11 -0
  82. package/transport/tunables.mjs +18 -0
  83. package/transport/user_room.mjs +93 -161
  84. package/trust_graph/build.mjs +0 -2
  85. package/trust_graph/cache.mjs +12 -3
  86. package/trust_graph/registry.mjs +6 -6
  87. package/trust_graph/send.mjs +1 -4
  88. package/utils/async_mutex.mjs +4 -4
  89. package/utils/emit_safe.mjs +11 -0
  90. package/utils/json_io.mjs +12 -12
  91. package/utils/map_pool.mjs +5 -5
  92. package/utils/shuffle.mjs +13 -0
  93. package/utils/ttl_map.mjs +29 -4
  94. package/wire/ingress.mjs +1 -1
  95. package/wire/part_ingress.mjs +38 -36
  96. package/wire/part_invoke.mjs +4 -4
  97. package/wire/part_query.mjs +28 -23
  98. package/wire/part_query.tunables.json +6 -1
  99. package/wire/part_query_cache.mjs +1 -1
  100. package/wire/volatile_signature.mjs +1 -1
  101. package/discovery/mdns.mjs +0 -191
  102. package/transport/signal_crypto.mjs +0 -82
@@ -2,21 +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 { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
6
- import { subscribeTopic } from '../discovery/index.mjs'
7
- import { verifySignedAdvert } from '../link/handshake.mjs'
5
+ import { watchVerifiedNodeAdvert, setDiscoveryLinkDialer, prepareConnectToNode } from '../discovery/index.mjs'
8
6
  import { listLinkProviders } from '../link/providers/index.mjs'
9
7
  import { ensureNodeSeed, getNodeHash } from '../node/identity.mjs'
8
+ import { nodeDebug, shortHash } from '../node/log.mjs'
9
+ import { loadPeerPoolView } from '../node/network.mjs'
10
10
  import { createOverlayRouter } from '../overlay/index.mjs'
11
+ import { emitSafe } from '../utils/emit_safe.mjs'
11
12
  import { createLruMap } from '../utils/lru.mjs'
12
13
 
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 {
17
- decryptSignalPacket,
18
- nodeRendezvousTopic,
19
- } from './signal_crypto.mjs'
20
20
 
21
21
  /**
22
22
  * 解析或从节点种子推导本地身份。
@@ -63,15 +63,23 @@ function subscribeBucket(buckets, key, listener) {
63
63
  * @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array }} [options.localIdentity] 本地身份
64
64
  * @param {RTCConfiguration['iceServers']} [options.iceServers] ICE 服务器列表(包内 webrtc provider 使用)
65
65
  * @param {number} [options.maxActive] 最大并发活跃链路数
66
+ * @param {boolean} [options.meshKeepalive=true] 是否启用 mesh 保活(N/K 扫描拨号)
66
67
  * @param {boolean} [options.autoRegisterDiscoveryProviders] 是否自动注册 discovery provider
67
68
  * @param {boolean} [options.autoRegisterLinkProviders] 是否自动注册内置 link provider
68
69
  * @returns {object} link registry 接口(对上层即 fount 网络:ensure/send/subscribe,无传输类型)
69
70
  */
70
71
  export function createLinkRegistry(options = {}) {
71
72
  const localIdentity = resolveLocalIdentity(options.localIdentity)
72
- const iceServers = options.iceServers?.length ? options.iceServers : DEFAULT_ICE_SERVERS
73
- const maxActive = Math.max(4, Number(options.maxActive) || 32)
74
- const selfTopic = nodeRendezvousTopic(localIdentity.nodeHash)
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
75
83
  /** @type {Map<string, object>} */
76
84
  const links = new Map()
77
85
  /** @type {Map<string, Promise<object | null>>} */
@@ -104,8 +112,8 @@ export function createLinkRegistry(options = {}) {
104
112
  * @returns {boolean} 候选链应保留为规范链时 true
105
113
  */
106
114
  function linkIsPreferred(link, remoteNodeHash, against = null) {
107
- const level = Number(link.level) || 0
108
- const againstLevel = Number(against?.level) || 0
115
+ const level = link.level || 0
116
+ const againstLevel = against?.level || 0
109
117
  if (against && level !== againstLevel) return level > againstLevel
110
118
  const cmp = compareHex64Asc(localIdentity.nodeHash, remoteNodeHash)
111
119
  return link.initiator ? cmp < 0 : cmp > 0
@@ -125,8 +133,7 @@ export function createLinkRegistry(options = {}) {
125
133
  const wasCanonical = links.get(remoteNodeHash) === link
126
134
  if (!wasCanonical) return
127
135
  links.delete(remoteNodeHash)
128
- for (const listener of linkDownListeners)
129
- try { listener(remoteNodeHash, reason) } catch { /* ignore */ }
136
+ emitSafe(linkDownListeners, remoteNodeHash, reason)
130
137
  })
131
138
  }
132
139
 
@@ -140,15 +147,14 @@ export function createLinkRegistry(options = {}) {
140
147
  const normalized = normalizeHex64(remoteNodeHash)
141
148
  const existing = links.get(normalized)
142
149
  if (existing && existing !== candidate && !linkIsPreferred(candidate, normalized, existing)) {
143
- await candidate.close(Number(candidate.level) !== Number(existing.level) ? 'provider-loser' : 'glare-loser')
150
+ await candidate.close(candidate.level !== existing.level ? 'provider-loser' : 'glare-loser')
144
151
  return
145
152
  }
146
153
  links.set(normalized, candidate)
147
154
  wireLink(normalized, candidate)
148
155
  if (existing && existing !== candidate)
149
- await existing.close(Number(candidate.level) !== Number(existing.level) ? 'provider-replaced' : 'glare-replaced')
150
- for (const listener of linkUpListeners)
151
- try { listener(normalized, candidate) } catch { /* ignore */ }
156
+ await existing.close(candidate.level !== existing.level ? 'provider-replaced' : 'glare-replaced')
157
+ emitSafe(linkUpListeners, normalized, candidate)
152
158
  }
153
159
 
154
160
  /**
@@ -173,10 +179,13 @@ export function createLinkRegistry(options = {}) {
173
179
 
174
180
  const bootstrap = createRuntimeBootstrap({
175
181
  localIdentity,
176
- selfTopic,
177
182
  autoRegisterDiscoveryProviders: options.autoRegisterDiscoveryProviders !== false,
178
183
  autoRegisterLinkProviders: options.autoRegisterLinkProviders !== false,
179
184
  onInboundLink,
185
+ /**
186
+ * @param {Uint8Array} bytes 入站加密信令
187
+ * @returns {Promise<void>}
188
+ */
180
189
  handleIncomingSignal: bytes => handleIncomingSignal(bytes),
181
190
  })
182
191
 
@@ -186,35 +195,41 @@ export function createLinkRegistry(options = {}) {
186
195
  * @returns {number} 权重值
187
196
  */
188
197
  function scopeWeight(remoteNodeHash) {
189
- let weight = 0
198
+ let weight = priorityWeightFunction?.(remoteNodeHash) ?? 0
190
199
  for (const hashes of scopeInterests.values())
191
200
  if (hashes.has(remoteNodeHash)) weight++
192
201
  return weight
193
202
  }
194
203
 
195
204
  /**
196
- * 超出 maxActive 时驱逐权重最低的链路。
205
+ * 超出 maxActive 时驱逐:探索链优先于熟人/scope 权重。
197
206
  * @returns {Promise<void>}
198
207
  */
199
208
  async function trimToBudget() {
200
209
  if (links.size < maxActive) return
201
- const candidates = [...links.entries()]
202
- .sort((left, right) => scopeWeight(left[0]) - scopeWeight(right[0]) || compareHex64Asc(left[0], right[0]))
203
- const victim = candidates[0]
204
- if (victim) await victim[1].close('budget-evict')
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
218
+ if (victimLink) await victimLink.close('budget-evict')
205
219
  }
206
220
 
207
- ;({ handleIncomingSignal, dialOfferAnswer } = createOfferAnswerDial({
221
+ ; ({ handleIncomingSignal, dialOfferAnswer } = createOfferAnswerDial({
208
222
  localIdentity,
209
- iceServers,
210
- selfTopic,
223
+ /**
224
+ * @returns {RTCConfiguration['iceServers']} 当前 ICE 服务器列表
225
+ */
226
+ get iceServers() { return iceServers },
211
227
  signalSessions,
212
228
  registerResolvedLink,
213
229
  trimToBudget,
214
230
  /**
215
- * 读取当前规范链。
216
- * @param {string} remoteNodeHash 远端节点 64 hex
217
- * @returns {object | null | undefined} 规范链实例
231
+ * @param {string} remoteNodeHash 远端 nodeHash
232
+ * @returns {object | null} 已有规范链路
218
233
  */
219
234
  getCanonicalLink: remoteNodeHash => links.get(normalizeHex64(remoteNodeHash)),
220
235
  }))
@@ -251,33 +266,100 @@ export function createLinkRegistry(options = {}) {
251
266
  if (links.has(normalized)) return links.get(normalized)
252
267
  if (inflights.has(normalized)) return await inflights.get(normalized)
253
268
  const task = (async () => {
269
+ nodeDebug('p2p:dial start', { peer: shortHash(normalized) })
270
+ await prepareConnectToNode(normalized)
254
271
  const providers = listLinkProviders()
255
272
  for (const provider of providers)
256
273
  try {
257
274
  if (typeof provider.canReach === 'function') {
258
275
  const reachable = await Promise.resolve(provider.canReach({ nodeHash: normalized }))
259
- if (!reachable) continue
276
+ if (!reachable) {
277
+ nodeDebug('p2p:dial skip', { peer: shortHash(normalized), provider: provider.id, reason: 'canReach=false' })
278
+ continue
279
+ }
260
280
  }
261
281
  if (typeof provider.isAvailable === 'function') {
262
282
  const available = await Promise.resolve(provider.isAvailable())
263
- if (!available) continue
283
+ if (!available) {
284
+ nodeDebug('p2p:dial skip', { peer: shortHash(normalized), provider: provider.id, reason: 'isAvailable=false' })
285
+ continue
286
+ }
264
287
  }
288
+ nodeDebug('p2p:dial try', { peer: shortHash(normalized), provider: provider.id })
265
289
  if (provider.caps?.needsOfferAnswer) {
266
290
  const link = await dialOfferAnswer(provider, normalized)
267
- if (link) return 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 })
268
296
  continue
269
297
  }
270
298
  const link = await dialProvider(provider, normalized)
271
- if (link) return 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
+ })
272
311
  }
273
- catch { /* dial failed — try next provider */ }
274
312
 
313
+ nodeDebug('p2p:dial exhausted', { peer: shortHash(normalized) })
275
314
  return null
276
315
  })().finally(() => inflights.delete(normalized))
277
316
  inflights.set(normalized, task)
278
317
  return await task
279
318
  }
280
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
+
281
363
  /**
282
364
  * 经已有直连发送 envelope。
283
365
  * @param {string} remoteNodeHash 远端节点 64 hex
@@ -318,6 +400,13 @@ export function createLinkRegistry(options = {}) {
318
400
  return overlayRouter
319
401
  }
320
402
 
403
+ /**
404
+ * @returns {import('../overlay/index.mjs').OverlayRouter} overlay 路由器单例
405
+ */
406
+ function ensureOverlayRouter() {
407
+ return getOverlayRouter()
408
+ }
409
+
321
410
  /**
322
411
  * 经 overlay 多跳 relay envelope 到无直连的节点。
323
412
  * @param {string} remoteNodeHash 远端节点 64 hex
@@ -327,8 +416,9 @@ export function createLinkRegistry(options = {}) {
327
416
  async function relayEnvelopeToNode(remoteNodeHash, envelope) {
328
417
  if (!links.size || envelope?.scope === 'overlay') return false
329
418
  try {
330
- const path = await getOverlayRouter().discoverRoute(remoteNodeHash)
331
- await getOverlayRouter().relay(path, envelope)
419
+ const overlay = getOverlayRouter()
420
+ const path = await overlay.discoverRoute(remoteNodeHash)
421
+ await overlay.relay(path, envelope)
332
422
  return true
333
423
  }
334
424
  catch {
@@ -347,6 +437,17 @@ export function createLinkRegistry(options = {}) {
347
437
  || await relayEnvelopeToNode(remoteNodeHash, envelope)
348
438
  }
349
439
 
440
+ /**
441
+ * 同步结果直接返回;thenable 才 await(避免每条 envelope 造 microtask)。
442
+ * @param {unknown} value 可能为 Promise 的返回值
443
+ * @returns {Promise<unknown>} 已 resolve 的值
444
+ */
445
+ async function maybeAwait(value) {
446
+ if (value != null && typeof /** @type {{ then?: unknown }} */ value.then === 'function')
447
+ return await value
448
+ return value
449
+ }
450
+
350
451
  /**
351
452
  * 将入站 envelope 派发到 scope 监听器(经 authorizer 校验)。
352
453
  * @param {string} senderNodeHash 发送方节点 64 hex
@@ -355,16 +456,16 @@ export function createLinkRegistry(options = {}) {
355
456
  * @returns {Promise<void>}
356
457
  */
357
458
  async function dispatchEnvelope(senderNodeHash, envelope, link) {
358
- const scope = String(envelope?.scope || '')
459
+ const scope = envelope.scope || ''
359
460
  for (const [prefix, authorizer] of scopeAuthorizers.entries())
360
461
  if (scope.startsWith(prefix)) {
361
- const allowed = await Promise.resolve(authorizer(scope, senderNodeHash, envelope, link))
462
+ const allowed = await maybeAwait(authorizer(scope, senderNodeHash, envelope, link))
362
463
  if (!allowed) return
363
464
  }
364
465
  for (const [prefix, listeners] of scopeListeners.entries())
365
466
  if (scope.startsWith(prefix))
366
467
  for (const listener of listeners)
367
- await Promise.resolve(listener(senderNodeHash, envelope, link))
468
+ await maybeAwait(listener(senderNodeHash, envelope, link))
368
469
  }
369
470
 
370
471
  /**
@@ -374,7 +475,7 @@ export function createLinkRegistry(options = {}) {
374
475
  * @returns {() => void} 取消订阅函数
375
476
  */
376
477
  function subscribeScope(prefix, listener) {
377
- return subscribeBucket(scopeListeners, String(prefix), listener)
478
+ return subscribeBucket(scopeListeners, prefix, listener)
378
479
  }
379
480
 
380
481
  /**
@@ -384,8 +485,8 @@ export function createLinkRegistry(options = {}) {
384
485
  * @returns {() => void} 取消注册函数
385
486
  */
386
487
  function registerScopeAuthorizer(prefix, authorizer) {
387
- scopeAuthorizers.set(String(prefix), authorizer)
388
- return () => scopeAuthorizers.delete(String(prefix))
488
+ scopeAuthorizers.set(prefix, authorizer)
489
+ return () => scopeAuthorizers.delete(prefix)
389
490
  }
390
491
 
391
492
  return {
@@ -393,8 +494,55 @@ export function createLinkRegistry(options = {}) {
393
494
  buildLocalAdvert: bootstrap.buildLocalAdvert,
394
495
  lanTcpPort: bootstrap.lanTcpPort,
395
496
  whenListening: bootstrap.whenListening,
396
- ensureRuntime: bootstrap.ensureRuntime,
497
+ ensureRuntime: ensureRuntimeWithMesh,
498
+ reloadDiscoveryRelays: bootstrap.reloadDiscoveryRelays,
499
+ ensureOverlayRouter,
500
+ getOverlayRouter,
397
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
+ },
398
546
  /**
399
547
  * 获取到指定节点的活跃链路。
400
548
  * @param {string} nodeHash 节点 64 hex
@@ -448,7 +596,7 @@ export function createLinkRegistry(options = {}) {
448
596
  * @returns {void}
449
597
  */
450
598
  registerScopeInterest(scope, nodeHashes) {
451
- scopeInterests.set(String(scope), new Set((Array.isArray(nodeHashes) ? nodeHashes : []).map(normalizeHex64).filter(Boolean)))
599
+ scopeInterests.set(scope, new Set((nodeHashes || []).map(normalizeHex64).filter(Boolean)))
452
600
  },
453
601
  /**
454
602
  * 释放 scope 兴趣。
@@ -456,26 +604,22 @@ export function createLinkRegistry(options = {}) {
456
604
  * @returns {void}
457
605
  */
458
606
  releaseScopeInterest(scope) {
459
- scopeInterests.delete(String(scope))
607
+ scopeInterests.delete(scope)
460
608
  },
461
609
  registerScopeAuthorizer,
462
610
  subscribeScope,
463
611
  /**
464
- * 订阅指定节点的 advert 广播。
612
+ * 监听指定节点的 advert(per-hash,无 topic)。
465
613
  * @param {string} nodeHash 目标节点 64 hex
466
614
  * @param {(verifiedNodeHash: string, body: object) => void | Promise<void>} onAdvert advert 回调
467
- * @returns {Promise<() => void>} 取消订阅函数
615
+ * @returns {Promise<() => void>} 取消函数
468
616
  */
469
- async subscribeNodeAdvert(nodeHash, onAdvert) {
470
- const topic = nodeRendezvousTopic(nodeHash)
471
- return await subscribeTopic(topic, async (bytes, meta) => {
472
- const packet = decryptSignalPacket(topic, bytes)
473
- if (packet?.type !== 'advert') return
474
- const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
475
- if (!verifiedNodeHash) return
476
- noteAdvertPeerHints(verifiedNodeHash, packet.body, meta)
617
+ async watchNodeAdvert(nodeHash, onAdvert) {
618
+ const hash = normalizeHex64(nodeHash)
619
+ return await watchVerifiedNodeAdvert(hash, async (verifiedNodeHash, body, meta) => {
620
+ applyAdvertPeerHints(verifiedNodeHash, body, meta)
477
621
  recentAdverts.touch(verifiedNodeHash, Date.now())
478
- await Promise.resolve(onAdvert(verifiedNodeHash, packet.body))
622
+ await Promise.resolve(onAdvert(verifiedNodeHash, body))
479
623
  })
480
624
  },
481
625
  recentAdverts,
@@ -485,6 +629,8 @@ export function createLinkRegistry(options = {}) {
485
629
  * @returns {Promise<void>}
486
630
  */
487
631
  async shutdown() {
632
+ await meshKeepalive?.stop()
633
+ setDiscoveryLinkDialer(null)
488
634
  await bootstrap.shutdown()
489
635
  overlayRouter?.close()
490
636
  overlayRouter = null
@@ -499,84 +645,97 @@ export function createLinkRegistry(options = {}) {
499
645
  }
500
646
 
501
647
  let defaultRegistry = null
648
+ /** @type {object | null} */
649
+ let pendingRegistryOptions = null
502
650
 
503
651
  /**
504
- * 获取进程级默认 link registry 单例。
505
- * @returns {ReturnType<typeof createLinkRegistry>} 默认 registry
652
+ * 在首次 getLinkRegistry 前配置默认 registry 选项。
653
+ * @param {object} options createLinkRegistry 选项
654
+ * @returns {void}
506
655
  */
507
- export function getLinkRegistry() {
508
- return defaultRegistry ||= createLinkRegistry()
656
+ export function configureLinkRegistry(options = {}) {
657
+ if (defaultRegistry) throw new Error('p2p: configureLinkRegistry must run before getLinkRegistry')
658
+ pendingRegistryOptions = { ...pendingRegistryOptions, ...options }
509
659
  }
510
660
 
511
661
  /**
512
- * 默认 registry 的 ensureLinkToNode 代理。
513
- * @param {...any} args 转发参数
514
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['ensureLinkToNode']>} 链路实例
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']>} 链路列表
662
+ * @returns {void}
527
663
  */
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']>} 取消订阅函数
551
- */
552
- export const onLinkUp = (...args) => getLinkRegistry().onLinkUp(...args)
553
- /**
554
- * 默认 registry 的 onLinkDown 代理。
555
- * @param {...any} args 转发参数
556
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['onLinkDown']>} 取消订阅函数
557
- */
558
- export const onLinkDown = (...args) => getLinkRegistry().onLinkDown(...args)
664
+ export function resetLinkRegistryForTests() {
665
+ defaultRegistry = null
666
+ pendingRegistryOptions = null
667
+ }
668
+
559
669
  /**
560
- * 默认 registry registerScopeInterest 代理。
561
- * @param {...any} args 转发参数
562
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['registerScopeInterest']>} 无返回值
670
+ * 默认 registry 尚未创建时暂存的 scope authorizer 条目。
671
+ * 注册阶段不应触发 createLinkRegistry / resolveLocalIdentity;绑定在 getLinkRegistry flush 时完成。
672
+ * @type {Array<{ prefix: string, authorizer: Function, unregister: (() => void) | null }>}
563
673
  */
564
- export const registerScopeInterest = (...args) => getLinkRegistry().registerScopeInterest(...args)
674
+ const pendingScopeAuthorizers = []
675
+
565
676
  /**
566
- * 默认 registry 的 releaseScopeInterest 代理。
567
- * @param {...any} args 转发参数
568
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['releaseScopeInterest']>} 无返回值
677
+ * 获取进程级默认 link registry 单例。
678
+ * @returns {ReturnType<typeof createLinkRegistry>} 默认 registry
569
679
  */
570
- export const releaseScopeInterest = (...args) => getLinkRegistry().releaseScopeInterest(...args)
680
+ export function getLinkRegistry() {
681
+ if (defaultRegistry) return defaultRegistry
682
+ defaultRegistry = createLinkRegistry(pendingRegistryOptions || {})
683
+ pendingRegistryOptions = null
684
+ for (const entry of pendingScopeAuthorizers)
685
+ entry.unregister = defaultRegistry.registerScopeAuthorizer(entry.prefix, entry.authorizer)
686
+ pendingScopeAuthorizers.length = 0
687
+ return defaultRegistry
688
+ }
689
+
571
690
  /**
572
- * 默认 registry 的 registerScopeAuthorizer 代理。
573
- * @param {...any} args 转发参数
574
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['registerScopeAuthorizer']>} 取消注册函数
691
+ * 默认 registry 方法代理。
692
+ * @param {string} name registry 方法名
693
+ * @returns {(...methodArguments: unknown[]) => unknown} 绑定到 getLinkRegistry()[name] 的函数
575
694
  */
576
- export const registerScopeAuthorizer = (...args) => getLinkRegistry().registerScopeAuthorizer(...args)
695
+ const bindRegistryMethod = name => (...methodArguments) => getLinkRegistry()[name](...methodArguments)
696
+
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} */
702
+ export const ensureLinkToNode = bindRegistryMethod('ensureLinkToNode')
703
+ /** @type {(...methodArguments: unknown[]) => unknown} */
704
+ export const getLink = bindRegistryMethod('getLink')
705
+ /** @type {(...methodArguments: unknown[]) => unknown} */
706
+ export const listLinks = bindRegistryMethod('listLinks')
707
+ /** @type {(...methodArguments: unknown[]) => unknown} */
708
+ export const closeLink = bindRegistryMethod('closeLink')
709
+ /** 经活跃链路向节点发送 envelope。 @type {(...methodArguments: unknown[]) => unknown} */
710
+ export const sendToNodeLink = bindRegistryMethod('sendToNodeLink')
711
+ /** @type {(...methodArguments: unknown[]) => unknown} */
712
+ export const relayEnvelopeToNode = bindRegistryMethod('relayEnvelopeToNode')
713
+ /** @type {(...methodArguments: unknown[]) => unknown} */
714
+ export const onLinkUp = bindRegistryMethod('onLinkUp')
715
+ /** @type {(...methodArguments: unknown[]) => unknown} */
716
+ export const onLinkDown = bindRegistryMethod('onLinkDown')
717
+ /** @type {(...methodArguments: unknown[]) => unknown} */
718
+ export const registerScopeInterest = bindRegistryMethod('registerScopeInterest')
719
+ /** @type {(...methodArguments: unknown[]) => unknown} */
720
+ export const releaseScopeInterest = bindRegistryMethod('releaseScopeInterest')
721
+ /** @type {(...methodArguments: unknown[]) => unknown} */
722
+ export const subscribeScope = bindRegistryMethod('subscribeScope')
723
+
577
724
  /**
578
- * 默认 registry 的 subscribeScope 代理。
579
- * @param {...any} args 转发参数
580
- * @returns {ReturnType<ReturnType<typeof createLinkRegistry>['subscribeScope']>} 取消订阅函数
725
+ * 注册默认 registry 的 scope authorizer。
726
+ * 不急切创建 registry(不必 resolveLocalIdentity);首次 getLinkRegistry 时 flush。
727
+ * @param {string} prefix scope 前缀
728
+ * @param {Function} authorizer 校验函数
729
+ * @returns {() => void} 取消注册函数
581
730
  */
582
- export const subscribeScope = (...args) => getLinkRegistry().subscribeScope(...args)
731
+ export function registerScopeAuthorizer(prefix, authorizer) {
732
+ if (defaultRegistry) return defaultRegistry.registerScopeAuthorizer(prefix, authorizer)
733
+
734
+ const entry = { prefix, authorizer, unregister: null }
735
+ pendingScopeAuthorizers.push(entry)
736
+ return () => {
737
+ const index = pendingScopeAuthorizers.indexOf(entry)
738
+ if (index !== -1) pendingScopeAuthorizers.splice(index, 1)
739
+ entry.unregister?.()
740
+ }
741
+ }