hypha-rpc 0.20.57 → 0.20.59

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.
@@ -941,8 +941,8 @@ class RPC extends _utils__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
941
941
  timer.clear();
942
942
  }
943
943
  if (clear_after_called && self._object_store[session_id]) {
944
- // console.log("Deleting session", session_id, "from", self._client_id);
945
- delete self._object_store[session_id];
944
+ // Simple cleanup - let the session manager handle the logic
945
+ self._cleanup_session_if_needed(session_id, name);
946
946
  }
947
947
  }
948
948
  };
@@ -950,6 +950,47 @@ class RPC extends _utils__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
950
950
  return [encoded, wrapped_callback];
951
951
  }
952
952
 
953
+ // Clean session management - all logic in one place
954
+ _cleanup_session_if_needed(session_id, callback_name) {
955
+ const store = this._get_session_store(session_id, false);
956
+ if (!store) return;
957
+
958
+ // Promise sessions: let the promise manager decide cleanup
959
+ if (store._promise_manager) {
960
+ if (store._promise_manager.should_cleanup_on_callback(callback_name)) {
961
+ store._promise_manager.settle();
962
+ delete this._object_store[session_id];
963
+ }
964
+ return;
965
+ }
966
+
967
+ // Regular sessions: cleanup immediately
968
+ delete this._object_store[session_id];
969
+ }
970
+
971
+ // Clean helper to identify promise method calls by session type
972
+ _is_promise_method_call(method_path) {
973
+ const session_id = method_path.split(".")[0];
974
+ const session = this._get_session_store(session_id, false);
975
+ return session && session._promise_manager;
976
+ }
977
+
978
+ // Clean Promise Manager - encapsulates all promise lifecycle logic
979
+ _create_promise_manager() {
980
+ return {
981
+ settled: false,
982
+ settle() {
983
+ this.settled = true;
984
+ },
985
+ is_settled() {
986
+ return this.settled;
987
+ },
988
+ should_cleanup_on_callback(callback_name) {
989
+ return callback_name === "resolve" || callback_name === "reject";
990
+ },
991
+ };
992
+ }
993
+
953
994
  async _encode_promise(
954
995
  resolve,
955
996
  reject,
@@ -960,12 +1001,17 @@ class RPC extends _utils__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
960
1001
  description,
961
1002
  ) {
962
1003
  let store = this._get_session_store(session_id, true);
963
- (0,_utils__WEBPACK_IMPORTED_MODULE_0__.assert)(
964
- store,
965
- `Failed to create session store ${session_id} due to invalid parent`,
966
- );
1004
+ if (!store) {
1005
+ console.warn(
1006
+ `Failed to create session store ${session_id}, session management may be impaired`,
1007
+ );
1008
+ store = {};
1009
+ }
967
1010
  let encoded = {};
968
1011
 
1012
+ // Clean promise lifecycle management
1013
+ store._promise_manager = this._create_promise_manager();
1014
+
969
1015
  if (timer && reject && this._method_timeout) {
970
1016
  [encoded.heartbeat, store.heartbeat] = this._encode_callback(
971
1017
  "heartbeat",
@@ -974,12 +1020,9 @@ class RPC extends _utils__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
974
1020
  false,
975
1021
  null,
976
1022
  local_workspace,
977
- // `heartbeat (${description})`,
978
1023
  );
979
1024
  store.timer = timer;
980
1025
  encoded.interval = this._method_timeout / 2;
981
- } else {
982
- timer = null;
983
1026
  }
984
1027
 
985
1028
  [encoded.resolve, store.resolve] = this._encode_callback(
@@ -1043,7 +1086,19 @@ class RPC extends _utils__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1043
1086
  }
1044
1087
 
1045
1088
  // Wait for all chunk uploads to finish
1046
- await Promise.all(tasks);
1089
+ try {
1090
+ await Promise.all(tasks);
1091
+ } catch (error) {
1092
+ // If any chunk fails, clean up the message cache
1093
+ try {
1094
+ await message_cache.remove(message_id);
1095
+ } catch (cleanupError) {
1096
+ console.error(
1097
+ `Failed to clean up message cache after error: ${cleanupError}`,
1098
+ );
1099
+ }
1100
+ throw error;
1101
+ }
1047
1102
  } else {
1048
1103
  // 3) Legacy version (sequential appends):
1049
1104
  await message_cache.create(message_id, !!session_id);
@@ -1181,13 +1236,24 @@ class RPC extends _utils__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1181
1236
  );
