From 6c7318fa156c6546a4012695ce85ee7502b6be62 Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Sun, 5 Oct 2025 12:46:49 +0545 Subject: [PATCH 1/2] fix(tasks): replace cron tasks on reload --- apps/test-bot/commandkit.config.ts | 5 +- apps/test-bot/src/app.ts | 4 -- apps/test-bot/src/app/tasks/current-time.ts | 11 ++++ packages/tasks/src/drivers/sqlite.ts | 57 ++++++++++++++++++--- packages/tasks/src/plugin.ts | 35 +++++++++++-- packages/tasks/src/task.ts | 9 ++-- 6 files changed, 102 insertions(+), 19 deletions(-) create mode 100644 apps/test-bot/src/app/tasks/current-time.ts diff --git a/apps/test-bot/commandkit.config.ts b/apps/test-bot/commandkit.config.ts index 21a613a2..05d14f16 100644 --- a/apps/test-bot/commandkit.config.ts +++ b/apps/test-bot/commandkit.config.ts @@ -13,6 +13,9 @@ export default defineConfig({ devtools(), cache(), ai(), - tasks(), + tasks({ + initializeDefaultDriver: true, + sqliteDriverDatabasePath: './tasks.db', + }), ], }); diff --git a/apps/test-bot/src/app.ts b/apps/test-bot/src/app.ts index 948fac76..1c59101e 100644 --- a/apps/test-bot/src/app.ts +++ b/apps/test-bot/src/app.ts @@ -1,7 +1,5 @@ import { Client, Partials } from 'discord.js'; import { Logger, commandkit } from 'commandkit'; -import { setDriver } from '@commandkit/tasks'; -import { SQLiteDriver } from '@commandkit/tasks/sqlite'; import config from './config.json' with { type: 'json' }; const client = new Client({ @@ -16,8 +14,6 @@ const client = new Client({ partials: [Partials.Channel, Partials.Message, Partials.User], }); -setDriver(new SQLiteDriver('./tasks.db')); - Logger.log('Application bootstrapped successfully!'); commandkit.setPrefixResolver((message) => { diff --git a/apps/test-bot/src/app/tasks/current-time.ts b/apps/test-bot/src/app/tasks/current-time.ts new file mode 100644 index 00000000..5c0293f7 --- /dev/null +++ b/apps/test-bot/src/app/tasks/current-time.ts @@ -0,0 +1,11 @@ +import { task } from '@commandkit/tasks'; +import { Logger } from 'commandkit'; + +export default task({ + name: 'current-time', + immediate: true, + schedule: '*/10 * * * * *', // every 10 seconds + async execute() { + Logger.info(`The current time is ${new Date().toLocaleString()}`); + }, +}); diff --git a/packages/tasks/src/drivers/sqlite.ts b/packages/tasks/src/drivers/sqlite.ts index b9daa48d..47a4f018 100644 --- a/packages/tasks/src/drivers/sqlite.ts +++ b/packages/tasks/src/drivers/sqlite.ts @@ -2,6 +2,7 @@ import { TaskDriver, TaskRunner } from '../driver'; import { TaskData } from '../types'; import { DatabaseSync, StatementSync } from 'node:sqlite'; import cronParser from 'cron-parser'; +import { defer } from 'commandkit'; /** * SQLite-based persistent job queue manager for CommandKit tasks. @@ -28,17 +29,39 @@ export class SQLiteDriver implements TaskDriver { delete: StatementSync; updateNextRun: StatementSync; updateCompleted: StatementSync; + findCronByName: StatementSync; + deleteByName: StatementSync; }; /** * Create a new SQLiteDriver instance. * @param dbPath Path to the SQLite database file (default: './commandkit-tasks.db'). Use `:memory:` for an in-memory database. + * @param pollingInterval The interval in milliseconds to poll for jobs (default: 5_000). */ - constructor(dbPath = './commandkit-tasks.db') { + constructor( + dbPath = './commandkit-tasks.db', + private pollingInterval = 5_000, + ) { this.db = new DatabaseSync(dbPath, { open: true }); this.init(); } + /** + * Get the polling interval. + * @returns The polling interval in milliseconds. + */ + public getPollingInterval() { + return this.pollingInterval; + } + + /** + * Set the polling interval. + * @param pollingInterval The interval in milliseconds to poll for jobs. + */ + public setPollingInterval(pollingInterval: number) { + this.pollingInterval = pollingInterval; + } + /** * Destroy the SQLite driver and stop the polling loop. */ @@ -81,6 +104,12 @@ export class SQLiteDriver implements TaskDriver { updateCompleted: this.db.prepare( /* sql */ `UPDATE jobs SET status = 'completed', last_run = ? WHERE id = ?`, ), + findCronByName: this.db.prepare( + /* sql */ `SELECT id FROM jobs WHERE name = ? AND schedule_type = 'cron' AND status = 'pending'`, + ), + deleteByName: this.db.prepare( + /* sql */ `DELETE FROM jobs WHERE name = ? AND schedule_type = 'cron'`, + ), }; this.startPolling(); @@ -110,6 +139,15 @@ export class SQLiteDriver implements TaskDriver { nextRun = typeof schedule === 'number' ? schedule : schedule.getTime(); } + if (scheduleType === 'cron') { + const existingTask = this.statements.findCronByName.get(name) as + | { id: number } + | undefined; + if (existingTask) { + this.statements.deleteByName.run(name); + } + } + const result = this.statements.insert.run( name, JSON.stringify(data ?? {}), @@ -120,11 +158,13 @@ export class SQLiteDriver implements TaskDriver { Date.now(), ); - if (task.immediate) { - await this.runner?.({ - name, - data, - timestamp: Date.now(), + if (task.immediate && scheduleType === 'cron') { + defer(() => { + return this.runner?.({ + name, + data, + timestamp: Date.now(), + }); }); } @@ -153,7 +193,10 @@ export class SQLiteDriver implements TaskDriver { */ private startPolling() { if (this.interval) clearInterval(this.interval); - this.interval = setInterval(() => this.pollJobs(), 1000).unref(); + this.interval = setInterval( + () => this.pollJobs(), + this.pollingInterval, + ).unref(); // Run immediately on startup this.pollJobs(); } diff --git a/packages/tasks/src/plugin.ts b/packages/tasks/src/plugin.ts index ae795f2d..c6d9ddf8 100644 --- a/packages/tasks/src/plugin.ts +++ b/packages/tasks/src/plugin.ts @@ -31,6 +31,18 @@ export interface TasksPluginOptions { * @default true */ initializeDefaultDriver?: boolean; + /** + * The polling interval for the default sqlite driver. + * Default is 5_000. + * @default 5_000 + */ + sqliteDriverPollingInterval?: number; + /** + * The path to the sqlite database file for the default sqlite driver. + * Default is './commandkit-tasks.db' but `:memory:` can be used for an in-memory database. + * @default './commandkit-tasks.db' + */ + sqliteDriverDatabasePath?: string; } /** @@ -74,7 +86,12 @@ export class TasksPlugin extends RuntimePlugin { const { SQLiteDriver } = require('./drivers/sqlite') as typeof import('./drivers/sqlite'); - taskDriverManager.setDriver(new SQLiteDriver()); + taskDriverManager.setDriver( + new SQLiteDriver( + this.options.sqliteDriverDatabasePath ?? './commandkit-tasks.db', + this.options.sqliteDriverPollingInterval ?? 5_000, + ), + ); } catch (e: any) { Logger.error( `Failed to initialize the default driver for tasks plugin: ${e?.stack || e}`, @@ -182,6 +199,8 @@ export class TasksPlugin extends RuntimePlugin { name: task.name, data: {}, schedule: task.schedule, + immediate: task.immediate, + timezone: task.timezone, }); } @@ -225,14 +244,22 @@ export class TasksPlugin extends RuntimePlugin { if (!taskData || !(taskData instanceof Task)) return; if (this.tasks.has(taskData.name)) { - Logger.info(`Reloading task: ${taskData.name}`); - await taskDriverManager.deleteTask(taskData.name); + if (taskData.isCron()) { + Logger.info(`Replacing cron task: ${taskData.name}`); + // For cron tasks, the SQLiteDriver.create() method will handle the replacement + // No need to manually delete the existing task + } else { + Logger.info(`Reloading task: ${taskData.name}`); + await taskDriverManager.deleteTask(taskData.name); + } this.tasks.set(taskData.name, taskData); if (taskData.schedule) { await taskDriverManager.createTask({ name: taskData.name, data: {}, schedule: taskData.schedule, + immediate: taskData.immediate, + timezone: taskData.timezone, }); } } else { @@ -243,6 +270,8 @@ export class TasksPlugin extends RuntimePlugin { name: taskData.name, data: {}, schedule: taskData.schedule, + immediate: taskData.immediate, + timezone: taskData.timezone, }); } } diff --git a/packages/tasks/src/task.ts b/packages/tasks/src/task.ts index 7edad0c7..53ad5bc0 100644 --- a/packages/tasks/src/task.ts +++ b/packages/tasks/src/task.ts @@ -1,5 +1,5 @@ import { TaskContext } from './context'; -import { TaskDefinition, TaskSchedule } from './types'; +import { TaskData, TaskDefinition, TaskSchedule } from './types'; /** * Represents a task instance with execution logic and metadata. @@ -14,7 +14,7 @@ import { TaskDefinition, TaskSchedule } from './types'; * * export default task({ * name: 'cleanup-old-data', - * schedule: { type: 'cron', value: '0 2 * * *' }, // Daily at 2 AM + * schedule: '0 2 * * *', // Daily at 2 AM * async prepare(ctx) { * // Only run if there's old data to clean * return await hasOldData(); @@ -40,7 +40,8 @@ export class Task = Record> { * Only applicable to cron tasks, defaults to false. */ public get immediate(): boolean { - return this.data.immediate ?? false; + if (this.isCron()) return !!this.data.immediate; + return false; } /** @@ -126,7 +127,7 @@ export class Task = Record> { * // Simple scheduled task * export default task({ * name: 'daily-backup', - * schedule: { type: 'cron', value: '0 0 * * *' }, + * schedule: '0 0 * * *', * async execute(ctx) { * await performBackup(); * }, From bd6f8127684422aa141adfd3a3d1a6fea04e2bfb Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Sun, 5 Oct 2025 12:47:21 +0545 Subject: [PATCH 2/2] docs: generate api docs --- .../tasks/classes/sqlite-driver.mdx | 18 +++++++++++++++--- .../docs/api-reference/tasks/classes/task.mdx | 2 +- .../tasks/classes/tasks-plugin.mdx | 2 +- .../api-reference/tasks/functions/task.mdx | 4 ++-- .../tasks/interfaces/tasks-plugin-options.mdx | 14 ++++++++++++++ 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/website/docs/api-reference/tasks/classes/sqlite-driver.mdx b/apps/website/docs/api-reference/tasks/classes/sqlite-driver.mdx index 698e1b04..b63bddaa 100644 --- a/apps/website/docs/api-reference/tasks/classes/sqlite-driver.mdx +++ b/apps/website/docs/api-reference/tasks/classes/sqlite-driver.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## SQLiteDriver - + SQLite-based persistent job queue manager for CommandKit tasks. @@ -35,7 +35,9 @@ setDriver(driver); ```ts title="Signature" class SQLiteDriver implements TaskDriver { - constructor(dbPath: = './commandkit-tasks.db') + constructor(dbPath: = './commandkit-tasks.db', pollingInterval: = 5_000) + getPollingInterval() => ; + setPollingInterval(pollingInterval: number) => ; destroy() => ; create(task: TaskData) => Promise; delete(identifier: string) => Promise; @@ -50,9 +52,19 @@ class SQLiteDriver implements TaskDriver { ### constructor - SQLiteDriver`} /> + SQLiteDriver`} /> Create a new SQLiteDriver instance. +### getPollingInterval + + `} /> + +Get the polling interval. +### setPollingInterval + + `} /> + +Set the polling interval. ### destroy `} /> diff --git a/apps/website/docs/api-reference/tasks/classes/task.mdx b/apps/website/docs/api-reference/tasks/classes/task.mdx index 0f7042ea..8cf30830 100644 --- a/apps/website/docs/api-reference/tasks/classes/task.mdx +++ b/apps/website/docs/api-reference/tasks/classes/task.mdx @@ -30,7 +30,7 @@ import { task } from '@commandkit/tasks'; export default task({ name: 'cleanup-old-data', - schedule: { type: 'cron', value: '0 2 * * *' }, // Daily at 2 AM + schedule: '0 2 * * *', // Daily at 2 AM async prepare(ctx) { // Only run if there's old data to clean return await hasOldData(); diff --git a/apps/website/docs/api-reference/tasks/classes/tasks-plugin.mdx b/apps/website/docs/api-reference/tasks/classes/tasks-plugin.mdx index dc22cc82..bdae7f16 100644 --- a/apps/website/docs/api-reference/tasks/classes/tasks-plugin.mdx +++ b/apps/website/docs/api-reference/tasks/classes/tasks-plugin.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## TasksPlugin - + CommandKit plugin that provides task management capabilities. diff --git a/apps/website/docs/api-reference/tasks/functions/task.mdx b/apps/website/docs/api-reference/tasks/functions/task.mdx index cadd0056..15e6c7d2 100644 --- a/apps/website/docs/api-reference/tasks/functions/task.mdx +++ b/apps/website/docs/api-reference/tasks/functions/task.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## task - + Creates a new task definition. @@ -31,7 +31,7 @@ import { task } from '@commandkit/tasks'; // Simple scheduled task export default task({ name: 'daily-backup', - schedule: { type: 'cron', value: '0 0 * * *' }, + schedule: '0 0 * * *', async execute(ctx) { await performBackup(); }, diff --git a/apps/website/docs/api-reference/tasks/interfaces/tasks-plugin-options.mdx b/apps/website/docs/api-reference/tasks/interfaces/tasks-plugin-options.mdx index 4b505e67..58b8c230 100644 --- a/apps/website/docs/api-reference/tasks/interfaces/tasks-plugin-options.mdx +++ b/apps/website/docs/api-reference/tasks/interfaces/tasks-plugin-options.mdx @@ -23,6 +23,8 @@ Future versions may support customizing the tasks directory path and HMR behavio ```ts title="Signature" interface TasksPluginOptions { initializeDefaultDriver?: boolean; + sqliteDriverPollingInterval?: number; + sqliteDriverDatabasePath?: string; } ``` @@ -36,6 +38,18 @@ Whether to initialize the default driver. If true, the plugin will initialize the default driver. If false, the plugin will not initialize the default driver. +### sqliteDriverPollingInterval + + + +The polling interval for the default sqlite driver. +Default is 5_000. +### sqliteDriverDatabasePath + +commandkit-tasks.db'`} /> + +The path to the sqlite database file for the default sqlite driver. +Default is './commandkit-tasks.db' but `:memory:` can be used for an in-memory database.