hypha-rpc 0.20.72 → 0.20.73

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.
@@ -336,6 +336,43 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
336
336
  services: this._services,
337
337
  };
338
338
 
339
+ // Set up global unhandled promise rejection handler for RPC-related errors
340
+ const handleUnhandledRejection = (event) => {
341
+ const reason = event.reason;
342
+ if (reason && typeof reason === "object") {
343
+ // Check if this is a "Method not found" or "Session not found" error that we can ignore
344
+ const reasonStr = reason.toString();
345
+ if (
346
+ reasonStr.includes("Method not found") ||
347
+ reasonStr.includes("Session not found") ||
348
+ reasonStr.includes("Method expired") ||
349
+ reasonStr.includes("Session not found")
350
+ ) {
351
+ console.debug(
352
+ "Ignoring expected method/session not found error:",
353
+ reason,
354
+ );
355
+ event.preventDefault(); // Prevent the default unhandled rejection behavior
356
+ return;
357
+ }
358
+ }
359
+ console.warn("Unhandled RPC promise rejection:", reason);
360
+ };
361
+
362
+ // Only set the handler if we haven't already set one for this RPC instance
363
+ if (typeof window !== "undefined" && !window._hypha_rejection_handler_set) {
364
+ window.addEventListener("unhandledrejection", handleUnhandledRejection);
365
+ window._hypha_rejection_handler_set = true;
366
+ } else if (
367
+ typeof process !== "undefined" &&
368
+ !process._hypha_rejection_handler_set
369
+ ) {
370
+ process.on("unhandledRejection", (reason, promise) => {
371
+ handleUnhandledRejection({ reason, promise, preventDefault: () => {} });
372
+ });
373
+ process._hypha_rejection_handler_set = true;
374
+ }
375
+
339
376
  if (connection) {
340
377
  this.add_service({
341
378
  id: "built-in",
@@ -1044,35 +1081,291 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1044
1081
  return [encoded, wrapped_callback];
1045
1082
  }
1046
1083
 
1047
- // Clean session management - all logic in one place
1048
1084
  _cleanup_session_if_needed(session_id, callback_name) {
1049
- // Python-style immediate cleanup - no complex logic needed
1050
- if (this._object_store[session_id]) {
1051
- delete this._object_store[session_id];
1085
+ /**
1086
+ * Clean session management - all logic in one place.
1087
+ */
1088
+ if (!session_id) {
1089
+ console.debug("Cannot cleanup session: session_id is empty");
1090
+ return;
1091
+ }
1092
+
1093
+ try {
1094
+ const store = this._get_session_store(session_id, false);
1095
+ if (!store) {
1096
+ console.debug(`Session ${session_id} not found for cleanup`);
1097
+ return;
1098
+ }
1099
+
1100
+ let should_cleanup = false;
1101
+
1102
+ // Promise sessions: let the promise manager decide cleanup
1103
+ if (store._promise_manager) {
1104
+ try {
1105
+ const promise_manager = store._promise_manager;
1106
+ if (
1107
+ promise_manager.should_cleanup_on_callback &&
1108
+ promise_manager.should_cleanup_on_callback(callback_name)
1109
+ ) {
1110
+ if (promise_manager.settle) {
1111
+ promise_manager.settle();
1112
+ }
1113
+ should_cleanup = true;
1114
+ console.debug(
1115
+ `Promise session ${session_id} settled and marked for cleanup`,
1116
+ );
1117
+ }
1118
+ } catch (e) {
1119
+ console.warn(
1120
+ `Error in promise manager cleanup for session ${session_id}:`,
1121
+ e,
1122
+ );
1123
+ }
1124
+ } else {
1125
+ // Regular sessions: only cleanup temporary method call sessions
1126
+ // Don't cleanup service registration sessions or persistent sessions
1127
+ // Only cleanup sessions that are clearly temporary promises for method calls
1128
+ if (
1129
+ (callback_name === "resolve" || callback_name === "reject") &&
1130
+ store._callbacks &&
1131
+ Object.keys(store._callbacks).includes(callback_name)
1132
+ ) {
1133
+ should_cleanup = true;
1134
+ console.debug(
1135
+ `Regular session ${session_id} marked for cleanup after ${callback_name}`,
1136
+ );
1137
+ }
1138
+ }
1139
+
1140
+ if (should_cleanup) {
1141
+ this._cleanup_session_completely(session_id);
1142
+ }
1143
+ } catch (error) {
1144
+ console.warn(`Error during session cleanup for ${session_id}:`, error);
1145
+ }
1146
+ }
1147
+
1148
+ _cleanup_session_completely(session_id) {
1149
+ /**
1150
+ * Complete session cleanup with resource management.
1151
+ */
1152
+ try {
1153
+ const store = this._get_session_store(session_id, false);
1154
+ if (!store) {
1155
+ console.debug(`Session ${session_id} already cleaned up`);
1156
+ return;
1157
+ }
1158
+
1159
+ // Clean up resources before removing session
1160
+ if (store.timer && typeof store.timer.clear === "function") {
1161
+ try {
1162
+ store.timer.clear();
1163
+ } catch (error) {
1164
+ console.warn(
1165
+ `Error clearing timer for session ${session_id}:`,
1166
+ error,
1167
+ );
1168
+ }
1169
+ }
1170
+
1171
+ if (
1172
+ store.heartbeat_task &&
1173
+ typeof store.heartbeat_task.cancel === "function"
1174
+ ) {
1175
+ try {
1176
+ store.heartbeat_task.cancel();
1177
+ } catch (error) {
1178
+ console.warn(
1179
+ `Error canceling heartbeat for session ${session_id}:`,
1180
+ error,
1181
+ );
1182
+ }
1183
+ }
1184
+
1185
+ // Navigate and clean session path
1186
+ const levels = session_id.split(".");
1187
+ let current_store = this._object_store;
1188
+
1189
+ // Navigate to parent of target level
1190
+ for (let i = 0; i < levels.length - 1; i++) {
1191
+ const level = levels[i];
1192
+ if (!current_store[level]) {
1193
+ console.debug(
1194
+ `Session path ${session_id} not found at level ${level}`,
1195
+ );
1196
+ return;
1197
+ }
1198
+ current_store = current_store[level];
1199
+ }
1200
+
1201
+ // Delete the final level
1202
+ const final_key = levels[levels.length - 1];
1203
+ if (current_store[final_key]) {
1204
+ delete current_store[final_key];
1205
+ console.debug(`Cleaned up session ${session_id}`);
1206
+
1207
+ // Clean up empty parent containers
1208
+ this._cleanup_empty_containers(levels.slice(0, -1));
1209
+ }
1210
+ } catch (error) {
1211
+ console.warn(
1212
+ `Error in complete session cleanup for ${session_id}:`,
1213
+ error,
1214
+ );
1215
+ }
1216
+ }
1217
+
1218
+ _cleanup_empty_containers(path_levels) {
1219
+ /**
1220
+ * Clean up empty parent containers to prevent memory leaks.
1221
+ */
1222
+ try {
1223
+ // Work backwards from the deepest level
1224
+ for (let depth = path_levels.length - 1; depth >= 0; depth--) {
1225
+ let current_store = this._object_store;
1226
+
1227
+ // Navigate to parent of current depth
1228
+ for (let i = 0; i < depth; i++) {
1229
+ current_store = current_store[path_levels[i]];
1230
+ if (!current_store) return; // Path doesn't exist
1231
+ }
1232
+
1233
+ // Check if container at current depth is empty
1234
+ const container_key = path_levels[depth];
1235
+ const container = current_store[container_key];
1236
+
1237
+ if (
1238
+ container &&
1239
+ typeof container === "object" &&
1240
+ Object.keys(container).length === 0
1241
+ ) {
1242
+ delete current_store[container_key];
1243
+ console.debug(
1244
+ `Cleaned up empty container at depth ${depth}: ${path_levels.slice(0, depth + 1).join(".")}`,
1245
+ );
1246
+ } else {
1247
+ // Container is not empty, stop cleanup
1248
+ break;
1249
+ }
1250
+ }
1251
+ } catch (error) {
1252
+ console.warn("Error cleaning up empty containers:", error);
1253
+ }
1254
+ }
1255
+
1256
+ get_session_stats() {
1257
+ /**
1258
+ * Get detailed session statistics.
1259
+ */
1260
+ const stats = {
1261
+ total_sessions: 0,
1262
+ promise_sessions: 0,
1263
+ regular_sessions: 0,
1264
+ sessions_with_timers: 0,
1265
+ sessions_with_heartbeat: 0,
1266
+ system_stores: {},
1267
+ session_ids: [],
1268
+ memory_usage: 0,
1269
+ };
1270
+
1271
+ if (!this._object_store) {
1272
+ return stats;
1273
+ }
1274
+
1275
+ for (const key in this._object_store) {
1276
+ const value = this._object_store[key];
1277
+
1278
+ if (["services", "message_cache"].includes(key)) {
1279
+ // System stores - don't count these as sessions
1280
+ stats.system_stores[key] = {
1281
+ size:
1282
+ typeof value === "object" && value ? Object.keys(value).length : 0,
1283
+ };
1284
+ continue;
1285
+ }
1286
+
1287
+ // Count all non-system non-empty objects as sessions
1288
+ if (value && typeof value === "object") {
1289
+ const sessionKeys = Object.keys(value);
1290
+
1291
+ // Only skip completely empty objects
1292
+ if (sessionKeys.length > 0) {
1293
+ stats.total_sessions++;
1294
+ stats.session_ids.push(key);
1295
+
1296
+ if (value._promise_manager) {
1297
+ stats.promise_sessions++;
1298
+ } else {
1299
+ stats.regular_sessions++;
1300
+ }
1301
+
1302
+ if (value._timer || value.timer) stats.sessions_with_timers++;
1303
+ if (value._heartbeat || value.heartbeat)
1304
+ stats.sessions_with_heartbeat++;
1305
+
1306
+ // Estimate memory usage
1307
+ stats.memory_usage += JSON.stringify(value).length;
1308
+ }
1309
+ }
1310
+ }
1311
+
1312
+ return stats;
1313
+ }
1314
+
1315
+ _force_cleanup_all_sessions() {
1316
+ /**
1317
+ * Force cleanup all sessions (for testing purposes).
1318
+ */
1319
+ if (!this._object_store) {
1320
+ console.debug("Force cleaning up 0 sessions");
1321
+ return;
1322
+ }
1323
+
1324
+ let cleaned_count = 0;
1325
+ const keys_to_delete = [];
1326
+
1327
+ for (const key in this._object_store) {
1328
+ // Don't delete system stores
1329
+ if (!["services", "message_cache"].includes(key)) {
1330
+ const value = this._object_store[key];
1331
+ if (
1332
+ value &&
1333
+ typeof value === "object" &&
1334
+ Object.keys(value).length > 0
1335
+ ) {
1336
+ keys_to_delete.push(key);
1337
+ cleaned_count++;
1338
+ }
1339
+ }
1340
+ }
1341
+
1342
+ // Delete the sessions
1343
+ for (const key of keys_to_delete) {
1344
+ delete this._object_store[key];
1052
1345
  }
1346
+
1347
+ console.debug(`Force cleaning up ${cleaned_count} sessions`);
1053
1348
  }
1054
1349
 
1055
1350
  // Clean helper to identify promise method calls by session type
1056
1351
  _is_promise_method_call(method_path) {
1057
1352
  const session_id = method_path.split(".")[0];
1058
- // Simply check if session exists - no complex promise manager needed
1059
- return this._object_store[session_id] !== undefined;
1353
+ const session = this._get_session_store(session_id, false);
1354
+ return session && session._promise_manager;
1060
1355
  }
1061
1356
 
1062
- // Simplified Promise Manager - no complex lifecycle needed
1357
+ // Simplified Promise Manager - enhanced version
1063
1358
  _create_promise_manager() {
1064
- // Return minimal manager - Python doesn't need complex promise tracking
1359
+ /**
1360
+ * Create a promise manager to track promise state and decide cleanup.
1361
+ */
1065
1362
  return {
1066
- settled: false,
1067
- settle() {
1068
- this.settled = true;
1363
+ should_cleanup_on_callback: (callback_name) => {
1364
+ return ["resolve", "reject"].includes(callback_name);
1069
1365
  },
1070
- is_settled() {
1071
- return this.settled;
1072
- },
1073
- should_cleanup_on_callback(callback_name) {
1074
- // Always cleanup on resolve/reject like Python
1075
- return callback_name === "resolve" || callback_name === "reject";
1366
+ settle: () => {
1367
+ // Promise is settled (resolved or rejected)
1368
+ console.debug("Promise settled");
1076
1369
  },
1077
1370
  };
1078
1371
  }
@@ -1093,10 +1386,15 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1093
1386
  );
1094
1387
  store = {};
1095
1388
  }
1389
+
1390
+ // Clean promise lifecycle management - TYPE-BASED, not string-based
1391
+ store._promise_manager = this._create_promise_manager();
1392
+ console.debug(
1393
+ `Created PROMISE session ${session_id} (type-based detection)`,
1394
+ );
1395
+
1096
1396
  let encoded = {};
1097
1397
 
1098
- // Simplified promise lifecycle - no complex manager needed
1099
- // Just store the timer if needed
1100
1398
  if (timer && reject && this._method_timeout) {
1101
1399
  [encoded.heartbeat, store.heartbeat] = this._encode_callback(
1102
1400
  "heartbeat",
@@ -1108,14 +1406,15 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1108
1406
  );
1109
1407
  store.timer = timer;
1110
1408
  encoded.interval = this._method_timeout / 2;
1409
+ } else {
1410
+ timer = null;
1111
1411
  }
1112
1412
 
1113
- // Always use immediate cleanup like Python
1114
1413
  [encoded.resolve, store.resolve] = this._encode_callback(
1115
1414
  "resolve",
1116
1415
  resolve,
1117
1416
  session_id,
1118
- true, // Always cleanup immediately
1417
+ clear_after_called,
1119
1418
  timer,
1120
1419
  local_workspace,
1121
1420
  `resolve (${description})`,
@@ -1124,7 +1423,7 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1124
1423
  "reject",
1125
1424
  reject,
1126
1425
  session_id,
1127
- true, // Always cleanup immediately
1426
+ clear_after_called,
1128
1427
  timer,
1129
1428
  local_workspace,
1130
1429
  `reject (${description})`,
@@ -1379,8 +1678,13 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1379
1678
  }
1380
1679
  })
1381
1680
  .catch(function (err) {
1382
- console.error("Failed to send message", err);
1383
- reject(err);
1681
+ const error_msg = `Failed to send the request when calling method (${target_id}:${method_id}), error: ${err}`;
1682
+ if (reject) {
1683
+ reject(new Error(error_msg));
1684
+ } else {
1685
+ // No reject callback available, log the error to prevent unhandled promise rejections
1686
+ console.warn("Unhandled RPC method call error:", error_msg);
1687
+ }
1384
1688
  if (timer) {
1385
1689
  timer.clear();
1386
1690
  }
@@ -1396,8 +1700,13 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1396
1700
  }
1397
1701
  })
