diff --git a/src/core/contracts/module-manager.ts b/src/core/contracts/module-manager.ts index 711582e..1175cf6 100644 --- a/src/core/contracts/module-manager.ts +++ b/src/core/contracts/module-manager.ts @@ -6,13 +6,22 @@ import type { } from '../../types/core-modules'; import { CommandType } from '../structures'; -/** - * @since 2.0.0 - */ -export interface ModuleManager { - get(id: string): string | undefined; +interface MetadataAccess { getMetadata(m: Module): CommandMeta | undefined; setMetadata(m: Module, c: CommandMeta): void; +} + +interface OnErrorAccess { + getErrorCallback(m: Module): Function|undefined; + setErrorCallback(m: Module, c: Function): void; +} +/** + * @since 2.0.0 + * @deprecated - direct access to the module manager will be removed in version 4 + */ +export interface ModuleManager extends MetadataAccess, OnErrorAccess { + get(id: string): string | undefined; + set(id: string, path: string): void; getPublishableCommands(): Promise; getByNameCommandType( diff --git a/src/core/contracts/module-store.ts b/src/core/contracts/module-store.ts index b6157b6..d431e9f 100644 --- a/src/core/contracts/module-store.ts +++ b/src/core/contracts/module-store.ts @@ -6,4 +6,5 @@ import type { CommandMeta, Module } from '../../types/core-modules'; export interface CoreModuleStore { commands: Map; metadata: WeakMap; + onError: WeakMap; } diff --git a/src/core/module-loading.ts b/src/core/module-loading.ts index 80977c9..5402427 100644 --- a/src/core/module-loading.ts +++ b/src/core/module-loading.ts @@ -36,15 +36,14 @@ export async function importModule(absPath: string) { .wrap(() => ({ module: commandModule.getInstance(), onError })) .unwrapOr({ module: commandModule, onError }) as T; } -interface FileModuleImports { - module: T, +interface FileExtras { onError : Function } export async function defaultModuleLoader(absPath: string): ModuleResult { - let module = await importModule(absPath); + let { onError, module } = await importModule<{ module: T } & FileExtras>(absPath); assert(module, `Found an undefined module: ${absPath}`); - return { module, absPath, errorHandler: '' }; + return { module, absPath, onError }; } export const fmtFileName = (fileName: string) => parse(fileName).name; @@ -58,7 +57,8 @@ export const fmtFileName = (fileName: string) => parse(fileName).name; export function buildModuleStream( input: ObservableInput, ): Observable> { - return from(input).pipe(mergeMap(defaultModuleLoader)); + return from(input) + .pipe(mergeMap(defaultModuleLoader)); } export const getFullPathTree = (dir: string) => readPaths(resolve(dir)); diff --git a/src/handlers/dispatchers.ts b/src/handlers/dispatchers.ts index 6826cf7..432a732 100644 --- a/src/handlers/dispatchers.ts +++ b/src/handlers/dispatchers.ts @@ -9,20 +9,11 @@ import { SernError, } from '../core/_internal'; import { createResultResolver } from './event-utils'; -import { AutocompleteInteraction, BaseInteraction, Message } from 'discord.js'; +import { BaseInteraction, Message } from 'discord.js'; import { CommandType, Context } from '../core'; import type { Args } from '../types/utility'; import type { BothCommand, CommandModule, Module, Processed } from '../types/core-modules'; -function dispatchInteraction( - payload: { module: Processed; event: V }, - createArgs: (m: typeof payload.event) => unknown[], -) { - return { - module: payload.module, - args: createArgs(payload.event), - }; -} //TODO: refactor dispatchers so that it implements a strategy for each different type of payload? export function dispatchMessage(module: Processed, args: [Context, Args]) { return { @@ -31,21 +22,6 @@ export function dispatchMessage(module: Processed, args: [Context }; } -function dispatchAutocomplete(payload: { - module: Processed; - event: AutocompleteInteraction; -}) { - const option = treeSearch(payload.event, payload.module.options); - assert.ok( - option, - Error(SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`), - ); - return { - module: option.command as Processed, //autocomplete is not a true "module" warning cast! - args: [payload.event], - }; -} - export function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[]) { const ctx = Context.wrap(wrappable); const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.options]; @@ -56,16 +32,16 @@ function interactionArg(interaction: T) { return [interaction] as [T]; } -function intoPayload(module: Processed) { +function intoPayload(module: Processed, onError: Function|undefined) { return pipe( arrayifySource, - map(args => ({ module, args })), + map(args => ({ module, args, onError })), ); } const createResult = createResultResolver< Processed, - { module: Processed; args: unknown[] }, + { module: Processed; args: unknown[], onError: Function|undefined }, unknown[] >({ createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)), @@ -76,14 +52,14 @@ const createResult = createResultResolver< * @param module * @param source */ -export function eventDispatcher(module: Processed, source: unknown) { +export function eventDispatcher(module: Processed, onError: Function|undefined, source: unknown) { assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`); const execute: OperatorFunction = concatMap(async args => module.execute(...args), ); return fromEvent(source, module.name).pipe( - intoPayload(module), + intoPayload(module, onError), concatMap(createResult), execute, ); @@ -92,6 +68,7 @@ export function eventDispatcher(module: Processed, source: unknown) { export function createDispatcher(payload: { module: Processed; event: BaseInteraction; + onError: Function }) { assert.ok( CommandType.Text !== payload.module.type, @@ -101,17 +78,26 @@ export function createDispatcher(payload: { case CommandType.Slash: case CommandType.Both: { if (isAutocomplete(payload.event)) { - /** - * Autocomplete is a special case that - * must be handled separately, since it's - * too different from regular command modules - * CAST SAFETY: payload is already guaranteed to be a slash command or both command - */ - return dispatchAutocomplete(payload as never); + const option = treeSearch(payload.event, payload.module.options); + assert.ok( + option, + Error(SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`), + ); + return { + module: option.command as Processed, //autocomplete is not a true "module" warning cast! + args: [payload.event], + onError: undefined + }; } - return dispatchInteraction(payload, contextArgs); + return { + args: contextArgs(payload.event), + ...payload + }; } default: - return dispatchInteraction(payload, interactionArg); + return { + args: interactionArg(payload.event), + ...payload + } } } diff --git a/src/handlers/event-utils.ts b/src/handlers/event-utils.ts index e61cc2e..d5a240c 100644 --- a/src/handlers/event-utils.ts +++ b/src/handlers/event-utils.ts @@ -78,8 +78,7 @@ export function createInteractionHandler( return Files .defaultModuleLoader>(fullPath) .then(payload => - Ok(createDispatcher({ module: payload.module, event })) - ); + Ok(createDispatcher({ module: payload.module, event, onError: payload.onError }))); }, ); } @@ -172,7 +171,7 @@ export function executeModule( */ export function createResultResolver< T extends { execute: (...args: any[]) => any; onEvent: ControlPlugin[] }, - Args extends { module: T; [key: string]: unknown }, + Args extends { module: T; onError: Function|undefined, [key: string]: unknown }, Output, >(config: { onStop?: (module: T) => unknown; @@ -205,9 +204,9 @@ export function callInitPlugins>(sernEmitter: Emi SernEmitter.failure(module, SernError.PluginFailure), ); }, - onNext: ({ module }) => { + onNext: ({ module, onError }) => { sernEmitter.emit('module.register', SernEmitter.success(module)); - return module; + return { module, onError }; }, }), ); @@ -219,16 +218,23 @@ export function callInitPlugins>(sernEmitter: Emi */ export function makeModuleExecutor< M extends Processed, - Args extends { module: M; args: unknown[] }, + Args extends { + module: M; + args: unknown[]; + onError: Function|undefined + }, >(onStop: (m: M) => unknown) { - const onNext = ({ args, module }: Args) => ({ + const onNext = ({ args, module, onError }: Args) => ({ task: () => module.execute(...args), module, + onError }); return concatMap( createResultResolver({ onStop, - createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)), + createStream: ({ args, module }) => + from(module.onEvent) + .pipe(callPlugin(args)), onNext, }), ); diff --git a/src/handlers/ready-event.ts b/src/handlers/ready-event.ts index 30d02fe..3f2fe24 100644 --- a/src/handlers/ready-event.ts +++ b/src/handlers/ready-event.ts @@ -17,10 +17,12 @@ export function startReadyEvent( return concat(ready$, buildModules(allPaths, moduleManager)) .pipe(callInitPlugins(sEmitter)) - .subscribe(module => + .subscribe(({ module, onError }) => { register(moduleManager, module) - .expect(SernError.InvalidModuleType + ' ' + util.inspect(module)) - ); + .expect(SernError.InvalidModuleType + ' ' + util.inspect(module)); + registerOnError(moduleManager, module, onError); + + }); } const once = () => pipe( @@ -28,6 +30,11 @@ const once = () => pipe( ignoreElements() ) +const registerOnError = (manager: ModuleManager, module: Processed, onError: Function|undefined) => { + if(onError) { + manager.setErrorCallback(module, onError) + } +} function register>( manager: ModuleManager, diff --git a/src/types/core-modules.ts b/src/types/core-modules.ts index 3d77e1c..d63f8a3 100644 --- a/src/types/core-modules.ts +++ b/src/types/core-modules.ts @@ -15,10 +15,12 @@ import type { UserContextMenuCommandInteraction, UserSelectMenuInteraction, } from 'discord.js'; -import { CommandType, Context, EventType } from '../../src/core'; +import { CommandType, Context, ErrorHandling, EventType } from '../../src/core'; import { AnyCommandPlugin, AnyEventPlugin, ControlPlugin, InitPlugin } from './core-plugin'; import { Awaitable, Args, SlashOptions, SernEventsMapping } from './utility'; +export type OnError = (errorHandling: ErrorHandling, err: unknown) => unknown + export interface CommandMeta { fullPath: string; id: string; diff --git a/src/types/core.ts b/src/types/core.ts index 7473aa8..c1d10ea 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -1,6 +1,7 @@ export interface ImportPayload { module: T; absPath: string; + onError: Function [key: string]: unknown; }