refactor: simplify request handlers and deduplicate config
- Re-enable TanStack Query retry (retry: 3 with exponential backoff), delete all server-side retry logic from request handlers - Strip handler factories to bare minimum: remove isValid, retry, widgetKind - Remove redundant refetchOnWindowFocus/refetchOnReconnect from docker-table - Deduplicate queryCacheMaxValueBytes (pass as param, single source of truth) - Simplify system-resources chart: use dataUpdatedAt instead of ref-based tracking - Fix leading blank lines in 27 request handler files
This commit is contained in:
15 files changed
+36
-114
No files matched your search
@@ -73,7 +73,6 @@ export const beszelSystemsRequestHandler = createIntegrationRequestHandler<
|
||||
});
|
||||
return enriched;
|
||||
},
|
||||
retry: { attempts: 2, delayMs: 500 },
|
||||
queryKey: "beszelSystems",
|
||||
});
|
||||
|
||||
@@ -100,7 +99,6 @@ export const beszelAlertsRequestHandler = createIntegrationRequestHandler<
|
||||
});
|
||||
return { alerts, history };
|
||||
},
|
||||
retry: { attempts: 2, delayMs: 500 },
|
||||
queryKey: "beszelAlerts",
|
||||
});
|
||||
|
||||
@@ -141,6 +139,5 @@ export const beszelStatsRequestHandler = createIntegrationRequestHandler<
|
||||
});
|
||||
return { systemStats, containerStats };
|
||||
},
|
||||
retry: { attempts: 2, delayMs: 500 },
|
||||
queryKey: "beszelStats",
|
||||
});
|
||||
@@ -172,7 +172,6 @@ const mockContainers: {
|
||||
|
||||
export const dockerContainersRequestHandler = createWidgetRequestHandler({
|
||||
queryKey: "dockerContainersResult",
|
||||
widgetKind: "dockerContainers",
|
||||
async requestAsync() {
|
||||
if (isDemoMode) {
|
||||
return mockContainers;
|
||||
|
||||
@@ -13,8 +13,6 @@ type IntegrationOfKind<TKind extends IntegrationKind> = Omit<Integration, "kind"
|
||||
interface Options<TData, TKind extends IntegrationKind, TInput extends Record<string, unknown>> {
|
||||
queryKey: string;
|
||||
requestAsync: (integration: IntegrationOfKind<TKind>, input: TInput) => Promise<TData>;
|
||||
retry?: { attempts?: number; delayMs?: number };
|
||||
isValid?: (data: TData) => boolean;
|
||||
}
|
||||
|
||||
export const createIntegrationRequestHandler = <
|
||||
@@ -23,14 +21,10 @@ export const createIntegrationRequestHandler = <
|
||||
TInput extends Record<string, unknown>,
|
||||
>(
|
||||
options: Options<TData, TKind, TInput>,
|
||||
) => {
|
||||
return {
|
||||
handler: (integration: IntegrationOfKind<TKind>, itemOptions: TInput) =>
|
||||
createRequestHandler<TData, { options: TInput; integration: IntegrationOfKind<TKind> }>({
|
||||
queryKey: options.queryKey,
|
||||
requestAsync: async (input) => options.requestAsync(input.integration, input.options),
|
||||
retry: options.retry,
|
||||
isValid: options.isValid,
|
||||
}).handler({ options: itemOptions, integration }),
|
||||
};
|
||||
};
|
||||
) => ({
|
||||
handler: (integration: IntegrationOfKind<TKind>, itemOptions: TInput) =>
|
||||
createRequestHandler<TData, { options: TInput; integration: IntegrationOfKind<TKind> }>({
|
||||
queryKey: options.queryKey,
|
||||
requestAsync: async (input) => options.requestAsync(input.integration, input.options),
|
||||
}).handler({ options: itemOptions, integration }),
|
||||
});
|
||||
@@ -1,59 +1,15 @@
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
|
||||
const logger = createLogger({ module: "requestHandler" });
|
||||
|
||||
const isAbortedError = (error: unknown): boolean => {
|
||||
if (!(error instanceof Error)) return false;
|
||||
return error.name === "AbortError" || error.name === "TimeoutError";
|
||||
};
|
||||
|
||||
interface Options<TData, TInput extends Record<string, unknown>> {
|
||||
queryKey: string;
|
||||
requestAsync: (input: TInput) => Promise<TData>;
|
||||
retry?: { attempts?: number; delayMs?: number };
|
||||
isValid?: (data: TData) => boolean;
|
||||
}
|
||||
|
||||
const defaultRetry = { attempts: 1, delayMs: 0 };
|
||||
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export const createRequestHandler = <TData, TInput extends Record<string, unknown>>(
|
||||
options: Options<TData, TInput>,
|
||||
) => {
|
||||
return {
|
||||
handler: (input: TInput) => {
|
||||
const retryConfig = { ...defaultRetry, ...options.retry };
|
||||
|
||||
return {
|
||||
async getDataAsync(): Promise<{ data: TData; timestamp: Date }> {
|
||||
let lastError: unknown;
|
||||
for (let attempt = 1; attempt <= retryConfig.attempts; attempt++) {
|
||||
try {
|
||||
const data = await options.requestAsync(input);
|
||||
if (options.isValid && !options.isValid(data)) {
|
||||
logger.warn("Request handler received invalid data", {
|
||||
queryKey: options.queryKey,
|
||||
attempt,
|
||||
});
|
||||
}
|
||||
return { data, timestamp: new Date() };
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (isAbortedError(error) || attempt === retryConfig.attempts) {
|
||||
throw error;
|
||||
}
|
||||
logger.warn("Request handler fetch failed, retrying", {
|
||||
queryKey: options.queryKey,
|
||||
attempt,
|
||||
nextDelayMs: retryConfig.delayMs,
|
||||
error: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
|
||||
});
|
||||
await sleep(retryConfig.delayMs);
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
},
|
||||
};
|
||||
) => ({
|
||||
handler: (input: TInput) => ({
|
||||
async getDataAsync(): Promise<{ data: TData; timestamp: Date }> {
|
||||
const data = await options.requestAsync(input);
|
||||
return { data, timestamp: new Date() };
|
||||
},
|
||||
};
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -1,25 +1,16 @@
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
|
||||
import { createRequestHandler } from "./request-handler";
|
||||
|
||||
interface Options<TData, TKind extends WidgetKind, TInput extends Record<string, unknown>> {
|
||||
interface Options<TData, TInput extends Record<string, unknown>> {
|
||||
queryKey: string;
|
||||
requestAsync: (input: TInput) => Promise<TData>;
|
||||
retry?: { attempts?: number; delayMs?: number };
|
||||
isValid?: (data: TData) => boolean;
|
||||
widgetKind: TKind;
|
||||
}
|
||||
|
||||
export const createWidgetRequestHandler = <TData, TKind extends WidgetKind, TInput extends Record<string, unknown>>(
|
||||
options: Options<TData, TKind, TInput>,
|
||||
) => {
|
||||
return {
|
||||
handler: (widgetOptions: TInput) =>
|
||||
createRequestHandler<TData, TInput>({
|
||||
queryKey: options.queryKey,
|
||||
requestAsync: options.requestAsync,
|
||||
retry: options.retry,
|
||||
isValid: options.isValid,
|
||||
}).handler(widgetOptions),
|
||||
};
|
||||
};
|
||||
export const createWidgetRequestHandler = <TData, TInput extends Record<string, unknown>>(
|
||||
options: Options<TData, TInput>,
|
||||
) => ({
|
||||
handler: (widgetOptions: TInput) =>
|
||||
createRequestHandler<TData, TInput>({
|
||||
queryKey: options.queryKey,
|
||||
requestAsync: options.requestAsync,
|
||||
}).handler(widgetOptions),
|
||||
});
|
||||
@@ -7,7 +7,6 @@ import { createWidgetRequestHandler } from "./lib/widget-request-handler";
|
||||
|
||||
export const minecraftServerStatusRequestHandler = createWidgetRequestHandler({
|
||||
queryKey: "minecraftServerStatusApiResult",
|
||||
widgetKind: "minecraftServerStatus",
|
||||
async requestAsync(input: { domain: string; isBedrockServer: boolean }) {
|
||||
const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`;
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ const logger = createLogger({ module: "rssFeedsRequestHandler" });
|
||||
|
||||
export const rssFeedsRequestHandler = createWidgetRequestHandler({
|
||||
queryKey: "rssFeedList",
|
||||
widgetKind: "rssFeed",
|
||||
async requestAsync(input: { url: string; count: number }) {
|
||||
const result = (await extract(input.url, {
|
||||
getExtraEntryFields: (feedEntry) => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { createWidgetRequestHandler } from "./lib/widget-request-handler";
|
||||
|
||||
export const fetchStockPriceHandler = createWidgetRequestHandler({
|
||||
queryKey: "fetchStockPriceResult",
|
||||
widgetKind: "stockPrice",
|
||||
async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) {
|
||||
const response = await withTimeoutAsync(async (signal) => {
|
||||
return await fetchWithTrustedCertificatesAsync(
|
||||
|
||||
@@ -7,7 +7,6 @@ import { createWidgetRequestHandler } from "./lib/widget-request-handler";
|
||||
|
||||
export const weatherRequestHandler = createWidgetRequestHandler({
|
||||
queryKey: "weatherAtLocation",
|
||||
widgetKind: "weather",
|
||||
async requestAsync(input: { latitude: number; longitude: number }) {
|
||||
const res = await withTimeoutAsync(async (signal) => {
|
||||
return await fetchWithTrustedCertificatesAsync(
|
||||
|
||||
Reference in new issue
Block a user