refactor: move metadata outside of module declarations

This commit is contained in:
Jacob Nguyen
2023-05-15 17:10:55 -05:00
parent b06e457caa
commit b2ab99b7cc
9 changed files with 90 additions and 61 deletions

View File

@@ -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<CommandModule[]>;
remove(id: string) : boolean

View File

@@ -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<string, string>;
metadata : WeakMap<Module, CommandMeta>
}

View File

@@ -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<T> = Promise<Result<ImportPayload<T>, SernError>>;
@@ -18,14 +18,16 @@ export async function importModule<T>(absPath: string) {
/// #endif
}
export async function defaultModuleLoader<T extends Module>(absPath: string): ModuleResult<T> {
const module = await importModule<T>(absPath);
let module = await importModule<T>(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 });
}

View File

@@ -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.');

View File

@@ -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<any>;
}
@@ -108,7 +106,7 @@ export interface ModalSubmitCommand extends Module {
}
export interface AutocompleteCommand
extends Omit<Module, 'name' | 'type' | 'plugins' | 'description' | typeof sernMeta> {
extends Omit<Module, 'name' | 'type' | 'plugins' | 'description'> {
onEvent: ControlPlugin[];
execute: (ctx: AutocompleteInteraction) => Awaitable<unknown>;
}
@@ -191,10 +189,10 @@ export interface SernAutocompleteData
}
export type CommandModuleNoPlugins = {
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent' | typeof sernMeta>;
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent'>;
};
export type EventModulesNoPlugins = {
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent' | typeof sernMeta>;
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent'>;
};
export type InputEvent = {

View File

@@ -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<T extends keyof ClientEvents>(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<Type extends CommandType> {
export abstract class CommandExecutable<const Type extends CommandType> {
abstract type: Type;
[sernMeta] = {
id: UNREGISTERED,
fullPath: EMPTY_PATH
};
plugins: InitPlugin[] = [];
onEvent: ControlPlugin[] = [];
abstract execute() : Awaitable<unknown>
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<Type, PluginType.Control>) : Awaitable<unknown>
}
/**
* @Experimental
* Will be refactored in future
*/
export abstract class EventExecutable<Type extends EventType> {
abstract type: Type;
[sernMeta] = {
id: UNREGISTERED,
fullPath: EMPTY_PATH
};
plugins: InitPlugin[] = [];
onEvent: ControlPlugin[] = [];
abstract execute(): Awaitable<unknown>;
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<Type, PluginType.Control>): Awaitable<unknown>;
}

View File

@@ -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<T extends Module>(): MonoTypeOperatorFunction<ImportPayload<T>> {
function assignDefaults<T extends Module>(moduleManager: ModuleManager): MonoTypeOperatorFunction<ImportPayload<T>> {
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<T extends Module>(): MonoTypeOperatorFunction<ImportPayl
export function buildModules<T extends AnyModule>(
input: ObservableInput<string>,
sernEmitter: SernEmitter,
moduleManager: ModuleManager
) {
return pipe(
switchMap(() => Files.buildModuleStream<T>(input)),
errTap(error => {
sernEmitter.emit('module.register', SernEmitter.failure(undefined, error));
}),
assignDefaults<T>()
assignDefaults<T>(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

View File

@@ -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<Processed<AnyModule>>(allPaths, sEmitter),
buildModules<Processed<AnyModule>>(allPaths, sEmitter, moduleManager),
callInitPlugins({
onStop: module => {
sEmitter.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure));
@@ -39,7 +38,9 @@ function registerModule<T extends Processed<AnyModule>>(
manager: ModuleManager,
module: T,
): Result<void, void> {
const { id, fullPath } = module[sernMeta];
const { id, fullPath } = manager.getMetadata(module);
if (module.type === CommandType.Both
|| module.type === CommandType.Text
) {

View File

@@ -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<string>,
) {
@@ -34,7 +34,7 @@ export function makeEventsHandler(
};
of(null)
.pipe(
buildModules<Processed<EventModule>>(allPaths, emitter),
buildModules<Processed<EventModule>>(allPaths, emitter, moduleManager),
callInitPlugins({
onStop: module =>
emitter.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),