@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
@@ -23,7 +23,7 @@ export function resolveRtcBudgetLimits(limits = {}) {
23
23
  maxActive: Math.max(4, Math.min(128, Number(limits.maxActive) || 32)),
24
24
  maxJoinsPerMin: Math.max(1, Math.min(120, Number(limits.maxJoinsPerMin) || 12)),
25
25
  overloadCooldownMs: Math.max(1000, Number(limits.overloadCooldownMs) || 15_000),
26
- trustedPeers: Array.isArray(limits.trustedPeers) ? limits.trustedPeers.map(String) : [],
26
+ trustedPeers: limits.trustedPeers || [],
27
27
  trustedReserveFraction: Math.max(0.1, Math.min(0.5, Number(limits.trustedReserveFraction) || DEFAULT_TRUSTED_RESERVE_FRACTION)),
28
28
  minTrustedReserved: Math.max(1, Math.floor(Number(limits.minTrustedReserved) || DEFAULT_MIN_TRUSTED_RESERVED)),
29
29
  }
@@ -85,10 +85,14 @@ export function takeRtcJoinSlot(roomKey, peerId, limits = {}, sourceId = 'peer')
85
85
  const isTrusted = peerId && bucket.trustedPeers.has(peerId)
86
86
  const trustedReserved = Math.max(minTrustedReserved, Math.floor(maxActive * trustedReserveFraction))
87
87
  const maxNonTrusted = Math.max(1, maxActive - trustedReserved)
