@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.
- package/README.md +41 -10
- package/core/composite_key.mjs +5 -5
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- 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 +153 -112
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +2 -9
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +394 -59
- package/federation/chunk_fetch_pending.mjs +10 -13
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +29 -28
- 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/frame.mjs +9 -16
- package/link/handshake.mjs +14 -15
- package/link/pipe.mjs +8 -8
- package/link/providers/ble_gatt.mjs +6 -6
- package/link/rtc.mjs +3 -3
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +5 -5
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +37 -5
- 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 +22 -1
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -166
- package/transport/advert_ingest.mjs +11 -14
- package/transport/group_link_set.mjs +149 -99
- package/transport/link_registry.mjs +211 -60
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +45 -48
- package/transport/peer_pool.mjs +116 -14
- package/transport/remote_user_room.mjs +6 -9
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +170 -108
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +83 -158
- 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 +0 -3
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +3 -3
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/wire/part_ingress.mjs +32 -28
- package/wire/part_query.mjs +26 -20
- package/discovery/mdns.mjs +0 -197
- package/transport/signal_crypto.mjs +0 -104
|
@@ -1,38 +1,44 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
|
18
|
-
* @param {string} options.
|
|
19
|
-
* @param {string
|
|
20
|
-
* @param {
|
|
21
|
-
* @
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 ||
|
|
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 (!
|
|
104
|
-
const targets =
|
|
105
|
-
members
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 (!
|
|
126
|
+
if (!autoconnectEnabled || !active || dialTimer) return
|
|
123
127
|
dialTimer = setTimeout(() => { dialTimer = null; selectAndDial() }, 200)
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
/**
|
|
127
|
-
*
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
214
|
+
|
|
215
|
+
registerCleanup(await startGroupPresence(roomSecret, async () => ({
|
|
216
|
+
nodeHash: selfNodeHash,
|
|
217
|
+
advertBody: await registry.buildLocalAdvert({ roomSecret }),
|
|
184
218
|
})))
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
210
|
-
* @
|
|
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
|
-
*
|
|
218
|
-
* @param {string}
|
|
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
|
-
*
|
|
228
|
-
* @param {string}
|
|
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]
|
|
231
|
-
* @returns {Promise<number>}
|
|
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
|
-
*
|
|
242
|
-
* @
|
|
243
|
-
* @returns {() => void} 取消订阅函数
|
|
297
|
+
* @param {(senderNodeHash: string, envelope: object) => void} listener scope envelope 回调
|
|
298
|
+
* @returns {() => void} 取消订阅
|
|
244
299
|
*/
|
|
245
|
-
onEnvelope(
|
|
246
|
-
envelopeListeners.add(
|
|
247
|
-
return () => envelopeListeners.delete(
|
|
300
|
+
onEnvelope(listener) {
|
|
301
|
+
envelopeListeners.add(listener)
|
|
302
|
+
return () => envelopeListeners.delete(listener)
|
|
248
303
|
},
|
|
249
304
|
/**
|
|
250
|
-
*
|
|
251
|
-
* @
|
|
252
|
-
* @returns {() => void} 取消订阅函数
|
|
305
|
+
* @param {(peerId: string) => void} listener peer 加入回调
|
|
306
|
+
* @returns {() => void} 取消订阅
|
|
253
307
|
*/
|
|
254
|
-
onPeerJoin(
|
|
255
|
-
peerJoinListeners.add(
|
|
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 {
|
|
260
|
-
return () => peerJoinListeners.delete(
|
|
313
|
+
try { listener(peerId) } catch { /* ignore */ }
|
|
314
|
+
return () => peerJoinListeners.delete(listener)
|
|
261
315
|
},
|
|
262
316
|
/**
|
|
263
|
-
*
|
|
264
|
-
* @
|
|
265
|
-
* @returns {() => void} 取消订阅函数
|
|
317
|
+
* @param {(peerId: string) => void} listener peer 离开回调
|
|
318
|
+
* @returns {() => void} 取消订阅
|
|
266
319
|
*/
|
|
267
|
-
onPeerLeave(
|
|
268
|
-
peerLeaveListeners.add(
|
|
269
|
-
return () => peerLeaveListeners.delete(
|
|
320
|
+
onPeerLeave(listener) {
|
|
321
|
+
peerLeaveListeners.add(listener)
|
|
322
|
+
return () => peerLeaveListeners.delete(listener)
|
|
270
323
|
},
|
|
271
324
|
/**
|
|
272
|
-
*
|
|
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
|
-
*
|
|
280
|
-
* @
|
|
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
|
}
|