@reactor-team/js-sdk 1.0.1
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/.env.example +2 -0
- package/.prettierignore +7 -0
- package/.prettierrc +9 -0
- package/README.md +26 -0
- package/dist/index.d.mts +151 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.js +1000 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +971 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +45 -0
- package/publish_package.sh +45 -0
- package/src/core/CoordinatorClient.ts +160 -0
- package/src/core/GPUMachineClient.ts +172 -0
- package/src/core/Reactor.ts +407 -0
- package/src/core/store.ts +163 -0
- package/src/core/types.ts +99 -0
- package/src/index.ts +5 -0
- package/src/react/ReactorProvider.tsx +86 -0
- package/src/react/ReactorView.tsx +116 -0
- package/src/react/hooks.ts +50 -0
- package/src/types.ts +37 -0
- package/tsconfig.json +19 -0
- package/tsup.config.ts +15 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApplicationMessage,
|
|
3
|
+
InternalConnectionStatus,
|
|
4
|
+
GPUMachineAssignmentData,
|
|
5
|
+
} from "./types";
|
|
6
|
+
import type {
|
|
7
|
+
ReactorEvent,
|
|
8
|
+
ReactorStatus,
|
|
9
|
+
ReactorState,
|
|
10
|
+
ReactorError,
|
|
11
|
+
ReactorWaitingInfo,
|
|
12
|
+
} from "../types";
|
|
13
|
+
import { CoordinatorClient } from "./CoordinatorClient";
|
|
14
|
+
import { GPUMachineClient } from "./GPUMachineClient";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
const OptionsSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
directConnection: z
|
|
20
|
+
.object({
|
|
21
|
+
livekitJwtToken: z.string(),
|
|
22
|
+
livekitWsUrl: z.string(),
|
|
23
|
+
})
|
|
24
|
+
.optional(),
|
|
25
|
+
insecureApiKey: z.string().optional(),
|
|
26
|
+
jwtToken: z.string().optional(),
|
|
27
|
+
coordinatorUrl: z.string().optional(),
|
|
28
|
+
modelName: z.string(),
|
|
29
|
+
})
|
|
30
|
+
.refine(
|
|
31
|
+
(data) => data.directConnection || data.insecureApiKey || data.jwtToken,
|
|
32
|
+
{
|
|
33
|
+
message:
|
|
34
|
+
"At least one of directConnection, insecureApiKey, or jwtToken must be provided.",
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
export type Options = z.input<typeof OptionsSchema>;
|
|
38
|
+
|
|
39
|
+
type EventHandler = (...args: any[]) => void;
|
|
40
|
+
|
|
41
|
+
export class Reactor {
|
|
42
|
+
private coordinatorClient: CoordinatorClient | undefined; //client for the coordinator
|
|
43
|
+
private machineClient: GPUMachineClient | undefined; //client for the machine instance
|
|
44
|
+
private status: ReactorStatus = "disconnected";
|
|
45
|
+
private coordinatorUrl: string;
|
|
46
|
+
private lastError?: ReactorError;
|
|
47
|
+
private waitingInfo?: ReactorWaitingInfo;
|
|
48
|
+
private jwtToken?: string;
|
|
49
|
+
private insecureApiKey?: string;
|
|
50
|
+
private directConnection?: {
|
|
51
|
+
livekitJwtToken: string;
|
|
52
|
+
livekitWsUrl: string;
|
|
53
|
+
};
|
|
54
|
+
private modelName: string;
|
|
55
|
+
private modelVersion: string;
|
|
56
|
+
|
|
57
|
+
constructor(options: Options) {
|
|
58
|
+
const validatedOptions = OptionsSchema.parse(options);
|
|
59
|
+
this.coordinatorUrl =
|
|
60
|
+
validatedOptions.coordinatorUrl || "ws://localhost:8080/ws";
|
|
61
|
+
this.jwtToken = validatedOptions.jwtToken;
|
|
62
|
+
this.insecureApiKey = validatedOptions.insecureApiKey;
|
|
63
|
+
this.directConnection = validatedOptions.directConnection;
|
|
64
|
+
this.modelName = validatedOptions.modelName;
|
|
65
|
+
// TODO: Use the model version from the options once we have a way to handle multiple versions
|
|
66
|
+
this.modelVersion = "1.0.0";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Generic event map
|
|
70
|
+
private eventListeners: Map<ReactorEvent, Set<EventHandler>> = new Map();
|
|
71
|
+
|
|
72
|
+
// Event Emitter API
|
|
73
|
+
on(event: ReactorEvent, handler: EventHandler) {
|
|
74
|
+
if (!this.eventListeners.has(event)) {
|
|
75
|
+
this.eventListeners.set(event, new Set());
|
|
76
|
+
}
|
|
77
|
+
this.eventListeners.get(event)!.add(handler);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
off(event: ReactorEvent, handler: EventHandler) {
|
|
81
|
+
this.eventListeners.get(event)?.delete(handler);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
emit(event: ReactorEvent, ...args: any[]) {
|
|
85
|
+
this.eventListeners.get(event)?.forEach((handler) => handler(...args));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Public method to send a message to the machine.
|
|
90
|
+
* Automatically wraps the message in an application message.
|
|
91
|
+
* @param message The message to send to the machine.
|
|
92
|
+
* @throws Error if not in ready state
|
|
93
|
+
*/
|
|
94
|
+
sendMessage(message: any) {
|
|
95
|
+
// Synchronous validation - throw immediately
|
|
96
|
+
if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
|
|
97
|
+
const errorMessage = `Cannot send message, status is ${this.status}`;
|
|
98
|
+
console.error("[Reactor] Not ready, cannot send message");
|
|
99
|
+
throw new Error(errorMessage);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Automatically wrap the user-message in an application message.
|
|
104
|
+
const applicationMessage: ApplicationMessage = {
|
|
105
|
+
type: "application",
|
|
106
|
+
data: message,
|
|
107
|
+
};
|
|
108
|
+
this.machineClient?.sendMessage(applicationMessage);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// Async operational error - emit event only
|
|
111
|
+
console.error("[Reactor] Failed to send message:", error);
|
|
112
|
+
this.createError(
|
|
113
|
+
"MESSAGE_SEND_FAILED",
|
|
114
|
+
`Failed to send message: ${error}`,
|
|
115
|
+
"gpu",
|
|
116
|
+
true
|
|
117
|
+
);
|
|
118
|
+
// Don't re-throw - let the error event handle it
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Connects to the machine via LiveKit and waits for the gpu machine to be ready.
|
|
124
|
+
* Once the machine is ready, the Reactor will establish the LiveKit connection.
|
|
125
|
+
* @param livekitJwtToken The JWT token for LiveKit authentication
|
|
126
|
+
* @param livekitWsUrl The WebSocket URL for LiveKit connection
|
|
127
|
+
*/
|
|
128
|
+
private async connectToGPUMachine(
|
|
129
|
+
livekitJwtToken: string,
|
|
130
|
+
livekitWsUrl: string
|
|
131
|
+
) {
|
|
132
|
+
console.debug("[Reactor] Connecting to machine room...");
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
this.machineClient = new GPUMachineClient(livekitJwtToken, livekitWsUrl);
|
|
136
|
+
|
|
137
|
+
this.machineClient.on("application", (message: any) => {
|
|
138
|
+
// escalate the message event through the instance, so that the user can listen to it.
|
|
139
|
+
this.emit("newMessage", message);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
this.machineClient.on(
|
|
143
|
+
"statusChanged",
|
|
144
|
+
(status: InternalConnectionStatus) => {
|
|
145
|
+
switch (status) {
|
|
146
|
+
case "connected":
|
|
147
|
+
this.setStatus("ready");
|
|
148
|
+
break;
|
|
149
|
+
case "disconnected":
|
|
150
|
+
// gpu machine has disconnected. Close any other connection to any other entity
|
|
151
|
+
// such as the coordinator or the LiveKit connection.
|
|
152
|
+
this.disconnect();
|
|
153
|
+
break;
|
|
154
|
+
case "error":
|
|
155
|
+
// gpu machine has errored. Close any other connection to any other entity
|
|
156
|
+
// such as the coordinator or the LiveKit connection.
|
|
157
|
+
this.createError(
|
|
158
|
+
"GPU_CONNECTION_ERROR",
|
|
159
|
+
"GPU machine connection failed",
|
|
160
|
+
"gpu",
|
|
161
|
+
true
|
|
162
|
+
);
|
|
163
|
+
this.disconnect();
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
this.machineClient.on("fps", (fps: number) => {
|
|
170
|
+
this.emit("fps", fps);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
this.machineClient.on("streamChanged", (videoTrack) => {
|
|
174
|
+
this.emit("streamChanged", videoTrack);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
console.debug("[Reactor] About to connect to machine");
|
|
178
|
+
await this.machineClient.connect();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
// For private methods, we can still throw since the caller will handle it appropriately
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Connects to the coordinator and waits for a GPU to be assigned.
|
|
187
|
+
* Once a GPU is assigned, the Reactor will connect to the gpu machine via LiveKit.
|
|
188
|
+
*/
|
|
189
|
+
async connect(): Promise<void> {
|
|
190
|
+
console.debug("[Reactor] Connecting, status:", this.status);
|
|
191
|
+
|
|
192
|
+
// Synchronous validation - throw immediately
|
|
193
|
+
if (this.status !== "disconnected")
|
|
194
|
+
throw new Error("Already connected or connecting");
|
|
195
|
+
|
|
196
|
+
if (this.directConnection) {
|
|
197
|
+
return this.connectToGPUMachine(
|
|
198
|
+
this.directConnection.livekitJwtToken,
|
|
199
|
+
this.directConnection.livekitWsUrl
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.setStatus("connecting");
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
console.debug(
|
|
207
|
+
"[Reactor] Connecting to coordinator with authenticated URL"
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
this.coordinatorClient = new CoordinatorClient({
|
|
211
|
+
wsUrl: this.coordinatorUrl,
|
|
212
|
+
jwtToken: this.jwtToken,
|
|
213
|
+
insecureApiKey: this.insecureApiKey,
|
|
214
|
+
modelName: this.modelName,
|
|
215
|
+
modelVersion: this.modelVersion,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
this.coordinatorClient.on(
|
|
219
|
+
"gpu-machine-assigned",
|
|
220
|
+
async (assignmentData: GPUMachineAssignmentData) => {
|
|
221
|
+
console.debug(
|
|
222
|
+
"[Reactor] GPU machine assigned by coordinator:",
|
|
223
|
+
assignmentData
|
|
224
|
+
);
|
|
225
|
+
try {
|
|
226
|
+
await this.connectToGPUMachine(
|
|
227
|
+
assignmentData.livekitJwtToken,
|
|
228
|
+
assignmentData.livekitWsUrl
|
|
229
|
+
);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error("[Reactor] Failed to connect to GPU machine:", error);
|
|
232
|
+
// Emit error event for async GPU connection failure
|
|
233
|
+
this.createError(
|
|
234
|
+
"GPU_CONNECTION_FAILED",
|
|
235
|
+
`Failed to connect to GPU machine: ${error}`,
|
|
236
|
+
"gpu",
|
|
237
|
+
true
|
|
238
|
+
);
|
|
239
|
+
this.disconnect();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
this.coordinatorClient.on(
|
|
245
|
+
"waiting-info",
|
|
246
|
+
(waitingData: ReactorWaitingInfo) => {
|
|
247
|
+
console.debug("[Reactor] Waiting info update received:", waitingData);
|
|
248
|
+
// Update waiting info
|
|
249
|
+
this.setWaitingInfo({
|
|
250
|
+
...this.waitingInfo,
|
|
251
|
+
...waitingData,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
this.coordinatorClient.on(
|
|
257
|
+
"statusChanged",
|
|
258
|
+
(newStatus: InternalConnectionStatus) => {
|
|
259
|
+
switch (newStatus) {
|
|
260
|
+
case "connected":
|
|
261
|
+
this.setStatus("waiting");
|
|
262
|
+
// Initialize waiting info when entering waiting state
|
|
263
|
+
this.setWaitingInfo({
|
|
264
|
+
position: undefined,
|
|
265
|
+
estimatedWaitTime: undefined,
|
|
266
|
+
averageWaitTime: undefined,
|
|
267
|
+
});
|
|
268
|
+
break;
|
|
269
|
+
case "disconnected":
|
|
270
|
+
this.disconnect();
|
|
271
|
+
break;
|
|
272
|
+
case "error":
|
|
273
|
+
this.createError(
|
|
274
|
+
"COORDINATOR_CONNECTION_ERROR",
|
|
275
|
+
"Coordinator connection failed",
|
|
276
|
+
"coordinator",
|
|
277
|
+
true
|
|
278
|
+
);
|
|
279
|
+
this.disconnect();
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
await this.coordinatorClient.connect();
|
|
286
|
+
} catch (error) {
|
|
287
|
+
console.error("[Reactor] Authentication failed:", error);
|
|
288
|
+
this.createError(
|
|
289
|
+
"AUTHENTICATION_FAILED",
|
|
290
|
+
`Authentication failed: ${error}`,
|
|
291
|
+
"coordinator",
|
|
292
|
+
true
|
|
293
|
+
);
|
|
294
|
+
this.setStatus("disconnected");
|
|
295
|
+
// Keep throwing for authentication failures as these are immediate/sync failures
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Disconnects from the coordinator and the gpu machine.
|
|
302
|
+
* Ensures cleanup completes even if individual disconnections fail.
|
|
303
|
+
*/
|
|
304
|
+
async disconnect() {
|
|
305
|
+
if (this.status === "disconnected") return;
|
|
306
|
+
|
|
307
|
+
// Disconnect coordinator client with error handling
|
|
308
|
+
if (this.coordinatorClient) {
|
|
309
|
+
try {
|
|
310
|
+
this.coordinatorClient.disconnect();
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error("[Reactor] Error disconnecting from coordinator:", error);
|
|
313
|
+
// Continue with cleanup even if coordinator disconnect fails
|
|
314
|
+
}
|
|
315
|
+
this.coordinatorClient = undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Disconnect machine client with error handling
|
|
319
|
+
if (this.machineClient) {
|
|
320
|
+
try {
|
|
321
|
+
await this.machineClient.disconnect();
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error("[Reactor] Error disconnecting from GPU machine:", error);
|
|
324
|
+
// Continue with cleanup even if machine disconnect fails
|
|
325
|
+
}
|
|
326
|
+
this.machineClient = undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this.setStatus("disconnected");
|
|
330
|
+
|
|
331
|
+
// Clear waiting info when disconnecting
|
|
332
|
+
this.waitingInfo = undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private setStatus(newStatus: ReactorStatus) {
|
|
336
|
+
console.debug("[Reactor] Setting status:", newStatus, "from", this.status);
|
|
337
|
+
if (this.status !== newStatus) {
|
|
338
|
+
this.status = newStatus;
|
|
339
|
+
this.emit("statusChanged", newStatus);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private setWaitingInfo(newWaitingInfo: ReactorWaitingInfo) {
|
|
344
|
+
console.debug(
|
|
345
|
+
"[Reactor] Setting waiting info:",
|
|
346
|
+
newWaitingInfo,
|
|
347
|
+
"from",
|
|
348
|
+
this.waitingInfo
|
|
349
|
+
);
|
|
350
|
+
if (this.waitingInfo !== newWaitingInfo) {
|
|
351
|
+
this.waitingInfo = newWaitingInfo;
|
|
352
|
+
this.emit("waitingInfoChanged", newWaitingInfo);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
getStatus(): ReactorStatus {
|
|
357
|
+
return this.status;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Get the current state including status, error, and waiting info
|
|
362
|
+
*/
|
|
363
|
+
getState(): ReactorState {
|
|
364
|
+
return {
|
|
365
|
+
status: this.status,
|
|
366
|
+
waitingInfo: this.waitingInfo,
|
|
367
|
+
lastError: this.lastError,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get waiting information when status is 'waiting'
|
|
373
|
+
*/
|
|
374
|
+
getWaitingInfo(): ReactorWaitingInfo | undefined {
|
|
375
|
+
return this.waitingInfo;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Get the last error that occurred
|
|
380
|
+
*/
|
|
381
|
+
getLastError(): ReactorError | undefined {
|
|
382
|
+
return this.lastError;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Create and store an error
|
|
387
|
+
*/
|
|
388
|
+
private createError(
|
|
389
|
+
code: string,
|
|
390
|
+
message: string,
|
|
391
|
+
component: "coordinator" | "gpu" | "livekit",
|
|
392
|
+
recoverable: boolean,
|
|
393
|
+
retryAfter?: number
|
|
394
|
+
) {
|
|
395
|
+
this.lastError = {
|
|
396
|
+
code,
|
|
397
|
+
message,
|
|
398
|
+
timestamp: Date.now(),
|
|
399
|
+
recoverable,
|
|
400
|
+
component,
|
|
401
|
+
retryAfter,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Emit error event
|
|
405
|
+
this.emit("error", this.lastError);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { StoreApi } from "zustand";
|
|
2
|
+
import type { ReactorStatus, ReactorWaitingInfo, ReactorError } from "../types";
|
|
3
|
+
import { Reactor, type Options as ReactorOptions } from "./Reactor";
|
|
4
|
+
import { create } from "zustand/react";
|
|
5
|
+
import { createContext } from "react";
|
|
6
|
+
import type { RemoteVideoTrack } from "livekit-client";
|
|
7
|
+
|
|
8
|
+
export type ReactorStoreApi = ReturnType<typeof createReactorStore>;
|
|
9
|
+
|
|
10
|
+
export interface ReactorState {
|
|
11
|
+
status: ReactorStatus;
|
|
12
|
+
videoTrack: RemoteVideoTrack | null;
|
|
13
|
+
//These are the machine FPS. The machine internally reports to the client the rate at which the frames are
|
|
14
|
+
//being generated. This can be useful for the client to calcolate at which rate to run commands.
|
|
15
|
+
fps?: number;
|
|
16
|
+
waitingInfo?: ReactorWaitingInfo;
|
|
17
|
+
lastError?: ReactorError;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ReactorActions {
|
|
21
|
+
sendMessage(message: any): void;
|
|
22
|
+
connect(): Promise<void>;
|
|
23
|
+
disconnect(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Internal state not exposed to components
|
|
27
|
+
interface ReactorInternalState {
|
|
28
|
+
reactor: Reactor;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const ReactorContext = createContext<ReactorStoreApi | undefined>(
|
|
32
|
+
undefined
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export type ReactorStore = ReactorState &
|
|
36
|
+
ReactorActions & {
|
|
37
|
+
internal: ReactorInternalState;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const defaultInitState: ReactorState = {
|
|
41
|
+
status: "disconnected",
|
|
42
|
+
videoTrack: null,
|
|
43
|
+
fps: undefined,
|
|
44
|
+
waitingInfo: undefined,
|
|
45
|
+
lastError: undefined,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface ReactorInitializationProps extends ReactorOptions {}
|
|
49
|
+
|
|
50
|
+
export const initReactorStore = (
|
|
51
|
+
props: ReactorInitializationProps
|
|
52
|
+
): ReactorState & ReactorInitializationProps => {
|
|
53
|
+
return {
|
|
54
|
+
...defaultInitState,
|
|
55
|
+
// These are only used for dev initialization, not exposed in the store
|
|
56
|
+
...props,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const createReactorStore = (
|
|
61
|
+
initProps: ReactorInitializationProps,
|
|
62
|
+
publicState: ReactorState = defaultInitState
|
|
63
|
+
): StoreApi<ReactorStore> => {
|
|
64
|
+
console.debug("[ReactorStore] Creating store", {
|
|
65
|
+
coordinatorUrl: initProps.coordinatorUrl,
|
|
66
|
+
initialState: publicState,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return create<ReactorStore>()((set, get) => {
|
|
70
|
+
const reactor = new Reactor(initProps);
|
|
71
|
+
|
|
72
|
+
console.debug("[ReactorStore] Setting up event listeners");
|
|
73
|
+
|
|
74
|
+
reactor.on("statusChanged", (newStatus: ReactorStatus) => {
|
|
75
|
+
console.debug("[ReactorStore] Status changed", {
|
|
76
|
+
oldStatus: get().status,
|
|
77
|
+
newStatus,
|
|
78
|
+
});
|
|
79
|
+
set({ status: newStatus });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
reactor.on("waitingInfoChanged", (newWaitingInfo: ReactorWaitingInfo) => {
|
|
83
|
+
console.debug("[ReactorStore] Waiting info changed", {
|
|
84
|
+
oldWaitingInfo: get().waitingInfo,
|
|
85
|
+
newWaitingInfo,
|
|
86
|
+
});
|
|
87
|
+
set({ waitingInfo: newWaitingInfo });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
reactor.on("streamChanged", (videoTrack: RemoteVideoTrack | null) => {
|
|
91
|
+
console.debug("[ReactorStore] Stream changed", {
|
|
92
|
+
hasVideoTrack: !!videoTrack,
|
|
93
|
+
videoTrackKind: videoTrack?.kind,
|
|
94
|
+
videoTrackSid: videoTrack?.sid,
|
|
95
|
+
});
|
|
96
|
+
set({ videoTrack: videoTrack });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
reactor.on("fps", (fps: number) => {
|
|
100
|
+
console.debug("[ReactorStore] FPS updated", { fps });
|
|
101
|
+
set({ fps: fps });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
reactor.on("error", (error: ReactorError) => {
|
|
105
|
+
console.debug("[ReactorStore] Error occurred", error);
|
|
106
|
+
set({ lastError: error });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
...publicState,
|
|
111
|
+
internal: { reactor },
|
|
112
|
+
|
|
113
|
+
// actions
|
|
114
|
+
onMessage: (handler: (message: any) => void) => {
|
|
115
|
+
console.debug("[ReactorStore] Registering message handler");
|
|
116
|
+
|
|
117
|
+
// Simply register the handler
|
|
118
|
+
get().internal.reactor.on("newMessage", handler);
|
|
119
|
+
|
|
120
|
+
// Return a cleanup function that can be called to unregister
|
|
121
|
+
return () => {
|
|
122
|
+
console.debug("[ReactorStore] Cleaning up message handler");
|
|
123
|
+
get().internal.reactor.off("newMessage", handler);
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
sendMessage: (mess: any) => {
|
|
127
|
+
console.debug("[ReactorStore] Sending message", { message: mess });
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
get().internal.reactor.sendMessage(mess);
|
|
131
|
+
console.debug("[ReactorStore] Message sent successfully");
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error("[ReactorStore] Failed to send message:", error);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
connect: async () => {
|
|
138
|
+
console.debug("[ReactorStore] Connect called");
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await get().internal.reactor.connect();
|
|
142
|
+
console.debug("[ReactorStore] Connect completed successfully");
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error("[ReactorStore] Connect failed:", error);
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
disconnect: async () => {
|
|
149
|
+
console.debug("[ReactorStore] Disconnect called", {
|
|
150
|
+
currentStatus: get().status,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await get().internal.reactor.disconnect();
|
|
155
|
+
console.debug("[ReactorStore] Disconnect completed successfully");
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error("[ReactorStore] Disconnect failed:", error);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal types for the Reactor SDK.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
// Base message schemas corresponding to Python Pydantic models
|
|
8
|
+
|
|
9
|
+
export const ApplicationMessageSchema = z.object({
|
|
10
|
+
type: z.literal("application"),
|
|
11
|
+
data: z.any(), // Can be any JSON-serializable data
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Internal message that notifies the client of the current push rate of the machine
|
|
15
|
+
export const FPSMessageSchema = z.object({
|
|
16
|
+
type: z.literal("fps"),
|
|
17
|
+
data: z.number(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const GPUMachineReceiveMessageSchema = z.discriminatedUnion("type", [
|
|
21
|
+
ApplicationMessageSchema,
|
|
22
|
+
FPSMessageSchema,
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export const WelcomeMessageSchema = z.object({
|
|
26
|
+
type: z.literal("welcome"),
|
|
27
|
+
data: z.record(z.string(), z.any()),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const GPUMachineAssignmentDataSchema = z.object({
|
|
31
|
+
livekitWsUrl: z.string(),
|
|
32
|
+
livekitJwtToken: z.string(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const GPUMachineAssignmentMessageSchema = z.object({
|
|
36
|
+
type: z.literal("gpu-machine-assigned"),
|
|
37
|
+
data: GPUMachineAssignmentDataSchema,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const EchoMessageSchema = z.object({
|
|
41
|
+
type: z.literal("echo"),
|
|
42
|
+
data: z.record(z.string(), z.any()),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const WaitingInfoDataSchema = z.object({
|
|
46
|
+
position: z.number().optional(),
|
|
47
|
+
estimatedWaitTime: z.number().optional(),
|
|
48
|
+
averageWaitTime: z.number().optional(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const WaitingInfoMessageSchema = z.object({
|
|
52
|
+
type: z.literal("waiting-info"),
|
|
53
|
+
data: WaitingInfoDataSchema,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const CoordinatorMessageSchema = z.discriminatedUnion("type", [
|
|
57
|
+
WelcomeMessageSchema,
|
|
58
|
+
GPUMachineAssignmentMessageSchema,
|
|
59
|
+
EchoMessageSchema,
|
|
60
|
+
WaitingInfoMessageSchema,
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
export const GPUMachineSendMessageSchema = z.discriminatedUnion("type", [
|
|
64
|
+
ApplicationMessageSchema,
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
export const SessionSetupMessageSchema = z.object({
|
|
68
|
+
type: z.literal("sessionSetup"),
|
|
69
|
+
data: z.object({
|
|
70
|
+
modelName: z.string(),
|
|
71
|
+
modelVersion: z.string().default("latest"),
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// TypeScript types derived from schemas
|
|
76
|
+
export type ApplicationMessage = z.infer<typeof ApplicationMessageSchema>;
|
|
77
|
+
export type GPUMachineReceiveMessageSchema = z.infer<
|
|
78
|
+
typeof GPUMachineReceiveMessageSchema
|
|
79
|
+
>;
|
|
80
|
+
export type WelcomeMessage = z.infer<typeof WelcomeMessageSchema>;
|
|
81
|
+
export type GPUMachineAssignmentData = z.infer<
|
|
82
|
+
typeof GPUMachineAssignmentDataSchema
|
|
83
|
+
>;
|
|
84
|
+
export type GPUMachineAssignmentMessage = z.infer<
|
|
85
|
+
typeof GPUMachineAssignmentMessageSchema
|
|
86
|
+
>;
|
|
87
|
+
export type EchoMessage = z.infer<typeof EchoMessageSchema>;
|
|
88
|
+
export type WaitingInfoData = z.infer<typeof WaitingInfoDataSchema>;
|
|
89
|
+
export type WaitingInfoMessage = z.infer<typeof WaitingInfoMessageSchema>;
|
|
90
|
+
export type CoordinatorMessage = z.infer<typeof CoordinatorMessageSchema>;
|
|
91
|
+
export type GPUMachineSendMessage = z.infer<typeof GPUMachineSendMessageSchema>;
|
|
92
|
+
export type SessionSetupMessage = z.infer<typeof SessionSetupMessageSchema>;
|
|
93
|
+
|
|
94
|
+
// Internal connection status for individual components
|
|
95
|
+
export type InternalConnectionStatus =
|
|
96
|
+
| "disconnected"
|
|
97
|
+
| "connecting"
|
|
98
|
+
| "connected"
|
|
99
|
+
| "error";
|