88
- const nonTrustedCount = [...bucket.active].filter(id => !bucket.trustedPeers.has(id)).length
88
+ let nonTrustedCount = 0
89
+ for (const id of bucket.active)
90
+ if (!bucket.trustedPeers.has(id)) nonTrustedCount++
89
91
  if (!isTrusted) {
90
92
  const source = String(sourceId || 'peer')
91
- const sameSource = [...bucket.sourceByPeer.entries()].filter(([, s]) => s === source).length
93
+ let sameSource = 0
94
+ for (const peerSource of bucket.sourceByPeer.values())
95
+ if (peerSource === source) sameSource++
92
96
  const sourceCap = Math.max(1, Math.floor(maxActive * MAX_SOURCE_SLOT_FRACTION))
93
97
  if (sameSource >= sourceCap) return false
94
98
  if (nonTrustedCount >= maxNonTrusted && bucket.active.size >= maxActive) {
@@ -149,6 +153,16 @@ export function releaseRtcPeer(roomKey, peerId) {
149
153
  bucket.active.delete(peerId)
150
154
  bucket.sourceByPeer.delete(peerId)
151
155
  bucket.peerNodeHash.delete(peerId)
156
+ // 房间空闲后丢掉桶,避免 leave 过的 roomKey 永久占内存
157
+ if (!bucket.active.size) budgets.delete(roomKey)
158
+ }
159
+
160
+ /**
161
+ * 当前预算桶数量(测试用)。
162
+ * @returns {number} 房间桶数
163
+ */
164
+ export function rtcBudgetRoomCount() {
165
+ return budgets.size
152
166
  }
153
167
 
154
168
  /** RTC 过载时跳过的非关键联邦 action */
@@ -180,5 +194,5 @@ const NON_CRITICAL_FED_ACTIONS = new Set([
180
194
  */
181
195
  export function isFederationActionAllowedUnderLoad(roomKey, actionName, limits = {}) {
182
196
  if (!isRtcRoomOverloaded(roomKey, limits)) return true
183
- return !NON_CRITICAL_FED_ACTIONS.has(String(actionName || '').trim())
197
+ return !NON_CRITICAL_FED_ACTIONS.has(actionName)
184
198
  }
@@ -1,16 +1,16 @@
1
1
  /**
2
- * 服务端 WebRTC polyfill(werift / node-datachannel)在 Windows 等环境常产出 `.local` mDNS host candidate,
3
- * 远端无法解析。按策略改写为 loopback 或丢弃。
2
+ * 服务端 WebRTC polyfill Windows 等环境常产出 `.local` host candidate,
3
+ * 远端无法解析。按 iceLocalHostnamePolicy 改写为 loopback 或丢弃。
4
4
  */
5
5
 
6
- /** @typedef {'none' | 'rewrite-loopback' | 'drop'} MdnsCandidatePolicy */
6
+ /** @typedef {'none' | 'rewrite-loopback' | 'drop'} IceLocalHostnamePolicy */
7
7
 
8
8
  /**
9
9
  * @param {string | null | undefined} candidateSdp ICE candidate SDP 行
10
- * @param {MdnsCandidatePolicy} policy 处理策略
10
+ * @param {IceLocalHostnamePolicy} policy 处理策略
11
11
  * @returns {string | null} 处理后的 SDP;drop 策略下不可用时返回 null
12
12
  */
13
- export function applyMdnsHostCandidatePolicy(candidateSdp, policy) {
13
+ export function applyIceLocalHostnamePolicy(candidateSdp, policy) {
14
14
  const sdp = String(candidateSdp || '').trim()
15
15
  if (!sdp || !/\.local/i.test(sdp)) return sdp || null
16
16
  if (!/\btyp host\b/i.test(sdp)) return sdp
@@ -25,15 +25,15 @@ export function applyMdnsHostCandidatePolicy(candidateSdp, policy) {
25
25
  /**
26
26
  * @param {RTCIceCandidate | { candidate?: string } | null | undefined} candidate ICE candidate
27
27
  * @param {typeof RTCIceCandidate} RTCIceCandidateCtor 构造函数
28
- * @param {MdnsCandidatePolicy} policy 处理策略
28
+ * @param {IceLocalHostnamePolicy} policy 处理策略
29
29
  * @returns {RTCIceCandidate | { candidate?: string } | null | undefined} 过滤/改写后的 candidate
30
30
  */
31
- export function filterMdnsIceCandidate(candidate, RTCIceCandidateCtor, policy) {
31
+ export function filterIceLocalHostnameCandidate(candidate, RTCIceCandidateCtor, policy) {
32
32
  if (!candidate || policy === 'none') return candidate
33
33
  const raw = typeof candidate === 'string'
34
34
  ? candidate
35
35
  : candidate.candidate ?? candidate.toJSON?.()?.candidate ?? ''
36
- const rewritten = applyMdnsHostCandidatePolicy(raw, policy)
36
+ const rewritten = applyIceLocalHostnamePolicy(raw, policy)
37
37
  if (!rewritten) return null
38
38
  if (rewritten === raw) return candidate
39
39
  try {
@@ -50,10 +50,10 @@ export function filterMdnsIceCandidate(candidate, RTCIceCandidateCtor, policy) {
50
50
  /**
51
51
  * @param {typeof RTCPeerConnection} BaseRTC 原始 polyfill 类
52
52
  * @param {typeof RTCIceCandidate} [RTCIceCandidate] ICE candidate 构造函数
53
- * @param {MdnsCandidatePolicy} [policy='drop'] mDNS 策略
53
+ * @param {IceLocalHostnamePolicy} [policy='drop'] 策略
54
54
  * @returns {typeof RTCPeerConnection} 包装后的 RTCPeerConnection 类(none 时原样返回)
55
55
  */
56
- export function wrapRtcPeerConnectionForMdns(BaseRTC, RTCIceCandidate = globalThis.RTCIceCandidate, policy = 'drop') {
56
+ export function wrapRtcPeerConnectionForIceLocalHostname(BaseRTC, RTCIceCandidate = globalThis.RTCIceCandidate, policy = 'drop') {
57
57
  if (policy === 'none') return BaseRTC
58
58
 
59
59
  /**
@@ -66,7 +66,7 @@ export function wrapRtcPeerConnectionForMdns(BaseRTC, RTCIceCandidate = globalTh
66
66
  handler(event)
67
67
  return
68
68
  }
69
- const filtered = filterMdnsIceCandidate(event.candidate, RTCIceCandidate, policy)
69
+ const filtered = filterIceLocalHostnameCandidate(event.candidate, RTCIceCandidate, policy)
70
70
  if (!filtered) return
71
71
  if (filtered === event.candidate) {
72
72
  handler(event)
@@ -75,7 +75,7 @@ export function wrapRtcPeerConnectionForMdns(BaseRTC, RTCIceCandidate = globalTh
75
75
  handler({ ...event, candidate: filtered })
76
76
  }
77
77
 
78
- return class MdnsFilteredRTCPeerConnection extends BaseRTC {
78
+ return class IceLocalHostnameFilteredRTCPeerConnection extends BaseRTC {
79
79
  /** @type {((event: RTCPeerConnectionIceEvent) => void) | null} */
80
80
  #userIceHandler = null
81
81
 
@@ -98,7 +98,7 @@ export function wrapRtcPeerConnectionForMdns(BaseRTC, RTCIceCandidate = globalTh
98
98
  this.#userIceHandler({ candidate: null })
99
99
  return
100
100
  }
101
- const filtered = filterMdnsIceCandidate(candidate, RTCIceCandidate, policy)
101
+ const filtered = filterIceLocalHostnameCandidate(candidate, RTCIceCandidate, policy)
102
102
  if (filtered)
103
103
  this.#userIceHandler({ candidate: filtered })
104
104
  })
@@ -1,8 +1,16 @@
1
1
  import { normalizeTcpPort } from '../core/tcp_port.mjs'
2
- import { advertiseTopic, listenSignals, listDiscoveryProviders, registerDiscoveryProvider } from '../discovery/index.mjs'
3
- import { createMdnsDiscoveryProvider } from '../discovery/mdns.mjs'
2
+ import {
3
+ buildSignedAdvertForScope,
4
+ clearDiscoveryProviders,
5
+ encryptAdvertForScope,
6
+ listDiscoveryProviders,
7
+ listenNodeSignals,
8
+ registerDiscoveryProvider,
9
+ startDiscoveryPresence,
10
+ unregisterDiscoveryProvider,
11
+ } from '../discovery/index.mjs'
12
+ import { createLanDiscoveryProvider } from '../discovery/lan.mjs'
4
13
  import { mergeSignalingRelayUrls, createNostrDiscoveryProvider } from '../discovery/nostr.mjs'
5
- import { buildSignedAdvert } from '../link/handshake.mjs'
6
14
  import { createBleGattLinkProvider } from '../link/providers/ble_gatt.mjs'
7
15
  import {
8
16
  listLinkProviders,
@@ -12,24 +20,20 @@ import {
12
20
  import { createLanTcpLinkProvider } from '../link/providers/lan_tcp.mjs'
13
21
  import { createWebRtcLinkProvider } from '../link/providers/webrtc.mjs'
14
22
  import { getNodeTransportSettings } from '../node/identity.mjs'
15
- import { getSignalingRuntimeConfig } from '../node/instance.mjs'
16
-
17
- import { encryptSignalPacket } from './signal_crypto.mjs'
23
+ import { getSignalingRuntimeConfig, onNodeChange } from '../node/instance.mjs'
24
+ import { isConnectivityDebug, nodeDebug, shortHash } from '../node/log.mjs'
18
25
 
19
26
  /**
20
- * Provider 是否会在 isAvailable() 调用时触发 native/异步探测(不得在 ensureRuntime 快路径调用)。
21
27
  * @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
22
- * @returns {boolean} `caps.probe === 'native'` 时 true
28
+ * @returns {boolean} 是否使用原生 probe 路径
23
29
  */
24
30
  export function providerHasNativeProbe(provider) {
25
31
  return provider.caps?.probe === 'native'
26
32
  }
27
33
 
28
34
  /**
29
- * 收集快路径应 listen 的 provider(仅 owned lan_tcp + 同步可用者)。
30
- * 切勿调用 caps.probe==='native' 的 isAvailable(会触发 ble/webrtc 探测)。
31
35
  * @param {import('../link/providers/index.mjs').LinkProvider | null} ownedLanTcp 本 registry 持有的 lan_tcp
32
- * @returns {import('../link/providers/index.mjs').LinkProvider[]} 可在 ensureRuntime 快路径 listen 的 provider
36
+ * @returns {import('../link/providers/index.mjs').LinkProvider[]} 可快速启动监听的 provider 列表
33
37
  */
34
38
  export function collectFastListenProviders(ownedLanTcp) {
35
39
  /** @type {import('../link/providers/index.mjs').LinkProvider[]} */
@@ -40,44 +44,31 @@ export function collectFastListenProviders(ownedLanTcp) {
40
44
  if (id.startsWith('lan_tcp') || id.startsWith('ble_gatt')) continue
41
45
  if (typeof provider.ensureListening !== 'function') continue
42
46
  if (providerHasNativeProbe(provider)) continue
43
- if (typeof provider.isAvailable === 'function') {
47
+ if (typeof provider.isAvailable === 'function')
44
48
  try {
45
49
  const available = provider.isAvailable()
46
- // 未标 probe:native 却返回 thenable:跳过,勿 await。
47
50
  if (available && typeof available.then === 'function') continue
48
51
  if (!available) continue
49
52
  }
50
53
  catch { continue }
51
- }
54
+
52
55
  listenProviders.push(provider)
53
56
  }
54
57
  return listenProviders
55
58
  }
56
59
 
57
60
  /**
58
- * Discovery + link listen 暖机(ensureRuntime 立即返回;listen/公网/BT 后台渐进)。
59
61
  * @param {object} deps 依赖注入
60
62
  * @param {{ nodeHash: string, nodePubKey: string, secretKey: Uint8Array }} deps.localIdentity 本地身份
61
- * @param {string} deps.selfTopic 本机 rendezvous topic
62
63
  * @param {boolean} deps.autoRegisterDiscoveryProviders 是否自动注册 discovery provider
63
64
  * @param {boolean} deps.autoRegisterLinkProviders 是否自动注册内置 link provider
64
65
  * @param {(link: object) => void} deps.onInboundLink 入站链路回调
65
66
  * @param {(bytes: Uint8Array) => Promise<void>} deps.handleIncomingSignal 入站加密信令处理
66
- * @returns {{
67
- * ensureRuntime: () => Promise<void>,
68
- * whenListening: () => Promise<void>,
69
- * whenSignalListening: () => Promise<void>,
70
- * buildLocalAdvert: (topic: string) => Promise<object>,
71
- * lanTcpPort: () => number | null,
72
- * ownedLanTcp: () => object | null,
73
- * ownedBleGatt: () => object | null,
74
- * shutdown: () => Promise<void>,
75
- * }} 运行时暖机句柄
67
+ * @returns {object} 运行时暖机句柄
76
68
  */
77
69
  export function createRuntimeBootstrap(deps) {
78
70
  const {
79
71
  localIdentity,
80
- selfTopic,
81
72
  autoRegisterDiscoveryProviders,
82
73
  autoRegisterLinkProviders,
83
74
  onInboundLink,
@@ -89,14 +80,14 @@ export function createRuntimeBootstrap(deps) {
89
80
  let runtimeStart = null
90
81
  /** @type {Promise<void> | null} */
91
82
  let lanListenReady = null
92
- /** @type {Promise<void> | null} selfTopic listenSignals 已挂上(offer/answer dial 前必须等) */
83
+ /** @type {Promise<void> | null} */
93
84
  let signalListenReady = null
94
85
  /** @type {Promise<void> | null} */
95
86
  let runtimeWarm = null
96
87
  /** @type {Promise<void> | null} */
97
88
  let bluetoothWarm = null
98
89
  /** @type {(() => void) | null} */
99
- let stopAdvert = null
90
+ let stopPresence = null
100
91
  /** @type {(() => void) | null} */
101
92
  let stopSignalListener = null
102
93
  /** @type {Array<() => void>} */
@@ -105,19 +96,49 @@ export function createRuntimeBootstrap(deps) {
105
96
  let ownedLanTcp = null
106
97
  /** @type {ReturnType<typeof createBleGattLinkProvider> | null} */
107
98
  let ownedBleGatt = null
108
- /** @type {number} shutdown 时递增,后台任务检查后退出 */
109
99
  let generation = 0
100
+ /** @type {Promise<void> | null} */
101
+ let reloadInflight = null
102
+ /** @type {(() => void) | null} */
103
+ let stopSignalingWatch = null
104
+
105
+ /**
106
+ * @returns {string[]} 当前 Nostr relay URL 列表
107
+ */
108
+ function resolveNostrRelayUrls() {
109
+ return getSignalingRuntimeConfig().relayOverride
110
+ ?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls)
111
+ }
112
+
113
+ /**
114
+ * @returns {void}
115
+ */
116
+ function registerDiscoveryDefaults() {
117
+ const providerIds = new Set(listDiscoveryProviders().map(provider => provider.id))
118
+ if (!providerIds.has('lan'))
119
+ registerDiscoveryProvider(createLanDiscoveryProvider())
120
+ if (!providerIds.has('nostr'))
121
+ registerNostrProvider()
122
+ }
110
123
 
111
124
  /**
112
- * 暖机是否仍在运行(shutdown 后为 false)。
113
- * @returns {boolean} runtime 已启动且未 shutdown 时 true
125
+ * @returns {void}
126
+ */
127
+ function registerNostrProvider() {
128
+ unregisterDiscoveryProvider('nostr')
129
+ registerDiscoveryProvider(createNostrDiscoveryProvider({
130
+ getRelayUrls: resolveNostrRelayUrls,
131
+ }))
132
+ }
133
+
134
+ /**
135
+ * @returns {boolean} runtime 是否已启动
114
136
  */
115
137
  function isLive() {
116
138
  return runtimeStarted
117
139
  }
118
140
 
119
141
  /**
120
- * 自动注册默认 LinkProvider(lan_tcp / webrtc / ble_gatt)。
121
142
  * @returns {Promise<void>}
122
143
  */
123
144
  async function ensureLinkProviders() {
@@ -136,8 +157,7 @@ export function createRuntimeBootstrap(deps) {
136
157
  }
137
158
 
138
159
  /**
139
- * registry LAN TCP 监听端口。
140
- * @returns {number | null} listen 端口;未 listen 为 null
160
+ * @returns {number | null} 本机 lan_tcp 监听端口,未就绪为 null
141
161
  */
142
162
  function lanTcpPort() {
143
163
  const endpoint = typeof ownedLanTcp?.localEndpoint === 'function' ? ownedLanTcp.localEndpoint() : null
@@ -145,7 +165,6 @@ export function createRuntimeBootstrap(deps) {
145
165
  }
146
166
 
147
167
  /**
148
- * 等待本地 lan_tcp listen 落定。shell / startNode 不应调用。
149
168
  * @returns {Promise<void>}
150
169
  */
151
170
  async function whenListening() {
@@ -153,7 +172,6 @@ export function createRuntimeBootstrap(deps) {
153
172
  }
154
173
 
155
174
  /**
156
- * 等待 selfTopic 信令监听挂上(needsOfferAnswer dial 前调用)。
157
175
  * @returns {Promise<void>}
158
176
  */
159
177
  async function whenSignalListening() {
@@ -161,21 +179,16 @@ export function createRuntimeBootstrap(deps) {
161
179
  }
162
180
 
163
181
  /**
164
- * 构造带本机身份与(若已 listen)tcpPort 的签名 advert
165
- * @param {string} topic 广播主题
166
- * @returns {Promise<object>} 签名 advert
182
+ * @param {import('../discovery/adverts.mjs').AdvertScope} [scope='node'] advert
183
+ * @returns {Promise<object>} 签名后的 advert body
167
184
  */
168
- async function buildLocalAdvert(topic) {
185
+ async function buildLocalAdvert(scope = 'node') {
169
186
  await whenListening()
170
187
  const tcpPort = lanTcpPort()
171
- return await buildSignedAdvert(topic, Date.now(), {
172
- ...localIdentity,
173
- ...tcpPort != null ? { tcpPort } : {},
174
- })
188
+ return await buildSignedAdvertForScope(scope, localIdentity, tcpPort ?? undefined)
175
189
  }
176
190
 
177
191
  /**
178
- * 对单个 link provider 调用 ensureListening,收集 stop。
179
192
  * @param {import('../link/providers/index.mjs').LinkProvider} provider 链路提供者
180
193
  * @returns {Promise<void>}
181
194
  */
@@ -188,33 +201,18 @@ export function createRuntimeBootstrap(deps) {
188
201
  })
189
202
  if (typeof stop === 'function') stopLinkListeners.push(stop)
190
203
  }
191
- catch { /* provider listen unavailable — normal degrade */ }
204
+ catch { /* provider listen unavailable */ }
192
205
  }
193
206
 
194
207
  /**
195
- * 把晚注册 provider stop 链到现有 cleanup。
196
- * @param {'signal' | 'advert'} kind 停止钩子类型
197
- * @param {() => void} stop 新 provider 的取消函数
198
- * @returns {void}
208
+ * @returns {Promise<Uint8Array>} 加密后的全网 advert 字节
199
209
  */
200
- function chainStop(kind, stop) {
201
- if (kind === 'signal') {
202
- const prev = stopSignalListener
203
- stopSignalListener = () => {
204
- try { stop() } catch { /* ignore */ }
205
- prev?.()
206
- }
207
- return
208
- }
209
- const prev = stopAdvert
210
- stopAdvert = () => {
211
- try { stop() } catch { /* ignore */ }
212
- prev?.()
213
- }
210
+ async function buildNetworkAdvertBytes() {
211
+ const body = await buildLocalAdvert('network')
212
+ return encryptAdvertForScope('network', localIdentity, body)
214
213
  }
215
214
 
216
215
  /**
217
- * 后台探测 BT discovery / ble_gatt listen。
218
216
  * @param {number} gen 启动世代
219
217
  * @returns {Promise<void>}
220
218
  */
@@ -229,23 +227,33 @@ export function createRuntimeBootstrap(deps) {
229
227
  const provider = bt.createBluetoothDiscoveryProvider()
230
228
  registerDiscoveryProvider(provider)
231
229
  if (generation !== gen || !isLive()) return
232
- if (provider.caps?.canSignal && typeof provider.onSignal === 'function')
230
+ if (typeof provider.startPresence === 'function')
233
231
  try {
234
- const stop = await provider.onSignal(selfTopic, bytes => {
235
- void handleIncomingSignal(bytes).catch(() => { })
236
- })
237
- if (typeof stop === 'function' && generation === gen && isLive())
238
- chainStop('signal', stop)
232
+ const stop = await provider.startPresence(async () => ({
233
+ nodeHash: localIdentity.nodeHash,
234
+ advertBytes: await buildNetworkAdvertBytes(),
235
+ }))
236
+ if (typeof stop === 'function' && generation === gen && isLive()) {
237
+ const prev = stopPresence
238
+ /**
239
+ *
240
+ */
241
+ stopPresence = () => { try { stop() } catch { /* ignore */ }; prev?.() }
242
+ }
239
243
  }
240
244
  catch { /* ignore */ }
241
- if (provider.caps?.canDiscover && typeof provider.advertise === 'function')
245
+ if (provider.caps?.canSignal && typeof provider.listenNodeSignals === 'function')
242
246
  try {
243
- const stop = await provider.advertise(selfTopic, encryptSignalPacket(selfTopic, {
244
- type: 'advert',
245
- body: await buildLocalAdvert(selfTopic),
246
- }))
247
- if (typeof stop === 'function' && generation === gen && isLive())
248
- chainStop('advert', stop)
247
+ const stop = await provider.listenNodeSignals(localIdentity.nodeHash, bytes => {
248
+ void handleIncomingSignal(bytes).catch(() => { })
249
+ })
250
+ if (typeof stop === 'function' && generation === gen && isLive()) {
251
+ const prev = stopSignalListener
252
+ /**
253
+ *
254
+ */
255
+ stopSignalListener = () => { try { stop() } catch { /* ignore */ }; prev?.() }
256
+ }
249
257
  }
250
258
  catch { /* ignore */ }
251
259
  }
@@ -259,35 +267,82 @@ export function createRuntimeBootstrap(deps) {
259
267
  }
260
268
 
261
269
  /**
262
- * 后台:lan_tcp listen selfTopic listenSignals/advertise。
263
- * @param {number} gen 启动世代(shutdown 时递增以作废后台任务)
270
+ * @param {number} gen 启动世代
264
271
  * @returns {Promise<void>}
265
272
  */
266
273
  function warmListenAndDiscovery(gen) {
267
274
  const listenProviders = collectFastListenProviders(ownedLanTcp)
268
- // 同步挂上 Promise,避免 ensureRuntime 返回后 buildLocalAdvert 看不到 lanListenReady。
269
275
  lanListenReady = Promise.all(listenProviders.map(provider => startProviderListening(provider))).then(() => { })
270
276
  signalListenReady = (async () => {
271
277
  await lanListenReady.catch(() => { })
272
278
  if (generation !== gen || !isLive()) return
273
279
  if (!listDiscoveryProviders().length) return
274
- stopSignalListener = await listenSignals(selfTopic, bytes => {
280
+ stopSignalListener = await listenNodeSignals(localIdentity.nodeHash, bytes => {
275
281
  void handleIncomingSignal(bytes).catch(() => { })
276
282
  })
283
+ nodeDebug('p2p:runtime signal listening', {
284
+ self: shortHash(localIdentity.nodeHash),
285
+ providers: listDiscoveryProviders().map(provider => provider.id),
286
+ })
277
287
  })()
278
288
  return (async () => {
279
289
  await signalListenReady.catch(() => { })
280
290
  if (generation !== gen || !isLive()) return
281
291
  if (!listDiscoveryProviders().length) return
282
- stopAdvert = await advertiseTopic(selfTopic, encryptSignalPacket(selfTopic, {
283
- type: 'advert',
284
- body: await buildLocalAdvert(selfTopic),
292
+ stopPresence = await startDiscoveryPresence(async () => ({
293
+ nodeHash: localIdentity.nodeHash,
294
+ tcpPort: lanTcpPort() ?? undefined,
295
+ advertBody: await buildLocalAdvert('network'),
296
+ advertBytes: await buildNetworkAdvertBytes(),
285
297
  }))
298
+ if (isConnectivityDebug())
299
+ nodeDebug('p2p:runtime presence started', {
300
+ self: shortHash(localIdentity.nodeHash),
301
+ lanTcpPort: lanTcpPort(),
302
+ relays: resolveNostrRelayUrls().length,
303
+ })
286
304
  })()
287
305
  }
288
306
 
289
307
  /**
290
- * 仅注册 provider 并调度后台暖机后立即返回。
308
+ * @returns {Promise<void>}
309
+ */
310
+ async function reloadDiscoveryRelays() {
311
+ if (!runtimeStarted || !autoRegisterDiscoveryProviders) return
312
+ if (reloadInflight) return await reloadInflight
313
+ reloadInflight = (async () => {
314
+ const gen = generation
315
+ stopPresence?.()
316
+ stopSignalListener?.()
317
+ stopPresence = null
318
+ stopSignalListener = null
319
+ registerNostrProvider()
320
+ if (generation !== gen || !isLive()) return
321
+ signalListenReady = (async () => {
322
+ if (generation !== gen || !isLive()) return
323
+ if (!listDiscoveryProviders().length) return
324
+ stopSignalListener = await listenNodeSignals(localIdentity.nodeHash, bytes => {
325
+ void handleIncomingSignal(bytes).catch(() => { })
326
+ })
327
+ })()
328
+ await signalListenReady.catch(() => { })
329
+ if (generation !== gen || !isLive()) return
330
+ stopPresence = await startDiscoveryPresence(async () => ({
331
+ nodeHash: localIdentity.nodeHash,
332
+ tcpPort: lanTcpPort() ?? undefined,
333
+ advertBody: await buildLocalAdvert('network'),
334
+ advertBytes: await buildNetworkAdvertBytes(),
335
+ }))
336
+ })()
337
+ try {
338
+ await reloadInflight
339
+ }
340
+ finally {
341
+ reloadInflight = null
342
+ }
343
+ }
344
+
345
+ /**
291
346
  * @returns {Promise<void>}
292
347
  */
293
348
  async function ensureRuntime() {
@@ -297,16 +352,19 @@ export function createRuntimeBootstrap(deps) {
297
352
  runtimeStarted = true
298
353
  const gen = generation
299
354
  await ensureLinkProviders()
300
- if (autoRegisterDiscoveryProviders) {
301
- const providerIds = new Set(listDiscoveryProviders().map(provider => provider.id))
302
- if (!providerIds.has('mdns'))
303
- registerDiscoveryProvider(createMdnsDiscoveryProvider())
304
- if (!providerIds.has('nostr'))
305
- registerDiscoveryProvider(createNostrDiscoveryProvider({
306
- relayUrls: getSignalingRuntimeConfig().relayOverride
307
- ?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls),
308
- }))
309
- }
355
+ if (autoRegisterDiscoveryProviders)
356
+ registerDiscoveryDefaults()
357
+ if (isConnectivityDebug())
358
+ nodeDebug('p2p:runtime ensure', {
359
+ self: shortHash(localIdentity.nodeHash),
360
+ discovery: listDiscoveryProviders().map(provider => provider.id),
361
+ relays: resolveNostrRelayUrls(),
362
+ })
363
+ if (!stopSignalingWatch)
364
+ stopSignalingWatch = onNodeChange(event => {
365
+ if (event === 'signaling-changed')
366
+ void reloadDiscoveryRelays().catch(() => { })
367
+ })
310
368
  bluetoothWarm = warmBluetoothTask(gen).catch(() => { })
311
369
  runtimeWarm = warmListenAndDiscovery(gen)
312
370
  void runtimeWarm.catch(() => { })
@@ -320,20 +378,24 @@ export function createRuntimeBootstrap(deps) {
320
378
  }
321
379
 
322
380
  /**
323
- * 停止暖机与 owned providers(链路表由 registry 自行清理)。
324
- * 不 await bluetoothWarm:noble/poweredOn 可能挂死;generation 已作废其后副作用。
325
381
  * @returns {Promise<void>}
326
382
  */
327
383
  async function shutdown() {
328
384
  runtimeStarted = false
329
385
  generation++
330
- await runtimeWarm?.catch(() => { })
331
- stopAdvert?.()
386
+ stopSignalingWatch?.()
387
+ stopSignalingWatch = null
388
+ stopPresence?.()
332
389
  stopSignalListener?.()
333
- stopAdvert = null
390
+ stopPresence = null
334
391
  stopSignalListener = null
392
+ await Promise.race([
393
+ runtimeWarm?.catch(() => { }) ?? Promise.resolve(),
394
+ new Promise(resolve => setTimeout(resolve, 500)),
395
+ ])
335
396
  for (const stop of stopLinkListeners.splice(0))
336
397
  try { stop() } catch { /* ignore */ }
398
+ clearDiscoveryProviders()
337
399
  if (ownedLanTcp) {
338
400
  unregisterLinkProvider(ownedLanTcp.id)
339
401
  ownedLanTcp = null
@@ -346,6 +408,7 @@ export function createRuntimeBootstrap(deps) {
346
408
  signalListenReady = null
347
409
  runtimeWarm = null
348
410
  bluetoothWarm = null
411
+ reloadInflight = null
349
412
  }
350
413
 
351
414
  return {
@@ -354,10 +417,15 @@ export function createRuntimeBootstrap(deps) {
354
417
  whenSignalListening,
355
418
  buildLocalAdvert,
356
419
  lanTcpPort,
357
- /** @returns {ReturnType<typeof createLanTcpLinkProvider> | null} 本 registry 持有的 lan_tcp */
420
+ /**
421
+ * @returns {ReturnType<typeof createLanTcpLinkProvider> | null} 本 registry 持有的 lan_tcp provider
422
+ */
358
423
  ownedLanTcp: () => ownedLanTcp,
359
- /** @returns {ReturnType<typeof createBleGattLinkProvider> | null} 本 registry 持有的 ble_gatt */
424
+ /**
425
+ * @returns {ReturnType<typeof createBleGattLinkProvider> | null} 本 registry 持有的 BLE GATT provider
426
+ */
360
427
  ownedBleGatt: () => ownedBleGatt,
428
+ reloadDiscoveryRelays,
361
429
  shutdown,
362
430
  }
363
431
  }
@@ -0,0 +1,11 @@
1
+ {
2
+ "meshKeepaliveIntervalMs": 60000,
3
+ "meshScanLimit": 64,
4
+ "meshPromoteStableMs": 1800000,
5
+ "meshN": 8,
6
+ "meshKMax": 5,
7
+ "meshNLow": 4,
8
+ "meshKMaxLow": 2,
9
+ "groupMemberScanIntervalMs": 30000,
10
+ "groupMemberScanLimit": 64
11
+ }
@@ -0,0 +1,18 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url))
6
+
7
+ /** @type {object | null} */
8
+ let cache = null
9
+
10
+ /**
11
+ * 读取 transport/tunables.json(进程内缓存)。
12
+ * @returns {object} transport/tunables.json
13
+ */
14
+ export function loadTransportTunables() {
15
+ if (cache) return cache
16
+ cache = JSON.parse(readFileSync(join(__dirname, 'tunables.json'), 'utf8'))
17
+ return cache
18
+ }