@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,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
|
+
});
|