Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"sidequest": "workspace:*"
},
"devDependencies": {
"@types/express": "^5",
"typescript": "^5.8.3"
"@types/express": "^5.0.3",
"typescript": "^5.9.2"
}
}
46 changes: 23 additions & 23 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,46 +45,46 @@
"devDependencies": {
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@eslint/js": "^9.29.0",
"@rollup/plugin-commonjs": "^28.0.5",
"@eslint/js": "^9.35.0",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-replace": "^6.0.2",
"@rollup/plugin-typescript": "^12.1.2",
"@rollup/plugin-typescript": "^12.1.4",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/exec": "^7.1.0",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^11.0.3",
"@semantic-release/release-notes-generator": "^14.0.3",
"@tailwindcss/cli": "^4.1.10",
"@tailwindcss/postcss": "^4.1.10",
"@tsconfig/recommended": "^1.0.8",
"@semantic-release/github": "^11.0.5",
"@semantic-release/release-notes-generator": "^14.1.0",
"@tailwindcss/cli": "^4.1.13",
"@tailwindcss/postcss": "^4.1.13",
"@tsconfig/recommended": "^1.0.10",
"@types/express": "^5.0.3",
"@types/node": "^24.0.3",
"@types/node": "^24.3.1",
"@vitest/coverage-v8": "3.2.4",
"cross-env": "^7.0.3",
"eslint": "^9.29.0",
"globals": "^16.2.0",
"cross-env": "^10.0.0",
"eslint": "^9.35.0",
"globals": "^16.3.0",
"husky": "^9.1.7",
"postcss": "^8.5.5",
"prettier": "^3.5.3",
"prettier-plugin-organize-imports": "^4.1.0",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.2.0",
"rimraf": "^6.0.1",
"rollup": "^4.43.0",
"rollup": "^4.50.0",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-copy-watch": "^0.0.1",
"rollup-plugin-delete": "^3.0.1",
"rollup-plugin-dts": "^6.2.1",
"rollup-plugin-dts": "^6.2.3",
"rollup-plugin-postcss": "^4.0.2",
"semantic-release": "^24.2.7",
"tailwindcss": "^4.1.10",
"tsx": "^4.19.4",
"tailwindcss": "^4.1.13",
"tsx": "^4.20.5",
"turbo": "^2.5.6",
"typescript": "^5.8.3",
"typescript-eslint": "^8.34.0",
"vitepress": "^1.6.3",
"vitest": "^3.2.3"
"typescript": "^5.9.2",
"typescript-eslint": "^8.42.0",
"vitepress": "^1.6.4",
"vitest": "^3.2.4"
},
"packageManager": "yarn@4.9.4"
}
2 changes: 1 addition & 1 deletion packages/backends/mongo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"dependencies": {
"@sidequest/backend": "workspace:*",
"@sidequest/core": "workspace:*",
"mongodb": "^6.17.0"
"mongodb": "^6.19.0"
},
"devDependencies": {
"@sidequest/backend-test": "workspace:*"
Expand Down
2 changes: 1 addition & 1 deletion packages/backends/mysql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@sidequest/backend": "workspace:*",
"@sidequest/core": "workspace:*",
"knex": "^3.1.0",
"mysql2": "^3.14.2"
"mysql2": "^3.14.4"
},
"devDependencies": {
"@sidequest/backend-test": "workspace:*"
Expand Down
2 changes: 1 addition & 1 deletion packages/backends/postgres/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@sidequest/backend": "workspace:*",
"@sidequest/core": "workspace:*",
"knex": "^3.1.0",
"pg": "^8.15.6"
"pg": "^8.16.3"
},
"devDependencies": {
"@sidequest/backend-test": "workspace:*"
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@
"dependencies": {
"@sidequest/backend": "workspace:*",
"commander": "^14.0.0",
"inquirer": "^12.7.0"
"inquirer": "^12.9.4"
}
}
10 changes: 5 additions & 5 deletions packages/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@
"express": "^5.1.0",
"express-basic-auth": "^1.2.1",
"express-ejs-layouts": "^2.5.1",
"morgan": "^1.10.0"
"morgan": "^1.10.1"
},
"devDependencies": {
"@highlightjs/cdn-assets": "^11.11.1",
"@types/feather-icons": "^4",
"@types/morgan": "^1",
"@types/feather-icons": "^4.29.4",
"@types/morgan": "^1.9.10",
"chart.js": "^4.5.0",
"daisyui": "^5.0.43",
"daisyui": "^5.1.7",
"feather-icons": "^4.29.2",
"htmx.org": "^2.0.4"
"htmx.org": "^2.0.6"
}
}
4 changes: 2 additions & 2 deletions packages/engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
"dependencies": {
"@sidequest/backend": "workspace:*",
"@sidequest/core": "workspace:*",
"node-cron": "^4.1.1",
"piscina": "^5.1.2"
"node-cron": "^4.2.1",
"piscina": "^5.1.3"
},
"peerDependencies": {
"sidequest": "workspace:*"
Expand Down
5 changes: 5 additions & 0 deletions packages/engine/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cpus } from "os";
import { inspect } from "util";
import { DEFAULT_WORKER_PATH } from "./constants";
import { JOB_BUILDER_FALLBACK } from "./job/constants";
import { ScheduledJobRegistry } from "./job/cron-registry";
import { JobBuilder, JobBuilderDefaults } from "./job/job-builder";
import { grantQueueConfig, QueueDefaults } from "./queue/grant-queue-config";
import { clearGracefulShutdown, gracefulShutdown } from "./utils/shutdown";
Expand Down Expand Up @@ -253,6 +254,10 @@ export class Engine {
if (!this.shuttingDown) {
this.shuttingDown = true;
logger("Engine").debug("Closing Sidequest engine...");

// Stop all scheduled cron jobs first
await ScheduledJobRegistry.stopAll();

if (this.mainWorker) {
const promise = new Promise((resolve) => {
this.mainWorker!.on("exit", resolve);
Expand Down
208 changes: 208 additions & 0 deletions packages/engine/src/job/cron-registry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { ScheduledTask } from "node-cron";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ScheduledJobRegistry } from "./cron-registry";

describe("ScheduledJobRegistry", () => {
let mockTask: ScheduledTask;
let stopSpy: ReturnType<typeof vi.fn>;

beforeEach(() => {
// Create a mock ScheduledTask
stopSpy = vi.fn().mockResolvedValue(undefined);
mockTask = {
id: "test-task-1",
stop: stopSpy,
getStatus: vi.fn().mockReturnValue("scheduled"),
} as unknown as ScheduledTask;
});

afterEach(async () => {
// Clean up after each test
await ScheduledJobRegistry.stopAll();
vi.clearAllMocks();
});

describe("register", () => {
it("should register a scheduled task and return the task ID", () => {
const taskId = ScheduledJobRegistry.register(mockTask);

expect(taskId).toBe("test-task-1");
expect(ScheduledJobRegistry.hasTask("test-task-1")).toBe(true);
expect(ScheduledJobRegistry.hasTask("test-task-2")).toBe(false);
expect(ScheduledJobRegistry.getTaskCount()).toBe(1);
});

it("should handle multiple task registrations", () => {
const mockTask2 = {
id: "test-task-2",
stop: vi.fn().mockResolvedValue(undefined),
getStatus: vi.fn().mockReturnValue("scheduled"),
} as unknown as ScheduledTask;

const taskId1 = ScheduledJobRegistry.register(mockTask);
const taskId2 = ScheduledJobRegistry.register(mockTask2);

expect(taskId1).toBe("test-task-1");
expect(taskId2).toBe("test-task-2");
expect(ScheduledJobRegistry.getTaskCount()).toBe(2);
expect(ScheduledJobRegistry.hasTask("test-task-1")).toBe(true);
expect(ScheduledJobRegistry.hasTask("test-task-2")).toBe(true);
});

it("should overwrite task with same ID", () => {
const mockTask2 = {
id: "test-task-1", // Same ID as first task
stop: vi.fn().mockResolvedValue(undefined),
getStatus: vi.fn().mockReturnValue("scheduled"),
} as unknown as ScheduledTask;

ScheduledJobRegistry.register(mockTask);
expect(ScheduledJobRegistry.getTaskCount()).toBe(1);

ScheduledJobRegistry.register(mockTask2);
expect(ScheduledJobRegistry.getTaskCount()).toBe(1); // Still 1, task was overwritten
});
});

describe("stop", () => {
it("should stop and unregister a specific task", async () => {
ScheduledJobRegistry.register(mockTask);
expect(ScheduledJobRegistry.hasTask("test-task-1")).toBe(true);

const result = await ScheduledJobRegistry.stop("test-task-1");

expect(result).toBe(true);
expect(stopSpy).toHaveBeenCalledOnce();
expect(ScheduledJobRegistry.hasTask("test-task-1")).toBe(false);
expect(ScheduledJobRegistry.getTaskCount()).toBe(0);
});

it("should return false when trying to stop a non-existent task", async () => {
const result = await ScheduledJobRegistry.stop("non-existent-task");
expect(result).toBe(false);
});

it("should handle task stop errors gracefully", async () => {
const errorStopSpy = vi.fn().mockRejectedValue(new Error("Stop failed"));
const errorTask = {
id: "error-task",
stop: errorStopSpy,
getStatus: vi.fn().mockReturnValue("scheduled"),
} as unknown as ScheduledTask;

ScheduledJobRegistry.register(errorTask);
expect(ScheduledJobRegistry.hasTask("error-task")).toBe(true);

// The stop method should still throw the error
await ScheduledJobRegistry.stop("error-task");

// But the task should still be removed from registry even if stop fails
expect(ScheduledJobRegistry.hasTask("error-task")).toBe(false);
expect(ScheduledJobRegistry.getTaskCount()).toBe(0);
});
});

describe("stopAll", () => {
it("should stop all registered tasks and clear the registry", async () => {
const stopSpy1 = vi.fn().mockResolvedValue(undefined);
const stopSpy2 = vi.fn().mockResolvedValue(undefined);

const mockTask1 = {
id: "task-1",
stop: stopSpy1,
getStatus: vi.fn().mockReturnValue("scheduled"),
} as unknown as ScheduledTask;

const mockTask2 = {
id: "task-2",
stop: stopSpy2,
getStatus: vi.fn().mockReturnValue("scheduled"),
} as unknown as ScheduledTask;

ScheduledJobRegistry.register(mockTask1);
ScheduledJobRegistry.register(mockTask2);
expect(ScheduledJobRegistry.getTaskCount()).toBe(2);

await ScheduledJobRegistry.stopAll();

expect(stopSpy1).toHaveBeenCalledOnce();
expect(stopSpy2).toHaveBeenCalledOnce();
expect(ScheduledJobRegistry.getTaskCount()).toBe(0);
});

it("should handle empty registry gracefully", async () => {
expect(ScheduledJobRegistry.getTaskCount()).toBe(0);
await expect(ScheduledJobRegistry.stopAll()).resolves.not.toThrow();
expect(ScheduledJobRegistry.getTaskCount()).toBe(0);
});

it("should continue stopping other tasks even if one fails", async () => {
const stopSpy1 = vi.fn().mockResolvedValue(undefined);
const stopSpy2 = vi.fn().mockResolvedValue(undefined);
const errorStopSpy = vi.fn().mockRejectedValue(new Error("Stop failed"));

const mockTask1 = {
id: "task-1",
stop: stopSpy1,
getStatus: vi.fn().mockReturnValue("scheduled"),
} as unknown as ScheduledTask;

const errorTask = {
id: "error-task",
stop: errorStopSpy,
getStatus: vi.fn().mockReturnValue("scheduled"),
} as unknown as ScheduledTask;

const mockTask2 = {
id: "task-2",
stop: stopSpy2,
getStatus: vi.fn().mockReturnValue("scheduled"),
} as unknown as ScheduledTask;

ScheduledJobRegistry.register(mockTask1);
ScheduledJobRegistry.register(errorTask);
ScheduledJobRegistry.register(mockTask2);
expect(ScheduledJobRegistry.getTaskCount()).toBe(3);

await ScheduledJobRegistry.stopAll();

expect(stopSpy1).toHaveBeenCalledOnce();
expect(errorStopSpy).toHaveBeenCalledOnce();
expect(stopSpy2).toHaveBeenCalledOnce();

// Registry should be cleared even with errors
expect(ScheduledJobRegistry.getTaskCount()).toBe(0);
});
});

describe("registry state management", () => {
it("should properly manage registry state across multiple operations", async () => {
// Start with empty registry
expect(ScheduledJobRegistry.getTaskCount()).toBe(0);

// Register multiple tasks
ScheduledJobRegistry.register(mockTask);
const mockTask2 = {
id: "task-2",
stop: vi.fn().mockResolvedValue(undefined),
getStatus: vi.fn().mockReturnValue("scheduled"),
} as unknown as ScheduledTask;
ScheduledJobRegistry.register(mockTask2);

expect(ScheduledJobRegistry.getTaskCount()).toBe(2);
expect(ScheduledJobRegistry.hasTask("test-task-1")).toBe(true);
expect(ScheduledJobRegistry.hasTask("task-2")).toBe(true);

// Stop one task
await ScheduledJobRegistry.stop("test-task-1");
expect(ScheduledJobRegistry.getTaskCount()).toBe(1);
expect(ScheduledJobRegistry.hasTask("test-task-1")).toBe(false);
expect(ScheduledJobRegistry.hasTask("task-2")).toBe(true);

// Stop all remaining tasks
await ScheduledJobRegistry.stopAll();
expect(ScheduledJobRegistry.getTaskCount()).toBe(0);
expect(ScheduledJobRegistry.hasTask("task-2")).toBe(false);
});
});
});
Loading
Loading