chore: cleanup dispatchers subdirectory for single file

This commit is contained in:
Jacob Nguyen
2023-05-07 23:37:23 -05:00
parent 98de65965e
commit 8dacfafbac
8 changed files with 175 additions and 178 deletions

View File

@@ -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<T>; event: V },
createArgs: (m: typeof payload.event) => unknown[]
) {
return {
module: payload.module,
args: createArgs(payload.event),
};
}
export function dispatchMessage(module: Processed<CommandModule>, args: [Context, Args]) {
return {
module,
args
}
}
export function dispatchAutocomplete(payload: { module: Processed<BothCommand>, event: AutocompleteInteraction }) {
const option = treeSearch(payload.event, payload.module.options);
if (option !== undefined) {
return {
module: option.command as Processed<Module>, //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<T extends BaseInteraction>(interaction: T) {
return [interaction] as [T];
}
function intoPayload(module: Processed<Module>) {
return pipe(
arrayifySource,
map(args => ({ module, args })),
);
}
const createResult = createResultResolver<
Processed<Module>,
{ module: Processed<Module>; 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<Module>, 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),
concatMap(createResult),
execute,
);
}
export function createDispatcher(payload: {
module: Processed<CommandModule>;
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);
}
}

View File

@@ -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<CommandModule>, createArgs: () => unknown[]) {
const args = createArgs();
return {
module,
args,
};
}
function intoPayload(module: Processed<Module>) {
return pipe(
arrayifySource,
map(args => ({ module, args })),
);
}
const createResult = createResultResolver<
Processed<Module>,
{ module: Processed<Module>; 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<Module>, 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),
concatMap(createResult),
execute,
);
}
export function dispatchAutocomplete(
module: Processed<BothCommand>,
interaction: AutocompleteInteraction,
) {
const option = treeSearch(interaction, module.options);
if (option !== undefined) {
return {
module: option.command as Processed<Module>, //autocomplete is not a true "module" warning cast!
args: [interaction],
};
}
throw Error(
SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`,
);
}

View File

@@ -1,2 +0,0 @@
export * from './dispatchers';
export * from './provide-args';

View File

@@ -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<T>(interaction: T) {
return () => [interaction] as [T];
}

View File

@@ -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<CommandModule>(fullPath).then(result => {
const args = contextArgs(event, rest);
return result.map(module => dispatchCommand(module, args));
});
return defaultModuleLoader<CommandModule>(fullPath)
.then(result => {
const args = contextArgs(event, rest);
return result.map(module => dispatchMessage(module, args))
})
});
}
/**
@@ -101,32 +101,7 @@ function createId<T extends Interaction>(event: T) {
return id;
}
function createDispatcher({
module,
event,
}: {
module: Processed<CommandModule>;
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<BothCommand>, event);
}
return dispatchCommand(module, contextArgs(event as ChatInputCommandInteraction));
}
default:
return dispatchCommand(module, interactionArg(event));
}
}
export function buildModules<T extends AnyModule>(
input: ObservableInput<string>,
@@ -221,7 +196,7 @@ export function createResultResolver<
* ignore the module
*/
export function callInitPlugins<
T extends Processed<CommandModule | EventModule>,
T extends Processed<Module>,
Args extends ImportPayload<T>,
>(config: { onStop?: (module: T) => unknown; onNext: (module: Args) => T }) {
return concatMap(
@@ -249,3 +224,5 @@ export function makeModuleExecutor<
}),
);
}

View File

@@ -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<Interaction>(client, 'interactionCreate');
const handle = createInteractionHandler<Interaction>(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)),
);
}

View File

@@ -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<Message>(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)),
);
}

View File

@@ -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