@steve02081504/fount-p2p 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +41 -10
  2. package/core/composite_key.mjs +5 -5
  3. package/crypto/crypto.mjs +3 -3
  4. package/crypto/key.mjs +12 -12
  5. package/dag/storage.mjs +36 -36
  6. package/discovery/advert_peer_hints.mjs +1 -1
  7. package/discovery/adverts.mjs +109 -0
  8. package/discovery/bt/index.mjs +153 -112
  9. package/discovery/bt/probe_child.mjs +2 -1
  10. package/discovery/bt/runtime.mjs +2 -9
  11. package/discovery/index.mjs +267 -62
  12. package/discovery/internal/signal_crypto.mjs +109 -0
  13. package/discovery/lan.mjs +228 -0
  14. package/discovery/nostr.mjs +394 -59
  15. package/federation/chunk_fetch_pending.mjs +10 -13
  16. package/federation/manifest_fetch_pending.mjs +1 -3
  17. package/files/assemble.mjs +14 -14
  18. package/files/assemble_stream.mjs +6 -6
  19. package/files/chunk_responder.mjs +29 -20
  20. package/files/evfs.mjs +29 -28
  21. package/files/manifest_fetch.mjs +16 -3
  22. package/files/public_manifest.mjs +11 -11
  23. package/files/transfer_key_registry.mjs +2 -2
  24. package/index.mjs +86 -11
  25. package/infra/cli.mjs +62 -0
  26. package/infra/debug_log.mjs +56 -0
  27. package/infra/default_node_dir.mjs +22 -0
  28. package/infra/priority.mjs +56 -0
  29. package/infra/service.mjs +140 -0
  30. package/infra/tunables.json +5 -0
  31. package/link/frame.mjs +9 -16
  32. package/link/handshake.mjs +14 -15
  33. package/link/pipe.mjs +8 -8
  34. package/link/providers/ble_gatt.mjs +6 -6
  35. package/link/rtc.mjs +3 -3
  36. package/mailbox/consumer_registry.mjs +2 -2
  37. package/mailbox/deliver_or_store.mjs +5 -5
  38. package/mailbox/wire.mjs +28 -24
  39. package/node/entity_store.mjs +6 -6
  40. package/node/instance.mjs +46 -27
  41. package/node/local_data_revision.mjs +26 -0
  42. package/node/log.mjs +56 -0
  43. package/node/network.mjs +37 -5
  44. package/node/reputation_store.mjs +4 -4
  45. package/node/reputation_sync.mjs +318 -0
  46. package/node/routing_profile.mjs +21 -0
  47. package/node/signaling_config.mjs +30 -11
  48. package/overlay/index.mjs +22 -1
  49. package/package.json +13 -2
  50. package/rooms/scoped_link.mjs +17 -166
  51. package/transport/advert_ingest.mjs +11 -14
  52. package/transport/group_link_set.mjs +149 -99
  53. package/transport/link_registry.mjs +211 -60
  54. package/transport/mesh_keepalive.mjs +217 -0
  55. package/transport/node_scope.mjs +289 -0
  56. package/transport/offer_answer.mjs +45 -48
  57. package/transport/peer_pool.mjs +116 -14
  58. package/transport/remote_user_room.mjs +6 -9
  59. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  60. package/transport/runtime_bootstrap.mjs +170 -108
  61. package/transport/tunables.json +11 -0
  62. package/transport/tunables.mjs +18 -0
  63. package/transport/user_room.mjs +83 -158
  64. package/trust_graph/build.mjs +0 -2
  65. package/trust_graph/cache.mjs +12 -3
  66. package/trust_graph/registry.mjs +6 -6
  67. package/trust_graph/send.mjs +0 -3
  68. package/utils/async_mutex.mjs +4 -4
  69. package/utils/emit_safe.mjs +3 -3
  70. package/utils/json_io.mjs +12 -12
  71. package/utils/map_pool.mjs +5 -5
  72. package/wire/part_ingress.mjs +32 -28
  73. package/wire/part_query.mjs +26 -20
  74. package/discovery/mdns.mjs +0 -197
  75. package/transport/signal_crypto.mjs +0 -104
@@ -1,5 +1,5 @@
1
+ import { bumpLocalDataRevision } from '../node/local_data_revision.mjs'
1
2
  import { registerFederationRoomProvider } from '../registries/room_provider.mjs'
2
- import { invalidateTrustGraphCache } from '../trust_graph/cache.mjs'
3
3
 
4
4
  import { closeLink, ensureLinkToNode, getLink } from './link_registry.mjs'
5
5
  import { USER_ROOM_SCOPE } from './room_scopes.mjs'
@@ -14,7 +14,7 @@ import { USER_ROOM_SCOPE } from './room_scopes.mjs'
14
14
 
15
15
  /** @type {Map<string, RemoteUserRoomSlot>} nodeHash → slot */
16
16
  const slots = new Map()
17
- /** @type {Map<string, Promise<RemoteUserRoomSlot | null>>} nodeHash → 进行中的 promise */
17
+ /** @type {Map<string, Promise<RemoteUserRoomSlot>>} nodeHash → 进行中的 promise */
18
18
  const inflights = new Map()
