hypha-rpc 0.21.25 → 0.21.28
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/coverage/html/index.html +1 -1
- package/dist/hypha-rpc-websocket.js +138 -45
- 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 +138 -45
- package/dist/hypha-rpc-websocket.mjs.map +1 -1
- package/package.json +1 -1
- package/src/http-client.js +7 -7
- package/src/rpc.js +116 -7
- package/src/websocket-client.js +15 -31
package/coverage/html/index.html
CHANGED
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
87
87
|
Code coverage generated by
|
|
88
88
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
89
|
-
at 2026-02-
|
|
89
|
+
at 2026-02-27T13:29:51.171Z
|
|
90
90
|
</div>
|
|
91
91
|
<script src="prettify.js"></script>
|
|
92
92
|
<script>
|
|
@@ -3281,25 +3281,25 @@ async function _connectToServerHTTP(config) {
|
|
|
3281
3281
|
// Auto-refresh workspace manager proxy after reconnection.
|
|
3282
3282
|
// See websocket-client.js for detailed explanation.
|
|
3283
3283
|
let isInitialRefresh = true;
|
|
3284
|
-
rpc.on("manager_refreshed", async (
|
|
3284
|
+
rpc.on("manager_refreshed", async () => {
|
|
3285
3285
|
if (isInitialRefresh) {
|
|
3286
3286
|
isInitialRefresh = false;
|
|
3287
3287
|
return;
|
|
3288
3288
|
}
|
|
3289
3289
|
try {
|
|
3290
|
-
const
|
|
3291
|
-
for (const key of Object.keys(
|
|
3292
|
-
if (typeof
|
|
3293
|
-
wm[key] =
|
|
3290
|
+
const newTarget = `*/${rpc._connection.manager_id}`;
|
|
3291
|
+
for (const key of Object.keys(wm)) {
|
|
3292
|
+
if (typeof wm[key] === "function" && wm[key].__rpc_object__) {
|
|
3293
|
+
wm[key].__rpc_object__._rtarget = newTarget;
|
|
3294
3294
|
}
|
|
3295
3295
|
}
|
|
3296
3296
|
console.info(
|
|
3297
|
-
"Workspace manager proxy
|
|
3297
|
+
"Workspace manager proxy retargeted after reconnection (new manager_id:",
|
|
3298
3298
|
rpc._connection?.manager_id + ")",
|
|
3299
3299
|
);
|
|
3300
3300
|
} catch (err) {
|
|
3301
3301
|
console.warn(
|
|
3302
|
-
"Failed to
|
|
3302
|
+
"Failed to retarget workspace manager after reconnection:",
|
|
3303
3303
|
err,
|
|
3304
3304
|
);
|
|
3305
3305
|
}
|
|
@@ -3829,6 +3829,8 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
3829
3829
|
};
|
|
3830
3830
|
// Index: target_id -> Set of top-level session keys for fast cleanup
|
|
3831
3831
|
this._targetIdIndex = {};
|
|
3832
|
+
// Index: allowed_caller -> Set of _rintf service IDs for lifecycle cleanup
|
|
3833
|
+
this._rintfCallerIndex = {};
|
|
3832
3834
|
// Track last known manager_id for stale call rejection on reconnection
|
|
3833
3835
|
this._last_manager_id = null;
|
|
3834
3836
|
|
|
@@ -4471,23 +4473,55 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4471
4473
|
this._fire("disconnected");
|
|
4472
4474
|
}
|
|
4473
4475
|
|
|
4476
|
+
/**
|
|
4477
|
+
* Unregister a single _rintf service and remove it from the caller index.
|
|
4478
|
+
* Safe to call even if the service was already removed.
|
|
4479
|
+
*/
|
|
4480
|
+
_unregisterRintfService(serviceId, allowedCaller) {
|
|
4481
|
+
if (this._services[serviceId]) {
|
|
4482
|
+
delete this._services[serviceId];
|
|
4483
|
+
}
|
|
4484
|
+
if (allowedCaller && this._rintfCallerIndex[allowedCaller]) {
|
|
4485
|
+
this._rintfCallerIndex[allowedCaller].delete(serviceId);
|
|
4486
|
+
if (this._rintfCallerIndex[allowedCaller].size === 0) {
|
|
4487
|
+
delete this._rintfCallerIndex[allowedCaller];
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
/**
|
|
4493
|
+
* Remove all _rintf services whose allowed caller is the given client.
|
|
4494
|
+
* Passive lifecycle cleanup: when a client disconnects, any _rintf
|
|
4495
|
+
* callbacks that only it could invoke become dead resources.
|
|
4496
|
+
* @returns {number} Number of _rintf services cleaned up
|
|
4497
|
+
*/
|
|
4498
|
+
_cleanupRintfForCaller(clientId) {
|
|
4499
|
+
const serviceIds = this._rintfCallerIndex[clientId];
|
|
4500
|
+
if (!serviceIds) return 0;
|
|
4501
|
+
delete this._rintfCallerIndex[clientId];
|
|
4502
|
+
let cleaned = 0;
|
|
4503
|
+
for (const sid of serviceIds) {
|
|
4504
|
+
if (this._services[sid]) {
|
|
4505
|
+
delete this._services[sid];
|
|
4506
|
+
cleaned++;
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
return cleaned;
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4474
4512
|
async _handleClientDisconnected(clientId) {
|
|
4475
4513
|
try {
|
|
4476
|
-
// console.debug(`Handling disconnection for client: ${clientId}`);
|
|
4477
|
-
|
|
4478
4514
|
// Clean up all sessions for the disconnected client
|
|
4479
4515
|
const sessionsCleaned = this._cleanupSessionsForClient(clientId);
|
|
4480
4516
|
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
// `Cleaned up ${sessionsCleaned} sessions for disconnected client: ${clientId}`,
|
|
4484
|
-
// );
|
|
4485
|
-
}
|
|
4517
|
+
// Clean up _rintf services whose allowed caller is the disconnected client
|
|
4518
|
+
const rintfCleaned = this._cleanupRintfForCaller(clientId);
|
|
4486
4519
|
|
|
4487
4520
|
// Fire an event to notify about the client disconnection
|
|
4488
4521
|
this._fire("remote_client_disconnected", {
|
|
4489
4522
|
client_id: clientId,
|
|
4490
4523
|
sessions_cleaned: sessionsCleaned,
|
|
4524
|
+
rintf_cleaned: rintfCleaned,
|
|
4491
4525
|
});
|
|
4492
4526
|
} catch (e) {
|
|
4493
4527
|
console.error(
|
|
@@ -4649,6 +4683,7 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4649
4683
|
}
|
|
4650
4684
|
|
|
4651
4685
|
this._targetIdIndex = {};
|
|
4686
|
+
this._rintfCallerIndex = {};
|
|
4652
4687
|
} catch (e) {
|
|
4653
4688
|
console.error(`Error during cleanup on disconnect: ${e}`);
|
|
4654
4689
|
}
|
|
@@ -4821,6 +4856,7 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4821
4856
|
visibility,
|
|
4822
4857
|
authorized_workspaces,
|
|
4823
4858
|
trusted_keys,
|
|
4859
|
+
rintf_allowed_caller,
|
|
4824
4860
|
) {
|
|
4825
4861
|
if (typeof aObject === "function") {
|
|
4826
4862
|
// mark the method as a remote method that requires context
|
|
@@ -4834,6 +4870,7 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4834
4870
|
visibility: visibility,
|
|
4835
4871
|
authorized_workspaces: authorized_workspaces,
|
|
4836
4872
|
trusted_keys: trusted_keys,
|
|
4873
|
+
rintf_allowed_caller: rintf_allowed_caller,
|
|
4837
4874
|
});
|
|
4838
4875
|
} else if (aObject instanceof Array || aObject instanceof Object) {
|
|
4839
4876
|
for (let key of Object.keys(aObject)) {
|
|
@@ -4867,6 +4904,7 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4867
4904
|
visibility,
|
|
4868
4905
|
authorized_workspaces,
|
|
4869
4906
|
trusted_keys,
|
|
4907
|
+
rintf_allowed_caller,
|
|
4870
4908
|
);
|
|
4871
4909
|
}
|
|
4872
4910
|
}
|
|
@@ -4951,6 +4989,7 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4951
4989
|
trusted_keys.add(keyHex);
|
|
4952
4990
|
}
|
|
4953
4991
|
}
|
|
4992
|
+
const rintf_allowed_caller = api.config._rintf_allowed_caller || null;
|
|
4954
4993
|
this._annotate_service_methods(
|
|
4955
4994
|
api,
|
|
4956
4995
|
api["id"],
|
|
@@ -4959,6 +4998,7 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4959
4998
|
visibility,
|
|
4960
4999
|
authorized_workspaces,
|
|
4961
5000
|
trusted_keys,
|
|
5001
|
+
rintf_allowed_caller,
|
|
4962
5002
|
);
|
|
4963
5003
|
|
|
4964
5004
|
if (this._services[api.id]) {
|
|
@@ -5078,6 +5118,13 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
5078
5118
|
// Auto-detect _rintf services (local-only, never registered with server)
|
|
5079
5119
|
if (service_id.startsWith("_rintf_")) {
|
|
5080
5120
|
notify = false;
|
|
5121
|
+
// Also clean up from the caller index
|
|
5122
|
+
for (const [caller, sids] of Object.entries(this._rintfCallerIndex)) {
|
|
5123
|
+
sids.delete(service_id);
|
|
5124
|
+
if (sids.size === 0) {
|
|
5125
|
+
delete this._rintfCallerIndex[caller];
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5081
5128
|
}
|
|
5082
5129
|
if (notify) {
|
|
5083
5130
|
const manager = await this.get_manager_service({
|
|
@@ -5425,6 +5472,7 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
5425
5472
|
|
|
5426
5473
|
// Clear the target_id index since all sessions are removed
|
|
5427
5474
|
this._targetIdIndex = {};
|
|
5475
|
+
this._rintfCallerIndex = {};
|
|
5428
5476
|
|
|
5429
5477
|
// console.debug(`Force cleaning up ${cleaned_count} sessions`);
|
|
5430
5478
|
}
|
|
@@ -5671,6 +5719,10 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
5671
5719
|
function remote_method() {
|
|
5672
5720
|
return new Promise(async (resolve, reject) => {
|
|
5673
5721
|
try {
|
|
5722
|
+
// Read target_id from encoded_method at call time (not captured
|
|
5723
|
+
// at generation time) so that _rtarget can be updated after
|
|
5724
|
+
// reconnection without regenerating the method closures.
|
|
5725
|
+
const target_id = encoded_method._rtarget;
|
|
5674
5726
|
let local_session_id = (0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.randId)();
|
|
5675
5727
|
if (local_parent) {
|
|
5676
5728
|
// Store the children session under the parent
|
|
@@ -6118,6 +6170,25 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
6118
6170
|
remote_client_id === this._connection.manager_id
|
|
6119
6171
|
) {
|
|
6120
6172
|
// Access granted
|
|
6173
|
+
}
|
|
6174
|
+
// Allow _rintf callbacks from the specific client they were sent to
|
|
6175
|
+
else if (
|
|
6176
|
+
this._method_annotations.get(method).rintf_allowed_caller
|
|
6177
|
+
) {
|
|
6178
|
+
const allowed =
|
|
6179
|
+
this._method_annotations.get(method).rintf_allowed_caller;
|
|
6180
|
+
const caller = data.from || "";
|
|
6181
|
+
if (caller !== allowed) {
|
|
6182
|
+
throw new Error(
|
|
6183
|
+
"Permission denied for _rintf callback " +
|
|
6184
|
+
method_name +
|
|
6185
|
+
", caller " +
|
|
6186
|
+
caller +
|
|
6187
|
+
" is not the allowed caller " +
|
|
6188
|
+
allowed,
|
|
6189
|
+
);
|
|
6190
|
+
}
|
|
6191
|
+
// Access granted — caller matches the _rintf target
|
|
6121
6192
|
} else {
|
|
6122
6193
|
throw new Error(
|
|
6123
6194
|
"Permission denied for invoking protected method " +
|
|
@@ -6613,13 +6684,45 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
6613
6684
|
)
|
|
6614
6685
|
) {
|
|
6615
6686
|
const serviceId = `_rintf_${(0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.randId)()}`;
|
|
6687
|
+
// Resolve the allowed caller from the session's target_id.
|
|
6688
|
+
// Only the client this _rintf is being sent to can call it back.
|
|
6689
|
+
let allowedCaller = null;
|
|
6690
|
+
if (session_id) {
|
|
6691
|
+
const topKey = session_id.split(".")[0];
|
|
6692
|
+
const sessionStore = this._object_store[topKey];
|
|
6693
|
+
if (sessionStore && sessionStore.target_id) {
|
|
6694
|
+
allowedCaller = sessionStore.target_id;
|
|
6695
|
+
}
|
|
6696
|
+
}
|
|
6616
6697
|
const serviceApi = { id: serviceId };
|
|
6698
|
+
if (allowedCaller) {
|
|
6699
|
+
serviceApi.config = {
|
|
6700
|
+
visibility: "protected",
|
|
6701
|
+
_rintf_allowed_caller: allowedCaller,
|
|
6702
|
+
};
|
|
6703
|
+
}
|
|
6704
|
+
|
|
6705
|
+
// Add _dispose method for active lifecycle management.
|
|
6706
|
+
// The remote side (allowed caller) can call _dispose() to
|
|
6707
|
+
// actively unregister this _rintf service when it's done.
|
|
6708
|
+
const self = this;
|
|
6709
|
+
serviceApi._dispose = () => {
|
|
6710
|
+
self._unregisterRintfService(serviceId, allowedCaller);
|
|
6711
|
+
};
|
|
6712
|
+
|
|
6617
6713
|
for (const k of Object.keys(aObject)) {
|
|
6618
6714
|
if (!k.startsWith("_") && typeof aObject[k] === "function") {
|
|
6619
6715
|
serviceApi[k] = aObject[k];
|
|
6620
6716
|
}
|
|
6621
6717
|
}
|
|
6622
6718
|
this.add_service(serviceApi, true);
|
|
6719
|
+
// Track in caller index for passive cleanup on disconnect
|
|
6720
|
+
if (allowedCaller) {
|
|
6721
|
+
if (!this._rintfCallerIndex[allowedCaller]) {
|
|
6722
|
+
this._rintfCallerIndex[allowedCaller] = new Set();
|
|
6723
|
+
}
|
|
6724
|
+
this._rintfCallerIndex[allowedCaller].add(serviceId);
|
|
6725
|
+
}
|
|
6623
6726
|
// Store service_id back on the original object so the caller
|
|
6624
6727
|
// can later call rpc.unregister_service(serviceId) to clean up.
|
|
6625
6728
|
aObject._rintf_service_id = serviceId;
|
|
@@ -6632,6 +6735,12 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
6632
6735
|
local_workspace,
|
|
6633
6736
|
);
|
|
6634
6737
|
}
|
|
6738
|
+
// Encode _dispose so the remote side can call it
|
|
6739
|
+
bObject._dispose = await this._encode(
|
|
6740
|
+
serviceApi._dispose,
|
|
6741
|
+
session_id,
|
|
6742
|
+
local_workspace,
|
|
6743
|
+
);
|
|
6635
6744
|
bObject._rintf_service_id = serviceId;
|
|
6636
6745
|
return bObject;
|
|
6637
6746
|
}
|
|
@@ -10861,50 +10970,34 @@ async function connectToServer(config) {
|
|
|
10861
10970
|
wm.rpc = rpc;
|
|
10862
10971
|
|
|
10863
10972
|
// Auto-refresh workspace manager proxy after reconnection.
|
|
10864
|
-
// When the server restarts, it assigns a new manager_id. The
|
|
10865
|
-
//
|
|
10866
|
-
//
|
|
10867
|
-
//
|
|
10868
|
-
//
|
|
10869
|
-
//
|
|
10870
|
-
//
|
|
10871
|
-
// Combined with the RPC layer's immediate rejection of pending calls to
|
|
10872
|
-
// the old manager_id, recovery is near-instant.
|
|
10973
|
+
// When the server restarts, it assigns a new manager_id. The remote
|
|
10974
|
+
// methods on wm have their target baked into __rpc_object__._rtarget.
|
|
10975
|
+
// Since remote_method() reads _rtarget at call time (not generation
|
|
10976
|
+
// time), we just update _rtarget on every existing method — no need
|
|
10977
|
+
// to regenerate methods or copy from a fresh proxy. This preserves
|
|
10978
|
+
// locally-overridden methods (registerService, unregisterService, etc.)
|
|
10979
|
+
// that would otherwise be lost by wholesale function copying.
|
|
10873
10980
|
let isInitialRefresh = true;
|
|
10874
|
-
rpc.on("manager_refreshed", async (
|
|
10981
|
+
rpc.on("manager_refreshed", async () => {
|
|
10875
10982
|
if (isInitialRefresh) {
|
|
10876
10983
|
isInitialRefresh = false;
|
|
10877
10984
|
return; // Skip the first event (initial connection, wm is already fresh)
|
|
10878
10985
|
}
|
|
10879
10986
|
try {
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
timeout: config.method_timeout || 30,
|
|
10886
|
-
case_conversion: "camel",
|
|
10887
|
-
kwargs_expansion: config.kwargs_expansion,
|
|
10888
|
-
});
|
|
10889
|
-
} else {
|
|
10890
|
-
// The internal manager already uses case_conversion: "camel",
|
|
10891
|
-
// so we can copy directly without an extra RPC call
|
|
10892
|
-
freshWm = internalManager;
|
|
10893
|
-
}
|
|
10894
|
-
// Copy all function properties from fresh wm onto existing wm object.
|
|
10895
|
-
// This preserves the caller's reference while updating method targets.
|
|
10896
|
-
for (const key of Object.keys(freshWm)) {
|
|
10897
|
-
if (typeof freshWm[key] === "function") {
|
|
10898
|
-
wm[key] = freshWm[key];
|
|
10987
|
+
const newTarget = `*/${rpc._connection.manager_id}`;
|
|
10988
|
+
// Retarget all remote methods on the wm proxy to the new manager
|
|
10989
|
+
for (const key of Object.keys(wm)) {
|
|
10990
|
+
if (typeof wm[key] === "function" && wm[key].__rpc_object__) {
|
|
10991
|
+
wm[key].__rpc_object__._rtarget = newTarget;
|
|
10899
10992
|
}
|
|
10900
10993
|
}
|
|
10901
10994
|
console.info(
|
|
10902
|
-
"Workspace manager proxy
|
|
10995
|
+
"Workspace manager proxy retargeted after reconnection (new manager_id:",
|
|
10903
10996
|
rpc._connection?.manager_id + ")",
|
|
10904
10997
|
);
|
|
10905
10998
|
} catch (err) {
|
|
10906
10999
|
console.warn(
|
|
10907
|
-
"Failed to
|
|
11000
|
+
"Failed to retarget workspace manager after reconnection:",
|
|
10908
11001
|
err,
|
|
10909
11002
|
);
|
|
10910
11003
|
}
|