@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.
Files changed (102) hide show
  1. package/README.md +42 -11
  2. package/core/bytes_codec.mjs +65 -10
  3. package/core/composite_key.mjs +5 -5
  4. package/core/tcp_port.mjs +1 -1
  5. package/crypto/checkpoint_sign.mjs +2 -2
  6. package/crypto/crypto.mjs +3 -3
  7. package/crypto/key.mjs +12 -12
  8. package/dag/canonicalize_row.mjs +3 -3
  9. package/dag/index.mjs +2 -3
  10. package/dag/storage.mjs +36 -36
  11. package/discovery/advert_peer_hints.mjs +1 -1
  12. package/discovery/adverts.mjs +109 -0
  13. package/discovery/bt/index.mjs +154 -113
  14. package/discovery/bt/probe_child.mjs +2 -1
  15. package/discovery/bt/runtime.mjs +5 -12
  16. package/discovery/index.mjs +267 -62
  17. package/discovery/internal/signal_crypto.mjs +109 -0
  18. package/discovery/lan.mjs +228 -0
  19. package/discovery/nostr.mjs +430 -141
  20. package/federation/chunk_fetch_pending.mjs +14 -18
  21. package/federation/dag_order_cache.mjs +1 -1
  22. package/federation/entity_key_chain.mjs +3 -4
  23. package/federation/manifest_fetch_pending.mjs +1 -3
  24. package/federation/topo_order_memo.mjs +11 -4
  25. package/files/assemble.mjs +14 -14
  26. package/files/assemble_stream.mjs +6 -6
  27. package/files/chunk_fetch.mjs +2 -2
  28. package/files/chunk_responder.mjs +29 -20
  29. package/files/evfs.mjs +31 -30
  30. package/files/manifest_fetch.mjs +16 -3
  31. package/files/public_manifest.mjs +11 -11
  32. package/files/transfer_key_registry.mjs +2 -2
  33. package/index.mjs +86 -11
  34. package/infra/cli.mjs +62 -0
  35. package/infra/debug_log.mjs +56 -0
  36. package/infra/default_node_dir.mjs +22 -0
  37. package/infra/priority.mjs +56 -0
  38. package/infra/service.mjs +140 -0
  39. package/infra/tunables.json +5 -0
  40. package/link/channel_mux.mjs +10 -10
  41. package/link/frame.mjs +76 -131
  42. package/link/handshake.mjs +19 -20
  43. package/link/pipe.mjs +53 -79
  44. package/link/providers/ble_gatt.mjs +19 -31
  45. package/link/providers/lan_tcp.mjs +17 -32
  46. package/link/providers/link_id_pipe.mjs +46 -0
  47. package/link/providers/webrtc.mjs +42 -51
  48. package/link/rtc.mjs +24 -12
  49. package/mailbox/consumer_registry.mjs +2 -2
  50. package/mailbox/deliver_or_store.mjs +6 -6
  51. package/mailbox/wire.mjs +28 -24
  52. package/node/entity_store.mjs +6 -6
  53. package/node/identity.mjs +4 -4
  54. package/node/instance.mjs +46 -27
  55. package/node/local_data_revision.mjs +26 -0
  56. package/node/log.mjs +56 -0
  57. package/node/network.mjs +46 -14
  58. package/node/reputation_store.mjs +4 -4
  59. package/node/reputation_sync.mjs +318 -0
  60. package/node/routing_profile.mjs +21 -0
  61. package/node/signaling_config.mjs +30 -11
  62. package/overlay/index.mjs +35 -14
  63. package/package.json +13 -2
  64. package/rooms/scoped_link.mjs +17 -182
  65. package/schemas/discovery.mjs +1 -2
  66. package/schemas/federation_pull.mjs +2 -2
  67. package/schemas/part_query.mjs +1 -1
  68. package/schemas/remote_event.mjs +1 -1
  69. package/transport/advert_ingest.mjs +17 -0
  70. package/transport/group_link_set.mjs +165 -134
  71. package/transport/ice_servers.mjs +2 -2
  72. package/transport/link_registry.mjs +283 -124
  73. package/transport/mesh_keepalive.mjs +217 -0
  74. package/transport/node_scope.mjs +289 -0
  75. package/transport/offer_answer.mjs +48 -47
  76. package/transport/peer_pool.mjs +169 -78
  77. package/transport/remote_user_room.mjs +18 -23
  78. package/transport/rtc_connection_budget.mjs +18 -4
  79. package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
  80. package/transport/runtime_bootstrap.mjs +172 -104
  81. package/transport/tunables.json +11 -0
  82. package/transport/tunables.mjs +18 -0
  83. package/transport/user_room.mjs +93 -161
  84. package/trust_graph/build.mjs +0 -2
  85. package/trust_graph/cache.mjs +12 -3
  86. package/trust_graph/registry.mjs +6 -6
  87. package/trust_graph/send.mjs +1 -4
  88. package/utils/async_mutex.mjs +4 -4
  89. package/utils/emit_safe.mjs +11 -0
  90. package/utils/json_io.mjs +12 -12
  91. package/utils/map_pool.mjs +5 -5
  92. package/utils/shuffle.mjs +13 -0
  93. package/utils/ttl_map.mjs +29 -4
  94. package/wire/ingress.mjs +1 -1
  95. package/wire/part_ingress.mjs +38 -36
  96. package/wire/part_invoke.mjs +4 -4
  97. package/wire/part_query.mjs +28 -23
  98. package/wire/part_query.tunables.json +6 -1
  99. package/wire/part_query_cache.mjs +1 -1
  100. package/wire/volatile_signature.mjs +1 -1
  101. package/discovery/mdns.mjs +0 -191
  102. 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, coercePipeInbound, createLinkPipe } from '../pipe.mjs'
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 linkId = normalizeHex64(options.linkId)
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(coercePipeInbound(data))
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(JSON.stringify({
71
- type: 'link-open',
72
- linkId,
73
- from: options.localIdentity?.nodeHash || '',
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 == null) return
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 buf = Buffer.from(data)
217
- if (sessionInbound) sessionInbound(buf)
218
- else void acceptPeripheralWrite(buf).catch(() => { })
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} buf 入站写(首包应为 link-open)
231
+ * @param {Buffer} buffer 入站写(首包应为 link-open)
238
232
  * @returns {Promise<void>}
239
233
  */
240
- async function acceptPeripheralWrite(buf) {
234
+ async function acceptPeripheralWrite(buffer) {
241
235
  if (activeInbound || acceptInflight || !onInbound || !localIdentity) return
242
- let parsed
243
- try {
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: parsed.linkId,
255
- nodeHash: normalizeHex64(parsed.from) || null,
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, coercePipeInbound, createLinkPipe } from '../pipe.mjs'
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 createLinkPipe>} pipe 句柄
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 = createLinkPipe({
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.socket
145
- /** @type {ReturnType<typeof createLinkPipe> | null} */
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(coercePipeInbound(payload))
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(coercePipeInbound(payload))
155
+ pipe.handleInbound(payload)
160
156
 
161
157
  if (options.initiator)
162
- codec.write(JSON.stringify({
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 createLinkPipe> | null} */
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(coercePipeInbound(payload))
237
+ pipe.handleInbound(payload)
246
238
  return
247
239
  }
248
- let parsed
249
- try {
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: parsed.linkId,
264
- nodeHash: normalizeHex64(parsed.from) || null,
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 = typeof addr === 'object' && addr ? addr.port : 0
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
- signalDataToBytes,
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 pc = new rtc.RTCPeerConnection(options.iceServers?.length ? { iceServers: options.iceServers } : undefined)
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(pc.localDescription?.sdp || ''),
101
+ getLocalBinding: () => extractDtlsFingerprint(peerConnection.localDescription?.sdp || ''),
117
102
  /** @returns {string} 对端 DTLS fingerprint */
118
- getRemoteBinding: () => extractDtlsFingerprint(pc.remoteDescription?.sdp || ''),
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 pc.close() } catch { /* ignore */ }
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: pc.connectionState,
152
- iceConnectionState: pc.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) 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)
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 pc.setRemoteDescription(description)
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 || pc.iceGatheringState === 'complete') return
188
+ if (!trickleIceOff || peerConnection.iceGatheringState === 'complete') return
200
189
  const deadline = Date.now() + handshakeTimeoutMs
201
- while (pc.iceGatheringState !== 'complete' && Date.now() < deadline)
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 || !pc.localDescription) return
198
+ if (!remoteDescriptionSet || !peerConnection.localDescription) return
210
199
  while (remoteSignalQueue.length) {
211
200
  const candidate = remoteSignalQueue.shift()
212
201
  try {
213
- await pc.addIceCandidate(candidate)
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 || typeof message !== 'object') return
231
- const signalKey = JSON.stringify(message)
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' && pc.signalingState === 'stable') return
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 pc.createAnswer()
239
- await pc.setLocalDescription(answer)
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: pc.localDescription?.toJSON?.() ?? pc.localDescription ?? answer,
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 || !pc.localDescription || pc.signalingState !== 'stable') {
242
+ if (!remoteDescriptionSet || !peerConnection.localDescription || peerConnection.signalingState !== 'stable') {
252
243
  remoteSignalQueue.push(message.candidate)
253
244
  return
254
245
  }
255
246
  try {
256
- await pc.addIceCandidate(message.candidate)
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(signalDataToBytes(data))
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(pc, event => {
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(pc, event => {
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
- pc.onconnectionstatechange = () => {
325
- if (['failed', 'closed', 'disconnected'].includes(pc.connectionState)) {
315
+ peerConnection.onconnectionstatechange = () => {
316
+ if (['failed', 'closed', 'disconnected'].includes(peerConnection.connectionState)) {
326
317
  reconnectCount++
327
- void pipe.close(`connection-${pc.connectionState}`)
318
+ void pipe.close(`connection-${peerConnection.connectionState}`)
328
319
  }
329
320
  }
330
321
 
331
322
  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)
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: pc.localDescription?.toJSON?.() ?? pc.localDescription ?? offer,
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 { wrapRtcPeerConnectionForMdns } from '../transport/rtc_mdns_filter.mjs'
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 { mdnsPolicy } = getSignalingRuntimeConfig()
23
+ const { iceLocalHostnamePolicy } = getSignalingRuntimeConfig()
23
24
  return {
24
- RTCPeerConnection: wrapRtcPeerConnectionForMdns(mod.RTCPeerConnection, mod.RTCIceCandidate, mdnsPolicy),
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
- * 将信令/消息数据统一转为 Uint8Array。
100
- * @param {unknown} data 原始数据(字符串、ArrayBuffer、TypedArray 等)
101
- * @returns {Uint8Array} 字节视图
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 signalDataToBytes(data) {
104
- if (data instanceof Uint8Array) return data
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 (err) {
58
- console.error('mailbox: consumer batch failed, retry per record', err)
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 { deliverToUserRoomPeers, ensureUserRoom } from '../transport/user_room.mjs'
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 slot = await ensureUserRoom({ replicaUsername: username })
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 slot = await ensureUserRoom({ replicaUsername: username })
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 = Array.isArray(want.ids) ? want.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
- wire.on('mailbox_put', (payload, peerId) => {
19
- const put = parseMailboxPut(payload)
20
- if (!put.ok) return
21
- void ingestMailboxPut(wireContext, put.value, peerId).catch(err => console.error('mailbox: put ingest failed', err))
22
- })
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(err => console.error('mailbox: want failed', err))
33
- })
34
-
35
- wire.on('mailbox_give', payload => {
36
- const give = parseMailboxGive(payload)
37
- if (!give.ok) return
38
- void ingestMailboxGive(wireContext, give.value).catch(err => console.error('mailbox: give ingest failed', err))
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
  }