@steve02081504/fount-p2p 0.0.19 → 0.0.21
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_fetch.mjs +30 -13
- package/files/chunk_responder.mjs +2 -2
- package/files/manifest_fetch.mjs +38 -23
- 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/utils/inflight_table.mjs +95 -0
- package/wire/part_fanout.mjs +1 -1
- package/link/rtc.mjs +0 -130
- package/transport/rtc_ice_local_hostname.mjs +0 -118
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-flight 去重表:同 key 复用并 touch 到队尾;队满时仅淘汰「已超过 baseTimeout」的队首。
|
|
3
|
+
* 双窗口:size >= maxSize 且 entry 年龄 >= baseTimeoutMs 才 cancel。
|
|
4
|
+
*
|
|
5
|
+
* @template T
|
|
6
|
+
* @param {{ maxSize: number, baseTimeoutMs: number, now?: () => number }} options 容量与基础超时
|
|
7
|
+
* @returns {{
|
|
8
|
+
* size: () => number,
|
|
9
|
+
* has: (key: string) => boolean,
|
|
10
|
+
* acquire: (key: string, start: () => { done: Promise<T>, cancel: () => void }) => Promise<T> | null,
|
|
11
|
+
* clear: () => void,
|
|
12
|
+
* }} 表句柄
|
|
13
|
+
*/
|
|
14
|
+
export function createInflightTable(options) {
|
|
15
|
+
const maxSize = Math.max(1, Math.floor(Number(options.maxSize) || 1))
|
|
16
|
+
const baseTimeoutMs = Math.max(0, Number(options.baseTimeoutMs) || 0)
|
|
17
|
+
const now = options.now || Date.now
|
|
18
|
+
|
|
19
|
+
/** @type {Map<string, { done: Promise<T>, cancel: () => void, startedAt: number }>} */
|
|
20
|
+
const map = new Map()
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 队满时从队首取消已超时项。
|
|
24
|
+
* @returns {void}
|
|
25
|
+
*/
|
|
26
|
+
function pruneAgedOverCap() {
|
|
27
|
+
const t = now()
|
|
28
|
+
while (map.size >= maxSize) {
|
|
29
|
+
const oldestKey = map.keys().next().value
|
|
30
|
+
const entry = map.get(oldestKey)
|
|
31
|
+
if (!entry || t - entry.startedAt < baseTimeoutMs) break
|
|
32
|
+
map.delete(oldestKey)
|
|
33
|
+
entry.cancel()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} key 逻辑键
|
|
39
|
+
* @param {{ done: Promise<T>, cancel: () => void, startedAt: number }} entry 条目
|
|
40
|
+
* @returns {void}
|
|
41
|
+
*/
|
|
42
|
+
function track(key, entry) {
|
|
43
|
+
entry.done.finally(() => {
|
|
44
|
+
if (map.get(key) === entry) map.delete(key)
|
|
45
|
+
})
|
|
46
|
+
map.set(key, entry)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
/**
|
|
51
|
+
* @returns {number} 当前 in-flight 数
|
|
52
|
+
*/
|
|
53
|
+
size: () => map.size,
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} key 逻辑键
|
|
56
|
+
* @returns {boolean} 是否在飞
|
|
57
|
+
*/
|
|
58
|
+
has: key => map.has(key),
|
|
59
|
+
/**
|
|
60
|
+
* 复用或启动;队满且无法淘汰超时项时返回 null(拒绝新开)。
|
|
61
|
+
* @param {string} key 逻辑键
|
|
62
|
+
* @param {() => { done: Promise<T>, cancel: () => void }} start 仅在未命中时调用
|
|
63
|
+
* @returns {Promise<T> | null} 共享 Promise,或拒绝新开
|
|
64
|
+
*/
|
|
65
|
+
acquire(key, start) {
|
|
66
|
+
const existing = map.get(key)
|
|
67
|
+
if (existing) {
|
|
68
|
+
map.delete(key)
|
|
69
|
+
map.set(key, existing)
|
|
70
|
+
pruneAgedOverCap()
|
|
71
|
+
return existing.done
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pruneAgedOverCap()
|
|
75
|
+
if (map.size >= maxSize) return null
|
|
76
|
+
|
|
77
|
+
const started = start()
|
|
78
|
+
const entry = {
|
|
79
|
+
done: started.done,
|
|
80
|
+
cancel: started.cancel,
|
|
81
|
+
startedAt: now(),
|
|
82
|
+
}
|
|
83
|
+
track(key, entry)
|
|
84
|
+
return entry.done
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* 取消全部并清空(测试用)。
|
|
88
|
+
* @returns {void}
|
|
89
|
+
*/
|
|
90
|
+
clear() {
|
|
91
|
+
for (const entry of map.values()) entry.cancel()
|
|
92
|
+
map.clear()
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
package/wire/part_fanout.mjs
CHANGED
package/link/rtc.mjs
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import process from 'node:process'
|
|
2
|
-
|
|
3
|
-
import { toBytes } from '../core/bytes_codec.mjs'
|
|
4
|
-
import { getSignalingRuntimeConfig } from '../node/instance.mjs'
|
|
5
|
-
import { wrapRtcPeerConnectionForIceLocalHostname } from '../transport/rtc_ice_local_hostname.mjs'
|
|
6
|
-
|
|
7
|
-
/** @type {boolean} */
|
|
8
|
-
let exitCleanupHooked = false
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 注册进程退出时销毁 libdatachannel 原生资源(首次成功加载后挂一次)。
|
|
12
|
-
* libdatachannel 的原生线程在 pc.close() 后仍需时间回收;进程退出时若原生资源未同步销毁,
|
|
13
|
-
* Windows 上会触发堆损坏(退出码 0xC0000374)。
|
|
14
|
-
* @returns {Promise<void>}
|
|
15
|
-
*/
|
|
16
|
-
async function ensureNodeDatachannelExitCleanup() {
|
|
17
|
-
if (exitCleanupHooked) return
|
|
18
|
-
exitCleanupHooked = true
|
|
19
|
-
const { cleanup = undefined } = await import('node-datachannel').catch(() => ({}))
|
|
20
|
-
process.on('exit', () => {
|
|
21
|
-
try { cleanup?.() } catch { /* already torn down */ }
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 加载 node-datachannel polyfill,并按配置包装 RTCPeerConnection。
|
|
27
|
-
* @returns {Promise<{ RTCPeerConnection: typeof RTCPeerConnection, RTCIceCandidate: typeof RTCIceCandidate }>} RTC 构造器
|
|
28
|
-
*/
|
|
29
|
-
export async function loadNodeRtcPolyfill() {
|
|
30
|
-
const mod = await import('node-datachannel/polyfill')
|
|
31
|
-
await ensureNodeDatachannelExitCleanup()
|
|
32
|
-
const { iceLocalHostnamePolicy } = getSignalingRuntimeConfig()
|
|
33
|
-
return {
|
|
34
|
-
RTCPeerConnection: wrapRtcPeerConnectionForIceLocalHostname(mod.RTCPeerConnection, mod.RTCIceCandidate, iceLocalHostnamePolicy),
|
|
35
|
-
RTCIceCandidate: mod.RTCIceCandidate,
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 绑定 ICE candidate 回调,兼容 onicecandidate 与 onIceCandidate.subscribe。
|
|
41
|
-
* @param {RTCPeerConnection} pc 对等连接
|
|
42
|
-
* @param {(event: { candidate: RTCIceCandidate | null }) => void} handler candidate 事件处理器
|
|
43
|
-
* @returns {void}
|
|
44
|
-
*/
|
|
45
|
-
export function attachIceCandidateListener(pc, handler) {
|
|
46
|
-
pc.onicecandidate = handler
|
|
47
|
-
pc.onIceCandidate?.subscribe?.(candidate =>
|
|
48
|
-
handler({ candidate: candidate ?? null })
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 绑定远端 data channel 回调,兼容 ondatachannel 与 onDataChannel.subscribe。
|
|
54
|
-
* @param {RTCPeerConnection} pc 对等连接
|
|
55
|
-
* @param {(event: { channel: RTCDataChannel }) => void} handler data channel 事件处理器
|
|
56
|
-
* @returns {void}
|
|
57
|
-
*/
|
|
58
|
-
export function attachDataChannelListener(pc, handler) {
|
|
59
|
-
pc.ondatachannel = handler
|
|
60
|
-
pc.onDataChannel?.subscribe?.(channel => handler({ channel }))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 等待 data channel 进入 open 或 close 状态,超时则 reject。
|
|
65
|
-
* @param {RTCDataChannel} channel RTC 数据通道
|
|
66
|
-
* @param {'open' | 'close'} eventName 目标状态事件名
|
|
67
|
-
* @param {number} timeoutMs 超时毫秒数
|
|
68
|
-
* @returns {Promise<void>}
|
|
69
|
-
*/
|
|
70
|
-
export function waitForChannelState(channel, eventName, timeoutMs) {
|
|
71
|
-
return new Promise((resolve, reject) => {
|
|
72
|
-
if (eventName === 'open' && channel.readyState === 'open') {
|
|
73
|
-
resolve()
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
if (eventName === 'close' && channel.readyState === 'closed') {
|
|
77
|
-
resolve()
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
const timer = setTimeout(() => {
|
|
81
|
-
cleanup()
|
|
82
|
-
reject(new Error(`p2p: data channel ${eventName} timeout after ${timeoutMs}ms`))
|
|
83
|
-
}, timeoutMs)
|
|
84
|
-
/**
|
|
85
|
-
* 通道状态变化处理函数。
|
|
86
|
-
* @returns {void}
|
|
87
|
-
*/
|
|
88
|
-
const handler = () => {
|
|
89
|
-
cleanup()
|
|
90
|
-
resolve()
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* 移除监听器并清除超时定时器。
|
|
94
|
-
* @returns {void}
|
|
95
|
-
*/
|
|
96
|
-
const cleanup = () => {
|
|
97
|
-
clearTimeout(timer)
|
|
98
|
-
channel.removeEventListener?.(eventName, handler)
|
|
99
|
-
if (eventName === 'open' && channel.onopen === handler) channel.onopen = null
|
|
100
|
-
if (eventName === 'close' && channel.onclose === handler) channel.onclose = null
|
|
101
|
-
}
|
|
102
|
-
channel.addEventListener?.(eventName, handler)
|
|
103
|
-
if (eventName === 'open') channel.onopen = handler
|
|
104
|
-
if (eventName === 'close') channel.onclose = handler
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* 绑定 data channel message 回调(addEventListener / onmessage / onMessage.subscribe)。
|
|
110
|
-
* @param {RTCDataChannel} channel data channel
|
|
111
|
-
* @param {(data: unknown) => void} handler 消息回调
|
|
112
|
-
* @returns {void}
|
|
113
|
-
*/
|
|
114
|
-
export function attachChannelMessageListener(channel, handler) {
|
|
115
|
-
channel.addEventListener?.('message', event => handler(event?.data))
|
|
116
|
-
/**
|
|
117
|
-
* @param {{ data?: unknown }} event message 事件
|
|
118
|
-
* @returns {void}
|
|
119
|
-
*/
|
|
120
|
-
channel.onmessage = event => handler(event?.data)
|
|
121
|
-
channel.onMessage?.subscribe(message => handler(message))
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* @param {unknown} data 通道原始数据
|
|
126
|
-
* @returns {Uint8Array} 字节
|
|
127
|
-
*/
|
|
128
|
-
export function dataToBytes(data) {
|
|
129
|
-
return toBytes(data, { allowString: true })
|
|
130
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
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 = typeof candidate === 'string'
|
|
34
|
-
? candidate
|
|
35
|
-
: candidate.candidate ?? candidate.toJSON?.()?.candidate ?? ''
|
|
36
|
-
const rewritten = applyIceLocalHostnamePolicy(raw, policy)
|
|
37
|
-
if (!rewritten) return null
|
|
38
|
-
if (rewritten === raw) return candidate
|
|
39
|
-
try {
|
|
40
|
-
const init = typeof candidate.toJSON === 'function'
|
|
41
|
-
? { ...candidate.toJSON(), candidate: rewritten }
|
|
42
|
-
: { candidate: rewritten, sdpMid: candidate.sdpMid, sdpMLineIndex: candidate.sdpMLineIndex }
|
|
43
|
-
return new RTCIceCandidateCtor(init)
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return { ...candidate, candidate: rewritten }
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @param {typeof RTCPeerConnection} BaseRTC 原始 polyfill 类
|
|
52
|
-
* @param {typeof RTCIceCandidate} [RTCIceCandidate] ICE candidate 构造函数
|
|
53
|
-
* @param {IceLocalHostnamePolicy} [policy='drop'] 策略
|
|
54
|
-
* @returns {typeof RTCPeerConnection} 包装后的 RTCPeerConnection 类(none 时原样返回)
|
|
55
|
-
*/
|
|
56
|
-
export function wrapRtcPeerConnectionForIceLocalHostname(BaseRTC, RTCIceCandidate = globalThis.RTCIceCandidate, policy = 'drop') {
|
|
57
|
-
if (policy === 'none') return BaseRTC
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* @param {RTCPeerConnectionIceEvent} event ICE 候选事件
|
|
61
|
-
* @param {(event: RTCPeerConnectionIceEvent) => void} handler 用户 handler
|
|
62
|
-
* @returns {void}
|
|
63
|
-
*/
|
|
64
|
-
function invokeFilteredIceHandler(event, handler) {
|
|
65
|
-
if (!event?.candidate) {
|
|
66
|
-
handler(event)
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
const filtered = filterIceLocalHostnameCandidate(event.candidate, RTCIceCandidate, policy)
|
|
70
|
-
if (!filtered) return
|
|
71
|
-
if (filtered === event.candidate) {
|
|
72
|
-
handler(event)
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
handler({ ...event, candidate: filtered })
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return class IceLocalHostnameFilteredRTCPeerConnection extends BaseRTC {
|
|
79
|
-
/** @type {((event: RTCPeerConnectionIceEvent) => void) | null} */
|
|
80
|
-
#userIceHandler = null
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* @param {RTCConfiguration} [config] RTC 配置
|
|
84
|
-
*/
|
|
85
|
-
constructor(config) {
|
|
86
|
-
super(config)
|
|
87
|
-
/** @param {RTCPeerConnectionIceEvent} event ICE 候选事件 */
|
|
88
|
-
const relayIce = event => {
|
|
89
|
-
if (this.#userIceHandler)
|
|
90
|
-
invokeFilteredIceHandler(event, this.#userIceHandler)
|
|
91
|
-
}
|
|
92
|
-
super.onicecandidate = relayIce
|
|
93
|
-
const iceObs = this.onIceCandidate
|
|
94
|
-
if (iceObs && typeof iceObs.subscribe === 'function')
|
|
95
|
-
iceObs.subscribe(candidate => {
|
|
96
|
-
if (!this.#userIceHandler) return
|
|
97
|
-
if (!candidate) {
|
|
98
|
-
this.#userIceHandler({ candidate: null })
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
const filtered = filterIceLocalHostnameCandidate(candidate, RTCIceCandidate, policy)
|
|
102
|
-
if (filtered)
|
|
103
|
-
this.#userIceHandler({ candidate: filtered })
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** @returns {((event: RTCPeerConnectionIceEvent) => void) | null} 用户 ICE 处理器 */
|
|
109
|
-
get onicecandidate() {
|
|
110
|
-
return this.#userIceHandler
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** @param {((event: RTCPeerConnectionIceEvent) => void) | null} handler ICE 处理器 */
|
|
114
|
-
set onicecandidate(handler) {
|
|
115
|
-
this.#userIceHandler = handler
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|