@leofcoin/chain 1.7.99 → 1.7.100

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.
@@ -5317,6 +5317,10 @@ class ConnectionMonitor {
5317
5317
  #reconnectDelay = 5000;
5318
5318
  #healthCheckInterval = 10000;
5319
5319
  #version;
5320
+ // event handlers to remove later
5321
+ #onOnline = null;
5322
+ #onVisibilityChange = null;
5323
+ #onSigcont = null;
5320
5324
  get isMonitoring() {
5321
5325
  return this.#isMonitoring;
5322
5326
  }
@@ -5336,6 +5340,33 @@ class ConnectionMonitor {
5336
5340
  return;
5337
5341
  this.#isMonitoring = true;
5338
5342
  console.log('🔄 Starting connection monitor...');
5343
+ // Listen for resume/network events (browser + node/electron)
5344
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
5345
+ this.#onOnline = () => {
5346
+ console.log('🌐 Network online — attempting restore');
5347
+ void this.#restoreNetwork();
5348
+ };
5349
+ window.addEventListener('online', this.#onOnline);
5350
+ this.#onVisibilityChange = () => {
5351
+ if (document.visibilityState === 'visible') {
5352
+ console.log('💡 Visibility regained — attempting restore');
5353
+ void this.#restoreNetwork();
5354
+ }
5355
+ };
5356
+ document.addEventListener('visibilitychange', this.#onVisibilityChange);
5357
+ }
5358
+ if (typeof process !== 'undefined' && typeof process.on === 'function') {
5359
+ this.#onSigcont = () => {
5360
+ console.log('🔔 Process resumed (SIGCONT) — attempting restore');
5361
+ void this.#restoreNetwork();
5362
+ };
5363
+ try {
5364
+ process.on('SIGCONT', this.#onSigcont);
5365
+ }
5366
+ catch (e) {
5367
+ // ignore if not supported
5368
+ }
5369
+ }
5339
5370
  this.#checkInterval = setInterval(() => {
5340
5371
  this.#healthCheck();
5341
5372
  }, this.#healthCheckInterval);
