simplify plugin args and prepare for reduction among plugins

This commit is contained in:
Jacob Nguyen
2024-05-17 23:13:12 -05:00
parent 960f90c544
commit 699adf276c
9 changed files with 81 additions and 159 deletions

View File

@@ -1,5 +1,5 @@
import { CommandType, EventType, PluginType } from './structures/enums';
import type { Plugin, PluginResult, EventArgs, CommandArgs } from '../types/core-plugin';
import type { Plugin, PluginResult, EventArgs, CommandArgs, InitArgs } from '../types/core-plugin';
import type { ClientEvents } from 'discord.js';
import { err, ok } from './functions';
@@ -12,16 +12,14 @@ export function makePlugin<V extends unknown[]>(
/**
* @since 2.5.0
*/
export function EventInitPlugin<I extends EventType>(
execute: (...args: EventArgs<I, PluginType.Init>) => PluginResult,
) {
export function EventInitPlugin(execute: (args: InitArgs) => PluginResult) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*/
export function CommandInitPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I, PluginType.Init>) => PluginResult,
execute: (args: InitArgs) => PluginResult
) {
return makePlugin(PluginType.Init, execute);
}
@@ -29,7 +27,7 @@ export function CommandInitPlugin<I extends CommandType>(
* @since 2.5.0
*/
export function CommandControlPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I, PluginType.Control>) => PluginResult,
execute: (...args: CommandArgs<I>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}
@@ -37,7 +35,7 @@ export function CommandControlPlugin<I extends CommandType>(
* @since 2.5.0
*/
export function EventControlPlugin<I extends EventType>(
execute: (...args: EventArgs<I, PluginType.Control>) => PluginResult,
execute: (...args: EventArgs<I>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}

View File

@@ -22,7 +22,6 @@ export const shouldHandle = (pth: string, filenam: string) => {
}
/**
* Import any module based on the absolute path.
* This can accept four types of exported modules
@@ -47,7 +46,6 @@ export async function importModule<T>(absPath: string) {
const p = path.parse(absPath)
commandModule.name ??= p.name; commandModule.description ??= "...";
commandModule.meta = {
//@ts-ignore
id: Id.create(commandModule.name, commandModule.type),
absPath,
};

View File

@@ -26,11 +26,6 @@ import { disposeAll } from '../core/ioc/base';
import { arrayifySource, handleError } from '../core/operators';
import { resultPayload, isAutocomplete, treeSearch } from '../core/functions'
function contextArgs(wrappable: Message | BaseInteraction, prefix?: string) {
const ctx = Context.wrap(wrappable, prefix);
return [ctx] as [Context];
}
function intoPayload(module: Module) {
return pipe(map(arrayifySource),
map(args => ({ module, args })));
@@ -39,11 +34,7 @@ const createResult = createResultResolver<
Processed<Module>,
{ module: Processed<Module>; args: unknown[] },
unknown[]
>({
//@ts-ignore fix later
callPlugins,
onNext: (p) => p.args,
});
>({ onNext: (p) => p.args, });
/**
* Creates an observable from { source }
* @param module
@@ -62,9 +53,12 @@ export function eventDispatcher(module: Module, source: unknown) {
execute);
}
export function createDispatcher(
{ module, event }: { module: Processed<CommandModule>; event: BaseInteraction; }
) {
interface DispatchPayload {
module: Processed<CommandModule>;
event: BaseInteraction;
defaultPrefix?: string
};
export function createDispatcher({ module, event, defaultPrefix }: DispatchPayload) {
assert.ok(CommandType.Text !== module.type,
SernError.MismatchEvent + 'Found text command in interaction stream');
if(isAutocomplete(event)) {
@@ -79,7 +73,10 @@ export function createDispatcher(
switch (module.type) {
case CommandType.Slash:
case CommandType.Both: {
return { module, args: contextArgs(event) };
return {
module,
args: [Context.wrap(event, defaultPrefix)]
};
}
default: return { module, args: [event] };
}
@@ -132,14 +129,18 @@ export function createInteractionHandler<T extends Interaction>(
return Err.EMPTY;
}
const [ path ] = fullPaths;
return Ok(createDispatcher({ module: path as Processed<CommandModule>, event }));
return Ok(createDispatcher({
module: path as Processed<CommandModule>,
event,
defaultPrefix
}));
});
}
export function createMessageHandler(
source: Observable<Message>,
defaultPrefix: string,
mg: any,
mg: Map<string, Module>,
) {
return createGenericHandler(source, async event => {
const [prefix] = fmt(event.content, defaultPrefix);
@@ -156,7 +157,6 @@ export function createMessageHandler(
interface ExecutePayload {
module: Processed<Module>;
task: () => Awaitable<unknown>;
args: unknown[]
}
/**
* Wraps the task in a Result as a try / catch.
@@ -171,7 +171,7 @@ export function executeModule(
emitter: Emitter,
logger: Logging|undefined,
errHandler: ErrorHandling,
{ module, task, args }: ExecutePayload,
{ module, task }: ExecutePayload,
) {
return of(module).pipe(
//converting the task into a promise so rxjs can resolve the Awaitable properly
@@ -200,15 +200,15 @@ export function createResultResolver<
Output,
>(config: {
onStop?: (module: T) => unknown;
onNext: (args: Args) => Output;
onNext: (args: Args, map: Record<string, unknown>) => Output;
}) {
return async (args: Args) => {
return async (payload: Args) => {
//@ts-ignore fix later
const task = await callPlugins(args);
const task = await callPlugins(payload);
if(task.isOk()) {
return config.onNext(args) as ExecutePayload;
return config.onNext(payload, task.value) as ExecutePayload;
} else {
config.onStop?.(args.module);
config.onStop?.(payload.module);
}
};
};
@@ -220,8 +220,7 @@ async function callPlugins({ args, module }: { args: unknown[], module: Module }
if(result.isErr()) {
return result;
}
if(typeof result.value === 'object') {
//@ts-ignore TODO
if(typeof result.value === 'object' && result.value !== null) {
state = { ...result.value, ...state };
}
}
@@ -231,12 +230,11 @@ async function callPlugins({ args, module }: { args: unknown[], module: Module }
* Creates an executable task ( execute the command ) if all control plugins are successful
* @param onStop emits a failure response to the SernEmitter
*/
export function makeModuleExecutor< M extends Processed<Module>, Args extends { module: M; args: unknown[]; }>
export function makeModuleExecutor<M extends Module, Args extends { module: M; args: unknown[]; }>
(onStop: (m: M) => unknown) {
const onNext = ({ args, module }: Args) => ({
task: () => module.execute(...args),
const onNext = ({ args, module }: Args, state: Record<string, unknown>) => ({
task: () => module.execute(...args, state),
module,
args
});
return createResultResolver({ onStop, onNext })
}

View File

@@ -21,7 +21,7 @@ export default async function(dir: string, deps : UnpackedDependencies) {
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 an incorrect \`type\``);
throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``);
}
for(const plugin of module.plugins) {
const res = await plugin.execute({
@@ -37,8 +37,8 @@ export default async function(dir: string, deps : UnpackedDependencies) {
throw Error("Plugin failed with controller.stop()");
}
}
Object.freeze(module); // no more writing!!
commands.set(module.meta.id, module);
// FREEZE! no more writing!!
commands.set(module.meta.id, Object.freeze(module));
sEmitter.emit('module.register', resultPayload(PayloadType.Success, module));
}
sEmitter.emit('modulesLoaded');

View File

@@ -29,9 +29,16 @@ const intoDispatcher = (deps: UnpackedDependencies) =>
export default async function(deps: UnpackedDependencies, eventDir: string) {
const eventModules: EventModule[] = [];
for await (const path of Files.readRecursive(eventDir)) {
const { module } = await Files.importModule<Module>(path);
let { module } = await Files.importModule<Module>(path);
for(const plugin of module.plugins) {
const res = await plugin.execute({ module, absPath: module.meta.absPath });
const res = await plugin.execute({
module,
absPath: module.meta.absPath ,
updateModule: (partial: Partial<Module>) => {
module = { ...module, ...partial };
return module;
}
});
if(res.isErr()) {
deps['@sern/emitter'].emit('module.register', resultPayload(PayloadType.Failure, module, SernError.PluginFailure));
throw Error("Plugin failed with controller.stop()");

View File

@@ -37,7 +37,7 @@ export type {
} from './types/core-plugin';
export type { Args, SlashOptions, Payload, SernEventsMapping } from './types/utility';
export type { Payload, SernEventsMapping } from './types/utility';
export type { CoreDependencies } from './types/ioc';
export {

View File

@@ -18,9 +18,11 @@ import type {
import type { CommandType, EventType } from '../core/structures/enums';
import { Context } from '../core/structures/context'
import { AnyCommandPlugin, AnyEventPlugin, ControlPlugin, InitPlugin } from './core-plugin';
import { Awaitable, Args, SlashOptions, SernEventsMapping } from './utility';
import { Awaitable, SernEventsMapping } from './utility';
type ToBeDecided = {
result: Record<string,unknown>;
}
export type Processed<T> = T & { name: string; description: string };
export interface Module {
@@ -70,42 +72,41 @@ export interface ContextMenuMsg extends Module {
export interface ButtonCommand extends Module {
type: CommandType.Button;
execute: (ctx: ButtonInteraction) => Awaitable<unknown>;
execute: (ctx: ButtonInteraction, tbd: ToBeDecided) => Awaitable<unknown>;
}
export interface StringSelectCommand extends Module {
type: CommandType.StringSelect;
execute: (ctx: StringSelectMenuInteraction) => Awaitable<unknown>;
execute: (ctx: StringSelectMenuInteraction, tbd: ToBeDecided) => Awaitable<unknown>;
}
export interface ChannelSelectCommand extends Module {
type: CommandType.ChannelSelect;
execute: (ctx: ChannelSelectMenuInteraction) => Awaitable<unknown>;
execute: (ctx: ChannelSelectMenuInteraction, tbd: ToBeDecided) => Awaitable<unknown>;
}
export interface RoleSelectCommand extends Module {
type: CommandType.RoleSelect;
execute: (ctx: RoleSelectMenuInteraction) => Awaitable<unknown>;
execute: (ctx: RoleSelectMenuInteraction, tbd: ToBeDecided) => Awaitable<unknown>;
}
export interface MentionableSelectCommand extends Module {
type: CommandType.MentionableSelect;
execute: (ctx: MentionableSelectMenuInteraction) => Awaitable<unknown>;
execute: (ctx: MentionableSelectMenuInteraction, tbd: ToBeDecided) => Awaitable<unknown>;
}
export interface UserSelectCommand extends Module {
type: CommandType.UserSelect;
execute: (ctx: UserSelectMenuInteraction) => Awaitable<unknown>;
execute: (ctx: UserSelectMenuInteraction, tbd: ToBeDecided) => Awaitable<unknown>;
}
export interface ModalSubmitCommand extends Module {
type: CommandType.Modal;
execute: (ctx: ModalSubmitInteraction) => Awaitable<unknown>;
execute: (ctx: ModalSubmitInteraction, tbd: ToBeDecided) => Awaitable<unknown>;
}
export interface AutocompleteCommand
extends Omit<Module, 'name' | 'type' | 'plugins' | 'description' | 'meta'> {
onEvent: ControlPlugin[];
export interface AutocompleteCommand {
onEvent?: ControlPlugin[];
execute: (ctx: AutocompleteInteraction) => Awaitable<unknown>;
}

View File

@@ -13,27 +13,10 @@
import type { Err, Ok, Result } from 'ts-results-es';
import type {
BothCommand,
ButtonCommand,
ChannelSelectCommand,
CommandModule,
ContextMenuMsg,
ContextMenuUser,
DiscordEventCommand,
EventModule,
ExternalEventCommand,
MentionableSelectCommand,
ModalSubmitCommand,
Module,
Processed,
RoleSelectCommand,
SernEventCommand,
SlashCommand,
StringSelectCommand,
TextCommand,
UserSelectCommand,
} from './core-modules';
import type { Args, Awaitable, Payload, SlashOptions } from './utility';
import type { Awaitable, Payload } from './utility';
import type { CommandType, EventType, PluginType } from '../core/structures/enums'
import type { Context } from '../core/structures/context'
import type {
@@ -51,14 +34,14 @@ import type {
export type PluginResult = Awaitable<Result<unknown, unknown>>;
export interface InitArgs<T extends Processed<Module>> {
export interface InitArgs<T extends Processed<Module> = Processed<Module>> {
module: T;
absPath: string;
updateModule: (module: Partial<T>) => T
}
export interface Controller {
next: () => Ok<void>;
stop: () => Err<void>;
next: () => Ok<unknown>;
stop: () => Err<string|undefined>;
}
export interface Plugin<Args extends any[] = any[]> {
type: PluginType;
@@ -77,81 +60,27 @@ export interface ControlPlugin<Args extends any[] = any[]> {
export type AnyCommandPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<Module>>]>;
export type AnyEventPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<Module>>]>;
export type CommandArgs<
I extends CommandType = CommandType,
J extends PluginType = PluginType,
> = CommandArgsMatrix[I][J];
export type EventArgs< I extends EventType = EventType,
J extends PluginType = PluginType,
> = EventArgsMatrix[I][J];
export type CommandArgs<I extends CommandType = CommandType > = CommandArgsMatrix[I]
export type EventArgs<I extends EventType = EventType> = EventArgsMatrix[I]
interface CommandArgsMatrix {
[CommandType.Text]: {
[PluginType.Control]: [Context, ['text', string[]]];
[PluginType.Init]: [InitArgs<Processed<Module>>];
};
[CommandType.Slash]: {
[PluginType.Control]: [Context, ['slash', /* library coupled */ SlashOptions]];
[PluginType.Init]: [InitArgs<Processed<Module>>];
};
[CommandType.Both]: {
[PluginType.Control]: [Context, Args];
[PluginType.Init]: [InitArgs<Processed<Module>>];
};
[CommandType.CtxMsg]: {
[PluginType.Control]: [/* library coupled */ MessageContextMenuCommandInteraction];
[PluginType.Init]: [InitArgs<Processed<Module>>];
};
[CommandType.CtxUser]: {
[PluginType.Control]: [/* library coupled */ UserContextMenuCommandInteraction];
[PluginType.Init]: [InitArgs<Processed<Module>>];
};
[CommandType.Button]: {
[PluginType.Control]: [/* library coupled */ ButtonInteraction];
[PluginType.Init]: [InitArgs<Processed<Module>>];
};
[CommandType.StringSelect]: {
[PluginType.Control]: [/* library coupled */ StringSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<Module>>];
};
[CommandType.RoleSelect]: {
[PluginType.Control]: [/* library coupled */ RoleSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<Module>>];
};
[CommandType.ChannelSelect]: {
[PluginType.Control]: [/* library coupled */ ChannelSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<Module>>];
};
[CommandType.MentionableSelect]: {
[PluginType.Control]: [/* library coupled */ MentionableSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<Module>>];
};
[CommandType.UserSelect]: {
[PluginType.Control]: [/* library coupled */ UserSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<Module>>];
};
[CommandType.Modal]: {
[PluginType.Control]: [/* library coupled */ ModalSubmitInteraction];
[PluginType.Init]: [InitArgs<Processed<Module>>];
};
[CommandType.Text]: [Context];
[CommandType.Slash]: [Context];
[CommandType.Both]: [Context];
[CommandType.CtxMsg]: [MessageContextMenuCommandInteraction];
[CommandType.CtxUser]: [UserContextMenuCommandInteraction];
[CommandType.Button]: [ButtonInteraction];
[CommandType.StringSelect]: [StringSelectMenuInteraction];
[CommandType.RoleSelect]: [RoleSelectMenuInteraction];
[CommandType.ChannelSelect]: [ChannelSelectMenuInteraction];
[CommandType.MentionableSelect]: [MentionableSelectMenuInteraction];
[CommandType.UserSelect]: [UserSelectMenuInteraction];
[CommandType.Modal]: [ModalSubmitInteraction];
}
interface EventArgsMatrix {
[EventType.Discord]: {
[PluginType.Control]: /* library coupled */ ClientEvents[keyof ClientEvents];
[PluginType.Init]: [InitArgs<Processed<DiscordEventCommand>>];
};
[EventType.Sern]: {
[PluginType.Control]: [Payload];
[PluginType.Init]: [InitArgs<Processed<SernEventCommand>>];
};
[EventType.External]: {
[PluginType.Control]: unknown[];
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
};
[EventType.Cron]: {
[PluginType.Control]: unknown[];
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
};
[EventType.Discord]: ClientEvents[keyof ClientEvents];
[EventType.Sern]: [Payload];
[EventType.External]: unknown[];
[EventType.Cron]: unknown[];
}

View File

@@ -1,4 +1,4 @@
import type { CommandInteractionOptionResolver, InteractionReplyOptions, MessageReplyOptions } from 'discord.js';
import type { InteractionReplyOptions, MessageReplyOptions } from 'discord.js';
import type { PayloadType } from '../core/structures/enums';
import type { Module } from './core-modules';
import type { Result } from 'ts-results-es';
@@ -8,15 +8,6 @@ export type Awaitable<T> = PromiseLike<T> | T;
export type VoidResult = Result<void, void>;
export type AnyFunction = (...args: any[]) => unknown;
// Thanks to @kelsny
type ParseType<T> = {
[K in keyof T]: T[K] extends unknown ? [k: K, args: T[K]] : never;
}[keyof T];
export type SlashOptions = Omit<CommandInteractionOptionResolver, 'getMessage' | 'getFocused'>;
export type Args = ParseType<{ text: string[]; slash: SlashOptions }>;
export interface SernEventsMapping {
'module.register': [Payload];
'module.activate': [Payload];