@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.
Files changed (102) hide show
  1. package/README.md +42 -11
  2. package/core/bytes_codec.mjs +65 -10
  3. package/core/composite_key.mjs +5 -5
  4. package/core/tcp_port.mjs +1 -1
  5. package/crypto/checkpoint_sign.mjs +2 -2
  6. package/crypto/crypto.mjs +3 -3
  7. package/crypto/key.mjs +12 -12
  8. package/dag/canonicalize_row.mjs +3 -3
  9. package/dag/index.mjs +2 -3
  10. package/dag/storage.mjs +36 -36
  11. package/discovery/advert_peer_hints.mjs +1 -1
  12. package/discovery/adverts.mjs +109 -0
  13. package/discovery/bt/index.mjs +154 -113
  14. package/discovery/bt/probe_child.mjs +2 -1
  15. package/discovery/bt/runtime.mjs +5 -12
  16. package/discovery/index.mjs +267 -62
  17. package/discovery/internal/signal_crypto.mjs +109 -0
  18. package/discovery/lan.mjs +228 -0
  19. package/discovery/nostr.mjs +430 -141
  20. package/federation/chunk_fetch_pending.mjs +14 -18
  21. package/federation/dag_order_cache.mjs +1 -1
  22. package/federation/entity_key_chain.mjs +3 -4
  23. package/federation/manifest_fetch_pending.mjs +1 -3
  24. package/federation/topo_order_memo.mjs +11 -4
  25. package/files/assemble.mjs +14 -14
  26. package/files/assemble_stream.mjs +6 -6
  27. package/files/chunk_fetch.mjs +2 -2
  28. package/files/chunk_responder.mjs +29 -20
  29. package/files/evfs.mjs +31 -30
  30. package/files/manifest_fetch.mjs +16 -3
  31. package/files/public_manifest.mjs +11 -11
  32. package/files/transfer_key_registry.mjs +2 -2
  33. package/index.mjs +86 -11
  34. package/infra/cli.mjs +62 -0
  35. package/infra/debug_log.mjs +56 -0
  36. package/infra/default_node_dir.mjs +22 -0
  37. package/infra/priority.mjs +56 -0
  38. package/infra/service.mjs +140 -0
  39. package/infra/tunables.json +5 -0
  40. package/link/channel_mux.mjs +10 -10
  41. package/link/frame.mjs +76 -131
  42. package/link/handshake.mjs +19 -20
  43. package/link/pipe.mjs +53 -79
  44. package/link/providers/ble_gatt.mjs +19 -31
  45. package/link/providers/lan_tcp.mjs +17 -32
  46. package/link/providers/link_id_pipe.mjs +46 -0
  47. package/link/providers/webrtc.mjs +42 -51
  48. package/link/rtc.mjs +24 -12
  49. package/mailbox/consumer_registry.mjs +2 -2
  50. package/mailbox/deliver_or_store.mjs +6 -6
  51. package/mailbox/wire.mjs +28 -24
  52. package/node/entity_store.mjs +6 -6
  53. package/node/identity.mjs +4 -4
  54. package/node/instance.mjs +46 -27
  55. package/node/local_data_revision.mjs +26 -0
  56. package/node/log.mjs +56 -0
  57. package/node/network.mjs +46 -14
  58. package/node/reputation_store.mjs +4 -4
  59. package/node/reputation_sync.mjs +318 -0
  60. package/node/routing_profile.mjs +21 -0
  61. package/node/signaling_config.mjs +30 -11
  62. package/overlay/index.mjs +35 -14
  63. package/package.json +13 -2
  64. package/rooms/scoped_link.mjs +17 -182
  65. package/schemas/discovery.mjs +1 -2
  66. package/schemas/federation_pull.mjs +2 -2
  67. package/schemas/part_query.mjs +1 -1
  68. package/schemas/remote_event.mjs +1 -1
  69. package/transport/advert_ingest.mjs +17 -0
  70. package/transport/group_link_set.mjs +165 -134
  71. package/transport/ice_servers.mjs +2 -2
  72. package/transport/link_registry.mjs +283 -124
  73. package/transport/mesh_keepalive.mjs +217 -0
  74. package/transport/node_scope.mjs +289 -0
  75. package/transport/offer_answer.mjs +48 -47
  76. package/transport/peer_pool.mjs +169 -78
  77. package/transport/remote_user_room.mjs +18 -23
  78. package/transport/rtc_connection_budget.mjs +18 -4
  79. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  80. package/transport/runtime_bootstrap.mjs +172 -104
  81. package/transport/tunables.json +11 -0
  82. package/transport/tunables.mjs +18 -0
  83. package/transport/user_room.mjs +93 -161
  84. package/trust_graph/build.mjs +0 -2
  85. package/trust_graph/cache.mjs +12 -3
  86. package/trust_graph/registry.mjs +6 -6
  87. package/trust_graph/send.mjs +1 -4
  88. package/utils/async_mutex.mjs +4 -4
  89. package/utils/emit_safe.mjs +11 -0
  90. package/utils/json_io.mjs +12 -12
  91. package/utils/map_pool.mjs +5 -5
  92. package/utils/shuffle.mjs +13 -0
  93. package/utils/ttl_map.mjs +29 -4
  94. package/wire/ingress.mjs +1 -1
  95. package/wire/part_ingress.mjs +38 -36
  96. package/wire/part_invoke.mjs +4 -4
  97. package/wire/part_query.mjs +28 -23
  98. package/wire/part_query.tunables.json +6 -1
  99. package/wire/part_query_cache.mjs +1 -1
  100. package/wire/volatile_signature.mjs +1 -1
  101. package/discovery/mdns.mjs +0 -191
  102. package/transport/signal_crypto.mjs +0 -82
