From 6416080b79036df17655d7ac226d5a7e9a179402 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 24 Jun 2026 14:55:17 +0200 Subject: [PATCH] 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 --- .gitignore | 1 + apps/nextjs/package.json | 3 +- .../query-cache-persister.ts | 46 ++--- .../app/[locale]/_client-providers/trpc.tsx | 33 ++-- .../[locale]/boards/(content)/_creator.tsx | 34 +--- .../manage/tools/docker/docker-table.tsx | 15 +- .../invalidate-update-checker-cache.ts | 4 +- packages/api/src/query-cache.ts | 7 +- .../api/src/router/docker/docker-router.ts | 36 +--- packages/api/src/router/query-cache.ts | 36 +--- .../router/test/docker/docker-router.spec.ts | 12 +- packages/api/src/router/update-checker.ts | 2 +- .../api/src/router/widgets/anchor-notes.ts | 11 +- packages/api/src/router/widgets/app.ts | 53 ------ .../router/widgets/archive-team-warrior.ts | 23 +-- .../api/src/router/widgets/audio-stats.ts | 4 +- packages/api/src/router/widgets/beszel.ts | 128 +------------- packages/api/src/router/widgets/calendar.ts | 44 +---- packages/api/src/router/widgets/coolify.ts | 25 +-- packages/api/src/router/widgets/dns-hole.ts | 48 +----- packages/api/src/router/widgets/downloads.ts | 38 +---- packages/api/src/router/widgets/firewall.ts | 123 +------------- .../src/router/widgets/health-monitoring.ts | 56 +----- packages/api/src/router/widgets/immich.ts | 6 +- .../api/src/router/widgets/indexer-manager.ts | 27 +-- .../api/src/router/widgets/media-release.ts | 35 +--- .../api/src/router/widgets/media-requests.ts | 38 +---- .../api/src/router/widgets/media-server.ts | 32 +--- .../src/router/widgets/media-transcoding.ts | 20 +-- packages/api/src/router/widgets/minecraft.ts | 19 +-- .../src/router/widgets/network-controller.ts | 35 +--- .../api/src/router/widgets/notifications.ts | 34 +--- .../api/src/router/widgets/paperless-ngx.ts | 2 +- packages/api/src/router/widgets/releases.ts | 4 +- packages/api/src/router/widgets/rssFeed.ts | 21 +-- packages/api/src/router/widgets/smart-home.ts | 28 +-- .../src/router/widgets/speedtest-tracker.ts | 30 +--- packages/api/src/router/widgets/stocks.ts | 2 +- packages/api/src/router/widgets/timetable.ts | 4 +- packages/api/src/router/widgets/tracearr.ts | 25 +-- packages/api/src/router/widgets/umami.ts | 30 +--- packages/api/src/router/widgets/ups.ts | 25 +-- .../api/src/router/widgets/uptime-kuma.ts | 30 +--- packages/api/src/router/widgets/weather.ts | 14 +- packages/api/src/services/duckduckgo-bangs.ts | 2 +- packages/api/src/test/query-cache.spec.ts | 120 +++---------- packages/cron-jobs/src/index.ts | 50 ------ packages/cron-jobs/src/jobs/docker.ts | 31 ---- .../src/jobs/integrations/dns-hole.ts | 15 -- .../src/jobs/integrations/downloads.ts | 16 -- .../src/jobs/integrations/firewall.ts | 46 ----- .../jobs/integrations/health-monitoring.ts | 33 ---- .../src/jobs/integrations/home-assistant.ts | 16 -- .../src/jobs/integrations/indexer-manager.ts | 14 -- .../src/jobs/integrations/media-organizer.ts | 36 ---- .../src/jobs/integrations/media-requests.ts | 24 --- .../src/jobs/integrations/media-server.ts | 14 -- .../jobs/integrations/media-transcoding.ts | 14 -- .../jobs/integrations/network-controller.ts | 14 -- .../src/jobs/integrations/notifications.ts | 14 -- .../src/jobs/integrations/timetable.ts | 20 --- .../src/jobs/integrations/tracearr.ts | 14 -- .../cron-jobs/src/jobs/integrations/umami.ts | 14 -- .../src/jobs/minecraft-server-status.ts | 25 --- packages/cron-jobs/src/jobs/rss-feeds.ts | 39 ----- packages/cron-jobs/src/jobs/update-checker.ts | 11 -- packages/cron-jobs/src/jobs/weather.ts | 36 ---- packages/redis/src/index.ts | 9 +- packages/redis/src/lib/channel.ts | 87 +++------- packages/request-handler/src/anchor-notes.ts | 9 +- .../src/archive-team-warrior.ts | 6 +- .../request-handler/src/audiobookshelf.ts | 6 +- packages/request-handler/src/beszel.ts | 21 +-- packages/request-handler/src/calendar.ts | 5 +- packages/request-handler/src/coolify.ts | 6 +- packages/request-handler/src/dns-hole.ts | 6 +- packages/request-handler/src/docker.ts | 6 +- packages/request-handler/src/downloads.ts | 6 +- .../request-handler/src/duckduckgo-bangs.ts | 10 +- packages/request-handler/src/firewall.ts | 15 +- .../request-handler/src/health-monitoring.ts | 9 +- packages/request-handler/src/immich.ts | 12 +- .../request-handler/src/indexer-manager.ts | 6 +- .../src/lib/cached-request-handler.ts | 160 ------------------ .../cached-request-integration-job-handler.ts | 115 ------------- .../src/lib/cached-widget-request-handler.ts | 36 ---- ...dler.ts => integration-request-handler.ts} | 21 +-- .../src/lib/request-handler.ts | 59 +++++++ .../src/lib/widget-request-handler.ts | 25 +++ packages/request-handler/src/media-release.ts | 6 +- .../request-handler/src/media-request-list.ts | 6 +- .../src/media-request-stats.ts | 6 +- packages/request-handler/src/media-server.ts | 6 +- .../request-handler/src/media-transcoding.ts | 6 +- .../src/minecraft-server-status.ts | 6 +- packages/request-handler/src/navidrome.ts | 6 +- .../request-handler/src/network-controller.ts | 6 +- packages/request-handler/src/notifications.ts | 6 +- packages/request-handler/src/paperless-ngx.ts | 6 +- packages/request-handler/src/releases.ts | 6 +- packages/request-handler/src/rss-feeds.ts | 6 +- .../src/smart-home-entity-state.ts | 6 +- .../request-handler/src/speedtest-tracker.ts | 6 +- packages/request-handler/src/stock-price.ts | 6 +- packages/request-handler/src/timetable.ts | 9 +- packages/request-handler/src/tracearr.ts | 6 +- packages/request-handler/src/umami.ts | 21 +-- .../request-handler/src/update-checker.ts | 13 +- packages/request-handler/src/ups.ts | 6 +- packages/request-handler/src/uptime-kuma.ts | 6 +- packages/request-handler/src/weather.ts | 6 +- packages/translation/src/lang/en.json | 72 -------- .../src/_inputs/widget-anchor-note-input.tsx | 3 - ...widget-multiReleasesRepositories-input.tsx | 4 - .../widgets/src/anchor-note/component.tsx | 16 +- packages/widgets/src/app/component.tsx | 13 +- .../widgets/src/app/ping/ping-indicator.tsx | 12 +- .../src/archive-team-warrior/component.tsx | 15 +- .../widgets/src/audio-stats/component.tsx | 7 +- .../widgets/src/beszel-alerts/component.tsx | 26 +-- .../src/beszel-system-grid/component.tsx | 8 +- .../src/beszel-system-stats/component.tsx | 4 +- .../widgets/src/beszel-system-stats/index.ts | 2 +- .../src/beszel-system-table/component.tsx | 8 +- packages/widgets/src/beszel/_shared/hooks.ts | 18 -- .../src/beszel/_shared/system-stats-modal.tsx | 1 - .../src/beszel/_shared/use-live-stats.ts | 96 ++--------- packages/widgets/src/calendar/component.tsx | 22 +-- packages/widgets/src/coolify/component.tsx | 22 +-- .../src/dns-hole/controls/component.tsx | 46 +---- .../src/dns-hole/summary/component.tsx | 40 +---- packages/widgets/src/docker/component.tsx | 11 +- packages/widgets/src/downloads/component.tsx | 45 +---- packages/widgets/src/firewall/component.tsx | 155 ++--------------- .../cluster/cluster-health.tsx | 24 +-- .../src/health-monitoring/system-health.tsx | 33 +--- .../src/immich/album-carousel/component.tsx | 2 +- .../src/immich/server-stats/component.tsx | 9 +- .../widgets/src/indexer-manager/component.tsx | 25 +-- .../widgets/src/media-releases/component.tsx | 9 +- .../src/media-requests/list/component.tsx | 41 +---- .../src/media-requests/stats/component.tsx | 13 +- .../widgets/src/media-server/component.tsx | 42 +---- .../src/media-transcoding/component.tsx | 13 +- .../src/minecraft/server-status/component.tsx | 13 +- .../network-status/component.tsx | 45 +---- .../network-controller/summary/component.tsx | 39 +---- .../widgets/src/notifications/component.tsx | 41 +---- .../widgets/src/paperless-ngx/component.tsx | 7 +- packages/widgets/src/rssFeed/component.tsx | 24 +-- .../src/smart-home/entity-state/component.tsx | 12 +- .../src/speedtest-tracker/component.tsx | 23 +-- packages/widgets/src/stocks/component.tsx | 4 +- .../widgets/src/system-disks/component.tsx | 13 +- .../src/system-resources/component.tsx | 73 ++++---- packages/widgets/src/timetable/component.tsx | 15 +- packages/widgets/src/tracearr/component.tsx | 23 +-- packages/widgets/src/umami/umami-content.tsx | 27 +-- .../src/umami/umami-events-content.tsx | 15 +- packages/widgets/src/umami/umami-top-list.tsx | 30 ++-- packages/widgets/src/ups/component.tsx | 20 +-- .../widgets/src/uptime-kuma/component.tsx | 24 +-- packages/widgets/src/weather/component.tsx | 11 +- pnpm-lock.yaml | 30 +++- pnpm-workspace.yaml | 3 +- 165 files changed, 546 insertions(+), 3456 deletions(-) delete mode 100644 packages/cron-jobs/src/jobs/docker.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/dns-hole.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/downloads.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/firewall.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/health-monitoring.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/home-assistant.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/indexer-manager.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/media-organizer.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/media-requests.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/media-server.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/media-transcoding.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/network-controller.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/notifications.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/timetable.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/tracearr.ts delete mode 100644 packages/cron-jobs/src/jobs/integrations/umami.ts delete mode 100644 packages/cron-jobs/src/jobs/minecraft-server-status.ts delete mode 100644 packages/cron-jobs/src/jobs/rss-feeds.ts delete mode 100644 packages/cron-jobs/src/jobs/update-checker.ts delete mode 100644 packages/cron-jobs/src/jobs/weather.ts delete mode 100644 packages/request-handler/src/lib/cached-request-handler.ts delete mode 100644 packages/request-handler/src/lib/cached-request-integration-job-handler.ts delete mode 100644 packages/request-handler/src/lib/cached-widget-request-handler.ts rename packages/request-handler/src/lib/{cached-integration-request-handler.ts => integration-request-handler.ts} (53%) create mode 100644 packages/request-handler/src/lib/request-handler.ts create mode 100644 packages/request-handler/src/lib/widget-request-handler.ts diff --git a/.gitignore b/.gitignore index 1bb62e979..d446ca981 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,4 @@ apps/nextjs/public/images/background.png # docusaurus apps/docs/.docusaurus apps/docs/build +.pnpm-store \ No newline at end of file diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index cfea600e7..c9346beb3 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -59,7 +59,8 @@ "@modelcontextprotocol/sdk": "catalog:", "@scalar/api-reference-react": "catalog:", "@tabler/icons-react": "catalog:", - "@tanstack/query-persist-client-core": "catalog:", + "@tanstack/query-async-storage-persister": "catalog:", + "@tanstack/react-query-persist-client": "catalog:", "@tanstack/react-query": "catalog:", "@tanstack/react-query-devtools": "catalog:", "@tanstack/react-query-next-experimental": "catalog:", diff --git a/apps/nextjs/src/app/[locale]/_client-providers/query-cache-persister.ts b/apps/nextjs/src/app/[locale]/_client-providers/query-cache-persister.ts index 70d8581c7..6e72f26e3 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/query-cache-persister.ts +++ b/apps/nextjs/src/app/[locale]/_client-providers/query-cache-persister.ts @@ -1,49 +1,25 @@ -import type { AsyncStorage, PersistedQuery } from "@tanstack/query-persist-client-core"; -import { experimental_createQueryPersister } from "@tanstack/query-persist-client-core"; -import { parse, stringify } from "superjson"; +import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"; import { fetchApi } from "@homarr/api/client"; -import { - getActiveQueryCacheBoardId, - isPersistableWidgetQueryKey, - queryCacheStoragePrefix, -} from "@homarr/api/query-cache"; +import { getActiveQueryCacheBoardId, queryCacheStoragePrefix } from "@homarr/api/query-cache"; const queryCacheStorage = { - async getItem(_key) { - return null; - }, - async setItem(key, value) { + getItem: (_key: string) => null as string | null, + async setItem(_key: string, value: string) { const boardId = getActiveQueryCacheBoardId(); if (!boardId) return; - - try { - await fetchApi.queryCache.setItem.mutate({ boardId, key, value }); - } catch { - // Query persistence is best-effort; failed writes should not affect widget rendering. - } + await fetchApi.queryCache.setItem.mutate({ boardId, value }).catch(() => {}); }, - async removeItem(key) { + async removeItem(_key: string) { const boardId = getActiveQueryCacheBoardId(); if (!boardId) return; - - try { - await fetchApi.queryCache.removeItem.mutate({ boardId, key }); - } catch { - // Query persistence is best-effort; failed removals should not affect widget rendering. - } + await fetchApi.queryCache.removeItem.mutate({ boardId }).catch(() => {}); }, -} satisfies AsyncStorage; +}; export const createWidgetQueryPersister = () => - experimental_createQueryPersister({ + createAsyncStoragePersister({ storage: typeof window === "undefined" ? undefined : queryCacheStorage, - maxAge: Number.POSITIVE_INFINITY, - prefix: queryCacheStoragePrefix, - refetchOnRestore: true, - serialize: (query) => stringify(query), - deserialize: (value) => parse(value), - filters: { - predicate: (query) => isPersistableWidgetQueryKey(query.queryKey), - }, + key: queryCacheStoragePrefix, + throttleTime: 2000, }); diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx index d09acd8c8..0abe33ae9 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx @@ -2,9 +2,10 @@ import type { PropsWithChildren } from "react"; import { useState } from "react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { QueryClient } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental"; +import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; import { createWSClient, httpBatchStreamLink, @@ -21,7 +22,7 @@ import { TRPCClientError } from "@trpc/client"; import type { AppRouter } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; -import { queryCacheDefaultStaleTimeMs } from "@homarr/api/query-cache"; +import { isPersistableWidgetQueryKey, queryCacheDefaultGcTimeMs, queryCacheDefaultRefetchIntervalMs, queryCacheDefaultStaleTimeMs } from "@homarr/api/query-cache"; import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared"; import { env } from "@homarr/common/env"; import { showWarningNotification } from "@homarr/notifications"; @@ -29,7 +30,6 @@ import { showWarningNotification } from "@homarr/notifications"; import { createWidgetQueryPersister } from "./query-cache-persister"; const getWebSocketProtocol = () => { - // window is not defined on server side if (typeof window === "undefined") { return "ws"; } @@ -55,14 +55,18 @@ const wsClient = createWSClient({ }); export function TRPCReactProvider(props: PropsWithChildren) { - const [queryPersister] = useState(() => createWidgetQueryPersister()); + const [persister] = useState(() => createWidgetQueryPersister()); const [queryClient] = useState( () => new QueryClient({ defaultOptions: { queries: { - persister: queryPersister.persisterFn, staleTime: queryCacheDefaultStaleTimeMs, + refetchInterval: queryCacheDefaultRefetchIntervalMs, + gcTime: queryCacheDefaultGcTimeMs, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, }, mutations: { onError(error) { @@ -98,9 +102,6 @@ export function TRPCReactProvider(props: PropsWithChildren) { false: splitLink({ condition: ({ input }) => isNonJsonSerializable(input), true: httpLink({ - /** - * We don't want to transform the data here as we want to use form data - */ transformer: { serialize(object: unknown) { return object; @@ -115,7 +116,7 @@ export function TRPCReactProvider(props: PropsWithChildren) { false: httpBatchStreamLink({ transformer: superjson, url: getTrpcUrl(), - maxURLLength: 2083, // Suggested by tRPC: https://trpc.io/docs/client/links/httpBatchLink#setting-a-maximum-url-length + maxURLLength: 2083, headers: createHeadersCallbackForSource("nextjs-react (json)"), }), }), @@ -126,10 +127,20 @@ export function TRPCReactProvider(props: PropsWithChildren) { return ( - + + query.state.status === "success" && isPersistableWidgetQueryKey(query.queryKey), + }, + }} + > {props.children} - + ); } diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx index 2144304c5..ad16c2e92 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx @@ -5,8 +5,8 @@ import { TRPCError } from "@trpc/server"; // Placed here because gridstack styles are used for board content import "~/styles/gridstack.scss"; -import type { DehydratedState } from "@tanstack/react-query"; import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; +import type { PersistedClient } from "@tanstack/react-query-persist-client"; import { parse } from "superjson"; import { getQueryClient } from "@homarr/api/server"; @@ -17,7 +17,7 @@ import { isNullOrWhitespace } from "@homarr/common"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import type { WidgetKind } from "@homarr/definitions"; -import { getAllQueryCacheAsync } from "@homarr/redis"; +import { getQueryCacheAsync } from "@homarr/redis"; import { getI18n } from "@homarr/translation/server"; import { prefetchForKindAsync } from "@homarr/widgets/prefetch"; @@ -118,35 +118,15 @@ export const createBoardContentPage = >( }; }; -interface PersistedEntry { - state: { data: unknown; dataUpdatedAt: number; status: string }; - queryKey: unknown[]; - queryHash: string; -} - async function QueryCacheHydration({ userId, boardId }: { userId: string; boardId: string }) { try { - const entries = await getAllQueryCacheAsync(userId, boardId); - const queries: DehydratedState["queries"] = []; + const serialized = await getQueryCacheAsync(userId, boardId); + if (!serialized) return null; - for (const serialized of Object.values(entries)) { - try { - const persisted = parse(serialized); - if (persisted.state.status !== "success") continue; + const persisted = parse(serialized); + if (!persisted?.clientState?.queries?.length) return null; - queries.push({ - state: persisted.state as DehydratedState["queries"][number]["state"], - queryKey: persisted.queryKey, - queryHash: persisted.queryHash, - }); - } catch { - // skip malformed entries - } - } - - if (queries.length === 0) return null; - - return ; + return ; } catch { return null; } diff --git a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx index d2c1def97..3f846e5ed 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/docker/docker-table.tsx @@ -1,5 +1,6 @@ "use client"; +import { useCallback, useState } from "react"; import { useRouter } from "next/navigation"; import type { MantineColor } from "@mantine/core"; import { ActionIcon, Avatar, Badge, Box, Button, Group, Menu, Text, Tooltip } from "@mantine/core"; @@ -160,21 +161,21 @@ export function DockerTable({ containers, timestamp }: RouterOutputs["docker"][" }); const relativeTime = useTimeAgo(data.timestamp); const utils = clientApi.useUtils(); - const { mutate, isPending } = clientApi.docker.invalidate.useMutation({ - async onSuccess() { - await utils.docker.getContainers.invalidate(); + const [isPending, setIsPending] = useState(false); + const mutate = useCallback(() => { + setIsPending(true); + void utils.docker.getContainers.invalidate().then(() => { showSuccessNotification({ title: tDocker("action.refresh.notification.success.title"), message: tDocker("action.refresh.notification.success.message"), }); - }, - onError() { + }).catch(() => { showErrorNotification({ title: tDocker("action.refresh.notification.error.title"), message: tDocker("action.refresh.notification.error.message"), }); - }, - }); + }).finally(() => setIsPending(false)); + }, [utils, tDocker]); const table = useTranslatedMantineReactTable({ data: data.containers, diff --git a/apps/tasks/src/on-start/invalidate-update-checker-cache.ts b/apps/tasks/src/on-start/invalidate-update-checker-cache.ts index 470989dcf..de1be2a81 100644 --- a/apps/tasks/src/on-start/invalidate-update-checker-cache.ts +++ b/apps/tasks/src/on-start/invalidate-update-checker-cache.ts @@ -10,8 +10,8 @@ const logger = createLogger({ module: "invalidateUpdateCheckerCache" }); export async function invalidateUpdateCheckerCacheAsync() { try { const handler = updateCheckerRequestHandler.handler({}); - await handler.invalidateAsync(); - logger.debug("Update checker cache invalidated"); + await handler.getDataAsync(); + logger.debug("Update checker cache refreshed"); } catch (error) { logger.error(new Error("Failed to invalidate update checker cache", { cause: error })); } diff --git a/packages/api/src/query-cache.ts b/packages/api/src/query-cache.ts index 690059723..79f08f0be 100644 --- a/packages/api/src/query-cache.ts +++ b/packages/api/src/query-cache.ts @@ -1,7 +1,10 @@ import type { QueryKey } from "@tanstack/react-query"; -export const queryCacheRetentionMs = 1000 * 60 * 60 * 24; -export const queryCacheDefaultStaleTimeMs = 1000 * 60 * 5; +// ponytail: staleTime 0 = always refetch on mount (fresh data every dashboard load). +// refetchInterval 30s = poll while page is open. gcTime 24h = Redis TTL for persisted cache. +export const queryCacheDefaultStaleTimeMs = 0; +export const queryCacheDefaultRefetchIntervalMs = 30_000; +export const queryCacheDefaultGcTimeMs = 1000 * 60 * 60 * 24; export const queryCacheMaxValueBytes = 1024 * 1024; export const queryCacheStoragePrefix = "homarr-widget-query"; diff --git a/packages/api/src/router/docker/docker-router.ts b/packages/api/src/router/docker/docker-router.ts index 0011545d0..44b4ce381 100644 --- a/packages/api/src/router/docker/docker-router.ts +++ b/packages/api/src/router/docker/docker-router.ts @@ -25,9 +25,7 @@ export const dockerRouter = createTRPCRouter({ .concat(dockerMiddleware()) .query(async () => { const innerHandler = dockerContainersRequestHandler.handler({}); - const result = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const result = await innerHandler.getDataAsync(); const { data, timestamp } = result; @@ -36,26 +34,6 @@ export const dockerRouter = createTRPCRouter({ timestamp, }; }), - subscribeContainers: permissionRequiredProcedure - .requiresPermission("admin") - .concat(dockerMiddleware()) - .subscription(() => { - return observable((emit) => { - const innerHandler = dockerContainersRequestHandler.handler({}); - const unsubscribe = innerHandler.subscribe((data) => { - emit.next(data); - }); - - return unsubscribe; - }); - }), - invalidate: permissionRequiredProcedure - .requiresPermission("admin") - .concat(dockerMiddleware()) - .mutation(async () => { - const innerHandler = dockerContainersRequestHandler.handler({}); - await innerHandler.invalidateAsync(); - }), startAll: permissionRequiredProcedure .requiresPermission("admin") .meta({ @@ -73,9 +51,6 @@ export const dockerRouter = createTRPCRouter({ await container.start(); }), ); - - const innerHandler = dockerContainersRequestHandler.handler({}); - await innerHandler.invalidateAsync(); }), stopAll: permissionRequiredProcedure .requiresPermission("admin") @@ -94,9 +69,6 @@ export const dockerRouter = createTRPCRouter({ await container.stop(); }), ); - - const innerHandler = dockerContainersRequestHandler.handler({}); - await innerHandler.invalidateAsync(); }), restartAll: permissionRequiredProcedure .requiresPermission("admin") @@ -116,9 +88,6 @@ export const dockerRouter = createTRPCRouter({ await container.restart(); }), ); - - const innerHandler = dockerContainersRequestHandler.handler({}); - await innerHandler.invalidateAsync(); }), removeAll: permissionRequiredProcedure .requiresPermission("admin") @@ -138,9 +107,6 @@ export const dockerRouter = createTRPCRouter({ await container.remove(); }), ); - - const innerHandler = dockerContainersRequestHandler.handler({}); - await innerHandler.invalidateAsync(); }), logs: permissionRequiredProcedure .requiresPermission("admin") diff --git a/packages/api/src/router/query-cache.ts b/packages/api/src/router/query-cache.ts index 6143501fc..910525ac9 100644 --- a/packages/api/src/router/query-cache.ts +++ b/packages/api/src/router/query-cache.ts @@ -2,53 +2,35 @@ import { z } from "zod/v4"; import { eq } from "@homarr/db"; import { boards } from "@homarr/db/schema"; -import { createQueryCacheChannel } from "@homarr/redis"; +import { removeQueryCacheAsync, setQueryCacheAsync } from "@homarr/redis"; -import { queryCacheMaxValueBytes, queryCacheRetentionMs } from "../query-cache"; +import { queryCacheDefaultGcTimeMs, queryCacheMaxValueBytes } from "../query-cache"; import { createTRPCRouter, publicProcedure } from "../trpc"; import { throwIfActionForbiddenAsync } from "./board/board-access"; -const queryCacheKeyInput = z.object({ +const queryCacheBoardInput = z.object({ boardId: z.string().min(1), - key: z.string().min(1).max(4096), }); -const queryCacheSetInput = queryCacheKeyInput.extend({ +const queryCacheSetInput = queryCacheBoardInput.extend({ value: z.string().max(queryCacheMaxValueBytes * 2), }); -const createChannel = (input: z.infer, userId: string) => - createQueryCacheChannel({ - userId, - boardId: input.boardId, - key: input.key, - ttlMs: queryCacheRetentionMs, - maxValueBytes: queryCacheMaxValueBytes, - }); - const validateBoardAccessAsync = async (ctx: Parameters[0], boardId: string) => { await throwIfActionForbiddenAsync(ctx, eq(boards.id, boardId), "view"); }; export const queryCacheRouter = createTRPCRouter({ - getItem: publicProcedure.input(queryCacheKeyInput).query(async ({ ctx, input }) => { - await validateBoardAccessAsync(ctx, input.boardId); - - const channel = createChannel(input, ctx.session?.user.id ?? "anonymous"); - return await channel.getAsync(); - }), setItem: publicProcedure.input(queryCacheSetInput).mutation(async ({ ctx, input }) => { await validateBoardAccessAsync(ctx, input.boardId); - - const channel = createChannel(input, ctx.session?.user.id ?? "anonymous"); + const userId = ctx.session?.user.id ?? "anonymous"; return { - stored: await channel.setAsync(input.value), + stored: await setQueryCacheAsync(userId, input.boardId, input.value, queryCacheDefaultGcTimeMs), }; }), - removeItem: publicProcedure.input(queryCacheKeyInput).mutation(async ({ ctx, input }) => { + removeItem: publicProcedure.input(queryCacheBoardInput).mutation(async ({ ctx, input }) => { await validateBoardAccessAsync(ctx, input.boardId); - - const channel = createChannel(input, ctx.session?.user.id ?? "anonymous"); - await channel.removeAsync(); + const userId = ctx.session?.user.id ?? "anonymous"; + await removeQueryCacheAsync(userId, input.boardId); }), }); diff --git a/packages/api/src/router/test/docker/docker-router.spec.ts b/packages/api/src/router/test/docker/docker-router.spec.ts index cbea4063b..a40f5e4dd 100644 --- a/packages/api/src/router/test/docker/docker-router.spec.ts +++ b/packages/api/src/router/test/docker/docker-router.spec.ts @@ -23,26 +23,20 @@ vi.mock("@homarr/request-handler/docker", () => ({ }, dockerContainersRequestHandler: { handler: () => ({ - getCachedOrUpdatedDataAsync: async () => { - return await Promise.resolve({ containers: [] }); - }, - invalidateAsync: async () => { - return await Promise.resolve(); + getDataAsync: async () => { + return await Promise.resolve({ data: [], timestamp: new Date() }); }, }), }, })); vi.mock("@homarr/redis", () => ({ createCacheChannel: () => ({ - // eslint-disable-next-line @typescript-eslint/require-await consumeAsync: async () => ({ timestamp: new Date().toISOString(), data: { containers: [] }, }), - // eslint-disable-next-line @typescript-eslint/no-empty-function invalidateAsync: async () => {}, }), - createWidgetOptionsChannel: () => ({}), })); vi.mock("@homarr/docker/env", () => ({ @@ -67,12 +61,10 @@ const validInputs: { [key in (typeof procedureKeys)[number]]: RouterInputs["docker"][key]; } = { getContainers: undefined, - subscribeContainers: undefined, startAll: { ids: ["1"] }, stopAll: { ids: ["1"] }, restartAll: { ids: ["1"] }, removeAll: { ids: ["1"] }, - invalidate: undefined, logs: { id: "1", tail: 200 }, subscribeLogs: { id: "1", tail: 200 }, }; diff --git a/packages/api/src/router/update-checker.ts b/packages/api/src/router/update-checker.ts index 8957be5f2..8cb6c097e 100644 --- a/packages/api/src/router/update-checker.ts +++ b/packages/api/src/router/update-checker.ts @@ -9,7 +9,7 @@ export const updateCheckerRouter = createTRPCRouter({ getAvailableUpdates: permissionRequiredProcedure.requiresPermission("admin").query(async () => { try { const handler = updateCheckerRequestHandler.handler({}); - const data = await handler.getCachedOrUpdatedDataAsync({}); + const data = await handler.getDataAsync(); return data.data.availableUpdates; } catch (error) { logger.error(new Error("Failed to get available updates", { cause: error })); diff --git a/packages/api/src/router/widgets/anchor-notes.ts b/packages/api/src/router/widgets/anchor-notes.ts index 7e5b7043c..d431c0de9 100644 --- a/packages/api/src/router/widgets/anchor-notes.ts +++ b/packages/api/src/router/widgets/anchor-notes.ts @@ -45,9 +45,7 @@ export const anchorNotesRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { const handler = anchorNotesListRequestHandler.handler(ctx.integration, { ...input, limit: input.limit ?? 50 }); - const { data } = await handler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await handler.getDataAsync(); return data; }), @@ -57,9 +55,7 @@ export const anchorNotesRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { const handler = anchorNoteRequestHandler.handler(ctx.integration, { noteId: input.noteId }); - const { data } = await handler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await handler.getDataAsync(); return data; }), @@ -79,9 +75,6 @@ export const anchorNotesRouter = createTRPCRouter({ ...(normalizedContent !== undefined ? { content: normalizedContent } : {}), }); - const noteHandler = anchorNoteRequestHandler.handler(ctx.integration, { noteId: input.noteId }); - await noteHandler.invalidateAsync(); - return updatedNote; } catch (error) { if (error instanceof ResponseError && error.statusCode === 400) { diff --git a/packages/api/src/router/widgets/app.ts b/packages/api/src/router/widgets/app.ts index d07241725..92e1298da 100644 --- a/packages/api/src/router/widgets/app.ts +++ b/packages/api/src/router/widgets/app.ts @@ -1,10 +1,8 @@ import { TRPCError } from "@trpc/server"; -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { sendPingRequestAsync } from "@homarr/ping"; -import { pingChannel, pingUrlChannel } from "@homarr/redis"; import { createTRPCRouter, publicProcedure } from "../../trpc"; import { AppRepository } from "../app"; @@ -45,55 +43,4 @@ export const appRouter = createTRPCRouter({ ...pingResult, }; }), - updatedPing: publicProcedure - .input( - z.object({ - id: z.string(), - }), - ) - .subscription(async ({ ctx, input }) => { - const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board"); - if (boardSettings.forceDisableStatus) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "Ping functionality is disabled by server settings", - }); - } - - const repository = new AppRepository(ctx.db, ctx.session?.user ?? null); - const app = await repository.getByIdAsync(input.id); - - if (!app) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "App not found", - }); - } - - const pingUrl = app.pingUrl ?? app.href; - - if (!pingUrl) { - throw new TRPCError({ - code: "CONFLICT", - message: "No URL to ping configured for this app", - }); - } - - await pingUrlChannel.addAsync(pingUrl); - - return observable<{ url: string; statusCode: number; durationMs: number } | { url: string; error: string }>( - (emit) => { - const unsubscribe = pingChannel.subscribe((message) => { - // Only emit if same url - if (message.url !== pingUrl) return; - emit.next(message); - }); - - return () => { - unsubscribe(); - void pingUrlChannel.removeAsync(pingUrl); - }; - }, - ); - }), }); diff --git a/packages/api/src/router/widgets/archive-team-warrior.ts b/packages/api/src/router/widgets/archive-team-warrior.ts index 0824c165a..d71b7546a 100644 --- a/packages/api/src/router/widgets/archive-team-warrior.ts +++ b/packages/api/src/router/widgets/archive-team-warrior.ts @@ -1,6 +1,3 @@ -import { observable } from "@trpc/server/observable"; - -import type { ArchiveTeamWarriorStatus } from "@homarr/integrations"; import { archiveTeamWarriorRequestHandler } from "@homarr/request-handler/archive-team-warrior"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; @@ -11,29 +8,11 @@ export const archiveTeamWarriorRouter = createTRPCRouter({ .concat(createOneIntegrationMiddleware("query", "archiveTeamWarrior")) .query(async ({ ctx }) => { const handler = archiveTeamWarriorRequestHandler.handler(ctx.integration, {}); - const { data, timestamp } = await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await handler.getDataAsync(); return { status: data, updatedAt: timestamp, }; }), - - subscribeStatus: publicProcedure - .concat(createOneIntegrationMiddleware("query", "archiveTeamWarrior")) - .subscription(({ ctx }) => { - return observable<{ status: ArchiveTeamWarriorStatus; updatedAt: Date }>((emit) => { - const handler = archiveTeamWarriorRequestHandler.handler(ctx.integration, {}); - const unsubscribe = handler.subscribe((status) => { - emit.next({ - status, - updatedAt: new Date(), - }); - }); - - return () => { - unsubscribe(); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/audio-stats.ts b/packages/api/src/router/widgets/audio-stats.ts index d43876583..14fd7d49c 100644 --- a/packages/api/src/router/widgets/audio-stats.ts +++ b/packages/api/src/router/widgets/audio-stats.ts @@ -13,14 +13,14 @@ export const audioStatsRouter = createTRPCRouter({ const fetchByKind = { navidrome: async () => { const handler = navidromeRequestHandler.handler({ ...ctx.integration, kind: "navidrome" as const }, {}); - return (await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false })).data; + return (await handler.getDataAsync()).data; }, audiobookshelf: async () => { const handler = audiobookshelfRequestHandler.handler( { ...ctx.integration, kind: "audiobookshelf" as const }, {}, ); - return (await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false })).data; + return (await handler.getDataAsync()).data; }, } as const; diff --git a/packages/api/src/router/widgets/beszel.ts b/packages/api/src/router/widgets/beszel.ts index cbb453d03..54781518d 100644 --- a/packages/api/src/router/widgets/beszel.ts +++ b/packages/api/src/router/widgets/beszel.ts @@ -1,16 +1,12 @@ import { TRPCError } from "@trpc/server"; -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { createLogger } from "@homarr/core/infrastructure/logs"; -import { createIntegrationAsync } from "@homarr/integrations"; -import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types"; import { beszelAlertsRequestHandler, beszelStatsRequestHandler, beszelSystemsRequestHandler, } from "@homarr/request-handler/beszel"; -import type { BeszelAlertsData, BeszelSystemRow } from "@homarr/request-handler/beszel"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; import { createTRPCRouter, publicProcedure } from "../../trpc"; @@ -33,9 +29,7 @@ export const beszelRouter = createTRPCRouter({ const settled = await Promise.allSettled( ctx.integrations.map(async (integration) => { const innerHandler = beszelSystemsRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integrationId: integration.id, integrationName: integration.name, @@ -71,30 +65,6 @@ export const beszelRouter = createTRPCRouter({ return results; }), - subscribeSystems: publicProcedure - .concat(createManyIntegrationMiddleware("query", "beszel", "mock")) - .subscription(({ ctx }) => { - return observable<{ - integrationId: string; - systems: BeszelSystemRow[]; - timestamp: Date; - }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = beszelSystemsRequestHandler.handler(integration, {}); - return innerHandler.subscribe((systems) => { - emit.next({ - integrationId: integration.id, - systems, - timestamp: new Date(), - }); - }); - }); - return () => { - unsubscribes.forEach((fn) => fn()); - }; - }); - }), - getAlerts: publicProcedure .meta({ mcp: { @@ -126,8 +96,8 @@ export const beszelRouter = createTRPCRouter({ }); const systemsHandler = beszelSystemsRequestHandler.handler(integration, {}); const [alertsResult, systemsResult] = await Promise.all([ - alertsHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }), - systemsHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }), + alertsHandler.getDataAsync(), + systemsHandler.getDataAsync(), ]); const systemNameMap: Record = {}; for (const system of systemsResult.data) { @@ -170,39 +140,6 @@ export const beszelRouter = createTRPCRouter({ return results; }), - subscribeAlerts: publicProcedure - .concat(createManyIntegrationMiddleware("query", "beszel", "mock")) - .input( - z.object({ - includeHistory: z.boolean().default(true), - maxHistoryItems: z.number().min(1).max(100).default(10), - }), - ) - .subscription(({ ctx, input }) => { - return observable<{ - integrationId: string; - alerts: BeszelAlertsData; - timestamp: Date; - }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = beszelAlertsRequestHandler.handler(integration, { - includeHistory: input.includeHistory, - maxHistoryItems: input.maxHistoryItems, - }); - return innerHandler.subscribe((data) => { - emit.next({ - integrationId: integration.id, - alerts: data, - timestamp: new Date(), - }); - }); - }); - return () => { - unsubscribes.forEach((fn) => fn()); - }; - }); - }), - getSystemStats: publicProcedure .meta({ mcp: { @@ -237,7 +174,7 @@ export const beszelRouter = createTRPCRouter({ timePeriod: input.timePeriod, includeDocker: input.includeDocker, }); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); logger.debug("getSystemStats completed", { userId: ctx.session?.user?.id, integrationId: integration.id, @@ -260,61 +197,4 @@ export const beszelRouter = createTRPCRouter({ }; } }), - - subscribeSystemStats: publicProcedure - .concat(createManyIntegrationMiddleware("query", "beszel", "mock")) - .input( - z.object({ - systemId: z.string(), - }), - ) - .subscription(({ ctx, input }) => { - return observable<{ - stats: BeszelSystemStatsRecord; - containerStats: BeszelContainerStatsRecord | null; - }>((emit) => { - const integration = ctx.integrations[0]; - if (!integration) { - emit.error(new Error("No Beszel integration configured")); - return () => {}; - } - logger.debug("subscribeSystemStats opened", { - userId: ctx.session?.user?.id, - integrationId: integration.id, - systemId: input.systemId, - }); - - const abortController = new AbortController(); - - void (async () => { - try { - const instance = await createIntegrationAsync(integration); - await instance.subscribeRealtimeMetrics(input.systemId, (data) => emit.next(data), abortController.signal); - } catch (error) { - if (!abortController.signal.aborted) { - logger.warn("subscribeSystemStats error", { - userId: ctx.session?.user?.id, - integrationId: integration.id, - systemId: input.systemId, - error, - }); - if (error instanceof Error) { - emit.error(error); - } else { - emit.error(new Error(String(error))); - } - } - } - })(); - - return () => { - abortController.abort(); - logger.debug("subscribeSystemStats closed", { - userId: ctx.session?.user?.id, - integrationId: integration.id, - systemId: input.systemId, - }); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/calendar.ts b/packages/api/src/router/widgets/calendar.ts index 50ba71b3e..729e82634 100644 --- a/packages/api/src/router/widgets/calendar.ts +++ b/packages/api/src/router/widgets/calendar.ts @@ -1,13 +1,8 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; -import type { Modify } from "@homarr/common/types"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { CalendarEvent } from "@homarr/integrations/types"; import { radarrReleaseTypes } from "@homarr/integrations/types"; import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar"; @@ -39,9 +34,7 @@ export const calendarRouter = createTRPCRouter({ ctx.integrations.map(async (integration) => { const { integrationIds: _integrationIds, ...handlerInput } = input; const innerHandler = calendarMonthRequestHandler.handler(integration, handlerInput); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await innerHandler.getDataAsync(); return { events: data, @@ -72,39 +65,4 @@ export const calendarRouter = createTRPCRouter({ return []; }); }), - subscribeToEvents: publicProcedure - .input( - z.object({ - year: z.number(), - month: z.number(), - releaseType: z.array(z.enum(radarrReleaseTypes)), - showUnmonitored: z.boolean(), - }), - ) - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar"))) - .subscription(({ ctx, input }) => { - return observable<{ - integration: Modify }>; - events: CalendarEvent[]; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const { integrationIds: _integrationIds, ...handlerInput } = input; - const innerHandler = calendarMonthRequestHandler.handler(integrationWithSecrets, handlerInput); - const unsubscribe = innerHandler.subscribe((events) => { - emit.next({ - integration, - events, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/coolify.ts b/packages/api/src/router/widgets/coolify.ts index 8bdaa6f3e..a876a9ea6 100644 --- a/packages/api/src/router/widgets/coolify.ts +++ b/packages/api/src/router/widgets/coolify.ts @@ -1,6 +1,3 @@ -import { observable } from "@trpc/server/observable"; - -import type { CoolifyInstanceInfo } from "@homarr/integrations/types"; import { coolifyRequestHandler } from "@homarr/request-handler/coolify"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; @@ -13,7 +10,7 @@ export const coolifyRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = coolifyRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integrationId: integration.id, @@ -27,24 +24,4 @@ export const coolifyRouter = createTRPCRouter({ return results; }), - subscribeInstancesInfo: publicProcedure - .concat(createManyIntegrationMiddleware("query", "coolify")) - .subscription(({ ctx }) => { - return observable<{ integrationId: string; instanceInfo: CoolifyInstanceInfo; timestamp: Date }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = coolifyRequestHandler.handler(integration, {}); - return innerHandler.subscribe((instanceInfo) => { - emit.next({ - integrationId: integration.id, - instanceInfo, - timestamp: new Date(), - }); - }); - }); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/dns-hole.ts b/packages/api/src/router/widgets/dns-hole.ts index 93162a1e3..0d4d673cf 100644 --- a/packages/api/src/router/widgets/dns-hole.ts +++ b/packages/api/src/router/widgets/dns-hole.ts @@ -1,12 +1,7 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; -import type { DnsHoleSummary } from "@homarr/integrations/types"; import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; @@ -26,9 +21,7 @@ export const dnsHoleRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = dnsHoleRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integration: { @@ -44,33 +37,6 @@ export const dnsHoleRouter = createTRPCRouter({ return results; }), - subscribeToSummary: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: DnsHoleSummary; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = dnsHoleRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), - enable: protectedProcedure .meta({ mcp: { @@ -83,12 +49,6 @@ export const dnsHoleRouter = createTRPCRouter({ .mutation(async ({ ctx: { integration } }) => { const client = await createIntegrationAsync(integration); await client.enableAsync(); - - const innerHandler = dnsHoleRequestHandler.handler(integration, {}); - // We need to wait for the integration to be enabled before invalidating the cache - await new Promise((resolve) => { - setTimeout(() => void innerHandler.invalidateAsync().then(resolve), 1000); - }); }), disable: protectedProcedure @@ -108,11 +68,5 @@ export const dnsHoleRouter = createTRPCRouter({ .mutation(async ({ ctx: { integration }, input }) => { const client = await createIntegrationAsync(integration); await client.disableAsync(input.duration); - - const innerHandler = dnsHoleRequestHandler.handler(integration, {}); - // We need to wait for the integration to be disabled before invalidating the cache - await new Promise((resolve) => { - setTimeout(() => void innerHandler.invalidateAsync().then(resolve), 1000); - }); }), }); diff --git a/packages/api/src/router/widgets/downloads.ts b/packages/api/src/router/widgets/downloads.ts index 682decdc4..3503ff591 100644 --- a/packages/api/src/router/widgets/downloads.ts +++ b/packages/api/src/router/widgets/downloads.ts @@ -1,11 +1,6 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { DownloadClientJobsAndStatus } from "@homarr/integrations"; import { createIntegrationAsync, downloadClientItemSchema } from "@homarr/integrations"; import { downloadClientRequestHandler } from "@homarr/request-handler/downloads"; @@ -32,9 +27,7 @@ export const downloadsRouter = createTRPCRouter({ ctx.integrations.map(async (integration) => { const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration }); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integration: { @@ -48,35 +41,6 @@ export const downloadsRouter = createTRPCRouter({ }), ); }), - subscribeToJobsAndStatuses: publicProcedure - .concat(createDownloadClientIntegrationMiddleware("query")) - .input(z.object({ limitPerIntegration: z.number().default(50) })) - .subscription(({ ctx, input }) => { - return observable<{ - integration: Modify }>; - data: DownloadClientJobsAndStatus; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = downloadClientRequestHandler.handler(integrationWithSecrets, { - limit: input.limitPerIntegration, - }); - const unsubscribe = innerHandler.subscribe((data) => { - emit.next({ - integration, - data, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), pause: protectedProcedure .meta({ mcp: { diff --git a/packages/api/src/router/widgets/firewall.ts b/packages/api/src/router/widgets/firewall.ts index 2d582cb1f..459222f56 100644 --- a/packages/api/src/router/widgets/firewall.ts +++ b/packages/api/src/router/widgets/firewall.ts @@ -1,15 +1,4 @@ -import { observable } from "@trpc/server/observable"; - -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { - FirewallCpuSummary, - FirewallInterfacesSummary, - FirewallMemorySummary, - FirewallVersionSummary, -} from "@homarr/integrations"; import { firewallCpuRequestHandler, firewallInterfacesRequestHandler, @@ -27,7 +16,7 @@ export const firewallRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = firewallCpuRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integration: { @@ -42,32 +31,6 @@ export const firewallRouter = createTRPCRouter({ ); return results; }), - subscribeFirewallCpuStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: FirewallCpuSummary; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = firewallCpuRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), getFirewallInterfacesStatus: publicProcedure .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) @@ -75,7 +38,7 @@ export const firewallRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = firewallInterfacesRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integration: { @@ -90,32 +53,6 @@ export const firewallRouter = createTRPCRouter({ ); return results; }), - subscribeFirewallInterfacesStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: FirewallInterfacesSummary[]; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = firewallInterfacesRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), getFirewallVersionStatus: publicProcedure .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) @@ -123,7 +60,7 @@ export const firewallRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = firewallVersionRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integration: { @@ -138,32 +75,6 @@ export const firewallRouter = createTRPCRouter({ ); return results; }), - subscribeFirewallVersionStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: FirewallVersionSummary; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = firewallVersionRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), getFirewallMemoryStatus: publicProcedure .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) @@ -171,7 +82,7 @@ export const firewallRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = firewallMemoryRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integration: { @@ -186,30 +97,4 @@ export const firewallRouter = createTRPCRouter({ ); return results; }), - subscribeFirewallMemoryStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: FirewallMemorySummary; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = firewallMemoryRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/health-monitoring.ts b/packages/api/src/router/widgets/health-monitoring.ts index 03856f5a0..7c45e6c64 100644 --- a/packages/api/src/router/widgets/health-monitoring.ts +++ b/packages/api/src/router/widgets/health-monitoring.ts @@ -1,7 +1,3 @@ -import { observable } from "@trpc/server/observable"; - -import type { SystemHealthMonitoring } from "@homarr/integrations"; -import type { ProxmoxClusterInfo } from "@homarr/integrations/types"; import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; @@ -23,9 +19,7 @@ export const healthMonitoringRouter = createTRPCRouter({ return await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = systemInfoRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integrationId: integration.id, @@ -36,35 +30,6 @@ export const healthMonitoringRouter = createTRPCRouter({ }), ); }), - subscribeSystemHealthStatus: publicProcedure - .concat( - createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "truenas", "unraid", "glances", "mock"), - ) - .subscription(({ ctx }) => { - return observable<{ - integrationId: string; - healthInfo: SystemHealthMonitoring; - timestamp: Date; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integration of ctx.integrations) { - const innerHandler = systemInfoRequestHandler.handler(integration, {}); - const unsubscribe = innerHandler.subscribe((healthInfo) => { - emit.next({ - integrationId: integration.id, - healthInfo, - timestamp: new Date(), - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), getClusterHealthStatus: publicProcedure .meta({ mcp: { @@ -76,24 +41,7 @@ export const healthMonitoringRouter = createTRPCRouter({ .concat(createOneIntegrationMiddleware("query", "proxmox", "mock")) .query(async ({ ctx }) => { const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {}); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await innerHandler.getDataAsync(); return data; }), - subscribeClusterHealthStatus: publicProcedure - .concat(createOneIntegrationMiddleware("query", "proxmox", "mock")) - .subscription(({ ctx }) => { - return observable((emit) => { - const unsubscribes: (() => void)[] = []; - const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {}); - const unsubscribe = innerHandler.subscribe((healthInfo) => { - emit.next(healthInfo); - }); - unsubscribes.push(unsubscribe); - return () => { - unsubscribe(); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/immich.ts b/packages/api/src/router/widgets/immich.ts index f1428a80c..7e8c54d49 100644 --- a/packages/api/src/router/widgets/immich.ts +++ b/packages/api/src/router/widgets/immich.ts @@ -17,7 +17,7 @@ const createImmichIntegrationMiddleware = (action: IntegrationAction) => export const immichRouter = createTRPCRouter({ getServerStats: publicProcedure.concat(createImmichIntegrationMiddleware("query")).query(async ({ ctx }) => { const innerHandler = immichStatsRequestHandler.handler(ctx.integration, {}); - const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const data = await innerHandler.getDataAsync(); return data.data; }), @@ -30,13 +30,13 @@ export const immichRouter = createTRPCRouter({ ) .query(async ({ ctx, input }) => { const innerHandler = immichAlbumRequestHandler.handler(ctx.integration, { albumId: input.albumId }); - const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const data = await innerHandler.getDataAsync(); return data.data; }), getAlbums: publicProcedure.concat(createImmichIntegrationMiddleware("query")).query(async ({ ctx }) => { const innerHandler = immichAlbumsRequestHandler.handler(ctx.integration, {}); - const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const data = await innerHandler.getDataAsync(); return data.data; }), }); diff --git a/packages/api/src/router/widgets/indexer-manager.ts b/packages/api/src/router/widgets/indexer-manager.ts index cb8b88262..54ac3b9a7 100644 --- a/packages/api/src/router/widgets/indexer-manager.ts +++ b/packages/api/src/router/widgets/indexer-manager.ts @@ -1,9 +1,7 @@ import { TRPCError } from "@trpc/server"; -import { observable } from "@trpc/server/observable"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; -import type { Indexer } from "@homarr/integrations/types"; import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager"; import type { IntegrationAction } from "../../middlewares/integration"; @@ -20,7 +18,7 @@ export const indexerManagerRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = indexerManagerRequestHandler.handler(integration, {}); - const { data: indexers } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data: indexers } = await innerHandler.getDataAsync(); return { integrationId: integration.id, @@ -30,29 +28,6 @@ export const indexerManagerRouter = createTRPCRouter({ ); return results; }), - - subscribeIndexersStatus: publicProcedure - .concat(createIndexerManagerIntegrationMiddleware("query")) - .subscription(({ ctx }) => { - return observable<{ integrationId: string; indexers: Indexer[] }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const innerHandler = indexerManagerRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((indexers) => { - emit.next({ - integrationId: integrationWithSecrets.id, - indexers, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), testAllIndexers: protectedProcedure .concat(createIndexerManagerIntegrationMiddleware("interact")) .mutation(async ({ ctx }) => { diff --git a/packages/api/src/router/widgets/media-release.ts b/packages/api/src/router/widgets/media-release.ts index fddc174cc..4da950844 100644 --- a/packages/api/src/router/widgets/media-release.ts +++ b/packages/api/src/router/widgets/media-release.ts @@ -1,10 +1,4 @@ -import { observable } from "@trpc/server/observable"; - -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { MediaRelease } from "@homarr/integrations/types"; import { mediaReleaseRequestHandler } from "@homarr/request-handler/media-release"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; @@ -17,7 +11,7 @@ export const mediaReleaseRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = mediaReleaseRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integration: { @@ -37,31 +31,4 @@ export const mediaReleaseRouter = createTRPCRouter({ })), ); }), - - subscribeToReleases: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRelease"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - releases: MediaRelease[]; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = mediaReleaseRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((releases) => { - emit.next({ - integration, - releases, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/media-requests.ts b/packages/api/src/router/widgets/media-requests.ts index 715ae1327..a5d51ac9f 100644 --- a/packages/api/src/router/widgets/media-requests.ts +++ b/packages/api/src/router/widgets/media-requests.ts @@ -1,10 +1,8 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import { mediaRequestStatusConfiguration } from "@homarr/integrations/types"; -import type { MediaRequest } from "@homarr/integrations/types"; import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list"; import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats"; @@ -25,9 +23,7 @@ export const mediaRequestsRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = mediaRequestListRequestHandler.handler(integration, {}); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await innerHandler.getDataAsync(); return { integration: { id: integration.id, @@ -56,32 +52,6 @@ export const mediaRequestsRouter = createTRPCRouter({ ); }); }), - subscribeToLatestRequests: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest"))) - .subscription(({ ctx }) => { - return observable<{ - integrationId: string; - requests: MediaRequest[]; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = mediaRequestListRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((requests) => { - emit.next({ - integrationId: integration.id, - requests, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), getStats: publicProcedure .meta({ mcp: { @@ -95,9 +65,7 @@ export const mediaRequestsRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = mediaRequestStatsRequestHandler.handler(integration, {}); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await innerHandler.getDataAsync(); return { integration: { id: integration.id, @@ -139,7 +107,6 @@ export const mediaRequestsRouter = createTRPCRouter({ ) .mutation(async ({ ctx: { integration }, input }) => { const integrationInstance = await createIntegrationAsync(integration); - const innerHandler = mediaRequestListRequestHandler.handler(integration, {}); const answerActions = { approve: (id: number) => integrationInstance.approveRequestAsync(id), @@ -147,6 +114,5 @@ export const mediaRequestsRouter = createTRPCRouter({ } as const; await answerActions[input.answer](input.requestId); - await innerHandler.invalidateAsync(); }), }); diff --git a/packages/api/src/router/widgets/media-server.ts b/packages/api/src/router/widgets/media-server.ts index de61fef75..a1eab6c82 100644 --- a/packages/api/src/router/widgets/media-server.ts +++ b/packages/api/src/router/widgets/media-server.ts @@ -1,8 +1,6 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { StreamSession } from "@homarr/integrations"; import { mediaServerRequestHandler } from "@homarr/request-handler/media-server"; import type { IntegrationAction } from "../../middlewares/integration"; @@ -29,9 +27,7 @@ export const mediaServerRouter = createTRPCRouter({ const innerHandler = mediaServerRequestHandler.handler(integration, { showOnlyPlaying: input.showOnlyPlaying, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await innerHandler.getDataAsync(); return { integrationId: integration.id, integrationKind: integration.kind, @@ -40,30 +36,4 @@ export const mediaServerRouter = createTRPCRouter({ }), ); }), - subscribeToCurrentStreams: publicProcedure - .concat(createMediaServerIntegrationMiddleware("query")) - .input(z.object({ showOnlyPlaying: z.boolean() })) - .subscription(({ ctx, input }) => { - return observable<{ integrationId: string; data: StreamSession[] }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integration of ctx.integrations) { - const innerHandler = mediaServerRequestHandler.handler(integration, { - showOnlyPlaying: input.showOnlyPlaying, - }); - - const unsubscribe = innerHandler.subscribe((sessions) => { - emit.next({ - integrationId: integration.id, - data: sessions, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/media-transcoding.ts b/packages/api/src/router/widgets/media-transcoding.ts index b02f0a714..88bd91704 100644 --- a/packages/api/src/router/widgets/media-transcoding.ts +++ b/packages/api/src/router/widgets/media-transcoding.ts @@ -1,7 +1,4 @@ -import { observable } from "@trpc/server/observable"; - import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { MediaTranscoding } from "@homarr/request-handler/media-transcoding"; import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding"; import { paginatedSchema } from "@homarr/validation/common"; @@ -21,26 +18,11 @@ export const mediaTranscodingRouter = createTRPCRouter({ pageOffset: (input.page - 1) * input.pageSize, pageSize: input.pageSize, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return { integrationId: ctx.integration.id, data, }; }), - subscribeData: publicProcedure - .concat(createIndexerManagerIntegrationMiddleware("query")) - .input(paginatedSchema.pick({ page: true, pageSize: true })) - .subscription(({ ctx, input }) => { - return observable<{ integrationId: string; data: MediaTranscoding }>((emit) => { - const innerHandler = mediaTranscodingRequestHandler.handler(ctx.integration, { - pageOffset: (input.page - 1) * input.pageSize, - pageSize: input.pageSize, - }); - const unsubscribe = innerHandler.subscribe((data) => { - emit.next({ integrationId: input.integrationId, data }); - }); - return unsubscribe; - }); - }), }); diff --git a/packages/api/src/router/widgets/minecraft.ts b/packages/api/src/router/widgets/minecraft.ts index 7f646c3bc..9cc5e25d7 100644 --- a/packages/api/src/router/widgets/minecraft.ts +++ b/packages/api/src/router/widgets/minecraft.ts @@ -1,7 +1,5 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; -import type { MinecraftServerStatus } from "@homarr/request-handler/minecraft-server-status"; import { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status"; import { createTRPCRouter, publicProcedure } from "../../trpc"; @@ -16,21 +14,6 @@ export const minecraftRouter = createTRPCRouter({ isBedrockServer: input.isBedrockServer, domain: input.domain, }); - return await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - }), - subscribeServerStatus: publicProcedure.input(serverStatusInputSchema).subscription(({ input }) => { - return observable((emit) => { - const innerHandler = minecraftServerStatusRequestHandler.handler({ - isBedrockServer: input.isBedrockServer, - domain: input.domain, - }); - const unsubscribe = innerHandler.subscribe((data) => { - emit.next(data); - }); - - return () => { - unsubscribe(); - }; - }); + return await innerHandler.getDataAsync(); }), }); diff --git a/packages/api/src/router/widgets/network-controller.ts b/packages/api/src/router/widgets/network-controller.ts index 053bd0dcb..1b75645af 100644 --- a/packages/api/src/router/widgets/network-controller.ts +++ b/packages/api/src/router/widgets/network-controller.ts @@ -1,10 +1,4 @@ -import { observable } from "@trpc/server/observable"; - -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { NetworkControllerSummary } from "@homarr/integrations/types"; import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; @@ -17,7 +11,7 @@ export const networkControllerRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = networkControllerRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integration: { @@ -32,31 +26,4 @@ export const networkControllerRouter = createTRPCRouter({ ); return results; }), - - subscribeToSummary: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController"))) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - summary: NetworkControllerSummary; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = networkControllerRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((summary) => { - emit.next({ - integration, - summary, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/notifications.ts b/packages/api/src/router/widgets/notifications.ts index 89ec95165..fe587e78a 100644 --- a/packages/api/src/router/widgets/notifications.ts +++ b/packages/api/src/router/widgets/notifications.ts @@ -1,10 +1,4 @@ -import { observable } from "@trpc/server/observable"; - -import type { Modify } from "@homarr/common/types"; -import type { Integration } from "@homarr/db/schema"; -import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { Notification } from "@homarr/integrations"; import { notificationsRequestHandler } from "@homarr/request-handler/notifications"; import type { IntegrationAction } from "../../middlewares/integration"; @@ -21,7 +15,7 @@ export const notificationsRouter = createTRPCRouter({ return await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = notificationsRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integration: { @@ -35,30 +29,4 @@ export const notificationsRouter = createTRPCRouter({ }), ); }), - subscribeNotifications: publicProcedure - .unstable_concat(createNotificationsIntegrationMiddleware("query")) - .subscription(({ ctx }) => { - return observable<{ - integration: Modify }>; - data: Notification[]; - }>((emit) => { - const unsubscribes: (() => void)[] = []; - for (const integrationWithSecrets of ctx.integrations) { - const { decryptedSecrets: _, ...integration } = integrationWithSecrets; - const innerHandler = notificationsRequestHandler.handler(integrationWithSecrets, {}); - const unsubscribe = innerHandler.subscribe((data) => { - emit.next({ - integration, - data, - }); - }); - unsubscribes.push(unsubscribe); - } - return () => { - unsubscribes.forEach((unsubscribe) => { - unsubscribe(); - }); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/paperless-ngx.ts b/packages/api/src/router/widgets/paperless-ngx.ts index 0fb81e72a..2a1bf40de 100644 --- a/packages/api/src/router/widgets/paperless-ngx.ts +++ b/packages/api/src/router/widgets/paperless-ngx.ts @@ -6,7 +6,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc"; export const paperlessNgxRouter = createTRPCRouter({ getStats: publicProcedure.concat(createOneIntegrationMiddleware("query", "paperlessNgx")).query(async ({ ctx }) => { const innerHandler = paperlessNgxStatsRequestHandler.handler(ctx.integration, {}); - const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const data = await innerHandler.getDataAsync(); return data.data; }), }); diff --git a/packages/api/src/router/widgets/releases.ts b/packages/api/src/router/widgets/releases.ts index 3d355b26c..46ef33e3d 100644 --- a/packages/api/src/router/widgets/releases.ts +++ b/packages/api/src/router/widgets/releases.ts @@ -46,9 +46,7 @@ export const releasesRouter = createTRPCRouter({ identifier: repository.identifier, versionRegex: formatVersionFilterRegex(repository.versionFilter), }) - .getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + .getDataAsync(); return { id: repository.id, diff --git a/packages/api/src/router/widgets/rssFeed.ts b/packages/api/src/router/widgets/rssFeed.ts index ec9c223e0..d13903aa9 100644 --- a/packages/api/src/router/widgets/rssFeed.ts +++ b/packages/api/src/router/widgets/rssFeed.ts @@ -1,4 +1,3 @@ -import { observable } from "@trpc/server/observable"; import SuperJSON from "superjson"; import { z } from "zod/v4"; @@ -6,7 +5,6 @@ import type { Session } from "@homarr/auth"; import type { Database } from "@homarr/db"; import { eq } from "@homarr/db"; import { items, users } from "@homarr/db/schema"; -import type { ExtendedFeedEntry } from "@homarr/request-handler/rss-feeds"; import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds"; import type { WidgetComponentProps } from "../../../../widgets/src"; @@ -29,9 +27,7 @@ export const rssFeedRouter = createTRPCRouter({ url, count: input.maximumAmountPosts, }); - return await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + return await innerHandler.getDataAsync(); }), ); @@ -44,21 +40,6 @@ export const rssFeedRouter = createTRPCRouter({ : 0; }); }), - subscribeFeeds: publicProcedure.input(feedsInput).subscription(({ input }) => { - // It is not necessary to check access to all feeds here, as the client will only subscribe to feeds which already exist - return observable<{ url: string; entries: ExtendedFeedEntry[] }>((emit) => { - const unsubscribes = input.urls.map((url) => { - const innerHandler = rssFeedsRequestHandler.handler({ url, count: input.maximumAmountPosts }); - return innerHandler.subscribe((data) => { - emit.next({ url, entries: data.entries }); - }); - }); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; - }); - }), }); export async function canAccessAllFeedsAsync(db: Database, session: Session | null) { diff --git a/packages/api/src/router/widgets/smart-home.ts b/packages/api/src/router/widgets/smart-home.ts index dec3770a7..c37468752 100644 --- a/packages/api/src/router/widgets/smart-home.ts +++ b/packages/api/src/router/widgets/smart-home.ts @@ -1,4 +1,3 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; @@ -25,31 +24,9 @@ export const smartHomeRouter = createTRPCRouter({ .concat(createSmartHomeIntegrationMiddleware("query")) .query(async ({ ctx: { integration }, input }) => { const innerHandler = smartHomeEntityStateRequestHandler.handler(integration, { entityId: input.entityId }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: false, - }); + const { data } = await innerHandler.getDataAsync(); return data; }), - subscribeEntityState: publicProcedure - .concat(createSmartHomeIntegrationMiddleware("query")) - .input(z.object({ entityId: z.string() })) - .subscription(({ input, ctx }) => { - return observable<{ - entityId: string; - state: string; - }>((emit) => { - const innerHandler = smartHomeEntityStateRequestHandler.handler(ctx.integration, { - entityId: input.entityId, - }); - const unsubscribe = innerHandler.subscribe((state) => { - emit.next({ state, entityId: input.entityId }); - }); - - return () => { - unsubscribe(); - }; - }); - }), switchEntity: protectedProcedure .meta({ mcp: { @@ -64,9 +41,6 @@ export const smartHomeRouter = createTRPCRouter({ const client = await createIntegrationAsync(integration); const success = await client.triggerToggleAsync(input.entityId); - const innerHandler = smartHomeEntityStateRequestHandler.handler(integration, { entityId: input.entityId }); - await innerHandler.invalidateAsync(); - return success; }), executeAutomation: protectedProcedure diff --git a/packages/api/src/router/widgets/speedtest-tracker.ts b/packages/api/src/router/widgets/speedtest-tracker.ts index be4245921..a75dcfd93 100644 --- a/packages/api/src/router/widgets/speedtest-tracker.ts +++ b/packages/api/src/router/widgets/speedtest-tracker.ts @@ -1,6 +1,3 @@ -import { observable } from "@trpc/server/observable"; - -import type { SpeedtestTrackerDashboardData } from "@homarr/integrations/types"; import { speedtestTrackerRequestHandler } from "@homarr/request-handler/speedtest-tracker"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; @@ -13,7 +10,7 @@ export const speedtestTrackerRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = speedtestTrackerRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integrationId: integration.id, @@ -27,29 +24,4 @@ export const speedtestTrackerRouter = createTRPCRouter({ return results; }), - - subscribeToDashboard: publicProcedure - .concat(createManyIntegrationMiddleware("query", "speedtestTracker")) - .subscription(({ ctx }) => { - return observable<{ - integrationId: string; - dashboard: SpeedtestTrackerDashboardData; - timestamp: Date; - }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = speedtestTrackerRequestHandler.handler(integration, {}); - return innerHandler.subscribe((dashboard) => { - emit.next({ - integrationId: integration.id, - dashboard, - timestamp: new Date(), - }); - }); - }); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/stocks.ts b/packages/api/src/router/widgets/stocks.ts index d7b4ed13a..7fc08005b 100644 --- a/packages/api/src/router/widgets/stocks.ts +++ b/packages/api/src/router/widgets/stocks.ts @@ -18,6 +18,6 @@ export const stockPriceRouter = createTRPCRouter({ timeRange: input.timeRange, timeInterval: input.timeInterval, }); - return await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + return await innerHandler.getDataAsync(); }), }); diff --git a/packages/api/src/router/widgets/timetable.ts b/packages/api/src/router/widgets/timetable.ts index 787be4c37..c33496cdc 100644 --- a/packages/api/src/router/widgets/timetable.ts +++ b/packages/api/src/router/widgets/timetable.ts @@ -20,7 +20,7 @@ export const timetableRouter = createTRPCRouter({ limit: input.limit, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return data; }), @@ -36,7 +36,7 @@ export const timetableRouter = createTRPCRouter({ query: input.query, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return data; }), diff --git a/packages/api/src/router/widgets/tracearr.ts b/packages/api/src/router/widgets/tracearr.ts index d46c80c66..94b612a4a 100644 --- a/packages/api/src/router/widgets/tracearr.ts +++ b/packages/api/src/router/widgets/tracearr.ts @@ -1,6 +1,3 @@ -import { observable } from "@trpc/server/observable"; - -import type { TracearrDashboardData } from "@homarr/integrations/types"; import { tracearrRequestHandler } from "@homarr/request-handler/tracearr"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; @@ -11,7 +8,7 @@ export const tracearrRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = tracearrRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integrationId: integration.id, @@ -25,24 +22,4 @@ export const tracearrRouter = createTRPCRouter({ return results; }), - subscribeToDashboard: publicProcedure - .concat(createManyIntegrationMiddleware("query", "tracearr")) - .subscription(({ ctx }) => { - return observable<{ integrationId: string; dashboard: TracearrDashboardData; timestamp: Date }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = tracearrRequestHandler.handler(integration, {}); - return innerHandler.subscribe((dashboard) => { - emit.next({ - integrationId: integration.id, - dashboard, - timestamp: new Date(), - }); - }); - }); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/umami.ts b/packages/api/src/router/widgets/umami.ts index 2c240f4bb..852d1f1a0 100644 --- a/packages/api/src/router/widgets/umami.ts +++ b/packages/api/src/router/widgets/umami.ts @@ -1,4 +1,3 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; import { createLogger } from "@homarr/core/infrastructure/logs"; @@ -46,7 +45,7 @@ export const umamiRouter = createTRPCRouter({ timeFrame: input.timeFrame, eventName: input.eventName, }); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integrationId: integration.id, @@ -67,7 +66,7 @@ export const umamiRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { try { const innerHandler = umamiEventNamesRequestHandler.handler(ctx.integration, { websiteId: input.websiteId }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { logger.warn( @@ -93,7 +92,7 @@ export const umamiRouter = createTRPCRouter({ timeFrame: input.timeFrame, limit: input.limit, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { logger.warn( @@ -123,7 +122,7 @@ export const umamiRouter = createTRPCRouter({ timeFrame: input.timeFrame, limit: input.limit, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { logger.warn( @@ -154,7 +153,7 @@ export const umamiRouter = createTRPCRouter({ timeFrame: input.timeFrame, eventNames: sortedNames, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { logger.warn( @@ -176,7 +175,7 @@ export const umamiRouter = createTRPCRouter({ const innerHandler = umamiActiveVisitorsRequestHandler.handler(ctx.integration, { websiteId: input.websiteId, }); - const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data } = await innerHandler.getDataAsync(); return data; } catch (error) { logger.warn( @@ -185,21 +184,4 @@ export const umamiRouter = createTRPCRouter({ return 0; } }), - - subscribeActiveVisitors: publicProcedure - .input(z.object({ websiteId: z.string() })) - .concat(createOneIntegrationMiddleware("query", "umami")) - .subscription(({ ctx, input }) => { - return observable((emit) => { - const innerHandler = umamiActiveVisitorsRequestHandler.handler(ctx.integration, { - websiteId: input.websiteId, - }); - const unsubscribe = innerHandler.subscribe((count) => { - emit.next(count); - }); - return () => { - unsubscribe(); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/ups.ts b/packages/api/src/router/widgets/ups.ts index f1c27c434..02878665d 100644 --- a/packages/api/src/router/widgets/ups.ts +++ b/packages/api/src/router/widgets/ups.ts @@ -1,7 +1,4 @@ -import { observable } from "@trpc/server/observable"; - import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import type { UpsSummary } from "@homarr/integrations/types"; import { upsSummariesRequestHandler } from "@homarr/request-handler/ups"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; @@ -14,7 +11,7 @@ export const upsRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = upsSummariesRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integrationId: integration.id, @@ -28,24 +25,4 @@ export const upsRouter = createTRPCRouter({ return results; }), - subscribeSummaries: publicProcedure - .concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("ups"))) - .subscription(({ ctx }) => { - return observable<{ integrationId: string; summaries: UpsSummary[]; timestamp: Date }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = upsSummariesRequestHandler.handler(integration, {}); - return innerHandler.subscribe((summaries) => { - emit.next({ - integrationId: integration.id, - summaries, - timestamp: new Date(), - }); - }); - }); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/uptime-kuma.ts b/packages/api/src/router/widgets/uptime-kuma.ts index eb28f9e7e..ca3a6b01e 100644 --- a/packages/api/src/router/widgets/uptime-kuma.ts +++ b/packages/api/src/router/widgets/uptime-kuma.ts @@ -1,6 +1,3 @@ -import { observable } from "@trpc/server/observable"; - -import type { UptimeKumaDashboardData } from "@homarr/integrations/types"; import { uptimeKumaRequestHandler } from "@homarr/request-handler/uptime-kuma"; import { createManyIntegrationMiddleware } from "../../middlewares/integration"; @@ -13,7 +10,7 @@ export const uptimeKumaRouter = createTRPCRouter({ const results = await Promise.all( ctx.integrations.map(async (integration) => { const innerHandler = uptimeKumaRequestHandler.handler(integration, {}); - const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); + const { data, timestamp } = await innerHandler.getDataAsync(); return { integrationId: integration.id, @@ -27,29 +24,4 @@ export const uptimeKumaRouter = createTRPCRouter({ return results; }), - - subscribeToDashboard: publicProcedure - .concat(createManyIntegrationMiddleware("query", "uptimeKuma")) - .subscription(({ ctx }) => { - return observable<{ - integrationId: string; - dashboard: UptimeKumaDashboardData; - timestamp: Date; - }>((emit) => { - const unsubscribes = ctx.integrations.map((integration) => { - const innerHandler = uptimeKumaRequestHandler.handler(integration, {}); - return innerHandler.subscribe((dashboard) => { - emit.next({ - integrationId: integration.id, - dashboard, - timestamp: new Date(), - }); - }); - }); - - return () => { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; - }); - }), }); diff --git a/packages/api/src/router/widgets/weather.ts b/packages/api/src/router/widgets/weather.ts index bb1077569..87ea6b48a 100644 --- a/packages/api/src/router/widgets/weather.ts +++ b/packages/api/src/router/widgets/weather.ts @@ -1,7 +1,5 @@ -import { observable } from "@trpc/server/observable"; import { z } from "zod/v4"; -import type { Weather } from "@homarr/request-handler/weather"; import { weatherRequestHandler } from "@homarr/request-handler/weather"; import { createTRPCRouter, publicProcedure } from "../../trpc"; @@ -14,16 +12,6 @@ const atLocationInput = z.object({ export const weatherRouter = createTRPCRouter({ atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => { const handler = weatherRequestHandler.handler(input); - return await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false }).then((result) => result.data); - }), - subscribeAtLocation: publicProcedure.input(atLocationInput).subscription(({ input }) => { - return observable((emit) => { - const handler = weatherRequestHandler.handler(input); - const unsubscribe = handler.subscribe((data) => { - emit.next(data); - }); - - return unsubscribe; - }); + return await handler.getDataAsync().then((result) => result.data); }), }); diff --git a/packages/api/src/services/duckduckgo-bangs.ts b/packages/api/src/services/duckduckgo-bangs.ts index 297f5c8ed..48de45dd6 100644 --- a/packages/api/src/services/duckduckgo-bangs.ts +++ b/packages/api/src/services/duckduckgo-bangs.ts @@ -39,7 +39,7 @@ export const searchDuckDuckGoBangsAsync = async (input: { const queryTokenPrefix = normalizeBangToken(input.query); if (!queryTokenPrefix) return []; - const { data: allBangs } = await duckDuckGoBangsRequestHandler.handler({}).getCachedOrUpdatedDataAsync({}); + const { data: allBangs } = await duckDuckGoBangsRequestHandler.handler({}).getDataAsync(); const startIndex = lowerBound(allBangs, queryTokenPrefix); const matches: DuckDuckGoBang[] = []; diff --git a/packages/api/src/test/query-cache.spec.ts b/packages/api/src/test/query-cache.spec.ts index c765af031..e25051903 100644 --- a/packages/api/src/test/query-cache.spec.ts +++ b/packages/api/src/test/query-cache.spec.ts @@ -6,63 +6,29 @@ import { createId } from "@homarr/common"; import type { Database } from "@homarr/db"; import { boards, users } from "@homarr/db/schema"; import { createDb } from "@homarr/db/test"; -import { - isPersistableWidgetQueryKey, - queryCacheDefaultStaleTimeMs, - queryCacheMaxValueBytes, - queryCacheRetentionMs, -} from "@homarr/api/query-cache"; +import { isPersistableWidgetQueryKey, queryCacheDefaultStaleTimeMs, queryCacheMaxValueBytes } from "@homarr/api/query-cache"; import { queryCacheRouter } from "../router/query-cache"; const redisMock = vi.hoisted(() => { - const values = new Map(); - const calls: { - userId: string; - boardId: string; - key: string; - ttlMs: number; - maxValueBytes: number; - }[] = []; + const values = new Map(); return { values, - calls, - createQueryCacheChannel: vi.fn( - (options: { userId: string; boardId: string; key: string; ttlMs: number; maxValueBytes: number }) => { - calls.push(options); - const storageKey = `${options.userId}:${options.boardId}:${options.key}`; - - return { - getAsync: async () => { - const item = values.get(storageKey); - if (!item) return null; - if (Date.now() > item.expiresAt) { - values.delete(storageKey); - return null; - } - - return item.value; - }, - setAsync: async (value: string) => { - if (Buffer.byteLength(value, "utf8") > options.maxValueBytes) { - return false; - } - - values.set(storageKey, { value, expiresAt: Date.now() + options.ttlMs }); - return true; - }, - removeAsync: async () => { - values.delete(storageKey); - }, - }; - }, - ), + setQueryCacheAsync: vi.fn(async (_userId: string, _boardId: string, value: string, _ttlMs: number) => { + if (Buffer.byteLength(value, "utf8") > 1024 * 1024) return false; + values.set(`${_userId}:${_boardId}`, value); + return true; + }), + removeQueryCacheAsync: vi.fn(async (_userId: string, _boardId: string) => { + values.delete(`${_userId}:${_boardId}`); + }), }; }); vi.mock("@homarr/redis", () => ({ - createQueryCacheChannel: redisMock.createQueryCacheChannel, + setQueryCacheAsync: redisMock.setQueryCacheAsync, + removeQueryCacheAsync: redisMock.removeQueryCacheAsync, })); vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session })); @@ -70,8 +36,8 @@ vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session })); afterEach(() => { vi.useRealTimers(); redisMock.values.clear(); - redisMock.calls.length = 0; - redisMock.createQueryCacheChannel.mockClear(); + redisMock.setQueryCacheAsync.mockClear(); + redisMock.removeQueryCacheAsync.mockClear(); }); const createSession = (userId = createId()): Session => ({ @@ -104,7 +70,7 @@ describe("isPersistableWidgetQueryKey", () => { }); describe("queryCacheRouter", () => { - test("stores, reads, and removes data for a board viewer", async () => { + test("stores and removes data for a board viewer", async () => { const db = createDb(); const session = createSession(); await insertUserAsync(db, session.user.id); @@ -112,14 +78,13 @@ describe("queryCacheRouter", () => { const caller = queryCacheRouter.createCaller({ db, deviceType: undefined, session }); - await expect(caller.setItem({ boardId: "board-1", key: "query-key", value: "cached-value" })).resolves.toEqual({ + await expect(caller.setItem({ boardId: "board-1", value: "cached-value" })).resolves.toEqual({ stored: true, }); - await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).resolves.toBe("cached-value"); + expect(redisMock.setQueryCacheAsync).toHaveBeenCalledWith(session.user.id, "board-1", "cached-value", expect.any(Number)); - await caller.removeItem({ boardId: "board-1", key: "query-key" }); - - await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).resolves.toBeNull(); + await caller.removeItem({ boardId: "board-1" }); + expect(redisMock.removeQueryCacheAsync).toHaveBeenCalledWith(session.user.id, "board-1"); }); test("does not touch Redis when board access is denied", async () => { @@ -127,32 +92,26 @@ describe("queryCacheRouter", () => { await insertUserAsync(db, "owner"); await db.insert(boards).values({ id: "board-1", name: "board", creatorId: "owner", isPublic: false }); - redisMock.createQueryCacheChannel.mockClear(); + redisMock.setQueryCacheAsync.mockClear(); const caller = queryCacheRouter.createCaller({ db, deviceType: undefined, session: createSession("other-user") }); - await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).rejects.toThrow("Board not found"); - expect(redisMock.createQueryCacheChannel).not.toHaveBeenCalled(); + await expect(caller.setItem({ boardId: "board-1", value: "cached-value" })).rejects.toThrow("Board not found"); + expect(redisMock.setQueryCacheAsync).not.toHaveBeenCalled(); }); - test("scopes anonymous public-board cache entries by anonymous user and board", async () => { + test("scopes anonymous public-board cache entries by anonymous user", async () => { const db = createDb(); await insertUserAsync(db, "owner"); await db.insert(boards).values({ id: "board-1", name: "board", creatorId: "owner", isPublic: true }); const caller = queryCacheRouter.createCaller({ db, deviceType: undefined, session: null }); - await caller.setItem({ boardId: "board-1", key: "query-key", value: "cached-value" }); + await caller.setItem({ boardId: "board-1", value: "cached-value" }); - expect(redisMock.calls.at(-1)).toMatchObject({ - userId: "anonymous", - boardId: "board-1", - key: "query-key", - ttlMs: queryCacheRetentionMs, - maxValueBytes: queryCacheMaxValueBytes, - }); + expect(redisMock.setQueryCacheAsync).toHaveBeenCalledWith("anonymous", "board-1", "cached-value", expect.any(Number)); }); - test("scopes signed-in cache entries by user and rejects oversized values", async () => { + test("rejects oversized values", async () => { const db = createDb(); const session = createSession("user-1"); await insertUserAsync(db, session.user.id); @@ -161,34 +120,9 @@ describe("queryCacheRouter", () => { const caller = queryCacheRouter.createCaller({ db, deviceType: undefined, session }); const value = "x".repeat(queryCacheMaxValueBytes + 1); - await expect(caller.setItem({ boardId: "board-1", key: "query-key", value })).resolves.toEqual({ + await expect(caller.setItem({ boardId: "board-1", value })).resolves.toEqual({ stored: false, }); - expect(redisMock.calls.at(-1)).toMatchObject({ - userId: "user-1", - boardId: "board-1", - ttlMs: queryCacheRetentionMs, - maxValueBytes: queryCacheMaxValueBytes, - }); - }); - - test("expires values through the Redis TTL path", async () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z")); - - const db = createDb(); - const session = createSession("user-1"); - await insertUserAsync(db, session.user.id); - await db.insert(boards).values({ id: "board-1", name: "board", creatorId: session.user.id, isPublic: false }); - - const caller = queryCacheRouter.createCaller({ db, deviceType: undefined, session }); - - await caller.setItem({ boardId: "board-1", key: "query-key", value: "cached-value" }); - await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).resolves.toBe("cached-value"); - - await vi.advanceTimersByTimeAsync(queryCacheRetentionMs + 1); - - await expect(caller.getItem({ boardId: "board-1", key: "query-key" })).resolves.toBeNull(); }); }); diff --git a/packages/cron-jobs/src/index.ts b/packages/cron-jobs/src/index.ts index 576e001c9..d9e42493e 100644 --- a/packages/cron-jobs/src/index.ts +++ b/packages/cron-jobs/src/index.ts @@ -1,31 +1,6 @@ import { analyticsJob } from "./jobs/analytics"; -import { dockerContainersJob } from "./jobs/docker"; import { iconsUpdaterJob } from "./jobs/icons-updater"; -import { dnsHoleJob } from "./jobs/integrations/dns-hole"; -import { downloadsJob } from "./jobs/integrations/downloads"; -import { - firewallCpuJob, - firewallInterfacesJob, - firewallMemoryJob, - firewallVersionJob, -} from "./jobs/integrations/firewall"; -import { healthMonitoringJob } from "./jobs/integrations/health-monitoring"; -import { smartHomeEntityStateJob } from "./jobs/integrations/home-assistant"; -import { indexerManagerJob } from "./jobs/integrations/indexer-manager"; -import { mediaOrganizerJob } from "./jobs/integrations/media-organizer"; -import { mediaRequestListJob, mediaRequestStatsJob } from "./jobs/integrations/media-requests"; -import { mediaServerJob } from "./jobs/integrations/media-server"; -import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding"; -import { networkControllerJob } from "./jobs/integrations/network-controller"; -import { refreshNotificationsJob } from "./jobs/integrations/notifications"; -import { timetableJob } from "./jobs/integrations/timetable"; -import { tracearrJob } from "./jobs/integrations/tracearr"; -import { umamiActiveVisitorsJob } from "./jobs/integrations/umami"; -import { minecraftServerStatusJob } from "./jobs/minecraft-server-status"; import { pingJob } from "./jobs/ping"; -import { rssFeedsJob } from "./jobs/rss-feeds"; -import { updateCheckerJob } from "./jobs/update-checker"; -import { weatherJob } from "./jobs/weather"; import { createCronJobGroup } from "./lib"; const getJobGroup = () => { @@ -33,34 +8,9 @@ const getJobGroup = () => { analytics: analyticsJob, iconsUpdater: iconsUpdaterJob, ping: pingJob, - smartHomeEntityState: smartHomeEntityStateJob, - mediaServer: mediaServerJob, - mediaOrganizer: mediaOrganizerJob, - downloads: downloadsJob, - dnsHole: dnsHoleJob, - mediaRequestStats: mediaRequestStatsJob, - mediaRequestList: mediaRequestListJob, - rssFeeds: rssFeedsJob, - indexerManager: indexerManagerJob, - healthMonitoring: healthMonitoringJob, - updateChecker: updateCheckerJob, - mediaTranscoding: mediaTranscodingJob, - minecraftServerStatus: minecraftServerStatusJob, - dockerContainers: dockerContainersJob, - networkController: networkControllerJob, - firewallCpu: firewallCpuJob, - firewallMemory: firewallMemoryJob, - firewallVersion: firewallVersionJob, - firewallInterfaces: firewallInterfacesJob, - refreshNotifications: refreshNotificationsJob, - weather: weatherJob, - timetable: timetableJob, - tracearr: tracearrJob, - umamiActiveVisitors: umamiActiveVisitorsJob, }); }; -// Add global type augmentation for homarr declare global { var cronJobs: ReturnType | undefined; } diff --git a/packages/cron-jobs/src/jobs/docker.ts b/packages/cron-jobs/src/jobs/docker.ts deleted file mode 100644 index 963f395bf..000000000 --- a/packages/cron-jobs/src/jobs/docker.ts +++ /dev/null @@ -1,31 +0,0 @@ -import SuperJSON from "superjson"; - -import { createLogger } from "@homarr/core/infrastructure/logs"; -import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { db, eq } from "@homarr/db"; -import { items } from "@homarr/db/schema"; -import { dockerContainersRequestHandler } from "@homarr/request-handler/docker"; - -import type { WidgetComponentProps } from "../../../widgets"; -import { createCronJob } from "../lib"; - -const logger = createLogger({ module: "dockerJobs" }); - -export const dockerContainersJob = createCronJob("dockerContainers", EVERY_MINUTE).withCallback(async () => { - const dockerItems = await db.query.items.findMany({ - where: eq(items.kind, "dockerContainers"), - }); - - await Promise.allSettled( - dockerItems.map(async (item) => { - try { - const options = SuperJSON.parse["options"]>(item.options); - const innerHandler = dockerContainersRequestHandler.handler(options); - await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - } catch (error) { - logger.error(new ErrorWithMetadata("Failed to update Docker container status", { item }, { cause: error })); - } - }), - ); -}); diff --git a/packages/cron-jobs/src/jobs/integrations/dns-hole.ts b/packages/cron-jobs/src/jobs/integrations/dns-hole.ts deleted file mode 100644 index a78b28e08..000000000 --- a/packages/cron-jobs/src/jobs/integrations/dns-hole.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const dnsHoleJob = createCronJob("dnsHole", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(dnsHoleRequestHandler.handler, { - widgetKinds: ["dnsHoleSummary", "dnsHoleControls"], - getInput: { - dnsHoleSummary: () => ({}), - dnsHoleControls: () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/downloads.ts b/packages/cron-jobs/src/jobs/integrations/downloads.ts deleted file mode 100644 index 54eb476dc..000000000 --- a/packages/cron-jobs/src/jobs/integrations/downloads.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions"; -import { downloadClientRequestHandler } from "@homarr/request-handler/downloads"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const downloadsJob = createCronJob("downloads", EVERY_5_SECONDS).withCallback( - createRequestIntegrationJobHandler(downloadClientRequestHandler.handler, { - widgetKinds: ["downloads"], - getInput: { - downloads: (options) => ({ - limit: options.limitPerIntegration, - }), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/firewall.ts b/packages/cron-jobs/src/jobs/integrations/firewall.ts deleted file mode 100644 index 50637a642..000000000 --- a/packages/cron-jobs/src/jobs/integrations/firewall.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { EVERY_5_SECONDS, EVERY_30_SECONDS, EVERY_HOUR, EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { - firewallCpuRequestHandler, - firewallInterfacesRequestHandler, - firewallMemoryRequestHandler, - firewallVersionRequestHandler, -} from "@homarr/request-handler/firewall"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const firewallCpuJob = createCronJob("firewallCpu", EVERY_5_SECONDS).withCallback( - createRequestIntegrationJobHandler(firewallCpuRequestHandler.handler, { - widgetKinds: ["firewall"], - getInput: { - firewall: () => ({}), - }, - }), -); - -export const firewallMemoryJob = createCronJob("firewallMemory", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(firewallMemoryRequestHandler.handler, { - widgetKinds: ["firewall"], - getInput: { - firewall: () => ({}), - }, - }), -); - -export const firewallInterfacesJob = createCronJob("firewallInterfaces", EVERY_30_SECONDS).withCallback( - createRequestIntegrationJobHandler(firewallInterfacesRequestHandler.handler, { - widgetKinds: ["firewall"], - getInput: { - firewall: () => ({}), - }, - }), -); - -export const firewallVersionJob = createCronJob("firewallVersion", EVERY_HOUR).withCallback( - createRequestIntegrationJobHandler(firewallVersionRequestHandler.handler, { - widgetKinds: ["firewall"], - getInput: { - firewall: () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts b/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts deleted file mode 100644 index 4d09801d2..000000000 --- a/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions"; -import { coolifyRequestHandler } from "@homarr/request-handler/coolify"; -import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const healthMonitoringJob = createCronJob("healthMonitoring", EVERY_5_SECONDS).withCallback( - createRequestIntegrationJobHandler( - (integration, itemOptions: Record) => { - const { kind } = integration; - - if (kind === "beszel") { - return null as never; - } - if (kind !== "proxmox" && kind !== "mock" && kind !== "coolify") { - return systemInfoRequestHandler.handler({ ...integration, kind }, itemOptions); - } - if (kind === "coolify") { - return coolifyRequestHandler.handler({ ...integration, kind }, itemOptions); - } - return clusterInfoRequestHandler.handler({ ...integration, kind }, itemOptions); - }, - { - widgetKinds: ["healthMonitoring", "systemResources", "coolify"], - getInput: { - healthMonitoring: () => ({}), - systemResources: () => ({}), - coolify: () => ({}), - }, - }, - ), -); diff --git a/packages/cron-jobs/src/jobs/integrations/home-assistant.ts b/packages/cron-jobs/src/jobs/integrations/home-assistant.ts deleted file mode 100644 index 2723391a7..000000000 --- a/packages/cron-jobs/src/jobs/integrations/home-assistant.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { smartHomeEntityStateRequestHandler } from "@homarr/request-handler/smart-home-entity-state"; - -import { createCronJob } from "../../lib"; - -export const smartHomeEntityStateJob = createCronJob("smartHomeEntityState", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(smartHomeEntityStateRequestHandler.handler, { - widgetKinds: ["smartHome-entityState"], - getInput: { - "smartHome-entityState": (options) => ({ - entityId: options.entityId, - }), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/indexer-manager.ts b/packages/cron-jobs/src/jobs/integrations/indexer-manager.ts deleted file mode 100644 index 11da75508..000000000 --- a/packages/cron-jobs/src/jobs/integrations/indexer-manager.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const indexerManagerJob = createCronJob("indexerManager", EVERY_5_MINUTES).withCallback( - createRequestIntegrationJobHandler(indexerManagerRequestHandler.handler, { - widgetKinds: ["indexerManager"], - getInput: { - indexerManager: () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/media-organizer.ts b/packages/cron-jobs/src/jobs/integrations/media-organizer.ts deleted file mode 100644 index a84696da9..000000000 --- a/packages/cron-jobs/src/jobs/integrations/media-organizer.ts +++ /dev/null @@ -1,36 +0,0 @@ -import dayjs from "dayjs"; - -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; - -import { createCronJob } from "../../lib"; - -export const mediaOrganizerJob = createCronJob("mediaOrganizer", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(calendarMonthRequestHandler.handler, { - widgetKinds: ["calendar"], - getInput: { - // Request handler will run for all specified months - calendar: (options) => { - const inputs = []; - - const startOffset = -Number(options.filterPastMonths); - const endOffset = Number(options.filterFutureMonths); - - for (let offsetMonths = startOffset; offsetMonths <= endOffset; offsetMonths++) { - const year = dayjs().subtract(offsetMonths, "months").year(); - const month = dayjs().subtract(offsetMonths, "months").month(); - - inputs.push({ - year, - month, - releaseType: options.releaseType, - showUnmonitored: options.showUnmonitored, - }); - } - - return inputs; - }, - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/media-requests.ts b/packages/cron-jobs/src/jobs/integrations/media-requests.ts deleted file mode 100644 index ededb6141..000000000 --- a/packages/cron-jobs/src/jobs/integrations/media-requests.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list"; -import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats"; - -import { createCronJob } from "../../lib"; - -export const mediaRequestStatsJob = createCronJob("mediaRequestStats", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(mediaRequestStatsRequestHandler.handler, { - widgetKinds: ["mediaRequests-requestStats"], - getInput: { - "mediaRequests-requestStats": () => ({}), - }, - }), -); - -export const mediaRequestListJob = createCronJob("mediaRequestList", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(mediaRequestListRequestHandler.handler, { - widgetKinds: ["mediaRequests-requestList"], - getInput: { - "mediaRequests-requestList": () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/media-server.ts b/packages/cron-jobs/src/jobs/integrations/media-server.ts deleted file mode 100644 index 099ea53ae..000000000 --- a/packages/cron-jobs/src/jobs/integrations/media-server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { mediaServerRequestHandler } from "@homarr/request-handler/media-server"; - -import { createCronJob } from "../../lib"; - -export const mediaServerJob = createCronJob("mediaServer", EVERY_5_SECONDS).withCallback( - createRequestIntegrationJobHandler(mediaServerRequestHandler.handler, { - widgetKinds: ["mediaServer"], - getInput: { - mediaServer: ({ showOnlyPlaying }) => ({ showOnlyPlaying }), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/media-transcoding.ts b/packages/cron-jobs/src/jobs/integrations/media-transcoding.ts deleted file mode 100644 index c039dc250..000000000 --- a/packages/cron-jobs/src/jobs/integrations/media-transcoding.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { mediaTranscodingRequestHandler } from "@homarr/request-handler/media-transcoding"; - -import { createCronJob } from "../../lib"; - -export const mediaTranscodingJob = createCronJob("mediaTranscoding", EVERY_5_MINUTES).withCallback( - createRequestIntegrationJobHandler(mediaTranscodingRequestHandler.handler, { - widgetKinds: ["mediaTranscoding"], - getInput: { - mediaTranscoding: () => ({ pageOffset: 0, pageSize: 10 }), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/network-controller.ts b/packages/cron-jobs/src/jobs/integrations/network-controller.ts deleted file mode 100644 index 5d9354588..000000000 --- a/packages/cron-jobs/src/jobs/integrations/network-controller.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller"; - -import { createCronJob } from "../../lib"; - -export const networkControllerJob = createCronJob("networkController", EVERY_MINUTE).withCallback( - createRequestIntegrationJobHandler(networkControllerRequestHandler.handler, { - widgetKinds: ["networkControllerSummary"], - getInput: { - networkControllerSummary: () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/notifications.ts b/packages/cron-jobs/src/jobs/integrations/notifications.ts deleted file mode 100644 index 0f544995f..000000000 --- a/packages/cron-jobs/src/jobs/integrations/notifications.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { notificationsRequestHandler } from "@homarr/request-handler/notifications"; - -import { createCronJob } from "../../lib"; - -export const refreshNotificationsJob = createCronJob("refreshNotifications", EVERY_5_MINUTES).withCallback( - createRequestIntegrationJobHandler(notificationsRequestHandler.handler, { - widgetKinds: ["notifications"], - getInput: { - notifications: (options) => options, - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/timetable.ts b/packages/cron-jobs/src/jobs/integrations/timetable.ts deleted file mode 100644 index b3964dc38..000000000 --- a/packages/cron-jobs/src/jobs/integrations/timetable.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { timetableGetTimetableRequestHandler } from "@homarr/request-handler/timetable"; - -import { createCronJob } from "../../lib"; - -export const timetableJob = createCronJob("timetable", EVERY_5_MINUTES).withCallback( - createRequestIntegrationJobHandler(timetableGetTimetableRequestHandler.handler, { - widgetKinds: ["timetable"], - getInput: { - timetable: (options) => - options.station - ? { - stationId: options.station.value, - limit: 10, - } - : [], - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/tracearr.ts b/packages/cron-jobs/src/jobs/integrations/tracearr.ts deleted file mode 100644 index f80130374..000000000 --- a/packages/cron-jobs/src/jobs/integrations/tracearr.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { tracearrRequestHandler } from "@homarr/request-handler/tracearr"; - -import { createCronJob } from "../../lib"; - -export const tracearrJob = createCronJob("tracearr", EVERY_5_SECONDS).withCallback( - createRequestIntegrationJobHandler(tracearrRequestHandler.handler, { - widgetKinds: ["tracearr"], - getInput: { - tracearr: () => ({}), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/integrations/umami.ts b/packages/cron-jobs/src/jobs/integrations/umami.ts deleted file mode 100644 index c50a3c604..000000000 --- a/packages/cron-jobs/src/jobs/integrations/umami.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EVERY_30_SECONDS } from "@homarr/cron-jobs-core/expressions"; -import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler"; -import { umamiActiveVisitorsRequestHandler } from "@homarr/request-handler/umami"; - -import { createCronJob } from "../../lib"; - -export const umamiActiveVisitorsJob = createCronJob("umamiActiveVisitors", EVERY_30_SECONDS).withCallback( - createRequestIntegrationJobHandler(umamiActiveVisitorsRequestHandler.handler, { - widgetKinds: ["umami"], - getInput: { - umami: (options) => (options.websiteId ? { websiteId: options.websiteId } : []), - }, - }), -); diff --git a/packages/cron-jobs/src/jobs/minecraft-server-status.ts b/packages/cron-jobs/src/jobs/minecraft-server-status.ts deleted file mode 100644 index e6c084702..000000000 --- a/packages/cron-jobs/src/jobs/minecraft-server-status.ts +++ /dev/null @@ -1,25 +0,0 @@ -import SuperJSON from "superjson"; - -import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { db, eq } from "@homarr/db"; -import { items } from "@homarr/db/schema"; -import { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status"; - -import type { WidgetComponentProps } from "../../../widgets/src"; -import { createCronJob } from "../lib"; - -export const minecraftServerStatusJob = createCronJob("minecraftServerStatus", EVERY_5_MINUTES).withCallback( - async () => { - const dbItems = await db.query.items.findMany({ - where: eq(items.kind, "minecraftServerStatus"), - }); - - await Promise.allSettled( - dbItems.map(async (item) => { - const options = SuperJSON.parse["options"]>(item.options); - const innerHandler = minecraftServerStatusRequestHandler.handler(options); - await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - }), - ); - }, -); diff --git a/packages/cron-jobs/src/jobs/rss-feeds.ts b/packages/cron-jobs/src/jobs/rss-feeds.ts deleted file mode 100644 index 6a497a043..000000000 --- a/packages/cron-jobs/src/jobs/rss-feeds.ts +++ /dev/null @@ -1,39 +0,0 @@ -import SuperJSON from "superjson"; - -import { createLogger } from "@homarr/core/infrastructure/logs"; -import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { db, eq } from "@homarr/db"; -import { items } from "@homarr/db/schema"; -import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds"; - -// This import is done that way to avoid circular dependencies. -import type { WidgetComponentProps } from "../../../widgets"; -import { createCronJob } from "../lib"; - -const logger = createLogger({ module: "rssFeedsJobs" }); - -export const rssFeedsJob = createCronJob("rssFeeds", EVERY_10_MINUTES).withCallback(async () => { - const rssItems = await db.query.items.findMany({ - where: eq(items.kind, "rssFeed"), - }); - - const itemOptions = rssItems.map((item) => SuperJSON.parse["options"]>(item.options)); - - for (const option of itemOptions) { - const maxAmountPosts = typeof option.maximumAmountPosts === "number" ? option.maximumAmountPosts : 100; - for (const url of option.feedUrls) { - try { - const innerHandler = rssFeedsRequestHandler.handler({ - url, - count: maxAmountPosts, - }); - await innerHandler.getCachedOrUpdatedDataAsync({ - forceUpdate: true, - }); - } catch (error) { - logger.error(new ErrorWithMetadata("Failed to update RSS feed", { url }, { cause: error })); - } - } - } -}); diff --git a/packages/cron-jobs/src/jobs/update-checker.ts b/packages/cron-jobs/src/jobs/update-checker.ts deleted file mode 100644 index eea68d07b..000000000 --- a/packages/cron-jobs/src/jobs/update-checker.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { EVERY_DAY } from "@homarr/cron-jobs-core/expressions"; -import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; - -import { createCronJob } from "../lib"; - -export const updateCheckerJob = createCronJob("updateChecker", EVERY_DAY, { - runOnStart: true, -}).withCallback(async () => { - const handler = updateCheckerRequestHandler.handler({}); - await handler.getCachedOrUpdatedDataAsync({}); -}); diff --git a/packages/cron-jobs/src/jobs/weather.ts b/packages/cron-jobs/src/jobs/weather.ts deleted file mode 100644 index 1b23ade4c..000000000 --- a/packages/cron-jobs/src/jobs/weather.ts +++ /dev/null @@ -1,36 +0,0 @@ -import SuperJSON from "superjson"; - -import { createLogger } from "@homarr/core/infrastructure/logs"; -import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions"; -import { db, eq } from "@homarr/db"; -import { items } from "@homarr/db/schema"; -import { weatherRequestHandler } from "@homarr/request-handler/weather"; - -import type { WidgetComponentProps } from "../../../widgets"; -import { createCronJob } from "../lib"; - -const logger = createLogger({ module: "weatherJobs" }); - -export const weatherJob = createCronJob("weather", EVERY_10_MINUTES).withCallback(async () => { - const weatherItems = await db.query.items.findMany({ - where: eq(items.kind, "weather"), - }); - - const parsedItems = weatherItems.map((item) => ({ - id: item.id, - options: SuperJSON.parse["options"]>(item.options), - })); - - for (const item of parsedItems) { - try { - const innerHandler = weatherRequestHandler.handler({ - longitude: item.options.location.longitude, - latitude: item.options.location.latitude, - }); - await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - } catch (error) { - logger.error(new ErrorWithMetadata("Failed to update weather", { id: item.id }, { cause: error })); - } - } -}); diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts index 6c99c11b6..4186948ce 100644 --- a/packages/redis/src/index.ts +++ b/packages/redis/src/index.ts @@ -4,17 +4,14 @@ import { createListChannel, createQueueChannel, createSubPubChannel } from "./li export { createCacheChannel, - createItemAndIntegrationChannel, - createItemChannel, - createIntegrationOptionsChannel, - createWidgetOptionsChannel, createChannelWithLatestAndEvents, createChannelEventHistory, handshakeAsync, createSubPubChannel, createGetSetChannel, - createQueryCacheChannel, - getAllQueryCacheAsync, + setQueryCacheAsync, + getQueryCacheAsync, + removeQueryCacheAsync, invalidateIntegrationCacheAsync, } from "./lib/channel"; diff --git a/packages/redis/src/lib/channel.ts b/packages/redis/src/lib/channel.ts index d04d1499a..848c47afd 100644 --- a/packages/redis/src/lib/channel.ts +++ b/packages/redis/src/lib/channel.ts @@ -1,8 +1,8 @@ import superjson from "superjson"; -import { createId, hashObjectBase64 } from "@homarr/common"; +import { createId } from "@homarr/common"; import { createLogger } from "@homarr/core/infrastructure/logs"; -import type { WidgetKind } from "@homarr/definitions"; + import { ChannelSubscriptionTracker } from "./channel-subscription-tracker"; import { createRedisConnection } from "./connection"; @@ -129,47 +129,27 @@ export const createGetSetChannel = (name: string) => { }; }; -interface QueryCacheChannelOptions { - userId: string; - boardId: string; - key: string; - ttlMs: number; - maxValueBytes: number; -} +const queryCacheKey = (userId: string, boardId: string) => `qc:${userId}:${boardId}`; +const queryCacheMaxValueBytes = 1024 * 1024; -const queryCacheHashKey = (userId: string, boardId: string) => `qc:${userId}:${boardId}`; - -export const createQueryCacheChannel = ({ userId, boardId, key, ttlMs, maxValueBytes }: QueryCacheChannelOptions) => { - const hkey = queryCacheHashKey(userId, boardId); - - return { - name: `${hkey}:${key}`, - getAsync: async () => { - return await getSetClient.hget(hkey, key); - }, - setAsync: async (value: string) => { - if (Buffer.byteLength(value, "utf8") > maxValueBytes) { - logger.warn("Query cache value exceeded maximum size", { - channel: hkey, - valueBytes: Buffer.byteLength(value, "utf8"), - maxValueBytes, - }); - return false; - } - - await getSetClient.hset(hkey, key, value); - await getSetClient.pexpire(hkey, ttlMs); - return true; - }, - removeAsync: async () => { - await getSetClient.hdel(hkey, key); - }, - }; +export const setQueryCacheAsync = async (userId: string, boardId: string, value: string, ttlMs: number) => { + if (Buffer.byteLength(value, "utf8") > queryCacheMaxValueBytes) { + logger.warn("Query cache value exceeded maximum size", { + key: queryCacheKey(userId, boardId), + valueBytes: Buffer.byteLength(value, "utf8"), + maxValueBytes: queryCacheMaxValueBytes, + }); + return false; + } + await getSetClient.set(queryCacheKey(userId, boardId), value, "PX", ttlMs); + return true; }; -export const getAllQueryCacheAsync = async (userId: string, boardId: string): Promise> => { - const hkey = queryCacheHashKey(userId, boardId); - return await getSetClient.hgetall(hkey); +export const getQueryCacheAsync = async (userId: string, boardId: string) => + await getSetClient.get(queryCacheKey(userId, boardId)); + +export const removeQueryCacheAsync = async (userId: string, boardId: string) => { + await getSetClient.del(queryCacheKey(userId, boardId)); }; /** @@ -246,20 +226,6 @@ export const createCacheChannel = (name: string, cacheDurationMs: number }; }; -export const createItemAndIntegrationChannel = (kind: WidgetKind, integrationId: string) => { - const channelName = `item:${kind}:integration:${integrationId}`; - return createChannelWithLatestAndEvents(channelName); -}; - -export const createIntegrationOptionsChannel = ( - integrationId: string, - queryKey: string, - options: Record, -) => { - const optionsKey = hashObjectBase64(options); - const channelName = `integration:${integrationId}:${queryKey}:options:${optionsKey}`; - return createChannelWithLatestAndEvents(channelName); -}; /** * Invalidates all cached data for a given integration by deleting every Redis key @@ -291,19 +257,6 @@ export const invalidateIntegrationCacheAsync = async (integrationId: string): Pr } }; -export const createWidgetOptionsChannel = ( - widgetKind: WidgetKind, - queryKey: string, - options: Record, -) => { - const optionsKey = hashObjectBase64(options); - const channelName = `widget:${widgetKind}:${queryKey}:options:${optionsKey}`; - return createChannelWithLatestAndEvents(channelName); -}; - -export const createItemChannel = (itemId: string) => { - return createChannelWithLatestAndEvents(`item:${itemId}`); -}; export const createChannelEventHistory = (channelName: string, maxElements = 32) => { return { diff --git a/packages/request-handler/src/anchor-notes.ts b/packages/request-handler/src/anchor-notes.ts index bd6f19e44..2d0bec2f7 100644 --- a/packages/request-handler/src/anchor-notes.ts +++ b/packages/request-handler/src/anchor-notes.ts @@ -1,27 +1,24 @@ -import dayjs from "dayjs"; import type { AnchorNote, AnchorNotesListInput, AnchorNoteSummary } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const anchorNotesListRequestHandler = createCachedIntegrationRequestHandler< +export const anchorNotesListRequestHandler = createIntegrationRequestHandler< AnchorNoteSummary[], "anchor", AnchorNotesListInput >({ queryKey: "anchorNotesList", - cacheDuration: dayjs.duration(15, "seconds"), async requestAsync(integration, input) { const instance = await createIntegrationAsync(integration); return instance.listNotesAsync(input); }, }); -export const anchorNoteRequestHandler = createCachedIntegrationRequestHandler( +export const anchorNoteRequestHandler = createIntegrationRequestHandler( { queryKey: "anchorNote", - cacheDuration: dayjs.duration(15, "seconds"), async requestAsync(integration, input) { const instance = await createIntegrationAsync(integration); return instance.getNoteAsync(input.noteId); diff --git a/packages/request-handler/src/archive-team-warrior.ts b/packages/request-handler/src/archive-team-warrior.ts index 4038ec9dc..172b41fe2 100644 --- a/packages/request-handler/src/archive-team-warrior.ts +++ b/packages/request-handler/src/archive-team-warrior.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { ArchiveTeamWarriorStatus } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const archiveTeamWarriorRequestHandler = createCachedIntegrationRequestHandler< +export const archiveTeamWarriorRequestHandler = createIntegrationRequestHandler< ArchiveTeamWarriorStatus, IntegrationKindByCategory<"archiving">, Record @@ -15,6 +14,5 @@ export const archiveTeamWarriorRequestHandler = createCachedIntegrationRequestHa const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getStatusAsync(); }, - cacheDuration: dayjs.duration(5, "minute"), queryKey: "archiveTeamWarriorStatus", }); diff --git a/packages/request-handler/src/audiobookshelf.ts b/packages/request-handler/src/audiobookshelf.ts index bb4b2d83c..57980d509 100644 --- a/packages/request-handler/src/audiobookshelf.ts +++ b/packages/request-handler/src/audiobookshelf.ts @@ -1,11 +1,10 @@ -import dayjs from "dayjs"; import { createIntegrationAsync } from "@homarr/integrations"; import type { AudiobookshelfDashboardData } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const audiobookshelfRequestHandler = createCachedIntegrationRequestHandler< +export const audiobookshelfRequestHandler = createIntegrationRequestHandler< AudiobookshelfDashboardData, "audiobookshelf", Record @@ -14,6 +13,5 @@ export const audiobookshelfRequestHandler = createCachedIntegrationRequestHandle const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, - cacheDuration: dayjs.duration(10, "minutes"), queryKey: "audiobookshelfDashboard", }); diff --git a/packages/request-handler/src/beszel.ts b/packages/request-handler/src/beszel.ts index 235677682..81d19ded2 100644 --- a/packages/request-handler/src/beszel.ts +++ b/packages/request-handler/src/beszel.ts @@ -1,5 +1,3 @@ -import dayjs from "dayjs"; - import { createLogger } from "@homarr/core/infrastructure/logs"; import { createIntegrationAsync } from "@homarr/integrations"; import type { @@ -12,7 +10,7 @@ import type { BeszelSystemStatsRecord, } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; const logger = createLogger({ module: "beszelRequestHandler" }); @@ -53,7 +51,7 @@ function mapToSystemRow(system: BeszelSystem, details: BeszelSystemDetails | nul }; } -export const beszelSystemsRequestHandler = createCachedIntegrationRequestHandler< +export const beszelSystemsRequestHandler = createIntegrationRequestHandler< BeszelSystemRow[], "beszel" | "mock", Record @@ -75,8 +73,6 @@ export const beszelSystemsRequestHandler = createCachedIntegrationRequestHandler }); return enriched; }, - cacheDuration: dayjs.duration(5, "seconds"), - fallbackToStaleOnError: true, retry: { attempts: 2, delayMs: 500 }, queryKey: "beszelSystems", }); @@ -86,7 +82,7 @@ export interface BeszelAlertsData { history: BeszelAlertHistory[]; } -export const beszelAlertsRequestHandler = createCachedIntegrationRequestHandler< +export const beszelAlertsRequestHandler = createIntegrationRequestHandler< BeszelAlertsData, "beszel" | "mock", { includeHistory: boolean; maxHistoryItems: number } @@ -104,8 +100,6 @@ export const beszelAlertsRequestHandler = createCachedIntegrationRequestHandler< }); return { alerts, history }; }, - cacheDuration: dayjs.duration(15, "seconds"), - fallbackToStaleOnError: true, retry: { attempts: 2, delayMs: 500 }, queryKey: "beszelAlerts", }); @@ -124,7 +118,7 @@ export interface BeszelStatsData { containerStats: BeszelContainerStatsRecord[]; } -export const beszelStatsRequestHandler = createCachedIntegrationRequestHandler< +export const beszelStatsRequestHandler = createIntegrationRequestHandler< BeszelStatsData, "beszel" | "mock", { systemId: string; timePeriod: string; includeDocker: boolean } @@ -147,13 +141,6 @@ export const beszelStatsRequestHandler = createCachedIntegrationRequestHandler< }); return { systemStats, containerStats }; }, - cacheDuration: dayjs.duration(60, "seconds"), - fallbackToStaleOnError: true, retry: { attempts: 2, delayMs: 500 }, queryKey: "beszelStats", - cacheDurationForInput(input) { - const config = timePeriodConfig[input.timePeriod]; - if (!config) return undefined; - return dayjs.duration(config.cacheSeconds, "seconds"); - }, }); diff --git a/packages/request-handler/src/calendar.ts b/packages/request-handler/src/calendar.ts index dd198912a..d3ff2414f 100644 --- a/packages/request-handler/src/calendar.ts +++ b/packages/request-handler/src/calendar.ts @@ -4,9 +4,9 @@ import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { CalendarEvent, RadarrReleaseType } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const calendarMonthRequestHandler = createCachedIntegrationRequestHandler< +export const calendarMonthRequestHandler = createIntegrationRequestHandler< CalendarEvent[], IntegrationKindByCategory<"calendar">, { year: number; month: number; releaseType: RadarrReleaseType[]; showUnmonitored: boolean } @@ -23,6 +23,5 @@ export const calendarMonthRequestHandler = createCachedIntegrationRequestHandler input.showUnmonitored, ); }, - cacheDuration: dayjs.duration(1, "minute"), queryKey: "calendarMonth", }); diff --git a/packages/request-handler/src/coolify.ts b/packages/request-handler/src/coolify.ts index d307a780f..0c44bbc2f 100644 --- a/packages/request-handler/src/coolify.ts +++ b/packages/request-handler/src/coolify.ts @@ -1,11 +1,10 @@ -import dayjs from "dayjs"; import { createIntegrationAsync } from "@homarr/integrations"; import type { CoolifyInstanceInfo } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const coolifyRequestHandler = createCachedIntegrationRequestHandler< +export const coolifyRequestHandler = createIntegrationRequestHandler< CoolifyInstanceInfo, "coolify", Record @@ -14,6 +13,5 @@ export const coolifyRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getInstanceInfoAsync(); }, - cacheDuration: dayjs.duration(30, "seconds"), queryKey: "coolifyInfo", }); diff --git a/packages/request-handler/src/dns-hole.ts b/packages/request-handler/src/dns-hole.ts index a8c920801..2603b1b96 100644 --- a/packages/request-handler/src/dns-hole.ts +++ b/packages/request-handler/src/dns-hole.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { DnsHoleSummary } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const dnsHoleRequestHandler = createCachedIntegrationRequestHandler< +export const dnsHoleRequestHandler = createIntegrationRequestHandler< DnsHoleSummary, IntegrationKindByCategory<"dnsHole">, Record @@ -15,6 +14,5 @@ export const dnsHoleRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getSummaryAsync(); }, - cacheDuration: dayjs.duration(5, "seconds"), queryKey: "dnsHoleSummary", }); diff --git a/packages/request-handler/src/docker.ts b/packages/request-handler/src/docker.ts index 2d228a598..29936bfb1 100644 --- a/packages/request-handler/src/docker.ts +++ b/packages/request-handler/src/docker.ts @@ -1,5 +1,4 @@ import type { Readable } from "node:stream"; -import dayjs from "dayjs"; import type { Container, ContainerInfo, ContainerStats } from "dockerode"; import type Dockerode from "dockerode"; @@ -13,7 +12,7 @@ import type { ContainerState, Port } from "@homarr/docker"; import { dockerLabels, DockerSingleton } from "@homarr/docker"; import { createDockerLogStreamProcessor, decodeDockerLogs } from "./docker-log-decode"; -import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; +import { createWidgetRequestHandler } from "./lib/widget-request-handler"; const logger = createLogger({ module: "dockerRequestHandler" }); @@ -171,7 +170,7 @@ const mockContainers: { }, ]; -export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({ +export const dockerContainersRequestHandler = createWidgetRequestHandler({ queryKey: "dockerContainersResult", widgetKind: "dockerContainers", async requestAsync() { @@ -180,7 +179,6 @@ export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({ } return await getContainersWithStatsAsync(); }, - cacheDuration: dayjs.duration(20, "seconds"), }); const extractImage = (container: ContainerInfo) => extractContainerImageName(container.Image); diff --git a/packages/request-handler/src/downloads.ts b/packages/request-handler/src/downloads.ts index 06d4e9c64..1b6ac6378 100644 --- a/packages/request-handler/src/downloads.ts +++ b/packages/request-handler/src/downloads.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { DownloadClientJobsAndStatus } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const downloadClientRequestHandler = createCachedIntegrationRequestHandler< +export const downloadClientRequestHandler = createIntegrationRequestHandler< DownloadClientJobsAndStatus, IntegrationKindByCategory<"downloadClient">, { limit: number } @@ -15,6 +14,5 @@ export const downloadClientRequestHandler = createCachedIntegrationRequestHandle const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getClientJobsAndStatusAsync(input); }, - cacheDuration: dayjs.duration(5, "seconds"), queryKey: "downloadClientJobStatus", }); diff --git a/packages/request-handler/src/duckduckgo-bangs.ts b/packages/request-handler/src/duckduckgo-bangs.ts index a51bf5327..8b2bd4877 100644 --- a/packages/request-handler/src/duckduckgo-bangs.ts +++ b/packages/request-handler/src/duckduckgo-bangs.ts @@ -1,12 +1,10 @@ -import dayjs from "dayjs"; import { z } from "zod"; import { env } from "@homarr/common/env"; import { ResponseError } from "@homarr/common/server"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; -import { createChannelWithLatestAndEvents } from "@homarr/redis"; -import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; +import { createRequestHandler } from "@homarr/request-handler/lib/request-handler"; export type DuckDuckGoBang = z.infer; @@ -38,9 +36,8 @@ const normalizeBangToken = (token: string) => token.toLowerCase().trim(); const compareBangTokenAsc = (left: DuckDuckGoBang, right: DuckDuckGoBang) => left.t.localeCompare(right.t); -export const duckDuckGoBangsRequestHandler = createCachedRequestHandler({ +export const duckDuckGoBangsRequestHandler = createRequestHandler({ queryKey: "duckduckgo-bangs", - cacheDuration: dayjs.duration(1, "day"), async requestAsync(_) { if (env.NO_EXTERNAL_CONNECTION) { return []; @@ -73,7 +70,4 @@ export const duckDuckGoBangsRequestHandler = createCachedRequestHandler({ return normalized; }, - createRedisChannel() { - return createChannelWithLatestAndEvents("homarr:duckduckgo-bangs"); - }, }); diff --git a/packages/request-handler/src/firewall.ts b/packages/request-handler/src/firewall.ts index 3754f1099..0bfd7b721 100644 --- a/packages/request-handler/src/firewall.ts +++ b/packages/request-handler/src/firewall.ts @@ -1,4 +1,3 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; @@ -9,9 +8,9 @@ import type { FirewallVersionSummary, } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const firewallCpuRequestHandler = createCachedIntegrationRequestHandler< +export const firewallCpuRequestHandler = createIntegrationRequestHandler< FirewallCpuSummary, IntegrationKindByCategory<"firewall">, Record @@ -20,11 +19,10 @@ export const firewallCpuRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return integrationInstance.getFirewallCpuAsync(); }, - cacheDuration: dayjs.duration(5, "seconds"), queryKey: "firewallCpuSummary", }); -export const firewallMemoryRequestHandler = createCachedIntegrationRequestHandler< +export const firewallMemoryRequestHandler = createIntegrationRequestHandler< FirewallMemorySummary, IntegrationKindByCategory<"firewall">, Record @@ -33,11 +31,10 @@ export const firewallMemoryRequestHandler = createCachedIntegrationRequestHandle const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getFirewallMemoryAsync(); }, - cacheDuration: dayjs.duration(15, "seconds"), queryKey: "firewallMemorySummary", }); -export const firewallInterfacesRequestHandler = createCachedIntegrationRequestHandler< +export const firewallInterfacesRequestHandler = createIntegrationRequestHandler< FirewallInterfacesSummary[], IntegrationKindByCategory<"firewall">, Record @@ -46,11 +43,10 @@ export const firewallInterfacesRequestHandler = createCachedIntegrationRequestHa const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getFirewallInterfacesAsync(); }, - cacheDuration: dayjs.duration(30, "seconds"), queryKey: "firewallInterfacesSummary", }); -export const firewallVersionRequestHandler = createCachedIntegrationRequestHandler< +export const firewallVersionRequestHandler = createIntegrationRequestHandler< FirewallVersionSummary, IntegrationKindByCategory<"firewall">, Record @@ -59,6 +55,5 @@ export const firewallVersionRequestHandler = createCachedIntegrationRequestHandl const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getFirewallVersionAsync(); }, - cacheDuration: dayjs.duration(1, "hour"), queryKey: "firewallVersionSummary", }); diff --git a/packages/request-handler/src/health-monitoring.ts b/packages/request-handler/src/health-monitoring.ts index 640eb6b97..5c5051983 100644 --- a/packages/request-handler/src/health-monitoring.ts +++ b/packages/request-handler/src/health-monitoring.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { ProxmoxClusterInfo, SystemHealthMonitoring } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const systemInfoRequestHandler = createCachedIntegrationRequestHandler< +export const systemInfoRequestHandler = createIntegrationRequestHandler< SystemHealthMonitoring, Exclude, "proxmox" | "coolify" | "beszel">, Record @@ -15,11 +14,10 @@ export const systemInfoRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getSystemInfoAsync(); }, - cacheDuration: dayjs.duration(5, "seconds"), queryKey: "systemInfo", }); -export const clusterInfoRequestHandler = createCachedIntegrationRequestHandler< +export const clusterInfoRequestHandler = createIntegrationRequestHandler< ProxmoxClusterInfo, "proxmox" | "mock", Record @@ -28,6 +26,5 @@ export const clusterInfoRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getClusterInfoAsync(); }, - cacheDuration: dayjs.duration(5, "seconds"), queryKey: "clusterInfo", }); diff --git a/packages/request-handler/src/immich.ts b/packages/request-handler/src/immich.ts index e95cb32b8..4f8341b76 100644 --- a/packages/request-handler/src/immich.ts +++ b/packages/request-handler/src/immich.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { ImmichAlbum, ImmichServerStats } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const immichStatsRequestHandler = createCachedIntegrationRequestHandler< +export const immichStatsRequestHandler = createIntegrationRequestHandler< ImmichServerStats, IntegrationKindByCategory<"photoService">, Record @@ -15,11 +14,10 @@ export const immichStatsRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getServerStatsAsync(); }, - cacheDuration: dayjs.duration(15, "minute"), queryKey: "immich-server-stats", }); -export const immichAlbumsRequestHandler = createCachedIntegrationRequestHandler< +export const immichAlbumsRequestHandler = createIntegrationRequestHandler< { id: string; albumName: string; @@ -32,11 +30,10 @@ export const immichAlbumsRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getAlbumsAsync(); }, - cacheDuration: dayjs.duration(15, "minute"), queryKey: "immich-albums", }); -export const immichAlbumRequestHandler = createCachedIntegrationRequestHandler< +export const immichAlbumRequestHandler = createIntegrationRequestHandler< ImmichAlbum, IntegrationKindByCategory<"photoService">, { albumId: string } @@ -45,6 +42,5 @@ export const immichAlbumRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getAlbumAsync(input.albumId); }, - cacheDuration: dayjs.duration(15, "minute"), queryKey: "immich-album", }); diff --git a/packages/request-handler/src/indexer-manager.ts b/packages/request-handler/src/indexer-manager.ts index a957b32ad..f87d2184c 100644 --- a/packages/request-handler/src/indexer-manager.ts +++ b/packages/request-handler/src/indexer-manager.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { Indexer } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const indexerManagerRequestHandler = createCachedIntegrationRequestHandler< +export const indexerManagerRequestHandler = createIntegrationRequestHandler< Indexer[], IntegrationKindByCategory<"indexerManager">, Record @@ -15,6 +14,5 @@ export const indexerManagerRequestHandler = createCachedIntegrationRequestHandle const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getIndexersAsync(); }, - cacheDuration: dayjs.duration(5, "minutes"), queryKey: "indexerManager", }); diff --git a/packages/request-handler/src/lib/cached-request-handler.ts b/packages/request-handler/src/lib/cached-request-handler.ts deleted file mode 100644 index c2342eed3..000000000 --- a/packages/request-handler/src/lib/cached-request-handler.ts +++ /dev/null @@ -1,160 +0,0 @@ -import dayjs from "dayjs"; -import type { Duration } from "dayjs/plugin/duration"; - -import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { createLogger } from "@homarr/core/infrastructure/logs"; -import type { createChannelWithLatestAndEvents } from "@homarr/redis"; - -const logger = createLogger({ module: "cachedRequestHandler" }); - -type FetchResult = { data: TData; timestamp: Date }; -type InFlightEntry = Promise>; - -const inFlightFetches = new Map(); - -const isAbortedError = (error: unknown): boolean => { - if (!(error instanceof Error)) return false; - return error.name === "AbortError" || error.name === "TimeoutError"; -}; - -interface Options> { - // Unique key for this request handler - queryKey: string; - requestAsync: (input: TInput) => Promise; - createRedisChannel: ( - input: TInput, - options: Options, - ) => ReturnType>; - cacheDuration: Duration; - fallbackToStaleOnError?: boolean; - // Retry the upstream call on failure. attempts=1 means no retry (default). - // AbortError / TimeoutError are never retried. - retry?: { attempts?: number; delayMs?: number }; - // Predicate to decide whether a fetched response is worth caching. - // Return false to skip the cache write (next call will refetch). - isValid?: (data: TData) => boolean; -} - -const defaultRetry = { attempts: 1, delayMs: 0 }; -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -export const createCachedRequestHandler = >( - options: Options, -) => { - return { - handler: (input: TInput) => { - const channel = options.createRedisChannel(input, options); - const retryConfig = { ...defaultRetry, ...options.retry }; - - return { - async getCachedOrUpdatedDataAsync({ forceUpdate = false }) { - const channelData = await channel.getAsync(); - - const requestNewDataAsync = (): Promise> => { - const inflight = inFlightFetches.get(channel.name); - if (inflight) { - logger.debug("Cached request handler joining in-flight fetch", { - channel: channel.name, - queryKey: options.queryKey, - }); - return inflight as Promise>; - } - - const promise = (async (): Promise> => { - 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("Cached request handler received invalid data, not caching", { - channel: channel.name, - queryKey: options.queryKey, - attempt, - }); - return { data, timestamp: new Date() }; - } - await channel.publishAndUpdateLastStateAsync(data); - return { data, timestamp: new Date() }; - } catch (error) { - lastError = error; - if (isAbortedError(error) || attempt === retryConfig.attempts) { - break; - } - logger.warn("Cached request handler fetch failed, retrying", { - channel: channel.name, - queryKey: options.queryKey, - attempt, - nextDelayMs: retryConfig.delayMs, - error: error instanceof Error ? `${error.name}: ${error.message}` : String(error), - }); - await sleep(retryConfig.delayMs); - } - } - - if (options.fallbackToStaleOnError && channelData) { - logger.warn( - new ErrorWithMetadata( - "Cached request handler using stale cache after fetch failure", - { - channel: channel.name, - queryKey: options.queryKey, - attempts: retryConfig.attempts, - }, - { cause: lastError }, - ), - ); - return channelData; - } - - throw lastError; - })().finally(() => { - inFlightFetches.delete(channel.name); - }); - - inFlightFetches.set(channel.name, promise as InFlightEntry); - return promise; - }; - - if (forceUpdate) { - logger.debug("Cached request handler forced update", { - channel: channel.name, - queryKey: options.queryKey, - }); - return await requestNewDataAsync(); - } - - const shouldRequestNewData = - !channelData || - dayjs().diff(channelData.timestamp, "milliseconds") > options.cacheDuration.asMilliseconds(); - - if (shouldRequestNewData) { - logger.debug("Cached request handler cache miss", { - channel: channel.name, - queryKey: options.queryKey, - reason: !channelData ? "no data" : "cache expired", - }); - return await requestNewDataAsync(); - } - - logger.debug("Cached request handler cache hit", { - channel: channel.name, - queryKey: options.queryKey, - expiresAt: dayjs(channelData.timestamp).add(options.cacheDuration).toISOString(), - }); - - return channelData; - }, - async invalidateAsync() { - logger.debug("Cached request handler invalidating cache", { - channel: channel.name, - queryKey: options.queryKey, - }); - await this.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - }, - subscribe(callback: (data: TData) => void) { - return channel.subscribe(callback); - }, - }; - }, - }; -}; diff --git a/packages/request-handler/src/lib/cached-request-integration-job-handler.ts b/packages/request-handler/src/lib/cached-request-integration-job-handler.ts deleted file mode 100644 index 63e7682c2..000000000 --- a/packages/request-handler/src/lib/cached-request-integration-job-handler.ts +++ /dev/null @@ -1,115 +0,0 @@ -import SuperJSON from "superjson"; - -import { hashObjectBase64, Stopwatch } from "@homarr/common"; -import { decryptSecret } from "@homarr/common/server"; -import type { MaybeArray } from "@homarr/common/types"; -import { createLogger } from "@homarr/core/infrastructure/logs"; -import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; -import { db } from "@homarr/db"; -import { getItemsWithIntegrationsAsync, getServerSettingsAsync } from "@homarr/db/queries"; -import type { WidgetKind } from "@homarr/definitions"; - -// This imports are done that way to avoid circular dependencies. -import type { inferSupportedIntegrationsStrict } from "../../../widgets/src"; -import { reduceWidgetOptionsWithDefaultValues } from "../../../widgets/src"; -import type { WidgetComponentProps } from "../../../widgets/src/definition"; -import type { createCachedIntegrationRequestHandler } from "./cached-integration-request-handler"; - -const logger = createLogger({ module: "cachedRequestIntegrationJobHandler" }); - -export const createRequestIntegrationJobHandler = < - TWidgetKind extends WidgetKind, - TIntegrationKind extends inferSupportedIntegrationsStrict, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - THandler extends ReturnType>["handler"], ->( - handler: THandler, - { - widgetKinds, - getInput, - }: { - widgetKinds: TWidgetKind[]; - getInput: { - [key in TWidgetKind]: (options: WidgetComponentProps["options"]) => MaybeArray[1]>; - }; - }, -) => { - return async () => { - const serverSettings = await getServerSettingsAsync(db); - const itemsForIntegration = await getItemsWithIntegrationsAsync(db, { - kinds: widgetKinds, - }); - - logger.debug("Found items for integration", { - widgetKinds: widgetKinds.join(","), - count: itemsForIntegration.length, - }); - - const distinctIntegrations: { - integrationId: string; - inputHash: string; - integration: (typeof itemsForIntegration)[number]["integrations"][number]["integration"]; - input: Parameters[1]; - }[] = []; - - for (const itemForIntegration of itemsForIntegration) { - const oneOrMultipleInputs = getInput[itemForIntegration.kind]( - reduceWidgetOptionsWithDefaultValues( - itemForIntegration.kind, - { - enableStatusByDefault: serverSettings.board.enableStatusByDefault, - forceDisableStatus: serverSettings.board.forceDisableStatus, - }, - SuperJSON.parse>(itemForIntegration.options), - ) as never, - ); - for (const { integration } of itemForIntegration.integrations) { - const inputArray = Array.isArray(oneOrMultipleInputs) ? oneOrMultipleInputs : [oneOrMultipleInputs]; - - for (const input of inputArray) { - const inputHash = hashObjectBase64(input); - if ( - distinctIntegrations.some( - (distinctIntegration) => - distinctIntegration.integrationId === integration.id && distinctIntegration.inputHash === inputHash, - ) - ) { - continue; - } - - distinctIntegrations.push({ integrationId: integration.id, inputHash, integration, input }); - } - } - } - - for (const { integrationId, integration, input, inputHash } of distinctIntegrations) { - try { - const decryptedSecrets = integration.secrets.map((secret) => ({ - ...secret, - value: decryptSecret(secret.value), - })); - - const innerHandler = handler( - { - ...integration, - kind: integration.kind as TIntegrationKind, - decryptedSecrets, - externalUrl: integration.app?.href ?? null, - }, - input, - ); - const stopWatch = new Stopwatch(); - await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true }); - logger.debug("Ran integration job", { - integration: integrationId, - inputHash, - elapsed: stopWatch.getElapsedInHumanWords(), - }); - } catch (error) { - logger.error( - new ErrorWithMetadata("Failed to run integration job", { integrationId, inputHash }, { cause: error }), - ); - } - } - }; -}; diff --git a/packages/request-handler/src/lib/cached-widget-request-handler.ts b/packages/request-handler/src/lib/cached-widget-request-handler.ts deleted file mode 100644 index 4016bc5b4..000000000 --- a/packages/request-handler/src/lib/cached-widget-request-handler.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Duration } from "dayjs/plugin/duration"; - -import type { WidgetKind } from "@homarr/definitions"; -import { createWidgetOptionsChannel } from "@homarr/redis"; - -import { createCachedRequestHandler } from "./cached-request-handler"; - -interface Options> { - // Unique key for this request handler - queryKey: string; - requestAsync: (input: TInput) => Promise; - cacheDuration: Duration; - widgetKind: TKind; -} - -export const createCachedWidgetRequestHandler = < - TData, - TKind extends WidgetKind, - TInput extends Record, ->( - requestHandlerOptions: Options, -) => { - return { - handler: (widgetOptions: TInput) => - createCachedRequestHandler({ - queryKey: requestHandlerOptions.queryKey, - requestAsync: async (input: TInput) => { - return await requestHandlerOptions.requestAsync(input); - }, - cacheDuration: requestHandlerOptions.cacheDuration, - createRedisChannel(input, options) { - return createWidgetOptionsChannel(requestHandlerOptions.widgetKind, options.queryKey, input); - }, - }).handler(widgetOptions), - }; -}; diff --git a/packages/request-handler/src/lib/cached-integration-request-handler.ts b/packages/request-handler/src/lib/integration-request-handler.ts similarity index 53% rename from packages/request-handler/src/lib/cached-integration-request-handler.ts rename to packages/request-handler/src/lib/integration-request-handler.ts index aee69e39c..45b089ca9 100644 --- a/packages/request-handler/src/lib/cached-integration-request-handler.ts +++ b/packages/request-handler/src/lib/integration-request-handler.ts @@ -1,11 +1,8 @@ -import type { Duration } from "dayjs/plugin/duration"; - import type { Modify } from "@homarr/common/types"; import type { Integration, IntegrationSecret } from "@homarr/db/schema"; import type { IntegrationKind } from "@homarr/definitions"; -import { createIntegrationOptionsChannel } from "@homarr/redis"; -import { createCachedRequestHandler } from "./cached-request-handler"; +import { createRequestHandler } from "./request-handler"; type IntegrationOfKind = Omit & { kind: TKind; @@ -16,14 +13,11 @@ type IntegrationOfKind = Omit> { queryKey: string; requestAsync: (integration: IntegrationOfKind, input: TInput) => Promise; - cacheDuration: Duration; - cacheDurationForInput?: (input: TInput) => Duration | undefined; - fallbackToStaleOnError?: boolean; retry?: { attempts?: number; delayMs?: number }; isValid?: (data: TData) => boolean; } -export const createCachedIntegrationRequestHandler = < +export const createIntegrationRequestHandler = < TData, TKind extends IntegrationKind, TInput extends Record, @@ -32,18 +26,11 @@ export const createCachedIntegrationRequestHandler = < ) => { return { handler: (integration: IntegrationOfKind, itemOptions: TInput) => - createCachedRequestHandler({ + createRequestHandler }>({ queryKey: options.queryKey, - requestAsync: async (input: { options: TInput; integration: IntegrationOfKind }) => { - return await options.requestAsync(input.integration, input.options); - }, - cacheDuration: options.cacheDurationForInput?.(itemOptions) ?? options.cacheDuration, - fallbackToStaleOnError: options.fallbackToStaleOnError, + requestAsync: async (input) => options.requestAsync(input.integration, input.options), retry: options.retry, isValid: options.isValid, - createRedisChannel(input, handlerOptions) { - return createIntegrationOptionsChannel(input.integration.id, handlerOptions.queryKey, input.options); - }, }).handler({ options: itemOptions, integration }), }; }; diff --git a/packages/request-handler/src/lib/request-handler.ts b/packages/request-handler/src/lib/request-handler.ts new file mode 100644 index 000000000..7e605dd6a --- /dev/null +++ b/packages/request-handler/src/lib/request-handler.ts @@ -0,0 +1,59 @@ +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> { + queryKey: string; + requestAsync: (input: TInput) => Promise; + retry?: { attempts?: number; delayMs?: number }; + isValid?: (data: TData) => boolean; +} + +const defaultRetry = { attempts: 1, delayMs: 0 }; +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export const createRequestHandler = >( + options: Options, +) => { + 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; + }, + }; + }, + }; +}; diff --git a/packages/request-handler/src/lib/widget-request-handler.ts b/packages/request-handler/src/lib/widget-request-handler.ts new file mode 100644 index 000000000..2bd41142f --- /dev/null +++ b/packages/request-handler/src/lib/widget-request-handler.ts @@ -0,0 +1,25 @@ +import type { WidgetKind } from "@homarr/definitions"; + +import { createRequestHandler } from "./request-handler"; + +interface Options> { + queryKey: string; + requestAsync: (input: TInput) => Promise; + retry?: { attempts?: number; delayMs?: number }; + isValid?: (data: TData) => boolean; + widgetKind: TKind; +} + +export const createWidgetRequestHandler = >( + options: Options, +) => { + return { + handler: (widgetOptions: TInput) => + createRequestHandler({ + queryKey: options.queryKey, + requestAsync: options.requestAsync, + retry: options.retry, + isValid: options.isValid, + }).handler(widgetOptions), + }; +}; diff --git a/packages/request-handler/src/media-release.ts b/packages/request-handler/src/media-release.ts index c0ffe48f3..442730c9b 100644 --- a/packages/request-handler/src/media-release.ts +++ b/packages/request-handler/src/media-release.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRelease } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const mediaReleaseRequestHandler = createCachedIntegrationRequestHandler< +export const mediaReleaseRequestHandler = createIntegrationRequestHandler< MediaRelease[], IntegrationKindByCategory<"mediaRelease">, Record @@ -15,6 +14,5 @@ export const mediaReleaseRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getMediaReleasesAsync(); }, - cacheDuration: dayjs.duration(5, "minutes"), queryKey: "mediaReleases", }); diff --git a/packages/request-handler/src/media-request-list.ts b/packages/request-handler/src/media-request-list.ts index fd4e02234..e101ef4f8 100644 --- a/packages/request-handler/src/media-request-list.ts +++ b/packages/request-handler/src/media-request-list.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRequest } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const mediaRequestListRequestHandler = createCachedIntegrationRequestHandler< +export const mediaRequestListRequestHandler = createIntegrationRequestHandler< MediaRequest[], IntegrationKindByCategory<"mediaRequest">, Record @@ -15,6 +14,5 @@ export const mediaRequestListRequestHandler = createCachedIntegrationRequestHand const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getRequestsAsync(); }, - cacheDuration: dayjs.duration(1, "minute"), queryKey: "mediaRequestList", }); diff --git a/packages/request-handler/src/media-request-stats.ts b/packages/request-handler/src/media-request-stats.ts index a6568aecd..f692835a7 100644 --- a/packages/request-handler/src/media-request-stats.ts +++ b/packages/request-handler/src/media-request-stats.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRequestStats } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const mediaRequestStatsRequestHandler = createCachedIntegrationRequestHandler< +export const mediaRequestStatsRequestHandler = createIntegrationRequestHandler< MediaRequestStats, IntegrationKindByCategory<"mediaRequest">, Record @@ -18,6 +17,5 @@ export const mediaRequestStatsRequestHandler = createCachedIntegrationRequestHan users: await integrationInstance.getUsersAsync(), }; }, - cacheDuration: dayjs.duration(1, "minute"), queryKey: "mediaRequestStats", }); diff --git a/packages/request-handler/src/media-server.ts b/packages/request-handler/src/media-server.ts index b13b90eb0..7bb818622 100644 --- a/packages/request-handler/src/media-server.ts +++ b/packages/request-handler/src/media-server.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { StreamSession } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const mediaServerRequestHandler = createCachedIntegrationRequestHandler< +export const mediaServerRequestHandler = createIntegrationRequestHandler< StreamSession[], IntegrationKindByCategory<"mediaService">, { @@ -17,6 +16,5 @@ export const mediaServerRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getCurrentSessionsAsync({ showOnlyPlaying: input.showOnlyPlaying }); }, - cacheDuration: dayjs.duration(5, "seconds"), queryKey: "mediaServerSessions", }); diff --git a/packages/request-handler/src/media-transcoding.ts b/packages/request-handler/src/media-transcoding.ts index 698d7347b..76d48a21d 100644 --- a/packages/request-handler/src/media-transcoding.ts +++ b/packages/request-handler/src/media-transcoding.ts @@ -1,18 +1,16 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const mediaTranscodingRequestHandler = createCachedIntegrationRequestHandler< +export const mediaTranscodingRequestHandler = createIntegrationRequestHandler< MediaTranscoding, IntegrationKindByCategory<"mediaTranscoding">, { pageOffset: number; pageSize: number } >({ queryKey: "mediaTranscoding", - cacheDuration: dayjs.duration(5, "minutes"), async requestAsync(integration, input) { const integrationInstance = await createIntegrationAsync(integration); return { diff --git a/packages/request-handler/src/minecraft-server-status.ts b/packages/request-handler/src/minecraft-server-status.ts index 98d850171..c015afa7b 100644 --- a/packages/request-handler/src/minecraft-server-status.ts +++ b/packages/request-handler/src/minecraft-server-status.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; -import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; +import { createWidgetRequestHandler } from "./lib/widget-request-handler"; -export const minecraftServerStatusRequestHandler = createCachedWidgetRequestHandler({ +export const minecraftServerStatusRequestHandler = createWidgetRequestHandler({ queryKey: "minecraftServerStatusApiResult", widgetKind: "minecraftServerStatus", async requestAsync(input: { domain: string; isBedrockServer: boolean }) { @@ -17,7 +16,6 @@ export const minecraftServerStatusRequestHandler = createCachedWidgetRequestHand ); return responseSchema.parse(await response.json()); }, - cacheDuration: dayjs.duration(5, "minutes"), }); const responseSchema = z diff --git a/packages/request-handler/src/navidrome.ts b/packages/request-handler/src/navidrome.ts index b59f852a8..ca19ccbe2 100644 --- a/packages/request-handler/src/navidrome.ts +++ b/packages/request-handler/src/navidrome.ts @@ -1,11 +1,10 @@ -import dayjs from "dayjs"; import { createIntegrationAsync } from "@homarr/integrations"; import type { NavidromeDashboardData } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const navidromeRequestHandler = createCachedIntegrationRequestHandler< +export const navidromeRequestHandler = createIntegrationRequestHandler< NavidromeDashboardData, "navidrome", Record @@ -14,6 +13,5 @@ export const navidromeRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, - cacheDuration: dayjs.duration(10, "minutes"), queryKey: "navidromeDashboard", }); diff --git a/packages/request-handler/src/network-controller.ts b/packages/request-handler/src/network-controller.ts index f5975acf2..26fe3693a 100644 --- a/packages/request-handler/src/network-controller.ts +++ b/packages/request-handler/src/network-controller.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { NetworkControllerSummary } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const networkControllerRequestHandler = createCachedIntegrationRequestHandler< +export const networkControllerRequestHandler = createIntegrationRequestHandler< NetworkControllerSummary, IntegrationKindByCategory<"networkController">, Record @@ -15,6 +14,5 @@ export const networkControllerRequestHandler = createCachedIntegrationRequestHan const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getNetworkSummaryAsync(); }, - cacheDuration: dayjs.duration(5, "minutes"), queryKey: "networkControllerSummary", }); diff --git a/packages/request-handler/src/notifications.ts b/packages/request-handler/src/notifications.ts index 41098e660..f244d1ba9 100644 --- a/packages/request-handler/src/notifications.ts +++ b/packages/request-handler/src/notifications.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { Notification } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const notificationsRequestHandler = createCachedIntegrationRequestHandler< +export const notificationsRequestHandler = createIntegrationRequestHandler< Notification[], IntegrationKindByCategory<"notifications">, Record @@ -15,6 +14,5 @@ export const notificationsRequestHandler = createCachedIntegrationRequestHandler const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getNotificationsAsync(); }, - cacheDuration: dayjs.duration(5, "minutes"), queryKey: "notificationsJobStatus", }); diff --git a/packages/request-handler/src/paperless-ngx.ts b/packages/request-handler/src/paperless-ngx.ts index a3793658f..b2d6d3675 100644 --- a/packages/request-handler/src/paperless-ngx.ts +++ b/packages/request-handler/src/paperless-ngx.ts @@ -1,11 +1,10 @@ -import dayjs from "dayjs"; import { createIntegrationAsync } from "@homarr/integrations"; import type { PaperlessNgxStats } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const paperlessNgxStatsRequestHandler = createCachedIntegrationRequestHandler< +export const paperlessNgxStatsRequestHandler = createIntegrationRequestHandler< PaperlessNgxStats, "paperlessNgx", Record @@ -14,6 +13,5 @@ export const paperlessNgxStatsRequestHandler = createCachedIntegrationRequestHan const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getStatsAsync(); }, - cacheDuration: dayjs.duration(15, "minute"), queryKey: "paperless-ngx-stats", }); diff --git a/packages/request-handler/src/releases.ts b/packages/request-handler/src/releases.ts index 7bfd29348..2f50782fa 100644 --- a/packages/request-handler/src/releases.ts +++ b/packages/request-handler/src/releases.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { ReleaseResponse, ReleasesRepository } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const releasesRequestHandler = createCachedIntegrationRequestHandler< +export const releasesRequestHandler = createIntegrationRequestHandler< ReleaseResponse, IntegrationKindByCategory<"releasesProvider">, ReleasesRepository @@ -15,6 +14,5 @@ export const releasesRequestHandler = createCachedIntegrationRequestHandler< const instance = await createIntegrationAsync(integration); return instance.getLatestMatchingReleaseAsync(input.identifier, input.versionRegex); }, - cacheDuration: dayjs.duration(5, "minutes"), queryKey: "repositoriesReleases", }); diff --git a/packages/request-handler/src/rss-feeds.ts b/packages/request-handler/src/rss-feeds.ts index 9eaf026ea..6e1a3f364 100644 --- a/packages/request-handler/src/rss-feeds.ts +++ b/packages/request-handler/src/rss-feeds.ts @@ -1,16 +1,15 @@ import type { FeedData, FeedEntry } from "@extractus/feed-extractor"; import { extract } from "@extractus/feed-extractor"; -import dayjs from "dayjs"; import { z } from "zod/v4"; import type { Modify } from "@homarr/common/types"; import { createLogger } from "@homarr/core/infrastructure/logs"; -import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; +import { createWidgetRequestHandler } from "./lib/widget-request-handler"; const logger = createLogger({ module: "rssFeedsRequestHandler" }); -export const rssFeedsRequestHandler = createCachedWidgetRequestHandler({ +export const rssFeedsRequestHandler = createWidgetRequestHandler({ queryKey: "rssFeedList", widgetKind: "rssFeed", async requestAsync(input: { url: string; count: number }) { @@ -31,7 +30,6 @@ export const rssFeedsRequestHandler = createCachedWidgetRequestHandler({ entries: result.entries?.map((entry) => ({ ...entry, feedUrl: input.url })).slice(0, input.count) ?? [], }; }, - cacheDuration: dayjs.duration(5, "minutes"), }); const attemptGetImageFromEntry = (feedUrl: string, entry: object) => { diff --git a/packages/request-handler/src/smart-home-entity-state.ts b/packages/request-handler/src/smart-home-entity-state.ts index 5fe02b2ac..c80ffce4d 100644 --- a/packages/request-handler/src/smart-home-entity-state.ts +++ b/packages/request-handler/src/smart-home-entity-state.ts @@ -1,11 +1,10 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const smartHomeEntityStateRequestHandler = createCachedIntegrationRequestHandler< +export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandler< string, IntegrationKindByCategory<"smartHomeServer">, { entityId: string } @@ -20,6 +19,5 @@ export const smartHomeEntityStateRequestHandler = createCachedIntegrationRequest return result.data.state; }, - cacheDuration: dayjs.duration(1, "minute"), queryKey: "smartHome-entityState", }); diff --git a/packages/request-handler/src/speedtest-tracker.ts b/packages/request-handler/src/speedtest-tracker.ts index 799d37f16..bd435ebe3 100644 --- a/packages/request-handler/src/speedtest-tracker.ts +++ b/packages/request-handler/src/speedtest-tracker.ts @@ -1,11 +1,10 @@ -import dayjs from "dayjs"; import { createIntegrationAsync } from "@homarr/integrations"; import type { SpeedtestTrackerDashboardData } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const speedtestTrackerRequestHandler = createCachedIntegrationRequestHandler< +export const speedtestTrackerRequestHandler = createIntegrationRequestHandler< SpeedtestTrackerDashboardData, "speedtestTracker", Record @@ -14,6 +13,5 @@ export const speedtestTrackerRequestHandler = createCachedIntegrationRequestHand const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, - cacheDuration: dayjs.duration(10, "minutes"), queryKey: "speedtestTrackerDashboard", }); diff --git a/packages/request-handler/src/stock-price.ts b/packages/request-handler/src/stock-price.ts index b0ca15e47..abc9abe0b 100644 --- a/packages/request-handler/src/stock-price.ts +++ b/packages/request-handler/src/stock-price.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import { z } from "zod/v4"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; -import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; +import { createWidgetRequestHandler } from "./lib/widget-request-handler"; -export const fetchStockPriceHandler = createCachedWidgetRequestHandler({ +export const fetchStockPriceHandler = createWidgetRequestHandler({ queryKey: "fetchStockPriceResult", widgetKind: "stockPrice", async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) { @@ -42,7 +41,6 @@ export const fetchStockPriceHandler = createCachedWidgetRequestHandler({ shortName: firstResult.meta.shortName, }; }, - cacheDuration: dayjs.duration(5, "minutes"), }); const dataSchema = z diff --git a/packages/request-handler/src/timetable.ts b/packages/request-handler/src/timetable.ts index 0946ac67d..b753e9485 100644 --- a/packages/request-handler/src/timetable.ts +++ b/packages/request-handler/src/timetable.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { Station, Timetable } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const timetableSearchStationsRequestHandler = createCachedIntegrationRequestHandler< +export const timetableSearchStationsRequestHandler = createIntegrationRequestHandler< Station[], IntegrationKindByCategory<"timetable">, { query: string } @@ -16,10 +15,9 @@ export const timetableSearchStationsRequestHandler = createCachedIntegrationRequ return await integrationInstance.searchStationsAsync(input.query); }, queryKey: "timetableSearchStations", - cacheDuration: dayjs.duration(1, "day"), }); -export const timetableGetTimetableRequestHandler = createCachedIntegrationRequestHandler< +export const timetableGetTimetableRequestHandler = createIntegrationRequestHandler< Timetable, IntegrationKindByCategory<"timetable">, { stationId: string; limit: number } @@ -32,5 +30,4 @@ export const timetableGetTimetableRequestHandler = createCachedIntegrationReques }); }, queryKey: "timetableGetTimetable", - cacheDuration: dayjs.duration(1, "minute"), }); diff --git a/packages/request-handler/src/tracearr.ts b/packages/request-handler/src/tracearr.ts index 8f593ac03..ae6e98f92 100644 --- a/packages/request-handler/src/tracearr.ts +++ b/packages/request-handler/src/tracearr.ts @@ -1,11 +1,10 @@ -import dayjs from "dayjs"; import { createIntegrationAsync } from "@homarr/integrations"; import type { TracearrDashboardData } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const tracearrRequestHandler = createCachedIntegrationRequestHandler< +export const tracearrRequestHandler = createIntegrationRequestHandler< TracearrDashboardData, "tracearr", Record @@ -14,6 +13,5 @@ export const tracearrRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, - cacheDuration: dayjs.duration(5, "seconds"), queryKey: "tracearrDashboard", }); diff --git a/packages/request-handler/src/umami.ts b/packages/request-handler/src/umami.ts index e943c568a..494759c13 100644 --- a/packages/request-handler/src/umami.ts +++ b/packages/request-handler/src/umami.ts @@ -1,11 +1,10 @@ -import dayjs from "dayjs"; import { createIntegrationAsync } from "@homarr/integrations"; import type { UmamiEventSeries, UmamiMetricItem, UmamiVisitorStats } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const umamiRequestHandler = createCachedIntegrationRequestHandler< +export const umamiRequestHandler = createIntegrationRequestHandler< UmamiVisitorStats, "umami", { websiteId: string; timeFrame: string; eventName?: string } @@ -14,11 +13,10 @@ export const umamiRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getVisitorStatsAsync(input.websiteId, input.timeFrame, input.eventName); }, - cacheDuration: dayjs.duration(5, "minutes"), queryKey: "umamiVisitorStats", }); -export const umamiEventNamesRequestHandler = createCachedIntegrationRequestHandler< +export const umamiEventNamesRequestHandler = createIntegrationRequestHandler< string[], "umami", { websiteId: string } @@ -27,11 +25,10 @@ export const umamiEventNamesRequestHandler = createCachedIntegrationRequestHandl const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getEventNamesAsync(input.websiteId); }, - cacheDuration: dayjs.duration(5, "minutes"), queryKey: "umamiEventNames", }); -export const umamiTopPagesRequestHandler = createCachedIntegrationRequestHandler< +export const umamiTopPagesRequestHandler = createIntegrationRequestHandler< UmamiMetricItem[], "umami", { websiteId: string; timeFrame: string; limit: number } @@ -40,11 +37,10 @@ export const umamiTopPagesRequestHandler = createCachedIntegrationRequestHandler const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getTopPagesAsync(input.websiteId, input.timeFrame, input.limit); }, - cacheDuration: dayjs.duration(5, "minutes"), queryKey: "umamiTopPages", }); -export const umamiTopReferrersRequestHandler = createCachedIntegrationRequestHandler< +export const umamiTopReferrersRequestHandler = createIntegrationRequestHandler< UmamiMetricItem[], "umami", { websiteId: string; timeFrame: string; limit: number } @@ -53,11 +49,10 @@ export const umamiTopReferrersRequestHandler = createCachedIntegrationRequestHan const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getTopReferrersAsync(input.websiteId, input.timeFrame, input.limit); }, - cacheDuration: dayjs.duration(5, "minutes"), queryKey: "umamiTopReferrers", }); -export const umamiMultiEventRequestHandler = createCachedIntegrationRequestHandler< +export const umamiMultiEventRequestHandler = createIntegrationRequestHandler< UmamiEventSeries[], "umami", { websiteId: string; timeFrame: string; eventNames: string[] } @@ -66,11 +61,10 @@ export const umamiMultiEventRequestHandler = createCachedIntegrationRequestHandl const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getMultiEventTimeSeriesAsync(input.websiteId, input.timeFrame, input.eventNames); }, - cacheDuration: dayjs.duration(5, "minutes"), queryKey: "umamiMultiEvent", }); -export const umamiActiveVisitorsRequestHandler = createCachedIntegrationRequestHandler< +export const umamiActiveVisitorsRequestHandler = createIntegrationRequestHandler< number, "umami", { websiteId: string } @@ -79,6 +73,5 @@ export const umamiActiveVisitorsRequestHandler = createCachedIntegrationRequestH const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getActiveVisitorsAsync(input.websiteId); }, - cacheDuration: dayjs.duration(30, "seconds"), queryKey: "umamiActiveVisitors", }); diff --git a/packages/request-handler/src/update-checker.ts b/packages/request-handler/src/update-checker.ts index 8709f20c5..399a82abf 100644 --- a/packages/request-handler/src/update-checker.ts +++ b/packages/request-handler/src/update-checker.ts @@ -1,4 +1,3 @@ -import dayjs from "dayjs"; import { Octokit } from "octokit"; import { compareSemVer, isValidSemVer, parseSemVer } from "semver-parser"; @@ -6,27 +5,19 @@ 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 { createChannelWithLatestAndEvents } from "@homarr/redis"; -import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; +import { createRequestHandler } from "@homarr/request-handler/lib/request-handler"; import packageJson from "../../../package.json"; const logger = createLogger({ module: "updateCheckerRequestHandler" }); -export const updateCheckerRequestHandler = createCachedRequestHandler({ +export const updateCheckerRequestHandler = createRequestHandler({ queryKey: "homarr-update-checker", - cacheDuration: dayjs.duration(1, "day"), - fallbackToStaleOnError: true, async requestAsync(_) { return { availableUpdates: await getAvailableUpdatesAsync(packageJson.version), }; }, - createRedisChannel() { - return createChannelWithLatestAndEvents<{ - availableUpdates: Update[]; - }>("homarr:update"); - }, }); interface Update { diff --git a/packages/request-handler/src/ups.ts b/packages/request-handler/src/ups.ts index acc97eb6d..1627913bf 100644 --- a/packages/request-handler/src/ups.ts +++ b/packages/request-handler/src/ups.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; import type { UpsSummary } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const upsSummariesRequestHandler = createCachedIntegrationRequestHandler< +export const upsSummariesRequestHandler = createIntegrationRequestHandler< UpsSummary[], IntegrationKindByCategory<"ups">, Record @@ -15,6 +14,5 @@ export const upsSummariesRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getUpsSummariesAsync(); }, - cacheDuration: dayjs.duration(1, "minute"), queryKey: "upsSummaries", }); diff --git a/packages/request-handler/src/uptime-kuma.ts b/packages/request-handler/src/uptime-kuma.ts index 39149c137..328619ce3 100644 --- a/packages/request-handler/src/uptime-kuma.ts +++ b/packages/request-handler/src/uptime-kuma.ts @@ -1,11 +1,10 @@ -import dayjs from "dayjs"; import { createIntegrationAsync } from "@homarr/integrations"; import type { UptimeKumaDashboardData } from "@homarr/integrations/types"; -import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; +import { createIntegrationRequestHandler } from "./lib/integration-request-handler"; -export const uptimeKumaRequestHandler = createCachedIntegrationRequestHandler< +export const uptimeKumaRequestHandler = createIntegrationRequestHandler< UptimeKumaDashboardData, "uptimeKuma", Record @@ -14,6 +13,5 @@ export const uptimeKumaRequestHandler = createCachedIntegrationRequestHandler< const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getDashboardDataAsync(); }, - cacheDuration: dayjs.duration(1, "minute"), queryKey: "uptimeKumaDashboard", }); diff --git a/packages/request-handler/src/weather.ts b/packages/request-handler/src/weather.ts index eb4828048..0dac8432e 100644 --- a/packages/request-handler/src/weather.ts +++ b/packages/request-handler/src/weather.ts @@ -1,12 +1,11 @@ -import dayjs from "dayjs"; import { z } from "zod"; import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; -import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; +import { createWidgetRequestHandler } from "./lib/widget-request-handler"; -export const weatherRequestHandler = createCachedWidgetRequestHandler({ +export const weatherRequestHandler = createWidgetRequestHandler({ queryKey: "weatherAtLocation", widgetKind: "weather", async requestAsync(input: { latitude: number; longitude: number }) { @@ -34,7 +33,6 @@ export const weatherRequestHandler = createCachedWidgetRequestHandler({ }), } satisfies Weather; }, - cacheDuration: dayjs.duration(1, "minute"), }); const atLocationOutput = z.object({ diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index c831c6223..affa84121 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -4508,86 +4508,14 @@ "disabled": "Disabled" }, "job": { - "minecraftServerStatus": { - "label": "Minecraft server status" - }, - "timetable": { - "label": "Timetable" - }, "iconsUpdater": { "label": "Icons Updater" }, "analytics": { "label": "Analytics" }, - "smartHomeEntityState": { - "label": "Smart Home Entity State" - }, "ping": { "label": "Pings" - }, - "mediaServer": { - "label": "Media Server" - }, - "mediaOrganizer": { - "label": "Media Organizers" - }, - "downloads": { - "label": "Downloads" - }, - "mediaRequestStats": { - "label": "Media Request Stats" - }, - "mediaRequestList": { - "label": "Media Request List" - }, - "rssFeeds": { - "label": "RSS feeds" - }, - "indexerManager": { - "label": "Indexer Manager" - }, - "healthMonitoring": { - "label": "Health Monitoring" - }, - "dnsHole": { - "label": "DNS Hole Data" - }, - "updateChecker": { - "label": "Update checker" - }, - "mediaTranscoding": { - "label": "Media transcoding" - }, - "networkController": { - "label": "Network Controller" - }, - "refreshNotifications": { - "label": "Notification Updater" - }, - "dockerContainers": { - "label": "Docker containers" - }, - "firewallCpu": { - "label": "Firewall CPU" - }, - "firewallMemory": { - "label": "Firewall Memory" - }, - "firewallVersion": { - "label": "Firewall Version" - }, - "firewallInterfaces": { - "label": "Firewall Interfaces" - }, - "weather": { - "label": "Weather" - }, - "tracearr": { - "label": "Tracearr" - }, - "umamiActiveVisitors": { - "label": "Umami Active Visitors" } }, "interval": { diff --git a/packages/widgets/src/_inputs/widget-anchor-note-input.tsx b/packages/widgets/src/_inputs/widget-anchor-note-input.tsx index e045e7df0..e909477b5 100644 --- a/packages/widgets/src/_inputs/widget-anchor-note-input.tsx +++ b/packages/widgets/src/_inputs/widget-anchor-note-input.tsx @@ -30,9 +30,6 @@ export const WidgetAnchorNoteInput = ({ property, kind, options }: CommonWidgetI { enabled: Boolean(integrationId), refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, }, ); diff --git a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx index f7f026c3c..adc579ac4 100644 --- a/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx +++ b/packages/widgets/src/_inputs/widget-multiReleasesRepositories-input.tsx @@ -84,8 +84,6 @@ export const WidgetMultiReleasesRepositoriesInput = ({ }, { refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, }, ); const integrations = useMemo( @@ -609,8 +607,6 @@ const RepositoryImportModal = createModal(({ innerProps, const docker = clientApi.docker.getContainers.useQuery(undefined, { refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, enabled: innerProps.isAdmin, }); diff --git a/packages/widgets/src/anchor-note/component.tsx b/packages/widgets/src/anchor-note/component.tsx index 99585b838..90a1c998c 100644 --- a/packages/widgets/src/anchor-note/component.tsx +++ b/packages/widgets/src/anchor-note/component.tsx @@ -145,18 +145,10 @@ interface AnchorNoteWidgetContentProps { const AnchorNoteWidgetContent = ({ options, integrationId, noteId }: AnchorNoteWidgetContentProps) => { const t = useScopedI18n("widget.anchorNote"); - const { data: note, refetch } = clientApi.widget.anchorNotes.getNote.useQuery( - { - integrationId, - noteId, - }, - { - staleTime: 15 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); + const { data: note, refetch } = clientApi.widget.anchorNotes.getNote.useQuery({ + integrationId, + noteId, + }); const { mutateAsync: updateNoteAsync, isPending: isUpdating } = clientApi.widget.anchorNotes.updateNote.useMutation(); const [isEditing, setIsEditing] = useState(false); diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx index 054a2da6b..a23e09c34 100644 --- a/packages/widgets/src/app/component.tsx +++ b/packages/widgets/src/app/component.tsx @@ -23,16 +23,9 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget const t = useI18n(); const settings = useSettings(); const board = useRequiredBoard(); - const { data: app } = clientApi.app.byId.useQuery( - { - id: options.appId, - }, - { - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); + const { data: app } = clientApi.app.byId.useQuery({ + id: options.appId, + }); useRegisterSpotlightContextResults( `app-${app?.id ?? options.appId}`, app?.href diff --git a/packages/widgets/src/app/ping/ping-indicator.tsx b/packages/widgets/src/app/ping/ping-indicator.tsx index b8f88cdc8..6a460a3de 100644 --- a/packages/widgets/src/app/ping/ping-indicator.tsx +++ b/packages/widgets/src/app/ping/ping-indicator.tsx @@ -13,17 +13,7 @@ export const PingIndicator = ({ appId }: PingIndicatorProps) => { const t = useI18n(); const { data: pingResult } = clientApi.widget.app.ping.useQuery( { id: appId }, - { refetchOnMount: false, refetchOnWindowFocus: false }, - ); - - const utils = clientApi.useUtils(); - clientApi.widget.app.updatedPing.useSubscription( - { id: appId }, - { - onData(data) { - utils.widget.app.ping.setData({ id: appId }, data); - }, - }, + { refetchOnMount: false }, ); if (!pingResult) return ; diff --git a/packages/widgets/src/archive-team-warrior/component.tsx b/packages/widgets/src/archive-team-warrior/component.tsx index dacac283d..032858c2f 100644 --- a/packages/widgets/src/archive-team-warrior/component.tsx +++ b/packages/widgets/src/archive-team-warrior/component.tsx @@ -28,20 +28,7 @@ const ArchiveTeamWarriorWidgetContent = ({ integrationId: string; options: WidgetComponentProps<"archiveTeamWarrior">["options"]; }) => { - const [data] = clientApi.widget.archiveTeamWarrior.getStatus.useSuspenseQuery({ - integrationId, - }); - - const utils = clientApi.useUtils(); - - clientApi.widget.archiveTeamWarrior.subscribeStatus.useSubscription( - { integrationId }, - { - onData: (next) => { - utils.widget.archiveTeamWarrior.getStatus.setData({ integrationId }, next); - }, - }, - ); + const [data] = clientApi.widget.archiveTeamWarrior.getStatus.useSuspenseQuery({ integrationId }); const status = data.status; const projectName = status.project?.title ?? status.selectedProject ?? "No project selected"; diff --git a/packages/widgets/src/audio-stats/component.tsx b/packages/widgets/src/audio-stats/component.tsx index d9d8d26d3..9e410f527 100644 --- a/packages/widgets/src/audio-stats/component.tsx +++ b/packages/widgets/src/audio-stats/component.tsx @@ -7,10 +7,9 @@ import type { WidgetComponentProps } from "../definition"; import { AudioStatsContent } from "./audio-stats-content"; export default function AudioStatsWidget({ integrationIds, options, width }: WidgetComponentProps<"audioStats">) { - const { data: response } = clientApi.widget.audioStats.getStats.useQuery( - { integrationId: integrationIds[0] ?? "" }, - { staleTime: 5 * 60 * 1000 }, - ); + const { data: response } = clientApi.widget.audioStats.getStats.useQuery({ + integrationId: integrationIds[0] ?? "", + }); if (!response) return ; diff --git a/packages/widgets/src/beszel-alerts/component.tsx b/packages/widgets/src/beszel-alerts/component.tsx index faaa42771..8b6bb7723 100644 --- a/packages/widgets/src/beszel-alerts/component.tsx +++ b/packages/widgets/src/beszel-alerts/component.tsx @@ -45,31 +45,7 @@ export default function BeszelAlertsWidget({ data: results = [], error: alertsError, isPending, - } = clientApi.widget.beszel.getAlerts.useQuery(alertsInput, { - staleTime: 15_000, - refetchInterval: 15_000, - retry: false, - }); - const utils = clientApi.useUtils(); - - clientApi.widget.beszel.subscribeAlerts.useSubscription(alertsInput, { - enabled: !isEditMode, - onData(data) { - utils.widget.beszel.getAlerts.setData(alertsInput, (prev) => { - if (!prev) return prev; - return prev.map((r) => - r.integrationId === data.integrationId - ? { - ...r, - alerts: data.alerts.alerts, - history: data.alerts.history, - updatedAt: data.timestamp, - } - : r, - ); - }); - }, - }); + } = clientApi.widget.beszel.getAlerts.useQuery(alertsInput); const systemNameMap = useMemo(() => { const map: Record = {}; diff --git a/packages/widgets/src/beszel-system-grid/component.tsx b/packages/widgets/src/beszel-system-grid/component.tsx index 68df8f207..5d4671f5f 100644 --- a/packages/widgets/src/beszel-system-grid/component.tsx +++ b/packages/widgets/src/beszel-system-grid/component.tsx @@ -28,7 +28,7 @@ import type { WidgetComponentProps } from "../definition"; import type { BeszelSystemRow } from "../beszel/_shared/types"; import { statusColorMap, thresholdColor } from "../beszel/_shared/colors"; import { formatByteRate, formatLoadAvg, formatPercent, formatTemp, formatUptime } from "../beszel/_shared/format"; -import { useBeszelFilteredSystems, useBeszelSystemsSubscription } from "../beszel/_shared/hooks"; +import { useBeszelFilteredSystems } from "../beszel/_shared/hooks"; import { BeszelIntegrationErrorIndicator } from "../beszel/_shared/error-indicator"; import { BeszelSystemStatsModal } from "../beszel/_shared/system-stats-modal"; @@ -369,12 +369,8 @@ export default function BeszelSystemGridWidget({ data: results = [], error: systemsError, isPending, - } = clientApi.widget.beszel.getSystems.useQuery( - { integrationIds }, - { staleTime: 5_000, refetchInterval: 5_000, retry: false }, - ); + } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }); - useBeszelSystemsSubscription(integrationIds, !isEditMode); const filteredSystems = useBeszelFilteredSystems(results, options.statusFilter); if (systemsError) throw systemsError; diff --git a/packages/widgets/src/beszel-system-stats/component.tsx b/packages/widgets/src/beszel-system-stats/component.tsx index 4784877a5..c82196639 100644 --- a/packages/widgets/src/beszel-system-stats/component.tsx +++ b/packages/widgets/src/beszel-system-stats/component.tsx @@ -67,7 +67,7 @@ export default function BeszelSystemStatsWidget({ data: systemsResult = [], isPending: systemsPending, error: systemsError, - } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }, { staleTime: 30 * 1000, retry: false }); + } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }); const systems = useMemo( () => systemsResult.flatMap((r) => r.systems.map((s) => ({ value: s.id, label: s.name }))), @@ -100,10 +100,8 @@ export default function BeszelSystemStatsWidget({ includeDocker: showDocker, }, { - staleTime: 30 * 1000, refetchInterval: isLive ? false : 5_000, enabled: !isLive && systemReady && selectedSystem !== "", - retry: false, }, ); diff --git a/packages/widgets/src/beszel-system-stats/index.ts b/packages/widgets/src/beszel-system-stats/index.ts index 0b1cdf127..6345de9ea 100644 --- a/packages/widgets/src/beszel-system-stats/index.ts +++ b/packages/widgets/src/beszel-system-stats/index.ts @@ -30,7 +30,7 @@ export const { definition, componentLoader } = createWidgetDefinition("beszelSys isError, } = clientApi.widget.beszel.getSystems.useQuery( { integrationIds }, - { enabled: integrationIds.length > 0, staleTime: 30_000 }, + { enabled: integrationIds.length > 0 }, ); const selectData = data.flatMap((r) => r.systems.map((s) => ({ value: s.id, label: s.name }))); return { data: selectData, isPending, isError }; diff --git a/packages/widgets/src/beszel-system-table/component.tsx b/packages/widgets/src/beszel-system-table/component.tsx index 2604b5a0b..49b3dca74 100644 --- a/packages/widgets/src/beszel-system-table/component.tsx +++ b/packages/widgets/src/beszel-system-table/component.tsx @@ -27,7 +27,7 @@ import type { WidgetComponentProps } from "../definition"; import type { BeszelSystemRow } from "../beszel/_shared/types"; import { loadAvgColor, statusColorMap, thresholdColor } from "../beszel/_shared/colors"; import { formatByteRate, formatLoadAvg, formatPercent, formatTemp, formatUptime } from "../beszel/_shared/format"; -import { useBeszelFilteredSystems, useBeszelSystemsSubscription } from "../beszel/_shared/hooks"; +import { useBeszelFilteredSystems } from "../beszel/_shared/hooks"; import { BeszelIntegrationErrorIndicator } from "../beszel/_shared/error-indicator"; import { BeszelSystemStatsModal } from "../beszel/_shared/system-stats-modal"; @@ -74,13 +74,9 @@ export default function BeszelSystemTableWidget({ data: results = [], error: systemsError, isPending, - } = clientApi.widget.beszel.getSystems.useQuery( - { integrationIds }, - { staleTime: 5_000, refetchInterval: 5_000, retry: false }, - ); + } = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }); const size = getSizeConfig(width); - useBeszelSystemsSubscription(integrationIds, !isEditMode); const filteredSystems = useBeszelFilteredSystems(results, options.statusFilter); const [sortStatus, setSortStatus] = useState>({ diff --git a/packages/widgets/src/beszel/_shared/hooks.ts b/packages/widgets/src/beszel/_shared/hooks.ts index ebb84b4eb..3591defcb 100644 --- a/packages/widgets/src/beszel/_shared/hooks.ts +++ b/packages/widgets/src/beszel/_shared/hooks.ts @@ -8,24 +8,6 @@ import type { BeszelSystemRow } from "./types"; type SystemRowWithKey = BeszelSystemRow & { _key: string }; -export const useBeszelSystemsSubscription = (integrationIds: string[], enabled = true) => { - const utils = clientApi.useUtils(); - clientApi.widget.beszel.subscribeSystems.useSubscription( - { integrationIds }, - { - enabled, - onData(data) { - utils.widget.beszel.getSystems.setData({ integrationIds }, (prev) => { - if (!prev) return prev; - return prev.map((r) => - r.integrationId === data.integrationId ? { ...r, systems: data.systems, updatedAt: data.timestamp } : r, - ); - }); - }, - }, - ); -}; - export const useBeszelFilteredSystems = ( results: { integrationId: string; systems: BeszelSystemRow[] }[], statusFilter: string, diff --git a/packages/widgets/src/beszel/_shared/system-stats-modal.tsx b/packages/widgets/src/beszel/_shared/system-stats-modal.tsx index 36d26ad1a..ab8419206 100644 --- a/packages/widgets/src/beszel/_shared/system-stats-modal.tsx +++ b/packages/widgets/src/beszel/_shared/system-stats-modal.tsx @@ -43,7 +43,6 @@ export const BeszelSystemStatsModal = createModal(( timePeriod, includeDocker: false, }, - { staleTime: 30_000 }, ); const mappers = useMemo( diff --git a/packages/widgets/src/beszel/_shared/use-live-stats.ts b/packages/widgets/src/beszel/_shared/use-live-stats.ts index 440800d0d..4a06d11da 100644 --- a/packages/widgets/src/beszel/_shared/use-live-stats.ts +++ b/packages/widgets/src/beszel/_shared/use-live-stats.ts @@ -1,92 +1,26 @@ "use client"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useMemo } from "react"; import { clientApi } from "@homarr/api/client"; -import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types"; - -const MAX_BUFFER = 30; -const FLUSH_INTERVAL_MS = 3000; - -interface LiveStatsData { - systemStats: BeszelSystemStatsRecord[]; - containerStats: BeszelContainerStatsRecord[]; -} - -function appendToBuffer( - prev: LiveStatsData | null, - incoming: { stats: BeszelSystemStatsRecord; containerStats: BeszelContainerStatsRecord | null }, -): LiveStatsData { - const systemStats = [...(prev?.systemStats ?? []), incoming.stats].slice(-MAX_BUFFER); - const prevContainer = prev?.containerStats ?? []; - if (!incoming.containerStats) { - return { systemStats, containerStats: prevContainer }; - } - return { systemStats, containerStats: [...prevContainer, incoming.containerStats].slice(-MAX_BUFFER) }; -} export const useLiveStats = (integrationIds: string[], systemId: string, enabled: boolean) => { - const [data, setData] = useState(null); - const [error, setError] = useState(null); - const bufferRef = useRef(null); - const dirtyRef = useRef(false); - const generationRef = useRef(0); - - const integrationKey = [...integrationIds].sort().join(","); - - useEffect(() => { - generationRef.current += 1; - setData(null); - setError(null); - bufferRef.current = null; - dirtyRef.current = false; - }, [systemId, enabled, integrationKey]); - - const onData = useCallback( - (incoming: { stats: BeszelSystemStatsRecord; containerStats: BeszelContainerStatsRecord | null }) => { - const gen = generationRef.current; - queueMicrotask(() => { - if (generationRef.current !== gen) return; - setError(null); - const isFirst = !bufferRef.current; - bufferRef.current = appendToBuffer(bufferRef.current, incoming); - dirtyRef.current = true; - if (isFirst) { - setData(bufferRef.current); - } - }); - }, - [], + const queryInput = useMemo( + () => ({ + integrationIds, + systemId, + timePeriod: "1m" as const, + includeDocker: true, + }), + [integrationIds, systemId], ); - const onError = useCallback((err: unknown) => { - if (err instanceof Error) { - setError(err); - return; - } - setError(new Error(String(err))); - }, []); - - const subscriptionInput = useMemo(() => ({ integrationIds, systemId }), [integrationKey, systemId]); - const subscriptionEnabled = enabled && systemId !== ""; - - useEffect(() => { - if (!subscriptionEnabled) return; - const gen = generationRef.current; - const id = setInterval(() => { - if (generationRef.current !== gen) return; - if (!dirtyRef.current || !bufferRef.current) return; - dirtyRef.current = false; - setData(bufferRef.current); - }, FLUSH_INTERVAL_MS); - return () => clearInterval(id); - }, [subscriptionEnabled, integrationKey, systemId]); - - clientApi.widget.beszel.subscribeSystemStats.useSubscription(subscriptionInput, { - enabled: subscriptionEnabled, - onData, - onError, + const { data, error } = clientApi.widget.beszel.getSystemStats.useQuery(queryInput, { + enabled: enabled && systemId !== "", }); - return { data, error }; + return { + data: data ? { systemStats: data.systemStats, containerStats: data.containerStats } : null, + error: error ?? (data?.error ? new Error(data.error) : null), + }; }; diff --git a/packages/widgets/src/calendar/component.tsx b/packages/widgets/src/calendar/component.tsx index ac3d8b37b..6d309ce42 100644 --- a/packages/widgets/src/calendar/component.tsx +++ b/packages/widgets/src/calendar/component.tsx @@ -39,27 +39,7 @@ const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }: releaseType: options.releaseType, showUnmonitored: options.showUnmonitored, }; - const { data } = clientApi.widget.calendar.findAllEvents.useQuery(input, { - staleTime: 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }); - - const utils = clientApi.useUtils(); - clientApi.widget.calendar.subscribeToEvents.useSubscription(input, { - onData(data) { - utils.widget.calendar.findAllEvents.setData(input, (old) => { - return old?.map((item) => { - if (item.integration.id !== data.integration.id) return item; - return { - ...item, - events: data.events, - }; - }); - }); - }, - }); + const { data } = clientApi.widget.calendar.findAllEvents.useQuery(input); const events = useMemo(() => data?.flatMap((item) => item.events) ?? [], [data]); diff --git a/packages/widgets/src/coolify/component.tsx b/packages/widgets/src/coolify/component.tsx index 91b37ecd4..46940f56e 100644 --- a/packages/widgets/src/coolify/component.tsx +++ b/packages/widgets/src/coolify/component.tsx @@ -31,27 +31,7 @@ interface CoolifyContentProps { } function CoolifyContent({ integrationIds, options, width }: CoolifyContentProps) { - const { data: instancesData = [] } = clientApi.widget.coolify.getInstancesInfo.useQuery( - { integrationIds }, - { staleTime: 30 * 1000 }, - ); - - const utils = clientApi.useUtils(); - clientApi.widget.coolify.subscribeInstancesInfo.useSubscription( - { integrationIds }, - { - onData(newData) { - utils.widget.coolify.getInstancesInfo.setData({ integrationIds }, (prevData) => { - if (!prevData) return prevData; - return prevData.map((instance) => - instance.integrationId === newData.integrationId - ? { ...instance, instanceInfo: newData.instanceInfo, updatedAt: newData.timestamp } - : instance, - ); - }); - }, - }, - ); + const { data: instancesData = [] } = clientApi.widget.coolify.getInstancesInfo.useQuery({ integrationIds }); const isTiny = width < 256; const [firstInstance] = instancesData; diff --git a/packages/widgets/src/dns-hole/controls/component.tsx b/packages/widgets/src/dns-hole/controls/component.tsx index afe397cfd..97e3ac7ad 100644 --- a/packages/widgets/src/dns-hole/controls/component.tsx +++ b/packages/widgets/src/dns-hole/controls/component.tsx @@ -51,50 +51,10 @@ export default function DnsHoleControlsWidget({ .map(({ id }) => id) .filter((id) => integrationIds.includes(id)); - const { data: summaries = [] } = clientApi.widget.dnsHole.summary.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); + const { data: summaries = [] } = clientApi.widget.dnsHole.summary.useQuery({ + integrationIds, + }); const utils = clientApi.useUtils(); - // Subscribe to summary updates - clientApi.widget.dnsHole.subscribeToSummary.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.dnsHole.summary.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) return undefined; - - const newData = prevData.map((summary) => - summary.integration.id === data.integration.id - ? { - integration: { - ...summary.integration, - updatedAt: new Date(), - }, - summary: data.summary, - } - : summary, - ); - - return newData; - }, - ); - }, - }, - ); // Mutations for dnsHole state, set to undefined on click, and change again on settle const { mutate: enableDns } = clientApi.widget.dnsHole.enable.useMutation({ diff --git a/packages/widgets/src/dns-hole/summary/component.tsx b/packages/widgets/src/dns-hole/summary/component.tsx index 4a4c2879c..4bcc82568 100644 --- a/packages/widgets/src/dns-hole/summary/component.tsx +++ b/packages/widgets/src/dns-hole/summary/component.tsx @@ -20,46 +20,12 @@ import type { widgetKind } from "."; import type { WidgetComponentProps, WidgetProps } from "../../definition"; export default function DnsHoleSummaryWidget({ options, integrationIds }: WidgetComponentProps) { - const { data: summaries = [] } = clientApi.widget.dnsHole.summary.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - const utils = clientApi.useUtils(); + const { data: summaries = [] } = clientApi.widget.dnsHole.summary.useQuery({ + integrationIds, + }); const t = useI18n(); - clientApi.widget.dnsHole.subscribeToSummary.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.dnsHole.summary.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - - const newData = prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); - return newData; - }, - ); - }, - }, - ); - const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]); return ( diff --git a/packages/widgets/src/docker/component.tsx b/packages/widgets/src/docker/component.tsx index b50139361..176168509 100644 --- a/packages/widgets/src/docker/component.tsx +++ b/packages/widgets/src/docker/component.tsx @@ -184,20 +184,11 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo const t = useScopedI18n("docker"); const isTiny = width <= 256; - const utils = clientApi.useUtils(); - const { data } = clientApi.docker.getContainers.useQuery(undefined, { - staleTime: 20 * 1000, - }); + const { data } = clientApi.docker.getContainers.useQuery(undefined); const containers = data?.containers ?? []; const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]); const relativeTime = useTimeAgo(timestamp); - clientApi.docker.subscribeContainers.useSubscription(undefined, { - onData(data) { - utils.docker.getContainers.setData(undefined, { containers: data, timestamp: new Date() }); - }, - }); - const totalContainers = containers.length; const totals = useMemo(() => { diff --git a/packages/widgets/src/downloads/component.tsx b/packages/widgets/src/downloads/component.tsx index 60e970c22..2dc01040d 100644 --- a/packages/widgets/src/downloads/component.tsx +++ b/packages/widgets/src/downloads/component.tsx @@ -87,19 +87,10 @@ export default function DownloadClientsWidget({ integrationIds.includes(id) ? [id] : [], ); - const { data: currentItems = [] } = clientApi.widget.downloads.getJobsAndStatuses.useQuery( - { - integrationIds, - limitPerIntegration: options.limitPerIntegration, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - const utils = clientApi.useUtils(); + const { data: currentItems = [] } = clientApi.widget.downloads.getJobsAndStatuses.useQuery({ + integrationIds, + limitPerIntegration: options.limitPerIntegration, + }); const t = useScopedI18n("widget.downloads"); const tCommon = useScopedI18n("common"); @@ -125,34 +116,6 @@ export default function DownloadClientsWidget({ const { mutate: mutatePauseItem } = clientApi.widget.downloads.pauseItem.useMutation(); const { mutate: mutateDeleteItem } = clientApi.widget.downloads.deleteItem.useMutation(); - //Subscribe to dynamic data changes - clientApi.widget.downloads.subscribeToJobsAndStatuses.useSubscription( - { - integrationIds, - limitPerIntegration: options.limitPerIntegration, - }, - { - onData: (data) => { - utils.widget.downloads.getJobsAndStatuses.setData( - { integrationIds, limitPerIntegration: options.limitPerIntegration }, - (prevData) => { - return prevData?.map((item) => { - if (item.integration.id !== data.integration.id) return item; - - return { - data: data.data, - integration: { - ...data.integration, - updatedAt: new Date(), - }, - }; - }); - }, - ); - }, - }, - ); - //Flatten Data array for which each element has it's integration, data (base + calculated) and actions. Memoized on data subscription const data = useMemo( () => diff --git a/packages/widgets/src/firewall/component.tsx b/packages/widgets/src/firewall/component.tsx index 7318cd3d3..4b5211170 100644 --- a/packages/widgets/src/firewall/component.tsx +++ b/packages/widgets/src/firewall/component.tsx @@ -183,163 +183,32 @@ export default function FirewallWidget({ integrationIds, width, itemId }: Widget } export const useUpdatingCpuStatus = (integrationIds: string[]) => { - const utils = clientApi.useUtils(); - const { data: firewallsCpuData = [] } = clientApi.widget.firewall.getFirewallCpuStatus.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - - clientApi.widget.firewall.subscribeFirewallCpuStatus.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.firewall.getFirewallCpuStatus.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - - return prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); - }, - ); - }, - }, - ); + const { data: firewallsCpuData = [] } = clientApi.widget.firewall.getFirewallCpuStatus.useQuery({ + integrationIds, + }); return firewallsCpuData; }; export const useUpdatingMemoryStatus = (integrationIds: string[]) => { - const utils = clientApi.useUtils(); - const { data: firewallsMemoryData = [] } = clientApi.widget.firewall.getFirewallMemoryStatus.useQuery( - { - integrationIds, - }, - { - staleTime: 15 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - - clientApi.widget.firewall.subscribeFirewallMemoryStatus.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.firewall.getFirewallMemoryStatus.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - - return prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); - }, - ); - }, - }, - ); + const { data: firewallsMemoryData = [] } = clientApi.widget.firewall.getFirewallMemoryStatus.useQuery({ + integrationIds, + }); return firewallsMemoryData; }; export const useUpdatingVersionStatus = (integrationIds: string[]) => { - const utils = clientApi.useUtils(); - const { data: firewallsVersionData = [] } = clientApi.widget.firewall.getFirewallVersionStatus.useQuery( - { - integrationIds, - }, - { - staleTime: 60 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - - clientApi.widget.firewall.subscribeFirewallVersionStatus.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.firewall.getFirewallVersionStatus.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - - return prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); - }, - ); - }, - }, - ); + const { data: firewallsVersionData = [] } = clientApi.widget.firewall.getFirewallVersionStatus.useQuery({ + integrationIds, + }); return firewallsVersionData; }; export const useUpdatingInterfacesStatus = (integrationIds: string[]) => { - const utils = clientApi.useUtils(); - const { data: firewallsInterfacesData = [] } = clientApi.widget.firewall.getFirewallInterfacesStatus.useQuery( - { - integrationIds, - }, - { - staleTime: 30 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - - clientApi.widget.firewall.subscribeFirewallInterfacesStatus.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.firewall.getFirewallInterfacesStatus.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - return prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); - }, - ); - }, - }, - ); + const { data: firewallsInterfacesData = [] } = clientApi.widget.firewall.getFirewallInterfacesStatus.useQuery({ + integrationIds, + }); return firewallsInterfacesData; }; diff --git a/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx b/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx index daa0b40ec..1ac7922d5 100644 --- a/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx +++ b/packages/widgets/src/health-monitoring/cluster/cluster-health.tsx @@ -35,27 +35,9 @@ export const ClusterHealthMonitoring = ({ width, }: WidgetComponentProps<"healthMonitoring"> & { integrationId: string }) => { const t = useI18n(); - const { data: healthData } = clientApi.widget.healthMonitoring.getClusterHealthStatus.useQuery( - { - integrationId, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - - const utils = clientApi.useUtils(); - clientApi.widget.healthMonitoring.subscribeClusterHealthStatus.useSubscription( - { integrationId }, - { - onData(data) { - utils.widget.healthMonitoring.getClusterHealthStatus.setData({ integrationId }, data); - }, - }, - ); + const { data: healthData } = clientApi.widget.healthMonitoring.getClusterHealthStatus.useQuery({ + integrationId, + }); if (!healthData) return ; diff --git a/packages/widgets/src/health-monitoring/system-health.tsx b/packages/widgets/src/health-monitoring/system-health.tsx index 405838cac..8b77b1d2f 100644 --- a/packages/widgets/src/health-monitoring/system-health.tsx +++ b/packages/widgets/src/health-monitoring/system-health.tsx @@ -52,39 +52,12 @@ export const SystemHealthMonitoring = ({ width, }: WidgetComponentProps<"healthMonitoring">) => { const t = useI18n(); - const { data: healthData = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); + const { data: healthData = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery({ + integrationIds, + }); const [opened, { open, close }] = useDisclosure(false); - const utils = clientApi.useUtils(); const board = useRequiredBoard(); - clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription( - { integrationIds }, - { - onData(data) { - utils.widget.healthMonitoring.getSystemHealthStatus.setData({ integrationIds }, (prevData) => { - if (!prevData) { - return undefined; - } - return prevData.map((item) => - item.integrationId === data.integrationId - ? { ...item, healthInfo: data.healthInfo, updatedAt: data.timestamp } - : item, - ); - }); - }, - }, - ); - const isTiny = width < 256; if (healthData.length === 0) return ; diff --git a/packages/widgets/src/immich/album-carousel/component.tsx b/packages/widgets/src/immich/album-carousel/component.tsx index 94795991c..9c157bfaa 100644 --- a/packages/widgets/src/immich/album-carousel/component.tsx +++ b/packages/widgets/src/immich/album-carousel/component.tsx @@ -23,7 +23,7 @@ export default function ImmichAlbumCarouselWidget({ integrationId: integrationIds[0] ?? "", albumId: options.albumId ?? "", }, - { enabled: Boolean(options.albumId), staleTime: 15 * 60 * 1000 }, + { enabled: Boolean(options.albumId) }, ); if (!options.albumId) { diff --git a/packages/widgets/src/immich/server-stats/component.tsx b/packages/widgets/src/immich/server-stats/component.tsx index e3e1596ea..919b0caf0 100644 --- a/packages/widgets/src/immich/server-stats/component.tsx +++ b/packages/widgets/src/immich/server-stats/component.tsx @@ -17,12 +17,9 @@ export default function ImmichServerStatsWidget({ options, }: WidgetComponentProps<"immich-serverStats">) { const t = useI18n(); - const { data: stats } = clientApi.widget.immich.getServerStats.useQuery( - { - integrationId: integrationIds[0] ?? "", - }, - { staleTime: 15 * 60 * 1000 }, - ); + const { data: stats } = clientApi.widget.immich.getServerStats.useQuery({ + integrationId: integrationIds[0] ?? "", + }); if (!stats) return ; diff --git a/packages/widgets/src/indexer-manager/component.tsx b/packages/widgets/src/indexer-manager/component.tsx index 51ddb1805..e579b7319 100644 --- a/packages/widgets/src/indexer-manager/component.tsx +++ b/packages/widgets/src/indexer-manager/component.tsx @@ -18,33 +18,10 @@ export default function IndexerManagerWidget({ height, }: WidgetComponentProps<"indexerManager">) { const t = useI18n(); - const { data: indexersData = [] } = clientApi.widget.indexerManager.getIndexersStatus.useQuery( - { integrationIds }, - { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - const utils = clientApi.useUtils(); + const { data: indexersData = [] } = clientApi.widget.indexerManager.getIndexersStatus.useQuery({ integrationIds }); const { mutate: testAll, isPending } = clientApi.widget.indexerManager.testAllIndexers.useMutation(); const board = useRequiredBoard(); - - clientApi.widget.indexerManager.subscribeIndexersStatus.useSubscription( - { integrationIds }, - { - onData(newData) { - utils.widget.indexerManager.getIndexersStatus.setData({ integrationIds }, (previousData) => - previousData?.map((item) => - item.integrationId === newData.integrationId ? { ...item, indexers: newData.indexers } : item, - ), - ); - }, - }, - ); - const hasSmallWidth = width < 256; const hasSmallHeight = height < 256; diff --git a/packages/widgets/src/media-releases/component.tsx b/packages/widgets/src/media-releases/component.tsx index 61fc9216d..26fe367a9 100644 --- a/packages/widgets/src/media-releases/component.tsx +++ b/packages/widgets/src/media-releases/component.tsx @@ -18,12 +18,9 @@ import { OverflowBadge } from "@homarr/ui"; import type { WidgetComponentProps } from "../definition"; export default function MediaReleasesWidget({ options, integrationIds }: WidgetComponentProps<"mediaReleases">) { - const { data: releases = [] } = clientApi.widget.mediaRelease.getMediaReleases.useQuery( - { - integrationIds, - }, - { staleTime: 5 * 60 * 1000 }, - ); + const { data: releases = [] } = clientApi.widget.mediaRelease.getMediaReleases.useQuery({ + integrationIds, + }); return ( diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx index 3bc24cf70..efa32f911 100644 --- a/packages/widgets/src/media-requests/list/component.tsx +++ b/packages/widgets/src/media-requests/list/component.tsx @@ -35,44 +35,9 @@ export default function MediaServerWidget({ options, width, }: WidgetComponentProps<"mediaRequests-requestList">) { - const { data: mediaRequests } = clientApi.widget.mediaRequests.getLatestRequests.useQuery( - { - integrationIds, - }, - { - staleTime: 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }, - ); - const utils = clientApi.useUtils(); - clientApi.widget.mediaRequests.subscribeToLatestRequests.useSubscription( - { - integrationIds, - }, - { - onData(data) { - utils.widget.mediaRequests.getLatestRequests.setData({ integrationIds }, (prevData) => { - if (!prevData) return []; - - const filteredData = prevData.filter(({ integrationId }) => integrationId !== data.integrationId); - const newData = filteredData.concat( - data.requests.map((request) => ({ ...request, integrationId: data.integrationId })), - ); - return newData.toSorted((dataA, dataB) => { - if (dataA.status === dataB.status) { - return dataB.createdAt.getTime() - dataA.createdAt.getTime(); - } - - return ( - mediaRequestStatusConfiguration[dataA.status].position - - mediaRequestStatusConfiguration[dataB.status].position - ); - }); - }); - }, - }, - ); + const { data: mediaRequests } = clientApi.widget.mediaRequests.getLatestRequests.useQuery({ + integrationIds, + }); if (!mediaRequests) return ; if (mediaRequests.length === 0) throw new NoIntegrationDataError(); diff --git a/packages/widgets/src/media-requests/stats/component.tsx b/packages/widgets/src/media-requests/stats/component.tsx index 51a3e3d5e..5348b845a 100644 --- a/packages/widgets/src/media-requests/stats/component.tsx +++ b/packages/widgets/src/media-requests/stats/component.tsx @@ -36,16 +36,9 @@ export default function MediaServerWidget({ width, }: WidgetComponentProps<"mediaRequests-requestStats">) { const t = useScopedI18n("widget.mediaRequests-requestStats"); - const { data: requestStats } = clientApi.widget.mediaRequests.getStats.useQuery( - { - integrationIds, - }, - { - staleTime: 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }, - ); + const { data: requestStats } = clientApi.widget.mediaRequests.getStats.useQuery({ + integrationIds, + }); const board = useRequiredBoard(); diff --git a/packages/widgets/src/media-server/component.tsx b/packages/widgets/src/media-server/component.tsx index efa2c2415..ecbb9f805 100644 --- a/packages/widgets/src/media-server/component.tsx +++ b/packages/widgets/src/media-server/component.tsx @@ -23,18 +23,10 @@ export default function MediaServerWidget({ integrationIds, isEditMode, }: WidgetComponentProps<"mediaServer">) { - const { data: currentStreams = [] } = clientApi.widget.mediaServer.getCurrentStreams.useQuery( - { - integrationIds, - showOnlyPlaying: options.showOnlyPlaying, - }, - { - staleTime: 5 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }, - ); - const utils = clientApi.useUtils(); + const { data: currentStreams = [] } = clientApi.widget.mediaServer.getCurrentStreams.useQuery({ + integrationIds, + showOnlyPlaying: options.showOnlyPlaying, + }); const t = useScopedI18n("widget.mediaServer"); const columns = useMemo[]>( @@ -83,32 +75,6 @@ export default function MediaServerWidget({ [t], ); - clientApi.widget.mediaServer.subscribeToCurrentStreams.useSubscription( - { - integrationIds, - showOnlyPlaying: options.showOnlyPlaying, - }, - { - enabled: !isEditMode, - onData(data) { - utils.widget.mediaServer.getCurrentStreams.setData( - { integrationIds, showOnlyPlaying: options.showOnlyPlaying }, - (previousData) => { - return previousData?.map((pair) => { - if (pair.integrationId === data.integrationId) { - return { - ...pair, - sessions: data.data, - }; - } - return pair; - }); - }, - ); - }, - }, - ); - // Only render the flat list of sessions when the currentStreams change // Otherwise it will always create a new array reference and cause the table to re-render const flatSessions = useMemo( diff --git a/packages/widgets/src/media-transcoding/component.tsx b/packages/widgets/src/media-transcoding/component.tsx index 8b0ba6591..fe4873e45 100644 --- a/packages/widgets/src/media-transcoding/component.tsx +++ b/packages/widgets/src/media-transcoding/component.tsx @@ -38,18 +38,7 @@ export default function MediaTranscodingWidget({ pageSize: queuePageSize, page: queuePage, }; - const { data: transcodingData } = clientApi.widget.mediaTranscoding.getDataAsync.useQuery(input, { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }); - - const utils = clientApi.useUtils(); - clientApi.widget.mediaTranscoding.subscribeData.useSubscription(input, { - onData(data) { - utils.widget.mediaTranscoding.getDataAsync.setData(input, data); - }, - }); + const { data: transcodingData } = clientApi.widget.mediaTranscoding.getDataAsync.useQuery(input); const [view, setView] = useState(options.defaultView); const t = useI18n("widget.mediaTranscoding"); diff --git a/packages/widgets/src/minecraft/server-status/component.tsx b/packages/widgets/src/minecraft/server-status/component.tsx index ddf4d6e4b..20214e3ac 100644 --- a/packages/widgets/src/minecraft/server-status/component.tsx +++ b/packages/widgets/src/minecraft/server-status/component.tsx @@ -11,18 +11,7 @@ import { WidgetEmptyState } from "../../common/empty-state"; import type { WidgetComponentProps } from "../../definition"; export default function MinecraftServerStatusWidget({ options }: WidgetComponentProps<"minecraftServerStatus">) { - const { data: result } = clientApi.widget.minecraft.getServerStatus.useQuery(options, { - staleTime: 5 * 60 * 1000, - }); - const utils = clientApi.useUtils(); - clientApi.widget.minecraft.subscribeServerStatus.useSubscription(options, { - onData(data) { - utils.widget.minecraft.getServerStatus.setData(options, { - data, - timestamp: new Date(), - }); - }, - }); + const { data: result } = clientApi.widget.minecraft.getServerStatus.useQuery(options); const tStatus = useScopedI18n("widget.minecraftServerStatus.status"); if (!result) return ; diff --git a/packages/widgets/src/network-controller/network-status/component.tsx b/packages/widgets/src/network-controller/network-status/component.tsx index 846d1128f..c4e892dba 100644 --- a/packages/widgets/src/network-controller/network-status/component.tsx +++ b/packages/widgets/src/network-controller/network-status/component.tsx @@ -21,48 +21,9 @@ export default function NetworkControllerNetworkStatusWidget({ options, integrationIds, }: WidgetComponentProps<"networkControllerStatus">) { - const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - const utils = clientApi.useUtils(); - - clientApi.widget.networkController.subscribeToSummary.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.networkController.summary.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - - return prevData.map((item) => - item.integration.id === data.integration.id - ? { - ...item, - summary: data.summary, - updatedAt: new Date(), - } - : item, - ); - }, - ); - }, - }, - ); + const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery({ + integrationIds, + }); const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]); diff --git a/packages/widgets/src/network-controller/summary/component.tsx b/packages/widgets/src/network-controller/summary/component.tsx index 21a4b6a93..cb5dc7545 100644 --- a/packages/widgets/src/network-controller/summary/component.tsx +++ b/packages/widgets/src/network-controller/summary/component.tsx @@ -20,45 +20,12 @@ dayjs.extend(duration); export default function NetworkControllerSummaryWidget({ integrationIds, }: WidgetComponentProps<"networkControllerSummary">) { - const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - const utils = clientApi.useUtils(); + const { data: summaries = [] } = clientApi.widget.networkController.summary.useQuery({ + integrationIds, + }); const t = useI18n(); - clientApi.widget.networkController.subscribeToSummary.useSubscription( - { - integrationIds, - }, - { - onData: (data) => { - utils.widget.networkController.summary.setData( - { - integrationIds, - }, - (prevData) => { - if (!prevData) { - return undefined; - } - - return prevData.map((item) => - item.integration.id === data.integration.id ? { ...item, summary: data.summary } : item, - ); - }, - ); - }, - }, - ); - const data = useMemo(() => summaries.flatMap(({ summary }) => summary), [summaries]); return ( diff --git a/packages/widgets/src/notifications/component.tsx b/packages/widgets/src/notifications/component.tsx index 47aa829be..3b7cc5ba7 100644 --- a/packages/widgets/src/notifications/component.tsx +++ b/packages/widgets/src/notifications/component.tsx @@ -12,43 +12,10 @@ import { useScopedI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; export default function NotificationsWidget({ options, integrationIds }: WidgetComponentProps<"notifications">) { - const { data: notificationIntegrations = [] } = clientApi.widget.notifications.getNotifications.useQuery( - { - ...options, - integrationIds, - }, - { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }, - ); - const utils = clientApi.useUtils(); - - clientApi.widget.notifications.subscribeNotifications.useSubscription( - { - ...options, - integrationIds, - }, - { - onData: (data) => { - utils.widget.notifications.getNotifications.setData({ ...options, integrationIds }, (prevData) => { - return prevData?.map((item) => { - if (item.integration.id !== data.integration.id) return item; - - return { - data: data.data, - integration: { - ...data.integration, - updatedAt: new Date(), - }, - }; - }); - }); - }, - }, - ); + const { data: notificationIntegrations = [] } = clientApi.widget.notifications.getNotifications.useQuery({ + ...options, + integrationIds, + }); const t = useScopedI18n("widget.notifications"); diff --git a/packages/widgets/src/paperless-ngx/component.tsx b/packages/widgets/src/paperless-ngx/component.tsx index 3b476aa0d..08bcee3fb 100644 --- a/packages/widgets/src/paperless-ngx/component.tsx +++ b/packages/widgets/src/paperless-ngx/component.tsx @@ -72,10 +72,9 @@ const heroPartVisibility = { export default function PaperlessNgxWidget({ integrationIds, options, width }: WidgetComponentProps<"paperlessNgx">) { const t = useScopedI18n("widget.paperlessNgx"); - const { data: stats } = clientApi.widget.paperlessNgx.getStats.useQuery( - { integrationId: integrationIds[0] ?? "" }, - { staleTime: 5 * 60 * 1000 }, - ); + const { data: stats } = clientApi.widget.paperlessNgx.getStats.useQuery({ + integrationId: integrationIds[0] ?? "", + }); if (!stats) return ; diff --git a/packages/widgets/src/rssFeed/component.tsx b/packages/widgets/src/rssFeed/component.tsx index a2cf07ebb..626f87328 100644 --- a/packages/widgets/src/rssFeed/component.tsx +++ b/packages/widgets/src/rssFeed/component.tsx @@ -12,29 +12,7 @@ import type { WidgetComponentProps } from "../definition"; import classes from "./component.module.scss"; const useLiveFeedEntries = (input: RouterInputs["widget"]["rssFeed"]["getFeeds"]) => { - const { data: feedEntries = [] } = clientApi.widget.rssFeed.getFeeds.useQuery(input, { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - }); - const utils = clientApi.useUtils(); - - clientApi.widget.rssFeed.subscribeFeeds.useSubscription(input, { - onData(updatedData) { - utils.widget.rssFeed.getFeeds.setData(input, (oldData) => { - return oldData - ?.filter((entry) => entry.feedUrl !== updatedData.url) - .concat(updatedData.entries) - .toSorted((entryA, entryB) => { - return entryA.published && entryB.published - ? new Date(entryB.published).getTime() - new Date(entryA.published).getTime() - : 0; - }) - .slice(0, input.maximumAmountPosts); - }); - }, - }); + const { data: feedEntries = [] } = clientApi.widget.rssFeed.getFeeds.useQuery(input); return feedEntries; }; diff --git a/packages/widgets/src/smart-home/entity-state/component.tsx b/packages/widgets/src/smart-home/entity-state/component.tsx index 2ad45b7a3..e729b8986 100644 --- a/packages/widgets/src/smart-home/entity-state/component.tsx +++ b/packages/widgets/src/smart-home/entity-state/component.tsx @@ -23,17 +23,7 @@ export default function SmartHomeEntityStateWidget({ entityId: options.entityId, integrationId, }; - const { data: entityState } = clientApi.widget.smartHome.entityState.useQuery(input, { - staleTime: 60 * 1000, - }); - - const utils = clientApi.useUtils(); - - clientApi.widget.smartHome.subscribeEntityState.useSubscription(input, { - onData(data) { - utils.widget.smartHome.entityState.setData(input, data.state); - }, - }); + const { data: entityState } = clientApi.widget.smartHome.entityState.useQuery(input); const { mutate } = clientApi.widget.smartHome.switchEntity.useMutation(); diff --git a/packages/widgets/src/speedtest-tracker/component.tsx b/packages/widgets/src/speedtest-tracker/component.tsx index dcde145a4..2be426fb1 100644 --- a/packages/widgets/src/speedtest-tracker/component.tsx +++ b/packages/widgets/src/speedtest-tracker/component.tsx @@ -20,28 +20,7 @@ export default function SpeedtestTrackerWidget({ isEditMode, }: WidgetComponentProps<"speedtestTracker">) { const t = useScopedI18n("widget.speedtestTracker"); - const { data: dashboardData = [] } = clientApi.widget.speedtestTracker.getDashboard.useQuery( - { integrationIds }, - { staleTime: 10 * 60 * 1000 }, - ); - - const utils = clientApi.useUtils(); - clientApi.widget.speedtestTracker.subscribeToDashboard.useSubscription( - { integrationIds }, - { - enabled: !isEditMode, - onData(newData) { - utils.widget.speedtestTracker.getDashboard.setData({ integrationIds }, (prevData) => { - if (!prevData) return prevData; - return prevData.map((instance) => - instance.integrationId === newData.integrationId - ? { ...instance, dashboard: newData.dashboard, updatedAt: newData.timestamp } - : instance, - ); - }); - }, - }, - ); + const { data: dashboardData = [] } = clientApi.widget.speedtestTracker.getDashboard.useQuery({ integrationIds }); const combined = useMemo( () => diff --git a/packages/widgets/src/stocks/component.tsx b/packages/widgets/src/stocks/component.tsx index 99078b4d2..918851ce8 100644 --- a/packages/widgets/src/stocks/component.tsx +++ b/packages/widgets/src/stocks/component.tsx @@ -25,9 +25,7 @@ function calculateChangePercentage(currentPrice: number, previousClose: number) export default function StockPriceWidget({ options, width, height }: WidgetComponentProps<"stockPrice">) { const t = useScopedI18n("widget.stockPrice"); const theme = useMantineTheme(); - const { data: result } = clientApi.widget.stockPrice.getPriceHistory.useQuery(options, { - staleTime: 5 * 60 * 1000, - }); + const { data: result } = clientApi.widget.stockPrice.getPriceHistory.useQuery(options); if (!result) return ; const { data } = result; diff --git a/packages/widgets/src/system-disks/component.tsx b/packages/widgets/src/system-disks/component.tsx index efe2fe73d..d24a68c37 100644 --- a/packages/widgets/src/system-disks/component.tsx +++ b/packages/widgets/src/system-disks/component.tsx @@ -118,18 +118,7 @@ const SystemDiskCard = ({ export default function SystemResources({ integrationIds, options }: WidgetComponentProps<"systemDisks">) { const queryInput = { integrationIds }; - const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery(queryInput, { - staleTime: 5 * 1000, - }); - const utils = clientApi.useUtils(); - - clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription(queryInput, { - onData(data) { - utils.widget.healthMonitoring.getSystemHealthStatus.setData(queryInput, (oldData) => - oldData?.map((item) => (item.integrationId === data.integrationId ? { ...item, ...data } : item)), - ); - }, - }); + const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery(queryInput); const lastItem = data.at(-1); diff --git a/packages/widgets/src/system-resources/component.tsx b/packages/widgets/src/system-resources/component.tsx index d9e5a8604..b67e0299a 100644 --- a/packages/widgets/src/system-resources/component.tsx +++ b/packages/widgets/src/system-resources/component.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Box, Group, Stack } from "@mantine/core"; import { useElementSize } from "@mantine/hooks"; @@ -15,56 +15,47 @@ import { NetworkTrafficChart } from "./chart/network-traffic"; const MAX_QUEUE_SIZE = 15; +const toChartItem = (healthInfo: { + cpuUtilization: number; + memUsedInBytes: number; + gpu: { processorUtilization: number }[]; + network: { up: number; down: number } | null; +}) => ({ + cpu: healthInfo.cpuUtilization, + memory: healthInfo.memUsedInBytes, + gpu: + healthInfo.gpu.length > 0 + ? healthInfo.gpu.reduce((acc, g) => acc + g.processorUtilization, 0) / healthInfo.gpu.length + : 0, + network: healthInfo.network, +}); + export default function SystemResources({ integrationIds, options }: WidgetComponentProps<"systemResources">) { const { ref, width } = useElementSize(); - const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery( - { - integrationIds, - }, - { - staleTime: 5 * 1000, - }, - ); + const { data = [] } = clientApi.widget.healthMonitoring.getSystemHealthStatus.useQuery({ + integrationIds, + }); const memoryCapacityInBytes = (data[0]?.healthInfo.memAvailableInBytes ?? 0) + (data[0]?.healthInfo.memUsedInBytes ?? 0); const [items, setItems] = useState< { cpu: number; memory: number; gpu: number; network: { up: number; down: number } | null }[] - >( - data.map((item) => ({ - cpu: item.healthInfo.cpuUtilization, - memory: item.healthInfo.memUsedInBytes, - gpu: - item.healthInfo.gpu.length > 0 - ? item.healthInfo.gpu.reduce((acc, g) => acc + g.processorUtilization, 0) / item.healthInfo.gpu.length - : 0, - network: item.healthInfo.network, - })), - ); + >(() => data.map((item) => toChartItem(item.healthInfo))); - clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription( - { - integrationIds, - }, - { - onData(data) { - setItems((previousItems) => { - const next = { - cpu: data.healthInfo.cpuUtilization, - memory: data.healthInfo.memUsedInBytes, - gpu: - data.healthInfo.gpu.length > 0 - ? data.healthInfo.gpu.reduce((acc, g) => acc + g.processorUtilization, 0) / data.healthInfo.gpu.length - : 0, - network: data.healthInfo.network, - }; + const lastUpdatedAtRef = useRef(null); - return [...previousItems, next].slice(-MAX_QUEUE_SIZE); - }); - }, - }, - ); + useEffect(() => { + const firstItem = data[0]; + if (!firstItem) return; + + const updatedAt = firstItem.updatedAt?.getTime?.() ?? null; + if (updatedAt !== null && updatedAt === lastUpdatedAtRef.current) return; + lastUpdatedAtRef.current = updatedAt; + + const next = toChartItem(firstItem.healthInfo); + setItems((previousItems) => [...previousItems, next].slice(-MAX_QUEUE_SIZE)); + }, [data]); const showNetwork = items.length === 0 || (items.every((item) => item.network !== null) && options.visibleCharts.includes("network")); diff --git a/packages/widgets/src/timetable/component.tsx b/packages/widgets/src/timetable/component.tsx index 539d7e6c7..2f1ad982b 100644 --- a/packages/widgets/src/timetable/component.tsx +++ b/packages/widgets/src/timetable/component.tsx @@ -26,16 +26,11 @@ interface TimetableWidgetInnerProps { } const TimetableWidgetInner = ({ station, integrationId }: TimetableWidgetInnerProps) => { - const { data: timetable } = clientApi.widget.timetable.getTimetable.useQuery( - { - integrationId, - stationId: station.value, - limit: 10, - }, - { - staleTime: 60 * 1000, - }, - ); + const { data: timetable } = clientApi.widget.timetable.getTimetable.useQuery({ + integrationId, + stationId: station.value, + limit: 10, + }); const t = useScopedI18n("widget.timetable"); return ( diff --git a/packages/widgets/src/tracearr/component.tsx b/packages/widgets/src/tracearr/component.tsx index 0ff5e9132..a56e25ae5 100644 --- a/packages/widgets/src/tracearr/component.tsx +++ b/packages/widgets/src/tracearr/component.tsx @@ -35,28 +35,7 @@ interface TracearrContentProps { function TracearrContent({ integrationIds, options, width, isEditMode }: TracearrContentProps) { const t = useScopedI18n("widget.tracearr"); - const { data: dashboardData = [] } = clientApi.widget.tracearr.getDashboard.useQuery( - { integrationIds }, - { staleTime: 5 * 1000 }, - ); - - const utils = clientApi.useUtils(); - clientApi.widget.tracearr.subscribeToDashboard.useSubscription( - { integrationIds }, - { - enabled: !isEditMode, - onData(newData) { - utils.widget.tracearr.getDashboard.setData({ integrationIds }, (prevData) => { - if (!prevData) return prevData; - return prevData.map((instance) => - instance.integrationId === newData.integrationId - ? { ...instance, dashboard: newData.dashboard, updatedAt: newData.timestamp } - : instance, - ); - }); - }, - }, - ); + const { data: dashboardData = [] } = clientApi.widget.tracearr.getDashboard.useQuery({ integrationIds }); // Merge data from all integrations const combined = dashboardData.reduce( diff --git a/packages/widgets/src/umami/umami-content.tsx b/packages/widgets/src/umami/umami-content.tsx index 2f8dea22c..b1081c7e2 100644 --- a/packages/widgets/src/umami/umami-content.tsx +++ b/packages/widgets/src/umami/umami-content.tsx @@ -40,30 +40,19 @@ export function UmamiContent({ const { colorScheme } = useMantineColorScheme(); const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057"; - const { data: results = [] } = clientApi.widget.umami.getVisitorStats.useQuery( - { - integrationIds, - websiteId, - timeFrame, - eventName, - }, - { staleTime: 5 * 60 * 1000 }, - ); + const { data: results = [] } = clientApi.widget.umami.getVisitorStats.useQuery({ + integrationIds, + websiteId, + timeFrame, + eventName, + }); const activeVisitorsInput = { integrationId: integrationIds[0] ?? "", websiteId }; - const { data: activeVisitors = 0 } = clientApi.widget.umami.getActiveVisitors.useQuery(activeVisitorsInput, { - staleTime: 30 * 1000, - }); - const utils = clientApi.useUtils(); - clientApi.widget.umami.subscribeActiveVisitors.useSubscription(activeVisitorsInput, { - onData(count) { - utils.widget.umami.getActiveVisitors.setData(activeVisitorsInput, () => count); - }, - }); + const { data: activeVisitors = 0 } = clientApi.widget.umami.getActiveVisitors.useQuery(activeVisitorsInput); const { data: multiEventSeries } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery( { integrationId: integrationIds[0] ?? "", websiteId, timeFrame, eventNames: [...eventNames].toSorted() }, - { enabled: viewMode === "events" && eventNames.length > 0, staleTime: 5 * 60 * 1000 }, + { enabled: viewMode === "events" && eventNames.length > 0 }, ); const multiEventTotal = multiEventSeries diff --git a/packages/widgets/src/umami/umami-events-content.tsx b/packages/widgets/src/umami/umami-events-content.tsx index 0351e906c..6dfac617c 100644 --- a/packages/widgets/src/umami/umami-events-content.tsx +++ b/packages/widgets/src/umami/umami-events-content.tsx @@ -28,15 +28,12 @@ export function UmamiEventsContent({ const { colorScheme } = useMantineColorScheme(); const tickColor = colorScheme === "dark" ? "#c1c2c5" : "#495057"; - const { data: series = [] } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery( - { - integrationId: integrationIds[0] ?? "", - websiteId, - timeFrame, - eventNames: [...eventNames].toSorted(), - }, - { staleTime: 5 * 60 * 1000 }, - ); + const { data: series = [] } = clientApi.widget.umami.getMultiEventTimeSeries.useQuery({ + integrationId: integrationIds[0] ?? "", + websiteId, + timeFrame, + eventNames: [...eventNames].toSorted(), + }); if (eventNames.length === 0) { return ( diff --git a/packages/widgets/src/umami/umami-top-list.tsx b/packages/widgets/src/umami/umami-top-list.tsx index 125115f0c..699df9b74 100644 --- a/packages/widgets/src/umami/umami-top-list.tsx +++ b/packages/widgets/src/umami/umami-top-list.tsx @@ -15,29 +15,23 @@ interface UmamiTopListProps { export function UmamiTopPagesContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) { const t = useScopedI18n("widget.umami"); - const { data = [] } = clientApi.widget.umami.getTopPages.useQuery( - { - integrationId: integrationIds[0] ?? "", - websiteId, - timeFrame, - limit, - }, - { staleTime: 5 * 60 * 1000 }, - ); + const { data = [] } = clientApi.widget.umami.getTopPages.useQuery({ + integrationId: integrationIds[0] ?? "", + websiteId, + timeFrame, + limit, + }); return ; } export function UmamiTopReferrersContent({ integrationIds, websiteId, timeFrame, limit }: UmamiTopListProps) { const t = useScopedI18n("widget.umami"); - const { data = [] } = clientApi.widget.umami.getTopReferrers.useQuery( - { - integrationId: integrationIds[0] ?? "", - websiteId, - timeFrame, - limit, - }, - { staleTime: 5 * 60 * 1000 }, - ); + const { data = [] } = clientApi.widget.umami.getTopReferrers.useQuery({ + integrationId: integrationIds[0] ?? "", + websiteId, + timeFrame, + limit, + }); return ( { - if (!prevData) return prevData; - return prevData.map((instance) => - instance.integrationId === newData.integrationId - ? { ...instance, summaries: newData.summaries, updatedAt: newData.timestamp } - : instance, - ); - }); - }, - }, - ); + const { data } = clientApi.widget.ups.getSummaries.useQuery({ integrationIds }); if (!data) return ; diff --git a/packages/widgets/src/uptime-kuma/component.tsx b/packages/widgets/src/uptime-kuma/component.tsx index 2a3b6136b..679df8ea5 100644 --- a/packages/widgets/src/uptime-kuma/component.tsx +++ b/packages/widgets/src/uptime-kuma/component.tsx @@ -107,29 +107,7 @@ interface UptimeKumaContentProps { function UptimeKumaContent({ integrationIds, isEditMode, options, width }: UptimeKumaContentProps) { const t = useScopedI18n("widget.uptimeKuma"); - const { data: dashboardData } = clientApi.widget.uptimeKuma.getDashboard.useQuery( - { integrationIds }, - { staleTime: 30 * 1000 }, - ); - - const utils = clientApi.useUtils(); - clientApi.widget.uptimeKuma.subscribeToDashboard.useSubscription( - { integrationIds }, - { - enabled: !isEditMode, - onData(newData) { - utils.widget.uptimeKuma.getDashboard.setData({ integrationIds }, (prevData) => { - if (!prevData) return prevData; - return prevData.map((instance) => { - if (instance.integrationId === newData.integrationId) { - return { ...instance, dashboard: newData.dashboard, updatedAt: newData.timestamp }; - } - return instance; - }); - }); - }, - }, - ); + const { data: dashboardData } = clientApi.widget.uptimeKuma.getDashboard.useQuery({ integrationIds }); if (!dashboardData) return ; diff --git a/packages/widgets/src/weather/component.tsx b/packages/widgets/src/weather/component.tsx index a2e9778ca..6e76ef8f2 100644 --- a/packages/widgets/src/weather/component.tsx +++ b/packages/widgets/src/weather/component.tsx @@ -19,16 +19,7 @@ export default function WeatherWidget({ isEditMode, options }: WidgetComponentPr latitude: options.location.latitude, longitude: options.location.longitude, }; - const { data: weather } = clientApi.widget.weather.atLocation.useQuery(input, { - staleTime: 60 * 1000, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }); - - const utils = clientApi.useUtils(); - clientApi.widget.weather.subscribeAtLocation.useSubscription(input, { - onData: (data) => utils.widget.weather.atLocation.setData(input, data), - }); + const { data: weather } = clientApi.widget.weather.atLocation.useQuery(input); if (!weather) return ; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5baf6a38f..eb6fbb329 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,7 +132,7 @@ catalogs: '@tabler/icons-react': specifier: ^3.44.0 version: 3.44.0 - '@tanstack/query-persist-client-core': + '@tanstack/query-async-storage-persister': specifier: ^5.101.0 version: 5.101.0 '@tanstack/react-query': @@ -144,6 +144,9 @@ catalogs: '@tanstack/react-query-next-experimental': specifier: ^5.101.0 version: 5.101.0 + '@tanstack/react-query-persist-client': + specifier: ^5.101.0 + version: 5.101.0 '@testcontainers/mysql': specifier: ^12.0.2 version: 12.0.2 @@ -920,7 +923,7 @@ importers: '@tabler/icons-react': specifier: 'catalog:' version: 3.44.0(react@19.2.7) - '@tanstack/query-persist-client-core': + '@tanstack/query-async-storage-persister': specifier: 'catalog:' version: 5.101.0 '@tanstack/react-query': @@ -932,6 +935,9 @@ importers: '@tanstack/react-query-next-experimental': specifier: 'catalog:' version: 5.101.0(@tanstack/react-query@5.101.0(react@19.2.7))(next@16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0))(react@19.2.7) + '@tanstack/react-query-persist-client': + specifier: 'catalog:' + version: 5.101.0(@tanstack/react-query@5.101.0(react@19.2.7))(react@19.2.7) '@trpc/client': specifier: 'catalog:' version: 11.17.0(@trpc/server@11.17.0(typescript@6.0.3))(typescript@6.0.3) @@ -7121,6 +7127,9 @@ packages: resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} engines: {node: '>=12'} + '@tanstack/query-async-storage-persister@5.101.0': + resolution: {integrity: sha512-8Y8pwMVPh4kqU9m+MhbDYBuub+i5T2M9P/0+Ae3KccX1BwMIp6AdFAx3zDgLoZcEVI830mbJYf0sjAgsawCWVw==} + '@tanstack/query-core@5.101.0': resolution: {integrity: sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==} @@ -7143,6 +7152,12 @@ packages: next: ^13 || ^14 || ^15 || ^16 react: ^18 || ^19 + '@tanstack/react-query-persist-client@5.101.0': + resolution: {integrity: sha512-AUcdBgz8V6sM9axzdqkVmWjYSOETkhr6yAZSBnEFyZT2jo6vkFq3UrpRuxGs6fmhKMWv8FA+ZJGcbaKPaoAElQ==} + peerDependencies: + '@tanstack/react-query': ^5.101.0 + react: ^18 || ^19 + '@tanstack/react-query@5.101.0': resolution: {integrity: sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==} peerDependencies: @@ -20602,6 +20617,11 @@ snapshots: dependencies: remove-accents: 0.5.0 + '@tanstack/query-async-storage-persister@5.101.0': + dependencies: + '@tanstack/query-core': 5.101.0 + '@tanstack/query-persist-client-core': 5.101.0 + '@tanstack/query-core@5.101.0': {} '@tanstack/query-devtools@5.101.0': {} @@ -20622,6 +20642,12 @@ snapshots: next: 16.2.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.60.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.101.0) react: 19.2.7 + '@tanstack/react-query-persist-client@5.101.0(@tanstack/react-query@5.101.0(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/query-persist-client-core': 5.101.0 + '@tanstack/react-query': 5.101.0(react@19.2.7) + react: 19.2.7 + '@tanstack/react-query@5.101.0(react@19.2.7)': dependencies: '@tanstack/query-core': 5.101.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b140ae1b1..4c9bea37f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -45,10 +45,11 @@ catalog: "@semantic-release/release-notes-generator": ^14.1.1 "@t3-oss/env-nextjs": ^0.13.11 "@tabler/icons-react": ^3.44.0 - "@tanstack/query-persist-client-core": ^5.101.0 + "@tanstack/query-async-storage-persister": ^5.101.0 "@tanstack/react-query": ^5.101.0 "@tanstack/react-query-devtools": ^5.101.0 "@tanstack/react-query-next-experimental": ^5.101.0 + "@tanstack/react-query-persist-client": ^5.101.0 "@testcontainers/mysql": ^12.0.2 "@testcontainers/postgresql": ^12.0.2 "@testcontainers/redis": ^12.0.2