@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.d.mts CHANGED
@@ -89,7 +89,7 @@ interface ReactorError {
89
89
  message: string;
90
90
  timestamp: number;
91
91
  recoverable: boolean;
92
- component: "coordinator" | "gpu" | "livekit";
92
+ component: "api" | "gpu";
93
93
  retryAfter?: number;
94
94
  }
95
95
  declare class ConflictError extends Error {
@@ -111,6 +111,25 @@ interface ConnectOptions {
111
111
  /** Maximum number of SDP polling attempts before giving up. Default: 6. */
112
112
  maxAttempts?: number;
113
113
  }
114
+ /**
115
+ * One-shot timing breakdown of the connect() handshake, recorded once per
116
+ * connection and included in every subsequent {@link ConnectionStats} update.
117
+ * All durations are in milliseconds (from `performance.now()`).
118
+ */
119
+ interface ConnectionTimings {
120
+ /** POST /sessions round-trip time */
121
+ sessionCreationMs: number;
122
+ /** Total time spent polling for the SDP answer */
123
+ sdpPollingMs: number;
124
+ /** Number of SDP poll requests made (1 = answered on first try) */
125
+ sdpPollingAttempts: number;
126
+ /** setRemoteDescription → RTCPeerConnection connectionState "connected" */
127
+ iceNegotiationMs: number;
128
+ /** setRemoteDescription → RTCDataChannel "open" */
129
+ dataChannelMs: number;
130
+ /** End-to-end: connect() invocation → status "ready" */
131
+ totalMs: number;
132
+ }
114
133
  interface ConnectionStats {
115
134
  /** ICE candidate-pair round-trip time in milliseconds */
116
135
  rtt?: number;
@@ -124,13 +143,15 @@ interface ConnectionStats {
124
143
  packetLossRatio?: number;
125
144
  /** Network jitter in seconds (from inbound-rtp) */
126
145
  jitter?: number;
146
+ /** Timing breakdown of the initial connection handshake (set once, persisted until disconnect) */
147
+ connectionTimings?: ConnectionTimings;
127
148
  timestamp: number;
128
149
  }
129
150
  type ReactorEvent = "statusChanged" | "sessionIdChanged" | "message" | "runtimeMessage" | "trackReceived" | "error" | "sessionExpirationChanged" | "statsUpdate";
130
151
 
131
- declare const PROD_COORDINATOR_URL = "https://api.reactor.inc";
152
+ declare const DEFAULT_BASE_URL = "https://api.reactor.inc";
132
153
  declare const OptionsSchema: z.ZodObject<{
133
- coordinatorUrl: z.ZodDefault<z.ZodString>;
154
+ apiUrl: z.ZodDefault<z.ZodString>;
134
155
  modelName: z.ZodString;
135
156
  local: z.ZodDefault<z.ZodBoolean>;
136
157
  receive: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -164,6 +185,8 @@ declare class Reactor {
164
185
  /** Tracks the client SENDS to the model (client → model). */
165
186
  private send;
166
187
  private sessionId?;
188
+ private connectStartTime?;
189
+ private connectionTimings?;
167
190
  constructor(options: Options);
168
191
  private eventListeners;
169
192
  on(event: ReactorEvent, handler: EventHandler): void;
@@ -235,6 +258,8 @@ declare class Reactor {
235
258
  */
236
259
  getLastError(): ReactorError | undefined;
237
260
  getStats(): ConnectionStats | undefined;
261
+ private resetConnectionTimings;
262
+ private finalizeConnectionTimings;
238
263
  /**
239
264
  * Create and store an error
240
265
  */
@@ -372,9 +397,9 @@ declare function useStats(): ConnectionStats | undefined;
372
397
  * In production, call /tokens from your server and pass the JWT to your frontend.
373
398
  *
374
399
  * @param apiKey - Your Reactor API key (will be exposed in client code!)
375
- * @param coordinatorUrl - Optional coordinator URL, defaults to production
400
+ * @param apiUrl - Optional API URL, defaults to production
376
401
  * @returns string containing the JWT token
377
402
  */
378
- declare function fetchInsecureJwtToken(apiKey: string, coordinatorUrl?: string): Promise<string>;
403
+ declare function fetchInsecureToken(apiKey: string, apiUrl?: string): Promise<string>;
379
404
 
380
- export { AbortError, type AudioTrackOptions, ConflictError, type ConnectOptions, type ConnectionStats, type MessageScope, type Options, PROD_COORDINATOR_URL, Reactor, type ReactorConnectOptions, ReactorController, type ReactorControllerProps, type ReactorError, type ReactorEvent, ReactorProvider, type ReactorState$1 as ReactorState, type ReactorStatus, ReactorView, type ReactorViewProps, type TrackConfig, type VideoTrackOptions, WebcamStream, type WebcamStreamProps, audio, fetchInsecureJwtToken, isAbortError, useReactor, useReactorInternalMessage, useReactorMessage, useReactorStore, useStats, video };
405
+ export { AbortError, type AudioTrackOptions, ConflictError, type ConnectOptions, type ConnectionStats, type ConnectionTimings, DEFAULT_BASE_URL, type MessageScope, type Options, Reactor, type ReactorConnectOptions, ReactorController, type ReactorControllerProps, type ReactorError, type ReactorEvent, ReactorProvider, type ReactorState$1 as ReactorState, type ReactorStatus, ReactorView, type ReactorViewProps, type TrackConfig, type VideoTrackOptions, WebcamStream, type WebcamStreamProps, audio, fetchInsecureToken, isAbortError, useReactor, useReactorInternalMessage, useReactorMessage, useReactorStore, useStats, video };
package/dist/index.d.ts CHANGED
@@ -89,7 +89,7 @@ interface ReactorError {
89
89
  message: string;
90
90
  timestamp: number;
91
91
  recoverable: boolean;
92
- component: "coordinator" | "gpu" | "livekit";
92
+ component: "api" | "gpu";
93
93
  retryAfter?: number;
94
94
  }
95
95
  declare class ConflictError extends Error {
@@ -111,6 +111,25 @@ interface ConnectOptions {
111
111
  /** Maximum number of SDP polling attempts before giving up. Default: 6. */
112
112
  maxAttempts?: number;
113
113
  }
114
+ /**
115
+ * One-shot timing breakdown of the connect() handshake, recorded once per
116
+ * connection and included in every subsequent {@link ConnectionStats} update.
117
+ * All durations are in milliseconds (from `performance.now()`).
118
+ */
119
+ interface ConnectionTimings {
120
+ /** POST /sessions round-trip time */
121
+ sessionCreationMs: number;
122
+ /** Total time spent polling for the SDP answer */
123
+ sdpPollingMs: number;
124
+ /** Number of SDP poll requests made (1 = answered on first try) */
125
+ sdpPollingAttempts: number;
126
+ /** setRemoteDescription → RTCPeerConnection connectionState "connected" */
127
+ iceNegotiationMs: number;
128
+ /** setRemoteDescription → RTCDataChannel "open" */
129
+ dataChannelMs: number;
130
+ /** End-to-end: connect() invocation → status "ready" */
131
+ totalMs: number;
132
+ }
114
133
  interface ConnectionStats {
115
134
  /** ICE candidate-pair round-trip time in milliseconds */
116
135
  rtt?: number;
@@ -124,13 +143,15 @@ interface ConnectionStats {
124
143
  packetLossRatio?: number;
125
144
  /** Network jitter in seconds (from inbound-rtp) */
126
145
  jitter?: number;
146
+ /** Timing breakdown of the initial connection handshake (set once, persisted until disconnect) */
147
+ connectionTimings?: ConnectionTimings;
127
148
  timestamp: number;
128
149
  }
129
150
  type ReactorEvent = "statusChanged" | "sessionIdChanged" | "message" | "runtimeMessage" | "trackReceived" | "error" | "sessionExpirationChanged" | "statsUpdate";
130
151
 
131
- declare const PROD_COORDINATOR_URL = "https://api.reactor.inc";
152
+ declare const DEFAULT_BASE_URL = "https://api.reactor.inc";
132
153
  declare const OptionsSchema: z.ZodObject<{
133
- coordinatorUrl: z.ZodDefault<z.ZodString>;
154
+ apiUrl: z.ZodDefault<z.ZodString>;
134
155
  modelName: z.ZodString;
135
156
  local: z.ZodDefault<z.ZodBoolean>;
136
157
  receive: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -164,6 +185,8 @@ declare class Reactor {
164
185
  /** Tracks the client SENDS to the model (client → model). */
165
186
  private send;
166
187
  private sessionId?;
188
+ private connectStartTime?;
189
+ private connectionTimings?;
167
190
  constructor(options: Options);
168
191
  private eventListeners;
169
192
  on(event: ReactorEvent, handler: EventHandler): void;
@@ -235,6 +258,8 @@ declare class Reactor {
235
258
  */
236
259
  getLastError(): ReactorError | undefined;
237
260
  getStats(): ConnectionStats | undefined;
261
+ private resetConnectionTimings;
262
+ private finalizeConnectionTimings;
238
263
  /**
239
264
  * Create and store an error
240
265
  */
@@ -372,9 +397,9 @@ declare function useStats(): ConnectionStats | undefined;
372
397
  * In production, call /tokens from your server and pass the JWT to your frontend.
373
398
  *
374
399
  * @param apiKey - Your Reactor API key (will be exposed in client code!)
375
- * @param coordinatorUrl - Optional coordinator URL, defaults to production
400
+ * @param apiUrl - Optional API URL, defaults to production
376
401
  * @returns string containing the JWT token
377
402
  */
378
- declare function fetchInsecureJwtToken(apiKey: string, coordinatorUrl?: string): Promise<string>;
403
+ declare function fetchInsecureToken(apiKey: string, apiUrl?: string): Promise<string>;
379
404
 
380
- export { AbortError, type AudioTrackOptions, ConflictError, type ConnectOptions, type ConnectionStats, type MessageScope, type Options, PROD_COORDINATOR_URL, Reactor, type ReactorConnectOptions, ReactorController, type ReactorControllerProps, type ReactorError, type ReactorEvent, ReactorProvider, type ReactorState$1 as ReactorState, type ReactorStatus, ReactorView, type ReactorViewProps, type TrackConfig, type VideoTrackOptions, WebcamStream, type WebcamStreamProps, audio, fetchInsecureJwtToken, isAbortError, useReactor, useReactorInternalMessage, useReactorMessage, useReactorStore, useStats, video };
405
+ export { AbortError, type AudioTrackOptions, ConflictError, type ConnectOptions, type ConnectionStats, type ConnectionTimings, DEFAULT_BASE_URL, type MessageScope, type Options, Reactor, type ReactorConnectOptions, ReactorController, type ReactorControllerProps, type ReactorError, type ReactorEvent, ReactorProvider, type ReactorState$1 as ReactorState, type ReactorStatus, ReactorView, type ReactorViewProps, type TrackConfig, type VideoTrackOptions, WebcamStream, type WebcamStreamProps, audio, fetchInsecureToken, isAbortError, useReactor, useReactorInternalMessage, useReactorMessage, useReactorStore, useStats, video };
package/dist/index.js CHANGED
@@ -81,14 +81,14 @@ var index_exports = {};
81
81
  __export(index_exports, {
82
82
  AbortError: () => AbortError,
83
83
  ConflictError: () => ConflictError,
84
- PROD_COORDINATOR_URL: () => PROD_COORDINATOR_URL,
84
+ DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
85
85
  Reactor: () => Reactor,
86
86
  ReactorController: () => ReactorController,
87
87
  ReactorProvider: () => ReactorProvider,
88
88
  ReactorView: () => ReactorView,
89
89
  WebcamStream: () => WebcamStream,
90
90
  audio: () => audio,
91
- fetchInsecureJwtToken: () => fetchInsecureJwtToken,
91
+ fetchInsecureToken: () => fetchInsecureToken,
92
92
  isAbortError: () => isAbortError,
93
93
  useReactor: () => useReactor,
94
94
  useReactorInternalMessage: () => useReactorInternalMessage,
@@ -178,6 +178,7 @@ var IceServersResponseSchema = import_zod.z.object({
178
178
  // src/utils/webrtc.ts
179
179
  var DEFAULT_DATA_CHANNEL_LABEL = "data";
180
180
  var FORCE_RELAY_MODE = false;
181
+ var DEFAULT_MAX_MESSAGE_BYTES = 256 * 1024;
181
182
  function createPeerConnection(config) {
182
183
  return new RTCPeerConnection({
183
184
  iceServers: config.iceServers,
@@ -334,14 +335,21 @@ function waitForIceGathering(pc, timeoutMs = 5e3) {
334
335
  }, timeoutMs);
335
336
  });
336
337
  }
337
- function sendMessage(channel, command, data, scope = "application") {
338
+ function sendMessage(channel, command, data, scope = "application", maxBytes = DEFAULT_MAX_MESSAGE_BYTES) {
338
339
  if (channel.readyState !== "open") {
339
340
  throw new Error(`Data channel not open: ${channel.readyState}`);
340
341
  }
341
342
  const jsonData = typeof data === "string" ? JSON.parse(data) : data;
342
343
  const inner = { type: command, data: jsonData };
343
344
  const payload = { scope, data: inner };
344
- channel.send(JSON.stringify(payload));
345
+ const serialized = JSON.stringify(payload);
346
+ const byteLength = new TextEncoder().encode(serialized).byteLength;
347
+ if (byteLength > maxBytes) {
348
+ throw new Error(
349
+ `Data channel message too large: ${byteLength} bytes exceeds limit of ${maxBytes} bytes (command: "${command}")`
350
+ );
351
+ }
352
+ channel.send(serialized);
345
353
  }
346
354
  function parseMessage(data) {
347
355
  if (typeof data === "string") {
@@ -662,7 +670,7 @@ var CoordinatorClient = class {
662
670
  if (response.status === 200) {
663
671
  const answerData = yield response.json();
664
672
  console.debug("[CoordinatorClient] Received SDP answer via polling");
665
- return answerData.sdp_answer;
673
+ return { sdpAnswer: answerData.sdp_answer, attempts: attempt };
666
674
  }
667
675
  if (response.status === 202) {
668
676
  console.warn(
@@ -686,7 +694,7 @@ var CoordinatorClient = class {
686
694
  * @param sessionId - The session ID to connect to
687
695
  * @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
688
696
  * @param maxAttempts - Optional maximum number of polling attempts before giving up
689
- * @returns The SDP answer from the server
697
+ * @returns The SDP answer and the number of polling attempts made (0 if answered immediately via PUT)
690
698
  */
691
699
  connect(sessionId, sdpOffer, maxAttempts) {
692
700
  return __async(this, null, function* () {
@@ -694,10 +702,11 @@ var CoordinatorClient = class {
694
702
  if (sdpOffer) {
695
703
  const answer = yield this.sendSdpOffer(sessionId, sdpOffer);
696
704
  if (answer !== null) {
697
- return answer;
705
+ return { sdpAnswer: answer, sdpPollingAttempts: 0 };
698
706
  }
699
707
  }
700
- return this.pollSdpAnswer(sessionId, maxAttempts);
708
+ const result = yield this.pollSdpAnswer(sessionId, maxAttempts);
709
+ return { sdpAnswer: result.sdpAnswer, sdpPollingAttempts: result.attempts };
701
710
  });
702
711
  }
703
712
  /**
@@ -779,9 +788,10 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
779
788
  }
780
789
  /**
781
790
  * Connects to the local session by posting SDP params to /sdp_params.
791
+ * Local connections are always immediate (no polling).
782
792
  * @param sessionId - The session ID (ignored for local)
783
793
  * @param sdpMessage - The SDP offer from the local WebRTC peer connection
784
- * @returns The SDP answer from the server
794
+ * @returns The SDP answer and polling attempts (always 0 for local)
785
795
  */
786
796
  connect(sessionId, sdpMessage) {
787
797
  return __async(this, null, function* () {
@@ -807,7 +817,7 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
807
817
  }
808
818
  const sdpAnswer = yield response.json();
809
819
  console.debug("[LocalCoordinatorClient] Received SDP answer");
810
- return sdpAnswer.sdp;
820
+ return { sdpAnswer: sdpAnswer.sdp, sdpPollingAttempts: 0 };
811
821
  });
812
822
  }
813
823
  terminateSession() {
@@ -952,6 +962,9 @@ var GPUMachineClient = class {
952
962
  );
953
963
  }
954
964
  this.setStatus("connecting");
965
+ this.iceStartTime = performance.now();
966
+ this.iceNegotiationMs = void 0;
967
+ this.dataChannelMs = void 0;
955
968
  try {
956
969
  let answer = sdpAnswer;
957
970
  if (this.midMapping) {
@@ -991,6 +1004,7 @@ var GPUMachineClient = class {
991
1004
  this.midMapping = void 0;
992
1005
  this.peerConnected = false;
993
1006
  this.dataChannelOpen = false;
1007
+ this.resetConnectionTimings();
994
1008
  this.setStatus("disconnected");
995
1009
  console.debug("[GPUMachineClient] Disconnected");
996
1010
  });
@@ -1015,6 +1029,14 @@ var GPUMachineClient = class {
1015
1029
  // ─────────────────────────────────────────────────────────────────────────────
1016
1030
  // Messaging
1017
1031
  // ─────────────────────────────────────────────────────────────────────────────
1032
+ /**
1033
+ * Returns the negotiated SCTP max message size (bytes) if available,
1034
+ * otherwise `undefined` so `sendMessage` falls back to its built-in default.
1035
+ */
1036
+ get maxMessageBytes() {
1037
+ var _a, _b, _c;
1038
+ return (_c = (_b = (_a = this.peerConnection) == null ? void 0 : _a.sctp) == null ? void 0 : _b.maxMessageSize) != null ? _c : void 0;
1039
+ }
1018
1040
  /**
1019
1041
  * Sends a command to the GPU machine via the data channel.
1020
1042
  * @param command The command to send
@@ -1026,7 +1048,13 @@ var GPUMachineClient = class {
1026
1048
  throw new Error("[GPUMachineClient] Data channel not available");
1027
1049
  }
1028
1050
  try {
1029
- sendMessage(this.dataChannel, command, data, scope);
1051
+ sendMessage(
1052
+ this.dataChannel,
1053
+ command,
1054
+ data,
1055
+ scope,
1056
+ this.maxMessageBytes
1057
+ );
1030
1058
  } catch (error) {
1031
1059
  console.warn("[GPUMachineClient] Failed to send message:", error);
1032
1060
  }
@@ -1154,6 +1182,24 @@ var GPUMachineClient = class {
1154
1182
  getStats() {
1155
1183
  return this.stats;
1156
1184
  }
1185
+ /**
1186
+ * Returns the ICE/data-channel durations recorded during the last connect(),
1187
+ * or undefined if no connection has completed yet.
1188
+ */
1189
+ getConnectionTimings() {
1190
+ if (this.iceNegotiationMs == null || this.dataChannelMs == null) {
1191
+ return void 0;
1192
+ }
1193
+ return {
1194
+ iceNegotiationMs: this.iceNegotiationMs,
1195
+ dataChannelMs: this.dataChannelMs
1196
+ };
1197
+ }
1198
+ resetConnectionTimings() {
1199
+ this.iceStartTime = void 0;
1200
+ this.iceNegotiationMs = void 0;
1201
+ this.dataChannelMs = void 0;
1202
+ }
1157
1203
  startStatsPolling() {
1158
1204
  this.stopStatsPolling();
1159
1205
  this.statsInterval = setInterval(() => __async(this, null, function* () {
@@ -1197,6 +1243,9 @@ var GPUMachineClient = class {
1197
1243
  if (state) {
1198
1244
  switch (state) {
1199
1245
  case "connected":
1246
+ if (this.iceStartTime != null && this.iceNegotiationMs == null) {
1247
+ this.iceNegotiationMs = performance.now() - this.iceStartTime;
1248
+ }
1200
1249
  this.peerConnected = true;
1201
1250
  this.checkFullyConnected();
1202
1251
  break;
@@ -1246,6 +1295,9 @@ var GPUMachineClient = class {
1246
1295
  if (!this.dataChannel) return;
1247
1296
  this.dataChannel.onopen = () => {
1248
1297
  console.debug("[GPUMachineClient] Data channel open");
1298
+ if (this.iceStartTime != null && this.dataChannelMs == null) {
1299
+ this.dataChannelMs = performance.now() - this.iceStartTime;
1300
+ }
1249
1301
  this.dataChannelOpen = true;
1250
1302
  this.startPing();
1251
1303
  this.checkFullyConnected();
@@ -1285,13 +1337,13 @@ var GPUMachineClient = class {
1285
1337
  // src/core/Reactor.ts
1286
1338
  var import_zod2 = require("zod");
1287
1339
  var LOCAL_COORDINATOR_URL = "http://localhost:8080";
1288
- var PROD_COORDINATOR_URL = "https://api.reactor.inc";
1340
+ var DEFAULT_BASE_URL = "https://api.reactor.inc";
1289
1341
  var TrackConfigSchema = import_zod2.z.object({
1290
1342
  name: import_zod2.z.string(),
1291
1343
  kind: import_zod2.z.enum(["audio", "video"])
1292
1344
  });
1293
1345
  var OptionsSchema = import_zod2.z.object({
1294
- coordinatorUrl: import_zod2.z.string().default(PROD_COORDINATOR_URL),
1346
+ apiUrl: import_zod2.z.string().default(DEFAULT_BASE_URL),
1295
1347
  modelName: import_zod2.z.string(),
1296
1348
  local: import_zod2.z.boolean().default(false),
1297
1349
  /**
@@ -1316,12 +1368,12 @@ var Reactor = class {
1316
1368
  // Generic event map
1317
1369
  this.eventListeners = /* @__PURE__ */ new Map();
1318
1370
  const validatedOptions = OptionsSchema.parse(options);
1319
- this.coordinatorUrl = validatedOptions.coordinatorUrl;
1371
+ this.coordinatorUrl = validatedOptions.apiUrl;
1320
1372
  this.model = validatedOptions.modelName;
1321
1373
  this.local = validatedOptions.local;
1322
1374
  this.receive = validatedOptions.receive;
1323
1375
  this.send = validatedOptions.send;
1324
- if (this.local && options.coordinatorUrl === void 0) {
1376
+ if (this.local && options.apiUrl === void 0) {
1325
1377
  this.coordinatorUrl = LOCAL_COORDINATOR_URL;
1326
1378
  }
1327
1379
  }
@@ -1442,7 +1494,7 @@ var Reactor = class {
1442
1494
  receive: this.receive
1443
1495
  });
1444
1496
  try {
1445
- const sdpAnswer = yield this.coordinatorClient.connect(
1497
+ const { sdpAnswer } = yield this.coordinatorClient.connect(
1446
1498
  this.sessionId,
1447
1499
  sdpOffer,
1448
1500
  options == null ? void 0 : options.maxAttempts
@@ -1459,7 +1511,7 @@ var Reactor = class {
1459
1511
  this.createError(
1460
1512
  "RECONNECTION_FAILED",
1461
1513
  `Failed to reconnect: ${error}`,
1462
- "coordinator",
1514
+ "api",
1463
1515
  true
1464
1516
  );
1465
1517
  }
@@ -1482,6 +1534,7 @@ var Reactor = class {
1482
1534
  throw new Error("Already connected or connecting");
1483
1535
  }
1484
1536
  this.setStatus("connecting");
1537
+ this.connectStartTime = performance.now();
1485
1538
  try {
1486
1539
  console.debug(
1487
1540
  "[Reactor] Connecting to coordinator with authenticated URL"
@@ -1499,13 +1552,25 @@ var Reactor = class {
1499
1552
  send: this.send,
1500
1553
  receive: this.receive
1501
1554
  });
1555
+ const tSession = performance.now();
1502
1556
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1557
+ const sessionCreationMs = performance.now() - tSession;
1503
1558
  this.setSessionId(sessionId);
1504
- const sdpAnswer = yield this.coordinatorClient.connect(
1559
+ const tSdp = performance.now();
1560
+ const { sdpAnswer, sdpPollingAttempts } = yield this.coordinatorClient.connect(
1505
1561
  sessionId,
1506
1562
  void 0,
1507
1563
  options == null ? void 0 : options.maxAttempts
1508
1564
  );
1565
+ const sdpPollingMs = performance.now() - tSdp;
1566
+ this.connectionTimings = {
1567
+ sessionCreationMs,
1568
+ sdpPollingMs,
1569
+ sdpPollingAttempts,
1570
+ iceNegotiationMs: 0,
1571
+ dataChannelMs: 0,
1572
+ totalMs: 0
1573
+ };
1509
1574
  yield this.machineClient.connect(sdpAnswer);
1510
1575
  } catch (error) {
1511
1576
  if (isAbortError(error)) return;
@@ -1513,7 +1578,7 @@ var Reactor = class {
1513
1578
  this.createError(
1514
1579
  "CONNECTION_FAILED",
1515
1580
  `Connection failed: ${error}`,
1516
- "coordinator",
1581
+ "api",
1517
1582
  true
1518
1583
  );
1519
1584
  try {
@@ -1551,6 +1616,7 @@ var Reactor = class {
1551
1616
  if (this.machineClient !== client) return;
1552
1617
  switch (status) {
1553
1618
  case "connected":
1619
+ this.finalizeConnectionTimings(client);
1554
1620
  this.setStatus("ready");
1555
1621
  break;
1556
1622
  case "disconnected":
@@ -1576,7 +1642,9 @@ var Reactor = class {
1576
1642
  );
1577
1643
  client.on("statsUpdate", (stats) => {
1578
1644
  if (this.machineClient !== client) return;
1579
- this.emit("statsUpdate", stats);
1645
+ this.emit("statsUpdate", __spreadProps(__spreadValues({}, stats), {
1646
+ connectionTimings: this.connectionTimings
1647
+ }));
1580
1648
  });
1581
1649
  }
1582
1650
  /**
@@ -1610,6 +1678,7 @@ var Reactor = class {
1610
1678
  }
1611
1679
  }
1612
1680
  this.setStatus("disconnected");
1681
+ this.resetConnectionTimings();
1613
1682
  if (!recoverable) {
1614
1683
  this.setSessionExpiration(void 0);
1615
1684
  this.setSessionId(void 0);
@@ -1672,7 +1741,23 @@ var Reactor = class {
1672
1741
  }
1673
1742
  getStats() {
1674
1743
  var _a;
1675
- return (_a = this.machineClient) == null ? void 0 : _a.getStats();
1744
+ const stats = (_a = this.machineClient) == null ? void 0 : _a.getStats();
1745
+ if (!stats) return void 0;
1746
+ return __spreadProps(__spreadValues({}, stats), { connectionTimings: this.connectionTimings });
1747
+ }
1748
+ resetConnectionTimings() {
1749
+ this.connectStartTime = void 0;
1750
+ this.connectionTimings = void 0;
1751
+ }
1752
+ finalizeConnectionTimings(client) {
1753
+ var _a, _b;
1754
+ if (!this.connectionTimings || this.connectStartTime == null) return;
1755
+ const webrtcTimings = client.getConnectionTimings();
1756
+ this.connectionTimings.iceNegotiationMs = (_a = webrtcTimings == null ? void 0 : webrtcTimings.iceNegotiationMs) != null ? _a : 0;
1757
+ this.connectionTimings.dataChannelMs = (_b = webrtcTimings == null ? void 0 : webrtcTimings.dataChannelMs) != null ? _b : 0;
1758
+ this.connectionTimings.totalMs = performance.now() - this.connectStartTime;
1759
+ this.connectStartTime = void 0;
1760
+ console.debug("[Reactor] Connection timings:", this.connectionTimings);
1676
1761
  }
1677
1762
  /**
1678
1763
  * Create and store an error
@@ -1712,7 +1797,7 @@ var initReactorStore = (props) => {
1712
1797
  };
1713
1798
  var createReactorStore = (initProps, publicState = defaultInitState) => {
1714
1799
  console.debug("[ReactorStore] Creating store", {
1715
- coordinatorUrl: initProps.coordinatorUrl,
1800
+ apiUrl: initProps.apiUrl,
1716
1801
  jwtToken: initProps.jwtToken,
1717
1802
  initialState: publicState
1718
1803
  });
@@ -1880,7 +1965,7 @@ function ReactorProvider(_a) {
1880
1965
  console.debug("[ReactorProvider] Reactor store created successfully");
1881
1966
  }
1882
1967
  const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
1883
- const { coordinatorUrl, modelName, local, receive, send } = props;
1968
+ const { apiUrl, modelName, local, receive, send } = props;
1884
1969
  const maxAttempts = pollingOptions.maxAttempts;
1885
1970
  (0, import_react3.useEffect)(() => {
1886
1971
  const handleBeforeUnload = () => {
@@ -1933,7 +2018,7 @@ function ReactorProvider(_a) {
1933
2018
  console.debug("[ReactorProvider] Updating reactor store");
1934
2019
  storeRef.current = createReactorStore(
1935
2020
  initReactorStore({
1936
- coordinatorUrl,
2021
+ apiUrl,
1937
2022
  modelName,
1938
2023
  local,
1939
2024
  receive,
@@ -1965,7 +2050,7 @@ function ReactorProvider(_a) {
1965
2050
  });
1966
2051
  };
1967
2052
  }, [
1968
- coordinatorUrl,
2053
+ apiUrl,
1969
2054
  modelName,
1970
2055
  autoConnect,
1971
2056
  local,
@@ -2794,12 +2879,12 @@ function WebcamStream({
2794
2879
  }
2795
2880
 
2796
2881
  // src/utils/tokens.ts
2797
- function fetchInsecureJwtToken(_0) {
2798
- return __async(this, arguments, function* (apiKey, coordinatorUrl = PROD_COORDINATOR_URL) {
2882
+ function fetchInsecureToken(_0) {
2883
+ return __async(this, arguments, function* (apiKey, apiUrl = DEFAULT_BASE_URL) {
2799
2884
  console.warn(
2800
- "[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."
2885
+ "[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."
2801
2886
  );
2802
- const response = yield fetch(`${coordinatorUrl}/tokens`, {
2887
+ const response = yield fetch(`${apiUrl}/tokens`, {
2803
2888
  method: "GET",
2804
2889
  headers: {
2805
2890
  "X-API-Key": apiKey
@@ -2817,14 +2902,14 @@ function fetchInsecureJwtToken(_0) {
2817
2902
  0 && (module.exports = {
2818
2903
  AbortError,
2819
2904
  ConflictError,
2820
- PROD_COORDINATOR_URL,
2905
+ DEFAULT_BASE_URL,
2821
2906
  Reactor,
2822
2907
  ReactorController,
2823
2908
  ReactorProvider,
2824
2909
  ReactorView,
2825
2910
  WebcamStream,
2826
2911
  audio,
2827
- fetchInsecureJwtToken,
2912
+ fetchInsecureToken,
2828
2913
  isAbortError,
2829
2914
  useReactor,
2830
2915
  useReactorInternalMessage,