@reactor-team/js-sdk 2.3.1 → 2.4.0

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
@@ -206,11 +206,58 @@ function parseMessage(data) {
206
206
  function closePeerConnection(pc) {
207
207
  pc.close();
208
208
  }
209
+ function extractConnectionStats(report) {
210
+ let rtt;
211
+ let availableOutgoingBitrate;
212
+ let localCandidateId;
213
+ let framesPerSecond;
214
+ let jitter;
215
+ let packetLossRatio;
216
+ report.forEach((stat) => {
217
+ if (stat.type === "candidate-pair" && stat.state === "succeeded") {
218
+ if (stat.currentRoundTripTime !== void 0) {
219
+ rtt = stat.currentRoundTripTime * 1e3;
220
+ }
221
+ if (stat.availableOutgoingBitrate !== void 0) {
222
+ availableOutgoingBitrate = stat.availableOutgoingBitrate;
223
+ }
224
+ localCandidateId = stat.localCandidateId;
225
+ }
226
+ if (stat.type === "inbound-rtp" && stat.kind === "video") {
227
+ if (stat.framesPerSecond !== void 0) {
228
+ framesPerSecond = stat.framesPerSecond;
229
+ }
230
+ if (stat.jitter !== void 0) {
231
+ jitter = stat.jitter;
232
+ }
233
+ if (stat.packetsReceived !== void 0 && stat.packetsLost !== void 0 && stat.packetsReceived + stat.packetsLost > 0) {
234
+ packetLossRatio = stat.packetsLost / (stat.packetsReceived + stat.packetsLost);
235
+ }
236
+ }
237
+ });
238
+ let candidateType;
239
+ if (localCandidateId) {
240
+ const localCandidate = report.get(localCandidateId);
241
+ if (localCandidate == null ? void 0 : localCandidate.candidateType) {
242
+ candidateType = localCandidate.candidateType;
243
+ }
244
+ }
245
+ return {
246
+ rtt,
247
+ candidateType,
248
+ availableOutgoingBitrate,
249
+ framesPerSecond,
250
+ packetLossRatio,
251
+ jitter,
252
+ timestamp: Date.now()
253
+ };
254
+ }
209
255
 
210
256
  // src/core/CoordinatorClient.ts
211
257
  var INITIAL_BACKOFF_MS = 500;
212
- var MAX_BACKOFF_MS = 3e4;
258
+ var MAX_BACKOFF_MS = 15e3;
213
259
  var BACKOFF_MULTIPLIER = 2;
260
+ var DEFAULT_MAX_ATTEMPTS = 6;
214
261
  var CoordinatorClient = class {
215
262
  constructor(options) {
216
263
  this.baseUrl = options.baseUrl;
@@ -403,13 +450,14 @@ var CoordinatorClient = class {
403
450
  });
404
451
  }
405
452
  /**
406
- * Polls for the SDP answer with geometric backoff.
453
+ * Polls for the SDP answer with exponential backoff.
407
454
  * Used for async reconnection when the answer is not immediately available.
408
455
  * @param sessionId - The session ID to poll for
456
+ * @param maxAttempts - Optional maximum number of polling attempts before giving up
409
457
  * @returns The SDP answer from the server
410
458
  */
411
- pollSdpAnswer(sessionId) {
412
- return __async(this, null, function* () {
459
+ pollSdpAnswer(_0) {
460
+ return __async(this, arguments, function* (sessionId, maxAttempts = DEFAULT_MAX_ATTEMPTS) {
413
461
  console.debug(
414
462
  "[CoordinatorClient] Polling for SDP answer for session:",
415
463
  sessionId
@@ -417,9 +465,14 @@ var CoordinatorClient = class {
417
465
  let backoffMs = INITIAL_BACKOFF_MS;
418
466
  let attempt = 0;
419
467
  while (true) {
468
+ if (attempt >= maxAttempts) {
469
+ throw new Error(
470
+ `SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
471
+ );
472
+ }
420
473
  attempt++;
421
474
  console.debug(
422
- `[CoordinatorClient] SDP poll attempt ${attempt} for session ${sessionId}`
475
+ `[CoordinatorClient] SDP poll attempt ${attempt}/${maxAttempts} for session ${sessionId}`
423
476
  );
424
477
  const response = yield fetch(
425
478
  `${this.baseUrl}/sessions/${sessionId}/sdp_params`,
@@ -456,9 +509,10 @@ var CoordinatorClient = class {
456
509
  * falls back to polling. If no sdpOffer is provided, goes directly to polling.
457
510
  * @param sessionId - The session ID to connect to
458
511
  * @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
512
+ * @param maxAttempts - Optional maximum number of polling attempts before giving up
459
513
  * @returns The SDP answer from the server
460
514
  */
461
- connect(sessionId, sdpOffer) {
515
+ connect(sessionId, sdpOffer, maxAttempts) {
462
516
  return __async(this, null, function* () {
463
517
  console.debug("[CoordinatorClient] Connecting to session:", sessionId);
464
518
  if (sdpOffer) {
@@ -467,7 +521,7 @@ var CoordinatorClient = class {
467
521
  return answer;
468
522
  }
469
523
  }
470
- return this.pollSdpAnswer(sessionId);
524
+ return this.pollSdpAnswer(sessionId, maxAttempts);
471
525
  });
472
526
  }
473
527
  /**
@@ -573,6 +627,7 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
573
627
 
574
628
  // src/core/GPUMachineClient.ts
575
629
  var PING_INTERVAL_MS = 5e3;
630
+ var STATS_INTERVAL_MS = 2e3;
576
631
  var GPUMachineClient = class {
577
632
  constructor(config) {
578
633
  this.eventListeners = /* @__PURE__ */ new Map();
@@ -656,6 +711,7 @@ var GPUMachineClient = class {
656
711
  disconnect() {
657
712
  return __async(this, null, function* () {
658
713
  this.stopPing();
714
+ this.stopStatsPolling();
659
715
  if (this.publishedTrack) {
660
716
  yield this.unpublishTrack();
661
717
  }
@@ -812,6 +868,31 @@ var GPUMachineClient = class {
812
868
  }
813
869
  }
814
870
  // ─────────────────────────────────────────────────────────────────────────────
871
+ // Stats Polling (RTT)
872
+ // ─────────────────────────────────────────────────────────────────────────────
873
+ getStats() {
874
+ return this.stats;
875
+ }
876
+ startStatsPolling() {
877
+ this.stopStatsPolling();
878
+ this.statsInterval = setInterval(() => __async(this, null, function* () {
879
+ if (!this.peerConnection) return;
880
+ try {
881
+ const report = yield this.peerConnection.getStats();
882
+ this.stats = extractConnectionStats(report);
883
+ this.emit("statsUpdate", this.stats);
884
+ } catch (e) {
885
+ }
886
+ }), STATS_INTERVAL_MS);
887
+ }
888
+ stopStatsPolling() {
889
+ if (this.statsInterval !== void 0) {
890
+ clearInterval(this.statsInterval);
891
+ this.statsInterval = void 0;
892
+ }
893
+ this.stats = void 0;
894
+ }
895
+ // ─────────────────────────────────────────────────────────────────────────────
815
896
  // Private Helpers
816
897
  // ─────────────────────────────────────────────────────────────────────────────
817
898
  setStatus(newStatus) {
@@ -830,6 +911,7 @@ var GPUMachineClient = class {
830
911
  switch (state) {
831
912
  case "connected":
832
913
  this.setStatus("connected");
914
+ this.startStatsPolling();
833
915
  break;
834
916
  case "disconnected":
835
917
  case "closed":
@@ -1011,8 +1093,9 @@ var Reactor = class {
1011
1093
  }
1012
1094
  /**
1013
1095
  * Public method for reconnecting to an existing session, that may have been interrupted but can be recovered.
1096
+ * @param options Optional connect options (e.g. maxAttempts for SDP polling)
1014
1097
  */
1015
- reconnect() {
1098
+ reconnect(options) {
1016
1099
  return __async(this, null, function* () {
1017
1100
  if (!this.sessionId || !this.coordinatorClient) {
1018
1101
  console.warn("[Reactor] No active session to reconnect to.");
@@ -1032,7 +1115,8 @@ var Reactor = class {
1032
1115
  try {
1033
1116
  const sdpAnswer = yield this.coordinatorClient.connect(
1034
1117
  this.sessionId,
1035
- sdpOffer
1118
+ sdpOffer,
1119
+ options == null ? void 0 : options.maxAttempts
1036
1120
  );
1037
1121
  yield this.machineClient.connect(sdpAnswer);
1038
1122
  this.setStatus("ready");
@@ -1056,8 +1140,10 @@ var Reactor = class {
1056
1140
  * Connects to the coordinator and waits for a GPU to be assigned.
1057
1141
  * Once a GPU is assigned, the Reactor will connect to the gpu machine via WebRTC.
1058
1142
  * If no authentication is provided and not in local mode, an error is thrown.
1143
+ * @param jwtToken Optional JWT token for authentication
1144
+ * @param options Optional connect options (e.g. maxAttempts for SDP polling)
1059
1145
  */
1060
- connect(jwtToken) {
1146
+ connect(jwtToken, options) {
1061
1147
  return __async(this, null, function* () {
1062
1148
  console.debug("[Reactor] Connecting, status:", this.status);
1063
1149
  if (jwtToken == void 0 && !this.local) {
@@ -1074,7 +1160,7 @@ var Reactor = class {
1074
1160
  this.coordinatorClient = this.local ? new LocalCoordinatorClient(this.coordinatorUrl) : new CoordinatorClient({
1075
1161
  baseUrl: this.coordinatorUrl,
1076
1162
  jwtToken,
1077
- // Safe: validated on line 186-188
1163
+ // Safe: validated above
1078
1164
  model: this.model
1079
1165
  });
1080
1166
  const iceServers = yield this.coordinatorClient.getIceServers();
@@ -1083,7 +1169,11 @@ var Reactor = class {
1083
1169
  const sdpOffer = yield this.machineClient.createOffer();
1084
1170
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1085
1171
  this.setSessionId(sessionId);
1086
- const sdpAnswer = yield this.coordinatorClient.connect(sessionId);
1172
+ const sdpAnswer = yield this.coordinatorClient.connect(
1173
+ sessionId,
1174
+ void 0,
1175
+ options == null ? void 0 : options.maxAttempts
1176
+ );
1087
1177
  yield this.machineClient.connect(sdpAnswer);
1088
1178
  } catch (error) {
1089
1179
  console.error("[Reactor] Connection failed:", error);
@@ -1093,7 +1183,14 @@ var Reactor = class {
1093
1183
  "coordinator",
1094
1184
  true
1095
1185
  );
1096
- this.setStatus("disconnected");
1186
+ try {
1187
+ yield this.disconnect(false);
1188
+ } catch (disconnectError) {
1189
+ console.error(
1190
+ "[Reactor] Failed to clean up after connection failure:",
1191
+ disconnectError
1192
+ );
1193
+ }
1097
1194
  throw error;
1098
1195
  }
1099
1196
  });
@@ -1104,7 +1201,11 @@ var Reactor = class {
1104
1201
  setupMachineClientHandlers() {
1105
1202
  if (!this.machineClient) return;
1106
1203
  this.machineClient.on("message", (message, scope) => {
1107
- this.emit("newMessage", message, scope);
1204
+ if (scope === "application") {
1205
+ this.emit("message", message);
1206
+ } else if (scope === "runtime") {
1207
+ this.emit("runtimeMessage", message);
1208
+ }
1108
1209
  });
1109
1210
  this.machineClient.on("statusChanged", (status) => {
1110
1211
  switch (status) {
@@ -1131,6 +1232,9 @@ var Reactor = class {
1131
1232
  this.emit("streamChanged", track, stream);
1132
1233
  }
1133
1234
  );
1235
+ this.machineClient.on("statsUpdate", (stats) => {
1236
+ this.emit("statsUpdate", stats);
1237
+ });
1134
1238
  }
1135
1239
  /**
1136
1240
  * Disconnects from the coordinator and the gpu machine.
@@ -1217,6 +1321,10 @@ var Reactor = class {
1217
1321
  getLastError() {
1218
1322
  return this.lastError;
1219
1323
  }
1324
+ getStats() {
1325
+ var _a;
1326
+ return (_a = this.machineClient) == null ? void 0 : _a.getStats();
1327
+ }
1220
1328
  /**
1221
1329
  * Create and store an error
1222
1330
  */
@@ -1305,10 +1413,10 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1305
1413
  // actions
1306
1414
  onMessage: (handler) => {
1307
1415
  console.debug("[ReactorStore] Registering message handler");
1308
- get().internal.reactor.on("newMessage", handler);
1416
+ get().internal.reactor.on("message", handler);
1309
1417
  return () => {
1310
1418
  console.debug("[ReactorStore] Cleaning up message handler");
1311
- get().internal.reactor.off("newMessage", handler);
1419
+ get().internal.reactor.off("message", handler);
1312
1420
  };
1313
1421
  },
1314
1422
  sendCommand: (command, data, scope) => __async(null, null, function* () {
@@ -1325,13 +1433,13 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1325
1433
  throw error;
1326
1434
  }
1327
1435
  }),
1328
- connect: (jwtToken) => __async(null, null, function* () {
1436
+ connect: (jwtToken, options) => __async(null, null, function* () {
1329
1437
  if (jwtToken === void 0) {
1330
1438
  jwtToken = get().jwtToken;
1331
1439
  }
1332
1440
  console.debug("[ReactorStore] Connect called.");
1333
1441
  try {
1334
- yield get().internal.reactor.connect(jwtToken);
1442
+ yield get().internal.reactor.connect(jwtToken, options);
1335
1443
  console.debug("[ReactorStore] Connect completed successfully");
1336
1444
  } catch (error) {
1337
1445
  console.error("[ReactorStore] Connect failed:", error);
@@ -1376,10 +1484,10 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1376
1484
  throw error;
1377
1485
  }
1378
1486
  }),
1379
- reconnect: () => __async(null, null, function* () {
1487
+ reconnect: (options) => __async(null, null, function* () {
1380
1488
  console.debug("[ReactorStore] Reconnecting");
1381
1489
  try {
1382
- yield get().internal.reactor.reconnect();
1490
+ yield get().internal.reactor.reconnect(options);
1383
1491
  console.debug("[ReactorStore] Reconnect completed successfully");
1384
1492
  } catch (error) {
1385
1493
  console.error("[ReactorStore] Failed to reconnect:", error);
@@ -1396,11 +1504,11 @@ import { jsx } from "react/jsx-runtime";
1396
1504
  function ReactorProvider(_a) {
1397
1505
  var _b = _a, {
1398
1506
  children,
1399
- autoConnect = true,
1507
+ connectOptions,
1400
1508
  jwtToken
1401
1509
  } = _b, props = __objRest(_b, [
1402
1510
  "children",
1403
- "autoConnect",
1511
+ "connectOptions",
1404
1512
  "jwtToken"
1405
1513
  ]);
1406
1514
  const storeRef = useRef(void 0);
@@ -1415,14 +1523,16 @@ function ReactorProvider(_a) {
1415
1523
  );
1416
1524
  console.debug("[ReactorProvider] Reactor store created successfully");
1417
1525
  }
1526
+ const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
1418
1527
  const { coordinatorUrl, modelName, local } = props;
1528
+ const maxAttempts = pollingOptions.maxAttempts;
1419
1529
  useEffect(() => {
1420
1530
  const handleBeforeUnload = () => {
1421
- var _a2;
1531
+ var _a3;
1422
1532
  console.debug(
1423
1533
  "[ReactorProvider] Page unloading, performing non-recoverable disconnect"
1424
1534
  );
1425
- (_a2 = storeRef.current) == null ? void 0 : _a2.getState().internal.reactor.disconnect(false);
1535
+ (_a3 = storeRef.current) == null ? void 0 : _a3.getState().internal.reactor.disconnect(false);
1426
1536
  };
1427
1537
  window.addEventListener("beforeunload", handleBeforeUnload);
1428
1538
  return () => {
@@ -1437,7 +1547,7 @@ function ReactorProvider(_a) {
1437
1547
  console.debug(
1438
1548
  "[ReactorProvider] Starting autoconnect in first render..."
1439
1549
  );
1440
- current2.getState().connect(jwtToken).then(() => {
1550
+ current2.getState().connect(jwtToken, pollingOptions).then(() => {
1441
1551
  console.debug(
1442
1552
  "[ReactorProvider] Autoconnect successful in first render"
1443
1553
  );
@@ -1480,7 +1590,7 @@ function ReactorProvider(_a) {
1480
1590
  );
1481
1591
  if (autoConnect && current.getState().status === "disconnected" && jwtToken) {
1482
1592
  console.debug("[ReactorProvider] Starting autoconnect...");
1483
- current.getState().connect(jwtToken).then(() => {
1593
+ current.getState().connect(jwtToken, pollingOptions).then(() => {
1484
1594
  console.debug("[ReactorProvider] Autoconnect successful");
1485
1595
  }).catch((error) => {
1486
1596
  console.error("[ReactorProvider] Failed to autoconnect:", error);
@@ -1496,7 +1606,7 @@ function ReactorProvider(_a) {
1496
1606
  console.error("[ReactorProvider] Failed to disconnect:", error);
1497
1607
  });
1498
1608
  };
1499
- }, [coordinatorUrl, modelName, autoConnect, local, jwtToken]);
1609
+ }, [coordinatorUrl, modelName, autoConnect, local, jwtToken, maxAttempts]);
1500
1610
  return /* @__PURE__ */ jsx(ReactorContext.Provider, { value: storeRef.current, children });
1501
1611
  }
1502
1612
  function useReactorStore(selector) {
@@ -1509,7 +1619,7 @@ function useReactorStore(selector) {
1509
1619
 
1510
1620
  // src/react/hooks.ts
1511
1621
  import { useShallow } from "zustand/shallow";
1512
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
1622
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
1513
1623
  function useReactor(selector) {
1514
1624
  return useReactorStore(useShallow(selector));
1515
1625
  }
@@ -1520,21 +1630,45 @@ function useReactorMessage(handler) {
1520
1630
  handlerRef.current = handler;
1521
1631
  }, [handler]);
1522
1632
  useEffect2(() => {
1523
- console.debug("[useReactorMessage] Setting up message subscription");
1524
- const stableHandler = (message, scope) => {
1525
- console.debug("[useReactorMessage] Message received", {
1526
- message,
1527
- scope
1528
- });
1529
- handlerRef.current(message, scope);
1633
+ const stableHandler = (message) => {
1634
+ handlerRef.current(message);
1635
+ };
1636
+ reactor.on("message", stableHandler);
1637
+ return () => {
1638
+ reactor.off("message", stableHandler);
1639
+ };
1640
+ }, [reactor]);
1641
+ }
1642
+ function useReactorInternalMessage(handler) {
1643
+ const reactor = useReactor((state) => state.internal.reactor);
1644
+ const handlerRef = useRef2(handler);
1645
+ useEffect2(() => {
1646
+ handlerRef.current = handler;
1647
+ }, [handler]);
1648
+ useEffect2(() => {
1649
+ const stableHandler = (message) => {
1650
+ handlerRef.current(message);
1651
+ };
1652
+ reactor.on("runtimeMessage", stableHandler);
1653
+ return () => {
1654
+ reactor.off("runtimeMessage", stableHandler);
1655
+ };
1656
+ }, [reactor]);
1657
+ }
1658
+ function useStats() {
1659
+ const reactor = useReactor((state) => state.internal.reactor);
1660
+ const [stats, setStats] = useState2(void 0);
1661
+ useEffect2(() => {
1662
+ const handler = (newStats) => {
1663
+ setStats(newStats);
1530
1664
  };
1531
- reactor.on("newMessage", stableHandler);
1532
- console.debug("[useReactorMessage] Message handler registered");
1665
+ reactor.on("statsUpdate", handler);
1533
1666
  return () => {
1534
- console.debug("[useReactorMessage] Cleaning up message subscription");
1535
- reactor.off("newMessage", stableHandler);
1667
+ reactor.off("statsUpdate", handler);
1668
+ setStats(void 0);
1536
1669
  };
1537
1670
  }, [reactor]);
1671
+ return stats;
1538
1672
  }
1539
1673
 
1540
1674
  // src/react/ReactorView.tsx
@@ -1633,7 +1767,7 @@ function ReactorView({
1633
1767
  }
1634
1768
 
1635
1769
  // src/react/ReactorController.tsx
1636
- import React, { useState as useState2, useCallback } from "react";
1770
+ import React, { useState as useState3, useCallback } from "react";
1637
1771
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1638
1772
  function ReactorController({
1639
1773
  className,
@@ -1643,9 +1777,9 @@ function ReactorController({
1643
1777
  sendCommand: state.sendCommand,
1644
1778
  status: state.status
1645
1779
  }));
1646
- const [commands, setCommands] = useState2({});
1647
- const [formValues, setFormValues] = useState2({});
1648
- const [expandedCommands, setExpandedCommands] = useState2({});
1780
+ const [commands, setCommands] = useState3({});
1781
+ const [formValues, setFormValues] = useState3({});
1782
+ const [expandedCommands, setExpandedCommands] = useState3({});
1649
1783
  React.useEffect(() => {
1650
1784
  if (status === "disconnected") {
1651
1785
  setCommands({});
@@ -1672,8 +1806,8 @@ function ReactorController({
1672
1806
  }, 5e3);
1673
1807
  return () => clearInterval(interval);
1674
1808
  }, [status, commands, requestCapabilities]);
1675
- useReactorMessage((message, scope) => {
1676
- if (scope === "runtime" && message && typeof message === "object" && message.type === "modelCapabilities" && message.data && "commands" in message.data) {
1809
+ useReactorInternalMessage((message) => {
1810
+ if (message && typeof message === "object" && message.type === "modelCapabilities" && message.data && "commands" in message.data) {
1677
1811
  const commandsMessage = message.data;
1678
1812
  setCommands(commandsMessage.commands);
1679
1813
  const initialValues = {};
@@ -2086,7 +2220,7 @@ function ReactorController({
2086
2220
  }
2087
2221
 
2088
2222
  // src/react/WebcamStream.tsx
2089
- import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
2223
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
2090
2224
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2091
2225
  function WebcamStream({
2092
2226
  className,
@@ -2098,9 +2232,9 @@ function WebcamStream({
2098
2232
  showWebcam = true,
2099
2233
  videoObjectFit = "contain"
2100
2234
  }) {
2101
- const [stream, setStream] = useState3(null);
2102
- const [isPublishing, setIsPublishing] = useState3(false);
2103
- const [permissionDenied, setPermissionDenied] = useState3(false);
2235
+ const [stream, setStream] = useState4(null);
2236
+ const [isPublishing, setIsPublishing] = useState4(false);
2237
+ const [permissionDenied, setPermissionDenied] = useState4(false);
2104
2238
  const { status, publishVideoStream, unpublishVideoStream, reactor } = useReactor((state) => ({
2105
2239
  status: state.status,
2106
2240
  publishVideoStream: state.publishVideoStream,
@@ -2305,7 +2439,9 @@ export {
2305
2439
  WebcamStream,
2306
2440
  fetchInsecureJwtToken,
2307
2441
  useReactor,
2442
+ useReactorInternalMessage,
2308
2443
  useReactorMessage,
2309
- useReactorStore
2444
+ useReactorStore,
2445
+ useStats
2310
2446
  };
2311
2447
  //# sourceMappingURL=index.mjs.map