mirror of
https://github.com/sern-handler/handler
synced 2026-06-18 05:42:15 +00:00
wiring onError callback through module loader and resolver
This commit is contained in:
@@ -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>(
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface ImportPayload<T> {
|
||||
module: T;
|
||||
absPath: string;
|
||||
onError: Function
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user