@reactor-team/js-sdk 2.5.0 → 2.5.1

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
@@ -95,6 +95,11 @@ interface ReactorError {
95
95
  declare class ConflictError extends Error {
96
96
  constructor(message: string);
97
97
  }
98
+ declare class AbortError extends Error {
99
+ constructor(message: string);
100
+ }
101
+ /** Matches both our custom AbortError and the native DOMException thrown by fetch(). */
102
+ declare function isAbortError(error: unknown): boolean;
98
103
  interface ReactorState$1 {
99
104
  status: ReactorStatus;
100
105
  lastError?: ReactorError;
@@ -200,6 +205,11 @@ declare class Reactor {
200
205
  connect(jwtToken?: string, options?: ConnectOptions): Promise<void>;
201
206
  /**
202
207
  * Sets up event handlers for the machine client.
208
+ *
209
+ * Each handler captures the client reference at registration time and
210
+ * ignores events if this.machineClient has since changed (e.g. after
211
+ * disconnect + reconnect), preventing stale WebRTC teardown events from
212
+ * interfering with a new connection.
203
213
  */
204
214
  private setupMachineClientHandlers;
205
215
  /**
@@ -367,4 +377,4 @@ declare function useStats(): ConnectionStats | undefined;
367
377
  */
368
378
  declare function fetchInsecureJwtToken(apiKey: string, coordinatorUrl?: string): Promise<string>;
369
379
 
370
- export { 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, useReactor, useReactorInternalMessage, useReactorMessage, useReactorStore, useStats, video };
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 };
package/dist/index.d.ts CHANGED
@@ -95,6 +95,11 @@ interface ReactorError {
95
95
  declare class ConflictError extends Error {
96
96
  constructor(message: string);
97
97
  }
98
+ declare class AbortError extends Error {
99
+ constructor(message: string);
100
+ }
101
+ /** Matches both our custom AbortError and the native DOMException thrown by fetch(). */
102
+ declare function isAbortError(error: unknown): boolean;
98
103
  interface ReactorState$1 {
99
104
  status: ReactorStatus;
100
105
  lastError?: ReactorError;
@@ -200,6 +205,11 @@ declare class Reactor {
200
205
  connect(jwtToken?: string, options?: ConnectOptions): Promise<void>;
201
206
  /**
202
207
  * Sets up event handlers for the machine client.
208
+ *
209
+ * Each handler captures the client reference at registration time and
210
+ * ignores events if this.machineClient has since changed (e.g. after
211
+ * disconnect + reconnect), preventing stale WebRTC teardown events from
212
+ * interfering with a new connection.
203
213
  */
204
214
  private setupMachineClientHandlers;
205
215
  /**
@@ -367,4 +377,4 @@ declare function useStats(): ConnectionStats | undefined;
367
377
  */
368
378
  declare function fetchInsecureJwtToken(apiKey: string, coordinatorUrl?: string): Promise<string>;
369
379
 
370
- export { 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, useReactor, useReactorInternalMessage, useReactorMessage, useReactorStore, useStats, video };
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 };
package/dist/index.js CHANGED
@@ -79,6 +79,7 @@ var __async = (__this, __arguments, generator) => {
79
79
  // src/index.ts
80
80
  var index_exports = {};
81
81
  __export(index_exports, {
82
+ AbortError: () => AbortError,
82
83
  ConflictError: () => ConflictError,
83
84
  PROD_COORDINATOR_URL: () => PROD_COORDINATOR_URL,
84
85
  Reactor: () => Reactor,
@@ -88,6 +89,7 @@ __export(index_exports, {
88
89
  WebcamStream: () => WebcamStream,
89
90
  audio: () => audio,
90
91
  fetchInsecureJwtToken: () => fetchInsecureJwtToken,
92
+ isAbortError: () => isAbortError,
91
93
  useReactor: () => useReactor,
92
94
  useReactorInternalMessage: () => useReactorInternalMessage,
93
95
  useReactorMessage: () => useReactorMessage,
@@ -109,6 +111,14 @@ var ConflictError = class extends Error {
109
111
  super(message);
110
112
  }
111
113
  };
114
+ var AbortError = class extends Error {
115
+ constructor(message) {
116
+ super(message);
117
+ }
118
+ };
119
+ function isAbortError(error) {
120
+ return error instanceof AbortError || error instanceof Error && error.name === "AbortError";
121
+ }
112
122
 
113
123
  // src/core/types.ts
114
124
  var import_zod = require("zod");
@@ -214,13 +224,17 @@ function rewriteMids(sdp, trackNames) {
214
224
  function createOffer(pc, trackNames) {
215
225
  return __async(this, null, function* () {
216
226
  const offer = yield pc.createOffer();
227
+ let needsAnswerRestore = false;
217
228
  if (trackNames && trackNames.length > 0 && offer.sdp) {
218
229
  const munged = rewriteMids(offer.sdp, trackNames);
219
- const mungedOffer = new RTCSessionDescription({
220
- type: "offer",
221
- sdp: munged
222
- });
223
- yield pc.setLocalDescription(mungedOffer);
230
+ try {
231
+ yield pc.setLocalDescription(
232
+ new RTCSessionDescription({ type: "offer", sdp: munged })
233
+ );
234
+ } catch (e) {
235
+ yield pc.setLocalDescription(offer);
236
+ needsAnswerRestore = true;
237
+ }
224
238
  } else {
225
239
  yield pc.setLocalDescription(offer);
226
240
  }
@@ -229,9 +243,49 @@ function createOffer(pc, trackNames) {
229
243
  if (!localDescription) {
230
244
  throw new Error("Failed to create local description");
231
245
  }
232
- return localDescription.sdp;
246
+ let sdp = localDescription.sdp;
247
+ if (needsAnswerRestore && trackNames && trackNames.length > 0) {
248
+ sdp = rewriteMids(sdp, trackNames);
249
+ }
250
+ return { sdp, needsAnswerRestore };
233
251
  });
234
252
  }
253
+ function buildMidMapping(transceivers) {
254
+ var _a;
255
+ const localToRemote = /* @__PURE__ */ new Map();
256
+ const remoteToLocal = /* @__PURE__ */ new Map();
257
+ for (const entry of transceivers) {
258
+ const mid = (_a = entry.transceiver) == null ? void 0 : _a.mid;
259
+ if (mid) {
260
+ localToRemote.set(mid, entry.name);
261
+ remoteToLocal.set(entry.name, mid);
262
+ }
263
+ }
264
+ return { localToRemote, remoteToLocal };
265
+ }
266
+ function restoreAnswerMids(sdp, remoteToLocal) {
267
+ const lines = sdp.split("\r\n");
268
+ for (let i = 0; i < lines.length; i++) {
269
+ if (lines[i].startsWith("a=mid:")) {
270
+ const remoteMid = lines[i].substring("a=mid:".length);
271
+ const localMid = remoteToLocal.get(remoteMid);
272
+ if (localMid !== void 0) {
273
+ lines[i] = `a=mid:${localMid}`;
274
+ }
275
+ }
276
+ if (lines[i].startsWith("a=group:BUNDLE ")) {
277
+ const parts = lines[i].split(" ");
278
+ for (let j = 1; j < parts.length; j++) {
279
+ const localMid = remoteToLocal.get(parts[j]);
280
+ if (localMid !== void 0) {
281
+ parts[j] = localMid;
282
+ }
283
+ }
284
+ lines[i] = parts.join(" ");
285
+ }
286
+ }
287
+ return lines.join("\r\n");
288
+ }
235
289
  function setRemoteDescription(pc, sdp) {
236
290
  return __async(this, null, function* () {
237
291
  const sessionDescription = new RTCSessionDescription({
@@ -359,6 +413,22 @@ var CoordinatorClient = class {
359
413
  this.baseUrl = options.baseUrl;
360
414
  this.jwtToken = options.jwtToken;
361
415
  this.model = options.model;
416
+ this.abortController = new AbortController();
417
+ }
418
+ /**
419
+ * Aborts any in-flight HTTP requests and polling loops.
420
+ * A fresh AbortController is created so the client remains reusable.
421
+ */
422
+ abort() {
423
+ this.abortController.abort();
424
+ this.abortController = new AbortController();
425
+ }
426
+ /**
427
+ * The current abort signal, passed to every fetch() and sleep() call.
428
+ * Protected so subclasses can forward it to their own fetch calls.
429
+ */
430
+ get signal() {
431
+ return this.abortController.signal;
362
432
  }
363
433
  /**
364
434
  * Returns the authorization header with JWT Bearer token
@@ -379,7 +449,8 @@ var CoordinatorClient = class {
379
449
  `${this.baseUrl}/ice_servers?model=${this.model}`,
380
450
  {
381
451
  method: "GET",
382
- headers: this.getAuthHeaders()
452
+ headers: this.getAuthHeaders(),
453
+ signal: this.signal
383
454
  }
384
455
  );
385
456
  if (!response.ok) {
@@ -413,7 +484,8 @@ var CoordinatorClient = class {
413
484
  headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
414
485
  "Content-Type": "application/json"
415
486
  }),
416
- body: JSON.stringify(requestBody)
487
+ body: JSON.stringify(requestBody),
488
+ signal: this.signal
417
489
  });
418
490
  if (!response.ok) {
419
491
  const errorText = yield response.text();
@@ -447,7 +519,8 @@ var CoordinatorClient = class {
447
519
  `${this.baseUrl}/sessions/${this.currentSessionId}`,
448
520
  {
449
521
  method: "GET",
450
- headers: this.getAuthHeaders()
522
+ headers: this.getAuthHeaders(),
523
+ signal: this.signal
451
524
  }
452
525
  );
453
526
  if (!response.ok) {
@@ -460,12 +533,13 @@ var CoordinatorClient = class {
460
533
  }
461
534
  /**
462
535
  * Terminates the current session by sending a DELETE request to the coordinator.
463
- * @throws Error if no active session exists or if the request fails (except for 404)
536
+ * No-op if no session has been created yet.
537
+ * @throws Error if the request fails (except for 404, which clears local state)
464
538
  */
465
539
  terminateSession() {
466
540
  return __async(this, null, function* () {
467
541
  if (!this.currentSessionId) {
468
- throw new Error("No active session. Call createSession() first.");
542
+ return;
469
543
  }
470
544
  console.debug(
471
545
  "[CoordinatorClient] Terminating session:",
@@ -475,7 +549,8 @@ var CoordinatorClient = class {
475
549
  `${this.baseUrl}/sessions/${this.currentSessionId}`,
476
550
  {
477
551
  method: "DELETE",
478
- headers: this.getAuthHeaders()
552
+ headers: this.getAuthHeaders(),
553
+ signal: this.signal
479
554
  }
480
555
  );
481
556
  if (response.ok) {
@@ -525,7 +600,8 @@ var CoordinatorClient = class {
525
600
  headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
526
601
  "Content-Type": "application/json"
527
602
  }),
528
- body: JSON.stringify(requestBody)
603
+ body: JSON.stringify(requestBody),
604
+ signal: this.signal
529
605
  }
530
606
  );
531
607
  if (response.status === 200) {
@@ -561,6 +637,9 @@ var CoordinatorClient = class {
561
637
  let backoffMs = INITIAL_BACKOFF_MS;
562
638
  let attempt = 0;
563
639
  while (true) {
640
+ if (this.signal.aborted) {
641
+ throw new AbortError("SDP polling aborted");
642
+ }
564
643
  if (attempt >= maxAttempts) {
565
644
  throw new Error(
566
645
  `SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
@@ -576,7 +655,8 @@ var CoordinatorClient = class {
576
655
  method: "GET",
577
656
  headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
578
657
  "Content-Type": "application/json"
579
- })
658
+ }),
659
+ signal: this.signal
580
660
  }
581
661
  );
582
662
  if (response.status === 200) {
@@ -621,10 +701,26 @@ var CoordinatorClient = class {
621
701
  });
622
702
  }
623
703
  /**
624
- * Utility function to sleep for a given number of milliseconds
704
+ * Abort-aware sleep. Resolves after `ms` milliseconds unless the
705
+ * abort signal fires first, in which case it rejects with AbortError.
625
706
  */
626
707
  sleep(ms) {
627
- return new Promise((resolve) => setTimeout(resolve, ms));
708
+ return new Promise((resolve, reject) => {
709
+ const { signal } = this;
710
+ if (signal.aborted) {
711
+ reject(new AbortError("Sleep aborted"));
712
+ return;
713
+ }
714
+ const timer = setTimeout(() => {
715
+ signal.removeEventListener("abort", onAbort);
716
+ resolve();
717
+ }, ms);
718
+ const onAbort = () => {
719
+ clearTimeout(timer);
720
+ reject(new AbortError("Sleep aborted"));
721
+ };
722
+ signal.addEventListener("abort", onAbort, { once: true });
723
+ });
628
724
  }
629
725
  };
630
726
 
@@ -646,7 +742,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
646
742
  return __async(this, null, function* () {
647
743
  console.debug("[LocalCoordinatorClient] Fetching ICE servers...");
648
744
  const response = yield fetch(`${this.localBaseUrl}/ice_servers`, {
649
- method: "GET"
745
+ method: "GET",
746
+ signal: this.signal
650
747
  });
651
748
  if (!response.ok) {
652
749
  throw new Error("Failed to get ICE servers from local coordinator.");
@@ -670,7 +767,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
670
767
  console.debug("[LocalCoordinatorClient] Creating local session...");
671
768
  this.sdpOffer = sdpOffer;
672
769
  const response = yield fetch(`${this.localBaseUrl}/start_session`, {
673
- method: "POST"
770
+ method: "POST",
771
+ signal: this.signal
674
772
  });
675
773
  if (!response.ok) {
676
774
  throw new Error("Failed to send local start session command.");
@@ -698,7 +796,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
698
796
  headers: {
699
797
  "Content-Type": "application/json"
700
798
  },
701
- body: JSON.stringify(sdpBody)
799
+ body: JSON.stringify(sdpBody),
800
+ signal: this.signal
702
801
  });
703
802
  if (!response.ok) {
704
803
  if (response.status === 409) {
@@ -715,7 +814,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
715
814
  return __async(this, null, function* () {
716
815
  console.debug("[LocalCoordinatorClient] Stopping local session...");
717
816
  yield fetch(`${this.localBaseUrl}/stop_session`, {
718
- method: "POST"
817
+ method: "POST",
818
+ signal: this.signal
719
819
  });
720
820
  });
721
821
  }
@@ -790,12 +890,21 @@ var GPUMachineClient = class {
790
890
  );
791
891
  }
792
892
  const trackNames = entries.map((e) => e.name);
793
- const offer = yield createOffer(this.peerConnection, trackNames);
893
+ const { sdp, needsAnswerRestore } = yield createOffer(
894
+ this.peerConnection,
895
+ trackNames
896
+ );
897
+ if (needsAnswerRestore) {
898
+ this.midMapping = buildMidMapping(entries);
899
+ } else {
900
+ this.midMapping = void 0;
901
+ }
794
902
  console.debug(
795
903
  "[GPUMachineClient] Created SDP offer with MIDs:",
796
- trackNames
904
+ trackNames,
905
+ needsAnswerRestore ? "(needs answer restore)" : "(native munging)"
797
906
  );
798
- return offer;
907
+ return sdp;
799
908
  });
800
909
  }
801
910
  /**
@@ -844,7 +953,14 @@ var GPUMachineClient = class {
844
953
  }
845
954
  this.setStatus("connecting");
846
955
  try {
847
- yield setRemoteDescription(this.peerConnection, sdpAnswer);
956
+ let answer = sdpAnswer;
957
+ if (this.midMapping) {
958
+ answer = restoreAnswerMids(
959
+ answer,
960
+ this.midMapping.remoteToLocal
961
+ );
962
+ }
963
+ yield setRemoteDescription(this.peerConnection, answer);
848
964
  console.debug("[GPUMachineClient] Remote description set");
849
965
  } catch (error) {
850
966
  console.error("[GPUMachineClient] Failed to connect:", error);
@@ -872,6 +988,7 @@ var GPUMachineClient = class {
872
988
  this.peerConnection = void 0;
873
989
  }
874
990
  this.transceiverMap.clear();
991
+ this.midMapping = void 0;
875
992
  this.peerConnected = false;
876
993
  this.dataChannelOpen = false;
877
994
  this.setStatus("disconnected");
@@ -1096,13 +1213,19 @@ var GPUMachineClient = class {
1096
1213
  }
1097
1214
  };
1098
1215
  this.peerConnection.ontrack = (event) => {
1099
- var _a;
1100
- const mid = event.transceiver.mid;
1101
- const trackName = mid != null ? mid : `unknown-${event.track.id}`;
1216
+ var _a, _b;
1217
+ let trackName;
1218
+ for (const [name, entry] of this.transceiverMap) {
1219
+ if (entry.transceiver === event.transceiver) {
1220
+ trackName = name;
1221
+ break;
1222
+ }
1223
+ }
1224
+ trackName != null ? trackName : trackName = (_a = event.transceiver.mid) != null ? _a : `unknown-${event.track.id}`;
1102
1225
  console.debug(
1103
- `[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${mid})`
1226
+ `[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${event.transceiver.mid})`
1104
1227
  );
1105
- const stream = (_a = event.streams[0]) != null ? _a : new MediaStream([event.track]);
1228
+ const stream = (_b = event.streams[0]) != null ? _b : new MediaStream([event.track]);
1106
1229
  this.emit("trackReceived", trackName, event.track, stream);
1107
1230
  };
1108
1231
  this.peerConnection.onicecandidate = (event) => {
@@ -1325,8 +1448,8 @@ var Reactor = class {
1325
1448
  options == null ? void 0 : options.maxAttempts
1326
1449
  );
1327
1450
  yield this.machineClient.connect(sdpAnswer);
1328
- this.setStatus("ready");
1329
1451
  } catch (error) {
1452
+ if (isAbortError(error)) return;
1330
1453
  let recoverable = false;
1331
1454
  if (error instanceof ConflictError) {
1332
1455
  recoverable = true;
@@ -1385,6 +1508,7 @@ var Reactor = class {
1385
1508
  );
1386
1509
  yield this.machineClient.connect(sdpAnswer);
1387
1510
  } catch (error) {
1511
+ if (isAbortError(error)) return;
1388
1512
  console.error("[Reactor] Connection failed:", error);
1389
1513
  this.createError(
1390
1514
  "CONNECTION_FAILED",
@@ -1406,17 +1530,25 @@ var Reactor = class {
1406
1530
  }
1407
1531
  /**
1408
1532
  * Sets up event handlers for the machine client.
1533
+ *
1534
+ * Each handler captures the client reference at registration time and
1535
+ * ignores events if this.machineClient has since changed (e.g. after
1536
+ * disconnect + reconnect), preventing stale WebRTC teardown events from
1537
+ * interfering with a new connection.
1409
1538
  */
1410
1539
  setupMachineClientHandlers() {
1411
1540
  if (!this.machineClient) return;
1412
- this.machineClient.on("message", (message, scope) => {
1541
+ const client = this.machineClient;
1542
+ client.on("message", (message, scope) => {
1543
+ if (this.machineClient !== client) return;
1413
1544
  if (scope === "application") {
1414
1545
  this.emit("message", message);
1415
1546
  } else if (scope === "runtime") {
1416
1547
  this.emit("runtimeMessage", message);
1417
1548
  }
1418
1549
  });
1419
- this.machineClient.on("statusChanged", (status) => {
1550
+ client.on("statusChanged", (status) => {
1551
+ if (this.machineClient !== client) return;
1420
1552
  switch (status) {
1421
1553
  case "connected":
1422
1554
  this.setStatus("ready");
@@ -1435,13 +1567,15 @@ var Reactor = class {
1435
1567
  break;
1436
1568
  }
1437
1569
  });
1438
- this.machineClient.on(
1570
+ client.on(
1439
1571
  "trackReceived",
1440
1572
  (name, track, stream) => {
1573
+ if (this.machineClient !== client) return;
1441
1574
  this.emit("trackReceived", name, track, stream);
1442
1575
  }
1443
1576
  );
1444
- this.machineClient.on("statsUpdate", (stats) => {
1577
+ client.on("statsUpdate", (stats) => {
1578
+ if (this.machineClient !== client) return;
1445
1579
  this.emit("statsUpdate", stats);
1446
1580
  });
1447
1581
  }
@@ -1451,10 +1585,12 @@ var Reactor = class {
1451
1585
  */
1452
1586
  disconnect(recoverable = false) {
1453
1587
  return __async(this, null, function* () {
1588
+ var _a;
1454
1589
  if (this.status === "disconnected" && !this.sessionId) {
1455
1590
  console.warn("[Reactor] Already disconnected");
1456
1591
  return;
1457
1592
  }
1593
+ (_a = this.coordinatorClient) == null ? void 0 : _a.abort();
1458
1594
  if (this.coordinatorClient && !recoverable) {
1459
1595
  try {
1460
1596
  yield this.coordinatorClient.terminateSession();
@@ -2679,6 +2815,7 @@ function fetchInsecureJwtToken(_0) {
2679
2815
  }
2680
2816
  // Annotate the CommonJS export names for ESM import in node:
2681
2817
  0 && (module.exports = {
2818
+ AbortError,
2682
2819
  ConflictError,
2683
2820
  PROD_COORDINATOR_URL,
2684
2821
  Reactor,
@@ -2688,6 +2825,7 @@ function fetchInsecureJwtToken(_0) {
2688
2825
  WebcamStream,
2689
2826
  audio,
2690
2827
  fetchInsecureJwtToken,
2828
+ isAbortError,
2691
2829
  useReactor,
2692
2830
  useReactorInternalMessage,
2693
2831
  useReactorMessage,