From 0beeb4c0646bea6ef93ccb8777c3ff1155141599 Mon Sep 17 00:00:00 2001 From: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com> Date: Sun, 19 May 2024 22:33:57 -0500 Subject: [PATCH] add deps to plugin calls and execute --- src/core/operators.ts | 1 - src/core/structures/context.ts | 2 +- src/core/structures/core-context.ts | 6 -- src/core/structures/default-services.ts | 7 +- src/handlers/event-utils.ts | 137 ++++++++++++------------ src/handlers/interaction.ts | 16 ++- src/handlers/message.ts | 16 +-- src/handlers/user-defined-events.ts | 13 ++- src/sern.ts | 3 +- 9 files changed, 99 insertions(+), 102 deletions(-) diff --git a/src/core/operators.ts b/src/core/operators.ts index 09986318..77fa2579 100644 --- a/src/core/operators.ts +++ b/src/core/operators.ts @@ -24,7 +24,6 @@ export function filterMapTo(item: () => V): OperatorFunction { return concatMap(keep => keep ? of(item()) : EMPTY); } - export const arrayifySource = (src: T) => Array.isArray(src) ? src : [src]; diff --git a/src/core/structures/context.ts b/src/core/structures/context.ts index 2b22dee0..f3aa1591 100644 --- a/src/core/structures/context.ts +++ b/src/core/structures/context.ts @@ -124,7 +124,7 @@ export class Context extends CoreContext { ); } - static override wrap(wrappable: BaseInteraction | Message, prefix?: string): Context { + static wrap(wrappable: BaseInteraction | Message, prefix?: string): Context { if ('interaction' in wrappable) { return new Context(Ok(wrappable), prefix); } diff --git a/src/core/structures/core-context.ts b/src/core/structures/core-context.ts index e5eed997..8ffd6a08 100644 --- a/src/core/structures/core-context.ts +++ b/src/core/structures/core-context.ts @@ -23,10 +23,4 @@ export abstract class CoreContext { public isSlash(): this is CoreContext { return !this.isMessage(); } - //todo: add agnostic options resolver for Context - abstract get options(): unknown; - - static wrap(_: unknown): unknown { - throw Error('You need to override this method; cannot wrap an abstract class'); - } } diff --git a/src/core/structures/default-services.ts b/src/core/structures/default-services.ts index 0981b9cb..06b403df 100644 --- a/src/core/structures/default-services.ts +++ b/src/core/structures/default-services.ts @@ -65,10 +65,9 @@ export class Cron implements Emitter { const retrievedModule = this.modules.get(eventName); if(!retrievedModule) throw Error("Adding task: module " +eventName +"was not found"); const { pattern, name, runOnInit, timezone } = retrievedModule; - const task = cron.schedule(pattern, - (date) => listener({ date, deps: this.deps }), - { name, runOnInit, timezone, scheduled: true }); - task.on('task-failed', console.error) + cron.schedule(pattern, + (date) => listener({ date, deps: this.deps }), + { name, runOnInit, timezone, scheduled: true }); return this; } removeListener(eventName: string | symbol, listener: AnyFunction) { diff --git a/src/handlers/event-utils.ts b/src/handlers/event-utils.ts index 1a529536..474b6486 100644 --- a/src/handlers/event-utils.ts +++ b/src/handlers/event-utils.ts @@ -4,19 +4,20 @@ import { Observable, concatMap, filter, - of, throwError, - fromEvent, map, OperatorFunction, + fromEvent, + map, + type OperatorFunction, catchError, finalize, pipe, + from, } from 'rxjs'; import * as Id from '../core/id' -import type { Emitter, ErrorHandling, Logging } from '../core/interfaces'; +import type { Emitter } from '../core/interfaces'; import { PayloadType, SernError } from '../core/structures/enums' import { Err, Ok, Result } from 'ts-results-es'; -import type { Awaitable, UnpackedDependencies, VoidResult } from '../types/utility'; -import type { ControlPlugin } from '../types/core-plugin'; +import type { UnpackedDependencies, VoidResult } from '../types/utility'; import type { CommandModule, Module, Processed } from '../types/core-modules'; import * as assert from 'node:assert'; import { Context } from '../core/structures/context'; @@ -26,59 +27,75 @@ import { disposeAll } from '../core/ioc/base'; import { arrayifySource, handleError } from '../core/operators'; import { resultPayload, isAutocomplete, treeSearch } from '../core/functions' -function intoPayload(module: Module) { +interface ExecutePayload { + module: Module; + args: unknown[]; + deps: Dependencies + [key: string]: unknown +} + +function intoPayload(module: Module, deps: Dependencies) { return pipe(map(arrayifySource), - map(args => ({ module, args }))); + map(args => ({ module, args, deps }))); } -const createResult = createResultResolver< - Processed, - { module: Processed; args: unknown[] }, - unknown[] ->({ onNext: (p) => p.args, }); +const createResult = (deps: Dependencies) => + createResultResolver({ + onNext: (p) => p.args, + onStop: (module) => { + //maybe do something when plugins fail? + } + }); /** * Creates an observable from { source } * @param module * @param source */ -export function eventDispatcher(module: Module, source: unknown) { +export function eventDispatcher(deps: Dependencies, module: Module, source: unknown) { assert.ok(source && typeof source === 'object', `${source} cannot be constructed into an event listener`); - const execute: OperatorFunction = - concatMap(async args => module.execute(...args)); + const execute: OperatorFunction = + concatMap(async args => { + if(args) + return module.execute.apply(null, args); + }); //@ts-ignore return fromEvent(source, module.name!) - //@ts-ignore - .pipe(intoPayload(module), - concatMap(createResult), + .pipe(intoPayload(module, deps), + concatMap(createResult(deps)), execute); } interface DispatchPayload { module: Processed; event: BaseInteraction; - defaultPrefix?: string + defaultPrefix?: string; + deps: Dependencies }; -export function createDispatcher({ module, event, defaultPrefix }: DispatchPayload) { +export function createDispatcher({ module, event, defaultPrefix, deps }: DispatchPayload): ExecutePayload { assert.ok(CommandType.Text !== module.type, SernError.MismatchEvent + 'Found text command in interaction stream'); + if(isAutocomplete(event)) { assert.ok(module.type === CommandType.Slash - || module.type === CommandType.Both); + || module.type === CommandType.Both, "Autocomplete option on non command interaction"); const option = treeSearch(event, module.options); assert.ok(option, SernError.NotSupportedInteraction + ` There is no autocomplete tag for ` + inspect(module)); const { command } = option; return { module: command as Processed, //autocomplete is not a true "module" warning cast! - args: [event] }; + args: [event], + deps }; } + switch (module.type) { case CommandType.Slash: case CommandType.Both: { return { module, - args: [Context.wrap(event, defaultPrefix)] + args: [Context.wrap(event, defaultPrefix)], + deps }; } - default: return { module, args: [event] }; + default: return { module, args: [event], deps }; } } function createGenericHandler( @@ -114,25 +131,27 @@ export function fmt(msg: string, prefix: string): string[] { */ export function createInteractionHandler( source: Observable, - mg: Map, + deps: Dependencies, defaultPrefix?: string ) { + const mg = deps['@sern/modules'] return createGenericHandler, void>>( source, async event => { const possibleIds = Id.reconstruct(event); - let fullPaths= possibleIds + let modules = possibleIds .map(id => mg.get(id)) .filter((id): id is Module => id !== undefined); - if(fullPaths.length == 0) { + if(modules.length == 0) { return Err.EMPTY; } - const [ path ] = fullPaths; + const [ module ] = modules; return Ok(createDispatcher({ - module: path as Processed, + module: module as Processed, event, - defaultPrefix + defaultPrefix, + deps })); }); } @@ -140,24 +159,20 @@ export function createInteractionHandler( export function createMessageHandler( source: Observable, defaultPrefix: string, - mg: Map, + deps: Dependencies ) { + const mg = deps['@sern/modules']; return createGenericHandler(source, async event => { const [prefix] = fmt(event.content, defaultPrefix); let module= mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`) as Module; if(!module) { return Err('Possibly undefined behavior: could not find a static id to resolve'); } - return Ok({ args: [Context.wrap(event, defaultPrefix)], module }) + return Ok({ args: [Context.wrap(event, defaultPrefix)], module, deps }) }); } - -interface ExecutePayload { - module: Processed; - task: () => Awaitable; -} /** * Wraps the task in a Result as a try / catch. * if the task is ok, an event is emitted and the stream becomes empty @@ -169,20 +184,16 @@ interface ExecutePayload { */ export function executeModule( emitter: Emitter, - logger: Logging|undefined, - errHandler: ErrorHandling, - { module, task }: ExecutePayload, + { module, args }: ExecutePayload, ) { - return of(module).pipe( - //converting the task into a promise so rxjs can resolve the Awaitable properly - concatMap(() => Result.wrapAsync(async () => task())), - concatMap(result => { + return from(Result.wrapAsync(async () => module.execute(...args))) + .pipe(concatMap(result => { if (result.isOk()) { emitter.emit('module.activate', resultPayload(PayloadType.Success, module)); return EMPTY; - } + } return throwError(() => resultPayload(PayloadType.Failure, module, result.error)); - })); + })) }; /** @@ -194,29 +205,25 @@ export function executeModule( * @param config * @returns receiver function for flattening a stream of data */ -export function createResultResolver< - T extends { execute: (...args: any[]) => any; onEvent: ControlPlugin[] }, - Args extends { module: T; [key: string]: unknown }, - Output, ->(config: { - onStop?: (module: T) => unknown; - onNext: (args: Args, map: Record) => Output; +export function createResultResolver(config: { + onStop?: (module: Module) => unknown; + onNext: (args: ExecutePayload, map: Record) => Output; }) { - return async (payload: Args) => { - //@ts-ignore fix later + const { onStop, onNext } = config; + return async (payload: ExecutePayload) => { const task = await callPlugins(payload); if(task.isOk()) { - return config.onNext(payload, task.value) as ExecutePayload; + return onNext(payload, task.value) as Output; } else { - config.onStop?.(payload.module); + onStop?.(payload.module); } }; }; -async function callPlugins({ args, module }: { args: unknown[], module: Module }) { +async function callPlugins({ args, module, deps }: ExecutePayload) { let state = {}; for(const plugin of module.onEvent) { - const result = await plugin.execute.apply(null, arrayifySource(args)); + const result = await plugin.execute(...args, { state, deps }); if(result.isErr()) { return result; } @@ -230,11 +237,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 -(onStop: (m: M) => unknown) { - const onNext = ({ args, module }: Args, state: Record) => ({ - task: () => module.execute(...args, state), +export function intoTask(onStop: (m: Module) => unknown) { + const onNext = ({ args, module, deps }: ExecutePayload, state: Record) => ({ module, + args: [...args, { state }], + deps }); return createResultResolver({ onStop, onNext }) } @@ -243,8 +250,6 @@ export const handleCrash = ({ "@sern/errors": err, '@sern/emitter': sem, '@sern/logger': log } : UnpackedDependencies) => pipe(catchError(handleError(err, sem, log)), finalize(() => { - log?.info({ - message: 'A stream closed or reached end of lifetime', - }); + log?.info({ message: 'A stream closed or reached end of lifetime' }); disposeAll(log); })) diff --git a/src/handlers/interaction.ts b/src/handlers/interaction.ts index 5e4c54ea..b56debde 100644 --- a/src/handlers/interaction.ts +++ b/src/handlers/interaction.ts @@ -2,7 +2,7 @@ import type { Interaction } from 'discord.js'; import { mergeMap, merge, concatMap, EMPTY } from 'rxjs'; import { PayloadType } from '../core/structures/enums'; import { filterTap, sharedEventStream } from '../core/operators' -import { createInteractionHandler, executeModule, makeModuleExecutor } from './event-utils'; +import { createInteractionHandler, executeModule, intoTask, } from './event-utils'; import { SernError } from '../core/structures/enums' import { isAutocomplete, isCommand, isMessageComponent, isModal, resultPayload, } from '../core/functions' import { UnpackedDependencies } from '../types/utility'; @@ -10,13 +10,10 @@ import { Emitter } from '../core/interfaces'; export default function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: string) { //i wish javascript had clojure destructuring - const { '@sern/modules': modules, - '@sern/client': client, - '@sern/logger': log, - '@sern/errors': err, + const { '@sern/client': client, '@sern/emitter': emitter } = deps const interactionStream$ = sharedEventStream(client as unknown as Emitter, 'interactionCreate'); - const handle = createInteractionHandler(interactionStream$, modules, defaultPrefix); + const handle = createInteractionHandler(interactionStream$, deps, defaultPrefix); const interactionHandler$ = merge(handle(isMessageComponent), handle(isAutocomplete), @@ -24,11 +21,12 @@ export default function interactionHandler(deps: UnpackedDependencies, defaultPr handle(isModal)); return interactionHandler$ .pipe(filterTap(e => emitter.emit('warning', resultPayload(PayloadType.Warning, undefined, e))), - concatMap(makeModuleExecutor(module => - emitter.emit('module.activate', resultPayload(PayloadType.Failure, module, SernError.PluginFailure)))), + concatMap(intoTask(module => { + emitter.emit('module.activate', resultPayload(PayloadType.Failure, module, SernError.PluginFailure)) + })), mergeMap(payload => { if(payload) - return executeModule(emitter, log, err, payload) + return executeModule(emitter, payload) return EMPTY; })); } diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 5005ec21..dc22fd53 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -1,6 +1,6 @@ import { EMPTY, mergeMap, concatMap } from 'rxjs'; import type { Message } from 'discord.js'; -import { createMessageHandler, executeModule, makeModuleExecutor } from './event-utils'; +import { createMessageHandler, executeModule, intoTask } from './event-utils'; import { PayloadType, SernError } from '../core/structures/enums' import { resultPayload } from '../core/functions' import { filterTap, sharedEventStream } from '../core/operators' @@ -21,29 +21,31 @@ function hasPrefix(prefix: string, content: string) { } export default function message( - {"@sern/emitter": emitter, '@sern/errors':err, - '@sern/logger': log, '@sern/client': client, - '@sern/modules': commands}: UnpackedDependencies, + deps: UnpackedDependencies, defaultPrefix: string | undefined ) { + const {"@sern/emitter": emitter, + '@sern/logger': log, + '@sern/client': client } = deps + if (!defaultPrefix) { log?.debug({ message: 'No prefix found. message handler shutting down' }); return EMPTY; } const messageStream$ = sharedEventStream(client as unknown as Emitter, 'messageCreate'); - const handle = createMessageHandler(messageStream$, defaultPrefix, commands); + const handle = createMessageHandler(messageStream$, defaultPrefix, deps); const msgCommands$ = handle(isNonBot(defaultPrefix)); return msgCommands$.pipe( filterTap((e) => emitter.emit('warning', resultPayload(PayloadType.Warning, undefined, e))), - concatMap(makeModuleExecutor(module => { + concatMap(intoTask(module => { const result = resultPayload(PayloadType.Failure, module, SernError.PluginFailure); emitter.emit('module.activate', result); })), mergeMap(payload => { if(payload) - return executeModule(emitter, log, err, payload) + return executeModule(emitter, payload) return EMPTY; })); } diff --git a/src/handlers/user-defined-events.ts b/src/handlers/user-defined-events.ts index c58cff42..f6276da4 100644 --- a/src/handlers/user-defined-events.ts +++ b/src/handlers/user-defined-events.ts @@ -10,19 +10,18 @@ const intoDispatcher = (deps: UnpackedDependencies) => (module : EventModule) => { switch (module.type) { case EventType.Sern: - return eventDispatcher(module, deps['@sern/emitter']); + return eventDispatcher(deps, module, deps['@sern/emitter']); case EventType.Discord: - return eventDispatcher(module, deps['@sern/client']); + return eventDispatcher(deps, module, deps['@sern/client']); case EventType.External: - return eventDispatcher(module, deps[module.emitter]); + return eventDispatcher(deps, module, deps[module.emitter]); case EventType.Cron: { //@ts-ignore const cron = deps['@sern/cron']; cron.addCronModule(module); - return eventDispatcher(module, cron); + return eventDispatcher(deps, module, cron); } - default: - throw Error(SernError.InvalidModuleType + ' while creating event handler'); + default: throw Error(SernError.InvalidModuleType + ' while creating event handler'); } }; @@ -33,7 +32,7 @@ export default async function(deps: UnpackedDependencies, eventDir: string) { for(const plugin of module.plugins) { const res = await plugin.execute({ module, - absPath: module.meta.absPath , + absPath: module.meta.absPath, updateModule: (partial: Partial) => { module = { ...module, ...partial }; return module; diff --git a/src/sern.ts b/src/sern.ts index 41443aae..3cd067de 100644 --- a/src/sern.ts +++ b/src/sern.ts @@ -9,6 +9,7 @@ import { presenceHandler } from './handlers/presence'; import { handleCrash } from './handlers/event-utils'; import { useContainerRaw } from './core/ioc/global'; import { UnpackedDependencies } from './types/utility'; +import type { PresenceResult } from './core/presences'; interface Wrapper { commands: string; @@ -49,7 +50,7 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) { const time = ((performance.now() - startTime) / 1000).toFixed(2); deps['@sern/logger']?.info({ message: `sern: registered in ${time} s` }); if(presencePath.exists) { - const setPresence = async (p: any) => { + const setPresence = async (p: PresenceResult) => { return deps['@sern/client'].user?.setPresence(p); } presenceHandler(presencePath.path, setPresence).subscribe();