hypha-rpc 0.20.79 → 0.20.81

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.
@@ -336,6 +336,9 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
336
336
  services: this._services,
337
337
  };
338
338
 
339
+ // Track background tasks for proper cleanup
340
+ this._background_tasks = new Set();
341
+
339
342
  // Set up global unhandled promise rejection handler for RPC-related errors
340
343
  const handleUnhandledRejection = (event) => {
341
344
  const reason = event.reason;
@@ -447,6 +450,57 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
447
450
  registered: registeredCount,
448
451
  failed: failedServices,
449
452
  });
453
+
454
+ // Subscribe to client_disconnected events if the manager supports it
455
+ try {
456
+ if (
457
+ manager.subscribe &&
458
+ typeof manager.subscribe === "function"
459
+ ) {
460
+ console.debug("Subscribing to client_disconnected events");
461
+
462
+ const handleClientDisconnected = async (event) => {
463
+ // The client ID is in event.data.id based on the event structure
464
+ const clientId = event.data?.id || event.client;
465
+ const workspace = event.data?.workspace;
466
+ if (clientId && workspace) {
467
+ // Construct the full client path with workspace prefix
468
+ const fullClientId = `${workspace}/${clientId}`;
469
+ console.debug(
470
+ `Client ${fullClientId} disconnected, cleaning up sessions`,
471
+ );
472
+ await this._handleClientDisconnected(fullClientId);
473
+ } else if (clientId) {
474
+ console.debug(
475
+ `Client ${clientId} disconnected, cleaning up sessions`,
476
+ );
477
+ await this._handleClientDisconnected(clientId);
478
+ }
479
+ };
480
+
481
+ // Subscribe to the event topic first
482
+ this._clientDisconnectedSubscription = await manager.subscribe([
483
+ "client_disconnected",
484
+ ]);
485
+
486
+ // Then register the local event handler
487
+ this.on("client_disconnected", handleClientDisconnected);
488
+
489
+ console.debug(
490
+ "Successfully subscribed to client_disconnected events",
491
+ );
492
+ } else {
493
+ console.debug(
494
+ "Manager does not support subscribe method, skipping client_disconnected handling",
495
+ );
496
+ this._clientDisconnectedSubscription = null;
497
+ }
498
+ } catch (subscribeError) {
499
+ console.warn(
500
+ `Failed to subscribe to client_disconnected events: ${subscribeError}`,
501
+ );
502
+ this._clientDisconnectedSubscription = null;
503
+ }
450
504
  } catch (managerError) {
451
505
  console.error(
452
506
  `Failed to get manager service for registering services: ${managerError}`,
@@ -640,6 +694,9 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
640
694
  }
641
695
 
642
696
  close() {
697
+ // Clean up all pending sessions before closing
698
+ this._cleanupOnDisconnect();
699
+
643
700
  // Clear all heartbeat intervals
644
701
  for (const session_id in this._object_store) {
645
702
  if (this._object_store.hasOwnProperty(session_id)) {
@@ -652,12 +709,220 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
652
709
  }
653
710
  }
654
711
  }
712
+
713
+ // Unsubscribe from client_disconnected events if subscribed
714
+ if (this._clientDisconnectedSubscription) {
715
+ try {
716
+ // Get the manager service to unsubscribe (non-blocking)
717
+ if (this._connection && this._connection.manager_id) {
718
+ this.get_remote_service("*/" + this._connection.manager_id)
719
+ .then((manager) => {
720
+ if (
721
+ manager.unsubscribe &&
722
+ typeof manager.unsubscribe === "function"
723
+ ) {
724
+ return manager.unsubscribe("client_disconnected");
725
+ }
726
+ })
727
+ .catch((e) => {
728
+ console.debug(
729
+ `Error unsubscribing from client_disconnected: ${e}`,
730
+ );
731
+ });
732
+ }
733
+ // Remove the local event handler
734
+ this.off("client_disconnected");
735
+ } catch (e) {
736
+ console.debug(`Error unsubscribing from client_disconnected: ${e}`);
737
+ }
738
+ }
739
+
740
+ // Clean up background tasks
741
+ try {
742
+ // Cancel all background tasks
743
+ for (const task of this._background_tasks) {
744
+ if (task && typeof task.cancel === "function") {
745
+ try {
746
+ task.cancel();
747
+ } catch (e) {
748
+ console.debug(`Error canceling background task: ${e}`);
749
+ }
750
+ }
751
+ }
752
+ this._background_tasks.clear();
753
+ } catch (e) {
754
+ console.debug(`Error cleaning up background tasks: ${e}`);
755
+ }
756
+
757
+ // Clean up connection references to prevent circular references
758
+ try {
759
+ // Clear connection reference to break circular references
760
+ this._connection = null;
761
+
762
+ // Replace emit_message with a no-op to prevent further calls
763
+ this._emit_message = function () {
764
+ console.debug("RPC connection closed, ignoring message");
765
+ return Promise.reject(new Error("Connection is closed"));
766
+ };
767
+ } catch (e) {
768
+ console.debug(`Error during connection cleanup: ${e}`);
769
+ }
770
+
655
771
  this._fire("disconnected");
656
772
  }
657
773
 
