@steve02081504/fount-p2p 0.0.5 → 0.0.7

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 (47) hide show
  1. package/core/tcp_port.mjs +11 -0
  2. package/dag/canonicalize_row.mjs +5 -5
  3. package/discovery/advert_peer_hints.mjs +21 -0
  4. package/discovery/{bt.mjs → bt/index.mjs} +151 -55
  5. package/discovery/bt/peer_hints.mjs +41 -0
  6. package/discovery/bt/runtime.mjs +46 -0
  7. package/discovery/lan_peer_hints.mjs +43 -0
  8. package/discovery/mdns.mjs +10 -6
  9. package/discovery/nostr.mjs +3 -3
  10. package/federation/chunk_fetch_pending.mjs +3 -3
  11. package/federation/chunk_fetch_scheduler.mjs +3 -3
  12. package/federation/dag_order_cache.mjs +3 -3
  13. package/federation/topo_order_memo.mjs +3 -3
  14. package/files/chunk_responder.mjs +1 -1
  15. package/files/evfs.mjs +26 -26
  16. package/files/transfer_key.mjs +9 -9
  17. package/files/transfer_key_registry.mjs +9 -9
  18. package/governance/join_pow.mjs +17 -17
  19. package/index.mjs +3 -2
  20. package/link/channel_mux.mjs +7 -7
  21. package/link/frame.mjs +5 -5
  22. package/link/handshake.mjs +63 -36
  23. package/link/pipe.mjs +404 -0
  24. package/link/providers/ble_gatt.mjs +339 -0
  25. package/link/providers/index.mjs +90 -0
  26. package/link/providers/lan_tcp.mjs +342 -0
  27. package/link/providers/levels.mjs +9 -0
  28. package/link/providers/webrtc.mjs +385 -0
  29. package/mailbox/deliver_or_store.mjs +13 -13
  30. package/mailbox/wire.mjs +4 -4
  31. package/node/reputation_store.mjs +5 -5
  32. package/node/retention_policy.mjs +8 -8
  33. package/overlay/index.mjs +6 -6
  34. package/package.json +3 -2
  35. package/registries/inbound.mjs +8 -8
  36. package/rooms/scoped_link.mjs +23 -21
  37. package/timeline/append_core.mjs +3 -3
  38. package/transport/group_link_set.mjs +35 -33
  39. package/transport/link_registry.mjs +250 -63
  40. package/transport/peer_pool.mjs +4 -4
  41. package/transport/user_room.mjs +12 -14
  42. package/utils/ttl_map.mjs +41 -0
  43. package/wire/group_part.mjs +3 -3
  44. package/wire/part_ingress.mjs +14 -14
  45. package/wire/part_query.mjs +81 -82
  46. package/wire/part_query_cache.mjs +7 -0
  47. package/link/link.mjs +0 -617
