@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.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";
|
|
@@ -121,6 +129,7 @@ var IceServersResponseSchema = z.object({
|
|
|
121
129
|
// src/utils/webrtc.ts
|
|
122
130
|
var DEFAULT_DATA_CHANNEL_LABEL = "data";
|
|
123
131
|
var FORCE_RELAY_MODE = false;
|
|
132
|
+
var DEFAULT_MAX_MESSAGE_BYTES = 256 * 1024;
|
|
124
133
|
function createPeerConnection(config) {
|
|
125
134
|
return new RTCPeerConnection({
|
|
126
135
|
iceServers: config.iceServers,
|
|
@@ -167,13 +176,17 @@ function rewriteMids(sdp, trackNames) {
|
|
|
167
176
|
function createOffer(pc, trackNames) {
|
|
168
177
|
return __async(this, null, function* () {
|
|
169
178
|
const offer = yield pc.createOffer();
|
|
179
|
+
let needsAnswerRestore = false;
|
|
170
180
|
if (trackNames && trackNames.length > 0 && offer.sdp) {
|
|
171
181
|
const munged = rewriteMids(offer.sdp, trackNames);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
182
|
+
try {
|
|
183
|
+
yield pc.setLocalDescription(
|
|
184
|
+
new RTCSessionDescription({ type: "offer", sdp: munged })
|
|
185
|
+
);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
yield pc.setLocalDescription(offer);
|
|
188
|
+
needsAnswerRestore = true;
|
|
189
|
+
}
|
|
177
190
|
} else {
|
|
178
191
|
yield pc.setLocalDescription(offer);
|
|
179
192
|
}
|
|
@@ -182,9 +195,49 @@ function createOffer(pc, trackNames) {
|
|
|
182
195
|
if (!localDescription) {
|
|
183
196
|
throw new Error("Failed to create local description");
|
|
184
197
|
}
|
|
185
|
-
|
|
198
|
+
let sdp = localDescription.sdp;
|
|
199
|
+
if (needsAnswerRestore && trackNames && trackNames.length > 0) {
|
|
200
|
+
sdp = rewriteMids(sdp, trackNames);
|
|
201
|
+
}
|
|
202
|
+
return { sdp, needsAnswerRestore };
|
|
186
203
|
});
|
|
187
204
|
}
|
|
205
|
+
function buildMidMapping(transceivers) {
|
|
206
|
+
var _a;
|
|
207
|
+
const localToRemote = /* @__PURE__ */ new Map();
|
|
208
|
+
const remoteToLocal = /* @__PURE__ */ new Map();
|
|
209
|
+
for (const entry of transceivers) {
|
|
210
|
+
const mid = (_a = entry.transceiver) == null ? void 0 : _a.mid;
|
|
211
|
+
if (mid) {
|
|
212
|
+
localToRemote.set(mid, entry.name);
|
|
213
|
+
remoteToLocal.set(entry.name, mid);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { localToRemote, remoteToLocal };
|
|
217
|
+
}
|
|
218
|
+
function restoreAnswerMids(sdp, remoteToLocal) {
|
|
219
|
+
const lines = sdp.split("\r\n");
|
|
220
|
+
for (let i = 0; i < lines.length; i++) {
|
|
221
|
+
if (lines[i].startsWith("a=mid:")) {
|
|
222
|
+
const remoteMid = lines[i].substring("a=mid:".length);
|
|
223
|
+
const localMid = remoteToLocal.get(remoteMid);
|
|
224
|
+
if (localMid !== void 0) {
|
|
225
|
+
lines[i] = `a=mid:${localMid}`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (lines[i].startsWith("a=group:BUNDLE ")) {
|
|
229
|
+
const parts = lines[i].split(" ");
|
|
230
|
+
for (let j = 1; j < parts.length; j++) {
|
|
231
|
+
const localMid = remoteToLocal.get(parts[j]);
|
|
232
|
+
if (localMid !== void 0) {
|
|
233
|
+
parts[j] = localMid;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
lines[i] = parts.join(" ");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return lines.join("\r\n");
|
|
240
|
+
}
|
|
188
241
|
function setRemoteDescription(pc, sdp) {
|
|
189
242
|
return __async(this, null, function* () {
|
|
190
243
|
const sessionDescription = new RTCSessionDescription({
|
|
@@ -233,14 +286,21 @@ function waitForIceGathering(pc, timeoutMs = 5e3) {
|
|
|
233
286
|
}, timeoutMs);
|
|
234
287
|
});
|
|
235
288
|
}
|
|
236
|
-
function sendMessage(channel, command, data, scope = "application") {
|
|
289
|
+
function sendMessage(channel, command, data, scope = "application", maxBytes = DEFAULT_MAX_MESSAGE_BYTES) {
|
|
237
290
|
if (channel.readyState !== "open") {
|
|
238
291
|
throw new Error(`Data channel not open: ${channel.readyState}`);
|
|
239
292
|
}
|
|
240
293
|
const jsonData = typeof data === "string" ? JSON.parse(data) : data;
|
|
241
294
|
const inner = { type: command, data: jsonData };
|
|
242
295
|
const payload = { scope, data: inner };
|
|
243
|
-
|
|
296
|
+
const serialized = JSON.stringify(payload);
|
|
297
|
+
const byteLength = new TextEncoder().encode(serialized).byteLength;
|
|
298
|
+
if (byteLength > maxBytes) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Data channel message too large: ${byteLength} bytes exceeds limit of ${maxBytes} bytes (command: "${command}")`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
channel.send(serialized);
|
|
244
304
|
}
|
|
245
305
|
function parseMessage(data) {
|
|
246
306
|
if (typeof data === "string") {
|
|
@@ -312,6 +372,22 @@ var CoordinatorClient = class {
|
|
|
312
372
|
this.baseUrl = options.baseUrl;
|
|
313
373
|
this.jwtToken = options.jwtToken;
|
|
314
374
|
this.model = options.model;
|
|
375
|
+
this.abortController = new AbortController();
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Aborts any in-flight HTTP requests and polling loops.
|
|
379
|
+
* A fresh AbortController is created so the client remains reusable.
|
|
380
|
+
*/
|
|
381
|
+
abort() {
|
|
382
|
+
this.abortController.abort();
|
|
383
|
+
this.abortController = new AbortController();
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* The current abort signal, passed to every fetch() and sleep() call.
|
|
387
|
+
* Protected so subclasses can forward it to their own fetch calls.
|
|
388
|
+
*/
|
|
389
|
+
get signal() {
|
|
390
|
+
return this.abortController.signal;
|
|
315
391
|
}
|
|
316
392
|
/**
|
|
317
393
|
* Returns the authorization header with JWT Bearer token
|
|
@@ -332,7 +408,8 @@ var CoordinatorClient = class {
|
|
|
332
408
|
`${this.baseUrl}/ice_servers?model=${this.model}`,
|
|
333
409
|
{
|
|
334
410
|
method: "GET",
|
|
335
|
-
headers: this.getAuthHeaders()
|
|
411
|
+
headers: this.getAuthHeaders(),
|
|
412
|
+
signal: this.signal
|
|
336
413
|
}
|
|
337
414
|
);
|
|
338
415
|
if (!response.ok) {
|
|
@@ -366,7 +443,8 @@ var CoordinatorClient = class {
|
|
|
366
443
|
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
367
444
|
"Content-Type": "application/json"
|
|
368
445
|
}),
|
|
369
|
-
body: JSON.stringify(requestBody)
|
|
446
|
+
body: JSON.stringify(requestBody),
|
|
447
|
+
signal: this.signal
|
|
370
448
|
});
|
|
371
449
|
if (!response.ok) {
|
|
372
450
|
const errorText = yield response.text();
|
|
@@ -400,7 +478,8 @@ var CoordinatorClient = class {
|
|
|
400
478
|
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
401
479
|
{
|
|
402
480
|
method: "GET",
|
|
403
|
-
headers: this.getAuthHeaders()
|
|
481
|
+
headers: this.getAuthHeaders(),
|
|
482
|
+
signal: this.signal
|
|
404
483
|
}
|
|
405
484
|
);
|
|
406
485
|
if (!response.ok) {
|
|
@@ -413,12 +492,13 @@ var CoordinatorClient = class {
|
|
|
413
492
|
}
|
|
414
493
|
/**
|
|
415
494
|
* Terminates the current session by sending a DELETE request to the coordinator.
|
|
416
|
-
*
|
|
495
|
+
* No-op if no session has been created yet.
|
|
496
|
+
* @throws Error if the request fails (except for 404, which clears local state)
|
|
417
497
|
*/
|
|
418
498
|
terminateSession() {
|
|
419
499
|
return __async(this, null, function* () {
|
|
420
500
|
if (!this.currentSessionId) {
|
|
421
|
-
|
|
501
|
+
return;
|
|
422
502
|
}
|
|
423
503
|
console.debug(
|
|
424
504
|
"[CoordinatorClient] Terminating session:",
|
|
@@ -428,7 +508,8 @@ var CoordinatorClient = class {
|
|
|
428
508
|
`${this.baseUrl}/sessions/${this.currentSessionId}`,
|
|
429
509
|
{
|
|
430
510
|
method: "DELETE",
|
|
431
|
-
headers: this.getAuthHeaders()
|
|
511
|
+
headers: this.getAuthHeaders(),
|
|
512
|
+
signal: this.signal
|
|
432
513
|
}
|
|
433
514
|
);
|
|
434
515
|
if (response.ok) {
|
|
@@ -478,7 +559,8 @@ var CoordinatorClient = class {
|
|
|
478
559
|
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
479
560
|
"Content-Type": "application/json"
|
|
480
561
|
}),
|
|
481
|
-
body: JSON.stringify(requestBody)
|
|
562
|
+
body: JSON.stringify(requestBody),
|
|
563
|
+
signal: this.signal
|
|
482
564
|
}
|
|
483
565
|
);
|
|
484
566
|
if (response.status === 200) {
|
|
@@ -514,6 +596,9 @@ var CoordinatorClient = class {
|
|
|
514
596
|
let backoffMs = INITIAL_BACKOFF_MS;
|
|
515
597
|
let attempt = 0;
|
|
516
598
|
while (true) {
|
|
599
|
+
if (this.signal.aborted) {
|
|
600
|
+
throw new AbortError("SDP polling aborted");
|
|
601
|
+
}
|
|
517
602
|
if (attempt >= maxAttempts) {
|
|
518
603
|
throw new Error(
|
|
519
604
|
`SDP polling exceeded maximum attempts (${maxAttempts}) for session ${sessionId}`
|
|
@@ -529,13 +614,14 @@ var CoordinatorClient = class {
|
|
|
529
614
|
method: "GET",
|
|
530
615
|
headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
|
|
531
616
|
"Content-Type": "application/json"
|
|
532
|
-
})
|
|
617
|
+
}),
|
|
618
|
+
signal: this.signal
|
|
533
619
|
}
|
|
534
620
|
);
|
|
535
621
|
if (response.status === 200) {
|
|
536
622
|
const answerData = yield response.json();
|
|
537
623
|
console.debug("[CoordinatorClient] Received SDP answer via polling");
|
|
538
|
-
return answerData.sdp_answer;
|
|
624
|
+
return { sdpAnswer: answerData.sdp_answer, attempts: attempt };
|
|
539
625
|
}
|
|
540
626
|
if (response.status === 202) {
|
|
541
627
|
console.warn(
|
|
@@ -559,7 +645,7 @@ var CoordinatorClient = class {
|
|
|
559
645
|
* @param sessionId - The session ID to connect to
|
|
560
646
|
* @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
|
|
561
647
|
* @param maxAttempts - Optional maximum number of polling attempts before giving up
|
|
562
|
-
* @returns The SDP answer
|
|
648
|
+
* @returns The SDP answer and the number of polling attempts made (0 if answered immediately via PUT)
|
|
563
649
|
*/
|
|
564
650
|
connect(sessionId, sdpOffer, maxAttempts) {
|
|
565
651
|
return __async(this, null, function* () {
|
|
@@ -567,17 +653,34 @@ var CoordinatorClient = class {
|
|
|
567
653
|
if (sdpOffer) {
|
|
568
654
|
const answer = yield this.sendSdpOffer(sessionId, sdpOffer);
|
|
569
655
|
if (answer !== null) {
|
|
570
|
-
return answer;
|
|
656
|
+
return { sdpAnswer: answer, sdpPollingAttempts: 0 };
|
|
571
657
|
}
|
|
572
658
|
}
|
|
573
|
-
|
|
659
|
+
const result = yield this.pollSdpAnswer(sessionId, maxAttempts);
|
|
660
|
+
return { sdpAnswer: result.sdpAnswer, sdpPollingAttempts: result.attempts };
|
|
574
661
|
});
|
|
575
662
|
}
|
|
576
663
|
/**
|
|
577
|
-
*
|
|
664
|
+
* Abort-aware sleep. Resolves after `ms` milliseconds unless the
|
|
665
|
+
* abort signal fires first, in which case it rejects with AbortError.
|
|
578
666
|
*/
|
|
579
667
|
sleep(ms) {
|
|
580
|
-
return new Promise((resolve) =>
|
|
668
|
+
return new Promise((resolve, reject) => {
|
|
669
|
+
const { signal } = this;
|
|
670
|
+
if (signal.aborted) {
|
|
671
|
+
reject(new AbortError("Sleep aborted"));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const timer = setTimeout(() => {
|
|
675
|
+
signal.removeEventListener("abort", onAbort);
|
|
676
|
+
resolve();
|
|
677
|
+
}, ms);
|
|
678
|
+
const onAbort = () => {
|
|
679
|
+
clearTimeout(timer);
|
|
680
|
+
reject(new AbortError("Sleep aborted"));
|
|
681
|
+
};
|
|
682
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
683
|
+
});
|
|
581
684
|
}
|
|
582
685
|
};
|
|
583
686
|
|
|
@@ -599,7 +702,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
599
702
|
return __async(this, null, function* () {
|
|
600
703
|
console.debug("[LocalCoordinatorClient] Fetching ICE servers...");
|
|
601
704
|
const response = yield fetch(`${this.localBaseUrl}/ice_servers`, {
|
|
602
|
-
method: "GET"
|
|
705
|
+
method: "GET",
|
|
706
|
+
signal: this.signal
|
|
603
707
|
});
|
|
604
708
|
if (!response.ok) {
|
|
605
709
|
throw new Error("Failed to get ICE servers from local coordinator.");
|
|
@@ -623,7 +727,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
623
727
|
console.debug("[LocalCoordinatorClient] Creating local session...");
|
|
624
728
|
this.sdpOffer = sdpOffer;
|
|
625
729
|
const response = yield fetch(`${this.localBaseUrl}/start_session`, {
|
|
626
|
-
method: "POST"
|
|
730
|
+
method: "POST",
|
|
731
|
+
signal: this.signal
|
|
627
732
|
});
|
|
628
733
|
if (!response.ok) {
|
|
629
734
|
throw new Error("Failed to send local start session command.");
|
|
@@ -634,9 +739,10 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
634
739
|
}
|
|
635
740
|
/**
|
|
636
741
|
* Connects to the local session by posting SDP params to /sdp_params.
|
|
742
|
+
* Local connections are always immediate (no polling).
|
|
637
743
|
* @param sessionId - The session ID (ignored for local)
|
|
638
744
|
* @param sdpMessage - The SDP offer from the local WebRTC peer connection
|
|
639
|
-
* @returns The SDP answer
|
|
745
|
+
* @returns The SDP answer and polling attempts (always 0 for local)
|
|
640
746
|
*/
|
|
641
747
|
connect(sessionId, sdpMessage) {
|
|
642
748
|
return __async(this, null, function* () {
|
|
@@ -651,7 +757,8 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
651
757
|
headers: {
|
|
652
758
|
"Content-Type": "application/json"
|
|
653
759
|
},
|
|
654
|
-
body: JSON.stringify(sdpBody)
|
|
760
|
+
body: JSON.stringify(sdpBody),
|
|
761
|
+
signal: this.signal
|
|
655
762
|
});
|
|
656
763
|
if (!response.ok) {
|
|
657
764
|
if (response.status === 409) {
|
|
@@ -661,14 +768,15 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
661
768
|
}
|
|
662
769
|
const sdpAnswer = yield response.json();
|
|
663
770
|
console.debug("[LocalCoordinatorClient] Received SDP answer");
|
|
664
|
-
return sdpAnswer.sdp;
|
|
771
|
+
return { sdpAnswer: sdpAnswer.sdp, sdpPollingAttempts: 0 };
|
|
665
772
|
});
|
|
666
773
|
}
|
|
667
774
|
terminateSession() {
|
|
668
775
|
return __async(this, null, function* () {
|
|
669
776
|
console.debug("[LocalCoordinatorClient] Stopping local session...");
|
|
670
777
|
yield fetch(`${this.localBaseUrl}/stop_session`, {
|
|
671
|
-
method: "POST"
|
|
778
|
+
method: "POST",
|
|
779
|
+
signal: this.signal
|
|
672
780
|
});
|
|
673
781
|
});
|
|
674
782
|
}
|
|
@@ -743,12 +851,21 @@ var GPUMachineClient = class {
|
|
|
743
851
|
);
|
|
744
852
|
}
|
|
745
853
|
const trackNames = entries.map((e) => e.name);
|
|
746
|
-
const
|
|
854
|
+
const { sdp, needsAnswerRestore } = yield createOffer(
|
|
855
|
+
this.peerConnection,
|
|
856
|
+
trackNames
|
|
857
|
+
);
|
|
858
|
+
if (needsAnswerRestore) {
|
|
859
|
+
this.midMapping = buildMidMapping(entries);
|
|
860
|
+
} else {
|
|
861
|
+
this.midMapping = void 0;
|
|
862
|
+
}
|
|
747
863
|
console.debug(
|
|
748
864
|
"[GPUMachineClient] Created SDP offer with MIDs:",
|
|
749
|
-
trackNames
|
|
865
|
+
trackNames,
|
|
866
|
+
needsAnswerRestore ? "(needs answer restore)" : "(native munging)"
|
|
750
867
|
);
|
|
751
|
-
return
|
|
868
|
+
return sdp;
|
|
752
869
|
});
|
|
753
870
|
}
|
|
754
871
|
/**
|
|
@@ -796,8 +913,18 @@ var GPUMachineClient = class {
|
|
|
796
913
|
);
|
|
797
914
|
}
|
|
798
915
|
this.setStatus("connecting");
|
|
916
|
+
this.iceStartTime = performance.now();
|
|
917
|
+
this.iceNegotiationMs = void 0;
|
|
918
|
+
this.dataChannelMs = void 0;
|
|
799
919
|
try {
|
|
800
|
-
|
|
920
|
+
let answer = sdpAnswer;
|
|
921
|
+
if (this.midMapping) {
|
|
922
|
+
answer = restoreAnswerMids(
|
|
923
|
+
answer,
|
|
924
|
+
this.midMapping.remoteToLocal
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
yield setRemoteDescription(this.peerConnection, answer);
|
|
801
928
|
console.debug("[GPUMachineClient] Remote description set");
|
|
802
929
|
} catch (error) {
|
|
803
930
|
console.error("[GPUMachineClient] Failed to connect:", error);
|
|
@@ -825,8 +952,10 @@ var GPUMachineClient = class {
|
|
|
825
952
|
this.peerConnection = void 0;
|
|
826
953
|
}
|
|
827
954
|
this.transceiverMap.clear();
|
|
955
|
+
this.midMapping = void 0;
|
|
828
956
|
this.peerConnected = false;
|
|
829
957
|
this.dataChannelOpen = false;
|
|
958
|
+
this.resetConnectionTimings();
|
|
830
959
|
this.setStatus("disconnected");
|
|
831
960
|
console.debug("[GPUMachineClient] Disconnected");
|
|
832
961
|
});
|
|
@@ -851,6 +980,14 @@ var GPUMachineClient = class {
|
|
|
851
980
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
852
981
|
// Messaging
|
|
853
982
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
983
|
+
/**
|
|
984
|
+
* Returns the negotiated SCTP max message size (bytes) if available,
|
|
985
|
+
* otherwise `undefined` so `sendMessage` falls back to its built-in default.
|
|
986
|
+
*/
|
|
987
|
+
get maxMessageBytes() {
|
|
988
|
+
var _a, _b, _c;
|
|
989
|
+
return (_c = (_b = (_a = this.peerConnection) == null ? void 0 : _a.sctp) == null ? void 0 : _b.maxMessageSize) != null ? _c : void 0;
|
|
990
|
+
}
|
|
854
991
|
/**
|
|
855
992
|
* Sends a command to the GPU machine via the data channel.
|
|
856
993
|
* @param command The command to send
|
|
@@ -862,7 +999,13 @@ var GPUMachineClient = class {
|
|
|
862
999
|
throw new Error("[GPUMachineClient] Data channel not available");
|
|
863
1000
|
}
|
|
864
1001
|
try {
|
|
865
|
-
sendMessage(
|
|
1002
|
+
sendMessage(
|
|
1003
|
+
this.dataChannel,
|
|
1004
|
+
command,
|
|
1005
|
+
data,
|
|
1006
|
+
scope,
|
|
1007
|
+
this.maxMessageBytes
|
|
1008
|
+
);
|
|
866
1009
|
} catch (error) {
|
|
867
1010
|
console.warn("[GPUMachineClient] Failed to send message:", error);
|
|
868
1011
|
}
|
|
@@ -990,6 +1133,24 @@ var GPUMachineClient = class {
|
|
|
990
1133
|
getStats() {
|
|
991
1134
|
return this.stats;
|
|
992
1135
|
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Returns the ICE/data-channel durations recorded during the last connect(),
|
|
1138
|
+
* or undefined if no connection has completed yet.
|
|
1139
|
+
*/
|
|
1140
|
+
getConnectionTimings() {
|
|
1141
|
+
if (this.iceNegotiationMs == null || this.dataChannelMs == null) {
|
|
1142
|
+
return void 0;
|
|
1143
|
+
}
|
|
1144
|
+
return {
|
|
1145
|
+
iceNegotiationMs: this.iceNegotiationMs,
|
|
1146
|
+
dataChannelMs: this.dataChannelMs
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
resetConnectionTimings() {
|
|
1150
|
+
this.iceStartTime = void 0;
|
|
1151
|
+
this.iceNegotiationMs = void 0;
|
|
1152
|
+
this.dataChannelMs = void 0;
|
|
1153
|
+
}
|
|
993
1154
|
startStatsPolling() {
|
|
994
1155
|
this.stopStatsPolling();
|
|
995
1156
|
this.statsInterval = setInterval(() => __async(this, null, function* () {
|
|
@@ -1033,6 +1194,9 @@ var GPUMachineClient = class {
|
|
|
1033
1194
|
if (state) {
|
|
1034
1195
|
switch (state) {
|
|
1035
1196
|
case "connected":
|
|
1197
|
+
if (this.iceStartTime != null && this.iceNegotiationMs == null) {
|
|
1198
|
+
this.iceNegotiationMs = performance.now() - this.iceStartTime;
|
|
1199
|
+
}
|
|
1036
1200
|
this.peerConnected = true;
|
|
1037
1201
|
this.checkFullyConnected();
|
|
1038
1202
|
break;
|
|
@@ -1049,13 +1213,19 @@ var GPUMachineClient = class {
|
|
|
1049
1213
|
}
|
|
1050
1214
|
};
|
|
1051
1215
|
this.peerConnection.ontrack = (event) => {
|
|
1052
|
-
var _a;
|
|
1053
|
-
|
|
1054
|
-
const
|
|
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}`;
|
|
1055
1225
|
console.debug(
|
|
1056
|
-
`[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${mid})`
|
|
1226
|
+
`[GPUMachineClient] Track received: "${trackName}" (${event.track.kind}, mid=${event.transceiver.mid})`
|
|
1057
1227
|
);
|
|
1058
|
-
const stream = (
|
|
1228
|
+
const stream = (_b = event.streams[0]) != null ? _b : new MediaStream([event.track]);
|
|
1059
1229
|
this.emit("trackReceived", trackName, event.track, stream);
|
|
1060
1230
|
};
|
|
1061
1231
|
this.peerConnection.onicecandidate = (event) => {
|
|
@@ -1076,6 +1246,9 @@ var GPUMachineClient = class {
|
|
|
1076
1246
|
if (!this.dataChannel) return;
|
|
1077
1247
|
this.dataChannel.onopen = () => {
|
|
1078
1248
|
console.debug("[GPUMachineClient] Data channel open");
|
|
1249
|
+
if (this.iceStartTime != null && this.dataChannelMs == null) {
|
|
1250
|
+
this.dataChannelMs = performance.now() - this.iceStartTime;
|
|
1251
|
+
}
|
|
1079
1252
|
this.dataChannelOpen = true;
|
|
1080
1253
|
this.startPing();
|
|
1081
1254
|
this.checkFullyConnected();
|
|
@@ -1115,13 +1288,13 @@ var GPUMachineClient = class {
|
|
|
1115
1288
|
// src/core/Reactor.ts
|
|
1116
1289
|
import { z as z2 } from "zod";
|
|
1117
1290
|
var LOCAL_COORDINATOR_URL = "http://localhost:8080";
|
|
1118
|
-
var
|
|
1291
|
+
var DEFAULT_BASE_URL = "https://api.reactor.inc";
|
|
1119
1292
|
var TrackConfigSchema = z2.object({
|
|
1120
1293
|
name: z2.string(),
|
|
1121
1294
|
kind: z2.enum(["audio", "video"])
|
|
1122
1295
|
});
|
|
1123
1296
|
var OptionsSchema = z2.object({
|
|
1124
|
-
|
|
1297
|
+
apiUrl: z2.string().default(DEFAULT_BASE_URL),
|
|
1125
1298
|
modelName: z2.string(),
|
|
1126
1299
|
local: z2.boolean().default(false),
|
|
1127
1300
|
/**
|
|
@@ -1146,12 +1319,12 @@ var Reactor = class {
|
|
|
1146
1319
|
// Generic event map
|
|
1147
1320
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
1148
1321
|
const validatedOptions = OptionsSchema.parse(options);
|
|
1149
|
-
this.coordinatorUrl = validatedOptions.
|
|
1322
|
+
this.coordinatorUrl = validatedOptions.apiUrl;
|
|
1150
1323
|
this.model = validatedOptions.modelName;
|
|
1151
1324
|
this.local = validatedOptions.local;
|
|
1152
1325
|
this.receive = validatedOptions.receive;
|
|
1153
1326
|
this.send = validatedOptions.send;
|
|
1154
|
-
if (this.local && options.
|
|
1327
|
+
if (this.local && options.apiUrl === void 0) {
|
|
1155
1328
|
this.coordinatorUrl = LOCAL_COORDINATOR_URL;
|
|
1156
1329
|
}
|
|
1157
1330
|
}
|
|
@@ -1272,14 +1445,14 @@ var Reactor = class {
|
|
|
1272
1445
|
receive: this.receive
|
|
1273
1446
|
});
|
|
1274
1447
|
try {
|
|
1275
|
-
const sdpAnswer = yield this.coordinatorClient.connect(
|
|
1448
|
+
const { sdpAnswer } = yield this.coordinatorClient.connect(
|
|
1276
1449
|
this.sessionId,
|
|
1277
1450
|
sdpOffer,
|
|
1278
1451
|
options == null ? void 0 : options.maxAttempts
|
|
1279
1452
|
);
|
|
1280
1453
|
yield this.machineClient.connect(sdpAnswer);
|
|
1281
|
-
this.setStatus("ready");
|
|
1282
1454
|
} catch (error) {
|
|
1455
|
+
if (isAbortError(error)) return;
|
|
1283
1456
|
let recoverable = false;
|
|
1284
1457
|
if (error instanceof ConflictError) {
|
|
1285
1458
|
recoverable = true;
|
|
@@ -1289,7 +1462,7 @@ var Reactor = class {
|
|
|
1289
1462
|
this.createError(
|
|
1290
1463
|
"RECONNECTION_FAILED",
|
|
1291
1464
|
`Failed to reconnect: ${error}`,
|
|
1292
|
-
"
|
|
1465
|
+
"api",
|
|
1293
1466
|
true
|
|
1294
1467
|
);
|
|
1295
1468
|
}
|
|
@@ -1312,6 +1485,7 @@ var Reactor = class {
|
|
|
1312
1485
|
throw new Error("Already connected or connecting");
|
|
1313
1486
|
}
|
|
1314
1487
|
this.setStatus("connecting");
|
|
1488
|
+
this.connectStartTime = performance.now();
|
|
1315
1489
|
try {
|
|
1316
1490
|
console.debug(
|
|
1317
1491
|
"[Reactor] Connecting to coordinator with authenticated URL"
|
|
@@ -1329,20 +1503,33 @@ var Reactor = class {
|
|
|
1329
1503
|
send: this.send,
|
|
1330
1504
|
receive: this.receive
|
|
1331
1505
|
});
|
|
1506
|
+
const tSession = performance.now();
|
|
1332
1507
|
const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
|
|
1508
|
+
const sessionCreationMs = performance.now() - tSession;
|
|
1333
1509
|
this.setSessionId(sessionId);
|
|
1334
|
-
const
|
|
1510
|
+
const tSdp = performance.now();
|
|
1511
|
+
const { sdpAnswer, sdpPollingAttempts } = yield this.coordinatorClient.connect(
|
|
1335
1512
|
sessionId,
|
|
1336
1513
|
void 0,
|
|
1337
1514
|
options == null ? void 0 : options.maxAttempts
|
|
1338
1515
|
);
|
|
1516
|
+
const sdpPollingMs = performance.now() - tSdp;
|
|
1517
|
+
this.connectionTimings = {
|
|
1518
|
+
sessionCreationMs,
|
|
1519
|
+
sdpPollingMs,
|
|
1520
|
+
sdpPollingAttempts,
|
|
1521
|
+
iceNegotiationMs: 0,
|
|
1522
|
+
dataChannelMs: 0,
|
|
1523
|
+
totalMs: 0
|
|
1524
|
+
};
|
|
1339
1525
|
yield this.machineClient.connect(sdpAnswer);
|
|
1340
1526
|
} catch (error) {
|
|
1527
|
+
if (isAbortError(error)) return;
|
|
1341
1528
|
console.error("[Reactor] Connection failed:", error);
|
|
1342
1529
|
this.createError(
|
|
1343
1530
|
"CONNECTION_FAILED",
|
|
1344
1531
|
`Connection failed: ${error}`,
|
|
1345
|
-
"
|
|
1532
|
+
"api",
|
|
1346
1533
|
true
|
|
1347
1534
|
);
|
|
1348
1535
|
try {
|
|
@@ -1359,19 +1546,28 @@ var Reactor = class {
|
|
|
1359
1546
|
}
|
|
1360
1547
|
/**
|
|
1361
1548
|
* Sets up event handlers for the machine client.
|
|
1549
|
+
*
|
|
1550
|
+
* Each handler captures the client reference at registration time and
|
|
1551
|
+
* ignores events if this.machineClient has since changed (e.g. after
|
|
1552
|
+
* disconnect + reconnect), preventing stale WebRTC teardown events from
|
|
1553
|
+
* interfering with a new connection.
|
|
1362
1554
|
*/
|
|
1363
1555
|
setupMachineClientHandlers() {
|
|
1364
1556
|
if (!this.machineClient) return;
|
|
1365
|
-
this.machineClient
|
|
1557
|
+
const client = this.machineClient;
|
|
1558
|
+
client.on("message", (message, scope) => {
|
|
1559
|
+
if (this.machineClient !== client) return;
|
|
1366
1560
|
if (scope === "application") {
|
|
1367
1561
|
this.emit("message", message);
|
|
1368
1562
|
} else if (scope === "runtime") {
|
|
1369
1563
|
this.emit("runtimeMessage", message);
|
|
1370
1564
|
}
|
|
1371
1565
|
});
|
|
1372
|
-
|
|
1566
|
+
client.on("statusChanged", (status) => {
|
|
1567
|
+
if (this.machineClient !== client) return;
|
|
1373
1568
|
switch (status) {
|
|
1374
1569
|
case "connected":
|
|
1570
|
+
this.finalizeConnectionTimings(client);
|
|
1375
1571
|
this.setStatus("ready");
|
|
1376
1572
|
break;
|
|
1377
1573
|
case "disconnected":
|
|
@@ -1388,14 +1584,18 @@ var Reactor = class {
|
|
|
1388
1584
|
break;
|
|
1389
1585
|
}
|
|
1390
1586
|
});
|
|
1391
|
-
|
|
1587
|
+
client.on(
|
|
1392
1588
|
"trackReceived",
|
|
1393
1589
|
(name, track, stream) => {
|
|
1590
|
+
if (this.machineClient !== client) return;
|
|
1394
1591
|
this.emit("trackReceived", name, track, stream);
|
|
1395
1592
|
}
|
|
1396
1593
|
);
|
|
1397
|
-
|
|
1398
|
-
this.
|
|
1594
|
+
client.on("statsUpdate", (stats) => {
|
|
1595
|
+
if (this.machineClient !== client) return;
|
|
1596
|
+
this.emit("statsUpdate", __spreadProps(__spreadValues({}, stats), {
|
|
1597
|
+
connectionTimings: this.connectionTimings
|
|
1598
|
+
}));
|
|
1399
1599
|
});
|
|
1400
1600
|
}
|
|
1401
1601
|
/**
|
|
@@ -1404,10 +1604,12 @@ var Reactor = class {
|
|
|
1404
1604
|
*/
|
|
1405
1605
|
disconnect(recoverable = false) {
|
|
1406
1606
|
return __async(this, null, function* () {
|
|
1607
|
+
var _a;
|
|
1407
1608
|
if (this.status === "disconnected" && !this.sessionId) {
|
|
1408
1609
|
console.warn("[Reactor] Already disconnected");
|
|
1409
1610
|
return;
|
|
1410
1611
|
}
|
|
1612
|
+
(_a = this.coordinatorClient) == null ? void 0 : _a.abort();
|
|
1411
1613
|
if (this.coordinatorClient && !recoverable) {
|
|
1412
1614
|
try {
|
|
1413
1615
|
yield this.coordinatorClient.terminateSession();
|
|
@@ -1427,6 +1629,7 @@ var Reactor = class {
|
|
|
1427
1629
|
}
|
|
1428
1630
|
}
|
|
1429
1631
|
this.setStatus("disconnected");
|
|
1632
|
+
this.resetConnectionTimings();
|
|
1430
1633
|
if (!recoverable) {
|
|
1431
1634
|
this.setSessionExpiration(void 0);
|
|
1432
1635
|
this.setSessionId(void 0);
|
|
@@ -1489,7 +1692,23 @@ var Reactor = class {
|
|
|
1489
1692
|
}
|
|
1490
1693
|
getStats() {
|
|
1491
1694
|
var _a;
|
|
1492
|
-
|
|
1695
|
+
const stats = (_a = this.machineClient) == null ? void 0 : _a.getStats();
|
|
1696
|
+
if (!stats) return void 0;
|
|
1697
|
+
return __spreadProps(__spreadValues({}, stats), { connectionTimings: this.connectionTimings });
|
|
1698
|
+
}
|
|
1699
|
+
resetConnectionTimings() {
|
|
1700
|
+
this.connectStartTime = void 0;
|
|
1701
|
+
this.connectionTimings = void 0;
|
|
1702
|
+
}
|
|
1703
|
+
finalizeConnectionTimings(client) {
|
|
1704
|
+
var _a, _b;
|
|
1705
|
+
if (!this.connectionTimings || this.connectStartTime == null) return;
|
|
1706
|
+
const webrtcTimings = client.getConnectionTimings();
|
|
1707
|
+
this.connectionTimings.iceNegotiationMs = (_a = webrtcTimings == null ? void 0 : webrtcTimings.iceNegotiationMs) != null ? _a : 0;
|
|
1708
|
+
this.connectionTimings.dataChannelMs = (_b = webrtcTimings == null ? void 0 : webrtcTimings.dataChannelMs) != null ? _b : 0;
|
|
1709
|
+
this.connectionTimings.totalMs = performance.now() - this.connectStartTime;
|
|
1710
|
+
this.connectStartTime = void 0;
|
|
1711
|
+
console.debug("[Reactor] Connection timings:", this.connectionTimings);
|
|
1493
1712
|
}
|
|
1494
1713
|
/**
|
|
1495
1714
|
* Create and store an error
|
|
@@ -1529,7 +1748,7 @@ var initReactorStore = (props) => {
|
|
|
1529
1748
|
};
|
|
1530
1749
|
var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
1531
1750
|
console.debug("[ReactorStore] Creating store", {
|
|
1532
|
-
|
|
1751
|
+
apiUrl: initProps.apiUrl,
|
|
1533
1752
|
jwtToken: initProps.jwtToken,
|
|
1534
1753
|
initialState: publicState
|
|
1535
1754
|
});
|
|
@@ -1697,7 +1916,7 @@ function ReactorProvider(_a) {
|
|
|
1697
1916
|
console.debug("[ReactorProvider] Reactor store created successfully");
|
|
1698
1917
|
}
|
|
1699
1918
|
const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
|
|
1700
|
-
const {
|
|
1919
|
+
const { apiUrl, modelName, local, receive, send } = props;
|
|
1701
1920
|
const maxAttempts = pollingOptions.maxAttempts;
|
|
1702
1921
|
useEffect(() => {
|
|
1703
1922
|
const handleBeforeUnload = () => {
|
|
@@ -1750,7 +1969,7 @@ function ReactorProvider(_a) {
|
|
|
1750
1969
|
console.debug("[ReactorProvider] Updating reactor store");
|
|
1751
1970
|
storeRef.current = createReactorStore(
|
|
1752
1971
|
initReactorStore({
|
|
1753
|
-
|
|
1972
|
+
apiUrl,
|
|
1754
1973
|
modelName,
|
|
1755
1974
|
local,
|
|
1756
1975
|
receive,
|
|
@@ -1782,7 +2001,7 @@ function ReactorProvider(_a) {
|
|
|
1782
2001
|
});
|
|
1783
2002
|
};
|
|
1784
2003
|
}, [
|
|
1785
|
-
|
|
2004
|
+
apiUrl,
|
|
1786
2005
|
modelName,
|
|
1787
2006
|
autoConnect,
|
|
1788
2007
|
local,
|
|
@@ -2611,12 +2830,12 @@ function WebcamStream({
|
|
|
2611
2830
|
}
|
|
2612
2831
|
|
|
2613
2832
|
// src/utils/tokens.ts
|
|
2614
|
-
function
|
|
2615
|
-
return __async(this, arguments, function* (apiKey,
|
|
2833
|
+
function fetchInsecureToken(_0) {
|
|
2834
|
+
return __async(this, arguments, function* (apiKey, apiUrl = DEFAULT_BASE_URL) {
|
|
2616
2835
|
console.warn(
|
|
2617
|
-
"[Reactor] \u26A0\uFE0F SECURITY WARNING:
|
|
2836
|
+
"[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."
|
|
2618
2837
|
);
|
|
2619
|
-
const response = yield fetch(`${
|
|
2838
|
+
const response = yield fetch(`${apiUrl}/tokens`, {
|
|
2620
2839
|
method: "GET",
|
|
2621
2840
|
headers: {
|
|
2622
2841
|
"X-API-Key": apiKey
|
|
@@ -2631,15 +2850,17 @@ function fetchInsecureJwtToken(_0) {
|
|
|
2631
2850
|
});
|
|
2632
2851
|
}
|
|
2633
2852
|
export {
|
|
2853
|
+
AbortError,
|
|
2634
2854
|
ConflictError,
|
|
2635
|
-
|
|
2855
|
+
DEFAULT_BASE_URL,
|
|
2636
2856
|
Reactor,
|
|
2637
2857
|
ReactorController,
|
|
2638
2858
|
ReactorProvider,
|
|
2639
2859
|
ReactorView,
|
|
2640
2860
|
WebcamStream,
|
|
2641
2861
|
audio,
|
|
2642
|
-
|
|
2862
|
+
fetchInsecureToken,
|
|
2863
|
+
isAbortError,
|
|
2643
2864
|
useReactor,
|
|
2644
2865
|
useReactorInternalMessage,
|
|
2645
2866
|
useReactorMessage,
|