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