@steve02081504/fount-p2p 0.0.11 → 0.0.12
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 +1 -1
- package/core/bytes_codec.mjs +65 -10
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- package/discovery/bt/index.mjs +5 -5
- package/discovery/bt/runtime.mjs +3 -3
- package/discovery/mdns.mjs +7 -1
- package/discovery/nostr.mjs +53 -99
- package/federation/chunk_fetch_pending.mjs +4 -5
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/topo_order_memo.mjs +11 -4
- package/files/chunk_fetch.mjs +2 -2
- package/files/evfs.mjs +2 -2
- package/link/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -124
- package/link/handshake.mjs +5 -5
- package/link/pipe.mjs +49 -75
- package/link/providers/ble_gatt.mjs +14 -26
- 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 +21 -9
- package/mailbox/deliver_or_store.mjs +1 -1
- package/node/identity.mjs +4 -4
- package/node/network.mjs +9 -9
- package/overlay/index.mjs +13 -13
- package/package.json +1 -1
- package/rooms/scoped_link.mjs +19 -35
- 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 +20 -0
- package/transport/group_link_set.mjs +22 -41
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +113 -105
- package/transport/offer_answer.mjs +6 -2
- package/transport/peer_pool.mjs +53 -64
- package/transport/remote_user_room.mjs +13 -15
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/runtime_bootstrap.mjs +8 -2
- package/transport/signal_crypto.mjs +25 -3
- package/transport/user_room.mjs +22 -15
- package/trust_graph/send.mjs +1 -1
- package/utils/emit_safe.mjs +11 -0
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +6 -8
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +2 -3
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
|
@@ -4,9 +4,10 @@ import net from 'node:net'
|
|
|
4
4
|
|
|
5
5
|
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
6
6
|
import { getLanPeerHint } from '../../discovery/lan_peer_hints.mjs'
|
|
7
|
-
import { asLinkHandle
|
|
7
|
+
import { asLinkHandle } from '../pipe.mjs'
|
|
8
8
|
|
|
9
9
|
import { LINK_LEVEL_LAN_TCP } from './levels.mjs'
|
|
10
|
+
import { buildLinkOpen, createLinkIdBoundPipe, parseLinkOpen } from './link_id_pipe.mjs'
|
|
10
11
|
|
|
11
12
|
const MAX_FRAME_BYTES = 1 << 20
|
|
12
13
|
|
|
@@ -85,22 +86,17 @@ function attachLengthPrefix(socket, onPayload) {
|
|
|
85
86
|
/**
|
|
86
87
|
* 在已挂 codec 的 socket 上创建 pipe。
|
|
87
88
|
* @param {object} options 配置
|
|
88
|
-
* @returns {ReturnType<typeof
|
|
89
|
+
* @returns {ReturnType<typeof createLinkIdBoundPipe>} pipe 句柄
|
|
89
90
|
*/
|
|
90
91
|
function createTcpPipe(options) {
|
|
91
|
-
const linkId = normalizeHex64(options.linkId)
|
|
92
|
-
if (!linkId) throw new Error('p2p: lan_tcp linkId required')
|
|
93
92
|
const { socket, codec } = options
|
|
94
|
-
const pipe =
|
|
93
|
+
const pipe = createLinkIdBoundPipe({
|
|
95
94
|
providerId: 'lan_tcp',
|
|
96
95
|
level: LINK_LEVEL_LAN_TCP,
|
|
97
96
|
initiator: !!options.initiator,
|
|
97
|
+
linkId: options.linkId,
|
|
98
98
|
nodeHash: options.nodeHash,
|
|
99
99
|
localIdentity: options.localIdentity,
|
|
100
|
-
/** @returns {string} 本端 binding(linkId) */
|
|
101
|
-
getLocalBinding: () => linkId,
|
|
102
|
-
/** @returns {string} 对端 binding(linkId) */
|
|
103
|
-
getRemoteBinding: () => linkId,
|
|
104
100
|
/**
|
|
105
101
|
* @param {string} text control JSON
|
|
106
102
|
* @returns {void}
|
|
@@ -141,8 +137,8 @@ function createTcpPipe(options) {
|
|
|
141
137
|
* @returns {Promise<import('./index.mjs').LinkHandle>} 已启动握手的 link
|
|
142
138
|
*/
|
|
143
139
|
async function openTcpPipe(options) {
|
|
144
|
-
const socket = options
|
|
145
|
-
/** @type {ReturnType<typeof
|
|
140
|
+
const { socket } = options
|
|
141
|
+
/** @type {ReturnType<typeof createLinkIdBoundPipe> | null} */
|
|
146
142
|
let pipe = null
|
|
147
143
|
/** @type {Buffer[]} */
|
|
148
144
|
const pending = []
|
|
@@ -151,19 +147,15 @@ async function openTcpPipe(options) {
|
|
|
151
147
|
pending.push(payload)
|
|
152
148
|
return
|
|
153
149
|
}
|
|
154
|
-
pipe.handleInbound(
|
|
150
|
+
pipe.handleInbound(payload)
|
|
155
151
|
})
|
|
156
152
|
|
|
157
153
|
pipe = createTcpPipe({ ...options, codec, socket })
|
|
158
154
|
for (const payload of pending.splice(0))
|
|
159
|
-
pipe.handleInbound(
|
|
155
|
+
pipe.handleInbound(payload)
|
|
160
156
|
|
|
161
157
|
if (options.initiator)
|
|
162
|
-
codec.write(
|
|
163
|
-
type: 'link-open',
|
|
164
|
-
linkId: normalizeHex64(options.linkId),
|
|
165
|
-
from: options.localIdentity?.nodeHash || '',
|
|
166
|
-
}))
|
|
158
|
+
codec.write(buildLinkOpen(normalizeHex64(options.linkId), options.localIdentity?.nodeHash))
|
|
167
159
|
|
|
168
160
|
await pipe.startHandshake()
|
|
169
161
|
return asLinkHandle(pipe)
|
|
@@ -238,30 +230,23 @@ export function createLanTcpLinkProvider() {
|
|
|
238
230
|
socket.destroy()
|
|
239
231
|
return
|
|
240
232
|
}
|
|
241
|
-
/** @type {ReturnType<typeof
|
|
233
|
+
/** @type {ReturnType<typeof createLinkIdBoundPipe> | null} */
|
|
242
234
|
let pipe = null
|
|
243
235
|
const codec = attachLengthPrefix(socket, payload => {
|
|
244
236
|
if (pipe) {
|
|
245
|
-
pipe.handleInbound(
|
|
237
|
+
pipe.handleInbound(payload)
|
|
246
238
|
return
|
|
247
239
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
parsed = JSON.parse(payload.toString('utf8'))
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
socket.destroy()
|
|
254
|
-
return
|
|
255
|
-
}
|
|
256
|
-
if (parsed?.type !== 'link-open' || !parsed.linkId) {
|
|
240
|
+
const opened = parseLinkOpen(payload)
|
|
241
|
+
if (!opened) {
|
|
257
242
|
socket.destroy()
|
|
258
243
|
return
|
|
259
244
|
}
|
|
260
245
|
try {
|
|
261
246
|
pipe = createTcpPipe({
|
|
262
247
|
initiator: false,
|
|
263
|
-
linkId:
|
|
264
|
-
nodeHash:
|
|
248
|
+
linkId: opened.linkId,
|
|
249
|
+
nodeHash: opened.from,
|
|
265
250
|
localIdentity,
|
|
266
251
|
socket,
|
|
267
252
|
codec,
|
|
@@ -324,7 +309,7 @@ export function createLanTcpLinkProvider() {
|
|
|
324
309
|
server.listen(0, '0.0.0.0', () => {
|
|
325
310
|
server.off('error', reject)
|
|
326
311
|
const addr = server.address()
|
|
327
|
-
listenPort =
|
|
312
|
+
listenPort = addr?.port || 0
|
|
328
313
|
resolve()
|
|
329
314
|
})
|
|
330
315
|
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
|
|
3
|
+
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
4
|
+
import { createLinkPipe } from '../pipe.mjs'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 构造 link-open control JSON。
|
|
8
|
+
* @param {string} linkId 链路 id(64 hex)
|
|
9
|
+
* @param {string} [fromNodeHash] 本端 nodeHash
|
|
10
|
+
* @returns {string} JSON 文本
|
|
11
|
+
*/
|
|
12
|
+
export function buildLinkOpen(linkId, fromNodeHash = '') {
|
|
13
|
+
return JSON.stringify({ type: 'link-open', linkId, from: fromNodeHash || '' })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 解析 link-open;非法返回 null。
|
|
18
|
+
* @param {string | Uint8Array | Buffer} raw 原始 control
|
|
19
|
+
* @returns {{ linkId: string, from: string | null } | null} 解析结果
|
|
20
|
+
*/
|
|
21
|
+
export function parseLinkOpen(raw) {
|
|
22
|
+
let parsed
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(typeof raw === 'string' ? raw : Buffer.from(raw).toString('utf8'))
|
|
25
|
+
}
|
|
26
|
+
catch { return null }
|
|
27
|
+
if (parsed?.type !== 'link-open' || !parsed.linkId) return null
|
|
28
|
+
return { linkId: String(parsed.linkId), from: normalizeHex64(parsed.from) || null }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 以固定 linkId 作为 hello/auth binding 的 pipe。
|
|
33
|
+
* @param {Parameters<typeof createLinkPipe>[0] & { linkId: string }} options pipe 配置(须含 linkId)
|
|
34
|
+
* @returns {ReturnType<typeof createLinkPipe>} link pipe
|
|
35
|
+
*/
|
|
36
|
+
export function createLinkIdBoundPipe(options) {
|
|
37
|
+
const linkId = normalizeHex64(options.linkId)
|
|
38
|
+
if (!linkId) throw new Error(`p2p: ${options.providerId || 'link'} linkId required`)
|
|
39
|
+
return createLinkPipe({
|
|
40
|
+
...options,
|
|
41
|
+
/** @returns {string} 本地 binding */
|
|
42
|
+
getLocalBinding: () => linkId,
|
|
43
|
+
/** @returns {string} 远端 binding */
|
|
44
|
+
getRemoteBinding: () => linkId,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
@@ -11,10 +11,11 @@ import {
|
|
|
11
11
|
} from '../channel_mux.mjs'
|
|
12
12
|
import { asLinkHandle, createLinkPipe } from '../pipe.mjs'
|
|
13
13
|
import {
|
|
14
|
+
attachChannelMessageListener,
|
|
14
15
|
attachDataChannelListener,
|
|
15
16
|
attachIceCandidateListener,
|
|
16
17
|
loadNodeRtcPolyfill,
|
|
17
|
-
|
|
18
|
+
dataToBytes,
|
|
18
19
|
waitForChannelState,
|
|
19
20
|
} from '../rtc.mjs'
|
|
20
21
|
import { extractDtlsFingerprint } from '../sdp_fingerprint.mjs'
|
|
@@ -30,22 +31,6 @@ function formatErrorReason(error) {
|
|
|
30
31
|
return String(error?.message ?? error ?? 'unknown-error').replace(/\s+/g, ' ').slice(0, 240)
|
|
31
32
|
}
|
|
32
33
|
|
|
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
34
|
let cachedAvailable = null
|
|
50
35
|
|
|
51
36
|
/**
|
|
@@ -83,7 +68,7 @@ export async function createWebRtcLink(options) {
|
|
|
83
68
|
const channelOpenTimeoutMs = Math.max(handshakeTimeoutMs, ms('30s'))
|
|
84
69
|
const trickleIceOff = getSignalingRuntimeConfig().trickleIceOff === true
|
|
85
70
|
const rtc = options.rtc ?? await loadNodeRtcPolyfill()
|
|
86
|
-
const
|
|
71
|
+
const peerConnection = new rtc.RTCPeerConnection(options.iceServers?.length ? { iceServers: options.iceServers } : undefined)
|
|
87
72
|
const remoteSignalQueue = []
|
|
88
73
|
const seenRemoteSignals = createLruMap(1024)
|
|
89
74
|
let remoteDescriptionSet = false
|
|
@@ -113,9 +98,9 @@ export async function createWebRtcLink(options) {
|
|
|
113
98
|
idleTimeoutMs: options.idleTimeoutMs,
|
|
114
99
|
handshakeTimeoutMs,
|
|
115
100
|
/** @returns {string} 本端 DTLS fingerprint */
|
|
116
|
-
getLocalBinding: () => extractDtlsFingerprint(
|
|
101
|
+
getLocalBinding: () => extractDtlsFingerprint(peerConnection.localDescription?.sdp || ''),
|
|
117
102
|
/** @returns {string} 对端 DTLS fingerprint */
|
|
118
|
-
getRemoteBinding: () => extractDtlsFingerprint(
|
|
103
|
+
getRemoteBinding: () => extractDtlsFingerprint(peerConnection.remoteDescription?.sdp || ''),
|
|
119
104
|
/**
|
|
120
105
|
* @param {string} text control JSON
|
|
121
106
|
* @returns {void}
|
|
@@ -141,15 +126,15 @@ export async function createWebRtcLink(options) {
|
|
|
141
126
|
sendQueues?.clear()
|
|
142
127
|
try { controlChannel?.close() } catch { /* ignore */ }
|
|
143
128
|
try { bulkChannel?.close() } catch { /* ignore */ }
|
|
144
|
-
try { await
|
|
129
|
+
try { await peerConnection.close() } catch { /* ignore */ }
|
|
145
130
|
},
|
|
146
131
|
/**
|
|
147
132
|
* @returns {object} WebRTC 附加 stats
|
|
148
133
|
*/
|
|
149
134
|
extraStats() {
|
|
150
135
|
return {
|
|
151
|
-
connectionState:
|
|
152
|
-
iceConnectionState:
|
|
136
|
+
connectionState: peerConnection.connectionState,
|
|
137
|
+
iceConnectionState: peerConnection.iceConnectionState,
|
|
153
138
|
reconnectCount,
|
|
154
139
|
pending: sendQueues?.pending() ?? { control: 0, bulk: 0 },
|
|
155
140
|
controlBufferedAmount: controlChannel?.bufferedAmount ?? 0,
|
|
@@ -167,10 +152,14 @@ export async function createWebRtcLink(options) {
|
|
|
167
152
|
function attachBackpressurePump(channel) {
|
|
168
153
|
configureBufferedAmountLowThreshold(channel, CHANNEL_LOW_THRESHOLD_BYTES)
|
|
169
154
|
onBufferedAmountLow(channel, () => {
|
|
170
|
-
if (channel.label === CHANNEL_CONTROL)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
155
|
+
if (channel.label === CHANNEL_CONTROL) {
|
|
156
|
+
controlLowEvents++
|
|
157
|
+
sendQueues?.flush(CHANNEL_CONTROL)
|
|
158
|
+
}
|
|
159
|
+
else if (channel.label === CHANNEL_BULK) {
|
|
160
|
+
bulkLowEvents++
|
|
161
|
+
sendQueues?.flush(CHANNEL_BULK)
|
|
162
|
+
}
|
|
174
163
|
})
|
|
175
164
|
}
|
|
176
165
|
|
|
@@ -179,7 +168,7 @@ export async function createWebRtcLink(options) {
|
|
|
179
168
|
* @returns {Promise<void>}
|
|
180
169
|
*/
|
|
181
170
|
async function applyRemoteDescription(description) {
|
|
182
|
-
await
|
|
171
|
+
await peerConnection.setRemoteDescription(description)
|
|
183
172
|
remoteDescriptionSet = true
|
|
184
173
|
await flushQueuedIceCandidates()
|
|
185
174
|
}
|
|
@@ -196,9 +185,9 @@ export async function createWebRtcLink(options) {
|
|
|
196
185
|
* @returns {Promise<void>}
|
|
197
186
|
*/
|
|
198
187
|
async function waitForIceGatheringComplete() {
|
|
199
|
-
if (!trickleIceOff ||
|
|
188
|
+
if (!trickleIceOff || peerConnection.iceGatheringState === 'complete') return
|
|
200
189
|
const deadline = Date.now() + handshakeTimeoutMs
|
|
201
|
-
while (
|
|
190
|
+
while (peerConnection.iceGatheringState !== 'complete' && Date.now() < deadline)
|
|
202
191
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
203
192
|
}
|
|
204
193
|
|
|
@@ -206,11 +195,11 @@ export async function createWebRtcLink(options) {
|
|
|
206
195
|
* @returns {Promise<void>}
|
|
207
196
|
*/
|
|
208
197
|
async function flushQueuedIceCandidates() {
|
|
209
|
-
if (!remoteDescriptionSet || !
|
|
198
|
+
if (!remoteDescriptionSet || !peerConnection.localDescription) return
|
|
210
199
|
while (remoteSignalQueue.length) {
|
|
211
200
|
const candidate = remoteSignalQueue.shift()
|
|
212
201
|
try {
|
|
213
|
-
await
|
|
202
|
+
await peerConnection.addIceCandidate(candidate)
|
|
214
203
|
}
|
|
215
204
|
catch (error) {
|
|
216
205
|
if (shouldRetryQueuedIce(error)) {
|
|
@@ -227,33 +216,35 @@ export async function createWebRtcLink(options) {
|
|
|
227
216
|
* @returns {Promise<void>}
|
|
228
217
|
*/
|
|
229
218
|
async function handleRemoteSignal(message) {
|
|
230
|
-
if (!message
|
|
231
|
-
const signalKey =
|
|
219
|
+
if (!message?.type) return
|
|
220
|
+
const signalKey = message.type === 'ice' && message.candidate
|
|
221
|
+
? `ice:${message.candidate.candidate ?? ''}:${message.candidate.sdpMid ?? ''}:${message.candidate.sdpMLineIndex ?? ''}`
|
|
222
|
+
: JSON.stringify(message)
|
|
232
223
|
if (seenRemoteSignals.has(signalKey)) return
|
|
233
224
|
seenRemoteSignals.touch(signalKey, true)
|
|
234
225
|
if (message.type === 'description' && message.description) {
|
|
235
|
-
if (message.description.type === 'answer' &&
|
|
226
|
+
if (message.description.type === 'answer' && peerConnection.signalingState === 'stable') return
|
|
236
227
|
await applyRemoteDescription(message.description)
|
|
237
228
|
if (message.description.type === 'offer') {
|
|
238
|
-
const answer = await
|
|
239
|
-
await
|
|
229
|
+
const answer = await peerConnection.createAnswer()
|
|
230
|
+
await peerConnection.setLocalDescription(answer)
|
|
240
231
|
await flushQueuedIceCandidates()
|
|
241
232
|
await waitForIceGatheringComplete()
|
|
242
233
|
await sendSignal({
|
|
243
234
|
type: 'description',
|
|
244
|
-
description:
|
|
235
|
+
description: peerConnection.localDescription?.toJSON?.() ?? peerConnection.localDescription ?? answer,
|
|
245
236
|
})
|
|
246
237
|
await pipe.maybeSendAuth()
|
|
247
238
|
}
|
|
248
239
|
return
|
|
249
240
|
}
|
|
250
241
|
if (message.type === 'ice' && message.candidate) {
|
|
251
|
-
if (!remoteDescriptionSet || !
|
|
242
|
+
if (!remoteDescriptionSet || !peerConnection.localDescription || peerConnection.signalingState !== 'stable') {
|
|
252
243
|
remoteSignalQueue.push(message.candidate)
|
|
253
244
|
return
|
|
254
245
|
}
|
|
255
246
|
try {
|
|
256
|
-
await
|
|
247
|
+
await peerConnection.addIceCandidate(message.candidate)
|
|
257
248
|
}
|
|
258
249
|
catch (error) {
|
|
259
250
|
if (shouldRetryQueuedIce(error)) {
|
|
@@ -276,7 +267,7 @@ export async function createWebRtcLink(options) {
|
|
|
276
267
|
attachChannelMessageListener(channel, data => {
|
|
277
268
|
try {
|
|
278
269
|
if (typeof data === 'string') pipe.handleInbound(data)
|
|
279
|
-
else pipe.handleInbound(
|
|
270
|
+
else pipe.handleInbound(dataToBytes(data))
|
|
280
271
|
}
|
|
281
272
|
catch { /* drop */ }
|
|
282
273
|
})
|
|
@@ -307,36 +298,36 @@ export async function createWebRtcLink(options) {
|
|
|
307
298
|
void handleRemoteSignal(message).catch(error => pipe.close(`signal-error:${formatErrorReason(error)}`))
|
|
308
299
|
}) ?? null
|
|
309
300
|
|
|
310
|
-
attachIceCandidateListener(
|
|
301
|
+
attachIceCandidateListener(peerConnection, event => {
|
|
311
302
|
if (trickleIceOff || !event.candidate) return
|
|
312
303
|
void sendSignal({
|
|
313
304
|
type: 'ice',
|
|
314
305
|
candidate: typeof event.candidate.toJSON === 'function' ? event.candidate.toJSON() : event.candidate,
|
|
315
306
|
}).catch(error => pipe.close(`signal-send-failed:${formatErrorReason(error)}`))
|
|
316
307
|
})
|
|
317
|
-
attachDataChannelListener(
|
|
308
|
+
attachDataChannelListener(peerConnection, event => {
|
|
318
309
|
void attachChannel(event.channel)
|
|
319
310
|
.then(() => maybeStartPostOpenFlow())
|
|
320
311
|
.catch(error => pipe.close(`channel-attach-failed:${error?.message ?? error}`))
|
|
321
312
|
})
|
|
322
313
|
|
|
323
314
|
/** 连接失败/关闭时关掉 pipe。 */
|
|
324
|
-
|
|
325
|
-
if (['failed', 'closed', 'disconnected'].includes(
|
|
315
|
+
peerConnection.onconnectionstatechange = () => {
|
|
316
|
+
if (['failed', 'closed', 'disconnected'].includes(peerConnection.connectionState)) {
|
|
326
317
|
reconnectCount++
|
|
327
|
-
void pipe.close(`connection-${
|
|
318
|
+
void pipe.close(`connection-${peerConnection.connectionState}`)
|
|
328
319
|
}
|
|
329
320
|
}
|
|
330
321
|
|
|
331
322
|
if (options.initiator) {
|
|
332
|
-
await attachChannel(
|
|
333
|
-
await attachChannel(
|
|
334
|
-
const offer = await
|
|
335
|
-
await
|
|
323
|
+
await attachChannel(peerConnection.createDataChannel(CHANNEL_CONTROL))
|
|
324
|
+
await attachChannel(peerConnection.createDataChannel(CHANNEL_BULK))
|
|
325
|
+
const offer = await peerConnection.createOffer()
|
|
326
|
+
await peerConnection.setLocalDescription(offer)
|
|
336
327
|
await waitForIceGatheringComplete()
|
|
337
328
|
await sendSignal({
|
|
338
329
|
type: 'description',
|
|
339
|
-
description:
|
|
330
|
+
description: peerConnection.localDescription?.toJSON?.() ?? peerConnection.localDescription ?? offer,
|
|
340
331
|
})
|
|
341
332
|
}
|
|
342
333
|
|
package/link/rtc.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
2
|
|
|
3
|
+
import { toBytes } from '../core/bytes_codec.mjs'
|
|
3
4
|
import { getSignalingRuntimeConfig } from '../node/instance.mjs'
|
|
4
5
|
import { wrapRtcPeerConnectionForMdns } from '../transport/rtc_mdns_filter.mjs'
|
|
5
6
|
|
|
@@ -96,14 +97,25 @@ export function waitForChannelState(channel, eventName, timeoutMs) {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
/**
|
|
99
|
-
*
|
|
100
|
-
* @param {
|
|
101
|
-
* @
|
|
100
|
+
* 绑定 data channel message 回调(addEventListener / onmessage / onMessage.subscribe)。
|
|
101
|
+
* @param {RTCDataChannel} channel data channel
|
|
102
|
+
* @param {(data: unknown) => void} handler 消息回调
|
|
103
|
+
* @returns {void}
|
|
104
|
+
*/
|
|
105
|
+
export function attachChannelMessageListener(channel, handler) {
|
|
106
|
+
channel.addEventListener?.('message', event => handler(event?.data))
|
|
107
|
+
/**
|
|
108
|
+
* @param {{ data?: unknown }} event message 事件
|
|
109
|
+
* @returns {void}
|
|
110
|
+
*/
|
|
111
|
+
channel.onmessage = event => handler(event?.data)
|
|
112
|
+
channel.onMessage?.subscribe(message => handler(message))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {unknown} data 通道原始数据
|
|
117
|
+
* @returns {Uint8Array} 字节
|
|
102
118
|
*/
|
|
103
|
-
export function
|
|
104
|
-
|
|
105
|
-
if (data instanceof ArrayBuffer) return new Uint8Array(data)
|
|
106
|
-
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
107
|
-
if (typeof data === 'string') return new TextEncoder().encode(data)
|
|
108
|
-
return new TextEncoder().encode(String(data ?? ''))
|
|
119
|
+
export function dataToBytes(data) {
|
|
120
|
+
return toBytes(data, { allowString: true })
|
|
109
121
|
}
|
|
@@ -147,7 +147,7 @@ export async function respondMailboxWant(want, sendGive, peerId) {
|
|
|
147
147
|
const { getMailboxRecords, takeMailboxForRecipient } = await import('./store.mjs')
|
|
148
148
|
const recipient = normalizeHex64(want.toPubKeyHash)
|
|
149
149
|
if (!recipient) return
|
|
150
|
-
const ids =
|
|
150
|
+
const ids = want.ids || []
|
|
151
151
|
const rows = (ids.length
|
|
152
152
|
? await getMailboxRecords(ids)
|
|
153
153
|
: await takeMailboxForRecipient(recipient)
|
package/node/identity.mjs
CHANGED
|
@@ -66,9 +66,9 @@ export function getNodeHash() {
|
|
|
66
66
|
*/
|
|
67
67
|
export function getNodeTransportSettings() {
|
|
68
68
|
const data = loadNodeFile()
|
|
69
|
-
const relayUrls =
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const relayUrls = (data.relayUrls || [])
|
|
70
|
+
.map(url => url.trim())
|
|
71
|
+
.filter(url => url.startsWith('wss://'))
|
|
72
72
|
const batterySaver = !!data.batterySaver
|
|
73
73
|
const mailbox = normalizeMailboxSettings(data.mailbox || {})
|
|
74
74
|
return { relayUrls, batterySaver, mailbox }
|
|
@@ -82,7 +82,7 @@ export function saveNodeTransportSettings(patch) {
|
|
|
82
82
|
const data = loadNodeFile()
|
|
83
83
|
if (patch.batterySaver != null) data.batterySaver = !!patch.batterySaver
|
|
84
84
|
if (patch.relayUrls)
|
|
85
|
-
data.relayUrls = patch.relayUrls.map(url =>
|
|
85
|
+
data.relayUrls = patch.relayUrls.map(url => url.trim()).filter(url => url.startsWith('wss://'))
|
|
86
86
|
if (patch.mailbox)
|
|
87
87
|
data.mailbox = normalizeMailboxSettings({ ...data.mailbox, ...patch.mailbox })
|
|
88
88
|
saveNodeFile(data)
|
package/node/network.mjs
CHANGED
|
@@ -52,18 +52,18 @@ export function normalizeNetwork(raw) {
|
|
|
52
52
|
* @returns {string[]} 去重 nodeHash 列表
|
|
53
53
|
*/
|
|
54
54
|
const pickIds = key => [...new Set(
|
|
55
|
-
(
|
|
56
|
-
.map(id => normalizeHex64(id) ||
|
|
55
|
+
(file[key] || [])
|
|
56
|
+
.map(id => normalizeHex64(id) || id.trim())
|
|
57
57
|
.filter(id => isHex64(id)),
|
|
58
58
|
)]
|
|
59
|
-
const hints = (
|
|
59
|
+
const hints = (file.hints || [])
|
|
60
60
|
.map(hint => ({
|
|
61
|
-
nodeHash: normalizeHex64(hint
|
|
62
|
-
source:
|
|
63
|
-
kind:
|
|
64
|
-
weight: Number.isFinite(Number(hint
|
|
65
|
-
expiresAt: Number(hint
|
|
66
|
-
...hint
|
|
61
|
+
nodeHash: normalizeHex64(hint.nodeHash) || '',
|
|
62
|
+
source: hint.source?.trim() || '',
|
|
63
|
+
kind: hint.kind?.trim() || '',
|
|
64
|
+
weight: Number.isFinite(Number(hint.weight)) ? Number(hint.weight) : 0.1,
|
|
65
|
+
expiresAt: Number(hint.expiresAt) || 0,
|
|
66
|
+
...hint.groupId ? { groupId: hint.groupId.trim() } : {},
|
|
67
67
|
}))
|
|
68
68
|
.filter(hint => isHex64(hint.nodeHash))
|
|
69
69
|
return {
|
package/overlay/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
2
|
|
|
3
3
|
import { pubKeyHash, sign, verify } from '../crypto/crypto.mjs'
|
|
4
|
-
import {
|
|
4
|
+
import { randomFrameIdHex } from '../link/frame.mjs'
|
|
5
5
|
import { createLruMap } from '../utils/lru.mjs'
|
|
6
6
|
|
|
7
7
|
const ROUTE_DOMAIN = 'fount-route'
|
|
@@ -50,12 +50,12 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
50
50
|
*/
|
|
51
51
|
async function handleOverlay(senderNodeHash, envelope) {
|
|
52
52
|
const payload = envelope?.payload
|
|
53
|
-
const action =
|
|
54
|
-
if (!payload
|
|
53
|
+
const action = envelope?.action || ''
|
|
54
|
+
if (!payload) return
|
|
55
55
|
if (action === 'route_req') {
|
|
56
|
-
const reqId =
|
|
57
|
-
const target =
|
|
58
|
-
const hops =
|
|
56
|
+
const reqId = payload.reqId || ''
|
|
57
|
+
const target = payload.target || ''
|
|
58
|
+
const hops = payload.path || []
|
|
59
59
|
const remainingTtl = Number(payload.ttl)
|
|
60
60
|
if (!reqId || !target || !hops.length || remainingTtl <= 0) return
|
|
61
61
|
if (seenReqs.has(reqId) || hops.includes(selfNodeHash) || hops.length > 6) return
|
|
@@ -86,10 +86,10 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
86
86
|
return
|
|
87
87
|
}
|
|
88
88
|
if (action === 'route_resp') {
|
|
89
|
-
const reqId =
|
|
90
|
-
const path =
|
|
91
|
-
const nodePubKey =
|
|
92
|
-
const sigHex =
|
|
89
|
+
const reqId = payload.reqId || ''
|
|
90
|
+
const path = payload.path || []
|
|
91
|
+
const nodePubKey = payload.nodePubKey || ''
|
|
92
|
+
const sigHex = payload.sig || ''
|
|
93
93
|
if (!reqId || path.length < 2 || !nodePubKey || !sigHex) return
|
|
94
94
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== path[path.length - 1]) return
|
|
95
95
|
const ok = await verify(Buffer.from(sigHex, 'hex'), routeSignBytes(reqId, path), Buffer.from(nodePubKey, 'hex'))
|
|
@@ -108,7 +108,7 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
108
108
|
return
|
|
109
109
|
}
|
|
110
110
|
if (action === 'relay') {
|
|
111
|
-
const path =
|
|
111
|
+
const path = payload.path || []
|
|
112
112
|
const index = Number(payload.idx)
|
|
113
113
|
if (!path.length || path[index] !== selfNodeHash) return
|
|
114
114
|
if (index === path.length - 1) {
|
|
@@ -137,7 +137,7 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
137
137
|
* @returns {Promise<string[]>} 从本节点到目标的 nodeHash 路径
|
|
138
138
|
*/
|
|
139
139
|
async discoverRoute(targetNodeHash, options = {}) {
|
|
140
|
-
const reqId =
|
|
140
|
+
const reqId = randomFrameIdHex()
|
|
141
141
|
const maxTtl = Number(options.ttl) || ttl
|
|
142
142
|
const timeoutMs = Number(options.timeoutMs) || 10_000
|
|
143
143
|
const promise = new Promise((resolve, reject) => {
|
|
@@ -164,7 +164,7 @@ export function createOverlayRouter(registry, ttl = 3) {
|
|
|
164
164
|
* @returns {Promise<void>}
|
|
165
165
|
*/
|
|
166
166
|
async relay(path, body) {
|
|
167
|
-
if (
|
|
167
|
+
if (path?.[0] !== selfNodeHash || path.length < 2)
|
|
168
168
|
throw new Error('overlay: invalid relay path')
|
|
169
169
|
await sendOverlay(path[1], { action: 'relay', path, idx: 1, body })
|
|
170
170
|
},
|