diff --git a/.gitignore b/.gitignore index 881a5f0fa..c22262f6d 100644 --- a/.gitignore +++ b/.gitignore @@ -77,4 +77,4 @@ skills-lock.json # docusaurus apps/docs/.docusaurus apps/docs/build -.pnpm-store \ No newline at end of file +.pnpm-store diff --git a/apps/nextjs/src/components/board/items/widget-context-menu.tsx b/apps/nextjs/src/components/board/items/widget-context-menu.tsx index d7f4bdc06..415151a52 100644 --- a/apps/nextjs/src/components/board/items/widget-context-menu.tsx +++ b/apps/nextjs/src/components/board/items/widget-context-menu.tsx @@ -49,9 +49,10 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont const { gridstack } = useSectionContext().refs; const queryClient = useQueryClient(); + const widgetQueryKey = useMemo(() => [["widget", item.kind]], [item.kind]); const handleRefetch = useCallback(() => { - void queryClient.invalidateQueries({ queryKey: [["widget"]] }); - }, [queryClient]); + void queryClient.invalidateQueries({ queryKey: widgetQueryKey }); + }, [queryClient, widgetQueryKey]); const options = useMemo( () => reduceWidgetOptionsWithDefaultValues(item.kind, settings, item.options) as Record, @@ -226,7 +227,7 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont {tMenu("refresh")} - + @@ -264,7 +265,7 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont ); }; -const WidgetCacheAge = ({ queryClient }: { queryClient: QueryClient }) => { +const WidgetCacheAge = ({ queryClient, queryKey }: { queryClient: QueryClient; queryKey: unknown[] }) => { const [, setTick] = useState(0); useEffect(() => { const id = setInterval(() => setTick((n) => n + 1), 1000); @@ -273,12 +274,13 @@ const WidgetCacheAge = ({ queryClient }: { queryClient: QueryClient }) => { const timestamps = queryClient .getQueryCache() - .findAll({ queryKey: [["widget"]] }) + .findAll({ queryKey }) .map((q) => q.state.dataUpdatedAt) .filter(Boolean); if (timestamps.length === 0) return null; - const seconds = Math.floor((Date.now() - Math.max(...timestamps)) / 1000); + const oldest = Math.min(...timestamps); + const seconds = Math.floor((Date.now() - oldest) / 1000); if (seconds < 5) return "just now"; if (seconds < 60) return `${seconds}s ago`; return `${Math.floor(seconds / 60)}m ago`; diff --git a/packages/api/src/router/widgets/timetable.ts b/packages/api/src/router/widgets/timetable.ts index d04fa71bd..ef60ce320 100644 --- a/packages/api/src/router/widgets/timetable.ts +++ b/packages/api/src/router/widgets/timetable.ts @@ -19,14 +19,7 @@ export const timetableRouter = createTRPCRouter({ }), ) .query(async ({ input }) => { - const innerHandler = timetableGetTimetableRequestHandler.handler({ - baseUrl: input.baseUrl, - stationId: input.stationId, - limit: input.limit, - }); - - const { data } = await innerHandler.getDataAsync(); - + const { data } = await timetableGetTimetableRequestHandler.handler(input).getDataAsync(); return data; }), searchStations: publicProcedure @@ -37,13 +30,7 @@ export const timetableRouter = createTRPCRouter({ }), ) .query(async ({ input }) => { - const innerHandler = timetableSearchStationsRequestHandler.handler({ - baseUrl: input.baseUrl, - query: input.query, - }); - - const { data } = await innerHandler.getDataAsync(); - + const { data } = await timetableSearchStationsRequestHandler.handler(input).getDataAsync(); return data; }), }); diff --git a/packages/request-handler/src/lib/integration-request-handler.ts b/packages/request-handler/src/lib/integration-request-handler.ts index e2c0795ad..d2fa0f1a4 100644 --- a/packages/request-handler/src/lib/integration-request-handler.ts +++ b/packages/request-handler/src/lib/integration-request-handler.ts @@ -21,8 +21,10 @@ export const createIntegrationRequestHandler = < >( options: Options, ) => ({ - handler: (integration: IntegrationOfKind, itemOptions: TInput) => - createRequestHandler }>({ - requestAsync: async (input) => options.requestAsync(input.integration, input.options), - }).handler({ options: itemOptions, integration }), + handler: (integration: IntegrationOfKind, itemOptions: TInput) => { + const inner = createRequestHandler({ + requestAsync: async (input) => options.requestAsync(integration, input.options), + }); + return inner.handler({ integrationId: integration.id, options: itemOptions }); + }, }); diff --git a/packages/request-handler/src/lib/request-handler.ts b/packages/request-handler/src/lib/request-handler.ts index 4fc3bb361..7018dd4ee 100644 --- a/packages/request-handler/src/lib/request-handler.ts +++ b/packages/request-handler/src/lib/request-handler.ts @@ -5,11 +5,19 @@ interface Options> { type CacheEntry = { data: TData; timestamp: Date; expiresAt: number }; +const MAX_CACHE_SIZE = 1000; const cache = new Map>(); const inflight = new Map>>(); const DEFAULT_TTL_MS = 10_000; +const evictExpired = () => { + const now = Date.now(); + for (const [key, entry] of cache) { + if (now >= entry.expiresAt) cache.delete(key); + } +}; + export const createRequestHandler = >( options: Options, ) => ({ @@ -22,6 +30,7 @@ export const createRequestHandler = >; @@ -29,6 +38,11 @@ export const createRequestHandler = { + if (cache.size >= MAX_CACHE_SIZE) evictExpired(); + if (cache.size >= MAX_CACHE_SIZE) { + const oldest = cache.keys().next().value; + if (oldest) cache.delete(oldest); + } const entry: CacheEntry = { data, timestamp: new Date(), expiresAt: Date.now() + ttl }; cache.set(key, entry as CacheEntry); inflight.delete(key); diff --git a/packages/request-handler/src/update-checker.ts b/packages/request-handler/src/update-checker.ts index dc3ee18b8..1cf4dbd6f 100644 --- a/packages/request-handler/src/update-checker.ts +++ b/packages/request-handler/src/update-checker.ts @@ -5,24 +5,18 @@ import { env } from "@homarr/common/env"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { createRequestHandler } from "@homarr/request-handler/lib/request-handler"; +import { createRequestHandler } from "./lib/request-handler"; import packageJson from "../../../package.json"; const logger = createLogger({ module: "updateCheckerRequestHandler" }); -const CACHE_TTL_MS = 30 * 60 * 1000; -let cached: { data: Update[]; expiresAt: number } | null = null; - export const updateCheckerRequestHandler = createRequestHandler({ async requestAsync(_) { - if (cached && Date.now() < cached.expiresAt) { - return { availableUpdates: cached.data }; - } const availableUpdates = await getAvailableUpdatesAsync(packageJson.version); - cached = { data: availableUpdates, expiresAt: Date.now() + CACHE_TTL_MS }; return { availableUpdates }; }, + cacheTtlMs: 30 * 60 * 1000, }); interface Update {