@steve02081504/fount-p2p 0.0.48 → 0.0.49

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/AGENTS.md CHANGED
@@ -54,6 +54,7 @@ Deno / native / BT: [runtime.md](docs/runtime.md).
54
54
  - Assertions: `test/helpers/assert.mjs`
55
55
  - Fixed-seed identity: `test/helpers/identity.mjs`
56
56
  - Mock discovery: `test/helpers/mock_discovery.mjs`
57
+ - Transport fakes must mirror the backend's failure signalling: a fake `RTCPeerConnection` whose `close()` stays silent (no `connectionstatechange`) hides re-entrancy bugs where a replaced connection closes the live one (fount-p2p#39, `link/providers/webrtc.mjs`).
57
58
 
58
59
  ## Static checks (`scripts/checks/`, ported from fount's check standard)
59
60
 
@@ -135,7 +135,7 @@ export async function createWebRtcLink(options) {
135
135
  const rungs = iceLocalHostnameLadder(icePolicy)
136
136
  const remoteSignalQueue = []
137
137
  const seenRemoteSignals = createLruMap(1024)
138
- /** @type {InstanceType<import('../rtc/polyfill.mjs').LoadedRtcPolyfill['RTCPeerConnection']> & { onicecandidate?: unknown, ondatachannel?: unknown, onconnectionstatechange?: unknown }} */
138
+ /** @type {InstanceType<import('../rtc/polyfill.mjs').LoadedRtcPolyfill['RTCPeerConnection']> & { ondatachannel?: unknown, onconnectionstatechange?: unknown, candidateCountHandler?: unknown }} */
139
139
  let peerConnection = null
140
140
  let remoteDescriptionSet = false
141
141
  let controlChannel = null
@@ -269,8 +269,12 @@ export async function createWebRtcLink(options) {
269
269
  remoteSignalQueue.length = 0
270
270
  localCandidateCount = 0
271
271
  attachPeerConnection(peerConnection)
272
- if (previous)
272
+ if (previous) {
273
+ // 先摘回调再关:旧连接 close() 会经 connectionstatechange 触发 pipe.close,而
274
+ // closeTransport() 关的是 peerConnection 这个变量(此刻已指向新连接),于是刚建好的一级被自己关掉(fount-p2p#39)。
275
+ detachPeerConnection(previous)
273
276
  try { await previous.close() } catch { /* ignore */ }
277
+ }
274
278
  nodeDebug('p2p:webrtc ice rung', {
275
279
  rung: rungIndex,
276
280
  policy: rungs[rungIndex],
@@ -288,22 +292,29 @@ export async function createWebRtcLink(options) {
288
292
 
289
293
  /**
290
294
  * 给 peer connection 挂上事件与回调(换级重建后需重新挂)。
295
+ *
296
+ * 回调一律用传入的这个连接,不读 `peerConnection` 闭包变量:换级后旧连接仍可能派发事件,
297
+ * 读变量就会把事件算到新连接头上。
291
298
  * @param {RTCPeerConnection} connection peer connection
292
299
  * @returns {void}
293
300
  */
294
301
  function attachPeerConnection(connection) {
295
302
  // 候选随 description 一次性带出(对端要先有 remoteDescription 才吃候选),故这里只计数、不外发。
296
- // 事件 API 由后端提供(W3C 是 addEventListener);缺失时退化为 onicecandidate 计数。
303
+ // 事件 API 由后端提供(W3C 是 addEventListener);缺失时退化为 onicecandidate,摘的时候看 candidateCountHandler 在不在。
297
304
  const countCandidate = event => {
298
305
  if (event?.candidate) localCandidateCount++
299
306
  }
300
- if (connection.addEventListener) connection.addEventListener('icecandidate', countCandidate)
307
+ if (connection.addEventListener) {
308
+ connection.candidateCountHandler = countCandidate
309
+ connection.addEventListener('icecandidate', countCandidate)
310
+ }
301
311
  else {
302
312
  const previousHandler = connection.onicecandidate
303
- connection.onicecandidate = event => {
313
+ connection.candidateCountHandler = event => {
304
314
  countCandidate(event)
305
315
  previousHandler?.(event)
306
316
  }
317
+ connection.onicecandidate = connection.candidateCountHandler
307
318
  }
308
319
  connection.ondatachannel = event => {
309
320
  attachChannel(event.channel)
@@ -317,6 +328,19 @@ export async function createWebRtcLink(options) {
317
328
  }
318
329
  }
319
330
 
331
+ /**
332
+ * 摘掉某一级 peer connection 上挂的事件与回调(重建前必须调用,见 [buildPeerConnection])。
333
+ * @param {RTCPeerConnection} connection 待摘的连接
334
+ * @returns {void}
335
+ */
336
+ function detachPeerConnection(connection) {
337
+ if (connection.candidateCountHandler) connection.removeEventListener?.('icecandidate', connection.candidateCountHandler)
338
+ else connection.onicecandidate = null
339
+ delete connection.candidateCountHandler
340
+ connection.ondatachannel = null
341
+ connection.onconnectionstatechange = null
342
+ }
343
+
320
344
  /**
321
345
  * 等本地候选收齐(停滞判定见 [collectIceGathering]),并回报本次是否产出了候选。
322
346
  * @returns {Promise<boolean>} 是否收到过候选
@@ -469,7 +493,7 @@ export async function createWebRtcLink(options) {
469
493
  void handleRemoteSignal(message).catch(error => pipe.close(`signal-error:${formatErrorReason(error)}`))
470
494
  }) ?? null
471
495
 
472
- // 信令随时可能到,故先为当前一级建好 pc(发起方随后会在阶梯里逐级重建)。
496
+ // 信令随时可能到,故先为当前一级(第 0 级)建好 pc;发起方阶梯的首次迭代直接用它,不重建。
473
497
  await ensurePeerConnection()
474
498
 
475
499
  if (options.initiator) {
@@ -478,7 +502,8 @@ export async function createWebRtcLink(options) {
478
502
  const ladderDeadline = Date.now() + handshakeTimeoutMs
479
503
  let sentOffer = false
480
504
  for (let index = 0; index < rungs.length; index++) {
481
- await buildPeerConnection(index)
505
+ // 同一级不重建:换 pc 会牵动并发信令与旧连接的关闭,只在真的换级时才需要(fount-p2p#39)。
506
+ if (rungIndex !== index) await buildPeerConnection(index)
482
507
  attachChannel(peerConnection.createDataChannel(CHANNEL_CONTROL))
483
508
  attachChannel(peerConnection.createDataChannel(CHANNEL_BULK))
484
509
  const offer = await peerConnection.createOffer()
@@ -491,7 +516,8 @@ export async function createWebRtcLink(options) {
491
516
  })
492
517
  // 候选集为空说明这一级策略把本机候选全滤掉了(例如只产出 mDNS 候选),换更宽松的一级重建。
493
518
  // 只在预算内、且还有更宽松的一级时升级;`rewrite-loopback` 起点不会走到这里(阶梯只有它自己)。
494
- if (!hasCandidates && Date.now() < ladderDeadline && index + 1 < rungs.length) {
519
+ const escalatable = index + 1 < rungs.length
520
+ if (!hasCandidates && escalatable && Date.now() < ladderDeadline) {
495
521
  nodeDebug('p2p:webrtc ice rung escalated', {
496
522
  from: rungs[index],
497
523
  to: rungs[index + 1],
@@ -509,7 +535,7 @@ export async function createWebRtcLink(options) {
509
535
  nodeDebug('p2p:webrtc offer has no usable ice candidates', {
510
536
  rung: index,
511
537
  policy: rungs[index],
512
- escalatable: index + 1 < rungs.length,
538
+ escalatable,
513
539
  iceServers: options.iceServers?.length || 0,
514
540
  })
515
541
  break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve02081504/fount-p2p",
3
- "version": "0.0.48",
3
+ "version": "0.0.49",
4
4
  "description": "fount federation P2P layer — link, trust graph, mailbox, DAG, EVFS.",
5
5
  "keywords": [
6
6
  "network",