@steve02081504/fount-p2p 0.0.9 → 0.0.11
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 +25 -7
- package/discovery/bt/index.mjs +4 -12
- package/discovery/bt/probe_child.mjs +20 -0
- package/discovery/bt/runtime.mjs +99 -5
- package/discovery/index.mjs +45 -48
- package/discovery/mdns.mjs +66 -17
- package/discovery/nostr.mjs +149 -67
- package/link/providers/ble_gatt.mjs +4 -15
- package/link/providers/index.mjs +7 -9
- package/link/providers/lan_tcp.mjs +1 -1
- package/link/providers/webrtc.mjs +1 -1
- package/package.json +3 -2
- package/rooms/scoped_link.mjs +3 -3
- package/transport/group_link_set.mjs +4 -4
- package/transport/ice_servers.mjs +10 -1
- package/transport/link_registry.mjs +76 -424
- package/transport/offer_answer.mjs +190 -0
- package/transport/runtime_bootstrap.mjs +363 -0
- package/transport/signal_crypto.mjs +82 -0
|
@@ -1,106 +1,22 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
|
-
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'
|
|
3
2
|
|
|
4
3
|
import { compareHex64Asc, normalizeHex64 } from '../core/hexIds.mjs'
|
|
5
|
-
import {
|
|
6
|
-
import { sha256Hex, keyPairFromSeed } from '../crypto/crypto.mjs'
|
|
4
|
+
import { keyPairFromSeed } from '../crypto/crypto.mjs'
|
|
7
5
|
import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { createBleGattLinkProvider } from '../link/providers/ble_gatt.mjs'
|
|
13
|
-
import {
|
|
14
|
-
listAvailableLinkProviders,
|
|
15
|
-
listLinkProviders,
|
|
16
|
-
registerLinkProvider,
|
|
17
|
-
unregisterLinkProvider,
|
|
18
|
-
} from '../link/providers/index.mjs'
|
|
19
|
-
import { createLanTcpLinkProvider } from '../link/providers/lan_tcp.mjs'
|
|
20
|
-
import { createWebRtcLinkProvider } from '../link/providers/webrtc.mjs'
|
|
21
|
-
import { ensureNodeSeed, getNodeHash, getNodeTransportSettings } from '../node/identity.mjs'
|
|
22
|
-
import { getSignalingRuntimeConfig } from '../node/instance.mjs'
|
|
6
|
+
import { subscribeTopic } from '../discovery/index.mjs'
|
|
7
|
+
import { verifySignedAdvert } from '../link/handshake.mjs'
|
|
8
|
+
import { listLinkProviders } from '../link/providers/index.mjs'
|
|
9
|
+
import { ensureNodeSeed, getNodeHash } from '../node/identity.mjs'
|
|
23
10
|
import { createOverlayRouter } from '../overlay/index.mjs'
|
|
24
11
|
import { createLruMap } from '../utils/lru.mjs'
|
|
25
12
|
|
|
26
13
|
import { DEFAULT_ICE_SERVERS } from './ice_servers.mjs'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* 由 nodeHash 派生节点 rendezvous topic。
|
|
34
|
-
* @param {string} nodeHash 节点 64 hex
|
|
35
|
-
* @returns {string} rendezvous topic 哈希
|
|
36
|
-
*/
|
|
37
|
-
export function nodeRendezvousTopic(nodeHash) {
|
|
38
|
-
return sha256Hex(`${NODE_TOPIC_DOMAIN}${normalizeHex64(nodeHash)}`)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 由房间密钥派生群组 rendezvous topic。
|
|
43
|
-
* @param {string} roomSecret 房间密钥
|
|
44
|
-
* @returns {string} rendezvous topic 哈希
|
|
45
|
-
*/
|
|
46
|
-
export function groupRendezvousTopic(roomSecret) {
|
|
47
|
-
return sha256Hex(`${GROUP_TOPIC_DOMAIN}${String(roomSecret || '')}`)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 由 topic 派生信令 AES 密钥。
|
|
52
|
-
* @param {string} topic rendezvous 主题
|
|
53
|
-
* @returns {Buffer} AES-256 密钥
|
|
54
|
-
*/
|
|
55
|
-
function signalKeyForTopic(topic) {
|
|
56
|
-
return createHash('sha256').update(`${SIGNAL_DOMAIN}:${String(topic)}`).digest()
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 加密信令包为 AES-GCM 字节序列。
|
|
61
|
-
* @param {string} topic rendezvous 主题
|
|
62
|
-
* @param {unknown} packet 待加密 JSON 对象
|
|
63
|
-
* @returns {Uint8Array} 加密后的字节
|
|
64
|
-
*/
|
|
65
|
-
export function encryptSignalPacket(topic, packet) {
|
|
66
|
-
const iv = randomBytes(12)
|
|
67
|
-
const cipher = createCipheriv('aes-256-gcm', signalKeyForTopic(topic), iv)
|
|
68
|
-
const ciphertext = Buffer.concat([
|
|
69
|
-
cipher.update(Buffer.from(JSON.stringify(packet), 'utf8')),
|
|
70
|
-
cipher.final(),
|
|
71
|
-
])
|
|
72
|
-
return Buffer.from(JSON.stringify({
|
|
73
|
-
iv: iv.toString('base64'),
|
|
74
|
-
authTag: cipher.getAuthTag().toString('base64'),
|
|
75
|
-
ciphertext: ciphertext.toString('base64'),
|
|
76
|
-
}))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* 解密信令包;失败时返回 null。
|
|
81
|
-
* @param {string} topic rendezvous 主题
|
|
82
|
-
* @param {Uint8Array} bytes 加密字节
|
|
83
|
-
* @returns {object | null} 解密后的 JSON 对象
|
|
84
|
-
*/
|
|
85
|
-
export function decryptSignalPacket(topic, bytes) {
|
|
86
|
-
try {
|
|
87
|
-
const payload = JSON.parse(Buffer.from(bytes).toString('utf8'))
|
|
88
|
-
const decipher = createDecipheriv(
|
|
89
|
-
'aes-256-gcm',
|
|
90
|
-
signalKeyForTopic(topic),
|
|
91
|
-
Buffer.from(payload.iv, 'base64'),
|
|
92
|
-
)
|
|
93
|
-
decipher.setAuthTag(Buffer.from(payload.authTag, 'base64'))
|
|
94
|
-
const plain = Buffer.concat([
|
|
95
|
-
decipher.update(Buffer.from(payload.ciphertext, 'base64')),
|
|
96
|
-
decipher.final(),
|
|
97
|
-
])
|
|
98
|
-
return JSON.parse(plain.toString('utf8'))
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return null
|
|
102
|
-
}
|
|
103
|
-
}
|
|
14
|
+
import { createOfferAnswerDial } from './offer_answer.mjs'
|
|
15
|
+
import { createRuntimeBootstrap } from './runtime_bootstrap.mjs'
|
|
16
|
+
import {
|
|
17
|
+
decryptSignalPacket,
|
|
18
|
+
nodeRendezvousTopic,
|
|
19
|
+
} from './signal_crypto.mjs'
|
|
104
20
|
|
|
105
21
|
/**
|
|
106
22
|
* 解析或从节点种子推导本地身份。
|
|
@@ -123,60 +39,6 @@ function resolveLocalIdentity(localIdentity) {
|
|
|
123
39
|
}
|
|
124
40
|
}
|
|
125
41
|
|
|
126
|
-
/**
|
|
127
|
-
* 创建带 backlog 的缓冲信令会话。
|
|
128
|
-
* @param {(message: unknown) => Promise<void>} sendRemote 远端发送回调
|
|
129
|
-
* @returns {{ send: (message: unknown) => Promise<void>, onRemote: (handler: (message: unknown) => void) => () => void, deliver: (message: unknown) => void, clear: () => void }} 信令会话
|
|
130
|
-
*/
|
|
131
|
-
function createBufferedSignalSession(sendRemote) {
|
|
132
|
-
/** @type {Set<(message: unknown) => void>} */
|
|
133
|
-
const handlers = new Set()
|
|
134
|
-
/** @type {unknown[]} */
|
|
135
|
-
const backlog = []
|
|
136
|
-
return {
|
|
137
|
-
/**
|
|
138
|
-
* 发送信令消息到远端。
|
|
139
|
-
* @param {unknown} message 信令消息
|
|
140
|
-
* @returns {Promise<void>}
|
|
141
|
-
*/
|
|
142
|
-
async send(message) {
|
|
143
|
-
await sendRemote(message)
|
|
144
|
-
},
|
|
145
|
-
/**
|
|
146
|
-
* 注册远端信令 handler(含 backlog 回放)。
|
|
147
|
-
* @param {(message: unknown) => void} handler 入站回调
|
|
148
|
-
* @returns {() => void} 取消订阅函数
|
|
149
|
-
*/
|
|
150
|
-
onRemote(handler) {
|
|
151
|
-
handlers.add(handler)
|
|
152
|
-
for (const pending of backlog.splice(0))
|
|
153
|
-
handler(pending)
|
|
154
|
-
return () => handlers.delete(handler)
|
|
155
|
-
},
|
|
156
|
-
/**
|
|
157
|
-
* 投递信令消息;无 handler 时入 backlog。
|
|
158
|
-
* @param {unknown} message 信令消息
|
|
159
|
-
* @returns {void}
|
|
160
|
-
*/
|
|
161
|
-
deliver(message) {
|
|
162
|
-
if (!handlers.size) {
|
|
163
|
-
backlog.push(message)
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
for (const handler of handlers)
|
|
167
|
-
handler(message)
|
|
168
|
-
},
|
|
169
|
-
/**
|
|
170
|
-
* 清空 backlog 与 handler。
|
|
171
|
-
* @returns {void}
|
|
172
|
-
*/
|
|
173
|
-
clear() {
|
|
174
|
-
backlog.length = 0
|
|
175
|
-
handlers.clear()
|
|
176
|
-
},
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
42
|
/**
|
|
181
43
|
* 向 Map<key, Set<listener>> 订阅并返回取消函数。
|
|
182
44
|
* @param {Map<string, Set<Function>>} buckets 监听器桶
|
|
@@ -209,14 +71,12 @@ export function createLinkRegistry(options = {}) {
|
|
|
209
71
|
const localIdentity = resolveLocalIdentity(options.localIdentity)
|
|
210
72
|
const iceServers = options.iceServers?.length ? options.iceServers : DEFAULT_ICE_SERVERS
|
|
211
73
|
const maxActive = Math.max(4, Number(options.maxActive) || 32)
|
|
212
|
-
const autoRegisterDiscoveryProviders = options.autoRegisterDiscoveryProviders !== false
|
|
213
|
-
const autoRegisterLinkProviders = options.autoRegisterLinkProviders !== false
|
|
214
74
|
const selfTopic = nodeRendezvousTopic(localIdentity.nodeHash)
|
|
215
75
|
/** @type {Map<string, object>} */
|
|
216
76
|
const links = new Map()
|
|
217
|
-
/** @type {Map<string, Promise<object | null>>}
|
|
77
|
+
/** @type {Map<string, Promise<object | null>>} */
|
|
218
78
|
const inflights = new Map()
|
|
219
|
-
/** @type {Map<string, ReturnType<typeof createBufferedSignalSession>>}
|
|
79
|
+
/** @type {Map<string, ReturnType<typeof import('./offer_answer.mjs').createBufferedSignalSession>>} */
|
|
220
80
|
const signalSessions = new Map()
|
|
221
81
|
/** @type {Map<string, Set<string>>} */
|
|
222
82
|
const scopeInterests = new Map()
|
|
@@ -229,174 +89,19 @@ export function createLinkRegistry(options = {}) {
|
|
|
229
89
|
/** @type {Set<(nodeHash: string, reason: string) => void>} */
|
|
230
90
|
const linkDownListeners = new Set()
|
|
231
91
|
const recentAdverts = createLruMap(1024)
|
|
232
|
-
let runtimeStarted = false
|
|
233
|
-
let stopAdvert = null
|
|
234
|
-
let stopSignalListener = null
|
|
235
|
-
/** @type {Array<() => void>} */
|
|
236
|
-
const stopLinkListeners = []
|
|
237
92
|
let overlayRouter = null
|
|
238
|
-
/** @type {ReturnType<typeof createLanTcpLinkProvider> | null} 本 registry 持有的 lan_tcp 实例 */
|
|
239
|
-
let ownedLanTcp = null
|
|
240
|
-
/** @type {ReturnType<typeof createBleGattLinkProvider> | null} 本 registry 持有的 ble_gatt 实例 */
|
|
241
|
-
let ownedBleGatt = null
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* 自动注册默认 LinkProvider(lan_tcp / webrtc / ble_gatt)。
|
|
245
|
-
* lan_tcp / ble_gatt 每 registry 一个实例(避免 ensureListening 覆盖他人的 localIdentity);
|
|
246
|
-
* webrtc 按 id 单例注册。拨号时再经 isAvailable / canReach 跳过。
|
|
247
|
-
* @returns {Promise<void>}
|
|
248
|
-
*/
|
|
249
|
-
async function ensureLinkProviders() {
|
|
250
|
-
if (!autoRegisterLinkProviders) return
|
|
251
|
-
if (!ownedLanTcp) {
|
|
252
|
-
ownedLanTcp = createLanTcpLinkProvider()
|
|
253
|
-
registerLinkProvider(ownedLanTcp)
|
|
254
|
-
}
|
|
255
|
-
if (!ownedBleGatt) {
|
|
256
|
-
ownedBleGatt = createBleGattLinkProvider()
|
|
257
|
-
registerLinkProvider(ownedBleGatt)
|
|
258
|
-
}
|
|
259
|
-
const ids = new Set(listLinkProviders().map(provider => provider.id))
|
|
260
|
-
if (!ids.has('webrtc'))
|
|
261
|
-
registerLinkProvider(createWebRtcLinkProvider())
|
|
262
|
-
}
|
|
263
93
|
|
|
264
|
-
/**
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
function resolveLanTcpPort() {
|
|
269
|
-
const endpoint = typeof ownedLanTcp?.localEndpoint === 'function' ? ownedLanTcp.localEndpoint() : null
|
|
270
|
-
return normalizeTcpPort(endpoint?.port)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* 构造带本机身份与(若已 listen)tcpPort 的签名 advert。
|
|
275
|
-
* node / group / scoped 广播共用,保证 LAN hint 可从任意 topic 学到。
|
|
276
|
-
* @param {string} topic 广播主题
|
|
277
|
-
* @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string, tcpPort?: number }>} 签名 advert
|
|
278
|
-
*/
|
|
279
|
-
async function buildLocalAdvert(topic) {
|
|
280
|
-
const tcpPort = resolveLanTcpPort()
|
|
281
|
-
return await buildSignedAdvert(topic, Date.now(), {
|
|
282
|
-
...localIdentity,
|
|
283
|
-
...tcpPort != null ? { tcpPort } : {},
|
|
284
|
-
})
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* 启动 discovery + link provider listening。
|
|
289
|
-
* 先 ensureListening(拿到 lan_tcp 端口),再广播带 tcpPort 的 advert。
|
|
290
|
-
* @returns {Promise<void>}
|
|
291
|
-
*/
|
|
292
|
-
async function ensureDiscoveryRuntime() {
|
|
293
|
-
if (runtimeStarted) return
|
|
294
|
-
runtimeStarted = true
|
|
295
|
-
await ensureLinkProviders()
|
|
296
|
-
if (autoRegisterDiscoveryProviders) {
|
|
297
|
-
const providerIds = new Set(listDiscoveryProviders().map(provider => provider.id))
|
|
298
|
-
if (!providerIds.has('mdns'))
|
|
299
|
-
registerDiscoveryProvider(createMdnsDiscoveryProvider())
|
|
300
|
-
if (!providerIds.has('bt')) {
|
|
301
|
-
const bt = await import('../discovery/bt/index.mjs').catch(() => null)
|
|
302
|
-
if (await bt?.canUseBluetoothDiscovery?.())
|
|
303
|
-
registerDiscoveryProvider(bt.createBluetoothDiscoveryProvider())
|
|
304
|
-
}
|
|
305
|
-
if (!providerIds.has('nostr'))
|
|
306
|
-
// 测试通过 initNode({ signaling: { relayOverride } }) 注入共享 loopback relay;生产则回落到用户 relay + 默认公网 relay。
|
|
307
|
-
// 新 discovery 栈也必须尊重这层 runtime override,否则 live 双节点测试会各打各的公网 relay。
|
|
308
|
-
registerDiscoveryProvider(createNostrDiscoveryProvider({
|
|
309
|
-
relayUrls: getSignalingRuntimeConfig().relayOverride
|
|
310
|
-
?? mergeSignalingRelayUrls(getNodeTransportSettings().relayUrls),
|
|
311
|
-
}))
|
|
312
|
-
}
|
|
313
|
-
// 只对本 registry 持有的 lan_tcp / ble_gatt 调用 ensureListening。
|
|
314
|
-
// 切勿遍历其它 registry 的 lan_tcp:* / ble_gatt:* ——会覆盖其 localIdentity/onInbound。
|
|
315
|
-
/** @type {import('../link/providers/index.mjs').LinkProvider[]} */
|
|
316
|
-
const listenProviders = []
|
|
317
|
-
if (ownedLanTcp) listenProviders.push(ownedLanTcp)
|
|
318
|
-
if (ownedBleGatt) listenProviders.push(ownedBleGatt)
|
|
319
|
-
for (const provider of await listAvailableLinkProviders()) {
|
|
320
|
-
const id = String(provider.id)
|
|
321
|
-
if (id.startsWith('lan_tcp') || id.startsWith('ble_gatt')) continue
|
|
322
|
-
if (typeof provider.ensureListening === 'function')
|
|
323
|
-
listenProviders.push(provider)
|
|
324
|
-
}
|
|
325
|
-
for (const provider of listenProviders)
|
|
326
|
-
try {
|
|
327
|
-
const stop = await provider.ensureListening({
|
|
328
|
-
localIdentity,
|
|
329
|
-
/**
|
|
330
|
-
* @param {object} link 入站链路
|
|
331
|
-
* @returns {void}
|
|
332
|
-
*/
|
|
333
|
-
onInbound(link) {
|
|
334
|
-
void (async () => {
|
|
335
|
-
try {
|
|
336
|
-
await link.ready
|
|
337
|
-
const remote = normalizeHex64(link.nodeHash)
|
|
338
|
-
if (!remote) {
|
|
339
|
-
await link.close('inbound-no-nodehash')
|
|
340
|
-
return
|
|
341
|
-
}
|
|
342
|
-
await registerResolvedLink(remote, link)
|
|
343
|
-
}
|
|
344
|
-
catch { /* ignore failed inbound */ }
|
|
345
|
-
})()
|
|
346
|
-
},
|
|
347
|
-
})
|
|
348
|
-
if (typeof stop === 'function') stopLinkListeners.push(stop)
|
|
349
|
-
}
|
|
350
|
-
catch { /* provider listen unavailable — normal degrade */ }
|
|
351
|
-
|
|
352
|
-
if (listDiscoveryProviders().length) {
|
|
353
|
-
stopSignalListener = await listenSignals(selfTopic, bytes => {
|
|
354
|
-
void handleIncomingSignal(bytes).catch(() => { })
|
|
355
|
-
})
|
|
356
|
-
stopAdvert = await advertiseTopic(selfTopic, encryptSignalPacket(selfTopic, {
|
|
357
|
-
type: 'advert',
|
|
358
|
-
body: await buildLocalAdvert(selfTopic),
|
|
359
|
-
}))
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* 为单条 PC 创建按 connId 标记的信令会话。出站信令都带上 connId,
|
|
365
|
-
* 使同一对节点的两个方向(各自一条 PC)在信令层互不串扰。
|
|
366
|
-
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
367
|
-
* @param {string} connId 连接标识
|
|
368
|
-
* @returns {ReturnType<typeof createBufferedSignalSession>} 信令会话
|
|
369
|
-
*/
|
|
370
|
-
function createConnSession(remoteNodeHash, connId) {
|
|
371
|
-
const normalized = normalizeHex64(remoteNodeHash)
|
|
372
|
-
const topic = nodeRendezvousTopic(normalized)
|
|
373
|
-
return createBufferedSignalSession(async message => {
|
|
374
|
-
await sendSignal(topic, normalized, encryptSignalPacket(topic, {
|
|
375
|
-
type: 'signal',
|
|
376
|
-
from: localIdentity.nodeHash,
|
|
377
|
-
connId,
|
|
378
|
-
body: message,
|
|
379
|
-
}))
|
|
380
|
-
})
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* 判断信令 body 是否为发起方 offer(据此决定入站是否新建应答 PC)。
|
|
385
|
-
* @param {unknown} body 信令 body
|
|
386
|
-
* @returns {boolean} 是 offer 则 true
|
|
387
|
-
*/
|
|
388
|
-
function isOfferSignalBody(body) {
|
|
389
|
-
return !!body && typeof body === 'object'
|
|
390
|
-
&& body.type === 'description'
|
|
391
|
-
&& body.description?.type === 'offer'
|
|
392
|
-
}
|
|
94
|
+
/** @type {(bytes: Uint8Array) => Promise<void>} */
|
|
95
|
+
let handleIncomingSignal = async () => { }
|
|
96
|
+
/** @type {(provider: import('../link/providers/index.mjs').LinkProvider, remoteNodeHash: string) => Promise<object | null>} */
|
|
97
|
+
let dialOfferAnswer = async () => null
|
|
393
98
|
|
|
394
99
|
/**
|
|
395
100
|
* 择链:更高 level 优先;同 level 时保留由较小 nodeHash 发起的那条(glare 两端一致)。
|
|
396
|
-
* @param {object} link
|
|
101
|
+
* @param {object} link 候选链路
|
|
397
102
|
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
398
|
-
* @param {object | null} [against]
|
|
399
|
-
* @returns {boolean}
|
|
103
|
+
* @param {object | null} [against] 当前规范链
|
|
104
|
+
* @returns {boolean} 候选链应保留为规范链时 true
|
|
400
105
|
*/
|
|
401
106
|
function linkIsPreferred(link, remoteNodeHash, against = null) {
|
|
402
107
|
const level = Number(link.level) || 0
|
|
@@ -417,7 +122,6 @@ export function createLinkRegistry(options = {}) {
|
|
|
417
122
|
void dispatchEnvelope(senderNodeHash, envelope, link).catch(() => { })
|
|
418
123
|
})
|
|
419
124
|
link.onDown(reason => {
|
|
420
|
-
// 只有关闭的是当前规范链路才对外报 linkDown;双 PC 并存期关掉的败者不应误发 peer-leave。
|
|
421
125
|
const wasCanonical = links.get(remoteNodeHash) === link
|
|
422
126
|
if (!wasCanonical) return
|
|
423
127
|
links.delete(remoteNodeHash)
|
|
@@ -448,67 +152,33 @@ export function createLinkRegistry(options = {}) {
|
|
|
448
152
|
}
|
|
449
153
|
|
|
450
154
|
/**
|
|
451
|
-
*
|
|
452
|
-
* @
|
|
453
|
-
|
|
454
|
-
function findOfferAnswerProvider() {
|
|
455
|
-
return listLinkProviders().find(provider => provider.caps?.needsOfferAnswer) ?? null
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Offer/answer provider:为一条 connId 会话建链并走 glare 择一。
|
|
460
|
-
* @param {{ provider: import('../link/providers/index.mjs').LinkProvider, remoteNodeHash: string, connId: string, session: ReturnType<typeof createBufferedSignalSession>, initiator: boolean }} options 建链参数
|
|
461
|
-
* @returns {Promise<object | null>} 当前规范链;失败 null
|
|
155
|
+
* 入站链路共用 onInbound(lan_tcp / 后台 ble 等)。
|
|
156
|
+
* @param {object} link 入站链路
|
|
157
|
+
* @returns {void}
|
|
462
158
|
*/
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
nodeHash
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
signalSessions.delete(connId)
|
|
474
|
-
return null
|
|
159
|
+
function onInboundLink(link) {
|
|
160
|
+
void (async () => {
|
|
161
|
+
try {
|
|
162
|
+
await link.ready
|
|
163
|
+
const remote = normalizeHex64(link.nodeHash)
|
|
164
|
+
if (!remote) {
|
|
165
|
+
await link.close('inbound-no-nodehash')
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
await registerResolvedLink(remote, link)
|
|
475
169
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
await registerResolvedLink(remoteNodeHash, link)
|
|
479
|
-
return links.get(remoteNodeHash) ?? null
|
|
480
|
-
}
|
|
481
|
-
catch {
|
|
482
|
-
signalSessions.delete(connId)
|
|
483
|
-
return null
|
|
484
|
-
}
|
|
170
|
+
catch { /* ignore failed inbound */ }
|
|
171
|
+
})()
|
|
485
172
|
}
|
|
486
173
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const remoteNodeHash = normalizeHex64(packet.from)
|
|
496
|
-
const connId = String(packet.connId || '')
|
|
497
|
-
if (!remoteNodeHash || remoteNodeHash === localIdentity.nodeHash || !connId) return
|
|
498
|
-
// 仅 needsOfferAnswer 走双 PC glare;其它 provider 不吃 SDP 信令。
|
|
499
|
-
let session = signalSessions.get(connId)
|
|
500
|
-
if (!session) {
|
|
501
|
-
// 只有全新的 offer 才开一条独立应答 PC;answer/ice 若无对应 connId 会话则是迟到/无效帧,丢弃。
|
|
502
|
-
// 应答 PC 按 connId 独立创建,不受按 nodeHash 去重的 inflight 阻挡——这是支持双向同时建链的关键。
|
|
503
|
-
if (!isOfferSignalBody(packet.body)) return
|
|
504
|
-
const provider = findOfferAnswerProvider()
|
|
505
|
-
if (!provider) return
|
|
506
|
-
session = createConnSession(remoteNodeHash, connId)
|
|
507
|
-
signalSessions.set(connId, session)
|
|
508
|
-
void buildConnLink({ provider, remoteNodeHash, connId, session, initiator: false })
|
|
509
|
-
}
|
|
510
|
-
session.deliver(packet.body)
|
|
511
|
-
}
|
|
174
|
+
const bootstrap = createRuntimeBootstrap({
|
|
175
|
+
localIdentity,
|
|
176
|
+
selfTopic,
|
|
177
|
+
autoRegisterDiscoveryProviders: options.autoRegisterDiscoveryProviders !== false,
|
|
178
|
+
autoRegisterLinkProviders: options.autoRegisterLinkProviders !== false,
|
|
179
|
+
onInboundLink,
|
|
180
|
+
handleIncomingSignal: bytes => handleIncomingSignal(bytes),
|
|
181
|
+
})
|
|
512
182
|
|
|
513
183
|
/**
|
|
514
184
|
* 计算节点在 scope 兴趣中的权重(用于 eviction)。
|
|
@@ -534,18 +204,20 @@ export function createLinkRegistry(options = {}) {
|
|
|
534
204
|
if (victim) await victim[1].close('budget-evict')
|
|
535
205
|
}
|
|
536
206
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
207
|
+
;({ handleIncomingSignal, dialOfferAnswer } = createOfferAnswerDial({
|
|
208
|
+
localIdentity,
|
|
209
|
+
iceServers,
|
|
210
|
+
selfTopic,
|
|
211
|
+
signalSessions,
|
|
212
|
+
registerResolvedLink,
|
|
213
|
+
trimToBudget,
|
|
214
|
+
/**
|
|
215
|
+
* 读取当前规范链。
|
|
216
|
+
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
217
|
+
* @returns {object | null | undefined} 规范链实例
|
|
218
|
+
*/
|
|
219
|
+
getCanonicalLink: remoteNodeHash => links.get(normalizeHex64(remoteNodeHash)),
|
|
220
|
+
}))
|
|
549
221
|
|
|
550
222
|
/**
|
|
551
223
|
* 经非 offer/answer provider 拨号。
|
|
@@ -572,21 +244,25 @@ export function createLinkRegistry(options = {}) {
|
|
|
572
244
|
* @returns {Promise<object | null>} 链路实例;失败时 null
|
|
573
245
|
*/
|
|
574
246
|
async function ensureDirectLinkToNode(remoteNodeHash) {
|
|
575
|
-
await
|
|
247
|
+
await bootstrap.ensureRuntime()
|
|
248
|
+
await bootstrap.whenSignalListening()
|
|
576
249
|
const normalized = normalizeHex64(remoteNodeHash)
|
|
577
250
|
if (!normalized || normalized === localIdentity.nodeHash) return null
|
|
578
251
|
if (links.has(normalized)) return links.get(normalized)
|
|
579
252
|
if (inflights.has(normalized)) return await inflights.get(normalized)
|
|
580
253
|
const task = (async () => {
|
|
581
|
-
const providers =
|
|
582
|
-
for (const provider of providers)
|
|
254
|
+
const providers = listLinkProviders()
|
|
255
|
+
for (const provider of providers)
|
|
583
256
|
try {
|
|
584
257
|
if (typeof provider.canReach === 'function') {
|
|
585
258
|
const reachable = await Promise.resolve(provider.canReach({ nodeHash: normalized }))
|
|
586
259
|
if (!reachable) continue
|
|
587
260
|
}
|
|
261
|
+
if (typeof provider.isAvailable === 'function') {
|
|
262
|
+
const available = await Promise.resolve(provider.isAvailable())
|
|
263
|
+
if (!available) continue
|
|
264
|
+
}
|
|
588
265
|
if (provider.caps?.needsOfferAnswer) {
|
|
589
|
-
// soft-fail 返回 null(不 throw);必须 continue 才能落到更低 level。
|
|
590
266
|
const link = await dialOfferAnswer(provider, normalized)
|
|
591
267
|
if (link) return link
|
|
592
268
|
continue
|
|
@@ -595,7 +271,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
595
271
|
if (link) return link
|
|
596
272
|
}
|
|
597
273
|
catch { /* dial failed — try next provider */ }
|
|
598
|
-
|
|
274
|
+
|
|
599
275
|
return null
|
|
600
276
|
})().finally(() => inflights.delete(normalized))
|
|
601
277
|
inflights.set(normalized, task)
|
|
@@ -630,10 +306,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
630
306
|
overlayRouter = createOverlayRouter({
|
|
631
307
|
localIdentity,
|
|
632
308
|
sendToNodeLink: sendDirectToNodeLink,
|
|
633
|
-
/**
|
|
634
|
-
* 列出当前所有活跃链路。
|
|
635
|
-
* @returns {Array<{ nodeHash: string, link: object }>} 链路列表
|
|
636
|
-
*/
|
|
309
|
+
/** @returns {Array<{ nodeHash: string, link: object }>} 当前活跃链路列表 */
|
|
637
310
|
listLinks() {
|
|
638
311
|
return [...links.entries()].map(([nodeHash, link]) => ({ nodeHash, link }))
|
|
639
312
|
},
|
|
@@ -663,15 +336,6 @@ export function createLinkRegistry(options = {}) {
|
|
|
663
336
|
}
|
|
664
337
|
}
|
|
665
338
|
|
|
666
|
-
/**
|
|
667
|
-
* 确保到远端节点的链路(直连或被动应答)。
|
|
668
|
-
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
669
|
-
* @returns {Promise<object | null>} 链路实例
|
|
670
|
-
*/
|
|
671
|
-
async function ensureLinkToNode(remoteNodeHash) {
|
|
672
|
-
return await ensureDirectLinkToNode(remoteNodeHash)
|
|
673
|
-
}
|
|
674
|
-
|
|
675
339
|
/**
|
|
676
340
|
* 发送 envelope:优先直连,失败则 overlay relay。
|
|
677
341
|
* @param {string} remoteNodeHash 远端节点 64 hex
|
|
@@ -706,7 +370,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
706
370
|
/**
|
|
707
371
|
* 订阅指定 scope 前缀的 envelope。
|
|
708
372
|
* @param {string} prefix scope 前缀
|
|
709
|
-
* @param {
|
|
373
|
+
* @param {Function} listener 监听器
|
|
710
374
|
* @returns {() => void} 取消订阅函数
|
|
711
375
|
*/
|
|
712
376
|
function subscribeScope(prefix, listener) {
|
|
@@ -716,7 +380,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
716
380
|
/**
|
|
717
381
|
* 注册 scope 前缀的 authorizer(入站校验)。
|
|
718
382
|
* @param {string} prefix scope 前缀
|
|
719
|
-
* @param {
|
|
383
|
+
* @param {Function} authorizer 校验函数
|
|
720
384
|
* @returns {() => void} 取消注册函数
|
|
721
385
|
*/
|
|
722
386
|
function registerScopeAuthorizer(prefix, authorizer) {
|
|
@@ -726,10 +390,11 @@ export function createLinkRegistry(options = {}) {
|
|
|
726
390
|
|
|
727
391
|
return {
|
|
728
392
|
localIdentity,
|
|
729
|
-
buildLocalAdvert,
|
|
730
|
-
lanTcpPort:
|
|
731
|
-
|
|
732
|
-
|
|
393
|
+
buildLocalAdvert: bootstrap.buildLocalAdvert,
|
|
394
|
+
lanTcpPort: bootstrap.lanTcpPort,
|
|
395
|
+
whenListening: bootstrap.whenListening,
|
|
396
|
+
ensureRuntime: bootstrap.ensureRuntime,
|
|
397
|
+
ensureLinkToNode: ensureDirectLinkToNode,
|
|
733
398
|
/**
|
|
734
399
|
* 获取到指定节点的活跃链路。
|
|
735
400
|
* @param {string} nodeHash 节点 64 hex
|
|
@@ -820,10 +485,7 @@ export function createLinkRegistry(options = {}) {
|
|
|
820
485
|
* @returns {Promise<void>}
|
|
821
486
|
*/
|
|
822
487
|
async shutdown() {
|
|
823
|
-
|
|
824
|
-
stopSignalListener?.()
|
|
825
|
-
for (const stop of stopLinkListeners.splice(0))
|
|
826
|
-
try { stop() } catch { /* ignore */ }
|
|
488
|
+
await bootstrap.shutdown()
|
|
827
489
|
overlayRouter?.close()
|
|
828
490
|
overlayRouter = null
|
|
829
491
|
for (const link of links.values())
|
|
@@ -832,15 +494,6 @@ export function createLinkRegistry(options = {}) {
|
|
|
832
494
|
inflights.clear()
|
|
833
495
|
for (const session of signalSessions.values()) session.clear()
|
|
834
496
|
signalSessions.clear()
|
|
835
|
-
if (ownedLanTcp) {
|
|
836
|
-
unregisterLinkProvider(ownedLanTcp.id)
|
|
837
|
-
ownedLanTcp = null
|
|
838
|
-
}
|
|
839
|
-
if (ownedBleGatt) {
|
|
840
|
-
unregisterLinkProvider(ownedBleGatt.id)
|
|
841
|
-
ownedBleGatt = null
|
|
842
|
-
}
|
|
843
|
-
runtimeStarted = false
|
|
844
497
|
},
|
|
845
498
|
}
|
|
846
499
|
}
|
|
@@ -852,8 +505,7 @@ let defaultRegistry = null
|
|
|
852
505
|
* @returns {ReturnType<typeof createLinkRegistry>} 默认 registry
|
|
853
506
|
*/
|
|
854
507
|
export function getLinkRegistry() {
|
|
855
|
-
|
|
856
|
-
return defaultRegistry
|
|
508
|
+
return defaultRegistry ||= createLinkRegistry()
|
|
857
509
|
}
|
|
858
510
|
|
|
859
511
|
/**
|