@reactor-team/js-sdk 2.5.1 → 2.6.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
@@ -129,6 +129,7 @@ var IceServersResponseSchema = z.object({
129
129
  // src/utils/webrtc.ts
130
130
  var DEFAULT_DATA_CHANNEL_LABEL = "data";
131
131
  var FORCE_RELAY_MODE = false;
132
+ var DEFAULT_MAX_MESSAGE_BYTES = 256 * 1024;
132
133
  function createPeerConnection(config) {
133
134
  return new RTCPeerConnection({
134
135
  iceServers: config.iceServers,
@@ -285,14 +286,21 @@ function waitForIceGathering(pc, timeoutMs = 5e3) {
285
286
  }, timeoutMs);
286
287
  });
287
288
  }
288
- function sendMessage(channel, command, data, scope = "application") {
289
+ function sendMessage(channel, command, data, scope = "application", maxBytes = DEFAULT_MAX_MESSAGE_BYTES) {
289
290
  if (channel.readyState !== "open") {
290
291
  throw new Error(`Data channel not open: ${channel.readyState}`);
291
292
  }
292
293
  const jsonData = typeof data === "string" ? JSON.parse(data) : data;
293
294
  const inner = { type: command, data: jsonData };
294
295
  const payload = { scope, data: inner };
295
- channel.send(JSON.stringify(payload));
296
+ const serialized = JSON.stringify(payload);
297
+ const byteLength = new TextEncoder().encode(serialized).byteLength;
298
+ if (byteLength > maxBytes) {
299
+ throw new Error(
300
+ `Data channel message too large: ${byteLength} bytes exceeds limit of ${maxBytes} bytes (command: "${command}")`
301
+ );
302
+ }
303
+ channel.send(serialized);
296
304
  }
297
305
  function parseMessage(data) {
298
306
  if (typeof data === "string") {
@@ -613,7 +621,7 @@ var CoordinatorClient = class {
613
621
  if (response.status === 200) {
614
622
  const answerData = yield response.json();
615
623
  console.debug("[CoordinatorClient] Received SDP answer via polling");
616
- return answerData.sdp_answer;
624
+ return { sdpAnswer: answerData.sdp_answer, attempts: attempt };
617
625
  }
618
626
  if (response.status === 202) {
619
627
  console.warn(
@@ -637,7 +645,7 @@ var CoordinatorClient = class {
637
645
  * @param sessionId - The session ID to connect to
638
646
  * @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
639
647
  * @param maxAttempts - Optional maximum number of polling attempts before giving up
640
- * @returns The SDP answer from the server
648
+ * @returns The SDP answer and the number of polling attempts made (0 if answered immediately via PUT)
641
649
  */
642
650
  connect(sessionId, sdpOffer, maxAttempts) {
643
651
  return __async(this, null, function* () {
@@ -645,10 +653,11 @@ var CoordinatorClient = class {
645
653
  if (sdpOffer) {
646
654
  const answer = yield this.sendSdpOffer(sessionId, sdpOffer);
647
655
  if (answer !== null) {
648
- return answer;
656
+ return { sdpAnswer: answer, sdpPollingAttempts: 0 };
649
657
  }
650
658
  }
651
- return this.pollSdpAnswer(sessionId, maxAttempts);
659
+ const result = yield this.pollSdpAnswer(sessionId, maxAttempts);
660
+ return { sdpAnswer: result.sdpAnswer, sdpPollingAttempts: result.attempts };
652
661
  });
653
662
  }
654
663
  /**
@@ -730,9 +739,10 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
730
739
  }
731
740
  /**
732
741
  * Connects to the local session by posting SDP params to /sdp_params.
742
+ * Local connections are always immediate (no polling).
733
743
  * @param sessionId - The session ID (ignored for local)
734
744
  * @param sdpMessage - The SDP offer from the local WebRTC peer connection
735
- * @returns The SDP answer from the server
745
+ * @returns The SDP answer and polling attempts (always 0 for local)
736
746
  */
737
747
  connect(sessionId, sdpMessage) {
738
748
  return __async(this, null, function* () {
@@ -758,7 +768,7 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
758
768
  }
759
769
  const sdpAnswer = yield response.json();
760
770
  console.debug("[LocalCoordinatorClient] Received SDP answer");
761
- return sdpAnswer.sdp;
771
+ return { sdpAnswer: sdpAnswer.sdp, sdpPollingAttempts: 0 };
762
772
  });
763
773
  }
764
774
  terminateSession() {
@@ -903,6 +913,9 @@ var GPUMachineClient = class {
903
913
  );
904
914
  }
905
915
  this.setStatus("connecting");
916
+ this.iceStartTime = performance.now();
917
+ this.iceNegotiationMs = void 0;
918
+ this.dataChannelMs = void 0;
906
919
  try {
907
920
  let answer = sdpAnswer;
908
921
  if (this.midMapping) {
@@ -942,6 +955,7 @@ var GPUMachineClient = class {
942
955
  this.midMapping = void 0;
943
956
  this.peerConnected = false;
944
957
  this.dataChannelOpen = false;
958
+ this.resetConnectionTimings();
945
959
  this.setStatus("disconnected");
946
960
  console.debug("[GPUMachineClient] Disconnected");
947
961
  });
@@ -966,6 +980,14 @@ var GPUMachineClient = class {
966
980
  // ─────────────────────────────────────────────────────────────────────────────
967
981
  // Messaging
968
982
  // ─────────────────────────────────────────────────────────────────────────────
983
+ /**
984
+ * Returns the negotiated SCTP max message size (bytes) if available,
985
+ * otherwise `undefined` so `sendMessage` falls back to its built-in default.
986
+ */
987
+ get maxMessageBytes() {
988
+ var _a, _b, _c;
989
+ return (_c = (_b = (_a = this.peerConnection) == null ? void 0 : _a.sctp) == null ? void 0 : _b.maxMessageSize) != null ? _c : void 0;
990
+ }
969
991
  /**
970
992
  * Sends a command to the GPU machine via the data channel.
971
993
  * @param command The command to send
@@ -977,7 +999,13 @@ var GPUMachineClient = class {
977
999
  throw new Error("[GPUMachineClient] Data channel not available");
978
1000
  }
979
1001
  try {
980
- sendMessage(this.dataChannel, command, data, scope);
1002
+ sendMessage(
1003
+ this.dataChannel,
1004
+ command,
1005
+ data,
1006
+ scope,
1007
+ this.maxMessageBytes
1008
+ );
981
1009
  } catch (error) {
982
1010
  console.warn("[GPUMachineClient] Failed to send message:", error);
983
1011
  }
@@ -1105,6 +1133,24 @@ var GPUMachineClient = class {
1105
1133
  getStats() {
1106
1134
  return this.stats;
1107
1135
  }
1136
+ /**
1137
+ * Returns the ICE/data-channel durations recorded during the last connect(),
1138
+ * or undefined if no connection has completed yet.
1139
+ */
1140
+ getConnectionTimings() {
1141
+ if (this.iceNegotiationMs == null || this.dataChannelMs == null) {
1142
+ return void 0;
1143
+ }
1144
+ return {
1145
+ iceNegotiationMs: this.iceNegotiationMs,
1146
+ dataChannelMs: this.dataChannelMs
1147
+ };
1148
+ }
1149
+ resetConnectionTimings() {
1150
+ this.iceStartTime = void 0;
1151
+ this.iceNegotiationMs = void 0;
1152
+ this.dataChannelMs = void 0;
1153
+ }
1108
1154
  startStatsPolling() {
1109
1155
  this.stopStatsPolling();
1110
1156
  this.statsInterval = setInterval(() => __async(this, null, function* () {
@@ -1148,6 +1194,9 @@ var GPUMachineClient = class {
1148
1194
  if (state) {
1149
1195
  switch (state) {
1150
1196
  case "connected":
1197
+ if (this.iceStartTime != null && this.iceNegotiationMs == null) {
1198
+ this.iceNegotiationMs = performance.now() - this.iceStartTime;
1199
+ }
1151
1200
  this.peerConnected = true;
1152
1201
  this.checkFullyConnected();
1153
1202
  break;
@@ -1197,6 +1246,9 @@ var GPUMachineClient = class {
1197
1246
  if (!this.dataChannel) return;
1198
1247
  this.dataChannel.onopen = () => {
1199
1248
  console.debug("[GPUMachineClient] Data channel open");
1249
+ if (this.iceStartTime != null && this.dataChannelMs == null) {
1250
+ this.dataChannelMs = performance.now() - this.iceStartTime;
1251
+ }
1200
1252
  this.dataChannelOpen = true;
1201
1253
  this.startPing();
1202
1254
  this.checkFullyConnected();
@@ -1236,13 +1288,13 @@ var GPUMachineClient = class {
1236
1288
  // src/core/Reactor.ts
1237
1289
  import { z as z2 } from "zod";
1238
1290
  var LOCAL_COORDINATOR_URL = "http://localhost:8080";
1239
- var PROD_COORDINATOR_URL = "https://api.reactor.inc";
1291
+ var DEFAULT_BASE_URL = "https://api.reactor.inc";
1240
1292
  var TrackConfigSchema = z2.object({
1241
1293
  name: z2.string(),
1242
1294
  kind: z2.enum(["audio", "video"])
1243
1295
  });
1244
1296
  var OptionsSchema = z2.object({
1245
- coordinatorUrl: z2.string().default(PROD_COORDINATOR_URL),
1297
+ apiUrl: z2.string().default(DEFAULT_BASE_URL),
1246
1298
  modelName: z2.string(),
1247
1299
  local: z2.boolean().default(false),
1248
1300
  /**
@@ -1267,12 +1319,12 @@ var Reactor = class {
1267
1319
  // Generic event map
1268
1320
  this.eventListeners = /* @__PURE__ */ new Map();
1269
1321
  const validatedOptions = OptionsSchema.parse(options);
1270
- this.coordinatorUrl = validatedOptions.coordinatorUrl;
1322
+ this.coordinatorUrl = validatedOptions.apiUrl;
1271
1323
  this.model = validatedOptions.modelName;
1272
1324
  this.local = validatedOptions.local;
1273
1325
  this.receive = validatedOptions.receive;
1274
1326
  this.send = validatedOptions.send;
1275
- if (this.local && options.coordinatorUrl === void 0) {
1327
+ if (this.local && options.apiUrl === void 0) {
1276
1328
  this.coordinatorUrl = LOCAL_COORDINATOR_URL;
1277
1329
  }
1278
1330
  }
@@ -1393,7 +1445,7 @@ var Reactor = class {
1393
1445
  receive: this.receive
1394
1446
  });
1395
1447
  try {
1396
- const sdpAnswer = yield this.coordinatorClient.connect(
1448
+ const { sdpAnswer } = yield this.coordinatorClient.connect(
1397
1449
  this.sessionId,
1398
1450
  sdpOffer,
1399
1451
  options == null ? void 0 : options.maxAttempts
@@ -1410,7 +1462,7 @@ var Reactor = class {
1410
1462
  this.createError(
1411
1463
  "RECONNECTION_FAILED",
1412
1464
  `Failed to reconnect: ${error}`,
1413
- "coordinator",
1465
+ "api",
1414
1466
  true
1415
1467
  );
1416
1468
  }
@@ -1433,6 +1485,7 @@ var Reactor = class {
1433
1485
  throw new Error("Already connected or connecting");
1434
1486
  }
1435
1487
  this.setStatus("connecting");
1488
+ this.connectStartTime = performance.now();
1436
1489
  try {
1437
1490
  console.debug(
1438
1491
  "[Reactor] Connecting to coordinator with authenticated URL"
@@ -1450,13 +1503,25 @@ var Reactor = class {
1450
1503
  send: this.send,
1451
1504
  receive: this.receive
1452
1505
  });
1506
+ const tSession = performance.now();
1453
1507
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1508
+ const sessionCreationMs = performance.now() - tSession;
1454
1509
  this.setSessionId(sessionId);
1455
- const sdpAnswer = yield this.coordinatorClient.connect(
1510
+ const tSdp = performance.now();
1511
+ const { sdpAnswer, sdpPollingAttempts } = yield this.coordinatorClient.connect(
1456
1512
  sessionId,
1457
1513
  void 0,
1458
1514
  options == null ? void 0 : options.maxAttempts
1459
1515
  );
1516
+ const sdpPollingMs = performance.now() - tSdp;
1517
+ this.connectionTimings = {
1518
+ sessionCreationMs,
1519
+ sdpPollingMs,
1520
+ sdpPollingAttempts,
1521
+ iceNegotiationMs: 0,
1522
+ dataChannelMs: 0,
1523
+ totalMs: 0
1524
+ };
1460
1525
  yield this.machineClient.connect(sdpAnswer);
1461
1526
  } catch (error) {
1462
1527
  if (isAbortError(error)) return;
@@ -1464,7 +1529,7 @@ var Reactor = class {
1464
1529
  this.createError(
1465
1530
  "CONNECTION_FAILED",
1466
1531
  `Connection failed: ${error}`,
1467
- "coordinator",
1532
+ "api",
1468
1533
  true
1469
1534
  );
1470
1535
  try {
@@ -1502,6 +1567,7 @@ var Reactor = class {
1502
1567
  if (this.machineClient !== client) return;
1503
1568
  switch (status) {
1504
1569
  case "connected":
1570
+ this.finalizeConnectionTimings(client);
1505
1571
  this.setStatus("ready");
1506
1572
  break;
1507
1573
  case "disconnected":
@@ -1527,7 +1593,9 @@ var Reactor = class {
1527
1593
  );
1528
1594
  client.on("statsUpdate", (stats) => {
1529
1595
  if (this.machineClient !== client) return;
1530
- this.emit("statsUpdate", stats);
1596
+ this.emit("statsUpdate", __spreadProps(__spreadValues({}, stats), {
1597
+ connectionTimings: this.connectionTimings
1598
+ }));
1531
1599
  });
1532
1600
  }
1533
1601
  /**
@@ -1561,6 +1629,7 @@ var Reactor = class {
1561
1629
  }
1562
1630
  }
1563
1631
  this.setStatus("disconnected");
1632
+ this.resetConnectionTimings();
1564
1633
  if (!recoverable) {
1565
1634
  this.setSessionExpiration(void 0);
1566
1635
  this.setSessionId(void 0);
@@ -1623,7 +1692,23 @@ var Reactor = class {
1623
1692
  }
1624
1693
  getStats() {
1625
1694
  var _a;
1626
- return (_a = this.machineClient) == null ? void 0 : _a.getStats();
1695
+ const stats = (_a = this.machineClient) == null ? void 0 : _a.getStats();
1696
+ if (!stats) return void 0;
1697
+ return __spreadProps(__spreadValues({}, stats), { connectionTimings: this.connectionTimings });
1698
+ }
1699
+ resetConnectionTimings() {
1700
+ this.connectStartTime = void 0;
1701
+ this.connectionTimings = void 0;
1702
+ }
1703
+ finalizeConnectionTimings(client) {
1704
+ var _a, _b;
1705
+ if (!this.connectionTimings || this.connectStartTime == null) return;
1706
+ const webrtcTimings = client.getConnectionTimings();
1707
+ this.connectionTimings.iceNegotiationMs = (_a = webrtcTimings == null ? void 0 : webrtcTimings.iceNegotiationMs) != null ? _a : 0;
1708
+ this.connectionTimings.dataChannelMs = (_b = webrtcTimings == null ? void 0 : webrtcTimings.dataChannelMs) != null ? _b : 0;
1709
+ this.connectionTimings.totalMs = performance.now() - this.connectStartTime;
1710
+ this.connectStartTime = void 0;
1711
+ console.debug("[Reactor] Connection timings:", this.connectionTimings);
1627
1712
  }
1628
1713
  /**
1629
1714
  * Create and store an error
@@ -1663,7 +1748,7 @@ var initReactorStore = (props) => {
1663
1748
  };
1664
1749
  var createReactorStore = (initProps, publicState = defaultInitState) => {
1665
1750
  console.debug("[ReactorStore] Creating store", {
1666
- coordinatorUrl: initProps.coordinatorUrl,
1751
+ apiUrl: initProps.apiUrl,
1667
1752
  jwtToken: initProps.jwtToken,
1668
1753
  initialState: publicState
1669
1754
  });
@@ -1831,7 +1916,7 @@ function ReactorProvider(_a) {
1831
1916
  console.debug("[ReactorProvider] Reactor store created successfully");
1832
1917
  }
1833
1918
  const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
1834
- const { coordinatorUrl, modelName, local, receive, send } = props;
1919
+ const { apiUrl, modelName, local, receive, send } = props;
1835
1920
  const maxAttempts = pollingOptions.maxAttempts;
1836
1921
  useEffect(() => {
1837
1922
  const handleBeforeUnload = () => {
@@ -1884,7 +1969,7 @@ function ReactorProvider(_a) {
1884
1969
  console.debug("[ReactorProvider] Updating reactor store");
1885
1970
  storeRef.current = createReactorStore(
1886
1971
  initReactorStore({
1887
- coordinatorUrl,
1972
+ apiUrl,
1888
1973
  modelName,
1889
1974
  local,
1890
1975
  receive,
@@ -1916,7 +2001,7 @@ function ReactorProvider(_a) {
1916
2001
  });
1917
2002
  };
1918
2003
  }, [
1919
- coordinatorUrl,
2004
+ apiUrl,
1920
2005
  modelName,
1921
2006
  autoConnect,
1922
2007
  local,
@@ -2745,12 +2830,12 @@ function WebcamStream({
2745
2830
  }
2746
2831
 
2747
2832
  // src/utils/tokens.ts
2748
- function fetchInsecureJwtToken(_0) {
2749
- return __async(this, arguments, function* (apiKey, coordinatorUrl = PROD_COORDINATOR_URL) {
2833
+ function fetchInsecureToken(_0) {
2834
+ return __async(this, arguments, function* (apiKey, apiUrl = DEFAULT_BASE_URL) {
2750
2835
  console.warn(
2751
- "[Reactor] \u26A0\uFE0F SECURITY WARNING: fetchInsecureJwtToken() exposes your API key in client-side code. This should ONLY be used for local development or testing. In production, fetch tokens from your server instead."
2836
+ "[Reactor] \u26A0\uFE0F SECURITY WARNING: fetchInsecureToken() exposes your API key in client-side code. This should ONLY be used for local development or testing. In production, fetch tokens from your server instead."
2752
2837
  );
2753
- const response = yield fetch(`${coordinatorUrl}/tokens`, {
2838
+ const response = yield fetch(`${apiUrl}/tokens`, {
2754
2839
  method: "GET",
2755
2840
  headers: {
2756
2841
  "X-API-Key": apiKey
@@ -2767,14 +2852,14 @@ function fetchInsecureJwtToken(_0) {
2767
2852
  export {
2768
2853
  AbortError,
2769
2854
  ConflictError,
2770
- PROD_COORDINATOR_URL,
2855
+ DEFAULT_BASE_URL,
2771
2856
  Reactor,
2772
2857
  ReactorController,
2773
2858
  ReactorProvider,
2774
2859
  ReactorView,
2775
2860
  WebcamStream,
2776
2861
  audio,
2777
- fetchInsecureJwtToken,
2862
+ fetchInsecureToken,
2778
2863
  isAbortError,
2779
2864
  useReactor,
2780
2865
  useReactorInternalMessage,