diff --git a/src/core/contracts/module-manager.ts b/src/core/contracts/module-manager.ts index 70cb359..5834897 100644 --- a/src/core/contracts/module-manager.ts +++ b/src/core/contracts/module-manager.ts @@ -1,10 +1,12 @@ -import { CommandModule } from '../types/modules'; +import { CommandMeta, CommandModule, Module } from '../types/modules'; /** * @since 2.0.0 */ export interface ModuleManager { get(id: string): string | undefined; + getMetadata(m: Module): CommandMeta; + setMetadata(m: Module, c: CommandMeta): void; set(id: string, path: string): void; getPublishableCommands(): Promise; remove(id: string) : boolean diff --git a/src/core/contracts/module-store.ts b/src/core/contracts/module-store.ts index c008b9a..2d4b0b2 100644 --- a/src/core/contracts/module-store.ts +++ b/src/core/contracts/module-store.ts @@ -1,6 +1,9 @@ +import { CommandMeta, Module } from "../types/modules"; + /** * Represents a core module store that stores IDs mapped to file paths. */ export interface CoreModuleStore { commands : Map; + metadata : WeakMap } diff --git a/src/core/module-loading.ts b/src/core/module-loading.ts index d397d40..33ae374 100644 --- a/src/core/module-loading.ts +++ b/src/core/module-loading.ts @@ -6,7 +6,7 @@ import { readdir, stat } from 'fs/promises'; import { basename, extname, join, resolve } from 'path'; import { ImportPayload } from '../handler/types'; import * as assert from 'node:assert'; -import { sernMeta } from '../handler/commands'; +import { clazz } from '../handler/commands'; export type ModuleResult = Promise, SernError>>; @@ -18,14 +18,16 @@ export async function importModule(absPath: string) { /// #endif } export async function defaultModuleLoader(absPath: string): ModuleResult { - const module = await importModule(absPath); + let module = await importModule(absPath); if (module === undefined) { return Err(SernError.UndefinedModule); } + if(Reflect.has(module, clazz)) { + //@ts-ignore + module = module.getInstance(); + } //todo readd class modules assert.ok(module.type > 0 && module.type < 1<<10, 'Found a module that does not have a valid type'); - assert.ok(module[sernMeta], "Found a module that isn't marked with sernMeta"); - return Ok({ module, absPath }); } diff --git a/src/core/structures/services/module-manager.ts b/src/core/structures/services/module-manager.ts index 55cdad0..eb34a24 100644 --- a/src/core/structures/services/module-manager.ts +++ b/src/core/structures/services/module-manager.ts @@ -1,14 +1,24 @@ import { CoreModuleStore, ModuleManager } from '../../contracts'; import { importModule } from '../../module-loading'; -import { CommandModule } from '../../types/modules'; - +import { CommandMeta, CommandModule, Module } from '../../types/modules'; /** * @internal -* @since 2.0.0/* +* @since 2.0.0 * Version 4.0.0 will internalize this api. Please refrain from using ModuleStore! */ export class DefaultModuleManager implements ModuleManager { constructor(private moduleStore: CoreModuleStore) {} + setMetadata(m: Module, c: CommandMeta): void { + this.moduleStore.metadata.set(m, c); + } + + getMetadata(m: Module): CommandMeta { + const maybeModule = this.moduleStore.metadata.get(m); + if(!maybeModule) { + throw Error("Could not find metadata in store for " + maybeModule); + } + return maybeModule; + } remove(id: string): boolean { throw new Error('Method not implemented.'); diff --git a/src/core/types/modules.ts b/src/core/types/modules.ts index 4a854ef..327278f 100644 --- a/src/core/types/modules.ts +++ b/src/core/types/modules.ts @@ -26,13 +26,12 @@ import { import { CommandType, Context, EventType } from '../structures'; import { AnyCommandPlugin, AnyEventPlugin, ControlPlugin, InitPlugin } from './plugins'; import { Awaitable, SernEventsMapping } from '../../shared'; -import { sernMeta } from '../../handler/commands'; import { Processed } from '../../handler/types'; import { Args, SlashOptions } from '../../shared'; -interface CommandMeta { +export interface CommandMeta { fullPath: string; id: string; } @@ -45,7 +44,6 @@ export interface Module { onEvent: ControlPlugin[]; plugins: InitPlugin[]; description?: string; - [sernMeta]: CommandMeta; execute: (...args: any[]) => Awaitable; } @@ -108,7 +106,7 @@ export interface ModalSubmitCommand extends Module { } export interface AutocompleteCommand - extends Omit { + extends Omit { onEvent: ControlPlugin[]; execute: (ctx: AutocompleteInteraction) => Awaitable; } @@ -191,10 +189,10 @@ export interface SernAutocompleteData } export type CommandModuleNoPlugins = { - [T in CommandType]: Omit; + [T in CommandType]: Omit; }; export type EventModulesNoPlugins = { - [T in EventType]: Omit; + [T in EventType]: Omit; }; export type InputEvent = { diff --git a/src/handler/commands.ts b/src/handler/commands.ts index 140433f..2f388c2 100644 --- a/src/handler/commands.ts +++ b/src/handler/commands.ts @@ -1,29 +1,22 @@ import { ClientEvents } from 'discord.js'; import { CommandType, EventType, PluginType } from '../core/structures'; -import { AnyEventPlugin, ControlPlugin, InitPlugin, Plugin } from '../core/types/plugins'; +import { AnyCommandPlugin, AnyEventPlugin, CommandArgs, EventArgs } from '../core/types/plugins'; import { CommandModule, EventModule, InputCommand, InputEvent } from '../core/types/modules'; -import { partition } from '../core/functions'; +import { partitionPlugins } from '../core/functions'; import { Awaitable } from '../shared'; -export const sernMeta = Symbol('@sern/meta'); -export const UNREGISTERED = 'meow meow meow'; -export const EMPTY_PATH = 'purr purr purr'; + + +export const clazz = Symbol('@sern/class'); /** * @since 1.0.0 The wrapper function to define command modules for sern * @param mod */ export function commandModule(mod: InputCommand): CommandModule { - const [onEvent, plugins] = partition( - mod.plugins ?? [], - el => (el as Plugin).type === PluginType.Control, - ); + const [onEvent, plugins] = partitionPlugins(mod.plugins); return { ...mod, onEvent, plugins, - [sernMeta]: { - id: UNREGISTERED, - fullPath: EMPTY_PATH - } } as CommandModule; } /** @@ -32,17 +25,10 @@ export function commandModule(mod: InputCommand): CommandModule { * @param mod */ export function eventModule(mod: InputEvent): EventModule { - const [onEvent, plugins] = partition( - mod.plugins ?? [], - el => (el as Plugin).type === PluginType.Control, - ); + const [onEvent, plugins] = partitionPlugins(mod.plugins); return { onEvent, plugins, - [sernMeta]: { - id: UNREGISTERED, - fullPath: EMPTY_PATH - }, ...mod, } as EventModule; } @@ -64,32 +50,58 @@ export function discordEvent(mod: { }); } + +// +// Class modules: +// Can be refactored. +// Both implement singleton, could I make them inherit a singleton parent class? /** * @Experimental * Will be refactored / changed in future */ -export abstract class CommandExecutable { +export abstract class CommandExecutable { abstract type: Type; - [sernMeta] = { - id: UNREGISTERED, - fullPath: EMPTY_PATH - }; - plugins: InitPlugin[] = []; - onEvent: ControlPlugin[] = []; - abstract execute() : Awaitable + plugins?: AnyCommandPlugin[]; + private static _instance : CommandModule; + static readonly [clazz] = true; + constructor() { + const [onEvent, plugins] = partitionPlugins(this.plugins); + this.plugins = plugins as AnyCommandPlugin[]; + Reflect.set(this, 'onEvent', onEvent); + } + static getInstance() { + if (!CommandExecutable._instance) { + //@ts-ignore + CommandExecutable._instance = new this(); + } + return CommandExecutable._instance; + } + abstract execute(...args: CommandArgs) : Awaitable } + /** * @Experimental * Will be refactored in future */ export abstract class EventExecutable { abstract type: Type; - [sernMeta] = { - id: UNREGISTERED, - fullPath: EMPTY_PATH - }; - plugins: InitPlugin[] = []; - onEvent: ControlPlugin[] = []; - abstract execute(): Awaitable; + plugins?: AnyEventPlugin[]; + static readonly [clazz] = true; + private static _instance : EventModule; + constructor() { + const [onEvent, plugins] = partitionPlugins(this.plugins); + this.plugins = plugins as AnyEventPlugin[]; + Reflect.set(this, 'onEvent', onEvent); + } + static getInstance() { + if (!EventExecutable._instance) { + //@ts-ignore + EventExecutable._instance = new this(); + } + return EventExecutable._instance; + } + abstract execute(...args: EventArgs): Awaitable; } + + diff --git a/src/handler/events/generic.ts b/src/handler/events/generic.ts index b73f152..9d94671 100644 --- a/src/handler/events/generic.ts +++ b/src/handler/events/generic.ts @@ -13,7 +13,6 @@ import { ObservableInput, pipe, switchMap } from 'rxjs'; import { SernEmitter } from '../../core'; import { errTap } from '../../core/operators'; import * as Files from '../../core/module-loading'; -import { sernMeta } from '../commands'; import { Err, Result } from 'ts-results-es'; import { fmt } from './messages'; import { ControlPlugin, VoidResult } from '../../core/types/plugins'; @@ -73,13 +72,14 @@ export function createMessageHandler( * IMPURE SIDE EFFECT * This function assigns remaining, incomplete data to each imported module. */ -function assignDefaults(): MonoTypeOperatorFunction> { +function assignDefaults(moduleManager: ModuleManager): MonoTypeOperatorFunction> { return tap( ({ module, absPath }) => { module.name ??= Files.filename(absPath); module.description ??= '...'; - module[sernMeta].fullPath = absPath; - module[sernMeta].id = `${module.name}_${uniqueId(module.type)}`; + moduleManager.setMetadata( + module, { fullPath: absPath, id: `${module.name}_${uniqueId(module.type)}` } + ); } ); } @@ -87,13 +87,14 @@ function assignDefaults(): MonoTypeOperatorFunction( input: ObservableInput, sernEmitter: SernEmitter, + moduleManager: ModuleManager ) { return pipe( switchMap(() => Files.buildModuleStream(input)), errTap(error => { sernEmitter.emit('module.register', SernEmitter.failure(undefined, error)); }), - assignDefaults() + assignDefaults(moduleManager) ); } @@ -146,7 +147,7 @@ export function executeModule( * A higher order function that * - creates a stream of {@link VoidResult} { config.createStream } * - any failures results to { config.onFailure } being called - * - if all results are ok, the stream is converted to { config.onSuccess } + * - if all results are ok, the stream is converted to { config.onNext } * emit config.onSuccess Observable * @param config * @returns receiver function for flattening a stream of data diff --git a/src/handler/events/ready.ts b/src/handler/events/ready.ts index 271e864..5e476f0 100644 --- a/src/handler/events/ready.ts +++ b/src/handler/events/ready.ts @@ -4,7 +4,6 @@ import { SernError } from '../../core/structures/errors'; import { Result } from 'ts-results-es'; import { ModuleManager } from '../../core/contracts'; import { SernEmitter } from '../../core'; -import { sernMeta } from '../commands'; import { Processed, DependencyList } from '../types'; import { buildModules, callInitPlugins } from './generic'; import { AnyModule } from '../../core/types/modules'; @@ -16,7 +15,7 @@ export function startReadyEvent( const ready$ = fromEvent(client!, 'ready').pipe(take(1)); return ready$ .pipe( - buildModules>(allPaths, sEmitter), + buildModules>(allPaths, sEmitter, moduleManager), callInitPlugins({ onStop: module => { sEmitter.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)); @@ -39,7 +38,9 @@ function registerModule>( manager: ModuleManager, module: T, ): Result { - const { id, fullPath } = module[sernMeta]; + + const { id, fullPath } = manager.getMetadata(module); + if (module.type === CommandType.Both || module.type === CommandType.Text ) { diff --git a/src/handler/events/user-defined.ts b/src/handler/events/user-defined.ts index 03feea2..b21da99 100644 --- a/src/handler/events/user-defined.ts +++ b/src/handler/events/user-defined.ts @@ -13,7 +13,7 @@ import { Dependencies } from '../../core/ioc/types'; export function makeEventsHandler( - [emitter, err, log,, client]: DependencyList, + [emitter, err, log, moduleManager, client]: DependencyList, allPaths: ObservableInput, ) { @@ -34,7 +34,7 @@ export function makeEventsHandler( }; of(null) .pipe( - buildModules>(allPaths, emitter), + buildModules>(allPaths, emitter, moduleManager), callInitPlugins({ onStop: module => emitter.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),