mirror of https://github.com/jetkvm/kvm.git
167 lines
5.1 KiB
TypeScript
167 lines
5.1 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
|
|
import { useSessionStore, SessionInfo } from "@/stores/sessionStore";
|
|
import { useRTCStore } from "@/hooks/stores";
|
|
import { sessionApi } from "@/api/sessionApi";
|
|
import { notify } from "@/notifications";
|
|
|
|
type RpcSendFunction = (method: string, params: Record<string, unknown>, callback: (response: { result?: unknown; error?: { message: string } }) => void) => void;
|
|
|
|
interface SessionEventData {
|
|
sessions: SessionInfo[];
|
|
yourMode: string;
|
|
}
|
|
|
|
interface ModeChangedData {
|
|
mode: string;
|
|
}
|
|
|
|
interface ConnectionModeChangedData {
|
|
newMode: string;
|
|
}
|
|
|
|
export function useSessionEvents(sendFn: RpcSendFunction | null) {
|
|
const {
|
|
currentMode,
|
|
setSessions,
|
|
updateSessionMode,
|
|
setSessionError
|
|
} = useSessionStore();
|
|
|
|
const sendFnRef = useRef(sendFn);
|
|
sendFnRef.current = sendFn;
|
|
|
|
const handleSessionEvent = (method: string, params: unknown) => {
|
|
switch (method) {
|
|
case "sessionsUpdated":
|
|
handleSessionsUpdated(params as SessionEventData);
|
|
break;
|
|
case "modeChanged":
|
|
handleModeChanged(params as ModeChangedData);
|
|
break;
|
|
case "connectionModeChanged":
|
|
handleConnectionModeChanged(params as ConnectionModeChangedData);
|
|
break;
|
|
case "hidReadyForPrimary":
|
|
handleHidReadyForPrimary();
|
|
break;
|
|
case "otherSessionConnected":
|
|
handleOtherSessionConnected();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
const handleSessionsUpdated = (data: SessionEventData) => {
|
|
if (data.sessions) {
|
|
setSessions(data.sessions);
|
|
}
|
|
|
|
// CRITICAL: Only update mode, never show notifications from sessionsUpdated
|
|
// Notifications are exclusively handled by handleModeChanged to prevent duplicates
|
|
if (data.yourMode && data.yourMode !== currentMode) {
|
|
updateSessionMode(data.yourMode as "primary" | "observer" | "queued" | "pending");
|
|
}
|
|
};
|
|
|
|
// Debounce notifications to prevent rapid-fire duplicates
|
|
const lastNotificationRef = useRef<{mode: string, timestamp: number}>({mode: "", timestamp: 0});
|
|
|
|
const handleModeChanged = (data: ModeChangedData) => {
|
|
if (data.mode) {
|
|
// Get the most current mode from the store to avoid race conditions
|
|
const { currentMode: currentModeFromStore } = useSessionStore.getState();
|
|
const previousMode = currentModeFromStore;
|
|
updateSessionMode(data.mode as "primary" | "observer" | "queued" | "pending");
|
|
|
|
if (previousMode === "queued" && data.mode !== "queued") {
|
|
const { setRequestingPrimary } = useSessionStore.getState();
|
|
setRequestingPrimary(false);
|
|
}
|
|
|
|
if (previousMode === "pending" && data.mode === "observer") {
|
|
const { resetRejectionCount } = useSessionStore.getState();
|
|
resetRejectionCount();
|
|
}
|
|
|
|
// HID re-initialization is now handled automatically by permission changes in usePermissions
|
|
|
|
// CRITICAL: Debounce notifications to prevent duplicates from rapid-fire events
|
|
const now = Date.now();
|
|
const lastNotification = lastNotificationRef.current;
|
|
|
|
// Only show notification if:
|
|
// 1. Mode actually changed, AND
|
|
// 2. Haven't shown the same notification in the last 2 seconds
|
|
const shouldNotify = previousMode !== data.mode &&
|
|
(lastNotification.mode !== data.mode || now - lastNotification.timestamp > 2000);
|
|
|
|
if (shouldNotify) {
|
|
if (data.mode === "primary") {
|
|
notify.success("Primary control granted");
|
|
lastNotificationRef.current = {mode: "primary", timestamp: now};
|
|
} else if (data.mode === "observer" && previousMode === "primary") {
|
|
notify.info("Primary control released");
|
|
lastNotificationRef.current = {mode: "observer", timestamp: now};
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleConnectionModeChanged = (data: ConnectionModeChangedData) => {
|
|
if (data.newMode) {
|
|
handleModeChanged({ mode: data.newMode });
|
|
}
|
|
};
|
|
|
|
const handleHidReadyForPrimary = () => {
|
|
const { rpcHidChannel } = useRTCStore.getState();
|
|
if (rpcHidChannel?.readyState === "open") {
|
|
rpcHidChannel.dispatchEvent(new Event("open"));
|
|
}
|
|
};
|
|
|
|
const handleOtherSessionConnected = () => {
|
|
notify.warning("Another session is connecting", {
|
|
duration: 5000
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!sendFnRef.current) return;
|
|
|
|
const fetchSessions = async () => {
|
|
try {
|
|
const sessions = await sessionApi.getSessions(sendFnRef.current!);
|
|
setSessions(sessions);
|
|
} catch (error) {
|
|
console.error("Failed to fetch sessions:", error);
|
|
setSessionError("Failed to fetch session information");
|
|
}
|
|
};
|
|
|
|
fetchSessions();
|
|
}, [setSessions, setSessionError]);
|
|
|
|
useEffect(() => {
|
|
if (!sendFnRef.current) return;
|
|
|
|
const intervalId = setInterval(async () => {
|
|
if (!sendFnRef.current) return;
|
|
|
|
try {
|
|
const sessions = await sessionApi.getSessions(sendFnRef.current);
|
|
setSessions(sessions);
|
|
} catch {
|
|
// Silently fail on refresh errors
|
|
}
|
|
}, 30000); // Refresh every 30 seconds
|
|
|
|
return () => clearInterval(intervalId);
|
|
}, [setSessions]);
|
|
|
|
return {
|
|
handleSessionEvent
|
|
};
|
|
} |