@leofcoin/chain 1.7.98 → 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');
@@ -1,4 +1,4 @@
1
- import { L as LittlePubSub } from './node-browser-D0JliYju.js';
1
+ import { L as LittlePubSub } from './node-browser-DPfaEz6Y.js';
2
2
  import './identity-Cn0iQbY3-CeW0giQS.js';
3
3
  import './index-DUfUgiQY.js';
4
4
 
@@ -428,7 +428,11 @@ class Client {
428
428
  #peerId;
429
429
  #connections = {};
430
430
  #stars = {};
431
+ #starListeners = {};
432
+ #handlersSetup = false;
433
+ #reinitLock = null;
431
434
  #connectEvent = 'peer:connected';
435
+ #retryOptions = { retries: 5, factor: 2, minTimeout: 1000, maxTimeout: 30000 };
432
436
  id;
433
437
  networkVersion;
434
438
  starsConfig;
@@ -467,21 +471,80 @@ class Client {
467
471
  this.version = version;
468
472
  this.#connectEvent = connectEvent;
469
473
  this.starsConfig = stars;
474
+ if (options?.retry)
475
+ this.#retryOptions = { ...this.#retryOptions, ...options.retry };
470
476
  this._init();
471
477
  }
472
- async _init() {
473
- // reconnectJob()
474
- if (!globalThis.RTCPeerConnection)
475
- globalThis.wrtc = (await import('./browser-Cjcx-T47-D0KbGYCz.js').then(function (n) { return n.b; })).default;
476
- for (const star of this.starsConfig) {
478
+ /**
479
+ * Safely reinitialize the client (used after system resume/sleep).
480
+ * It closes existing connections and reconnects to configured stars.
481
+ */
482
+ async reinit() {
483
+ // avoid concurrent reinit runs
484
+ if (this.#reinitLock)
485
+ return this.#reinitLock;
486
+ this.#reinitLock = (async () => {
487
+ debug('reinit: start');
488
+ try {
489
+ await this.close();
490
+ // clear internal maps so setupStar starts fresh
491
+ this.#stars = {};
492
+ this.#connections = {};
493
+ for (const star of this.starsConfig) {
494
+ try {
495
+ await this.setupStar(star);
496
+ }
497
+ catch (e) {
498
+ // If last star fails and none connected, surface error
499
+ if (this.starsConfig.indexOf(star) === this.starsConfig.length - 1 &&
500
+ Object.keys(this.#stars).length === 0)
501
+ throw new Error(`No star available to connect`);
502
+ }
503
+ }
504
+ }
505
+ finally {
506
+ debug('reinit: done');
507
+ this.#reinitLock = null;
508
+ }
509
+ })();
510
+ return this.#reinitLock;
511
+ }
512
+ async setupStar(star) {
513
+ const { retries, factor, minTimeout, maxTimeout } = this.#retryOptions;
514
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
515
+ let attempt = 0;
516
+ let lastErr;
517
+ while (attempt <= retries) {
477
518
  try {
478
519
  const client = new SocketRequestClient(star, this.networkVersion);
479
520
  this.#stars[star] = await client.init();
480
- this.setupStarListeners(this.#stars[star]);
521
+ this.setupStarListeners(this.#stars[star], star);
481
522
  this.#stars[star].send({
482
523
  url: 'join',
483
524
  params: { version: this.version, peerId: this.peerId }
484
525
  });
526
+ return this.#stars[star];
527
+ }
528
+ catch (e) {
529
+ lastErr = e;
530
+ attempt += 1;
531
+ if (attempt > retries)
532
+ break;
533
+ const delay = Math.min(maxTimeout, Math.round(minTimeout * Math.pow(factor, attempt - 1)));
534
+ debug(`setupStar ${star} failed, retrying in ${delay}ms (attempt ${attempt})`);
535
+ // eslint-disable-next-line no-await-in-loop
536
+ await sleep(delay);
537
+ }
538
+ }
539
+ throw lastErr;
540
+ }
541
+ async _init() {
542
+ // reconnectJob()
543
+ if (!globalThis.RTCPeerConnection)
544
+ globalThis.wrtc = (await import('./browser-Cjcx-T47-D0KbGYCz.js').then(function (n) { return n.b; })).default;
545
+ for (const star of this.starsConfig) {
546
+ try {
547
+ await this.setupStar(star);
485
548
  }
486
549
  catch (e) {
487
550
  if (this.starsConfig.indexOf(star) === this.starsConfig.length - 1 &&
@@ -499,13 +562,68 @@ class Client {
499
562
  else {
500
563
  globalThis.addEventListener('beforeunload', this.close.bind(this));
501
564
  }
565
+ // Setup resume/sleep detection so we can reinit connections after wake
566
+ this._setupResumeHandler();
567
+ }
568
+ setupStarListeners(starConnection, starId) {
569
+ // create stable references to handlers so we can unsubscribe later
570
+ const onPeerJoined = (id) => this.#peerJoined(id, starConnection);
571
+ const onPeerLeft = (id) => this.#peerLeft(id, starConnection);
572
+ const onStarJoined = this.#starJoined;
573
+ const onStarLeft = this.#starLeft;
574
+ const onSignal = (message) => this.#inComingSignal(message, starConnection);
575
+ starConnection.pubsub.subscribe('peer:joined', onPeerJoined);
576
+ starConnection.pubsub.subscribe('peer:left', onPeerLeft);
577
+ starConnection.pubsub.subscribe('star:joined', onStarJoined);
578
+ starConnection.pubsub.subscribe('star:left', onStarLeft);
579
+ starConnection.pubsub.subscribe('signal', onSignal);
580
+ this.#starListeners[starId] = [
581
+ { topic: 'peer:joined', handler: onPeerJoined },
582
+ { topic: 'peer:left', handler: onPeerLeft },
583
+ { topic: 'star:joined', handler: onStarJoined },
584
+ { topic: 'star:left', handler: onStarLeft },
585
+ { topic: 'signal', handler: onSignal }
586
+ ];
502
587
  }
503
- setupStarListeners(star) {
504
- star.pubsub.subscribe('peer:joined', (id) => this.#peerJoined(id, star));
505
- star.pubsub.subscribe('peer:left', (id) => this.#peerLeft(id, star));
506
- star.pubsub.subscribe('star:joined', this.#starJoined);
507
- star.pubsub.subscribe('star:left', this.#starLeft);
508
- star.pubsub.subscribe('signal', (message) => this.#inComingSignal(message, star));
588
+ _setupResumeHandler() {
589
+ if (this.#handlersSetup)
590
+ return;
591
+ this.#handlersSetup = true;
592
+ const THRESHOLD = 10 * 1000; // 10s gap indicates sleep/wake
593
+ let last = Date.now();
594
+ const check = () => {
595
+ const now = Date.now();
596
+ const delta = now - last;
597
+ last = now;
598
+ if (delta > THRESHOLD) {
599
+ debug(`resume detected (gap ${delta}ms)`);
600
+ this.reinit().catch((e) => debug('reinit error', e));
601
+ }
602
+ };
603
+ // Start interval checker
604
+ const iv = setInterval(check, 2000);
605
+ // Browser specific events
606
+ if (typeof document !== 'undefined' && document.addEventListener) {
607
+ document.addEventListener('visibilitychange', () => {
608
+ if (document.visibilityState === 'visible') {
609
+ // small delay to let timers update
610
+ setTimeout(() => check(), 50);
611
+ }
612
+ });
613
+ window.addEventListener('online', () => setTimeout(() => check(), 50));
614
+ }
615
+ // Node: listen for SIGCONT (process continued) as well
616
+ if (globalThis.process?.on) {
617
+ try {
618
+ process.on('SIGCONT', () => setTimeout(() => check(), 50));
619
+ }
620
+ catch (e) {
621
+ // ignore
622
+ }
623
+ }
624
+ // keep reference so it can be cleared on close
625
+ // @ts-ignore
626
+ this._resumeInterval = iv;
509
627
  }
510
628
  #starJoined = (id) => {
511
629
  if (this.#stars[id]) {
@@ -519,20 +637,16 @@ class Client {
519
637
  this.#stars[id].close(0);
520
638
  delete this.#stars[id];
521
639
  }
640
+ // if we lost all stars, try to reconnect to configured stars with backoff
522
641
  if (Object.keys(this.#stars).length === 0) {
523
642
  for (const star of this.starsConfig) {
524
643
  try {
525
- const socketClient = await new SocketRequestClient(star, this.networkVersion).init();
526
- if (!socketClient?.client?.OPEN)
527
- return;
528
- this.#stars[star] = socketClient;
529
- this.#stars[star].send({
530
- url: 'join',
531
- params: { peerId: this.peerId, version: this.version }
532
- });
533
- this.setupStarListeners(socketClient);
644
+ await this.setupStar(star);
645
+ // stop at first success
646
+ return;
534
647
  }
535
648
  catch (e) {
649
+ debug(`reconnect star ${star} failed: ${e.message || e}`);
536
650
  if (this.starsConfig.indexOf(star) === this.starsConfig.length - 1)
537
651
  throw new Error(`No star available to connect`);
538
652
  }
@@ -611,7 +725,7 @@ class Client {
611
725
  // Destroy the existing peer connection
612
726
  // peer.destroy()
613
727
  // delete this.#connections[from]
614
- // // Create a new peer connection with the correct configuration
728
+ // // // Create a new peer connection with the correct configuration
615
729
  // this.#createRTCPeerConnection(from, star, version, false)
616
730
  // peer = this.#connections[from]
617
731
  }
@@ -685,15 +799,53 @@ class Client {
685
799
  peer.destroy();
686
800
  };
687
801
  async close() {
802
+ // clear resume interval if set
803
+ // @ts-ignore
804
+ if (this._resumeInterval) {
805
+ // @ts-ignore
806
+ clearInterval(this._resumeInterval);
807
+ // @ts-ignore
808
+ this._resumeInterval = null;
809
+ }
688
810
  for (const star in this.#stars) {
689
- if (this.#stars[star].connectionState() === 'open')
811
+ if (this.#stars[star].connectionState() === 'open') {
690
812
  await this.#stars[star].send({ url: 'leave', params: this.peerId });
813
+ // unsubscribe handlers we registered earlier
814
+ const listeners = this.#starListeners[star];
815
+ if (listeners && listeners.length) {
816
+ for (const { topic, handler } of listeners) {
817
+ try {
818
+ this.#stars[star].pubsub.unsubscribe(topic, handler);
819
+ }
820
+ catch (e) {
821
+ // ignore
822
+ }
823
+ }
824
+ }
825
+ }
691
826
  }
692
- const promises = [
693
- Object.values(this.#connections).map((connection) => connection.destroy()),
694
- Object.values(this.#stars).map((connection) => connection.close(0))
695
- ];
696
- return Promise.allSettled(promises);
827
+ // Ensure we wait for all peer and star close/destroy operations.
828
+ // Previous code passed an array of arrays to Promise.allSettled which
829
+ // resolves immediately; flatten into a single array of promises (or
830
+ // values) so we actually wait for async close operations.
831
+ const peerClosers = Object.values(this.#connections).map((connection) => {
832
+ try {
833
+ // destroy() may be sync or return a promise
834
+ return connection.destroy();
835
+ }
836
+ catch (e) {
837
+ return undefined;
838
+ }
839
+ });
840
+ const starClosers = Object.values(this.#stars).map((connection) => {
841
+ try {
842
+ return connection.close(0);
843
+ }
844
+ catch (e) {
845
+ return undefined;
846
+ }
847
+ });
848
+ return Promise.allSettled([...peerClosers, ...starClosers]);
697
849
  }
698
850
  }
699
851
 
@@ -1,4 +1,4 @@
1
- import { F as FormatInterface } from './node-browser-D0JliYju.js';
1
+ import { F as FormatInterface } from './node-browser-DPfaEz6Y.js';
2
2
  import './identity-Cn0iQbY3-CeW0giQS.js';
3
3
  import './index-DUfUgiQY.js';
4
4
 
@@ -8491,7 +8491,7 @@ class Peernet {
8491
8491
  this.root = options.root;
8492
8492
  const { RequestMessage, ResponseMessage, PeerMessage, PeerMessageResponse, PeernetMessage, DHTMessage, DHTMessageResponse, DataMessage, DataMessageResponse, PsMessage, ChatMessage, PeernetFile
8493
8493
  // FolderMessageResponse
8494
- } = await import(/* webpackChunkName: "messages" */ './messages-DDEOxfJp-CggxH-2R.js');
8494
+ } = await import(/* webpackChunkName: "messages" */ './messages-CFqwXbrQ-D0JHZ5TG.js');
8495
8495
  /**
8496
8496
  * proto Object containing protos
8497
8497
  * @type {Object}
@@ -8585,7 +8585,7 @@ class Peernet {
8585
8585
  if (this.#starting || this.#started)
8586
8586
  return;
8587
8587
  this.#starting = true;
8588
- const importee = await import('./client-BDXkFVRR-BZjYAtds.js');
8588
+ const importee = await import('./client-UwSHOtRi-BsddY8I0.js');
8589
8589
  /**
8590
8590
  * @access public
8591
8591
  * @type {PeernetClient}
@@ -1,3 +1,3 @@
1
- export { N as default } from './node-browser-D0JliYju.js';
1
+ export { N as default } from './node-browser-DPfaEz6Y.js';
2
2
  import './identity-Cn0iQbY3-CeW0giQS.js';
3
3
  import './index-DUfUgiQY.js';
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.98",
3
+ "version": "1.7.100",
4
4
  "description": "Official javascript implementation",
5
5
  "private": false,
6
6
  "exports": {
@@ -69,7 +69,7 @@
69
69
  "@leofcoin/messages": "^1.4.40",
70
70
  "@leofcoin/multi-wallet": "^3.1.8",
71
71
  "@leofcoin/networks": "^1.1.25",
72
- "@leofcoin/peernet": "^1.1.97",
72
+ "@leofcoin/peernet": "^1.1.98",
73
73
  "@leofcoin/storage": "^3.5.38",
74
74
  "@leofcoin/utils": "^1.1.40",
75
75
  "@leofcoin/workers": "^1.5.27",