refactor and clean up and reenter v3 module loading

This commit is contained in:
Jacob Nguyen
2024-05-13 15:27:12 -05:00
parent 8554eeaef4
commit 6e2f4b616f
13 changed files with 152 additions and 101 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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';
/**

View File

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

View 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)));
}

View File

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

View File

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

View File

@@ -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`);

View File

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

View File

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