convert job queue and job queue helper

This commit is contained in:
Alex Holliday
2026-01-15 22:06:46 +00:00
parent 5a7b51bfec
commit cf67c82f5e
5 changed files with 149 additions and 26 deletions
+10 -1
View File
@@ -1,5 +1,14 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { pathsToModuleNameMapper } from "ts-jest";
import type { Config } from "jest";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const tsconfigPath = path.resolve(__dirname, "./tsconfig.json");
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf-8"));
const config: Config = {
rootDir: ".",
testEnvironment: "node",
@@ -8,7 +17,7 @@ const config: Config = {
"^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }],
},
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
...pathsToModuleNameMapper(tsconfig.compilerOptions.paths || {}, { prefix: "<rootDir>/" }),
},
testMatch: ["<rootDir>/test/**/*.test.ts"],
setupFilesAfterEnv: [],
@@ -1,18 +1,30 @@
const SERVICE_NAME = "JobQueueHelper";
import type { Monitor } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js";
import { error } from "winston";
class SuperSimpleQueueHelper {
static SERVICE_NAME = SERVICE_NAME;
/**
* @param {{
* db: import("../database.js").Database,
* logger: import("../logger.js").Logger,
* networkService: import("../networkService.js").NetworkService,
* statusService: import("../statusService.js").StatusService,
* notificationService: import("../notificationService.js").NotificationService
* }}
*/
constructor({ db, logger, networkService, statusService, notificationService }) {
private db: any;
private logger: any;
private networkService: any;
private statusService: any;
private notificationService: any;
constructor({
db,
logger,
networkService,
statusService,
notificationService,
}: {
db: any;
logger: any;
networkService: any;
statusService: any;
notificationService: any;
}) {
this.db = db;
this.logger = logger;
this.networkService = networkService;
@@ -25,39 +37,41 @@ class SuperSimpleQueueHelper {
}
getMonitorJob = () => {
return async (monitor) => {
return async (monitor: Monitor) => {
try {
const monitorId = monitor.id;
const teamId = monitor.teamId;
if (!monitorId) {
throw new Error("No monitor id");
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getMonitorJob" });
}
// Step 1. Check for maintenacne window, if found, skip the check
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
if (maintenanceWindowActive) {
this.logger.info({
this.logger.debug({
message: `Monitor ${monitorId} is in maintenance window`,
service: SERVICE_NAME,
method: "getMonitorJob",
});
return;
}
const networkResponse = await this.networkService.requestStatus(monitor);
if (!networkResponse) {
// Step 2. Request monitor status
const status = await this.networkService.requestStatus(monitor);
if (!status) {
throw new Error("No network response");
}
const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse);
const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(status);
this.notificationService
.handleNotifications({
...networkResponse,
...status,
monitor: updatedMonitor,
prevStatus,
statusChanged,
})
.catch((error) => {
.catch((error: any) => {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
@@ -66,7 +80,7 @@ class SuperSimpleQueueHelper {
stack: error.stack,
});
});
} catch (error) {
} catch (error: any) {
this.logger.warn({
message: error.message,
service: error.service || SERVICE_NAME,
@@ -78,13 +92,13 @@ class SuperSimpleQueueHelper {
};
};
async isInMaintenanceWindow(monitorId, teamId) {
async isInMaintenanceWindow(monitorId: string, teamId: string) {
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({
monitorId: monitorId.toString(),
teamId: teamId.toString(),
monitorId: monitorId,
teamId: teamId,
});
// Check for active maintenance window:
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => {
if (window.active) {
const start = new Date(window.start);
const end = new Date(window.end);
@@ -0,0 +1,99 @@
import { describe, expect, it, jest } from "@jest/globals";
import SuperSimpleQueueHelper from "../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts";
import type { Monitor } from "../src/types/monitor.ts";
const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn() });
const createHelper = (overrides?: Partial<ConstructorParameters<typeof SuperSimpleQueueHelper>[0]>) => {
const maintenanceWindowModule = {
getMaintenanceWindowsByMonitorId: jest.fn().mockResolvedValue([]),
};
const helper = new SuperSimpleQueueHelper({
db: { maintenanceWindowModule },
logger: createLogger(),
networkService: { requestStatus: jest.fn() },
statusService: { updateStatus: jest.fn() },
notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) },
...overrides,
});
return { helper, maintenanceWindowModule };
};
describe("SuperSimpleQueueHelper", () => {
describe("getMonitorJob", () => {
it("skips execution when monitor is in maintenance window", async () => {
const { helper } = createHelper();
const spy = jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(true);
const job = helper.getMonitorJob();
await job({ id: "m1", teamId: "team", interval: 60000 } as Monitor);
expect(helper["networkService"].requestStatus).not.toHaveBeenCalled();
expect(helper["logger"].info).toHaveBeenCalledWith(
expect.objectContaining({ message: expect.stringContaining("Monitor m1 is in maintenance window") })
);
spy.mockRestore();
});
it("processes monitor status and notifications when active", async () => {
const networkResponse = { monitor: { id: "m1" }, status: true };
const updatedMonitor = { id: "m1", status: true };
const { helper } = createHelper({
networkService: { requestStatus: jest.fn().mockResolvedValue(networkResponse) },
statusService: {
updateStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }),
},
notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) },
});
jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(false);
const job = helper.getMonitorJob();
const monitor = { id: "m1", teamId: "team" } as Monitor;
await job(monitor);
expect(helper["networkService"].requestStatus).toHaveBeenCalledWith(monitor);
expect(helper["statusService"].updateStatus).toHaveBeenCalledWith(networkResponse);
expect(helper["notificationService"].handleNotifications).toHaveBeenCalledWith(
expect.objectContaining({ monitor: updatedMonitor, statusChanged: true, prevStatus: false })
);
});
it("throws when monitor id is missing", async () => {
const { helper } = createHelper();
const job = helper.getMonitorJob();
await expect(job({} as Monitor)).rejects.toThrow("No monitor id");
expect(helper["logger"].warn).toHaveBeenCalled();
});
});
describe("isInMaintenanceWindow", () => {
it("returns true when an active window spans now", async () => {
const now = new Date();
const { helper, maintenanceWindowModule } = createHelper();
maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([
{
active: true,
start: new Date(now.getTime() - 1000).toISOString(),
end: new Date(now.getTime() + 1000).toISOString(),
repeat: 0,
},
]);
await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(true);
});
it("returns true when repeat interval advances window into current time", async () => {
const now = Date.now();
const { helper, maintenanceWindowModule } = createHelper();
maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([
{
active: true,
start: new Date(now - 7200000).toISOString(),
end: new Date(now - 6600000).toISOString(),
repeat: 3600000,
},
]);
await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(true);
});
it("returns false when no active windows exist", async () => {
const { helper } = createHelper();
await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(false);
});
});
});
+2 -1
View File
@@ -3,7 +3,8 @@
"compilerOptions": {
"rootDir": ".",
"types": ["jest"],
"noEmit": true
"noEmit": true,
"resolveJsonModule": true
},
"include": ["src", "test"]
}
+1 -1
View File
@@ -4,7 +4,7 @@
"allowJs": true,
"checkJs": false,
"paths": {
"@/*": ["./src/*"] // allows "@/db" -> "./src/db"
"@/*": ["./src/*"]
},
"outDir": "./dist",
"strict": true,