@steve02081504/fount-p2p 0.0.20 → 0.0.22
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 +3 -2
- package/core/entity_id.mjs +1 -1
- package/dag/index.mjs +2 -2
- package/dag/storage.mjs +4 -1
- package/federation/chunk_fetch_pending.mjs +1 -1
- package/federation/entity_key_chain.mjs +1 -1
- package/federation/manifest_fetch_pending.mjs +2 -2
- package/files/assemble_stream.mjs +4 -4
- package/files/chunk_responder.mjs +2 -2
- package/files/manifest_fetch.mjs +17 -10
- package/link/channel_mux.mjs +11 -32
- package/link/providers/webrtc.mjs +42 -49
- package/link/rtc/channel.mjs +41 -0
- package/link/rtc/ice_local_hostname.mjs +139 -0
- package/link/rtc/index.mjs +16 -0
- package/link/rtc/polyfill.mjs +159 -0
- package/link/rtc/w3c_bridge.mjs +152 -0
- package/mailbox/store.mjs +1 -1
- package/node/instance.mjs +14 -0
- package/package.json +7 -3
- package/permissions/index.mjs +3 -3
- package/transport/group_link_set.mjs +2 -2
- package/transport/link_registry.mjs +1 -1
- package/transport/runtime_bootstrap.mjs +2 -2
- package/wire/part_fanout.mjs +1 -1
- package/link/rtc.mjs +0 -130
- package/transport/rtc_ice_local_hostname.mjs +0 -118
package/README.md
CHANGED
|
@@ -107,8 +107,9 @@ Maintainer notes for agents / contributors: [AGENTS.md](./AGENTS.md). Sim harnes
|
|
|
107
107
|
|
|
108
108
|
## Optional dependencies
|
|
109
109
|
|
|
110
|
-
- `@stoprocent/noble` / `@stoprocent/bleno` — Bluetooth (optionalDependencies). Hardware probe is
|
|
111
|
-
- `node-datachannel` — WebRTC DataChannels (
|
|
110
|
+
- `@stoprocent/noble` / `@stoprocent/bleno` — Bluetooth (optionalDependencies). Hardware probe is in-process; see [docs/runtime.md](./docs/runtime.md).
|
|
111
|
+
- `node-datachannel` — native WebRTC DataChannels (optionalDependencies). Preferred when the addon loads.
|
|
112
|
+
- `node-rtc-connection` — pure-JS WebRTC DataChannel fallback (dependency; used on Termux/Android or when native is missing).
|
|
112
113
|
- `ws` — Nostr discovery / signaling / last-resort link WebSockets (dependency).
|
|
113
114
|
|
|
114
115
|
Group chunk remote storage (S3, etc.) is implemented by the shell as `GroupStoragePlugin` and injected; see `node/storage_plugins.mjs` for the local reference implementation.
|
package/core/entity_id.mjs
CHANGED
package/dag/index.mjs
CHANGED
|
@@ -252,10 +252,10 @@ export function eventsToMetas(events) {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/**
|
|
255
|
-
*
|
|
255
|
+
* DAG 行内容/签名 canonicalize(re-export)。
|
|
256
256
|
*/
|
|
257
257
|
export { canonicalizeRowContent, canonicalizeSignedRow } from './canonicalize_row.mjs'
|
|
258
258
|
/**
|
|
259
|
-
*
|
|
259
|
+
* 剥离 DAG 事件本地扩展字段(re-export)。
|
|
260
260
|
*/
|
|
261
261
|
export { stripDagEventLocalExtensions } from './strip_extensions.mjs'
|
package/dag/storage.mjs
CHANGED
|
@@ -6,9 +6,12 @@ import { createInterface } from 'node:readline'
|
|
|
6
6
|
import { Readable } from 'node:stream'
|
|
7
7
|
import { pipeline } from 'node:stream/promises'
|
|
8
8
|
|
|
9
|
-
import { atomicTemporaryPath, finalizeAtomicRename } from '../utils/atomic_fs.mjs'
|
|
10
9
|
import { withAsyncMutex } from '../utils/async_mutex.mjs'
|
|
10
|
+
import { atomicTemporaryPath, finalizeAtomicRename } from '../utils/atomic_fs.mjs'
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* 原子重命名 finalize(re-export)。
|
|
14
|
+
*/
|
|
12
15
|
export { finalizeAtomicRename } from '../utils/atomic_fs.mjs'
|
|
13
16
|
|
|
14
17
|
/** 流式重写 JSONL 时分块写入的行数上限 */
|
|
@@ -26,7 +26,7 @@ export function registerManifestFetchWait(key, expectedKey, timeoutMs) {
|
|
|
26
26
|
return {
|
|
27
27
|
done: Promise.resolve(null),
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* 槽位已满时的空操作取消。
|
|
30
30
|
*/
|
|
31
31
|
cancel: () => { },
|
|
32
32
|
}
|
|
@@ -57,7 +57,7 @@ export function registerManifestFetchWait(key, expectedKey, timeoutMs) {
|
|
|
57
57
|
return {
|
|
58
58
|
done,
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
60
|
+
* 取消等待并以 null 结算。
|
|
61
61
|
*/
|
|
62
62
|
cancel: () => {
|
|
63
63
|
clearTimeout(timer)
|
|
@@ -76,7 +76,7 @@ function readStreamChunk(stream) {
|
|
|
76
76
|
if (stream.readableEnded) return Promise.resolve(null)
|
|
77
77
|
return new Promise((resolve, reject) => {
|
|
78
78
|
/**
|
|
79
|
-
*
|
|
79
|
+
* readable 时读取下一块密文。
|
|
80
80
|
*/
|
|
81
81
|
const onReadable = () => {
|
|
82
82
|
cleanup()
|
|
@@ -84,7 +84,7 @@ function readStreamChunk(stream) {
|
|
|
84
84
|
resolve(next ? Buffer.from(next) : Buffer.alloc(0))
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
87
|
+
* 流结束时 resolve null。
|
|
88
88
|
*/
|
|
89
89
|
const onEnd = () => {
|
|
90
90
|
cleanup()
|
|
@@ -98,7 +98,7 @@ function readStreamChunk(stream) {
|
|
|
98
98
|
reject(error)
|
|
99
99
|
}
|
|
100
100
|
/**
|
|
101
|
-
*
|
|
101
|
+
* 注销流事件监听。
|
|
102
102
|
*/
|
|
103
103
|
const cleanup = () => {
|
|
104
104
|
stream.off('readable', onReadable)
|
|
@@ -130,7 +130,7 @@ export function createManifestPlaintextStream(manifest, partStreams, contentKey)
|
|
|
130
130
|
|
|
131
131
|
return new Readable({
|
|
132
132
|
/**
|
|
133
|
-
*
|
|
133
|
+
* 按 manifest part 顺序解密密文块并推送明文。
|
|
134
134
|
*/
|
|
135
135
|
async read() {
|
|
136
136
|
if (finished) return
|
|
@@ -99,7 +99,7 @@ export function attachTrustGraphFedChunkResponder(username, room, fedOut, guardG
|
|
|
99
99
|
if (!String(data.requestId || '')) return
|
|
100
100
|
await handleFedChunkGetIngress(username, data, peerId, (resp, pid) => {
|
|
101
101
|
/**
|
|
102
|
-
*
|
|
102
|
+
* 发送 fed_chunk_data 响应(可经 fedOut 限速队列)。
|
|
103
103
|
*/
|
|
104
104
|
const send = () => {
|
|
105
105
|
try { sendChunkData(resp, pid) }
|
|
@@ -123,7 +123,7 @@ export function attachTrustGraphFedChunkResponder(username, room, fedOut, guardG
|
|
|
123
123
|
if (!String(data.requestId || '')) return
|
|
124
124
|
await handleFedManifestGetIngress(username, data, peerId, (resp, pid) => {
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
126
|
+
* 发送 fed_manifest_data 响应(可经 fedOut 限速队列)。
|
|
127
127
|
*/
|
|
128
128
|
const send = () => {
|
|
129
129
|
try { sendManifestData(resp, pid) }
|
package/files/manifest_fetch.mjs
CHANGED
|
@@ -26,8 +26,8 @@ const manifestInflight = createInflightTable({
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* 拉取公开 manifest;默认不写盘。`cache: true` 或 `cachePublicManifest` 才缓存。
|
|
29
|
-
* 本地已有 publicSig
|
|
30
|
-
*
|
|
29
|
+
* 本地已有 publicSig 时立即返回,并后台 fanout;`cache: true` 时按 publishedAt 择新写盘。
|
|
30
|
+
* 冷 miss 仍等 fanout。同 key in-flight 去重;调用方外层超时不 abort,后台继续填缓存。
|
|
31
31
|
* @param {{ username: string, ownerEntityHash: string, logicalPath: string, cache?: boolean, timeoutMs?: number }} context - 拉取上下文
|
|
32
32
|
* @returns {Promise<import('./manifest.mjs').FileManifest | null>} 验签后的 manifest,失败为 null
|
|
33
33
|
*/
|
|
@@ -42,6 +42,7 @@ export async function fetchPublicManifest(context) {
|
|
|
42
42
|
: DEFAULT_MANIFEST_FETCH_TIMEOUT_MS
|
|
43
43
|
const expectedKey = manifestFetchExpectedKey(ownerEntityHash, logicalPath)
|
|
44
44
|
const inflightKey = `${username}\0${expectedKey}`
|
|
45
|
+
const wantCache = context.cache === true
|
|
45
46
|
|
|
46
47
|
// 先挂 in-flight(同步),再读本地 — 避免并发调用在 await 间隙各自 start
|
|
47
48
|
const localPromise = loadFileManifest(ownerEntityHash, logicalPath)
|
|
@@ -67,15 +68,21 @@ export async function fetchPublicManifest(context) {
|
|
|
67
68
|
const hasLocalPublic = local?.transferKeyDescriptor?.type === 'public' && !!local?.meta?.publicSig
|
|
68
69
|
if (!shared) return hasLocalPublic ? local : null
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
if (hasLocalPublic) {
|
|
72
|
+
// SWR:立刻回本地;后台 fanout 择新写盘(需 cache: true)
|
|
73
|
+
if (wantCache) {
|
|
74
|
+
void shared.then(async result => {
|
|
75
|
+
if (result && shouldPreferIncomingPublicManifest(local, result))
|
|
76
|
+
await cachePublicManifest(ownerEntityHash, logicalPath, result)
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
return local
|
|
76
80
|
}
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
|
|
82
|
+
const result = await shared
|
|
83
|
+
if (!result) return null
|
|
84
|
+
if (wantCache) await cachePublicManifest(ownerEntityHash, logicalPath, result)
|
|
85
|
+
return result
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
/**
|
package/link/channel_mux.mjs
CHANGED
|
@@ -75,35 +75,23 @@ export function pickChannel(action, byteLength) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
* 读取 RTC 数据通道当前 bufferedAmount
|
|
78
|
+
* 读取 RTC 数据通道当前 bufferedAmount。
|
|
79
79
|
* @param {RTCDataChannel} channel RTC 数据通道
|
|
80
80
|
* @returns {number} 缓冲区待发送字节数
|
|
81
81
|
*/
|
|
82
82
|
export function readBufferedAmount(channel) {
|
|
83
|
-
|
|
84
|
-
return Number.isFinite(channel?.bufferedAmount) ? Number(channel.bufferedAmount) : 0
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
return 0
|
|
88
|
-
}
|
|
83
|
+
return channel.bufferedAmount
|
|
89
84
|
}
|
|
90
85
|
|
|
91
86
|
/**
|
|
92
87
|
* 配置数据通道的 bufferedAmountLowThreshold。
|
|
93
88
|
* @param {RTCDataChannel} channel RTC 数据通道
|
|
94
89
|
* @param {number} [thresholdBytes=CHANNEL_LOW_THRESHOLD_BYTES] 低水位阈值(字节)
|
|
95
|
-
* @returns {number
|
|
90
|
+
* @returns {number} 实际生效的阈值
|
|
96
91
|
*/
|
|
97
92
|
export function configureBufferedAmountLowThreshold(channel, thresholdBytes = CHANNEL_LOW_THRESHOLD_BYTES) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return Number.isFinite(channel.bufferedAmountLowThreshold)
|
|
101
|
-
? Number(channel.bufferedAmountLowThreshold)
|
|
102
|
-
: null
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return null
|
|
106
|
-
}
|
|
93
|
+
channel.bufferedAmountLowThreshold = thresholdBytes
|
|
94
|
+
return channel.bufferedAmountLowThreshold
|
|
107
95
|
}
|
|
108
96
|
|
|
109
97
|
/**
|
|
@@ -113,18 +101,9 @@ export function configureBufferedAmountLowThreshold(channel, thresholdBytes = CH
|
|
|
113
101
|
* @returns {() => void} 取消订阅函数
|
|
114
102
|
*/
|
|
115
103
|
export function onBufferedAmountLow(channel, callback) {
|
|
116
|
-
|
|
117
|
-
* bufferedamountlow 事件处理函数。
|
|
118
|
-
* @returns {void}
|
|
119
|
-
*/
|
|
120
|
-
const handler = () => callback()
|
|
121
|
-
channel.addEventListener?.('bufferedamountlow', handler)
|
|
122
|
-
channel.onbufferedamountlow = handler
|
|
123
|
-
if (channel.bufferedAmountLow?.subscribe)
|
|
124
|
-
channel.bufferedAmountLow.subscribe(handler)
|
|
104
|
+
channel.onbufferedamountlow = callback
|
|
125
105
|
return () => {
|
|
126
|
-
channel.
|
|
127
|
-
if (channel.onbufferedamountlow === handler) channel.onbufferedamountlow = null
|
|
106
|
+
if (channel.onbufferedamountlow === callback) channel.onbufferedamountlow = null
|
|
128
107
|
}
|
|
129
108
|
}
|
|
130
109
|
|
|
@@ -163,12 +142,12 @@ export function createChannelSendQueues(options) {
|
|
|
163
142
|
const channel = getChannel(channelName)
|
|
164
143
|
if (channel?.readyState !== 'open') return
|
|
165
144
|
const queue = queues[channelName]
|
|
166
|
-
let
|
|
167
|
-
while (
|
|
145
|
+
let sent = 0
|
|
146
|
+
while (sent < queue.length) {
|
|
168
147
|
if (readBufferedAmount(channel) > highWatermarkBytes) break
|
|
169
|
-
channel.send(queue[
|
|
148
|
+
channel.send(queue[sent++].bytes)
|
|
170
149
|
}
|
|
171
|
-
if (
|
|
150
|
+
if (sent > 0) queue.splice(0, sent)
|
|
172
151
|
}
|
|
173
152
|
|
|
174
153
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toBytes } from '../../core/bytes_codec.mjs'
|
|
1
2
|
import { getSignalingRuntimeConfig } from '../../node/instance.mjs'
|
|
2
3
|
import { nodeDebug } from '../../node/log.mjs'
|
|
3
4
|
import { ms } from '../../utils/duration.mjs'
|
|
@@ -11,20 +12,12 @@ import {
|
|
|
11
12
|
onBufferedAmountLow,
|
|
12
13
|
} from '../channel_mux.mjs'
|
|
13
14
|
import { asLinkHandle, createLinkPipe } from '../pipe.mjs'
|
|
14
|
-
import {
|
|
15
|
-
attachChannelMessageListener,
|
|
16
|
-
attachDataChannelListener,
|
|
17
|
-
attachIceCandidateListener,
|
|
18
|
-
loadNodeRtcPolyfill,
|
|
19
|
-
dataToBytes,
|
|
20
|
-
waitForChannelState,
|
|
21
|
-
} from '../rtc.mjs'
|
|
15
|
+
import { loadNodeRtcPolyfill, waitForChannelState } from '../rtc/index.mjs'
|
|
22
16
|
import { extractDtlsFingerprint } from '../sdp_fingerprint.mjs'
|
|
23
17
|
|
|
24
18
|
import { LINK_LEVEL_WEBRTC } from './levels.mjs'
|
|
25
19
|
|
|
26
20
|
/**
|
|
27
|
-
* 将错误对象格式化为短字符串。
|
|
28
21
|
* @param {unknown} error 原始错误
|
|
29
22
|
* @returns {string} 短 reason
|
|
30
23
|
*/
|
|
@@ -35,27 +28,21 @@ function formatErrorReason(error) {
|
|
|
35
28
|
let cachedAvailable = null
|
|
36
29
|
|
|
37
30
|
/**
|
|
38
|
-
*
|
|
39
|
-
*/
|
|
40
|
-
function isDenoRuntime() {
|
|
41
|
-
return typeof globalThis.Deno !== 'undefined'
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 探测 WebRTC(node-datachannel)是否可用。
|
|
31
|
+
* 探测 WebRTC(node-datachannel 或纯 JS fallback)是否可用。
|
|
46
32
|
* @returns {Promise<boolean>} 可用为 true
|
|
47
33
|
*/
|
|
48
34
|
export async function canUseWebRtcLink() {
|
|
49
35
|
if (cachedAvailable !== null) return cachedAvailable
|
|
50
36
|
try {
|
|
51
|
-
await loadNodeRtcPolyfill()
|
|
37
|
+
const rtc = await loadNodeRtcPolyfill()
|
|
52
38
|
cachedAvailable = true
|
|
39
|
+
nodeDebug('p2p:webrtc backend', { backend: rtc.backend })
|
|
53
40
|
}
|
|
54
41
|
catch (error) {
|
|
55
42
|
cachedAvailable = false
|
|
56
43
|
const reason = formatErrorReason(error)
|
|
57
44
|
nodeDebug('p2p:webrtc unavailable', {
|
|
58
|
-
err:
|
|
45
|
+
err: typeof globalThis.Deno !== 'undefined'
|
|
59
46
|
? `${reason} | deno: see docs/runtime.md (node-datachannel scripts only)`
|
|
60
47
|
: reason,
|
|
61
48
|
})
|
|
@@ -73,15 +60,16 @@ export async function canUseWebRtcLink() {
|
|
|
73
60
|
* @param {number} [options.heartbeatMs] 心跳间隔
|
|
74
61
|
* @param {number} [options.idleTimeoutMs] 无入站流量超时
|
|
75
62
|
* @param {number} [options.handshakeTimeoutMs] 握手超时
|
|
76
|
-
* @param {
|
|
63
|
+
* @param {import('../rtc/polyfill.mjs').LoadedRtcPolyfill} [options.rtc] RTC 构造器
|
|
77
64
|
* @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array, nonce?: string } | null} [options.localIdentity] 本地握手身份
|
|
78
65
|
* @returns {Promise<import('./index.mjs').LinkHandle>} link 句柄
|
|
79
66
|
*/
|
|
80
67
|
export async function createWebRtcLink(options) {
|
|
81
68
|
const handshakeTimeoutMs = Number(options.handshakeTimeoutMs) || ms('10s')
|
|
82
69
|
const channelOpenTimeoutMs = Math.max(handshakeTimeoutMs, ms('30s'))
|
|
83
|
-
const trickleIceOff = getSignalingRuntimeConfig().trickleIceOff === true
|
|
84
70
|
const rtc = options.rtc ?? await loadNodeRtcPolyfill()
|
|
71
|
+
// 纯 JS 后端只做 trickle(SDP 不含 candidate);强制开启 trickle。
|
|
72
|
+
const trickleIceOff = !rtc.forcesTrickleIce && getSignalingRuntimeConfig().trickleIceOff === true
|
|
85
73
|
const peerConnection = new rtc.RTCPeerConnection(options.iceServers?.length ? { iceServers: options.iceServers } : undefined)
|
|
86
74
|
const remoteSignalQueue = []
|
|
87
75
|
const seenRemoteSignals = createLruMap(1024)
|
|
@@ -187,14 +175,6 @@ export async function createWebRtcLink(options) {
|
|
|
187
175
|
await flushQueuedIceCandidates()
|
|
188
176
|
}
|
|
189
177
|
|
|
190
|
-
/**
|
|
191
|
-
* @param {unknown} error addIceCandidate 错误
|
|
192
|
-
* @returns {boolean} 是否应暂存后重试
|
|
193
|
-
*/
|
|
194
|
-
function shouldRetryQueuedIce(error) {
|
|
195
|
-
return /without ice transport/i.test(String(error?.message ?? error ?? ''))
|
|
196
|
-
}
|
|
197
|
-
|
|
198
178
|
/**
|
|
199
179
|
* @returns {Promise<void>}
|
|
200
180
|
*/
|
|
@@ -216,7 +196,7 @@ export async function createWebRtcLink(options) {
|
|
|
216
196
|
await peerConnection.addIceCandidate(candidate)
|
|
217
197
|
}
|
|
218
198
|
catch (error) {
|
|
219
|
-
if (
|
|
199
|
+
if (/without ice transport/i.test(String(error?.message ?? error))) {
|
|
220
200
|
remoteSignalQueue.unshift(candidate)
|
|
221
201
|
return
|
|
222
202
|
}
|
|
@@ -261,7 +241,7 @@ export async function createWebRtcLink(options) {
|
|
|
261
241
|
await peerConnection.addIceCandidate(message.candidate)
|
|
262
242
|
}
|
|
263
243
|
catch (error) {
|
|
264
|
-
if (
|
|
244
|
+
if (/without ice transport/i.test(String(error?.message ?? error))) {
|
|
265
245
|
remoteSignalQueue.push(message.candidate)
|
|
266
246
|
return
|
|
267
247
|
}
|
|
@@ -272,19 +252,23 @@ export async function createWebRtcLink(options) {
|
|
|
272
252
|
|
|
273
253
|
/**
|
|
274
254
|
* @param {RTCDataChannel} channel RTC 数据通道
|
|
275
|
-
* @returns {
|
|
255
|
+
* @returns {void}
|
|
276
256
|
*/
|
|
277
|
-
|
|
257
|
+
function attachChannel(channel) {
|
|
278
258
|
if (channel.label === CHANNEL_CONTROL) controlChannel = channel
|
|
279
259
|
else if (channel.label === CHANNEL_BULK) bulkChannel = channel
|
|
280
260
|
else return
|
|
281
|
-
|
|
261
|
+
/**
|
|
262
|
+
* @param {MessageEvent} event 入站消息事件
|
|
263
|
+
* @returns {void}
|
|
264
|
+
*/
|
|
265
|
+
channel.onmessage = event => {
|
|
282
266
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
267
|
+
const data = event.data
|
|
268
|
+
pipe.handleInbound(typeof data === 'string' ? data : toBytes(data, { allowString: true }))
|
|
285
269
|
}
|
|
286
270
|
catch { /* drop */ }
|
|
287
|
-
}
|
|
271
|
+
}
|
|
288
272
|
attachBackpressurePump(channel)
|
|
289
273
|
}
|
|
290
274
|
|
|
@@ -312,20 +296,29 @@ export async function createWebRtcLink(options) {
|
|
|
312
296
|
void handleRemoteSignal(message).catch(error => pipe.close(`signal-error:${formatErrorReason(error)}`))
|
|
313
297
|
}) ?? null
|
|
314
298
|
|
|
315
|
-
|
|
299
|
+
/**
|
|
300
|
+
* @param {RTCPeerConnectionIceEvent} event ICE candidate 事件
|
|
301
|
+
* @returns {void}
|
|
302
|
+
*/
|
|
303
|
+
peerConnection.onicecandidate = event => {
|
|
316
304
|
if (trickleIceOff || !event.candidate) return
|
|
317
305
|
void sendSignal({
|
|
318
306
|
type: 'ice',
|
|
319
307
|
candidate: typeof event.candidate.toJSON === 'function' ? event.candidate.toJSON() : event.candidate,
|
|
320
308
|
}).catch(error => pipe.close(`signal-send-failed:${formatErrorReason(error)}`))
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* @param {RTCDataChannelEvent} event 远端 data channel 事件
|
|
312
|
+
* @returns {void}
|
|
313
|
+
*/
|
|
314
|
+
peerConnection.ondatachannel = event => {
|
|
315
|
+
attachChannel(event.channel)
|
|
316
|
+
void maybeStartPostOpenFlow().catch(error => pipe.close(`channel-attach-failed:${formatErrorReason(error)}`))
|
|
317
|
+
}
|
|
327
318
|
|
|
328
|
-
/**
|
|
319
|
+
/**
|
|
320
|
+
* 连接失败/断开时递增重连计数并关闭 pipe。
|
|
321
|
+
*/
|
|
329
322
|
peerConnection.onconnectionstatechange = () => {
|
|
330
323
|
if (['failed', 'closed', 'disconnected'].includes(peerConnection.connectionState)) {
|
|
331
324
|
reconnectCount++
|
|
@@ -334,8 +327,8 @@ export async function createWebRtcLink(options) {
|
|
|
334
327
|
}
|
|
335
328
|
|
|
336
329
|
if (options.initiator) {
|
|
337
|
-
|
|
338
|
-
|
|
330
|
+
attachChannel(peerConnection.createDataChannel(CHANNEL_CONTROL))
|
|
331
|
+
attachChannel(peerConnection.createDataChannel(CHANNEL_BULK))
|
|
339
332
|
const offer = await peerConnection.createOffer()
|
|
340
333
|
await peerConnection.setLocalDescription(offer)
|
|
341
334
|
await waitForIceGatheringComplete()
|
|
@@ -345,7 +338,7 @@ export async function createWebRtcLink(options) {
|
|
|
345
338
|
})
|
|
346
339
|
}
|
|
347
340
|
|
|
348
|
-
void maybeStartPostOpenFlow().catch(error => pipe.close(`open-flow-failed:${error
|
|
341
|
+
void maybeStartPostOpenFlow().catch(error => pipe.close(`open-flow-failed:${formatErrorReason(error)}`))
|
|
349
342
|
|
|
350
343
|
return asLinkHandle(pipe, {
|
|
351
344
|
/**
|
|
@@ -353,7 +346,7 @@ export async function createWebRtcLink(options) {
|
|
|
353
346
|
* @param {'control' | 'bulk'} name 通道名
|
|
354
347
|
* @returns {RTCDataChannel | null} 测试用通道
|
|
355
348
|
*/
|
|
356
|
-
|
|
349
|
+
channelForTest(name) {
|
|
357
350
|
return name === CHANNEL_CONTROL ? controlChannel : bulkChannel
|
|
358
351
|
},
|
|
359
352
|
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 等待 data channel 进入 open 或 close 状态,超时则 reject。
|
|
3
|
+
* 同一 channel 可并发等待:新订阅链式调用已有 onopen/onclose,cleanup 只摘掉自身。
|
|
4
|
+
* @param {RTCDataChannel} channel RTC 数据通道
|
|
5
|
+
* @param {'open' | 'close'} eventName 目标状态事件名
|
|
6
|
+
* @param {number} timeoutMs 超时毫秒数
|
|
7
|
+
* @returns {Promise<void>}
|
|
8
|
+
*/
|
|
9
|
+
export function waitForChannelState(channel, eventName, timeoutMs) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
if (eventName === 'open' && channel.readyState === 'open') return resolve()
|
|
12
|
+
if (eventName === 'close' && channel.readyState === 'closed') return resolve()
|
|
13
|
+
const eventHandlerProperty = eventName === 'open' ? 'onopen' : 'onclose'
|
|
14
|
+
const previousHandler = channel[eventHandlerProperty]
|
|
15
|
+
let active = true
|
|
16
|
+
const timer = setTimeout(() => {
|
|
17
|
+
cleanup()
|
|
18
|
+
reject(new Error(`p2p: data channel ${eventName} timeout after ${timeoutMs}ms`))
|
|
19
|
+
}, timeoutMs)
|
|
20
|
+
/**
|
|
21
|
+
* 清除超时并恢复 data channel 事件 handler。
|
|
22
|
+
*/
|
|
23
|
+
const cleanup = () => {
|
|
24
|
+
if (!active) return
|
|
25
|
+
active = false
|
|
26
|
+
clearTimeout(timer)
|
|
27
|
+
if (channel[eventHandlerProperty] === chained) channel[eventHandlerProperty] = previousHandler
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @param {...any} eventArguments 原 handler 参数
|
|
31
|
+
* @returns {void}
|
|
32
|
+
*/
|
|
33
|
+
const chained = (...eventArguments) => {
|
|
34
|
+
previousHandler?.(...eventArguments)
|
|
35
|
+
if (!active) return
|
|
36
|
+
cleanup()
|
|
37
|
+
resolve()
|
|
38
|
+
}
|
|
39
|
+
channel[eventHandlerProperty] = chained
|
|
40
|
+
})
|
|
41
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 服务端 WebRTC polyfill 在 Windows 等环境常产出 `.local` host candidate,
|
|
3
|
+
* 远端无法解析。按 iceLocalHostnamePolicy 改写为 loopback 或丢弃。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @typedef {'none' | 'rewrite-loopback' | 'drop'} IceLocalHostnamePolicy */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string | null | undefined} candidateSdp ICE candidate SDP 行
|
|
10
|
+
* @param {IceLocalHostnamePolicy} policy 处理策略
|
|
11
|
+
* @returns {string | null} 处理后的 SDP;drop 策略下不可用时返回 null
|
|
12
|
+
*/
|
|
13
|
+
export function applyIceLocalHostnamePolicy(candidateSdp, policy) {
|
|
14
|
+
const sdp = String(candidateSdp || '').trim()
|
|
15
|
+
if (!sdp || !/\.local/i.test(sdp)) return sdp || null
|
|
16
|
+
if (!/\btyp host\b/i.test(sdp)) return sdp
|
|
17
|
+
if (policy === 'drop') return null
|
|
18
|
+
if (policy === 'rewrite-loopback') {
|
|
19
|
+
const rewritten = sdp.replace(/(\s)[\w-]+\.local(\s|$)/gi, '$1127.0.0.1$2')
|
|
20
|
+
return rewritten === sdp ? null : rewritten
|
|
21
|
+
}
|
|
22
|
+
return sdp
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {RTCIceCandidate | { candidate?: string } | null | undefined} candidate ICE candidate
|
|
27
|
+
* @param {typeof RTCIceCandidate} RTCIceCandidateCtor 构造函数
|
|
28
|
+
* @param {IceLocalHostnamePolicy} policy 处理策略
|
|
29
|
+
* @returns {RTCIceCandidate | { candidate?: string } | null | undefined} 过滤/改写后的 candidate
|
|
30
|
+
*/
|
|
31
|
+
export function filterIceLocalHostnameCandidate(candidate, RTCIceCandidateCtor, policy) {
|
|
32
|
+
if (!candidate || policy === 'none') return candidate
|
|
33
|
+
const raw = candidate.candidate ?? candidate.toJSON?.()?.candidate ?? ''
|
|
34
|
+
const rewritten = applyIceLocalHostnamePolicy(raw, policy)
|
|
35
|
+
if (!rewritten) return null
|
|
36
|
+
if (rewritten === raw) return candidate
|
|
37
|
+
const init = typeof candidate.toJSON === 'function'
|
|
38
|
+
? { ...candidate.toJSON(), candidate: rewritten }
|
|
39
|
+
: { candidate: rewritten, sdpMid: candidate.sdpMid, sdpMLineIndex: candidate.sdpMLineIndex }
|
|
40
|
+
return new RTCIceCandidateCtor(init)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {typeof RTCPeerConnection} BaseRTC 原始 polyfill 类
|
|
45
|
+
* @param {typeof RTCIceCandidate} RTCIceCandidate ICE candidate 构造函数
|
|
46
|
+
* @param {IceLocalHostnamePolicy} [policy='drop'] 策略
|
|
47
|
+
* @returns {typeof RTCPeerConnection} 包装后的 RTCPeerConnection 类(none 时原样返回)
|
|
48
|
+
*/
|
|
49
|
+
export function wrapRtcPeerConnectionForIceLocalHostname(BaseRTC, RTCIceCandidate, policy = 'drop') {
|
|
50
|
+
if (policy === 'none') return BaseRTC
|
|
51
|
+
|
|
52
|
+
const baseRoutesIce = typeof BaseRTC.prototype.prepareIceCandidateEvent === 'function'
|
|
53
|
+
|
|
54
|
+
return class IceLocalHostnameFilteredRTCPeerConnection extends BaseRTC {
|
|
55
|
+
/** @type {((event: RTCPeerConnectionIceEvent) => void) | null} */
|
|
56
|
+
#userIceHandler = null
|
|
57
|
+
/** @type {Set<(event: unknown) => void>} */
|
|
58
|
+
#iceListeners = new Set()
|
|
59
|
+
/** 去重:同一次 native 派发可能既走 attribute 又走 listener */
|
|
60
|
+
#lastIceEvent = null
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* drop:不派发;rewrite:仅派发替换 candidate 后的事件。
|
|
64
|
+
* @param {RTCPeerConnectionIceEvent | { candidate?: unknown }} event 原始 ICE 事件
|
|
65
|
+
* @returns {RTCPeerConnectionIceEvent | { candidate?: unknown } | null} 规范化后的事件;drop 时为 null
|
|
66
|
+
*/
|
|
67
|
+
prepareIceCandidateEvent(event) {
|
|
68
|
+
if (!event?.candidate) return event
|
|
69
|
+
const filtered = filterIceLocalHostnameCandidate(event.candidate, RTCIceCandidate, policy)
|
|
70
|
+
if (!filtered) return null
|
|
71
|
+
return filtered === event.candidate ? event : { candidate: filtered }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {RTCConfiguration} [config] RTC 配置
|
|
76
|
+
*/
|
|
77
|
+
constructor(config) {
|
|
78
|
+
super(config)
|
|
79
|
+
if (baseRoutesIce) return
|
|
80
|
+
|
|
81
|
+
// native EventTarget:在派发前规范化,自管 listener,不依赖 stopImmediatePropagation。
|
|
82
|
+
Object.defineProperty(this, 'onicecandidate', {
|
|
83
|
+
configurable: true,
|
|
84
|
+
enumerable: true,
|
|
85
|
+
/**
|
|
86
|
+
* @returns {((event: RTCPeerConnectionIceEvent) => void) | null} 用户 ICE handler
|
|
87
|
+
*/
|
|
88
|
+
get: () => this.#userIceHandler,
|
|
89
|
+
/**
|
|
90
|
+
* @param {((event: RTCPeerConnectionIceEvent) => void) | null} handler 用户 ICE handler
|
|
91
|
+
* @returns {void}
|
|
92
|
+
*/
|
|
93
|
+
set: handler => { this.#userIceHandler = handler },
|
|
94
|
+
})
|
|
95
|
+
super.addEventListener('icecandidate', event => this.#deliverIce(event))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {RTCPeerConnectionIceEvent | { candidate?: unknown }} event 原始 ICE 事件
|
|
100
|
+
* @returns {void}
|
|
101
|
+
*/
|
|
102
|
+
#deliverIce = event => {
|
|
103
|
+
if (this.#lastIceEvent === event) return
|
|
104
|
+
this.#lastIceEvent = event
|
|
105
|
+
const normalized = this.prepareIceCandidateEvent(event)
|
|
106
|
+
if (normalized == null) return
|
|
107
|
+
this.#userIceHandler?.(normalized)
|
|
108
|
+
for (const listener of this.#iceListeners) listener(normalized)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} type 事件名
|
|
113
|
+
* @param {(event: unknown) => void} listener 回调
|
|
114
|
+
* @param {boolean | AddEventListenerOptions} [options] 监听选项
|
|
115
|
+
* @returns {void}
|
|
116
|
+
*/
|
|
117
|
+
addEventListener(type, listener, options) {
|
|
118
|
+
if (!baseRoutesIce && type === 'icecandidate') {
|
|
119
|
+
this.#iceListeners.add(listener)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
return super.addEventListener(type, listener, options)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} type 事件名
|
|
127
|
+
* @param {(event: unknown) => void} listener 回调
|
|
128
|
+
* @param {boolean | EventListenerOptions} [options] 监听选项
|
|
129
|
+
* @returns {void}
|
|
130
|
+
*/
|
|
131
|
+
removeEventListener(type, listener, options) {
|
|
132
|
+
if (!baseRoutesIce && type === 'icecandidate') {
|
|
133
|
+
this.#iceListeners.delete(listener)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
return super.removeEventListener(type, listener, options)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 等待 data channel open/close(re-export)。
|
|
3
|
+
*/
|
|
4
|
+
export { waitForChannelState } from './channel.mjs'
|
|
5
|
+
/**
|
|
6
|
+
* ICE candidate 本地主机名策略(re-export)。
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
applyIceLocalHostnamePolicy,
|
|
10
|
+
filterIceLocalHostnameCandidate,
|
|
11
|
+
wrapRtcPeerConnectionForIceLocalHostname,
|
|
12
|
+
} from './ice_local_hostname.mjs'
|
|
13
|
+
/**
|
|
14
|
+
* Node WebRTC polyfill 加载(re-export)。
|
|
15
|
+
*/
|
|
16
|
+
export { clearNodeRtcPolyfillCache, loadNodeRtcPolyfill } from './polyfill.mjs'
|