hypha-rpc 0.20.93 → 0.20.95

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.
@@ -30,13 +30,12 @@ __webpack_require__.r(__webpack_exports__);
30
30
  /* harmony import */ var _utils_index_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./utils/index.js */ "./src/utils/index.js");
31
31
  /* harmony import */ var _utils_schema_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./utils/schema.js */ "./src/utils/schema.js");
32
32
  /* harmony import */ var _msgpack_msgpack__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @msgpack/msgpack */ "./node_modules/@msgpack/msgpack/dist.es5+esm/decode.mjs");
33
- /* harmony import */ var _msgpack_msgpack__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @msgpack/msgpack */ "./node_modules/@msgpack/msgpack/dist.es5+esm/encode.mjs");
34
33
  /**
35
34
  * HTTP Streaming RPC Client for Hypha.
36
35
  *
37
36
  * This module provides HTTP-based RPC transport as an alternative to WebSocket.
38
37
  * It uses:
39
- * - HTTP GET with streaming (NDJSON/msgpack) for server-to-client messages
38
+ * - HTTP GET with streaming (msgpack) for server-to-client messages
40
39
  * - HTTP POST for client-to-server messages
41
40
  *
42
41
  * This is more resilient to network issues than WebSocket because:
@@ -97,9 +96,7 @@ const MAX_RETRY = 1000000;
97
96
  * HTTP Streaming RPC Connection.
98
97
  *
99
98
  * Uses HTTP GET with streaming for receiving messages and HTTP POST for sending messages.
100
- * Supports two formats:
101
- * - NDJSON (default): JSON lines for text-based messages
102
- * - msgpack: Binary format with length-prefixed frames for binary data support
99
+ * Uses msgpack binary format with length-prefixed frames for efficient binary data support.
103
100
  */
