@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.mjs CHANGED
@@ -184,12 +184,13 @@ function waitForIceGathering(pc, timeoutMs = 5e3) {
184
184
  }, timeoutMs);
185
185
  });
186
186
  }
187
- function sendMessage(channel, command, data) {
187
+ function sendMessage(channel, command, data, scope = "application") {
188
188
  if (channel.readyState !== "open") {
189
189
  throw new Error(`Data channel not open: ${channel.readyState}`);
190
190
  }
191
191
  const jsonData = typeof data === "string" ? JSON.parse(data) : data;
192
- const payload = { type: command, data: jsonData };
192
+ const inner = { type: command, data: jsonData };
193
+ const payload = { scope, data: inner };
193
194
  channel.send(JSON.stringify(payload));
194
195
  }
195
196
  function parseMessage(data) {
@@ -208,8 +209,9 @@ function closePeerConnection(pc) {
208
209
 
209
210
  // src/core/CoordinatorClient.ts
210
211
  var INITIAL_BACKOFF_MS = 500;
211
- var MAX_BACKOFF_MS = 3e4;
212
+ var MAX_BACKOFF_MS = 15e3;
212
213
  var BACKOFF_MULTIPLIER = 2;
214
+ var DEFAULT_MAX_ATTEMPTS = 6;
213
215
  var CoordinatorClient = class {
214
216
  constructor(options) {
215
217
  this.baseUrl = options.baseUrl;
@@ -402,13 +404,14 @@ var CoordinatorClient = class {
402
404
  });
403
405
  }
404
406
  /**
405
- * Polls for the SDP answer with geometric backoff.
407
+ * Polls for the SDP answer with exponential backoff.
406
408
  * Used for async reconnection when the answer is not immediately available.
407
409
  * @param sessionId - The session ID to poll for
410
+ * @param maxAttempts - Optional maximum number of polling attempts before giving up
408
411
  * @returns The SDP answer from the server
409
412
  */
410
- pollSdpAnswer(sessionId) {
411
- return __async(this, null, function* () {
413
+ pollSdpAnswer(_0) {
414
+ return __async(this, arguments, function* (sessionId, maxAttempts = DEFAULT_MAX_ATTEMPTS) {
412
415
  console.debug(
413
416
  "[CoordinatorClient] Polling for SDP answer for session:",
414
417
  sessionId
@@ -416,9 +419,14 @@ var CoordinatorClient = class {
416
419
  let backoffMs = INITIAL_BACKOFF_MS;
417
420
  let attempt = 0;
418
421
  while (true) {
422
+ if (attempt >= maxAttempts) {
423
+ throw new Error(
424
+ `SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
425
+ );
426
+ }
419
427
  attempt++;
420
428
  console.debug(
421
- `[CoordinatorClient] SDP poll attempt ${attempt} for session ${sessionId}`
429
+ `[CoordinatorClient] SDP poll attempt ${attempt}/${maxAttempts} for session ${sessionId}`
422
430
  );
423
431
  const response = yield fetch(
424
432
  `${this.baseUrl}/sessions/${sessionId}/sdp_params`,
@@ -455,9 +463,10 @@ var CoordinatorClient = class {
455
463
  * falls back to polling. If no sdpOffer is provided, goes directly to polling.
456
464
  * @param sessionId - The session ID to connect to
457
465
  * @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
466
+ * @param maxAttempts - Optional maximum number of polling attempts before giving up
458
467
  * @returns The SDP answer from the server
459
468
  */
460
- connect(sessionId, sdpOffer) {
469
+ connect(sessionId, sdpOffer, maxAttempts) {
461
470
  return __async(this, null, function* () {
462
471
  console.debug("[CoordinatorClient] Connecting to session:", sessionId);
463
472
  if (sdpOffer) {
@@ -466,7 +475,7 @@ var CoordinatorClient = class {
466
475
  return answer;
467
476
  }
468
477
  }
469
- return this.pollSdpAnswer(sessionId);
478
+ return this.pollSdpAnswer(sessionId, maxAttempts);
470
479
  });
471
480
  }
472
481
  /**
@@ -571,6 +580,7 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
571
580
  };
572
581
 
573
582
  // src/core/GPUMachineClient.ts
583
+ var PING_INTERVAL_MS = 5e3;
574
584
  var GPUMachineClient = class {
575
585
  constructor(config) {
576
586
  this.eventListeners = /* @__PURE__ */ new Map();
@@ -653,6 +663,7 @@ var GPUMachineClient = class {
653
663
  */
654
664
  disconnect() {
655
665
  return __async(this, null, function* () {
666
+ this.stopPing();
656
667
  if (this.publishedTrack) {
657
668
  yield this.unpublishTrack();
658
669
  }
@@ -693,13 +704,14 @@ var GPUMachineClient = class {
693
704
  * Sends a command to the GPU machine via the data channel.
694
705
  * @param command The command to send
695
706
  * @param data The data to send with the command. These are the parameters for the command, matching the scheme in the capabilities dictionary.
707
+ * @param scope The message scope – "application" (default) for model commands, "runtime" for platform-level messages.
696
708
  */
697
- sendCommand(command, data) {
709
+ sendCommand(command, data, scope = "application") {
698
710
  if (!this.dataChannel) {
699
711
  throw new Error("[GPUMachineClient] Data channel not available");
700
712
  }
701
713
  try {
702
- sendMessage(this.dataChannel, command, data);
714
+ sendMessage(this.dataChannel, command, data, scope);
703
715
  } catch (error) {
704
716
  console.warn("[GPUMachineClient] Failed to send message:", error);
705
717
  }
@@ -780,6 +792,34 @@ var GPUMachineClient = class {
780
792
  return new MediaStream(tracks);
781
793
  }
782
794
  // ─────────────────────────────────────────────────────────────────────────────
795
+ // Ping (Client Liveness)
796
+ // ─────────────────────────────────────────────────────────────────────────────
797
+ /**
798
+ * Starts sending periodic "ping" messages on the runtime channel so the
799
+ * server can detect stale connections quickly.
800
+ */
801
+ startPing() {
802
+ this.stopPing();
803
+ this.pingInterval = setInterval(() => {
804
+ var _a;
805
+ if (((_a = this.dataChannel) == null ? void 0 : _a.readyState) === "open") {
806
+ try {
807
+ sendMessage(this.dataChannel, "ping", {}, "runtime");
808
+ } catch (e) {
809
+ }
810
+ }
811
+ }, PING_INTERVAL_MS);
812
+ }
813
+ /**
814
+ * Stops the periodic ping.
815
+ */
816
+ stopPing() {
817
+ if (this.pingInterval !== void 0) {
818
+ clearInterval(this.pingInterval);
819
+ this.pingInterval = void 0;
820
+ }
821
+ }
822
+ // ─────────────────────────────────────────────────────────────────────────────
783
823
  // Private Helpers
784
824
  // ─────────────────────────────────────────────────────────────────────────────
785
825
  setStatus(newStatus) {
@@ -833,18 +873,29 @@ var GPUMachineClient = class {
833
873
  if (!this.dataChannel) return;
834
874
  this.dataChannel.onopen = () => {
835
875
  console.debug("[GPUMachineClient] Data channel open");
876
+ this.startPing();
836
877
  };
837
878
  this.dataChannel.onclose = () => {
838
879
  console.debug("[GPUMachineClient] Data channel closed");
880
+ this.stopPing();
839
881
  };
840
882
  this.dataChannel.onerror = (error) => {
841
883
  console.error("[GPUMachineClient] Data channel error:", error);
842
884
  };
843
885
  this.dataChannel.onmessage = (event) => {
844
- const data = parseMessage(event.data);
845
- console.debug("[GPUMachineClient] Received message:", data);
886
+ const rawData = parseMessage(event.data);
887
+ console.debug("[GPUMachineClient] Received message:", rawData);
846
888
  try {
847
- this.emit("application", data);
889
+ if ((rawData == null ? void 0 : rawData.scope) === "application" && (rawData == null ? void 0 : rawData.data) !== void 0) {
890
+ this.emit("message", rawData.data, "application");
891
+ } else if ((rawData == null ? void 0 : rawData.scope) === "runtime" && (rawData == null ? void 0 : rawData.data) !== void 0) {
892
+ this.emit("message", rawData.data, "runtime");
893
+ } else {
894
+ console.warn(
895
+ "[GPUMachineClient] Received message without envelope, treating as application"
896
+ );
897
+ this.emit("message", rawData, "application");
898
+ }
848
899
  } catch (error) {
849
900
  console.error(
850
901
  "[GPUMachineClient] Failed to parse/validate message:",
@@ -894,11 +945,14 @@ var Reactor = class {
894
945
  }
895
946
  /**
896
947
  * Public method to send a message to the machine.
897
- * Automatically wraps the message in an application message.
898
- * @param message The message to send to the machine.
948
+ * Wraps the message in the specified channel envelope (defaults to "application").
949
+ * @param command The command name to send.
950
+ * @param data The command payload.
951
+ * @param scope The envelope scope – "application" (default) for model commands,
952
+ * "runtime" for platform-level messages (e.g. requestCapabilities).
899
953
  * @throws Error if not in ready state
900
954
  */
901
- sendCommand(command, data) {
955
+ sendCommand(command, data, scope = "application") {
902
956
  return __async(this, null, function* () {
903
957
  var _a;
904
958
  if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
@@ -907,7 +961,7 @@ var Reactor = class {
907
961
  return;
908
962
  }
909
963
  try {
910
- (_a = this.machineClient) == null ? void 0 : _a.sendCommand(command, data);
964
+ (_a = this.machineClient) == null ? void 0 : _a.sendCommand(command, data, scope);
911
965
  } catch (error) {
912
966
  console.error("[Reactor] Failed to send message:", error);
913
967
  this.createError(
@@ -965,8 +1019,9 @@ var Reactor = class {
965
1019
  }
966
1020
  /**
967
1021
  * Public method for reconnecting to an existing session, that may have been interrupted but can be recovered.
1022
+ * @param options Optional connect options (e.g. maxAttempts for SDP polling)
968
1023
  */
969
- reconnect() {
1024
+ reconnect(options) {
970
1025
  return __async(this, null, function* () {
971
1026
  if (!this.sessionId || !this.coordinatorClient) {
972
1027
  console.warn("[Reactor] No active session to reconnect to.");
@@ -986,7 +1041,8 @@ var Reactor = class {
986
1041
  try {
987
1042
  const sdpAnswer = yield this.coordinatorClient.connect(
988
1043
  this.sessionId,
989
- sdpOffer
1044
+ sdpOffer,
1045
+ options == null ? void 0 : options.maxAttempts
990
1046
  );
991
1047
  yield this.machineClient.connect(sdpAnswer);
992
1048
  this.setStatus("ready");
@@ -1010,8 +1066,10 @@ var Reactor = class {
1010
1066
  * Connects to the coordinator and waits for a GPU to be assigned.
1011
1067
  * Once a GPU is assigned, the Reactor will connect to the gpu machine via WebRTC.
1012
1068
  * If no authentication is provided and not in local mode, an error is thrown.
1069
+ * @param jwtToken Optional JWT token for authentication
1070
+ * @param options Optional connect options (e.g. maxAttempts for SDP polling)
1013
1071
  */
1014
- connect(jwtToken) {
1072
+ connect(jwtToken, options) {
1015
1073
  return __async(this, null, function* () {
1016
1074
  console.debug("[Reactor] Connecting, status:", this.status);
1017
1075
  if (jwtToken == void 0 && !this.local) {
@@ -1028,7 +1086,7 @@ var Reactor = class {
1028
1086
  this.coordinatorClient = this.local ? new LocalCoordinatorClient(this.coordinatorUrl) : new CoordinatorClient({
1029
1087
  baseUrl: this.coordinatorUrl,
1030
1088
  jwtToken,
1031
- // Safe: validated on line 186-188
1089
+ // Safe: validated above
1032
1090
  model: this.model
1033
1091
  });
1034
1092
  const iceServers = yield this.coordinatorClient.getIceServers();
@@ -1037,7 +1095,11 @@ var Reactor = class {
1037
1095
  const sdpOffer = yield this.machineClient.createOffer();
1038
1096
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1039
1097
  this.setSessionId(sessionId);
1040
- const sdpAnswer = yield this.coordinatorClient.connect(sessionId);
1098
+ const sdpAnswer = yield this.coordinatorClient.connect(
1099
+ sessionId,
1100
+ void 0,
1101
+ options == null ? void 0 : options.maxAttempts
1102
+ );
1041
1103
  yield this.machineClient.connect(sdpAnswer);
1042
1104
  } catch (error) {
1043
1105
  console.error("[Reactor] Connection failed:", error);
@@ -1047,7 +1109,14 @@ var Reactor = class {
1047
1109
  "coordinator",
1048
1110
  true
1049
1111
  );
1050
- this.setStatus("disconnected");
1112
+ try {
1113
+ yield this.disconnect(false);
1114
+ } catch (disconnectError) {
1115
+ console.error(
1116
+ "[Reactor] Failed to clean up after connection failure:",
1117
+ disconnectError
1118
+ );
1119
+ }
1051
1120
  throw error;
1052
1121
  }
1053
1122
  });
@@ -1057,12 +1126,8 @@ var Reactor = class {
1057
1126
  */
1058
1127
  setupMachineClientHandlers() {
1059
1128
  if (!this.machineClient) return;
1060
- this.machineClient.on("application", (message) => {
1061
- if ((message == null ? void 0 : message.type) === "application" && (message == null ? void 0 : message.data) !== void 0) {
1062
- this.emit("newMessage", message.data);
1063
- } else {
1064
- this.emit("newMessage", message);
1065
- }
1129
+ this.machineClient.on("message", (message, scope) => {
1130
+ this.emit("newMessage", message, scope);
1066
1131
  });
1067
1132
  this.machineClient.on("statusChanged", (status) => {
1068
1133
  switch (status) {
@@ -1269,23 +1334,27 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1269
1334
  get().internal.reactor.off("newMessage", handler);
1270
1335
  };
1271
1336
  },
1272
- sendCommand: (command, data) => __async(null, null, function* () {
1273
- console.debug("[ReactorStore] Sending command", { command, data });
1337
+ sendCommand: (command, data, scope) => __async(null, null, function* () {
1338
+ console.debug("[ReactorStore] Sending command", {
1339
+ command,
1340
+ data,
1341
+ scope
1342
+ });
1274
1343
  try {
1275
- yield get().internal.reactor.sendCommand(command, data);
1344
+ yield get().internal.reactor.sendCommand(command, data, scope);
1276
1345
  console.debug("[ReactorStore] Command sent successfully");
1277
1346
  } catch (error) {
1278
1347
  console.error("[ReactorStore] Failed to send command:", error);
1279
1348
  throw error;
1280
1349
  }
1281
1350
  }),
1282
- connect: (jwtToken) => __async(null, null, function* () {
1351
+ connect: (jwtToken, options) => __async(null, null, function* () {
1283
1352
  if (jwtToken === void 0) {
1284
1353
  jwtToken = get().jwtToken;
1285
1354
  }
1286
1355
  console.debug("[ReactorStore] Connect called.");
1287
1356
  try {
1288
- yield get().internal.reactor.connect(jwtToken);
1357
+ yield get().internal.reactor.connect(jwtToken, options);
1289
1358
  console.debug("[ReactorStore] Connect completed successfully");
1290
1359
  } catch (error) {
1291
1360
  console.error("[ReactorStore] Connect failed:", error);
@@ -1330,10 +1399,10 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1330
1399
  throw error;
1331
1400
  }
1332
1401
  }),
1333
- reconnect: () => __async(null, null, function* () {
1402
+ reconnect: (options) => __async(null, null, function* () {
1334
1403
  console.debug("[ReactorStore] Reconnecting");
1335
1404
  try {
1336
- yield get().internal.reactor.reconnect();
1405
+ yield get().internal.reactor.reconnect(options);
1337
1406
  console.debug("[ReactorStore] Reconnect completed successfully");
1338
1407
  } catch (error) {
1339
1408
  console.error("[ReactorStore] Failed to reconnect:", error);
@@ -1350,11 +1419,11 @@ import { jsx } from "react/jsx-runtime";
1350
1419
  function ReactorProvider(_a) {
1351
1420
  var _b = _a, {
1352
1421
  children,
1353
- autoConnect = true,
1422
+ connectOptions,
1354
1423
  jwtToken
1355
1424
  } = _b, props = __objRest(_b, [
1356
1425
  "children",
1357
- "autoConnect",
1426
+ "connectOptions",
1358
1427
  "jwtToken"
1359
1428
  ]);
1360
1429
  const storeRef = useRef(void 0);
@@ -1369,14 +1438,16 @@ function ReactorProvider(_a) {
1369
1438
  );
1370
1439
  console.debug("[ReactorProvider] Reactor store created successfully");
1371
1440
  }
1441
+ const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
1372
1442
  const { coordinatorUrl, modelName, local } = props;
1443
+ const maxAttempts = pollingOptions.maxAttempts;
1373
1444
  useEffect(() => {
1374
1445
  const handleBeforeUnload = () => {
1375
- var _a2;
1446
+ var _a3;
1376
1447
  console.debug(
1377
1448
  "[ReactorProvider] Page unloading, performing non-recoverable disconnect"
1378
1449
  );
1379
- (_a2 = storeRef.current) == null ? void 0 : _a2.getState().internal.reactor.disconnect(false);
1450
+ (_a3 = storeRef.current) == null ? void 0 : _a3.getState().internal.reactor.disconnect(false);
1380
1451
  };
1381
1452
  window.addEventListener("beforeunload", handleBeforeUnload);
1382
1453
  return () => {
@@ -1391,7 +1462,7 @@ function ReactorProvider(_a) {
1391
1462
  console.debug(
1392
1463
  "[ReactorProvider] Starting autoconnect in first render..."
1393
1464
  );
1394
- current2.getState().connect(jwtToken).then(() => {
1465
+ current2.getState().connect(jwtToken, pollingOptions).then(() => {
1395
1466
  console.debug(
1396
1467
  "[ReactorProvider] Autoconnect successful in first render"
1397
1468
  );
@@ -1434,7 +1505,7 @@ function ReactorProvider(_a) {
1434
1505
  );
1435
1506
  if (autoConnect && current.getState().status === "disconnected" && jwtToken) {
1436
1507
  console.debug("[ReactorProvider] Starting autoconnect...");
1437
- current.getState().connect(jwtToken).then(() => {
1508
+ current.getState().connect(jwtToken, pollingOptions).then(() => {
1438
1509
  console.debug("[ReactorProvider] Autoconnect successful");
1439
1510
  }).catch((error) => {
1440
1511
  console.error("[ReactorProvider] Failed to autoconnect:", error);
@@ -1450,7 +1521,7 @@ function ReactorProvider(_a) {
1450
1521
  console.error("[ReactorProvider] Failed to disconnect:", error);
1451
1522
  });
1452
1523
  };
1453
- }, [coordinatorUrl, modelName, autoConnect, local, jwtToken]);
1524
+ }, [coordinatorUrl, modelName, autoConnect, local, jwtToken, maxAttempts]);
1454
1525
  return /* @__PURE__ */ jsx(ReactorContext.Provider, { value: storeRef.current, children });
1455
1526
  }
1456
1527
  function useReactorStore(selector) {
@@ -1475,9 +1546,12 @@ function useReactorMessage(handler) {
1475
1546
  }, [handler]);
1476
1547
  useEffect2(() => {
1477
1548
  console.debug("[useReactorMessage] Setting up message subscription");
1478
- const stableHandler = (message) => {
1479
- console.debug("[useReactorMessage] Message received", { message });
1480
- handlerRef.current(message);
1549
+ const stableHandler = (message, scope) => {
1550
+ console.debug("[useReactorMessage] Message received", {
1551
+ message,
1552
+ scope
1553
+ });
1554
+ handlerRef.current(message, scope);
1481
1555
  };
1482
1556
  reactor.on("newMessage", stableHandler);
1483
1557
  console.debug("[useReactorMessage] Message handler registered");
@@ -1606,7 +1680,7 @@ function ReactorController({
1606
1680
  }, [status]);
1607
1681
  const requestCapabilities = useCallback(() => {
1608
1682
  if (status === "ready") {
1609
- sendCommand("requestCapabilities", {});
1683
+ sendCommand("requestCapabilities", {}, "runtime");
1610
1684
  }
1611
1685
  }, [status, sendCommand]);
1612
1686
  React.useEffect(() => {
@@ -1623,9 +1697,9 @@ function ReactorController({
1623
1697
  }, 5e3);
1624
1698
  return () => clearInterval(interval);
1625
1699
  }, [status, commands, requestCapabilities]);
1626
- useReactorMessage((message) => {
1627
- if (message && typeof message === "object" && "commands" in message) {
1628
- const commandsMessage = message;
1700
+ useReactorMessage((message, scope) => {
1701
+ if (scope === "runtime" && message && typeof message === "object" && message.type === "modelCapabilities" && message.data && "commands" in message.data) {
1702
+ const commandsMessage = message.data;
1629
1703
  setCommands(commandsMessage.commands);
1630
1704
  const initialValues = {};
1631
1705
  const initialExpanded = {};