@steve02081504/fount-p2p 0.0.6 → 0.0.8
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/core/tcp_port.mjs +11 -0
- package/discovery/advert_peer_hints.mjs +21 -0
- package/discovery/{bt.mjs → bt/index.mjs} +159 -55
- package/discovery/bt/peer_hints.mjs +41 -0
- package/discovery/bt/runtime.mjs +46 -0
- package/discovery/index.mjs +9 -10
- package/discovery/lan_peer_hints.mjs +43 -0
- package/discovery/mdns.mjs +6 -2
- package/index.mjs +3 -2
- package/link/handshake.mjs +48 -21
- package/link/pipe.mjs +404 -0
- package/link/providers/ble_gatt.mjs +339 -0
- package/link/providers/index.mjs +90 -0
- package/link/providers/lan_tcp.mjs +342 -0
- package/link/providers/levels.mjs +9 -0
- package/link/providers/webrtc.mjs +385 -0
- package/package.json +3 -2
- package/rooms/scoped_link.mjs +5 -3
- package/transport/group_link_set.mjs +5 -3
- package/transport/link_registry.mjs +241 -54
- package/utils/ttl_map.mjs +41 -0
- package/link/link.mjs +0 -617
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { getSignalingRuntimeConfig } from '../../node/instance.mjs'
|
|
2
|
+
import { ms } from '../../utils/duration.mjs'
|
|
3
|
+
import { createLruMap } from '../../utils/lru.mjs'
|
|
4
|
+
import {
|
|
5
|
+
CHANNEL_BULK,
|
|
6
|
+
CHANNEL_CONTROL,
|
|
7
|
+
CHANNEL_LOW_THRESHOLD_BYTES,
|
|
8
|
+
configureBufferedAmountLowThreshold,
|
|
9
|
+
createChannelSendQueues,
|
|
10
|
+
onBufferedAmountLow,
|
|
11
|
+
} from '../channel_mux.mjs'
|
|
12
|
+
import { asLinkHandle, createLinkPipe } from '../pipe.mjs'
|
|
13
|
+
import {
|
|
14
|
+
attachDataChannelListener,
|
|
15
|
+
attachIceCandidateListener,
|
|
16
|
+
loadNodeRtcPolyfill,
|
|
17
|
+
signalDataToBytes,
|
|
18
|
+
waitForChannelState,
|
|
19
|
+
} from '../rtc.mjs'
|
|
20
|
+
import { extractDtlsFingerprint } from '../sdp_fingerprint.mjs'
|
|
21
|
+
|
|
22
|
+
import { LINK_LEVEL_WEBRTC } from './levels.mjs'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 将错误对象格式化为短字符串。
|
|
26
|
+
* @param {unknown} error 原始错误
|
|
27
|
+
* @returns {string} 短 reason
|
|
28
|
+
*/
|
|
29
|
+
function formatErrorReason(error) {
|
|
30
|
+
return String(error?.message ?? error ?? 'unknown-error').replace(/\s+/g, ' ').slice(0, 240)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 绑定 data channel message 回调。
|
|
35
|
+
* @param {RTCDataChannel} channel RTC 数据通道
|
|
36
|
+
* @param {(data: unknown) => void} handler 消息处理器
|
|
37
|
+
* @returns {void}
|
|
38
|
+
*/
|
|
39
|
+
function attachChannelMessageListener(channel, handler) {
|
|
40
|
+
channel.addEventListener?.('message', event => handler(event?.data))
|
|
41
|
+
/**
|
|
42
|
+
* @param {MessageEvent} event 消息事件
|
|
43
|
+
* @returns {void}
|
|
44
|
+
*/
|
|
45
|
+
channel.onmessage = event => handler(event?.data)
|
|
46
|
+
channel.onMessage?.subscribe(message => handler(message))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let cachedAvailable = null
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 探测 WebRTC(node-datachannel)是否可用。
|
|
53
|
+
* @returns {Promise<boolean>} 可用为 true
|
|
54
|
+
*/
|
|
55
|
+
export async function canUseWebRtcLink() {
|
|
56
|
+
if (cachedAvailable !== null) return cachedAvailable
|
|
57
|
+
try {
|
|
58
|
+
await loadNodeRtcPolyfill()
|
|
59
|
+
cachedAvailable = true
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
cachedAvailable = false
|
|
63
|
+
}
|
|
64
|
+
return cachedAvailable
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 建立 WebRTC link(双 DataChannel + discovery 信令)。
|
|
69
|
+
* @param {object} options link 配置
|
|
70
|
+
* @param {string | null} [options.nodeHash] 期望的对端 nodeHash
|
|
71
|
+
* @param {boolean} options.initiator 是否为连接发起方
|
|
72
|
+
* @param {{ send: (message: unknown) => void | Promise<void>, onRemote: (handler: (message: unknown) => void) => (() => void) | void }} options.signal 信令收发接口
|
|
73
|
+
* @param {RTCConfiguration['iceServers']} [options.iceServers] ICE 服务器列表
|
|
74
|
+
* @param {number} [options.heartbeatMs] 心跳间隔
|
|
75
|
+
* @param {number} [options.idleTimeoutMs] 无入站流量超时
|
|
76
|
+
* @param {number} [options.handshakeTimeoutMs] 握手超时
|
|
77
|
+
* @param {{ RTCPeerConnection: typeof RTCPeerConnection } | null} [options.rtc] RTC 构造器
|
|
78
|
+
* @param {{ nodeHash?: string, nodePubKey?: string, secretKey?: Uint8Array, nonce?: string } | null} [options.localIdentity] 本地握手身份
|
|
79
|
+
* @returns {Promise<import('./index.mjs').LinkHandle>} link 句柄
|
|
80
|
+
*/
|
|
81
|
+
export async function createWebRtcLink(options) {
|
|
82
|
+
const handshakeTimeoutMs = Number(options.handshakeTimeoutMs) || ms('10s')
|
|
83
|
+
const channelOpenTimeoutMs = Math.max(handshakeTimeoutMs, ms('30s'))
|
|
84
|
+
const trickleIceOff = getSignalingRuntimeConfig().trickleIceOff === true
|
|
85
|
+
const rtc = options.rtc ?? await loadNodeRtcPolyfill()
|
|
86
|
+
const pc = new rtc.RTCPeerConnection(options.iceServers?.length ? { iceServers: options.iceServers } : undefined)
|
|
87
|
+
const remoteSignalQueue = []
|
|
88
|
+
const seenRemoteSignals = createLruMap(1024)
|
|
89
|
+
let remoteDescriptionSet = false
|
|
90
|
+
let controlChannel = null
|
|
91
|
+
let bulkChannel = null
|
|
92
|
+
let unlistenRemote = null
|
|
93
|
+
let sendQueues = null
|
|
94
|
+
let controlLowEvents = 0
|
|
95
|
+
let bulkLowEvents = 0
|
|
96
|
+
let reconnectCount = 0
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {unknown} message 信令载荷
|
|
100
|
+
* @returns {Promise<void>}
|
|
101
|
+
*/
|
|
102
|
+
async function sendSignal(message) {
|
|
103
|
+
await Promise.resolve(options.signal.send(message))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const pipe = createLinkPipe({
|
|
107
|
+
providerId: 'webrtc',
|
|
108
|
+
level: LINK_LEVEL_WEBRTC,
|
|
109
|
+
initiator: !!options.initiator,
|
|
110
|
+
nodeHash: options.nodeHash,
|
|
111
|
+
localIdentity: options.localIdentity,
|
|
112
|
+
heartbeatMs: options.heartbeatMs,
|
|
113
|
+
idleTimeoutMs: options.idleTimeoutMs,
|
|
114
|
+
handshakeTimeoutMs,
|
|
115
|
+
/** @returns {string} 本端 DTLS fingerprint */
|
|
116
|
+
getLocalBinding: () => extractDtlsFingerprint(pc.localDescription?.sdp || ''),
|
|
117
|
+
/** @returns {string} 对端 DTLS fingerprint */
|
|
118
|
+
getRemoteBinding: () => extractDtlsFingerprint(pc.remoteDescription?.sdp || ''),
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} text control JSON
|
|
121
|
+
* @returns {void}
|
|
122
|
+
*/
|
|
123
|
+
sendControlText(text) {
|
|
124
|
+
if (!controlChannel) throw new Error('p2p: control channel unavailable')
|
|
125
|
+
controlChannel.send(text)
|
|
126
|
+
},
|
|
127
|
+
/**
|
|
128
|
+
* @param {string} action envelope action
|
|
129
|
+
* @param {Uint8Array} frame 帧字节
|
|
130
|
+
* @returns {void}
|
|
131
|
+
*/
|
|
132
|
+
sendFrame(action, frame) {
|
|
133
|
+
if (!sendQueues) throw new Error('p2p: send queues unavailable')
|
|
134
|
+
sendQueues.enqueue(action, frame)
|
|
135
|
+
},
|
|
136
|
+
/**
|
|
137
|
+
* @returns {Promise<void>}
|
|
138
|
+
*/
|
|
139
|
+
async closeTransport() {
|
|
140
|
+
unlistenRemote?.()
|
|
141
|
+
sendQueues?.clear()
|
|
142
|
+
try { controlChannel?.close() } catch { /* ignore */ }
|
|
143
|
+
try { bulkChannel?.close() } catch { /* ignore */ }
|
|
144
|
+
try { await pc.close() } catch { /* ignore */ }
|
|
145
|
+
},
|
|
146
|
+
/**
|
|
147
|
+
* @returns {object} WebRTC 附加 stats
|
|
148
|
+
*/
|
|
149
|
+
extraStats() {
|
|
150
|
+
return {
|
|
151
|
+
connectionState: pc.connectionState,
|
|
152
|
+
iceConnectionState: pc.iceConnectionState,
|
|
153
|
+
reconnectCount,
|
|
154
|
+
pending: sendQueues?.pending() ?? { control: 0, bulk: 0 },
|
|
155
|
+
controlBufferedAmount: controlChannel?.bufferedAmount ?? 0,
|
|
156
|
+
bulkBufferedAmount: bulkChannel?.bufferedAmount ?? 0,
|
|
157
|
+
controlLowEvents,
|
|
158
|
+
bulkLowEvents,
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {RTCDataChannel} channel RTC 数据通道
|
|
165
|
+
* @returns {void}
|
|
166
|
+
*/
|
|
167
|
+
function attachBackpressurePump(channel) {
|
|
168
|
+
configureBufferedAmountLowThreshold(channel, CHANNEL_LOW_THRESHOLD_BYTES)
|
|
169
|
+
onBufferedAmountLow(channel, () => {
|
|
170
|
+
if (channel.label === CHANNEL_CONTROL) controlLowEvents++
|
|
171
|
+
if (channel.label === CHANNEL_BULK) bulkLowEvents++
|
|
172
|
+
if (channel.label === CHANNEL_CONTROL) sendQueues?.flush(CHANNEL_CONTROL)
|
|
173
|
+
if (channel.label === CHANNEL_BULK) sendQueues?.flush(CHANNEL_BULK)
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {RTCSessionDescriptionInit | RTCSessionDescription} description 远端会话描述
|
|
179
|
+
* @returns {Promise<void>}
|
|
180
|
+
*/
|
|
181
|
+
async function applyRemoteDescription(description) {
|
|
182
|
+
await pc.setRemoteDescription(description)
|
|
183
|
+
remoteDescriptionSet = true
|
|
184
|
+
await flushQueuedIceCandidates()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {unknown} error addIceCandidate 错误
|
|
189
|
+
* @returns {boolean} 是否应暂存后重试
|
|
190
|
+
*/
|
|
191
|
+
function shouldRetryQueuedIce(error) {
|
|
192
|
+
return /without ice transport/i.test(String(error?.message ?? error ?? ''))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @returns {Promise<void>}
|
|
197
|
+
*/
|
|
198
|
+
async function waitForIceGatheringComplete() {
|
|
199
|
+
if (!trickleIceOff || pc.iceGatheringState === 'complete') return
|
|
200
|
+
const deadline = Date.now() + handshakeTimeoutMs
|
|
201
|
+
while (pc.iceGatheringState !== 'complete' && Date.now() < deadline)
|
|
202
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @returns {Promise<void>}
|
|
207
|
+
*/
|
|
208
|
+
async function flushQueuedIceCandidates() {
|
|
209
|
+
if (!remoteDescriptionSet || !pc.localDescription) return
|
|
210
|
+
while (remoteSignalQueue.length) {
|
|
211
|
+
const candidate = remoteSignalQueue.shift()
|
|
212
|
+
try {
|
|
213
|
+
await pc.addIceCandidate(candidate)
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (shouldRetryQueuedIce(error)) {
|
|
217
|
+
remoteSignalQueue.unshift(candidate)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
throw error
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {unknown} message 信令消息
|
|
227
|
+
* @returns {Promise<void>}
|
|
228
|
+
*/
|
|
229
|
+
async function handleRemoteSignal(message) {
|
|
230
|
+
if (!message || typeof message !== 'object') return
|
|
231
|
+
const signalKey = JSON.stringify(message)
|
|
232
|
+
if (seenRemoteSignals.has(signalKey)) return
|
|
233
|
+
seenRemoteSignals.touch(signalKey, true)
|
|
234
|
+
if (message.type === 'description' && message.description) {
|
|
235
|
+
if (message.description.type === 'answer' && pc.signalingState === 'stable') return
|
|
236
|
+
await applyRemoteDescription(message.description)
|
|
237
|
+
if (message.description.type === 'offer') {
|
|
238
|
+
const answer = await pc.createAnswer()
|
|
239
|
+
await pc.setLocalDescription(answer)
|
|
240
|
+
await flushQueuedIceCandidates()
|
|
241
|
+
await waitForIceGatheringComplete()
|
|
242
|
+
await sendSignal({
|
|
243
|
+
type: 'description',
|
|
244
|
+
description: pc.localDescription?.toJSON?.() ?? pc.localDescription ?? answer,
|
|
245
|
+
})
|
|
246
|
+
await pipe.maybeSendAuth()
|
|
247
|
+
}
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
if (message.type === 'ice' && message.candidate) {
|
|
251
|
+
if (!remoteDescriptionSet || !pc.localDescription || pc.signalingState !== 'stable') {
|
|
252
|
+
remoteSignalQueue.push(message.candidate)
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
await pc.addIceCandidate(message.candidate)
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (shouldRetryQueuedIce(error)) {
|
|
260
|
+
remoteSignalQueue.push(message.candidate)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
throw error
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @param {RTCDataChannel} channel RTC 数据通道
|
|
270
|
+
* @returns {Promise<void>}
|
|
271
|
+
*/
|
|
272
|
+
async function attachChannel(channel) {
|
|
273
|
+
if (channel.label === CHANNEL_CONTROL) controlChannel = channel
|
|
274
|
+
else if (channel.label === CHANNEL_BULK) bulkChannel = channel
|
|
275
|
+
else return
|
|
276
|
+
attachChannelMessageListener(channel, data => {
|
|
277
|
+
try {
|
|
278
|
+
if (typeof data === 'string') pipe.handleInbound(data)
|
|
279
|
+
else pipe.handleInbound(signalDataToBytes(data))
|
|
280
|
+
}
|
|
281
|
+
catch { /* drop */ }
|
|
282
|
+
})
|
|
283
|
+
attachBackpressurePump(channel)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @returns {Promise<void>}
|
|
288
|
+
*/
|
|
289
|
+
async function maybeStartPostOpenFlow() {
|
|
290
|
+
if (!controlChannel || !bulkChannel) return
|
|
291
|
+
await Promise.all([
|
|
292
|
+
waitForChannelState(controlChannel, 'open', channelOpenTimeoutMs),
|
|
293
|
+
waitForChannelState(bulkChannel, 'open', channelOpenTimeoutMs),
|
|
294
|
+
])
|
|
295
|
+
if (!sendQueues)
|
|
296
|
+
sendQueues = createChannelSendQueues({
|
|
297
|
+
/**
|
|
298
|
+
* @param {'control' | 'bulk'} name 通道名
|
|
299
|
+
* @returns {RTCDataChannel | null | undefined} 对应通道
|
|
300
|
+
*/
|
|
301
|
+
getChannel: name => name === CHANNEL_CONTROL ? controlChannel : bulkChannel,
|
|
302
|
+
})
|
|
303
|
+
await pipe.startHandshake()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
unlistenRemote = options.signal.onRemote(message => {
|
|
307
|
+
void handleRemoteSignal(message).catch(error => pipe.close(`signal-error:${formatErrorReason(error)}`))
|
|
308
|
+
}) ?? null
|
|
309
|
+
|
|
310
|
+
attachIceCandidateListener(pc, event => {
|
|
311
|
+
if (trickleIceOff || !event.candidate) return
|
|
312
|
+
void sendSignal({
|
|
313
|
+
type: 'ice',
|
|
314
|
+
candidate: typeof event.candidate.toJSON === 'function' ? event.candidate.toJSON() : event.candidate,
|
|
315
|
+
}).catch(error => pipe.close(`signal-send-failed:${formatErrorReason(error)}`))
|
|
316
|
+
})
|
|
317
|
+
attachDataChannelListener(pc, event => {
|
|
318
|
+
void attachChannel(event.channel)
|
|
319
|
+
.then(() => maybeStartPostOpenFlow())
|
|
320
|
+
.catch(error => pipe.close(`channel-attach-failed:${error?.message ?? error}`))
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
/** 连接失败/关闭时关掉 pipe。 */
|
|
324
|
+
pc.onconnectionstatechange = () => {
|
|
325
|
+
if (['failed', 'closed', 'disconnected'].includes(pc.connectionState)) {
|
|
326
|
+
reconnectCount++
|
|
327
|
+
void pipe.close(`connection-${pc.connectionState}`)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (options.initiator) {
|
|
332
|
+
await attachChannel(pc.createDataChannel(CHANNEL_CONTROL))
|
|
333
|
+
await attachChannel(pc.createDataChannel(CHANNEL_BULK))
|
|
334
|
+
const offer = await pc.createOffer()
|
|
335
|
+
await pc.setLocalDescription(offer)
|
|
336
|
+
await waitForIceGatheringComplete()
|
|
337
|
+
await sendSignal({
|
|
338
|
+
type: 'description',
|
|
339
|
+
description: pc.localDescription?.toJSON?.() ?? pc.localDescription ?? offer,
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
void maybeStartPostOpenFlow().catch(error => pipe.close(`open-flow-failed:${error?.message ?? error}`))
|
|
344
|
+
|
|
345
|
+
return asLinkHandle(pipe, {
|
|
346
|
+
/**
|
|
347
|
+
* @internal 仅 live 背压测试用,不属公开 LinkHandle
|
|
348
|
+
* @param {'control' | 'bulk'} name 通道名
|
|
349
|
+
* @returns {RTCDataChannel | null} 测试用通道
|
|
350
|
+
*/
|
|
351
|
+
_channelForTest(name) {
|
|
352
|
+
return name === CHANNEL_CONTROL ? controlChannel : bulkChannel
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 创建 WebRTC LinkProvider。
|
|
359
|
+
* @param {object} [options] 可选覆盖
|
|
360
|
+
* @param {typeof createWebRtcLink} [options.createWebRtcLink] 链路工厂(测试注入)
|
|
361
|
+
* @returns {import('./index.mjs').LinkProvider} WebRTC provider
|
|
362
|
+
*/
|
|
363
|
+
export function createWebRtcLinkProvider(options = {}) {
|
|
364
|
+
const createImpl = options.createWebRtcLink ?? createWebRtcLink
|
|
365
|
+
return {
|
|
366
|
+
id: 'webrtc',
|
|
367
|
+
level: LINK_LEVEL_WEBRTC,
|
|
368
|
+
caps: { needsOfferAnswer: true, needsDiscoverySignal: true },
|
|
369
|
+
isAvailable: canUseWebRtcLink,
|
|
370
|
+
/**
|
|
371
|
+
* @param {object} dialOptions dial 参数(含 signal / iceServers 等)
|
|
372
|
+
* @returns {Promise<import('./index.mjs').LinkHandle>} 已建链句柄
|
|
373
|
+
*/
|
|
374
|
+
async dial(dialOptions) {
|
|
375
|
+
return createImpl({ ...dialOptions, initiator: true })
|
|
376
|
+
},
|
|
377
|
+
/**
|
|
378
|
+
* @param {object} acceptOptions accept 参数
|
|
379
|
+
* @returns {Promise<import('./index.mjs').LinkHandle>} 已建链句柄
|
|
380
|
+
*/
|
|
381
|
+
async accept(acceptOptions) {
|
|
382
|
+
return createImpl({ ...acceptOptions, initiator: false })
|
|
383
|
+
},
|
|
384
|
+
}
|
|
385
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steve02081504/fount-p2p",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"network",
|
|
@@ -39,8 +39,9 @@
|
|
|
39
39
|
"./schemas/*": "./schemas/*.mjs",
|
|
40
40
|
"./node/*": "./node/*.mjs",
|
|
41
41
|
"./discovery": "./discovery/index.mjs",
|
|
42
|
+
"./discovery/bt": "./discovery/bt/index.mjs",
|
|
43
|
+
"./discovery/bt/*": "./discovery/bt/*.mjs",
|
|
42
44
|
"./discovery/*": "./discovery/*.mjs",
|
|
43
|
-
"./link/*": "./link/*.mjs",
|
|
44
45
|
"./transport/*": "./transport/*.mjs",
|
|
45
46
|
"./rooms/*": "./rooms/*.mjs",
|
|
46
47
|
"./overlay": "./overlay/index.mjs",
|
package/rooms/scoped_link.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
|
|
1
2
|
import { advertiseTopic, subscribeTopic } from '../discovery/index.mjs'
|
|
2
|
-
import {
|
|
3
|
+
import { verifySignedAdvert } from '../link/handshake.mjs'
|
|
3
4
|
import {
|
|
4
5
|
groupRendezvousTopic,
|
|
5
6
|
decryptSignalPacket,
|
|
@@ -102,18 +103,19 @@ export function createScopedLinkRoom(options) {
|
|
|
102
103
|
if (!discoveredPeers.has(nodeHash)) return
|
|
103
104
|
notePeerLeave(nodeHash)
|
|
104
105
|
}))
|
|
105
|
-
cleanups.add(await subscribeTopic(topic, async bytes => {
|
|
106
|
+
cleanups.add(await subscribeTopic(topic, async (bytes, meta) => {
|
|
106
107
|
const packet = decryptSignalPacket(topic, bytes)
|
|
107
108
|
if (packet?.type !== 'advert' || !packet.body) return
|
|
108
109
|
const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
|
|
109
110
|
if (!verifiedNodeHash || !allowNode(verifiedNodeHash)) return
|
|
111
|
+
noteAdvertPeerHints(verifiedNodeHash, packet.body, meta)
|
|
110
112
|
discoveredPeers.add(verifiedNodeHash)
|
|
111
113
|
await registry.ensureLinkToNode(verifiedNodeHash).catch(() => null)
|
|
112
114
|
if (registry.getLink(verifiedNodeHash)) notePeerJoin(verifiedNodeHash)
|
|
113
115
|
}))
|
|
114
116
|
cleanups.add(await advertiseTopic(topic, encryptSignalPacket(topic, {
|
|
115
117
|
type: 'advert',
|
|
116
|
-
body: await
|
|
118
|
+
body: await registry.buildLocalAdvert(topic),
|
|
117
119
|
})))
|
|
118
120
|
for (const peerId of activePeerIds())
|
|
119
121
|
notePeerJoin(peerId)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { noteAdvertPeerHints } from '../discovery/advert_peer_hints.mjs'
|
|
1
2
|
import { advertiseTopic, subscribeTopic } from '../discovery/index.mjs'
|
|
2
|
-
import {
|
|
3
|
+
import { verifySignedAdvert } from '../link/handshake.mjs'
|
|
3
4
|
import { loadPeerPoolView } from '../node/network.mjs'
|
|
4
5
|
import { loadReputation } from '../node/reputation_store.mjs'
|
|
5
6
|
|
|
@@ -186,17 +187,18 @@ export function createGroupLinkSet(options) {
|
|
|
186
187
|
if (!members.has(nodeHash) || nodeHash === selfNodeHash) return
|
|
187
188
|
notePeerLeave(nodeHash)
|
|
188
189
|
}))
|
|
189
|
-
registerCleanup(await subscribeTopic(topic, async bytes => {
|
|
190
|
+
registerCleanup(await subscribeTopic(topic, async (bytes, meta) => {
|
|
190
191
|
const packet = decryptSignalPacket(topic, bytes)
|
|
191
192
|
if (packet?.type !== 'advert' || !packet.body) return
|
|
192
193
|
const verifiedNodeHash = await verifySignedAdvert(topic, packet.body)
|
|
193
194
|
if (!verifiedNodeHash || verifiedNodeHash === selfNodeHash) return
|
|
195
|
+
noteAdvertPeerHints(verifiedNodeHash, packet.body, meta)
|
|
194
196
|
// 发现新成员:并入成员集并去抖重算稀疏建链目标(notePeerCandidate 内部会 scheduleDial)。
|
|
195
197
|
notePeerCandidate(verifiedNodeHash)
|
|
196
198
|
}))
|
|
197
199
|
registerCleanup(await advertiseTopic(topic, encryptSignalPacket(topic, {
|
|
198
200
|
type: 'advert',
|
|
199
|
-
body: await
|
|
201
|
+
body: await registry.buildLocalAdvert(topic),
|
|
200
202
|
})))
|
|
201
203
|
if (autoconnect) selectAndDial()
|
|
202
204
|
for (const { peerId } of activeRoster())
|