104
101
  class HTTPStreamingRPCConnection {
105
102
  /**
@@ -112,7 +109,6 @@ class HTTPStreamingRPCConnection {
112
109
  * @param {string} reconnection_token - Token for reconnection (optional)
113
110
  * @param {number} timeout - Request timeout in seconds (default: 60)
114
111
  * @param {number} token_refresh_interval - Interval for token refresh (default: 2 hours)
115
- * @param {string} format - Stream format - "json" (NDJSON) or "msgpack" (default: "json")
116
112
  */
117
113
  constructor(
118
114
  server_url,
@@ -122,7 +118,6 @@ class HTTPStreamingRPCConnection {
122
118
  reconnection_token = null,
123
119
  timeout = 60,
124
120
  token_refresh_interval = 2 * 60 * 60,
125
- format = "json",
126
121
  ) {
127
122
  (0,_utils_index_js__WEBPACK_IMPORTED_MODULE_1__.assert)(server_url && client_id, "server_url and client_id are required");
128
123
  this._server_url = server_url.replace(/\/$/, "");
@@ -132,7 +127,6 @@ class HTTPStreamingRPCConnection {
132
127
  this._reconnection_token = reconnection_token;
133
128
  this._timeout = timeout;
134
129
  this._token_refresh_interval = token_refresh_interval;
135
- this._format = format;
136
130
 
137
131
  this._handle_message = null;
138
132
  this._handle_disconnected = null;
@@ -143,8 +137,7 @@ class HTTPStreamingRPCConnection {
143
137
  this.connection_info = null;
144
138
  this.manager_id = null;
145
139
 
146
- this._stream_reader = null;
147
- this._stream_controller = null;
140
+ this._abort_controller = null;
148
141
  }
149
142
 
150
143
  /**
@@ -172,7 +165,7 @@ class HTTPStreamingRPCConnection {
172
165
  /**
173
166
  * Get HTTP headers with authentication.
174
167
  *
175
- * @param {boolean} for_stream - If true, set Accept header based on format preference
168
+ * @param {boolean} for_stream - If true, set Accept header for msgpack stream
176
169
  * @returns {Object} Headers object
177
170
  */
178
171
  _get_headers(for_stream = false) {
@@ -180,11 +173,7 @@ class HTTPStreamingRPCConnection {
180
173
  "Content-Type": "application/msgpack",
181
174
  };
182
175
  if (for_stream) {
183
- if (this._format === "msgpack") {
184
- headers["Accept"] = "application/x-msgpack-stream";
185
- } else {
186
- headers["Accept"] = "application/x-ndjson";
187
- }
176
+ headers["Accept"] = "application/x-msgpack-stream";
188
177
  }
189
178
  if (this._token) {
190
179
  headers["Authorization"] = `Bearer ${this._token}`;
@@ -196,16 +185,11 @@ class HTTPStreamingRPCConnection {
196
185
  * Open the streaming connection.
197
186
  */
198
187
  async open() {
199
- console.info(
200
- `Opening HTTP streaming connection to ${this._server_url} (format=${this._format})`,
201
- );
188
+ console.info(`Opening HTTP streaming connection to ${this._server_url}`);
202
189
 
203
- // Build stream URL
204
- const workspace = this._workspace || "public";
205
- let stream_url = `${this._server_url}/${workspace}/rpc?client_id=${this._client_id}`;
206
- if (this._format === "msgpack") {
207
- stream_url += "&format=msgpack";
208
- }
190
+ // Build stream URL - workspace is part of path, default to "public" for anonymous
191
+ const ws = this._workspace || "public";
192
+ const stream_url = `${this._server_url}/${ws}/rpc?client_id=${this._client_id}`;
209
193
 
210
194
  // Start streaming in background
211
195
  this._startStreamLoop(stream_url);
@@ -256,11 +240,11 @@ class HTTPStreamingRPCConnection {
256
240
  try {
257
241
  // OPTIMIZATION: Browser fetch automatically streams responses
258
242
  // and negotiates HTTP/2 when available for better performance
243
+ this._abort_controller = new AbortController();
259
244
  const response = await fetch(url, {
260
245
  method: "GET",
261
246
  headers: this._get_headers(true),
262
- // keepalive flag for connection reuse (important for reconnections)
263
- keepalive: true,
247
+ signal: this._abort_controller.signal,
264
248
  });
265
249
 
266
250
  if (!response.ok) {
@@ -272,13 +256,8 @@ class HTTPStreamingRPCConnection {
272
256
 
273
257
  retry = 0; // Reset retry counter on successful connection
274
258
 
275
- if (this._format === "msgpack") {
276
- // Binary msgpack stream with 4-byte length prefix
277
- await this._processMsgpackStream(response);
278
- } else {
279
- // NDJSON stream (line-based)
280
- await this._processNdjsonStream(response);
281
- }
259
+ // Process binary msgpack stream with 4-byte length prefix
260
+ await this._processMsgpackStream(response);
282
261
  } catch (error) {
283
262
  if (this._closed) break;
284
263
  console.error(`Connection error: ${error.message}`);
@@ -305,36 +284,6 @@ class HTTPStreamingRPCConnection {
305
284
  }
306
285
  }
307
286
 
308
- /**
309
- * Process NDJSON (line-based JSON) stream.
310
- */
311
- async _processNdjsonStream(response) {
312
- const reader = response.body.getReader();
313
- const decoder = new TextDecoder();
314
- let buffer = "";
315
-
316
- while (!this._closed) {
317
- const { done, value } = await reader.read();
318
-
319
- if (done) break;
320
-
321
- buffer += decoder.decode(value, { stream: true });
322
- const lines = buffer.split("\n");
323
- buffer = lines.pop() || "";
324
-
325
- for (const line of lines) {
326
- if (!line.trim()) continue;
327
-
328
- try {
329
- const message = JSON.parse(line);
330
- await this._handleStreamMessage(message);
331
- } catch (error) {
332
- console.warn(`Failed to parse JSON message: ${error.message}`);
333
- }
334
- }
335
- }
336
- }
337
-
338
287
  /**
339
288
  * Check if frame data is a control message and decode it.
340
289
  *
@@ -445,40 +394,6 @@ class HTTPStreamingRPCConnection {
445
394
  }
446
395
  }
447
396
 
448
- /**
449
- * Handle a decoded stream message.
450
- */
451
- async _handleStreamMessage(message) {
452
- // Handle connection info
453
- if (message.type === "connection_info") {
454
- this.connection_info = message;
455
- return;
456
- }
457
-
458
- // Handle ping (keep-alive)
459
- if (message.type === "ping") {
460
- return;
461
- }
462
-
463
- // Handle reconnection token refresh
464
- if (message.type === "reconnection_token") {
465
- this._reconnection_token = message.reconnection_token;
466
- return;
467
- }
468
-
469
- // Handle errors
470
- if (message.type === "error") {
471
- console.error(`Server error: ${message.message}`);
472
- return;
473
- }
474
-
475
- // Pass to message handler (convert to msgpack for RPC)
476
- if (this._handle_message) {
477
- const data = (0,_msgpack_msgpack__WEBPACK_IMPORTED_MODULE_4__.encode)(message);
478
- await this._handle_message(data);
479
- }
480
- }
481
-
482
397
  /**
483
398
  * Send a message to the server via HTTP POST.
484
399
  *
@@ -493,20 +408,21 @@ class HTTPStreamingRPCConnection {
493
408
  throw new Error("Connection is closed");
494
409
  }
495
410
 
496
- // Build POST URL - use the connected workspace
497
- const workspace = this._workspace || "public";
498
- const post_url = `${this._server_url}/${workspace}/rpc?client_id=${this._client_id}`;
411
+ // Build POST URL - workspace is part of path (must be set after connection)
412
+ const ws = this._workspace || "public";
413
+ let post_url = `${this._server_url}/${ws}/rpc?client_id=${this._client_id}`;
499
414
 
500
415
  // Ensure data is Uint8Array
501
416
  const body = data instanceof Uint8Array ? data : new Uint8Array(data);
502
417
 
503
- // OPTIMIZATION: keepalive flag hints to browser to reuse connections
504
- // This is particularly important for rapid successive requests
418
+ // Note: keepalive has a 64KB body size limit in browsers, so only use
419
+ // it for small payloads. For large payloads, skip keepalive.
420
+ const useKeepalive = body.length < 60000;
505
421
  const response = await fetch(post_url, {
506
422
  method: "POST",
507
423
  headers: this._get_headers(false),
508
424
  body: body,
509
- keepalive: true, // Enable connection reuse
425
+ ...(useKeepalive && { keepalive: true }),
510
426
  });
511
427
 
512
428
  if (!response.ok) {
@@ -534,6 +450,12 @@ class HTTPStreamingRPCConnection {
534
450
 
535
451
  this._closed = true;
536
452
 
453
+ // Abort any active stream fetch to release the connection immediately
454
+ if (this._abort_controller) {
455
+ this._abort_controller.abort();
456
+ this._abort_controller = null;
457
+ }
458
+
537
459
  if (this._handle_disconnected) {
538
460
  this._handle_disconnected(reason);
539
461
  }
@@ -582,8 +504,6 @@ async function _connectToServerHTTP(config) {
582
504
  config.reconnection_token,
583
505
  config.method_timeout || 30,
584
506
  config.token_refresh_interval || 2 * 60 * 60,
585
- // Default to msgpack for full binary support and proper RPC message handling
586
- config.format || "msgpack",
587
507
  );
588
508
 
589
509
  const connection_info = await connection.open();
@@ -603,15 +523,11 @@ async function _connectToServerHTTP(config) {
603
523
  server_base_url: connection_info.public_base_url,
604
524
  });
605
525
 
606
- await (0,_utils_index_js__WEBPACK_IMPORTED_MODULE_1__.waitFor)(
607
- () => rpc._services_registered,
608
- null,
609
- config.method_timeout || 120,
610
- "Timeout waiting for services to register",
611
- );
526
+ await rpc.waitFor("services_registered", config.method_timeout || 120);
612
527
 
613
528
  const wm = await rpc.get_manager_service({
614
529
  timeout: config.method_timeout || 30,
530
+ case_conversion: "camel",
615
531
  });
616
532
  wm.rpc = rpc;
617
533
 
@@ -649,7 +565,7 @@ async function _connectToServerHTTP(config) {
649
565
  wm.serve = (0,_utils_schema_js__WEBPACK_IMPORTED_MODULE_2__.schemaFunction)(serve, {
650
566
  name: "serve",
651
567
  description: "Run event loop forever",
652
- parameters: {},
568
+ parameters: { type: "object", properties: {} },
653
569
  });
654
570
 
655
571
  if (connection_info) {
@@ -1401,7 +1317,8 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1401
1317
  main["ctx"] = JSON.parse(JSON.stringify(main));
1402
1318
  Object.assign(main["ctx"], this.default_context);
1403
1319
  this._fire(main["type"], main);
1404
- } else if (message instanceof ArrayBuffer) {
1320
+ } else if (message instanceof ArrayBuffer || ArrayBuffer.isView(message)) {
1321
+ // Handle both ArrayBuffer (WebSocket) and Uint8Array/ArrayBufferView (HTTP transport)
1405
1322
  let unpacker = (0,_msgpack_msgpack__WEBPACK_IMPORTED_MODULE_2__.decodeMulti)(message);
1406
1323
  const { done, value } = unpacker.next();
1407
1324
  const main = value;
@@ -1445,31 +1362,12 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
1445
1362
  }
1446
1363
  }
1447
1364
 
1448
- // Unsubscribe from client_disconnected events if subscribed
1365
+ // Clean up client_disconnected subscription
1449
1366
  if (this._clientDisconnectedSubscription) {
1450
- try {
1451
- // Get the manager service to unsubscribe (non-blocking)
1452
- if (this._connection && this._connection.manager_id) {
1453
- this.get_remote_service("*/" + this._connection.manager_id)
1454
- .then((manager) => {
1455
- if (
1456
- manager.unsubscribe &&
1457
- typeof manager.unsubscribe === "function"
1458
- ) {
1459
- return manager.unsubscribe("client_disconnected");
1460
- }
1461
- })
1462
- .catch((e) => {
1463
- console.debug(
1464
- `Error unsubscribing from client_disconnected: ${e}`,
1465
- );
1466
- });
1467
- }
1468
- // Remove the local event handler
1469
- this.off("client_disconnected");
1470
- } catch (e) {
1471
- console.debug(`Error unsubscribing from client_disconnected: ${e}`);
1472
- }
1367
+ // Remove the local event handler (no need to unsubscribe from server -
1368
+ // the server will clean up when it detects the disconnection)
1369
+ this.off("client_disconnected");
1370
+ this._clientDisconnectedSubscription = null;
1473
1371
  }
1474
1372
 
1475
1373
  // Clean up background tasks
@@ -2591,11 +2489,14 @@ class RPC extends _utils_index_js__WEBPACK_IMPORTED_MODULE_0__.MessageEmitter {
2591
2489
  ) {
2592
2490
  let target_id = encoded_method._rtarget;
2593
2491
  if (remote_workspace && !target_id.includes("/")) {
2594
- if (remote_workspace !== target_id) {
2595
- target_id = remote_workspace + "/" + target_id;
2492
+ // Don't modify target_id if it starts with */ (workspace manager service)
2493
+ if (!target_id.startsWith("*/")) {
2494
+ if (remote_workspace !== target_id) {
2495
+ target_id = remote_workspace + "/" + target_id;
2496
+ }
2497
+ // Fix the target id to be an absolute id
2498
+ encoded_method._rtarget = target_id;
2596
2499
  }
2597
- // Fix the target id to be an absolute id
2598
- encoded_method._rtarget = target_id;
2599
2500
  }
2600
2501
  let method_id = encoded_method._rmethod;
2601
2502
  let with_promise = encoded_method._rpromise || false;
@@ -7177,6 +7078,20 @@ function normalizeServerUrl(server_url) {
7177
7078
  return server_url;
7178
7079
  }
7179
7080
 
7081
+ /**
7082
+ * Login to the hypha server.
7083
+ *
7084
+ * Configuration options:
7085
+ * server_url: The server URL (required)
7086
+ * workspace: Target workspace (optional)
7087
+ * login_service_id: Login service ID (default: "public/hypha-login")
7088
+ * expires_in: Token expiration time (optional)
7089
+ * login_timeout: Timeout for login process (default: 60)
7090
+ * login_callback: Callback function for login URL (optional)
7091
+ * profile: Whether to return user profile (optional)
7092
+ * additional_headers: Additional HTTP headers (optional)
7093
+ * transport: Transport type - "websocket" (default) or "http"
7094
+ */
7180
7095
  async function login(config) {
7181
7096
  const service_id = config.login_service_id || "public/hypha-login";
7182
7097
  const workspace = config.workspace;
@@ -7185,11 +7100,13 @@ async function login(config) {
7185
7100
  const callback = config.login_callback;
7186
7101
  const profile = config.profile;
7187
7102
  const additional_headers = config.additional_headers;
7103
+ const transport = config.transport || "websocket";
7188
7104
 
7189
7105
  const server = await connectToServer({
7190
7106
  name: "initial login client",
7191
7107
  server_url: config.server_url,
7192
7108
  additional_headers: additional_headers,
7109
+ transport: transport,
7193
7110
  });
7194
7111
  try {
7195
7112
  const svc = await server.getService(service_id);
@@ -7213,15 +7130,27 @@ async function login(config) {
7213
7130
  }
7214
7131
  }
7215
7132
 
7133
+ /**
7134
+ * Logout from the hypha server.
7135
+ *
7136
+ * Configuration options:
7137
+ * server_url: The server URL (required)
7138
+ * login_service_id: Login service ID (default: "public/hypha-login")
7139
+ * logout_callback: Callback function for logout URL (optional)
7140
+ * additional_headers: Additional HTTP headers (optional)
7141
+ * transport: Transport type - "websocket" (default) or "http"
7142
+ */
7216
7143
  async function logout(config) {
7217
7144
  const service_id = config.login_service_id || "public/hypha-login";
7218
7145
  const callback = config.logout_callback;
7219
7146
  const additional_headers = config.additional_headers;
7147
+ const transport = config.transport || "websocket";
7220
7148
 
7221
7149
  const server = await connectToServer({
7222
7150
  name: "initial logout client",
7223
7151
  server_url: config.server_url,
7224
7152
  additional_headers: additional_headers,
7153
+ transport: transport,
7225
7154
  });
7226
7155
  try {
7227
7156
  const svc = await server.getService(service_id);