@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 +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +172 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +170 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1054
|
-
const
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|