hypha-rpc 0.21.6 → 0.21.7
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 +168 -44
- 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 +168 -44
- package/dist/hypha-rpc-websocket.mjs.map +1 -1
- package/package.json +1 -1
- package/src/rpc.js +138 -44
- package/src/utils/index.js +30 -0
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-10T15:43:31.611Z
|
|
90
90
|
</div>
|
|
91
91
|
<script src="prettify.js"></script>
|
|
92
92
|
<script>
|
|
@@ -1201,9 +1201,9 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
1201
1201
|
// Track background tasks for proper cleanup
|
|
1202
1202
|
this._background_tasks = new Set();
|
|
1203
1203
|
|
|
1204
|
-
// Periodic session sweep for
|
|
1205
|
-
//
|
|
1206
|
-
this._sessionMaxAge =
|
|
1204
|
+
// Periodic session sweep for stale sessions with no activity.
|
|
1205
|
+
// Default: 10 minutes (matching Python).
|
|
1206
|
+
this._sessionMaxAge = 10 * 60 * 1000;
|
|
1207
1207
|
this._sessionSweepInterval = setInterval(() => {
|
|
1208
1208
|
this._sweepStaleSessions();
|
|
1209
1209
|
}, this._sessionMaxAge / 2);
|
|
@@ -2295,6 +2295,10 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
2295
2295
|
if (!this._services[service_id]) {
|
|
2296
2296
|
throw new Error(`Service not found: ${service_id}`);
|
|
2297
2297
|
}
|
|
2298
|
+
// Auto-detect _rintf services (local-only, never registered with server)
|
|
2299
|
+
if (service_id.startsWith("_rintf_")) {
|
|
2300
|
+
notify = false;
|
|
2301
|
+
}
|
|
2298
2302
|
if (notify) {
|
|
2299
2303
|
const manager = await this.get_manager_service({
|
|
2300
2304
|
timeout: 10,
|
|
@@ -2356,12 +2360,10 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
2356
2360
|
|
|
2357
2361
|
// Clean up the entire session when resolve/reject is called
|
|
2358
2362
|
if (clear_after_called && self._object_store[session_id]) {
|
|
2359
|
-
// For promise callbacks (resolve/reject), clean up the entire session
|
|
2360
2363
|
if (name === "resolve" || name === "reject") {
|
|
2361
2364
|
self._removeFromTargetIdIndex(session_id);
|
|
2362
2365
|
delete self._object_store[session_id];
|
|
2363
2366
|
} else {
|
|
2364
|
-
// For other callbacks, just clean up this specific callback
|
|
2365
2367
|
self._cleanup_session_if_needed(session_id, name);
|
|
2366
2368
|
}
|
|
2367
2369
|
}
|
|
@@ -2653,11 +2655,14 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
2653
2655
|
for (const key of Object.keys(this._object_store)) {
|
|
2654
2656
|
if (key === "services" || key === "message_cache") continue;
|
|
2655
2657
|
const session = this._object_store[key];
|
|
2658
|
+
// Use last-activity time if available, fall back to creation time
|
|
2659
|
+
const lastActivity =
|
|
2660
|
+
session && (session._last_activity_at || session._created_at);
|
|
2656
2661
|
if (
|
|
2657
2662
|
session &&
|
|
2658
2663
|
typeof session === "object" &&
|
|
2659
|
-
|
|
2660
|
-
now -
|
|
2664
|
+
lastActivity &&
|
|
2665
|
+
now - lastActivity > this._sessionMaxAge
|
|
2661
2666
|
) {
|
|
2662
2667
|
// Only sweep sessions that have no timer (active timers mean they are in use)
|
|
2663
2668
|
// and no active promise callbacks (resolve/reject mean the session is awaiting a response)
|
|
@@ -2983,25 +2988,7 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
2983
2988
|
],
|
|
2984
2989
|
method_name,
|
|
2985
2990
|
);
|
|
2986
|
-
|
|
2987
|
-
// However, if the args contains _rintf === true, we will not clear the session
|
|
2988
|
-
|
|
2989
|
-
// Helper function to recursively check for _rintf objects
|
|
2990
|
-
function hasInterfaceObject(obj) {
|
|
2991
|
-
if (!obj || typeof obj !== "object") return false;
|
|
2992
|
-
if (obj._rintf === true) return true;
|
|
2993
|
-
if (Array.isArray(obj)) {
|
|
2994
|
-
return obj.some((item) => hasInterfaceObject(item));
|
|
2995
|
-
}
|
|
2996
|
-
if (obj.constructor === Object) {
|
|
2997
|
-
return Object.values(obj).some((value) =>
|
|
2998
|
-
hasInterfaceObject(value),
|
|
2999
|
-
);
|
|
3000
|
-
}
|
|
3001
|
-
return false;
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
let clear_after_called = !hasInterfaceObject(args);
|
|
2991
|
+
let clear_after_called = true;
|
|
3005
2992
|
|
|
3006
2993
|
const promiseData = await self._encode_promise(
|
|
3007
2994
|
resolve,
|
|
@@ -3194,6 +3181,18 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
3194
3181
|
|
|
3195
3182
|
try {
|
|
3196
3183
|
method = indexObject(this._object_store, data["method"]);
|
|
3184
|
+
// Update last-activity time for session GC
|
|
3185
|
+
const methodParts = data["method"].split(".");
|
|
3186
|
+
if (methodParts.length > 1) {
|
|
3187
|
+
const topKey = methodParts[0];
|
|
3188
|
+
if (topKey !== "services" && topKey !== "message_cache") {
|
|
3189
|
+
// Skip system stores — they are not GC-managed sessions
|
|
3190
|
+
const topSession = this._object_store[topKey];
|
|
3191
|
+
if (topSession && typeof topSession === "object") {
|
|
3192
|
+
topSession._last_activity_at = Date.now();
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3197
3196
|
} catch (e) {
|
|
3198
3197
|
// Clean promise method detection - TYPE-BASED, not string-based
|
|
3199
3198
|
if (this._is_promise_method_call(data["method"])) {
|
|
@@ -3310,7 +3309,9 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
3310
3309
|
}
|
|
3311
3310
|
|
|
3312
3311
|
// Make sure the parent session is still open
|
|
3313
|
-
|
|
3312
|
+
// Skip for service methods — services are persistent and don't
|
|
3313
|
+
// depend on the originating session being alive.
|
|
3314
|
+
if (local_parent && !data.method.startsWith("services.")) {
|
|
3314
3315
|
// The parent session should be a session that generate the current method call
|
|
3315
3316
|
(0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.assert)(
|
|
3316
3317
|
this._get_session_store(local_parent, true) !== null,
|
|
@@ -3400,7 +3401,9 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
3400
3401
|
// Create the last level
|
|
3401
3402
|
if (!store[levels[last_index]]) {
|
|
3402
3403
|
store[levels[last_index]] = {};
|
|
3403
|
-
|
|
3404
|
+
const now = Date.now();
|
|
3405
|
+
store[levels[last_index]]._created_at = now;
|
|
3406
|
+
store[levels[last_index]]._last_activity_at = now;
|
|
3404
3407
|
}
|
|
3405
3408
|
return store[levels[last_index]];
|
|
3406
3409
|
} else {
|
|
@@ -3465,13 +3468,19 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
3465
3468
|
return bObject;
|
|
3466
3469
|
}
|
|
3467
3470
|
|
|
3468
|
-
if (
|
|
3469
|
-
|
|
3471
|
+
if (
|
|
3472
|
+
(0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.isGenerator)(aObject) ||
|
|
3473
|
+
(0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.isAsyncGenerator)(aObject) ||
|
|
3474
|
+
(0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.isAsyncIterator)(aObject) ||
|
|
3475
|
+
(0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.isSyncIterator)(aObject)
|
|
3476
|
+
) {
|
|
3477
|
+
// Handle generator/iterator objects
|
|
3470
3478
|
(0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.assert)(
|
|
3471
3479
|
session_id && typeof session_id === "string",
|
|
3472
3480
|
"Session ID is required for generator encoding",
|
|
3473
3481
|
);
|
|
3474
3482
|
const object_id = (0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.randId)();
|
|
3483
|
+
const close_id = object_id + ":close";
|
|
3475
3484
|
|
|
3476
3485
|
// Get the session store
|
|
3477
3486
|
const store = this._get_session_store(session_id, true);
|
|
@@ -3480,32 +3489,52 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
3480
3489
|
`Failed to create session store ${session_id} due to invalid parent`,
|
|
3481
3490
|
);
|
|
3482
3491
|
|
|
3483
|
-
// Check if it's an async generator
|
|
3484
|
-
const isAsync = (0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.isAsyncGenerator)(aObject);
|
|
3492
|
+
// Check if it's an async generator/iterator
|
|
3493
|
+
const isAsync = (0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.isAsyncGenerator)(aObject) || (0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.isAsyncIterator)(aObject);
|
|
3485
3494
|
|
|
3486
|
-
// Define method to get next item from the generator
|
|
3495
|
+
// Define method to get next item from the generator/iterator
|
|
3487
3496
|
const nextItemMethod = async () => {
|
|
3488
3497
|
if (isAsync) {
|
|
3489
|
-
const
|
|
3490
|
-
const result = await iterator.next();
|
|
3498
|
+
const result = await aObject.next();
|
|
3491
3499
|
if (result.done) {
|
|
3492
3500
|
delete store[object_id];
|
|
3501
|
+
delete store[close_id];
|
|
3493
3502
|
return { _rtype: "stop_iteration" };
|
|
3494
3503
|
}
|
|
3495
3504
|
return result.value;
|
|
3496
3505
|
} else {
|
|
3497
|
-
const
|
|
3498
|
-
const result = iterator.next();
|
|
3506
|
+
const result = aObject.next();
|
|
3499
3507
|
if (result.done) {
|
|
3500
3508
|
delete store[object_id];
|
|
3509
|
+
delete store[close_id];
|
|
3501
3510
|
return { _rtype: "stop_iteration" };
|
|
3502
3511
|
}
|
|
3503
3512
|
return result.value;
|
|
3504
3513
|
}
|
|
3505
3514
|
};
|
|
3506
3515
|
|
|
3507
|
-
//
|
|
3516
|
+
// Define method to close/cleanup the generator/iterator early
|
|
3517
|
+
const closeGeneratorMethod = async () => {
|
|
3518
|
+
try {
|
|
3519
|
+
if (typeof aObject.return === "function") {
|
|
3520
|
+
if (isAsync) {
|
|
3521
|
+
await aObject.return();
|
|
3522
|
+
} else {
|
|
3523
|
+
aObject.return();
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
} catch (e) {
|
|
3527
|
+
// ignore close errors
|
|
3528
|
+
} finally {
|
|
3529
|
+
delete store[object_id];
|
|
3530
|
+
delete store[close_id];
|
|
3531
|
+
}
|
|
3532
|
+
return true;
|
|
3533
|
+
};
|
|
3534
|
+
|
|
3535
|
+
// Store both methods in the session
|
|
3508
3536
|
store[object_id] = nextItemMethod;
|
|
3537
|
+
store[close_id] = closeGeneratorMethod;
|
|
3509
3538
|
|
|
3510
3539
|
// Create a method that will be used to fetch the next item from the generator
|
|
3511
3540
|
bObject = {
|
|
@@ -3513,6 +3542,7 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
3513
3542
|
_rserver: this._server_base_url,
|
|
3514
3543
|
_rtarget: this._client_id,
|
|
3515
3544
|
_rmethod: `${session_id}.${object_id}`,
|
|
3545
|
+
_rclose_method: `${session_id}.${close_id}`,
|
|
3516
3546
|
_rpromise: "*",
|
|
3517
3547
|
_rdoc: "Remote generator",
|
|
3518
3548
|
};
|
|
@@ -3708,6 +3738,38 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
3708
3738
|
Array.isArray(aObject) ||
|
|
3709
3739
|
aObject instanceof RemoteService
|
|
3710
3740
|
) {
|
|
3741
|
+
// Auto-register _rintf objects as local services
|
|
3742
|
+
if (
|
|
3743
|
+
!isarray &&
|
|
3744
|
+
aObject._rintf === true &&
|
|
3745
|
+
Object.keys(aObject).some(
|
|
3746
|
+
(k) => !k.startsWith("_") && typeof aObject[k] === "function",
|
|
3747
|
+
)
|
|
3748
|
+
) {
|
|
3749
|
+
const serviceId = `_rintf_${(0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.randId)()}`;
|
|
3750
|
+
const serviceApi = { id: serviceId };
|
|
3751
|
+
for (const k of Object.keys(aObject)) {
|
|
3752
|
+
if (!k.startsWith("_") && typeof aObject[k] === "function") {
|
|
3753
|
+
serviceApi[k] = aObject[k];
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
this.add_service(serviceApi, true);
|
|
3757
|
+
// Store service_id back on the original object so the caller
|
|
3758
|
+
// can later call rpc.unregister_service(serviceId) to clean up.
|
|
3759
|
+
aObject._rintf_service_id = serviceId;
|
|
3760
|
+
// Encode all values — callables are now annotated as service methods
|
|
3761
|
+
bObject = {};
|
|
3762
|
+
for (const key of Object.keys(aObject)) {
|
|
3763
|
+
bObject[key] = await this._encode(
|
|
3764
|
+
aObject[key],
|
|
3765
|
+
session_id,
|
|
3766
|
+
local_workspace,
|
|
3767
|
+
);
|
|
3768
|
+
}
|
|
3769
|
+
bObject._rintf_service_id = serviceId;
|
|
3770
|
+
return bObject;
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3711
3773
|
// Fast path: if all values are primitives, return as-is
|
|
3712
3774
|
if (isarray) {
|
|
3713
3775
|
if (_allPrimitivesArray(aObject)) return aObject;
|
|
@@ -3788,19 +3850,49 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
3788
3850
|
local_workspace,
|
|
3789
3851
|
);
|
|
3790
3852
|
|
|
3791
|
-
// Create
|
|
3853
|
+
// Create close method if available
|
|
3854
|
+
let close_method = null;
|
|
3855
|
+
if (aObject._rclose_method) {
|
|
3856
|
+
const closeObj = {
|
|
3857
|
+
_rtype: "method",
|
|
3858
|
+
_rserver: aObject._rserver,
|
|
3859
|
+
_rtarget: aObject._rtarget,
|
|
3860
|
+
_rmethod: aObject._rclose_method,
|
|
3861
|
+
_rpromise: "*",
|
|
3862
|
+
};
|
|
3863
|
+
close_method = this._generate_remote_method(
|
|
3864
|
+
closeObj,
|
|
3865
|
+
remote_parent,
|
|
3866
|
+
local_parent,
|
|
3867
|
+
remote_workspace,
|
|
3868
|
+
local_workspace,
|
|
3869
|
+
);
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
// Create an async generator proxy with cleanup support
|
|
3792
3873
|
async function* asyncGeneratorProxy() {
|
|
3793
|
-
|
|
3794
|
-
|
|
3874
|
+
let completedNormally = false;
|
|
3875
|
+
try {
|
|
3876
|
+
while (true) {
|
|
3795
3877
|
const next_item = await gen_method();
|
|
3796
3878
|
// Check for StopIteration signal
|
|
3797
3879
|
if (next_item && next_item._rtype === "stop_iteration") {
|
|
3880
|
+
completedNormally = true;
|
|
3798
3881
|
break;
|
|
3799
3882
|
}
|
|
3800
3883
|
yield next_item;
|
|
3801
|
-
}
|
|
3802
|
-
|
|
3803
|
-
|
|
3884
|
+
}
|
|
3885
|
+
} catch (error) {
|
|
3886
|
+
console.error("Error in generator:", error);
|
|
3887
|
+
throw error;
|
|
3888
|
+
} finally {
|
|
3889
|
+
// If not completed normally, send close signal to clean up remote generator
|
|
3890
|
+
if (!completedNormally && close_method) {
|
|
3891
|
+
try {
|
|
3892
|
+
await close_method();
|
|
3893
|
+
} catch (e) {
|
|
3894
|
+
// ignore close errors
|
|
3895
|
+
}
|
|
3804
3896
|
}
|
|
3805
3897
|
}
|
|
3806
3898
|
}
|
|
@@ -3982,7 +4074,9 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
3982
4074
|
/* harmony export */ dtypeToTypedArray: () => (/* binding */ dtypeToTypedArray),
|
|
3983
4075
|
/* harmony export */ expandKwargs: () => (/* binding */ expandKwargs),
|
|
3984
4076
|
/* harmony export */ isAsyncGenerator: () => (/* binding */ isAsyncGenerator),
|
|
4077
|
+
/* harmony export */ isAsyncIterator: () => (/* binding */ isAsyncIterator),
|
|
3985
4078
|
/* harmony export */ isGenerator: () => (/* binding */ isGenerator),
|
|
4079
|
+
/* harmony export */ isSyncIterator: () => (/* binding */ isSyncIterator),
|
|
3986
4080
|
/* harmony export */ loadRequirements: () => (/* binding */ loadRequirements),
|
|
3987
4081
|
/* harmony export */ loadRequirementsInWebworker: () => (/* binding */ loadRequirementsInWebworker),
|
|
3988
4082
|
/* harmony export */ loadRequirementsInWindow: () => (/* binding */ loadRequirementsInWindow),
|
|
@@ -4538,6 +4632,36 @@ function isAsyncGenerator(obj) {
|
|
|
4538
4632
|
);
|
|
4539
4633
|
}
|
|
4540
4634
|
|
|
4635
|
+
/**
|
|
4636
|
+
* Check if an object is a custom async iterator (has Symbol.asyncIterator and next()) but not an async generator
|
|
4637
|
+
* @param {any} obj - Object to check
|
|
4638
|
+
* @returns {boolean} True if object is a custom async iterator
|
|
4639
|
+
*/
|
|
4640
|
+
function isAsyncIterator(obj) {
|
|
4641
|
+
if (!obj || isAsyncGenerator(obj)) return false;
|
|
4642
|
+
return (
|
|
4643
|
+
typeof obj === "object" &&
|
|
4644
|
+
Symbol.asyncIterator in Object(obj) &&
|
|
4645
|
+
typeof obj.next === "function"
|
|
4646
|
+
);
|
|
4647
|
+
}
|
|
4648
|
+
|
|
4649
|
+
/**
|
|
4650
|
+
* Check if an object is a custom sync iterator (has Symbol.iterator and next()) but not a generator
|
|
4651
|
+
* @param {any} obj - Object to check
|
|
4652
|
+
* @returns {boolean} True if object is a custom sync iterator
|
|
4653
|
+
*/
|
|
4654
|
+
function isSyncIterator(obj) {
|
|
4655
|
+
if (!obj || isGenerator(obj)) return false;
|
|
4656
|
+
return (
|
|
4657
|
+
typeof obj === "object" &&
|
|
4658
|
+
Symbol.iterator in Object(obj) &&
|
|
4659
|
+
typeof obj.next === "function" &&
|
|
4660
|
+
!Array.isArray(obj) &&
|
|
4661
|
+
typeof obj !== "string"
|
|
4662
|
+
);
|
|
4663
|
+
}
|
|
4664
|
+
|
|
4541
4665
|
|
|
4542
4666
|
/***/ }),
|
|
4543
4667
|
|