wiring onError callback through module loader and resolver

This commit is contained in:
Jacob Nguyen
2023-09-08 23:11:46 -05:00
parent 59e7927816
commit cafca503f9
8 changed files with 73 additions and 61 deletions

View File

@@ -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<CommandModule[]>;
getByNameCommandType<T extends CommandType>(

View File

@@ -6,4 +6,5 @@ import type { CommandMeta, Module } from '../../types/core-modules';
export interface CoreModuleStore {
commands: Map<string, string>;
metadata: WeakMap<Module, CommandMeta>;
onError: WeakMap<Module, Function>;
}

View File

@@ -36,15 +36,14 @@ export async function importModule<T>(absPath: string) {
.wrap(() => ({ module: commandModule.getInstance(), onError }))
.unwrapOr({ module: commandModule, onError }) as T;
}
interface FileModuleImports<T extends Module> {
module: T,
interface FileExtras {
onError : Function
}
export async function defaultModuleLoader<T extends Module>(absPath: string): ModuleResult<T> {
let module = await importModule<T>(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<T extends Module>(
input: ObservableInput<string>,
): Observable<ImportPayload<T>> {
return from(input).pipe(mergeMap(defaultModuleLoader<T>));
return from(input)
.pipe(mergeMap(defaultModuleLoader<T>));
}
export const getFullPathTree = (dir: string) => readPaths(resolve(dir));

View File

@@ -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<T extends CommandModule, V extends BaseInteraction | Message>(
payload: { module: Processed<T>; 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<CommandModule>, args: [Context, Args]) {
return {
@@ -31,21 +22,6 @@ export function dispatchMessage(module: Processed<CommandModule>, args: [Context
};
}
function dispatchAutocomplete(payload: {
module: Processed<BothCommand>;
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<Module>, //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<T extends BaseInteraction>(interaction: T) {
return [interaction] as [T];
}
function intoPayload(module: Processed<Module>) {
function intoPayload(module: Processed<Module>, onError: Function|undefined) {
return pipe(
arrayifySource,
map(args => ({ module, args })),
map(args => ({ module, args, onError })),
);
}
const createResult = createResultResolver<
Processed<Module>,
{ module: Processed<Module>; args: unknown[] },
{ module: Processed<Module>; 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<Module>, source: unknown) {
export function eventDispatcher(module: Processed<Module>, onError: Function|undefined, source: unknown) {
assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`);
const execute: OperatorFunction<unknown[], unknown> = 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<Module>, source: unknown) {
export function createDispatcher(payload: {
module: Processed<CommandModule>;
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<Module>, //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
}
}
}

View File

@@ -78,8 +78,7 @@ export function createInteractionHandler<T extends Interaction>(
return Files
.defaultModuleLoader<Processed<CommandModule>>(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<T extends Processed<AnyModule>>(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<T extends Processed<AnyModule>>(sernEmitter: Emi
*/
export function makeModuleExecutor<
M extends Processed<Module>,
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,
}),
);

View File

@@ -17,10 +17,12 @@ export function startReadyEvent(
return concat(ready$, buildModules<AnyModule>(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<AnyModule>, onError: Function|undefined) => {
if(onError) {
manager.setErrorCallback(module, onError)
}
}
function register<T extends Processed<AnyModule>>(
manager: ModuleManager,

View File

@@ -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;

View File

@@ -1,6 +1,7 @@
export interface ImportPayload<T> {
module: T;
absPath: string;
onError: Function
[key: string]: unknown;
}