@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.mjs CHANGED
@@ -62,6 +62,14 @@ var ConflictError = class extends Error {
62
62
  super(message);
63
63
  }
64
64
  };
65
+ var AbortError = class extends Error {
66
+ constructor(message) {
67
+ super(message);
68
+ }
69
+ };
70
+ function isAbortError(error) {
71
+ return error instanceof AbortError || error instanceof Error && error.name === "AbortError";
72
+ }
65
73
 
66
74
  // src/core/types.ts
67
75
  import { z } from "zod";
@@ -167,13 +175,17 @@ function rewriteMids(sdp, trackNames) {
167
175
  function createOffer(pc, trackNames) {
168
176
  return __async(this, null, function* () {
169
177
  const offer = yield pc.createOffer();
178
+ let needsAnswerRestore = false;
170
179
  if (trackNames && trackNames.length > 0 && offer.sdp) {
171
180
  const munged = rewriteMids(offer.sdp, trackNames);
172
- const mungedOffer = new RTCSessionDescription({
173
- type: "offer",
174
- sdp: munged
175
- });
176
- yield pc.setLocalDescription(mungedOffer);
181
+ try {
182
+ yield pc.setLocalDescription(
183
+ new RTCSessionDescription({ type: "offer", sdp: munged })
184
+ );
185
+ } catch (e) {
186
+ yield pc.setLocalDescription(offer);
187
+ needsAnswerRestore = true;
188
+ }
177
189
  } else {
178
190
  yield pc.setLocalDescription(offer);
179
191
  }
@@ -182,9 +194,49 @@ function createOffer(pc, trackNames) {
182
194
  if (!localDescription) {
183
195
  throw new Error("Failed to create local description");
184
196
  }
185
- return localDescription.sdp;
197
+ let sdp = localDescription.sdp;
198
+ if (needsAnswerRestore && trackNames && trackNames.length > 0) {
199
+ sdp = rewriteMids(sdp, trackNames);
200
+ }
201
+ return { sdp, needsAnswerRestore };
186
202
  });
187
203
  }
204
+ function buildMidMapping(transceivers) {
205
+ var _a;
206
+ const localToRemote = /* @__PURE__ */ new Map();
207
+ const remoteToLocal = /* @__PURE__ */ new Map();
208
+ for (const entry of transceivers) {
209
+ const mid = (_a = entry.transceiver) == null ? void 0 : _a.mid;
210
+ if (mid) {
211
+ localToRemote.set(mid, entry.name);
212
+ remoteToLocal.set(entry.name, mid);
213
+ }
214
+ }
215
+ return { localToRemote, remoteToLocal };
216
+ }
217
+ function restoreAnswerMids(sdp, remoteToLocal) {
218
+ const lines = sdp.split("\r\n");
219
+ for (let i = 0; i < lines.length; i++) {
220
+ if (lines[i].startsWith("a=mid:")) {
221
+ const remoteMid = lines[i].substring("a=mid:".length);
222
+ const localMid = remoteToLocal.get(remoteMid);
223
+ if (localMid !== void 0) {
224
+ lines[i] = `a=mid:${localMid}`;
225
+ }
226
+ }
227
+ if (lines[i].startsWith("a=group:BUNDLE ")) {
228
+ const parts = lines[i].split(" ");
229
+ for (let j = 1; j < parts.length; j++) {
230
+ const localMid = remoteToLocal.get(parts[j]);
231
+ if (localMid !== void 0) {
232
+ parts[j] = localMid;
233
+ }
234
+ }
235
+ lines[i] = parts.join(" ");
236
+ }
237
+ }
238
+ return lines.join("\r\n");
239
+ }
188
240
  function setRemoteDescription(pc, sdp) {
189
241
  return __async(this, null, function* () {
190
242
  const sessionDescription = new RTCSessionDescription({
@@ -312,6 +364,22 @@ var CoordinatorClient = class {
312
364
  this.baseUrl = options.baseUrl;
313
365
  this.jwtToken = options.jwtToken;
314
366
  this.model = options.model;
367
+ this.abortController = new AbortController();
368
+ }
369
+ /**
370
+ * Aborts any in-flight HTTP requests and polling loops.
371
+ * A fresh AbortController is created so the client remains reusable.
372
+ */
373
+ abort() {
374
+ this.abortController.abort();
375
+ this.abortController = new AbortController();
376
+ }
377
+ /**
378
+ * The current abort signal, passed to every fetch() and sleep() call.
379
+ * Protected so subclasses can forward it to their own fetch calls.
380
+ */
381
+ get signal() {
382
+ return this.abortController.signal;
315
383
  }
316
384
  /**
317
385
  * Returns the authorization header with JWT Bearer token
@@ -332,7 +400,8 @@ var CoordinatorClient = class {
332
400
  `${this.baseUrl}/ice_servers?model=${this.model}`,
333
401
  {
334
402
  method: "GET",
335
- headers: this.getAuthHeaders()
403
+ headers: this.getAuthHeaders(),
404
+ signal: this.signal
336
405
  }
337
406
  );
338
407
  if (!response.ok) {
@@ -366,7 +435,8 @@ var CoordinatorClient = class {
366
435
  headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
367
436
  "Content-Type": "application/json"
368
437
  }),
369
- body: JSON.stringify(requestBody)
438
+ body: JSON.stringify(requestBody),
439
+ signal: this.signal
370
440
  });
371
441
  if (!response.ok) {
372
442
  const errorText = yield response.text();
@@ -400,7 +470,8 @@ var CoordinatorClient = class {
400
470
  `${this.baseUrl}/sessions/${this.currentSessionId}`,
401
471
  {
402
472
  method: "GET",
403
- headers: this.getAuthHeaders()
473
+ headers: this.getAuthHeaders(),
474
+ signal: this.signal
404
475
  }
405
476
  );
406
477
  if (!response.ok) {
@@ -413,12 +484,13 @@ var CoordinatorClient = class {
413
484
  }
414
485
  /**
415
486
  * Terminates the current session by sending a DELETE request to the coordinator.
416
- * @throws Error if no active session exists or if the request fails (except for 404)
487
+ * No-op if no session has been created yet.
488
+ * @throws Error if the request fails (except for 404, which clears local state)
417
489
  */
418
490
  terminateSession() {
419
491
  return __async(this, null, function* () {
420
492
  if (!this.currentSessionId) {
421
- throw new Error("No active session. Call createSession() first.");
493
+ return;
422
494
  }
423
495
  console.debug(
424
496
  "[CoordinatorClient] Terminating session:",
@@ -428,7 +500,8 @@ var CoordinatorClient = class {
428
500
  `${this.baseUrl}/sessions/${this.currentSessionId}`,
429
501
  {
430
502
  method: "DELETE",
431
- headers: this.getAuthHeaders()
503
+ headers: this.getAuthHeaders(),
504
+ signal: this.signal
432
505
  }
433
506
  );
434
507
  if (response.ok) {
@@ -478,7 +551,8 @@ var CoordinatorClient = class {
478
551
  headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
479
552
  "Content-Type": "application/json"
480
553
  }),
481
- body: JSON.stringify(requestBody)
554
+ body: JSON.stringify(requestBody),
555
+ signal: this.signal
482
556
  }
483
557
  );
484
558
  if (response.status === 200) {
@@ -514,6 +588,9 @@ var CoordinatorClient = class {
514
588
  let backoffMs = INITIAL_BACKOFF_MS;
515
589
  let attempt = 0;
516
590
  while (true) {
591
+ if (this.signal.aborted) {
592
+ throw new AbortError("SDP polling aborted");
593
+ }
517
594
  if (attempt >= maxAttempts) {
518
595
  throw new Error(
519
596
  `SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
@@ -529,7 +606,8 @@ var CoordinatorClient = class {
529
606
  method: "GET",
530
607
  headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
531
608
  "Content-Type": "application/json"
532
- })
609
+ }),
610
+ signal: this.signal
533
611
  }
534
612
  );
535
613
  if (response.status === 200) {
@@ -574,10 +652,26 @@ var CoordinatorClient = class {
574
652
  });
575
653
  }
576
654
  /**
577
- * Utility function to sleep for a given number of milliseconds
655
+ * Abort-aware sleep. Resolves after `ms` milliseconds unless the
656
+ * abort signal fires first, in which case it rejects with AbortError.
578
657
  */
579
658
  sleep(ms) {
580
- return new Promise((resolve) => setTimeout(resolve, ms));
659
+ return new Promise((resolve, reject) => {
660
+ const { signal } = this;
661
+ if (signal.aborted) {
662
+ reject(new AbortError("Sleep aborted"));
663
+ return;
664
+ }
665
+ const timer = setTimeout(() => {
666
+ signal.removeEventListener("abort", onAbort);
667
+ resolve();
668
+ }, ms);
669
+ const onAbort = () => {
670
+ clearTimeout(timer);
671
+ reject(new AbortError("Sleep aborted"));
672
+ };
673
+ signal.addEventListener("abort", onAbort, { once: true });
674
+ });
581
675
  }
582
676
  };
583
677
 
@@ -599,7 +693,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
599
693
  return __async(this, null, function* () {
600
694
  console.debug("[LocalCoordinatorClient] Fetching ICE servers...");
601
695
  const response = yield fetch(`${this.localBaseUrl}/ice_servers`, {
602
- method: "GET"
696
+ method: "GET",
697
+ signal: this.signal
603
698
  });
604
699
  if (!response.ok) {
605
700
  throw new Error("Failed to get ICE servers from local coordinator.");
@@ -623,7 +718,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
623
718
  console.debug("[LocalCoordinatorClient] Creating local session...");
624
719
  this.sdpOffer = sdpOffer;
625
720
  const response = yield fetch(`${this.localBaseUrl}/start_session`, {
626
- method: "POST"
721
+ method: "POST",
722
+ signal: this.signal
627
723
  });
628
724
  if (!response.ok) {
629
725
  throw new Error("Failed to send local start session command.");
@@ -651,7 +747,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
651
747
  headers: {
652
748
  "Content-Type": "application/json"
653
749
  },
654
- body: JSON.stringify(sdpBody)
750
+ body: JSON.stringify(sdpBody),
751
+ signal: this.signal
655
752
  });
656
753
  if (!response.ok) {
657
754
  if (response.status === 409) {
@@ -668,7 +765,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
668
765
  return __async(this, null, function* () {
669
766
  console.debug("[LocalCoordinatorClient] Stopping local session...");
670
767
  yield fetch(`${this.localBaseUrl}/stop_session`, {
671
- method: "POST"
768
+ method: "POST",
769
+ signal: this.signal
672
770
  });
673
771
  });
674
772
  }
@@ -743,12 +841,21 @@ var GPUMachineClient = class {
743
841
  );
744
842
  }
745
843
  const trackNames = entries.map((e) => e.name);
746
- const offer = yield createOffer(this.peerConnection, trackNames);
844
+ const { sdp, needsAnswerRestore } = yield createOffer(
845
+ this.peerConnection,
846
+ trackNames
847
+ );
848
+ if (needsAnswerRestore) {
849
+ this.midMapping = buildMidMapping(entries);
850
+ } else {
851
+ this.midMapping = void 0;
852
+ }
747
853
  console.debug(
748
854
  "[GPUMachineClient] Created SDP offer with MIDs:",
749
- trackNames
855
+ trackNames,
856
+ needsAnswerRestore ? "(needs answer restore)" : "(native munging)"
750
857
  );
751
- return offer;
858
+ return sdp;
752
859
  });
753
860
  }
754
861
  /**
@@ -797,7 +904,14 @@ var GPUMachineClient = class {
797
904
  }
798
905
  this.setStatus("connecting");
799
906
  try {
800
- yield setRemoteDescription(this.peerConnection, sdpAnswer);
907
+ let answer = sdpAnswer;
908
+ if (this.midMapping) {
909
+ answer = restoreAnswerMids(
910
+ answer,
911
+ this.midMapping.remoteToLocal
912
+ );
913
+ }
914
+ yield setRemoteDescription(this.peerConnection, answer);
801
915
  console.debug("[GPUMachineClient] Remote description set");
802
916
  } catch (error) {
803
917
  console.error("[GPUMachineClient] Failed to connect:", error);
@@ -825,6 +939,7 @@ var GPUMachineClient = class {
825
939
  this.peerConnection = void 0;
826
940
  }
827
941
  this.transceiverMap.clear();
942
+ this.midMapping = void 0;
828
943
  this.peerConnected = false;
829
944
  this.dataChannelOpen = false;
830
945
  this.setStatus("disconnected");
@@ -1049,13 +1164,19 @@ var GPUMachineClient = class {
1049
1164
  }
1050
1165
  };
1051
1166
  this.peerConnection.ontrack = (event) => {
1052
- var _a;
1053
- const mid = event.transceiver.mid;
1054
- const trackName = mid != null ? mid : `unknown-${event.track.id}`;
1167
+ var _a, _b;
1168
+ let trackName;
1169
+ for (const [name, entry] of this.transceiverMap) {
1170
+ if (entry.transceiver === event.transceiver) {
1171
+ trackName = name;
1172
+ break;
1173
+ }
1174
+ }
1175
+ trackName != null ? trackName : trackName = (_a = event.transceiver.mid) != null ? _a : `unknown-${event.track.id}`;
1055
1176
  console.debug(
1056
- `[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${mid})`
1177
+ `[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${event.transceiver.mid})`
1057
1178
  );
1058
- const stream = (_a = event.streams[0]) != null ? _a : new MediaStream([event.track]);
1179
+ const stream = (_b = event.streams[0]) != null ? _b : new MediaStream([event.track]);
1059
1180
  this.emit("trackReceived", trackName, event.track, stream);
1060
1181
  };
1061
1182
  this.peerConnection.onicecandidate = (event) => {
@@ -1278,8 +1399,8 @@ var Reactor = class {
1278
1399
  options == null ? void 0 : options.maxAttempts
1279
1400
  );
1280
1401
  yield this.machineClient.connect(sdpAnswer);
1281
- this.setStatus("ready");
1282
1402
  } catch (error) {
1403
+ if (isAbortError(error)) return;
1283
1404
  let recoverable = false;
1284
1405
  if (error instanceof ConflictError) {
1285
1406
  recoverable = true;
@@ -1338,6 +1459,7 @@ var Reactor = class {
1338
1459
  );
1339
1460
  yield this.machineClient.connect(sdpAnswer);
1340
1461
  } catch (error) {
1462
+ if (isAbortError(error)) return;
1341
1463
  console.error("[Reactor] Connection failed:", error);
1342
1464
  this.createError(
1343
1465
  "CONNECTION_FAILED",
@@ -1359,17 +1481,25 @@ var Reactor = class {
1359
1481
  }
1360
1482
  /**
1361
1483
  * Sets up event handlers for the machine client.
1484
+ *
1485
+ * Each handler captures the client reference at registration time and
1486
+ * ignores events if this.machineClient has since changed (e.g. after
1487
+ * disconnect + reconnect), preventing stale WebRTC teardown events from
1488
+ * interfering with a new connection.
1362
1489
  */
1363
1490
  setupMachineClientHandlers() {
1364
1491
  if (!this.machineClient) return;
1365
- this.machineClient.on("message", (message, scope) => {
1492
+ const client = this.machineClient;
1493
+ client.on("message", (message, scope) => {
1494
+ if (this.machineClient !== client) return;
1366
1495
  if (scope === "application") {
1367
1496
  this.emit("message", message);
1368
1497
  } else if (scope === "runtime") {
1369
1498
  this.emit("runtimeMessage", message);
1370
1499
  }
1371
1500
  });
1372
- this.machineClient.on("statusChanged", (status) => {
1501
+ client.on("statusChanged", (status) => {
1502
+ if (this.machineClient !== client) return;
1373
1503
  switch (status) {
1374
1504
  case "connected":
1375
1505
  this.setStatus("ready");
@@ -1388,13 +1518,15 @@ var Reactor = class {
1388
1518
  break;
1389
1519
  }
1390
1520
  });
1391
- this.machineClient.on(
1521
+ client.on(
1392
1522
  "trackReceived",
1393
1523
  (name, track, stream) => {
1524
+ if (this.machineClient !== client) return;
1394
1525
  this.emit("trackReceived", name, track, stream);
1395
1526
  }
1396
1527
  );
1397
- this.machineClient.on("statsUpdate", (stats) => {
1528
+ client.on("statsUpdate", (stats) => {
1529
+ if (this.machineClient !== client) return;
1398
1530
  this.emit("statsUpdate", stats);
1399
1531
  });
1400
1532
  }
@@ -1404,10 +1536,12 @@ var Reactor = class {
1404
1536
  */
1405
1537
  disconnect(recoverable = false) {
1406
1538
  return __async(this, null, function* () {
1539
+ var _a;
1407
1540
  if (this.status === "disconnected" && !this.sessionId) {
1408
1541
  console.warn("[Reactor] Already disconnected");
1409
1542
  return;
1410
1543
  }
1544
+ (_a = this.coordinatorClient) == null ? void 0 : _a.abort();
1411
1545
  if (this.coordinatorClient && !recoverable) {
1412
1546
  try {
1413
1547
  yield this.coordinatorClient.terminateSession();
@@ -2631,6 +2765,7 @@ function fetchInsecureJwtToken(_0) {
2631
2765
  });
2632
2766
  }
2633
2767
  export {
2768
+ AbortError,
2634
2769
  ConflictError,
2635
2770
  PROD_COORDINATOR_URL,
2636
2771
  Reactor,
@@ -2640,6 +2775,7 @@ export {
2640
2775
  WebcamStream,
2641
2776
  audio,
2642
2777
  fetchInsecureJwtToken,
2778
+ isAbortError,
2643
2779
  useReactor,
2644
2780
  useReactorInternalMessage,
2645
2781
  useReactorMessage,