Files
kvm/ui/src/hooks/useJsonRpc.ts
luckfox-eng29 9a4e604c61 Update App version to 0.1.1
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
2026-02-05 13:59:33 +08:00

188 lines
5.6 KiB
TypeScript

import { useCallback, useEffect } from "react";
import { useRTCStore, useSettingsStore } from "@/hooks/stores";
export interface JsonRpcRequest {
jsonrpc: string;
method: string;
params: object;
id: number | string;
}
export interface JsonRpcError {
code: number;
data?: string;
message: string;
}
export interface JsonRpcSuccessResponse {
jsonrpc: string;
result: boolean | number | object | string | [];
id: string | number;
}
export interface JsonRpcErrorResponse {
jsonrpc: string;
error: JsonRpcError;
id: string | number;
}
export type JsonRpcResponse = JsonRpcSuccessResponse | JsonRpcErrorResponse;
const callbackStore = new Map<number | string, (resp: JsonRpcResponse) => void>();
let requestCounter = 0;
let httpSessionId: string | null = null;
let httpSessionInvalidated = false;
function getHttpSessionId() {
if (httpSessionId) return httpSessionId;
try {
const existing = window.sessionStorage.getItem("httpSessionId");
if (existing) {
httpSessionId = existing;
return httpSessionId;
}
const generated = typeof crypto !== "undefined" && "randomUUID" in crypto
? crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
window.sessionStorage.setItem("httpSessionId", generated);
httpSessionId = generated;
return httpSessionId;
} catch {
const fallback = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
httpSessionId = fallback;
return httpSessionId;
}
}
export function resetHttpSessionId() {
try {
window.sessionStorage.removeItem("httpSessionId");
} catch {
void 0;
}
httpSessionId = null;
httpSessionInvalidated = true;
}
export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) {
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
const forceHttp = useSettingsStore(state => state.forceHttp);
const send = useCallback(
(method: string, params: unknown, callback?: (resp: JsonRpcResponse) => void) => {
if (forceHttp) {
if (httpSessionInvalidated) {
requestCounter++;
const payloadId = requestCounter;
if (callback) {
callback({
jsonrpc: "2.0",
error: { code: -32002, message: "HTTP session invalidated on client" },
id: payloadId,
} as JsonRpcErrorResponse);
}
return;
}
requestCounter++;
const payload = { jsonrpc: "2.0", method, params, id: requestCounter };
fetch("/api/rpc", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Session-ID': getHttpSessionId(),
},
body: JSON.stringify(payload)
})
.then(res => res.json())
.then((data: unknown) => {
const handleEvent = (event: JsonRpcRequest) => {
if (event.method === "refreshPage") {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set("networkChanged", "true");
window.location.href = currentUrl.toString();
return;
}
if (onRequest) onRequest(event);
};
if (data && typeof data === "object" && ("response" in data || "event" in data)) {
const wrapper = data as { response: JsonRpcResponse; event?: JsonRpcRequest };
if (wrapper.event) {
handleEvent(wrapper.event);
}
if (callback) callback(wrapper.response);
return;
}
if (data && typeof data === "object" && "method" in data) {
handleEvent(data as JsonRpcRequest);
return;
}
if (callback) callback(data as JsonRpcResponse);
})
.catch(err => {
console.error("RPC over HTTP failed", err);
if (callback) {
callback({
jsonrpc: "2.0",
error: { code: -32000, message: "HTTP RPC failed", data: err.toString() },
id: payload.id
});
}
});
return;
}
if (rpcDataChannel?.readyState !== "open") return;
requestCounter++;
const payload = { jsonrpc: "2.0", method, params, id: requestCounter };
// Store the callback if it exists
if (callback) callbackStore.set(payload.id, callback);
rpcDataChannel.send(JSON.stringify(payload));
},
[rpcDataChannel, forceHttp, onRequest],
);
useEffect(() => {
if (!rpcDataChannel) return;
const messageHandler = (e: MessageEvent) => {
const payload = JSON.parse(e.data) as JsonRpcResponse | JsonRpcRequest;
// The "API" can also "request" data from the client
// If the payload has a method, it's a request
if ("method" in payload) {
if ((payload as JsonRpcRequest).method === "refreshPage") {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set("networkChanged", "true");
window.location.href = currentUrl.toString();
return;
}
if (onRequest) onRequest(payload as JsonRpcRequest);
return;
}
if ("error" in payload) console.error(payload.error);
if (!payload.id) return;
const callback = callbackStore.get(payload.id);
if (callback) {
callback(payload);
callbackStore.delete(payload.id);
}
};
rpcDataChannel.addEventListener("message", messageHandler);
return () => {
rpcDataChannel.removeEventListener("message", messageHandler);
};
}, [rpcDataChannel, onRequest]);
return [send];
}