@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.js CHANGED
@@ -88,8 +88,10 @@ __export(index_exports, {
88
88
  WebcamStream: () => WebcamStream,
89
89
  fetchInsecureJwtToken: () => fetchInsecureJwtToken,
90
90
  useReactor: () => useReactor,
91
+ useReactorInternalMessage: () => useReactorInternalMessage,
91
92
  useReactorMessage: () => useReactorMessage,
92
- useReactorStore: () => useReactorStore
93
+ useReactorStore: () => useReactorStore,
94
+ useStats: () => useStats
93
95
  });
94
96
  module.exports = __toCommonJS(index_exports);
95
97
 
@@ -249,11 +251,58 @@ function parseMessage(data) {
249
251
  function closePeerConnection(pc) {
250
252
  pc.close();
251
253
  }
254
+ function extractConnectionStats(report) {
255
+ let rtt;
256
+ let availableOutgoingBitrate;
257
+ let localCandidateId;
258
+ let framesPerSecond;
259
+ let jitter;
260
+ let packetLossRatio;
261
+ report.forEach((stat) => {
262
+ if (stat.type === "candidate-pair" && stat.state === "succeeded") {
263
+ if (stat.currentRoundTripTime !== void 0) {
264
+ rtt = stat.currentRoundTripTime * 1e3;
265
+ }
266
+ if (stat.availableOutgoingBitrate !== void 0) {
267
+ availableOutgoingBitrate = stat.availableOutgoingBitrate;
268
+ }
269
+ localCandidateId = stat.localCandidateId;
270
+ }
271
+ if (stat.type === "inbound-rtp" && stat.kind === "video") {
272
+ if (stat.framesPerSecond !== void 0) {
273
+ framesPerSecond = stat.framesPerSecond;
274
+ }
275
+ if (stat.jitter !== void 0) {
276
+ jitter = stat.jitter;
277
+ }
278
+ if (stat.packetsReceived !== void 0 && stat.packetsLost !== void 0 && stat.packetsReceived + stat.packetsLost > 0) {
279
+ packetLossRatio = stat.packetsLost / (stat.packetsReceived + stat.packetsLost);
280
+ }
281
+ }
282
+ });
283
+ let candidateType;
284
+ if (localCandidateId) {
285
+ const localCandidate = report.get(localCandidateId);
286
+ if (localCandidate == null ? void 0 : localCandidate.candidateType) {
287
+ candidateType = localCandidate.candidateType;
288
+ }
289
+ }
290
+ return {
291
+ rtt,
292
+ candidateType,
293
+ availableOutgoingBitrate,
294
+ framesPerSecond,
295
+ packetLossRatio,
296
+ jitter,
297
+ timestamp: Date.now()
298
+ };
299
+ }
252
300
 
253
301
  // src/core/CoordinatorClient.ts
254
302
  var INITIAL_BACKOFF_MS = 500;
255
- var MAX_BACKOFF_MS = 3e4;
303
+ var MAX_BACKOFF_MS = 15e3;
256
304
  var BACKOFF_MULTIPLIER = 2;
305
+ var DEFAULT_MAX_ATTEMPTS = 6;
257
306
  var CoordinatorClient = class {
258
307
  constructor(options) {
259
308
  this.baseUrl = options.baseUrl;
@@ -446,13 +495,14 @@ var CoordinatorClient = class {
446
495
  });
447
496
  }
448
497
  /**
449
- * Polls for the SDP answer with geometric backoff.
498
+ * Polls for the SDP answer with exponential backoff.
450
499
  * Used for async reconnection when the answer is not immediately available.
451
500
  * @param sessionId - The session ID to poll for
501
+ * @param maxAttempts - Optional maximum number of polling attempts before giving up
452
502
  * @returns The SDP answer from the server
453
503
  */
454
- pollSdpAnswer(sessionId) {
455
- return __async(this, null, function* () {
504
+ pollSdpAnswer(_0) {
505
+ return __async(this, arguments, function* (sessionId, maxAttempts = DEFAULT_MAX_ATTEMPTS) {
456
506
  console.debug(
457
507
  "[CoordinatorClient] Polling for SDP answer for session:",
458
508
  sessionId
@@ -460,9 +510,14 @@ var CoordinatorClient = class {
460
510
  let backoffMs = INITIAL_BACKOFF_MS;
461
511
  let attempt = 0;
462
512
  while (true) {
513
+ if (attempt >= maxAttempts) {
514
+ throw new Error(
515
+ `SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
516
+ );
517
+ }
463
518
  attempt++;
464
519
  console.debug(
465
- `[CoordinatorClient] SDP poll attempt ${attempt} for session ${sessionId}`
520
+ `[CoordinatorClient] SDP poll attempt ${attempt}/${maxAttempts} for session ${sessionId}`
466
521
  );
467
522
  const response = yield fetch(
468
523
  `${this.baseUrl}/sessions/${sessionId}/sdp_params`,
@@ -499,9 +554,10 @@ var CoordinatorClient = class {
499
554
  * falls back to polling. If no sdpOffer is provided, goes directly to polling.
500
555
  * @param sessionId - The session ID to connect to
501
556
  * @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
557
+ * @param maxAttempts - Optional maximum number of polling attempts before giving up
502
558
  * @returns The SDP answer from the server
503
559
  */
504
- connect(sessionId, sdpOffer) {
560
+ connect(sessionId, sdpOffer, maxAttempts) {
505
561
  return __async(this, null, function* () {
506
562
  console.debug("[CoordinatorClient] Connecting to session:", sessionId);
507
563
  if (sdpOffer) {
@@ -510,7 +566,7 @@ var CoordinatorClient = class {
510
566
  return answer;
511
567
  }
512
568
  }
513
- return this.pollSdpAnswer(sessionId);
569
+ return this.pollSdpAnswer(sessionId, maxAttempts);
514
570
  });
515
571
  }
516
572
  /**
@@ -616,6 +672,7 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
616
672
 
617
673
  // src/core/GPUMachineClient.ts
618
674
  var PING_INTERVAL_MS = 5e3;
675
+ var STATS_INTERVAL_MS = 2e3;
619
676
  var GPUMachineClient = class {
620
677
  constructor(config) {
621
678
  this.eventListeners = /* @__PURE__ */ new Map();
@@ -699,6 +756,7 @@ var GPUMachineClient = class {
699
756
  disconnect() {
700
757
  return __async(this, null, function* () {
701
758
  this.stopPing();
759
+ this.stopStatsPolling();
702
760
  if (this.publishedTrack) {
703
761
  yield this.unpublishTrack();
704
762
  }
@@ -855,6 +913,31 @@ var GPUMachineClient = class {
855
913
  }
856
914
  }
857
915
  // ─────────────────────────────────────────────────────────────────────────────
916
+ // Stats Polling (RTT)
917
+ // ─────────────────────────────────────────────────────────────────────────────
918
+ getStats() {
919
+ return this.stats;
920
+ }
921
+ startStatsPolling() {
922
+ this.stopStatsPolling();
923
+ this.statsInterval = setInterval(() => __async(this, null, function* () {
924
+ if (!this.peerConnection) return;
925
+ try {
926
+ const report = yield this.peerConnection.getStats();
927
+ this.stats = extractConnectionStats(report);
928
+ this.emit("statsUpdate", this.stats);
929
+ } catch (e) {
930
+ }
931
+ }), STATS_INTERVAL_MS);
932
+ }
933
+ stopStatsPolling() {
934
+ if (this.statsInterval !== void 0) {
935
+ clearInterval(this.statsInterval);
936
+ this.statsInterval = void 0;
937
+ }
938
+ this.stats = void 0;
939
+ }
940
+ // ─────────────────────────────────────────────────────────────────────────────
858
941
  // Private Helpers
859
942
  // ─────────────────────────────────────────────────────────────────────────────
860
943
  setStatus(newStatus) {
@@ -873,6 +956,7 @@ var GPUMachineClient = class {
873
956
  switch (state) {
874
957
  case "connected":
875
958
  this.setStatus("connected");
959
+ this.startStatsPolling();
876
960
  break;
877
961
  case "disconnected":
878
962
  case "closed":
@@ -1054,8 +1138,9 @@ var Reactor = class {
1054
1138
  }
1055
1139
  /**
1056
1140
  * Public method for reconnecting to an existing session, that may have been interrupted but can be recovered.
1141
+ * @param options Optional connect options (e.g. maxAttempts for SDP polling)
1057
1142
  */
1058
- reconnect() {
1143
+ reconnect(options) {
1059
1144
  return __async(this, null, function* () {
1060
1145
  if (!this.sessionId || !this.coordinatorClient) {
1061
1146
  console.warn("[Reactor] No active session to reconnect to.");
@@ -1075,7 +1160,8 @@ var Reactor = class {
1075
1160
  try {
1076
1161
  const sdpAnswer = yield this.coordinatorClient.connect(
1077
1162
  this.sessionId,
1078
- sdpOffer
1163
+ sdpOffer,
1164
+ options == null ? void 0 : options.maxAttempts
1079
1165
  );
1080
1166
  yield this.machineClient.connect(sdpAnswer);
1081
1167
  this.setStatus("ready");
@@ -1099,8 +1185,10 @@ var Reactor = class {
1099
1185
  * Connects to the coordinator and waits for a GPU to be assigned.
1100
1186
  * Once a GPU is assigned, the Reactor will connect to the gpu machine via WebRTC.
1101
1187
  * If no authentication is provided and not in local mode, an error is thrown.
1188
+ * @param jwtToken Optional JWT token for authentication
1189
+ * @param options Optional connect options (e.g. maxAttempts for SDP polling)
1102
1190
  */
1103
- connect(jwtToken) {
1191
+ connect(jwtToken, options) {
1104
1192
  return __async(this, null, function* () {
1105
1193
  console.debug("[Reactor] Connecting, status:", this.status);
1106
1194
  if (jwtToken == void 0 && !this.local) {
@@ -1117,7 +1205,7 @@ var Reactor = class {
1117
1205
  this.coordinatorClient = this.local ? new LocalCoordinatorClient(this.coordinatorUrl) : new CoordinatorClient({
1118
1206
  baseUrl: this.coordinatorUrl,
1119
1207
  jwtToken,
1120
- // Safe: validated on line 186-188
1208
+ // Safe: validated above
1121
1209
  model: this.model
1122
1210
  });
1123
1211
  const iceServers = yield this.coordinatorClient.getIceServers();
@@ -1126,7 +1214,11 @@ var Reactor = class {
1126
1214
  const sdpOffer = yield this.machineClient.createOffer();
1127
1215
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1128
1216
  this.setSessionId(sessionId);
1129
- const sdpAnswer = yield this.coordinatorClient.connect(sessionId);
1217
+ const sdpAnswer = yield this.coordinatorClient.connect(
1218
+ sessionId,
1219
+ void 0,
1220
+ options == null ? void 0 : options.maxAttempts
1221
+ );
1130
1222
  yield this.machineClient.connect(sdpAnswer);
1131
1223
  } catch (error) {
1132
1224
  console.error("[Reactor] Connection failed:", error);
@@ -1136,7 +1228,14 @@ var Reactor = class {
1136
1228
  "coordinator",
1137
1229
  true
1138
1230
  );
1139
- this.setStatus("disconnected");
1231
+ try {
1232
+ yield this.disconnect(false);
1233
+ } catch (disconnectError) {
1234
+ console.error(
1235
+ "[Reactor] Failed to clean up after connection failure:",
1236
+ disconnectError
1237
+ );
1238
+ }
1140
1239
  throw error;
1141
1240
  }
1142
1241
  });
@@ -1147,7 +1246,11 @@ var Reactor = class {
1147
1246
  setupMachineClientHandlers() {
1148
1247
  if (!this.machineClient) return;
1149
1248
  this.machineClient.on("message", (message, scope) => {
1150
- this.emit("newMessage", message, scope);
1249
+ if (scope === "application") {
1250
+ this.emit("message", message);
1251
+ } else if (scope === "runtime") {
1252
+ this.emit("runtimeMessage", message);
1253
+ }
1151
1254
  });
1152
1255
  this.machineClient.on("statusChanged", (status) => {
1153
1256
  switch (status) {
@@ -1174,6 +1277,9 @@ var Reactor = class {
1174
1277
  this.emit("streamChanged", track, stream);
1175
1278
  }
1176
1279
  );
1280
+ this.machineClient.on("statsUpdate", (stats) => {
1281
+ this.emit("statsUpdate", stats);
1282
+ });
1177
1283
  }
1178
1284
  /**
1179
1285
  * Disconnects from the coordinator and the gpu machine.
@@ -1260,6 +1366,10 @@ var Reactor = class {
1260
1366
  getLastError() {
1261
1367
  return this.lastError;
1262
1368
  }
1369
+ getStats() {
1370
+ var _a;
1371
+ return (_a = this.machineClient) == null ? void 0 : _a.getStats();
1372
+ }
1263
1373
  /**
1264
1374
  * Create and store an error
1265
1375
  */
@@ -1348,10 +1458,10 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1348
1458
  // actions
1349
1459
  onMessage: (handler) => {
1350
1460
  console.debug("[ReactorStore] Registering message handler");
1351
- get().internal.reactor.on("newMessage", handler);
1461
+ get().internal.reactor.on("message", handler);
1352
1462
  return () => {
1353
1463
  console.debug("[ReactorStore] Cleaning up message handler");
1354
- get().internal.reactor.off("newMessage", handler);
1464
+ get().internal.reactor.off("message", handler);
1355
1465
  };
1356
1466
  },
1357
1467
  sendCommand: (command, data, scope) => __async(null, null, function* () {
@@ -1368,13 +1478,13 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1368
1478
  throw error;
1369
1479
  }
1370
1480
  }),
1371
- connect: (jwtToken) => __async(null, null, function* () {
1481
+ connect: (jwtToken, options) => __async(null, null, function* () {
1372
1482
  if (jwtToken === void 0) {
1373
1483
  jwtToken = get().jwtToken;
1374
1484
  }
1375
1485
  console.debug("[ReactorStore] Connect called.");
1376
1486
  try {
1377
- yield get().internal.reactor.connect(jwtToken);
1487
+ yield get().internal.reactor.connect(jwtToken, options);
1378
1488
  console.debug("[ReactorStore] Connect completed successfully");
1379
1489
  } catch (error) {
1380
1490
  console.error("[ReactorStore] Connect failed:", error);
@@ -1419,10 +1529,10 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1419
1529
  throw error;
1420
1530
  }
1421
1531
  }),
1422
- reconnect: () => __async(null, null, function* () {
1532
+ reconnect: (options) => __async(null, null, function* () {
1423
1533
  console.debug("[ReactorStore] Reconnecting");
1424
1534
  try {
1425
- yield get().internal.reactor.reconnect();
1535
+ yield get().internal.reactor.reconnect(options);
1426
1536
  console.debug("[ReactorStore] Reconnect completed successfully");
1427
1537
  } catch (error) {
1428
1538
  console.error("[ReactorStore] Failed to reconnect:", error);
@@ -1439,11 +1549,11 @@ var import_jsx_runtime = require("react/jsx-runtime");
1439
1549
  function ReactorProvider(_a) {
1440
1550
  var _b = _a, {
1441
1551
  children,
1442
- autoConnect = true,
1552
+ connectOptions,
1443
1553
  jwtToken
1444
1554
  } = _b, props = __objRest(_b, [
1445
1555
  "children",
1446
- "autoConnect",
1556
+ "connectOptions",
1447
1557
  "jwtToken"
1448
1558
  ]);
1449
1559
  const storeRef = (0, import_react3.useRef)(void 0);
@@ -1458,14 +1568,16 @@ function ReactorProvider(_a) {
1458
1568
  );
1459
1569
  console.debug("[ReactorProvider] Reactor store created successfully");
1460
1570
  }
1571
+ const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
1461
1572
  const { coordinatorUrl, modelName, local } = props;
1573
+ const maxAttempts = pollingOptions.maxAttempts;
1462
1574
  (0, import_react3.useEffect)(() => {
1463
1575
  const handleBeforeUnload = () => {
1464
- var _a2;
1576
+ var _a3;
1465
1577
  console.debug(
1466
1578
  "[ReactorProvider] Page unloading, performing non-recoverable disconnect"
1467
1579
  );
1468
- (_a2 = storeRef.current) == null ? void 0 : _a2.getState().internal.reactor.disconnect(false);
1580
+ (_a3 = storeRef.current) == null ? void 0 : _a3.getState().internal.reactor.disconnect(false);
1469
1581
  };
1470
1582
  window.addEventListener("beforeunload", handleBeforeUnload);
1471
1583
  return () => {
@@ -1480,7 +1592,7 @@ function ReactorProvider(_a) {
1480
1592
  console.debug(
1481
1593
  "[ReactorProvider] Starting autoconnect in first render..."
1482
1594
  );
1483
- current2.getState().connect(jwtToken).then(() => {
1595
+ current2.getState().connect(jwtToken, pollingOptions).then(() => {
1484
1596
  console.debug(
1485
1597
  "[ReactorProvider] Autoconnect successful in first render"
1486
1598
  );
@@ -1523,7 +1635,7 @@ function ReactorProvider(_a) {
1523
1635
  );
1524
1636
  if (autoConnect && current.getState().status === "disconnected" && jwtToken) {
1525
1637
  console.debug("[ReactorProvider] Starting autoconnect...");
1526
- current.getState().connect(jwtToken).then(() => {
1638
+ current.getState().connect(jwtToken, pollingOptions).then(() => {
1527
1639
  console.debug("[ReactorProvider] Autoconnect successful");
1528
1640
  }).catch((error) => {
1529
1641
  console.error("[ReactorProvider] Failed to autoconnect:", error);
@@ -1539,7 +1651,7 @@ function ReactorProvider(_a) {
1539
1651
  console.error("[ReactorProvider] Failed to disconnect:", error);
1540
1652
  });
1541
1653
  };
1542
- }, [coordinatorUrl, modelName, autoConnect, local, jwtToken]);
1654
+ }, [coordinatorUrl, modelName, autoConnect, local, jwtToken, maxAttempts]);
1543
1655
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReactorContext.Provider, { value: storeRef.current, children });
1544
1656
  }
1545
1657
  function useReactorStore(selector) {
@@ -1563,21 +1675,45 @@ function useReactorMessage(handler) {
1563
1675
  handlerRef.current = handler;
1564
1676
  }, [handler]);
1565
1677
  (0, import_react4.useEffect)(() => {
1566
- console.debug("[useReactorMessage] Setting up message subscription");
1567
- const stableHandler = (message, scope) => {
1568
- console.debug("[useReactorMessage] Message received", {
1569
- message,
1570
- scope
1571
- });
1572
- handlerRef.current(message, scope);
1678
+ const stableHandler = (message) => {
1679
+ handlerRef.current(message);
1680
+ };
1681
+ reactor.on("message", stableHandler);
1682
+ return () => {
1683
+ reactor.off("message", stableHandler);
1684
+ };
1685
+ }, [reactor]);
1686
+ }
1687
+ function useReactorInternalMessage(handler) {
1688
+ const reactor = useReactor((state) => state.internal.reactor);
1689
+ const handlerRef = (0, import_react4.useRef)(handler);
1690
+ (0, import_react4.useEffect)(() => {
1691
+ handlerRef.current = handler;
1692
+ }, [handler]);
1693
+ (0, import_react4.useEffect)(() => {
1694
+ const stableHandler = (message) => {
1695
+ handlerRef.current(message);
1696
+ };
1697
+ reactor.on("runtimeMessage", stableHandler);
1698
+ return () => {
1699
+ reactor.off("runtimeMessage", stableHandler);
1700
+ };
1701
+ }, [reactor]);
1702
+ }
1703
+ function useStats() {
1704
+ const reactor = useReactor((state) => state.internal.reactor);
1705
+ const [stats, setStats] = (0, import_react4.useState)(void 0);
1706
+ (0, import_react4.useEffect)(() => {
1707
+ const handler = (newStats) => {
1708
+ setStats(newStats);
1573
1709
  };
1574
- reactor.on("newMessage", stableHandler);
1575
- console.debug("[useReactorMessage] Message handler registered");
1710
+ reactor.on("statsUpdate", handler);
1576
1711
  return () => {
1577
- console.debug("[useReactorMessage] Cleaning up message subscription");
1578
- reactor.off("newMessage", stableHandler);
1712
+ reactor.off("statsUpdate", handler);
1713
+ setStats(void 0);
1579
1714
  };
1580
1715
  }, [reactor]);
1716
+ return stats;
1581
1717
  }
1582
1718
 
1583
1719
  // src/react/ReactorView.tsx
@@ -1715,8 +1851,8 @@ function ReactorController({
1715
1851
  }, 5e3);
1716
1852
  return () => clearInterval(interval);
1717
1853
  }, [status, commands, requestCapabilities]);
1718
- useReactorMessage((message, scope) => {
1719
- if (scope === "runtime" && message && typeof message === "object" && message.type === "modelCapabilities" && message.data && "commands" in message.data) {
1854
+ useReactorInternalMessage((message) => {
1855
+ if (message && typeof message === "object" && message.type === "modelCapabilities" && message.data && "commands" in message.data) {
1720
1856
  const commandsMessage = message.data;
1721
1857
  setCommands(commandsMessage.commands);
1722
1858
  const initialValues = {};
@@ -2349,7 +2485,9 @@ function fetchInsecureJwtToken(_0) {
2349
2485
  WebcamStream,
2350
2486
  fetchInsecureJwtToken,
2351
2487
  useReactor,
2488
+ useReactorInternalMessage,
2352
2489
  useReactorMessage,
2353
- useReactorStore
2490
+ useReactorStore,
2491
+ useStats
2354
2492
  });
2355
2493
  //# sourceMappingURL=index.js.map