diff --git a/src/core/ioc/base.ts b/src/core/ioc/base.ts index 459f61fe..3b1c1ab5 100644 --- a/src/core/ioc/base.ts +++ b/src/core/ioc/base.ts @@ -1,7 +1,6 @@ import type { DependencyConfiguration } from '../../types/ioc'; import { Container } from './container'; import * as __Services from '../structures/default-services'; -import { UnpackedDependencies } from '../../types/utility'; import type { Logging } from '../interfaces'; import { __add_container, __init_container, __swap_container, useContainerRaw } from './global'; import { EventEmitter } from 'node:events'; @@ -14,7 +13,7 @@ export function disposeAll(logger: Logging|undefined) { type Insertable = - | ((container: UnpackedDependencies) => object) + | ((container: Dependencies) => object) | object const dependencyBuilder = (container: Container, excluded: string[] ) => { return { @@ -26,7 +25,8 @@ const dependencyBuilder = (container: Container, excluded: string[] ) => { if(typeof v !== 'function') { container.addSingleton(key, v) } else { - container.addWiredSingleton(key, (cntr) => v(cntr as UnpackedDependencies)) + //@ts-ignore + container.addWiredSingleton(key, (cntr) => v(cntr)) } }, /** @@ -53,7 +53,6 @@ const dependencyBuilder = (container: Container, excluded: string[] ) => { type ValidDependencyConfig = | ((c: ReturnType) => any) | DependencyConfiguration; - /** * Given the user's conf, check for any excluded/included dependency keys. @@ -79,13 +78,14 @@ async function composeRoot( if (!hasLogger) { container.get('@sern/logger') - ?.info({ message: 'All dependencies loaded successfully.' }); + ?.info({ message: 'All dependencies loaded successfully.' }); } - container.ready(); + await container.ready(); } export async function makeDependencies (conf: ValidDependencyConfig) { await __init_container({ autowire: false }); + if(typeof conf === 'function') { const excluded: string[] = []; conf(dependencyBuilder(useContainerRaw(), excluded)); diff --git a/src/core/operators.ts b/src/core/operators.ts index 6fdbaf93..31780a99 100644 --- a/src/core/operators.ts +++ b/src/core/operators.ts @@ -35,14 +35,12 @@ interface PluginExecutable { * Calls any plugin with {args}. * @param args if an array, its spread and plugin called. */ -export function callPlugin(args: unknown): OperatorFunction +export function callPlugin(plugin: PluginExecutable, args: unknown) { - return concatMap(async plugin => { - if (Array.isArray(args)) { - return plugin.execute(...args); - } - return plugin.execute(args); - }); + if (Array.isArray(args)) { + return plugin.execute(...args); + } + return plugin.execute(args); } export const arrayifySource = (src: T) => @@ -52,7 +50,7 @@ export const arrayifySource = (src: T) => * Checks if the stream of results is all ok. */ export const everyPluginOk: OperatorFunction = - pipe(every(result => result.isOk()), + pipe(every(result => result.isOk()), //this shortcircuits defaultIfEmpty(true)); export const sharedEventStream = (e: Emitter, eventName: string) => diff --git a/src/core/structures/default-services.ts b/src/core/structures/default-services.ts index be2d3df1..933579e7 100644 --- a/src/core/structures/default-services.ts +++ b/src/core/structures/default-services.ts @@ -61,7 +61,7 @@ export class Cron extends EventEmitter { //@ts-ignore if(!cron.validate(module.pattern)) { - throw Error("Invalid cron expression while adding") + throw Error("Invalid cron expression while adding " + module.name) } this.modules.set(module.name!, module as CronEventCommand); } diff --git a/src/handlers/event-utils.ts b/src/handlers/event-utils.ts index 103a62a3..2ea4b193 100644 --- a/src/handlers/event-utils.ts +++ b/src/handlers/event-utils.ts @@ -20,7 +20,6 @@ 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 { CommandModule, Module, Processed } from '../types/core-modules'; -import { EventEmitter } from 'node:events'; import * as assert from 'node:assert'; import { Context } from '../core/structures/context'; import { CommandType } from '../core/structures/enums' @@ -28,7 +27,6 @@ import type { Args } from '../types/utility'; import { inspect } from 'node:util' import { disposeAll } from '../core/ioc/base'; import { arrayifySource, callPlugin, everyPluginOk, filterMapTo, handleError } from '../core/operators'; - import { resultPayload, isAutocomplete, treeSearch } from '../core/functions' function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[]) { @@ -41,13 +39,17 @@ function intoPayload(module: Module) { return pipe(map(arrayifySource), map(args => ({ module, args }))); } - const createResult = createResultResolver< Processed, { module: Processed; args: unknown[] }, unknown[] >({ - createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)), + createStream: async function* ({ module, args }) { + for(const plugin of module.onEvent) { + + } + //from(module.onEvent).pipe(callPlugin(args)) + }, onNext: ({ args }) => args, }); /** @@ -56,10 +58,10 @@ const createResult = createResultResolver< * @param source */ export function eventDispatcher(module: Module, source: unknown) { - assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`); - + assert.ok(source && typeof source === 'object', `${source} cannot be constructed into an event listener`); const execute: OperatorFunction = concatMap(async args => module.execute(...args)); + //@ts-ignore return fromEvent(source, module.name!) //@ts-ignore .pipe(intoPayload(module), @@ -67,25 +69,24 @@ export function eventDispatcher(module: Module, source: unknown) { execute); } -export function createDispatcher(payload: { module: Processed; event: BaseInteraction; }) { - assert.ok(CommandType.Text !== payload.module.type, +export function createDispatcher({ module, event }: { module: Processed; event: BaseInteraction; }) { + assert.ok(CommandType.Text !== module.type, SernError.MismatchEvent + 'Found text command in interaction stream'); - switch (payload.module.type) { + if(isAutocomplete(event)) { + assert.ok(module.type === CommandType.Slash + || module.type === CommandType.Both); + 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] }; + } + switch (module.type) { case CommandType.Slash: case CommandType.Both: { - if (isAutocomplete(payload.event)) { - const option = treeSearch(payload.event, payload.module.options); - assert.ok(option, SernError.NotSupportedInteraction + ` There is no autocomplete tag for ` + inspect(payload.module)); - const { command } = option; - - return { - module: command as Processed, //autocomplete is not a true "module" warning cast! - args: [payload.event], - }; - } - return { module: payload.module, args: contextArgs(payload.event) }; + return { module, args: contextArgs(event) }; } - default: return { module: payload.module, args: [payload.event] }; + default: return { module, args: [event] }; } } function createGenericHandler( @@ -94,8 +95,8 @@ function createGenericHandler( ) { return (pred: (i: Source) => i is Narrowed) => source.pipe( - filter(pred), - concatMap(makeModule)); + filter(pred), // only handle this stream if it passes pred + concatMap(makeModule)); // create a payload, preparing to execute } /** @@ -121,7 +122,7 @@ export function fmt(msg: string, prefix: string): string[] { */ export function createInteractionHandler( source: Observable, - mg: Map, //TODO + mg: Map, ) { return createGenericHandler, void>>( source, @@ -135,7 +136,6 @@ export function createInteractionHandler( return Err.EMPTY; } const [ path ] = fullPaths; - //@ts-ignore TODO fixme return Ok(createDispatcher({ module: path as Processed, event })); }); } @@ -147,12 +147,9 @@ export function createMessageHandler( ) { return createGenericHandler(source, async event => { const [prefix, ...rest] = fmt(event.content, defaultPrefix); - let fullPath = mg.get(`${prefix}_T`); + let fullPath = mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`); if(!fullPath) { - fullPath = mg.get(`${prefix}_B`); - if(!fullPath) { - return Err('Possibly undefined behavior: could not find a static id to resolve'); - } + return Err('Possibly undefined behavior: could not find a static id to resolve'); } return Ok({ args: contextArgs(event, rest), module: fullPath as Processed }) }); @@ -173,12 +170,13 @@ interface ExecutePayload { * @param module the module that will be executed with task * @param task the deferred execution which will be called */ -export function executeModule( +export async function executeModule( emitter: Emitter, logger: Logging|undefined, errHandler: ErrorHandling, { module, task, args }: ExecutePayload, ) { + const wrappedTask = await Result.wrapAsync(async () => task()); return of(module).pipe( //converting the task into a promise so rxjs can resolve the Awaitable properly concatMap(() => Result.wrapAsync(async () => task())), @@ -208,11 +206,11 @@ export function createResultResolver< >(config: { onStop?: (module: T) => unknown; onNext: (args: Args) => Output; - createStream: (args: Args) => Observable; + createStream: (args: Args) => AsyncGenerator; }) { return (args: Args) => { - const task$ = config.createStream(args); - return task$.pipe( + const task = config.createStream(args); + return from(task).pipe( tap(result => { result.isErr() && config.onStop?.(args.module); }), @@ -225,9 +223,7 @@ export function createResultResolver< * 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, - Args extends { module: M; args: unknown[]; }> +export function makeModuleExecutor< M extends Processed, Args extends { module: M; args: unknown[]; }> (onStop: (m: M) => unknown) { const onNext = ({ args, module }: Args) => ({ task: () => module.execute(...args), @@ -235,10 +231,17 @@ export function makeModuleExecutor< args }); return createResultResolver({ - onStop, - createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)), - onNext, - }) + onStop, + createStream: async function* ({ args, module }) { + for(const plugin of module.onEvent) { + const result = await callPlugin(plugin, args); + if(result.isErr()) { + return result.error + } + } + }, + onNext, + }) } export const handleCrash = ({ "@sern/errors": err, diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 05a0b1c4..26023474 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -20,7 +20,8 @@ function hasPrefix(prefix: string, content: string) { return (prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0); } -export default function message({"@sern/emitter": emitter, '@sern/errors':err, +export default function message( + {"@sern/emitter": emitter, '@sern/errors':err, '@sern/logger': log, '@sern/client': client, '@sern/modules': commands}: UnpackedDependencies, defaultPrefix: string | undefined) { diff --git a/src/handlers/presence.ts b/src/handlers/presence.ts index f492a696..f2528b6f 100644 --- a/src/handlers/presence.ts +++ b/src/handlers/presence.ts @@ -14,9 +14,8 @@ const parseConfig = async (conf: Promise) => { const src$ = typeof repeat === 'number' ? interval(repeat) : fromEvent(...repeat); - return src$ - .pipe(scan(onRepeat, s), - startWith(s)); + return src$.pipe(scan(onRepeat, s), + startWith(s)); } return of(s).pipe(take(1)); }) diff --git a/src/handlers/ready.ts b/src/handlers/ready.ts index 4d1d8597..b641ab92 100644 --- a/src/handlers/ready.ts +++ b/src/handlers/ready.ts @@ -7,10 +7,10 @@ import { Module } from '../types/core-modules'; import { UnpackedDependencies } from '../types/utility'; export default async function(dir: string, deps : UnpackedDependencies) { - const { "@sern/client": client, + const { '@sern/client': client, '@sern/logger': log, '@sern/emitter': sEmitter, - '@sern/modules': commands} = deps; + '@sern/modules': commands } = deps; log?.info({ message: "Waiting on discord client to be ready..." }) await once(client, "ready"); log?.info({ message: "Client signaled ready, registering modules" }); diff --git a/src/handlers/user-defined-events.ts b/src/handlers/user-defined-events.ts index 35b382cc..20982ae8 100644 --- a/src/handlers/user-defined-events.ts +++ b/src/handlers/user-defined-events.ts @@ -19,7 +19,7 @@ const intoDispatcher = (deps: UnpackedDependencies) => //@ts-ignore const cron = deps['@sern/cron']; cron.addCronModule(module); - return eventDispatcher(module, cron) + return eventDispatcher(module, cron); } default: throw Error(SernError.InvalidModuleType + ' while creating event handler'); diff --git a/src/types/ioc.ts b/src/types/ioc.ts index cac4d3f2..d3aa6711 100644 --- a/src/types/ioc.ts +++ b/src/types/ioc.ts @@ -6,11 +6,11 @@ import { Module } from './core-modules'; export interface CoreDependencies { - '@sern/client': () => Client; - '@sern/emitter': () => Contracts.Emitter; - '@sern/errors': () => Contracts.ErrorHandling; - '@sern/logger'?: () => Contracts.Logging; - '@sern/modules': () => Map + '@sern/client': Client; + '@sern/emitter': Contracts.Emitter; + '@sern/errors': Contracts.ErrorHandling; + '@sern/logger'?: Contracts.Logging; + '@sern/modules': Map; } export type DependencyFromKey = Dependencies[T]; diff --git a/test/handlers/dispatchers.test.ts b/test/handlers/dispatchers.test.ts index a5ce406f..dd62d52e 100644 --- a/test/handlers/dispatchers.test.ts +++ b/test/handlers/dispatchers.test.ts @@ -12,6 +12,7 @@ function createRandomModule(): Processed { min: CommandType.Text, max: CommandType.ChannelSelect, }), + meta: { id:"", absPath: faker.system.directoryPath() }, description: faker.string.alpha(), name: faker.string.alpha(), onEvent: [],