import { logger } from '@squeebot/core/lib/core'; import { Plugin, EventListener, IPlugin, } from '@squeebot/core/lib/plugin'; import nodeCron, { ScheduledTask } from 'node-cron'; type ExecutorFn = (...args: any[]) => void; class CronWrapper { private cronTask: ScheduledTask | null = null; public id = Math.random().toString(36).slice(2); public stopped = true; public destroyed = false; constructor( public expression: string, public taskFn: ExecutorFn, public origin: IPlugin, ) {} assert(): void { if (!nodeCron.validate(this.expression)) { throw new Error('Invalid cron pattern!'); } } start(): void { if (this.destroyed) { logger.warn('[cron] Someone tried to start a destroyed task! This could indicate a memory leak!'); return; } if (!this.cronTask) { this.assert(); this.cronTask = nodeCron.schedule(this.expression, () => { this.taskFn.call(this.origin); }, { scheduled: false, }); } this.cronTask.start(); this.stopped = false; } stop(): void { if (!this.cronTask) { return; } this.cronTask.stop(); this.stopped = true; } destroy(): void { this.destroyed = true; if (!this.cronTask) { return; } this.cronTask.destroy(); this.cronTask = null; } belongsTo(plugin: IPlugin | string): boolean { if (typeof plugin === 'string') { return this.origin.manifest.name === plugin; } return plugin === this.origin || plugin.manifest.name === this.origin.manifest.name; } } class CronPlugin extends Plugin { private timers: CronWrapper[] = []; @EventListener('pluginUnload') public unloadEventHandler(plugin: string | Plugin): void { if (plugin === this.name || plugin === this) { this.timers.forEach((timer) => timer.destroy()); this.timers = []; this.emit('pluginUnloaded', this); } } @EventListener('pluginUnloaded') public unloadedEventHandler(plugin: string | Plugin): void { this.timers.forEach((timer) => { if (timer.belongsTo(plugin)) { timer.destroy(); } }); this.cleanUp(); } /** * @returns A list of tasks including id, cron expression and origin plugin. */ public getList(): string[][] { return this.timers.map((timer) => ([ timer.id, timer.expression, timer.origin.manifest.name, timer.destroyed || timer.stopped ? 'stopped' : 'running', ])); } /** * Register a new cron task * @param plugin Plugin registering this task * @param expression Cron expression * @param taskFn Function to execute * @param autostart Start the scheduler on add * @returns The task */ public registerTimer( plugin: IPlugin, expression: string, taskFn: ExecutorFn, autostart = true, ): CronWrapper { const newTimer = new CronWrapper(expression, taskFn, plugin); newTimer.assert(); if (autostart) { newTimer.start(); } this.timers.push(newTimer); return newTimer; } /** * Remove a scheduled timer by ID or by the object itself. * Always use this to ensure that memory is properly cleared. * @param timer Timer or timer ID */ public removeTimer(timer: string | CronWrapper): void { if (typeof timer === 'string') { const find = this.timers.find((item) => item.id === timer); if (find) { find.destroy(); } } else { timer.destroy(); } this.cleanUp(); } /** * Remove destroyed timers from the cache */ private cleanUp(): void { this.timers = this.timers.filter((timer) => !timer.destroyed); } } module.exports = CronPlugin;