diff --git a/src/core/_internal.ts b/src/core/_internal.ts index 513abaa2..1b2f35c9 100644 --- a/src/core/_internal.ts +++ b/src/core/_internal.ts @@ -5,6 +5,6 @@ export * from './functions'; export type { VoidResult } from '../types/core-plugin'; export { SernError } from './structures/enums'; export { ModuleStore } from './structures/module-store'; -export * as DefaultServices from './structures/services'; -export { useContainerRaw } from './ioc/base' +export * as __Services from './structures/services'; +export { useContainerRaw } from './ioc/base'; diff --git a/src/core/contracts/module-manager.ts b/src/core/contracts/module-manager.ts index 624dac9f..5dbeaee4 100644 --- a/src/core/contracts/module-manager.ts +++ b/src/core/contracts/module-manager.ts @@ -16,12 +16,19 @@ interface MetadataAccess { * @internal - direct access to the module manager will be removed in version 4 */ export interface ModuleManager extends MetadataAccess { - get(id: string): string | undefined; + get(id: string): Module | undefined; - set(id: string, path: string): void; - getPublishableCommands(): Promise; + set(id: string, path: Module): void; + /** + * @deprecated + */ + getPublishableCommands(): CommandModule[]; + + /* + * @deprecated + */ getByNameCommandType( name: string, commandType: T, - ): Promise | undefined; + ): CommandModuleDefs[T] | undefined; } diff --git a/src/core/contracts/module-store.ts b/src/core/contracts/module-store.ts index b6157b60..90818f80 100644 --- a/src/core/contracts/module-store.ts +++ b/src/core/contracts/module-store.ts @@ -4,6 +4,6 @@ import type { CommandMeta, Module } from '../../types/core-modules'; * Represents a core module store that stores IDs mapped to file paths. */ export interface CoreModuleStore { - commands: Map; + commands: Map; metadata: WeakMap; } diff --git a/src/core/functions.ts b/src/core/functions.ts index 60d62e9b..878151b5 100644 --- a/src/core/functions.ts +++ b/src/core/functions.ts @@ -10,10 +10,10 @@ import type { UserContextMenuCommandInteraction, AutocompleteInteraction } from 'discord.js'; -import { ApplicationCommandOptionType, InteractionType } from 'discord.js' +import { ApplicationCommandOptionType, InteractionType } from 'discord.js'; import { PayloadType, PluginType } from './structures'; import assert from 'assert'; -import { Payload } from '../types/utility'; +import type { Payload } from '../types/utility'; //function wrappers for empty ok / err export const ok = /* @__PURE__*/ () => Ok.EMPTY; @@ -50,7 +50,7 @@ export function treeSearch( if (options === undefined) return undefined; //clone to prevent mutation of original command module const _options = options.map(a => ({ ...a })); - let subcommands = new Set(); + const subcommands = new Set(); while (_options.length > 0) { const cur = _options.pop()!; switch (cur.type) { diff --git a/src/core/id.ts b/src/core/id.ts index 28032c54..50a781ed 100644 --- a/src/core/id.ts +++ b/src/core/id.ts @@ -42,19 +42,10 @@ const TypeMap = new Map([ [CommandType.RoleSelect, ComponentType.RoleSelect], [CommandType.ChannelSelect, ComponentType.ChannelSelect]]); -/* - * Generates a number based on CommandType. - * This corresponds to an ApplicationCommandType or ComponentType - * TextCommands are 0 as they aren't either or. - */ -function apiType(t: CommandType | EventType) { - return TypeMap.get(t)!; -} - /* * Generates an id based on name and CommandType. * A is for any ApplicationCommand. C is for any ComponentCommand - * Then, another number generated by apiType function is appended + * Then, another number fetched from TypeMap */ export function create(name: string, type: CommandType | EventType) { if(type == CommandType.Text) { @@ -67,7 +58,7 @@ export function create(name: string, type: CommandType | EventType) { return `${name}_M`; } const am = (appBitField & type) !== 0 ? 'A' : 'C'; - return `${name}_${am}${apiType(type)}` + return `${name}_${am}${TypeMap.get(type)!}` } diff --git a/src/core/ioc/base.ts b/src/core/ioc/base.ts index 540204aa..3fd8c39a 100644 --- a/src/core/ioc/base.ts +++ b/src/core/ioc/base.ts @@ -11,7 +11,27 @@ import type { Logging } from '../contracts/logging'; let containerSubject: CoreContainer>; /** - * @deprecated + * @internal + * Don't use this unless you know what you're doing. Destroys old containerSubject if it exists and disposes everything + * then it will swap + */ +export async function __swap_container(c: CoreContainer>) { + if(containerSubject) { + await containerSubject.disposeAll() + } + containerSubject = c; +} + +/** + * @internal + * Don't use this unless you know what you're doing. Destroys old containerSubject if it exists and disposes everything + * then it will swap + */ +export function __add_container(key: string,v : Insertable) { + containerSubject.add({ [key]: v }); +} + +/** * Returns the underlying data structure holding all dependencies. * Exposes methods from iti * Use the Service API. The container should be readonly @@ -29,19 +49,24 @@ export function disposeAll(logger: Logging|undefined) { ?.disposeAll() .then(() => logger?.info({ message: 'Cleaning container and crashing' })); } - -const dependencyBuilder = (container: any, excluded: string[] ) => { - type Insertable = - | ((container: CoreContainer) => unknown ) +type Insertable = + | ((container: CoreContainer) => unknown) | object +const dependencyBuilder = (container: any, excluded: string[] ) => { return { /** * Insert a dependency into your container. * Supply the correct key and dependency */ add(key: keyof Dependencies, v: Insertable) { - Result.wrap(() => container.add({ [key]: v})) - .expect("Failed to add " + key); + if(typeof v !== 'function') { + Result.wrap(() => container.add({ [key]: v})) + .expect("Failed to add " + key); + } else { + Result.wrap(() => + container.add((cntr: CoreContainer) => ({ [key]: v(cntr)} ))) + .expect("Failed to add " + key); + } }, /** * Exclude any dependencies from being added. @@ -57,8 +82,14 @@ const dependencyBuilder = (container: any, excluded: string[] ) => { * Swap out a preexisting dependency. */ swap(key: keyof Dependencies, v: Insertable) { - Result.wrap(() => container.upsert({ [key]: v })) - .expect("Failed to update " + key); + if(typeof v !== 'function') { + Result.wrap(() => container.upsert({ [key]: v})) + .expect("Failed to update " + key); + } else { + Result.wrap(() => + container.upsert((cntr: CoreContainer) => ({ [key]: v(cntr)}))) + .expect("Failed to update " + key); + } }, /** * @param key the key of the dependency @@ -82,11 +113,6 @@ type ValidDependencyConfig = | CallbackBuilder | DependencyConfiguration; -export const insertLogger = (containerSubject: CoreContainer) => { - containerSubject - .upsert({'@sern/logger': () => new DefaultServices.DefaultLogging}); -} - /** * Given the user's conf, check for any excluded/included dependency keys. @@ -101,7 +127,7 @@ function composeRoot( //container should have no client or logger yet. const hasLogger = conf.exclude?.has('@sern/logger'); if (!hasLogger) { - insertLogger(container); + __add_container('@sern/logger', new DefaultServices.DefaultLogging); } //Build the container based on the callback provided by the user conf.build(container as CoreContainer>); @@ -119,13 +145,13 @@ export async function makeDependencies if(typeof conf === 'function') { const excluded: string[] = []; conf(dependencyBuilder(containerSubject, excluded)); - + //We only include logger if it does not exist const includeLogger = !excluded.includes('@sern/logger') - && !containerSubject.getTokens()['@sern/logger']; + && !containerSubject.hasKey('@sern/logger'); if(includeLogger) { - insertLogger(containerSubject); + __add_container('@sern/logger', new DefaultServices.DefaultLogging); } containerSubject.ready(); diff --git a/src/core/ioc/container.ts b/src/core/ioc/container.ts index 638eb8f4..e8c49706 100644 --- a/src/core/ioc/container.ts +++ b/src/core/ioc/container.ts @@ -2,7 +2,7 @@ import { Container } from 'iti'; import { Disposable } from '../'; import * as assert from 'node:assert'; import { Subject } from 'rxjs'; -import { DefaultServices, ModuleStore } from '../_internal'; +import { __Services, ModuleStore } from '../_internal'; import * as Hooks from './hooks'; import { EventEmitter } from 'node:events'; @@ -23,12 +23,11 @@ export class CoreContainer> extends Container) - .add({ '@sern/errors': () => new DefaultServices.DefaultErrorHandling, + .add({ '@sern/errors': () => new __Services.DefaultErrorHandling, '@sern/emitter': () => new EventEmitter({ captureRejections: true }), '@sern/store': () => new ModuleStore }) .add(ctx => { - return { '@sern/modules': () => - new DefaultServices.DefaultModuleManager(ctx['@sern/store']) }; + return { '@sern/modules': new __Services.DefaultModuleManager(ctx['@sern/store'])}; }); } @@ -52,8 +51,6 @@ export class CoreContainer> extends Container(mod: { }); } - -/** - * @deprecated - */ -function prepareClassPlugins(c: Module) { - const [onEvent, initPlugins] = partitionPlugins(c.plugins); - c.plugins = initPlugins as InitPlugin[]; - c.onEvent = onEvent as ControlPlugin[]; -} - -/** - * @deprecated - * Will be removed in future - */ -export abstract class CommandExecutable { - abstract type: Type; - plugins: AnyCommandPlugin[] = []; - private static _instance: CommandModule; - - static getInstance() { - if (!CommandExecutable._instance) { - //@ts-ignore - CommandExecutable._instance = new this(); - prepareClassPlugins(CommandExecutable._instance); - } - return CommandExecutable._instance; - } - - abstract execute(...args: CommandArgs): Awaitable; -} - -/** - * @deprecated - * Will be removed in future - */ -export abstract class EventExecutable { - abstract type: Type; - plugins: AnyEventPlugin[] = []; - - private static _instance: EventModule; - static getInstance() { - if (!EventExecutable._instance) { - //@ts-ignore - EventExecutable._instance = new this(); - prepareClassPlugins(EventExecutable._instance); - } - return EventExecutable._instance; - } - abstract execute(...args: EventArgs): Awaitable; -} diff --git a/src/core/structures/module-store.ts b/src/core/structures/module-store.ts index 44d9ca1c..49b22c55 100644 --- a/src/core/structures/module-store.ts +++ b/src/core/structures/module-store.ts @@ -7,5 +7,5 @@ import { CommandMeta, Module } from '../../types/core-modules'; */ export class ModuleStore { metadata = new WeakMap(); - commands = new Map(); + commands = new Map(); } diff --git a/src/core/structures/services/module-manager.ts b/src/core/structures/services/module-manager.ts index 356936bf..dad28889 100644 --- a/src/core/structures/services/module-manager.ts +++ b/src/core/structures/services/module-manager.ts @@ -1,6 +1,5 @@ import * as Id from '../../../core/id'; import { CoreModuleStore, ModuleManager } from '../../contracts'; -import { Files } from '../../_internal'; import { CommandMeta, CommandModule, CommandModuleDefs, Module } from '../../../types/core-modules'; import { CommandType } from '../enums'; /** @@ -13,11 +12,11 @@ export class DefaultModuleManager implements ModuleManager { getByNameCommandType(name: string, commandType: T) { - const id = this.get(Id.create(name, commandType)); - if (!id) { + const module = this.get(Id.create(name, commandType)); + if (!module) { return undefined; } - return Files.importModule(id); + return module as CommandModuleDefs[T]; } setMetadata(m: Module, c: CommandMeta): void { @@ -35,20 +34,18 @@ export class DefaultModuleManager implements ModuleManager { get(id: string) { return this.moduleStore.commands.get(id); } - set(id: string, path: string): void { + set(id: string, path: CommandModule): void { this.moduleStore.commands.set(id, path); } //not tested - getPublishableCommands(): Promise { + getPublishableCommands(): CommandModule[] { const entries = this.moduleStore.commands.entries(); const publishable = 0b000000110; - return Promise.all( - Array.from(entries) + return Array.from(entries) .filter(([id]) => { const last_entry = id.at(-1); return last_entry == 'B' || !(publishable & Number.parseInt(last_entry!)); }) - .map(([, path]) => Files.importModule(path)), - ); + .map(([, path]) => path as CommandModule); } } diff --git a/src/handlers/dispatchers.ts b/src/handlers/dispatchers.ts index d52f7bee..8e4eb10c 100644 --- a/src/handlers/dispatchers.ts +++ b/src/handlers/dispatchers.ts @@ -17,10 +17,7 @@ import type { CommandModule, Module, Processed } from '../types/core-modules'; //TODO: refactor dispatchers so that it implements a strategy for each different type of payload? export function dispatchMessage(module: Processed, args: [Context, Args]) { - return { - module, - args, - }; + return { module, args }; } export function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[]) { @@ -87,9 +84,6 @@ export function createDispatcher(payload: { } return { module: payload.module, args: contextArgs(payload.event) }; } - default: return { - module: payload.module, - args: [payload.event], - }; + default: return { module: payload.module, args: [payload.event] }; } } diff --git a/src/handlers/event-utils.ts b/src/handlers/event-utils.ts index a2e757b1..643ab356 100644 --- a/src/handlers/event-utils.ts +++ b/src/handlers/event-utils.ts @@ -8,9 +8,9 @@ import { of, throwError, tap, - MonoTypeOperatorFunction, catchError, finalize, + map, } from 'rxjs'; import { Files, @@ -29,8 +29,7 @@ import { ObservableInput, pipe } from 'rxjs'; import { Err, Ok, Result } from 'ts-results-es'; import type { Awaitable } from '../types/utility'; import type { ControlPlugin } from '../types/core-plugin'; -import type { AnyModule, CommandModule, Module, Processed } from '../types/core-modules'; -import type { ImportPayload } from '../types/core'; +import type { AnyModule, CommandMeta, CommandModule, Module, Processed } from '../types/core-modules'; import { disposeAll } from '../core/ioc/base'; function createGenericHandler( @@ -74,18 +73,13 @@ export function createInteractionHandler( const possibleIds = Id.reconstruct(event); let fullPaths= possibleIds .map(id => mg.get(id)) - .filter((id): id is string => id !== undefined); + .filter((id): id is Module => id !== undefined); if(fullPaths.length == 0) { return Err.EMPTY; } const [ path ] = fullPaths; - return Files - .defaultModuleLoader>(path) - .then(payload => Ok(createDispatcher({ - module: payload.module, - event, - }))); + return Ok(createDispatcher({ module: path as Processed, event })); }); } @@ -103,39 +97,37 @@ export function createMessageHandler( return Err('Possibly undefined behavior: could not find a static id to resolve'); } } - return Files - .defaultModuleLoader>(fullPath) - .then(payload => { - const args = contextArgs(event, rest); - return Ok({ args, ...payload }); - }); + return Ok({ args: contextArgs(event, rest), module: fullPath as Processed }) }); } /** - * IMPURE SIDE EFFECT * This function assigns remaining, incomplete data to each imported module. */ -function assignDefaults( - moduleManager: ModuleManager, -): MonoTypeOperatorFunction> { - return tap(({ module, absPath }) => { - module.name ??= Files.filename(absPath); - module.description ??= '...'; - moduleManager.setMetadata(module, { - isClass: module.constructor.name === 'Function', - fullPath: absPath, - id: Id.create(module.name, module.type), - }); +function assignDefaults() { + return map(({ module, absPath }) => { + const processed = { + name: module.name ?? Files.filename(absPath), + description: module.description ?? '...', + ...module + } + return { + module: processed, + absPath, + metadata: { + isClass: module.constructor.name === 'Function', + fullPath: absPath, + id: Id.create(processed.name, module.type), + } + } }); } export function buildModules( input: ObservableInput, - moduleManager: ModuleManager, ) { return Files .buildModuleStream>(input) - .pipe(assignDefaults(moduleManager)); + .pipe(assignDefaults()); } @@ -219,9 +211,9 @@ export function callInitPlugins>(sernEmitter: Emi onStop: (module: T) => { sernEmitter.emit('module.register', resultPayload(PayloadType.Failure, module, SernError.PluginFailure)); }, - onNext: ({ module }) => { - sernEmitter.emit('module.register', resultPayload(PayloadType.Success, module)); - return { module }; + onNext: (payload) => { + sernEmitter.emit('module.register', resultPayload(PayloadType.Success, payload.module)); + return payload as { module: T; metadata: CommandMeta }; }, }), ); diff --git a/src/handlers/ready-event.ts b/src/handlers/ready-event.ts index 80c6ad03..e1ddb8eb 100644 --- a/src/handlers/ready-event.ts +++ b/src/handlers/ready-event.ts @@ -1,29 +1,31 @@ -import { ObservableInput, concat, first, fromEvent, ignoreElements, pipe } from 'rxjs'; +import { ObservableInput, concat, first, fromEvent, ignoreElements, pipe, tap } from 'rxjs'; import { CommandType } from '../core/structures'; import { SernError } from '../core/_internal'; import { Result } from 'ts-results-es'; -import { ModuleManager } from '../core/contracts'; +import { Logging, ModuleManager } from '../core/contracts'; import { buildModules, callInitPlugins } from './_internal'; import * as assert from 'node:assert'; import * as util from 'node:util'; import type { DependencyList } from '../types/ioc'; -import type { AnyModule, Processed } from '../types/core-modules'; +import type { AnyModule, CommandMeta, Processed } from '../types/core-modules'; export function readyHandler( - [sEmitter, , , moduleManager, client]: DependencyList, + [sEmitter, , log , moduleManager, client]: DependencyList, allPaths: ObservableInput, ) { - const ready$ = fromEvent(client!, 'ready').pipe(once()); - - return concat(ready$, buildModules(allPaths, moduleManager)) + //Todo: add module manager on on ready + const ready$ = fromEvent(client!, 'ready').pipe(once(log)); + + return concat(ready$, buildModules(allPaths)) .pipe(callInitPlugins(sEmitter)) - .subscribe(({ module }) => { - register(moduleManager, module) + .subscribe(({ module, metadata }) => { + register(moduleManager, module, metadata) .expect(SernError.InvalidModuleType + ' ' + util.inspect(module)); }); } -const once = () => pipe( +const once = (log: Logging | undefined) => pipe( + tap(() => { log?.info({ message: "Waiting on discord client to be ready..." }) }), first(), ignoreElements()) @@ -31,20 +33,22 @@ const once = () => pipe( function register>( manager: ModuleManager, module: T, + metadata:CommandMeta ): Result { - const { id, fullPath } = manager.getMetadata(module)!; + manager.setMetadata(module, metadata)!; const validModuleType = module.type >= 0 && module.type <= 1 << 10; assert.ok( validModuleType, - `Found ${module.name} at ${fullPath}, which does not have a valid type`, + //@ts-ignore + `Found ${module.name} at ${metadata.fullPath}, which does not have a valid type`, ); if (module.type === CommandType.Both) { - module.alias?.forEach(a => manager.set(`${a}_B`, fullPath)); + module.alias?.forEach(a => manager.set(`${a}_B`, module)); } else { if(module.type === CommandType.Text){ - module.alias?.forEach(a => manager.set(`${a}_T`, fullPath)); + module.alias?.forEach(a => manager.set(`${a}_T`, module)); } } - return Result.wrap(() => manager.set(id, fullPath)); + return Result.wrap(() => manager.set(metadata.id, module)); } diff --git a/src/handlers/user-defined-events.ts b/src/handlers/user-defined-events.ts index 95d6b4c0..3dd49894 100644 --- a/src/handlers/user-defined-events.ts +++ b/src/handlers/user-defined-events.ts @@ -23,7 +23,7 @@ export function eventsHandler( throw Error(SernError.InvalidModuleType + ' while creating event handler'); } }; - buildModules(allPaths, moduleManager) + buildModules(allPaths) .pipe( callInitPlugins(emitter), map(intoDispatcher), diff --git a/src/index.ts b/src/index.ts index dd9bec69..1f0af398 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,12 +46,8 @@ export { commandModule, eventModule, discordEvent, - EventExecutable, - CommandExecutable, } from './core/modules'; export * as Presence from './core/presences' -export { - useContainerRaw -} from './core/_internal' + diff --git a/src/sern.ts b/src/sern.ts index e633be01..e88bd069 100644 --- a/src/sern.ts +++ b/src/sern.ts @@ -43,9 +43,10 @@ export function init(maybeWrapper: Wrapper | 'file') { //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 all modules in ${time} s`, }); + logger?.info({ message: `sern: registered in ${time} s`, }); if(presencePath.exists) { const setPresence = async (p: any) => { return (dependencies[4] as Client).user?.setPresence(p); diff --git a/test/core/ioc.test.ts b/test/core/ioc.test.ts index de8015c1..e7903071 100644 --- a/test/core/ioc.test.ts +++ b/test/core/ioc.test.ts @@ -98,4 +98,16 @@ describe('ioc container', () => { container.ready(); expect(dependency.init).toHaveBeenCalledTimes(1); }) + + it('should detect a key already exists', () => { + container.add({ '@sern/client': dependency2 }); + expect(container.hasKey('@sern/client')).toBeTruthy() + }) + + + it('should detect a key already exists', () => { + container.add({ '@sern/client': () => dependency2 }); + expect(container.hasKey('@sern/client')).toBeTruthy() + }) + });