@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
|
@@ -2,21 +2,21 @@ import { Buffer } from 'node:buffer'
|
|
|
2
2
|
|
|
3
3
|
import { compareHex64Asc, normalizeHex64 } from '../core/hexIds.mjs'
|
|
4
4
|
import { keyPairFromSeed } from '../crypto/crypto.mjs'
|
|
5
|
-
import {
|
|
6
|
-
import { subscribeTopic } from '../discovery/index.mjs'
|
|
7
|
-
import { verifySignedAdvert } from '../link/handshake.mjs'
|
|
5
|
+
import { watchVerifiedNodeAdvert, setDiscoveryLinkDialer, prepareConnectToNode } from '../discovery/index.mjs'
|
|
8
6
|
import { listLinkProviders } from '../link/providers/index.mjs'
|
|
9
7
|
import { ensureNodeSeed, getNodeHash } from '../node/identity.mjs'
|
|
8
|
+
import { nodeDebug, shortHash } from '../node/log.mjs'
|
|
9
|
+
import { loadPeerPoolView } from '../node/network.mjs'
|
|
10
10
|
import { createOverlayRouter } from '../overlay/index.mjs'
|
|
11
|
+
import { emitSafe } from '../utils/emit_safe.mjs'
|
|
11
12
|
import { createLruMap } from '../utils/lru.mjs'
|
|
12
13
|
|
|
14
|
+
import { applyAdvertPeerHints } from './advert_ingest.mjs'
|
|
13
15
|
import { DEFAULT_ICE_SERVERS } from './ice_servers.mjs'
|
|
16
|
+
import { createMeshKeepalive } from './mesh_keepalive.mjs'
|
|
14
17
|
import { createOfferAnswerDial } from './offer_answer.mjs'
|
|
18
|
+
import { pickMeshEvictionVictim } from './peer_pool.mjs'
|
|
15
19
|
import { createRuntimeBootstrap } from './runtime_bootstrap.mjs'
|
|
16
|
-
import {
|
|
17
|
-
decryptSignalPacket,
|
|
18
|
-
nodeRendezvousTopic,
|
|
19
|
-
} from './signal_crypto.mjs'
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* 解析或从节点种子推导本地身份。
|
|
@@ -63,15 +63,23 @@ function subscribeBucket(buckets, key, listener) {
|
|
|
63
63
|
* @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array }} [options.localIdentity] 本地身份
|
|
64
64
|
* @param {RTCConfiguration['iceServers']} [options.iceServers] ICE 服务器列表(包内 webrtc provider 使用)
|
|
65
65
|
* @param {number} [options.maxActive] 最大并发活跃链路数
|
|
66
|
+
* @param {boolean} [options.meshKeepalive=true] 是否启用 mesh 保活(N/K 扫描拨号)
|
|
66
67
|
* @param {boolean} [options.autoRegisterDiscoveryProviders] 是否自动注册 discovery provider
|
|
67
68
|
* @param {boolean} [options.autoRegisterLinkProviders] 是否自动注册内置 link provider
|
|
68
69
|
* @returns {object} link registry 接口(对上层即 fount 网络:ensure/send/subscribe,无传输类型)
|
|
69
70
|
*/
|
|
70
71
|
export function createLinkRegistry(options = {}) {
|
|
71
72
|
const localIdentity = resolveLocalIdentity(options.localIdentity)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
73
|
+
let iceServers = options.iceServers?.length ? options.iceServers : DEFAULT_ICE_SERVERS
|
|
74
|
+
let maxActive = Math.max(4, Number(options.maxActive) || 32)
|
|
75
|
+
const savedMaxActive = maxActive
|
|
76
|
+
const meshKeepaliveEnabled = options.meshKeepalive !== false
|
|
77
|
+
/** @type {((nodeHash: string) => number) | null} */
|
|
78
|
+
let priorityWeightFunction = null
|
|
79
|
+
/** @type {Set<string>} */
|
|
80
|
+
let exploreLinkHashes = new Set()
|
|
81
|
+
/** @type {ReturnType<typeof createMeshKeepalive> | null} */
|
|
82
|
+
let meshKeepalive = null
|
|
75
83
|
/** @type {Map<string, object>} */
|
|
76
84
|
const links = new Map()
|
|
77
85
|
/** @type {Map<string, Promise<object | null>>} */
|
|
@@ -104,8 +112,8 @@ export function createLinkRegistry(options = {}) {
|
|
|
104
112
|
* @returns {boolean} 候选链应保留为规范链时 true
|
|
105
113
|
*/
|
|
106
114
|
function linkIsPreferred(link, remoteNodeHash, against = null) {
|
|
107
|
-
const level =
|
|
108
|
-
const againstLevel =
|
|
115
|
+
const level = link.level || 0
|
|
116
|
+
const againstLevel = against?.level || 0
|
|
109
117
|
if (against && level !== againstLevel) return level > againstLevel
|
|
110
118
|
const cmp = compareHex64Asc(localIdentity.nodeHash, remoteNodeHash)
|
|
111
119
|
return link.initiator ? cmp < 0 : cmp > 0
|
|
@@ -125,8 +133,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
125
133
|
const wasCanonical = links.get(remoteNodeHash) === link
|
|
126
134
|
if (!wasCanonical) return
|
|
127
135
|
links.delete(remoteNodeHash)
|
|
128
|
-
|
|
129
|
-
try { listener(remoteNodeHash, reason) } catch { /* ignore */ }
|
|
136
|
+
emitSafe(linkDownListeners, remoteNodeHash, reason)
|
|
130
137
|
})
|
|
131
138
|
}
|
|
132
139
|
|
|
@@ -140,15 +147,14 @@ export function createLinkRegistry(options = {}) {
|
|
|
140
147
|
const normalized = normalizeHex64(remoteNodeHash)
|
|
141
148
|
const existing = links.get(normalized)
|
|
142
149
|
if (existing && existing !== candidate && !linkIsPreferred(candidate, normalized, existing)) {
|
|
143
|
-
await candidate.close(
|
|
150
|
+
await candidate.close(candidate.level !== existing.level ? 'provider-loser' : 'glare-loser')
|
|
144
151
|
return
|
|
145
152
|
}
|
|
146
153
|
links.set(normalized, candidate)
|
|
147
154
|
wireLink(normalized, candidate)
|
|
148
155
|
if (existing && existing !== candidate)
|
|
149
|
-
await existing.close(
|
|
150
|
-
|
|
151
|
-
try { listener(normalized, candidate) } catch { /* ignore */ }
|
|
156
|
+
await existing.close(candidate.level !== existing.level ? 'provider-replaced' : 'glare-replaced')
|
|
157
|
+
emitSafe(linkUpListeners, normalized, candidate)
|
|
152
158
|
}
|
|
153
159
|
|
|
154
160
|
/**
|
|
@@ -173,10 +179,13 @@ export function createLinkRegistry(options = {}) {
|
|
|
173
179
|
|
|
174
180
|
const bootstrap = createRuntimeBootstrap({
|
|
175
181
|
localIdentity,
|
|
176
|
-
selfTopic,
|
|
177
182
|
autoRegisterDiscoveryProviders: options.autoRegisterDiscoveryProviders !== false,
|
|
178
183
|
autoRegisterLinkProviders: options.autoRegisterLinkProviders !== false,
|
|
179
184
|
onInboundLink,
|
|
185
|
+
/**
|
|
186
|
+
* @param {Uint8Array} bytes 入站加密信令
|
|
187
|
+
* @returns {Promise<void>}
|
|
188
|
+
*/
|
|
180
189
|
handleIncomingSignal: bytes => handleIncomingSignal(bytes),
|
|
181
190
|
})
|
|
182
191
|
|
|
@@ -186,35 +195,41 @@ export function createLinkRegistry(options = {}) {
|
|
|
186
195
|
* @returns {number} 权重值
|
|
187
196
|
*/
|
|
188
197
|
function scopeWeight(remoteNodeHash) {
|
|
189
|
-
let weight = 0
|
|
198
|
+
let weight = priorityWeightFunction?.(remoteNodeHash) ?? 0
|
|
190
199
|
for (const hashes of scopeInterests.values())
|
|
191
200
|
if (hashes.has(remoteNodeHash)) weight++
|
|
192
201
|
return weight
|
|
193
202
|
}
|
|
194
203
|
|
|
195
204
|
/**
|
|
196
|
-
* 超出 maxActive
|
|
205
|
+
* 超出 maxActive 时驱逐:探索链优先于熟人/scope 权重。
|
|
197
206
|
* @returns {Promise<void>}
|
|
198
207
|
*/
|
|
199
208
|
async function trimToBudget() {
|
|
200
209
|
if (links.size < maxActive) return
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
const peers = loadPeerPoolView()
|
|
211
|
+
const victimHash = pickMeshEvictionVictim(
|
|
212
|
+
[...links.keys()],
|
|
213
|
+
exploreLinkHashes,
|
|
214
|
+
peers.trustedPeers,
|
|
215
|
+
scopeWeight,
|
|
216
|
+
)
|
|
217
|
+
const victimLink = victimHash ? links.get(victimHash) : null
|
|
218
|
+
if (victimLink) await victimLink.close('budget-evict')
|
|
205
219
|
}
|
|
206
220
|
|
|
207
|
-
;({ handleIncomingSignal, dialOfferAnswer } = createOfferAnswerDial({
|
|
221
|
+
; ({ handleIncomingSignal, dialOfferAnswer } = createOfferAnswerDial({
|
|
208
222
|
localIdentity,
|
|
209
|
-
|
|
210
|
-
|
|
223
|
+
/**
|
|
224
|
+
* @returns {RTCConfiguration['iceServers']} 当前 ICE 服务器列表
|
|
225
|
+
*/
|
|
226
|
+
get iceServers() { return iceServers },
|
|
211
227
|
signalSessions,
|
|
212
228
|
registerResolvedLink,
|
|
213
229
|
trimToBudget,
|
|
214
230
|
/**
|
|
215
|
-
*
|
|
216
|
-
* @
|
|
217
|
-
* @returns {object | null | undefined} 规范链实例
|
|
231
|
+
* @param {string} remoteNodeHash 远端 nodeHash
|
|
232
|
+
* @returns {object | null} 已有规范链路
|
|
218
233
|
*/
|
|
219
234
|
getCanonicalLink: remoteNodeHash => links.get(normalizeHex64(remoteNodeHash)),
|
|
220
235
|
}))
|
|
@@ -251,33 +266,100 @@ export function createLinkRegistry(options = {}) {
|
|
|
251
266
|
if (links.has(normalized)) return links.get(normalized)
|
|
252
267
|
if (inflights.has(normalized)) return await inflights.get(normalized)
|
|
253
268
|
const task = (async () => {
|
|
269
|
+
nodeDebug('p2p:dial start', { peer: shortHash(normalized) })
|
|
270
|
+
await prepareConnectToNode(normalized)
|
|
254
271
|
const providers = listLinkProviders()
|
|
255
272
|
for (const provider of providers)
|
|
256
273
|
try {
|
|
257
274
|
if (typeof provider.canReach === 'function') {
|
|
258
275
|
const reachable = await Promise.resolve(provider.canReach({ nodeHash: normalized }))
|
|
259
|
-
if (!reachable)
|
|
276
|
+
if (!reachable) {
|
|
277
|
+
nodeDebug('p2p:dial skip', { peer: shortHash(normalized), provider: provider.id, reason: 'canReach=false' })
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
260
280
|
}
|
|
261
281
|
if (typeof provider.isAvailable === 'function') {
|
|
262
282
|
const available = await Promise.resolve(provider.isAvailable())
|
|
263
|
-
if (!available)
|
|
283
|
+
if (!available) {
|
|
284
|
+
nodeDebug('p2p:dial skip', { peer: shortHash(normalized), provider: provider.id, reason: 'isAvailable=false' })
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
264
287
|
}
|
|
288
|
+
nodeDebug('p2p:dial try', { peer: shortHash(normalized), provider: provider.id })
|
|
265
289
|
if (provider.caps?.needsOfferAnswer) {
|
|
266
290
|
const link = await dialOfferAnswer(provider, normalized)
|
|
267
|
-
if (link)
|
|
291
|
+
if (link) {
|
|
292
|
+
nodeDebug('p2p:dial ok', { peer: shortHash(normalized), provider: provider.id })
|
|
293
|
+
return link
|
|
294
|
+
}
|
|
295
|
+
nodeDebug('p2p:dial miss', { peer: shortHash(normalized), provider: provider.id })
|
|
268
296
|
continue
|
|
269
297
|
}
|
|
270
298
|
const link = await dialProvider(provider, normalized)
|
|
271
|
-
if (link)
|
|
299
|
+
if (link) {
|
|
300
|
+
nodeDebug('p2p:dial ok', { peer: shortHash(normalized), provider: provider.id })
|
|
301
|
+
return link
|
|
302
|
+
}
|
|
303
|
+
nodeDebug('p2p:dial miss', { peer: shortHash(normalized), provider: provider.id })
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
nodeDebug('p2p:dial fail', {
|
|
307
|
+
peer: shortHash(normalized),
|
|
308
|
+
provider: provider.id,
|
|
309
|
+
err: String(error?.message || error),
|
|
310
|
+
})
|
|
272
311
|
}
|
|
273
|
-
catch { /* dial failed — try next provider */ }
|
|
274
312
|
|
|
313
|
+
nodeDebug('p2p:dial exhausted', { peer: shortHash(normalized) })
|
|
275
314
|
return null
|
|
276
315
|
})().finally(() => inflights.delete(normalized))
|
|
277
316
|
inflights.set(normalized, task)
|
|
278
317
|
return await task
|
|
279
318
|
}
|
|
280
319
|
|
|
320
|
+
meshKeepalive = createMeshKeepalive({
|
|
321
|
+
registry: {
|
|
322
|
+
localIdentity,
|
|
323
|
+
/**
|
|
324
|
+
* @returns {Array<{ nodeHash: string, link: object }>} 当前链路列表
|
|
325
|
+
*/
|
|
326
|
+
listLinks: () => [...links.entries()].map(([nodeHash, link]) => ({ nodeHash, link })),
|
|
327
|
+
/**
|
|
328
|
+
* @param {string} nodeHash 目标 nodeHash
|
|
329
|
+
* @returns {object | null} 已有链路或 null
|
|
330
|
+
*/
|
|
331
|
+
getLink: nodeHash => links.get(normalizeHex64(nodeHash)) || null,
|
|
332
|
+
ensureLinkToNode: ensureDirectLinkToNode,
|
|
333
|
+
/**
|
|
334
|
+
* @param {(nodeHash: string) => void} listener link up 回调
|
|
335
|
+
* @returns {() => void} 取消订阅
|
|
336
|
+
*/
|
|
337
|
+
onLinkUp: listener => {
|
|
338
|
+
linkUpListeners.add(listener)
|
|
339
|
+
return () => linkUpListeners.delete(listener)
|
|
340
|
+
},
|
|
341
|
+
/**
|
|
342
|
+
* @param {(nodeHash: string, reason: string) => void} listener link down 回调
|
|
343
|
+
* @returns {() => void} 取消订阅
|
|
344
|
+
*/
|
|
345
|
+
onLinkDown: listener => {
|
|
346
|
+
linkDownListeners.add(listener)
|
|
347
|
+
return () => linkDownListeners.delete(listener)
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
enabled: meshKeepaliveEnabled,
|
|
351
|
+
})
|
|
352
|
+
exploreLinkHashes = meshKeepalive.exploreLinkHashes
|
|
353
|
+
setDiscoveryLinkDialer(ensureDirectLinkToNode)
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
*
|
|
357
|
+
*/
|
|
358
|
+
const ensureRuntimeWithMesh = async () => {
|
|
359
|
+
await bootstrap.ensureRuntime()
|
|
360
|
+
meshKeepalive?.start()
|
|
361
|
+
}
|
|
362
|
+
|
|
281
363
|
/**
|
|
282
364
|
* 经已有直连发送 envelope。
|
|
283
365
|
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
@@ -318,6 +400,13 @@ export function createLinkRegistry(options = {}) {
|
|
|
318
400
|
return overlayRouter
|
|
319
401
|
}
|
|
320
402
|
|
|
403
|
+
/**
|
|
404
|
+
* @returns {import('../overlay/index.mjs').OverlayRouter} overlay 路由器单例
|
|
405
|
+
*/
|
|
406
|
+
function ensureOverlayRouter() {
|
|
407
|
+
return getOverlayRouter()
|
|
408
|
+
}
|
|
409
|
+
|
|
321
410
|
/**
|
|
322
411
|
* 经 overlay 多跳 relay envelope 到无直连的节点。
|
|
323
412
|
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
@@ -327,8 +416,9 @@ export function createLinkRegistry(options = {}) {
|
|
|
327
416
|
async function relayEnvelopeToNode(remoteNodeHash, envelope) {
|
|
328
417
|
if (!links.size || envelope?.scope === 'overlay') return false
|
|
329
418
|
try {
|
|
330
|
-
const
|
|
331
|
-
await
|
|
419
|
+
const overlay = getOverlayRouter()
|
|
420
|
+
const path = await overlay.discoverRoute(remoteNodeHash)
|
|
421
|
+
await overlay.relay(path, envelope)
|
|
332
422
|
return true
|
|
333
423
|
}
|
|
334
424
|
catch {
|
|
@@ -347,6 +437,17 @@ export function createLinkRegistry(options = {}) {
|
|
|
347
437
|
|| await relayEnvelopeToNode(remoteNodeHash, envelope)
|
|
348
438
|
}
|
|
349
439
|
|
|
440
|
+
/**
|
|
441
|
+
* 同步结果直接返回;thenable 才 await(避免每条 envelope 造 microtask)。
|
|
442
|
+
* @param {unknown} value 可能为 Promise 的返回值
|
|
443
|
+
* @returns {Promise<unknown>} 已 resolve 的值
|
|
444
|
+
*/
|
|
445
|
+
async function maybeAwait(value) {
|
|
446
|
+
if (value != null && typeof /** @type {{ then?: unknown }} */ value.then === 'function')
|
|
447
|
+
return await value
|
|
448
|
+
return value
|
|
449
|
+
}
|
|
450
|
+
|
|
350
451
|
/**
|
|
351
452
|
* 将入站 envelope 派发到 scope 监听器(经 authorizer 校验)。
|
|
352
453
|
* @param {string} senderNodeHash 发送方节点 64 hex
|
|
@@ -355,16 +456,16 @@ export function createLinkRegistry(options = {}) {
|
|
|
355
456
|
* @returns {Promise<void>}
|
|
356
457
|
*/
|
|
357
458
|
async function dispatchEnvelope(senderNodeHash, envelope, link) {
|
|
358
|
-
const scope =
|
|
459
|
+
const scope = envelope.scope || ''
|
|
359
460
|
for (const [prefix, authorizer] of scopeAuthorizers.entries())
|
|
360
461
|
if (scope.startsWith(prefix)) {
|
|
361
|
-
const allowed = await
|
|
462
|
+
const allowed = await maybeAwait(authorizer(scope, senderNodeHash, envelope, link))
|
|
362
463
|
if (!allowed) return
|
|
363
464
|
}
|
|
364
465
|
for (const [prefix, listeners] of scopeListeners.entries())
|
|
365
466
|
if (scope.startsWith(prefix))
|
|
366
467
|
for (const listener of listeners)
|
|
367
|
-
await
|
|
468
|
+
await maybeAwait(listener(senderNodeHash, envelope, link))
|
|
368
469
|
}
|
|
369
470
|
|
|
370
471
|
/**
|
|
@@ -374,7 +475,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
374
475
|
* @returns {() => void} 取消订阅函数
|
|
375
476
|
*/
|
|
376
477
|
function subscribeScope(prefix, listener) {
|
|
377
|
-
return subscribeBucket(scopeListeners,
|
|
478
|
+
return subscribeBucket(scopeListeners, prefix, listener)
|
|
378
479
|
}
|
|
379
480
|
|
|
380
481
|
/**
|
|
@@ -384,8 +485,8 @@ export function createLinkRegistry(options = {}) {
|
|
|
384
485
|
* @returns {() => void} 取消注册函数
|
|
385
486
|
*/
|
|
386
487
|
function registerScopeAuthorizer(prefix, authorizer) {
|
|
387
|
-
scopeAuthorizers.set(
|
|
388
|
-
return () => scopeAuthorizers.delete(
|
|
488
|
+
scopeAuthorizers.set(prefix, authorizer)
|
|
489
|
+
return () => scopeAuthorizers.delete(prefix)
|
|
389
490
|
}
|
|
390
491
|
|
|
391
492
|
return {
|
|
@@ -393,8 +494,55 @@ export function createLinkRegistry(options = {}) {
|
|
|
393
494
|
buildLocalAdvert: bootstrap.buildLocalAdvert,
|
|
394
495
|
lanTcpPort: bootstrap.lanTcpPort,
|
|
395
496
|
whenListening: bootstrap.whenListening,
|
|
396
|
-
ensureRuntime:
|
|
497
|
+
ensureRuntime: ensureRuntimeWithMesh,
|
|
498
|
+
reloadDiscoveryRelays: bootstrap.reloadDiscoveryRelays,
|
|
499
|
+
ensureOverlayRouter,
|
|
500
|
+
getOverlayRouter,
|
|
397
501
|
ensureLinkToNode: ensureDirectLinkToNode,
|
|
502
|
+
/**
|
|
503
|
+
* 设置最大并发活跃链路数。
|
|
504
|
+
* @param {number} value 最大并发活跃链路数
|
|
505
|
+
* @returns {Promise<void>}
|
|
506
|
+
*/
|
|
507
|
+
async setMaxActive(value) {
|
|
508
|
+
maxActive = Math.max(4, Math.min(128, Math.floor(Number(value) || savedMaxActive)))
|
|
509
|
+
await trimToBudget()
|
|
510
|
+
},
|
|
511
|
+
/**
|
|
512
|
+
* @returns {number} 当前 maxActive
|
|
513
|
+
*/
|
|
514
|
+
getMaxActive() {
|
|
515
|
+
return maxActive
|
|
516
|
+
},
|
|
517
|
+
/**
|
|
518
|
+
* 设置 ICE 服务器列表。
|
|
519
|
+
* @param {RTCConfiguration['iceServers']} servers ICE 列表
|
|
520
|
+
* @returns {void}
|
|
521
|
+
*/
|
|
522
|
+
setIceServers(servers) {
|
|
523
|
+
iceServers = servers?.length ? servers : DEFAULT_ICE_SERVERS
|
|
524
|
+
},
|
|
525
|
+
/**
|
|
526
|
+
* @returns {RTCConfiguration['iceServers']} 当前 ICE 服务器列表
|
|
527
|
+
*/
|
|
528
|
+
getIceServers() {
|
|
529
|
+
return iceServers
|
|
530
|
+
},
|
|
531
|
+
/**
|
|
532
|
+
* infra/trim:额外优先级权重;null 清除。
|
|
533
|
+
* @param {((nodeHash: string) => number) | null} weightFunction 额外 trim 权重;null 清除
|
|
534
|
+
* @returns {void}
|
|
535
|
+
*/
|
|
536
|
+
setPriorityWeightFunction(weightFunction) {
|
|
537
|
+
priorityWeightFunction = typeof weightFunction === 'function' ? weightFunction : null
|
|
538
|
+
},
|
|
539
|
+
/**
|
|
540
|
+
* @param {string} nodeHash - 节点 64-hex hash
|
|
541
|
+
* @returns {number} 额外路由 trim 权重
|
|
542
|
+
*/
|
|
543
|
+
getPriorityWeight(nodeHash) {
|
|
544
|
+
return priorityWeightFunction?.(nodeHash) ?? 0
|
|
545
|
+
},
|
|
398
546
|
/**
|
|
399
547
|
* 获取到指定节点的活跃链路。
|
|
400
548
|
* @param {string} nodeHash 节点 64 hex
|
|
@@ -448,7 +596,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
448
596
|
* @returns {void}
|
|
449
597
|
*/
|
|
450
598
|
registerScopeInterest(scope, nodeHashes) {
|
|
451
|
-
scopeInterests.set(
|
|
599
|
+
scopeInterests.set(scope, new Set((nodeHashes || []).map(normalizeHex64).filter(Boolean)))
|
|
452
600
|
},
|
|
453
601
|
/**
|
|
454
602
|
* 释放 scope 兴趣。
|
|
@@ -456,26 +604,22 @@ export function createLinkRegistry(options = {}) {
|
|
|
456
604
|
* @returns {void}
|
|
457
605
|
*/
|
|
458
606
|
releaseScopeInterest(scope) {
|
|
459
|
-
scopeInterests.delete(
|
|
607
|
+
scopeInterests.delete(scope)
|
|
460
608
|
},
|
|
461
609
|
registerScopeAuthorizer,
|
|
462
610
|
subscribeScope,
|
|
463
611
|
/**
|
|
464
|
-
*
|
|
612
|
+
* 监听指定节点的 advert(per-hash,无 topic)。
|
|
465
613
|
* @param {string} nodeHash 目标节点 64 hex
|
|
466
614
|
* @param {(verifiedNodeHash: string, body: object) => void | Promise<void>} onAdvert advert 回调
|
|
467
|
-
* @returns {Promise<() => void>}
|
|
615
|
+
* @returns {Promise<() => void>} 取消函数
|
|
468
616
|
*/
|
|
469
|
-
async
|
|
470
|
-
const
|
|
471
|
-
return await
|
|
472
|
-
|
|
473
|
-
if (packet?.type !== 'advert') return
|
|
474
|
-
const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
|
|
475
|
-
if (!verifiedNodeHash) return
|
|
476
|
-
noteAdvertPeerHints(verifiedNodeHash, packet.body, meta)
|
|
617
|
+
async watchNodeAdvert(nodeHash, onAdvert) {
|
|
618
|
+
const hash = normalizeHex64(nodeHash)
|
|
619
|
+
return await watchVerifiedNodeAdvert(hash, async (verifiedNodeHash, body, meta) => {
|
|
620
|
+
applyAdvertPeerHints(verifiedNodeHash, body, meta)
|
|
477
621
|
recentAdverts.touch(verifiedNodeHash, Date.now())
|
|
478
|
-
await Promise.resolve(onAdvert(verifiedNodeHash,
|
|
622
|
+
await Promise.resolve(onAdvert(verifiedNodeHash, body))
|
|
479
623
|
})
|
|
480
624
|
},
|
|
481
625
|
recentAdverts,
|
|
@@ -485,6 +629,8 @@ export function createLinkRegistry(options = {}) {
|
|
|
485
629
|
* @returns {Promise<void>}
|
|
486
630
|
*/
|
|
487
631
|
async shutdown() {
|
|
632
|
+
await meshKeepalive?.stop()
|
|
633
|
+
setDiscoveryLinkDialer(null)
|
|
488
634
|
await bootstrap.shutdown()
|
|
489
635
|
overlayRouter?.close()
|
|
490
636
|
overlayRouter = null
|
|
@@ -499,84 +645,97 @@ export function createLinkRegistry(options = {}) {
|
|
|
499
645
|
}
|
|
500
646
|
|
|
501
647
|
let defaultRegistry = null
|
|
648
|
+
/** @type {object | null} */
|
|
649
|
+
let pendingRegistryOptions = null
|
|
502
650
|
|
|
503
651
|
/**
|
|
504
|
-
*
|
|
505
|
-
* @
|
|
652
|
+
* 在首次 getLinkRegistry 前配置默认 registry 选项。
|
|
653
|
+
* @param {object} options createLinkRegistry 选项
|
|
654
|
+
* @returns {void}
|
|
506
655
|
*/
|
|
507
|
-
export function
|
|
508
|
-
|
|
656
|
+
export function configureLinkRegistry(options = {}) {
|
|
657
|
+
if (defaultRegistry) throw new Error('p2p: configureLinkRegistry must run before getLinkRegistry')
|
|
658
|
+
pendingRegistryOptions = { ...pendingRegistryOptions, ...options }
|
|
509
659
|
}
|
|
510
660
|
|
|
511
661
|
/**
|
|
512
|
-
*
|
|
513
|
-
* @param {...any} args 转发参数
|
|
514
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['ensureLinkToNode']>} 链路实例
|
|
515
|
-
*/
|
|
516
|
-
export const ensureLinkToNode = (...args) => getLinkRegistry().ensureLinkToNode(...args)
|
|
517
|
-
/**
|
|
518
|
-
* 默认 registry 的 getLink 代理。
|
|
519
|
-
* @param {...any} args 转发参数
|
|
520
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['getLink']>} 链路实例
|
|
521
|
-
*/
|
|
522
|
-
export const getLink = (...args) => getLinkRegistry().getLink(...args)
|
|
523
|
-
/**
|
|
524
|
-
* 默认 registry 的 listLinks 代理。
|
|
525
|
-
* @param {...any} args 转发参数
|
|
526
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['listLinks']>} 链路列表
|
|
662
|
+
* @returns {void}
|
|
527
663
|
*/
|
|
528
|
-
export
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
*/
|
|
534
|
-
export const closeLink = (...args) => getLinkRegistry().closeLink(...args)
|
|
535
|
-
/**
|
|
536
|
-
* 默认 registry 的 sendToNodeLink 代理。
|
|
537
|
-
* @param {...any} args 转发参数
|
|
538
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['sendToNodeLink']>} 是否成功
|
|
539
|
-
*/
|
|
540
|
-
export const sendToNodeLink = (...args) => getLinkRegistry().sendToNodeLink(...args)
|
|
541
|
-
/**
|
|
542
|
-
* 默认 registry 的 relayEnvelopeToNode 代理。
|
|
543
|
-
* @param {...any} args 转发参数
|
|
544
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['relayEnvelopeToNode']>} 是否成功
|
|
545
|
-
*/
|
|
546
|
-
export const relayEnvelopeToNode = (...args) => getLinkRegistry().relayEnvelopeToNode(...args)
|
|
547
|
-
/**
|
|
548
|
-
* 默认 registry 的 onLinkUp 代理。
|
|
549
|
-
* @param {...any} args 转发参数
|
|
550
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['onLinkUp']>} 取消订阅函数
|
|
551
|
-
*/
|
|
552
|
-
export const onLinkUp = (...args) => getLinkRegistry().onLinkUp(...args)
|
|
553
|
-
/**
|
|
554
|
-
* 默认 registry 的 onLinkDown 代理。
|
|
555
|
-
* @param {...any} args 转发参数
|
|
556
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['onLinkDown']>} 取消订阅函数
|
|
557
|
-
*/
|
|
558
|
-
export const onLinkDown = (...args) => getLinkRegistry().onLinkDown(...args)
|
|
664
|
+
export function resetLinkRegistryForTests() {
|
|
665
|
+
defaultRegistry = null
|
|
666
|
+
pendingRegistryOptions = null
|
|
667
|
+
}
|
|
668
|
+
|
|
559
669
|
/**
|
|
560
|
-
* 默认 registry
|
|
561
|
-
*
|
|
562
|
-
* @
|
|
670
|
+
* 默认 registry 尚未创建时暂存的 scope authorizer 条目。
|
|
671
|
+
* 注册阶段不应触发 createLinkRegistry / resolveLocalIdentity;绑定在 getLinkRegistry flush 时完成。
|
|
672
|
+
* @type {Array<{ prefix: string, authorizer: Function, unregister: (() => void) | null }>}
|
|
563
673
|
*/
|
|
564
|
-
|
|
674
|
+
const pendingScopeAuthorizers = []
|
|
675
|
+
|
|
565
676
|
/**
|
|
566
|
-
*
|
|
567
|
-
* @
|
|
568
|
-
* @returns {ReturnType<ReturnType<typeof createLinkRegistry>['releaseScopeInterest']>} 无返回值
|
|
677
|
+
* 获取进程级默认 link registry 单例。
|
|
678
|
+
* @returns {ReturnType<typeof createLinkRegistry>} 默认 registry
|
|
569
679
|
*/
|
|
570
|
-
export
|
|
680
|
+
export function getLinkRegistry() {
|
|
681
|
+
if (defaultRegistry) return defaultRegistry
|
|
682
|
+
defaultRegistry = createLinkRegistry(pendingRegistryOptions || {})
|
|
683
|
+
pendingRegistryOptions = null
|
|
684
|
+
for (const entry of pendingScopeAuthorizers)
|
|
685
|
+
entry.unregister = defaultRegistry.registerScopeAuthorizer(entry.prefix, entry.authorizer)
|
|
686
|
+
pendingScopeAuthorizers.length = 0
|
|
687
|
+
return defaultRegistry
|
|
688
|
+
}
|
|
689
|
+
|
|
571
690
|
/**
|
|
572
|
-
* 默认 registry
|
|
573
|
-
* @param {
|
|
574
|
-
* @returns {
|
|
691
|
+
* 默认 registry 方法代理。
|
|
692
|
+
* @param {string} name registry 方法名
|
|
693
|
+
* @returns {(...methodArguments: unknown[]) => unknown} 绑定到 getLinkRegistry()[name] 的函数
|
|
575
694
|
*/
|
|
576
|
-
|
|
695
|
+
const bindRegistryMethod = name => (...methodArguments) => getLinkRegistry()[name](...methodArguments)
|
|
696
|
+
|
|
697
|
+
/** 确保 overlay 路由器已创建。 @type {(...methodArguments: unknown[]) => unknown} */
|
|
698
|
+
export const ensureOverlayRouter = bindRegistryMethod('ensureOverlayRouter')
|
|
699
|
+
/** 热重载 discovery relay 配置。 @type {(...methodArguments: unknown[]) => unknown} */
|
|
700
|
+
export const reloadDiscoveryRelays = bindRegistryMethod('reloadDiscoveryRelays')
|
|
701
|
+
/** 确保到 nodeHash 的活跃链路。 @type {(...methodArguments: unknown[]) => unknown} */
|
|
702
|
+
export const ensureLinkToNode = bindRegistryMethod('ensureLinkToNode')
|
|
703
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
704
|
+
export const getLink = bindRegistryMethod('getLink')
|
|
705
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
706
|
+
export const listLinks = bindRegistryMethod('listLinks')
|
|
707
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
708
|
+
export const closeLink = bindRegistryMethod('closeLink')
|
|
709
|
+
/** 经活跃链路向节点发送 envelope。 @type {(...methodArguments: unknown[]) => unknown} */
|
|
710
|
+
export const sendToNodeLink = bindRegistryMethod('sendToNodeLink')
|
|
711
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
712
|
+
export const relayEnvelopeToNode = bindRegistryMethod('relayEnvelopeToNode')
|
|
713
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
714
|
+
export const onLinkUp = bindRegistryMethod('onLinkUp')
|
|
715
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
716
|
+
export const onLinkDown = bindRegistryMethod('onLinkDown')
|
|
717
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
718
|
+
export const registerScopeInterest = bindRegistryMethod('registerScopeInterest')
|
|
719
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
720
|
+
export const releaseScopeInterest = bindRegistryMethod('releaseScopeInterest')
|
|
721
|
+
/** @type {(...methodArguments: unknown[]) => unknown} */
|
|
722
|
+
export const subscribeScope = bindRegistryMethod('subscribeScope')
|
|
723
|
+
|
|
577
724
|
/**
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
* @
|
|
725
|
+
* 注册默认 registry 的 scope authorizer。
|
|
726
|
+
* 不急切创建 registry(不必 resolveLocalIdentity);首次 getLinkRegistry 时 flush。
|
|
727
|
+
* @param {string} prefix scope 前缀
|
|
728
|
+
* @param {Function} authorizer 校验函数
|
|
729
|
+
* @returns {() => void} 取消注册函数
|
|
581
730
|
*/
|
|
582
|
-
export
|
|
731
|
+
export function registerScopeAuthorizer(prefix, authorizer) {
|
|
732
|
+
if (defaultRegistry) return defaultRegistry.registerScopeAuthorizer(prefix, authorizer)
|
|
733
|
+
|
|
734
|
+
const entry = { prefix, authorizer, unregister: null }
|
|
735
|
+
pendingScopeAuthorizers.push(entry)
|
|
736
|
+
return () => {
|
|
737
|
+
const index = pendingScopeAuthorizers.indexOf(entry)
|
|
738
|
+
if (index !== -1) pendingScopeAuthorizers.splice(index, 1)
|
|
739
|
+
entry.unregister?.()
|
|
740
|
+
}
|
|
741
|
+
}
|