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.
- package/dist/hypha-rpc-websocket.js +266 -1
- package/dist/hypha-rpc-websocket.js.map +1 -1
- package/dist/hypha-rpc-websocket.min.js +1 -1
- package/dist/hypha-rpc-websocket.min.js.map +1 -1
- package/dist/hypha-rpc-websocket.min.mjs +1 -1
- package/dist/hypha-rpc-websocket.min.mjs.map +1 -1
- package/dist/hypha-rpc-websocket.mjs +266 -1
- package/dist/hypha-rpc-websocket.mjs.map +1 -1
- package/package.json +1 -1
- package/src/rpc.js +266 -1
|
@@ -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
|
-
|
|
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) {
|