diff --git a/.npmignore b/.npmignore index 243080d9..dd784f7c 100644 --- a/.npmignore +++ b/.npmignore @@ -113,3 +113,4 @@ tsconfig-cjs.json tsconfig-esm.json renovate.json +fortnite diff --git a/package.json b/package.json index ffde8ce0..83075b12 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@sern/handler", "packageManager": "yarn@3.5.0", - "version": "3.3.4", + "version": "4.0.0", "description": "A complete, customizable, typesafe, & reactive framework for discord bots.", "main": "./dist/index.js", "module": "./dist/index.js", @@ -35,10 +35,10 @@ "author": "SernDevs", "license": "MIT", "dependencies": { - "@sern/ioc": "^1.0.4", + "@sern/ioc": "^1.1.0", "callsites": "^3.1.0", + "cron": "^3.1.7", "deepmerge": "^4.3.1", - "node-cron": "^3.0.3", "rxjs": "^7.8.0", "ts-results-es": "^4.1.0" }, diff --git a/src/core/ioc.ts b/src/core/ioc.ts index 0f0a7b1a..65a6c587 100644 --- a/src/core/ioc.ts +++ b/src/core/ioc.ts @@ -67,8 +67,8 @@ export async function makeDependencies (conf: ValidDependencyConfig) { container.addSingleton('@sern/errors', new __Services.DefaultErrorHandling); container.addSingleton('@sern/modules', new Map); container.addSingleton('@sern/emitter', new EventEmitter) - container.addWiredSingleton('@sern/cron', - (deps) => new __Services.Cron(deps as unknown as Dependencies)) + container.addWiredSingleton('@sern/scheduler', + (deps) => new __Services.CronScheduler(deps as unknown as Dependencies)) conf(dependencyBuilder(container)); await container.ready(); } diff --git a/src/core/presences.ts b/src/core/presences.ts index ee2850b7..3dbc9d64 100644 --- a/src/core/presences.ts +++ b/src/core/presences.ts @@ -39,10 +39,8 @@ export const Presence = { * @example * ```ts * Presence.of({ - * activities: [ - * { name: "Chilling out" } - * ] - * }).once() // Sets the presence once, with what's provided in '.of()' + * activities: [{ name: "Chilling out" }] + * }).once() // Sets the presence once, with what's provided in '.of()' * ``` */ once: () => root @@ -50,7 +48,7 @@ export const Presence = { } } export declare namespace Presence { - type Config = { + export type Config = { inject?: [...T] execute: (...v: IntoDependencies) => Presence.Result; diff --git a/src/core/schedule.ts b/src/core/schedule.ts new file mode 100644 index 00000000..fea65c49 --- /dev/null +++ b/src/core/schedule.ts @@ -0,0 +1,43 @@ +import { CronJob } from 'cron'; +export class TaskScheduler { + private __tasks: Map = new Map(); + + scheduleTask(taskName: string, cronExpression: string, task: () => void): boolean { + if (this.__tasks.has(taskName)) { + return false; + } + try { + const job = new CronJob(cronExpression, task); + job.start(); + this.__tasks.set(taskName, job); + return true; + } catch (error) { + return false; + } + } + + private stopTask(taskName: string): boolean { + const job = this.__tasks.get(taskName); + if (job) { + job.stop(); + this.__tasks.delete(taskName); + return true; + } + return false; + } + + private restartTask(taskName: string): boolean { + const job = this.__tasks.get(taskName); + if (job) { + job.start(); + return true; + } + return false; + } + + + tasks(): string[] { + return Array.from(this.__tasks.keys()); + } + +} diff --git a/src/core/structures/default-services.ts b/src/core/structures/default-services.ts index 474a6e49..18ddc77f 100644 --- a/src/core/structures/default-services.ts +++ b/src/core/structures/default-services.ts @@ -1,8 +1,6 @@ import type { LogPayload, Logging, ErrorHandling, Emitter } from '../interfaces'; import { AnyFunction, UnpackedDependencies } from '../../types/utility'; -import cron from 'node-cron' -import type { CronEventCommand, Module } from '../../types/core-modules' -import { EventType } from './enums'; + /** * @internal * @since 2.0.0 @@ -42,48 +40,30 @@ export class DefaultLogging implements Logging { } } -export class Cron implements Emitter { +export class CronScheduler { tasks: string[] = []; - modules: Map = new Map(); constructor(private deps: UnpackedDependencies) {} - private sanityCheck(eventName: string | symbol) : asserts eventName is string { - if(typeof eventName === 'symbol') throw Error("Cron cannot add symbol based listener") - } - addCronModule(module: Module) { - if(module.type !== EventType.Cron) { - throw Error("Can only add cron modules"); - } - //@ts-ignore - if(!cron.validate(module.pattern)) { - throw Error("Invalid cron expression while adding " + module.name) - } - (module as CronEventCommand) - this.modules.set(module.name!, module as CronEventCommand); - } - addListener(eventName: string | symbol, listener: AnyFunction): this { - this.sanityCheck(eventName); - const retrievedModule = this.modules.get(eventName); - if(!retrievedModule) throw Error("Adding task: module " +eventName +"was not found"); - const { pattern, name, runOnInit, timezone } = retrievedModule; - cron.schedule(pattern, - (date) => listener({ date, deps: this.deps }), - { name, runOnInit, timezone, scheduled: true }); - return this; - } - removeListener(eventName: string | symbol, listener: AnyFunction) { - this.sanityCheck(eventName); - const retrievedModule = this.modules.get(eventName); - if(!retrievedModule) throw Error("Removing cron: module " +eventName +"was not found"); - const task = cron.getTasks().get(retrievedModule.name!) - if(!task) throw Error("Finding cron task with"+ retrievedModule.name + " not found"); - task.stop(); - return this; - } - emit(eventName: string | symbol, ...payload: any[]): boolean { - this.sanityCheck(eventName); - const retrievedModule = this.modules.get(eventName); - if(!retrievedModule) throw Error("Removing cron: module " +eventName +"was not found"); - const task= cron.getTasks().get(retrievedModule.name!) - return task?.emit(eventName, payload) ?? false; - } +// addListener(eventName: string | symbol, listener: AnyFunction): this { +// const retrievedModule = this.modules.get(eventName); +// if(!retrievedModule) throw Error("Adding task: module " +eventName +"was not found"); +// const { pattern, name, runOnInit, timezone } = retrievedModule; +// cron.schedule(pattern, +// (date) => listener({ date, deps: this.deps }), +// { name, runOnInit, timezone, scheduled: true }); +// return this; +// } +// removeListener(eventName: string | symbol, listener: AnyFunction) { +// const retrievedModule = this.modules.get(eventName); +// if(!retrievedModule) throw Error("Removing cron: module " +eventName +"was not found"); +// const task = cron.getTasks().get(retrievedModule.name!) +// if(!task) throw Error("Finding cron task with"+ retrievedModule.name + " not found"); +// task.stop(); +// return this; +// } +// emit(eventName: string | symbol, ...payload: any[]): boolean { +// const retrievedModule = this.modules.get(eventName); +// if(!retrievedModule) throw Error("Removing cron: module " +eventName +"was not found"); +// const task= cron.getTasks().get(retrievedModule.name!) +// return task?.emit(eventName, payload) ?? false; +// } } diff --git a/src/core/structures/enums.ts b/src/core/structures/enums.ts index 2990c2e7..83675a77 100644 --- a/src/core/structures/enums.ts +++ b/src/core/structures/enums.ts @@ -58,7 +58,6 @@ export enum EventType { * Could be for example, `process` events, database events */ External, - Cron } /** diff --git a/src/handlers/event-utils.ts b/src/handlers/event-utils.ts index 2f2f17a3..a6a4089d 100644 --- a/src/handlers/event-utils.ts +++ b/src/handlers/event-utils.ts @@ -262,10 +262,9 @@ export function intoTask(onStop: (m: Module) => unknown) { return createResultResolver({ onStop, onNext }); } -export const handleCrash = - ({ "@sern/errors": err, '@sern/emitter': sem, '@sern/logger': log } : UnpackedDependencies) => +export const handleCrash = ({ "@sern/errors": err, '@sern/emitter': sem, '@sern/logger': log } : UnpackedDependencies, metadata: string) => pipe(catchError(handleError(err, sem, log)), finalize(() => { - log?.info({ message: 'A stream closed or reached end of lifetime' }); + log?.info({ message: 'A stream closed: ' + metadata }); disposeAll(log); })) diff --git a/src/handlers/interaction.ts b/src/handlers/interaction.ts index e09eeef5..6b57e415 100644 --- a/src/handlers/interaction.ts +++ b/src/handlers/interaction.ts @@ -1,6 +1,6 @@ import type { Interaction } from 'discord.js'; import { mergeMap, merge, concatMap, EMPTY } from 'rxjs'; -import { createInteractionHandler, executeModule, intoTask, sharedEventStream, filterTap } from './event-utils'; +import { createInteractionHandler, executeModule, intoTask, sharedEventStream, filterTap, handleCrash } from './event-utils'; import { SernError } from '../core/structures/enums' import { isAutocomplete, isCommand, isMessageComponent, isModal, resultPayload } from '../core/functions' import { UnpackedDependencies } from '../types/utility'; @@ -25,5 +25,6 @@ export default function interactionHandler(deps: UnpackedDependencies, defaultPr if(payload) return executeModule(emitter, payload) return EMPTY; - })); + }), + handleCrash(deps, "interaction handling")); } diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 60b3b8de..4fac7d2e 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -1,6 +1,6 @@ import { EMPTY, mergeMap, concatMap } from 'rxjs'; import type { Message } from 'discord.js'; -import { createMessageHandler, executeModule, intoTask, sharedEventStream, filterTap} from './event-utils'; +import { createMessageHandler, executeModule, intoTask, sharedEventStream, filterTap, handleCrash} from './event-utils'; import { SernError } from '../core/structures/enums' import { resultPayload } from '../core/functions' import { UnpackedDependencies } from '../types/utility'; @@ -44,5 +44,7 @@ function (deps: UnpackedDependencies, defaultPrefix?: string) { if(payload) return executeModule(emitter, payload) return EMPTY; - })); + }), + handleCrash(deps, "message handling") + ) } diff --git a/src/handlers/tasks.ts b/src/handlers/tasks.ts new file mode 100644 index 00000000..441b1cf8 --- /dev/null +++ b/src/handlers/tasks.ts @@ -0,0 +1,23 @@ +import { TaskScheduler } from "../core/schedule" +import * as Files from '../core/module-loading' +import { UnpackedDependencies } from "../types/utility"; + +interface ScheduledTaskModule { + name?: string; + description?: string; + pattern: string; + execute(deps: UnpackedDependencies, tasks: string[]): any +} + +export const registerTasks = async (path: string, deps: UnpackedDependencies) => { + const taskManager = new TaskScheduler() + + for await (const f of Files.readRecursive(path)) { + let { module } = await Files.importModule(f); + //module.name is assigned by Files.importModule<> + taskManager.scheduleTask(module.name!, module.pattern, () => { + module.execute(deps, taskManager.tasks()) + }) + } + +} diff --git a/src/handlers/user-defined-events.ts b/src/handlers/user-defined-events.ts index ebbe4b9b..aea565a5 100644 --- a/src/handlers/user-defined-events.ts +++ b/src/handlers/user-defined-events.ts @@ -14,12 +14,6 @@ const intoDispatcher = (deps: UnpackedDependencies) => return eventDispatcher(deps, module, deps['@sern/client']); case EventType.External: return eventDispatcher(deps, module, deps[module.emitter]); - case EventType.Cron: { - //@ts-ignore - const cron = deps['@sern/cron']; - cron.addCronModule(module); - return eventDispatcher(deps, module, cron); - } default: throw Error(SernError.InvalidModuleType + ' while creating event handler'); } }; @@ -34,6 +28,6 @@ export default async function(deps: UnpackedDependencies, eventDir: string) { from(eventModules) .pipe(map(intoDispatcher(deps)), mergeAll(), // all eventListeners are turned on - handleCrash(deps)) + handleCrash(deps, "event modules")) .subscribe(); } diff --git a/src/sern.ts b/src/sern.ts index 07f50a9d..72ea4b3d 100644 --- a/src/sern.ts +++ b/src/sern.ts @@ -12,11 +12,13 @@ import { presenceHandler } from './handlers/presence'; import { handleCrash } from './handlers/event-utils'; import { UnpackedDependencies } from './types/utility'; import type { Presence} from './core/presences'; +import { registerTasks } from './handlers/tasks'; interface Wrapper { commands: string; defaultPrefix?: string; events?: string; + tasks?: string; } /** * @since 1.0.0 @@ -57,11 +59,14 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) { } presenceHandler(presencePath.path, setPresence).subscribe(); } + if(maybeWrapper.tasks) { + registerTasks(maybeWrapper.tasks, deps); + } }) .catch(err => { throw err }); const messages$ = messageHandler(deps, maybeWrapper.defaultPrefix); const interactions$ = interactionHandler(deps, maybeWrapper.defaultPrefix); // listening to the message stream and interaction stream - merge(messages$, interactions$).pipe(handleCrash(deps)).subscribe(); + merge(messages$, interactions$).subscribe(); } diff --git a/src/types/core-modules.ts b/src/types/core-modules.ts index 8437fe2e..a9bf143b 100644 --- a/src/types/core-modules.ts +++ b/src/types/core-modules.ts @@ -58,14 +58,7 @@ export interface ExternalEventCommand extends Module { type: EventType.External; execute(...args: unknown[]): Awaitable; } -export interface CronEventCommand extends Module { - type: EventType.Cron; - name?: string; - pattern: string; - runOnInit?: boolean - timezone?: string; - execute(...args: unknown[]): Awaitable; -} + export interface ContextMenuUser extends Module { type: CommandType.CtxUser; @@ -142,7 +135,7 @@ export interface BothCommand extends Module { execute: (ctx: Context, tbd: SDT) => Awaitable; } -export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand | CronEventCommand; +export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand; export type CommandModule = | TextCommand | SlashCommand @@ -178,7 +171,6 @@ export interface EventModuleDefs { [EventType.Sern]: SernEventCommand; [EventType.Discord]: DiscordEventCommand; [EventType.External]: ExternalEventCommand; - [EventType.Cron]: CronEventCommand; } export interface SernAutocompleteData diff --git a/yarn.lock b/yarn.lock index 064b9d75..177077c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -546,16 +546,16 @@ __metadata: resolution: "@sern/handler@workspace:." dependencies: "@faker-js/faker": ^8.0.1 - "@sern/ioc": ^1.0.4 + "@sern/ioc": ^1.1.0 "@types/node": ^20.0.0 "@types/node-cron": ^3.0.11 "@typescript-eslint/eslint-plugin": 5.58.0 "@typescript-eslint/parser": 5.59.1 callsites: ^3.1.0 + cron: ^3.1.7 deepmerge: ^4.3.1 discord.js: ^14.15.3 eslint: 8.39.0 - node-cron: ^3.0.3 rxjs: ^7.8.0 ts-results-es: ^4.1.0 typescript: 5.0.2 @@ -563,10 +563,10 @@ __metadata: languageName: unknown linkType: soft -"@sern/ioc@npm:^1.0.4": - version: 1.0.4 - resolution: "@sern/ioc@npm:1.0.4" - checksum: 3d1a63099b3e8ff0d44bb73007b1d66c3f3b27cf7a193c2d9122e021cb72be1ced535ea98efaf72602f371a489177e3b144ed72da8d7de80887c0408ce79cce2 +"@sern/ioc@npm:^1.1.0": + version: 1.1.0 + resolution: "@sern/ioc@npm:1.1.0" + checksum: 0882ef51c3fcd28e7fe803762f2a8d7eb3dca4e494d3475ef7d4a43158d3b24243bda3679c3d58485c89bdc820719d22351007503e44b5cf8e6f2d0efe342921 languageName: node linkType: hard @@ -591,6 +591,13 @@ __metadata: languageName: node linkType: hard +"@types/luxon@npm:~3.4.0": + version: 3.4.2 + resolution: "@types/luxon@npm:3.4.2" + checksum: 6f92d5bd02e89f310395753506bcd9cef3a56f5940f7a50db2a2b9822bce753553ac767d143cb5b4f9ed5ddd4a84e64f89ff538082ceb4d18739af7781b56925 + languageName: node + linkType: hard + "@types/node-cron@npm:^3.0.11": version: 3.0.11 resolution: "@types/node-cron@npm:3.0.11" @@ -1120,6 +1127,16 @@ __metadata: languageName: node linkType: hard +"cron@npm:^3.1.7": + version: 3.1.7 + resolution: "cron@npm:3.1.7" + dependencies: + "@types/luxon": ~3.4.0 + luxon: ~3.4.0 + checksum: d98ee5297543c138221d96dd49270bf6576db80134e6041f4ce4a3c0cb6060863d76910209b34fee66fbf134461449ec3bd283d6a76d1c50da220cde7fc10c65 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -2065,6 +2082,13 @@ __metadata: languageName: node linkType: hard +"luxon@npm:~3.4.0": + version: 3.4.4 + resolution: "luxon@npm:3.4.4" + checksum: 36c1f99c4796ee4bfddf7dc94fa87815add43ebc44c8934c924946260a58512f0fd2743a629302885df7f35ccbd2d13f178c15df046d0e3b6eb71db178f1c60c + languageName: node + linkType: hard + "magic-bytes.js@npm:^1.10.0": version: 1.10.0 resolution: "magic-bytes.js@npm:1.10.0" @@ -2292,15 +2316,6 @@ __metadata: languageName: node linkType: hard -"node-cron@npm:^3.0.3": - version: 3.0.3 - resolution: "node-cron@npm:3.0.3" - dependencies: - uuid: 8.3.2 - checksum: 351c37491ebf717d0ae69cc941465de118e5c2ef5d48bc3f87c98556241b060f100402c8a618c7b86f9f626b44756b20d8b5385b70e52f80716f21e55db0f1c5 - languageName: node - linkType: hard - "node-gyp@npm:latest": version: 10.1.0 resolution: "node-gyp@npm:10.1.0" @@ -3074,15 +3089,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:8.3.2": - version: 8.3.2 - resolution: "uuid@npm:8.3.2" - bin: - uuid: dist/bin/uuid - checksum: 5575a8a75c13120e2f10e6ddc801b2c7ed7d8f3c8ac22c7ed0c7b2ba6383ec0abda88c905085d630e251719e0777045ae3236f04c812184b7c765f63a70e58df - languageName: node - linkType: hard - "vite-node@npm:1.6.0": version: 1.6.0 resolution: "vite-node@npm:1.6.0"