@steve02081504/fount-p2p 0.0.11 → 0.0.13
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 +42 -11
- package/core/bytes_codec.mjs +65 -10
- package/core/composite_key.mjs +5 -5
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +1 -1
- package/discovery/adverts.mjs +109 -0
- package/discovery/bt/index.mjs +154 -113
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +5 -12
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +430 -141
- package/federation/chunk_fetch_pending.mjs +14 -18
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/federation/topo_order_memo.mjs +11 -4
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_fetch.mjs +2 -2
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +31 -30
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -131
- package/link/handshake.mjs +19 -20
- package/link/pipe.mjs +53 -79
- package/link/providers/ble_gatt.mjs +19 -31
- package/link/providers/lan_tcp.mjs +17 -32
- package/link/providers/link_id_pipe.mjs +46 -0
- package/link/providers/webrtc.mjs +42 -51
- package/link/rtc.mjs +24 -12
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +6 -6
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/identity.mjs +4 -4
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +46 -14
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +35 -14
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -182
- package/schemas/discovery.mjs +1 -2
- package/schemas/federation_pull.mjs +2 -2
- package/schemas/part_query.mjs +1 -1
- package/schemas/remote_event.mjs +1 -1
- package/transport/advert_ingest.mjs +17 -0
- package/transport/group_link_set.mjs +165 -134
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +283 -124
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +48 -47
- package/transport/peer_pool.mjs +169 -78
- package/transport/remote_user_room.mjs +18 -23
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +172 -104
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +93 -161
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +1 -4
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +11 -0
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +38 -36
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +28 -23
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
- package/discovery/mdns.mjs +0 -191
- package/transport/signal_crypto.mjs +0 -82
|
@@ -1,40 +1,44 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { advertiseTopic, subscribeTopic } from '../discovery/index.mjs'
|
|
3
|
-
import { verifySignedAdvert } from '../link/handshake.mjs'
|
|
1
|
+
import { listVisibleNodeHashes, startGroupPresence, watchVerifiedGroupAdverts } from '../discovery/index.mjs'
|
|
4
2
|
import { loadPeerPoolView } from '../node/network.mjs'
|
|
5
3
|
import { loadReputation } from '../node/reputation_store.mjs'
|
|
4
|
+
import { emitSafe } from '../utils/emit_safe.mjs'
|
|
6
5
|
|
|
6
|
+
import { applyAdvertPeerHints } from './advert_ingest.mjs'
|
|
7
7
|
import { getLinkRegistry } from './link_registry.mjs'
|
|
8
8
|
import { resolveFederationPoolLimits, selectLinkTargetsFromMembers } from './peer_pool.mjs'
|
|
9
|
-
import {
|
|
10
|
-
decryptSignalPacket,
|
|
11
|
-
encryptSignalPacket,
|
|
12
|
-
groupRendezvousTopic,
|
|
13
|
-
} from './signal_crypto.mjs'
|
|
14
|
-
|
|
9
|
+
import { loadTransportTunables } from './tunables.mjs'
|
|
15
10
|
|
|
16
11
|
/**
|
|
17
|
-
* 创建基于 link registry
|
|
12
|
+
* 创建基于 link registry 的群组联邦房间(唯一内核;scoped_link 为其薄预设)。
|
|
18
13
|
* @param {object} options 选项
|
|
19
|
-
* @param {string} options.groupId
|
|
20
|
-
* @param {string} options.
|
|
21
|
-
* @param {string
|
|
22
|
-
* @param {
|
|
23
|
-
* @
|
|
14
|
+
* @param {string} options.groupId 群 ID
|
|
15
|
+
* @param {string} [options.scope] scope 前缀(默认 `group:${groupId}`)
|
|
16
|
+
* @param {string} [options.roomSecret] 群 discovery / advert 密钥
|
|
17
|
+
* @param {string[]} [options.members] 初始成员 nodeHash
|
|
18
|
+
* @param {(nodeHash: string) => boolean} [options.allowNode] 入站成员过滤
|
|
19
|
+
* @param {boolean} [options.dialAll=false] true 时拨号全部成员,否则按 mesh 策略
|
|
20
|
+
* @param {boolean} [options.autoconnect=true] start 时是否自动拨号
|
|
21
|
+
* @param {object} [options.groupSettings] 群设置透传
|
|
22
|
+
* @param {object} [options.registry] link registry(默认进程单例)
|
|
23
|
+
* @returns {object} 群组 link set 接口
|
|
24
24
|
*/
|
|
25
25
|
export function createGroupLinkSet(options) {
|
|
26
26
|
const registry = options.registry ?? getLinkRegistry()
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
27
|
+
let autoconnectEnabled = false
|
|
28
|
+
const startWithAutoconnect = options.autoconnect !== false
|
|
29
|
+
const dialAll = options.dialAll === true
|
|
30
|
+
const allowNode = options.allowNode ?? (() => true)
|
|
31
|
+
const { groupId } = options
|
|
32
|
+
const scope = options.scope ?? `group:${groupId}`
|
|
33
|
+
const roomSecret = options.roomSecret
|
|
34
|
+
const members = new Set(options.members || [])
|
|
32
35
|
const selfNodeHash = registry.localIdentity.nodeHash
|
|
33
36
|
const groupSettings = options.groupSettings ?? {}
|
|
34
|
-
// 初始成员是调用方明确知道的引导集合(如 introducer/creator/seed),作为必连锚点保证引导期连通。
|
|
35
37
|
const initialAnchors = new Set(members)
|
|
36
38
|
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
37
39
|
let dialTimer = null
|
|
40
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
41
|
+
let scanTimer = null
|
|
38
42
|
/** @type {Set<Function>} */
|
|
39
43
|
const cleanups = new Set()
|
|
40
44
|
/** @type {Set<Function>} */
|
|
@@ -48,9 +52,9 @@ export function createGroupLinkSet(options) {
|
|
|
48
52
|
/** @type {Map<string, { handler: ((payload: unknown, peerId: string) => void) | null, backlog: Array<{ payload: unknown, peerId: string }> }>} */
|
|
49
53
|
const actionEntries = new Map()
|
|
50
54
|
let active = true
|
|
55
|
+
let started = false
|
|
51
56
|
|
|
52
57
|
/**
|
|
53
|
-
* 注册 leave 时执行的清理回调。
|
|
54
58
|
* @param {() => void} cleanup 清理函数
|
|
55
59
|
* @returns {void}
|
|
56
60
|
*/
|
|
@@ -60,118 +64,131 @@ export function createGroupLinkSet(options) {
|
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
|
-
*
|
|
64
|
-
* @param {Set<(peerId: string) => void>} listeners 监听器集合
|
|
65
|
-
* @param {string} peerId 目标 peer id
|
|
66
|
-
* @returns {void}
|
|
67
|
-
*/
|
|
68
|
-
function emitPeerListeners(listeners, peerId) {
|
|
69
|
-
for (const listener of listeners)
|
|
70
|
-
try { listener(peerId) } catch { /* ignore */ }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 记录 peer 加入并通知监听器。
|
|
75
|
-
* @param {string} peerId 加入的 peer id
|
|
67
|
+
* @param {string} peerId peer id
|
|
76
68
|
* @returns {void}
|
|
77
69
|
*/
|
|
78
70
|
function notePeerJoin(peerId) {
|
|
79
|
-
if (!peerId || announcedPeers.has(peerId)) return
|
|
71
|
+
if (!peerId || !allowNode(peerId) || announcedPeers.has(peerId)) return
|
|
80
72
|
announcedPeers.add(peerId)
|
|
81
|
-
|
|
73
|
+
emitSafe(peerJoinListeners, peerId)
|
|
82
74
|
}
|
|
83
75
|
|
|
84
76
|
/**
|
|
85
|
-
*
|
|
86
|
-
* @param {string} peerId 离开的 peer id
|
|
77
|
+
* @param {string} peerId peer id
|
|
87
78
|
* @returns {void}
|
|
88
79
|
*/
|
|
89
80
|
function notePeerLeave(peerId) {
|
|
90
81
|
if (!peerId || !announcedPeers.has(peerId)) return
|
|
91
82
|
announcedPeers.delete(peerId)
|
|
92
|
-
|
|
83
|
+
emitSafe(peerLeaveListeners, peerId)
|
|
93
84
|
}
|
|
94
85
|
|
|
95
86
|
/**
|
|
96
|
-
*
|
|
97
|
-
* @param {string} nodeHash 候选节点 64 hex
|
|
87
|
+
* @param {string} nodeHash 候选节点
|
|
98
88
|
* @returns {void}
|
|
99
89
|
*/
|
|
100
90
|
function notePeerCandidate(nodeHash) {
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
91
|
+
if (!nodeHash || nodeHash === selfNodeHash || !allowNode(nodeHash)) return
|
|
92
|
+
if (members.has(nodeHash)) {
|
|
93
|
+
if (registry.getLink(nodeHash)) notePeerJoin(nodeHash)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
members.add(nodeHash)
|
|
104
97
|
registry.registerScopeInterest(scope, [...members])
|
|
105
|
-
|
|
106
|
-
// onLinkUp 当时因 !members.has 被跳过,这里补发 peer-join,触发 bootstrap flush(member_join 自证推送)。
|
|
107
|
-
if (registry.getLink(normalized)) notePeerJoin(normalized)
|
|
98
|
+
if (registry.getLink(nodeHash)) notePeerJoin(nodeHash)
|
|
108
99
|
scheduleDial()
|
|
109
100
|
}
|
|
110
101
|
|
|
111
102
|
/**
|
|
112
|
-
* 依信任稀疏池从当前成员选出建链目标(top-K 信任 + 随机 explore + 初始锚点必连),拨号未连接者。
|
|
113
|
-
* 不主动断连(超预算由 registry 全局 trimToBudget 兜底)。
|
|
114
103
|
* @returns {void}
|
|
115
104
|
*/
|
|
116
105
|
function selectAndDial() {
|
|
117
|
-
if (!
|
|
118
|
-
const targets =
|
|
119
|
-
members
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
106
|
+
if (!autoconnectEnabled || !active) return
|
|
107
|
+
const targets = dialAll
|
|
108
|
+
? [...members].filter(nodeHash => nodeHash !== selfNodeHash && allowNode(nodeHash))
|
|
109
|
+
: selectLinkTargetsFromMembers({
|
|
110
|
+
members,
|
|
111
|
+
selfNodeHash,
|
|
112
|
+
rep: loadReputation(),
|
|
113
|
+
peers: loadPeerPoolView(groupId),
|
|
114
|
+
limits: resolveFederationPoolLimits(groupSettings),
|
|
115
|
+
anchors: initialAnchors,
|
|
116
|
+
}).filter(allowNode)
|
|
126
117
|
for (const nodeHash of targets)
|
|
127
118
|
if (nodeHash !== selfNodeHash && !registry.getLink(nodeHash))
|
|
128
119
|
void registry.ensureLinkToNode(nodeHash).catch(() => null)
|
|
129
120
|
}
|
|
130
121
|
|
|
131
122
|
/**
|
|
132
|
-
* 去抖触发一次建链目标重算(成员变化时调用,避免频繁重算)。
|
|
133
123
|
* @returns {void}
|
|
134
124
|
*/
|
|
135
125
|
function scheduleDial() {
|
|
136
|
-
if (!
|
|
126
|
+
if (!autoconnectEnabled || !active || dialTimer) return
|
|
137
127
|
dialTimer = setTimeout(() => { dialTimer = null; selectAndDial() }, 200)
|
|
138
128
|
}
|
|
139
129
|
|
|
140
130
|
/**
|
|
141
|
-
*
|
|
142
|
-
|
|
143
|
-
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
async function scanVisibleMembers() {
|
|
134
|
+
const tunables = loadTransportTunables()
|
|
135
|
+
const limit = Math.max(8, Number(tunables.groupMemberScanLimit ?? tunables.meshScanLimit) || 64)
|
|
136
|
+
for (const hash of await listVisibleNodeHashes({ roomSecret, limit }))
|
|
137
|
+
notePeerCandidate(hash)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} name action 名
|
|
142
|
+
* @returns {object} action 表项
|
|
144
143
|
*/
|
|
145
144
|
function getActionEntry(name) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return actionEntries.get(key)
|
|
145
|
+
if (!actionEntries.has(name))
|
|
146
|
+
actionEntries.set(name, { handler: null, backlog: [] })
|
|
147
|
+
return actionEntries.get(name)
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
/**
|
|
153
|
-
*
|
|
154
|
-
* @returns {Array<{ peerId: string, remoteNodeHash: string }>} 在线成员列表
|
|
151
|
+
* @returns {Array<{ peerId: string, remoteNodeHash: string }>} 当前在线 roster
|
|
155
152
|
*/
|
|
156
153
|
function activeRoster() {
|
|
157
154
|
return [...members]
|
|
158
|
-
.filter(nodeHash => nodeHash !== selfNodeHash && registry.getLink(nodeHash))
|
|
155
|
+
.filter(nodeHash => nodeHash !== selfNodeHash && allowNode(nodeHash) && registry.getLink(nodeHash))
|
|
159
156
|
.map(nodeHash => ({ peerId: nodeHash, remoteNodeHash: nodeHash }))
|
|
160
157
|
}
|
|
161
158
|
|
|
162
159
|
/**
|
|
163
|
-
*
|
|
160
|
+
*
|
|
161
|
+
*/
|
|
162
|
+
function startAutoconnect() {
|
|
163
|
+
autoconnectEnabled = true
|
|
164
|
+
selectAndDial()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
*
|
|
169
|
+
*/
|
|
170
|
+
function stopAutoconnect() {
|
|
171
|
+
autoconnectEnabled = false
|
|
172
|
+
if (dialTimer) { clearTimeout(dialTimer); dialTimer = null }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
164
176
|
* @returns {Promise<void>}
|
|
165
177
|
*/
|
|
166
178
|
async function start() {
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
if (started) {
|
|
180
|
+
if (startWithAutoconnect) startAutoconnect()
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
started = true
|
|
184
|
+
active = true
|
|
169
185
|
if (typeof registry.ensureRuntime === 'function')
|
|
170
186
|
await registry.ensureRuntime()
|
|
171
187
|
registry.registerScopeInterest(scope, [...members])
|
|
172
188
|
registerCleanup(registry.subscribeScope(scope, (senderNodeHash, envelope) => {
|
|
189
|
+
if (!allowNode(senderNodeHash)) return
|
|
173
190
|
notePeerCandidate(senderNodeHash)
|
|
174
|
-
const entry = actionEntries.get(
|
|
191
|
+
const entry = actionEntries.get(envelope.action)
|
|
175
192
|
if (entry)
|
|
176
193
|
if (entry.handler) entry.handler(envelope.payload, senderNodeHash)
|
|
177
194
|
else entry.backlog.push({ payload: envelope.payload, peerId: senderNodeHash })
|
|
@@ -180,27 +197,34 @@ export function createGroupLinkSet(options) {
|
|
|
180
197
|
listener(senderNodeHash, envelope)
|
|
181
198
|
}))
|
|
182
199
|
registerCleanup(registry.onLinkUp(nodeHash => {
|
|
183
|
-
if (!members.has(nodeHash) || nodeHash === selfNodeHash) return
|
|
200
|
+
if (!members.has(nodeHash) || nodeHash === selfNodeHash || !allowNode(nodeHash)) return
|
|
184
201
|
notePeerJoin(nodeHash)
|
|
185
202
|
}))
|
|
186
203
|
registerCleanup(registry.onLinkDown(nodeHash => {
|
|
187
204
|
if (!members.has(nodeHash) || nodeHash === selfNodeHash) return
|
|
188
205
|
notePeerLeave(nodeHash)
|
|
189
206
|
}))
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
noteAdvertPeerHints(verifiedNodeHash, packet.body, meta)
|
|
196
|
-
// 发现新成员:并入成员集并去抖重算稀疏建链目标(notePeerCandidate 内部会 scheduleDial)。
|
|
207
|
+
|
|
208
|
+
registerCleanup(await watchVerifiedGroupAdverts(roomSecret, async (verifiedNodeHash, body, meta) => {
|
|
209
|
+
if (verifiedNodeHash === selfNodeHash) return
|
|
210
|
+
if (!allowNode(verifiedNodeHash)) return
|
|
211
|
+
applyAdvertPeerHints(verifiedNodeHash, body, meta)
|
|
197
212
|
notePeerCandidate(verifiedNodeHash)
|
|
198
213
|
}))
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
214
|
+
|
|
215
|
+
registerCleanup(await startGroupPresence(roomSecret, async () => ({
|
|
216
|
+
nodeHash: selfNodeHash,
|
|
217
|
+
advertBody: await registry.buildLocalAdvert({ roomSecret }),
|
|
202
218
|
})))
|
|
203
|
-
|
|
219
|
+
|
|
220
|
+
await scanVisibleMembers()
|
|
221
|
+
const scanMs = Math.max(5_000, Number(loadTransportTunables().groupMemberScanIntervalMs) || 30_000)
|
|
222
|
+
scanTimer = setInterval(() => { void scanVisibleMembers().catch(() => { }) }, scanMs)
|
|
223
|
+
registerCleanup(() => {
|
|
224
|
+
if (scanTimer) { clearInterval(scanTimer); scanTimer = null }
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
if (startWithAutoconnect) startAutoconnect()
|
|
204
228
|
for (const { peerId } of activeRoster())
|
|
205
229
|
notePeerJoin(peerId)
|
|
206
230
|
}
|
|
@@ -209,14 +233,16 @@ export function createGroupLinkSet(options) {
|
|
|
209
233
|
groupId,
|
|
210
234
|
scope,
|
|
211
235
|
start,
|
|
236
|
+
startAutoconnect,
|
|
237
|
+
stopAutoconnect,
|
|
212
238
|
/**
|
|
213
|
-
* 停止房间并执行所有已注册清理。
|
|
214
239
|
* @returns {Promise<void>}
|
|
215
240
|
*/
|
|
216
241
|
async leave() {
|
|
217
|
-
if (!active) return
|
|
242
|
+
if (!active && !started) return
|
|
218
243
|
active = false
|
|
219
|
-
|
|
244
|
+
started = false
|
|
245
|
+
stopAutoconnect()
|
|
220
246
|
registry.releaseScopeInterest(scope)
|
|
221
247
|
for (const cleanup of cleanups)
|
|
222
248
|
try { cleanup() } catch { /* ignore */ }
|
|
@@ -224,92 +250,98 @@ export function createGroupLinkSet(options) {
|
|
|
224
250
|
},
|
|
225
251
|
getRoster: activeRoster,
|
|
226
252
|
/**
|
|
227
|
-
*
|
|
228
|
-
* @
|
|
229
|
-
* @returns {string | null} 对端 id;无链路时为 null
|
|
253
|
+
* @param {string} nodeHash 远端节点 hash
|
|
254
|
+
* @returns {string | null} 已建链时返回 peerId,否则 null
|
|
230
255
|
*/
|
|
231
256
|
getPeerIdByNodeHash(nodeHash) {
|
|
232
|
-
return registry.getLink(nodeHash) ?
|
|
257
|
+
return registry.getLink(nodeHash) ? nodeHash : null
|
|
233
258
|
},
|
|
234
259
|
/**
|
|
235
|
-
*
|
|
236
|
-
* @param {string}
|
|
237
|
-
* @param {string} actionName action 名称
|
|
260
|
+
* @param {string} peerId 目标 peer
|
|
261
|
+
* @param {string} actionName scope action 名
|
|
238
262
|
* @param {unknown} payload 载荷
|
|
239
|
-
* @returns {Promise<boolean>}
|
|
263
|
+
* @returns {Promise<boolean>} 发送是否成功
|
|
240
264
|
*/
|
|
241
265
|
async sendToPeer(peerId, actionName, payload) {
|
|
242
|
-
|
|
266
|
+
if (!allowNode(peerId)) return false
|
|
267
|
+
return await registry.sendToNodeLink(peerId, { scope, action: actionName, payload })
|
|
268
|
+
},
|
|
269
|
+
/**
|
|
270
|
+
* @param {string} actionName action 名
|
|
271
|
+
* @param {(payload: unknown, peerId: string) => void} handler 回调
|
|
272
|
+
* @returns {() => void} 取消订阅
|
|
273
|
+
*/
|
|
274
|
+
onAction(actionName, handler) {
|
|
275
|
+
const entry = getActionEntry(actionName)
|
|
276
|
+
entry.handler = handler
|
|
277
|
+
for (const pending of entry.backlog.splice(0))
|
|
278
|
+
handler(pending.payload, pending.peerId)
|
|
279
|
+
return () => {
|
|
280
|
+
if (entry.handler === handler) entry.handler = null
|
|
281
|
+
}
|
|
243
282
|
},
|
|
244
283
|
/**
|
|
245
|
-
*
|
|
246
|
-
* @param {string} actionName action 名称
|
|
284
|
+
* @param {string} actionName action 名
|
|
247
285
|
* @param {unknown} payload 载荷
|
|
248
|
-
* @param {string | null} [peerId]
|
|
249
|
-
* @returns {Promise<number>}
|
|
286
|
+
* @param {string | null} [peerId] 单播目标;省略则广播 roster
|
|
287
|
+
* @returns {Promise<number>} 成功发送数
|
|
250
288
|
*/
|
|
251
289
|
async send(actionName, payload, peerId = null) {
|
|
252
290
|
if (peerId) return await this.sendToPeer(peerId, actionName, payload) ? 1 : 0
|
|
253
291
|
let sent = 0
|
|
254
292
|
for (const { peerId: targetPeerId } of activeRoster())
|
|
255
|
-
if (await registry.sendToNodeLink(targetPeerId, { scope, action:
|
|
293
|
+
if (await registry.sendToNodeLink(targetPeerId, { scope, action: actionName, payload })) sent++
|
|
256
294
|
return sent
|
|
257
295
|
},
|
|
258
296
|
/**
|
|
259
|
-
*
|
|
260
|
-
* @
|
|
261
|
-
* @returns {() => void} 取消订阅函数
|
|
297
|
+
* @param {(senderNodeHash: string, envelope: object) => void} listener scope envelope 回调
|
|
298
|
+
* @returns {() => void} 取消订阅
|
|
262
299
|
*/
|
|
263
|
-
onEnvelope(
|
|
264
|
-
envelopeListeners.add(
|
|
265
|
-
return () => envelopeListeners.delete(
|
|
300
|
+
onEnvelope(listener) {
|
|
301
|
+
envelopeListeners.add(listener)
|
|
302
|
+
return () => envelopeListeners.delete(listener)
|
|
266
303
|
},
|
|
267
304
|
/**
|
|
268
|
-
*
|
|
269
|
-
* @
|
|
270
|
-
* @returns {() => void} 取消订阅函数
|
|
305
|
+
* @param {(peerId: string) => void} listener peer 加入回调
|
|
306
|
+
* @returns {() => void} 取消订阅
|
|
271
307
|
*/
|
|
272
|
-
onPeerJoin(
|
|
273
|
-
peerJoinListeners.add(
|
|
308
|
+
onPeerJoin(listener) {
|
|
309
|
+
peerJoinListeners.add(listener)
|
|
274
310
|
for (const { peerId } of activeRoster())
|
|
275
311
|
if (peerId) announcedPeers.add(peerId)
|
|
276
312
|
for (const peerId of announcedPeers)
|
|
277
|
-
try {
|
|
278
|
-
return () => peerJoinListeners.delete(
|
|
313
|
+
try { listener(peerId) } catch { /* ignore */ }
|
|
314
|
+
return () => peerJoinListeners.delete(listener)
|
|
279
315
|
},
|
|
280
316
|
/**
|
|
281
|
-
*
|
|
282
|
-
* @
|
|
283
|
-
* @returns {() => void} 取消订阅函数
|
|
317
|
+
* @param {(peerId: string) => void} listener peer 离开回调
|
|
318
|
+
* @returns {() => void} 取消订阅
|
|
284
319
|
*/
|
|
285
|
-
onPeerLeave(
|
|
286
|
-
peerLeaveListeners.add(
|
|
287
|
-
return () => peerLeaveListeners.delete(
|
|
320
|
+
onPeerLeave(listener) {
|
|
321
|
+
peerLeaveListeners.add(listener)
|
|
322
|
+
return () => peerLeaveListeners.delete(listener)
|
|
288
323
|
},
|
|
289
324
|
/**
|
|
290
|
-
*
|
|
291
|
-
* @returns {Record<string, true>} peerId → true
|
|
325
|
+
* @returns {Record<string, true>} 当前在线 peer 表
|
|
292
326
|
*/
|
|
293
327
|
getPeers() {
|
|
294
328
|
return Object.fromEntries(activeRoster().map(({ peerId }) => [peerId, true]))
|
|
295
329
|
},
|
|
296
330
|
/**
|
|
297
|
-
*
|
|
298
|
-
* @
|
|
299
|
-
* @returns {[(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void]} send 与 on 函数对
|
|
331
|
+
* @param {string} name action 名
|
|
332
|
+
* @returns {[Function, Function]} [send, onHandler] 元组:发送函数与注册 handler 函数
|
|
300
333
|
*/
|
|
301
334
|
makeAction(name) {
|
|
302
|
-
const actionName = String(name)
|
|
303
335
|
return [
|
|
304
336
|
async (payload, peerId = null) => {
|
|
305
337
|
if (Array.isArray(peerId)) {
|
|
306
|
-
await Promise.all(peerId.map(targetPeerId => this.sendToPeer(targetPeerId,
|
|
338
|
+
await Promise.all(peerId.map(targetPeerId => this.sendToPeer(targetPeerId, name, payload)))
|
|
307
339
|
return
|
|
308
340
|
}
|
|
309
|
-
await this.send(
|
|
341
|
+
await this.send(name, payload, peerId)
|
|
310
342
|
},
|
|
311
343
|
handler => {
|
|
312
|
-
const entry = getActionEntry(
|
|
344
|
+
const entry = getActionEntry(name)
|
|
313
345
|
entry.handler = handler
|
|
314
346
|
for (const pending of entry.backlog.splice(0))
|
|
315
347
|
handler(pending.payload, pending.peerId)
|
|
@@ -318,8 +350,7 @@ export function createGroupLinkSet(options) {
|
|
|
318
350
|
},
|
|
319
351
|
registerCleanup,
|
|
320
352
|
/**
|
|
321
|
-
*
|
|
322
|
-
* @returns {boolean} 是否活跃
|
|
353
|
+
* @returns {boolean} 群 link set 是否仍活跃
|
|
323
354
|
*/
|
|
324
355
|
isActive() { return active },
|
|
325
356
|
}
|
|
@@ -46,7 +46,7 @@ function normalizeIceEntry(raw) {
|
|
|
46
46
|
* @returns {{ urls: string, username?: string, credential?: string }[]} 合法 ICE 列表
|
|
47
47
|
*/
|
|
48
48
|
export function resolveIceServers(groupSettings) {
|
|
49
|
-
const fromSettings =
|
|
49
|
+
const fromSettings = groupSettings?.iceServers || []
|
|
50
50
|
const out = []
|
|
51
51
|
for (const raw of fromSettings) {
|
|
52
52
|
const entry = normalizeIceEntry(raw)
|
|
@@ -62,7 +62,7 @@ export function resolveIceServers(groupSettings) {
|
|
|
62
62
|
* @returns {{ urls: string, username?: string, credential?: string }[]} 校验后的 ICE 列表
|
|
63
63
|
*/
|
|
64
64
|
export function sanitizeIceServersForSettings(raw) {
|
|
65
|
-
if (!
|
|
65
|
+
if (!raw?.length) return [...DEFAULT_ICE_SERVERS]
|
|
66
66
|
const out = []
|
|
67
67
|
for (const item of raw) {
|
|
68
68
|
const entry = normalizeIceEntry(item)
|