Skip to content

Commit

Permalink
feat: 4.2.0 load multiple directories & handleModuleErrors (#378)
Browse files Browse the repository at this point in the history
* error-handling-draft

* feat: array based module loading (#379)

Co-authored-by: Jacob Nguyen <[email protected]>

* Update utility.ts

* Update sern.ts

* describesemanticsbetter

---------

Co-authored-by: Duro <[email protected]>
  • Loading branch information
jacoobes and DuroCodes authored Jan 18, 2025
1 parent 52e1456 commit f9e7eaf
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 50 deletions.
27 changes: 16 additions & 11 deletions src/handlers/ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Module>(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<Module>(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');
}
21 changes: 13 additions & 8 deletions src/handlers/tasks.ts
Original file line number Diff line number Diff line change
@@ -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<ScheduledTask>(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<ScheduledTask>(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)
}
}
}
15 changes: 10 additions & 5 deletions src/handlers/user-defined-events.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<Module>(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<EventModule>(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;
Expand Down
17 changes: 0 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
23 changes: 17 additions & 6 deletions src/sern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<UnpackedDependencies>();

if (maybeWrapper.events !== undefined) {
eventsHandler(deps, maybeWrapper)
.then(() => {
Expand All @@ -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
Expand All @@ -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();
}
62 changes: 59 additions & 3 deletions src/types/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,65 @@ export type UnpackedDependencies = {
export type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | 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[];
}

1 comment on commit f9e7eaf

@GlitchApotamus
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any chance we could rename commands to modules so that new developers can understand it a little better?

Please sign in to comment.