@@ -1,40 +1,44 @@
1
- import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
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 群组 id
20
- * @param {string} options.roomSecret 房间密钥(用于 rendezvous topic)
21
- * @param {string[]} options.members 初始成员 nodeHash 列表
22
- * @param {object} [options.groupSettings] 群设置(连接预算:trustedPeerSlots/explorePeerSlots/maxPeers 等)
23
- * @returns {{ groupId: string, scope: string, start: () => Promise<void>, leave: () => Promise<void>, getRoster: () => Array<{ peerId: string, remoteNodeHash: string }>, getPeerIdByNodeHash: (nodeHash: string) => string | null, sendToPeer: (peerId: string, actionName: string, payload: unknown) => Promise<boolean>, send: (actionName: string, payload: unknown, peerId?: string | null) => Promise<number>, onEnvelope: (callback: (senderNodeHash: string, envelope: object) => void) => () => void, onPeerJoin: (callback: (peerId: string) => void) => () => void, onPeerLeave: (callback: (peerId: string) => void) => () => void, getPeers: () => Record<string, true>, makeAction: (name: string) => [(payload: unknown, peerId?: string | string[] | null) => Promise<void>, (handler: (payload: unknown, peerId: string) => void) => void], registerCleanup: (cleanup: () => void) => void, isActive: () => boolean }} 群组 link set 接口
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
- const autoconnect = options.autoconnect !== false
28
- const groupId = String(options.groupId)
29
- const scope = `group:${groupId}`
30
- const topic = groupRendezvousTopic(options.roomSecret)
31
- const members = new Set((Array.isArray(options.members) ? options.members : []).map(String))
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
- * peer 事件监听器集合广播。
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
- emitPeerListeners(peerJoinListeners, peerId)
73
+ emitSafe(peerJoinListeners, peerId)
82
74
  }
83
75
 
84
76
  /**
85
- * 记录 peer 离开并通知监听器。
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
- emitPeerListeners(peerLeaveListeners, peerId)
83
+ emitSafe(peerLeaveListeners, peerId)
93
84
  }
94
85
 
95
86
  /**
96
- * 将新发现的 nodeHash 加入成员集合并更新 scope 兴趣。
97
- * @param {string} nodeHash 候选节点 64 hex
87
+ * @param {string} nodeHash 候选节点
98
88
  * @returns {void}
99
89
  */
