From 220a60ecf853df8d288de2533c669562a430c3f9 Mon Sep 17 00:00:00 2001 From: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:00:02 -0600 Subject: [PATCH] feat: moduleinfo-in-eventplugins (#373) --- src/core/modules.ts | 62 +++++++++++++- src/core/plugin.ts | 3 +- src/handlers/event-utils.ts | 34 ++++++-- src/handlers/presence.ts | 2 +- src/types/core-modules.ts | 163 ++++++++++++++++++++++++++++++++++-- src/types/utility.ts | 1 + 6 files changed, 247 insertions(+), 18 deletions(-) diff --git a/src/core/modules.ts b/src/core/modules.ts index d31e5402..3aa93f66 100644 --- a/src/core/modules.ts +++ b/src/core/modules.ts @@ -10,8 +10,39 @@ import { partitionPlugins } from './functions' import type { Awaitable } from '../types/utility'; /** - * @since 1.0.0 The wrapper function to define command modules for sern - * @param mod + * Creates a command module with standardized structure and plugin support. + * + * @since 1.0.0 + * @param {InputCommand} mod - Command module configuration + * @returns {Module} Processed command module ready for registration + * + * @example + * // Basic slash command + * export default commandModule({ + * type: CommandType.Slash, + * description: "Ping command", + * execute: async (ctx) => { + * await ctx.reply("Pong! 🏓"); + * } + * }); + * + * @example + * // Command with component interaction + * export default commandModule({ + * type: CommandType.Slash, + * description: "Interactive command", + * execute: async (ctx) => { + * const button = new ButtonBuilder({ + * customId: "btn/someData", + * label: "Click me", + * style: ButtonStyle.Primary + * }); + * await ctx.reply({ + * content: "Interactive message", + * components: [new ActionRowBuilder().addComponents(button)] + * }); + * } + * }); */ export function commandModule(mod: InputCommand): Module { const [onEvent, plugins] = partitionPlugins(mod.plugins); @@ -21,10 +52,33 @@ export function commandModule(mod: InputCommand): Module { locals: {} } as Module; } + /** + * Creates an event module for handling Discord.js or custom events. + * * @since 1.0.0 - * The wrapper function to define event modules for sern - * @param mod + * @template T - Event name from ClientEvents + * @param {InputEvent} mod - Event module configuration + * @returns {Module} Processed event module ready for registration + * @throws {Error} If ControlPlugins are used in event modules + * + * @example + * // Discord event listener + * export default eventModule({ + * type: EventType.Discord, + * execute: async (message) => { + * console.log(`${message.author.tag}: ${message.content}`); + * } + * }); + * + * @example + * // Custom sern event + * export default eventModule({ + * type: EventType.Sern, + * execute: async (eventData) => { + * // Handle sern-specific event + * } + * }); */ export function eventModule(mod: InputEvent): Module { const [onEvent, plugins] = partitionPlugins(mod.plugins); diff --git a/src/core/plugin.ts b/src/core/plugin.ts index f2e39114..88c21db1 100644 --- a/src/core/plugin.ts +++ b/src/core/plugin.ts @@ -1,6 +1,7 @@ import { CommandType, PluginType } from './structures/enums'; import type { Plugin, PluginResult, CommandArgs, InitArgs } from '../types/core-plugin'; import { Err, Ok } from './structures/result'; +import type { Dictionary } from '../types/utility'; export function makePlugin( type: PluginType, @@ -37,7 +38,7 @@ export function CommandControlPlugin( * The object passed into every plugin to control a command's behavior */ export const controller = { - next: (val?: Record) => Ok(val), + next: (val?: Dictionary) => Ok(val), stop: (val?: string) => Err(val), }; diff --git a/src/handlers/event-utils.ts b/src/handlers/event-utils.ts index 2ae15f0c..76a7f3fd 100644 --- a/src/handlers/event-utils.ts +++ b/src/handlers/event-utils.ts @@ -241,7 +241,17 @@ export async function callInitPlugins(_module: Module, deps: Dependencies, emit? export async function callPlugins({ args, module, deps, params }: ExecutePayload) { let state = {}; for(const plugin of module.onEvent??[]) { - const result = await plugin.execute(...args, { state, deps, params, type: module.type }); + const executionContext = { + state, + deps, + params, + type: module.type, + module: { name: module.name, + description: module.description, + locals: module.locals, + meta: module.meta } + }; + const result = await plugin.execute(...args, executionContext); if(!result.ok) { return result; } @@ -253,14 +263,26 @@ export async function callPlugins({ args, module, deps, params }: ExecutePayload } /** * Creates an executable task ( execute the command ) if all control plugins are successful + * this needs to go * @param onStop emits a failure response to the SernEmitter */ export function intoTask(onStop: (m: Module) => unknown) { - const onNext = ({ args, module, deps, params }: ExecutePayload, state: Record) => ({ - module, - args: [...args, { state, deps, params, type: module.type }], - deps - }); + const onNext = ({ args, module, deps, params }: ExecutePayload, state: Record) => { + + return { + module, + args: [...args, { state, + deps, + params, + type: module.type, + module: { name: module.name, + description: module.description, + locals: module.locals, + meta: module.meta } }], + deps + } + + }; return createResultResolver({ onStop, onNext }); } diff --git a/src/handlers/presence.ts b/src/handlers/presence.ts index 0a59b61f..e4646ab8 100644 --- a/src/handlers/presence.ts +++ b/src/handlers/presence.ts @@ -1,4 +1,4 @@ -import { concatMap, from, interval, of, map, scan, startWith, fromEvent, take, mergeScan } from "rxjs" +import { concatMap, from, interval, of, map, startWith, fromEvent, take, mergeScan } from "rxjs" import { Presence } from "../core/presences"; import { Services } from "../core/ioc"; import assert from "node:assert"; diff --git a/src/types/core-modules.ts b/src/types/core-modules.ts index 7212e5a8..3d97eff7 100644 --- a/src/types/core-modules.ts +++ b/src/types/core-modules.ts @@ -19,14 +19,97 @@ import type { import type { CommandType, EventType } from '../core/structures/enums'; import { Context } from '../core/structures/context' import { ControlPlugin, InitPlugin, Plugin } from './core-plugin'; -import { Awaitable, SernEventsMapping, UnpackedDependencies } from './utility'; +import { Awaitable, SernEventsMapping, UnpackedDependencies, Dictionary } from './utility'; -//state, deps, type (very original) +/** + * SDT (State, Dependencies, Type) interface represents the core data structure + * passed through the plugin pipeline to command modules. + * + * @interface SDT + * @template TState - Type parameter for the state object's structure + * @template TDeps - Type parameter for dependencies interface + * + * @property {Record} state - Accumulated state data passed between plugins + * @property {TDeps} deps - Instance of application dependencies + * @property {CommandType} type - Command type identifier + * @property {string} [params] - Optional parameters passed to the command + * + * @example + * // Example of a plugin using SDT + * const loggingPlugin = CommandControlPlugin((ctx, sdt: SDT) => { + * console.log(`User ${ctx.user.id} executed command`); + * return controller.next({ 'logging/timestamp': Date.now() }); + * }); + * + * @example + * // Example of state accumulation through multiple plugins + * const plugin1 = CommandControlPlugin((ctx, sdt: SDT) => { + * return controller.next({ 'plugin1/data': 'value1' }); + * }); + * + * const plugin2 = CommandControlPlugin((ctx, sdt: SDT) => { + * // Access previous state + * const prevData = sdt.state['plugin1/data']; + * return controller.next({ 'plugin2/data': 'value2' }); + * }); + * + * @remarks + * - State is immutable and accumulated through the plugin chain + * - Keys in state should be namespaced to avoid collisions + * - Dependencies are injected and available throughout the pipeline + * - Type information helps plugins make type-safe decisions + * + * @see {@link CommandControlPlugin} for plugin implementation + * @see {@link CommandType} for available command types + * @see {@link Dependencies} for dependency injection interface + */ export type SDT = { - state: Record; + /** + * Accumulated state passed between plugins in the pipeline. + * Each plugin can add to or modify this state using controller.next(). + * + * @type {Record} + * @example + * // Good: Namespaced state key + * { 'myPlugin/userData': { id: '123', name: 'User' } } + * + * // Avoid: Non-namespaced keys that might collide + * { userData: { id: '123' } } + */ + state: Record; + + /** + * Application dependencies available to plugins and command modules. + * Typically includes services, configurations, and utilities. + * + * @type {Dependencies} + */ deps: Dependencies; - type: CommandType, - params?: string + + /** + * Identifies the type of command being processed. + * Used by plugins to apply type-specific logic. + * + * @type {CommandType} + */ + type: CommandType; + + /** + * Optional parameters passed to the command. + * May contain additional configuration or runtime data. + * + * @type {string} + * @optional + */ + params?: string; + + /** + * A copy of the current module that the plugin is running in. + */ + module: { name: string; + description: string; + meta: Dictionary; + locals: Dictionary; } }; export type Processed = T & { name: string; description: string }; @@ -41,7 +124,75 @@ export interface Module { id: string; absPath: string; } - locals: Record + + /** + * Custom data storage object for module-specific information. + * Plugins and module code can use this to store and retrieve metadata, + * configuration, or any other module-specific information. + * + * @type {Dictionary} + * @description A key-value store that allows plugins and module code to persist + * data at the module level. This is especially useful for InitPlugins that need + * to attach metadata or configuration to modules. + * + * @example + * // In a plugin + * module.locals.registrationDate = Date.now(); + * module.locals.version = "1.0.0"; + * module.locals.permissions = ["ADMIN", "MODERATE"]; + * + * @example + * // In module execution + * console.log(`Command registered on: ${new Date(module.locals.registrationDate)}`); + * + * @example + * // Storing localization data + * module.locals.translations = { + * en: "Hello", + * es: "Hola", + * fr: "Bonjour" + * }; + * + * @example + * // Storing command metadata + * module.locals.metadata = { + * category: "admin", + * cooldown: 5000, + * requiresPermissions: true + * }; + * + * @remarks + * - The locals object is initialized as an empty object ({}) by default + * - Keys should be namespaced to avoid collisions between plugins + * - Values can be of any type + * - Data persists for the lifetime of the module + * - Commonly used by InitPlugins during module initialization + * + * @best-practices + * 1. Namespace your keys to avoid conflicts: + * ```typescript + * module.locals['myPlugin:data'] = value; + * ``` + * + * 2. Document the data structure you're storing: + * ```typescript + * interface MyPluginData { + * version: string; + * timestamp: number; + * } + * module.locals['myPlugin:data'] = { + * version: '1.0.0', + * timestamp: Date.now() + * } as MyPluginData; + * ``` + * + * 3. Use type-safe accessors when possible: + * ```typescript + * const getPluginData = (module: Module): MyPluginData => + * module.locals['myPlugin:data']; + * ``` + */ + locals: Dictionary; execute(...args: any[]): Awaitable; } diff --git a/src/types/utility.ts b/src/types/utility.ts index 50264fba..595aa2de 100644 --- a/src/types/utility.ts +++ b/src/types/utility.ts @@ -3,6 +3,7 @@ import type { Module } from './core-modules'; import type { Result } from '../core/structures/result'; export type Awaitable = PromiseLike | T; +export type Dictionary = Record export type VoidResult = Result; export type AnyFunction = (...args: any[]) => unknown;