1182
1237
  // By default, hypha will clear the session after the method is called
1183
1238
  // However, if the args contains _rintf === true, we will not clear the session
1184
- let clear_after_called = true;
1185
- for (let arg of args) {
1186
- if (typeof arg === "object" && arg._rintf === true) {
1187
- clear_after_called = false;
1188
- break;
1239
+
1240
+ // Helper function to recursively check for _rintf objects
1241
+ function hasInterfaceObject(obj) {
1242
+ if (!obj || typeof obj !== "object") return false;
1243
+ if (obj._rintf === true) return true;
1244
+ if (Array.isArray(obj)) {
1245
+ return obj.some((item) => hasInterfaceObject(item));
1189
1246
  }
1247
+ if (obj.constructor === Object) {
1248
+ return Object.values(obj).some((value) =>
1249
+ hasInterfaceObject(value),
1250
+ );
1251
+ }
1252
+ return false;
1190
1253
  }
1254
+
1255
+ let clear_after_called = !hasInterfaceObject(args);
1256
+
1191
1257
  const promiseData = await self._encode_promise(
1192
1258
  resolve,
1193
1259
  reject,
@@ -1344,7 +1410,14 @@ class RPC extends _utils__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1344
1410
  try {
1345
1411
  method = indexObject(this._object_store, data["method"]);
1346
1412
  } catch (e) {
1347
- // console.debug("Failed to find method", method_name, this._client_id, e);
1413
+ // Clean promise method error handling
1414
+ if (this._is_promise_method_call(data["method"])) {
1415
+ console.debug(
1416
+ `Promise method ${data["method"]} not available (session settled or cleaned up), ignoring: ${method_name}`,
1417
+ );
1418
+ return;
1419
+ }
1420
+
1348
1421
  throw new Error(
1349
1422
  `Method not found: ${method_name} at ${this._client_id}`,
1350
1423
  );
@@ -1478,7 +1551,8 @@ class RPC extends _utils__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1478
1551
  const last_index = levels.length - 1;
1479
1552
  for (let level of levels.slice(0, last_index)) {
1480
1553
  if (!store[level]) {
1481
- return null;
1554
+ // Instead of returning null, create intermediate sessions as needed
1555
+ store[level] = {};
1482
1556
  }
1483
1557
  store = store[level];
1484
1558
  }
@@ -1692,6 +1766,9 @@ class RPC extends _utils__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1692
1766
  nj.NdArray &&
1693
1767
  aObject instanceof nj.NdArray
1694
1768
  ) {
1769
+ if (!aObject.selection || !aObject.selection.data) {
1770
+ throw new Error("Invalid NumJS array: missing selection or data");
1771
+ }
1695
1772
  const dtype = (0,_utils__WEBPACK_IMPORTED_MODULE_0__.typedArrayToDtype)(aObject.selection.data);
1696
1773
  bObject = {
1697
1774
  _rtype: "ndarray",
@@ -1784,10 +1861,7 @@ class RPC extends _utils__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1784
1861
  local_workspace,
1785
1862
  ),
1786
1863
  };
1787
- } else if (
1788
- aObject.constructor instanceof Object ||
1789
- Array.isArray(aObject)
1790
- ) {
1864
+ } else if (aObject.constructor === Object || Array.isArray(aObject)) {
1791
1865
  bObject = isarray ? [] : {};
1792
1866
  const keys = Object.keys(aObject);
1793
1867
  for (let k of keys) {
@@ -1861,23 +1935,18 @@ class RPC extends _utils__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1861
1935
 
1862
1936
  // Create an async generator proxy
1863
1937
  async function* asyncGeneratorProxy() {
1864
- try {
1865
- while (true) {
1866
- try {
1867
- const next_item = await gen_method();
1868
- // Check for StopIteration signal
1869
- if (next_item && next_item._rtype === "stop_iteration") {
1870
- break;
1871
- }
1872
- yield next_item;
1873
- } catch (error) {
1874
- console.error("Error in generator:", error);
1875
- throw error;
1938
+ while (true) {
1939
+ try {
1940
+ const next_item = await gen_method();
1941
+ // Check for StopIteration signal
1942
+ if (next_item && next_item._rtype === "stop_iteration") {
1943
+ break;
1876
1944
  }
1945
+ yield next_item;
1946
+ } catch (error) {
1947
+ console.error("Error in generator:", error);
1948
+ throw error;
1877
1949
  }
1878
- } catch (error) {
1879
- console.error("Error in generator:", error);
1880
- throw error;
1881
1950
  }
1882
1951
  }
1883
1952
  bObject = asyncGeneratorProxy();
@@ -2791,6 +2860,21 @@ async function _createOffer(params, server, config, onInit, context) {
2791
2860
  let answer = await pc.createAnswer();
2792
2861
  await pc.setLocalDescription(answer);
2793
2862
 
2863
+ // Wait for ICE candidates to be gathered (important for Firefox)
2864
+ await new Promise((resolveIce) => {
2865
+ if (pc.iceGatheringState === "complete") {
2866
+ resolveIce();
2867
+ } else {
2868
+ pc.addEventListener("icegatheringstatechange", () => {
2869
+ if (pc.iceGatheringState === "complete") {
2870
+ resolveIce();
2871
+ }
2872
+ });
2873
+ // Don't wait forever for ICE gathering
2874
+ setTimeout(resolveIce, 5000);
2875
+ }
2876
+ });
2877
+
2794
2878
  return {
2795
2879
  sdp: pc.localDescription.sdp,
2796
2880
  type: pc.localDescription.type,
@@ -2810,30 +2894,80 @@ async function getRTCService(server, service_id, config) {
2810
2894
  });
2811
2895
 
2812
2896
  return new Promise(async (resolve, reject) => {
2897
+ let resolved = false;
2898
+ const timeout = setTimeout(() => {
2899
+ if (!resolved) {
2900
+ resolved = true;
2901
+ pc.close();
2902
+ reject(new Error("WebRTC Connection timeout"));
2903
+ }
2904
+ }, 30000); // Increase timeout to 30 seconds
2905
+
2813
2906
  try {
2814
2907
  pc.addEventListener(
2815
2908
  "connectionstatechange",
2816
2909
  () => {
2910
+ console.log("WebRTC Connection state: ", pc.connectionState);
2817
2911
  if (pc.connectionState === "failed") {
2818
- pc.close();
2819
- reject(new Error("WebRTC Connection failed"));
2912
+ if (!resolved) {
2913
+ resolved = true;
2914
+ clearTimeout(timeout);
2915
+ pc.close();
2916
+ reject(new Error("WebRTC Connection failed"));
2917
+ }
2820
2918
  } else if (pc.connectionState === "closed") {
2821
- reject(new Error("WebRTC Connection closed"));
2822
- } else {
2823
- console.log("WebRTC Connection state: ", pc.connectionState);
2919
+ if (!resolved) {
2920
+ resolved = true;
2921
+ clearTimeout(timeout);
2922
+ reject(new Error("WebRTC Connection closed"));
2923
+ }
2924
+ } else if (pc.connectionState === "connected") {
2925
+ console.log("WebRTC Connection established successfully");
2824
2926
  }
2825
2927
  },
2826
2928
  false,
2827
2929
  );
2828
2930
 
2931
+ // Add ICE connection state change handler for better debugging
2932
+ pc.addEventListener("iceconnectionstatechange", () => {
2933
+ console.log("ICE Connection state: ", pc.iceConnectionState);
2934
+ if (pc.iceConnectionState === "failed") {
2935
+ if (!resolved) {
2936
+ resolved = true;
2937
+ clearTimeout(timeout);
2938
+ pc.close();
2939
+ reject(new Error("ICE Connection failed"));
2940
+ }
2941
+ }
2942
+ });
2943
+
2829
2944
  if (config.on_init) {
2830
2945
  await config.on_init(pc);
2831
2946
  delete config.on_init;
2832
2947
  }
2948
+
2833
2949
  let channel = pc.createDataChannel(config.peer_id, { ordered: true });
2834
2950
  channel.binaryType = "arraybuffer";
2951
+
2952
+ // Wait for ICE gathering to complete before creating offer
2835
2953
  const offer = await pc.createOffer();
2836
2954
  await pc.setLocalDescription(offer);
2955
+
2956
+ // Wait for ICE candidates to be gathered (important for Firefox)
2957
+ await new Promise((resolveIce) => {
2958
+ if (pc.iceGatheringState === "complete") {
2959
+ resolveIce();
2960
+ } else {
2961
+ pc.addEventListener("icegatheringstatechange", () => {
2962
+ if (pc.iceGatheringState === "complete") {
2963
+ resolveIce();
2964
+ }
2965
+ });
2966
+ // Don't wait forever for ICE gathering
2967
+ setTimeout(resolveIce, 5000);
2968
+ }
2969
+ });
2970
+
2837
2971
  const svc = await server.getService(service_id);
2838
2972
  const answer = await svc.offer({
2839
2973
  sdp: pc.localDescription.sdp,
@@ -2843,77 +2977,102 @@ async function getRTCService(server, service_id, config) {
2843
2977
  channel.onopen = () => {
2844
2978
  config.channel = channel;
2845
2979
  config.workspace = answer.workspace;
2846
- // Wait for the channel to be open before returning the rpc
2847
- // This is needed for safari to work
2980
+ // Increase timeout for Firefox compatibility
2848
2981
  setTimeout(async () => {
2849
- const rpc = await _setupRPC(config);
2850
- pc.rpc = rpc;
2851
- async function get_service(name, ...args) {
2852
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.assert)(
2853
- !name.includes(":"),
2854
- "WebRTC service name should not contain ':'",
2855
- );
2856
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.assert)(
2857
- !name.includes("/"),
2858
- "WebRTC service name should not contain '/'",
2859
- );
2860
- return await rpc.get_remote_service(
2861
- config.workspace + "/" + config.peer_id + ":" + name,
2862
- ...args,
2863
- );
2864
- }
2865
- async function disconnect() {
2866
- await rpc.disconnect();
2867
- pc.close();
2868
- }
2869
- pc.getService = (0,_utils_schema_js__WEBPACK_IMPORTED_MODULE_2__.schemaFunction)(get_service, {
2870
- name: "getService",
2871
- description: "Get a remote service via webrtc",
2872
- parameters: {
2873
- type: "object",
2874
- properties: {
2875
- service_id: {
2876
- type: "string",
2877
- description:
2878
- "Service ID. This should be a service id in the format: 'workspace/service_id', 'workspace/client_id:service_id' or 'workspace/client_id:service_id@app_id'",
2879
- },
2880
- config: {
2982
+ if (!resolved) {
2983
+ try {
2984
+ const rpc = await _setupRPC(config);
2985
+ pc.rpc = rpc;
2986
+ async function get_service(name, ...args) {
2987
+ (0,_utils__WEBPACK_IMPORTED_MODULE_1__.assert)(
2988
+ !name.includes(":"),
2989
+ "WebRTC service name should not contain ':'",
2990
+ );
2991
+ (0,_utils__WEBPACK_IMPORTED_MODULE_1__.assert)(
2992
+ !name.includes("/"),
2993
+ "WebRTC service name should not contain '/'",
2994
+ );
2995
+ return await rpc.get_remote_service(
2996
+ config.workspace + "/" + config.peer_id + ":" + name,
2997
+ ...args,
2998
+ );
2999
+ }
3000
+ async function disconnect() {
3001
+ await rpc.disconnect();
3002
+ pc.close();
3003
+ }
3004
+ pc.getService = (0,_utils_schema_js__WEBPACK_IMPORTED_MODULE_2__.schemaFunction)(get_service, {
3005
+ name: "getService",
3006
+ description: "Get a remote service via webrtc",
3007
+ parameters: {
2881
3008
  type: "object",
2882
- description: "Options for the service",
3009
+ properties: {
3010
+ service_id: {
3011
+ type: "string",
3012
+ description:
3013
+ "Service ID. This should be a service id in the format: 'workspace/service_id', 'workspace/client_id:service_id' or 'workspace/client_id:service_id@app_id'",
3014
+ },
3015
+ config: {
3016
+ type: "object",
3017
+ description: "Options for the service",
3018
+ },
3019
+ },
3020
+ required: ["id"],
2883
3021
  },
2884
- },
2885
- required: ["id"],
2886
- },
2887
- });
2888
- pc.disconnect = (0,_utils_schema_js__WEBPACK_IMPORTED_MODULE_2__.schemaFunction)(disconnect, {
2889
- name: "disconnect",
2890
- description: "Disconnect from the webrtc connection via webrtc",
2891
- parameters: { type: "object", properties: {} },
2892
- });
2893
- pc.registerCodec = (0,_utils_schema_js__WEBPACK_IMPORTED_MODULE_2__.schemaFunction)(rpc.register_codec, {
2894
- name: "registerCodec",
2895
- description: "Register a codec for the webrtc connection",
2896
- parameters: {
2897
- type: "object",
2898
- properties: {
2899
- codec: {
3022
+ });
3023
+ pc.disconnect = (0,_utils_schema_js__WEBPACK_IMPORTED_MODULE_2__.schemaFunction)(disconnect, {
3024
+ name: "disconnect",
3025
+ description: "Disconnect from the webrtc connection via webrtc",
3026
+ parameters: { type: "object", properties: {} },
3027
+ });
3028
+ pc.registerCodec = (0,_utils_schema_js__WEBPACK_IMPORTED_MODULE_2__.schemaFunction)(rpc.register_codec, {
3029
+ name: "registerCodec",
3030
+ description: "Register a codec for the webrtc connection",
3031
+ parameters: {
2900
3032
  type: "object",
2901
- description: "Codec to register",
2902
3033
  properties: {
2903
- name: { type: "string" },
2904
- type: {},
2905
- encoder: { type: "function" },
2906
- decoder: { type: "function" },
3034
+ codec: {
3035
+ type: "object",
3036
+ description: "Codec to register",
3037
+ properties: {
3038
+ name: { type: "string" },
3039
+ type: {},
3040
+ encoder: { type: "function" },
3041
+ decoder: { type: "function" },
3042
+ },
3043
+ },
2907
3044
  },
2908
3045
  },
2909
- },
2910
- },
2911
- });
2912
- resolve(pc);
2913
- }, 500);
3046
+ });
3047
+ resolved = true;
3048
+ clearTimeout(timeout);
3049
+ resolve(pc);
3050
+ } catch (e) {
3051
+ if (!resolved) {
3052
+ resolved = true;
3053
+ clearTimeout(timeout);
3054
+ reject(e);
3055
+ }
3056
+ }
3057
+ }
3058
+ }, 1000); // Increase timeout to 1 second for Firefox
2914
3059
  };
2915
3060
 
2916
- channel.onclose = () => reject(new Error("Data channel closed"));
3061
+ channel.onclose = () => {
3062
+ if (!resolved) {
3063
+ resolved = true;
3064
+ clearTimeout(timeout);
3065
+ reject(new Error("Data channel closed"));
3066
+ }
3067
+ };
3068
+
3069
+ channel.onerror = (error) => {
3070
+ if (!resolved) {
3071
+ resolved = true;
3072
+ clearTimeout(timeout);
3073
+ reject(new Error(`Data channel error: ${error}`));
3074
+ }
3075
+ };
2917
3076
 
2918
3077
  await pc.setRemoteDescription(
2919
3078
  new RTCSessionDescription({
@@ -2922,7 +3081,11 @@ async function getRTCService(server, service_id, config) {
2922
3081
  }),
2923
3082
  );
2924
3083
  } catch (e) {
2925
- reject(e);
3084
+ if (!resolved) {
3085
+ resolved = true;
3086
+ clearTimeout(timeout);
3087
+ reject(e);
3088
+ }
2926
3089
  }
2927
3090
  });
2928
3091
  }