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
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@
"@sern/ioc": "^1.1.0",
"callsites": "^3.1.0",
"cron": "^3.1.7",
"deepmerge": "^4.3.1",
"rxjs": "^7.8.0"
"deepmerge": "^4.3.1"
},
"devDependencies": {
"@faker-js/faker": "^8.0.1",
Expand Down
17 changes: 16 additions & 1 deletion src/core/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,22 @@ import type {
import { ApplicationCommandOptionType, InteractionType } from 'discord.js';
import { PluginType } from './structures/enums';
import assert from 'assert';
import type { Payload } from '../types/utility';
import type { Payload, UnpackedDependencies } from '../types/utility';

export const createSDT = (module: Module, deps: UnpackedDependencies, params: string|undefined) => {
return {
state: {},
deps,
params,
type: module.type,
module: {
name: module.name,
description: module.description,
locals: module.locals,
meta: module.meta
}
}
}

/**
* Removes the first character(s) _[depending on prefix length]_ of the message
Expand Down
2 changes: 1 addition & 1 deletion src/core/ioc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ 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.addSingleton('@sern/emitter', new EventEmitter({ captureRejections: true }))
container.addSingleton('@sern/scheduler', new __Services.TaskScheduler)
conf(dependencyBuilder(container));
await container.ready();
Expand Down
31 changes: 30 additions & 1 deletion src/core/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,34 @@ export function discordEvent<T extends keyof ClientEvents>(mod: {
return eventModule({ type: EventType.Discord, ...mod, });
}

export function scheduledTask(ism: ScheduledTask) { return ism }
/**
* Creates a scheduled task that can be executed at specified intervals using cron patterns
*
* @param {ScheduledTask} ism - The scheduled task configuration object
* @param {string} ism.trigger - A cron pattern that determines when the task should execute
* Format: "* * * * *" (minute hour day month day-of-week)
* @param {Function} ism.execute - The function to execute when the task is triggered
* @param {Object} ism.execute.context - The execution context passed to the task
*
* @returns {ScheduledTask} The configured scheduled task
*
* @example
* // Create a task that runs every minute
* export default scheduledTask({
* trigger: "* * * * *",
* execute: (context) => {
* console.log("Task executed!");
* }
* });
*
* @remarks
* - Tasks must be placed in the 'tasks' directory specified in your config
* - The file name serves as a unique identifier for the task
* - Tasks can be cancelled using deps['@sern/scheduler'].kill(uuid)
*
* @see {@link https://crontab.guru/} for testing and creating cron patterns
*/
export function scheduledTask(ism: ScheduledTask): ScheduledTask {
return ism
}

93 changes: 92 additions & 1 deletion src/core/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,107 @@ export function makePlugin<V extends unknown[]>(
export function EventInitPlugin(execute: (args: InitArgs) => PluginResult) {
return makePlugin(PluginType.Init, execute);
}

/**
* Creates an initialization plugin for command preprocessing and modification
*
* @since 2.5.0
* @template I - Extends CommandType to enforce type safety for command modules
*
* @param {function} execute - Function to execute during command initialization
* @param {InitArgs<T>} execute.args - The initialization arguments
* @param {T} execute.args.module - The command module being initialized
* @param {string} execute.args.absPath - The absolute path to the module file
* @param {Dependencies} execute.args.deps - Dependency injection container
*
* @returns {Plugin} A plugin that runs during command initialization
*
* @example
* // Plugin to update command description
* export const updateDescription = (description: string) => {
* return CommandInitPlugin(({ deps }) => {
* if(description.length > 100) {
* deps.logger?.info({ message: "Invalid description" })
* return controller.stop("From updateDescription: description is invalid");
* }
* module.description = description;
* return controller.next();
* });
* };
*
* @example
* // Plugin to store registration date in module locals
* export const dateRegistered = () => {
* return CommandInitPlugin(({ module }) => {
* module.locals.registered = Date.now()
* return controller.next();
* });
* };
*
* @remarks
* - Init plugins can modify how commands are loaded and perform preprocessing
* - The module.locals object can be used to store custom plugin-specific data
* - Be careful when modifying module fields as multiple plugins may interact with them
* - Use controller.next() to continue to the next plugin
* - Use controller.stop(reason) to halt plugin execution
*/
export function CommandInitPlugin<I extends CommandType>(
execute: (args: InitArgs) => PluginResult
) {
): Plugin {
return makePlugin(PluginType.Init, execute);
}

/**
* Creates a control plugin for command preprocessing, filtering, and state management
*
* @since 2.5.0
* @template I - Extends CommandType to enforce type safety for command modules
*
* @param {function} execute - Function to execute during command control flow
* @param {CommandArgs<I>} execute.args - The command arguments array
* @param {Context} execute.args[0] - The discord context (e.g., guild, channel, user info, interaction)
* @param {SDT} execute.args[1] - The State, Dependencies, Params, Module, and Type object
*
* @returns {Plugin} A plugin that runs during command execution flow
*
* @example
* // Plugin to restrict command to specific guild
* export const inGuild = (guildId: string) => {
* return CommandControlPlugin((ctx, sdt) => {
* if(ctx.guild.id !== guildId) {
* return controller.stop();
* }
* return controller.next();
* });
* };
*
* @example
* // Plugins passing state through the chain
* const plugin1 = CommandControlPlugin((ctx, sdt) => {
* return controller.next({ 'plugin1/data': 'from plugin1' });
* });
*
* const plugin2 = CommandControlPlugin((ctx, sdt) => {
* return controller.next({ 'plugin2/data': ctx.user.id });
* });
*
* export default commandModule({
* type: CommandType.Slash,
* plugins: [plugin1, plugin2],
* execute: (ctx, sdt) => {
* console.log(sdt.state); // Access accumulated state
* }
* });
*
* @remarks
* - Control plugins are executed in order when a discord.js event is emitted
* - Use controller.next() to continue to next plugin or controller.stop() to halt execution
* - State can be passed between plugins using controller.next({ key: value })
* - State keys should be namespaced to avoid collisions (e.g., 'plugin-name/key')
* - Final accumulated state is passed to the command's execute function
* - All plugins must succeed for the command to execute
* - Plugins have access to dependencies through the sdt.deps object
* - Useful for implementing preconditions, filters, and command preprocessing
*/
export function CommandControlPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I>) => PluginResult,
Expand Down
Loading