Files
homarr/packages/request-handler/src/lib/request-handler.ts
T
Thomas Camlong 6416080b79 feat: replace server-side Redis caching with TanStack Query AsyncStoragePersister
Consolidate two overlapping cache systems (broken TanStack persister + per-query Redis hash + pub/sub) into one: TanStack Query as single source of truth with Redis as its persistence layer via createAsyncStoragePersister.

- Replace QueryClientProvider with PersistQueryClientProvider backed by Redis
- Set global query defaults (staleTime: 0, refetchInterval: 30s, gcTime: 24h)
- Remove all per-widget staleTime/refetchInterval/retry/refetchOnWindowFocus overrides
- Replace cached request handlers with simplified fetch-only versions
- Remove all tRPC subscription procedures and useSubscription calls in favor of polling
- Delete 20 cron jobs that only pre-warmed server-side cache
- Simplify Redis from per-query hash to single blob per user+board
2026-06-24 14:55:17 +02:00

60 lines
2.1 KiB
TypeScript

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