mirror of
https://github.com/sern-handler/handler
synced 2026-06-06 01:16:55 +00:00
refactor and clean up and reenter v3 module loading
This commit is contained in:
@@ -2,7 +2,6 @@ import type { Result } from 'ts-results-es'
|
||||
|
||||
export * from './operators';
|
||||
export * from './functions';
|
||||
export { SernError } from './structures/enums';
|
||||
|
||||
export type _Module = {
|
||||
meta: {
|
||||
|
||||
@@ -61,10 +61,7 @@ export function treeSearch(
|
||||
{
|
||||
if ('autocomplete' in cur && cur.autocomplete) {
|
||||
const choice = iAutocomplete.options.getFocused(true);
|
||||
assert(
|
||||
'command' in cur,
|
||||
'No `command` property found for autocomplete option',
|
||||
);
|
||||
assert( 'command' in cur, 'No `command` property found for option ' + cur.name);
|
||||
if (subcommands.size > 0) {
|
||||
const parent = iAutocomplete.options.getSubcommand();
|
||||
const parentAndOptionMatches =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'node:path';
|
||||
import { existsSync } from 'fs';
|
||||
import assert from 'node:assert';
|
||||
|
||||
|
||||
export const parseCallsite = (site: string) => {
|
||||
@@ -33,17 +34,17 @@ export const shouldHandle = (pth: string, filenam: string) => {
|
||||
* esm javascript, typescript, and commonjs typescript
|
||||
* export default commandModule({})
|
||||
*/
|
||||
//async function importModule<T>(absPath: string) {
|
||||
// let fileModule = await import(absPath);
|
||||
//
|
||||
// let commandModule = fileModule.default;
|
||||
//
|
||||
// assert(commandModule , `No export @ ${absPath}. Forgot to ignore with "!"? (!${path.basename(absPath)})?`);
|
||||
// if ('default' in commandModule) {
|
||||
// commandModule = commandModule.default;
|
||||
// }
|
||||
// return { module: commandModule } as T;
|
||||
//}
|
||||
export async function importModule<T>(absPath: string) {
|
||||
let fileModule = await import(absPath);
|
||||
|
||||
let commandModule = fileModule.default;
|
||||
|
||||
assert(commandModule , `No export @ ${absPath}. Forgot to ignore with "!"? (!${path.basename(absPath)})?`);
|
||||
if ('default' in commandModule) {
|
||||
commandModule = commandModule.default;
|
||||
}
|
||||
return { module: commandModule } as T;
|
||||
}
|
||||
|
||||
|
||||
export const fmtFileName = (fileName: string) => path.parse(fileName).name;
|
||||
|
||||
@@ -21,7 +21,6 @@ export function commandModule(mod: InputCommand): _Module {
|
||||
const [onEvent, plugins] = partitionPlugins(mod.plugins);
|
||||
const initCallsite = get_callsite(callsites()).at(-2);
|
||||
if(!initCallsite) throw Error("initCallsite is null");
|
||||
|
||||
const { name, absPath } = Files.parseCallsite(initCallsite);
|
||||
mod.name ??= name; mod.description ??= '...'
|
||||
//@ts-ignore
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { Emitter, ErrorHandling, Logging } from './interfaces';
|
||||
import util from 'node:util';
|
||||
import type { PluginResult } from '../types/core-plugin';
|
||||
import type { VoidResult } from './_internal';
|
||||
import { Result } from 'ts-results-es';
|
||||
/**
|
||||
* if {src} is true, mapTo V, else ignore
|
||||
* @param item
|
||||
@@ -70,11 +71,11 @@ export function handleError<C>(crashHandler: ErrorHandling, emitter: Emitter, lo
|
||||
};
|
||||
}
|
||||
//// Temporary until i get rxjs operators working on ts-results-es
|
||||
//const filterTap = <K, R>(onErr: (e: R) => void): OperatorFunction<Result<K, R>, K> =>
|
||||
// pipe(concatMap(result => {
|
||||
// if(result.isOk()) {
|
||||
// return of(result.value)
|
||||
// }
|
||||
// onErr(result.error);
|
||||
// return EMPTY
|
||||
// }))
|
||||
export const filterTap = <K, R>(onErr: (e: R) => void): OperatorFunction<Result<K, R>, K> =>
|
||||
concatMap(result => {
|
||||
if(result.isOk()) {
|
||||
return of(result.value)
|
||||
}
|
||||
onErr(result.error);
|
||||
return EMPTY
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Result as Either } from 'ts-results-es';
|
||||
import { SernError } from '../_internal';
|
||||
import { SernError } from './enums';
|
||||
import * as assert from 'node:assert';
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
everyPluginOk,
|
||||
filterMapTo,
|
||||
handleError,
|
||||
SernError,
|
||||
type VoidResult,
|
||||
resultPayload,
|
||||
arrayifySource,
|
||||
@@ -28,7 +27,7 @@ import {
|
||||
} from '../core/_internal';
|
||||
import * as Id from '../core/id'
|
||||
import type { Emitter, ErrorHandling, Logging } from '../core/interfaces';
|
||||
import { PayloadType } from '../core/structures/enums'
|
||||
import { PayloadType, SernError } from '../core/structures/enums'
|
||||
import { Err, Ok, Result } from 'ts-results-es';
|
||||
import type { Awaitable } from '../types/utility';
|
||||
import type { ControlPlugin } from '../types/core-plugin';
|
||||
@@ -90,7 +89,6 @@ export function createDispatcher(payload: { module: Processed<CommandModule>; ev
|
||||
const { command } = option;
|
||||
|
||||
return {
|
||||
...payload,
|
||||
module: command as Processed<Module>, //autocomplete is not a true "module" warning cast!
|
||||
args: [payload.event],
|
||||
};
|
||||
@@ -256,16 +254,13 @@ export function callInitPlugins<T extends Processed<Module>>(sernEmitter: Emitte
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an executable task ( execute the command ) if all control plugins are successful
|
||||
* 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<Module>,
|
||||
Args extends {
|
||||
module: M;
|
||||
args: unknown[];
|
||||
},
|
||||
>(onStop: (m: M) => unknown) {
|
||||
Args extends { module: M; args: unknown[]; }>
|
||||
(onStop: (m: M) => unknown) {
|
||||
const onNext = ({ args, module }: Args) => ({
|
||||
task: () => module.execute(...args),
|
||||
module,
|
||||
|
||||
30
src/handlers/interaction.ts
Normal file
30
src/handlers/interaction.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Interaction } from 'discord.js';
|
||||
import { mergeMap, merge, concatMap } from 'rxjs';
|
||||
import { PayloadType } from '../core/structures/enums';
|
||||
import { filterTap } from '../core/operators'
|
||||
import {
|
||||
isAutocomplete,
|
||||
isCommand,
|
||||
isMessageComponent,
|
||||
isModal,
|
||||
sharedEventStream,
|
||||
resultPayload,
|
||||
} from '../core/_internal';
|
||||
import { createInteractionHandler, executeModule, makeModuleExecutor } from './event-utils';
|
||||
import type { DependencyList } from '../types/ioc';
|
||||
import { SernError } from '../core/structures/enums'
|
||||
export function interactionHandler([emitter, err, log, client]: DependencyList) {
|
||||
const interactionStream$ = sharedEventStream<Interaction>(client, 'interactionCreate');
|
||||
const modules = new Map();
|
||||
const handle = createInteractionHandler(interactionStream$, modules);
|
||||
|
||||
const interactionHandler$ = merge(handle(isMessageComponent),
|
||||
handle(isAutocomplete),
|
||||
handle(isCommand),
|
||||
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)))),
|
||||
mergeMap(payload => executeModule(emitter, log, err, payload)));
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { EMPTY } from 'rxjs';
|
||||
import { EMPTY, mergeMap, concatMap } from 'rxjs';
|
||||
import type { Message } from 'discord.js';
|
||||
import { sharedEventStream } from '../core/_internal';
|
||||
import type { DependencyList } from '../types/ioc';
|
||||
|
||||
import { createMessageHandler, executeModule, makeModuleExecutor } from './event-utils';
|
||||
import { PayloadType, SernError } from '../core/structures/enums'
|
||||
import { resultPayload } from '../core/functions'
|
||||
import { filterTap } from '../core/operators'
|
||||
/**
|
||||
* Ignores messages from any person / bot except itself
|
||||
* @param prefix
|
||||
@@ -24,16 +27,17 @@ export function messageHandler(
|
||||
log?.debug({ message: 'No prefix found. message handler shutting down' });
|
||||
return EMPTY;
|
||||
}
|
||||
const modules = new Map()
|
||||
const messageStream$ = sharedEventStream<Message>(client, 'messageCreate');
|
||||
// const handle = createMessageHandler(messageStream$, defaultPrefix, modules);
|
||||
//
|
||||
// const msgCommands$ = handle(isNonBot(defaultPrefix));
|
||||
//
|
||||
// return msgCommands$.pipe(
|
||||
// filterTap((e) => emitter.emit('warning', resultPayload(PayloadType.Warning, undefined, e))),
|
||||
// concatMap(makeModuleExecutor(module => {
|
||||
// const result = resultPayload(PayloadType.Failure, module, SernError.PluginFailure);
|
||||
// emitter.emit('module.activate', result);
|
||||
// })),
|
||||
// mergeMap(payload => executeModule(emitter, log, err, payload)));
|
||||
const handle = createMessageHandler(messageStream$, defaultPrefix, modules);
|
||||
|
||||
const msgCommands$ = handle(isNonBot(defaultPrefix));
|
||||
|
||||
return msgCommands$.pipe(
|
||||
filterTap((e) => emitter.emit('warning', resultPayload(PayloadType.Warning, undefined, e))),
|
||||
concatMap(makeModuleExecutor(module => {
|
||||
const result = resultPayload(PayloadType.Failure, module, SernError.PluginFailure);
|
||||
emitter.emit('module.activate', result);
|
||||
})),
|
||||
mergeMap(payload => executeModule(emitter, log, err, payload)));
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { interval, scan, startWith, fromEvent, take, of } from "rxjs"
|
||||
import { concatMap, from, interval, of, map, scan, startWith, fromEvent, take } from "rxjs"
|
||||
import { PresenceConfig, PresenceResult } from "../core/presences";
|
||||
import { Services } from "../core/ioc";
|
||||
import assert from "node:assert";
|
||||
|
||||
import * as Files from "../core/module-loading";
|
||||
type SetPresence = (conf: PresenceResult) => Promise<unknown>
|
||||
|
||||
const parseConfig = async (conf: Promise<PresenceResult>) => {
|
||||
@@ -21,19 +22,23 @@ const parseConfig = async (conf: Promise<PresenceResult>) => {
|
||||
})
|
||||
};
|
||||
|
||||
// const presence = Files
|
||||
// .importModule<PresenceModule>(path)
|
||||
// .then(({ module }) => {
|
||||
// //fetch services with the order preserved, passing it to the execute fn
|
||||
// const fetchedServices = Services(...module.inject ?? []);
|
||||
// return async () => module.execute(...fetchedServices);
|
||||
// })
|
||||
// const module$ = from(presence);
|
||||
// return module$.pipe(
|
||||
// //compose:
|
||||
// //call the execute function, passing that result into parseConfig.
|
||||
// //concatMap resolves the promise, and passes it to the next concatMap.
|
||||
// concatMap(fn => parseConfig(fn())),
|
||||
// // subscribe to the observable parseConfig yields, and set the presence.
|
||||
// concatMap(conf => conf.pipe(map(setPresence))));
|
||||
|
||||
export const presenceHandler = (path: string, setPresence: SetPresence) => {
|
||||
interface PresenceModule {
|
||||
module: PresenceConfig<(keyof Dependencies)[]>
|
||||
}
|
||||
const presence = Files
|
||||
.importModule<PresenceModule>(path)
|
||||
.then(({ module }) => {
|
||||
//fetch services with the order preserved, passing it to the execute fn
|
||||
const fetchedServices = Services(...module.inject ?? []);
|
||||
return async () => module.execute(...fetchedServices);
|
||||
})
|
||||
const module$ = from(presence);
|
||||
return module$.pipe(
|
||||
//compose:
|
||||
//call the execute function, passing that result into parseConfig.
|
||||
//concatMap resolves the promise, and passes it to the next concatMap.
|
||||
concatMap(fn => parseConfig(fn())),
|
||||
// subscribe to the observable parseConfig yields, and set the presence.
|
||||
concatMap(conf => conf.pipe(map(setPresence))));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ObservableInput, concat, first, fromEvent, ignoreElements, pipe, tap }
|
||||
import { _Module } from '../core/_internal';
|
||||
import { Logging } from '../core/interfaces';
|
||||
import type { DependencyList } from '../types/ioc';
|
||||
import { callInitPlugins } from './event-utils';
|
||||
|
||||
const once = (log: Logging | undefined) => pipe(
|
||||
tap(() => { log?.info({ message: "Waiting on discord client to be ready..." }) }),
|
||||
@@ -15,8 +16,7 @@ export function readyHandler(
|
||||
//Todo: add module manager on on ready
|
||||
const ready$ = fromEvent(client!, 'ready').pipe(once(log));
|
||||
|
||||
concat(ready$)
|
||||
//.pipe(callInitPlugins(sEmitter))
|
||||
return concat(ready$).pipe(callInitPlugins(sEmitter))
|
||||
// const validModuleType = module.type >= 0 && module.type <= 1 << 10;
|
||||
// assert.ok(validModuleType,
|
||||
// `Found ${module.name} at ${module.meta.fullPath}, which does not have a valid type`);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ObservableInput } from 'rxjs';
|
||||
import { EventType } from '../core/structures/enums';
|
||||
import { SernError } from '../core/_internal';
|
||||
import { EventType, SernError } from '../core/structures/enums';
|
||||
import { eventDispatcher } from './event-utils'
|
||||
import { Service } from '../core/ioc';
|
||||
import type { DependencyList } from '../types/ioc';
|
||||
@@ -23,14 +22,14 @@ export function eventsHandler(
|
||||
throw Error(SernError.InvalidModuleType + ' while creating event handler');
|
||||
}
|
||||
};
|
||||
// buildModules<EventModule>(allPaths)
|
||||
// .pipe(
|
||||
// callInitPlugins(emitter),
|
||||
// map(intoDispatcher),
|
||||
// /**
|
||||
// * Where all events are turned on
|
||||
// */
|
||||
// mergeAll(),
|
||||
// handleCrash(err, emitter, log))
|
||||
// .subscribe();
|
||||
buildModules<EventModule>(allPaths)
|
||||
.pipe(
|
||||
callInitPlugins(emitter),
|
||||
map(intoDispatcher),
|
||||
/**
|
||||
* Where all events are turned on
|
||||
*/
|
||||
mergeAll(),
|
||||
handleCrash(err, emitter, log))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
63
src/sern.ts
63
src/sern.ts
@@ -1,44 +1,65 @@
|
||||
import callsites from 'callsites';
|
||||
import * as Files from './core/module-loading';
|
||||
import * as Files from './core/module-loading';
|
||||
import { merge } from 'rxjs';
|
||||
import { Services } from './core/ioc';
|
||||
import type { DependencyList } from './types/ioc';
|
||||
import { eventsHandler } from './handlers/user-defined-events';
|
||||
import { readyHandler } from './handlers/ready-event';
|
||||
import { messageHandler } from './handlers/message';
|
||||
import { interactionHandler } from './handlers/interaction';
|
||||
import { presenceHandler } from './handlers/presence';
|
||||
import { Client } from 'discord.js';
|
||||
import { handleCrash } from './handlers/event-utils';
|
||||
|
||||
interface Wrapper {
|
||||
commands?: string;
|
||||
defaultPrefix?: string;
|
||||
events?: string;
|
||||
}
|
||||
|
||||
const __start = (entryPoint: string,
|
||||
wrapper: { defaultPrefix?: string },
|
||||
dependencies: DependencyList) => {
|
||||
//@ts-ignore sern handler generates handler.js
|
||||
import(entryPoint)
|
||||
.then(({ commands=new Map(), events=new Map() }) => {
|
||||
console.log(commands, events)
|
||||
})
|
||||
.catch(err => dependencies[2]?.error({ message: err }));
|
||||
}
|
||||
/**
|
||||
* @since 1.0.0
|
||||
* @param wrapper Options to pass into sern.
|
||||
* Function to start the handler up
|
||||
* @example
|
||||
* ```ts title="src/index.ts"
|
||||
* Sern.init()
|
||||
* Sern.init({
|
||||
* commands: 'dist/commands',
|
||||
* events: 'dist/events',
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function init(wrapper: Wrapper) {
|
||||
export function init(wrapper?: Wrapper) {
|
||||
wrapper ??= { commands: "./dist/commands", events: "./dist/events" };
|
||||
const startTime = performance.now();
|
||||
const dependencies = Services('@sern/emitter',
|
||||
'@sern/errors',
|
||||
'@sern/logger',
|
||||
'@sern/client');
|
||||
const logger = dependencies[2],
|
||||
errorHandler = dependencies[1];
|
||||
|
||||
if (wrapper.events !== undefined) {
|
||||
eventsHandler(dependencies, Files.getFullPathTree(wrapper.events));
|
||||
}
|
||||
|
||||
const initCallsite = callsites()[1].getFileName();
|
||||
const handlerModule = Files.shouldHandle(initCallsite!, "handler");
|
||||
if(!handlerModule.exists) {
|
||||
throw Error("Could not find handler module, did you run sern build?")
|
||||
}
|
||||
__start(handlerModule.path, wrapper, dependencies);
|
||||
}
|
||||
const presencePath = Files.shouldHandle(initCallsite!, "presence");
|
||||
//Ready event: load all modules and when finished, time should be taken and logged
|
||||
readyHandler(dependencies, Files.getFullPathTree(wrapper.commands))
|
||||
.add(() => {
|
||||
logger?.info({ message: "Client signaled ready, registering modules" });
|
||||
const time = ((performance.now() - startTime) / 1000).toFixed(2);
|
||||
dependencies[0].emit('modulesLoaded');
|
||||
logger?.info({ message: `sern: registered in ${time} s`, });
|
||||
if(presencePath.exists) {
|
||||
const setPresence = async (p: any) => {
|
||||
return (dependencies[4] as Client).user?.setPresence(p);
|
||||
}
|
||||
presenceHandler(presencePath.path, setPresence).subscribe();
|
||||
}
|
||||
});
|
||||
|
||||
const messages$ = messageHandler(dependencies, wrapper.defaultPrefix);
|
||||
const interactions$ = interactionHandler(dependencies);
|
||||
// listening to the message stream and interaction stream
|
||||
merge(messages$, interactions$).pipe(handleCrash(errorHandler, dependencies[0], logger)).subscribe();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user