package/link/link.mjs DELETED
@@ -1,617 +0,0 @@
1
- import { normalizeHex64 } from '../core/hexIds.mjs'
2
- import { getSignalingRuntimeConfig } from '../node/instance.mjs'
3
- import { ms } from '../utils/duration.mjs'
4
- import { createLruMap } from '../utils/lru.mjs'
5
-
6
- import {
7
- CHANNEL_BULK,
8
- CHANNEL_CONTROL,
9
- CHANNEL_LOW_THRESHOLD_BYTES,
10
- configureBufferedAmountLowThreshold,
11
- createChannelSendQueues,
12
- onBufferedAmountLow,
13
- } from './channel_mux.mjs'
14
- import { createReassembler, encodeFrames, randomMsgIdHex } from './frame.mjs'
15
- import { buildAuth, buildHello, parseHello, verifyAuth } from './handshake.mjs'
16
- import { attachDataChannelListener, attachIceCandidateListener, loadNodeRtcPolyfill, signalDataToBytes, waitForChannelState } from './rtc.mjs'
17
- import { extractDtlsFingerprint } from './sdp_fingerprint.mjs'
18
-
19
- const encoder = new TextEncoder()
20
- const decoder = new TextDecoder()
21
-
22
- /**
23
- * 尝试将 channel 消息数据转为 UTF-8 字符串。
24
- * @param {unknown} data 原始消息数据
25
- * @returns {string | null} 解码后的字符串,无法解码时返回 null
26
- */
27
- function coerceString(data) {
28
- if (typeof data === 'string') return data
29
- if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || data instanceof Uint8Array)
30
- try { return decoder.decode(signalDataToBytes(data)) }
31
- catch { return null }
32
-
33
- return null
34
- }
35
-
36
- /**
37
- * 绑定 data channel message 回调,兼容 addEventListener/onmessage/onMessage。
38
- * @param {RTCDataChannel} channel RTC 数据通道
39
- * @param {(data: unknown) => void} handler 消息处理器
40
- * @returns {void}
41
- */
42
- function attachChannelMessageListener(channel, handler) {
43
- channel.addEventListener?.('message', event => handler(event?.data))
44
- /**
45
- * onmessage 回退处理器。
46
- * @param {MessageEvent} event 消息事件
47
- * @returns {void}
48
- */
49
- channel.onmessage = event => handler(event?.data)
50
- channel.onMessage?.subscribe(message => handler(message))
51
- }
52
-
53
- /**
54
- * 依次调用监听器集合,忽略单个 listener 抛错。
55
- * @param {Set<Function>} listeners 监听器集合
56
- * @param {...unknown} args 传递给 listener 的参数
57
- * @returns {void}
58
- */
59
- function emitListeners(listeners, ...args) {
60
- for (const listener of listeners)
61
- try { listener(...args) }
62
- catch { /* ignore */ }
63
- }
64
-
65
- /**
66
- * 将错误对象格式化为短字符串,用于 close reason。
67
- * @param {unknown} error 原始错误
68
- * @returns {string} 最多 240 字符的单行 reason
69
- */
70
- function formatErrorReason(error) {
71
- return String(error?.message ?? error ?? 'unknown-error').replace(/\s+/g, ' ').slice(0, 240)
72
- }
73
-
74
- /**
75
- * 建立 P2P link:WebRTC 双通道、握手、分帧收发与心跳。
76
- * @param {object} opts link 配置
77
- * @param {string | null} [opts.nodeHash] 期望的对端 nodeHash,省略则不校验
78
- * @param {boolean} opts.initiator 是否为连接发起方
79
- * @param {{ send: (message: unknown) => void | Promise<void>, onRemote: (handler: (message: unknown) => void) => (() => void) | void }} opts.signal 信令收发接口
80
- * @param {RTCConfiguration['iceServers']} [opts.iceServers] ICE 服务器列表
81
- * @param {number} [opts.heartbeatMs] 心跳间隔(毫秒)
82
- * @param {number} [opts.idleTimeoutMs] 无入站流量超时(毫秒)
83
- * @param {number} [opts.handshakeTimeoutMs] 握手超时(毫秒)
84
- * @param {{ RTCPeerConnection: typeof RTCPeerConnection } | null} [opts.rtc] RTC 构造器,省略则加载 polyfill
85
- * @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array, nonce?: string } | null} [opts.localIdentity] 本地握手身份
86
- * @returns {Promise<{ ready: Promise<void>, get nodeHash(): string | null, send: (envelope: { scope: string, action: string, payload: unknown }) => Promise<boolean>, onEnvelope: (cb: (envelope: { scope: string, action: string, payload: unknown }, remoteNodeHash: string) => void) => () => void, onDown: (cb: (reason: string) => void) => () => void, close: (reason?: string) => Promise<void>, stats: () => object }>} link 句柄
87
- */
88
- export async function createLink(opts) {
89
- const heartbeatMs = Number(opts.heartbeatMs) || ms('15s')
90
- const idleTimeoutMs = Number(opts.idleTimeoutMs) || ms('45s')
91
- const handshakeTimeoutMs = Number(opts.handshakeTimeoutMs) || ms('10s')
92
- const channelOpenTimeoutMs = Math.max(handshakeTimeoutMs, ms('30s'))
93
- const trickleIceOff = getSignalingRuntimeConfig().trickleIceOff === true
94
- const rtc = opts.rtc ?? await loadNodeRtcPolyfill()
95
- const pc = new rtc.RTCPeerConnection(opts.iceServers?.length ? { iceServers: opts.iceServers } : undefined)
96
- const targetNodeHash = normalizeHex64(opts.nodeHash || '')
97
- const remoteSignalQueue = []
98
- const seenRemoteSignals = createLruMap(1024)
99
- let remoteDescriptionSet = false
100
- let closed = false
101
- let ready = false
102
- let closeReason = 'closed'
103
- let remoteNodeHash = targetNodeHash || null
104
- let remoteHello = null
105
- let localHello = null
106
- let controlChannel = null
107
- let bulkChannel = null
108
- let handshakeTimer = null
109
- let idleTimer = null
110
- let heartbeatTimer = null
111
- let unlistenRemote = null
112
- let helloSent = false
113
- let authSent = false
114
- let remoteAuthVerified = false
115
- /** @type {object | null} 早于 remoteHello 到达的 auth(发送方在发出自身 hello 前即回 auth 时会乱序),暂存待 hello 到达后校验。 */
116
- let pendingAuth = null
117
- let lastInboundAt = Date.now()
118
- let lastOutboundAt = 0
119
- let sentFrames = 0
120
- let recvFrames = 0
121
- let reconnectCount = 0
122
- let controlLowEvents = 0
123
- let bulkLowEvents = 0
124
- const envelopeListeners = new Set()
125
- const downListeners = new Set()
126
- const completedMsgIds = createLruMap(4096)
127
- const reassembler = createReassembler()
128
- let sendQueues = null
129
- /** @type {(value: void | PromiseLike<void>) => void} */
130
- let resolveReady
131
- /** @type {(reason?: unknown) => void} */
132
- let rejectReady
133
- const readyPromise = new Promise((resolve, reject) => {
134
- resolveReady = resolve
135
- rejectReady = reject
136
- })
137
- void readyPromise.catch(() => { })
138
-
139
- /**
140
- * 经信令通道发送消息。
141
- * @param {unknown} message 信令载荷
142
- * @returns {Promise<void>}
143
- */
144
- async function sendSignal(message) {
145
- await Promise.resolve(opts.signal.send(message))
146
- }
147
-
148
- /**
149
- * 从本地 SDP 提取 DTLS fingerprint。
150
- * @returns {string | null} 本地 fingerprint,未就绪时返回 null
151
- */
152
- function localFingerprint() {
153
- return extractDtlsFingerprint(pc.localDescription?.sdp || '')
154
- }
155
-
156
- /**
157
- * 从远端 SDP 提取 DTLS fingerprint。
158
- * @returns {string | null} 远端 fingerprint,未就绪时返回 null
159
- */
160
- function remoteFingerprint() {
161
- return extractDtlsFingerprint(pc.remoteDescription?.sdp || '')
162
- }
163
-
164
- /**
165
- * 为通道配置背压泵:低水位时 flush 发送队列。
166
- * @param {RTCDataChannel} channel RTC 数据通道
167
- * @returns {void}
168
- */
169
- function attachBackpressurePump(channel) {
170
- configureBufferedAmountLowThreshold(channel, CHANNEL_LOW_THRESHOLD_BYTES)
171
- onBufferedAmountLow(channel, () => {
172
- if (channel.label === CHANNEL_CONTROL) controlLowEvents++
173
- if (channel.label === CHANNEL_BULK) bulkLowEvents++
174
- if (channel.label === CHANNEL_CONTROL) sendQueues?.flush(CHANNEL_CONTROL)
175
- if (channel.label === CHANNEL_BULK) sendQueues?.flush(CHANNEL_BULK)
176
- })
177
- }
178
-
179
- /**
180
- * 收到远端 hello 后发送 auth 签名。
181
- * @returns {Promise<void>}
182
- */
183
- async function maybeSendAuth() {
184
- if (authSent || !remoteHello) return
185
- const fingerprint = localFingerprint()
186
- if (!fingerprint) return
187
- authSent = true
188
- await sendRawControl(await buildAuth(remoteHello.nonce, fingerprint, opts.localIdentity ?? {}))
189
- }
190
-
191
- /**
192
- * 握手完成后启动心跳/idle 检测并 resolve ready。
193
- * @returns {Promise<void>}
194
- */
195
- async function maybeFinishHandshake() {
196
- if (ready || !remoteHello || !remoteAuthVerified) return
197
- ready = true
198
- clearTimeout(handshakeTimer)
199
- heartbeatTimer = setInterval(() => {
200
- void send({ scope: 'link', action: 'ping', payload: {} }).catch(() => { })
201
- }, heartbeatMs)
202
- idleTimer = setInterval(() => {
203
- if (Date.now() - lastInboundAt > idleTimeoutMs)
204
- void close('idle-timeout')
205
- }, Math.max(1000, Math.floor(heartbeatMs / 3)))
206
- resolveReady()
207
- }
208
-
209
- /**
210
- * 绑定分帧入站:JSON 控制消息或二进制帧重组为 envelope。
211
- * @param {RTCDataChannel} channel RTC 数据通道
212
- * @returns {void}
213
- */
214
- function attachFramedIngress(channel) {
215
- attachChannelMessageListener(channel, data => {
216
- lastInboundAt = Date.now()
217
- const maybeText = coerceString(data)
218
- if (maybeText && maybeText.startsWith('{'))
219
- try {
220
- const parsed = JSON.parse(maybeText)
221
- void handleRawControlMessage(parsed)
222
- return
223
- }
224
- catch {
225
- /* fall through to binary path */
226
- }
227
-
228
- let bytes
229
- try {
230
- bytes = signalDataToBytes(data)
231
- }
232
- catch {
233
- return
234
- }
235
- try {
236
- recvFrames++
237
- const merged = reassembler.push(bytes)
238
- if (!merged) return
239
- const envelope = JSON.parse(decoder.decode(merged))
240
- const msgId = envelope?.msgId ? String(envelope.msgId) : null
241
- if (msgId && completedMsgIds.has(msgId)) return
242
- if (msgId) completedMsgIds.touch(msgId, true)
243
- if (envelope?.scope === 'link') {
244
- if (envelope.action === 'ping') {
245
- void send({ scope: 'link', action: 'pong', payload: {} }).catch(() => { })
246
- return
247
- }
248
- if (envelope.action === 'pong') return
249
- }
250
- if (ready && remoteNodeHash)
251
- emitListeners(envelopeListeners, envelope, remoteNodeHash)
252
- }
253
- catch {
254
- /* drop malformed network ingress */
255
- }
256
- })
257
- }
258
-
259
- /**
260
- * 验证远端 auth 并完成握手。
261
- * @param {{ sig: string }} auth 远端 auth 载荷
262
- * @returns {Promise<void>}
263
- */
264
- async function handleAuth(auth) {
265
- // auth 依赖 remoteHello 里的 nonce/pubKey 才能校验;对端可能在发出自身 hello 之前就先回了 auth
266
- // (initiator 收到我方 hello 即 maybeSendAuth,其 localDescription 早已就绪)。此时暂存,待 hello 到达补校验。
267
- if (!remoteHello) {
268
- pendingAuth = auth
269
- return
270
- }
271
- const fingerprint = remoteFingerprint()
272
- const verifiedNodeHash = await verifyAuth(remoteHello, auth, localHello?.nonce, fingerprint)
273
- if (!verifiedNodeHash) {
274
- await close(`auth-failed:fingerprint=${fingerprint || 'missing'} localNonce=${localHello?.nonce || 'missing'}`)
275
- return
276
- }
277
- if (targetNodeHash && verifiedNodeHash !== targetNodeHash) {
278
- await close('nodehash-mismatch')
279
- return
280
- }
281
- remoteNodeHash = verifiedNodeHash
282
- remoteAuthVerified = true
283
- await maybeFinishHandshake()
284
- }
285
-
286
- /**
287
- * 处理 control 通道上的 JSON hello/auth 消息。
288
- * @param {unknown} message 原始 JSON 对象
289
- * @returns {Promise<void>}
290
- */
291
- async function handleRawControlMessage(message) {
292
- if (!message || typeof message !== 'object') return
293
- if (message.type === 'hello') {
294
- const parsed = parseHello(message)
295
- if (!parsed) {
296
- await close('hello-invalid')
297
- return
298
- }
299
- remoteHello = parsed
300
- await maybeSendAuth()
301
- if (pendingAuth) {
302
- const bufferedAuth = pendingAuth
303
- pendingAuth = null
304
- await handleAuth(bufferedAuth)
305
- }
306
- return
307
- }
308
- if (message.type === 'auth')
309
- await handleAuth(message)
310
- }
311
-
312
- /**
313
- * 应用远端 SDP 并 flush 排队的 ICE candidate。
314
- * @param {RTCSessionDescriptionInit | RTCSessionDescription} description 远端会话描述
315
- * @returns {Promise<void>}
316
- */
317
- async function applyRemoteDescription(description) {
318
- await pc.setRemoteDescription(description)
319
- remoteDescriptionSet = true
320
- await flushQueuedIceCandidates()
321
- }
322
-
323
- /**
324
- * 判断 ICE 错误是否应重试排队 candidate。
325
- * @param {unknown} error addIceCandidate 抛出的错误
326
- * @returns {boolean} 应重新入队则 true
327
- */
328
- function shouldRetryQueuedIce(error) {
329
- return /without ice transport/i.test(String(error?.message ?? error ?? ''))
330
- }
331
-
332
- /**
333
- * trickle ICE 关闭时等待 ICE gathering 完成。
334
- * @returns {Promise<void>}
335
- */
336
- async function waitForIceGatheringComplete() {
337
- if (!trickleIceOff || pc.iceGatheringState === 'complete') return
338
- const deadline = Date.now() + handshakeTimeoutMs
339
- while (pc.iceGatheringState !== 'complete' && !closed && Date.now() < deadline)
340
- await new Promise(resolve => setTimeout(resolve, 50))
341
- }
342
-
343
- /**
344
- * 将排队的远端 ICE candidate 依次加入 PeerConnection。
345
- * @returns {Promise<void>}
346
- */
347
- async function flushQueuedIceCandidates() {
348
- if (!remoteDescriptionSet || !pc.localDescription) return
349
- while (remoteSignalQueue.length) {
350
- const candidate = remoteSignalQueue.shift()
351
- try {
352
- await pc.addIceCandidate(candidate)
353
- }
354
- catch (error) {
355
- if (shouldRetryQueuedIce(error)) {
356
- remoteSignalQueue.unshift(candidate)
357
- return
358
- }
359
- throw error
360
- }
361
- }
362
- }
363
-
364
- /**
365
- * 处理远端信令:SDP description 与 ICE candidate。
366
- * @param {unknown} message 信令消息对象
367
- * @returns {Promise<void>}
368
- */
369
- async function handleRemoteSignal(message) {
370
- if (closed || !message || typeof message !== 'object') return
371
- const signalKey = JSON.stringify(message)
372
- if (seenRemoteSignals.has(signalKey)) return
373
- seenRemoteSignals.touch(signalKey, true)
374
- if (message.type === 'description' && message.description) {
375
- if (message.description.type === 'answer' && pc.signalingState === 'stable') return
376
- await applyRemoteDescription(message.description)
377
- if (message.description.type === 'offer') {
378
- const answer = await pc.createAnswer()
379
- await pc.setLocalDescription(answer)
380
- await flushQueuedIceCandidates()
381
- await waitForIceGatheringComplete()
382
- await sendSignal({
383
- type: 'description',
384
- description: pc.localDescription?.toJSON?.() ?? pc.localDescription ?? answer,
385
- })
386
- await maybeSendAuth()
387
- }
388
- return
389
- }
390
- if (message.type === 'ice' && message.candidate) {
391
- if (!remoteDescriptionSet || !pc.localDescription || pc.signalingState !== 'stable') {
392
- remoteSignalQueue.push(message.candidate)
393
- return
394
- }
395
- try {
396
- await pc.addIceCandidate(message.candidate)
397
- }
398
- catch (error) {
399
- if (shouldRetryQueuedIce(error)) {
400
- remoteSignalQueue.push(message.candidate)
401
- return
402
- }
403
- throw error
404
- }
405
- }
406
- }
407
-
408
- /**
409
- * 绑定 data channel 并配置入站/背压。
410
- * @param {RTCDataChannel} channel RTC 数据通道
411
- * @returns {Promise<void>}
412
- */
413
- async function attachChannel(channel) {
414
- if (channel.label === CHANNEL_CONTROL) {
415
- controlChannel = channel
416
- attachFramedIngress(channel)
417
- attachBackpressurePump(channel)
418
- }
419
- if (channel.label === CHANNEL_BULK) {
420
- bulkChannel = channel
421
- attachFramedIngress(channel)
422
- attachBackpressurePump(channel)
423
- }
424
- }
425
-
426
- /**
427
- * 双通道就绪后初始化发送队列并发送 hello。
428
- * @returns {Promise<void>}
429
- */
430
- async function maybeStartPostOpenFlow() {
431
- if (!controlChannel || !bulkChannel) return
432
- await Promise.all([
433
- waitForChannelState(controlChannel, 'open', channelOpenTimeoutMs),
434
- waitForChannelState(bulkChannel, 'open', channelOpenTimeoutMs),
435
- ])
436
- if (!sendQueues)
437
- sendQueues = createChannelSendQueues({
438
- /**
439
- * 按名称返回 control 或 bulk 通道。
440
- * @param {'control' | 'bulk'} name 通道名
441
- * @returns {RTCDataChannel | null | undefined} 对应通道实例
442
- */
443
- getChannel: name => name === CHANNEL_CONTROL ? controlChannel : bulkChannel,
444
- })
445
-
446
- if (!helloSent) {
447
- handshakeTimer = setTimeout(() => { void close('handshake-timeout') }, handshakeTimeoutMs)
448
- helloSent = true
449
- localHello = buildHello(opts.localIdentity ?? {})
450
- await sendRawControl(localHello)
451
- }
452
- await maybeSendAuth()
453
- }
454
-
455
- /**
456
- * 经 control 通道发送 JSON hello/auth。
457
- * @param {object} body hello 或 auth 字段
458
- * @returns {Promise<void>}
459
- */
460
- async function sendRawControl(body) {
461
- if (!controlChannel) throw new Error('p2p: control channel unavailable')
462
- controlChannel.send(JSON.stringify({ type: body.sig ? 'auth' : 'hello', ...body }))
463
- lastOutboundAt = Date.now()
464
- }
465
-
466
- /**
467
- * 发送 envelope:分帧后经发送队列发出。
468
- * @param {{ scope: string, action: string, payload: unknown }} envelope 业务信封
469
- * @returns {Promise<boolean>} 发送成功 true,link 未就绪或已关闭 false
470
- */
471
- async function send(envelope) {
472
- await readyPromise
473
- if (closed || !sendQueues) return false
474
- const message = {
475
- scope: String(envelope?.scope || ''),
476
- action: String(envelope?.action || ''),
477
- payload: envelope?.payload ?? null,
478
- msgId: randomMsgIdHex(),
479
- }
480
- const bytes = encoder.encode(JSON.stringify(message))
481
- for (const frame of encodeFrames(message.msgId, bytes)) {
482
- sendQueues.enqueue(message.action, frame)
483
- sentFrames++
484
- }
485
- lastOutboundAt = Date.now()
486
- return true
487
- }
488
-
489
- /**
490
- * 关闭 link 并清理资源。
491
- * @param {string} [reason='closed'] 关闭原因
492
- * @returns {Promise<void>}
493
- */
494
- async function close(reason = 'closed') {
495
- if (closed) return
496
- closed = true
497
- closeReason = reason
498
- clearTimeout(handshakeTimer)
499
- if (heartbeatTimer) clearInterval(heartbeatTimer)
500
- if (idleTimer) clearInterval(idleTimer)
501
- unlistenRemote?.()
502
- sendQueues?.clear()
503
- try { controlChannel?.close() } catch { /* ignore */ }
504
- try { bulkChannel?.close() } catch { /* ignore */ }
505
- try { await pc.close() } catch { /* ignore */ }
506
- if (!ready) rejectReady(new Error(`p2p: link closed before ready (${reason})`))
507
- emitListeners(downListeners, reason)
508
- }
509
-
510
- unlistenRemote = opts.signal.onRemote(message => {
511
- void handleRemoteSignal(message).catch(error => close(`signal-error:${formatErrorReason(error)}`))
512
- }) ?? null
513
-
514
- attachIceCandidateListener(pc, event => {
515
- if (trickleIceOff || !event.candidate) return
516
- void sendSignal({
517
- type: 'ice',
518
- candidate: typeof event.candidate.toJSON === 'function' ? event.candidate.toJSON() : event.candidate,
519
- }).catch(error => close(`signal-send-failed:${formatErrorReason(error)}`))
520
- })
521
- attachDataChannelListener(pc, event => {
522
- void attachChannel(event.channel)
523
- .then(() => maybeStartPostOpenFlow())
524
- .catch(error => close(`channel-attach-failed:${error?.message ?? error}`))
525
- })
526
-
527
- /**
528
- * PeerConnection 状态变化时触发 close。
529
- * @returns {void}
530
- */
531
- pc.onconnectionstatechange = () => {
532
- if (pc.connectionState === 'failed' || pc.connectionState === 'closed' || pc.connectionState === 'disconnected') {
533
- reconnectCount++
534
- void close(`connection-${pc.connectionState}`)
535
- }
536
- }
537
-
538
- if (opts.initiator) {
539
- await attachChannel(pc.createDataChannel(CHANNEL_CONTROL))
540
- await attachChannel(pc.createDataChannel(CHANNEL_BULK))
541
- const offer = await pc.createOffer()
542
- await pc.setLocalDescription(offer)
543
- await waitForIceGatheringComplete()
544
- await sendSignal({
545
- type: 'description',
546
- description: pc.localDescription?.toJSON?.() ?? pc.localDescription ?? offer,
547
- })
548
- }
549
-
550
- void maybeStartPostOpenFlow().catch(error => close(`open-flow-failed:${error?.message ?? error}`))
551
- return {
552
- ready: readyPromise,
553
- /**
554
- * 握手完成后确认的远端 nodeHash。
555
- * @returns {string | null} 远端 nodeHash
556
- */
557
- get nodeHash() { return remoteNodeHash },
558
- /**
559
- * 本端是否为连接发起方。
560
- * @returns {boolean} 发起方标志
561
- */
562
- get initiator() { return !!opts.initiator },
563
- send,
564
- /**
565
- * 订阅业务 envelope 入站。
566
- * @param {(envelope: { scope: string, action: string, payload: unknown }, remoteNodeHash: string) => void} cb 回调
567
- * @returns {() => void} 取消订阅函数
568
- */
569
- onEnvelope(cb) {
570
- envelopeListeners.add(cb)
571
- return () => envelopeListeners.delete(cb)
572
- },
573
- /**
574
- * 订阅 link 断开事件。
575
- * @param {(reason: string) => void} cb 回调
576
- * @returns {() => void} 取消订阅函数
577
- */
578
- onDown(cb) {
579
- downListeners.add(cb)
580
- return () => downListeners.delete(cb)
581
- },
582
- close,
583
- /**
584
- * 按名称获取底层 data channel(调试用)。
585
- * @param {'control' | 'bulk'} name 通道名
586
- * @returns {RTCDataChannel | null} 通道实例
587
- */
588
- channel(name) {
589
- return name === CHANNEL_CONTROL ? controlChannel : bulkChannel
590
- },
591
- /**
592
- * 返回 link 运行时统计快照。
593
- * @returns {object} 连接状态、计数与缓冲信息
594
- */
595
- stats() {
596
- return {
597
- ready,
598
- nodeHash: remoteNodeHash,
599
- targetNodeHash: targetNodeHash || null,
600
- initiator: !!opts.initiator,
601
- connectionState: pc.connectionState,
602
- iceConnectionState: pc.iceConnectionState,
603
- lastInboundAt,
604
- lastOutboundAt,
605
- sentFrames,
606
- recvFrames,
607
- closeReason,
608
- reconnectCount,
609
- pending: sendQueues?.pending() ?? { control: 0, bulk: 0 },
610
- controlBufferedAmount: controlChannel?.bufferedAmount ?? 0,
611
- bulkBufferedAmount: bulkChannel?.bufferedAmount ?? 0,
612
- controlLowEvents,
613
- bulkLowEvents,
614
- }
615
- },
616
- }
617
- }