mirror of https://github.com/jetkvm/kvm.git
refactor: streamline version retrieval logic and enhance error handling in useVersion hook
This commit is contained in:
parent
0bd2821924
commit
632125e38f
|
|
@ -1,23 +1,11 @@
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { useDeviceStore } from "@/hooks/stores";
|
import { useDeviceStore } from "@/hooks/stores";
|
||||||
import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
|
import { JsonRpcError, RpcMethodNotFound } from "@/hooks/useJsonRpc";
|
||||||
|
import { getUpdateStatus, getLocalVersion as getLocalVersionRpc } from "@/utils/jsonrpc";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
export interface VersionInfo {
|
|
||||||
appVersion: string;
|
|
||||||
systemVersion: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SystemVersionInfo {
|
|
||||||
local: VersionInfo;
|
|
||||||
remote?: VersionInfo;
|
|
||||||
systemUpdateAvailable: boolean;
|
|
||||||
appUpdateAvailable: boolean;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useVersion() {
|
export function useVersion() {
|
||||||
const {
|
const {
|
||||||
appVersion,
|
appVersion,
|
||||||
|
|
@ -25,51 +13,40 @@ export function useVersion() {
|
||||||
setAppVersion,
|
setAppVersion,
|
||||||
setSystemVersion,
|
setSystemVersion,
|
||||||
} = useDeviceStore();
|
} = useDeviceStore();
|
||||||
const { send } = useJsonRpc();
|
|
||||||
const getVersionInfo = useCallback(() => {
|
const getVersionInfo = useCallback(async () => {
|
||||||
return new Promise<SystemVersionInfo>((resolve, reject) => {
|
try {
|
||||||
send("getUpdateStatus", {}, (resp: JsonRpcResponse) => {
|
const result = await getUpdateStatus();
|
||||||
if ("error" in resp) {
|
|
||||||
notifications.error(m.updates_failed_check({ error: String(resp.error) }));
|
|
||||||
reject(new Error("Failed to check for updates"));
|
|
||||||
} else {
|
|
||||||
const result = resp.result as SystemVersionInfo;
|
|
||||||
setAppVersion(result.local.appVersion);
|
setAppVersion(result.local.appVersion);
|
||||||
setSystemVersion(result.local.systemVersion);
|
setSystemVersion(result.local.systemVersion);
|
||||||
|
return result;
|
||||||
if (result.error) {
|
} catch (error) {
|
||||||
notifications.error(m.updates_failed_check({ error: String(result.error) }));
|
const jsonRpcError = error as JsonRpcError;
|
||||||
reject(new Error("Failed to check for updates"));
|
notifications.error(m.updates_failed_check({ error: jsonRpcError.message }));
|
||||||
} else {
|
throw jsonRpcError;
|
||||||
resolve(result);
|
|
||||||
}
|
}
|
||||||
}
|
}, [setAppVersion, setSystemVersion]);
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [send, setAppVersion, setSystemVersion]);
|
|
||||||
|
|
||||||
const getLocalVersion = useCallback(() => {
|
|
||||||
return new Promise<VersionInfo>((resolve, reject) => {
|
|
||||||
send("getLocalVersion", {}, (resp: JsonRpcResponse) => {
|
|
||||||
if ("error" in resp) {
|
|
||||||
console.log(resp.error)
|
|
||||||
if (resp.error.code === RpcMethodNotFound) {
|
|
||||||
console.warn("Failed to get device version, using legacy version");
|
|
||||||
return getVersionInfo().then(result => resolve(result.local)).catch(reject);
|
|
||||||
}
|
|
||||||
console.error("Failed to get device version N", resp.error);
|
|
||||||
notifications.error(m.updates_failed_get_device_version({ error: String(resp.error) }));
|
|
||||||
reject(new Error("Failed to get device version"));
|
|
||||||
} else {
|
|
||||||
const result = resp.result as VersionInfo;
|
|
||||||
|
|
||||||
|
const getLocalVersion = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await getLocalVersionRpc();
|
||||||
setAppVersion(result.appVersion);
|
setAppVersion(result.appVersion);
|
||||||
setSystemVersion(result.systemVersion);
|
setSystemVersion(result.systemVersion);
|
||||||
resolve(result);
|
return result;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const jsonRpcError = error as JsonRpcError;
|
||||||
|
|
||||||
|
if (jsonRpcError.code === RpcMethodNotFound) {
|
||||||
|
console.error("Failed to get local version, using legacy remote version");
|
||||||
|
const result = await getVersionInfo();
|
||||||
|
return result.local;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
console.error("Failed to get device version", jsonRpcError);
|
||||||
}, [send, setAppVersion, setSystemVersion, getVersionInfo]);
|
notifications.error(m.updates_failed_get_device_version({ error: jsonRpcError.message }));
|
||||||
|
throw jsonRpcError;
|
||||||
|
}
|
||||||
|
}, [setAppVersion, setSystemVersion, getVersionInfo]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getVersionInfo,
|
getVersionInfo,
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,15 @@ import { useLocation, useNavigate } from "react-router";
|
||||||
import { useJsonRpc } from "@hooks/useJsonRpc";
|
import { useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import { UpdateState, useUpdateStore } from "@hooks/stores";
|
import { UpdateState, useUpdateStore } from "@hooks/stores";
|
||||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||||
import { SystemVersionInfo, useVersion } from "@hooks/useVersion";
|
import { useVersion } from "@hooks/useVersion";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import LoadingSpinner from "@components/LoadingSpinner";
|
import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import UpdatingStatusCard, { type UpdatePart} from "@components/UpdatingStatusCard";
|
import UpdatingStatusCard, { type UpdatePart } from "@components/UpdatingStatusCard";
|
||||||
import { m } from "@localizations/messages.js";
|
import { m } from "@localizations/messages.js";
|
||||||
|
|
||||||
|
import { SystemVersionInfo } from "../utils/jsonrpc";
|
||||||
|
|
||||||
export default function SettingsGeneralUpdateRoute() {
|
export default function SettingsGeneralUpdateRoute() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import { useRTCStore } from "@/hooks/stores";
|
import { useRTCStore } from "@/hooks/stores";
|
||||||
|
|
||||||
// JSON-RPC utility for use outside of React components
|
// JSON-RPC utility for use outside of React components
|
||||||
|
|
||||||
export interface JsonRpcCallOptions {
|
export interface JsonRpcCallOptions {
|
||||||
method: string;
|
method: string;
|
||||||
params?: unknown;
|
params?: unknown;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
retriesOnError?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JsonRpcCallResponse {
|
export interface JsonRpcCallResponse<T = unknown> {
|
||||||
jsonrpc: string;
|
jsonrpc: string;
|
||||||
result?: unknown;
|
result?: T;
|
||||||
error?: {
|
error?: {
|
||||||
code: number;
|
code: number;
|
||||||
message: string;
|
message: string;
|
||||||
|
|
@ -20,16 +22,31 @@ export interface JsonRpcCallResponse {
|
||||||
|
|
||||||
let rpcCallCounter = 0;
|
let rpcCallCounter = 0;
|
||||||
|
|
||||||
export function callJsonRpc(options: JsonRpcCallOptions): Promise<JsonRpcCallResponse> {
|
// Helper: sleep utility for retry delays
|
||||||
return new Promise((resolve, reject) => {
|
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
// Access the RTC store directly outside of React context
|
|
||||||
const rpcDataChannel = useRTCStore.getState().rpcDataChannel;
|
|
||||||
|
|
||||||
if (!rpcDataChannel || rpcDataChannel.readyState !== "open") {
|
// Helper: wait for RTC data channel to be ready
|
||||||
reject(new Error("RPC data channel not available"));
|
async function waitForRtcReady(signal: AbortSignal): Promise<RTCDataChannel> {
|
||||||
return;
|
const pollInterval = 100;
|
||||||
|
|
||||||
|
while (!signal.aborted) {
|
||||||
|
const state = useRTCStore.getState();
|
||||||
|
if (state.rpcDataChannel?.readyState === "open") {
|
||||||
|
return state.rpcDataChannel;
|
||||||
|
}
|
||||||
|
await sleep(pollInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error("RTC readiness check aborted");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: send RPC request and wait for response
|
||||||
|
async function sendRpcRequest<T>(
|
||||||
|
rpcDataChannel: RTCDataChannel,
|
||||||
|
options: JsonRpcCallOptions,
|
||||||
|
signal: AbortSignal,
|
||||||
|
): Promise<JsonRpcCallResponse<T>> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
rpcCallCounter++;
|
rpcCallCounter++;
|
||||||
const requestId = `rpc_${Date.now()}_${rpcCallCounter}`;
|
const requestId = `rpc_${Date.now()}_${rpcCallCounter}`;
|
||||||
|
|
||||||
|
|
@ -40,32 +57,90 @@ export function callJsonRpc(options: JsonRpcCallOptions): Promise<JsonRpcCallRes
|
||||||
id: requestId,
|
id: requestId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeout = options.timeout || 5000;
|
|
||||||
let timeoutId: number | undefined; // eslint-disable-line prefer-const
|
|
||||||
|
|
||||||
const messageHandler = (event: MessageEvent) => {
|
const messageHandler = (event: MessageEvent) => {
|
||||||
try {
|
try {
|
||||||
const response = JSON.parse(event.data) as JsonRpcCallResponse;
|
const response = JSON.parse(event.data) as JsonRpcCallResponse<T>;
|
||||||
if (response.id === requestId) {
|
if (response.id === requestId) {
|
||||||
clearTimeout(timeoutId);
|
cleanup();
|
||||||
rpcDataChannel.removeEventListener("message", messageHandler);
|
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Ignore parse errors from other messages
|
// Ignore parse errors from other messages
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
const abortHandler = () => {
|
||||||
rpcDataChannel.removeEventListener("message", messageHandler);
|
cleanup();
|
||||||
reject(new Error(`JSON-RPC call timed out after ${timeout}ms`));
|
reject(new Error("Request aborted"));
|
||||||
}, timeout);
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
rpcDataChannel.removeEventListener("message", messageHandler);
|
||||||
|
signal.removeEventListener("abort", abortHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
signal.addEventListener("abort", abortHandler);
|
||||||
rpcDataChannel.addEventListener("message", messageHandler);
|
rpcDataChannel.addEventListener("message", messageHandler);
|
||||||
rpcDataChannel.send(JSON.stringify(request));
|
rpcDataChannel.send(JSON.stringify(request));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function overloads for better typing
|
||||||
|
export function callJsonRpc<T>(
|
||||||
|
options: JsonRpcCallOptions,
|
||||||
|
): Promise<JsonRpcCallResponse<T> & { result: T }>;
|
||||||
|
export function callJsonRpc(
|
||||||
|
options: JsonRpcCallOptions,
|
||||||
|
): Promise<JsonRpcCallResponse<unknown>>;
|
||||||
|
export async function callJsonRpc<T = unknown>(
|
||||||
|
options: JsonRpcCallOptions,
|
||||||
|
): Promise<JsonRpcCallResponse<T>> {
|
||||||
|
const maxRetries = options.retriesOnError ?? 0;
|
||||||
|
const timeout = options.timeout || 5000;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => abortController.abort(), timeout);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wait for RTC readiness
|
||||||
|
const rpcDataChannel = await waitForRtcReady(abortController.signal);
|
||||||
|
|
||||||
|
// Send RPC request and wait for response
|
||||||
|
const response = await sendRpcRequest<T>(
|
||||||
|
rpcDataChannel,
|
||||||
|
options,
|
||||||
|
abortController.signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// Retry on error if attempts remain
|
||||||
|
if (response.error && attempt < maxRetries) {
|
||||||
|
await sleep(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// Retry on timeout/error if attempts remain
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
await sleep(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error instanceof Error
|
||||||
|
? error
|
||||||
|
: new Error(`JSON-RPC call failed after ${timeout}ms`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should never reach here due to loop logic, but TypeScript needs this
|
||||||
|
throw new Error("Unexpected error in callJsonRpc");
|
||||||
|
}
|
||||||
|
|
||||||
// Specific network settings API calls
|
// Specific network settings API calls
|
||||||
export async function getNetworkSettings() {
|
export async function getNetworkSettings() {
|
||||||
const response = await callJsonRpc({ method: "getNetworkSettings" });
|
const response = await callJsonRpc({ method: "getNetworkSettings" });
|
||||||
|
|
@ -101,3 +176,36 @@ export async function renewDHCPLease() {
|
||||||
}
|
}
|
||||||
return response.result;
|
return response.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VersionInfo {
|
||||||
|
appVersion: string;
|
||||||
|
systemVersion: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemVersionInfo {
|
||||||
|
local: VersionInfo;
|
||||||
|
remote?: VersionInfo;
|
||||||
|
systemUpdateAvailable: boolean;
|
||||||
|
appUpdateAvailable: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUpdateStatus() {
|
||||||
|
const response = await callJsonRpc<SystemVersionInfo>({
|
||||||
|
method: "getUpdateStatus",
|
||||||
|
// This function calls our api server to see if there are any updates available.
|
||||||
|
// It can be called on page load right after a restart, so we need to give it time to
|
||||||
|
// establish a connection to the api server.
|
||||||
|
timeout: 10000,
|
||||||
|
retriesOnError: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) throw response.error;
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLocalVersion() {
|
||||||
|
const response = await callJsonRpc<VersionInfo>({ method: "getLocalVersion" });
|
||||||
|
if (response.error) throw response.error;
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue