diff --git a/src/core/contracts/error-handling.ts b/src/core/contracts/error-handling.ts index 3237151..f905c2e 100644 --- a/src/core/contracts/error-handling.ts +++ b/src/core/contracts/error-handling.ts @@ -1,6 +1,3 @@ -import type { Observable } from 'rxjs'; -import type { Logging } from './logging'; -import util from 'node:util'; /** * @since 2.0.0 */ @@ -35,16 +32,4 @@ export class DefaultErrorHandling implements ErrorHandling { } } -export function handleError(crashHandler: ErrorHandling, logging?: Logging) { - return (pload: unknown, caught: Observable) => { - // This is done to fit the ErrorHandling contract - const err = pload instanceof Error ? pload : Error(util.format(pload)); - if (crashHandler.keepAlive == 0) { - crashHandler.crash(err); - } - //formatted payload - logging?.error({ message: util.format(pload) }); - crashHandler.updateAlive(err); - return caught; - }; -} + diff --git a/src/core/contracts/module-manager.ts b/src/core/contracts/module-manager.ts index f7bdd04..930406a 100644 --- a/src/core/contracts/module-manager.ts +++ b/src/core/contracts/module-manager.ts @@ -8,12 +8,18 @@ export interface ModuleManager { get(id: string): string | undefined; set(id: string, path: string): void; getPublishableCommands(): Promise; + remove(id: string) : boolean } /** * @since 2.0.0 */ export class DefaultModuleManager implements ModuleManager { constructor(private moduleStore: ModuleStore) {} + + remove(id: string): boolean { + throw new Error('Method not implemented.'); + } + get(id: string) { return this.moduleStore.get(id); } diff --git a/src/core/create-plugins.ts b/src/core/create-plugins.ts index fa1bda9..f4d710c 100644 --- a/src/core/create-plugins.ts +++ b/src/core/create-plugins.ts @@ -1,5 +1,5 @@ -import { CommandType, EventType, PluginType } from '../structures'; -import type { Plugin, PluginResult, EventArgs, CommandArgs } from '../../types/plugin'; +import { CommandType, EventType, PluginType } from './structures'; +import type { Plugin, PluginResult, EventArgs, CommandArgs } from '../types/plugin'; import type { ClientEvents } from 'discord.js'; export function makePlugin( diff --git a/src/core/dependencies.ts b/src/core/dependencies.ts index bb8ffc4..5219825 100644 --- a/src/core/dependencies.ts +++ b/src/core/dependencies.ts @@ -1,11 +1,12 @@ import { Container } from 'iti'; import type { Dependencies, DependencyConfiguration, MapDeps, Wrapper } from '../types/core'; import { DefaultErrorHandling, DefaultLogging, DefaultModuleManager } from './contracts'; -import { Result } from 'ts-results-es'; -import { createContainer } from 'iti'; import { SernEmitter } from './structures'; import { SernError } from './structures/errors'; -export let containerSubject: Container<{}, {}> +import * as assert from 'node:assert' +import * as types from 'node:util/types' +import { Awaitable } from '../types/handler'; +export let containerSubject: Container<{}, {}>; const requiredDependencyKeys = ['@sern/emitter', '@sern/errors', '@sern/logger'] as const; /** * @__PURE__ @@ -33,8 +34,8 @@ export function transient(cb: () => () => T) { * Finally, update the containerSubject with the new container state * @param conf */ -export function composeRoot(conf: DependencyConfiguration) { - //This should have no client or logger yet. +export async function composeRoot(conf: DependencyConfiguration) { + //container should have no client or logger yet. const excludeLogger = conf.exclude?.has('@sern/logger'); if (!excludeLogger) { containerSubject.add({ @@ -42,52 +43,97 @@ export function composeRoot(conf: DependencyConfiguratio }); } //Build the container based on the callback provided by the user - const container = conf.build(containerSubject as Container, {}>); + const updatedContainer = await conf.build(containerSubject as Container, {}>); try { - container.get('@sern/client'); + updatedContainer.get('@sern/client'); } catch { throw new Error(SernError.MissingRequired + " No client was provided") } if (!excludeLogger) { - container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' }); + updatedContainer.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' }); } } export function useContainer() { - const container = containerSubject as Container; + console.warn(`Warning: using a container hook is not recommended. Could lead to many unwanted side effects`); return (...keys: [...V]) => - keys.map(key => Result.wrap(() => container.get(key)).expect(`Unregistered dependency: ${String(key)}`)) as MapDeps; + keys.map(key => (containerSubject as Container).get(key)) as MapDeps; } /** * Returns the underlying data structure holding all dependencies. - * Please be careful as this only gets the client's current state. - * Exposes some methods from iti + * Exposes methods from iti */ -export function useContainerRaw() { - if(!containerSubject) { - throw Error("Could not find container. Did you call makeDependencies?") - } - return containerSubject as Container; +export function useContainerRaw() { + assert.ok( + containerSubject && (containerSubject as CoreContainer).isReady(), + "Could not find container or container wasn't ready. Did you call makeDependencies?" + ); + return containerSubject; +} + +/** + * @since 2.0.0 + * @param conf a configuration for creating your project dependencies + */ +export async function makeDependencies( + conf: DependencyConfiguration, +) { + containerSubject = new CoreContainer(); + //Until there are more optional dependencies, just check if the logger exists + await composeRoot(conf); + (containerSubject as CoreContainer).ready(); + + return useContainer(); +} + +export interface Init { + init() : Awaitable } /** * Provides all the defaults for sern to function properly. * The only user provided dependency needs to be @sern/client */ -function defaultContainer() { - return createContainer() - .add({ - '@sern/errors': () => new DefaultErrorHandling(), - '@sern/store': () => new Map(), - '@sern/emitter': () => new SernEmitter() - }) - .add(ctx => { - return { - '@sern/modules': () => new DefaultModuleManager(ctx['@sern/store']), - }; - }) +class CoreContainer extends Container { + private _ready = false; + constructor() { + super(); + (this as Container<{}, {}>) + .add({ + '@sern/errors': () => new DefaultErrorHandling(), + '@sern/store': () => new Map(), + '@sern/emitter': () => new SernEmitter() + }) + .add(ctx => { + return { '@sern/modules': () => new DefaultModuleManager(ctx['@sern/store']) }; + }) + } + + async withInit(...keys: Keys[]) { + if(this.isReady()) { + throw Error("You cannot call this method after sern has started"); + } + for await (const k of keys) { + const dep = this.get(k); + assert.ok(dep !== undefined); + if('init' in dep && typeof dep.init === 'function') { + types.isAsyncFunction(dep.init) + ? await dep.init() + : dep.init() + } else { + throw Error(`called withInit with key ${k} but found nothing to init`) + } + } + return this; + } + isReady() { + return this._ready; + } + ready() { + this._ready = true; + } } @@ -104,16 +150,3 @@ export function makeFetcher( ...(otherKeys as (keyof Dependencies)[]), ) as MapDeps; } - -/** - * @since 2.0.0 - * @param conf a configuration for creating your project dependencies - */ -export function makeDependencies( - conf: DependencyConfiguration, -) { - containerSubject = defaultContainer() - //Until there are more optional dependencies, just check if the logger exists - composeRoot(conf); - return useContainer(); -} diff --git a/src/core/module-loading.ts b/src/core/module-loading.ts index 05bea88..de86807 100644 --- a/src/core/module-loading.ts +++ b/src/core/module-loading.ts @@ -32,6 +32,7 @@ function checkIsProcessed(m: T): asserts m is Processed { } export const fmtFileName = (n: string) => n.substring(0, n.length - 3); + /** * a directory string is converted into a stream of modules. * starts the stream of modules that sern needs to process on init diff --git a/src/core/operators.ts b/src/core/operators.ts index cb1f5e9..64e9579 100644 --- a/src/core/operators.ts +++ b/src/core/operators.ts @@ -1,7 +1,7 @@ /** * This file holds sern's rxjs operators used for processing data. * Each function should be modular and testable, not bound to discord / sern - * and independent of each other + * and independent of each other. */ import { concatMap, @@ -21,6 +21,8 @@ import type { PluginResult, VoidResult } from '../types/plugin'; import { Result } from 'ts-results-es'; import { Awaitable } from '../types/handler'; import { EventEmitter } from 'node:events'; +import { ErrorHandling, Logging } from './contracts'; +import util from 'node:util' /** * if {src} is true, mapTo V, else ignore * @param item @@ -91,3 +93,19 @@ export const everyPluginOk: OperatorFunction = pipe( export const sharedObservable = (e: EventEmitter, eventName: string) => { return (fromEvent(e, eventName) as Observable).pipe(share()); }; + +export function handleError(crashHandler: ErrorHandling, logging?: Logging) { + return (pload: unknown, caught: Observable) => { + // This is done to fit the ErrorHandling contract + const err = pload instanceof Error + ? pload + : Error(util.inspect(pload, { colors: true })); + if (crashHandler.keepAlive == 0) { + crashHandler.crash(err); + } + //formatted payload + logging?.error({ message: util.inspect(pload) }); + crashHandler.updateAlive(err); + return caught; + }; +} diff --git a/src/core/predicates.ts b/src/core/predicates.ts index e811cfa..31bdf6d 100644 --- a/src/core/predicates.ts +++ b/src/core/predicates.ts @@ -18,6 +18,7 @@ type AnyCommandInteraction = | ChatInputCommandInteraction | MessageContextMenuCommandInteraction | UserContextMenuCommandInteraction; + export function isMessageComponent(i: InteractionTypable): i is AnyMessageComponentInteraction { return i.type === InteractionType.MessageComponent; }