774
+ async _handleClientDisconnected(clientId) {
775
+ try {
776
+ console.debug(`Handling disconnection for client: ${clientId}`);
777
+
778
+ // Clean up all sessions for the disconnected client
779
+ const sessionsCleaned = this._cleanupSessionsForClient(clientId);
780
+
781
+ if (sessionsCleaned > 0) {
782
+ console.debug(
783
+ `Cleaned up ${sessionsCleaned} sessions for disconnected client: ${clientId}`,
784
+ );
785
+ }
786
+
787
+ // Fire an event to notify about the client disconnection
788
+ this._fire("remote_client_disconnected", {
789
+ client_id: clientId,
790
+ sessions_cleaned: sessionsCleaned,
791
+ });
792
+ } catch (e) {
793
+ console.error(
794
+ `Error handling client disconnection for ${clientId}: ${e}`,
795
+ );
796
+ }
797
+ }
798
+
799
+ _cleanupSessionsForClient(clientId) {
800
+ let sessionsCleaned = 0;
801
+
802
+ // Iterate through all top-level session keys
803
+ for (const sessionKey of Object.keys(this._object_store)) {
804
+ if (sessionKey === "services" || sessionKey === "message_cache") {
805
+ continue;
806
+ }
807
+
808
+ const session = this._object_store[sessionKey];
809
+ if (!session || typeof session !== "object") {
810
+ continue;
811
+ }
812
+
813
+ // Check if this session belongs to the disconnected client
814
+ // Sessions have a target_id property that identifies which client they're calling
815
+ if (session.target_id === clientId) {
816
+ // Reject any pending promises in this session
817
+ if (session.reject && typeof session.reject === "function") {
818
+ console.debug(`Rejecting session ${sessionKey}`);
819
+ try {
820
+ session.reject(new Error(`Client disconnected: ${clientId}`));
821
+ } catch (e) {
822
+ console.warn(`Error rejecting session ${sessionKey}: ${e}`);
823
+ }
824
+ }
825
+
826
+ if (session.resolve && typeof session.resolve === "function") {
827
+ console.debug(`Resolving session ${sessionKey} with error`);
828
+ try {
829
+ session.resolve(new Error(`Client disconnected: ${clientId}`));
830
+ } catch (e) {
831
+ console.warn(`Error resolving session ${sessionKey}: ${e}`);
832
+ }
833
+ }
834
+
835
+ // Clear any timers
836
+ if (session.timer && typeof session.timer.clear === "function") {
837
+ try {
838
+ session.timer.clear();
839
+ } catch (e) {
840
+ console.warn(`Error clearing timer for ${sessionKey}: ${e}`);
841
+ }
842
+ }
843
+
844
+ // Clear heartbeat tasks
845
+ if (session.heartbeat_task) {
846
+ try {
847
+ clearInterval(session.heartbeat_task);
848
+ } catch (e) {
849
+ console.warn(`Error clearing heartbeat for ${sessionKey}: ${e}`);
850
+ }
851
+ }
852
+
853
+ // Remove the entire session
854
+ delete this._object_store[sessionKey];
855
+ sessionsCleaned++;
856
+ console.debug(`Cleaned up session: ${sessionKey}`);
857
+ }
858
+ }
859
+
860
+ return sessionsCleaned;
861
+ }
862
+
863
+ _cleanupOnDisconnect() {
864
+ try {
865
+ console.debug("Cleaning up all sessions due to local RPC disconnection");
866
+
867
+ // Get all keys to delete after cleanup
868
+ const keysToDelete = [];
869
+
870
+ for (const key of Object.keys(this._object_store)) {
871
+ if (key === "services") {
872
+ continue;
873
+ }
874
+
875
+ const value = this._object_store[key];
876
+
877
+ if (typeof value === "object" && value !== null) {
878
+ // Reject any pending promises
879
+ if (value.reject && typeof value.reject === "function") {
880
+ try {
881
+ value.reject(new Error("RPC connection closed"));
882
+ } catch (e) {
883
+ console.debug(`Error rejecting promise during cleanup: ${e}`);
884
+ }
885
+ }
886
+
887
+ // Clean up timers and tasks
888
+ if (value.heartbeat_task) {
889
+ clearInterval(value.heartbeat_task);
890
+ }
891
+ if (value.timer && typeof value.timer.clear === "function") {
892
+ try {
893
+ value.timer.clear();
894
+ } catch (e) {
895
+ console.debug(`Error clearing timer: ${e}`);
896
+ }
897
+ }
898
+ }
899
+
900
+ // Mark ALL keys for deletion except services
901
+ keysToDelete.push(key);
902
+ }
903
+
904
+ // Delete all marked sessions
905
+ for (const key of keysToDelete) {
906
+ delete this._object_store[key];
907
+ }
908
+ } catch (e) {
909
+ console.error(`Error during cleanup on disconnect: ${e}`);
910
+ }
911
+ }
912
+
658
913
  async disconnect() {
914
+ // Store connection reference before closing
915
+ const connection = this._connection;
659
916
  this.close();
660
- await this._connection.disconnect();
917
+
918
+ // Disconnect the underlying connection if it exists
919
+ if (connection) {
920
+ try {
921
+ await connection.disconnect();
922
+ } catch (e) {
923
+ console.debug(`Error disconnecting underlying connection: ${e}`);
924
+ }
925
+ }
661
926
  }
662
927
 
663
928
  async _get_manager_with_retry(maxRetries = 20) {