diff --git a/.dependency-cruiser.js b/.dependency-cruiser.js
new file mode 100644
index 0000000..076dad5
--- /dev/null
+++ b/.dependency-cruiser.js
@@ -0,0 +1,450 @@
+/** @type {import('dependency-cruiser').IConfiguration} */
+module.exports = {
+ forbidden: [
+ /* rules from the 'recommended' preset: */
+ {
+ name: 'no-circular',
+ severity: 'warn',
+ comment:
+ 'This dependency is part of a circular relationship. You might want to revise ' +
+ 'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ',
+ from: {},
+ to: {
+ circular: true,
+ },
+ },
+ {
+ name: 'no-orphans',
+ comment:
+ "This is an orphan module - it's likely not used (anymore?). Either use it or " +
+ "remove it. If it's logical this module is an orphan (i.e. it's a config file), " +
+ 'add an exception for it in your dependency-cruiser configuration. By default ' +
+ 'this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration ' +
+ 'files (.d.ts), tsconfig.json and some of the babel and webpack configs.',
+ severity: 'warn',
+ from: {
+ orphan: true,
+ pathNot: [
+ '(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$', // dot files
+ '\\.d\\.ts$', // TypeScript declaration files
+ '(^|/)tsconfig\\.json$', // TypeScript config
+ '(^|/)(babel|webpack)\\.config\\.(js|cjs|mjs|ts|json)$', // other configs
+ ],
+ },
+ to: {},
+ },
+ {
+ name: 'no-deprecated-core',
+ comment:
+ 'A module depends on a node core module that has been deprecated. Find an alternative - these are ' +
+ "bound to exist - node doesn't deprecate lightly.",
+ severity: 'warn',
+ from: {},
+ to: {
+ dependencyTypes: ['core'],
+ path: [
+ '^(v8/tools/codemap)$',
+ '^(v8/tools/consarray)$',
+ '^(v8/tools/csvparser)$',
+ '^(v8/tools/logreader)$',
+ '^(v8/tools/profile_view)$',
+ '^(v8/tools/profile)$',
+ '^(v8/tools/SourceMap)$',
+ '^(v8/tools/splaytree)$',
+ '^(v8/tools/tickprocessor-driver)$',
+ '^(v8/tools/tickprocessor)$',
+ '^(node-inspect/lib/_inspect)$',
+ '^(node-inspect/lib/internal/inspect_client)$',
+ '^(node-inspect/lib/internal/inspect_repl)$',
+ '^(async_hooks)$',
+ '^(punycode)$',
+ '^(domain)$',
+ '^(constants)$',
+ '^(sys)$',
+ '^(_linklist)$',
+ '^(_stream_wrap)$',
+ ],
+ },
+ },
+ {
+ name: 'not-to-deprecated',
+ comment:
+ 'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' +
+ 'version of that module, or find an alternative. Deprecated modules are a security risk.',
+ severity: 'warn',
+ from: {},
+ to: {
+ dependencyTypes: ['deprecated'],
+ },
+ },
+ {
+ name: 'no-non-package-json',
+ severity: 'error',
+ comment:
+ "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " +
+ "That's problematic as the package either (1) won't be available on live (2 - worse) will be " +
+ 'available on live with an non-guaranteed version. Fix it by adding the package to the dependencies ' +
+ 'in your package.json.',
+ from: {},
+ to: {
+ dependencyTypes: ['npm-no-pkg', 'npm-unknown'],
+ },
+ },
+ {
+ name: 'not-to-unresolvable',
+ comment:
+ "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " +
+ 'module: add it to your package.json. In all other cases you likely already know what to do.',
+ severity: 'error',
+ from: {},
+ to: {
+ couldNotResolve: true,
+ },
+ },
+ {
+ name: 'no-duplicate-dep-types',
+ comment:
+ "Likely this module depends on an external ('npm') package that occurs more than once " +
+ 'in your package.json i.e. bot as a devDependencies and in dependencies. This will cause ' +
+ 'maintenance problems later on.',
+ severity: 'warn',
+ from: {},
+ to: {
+ moreThanOneDependencyType: true,
+ // as it's pretty common to have a type import be a type only import
+ // _and_ (e.g.) a devDependency - don't consider type-only dependency
+ // types for this rule
+ dependencyTypesNot: ['type-only'],
+ },
+ },
+
+ /* rules you might want to tweak for your specific situation: */
+ {
+ name: 'not-to-test',
+ comment:
+ "This module depends on code within a folder that should only contain tests. As tests don't " +
+ "implement functionality this is odd. Either you're writing a test outside the test folder " +
+ "or there's something in the test folder that isn't a test.",
+ severity: 'error',
+ from: {
+ pathNot: '^(test)',
+ },
+ to: {
+ path: '^(test)',
+ },
+ },
+ {
+ name: 'not-to-spec',
+ comment:
+ 'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' +
+ "If there's something in a spec that's of use to other modules, it doesn't have that single " +
+ 'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.',
+ severity: 'error',
+ from: {},
+ to: {
+ path: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$',
+ },
+ },
+ {
+ name: 'not-to-dev-dep',
+ severity: 'error',
+ comment:
+ "This module depends on an npm package from the 'devDependencies' section of your " +
+ 'package.json. It looks like something that ships to production, though. To prevent problems ' +
+ "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" +
+ 'section of your package.json. If this module is development only - add it to the ' +
+ 'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration',
+ from: {
+ path: '^(src)',
+ pathNot: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$',
+ },
+ to: {
+ dependencyTypes: ['npm-dev'],
+ },
+ },
+ {
+ name: 'optional-deps-used',
+ severity: 'info',
+ comment:
+ 'This module depends on an npm package that is declared as an optional dependency ' +
+ "in your package.json. As this makes sense in limited situations only, it's flagged here. " +
+ "If you're using an optional dependency here by design - add an exception to your" +
+ 'dependency-cruiser configuration.',
+ from: {},
+ to: {
+ dependencyTypes: ['npm-optional'],
+ },
+ },
+ {
+ name: 'peer-deps-used',
+ comment:
+ 'This module depends on an npm package that is declared as a peer dependency ' +
+ 'in your package.json. This makes sense if your package is e.g. a plugin, but in ' +
+ 'other cases - maybe not so much. If the use of a peer dependency is intentional ' +
+ 'add an exception to your dependency-cruiser configuration.',
+ severity: 'warn',
+ from: {},
+ to: {
+ dependencyTypes: ['npm-peer'],
+ },
+ },
+ ],
+ options: {
+ /* conditions specifying which files not to follow further when encountered:
+ - path: a regular expression to match
+ - dependencyTypes: see https://github.com/sverweij/dependency-cruiser/blob/main/doc/rules-reference.md#dependencytypes-and-dependencytypesnot
+ for a complete list
+ */
+ doNotFollow: {
+ path: 'node_modules',
+ },
+
+ /* conditions specifying which dependencies to exclude
+ - path: a regular expression to match
+ - dynamic: a boolean indicating whether to ignore dynamic (true) or static (false) dependencies.
+ leave out if you want to exclude neither (recommended!)
+ */
+ // exclude : {
+ // path: '',
+ // dynamic: true
+ // },
+
+ /* pattern specifying which files to include (regular expression)
+ dependency-cruiser will skip everything not matching this pattern
+ */
+ // includeOnly : '',
+
+ /* dependency-cruiser will include modules matching against the focus
+ regular expression in its output, as well as their neighbours (direct
+ dependencies and dependents)
+ */
+ // focus : '',
+
+ /* list of module systems to cruise */
+ // moduleSystems: ['amd', 'cjs', 'es6', 'tsd'],
+
+ /* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/develop/'
+ to open it on your online repo or `vscode://file/${process.cwd()}/` to
+ open it in visual studio code),
+ */
+ // prefix: '',
+
+ /* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation
+ true: also detect dependencies that only exist before typescript-to-javascript compilation
+ "specify": for each dependency identify whether it only exists before compilation or also after
+ */
+ tsPreCompilationDeps: true,
+
+ /*
+ list of extensions to scan that aren't javascript or compile-to-javascript.
+ Empty by default. Only put extensions in here that you want to take into
+ account that are _not_ parsable.
+ */
+ // extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"],
+
+ /* if true combines the package.jsons found from the module up to the base
+ folder the cruise is initiated from. Useful for how (some) mono-repos
+ manage dependencies & dependency definitions.
+ */
+ // combinedDependencies: false,
+
+ /* if true leave symlinks untouched, otherwise use the realpath */
+ // preserveSymlinks: false,
+
+ /* TypeScript project file ('tsconfig.json') to use for
+ (1) compilation and
+ (2) resolution (e.g. with the paths property)
+
+ The (optional) fileName attribute specifies which file to take (relative to
+ dependency-cruiser's current working directory). When not provided
+ defaults to './tsconfig.json'.
+ */
+ tsConfig: {
+ fileName: 'tsconfig.json',
+ },
+
+ /* Webpack configuration to use to get resolve options from.
+
+ The (optional) fileName attribute specifies which file to take (relative
+ to dependency-cruiser's current working directory. When not provided defaults
+ to './webpack.conf.js'.
+
+ The (optional) `env` and `arguments` attributes contain the parameters to be passed if
+ your webpack config is a function and takes them (see webpack documentation
+ for details)
+ */
+ // webpackConfig: {
+ // fileName: './webpack.config.js',
+ // env: {},
+ // arguments: {},
+ // },
+
+ /* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use
+ for compilation (and whatever other naughty things babel plugins do to
+ source code). This feature is well tested and usable, but might change
+ behavior a bit over time (e.g. more precise results for used module
+ systems) without dependency-cruiser getting a major version bump.
+ */
+ // babelConfig: {
+ // fileName: './.babelrc'
+ // },
+
+ /* List of strings you have in use in addition to cjs/ es6 requires
+ & imports to declare module dependencies. Use this e.g. if you've
+ re-declared require, use a require-wrapper or use window.require as
+ a hack.
+ */
+ // exoticRequireStrings: [],
+ /* options to pass on to enhanced-resolve, the package dependency-cruiser
+ uses to resolve module references to disk. You can set most of these
+ options in a webpack.conf.js - this section is here for those
+ projects that don't have a separate webpack config file.
+
+ Note: settings in webpack.conf.js override the ones specified here.
+ */
+ enhancedResolveOptions: {
+ /* List of strings to consider as 'exports' fields in package.json. Use
+ ['exports'] when you use packages that use such a field and your environment
+ supports it (e.g. node ^12.19 || >=14.7 or recent versions of webpack).
+
+ If you have an `exportsFields` attribute in your webpack config, that one
+ will have precedence over the one specified here.
+ */
+ exportsFields: ['exports'],
+ /* List of conditions to check for in the exports field. e.g. use ['imports']
+ if you're only interested in exposed es6 modules, ['require'] for commonjs,
+ or all conditions at once `(['import', 'require', 'node', 'default']`)
+ if anything goes for you. Only works when the 'exportsFields' array is
+ non-empty.
+
+ If you have a 'conditionNames' attribute in your webpack config, that one will
+ have precedence over the one specified here.
+ */
+ conditionNames: ['import', 'require', 'node', 'default'],
+ /*
+ The extensions, by default are the same as the ones dependency-cruiser
+ can access (run `npx depcruise --info` to see which ones that are in
+ _your_ environment. If that list is larger than what you need (e.g.
+ it contains .js, .jsx, .ts, .tsx, .cts, .mts - but you don't use
+ TypeScript you can pass just the extensions you actually use (e.g.
+ [".js", ".jsx"]). This can speed up the most expensive step in
+ dependency cruising (module resolution) quite a bit.
+ */
+ // extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"],
+ /*
+ If your TypeScript project makes use of types specified in 'types'
+ fields in package.jsons of external dependencies, specify "types"
+ in addition to "main" in here, so enhanced-resolve (the resolver
+ dependency-cruiser uses) knows to also look there. You can also do
+ this if you're not sure, but still use TypeScript. In a future version
+ of dependency-cruiser this will likely become the default.
+ */
+ mainFields: ['main', 'types'],
+ },
+ reporterOptions: {
+ dot: {
+ /* pattern of modules that can be consolidated in the detailed
+ graphical dependency graph. The default pattern in this configuration
+ collapses everything in node_modules to one folder deep so you see
+ the external modules, but not the innards your app depends upon.
+ */
+ collapsePattern: 'node_modules/(@[^/]+/[^/]+|[^/]+)',
+
+ /* Options to tweak the appearance of your graph.See
+ https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions
+ for details and some examples. If you don't specify a theme
+ don't worry - dependency-cruiser will fall back to the default one.
+ */
+ // theme: {
+ // graph: {
+ // /* use splines: "ortho" for straight lines. Be aware though
+ // graphviz might take a long time calculating ortho(gonal)
+ // routings.
+ // */
+ // splines: "true"
+ // },
+ // modules: [
+ // {
+ // criteria: { matchesFocus: true },
+ // attributes: {
+ // fillcolor: "lime",
+ // penwidth: 2,
+ // },
+ // },
+ // {
+ // criteria: { matchesFocus: false },
+ // attributes: {
+ // fillcolor: "lightgrey",
+ // },
+ // },
+ // {
+ // criteria: { matchesReaches: true },
+ // attributes: {
+ // fillcolor: "lime",
+ // penwidth: 2,
+ // },
+ // },
+ // {
+ // criteria: { matchesReaches: false },
+ // attributes: {
+ // fillcolor: "lightgrey",
+ // },
+ // },
+ // {
+ // criteria: { source: "^src/model" },
+ // attributes: { fillcolor: "#ccccff" }
+ // },
+ // {
+ // criteria: { source: "^src/view" },
+ // attributes: { fillcolor: "#ccffcc" }
+ // },
+ // ],
+ // dependencies: [
+ // {
+ // criteria: { "rules[0].severity": "error" },
+ // attributes: { fontcolor: "red", color: "red" }
+ // },
+ // {
+ // criteria: { "rules[0].severity": "warn" },
+ // attributes: { fontcolor: "orange", color: "orange" }
+ // },
+ // {
+ // criteria: { "rules[0].severity": "info" },
+ // attributes: { fontcolor: "blue", color: "blue" }
+ // },
+ // {
+ // criteria: { resolved: "^src/model" },
+ // attributes: { color: "#0000ff77" }
+ // },
+ // {
+ // criteria: { resolved: "^src/view" },
+ // attributes: { color: "#00770077" }
+ // }
+ // ]
+ // }
+ },
+ archi: {
+ /* pattern of modules that can be consolidated in the high level
+ graphical dependency graph. If you use the high level graphical
+ dependency graph reporter (`archi`) you probably want to tweak
+ this collapsePattern to your situation.
+ */
+ collapsePattern:
+ '^(packages|src|lib|app|bin|test(s?)|spec(s?))/[^/]+|node_modules/(@[^/]+/[^/]+|[^/]+)',
+
+ /* Options to tweak the appearance of your graph.See
+ https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions
+ for details and some examples. If you don't specify a theme
+ for 'archi' dependency-cruiser will use the one specified in the
+ dot section (see above), if any, and otherwise use the default one.
+ */
+ // theme: {
+ // },
+ },
+ text: {
+ highlightFocused: true,
+ },
+ },
+ },
+};
+// generated: dependency-cruiser@13.0.5 on 2023-07-08T03:48:00.632Z
diff --git a/.eslintrc b/.eslintrc
deleted file mode 100644
index b395b17..0000000
--- a/.eslintrc
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "parser": "@typescript-eslint/parser",
- "extends": ["plugin:@typescript-eslint/recommended"],
- "parserOptions": { "ecmaVersion": "latest", "sourceType": "script" },
- "rules": {
- "@typescript-eslint/no-non-null-assertion": "off",
- "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
- "semi": ["error", "always"],
- "@typescript-eslint/no-empty-interface": 0,
- "@typescript-eslint/ban-types": 0,
- "@typescript-eslint/no-explicit-any": "off"
- }
-}
diff --git a/.npmignore b/.npmignore
index d9da6cb..243080d 100644
--- a/.npmignore
+++ b/.npmignore
@@ -8,6 +8,7 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+.yarn
# Runtime data
pids
*.pid
@@ -80,9 +81,7 @@ typings/
# FuseBox cache
.fusebox/
-# TypeScript build output
-dist
-
+test
# VisualStudio Config file
.vs
diff --git a/.prettierrc b/.prettierrc
deleted file mode 100644
index 78e2e0b..0000000
--- a/.prettierrc
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "semi": true,
- "trailingComma": "all",
- "singleQuote": true,
- "printWidth": 100,
- "tabWidth": 4,
- "arrowParens": "avoid"
-}
diff --git a/dependency-graph.svg b/dependency-graph.svg
new file mode 100644
index 0000000..2ae619b
--- /dev/null
+++ b/dependency-graph.svg
@@ -0,0 +1,1484 @@
+
+
+
+
+
diff --git a/package.json b/package.json
index 204c53c..420041a 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,16 @@
{
"name": "@sern/handler",
- "packageManager": "yarn@3.5.1",
- "version": "2.6.3",
+ "packageManager": "yarn@3.5.0",
+ "version": "3.0.0",
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
- "main": "dist/cjs/index.cjs",
- "module": "dist/esm/index.mjs",
- "types": "dist/index.d.ts",
+ "main": "./dist/index.js",
+ "module": "./dist/cjs/index.cjs",
+ "types": "./dist/index.d.ts",
"exports": {
".": {
- "import": "./dist/esm/index.mjs",
- "require": "./dist/cjs/index.cjs"
+ "import": "./dist/index.mjs",
+ "require": "./dist/index.js",
+ "types": "./dist/index.d.ts"
}
},
"scripts": {
@@ -17,10 +18,13 @@
"clean-modules": "rimraf node_modules/ && npm install",
"lint": "eslint src/**/*.ts",
"format": "eslint src/**/*.ts --fix",
- "build:dev": "tsup && tsup --dts-only --outDir dist",
- "build:prod": "tsup --minify && tsup --dts-only --outDir dist",
- "publish": "npm run build:prod",
- "pretty": "prettier --write ."
+ "build:dev": "tsup --metafile",
+ "build:prod": "tsup --minify",
+ "prepare": "npm run build:prod",
+ "pretty": "prettier --write .",
+ "tdd": "vitest",
+ "test": "vitest --run",
+ "analyze-imports": "npx depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg"
},
"keywords": [
"sern-handler",
@@ -39,16 +43,54 @@
"ts-results-es": "^3.6.0"
},
"devDependencies": {
+ "@faker-js/faker": "^8.0.1",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.59.1",
- "discord.js": "^14.9.0",
+ "dependency-cruiser": "^13.0.5",
+ "discord.js": "14.11.0",
"esbuild": "^0.17.0",
- "esbuild-ifdef": "^0.2.0",
"eslint": "8.39.0",
"prettier": "2.8.8",
"tsup": "^6.7.0",
- "typescript": "5.0.2"
+ "typescript": "5.0.2",
+ "vitest": "latest"
+ },
+ "prettier": {
+ "semi": true,
+ "trailingComma": "all",
+ "singleQuote": true,
+ "printWidth": 100,
+ "tabWidth": 4,
+ "arrowParens": "avoid"
+ },
+ "eslintConfig": {
+ "parser": "@typescript-eslint/parser",
+ "extends": [
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "script"
+ },
+ "rules": {
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "quotes": [
+ 2,
+ "single",
+ {
+ "avoidEscape": true,
+ "allowTemplateLiterals": true
+ }
+ ],
+ "semi": [
+ "error",
+ "always"
+ ],
+ "@typescript-eslint/no-empty-interface": 0,
+ "@typescript-eslint/ban-types": 0,
+ "@typescript-eslint/no-explicit-any": "off"
+ }
},
"repository": {
"type": "git",
diff --git a/src/core/_internal.ts b/src/core/_internal.ts
new file mode 100644
index 0000000..2899afc
--- /dev/null
+++ b/src/core/_internal.ts
@@ -0,0 +1,9 @@
+export * as Id from './id';
+export * from './operators';
+export * from './predicates';
+export * as Files from './module-loading';
+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';
diff --git a/src/core/contracts/emitter.ts b/src/core/contracts/emitter.ts
new file mode 100644
index 0000000..abb3a29
--- /dev/null
+++ b/src/core/contracts/emitter.ts
@@ -0,0 +1,7 @@
+import type { AnyFunction } from '../../types/utility';
+
+export interface Emitter {
+ addListener(eventName: string | symbol, listener: AnyFunction): this;
+ removeListener(eventName: string | symbol, listener: AnyFunction): this;
+ emit(eventName: string | symbol, ...payload: any[]): boolean;
+}
diff --git a/src/core/contracts/error-handling.ts b/src/core/contracts/error-handling.ts
new file mode 100644
index 0000000..79d7fe7
--- /dev/null
+++ b/src/core/contracts/error-handling.ts
@@ -0,0 +1,21 @@
+/**
+ * @since 2.0.0
+ */
+export interface ErrorHandling {
+ /**
+ * Number of times the process should throw an error until crashing and exiting
+ */
+ keepAlive: number;
+
+ /**
+ * @deprecated
+ * Version 4 will remove this method
+ */
+ crash(err: Error): never;
+ /**
+ * A function that is called on every crash. Updates keepAlive.
+ * If keepAlive is 0, the process crashes.
+ * @param error
+ */
+ updateAlive(error: Error): void;
+}
diff --git a/src/core/contracts/index.ts b/src/core/contracts/index.ts
new file mode 100644
index 0000000..f19756e
--- /dev/null
+++ b/src/core/contracts/index.ts
@@ -0,0 +1,6 @@
+export * from './error-handling';
+export * from './logging';
+export * from './module-manager';
+export * from './module-store';
+export * from './init';
+export * from './emitter';
diff --git a/src/core/contracts/init.ts b/src/core/contracts/init.ts
new file mode 100644
index 0000000..eec1e4c
--- /dev/null
+++ b/src/core/contracts/init.ts
@@ -0,0 +1,9 @@
+import type { Awaitable } from '../../types/utility';
+
+/**
+ * Represents an initialization contract.
+ * Let dependencies implement this to initiate some logic.
+ */
+export interface Init {
+ init(): Awaitable;
+}
diff --git a/src/core/contracts/logging.ts b/src/core/contracts/logging.ts
new file mode 100644
index 0000000..9ceb761
--- /dev/null
+++ b/src/core/contracts/logging.ts
@@ -0,0 +1,11 @@
+/**
+ * @since 2.0.0
+ */
+export interface Logging {
+ error(payload: LogPayload): void;
+ warning(payload: LogPayload): void;
+ info(payload: LogPayload): void;
+ debug(payload: LogPayload): void;
+}
+
+export type LogPayload = { message: T };
diff --git a/src/core/contracts/module-manager.ts b/src/core/contracts/module-manager.ts
new file mode 100644
index 0000000..711582e
--- /dev/null
+++ b/src/core/contracts/module-manager.ts
@@ -0,0 +1,22 @@
+import type {
+ CommandMeta,
+ CommandModule,
+ CommandModuleDefs,
+ Module,
+} from '../../types/core-modules';
+import { CommandType } from '../structures';
+
+/**
+ * @since 2.0.0
+ */
+export interface ModuleManager {
+ get(id: string): string | undefined;
+ getMetadata(m: Module): CommandMeta | undefined;
+ setMetadata(m: Module, c: CommandMeta): void;
+ set(id: string, path: string): void;
+ getPublishableCommands(): Promise;
+ getByNameCommandType(
+ name: string,
+ commandType: T,
+ ): Promise | undefined;
+}
diff --git a/src/core/contracts/module-store.ts b/src/core/contracts/module-store.ts
new file mode 100644
index 0000000..b6157b6
--- /dev/null
+++ b/src/core/contracts/module-store.ts
@@ -0,0 +1,9 @@
+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;
+ metadata: WeakMap;
+}
diff --git a/src/handler/plugins/createPlugin.ts b/src/core/create-plugins.ts
similarity index 84%
rename from src/handler/plugins/createPlugin.ts
rename to src/core/create-plugins.ts
index fef8cda..fd0399a 100644
--- a/src/handler/plugins/createPlugin.ts
+++ b/src/core/create-plugins.ts
@@ -1,8 +1,7 @@
-import { CommandType, EventType, PluginType } from '../structures';
-import type { Plugin, PluginResult } from '../../types/plugin';
-import type { CommandArgs, EventArgs } from './args';
+import { CommandType, EventType, PluginType } from './structures';
+import type { Plugin, PluginResult, EventArgs, CommandArgs } from '../types/core-plugin';
import type { ClientEvents } from 'discord.js';
-export const guayin = Symbol('twice<3');
+
export function makePlugin(
type: PluginType,
execute: (...args: any[]) => any,
@@ -10,12 +9,11 @@ export function makePlugin(
return {
type,
execute,
- [guayin]: undefined,
} as Plugin;
}
/**
* @since 2.5.0
- *
+ * @__PURE__
*/
export function EventInitPlugin(
execute: (...args: EventArgs) => PluginResult,
@@ -24,7 +22,7 @@ export function EventInitPlugin(
}
/**
* @since 2.5.0
- *
+ * @__PURE__
*/
export function CommandInitPlugin(
execute: (...args: CommandArgs) => PluginResult,
@@ -33,7 +31,7 @@ export function CommandInitPlugin(
}
/**
* @since 2.5.0
- *
+ * @__PURE__
*/
export function CommandControlPlugin(
execute: (...args: CommandArgs) => PluginResult,
@@ -42,7 +40,7 @@ export function CommandControlPlugin(
}
/**
* @since 2.5.0
- *
+ * @__PURE__
*/
export function EventControlPlugin(
execute: (...args: EventArgs) => PluginResult,
diff --git a/src/core/functions.ts b/src/core/functions.ts
new file mode 100644
index 0000000..d9b8861
--- /dev/null
+++ b/src/core/functions.ts
@@ -0,0 +1,83 @@
+import { Err, Ok } from 'ts-results-es';
+import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
+import type { SernAutocompleteData, SernOptionsData } from '../types/core-modules';
+import type { AnyCommandPlugin, AnyEventPlugin, Plugin } from '../types/core-plugin';
+import { PluginType } from './structures';
+import assert from 'assert';
+
+//function wrappers for empty ok / err
+export const ok = /* @__PURE__*/ () => Ok.EMPTY;
+export const err = /* @__PURE__*/ () => Err.EMPTY;
+
+export function partitionPlugins(
+ arr: (AnyEventPlugin | AnyCommandPlugin)[] = [],
+): [Plugin[], Plugin[]] {
+ const controlPlugins = [];
+ const initPlugins = [];
+
+ for (const el of arr) {
+ switch (el.type) {
+ case PluginType.Control:
+ controlPlugins.push(el);
+ break;
+ case PluginType.Init:
+ initPlugins.push(el);
+ break;
+ }
+ }
+ return [controlPlugins, initPlugins];
+}
+
+/**
+ * Uses an iterative DFS to check if an autocomplete node exists on the option tree
+ * @param iAutocomplete
+ * @param options
+ */
+export function treeSearch(
+ iAutocomplete: AutocompleteInteraction,
+ options: SernOptionsData[] | undefined,
+): SernAutocompleteData | undefined {
+ if (options === undefined) return undefined;
+ //clone to prevent mutation of original command module
+ const _options = options.map(a => ({ ...a }));
+ let subcommands = new Set();
+ while (_options.length > 0) {
+ const cur = _options.pop()!;
+ switch (cur.type) {
+ case ApplicationCommandOptionType.Subcommand:
+ {
+ subcommands.add(cur.name);
+ for (const option of cur.options ?? []) _options.push(option);
+ }
+ break;
+ case ApplicationCommandOptionType.SubcommandGroup:
+ {
+ for (const command of cur.options ?? []) _options.push(command);
+ }
+ break;
+ default:
+ {
+ if ('autocomplete' in cur && cur.autocomplete) {
+ const choice = iAutocomplete.options.getFocused(true);
+ assert(
+ 'command' in cur,
+ 'No command property found for autocomplete option',
+ );
+ if (subcommands.size > 0) {
+ const parent = iAutocomplete.options.getSubcommand();
+ const parentAndOptionMatches =
+ subcommands.has(parent) && cur.name === choice.name;
+ if (parentAndOptionMatches) {
+ return cur;
+ }
+ } else {
+ if (cur.name === choice.name) {
+ return cur;
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+}
diff --git a/src/core/id.ts b/src/core/id.ts
new file mode 100644
index 0000000..b8975e7
--- /dev/null
+++ b/src/core/id.ts
@@ -0,0 +1,63 @@
+import { ApplicationCommandType, ComponentType, Interaction, InteractionType } from 'discord.js';
+import { CommandType, EventType } from './structures';
+
+/**
+ * Construct unique ID for a given interaction object.
+ * @param event The interaction object for which to create an ID.
+ * @returns A unique string ID based on the type and properties of the interaction object.
+ */
+export function reconstruct(event: T) {
+ switch (event.type) {
+ case InteractionType.MessageComponent: {
+ return `${event.customId}_C${event.componentType}`;
+ }
+ case InteractionType.ApplicationCommand:
+ case InteractionType.ApplicationCommandAutocomplete: {
+ return `${event.commandName}_A${event.commandType}`;
+ }
+ //Modal interactions are classified as components for sern
+ case InteractionType.ModalSubmit: {
+ return `${event.customId}_C1`;
+ }
+ }
+}
+/**
+ *
+ * A magic number to represent any commandtype that is an ApplicationCommand.
+ */
+const appBitField = 0b000000001111;
+
+// Each index represents the exponent of a CommandType.
+// Every CommandType is a power of two.
+export const CommandTypeDiscordApi = [
+ 1, // CommandType.Text
+ ApplicationCommandType.ChatInput,
+ ApplicationCommandType.User,
+ ApplicationCommandType.Message,
+ ComponentType.Button,
+ ComponentType.StringSelect,
+ 1, // CommandType.Modal
+ ComponentType.UserSelect,
+ ComponentType.RoleSelect,
+ ComponentType.MentionableSelect,
+ 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) {
+ if (t === CommandType.Both || t === CommandType.Modal) return 1;
+ return CommandTypeDiscordApi[Math.log2(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
+ */
+export function create(name: string, type: CommandType | EventType) {
+ const am = (appBitField & type) !== 0 ? 'A' : 'C';
+ return name + '_' + am + apiType(type);
+}
diff --git a/src/core/index.ts b/src/core/index.ts
new file mode 100644
index 0000000..ef0881d
--- /dev/null
+++ b/src/core/index.ts
@@ -0,0 +1,4 @@
+export * from './contracts';
+export * from './create-plugins';
+export * from './structures';
+export * from './ioc';
diff --git a/src/core/ioc/base.ts b/src/core/ioc/base.ts
new file mode 100644
index 0000000..efb2564
--- /dev/null
+++ b/src/core/ioc/base.ts
@@ -0,0 +1,34 @@
+import * as assert from 'assert';
+import { composeRoot, useContainer } from './dependency-injection';
+import type { DependencyConfiguration } from '../../types/ioc';
+import { CoreContainer } from './container';
+
+//SIDE EFFECT: GLOBAL DI
+let containerSubject: CoreContainer>;
+
+/**
+ * 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(
+ conf: DependencyConfiguration,
+) {
+ //Until there are more optional dependencies, just check if the logger exists
+ //SIDE EFFECT
+ containerSubject = new CoreContainer();
+ await composeRoot(containerSubject, conf);
+
+ return useContainer();
+}
diff --git a/src/core/ioc/container.ts b/src/core/ioc/container.ts
new file mode 100644
index 0000000..98bbd61
--- /dev/null
+++ b/src/core/ioc/container.ts
@@ -0,0 +1,67 @@
+import { Container } from 'iti';
+import { SernEmitter } from '../';
+import { isAsyncFunction } from 'node:util/types';
+
+import * as assert from 'node:assert';
+import { Subject } from 'rxjs';
+import { DefaultServices, ModuleStore } from '../_internal';
+
+/**
+ * Provides all the defaults for sern to function properly.
+ * The only user provided dependency needs to be @sern/client
+ */
+export class CoreContainer> extends Container {
+ private ready$ = new Subject();
+ private beenCalled = new Set();
+ constructor() {
+ super();
+
+ this.listenForInsertions();
+
+ (this as Container<{}, {}>)
+ .add({
+ '@sern/errors': () => new DefaultServices.DefaultErrorHandling(),
+ '@sern/emitter': () => new SernEmitter(),
+ '@sern/store': () => new ModuleStore(),
+ })
+ .add(ctx => {
+ return {
+ '@sern/modules': () =>
+ new DefaultServices.DefaultModuleManager(ctx['@sern/store']),
+ };
+ });
+ }
+
+ private listenForInsertions() {
+ assert.ok(
+ !this.isReady(),
+ 'listening for init functions should only occur prior to sern being ready.',
+ );
+ const unsubscriber = this.on('containerUpserted', e => this.callInitHooks(e));
+
+ this.ready$.subscribe({
+ complete: unsubscriber,
+ });
+ }
+
+ private async callInitHooks(e: { key: keyof T; newContainer: T[keyof T] | null }) {
+ const dep = e.newContainer;
+
+ assert.ok(dep);
+ //Ignore any dependencies that are not objects or array
+ if (typeof dep !== 'object' || Array.isArray(dep)) {
+ return;
+ }
+ if ('init' in dep && typeof dep.init === 'function' && !this.beenCalled.has(e.key)) {
+ isAsyncFunction(dep.init) ? await dep.init() : dep.init();
+ this.beenCalled.add(e.key);
+ }
+ }
+
+ isReady() {
+ return this.ready$.closed;
+ }
+ ready() {
+ this.ready$.unsubscribe();
+ }
+}
diff --git a/src/core/ioc/dependency-injection.ts b/src/core/ioc/dependency-injection.ts
new file mode 100644
index 0000000..39eb497
--- /dev/null
+++ b/src/core/ioc/dependency-injection.ts
@@ -0,0 +1,90 @@
+import type { CoreDependencies, DependencyConfiguration, IntoDependencies } from '../../types/ioc';
+import { SernError, DefaultServices } from '../_internal';
+import { useContainerRaw } from './base';
+import { CoreContainer } from './container';
+
+/**
+ * @__PURE__
+ * @since 2.0.0.
+ * Creates a singleton object.
+ * @param cb
+ */
+export function single(cb: () => T) {
+ return cb;
+}
+
+/**
+ * @__PURE__
+ * @since 2.0.0
+ * Creates a transient object
+ * @param cb
+ */
+export function transient(cb: () => () => T) {
+ return cb;
+}
+/**
+ * The new Service api, a cleaner alternative to useContainer
+ * To obtain intellisense, ensure a .d.ts file exists in the root of compilation.
+ * Usually our scaffolding tool takes care of this.
+ * @since 3.0.0
+ * @example
+ * ```ts
+ * const client = Service('@sern/client');
+ * ```
+ * @param key a key that corresponds to a dependency registered.
+ *
+ */
+export function Service(key: T) {
+ return useContainerRaw().get(key)!;
+}
+/**
+ * @since 3.0.0
+ * The plural version of {@link Service}
+ * @returns array of dependencies, in the same order of keys provided
+ */
+export function Services(...keys: [...T]) {
+ const container = useContainerRaw();
+ return keys.map(k => container.get(k)!) as IntoDependencies;
+}
+
+/**
+ * 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(
+ container: CoreContainer>,
+ conf: DependencyConfiguration,
+) {
+ //container should have no client or logger yet.
+ const hasLogger = conf.exclude?.has('@sern/logger');
+ if (!hasLogger) {
+ container.upsert({
+ '@sern/logger': () => new DefaultServices.DefaultLogging(),
+ });
+ }
+ //Build the container based on the callback provided by the user
+ conf.build(container as CoreContainer>);
+ try {
+ container.get('@sern/client');
+ } catch {
+ throw new Error(SernError.MissingRequired + ' No client was provided');
+ }
+
+ if (!hasLogger) {
+ container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
+ }
+
+ container.ready();
+}
+
+export function useContainer() {
+ console.warn(`
+ Warning: using a container hook (useContainer) is not recommended.
+ Could lead to many unwanted side effects.
+ Use the new Service(s) api function instead.
+ `);
+ return (...keys: [...V]) =>
+ keys.map(key => useContainerRaw().get(key as keyof Dependencies)) as IntoDependencies;
+}
diff --git a/src/core/ioc/index.ts b/src/core/ioc/index.ts
new file mode 100644
index 0000000..436b54e
--- /dev/null
+++ b/src/core/ioc/index.ts
@@ -0,0 +1,2 @@
+export { useContainerRaw, makeDependencies } from './base';
+export { Service, Services, single, transient } from './dependency-injection';
diff --git a/src/core/module-loading.ts b/src/core/module-loading.ts
new file mode 100644
index 0000000..a84ff69
--- /dev/null
+++ b/src/core/module-loading.ts
@@ -0,0 +1,141 @@
+import { Result } from 'ts-results-es';
+import { type Observable, from, mergeMap, ObservableInput } from 'rxjs';
+import { readdir, stat } from 'fs/promises';
+import { basename, extname, join, resolve } from 'path';
+import assert from 'assert';
+import { createRequire } from 'node:module';
+import type { ImportPayload, Wrapper } from '../types/core';
+import type { Module } from '../types/core-modules';
+
+export type ModuleResult = Promise>;
+
+/**
+ * Import any module based on the absolute path.
+ * This can accept four types of exported modules
+ * commonjs, javascript :
+ * ```js
+ * exports = commandModule({ })
+ *
+ * //or
+ * exports.default = commandModule({ })
+ * ```
+ * esm javascript, typescript, and commonjs typescript
+ * export default commandModule({})
+ */
+export async function importModule(absPath: string) {
+ let module = await import(absPath).then(esm => esm.default);
+
+ assert(
+ module,
+ 'Found no default export for command module at ' +
+ absPath +
+ 'Forgot to ignore with "!"? (!filename.ts)?',
+ );
+ if ('default' in module) {
+ module = module.default;
+ }
+ return Result.wrap(() => module.getInstance()).unwrapOr(module) as T;
+}
+export async function defaultModuleLoader(absPath: string): ModuleResult {
+ let module = await importModule(absPath);
+ assert.ok(
+ module,
+ "Found an undefined module. Forgot to ignore it with a '!' ie (!filename.ts)?",
+ );
+ return { module, absPath };
+}
+
+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
+ * @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
+ * @param commandDir
+ */
+export function buildModuleStream(
+ input: ObservableInput,
+): Observable> {
+ return from(input).pipe(mergeMap(defaultModuleLoader));
+}
+
+export const getFullPathTree = (dir: string, mode: boolean) => readPaths(resolve(dir), mode);
+
+export const filename = (path: string) => fmtFileName(basename(path));
+
+const isSkippable = (filename: string) => {
+ //empty string is for non extension files (directories)
+ const validExtensions = ['.js', '.cjs', '.mts', '.mjs', '.cts', '.ts', ''];
+ return filename[0] === '!' || !validExtensions.includes(extname(filename));
+};
+async function deriveFileInfo(dir: string, file: string) {
+ const fullPath = join(dir, file);
+ return {
+ fullPath,
+ fileStats: await stat(fullPath),
+ base: basename(file),
+ };
+}
+async function* readPaths(dir: string, shouldDebug: boolean): AsyncGenerator {
+ try {
+ const files = await readdir(dir);
+ for (const file of files) {
+ const { fullPath, fileStats, base } = await deriveFileInfo(dir, file);
+ if (fileStats.isDirectory()) {
+ //Todo: refactor so that i dont repeat myself for files (line 71)
+ if (isSkippable(base)) {
+ if (shouldDebug) console.info(`ignored directory: ${fullPath}`);
+ } else {
+ yield* readPaths(fullPath, shouldDebug);
+ }
+ } else {
+ if (isSkippable(base)) {
+ if (shouldDebug) console.info(`ignored: ${fullPath}`);
+ } else {
+ yield 'file:///' + fullPath;
+ }
+ }
+ }
+ } catch (err) {
+ throw err;
+ }
+}
+
+const requir = createRequire(import.meta.url);
+
+export function loadConfig(wrapper: Wrapper | 'file'): Wrapper {
+ if (wrapper === 'file') {
+ console.log('Experimental loading of sern.config.json');
+ const config = requir(resolve('sern.config.json')) as {
+ language: string;
+ defaultPrefix?: string;
+ mode?: 'PROD' | 'DEV';
+ paths: {
+ base: string;
+ commands: string;
+ events?: string;
+ };
+ };
+ const makePath = (dir: keyof typeof config.paths) =>
+ config.language === 'typescript'
+ ? join('dist', config.paths[dir]!)
+ : join(config.paths[dir]!);
+
+ console.log('Loading config: ', config);
+ const commandsPath = makePath('commands');
+
+ console.log('Commands path is set to', commandsPath);
+ let eventsPath: string | undefined;
+ if (config.paths.events) {
+ eventsPath = makePath('events');
+ console.log('Events path is set to', eventsPath);
+ }
+ return {
+ defaultPrefix: config.defaultPrefix,
+ commands: commandsPath,
+ events: eventsPath,
+ mode: config.mode,
+ };
+ }
+ return wrapper;
+}
diff --git a/src/core/modules.ts b/src/core/modules.ts
new file mode 100644
index 0000000..d944f84
--- /dev/null
+++ b/src/core/modules.ts
@@ -0,0 +1,112 @@
+import { ClientEvents } from 'discord.js';
+import { CommandType, EventType, PluginType } from '../core/structures';
+import type {
+ AnyCommandPlugin,
+ AnyEventPlugin,
+ CommandArgs,
+ ControlPlugin,
+ EventArgs,
+ InitPlugin,
+} from '../types/core-plugin';
+import type {
+ CommandModule,
+ EventModule,
+ InputCommand,
+ InputEvent,
+ Module,
+} from '../types/core-modules';
+import { partitionPlugins } from './_internal';
+import type { Awaitable } from '../types/utility';
+
+/**
+ * @since 1.0.0 The wrapper function to define command modules for sern
+ * @param mod
+ */
+export function commandModule(mod: InputCommand): CommandModule {
+ const [onEvent, plugins] = partitionPlugins(mod.plugins);
+ return {
+ ...mod,
+ onEvent,
+ plugins,
+ } as CommandModule;
+}
+/**
+ * @since 1.0.0
+ * The wrapper function to define event modules for sern
+ * @param mod
+ */
+export function eventModule(mod: InputEvent): EventModule {
+ const [onEvent, plugins] = partitionPlugins(mod.plugins);
+ return {
+ ...mod,
+ plugins,
+ onEvent,
+ } as EventModule;
+}
+
+/** Create event modules from discord.js client events,
+ * This is an {@link eventModule} for discord events,
+ * where typings can be very bad.
+ * @Experimental
+ * @param mod
+ */
+export function discordEvent(mod: {
+ name: T;
+ plugins?: AnyEventPlugin[];
+ execute: (...args: ClientEvents[T]) => Awaitable;
+}) {
+ return eventModule({
+ type: EventType.Discord,
+ ...mod,
+ });
+}
+
+function prepareClassPlugins(c: Module) {
+ const [onEvent, initPlugins] = partitionPlugins(c.plugins);
+ c.plugins = initPlugins as InitPlugin[];
+ c.onEvent = onEvent as ControlPlugin[];
+}
+//
+// 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 {
+ 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;
+}
+
+/**
+ * @Experimental
+ * Will be refactored 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/operators.ts b/src/core/operators.ts
new file mode 100644
index 0000000..8030e95
--- /dev/null
+++ b/src/core/operators.ts
@@ -0,0 +1,71 @@
+/**
+ * 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.
+ */
+import {
+ concatMap,
+ defaultIfEmpty,
+ EMPTY,
+ every,
+ fromEvent,
+ map,
+ Observable,
+ of,
+ OperatorFunction,
+ pipe,
+ share,
+} from 'rxjs';
+import { Emitter, ErrorHandling, Logging } from './contracts';
+import util from 'node:util';
+import type { PluginResult, VoidResult } from '../types/core-plugin';
+/**
+ * if {src} is true, mapTo V, else ignore
+ * @param item
+ */
+export function filterMapTo(item: () => V): OperatorFunction {
+ return concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY));
+}
+
+/**
+ * Calls any plugin with {args}.
+ * @param args if an array, its spread and plugin called.
+ */
+export function callPlugin(args: unknown): OperatorFunction<
+ {
+ execute: (...args: unknown[]) => PluginResult;
+ },
+ VoidResult
+> {
+ return concatMap(async plugin => {
+ if (Array.isArray(args)) {
+ return plugin.execute(...args);
+ }
+ return plugin.execute(args);
+ });
+}
+
+export const arrayifySource = map(src => (Array.isArray(src) ? (src as unknown[]) : [src]));
+
+/**
+ * Checks if the stream of results is all ok.
+ */
+export const everyPluginOk: OperatorFunction = pipe(
+ every(result => result.ok),
+ defaultIfEmpty(true),
+);
+
+export const sharedEventStream = (e: Emitter, 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 }));
+ //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
new file mode 100644
index 0000000..a325e7a
--- /dev/null
+++ b/src/core/predicates.ts
@@ -0,0 +1,34 @@
+import type {
+ AnySelectMenuInteraction,
+ AutocompleteInteraction,
+ ButtonInteraction,
+ ChatInputCommandInteraction,
+ MessageContextMenuCommandInteraction,
+ ModalSubmitInteraction,
+ UserContextMenuCommandInteraction,
+} from 'discord.js';
+import { InteractionType } from 'discord.js';
+
+interface InteractionTypable {
+ type: InteractionType;
+}
+//discord.js pls fix ur typings or i will >:(
+type AnyMessageComponentInteraction = AnySelectMenuInteraction | ButtonInteraction;
+type AnyCommandInteraction =
+ | ChatInputCommandInteraction
+ | MessageContextMenuCommandInteraction
+ | UserContextMenuCommandInteraction;
+
+export function isMessageComponent(i: InteractionTypable): i is AnyMessageComponentInteraction {
+ return i.type === InteractionType.MessageComponent;
+}
+export function isCommand(i: InteractionTypable): i is AnyCommandInteraction {
+ return i.type === InteractionType.ApplicationCommand;
+}
+export function isAutocomplete(i: InteractionTypable): i is AutocompleteInteraction {
+ return i.type === InteractionType.ApplicationCommandAutocomplete;
+}
+
+export function isModal(i: InteractionTypable): i is ModalSubmitInteraction {
+ return i.type === InteractionType.ModalSubmit;
+}
diff --git a/src/handler/structures/context.ts b/src/core/structures/context.ts
similarity index 54%
rename from src/handler/structures/context.ts
rename to src/core/structures/context.ts
index 61ad0c3..5333693 100644
--- a/src/handler/structures/context.ts
+++ b/src/core/structures/context.ts
@@ -1,42 +1,33 @@
-import type {
+import {
+ BaseInteraction,
ChatInputCommandInteraction,
Client,
InteractionReplyOptions,
Message,
- Snowflake,
MessageReplyOptions,
+ Snowflake,
User,
} from 'discord.js';
-import { Result as Either, Ok as Left, Err as Right } from 'ts-results-es';
-import type { ReplyOptions } from '../../types/handler';
-import { SernError } from './errors';
+import { CoreContext } from '../structures/core-context';
+import { Result, Ok, Err } from 'ts-results-es';
+import * as assert from 'assert';
+
+type ReplyOptions = string | Omit | MessageReplyOptions;
-function safeUnwrap(res: Either) {
- return res.val;
-}
/**
* @since 1.0.0
* Provides values shared between
* Message and ChatInputCommandInteraction
*/
-export default class Context {
- private constructor(private ctx: Either) {}
-
- /**
- * Getting the Message object. Crashes if module type is
- * CommandType.Slash or the event fired in a Both command was
- * ChatInputCommandInteraction
+export class Context extends CoreContext {
+ /*
+ * @Experimental
*/
- public get message() {
- return this.ctx.expect(SernError.MismatchEvent);
+ get options() {
+ return this.interaction.options;
}
- /**
- * Getting the ChatInputCommandInteraction object. Crashes if module type is
- * CommandType.Text or the event fired in a Both command was
- * Message
- */
- public get interaction() {
- return this.ctx.expectErr(SernError.MismatchEvent);
+ protected constructor(protected ctx: Result) {
+ super(ctx);
}
public get id(): Snowflake {
@@ -65,7 +56,6 @@ export default class Context {
public get guildId() {
return this.ctx.val.guildId;
}
-
/*
* interactions can return APIGuildMember if the guild it is emitted from is not cached
*/
@@ -80,22 +70,8 @@ export default class Context {
public get inGuild(): boolean {
return this.ctx.val.inGuild();
}
- public isMessage() {
- return this.ctx.map(() => true).unwrapOr(false);
- }
- public isSlash() {
- return !this.isMessage();
- }
-
- static wrap(wrappable: ChatInputCommandInteraction | Message): Context {
- if ('token' in wrappable) {
- return new Context(Right(wrappable));
- }
- return new Context(Left(wrappable));
- }
-
- public reply(content: ReplyOptions) {
+ public async reply(content: ReplyOptions) {
return safeUnwrap(
this.ctx
.map(m => m.reply(content as string | MessageReplyOptions))
@@ -104,4 +80,16 @@ export default class Context {
),
);
}
+
+ static override wrap(wrappable: BaseInteraction | Message): Context {
+ if ('interaction' in wrappable) {
+ return new Context(Ok(wrappable));
+ }
+ assert.ok(wrappable.isChatInputCommand());
+ return new Context(Err(wrappable));
+ }
+}
+
+function safeUnwrap(res: Result) {
+ return res.val;
}
diff --git a/src/core/structures/core-context.ts b/src/core/structures/core-context.ts
new file mode 100644
index 0000000..08a0e78
--- /dev/null
+++ b/src/core/structures/core-context.ts
@@ -0,0 +1,32 @@
+import { Result as Either } from 'ts-results-es';
+import { SernError } from '../_internal';
+import * as assert from 'node:assert';
+
+/**
+ * @since 3.0.0
+ */
+export abstract class CoreContext {
+ protected constructor(protected ctx: Either) {
+ assert.ok(typeof ctx.val === 'object' && ctx.val != null);
+ }
+ get message(): M {
+ return this.ctx.expect(SernError.MismatchEvent);
+ }
+ get interaction(): I {
+ return this.ctx.expectErr(SernError.MismatchEvent);
+ }
+
+ public isMessage(): this is CoreContext {
+ return this.ctx.ok;
+ }
+
+ public isSlash(): this is CoreContext {
+ return !this.isMessage();
+ }
+ //todo: add agnostic options resolver for Context
+ abstract get options(): unknown;
+
+ static wrap(_: unknown): unknown {
+ throw Error('You need to override this method; cannot wrap an abstract class');
+ }
+}
diff --git a/src/handler/structures/enums.ts b/src/core/structures/enums.ts
similarity index 58%
rename from src/handler/structures/enums.ts
rename to src/core/structures/enums.ts
index f1cfa9b..663785e 100644
--- a/src/handler/structures/enums.ts
+++ b/src/core/structures/enums.ts
@@ -15,45 +15,18 @@
* ```
*/
export enum CommandType {
- /**
- * The CommandType for text commands
- */
- Text = 1,
- /**
- * The CommandType for slash commands
- */
- Slash = 2,
- /**
- * The CommandType for hybrid commands, text and slash
- */
+ Text = 1 << 0,
+ Slash = 1 << 1,
Both = 3,
- /**
- * The CommandType for UserContextMenuInteraction commands
- */
- CtxUser = 4,
- /**
- * The CommandType for MessageContextMenuInteraction commands
- */
- CtxMsg = 8,
- /**
- * The CommandType for ButtonInteraction commands
- */
- Button = 16,
- /**
- * The CommandType for StringSelectMenuInteraction commands
- */
- StringSelect = 32,
- /**
- * The CommandType for ModalSubmitInteraction commands
- */
- Modal = 64,
- /**
- * The CommandType for the other SelectMenuInteractions
- */
- ChannelSelect = 256,
- MentionableSelect = 512,
- RoleSelect = 1024,
- UserSelect = 2048,
+ CtxUser = 1 << 2,
+ CtxMsg = 1 << 3,
+ Button = 1 << 4,
+ StringSelect = 1 << 5,
+ Modal = 1 << 6,
+ UserSelect = 1 << 7,
+ RoleSelect = 1 << 8,
+ MentionableSelect = 1 << 9,
+ ChannelSelect = 1 << 10,
}
/**
@@ -106,16 +79,6 @@ export enum PluginType {
* The PluginType for InitPlugins
*/
Init = 1,
- /**
- * @deprecated
- * Use PluginType.Init instead
- */
- Command = 1,
- /**
- * @deprecated
- * Use PluginType.Control instead
- */
- Event = 2,
/**
* The PluginType for EventPlugins
*/
@@ -138,3 +101,42 @@ export enum PayloadType {
*/
Warning = 'warning',
}
+
+/**
+ * @enum { string }
+ */
+export const enum SernError {
+ /**
+ * Throws when registering an invalid module.
+ * This means it is undefined or an invalid command type was provided
+ */
+ InvalidModuleType = 'Detected an unknown module type',
+ /**
+ * Attempted to lookup module in command module store. Nothing was found!
+ */
+ UndefinedModule = `A module could not be detected`,
+ /**
+ * Attempted to lookup module in command module store. Nothing was found!
+ */
+ MismatchModule = `A module type mismatched with event emitted!`,
+ /**
+ * Unsupported interaction at this moment.
+ */
+ NotSupportedInteraction = `This interaction is not supported.`,
+ /**
+ * One plugin called `controller.stop()` (end command execution / loading)
+ */
+ PluginFailure = `A plugin failed to call controller.next()`,
+ /**
+ * A crash that occurs when accessing an invalid property of Context
+ */
+ MismatchEvent = `You cannot use message when an interaction fired or vice versa`,
+ /**
+ * Unsupported feature attempted to access at this time
+ */
+ NotSupportedYet = `This feature is not supported yet`,
+ /**
+ * Required Dependency not found
+ */
+ MissingRequired = `@sern/client is required but was not found`,
+}
diff --git a/src/core/structures/index.ts b/src/core/structures/index.ts
new file mode 100644
index 0000000..e3c08dc
--- /dev/null
+++ b/src/core/structures/index.ts
@@ -0,0 +1,5 @@
+export { CommandType, PluginType, PayloadType, EventType } from './enums';
+export * from './context';
+export * from './sern-emitter';
+export * from './services';
+export * from './module-store';
diff --git a/src/core/structures/module-store.ts b/src/core/structures/module-store.ts
new file mode 100644
index 0000000..6e754e1
--- /dev/null
+++ b/src/core/structures/module-store.ts
@@ -0,0 +1,12 @@
+import { CommandMeta, Module } from '../../types/core-modules';
+import { CoreModuleStore } from '../contracts';
+
+/*
+ * @internal
+ * Version 4.0.0 will internalize this api. Please refrain from using ModuleStore!
+ * For interacting with modules, use the ModuleManager instead.
+ */
+export class ModuleStore implements CoreModuleStore {
+ metadata = new WeakMap();
+ commands = new Map();
+}
diff --git a/src/handler/sernEmitter.ts b/src/core/structures/sern-emitter.ts
similarity index 88%
rename from src/handler/sernEmitter.ts
rename to src/core/structures/sern-emitter.ts
index 82d6077..0b4f188 100644
--- a/src/handler/sernEmitter.ts
+++ b/src/core/structures/sern-emitter.ts
@@ -1,12 +1,15 @@
-import { EventEmitter } from 'events';
-import type { Payload, SernEventsMapping } from '../types/handler';
-import { PayloadType } from './structures';
-import type { Module } from '../types/module';
+import { EventEmitter } from 'node:events';
+import { PayloadType } from '../../core/structures';
+import { Module } from '../../types/core-modules';
+import { SernEventsMapping, Payload } from '../../types/utility';
/**
* @since 1.0.0
*/
-class SernEmitter extends EventEmitter {
+export class SernEmitter extends EventEmitter {
+ constructor() {
+ super({ captureRejections: true });
+ }
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
* @param eventName
@@ -84,5 +87,3 @@ class SernEmitter extends EventEmitter {
);
}
}
-
-export default SernEmitter;
diff --git a/src/core/structures/services/error-handling.ts b/src/core/structures/services/error-handling.ts
new file mode 100644
index 0000000..f5ef972
--- /dev/null
+++ b/src/core/structures/services/error-handling.ts
@@ -0,0 +1,21 @@
+import { ErrorHandling } from '../../contracts';
+
+/**
+ * @internal
+ * @since 2.0.0
+ * Version 4.0.0 will internalize this api. Please refrain from using ModuleStore!
+ */
+export class DefaultErrorHandling implements ErrorHandling {
+ crash(err: Error): never {
+ throw err;
+ }
+
+ keepAlive = 5;
+
+ updateAlive(err: Error) {
+ this.keepAlive--;
+ if (this.keepAlive === 0) {
+ throw err;
+ }
+ }
+}
diff --git a/src/core/structures/services/index.ts b/src/core/structures/services/index.ts
new file mode 100644
index 0000000..3f1d4ab
--- /dev/null
+++ b/src/core/structures/services/index.ts
@@ -0,0 +1,3 @@
+export * from './error-handling';
+export * from './logger';
+export * from './module-manager';
diff --git a/src/handler/contracts/logging.ts b/src/core/structures/services/logger.ts
similarity index 68%
rename from src/handler/contracts/logging.ts
rename to src/core/structures/services/logger.ts
index f431f43..5de19c7 100644
--- a/src/handler/contracts/logging.ts
+++ b/src/core/structures/services/logger.ts
@@ -1,15 +1,9 @@
-import type { LogPayload } from '../../types/handler';
-/**
- * @since 2.0.0
- */
-export interface Logging {
- error(payload: LogPayload): void;
- warning(payload: LogPayload): void;
- info(payload: LogPayload): void;
- debug(payload: LogPayload): void;
-}
+import { LogPayload, Logging } from '../../contracts';
+
/**
+ * @internal
* @since 2.0.0
+ * Version 4.0.0 will internalize this api. Please refrain from using ModuleStore!
*/
export class DefaultLogging implements Logging {
private date = () => new Date();
diff --git a/src/core/structures/services/module-manager.ts b/src/core/structures/services/module-manager.ts
new file mode 100644
index 0000000..74f1aa7
--- /dev/null
+++ b/src/core/structures/services/module-manager.ts
@@ -0,0 +1,50 @@
+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';
+/**
+ * @internal
+ * @since 2.0.0
+ * Version 4.0.0 will internalize this api. Please refrain from using DefaultModuleManager!
+ */
+export class DefaultModuleManager implements ModuleManager {
+ constructor(private moduleStore: CoreModuleStore) {}
+
+ getByNameCommandType(name: string, commandType: T) {
+ const id = this.get(Id.create(name, commandType));
+ if (!id) {
+ return undefined;
+ }
+ return Files.importModule(id);
+ }
+
+ 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 ' + m);
+ }
+ return maybeModule;
+ }
+
+ get(id: string) {
+ return this.moduleStore.commands.get(id);
+ }
+ set(id: string, path: string): void {
+ this.moduleStore.commands.set(id, path);
+ }
+ //not tested
+ getPublishableCommands(): Promise {
+ const entries = this.moduleStore.commands.entries();
+ const publishable = 0b000000110;
+ return Promise.all(
+ Array.from(entries)
+ .filter(([id]) => !(Number.parseInt(id.at(-1)!) & publishable))
+ .map(([, path]) => Files.importModule(path)),
+ );
+ }
+}
diff --git a/src/handler/contracts/errorHandling.ts b/src/handler/contracts/errorHandling.ts
deleted file mode 100644
index 6fc053a..0000000
--- a/src/handler/contracts/errorHandling.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import type { Observable } from 'rxjs';
-import type { Logging } from './logging';
-import util from 'util';
-/**
- * @since 2.0.0
- */
-export interface ErrorHandling {
- /**
- * Number of times the process should throw an error until crashing and exiting
- */
- keepAlive: number;
-
- /**
- * Utility function to crash
- * @param error
- */
- crash(error: Error): never;
-
- /**
- * A function that is called on every crash. Updates keepAlive
- * @param error
- */
- 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--;
- }
-}
-
-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/handler/contracts/index.ts b/src/handler/contracts/index.ts
deleted file mode 100644
index ad878b1..0000000
--- a/src/handler/contracts/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { ErrorHandling, DefaultErrorHandling } from './errorHandling';
-export { Logging, DefaultLogging } from './logging';
-export { ModuleManager, DefaultModuleManager } from './moduleManager';
diff --git a/src/handler/contracts/moduleManager.ts b/src/handler/contracts/moduleManager.ts
deleted file mode 100644
index b7da765..0000000
--- a/src/handler/contracts/moduleManager.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { CommandModuleDefs } from '../../types/module';
-import type { CommandType, ModuleStore } from '../structures';
-import type { Processed } from '../../types/handler';
-/**
- * @since 2.0.0
- */
-export interface ModuleManager {
- get(
- strat: (ms: ModuleStore) => Processed | undefined,
- ): Processed | undefined;
- set(strat: (ms: ModuleStore) => void): void;
-}
-/**
- * @since 2.0.0
- */
-export class DefaultModuleManager implements ModuleManager {
- constructor(private moduleStore: ModuleStore) {}
- get(
- strat: (ms: ModuleStore) => Processed | undefined,
- ) {
- return strat(this.moduleStore);
- }
-
- set(strat: (ms: ModuleStore) => void): void {
- strat(this.moduleStore);
- }
-}
diff --git a/src/handler/dependencies/index.ts b/src/handler/dependencies/index.ts
deleted file mode 100644
index dde2b2b..0000000
--- a/src/handler/dependencies/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { single, transient, many } from './lifetimeFunctions';
-export { useContainerRaw } from './provider';
diff --git a/src/handler/dependencies/lifetimeFunctions.ts b/src/handler/dependencies/lifetimeFunctions.ts
deleted file mode 100644
index 3fd66e6..0000000
--- a/src/handler/dependencies/lifetimeFunctions.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { _const } from '../utilities/functions';
-
-type NotFunction =
- | string
- | number
- | boolean
- | null
- | undefined
- | bigint
- | readonly any[]
- | { apply?: never; [k: string]: any }
- | { call?: never; [k: string]: any };
-
-/**
- * @deprecated
- * @param cb
- */
-export function single(cb: T): () => T;
-/**
- * New signature
- * @since 2.0.0
- * @param cb
- */
-export function single unknown>(cb: T): T;
-/**
- * @__PURE__
- * @since 2.0.0.
- * Please note that on intellij, the deprecation is for all signatures, which is unintended behavior (and
- * very annoying).
- * For future versions, ensure that single is being passed as a **callback!!**
- * @param cb
- */
-export function single(cb: T) {
- if (typeof cb === 'function') return cb;
- return () => cb;
-}
-/**
- * @deprecated
- * @param cb
- * Deprecated signature
- */
-export function transient(cb: T): () => () => T;
-export function transient () => unknown>(cb: T): T;
-/**
- * @__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(cb: (() => () => T) | T) {
- if (typeof cb !== 'function') return () => () => cb;
- return cb;
-}
-
-/**
- * @__PURE__
- * @deprecated
- * @param value
- * Please use the transient function instead
- */
-// prettier-ignore
-export const many = (value: T) => () => _const(value);
diff --git a/src/handler/dependencies/provider.ts b/src/handler/dependencies/provider.ts
deleted file mode 100644
index 7acf3e4..0000000
--- a/src/handler/dependencies/provider.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import type { Container } from 'iti';
-import type { Dependencies, DependencyConfiguration, MapDeps } from '../../types/handler';
-import SernEmitter from '../sernEmitter';
-import { DefaultErrorHandling, DefaultLogging, DefaultModuleManager } from '../contracts';
-import { Result } from 'ts-results-es';
-import { BehaviorSubject } from 'rxjs';
-import { createContainer } from 'iti';
-import { type Wrapper, ModuleStore, SernError } from '../structures';
-
-export const containerSubject = new BehaviorSubject(defaultContainer());
-
-/**
- * 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 function composeRoot(conf: DependencyConfiguration) {
- //Get the current container. This should have no client or possible logger yet.
- const currentContainer = containerSubject.getValue();
- const excludeLogger = conf.exclude?.has('@sern/logger');
- if (!excludeLogger) {
- currentContainer.add({
- '@sern/logger': () => new DefaultLogging(),
- });
- }
- //Build the container based on the callback provided by the user
- const container = conf.build(currentContainer);
- //Check if the built container contains @sern/client or throw
- // a runtime exception
- Result.wrap(() => container.get('@sern/client')).expect(SernError.MissingRequired);
-
- if (!excludeLogger) {
- container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
- }
- //I'm sorry little one
- containerSubject.next(container as any);
-}
-
-export function useContainer() {
- const container = containerSubject.getValue() as Container;
- return (...keys: [...V]) =>
- keys.map(key => Result.wrap(() => container.get(key)).unwrapOr(undefined)) 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
- */
-export function useContainerRaw() {
- return containerSubject.getValue() as Container;
-}
-
-/**
- * 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() })
- .add({ '@sern/store': () => new ModuleStore() })
- .add(ctx => {
- return {
- '@sern/modules': () => new DefaultModuleManager(ctx['@sern/store']),
- };
- })
- .add({ '@sern/emitter': () => new SernEmitter() }) as Container<
- Omit,
- {}
- >;
-}
-
-export function makeFetcher(wrapper: Wrapper) {
- const requiredDependencyKeys = [
- '@sern/emitter',
- '@sern/client',
- '@sern/errors',
- '@sern/logger',
- ] as ['@sern/emitter', '@sern/client', '@sern/errors', '@sern/logger'];
- return (otherKeys: [...Keys]) =>
- wrapper.containerConfig.get(...requiredDependencyKeys, ...otherKeys) as MapDeps<
- Dependencies,
- [...typeof requiredDependencyKeys, ...Keys]
- >;
-}
diff --git a/src/handler/events/dispatchers/dispatchers.ts b/src/handler/events/dispatchers/dispatchers.ts
deleted file mode 100644
index e966c4c..0000000
--- a/src/handler/events/dispatchers/dispatchers.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import type { Processed } from '../../../types/handler';
-import type { AutocompleteInteraction } from 'discord.js';
-import { SernError } from '../../structures';
-import treeSearch from '../../utilities/treeSearch';
-import type { BothCommand, CommandModule, Module, SlashCommand } from '../../../types/module';
-import { EventEmitter } from 'events';
-import * as assert from 'assert';
-import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs';
-import { arrayifySource, callPlugin } from '../operators';
-import { createResultResolver } from '../observableHandling';
-
-export function dispatchCommand(module: Processed, createArgs: () => unknown[]) {
- const args = createArgs();
- return {
- module,
- args,
- };
-}
-
-function intoPayload(module: Processed) {
- return pipe(
- arrayifySource,
- map(args => ({ module, args })),
- );
-}
-
-const createResult = createResultResolver<
- Processed,
- { module: Processed; args: unknown[] },
- unknown[]
->({
- createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)),
- onNext: ({ args }) => args,
-});
-/**
- * Creates an observable from { source }
- * @param module
- * @param source
- */
-export function eventDispatcher(module: Processed, source: unknown) {
- assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`);
-
- const execute: OperatorFunction = concatMap(async args =>
- module.execute(...args),
- );
- return fromEvent(source, module.name).pipe(
- intoPayload(module),
- concatMap(createResult),
- execute,
- );
-}
-
-export function dispatchAutocomplete(
- module: Processed,
- interaction: AutocompleteInteraction,
-) {
- const option = treeSearch(interaction, module.options);
- if (option !== undefined) {
- return {
- module: option.command as Processed, //autocomplete is not a true "module" warning cast!
- args: [interaction],
- };
- }
- throw Error(
- SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`,
- );
-}
diff --git a/src/handler/events/dispatchers/provideArgs.ts b/src/handler/events/dispatchers/provideArgs.ts
deleted file mode 100644
index 33aa91e..0000000
--- a/src/handler/events/dispatchers/provideArgs.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { ChatInputCommandInteraction, Interaction, Message } from 'discord.js';
-import { Context } from '../../structures';
-import type { Args, SlashOptions } from '../../../types/handler';
-
-/**
- * function overloads to create an arguments list for Context
- * @param wrap
- * @param messageArgs
- */
-export function contextArgs(
- wrap: Message,
- messageArgs?: string[],
-): () => [Context, ['text', string[]]];
-export function contextArgs(wrap: Interaction): () => [Context, ['slash', SlashOptions]];
-export function contextArgs(wrap: Interaction | Message, messageArgs?: string[]) {
- const ctx = Context.wrap(wrap as ChatInputCommandInteraction | Message);
- const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.interaction.options];
- return () => [ctx, args] as [Context, Args];
-}
-
-export function interactionArg(interaction: T) {
- return () => [interaction] as [T];
-}
diff --git a/src/handler/events/interactionHandler.ts b/src/handler/events/interactionHandler.ts
deleted file mode 100644
index 1febb38..0000000
--- a/src/handler/events/interactionHandler.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { Interaction } from 'discord.js';
-import {
- catchError,
- concatMap,
- EMPTY,
- filter,
- finalize,
- fromEvent,
- map,
- Observable,
- of,
- OperatorFunction,
- pipe,
-} from 'rxjs';
-import { CommandType, type ModuleStore, SernError } from '../structures';
-import { contextArgs, dispatchAutocomplete, dispatchCommand, interactionArg } from './dispatchers';
-import { executeModule, makeModuleExecutor } from './observableHandling';
-import type { CommandModule } from '../../types/module';
-import { ErrorHandling, handleError } from '../contracts/errorHandling';
-import SernEmitter from '../sernEmitter';
-import type { Processed } from '../../types/handler';
-import { useContainerRaw } from '../dependencies';
-import type { Logging, ModuleManager } from '../contracts';
-import type { EventEmitter } from 'node:events';
-
-function makeInteractionProcessor(
- modules: ModuleManager,
-): OperatorFunction; event: Interaction }> {
- const get = (cb: (ms: ModuleStore) => Processed | undefined) => {
- return modules.get(cb);
- };
- return pipe(
- concatMap(event => {
- if (event.isMessageComponent()) {
- const customId = event.customId;
- const module = get(ms => {
- return ms.InteractionHandlers[event.componentType].get(customId);
- });
- return of({ module, event });
- } else if (event.isCommand() || event.isAutocomplete()) {
- const commandName = event.commandName;
- const module = get(
- ms =>
- /**
- * try to fetch from ApplicationCommands, if nothing, try BothCommands
- * exists on the API but not sern
- */
- ms.ApplicationCommands[event.commandType].get(commandName) ??
- ms.BothCommands.get(commandName),
- );
- return of({ module, event });
- } else if (event.isModalSubmit()) {
- const module = get(ms => ms.ModalSubmit.get(event.customId));
- return of({ module, event });
- } else return EMPTY;
- }),
- filter(m => m.module !== undefined),
- ) as OperatorFunction; event: Interaction }>;
-}
-
-export function makeInteractionCreate([s, client, err, log, modules]: [
- SernEmitter,
- EventEmitter,
- ErrorHandling,
- Logging | undefined,
- ModuleManager,
-]) {
- //map. If nothing again,this means a slash command
- const interactionStream$ = fromEvent(client, 'interactionCreate') as Observable;
- const interactionProcessor = makeInteractionProcessor(modules);
- return interactionStream$
- .pipe(
- interactionProcessor,
- map(createDispatcher),
- makeModuleExecutor(module => {
- s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
- }),
- concatMap(module => executeModule(s, module)),
- catchError(handleError(err, log)),
- finalize(() => {
- log?.info({
- message: 'interactionCreate stream closed or reached end of lifetime',
- });
- useContainerRaw()
- ?.disposeAll()
- .then(() => log?.info({ message: 'Cleaning container and crashing' }));
- }),
- )
- .subscribe();
-}
-
-function createDispatcher({
- module,
- event,
-}: {
- event: Interaction;
- module: Processed;
-}) {
- switch (module.type) {
- case CommandType.Text:
- throw Error(SernError.MismatchEvent);
- case CommandType.Slash:
- case CommandType.Both: {
- if (event.isAutocomplete()) {
- /**
- * Autocomplete is a special case that
- * must be handled separately, since it's
- * too different from regular command modules
- */
- return dispatchAutocomplete(module, event);
- } else {
- return dispatchCommand(module, contextArgs(event));
- }
- }
- default:
- return dispatchCommand(module, interactionArg(event));
- }
-}
diff --git a/src/handler/events/messageHandler.ts b/src/handler/events/messageHandler.ts
deleted file mode 100644
index b76e070..0000000
--- a/src/handler/events/messageHandler.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { catchError, concatMap, EMPTY, finalize, fromEvent, map, Observable, of, pipe } from 'rxjs';
-import { type ModuleStore, SernError } from '../structures';
-import type { Message } from 'discord.js';
-import { executeModule, ignoreNonBot, makeModuleExecutor } from './observableHandling';
-import type { CommandModule, TextCommand } from '../../types/module';
-import { ErrorHandling, handleError } from '../contracts/errorHandling';
-import { contextArgs, dispatchCommand } from './dispatchers';
-import SernEmitter from '../sernEmitter';
-import type { Processed } from '../../types/handler';
-import { useContainerRaw } from '../dependencies';
-import type { Logging, ModuleManager } from '../contracts';
-import type { EventEmitter } from 'node:events';
-
-/**
- * Removes the first character(s) _[depending on prefix length]_ of the message
- * @param msg
- * @param prefix The prefix to remove
- * @returns The message without the prefix
- * @example
- * message.content = '!ping';
- * console.log(fmt(message, '!'));
- * // [ 'ping' ]
- */
-export function fmt(msg: string, prefix: string): string[] {
- return msg.slice(prefix.length).trim().split(/\s+/g);
-}
-
-/**
- * An operator function that processes a message to fetch a command module and prepares context payload.
- * @param defaultPrefix
- * @param get
- */
-const createMessageProcessor = (
- defaultPrefix: string,
- get: (
- cb: (ms: ModuleStore) => Processed | undefined,
- ) => CommandModule | undefined,
-) =>
- pipe(
- ignoreNonBot(defaultPrefix),
- //This concatMap checks if module is undefined, and if it is, do not continue.
- // Synonymous to filterMap, but I haven't thought of a generic implementation for filterMap yet
- concatMap(message => {
- const [prefix, ...rest] = fmt(message.content, defaultPrefix);
- const module = get(ms => ms.TextCommands.get(prefix) ?? ms.BothCommands.get(prefix));
- if (module === undefined) {
- return EMPTY;
- }
- const payload = {
- args: contextArgs(message, rest),
- module,
- };
- return of(payload);
- }),
- map(({ args, module }) => dispatchCommand(module as Processed, args)),
- );
-
-export function makeMessageCreate(
- [s, client, err, log, modules]: [
- SernEmitter,
- EventEmitter,
- ErrorHandling,
- Logging | undefined,
- ModuleManager,
- ],
- defaultPrefix?: string,
-) {
- if (!defaultPrefix) {
- return EMPTY.subscribe();
- }
- const get = (cb: (ms: ModuleStore) => Processed | undefined) => {
- return modules.get(cb);
- };
- const messageStream$ = fromEvent(client, 'messageCreate') as Observable;
- const messageProcessor = createMessageProcessor(defaultPrefix, get);
- return messageStream$
- .pipe(
- messageProcessor,
- makeModuleExecutor(module => {
- s.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
- }),
- concatMap(payload => executeModule(s, payload)),
- catchError(handleError(err, log)),
- finalize(() => {
- log?.info({ message: 'messageCreate stream closed or reached end of lifetime' });
- useContainerRaw()
- ?.disposeAll()
- .then(() => log?.info({ message: 'Cleaning container and crashing' }));
- }),
- )
- .subscribe();
-}
diff --git a/src/handler/events/observableHandling.ts b/src/handler/events/observableHandling.ts
deleted file mode 100644
index 43bc2e3..0000000
--- a/src/handler/events/observableHandling.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import type { Awaitable, Message } from 'discord.js';
-import { concatMap, EMPTY, filter, from, Observable, of, tap, throwError } from 'rxjs';
-import { Result } from 'ts-results-es';
-import type { CommandModule, EventModule, Module } from '../../types/module';
-import SernEmitter from '../sernEmitter';
-import { callPlugin, everyPluginOk, filterMapTo } from './operators';
-import type { ImportPayload, Processed } from '../../types/handler';
-import type { ControlPlugin, VoidResult } from '../../types/plugin';
-
-function hasPrefix(prefix: string, content: string) {
- const prefixInContent = content.slice(0, prefix.length);
- return prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0;
-}
-
-/**
- * Ignores messages from any person / bot except itself
- * @param prefix
- */
-export function ignoreNonBot(prefix: string) {
- const messageFromHumanAndHasPrefix = ({ author, content }: Message) =>
- !author.bot && hasPrefix(prefix, content);
- return filter(messageFromHumanAndHasPrefix);
-}
-
-/**
- * Wraps the task in a Result as a try / catch.
- * if the task is ok, an event is emitted and the stream becomes empty
- * if the task is an error, throw an error down the stream which will be handled by catchError
- * @param emitter reference to SernEmitter that will emit a successful execution of module
- * @param module the module that will be executed with task
- * @param task the deferred execution which will be called
- */
-export function executeModule(
- emitter: SernEmitter,
- {
- module,
- task,
- }: {
- module: Processed;
- task: () => Awaitable;
- },
-) {
- return of(module).pipe(
- //converting the task into a promise so rxjs can resolve the Awaitable properly
- concatMap(() => Result.wrapAsync(async () => task())),
- concatMap(result => {
- if (result.ok) {
- emitter.emit('module.activate', SernEmitter.success(module));
- return EMPTY;
- } else {
- return throwError(() => SernEmitter.failure(module, result.val));
- }
- }),
- );
-}
-
-/**
- * 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 }
- * emit config.onSuccess Observable
- * @param config
- * @returns receiver function for flattening a stream of data
- */
-export function createResultResolver<
- T extends { execute: (...args: any[]) => any; onEvent: ControlPlugin[] },
- Args extends { module: T; [key: string]: unknown },
- Output,
->(config: {
- onStop?: (module: T) => unknown;
- onNext: (args: Args) => Output;
- createStream: (args: Args) => Observable;
-}) {
- return (args: Args) => {
- const task$ = config.createStream(args);
- return task$.pipe(
- tap(result => {
- if (result.err) {
- config.onStop?.(args.module);
- }
- }),
- everyPluginOk,
- filterMapTo(() => config.onNext(args)),
- );
- };
-}
-
-/**
- * Calls a module's init plugins and checks for Err. If so, call { onStop } and
- * ignore the module
- */
-export function callInitPlugins<
- T extends Processed,
- Args extends ImportPayload,
->(config: { onStop?: (module: T) => unknown; onNext: (module: Args) => T }) {
- return concatMap(
- createResultResolver({
- createStream: args => from(args.module.plugins).pipe(callPlugin(args)),
- ...config,
- }),
- );
-}
-
-/**
- * Creates an executable task ( execute the command ) if all control plugins are successful
- * @param onStop emits a failure response to the SernEmitter
- */
-export function makeModuleExecutor<
- M extends Processed,
- Args extends { module: M; args: unknown[] },
->(onStop: (m: M) => unknown) {
- const onNext = ({ args, module }: Args) => ({ task: () => module.execute(...args), module });
- return concatMap(
- createResultResolver({
- onStop,
- createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)),
- onNext,
- }),
- );
-}
diff --git a/src/handler/events/operators/index.ts b/src/handler/events/operators/index.ts
deleted file mode 100644
index 9c03195..0000000
--- a/src/handler/events/operators/index.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * 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
- */
-
-import { concatMap, defaultIfEmpty, EMPTY, every, map, of, OperatorFunction, pipe } from 'rxjs';
-import type { AnyModule } from '../../../types/module';
-import { nameOrFilename } from '../../utilities/functions';
-import type { PluginResult, VoidResult } from '../../../types/plugin';
-import { guayin } from '../../plugins';
-import { controller } from '../../sern';
-import { Result } from 'ts-results-es';
-import { ImportPayload, Processed } from '../../../types/handler';
-/**
- * if {src} is true, mapTo V, else ignore
- * @param item
- */
-export function filterMapTo(item: () => V): OperatorFunction {
- return concatMap(shouldKeep => (shouldKeep ? of(item()) : EMPTY));
-}
-
-/**
- * Calls any plugin with {args}.
- * @param args if an array, its spread and plugin called.
- */
-export function callPlugin(args: unknown): OperatorFunction<
- {
- execute: (...args: unknown[]) => PluginResult;
- },
- VoidResult
-> {
- return concatMap(async plugin => {
- const isNewPlugin = Reflect.has(plugin, guayin);
- if (isNewPlugin) {
- if (Array.isArray(args)) {
- return plugin.execute(...args);
- }
- return plugin.execute(args);
- } else {
- return plugin.execute(args, controller);
- }
- });
-}
-
-export const arrayifySource = map(src => (Array.isArray(src) ? (src as unknown[]) : [src]));
-
-export const fillDefaults = ({ module, absPath }: ImportPayload) => {
- return {
- absPath,
- module: {
- name: nameOrFilename(module?.name, absPath),
- description: module?.description ?? '...',
- ...module,
- },
- };
-};
-
-/**
- * If the current value in Result stream is an error, calls callback.
- * This also extracts the Ok value from Result
- * @param cb
- * @returns Observable<{ module: T; absPath: string }>
- */
-export function errTap(cb: (err: Err) => void): OperatorFunction, Ok> {
- return concatMap(result => {
- if (result.ok) {
- return of(result.val);
- } else {
- cb(result.val as Err);
- return EMPTY;
- }
- });
-}
-
-/**
- * Checks if the stream of results is all ok.
- */
-export const everyPluginOk: OperatorFunction = pipe(
- every(result => result.ok),
- defaultIfEmpty(true),
-);
diff --git a/src/handler/events/readyHandler.ts b/src/handler/events/readyHandler.ts
deleted file mode 100644
index 4e489ab..0000000
--- a/src/handler/events/readyHandler.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { fromEvent, map, pipe, switchMap, take } from 'rxjs';
-import * as Files from '../module-loading/readFile';
-import { callInitPlugins } from './observableHandling';
-import { CommandType, type ModuleStore, SernError } from '../structures';
-import { Result } from 'ts-results-es';
-import { ApplicationCommandType, ComponentType } from 'discord.js';
-import type { CommandModule } from '../../types/module';
-import type { Processed } from '../../types/handler';
-import type { ErrorHandling, Logging, ModuleManager } from '../contracts';
-import { err, ok } from '../utilities/functions';
-import { errTap, fillDefaults } from './operators';
-import SernEmitter from '../sernEmitter';
-import type { EventEmitter } from 'node:events';
-
-function buildCommandModules(commandDir: string, sernEmitter: SernEmitter) {
- return pipe(
- switchMap(() => Files.buildModuleStream(commandDir)),
- errTap(error => {
- sernEmitter.emit('module.register', SernEmitter.failure(undefined, error));
- }),
- map(fillDefaults),
- );
-}
-export function makeReadyEvent(
- [sEmitter, client, errorHandler, , moduleManager]: [
- SernEmitter,
- EventEmitter,
- ErrorHandling,
- Logging | undefined,
- ModuleManager,
- ],
- commandDir: string,
-) {
- const readyOnce$ = fromEvent(client, 'ready').pipe(take(1));
- return readyOnce$
- .pipe(
- buildCommandModules(commandDir, sEmitter),
- callInitPlugins({
- onStop: module => {
- sEmitter.emit(
- 'module.register',
- SernEmitter.failure(module, SernError.PluginFailure),
- );
- },
- onNext: ({ module }) => {
- sEmitter.emit('module.register', SernEmitter.success(module));
- return module;
- },
- }),
- )
- .subscribe(module => {
- const result = registerModule(moduleManager, module as Processed);
- if (result.err) {
- errorHandler.crash(Error(SernError.InvalidModuleType));
- }
- });
-}
-
-function registerModule>(
- manager: ModuleManager,
- mod: T,
-): Result {
- const name = mod.name;
- const insert = (cb: (ms: ModuleStore) => void) => {
- const set = Result.wrap(() => manager.set(cb));
- return set.ok ? ok() : err();
- };
- switch (mod.type) {
- case CommandType.Text: {
- mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
- return insert(ms => ms.TextCommands.set(name, mod));
- }
- case CommandType.Slash:
- return insert(ms =>
- ms.ApplicationCommands[ApplicationCommandType.ChatInput].set(name, mod),
- );
- case CommandType.Both: {
- mod.alias?.forEach(a => insert(ms => ms.TextCommands.set(a, mod)));
- return insert(ms => ms.BothCommands.set(name, mod));
- }
- case CommandType.CtxUser:
- return insert(ms => ms.ApplicationCommands[ApplicationCommandType.User].set(name, mod));
- case CommandType.CtxMsg:
- return insert(ms =>
- ms.ApplicationCommands[ApplicationCommandType.Message].set(name, mod),
- );
- case CommandType.Button:
- return insert(ms => ms.InteractionHandlers[ComponentType.Button].set(name, mod));
- case CommandType.StringSelect:
- return insert(ms => ms.InteractionHandlers[ComponentType.StringSelect].set(name, mod));
- case CommandType.MentionableSelect:
- return insert(ms =>
- ms.InteractionHandlers[ComponentType.MentionableSelect].set(name, mod),
- );
- case CommandType.UserSelect:
- return insert(ms => ms.InteractionHandlers[ComponentType.UserSelect].set(name, mod));
- case CommandType.ChannelSelect:
- return insert(ms => ms.InteractionHandlers[ComponentType.ChannelSelect].set(name, mod));
- case CommandType.RoleSelect:
- return insert(ms => ms.InteractionHandlers[ComponentType.RoleSelect].set(name, mod));
- case CommandType.Modal:
- return insert(ms => ms.ModalSubmit.set(name, mod));
- default:
- return err();
- }
-}
diff --git a/src/handler/events/userDefinedEventsHandling.ts b/src/handler/events/userDefinedEventsHandling.ts
deleted file mode 100644
index 9902bb2..0000000
--- a/src/handler/events/userDefinedEventsHandling.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { catchError, finalize, map, mergeAll } from 'rxjs';
-import * as Files from '../module-loading/readFile';
-import type { Dependencies, Processed } from '../../types/handler';
-import { callInitPlugins } from './observableHandling';
-import type { CommandModule, EventModule } from '../../types/module';
-import type { EventEmitter } from 'events';
-import SernEmitter from '../sernEmitter';
-import type { ErrorHandling, Logging } from '../contracts';
-import { SernError, EventType, type Wrapper } from '../structures';
-import { eventDispatcher } from './dispatchers';
-import { handleError } from '../contracts/errorHandling';
-import { errTap, fillDefaults } from './operators';
-import { useContainerRaw } from '../dependencies';
-
-export function makeEventsHandler(
- [s, client, err, log]: [SernEmitter, EventEmitter, ErrorHandling, Logging | undefined],
- eventsPath: string,
- containerGetter: Wrapper['containerConfig'],
-) {
- const lazy = (k: string) => containerGetter.get(k as keyof Dependencies)[0];
- const eventStream$ = eventObservable(eventsPath, s);
-
- const eventCreation$ = eventStream$.pipe(
- map(fillDefaults),
- callInitPlugins({
- onStop: module =>
- s.emit('module.register', SernEmitter.failure(module, SernError.PluginFailure)),
- onNext: ({ module }) => {
- s.emit('module.register', SernEmitter.success(module));
- return module;
- },
- }),
- );
- const intoDispatcher = (e: Processed) => {
- switch (e.type) {
- case EventType.Sern:
- return eventDispatcher(e, s);
- case EventType.Discord:
- return eventDispatcher(e, client);
- case EventType.External:
- return eventDispatcher(e, lazy(e.emitter));
- default:
- return err.crash(
- Error(SernError.InvalidModuleType + ' while creating event handler'),
- );
- }
- };
- eventCreation$
- .pipe(
- map(intoDispatcher),
- /**
- * Where all events are turned on
- */
- mergeAll(),
- catchError(handleError(err, log)),
- finalize(() => {
- log?.info({ message: 'an event module reached end of lifetime' });
- useContainerRaw()
- ?.disposeAll()
- .then(() => {
- log?.info({ message: 'Cleaning container and crashing' });
- });
- }),
- )
- .subscribe();
-}
-
-function eventObservable(events: string, emitter: SernEmitter) {
- return Files.buildModuleStream(events).pipe(
- errTap(reason => {
- emitter.emit('module.register', SernEmitter.failure(undefined, reason));
- }),
- );
-}
diff --git a/src/handler/module-loading/readFile.ts b/src/handler/module-loading/readFile.ts
deleted file mode 100644
index 80e89bc..0000000
--- a/src/handler/module-loading/readFile.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { readdirSync, statSync } from 'fs';
-import { join } from 'path';
-import { type Observable, from, mergeMap } from 'rxjs';
-import { SernError } from '../structures/errors';
-import { type Result, Err, Ok } from 'ts-results-es';
-import { ImportPayload } from '../../types/handler';
-import { pathToFileURL } from 'node:url';
-
-// Courtesy @Townsy45
-function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
- try {
- const files = readdirSync(dir);
- for (const file of files) {
- if (statSync(dir + '/' + file).isDirectory()) readPath(dir + '/' + file, arrayOfFiles);
- else arrayOfFiles.push(join(dir, '/', file));
- }
- } catch (err) {
- throw err;
- }
-
- return arrayOfFiles;
-}
-export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
-// export const isLazy = (n: string) => n.indexOf(".lazy.", n.length-9) !== -1;
-
-export async function defaultModuleLoader(
- absPath: string,
-): Promise, SernError>> {
- // prettier-ignore
- let module: T | undefined
- /// #if MODE === 'esm'
- = (await import(pathToFileURL(absPath).toString())).default
- /// #elif MODE === 'cjs'
- = require(absPath).default; // eslint-disable-line
- /// #endif
- if (module === undefined) {
- return Err(SernError.UndefinedModule);
- }
- try {
- module = new (module as unknown as new () => T)();
- } catch {}
- return Ok({ module, absPath });
-}
-
-/**
- * a directory string is converted into a stream of modules.
- * starts the stream of modules that sern needs to process on init
- * @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
- * @param commandDir
- */
-export function buildModuleStream(
- commandDir: string,
-): Observable, SernError>> {
- const commands = getCommands(commandDir);
- return from(commands).pipe(mergeMap(defaultModuleLoader));
-}
-
-export function fullPathFrom(dir: string) {
- return join(process.cwd(), dir);
-}
-
-export function getCommands(dir: string): string[] {
- return readPath(fullPathFrom(dir));
-}
diff --git a/src/handler/plugins/index.ts b/src/handler/plugins/index.ts
deleted file mode 100644
index 3f8a662..0000000
--- a/src/handler/plugins/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { EventArgs, InitArgs, CommandArgs } from './args';
-export * from './createPlugin';
diff --git a/src/handler/sern.ts b/src/handler/sern.ts
deleted file mode 100644
index b4ac2d1..0000000
--- a/src/handler/sern.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import type Wrapper from './structures/wrapper';
-import { makeEventsHandler } from './events/userDefinedEventsHandling';
-import { CommandType, EventType, PluginType } from './structures/enums';
-import type { AnyEventPlugin, ControlPlugin, InitPlugin, Plugin } from '../types/plugin';
-import { makeInteractionCreate } from './events/interactionHandler';
-import { makeReadyEvent } from './events/readyHandler';
-import { makeMessageCreate } from './events/messageHandler';
-import type {
- CommandModule,
- CommandModuleDefs,
- EventModule,
- EventModuleDefs,
- InputCommand,
- InputEvent,
-} from '../types/module';
-import type { Dependencies, DependencyConfiguration } from '../types/handler';
-import { composeRoot, makeFetcher, useContainer } from './dependencies/provider';
-import type { Logging } from './contracts';
-import { err, ok, partition } from './utilities/functions';
-import type { Awaitable, ClientEvents } from 'discord.js';
-
-/**
- * @since 1.0.0
- * @param wrapper Options to pass into sern.
- * Function to start the handler up
- * @example
- * ```ts title="src/index.ts"
- * Sern.init({
- * defaultPrefix: '!',
- * commands: 'dist/commands',
- * events: 'dist/events',
- * containerConfig : {
- * get: useContainer
- * }
- * })
- * ```
- */
-export function init(wrapper: Wrapper) {
- const logger = wrapper.containerConfig.get('@sern/logger')[0] as Logging | undefined;
- const requiredDependenciesAnd = makeFetcher(wrapper);
- const startTime = performance.now();
- const { events } = wrapper;
- if (events !== undefined) {
- makeEventsHandler(requiredDependenciesAnd([]), events, wrapper.containerConfig);
- }
- const dependencies = requiredDependenciesAnd(['@sern/modules']);
- makeReadyEvent(dependencies, wrapper.commands);
- makeMessageCreate(dependencies, wrapper.defaultPrefix);
- makeInteractionCreate(dependencies);
- const endTime = performance.now();
- logger?.info({ message: `sern : ${(endTime - startTime).toFixed(2)} ms` });
-}
-
-/**
- * @since 1.0.0
- * The object passed into every plugin to control a command's behavior
- */
-export const controller = {
- next: ok,
- stop: err,
-};
-
-/**
- * @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,
- );
- return {
- ...mod,
- onEvent,
- plugins,
- } as CommandModule;
-}
-/**
- * @since 1.0.0
- * The wrapper function to define event modules for sern
- * @param mod
- */
-export function eventModule(mod: InputEvent): EventModule {
- const [onEvent, plugins] = partition(
- mod.plugins ?? [],
- el => (el as Plugin).type === PluginType.Control,
- );
- return {
- ...mod,
- onEvent,
- plugins,
- } as EventModule;
-}
-
-/**
- * Create event modules from discord.js client events,
- * This is an {@link eventModule} for discord events,
- * where typings can be very bad.
- * @param mod
- */
-export function discordEvent(mod: {
- name: T;
- plugins?: AnyEventPlugin[];
- execute: (...args: ClientEvents[T]) => Awaitable;
-}) {
- return eventModule({ type: EventType.Discord, ...mod });
-}
-/**
- * @since 2.0.0
- * @param conf a configuration for creating your project dependencies
- */
-export function makeDependencies(conf: DependencyConfiguration) {
- //Until there are more optional dependencies, just check if the logger exists
- composeRoot(conf);
- return useContainer();
-}
-
-/**
- * @Experimental
- * Will be refactored / changed in future
- */
-export abstract class CommandExecutable {
- abstract type: Type;
- plugins: InitPlugin[] = [];
- onEvent: ControlPlugin[] = [];
- abstract execute: CommandModuleDefs[Type]['execute'];
-}
-/**
- * @Experimental
- * Will be refactored in future
- */
-export abstract class EventExecutable {
- abstract type: Type;
- plugins: InitPlugin[] = [];
- onEvent: ControlPlugin[] = [];
- abstract execute: EventModuleDefs[Type]['execute'];
-}
diff --git a/src/handler/structures/errors.ts b/src/handler/structures/errors.ts
deleted file mode 100644
index f082485..0000000
--- a/src/handler/structures/errors.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * @enum { string }
- */
-export enum SernError {
- /**
- * Throws when registering an invalid module.
- * This means it is undefined or an invalid command type was provided
- */
- InvalidModuleType = 'Detected an unknown module type',
- /**
- * Attempted to lookup module in command module store. Nothing was found!
- */
- UndefinedModule = `A module could not be detected`,
- /**
- * Attempted to lookup module in command module store. Nothing was found!
- */
- MismatchModule = `A module type mismatched with event emitted!`,
- /**
- * Unsupported interaction at this moment.
- */
- NotSupportedInteraction = `This interaction is not supported.`,
- /**
- * One plugin called `controller.stop()` (end command execution / loading)
- */
- PluginFailure = `A plugin failed to call controller.next()`,
- /**
- * A crash that occurs when accessing an invalid property of Context
- */
- MismatchEvent = `You cannot use message when an interaction fired or vice versa`,
- /**
- * Unsupported feature attempted to access at this time
- */
- NotSupportedYet = `This feature is not supported yet`,
- /**
- * Required Dependency not found
- */
- MissingRequired = `@sern/client is required but was not found`,
-}
diff --git a/src/handler/structures/index.ts b/src/handler/structures/index.ts
deleted file mode 100644
index 917094c..0000000
--- a/src/handler/structures/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import Context from './context';
-import type Wrapper from './wrapper';
-import { ModuleStore } from './moduleStore';
-export * from './errors';
-export * from './enums';
-export { Context, Wrapper, ModuleStore };
diff --git a/src/handler/structures/moduleStore.ts b/src/handler/structures/moduleStore.ts
deleted file mode 100644
index 856c012..0000000
--- a/src/handler/structures/moduleStore.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { CommandModule } from '../../types/module';
-import { ApplicationCommandType, ComponentType } from 'discord.js';
-import type { Processed } from '../../types/handler';
-
-/**
- * @since 2.0.0
- * Storing all command modules
- * This dependency is usually injected into ModuleManager
- */
-export class ModuleStore {
- readonly BothCommands = new Map>();
- readonly ApplicationCommands = {
- [ApplicationCommandType.User]: new Map>(),
- [ApplicationCommandType.Message]: new Map>(),
- [ApplicationCommandType.ChatInput]: new Map>(),
- };
- readonly ModalSubmit = new Map>();
- readonly TextCommands = new Map>();
- readonly InteractionHandlers = {
- [ComponentType.Button]: new Map>(),
- [ComponentType.StringSelect]: new Map>(),
- [ComponentType.ChannelSelect]: new Map>(),
- [ComponentType.MentionableSelect]: new Map>(),
- [ComponentType.RoleSelect]: new Map>(),
- [ComponentType.UserSelect]: new Map>(),
- };
-}
diff --git a/src/handler/structures/wrapper.ts b/src/handler/structures/wrapper.ts
deleted file mode 100644
index 91514a1..0000000
--- a/src/handler/structures/wrapper.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { Dependencies } from '../../types/handler';
-
-/**
- * @since 1.0.0
- * An object to be passed into Sern#init() function.
- * @typedef {object} Wrapper
- */
-interface Wrapper {
- /**
- * @deprecated
- * This will be moved to a new field in 3.0.0
- */
- readonly defaultPrefix?: string;
- readonly commands: string;
- readonly events?: string;
- readonly containerConfig: {
- get: (...keys: (keyof Dependencies)[]) => unknown[];
- };
-}
-export default Wrapper;
diff --git a/src/handler/utilities/functions.ts b/src/handler/utilities/functions.ts
deleted file mode 100644
index ab67f99..0000000
--- a/src/handler/utilities/functions.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as Files from '../module-loading/readFile';
-import { basename } from 'path';
-import { Err, Ok } from 'ts-results-es';
-/**
- * A function that returns whatever value is provided.
- * Warning: this evaluates { @param value }. It does not defer a value.
- * @param value
- * @__PURE__
- */
-// prettier-ignore
-export const _const = (value: T) => () => value;
-/**
- *
- * @param modName
- * @param absPath
- */
-export function nameOrFilename(modName: string | undefined, absPath: string) {
- return modName ?? Files.fmtFileName(basename(absPath));
-}
-
-//function wrappers for empty ok / err
-export const ok = _const(Ok.EMPTY);
-export const err = _const(Err.EMPTY);
-
-export function partition(arr: (T & V)[], condition: (e: T & V) => boolean): [T[], V[]] {
- const t: T[] = [];
- const v: V[] = [];
- for (const el of arr) {
- if (condition(el)) {
- t.push(el as T);
- } else {
- v.push(el as V);
- }
- }
- return [t, v];
-}
diff --git a/src/handler/utilities/treeSearch.ts b/src/handler/utilities/treeSearch.ts
deleted file mode 100644
index 2fe3c5b..0000000
--- a/src/handler/utilities/treeSearch.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
-import type { SernAutocompleteData, SernOptionsData } from '../../types/module';
-import assert from 'assert';
-
-/**
- * Uses an iterative DFS to check if an autocomplete node exists
- * @param iAutocomplete
- * @param options
- */
-export default function treeSearch(
- iAutocomplete: AutocompleteInteraction,
- options: SernOptionsData[] | undefined,
-): SernAutocompleteData | undefined {
- if (options === undefined) return undefined;
- //clone to prevent mutation of original command module
- const _options = options.map(a => ({...a}));
- let subcommands = new Set();
- while (_options.length > 0) {
- const cur = _options.pop()!;
- switch (cur.type) {
- case ApplicationCommandOptionType.Subcommand:
- {
- subcommands.add(cur.name);
- for (const option of cur.options ?? [])
- _options.push(option);
- }
- break;
- case ApplicationCommandOptionType.SubcommandGroup:
- {
- for (const command of cur.options ?? [])
- _options.push(command);
- }
- break;
- default:
- {
- if ('autocomplete' in cur && cur.autocomplete) {
- const choice = iAutocomplete.options.getFocused(true);
- assert('command' in cur, "No command property found for autocomplete option");
- if(subcommands.size > 0) {
- const parent = iAutocomplete.options.getSubcommand();
- const parentAndOptionMatches = subcommands.has(parent) && cur.name === choice.name;
- if (parentAndOptionMatches) {
- return cur;
- }
- } else {
- if(cur.name === choice.name) {
- return cur;
- }
- }
- }
- }
- break;
- }
- }}
diff --git a/src/handler/events/dispatchers/index.ts b/src/handlers/_internal.ts
similarity index 50%
rename from src/handler/events/dispatchers/index.ts
rename to src/handlers/_internal.ts
index b754c3e..4813218 100644
--- a/src/handler/events/dispatchers/index.ts
+++ b/src/handlers/_internal.ts
@@ -1,2 +1,2 @@
export * from './dispatchers';
-export * from './provideArgs';
+export * from './event-utils';
diff --git a/src/handlers/dispatchers.ts b/src/handlers/dispatchers.ts
new file mode 100644
index 0000000..6826cf7
--- /dev/null
+++ b/src/handlers/dispatchers.ts
@@ -0,0 +1,117 @@
+import { EventEmitter } from 'node:events';
+import * as assert from 'node:assert';
+import { concatMap, from, fromEvent, map, OperatorFunction, pipe } from 'rxjs';
+import {
+ arrayifySource,
+ callPlugin,
+ isAutocomplete,
+ treeSearch,
+ SernError,
+} from '../core/_internal';
+import { createResultResolver } from './event-utils';
+import { AutocompleteInteraction, BaseInteraction, Message } from 'discord.js';
+import { CommandType, Context } from '../core';
+import type { Args } from '../types/utility';
+import type { BothCommand, CommandModule, Module, Processed } from '../types/core-modules';
+
+function dispatchInteraction(
+ payload: { module: Processed; event: V },
+ createArgs: (m: typeof payload.event) => unknown[],
+) {
+ return {
+ module: payload.module,
+ args: createArgs(payload.event),
+ };
+}
+//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,
+ };
+}
+
+function dispatchAutocomplete(payload: {
+ module: Processed;
+ event: AutocompleteInteraction;
+}) {
+ const option = treeSearch(payload.event, payload.module.options);
+ assert.ok(
+ option,
+ Error(SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`),
+ );
+ return {
+ module: option.command as Processed, //autocomplete is not a true "module" warning cast!
+ args: [payload.event],
+ };
+}
+
+export function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[]) {
+ const ctx = Context.wrap(wrappable);
+ const args = ctx.isMessage() ? ['text', messageArgs!] : ['slash', ctx.options];
+ return [ctx, args] as [Context, Args];
+}
+
+function interactionArg(interaction: T) {
+ return [interaction] as [T];
+}
+
+function intoPayload(module: Processed) {
+ return pipe(
+ arrayifySource,
+ map(args => ({ module, args })),
+ );
+}
+
+const createResult = createResultResolver<
+ Processed,
+ { module: Processed; args: unknown[] },
+ unknown[]
+>({
+ createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)),
+ onNext: ({ args }) => args,
+});
+/**
+ * Creates an observable from { source }
+ * @param module
+ * @param source
+ */
+export function eventDispatcher(module: Processed, source: unknown) {
+ assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`);
+
+ const execute: OperatorFunction = concatMap(async args =>
+ module.execute(...args),
+ );
+ return fromEvent(source, module.name).pipe(
+ intoPayload(module),
+ concatMap(createResult),
+ execute,
+ );
+}
+
+export function createDispatcher(payload: {
+ module: Processed;
+ event: BaseInteraction;
+}) {
+ assert.ok(
+ CommandType.Text !== payload.module.type,
+ SernError.MismatchEvent + 'Found text command in interaction stream',
+ );
+ switch (payload.module.type) {
+ case CommandType.Slash:
+ case CommandType.Both: {
+ if (isAutocomplete(payload.event)) {
+ /**
+ * 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
+ */
+ return dispatchAutocomplete(payload as never);
+ }
+ return dispatchInteraction(payload, contextArgs);
+ }
+ default:
+ return dispatchInteraction(payload, interactionArg);
+ }
+}
diff --git a/src/handlers/event-utils.ts b/src/handlers/event-utils.ts
new file mode 100644
index 0000000..75935f6
--- /dev/null
+++ b/src/handlers/event-utils.ts
@@ -0,0 +1,237 @@
+import { Interaction, Message } from 'discord.js';
+import {
+ EMPTY,
+ Observable,
+ concatMap,
+ filter,
+ from,
+ of,
+ throwError,
+ tap,
+ MonoTypeOperatorFunction,
+ catchError,
+ finalize,
+} from 'rxjs';
+import {
+ Files,
+ Id,
+ callPlugin,
+ everyPluginOk,
+ filterMapTo,
+ handleError,
+ SernError,
+ VoidResult,
+} from '../core/_internal';
+import { Emitter, ErrorHandling, Logging, ModuleManager, useContainerRaw } from '../core';
+import { contextArgs, createDispatcher, dispatchMessage } from './dispatchers';
+import { ObservableInput, pipe } from 'rxjs';
+import { SernEmitter } from '../core';
+import { Result } from 'ts-results-es';
+import type { Awaitable } from '../types/utility';
+import assert from 'node:assert';
+import type { ControlPlugin } from '../types/core-plugin';
+import type { AnyModule, CommandModule, Module, Processed } from '../types/core-modules';
+import type { ImportPayload } from '../types/core';
+
+function createGenericHandler(
+ source: Observable,
+ makeModule: (event: Narrowed) => Promise