@@ -5350,6 +5381,26 @@ class ConnectionMonitor {
5350
5381
  clearInterval(this.#checkInterval);
5351
5382
  this.#checkInterval = null;
5352
5383
  }
5384
+ // remove listeners
5385
+ if (typeof window !== 'undefined') {
5386
+ if (this.#onOnline) {
5387
+ window.removeEventListener('online', this.#onOnline);
5388
+ this.#onOnline = null;
5389
+ }
5390
+ if (this.#onVisibilityChange) {
5391
+ document.removeEventListener('visibilitychange', this.#onVisibilityChange);
5392
+ this.#onVisibilityChange = null;
5393
+ }
5394
+ }
5395
+ if (typeof process !== 'undefined' && typeof process.removeListener === 'function' && this.#onSigcont) {
5396
+ try {
5397
+ process.removeListener('SIGCONT', this.#onSigcont);
5398
+ }
5399
+ catch (e) {
5400
+ // ignore
5401
+ }
5402
+ this.#onSigcont = null;
5403
+ }
5353
5404
  console.log('âšī¸ Connection monitor stopped');
5354
5405
  }
5355
5406
  async #healthCheck() {
@@ -5369,12 +5420,14 @@ class ConnectionMonitor {
5369
5420
  const disconnectedPeers = this.disconnectedPeers;
5370
5421
  if (disconnectedPeers.length > 0) {
5371
5422
  console.warn(`âš ī¸ Disconnected peers: ${disconnectedPeers.map((peer) => peer.peerId).join(', ')}`);
5372
- // Attempt to reconnect each disconnected peer
5373
- const promises = [];
5423
+ // Attempt to reconnect each disconnected peer sequentially to avoid racing signaling/state
5374
5424
  for (const peer of disconnectedPeers) {
5375
- promises.push(this.#attemptPeerReconnection(peer));
5425
+ // small spacing between attempts to reduce signaling races
5426
+ // eslint-disable-next-line no-await-in-loop
5427
+ await new Promise((r) => setTimeout(r, 150));
5428
+ // eslint-disable-next-line no-await-in-loop
5429
+ await this.#attemptPeerReconnection(peer);
5376
5430
  }
5377
- await Promise.all(promises);
5378
5431
  }
5379
5432
  // Publish connection status
5380
5433
  globalThis.pubsub?.publish('connection-status', {
@@ -5384,54 +5437,169 @@ class ConnectionMonitor {
5384
5437
  });
5385
5438
  }
5386
5439
  async #attemptPeerReconnection(peer) {
5387
- if (this.#peerReconnectAttempts[peer.peerId] >= this.#maxReconnectAttempts) {
5388
- console.error('❌ Max reconnection attempts reached');
5389
- this.#peerReconnectAttempts[peer.peerId] = 0;
5440
+ if (!peer)
5441
+ return;
5442
+ const peerId = peer.peerId || peer.id;
5443
+ if (!peerId)
5444
+ return;
5445
+ if (!this.#peerReconnectAttempts[peerId]) {
5446
+ this.#peerReconnectAttempts[peerId] = 0;
5390
5447
  }
5391
- if (!this.#peerReconnectAttempts[peer.peerId]) {
5392
- this.#peerReconnectAttempts[peer.peerId] = 0;
5448
+ if (this.#peerReconnectAttempts[peerId] >= this.#maxReconnectAttempts) {
5449
+ console.error('❌ Max reconnection attempts reached for', peerId);
5450
+ this.#peerReconnectAttempts[peerId] = 0;
5451
+ return;
5393
5452
  }
5394
- this.#peerReconnectAttempts[peer.peerId]++;
5395
- console.log(`🔄 Attempting reconnection ${this.#peerReconnectAttempts[peer.peerId]}/${this.#maxReconnectAttempts}`);
5453
+ this.#peerReconnectAttempts[peerId]++;
5454
+ console.log(`🔄 Attempting reconnection ${this.#peerReconnectAttempts[peerId]}/${this.#maxReconnectAttempts} for ${peerId}`);
5396
5455
  try {
5397
- const peerId = peer.peerId || peer.id;
5398
- // Attempt to reconnect the specific peer
5399
- await peernet.client.reconnect(peerId, globalThis.peernet?.stars[0]);
5456
+ const peernet = globalThis.peernet;
5457
+ if (!peernet) {
5458
+ console.warn('âš ī¸ globalThis.peernet not available');
5459
+ return;
5460
+ }
5461
+ // Try targeted reconnect if available
5462
+ if (peernet.client?.reconnect) {
5463
+ try {
5464
+ await peernet.client.reconnect(peerId, peernet.stars?.[0]);
5465
+ return;
5466
+ }
5467
+ catch (err) {
5468
+ const msg = String(err?.message || err);
5469
+ console.warn('âš ī¸ Targeted reconnect failed:', msg);
5470
+ // handle signaling/state mismatches by cleaning up only that peer and retrying targeted reconnect
5471
+ if (msg.includes('Called in wrong state') ||
5472
+ msg.includes('setRemoteDescription') ||
5473
+ msg.includes('channelNames') ||
5474
+ msg.includes("channelNames don't match")) {
5475
+ console.warn('âš ī¸ Detected signaling/channel mismatch — cleaning up peer state and retrying targeted reconnect');
5476
+ try {
5477
+ await this.#cleanupPeerState(peerId, peernet);
5478
+ // small backoff before retry
5479
+ await new Promise((r) => setTimeout(r, 150));
5480
+ await peernet.client.reconnect(peerId, peernet.stars?.[0]);
5481
+ return;
5482
+ }
5483
+ catch (retryErr) {
5484
+ console.warn('âš ī¸ Retry targeted reconnect failed:', String(retryErr?.message || retryErr));
5485
+ // fall through to non-targeted fallback below
5486
+ }
5487
+ }
5488
+ throw err;
5489
+ }
5490
+ }
5491
+ // If no targeted reconnect, try start/restore
5492
+ if (peernet.start) {
5493
+ await peernet.start();
5494
+ }
5400
5495
  }
5401
5496
  catch (error) {
5402
- console.error('❌ Reconnection failed:', error.message);
5497
+ console.error('❌ Reconnection failed:', error?.message || error);
5498
+ // As fallback, try full restart only if even the cleanup+retry failed
5499
+ if (globalThis.peernet) {
5500
+ await this.#performFullRestart(globalThis.peernet);
5501
+ }
5403
5502
  }
5404
- // // Try to restart the network
5405
- // if (globalThis.peernet?.start) {
5406
- // await globalThis.peernet.start()
5407
- // } else {
5408
- // console.warn('âš ī¸ Peernet start method not available, skipping reconnection')
5409
- // }
5410
- // } catch (error) {
5411
- // console.error('❌ Reconnection failed:', error.message)
5412
- // }
5413
5503
  }
5414
- async #attemptReconnection() {
5415
- console.warn('âš ī¸ Attempting to reconnect to peers...');
5504
+ // helper: try to close/destroy a single peer connection and remove it from peernet's map
5505
+ async #cleanupPeerState(peerId, peernet) {
5416
5506
  try {
5417
- // Try to restart the network
5418
- // if (globalThis.peernet?.start) {
5419
- // await globalThis.peernet.start()
5420
- // }
5421
- // Wait a bit before next check
5422
- await new Promise((resolve) => setTimeout(resolve, this.#reconnectDelay));
5507
+ const conns = peernet.connections || {};
5508
+ const conn = conns[peerId] || conns[Object.keys(conns).find((k) => k.includes(peerId) || peerId.includes(k))];
5509
+ if (!conn)
5510
+ return;
5511
+ // close underlying RTCPeerConnection if exposed
5512
+ try {
5513
+ if (conn.pc && typeof conn.pc.close === 'function') {
5514
+ conn.pc.close();
5515
+ }
5516
+ }
5517
+ catch (e) {
5518
+ // ignore
5519
+ }
5520
+ // call any destroy/cleanup API on the connection object
5521
+ try {
5522
+ if (typeof conn.destroy === 'function') {
5523
+ conn.destroy();
5524
+ }
5525
+ else if (typeof conn.close === 'function') {
5526
+ conn.close();
5527
+ }
5528
+ }
5529
+ catch (e) {
5530
+ // ignore
5531
+ }
5532
+ // remove reference so reconnect path will create a fresh one
5533
+ try {
5534
+ delete peernet.connections[peerId];
5535
+ }
5536
+ catch (e) {
5537
+ // ignore
5538
+ }
5539
+ // small pause to let underlying sockets/RTCs settle
5540
+ await new Promise((r) => setTimeout(r, 100));
5423
5541
  }
5424
- catch (error) {
5425
- console.error('❌ Reconnection failed:', error.message);
5426
- if (this.#reconnectDelay >= 30000) {
5427
- console.warn('âš ī¸ Reconnection delay reached maximum, resetting to 5 seconds');
5428
- this.#reconnectDelay = 5000;
5542
+ catch (e) {
5543
+ // ignore cleanup errors
5544
+ }
5545
+ }
5546
+ // New helper: close stale RTCPeerConnections if present and then restart peernet
5547
+ async #performFullRestart(peernet) {
5548
+ try {
5549
+ // Close underlying peer RTCPeerConnections if the library exposes them
5550
+ try {
5551
+ const conns = peernet.connections || {};
5552
+ for (const id of Object.keys(conns)) {
5553
+ const p = conns[id];
5554
+ // try to close underlying RTCPeerConnection if exposed
5555
+ if (p && p.pc && typeof p.pc.close === 'function') {
5556
+ try {
5557
+ p.pc.close();
5558
+ }
5559
+ catch (e) {
5560
+ // ignore
5561
+ }
5562
+ }
5563
+ }
5564
+ }
5565
+ catch (e) {
5566
+ // ignore
5567
+ }
5568
+ // If the library supports stop -> start, do that to fully reset signaling state
5569
+ if (typeof peernet.stop === 'function') {
5570
+ try {
5571
+ await peernet.stop();
5572
+ }
5573
+ catch (e) {
5574
+ // ignore stop errors
5575
+ }
5576
+ }
5577
+ // small delay to ensure sockets/RTCs are closed
5578
+ await new Promise((r) => setTimeout(r, 250));
5579
+ if (typeof peernet.start === 'function') {
5580
+ await peernet.start();
5429
5581
  }
5430
5582
  else {
5431
- // Exponential backoff
5432
- this.#reconnectDelay = Math.min(this.#reconnectDelay * 1.5, 30000);
5433
- console.warn(`âš ī¸ Increasing reconnection delay to ${this.#reconnectDelay} ms`);
5583
+ console.warn('âš ī¸ peernet.start not available for full restart');
5434
5584
  }
5585
+ // reset reconnect attempts so we can try fresh
5586
+ this.#peerReconnectAttempts = {};
5587
+ console.log('✅ Full peernet restart completed');
5588
+ }
5589
+ catch (e) {
5590
+ console.error('❌ Full restart failed:', e?.message || e);
5591
+ }
5592
+ }
5593
+ // Called on visibility/online/resume events
5594
+ async #restoreNetwork() {
5595
+ console.log('🔁 Restoring network after resume/wake');
5596
+ // If there is a peernet instance, try a safe restore
5597
+ if (globalThis.peernet) {
5598
+ await this.#performFullRestart(globalThis.peernet);
5599
+ }
5600
+ else {
5601
+ // If no global peernet, attempt a normal reconnection flow
5602
+ await this.#attemptReconnection();
5435
5603
  }
5436
5604
  }
5437
5605
  async waitForPeers(timeoutMs = 30000) {
@@ -5451,6 +5619,39 @@ class ConnectionMonitor {
5451
5619
  checkPeers();
5452
5620
  });
5453
5621
  }
5622
+ // New: attempt reconnection flow (gentle start + sequential per-peer reconnect)
5623
+ async #attemptReconnection() {
5624
+ console.warn('âš ī¸ Attempting to reconnect to peers...');
5625
+ try {
5626
+ // gentle restore: if peernet supports start(), try that first
5627
+ if (globalThis.peernet?.start) {
5628
+ await globalThis.peernet.start();
5629
+ }
5630
+ // attempt targeted reconnection for disconnected peers sequentially (avoid racing WebRTC state)
5631
+ const disconnected = this.disconnectedPeers;
5632
+ for (const p of disconnected) {
5633
+ // small spacing between attempts to reduce signaling races
5634
+ // eslint-disable-next-line no-await-in-loop
5635
+ await new Promise((r) => setTimeout(r, 200));
5636
+ // eslint-disable-next-line no-await-in-loop
5637
+ await this.#attemptPeerReconnection(p);
5638
+ }
5639
+ // pause before next health check cycle
5640
+ await new Promise((resolve) => setTimeout(resolve, this.#reconnectDelay));
5641
+ }
5642
+ catch (error) {
5643
+ console.error('❌ Reconnection failed:', error?.message || error);
5644
+ if (this.#reconnectDelay >= 30000) {
5645
+ console.warn('âš ī¸ Reconnection delay reached maximum, resetting to 5 seconds');
5646
+ this.#reconnectDelay = 5000;
5647
+ }
5648
+ else {
5649
+ // exponential-ish backoff
5650
+ this.#reconnectDelay = Math.min(this.#reconnectDelay * 1.5, 30000);
5651
+ console.warn(`âš ī¸ Increasing reconnection delay to ${this.#reconnectDelay} ms`);
5652
+ }
5653
+ }
5654
+ }
5454
5655
  }
5455
5656
 
5456
5657
  const debug = globalThis.createDebugger('leofcoin/chain');
package/exports/chain.js CHANGED
@@ -1463,6 +1463,10 @@ class ConnectionMonitor {
1463
1463
  #reconnectDelay = 5000;
1464
1464
  #healthCheckInterval = 10000;
1465
1465
  #version;
1466
+ // event handlers to remove later
1467
+ #onOnline = null;
1468
+ #onVisibilityChange = null;
1469
+ #onSigcont = null;
1466
1470
  get isMonitoring() {
1467
1471
  return this.#isMonitoring;
1468
1472
  }
@@ -1482,6 +1486,33 @@ class ConnectionMonitor {
1482
1486
  return;
1483
1487
  this.#isMonitoring = true;
1484
1488
  console.log('🔄 Starting connection monitor...');
1489
+ // Listen for resume/network events (browser + node/electron)
1490
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
1491
+ this.#onOnline = () => {
1492
+ console.log('🌐 Network online — attempting restore');
1493
+ void this.#restoreNetwork();
1494
+ };
1495
+ window.addEventListener('online', this.#onOnline);
1496
+ this.#onVisibilityChange = () => {
1497
+ if (document.visibilityState === 'visible') {
1498
+ console.log('💡 Visibility regained — attempting restore');
1499
+ void this.#restoreNetwork();
1500
+ }
1501
+ };
1502
+ document.addEventListener('visibilitychange', this.#onVisibilityChange);
1503
+ }
1504
+ if (typeof process !== 'undefined' && typeof process.on === 'function') {
1505
+ this.#onSigcont = () => {
1506
+ console.log('🔔 Process resumed (SIGCONT) — attempting restore');
1507
+ void this.#restoreNetwork();
1508
+ };
1509
+ try {
1510
+ process.on('SIGCONT', this.#onSigcont);
1511
+ }
1512
+ catch (e) {
1513
+ // ignore if not supported
1514
+ }
1515
+ }
1485
1516
  this.#checkInterval = setInterval(() => {
1486
1517
  this.#healthCheck();
1487
1518
  }, this.#healthCheckInterval);
@@ -1496,6 +1527,26 @@ class ConnectionMonitor {
1496
1527
  clearInterval(this.#checkInterval);
1497
1528
  this.#checkInterval = null;
1498
1529
  }
1530
+ // remove listeners
1531
+ if (typeof window !== 'undefined') {
1532
+ if (this.#onOnline) {
1533
+ window.removeEventListener('online', this.#onOnline);
1534
+ this.#onOnline = null;
1535
+ }
1536
+ if (this.#onVisibilityChange) {
1537
+ document.removeEventListener('visibilitychange', this.#onVisibilityChange);
1538
+ this.#onVisibilityChange = null;
1539
+ }
1540
+ }
1541
+ if (typeof process !== 'undefined' && typeof process.removeListener === 'function' && this.#onSigcont) {
1542
+ try {
1543
+ process.removeListener('SIGCONT', this.#onSigcont);
1544
+ }
1545
+ catch (e) {
1546
+ // ignore
1547
+ }
1548
+ this.#onSigcont = null;
1549
+ }
1499
1550
  console.log('âšī¸ Connection monitor stopped');
1500
1551
  }
1501
1552
  async #healthCheck() {
@@ -1515,12 +1566,14 @@ class ConnectionMonitor {
1515
1566
  const disconnectedPeers = this.disconnectedPeers;
1516
1567
  if (disconnectedPeers.length > 0) {
1517
1568
  console.warn(`âš ī¸ Disconnected peers: ${disconnectedPeers.map((peer) => peer.peerId).join(', ')}`);
1518
- // Attempt to reconnect each disconnected peer
1519
- const promises = [];
1569
+ // Attempt to reconnect each disconnected peer sequentially to avoid racing signaling/state
1520
1570
  for (const peer of disconnectedPeers) {
1521
- promises.push(this.#attemptPeerReconnection(peer));
1571
+ // small spacing between attempts to reduce signaling races
1572
+ // eslint-disable-next-line no-await-in-loop
1573
+ await new Promise((r) => setTimeout(r, 150));
1574
+ // eslint-disable-next-line no-await-in-loop
1575
+ await this.#attemptPeerReconnection(peer);
1522
1576
  }
1523
- await Promise.all(promises);
1524
1577
  }
1525
1578
  // Publish connection status
1526
1579
  globalThis.pubsub?.publish('connection-status', {
@@ -1530,54 +1583,169 @@ class ConnectionMonitor {
1530
1583
  });
1531
1584
  }
1532
1585
  async #attemptPeerReconnection(peer) {
1533
- if (this.#peerReconnectAttempts[peer.peerId] >= this.#maxReconnectAttempts) {
1534
- console.error('❌ Max reconnection attempts reached');
1535
- this.#peerReconnectAttempts[peer.peerId] = 0;
1586
+ if (!peer)
1587
+ return;
1588
+ const peerId = peer.peerId || peer.id;
1589
+ if (!peerId)
1590
+ return;
1591
+ if (!this.#peerReconnectAttempts[peerId]) {
1592
+ this.#peerReconnectAttempts[peerId] = 0;
1536
1593
  }
1537
- if (!this.#peerReconnectAttempts[peer.peerId]) {
1538
- this.#peerReconnectAttempts[peer.peerId] = 0;
1594
+ if (this.#peerReconnectAttempts[peerId] >= this.#maxReconnectAttempts) {
1595
+ console.error('❌ Max reconnection attempts reached for', peerId);
1596
+ this.#peerReconnectAttempts[peerId] = 0;
1597
+ return;
1539
1598
  }
1540
- this.#peerReconnectAttempts[peer.peerId]++;
1541
- console.log(`🔄 Attempting reconnection ${this.#peerReconnectAttempts[peer.peerId]}/${this.#maxReconnectAttempts}`);
1599
+ this.#peerReconnectAttempts[peerId]++;
1600
+ console.log(`🔄 Attempting reconnection ${this.#peerReconnectAttempts[peerId]}/${this.#maxReconnectAttempts} for ${peerId}`);
1542
1601
  try {
1543
- const peerId = peer.peerId || peer.id;
1544
- // Attempt to reconnect the specific peer
1545
- await peernet.client.reconnect(peerId, globalThis.peernet?.stars[0]);
1602
+ const peernet = globalThis.peernet;
1603
+ if (!peernet) {
1604
+ console.warn('âš ī¸ globalThis.peernet not available');
1605
+ return;
1606
+ }
1607
+ // Try targeted reconnect if available
1608
+ if (peernet.client?.reconnect) {
1609
+ try {
1610
+ await peernet.client.reconnect(peerId, peernet.stars?.[0]);
1611
+ return;
1612
+ }
1613
+ catch (err) {
1614
+ const msg = String(err?.message || err);
1615
+ console.warn('âš ī¸ Targeted reconnect failed:', msg);
1616
+ // handle signaling/state mismatches by cleaning up only that peer and retrying targeted reconnect
1617
+ if (msg.includes('Called in wrong state') ||
1618
+ msg.includes('setRemoteDescription') ||
1619
+ msg.includes('channelNames') ||
1620
+ msg.includes("channelNames don't match")) {
1621
+ console.warn('âš ī¸ Detected signaling/channel mismatch — cleaning up peer state and retrying targeted reconnect');
1622
+ try {
1623
+ await this.#cleanupPeerState(peerId, peernet);
1624
+ // small backoff before retry
1625
+ await new Promise((r) => setTimeout(r, 150));
1626
+ await peernet.client.reconnect(peerId, peernet.stars?.[0]);
1627
+ return;
1628
+ }
1629
+ catch (retryErr) {
1630
+ console.warn('âš ī¸ Retry targeted reconnect failed:', String(retryErr?.message || retryErr));
1631
+ // fall through to non-targeted fallback below
1632
+ }
1633
+ }
1634
+ throw err;
1635
+ }
1636
+ }
1637
+ // If no targeted reconnect, try start/restore
1638
+ if (peernet.start) {
1639
+ await peernet.start();
1640
+ }
1546
1641
  }
1547
1642
  catch (error) {
1548
- console.error('❌ Reconnection failed:', error.message);
1643
+ console.error('❌ Reconnection failed:', error?.message || error);
1644
+ // As fallback, try full restart only if even the cleanup+retry failed
1645
+ if (globalThis.peernet) {
1646
+ await this.#performFullRestart(globalThis.peernet);
1647
+ }
1549
1648
  }
1550
- // // Try to restart the network
1551
- // if (globalThis.peernet?.start) {
1552
- // await globalThis.peernet.start()
1553
- // } else {
1554
- // console.warn('âš ī¸ Peernet start method not available, skipping reconnection')
1555
- // }
1556
- // } catch (error) {
1557
- // console.error('❌ Reconnection failed:', error.message)
1558
- // }
1559
1649
  }
1560
- async #attemptReconnection() {
1561
- console.warn('âš ī¸ Attempting to reconnect to peers...');
1650
+ // helper: try to close/destroy a single peer connection and remove it from peernet's map
1651
+ async #cleanupPeerState(peerId, peernet) {
1562
1652
  try {
1563
- // Try to restart the network
1564
- // if (globalThis.peernet?.start) {
1565
- // await globalThis.peernet.start()
1566
- // }
1567
- // Wait a bit before next check
1568
- await new Promise((resolve) => setTimeout(resolve, this.#reconnectDelay));
1653
+ const conns = peernet.connections || {};
1654
+ const conn = conns[peerId] || conns[Object.keys(conns).find((k) => k.includes(peerId) || peerId.includes(k))];
1655
+ if (!conn)
1656
+ return;
1657
+ // close underlying RTCPeerConnection if exposed
1658
+ try {
1659
+ if (conn.pc && typeof conn.pc.close === 'function') {
1660
+ conn.pc.close();
1661
+ }
1662
+ }
1663
+ catch (e) {
1664
+ // ignore
1665
+ }
1666
+ // call any destroy/cleanup API on the connection object
1667
+ try {
1668
+ if (typeof conn.destroy === 'function') {
1669
+ conn.destroy();
1670
+ }
1671
+ else if (typeof conn.close === 'function') {
1672
+ conn.close();
1673
+ }
1674
+ }
1675
+ catch (e) {
1676
+ // ignore
1677
+ }
1678
+ // remove reference so reconnect path will create a fresh one
1679
+ try {
1680
+ delete peernet.connections[peerId];
1681
+ }
1682
+ catch (e) {
1683
+ // ignore
1684
+ }
1685
+ // small pause to let underlying sockets/RTCs settle
1686
+ await new Promise((r) => setTimeout(r, 100));
1569
1687
  }
1570
- catch (error) {
1571
- console.error('❌ Reconnection failed:', error.message);
1572
- if (this.#reconnectDelay >= 30000) {
1573
- console.warn('âš ī¸ Reconnection delay reached maximum, resetting to 5 seconds');
1574
- this.#reconnectDelay = 5000;
1688
+ catch (e) {
1689
+ // ignore cleanup errors
1690
+ }
1691
+ }
1692
+ // New helper: close stale RTCPeerConnections if present and then restart peernet
1693
+ async #performFullRestart(peernet) {
1694
+ try {
1695
+ // Close underlying peer RTCPeerConnections if the library exposes them
1696
+ try {
1697
+ const conns = peernet.connections || {};
1698
+ for (const id of Object.keys(conns)) {
1699
+ const p = conns[id];
1700
+ // try to close underlying RTCPeerConnection if exposed
1701
+ if (p && p.pc && typeof p.pc.close === 'function') {
1702
+ try {
1703
+ p.pc.close();
1704
+ }
1705
+ catch (e) {
1706
+ // ignore
1707
+ }
1708
+ }
1709
+ }
1710
+ }
1711
+ catch (e) {
1712
+ // ignore
1713
+ }
1714
+ // If the library supports stop -> start, do that to fully reset signaling state
1715
+ if (typeof peernet.stop === 'function') {
1716
+ try {
1717
+ await peernet.stop();
1718
+ }
1719
+ catch (e) {
1720
+ // ignore stop errors
1721
+ }
1722
+ }
1723
+ // small delay to ensure sockets/RTCs are closed
1724
+ await new Promise((r) => setTimeout(r, 250));
1725
+ if (typeof peernet.start === 'function') {
1726
+ await peernet.start();
1575
1727
  }
1576
1728
  else {
1577
- // Exponential backoff
1578
- this.#reconnectDelay = Math.min(this.#reconnectDelay * 1.5, 30000);
1579
- console.warn(`âš ī¸ Increasing reconnection delay to ${this.#reconnectDelay} ms`);
1729
+ console.warn('âš ī¸ peernet.start not available for full restart');
1580
1730
  }
1731
+ // reset reconnect attempts so we can try fresh
1732
+ this.#peerReconnectAttempts = {};
1733
+ console.log('✅ Full peernet restart completed');
1734
+ }
1735
+ catch (e) {
1736
+ console.error('❌ Full restart failed:', e?.message || e);
1737
+ }
1738
+ }
1739
+ // Called on visibility/online/resume events
1740
+ async #restoreNetwork() {
1741
+ console.log('🔁 Restoring network after resume/wake');
1742
+ // If there is a peernet instance, try a safe restore
1743
+ if (globalThis.peernet) {
1744
+ await this.#performFullRestart(globalThis.peernet);
1745
+ }
1746
+ else {
1747
+ // If no global peernet, attempt a normal reconnection flow
1748
+ await this.#attemptReconnection();
1581
1749
  }
1582
1750
  }
1583
1751
  async waitForPeers(timeoutMs = 30000) {
@@ -1597,6 +1765,39 @@ class ConnectionMonitor {
1597
1765
  checkPeers();
1598
1766
  });
1599
1767
  }
1768
+ // New: attempt reconnection flow (gentle start + sequential per-peer reconnect)
1769
+ async #attemptReconnection() {
1770
+ console.warn('âš ī¸ Attempting to reconnect to peers...');
1771
+ try {
1772
+ // gentle restore: if peernet supports start(), try that first
1773
+ if (globalThis.peernet?.start) {
1774
+ await globalThis.peernet.start();
1775
+ }
1776
+ // attempt targeted reconnection for disconnected peers sequentially (avoid racing WebRTC state)
1777
+ const disconnected = this.disconnectedPeers;
1778
+ for (const p of disconnected) {
1779
+ // small spacing between attempts to reduce signaling races
1780
+ // eslint-disable-next-line no-await-in-loop
1781
+ await new Promise((r) => setTimeout(r, 200));
1782
+ // eslint-disable-next-line no-await-in-loop
1783
+ await this.#attemptPeerReconnection(p);
1784
+ }
1785
+ // pause before next health check cycle
1786
+ await new Promise((resolve) => setTimeout(resolve, this.#reconnectDelay));
1787
+ }
1788
+ catch (error) {
1789
+ console.error('❌ Reconnection failed:', error?.message || error);
1790
+ if (this.#reconnectDelay >= 30000) {
1791
+ console.warn('âš ī¸ Reconnection delay reached maximum, resetting to 5 seconds');
1792
+ this.#reconnectDelay = 5000;
1793
+ }
1794
+ else {
1795
+ // exponential-ish backoff
1796
+ this.#reconnectDelay = Math.min(this.#reconnectDelay * 1.5, 30000);
1797
+ console.warn(`âš ī¸ Increasing reconnection delay to ${this.#reconnectDelay} ms`);
1798
+ }
1799
+ }
1800
+ }
1600
1801
  }
1601
1802
 
1602
1803
  const debug = globalThis.createDebugger('leofcoin/chain');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leofcoin/chain",
3
- "version": "1.7.99",
3
+ "version": "1.7.100",
4
4
  "description": "Official javascript implementation",
5
5
  "private": false,
6
6
  "exports": {