@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.
- package/README.md +42 -11
- package/core/bytes_codec.mjs +65 -10
- package/core/composite_key.mjs +5 -5
- package/core/tcp_port.mjs +1 -1
- package/crypto/checkpoint_sign.mjs +2 -2
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/canonicalize_row.mjs +3 -3
- package/dag/index.mjs +2 -3
- 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 +154 -113
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +5 -12
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +228 -0
- package/discovery/nostr.mjs +430 -141
- package/federation/chunk_fetch_pending.mjs +14 -18
- package/federation/dag_order_cache.mjs +1 -1
- package/federation/entity_key_chain.mjs +3 -4
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/federation/topo_order_memo.mjs +11 -4
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_fetch.mjs +2 -2
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +31 -30
- 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/channel_mux.mjs +10 -10
- package/link/frame.mjs +76 -131
- package/link/handshake.mjs +19 -20
- package/link/pipe.mjs +53 -79
- package/link/providers/ble_gatt.mjs +19 -31
- 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 +24 -12
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +6 -6
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/identity.mjs +4 -4
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +46 -14
- 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 +35 -14
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -182
- 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 +17 -0
- package/transport/group_link_set.mjs +165 -134
- package/transport/ice_servers.mjs +2 -2
- package/transport/link_registry.mjs +283 -124
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +48 -47
- package/transport/peer_pool.mjs +169 -78
- package/transport/remote_user_room.mjs +18 -23
- package/transport/rtc_connection_budget.mjs +18 -4
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +172 -104
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +93 -161
- 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 +1 -4
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +11 -0
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/utils/shuffle.mjs +13 -0
- package/utils/ttl_map.mjs +29 -4
- package/wire/ingress.mjs +1 -1
- package/wire/part_ingress.mjs +38 -36
- package/wire/part_invoke.mjs +4 -4
- package/wire/part_query.mjs +28 -23
- package/wire/part_query.tunables.json +6 -1
- package/wire/part_query_cache.mjs +1 -1
- package/wire/volatile_signature.mjs +1 -1
- package/discovery/mdns.mjs +0 -191
- package/transport/signal_crypto.mjs +0 -82
|
@@ -4,9 +4,10 @@ import { randomBytes } from 'node:crypto'
|
|
|
4
4
|
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
5
5
|
import { getBtPeerHint } from '../../discovery/bt/peer_hints.mjs'
|
|
6
6
|
import { canUseBluetoothRuntime, loadBleno, loadNoble, resolveBtRole, waitPoweredOn } from '../../discovery/bt/runtime.mjs'
|
|
7
|
-
import { asLinkHandle
|
|
7
|
+
import { asLinkHandle } from '../pipe.mjs'
|
|
8
8
|
|
|
9
9
|
import { LINK_LEVEL_BLE_GATT } from './levels.mjs'
|
|
10
|
+
import { buildLinkOpen, createLinkIdBoundPipe, parseLinkOpen } from './link_id_pipe.mjs'
|
|
10
11
|
|
|
11
12
|
/** BLE GATT 数据 service UUID。 */
|
|
12
13
|
export const BLE_DATA_SERVICE_UUID = 'f017f017f017f017f017f017f017f019'
|
|
@@ -28,19 +29,13 @@ export async function canUseBleGattLink() {
|
|
|
28
29
|
* @returns {Promise<import('./index.mjs').LinkHandle>} 已启动握手的 link
|
|
29
30
|
*/
|
|
30
31
|
async function openGattPipe(options) {
|
|
31
|
-
const
|
|
32
|
-
if (!linkId) throw new Error('p2p: ble_gatt linkId required')
|
|
33
|
-
|
|
34
|
-
const pipe = createLinkPipe({
|
|
32
|
+
const pipe = createLinkIdBoundPipe({
|
|
35
33
|
providerId: 'ble_gatt',
|
|
36
34
|
level: LINK_LEVEL_BLE_GATT,
|
|
37
35
|
initiator: !!options.initiator,
|
|
36
|
+
linkId: options.linkId,
|
|
38
37
|
nodeHash: options.nodeHash,
|
|
39
38
|
localIdentity: options.localIdentity,
|
|
40
|
-
/** @returns {string} 本端 binding(linkId) */
|
|
41
|
-
getLocalBinding: () => linkId,
|
|
42
|
-
/** @returns {string} 对端 binding(linkId) */
|
|
43
|
-
getRemoteBinding: () => linkId,
|
|
44
39
|
/**
|
|
45
40
|
* @param {string} text control JSON
|
|
46
41
|
* @returns {Promise<void>}
|
|
@@ -60,18 +55,17 @@ async function openGattPipe(options) {
|
|
|
60
55
|
})
|
|
61
56
|
|
|
62
57
|
const stopNotify = options.onNotify(data => {
|
|
63
|
-
pipe.handleInbound(
|
|
58
|
+
pipe.handleInbound(data)
|
|
64
59
|
})
|
|
65
60
|
pipe.onDown(() => {
|
|
66
61
|
try { stopNotify() } catch { /* ignore */ }
|
|
67
62
|
})
|
|
68
63
|
|
|
69
64
|
if (options.initiator)
|
|
70
|
-
await Promise.resolve(options.write(Buffer.from(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}), 'utf8')))
|
|
65
|
+
await Promise.resolve(options.write(Buffer.from(
|
|
66
|
+
buildLinkOpen(normalizeHex64(options.linkId), options.localIdentity?.nodeHash),
|
|
67
|
+
'utf8',
|
|
68
|
+
)))
|
|
75
69
|
|
|
76
70
|
await pipe.startHandshake()
|
|
77
71
|
return asLinkHandle(pipe)
|
|
@@ -131,7 +125,7 @@ async function dialBleGatt(options) {
|
|
|
131
125
|
/** @type {Set<(data: Buffer) => void>} */
|
|
132
126
|
const notifyHandlers = new Set()
|
|
133
127
|
characteristic.on('data', data => {
|
|
134
|
-
if (data
|
|
128
|
+
if (!data) return
|
|
135
129
|
for (const handler of notifyHandlers)
|
|
136
130
|
handler(Buffer.from(data))
|
|
137
131
|
})
|
|
@@ -213,9 +207,9 @@ export function createBleGattLinkProvider() {
|
|
|
213
207
|
* @returns {void}
|
|
214
208
|
*/
|
|
215
209
|
onWriteRequest(_connection, data, _offset, _withoutResponse, callback) {
|
|
216
|
-
const
|
|
217
|
-
if (sessionInbound) sessionInbound(
|
|
218
|
-
else void acceptPeripheralWrite(
|
|
210
|
+
const buffer = Buffer.from(data)
|
|
211
|
+
if (sessionInbound) sessionInbound(buffer)
|
|
212
|
+
else void acceptPeripheralWrite(buffer).catch(() => { })
|
|
219
213
|
callback(bleno.Characteristic.RESULT_SUCCESS)
|
|
220
214
|
},
|
|
221
215
|
})
|
|
@@ -234,25 +228,19 @@ export function createBleGattLinkProvider() {
|
|
|
234
228
|
}
|
|
235
229
|
|
|
236
230
|
/**
|
|
237
|
-
* @param {Buffer}
|
|
231
|
+
* @param {Buffer} buffer 入站写(首包应为 link-open)
|
|
238
232
|
* @returns {Promise<void>}
|
|
239
233
|
*/
|
|
240
|
-
async function acceptPeripheralWrite(
|
|
234
|
+
async function acceptPeripheralWrite(buffer) {
|
|
241
235
|
if (activeInbound || acceptInflight || !onInbound || !localIdentity) return
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
parsed = JSON.parse(Buffer.from(buf).toString('utf8'))
|
|
245
|
-
}
|
|
246
|
-
catch {
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
if (parsed?.type !== 'link-open' || !parsed.linkId) return
|
|
236
|
+
const opened = parseLinkOpen(buffer)
|
|
237
|
+
if (!opened) return
|
|
250
238
|
acceptInflight = true
|
|
251
239
|
try {
|
|
252
240
|
const link = await openGattPipe({
|
|
253
241
|
initiator: false,
|
|
254
|
-
linkId:
|
|
255
|
-
nodeHash:
|
|
242
|
+
linkId: opened.linkId,
|
|
243
|
+
nodeHash: opened.from,
|
|
256
244
|
localIdentity,
|
|
257
245
|
/**
|
|
258
246
|
* @param {Buffer} data 出站
|
|
@@ -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,7 +1,8 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { wrapRtcPeerConnectionForIceLocalHostname } from '../transport/rtc_ice_local_hostname.mjs'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 注册进程退出时销毁 libdatachannel 全部原生资源(仅一次)。
|
|
@@ -19,9 +20,9 @@ process.on('exit', () => {
|
|
|
19
20
|
*/
|
|
20
21
|
export async function loadNodeRtcPolyfill() {
|
|
21
22
|
const mod = await import('node-datachannel/polyfill')
|
|
22
|
-
const {
|
|
23
|
+
const { iceLocalHostnamePolicy } = getSignalingRuntimeConfig()
|
|
23
24
|
return {
|
|
24
|
-
RTCPeerConnection:
|
|
25
|
+
RTCPeerConnection: wrapRtcPeerConnectionForIceLocalHostname(mod.RTCPeerConnection, mod.RTCIceCandidate, iceLocalHostnamePolicy),
|
|
25
26
|
RTCIceCandidate: mod.RTCIceCandidate,
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -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
|
}
|
|
@@ -54,8 +54,8 @@ export async function dispatchMailboxRecordsToConsumers(username, records) {
|
|
|
54
54
|
const ids = await handler(username, scoped)
|
|
55
55
|
for (const id of ids || []) delivered.add(String(id))
|
|
56
56
|
}
|
|
57
|
-
catch (
|
|
58
|
-
console.error('mailbox: consumer batch failed, retry per record',
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error('mailbox: consumer batch failed, retry per record', error)
|
|
59
59
|
for (const row of scoped)
|
|
60
60
|
try {
|
|
61
61
|
const ids = await handler(username, [row])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
2
|
import { getNodeTransportSettings, getNodeHash } from '../node/identity.mjs'
|
|
3
|
-
import {
|
|
3
|
+
import { activeLinkRoster, deliverToUserRoomPeers } from '../transport/user_room.mjs'
|
|
4
4
|
import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
|
|
5
5
|
|
|
6
6
|
import { allowMailboxRelayForTier } from './importance.mjs'
|
|
@@ -23,9 +23,9 @@ import {
|
|
|
23
23
|
* @returns {Promise<string | null>} 已验证的 remote nodeHash
|
|
24
24
|
*/
|
|
25
25
|
async function resolveRemoteNodeHashForPeer(username, peerId) {
|
|
26
|
+
void username
|
|
26
27
|
if (!peerId) return null
|
|
27
|
-
const
|
|
28
|
-
const entry = slot?.getRoster()?.find(row => row.peerId === peerId)
|
|
28
|
+
const entry = activeLinkRoster().find(row => row.peerId === peerId)
|
|
29
29
|
const remote = entry?.remoteNodeHash?.trim().toLowerCase()
|
|
30
30
|
return remote || null
|
|
31
31
|
}
|
|
@@ -51,9 +51,9 @@ async function resolveRelayHopForIngress(record) {
|
|
|
51
51
|
* @returns {Promise<{ maxHop: number, relayFanoutTrusted: number, relayFanoutNormal: number, wantFanout: number, batterySaver: boolean }>} 按在线 peer 数缩放的路由
|
|
52
52
|
*/
|
|
53
53
|
async function resolveRouting(username) {
|
|
54
|
+
void username
|
|
54
55
|
const { batterySaver, mailbox } = getNodeTransportSettings()
|
|
55
|
-
const
|
|
56
|
-
const peerCount = slot?.getRoster()?.length ?? 0
|
|
56
|
+
const peerCount = activeLinkRoster().length
|
|
57
57
|
return resolveMailboxRoutingForPeerCount(peerCount, mailbox, batterySaver)
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -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/mailbox/wire.mjs
CHANGED
|
@@ -11,30 +11,34 @@ import { parseMailboxGive, parseMailboxPut, parseMailboxWant } from './parse.mjs
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* @param {MailboxWireContext} wireContext 入站上下文
|
|
14
|
-
* @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire Trystero 适配器
|
|
15
|
-
* @returns {void}
|
|
14
|
+
* @param {{ on: (name: string, handler: (payload: unknown, peerId: string) => void) => (() => void) | void, send: (name: string, payload: unknown, peerId: string | null) => void }} wire Trystero 适配器
|
|
15
|
+
* @returns {() => void} 取消挂载的 dispose
|
|
16
16
|
*/
|
|
17
17
|
export function attachMailboxWire(wireContext, wire) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
18
|
+
const offs = [
|
|
19
|
+
wire.on('mailbox_put', (payload, peerId) => {
|
|
20
|
+
const put = parseMailboxPut(payload)
|
|
21
|
+
if (!put.ok) return
|
|
22
|
+
void ingestMailboxPut(wireContext, put.value, peerId).catch(error => console.error('mailbox: put ingest failed', error))
|
|
23
|
+
}),
|
|
24
|
+
wire.on('mailbox_want', (payload, peerId) => {
|
|
25
|
+
const want = parseMailboxWant(payload)
|
|
26
|
+
if (!want.ok) return
|
|
27
|
+
void respondMailboxWant(want.value, (giveWire, targetPeerId) => {
|
|
28
|
+
try {
|
|
29
|
+
wire.send('mailbox_give', giveWire, targetPeerId)
|
|
30
|
+
}
|
|
31
|
+
catch { /* disconnected */ }
|
|
32
|
+
}, peerId).catch(error => console.error('mailbox: want failed', error))
|
|
33
|
+
}),
|
|
34
|
+
wire.on('mailbox_give', payload => {
|
|
35
|
+
const give = parseMailboxGive(payload)
|
|
36
|
+
if (!give.ok) return
|
|
37
|
+
void ingestMailboxGive(wireContext, give.value).catch(error => console.error('mailbox: give ingest failed', error))
|
|
38
|
+
}),
|
|
39
|
+
]
|
|
40
|
+
return () => {
|
|
41
|
+
for (const off of offs)
|
|
42
|
+
try { off?.() } catch { /* ignore */ }
|
|
43
|
+
}
|
|
40
44
|
}
|