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