@reactor-team/js-sdk 2.5.0 → 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 +41 -6
- package/dist/index.d.ts +41 -6
- package/dist/index.js +288 -65
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +284 -63
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -2
package/dist/index.js
CHANGED
|
@@ -79,15 +79,17 @@ 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
|
+
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
84
85
|
Reactor: () => Reactor,
|
|
85
86
|
ReactorController: () => ReactorController,
|
|
86
87
|
ReactorProvider: () => ReactorProvider,
|
|
87
88
|
ReactorView: () => ReactorView,
|
|
88
89
|
WebcamStream: () => WebcamStream,
|
|
89
90
|
audio: () => audio,
|
|
90
|
-
|
|
91
|
+
fetchInsecureToken: () => fetchInsecureToken,
|
|
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");
|
|
@@ -168,6 +178,7 @@ var IceServersResponseSchema = import_zod.z.object({
|
|
|
168
178
|
// src/utils/webrtc.ts
|
|
169
179
|
var DEFAULT_DATA_CHANNEL_LABEL = "data";
|
|
170
180
|
var FORCE_RELAY_MODE = false;
|
|
181
|
+
var DEFAULT_MAX_MESSAGE_BYTES = 256 * 1024;
|
|
171
182
|
function createPeerConnection(config) {
|
|
172
183
|
return new RTCPeerConnection({
|
|
173
184
|
iceServers: config.iceServers,
|
|
@@ -214,13 +225,17 @@ function rewriteMids(sdp, trackNames) {
|
|
|
214
225
|
function createOffer(pc, trackNames) {
|
|
215
226
|
return __async(this, null, function* () {
|
|
216
227
|
const offer = yield pc.createOffer();
|
|
228
|
+
let needsAnswerRestore = false;
|
|
217
229
|
if (trackNames && trackNames.length > 0 && offer.sdp) {
|
|
218
230
|
const munged = rewriteMids(offer.sdp, trackNames);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
231
|
+
try {
|
|
232
|
+
yield pc.setLocalDescription(
|
|
233
|
+
new RTCSessionDescription({ type: "offer", sdp: munged })
|
|
234
|
+
);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
yield pc.setLocalDescription(offer);
|
|
237
|
+
needsAnswerRestore = true;
|
|
238
|
+
}
|
|
224
239
|
} else {
|
|
225
240
|
yield pc.setLocalDescription(offer);
|
|
226
241
|
}
|
|
@@ -229,9 +244,49 @@ function createOffer(pc, trackNames) {
|
|
|
229
244
|
if (!localDescription) {
|
|
230
245
|
throw new Error("Failed to create local description");
|
|
231
246
|
}
|
|
232
|
-
|
|
247
|
+
let sdp = localDescription.sdp;
|
|
248
|
+
if (needsAnswerRestore && trackNames && trackNames.length > 0) {
|
|
249
|
+
sdp = rewriteMids(sdp, trackNames);
|
|
250
|
+
}
|
|
251
|
+
return { sdp, needsAnswerRestore };
|
|
233
252
|
});
|
|
234
253
|
}
|
|
254
|
+
function buildMidMapping(transceivers) {
|
|
255
|
+
var _a;
|
|
256
|
+
const localToRemote = /* @__PURE__ */ new Map();
|
|
257
|
+
const remoteToLocal = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const entry of transceivers) {
|
|
259
|
+
const mid = (_a = entry.transceiver) == null ? void 0 : _a.mid;
|
|
260
|
+
if (mid) {
|
|
261
|
+
localToRemote.set(mid, entry.name);
|
|
262
|
+
remoteToLocal.set(entry.name, mid);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { localToRemote, remoteToLocal };
|
|
266
|
+
}
|
|
267
|
+
function restoreAnswerMids(sdp, remoteToLocal) {
|
|
268
|
+
const lines = sdp.split("\r\n");
|
|
269
|
+
for (let i = 0; i < lines.length; i++) {
|
|
270
|
+
if (lines[i].startsWith("a=mid:")) {
|
|
271
|
+
const remoteMid = lines[i].substring("a=mid:".length);
|
|
272
|
+
const localMid = remoteToLocal.get(remoteMid);
|
|
273
|
+
if (localMid !== void 0) {
|
|
274
|
+
lines[i] = `a=mid:${localMid}`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (lines[i].startsWith("a=group:BUNDLE ")) {
|
|
278
|
+
const parts = lines[i].split(" ");
|
|
279
|
+
for (let j = 1; j < parts.length; j++) {
|
|
280
|
+
const localMid = remoteToLocal.get(parts[j]);
|
|
281
|
+
if (localMid !== void 0) {
|
|
282
|
+
parts[j] = localMid;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
lines[i] = parts.join(" ");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return lines.join("\r\n");
|
|
289
|
+
}
|
|
235
290
|
function setRemoteDescription(pc, sdp) {
|
|
236
291
|
return __async(this, null, function* () {
|
|
237
292
|
const sessionDescription = new RTCSessionDescription({
|
|
@@ -280,14 +335,21 @@ function waitForIceGathering(pc, timeoutMs = 5e3) {
|
|
|
280
335
|
}, timeoutMs);
|
|
281
336
|
});
|
|
282
337
|
}
|
|
283
|
-
function sendMessage(channel, command, data, scope = "application") {
|
|
338
|
+
function sendMessage(channel, command, data, scope = "application", maxBytes = DEFAULT_MAX_MESSAGE_BYTES) {
|
|
284
339
|
if (channel.readyState !== "open") {
|
|
285
340
|
throw new Error(`Data channel not open: ${channel.readyState}`);
|
|
286
341
|
}
|
|
287
342
|
const jsonData = typeof data === "string" ? JSON.parse(data) : data;
|
|
288
343
|
const inner = { type: command, data: jsonData };
|
|
289
344
|
const payload = { scope, data: inner };
|
|
290
|
-
|
|
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);
|
|
291
353
|
}
|
|
292
354
|
function parseMessage(data) {
|
|
293
355
|
if (typeof data === "string") {
|
|
@@ -359,6 +421,22 @@ var CoordinatorClient = class {
|
|
|
359
421
|
this.baseUrl = options.baseUrl;
|
|
360
422
|
this.jwtToken = options.jwtToken;
|
|
361
423
|
this.model = options.model;
|
|
424
|
+
this.abortController = new AbortController();
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Aborts any in-flight HTTP requests and polling loops.
|
|
428
|
+
* A fresh AbortController is created so the client remains reusable.
|
|
429
|
+
*/
|
|
430
|
+
abort() {
|
|
431
|
+
this.abortController.abort();
|
|
432
|
+
this.abortController = new AbortController();
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* The current abort signal, passed to every fetch() and sleep() call.
|
|
436
|
+
* Protected so subclasses can forward it to their own fetch calls.
|
|
437
|
+
*/
|
|
438
|
+
get signal() {
|
|
439
|
+
return this.abortController.signal;
|
|
362
440
|
}
|
|
363
441
|
/**
|
|
364
442
|
* Returns the authorization header with JWT Bearer token
|
|
@@ -379,7 +457,8 @@ var CoordinatorClient = class {
|
|
|
379
457
|
`${this.baseUrl}/ice_servers?model=${this.model}`,
|
|
380
458
|
{
|
|
381
459
|
method: "GET",
|
|
382
|
-
headers: this.getAuthHeaders()
|
|
460
|
+
headers: this.getAuthHeaders(),
|
|
461
|
+
signal: this.signal
|
|
383
462
|
}
|
|
384
463
|
);
|
|
385
464
|
if (!response.ok) {
|
|
@@ -413,7 +492,8 @@ var CoordinatorClient = class {
|
|
|
413
492
|
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
414
493
|
"Content-Type": "application/json"
|
|
415
494
|
}),
|
|
416
|
-
body: JSON.stringify(requestBody)
|
|
495
|
+
body: JSON.stringify(requestBody),
|
|
496
|
+
signal: this.signal
|
|
417
497
|
});
|
|
418
498
|
if (!response.ok) {
|
|
419
499
|
const errorText = yield response.text();
|
|
@@ -447,7 +527,8 @@ var CoordinatorClient = class {
|
|
|
447
527
|
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
448
528
|
{
|
|
449
529
|
method: "GET",
|
|
450
|
-
headers: this.getAuthHeaders()
|
|
530
|
+
headers: this.getAuthHeaders(),
|
|
531
|
+
signal: this.signal
|
|
451
532
|
}
|
|
452
533
|
);
|
|
453
534
|
if (!response.ok) {
|
|
@@ -460,12 +541,13 @@ var CoordinatorClient = class {
|
|
|
460
541
|
}
|
|
461
542
|
/**
|
|
462
543
|
* Terminates the current session by sending a DELETE request to the coordinator.
|
|
463
|
-
*
|
|
544
|
+
* No-op if no session has been created yet.
|
|
545
|
+
* @throws Error if the request fails (except for 404, which clears local state)
|
|
464
546
|
*/
|
|
465
547
|
terminateSession() {
|
|
466
548
|
return __async(this, null, function* () {
|
|
467
549
|
if (!this.currentSessionId) {
|
|
468
|
-
|
|
550
|
+
return;
|
|
469
551
|
}
|
|
470
552
|
console.debug(
|
|
471
553
|
"[CoordinatorClient] Terminating session:",
|
|
@@ -475,7 +557,8 @@ var CoordinatorClient = class {
|
|
|
475
557
|
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
476
558
|
{
|
|
477
559
|
method: "DELETE",
|
|
478
|
-
headers: this.getAuthHeaders()
|
|
560
|
+
headers: this.getAuthHeaders(),
|
|
561
|
+
signal: this.signal
|
|
479
562
|
}
|
|
480
563
|
);
|
|
481
564
|
if (response.ok) {
|
|
@@ -525,7 +608,8 @@ var CoordinatorClient = class {
|
|
|
525
608
|
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
526
609
|
"Content-Type": "application/json"
|
|
527
610
|
}),
|
|
528
|
-
body: JSON.stringify(requestBody)
|
|
611
|
+
body: JSON.stringify(requestBody),
|
|
612
|
+
signal: this.signal
|
|
529
613
|
}
|
|
530
614
|
);
|
|
531
615
|
if (response.status === 200) {
|
|
@@ -561,6 +645,9 @@ var CoordinatorClient = class {
|
|
|
561
645
|
let backoffMs = INITIAL_BACKOFF_MS;
|
|
562
646
|
let attempt = 0;
|
|
563
647
|
while (true) {
|
|
648
|
+
if (this.signal.aborted) {
|
|
649
|
+
throw new AbortError("SDP polling aborted");
|
|
650
|
+
}
|
|
564
651
|
if (attempt >= maxAttempts) {
|
|
565
652
|
throw new Error(
|
|
566
653
|
`SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
|
|
@@ -576,13 +663,14 @@ var CoordinatorClient = class {
|
|
|
576
663
|
method: "GET",
|
|
577
664
|
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
578
665
|
"Content-Type": "application/json"
|
|
579
|
-
})
|
|
666
|
+
}),
|
|
667
|
+
signal: this.signal
|
|
580
668
|
}
|
|
581
669
|
);
|
|
582
670
|
if (response.status === 200) {
|
|
583
671
|
const answerData = yield response.json();
|
|
584
672
|
console.debug("[CoordinatorClient] Received SDP answer via polling");
|
|
585
|
-
return answerData.sdp_answer;
|
|
673
|
+
return { sdpAnswer: answerData.sdp_answer, attempts: attempt };
|
|
586
674
|
}
|
|
587
675
|
if (response.status === 202) {
|
|
588
676
|
console.warn(
|
|
@@ -606,7 +694,7 @@ var CoordinatorClient = class {
|
|
|
606
694
|
* @param sessionId - The session ID to connect to
|
|
607
695
|
* @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
|
|
608
696
|
* @param maxAttempts - Optional maximum number of polling attempts before giving up
|
|
609
|
-
* @returns The SDP answer
|
|
697
|
+
* @returns The SDP answer and the number of polling attempts made (0 if answered immediately via PUT)
|
|
610
698
|
*/
|
|
611
699
|
connect(sessionId, sdpOffer, maxAttempts) {
|
|
612
700
|
return __async(this, null, function* () {
|
|
@@ -614,17 +702,34 @@ var CoordinatorClient = class {
|
|
|
614
702
|
if (sdpOffer) {
|
|
615
703
|
const answer = yield this.sendSdpOffer(sessionId, sdpOffer);
|
|
616
704
|
if (answer !== null) {
|
|
617
|
-
return answer;
|
|
705
|
+
return { sdpAnswer: answer, sdpPollingAttempts: 0 };
|
|
618
706
|
}
|
|
619
707
|
}
|
|
620
|
-
|
|
708
|
+
const result = yield this.pollSdpAnswer(sessionId, maxAttempts);
|
|
709
|
+
return { sdpAnswer: result.sdpAnswer, sdpPollingAttempts: result.attempts };
|
|
621
710
|
});
|
|
622
711
|
}
|
|
623
712
|
/**
|
|
624
|
-
*
|
|
713
|
+
* Abort-aware sleep. Resolves after `ms` milliseconds unless the
|
|
714
|
+
* abort signal fires first, in which case it rejects with AbortError.
|
|
625
715
|
*/
|
|
626
716
|
sleep(ms) {
|
|
627
|
-
return new Promise((resolve) =>
|
|
717
|
+
return new Promise((resolve, reject) => {
|
|
718
|
+
const { signal } = this;
|
|
719
|
+
if (signal.aborted) {
|
|
720
|
+
reject(new AbortError("Sleep aborted"));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const timer = setTimeout(() => {
|
|
724
|
+
signal.removeEventListener("abort", onAbort);
|
|
725
|
+
resolve();
|
|
726
|
+
}, ms);
|
|
727
|
+
const onAbort = () => {
|
|
728
|
+
clearTimeout(timer);
|
|
729
|
+
reject(new AbortError("Sleep aborted"));
|
|
730
|
+
};
|
|
731
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
732
|
+
});
|
|
628
733
|
}
|
|
629
734
|
};
|
|
630
735
|
|
|
@@ -646,7 +751,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
646
751
|
return __async(this, null, function* () {
|
|
647
752
|
console.debug("[LocalCoordinatorClient] Fetching ICE servers...");
|
|
648
753
|
const response = yield fetch(`${this.localBaseUrl}/ice_servers`, {
|
|
649
|
-
method: "GET"
|
|
754
|
+
method: "GET",
|
|
755
|
+
signal: this.signal
|
|
650
756
|
});
|
|
651
757
|
if (!response.ok) {
|
|
652
758
|
throw new Error("Failed to get ICE servers from local coordinator.");
|
|
@@ -670,7 +776,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
670
776
|
console.debug("[LocalCoordinatorClient] Creating local session...");
|
|
671
777
|
this.sdpOffer = sdpOffer;
|
|
672
778
|
const response = yield fetch(`${this.localBaseUrl}/start_session`, {
|
|
673
|
-
method: "POST"
|
|
779
|
+
method: "POST",
|
|
780
|
+
signal: this.signal
|
|
674
781
|
});
|
|
675
782
|
if (!response.ok) {
|
|
676
783
|
throw new Error("Failed to send local start session command.");
|
|
@@ -681,9 +788,10 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
681
788
|
}
|
|
682
789
|
/**
|
|
683
790
|
* Connects to the local session by posting SDP params to /sdp_params.
|
|
791
|
+
* Local connections are always immediate (no polling).
|
|
684
792
|
* @param sessionId - The session ID (ignored for local)
|
|
685
793
|
* @param sdpMessage - The SDP offer from the local WebRTC peer connection
|
|
686
|
-
* @returns The SDP answer
|
|
794
|
+
* @returns The SDP answer and polling attempts (always 0 for local)
|
|
687
795
|
*/
|
|
688
796
|
connect(sessionId, sdpMessage) {
|
|
689
797
|
return __async(this, null, function* () {
|
|
@@ -698,7 +806,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
698
806
|
headers: {
|
|
699
807
|
"Content-Type": "application/json"
|
|
700
808
|
},
|
|
701
|
-
body: JSON.stringify(sdpBody)
|
|
809
|
+
body: JSON.stringify(sdpBody),
|
|
810
|
+
signal: this.signal
|
|
702
811
|
});
|
|
703
812
|
if (!response.ok) {
|
|
704
813
|
if (response.status === 409) {
|
|
@@ -708,14 +817,15 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
708
817
|
}
|
|
709
818
|
const sdpAnswer = yield response.json();
|
|
710
819
|
console.debug("[LocalCoordinatorClient] Received SDP answer");
|
|
711
|
-
return sdpAnswer.sdp;
|
|
820
|
+
return { sdpAnswer: sdpAnswer.sdp, sdpPollingAttempts: 0 };
|
|
712
821
|
});
|
|
713
822
|
}
|
|
714
823
|
terminateSession() {
|
|
715
824
|
return __async(this, null, function* () {
|
|
716
825
|
console.debug("[LocalCoordinatorClient] Stopping local session...");
|
|
717
826
|
yield fetch(`${this.localBaseUrl}/stop_session`, {
|
|
718
|
-
method: "POST"
|
|
827
|
+
method: "POST",
|
|
828
|
+
signal: this.signal
|
|
719
829
|
});
|
|
720
830
|
});
|
|
721
831
|
}
|
|
@@ -790,12 +900,21 @@ var GPUMachineClient = class {
|
|
|
790
900
|
);
|
|
791
901
|
}
|
|
792
902
|
const trackNames = entries.map((e) => e.name);
|
|
793
|
-
const
|
|
903
|
+
const { sdp, needsAnswerRestore } = yield createOffer(
|
|
904
|
+
this.peerConnection,
|
|
905
|
+
trackNames
|
|
906
|
+
);
|
|
907
|
+
if (needsAnswerRestore) {
|
|
908
|
+
this.midMapping = buildMidMapping(entries);
|
|
909
|
+
} else {
|
|
910
|
+
this.midMapping = void 0;
|
|
911
|
+
}
|
|
794
912
|
console.debug(
|
|
795
913
|
"[GPUMachineClient] Created SDP offer with MIDs:",
|
|
796
|
-
trackNames
|
|
914
|
+
trackNames,
|
|
915
|
+
needsAnswerRestore ? "(needs answer restore)" : "(native munging)"
|
|
797
916
|
);
|
|
798
|
-
return
|
|
917
|
+
return sdp;
|
|
799
918
|
});
|
|
800
919
|
}
|
|
801
920
|
/**
|
|
@@ -843,8 +962,18 @@ var GPUMachineClient = class {
|
|
|
843
962
|
);
|
|
844
963
|
}
|
|
845
964
|
this.setStatus("connecting");
|
|
965
|
+
this.iceStartTime = performance.now();
|
|
966
|
+
this.iceNegotiationMs = void 0;
|
|
967
|
+
this.dataChannelMs = void 0;
|
|
846
968
|
try {
|
|
847
|
-
|
|
969
|
+
let answer = sdpAnswer;
|
|
970
|
+
if (this.midMapping) {
|
|
971
|
+
answer = restoreAnswerMids(
|
|
972
|
+
answer,
|
|
973
|
+
this.midMapping.remoteToLocal
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
yield setRemoteDescription(this.peerConnection, answer);
|
|
848
977
|
console.debug("[GPUMachineClient] Remote description set");
|
|
849
978
|
} catch (error) {
|
|
850
979
|
console.error("[GPUMachineClient] Failed to connect:", error);
|
|
@@ -872,8 +1001,10 @@ var GPUMachineClient = class {
|
|
|
872
1001
|
this.peerConnection = void 0;
|
|
873
1002
|
}
|
|
874
1003
|
this.transceiverMap.clear();
|
|
1004
|
+
this.midMapping = void 0;
|
|
875
1005
|
this.peerConnected = false;
|
|
876
1006
|
this.dataChannelOpen = false;
|
|
1007
|
+
this.resetConnectionTimings();
|
|
877
1008
|
this.setStatus("disconnected");
|
|
878
1009
|
console.debug("[GPUMachineClient] Disconnected");
|
|
879
1010
|
});
|
|
@@ -898,6 +1029,14 @@ var GPUMachineClient = class {
|
|
|
898
1029
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
899
1030
|
// Messaging
|
|
900
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
|
+
}
|
|
901
1040
|
/**
|
|
902
1041
|
* Sends a command to the GPU machine via the data channel.
|
|
903
1042
|
* @param command The command to send
|
|
@@ -909,7 +1048,13 @@ var GPUMachineClient = class {
|
|
|
909
1048
|
throw new Error("[GPUMachineClient] Data channel not available");
|
|
910
1049
|
}
|
|
911
1050
|
try {
|
|
912
|
-
sendMessage(
|
|
1051
|
+
sendMessage(
|
|
1052
|
+
this.dataChannel,
|
|
1053
|
+
command,
|
|
1054
|
+
data,
|
|
1055
|
+
scope,
|
|
1056
|
+
this.maxMessageBytes
|
|
1057
|
+
);
|
|
913
1058
|
} catch (error) {
|
|
914
1059
|
console.warn("[GPUMachineClient] Failed to send message:", error);
|
|
915
1060
|
}
|
|
@@ -1037,6 +1182,24 @@ var GPUMachineClient = class {
|
|
|
1037
1182
|
getStats() {
|
|
1038
1183
|
return this.stats;
|
|
1039
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
|
+
}
|
|
1040
1203
|
startStatsPolling() {
|
|
1041
1204
|
this.stopStatsPolling();
|
|
1042
1205
|
this.statsInterval = setInterval(() => __async(this, null, function* () {
|
|
@@ -1080,6 +1243,9 @@ var GPUMachineClient = class {
|
|
|
1080
1243
|
if (state) {
|
|
1081
1244
|
switch (state) {
|
|
1082
1245
|
case "connected":
|
|
1246
|
+
if (this.iceStartTime != null && this.iceNegotiationMs == null) {
|
|
1247
|
+
this.iceNegotiationMs = performance.now() - this.iceStartTime;
|
|
1248
|
+
}
|
|
1083
1249
|
this.peerConnected = true;
|
|
1084
1250
|
this.checkFullyConnected();
|
|
1085
1251
|
break;
|
|
@@ -1096,13 +1262,19 @@ var GPUMachineClient = class {
|
|
|
1096
1262
|
}
|
|
1097
1263
|
};
|
|
1098
1264
|
this.peerConnection.ontrack = (event) => {
|
|
1099
|
-
var _a;
|
|
1100
|
-
|
|
1101
|
-
const
|
|
1265
|
+
var _a, _b;
|
|
1266
|
+
let trackName;
|
|
1267
|
+
for (const [name, entry] of this.transceiverMap) {
|
|
1268
|
+
if (entry.transceiver === event.transceiver) {
|
|
1269
|
+
trackName = name;
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
trackName != null ? trackName : trackName = (_a = event.transceiver.mid) != null ? _a : `unknown-${event.track.id}`;
|
|
1102
1274
|
console.debug(
|
|
1103
|
-
`[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${mid})`
|
|
1275
|
+
`[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${event.transceiver.mid})`
|
|
1104
1276
|
);
|
|
1105
|
-
const stream = (
|
|
1277
|
+
const stream = (_b = event.streams[0]) != null ? _b : new MediaStream([event.track]);
|
|
1106
1278
|
this.emit("trackReceived", trackName, event.track, stream);
|
|
1107
1279
|
};
|
|
1108
1280
|
this.peerConnection.onicecandidate = (event) => {
|
|
@@ -1123,6 +1295,9 @@ var GPUMachineClient = class {
|
|
|
1123
1295
|
if (!this.dataChannel) return;
|
|
1124
1296
|
this.dataChannel.onopen = () => {
|
|
1125
1297
|
console.debug("[GPUMachineClient] Data channel open");
|
|
1298
|
+
if (this.iceStartTime != null && this.dataChannelMs == null) {
|
|
1299
|
+
this.dataChannelMs = performance.now() - this.iceStartTime;
|
|
1300
|
+
}
|
|
1126
1301
|
this.dataChannelOpen = true;
|
|
1127
1302
|
this.startPing();
|
|
1128
1303
|
this.checkFullyConnected();
|
|
@@ -1162,13 +1337,13 @@ var GPUMachineClient = class {
|
|
|
1162
1337
|
// src/core/Reactor.ts
|
|
1163
1338
|
var import_zod2 = require("zod");
|
|
1164
1339
|
var LOCAL_COORDINATOR_URL = "http://localhost:8080";
|
|
1165
|
-
var
|
|
1340
|
+
var DEFAULT_BASE_URL = "https://api.reactor.inc";
|
|
1166
1341
|
var TrackConfigSchema = import_zod2.z.object({
|
|
1167
1342
|
name: import_zod2.z.string(),
|
|
1168
1343
|
kind: import_zod2.z.enum(["audio", "video"])
|
|
1169
1344
|
});
|
|
1170
1345
|
var OptionsSchema = import_zod2.z.object({
|
|
1171
|
-
|
|
1346
|
+
apiUrl: import_zod2.z.string().default(DEFAULT_BASE_URL),
|
|
1172
1347
|
modelName: import_zod2.z.string(),
|
|
1173
1348
|
local: import_zod2.z.boolean().default(false),
|
|
1174
1349
|
/**
|
|
@@ -1193,12 +1368,12 @@ var Reactor = class {
|
|
|
1193
1368
|
// Generic event map
|
|
1194
1369
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
1195
1370
|
const validatedOptions = OptionsSchema.parse(options);
|
|
1196
|
-
this.coordinatorUrl = validatedOptions.
|
|
1371
|
+
this.coordinatorUrl = validatedOptions.apiUrl;
|
|
1197
1372
|
this.model = validatedOptions.modelName;
|
|
1198
1373
|
this.local = validatedOptions.local;
|
|
1199
1374
|
this.receive = validatedOptions.receive;
|
|
1200
1375
|
this.send = validatedOptions.send;
|
|
1201
|
-
if (this.local && options.
|
|
1376
|
+
if (this.local && options.apiUrl === void 0) {
|
|
1202
1377
|
this.coordinatorUrl = LOCAL_COORDINATOR_URL;
|
|
1203
1378
|
}
|
|
1204
1379
|
}
|
|
@@ -1319,14 +1494,14 @@ var Reactor = class {
|
|
|
1319
1494
|
receive: this.receive
|
|
1320
1495
|
});
|
|
1321
1496
|
try {
|
|
1322
|
-
const sdpAnswer = yield this.coordinatorClient.connect(
|
|
1497
|
+
const { sdpAnswer } = yield this.coordinatorClient.connect(
|
|
1323
1498
|
this.sessionId,
|
|
1324
1499
|
sdpOffer,
|
|
1325
1500
|
options == null ? void 0 : options.maxAttempts
|
|
1326
1501
|
);
|
|
1327
1502
|
yield this.machineClient.connect(sdpAnswer);
|
|
1328
|
-
this.setStatus("ready");
|
|
1329
1503
|
} catch (error) {
|
|
1504
|
+
if (isAbortError(error)) return;
|
|
1330
1505
|
let recoverable = false;
|
|
1331
1506
|
if (error instanceof ConflictError) {
|
|
1332
1507
|
recoverable = true;
|
|
@@ -1336,7 +1511,7 @@ var Reactor = class {
|
|
|
1336
1511
|
this.createError(
|
|
1337
1512
|
"RECONNECTION_FAILED",
|
|
1338
1513
|
`Failed to reconnect: ${error}`,
|
|
1339
|
-
"
|
|
1514
|
+
"api",
|
|
1340
1515
|
true
|
|
1341
1516
|
);
|
|
1342
1517
|
}
|
|
@@ -1359,6 +1534,7 @@ var Reactor = class {
|
|
|
1359
1534
|
throw new Error("Already connected or connecting");
|
|
1360
1535
|
}
|
|
1361
1536
|
this.setStatus("connecting");
|
|
1537
|
+
this.connectStartTime = performance.now();
|
|
1362
1538
|
try {
|
|
1363
1539
|
console.debug(
|
|
1364
1540
|
"[Reactor] Connecting to coordinator with authenticated URL"
|
|
@@ -1376,20 +1552,33 @@ var Reactor = class {
|
|
|
1376
1552
|
send: this.send,
|
|
1377
1553
|
receive: this.receive
|
|
1378
1554
|
});
|
|
1555
|
+
const tSession = performance.now();
|
|
1379
1556
|
const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
|
|
1557
|
+
const sessionCreationMs = performance.now() - tSession;
|
|
1380
1558
|
this.setSessionId(sessionId);
|
|
1381
|
-
const
|
|
1559
|
+
const tSdp = performance.now();
|
|
1560
|
+
const { sdpAnswer, sdpPollingAttempts } = yield this.coordinatorClient.connect(
|
|
1382
1561
|
sessionId,
|
|
1383
1562
|
void 0,
|
|
1384
1563
|
options == null ? void 0 : options.maxAttempts
|
|
1385
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
|
+
};
|
|
1386
1574
|
yield this.machineClient.connect(sdpAnswer);
|
|
1387
1575
|
} catch (error) {
|
|
1576
|
+
if (isAbortError(error)) return;
|
|
1388
1577
|
console.error("[Reactor] Connection failed:", error);
|
|
1389
1578
|
this.createError(
|
|
1390
1579
|
"CONNECTION_FAILED",
|
|
1391
1580
|
`Connection failed: ${error}`,
|
|
1392
|
-
"
|
|
1581
|
+
"api",
|
|
1393
1582
|
true
|
|
1394
1583
|
);
|
|
1395
1584
|
try {
|
|
@@ -1406,19 +1595,28 @@ var Reactor = class {
|
|
|
1406
1595
|
}
|
|
1407
1596
|
/**
|
|
1408
1597
|
* Sets up event handlers for the machine client.
|
|
1598
|
+
*
|
|
1599
|
+
* Each handler captures the client reference at registration time and
|
|
1600
|
+
* ignores events if this.machineClient has since changed (e.g. after
|
|
1601
|
+
* disconnect + reconnect), preventing stale WebRTC teardown events from
|
|
1602
|
+
* interfering with a new connection.
|
|
1409
1603
|
*/
|
|
1410
1604
|
setupMachineClientHandlers() {
|
|
1411
1605
|
if (!this.machineClient) return;
|
|
1412
|
-
this.machineClient
|
|
1606
|
+
const client = this.machineClient;
|
|
1607
|
+
client.on("message", (message, scope) => {
|
|
1608
|
+
if (this.machineClient !== client) return;
|
|
1413
1609
|
if (scope === "application") {
|
|
1414
1610
|
this.emit("message", message);
|
|
1415
1611
|
} else if (scope === "runtime") {
|
|
1416
1612
|
this.emit("runtimeMessage", message);
|
|
1417
1613
|
}
|
|
1418
1614
|
});
|
|
1419
|
-
|
|
1615
|
+
client.on("statusChanged", (status) => {
|
|
1616
|
+
if (this.machineClient !== client) return;
|
|
1420
1617
|
switch (status) {
|
|
1421
1618
|
case "connected":
|
|
1619
|
+
this.finalizeConnectionTimings(client);
|
|
1422
1620
|
this.setStatus("ready");
|
|
1423
1621
|
break;
|
|
1424
1622
|
case "disconnected":
|
|
@@ -1435,14 +1633,18 @@ var Reactor = class {
|
|
|
1435
1633
|
break;
|
|
1436
1634
|
}
|
|
1437
1635
|
});
|
|
1438
|
-
|
|
1636
|
+
client.on(
|
|
1439
1637
|
"trackReceived",
|
|
1440
1638
|
(name, track, stream) => {
|
|
1639
|
+
if (this.machineClient !== client) return;
|
|
1441
1640
|
this.emit("trackReceived", name, track, stream);
|
|
1442
1641
|
}
|
|
1443
1642
|
);
|
|
1444
|
-
|
|
1445
|
-
this.
|
|
1643
|
+
client.on("statsUpdate", (stats) => {
|
|
1644
|
+
if (this.machineClient !== client) return;
|
|
1645
|
+
this.emit("statsUpdate", __spreadProps(__spreadValues({}, stats), {
|
|
1646
|
+
connectionTimings: this.connectionTimings
|
|
1647
|
+
}));
|
|
1446
1648
|
});
|
|
1447
1649
|
}
|
|
1448
1650
|
/**
|
|
@@ -1451,10 +1653,12 @@ var Reactor = class {
|
|
|
1451
1653
|
*/
|
|
1452
1654
|
disconnect(recoverable = false) {
|
|
1453
1655
|
return __async(this, null, function* () {
|
|
1656
|
+
var _a;
|
|
1454
1657
|
if (this.status === "disconnected" && !this.sessionId) {
|
|
1455
1658
|
console.warn("[Reactor] Already disconnected");
|
|
1456
1659
|
return;
|
|
1457
1660
|
}
|
|
1661
|
+
(_a = this.coordinatorClient) == null ? void 0 : _a.abort();
|
|
1458
1662
|
if (this.coordinatorClient && !recoverable) {
|
|
1459
1663
|
try {
|
|
1460
1664
|
yield this.coordinatorClient.terminateSession();
|
|
@@ -1474,6 +1678,7 @@ var Reactor = class {
|
|
|
1474
1678
|
}
|
|
1475
1679
|
}
|
|
1476
1680
|
this.setStatus("disconnected");
|
|
1681
|
+
this.resetConnectionTimings();
|
|
1477
1682
|
if (!recoverable) {
|
|
1478
1683
|
this.setSessionExpiration(void 0);
|
|
1479
1684
|
this.setSessionId(void 0);
|
|
@@ -1536,7 +1741,23 @@ var Reactor = class {
|
|
|
1536
1741
|
}
|
|
1537
1742
|
getStats() {
|
|
1538
1743
|
var _a;
|
|
1539
|
-
|
|
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);
|
|
1540
1761
|
}
|
|
1541
1762
|
/**
|
|
1542
1763
|
* Create and store an error
|
|
@@ -1576,7 +1797,7 @@ var initReactorStore = (props) => {
|
|
|
1576
1797
|
};
|
|
1577
1798
|
var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
1578
1799
|
console.debug("[ReactorStore] Creating store", {
|
|
1579
|
-
|
|
1800
|
+
apiUrl: initProps.apiUrl,
|
|
1580
1801
|
jwtToken: initProps.jwtToken,
|
|
1581
1802
|
initialState: publicState
|
|
1582
1803
|
});
|
|
@@ -1744,7 +1965,7 @@ function ReactorProvider(_a) {
|
|
|
1744
1965
|
console.debug("[ReactorProvider] Reactor store created successfully");
|
|
1745
1966
|
}
|
|
1746
1967
|
const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
|
|
1747
|
-
const {
|
|
1968
|
+
const { apiUrl, modelName, local, receive, send } = props;
|
|
1748
1969
|
const maxAttempts = pollingOptions.maxAttempts;
|
|
1749
1970
|
(0, import_react3.useEffect)(() => {
|
|
1750
1971
|
const handleBeforeUnload = () => {
|
|
@@ -1797,7 +2018,7 @@ function ReactorProvider(_a) {
|
|
|
1797
2018
|
console.debug("[ReactorProvider] Updating reactor store");
|
|
1798
2019
|
storeRef.current = createReactorStore(
|
|
1799
2020
|
initReactorStore({
|
|
1800
|
-
|
|
2021
|
+
apiUrl,
|
|
1801
2022
|
modelName,
|
|
1802
2023
|
local,
|
|
1803
2024
|
receive,
|
|
@@ -1829,7 +2050,7 @@ function ReactorProvider(_a) {
|
|
|
1829
2050
|
});
|
|
1830
2051
|
};
|
|
1831
2052
|
}, [
|
|
1832
|
-
|
|
2053
|
+
apiUrl,
|
|
1833
2054
|
modelName,
|
|
1834
2055
|
autoConnect,
|
|
1835
2056
|
local,
|
|
@@ -2658,12 +2879,12 @@ function WebcamStream({
|
|
|
2658
2879
|
}
|
|
2659
2880
|
|
|
2660
2881
|
// src/utils/tokens.ts
|
|
2661
|
-
function
|
|
2662
|
-
return __async(this, arguments, function* (apiKey,
|
|
2882
|
+
function fetchInsecureToken(_0) {
|
|
2883
|
+
return __async(this, arguments, function* (apiKey, apiUrl = DEFAULT_BASE_URL) {
|
|
2663
2884
|
console.warn(
|
|
2664
|
-
"[Reactor] \u26A0\uFE0F SECURITY WARNING:
|
|
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."
|
|
2665
2886
|
);
|
|
2666
|
-
const response = yield fetch(`${
|
|
2887
|
+
const response = yield fetch(`${apiUrl}/tokens`, {
|
|
2667
2888
|
method: "GET",
|
|
2668
2889
|
headers: {
|
|
2669
2890
|
"X-API-Key": apiKey
|
|
@@ -2679,15 +2900,17 @@ function fetchInsecureJwtToken(_0) {
|
|
|
2679
2900
|
}
|
|
2680
2901
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2681
2902
|
0 && (module.exports = {
|
|
2903
|
+
AbortError,
|
|
2682
2904
|
ConflictError,
|
|
2683
|
-
|
|
2905
|
+
DEFAULT_BASE_URL,
|
|
2684
2906
|
Reactor,
|
|
2685
2907
|
ReactorController,
|
|
2686
2908
|
ReactorProvider,
|
|
2687
2909
|
ReactorView,
|
|
2688
2910
|
WebcamStream,
|
|
2689
2911
|
audio,
|
|
2690
|
-
|
|
2912
|
+
fetchInsecureToken,
|
|
2913
|
+
isAbortError,
|
|
2691
2914
|
useReactor,
|
|
2692
2915
|
useReactorInternalMessage,
|
|
2693
2916
|
useReactorMessage,
|