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:
53 files changed
+998
-52
No files matched your search
@@ -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,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",
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user