diff --git a/apps/nextjs/src/components/layout/header/user.tsx b/apps/nextjs/src/components/layout/header/user.tsx index 9d6bd5445..0821420b7 100644 --- a/apps/nextjs/src/components/layout/header/user.tsx +++ b/apps/nextjs/src/components/layout/header/user.tsx @@ -13,7 +13,7 @@ export const UserButton = async () => { return ( - + diff --git a/apps/nextjs/src/components/user-avatar-menu.tsx b/apps/nextjs/src/components/user-avatar-menu.tsx index 8e8e2dc14..2c4251b6f 100644 --- a/apps/nextjs/src/components/user-avatar-menu.tsx +++ b/apps/nextjs/src/components/user-avatar-menu.tsx @@ -31,7 +31,7 @@ import { CurrentLanguageCombobox } from "./language/current-language-combobox"; interface UserAvatarMenuProps { children: ReactNode; - availableUpdates: RouterOutputs["updateChecker"]["getAvailableUpdates"]; + availableUpdates?: RouterOutputs["updateChecker"]["getAvailableUpdates"]; } export const UserAvatarMenu = ({ children, availableUpdates }: UserAvatarMenuProps) => { diff --git a/packages/api/src/router/update-checker.ts b/packages/api/src/router/update-checker.ts index 66f1a796f..730080873 100644 --- a/packages/api/src/router/update-checker.ts +++ b/packages/api/src/router/update-checker.ts @@ -1,17 +1,11 @@ -import { createSubPubChannel } from "@homarr/redis"; +import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; import { createTRPCRouter, protectedProcedure } from "../trpc"; export const updateCheckerRouter = createTRPCRouter({ getAvailableUpdates: protectedProcedure.query(async () => { - const channel = createSubPubChannel<{ - availableUpdates: { name: string | null; contentHtml?: string; url: string; tag_name: string }[]; - }>("homarr:update", { - persist: true, - }); - - const data = await channel.getLastDataAsync(); - - return data?.availableUpdates; + const handler = updateCheckerRequestHandler.handler({}); + const data = await handler.getCachedOrUpdatedDataAsync({}); + return data.data.availableUpdates; }), }); diff --git a/packages/cron-jobs/src/jobs/update-checker.ts b/packages/cron-jobs/src/jobs/update-checker.ts index 1e32e6efd..b985485ab 100644 --- a/packages/cron-jobs/src/jobs/update-checker.ts +++ b/packages/cron-jobs/src/jobs/update-checker.ts @@ -1,58 +1,13 @@ -import { Octokit } from "octokit"; -import { compareSemVer, isValidSemVer } from "semver-parser"; - import { EVERY_HOUR } from "@homarr/cron-jobs-core/expressions"; -import { logger } from "@homarr/log"; -import { createSubPubChannel } from "@homarr/redis"; +import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; -import packageJson from "../../../../package.json"; import { createCronJob } from "../lib"; export const updateCheckerJob = createCronJob("updateChecker", EVERY_HOUR, { runOnStart: true, }).withCallback(async () => { - const octokit = new Octokit(); - const releases = await octokit.rest.repos.listReleases({ - owner: "homarr-labs", - repo: "homarr", - }); - - const currentVersion = (packageJson as { version: string }).version; - const availableReleases = []; - - for (const release of releases.data) { - if (!isValidSemVer(release.tag_name)) { - logger.warn(`Unable to parse semantic tag '${release.tag_name}'. Update check might not work.`); - continue; - } - - availableReleases.push(release); - } - - const availableNewerReleases = availableReleases - .filter((release) => compareSemVer(release.tag_name, currentVersion) > 0) - .sort((releaseA, releaseB) => compareSemVer(releaseB.tag_name, releaseA.tag_name)); - if (availableReleases.length > 0) { - logger.info( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - `Update checker found a new available version: ${availableReleases[0]!.tag_name}. Current version is ${currentVersion}`, - ); - } else { - logger.debug(`Update checker did not find any available updates. Current version is ${currentVersion}`); - } - - const channel = createSubPubChannel<{ - availableUpdates: { name: string | null; contentHtml?: string; url: string; tag_name: string }[]; - }>("homarr:update", { - persist: true, - }); - - await channel.publishAsync({ - availableUpdates: availableNewerReleases.map((release) => ({ - name: release.name, - contentHtml: release.body_html, - url: release.html_url, - tag_name: release.tag_name, - })), + const handler = updateCheckerRequestHandler.handler({}); + await handler.getCachedOrUpdatedDataAsync({ + forceUpdate: true, }); }); diff --git a/packages/request-handler/src/update-checker.ts b/packages/request-handler/src/update-checker.ts new file mode 100644 index 000000000..b21982fae --- /dev/null +++ b/packages/request-handler/src/update-checker.ts @@ -0,0 +1,59 @@ +import dayjs from "dayjs"; +import { Octokit } from "octokit"; +import { compareSemVer, isValidSemVer } from "semver-parser"; + +import { logger } from "@homarr/log"; +import { createChannelWithLatestAndEvents } from "@homarr/redis"; +import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; + +import packageJson from "../../../package.json"; + +export const updateCheckerRequestHandler = createCachedRequestHandler({ + queryKey: "", + cacheDuration: dayjs.duration(1, "hour"), + async requestAsync(_) { + const octokit = new Octokit(); + const releases = await octokit.rest.repos.listReleases({ + owner: "homarr-labs", + repo: "homarr", + }); + + const currentVersion = (packageJson as { version: string }).version; + const availableReleases = []; + + for (const release of releases.data) { + if (!isValidSemVer(release.tag_name)) { + logger.warn(`Unable to parse semantic tag '${release.tag_name}'. Update check might not work.`); + continue; + } + + availableReleases.push(release); + } + + const availableNewerReleases = availableReleases + .filter((release) => compareSemVer(release.tag_name, currentVersion) > 0) + .sort((releaseA, releaseB) => compareSemVer(releaseB.tag_name, releaseA.tag_name)); + if (availableReleases.length > 0) { + logger.info( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + `Update checker found a new available version: ${availableReleases[0]!.tag_name}. Current version is ${currentVersion}`, + ); + } else { + logger.debug(`Update checker did not find any available updates. Current version is ${currentVersion}`); + } + + return { + availableUpdates: availableNewerReleases.map((release) => ({ + name: release.name, + contentHtml: release.body_html, + url: release.html_url, + tag_name: release.tag_name, + })), + }; + }, + createRedisChannel() { + return createChannelWithLatestAndEvents<{ + availableUpdates: { name: string | null; contentHtml?: string; url: string; tag_name: string }[]; + }>("homarr:update"); + }, +});