From f9e7eaf92d22b76d3d02a1bbe8324ca6813f48f8 Mon Sep 17 00:00:00 2001 From: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com> Date: Sat, 18 Jan 2025 11:47:51 -0600 Subject: [PATCH] feat: 4.2.0 load multiple directories & `handleModuleErrors` (#378) * error-handling-draft * feat: array based module loading (#379) Co-authored-by: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com> * Update utility.ts * Update sern.ts * describesemanticsbetter --------- Co-authored-by: Duro --- src/handlers/ready.ts | 27 ++++++++----- src/handlers/tasks.ts | 21 ++++++---- src/handlers/user-defined-events.ts | 15 ++++--- src/index.ts | 17 -------- src/sern.ts | 23 ++++++++--- src/types/utility.ts | 62 +++++++++++++++++++++++++++-- 6 files changed, 115 insertions(+), 50 deletions(-) diff --git a/src/handlers/ready.ts b/src/handlers/ready.ts index 4ae4fc42..78181a27 100644 --- a/src/handlers/ready.ts +++ b/src/handlers/ready.ts @@ -3,10 +3,10 @@ import { once } from 'node:events'; import { resultPayload } from '../core/functions'; import { CommandType } from '../core/structures/enums'; import { Module } from '../types/core-modules'; -import type { UnpackedDependencies } from '../types/utility'; +import type { UnpackedDependencies, Wrapper } from '../types/utility'; import { callInitPlugins } from './event-utils'; -export default async function(dir: string, deps : UnpackedDependencies) { +export default async function(dirs: string | string[], deps : UnpackedDependencies) { const { '@sern/client': client, '@sern/logger': log, '@sern/emitter': sEmitter, @@ -17,16 +17,21 @@ export default async function(dir: string, deps : UnpackedDependencies) { // https://observablehq.com/@ehouais/multiple-promises-as-an-async-generator // possibly optimize to concurrently import modules - for await (const path of Files.readRecursive(dir)) { - let { module } = await Files.importModule(path); - const validType = module.type >= CommandType.Text && module.type <= CommandType.ChannelSelect; - if(!validType) { - throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``); + + const directories = Array.isArray(dirs) ? dirs : [dirs]; + + for (const dir of directories) { + for await (const path of Files.readRecursive(dir)) { + let { module } = await Files.importModule(path); + const validType = module.type >= CommandType.Text && module.type <= CommandType.ChannelSelect; + if(!validType) { + throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``); + } + const resultModule = await callInitPlugins(module, deps, true); + // FREEZE! no more writing!! + commands.set(resultModule.meta.id, Object.freeze(resultModule)); + sEmitter.emit('module.register', resultPayload('success', resultModule)); } - const resultModule = await callInitPlugins(module, deps, true); - // FREEZE! no more writing!! - commands.set(resultModule.meta.id, Object.freeze(resultModule)); - sEmitter.emit('module.register', resultPayload('success', resultModule)); } sEmitter.emit('modulesLoaded'); } diff --git a/src/handlers/tasks.ts b/src/handlers/tasks.ts index a204fc5c..e0720e3f 100644 --- a/src/handlers/tasks.ts +++ b/src/handlers/tasks.ts @@ -1,16 +1,21 @@ import * as Files from '../core/module-loading' -import { UnpackedDependencies } from "../types/utility"; +import { UnpackedDependencies, Wrapper } from "../types/utility"; import type { ScheduledTask } from "../types/core-modules"; import { relative } from "path"; import { fileURLToPath } from "url"; -export const registerTasks = async (tasksPath: string, deps: UnpackedDependencies) => { +export const registerTasks = async (tasksDirs: string | string[], deps: UnpackedDependencies) => { const taskManager = deps['@sern/scheduler'] - for await (const f of Files.readRecursive(tasksPath)) { - let { module } = await Files.importModule(f); - //module.name is assigned by Files.importModule<> - // the id created for the task is unique - const uuid = module.name+"/"+relative(tasksPath,fileURLToPath(f)) - taskManager.schedule(uuid, module, deps) + + const directories = Array.isArray(tasksDirs) ? tasksDirs : [tasksDirs]; + + for (const dir of directories) { + for await (const path of Files.readRecursive(dir)) { + let { module } = await Files.importModule(path); + //module.name is assigned by Files.importModule<> + // the id created for the task is unique + const uuid = module.name+"/"+relative(dir,fileURLToPath(path)) + taskManager.schedule(uuid, module, deps) + } } } diff --git a/src/handlers/user-defined-events.ts b/src/handlers/user-defined-events.ts index 00b72cc7..0cfc56b6 100644 --- a/src/handlers/user-defined-events.ts +++ b/src/handlers/user-defined-events.ts @@ -1,6 +1,6 @@ import { EventType, SernError } from '../core/structures/enums'; import { callInitPlugins } from './event-utils' -import { EventModule, Module } from '../types/core-modules'; +import { EventModule } from '../types/core-modules'; import * as Files from '../core/module-loading' import type { UnpackedDependencies } from '../types/utility'; import type { Emitter } from '../core/interfaces'; @@ -10,11 +10,16 @@ import type { Wrapper } from '../' export default async function(deps: UnpackedDependencies, wrapper: Wrapper) { const eventModules: EventModule[] = []; - for await (const path of Files.readRecursive(wrapper.events!)) { - let { module } = await Files.importModule(path); - await callInitPlugins(module, deps) - eventModules.push(module as EventModule); + const eventDirs = Array.isArray(wrapper.events!) ? wrapper.events! : [wrapper.events!]; + + for (const dir of eventDirs) { + for await (const path of Files.readRecursive(dir)) { + let { module } = await Files.importModule(path); + await callInitPlugins(module, deps) + eventModules.push(module); + } } + const logger = deps['@sern/logger'], report = deps['@sern/emitter']; for (const module of eventModules) { let source: Emitter; diff --git a/src/index.ts b/src/index.ts index c0a01ef6..8becddc3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,20 +53,3 @@ export * from './core/plugin'; export { CommandType, PluginType, PayloadType, EventType } from './core/structures/enums'; export { Context } from './core/structures/context'; export { type CoreDependencies, makeDependencies, single, transient, Service, Services } from './core/ioc'; - - -import type { Container } from '@sern/ioc'; - -/** - * @deprecated This old signature will be incompatible with future versions of sern >= 4.0.0. See {@link makeDependencies} - * @example - * ```ts - * To switch your old code: - await makeDependencies(({ add }) => { - add('@sern/client', new Client()) - }) - * ``` - */ -export interface DependencyConfiguration { - build: (root: Container) => Container; -} diff --git a/src/sern.ts b/src/sern.ts index a9a461eb..b0bfde41 100644 --- a/src/sern.ts +++ b/src/sern.ts @@ -11,7 +11,7 @@ import ready from './handlers/ready'; import { interactionHandler } from './handlers/interaction'; import { messageHandler } from './handlers/message' import { presenceHandler } from './handlers/presence'; -import { UnpackedDependencies, Wrapper } from './types/utility'; +import type { Payload, UnpackedDependencies, Wrapper } from './types/utility'; import type { Presence} from './core/presences'; import { registerTasks } from './handlers/tasks'; @@ -32,7 +32,6 @@ import { registerTasks } from './handlers/tasks'; export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) { const startTime = performance.now(); const deps = useContainerRaw().deps(); - if (maybeWrapper.events !== undefined) { eventsHandler(deps, maybeWrapper) .then(() => { @@ -42,6 +41,22 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) { deps['@sern/logger']?.info({ message: "No events registered" }); } + // autohandle errors that occur in modules. + // convenient for rapid iteration + if(maybeWrapper.handleModuleErrors) { + if(!deps['@sern/logger']) { + throw Error('A logger is required to handleModuleErrors.\n A default logger is already supplied!'); + } + deps['@sern/logger']?.info({ 'message': 'handleModuleErrors enabled' }) + deps['@sern/emitter'].addListener('error', (payload: Payload) => { + if(payload.type === 'failure') { + deps['@sern/logger']?.error({ message: payload.reason }) + } else { + deps['@sern/logger']?.warning({ message: "error event should only have payloads of 'failure'" }); + } + }) + } + const initCallsite = callsites()[1].getFileName(); const presencePath = Files.shouldHandle(initCallsite!, "presence"); //Ready event: load all modules and when finished, time should be taken and logged @@ -60,10 +75,6 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) { } }) .catch(err => { throw err }); - - //const messages$ = messageHandler(deps, maybeWrapper.defaultPrefix); interactionHandler(deps, maybeWrapper.defaultPrefix); messageHandler(deps, maybeWrapper.defaultPrefix) - // listening to the message stream and interaction stream - //merge(messages$, interactions$).subscribe(); } diff --git a/src/types/utility.ts b/src/types/utility.ts index 681ade76..a2e5ca22 100644 --- a/src/types/utility.ts +++ b/src/types/utility.ts @@ -26,9 +26,65 @@ export type UnpackedDependencies = { export type ReplyOptions = string | Omit | MessageReplyOptions; +/** + * @interface Wrapper + * @description Configuration interface for the sern framework. This interface defines + * the structure for configuring essential framework features including command handling, + * event management, and task scheduling. + */ export interface Wrapper { - commands: string; + /** + * @property {string|string[]} commands + * @description Specifies the directory path where command modules are located. + * This is a required property that tells sern where to find and load command files. + * The path should be relative to the project root. If given an array, each directory is loaded in order + * they were declared. Order of modules in each directory is not guaranteed + * + * @example + * commands: ["./dist/commands"] + */ + commands: string | string[]; + /** + * @property {boolean} [handleModuleErrors] + * @description Optional flag to enable automatic error handling for modules. + * When enabled, sern will automatically catch and handle errors that occur + * during module execution, preventing crashes and providing error logging. + * + * @default false + */ + handleModuleErrors?: boolean; + /** + * @property {string} [defaultPrefix] + * @description Optional prefix for text commands. This prefix will be used + * to identify text commands in messages. If not specified, text commands {@link CommandType.Text} + * will be disabled. + * + * @example + * defaultPrefix: "?" + */ defaultPrefix?: string; - events?: string; - tasks?: string; + /** + * @property {string|string[]} [events] + * @description Optional directory path where event modules are located. + * If provided, Sern will automatically register and handle events from + * modules in this directory. The path should be relative to the project root. + * If given an array, each directory is loaded in order they were declared. + * Order of modules in each directory is not guaranteed. + * + * @example + * events: ["./dist/events"] + */ + events?: string | string[]; + /** + * @property {string|string[]} [tasks] + * @description Optional directory path where scheduled task modules are located. + * If provided, Sern will automatically register and handle scheduled tasks + * from modules in this directory. The path should be relative to the project root. + * If given an array, each directory is loaded in order they were declared. + * Order of modules in each directory is not guaranteed. + * + * @example + * tasks: ["./dist/tasks"] + */ + tasks?: string | string[]; }