mirror of
https://github.com/sern-handler/handler
synced 2026-06-06 01:16:55 +00:00
feat/abstractiti (#340)
* progress on better error handling * wiring onError callback through module loader and resolver * fix error callbacks not being stored * update onError to be record * type alias * wiring * seems to work * update error handling contract and wire more * add command error builder * fix merge * progress on error handling * naive onError handling, not tested * progres * proress * progress on abstracting away iti * seems to work * fix tests * better typings * add doc * abstracting iti * remove onerror for this pr * feat: better way to add dependencies * fix tests
This commit is contained in:
@@ -1,21 +1,18 @@
|
||||
import type { CommandModule,Processed, EventModule } from "../../types/core-modules";
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export interface ErrorHandling {
|
||||
/**
|
||||
* Number of times the process should throw an error until crashing and exiting
|
||||
*/
|
||||
keepAlive: number;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Version 4 will remove this method
|
||||
*/
|
||||
crash(err: Error): never;
|
||||
/**
|
||||
* A function that is called on every crash. Updates keepAlive.
|
||||
* If keepAlive is 0, the process crashes.
|
||||
* A function that is called on every throw.
|
||||
* @param error
|
||||
*/
|
||||
updateAlive(error: Error): void;
|
||||
|
||||
}
|
||||
|
||||
@@ -6,13 +6,18 @@ import type {
|
||||
} from '../../types/core-modules';
|
||||
import { CommandType } from '../structures';
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export interface ModuleManager {
|
||||
get(id: string): string | undefined;
|
||||
interface MetadataAccess {
|
||||
getMetadata(m: Module): CommandMeta | undefined;
|
||||
setMetadata(m: Module, c: CommandMeta): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
* @internal - direct access to the module manager will be removed in version 4
|
||||
*/
|
||||
export interface ModuleManager extends MetadataAccess {
|
||||
get(id: string): string | undefined;
|
||||
|
||||
set(id: string, path: string): void;
|
||||
getPublishableCommands(): Promise<CommandModule[]>;
|
||||
getByNameCommandType<T extends CommandType>(
|
||||
|
||||
@@ -36,7 +36,7 @@ export function partitionPlugins(
|
||||
export function treeSearch(
|
||||
iAutocomplete: AutocompleteInteraction,
|
||||
options: SernOptionsData[] | undefined,
|
||||
): SernAutocompleteData | undefined {
|
||||
): SernAutocompleteData & { parent?: string } | undefined {
|
||||
if (options === undefined) return undefined;
|
||||
//clone to prevent mutation of original command module
|
||||
const _options = options.map(a => ({ ...a }));
|
||||
@@ -68,11 +68,11 @@ export function treeSearch(
|
||||
const parentAndOptionMatches =
|
||||
subcommands.has(parent) && cur.name === choice.name;
|
||||
if (parentAndOptionMatches) {
|
||||
return cur;
|
||||
return { ...cur, parent };
|
||||
}
|
||||
} else {
|
||||
if (cur.name === choice.name) {
|
||||
return cur;
|
||||
return { ...cur, parent: undefined };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ import * as assert from 'assert';
|
||||
import { composeRoot, useContainer } from './dependency-injection';
|
||||
import type { DependencyConfiguration } from '../../types/ioc';
|
||||
import { CoreContainer } from './container';
|
||||
|
||||
import { Result } from 'ts-results-es'
|
||||
import { DefaultServices } from '../_internal';
|
||||
import { AnyFunction } from '../../types/utility';
|
||||
//SIDE EFFECT: GLOBAL DI
|
||||
let containerSubject: CoreContainer<Partial<Dependencies>>;
|
||||
|
||||
@@ -20,17 +22,83 @@ export function useContainerRaw() {
|
||||
return containerSubject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
* @param conf a configuration for creating your project dependencies
|
||||
*/
|
||||
export async function makeDependencies<const T extends Dependencies>(
|
||||
conf: DependencyConfiguration,
|
||||
) {
|
||||
const dependencyBuilder = (container: any, excluded: string[]) => {
|
||||
type Insertable =
|
||||
| ((container: CoreContainer<Dependencies>) => unknown )
|
||||
| Record<PropertyKey, unknown>
|
||||
return {
|
||||
/**
|
||||
* Insert a dependency into your container.
|
||||
* Supply the correct key and dependency
|
||||
*/
|
||||
add(key: keyof Dependencies, v: Insertable) {
|
||||
Result
|
||||
.wrap(() => container.add({ [key]: v}))
|
||||
.expect("Failed to add " + key);
|
||||
},
|
||||
/**
|
||||
* Exclude any dependencies from being added.
|
||||
* Warning: this could lead to bad errors if not used correctly
|
||||
*/
|
||||
exclude(...keys: (keyof Dependencies)[]) {
|
||||
keys.forEach(key => excluded.push(key));
|
||||
},
|
||||
/**
|
||||
* @param key the key of the dependency
|
||||
* @param v The dependency to swap out.
|
||||
* Swap out a preexisting dependency.
|
||||
*/
|
||||
swap(key: keyof Dependencies, v: Insertable) {
|
||||
Result
|
||||
.wrap(() => container.upsert({ [key]: v }))
|
||||
.expect("Failed to update " + key);
|
||||
},
|
||||
/**
|
||||
* @param key the key of the dependency
|
||||
* @param cleanup Provide cleanup for the dependency at key. First parameter is the dependency itself
|
||||
* @example
|
||||
* ```ts
|
||||
* addDisposer('dbConnection', (dbConnection) => dbConnection.end())
|
||||
* ```
|
||||
* Swap out a preexisting dependency.
|
||||
*/
|
||||
addDisposer(key: keyof Dependencies, cleanup: AnyFunction) {
|
||||
Result
|
||||
.wrap(() => container.addDisposer({ [key] : cleanup }))
|
||||
.expect("Failed to addDisposer for" + key);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
type CallbackBuilder = (c: ReturnType<typeof dependencyBuilder>) => any
|
||||
|
||||
type ValidDependencyConfig =
|
||||
| CallbackBuilder
|
||||
| DependencyConfiguration;
|
||||
|
||||
export const insertLogger = (containerSubject: CoreContainer<any>) => {
|
||||
containerSubject
|
||||
.upsert({'@sern/logger': () => new DefaultServices.DefaultLogging});
|
||||
}
|
||||
export async function makeDependencies<const T extends Dependencies>
|
||||
(conf: ValidDependencyConfig) {
|
||||
//Until there are more optional dependencies, just check if the logger exists
|
||||
//SIDE EFFECT
|
||||
containerSubject = new CoreContainer();
|
||||
await composeRoot(containerSubject, conf);
|
||||
if(typeof conf === 'function') {
|
||||
const excluded: string[] = [];
|
||||
conf(dependencyBuilder(containerSubject, excluded));
|
||||
if(!excluded.includes('@sern/logger')) {
|
||||
assert.ok(!containerSubject.getTokens()['@sern/logger'])
|
||||
insertLogger(containerSubject);
|
||||
}
|
||||
containerSubject.ready();
|
||||
} else {
|
||||
composeRoot(containerSubject, conf);
|
||||
}
|
||||
|
||||
return useContainer<T>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -21,11 +21,9 @@ export class CoreContainer<T extends Partial<Dependencies>> extends Container<T,
|
||||
.subscribe({ complete: unsubscribe });
|
||||
|
||||
(this as Container<{}, {}>)
|
||||
.add({
|
||||
'@sern/errors': () => new DefaultServices.DefaultErrorHandling(),
|
||||
'@sern/emitter': () => new SernEmitter(),
|
||||
'@sern/store': () => new ModuleStore(),
|
||||
})
|
||||
.add({ '@sern/errors': () => new DefaultServices.DefaultErrorHandling(),
|
||||
'@sern/emitter': () => new SernEmitter(),
|
||||
'@sern/store': () => new ModuleStore() })
|
||||
.add(ctx => {
|
||||
return {
|
||||
'@sern/modules': () =>
|
||||
@@ -34,9 +32,7 @@ export class CoreContainer<T extends Partial<Dependencies>> extends Container<T,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
isReady() {
|
||||
|
||||
return this.ready$.closed;
|
||||
}
|
||||
override async disposeAll() {
|
||||
@@ -44,9 +40,7 @@ export class CoreContainer<T extends Partial<Dependencies>> extends Container<T,
|
||||
const otherDisposables = Object
|
||||
.entries(this._context)
|
||||
.flatMap(([key, value]) =>
|
||||
'dispose' in value
|
||||
? [key]
|
||||
: []);
|
||||
'dispose' in value ? [key] : []);
|
||||
|
||||
for(const key of otherDisposables) {
|
||||
this.addDisposer({ [key]: (dep: Disposable) => dep.dispose() } as never);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { CoreDependencies, DependencyConfiguration, IntoDependencies } from '../../types/ioc';
|
||||
import { DefaultServices } from '../_internal';
|
||||
import { useContainerRaw } from './base';
|
||||
import { insertLogger, useContainerRaw } from './base';
|
||||
import { CoreContainer } from './container';
|
||||
|
||||
/**
|
||||
@@ -53,16 +52,14 @@ export function Services<const T extends (keyof Dependencies)[]>(...keys: [...T]
|
||||
* Finally, update the containerSubject with the new container state
|
||||
* @param conf
|
||||
*/
|
||||
export async function composeRoot(
|
||||
export function composeRoot(
|
||||
container: CoreContainer<Partial<Dependencies>>,
|
||||
conf: DependencyConfiguration,
|
||||
) {
|
||||
//container should have no client or logger yet.
|
||||
const hasLogger = conf.exclude?.has('@sern/logger');
|
||||
if (!hasLogger) {
|
||||
container.upsert({
|
||||
'@sern/logger': () => new DefaultServices.DefaultLogging(),
|
||||
});
|
||||
insertLogger(container);
|
||||
}
|
||||
//Build the container based on the callback provided by the user
|
||||
conf.build(container as CoreContainer<Omit<CoreDependencies, '@sern/client'>>);
|
||||
|
||||
@@ -23,19 +23,21 @@ export type ModuleResult<T> = Promise<ImportPayload<T>>;
|
||||
* export default commandModule({})
|
||||
*/
|
||||
export async function importModule<T>(absPath: string) {
|
||||
let module = await import(absPath).then(esm => esm.default);
|
||||
let fileModule = await import(absPath);
|
||||
|
||||
assert(module, `Found no export for module at ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`);
|
||||
if ('default' in module) {
|
||||
module = module.default;
|
||||
let commandModule = fileModule.default;
|
||||
|
||||
assert(commandModule , `Found no export @ ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`);
|
||||
if ('default' in commandModule ) {
|
||||
commandModule = commandModule.default;
|
||||
}
|
||||
return Result
|
||||
.wrap(() => module.getInstance())
|
||||
.unwrapOr(module) as T;
|
||||
.wrap(() => ({ module: commandModule.getInstance() }))
|
||||
.unwrapOr({ module: commandModule }) as T;
|
||||
}
|
||||
|
||||
export async function defaultModuleLoader<T extends Module>(absPath: string): ModuleResult<T> {
|
||||
let module = await importModule<T>(absPath);
|
||||
let { module } = await importModule<{ module: T }>(absPath);
|
||||
assert(module, `Found an undefined module: ${absPath}`);
|
||||
return { module, absPath };
|
||||
}
|
||||
@@ -51,7 +53,8 @@ export const fmtFileName = (fileName: string) => parse(fileName).name;
|
||||
export function buildModuleStream<T extends Module>(
|
||||
input: ObservableInput<string>,
|
||||
): Observable<ImportPayload<T>> {
|
||||
return from(input).pipe(mergeMap(defaultModuleLoader<T>));
|
||||
return from(input)
|
||||
.pipe(mergeMap(defaultModuleLoader<T>));
|
||||
}
|
||||
|
||||
export const getFullPathTree = (dir: string) => readPaths(resolve(dir));
|
||||
|
||||
41
src/core/structures/command-error.ts
Normal file
41
src/core/structures/command-error.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ReplyOptions } from "../../types/utility";
|
||||
import type { Logging } from "../contracts";
|
||||
|
||||
export interface Response {
|
||||
type: 'fail' | 'continue';
|
||||
body?: ReplyOptions;
|
||||
log?: { type: keyof Logging; message: unknown }
|
||||
}
|
||||
|
||||
export const of = () => {
|
||||
const payload = {
|
||||
type: 'fail',
|
||||
body: undefined,
|
||||
log : undefined
|
||||
} as Record<PropertyKey, unknown>
|
||||
|
||||
return {
|
||||
/**
|
||||
* @param {'fail' | 'continue'} p a status to determine if the error will
|
||||
* terminate your application or continue. Warning and
|
||||
*/
|
||||
status: (p: 'fail' | 'continue') => {
|
||||
payload.type = p;
|
||||
return payload;
|
||||
},
|
||||
/**
|
||||
* @param {keyof Logging} type Determine to log to logger[type].
|
||||
* @param {T} message the message to log
|
||||
*
|
||||
* Log this error with the logger.
|
||||
*/
|
||||
log: <T=string>(type: keyof Logging, message: T) => {
|
||||
payload.log = { type, message };
|
||||
return payload;
|
||||
},
|
||||
reply: (bodyContent: ReplyOptions) => {
|
||||
payload.body = bodyContent;
|
||||
return payload;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
import { CoreContext } from '../structures/core-context';
|
||||
import { Result, Ok, Err } from 'ts-results-es';
|
||||
import * as assert from 'assert';
|
||||
import { ReplyOptions } from '../../types/utility';
|
||||
|
||||
type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
@@ -103,9 +103,9 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
|
||||
public async reply(content: ReplyOptions) {
|
||||
return safeUnwrap(
|
||||
this.ctx
|
||||
.map(m => m.reply(content as string | MessageReplyOptions))
|
||||
.map(m => m.reply(content as MessageReplyOptions))
|
||||
.mapErr(i =>
|
||||
i.reply(content as string | InteractionReplyOptions).then(() => i.fetchReply()),
|
||||
i.reply(content as InteractionReplyOptions).then(() => i.fetchReply()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './context';
|
||||
export * from './sern-emitter';
|
||||
export * from './services';
|
||||
export * from './module-store';
|
||||
export * as CommandError from './command-error';
|
||||
|
||||
@@ -3,18 +3,18 @@ import { ErrorHandling } from '../../contracts';
|
||||
/**
|
||||
* @internal
|
||||
* @since 2.0.0
|
||||
* Version 4.0.0 will internalize this api. Please refrain from using ModuleStore!
|
||||
* Version 4.0.0 will internalize this api. Please refrain from using the defaults!
|
||||
*/
|
||||
export class DefaultErrorHandling implements ErrorHandling {
|
||||
crash(err: Error): never {
|
||||
throw err;
|
||||
}
|
||||
|
||||
keepAlive = 5;
|
||||
#keepAlive = 5;
|
||||
|
||||
updateAlive(err: Error) {
|
||||
this.keepAlive--;
|
||||
if (this.keepAlive === 0) {
|
||||
this.#keepAlive--;
|
||||
if (this.#keepAlive === 0) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { CommandType } from '../enums';
|
||||
export class DefaultModuleManager implements ModuleManager {
|
||||
constructor(private moduleStore: CoreModuleStore) {}
|
||||
|
||||
|
||||
getByNameCommandType<T extends CommandType>(name: string, commandType: T) {
|
||||
const id = this.get(Id.create(name, commandType));
|
||||
if (!id) {
|
||||
|
||||
@@ -9,23 +9,16 @@ import {
|
||||
SernError,
|
||||
} from '../core/_internal';
|
||||
import { createResultResolver } from './event-utils';
|
||||
import { AutocompleteInteraction, BaseInteraction, Message } from 'discord.js';
|
||||
import { BaseInteraction, Message } from 'discord.js';
|
||||
import { CommandType, Context } from '../core';
|
||||
import type { Args } from '../types/utility';
|
||||
import type { BothCommand, CommandModule, Module, Processed } from '../types/core-modules';
|
||||
import type { AnyFunction, Args } from '../types/utility';
|
||||
import type { CommandModule, Module, Processed } from '../types/core-modules';
|
||||
|
||||
function dispatchAutocomplete(payload: {
|
||||
module: Processed<BothCommand>;
|
||||
event: AutocompleteInteraction;
|
||||
}) {
|
||||
const option = treeSearch(payload.event, payload.module.options);
|
||||
assert.ok(
|
||||
option,
|
||||
Error(SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`),
|
||||
);
|
||||
//TODO: refactor dispatchers so that it implements a strategy for each different type of payload?
|
||||
export function dispatchMessage(module: Processed<CommandModule>, args: [Context, Args]) {
|
||||
return {
|
||||
module: option.command as Processed<Module>, //autocomplete is not a true "module" warning cast!
|
||||
args: [payload.event],
|
||||
module,
|
||||
args,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,16 +29,16 @@ export function contextArgs(wrappable: Message | BaseInteraction, messageArgs?:
|
||||
}
|
||||
|
||||
|
||||
function intoPayload(module: Processed<Module>) {
|
||||
function intoPayload(module: Processed<Module>, ) {
|
||||
return pipe(
|
||||
arrayifySource,
|
||||
map(args => ({ module, args })),
|
||||
map(args => ({ module, args, })),
|
||||
);
|
||||
}
|
||||
|
||||
const createResult = createResultResolver<
|
||||
Processed<Module>,
|
||||
{ module: Processed<Module>; args: unknown[] },
|
||||
{ module: Processed<Module>; args: unknown[] },
|
||||
unknown[]
|
||||
>({
|
||||
createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)),
|
||||
@@ -56,7 +49,7 @@ const createResult = createResultResolver<
|
||||
* @param module
|
||||
* @param source
|
||||
*/
|
||||
export function eventDispatcher(module: Processed<Module>, source: unknown) {
|
||||
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 =>
|
||||
@@ -81,13 +74,18 @@ export function createDispatcher(payload: {
|
||||
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);
|
||||
const option = treeSearch(payload.event, payload.module.options);
|
||||
assert.ok(
|
||||
option,
|
||||
Error(SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`),
|
||||
);
|
||||
const { command, name, parent } = option;
|
||||
|
||||
return {
|
||||
...payload,
|
||||
module: command as Processed<Module>, //autocomplete is not a true "module" warning cast!
|
||||
args: [payload.event],
|
||||
};
|
||||
}
|
||||
return {
|
||||
module: payload.module,
|
||||
|
||||
@@ -28,7 +28,7 @@ import { contextArgs, createDispatcher } from './dispatchers';
|
||||
import { ObservableInput, pipe } from 'rxjs';
|
||||
import { SernEmitter } from '../core';
|
||||
import { Err, Ok, Result } from 'ts-results-es';
|
||||
import type { Awaitable } from '../types/utility';
|
||||
import type { AnyFunction, Awaitable } from '../types/utility';
|
||||
import type { ControlPlugin } from '../types/core-plugin';
|
||||
import type { AnyModule, CommandModule, Module, Processed } from '../types/core-modules';
|
||||
import type { ImportPayload } from '../types/core';
|
||||
@@ -78,7 +78,10 @@ export function createInteractionHandler<T extends Interaction>(
|
||||
return Files
|
||||
.defaultModuleLoader<Processed<CommandModule>>(fullPath)
|
||||
.then(payload =>
|
||||
Ok(createDispatcher({ module: payload.module, event })));
|
||||
Ok(createDispatcher({
|
||||
module: payload.module,
|
||||
event,
|
||||
})));
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -93,13 +96,14 @@ export function createMessageHandler(
|
||||
const fullPath = mg.get(`${prefix}_A1`);
|
||||
|
||||
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 Files
|
||||
.defaultModuleLoader<Processed<CommandModule>>(fullPath)
|
||||
.then(({ module })=> {
|
||||
.then((payload)=> {
|
||||
const args = contextArgs(event, rest);
|
||||
return Ok({ module, args });
|
||||
return Ok({ args, ...payload });
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -130,6 +134,12 @@ export function buildModules<T extends AnyModule>(
|
||||
.pipe(assignDefaults(moduleManager));
|
||||
}
|
||||
|
||||
|
||||
interface ExecutePayload {
|
||||
module: Processed<Module>;
|
||||
task: () => Awaitable<unknown>;
|
||||
args: unknown[]
|
||||
}
|
||||
/**
|
||||
* Wraps the task in a Result as a try / catch.
|
||||
* if the task is ok, an event is emitted and the stream becomes empty
|
||||
@@ -140,13 +150,13 @@ export function buildModules<T extends AnyModule>(
|
||||
*/
|
||||
export function executeModule(
|
||||
emitter: Emitter,
|
||||
logger: Logging|undefined,
|
||||
errHandler: ErrorHandling,
|
||||
{
|
||||
module,
|
||||
task,
|
||||
}: {
|
||||
module: Processed<Module>;
|
||||
task: () => Awaitable<unknown>;
|
||||
},
|
||||
args
|
||||
}: ExecutePayload,
|
||||
) {
|
||||
return of(module).pipe(
|
||||
//converting the task into a promise so rxjs can resolve the Awaitable properly
|
||||
@@ -155,9 +165,9 @@ export function executeModule(
|
||||
if (result.isOk()) {
|
||||
emitter.emit('module.activate', SernEmitter.success(module));
|
||||
return EMPTY;
|
||||
} else {
|
||||
return throwError(() => SernEmitter.failure(module, result.error));
|
||||
}
|
||||
}
|
||||
return throwError(() => SernEmitter.failure(module, result.error));
|
||||
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -208,7 +218,7 @@ export function callInitPlugins<T extends Processed<AnyModule>>(sernEmitter: Emi
|
||||
},
|
||||
onNext: ({ module }) => {
|
||||
sernEmitter.emit('module.register', SernEmitter.success(module));
|
||||
return module;
|
||||
return { module };
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -220,16 +230,22 @@ export function callInitPlugins<T extends Processed<AnyModule>>(sernEmitter: Emi
|
||||
*/
|
||||
export function makeModuleExecutor<
|
||||
M extends Processed<Module>,
|
||||
Args extends { module: M; args: unknown[] },
|
||||
Args extends {
|
||||
module: M;
|
||||
args: unknown[];
|
||||
},
|
||||
>(onStop: (m: M) => unknown) {
|
||||
const onNext = ({ args, module }: Args) => ({
|
||||
task: () => module.execute(...args),
|
||||
module,
|
||||
args
|
||||
});
|
||||
return concatMap(
|
||||
createResultResolver({
|
||||
onStop,
|
||||
createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)),
|
||||
createStream: ({ args, module }) =>
|
||||
from(module.onEvent)
|
||||
.pipe(callPlugin(args)),
|
||||
onNext,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { createInteractionHandler, executeModule, makeModuleExecutor } from './_internal';
|
||||
import type { DependencyList } from '../types/ioc';
|
||||
|
||||
export function interactionHandler([emitter, , , modules, client]: DependencyList) {
|
||||
export function interactionHandler([emitter, err, log, modules, client]: DependencyList) {
|
||||
const interactionStream$ = sharedEventStream<Interaction>(client, 'interactionCreate');
|
||||
const handle = createInteractionHandler(interactionStream$, modules);
|
||||
|
||||
@@ -28,6 +28,6 @@ export function interactionHandler([emitter, , , modules, client]: DependencyLis
|
||||
filterTap(e => emitter.emit('warning', SernEmitter.warning(e))),
|
||||
makeModuleExecutor(module =>
|
||||
emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure))),
|
||||
mergeMap(payload => executeModule(emitter, payload)),
|
||||
mergeMap(payload => executeModule(emitter, log, err, payload)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ function hasPrefix(prefix: string, content: string) {
|
||||
}
|
||||
|
||||
export function messageHandler(
|
||||
[emitter, , log, modules, client]: DependencyList,
|
||||
[emitter, err, log, modules, client]: DependencyList,
|
||||
defaultPrefix: string | undefined,
|
||||
) {
|
||||
if (!defaultPrefix) {
|
||||
@@ -42,6 +42,6 @@ export function messageHandler(
|
||||
makeModuleExecutor(module => {
|
||||
emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
|
||||
}),
|
||||
mergeMap(payload => executeModule(emitter, payload)),
|
||||
mergeMap(payload => executeModule(emitter, log, err, payload)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,10 +17,9 @@ export function startReadyEvent(
|
||||
|
||||
return concat(ready$, buildModules<AnyModule>(allPaths, moduleManager))
|
||||
.pipe(callInitPlugins(sEmitter))
|
||||
.subscribe(module => {
|
||||
register(moduleManager, module).expect(
|
||||
SernError.InvalidModuleType + ' ' + util.inspect(module),
|
||||
);
|
||||
.subscribe(({ module }) => {
|
||||
register(moduleManager, module)
|
||||
.expect(SernError.InvalidModuleType + ' ' + util.inspect(module));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,32 +4,33 @@ import { SernError } from '../core/_internal';
|
||||
import { buildModules, callInitPlugins, handleCrash, eventDispatcher } from './_internal';
|
||||
import { Service } from '../core/ioc';
|
||||
import type { DependencyList } from '../types/ioc';
|
||||
import type { CommandModule, EventModule, Processed } from '../types/core-modules';
|
||||
import type { EventModule, Processed } from '../types/core-modules';
|
||||
|
||||
export function eventsHandler(
|
||||
[emitter, err, log, moduleManager, client]: DependencyList,
|
||||
allPaths: ObservableInput<string>,
|
||||
) {
|
||||
//code smell
|
||||
const intoDispatcher = (e: Processed<EventModule | CommandModule>) => {
|
||||
switch (e.type) {
|
||||
const intoDispatcher = (e: { module: Processed<EventModule> }) => {
|
||||
switch (e.module.type) {
|
||||
case EventType.Sern:
|
||||
return eventDispatcher(e, emitter);
|
||||
return eventDispatcher(e.module, emitter);
|
||||
case EventType.Discord:
|
||||
return eventDispatcher(e, client);
|
||||
return eventDispatcher(e.module, client);
|
||||
case EventType.External:
|
||||
return eventDispatcher(e, Service(e.emitter));
|
||||
return eventDispatcher(e.module, Service(e.module.emitter));
|
||||
default:
|
||||
throw Error(SernError.InvalidModuleType + ' while creating event handler');
|
||||
}
|
||||
};
|
||||
buildModules<EventModule>(allPaths, moduleManager)
|
||||
.pipe(callInitPlugins(emitter),
|
||||
map(intoDispatcher),
|
||||
/**
|
||||
* Where all events are turned on
|
||||
*/
|
||||
mergeAll(),
|
||||
handleCrash(err, log))
|
||||
.pipe(
|
||||
callInitPlugins(emitter),
|
||||
map(intoDispatcher),
|
||||
/**
|
||||
* Where all events are turned on
|
||||
*/
|
||||
mergeAll(),
|
||||
handleCrash(err, log))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import { CommandType, Context, EventType } from '../../src/core';
|
||||
import { AnyCommandPlugin, AnyEventPlugin, ControlPlugin, InitPlugin } from './core-plugin';
|
||||
import { Awaitable, Args, SlashOptions, SernEventsMapping } from './utility';
|
||||
|
||||
|
||||
|
||||
export interface CommandMeta {
|
||||
fullPath: string;
|
||||
id: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
export interface ImportPayload<T> {
|
||||
module: T;
|
||||
absPath: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CommandInteractionOptionResolver } from 'discord.js';
|
||||
import { PayloadType } from '../core';
|
||||
import { AnyModule } from './core-modules';
|
||||
import type { CommandInteractionOptionResolver, InteractionReplyOptions, MessageReplyOptions } from 'discord.js';
|
||||
import type { PayloadType } from '../core';
|
||||
import type { AnyModule } from './core-modules';
|
||||
|
||||
export type Awaitable<T> = PromiseLike<T> | T;
|
||||
|
||||
@@ -27,3 +27,7 @@ export type Payload =
|
||||
| { type: PayloadType.Success; module: AnyModule }
|
||||
| { type: PayloadType.Failure; module?: AnyModule; reason: string | Error }
|
||||
| { type: PayloadType.Warning; reason: string };
|
||||
|
||||
|
||||
|
||||
export type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;
|
||||
|
||||
Reference in New Issue
Block a user