@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.
@@ -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
+ }
@@ -27,7 +27,7 @@ export async function collectPartInvokeResponses(username, partpath, invoke, tim
27
27
 
28
28
  const waitForResponses = new Promise(resolve => {
29
29
  /**
30
- *
30
+ * 超时或收齐响应后结束等待并清理 pending part_invoke。
31
31
  */
32
32
  const finish = () => {
33
33
  clearTimeout(timer)
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
- }