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.
@@ -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-09T22:05:05.578Z
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 interface-object sessions (clear_after_called=false)
1205
- // that have no activity for a long time. Max age = 10 * method_timeout.
1206
- this._sessionMaxAge = (this._method_timeout || 30) * 10 * 1000;
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
- session._created_at &&
2660
- now - session._created_at > this._sessionMaxAge
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
- // By default, hypha will clear the session after the method is called
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
- if (local_parent) {
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
- store[levels[last_index]]._created_at = Date.now();
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 ((0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.isGenerator)(aObject) || (0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.isAsyncGenerator)(aObject)) {
3469
- // Handle generator functions and generator objects
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 iterator = aObject;
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 iterator = aObject;
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
- // Store the next_item method in the session
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 an async generator proxy
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
- while (true) {
3794
- try {
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
- } catch (error) {
3802
- console.error("Error in generator:", error);
3803
- throw error;
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