mirror of
https://github.com/sern-handler/handler
synced 2026-06-06 01:16:55 +00:00
feat: types organization and cleaning up code base
This commit is contained in:
@@ -19,17 +19,4 @@ export interface ErrorHandling {
|
||||
*/
|
||||
updateAlive(error: Error): void;
|
||||
}
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export class DefaultErrorHandling implements ErrorHandling {
|
||||
keepAlive = 5;
|
||||
crash(error: Error): never {
|
||||
throw error;
|
||||
}
|
||||
updateAlive(_: Error) {
|
||||
this.keepAlive--;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { type ErrorHandling, DefaultErrorHandling } from './error-handling';
|
||||
export { type Logging, DefaultLogging } from './logging';
|
||||
export { type ModuleManager, DefaultModuleManager } from './module-manager';
|
||||
export { type ErrorHandling } from './error-handling';
|
||||
export type { Logging, LogPayload } from './logging';
|
||||
export { type ModuleManager } from './module-manager';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { LogPayload } from '../../types/core';
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@@ -8,24 +7,5 @@ export interface Logging<T = unknown> {
|
||||
info(payload: LogPayload<T>): void;
|
||||
debug(payload: LogPayload<T>): void;
|
||||
}
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export class DefaultLogging implements Logging {
|
||||
private date = () => new Date();
|
||||
debug(payload: LogPayload): void {
|
||||
console.debug(`DEBUG: ${this.date().toISOString()} -> ${payload.message}`);
|
||||
}
|
||||
|
||||
error(payload: LogPayload): void {
|
||||
console.error(`ERROR: ${this.date().toISOString()} -> ${payload.message}`);
|
||||
}
|
||||
|
||||
info(payload: LogPayload): void {
|
||||
console.info(`INFO: ${this.date().toISOString()} -> ${payload.message}`);
|
||||
}
|
||||
|
||||
warning(payload: LogPayload): void {
|
||||
console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`);
|
||||
}
|
||||
}
|
||||
export type LogPayload<T = unknown> = { message: T };
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ModuleStore } from '../../types/core';
|
||||
import { CommandModule } from '../../types/module';
|
||||
import { importModule } from '../module-loading';
|
||||
import { CommandModule } from "../types/modules";
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@@ -10,30 +9,4 @@ export interface ModuleManager {
|
||||
getPublishableCommands(): Promise<CommandModule[]>;
|
||||
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);
|
||||
}
|
||||
set(id: string, path: string): void {
|
||||
this.moduleStore.set(id, path);
|
||||
}
|
||||
//not tested
|
||||
getPublishableCommands(): Promise<CommandModule[]> {
|
||||
const entries = this.moduleStore.entries();
|
||||
const publishable = 0b000000110;
|
||||
return Promise.all(
|
||||
Array.from(entries)
|
||||
.filter(([id]) => (Number.parseInt(id.at(-1)!) & publishable) !== 0)
|
||||
.map(([, path]) => importModule<CommandModule>(path)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommandType, EventType, PluginType } from './structures';
|
||||
import type { Plugin, PluginResult, EventArgs, CommandArgs } from '../types/plugin';
|
||||
import type { Plugin, PluginResult, EventArgs, CommandArgs } from './types/plugins';
|
||||
import type { ClientEvents } from 'discord.js';
|
||||
|
||||
export function makePlugin<V extends unknown[]>(
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import { Container } from 'iti';
|
||||
import type { Dependencies, DependencyConfiguration, MapDeps, Wrapper } from '../types/core';
|
||||
import { DefaultErrorHandling, DefaultLogging, DefaultModuleManager } from './contracts';
|
||||
import { SernEmitter } from './structures';
|
||||
import { SernError } from './structures/errors';
|
||||
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__
|
||||
* @since 2.0.0.
|
||||
* use single if you want a singleton, or an object that is called once.
|
||||
* @param cb
|
||||
*/
|
||||
export function single<T>(cb: () => T) {
|
||||
return cb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @__PURE__
|
||||
* @since 2.0.0
|
||||
* Following iti's singleton and transient implementation,
|
||||
* use transient if you want a new dependency every time your container getter is called
|
||||
* @param cb
|
||||
*/
|
||||
export function transient<T>(cb: () => () => T) {
|
||||
return cb;
|
||||
}
|
||||
/**
|
||||
* Given the user's conf, check for any excluded dependency keys.
|
||||
* Then, call conf.build to get the rest of the users' dependencies.
|
||||
* Finally, update the containerSubject with the new container state
|
||||
* @param conf
|
||||
*/
|
||||
export async function composeRoot<T extends Dependencies>(conf: DependencyConfiguration<T>) {
|
||||
//container should have no client or logger yet.
|
||||
const excludeLogger = conf.exclude?.has('@sern/logger');
|
||||
if (!excludeLogger) {
|
||||
containerSubject.add({
|
||||
'@sern/logger': () => new DefaultLogging(),
|
||||
});
|
||||
}
|
||||
//Build the container based on the callback provided by the user
|
||||
const updatedContainer = await conf.build(containerSubject as Container<Omit<Dependencies, '@sern/client'>, {}>);
|
||||
try {
|
||||
updatedContainer.get('@sern/client');
|
||||
} catch {
|
||||
throw new Error(SernError.MissingRequired + " No client was provided")
|
||||
}
|
||||
|
||||
if (!excludeLogger) {
|
||||
updatedContainer.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
|
||||
}
|
||||
}
|
||||
|
||||
export function useContainer<const T extends Dependencies>() {
|
||||
console.warn(`Warning: using a container hook is not recommended. Could lead to many unwanted side effects`);
|
||||
return <V extends (keyof T)[]>(...keys: [...V]) =>
|
||||
keys.map(key => (containerSubject as Container<T, {}>).get(key)) as MapDeps<T, V>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying data structure holding all dependencies.
|
||||
* Exposes methods from iti
|
||||
*/
|
||||
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<const T extends Dependencies>(
|
||||
conf: DependencyConfiguration<T>,
|
||||
) {
|
||||
containerSubject = new CoreContainer();
|
||||
//Until there are more optional dependencies, just check if the logger exists
|
||||
await composeRoot(conf);
|
||||
(containerSubject as CoreContainer).ready();
|
||||
|
||||
return useContainer<T>();
|
||||
}
|
||||
|
||||
export interface Init {
|
||||
init() : Awaitable<unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides all the defaults for sern to function properly.
|
||||
* The only user provided dependency needs to be @sern/client
|
||||
*/
|
||||
class CoreContainer extends Container<Dependencies, {}> {
|
||||
private _ready = false;
|
||||
constructor() {
|
||||
super();
|
||||
(this as Container<{}, {}>)
|
||||
.add({
|
||||
'@sern/errors': () => new DefaultErrorHandling(),
|
||||
'@sern/store': () => new Map<string, string>(),
|
||||
'@sern/emitter': () => new SernEmitter()
|
||||
})
|
||||
.add(ctx => {
|
||||
return { '@sern/modules': () => new DefaultModuleManager(ctx['@sern/store']) };
|
||||
})
|
||||
}
|
||||
|
||||
async withInit<const Keys extends keyof Dependencies>(...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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A way for sern to grab only the necessary dependencies.
|
||||
* Returns a function which allows for the user to call for more dependencies.
|
||||
*/
|
||||
export function makeFetcher<Dep extends Dependencies>(
|
||||
containerConfig: Wrapper['containerConfig'],
|
||||
) {
|
||||
return <const Keys extends (keyof Dep)[]>(otherKeys: [...Keys]) =>
|
||||
containerConfig.get(
|
||||
...requiredDependencyKeys,
|
||||
...(otherKeys as (keyof Dependencies)[]),
|
||||
) as MapDeps<Dep, [...typeof requiredDependencyKeys, ...Keys]>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Err, Ok } from 'ts-results-es';
|
||||
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
|
||||
import type { SernAutocompleteData, SernOptionsData } from '../types/module';
|
||||
import type { SernAutocompleteData, SernOptionsData } from './types/modules';
|
||||
|
||||
//function wrappers for empty ok / err
|
||||
export const ok = /* @__PURE__*/ () => Ok.EMPTY;
|
||||
|
||||
@@ -1,4 +1,34 @@
|
||||
export * from './contracts';
|
||||
export * from './create-plugins';
|
||||
export * from './structures';
|
||||
export { single, transient, useContainerRaw, makeDependencies } from './dependencies';
|
||||
export * from './ioc';
|
||||
export type {
|
||||
CommandModule,
|
||||
EventModule,
|
||||
BothCommand,
|
||||
ContextMenuMsg,
|
||||
ContextMenuUser,
|
||||
SlashCommand,
|
||||
TextCommand,
|
||||
ButtonCommand,
|
||||
StringSelectCommand,
|
||||
MentionableSelectCommand,
|
||||
UserSelectCommand,
|
||||
ChannelSelectCommand,
|
||||
RoleSelectCommand,
|
||||
ModalSubmitCommand,
|
||||
DiscordEventCommand,
|
||||
SernEventCommand,
|
||||
ExternalEventCommand,
|
||||
CommandModuleDefs,
|
||||
EventModuleDefs,
|
||||
BaseOptions,
|
||||
SernAutocompleteData
|
||||
} from './types/modules';
|
||||
export type {
|
||||
Controller,
|
||||
PluginResult,
|
||||
InitPlugin,
|
||||
ControlPlugin,
|
||||
Plugin
|
||||
} from './types/plugins';
|
||||
|
||||
39
src/core/ioc/base.ts
Normal file
39
src/core/ioc/base.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import * as assert from "assert";
|
||||
import { composeRoot, useContainer } from "./dependency-injection";
|
||||
import { DependencyConfiguration, Dependencies } from "./types";
|
||||
import { CoreContainer } from "../structures/container";
|
||||
|
||||
|
||||
//SIDE EFFECT: GLOBAL DI
|
||||
let containerSubject: CoreContainer<Partial<Dependencies>>;
|
||||
|
||||
/**
|
||||
* Returns the underlying data structure holding all dependencies.
|
||||
* Exposes methods from iti
|
||||
*/
|
||||
export function useContainerRaw() {
|
||||
assert.ok(
|
||||
containerSubject && containerSubject.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<const T extends Dependencies>(
|
||||
conf: DependencyConfiguration<T>,
|
||||
) {
|
||||
//Until there are more optional dependencies, just check if the logger exists
|
||||
//SIDE EFFECT
|
||||
containerSubject = new CoreContainer()
|
||||
await composeRoot(conf);
|
||||
|
||||
//SIDE EFFECT
|
||||
containerSubject.ready();
|
||||
|
||||
return useContainer<T>();
|
||||
}
|
||||
|
||||
78
src/core/ioc/dependency-injection.ts
Normal file
78
src/core/ioc/dependency-injection.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { CoreDependencies, Dependencies, DependencyConfiguration, MapDeps, IntoDependencies } from './types';
|
||||
import { DefaultLogging } from '../structures';
|
||||
import { SernError } from '../structures/errors';
|
||||
import { useContainerRaw } from './base';
|
||||
import { CoreContainer } from '../structures/container';
|
||||
|
||||
|
||||
/**
|
||||
* @__PURE__
|
||||
* @since 2.0.0.
|
||||
* Creates a singleton object.
|
||||
* @param cb
|
||||
*/
|
||||
export function single<T>(cb: () => T) {
|
||||
return cb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @__PURE__
|
||||
* @since 2.0.0
|
||||
* Creates a transient object
|
||||
* @param cb
|
||||
*/
|
||||
export function transient<T>(cb: () => () => T) {
|
||||
return cb;
|
||||
}
|
||||
|
||||
export function Service(key: string): unknown
|
||||
export function Service<T extends keyof Dependencies>(key: T) {
|
||||
return useContainerRaw().get(key)!
|
||||
}
|
||||
|
||||
export function Services<const T extends (keyof Dependencies)[]>(...keys: [...T]) {
|
||||
const container = useContainerRaw();
|
||||
return keys.map(k => container.get(k)!) as IntoDependencies<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the user's conf, check for any excluded dependency keys.
|
||||
* Then, call conf.build to get the rest of the users' dependencies.
|
||||
* Finally, update the containerSubject with the new container state
|
||||
* @param conf
|
||||
*/
|
||||
export async function composeRoot<T extends Dependencies>(conf: DependencyConfiguration<T>) {
|
||||
//container should have no client or logger yet.
|
||||
const excludeLogger = conf.exclude?.has('@sern/logger');
|
||||
const container = useContainerRaw();
|
||||
if (!excludeLogger) {
|
||||
container.upsert({
|
||||
'@sern/logger': () => new DefaultLogging(),
|
||||
});
|
||||
}
|
||||
//Build the container based on the callback provided by the user
|
||||
const updatedContainer = await conf.build(container as CoreContainer<CoreDependencies>);
|
||||
try {
|
||||
updatedContainer.get('@sern/client');
|
||||
} catch {
|
||||
throw new Error(SernError.MissingRequired + " No client was provided")
|
||||
}
|
||||
|
||||
if (!excludeLogger) {
|
||||
updatedContainer.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
|
||||
}
|
||||
}
|
||||
|
||||
export function useContainer<const T extends Dependencies>() {
|
||||
console.warn(`
|
||||
Warning: using a container hook is not recommended.
|
||||
Could lead to many unwanted side effects.
|
||||
Use the new Service(s) api function instead.
|
||||
`
|
||||
);
|
||||
return <V extends (keyof T)[]>(...keys: [...V]) =>
|
||||
keys.map(key => useContainerRaw().get(key as keyof Dependencies)) as MapDeps<T, V>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
3
src/core/ioc/index.ts
Normal file
3
src/core/ioc/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { useContainerRaw, makeDependencies } from './base';
|
||||
export { Service, Services, single, transient } from './dependency-injection';
|
||||
export type { Singleton, Transient } from './types'
|
||||
47
src/core/ioc/types.ts
Normal file
47
src/core/ioc/types.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Container, UnpackFunction } from "iti";
|
||||
import { Awaitable, ModuleStore } from "../../shared";
|
||||
import { ErrorHandling, Logging, ModuleManager } from "../contracts";
|
||||
import { SernEmitter } from "../";
|
||||
import EventEmitter from "node:events";
|
||||
|
||||
export type Singleton<T> = () => T;
|
||||
export type Transient<T> = () => () => T;
|
||||
|
||||
|
||||
export interface CoreDependencies {
|
||||
'@sern/logger'?: Singleton<Logging>;
|
||||
'@sern/emitter': Singleton<SernEmitter>;
|
||||
'@sern/store': Singleton<ModuleStore>;
|
||||
'@sern/modules': Singleton<ModuleManager>;
|
||||
'@sern/errors': Singleton<ErrorHandling>;
|
||||
}
|
||||
|
||||
export interface Dependencies extends CoreDependencies {
|
||||
'@sern/client': Singleton<EventEmitter>;
|
||||
}
|
||||
|
||||
|
||||
export type DependencyFromKey<T extends keyof Dependencies> = Dependencies[T];
|
||||
export type IntoDependencies<Tuple extends [...any[]]> = {
|
||||
[Index in keyof Tuple]: UnpackFunction<DependencyFromKey<Tuple[Index]>&{}>; //Unpack and make NonNullable
|
||||
} & { length: Tuple['length'] };
|
||||
|
||||
export interface DependencyConfiguration<T extends Dependencies> {
|
||||
//@deprecated. Loggers will always be included in the future
|
||||
exclude?: Set<'@sern/logger'>;
|
||||
build: (root: Container<CoreDependencies, {}>) => Awaitable<Container<T, {}>>;
|
||||
}
|
||||
|
||||
//To be removed in future
|
||||
//prettier-ignore
|
||||
export type MapDeps<Deps extends Dependencies, T extends readonly unknown[]> = T extends [
|
||||
infer First extends keyof Deps,
|
||||
...infer Rest extends readonly unknown[],
|
||||
]
|
||||
? [
|
||||
UnpackFunction<Deps[First]>,
|
||||
...(MapDeps<Deps, Rest> extends [never] ? [] : MapDeps<Deps, Rest>),
|
||||
]
|
||||
: [never];
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { SernError } from './structures/errors';
|
||||
import { type Result, Err, Ok } from 'ts-results-es';
|
||||
import { Processed } from '../types/core';
|
||||
import { Module } from '../types/module';
|
||||
import { Module } from './types/modules';
|
||||
import * as assert from 'node:assert';
|
||||
import util from 'node:util';
|
||||
import { type Observable, from, mergeMap, ObservableInput } from 'rxjs';
|
||||
import { readdir, stat } from 'fs/promises';
|
||||
import { basename, join, resolve } from 'path';
|
||||
import { Processed } from '../handler/types';
|
||||
|
||||
export type ModuleResult<T> = Promise<Result<Processed<T>, SernError>>;
|
||||
|
||||
export async function importModule<T>(absPath: string) {
|
||||
/// #if MODE === 'esm'
|
||||
return import(absPath).then(i => i.default as T);
|
||||
|
||||
@@ -17,12 +17,12 @@ import {
|
||||
share,
|
||||
switchMap,
|
||||
} from 'rxjs';
|
||||
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'
|
||||
import { Awaitable } from '../shared';
|
||||
import { PluginResult, VoidResult } from './types/plugins';
|
||||
/**
|
||||
* if {src} is true, mapTo V, else ignore
|
||||
* @param item
|
||||
|
||||
45
src/core/structures/container.ts
Normal file
45
src/core/structures/container.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Container } from "iti";
|
||||
import { DefaultErrorHandling, DefaultModuleManager, SernEmitter } from "../";
|
||||
import { isAsyncFunction} from "node:util/types";
|
||||
import * as assert from 'node:assert'
|
||||
import { Dependencies } from "../ioc/types";
|
||||
/**
|
||||
* Provides all the defaults for sern to function properly.
|
||||
* The only user provided dependency needs to be @sern/client
|
||||
*/
|
||||
export class CoreContainer<T extends Partial<Dependencies>> extends Container<T, {}> {
|
||||
private _ready = false;
|
||||
constructor() {
|
||||
super();
|
||||
(this as Container<{}, {}>)
|
||||
.add({
|
||||
'@sern/errors': () => new DefaultErrorHandling(),
|
||||
'@sern/emitter': () => new SernEmitter(),
|
||||
'@sern/modules': () => new DefaultModuleManager(new Map())
|
||||
})
|
||||
}
|
||||
|
||||
async withInit<const Keys extends keyof Dependencies>(...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') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
} from 'discord.js';
|
||||
import { CoreContext } from './core-context';
|
||||
import { Result, Ok, Err } from 'ts-results-es';
|
||||
import { ReplyOptions } from '../../types/handler';
|
||||
import * as assert from 'assert';
|
||||
import { ReplyOptions } from '../../shared';
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './enums';
|
||||
export * from './context';
|
||||
export * from './sern-emitter';
|
||||
export * from './services'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { Payload, SernEventsMapping } from '../../types/handler';
|
||||
import { PayloadType } from '../../core/structures';
|
||||
import type { Module } from '../../types/module';
|
||||
import { Payload, SernEventsMapping } from '../../shared';
|
||||
import { Module } from '../types/modules';
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
|
||||
14
src/core/structures/services/error-handling.ts
Normal file
14
src/core/structures/services/error-handling.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ErrorHandling } from "../../contracts";
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export class DefaultErrorHandling implements ErrorHandling {
|
||||
keepAlive = 5;
|
||||
crash(error: Error): never {
|
||||
throw error;
|
||||
}
|
||||
updateAlive(_: Error) {
|
||||
this.keepAlive--;
|
||||
}
|
||||
}
|
||||
3
src/core/structures/services/index.ts
Normal file
3
src/core/structures/services/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './error-handling';
|
||||
export * from './logger';
|
||||
export * from './module-manager';
|
||||
23
src/core/structures/services/logger.ts
Normal file
23
src/core/structures/services/logger.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { LogPayload, Logging } from "../../contracts";
|
||||
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export class DefaultLogging implements Logging {
|
||||
private date = () => new Date();
|
||||
debug(payload: LogPayload): void {
|
||||
console.debug(`DEBUG: ${this.date().toISOString()} -> ${payload.message}`);
|
||||
}
|
||||
|
||||
error(payload: LogPayload): void {
|
||||
console.error(`ERROR: ${this.date().toISOString()} -> ${payload.message}`);
|
||||
}
|
||||
|
||||
info(payload: LogPayload): void {
|
||||
console.info(`INFO: ${this.date().toISOString()} -> ${payload.message}`);
|
||||
}
|
||||
|
||||
warning(payload: LogPayload): void {
|
||||
console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`);
|
||||
}
|
||||
}
|
||||
32
src/core/structures/services/module-manager.ts
Normal file
32
src/core/structures/services/module-manager.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ModuleStore } from "../../../shared";
|
||||
import { ModuleManager } from "../../contracts";
|
||||
import { importModule } from "../../module-loading";
|
||||
import { CommandModule } from "../../types/modules";
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
set(id: string, path: string): void {
|
||||
this.moduleStore.set(id, path);
|
||||
}
|
||||
//not tested
|
||||
getPublishableCommands(): Promise<CommandModule[]> {
|
||||
const entries = this.moduleStore.entries();
|
||||
const publishable = 0b000000110;
|
||||
return Promise.all(
|
||||
Array.from(entries)
|
||||
.filter(([id]) => (Number.parseInt(id.at(-1)!) & publishable) !== 0)
|
||||
.map(([, path]) => importModule<CommandModule>(path)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,22 +23,22 @@ import {
|
||||
UserContextMenuCommandInteraction,
|
||||
UserSelectMenuInteraction,
|
||||
} from 'discord.js';
|
||||
import { InitArgs } from './core';
|
||||
import { Args, Payload, SlashOptions } from '../types/handler';
|
||||
import { Context } from '../classic/context';
|
||||
import { Processed } from '../types/core';
|
||||
import { CommandType, PluginType } from '../core/structures/enums';
|
||||
import type { Awaitable, SernEventsMapping } from './handler';
|
||||
import type { InitPlugin, ControlPlugin } from './plugin';
|
||||
import { EventType } from '../core/structures/enums';
|
||||
import type { AnyCommandPlugin, AnyEventPlugin } from './plugin';
|
||||
import { sernMeta } from '../commands';
|
||||
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 {
|
||||
fullPath: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type AnyDefinedModule = Processed<CommandModule | EventModule>;
|
||||
|
||||
export interface Module {
|
||||
type: CommandType | EventType;
|
||||
name?: string;
|
||||
@@ -48,12 +48,7 @@ export interface Module {
|
||||
[sernMeta]: CommandMeta;
|
||||
execute: (...args: any[]) => Awaitable<any>;
|
||||
}
|
||||
export interface CommandTypeModule extends Module {
|
||||
type: CommandType;
|
||||
}
|
||||
export interface EventTypeModule extends Module {
|
||||
type: EventType;
|
||||
}
|
||||
|
||||
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping>
|
||||
extends Module {
|
||||
name?: T;
|
||||
@@ -144,71 +139,7 @@ export interface BothCommand extends Module {
|
||||
options?: SernOptionsData[];
|
||||
execute: (ctx: Context, args: Args) => Awaitable<unknown>;
|
||||
}
|
||||
export interface CommandArgsMatrix {
|
||||
[CommandType.Text]: {
|
||||
[PluginType.Control]: [Context, ['text', string[]]];
|
||||
[PluginType.Init]: [InitArgs<Processed<TextCommand>>];
|
||||
};
|
||||
[CommandType.Slash]: {
|
||||
[PluginType.Control]: [Context, ['slash', /* library coupled */ SlashOptions]];
|
||||
[PluginType.Init]: [InitArgs<Processed<SlashCommand>>];
|
||||
};
|
||||
[CommandType.Both]: {
|
||||
[PluginType.Control]: [Context, Args];
|
||||
[PluginType.Init]: [InitArgs<Processed<BothCommand>>];
|
||||
};
|
||||
[CommandType.CtxMsg]: {
|
||||
[PluginType.Control]: [/* library coupled */ MessageContextMenuCommandInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ContextMenuMsg>>];
|
||||
};
|
||||
[CommandType.CtxUser]: {
|
||||
[PluginType.Control]: [/* library coupled */ UserContextMenuCommandInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ContextMenuUser>>];
|
||||
};
|
||||
[CommandType.Button]: {
|
||||
[PluginType.Control]: [/* library coupled */ ButtonInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ButtonCommand>>];
|
||||
};
|
||||
[CommandType.StringSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ StringSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<StringSelectCommand>>];
|
||||
};
|
||||
[CommandType.RoleSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ RoleSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<RoleSelectCommand>>];
|
||||
};
|
||||
[CommandType.ChannelSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ ChannelSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ChannelSelectCommand>>];
|
||||
};
|
||||
[CommandType.MentionableSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ MentionableSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<MentionableSelectCommand>>];
|
||||
};
|
||||
[CommandType.UserSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ UserSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<UserSelectCommand>>];
|
||||
};
|
||||
[CommandType.Modal]: {
|
||||
[PluginType.Control]: [/* library coupled */ ModalSubmitInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ModalSubmitCommand>>];
|
||||
};
|
||||
}
|
||||
|
||||
export interface EventArgsMatrix {
|
||||
[EventType.Discord]: {
|
||||
[PluginType.Control]: /* library coupled */ ClientEvents[keyof ClientEvents];
|
||||
[PluginType.Init]: [InitArgs<Processed<DiscordEventCommand>>];
|
||||
};
|
||||
[EventType.Sern]: {
|
||||
[PluginType.Control]: [Payload];
|
||||
[PluginType.Init]: [InitArgs<Processed<SernEventCommand>>];
|
||||
};
|
||||
[EventType.External]: {
|
||||
[PluginType.Control]: unknown[];
|
||||
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
|
||||
};
|
||||
}
|
||||
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
|
||||
export type CommandModule =
|
||||
| TextCommand
|
||||
119
src/core/types/plugins.ts
Normal file
119
src/core/types/plugins.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Plugins can be inserted on all commands and are emitted
|
||||
*
|
||||
* 1. On ready event, where all commands are loaded.
|
||||
* 2. On corresponding observable (when command triggers)
|
||||
*
|
||||
* The goal of plugins is to organize commands and
|
||||
* provide extensions to repetitive patterns
|
||||
* examples include refreshing modules,
|
||||
* categorizing commands, cool-downs, permissions, etc.
|
||||
* Plugins are reminiscent of middleware in express.
|
||||
*/
|
||||
|
||||
import type { Err, Ok, Result } from 'ts-results-es';
|
||||
import type { BothCommand, ButtonCommand, ChannelSelectCommand, CommandModule, ContextMenuMsg, ContextMenuUser, DiscordEventCommand, EventModule, ExternalEventCommand, MentionableSelectCommand, ModalSubmitCommand, RoleSelectCommand, SernEventCommand, SlashCommand, StringSelectCommand, TextCommand, UserSelectCommand } from './modules';
|
||||
import { Args, Awaitable, Payload, SlashOptions } from '../../shared';
|
||||
import { CommandType, Context, EventType, PluginType } from '../structures';
|
||||
import { InitArgs, Processed } from '../../handler/types';
|
||||
import { ButtonInteraction, ChannelSelectMenuInteraction, ClientEvents, MentionableSelectMenuInteraction, MessageContextMenuCommandInteraction, ModalSubmitInteraction, RoleSelectMenuInteraction, StringSelectMenuInteraction, UserContextMenuCommandInteraction, UserSelectMenuInteraction } from 'discord.js';
|
||||
|
||||
export type PluginResult = Awaitable<VoidResult>;
|
||||
export type VoidResult = Result<void, void>;
|
||||
|
||||
export interface Controller {
|
||||
next: () => Ok<void>;
|
||||
stop: () => Err<void>;
|
||||
}
|
||||
export interface Plugin<Args extends any[] = any[]> {
|
||||
type: PluginType;
|
||||
execute: (...args: Args) => PluginResult;
|
||||
}
|
||||
|
||||
export interface InitPlugin<Args extends any[] = any[]> {
|
||||
type: PluginType.Init;
|
||||
execute: (...args: Args) => PluginResult;
|
||||
}
|
||||
export interface ControlPlugin<Args extends any[] = any[]> {
|
||||
type: PluginType.Control;
|
||||
execute: (...args: Args) => PluginResult;
|
||||
}
|
||||
|
||||
export type AnyCommandPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<CommandModule>>]>;
|
||||
export type AnyEventPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<EventModule>>]>;
|
||||
|
||||
export type CommandArgs<
|
||||
I extends CommandType = CommandType,
|
||||
J extends PluginType = PluginType,
|
||||
> = CommandArgsMatrix[I][J];
|
||||
|
||||
export type EventArgs<
|
||||
I extends EventType = EventType,
|
||||
J extends PluginType = PluginType,
|
||||
> = EventArgsMatrix[I][J];
|
||||
|
||||
export interface CommandArgsMatrix {
|
||||
[CommandType.Text]: {
|
||||
[PluginType.Control]: [Context, ['text', string[]]];
|
||||
[PluginType.Init]: [InitArgs<Processed<TextCommand>>];
|
||||
};
|
||||
[CommandType.Slash]: {
|
||||
[PluginType.Control]: [Context, ['slash', /* library coupled */ SlashOptions]];
|
||||
[PluginType.Init]: [InitArgs<Processed<SlashCommand>>];
|
||||
};
|
||||
[CommandType.Both]: {
|
||||
[PluginType.Control]: [Context, Args];
|
||||
[PluginType.Init]: [InitArgs<Processed<BothCommand>>];
|
||||
};
|
||||
[CommandType.CtxMsg]: {
|
||||
[PluginType.Control]: [/* library coupled */ MessageContextMenuCommandInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ContextMenuMsg>>];
|
||||
};
|
||||
[CommandType.CtxUser]: {
|
||||
[PluginType.Control]: [/* library coupled */ UserContextMenuCommandInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ContextMenuUser>>];
|
||||
};
|
||||
[CommandType.Button]: {
|
||||
[PluginType.Control]: [/* library coupled */ ButtonInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ButtonCommand>>];
|
||||
};
|
||||
[CommandType.StringSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ StringSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<StringSelectCommand>>];
|
||||
};
|
||||
[CommandType.RoleSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ RoleSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<RoleSelectCommand>>];
|
||||
};
|
||||
[CommandType.ChannelSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ ChannelSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ChannelSelectCommand>>];
|
||||
};
|
||||
[CommandType.MentionableSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ MentionableSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<MentionableSelectCommand>>];
|
||||
};
|
||||
[CommandType.UserSelect]: {
|
||||
[PluginType.Control]: [/* library coupled */ UserSelectMenuInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<UserSelectCommand>>];
|
||||
};
|
||||
[CommandType.Modal]: {
|
||||
[PluginType.Control]: [/* library coupled */ ModalSubmitInteraction];
|
||||
[PluginType.Init]: [InitArgs<Processed<ModalSubmitCommand>>];
|
||||
};
|
||||
}
|
||||
|
||||
export interface EventArgsMatrix {
|
||||
[EventType.Discord]: {
|
||||
[PluginType.Control]: /* library coupled */ ClientEvents[keyof ClientEvents];
|
||||
[PluginType.Init]: [InitArgs<Processed<DiscordEventCommand>>];
|
||||
};
|
||||
[EventType.Sern]: {
|
||||
[PluginType.Control]: [Payload];
|
||||
[PluginType.Init]: [InitArgs<Processed<SernEventCommand>>];
|
||||
};
|
||||
[EventType.External]: {
|
||||
[PluginType.Control]: unknown[];
|
||||
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ClientEvents } from 'discord.js';
|
||||
import { CommandType, EventType, PluginType } from './core/structures';
|
||||
import { AnyEventPlugin, Plugin } from './types/plugin';
|
||||
import { CommandModule, EventModule, InputCommand, InputEvent } from './types/module';
|
||||
import { partition } from './core/functions';
|
||||
import { filename, filePath } from './core/module-loading';
|
||||
import { Awaitable } from './types/handler';
|
||||
import { CommandType, EventType, PluginType } from '../core/structures';
|
||||
import { AnyEventPlugin, Plugin } from '../core/types/plugins';
|
||||
import { CommandModule, EventModule, InputCommand, InputEvent } from '../core/types/modules';
|
||||
import { partition } from '../core/functions';
|
||||
import { filename, filePath } from '../core/module-loading';
|
||||
import { Awaitable } from '../shared';
|
||||
export const sernMeta = Symbol('@sern/meta');
|
||||
const appBitField = 0b000000011111;
|
||||
/*
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { Processed } from '../../types/core';
|
||||
import type { BothCommand, CommandModule, EventModule, Module } from '../../types/module';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import * as assert from 'node:assert';
|
||||
import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs';
|
||||
@@ -8,9 +6,10 @@ import { createResultResolver } from './generic';
|
||||
import { AutocompleteInteraction, BaseInteraction, Message } from 'discord.js';
|
||||
import { treeSearch } from '../../core/functions';
|
||||
import { SernError } from '../../core/structures/errors';
|
||||
import { Args } from '../../types/handler';
|
||||
import { CommandType, Context, EventType } from '../../core';
|
||||
import { CommandType, Context } from '../../core';
|
||||
import { isAutocomplete } from '../../core/predicates';
|
||||
import { Args, Processed } from '../types';
|
||||
import { BothCommand, CommandModule, Module } from '../../core/types/modules';
|
||||
|
||||
export function dispatchInteraction<
|
||||
T extends CommandModule,
|
||||
@@ -101,7 +100,7 @@ export function createDispatcher(payload: {
|
||||
}) {
|
||||
switch (payload.module.type) {
|
||||
case CommandType.Text:
|
||||
throw Error(SernError.MismatchEvent + ' Found a text module in interaction stream.');
|
||||
throw Error(SernError.MismatchEvent + ' Found a text module in interaction stream. ' + payload.module);
|
||||
case CommandType.Slash:
|
||||
case CommandType.Both: {
|
||||
if (isAutocomplete(payload.event)) {
|
||||
@@ -109,7 +108,7 @@ export function createDispatcher(payload: {
|
||||
* Autocomplete is a special case that
|
||||
* must be handled separately, since it's
|
||||
* too different from regular command modules
|
||||
* cast safety: payload is already guaranteed to be a slash command or both command
|
||||
* CAST SAFETY: payload is already guaranteed to be a slash command or both command
|
||||
*/
|
||||
return dispatchAutocomplete(payload as never);
|
||||
}
|
||||
|
||||
@@ -8,19 +8,18 @@ import { ModuleManager } from '../../core';
|
||||
import { SernError } from '../../core/structures/errors';
|
||||
import { callPlugin, everyPluginOk, filterMap, filterMapTo } from '../../core/operators';
|
||||
import { defaultModuleLoader } from '../../core/module-loading';
|
||||
import { ImportPayload, Processed } from '../../types/core';
|
||||
import { CommandModule, Module } from '../../types/module';
|
||||
import { CommandModule, Module, AnyModule } from '../../core/types/modules';
|
||||
import { contextArgs, createDispatcher, dispatchMessage } from './dispatchers';
|
||||
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 { AnyModule } from '../../types/module';
|
||||
import { sernMeta } from '../commands';
|
||||
import { Err, Result } from 'ts-results-es';
|
||||
import { Awaitable } from '../../types/handler';
|
||||
import { fmt } from './messages';
|
||||
import { ControlPlugin, VoidResult } from '../../types/plugin';
|
||||
import { ControlPlugin, VoidResult } from '../../core/types/plugins';
|
||||
import { ImportPayload, Processed } from '../types';
|
||||
import { Awaitable } from '../../shared';
|
||||
|
||||
function createGenericHandler<Source, Narrowed extends Source, Output>(
|
||||
source: Observable<Source>,
|
||||
|
||||
@@ -5,9 +5,10 @@ import { SernEmitter } from '../../core';
|
||||
import { sharedObservable } from '../../core/operators';
|
||||
import { isAutocomplete, isCommand, isMessageComponent, isModal } from '../../core/predicates';
|
||||
import { createInteractionHandler, executeModule, makeModuleExecutor } from './generic';
|
||||
import { DependencyList } from '../../types/core';
|
||||
import { DependencyList } from '../types';
|
||||
|
||||
export function makeInteractionHandler([emitter,,, modules, client]: DependencyList ) {
|
||||
|
||||
export function makeInteractionHandler([emitter, _, _1, modules, client]: DependencyList ) {
|
||||
const interactionStream$ = sharedObservable<Interaction>(client, 'interactionCreate');
|
||||
const handle = createInteractionHandler(interactionStream$, modules);
|
||||
|
||||
@@ -22,6 +23,6 @@ export function makeInteractionHandler([emitter, _, _1, modules, client]: Depend
|
||||
makeModuleExecutor(module => {
|
||||
emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
|
||||
}),
|
||||
concatMap(module => executeModule(emitter, module)),
|
||||
concatMap(payload => executeModule(emitter, payload)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Message } from 'discord.js';
|
||||
import { SernEmitter } from '../../core';
|
||||
import { sharedObservable } from '../../core/operators';
|
||||
import { createMessageHandler, executeModule, isNonBot, makeModuleExecutor } from './generic';
|
||||
import { DependencyList } from '../../types/core';
|
||||
import { DependencyList } from '../types';
|
||||
|
||||
/**
|
||||
* Removes the first character(s) _[depending on prefix length]_ of the message
|
||||
@@ -21,7 +21,7 @@ export function fmt(msg: string, prefix: string): string[] {
|
||||
}
|
||||
|
||||
export function makeMessageHandler(
|
||||
[emitter, , log, modules, client]: DependencyList,
|
||||
[emitter,, log, modules, client]: DependencyList,
|
||||
defaultPrefix: string | undefined,
|
||||
) {
|
||||
if (!defaultPrefix) {
|
||||
|
||||
@@ -3,15 +3,15 @@ import { CommandType } from '../../core/structures';
|
||||
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/core';
|
||||
import { Module } from '../../types/module';
|
||||
import { SernEmitter } from '../../core';
|
||||
import { sernMeta } from '../commands';
|
||||
import { Processed, DependencyList } from '../types';
|
||||
import * as assert from 'node:assert';
|
||||
import { buildModules, callInitPlugins } from './generic';
|
||||
import { Module } from '../../core/types/modules';
|
||||
|
||||
export function startReadyEvent(
|
||||
[sEmitter, errorHandler, , moduleManager, client]: DependencyList,
|
||||
[sEmitter,,, moduleManager, client]: DependencyList,
|
||||
allPaths: ObservableInput<string>,
|
||||
) {
|
||||
const ready$ = fromEvent(client!, 'ready').pipe(take(1));
|
||||
@@ -32,9 +32,9 @@ export function startReadyEvent(
|
||||
}),
|
||||
)
|
||||
.subscribe(module => {
|
||||
const result = registerModule(moduleManager, module as Processed<Module>);
|
||||
const result = registerModule(moduleManager, module);
|
||||
if (result.err) {
|
||||
errorHandler.crash(Error(SernError.InvalidModuleType));
|
||||
throw Error(SernError.InvalidModuleType);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -44,7 +44,9 @@ function registerModule<T extends Processed<Module>>(
|
||||
module: T,
|
||||
): Result<void, void> {
|
||||
const { id, fullPath } = module[sernMeta];
|
||||
if (module.type === CommandType.Both || module.type === CommandType.Text) {
|
||||
if (module.type === CommandType.Both
|
||||
|| module.type === CommandType.Text
|
||||
) {
|
||||
assert.ok('alias' in module);
|
||||
assert.ok(Array.isArray(module.alias));
|
||||
module.alias?.forEach(a => manager.set(`${a}__A0`, fullPath));
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
import { ObservableInput, catchError, finalize, map, mergeAll, of } from 'rxjs';
|
||||
import type { Dependencies, Processed, Wrapper } from '../../types/core';
|
||||
import type { CommandModule, EventModule } from '../../types/module';
|
||||
import type { EventEmitter } from 'node:events';
|
||||
import { SernEmitter } from '../../core';
|
||||
import type { ErrorHandling, Logging } from '../../core/contracts';
|
||||
import { EventType } from '../../core/structures';
|
||||
import { SernError } from '../../core/structures/errors';
|
||||
import { eventDispatcher } from './dispatchers';
|
||||
import { handleError } from '../../core/contracts/error-handling';
|
||||
import { useContainerRaw } from '../../core/dependencies';
|
||||
import { buildModules, callInitPlugins } from './generic';
|
||||
import { handleError } from '../../core/operators';
|
||||
import { Service, useContainerRaw } from '../../core/ioc';
|
||||
import { DependencyList, Processed } from '../types';
|
||||
|
||||
|
||||
|
||||
export function makeEventsHandler(
|
||||
[s, err, log, client]: [SernEmitter, ErrorHandling, Logging | undefined, EventEmitter],
|
||||
[emitter, err, log,, client]: DependencyList,
|
||||
allPaths: ObservableInput<string>,
|
||||
containerGetter: Wrapper['containerConfig'],
|
||||
) {
|
||||
const lazy = (k: string) => containerGetter.get(k as keyof Dependencies)[0];
|
||||
|
||||
//code smell
|
||||
const intoDispatcher = (e: Processed<EventModule | CommandModule>) => {
|
||||
switch (e.type) {
|
||||
case EventType.Sern:
|
||||
return eventDispatcher(e, s);
|
||||
return eventDispatcher(e, emitter);
|
||||
case EventType.Discord:
|
||||
return eventDispatcher(e, client);
|
||||
case EventType.External:
|
||||
return eventDispatcher(e, lazy(e.emitter));
|
||||
return eventDispatcher(e, Service(e.emitter));
|
||||
default:
|
||||
return err.crash(
|
||||
Error(SernError.InvalidModuleType + ' while creating event handler'),
|
||||
@@ -35,12 +33,12 @@ export function makeEventsHandler(
|
||||
};
|
||||
of(null)
|
||||
.pipe(
|
||||
buildModules(allPaths, s),
|
||||
buildModules(allPaths, emitter),
|
||||
callInitPlugins({
|
||||
onStop: module =>
|
||||
s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
|
||||
emitter.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
|
||||
onNext: ({ module }) => {
|
||||
s.emit('module.register', SernEmitter.success(module));
|
||||
emitter.emit('module.register', SernEmitter.success(module));
|
||||
return module;
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -2,12 +2,13 @@ import { makeEventsHandler } from './events/user-defined';
|
||||
import { makeInteractionHandler } from './events/interactions';
|
||||
import { startReadyEvent } from './events/ready';
|
||||
import { makeMessageHandler } from './events/messages';
|
||||
import { makeFetcher, makeDependencies, useContainerRaw } from '../core/dependencies';
|
||||
import { err, ok } from '../core/functions';
|
||||
import { Wrapper } from '../types/core';
|
||||
import { getCommands } from '../core/module-loading';
|
||||
import { getFullPathTree } from '../core/module-loading';
|
||||
import { catchError, finalize, merge } from 'rxjs';
|
||||
import { handleError } from '../core/contracts/error-handling';
|
||||
import { handleError } from '../core/operators';
|
||||
import { Services, useContainerRaw } from '../core/ioc';
|
||||
import { Wrapper } from '../shared';
|
||||
|
||||
/**
|
||||
* @since 1.0.0
|
||||
* @param wrapper Options to pass into sern.
|
||||
@@ -23,20 +24,20 @@ import { handleError } from '../core/contracts/error-handling';
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
|
||||
export function init(wrapper: Wrapper) {
|
||||
const startTime = performance.now();
|
||||
const dependenciesAnd = makeFetcher(wrapper.containerConfig);
|
||||
const dependencies = dependenciesAnd(['@sern/modules', '@sern/client']);
|
||||
|
||||
const dependencies = useDependencies();
|
||||
|
||||
if (wrapper.events !== undefined) {
|
||||
makeEventsHandler(
|
||||
dependenciesAnd(['@sern/client']),
|
||||
wrapper.events,
|
||||
wrapper.containerConfig,
|
||||
dependencies,
|
||||
getFullPathTree(wrapper.events),
|
||||
);
|
||||
}
|
||||
|
||||
startReadyEvent(dependencies, getCommands(wrapper.commands)).add(() => console.log('ready'));
|
||||
startReadyEvent(dependencies, getFullPathTree(wrapper.commands)).add(() => console.log('ready'));
|
||||
|
||||
const logger = dependencies[2];
|
||||
const errorHandler = dependencies[1];
|
||||
@@ -50,27 +51,28 @@ export function init(wrapper: Wrapper) {
|
||||
).pipe(
|
||||
catchError(handleError(errorHandler, logger)),
|
||||
finalize(() => {
|
||||
logger?.info({ message: 'a stream closed or reached end of lifetime' });
|
||||
logger?.info({ message: 'A stream closed or reached end of lifetime' });
|
||||
useContainerRaw()
|
||||
?.disposeAll()
|
||||
.then(() => logger?.info({ message: 'Cleaning container and crashing' }));
|
||||
})
|
||||
).subscribe()
|
||||
).subscribe();
|
||||
|
||||
const endTime = performance.now();
|
||||
dependencies[2]?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` });
|
||||
logger?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` });
|
||||
}
|
||||
|
||||
|
||||
function useDependencies() {
|
||||
return Services(
|
||||
'@sern/emitter',
|
||||
'@sern/errors',
|
||||
'@sern/logger',
|
||||
'@sern/modules',
|
||||
'@sern/client'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated - Please import the function directly:
|
||||
* ```ts
|
||||
* import { makeDependencies } from '@sern/handler'
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
export { makeDependencies };
|
||||
/**
|
||||
* @since 1.0.0
|
||||
* The object passed into every plugin to control a command's behavior
|
||||
|
||||
23
src/handler/types.ts
Normal file
23
src/handler/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ErrorHandling, Logging, ModuleManager, SernEmitter } from "../core";
|
||||
import EventEmitter from "node:events";
|
||||
import { Module } from "../core/types/modules";
|
||||
|
||||
export type Processed<T> = T & { name: string; description: string };
|
||||
|
||||
export type DependencyList = [
|
||||
SernEmitter,
|
||||
ErrorHandling,
|
||||
Logging | undefined,
|
||||
ModuleManager,
|
||||
EventEmitter,
|
||||
];
|
||||
|
||||
export interface InitArgs<T extends Processed<Module>> {
|
||||
module: T;
|
||||
absPath: string;
|
||||
}
|
||||
|
||||
export interface ImportPayload<T> {
|
||||
module: T;
|
||||
absPath: string;
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
export * as Sern from './handler/sern';
|
||||
export * from './types/core';
|
||||
export * from './types/handler';
|
||||
export * from './types/module';
|
||||
export * from './types/plugin';
|
||||
export * from './core';
|
||||
export { commandModule, eventModule, discordEvent } from './handler/commands'
|
||||
export { controller } from './handler/sern';
|
||||
export { commandModule, eventModule, discordEvent } from './commands';
|
||||
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
import type {
|
||||
CommandInteractionOptionResolver,
|
||||
InteractionReplyOptions,
|
||||
MessageReplyOptions,
|
||||
CommandInteractionOptionResolver,
|
||||
} from 'discord.js';
|
||||
import { Processed } from './core';
|
||||
import { AnyModule, CommandModule, EventModule } from './module';
|
||||
import { PayloadType } from '../core';
|
||||
|
||||
export type Awaitable<T> = PromiseLike<T> | T;
|
||||
|
||||
// Thanks to @kelsny
|
||||
export type ParseType<T> = {
|
||||
[K in keyof T]: T[K] extends unknown ? [k: K, args: T[K]] : never;
|
||||
}[keyof T];
|
||||
|
||||
export type Args = ParseType<{ text: string[]; slash: SlashOptions }>;
|
||||
|
||||
export type SlashOptions = Omit<CommandInteractionOptionResolver, 'getMessage' | 'getFocused'>;
|
||||
import { PayloadType } from './core';
|
||||
import { Dependencies } from './core/ioc/types';
|
||||
import { AnyModule } from './core/types/modules';
|
||||
|
||||
export type ReplyOptions =
|
||||
| string
|
||||
| Omit<InteractionReplyOptions, 'fetchReply'>
|
||||
| MessageReplyOptions;
|
||||
|
||||
export type AnyDefinedModule = Processed<CommandModule | EventModule>;
|
||||
export type Payload =
|
||||
| { type: PayloadType.Success; module: AnyModule }
|
||||
| { type: PayloadType.Failure; module?: AnyModule; reason: string | Error }
|
||||
@@ -35,3 +23,31 @@ export interface SernEventsMapping {
|
||||
error: [Payload];
|
||||
warning: [Payload];
|
||||
}
|
||||
|
||||
|
||||
export type Awaitable<T> = PromiseLike<T> | T;
|
||||
|
||||
export type ModuleStore = Map<string, string>;
|
||||
|
||||
export type Deprecated<Message extends string> = [never, Message];
|
||||
|
||||
|
||||
export interface Wrapper {
|
||||
commands: string;
|
||||
defaultPrefix?: string;
|
||||
events?: string;
|
||||
containerConfig?: {
|
||||
get: (...keys: (keyof Dependencies)[]) => unknown[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Thanks to @kelsny
|
||||
export type ParseType<T> = {
|
||||
[K in keyof T]: T[K] extends unknown ? [k: K, args: T[K]] : never;
|
||||
}[keyof T];
|
||||
|
||||
export type Args = ParseType<{ text: string[]; slash: SlashOptions }>;
|
||||
|
||||
export type SlashOptions = Omit<CommandInteractionOptionResolver, 'getMessage' | 'getFocused'>;
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* Core typings.
|
||||
* Includes iti, dependencies, and other commonly used types
|
||||
* Should not have discord.js imports
|
||||
*/
|
||||
import { type EventEmitter } from 'node:events';
|
||||
import { ErrorHandling, Logging, ModuleManager, SernEmitter } from '../core';
|
||||
import { Container, UnpackFunction } from 'iti';
|
||||
import { Module } from './module';
|
||||
import { Awaitable } from './handler';
|
||||
|
||||
export type ModuleStore = Map<string, string>;
|
||||
export type DependencyList = [
|
||||
SernEmitter,
|
||||
ErrorHandling,
|
||||
Logging | undefined,
|
||||
ModuleManager,
|
||||
EventEmitter,
|
||||
];
|
||||
/**
|
||||
* After modules are transformed, name and description are given default values if none
|
||||
* are provided to Module. This type represents that transformation
|
||||
*/
|
||||
|
||||
export type LogPayload<T = unknown> = { message: T };
|
||||
export type Singleton<T> = () => T;
|
||||
export type Transient<T> = () => () => T;
|
||||
|
||||
export interface CoreDependencies {
|
||||
'@sern/logger'?: Singleton<Logging>;
|
||||
'@sern/emitter': Singleton<SernEmitter>;
|
||||
'@sern/store': Singleton<ModuleStore>;
|
||||
'@sern/modules': Singleton<ModuleManager>;
|
||||
'@sern/errors': Singleton<ErrorHandling>;
|
||||
}
|
||||
|
||||
export interface Dependencies extends CoreDependencies {
|
||||
'@sern/client': Singleton<EventEmitter>;
|
||||
}
|
||||
|
||||
//prettier-ignore
|
||||
export type MapDeps<Deps extends Dependencies, T extends readonly unknown[]> = T extends [
|
||||
infer First extends keyof Deps,
|
||||
...infer Rest extends readonly unknown[],
|
||||
]
|
||||
? [
|
||||
UnpackFunction<Deps[First]>,
|
||||
...(MapDeps<Deps, Rest> extends [never] ? [] : MapDeps<Deps, Rest>),
|
||||
]
|
||||
: [never];
|
||||
|
||||
/*
|
||||
* @deprecated
|
||||
* Will remove optional logger in the future
|
||||
*/export type OptionalDependencies = '@sern/logger';
|
||||
export type Processed<T> = T & { name: string; description: string };
|
||||
export type Deprecated<Message extends string> = [never, Message];
|
||||
export interface DependencyConfiguration<T extends Dependencies> {
|
||||
//@deprecated. Loggers will always be included in the future
|
||||
exclude?: Set<OptionalDependencies>;
|
||||
build: (root: Container<CoreDependencies, {}>) => Awaitable<Container<T, {}>>;
|
||||
}
|
||||
|
||||
export interface ImportPayload<T> {
|
||||
module: T;
|
||||
absPath: string;
|
||||
}
|
||||
|
||||
export interface Wrapper {
|
||||
commands: string;
|
||||
defaultPrefix?: string;
|
||||
events?: string;
|
||||
containerConfig: {
|
||||
get: (...keys: (keyof Dependencies)[]) => unknown[];
|
||||
};
|
||||
}
|
||||
export interface InitArgs<T extends Processed<Module>> {
|
||||
module: T;
|
||||
absPath: string;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Plugins can be inserted on all commands and are emitted
|
||||
*
|
||||
* 1. On ready event, where all commands are loaded.
|
||||
* 2. On corresponding observable (when command triggers)
|
||||
*
|
||||
* The goal of plugins is to organize commands and
|
||||
* provide extensions to repetitive patterns
|
||||
* examples include refreshing modules,
|
||||
* categorizing commands, cool-downs, permissions, etc.
|
||||
* Plugins are reminiscent of middleware in express.
|
||||
*/
|
||||
|
||||
import type { Err, Ok, Result } from 'ts-results-es';
|
||||
import type { CommandType, EventType, PluginType } from '../core/structures';
|
||||
import type { CommandArgsMatrix, CommandModule, EventArgsMatrix, EventModule } from './module';
|
||||
import type { InitArgs } from './core';
|
||||
import type { Awaitable } from './handler';
|
||||
import { Processed } from './core';
|
||||
export type PluginResult = Awaitable<VoidResult>;
|
||||
export type VoidResult = Result<void, void>;
|
||||
|
||||
export interface Controller {
|
||||
next: () => Ok<void>;
|
||||
stop: () => Err<void>;
|
||||
}
|
||||
export interface Plugin<Args extends any[] = any[]> {
|
||||
type: PluginType;
|
||||
execute: (...args: Args) => PluginResult;
|
||||
}
|
||||
|
||||
export interface InitPlugin<Args extends any[] = any[]> {
|
||||
type: PluginType.Init;
|
||||
execute: (...args: Args) => PluginResult;
|
||||
}
|
||||
export interface ControlPlugin<Args extends any[] = any[]> {
|
||||
type: PluginType.Control;
|
||||
execute: (...args: Args) => PluginResult;
|
||||
}
|
||||
|
||||
export type AnyCommandPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<CommandModule>>]>;
|
||||
export type AnyEventPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<EventModule>>]>;
|
||||
|
||||
export type CommandArgs<
|
||||
I extends CommandType = CommandType,
|
||||
J extends PluginType = PluginType,
|
||||
> = CommandArgsMatrix[I][J];
|
||||
|
||||
export type EventArgs<
|
||||
I extends EventType = EventType,
|
||||
J extends PluginType = PluginType,
|
||||
> = EventArgsMatrix[I][J];
|
||||
@@ -19,7 +19,8 @@ export default defineConfig([
|
||||
target: 'node16',
|
||||
tsconfig: './tsconfig-esm.json',
|
||||
outDir: './dist/esm',
|
||||
splitting: false,
|
||||
splitting: true,
|
||||
bundle: true,
|
||||
esbuildPlugins: [ifdefPlugin({ variables: { MODE: 'esm' }, verbose: true })],
|
||||
outExtension() {
|
||||
return {
|
||||
@@ -50,5 +51,9 @@ export default defineConfig([
|
||||
},
|
||||
...shared,
|
||||
},
|
||||
|
||||
// {
|
||||
// dts: true,
|
||||
// entry: ['src/presets/*.ts' ]
|
||||
//
|
||||
// }
|
||||
]);
|
||||
|
||||
@@ -5,13 +5,6 @@ __metadata:
|
||||
version: 6
|
||||
cacheKey: 8
|
||||
|
||||
"@cloudflare/workers-types@npm:^4.20230419.0":
|
||||
version: 4.20230419.0
|
||||
resolution: "@cloudflare/workers-types@npm:4.20230419.0"
|
||||
checksum: db1eb1e74029da8ee3eaed8c8b83e1bc3a10801d328ccfddf39258e5eb679ffb34776fab441f6ac66aa304bc3715dedce77083aed3d79b4825639fb24970c164
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@discordjs/builders@npm:^1.6.3":
|
||||
version: 1.6.3
|
||||
resolution: "@discordjs/builders@npm:1.6.3"
|
||||
@@ -449,7 +442,6 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@sern/handler@workspace:."
|
||||
dependencies:
|
||||
"@cloudflare/workers-types": ^4.20230419.0
|
||||
"@types/node": ^18.15.11
|
||||
"@typescript-eslint/eslint-plugin": 5.58.0
|
||||
"@typescript-eslint/parser": 5.58.0
|
||||
|
||||
Reference in New Issue
Block a user