hypha-rpc 0.20.79 → 0.20.80

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