@reactor-team/js-sdk 2.5.1 → 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 +31 -6
- package/dist/index.d.ts +31 -6
- package/dist/index.js +116 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +114 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -2
package/dist/index.mjs
CHANGED
|
@@ -129,6 +129,7 @@ var IceServersResponseSchema = z.object({
|
|
|
129
129
|
// src/utils/webrtc.ts
|
|
130
130
|
var DEFAULT_DATA_CHANNEL_LABEL = "data";
|
|
131
131
|
var FORCE_RELAY_MODE = false;
|
|
132
|
+
var DEFAULT_MAX_MESSAGE_BYTES = 256 * 1024;
|
|
132
133
|
function createPeerConnection(config) {
|
|
133
134
|
return new RTCPeerConnection({
|
|
134
135
|
iceServers: config.iceServers,
|
|
@@ -285,14 +286,21 @@ function waitForIceGathering(pc, timeoutMs = 5e3) {
|
|
|
285
286
|
}, timeoutMs);
|
|
286
287
|
});
|
|
287
288
|
}
|
|
288
|
-
function sendMessage(channel, command, data, scope = "application") {
|
|
289
|
+
function sendMessage(channel, command, data, scope = "application", maxBytes = DEFAULT_MAX_MESSAGE_BYTES) {
|
|
289
290
|
if (channel.readyState !== "open") {
|
|
290
291
|
throw new Error(`Data channel not open: ${channel.readyState}`);
|
|
291
292
|
}
|
|
292
293
|
const jsonData = typeof data === "string" ? JSON.parse(data) : data;
|
|
293
294
|
const inner = { type: command, data: jsonData };
|
|
294
295
|
const payload = { scope, data: inner };
|
|
295
|
-
|
|
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);
|
|
296
304
|
}
|
|
297
305
|
function parseMessage(data) {
|
|
298
306
|
if (typeof data === "string") {
|
|
@@ -613,7 +621,7 @@ var CoordinatorClient = class {
|
|
|
613
621
|
if (response.status === 200) {
|
|
614
622
|
const answerData = yield response.json();
|
|
615
623
|
console.debug("[CoordinatorClient] Received SDP answer via polling");
|
|
616
|
-
return answerData.sdp_answer;
|
|
624
|
+
return { sdpAnswer: answerData.sdp_answer, attempts: attempt };
|
|
617
625
|
}
|
|
618
626
|
if (response.status === 202) {
|
|
619
627
|
console.warn(
|
|
@@ -637,7 +645,7 @@ var CoordinatorClient = class {
|
|
|
637
645
|
* @param sessionId - The session ID to connect to
|
|
638
646
|
* @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
|
|
639
647
|
* @param maxAttempts - Optional maximum number of polling attempts before giving up
|
|
640
|
-
* @returns The SDP answer
|
|
648
|
+
* @returns The SDP answer and the number of polling attempts made (0 if answered immediately via PUT)
|
|
641
649
|
*/
|
|
642
650
|
connect(sessionId, sdpOffer, maxAttempts) {
|
|
643
651
|
return __async(this, null, function* () {
|
|
@@ -645,10 +653,11 @@ var CoordinatorClient = class {
|
|
|
645
653
|
if (sdpOffer) {
|
|
646
654
|
const answer = yield this.sendSdpOffer(sessionId, sdpOffer);
|
|
647
655
|
if (answer !== null) {
|
|
648
|
-
return answer;
|
|
656
|
+
return { sdpAnswer: answer, sdpPollingAttempts: 0 };
|
|
649
657
|
}
|
|
650
658
|
}
|
|
651
|
-
|
|
659
|
+
const result = yield this.pollSdpAnswer(sessionId, maxAttempts);
|
|
660
|
+
return { sdpAnswer: result.sdpAnswer, sdpPollingAttempts: result.attempts };
|
|
652
661
|
});
|
|
653
662
|
}
|
|
654
663
|
/**
|
|
@@ -730,9 +739,10 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
730
739
|
}
|
|
731
740
|
/**
|
|
732
741
|
* Connects to the local session by posting SDP params to /sdp_params.
|
|
742
|
+
* Local connections are always immediate (no polling).
|
|
733
743
|
* @param sessionId - The session ID (ignored for local)
|
|
734
744
|
* @param sdpMessage - The SDP offer from the local WebRTC peer connection
|
|
735
|
-
* @returns The SDP answer
|
|
745
|
+
* @returns The SDP answer and polling attempts (always 0 for local)
|
|
736
746
|
*/
|
|
737
747
|
connect(sessionId, sdpMessage) {
|
|
738
748
|
return __async(this, null, function* () {
|
|
@@ -758,7 +768,7 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
|
|
|
758
768
|
}
|
|
759
769
|
const sdpAnswer = yield response.json();
|
|
760
770
|
console.debug("[LocalCoordinatorClient] Received SDP answer");
|
|
761
|
-
return sdpAnswer.sdp;
|
|
771
|
+
return { sdpAnswer: sdpAnswer.sdp, sdpPollingAttempts: 0 };
|
|
762
772
|
});
|
|
763
773
|
}
|
|
764
774
|
terminateSession() {
|
|
@@ -903,6 +913,9 @@ var GPUMachineClient = class {
|
|
|
903
913
|
);
|
|
904
914
|
}
|
|
905
915
|
this.setStatus("connecting");
|
|
916
|
+
this.iceStartTime = performance.now();
|
|
917
|
+
this.iceNegotiationMs = void 0;
|
|
918
|
+
this.dataChannelMs = void 0;
|
|
906
919
|
try {
|
|
907
920
|
let answer = sdpAnswer;
|
|
908
921
|
if (this.midMapping) {
|
|
@@ -942,6 +955,7 @@ var GPUMachineClient = class {
|
|
|
942
955
|
this.midMapping = void 0;
|
|
943
956
|
this.peerConnected = false;
|
|
944
957
|
this.dataChannelOpen = false;
|
|
958
|
+
this.resetConnectionTimings();
|
|
945
959
|
this.setStatus("disconnected");
|
|
946
960
|
console.debug("[GPUMachineClient] Disconnected");
|
|
947
961
|
});
|
|
@@ -966,6 +980,14 @@ var GPUMachineClient = class {
|
|
|
966
980
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
967
981
|
// Messaging
|
|
968
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
|
+
}
|
|
969
991
|
/**
|
|
970
992
|
* Sends a command to the GPU machine via the data channel.
|
|
971
993
|
* @param command The command to send
|
|
@@ -977,7 +999,13 @@ var GPUMachineClient = class {
|
|
|
977
999
|
throw new Error("[GPUMachineClient] Data channel not available");
|
|
978
1000
|
}
|
|
979
1001
|
try {
|
|
980
|
-
sendMessage(
|
|
1002
|
+
sendMessage(
|
|
1003
|
+
this.dataChannel,
|
|
1004
|
+
command,
|
|
1005
|
+
data,
|
|
1006
|
+
scope,
|
|
1007
|
+
this.maxMessageBytes
|
|
1008
|
+
);
|
|
981
1009
|
} catch (error) {
|
|
982
1010
|
console.warn("[GPUMachineClient] Failed to send message:", error);
|
|
983
1011
|
}
|
|
@@ -1105,6 +1133,24 @@ var GPUMachineClient = class {
|
|
|
1105
1133
|
getStats() {
|
|
1106
1134
|
return this.stats;
|
|
1107
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
|
+
}
|
|
1108
1154
|
startStatsPolling() {
|
|
1109
1155
|
this.stopStatsPolling();
|
|
1110
1156
|
this.statsInterval = setInterval(() => __async(this, null, function* () {
|
|
@@ -1148,6 +1194,9 @@ var GPUMachineClient = class {
|
|
|
1148
1194
|
if (state) {
|
|
1149
1195
|
switch (state) {
|
|
1150
1196
|
case "connected":
|
|
1197
|
+
if (this.iceStartTime != null && this.iceNegotiationMs == null) {
|
|
1198
|
+
this.iceNegotiationMs = performance.now() - this.iceStartTime;
|
|
1199
|
+
}
|
|
1151
1200
|
this.peerConnected = true;
|
|
1152
1201
|
this.checkFullyConnected();
|
|
1153
1202
|
break;
|
|
@@ -1197,6 +1246,9 @@ var GPUMachineClient = class {
|
|
|
1197
1246
|
if (!this.dataChannel) return;
|
|
1198
1247
|
this.dataChannel.onopen = () => {
|
|
1199
1248
|
console.debug("[GPUMachineClient] Data channel open");
|
|
1249
|
+
if (this.iceStartTime != null && this.dataChannelMs == null) {
|
|
1250
|
+
this.dataChannelMs = performance.now() - this.iceStartTime;
|
|
1251
|
+
}
|
|
1200
1252
|
this.dataChannelOpen = true;
|
|
1201
1253
|
this.startPing();
|
|
1202
1254
|
this.checkFullyConnected();
|
|
@@ -1236,13 +1288,13 @@ var GPUMachineClient = class {
|
|
|
1236
1288
|
// src/core/Reactor.ts
|
|
1237
1289
|
import { z as z2 } from "zod";
|
|
1238
1290
|
var LOCAL_COORDINATOR_URL = "http://localhost:8080";
|
|
1239
|
-
var
|
|
1291
|
+
var DEFAULT_BASE_URL = "https://api.reactor.inc";
|
|
1240
1292
|
var TrackConfigSchema = z2.object({
|
|
1241
1293
|
name: z2.string(),
|
|
1242
1294
|
kind: z2.enum(["audio", "video"])
|
|
1243
1295
|
});
|
|
1244
1296
|
var OptionsSchema = z2.object({
|
|
1245
|
-
|
|
1297
|
+
apiUrl: z2.string().default(DEFAULT_BASE_URL),
|
|
1246
1298
|
modelName: z2.string(),
|
|
1247
1299
|
local: z2.boolean().default(false),
|
|
1248
1300
|
/**
|
|
@@ -1267,12 +1319,12 @@ var Reactor = class {
|
|
|
1267
1319
|
// Generic event map
|
|
1268
1320
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
1269
1321
|
const validatedOptions = OptionsSchema.parse(options);
|
|
1270
|
-
this.coordinatorUrl = validatedOptions.
|
|
1322
|
+
this.coordinatorUrl = validatedOptions.apiUrl;
|
|
1271
1323
|
this.model = validatedOptions.modelName;
|
|
1272
1324
|
this.local = validatedOptions.local;
|
|
1273
1325
|
this.receive = validatedOptions.receive;
|
|
1274
1326
|
this.send = validatedOptions.send;
|
|
1275
|
-
if (this.local && options.
|
|
1327
|
+
if (this.local && options.apiUrl === void 0) {
|
|
1276
1328
|
this.coordinatorUrl = LOCAL_COORDINATOR_URL;
|
|
1277
1329
|
}
|
|
1278
1330
|
}
|
|
@@ -1393,7 +1445,7 @@ var Reactor = class {
|
|
|
1393
1445
|
receive: this.receive
|
|
1394
1446
|
});
|
|
1395
1447
|
try {
|
|
1396
|
-
const sdpAnswer = yield this.coordinatorClient.connect(
|
|
1448
|
+
const { sdpAnswer } = yield this.coordinatorClient.connect(
|
|
1397
1449
|
this.sessionId,
|
|
1398
1450
|
sdpOffer,
|
|
1399
1451
|
options == null ? void 0 : options.maxAttempts
|
|
@@ -1410,7 +1462,7 @@ var Reactor = class {
|
|
|
1410
1462
|
this.createError(
|
|
1411
1463
|
"RECONNECTION_FAILED",
|
|
1412
1464
|
`Failed to reconnect: ${error}`,
|
|
1413
|
-
"
|
|
1465
|
+
"api",
|
|
1414
1466
|
true
|
|
1415
1467
|
);
|
|
1416
1468
|
}
|
|
@@ -1433,6 +1485,7 @@ var Reactor = class {
|
|
|
1433
1485
|
throw new Error("Already connected or connecting");
|
|
1434
1486
|
}
|
|
1435
1487
|
this.setStatus("connecting");
|
|
1488
|
+
this.connectStartTime = performance.now();
|
|
1436
1489
|
try {
|
|
1437
1490
|
console.debug(
|
|
1438
1491
|
"[Reactor] Connecting to coordinator with authenticated URL"
|
|
@@ -1450,13 +1503,25 @@ var Reactor = class {
|
|
|
1450
1503
|
send: this.send,
|
|
1451
1504
|
receive: this.receive
|
|
1452
1505
|
});
|
|
1506
|
+
const tSession = performance.now();
|
|
1453
1507
|
const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
|
|
1508
|
+
const sessionCreationMs = performance.now() - tSession;
|
|
1454
1509
|
this.setSessionId(sessionId);
|
|
1455
|
-
const
|
|
1510
|
+
const tSdp = performance.now();
|
|
1511
|
+
const { sdpAnswer, sdpPollingAttempts } = yield this.coordinatorClient.connect(
|
|
1456
1512
|
sessionId,
|
|
1457
1513
|
void 0,
|
|
1458
1514
|
options == null ? void 0 : options.maxAttempts
|
|
1459
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
|
+
};
|
|
1460
1525
|
yield this.machineClient.connect(sdpAnswer);
|
|
1461
1526
|
} catch (error) {
|
|
1462
1527
|
if (isAbortError(error)) return;
|
|
@@ -1464,7 +1529,7 @@ var Reactor = class {
|
|
|
1464
1529
|
this.createError(
|
|
1465
1530
|
"CONNECTION_FAILED",
|
|
1466
1531
|
`Connection failed: ${error}`,
|
|
1467
|
-
"
|
|
1532
|
+
"api",
|
|
1468
1533
|
true
|
|
1469
1534
|
);
|
|
1470
1535
|
try {
|
|
@@ -1502,6 +1567,7 @@ var Reactor = class {
|
|
|
1502
1567
|
if (this.machineClient !== client) return;
|
|
1503
1568
|
switch (status) {
|
|
1504
1569
|
case "connected":
|
|
1570
|
+
this.finalizeConnectionTimings(client);
|
|
1505
1571
|
this.setStatus("ready");
|
|
1506
1572
|
break;
|
|
1507
1573
|
case "disconnected":
|
|
@@ -1527,7 +1593,9 @@ var Reactor = class {
|
|
|
1527
1593
|
);
|
|
1528
1594
|
client.on("statsUpdate", (stats) => {
|
|
1529
1595
|
if (this.machineClient !== client) return;
|
|
1530
|
-
this.emit("statsUpdate", stats)
|
|
1596
|
+
this.emit("statsUpdate", __spreadProps(__spreadValues({}, stats), {
|
|
1597
|
+
connectionTimings: this.connectionTimings
|
|
1598
|
+
}));
|
|
1531
1599
|
});
|
|
1532
1600
|
}
|
|
1533
1601
|
/**
|
|
@@ -1561,6 +1629,7 @@ var Reactor = class {
|
|
|
1561
1629
|
}
|
|
1562
1630
|
}
|
|
1563
1631
|
this.setStatus("disconnected");
|
|
1632
|
+
this.resetConnectionTimings();
|
|
1564
1633
|
if (!recoverable) {
|
|
1565
1634
|
this.setSessionExpiration(void 0);
|
|
1566
1635
|
this.setSessionId(void 0);
|
|
@@ -1623,7 +1692,23 @@ var Reactor = class {
|
|
|
1623
1692
|
}
|
|
1624
1693
|
getStats() {
|
|
1625
1694
|
var _a;
|
|
1626
|
-
|
|
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);
|
|
1627
1712
|
}
|
|
1628
1713
|
/**
|
|
1629
1714
|
* Create and store an error
|
|
@@ -1663,7 +1748,7 @@ var initReactorStore = (props) => {
|
|
|
1663
1748
|
};
|
|
1664
1749
|
var createReactorStore = (initProps, publicState = defaultInitState) => {
|
|
1665
1750
|
console.debug("[ReactorStore] Creating store", {
|
|
1666
|
-
|
|
1751
|
+
apiUrl: initProps.apiUrl,
|
|
1667
1752
|
jwtToken: initProps.jwtToken,
|
|
1668
1753
|
initialState: publicState
|
|
1669
1754
|
});
|
|
@@ -1831,7 +1916,7 @@ function ReactorProvider(_a) {
|
|
|
1831
1916
|
console.debug("[ReactorProvider] Reactor store created successfully");
|
|
1832
1917
|
}
|
|
1833
1918
|
const _a2 = connectOptions != null ? connectOptions : {}, { autoConnect = false } = _a2, pollingOptions = __objRest(_a2, ["autoConnect"]);
|
|
1834
|
-
const {
|
|
1919
|
+
const { apiUrl, modelName, local, receive, send } = props;
|
|
1835
1920
|
const maxAttempts = pollingOptions.maxAttempts;
|
|
1836
1921
|
useEffect(() => {
|
|
1837
1922
|
const handleBeforeUnload = () => {
|
|
@@ -1884,7 +1969,7 @@ function ReactorProvider(_a) {
|
|
|
1884
1969
|
console.debug("[ReactorProvider] Updating reactor store");
|
|
1885
1970
|
storeRef.current = createReactorStore(
|
|
1886
1971
|
initReactorStore({
|
|
1887
|
-
|
|
1972
|
+
apiUrl,
|
|
1888
1973
|
modelName,
|
|
1889
1974
|
local,
|
|
1890
1975
|
receive,
|
|
@@ -1916,7 +2001,7 @@ function ReactorProvider(_a) {
|
|
|
1916
2001
|
});
|
|
1917
2002
|
};
|
|
1918
2003
|
}, [
|
|
1919
|
-
|
|
2004
|
+
apiUrl,
|
|
1920
2005
|
modelName,
|
|
1921
2006
|
autoConnect,
|
|
1922
2007
|
local,
|
|
@@ -2745,12 +2830,12 @@ function WebcamStream({
|
|
|
2745
2830
|
}
|
|
2746
2831
|
|
|
2747
2832
|
// src/utils/tokens.ts
|
|
2748
|
-
function
|
|
2749
|
-
return __async(this, arguments, function* (apiKey,
|
|
2833
|
+
function fetchInsecureToken(_0) {
|
|
2834
|
+
return __async(this, arguments, function* (apiKey, apiUrl = DEFAULT_BASE_URL) {
|
|
2750
2835
|
console.warn(
|
|
2751
|
-
"[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."
|
|
2752
2837
|
);
|
|
2753
|
-
const response = yield fetch(`${
|
|
2838
|
+
const response = yield fetch(`${apiUrl}/tokens`, {
|
|
2754
2839
|
method: "GET",
|
|
2755
2840
|
headers: {
|
|
2756
2841
|
"X-API-Key": apiKey
|
|
@@ -2767,14 +2852,14 @@ function fetchInsecureJwtToken(_0) {
|
|
|
2767
2852
|
export {
|
|
2768
2853
|
AbortError,
|
|
2769
2854
|
ConflictError,
|
|
2770
|
-
|
|
2855
|
+
DEFAULT_BASE_URL,
|
|
2771
2856
|
Reactor,
|
|
2772
2857
|
ReactorController,
|
|
2773
2858
|
ReactorProvider,
|
|
2774
2859
|
ReactorView,
|
|
2775
2860
|
WebcamStream,
|
|
2776
2861
|
audio,
|
|
2777
|
-
|
|
2862
|
+
fetchInsecureToken,
|
|
2778
2863
|
isAbortError,
|
|
2779
2864
|
useReactor,
|
|
2780
2865
|
useReactorInternalMessage,
|