hypha-rpc 0.21.34 → 0.21.36
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/README.md +72 -1
- package/coverage/html/index.html +1 -1
- package/dist/hypha-rpc-websocket.js +120 -20
- 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 +120 -20
- package/dist/hypha-rpc-websocket.mjs.map +1 -1
- package/package.json +1 -1
- package/src/rpc.js +57 -2
- package/src/websocket-client.js +13 -0
- package/hypha-rpc-0.21.15.tgz +0 -0
package/README.md
CHANGED
|
@@ -24,4 +24,75 @@ hyphaWebsocketClient.connectToServer({
|
|
|
24
24
|
}
|
|
25
25
|
)
|
|
26
26
|
})
|
|
27
|
-
```
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Keyword Arguments (kwargs)
|
|
30
|
+
|
|
31
|
+
Hypha RPC supports Python-style keyword arguments when calling JavaScript services. This works seamlessly across Python→JS and JS→JS calls.
|
|
32
|
+
|
|
33
|
+
#### Calling from Python to JavaScript
|
|
34
|
+
|
|
35
|
+
When a Python client calls a JS service using keyword arguments, they are automatically unpacked into the matching positional parameters:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
# Python caller
|
|
39
|
+
svc = await server.get_service("my-js-service")
|
|
40
|
+
result = await svc.greet(name="Alice", greeting="Hello")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
// JavaScript service — params are matched by name
|
|
45
|
+
await api.registerService({
|
|
46
|
+
id: "my-js-service",
|
|
47
|
+
greet(name, greeting) {
|
|
48
|
+
return `${greeting}, ${name}!`; // "Hello, Alice!"
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### Calling from JavaScript to JavaScript
|
|
54
|
+
|
|
55
|
+
Use the `_rkwargs: true` flag to send keyword arguments from JS:
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
const svc = await api.getService("my-js-service");
|
|
59
|
+
|
|
60
|
+
// Positional (traditional)
|
|
61
|
+
await svc.greet("Alice", "Hello");
|
|
62
|
+
|
|
63
|
+
// Keyword arguments — params matched by name, order doesn't matter
|
|
64
|
+
await svc.greet({ greeting: "Hello", name: "Alice", _rkwargs: true });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### With `require_context`
|
|
68
|
+
|
|
69
|
+
When a service uses `require_context: true`, the `context` parameter is automatically injected by the server and cannot be overridden via kwargs:
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
await api.registerService({
|
|
73
|
+
id: "secure-svc",
|
|
74
|
+
config: { require_context: true },
|
|
75
|
+
greet(name, greeting, context) {
|
|
76
|
+
console.log("Called by:", context.user);
|
|
77
|
+
return `${greeting}, ${name}!`;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Caller — context is injected automatically, not passed by the caller
|
|
82
|
+
await svc.greet({ name: "Alice", greeting: "Hello", _rkwargs: true });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### With `kwargs_expansion`
|
|
86
|
+
|
|
87
|
+
For convenience when calling server APIs, connect with `kwargs_expansion: true` to automatically convert the last object argument to kwargs:
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
const api = await connectToServer({
|
|
91
|
+
server_url: "https://ai.imjoy.io",
|
|
92
|
+
kwargs_expansion: true,
|
|
93
|
+
});
|
|
94
|
+
// The last object arg is automatically sent as kwargs
|
|
95
|
+
const token = await api.generateToken({ config: { workspace: "my-ws" } });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> **Note:** Kwargs unpacking relies on parsing function parameter names from source code. This works with regular functions, arrow functions, and async functions, but will not work with minified/bundled code where parameter names are mangled.
|
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-
|
|
89
|
+
at 2026-03-11T17:52:10.251Z
|
|
90
90
|
</div>
|
|
91
91
|
<script src="prettify.js"></script>
|
|
92
92
|
<script>
|
|
@@ -2979,15 +2979,47 @@ class HTTPStreamingRPCConnection {
|
|
|
2979
2979
|
|
|
2980
2980
|
/**
|
|
2981
2981
|
* Process msgpack stream with 4-byte length prefix.
|
|
2982
|
+
*
|
|
2983
|
+
* Includes a read timeout to detect dead connections. The Hypha server
|
|
2984
|
+
* sends pings every ~30s, so if no data arrives within READ_TIMEOUT_MS,
|
|
2985
|
+
* the connection is considered dead and the stream is cancelled to
|
|
2986
|
+
* trigger reconnection.
|
|
2982
2987
|
*/
|
|
2983
2988
|
async _processMsgpackStream(response) {
|
|
2984
2989
|
const reader = response.body.getReader();
|
|
2990
|
+
// Expose the reader so external code (e.g. daemon heartbeat) can cancel it
|
|
2991
|
+
this._reader = reader;
|
|
2985
2992
|
// Growing buffer to avoid O(n^2) re-allocation on every chunk
|
|
2986
2993
|
let buffer = new Uint8Array(4096);
|
|
2987
2994
|
let bufferLen = 0;
|
|
2988
2995
|
|
|
2996
|
+
// Read timeout: server sends pings every ~30s, so 120s = 4 missed pings = dead
|
|
2997
|
+
const READ_TIMEOUT_MS = 120_000;
|
|
2998
|
+
|
|
2989
2999
|
while (!this._closed) {
|
|
2990
|
-
|
|
3000
|
+
let readResult;
|
|
3001
|
+
let timeoutId;
|
|
3002
|
+
try {
|
|
3003
|
+
readResult = await Promise.race([
|
|
3004
|
+
reader.read(),
|
|
3005
|
+
new Promise((_, reject) => {
|
|
3006
|
+
timeoutId = setTimeout(
|
|
3007
|
+
() => reject(new Error("Stream read timeout (no data for 120s)")),
|
|
3008
|
+
READ_TIMEOUT_MS,
|
|
3009
|
+
);
|
|
3010
|
+
}),
|
|
3011
|
+
]);
|
|
3012
|
+
clearTimeout(timeoutId);
|
|
3013
|
+
} catch (error) {
|
|
3014
|
+
clearTimeout(timeoutId);
|
|
3015
|
+
console.warn(`Stream read error: ${error.message}`);
|
|
3016
|
+
try {
|
|
3017
|
+
reader.cancel();
|
|
3018
|
+
} catch {}
|
|
3019
|
+
break;
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
const { done, value } = readResult;
|
|
2991
3023
|
|
|
2992
3024
|
if (done) break;
|
|
2993
3025
|
|
|
@@ -3067,6 +3099,8 @@ class HTTPStreamingRPCConnection {
|
|
|
3067
3099
|
bufferLen = remaining;
|
|
3068
3100
|
}
|
|
3069
3101
|
}
|
|
3102
|
+
|
|
3103
|
+
this._reader = null;
|
|
3070
3104
|
}
|
|
3071
3105
|
|
|
3072
3106
|
/**
|
|
@@ -3405,7 +3439,8 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
3405
3439
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
3406
3440
|
/* harmony export */ API_VERSION: () => (/* binding */ API_VERSION),
|
|
3407
3441
|
/* harmony export */ RPC: () => (/* binding */ RPC),
|
|
3408
|
-
/* harmony export */ _applyEncryptionKeyToService: () => (/* binding */ _applyEncryptionKeyToService)
|
|
3442
|
+
/* harmony export */ _applyEncryptionKeyToService: () => (/* binding */ _applyEncryptionKeyToService),
|
|
3443
|
+
/* harmony export */ getParamNames: () => (/* binding */ getParamNames)
|
|
3409
3444
|
/* harmony export */ });
|
|
3410
3445
|
/* harmony import */ var _utils_index_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils/index.js */ "./src/utils/index.js");
|
|
3411
3446
|
/* harmony import */ var _utils_schema_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./utils/schema.js */ "./src/utils/schema.js");
|
|
@@ -3422,6 +3457,35 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
3422
3457
|
|
|
3423
3458
|
|
|
3424
3459
|
|
|
3460
|
+
/**
|
|
3461
|
+
* Extract parameter names from a function's source text.
|
|
3462
|
+
* Works with regular functions, arrow functions, async functions,
|
|
3463
|
+
* destructured params, default values, and rest params.
|
|
3464
|
+
* Note: will not work with minified/bundled code where param names are mangled.
|
|
3465
|
+
* @param {Function} fn - The function to inspect.
|
|
3466
|
+
* @returns {string[]} Array of parameter names.
|
|
3467
|
+
*/
|
|
3468
|
+
function getParamNames(fn) {
|
|
3469
|
+
const src = fn.toString();
|
|
3470
|
+
// Match: function(a, b), async function name(a, b), (a, b) =>, async (a, b) =>
|
|
3471
|
+
// Also handles single param arrow without parens: a =>
|
|
3472
|
+
const match = src.match(
|
|
3473
|
+
/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)|^(?:async\s+)?(\w+)\s*=>/,
|
|
3474
|
+
);
|
|
3475
|
+
if (!match) return [];
|
|
3476
|
+
const paramStr = match[1] !== undefined ? match[1] : match[2];
|
|
3477
|
+
if (!paramStr || !paramStr.trim()) return [];
|
|
3478
|
+
return paramStr
|
|
3479
|
+
.split(",")
|
|
3480
|
+
.map((p) =>
|
|
3481
|
+
p
|
|
3482
|
+
.trim()
|
|
3483
|
+
.replace(/\s*=.*$/, "") // strip default values
|
|
3484
|
+
.replace(/^\.\.\.\s*/, ""), // strip rest operator
|
|
3485
|
+
)
|
|
3486
|
+
.filter(Boolean);
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3425
3489
|
/**
|
|
3426
3490
|
* Apply an out-of-band encryption public key to all remote methods in a service.
|
|
3427
3491
|
* @param {Object} svc - The decoded service object.
|
|
@@ -4002,12 +4066,10 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4002
4066
|
|
|
4003
4067
|
try {
|
|
4004
4068
|
// Get fresh manager service (one RPC roundtrip, ~50-100ms)
|
|
4005
|
-
const _t0 = Date.now();
|
|
4006
4069
|
const manager = await this.get_manager_service({
|
|
4007
4070
|
timeout: 20,
|
|
4008
4071
|
case_conversion: "camel",
|
|
4009
4072
|
});
|
|
4010
|
-
console.error(`[DEBUG] get_manager_service took ${Date.now() - _t0}ms`);
|
|
4011
4073
|
|
|
4012
4074
|
// Fire manager_refreshed IMMEDIATELY — before service re-registration.
|
|
4013
4075
|
// This allows connectToServer's wm proxy to be updated as soon as
|
|
@@ -4015,7 +4077,7 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4015
4077
|
this._fire("manager_refreshed", { manager });
|
|
4016
4078
|
|
|
4017
4079
|
const services = Object.values(this._services);
|
|
4018
|
-
|
|
4080
|
+
let servicesCount = services.length;
|
|
4019
4081
|
let registeredCount = 0;
|
|
4020
4082
|
const failedServices = [];
|
|
4021
4083
|
|
|
@@ -4032,14 +4094,12 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4032
4094
|
continue;
|
|
4033
4095
|
}
|
|
4034
4096
|
try {
|
|
4035
|
-
const _t1 = Date.now();
|
|
4036
4097
|
const serviceInfo = this._extract_service_info(service);
|
|
4037
4098
|
await withTimeout(
|
|
4038
4099
|
manager.registerService(serviceInfo),
|
|
4039
4100
|
serviceRegistrationTimeout,
|
|
4040
4101
|
`Timeout registering service ${service.id || "unknown"}`,
|
|
4041
4102
|
);
|
|
4042
|
-
console.error(`[DEBUG] registerService(${service.id}) took ${Date.now() - _t1}ms`);
|
|
4043
4103
|
registeredCount++;
|
|
4044
4104
|
} catch (serviceError) {
|
|
4045
4105
|
failedServices.push(service.id || "unknown");
|
|
@@ -4075,6 +4135,11 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4075
4135
|
failed: failedServices,
|
|
4076
4136
|
});
|
|
4077
4137
|
|
|
4138
|
+
// Track whether all services were registered so the
|
|
4139
|
+
// reconnection loop can detect partial failures and retry.
|
|
4140
|
+
this._connection._services_registered_ok =
|
|
4141
|
+
failedServices.length === 0;
|
|
4142
|
+
|
|
4078
4143
|
// Subscribe to client_disconnected events if the manager supports it
|
|
4079
4144
|
try {
|
|
4080
4145
|
if (
|
|
@@ -4167,6 +4232,9 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4167
4232
|
error: managerError.toString(),
|
|
4168
4233
|
total_services: Object.keys(this._services).length,
|
|
4169
4234
|
});
|
|
4235
|
+
// Mark registration as failed so the reconnection loop
|
|
4236
|
+
// can detect and retry
|
|
4237
|
+
this._connection._services_registered_ok = false;
|
|
4170
4238
|
}
|
|
4171
4239
|
} else {
|
|
4172
4240
|
// console.debug("Connection established", connectionInfo);
|
|
@@ -4184,10 +4252,11 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4184
4252
|
// This ensures no remote function call hangs forever when the connection drops
|
|
4185
4253
|
if (typeof connection.on_disconnected === "function") {
|
|
4186
4254
|
connection.on_disconnected((reason) => {
|
|
4187
|
-
// If reconnection is enabled
|
|
4188
|
-
//
|
|
4189
|
-
//
|
|
4190
|
-
|
|
4255
|
+
// If reconnection is enabled AND the connection is not permanently closed,
|
|
4256
|
+
// don't reject pending calls immediately — they may succeed after reconnect.
|
|
4257
|
+
// When _closed is true (max retries exhausted or server refused reconnect),
|
|
4258
|
+
// we must reject immediately so callers are not left hanging forever.
|
|
4259
|
+
if (connection._enable_reconnect && !connection._closed) {
|
|
4191
4260
|
this._logger.info(
|
|
4192
4261
|
`Connection lost (${reason}), reconnection enabled - pending calls will be handled by timeout`,
|
|
4193
4262
|
);
|
|
@@ -4541,7 +4610,15 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
4541
4610
|
delete this._rintfCallerIndex[clientId];
|
|
4542
4611
|
let cleaned = 0;
|
|
4543
4612
|
for (const sid of serviceIds) {
|
|
4544
|
-
|
|
4613
|
+
const svc = this._services[sid];
|
|
4614
|
+
if (svc) {
|
|
4615
|
+
if (typeof svc._dispose === "function") {
|
|
4616
|
+
try {
|
|
4617
|
+
svc._dispose();
|
|
4618
|
+
} catch (e) {
|
|
4619
|
+
/* ignore errors from dispose handlers */
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4545
4622
|
delete this._services[sid];
|
|
4546
4623
|
cleaned++;
|
|
4547
4624
|
}
|
|
@@ -6357,6 +6434,24 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
|
|
|
6357
6434
|
} else {
|
|
6358
6435
|
args = [];
|
|
6359
6436
|
}
|
|
6437
|
+
|
|
6438
|
+
// Unpack kwargs into positional arguments when with_kwargs is set.
|
|
6439
|
+
// This mirrors Python's _handle_method which pops the last arg as
|
|
6440
|
+
// a kwargs dict and passes it via **kwargs. Since JS doesn't have
|
|
6441
|
+
// **kwargs, we map the dict keys to the function's parameter names.
|
|
6442
|
+
if (data.with_kwargs && args.length > 0) {
|
|
6443
|
+
const kwargs = args.pop();
|
|
6444
|
+
if (typeof kwargs === "object" && kwargs !== null) {
|
|
6445
|
+
const paramNames = getParamNames(method);
|
|
6446
|
+
// Filter out 'context' — it's handled separately by require_context
|
|
6447
|
+
const mappableParams = paramNames.filter((n) => n !== "context");
|
|
6448
|
+
args = mappableParams.map((name) => kwargs[name]);
|
|
6449
|
+
} else {
|
|
6450
|
+
// Not a dict — push it back (shouldn't happen, but be safe)
|
|
6451
|
+
args.push(kwargs);
|
|
6452
|
+
}
|
|
6453
|
+
}
|
|
6454
|
+
|
|
6360
6455
|
if (
|
|
6361
6456
|
this._method_annotations.has(method) &&
|
|
6362
6457
|
this._method_annotations.get(method).require_context
|
|
@@ -10713,6 +10808,19 @@ class WebsocketRPCConnection {
|
|
|
10713
10808
|
throw new Error("Connection lost during reconnection settle");
|
|
10714
10809
|
}
|
|
10715
10810
|
|
|
10811
|
+
// Check if service re-registration succeeded.
|
|
10812
|
+
// The on_connected callback sets this flag; if any
|
|
10813
|
+
// services failed to register, retry instead of
|
|
10814
|
+
// declaring success with missing services.
|
|
10815
|
+
if (this._services_registered_ok === false) {
|
|
10816
|
+
this._logger.warn(
|
|
10817
|
+
"Service re-registration failed, retrying...",
|
|
10818
|
+
);
|
|
10819
|
+
throw new Error(
|
|
10820
|
+
"Service re-registration failed after reconnection",
|
|
10821
|
+
);
|
|
10822
|
+
}
|
|
10823
|
+
|
|
10716
10824
|
this._logger.warn(
|
|
10717
10825
|
`Successfully reconnected to server ${this._server_url} (services re-registered)`,
|
|
10718
10826
|
);
|
|
@@ -11059,9 +11167,7 @@ async function connectToServer(config) {
|
|
|
11059
11167
|
config.ping_interval,
|
|
11060
11168
|
config.logger,
|
|
11061
11169
|
);
|
|
11062
|
-
const _tOpen = Date.now();
|
|
11063
11170
|
const connection_info = await connection.open();
|
|
11064
|
-
console.error(`[DEBUG] connection.open() took ${Date.now() - _tOpen}ms`);
|
|
11065
11171
|
(0,_utils_index_js__WEBPACK_IMPORTED_MODULE_2__.assert)(
|
|
11066
11172
|
connection_info,
|
|
11067
11173
|
"Failed to connect to the server, no connection info obtained. This issue is most likely due to an outdated Hypha server version. Please use `imjoy-rpc` for compatibility, or upgrade the Hypha server to the latest version.",
|
|
@@ -11095,7 +11201,6 @@ async function connectToServer(config) {
|
|
|
11095
11201
|
}
|
|
11096
11202
|
|
|
11097
11203
|
const workspace = connection_info.workspace;
|
|
11098
|
-
const _tRpc = Date.now();
|
|
11099
11204
|
const rpc = new _rpc_js__WEBPACK_IMPORTED_MODULE_0__.RPC(connection, {
|
|
11100
11205
|
client_id: clientId,
|
|
11101
11206
|
workspace,
|
|
@@ -11112,17 +11217,12 @@ async function connectToServer(config) {
|
|
|
11112
11217
|
encryption_private_key: config.encryption_private_key || null,
|
|
11113
11218
|
encryption_public_key: config.encryption_public_key || null,
|
|
11114
11219
|
});
|
|
11115
|
-
console.error(`[DEBUG] RPC constructor took ${Date.now() - _tRpc}ms`);
|
|
11116
|
-
const _tWait = Date.now();
|
|
11117
11220
|
await rpc.waitFor("services_registered", config.method_timeout || 120);
|
|
11118
|
-
console.error(`[DEBUG] waitFor('services_registered') took ${Date.now() - _tWait}ms`);
|
|
11119
|
-
const _tWm = Date.now();
|
|
11120
11221
|
const wm = await rpc.get_manager_service({
|
|
11121
11222
|
timeout: config.method_timeout,
|
|
11122
11223
|
case_conversion: "camel",
|
|
11123
11224
|
kwargs_expansion: config.kwargs_expansion || false,
|
|
11124
11225
|
});
|
|
11125
|
-
console.error(`[DEBUG] get_manager_service (connectToServer) took ${Date.now() - _tWm}ms`);
|
|
11126
11226
|
wm.rpc = rpc;
|
|
11127
11227
|
|
|
11128
11228
|
// Auto-refresh workspace manager proxy after reconnection.
|