Inital commit
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import {
|
||||
buildBaseUrl,
|
||||
checkHealth,
|
||||
loadSavedComputers,
|
||||
loadSavedProjects,
|
||||
removeSavedComputer,
|
||||
saveHostConfig,
|
||||
saveSavedProjects,
|
||||
upsertSavedComputer,
|
||||
} from "@/lib/api";
|
||||
import { createSocket } from "@/lib/socket";
|
||||
import type {
|
||||
ChatEvent,
|
||||
ChatHistoryPayload,
|
||||
LiveRuntimeInfo,
|
||||
PersistedSessionInfo,
|
||||
RuntimeCreatedPayload,
|
||||
SavedComputer,
|
||||
} from "@/lib/types";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
||||
type SocketContextValue = {
|
||||
host: string;
|
||||
port: string;
|
||||
computerName: string;
|
||||
baseUrl: string | null;
|
||||
connected: boolean;
|
||||
connecting: boolean;
|
||||
error: string | null;
|
||||
savedComputers: SavedComputer[];
|
||||
persistedSessions: PersistedSessionInfo[];
|
||||
liveRuntimes: LiveRuntimeInfo[];
|
||||
savedProjects: string[];
|
||||
addProject: (cwd: string) => Promise<void>;
|
||||
removeProject: (cwd: string) => Promise<void>;
|
||||
setHost: (host: string) => void;
|
||||
setPort: (port: string) => void;
|
||||
setComputerName: (name: string) => void;
|
||||
connect: () => Promise<boolean>;
|
||||
connectToSavedComputer: (computer: SavedComputer) => Promise<boolean>;
|
||||
removeSavedComputerEntry: (id: string) => Promise<void>;
|
||||
disconnect: () => void;
|
||||
refreshSessions: () => void;
|
||||
createRuntime: (cwd: string, name?: string) => void;
|
||||
openRuntime: (sessionFile: string) => void;
|
||||
attachRuntime: (runtimeId: string) => void;
|
||||
sendPrompt: (runtimeId: string, message: string) => void;
|
||||
abortPrompt: (runtimeId: string) => void;
|
||||
subscribeChat: (listener: (event: ChatEvent) => void) => () => void;
|
||||
subscribeChatHistory: (listener: (payload: ChatHistoryPayload) => void) => () => void;
|
||||
waitForRuntime: (
|
||||
kind: "created" | "opened",
|
||||
timeoutMs?: number,
|
||||
) => Promise<RuntimeCreatedPayload>;
|
||||
};
|
||||
|
||||
const SocketContext = createContext<SocketContextValue | null>(null);
|
||||
|
||||
export function SocketProvider({ children }: { children: ReactNode }) {
|
||||
const [host, setHost] = useState("");
|
||||
const [port, setPort] = useState("8787");
|
||||
const [computerName, setComputerName] = useState("");
|
||||
const [baseUrl, setBaseUrl] = useState<string | null>(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [savedComputers, setSavedComputers] = useState<SavedComputer[]>([]);
|
||||
const [persistedSessions, setPersistedSessions] = useState<PersistedSessionInfo[]>([]);
|
||||
const [liveRuntimes, setLiveRuntimes] = useState<LiveRuntimeInfo[]>([]);
|
||||
const [savedProjects, setSavedProjects] = useState<string[]>([]);
|
||||
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
const chatListenersRef = useRef(new Set<(event: ChatEvent) => void>());
|
||||
const chatHistoryListenersRef = useRef(new Set<(payload: ChatHistoryPayload) => void>());
|
||||
const runtimeWaitersRef = useRef<{
|
||||
created: Array<(payload: RuntimeCreatedPayload) => void>;
|
||||
opened: Array<(payload: RuntimeCreatedPayload) => void>;
|
||||
}>({ created: [], opened: [] });
|
||||
|
||||
useEffect(() => {
|
||||
void loadSavedComputers().then((computers) => {
|
||||
setSavedComputers(computers);
|
||||
const recent = computers[0];
|
||||
if (!recent) {
|
||||
return;
|
||||
}
|
||||
setHost(recent.host);
|
||||
setPort(recent.port);
|
||||
setComputerName(recent.name);
|
||||
void loadSavedProjects(recent.host, recent.port).then(setSavedProjects);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeSavedComputerEntry = useCallback(
|
||||
async (id: string) => {
|
||||
const next = await removeSavedComputer(savedComputers, id);
|
||||
setSavedComputers(next);
|
||||
},
|
||||
[savedComputers],
|
||||
);
|
||||
|
||||
const addProject = useCallback(
|
||||
async (cwd: string) => {
|
||||
const trimmed = cwd.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSavedProjects((current) => {
|
||||
if (current.some((entry) => entry === trimmed)) {
|
||||
return current;
|
||||
}
|
||||
const next = [...current, trimmed];
|
||||
void saveSavedProjects(host, port, next);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[host, port],
|
||||
);
|
||||
|
||||
const removeProject = useCallback(
|
||||
async (cwd: string) => {
|
||||
setSavedProjects((current) => {
|
||||
const next = current.filter((entry) => entry !== cwd);
|
||||
void saveSavedProjects(host, port, next);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[host, port],
|
||||
);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
socketRef.current?.disconnect();
|
||||
socketRef.current = null;
|
||||
setConnected(false);
|
||||
setBaseUrl(null);
|
||||
}, []);
|
||||
|
||||
const connectWithHost = useCallback(
|
||||
async (nextHost: string, nextPort: string, nextName?: string) => {
|
||||
setConnecting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const nextBaseUrl = buildBaseUrl(nextHost, nextPort);
|
||||
const healthy = await checkHealth(nextBaseUrl);
|
||||
if (!healthy) {
|
||||
throw new Error("Could not reach Pi Mobile API");
|
||||
}
|
||||
|
||||
disconnect();
|
||||
|
||||
const socket = createSocket(nextBaseUrl, {
|
||||
onConnect: () => {
|
||||
setConnected(true);
|
||||
setConnecting(false);
|
||||
socket.emit("sessions:list");
|
||||
},
|
||||
onDisconnect: () => {
|
||||
setConnected(false);
|
||||
},
|
||||
onError: (message) => setError(message),
|
||||
onSessionsList: (payload) => {
|
||||
setPersistedSessions(payload.persisted);
|
||||
setLiveRuntimes(payload.live);
|
||||
},
|
||||
onRuntimeCreated: (payload) => {
|
||||
for (const resolve of runtimeWaitersRef.current.created.splice(0)) {
|
||||
resolve(payload);
|
||||
}
|
||||
},
|
||||
onRuntimeOpened: (payload) => {
|
||||
for (const resolve of runtimeWaitersRef.current.opened.splice(0)) {
|
||||
resolve(payload);
|
||||
}
|
||||
},
|
||||
onChatEvent: (payload) => {
|
||||
for (const listener of chatListenersRef.current) {
|
||||
listener(payload);
|
||||
}
|
||||
},
|
||||
onChatHistory: (payload) => {
|
||||
for (const listener of chatHistoryListenersRef.current) {
|
||||
listener(payload);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
socketRef.current = socket;
|
||||
setBaseUrl(nextBaseUrl);
|
||||
setHost(nextHost);
|
||||
setPort(nextPort);
|
||||
if (nextName !== undefined) {
|
||||
setComputerName(nextName);
|
||||
}
|
||||
await saveHostConfig({ host: nextHost, port: nextPort });
|
||||
const nextComputers = await upsertSavedComputer(
|
||||
savedComputers,
|
||||
nextHost,
|
||||
nextPort,
|
||||
nextName ?? computerName,
|
||||
);
|
||||
setSavedComputers(nextComputers);
|
||||
const projects = await loadSavedProjects(nextHost, nextPort);
|
||||
setSavedProjects(projects);
|
||||
return true;
|
||||
} catch (connectError) {
|
||||
const message =
|
||||
connectError instanceof Error ? connectError.message : "Failed to connect";
|
||||
setError(message);
|
||||
setConnecting(false);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[computerName, disconnect, savedComputers],
|
||||
);
|
||||
|
||||
const connect = useCallback(async () => {
|
||||
return connectWithHost(host, port, computerName);
|
||||
}, [connectWithHost, host, port, computerName]);
|
||||
|
||||
const connectToSavedComputer = useCallback(
|
||||
async (computer: SavedComputer) => {
|
||||
return connectWithHost(computer.host, computer.port, computer.name);
|
||||
},
|
||||
[connectWithHost],
|
||||
);
|
||||
|
||||
const refreshSessions = useCallback(() => {
|
||||
socketRef.current?.emit("sessions:list");
|
||||
}, []);
|
||||
|
||||
const createRuntime = useCallback((cwd: string, name?: string) => {
|
||||
socketRef.current?.emit("runtime:create", { cwd, name });
|
||||
}, []);
|
||||
|
||||
const openRuntime = useCallback((sessionFile: string) => {
|
||||
socketRef.current?.emit("runtime:open", { sessionFile });
|
||||
}, []);
|
||||
|
||||
const attachRuntime = useCallback((runtimeId: string) => {
|
||||
socketRef.current?.emit("runtime:attach", { runtimeId });
|
||||
}, []);
|
||||
|
||||
const sendPrompt = useCallback((runtimeId: string, message: string) => {
|
||||
socketRef.current?.emit("chat:prompt", { runtimeId, message });
|
||||
}, []);
|
||||
|
||||
const abortPrompt = useCallback((runtimeId: string) => {
|
||||
socketRef.current?.emit("chat:abort", { runtimeId });
|
||||
}, []);
|
||||
|
||||
const subscribeChat = useCallback((listener: (event: ChatEvent) => void) => {
|
||||
chatListenersRef.current.add(listener);
|
||||
return () => {
|
||||
chatListenersRef.current.delete(listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const subscribeChatHistory = useCallback((listener: (payload: ChatHistoryPayload) => void) => {
|
||||
chatHistoryListenersRef.current.add(listener);
|
||||
return () => {
|
||||
chatHistoryListenersRef.current.delete(listener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const waitForRuntime = useCallback(
|
||||
(kind: "created" | "opened", timeoutMs = 15000) =>
|
||||
new Promise<RuntimeCreatedPayload>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
runtimeWaitersRef.current[kind] = runtimeWaitersRef.current[kind].filter(
|
||||
(entry) => entry !== resolve,
|
||||
);
|
||||
reject(new Error("Timed out waiting for runtime"));
|
||||
}, timeoutMs);
|
||||
|
||||
const wrappedResolve = (payload: RuntimeCreatedPayload) => {
|
||||
clearTimeout(timer);
|
||||
resolve(payload);
|
||||
};
|
||||
|
||||
runtimeWaitersRef.current[kind].push(wrappedResolve);
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
host,
|
||||
port,
|
||||
computerName,
|
||||
baseUrl,
|
||||
connected,
|
||||
connecting,
|
||||
error,
|
||||
savedComputers,
|
||||
persistedSessions,
|
||||
liveRuntimes,
|
||||
savedProjects,
|
||||
addProject,
|
||||
removeProject,
|
||||
setHost,
|
||||
setPort,
|
||||
setComputerName,
|
||||
connect,
|
||||
connectToSavedComputer,
|
||||
removeSavedComputerEntry,
|
||||
disconnect,
|
||||
refreshSessions,
|
||||
createRuntime,
|
||||
openRuntime,
|
||||
attachRuntime,
|
||||
sendPrompt,
|
||||
abortPrompt,
|
||||
subscribeChat,
|
||||
subscribeChatHistory,
|
||||
waitForRuntime,
|
||||
}),
|
||||
[
|
||||
host,
|
||||
port,
|
||||
computerName,
|
||||
baseUrl,
|
||||
connected,
|
||||
connecting,
|
||||
error,
|
||||
savedComputers,
|
||||
persistedSessions,
|
||||
liveRuntimes,
|
||||
savedProjects,
|
||||
addProject,
|
||||
removeProject,
|
||||
connect,
|
||||
connectToSavedComputer,
|
||||
removeSavedComputerEntry,
|
||||
disconnect,
|
||||
refreshSessions,
|
||||
createRuntime,
|
||||
openRuntime,
|
||||
attachRuntime,
|
||||
sendPrompt,
|
||||
abortPrompt,
|
||||
subscribeChat,
|
||||
subscribeChatHistory,
|
||||
waitForRuntime,
|
||||
],
|
||||
);
|
||||
|
||||
return <SocketContext.Provider value={value}>{children}</SocketContext.Provider>;
|
||||
}
|
||||
|
||||
export function useSocket() {
|
||||
const context = useContext(SocketContext);
|
||||
if (!context) {
|
||||
throw new Error("useSocket must be used within SocketProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user