@reactor-team/js-sdk 1.0.18 → 1.0.22

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.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
+ PROD_COORDINATOR_URL: () => PROD_COORDINATOR_URL,
82
83
  Reactor: () => Reactor,
83
84
  ReactorController: () => ReactorController,
84
85
  ReactorProvider: () => ReactorProvider,
@@ -90,218 +91,603 @@ __export(index_exports, {
90
91
  });
91
92
  module.exports = __toCommonJS(index_exports);
92
93
 
93
- // src/core/types.ts
94
- var import_zod = require("zod");
95
- var ApplicationMessageSchema = import_zod.z.object({
96
- type: import_zod.z.literal("application"),
97
- data: import_zod.z.any()
98
- // Can be any JSON-serializable data
99
- });
100
- var FPSMessageSchema = import_zod.z.object({
101
- type: import_zod.z.literal("fps"),
102
- data: import_zod.z.number()
103
- });
104
- var GPUMachineReceiveMessageSchema = import_zod.z.discriminatedUnion("type", [
105
- ApplicationMessageSchema,
106
- FPSMessageSchema
107
- ]);
108
- var WelcomeMessageSchema = import_zod.z.object({
109
- type: import_zod.z.literal("welcome"),
110
- data: import_zod.z.record(import_zod.z.string(), import_zod.z.any())
111
- });
112
- var GPUMachineAssignmentDataSchema = import_zod.z.object({
113
- livekitWsUrl: import_zod.z.string(),
114
- livekitJwtToken: import_zod.z.string()
115
- });
116
- var GPUMachineAssignmentMessageSchema = import_zod.z.object({
117
- type: import_zod.z.literal("gpu-machine-assigned"),
118
- data: GPUMachineAssignmentDataSchema
119
- });
120
- var EchoMessageSchema = import_zod.z.object({
121
- type: import_zod.z.literal("echo"),
122
- data: import_zod.z.record(import_zod.z.string(), import_zod.z.any())
123
- });
124
- var WaitingInfoDataSchema = import_zod.z.object({
125
- position: import_zod.z.number().optional(),
126
- estimatedWaitTime: import_zod.z.number().optional(),
127
- averageWaitTime: import_zod.z.number().optional()
128
- });
129
- var WaitingInfoMessageSchema = import_zod.z.object({
130
- type: import_zod.z.literal("waiting-info"),
131
- data: WaitingInfoDataSchema
132
- });
133
- var SessionExpirationDataSchema = import_zod.z.object({
134
- expire: import_zod.z.number()
135
- });
136
- var SessionExpirationMessageSchema = import_zod.z.object({
137
- type: import_zod.z.literal("session-expiration"),
138
- data: SessionExpirationDataSchema
139
- });
140
- var CoordinatorMessageSchema = import_zod.z.discriminatedUnion("type", [
141
- WelcomeMessageSchema,
142
- GPUMachineAssignmentMessageSchema,
143
- EchoMessageSchema,
144
- WaitingInfoMessageSchema,
145
- SessionExpirationMessageSchema
146
- ]);
147
- var GPUMachineSendMessageSchema = import_zod.z.discriminatedUnion("type", [
148
- ApplicationMessageSchema
149
- ]);
150
- var SessionSetupMessageSchema = import_zod.z.object({
151
- type: import_zod.z.literal("sessionSetup"),
152
- data: import_zod.z.object({
153
- modelName: import_zod.z.string(),
154
- modelVersion: import_zod.z.string().default("latest")
155
- })
156
- });
94
+ // src/generated/api/types/api_types.ts
95
+ var import_wire2 = require("@bufbuild/protobuf/wire");
96
+
97
+ // src/generated/api/types/base.ts
98
+ var import_wire = require("@bufbuild/protobuf/wire");
99
+
100
+ // src/generated/api/types/api_types.ts
101
+ function createBaseSDPParamsResponse() {
102
+ return { sdpAnswer: "", extraArgs: {} };
103
+ }
104
+ var SDPParamsResponse = {
105
+ encode(message, writer = new import_wire2.BinaryWriter()) {
106
+ if (message.sdpAnswer !== "") {
107
+ writer.uint32(10).string(message.sdpAnswer);
108
+ }
109
+ globalThis.Object.entries(message.extraArgs).forEach(
110
+ ([key, value]) => {
111
+ SDPParamsResponse_ExtraArgsEntry.encode(
112
+ { key, value },
113
+ writer.uint32(18).fork()
114
+ ).join();
115
+ }
116
+ );
117
+ return writer;
118
+ },
119
+ decode(input, length) {
120
+ const reader = input instanceof import_wire2.BinaryReader ? input : new import_wire2.BinaryReader(input);
121
+ const end = length === void 0 ? reader.len : reader.pos + length;
122
+ const message = createBaseSDPParamsResponse();
123
+ while (reader.pos < end) {
124
+ const tag = reader.uint32();
125
+ switch (tag >>> 3) {
126
+ case 1: {
127
+ if (tag !== 10) {
128
+ break;
129
+ }
130
+ message.sdpAnswer = reader.string();
131
+ continue;
132
+ }
133
+ case 2: {
134
+ if (tag !== 18) {
135
+ break;
136
+ }
137
+ const entry2 = SDPParamsResponse_ExtraArgsEntry.decode(
138
+ reader,
139
+ reader.uint32()
140
+ );
141
+ if (entry2.value !== void 0) {
142
+ message.extraArgs[entry2.key] = entry2.value;
143
+ }
144
+ continue;
145
+ }
146
+ }
147
+ if ((tag & 7) === 4 || tag === 0) {
148
+ break;
149
+ }
150
+ reader.skip(tag & 7);
151
+ }
152
+ return message;
153
+ },
154
+ fromJSON(object) {
155
+ return {
156
+ sdpAnswer: isSet(object.sdpAnswer) ? globalThis.String(object.sdpAnswer) : "",
157
+ extraArgs: isObject(object.extraArgs) ? globalThis.Object.entries(object.extraArgs).reduce(
158
+ (acc, [key, value]) => {
159
+ acc[key] = globalThis.String(value);
160
+ return acc;
161
+ },
162
+ {}
163
+ ) : {}
164
+ };
165
+ },
166
+ toJSON(message) {
167
+ const obj = {};
168
+ if (message.sdpAnswer !== "") {
169
+ obj.sdpAnswer = message.sdpAnswer;
170
+ }
171
+ if (message.extraArgs) {
172
+ const entries = globalThis.Object.entries(message.extraArgs);
173
+ if (entries.length > 0) {
174
+ obj.extraArgs = {};
175
+ entries.forEach(([k, v]) => {
176
+ obj.extraArgs[k] = v;
177
+ });
178
+ }
179
+ }
180
+ return obj;
181
+ },
182
+ create(base) {
183
+ return SDPParamsResponse.fromPartial(base != null ? base : {});
184
+ },
185
+ fromPartial(object) {
186
+ var _a, _b;
187
+ const message = createBaseSDPParamsResponse();
188
+ message.sdpAnswer = (_a = object.sdpAnswer) != null ? _a : "";
189
+ message.extraArgs = globalThis.Object.entries((_b = object.extraArgs) != null ? _b : {}).reduce(
190
+ (acc, [key, value]) => {
191
+ if (value !== void 0) {
192
+ acc[key] = globalThis.String(value);
193
+ }
194
+ return acc;
195
+ },
196
+ {}
197
+ );
198
+ return message;
199
+ }
200
+ };
201
+ function createBaseSDPParamsResponse_ExtraArgsEntry() {
202
+ return { key: "", value: "" };
203
+ }
204
+ var SDPParamsResponse_ExtraArgsEntry = {
205
+ encode(message, writer = new import_wire2.BinaryWriter()) {
206
+ if (message.key !== "") {
207
+ writer.uint32(10).string(message.key);
208
+ }
209
+ if (message.value !== "") {
210
+ writer.uint32(18).string(message.value);
211
+ }
212
+ return writer;
213
+ },
214
+ decode(input, length) {
215
+ const reader = input instanceof import_wire2.BinaryReader ? input : new import_wire2.BinaryReader(input);
216
+ const end = length === void 0 ? reader.len : reader.pos + length;
217
+ const message = createBaseSDPParamsResponse_ExtraArgsEntry();
218
+ while (reader.pos < end) {
219
+ const tag = reader.uint32();
220
+ switch (tag >>> 3) {
221
+ case 1: {
222
+ if (tag !== 10) {
223
+ break;
224
+ }
225
+ message.key = reader.string();
226
+ continue;
227
+ }
228
+ case 2: {
229
+ if (tag !== 18) {
230
+ break;
231
+ }
232
+ message.value = reader.string();
233
+ continue;
234
+ }
235
+ }
236
+ if ((tag & 7) === 4 || tag === 0) {
237
+ break;
238
+ }
239
+ reader.skip(tag & 7);
240
+ }
241
+ return message;
242
+ },
243
+ fromJSON(object) {
244
+ return {
245
+ key: isSet(object.key) ? globalThis.String(object.key) : "",
246
+ value: isSet(object.value) ? globalThis.String(object.value) : ""
247
+ };
248
+ },
249
+ toJSON(message) {
250
+ const obj = {};
251
+ if (message.key !== "") {
252
+ obj.key = message.key;
253
+ }
254
+ if (message.value !== "") {
255
+ obj.value = message.value;
256
+ }
257
+ return obj;
258
+ },
259
+ create(base) {
260
+ return SDPParamsResponse_ExtraArgsEntry.fromPartial(base != null ? base : {});
261
+ },
262
+ fromPartial(object) {
263
+ var _a, _b;
264
+ const message = createBaseSDPParamsResponse_ExtraArgsEntry();
265
+ message.key = (_a = object.key) != null ? _a : "";
266
+ message.value = (_b = object.value) != null ? _b : "";
267
+ return message;
268
+ }
269
+ };
270
+ function isObject(value) {
271
+ return typeof value === "object" && value !== null;
272
+ }
273
+ function isSet(value) {
274
+ return value !== null && value !== void 0;
275
+ }
157
276
 
158
277
  // src/core/CoordinatorClient.ts
