@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,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'
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import { getRtcPolyfillCacheEpoch, getSignalingRuntimeConfig } from '../../node/instance.mjs'
|
|
4
|
+
import { nodeDebug } from '../../node/log.mjs'
|
|
5
|
+
|
|
6
|
+
import { wrapRtcPeerConnectionForIceLocalHostname } from './ice_local_hostname.mjs'
|
|
7
|
+
import { bridgePeerConnection } from './w3c_bridge.mjs'
|
|
8
|
+
|
|
9
|
+
/** @type {boolean} */
|
|
10
|
+
let exitCleanupHooked = false
|
|
11
|
+
|
|
12
|
+
/** @type {Promise<LoadedRtcPolyfill> | null} */
|
|
13
|
+
let cachedDefaultPolyfill = null
|
|
14
|
+
|
|
15
|
+
/** @type {number} 与 cachedDefaultPolyfill 绑定的策略世代 */
|
|
16
|
+
let cachedDefaultPolyfillEpoch = -1
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{
|
|
20
|
+
* RTCPeerConnection: typeof RTCPeerConnection,
|
|
21
|
+
* RTCIceCandidate: typeof RTCIceCandidate,
|
|
22
|
+
* backend: string,
|
|
23
|
+
* forcesTrickleIce: boolean,
|
|
24
|
+
* }} LoadedRtcPolyfill
|
|
25
|
+
* @typedef {{
|
|
26
|
+
* id: string,
|
|
27
|
+
* forcesTrickleIce?: boolean,
|
|
28
|
+
* load: () => Promise<{ RTCPeerConnection: typeof RTCPeerConnection, RTCIceCandidate: typeof RTCIceCandidate }>,
|
|
29
|
+
* }} RtcBackend
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 清除默认后端加载缓存(iceLocalHostnamePolicy 变更后调用)。
|
|
34
|
+
* @returns {void}
|
|
35
|
+
*/
|
|
36
|
+
export function clearNodeRtcPolyfillCache() {
|
|
37
|
+
cachedDefaultPolyfill = null
|
|
38
|
+
cachedDefaultPolyfillEpoch = -1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 注册进程退出时销毁 libdatachannel 原生资源(首次成功加载后挂一次)。
|
|
43
|
+
* libdatachannel 的原生线程在 pc.close() 后仍需时间回收;进程退出时若原生资源未同步销毁,
|
|
44
|
+
* Windows 上会触发堆损坏(退出码 0xC0000374)。
|
|
45
|
+
* @returns {Promise<void>}
|
|
46
|
+
*/
|
|
47
|
+
async function ensureNodeDatachannelExitCleanup() {
|
|
48
|
+
if (exitCleanupHooked) return
|
|
49
|
+
exitCleanupHooked = true
|
|
50
|
+
const { cleanup = undefined } = await import('node-datachannel').catch(() => ({}))
|
|
51
|
+
process.on('exit', () => {
|
|
52
|
+
try { cleanup?.() } catch { /* already torn down */ }
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @returns {Promise<{ RTCPeerConnection: typeof RTCPeerConnection, RTCIceCandidate: typeof RTCIceCandidate }>} node-datachannel 构造器
|
|
58
|
+
*/
|
|
59
|
+
async function loadNodeDatachannelBackend() {
|
|
60
|
+
const mod = await import('node-datachannel/polyfill')
|
|
61
|
+
await ensureNodeDatachannelExitCleanup()
|
|
62
|
+
return {
|
|
63
|
+
RTCPeerConnection: mod.RTCPeerConnection,
|
|
64
|
+
RTCIceCandidate: mod.RTCIceCandidate,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 纯 JS WebRTC DataChannel(Termux / 无 native prebuild 时的 fallback)。
|
|
70
|
+
* @returns {Promise<{ RTCPeerConnection: typeof RTCPeerConnection, RTCIceCandidate: typeof RTCIceCandidate }>} node-rtc-connection 构造器
|
|
71
|
+
*/
|
|
72
|
+
async function loadNodeRtcConnectionBackend() {
|
|
73
|
+
const module = await import('node-rtc-connection')
|
|
74
|
+
return {
|
|
75
|
+
RTCPeerConnection: /** @type {typeof RTCPeerConnection} */ bridgePeerConnection(module.RTCPeerConnection),
|
|
76
|
+
RTCIceCandidate: module.RTCIceCandidate,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @type {RtcBackend} */
|
|
81
|
+
const PURE_JS_BACKEND = {
|
|
82
|
+
id: 'node-rtc-connection',
|
|
83
|
+
forcesTrickleIce: true,
|
|
84
|
+
load: loadNodeRtcConnectionBackend,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @returns {RtcBackend[]} 默认后端顺序:优先 native,失败再纯 JS
|
|
89
|
+
*/
|
|
90
|
+
function defaultRtcBackends() {
|
|
91
|
+
/** @type {RtcBackend[]} */
|
|
92
|
+
const backends = []
|
|
93
|
+
// Android/Termux:无官方 prebuild,且 Bionic 不能跑 linux-arm64 glibc 包;直接走纯 JS。
|
|
94
|
+
if (process.platform !== 'android')
|
|
95
|
+
backends.push({ id: 'node-datachannel', load: loadNodeDatachannelBackend })
|
|
96
|
+
backends.push(PURE_JS_BACKEND)
|
|
97
|
+
return backends
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {{ backends?: RtcBackend[] }} options 后端列表
|
|
102
|
+
* @returns {Promise<LoadedRtcPolyfill>} 首个可用后端的 polyfill
|
|
103
|
+
*/
|
|
104
|
+
async function loadNodeRtcPolyfillUncached(options) {
|
|
105
|
+
const backends = options.backends?.length
|
|
106
|
+
? [...options.backends, PURE_JS_BACKEND]
|
|
107
|
+
: defaultRtcBackends()
|
|
108
|
+
/** @type {unknown} */
|
|
109
|
+
let lastError = null
|
|
110
|
+
for (const backend of backends)
|
|
111
|
+
try {
|
|
112
|
+
const mod = await backend.load()
|
|
113
|
+
const { iceLocalHostnamePolicy } = getSignalingRuntimeConfig()
|
|
114
|
+
return {
|
|
115
|
+
RTCPeerConnection: wrapRtcPeerConnectionForIceLocalHostname(
|
|
116
|
+
mod.RTCPeerConnection,
|
|
117
|
+
mod.RTCIceCandidate,
|
|
118
|
+
iceLocalHostnamePolicy,
|
|
119
|
+
),
|
|
120
|
+
RTCIceCandidate: mod.RTCIceCandidate,
|
|
121
|
+
backend: backend.id,
|
|
122
|
+
forcesTrickleIce: backend.forcesTrickleIce === true,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
lastError = error
|
|
127
|
+
nodeDebug('p2p:webrtc backend unavailable', {
|
|
128
|
+
backend: backend.id,
|
|
129
|
+
err: String(error?.message ?? error).replace(/\s+/g, ' ').slice(0, 240),
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError ?? 'no rtc backend'))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 加载 RTC polyfill(node-datachannel 优先,失败则 node-rtc-connection),并按配置包装 RTCPeerConnection。
|
|
138
|
+
* 默认后端路径会缓存首次成功结果;注入 backends 时不走缓存。
|
|
139
|
+
* @param {{ backends?: RtcBackend[] }} [options] 可注入后端列表(测试用)
|
|
140
|
+
* @returns {Promise<LoadedRtcPolyfill>} RTC 构造器
|
|
141
|
+
*/
|
|
142
|
+
export async function loadNodeRtcPolyfill(options = {}) {
|
|
143
|
+
if (options.backends?.length)
|
|
144
|
+
return loadNodeRtcPolyfillUncached(options)
|
|
145
|
+
const epoch = getRtcPolyfillCacheEpoch()
|
|
146
|
+
if (!cachedDefaultPolyfill || cachedDefaultPolyfillEpoch !== epoch) {
|
|
147
|
+
cachedDefaultPolyfill = null
|
|
148
|
+
cachedDefaultPolyfillEpoch = epoch
|
|
149
|
+
const pending = loadNodeRtcPolyfillUncached(options).catch(error => {
|
|
150
|
+
if (cachedDefaultPolyfill === pending) {
|
|
151
|
+
cachedDefaultPolyfill = null
|
|
152
|
+
cachedDefaultPolyfillEpoch = -1
|
|
153
|
+
}
|
|
154
|
+
throw error
|
|
155
|
+
})
|
|
156
|
+
cachedDefaultPolyfill = pending
|
|
157
|
+
}
|
|
158
|
+
return cachedDefaultPolyfill
|
|
159
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把 EventEmitter 风格的 RTC(如 node-rtc-connection)桥成 W3C 属性 handler。
|
|
3
|
+
* 其余链路只认 onicecandidate / onmessage / onbufferedamountlow 等。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {WeakSet<object>} */
|
|
7
|
+
const bridgedChannels = new WeakSet()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} target EventEmitter 目标
|
|
11
|
+
* @param {string} property W3C 属性名(如 onmessage)
|
|
12
|
+
* @param {string} eventName EventEmitter 事件名
|
|
13
|
+
* @param {(payload: unknown) => unknown} [adapt] 事件载荷适配
|
|
14
|
+
* @returns {void}
|
|
15
|
+
*/
|
|
16
|
+
function defineEmitterHandler(target, property, eventName, adapt = payload => payload) {
|
|
17
|
+
let handler = null
|
|
18
|
+
target.on(eventName, payload => handler?.(adapt(payload)))
|
|
19
|
+
Object.defineProperty(target, property, {
|
|
20
|
+
configurable: true,
|
|
21
|
+
enumerable: true,
|
|
22
|
+
/**
|
|
23
|
+
* @returns {((payload: unknown) => void) | null} 当前 handler
|
|
24
|
+
*/
|
|
25
|
+
get: () => handler,
|
|
26
|
+
/**
|
|
27
|
+
* @param {((payload: unknown) => void) | null} value 新 handler
|
|
28
|
+
* @returns {void}
|
|
29
|
+
*/
|
|
30
|
+
set: value => { handler = value },
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {RTCDataChannel} channel 原始 data channel
|
|
36
|
+
* @returns {RTCDataChannel} 已挂 W3C handler 的通道
|
|
37
|
+
*/
|
|
38
|
+
export function bridgeDataChannel(channel) {
|
|
39
|
+
if (bridgedChannels.has(channel) || typeof channel.on !== 'function') return channel
|
|
40
|
+
bridgedChannels.add(channel)
|
|
41
|
+
defineEmitterHandler(channel, 'onmessage', 'message')
|
|
42
|
+
defineEmitterHandler(channel, 'onopen', 'open')
|
|
43
|
+
defineEmitterHandler(channel, 'onclose', 'close')
|
|
44
|
+
defineEmitterHandler(channel, 'onbufferedamountlow', 'bufferedamountlow')
|
|
45
|
+
return channel
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {typeof RTCPeerConnection} BaseRTC EventEmitter 风格 RTCPeerConnection
|
|
50
|
+
* @returns {typeof RTCPeerConnection} W3C handler 版
|
|
51
|
+
*/
|
|
52
|
+
export function bridgePeerConnection(BaseRTC) {
|
|
53
|
+
return class W3cRtcPeerConnection extends BaseRTC {
|
|
54
|
+
/** @type {((event: RTCPeerConnectionIceEvent) => void) | null} */
|
|
55
|
+
#iceHandler = null
|
|
56
|
+
/** @type {((event: { channel: RTCDataChannel }) => void) | null} */
|
|
57
|
+
#dataChannelHandler = null
|
|
58
|
+
/** @type {(() => void) | null} */
|
|
59
|
+
#connectionStateHandler = null
|
|
60
|
+
/** @type {Map<string, Set<(event: unknown) => void>>} */
|
|
61
|
+
#listeners = new Map()
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {RTCConfiguration} [config] RTC 配置
|
|
65
|
+
*/
|
|
66
|
+
constructor(config) {
|
|
67
|
+
super(config)
|
|
68
|
+
super.on('icecandidate', event => {
|
|
69
|
+
const normalized = this.prepareIceCandidateEvent(event)
|
|
70
|
+
if (normalized == null) return
|
|
71
|
+
this.#iceHandler?.(normalized)
|
|
72
|
+
this.#emit('icecandidate', normalized)
|
|
73
|
+
})
|
|
74
|
+
super.on('datachannel', event => {
|
|
75
|
+
const adapted = { channel: bridgeDataChannel(event.channel) }
|
|
76
|
+
this.#dataChannelHandler?.(adapted)
|
|
77
|
+
this.#emit('datachannel', adapted)
|
|
78
|
+
})
|
|
79
|
+
super.on('connectionstatechange', () => {
|
|
80
|
+
this.#connectionStateHandler?.()
|
|
81
|
+
this.#emit('connectionstatechange', undefined)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* ICE 事件规范化钩子;子类可覆盖(drop 返回 null,rewrite 返回替换后的事件)。
|
|
87
|
+
* @param {RTCPeerConnectionIceEvent | { candidate?: unknown }} event 原始 ICE 事件
|
|
88
|
+
* @returns {RTCPeerConnectionIceEvent | { candidate?: unknown } | null} 规范化后的事件
|
|
89
|
+
*/
|
|
90
|
+
prepareIceCandidateEvent(event) {
|
|
91
|
+
return event
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} type 事件名
|
|
96
|
+
* @param {unknown} event 事件载荷(icecandidate 须已规范化)
|
|
97
|
+
* @returns {void}
|
|
98
|
+
*/
|
|
99
|
+
#emit(type, event) {
|
|
100
|
+
const listeners = this.#listeners.get(type)
|
|
101
|
+
if (!listeners) return
|
|
102
|
+
for (const listener of listeners) listener(event)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} type 事件名
|
|
107
|
+
* @param {(event: unknown) => void} listener 回调
|
|
108
|
+
* @returns {void}
|
|
109
|
+
*/
|
|
110
|
+
addEventListener(type, listener) {
|
|
111
|
+
let listeners = this.#listeners.get(type)
|
|
112
|
+
if (!listeners) {
|
|
113
|
+
listeners = new Set()
|
|
114
|
+
this.#listeners.set(type, listeners)
|
|
115
|
+
}
|
|
116
|
+
listeners.add(listener)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} type 事件名
|
|
121
|
+
* @param {(event: unknown) => void} listener 回调
|
|
122
|
+
* @returns {void}
|
|
123
|
+
*/
|
|
124
|
+
removeEventListener(type, listener) {
|
|
125
|
+
this.#listeners.get(type)?.delete(listener)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @returns {((event: RTCPeerConnectionIceEvent) => void) | null} ICE candidate handler */
|
|
129
|
+
get onicecandidate() { return this.#iceHandler }
|
|
130
|
+
/** @param {((event: RTCPeerConnectionIceEvent) => void) | null} handler ICE candidate handler */
|
|
131
|
+
set onicecandidate(handler) { this.#iceHandler = handler }
|
|
132
|
+
|
|
133
|
+
/** @returns {((event: { channel: RTCDataChannel }) => void) | null} data channel handler */
|
|
134
|
+
get ondatachannel() { return this.#dataChannelHandler }
|
|
135
|
+
/** @param {((event: { channel: RTCDataChannel }) => void) | null} handler data channel handler */
|
|
136
|
+
set ondatachannel(handler) { this.#dataChannelHandler = handler }
|
|
137
|
+
|
|
138
|
+
/** @returns {(() => void) | null} connection state handler */
|
|
139
|
+
get onconnectionstatechange() { return this.#connectionStateHandler }
|
|
140
|
+
/** @param {(() => void) | null} handler connection state handler */
|
|
141
|
+
set onconnectionstatechange(handler) { this.#connectionStateHandler = handler }
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} label data channel 标签
|
|
145
|
+
* @param {RTCDataChannelInit} [init] 创建选项
|
|
146
|
+
* @returns {RTCDataChannel} 已桥接 W3C handler 的通道
|
|
147
|
+
*/
|
|
148
|
+
createDataChannel(label, init) {
|
|
149
|
+
return bridgeDataChannel(super.createDataChannel(label, init))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
package/mailbox/store.mjs
CHANGED
package/node/instance.mjs
CHANGED
|
@@ -22,6 +22,16 @@ let runtime = null
|
|
|
22
22
|
/** @type {Set<(event: string, payload?: unknown) => void>} */
|
|
23
23
|
const changeListeners = new Set()
|
|
24
24
|
|
|
25
|
+
/** RTC polyfill 缓存世代:策略变更时同步推进,避免动态 import 清缓存前仍命中旧构造器 */
|
|
26
|
+
let rtcPolyfillCacheEpoch = 0
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @returns {number} 当前 RTC polyfill 缓存世代
|
|
30
|
+
*/
|
|
31
|
+
export function getRtcPolyfillCacheEpoch() {
|
|
32
|
+
return rtcPolyfillCacheEpoch
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
/**
|
|
26
36
|
* @param {{ nodeDir: string, entityStore?: import('./entity_store.mjs').EntityStore }} options - 节点目录与可选 entity store
|
|
27
37
|
* @returns {NodeRuntime} 初始化后的运行时
|
|
@@ -72,7 +82,10 @@ export function setNodeLogger(logger) {
|
|
|
72
82
|
*/
|
|
73
83
|
export function setSignalingRuntimeConfig(config) {
|
|
74
84
|
if (!runtime) throw new Error('p2p: setSignalingRuntimeConfig requires initNode')
|
|
85
|
+
const previousPolicy = runtime.signaling.iceLocalHostnamePolicy
|
|
75
86
|
runtime.signaling = resolveSignalingRuntimeConfig({ ...runtime.signaling, ...config })
|
|
87
|
+
if (runtime.signaling.iceLocalHostnamePolicy !== previousPolicy)
|
|
88
|
+
rtcPolyfillCacheEpoch++
|
|
76
89
|
emitNodeChange('signaling-changed', runtime.signaling)
|
|
77
90
|
}
|
|
78
91
|
|
|
@@ -131,4 +144,5 @@ export function onNodeChange(listener) {
|
|
|
131
144
|
export function resetNodeForTests() {
|
|
132
145
|
runtime = null
|
|
133
146
|
changeListeners.clear()
|
|
147
|
+
rtcPolyfillCacheEpoch++
|
|
134
148
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steve02081504/fount-p2p",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@noble/curves": "latest",
|
|
74
|
-
"node-
|
|
74
|
+
"node-rtc-connection": "latest",
|
|
75
75
|
"on-shutdown": "latest",
|
|
76
76
|
"ws": "latest"
|
|
77
77
|
},
|
|
@@ -80,6 +80,10 @@
|
|
|
80
80
|
},
|
|
81
81
|
"optionalDependencies": {
|
|
82
82
|
"@stoprocent/bleno": "latest",
|
|
83
|
-
"@stoprocent/noble": "latest"
|
|
83
|
+
"@stoprocent/noble": "latest",
|
|
84
|
+
"node-datachannel": "latest"
|
|
85
|
+
},
|
|
86
|
+
"allowScripts": {
|
|
87
|
+
"node-datachannel": true
|
|
84
88
|
}
|
|
85
89
|
}
|
package/permissions/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 权限位掩码编解码(re-export)。
|
|
3
3
|
*/
|
|
4
4
|
export { createPermissionCodec } from './bitmask.mjs'
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* 角色 deny/allow 覆盖合并(re-export)。
|
|
7
7
|
*/
|
|
8
8
|
export { applyDenyAllowOverride, mergeRoleOverrides } from './layered.mjs'
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* 分层权限求值器(re-export)。
|
|
11
11
|
*/
|
|
12
12
|
export { createLayeredEvaluator } from './evaluator.mjs'
|
|
@@ -157,7 +157,7 @@ export function createGroupLinkSet(options) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
|
-
*
|
|
160
|
+
* 开启成员自动拨号与 roster 维护。
|
|
161
161
|
*/
|
|
162
162
|
function startAutoconnect() {
|
|
163
163
|
autoconnectEnabled = true
|
|
@@ -165,7 +165,7 @@ export function createGroupLinkSet(options) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
|
-
*
|
|
168
|
+
* 停止自动拨号并清除待执行的拨号定时器。
|
|
169
169
|
*/
|
|
170
170
|
function stopAutoconnect() {
|
|
171
171
|
autoconnectEnabled = false
|
|
@@ -242,7 +242,7 @@ export function createRuntimeBootstrap(deps) {
|
|
|
242
242
|
if (typeof stop === 'function' && generation === gen && isLive()) {
|
|
243
243
|
const prev = stopPresence
|
|
244
244
|
/**
|
|
245
|
-
*
|
|
245
|
+
* 停止 presence 广播并链式调用上一轮清理。
|
|
246
246
|
*/
|
|
247
247
|
stopPresence = () => { try { stop() } catch { /* ignore */ }; prev?.() }
|
|
248
248
|
}
|
|
@@ -256,7 +256,7 @@ export function createRuntimeBootstrap(deps) {
|
|
|
256
256
|
if (typeof stop === 'function' && generation === gen && isLive()) {
|
|
257
257
|
const prev = stopSignalListener
|
|
258
258
|
/**
|
|
259
|
-
*
|
|
259
|
+
* 停止信令监听并链式调用上一轮清理。
|
|
260
260
|
*/
|
|
261
261
|
stopSignalListener = () => { try { stop() } catch { /* ignore */ }; prev?.() }
|
|
262
262
|
}
|