refactor: add cron job core package (#704)

* refactor: add cron job core package

* docs: add comments to cron validation types

* chore(deps): move node-cron dependencies from tasks app to cron-jobs-core package

* fix: runOnInit is not running on start and rather on job creation

* fix: format issues

* fix: build fails when using top level await in cjs

* chore: update turbo gen package json typescript version to 5.5.2

* fix: format issue

* fix: deepsource issues

* chore: update turbo gen package json eslint version to 9.5.0

* chore: fix frozen lockfile and format
This commit is contained in:
Meier Lukas
2024-06-22 20:00:20 +02:00
committed by GitHub
parent ea12da991a
commit 92afd82d22
26 changed files with 397 additions and 128 deletions

View File

@@ -31,8 +31,8 @@
"@homarr/widgets": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/analytics": "workspace:^0.1.0",
"@homarr/cron-jobs-core": "workspace:^0.1.0",
"dotenv": "^16.4.5",
"node-cron": "^3.0.3",
"superjson": "2.2.1",
"undici": "6.19.2"
},
@@ -40,7 +40,6 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"@types/node": "^20.14.8",
"dotenv-cli": "^7.4.2",
"eslint": "^9.5.0",

View File

@@ -3,9 +3,9 @@ import { smartHomeEntityStateJob } from "~/jobs/integrations/home-assistant";
import { analyticsJob } from "./jobs/analytics";
import { pingJob } from "./jobs/ping";
import { queuesJob } from "./jobs/queue";
import { createJobGroup } from "./lib/cron-job/group";
import { createCronJobGroup } from "./lib/jobs";
export const jobs = createJobGroup({
export const jobs = createCronJobGroup({
// Add your jobs here:
analytics: analyticsJob,
iconsUpdater: iconsUpdaterJob,

View File

@@ -1,14 +1,14 @@
import SuperJSON from "superjson";
import { sendServerAnalyticsAsync } from "@homarr/analytics";
import { EVERY_WEEK } from "@homarr/cron-jobs-core/expressions";
import { db, eq } from "@homarr/db";
import { serverSettings } from "@homarr/db/schema/sqlite";
import { EVERY_WEEK } from "~/lib/cron-job/constants";
import { createCronJob } from "~/lib/cron-job/creator";
import { createCronJob } from "~/lib/jobs";
import type { defaultServerSettings } from "../../../../packages/server-settings";
export const analyticsJob = createCronJob(EVERY_WEEK, {
export const analyticsJob = createCronJob("analytics", EVERY_WEEK, {
runOnStart: true,
}).withCallback(async () => {
const analyticSetting = await db.query.serverSettings.findFirst({

View File

@@ -1,4 +1,5 @@
import { Stopwatch } from "@homarr/common";
import { EVERY_WEEK } from "@homarr/cron-jobs-core/expressions";
import type { InferInsertModel } from "@homarr/db";
import { db, inArray } from "@homarr/db";
import { createId } from "@homarr/db/client";
@@ -6,10 +7,9 @@ import { iconRepositories, icons } from "@homarr/db/schema/sqlite";
import { fetchIconsAsync } from "@homarr/icons";
import { logger } from "@homarr/log";
import { EVERY_WEEK } from "~/lib/cron-job/constants";
import { createCronJob } from "~/lib/cron-job/creator";
import { createCronJob } from "~/lib/jobs";
export const iconsUpdaterJob = createCronJob(EVERY_WEEK, {
export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
runOnStart: true,
}).withCallback(async () => {
logger.info("Updating icon repository cache...");

View File

@@ -1,6 +1,7 @@
import SuperJSON from "superjson";
import { decryptSecret } from "@homarr/common";
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
import { db, eq } from "@homarr/db";
import { items } from "@homarr/db/schema/sqlite";
import { HomeAssistantIntegration } from "@homarr/integrations";
@@ -8,10 +9,9 @@ import { logger } from "@homarr/log";
import { homeAssistantEntityState } from "@homarr/redis";
import type { WidgetComponentProps } from "@homarr/widgets";
import { EVERY_MINUTE } from "~/lib/cron-job/constants";
import { createCronJob } from "~/lib/cron-job/creator";
import { createCronJob } from "~/lib/jobs";
export const smartHomeEntityStateJob = createCronJob(EVERY_MINUTE).withCallback(async () => {
export const smartHomeEntityStateJob = createCronJob("smartHomeEntityState", EVERY_MINUTE).withCallback(async () => {
const itemsForIntegration = await db.query.items.findMany({
where: eq(items.kind, "smartHome-entityState"),
with: {

View File

@@ -1,11 +1,11 @@
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
import { logger } from "@homarr/log";
import { sendPingRequestAsync } from "@homarr/ping";
import { pingChannel, pingUrlChannel } from "@homarr/redis";
import { EVERY_MINUTE } from "~/lib/cron-job/constants";
import { createCronJob } from "~/lib/cron-job/creator";
import { createCronJob } from "~/lib/jobs";
export const pingJob = createCronJob(EVERY_MINUTE).withCallback(async () => {
export const pingJob = createCronJob("ping", EVERY_MINUTE).withCallback(async () => {
const urls = await pingUrlChannel.getAllAsync();
for (const url of new Set(urls)) {

View File

@@ -1,8 +1,9 @@
import { EVERY_MINUTE } from "../lib/cron-job/constants";
import { createCronJob } from "../lib/cron-job/creator";
import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
import { createCronJob } from "~/lib/jobs";
import { queueWorkerAsync } from "../lib/queue/worker";
// This job processes queues, it runs every minute.
export const queuesJob = createCronJob(EVERY_MINUTE).withCallback(async () => {
export const queuesJob = createCronJob("queues", EVERY_MINUTE).withCallback(async () => {
await queueWorkerAsync();
});

View File

@@ -1,7 +0,0 @@
export const EVERY_5_SECONDS = "*/5 * * * * *";
export const EVERY_MINUTE = "* * * * *";
export const EVERY_5_MINUTES = "*/5 * * * *";
export const EVERY_10_MINUTES = "*/10 * * * *";
export const EVERY_HOUR = "0 * * * *";
export const EVERY_DAY = "0 0 * * */1";
export const EVERY_WEEK = "0 0 * * 1";

View File

@@ -1,37 +0,0 @@
import cron from "node-cron";
import type { MaybePromise } from "@homarr/common/types";
import { logger } from "@homarr/log";
interface CreateCronJobOptions {
runOnStart?: boolean;
}
export const createCronJob = (cronExpression: string, options: CreateCronJobOptions = { runOnStart: false }) => {
return {
withCallback: (callback: () => MaybePromise<void>) => {
const catchingCallbackAsync = async () => {
try {
await callback();
} catch (error) {
logger.error(
`apps/tasks/src/lib/cron-job/creator.ts: The callback of a cron job failed, expression ${cronExpression}, with error:`,
error,
);
}
};
if (options.runOnStart) {
void catchingCallbackAsync();
}
const task = cron.schedule(cronExpression, () => void catchingCallbackAsync(), {
scheduled: false,
});
return {
_expression: cronExpression,
_task: task,
};
},
};
};

View File

@@ -1,45 +0,0 @@
import { objectEntries } from "@homarr/common";
import type { createCronJob } from "./creator";
import { jobRegistry } from "./registry";
type Jobs = Record<string, ReturnType<ReturnType<typeof createCronJob>["withCallback"]>>;
export const createJobGroup = <TJobs extends Jobs>(jobs: TJobs) => {
for (const [name, job] of objectEntries(jobs)) {
if (typeof name !== "string") continue;
jobRegistry.set(name, {
name,
expression: job._expression,
active: false,
task: job._task,
});
}
return {
start: (name: keyof TJobs) => {
const job = jobRegistry.get(name as string);
if (!job) return;
job.active = true;
job.task.start();
},
startAll: () => {
for (const job of jobRegistry.values()) {
job.active = true;
job.task.start();
}
},
stop: (name: keyof TJobs) => {
const job = jobRegistry.get(name as string);
if (!job) return;
job.active = false;
job.task.stop();
},
stopAll: () => {
for (const job of jobRegistry.values()) {
job.active = false;
job.task.stop();
}
},
};
};

View File

@@ -1,9 +0,0 @@
import type cron from "node-cron";
interface Job {
name: string;
expression: string;
active: boolean;
task: cron.ScheduledTask;
}
export const jobRegistry = new Map<string, Job>();

View File

@@ -0,0 +1,21 @@
import { createCronJobFunctions } from "@homarr/cron-jobs-core";
import type { Logger } from "@homarr/cron-jobs-core/logger";
import { logger } from "@homarr/log";
class WinstonCronJobLogger implements Logger {
logDebug(message: string) {
logger.debug(message);
}
logInfo(message: string) {
logger.info(message);
}
logError(error: unknown) {
logger.error(error);
}
}
export const { createCronJob, createCronJobGroup } = createCronJobFunctions({
logger: new WinstonCronJobLogger(),
});

View File

@@ -4,6 +4,7 @@ import "./undici-log-agent-override";
import { jobs } from "./jobs";
import { seedServerSettingsAsync } from "./seed-server-settings";
jobs.startAll();
void seedServerSettingsAsync();
void (async () => {
await jobs.startAllAsync();
await seedServerSettingsAsync();
})();