chore: temporarily restore deleted files for CodeRabbit review

Restoring 27 deleted files and 26 trivially-changed request handlers
to bring the PR under CodeRabbit's 150-file review limit.
These will be re-deleted in the next commit.
This commit is contained in:
Thomas Camlong committed 2026-06-27 11:49:15 +02:00
1 parent 96fe0d55c7
commit e9611e8098
53 files changed
+998 -52

No files matched your search

@@ -0,0 +1,18 @@
import { createLogger } from "@homarr/core/infrastructure/logs";
import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
const logger = createLogger({ module: "invalidateUpdateCheckerCache" });
/**
* Invalidates the update checker cache on startup to ensure fresh data.
* It is important as we want to avoid showing pending updates after the update to latest version.
*/
export async function invalidateUpdateCheckerCacheAsync() {
try {
const handler = updateCheckerRequestHandler.handler({});
await handler.invalidateAsync();
logger.debug("Update checker cache invalidated");
} catch (error) {
logger.error(new Error("Failed to invalidate update checker cache", { cause: error }));
}
}
+31
View File
@@ -0,0 +1,31 @@
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<WidgetComponentProps<"dockerContainers">["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 }));
}
}),
);
});
@@ -0,0 +1,15 @@
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: () => ({}),
},
}),
);
@@ -0,0 +1,16 @@
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,
}),
},
}),
);
@@ -0,0 +1,46 @@
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: () => ({}),
},
}),
);
@@ -0,0 +1,33 @@
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<string, never>) => {
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: () => ({}),
},
},
),
);
@@ -0,0 +1,16 @@
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,
}),
},
}),
);
@@ -0,0 +1,14 @@
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: () => ({}),
},
}),
);
@@ -0,0 +1,36 @@
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;
},
},
}),
);
@@ -0,0 +1,24 @@
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": () => ({}),
},
}),
);
@@ -0,0 +1,14 @@
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 }),
},
}),
);
@@ -0,0 +1,14 @@
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 }),
},
}),
);
@@ -0,0 +1,14 @@
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: () => ({}),
},
}),
);
@@ -0,0 +1,14 @@
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,
},
}),
);
@@ -0,0 +1,20 @@
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,
}
: [],
},
}),
);
@@ -0,0 +1,14 @@
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: () => ({}),
},
}),
);
@@ -0,0 +1,14 @@
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 } : []),
},
}),
);
@@ -0,0 +1,14 @@
import { EVERY_30_SECONDS } from "@homarr/cron-jobs-core/expressions";
import { vpnSummaryHandler } from "@homarr/request-handler/vpn";
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
import { createCronJob } from "../../lib";
export const vpnJob = createCronJob("vpn", EVERY_30_SECONDS).withCallback(
createRequestIntegrationJobHandler(vpnSummaryHandler.handler, {
widgetKinds: ["vpn"],
getInput: {
vpn: () => ({}),
},
}),
);
@@ -0,0 +1,25 @@
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<WidgetComponentProps<"minecraftServerStatus">["options"]>(item.options);
const innerHandler = minecraftServerStatusRequestHandler.handler(options);
await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
}),
);
},
);
+39
View File
@@ -0,0 +1,39 @@
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<WidgetComponentProps<"rssFeed">["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 }));
}
}
}
});
@@ -0,0 +1,11 @@
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({});
});
+36
View File
@@ -0,0 +1,36 @@
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<WidgetComponentProps<"weather">["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 }));
}
}
});
@@ -0,0 +1,6 @@
import { createChannelEventHistory } from "../channel";
export const createIntegrationHistoryChannel = <TData>(integrationId: string, queryKey: string, maxElements = 32) => {
const channelName = `integration:${integrationId}:history:${queryKey}`;
return createChannelEventHistory<TData>(channelName, maxElements);
};
@@ -1,10 +1,12 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import type { ArchiveTeamWarriorStatus } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const archiveTeamWarriorRequestHandler = createIntegrationRequestHandler<
export const archiveTeamWarriorRequestHandler = createCachedIntegrationRequestHandler<
ArchiveTeamWarriorStatus,
IntegrationKindByCategory<"archiving">,
Record<string, never>
@@ -13,4 +15,6 @@ export const archiveTeamWarriorRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getStatusAsync();
},
cacheDuration: dayjs.duration(5, "minute"),
queryKey: "archiveTeamWarriorStatus",
});
@@ -1,9 +1,11 @@
import dayjs from "dayjs";
import { createIntegrationAsync } from "@homarr/integrations";
import type { AudiobookshelfDashboardData } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const audiobookshelfRequestHandler = createIntegrationRequestHandler<
export const audiobookshelfRequestHandler = createCachedIntegrationRequestHandler<
AudiobookshelfDashboardData,
"audiobookshelf",
Record<string, never>
@@ -12,4 +14,6 @@ export const audiobookshelfRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
cacheDuration: dayjs.duration(10, "minutes"),
queryKey: "audiobookshelfDashboard",
});
+4 -2
View File
@@ -4,9 +4,9 @@ import type { IntegrationKindByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import type { CalendarEvent, RadarrReleaseType } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const calendarMonthRequestHandler = createIntegrationRequestHandler<
export const calendarMonthRequestHandler = createCachedIntegrationRequestHandler<
CalendarEvent[],
IntegrationKindByCategory<"calendar">,
{ year: number; month: number; releaseType: RadarrReleaseType[]; showUnmonitored: boolean }
@@ -23,4 +23,6 @@ export const calendarMonthRequestHandler = createIntegrationRequestHandler<
input.showUnmonitored,
);
},
cacheDuration: dayjs.duration(1, "minute"),
queryKey: "calendarMonth",
});
+6 -2
View File
@@ -1,9 +1,11 @@
import dayjs from "dayjs";
import { createIntegrationAsync } from "@homarr/integrations";
import type { CoolifyInstanceInfo } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const coolifyRequestHandler = createIntegrationRequestHandler<
export const coolifyRequestHandler = createCachedIntegrationRequestHandler<
CoolifyInstanceInfo,
"coolify",
Record<string, never>
@@ -12,4 +14,6 @@ export const coolifyRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getInstanceInfoAsync();
},
cacheDuration: dayjs.duration(30, "seconds"),
queryKey: "coolifyInfo",
});
+6 -2
View File
@@ -1,10 +1,12 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import type { DnsHoleSummary } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const dnsHoleRequestHandler = createIntegrationRequestHandler<
export const dnsHoleRequestHandler = createCachedIntegrationRequestHandler<
DnsHoleSummary,
IntegrationKindByCategory<"dnsHole">,
Record<string, never>
@@ -13,4 +15,6 @@ export const dnsHoleRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getSummaryAsync();
},
cacheDuration: dayjs.duration(5, "seconds"),
queryKey: "dnsHoleSummary",
});
+6 -2
View File
@@ -1,10 +1,12 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import type { DownloadClientJobsAndStatus } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const downloadClientRequestHandler = createIntegrationRequestHandler<
export const downloadClientRequestHandler = createCachedIntegrationRequestHandler<
DownloadClientJobsAndStatus,
IntegrationKindByCategory<"downloadClient">,
{ limit: number }
@@ -13,4 +15,6 @@ export const downloadClientRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getClientJobsAndStatusAsync(input);
},
cacheDuration: dayjs.duration(5, "seconds"),
queryKey: "downloadClientJobStatus",
});
@@ -1,10 +1,12 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import type { Indexer } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const indexerManagerRequestHandler = createIntegrationRequestHandler<
export const indexerManagerRequestHandler = createCachedIntegrationRequestHandler<
Indexer[],
IntegrationKindByCategory<"indexerManager">,
Record<string, never>
@@ -13,4 +15,6 @@ export const indexerManagerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getIndexersAsync();
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "indexerManager",
});
@@ -0,0 +1,49 @@
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";
type IntegrationOfKind<TKind extends IntegrationKind> = Omit<Integration, "kind"> & {
kind: TKind;
decryptedSecrets: Modify<Pick<IntegrationSecret, "kind" | "value">, { value: string }>[];
externalUrl: string | null;
};
interface Options<TData, TKind extends IntegrationKind, TInput extends Record<string, unknown>> {
queryKey: string;
requestAsync: (integration: IntegrationOfKind<TKind>, input: TInput) => Promise<TData>;
cacheDuration: Duration;
cacheDurationForInput?: (input: TInput) => Duration | undefined;
fallbackToStaleOnError?: boolean;
retry?: { attempts?: number; delayMs?: number };
isValid?: (data: TData) => boolean;
}
export const createCachedIntegrationRequestHandler = <
TData,
TKind extends IntegrationKind,
TInput extends Record<string, unknown>,
>(
options: Options<TData, TKind, TInput>,
) => {
return {
handler: (integration: IntegrationOfKind<TKind>, itemOptions: TInput) =>
createCachedRequestHandler({
queryKey: options.queryKey,
requestAsync: async (input: { options: TInput; integration: IntegrationOfKind<TKind> }) => {
return await options.requestAsync(input.integration, input.options);
},
cacheDuration: options.cacheDurationForInput?.(itemOptions) ?? options.cacheDuration,
fallbackToStaleOnError: options.fallbackToStaleOnError,
retry: options.retry,
isValid: options.isValid,
createRedisChannel(input, handlerOptions) {
return createIntegrationOptionsChannel<TData>(input.integration.id, handlerOptions.queryKey, input.options);
},
}).handler({ options: itemOptions, integration }),
};
};
@@ -0,0 +1,160 @@
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<TData> = { data: TData; timestamp: Date };
type InFlightEntry = Promise<FetchResult<unknown>>;
const inFlightFetches = new Map<string, InFlightEntry>();
const isAbortedError = (error: unknown): boolean => {
if (!(error instanceof Error)) return false;
return error.name === "AbortError" || error.name === "TimeoutError";
};
interface Options<TData, TInput extends Record<string, unknown>> {
// Unique key for this request handler
queryKey: string;
requestAsync: (input: TInput) => Promise<TData>;
createRedisChannel: (
input: TInput,
options: Options<TData, TInput>,
) => ReturnType<typeof createChannelWithLatestAndEvents<TData>>;
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<void>((resolve) => setTimeout(resolve, ms));
export const createCachedRequestHandler = <TData, TInput extends Record<string, unknown>>(
options: Options<TData, TInput>,
) => {
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<FetchResult<TData>> => {
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<FetchResult<TData>>;
}
const promise = (async (): Promise<FetchResult<TData>> => {
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);
},
};
},
};
};
@@ -0,0 +1,115 @@
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<TWidgetKind>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
THandler extends ReturnType<typeof createCachedIntegrationRequestHandler<any, TIntegrationKind, any>>["handler"],
>(
handler: THandler,
{
widgetKinds,
getInput,
}: {
widgetKinds: TWidgetKind[];
getInput: {
[key in TWidgetKind]: (options: WidgetComponentProps<key>["options"]) => MaybeArray<Parameters<THandler>[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<THandler>[1];
}[] = [];
for (const itemForIntegration of itemsForIntegration) {
const oneOrMultipleInputs = getInput[itemForIntegration.kind](
reduceWidgetOptionsWithDefaultValues(
itemForIntegration.kind,
{
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
forceDisableStatus: serverSettings.board.forceDisableStatus,
},
SuperJSON.parse<Record<string, unknown>>(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 }),
);
}
}
};
};
@@ -0,0 +1,36 @@
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<TData, TKind extends WidgetKind, TInput extends Record<string, unknown>> {
// Unique key for this request handler
queryKey: string;
requestAsync: (input: TInput) => Promise<TData>;
cacheDuration: Duration;
widgetKind: TKind;
}
export const createCachedWidgetRequestHandler = <
TData,
TKind extends WidgetKind,
TInput extends Record<string, unknown>,
>(
requestHandlerOptions: Options<TData, TKind, TInput>,
) => {
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<TData>(requestHandlerOptions.widgetKind, options.queryKey, input);
},
}).handler(widgetOptions),
};
};
@@ -1,10 +1,12 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import type { MediaRelease } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const mediaReleaseRequestHandler = createIntegrationRequestHandler<
export const mediaReleaseRequestHandler = createCachedIntegrationRequestHandler<
MediaRelease[],
IntegrationKindByCategory<"mediaRelease">,
Record<string, never>
@@ -13,4 +15,6 @@ export const mediaReleaseRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getMediaReleasesAsync();
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "mediaReleases",
});
@@ -1,10 +1,12 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import type { MediaRequest } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const mediaRequestListRequestHandler = createIntegrationRequestHandler<
export const mediaRequestListRequestHandler = createCachedIntegrationRequestHandler<
MediaRequest[],
IntegrationKindByCategory<"mediaRequest">,
Record<string, never>
@@ -13,4 +15,6 @@ export const mediaRequestListRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getRequestsAsync();
},
cacheDuration: dayjs.duration(1, "minute"),
queryKey: "mediaRequestList",
});
@@ -1,10 +1,12 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import type { MediaRequestStats } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const mediaRequestStatsRequestHandler = createIntegrationRequestHandler<
export const mediaRequestStatsRequestHandler = createCachedIntegrationRequestHandler<
MediaRequestStats,
IntegrationKindByCategory<"mediaRequest">,
Record<string, never>
@@ -16,4 +18,6 @@ export const mediaRequestStatsRequestHandler = createIntegrationRequestHandler<
users: await integrationInstance.getUsersAsync(),
};
},
cacheDuration: dayjs.duration(1, "minute"),
queryKey: "mediaRequestStats",
});
+6 -2
View File
@@ -1,10 +1,12 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import type { StreamSession } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const mediaServerRequestHandler = createIntegrationRequestHandler<
export const mediaServerRequestHandler = createCachedIntegrationRequestHandler<
StreamSession[],
IntegrationKindByCategory<"mediaService">,
{
@@ -15,4 +17,6 @@ export const mediaServerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getCurrentSessionsAsync({ showOnlyPlaying: input.showOnlyPlaying });
},
cacheDuration: dayjs.duration(5, "seconds"),
queryKey: "mediaServerSessions",
});
@@ -1,14 +1,18 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "@homarr/integrations";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const mediaTranscodingRequestHandler = createIntegrationRequestHandler<
export const mediaTranscodingRequestHandler = createCachedIntegrationRequestHandler<
MediaTranscoding,
IntegrationKindByCategory<"mediaTranscoding">,
{ pageOffset: number; pageSize: number }
>({
queryKey: "mediaTranscoding",
cacheDuration: dayjs.duration(5, "minutes"),
async requestAsync(integration, input) {
const integrationInstance = await createIntegrationAsync(integration);
return {
@@ -1,11 +1,14 @@
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 { createWidgetRequestHandler } from "./lib/widget-request-handler";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
export const minecraftServerStatusRequestHandler = createWidgetRequestHandler({
export const minecraftServerStatusRequestHandler = createCachedWidgetRequestHandler({
queryKey: "minecraftServerStatusApiResult",
widgetKind: "minecraftServerStatus",
async requestAsync(input: { domain: string; isBedrockServer: boolean }) {
const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`;
@@ -14,6 +17,7 @@ export const minecraftServerStatusRequestHandler = createWidgetRequestHandler({
);
return responseSchema.parse(await response.json());
},
cacheDuration: dayjs.duration(5, "minutes"),
});
const responseSchema = z
+6 -2
View File
@@ -1,9 +1,11 @@
import dayjs from "dayjs";
import { createIntegrationAsync } from "@homarr/integrations";
import type { NavidromeDashboardData } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const navidromeRequestHandler = createIntegrationRequestHandler<
export const navidromeRequestHandler = createCachedIntegrationRequestHandler<
NavidromeDashboardData,
"navidrome",
Record<string, never>
@@ -12,4 +14,6 @@ export const navidromeRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
cacheDuration: dayjs.duration(10, "minutes"),
queryKey: "navidromeDashboard",
});
@@ -1,10 +1,12 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import type { NetworkControllerSummary } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const networkControllerRequestHandler = createIntegrationRequestHandler<
export const networkControllerRequestHandler = createCachedIntegrationRequestHandler<
NetworkControllerSummary,
IntegrationKindByCategory<"networkController">,
Record<string, never>
@@ -13,4 +15,6 @@ export const networkControllerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getNetworkSummaryAsync();
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "networkControllerSummary",
});
@@ -1,10 +1,12 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import type { Notification } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const notificationsRequestHandler = createIntegrationRequestHandler<
export const notificationsRequestHandler = createCachedIntegrationRequestHandler<
Notification[],
IntegrationKindByCategory<"notifications">,
Record<string, never>
@@ -13,4 +15,6 @@ export const notificationsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getNotificationsAsync();
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "notificationsJobStatus",
});
@@ -1,9 +1,11 @@
import dayjs from "dayjs";
import { createIntegrationAsync } from "@homarr/integrations";
import type { PaperlessNgxStats } from "@homarr/integrations";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const paperlessNgxStatsRequestHandler = createIntegrationRequestHandler<
export const paperlessNgxStatsRequestHandler = createCachedIntegrationRequestHandler<
PaperlessNgxStats,
"paperlessNgx",
Record<string, never>
@@ -12,4 +14,6 @@ export const paperlessNgxStatsRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getStatsAsync();
},
cacheDuration: dayjs.duration(15, "minute"),
queryKey: "paperless-ngx-stats",
});
+6 -2
View File
@@ -1,10 +1,12 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import type { ReleaseResponse, ReleasesRepository } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const releasesRequestHandler = createIntegrationRequestHandler<
export const releasesRequestHandler = createCachedIntegrationRequestHandler<
ReleaseResponse,
IntegrationKindByCategory<"releasesProvider">,
ReleasesRepository
@@ -13,4 +15,6 @@ export const releasesRequestHandler = createIntegrationRequestHandler<
const instance = await createIntegrationAsync(integration);
return instance.getLatestMatchingReleaseAsync(input.identifier, input.versionRegex);
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "repositoriesReleases",
});
+6 -2
View File
@@ -1,15 +1,18 @@
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 { createWidgetRequestHandler } from "./lib/widget-request-handler";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
const logger = createLogger({ module: "rssFeedsRequestHandler" });
export const rssFeedsRequestHandler = createWidgetRequestHandler({
export const rssFeedsRequestHandler = createCachedWidgetRequestHandler({
queryKey: "rssFeedList",
widgetKind: "rssFeed",
async requestAsync(input: { url: string; count: number }) {
const result = (await extract(input.url, {
getExtraEntryFields: (feedEntry) => {
@@ -28,6 +31,7 @@ export const rssFeedsRequestHandler = createWidgetRequestHandler({
entries: result.entries?.map((entry) => ({ ...entry, feedUrl: input.url })).slice(0, input.count) ?? [],
};
},
cacheDuration: dayjs.duration(5, "minutes"),
});
const attemptGetImageFromEntry = (feedUrl: string, entry: object) => {
@@ -1,9 +1,11 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandler<
export const smartHomeEntityStateRequestHandler = createCachedIntegrationRequestHandler<
string,
IntegrationKindByCategory<"smartHomeServer">,
{ entityId: string }
@@ -18,4 +20,6 @@ export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandle
return result.data.state;
},
cacheDuration: dayjs.duration(1, "minute"),
queryKey: "smartHome-entityState",
});
@@ -1,9 +1,11 @@
import dayjs from "dayjs";
import { createIntegrationAsync } from "@homarr/integrations";
import type { SpeedtestTrackerDashboardData } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const speedtestTrackerRequestHandler = createIntegrationRequestHandler<
export const speedtestTrackerRequestHandler = createCachedIntegrationRequestHandler<
SpeedtestTrackerDashboardData,
"speedtestTracker",
Record<string, never>
@@ -12,4 +14,6 @@ export const speedtestTrackerRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
cacheDuration: dayjs.duration(10, "minutes"),
queryKey: "speedtestTrackerDashboard",
});
+6 -2
View File
@@ -1,11 +1,14 @@
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 { createWidgetRequestHandler } from "./lib/widget-request-handler";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
export const fetchStockPriceHandler = createWidgetRequestHandler({
export const fetchStockPriceHandler = createCachedWidgetRequestHandler({
queryKey: "fetchStockPriceResult",
widgetKind: "stockPrice",
async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) {
const response = await withTimeoutAsync(async (signal) => {
return await fetchWithTrustedCertificatesAsync(
@@ -39,6 +42,7 @@ export const fetchStockPriceHandler = createWidgetRequestHandler({
shortName: firstResult.meta.shortName,
};
},
cacheDuration: dayjs.duration(5, "minutes"),
});
const dataSchema = z
+6 -2
View File
@@ -1,9 +1,11 @@
import dayjs from "dayjs";
import { createIntegrationAsync } from "@homarr/integrations";
import type { TracearrDashboardData } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const tracearrRequestHandler = createIntegrationRequestHandler<
export const tracearrRequestHandler = createCachedIntegrationRequestHandler<
TracearrDashboardData,
"tracearr",
Record<string, never>
@@ -12,4 +14,6 @@ export const tracearrRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
cacheDuration: dayjs.duration(5, "seconds"),
queryKey: "tracearrDashboard",
});
+6 -2
View File
@@ -1,10 +1,12 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import type { UpsSummary } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const upsSummariesRequestHandler = createIntegrationRequestHandler<
export const upsSummariesRequestHandler = createCachedIntegrationRequestHandler<
UpsSummary[],
IntegrationKindByCategory<"ups">,
Record<string, never>
@@ -13,4 +15,6 @@ export const upsSummariesRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getUpsSummariesAsync();
},
cacheDuration: dayjs.duration(1, "minute"),
queryKey: "upsSummaries",
});
+6 -2
View File
@@ -1,9 +1,11 @@
import dayjs from "dayjs";
import { createIntegrationAsync } from "@homarr/integrations";
import type { UptimeKumaDashboardData } from "@homarr/integrations/types";
import { createIntegrationRequestHandler } from "./lib/integration-request-handler";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const uptimeKumaRequestHandler = createIntegrationRequestHandler<
export const uptimeKumaRequestHandler = createCachedIntegrationRequestHandler<
UptimeKumaDashboardData,
"uptimeKuma",
Record<string, never>
@@ -12,4 +14,6 @@ export const uptimeKumaRequestHandler = createIntegrationRequestHandler<
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getDashboardDataAsync();
},
cacheDuration: dayjs.duration(1, "minute"),
queryKey: "uptimeKumaDashboard",
});
+6 -2
View File
@@ -1,11 +1,14 @@
import dayjs from "dayjs";
import { z } from "zod";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
import { createWidgetRequestHandler } from "./lib/widget-request-handler";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
export const weatherRequestHandler = createWidgetRequestHandler({
export const weatherRequestHandler = createCachedWidgetRequestHandler({
queryKey: "weatherAtLocation",
widgetKind: "weather",
async requestAsync(input: { latitude: number; longitude: number }) {
const res = await withTimeoutAsync(async (signal) => {
return await fetchWithTrustedCertificatesAsync(
@@ -31,6 +34,7 @@ export const weatherRequestHandler = createWidgetRequestHandler({
}),
} satisfies Weather;
},
cacheDuration: dayjs.duration(1, "minute"),
});
const atLocationOutput = z.object({