1398
1702
  .catch(function (err) {
1399
- console.error("Failed to send message", err);
1400
- reject(err);
1703
+ const error_msg = `Failed to send the request when calling method (${target_id}:${method_id}), error: ${err}`;
1704
+ if (reject) {
1705
+ reject(new Error(error_msg));
1706
+ } else {
1707
+ // No reject callback available, log the error to prevent unhandled promise rejections
1708
+ console.warn("Unhandled RPC method call error:", error_msg);
1709
+ }
1401
1710
  if (timer) {
1402
1711
  timer.clear();
1403
1712
  }
@@ -1503,17 +1812,56 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1503
1812
  try {
1504
1813
  method = indexObject(this._object_store, data["method"]);
1505
1814
  } catch (e) {
1506
- // Simplified error handling like Python - just check if method exists
1507
- if (!this._object_store[data["method"].split(".")[0]]) {
1815
+ // Clean promise method detection - TYPE-BASED, not string-based
1816
+ if (this._is_promise_method_call(data["method"])) {
1508
1817
  console.debug(
1509
- `Method ${data["method"]} not available (session cleaned up), ignoring: ${method_name}`,
1818
+ `Promise method ${data["method"]} not available (detected by session type), ignoring: ${method_name}`,
1510
1819
  );
1511
1820
  return;
1512
1821
  }
1513
1822
 
1514
- throw new Error(
1823
+ // Check if this is a session-based method call that might have expired
1824
+ const method_parts = data["method"].split(".");
1825
+ if (method_parts.length > 1) {
1826
+ const session_id = method_parts[0];
1827
+ // Check if the session exists but the specific method doesn't
1828
+ if (session_id in this._object_store) {
1829
+ console.debug(
1830
+ `Session ${session_id} exists but method ${data["method"]} not found, likely expired callback: ${method_name}`,
1831
+ );
1832
+ // For expired callbacks, don't throw an exception, just log and return
1833
+ if (typeof reject === "function") {
1834
+ reject(new Error(`Method expired or not found: ${method_name}`));
1835
+ }
1836
+ return;
1837
+ } else {
1838
+ console.debug(
1839
+ `Session ${session_id} not found for method ${data["method"]}, likely cleaned up: ${method_name}`,
1840
+ );
1841
+ // For cleaned up sessions, just log and return without throwing
1842
+ if (typeof reject === "function") {
1843
+ reject(new Error(`Session not found: ${method_name}`));
1844
+ }
1845
+ return;
1846
+ }
1847
+ }
1848
+
1849
+ console.debug(
1850
+ `Failed to find method ${method_name} at ${this._client_id}`,
1851
+ );
1852
+ const error = new Error(
1515
1853
  `Method not found: ${method_name} at ${this._client_id}`,
1516
1854
  );
1855
+ if (typeof reject === "function") {
1856
+ reject(error);
1857
+ } else {
1858
+ // Log the error instead of throwing to prevent unhandled exceptions
1859
+ console.warn(
1860
+ "Method not found and no reject callback:",
1861
+ error.message,
1862
+ );
1863
+ }
1864
+ return;
1517
1865
  }
1518
1866
 
1519
1867
  (0,_utils_index_js__WEBPACK_IMPORTED_MODULE_0__.assert)(
@@ -5524,20 +5872,20 @@ class WebsocketRPCConnection {
5524
5872
  // Clean up timers when connection closes
5525
5873
  this._cleanup();
5526
5874
 
5527
- if ([1000, 1001].includes(event.code)) {
5528
- console.info(
5529
- `Websocket connection closed (code: ${event.code}): ${event.reason}`,
5530
- );
5531
- if (this._handle_disconnected) {
5532
- this._handle_disconnected(event.reason);
5875
+ // Even if it's a graceful closure (codes 1000, 1001), if it wasn't user-initiated,
5876
+ // we should attempt to reconnect (e.g., server restart, k8s upgrade)
5877
+ if (this._enable_reconnect) {
5878
+ if ([1000, 1001].includes(event.code)) {
5879
+ console.warn(
5880
+ `Websocket connection closed gracefully by server (code: ${event.code}): ${event.reason} - attempting reconnect`,
5881
+ );
5882
+ } else {
5883
+ console.warn(
5884
+ "Websocket connection closed unexpectedly (code: %s): %s",
5885
+ event.code,
5886
+ event.reason,
5887
+ );
5533
5888
  }
5534
- this._closed = true;
5535
- } else if (this._enable_reconnect) {
5536
- console.warn(
5537
- "Websocket connection closed unexpectedly (code: %s): %s",
5538
- event.code,
5539
- event.reason,
5540
- );
5541
5889
 
5542
5890
  let retry = 0;
5543
5891
  const baseDelay = 1000; // Start with 1 second