@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.
- package/README.md +42 -11
- package/core/bytes_codec.mjs +65 -10
- package/core/composite_key.mjs +5 -5
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +109 -0
- package/discovery/bt/index.mjs +154 -113
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +5 -12
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +430 -141
- package/federation/chunk_fetch_pending.mjs +14 -18
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/federation/topo_order_memo.mjs +11 -4
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_fetch.mjs +2 -2
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +31 -30
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -131
- package/link/handshake.mjs +19 -20
- package/link/pipe.mjs +53 -79
- package/link/providers/ble_gatt.mjs +19 -31
- package/link/providers/lan_tcp.mjs +17 -32
- package/link/providers/link_id_pipe.mjs +46 -0
- package/link/providers/webrtc.mjs +42 -51
- package/link/rtc.mjs +24 -12
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +6 -6
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/identity.mjs +4 -4
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +46 -14
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +35 -14
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -182
- package/schemas/discovery.mjs +1 -2
- package/schemas/federation_pull.mjs +2 -2
- package/schemas/part_query.mjs +1 -1
- package/schemas/remote_event.mjs +1 -1
- package/transport/advert_ingest.mjs +17 -0
- package/transport/group_link_set.mjs +165 -134
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +283 -124
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +48 -47
- package/transport/peer_pool.mjs +169 -78
- package/transport/remote_user_room.mjs +18 -23
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +172 -104
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +93 -161
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +1 -4
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +11 -0
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +38 -36
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +28 -23
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
- package/discovery/mdns.mjs +0 -191
- 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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
197
|
+
return !NON_CRITICAL_FED_ACTIONS.has(actionName)
|
|
184
198
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 服务端 WebRTC polyfill
|
|
3
|
-
*
|
|
2
|
+
* 服务端 WebRTC polyfill 在 Windows 等环境常产出 `.local` host candidate,
|
|
3
|
+
* 远端无法解析。按 iceLocalHostnamePolicy 改写为 loopback 或丢弃。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
/** @typedef {'none' | 'rewrite-loopback' | 'drop'}
|
|
6
|
+
/** @typedef {'none' | 'rewrite-loopback' | 'drop'} IceLocalHostnamePolicy */
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @param {string | null | undefined} candidateSdp ICE candidate SDP 行
|
|
10
|
-
* @param {
|
|
10
|
+
* @param {IceLocalHostnamePolicy} policy 处理策略
|
|
11
11
|
* @returns {string | null} 处理后的 SDP;drop 策略下不可用时返回 null
|
|
12
12
|
*/
|
|
13
|
-
export function
|
|
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 {
|
|
28
|
+
* @param {IceLocalHostnamePolicy} policy 处理策略
|
|
29
29
|
* @returns {RTCIceCandidate | { candidate?: string } | null | undefined} 过滤/改写后的 candidate
|
|
30
30
|
*/
|
|
31
|
-
export function
|
|
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 =
|
|
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 {
|
|
53
|
+
* @param {IceLocalHostnamePolicy} [policy='drop'] 策略
|
|
54
54
|
* @returns {typeof RTCPeerConnection} 包装后的 RTCPeerConnection 类(none 时原样返回)
|
|
55
55
|
*/
|
|
56
|
-
export function
|
|
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 =
|
|
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
|
|
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 =
|
|
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 {
|
|
3
|
-
|
|
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}
|
|
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[]}
|
|
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}
|
|
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
|
|
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
|
-
*
|
|
113
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
165
|
-
* @
|
|
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(
|
|
185
|
+
async function buildLocalAdvert(scope = 'node') {
|
|
169
186
|
await whenListening()
|
|
170
187
|
const tcpPort = lanTcpPort()
|
|
171
|
-
return await
|
|
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
|
|
204
|
+
catch { /* provider listen unavailable */ }
|
|
192
205
|
}
|
|
193
206
|
|
|
194
207
|
/**
|
|
195
|
-
*
|
|
196
|
-
* @param {'signal' | 'advert'} kind 停止钩子类型
|
|
197
|
-
* @param {() => void} stop 新 provider 的取消函数
|
|
198
|
-
* @returns {void}
|
|
208
|
+
* @returns {Promise<Uint8Array>} 加密后的全网 advert 字节
|
|
199
209
|
*/
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
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 (
|
|
230
|
+
if (typeof provider.startPresence === 'function')
|
|
233
231
|
try {
|
|
234
|
-
const stop = await provider.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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?.
|
|
245
|
+
if (provider.caps?.canSignal && typeof provider.listenNodeSignals === 'function')
|
|
242
246
|
try {
|
|
243
|
-
const stop = await provider.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
331
|
-
|
|
386
|
+
stopSignalingWatch?.()
|
|
387
|
+
stopSignalingWatch = null
|
|
388
|
+
stopPresence?.()
|
|
332
389
|
stopSignalListener?.()
|
|
333
|
-
|
|
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
|
-
/**
|
|
420
|
+
/**
|
|
421
|
+
* @returns {ReturnType<typeof createLanTcpLinkProvider> | null} 本 registry 持有的 lan_tcp provider
|
|
422
|
+
*/
|
|
358
423
|
ownedLanTcp: () => ownedLanTcp,
|
|
359
|
-
/**
|
|
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,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
|
+
}
|