diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b7209b6..8a7000d 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -10,6 +10,10 @@ on: - "CONTRIBUTING.md" - "LICENSE.md" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build-and-test: runs-on: ubuntu-latest diff --git a/packages/dashboard/src/backend-driver.ts b/packages/dashboard/src/backend-driver.ts deleted file mode 100644 index 427a302..0000000 --- a/packages/dashboard/src/backend-driver.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Backend, BackendConfig, createBackendFromDriver } from "@sidequest/backend"; - -let backend: Backend; - -export async function initBackend(config: BackendConfig) { - backend = await createBackendFromDriver(config); -} - -export function getBackend() { - if (!backend) { - throw new Error("Backend not initialized!"); - } - return backend; -} diff --git a/packages/dashboard/src/index.ts b/packages/dashboard/src/index.ts index aac7eb1..3f02ca2 100644 --- a/packages/dashboard/src/index.ts +++ b/packages/dashboard/src/index.ts @@ -1,3 +1,4 @@ +import { Backend, createBackendFromDriver } from "@sidequest/backend"; import { logger } from "@sidequest/core"; import express from "express"; import basicAuth from "express-basic-auth"; @@ -5,22 +6,87 @@ import expressLayouts from "express-ejs-layouts"; import morgan from "morgan"; import { Server } from "node:http"; import path from "node:path"; -import { initBackend } from "./backend-driver"; import { DashboardConfig } from "./config"; -import dashboardRouter from "./resources/dashboard"; -import jobsRouter from "./resources/jobs"; -import queuesRouter from "./resources/queues"; +import { createDashboardRouter } from "./resources/dashboard"; +import { createJobsRouter } from "./resources/jobs"; +import { createQueuesRouter } from "./resources/queues"; +/** + * A dashboard server for monitoring and managing Sidequest jobs and queues. + * + * The SidequestDashboard class provides a web-based interface for viewing job status, + * queue information, and other Sidequest-related data. It sets up an Express.js server + * with EJS templating, optional basic authentication, and connects to a configurable backend. + * + * @example + * ```typescript + * const dashboard = new SidequestDashboard(); + * await dashboard.start({ + * port: 3000, + * enabled: true, + * auth: { + * user: 'admin', + * password: 'secret' + * }, + * backendConfig: { + * driver: '@sidequest/sqlite-backend' + * } + * }); + * ``` + */ export class SidequestDashboard { - private app: express.Express; + /** + * The Express application instance used by the dashboard server. + * This property is optional and may be undefined if the server has not been initialized. + */ + private app?: express.Express; + /** + * Optional dashboard configuration object that defines the behavior and appearance + * of the dashboard component. If not provided, default configuration will be used. + */ private config?: DashboardConfig; + /** + * The HTTP server instance used by the dashboard application. + * This server handles incoming requests and serves the dashboard interface. + * Will be undefined until the server is started. + */ private server?: Server; + /** + * Backend instance used for server communication and data operations. + * When undefined, indicates that no backend connection has been established. + */ + private backend?: Backend; constructor() { this.app = express(); } + /** + * Starts the dashboard server with the provided configuration. + * + * @param config - Optional dashboard configuration object. If not provided, default values will be used. + * @returns A promise that resolves when the server setup is complete. + * + * @remarks + * - If the server is already running, a warning is logged and the method returns early + * - If the dashboard is disabled in config, a debug message is logged and the method returns early + * - The method sets up the Express app, backend driver, middlewares, authentication, EJS templating, routes, and starts listening + * + * @example + * ```typescript + * await dashboard.start({ + * enabled: true, + * port: 3000, + * backendConfig: { driver: "@sidequest/sqlite-backend" } + * }); + * ``` + */ async start(config?: DashboardConfig) { + if (this.server?.listening) { + logger("Dashboard").warn("Dashboard is already running. Please stop it before starting again."); + return; + } + this.config = { enabled: true, backendConfig: { @@ -35,7 +101,8 @@ export class SidequestDashboard { return; } - await initBackend(this.config.backendConfig!); + this.app ??= express(); + this.backend = await createBackendFromDriver(this.config.backendConfig!); this.setupMiddlewares(); this.setupAuth(); @@ -45,6 +112,17 @@ export class SidequestDashboard { this.listen(); } + /** + * Sets up middleware for the Express application. + * + * Configures HTTP request logging using Morgan middleware when debug logging is enabled. + * The middleware uses the "combined" format for comprehensive request logging. + * + * @remarks + * - Only adds Morgan logging middleware when debug mode is active + * - Uses Apache combined log format for detailed request information + * - Logs are handled through the Dashboard logger instance + */ setupMiddlewares() { logger("Dashboard").debug(`Setting up Middlewares`); if (logger().isDebugEnabled()) { @@ -52,13 +130,31 @@ export class SidequestDashboard { } } - setupAuth(config?: DashboardConfig) { - if (config?.auth) { - const auth = config.auth; + /** + * Sets up basic authentication for the dashboard application. + * + * If authentication configuration is provided, this method configures + * HTTP Basic Authentication middleware using the specified username and password. + * The middleware will challenge unauthorized requests with a 401 response. + * + * @remarks + * - Only sets up authentication if `this.config.auth` is defined + * - Uses a single user/password combination from the configuration + * - Enables challenge mode to prompt for credentials in browsers + * + * @example + * ```typescript + * // Assuming config.auth = { user: "admin", password: "secret" } + * dashboard.setupAuth(); // Sets up basic auth for user "admin" + * ``` + */ + setupAuth() { + if (this.config!.auth) { + const auth = this.config!.auth; logger("Dashboard").debug(`Basic auth setup with User: ${auth.user}`); const users = {}; users[auth.user] = auth.password; - this.app?.use( + this.app!.use( basicAuth({ users: users, challenge: true, @@ -67,26 +163,56 @@ export class SidequestDashboard { } } + /** + * Sets up EJS templating engine for the dashboard application. + * This method configures the Express application to use EJS as the view engine, + * sets the views directory, and specifies the layout file. + * + * @remarks + * - Uses `express-ejs-layouts` for layout support + * - Sets the views directory to the `views` folder within the package + * - Serves static files from the `public` directory + * - Ensures that the EJS engine is ready to render views with layouts + */ setupEJS() { logger("Dashboard").debug(`Setting up EJS`); - this.app?.use(expressLayouts); - this.app?.set("view engine", "ejs"); - this.app?.set("views", path.join(import.meta.dirname, "views")); - this.app?.set("layout", path.join(import.meta.dirname, "views", "layout")); - this.app?.use("/public", express.static(path.join(import.meta.dirname, "public"))); + this.app!.use(expressLayouts); + this.app!.set("view engine", "ejs"); + this.app!.set("views", path.join(import.meta.dirname, "views")); + this.app!.set("layout", path.join(import.meta.dirname, "views", "layout")); + this.app!.use("/public", express.static(path.join(import.meta.dirname, "public"))); } + /** + * Sets up the main application routes for the dashboard. + * + * This method initializes and attaches the dashboard, jobs, and queues routers + * to the Express application instance. It also logs the setup process for debugging purposes. + * + * @remarks + * - Assumes that `this.app` and `this.backend` are initialized. + * - Uses the routers created by `createDashboardRouter`, `createJobsRouter`, and `createQueuesRouter`. + */ setupRoutes() { logger("Dashboard").debug(`Setting up routes`); - this.app?.use("/", dashboardRouter); - this.app?.use("/jobs", jobsRouter); - this.app?.use("/queues", queuesRouter); + this.app!.use(...createDashboardRouter(this.backend!)); + this.app!.use(...createJobsRouter(this.backend!)); + this.app!.use(...createQueuesRouter(this.backend!)); } + /** + * Starts the dashboard server on the configured port. + * Logs the startup process and handles any errors that occur during server initialization. + * + * @remarks + * If no port is specified in the configuration, the default port 8678 is used. + * + * @returns void + */ listen() { - const port = this.config?.port ?? 8678; + const port = this.config!.port ?? 8678; logger("Dashboard").debug(`Starting Dashboard with port ${port}`); - this.server = this.app?.listen(port, (error) => { + this.server = this.app!.listen(port, (error) => { if (error) { logger("Dashboard").error("Failed to start Sidequest Dashboard!", error); } else { @@ -95,12 +221,31 @@ export class SidequestDashboard { }); } - close() { - this.server?.close(() => { - logger("Dashboard").info("Sidequest Dashboard stopped"); + /** + * Closes the dashboard by shutting down the backend and server, + * and cleaning up associated resources. + * + * - Awaits the closure of the backend if it exists. + * - Closes the server and logs a message when stopped. + * - Resets backend, server, config, and app properties to `undefined`. + * - Logs a debug message indicating resources have been cleaned up. + * + * @returns {Promise} Resolves when all resources have been closed and cleaned up. + */ + async close() { + await this.backend?.close(); + await new Promise((resolve) => { + this.server?.close(() => { + logger("Dashboard").info("Sidequest Dashboard stopped"); + resolve(); + }); }); + + this.backend = undefined; this.server = undefined; this.config = undefined; + this.app = undefined; + logger("Dashboard").debug("Dashboard resources cleaned up"); } } diff --git a/packages/dashboard/src/resources/dashboard.ts b/packages/dashboard/src/resources/dashboard.ts index 688f710..9c84061 100644 --- a/packages/dashboard/src/resources/dashboard.ts +++ b/packages/dashboard/src/resources/dashboard.ts @@ -1,58 +1,57 @@ +import { Backend } from "@sidequest/backend"; import { Router } from "express"; -import { getBackend } from "../backend-driver"; - -const dashboardRouter = Router(); - -function rangeToMs(range: string | undefined) { - let rangeMs: number; - switch (range) { - case "12h": - rangeMs = 12 * 60 * 60 * 1000; - break; - case "12d": - rangeMs = 12 * 24 * 60 * 60 * 1000; - break; - case "12m": - default: - // Defaults to 12m - rangeMs = 12 * 60 * 1000; - break; - } - return rangeMs; -} +export function createDashboardRouter(backend: Backend) { + const dashboardRouter = Router(); + + function rangeToMs(range: string | undefined) { + let rangeMs: number; + switch (range) { + case "12h": + rangeMs = 12 * 60 * 60 * 1000; + break; + case "12d": + rangeMs = 12 * 24 * 60 * 60 * 1000; + break; + case "12m": + default: + // Defaults to 12m + rangeMs = 12 * 60 * 1000; + break; + } + + return rangeMs; + } -dashboardRouter.get("/", async (req, res) => { - const { range = "12m" } = req.query; - const from = new Date(Date.now() - rangeToMs(range as string)); - const backend = getBackend(); - const jobs = await backend.countJobs({ from }); + dashboardRouter.get("/", async (req, res) => { + const { range = "12m" } = req.query; + const from = new Date(Date.now() - rangeToMs(range as string)); + const jobs = await backend.countJobs({ from }); - res.render("pages/index", { - title: "Sidequest Dashboard", - stats: jobs, + res.render("pages/index", { + title: "Sidequest Dashboard", + stats: jobs, + }); }); -}); -dashboardRouter.get("/dashboard/stats", async (req, res) => { - const { range = "12m" } = req.query; - const from = new Date(Date.now() - rangeToMs(range as string)); - const backend = getBackend(); - const jobs = await backend.countJobs({ from }); + dashboardRouter.get("/dashboard/stats", async (req, res) => { + const { range = "12m" } = req.query; + const from = new Date(Date.now() - rangeToMs(range as string)); + const jobs = await backend.countJobs({ from }); - res.render("partials/dashboard-stats", { - stats: jobs, - layout: false, + res.render("partials/dashboard-stats", { + stats: jobs, + layout: false, + }); }); -}); -dashboardRouter.get("/dashboard/graph-data", async (req, res) => { - const { range = "12m" } = req.query; + dashboardRouter.get("/dashboard/graph-data", async (req, res) => { + const { range = "12m" } = req.query; - const backend = getBackend(); - const jobs = await backend.countJobsOverTime(range as string); + const jobs = await backend.countJobsOverTime(range as string); - res.json(jobs).end(); -}); + res.json(jobs).end(); + }); -export default dashboardRouter; + return ["/", dashboardRouter] as const; +} diff --git a/packages/dashboard/src/resources/jobs.ts b/packages/dashboard/src/resources/jobs.ts index 4b0b0bb..6c492b4 100644 --- a/packages/dashboard/src/resources/jobs.ts +++ b/packages/dashboard/src/resources/jobs.ts @@ -1,186 +1,180 @@ +import { Backend } from "@sidequest/backend"; import { JobState } from "@sidequest/core"; import { Router } from "express"; -import { getBackend } from "../backend-driver"; - -const jobsRouter = Router(); - -jobsRouter.get("/", async (req, res) => { - const { status, start, end, queue, class: jobClass } = req.query; - const backend = getBackend(); - - const time = typeof req.query.time === "string" && req.query.time.trim() ? req.query.time : "any"; - - const pageSize = req.query.pageSize ? parseInt(req.query.pageSize as string, 10) : 30; - const page = req.query.page ? Math.max(parseInt(req.query.page as string, 10), 1) : 1; - const offset = (page - 1) * pageSize; - - const filters: { - queue?: string; - jobClass?: string; - state?: JobState; - limit?: number; - offset?: number; - args?: unknown[]; - timeRange?: { - from?: Date; - to?: Date; + +export function createJobsRouter(backend: Backend) { + const jobsRouter = Router(); + + jobsRouter.get("/", async (req, res) => { + const { status, start, end, queue, class: jobClass } = req.query; + + const time = typeof req.query.time === "string" && req.query.time.trim() ? req.query.time : "any"; + + const pageSize = req.query.pageSize ? parseInt(req.query.pageSize as string, 10) : 30; + const page = req.query.page ? Math.max(parseInt(req.query.page as string, 10), 1) : 1; + const offset = (page - 1) * pageSize; + + const filters: { + queue?: string; + jobClass?: string; + state?: JobState; + limit?: number; + offset?: number; + args?: unknown[]; + timeRange?: { + from?: Date; + to?: Date; + }; + } = { + limit: pageSize, + offset: offset, + queue: typeof queue === "string" && queue.trim() ? queue : undefined, + jobClass: typeof jobClass === "string" && jobClass.trim() ? jobClass : undefined, + state: status as JobState, }; - } = { - limit: pageSize, - offset: offset, - queue: typeof queue === "string" && queue.trim() ? queue : undefined, - jobClass: typeof jobClass === "string" && jobClass.trim() ? jobClass : undefined, - state: status as JobState, - }; - - filters.timeRange = computeTimeRange(time, start, end); - - const timeRangeStrings = filters.timeRange - ? { - from: filters.timeRange.from!.toISOString(), - to: (filters.timeRange.to ?? new Date()).toISOString(), - } - : undefined; - - const [jobs, queues, nextPageJobs] = await Promise.all([ - backend?.listJobs(filters), - backend?.getQueuesFromJobs(), - backend?.listJobs({ ...filters, limit: 1, offset: page * pageSize }), - ]); - - const isHtmx = req.get("hx-request"); - - if (isHtmx) { - res.render("partials/jobs-table", { - jobs, - pagination: { - page, - pageSize, - hasNextPage: nextPageJobs.length > 0, - }, - layout: false, - }); - } else { - res.render("pages/jobs", { - title: "Jobs", - jobs, - queues, - filters: { - status: status ?? "", - time: time ?? "", - queue: queue ?? "", - class: jobClass ?? "", - start: start ?? timeRangeStrings?.from ?? "", - end: end ?? timeRangeStrings?.to ?? "", - }, - pagination: { - page, - pageSize, - hasNextPage: nextPageJobs.length > 0, - }, - }); - } -}); -jobsRouter.get("/:id", async (req, res) => { - const backend = getBackend(); - const jobId = parseInt(req.params.id); - const job = await backend?.getJob(jobId); + filters.timeRange = computeTimeRange(time, start, end); + + const timeRangeStrings = filters.timeRange + ? { + from: filters.timeRange.from!.toISOString(), + to: (filters.timeRange.to ?? new Date()).toISOString(), + } + : undefined; - const isHtmx = req.get("hx-request"); + const [jobs, queues, nextPageJobs] = await Promise.all([ + backend?.listJobs(filters), + backend?.getQueuesFromJobs(), + backend?.listJobs({ ...filters, limit: 1, offset: page * pageSize }), + ]); + + const isHtmx = req.get("hx-request"); - if (job) { if (isHtmx) { - res.render("partials/job-view", { - title: `Job #${job.id}`, - job, + res.render("partials/jobs-table", { + jobs, + pagination: { + page, + pageSize, + hasNextPage: nextPageJobs.length > 0, + }, layout: false, }); } else { - res.render("pages/job", { - title: `Job #${job.id}`, - job, + res.render("pages/jobs", { + title: "Jobs", + jobs, + queues, + filters: { + status: status ?? "", + time: time ?? "", + queue: queue ?? "", + class: jobClass ?? "", + start: start ?? timeRangeStrings?.from ?? "", + end: end ?? timeRangeStrings?.to ?? "", + }, + pagination: { + page, + pageSize, + hasNextPage: nextPageJobs.length > 0, + }, }); } - } else { - res.status(404).send("Job not found!"); - } -}); - -jobsRouter.patch("/:id/run", async (req, res) => { - const backend = getBackend(); - - const jobId = parseInt(req.params.id); - const job = await backend?.getJob(jobId); + }); + + jobsRouter.get("/:id", async (req, res) => { + const jobId = parseInt(req.params.id); + const job = await backend?.getJob(jobId); + + const isHtmx = req.get("hx-request"); + + if (job) { + if (isHtmx) { + res.render("partials/job-view", { + title: `Job #${job.id}`, + job, + layout: false, + }); + } else { + res.render("pages/job", { + title: `Job #${job.id}`, + job, + }); + } + } else { + res.status(404).send("Job not found!"); + } + }); - if (job) { - await backend.updateJob({ id: job.id, available_at: new Date() }); - res.header("HX-Trigger", "jobChanged").status(200).end(); - } else { - res.status(404).end(); - } -}); + jobsRouter.patch("/:id/run", async (req, res) => { + const jobId = parseInt(req.params.id); + const job = await backend?.getJob(jobId); -jobsRouter.patch("/:id/cancel", async (req, res) => { - const backend = getBackend(); + if (job) { + await backend.updateJob({ id: job.id, available_at: new Date() }); + res.header("HX-Trigger", "jobChanged").status(200).end(); + } else { + res.status(404).end(); + } + }); - const jobId = parseInt(req.params.id); - const job = await backend?.getJob(jobId); + jobsRouter.patch("/:id/cancel", async (req, res) => { + const jobId = parseInt(req.params.id); + const job = await backend?.getJob(jobId); - if (job) { - await backend.updateJob({ ...job, state: "canceled" }); - res.header("HX-Trigger", "jobChanged").status(200).end(); - } else { - res.status(404).end(); - } -}); + if (job) { + await backend.updateJob({ ...job, state: "canceled" }); + res.header("HX-Trigger", "jobChanged").status(200).end(); + } else { + res.status(404).end(); + } + }); -jobsRouter.patch("/:id/rerun", async (req, res) => { - const backend = getBackend(); + jobsRouter.patch("/:id/rerun", async (req, res) => { + const jobId = parseInt(req.params.id); + const job = await backend?.getJob(jobId); - const jobId = parseInt(req.params.id); - const job = await backend?.getJob(jobId); + if (job) { + const maxAttempts = job.max_attempts === job.attempt ? job.max_attempts + 1 : job.max_attempts; + await backend.updateJob({ ...job, state: "waiting", max_attempts: maxAttempts }); + res.header("HX-Trigger", "jobChanged").status(200).end(); + } else { + res.status(404).end(); + } + }); + + function computeTimeRange(time?: unknown, start?: unknown, end?: unknown) { + if (typeof time !== "string") return undefined; + + const now = Date.now(); + + const minutesMap: Record = { + "5m": 5, + "15m": 15, + "30m": 30, + "1h": 60, + "4h": 240, + "12h": 720, + "24h": 1440, + "2d": 2880, + "7d": 10080, + "30d": 43200, + }; - if (job) { - const maxAttempts = job.max_attempts === job.attempt ? job.max_attempts + 1 : job.max_attempts; - await backend.updateJob({ ...job, state: "waiting", max_attempts: maxAttempts }); - res.header("HX-Trigger", "jobChanged").status(200).end(); - } else { - res.status(404).end(); - } -}); - -function computeTimeRange(time?: unknown, start?: unknown, end?: unknown) { - if (typeof time !== "string") return undefined; - - const now = Date.now(); - - const minutesMap: Record = { - "5m": 5, - "15m": 15, - "30m": 30, - "1h": 60, - "4h": 240, - "12h": 720, - "24h": 1440, - "2d": 2880, - "7d": 10080, - "30d": 43200, - }; - - if (minutesMap[time]) { - return { from: new Date(now - minutesMap[time] * 60_000) }; - } + if (minutesMap[time]) { + return { from: new Date(now - minutesMap[time] * 60_000) }; + } - if (time === "custom" && typeof start === "string" && typeof end === "string") { - const fromDate = new Date(start); - const toDate = new Date(end); - if (!isNaN(fromDate.getTime()) && !isNaN(toDate.getTime())) { - return { from: fromDate, to: toDate }; + if (time === "custom" && typeof start === "string" && typeof end === "string") { + const fromDate = new Date(start); + const toDate = new Date(end); + if (!isNaN(fromDate.getTime()) && !isNaN(toDate.getTime())) { + return { from: fromDate, to: toDate }; + } } + + return undefined; } - return undefined; + return ["/jobs", jobsRouter] as const; } - -export default jobsRouter; diff --git a/packages/dashboard/src/resources/queues.ts b/packages/dashboard/src/resources/queues.ts index e6ed812..8107c53 100644 --- a/packages/dashboard/src/resources/queues.ts +++ b/packages/dashboard/src/resources/queues.ts @@ -1,6 +1,5 @@ import { Backend } from "@sidequest/backend"; import { Request, Response, Router } from "express"; -import { getBackend } from "../backend-driver"; export async function renderQueuesTable(backend: Backend, req: Request, res: Response) { const queues = await backend.listQueues({ column: "name", order: "asc" }); @@ -18,22 +17,22 @@ export async function renderQueuesTable(backend: Backend, req: Request, res: Res } } -const queuesRouter = Router(); +export function createQueuesRouter(backend: Backend) { + const queuesRouter = Router(); -queuesRouter.get("/", async (req, res) => { - const backend = getBackend(); - await renderQueuesTable(backend, req, res); -}); + queuesRouter.get("/", async (req, res) => { + await renderQueuesTable(backend, req, res); + }); -queuesRouter.patch("/:name/toggle", async (req, res) => { - const backend = getBackend(); - const queue = await backend.getQueue(req.params.name); - if (queue) { - await backend.updateQueue({ ...queue, state: queue.state === "active" ? "paused" : "active" }); - res.header("HX-Trigger", "toggleQueue").status(200).end(); - } else { - res.status(404).end(); - } -}); + queuesRouter.patch("/:name/toggle", async (req, res) => { + const queue = await backend.getQueue(req.params.name); + if (queue) { + await backend.updateQueue({ ...queue, state: queue.state === "active" ? "paused" : "active" }); + res.header("HX-Trigger", "toggleQueue").status(200).end(); + } else { + res.status(404).end(); + } + }); -export default queuesRouter; + return ["/queues", queuesRouter] as const; +} diff --git a/packages/sidequest/src/operations/job.ts b/packages/sidequest/src/operations/job.ts index 92cd4da..6a347da 100644 --- a/packages/sidequest/src/operations/job.ts +++ b/packages/sidequest/src/operations/job.ts @@ -14,7 +14,7 @@ export class JobOperations { * @returns The backend instance. * @throws Error if the engine is not configured. */ - private backend: Backend | undefined; + private backend?: Backend; /** * Singleton instance of JobOperations. @@ -36,7 +36,7 @@ export class JobOperations { * * @param backend - The backend instance to set */ - public setBackend(backend: Backend) { + public setBackend(backend: Backend | undefined) { this.backend = backend; } diff --git a/packages/sidequest/src/operations/queue.ts b/packages/sidequest/src/operations/queue.ts index a2b4277..cb9c1f3 100644 --- a/packages/sidequest/src/operations/queue.ts +++ b/packages/sidequest/src/operations/queue.ts @@ -13,7 +13,7 @@ export class QueueOperations { * @returns The backend instance. * @throws Error if the engine is not configured. */ - private backend: Backend | undefined; + private backend?: Backend; /** * Singleton instance of QueueOperations. @@ -35,7 +35,7 @@ export class QueueOperations { * * @param backend - The backend instance to set */ - public setBackend(backend: Backend) { + public setBackend(backend: Backend | undefined) { this.backend = backend; } diff --git a/packages/sidequest/src/operations/sidequest.ts b/packages/sidequest/src/operations/sidequest.ts index ad488d0..71b2b30 100644 --- a/packages/sidequest/src/operations/sidequest.ts +++ b/packages/sidequest/src/operations/sidequest.ts @@ -79,8 +79,8 @@ export class Sidequest { */ static async configure(config?: EngineConfig) { const _config = await this.engine.configure(config); - this.job.setBackend(this.engine.getBackend()!); - this.queue.setBackend(this.engine.getBackend()!); + this.job.setBackend(this.engine.getBackend()); + this.queue.setBackend(this.engine.getBackend()); return _config; } @@ -113,9 +113,22 @@ export class Sidequest { await dashboard; } + /** + * Stops the SideQuest instance by closing all active components. + * + * This method performs cleanup operations including: + * - Closing the engine + * - Clearing the job backend + * - Clearing the queue backend + * - Closing the dashboard + * + * @returns A promise that resolves when all cleanup operations are complete + */ static async stop() { await this.engine.close(); - this.dashboard.close(); + this.job.setBackend(undefined); + this.queue.setBackend(undefined); + await this.dashboard.close(); } /**