@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.
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import { ReactNode, useContext, useEffect, useRef } from "react";
4
+ import {
5
+ createReactorStore,
6
+ initReactorStore,
7
+ ReactorContext,
8
+ ReactorStore,
9
+ ReactorStoreApi,
10
+ type ReactorInitializationProps,
11
+ } from "../core/store";
12
+ import { useStore } from "zustand";
13
+
14
+ // Provider props
15
+ interface ReactorProviderProps extends ReactorInitializationProps {
16
+ autoConnect?: boolean;
17
+ children: ReactNode;
18
+ }
19
+
20
+ // tsx component
21
+ export function ReactorProvider({
22
+ children,
23
+ autoConnect = true,
24
+ ...props
25
+ }: ReactorProviderProps) {
26
+ // Stable Reactor instance
27
+ const storeRef = useRef<ReactorStoreApi | undefined>(undefined);
28
+ const firstRender = useRef(true);
29
+
30
+ /**
31
+ * Attempts connecting to Reactor, if the autoConnect flag is set.
32
+ */
33
+ function attemptAutoConnect() {
34
+ const status = storeRef.current?.getState().status;
35
+
36
+ if (autoConnect && status === "disconnected") {
37
+ console.debug("[ReactorProvider] Starting autoconnect...");
38
+ storeRef.current
39
+ ?.getState()
40
+ .connect()
41
+ .then(() => {
42
+ console.debug("[ReactorProvider] Autoconnect successful");
43
+ })
44
+ .catch((error) => {
45
+ console.error("[ReactorProvider] Failed to autoconnect:", error);
46
+ });
47
+ }
48
+ }
49
+
50
+ if (storeRef.current === undefined) {
51
+ console.debug("[ReactorProvider] Creating new reactor store");
52
+ // We create the store without autoconnecting, to avoid duplicate connections.
53
+ // We actually connect when the component is mounted, to be on sync with the react component lifecycle.
54
+ storeRef.current = createReactorStore(initReactorStore(props));
55
+ console.debug("[ReactorProvider] Reactor store created successfully");
56
+ attemptAutoConnect();
57
+ }
58
+
59
+ useEffect(() => {
60
+ if (firstRender.current) {
61
+ firstRender.current = false;
62
+ return;
63
+ }
64
+ console.debug("[ReactorProvider] Updating reactor store");
65
+ storeRef.current = createReactorStore(initReactorStore(props));
66
+ console.debug("[ReactorProvider] Reactor store updated successfully");
67
+ attemptAutoConnect();
68
+ }, [props, autoConnect]);
69
+
70
+ return (
71
+ <ReactorContext.Provider value={storeRef.current}>
72
+ {children}
73
+ </ReactorContext.Provider>
74
+ );
75
+ }
76
+
77
+ export function useReactorStore<T = ReactorStore>(
78
+ selector: (state: ReactorStore) => T
79
+ ): T {
80
+ const ctx = useContext(ReactorContext);
81
+ if (!ctx) {
82
+ throw new Error("useReactor must be used within a ReactorProvider");
83
+ }
84
+
85
+ return useStore(ctx, selector);
86
+ }
@@ -0,0 +1,116 @@
1
+ "use client";
2
+
3
+ import { useReactor } from "./hooks";
4
+ import { useEffect, useRef } from "react";
5
+ import React from "react";
6
+
7
+ export interface ReactorViewProps {
8
+ width?: number;
9
+ height?: number;
10
+ className?: string;
11
+ style?: React.CSSProperties;
12
+ }
13
+
14
+ export function ReactorView({
15
+ width,
16
+ height,
17
+ className,
18
+ style,
19
+ }: ReactorViewProps) {
20
+ const { videoTrack, status } = useReactor((state) => ({
21
+ videoTrack: state.videoTrack,
22
+ status: state.status,
23
+ }));
24
+
25
+ const videoRef = useRef<HTMLVideoElement>(null);
26
+
27
+ console.debug("[ReactorView] Render", {
28
+ hasVideoTrack: !!videoTrack,
29
+ status,
30
+ hasVideoElement: !!videoRef.current,
31
+ });
32
+
33
+ useEffect(() => {
34
+ console.debug("[ReactorView] Video track effect triggered", {
35
+ hasVideoElement: !!videoRef.current,
36
+ hasVideoTrack: !!videoTrack,
37
+ videoTrackKind: videoTrack?.kind,
38
+ });
39
+
40
+ if (videoRef.current && videoTrack) {
41
+ console.debug("[ReactorView] Attaching video track to element");
42
+ try {
43
+ // Attach the LiveKit track to the video element
44
+ videoTrack.attach(videoRef.current);
45
+ console.debug("[ReactorView] Video track attached successfully");
46
+ } catch (error) {
47
+ console.error("[ReactorView] Failed to attach video track:", error);
48
+ }
49
+
50
+ // Cleanup: detach when track changes or component unmounts
51
+ return () => {
52
+ console.debug("[ReactorView] Detaching video track from element");
53
+ if (videoRef.current) {
54
+ try {
55
+ videoTrack.detach(videoRef.current);
56
+ console.debug("[ReactorView] Video track detached successfully");
57
+ } catch (error) {
58
+ console.error("[ReactorView] Failed to detach video track:", error);
59
+ }
60
+ }
61
+ };
62
+ } else {
63
+ console.debug("[ReactorView] No video track or element to attach");
64
+ }
65
+ }, [videoTrack]);
66
+
67
+ const showPlaceholder = !videoTrack;
68
+ console.debug("[ReactorView] Placeholder state", { showPlaceholder, status });
69
+
70
+ return (
71
+ <div
72
+ style={{
73
+ position: "relative",
74
+ background: "#000",
75
+ ...(width && { width }),
76
+ ...(height && { height }),
77
+ ...style,
78
+ }}
79
+ className={className}
80
+ >
81
+ <video
82
+ ref={videoRef}
83
+ style={{
84
+ width: "100%",
85
+ height: "100%",
86
+ objectFit: "contain",
87
+ display: showPlaceholder ? "none" : "block",
88
+ }}
89
+ muted
90
+ playsInline
91
+ />
92
+ {showPlaceholder && (
93
+ <div
94
+ style={{
95
+ position: "absolute",
96
+ top: 0,
97
+ left: 0,
98
+ width: "100%",
99
+ height: "100%",
100
+ color: "#fff",
101
+ display: "flex",
102
+ alignItems: "center",
103
+ justifyContent: "center",
104
+ fontSize: "16px",
105
+ fontFamily: "monospace",
106
+ textAlign: "center",
107
+ padding: "20px",
108
+ boxSizing: "border-box",
109
+ }}
110
+ >
111
+ {status}
112
+ </div>
113
+ )}
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,50 @@
1
+ import { useReactorStore } from "./ReactorProvider";
2
+ import type { ReactorStore } from "../core/store";
3
+ import { useShallow } from "zustand/shallow";
4
+ import { useEffect, useRef } from "react";
5
+
6
+ /**
7
+ * Generic hook for accessing selected parts of the Reactor store.
8
+ *
9
+ * @param selector - A function that selects part of the store state.
10
+ * @returns The selected slice from the store.
11
+ */
12
+ export function useReactor<T>(selector: (state: ReactorStore) => T): T {
13
+ return useReactorStore(useShallow(selector));
14
+ }
15
+
16
+ /**
17
+ * Hook for handling message subscriptions with proper React lifecycle management.
18
+ *
19
+ * @param handler - The message handler function
20
+ */
21
+ export function useReactorMessage(handler: (message: any) => void): void {
22
+ const reactor = useReactor((state) => state.internal.reactor);
23
+ const handlerRef = useRef(handler);
24
+
25
+ // Update the ref when handler changes
26
+ useEffect(() => {
27
+ handlerRef.current = handler;
28
+ }, [handler]);
29
+
30
+ useEffect(() => {
31
+ console.debug("[useReactorMessage] Setting up message subscription");
32
+
33
+ // Create a stable handler that calls the current ref
34
+ const stableHandler = (message: any) => {
35
+ console.debug("[useReactorMessage] Message received", { message });
36
+ handlerRef.current(message);
37
+ };
38
+
39
+ // Register the handler and get the cleanup function
40
+ reactor.on("newMessage", stableHandler);
41
+
42
+ console.debug("[useReactorMessage] Message handler registered");
43
+
44
+ // Return the cleanup function
45
+ return () => {
46
+ console.debug("[useReactorMessage] Cleaning up message subscription");
47
+ reactor.off("newMessage", stableHandler);
48
+ };
49
+ }, [reactor]);
50
+ }
package/src/types.ts ADDED
@@ -0,0 +1,37 @@
1
+ export type ReactorStatus =
2
+ | "disconnected" // Not connected to anything
3
+ | "connecting" // Establishing connection to coordinator
4
+ | "waiting" // Connected to coordinator, waiting for GPU assignment
5
+ | "ready"; // Connected to GPU machine, can send/receive messages
6
+
7
+ // Queue information when in waiting state
8
+ export interface ReactorWaitingInfo {
9
+ position?: number; // Position in queue
10
+ estimatedWaitTime?: number; // Seconds
11
+ averageWaitTime?: number; // Historical data
12
+ }
13
+
14
+ // Error information
15
+ export interface ReactorError {
16
+ code: string;
17
+ message: string;
18
+ timestamp: number;
19
+ recoverable: boolean;
20
+ component: "coordinator" | "gpu" | "livekit";
21
+ retryAfter?: number; // Suggested retry delay in seconds
22
+ }
23
+
24
+ // Enhanced state with metadata
25
+ export interface ReactorState {
26
+ status: ReactorStatus;
27
+ waitingInfo?: ReactorWaitingInfo; // When status is 'waiting'
28
+ lastError?: ReactorError; // Most recent error
29
+ }
30
+
31
+ export type ReactorEvent =
32
+ | "statusChanged" //updates on the reactor status
33
+ | "waitingInfoChanged" //updates on the waiting info
34
+ | "newMessage" //new messages from the machine (coordinator handled internally)
35
+ | "fps" //update of the fps rate of the machine
36
+ | "streamChanged" //video stream has changed (LiveKit)
37
+ | "error"; //error events with ReactorError details
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES6",
4
+ "module": "ESNext",
5
+ "lib": ["DOM", "ESNext"],
6
+ "moduleResolution": "Node",
7
+ "declaration": true,
8
+ "emitDeclarationOnly": true,
9
+ "declarationMap": true,
10
+ "outDir": "./dist",
11
+ "jsx": "react-jsx",
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "strict": true,
16
+ "stripInternal": true
17
+ },
18
+ "include": ["src"]
19
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ dts: {
7
+ entry: ["src/index.ts"],
8
+ resolve: true,
9
+ },
10
+ splitting: false,
11
+ sourcemap: true,
12
+ clean: true,
13
+ outDir: "dist",
14
+ tsconfig: "tsconfig.json",
15
+ });