diff --git a/src/handler/events/dispatchers.ts b/src/handler/events/dispatchers.ts new file mode 100644 index 0000000..1eeb856 --- /dev/null +++ b/src/handler/events/dispatchers.ts @@ -0,0 +1,118 @@ +import type { Processed } from '../../types/core'; +import type { BothCommand, CommandModule, Module } from '../../types/module'; +import { EventEmitter } from 'node:events'; +import * as assert from 'node:assert'; +import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs'; +import { arrayifySource, callPlugin } from '../../core/operators'; +import { createResultResolver } from './generic'; +import { AutocompleteInteraction, BaseInteraction, Message } from 'discord.js'; +import { treeSearch } from '../../core/functions'; +import { SernError } from '../../core/structures/errors'; +import { Args } from '../../types/handler'; +import { CommandType, Context } from '../../core'; +import { isAutocomplete } from '../../core/predicates'; + +export function dispatchInteraction< + T extends CommandModule, + V extends BaseInteraction | Message +>( + payload: { module: Processed; event: V }, + createArgs: (m: typeof payload.event) => unknown[] +) { + return { + module: payload.module, + args: createArgs(payload.event), + }; +} + +export function dispatchMessage(module: Processed, args: [Context, Args]) { + return { + module, + args + } +} + +export function dispatchAutocomplete(payload: { module: Processed, event: AutocompleteInteraction }) { + const option = treeSearch(payload.event, payload.module.options); + if (option !== undefined) { + return { + module: option.command as Processed, //autocomplete is not a true "module" warning cast! + args: [payload.event], + }; + } + throw Error( + SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`, + ); + +} + +export function contextArgs( + wrappable: Message | BaseInteraction, + messageArgs?: string[], +) { + const ctx = Context.wrap(wrappable); + const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.options]; + return [ctx, args] as [Context, Args]; +} + +export function interactionArg(interaction: T) { + return [interaction] as [T]; +} + +function intoPayload(module: Processed) { + return pipe( + arrayifySource, + map(args => ({ module, args })), + ); +} + +const createResult = createResultResolver< + Processed, + { module: Processed; args: unknown[] }, + unknown[] +>({ + createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)), + onNext: ({ args }) => args, +}); +/** + * Creates an observable from { source } + * @param module + * @param source + */ +export function eventDispatcher(module: Processed, source: unknown) { + assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`); + + const execute: OperatorFunction = concatMap(async args => + module.execute(...args), + ); + return fromEvent(source, module.name).pipe( + intoPayload(module), + concatMap(createResult), + execute, + ); +} + +export function createDispatcher(payload: { + module: Processed; + event: BaseInteraction; +}) { + switch (payload.module.type) { + case CommandType.Text: + throw Error(SernError.MismatchEvent + ' Found a text module in interaction stream.'); + 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); + } + return dispatchInteraction(payload, contextArgs); + } + default: + return dispatchInteraction(payload, interactionArg); + } +} diff --git a/src/handler/events/dispatchers/dispatchers.ts b/src/handler/events/dispatchers/dispatchers.ts deleted file mode 100644 index dd62210..0000000 --- a/src/handler/events/dispatchers/dispatchers.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { Processed } from '../../../types/core'; -import type { AutocompleteInteraction } from 'discord.js'; -import { SernError } from '../../../core/structures/errors'; -import { treeSearch } from '../../../core/functions'; -import type { BothCommand, CommandModule, Module } from '../../../types/module'; -import { EventEmitter } from 'node:events'; -import * as assert from 'node:assert'; -import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs'; -import { arrayifySource, callPlugin } from '../../../core/operators'; -import { createResultResolver } from '../generic'; - -export function dispatchCommand(module: Processed, createArgs: () => unknown[]) { - const args = createArgs(); - return { - module, - args, - }; -} - -function intoPayload(module: Processed) { - return pipe( - arrayifySource, - map(args => ({ module, args })), - ); -} - -const createResult = createResultResolver< - Processed, - { module: Processed; args: unknown[] }, - unknown[] ->({ - createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)), - onNext: ({ args }) => args, -}); -/** - * Creates an observable from { source } - * @param module - * @param source - */ -export function eventDispatcher(module: Processed, source: unknown) { - assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`); - - const execute: OperatorFunction = concatMap(async args => - module.execute(...args), - ); - return fromEvent(source, module.name).pipe( - intoPayload(module), - concatMap(createResult), - execute, - ); -} - -export function dispatchAutocomplete( - module: Processed, - interaction: AutocompleteInteraction, -) { - const option = treeSearch(interaction, module.options); - if (option !== undefined) { - return { - module: option.command as Processed, //autocomplete is not a true "module" warning cast! - args: [interaction], - }; - } - throw Error( - SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`, - ); -} diff --git a/src/handler/events/dispatchers/index.ts b/src/handler/events/dispatchers/index.ts deleted file mode 100644 index 581daf2..0000000 --- a/src/handler/events/dispatchers/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './dispatchers'; -export * from './provide-args'; diff --git a/src/handler/events/dispatchers/provide-args.ts b/src/handler/events/dispatchers/provide-args.ts deleted file mode 100644 index 661ef94..0000000 --- a/src/handler/events/dispatchers/provide-args.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Message, ChatInputCommandInteraction } from 'discord.js'; -import type { Args, SlashOptions } from '../../../types/handler'; -import { Context } from '../../../classic/context'; - -/* - * @overload - */ -export function contextArgs( - wrap: Message, - messageArgs?: string[], -): () => [Context, ['text', string[]]]; -/* - * @overload - */ -export function contextArgs( - wrappable: ChatInputCommandInteraction, -): () => [Context, ['slash', SlashOptions]]; -/** - * function overloads to create an arguments list for Context - * @param wrap - * @param messageArgs - */ -export function contextArgs( - wrappable: Message | ChatInputCommandInteraction, - messageArgs?: string[], -) { - const ctx = Context.wrap(wrappable); - const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.options]; - return () => [ctx, args] as [Context, Args]; -} - -export function interactionArg(interaction: T) { - return () => [interaction] as [T]; -} diff --git a/src/handler/events/generic.ts b/src/handler/events/generic.ts index 47a5af1..7630474 100644 --- a/src/handler/events/generic.ts +++ b/src/handler/events/generic.ts @@ -1,6 +1,5 @@ import { BaseInteraction, - ChatInputCommandInteraction, Interaction, InteractionType, Message, @@ -11,8 +10,8 @@ import { SernError } from '../../core/structures/errors'; import { callPlugin, everyPluginOk, filterMap, filterMapTo } from '../../core/operators'; import { defaultModuleLoader } from '../../core/module-loading'; import { ImportPayload, Processed } from '../../types/core'; -import { BothCommand, CommandModule, EventModule, Module } from '../../types/module'; -import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } from './dispatchers'; +import { CommandModule, Module } from '../../types/module'; +import { contextArgs, createDispatcher, dispatchAutocomplete, dispatchInteraction, dispatchMessage, interactionArg } from './dispatchers'; import { isAutocomplete } from '../../core/predicates'; import { ObservableInput, pipe, switchMap } from 'rxjs'; import { SernEmitter } from '../../core'; @@ -66,10 +65,11 @@ export function createMessageHandler( if (fullPath === undefined) { return Err(SernError.UndefinedModule + ' No full path found in module store'); } - return defaultModuleLoader(fullPath).then(result => { - const args = contextArgs(event, rest); - return result.map(module => dispatchCommand(module, args)); - }); + return defaultModuleLoader(fullPath) + .then(result => { + const args = contextArgs(event, rest); + return result.map(module => dispatchMessage(module, args)) + }) }); } /** @@ -101,32 +101,7 @@ function createId(event: T) { return id; } -function createDispatcher({ - module, - event, -}: { - module: Processed; - event: BaseInteraction; -}) { - switch (module.type) { - case CommandType.Text: - throw Error(SernError.MismatchEvent + ' Found a text module in interaction stream.'); - case CommandType.Slash: - case CommandType.Both: { - if (isAutocomplete(event)) { - /** - * Autocomplete is a special case that - * must be handled separately, since it's - * too different from regular command modules - */ - return dispatchAutocomplete(module as Processed, event); - } - return dispatchCommand(module, contextArgs(event as ChatInputCommandInteraction)); - } - default: - return dispatchCommand(module, interactionArg(event)); - } -} + export function buildModules( input: ObservableInput, @@ -221,7 +196,7 @@ export function createResultResolver< * ignore the module */ export function callInitPlugins< - T extends Processed, + T extends Processed, Args extends ImportPayload, >(config: { onStop?: (module: T) => unknown; onNext: (module: Args) => T }) { return concatMap( @@ -249,3 +224,5 @@ export function makeModuleExecutor< }), ); } + + diff --git a/src/handler/events/interactions.ts b/src/handler/events/interactions.ts index dcdfc0f..3f1e803 100644 --- a/src/handler/events/interactions.ts +++ b/src/handler/events/interactions.ts @@ -1,17 +1,15 @@ import { Interaction } from 'discord.js'; -import { catchError, concatMap, finalize, merge } from 'rxjs'; +import { concatMap, merge } from 'rxjs'; import { SernError } from '../../core/structures/errors'; -import { handleError } from '../../core/contracts/error-handling'; import { SernEmitter } from '../../core'; import { sharedObservable } from '../../core/operators'; -import { useContainerRaw } from '../../core/dependencies'; import { isAutocomplete, isCommand, isMessageComponent, isModal } from '../../core/predicates'; import { createInteractionHandler, executeModule, makeModuleExecutor } from './generic'; import { DependencyList } from '../../types/core'; -export function makeInteractionCreate([s, err, log, modules, client]: DependencyList ) { +export function makeInteractionHandler([emitter, _, _1, modules, client]: DependencyList ) { const interactionStream$ = sharedObservable(client, 'interactionCreate'); - const handle = createInteractionHandler(interactionStream$, modules); + const handle = createInteractionHandler(interactionStream$, modules); const interactionHandler$ = merge( handle(isMessageComponent), handle(isAutocomplete), @@ -21,18 +19,8 @@ export function makeInteractionCreate([s, err, log, modules, client]: Dependency return interactionHandler$ .pipe( makeModuleExecutor(module => { - s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure)); + emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure)); }), - concatMap(module => executeModule(s, module)), - catchError(handleError(err, log)), - finalize(() => { - log?.info({ - message: 'interaction stream closed or reached end of lifetime', - }); - useContainerRaw() - ?.disposeAll() - .then(() => log?.info({ message: 'Cleaning container and crashing' })); - }), - ) - .subscribe(); + concatMap(module => executeModule(emitter, module)), + ); } diff --git a/src/handler/events/messages.ts b/src/handler/events/messages.ts index 31be551..b7b6ea2 100644 --- a/src/handler/events/messages.ts +++ b/src/handler/events/messages.ts @@ -1,10 +1,7 @@ -import { catchError, concatMap, EMPTY, finalize } from 'rxjs'; +import { concatMap, EMPTY } from 'rxjs'; import { SernError } from '../../core/structures/errors'; import type { Message } from 'discord.js'; -import { ErrorHandling, handleError } from '../../core/contracts/error-handling'; -import type { Logging, ModuleManager } from '../../core/contracts'; -import type { EventEmitter } from 'node:events'; -import { SernEmitter, useContainerRaw } from '../../core'; +import { SernEmitter } from '../../core'; import { sharedObservable } from '../../core/operators'; import { createMessageHandler, executeModule, isNonBot, makeModuleExecutor } from './generic'; import { DependencyList } from '../../types/core'; @@ -23,28 +20,23 @@ export function fmt(msg: string, prefix: string): string[] { return msg.slice(prefix.length).trim().split(/\s+/g); } -export function makeMessageCreate( - [s, err, log, modules, client]: DependencyList, +export function makeMessageHandler( + [emitter, , log, modules, client]: DependencyList, defaultPrefix: string | undefined, ) { if (!defaultPrefix) { log?.debug({ message: 'No prefix found. message handler shut down' }); - return EMPTY.subscribe(); + return EMPTY; } const messageStream$ = sharedObservable(client, 'messageCreate'); const handler = createMessageHandler(messageStream$, defaultPrefix, modules); + const messageHandler = handler(isNonBot(defaultPrefix) as (m: Message) => m is Message); return messageHandler.pipe( makeModuleExecutor(module => { - s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure)); - }), - concatMap(payload => executeModule(s, payload)), - catchError(handleError(err, log)), - finalize(() => { - log?.info({ message: 'messageCreate stream closed or reached end of lifetime' }); - useContainerRaw() - ?.disposeAll() - .then(() => log?.info({ message: 'Cleaning container and crashing' })); + emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure)); }), + concatMap(payload => executeModule(emitter, payload)), + ); } diff --git a/src/handler/sern.ts b/src/handler/sern.ts index a194d58..cac8e51 100644 --- a/src/handler/sern.ts +++ b/src/handler/sern.ts @@ -1,11 +1,13 @@ import { makeEventsHandler } from './events/user-defined'; -import { makeInteractionCreate } from './events/interactions'; +import { makeInteractionHandler } from './events/interactions'; import { startReadyEvent } from './events/ready'; -import { makeMessageCreate } from './events/messages'; -import { makeFetcher, makeDependencies } from '../core/dependencies'; +import { makeMessageHandler } from './events/messages'; +import { makeFetcher, makeDependencies, useContainerRaw } from '../core/dependencies'; import { err, ok } from '../core/functions'; import { Wrapper } from '../types/core'; import { getCommands } from '../core/module-loading'; +import { catchError, finalize, merge } from 'rxjs'; +import { handleError } from '../core/contracts/error-handling'; /** * @since 1.0.0 * @param wrapper Options to pass into sern. @@ -25,6 +27,7 @@ export function init(wrapper: Wrapper) { const startTime = performance.now(); const dependenciesAnd = makeFetcher(wrapper.containerConfig); const dependencies = dependenciesAnd(['@sern/modules', '@sern/client']); + if (wrapper.events !== undefined) { makeEventsHandler( dependenciesAnd(['@sern/client']), @@ -32,12 +35,34 @@ export function init(wrapper: Wrapper) { wrapper.containerConfig, ); } - startReadyEvent(dependencies, getCommands(wrapper.commands)); - makeMessageCreate(dependencies, wrapper.defaultPrefix); - makeInteractionCreate(dependencies); + + startReadyEvent(dependencies, getCommands(wrapper.commands)).add(() => console.log('ready')); + + const logger = dependencies[2]; + const errorHandler = dependencies[1]; + + const messages$ = makeMessageHandler(dependencies, wrapper.defaultPrefix); + const interactions$ = makeInteractionHandler(dependencies); + + merge( + messages$, + interactions$ + ).pipe( + catchError(handleError(errorHandler, logger)), + finalize(() => { + logger?.info({ message: 'a stream closed or reached end of lifetime' }); + useContainerRaw() + ?.disposeAll() + .then(() => logger?.info({ message: 'Cleaning container and crashing' })); + }) + ).subscribe() + const endTime = performance.now(); dependencies[2]?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` }); } + + + /** * @deprecated - Please import the function directly: * ```ts