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 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]; }