@reactor-team/js-sdk 2.2.2 → 2.3.2

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/dist/index.js CHANGED
@@ -227,12 +227,13 @@ function waitForIceGathering(pc, timeoutMs = 5e3) {
227
227
  }, timeoutMs);
228
228
  });
229
229
  }
230
- function sendMessage(channel, command, data) {
230
+ function sendMessage(channel, command, data, scope = "application") {
231
231
  if (channel.readyState !== "open") {
232
232
  throw new Error(`Data channel not open: ${channel.readyState}`);
233
233
  }
234
234
  const jsonData = typeof data === "string" ? JSON.parse(data) : data;
235
- const payload = { type: command, data: jsonData };
235
+ const inner = { type: command, data: jsonData };
236
+ const payload = { scope, data: inner };
236
237
  channel.send(JSON.stringify(payload));
237
238
  }
238
239
  function parseMessage(data) {
@@ -251,8 +252,9 @@ function closePeerConnection(pc) {
251
252
 
252
253
  // src/core/CoordinatorClient.ts
253
254
  var INITIAL_BACKOFF_MS = 500;
254
- var MAX_BACKOFF_MS = 3e4;
255
+ var MAX_BACKOFF_MS = 15e3;
255
256
  var BACKOFF_MULTIPLIER = 2;
257
+ var DEFAULT_MAX_ATTEMPTS = 6;
256
258
  var CoordinatorClient = class {
257
259
  constructor(options) {
258
260
  this.baseUrl = options.baseUrl;
@@ -445,13 +447,14 @@ var CoordinatorClient = class {
445
447
  });
446
448
  }
447
449
  /**
448
- * Polls for the SDP answer with geometric backoff.
450
+ * Polls for the SDP answer with exponential backoff.
449
451
  * Used for async reconnection when the answer is not immediately available.
450
452
  * @param sessionId - The session ID to poll for
453
+ * @param maxAttempts - Optional maximum number of polling attempts before giving up
451
454
  * @returns The SDP answer from the server
452
455
  */
453
- pollSdpAnswer(sessionId) {
454
- return __async(this, null, function* () {
456
+ pollSdpAnswer(_0) {
457
+ return __async(this, arguments, function* (sessionId, maxAttempts = DEFAULT_MAX_ATTEMPTS) {
455
458
  console.debug(
456
459
  "[CoordinatorClient] Polling for SDP answer for session:",
457
460
  sessionId
@@ -459,9 +462,14 @@ var CoordinatorClient = class {
459
462
  let backoffMs = INITIAL_BACKOFF_MS;
460
463
  let attempt = 0;
461
464
  while (true) {
465
+ if (attempt >= maxAttempts) {
466
+ throw new Error(
467
+ `SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
468
+ );
469
+ }
462
470
  attempt++;
463
471
  console.debug(
464
- `[CoordinatorClient] SDP poll attempt ${attempt} for session ${sessionId}`
472
+ `[CoordinatorClient] SDP poll attempt ${attempt}/${maxAttempts} for session ${sessionId}`
465
473
  );
466
474
  const response = yield fetch(
467
475
  `${this.baseUrl}/sessions/${sessionId}/sdp_params`,
@@ -498,9 +506,10 @@ var CoordinatorClient = class {
498
506
  * falls back to polling. If no sdpOffer is provided, goes directly to polling.
499
507
  * @param sessionId - The session ID to connect to
500
508
  * @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
509
+ * @param maxAttempts - Optional maximum number of polling attempts before giving up
501
510
  * @returns The SDP answer from the server
502
511
  */
503
- connect(sessionId, sdpOffer) {
512
+ connect(sessionId, sdpOffer, maxAttempts) {
504
513
  return __async(this, null, function* () {
505
514
  console.debug("[CoordinatorClient] Connecting to session:", sessionId);
506
515
  if (sdpOffer) {
@@ -509,7 +518,7 @@ var CoordinatorClient = class {
509
518
  return answer;
510
519
  }
511
520
  }
512
- return this.pollSdpAnswer(sessionId);
521
+ return this.pollSdpAnswer(sessionId, maxAttempts);
513
522
  });
514
523
  }
515
524
  /**
@@ -614,6 +623,7 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
614
623
  };
615
624
 
616
625
  // src/core/GPUMachineClient.ts
626
+ var PING_INTERVAL_MS = 5e3;
617
627
  var GPUMachineClient = class {
618
628
  constructor(config) {
619
629
  this.eventListeners = /* @__PURE__ */ new Map();
@@ -696,6 +706,7 @@ var GPUMachineClient = class {
696
706
  */
697
707
  disconnect() {
698
708
  return __async(this, null, function* () {
709
+ this.stopPing();
699
710
  if (this.publishedTrack) {
700
711
  yield this.unpublishTrack();
701
712
  }
@@ -736,13 +747,14 @@ var GPUMachineClient = class {
736
747
  * Sends a command to the GPU machine via the data channel.
737
748
  * @param command The command to send
738
749
  * @param data The data to send with the command. These are the parameters for the command, matching the scheme in the capabilities dictionary.
750
+ * @param scope The message scope – "application" (default) for model commands, "runtime" for platform-level messages.
739
751
  */
740
- sendCommand(command, data) {
752
+ sendCommand(command, data, scope = "application") {
741
753
  if (!this.dataChannel) {
742
754
  throw new Error("[GPUMachineClient] Data channel not available");
743
755
  }
744
756
  try {
745
- sendMessage(this.dataChannel, command, data);
757
+ sendMessage(this.dataChannel, command, data, scope);
746
758
  } catch (error) {
747
759
  console.warn("[GPUMachineClient] Failed to send message:", error);
748
760
  }
@@ -823,6 +835,34 @@ var GPUMachineClient = class {
823
835
  return new MediaStream(tracks);
824
836
  }
825
837
  // ─────────────────────────────────────────────────────────────────────────────
838
+ // Ping (Client Liveness)
839
+ // ─────────────────────────────────────────────────────────────────────────────
840
+ /**
841
+ * Starts sending periodic "ping" messages on the runtime channel so the
842
+ * server can detect stale connections quickly.
843
+ */
844
+ startPing() {
845
+ this.stopPing();
846
+ this.pingInterval = setInterval(() => {
847
+ var _a;
848
+ if (((_a = this.dataChannel) == null ? void 0 : _a.readyState) === "open") {
849
+ try {
850
+ sendMessage(this.dataChannel, "ping", {}, "runtime");
851
+ } catch (e) {
852
+ }
853
+ }
854
+ }, PING_INTERVAL_MS);
855
+ }
856
+ /**
857
+ * Stops the periodic ping.
858
+ */
859
+ stopPing() {
860
+ if (this.pingInterval !== void 0) {
861
+ clearInterval(this.pingInterval);
862
+ this.pingInterval = void 0;
863
+ }
864
+ }
865
+ // ─────────────────────────────────────────────────────────────────────────────
826
866
  // Private Helpers
827
867
  // ─────────────────────────────────────────────────────────────────────────────
828
868
  setStatus(newStatus) {
@@ -876,18 +916,29 @@ var GPUMachineClient = class {
876
916
  if (!this.dataChannel) return;
877
917
  this.dataChannel.onopen = () => {
878
918
  console.debug("[GPUMachineClient] Data channel open");
919
+ this.startPing();
879
920
  };
880
921
  this.dataChannel.onclose = () => {
881
922
  console.debug("[GPUMachineClient] Data channel closed");
923
+ this.stopPing();
882
924
  };
883
925
  this.dataChannel.onerror = (error) => {
884
926
  console.error("[GPUMachineClient] Data channel error:", error);
885
927
  };
886
928
  this.dataChannel.onmessage = (event) => {
887
- const data = parseMessage(event.data);
888
- console.debug("[GPUMachineClient] Received message:", data);
929
+ const rawData = parseMessage(event.data);
930
+ console.debug("[GPUMachineClient] Received message:", rawData);
889
931
  try {
890
- this.emit("application", data);
932
+ if ((rawData == null ? void 0 : rawData.scope) === "application" && (rawData == null ? void 0 : rawData.data) !== void 0) {
933
+ this.emit("message", rawData.data, "application");
934
+ } else if ((rawData == null ? void 0 : rawData.scope) === "runtime" && (rawData == null ? void 0 : rawData.data) !== void 0) {
935
+ this.emit("message", rawData.data, "runtime");
936
+ } else {
937
+ console.warn(
938
+ "[GPUMachineClient] Received message without envelope, treating as application"
939
+ );
940
+ this.emit("message", rawData, "application");
941
+ }
891
942
  } catch (error) {
892
943
  console.error(
893
944
  "[GPUMachineClient] Failed to parse/validate message:",
@@ -937,11 +988,14 @@ var Reactor = class {
937
988
  }
938
989
  /**
939
990
  * Public method to send a message to the machine.
940
- * Automatically wraps the message in an application message.
941
- * @param message The message to send to the machine.
991
+ * Wraps the message in the specified channel envelope (defaults to "application").
992
+ * @param command The command name to send.
993
+ * @param data The command payload.
994
+ * @param scope The envelope scope – "application" (default) for model commands,
995
+ * "runtime" for platform-level messages (e.g. requestCapabilities).
942
996
  * @throws Error if not in ready state
943
997
  */
944
- sendCommand(command, data) {
998
+ sendCommand(command, data, scope = "application") {
945
999
  return __async(this, null, function* () {
946
1000
  var _a;
947
1001
  if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
@@ -950,7 +1004,7 @@ var Reactor = class {
950
1004
  return;
951
1005
  }
952
1006
  try {
953
- (_a = this.machineClient) == null ? void 0 : _a.sendCommand(command, data);
1007
+ (_a = this.machineClient) == null ? void 0 : _a.sendCommand(command, data, scope);
954
1008
  } catch (error) {
955
1009
  console.error("[Reactor] Failed to send message:", error);
956
1010
  this.createError(
@@ -1008,8 +1062,9 @@ var Reactor = class {
1008
1062
  }
1009
1063
  /**
1010
1064
  * Public method for reconnecting to an existing session, that may have been interrupted but can be recovered.
1065
+ * @param options Optional connect options (e.g. maxAttempts for SDP polling)
1011
1066
  */
1012
- reconnect() {
1067
+ reconnect(options) {
1013
1068
  return __async(this, null, function* () {
1014
1069
  if (!this.sessionId || !this.coordinatorClient) {
1015
1070
  console.warn("[Reactor] No active session to reconnect to.");
@@ -1029,7 +1084,8 @@ var Reactor = class {
1029
1084
  try {
1030
1085
  const sdpAnswer = yield this.coordinatorClient.connect(
1031
1086
  this.sessionId,
1032
- sdpOffer
1087
+ sdpOffer,
1088
+ options == null ? void 0 : options.maxAttempts
1033
1089
  );
1034
1090
  yield this.machineClient.connect(sdpAnswer);
1035
1091
  this.setStatus("ready");
@@ -1053,8 +1109,10 @@ var Reactor = class {
1053
1109
  * Connects to the coordinator and waits for a GPU to be assigned.
1054
1110
  * Once a GPU is assigned, the Reactor will connect to the gpu machine via WebRTC.
1055
1111
  * If no authentication is provided and not in local mode, an error is thrown.
1112
+ * @param jwtToken Optional JWT token for authentication
1113
+ * @param options Optional connect options (e.g. maxAttempts for SDP polling)
1056
1114
  */
1057
- connect(jwtToken) {
1115
+ connect(jwtToken, options) {
1058
1116
  return __async(this, null, function* () {
1059
1117
  console.debug("[Reactor] Connecting, status:", this.status);
1060
1118
  if (jwtToken == void 0 && !this.local) {
@@ -1071,7 +1129,7 @@ var Reactor = class {
1071
1129
  this.coordinatorClient = this.local ? new LocalCoordinatorClient(this.coordinatorUrl) : new CoordinatorClient({
1072
1130
  baseUrl: this.coordinatorUrl,
1073
1131
  jwtToken,
1074
- // Safe: validated on line 186-188
1132
+ // Safe: validated above
1075
1133
  model: this.model
1076
1134
  });
1077
1135
  const iceServers = yield this.coordinatorClient.getIceServers();
@@ -1080,7 +1138,11 @@ var Reactor = class {
1080
1138
  const sdpOffer = yield this.machineClient.createOffer();
1081
1139
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1082
1140
  this.setSessionId(sessionId);
1083
- const sdpAnswer = yield this.coordinatorClient.connect(sessionId);
1141
+ const sdpAnswer = yield this.coordinatorClient.connect(
1142
+ sessionId,
1143
+ void 0,
1144
+ options == null ? void 0 : options.maxAttempts
1145
+ );
1084
1146
  yield this.machineClient.connect(sdpAnswer);
1085
1147
  } catch (error) {
1086
1148
  console.error("[Reactor] Connection failed:", error);
@@ -1090,7 +1152,14 @@ var Reactor = class {
1090
1152
  "coordinator",
1091
1153
  true
1092
1154
  );
1093
- this.setStatus("disconnected");
1155
+ try {
1156
+ yield this.disconnect(false);
1157
+ } catch (disconnectError) {
1158
+ console.error(
1159
+ "[Reactor] Failed to clean up after connection failure:",
1160
+ disconnectError
1161
+ );
1162
+ }
1094
1163
  throw error;
1095
1164
  }
1096
1165
  });
@@ -1100,12 +1169,8 @@ var Reactor = class {
1100
1169
  */
1101
1170
  setupMachineClientHandlers() {
1102
1171
  if (!this.machineClient) return;
1103
- this.machineClient.on("application", (message) => {
1104
- if ((message == null ? void 0 : message.type) === "application" && (message == null ? void 0 : message.data) !== void 0) {
1105
- this.emit("newMessage", message.data);
1106
- } else {
1107
- this.emit("newMessage", message);
1108
- }
1172
+ this.machineClient.on("message", (message, scope) => {
1173
+ this.emit("newMessage", message, scope);
1109
1174
  });
1110
1175
  this.machineClient.on("statusChanged", (status) => {
1111
1176
  switch (status) {
@@ -1312,23 +1377,27 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1312
1377
  get().internal.reactor.off("newMessage", handler);
1313
1378
  };
1314
1379
  },
1315
- sendCommand: (command, data) => __async(null, null, function* () {
1316
- console.debug("[ReactorStore] Sending command", { command, data });
1380
+ sendCommand: (command, data, scope) => __async(null, null, function* () {
1381
+ console.debug("[ReactorStore] Sending command", {
1382
+ command,
1383
+ data,
1384
+ scope
1385
+ });
1317
1386
  try {
1318
- yield get().internal.reactor.sendCommand(command, data);
1387
+ yield get().internal.reactor.sendCommand(command, data, scope);
1319
1388
  console.debug("[ReactorStore] Command sent successfully");
1320
1389
  } catch (error) {
1321
1390
  console.error("[ReactorStore] Failed to send command:", error);
1322
1391
  throw error;
1323
1392
  }
1324
1393
  }),
1325
- connect: (jwtToken) => __async(null, null, function* () {
1394
+ connect: (jwtToken, options) => __async(null, null, function* () {
1326
1395
  if (jwtToken === void 0) {
1327
1396
  jwtToken = get().jwtToken;
1328
1397
  }
1329
1398
  console.debug("[ReactorStore] Connect called.");
1330
1399
  try {
1331
- yield get().internal.reactor.connect(jwtToken);
1400
+ yield get().internal.reactor.connect(jwtToken, options);
1332
1401
  console.debug("[ReactorStore] Connect completed successfully");
1333
1402
  } catch (error) {
1334
1403
  console.error("[ReactorStore] Connect failed:", error);
@@ -1373,10 +1442,10 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1373
1442
  throw error;
1374
1443
  }
1375
1444
  }),
1376
- reconnect: () => __async(null, null, function* () {
1445
+ reconnect: (options) => __async(null, null, function* () {
1377
1446
  console.debug("[ReactorStore] Reconnecting");
1378
1447
  try {
1379
- yield get().internal.reactor.reconnect();
1448
+ yield get().internal.reactor.reconnect(options);
1380
1449
  console.debug("[ReactorStore] Reconnect completed successfully");
1381
1450
  } catch (error) {
1382
1451
  console.error("[ReactorStore] Failed to reconnect:", error);
@@ -1393,11 +1462,11 @@ var import_jsx_runtime = require("react/jsx-runtime");
1393
1462
  function ReactorProvider(_a) {
1394
1463
  var _b = _a, {
1395
1464
  children,
1396
- autoConnect = true,
1465
+ connectOptions,
1397
1466
  jwtToken
1398
1467
  } = _b, props = __objRest(_b, [
1399
1468
  "children",
1400
- "autoConnect",
1469
+ "connectOptions",
1401
1470
  "jwtToken"
1402
1471
  ]);
1403
1472
  const storeRef = (0, import_react3.useRef)(void 0);
@@ -1412,14 +1481,16 @@ function ReactorProvider(_a) {
1412
1481
  );
1413
1482
  console.debug("[ReactorProvider] Reactor store created successfully");
1414
1483
  }
1484
+ const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
1415
1485
  const { coordinatorUrl, modelName, local } = props;
1486
+ const maxAttempts = pollingOptions.maxAttempts;
1416
1487
  (0, import_react3.useEffect)(() => {
1417
1488
  const handleBeforeUnload = () => {
1418
- var _a2;
1489
+ var _a3;
1419
1490
  console.debug(
1420
1491
  "[ReactorProvider] Page unloading, performing non-recoverable disconnect"
1421
1492
  );
1422
- (_a2 = storeRef.current) == null ? void 0 : _a2.getState().internal.reactor.disconnect(false);
1493
+ (_a3 = storeRef.current) == null ? void 0 : _a3.getState().internal.reactor.disconnect(false);
1423
1494
  };
1424
1495
  window.addEventListener("beforeunload", handleBeforeUnload);
1425
1496
  return () => {
@@ -1434,7 +1505,7 @@ function ReactorProvider(_a) {
1434
1505
  console.debug(
1435
1506
  "[ReactorProvider] Starting autoconnect in first render..."
1436
1507
  );
1437
- current2.getState().connect(jwtToken).then(() => {
1508
+ current2.getState().connect(jwtToken, pollingOptions).then(() => {
1438
1509
  console.debug(
1439
1510
  "[ReactorProvider] Autoconnect successful in first render"
1440
1511
  );
@@ -1477,7 +1548,7 @@ function ReactorProvider(_a) {
1477
1548
  );
1478
1549
  if (autoConnect && current.getState().status === "disconnected" && jwtToken) {
1479
1550
  console.debug("[ReactorProvider] Starting autoconnect...");
1480
- current.getState().connect(jwtToken).then(() => {
1551
+ current.getState().connect(jwtToken, pollingOptions).then(() => {
1481
1552
  console.debug("[ReactorProvider] Autoconnect successful");
1482
1553
  }).catch((error) => {
1483
1554
  console.error("[ReactorProvider] Failed to autoconnect:", error);
@@ -1493,7 +1564,7 @@ function ReactorProvider(_a) {
1493
1564
  console.error("[ReactorProvider] Failed to disconnect:", error);
1494
1565
  });
1495
1566
  };
1496
- }, [coordinatorUrl, modelName, autoConnect, local, jwtToken]);
1567
+ }, [coordinatorUrl, modelName, autoConnect, local, jwtToken, maxAttempts]);
1497
1568
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReactorContext.Provider, { value: storeRef.current, children });
1498
1569
  }
1499
1570
  function useReactorStore(selector) {
@@ -1518,9 +1589,12 @@ function useReactorMessage(handler) {
1518
1589
  }, [handler]);
1519
1590
  (0, import_react4.useEffect)(() => {
1520
1591
  console.debug("[useReactorMessage] Setting up message subscription");
1521
- const stableHandler = (message) => {
1522
- console.debug("[useReactorMessage] Message received", { message });
1523
- handlerRef.current(message);
1592
+ const stableHandler = (message, scope) => {
1593
+ console.debug("[useReactorMessage] Message received", {
1594
+ message,
1595
+ scope
1596
+ });
1597
+ handlerRef.current(message, scope);
1524
1598
  };
1525
1599
  reactor.on("newMessage", stableHandler);
1526
1600
  console.debug("[useReactorMessage] Message handler registered");
@@ -1649,7 +1723,7 @@ function ReactorController({
1649
1723
  }, [status]);
1650
1724
  const requestCapabilities = (0, import_react6.useCallback)(() => {
1651
1725
  if (status === "ready") {
1652
- sendCommand("requestCapabilities", {});
1726
+ sendCommand("requestCapabilities", {}, "runtime");
1653
1727
  }
1654
1728
  }, [status, sendCommand]);
1655
1729
  import_react6.default.useEffect(() => {
@@ -1666,9 +1740,9 @@ function ReactorController({
1666
1740
  }, 5e3);
1667
1741
  return () => clearInterval(interval);
1668
1742
  }, [status, commands, requestCapabilities]);
1669
- useReactorMessage((message) => {
1670
- if (message && typeof message === "object" && "commands" in message) {
1671
- const commandsMessage = message;
1743
+ useReactorMessage((message, scope) => {
1744
+ if (scope === "runtime" && message && typeof message === "object" && message.type === "modelCapabilities" && message.data && "commands" in message.data) {
1745
+ const commandsMessage = message.data;
1672
1746
  setCommands(commandsMessage.commands);
1673
1747
  const initialValues = {};
1674
1748
  const initialExpanded = {};