vibex-sh 0.9.6 → 0.9.8

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 (2) hide show
  1. package/index.js +128 -41
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -437,19 +437,89 @@ async function main() {
437
437
  let reconnectAttempts = 0;
438
438
  const maxReconnectDelay = 5000;
439
439
 
440
+ // Connection state management
441
+ let connectionState = 'disconnected'; // disconnected, connecting, connected, closing, closed
442
+ let connectionEstablished = false;
443
+ let connectionLock = false;
444
+ let connectionStartTime = null;
445
+
440
446
  // Store auth code received from socket
441
447
  let receivedAuthCode = authCode;
442
448
 
443
449
  // Track if this is a new session (not reusing an existing one)
444
450
  const isNewSession = !options.sessionId;
445
451
 
452
+ // Graceful shutdown function
453
+ const closeWebSocket = () => {
454
+ return new Promise((resolve) => {
455
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
456
+ if (connectionState === 'connected' || connectionState === 'closing') {
457
+ connectionState = 'closed';
458
+ }
459
+ resolve();
460
+ return;
461
+ }
462
+
463
+ connectionState = 'closing';
464
+ const closeStartTime = Date.now();
465
+ console.log(' 🔄 Initiating graceful WebSocket close...');
466
+
467
+ const closeTimeout = setTimeout(() => {
468
+ const elapsed = Date.now() - closeStartTime;
469
+ console.log(` ⚠️ Close handshake timeout after ${elapsed}ms, forcing exit`);
470
+ connectionState = 'closed';
471
+ resolve();
472
+ }, 2000);
473
+
474
+ // Store original onclose handler
475
+ const originalOnClose = socket.onclose;
476
+
477
+ socket.onclose = (event) => {
478
+ clearTimeout(closeTimeout);
479
+ const elapsed = Date.now() - closeStartTime;
480
+ connectionState = 'closed';
481
+ connectionEstablished = false;
482
+ console.log(` ✓ WebSocket closed gracefully (code: ${event.code}, reason: ${event.reason || 'none'}, time: ${elapsed}ms)`);
483
+
484
+ // Call original handler if it exists
485
+ if (originalOnClose) {
486
+ originalOnClose(event);
487
+ }
488
+
489
+ resolve();
490
+ };
491
+
492
+ socket.close(1000, 'Stream ended');
493
+ });
494
+ };
495
+
446
496
  const connectWebSocket = () => {
497
+ // Prevent multiple simultaneous connections
498
+ if (connectionLock) {
499
+ console.log(' ⚠️ Connection already in progress, skipping...');
500
+ return;
501
+ }
502
+
503
+ if (connectionState === 'connected' || connectionState === 'connecting') {
504
+ console.log(` ⚠️ Already ${connectionState}, skipping new connection...`);
505
+ return;
506
+ }
507
+
508
+ connectionLock = true;
509
+ connectionState = 'connecting';
510
+ connectionStartTime = Date.now();
511
+ console.log(' 🔄 Connecting to WebSocket...');
512
+
447
513
  try {
448
514
  socket = new WebSocket(`${socketUrl}?sessionId=${sessionId}`);
449
515
 
450
516
  socket.onopen = () => {
517
+ const connectTime = Date.now() - connectionStartTime;
518
+ connectionLock = false;
519
+ connectionState = 'connected';
451
520
  isConnected = true;
452
- console.log(' ✓ Connected to server\n');
521
+ connectionEstablished = true;
522
+ console.log(` ✓ Connected to server (${connectTime}ms)\n`);
453
523
  reconnectAttempts = 0;
454
524
 
455
525
  // Join session
@@ -458,17 +528,15 @@ async function main() {
458
528
  sessionId,
459
529
  }));
460
530
 
461
- // Wait a bit for join-session to be processed
462
- setTimeout(() => {
463
- hasJoinedSession = true;
464
- // Process any queued logs
465
- while (logQueue.length > 0) {
466
- const logData = logQueue.shift();
467
- // Send logs via HTTP POST (non-blocking) instead of WebSocket
468
- // WebSocket is only for receiving logs
469
- sendLogViaHTTP(logData);
470
- }
471
- }, 100);
531
+ // Set hasJoinedSession immediately - HTTP POST doesn't need WebSocket
532
+ hasJoinedSession = true;
533
+ // Process any queued logs immediately
534
+ while (logQueue.length > 0) {
535
+ const logData = logQueue.shift();
536
+ // Send logs via HTTP POST (non-blocking) instead of WebSocket
537
+ // WebSocket is only for receiving logs
538
+ sendLogViaHTTP(logData);
539
+ }
472
540
  };
473
541
 