159
- var import_zod2 = require("zod");
160
- var OptionsSchema = import_zod2.z.object({
161
- wsUrl: import_zod2.z.string().nonempty(),
162
- jwtToken: import_zod2.z.string().optional(),
163
- insecureApiKey: import_zod2.z.string().optional(),
164
- modelName: import_zod2.z.string(),
165
- modelVersion: import_zod2.z.string().default("latest"),
166
- queueing: import_zod2.z.boolean().default(false)
167
- }).refine((data) => data.jwtToken || data.insecureApiKey, {
168
- message: "At least one of jwtToken or insecureApiKey must be provided."
169
- });
278
+ var INITIAL_BACKOFF_MS = 500;
279
+ var MAX_BACKOFF_MS = 3e4;
280
+ var BACKOFF_MULTIPLIER = 2;
170
281
  var CoordinatorClient = class {
171
282
  constructor(options) {
172
- this.eventListeners = /* @__PURE__ */ new Map();
173
- const validatedOptions = OptionsSchema.parse(options);
174
- this.wsUrl = validatedOptions.wsUrl;
175
- this.jwtToken = validatedOptions.jwtToken;
176
- this.insecureApiKey = validatedOptions.insecureApiKey;
177
- this.modelName = validatedOptions.modelName;
178
- this.modelVersion = validatedOptions.modelVersion;
179
- this.queueing = validatedOptions.queueing;
283
+ this.baseUrl = options.baseUrl;
284
+ this.jwtToken = options.jwtToken;
285
+ this.model = options.model;
180
286
  }
181
- // Event Emitter API
182
- on(event, handler) {
183
- if (!this.eventListeners.has(event)) {
184
- this.eventListeners.set(event, /* @__PURE__ */ new Set());
185
- }
186
- this.eventListeners.get(event).add(handler);
187
- }
188
- off(event, handler) {
189
- var _a;
190
- (_a = this.eventListeners.get(event)) == null ? void 0 : _a.delete(handler);
287
+ /**
288
+ * Returns the authorization header with JWT Bearer token
289
+ */
290
+ getAuthHeaders() {
291
+ return {
292
+ Authorization: `Bearer ${this.jwtToken}`
293
+ };
191
294
  }
192
- emit(event, ...args) {
193
- var _a;
194
- (_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
295
+ /**
296
+ * Creates a new session with the coordinator.
297
+ * Expects a 200 response and stores the session ID.
298
+ * @returns The session ID
299
+ */
300
+ createSession(sdp_offer) {
301
+ return __async(this, null, function* () {
302
+ console.debug("[CoordinatorClient] Creating session...");
303
+ const requestBody = {
304
+ model: this.model,
305
+ sdp_offer,
306
+ extra_args: {}
307
+ };
308
+ const response = yield fetch(`${this.baseUrl}/sessions`, {
309
+ method: "POST",
310
+ headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
311
+ "Content-Type": "application/json"
312
+ }),
313
+ body: JSON.stringify(requestBody)
314
+ });
315
+ if (response.status !== 200) {
316
+ const errorText = yield response.text();
317
+ throw new Error(
318
+ `Failed to create session: ${response.status} ${errorText}`
319
+ );
320
+ }
321
+ const data = yield response.json();
322
+ this.currentSessionId = data.session_id;
323
+ console.debug(
324
+ "[CoordinatorClient] Session created with ID:",
325
+ this.currentSessionId
326
+ );
327
+ return data.session_id;
328
+ });
195
329
  }
196
- sendMessage(message) {
197
- var _a;
198
- try {
199
- const messageStr = typeof message === "string" ? message : JSON.stringify(message);
200
- (_a = this.websocket) == null ? void 0 : _a.send(messageStr);
201
- } catch (error) {
202
- console.error("[CoordinatorClient] Failed to send message:", error);
203
- this.emit("statusChanged", "error");
204
- }
330
+ /**
331
+ * Gets the current session information from the coordinator.
332
+ * @returns The session data (untyped for now)
333
+ */
334
+ getSession() {
335
+ return __async(this, null, function* () {
336
+ if (!this.currentSessionId) {
337
+ throw new Error("No active session. Call createSession() first.");
338
+ }
339
+ console.debug(
340
+ "[CoordinatorClient] Getting session info for:",
341
+ this.currentSessionId
342
+ );
343
+ const response = yield fetch(
344
+ `${this.baseUrl}/sessions/${this.currentSessionId}`,
345
+ {
346
+ method: "GET",
347
+ headers: this.getAuthHeaders()
348
+ }
349
+ );
350
+ if (response.status !== 200) {
351
+ const errorText = yield response.text();
352
+ throw new Error(`Failed to get session: ${response.status} ${errorText}`);
353
+ }
354
+ const data = yield response.json();
355
+ return data;
356
+ });
205
357
  }
206
- connect() {
358
+ /**
359
+ * Terminates the current session by sending a DELETE request to the coordinator.
360
+ * @throws Error if no active session exists or if the request fails (except for 404)
361
+ */
362
+ terminateSession() {
207
363
  return __async(this, null, function* () {
208
- const url = new URL(this.wsUrl);
209
- if (this.jwtToken) {
210
- url.searchParams.set("jwt_token", this.jwtToken);
211
- } else if (this.insecureApiKey) {
212
- url.searchParams.set("api_key", this.insecureApiKey);
364
+ if (!this.currentSessionId) {
365
+ throw new Error("No active session. Call createSession() first.");
213
366
  }
214
- if (this.queueing) {
215
- url.searchParams.set("queueing", "true");
367
+ console.debug(
368
+ "[CoordinatorClient] Terminating session:",
369
+ this.currentSessionId
370
+ );
371
+ const response = yield fetch(
372
+ `${this.baseUrl}/sessions/${this.currentSessionId}`,
373
+ {
374
+ method: "DELETE",
375
+ headers: this.getAuthHeaders()
376
+ }
377
+ );
378
+ if (response.status === 200 || response.status === 204) {
379
+ const data = yield response.json();
380
+ console.debug(
381
+ "[CoordinatorClient] Session terminated:",
382
+ data.session_id,
383
+ data.status
384
+ );
385
+ this.currentSessionId = void 0;
386
+ return;
387
+ }
388
+ if (response.status === 404) {
389
+ console.debug(
390
+ "[CoordinatorClient] Session not found on server, clearing local state:",
391
+ this.currentSessionId
392
+ );
393
+ this.currentSessionId = void 0;
394
+ return;
216
395
  }
217
- console.debug("[CoordinatorClient] Connecting to", url.toString());
218
- this.websocket = new WebSocket(url.toString());
219
- this.websocket.onopen = () => {
220
- console.debug("[CoordinatorClient] WebSocket opened");
221
- this.emit("statusChanged", "connected");
396
+ const errorText = yield response.text();
397
+ throw new Error(
398
+ `Failed to terminate session: ${response.status} ${errorText}`
399
+ );
400
+ });
401
+ }
402
+ /**
403
+ * Get the current session ID
404
+ */
405
+ getSessionId() {
406
+ return this.currentSessionId;
407
+ }
408
+ /**
409
+ * Sends an SDP offer to the server for reconnection.
410
+ * @param sessionId - The session ID to connect to
411
+ * @param sdpOffer - The SDP offer from the local WebRTC peer connection
412
+ * @returns The SDP answer if ready (200), or null if pending (202)
413
+ */
414
+ sendSdpOffer(sessionId, sdpOffer) {
415
+ return __async(this, null, function* () {
416
+ console.debug(
417
+ "[CoordinatorClient] Sending SDP offer for session:",
418
+ sessionId
419
+ );
420
+ const requestBody = {
421
+ sdp_offer: sdpOffer,
422
+ extra_args: {}
222
423
  };
223
- this.websocket.onmessage = (event) => {
224
- try {
225
- let parsedData;
226
- if (typeof event.data === "string") {
227
- parsedData = JSON.parse(event.data);
228
- } else {
229
- parsedData = event.data;
424
+ const response = yield fetch(
425
+ `${this.baseUrl}/sessions/${sessionId}/sdp_params`,
426
+ {
427
+ method: "PUT",
428
+ headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
429
+ "Content-Type": "application/json"
430
+ }),
431
+ body: JSON.stringify(requestBody)
432
+ }
433
+ );
434
+ if (response.status === 200) {
435
+ const answerData = yield response.json();
436
+ console.debug("[CoordinatorClient] Received SDP answer immediately");
437
+ return answerData.sdp_answer;
438
+ }
439
+ if (response.status === 202) {
440
+ console.debug(
441
+ "[CoordinatorClient] SDP offer accepted, answer pending (202)"
442
+ );
443
+ return null;
444
+ }
445
+ const errorText = yield response.text();
446
+ throw new Error(
447
+ `Failed to send SDP offer: ${response.status} ${errorText}`
448
+ );
449
+ });
450
+ }
451
+ /**
452
+ * Polls for the SDP answer with geometric backoff.
453
+ * Used for async reconnection when the answer is not immediately available.
454
+ * @param sessionId - The session ID to poll for
455
+ * @returns The SDP answer from the server
456
+ */
457
+ pollSdpAnswer(sessionId) {
458
+ return __async(this, null, function* () {
459
+ console.debug(
460
+ "[CoordinatorClient] Polling for SDP answer for session:",
461
+ sessionId
462
+ );
463
+ let backoffMs = INITIAL_BACKOFF_MS;
464
+ let attempt = 0;
465
+ while (true) {
466
+ attempt++;
467
+ console.debug(
468
+ `[CoordinatorClient] SDP poll attempt ${attempt} for session ${sessionId}`
469
+ );
470
+ const response = yield fetch(
471
+ `${this.baseUrl}/sessions/${sessionId}/sdp_params`,
472
+ {
473
+ method: "GET",
474
+ headers: __spreadProps(__spreadValues({}, this.getAuthHeaders()), {
475
+ "Content-Type": "application/json"
476
+ })
230
477
  }
231
- console.debug(
232
- "[CoordinatorClient] Received message from coordinator:",
233
- parsedData
478
+ );
479
+ if (response.status === 200) {
480
+ const answerData = SDPParamsResponse.fromJSON(
481
+ yield response.json()
234
482
  );
235
- const validatedData = CoordinatorMessageSchema.parse(parsedData);
236
- this.emit(validatedData.type, validatedData.data);
237
- } catch (error) {
238
- console.error(
239
- "[CoordinatorClient] Failed to parse WebSocket message from coordinator:",
240
- error,
241
- "message",
242
- event.data
483
+ console.debug("[CoordinatorClient] Received SDP answer via polling");
484
+ return answerData.sdp_answer;
485
+ }
486
+ if (response.status === 202) {
487
+ console.warn(
488
+ `[CoordinatorClient] SDP answer pending (202), retrying in ${backoffMs}ms...`
243
489
  );
244
- this.emit("statusChanged", "error");
490
+ yield this.sleep(backoffMs);
491
+ backoffMs = Math.min(backoffMs * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
492
+ continue;
245
493
  }
246
- };
247
- this.websocket.onclose = (event) => {
248
- console.debug("[CoordinatorClient] WebSocket closed", event);
249
- this.websocket = void 0;
250
- this.emit("statusChanged", "disconnected");
251
- };
252
- this.websocket.onerror = (error) => {
253
- console.error("[CoordinatorClient] WebSocket error:", error);
254
- this.websocket = void 0;
255
- this.emit("statusChanged", "error");
256
- };
257
- yield new Promise((resolve, reject) => {
258
- var _a, _b;
259
- const onOpen = () => {
260
- var _a2;
261
- (_a2 = this.websocket) == null ? void 0 : _a2.removeEventListener("error", onError);
262
- resolve();
263
- };
264
- const onError = (error) => {
265
- var _a2;
266
- (_a2 = this.websocket) == null ? void 0 : _a2.removeEventListener("open", onOpen);
267
- reject(error);
268
- };
269
- (_a = this.websocket) == null ? void 0 : _a.addEventListener("open", onOpen);
270
- (_b = this.websocket) == null ? void 0 : _b.addEventListener("error", onError);
271
- });
272
- console.log("[CoordinatorClient] WebSocket connected");
273
- this.sendMessage({
274
- type: "sessionSetup",
275
- data: {
276
- modelName: this.modelName,
277
- modelVersion: this.modelVersion
494
+ const errorText = yield response.text();
495
+ throw new Error(
496
+ `Failed to poll SDP answer: ${response.status} ${errorText}`
497
+ );
498
+ }
499
+ });
500
+ }
501
+ /**
502
+ * Connects to the session by sending an SDP offer and receiving an SDP answer.
503
+ * If sdpOffer is provided, sends it first. If the answer is pending (202),
504
+ * falls back to polling. If no sdpOffer is provided, goes directly to polling.
505
+ * @param sessionId - The session ID to connect to
506
+ * @param sdpOffer - Optional SDP offer from the local WebRTC peer connection
507
+ * @returns The SDP answer from the server
508
+ */
509
+ connect(sessionId, sdpOffer) {
510
+ return __async(this, null, function* () {
511
+ console.debug("[CoordinatorClient] Connecting to session:", sessionId);
512
+ if (sdpOffer) {
513
+ const answer = yield this.sendSdpOffer(sessionId, sdpOffer);
514
+ if (answer !== null) {
515
+ return answer;
278
516
  }
517
+ }
518
+ return this.pollSdpAnswer(sessionId);
519
+ });
520
+ }
521
+ /**
522
+ * Utility function to sleep for a given number of milliseconds
523
+ */
524
+ sleep(ms) {
525
+ return new Promise((resolve) => setTimeout(resolve, ms));
526
+ }
527
+ };
528
+
529
+ // src/core/LocalCoordinatorClient.ts
530
+ var LocalCoordinatorClient = class extends CoordinatorClient {
531
+ constructor(baseUrl) {
532
+ super({
533
+ baseUrl,
534
+ jwtToken: "local",
535
+ model: "local"
536
+ });
537
+ this.localBaseUrl = baseUrl;
538
+ }
539
+ /**
540
+ * Creates a local session by posting to /start_session.
541
+ * @returns always "local"
542
+ */
543
+ createSession(sdpOffer) {
544
+ return __async(this, null, function* () {
545
+ console.debug("[LocalCoordinatorClient] Creating local session...");
546
+ this.sdpOffer = sdpOffer;
547
+ const response = yield fetch(`${this.localBaseUrl}/start_session`, {
548
+ method: "POST"
279
549
  });
280
- console.debug("[CoordinatorClient] Setup session message sent");
550
+ if (!response.ok) {
551
+ throw new Error("Failed to send local start session command.");
552
+ }
553
+ console.debug("[LocalCoordinatorClient] Local session created");
554
+ return "local";
281
555
  });
282
556
  }
283
557
  /**
284
- * Closes the WebSocket connection if it exists.
285
- * This will trigger the onclose event handler.
558
+ * Connects to the local session by posting SDP params to /sdp_params.
559
+ * @param sessionId - The session ID (ignored for local)
560
+ * @param sdpMessage - The SDP offer from the local WebRTC peer connection
561
+ * @returns The SDP answer from the server
286
562
  */
287
- disconnect() {
288
- if (this.websocket) {
289
- console.debug("[CoordinatorClient] Closing WebSocket connection");
290
- this.websocket.close();
291
- this.websocket = void 0;
292
- }
563
+ connect(sessionId, sdpMessage) {
564
+ return __async(this, null, function* () {
565
+ this.sdpOffer = sdpMessage || this.sdpOffer;
566
+ console.debug("[LocalCoordinatorClient] Connecting to local session...");
567
+ const sdpBody = {
568
+ sdp: this.sdpOffer,
569
+ type: "offer"
570
+ };
571
+ const response = yield fetch(`${this.localBaseUrl}/sdp_params`, {
572
+ method: "POST",
573
+ headers: {
574
+ "Content-Type": "application/json"
575
+ },
576
+ body: JSON.stringify(sdpBody)
577
+ });
578
+ if (!response.ok) {
579
+ throw new Error("Failed to get SDP answer from local coordinator.");
580
+ }
581
+ const sdpAnswer = yield response.json();
582
+ console.debug("[LocalCoordinatorClient] Received SDP answer");
583
+ return sdpAnswer.sdp;
584
+ });
585
+ }
586
+ terminateSession() {
587
+ return __async(this, null, function* () {
588
+ console.debug("[LocalCoordinatorClient] Stopping local session...");
589
+ yield fetch(`${this.localBaseUrl}/stop_session`, {
590
+ method: "POST"
591
+ });
592
+ });
293
593
  }
294
594
  };
295
595
 
596
+ // src/utils/webrtc.ts
597
+ var DEFAULT_ICE_SERVERS = [
598
+ { urls: "stun:stun.l.google.com:19302" },
599
+ { urls: "stun:stun1.l.google.com:19302" }
600
+ ];
601
+ var DEFAULT_DATA_CHANNEL_LABEL = "data";
602
+ function createPeerConnection(config) {
603
+ var _a;
604
+ return new RTCPeerConnection({
605
+ iceServers: (_a = config == null ? void 0 : config.iceServers) != null ? _a : DEFAULT_ICE_SERVERS
606
+ });
607
+ }
608
+ function createDataChannel(pc, label) {
609
+ return pc.createDataChannel(label != null ? label : DEFAULT_DATA_CHANNEL_LABEL);
610
+ }
611
+ function createOffer(pc) {
612
+ return __async(this, null, function* () {
613
+ const offer = yield pc.createOffer();
614
+ yield pc.setLocalDescription(offer);
615
+ yield waitForIceGathering(pc);
616
+ const localDescription = pc.localDescription;
617
+ if (!localDescription) {
618
+ throw new Error("Failed to create local description");
619
+ }
620
+ return localDescription.sdp;
621
+ });
622
+ }
623
+ function setRemoteDescription(pc, sdp) {
624
+ return __async(this, null, function* () {
625
+ const sessionDescription = new RTCSessionDescription({
626
+ sdp,
627
+ type: "answer"
628
+ });
629
+ yield pc.setRemoteDescription(sessionDescription);
630
+ });
631
+ }
632
+ function getLocalDescription(pc) {
633
+ const desc = pc.localDescription;
634
+ if (!desc) return void 0;
635
+ return desc.sdp;
636
+ }
637
+ function waitForIceGathering(pc, timeoutMs = 5e3) {
638
+ return new Promise((resolve) => {
639
+ if (pc.iceGatheringState === "complete") {
640
+ resolve();
641
+ return;
642
+ }
643
+ const onGatheringStateChange = () => {
644
+ if (pc.iceGatheringState === "complete") {
645
+ pc.removeEventListener(
646
+ "icegatheringstatechange",
647
+ onGatheringStateChange
648
+ );
649
+ resolve();
650
+ }
651
+ };
652
+ pc.addEventListener("icegatheringstatechange", onGatheringStateChange);
653
+ setTimeout(() => {
654
+ pc.removeEventListener("icegatheringstatechange", onGatheringStateChange);
655
+ resolve();
656
+ }, timeoutMs);
657
+ });
658
+ }
659
+ function sendMessage(channel, command, data) {
660
+ if (channel.readyState !== "open") {
661
+ throw new Error(`Data channel not open: ${channel.readyState}`);
662
+ }
663
+ const jsonData = typeof data === "string" ? JSON.parse(data) : data;
664
+ const payload = { type: command, data: jsonData };
665
+ channel.send(JSON.stringify(payload));
666
+ }
667
+ function parseMessage(data) {
668
+ if (typeof data === "string") {
669
+ try {
670
+ return JSON.parse(data);
671
+ } catch (e) {
672
+ return data;
673
+ }
674
+ }
675
+ return data;
676
+ }
677
+ function closePeerConnection(pc) {
678
+ pc.close();
679
+ }
680
+
296
681
  // src/core/GPUMachineClient.ts
297
- var import_livekit_client = require("livekit-client");
298
682
  var GPUMachineClient = class {
299
- constructor(token, liveKitUrl) {
683
+ constructor(config) {
300
684
  this.eventListeners = /* @__PURE__ */ new Map();
301
- this.token = token;
302
- this.liveKitUrl = liveKitUrl;
685
+ this.status = "disconnected";
686
+ this.config = config != null ? config : {};
303
687
  }
688
+ // ─────────────────────────────────────────────────────────────────────────────
304
689
  // Event Emitter API
690
+ // ─────────────────────────────────────────────────────────────────────────────
305
691
  on(event, handler) {
306
692
  if (!this.eventListeners.has(event)) {
307
693
  this.eventListeners.set(event, /* @__PURE__ */ new Set());
@@ -316,294 +702,287 @@ var GPUMachineClient = class {
316
702
  var _a;
317
703
  (_a = this.eventListeners.get(event)) == null ? void 0 : _a.forEach((handler) => handler(...args));
318
704
  }
319
- sendMessage(message) {
705
+ // ─────────────────────────────────────────────────────────────────────────────
706
+ // SDP & Connection
707
+ // ─────────────────────────────────────────────────────────────────────────────
708
+ /**
709
+ * Creates an SDP offer for initiating a connection.
710
+ * Must be called before connect().
711
+ */
712
+ createOffer() {
320
713
  return __async(this, null, function* () {
321
- try {
322
- if (this.roomInstance) {
323
- const messageStr = JSON.stringify(message);
324
- yield this.roomInstance.localParticipant.sendText(messageStr, {
325
- topic: "application"
326
- });
327
- } else {
328
- console.warn(
329
- "[GPUMachineClient] Cannot send message - not connected to room"
330
- );
331
- }
332
- } catch (error) {
333
- console.error("[GPUMachineClient] Failed to send message:", error);
334
- this.emit("statusChanged", "error");
714
+ if (!this.peerConnection) {
715
+ this.peerConnection = createPeerConnection(this.config);
716
+ this.setupPeerConnectionHandlers();
335
717
  }
718
+ this.dataChannel = createDataChannel(
719
+ this.peerConnection,
720
+ this.config.dataChannelLabel
721
+ );
722
+ this.setupDataChannelHandlers();
723
+ this.videoTransceiver = this.peerConnection.addTransceiver("video", {
724
+ direction: "sendrecv"
725
+ });
726
+ const offer = yield createOffer(this.peerConnection);
727
+ console.debug("[GPUMachineClient] Created SDP offer");
728
+ return offer;
336
729
  });
337
730
  }
338
- connect() {
731
+ /**
732
+ * Connects to the GPU machine using the provided SDP answer.
733
+ * createOffer() must be called first.
734
+ * @param sdpAnswer The SDP answer from the GPU machine
735
+ */
736
+ connect(sdpAnswer) {
339
737
  return __async(this, null, function* () {
340
- this.roomInstance = new import_livekit_client.Room({
341
- adaptiveStream: true,
342
- dynacast: true
343
- });
344
- this.roomInstance.on(import_livekit_client.RoomEvent.Connected, () => {
345
- console.debug("[GPUMachineClient] Connected to room");
346
- this.emit("statusChanged", "connected");
347
- });
348
- this.roomInstance.on(import_livekit_client.RoomEvent.Disconnected, () => {
349
- console.debug("[GPUMachineClient] Disconnected from room");
350
- this.emit("statusChanged", "disconnected");
351
- });
352
- this.roomInstance.on(
353
- import_livekit_client.RoomEvent.TrackSubscribed,
354
- (track, _publication, participant) => {
355
- console.debug(
356
- "[GPUMachineClient] Track subscribed:",
357
- track.kind,
358
- participant.identity
359
- );
360
- if (track.kind === import_livekit_client.Track.Kind.Video) {
361
- const videoTrack = track;
362
- this.emit("streamChanged", videoTrack);
363
- }
364
- }
365
- );
366
- this.roomInstance.on(
367
- import_livekit_client.RoomEvent.TrackUnsubscribed,
368
- (track, _publication, participant) => {
369
- console.debug(
370
- "[GPUMachineClient] Track unsubscribed:",
371
- track.kind,
372
- participant.identity
373
- );
374
- if (track.kind === import_livekit_client.Track.Kind.Video) {
375
- this.emit("streamChanged", null);
376
- }
377
- }
378
- );
379
- this.roomInstance.registerTextStreamHandler(
380
- "application",
381
- (reader, participant) => __async(this, null, function* () {
382
- const text = yield reader.readAll();
383
- console.log("[GPUMachineClient] Received message:", text);
384
- try {
385
- const parsedData = JSON.parse(text);
386
- const validatedMessage = GPUMachineReceiveMessageSchema.parse(parsedData);
387
- if (validatedMessage.type === "fps") {
388
- this.machineFPS = validatedMessage.data;
389
- }
390
- this.emit(validatedMessage.type, validatedMessage.data);
391
- } catch (error) {
392
- console.error(
393
- "[GPUMachineClient] Failed to parse/validate message:",
394
- error
395
- );
396
- this.emit("statusChanged", "error");
397
- }
398
- })
399
- );
400
- yield this.roomInstance.connect(this.liveKitUrl, this.token);
401
- console.log("[GPUMachineClient] Room connected");
738
+ if (!this.peerConnection) {
739
+ throw new Error(
740
+ "[GPUMachineClient] Cannot connect - call createOffer() first"
741
+ );
742
+ }
743
+ if (this.peerConnection.signalingState !== "have-local-offer") {
744
+ throw new Error(
745
+ `[GPUMachineClient] Invalid signaling state: ${this.peerConnection.signalingState}`
746
+ );
747
+ }
748
+ this.setStatus("connecting");
749
+ try {
750
+ yield setRemoteDescription(this.peerConnection, sdpAnswer);
751
+ console.debug("[GPUMachineClient] Remote description set");
752
+ } catch (error) {
753
+ console.error("[GPUMachineClient] Failed to connect:", error);
754
+ this.setStatus("error");
755
+ throw error;
756
+ }
402
757
  });
403
758
  }
404
759
  /**
405
- * Closes the LiveKit connection if it exists.
406
- * This will trigger the onclose event handler.
760
+ * Disconnects from the GPU machine and cleans up resources.
407
761
  */
408
762
  disconnect() {
409
763
  return __async(this, null, function* () {
410
- if (this.publishedVideoTrack) {
411
- yield this.unpublishVideoTrack();
764
+ if (this.publishedTrack) {
765
+ yield this.unpublishTrack();
412
766
  }
413
- if (this.publishedAudioTrack) {
414
- yield this.unpublishAudioTrack();
767
+ if (this.dataChannel) {
768
+ this.dataChannel.close();
769
+ this.dataChannel = void 0;
415
770
  }
416
- if (this.roomInstance) {
417
- console.debug("[GPUMachineClient] Closing LiveKit connection");
418
- yield this.roomInstance.disconnect();
419
- this.roomInstance = void 0;
771
+ if (this.peerConnection) {
772
+ closePeerConnection(this.peerConnection);
773
+ this.peerConnection = void 0;
420
774
  }
421
- this.machineFPS = void 0;
775
+ this.videoTransceiver = void 0;
776
+ this.setStatus("disconnected");
777
+ console.debug("[GPUMachineClient] Disconnected");
422
778
  });
423
779
  }
424
780
  /**
425
- * Returns the current fps rate of the machine.
426
- * @returns The current fps rate of the machine.
781
+ * Returns the current connection status.
782
+ */
783
+ getStatus() {
784
+ return this.status;
785
+ }
786
+ /**
787
+ * Gets the current local SDP description.
427
788
  */
428
- getFPS() {
429
- return this.machineFPS;
789
+ getLocalSDP() {
790
+ if (!this.peerConnection) return void 0;
791
+ return getLocalDescription(this.peerConnection);
430
792
  }
793
+ isOfferStillValid() {
794
+ if (!this.peerConnection) return false;
795
+ return this.peerConnection.signalingState === "have-local-offer";
796
+ }
797
+ // ─────────────────────────────────────────────────────────────────────────────
798
+ // Messaging
799
+ // ─────────────────────────────────────────────────────────────────────────────
431
800
  /**
432
- * Returns the current video stream from the GPU machine.
433
- * @returns The current video stream or undefined if not available.
801
+ * Sends a command to the GPU machine via the data channel.
802
+ * @param command The command to send
803
+ * @param data The data to send with the command. These are the parameters for the command, matching the scheme in the capabilities dictionary.
434
804
  */
435
- getVideoStream() {
436
- return this.videoStream;
805
+ sendCommand(command, data) {
806
+ if (!this.dataChannel) {
807
+ throw new Error("[GPUMachineClient] Data channel not available");
808
+ }
809
+ try {
810
+ sendMessage(this.dataChannel, command, data);
811
+ } catch (error) {
812
+ console.warn("[GPUMachineClient] Failed to send message:", error);
813
+ }
437
814
  }
815
+ // ─────────────────────────────────────────────────────────────────────────────
816
+ // Track Publishing
817
+ // ─────────────────────────────────────────────────────────────────────────────
438
818
  /**
439
- * Publishes a video track from the provided MediaStream to the LiveKit room.
440
- * Only one video track can be published at a time. If a video track is already
441
- * published, it will be unpublished first.
442
- *
443
- * @param mediaStream The MediaStream containing the video track to publish
444
- * @throws Error if no video track is found in the MediaStream or room is not connected
819
+ * Publishes a track to the GPU machine.
820
+ * Only one track can be published at a time.
821
+ * Uses the existing transceiver's sender to replace the track.
822
+ * @param track The MediaStreamTrack to publish
445
823
  */
446
- publishVideoTrack(mediaStream) {
824
+ publishTrack(track) {
447
825
  return __async(this, null, function* () {
448
- if (!this.roomInstance) {
826
+ if (!this.peerConnection) {
449
827
  throw new Error(
450
- "[GPUMachineClient] Cannot publish track - not connected to room"
828
+ "[GPUMachineClient] Cannot publish track - not initialized"
451
829
  );
452
830
  }
453
- const videoTracks = mediaStream.getVideoTracks();
454
- if (videoTracks.length === 0) {
455
- throw new Error("[GPUMachineClient] No video track found in MediaStream");
831
+ if (this.status !== "connected") {
832
+ throw new Error(
833
+ "[GPUMachineClient] Cannot publish track - not connected"
834
+ );
456
835
  }
457
- if (this.publishedVideoTrack) {
458
- yield this.unpublishVideoTrack();
836
+ if (!this.videoTransceiver) {
837
+ throw new Error(
838
+ "[GPUMachineClient] Cannot publish track - no video transceiver"
839
+ );
459
840
  }
460
841
  try {
461
- const videoTrack = videoTracks[0];
462
- const localVideoTrack = yield this.roomInstance.localParticipant.publishTrack(videoTrack, {
463
- name: "client-video",
464
- source: import_livekit_client.Track.Source.Camera
465
- });
466
- this.publishedVideoTrack = localVideoTrack.track;
467
- console.debug("[GPUMachineClient] Video track published successfully");
842
+ yield this.videoTransceiver.sender.replaceTrack(track);
843
+ this.publishedTrack = track;
844
+ console.debug(
845
+ "[GPUMachineClient] Track published successfully:",
846
+ track.kind
847
+ );
468
848
  } catch (error) {
469
- console.error("[GPUMachineClient] Failed to publish video track:", error);
849
+ console.error("[GPUMachineClient] Failed to publish track:", error);
470
850
  throw error;
471
851
  }
472
852
  });
473
853
  }
474
854
  /**
475
- * Unpublishes the currently published video track.
476
- * Note: We pass false to unpublishTrack to prevent LiveKit from stopping
477
- * the source MediaStreamTrack, as it's owned by the component that created it.
855
+ * Unpublishes the currently published track.
478
856
  */
479
- unpublishVideoTrack() {
857
+ unpublishTrack() {
480
858
  return __async(this, null, function* () {
481
- if (!this.roomInstance || !this.publishedVideoTrack) {
482
- return;
483
- }
484
- const publishedVideoTrack = this.publishedVideoTrack;
485
- this.publishedVideoTrack = void 0;
859
+ if (!this.videoTransceiver || !this.publishedTrack) return;
486
860
  try {
487
- yield this.roomInstance.localParticipant.unpublishTrack(
488
- publishedVideoTrack,
489
- false
490
- // Don't stop the source track - it's managed externally
491
- );
492
- console.debug("[GPUMachineClient] Video track unpublished successfully");
861
+ yield this.videoTransceiver.sender.replaceTrack(null);
862
+ console.debug("[GPUMachineClient] Track unpublished successfully");
493
863
  } catch (error) {
494
- console.error(
495
- "[GPUMachineClient] Failed to unpublish video track:",
496
- error
497
- );
864
+ console.error("[GPUMachineClient] Failed to unpublish track:", error);
498
865
  throw error;
866
+ } finally {
867
+ this.publishedTrack = void 0;
499
868
  }
500
869
  });
501
870
  }
502
871
  /**
503
- * Publishes an audio track from the provided MediaStream to the LiveKit room.
504
- * This is an internal method. Only one audio track can be published at a time.
505
- *
506
- * @param mediaStream The MediaStream containing the audio track to publish
507
- * @private
872
+ * Returns the currently published track.
508
873
  */
509
- publishAudioTrack(mediaStream) {
510
- return __async(this, null, function* () {
511
- if (!this.roomInstance) {
512
- throw new Error(
513
- "[GPUMachineClient] Cannot publish track - not connected to room"
514
- );
515
- }
516
- const audioTracks = mediaStream.getAudioTracks();
517
- if (audioTracks.length === 0) {
518
- throw new Error("[GPUMachineClient] No audio track found in MediaStream");
519
- }
520
- if (this.publishedAudioTrack) {
521
- yield this.unpublishAudioTrack();
522
- }
523
- try {
524
- const audioTrack = audioTracks[0];
525
- const localAudioTrack = yield this.roomInstance.localParticipant.publishTrack(audioTrack, {
526
- name: "client-audio",
527
- source: import_livekit_client.Track.Source.Microphone
528
- });
529
- this.publishedAudioTrack = localAudioTrack.track;
530
- console.debug("[GPUMachineClient] Audio track published successfully");
531
- } catch (error) {
532
- console.error("[GPUMachineClient] Failed to publish audio track:", error);
533
- throw error;
534
- }
535
- });
874
+ getPublishedTrack() {
875
+ return this.publishedTrack;
536
876
  }
877
+ // ─────────────────────────────────────────────────────────────────────────────
878
+ // Getters
879
+ // ─────────────────────────────────────────────────────────────────────────────
537
880
  /**
538
- * Unpublishes the currently published audio track.
539
- * Note: We pass false to unpublishTrack to prevent LiveKit from stopping
540
- * the source MediaStreamTrack, as it's owned by the component that created it.
541
- * @private
881
+ * Returns the remote media stream from the GPU machine.
542
882
  */
543
- unpublishAudioTrack() {
544
- return __async(this, null, function* () {
545
- if (!this.roomInstance || !this.publishedAudioTrack) {
546
- return;
883
+ getRemoteStream() {
884
+ if (!this.peerConnection) return void 0;
885
+ const receivers = this.peerConnection.getReceivers();
886
+ const tracks = receivers.map((r) => r.track).filter((t) => t !== null);
887
+ if (tracks.length === 0) return void 0;
888
+ return new MediaStream(tracks);
889
+ }
890
+ // ─────────────────────────────────────────────────────────────────────────────
891
+ // Private Helpers
892
+ // ─────────────────────────────────────────────────────────────────────────────
893
+ setStatus(newStatus) {
894
+ if (this.status !== newStatus) {
895
+ this.status = newStatus;
896
+ this.emit("statusChanged", newStatus);
897
+ }
898
+ }
899
+ setupPeerConnectionHandlers() {
900
+ if (!this.peerConnection) return;
901
+ this.peerConnection.onconnectionstatechange = () => {
902
+ var _a;
903
+ const state = (_a = this.peerConnection) == null ? void 0 : _a.connectionState;
904
+ console.debug("[GPUMachineClient] Connection state:", state);
905
+ if (state) {
906
+ switch (state) {
907
+ case "connected":
908
+ this.setStatus("connected");
909
+ break;
910
+ case "disconnected":
911
+ case "closed":
912
+ this.setStatus("disconnected");
913
+ break;
914
+ case "failed":
915
+ this.setStatus("error");
916
+ break;
917
+ }
547
918
  }
548
- const publishedAudioTrack = this.publishedAudioTrack;
549
- this.publishedAudioTrack = void 0;
919
+ };
920
+ this.peerConnection.ontrack = (event) => {
921
+ var _a;
922
+ console.debug("[GPUMachineClient] Track received:", event.track.kind);
923
+ const stream = (_a = event.streams[0]) != null ? _a : new MediaStream([event.track]);
924
+ this.emit("trackReceived", event.track, stream);
925
+ };
926
+ this.peerConnection.onicecandidate = (event) => {
927
+ if (event.candidate) {
928
+ console.debug("[GPUMachineClient] ICE candidate:", event.candidate);
929
+ }
930
+ };
931
+ this.peerConnection.onicecandidateerror = (event) => {
932
+ console.warn("[GPUMachineClient] ICE candidate error:", event);
933
+ };
934
+ this.peerConnection.ondatachannel = (event) => {
935
+ console.debug("[GPUMachineClient] Data channel received from remote");
936
+ this.dataChannel = event.channel;
937
+ this.setupDataChannelHandlers();
938
+ };
939
+ }
940
+ setupDataChannelHandlers() {
941
+ if (!this.dataChannel) return;
942
+ this.dataChannel.onopen = () => {
943
+ console.debug("[GPUMachineClient] Data channel open");
944
+ };
945
+ this.dataChannel.onclose = () => {
946
+ console.debug("[GPUMachineClient] Data channel closed");
947
+ };
948
+ this.dataChannel.onerror = (error) => {
949
+ console.error("[GPUMachineClient] Data channel error:", error);
950
+ };
951
+ this.dataChannel.onmessage = (event) => {
952
+ const data = parseMessage(event.data);
953
+ console.debug("[GPUMachineClient] Received message:", data);
550
954
  try {
551
- yield this.roomInstance.localParticipant.unpublishTrack(
552
- publishedAudioTrack,
553
- false
554
- // Don't stop the source track - it's managed externally
555
- );
556
- console.debug("[GPUMachineClient] Audio track unpublished successfully");
955
+ this.emit("application", data);
557
956
  } catch (error) {
558
957
  console.error(
559
- "[GPUMachineClient] Failed to unpublish audio track:",
958
+ "[GPUMachineClient] Failed to parse/validate message:",
560
959
  error
561
960
  );
562
- throw error;
563
961
  }
564
- });
962
+ };
565
963
  }
566
964
  };
567
965
 
568
966
  // src/core/Reactor.ts
569
- var import_zod3 = require("zod");
570
- var LOCAL_COORDINATOR_URL = "ws://localhost:8080/ws";
571
- var LOCAL_INSECURE_API_KEY = "1234";
572
- var PROD_COORDINATOR_URL = "wss://api.reactor.inc/ws";
573
- var OptionsSchema2 = import_zod3.z.object({
574
- directConnection: import_zod3.z.object({
575
- livekitJwtToken: import_zod3.z.string(),
576
- livekitWsUrl: import_zod3.z.string()
577
- }).optional(),
578
- insecureApiKey: import_zod3.z.string().optional(),
579
- jwtToken: import_zod3.z.string().optional(),
580
- coordinatorUrl: import_zod3.z.string().default(PROD_COORDINATOR_URL),
581
- modelName: import_zod3.z.string(),
582
- queueing: import_zod3.z.boolean().default(false),
583
- local: import_zod3.z.boolean().default(false)
584
- }).refine(
585
- (data) => data.directConnection || data.insecureApiKey || data.jwtToken || data.local,
586
- {
587
- message: "At least one of directConnection, insecureApiKey, or jwtToken or local must be provided."
588
- }
589
- );
967
+ var import_zod = require("zod");
968
+ var LOCAL_COORDINATOR_URL = "http://localhost:8080";
969
+ var PROD_COORDINATOR_URL = "https://api.reactor.inc";
970
+ var OptionsSchema = import_zod.z.object({
971
+ coordinatorUrl: import_zod.z.string().default(PROD_COORDINATOR_URL),
972
+ modelName: import_zod.z.string(),
973
+ local: import_zod.z.boolean().default(false)
974
+ });
590
975
  var Reactor = class {
591
976
  constructor(options) {
592
- //client for the machine instance
593
977
  this.status = "disconnected";
594
978
  // Generic event map
595
979
  this.eventListeners = /* @__PURE__ */ new Map();
596
- const validatedOptions = OptionsSchema2.parse(options);
980
+ const validatedOptions = OptionsSchema.parse(options);
597
981
  this.coordinatorUrl = validatedOptions.coordinatorUrl;
598
- this.jwtToken = validatedOptions.jwtToken;
599
- this.insecureApiKey = validatedOptions.insecureApiKey;
600
- this.directConnection = validatedOptions.directConnection;
601
- this.modelName = validatedOptions.modelName;
602
- this.queueing = validatedOptions.queueing;
603
- this.modelVersion = "1.0.0";
604
- if (validatedOptions.local) {
982
+ this.model = validatedOptions.modelName;
983
+ this.local = validatedOptions.local;
984
+ if (this.local) {
605
985
  this.coordinatorUrl = LOCAL_COORDINATOR_URL;
606
- this.insecureApiKey = LOCAL_INSECURE_API_KEY;
607
986
  }
608
987
  }
609
988
  // Event Emitter API
@@ -627,20 +1006,16 @@ var Reactor = class {
627
1006
  * @param message The message to send to the machine.
628
1007
  * @throws Error if not in ready state
629
1008
  */
630
- sendMessage(message) {
1009
+ sendCommand(command, data) {
631
1010
  return __async(this, null, function* () {
632
1011
  var _a;
633
1012
  if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
634
1013
  const errorMessage = `Cannot send message, status is ${this.status}`;
635
- console.error("[Reactor] Not ready, cannot send message");
636
- throw new Error(errorMessage);
1014
+ console.warn("[Reactor]", errorMessage);
1015
+ return;
637
1016
  }
638
1017
  try {
639
- const applicationMessage = {
640
- type: "application",
641
- data: message
642
- };
643
- yield (_a = this.machineClient) == null ? void 0 : _a.sendMessage(applicationMessage);
1018
+ (_a = this.machineClient) == null ? void 0 : _a.sendCommand(command, data);
644
1019
  } catch (error) {
645
1020
  console.error("[Reactor] Failed to send message:", error);
646
1021
  this.createError(
@@ -653,24 +1028,24 @@ var Reactor = class {
653
1028
  });
654
1029
  }
655
1030
  /**
656
- * Public method to publish a video stream to the machine.
657
- * @param videoStream The video stream to send to the machine.
1031
+ * Public method to publish a track to the machine.
1032
+ * @param track The track to send to the machine.
658
1033
  */
659
- publishVideoStream(videoStream) {
1034
+ publishTrack(track) {
660
1035
  return __async(this, null, function* () {
661
1036
  var _a;
662
1037
  if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
663
- const errorMessage = `Cannot publish video stream, status is ${this.status}`;
664
- console.error("[Reactor] Not ready, cannot publish video stream");
665
- throw new Error(errorMessage);
1038
+ const errorMessage = `Cannot publish track, status is ${this.status}`;
1039
+ console.warn("[Reactor]", errorMessage);
1040
+ return;
666
1041
  }
667
1042
  try {
668
- yield (_a = this.machineClient) == null ? void 0 : _a.publishVideoTrack(videoStream);
1043
+ yield (_a = this.machineClient) == null ? void 0 : _a.publishTrack(track);
669
1044
  } catch (error) {
670
- console.error("[Reactor] Failed to publish video:", error);
1045
+ console.error("[Reactor] Failed to publish track:", error);
671
1046
  this.createError(
672
- "VIDEO_PUBLISH_FAILED",
673
- `Failed to publish video: ${error}`,
1047
+ "TRACK_PUBLISH_FAILED",
1048
+ `Failed to publish track: ${error}`,
674
1049
  "gpu",
675
1050
  true
676
1051
  );
@@ -678,19 +1053,18 @@ var Reactor = class {
678
1053
  });
679
1054
  }
680
1055
  /**
681
- * Public method to unpublish video stream to the machine.
682
- * This unpublishes the video track that was previously sent.
1056
+ * Public method to unpublish the currently published track.
683
1057
  */
684
- unpublishVideoStream() {
1058
+ unpublishTrack() {
685
1059
  return __async(this, null, function* () {
686
1060
  var _a;
687
1061
  try {
688
- yield (_a = this.machineClient) == null ? void 0 : _a.unpublishVideoTrack();
1062
+ yield (_a = this.machineClient) == null ? void 0 : _a.unpublishTrack();
689
1063
  } catch (error) {
690
- console.error("[Reactor] Failed to unpublish video:", error);
1064
+ console.error("[Reactor] Failed to unpublish track:", error);
691
1065
  this.createError(
692
- "VIDEO_UNPUBLISH_FAILED",
693
- `Failed to unpublish video: ${error}`,
1066
+ "TRACK_UNPUBLISH_FAILED",
1067
+ `Failed to unpublish track: ${error}`,
694
1068
  "gpu",
695
1069
  true
696
1070
  );
@@ -698,152 +1072,76 @@ var Reactor = class {
698
1072
  });
699
1073
  }
700
1074
  /**
701
- * Connects to the machine via LiveKit and waits for the gpu machine to be ready.
702
- * Once the machine is ready, the Reactor will establish the LiveKit connection.
703
- * @param livekitJwtToken The JWT token for LiveKit authentication
704
- * @param livekitWsUrl The WebSocket URL for LiveKit connection
1075
+ * Public method for reconnecting to an existing session, that may have been interrupted but can be recovered.
705
1076
  */
706
- connectToGPUMachine(livekitJwtToken, livekitWsUrl) {
1077
+ reconnect() {
707
1078
  return __async(this, null, function* () {
708
- console.debug("[Reactor] Connecting to machine room...");
1079
+ if (!this.sessionId || !this.coordinatorClient) {
1080
+ console.warn("[Reactor] No active session to reconnect to.");
1081
+ return;
1082
+ }
1083
+ this.setStatus("connecting");
1084
+ if (!this.machineClient) {
1085
+ this.machineClient = new GPUMachineClient();
1086
+ this.setupMachineClientHandlers();
1087
+ }
1088
+ const sdpOffer = yield this.machineClient.createOffer();
709
1089
  try {
710
- this.machineClient = new GPUMachineClient(livekitJwtToken, livekitWsUrl);
711
- this.machineClient.on("application", (message) => {
712
- this.emit("newMessage", message);
713
- });
714
- this.machineClient.on(
715
- "statusChanged",
716
- (status) => {
717
- switch (status) {
718
- case "connected":
719
- this.setStatus("ready");
720
- break;
721
- case "disconnected":
722
- this.disconnect();
723
- break;
724
- case "error":
725
- this.createError(
726
- "GPU_CONNECTION_ERROR",
727
- "GPU machine connection failed",
728
- "gpu",
729
- true
730
- );
731
- this.disconnect();
732
- break;
733
- }
734
- }
1090
+ const sdpAnswer = yield this.coordinatorClient.connect(
1091
+ this.sessionId,
1092
+ sdpOffer
735
1093
  );
736
- this.machineClient.on("fps", (fps) => {
737
- this.emit("fps", fps);
738
- });
739
- this.machineClient.on("streamChanged", (videoTrack) => {
740
- this.emit("streamChanged", videoTrack);
741
- });
742
- console.debug("[Reactor] About to connect to machine");
743
- yield this.machineClient.connect();
1094
+ yield this.machineClient.connect(sdpAnswer);
1095
+ this.setStatus("ready");
744
1096
  } catch (error) {
745
- throw error;
1097
+ console.error("[Reactor] Failed to reconnect:", error);
1098
+ this.disconnect(false);
1099
+ this.createError(
1100
+ "RECONNECTION_FAILED",
1101
+ `Failed to reconnect: ${error}`,
1102
+ "coordinator",
1103
+ true
1104
+ );
746
1105
  }
747
1106
  });
748
1107
  }
749
1108
  /**
750
1109
  * Connects to the coordinator and waits for a GPU to be assigned.
751
- * Once a GPU is assigned, the Reactor will connect to the gpu machine via LiveKit.
1110
+ * Once a GPU is assigned, the Reactor will connect to the gpu machine via WebRTC.
1111
+ * If no authentication is provided and not in local mode, an error is thrown.
752
1112
  */
753
- connect() {
1113
+ connect(jwtToken) {
754
1114
  return __async(this, null, function* () {
755
1115
  console.debug("[Reactor] Connecting, status:", this.status);
756
- if (this.status !== "disconnected")
1116
+ if (jwtToken == void 0 && !this.local) {
1117
+ throw new Error("No authentication provided and not in local mode");
1118
+ }
1119
+ if (this.status !== "disconnected") {
757
1120
  throw new Error("Already connected or connecting");
758
- if (this.directConnection) {
759
- return this.connectToGPUMachine(
760
- this.directConnection.livekitJwtToken,
761
- this.directConnection.livekitWsUrl
762
- );
763
1121
  }
764
1122
  this.setStatus("connecting");
765
1123
  try {
766
1124
  console.debug(
767
1125
  "[Reactor] Connecting to coordinator with authenticated URL"
768
1126
  );
769
- this.coordinatorClient = new CoordinatorClient({
770
- wsUrl: this.coordinatorUrl,
771
- jwtToken: this.jwtToken,
772
- insecureApiKey: this.insecureApiKey,
773
- modelName: this.modelName,
774
- modelVersion: this.modelVersion,
775
- queueing: this.queueing
1127
+ this.coordinatorClient = this.local ? new LocalCoordinatorClient(this.coordinatorUrl) : new CoordinatorClient({
1128
+ baseUrl: this.coordinatorUrl,
1129
+ jwtToken,
1130
+ // Safe: validated on line 186-188
1131
+ model: this.model
776
1132
  });
777
- this.coordinatorClient.on(
778
- "gpu-machine-assigned",
779
- (assignmentData) => __async(this, null, function* () {
780
- console.debug(
781
- "[Reactor] GPU machine assigned by coordinator:",
782
- assignmentData
783
- );
784
- try {
785
- yield this.connectToGPUMachine(
786
- assignmentData.livekitJwtToken,
787
- assignmentData.livekitWsUrl
788
- );
789
- } catch (error) {
790
- console.error("[Reactor] Failed to connect to GPU machine:", error);
791
- this.createError(
792
- "GPU_CONNECTION_FAILED",
793
- `Failed to connect to GPU machine: ${error}`,
794
- "gpu",
795
- true
796
- );
797
- this.disconnect();
798
- }
799
- })
800
- );
801
- this.coordinatorClient.on(
802
- "waiting-info",
803
- (waitingData) => {
804
- console.debug("[Reactor] Waiting info update received:", waitingData);
805
- this.setWaitingInfo(__spreadValues(__spreadValues({}, this.waitingInfo), waitingData));
806
- }
807
- );
808
- this.coordinatorClient.on(
809
- "session-expiration",
810
- (sessionExpirationData) => {
811
- this.setSessionExpiration(sessionExpirationData.expire);
812
- }
813
- );
814
- this.coordinatorClient.on(
815
- "statusChanged",
816
- (newStatus) => {
817
- switch (newStatus) {
818
- case "connected":
819
- this.setStatus("waiting");
820
- this.setWaitingInfo({
821
- position: void 0,
822
- estimatedWaitTime: void 0,
823
- averageWaitTime: void 0
824
- });
825
- break;
826
- case "disconnected":
827
- this.disconnect();
828
- break;
829
- case "error":
830
- this.createError(
831
- "COORDINATOR_CONNECTION_ERROR",
832
- "Coordinator connection failed",
833
- "coordinator",
834
- true
835
- );
836
- this.disconnect();
837
- break;
838
- }
839
- }
840
- );
841
- yield this.coordinatorClient.connect();
1133
+ this.machineClient = new GPUMachineClient();
1134
+ this.setupMachineClientHandlers();
1135
+ const sdpOffer = yield this.machineClient.createOffer();
1136
+ const sessionId = yield this.coordinatorClient.createSession(sdpOffer);
1137
+ this.setSessionId(sessionId);
1138
+ const sdpAnswer = yield this.coordinatorClient.connect(sessionId);
1139
+ yield this.machineClient.connect(sdpAnswer);
842
1140
  } catch (error) {
843
- console.error("[Reactor] Authentication failed:", error);
1141
+ console.error("[Reactor] Connection failed:", error);
844
1142
  this.createError(
845
- "AUTHENTICATION_FAILED",
846
- `Authentication failed: ${error}`,
1143
+ "CONNECTION_FAILED",
1144
+ `Connection failed: ${error}`,
847
1145
  "coordinator",
848
1146
  true
849
1147
  );
@@ -852,19 +1150,52 @@ var Reactor = class {
852
1150
  }
853
1151
  });
854
1152
  }
1153
+ /**
1154
+ * Sets up event handlers for the machine client.
1155
+ */
1156
+ setupMachineClientHandlers() {
1157
+ if (!this.machineClient) return;
1158
+ this.machineClient.on("application", (message) => {
1159
+ this.emit("newMessage", message);
1160
+ });
1161
+ this.machineClient.on("statusChanged", (status) => {
1162
+ switch (status) {
1163
+ case "connected":
1164
+ this.setStatus("ready");
1165
+ break;
1166
+ case "disconnected":
1167
+ this.disconnect(true);
1168
+ break;
1169
+ case "error":
1170
+ this.createError(
1171
+ "GPU_CONNECTION_ERROR",
1172
+ "GPU machine connection failed",
1173
+ "gpu",
1174
+ true
1175
+ );
1176
+ this.disconnect();
1177
+ break;
1178
+ }
1179
+ });
1180
+ this.machineClient.on(
1181
+ "trackReceived",
1182
+ (track, stream) => {
1183
+ this.emit("streamChanged", track, stream);
1184
+ }
1185
+ );
1186
+ }
855
1187
  /**
856
1188
  * Disconnects from the coordinator and the gpu machine.
857
1189
  * Ensures cleanup completes even if individual disconnections fail.
858
1190
  */
859
- disconnect() {
1191
+ disconnect(recoverable = false) {
860
1192
  return __async(this, null, function* () {
861
- if (this.status === "disconnected") return;
862
- if (this.coordinatorClient) {
863
- try {
864
- this.coordinatorClient.disconnect();
865
- } catch (error) {
866
- console.error("[Reactor] Error disconnecting from coordinator:", error);
867
- }
1193
+ if (this.status === "disconnected" && !this.sessionId) {
1194
+ console.warn("[Reactor] Already disconnected");
1195
+ return;
1196
+ }
1197
+ if (this.coordinatorClient && !recoverable) {
1198
+ yield this.coordinatorClient.terminateSession();
868
1199
  this.coordinatorClient = void 0;
869
1200
  }
870
1201
  if (this.machineClient) {
@@ -873,13 +1204,32 @@ var Reactor = class {
873
1204
  } catch (error) {
874
1205
  console.error("[Reactor] Error disconnecting from GPU machine:", error);
875
1206
  }
876
- this.machineClient = void 0;
1207
+ if (!recoverable) {
1208
+ this.machineClient = void 0;
1209
+ }
877
1210
  }
878
1211
  this.setStatus("disconnected");
879
- this.setSessionExpiration(void 0);
880
- this.setWaitingInfo(void 0);
1212
+ if (!recoverable) {
1213
+ this.setSessionExpiration(void 0);
1214
+ this.setSessionId(void 0);
1215
+ }
881
1216
  });
882
1217
  }
1218
+ setSessionId(newSessionId) {
1219
+ console.debug(
1220
+ "[Reactor] Setting session ID:",
1221
+ newSessionId,
1222
+ "from",
1223
+ this.sessionId
1224
+ );
1225
+ if (this.sessionId !== newSessionId) {
1226
+ this.sessionId = newSessionId;
1227
+ this.emit("sessionIdChanged", newSessionId);
1228
+ }
1229
+ }
1230
+ getSessionId() {
1231
+ return this.sessionId;
1232
+ }
883
1233
  setStatus(newStatus) {
884
1234
  console.debug("[Reactor] Setting status:", newStatus, "from", this.status);
885
1235
  if (this.status !== newStatus) {
@@ -887,17 +1237,8 @@ var Reactor = class {
887
1237
  this.emit("statusChanged", newStatus);
888
1238
  }
889
1239
  }
890
- setWaitingInfo(newWaitingInfo) {
891
- console.debug(
892
- "[Reactor] Setting waiting info:",
893
- newWaitingInfo,
894
- "from",
895
- this.waitingInfo
896
- );
897
- if (this.waitingInfo !== newWaitingInfo) {
898
- this.waitingInfo = newWaitingInfo;
899
- this.emit("waitingInfoChanged", newWaitingInfo);
900
- }
1240
+ getStatus() {
1241
+ return this.status;
901
1242
  }
902
1243
  /**
903
1244
  * Set the session expiration time.
@@ -913,25 +1254,15 @@ var Reactor = class {
913
1254
  this.emit("sessionExpirationChanged", newSessionExpiration);
914
1255
  }
915
1256
  }
916
- getStatus() {
917
- return this.status;
918
- }
919
1257
  /**
920
1258
  * Get the current state including status, error, and waiting info
921
1259
  */
922
1260
  getState() {
923
1261
  return {
924
1262
  status: this.status,
925
- waitingInfo: this.waitingInfo,
926
1263
  lastError: this.lastError
927
1264
  };
928
1265
  }
929
- /**
930
- * Get waiting information when status is 'waiting'
931
- */
932
- getWaitingInfo() {
933
- return this.waitingInfo;
934
- }
935
1266
  /**
936
1267
  * Get the last error that occurred
937
1268
  */
@@ -966,10 +1297,11 @@ var ReactorContext = (0, import_react2.createContext)(
966
1297
  var defaultInitState = {
967
1298
  status: "disconnected",
968
1299
  videoTrack: null,
969
- fps: void 0,
970
- waitingInfo: void 0,
971
1300
  lastError: void 0,
972
- sessionExpiration: void 0
1301
+ sessionExpiration: void 0,
1302
+ insecureApiKey: void 0,
1303
+ jwtToken: void 0,
1304
+ sessionId: void 0
973
1305
  };
974
1306
  var initReactorStore = (props) => {
975
1307
  return __spreadValues(__spreadValues({}, defaultInitState), props);
@@ -977,6 +1309,7 @@ var initReactorStore = (props) => {
977
1309
  var createReactorStore = (initProps, publicState = defaultInitState) => {
978
1310
  console.debug("[ReactorStore] Creating store", {
979
1311
  coordinatorUrl: initProps.coordinatorUrl,
1312
+ jwtToken: initProps.jwtToken,
980
1313
  initialState: publicState
981
1314
  });
982
1315
  return (0, import_react.create)()((set, get) => {
@@ -989,37 +1322,37 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
989
1322
  });
990
1323
  set({ status: newStatus });
991
1324
  });
992
- reactor.on("waitingInfoChanged", (newWaitingInfo) => {
993
- console.debug("[ReactorStore] Waiting info changed", {
994
- oldWaitingInfo: get().waitingInfo,
995
- newWaitingInfo
996
- });
997
- set({ waitingInfo: newWaitingInfo });
998
- });
999
- reactor.on("sessionExpirationChanged", (newSessionExpiration) => {
1000
- console.debug("[ReactorStore] Session expiration changed", {
1001
- oldSessionExpiration: get().sessionExpiration,
1002
- newSessionExpiration
1003
- });
1004
- set({ sessionExpiration: newSessionExpiration });
1005
- });
1325
+ reactor.on(
1326
+ "sessionExpirationChanged",
1327
+ (newSessionExpiration) => {
1328
+ console.debug("[ReactorStore] Session expiration changed", {
1329
+ oldSessionExpiration: get().sessionExpiration,
1330
+ newSessionExpiration
1331
+ });
1332
+ set({ sessionExpiration: newSessionExpiration });
1333
+ }
1334
+ );
1006
1335
  reactor.on("streamChanged", (videoTrack) => {
1007
1336
  console.debug("[ReactorStore] Stream changed", {
1008
1337
  hasVideoTrack: !!videoTrack,
1009
1338
  videoTrackKind: videoTrack == null ? void 0 : videoTrack.kind,
1010
- videoTrackSid: videoTrack == null ? void 0 : videoTrack.sid
1339
+ videoTrackId: videoTrack == null ? void 0 : videoTrack.id
1011
1340
  });
1012
1341
  set({ videoTrack });
1013
1342
  });
1014
- reactor.on("fps", (fps) => {
1015
- console.debug("[ReactorStore] FPS updated", { fps });
1016
- set({ fps });
1017
- });
1018
1343
  reactor.on("error", (error) => {
1019
1344
  console.debug("[ReactorStore] Error occurred", error);
1020
1345
  set({ lastError: error });
1021
1346
  });
1347
+ reactor.on("sessionIdChanged", (newSessionId) => {
1348
+ console.debug("[ReactorStore] Session ID changed", {
1349
+ oldSessionId: get().sessionId,
1350
+ newSessionId
1351
+ });
1352
+ set({ sessionId: newSessionId });
1353
+ });
1022
1354
  return __spreadProps(__spreadValues({}, publicState), {
1355
+ jwtToken: initProps.jwtToken,
1023
1356
  internal: { reactor },
1024
1357
  // actions
1025
1358
  onMessage: (handler) => {
@@ -1030,32 +1363,35 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1030
1363
  get().internal.reactor.off("newMessage", handler);
1031
1364
  };
1032
1365
  },
1033
- sendMessage: (mess) => __async(null, null, function* () {
1034
- console.debug("[ReactorStore] Sending message", { message: mess });
1366
+ sendCommand: (command, data) => __async(null, null, function* () {
1367
+ console.debug("[ReactorStore] Sending command", { command, data });
1035
1368
  try {
1036
- yield get().internal.reactor.sendMessage(mess);
1037
- console.debug("[ReactorStore] Message sent successfully");
1369
+ yield get().internal.reactor.sendCommand(command, data);
1370
+ console.debug("[ReactorStore] Command sent successfully");
1038
1371
  } catch (error) {
1039
- console.error("[ReactorStore] Failed to send message:", error);
1372
+ console.error("[ReactorStore] Failed to send command:", error);
1040
1373
  throw error;
1041
1374
  }
1042
1375
  }),
1043
- connect: () => __async(null, null, function* () {
1044
- console.debug("[ReactorStore] Connect called");
1376
+ connect: (jwtToken) => __async(null, null, function* () {
1377
+ if (jwtToken === void 0) {
1378
+ jwtToken = get().jwtToken;
1379
+ }
1380
+ console.debug("[ReactorStore] Connect called.");
1045
1381
  try {
1046
- yield get().internal.reactor.connect();
1382
+ yield get().internal.reactor.connect(jwtToken);
1047
1383
  console.debug("[ReactorStore] Connect completed successfully");
1048
1384
  } catch (error) {
1049
1385
  console.error("[ReactorStore] Connect failed:", error);
1050
1386
  throw error;
1051
1387
  }
1052
1388
  }),
1053
- disconnect: () => __async(null, null, function* () {
1389
+ disconnect: (recoverable = false) => __async(null, null, function* () {
1054
1390
  console.debug("[ReactorStore] Disconnect called", {
1055
1391
  currentStatus: get().status
1056
1392
  });
1057
1393
  try {
1058
- yield get().internal.reactor.disconnect();
1394
+ yield get().internal.reactor.disconnect(recoverable);
1059
1395
  console.debug("[ReactorStore] Disconnect completed successfully");
1060
1396
  } catch (error) {
1061
1397
  console.error("[ReactorStore] Disconnect failed:", error);
@@ -1065,7 +1401,7 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1065
1401
  publishVideoStream: (stream) => __async(null, null, function* () {
1066
1402
  console.debug("[ReactorStore] Publishing video stream");
1067
1403
  try {
1068
- yield get().internal.reactor.publishVideoStream(stream);
1404
+ yield get().internal.reactor.publishTrack(stream.getVideoTracks()[0]);
1069
1405
  console.debug("[ReactorStore] Video stream published successfully");
1070
1406
  } catch (error) {
1071
1407
  console.error(
@@ -1078,7 +1414,7 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1078
1414
  unpublishVideoStream: () => __async(null, null, function* () {
1079
1415
  console.debug("[ReactorStore] Unpublishing video stream");
1080
1416
  try {
1081
- yield get().internal.reactor.unpublishVideoStream();
1417
+ yield get().internal.reactor.unpublishTrack();
1082
1418
  console.debug("[ReactorStore] Video stream unpublished successfully");
1083
1419
  } catch (error) {
1084
1420
  console.error(
@@ -1087,6 +1423,16 @@ var createReactorStore = (initProps, publicState = defaultInitState) => {
1087
1423
  );
1088
1424
  throw error;
1089
1425
  }
1426
+ }),
1427
+ reconnect: () => __async(null, null, function* () {
1428
+ console.debug("[ReactorStore] Reconnecting");
1429
+ try {
1430
+ yield get().internal.reactor.reconnect();
1431
+ console.debug("[ReactorStore] Reconnect completed successfully");
1432
+ } catch (error) {
1433
+ console.error("[ReactorStore] Failed to reconnect:", error);
1434
+ throw error;
1435
+ }
1090
1436
  })
1091
1437
  });
1092
1438
  });
@@ -1098,37 +1444,48 @@ var import_jsx_runtime = require("react/jsx-runtime");
1098
1444
  function ReactorProvider(_a) {
1099
1445
  var _b = _a, {
1100
1446
  children,
1101
- autoConnect = true
1447
+ autoConnect = true,
1448
+ jwtToken
1102
1449
  } = _b, props = __objRest(_b, [
1103
1450
  "children",
1104
- "autoConnect"
1451
+ "autoConnect",
1452
+ "jwtToken"
1105
1453
  ]);
1106
1454
  const storeRef = (0, import_react3.useRef)(void 0);
1107
1455
  const firstRender = (0, import_react3.useRef)(true);
1108
1456
  const [_storeVersion, setStoreVersion] = (0, import_react3.useState)(0);
1109
1457
  if (storeRef.current === void 0) {
1110
1458
  console.debug("[ReactorProvider] Creating new reactor store");
1111
- storeRef.current = createReactorStore(initReactorStore(props));
1459
+ storeRef.current = createReactorStore(
1460
+ initReactorStore(__spreadProps(__spreadValues({}, props), {
1461
+ jwtToken
1462
+ }))
1463
+ );
1112
1464
  console.debug("[ReactorProvider] Reactor store created successfully");
1113
1465
  }
1114
- const {
1115
- coordinatorUrl,
1116
- modelName,
1117
- jwtToken,
1118
- insecureApiKey,
1119
- directConnection,
1120
- queueing,
1121
- local
1122
- } = props;
1466
+ const { coordinatorUrl, modelName, local } = props;
1467
+ (0, import_react3.useEffect)(() => {
1468
+ const handleBeforeUnload = () => {
1469
+ var _a2;
1470
+ console.debug(
1471
+ "[ReactorProvider] Page unloading, performing non-recoverable disconnect"
1472
+ );
1473
+ (_a2 = storeRef.current) == null ? void 0 : _a2.getState().internal.reactor.disconnect(false);
1474
+ };
1475
+ window.addEventListener("beforeunload", handleBeforeUnload);
1476
+ return () => {
1477
+ window.removeEventListener("beforeunload", handleBeforeUnload);
1478
+ };
1479
+ }, []);
1123
1480
  (0, import_react3.useEffect)(() => {
1124
1481
  if (firstRender.current) {
1125
1482
  firstRender.current = false;
1126
1483
  const current2 = storeRef.current;
1127
- if (autoConnect && current2.getState().status === "disconnected") {
1484
+ if (autoConnect && current2.getState().status === "disconnected" && jwtToken) {
1128
1485
  console.debug(
1129
1486
  "[ReactorProvider] Starting autoconnect in first render..."
1130
1487
  );
1131
- current2.getState().connect().then(() => {
1488
+ current2.getState().connect(jwtToken).then(() => {
1132
1489
  console.debug(
1133
1490
  "[ReactorProvider] Autoconnect successful in first render"
1134
1491
  );
@@ -1160,11 +1517,8 @@ function ReactorProvider(_a) {
1160
1517
  initReactorStore({
1161
1518
  coordinatorUrl,
1162
1519
  modelName,
1163
- jwtToken,
1164
- insecureApiKey,
1165
- directConnection,
1166
- queueing,
1167
- local
1520
+ local,
1521
+ jwtToken
1168
1522
  })
1169
1523
  );
1170
1524
  const current = storeRef.current;
@@ -1172,9 +1526,9 @@ function ReactorProvider(_a) {
1172
1526
  console.debug(
1173
1527
  "[ReactorProvider] Reactor store updated successfully, and increased version"
1174
1528
  );
1175
- if (autoConnect && current.getState().status === "disconnected") {
1529
+ if (autoConnect && current.getState().status === "disconnected" && jwtToken) {
1176
1530
  console.debug("[ReactorProvider] Starting autoconnect...");
1177
- current.getState().connect().then(() => {
1531
+ current.getState().connect(jwtToken).then(() => {
1178
1532
  console.debug("[ReactorProvider] Autoconnect successful");
1179
1533
  }).catch((error) => {
1180
1534
  console.error("[ReactorProvider] Failed to autoconnect:", error);
@@ -1190,16 +1544,7 @@ function ReactorProvider(_a) {
1190
1544
  console.error("[ReactorProvider] Failed to disconnect:", error);
1191
1545
  });
1192
1546
  };
1193
- }, [
1194
- coordinatorUrl,
1195
- modelName,
1196
- jwtToken,
1197
- insecureApiKey,
1198
- directConnection,
1199
- queueing,
1200
- autoConnect,
1201
- local
1202
- ]);
1547
+ }, [coordinatorUrl, modelName, autoConnect, local, jwtToken]);
1203
1548
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReactorContext.Provider, { value: storeRef.current, children });
1204
1549
  }
1205
1550
  function useReactorStore(selector) {
@@ -1261,7 +1606,11 @@ function ReactorView({
1261
1606
  if (videoRef.current && videoTrack) {
1262
1607
  console.debug("[ReactorView] Attaching video track to element");
1263
1608
  try {
1264
- videoTrack.attach(videoRef.current);
1609
+ const stream = new MediaStream([videoTrack]);
1610
+ videoRef.current.srcObject = stream;
1611
+ videoRef.current.play().catch((e) => {
1612
+ console.warn("[ReactorView] Auto-play failed:", e);
1613
+ });
1265
1614
  console.debug("[ReactorView] Video track attached successfully");
1266
1615
  } catch (error) {
1267
1616
  console.error("[ReactorView] Failed to attach video track:", error);
@@ -1269,12 +1618,8 @@ function ReactorView({
1269
1618
  return () => {
1270
1619
  console.debug("[ReactorView] Detaching video track from element");
1271
1620
  if (videoRef.current) {
1272
- try {
1273
- videoTrack.detach(videoRef.current);
1274
- console.debug("[ReactorView] Video track detached successfully");
1275
- } catch (error) {
1276
- console.error("[ReactorView] Failed to detach video track:", error);
1277
- }
1621
+ videoRef.current.srcObject = null;
1622
+ console.debug("[ReactorView] Video track detached successfully");
1278
1623
  }
1279
1624
  };
1280
1625
  } else {
@@ -1339,8 +1684,8 @@ function ReactorController({
1339
1684
  className,
1340
1685
  style
1341
1686
  }) {
1342
- const { sendMessage, status } = useReactor((state) => ({
1343
- sendMessage: state.sendMessage,
1687
+ const { sendCommand, status } = useReactor((state) => ({
1688
+ sendCommand: state.sendCommand,
1344
1689
  status: state.status
1345
1690
  }));
1346
1691
  const [commands, setCommands] = (0, import_react6.useState)({});
@@ -1355,12 +1700,9 @@ function ReactorController({
1355
1700
  }, [status]);
1356
1701
  const requestCapabilities = (0, import_react6.useCallback)(() => {
1357
1702
  if (status === "ready") {
1358
- sendMessage({
1359
- type: "requestCapabilities",
1360
- data: {}
1361
- });
1703
+ sendCommand("requestCapabilities", {});
1362
1704
  }
1363
- }, [status, sendMessage]);
1705
+ }, [status, sendCommand]);
1364
1706
  import_react6.default.useEffect(() => {
1365
1707
  if (status === "ready") {
1366
1708
  requestCapabilities();
@@ -1451,12 +1793,9 @@ function ReactorController({
1451
1793
  }
1452
1794
  });
1453
1795
  console.log(`Executing command: ${commandName}`, data);
1454
- yield sendMessage({
1455
- type: commandName,
1456
- data
1457
- });
1796
+ yield sendCommand(commandName, data);
1458
1797
  }),
1459
- [formValues, sendMessage, commands]
1798
+ [formValues, sendCommand, commands]
1460
1799
  );
1461
1800
  const renderInput = (commandName, paramName, paramSchema) => {
1462
1801
  var _a, _b;
@@ -1982,6 +2321,7 @@ function WebcamStream({
1982
2321
  }
1983
2322
  // Annotate the CommonJS export names for ESM import in node:
1984
2323
  0 && (module.exports = {
2324
+ PROD_COORDINATOR_URL,
1985
2325
  Reactor,
1986
2326
  ReactorController,
1987
2327
  ReactorProvider,