19
19
 
20
20
  registerFederationRoomProvider('remote-user-room', () => {
@@ -25,7 +25,7 @@ registerFederationRoomProvider('remote-user-room', () => {
25
25
  * 加入目标节点的用户房间(幂等)。
26
26
  * @param {string} username 本地 replica 用户名
27
27
  * @param {string} targetNodeHash 目标节点 64 hex
28
- * @returns {Promise<RemoteUserRoomSlot | null>} 房间槽
28
+ * @returns {Promise<RemoteUserRoomSlot>} 房间槽
29
29
  */
30
30
  export async function ensureRemoteUserRoom(username, targetNodeHash) {
31
31
  void username
@@ -37,7 +37,8 @@ export async function ensureRemoteUserRoom(username, targetNodeHash) {
37
37
 
38
38
  const task = (async () => {
39
39
  try {
40
- if (!await ensureLinkToNode(key)) return null
40
+ if (!await ensureLinkToNode(key))
41
+ throw new Error(`p2p: ensureRemoteUserRoom link failed for ${key}`)
41
42
 
42
43
  /** @type {import('../registries/room_provider.mjs').FederationRoomSlot} */
43
44
  const roomSlot = {
@@ -78,13 +79,9 @@ export async function ensureRemoteUserRoom(username, targetNodeHash) {
78
79
  },
79
80
  }
80
81
  slots.set(key, slot)
81
- invalidateTrustGraphCache()
82
+ bumpLocalDataRevision()
82
83
  return slot
83
84
  }
84
- catch (error) {
85
- console.error('p2p: failed to join remote user room', key, error)
86
- return null
87
- }
88
85
  finally {
89
86
  inflights.delete(key)
90
87
  }
@@ -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[]} */
@@ -43,7 +47,6 @@ export function collectFastListenProviders(ownedLanTcp) {
43
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
  }
@@ -55,29 +58,17 @@ export function collectFastListenProviders(ownedLanTcp) {
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,39 +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
- /**
204
- *
205
- */
206
- stopSignalListener = () => {
207
- try { stop() } catch { /* ignore */ }
208
- prev?.()
209
- }
210
- return
211
- }
212
- const prev = stopAdvert
213
- /**
214
- *
215
- */
216
- stopAdvert = () => {
217
- try { stop() } catch { /* ignore */ }
218
- prev?.()
219
- }
210
+ async function buildNetworkAdvertBytes() {
211
+ const body = await buildLocalAdvert('network')
212
+ return encryptAdvertForScope('network', localIdentity, body)
220
213
  }
221
214
 
222
215
  /**
223
- * 后台探测 BT discovery / ble_gatt listen。
224
216
  * @param {number} gen 启动世代
225
217
  * @returns {Promise<void>}
226
218
  */
@@ -235,23 +227,33 @@ export function createRuntimeBootstrap(deps) {
235
227
  const provider = bt.createBluetoothDiscoveryProvider()
236
228
  registerDiscoveryProvider(provider)
237
229
  if (generation !== gen || !isLive()) return
238
- if (provider.caps?.canSignal && typeof provider.onSignal === 'function')
230
+ if (typeof provider.startPresence === 'function')
239
231
  try {
240
- const stop = await provider.onSignal(selfTopic, bytes => {
241
- void handleIncomingSignal(bytes).catch(() => { })
242
- })
243
- if (typeof stop === 'function' && generation === gen && isLive())
244
- 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
+ }
245
243
  }
246
244
  catch { /* ignore */ }
247
- if (provider.caps?.canDiscover && typeof provider.advertise === 'function')
245
+ if (provider.caps?.canSignal && typeof provider.listenNodeSignals === 'function')
248
246
  try {
249
- const stop = await provider.advertise(selfTopic, encryptSignalPacket(selfTopic, {
250
- type: 'advert',
251
- body: await buildLocalAdvert(selfTopic),
252
- }))
253
- if (typeof stop === 'function' && generation === gen && isLive())
254
- 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
+ }
255
257
  }
256
258
  catch { /* ignore */ }
257
259
  }
@@ -265,35 +267,82 @@ export function createRuntimeBootstrap(deps) {
265
267
  }
266
268
 
267
269
  /**
268
- * 后台:lan_tcp listen selfTopic listenSignals/advertise。
269
- * @param {number} gen 启动世代(shutdown 时递增以作废后台任务)
270
+ * @param {number} gen 启动世代
270
271
  * @returns {Promise<void>}
271
272
  */
272
273
  function warmListenAndDiscovery(gen) {
273
274
  const listenProviders = collectFastListenProviders(ownedLanTcp)
274
- // 同步挂上 Promise,避免 ensureRuntime 返回后 buildLocalAdvert 看不到 lanListenReady。
275
275
  lanListenReady = Promise.all(listenProviders.map(provider => startProviderListening(provider))).then(() => { })
276
276
  signalListenReady = (async () => {
277
277
  await lanListenReady.catch(() => { })
278
278
  if (generation !== gen || !isLive()) return
279
279
  if (!listDiscoveryProviders().length) return
280
- stopSignalListener = await listenSignals(selfTopic, bytes => {
280
+ stopSignalListener = await listenNodeSignals(localIdentity.nodeHash, bytes => {
281
281
  void handleIncomingSignal(bytes).catch(() => { })
282
282
  })
283
+ nodeDebug('p2p:runtime signal listening', {
284
+ self: shortHash(localIdentity.nodeHash),
285
+ providers: listDiscoveryProviders().map(provider => provider.id),
286
+ })
283
287
  })()
284
288
  return (async () => {
285
289
  await signalListenReady.catch(() => { })
286
290
  if (generation !== gen || !isLive()) return
287
291
  if (!listDiscoveryProviders().length) return
288
- stopAdvert = await advertiseTopic(selfTopic, encryptSignalPacket(selfTopic, {
289
- type: 'advert',
290
- 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(),
291
297
  }))
298
+ if (isConnectivityDebug())
299
+ nodeDebug('p2p:runtime presence started', {
300
+ self: shortHash(localIdentity.nodeHash),
301
+ lanTcpPort: lanTcpPort(),
302
+ relays: resolveNostrRelayUrls().length,
303
+ })
292
304
  })()
293
305
  }
294
306
 
295
307
  /**
296
- * 仅注册 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
+ /**
297
346
  * @returns {Promise<void>}
298
347
  */
299
348
  async function ensureRuntime() {
@@ -303,16 +352,19 @@ export function createRuntimeBootstrap(deps) {
303
352
  runtimeStarted = true
304
353
  const gen = generation
305
354
  await ensureLinkProviders()
306
- if (autoRegisterDiscoveryProviders) {
307
- const providerIds = new Set(listDiscoveryProviders().map(provider => provider.id))
308
- if (!providerIds.has('mdns'))
309
- registerDiscoveryProvider(createMdnsDiscoveryProvider())
310
- if (!providerIds.has('nostr'))
311
- registerDiscoveryProvider(createNostrDiscoveryProvider({
312
- relayUrls: getSignalingRuntimeConfig().relayOverride
313
- ?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls),
314
- }))
315
- }
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
+ })
316
368
  bluetoothWarm = warmBluetoothTask(gen).catch(() => { })
317
369
  runtimeWarm = warmListenAndDiscovery(gen)
318
370
  void runtimeWarm.catch(() => { })
@@ -326,20 +378,24 @@ export function createRuntimeBootstrap(deps) {
326
378
  }
327
379
 
328
380
  /**
329
- * 停止暖机与 owned providers(链路表由 registry 自行清理)。
330
- * 不 await bluetoothWarm:noble/poweredOn 可能挂死;generation 已作废其后副作用。
331
381
  * @returns {Promise<void>}
332
382
  */
333
383
  async function shutdown() {
334
384
  runtimeStarted = false
335
385
  generation++
336
- await runtimeWarm?.catch(() => { })
337
- stopAdvert?.()
386
+ stopSignalingWatch?.()
387
+ stopSignalingWatch = null
388
+ stopPresence?.()
338
389
  stopSignalListener?.()
339
- stopAdvert = null
390
+ stopPresence = null
340
391
  stopSignalListener = null
392
+ await Promise.race([
393
+ runtimeWarm?.catch(() => { }) ?? Promise.resolve(),
394
+ new Promise(resolve => setTimeout(resolve, 500)),
395
+ ])
341
396
  for (const stop of stopLinkListeners.splice(0))
342
397
  try { stop() } catch { /* ignore */ }
398
+ clearDiscoveryProviders()
343
399
  if (ownedLanTcp) {
344
400
  unregisterLinkProvider(ownedLanTcp.id)
345
401
  ownedLanTcp = null
@@ -352,6 +408,7 @@ export function createRuntimeBootstrap(deps) {
352
408
  signalListenReady = null
353
409
  runtimeWarm = null
354
410
  bluetoothWarm = null
411
+ reloadInflight = null
355
412
  }
356
413
 
357
414
  return {
@@ -360,10 +417,15 @@ export function createRuntimeBootstrap(deps) {
360
417
  whenSignalListening,
361
418
  buildLocalAdvert,
362
419
  lanTcpPort,
363
- /** @returns {ReturnType<typeof createLanTcpLinkProvider> | null} 本 registry 持有的 lan_tcp */
420
+ /**
421
+ * @returns {ReturnType<typeof createLanTcpLinkProvider> | null} 本 registry 持有的 lan_tcp provider
422
+ */
364
423
  ownedLanTcp: () => ownedLanTcp,
365
- /** @returns {ReturnType<typeof createBleGattLinkProvider> | null} 本 registry 持有的 ble_gatt */
424
+ /**
425
+ * @returns {ReturnType<typeof createBleGattLinkProvider> | null} 本 registry 持有的 BLE GATT provider
426
+ */
366
427
  ownedBleGatt: () => ownedBleGatt,
428
+ reloadDiscoveryRelays,
367
429
  shutdown,
368
430
  }
369
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
+ }