Skip to content

Commit f9e7eaf

Browse files
jacoobesDuroCodes
andauthored
feat: 4.2.0 load multiple directories & handleModuleErrors (#378)
* 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]>
1 parent 52e1456 commit f9e7eaf

File tree

6 files changed

+115
-50
lines changed

6 files changed

+115
-50
lines changed

src/handlers/ready.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import { once } from 'node:events';
33
import { resultPayload } from '../core/functions';
44
import { CommandType } from '../core/structures/enums';
55
import { Module } from '../types/core-modules';
6-
import type { UnpackedDependencies } from '../types/utility';
6+
import type { UnpackedDependencies, Wrapper } from '../types/utility';
77
import { callInitPlugins } from './event-utils';
88

9-
export default async function(dir: string, deps : UnpackedDependencies) {
9+
export default async function(dirs: string | string[], deps : UnpackedDependencies) {
1010
const { '@sern/client': client,
1111
'@sern/logger': log,
1212
'@sern/emitter': sEmitter,
@@ -17,16 +17,21 @@ export default async function(dir: string, deps : UnpackedDependencies) {
1717

1818
// https://observablehq.com/@ehouais/multiple-promises-as-an-async-generator
1919
// possibly optimize to concurrently import modules
20-
for await (const path of Files.readRecursive(dir)) {
21-
let { module } = await Files.importModule<Module>(path);
22-
const validType = module.type >= CommandType.Text && module.type <= CommandType.ChannelSelect;
23-
if(!validType) {
24-
throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``);
20+
21+
const directories = Array.isArray(dirs) ? dirs : [dirs];
22+
23+
for (const dir of directories) {
24+
for await (const path of Files.readRecursive(dir)) {
25+
let { module } = await Files.importModule<Module>(path);
26+
const validType = module.type >= CommandType.Text && module.type <= CommandType.ChannelSelect;
27+
if(!validType) {
28+
throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``);
29+
}
30+
const resultModule = await callInitPlugins(module, deps, true);
31+
// FREEZE! no more writing!!
32+
commands.set(resultModule.meta.id, Object.freeze(resultModule));
33+
sEmitter.emit('module.register', resultPayload('success', resultModule));
2534
}
26-
const resultModule = await callInitPlugins(module, deps, true);
27-
// FREEZE! no more writing!!
28-
commands.set(resultModule.meta.id, Object.freeze(resultModule));
29-
sEmitter.emit('module.register', resultPayload('success', resultModule));
3035
}
3136
sEmitter.emit('modulesLoaded');
3237
}

src/handlers/tasks.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import * as Files from '../core/module-loading'
2-
import { UnpackedDependencies } from "../types/utility";
2+
import { UnpackedDependencies, Wrapper } from "../types/utility";
33
import type { ScheduledTask } from "../types/core-modules";
44
import { relative } from "path";
55
import { fileURLToPath } from "url";
66

7-
export const registerTasks = async (tasksPath: string, deps: UnpackedDependencies) => {
7+
export const registerTasks = async (tasksDirs: string | string[], deps: UnpackedDependencies) => {
88
const taskManager = deps['@sern/scheduler']
9-
for await (const f of Files.readRecursive(tasksPath)) {
10-
let { module } = await Files.importModule<ScheduledTask>(f);
11-
//module.name is assigned by Files.importModule<>
12-
// the id created for the task is unique
13-
const uuid = module.name+"/"+relative(tasksPath,fileURLToPath(f))
14-
taskManager.schedule(uuid, module, deps)
9+
10+
const directories = Array.isArray(tasksDirs) ? tasksDirs : [tasksDirs];
11+
12+
for (const dir of directories) {
13+
for await (const path of Files.readRecursive(dir)) {
14+
let { module } = await Files.importModule<ScheduledTask>(path);
15+
//module.name is assigned by Files.importModule<>
16+
// the id created for the task is unique
17+
const uuid = module.name+"/"+relative(dir,fileURLToPath(path))
18+
taskManager.schedule(uuid, module, deps)
19+
}
1520
}
1621
}

src/handlers/user-defined-events.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EventType, SernError } from '../core/structures/enums';
22
import { callInitPlugins } from './event-utils'
3-
import { EventModule, Module } from '../types/core-modules';
3+
import { EventModule } from '../types/core-modules';
44
import * as Files from '../core/module-loading'
55
import type { UnpackedDependencies } from '../types/utility';
66
import type { Emitter } from '../core/interfaces';
@@ -10,11 +10,16 @@ import type { Wrapper } from '../'
1010

1111
export default async function(deps: UnpackedDependencies, wrapper: Wrapper) {
1212
const eventModules: EventModule[] = [];
13-
for await (const path of Files.readRecursive(wrapper.events!)) {
14-
let { module } = await Files.importModule<Module>(path);
15-
await callInitPlugins(module, deps)
16-
eventModules.push(module as EventModule);
13+
const eventDirs = Array.isArray(wrapper.events!) ? wrapper.events! : [wrapper.events!];
14+
15+
for (const dir of eventDirs) {
16+
for await (const path of Files.readRecursive(dir)) {
17+
let { module } = await Files.importModule<EventModule>(path);
18+
await callInitPlugins(module, deps)
19+
eventModules.push(module);
20+
}
1721
}
22+
1823
const logger = deps['@sern/logger'], report = deps['@sern/emitter'];
1924
for (const module of eventModules) {
2025
let source: Emitter;

src/index.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,3 @@ export * from './core/plugin';
5353
export { CommandType, PluginType, PayloadType, EventType } from './core/structures/enums';
5454
export { Context } from './core/structures/context';
5555
export { type CoreDependencies, makeDependencies, single, transient, Service, Services } from './core/ioc';
56-
57-
58-
import type { Container } from '@sern/ioc';
59-
60-
/**
61-
* @deprecated This old signature will be incompatible with future versions of sern >= 4.0.0. See {@link makeDependencies}
62-
* @example
63-
* ```ts
64-
* To switch your old code:
65-
await makeDependencies(({ add }) => {
66-
add('@sern/client', new Client())
67-
})
68-
* ```
69-
*/
70-
export interface DependencyConfiguration {
71-
build: (root: Container) => Container;
72-
}

src/sern.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import ready from './handlers/ready';
1111
import { interactionHandler } from './handlers/interaction';
1212
import { messageHandler } from './handlers/message'
1313
import { presenceHandler } from './handlers/presence';
14-
import { UnpackedDependencies, Wrapper } from './types/utility';
14+
import type { Payload, UnpackedDependencies, Wrapper } from './types/utility';
1515
import type { Presence} from './core/presences';
1616
import { registerTasks } from './handlers/tasks';
1717

@@ -32,7 +32,6 @@ import { registerTasks } from './handlers/tasks';
3232
export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) {
3333
const startTime = performance.now();
3434
const deps = useContainerRaw().deps<UnpackedDependencies>();
35-
3635
if (maybeWrapper.events !== undefined) {
3736
eventsHandler(deps, maybeWrapper)
3837
.then(() => {
@@ -42,6 +41,22 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) {
4241
deps['@sern/logger']?.info({ message: "No events registered" });
4342
}
4443

44+
// autohandle errors that occur in modules.
45+
// convenient for rapid iteration
46+
if(maybeWrapper.handleModuleErrors) {
47+
if(!deps['@sern/logger']) {
48+
throw Error('A logger is required to handleModuleErrors.\n A default logger is already supplied!');
49+
}
50+
deps['@sern/logger']?.info({ 'message': 'handleModuleErrors enabled' })
51+
deps['@sern/emitter'].addListener('error', (payload: Payload) => {
52+
if(payload.type === 'failure') {
53+
deps['@sern/logger']?.error({ message: payload.reason })
54+
} else {
55+
deps['@sern/logger']?.warning({ message: "error event should only have payloads of 'failure'" });
56+
}
57+
})
58+
}
59+
4560
const initCallsite = callsites()[1].getFileName();
4661
const presencePath = Files.shouldHandle(initCallsite!, "presence");
4762
//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" }) {
6075
}
6176
})
6277
.catch(err => { throw err });
63-
64-
//const messages$ = messageHandler(deps, maybeWrapper.defaultPrefix);
6578
interactionHandler(deps, maybeWrapper.defaultPrefix);
6679
messageHandler(deps, maybeWrapper.defaultPrefix)
67-
// listening to the message stream and interaction stream
68-
//merge(messages$, interactions$).subscribe();
6980
}

src/types/utility.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,65 @@ export type UnpackedDependencies = {
2626
export type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;
2727

2828

29+
/**
30+
* @interface Wrapper
31+
* @description Configuration interface for the sern framework. This interface defines
32+
* the structure for configuring essential framework features including command handling,
33+
* event management, and task scheduling.
34+
*/
2935
export interface Wrapper {
30-
commands: string;
36+
/**
37+
* @property {string|string[]} commands
38+
* @description Specifies the directory path where command modules are located.
39+
* This is a required property that tells sern where to find and load command files.
40+
* The path should be relative to the project root. If given an array, each directory is loaded in order
41+
* they were declared. Order of modules in each directory is not guaranteed
42+
*
43+
* @example
44+
* commands: ["./dist/commands"]
45+
*/
46+
commands: string | string[];
47+
/**
48+
* @property {boolean} [handleModuleErrors]
49+
* @description Optional flag to enable automatic error handling for modules.
50+
* When enabled, sern will automatically catch and handle errors that occur
51+
* during module execution, preventing crashes and providing error logging.
52+
*
53+
* @default false
54+
*/
55+
handleModuleErrors?: boolean;
56+
/**
57+
* @property {string} [defaultPrefix]
58+
* @description Optional prefix for text commands. This prefix will be used
59+
* to identify text commands in messages. If not specified, text commands {@link CommandType.Text}
60+
* will be disabled.
61+
*
62+
* @example
63+
* defaultPrefix: "?"
64+
*/
3165
defaultPrefix?: string;
32-
events?: string;
33-
tasks?: string;
66+
/**
67+
* @property {string|string[]} [events]
68+
* @description Optional directory path where event modules are located.
69+
* If provided, Sern will automatically register and handle events from
70+
* modules in this directory. The path should be relative to the project root.
71+
* If given an array, each directory is loaded in order they were declared.
72+
* Order of modules in each directory is not guaranteed.
73+
*
74+
* @example
75+
* events: ["./dist/events"]
76+
*/
77+
events?: string | string[];
78+
/**
79+
* @property {string|string[]} [tasks]
80+
* @description Optional directory path where scheduled task modules are located.
81+
* If provided, Sern will automatically register and handle scheduled tasks
82+
* from modules in this directory. The path should be relative to the project root.
83+
* If given an array, each directory is loaded in order they were declared.
84+
* Order of modules in each directory is not guaranteed.
85+
*
86+
* @example
87+
* tasks: ["./dist/tasks"]
88+
*/
89+
tasks?: string | string[];
3490
}

0 commit comments

Comments
 (0)