@steve02081504/fount-p2p 0.0.6 → 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.
- package/core/tcp_port.mjs +11 -0
- package/discovery/advert_peer_hints.mjs +21 -0
- package/discovery/{bt.mjs → bt/index.mjs} +151 -55
- package/discovery/bt/peer_hints.mjs +41 -0
- package/discovery/bt/runtime.mjs +46 -0
- package/discovery/lan_peer_hints.mjs +43 -0
- package/discovery/mdns.mjs +6 -2
- package/index.mjs +3 -2
- package/link/handshake.mjs +48 -21
- package/link/pipe.mjs +404 -0
- package/link/providers/ble_gatt.mjs +339 -0
- package/link/providers/index.mjs +90 -0
- package/link/providers/lan_tcp.mjs +342 -0
- package/link/providers/levels.mjs +9 -0
- package/link/providers/webrtc.mjs +385 -0
- package/package.json +3 -2
- package/rooms/scoped_link.mjs +5 -3
- package/transport/group_link_set.mjs +5 -3
- package/transport/link_registry.mjs +241 -54
- package/utils/ttl_map.mjs +41 -0
- 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} options link 配置
|
|
77
|
-
* @param {string | null} [options.nodeHash] 期望的对端 nodeHash,省略则不校验
|
|
78
|
-
* @param {boolean} options.initiator 是否为连接发起方
|
|
79
|
-
* @param {{ send: (message: unknown) => void | Promise<void>, onRemote: (handler: (message: unknown) => void) => (() => void) | void }} options.signal 信令收发接口
|
|
80
|
-
* @param {RTCConfiguration['iceServers']} [options.iceServers] ICE 服务器列表
|
|
81
|
-
* @param {number} [options.heartbeatMs] 心跳间隔(毫秒)
|
|
82
|
-
* @param {number} [options.idleTimeoutMs] 无入站流量超时(毫秒)
|
|
83
|
-
* @param {number} [options.handshakeTimeoutMs] 握手超时(毫秒)
|
|
84
|
-
* @param {{ RTCPeerConnection: typeof RTCPeerConnection } | null} [options.rtc] RTC 构造器,省略则加载 polyfill
|
|
85
|
-
* @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array, nonce?: string } | null} [options.localIdentity] 本地握手身份
|
|
86
|
-
* @returns {Promise<{ ready: Promise<void>, get nodeHash(): string | null, send: (envelope: { scope: string, action: string, payload: unknown }) => Promise<boolean>, onEnvelope: (callback: (envelope: { scope: string, action: string, payload: unknown }, remoteNodeHash: string) => void) => () => void, onDown: (callback: (reason: string) => void) => () => void, close: (reason?: string) => Promise<void>, stats: () => object }>} link 句柄
|
|
87
|
-
*/
|
|
88
|
-
export async function createLink(options) {
|
|
89
|
-
const heartbeatMs = Number(options.heartbeatMs) || ms('15s')
|
|
90
|
-
const idleTimeoutMs = Number(options.idleTimeoutMs) || ms('45s')
|
|
91
|
-
const handshakeTimeoutMs = Number(options.handshakeTimeoutMs) || ms('10s')
|
|
92
|
-
const channelOpenTimeoutMs = Math.max(handshakeTimeoutMs, ms('30s'))
|
|
93
|
-
const trickleIceOff = getSignalingRuntimeConfig().trickleIceOff === true
|
|
94
|
-
const rtc = options.rtc ?? await loadNodeRtcPolyfill()
|
|
95
|
-
const pc = new rtc.RTCPeerConnection(options.iceServers?.length ? { iceServers: options.iceServers } : undefined)
|
|
96
|
-
const targetNodeHash = normalizeHex64(options.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(options.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, options.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(options.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 = options.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 (options.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 !!options.initiator },
|
|
563
|
-
send,
|
|
564
|
-
/**
|
|
565
|
-
* 订阅业务 envelope 入站。
|
|
566
|
-
* @param {(envelope: { scope: string, action: string, payload: unknown }, remoteNodeHash: string) => void} callback 回调
|
|
567
|
-
* @returns {() => void} 取消订阅函数
|
|
568
|
-
*/
|
|
569
|
-
onEnvelope(callback) {
|
|
570
|
-
envelopeListeners.add(callback)
|
|
571
|
-
return () => envelopeListeners.delete(callback)
|
|
572
|
-
},
|
|
573
|
-
/**
|
|
574
|
-
* 订阅 link 断开事件。
|
|
575
|
-
* @param {(reason: string) => void} callback 回调
|
|
576
|
-
* @returns {() => void} 取消订阅函数
|
|
577
|
-
*/
|
|
578
|
-
onDown(callback) {
|
|
579
|
-
downListeners.add(callback)
|
|
580
|
-
return () => downListeners.delete(callback)
|
|
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: !!options.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
|
-
}
|