@reactor-team/js-sdk 1.0.6 → 1.0.7

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.
@@ -1,172 +0,0 @@
1
- /**
2
- * The GPUMachineClient is responsible for handling the direct connection to the machine instance
3
- * after the coordinator has assigned a machine.
4
- */
5
-
6
- import {
7
- type GPUMachineSendMessage,
8
- GPUMachineReceiveMessageSchema,
9
- } from "./types";
10
- import { Room, RoomEvent, Track, RemoteVideoTrack } from "livekit-client";
11
-
12
- type EventHandler = (...args: any[]) => void;
13
- export type GPUMachineEvent =
14
- | GPUMachineReceiveMessageSchema["type"]
15
- | "statusChanged"
16
- | "streamChanged";
17
-
18
- export class GPUMachineClient {
19
- private eventListeners: Map<GPUMachineEvent, Set<EventHandler>> = new Map();
20
- private roomInstance: Room | undefined;
21
- private machineFPS: number | undefined;
22
- private token: string;
23
- private videoStream: MediaStream | undefined;
24
- private liveKitUrl: string;
25
-
26
- constructor(token: string, liveKitUrl: string) {
27
- this.token = token;
28
- this.liveKitUrl = liveKitUrl;
29
- }
30
-
31
- // Event Emitter API
32
- on(event: GPUMachineEvent, handler: EventHandler) {
33
- if (!this.eventListeners.has(event)) {
34
- this.eventListeners.set(event, new Set());
35
- }
36
- this.eventListeners.get(event)!.add(handler);
37
- }
38
-
39
- off(event: GPUMachineEvent, handler: EventHandler) {
40
- this.eventListeners.get(event)?.delete(handler);
41
- }
42
-
43
- emit(event: GPUMachineEvent, ...args: any[]) {
44
- this.eventListeners.get(event)?.forEach((handler) => handler(...args));
45
- }
46
-
47
- sendMessage(message: GPUMachineSendMessage) {
48
- try {
49
- if (this.roomInstance) {
50
- const messageStr = JSON.stringify(message);
51
- // Send message via LiveKit text channel
52
- this.roomInstance.localParticipant.sendText(messageStr, {
53
- topic: "application",
54
- });
55
- } else {
56
- console.warn(
57
- "[GPUMachineClient] Cannot send message - not connected to room"
58
- );
59
- }
60
- } catch (error) {
61
- console.error("[GPUMachineClient] Failed to send message:", error);
62
- this.emit("statusChanged", "error");
63
- }
64
- }
65
-
66
- async connect(): Promise<void> {
67
- this.roomInstance = new Room({
68
- adaptiveStream: true,
69
- dynacast: true,
70
- });
71
-
72
- this.roomInstance.on(RoomEvent.Connected, () => {
73
- console.debug("[GPUMachineClient] Connected to room");
74
- this.emit("statusChanged", "connected");
75
- });
76
-
77
- this.roomInstance.on(RoomEvent.Disconnected, () => {
78
- console.debug("[GPUMachineClient] Disconnected from room");
79
- this.emit("statusChanged", "disconnected");
80
- });
81
-
82
- // Handle video track subscriptions from GPU machine
83
- this.roomInstance.on(
84
- RoomEvent.TrackSubscribed,
85
- (track, _publication, participant) => {
86
- console.debug(
87
- "[GPUMachineClient] Track subscribed:",
88
- track.kind,
89
- participant.identity
90
- );
91
-
92
- if (track.kind === Track.Kind.Video) {
93
- const videoTrack = track as RemoteVideoTrack;
94
- this.emit("streamChanged", videoTrack);
95
- }
96
- }
97
- );
98
-
99
- this.roomInstance.on(
100
- RoomEvent.TrackUnsubscribed,
101
- (track, _publication, participant) => {
102
- console.debug(
103
- "[GPUMachineClient] Track unsubscribed:",
104
- track.kind,
105
- participant.identity
106
- );
107
-
108
- if (track.kind === Track.Kind.Video) {
109
- this.emit("streamChanged", null);
110
- }
111
- }
112
- );
113
-
114
- this.roomInstance.registerTextStreamHandler(
115
- "application",
116
- async (reader, participant) => {
117
- const text = await reader.readAll();
118
- console.log("[GPUMachineClient] Received message:", text);
119
- try {
120
- const parsedData = JSON.parse(text);
121
-
122
- const validatedMessage =
123
- GPUMachineReceiveMessageSchema.parse(parsedData);
124
- if (validatedMessage.type === "fps") {
125
- this.machineFPS = validatedMessage.data;
126
- }
127
-
128
- this.emit(validatedMessage.type, validatedMessage.data);
129
- } catch (error) {
130
- console.error(
131
- "[GPUMachineClient] Failed to parse/validate message:",
132
- error
133
- );
134
- this.emit("statusChanged", "error");
135
- }
136
- }
137
- );
138
-
139
- await this.roomInstance.connect(this.liveKitUrl, this.token);
140
-
141
- console.log("[GPUMachineClient] Room connected");
142
- }
143
-
144
- /**
145
- * Closes the LiveKit connection if it exists.
146
- * This will trigger the onclose event handler.
147
- */
148
- async disconnect() {
149
- if (this.roomInstance) {
150
- console.debug("[GPUMachineClient] Closing LiveKit connection");
151
- await this.roomInstance.disconnect();
152
- this.roomInstance = undefined;
153
- }
154
- this.machineFPS = undefined;
155
- }
156
-
157
- /**
158
- * Returns the current fps rate of the machine.
159
- * @returns The current fps rate of the machine.
160
- */
161
- getFPS(): number | undefined {
162
- return this.machineFPS;
163
- }
164
-
165
- /**
166
- * Returns the current video stream from the GPU machine.
167
- * @returns The current video stream or undefined if not available.
168
- */
169
- getVideoStream(): MediaStream | undefined {
170
- return this.videoStream;
171
- }
172
- }
@@ -1,406 +0,0 @@
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().default("wss://api.reactor.inc/ws"),
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 = validatedOptions.coordinatorUrl;
60
- this.jwtToken = validatedOptions.jwtToken;
61
- this.insecureApiKey = validatedOptions.insecureApiKey;
62
- this.directConnection = validatedOptions.directConnection;
63
- this.modelName = validatedOptions.modelName;
64
- // TODO: Use the model version from the options once we have a way to handle multiple versions
65
- this.modelVersion = "1.0.0";
66
- }
67
-
68
- // Generic event map
69
- private eventListeners: Map<ReactorEvent, Set<EventHandler>> = new Map();
70
-
71
- // Event Emitter API
72
- on(event: ReactorEvent, handler: EventHandler) {
73
- if (!this.eventListeners.has(event)) {
74
- this.eventListeners.set(event, new Set());
75
- }
76
- this.eventListeners.get(event)!.add(handler);
77
- }
78
-
79
- off(event: ReactorEvent, handler: EventHandler) {
80
- this.eventListeners.get(event)?.delete(handler);
81
- }
82
-
83
- emit(event: ReactorEvent, ...args: any[]) {
84
- this.eventListeners.get(event)?.forEach((handler) => handler(...args));
85
- }
86
-
87
- /**
88
- * Public method to send a message to the machine.
89
- * Automatically wraps the message in an application message.
90
- * @param message The message to send to the machine.
91
- * @throws Error if not in ready state
92
- */
93
- sendMessage(message: any) {
94
- // Synchronous validation - throw immediately
95
- if (process.env.NODE_ENV !== "development" && this.status !== "ready") {
96
- const errorMessage = `Cannot send message, status is ${this.status}`;
97
- console.error("[Reactor] Not ready, cannot send message");
98
- throw new Error(errorMessage);
99
- }
100
-
101
- try {
102
- // Automatically wrap the user-message in an application message.
103
- const applicationMessage: ApplicationMessage = {
104
- type: "application",
105
- data: message,
106
- };
107
- this.machineClient?.sendMessage(applicationMessage);
108
- } catch (error) {
109
- // Async operational error - emit event only
110
- console.error("[Reactor] Failed to send message:", error);
111
- this.createError(
112
- "MESSAGE_SEND_FAILED",
113
- `Failed to send message: ${error}`,
114
- "gpu",
115
- true
116
- );
117
- // Don't re-throw - let the error event handle it
118
- }
119
- }
120
-
121
- /**
122
- * Connects to the machine via LiveKit and waits for the gpu machine to be ready.
123
- * Once the machine is ready, the Reactor will establish the LiveKit connection.
124
- * @param livekitJwtToken The JWT token for LiveKit authentication
125
- * @param livekitWsUrl The WebSocket URL for LiveKit connection
126
- */
127
- private async connectToGPUMachine(
128
- livekitJwtToken: string,
129
- livekitWsUrl: string
130
- ) {
131
- console.debug("[Reactor] Connecting to machine room...");
132
-
133
- try {
134
- this.machineClient = new GPUMachineClient(livekitJwtToken, livekitWsUrl);
135
-
136
- this.machineClient.on("application", (message: any) => {
137
- // escalate the message event through the instance, so that the user can listen to it.
138
- this.emit("newMessage", message);
139
- });
140
-
141
- this.machineClient.on(
142
- "statusChanged",
143
- (status: InternalConnectionStatus) => {
144
- switch (status) {
145
- case "connected":
146
- this.setStatus("ready");
147
- break;
148
- case "disconnected":
149
- // gpu machine has disconnected. Close any other connection to any other entity
150
- // such as the coordinator or the LiveKit connection.
151
- this.disconnect();
152
- break;
153
- case "error":
154
- // gpu machine has errored. Close any other connection to any other entity
155
- // such as the coordinator or the LiveKit connection.
156
- this.createError(
157
- "GPU_CONNECTION_ERROR",
158
- "GPU machine connection failed",
159
- "gpu",
160
- true
161
- );
162
- this.disconnect();
163
- break;
164
- }
165
- }
166
- );
167
-
168
- this.machineClient.on("fps", (fps: number) => {
169
- this.emit("fps", fps);
170
- });
171
-
172
- this.machineClient.on("streamChanged", (videoTrack) => {
173
- this.emit("streamChanged", videoTrack);
174
- });
175
-
176
- console.debug("[Reactor] About to connect to machine");
177
- await this.machineClient.connect();
178
- } catch (error) {
179
- // For private methods, we can still throw since the caller will handle it appropriately
180
- throw error;
181
- }
182
- }
183
-
184
- /**
185
- * Connects to the coordinator and waits for a GPU to be assigned.
186
- * Once a GPU is assigned, the Reactor will connect to the gpu machine via LiveKit.
187
- */
188
- async connect(): Promise<void> {
189
- console.debug("[Reactor] Connecting, status:", this.status);
190
-
191
- // Synchronous validation - throw immediately
192
- if (this.status !== "disconnected")
193
- throw new Error("Already connected or connecting");
194
-
195
- if (this.directConnection) {
196
- return this.connectToGPUMachine(
197
- this.directConnection.livekitJwtToken,
198
- this.directConnection.livekitWsUrl
199
- );
200
- }
201
-
202
- this.setStatus("connecting");
203
-
204
- try {
205
- console.debug(
206
- "[Reactor] Connecting to coordinator with authenticated URL"
207
- );
208
-
209
- this.coordinatorClient = new CoordinatorClient({
210
- wsUrl: this.coordinatorUrl,
211
- jwtToken: this.jwtToken,
212
- insecureApiKey: this.insecureApiKey,
213
- modelName: this.modelName,
214
- modelVersion: this.modelVersion,
215
- });
216
-
217
- this.coordinatorClient.on(
218
- "gpu-machine-assigned",
219
- async (assignmentData: GPUMachineAssignmentData) => {
220
- console.debug(
221
- "[Reactor] GPU machine assigned by coordinator:",
222
- assignmentData
223
- );
224
- try {
225
- await this.connectToGPUMachine(
226
- assignmentData.livekitJwtToken,
227
- assignmentData.livekitWsUrl
228
- );
229
- } catch (error) {
230
- console.error("[Reactor] Failed to connect to GPU machine:", error);
231
- // Emit error event for async GPU connection failure
232
- this.createError(
233
- "GPU_CONNECTION_FAILED",
234
- `Failed to connect to GPU machine: ${error}`,
235
- "gpu",
236
- true
237
- );
238
- this.disconnect();
239
- }
240
- }
241
- );
242
-
243
- this.coordinatorClient.on(
244
- "waiting-info",
245
- (waitingData: ReactorWaitingInfo) => {
246
- console.debug("[Reactor] Waiting info update received:", waitingData);
247
- // Update waiting info
248
- this.setWaitingInfo({
249
- ...this.waitingInfo,
250
- ...waitingData,
251
- });
252
- }
253
- );
254
-
255
- this.coordinatorClient.on(
256
- "statusChanged",
257
- (newStatus: InternalConnectionStatus) => {
258
- switch (newStatus) {
259
- case "connected":
260
- this.setStatus("waiting");
261
- // Initialize waiting info when entering waiting state
262
- this.setWaitingInfo({
263
- position: undefined,
264
- estimatedWaitTime: undefined,
265
- averageWaitTime: undefined,
266
- });
267
- break;
268
- case "disconnected":
269
- this.disconnect();
270
- break;
271
- case "error":
272
- this.createError(
273
- "COORDINATOR_CONNECTION_ERROR",
274
- "Coordinator connection failed",
275
- "coordinator",
276
- true
277
- );
278
- this.disconnect();
279
- break;
280
- }
281
- }
282
- );
283
-
284
- await this.coordinatorClient.connect();
285
- } catch (error) {
286
- console.error("[Reactor] Authentication failed:", error);
287
- this.createError(
288
- "AUTHENTICATION_FAILED",
289
- `Authentication failed: ${error}`,
290
- "coordinator",
291
- true
292
- );
293
- this.setStatus("disconnected");
294
- // Keep throwing for authentication failures as these are immediate/sync failures
295
- throw error;
296
- }
297
- }
298
-
299
- /**
300
- * Disconnects from the coordinator and the gpu machine.
301
- * Ensures cleanup completes even if individual disconnections fail.
302
- */
303
- async disconnect() {
304
- if (this.status === "disconnected") return;
305
-
306
- // Disconnect coordinator client with error handling
307
- if (this.coordinatorClient) {
308
- try {
309
- this.coordinatorClient.disconnect();
310
- } catch (error) {
311
- console.error("[Reactor] Error disconnecting from coordinator:", error);
312
- // Continue with cleanup even if coordinator disconnect fails
313
- }
314
- this.coordinatorClient = undefined;
315
- }
316
-
317
- // Disconnect machine client with error handling
318
- if (this.machineClient) {
319
- try {
320
- await this.machineClient.disconnect();
321
- } catch (error) {
322
- console.error("[Reactor] Error disconnecting from GPU machine:", error);
323
- // Continue with cleanup even if machine disconnect fails
324
- }
325
- this.machineClient = undefined;
326
- }
327
-
328
- this.setStatus("disconnected");
329
-
330
- // Clear waiting info when disconnecting
331
- this.waitingInfo = undefined;
332
- }
333
-
334
- private setStatus(newStatus: ReactorStatus) {
335
- console.debug("[Reactor] Setting status:", newStatus, "from", this.status);
336
- if (this.status !== newStatus) {
337
- this.status = newStatus;
338
- this.emit("statusChanged", newStatus);
339
- }
340
- }
341
-
342
- private setWaitingInfo(newWaitingInfo: ReactorWaitingInfo) {
343
- console.debug(
344
- "[Reactor] Setting waiting info:",
345
- newWaitingInfo,
346
- "from",
347
- this.waitingInfo
348
- );
349
- if (this.waitingInfo !== newWaitingInfo) {
350
- this.waitingInfo = newWaitingInfo;
351
- this.emit("waitingInfoChanged", newWaitingInfo);
352
- }
353
- }
354
-
355
- getStatus(): ReactorStatus {
356
- return this.status;
357
- }
358
-
359
- /**
360
- * Get the current state including status, error, and waiting info
361
- */
362
- getState(): ReactorState {
363
- return {
364
- status: this.status,
365
- waitingInfo: this.waitingInfo,
366
- lastError: this.lastError,
367
- };
368
- }
369
-
370
- /**
371
- * Get waiting information when status is 'waiting'
372
- */
373
- getWaitingInfo(): ReactorWaitingInfo | undefined {
374
- return this.waitingInfo;
375
- }
376
-
377
- /**
378
- * Get the last error that occurred
379
- */
380
- getLastError(): ReactorError | undefined {
381
- return this.lastError;
382
- }
383
-
384
- /**
385
- * Create and store an error
386
- */
387
- private createError(
388
- code: string,
389
- message: string,
390
- component: "coordinator" | "gpu" | "livekit",
391
- recoverable: boolean,
392
- retryAfter?: number
393
- ) {
394
- this.lastError = {
395
- code,
396
- message,
397
- timestamp: Date.now(),
398
- recoverable,
399
- component,
400
- retryAfter,
401
- };
402
-
403
- // Emit error event
404
- this.emit("error", this.lastError);
405
- }
406
- }