474
542
  socket.onmessage = (event) => {
@@ -607,11 +675,17 @@ async function main() {
607
675
  };
608
676
 
609
677
  socket.onclose = (event) => {
678
+ connectionLock = false;
679
+ connectionState = 'closed';
680
+ connectionEstablished = false;
610
681
  isConnected = false;
611
682
  hasJoinedSession = false;
683
+
684
+ const connectionDuration = connectionStartTime ? Date.now() - connectionStartTime : 0;
685
+ console.log(` 📊 Connection closed (code: ${event.code}, reason: ${event.reason || 'none'}, duration: ${connectionDuration}ms)`);
612
686
 
613
- // Reconnect logic
614
- if (event.code !== 1000) { // Not a normal closure
687
+ // Reconnect logic - only if not a normal closure and not already closing
688
+ if (event.code !== 1000 && connectionState !== 'closing') {
615
689
  const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), maxReconnectDelay);
616
690
  reconnectAttempts++;
617
691
  console.log(` ↻ Reconnecting in ${delay}ms (attempt ${reconnectAttempts})...\n`);
@@ -619,9 +693,14 @@ async function main() {
619
693
  reconnectTimeout = setTimeout(() => {
620
694
  connectWebSocket();
621
695
  }, delay);
696
+ } else if (event.code === 1000) {
697
+ console.log(' ✓ Normal closure, no reconnect needed\n');
622
698
  }
623
699
  };
624
700
  } catch (error) {
701
+ connectionLock = false;
702
+ connectionState = 'disconnected';
703
+ connectionEstablished = false;
625
704
  console.error(` ✗ Error creating WebSocket: ${error.message}`);
626
705
  console.error(` ↻ URL: ${socketUrl}`);
627
706
  // Retry connection
@@ -634,6 +713,7 @@ async function main() {
634
713
  // Send logs via HTTP POST (non-blocking, same as SDKs)
635
714
  // Use Cloudflare Worker endpoint (port 8787 for local, or Worker URL for production)
636
715
  // Token is optional - anonymous sessions can send logs without authentication
716
+ // HTTP POST works independently of WebSocket - don't wait for WebSocket connection
637
717
  const sendLogViaHTTP = async (logData) => {
638
718
  try {
639
719
  // Determine ingest URL
@@ -649,9 +729,7 @@ async function main() {
649
729
  const defaultWorkerUrl = 'https://vibex-ingest.prop.workers.dev';
650
730
  ingestUrl = `${defaultWorkerUrl}/api/v1/ingest`;
651
731
  }
652
-
653
- console.log(` 📤 Sending log to: ${ingestUrl}`);
654
-
732
+
655
733
  // Build headers - only include Authorization if token exists
656
734
  const headers = {
657
735
  'Content-Type': 'application/json',
@@ -790,43 +868,52 @@ async function main() {
790
868
  };
791
869
  }
792
870
 
793
- // Send logs via HTTP POST (non-blocking, same as SDKs)
794
- // WebSocket is only for receiving logs and auth codes
795
- if (hasJoinedSession) {
796
- sendLogViaHTTP(logData);
797
- } else {
798
- logQueue.push(logData);
799
- }
871
+ // Send logs via HTTP POST immediately - don't wait for WebSocket
872
+ // WebSocket is only for receiving logs and auth codes, not required for sending
873
+ sendLogViaHTTP(logData);
800
874
  });
801
875
 
802
- rl.on('close', () => {
876
+ rl.on('close', async () => {
803
877
  // Wait for queued logs to be sent
804
878
  const waitForQueue = () => {
805
- if (logQueue.length === 0) {
806
- console.log('\n Stream ended. Closing connection...\n');
807
- if (socket && socket.readyState === 1) { // WebSocket.OPEN = 1
808
- socket.close(1000, 'Stream ended');
809
- }
810
- if (reconnectTimeout) {
811
- clearTimeout(reconnectTimeout);
879
+ return new Promise((resolve) => {
880
+ if (logQueue.length === 0) {
881
+ resolve();
882
+ } else {
883
+ setTimeout(() => waitForQueue().then(resolve), 100);
812
884
  }
813
- setTimeout(() => process.exit(0), 100);
814
- } else {
815
- setTimeout(waitForQueue, 100);
816
- }
885
+ });
817
886
  };
818
887
 
819
- waitForQueue();
888
+ await waitForQueue();
889
+
890
+ console.log('\n Stream ended. Closing connection...\n');
891
+
892
+ // Cancel any pending reconnection attempts
893
+ if (reconnectTimeout) {
894
+ clearTimeout(reconnectTimeout);
895
+ reconnectTimeout = null;
896
+ }
897
+
898
+ // Graceful shutdown - wait for close handshake
899
+ await closeWebSocket();
900
+
901
+ // Give a moment for any final cleanup
902
+ setTimeout(() => process.exit(0), 100);
820
903
  });
821
904
 
822
- process.on('SIGINT', () => {
905
+ process.on('SIGINT', async () => {
823
906
  console.log('\n Interrupted. Closing connection...\n');
824
- if (socket && socket.readyState === 1) { // WebSocket.OPEN = 1
825
- socket.close(1000, 'Interrupted');
826
- }
907
+
908
+ // Cancel any pending reconnection attempts
827
909
  if (reconnectTimeout) {
828
910
  clearTimeout(reconnectTimeout);
911
+ reconnectTimeout = null;
829
912
  }
913
+
914
+ // Graceful shutdown
915
+ await closeWebSocket();
916
+
830
917
  process.exit(0);
831
918
  });
832
919
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibex-sh",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "description": "Zero-config observability CLI - pipe logs and visualize instantly",
5
5
  "type": "module",
6
6
  "bin": {