@reactor-team/js-sdk 2.0.1 → 2.1.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 CHANGED
@@ -11,6 +11,9 @@ interface ReactorError {
11
11
  component: "coordinator" | "gpu" | "livekit";
12
12
  retryAfter?: number;
13
13
  }
14
+ declare class ConflictError extends Error {
15
+ constructor(message: string);
16
+ }
14
17
  interface ReactorState$1 {
15
18
  status: ReactorStatus;
16
19
  lastError?: ReactorError;
@@ -184,4 +187,4 @@ declare function useReactorMessage(handler: (message: any) => void): void;
184
187
  */
185
188
  declare function fetchInsecureJwtToken(apiKey: string, coordinatorUrl?: string): Promise<string>;
186
189
 
187
- export { type Options, PROD_COORDINATOR_URL, Reactor, ReactorController, type ReactorControllerProps, type ReactorError, type ReactorEvent, ReactorProvider, type ReactorState$1 as ReactorState, type ReactorStatus, ReactorView, type ReactorViewProps, WebcamStream, fetchInsecureJwtToken, useReactor, useReactorMessage, useReactorStore };
190
+ export { ConflictError, type Options, PROD_COORDINATOR_URL, Reactor, ReactorController, type ReactorControllerProps, type ReactorError, type ReactorEvent, ReactorProvider, type ReactorState$1 as ReactorState, type ReactorStatus, ReactorView, type ReactorViewProps, WebcamStream, fetchInsecureJwtToken, useReactor, useReactorMessage, useReactorStore };
package/dist/index.d.ts CHANGED
@@ -11,6 +11,9 @@ interface ReactorError {
11
11
  component: "coordinator" | "gpu" | "livekit";
12
12
  retryAfter?: number;
13
13
  }
14
+ declare class ConflictError extends Error {
15
+ constructor(message: string);
16
+ }
14
17
  interface ReactorState$1 {
15
18
  status: ReactorStatus;
16
19
  lastError?: ReactorError;
@@ -184,4 +187,4 @@ declare function useReactorMessage(handler: (message: any) => void): void;
184
187
  */
185
188
  declare function fetchInsecureJwtToken(apiKey: string, coordinatorUrl?: string): Promise<string>;
186
189
 
187
- export { type Options, PROD_COORDINATOR_URL, Reactor, ReactorController, type ReactorControllerProps, type ReactorError, type ReactorEvent, ReactorProvider, type ReactorState$1 as ReactorState, type ReactorStatus, ReactorView, type ReactorViewProps, WebcamStream, fetchInsecureJwtToken, useReactor, useReactorMessage, useReactorStore };
190
+ export { ConflictError, type Options, PROD_COORDINATOR_URL, Reactor, ReactorController, type ReactorControllerProps, type ReactorError, type ReactorEvent, ReactorProvider, type ReactorState$1 as ReactorState, type ReactorStatus, ReactorView, type ReactorViewProps, WebcamStream, fetchInsecureJwtToken, useReactor, useReactorMessage, useReactorStore };
package/dist/index.js CHANGED
@@ -79,6 +79,7 @@ var __async = (__this, __arguments, generator) => {
79
79
  // src/index.ts
80
80
  var index_exports = {};
81
81
  __export(index_exports, {
82
+ ConflictError: () => ConflictError,
82
83
  PROD_COORDINATOR_URL: () => PROD_COORDINATOR_URL,
83
84
  Reactor: () => Reactor,
84
85
  ReactorController: () => ReactorController,
@@ -92,6 +93,158 @@ __export(index_exports, {
92
93
  });
93
94
  module.exports = __toCommonJS(index_exports);
94
95
 
96
+ // src/types.ts
97
+ var ConflictError = class extends Error {
98
+ constructor(message) {
99
+ super(message);
100
+ }
101
+ };
102
+
103
+ // src/core/types.ts
104
+ var import_zod = require("zod");
105
+ var SessionState = /* @__PURE__ */ ((SessionState2) => {
106
+ SessionState2[SessionState2["SESSION_STATE_UNKNOWN"] = 0] = "SESSION_STATE_UNKNOWN";
107
+ SessionState2[SessionState2["SESSION_STATE_WAITING"] = 1] = "SESSION_STATE_WAITING";
108
+ SessionState2[SessionState2["SESSION_STATE_ACTIVE"] = 2] = "SESSION_STATE_ACTIVE";
109
+ SessionState2[SessionState2["SESSION_STATE_DISCONNECTED"] = 3] = "SESSION_STATE_DISCONNECTED";
110
+ SessionState2[SessionState2["SESSION_STATE_CLOSED"] = 4] = "SESSION_STATE_CLOSED";
111
+ SessionState2[SessionState2["UNRECOGNIZED"] = -1] = "UNRECOGNIZED";
112
+ return SessionState2;
113
+ })(SessionState || {});
114
+ var CreateSessionRequestSchema = import_zod.z.object({
115
+ model: import_zod.z.string(),
116
+ sdp_offer: import_zod.z.string(),
117
+ extra_args: import_zod.z.record(import_zod.z.string(), import_zod.z.any())
118
+ // Dictionary
119
+ });
120
+ var CreateSessionResponseSchema = import_zod.z.object({
121
+ session_id: import_zod.z.uuidv4()
122
+ });
123
+ var SessionStatusResponseSchema = import_zod.z.object({
124
+ session_id: import_zod.z.uuidv4(),
125
+ state: SessionState
126
+ });
127
+ var SessionInfoResponseSchema = SessionStatusResponseSchema.extend({
128
+ session_info: CreateSessionRequestSchema.extend({
129
+ session_id: import_zod.z.uuidv4()
130
+ })
131
+ });
132
+ var SDPParamsRequestSchema = import_zod.z.object({
133
+ sdp_offer: import_zod.z.string(),
134
+ extra_args: import_zod.z.record(import_zod.z.string(), import_zod.z.any())
135
+ // Dictionary
136
+ });
137
+ var SDPParamsResponseSchema = import_zod.z.object({
138
+ sdp_answer: import_zod.z.string(),
139
+ extra_args: import_zod.z.record(import_zod.z.string(), import_zod.z.any())
140
+ // Dictionary
141
+ });
142
+ var IceServersResponseSchema = import_zod.z.object({
143
+ ice_servers: import_zod.z.array(
144
+ import_zod.z.object({
145
+ uris: import_zod.z.array(import_zod.z.string()),
146
+ credentials: import_zod.z.object({
147
+ username: import_zod.z.string(),
148
+ password: import_zod.z.string()
149
+ }).optional()
150
+ })
151
+ )
152
+ });
153
+
154
+ // src/utils/webrtc.ts
155
+ var DEFAULT_DATA_CHANNEL_LABEL = "data";
156
+ var FORCE_RELAY_MODE = false;
157
+ function createPeerConnection(config) {
158
+ return new RTCPeerConnection({
159
+ iceServers: config.iceServers,
160
+ iceTransportPolicy: FORCE_RELAY_MODE ? "relay" : "all"
161
+ });
162
+ }
163
+ function createDataChannel(pc, label) {
164
+ return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
165
+ }
166
+ function createOffer(pc) {
167
+ return __async(this, null, function* () {
168
+ const offer = yield pc.createOffer();
169
+ yield pc.setLocalDescription(offer);
170
+ yield waitForIceGathering(pc);
171
+ const localDescription = pc.localDescription;
172
+ if (!localDescription) {
173
+ throw new Error("Failed to create local description");
174
+ }
175
+ return localDescription.sdp;
176
+ });
177
+ }
178
+ function setRemoteDescription(pc, sdp) {
179
+ return __async(this, null, function* () {
180
+ const sessionDescription = new RTCSessionDescription({
181
+ sdp,
182
+ type: "answer"
183
+ });
184
+ yield pc.setRemoteDescription(sessionDescription);
185
+ });
186
+ }
187
+ function getLocalDescription(pc) {
188
+ const desc = pc.localDescription;
189
+ if (!desc) return void 0;
190
+ return desc.sdp;
191
+ }
192
+ function transformIceServers(response) {
193
+ return response.ice_servers.map((server) => {
194
+ const rtcServer = {
195
+ urls: server.uris
196
+ };
197
+ if (server.credentials) {
198
+ rtcServer.username = server.credentials.username;
199
+ rtcServer.credential = server.credentials.password;
200
+ }
201
+ return rtcServer;
202
+ });
203
+ }
204
+ function waitForIceGathering(pc, timeoutMs = 5e3) {
205
+ return new Promise((resolve) => {
206
+ if (pc.iceGatheringState === "complete") {
207
+ resolve();
208
+ return;
209
+ }
210
+ const onGatheringStateChange = () => {
211
+ if (pc.iceGatheringState === "complete") {
212
+ pc.removeEventListener(
213
+ "icegatheringstatechange",
214
+ onGatheringStateChange
215
+ );
216
+ resolve();
217
+ }
218
+ };
219
+ pc.addEventListener("icegatheringstatechange", onGatheringStateChange);
220
+ setTimeout(() => {
221
+ pc.removeEventListener("icegatheringstatechange", onGatheringStateChange);
222
+ resolve();
223
+ }, timeoutMs);
224
+ });
225
+ }
226
+ function sendMessage(channel, command, data) {
227
+ if (channel.readyState !== "open") {
228
+ throw new Error(`Data channel not open: ${channel.readyState}`);
229
+ }
230
+ const jsonData = typeof data === "string" ? JSON.parse(data) : data;
231
+ const payload = { type: command, data: jsonData };
232
+ channel.send(JSON.stringify(payload));
233
+ }
234
+ function parseMessage(data) {
235
+ if (typeof data === "string") {
236
+ try {
237
+ return JSON.parse(data);
238
+ } catch (e) {
239
+ return data;
240
+ }
241
+ }
242
+ return data;
243
+ }
244
+ function closePeerConnection(pc) {
245
+ pc.close();
246
+ }
247
+
95
248
  // src/core/CoordinatorClient.ts
96
249
  var INITIAL_BACKOFF_MS = 500;
97
250
  var MAX_BACKOFF_MS = 3e4;
@@ -110,6 +263,33 @@ var CoordinatorClient = class {
110
263
  Authorization: `Bearer ${this.jwtToken}`
111
264
  };
112
265
  }
266
+ /**
267
+ * Fetches ICE servers from the coordinator.
268
+ * @returns Array of RTCIceServer objects for WebRTC peer connection configuration
269
+ */
270
+ getIceServers() {
271
+ return __async(this, null, function* () {
272
+ console.debug("[CoordinatorClient] Fetching ICE servers...");
273
+ const response = yield fetch(
274
+ `${this.baseUrl}/ice_servers?model=${this.model}`,
275
+ {
276
+ method: "GET",
277
+ headers: this.getAuthHeaders()
278
+ }
279
+ );
280
+ if (!response.ok) {
281
+ throw new Error(`Failed to fetch ICE servers: ${response.status}`);
282
+ }
283
+ const data = yield response.json();
284
+ const parsed = IceServersResponseSchema.parse(data);
285
+ const iceServers = transformIceServers(parsed);
286
+ console.debug(
287
+ "[CoordinatorClient] Received ICE servers:",
288
+ iceServers.length
289
+ );
290
+ return iceServers;
291
+ });
292
+ }
113
293
  /**
114
294
  * Creates a new session with the coordinator.
115
295
  * Expects a 200 response and stores the session ID.
@@ -346,6 +526,29 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
346
526
  });
347
527
  this.localBaseUrl = baseUrl;
348
528
  }
529
+ /**
530
+ * Gets ICE servers from the local HTTP runtime.
531
+ * @returns The ICE server configuration
532
+ */
533
+ getIceServers() {
534
+ return __async(this, null, function* () {
535
+ console.debug("[LocalCoordinatorClient] Fetching ICE servers...");
536
+ const response = yield fetch(`${this.localBaseUrl}/ice_servers`, {
537
+ method: "GET"
538
+ });
539
+ if (!response.ok) {
540
+ throw new Error("Failed to get ICE servers from local coordinator.");
541
+ }
542
+ const data = yield response.json();
543
+ const parsed = IceServersResponseSchema.parse(data);
544
+ const iceServers = transformIceServers(parsed);
545
+ console.debug(
546
+ "[LocalCoordinatorClient] Received ICE servers:",
547
+ iceServers.length
548
+ );
549
+ return iceServers;
550
+ });
551
+ }
349
552
  /**
350
553
  * Creates a local session by posting to /start_session.
351
554
  * @returns always "local"
@@ -386,6 +589,9 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
386
589
  body: JSON.stringify(sdpBody)
387
590
  });
388
591
  if (!response.ok) {
592
+ if (response.status === 409) {
593
+ throw new ConflictError("Connection superseded by newer request");
594
+ }
389
595
  throw new Error("Failed to get SDP answer from local coordinator.");
390
596
  }
391
597
  const sdpAnswer = yield response.json();
@@ -403,97 +609,12 @@ var LocalCoordinatorClient = class extends CoordinatorClient {
403
609
  }
404
610
  };
405
611
 
406
- // src/utils/webrtc.ts
407
- var DEFAULT_ICE_SERVERS = [
408
- { urls: "stun:stun.l.google.com:19302" },
409
- { urls: "stun:stun1.l.google.com:19302" }
410
- ];
411
- var DEFAULT_DATA_CHANNEL_LABEL = "data";
412
- function createPeerConnection(config) {
413
- var _a;
414
- return new RTCPeerConnection({
415
- iceServers: (_a = config == null ? void 0 : config.iceServers) != null ? _a : DEFAULT_ICE_SERVERS
416
- });
417
- }
418
- function createDataChannel(pc, label) {
419
- return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
420
- }
421
- function createOffer(pc) {
422
- return __async(this, null, function* () {
423
- const offer = yield pc.createOffer();
424
- yield pc.setLocalDescription(offer);
425
- yield waitForIceGathering(pc);
426
- const localDescription = pc.localDescription;
427
- if (!localDescription) {
428
- throw new Error("Failed to create local description");
429
- }
430
- return localDescription.sdp;
431
- });
432
- }
433
- function setRemoteDescription(pc, sdp) {
434
- return __async(this, null, function* () {
435
- const sessionDescription = new RTCSessionDescription({
436
- sdp,
437
- type: "answer"
438
- });
439
- yield pc.setRemoteDescription(sessionDescription);
440
- });
441
- }
442
- function getLocalDescription(pc) {
443
- const desc = pc.localDescription;
444
- if (!desc) return void 0;
445
- return desc.sdp;
446
- }
447
- function waitForIceGathering(pc, timeoutMs = 5e3) {
448
- return new Promise((resolve) => {
449
- if (pc.iceGatheringState === "complete") {
450
- resolve();
451
- return;
452
- }
453
- const onGatheringStateChange = () => {
454
- if (pc.iceGatheringState === "complete") {
455
- pc.removeEventListener(
456
- "icegatheringstatechange",
457
- onGatheringStateChange
458
- );
459
- resolve();
460
- }
461
- };
462
- pc.addEventListener("icegatheringstatechange", onGatheringStateChange);
463
- setTimeout(() => {
464
- pc.removeEventListener("icegatheringstatechange", onGatheringStateChange);
465
- resolve();
466
- }, timeoutMs);
467
- });
468
- }
469
- function sendMessage(channel, command, data) {
470
- if (channel.readyState !== "open") {
471
- throw new Error(`Data channel not open: ${channel.readyState}`);
472
- }
473
- const jsonData = typeof data === "string" ? JSON.parse(data) : data;
474
- const payload = { type: command, data: jsonData };
475
- channel.send(JSON.stringify(payload));
476
- }
477
- function parseMessage(data) {
478
- if (typeof data === "string") {
479
- try {
480
- return JSON.parse(data);
481
- } catch (e) {
482
- return data;
483
- }
484
- }
485
- return data;
486
- }
487
- function closePeerConnection(pc) {
488
- pc.close();
489
- }
490
-
491
612
  // src/core/GPUMachineClient.ts
492
613
  var GPUMachineClient = class {
493
614
  constructor(config) {
494
615
  this.eventListeners = /* @__PURE__ */ new Map();
495
616
  this.status = "disconnected";
496
- this.config = config != null ? config : {};
617
+ this.config = config;
497
618
  }
498
619
  // ─────────────────────────────────────────────────────────────────────────────
499
620
  // Event Emitter API
@@ -774,13 +895,13 @@ var GPUMachineClient = class {
774
895
  };
775
896
 
776
897
  // src/core/Reactor.ts
777
- var import_zod = require("zod");
898
+ var import_zod2 = require("zod");
778
899
  var LOCAL_COORDINATOR_URL = "http://localhost:8080";
779
900
  var PROD_COORDINATOR_URL = "https://api.reactor.inc";
780
- var OptionsSchema = import_zod.z.object({
781
- coordinatorUrl: import_zod.z.string().default(PROD_COORDINATOR_URL),
782
- modelName: import_zod.z.string(),
783
- local: import_zod.z.boolean().default(false)
901
+ var OptionsSchema = import_zod2.z.object({
902
+ coordinatorUrl: import_zod2.z.string().default(PROD_COORDINATOR_URL),
903
+ modelName: import_zod2.z.string(),
904
+ local: import_zod2.z.boolean().default(false)
784
905
  });
785
906
  var Reactor = class {
786
907
  constructor(options) {
@@ -890,9 +1011,14 @@ var Reactor = class {
890
1011
  console.warn("[Reactor] No active session to reconnect to.");
891
1012
  return;
892
1013
  }
1014
+ if (this.status === "ready") {
1015
+ console.warn("[Reactor] Already connected, no need to reconnect.");
1016
+ return;
1017
+ }
893
1018
  this.setStatus("connecting");
894
1019
  if (!this.machineClient) {
895
- this.machineClient = new GPUMachineClient();
1020
+ const iceServers = yield this.coordinatorClient.getIceServers();
1021
+ this.machineClient = new GPUMachineClient({ iceServers });
896
1022
  this.setupMachineClientHandlers();
897
1023
  }
898
1024
  const sdpOffer = yield this.machineClient.createOffer();
@@ -904,8 +1030,12 @@ var Reactor = class {
904
1030
  yield this.machineClient.connect(sdpAnswer);
905
1031
  this.setStatus("ready");
906
1032
  } catch (error) {
1033
+ let recoverable = false;
1034
+ if (error instanceof ConflictError) {
1035
+ recoverable = true;
1036
+ }
907
1037
  console.error("[Reactor] Failed to reconnect:", error);
908
- this.disconnect(false);
1038
+ this.disconnect(recoverable);
909
1039
  this.createError(
910
1040
  "RECONNECTION_FAILED",
911
1041
  `Failed to reconnect: ${error}`,
@@ -940,7 +1070,8 @@ var Reactor = class {
940
1070
  // Safe: validated on line 186-188
941
1071
  model: this.model
942
1072
  });
943
- this.machineClient = new GPUMachineClient();
1073
+ const iceServers = yield this.coordinatorClient.getIceServers();
1074
+ this.machineClient = new GPUMachineClient({ iceServers });
944
1075
  this.setupMachineClientHandlers();
945
1076
  const sdpOffer = yield this.machineClient.createOffer();
946
1077
  const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
@@ -2152,6 +2283,7 @@ function fetchInsecureJwtToken(_0) {
2152
2283
  }
2153
2284
  // Annotate the CommonJS export names for ESM import in node:
2154
2285
  0 && (module.exports = {
2286
+ ConflictError,
2155
2287
  PROD_COORDINATOR_URL,
2156
2288
  Reactor,
2157
2289
  ReactorController,