ready handler revamped so much cleaner

This commit is contained in:
Jacob Nguyen
2024-05-14 23:19:57 -05:00
parent 880311f08c
commit a7aea4be1a
20 changed files with 153 additions and 148 deletions

View File

@@ -1,15 +1,7 @@
import type { Result } from 'ts-results-es'
import { CommandType, EventType, Plugin } from '..';
import { AnyFunction } from '../types/utility';
import { Module } from '../types/core-modules';
export * from './functions';
export type _Module = {
meta: {
id: string,
absPath: string
}
name: string,
execute : Function
[key: PropertyKey]: unknown
}
export type VoidResult = Result<void, void>;

View File

@@ -21,6 +21,7 @@ export interface Emitter {
addListener(eventName: string | symbol, listener: AnyFunction): this;
removeListener(eventName: string | symbol, listener: AnyFunction): this;
emit(eventName: string | symbol, ...payload: any[]): boolean;
on(eventName: string | symbol, listener: AnyFunction): this
}

View File

@@ -75,6 +75,8 @@ async function composeRoot(
__add_container('@sern/logger', new __Services.DefaultLogging());
}
__add_container('@sern/errors', new __Services.DefaultErrorHandling());
__add_container('@sern/cron', {})
__add_container('@sern/modules', new Map())
//Build the container based on the callback provided by the user
conf.build(container as Container);
@@ -99,6 +101,7 @@ export async function makeDependencies (conf: ValidDependencyConfig) {
__add_container('@sern/logger', new __Services.DefaultLogging);
}
__add_container('@sern/errors', new __Services.DefaultErrorHandling());
__add_container('@sern/cron', {})
await useContainerRaw().ready();
} else {
await composeRoot(useContainerRaw(), conf);

View File

@@ -4,7 +4,7 @@ import * as __Services from '../structures/default-services';
* A semi-generic container that provides error handling, emitter, and module store.
* For the handler to operate correctly, The only user provided dependency needs to be @sern/client
*/
export function hasCallableMethod(obj: object, name: PropertyKey) {
function hasCallableMethod(obj: object, name: PropertyKey) {
//@ts-ignore
return Object.hasOwn(obj, name) && typeof obj[name] == 'function';
}

View File

@@ -3,7 +3,7 @@ import { existsSync } from 'fs';
import { readdir } from 'fs/promises';
import assert from 'node:assert';
import * as Id from './id'
import type { _Module } from './_internal';
import { Module } from '../types/core-modules';
export const parseCallsite = (site: string) => {
const pathobj = path.parse(site.replace(/file:\\?/, "")
@@ -38,11 +38,11 @@ export const shouldHandle = (pth: string, filenam: string) => {
export async function importModule<T>(absPath: string) {
let fileModule = await import(absPath);
let commandModule: _Module = fileModule.default;
let commandModule: Module = fileModule.default;
assert(commandModule , `No export @ ${absPath}. Forgot to ignore with "!"? (!${path.basename(absPath)})?`);
if ('default' in commandModule) {
commandModule = commandModule.default as _Module;
commandModule = commandModule.default as Module;
}
const p = path.parse(absPath)
commandModule.name ??= p.name; commandModule.description ??= "...";
@@ -51,21 +51,18 @@ export async function importModule<T>(absPath: string) {
id: Id.create(commandModule.name, commandModule.type),
absPath,
};
return { module: commandModule } as T;
return { module: commandModule as T };
}
export async function* readRecursive(dir: string): AsyncGenerator<string> {
const files = await readdir(dir, { withFileTypes: true, recursive: true });
const files = await readdir(dir, { recursive: true, withFileTypes: true });
for (const file of files) {
const fullPath = path.join(file.path, file.name);
const fullPath = path.join(file.parentPath, file.name);
if(!file.name.startsWith('!') && !file.isDirectory()) {
yield fullPath;
}
}
}
export const fmtFileName = (fileName: string) => path.parse(fileName).name;
export const filename = (p: string) => fmtFileName(path.basename(p));

View File

@@ -4,37 +4,35 @@ import type { AnyEventPlugin, } from '../types/core-plugin';
import type {
InputCommand,
InputEvent,
Module,
} from '../types/core-modules';
import { type _Module, partitionPlugins } from './_internal';
import { partitionPlugins } from './functions'
import type { Awaitable } from '../types/utility';
/**
* @since 1.0.0 The wrapper function to define command modules for sern
* @param mod
*/
export function commandModule(mod: InputCommand): _Module {
export function commandModule(mod: InputCommand): Module {
const [onEvent, plugins] = partitionPlugins(mod.plugins);
//@ts-ignore
return {
...mod,
onEvent,
plugins,
};
} as Module;
}
/**
* @since 1.0.0
* The wrapper function to define event modules for sern
* @param mod
*/
export function eventModule(mod: InputEvent): _Module {
export function eventModule(mod: InputEvent): Module {
const [onEvent, plugins] = partitionPlugins(mod.plugins);
//@ts-ignore
return {
...mod,
plugins,
onEvent,
};
} as Module;
}
/** Create event modules from discord.js client events,

View File

@@ -15,10 +15,6 @@ import {
} from 'rxjs';
import {
type VoidResult,
resultPayload,
isAutocomplete,
treeSearch,
_Module,
} from '../core/_internal';
import * as Id from '../core/id'
import type { Emitter, ErrorHandling, Logging } from '../core/interfaces';
@@ -42,7 +38,7 @@ function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[
const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.options];
return [ctx, args] as [Context, Args];
}
import { resultPayload, isAutocomplete, treeSearch } from '../core/functions'
function intoPayload(module: Processed<Module>, ) {
return pipe(map(arrayifySource),
@@ -127,7 +123,7 @@ export function fmt(msg: string, prefix: string): string[] {
*/
export function createInteractionHandler<T extends Interaction>(
source: Observable<Interaction>,
mg: Map<string, _Module>, //TODO
mg: Map<string, Module>, //TODO
) {
return createGenericHandler<Interaction, T, Result<ReturnType<typeof createDispatcher>, void>>(
source,
@@ -135,7 +131,7 @@ export function createInteractionHandler<T extends Interaction>(
const possibleIds = Id.reconstruct(event);
let fullPaths= possibleIds
.map(id => mg.get(id))
.filter((id): id is _Module => id !== undefined);
.filter((id): id is Module => id !== undefined);
if(fullPaths.length == 0) {
return Err.EMPTY;
@@ -230,25 +226,6 @@ export function createResultResolver<
};
};
/**
* Calls a module's init plugins and checks for Err. If so, call { onStop } and
* ignore the module
*/
export function callInitPlugins<T extends Processed<Module>>(sernEmitter: Emitter) {
return concatMap(
createResultResolver({
createStream: args => from(args.module.plugins).pipe(callPlugin(args)),
onStop: (module: T) => {
sernEmitter.emit('module.register', resultPayload(PayloadType.Failure, module, SernError.PluginFailure));
},
onNext: (payload) => {
sernEmitter.emit('module.register', resultPayload(PayloadType.Success, payload.module));
return payload as { module: T; metadata: CommandMeta };
},
}),
);
}
/**
* Creates an executable task ( execute the command ) if all control plugins are successful
* @param onStop emits a failure response to the SernEmitter
@@ -264,19 +241,16 @@ export function makeModuleExecutor<
});
return createResultResolver({
onStop,
createStream: ({ args, module }) =>
from(module.onEvent)
.pipe(callPlugin(args)),
createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)),
onNext,
})
}
export const handleCrash = (err: ErrorHandling,sernemitter: Emitter, log?: Logging) =>
pipe(
catchError(handleError(err, sernemitter, log)),
pipe(catchError(handleError(err, sernemitter, log)),
finalize(() => {
log?.info({
message: 'A stream closed or reached end of lifetime',
});
disposeAll(log);
}));
}))

View File

@@ -2,16 +2,11 @@ import type { Interaction } from 'discord.js';
import { mergeMap, merge, concatMap } from 'rxjs';
import { PayloadType } from '../core/structures/enums';
import { filterTap, sharedEventStream } from '../core/operators'
import {
isAutocomplete,
isCommand,
isMessageComponent,
isModal,
resultPayload,
} from '../core/_internal';
import { createInteractionHandler, executeModule, makeModuleExecutor } from './event-utils';
import type { DependencyList } from '../types/ioc';
import { SernError } from '../core/structures/enums'
import { isAutocomplete, isCommand, isMessageComponent, isModal, resultPayload, } from '../core/functions'
export function interactionHandler([emitter, err, log, client]: DependencyList) {
const interactionStream$ = sharedEventStream<Interaction>(client, 'interactionCreate');
const modules = new Map();
@@ -25,5 +20,5 @@ export function interactionHandler([emitter, err, log, client]: DependencyList)
.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)));
mergeMap(payload => executeModule(emitter, log, err, payload)));
}

View File

@@ -20,16 +20,15 @@ function hasPrefix(prefix: string, content: string) {
}
export function messageHandler(
[emitter, err, log, client]: DependencyList,
[emitter, err, log, client, commands]: DependencyList,
defaultPrefix: string | undefined,
) {
if (!defaultPrefix) {
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 handle = createMessageHandler(messageStream$, defaultPrefix, commands);
const msgCommands$ = handle(isNonBot(defaultPrefix));

View File

@@ -23,11 +23,8 @@ const parseConfig = async (conf: Promise<PresenceResult>) => {
};
export const presenceHandler = (path: string, setPresence: SetPresence) => {
interface PresenceModule {
module: PresenceConfig<(keyof Dependencies)[]>
}
const presence = Files
.importModule<PresenceModule>(path)
.importModule<PresenceConfig<(keyof Dependencies)[]>>(path)
.then(({ module }) => {
//fetch services with the order preserved, passing it to the execute fn
const fetchedServices = Services(...module.inject ?? []);

View File

@@ -1,25 +0,0 @@
import { concat, first, fromEvent, ignoreElements, pipe, tap } from 'rxjs';
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..." }) }),
first(),
ignoreElements())
export function readyHandler(
[sEmitter, , log, client]: DependencyList,
) {
//Todo: add module manager on on ready
const ready$ = fromEvent(client!, 'ready').pipe(once(log));
return concat(ready$).pipe(callInitPlugins(sEmitter)).subscribe();
// 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`);
}

30
src/handlers/ready.ts Normal file
View File

@@ -0,0 +1,30 @@
import type { DependencyList } from '../types/ioc';
import * as Files from '../core/module-loading'
import { once } from 'events';
import { resultPayload } from '../core/functions';
import { PayloadType } from '..';
import { SernError } from '../core/structures/enums';
import { Module } from '../types/core-modules';
export default async function(dir: string, [sEmitter,, log, client, commands]: DependencyList) {
log?.info({ message: "Waiting on discord client to be ready..." })
await once(client, "ready");
log?.info({ message: "Client signaled ready, registering modules" });
for await (const path of Files.readRecursive(dir)) {
const { module } = await Files.importModule<Module>(path);
const validModuleType = module.type >= 0 && module.type <= 1 << 10;
if(!validModuleType) {
throw Error(`Found ${module.name} at ${module.meta.absPath}, which has an incorrect \`type\``);
}
for(const plugin of module.plugins) {
const res = await plugin.execute({ module, absPath: module.meta.absPath });
if(res.isErr()) {
sEmitter.emit('module.register', resultPayload(PayloadType.Failure, module, SernError.PluginFailure));
throw Error("Plugin failed with controller.stop()");
}
}
commands.set(module.meta.id, module);
sEmitter.emit('module.register', resultPayload(PayloadType.Success, module));
}
sEmitter.emit('modulesLoaded');
}

View File

@@ -1,14 +1,10 @@
import { ObservableInput, map, mergeAll } from 'rxjs';
import { EventType, SernError } from '../core/structures/enums';
import { callInitPlugins, eventDispatcher, handleCrash } from './event-utils'
import { eventDispatcher } from './event-utils'
import { Service } from '../core/ioc';
import type { DependencyList } from '../types/ioc';
import type { EventModule, Processed } from '../types/core-modules';
export function eventsHandler(
[emitter, err, log, client]: DependencyList,
//allPaths: ObservableInput<string>,
) {
export default function( [emitter, err, log, client]: DependencyList, eventDir: string) {
//code smell
const intoDispatcher = (e: { module: Processed<EventModule> }) => {
switch (e.module.type) {
@@ -18,6 +14,9 @@ export function eventsHandler(
return eventDispatcher(e.module, client);
case EventType.External:
return eventDispatcher(e.module, Service(e.module.emitter));
case EventType.Cron:
//@ts-ignore
return eventDispatcher(e.module, Service('@sern/cron'))
default:
throw Error(SernError.InvalidModuleType + ' while creating event handler');
}

View File

@@ -38,7 +38,7 @@ export type {
export type { Args, SlashOptions, Payload, SernEventsMapping } from './types/utility';
export type { Singleton, Transient, CoreDependencies } from './types/ioc';
export type { CoreDependencies } from './types/ioc';
export {
commandModule,

View File

@@ -2,8 +2,8 @@ import callsites from 'callsites';
import * as Files from './core/module-loading';
import { merge } from 'rxjs';
import { Services } from './core/ioc';
import { eventsHandler } from './handlers/user-defined-events';
import { readyHandler } from './handlers/ready-event';
import eventsHandler from './handlers/user-defined-events';
import ready from './handlers/ready';
import { messageHandler } from './handlers/message';
import { interactionHandler } from './handlers/interaction';
import { presenceHandler } from './handlers/presence';
@@ -11,9 +11,9 @@ import { Client } from 'discord.js';
import { handleCrash } from './handlers/event-utils';
interface Wrapper {
commands?: string;
commands: string;
defaultPrefix?: string;
events?: string;
events: string;
}
/**
* @since 1.0.0
@@ -27,37 +27,38 @@ interface Wrapper {
* })
* ```
*/
export function init(wrapper: Wrapper = { commands: "./dist/commands", events: "./dist/events" }) {
export function init(maybeWrapper: Wrapper = { commands: "./dist/commands", events: "./dist/events" }) {
const startTime = performance.now();
const dependencies = Services('@sern/emitter',
'@sern/errors',
'@sern/logger',
'@sern/client');
'@sern/client',
'@sern/modules');
const logger = dependencies[2],
errorHandler = dependencies[1];
if (wrapper.events !== undefined) {
eventsHandler(dependencies);
if (maybeWrapper.events !== undefined) {
eventsHandler(dependencies, maybeWrapper.events);
}
const initCallsite = callsites()[1].getFileName();
const presencePath = Files.shouldHandle(initCallsite!, "presence");
//Ready event: load all modules and when finished, time should be taken and logged
readyHandler(dependencies)
.add(() => {
logger?.info({ message: "Client signaled ready, registering modules" });
ready(maybeWrapper.commands, dependencies)
.then(() => {
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) => {
//@ts-ignore
return (dependencies[3] as Client).user?.setPresence(p);
}
presenceHandler(presencePath.path, setPresence).subscribe();
}
});
})
.catch(err => { throw err });
const messages$ = messageHandler(dependencies, wrapper.defaultPrefix);
const messages$ = messageHandler(dependencies, maybeWrapper.defaultPrefix);
const interactions$ = interactionHandler(dependencies);
// listening to the message stream and interaction stream
merge(messages$, interactions$).pipe(handleCrash(errorHandler, dependencies[0], logger)).subscribe();

View File

@@ -35,6 +35,10 @@ export interface Module {
onEvent: ControlPlugin[];
plugins: InitPlugin[];
description?: string;
meta: {
id: string;
absPath: string;
}
execute(...args: any[]): Awaitable<any>;
}
@@ -44,12 +48,18 @@ export interface SernEventCommand<T extends keyof SernEventsMapping = keyof Sern
type: EventType.Sern;
execute(...args: SernEventsMapping[T]): Awaitable<unknown>;
}
export interface ExternalEventCommand extends Module {
name?: string;
emitter: keyof Dependencies;
type: EventType.External;
execute(...args: unknown[]): Awaitable<unknown>;
}
export interface CronEventCommand extends Module {
name?: string;
type: EventType.Cron;
execute(...args: unknown[]): Awaitable<unknown>;
}
export interface ContextMenuUser extends Module {
type: CommandType.CtxUser;
@@ -127,7 +137,7 @@ export interface BothCommand extends Module {
execute: (ctx: Context, args: Args) => Awaitable<unknown>;
}
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand | CronEventCommand;
export type CommandModule =
| TextCommand
| SlashCommand
@@ -178,10 +188,10 @@ export interface SernAutocompleteData
}
type CommandModuleNoPlugins = {
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent'>;
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent' | 'meta'>;
};
type EventModulesNoPlugins = {
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent'>;
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent' | 'meta'>;
};
export type InputEvent = {

View File

@@ -1,29 +1,22 @@
import type { Container } from '../core/ioc/container';
import * as Contracts from '../core/interfaces';
import type { UnpackFunction } from './utility'
/**
* Type to annotate that something is a singleton.
* T is created once and lazily.
*/
export type Singleton<T> = () => T;
/**
* Type to annotate that something is transient.
* Every time this is called, a new object is created
*/
export type Transient<T> = () => () => T;
import type { Client } from 'discord.js'
import { Module } from './core-modules';
export type DependencyList = [
Contracts.Emitter,
Contracts.ErrorHandling,
Contracts.Logging | undefined,
Contracts.Emitter,
Client,
Map<string, Module>
];
export interface CoreDependencies {
'@sern/client': () => Contracts.Emitter;
'@sern/client': () => Client;
'@sern/emitter': () => Contracts.Emitter;
'@sern/errors': () => Contracts.ErrorHandling;
'@sern/logger'?: () => Contracts.Logging;
'@sern/modules': () => Map<string, Module>
}
export type DependencyFromKey<T extends keyof Dependencies> = Dependencies[T];

View File

@@ -4,7 +4,7 @@ import type { Module } from './core-modules';
export type Awaitable<T> = PromiseLike<T> | T;
export type AnyFunction = (...args: unknown[]) => unknown;
export type AnyFunction = (...args: never[]) => unknown;
// Thanks to @kelsny
type ParseType<T> = {