100
90
  function notePeerCandidate(nodeHash) {
101
- const normalized = String(nodeHash || '')
102
- if (!normalized || normalized === selfNodeHash || members.has(normalized)) return
103
- members.add(normalized)
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
- // 若链路先于"得知其群成员身份"建立(如 warmup/连节点先连上,之后才收到其群 advert/envelope),
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 (!autoconnect || !active) return
118
- const targets = selectLinkTargetsFromMembers({
119
- members,
120
- selfNodeHash,
121
- rep: loadReputation(),
122
- peers: loadPeerPoolView(groupId),
123
- limits: resolveFederationPoolLimits(groupSettings),
124
- anchors: initialAnchors,
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 (!autoconnect || !active || dialTimer) return
126
+ if (!autoconnectEnabled || !active || dialTimer) return
137
127
  dialTimer = setTimeout(() => { dialTimer = null; selectAndDial() }, 200)
138
128
  }
139
129
 
140
130
  /**
141
- * 获取或创建指定 action 的 handler/backlog 条目。
142
- * @param {string} name action 名称
143
- * @returns {{ handler: ((payload: unknown, peerId: string) => void) | null, backlog: Array<{ payload: unknown, peerId: string }> }} action 条目
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
- const key = String(name)
147
- if (!actionEntries.has(key))
148
- actionEntries.set(key, { handler: null, backlog: [] })
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
- * 返回当前已连接且非自身的成员 roster
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
- * 启动 discovery、scope 订阅与成员自动连接。
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
- // 房间就绪本身就意味着本节点应当可被发现;不能等到首次外拨 ensureLinkToNode 才懒启动 runtime,
168
- // 否则“只有自己一个成员”的创建者房间会永远不监听 node topic,后来的 joiner 也就永远拨不进来。
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(String(envelope?.action || ''))
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
- registerCleanup(await subscribeTopic(topic, async (bytes, meta) => {
191
- const packet = decryptSignalPacket(topic, bytes)
192
- if (packet?.type !== 'advert' || !packet.body) return
193
- const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
194
- if (!verifiedNodeHash || verifiedNodeHash === selfNodeHash) return
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
- registerCleanup(await advertiseTopic(topic, encryptSignalPacket(topic, {
200
- type: 'advert',
201
- body: await registry.buildLocalAdvert(topic),
214
+
215
+ registerCleanup(await startGroupPresence(roomSecret, async () => ({
216
+ nodeHash: selfNodeHash,
217
+ advertBody: await registry.buildLocalAdvert({ roomSecret }),
202
218
  })))
203
- if (autoconnect) selectAndDial()
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
- if (dialTimer) { clearTimeout(dialTimer); dialTimer = null }
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
- * nodeHash 查找已连接 peer id。
228
- * @param {string} nodeHash 目标节点 64 hex
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) ? String(nodeHash) : null
257
+ return registry.getLink(nodeHash) ? nodeHash : null
233
258
  },
234
259
  /**
235
- * 向单个 peer 发送 scope action。
236
- * @param {string} peerId 目标 peer id
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
- return await registry.sendToNodeLink(peerId, { scope, action: String(actionName), payload })
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
- * 向单个或全部在线成员发送 action
246
- * @param {string} actionName action 名称
284
+ * @param {string} actionName action
247
285
  * @param {unknown} payload 载荷
248
- * @param {string | null} [peerId] 目标 peer;null 时广播
249
- * @returns {Promise<number>} 成功发送的 peer 数
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: String(actionName), payload })) sent++
293
+ if (await registry.sendToNodeLink(targetPeerId, { scope, action: actionName, payload })) sent++
256
294
  return sent
257
295
  },
258
296
  /**
259
- * 订阅入站 envelope
260
- * @param {(senderNodeHash: string, envelope: object) => void} callback 回调
261
- * @returns {() => void} 取消订阅函数
297
+ * @param {(senderNodeHash: string, envelope: object) => void} listener scope envelope 回调
298
+ * @returns {() => void} 取消订阅
262
299
  */
263
- onEnvelope(callback) {
264
- envelopeListeners.add(callback)
265
- return () => envelopeListeners.delete(callback)
300
+ onEnvelope(listener) {
301
+ envelopeListeners.add(listener)
302
+ return () => envelopeListeners.delete(listener)
266
303
  },
267
304
  /**
268
- * 订阅 peer 加入事件(含当前已在线 peer 的即时回调)。
269
- * @param {(peerId: string) => void} callback 回调
270
- * @returns {() => void} 取消订阅函数
305
+ * @param {(peerId: string) => void} listener peer 加入回调
306
+ * @returns {() => void} 取消订阅
271
307
  */
272
- onPeerJoin(callback) {
273
- peerJoinListeners.add(callback)
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 { callback(peerId) } catch { /* ignore */ }
278
- return () => peerJoinListeners.delete(callback)
313
+ try { listener(peerId) } catch { /* ignore */ }
314
+ return () => peerJoinListeners.delete(listener)
279
315
  },
280
316
  /**
281
- * 订阅 peer 离开事件。
282
- * @param {(peerId: string) => void} callback 回调
283
- * @returns {() => void} 取消订阅函数
317
+ * @param {(peerId: string) => void} listener peer 离开回调
318
+ * @returns {() => void} 取消订阅
284
319
  */
285
- onPeerLeave(callback) {
286
- peerLeaveListeners.add(callback)
287
- return () => peerLeaveListeners.delete(callback)
320
+ onPeerLeave(listener) {
321
+ peerLeaveListeners.add(listener)
322
+ return () => peerLeaveListeners.delete(listener)
288
323
  },
289
324
  /**
290
- * 返回当前在线 peer Record 映射。
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
- * 创建兼容 room.makeAction [send, on] 函数对。
298
- * @param {string} name action 名称
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, actionName, payload)))
338
+ await Promise.all(peerId.map(targetPeerId => this.sendToPeer(targetPeerId, name, payload)))
307
339
  return
308
340
  }
309
- await this.send(actionName, payload, peerId)
341
+ await this.send(name, payload, peerId)
310
342
  },
311
343
  handler => {
312
- const entry = getActionEntry(actionName)
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 = Array.isArray(groupSettings?.iceServers) ? groupSettings.iceServers : []
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 (!Array.isArray(raw)) return [...DEFAULT_ICE_SERVERS]
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)