Compare commits

...

20 Commits

Author SHA1 Message Date
jacob
922567ff7b simplify, remove reply option 2023-11-02 15:16:12 -05:00
jacob
17eb701b5f decide not to support error handling for autocomplete 2023-11-02 14:19:43 -05:00
jacob
2b07bcc661 extract & refactor 2023-10-12 12:52:50 -05:00
jacob
a2b7158d24 Merge branch 'main' into feat/errors 2023-10-12 12:35:49 -05:00
Jacob Nguyen
56a74ab32a naive onError handling, not tested 2023-10-04 00:38:18 -05:00
Jacob Nguyen
f2218d02d4 progress on error handling 2023-10-02 22:01:15 -05:00
Jacob Nguyen
e954e2a399 fix merge 2023-10-02 11:07:13 -05:00
jacob
f3ed5354eb add command error builder 2023-09-28 19:42:36 -05:00
Jacob Nguyen
b1263c1c05 update error handling contract and wire more 2023-09-10 23:52:31 -05:00
Jacob Nguyen
0fbabea428 seems to work 2023-09-10 00:36:32 -05:00
Jacob Nguyen
fa66ff7471 wiring 2023-09-09 12:27:07 -05:00
Jacob Nguyen
c7ee2736b8 type alias 2023-09-09 11:39:04 -05:00
Jacob Nguyen
4591471e7e update onError to be record 2023-09-09 11:32:11 -05:00
Jacob Nguyen
374a12bb92 fix error callbacks not being stored 2023-09-09 01:08:56 -05:00
Jacob Nguyen
fc87e99ed0 Update README.md 2023-09-09 01:08:16 -05:00
Jacob Nguyen
cafca503f9 wiring onError callback through module loader and resolver 2023-09-08 23:11:46 -05:00
Jacob Nguyen
59e7927816 Merge branch 'main' into feat/errors 2023-09-08 14:19:21 -05:00
renovate[bot]
a08541a8e7 chore(deps): update actions/checkout digest to f43a0e5 (#329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-04 17:06:17 -05:00
renovate[bot]
8bd5eb4949 chore(deps): lock file maintenance (#293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-04 17:05:33 -05:00
jacob
a4848743c2 progress on better error handling 2023-08-29 18:09:31 -05:00
30 changed files with 546 additions and 1349 deletions

View File

@@ -1,450 +0,0 @@
/** @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

View File

@@ -20,7 +20,7 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: Set up Node.js
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3

View File

@@ -15,6 +15,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@0efb1d1d84fc9633afcdaad14c485cbbc90ef46c # v2

View File

@@ -10,7 +10,7 @@ jobs:
test-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
with:
node-version: 17

View File

@@ -18,7 +18,7 @@ jobs:
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
with:

View File

@@ -18,10 +18,9 @@
- For you. A framework that's tailored to your exact needs.
- Lightweight. Does a lot while being small.
- Latest features. Support for discord.js v14 and all of its interactions.
- Hybrid, customizable and composable commands. Create them just how you like.
- Start quickly. Plug and play or customize to your liking.
- Embraces reactive programming. For consistent and reliable backend.
- Switch and customize how errors are handled, logging, and more.
- works with [bun](https://bun.sh/) and [node](https://nodejs.org/en) out the box!
- Use it with TypeScript or JavaScript. CommonJS and ESM supported.
- Active and growing community, always here to help. [Join us](https://sern.dev/discord)
- Unleash its full potential with a powerful CLI and awesome plugins.

View File

@@ -47,7 +47,6 @@
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.59.1",
"dependency-cruiser": "^13.0.5",
"discord.js": "14.11.0",
"esbuild": "^0.17.0",
"eslint": "8.39.0",

View File

@@ -1,5 +1,9 @@
{
"extends": ["config:base", "helpers:pinGitHubActionDigests", "group:allNonMajor"],
"extends": [
"config:base",
"helpers:pinGitHubActionDigests",
"group:allNonMajor"
],
"major": {
"dependencyDashboardApproval": true,
"reviewers": ["EvolutionX-10", "jacoobes", "Murtatrxx"]

View File

@@ -1,21 +1,26 @@
import type { CommandModule,Processed, EventModule } from "../../types/core-modules";
/**
* @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.
* A function that is called on every throw,
* If and only if the command is not handled properly
* @param error
*/
updateAlive(error: Error): void;
/**
* This callback is called if a module
* handles onError with type 'fail'
*
*/
handleError(error: Error): void;
}

View File

@@ -3,16 +3,26 @@ import type {
CommandModule,
CommandModuleDefs,
Module,
OnError,
} from '../../types/core-modules';
import { CommandType } from '../structures';
/**
* @since 2.0.0
*/
export interface ModuleManager {
get(id: string): string | undefined;
interface MetadataAccess {
getMetadata(m: Module): CommandMeta | undefined;
setMetadata(m: Module, c: CommandMeta): void;
}
interface OnErrorAccess {
getErrorCallback(m: Module): OnError;
setErrorCallback(m: Module, c: NonNullable<OnError>): void;
}
/**
* @since 2.0.0
* @internal - direct access to the module manager will be removed in version 4
*/
export interface ModuleManager extends MetadataAccess, OnErrorAccess {
get(id: string): string | undefined;
set(id: string, path: string): void;
getPublishableCommands(): Promise<CommandModule[]>;
getByNameCommandType<T extends CommandType>(

View File

@@ -1,4 +1,4 @@
import type { CommandMeta, Module } from '../../types/core-modules';
import type { CommandMeta, Module, OnError } from '../../types/core-modules';
/**
* Represents a core module store that stores IDs mapped to file paths.
@@ -6,4 +6,5 @@ import type { CommandMeta, Module } from '../../types/core-modules';
export interface CoreModuleStore {
commands: Map<string, string>;
metadata: WeakMap<Module, CommandMeta>;
onError: WeakMap<Module, NonNullable<OnError>>;
}

View File

@@ -36,7 +36,7 @@ export function partitionPlugins(
export function treeSearch(
iAutocomplete: AutocompleteInteraction,
options: SernOptionsData[] | undefined,
): SernAutocompleteData | undefined {
): SernAutocompleteData & { parent?: string } | undefined {
if (options === undefined) return undefined;
//clone to prevent mutation of original command module
const _options = options.map(a => ({ ...a }));
@@ -68,11 +68,11 @@ export function treeSearch(
const parentAndOptionMatches =
subcommands.has(parent) && cur.name === choice.name;
if (parentAndOptionMatches) {
return cur;
return { ...cur, parent };
}
} else {
if (cur.name === choice.name) {
return cur;
return { ...cur, parent: undefined };
}
}
}

View File

@@ -5,7 +5,7 @@ import { basename, extname, join, resolve, parse } 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';
import type { Module, OnError } from '../types/core-modules';
export type ModuleResult<T> = Promise<ImportPayload<T>>;
@@ -23,21 +23,27 @@ export type ModuleResult<T> = Promise<ImportPayload<T>>;
* export default commandModule({})
*/
export async function importModule<T>(absPath: string) {
let module = await import(absPath).then(esm => esm.default);
let fileModule = await import(absPath);
assert(module, `Found no export for module at ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`);
if ('default' in module) {
module = module.default;
let commandModule = fileModule.default,
onError = fileModule.onError;
assert(commandModule , `Found no export @ ${absPath}. Forgot to ignore with "!"? (!${basename(absPath)})?`);
if ('default' in commandModule ) {
commandModule = commandModule.default;
}
return Result
.wrap(() => module.getInstance())
.unwrapOr(module) as T;
.wrap(() => ({ module: commandModule.getInstance(), onError }))
.unwrapOr({ module: commandModule, onError }) as T;
}
interface FileExtras {
onError : OnError
}
export async function defaultModuleLoader<T extends Module>(absPath: string): ModuleResult<T> {
let module = await importModule<T>(absPath);
let { onError, module } = await importModule<{ module: T } & FileExtras>(absPath);
assert(module, `Found an undefined module: ${absPath}`);
return { module, absPath };
return { module, absPath, onError };
}
export const fmtFileName = (fileName: string) => parse(fileName).name;
@@ -51,7 +57,8 @@ export const fmtFileName = (fileName: string) => parse(fileName).name;
export function buildModuleStream<T extends Module>(
input: ObservableInput<string>,
): Observable<ImportPayload<T>> {
return from(input).pipe(mergeMap(defaultModuleLoader<T>));
return from(input)
.pipe(mergeMap(defaultModuleLoader<T>));
}
export const getFullPathTree = (dir: string) => readPaths(resolve(dir));

View File

@@ -0,0 +1,6 @@
import type { Logging } from "../contracts";
export interface Response {
type: 'fail' | 'handled';
log?: { type: keyof Logging; message: unknown }
}

View File

@@ -11,8 +11,8 @@ import {
import { CoreContext } from '../structures/core-context';
import { Result, Ok, Err } from 'ts-results-es';
import * as assert from 'assert';
import { ReplyOptions } from '../../types/utility';
type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;
/**
* @since 1.0.0
@@ -103,9 +103,9 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
public async reply(content: ReplyOptions) {
return safeUnwrap(
this.ctx
.map(m => m.reply(content as string | MessageReplyOptions))
.map(m => m.reply(content as MessageReplyOptions))
.mapErr(i =>
i.reply(content as string | InteractionReplyOptions).then(() => i.fetchReply()),
i.reply(content as InteractionReplyOptions).then(() => i.fetchReply()),
),
);
}

View File

@@ -3,3 +3,4 @@ export * from './context';
export * from './sern-emitter';
export * from './services';
export * from './module-store';
export * as CommandError from './command-error';

View File

@@ -1,4 +1,4 @@
import { CommandMeta, Module } from '../../types/core-modules';
import { CommandMeta, Module, OnError } from '../../types/core-modules';
import { CoreModuleStore } from '../contracts';
/*
@@ -7,6 +7,7 @@ import { CoreModuleStore } from '../contracts';
* For interacting with modules, use the ModuleManager instead.
*/
export class ModuleStore implements CoreModuleStore {
onError = new WeakMap<Module, NonNullable<OnError>>();
metadata = new WeakMap<Module, CommandMeta>();
commands = new Map<string, string>();
}

View File

@@ -3,18 +3,18 @@ import { ErrorHandling } from '../../contracts';
/**
* @internal
* @since 2.0.0
* Version 4.0.0 will internalize this api. Please refrain from using ModuleStore!
* Version 4.0.0 will internalize this api. Please refrain from using the defaults!
*/
export class DefaultErrorHandling implements ErrorHandling {
crash(err: Error): never {
throw err;
}
keepAlive = 5;
#keepAlive = 5;
updateAlive(err: Error) {
this.keepAlive--;
if (this.keepAlive === 0) {
this.#keepAlive--;
if (this.#keepAlive === 0) {
throw err;
}
}

View File

@@ -1,7 +1,7 @@
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 { CommandMeta, CommandModule, CommandModuleDefs, Module, OnError } from '../../../types/core-modules';
import { CommandType } from '../enums';
/**
* @internal
@@ -11,6 +11,14 @@ import { CommandType } from '../enums';
export class DefaultModuleManager implements ModuleManager {
constructor(private moduleStore: CoreModuleStore) {}
getErrorCallback(m: Module): OnError {
return this.moduleStore.onError.get(m);
}
setErrorCallback(m: Module, c: NonNullable<OnError>): void {
this.moduleStore.onError.set(m, c);
}
getByNameCommandType<T extends CommandType>(name: string, commandType: T) {
const id = this.get(Id.create(name, commandType));
if (!id) {

View File

@@ -9,20 +9,11 @@ import {
SernError,
} from '../core/_internal';
import { createResultResolver } from './event-utils';
import { AutocompleteInteraction, BaseInteraction, Message } from 'discord.js';
import { 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';
import type { AnyFunction, Args } from '../types/utility';
import type { CommandModule, Module, OnError, Processed } from '../types/core-modules';
function dispatchInteraction<T extends CommandModule, V extends BaseInteraction | Message>(
payload: { module: Processed<T>; 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<CommandModule>, args: [Context, Args]) {
return {
@@ -31,21 +22,6 @@ export function dispatchMessage(module: Processed<CommandModule>, args: [Context
};
}
function dispatchAutocomplete(payload: {
module: Processed<BothCommand>;
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<Module>, //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];
@@ -56,16 +32,16 @@ function interactionArg<T extends BaseInteraction>(interaction: T) {
return [interaction] as [T];
}
function intoPayload(module: Processed<Module>) {
function intoPayload(module: Processed<Module>, onError: AnyFunction|undefined) {
return pipe(
arrayifySource,
map(args => ({ module, args })),
map(args => ({ module, args, onError })),
);
}
const createResult = createResultResolver<
Processed<Module>,
{ module: Processed<Module>; args: unknown[] },
{ module: Processed<Module>; args: unknown[], onError: AnyFunction|undefined },
unknown[]
>({
createStream: ({ module, args }) => from(module.onEvent).pipe(callPlugin(args)),
@@ -76,14 +52,14 @@ const createResult = createResultResolver<
* @param module
* @param source
*/
export function eventDispatcher(module: Processed<Module>, source: unknown) {
export function eventDispatcher(module: Processed<Module>, onError: OnError, source: unknown) {
assert.ok(source instanceof EventEmitter, `${source} is not an EventEmitter`);
const execute: OperatorFunction<unknown[], unknown> = concatMap(async args =>
module.execute(...args),
);
return fromEvent(source, module.name).pipe(
intoPayload(module),
intoPayload(module, onError),
concatMap(createResult),
execute,
);
@@ -92,6 +68,7 @@ export function eventDispatcher(module: Processed<Module>, source: unknown) {
export function createDispatcher(payload: {
module: Processed<CommandModule>;
event: BaseInteraction;
onError: OnError
}) {
assert.ok(
CommandType.Text !== payload.module.type,
@@ -101,17 +78,30 @@ export function createDispatcher(payload: {
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);
const option = treeSearch(payload.event, payload.module.options);
assert.ok(
option,
Error(SernError.NotSupportedInteraction + ` There is no autocomplete tag for this option`),
);
const { command } = option;
return {
...payload,
module: command as Processed<Module>, //autocomplete is not a true "module" warning cast!
args: [payload.event],
onError: undefined
};
}
return dispatchInteraction(payload, contextArgs);
return {
args: contextArgs(payload.event),
...payload,
onError: payload.onError
};
}
default:
return dispatchInteraction(payload, interactionArg);
return {
args: interactionArg(payload.event),
...payload,
onError: payload.onError
}
}
}

View File

@@ -23,15 +23,16 @@ import {
VoidResult,
useContainerRaw,
} from '../core/_internal';
import { Emitter, ErrorHandling, Logging, ModuleManager } from '../core';
import { contextArgs, createDispatcher, dispatchMessage } from './dispatchers';
import { CommandError, Emitter, ErrorHandling, Logging, ModuleManager } from '../core';
import { contextArgs, createDispatcher } from './dispatchers';
import { ObservableInput, pipe } from 'rxjs';
import { SernEmitter } from '../core';
import { Err, Ok, Result } from 'ts-results-es';
import type { Awaitable } from '../types/utility';
import type { AnyFunction, Awaitable } from '../types/utility';
import type { ControlPlugin } from '../types/core-plugin';
import type { AnyModule, CommandModule, Module, Processed } from '../types/core-modules';
import type { AnyModule, CommandModule, Module, OnError, Processed } from '../types/core-modules';
import type { ImportPayload } from '../types/core';
import assert from 'node:assert';
function createGenericHandler<Source, Narrowed extends Source, Output>(
source: Observable<Source>,
@@ -78,8 +79,11 @@ export function createInteractionHandler<T extends Interaction>(
return Files
.defaultModuleLoader<Processed<CommandModule>>(fullPath)
.then(payload =>
Ok(createDispatcher({ module: payload.module, event }))
);
Ok(createDispatcher({
module: payload.module,
onError: payload.onError,
event,
})));
},
);
}
@@ -94,13 +98,13 @@ export function createMessageHandler(
const fullPath = mg.get(`${prefix}_A1`);
if(!fullPath) {
return Err('Possibly undefined behavior: could not find a static id to resolve ')
return Err('Possibly undefined behavior: could not find a static id to resolve')
}
return Files
.defaultModuleLoader<Processed<CommandModule>>(fullPath)
.then(payload => {
const args = contextArgs(event, rest);
return Ok(dispatchMessage(payload.module, args));
return Ok({ args, ...payload });
});
});
}
@@ -129,6 +133,13 @@ export function buildModules<T extends AnyModule>(
return Files.buildModuleStream<Processed<T>>(input).pipe(assignDefaults(moduleManager));
}
interface ExecutePayload {
module: Processed<Module>;
task: () => Awaitable<unknown>;
onError: AnyFunction|undefined
args: unknown[]
}
/**
* Wraps the task in a Result as a try / catch.
* if the task is ok, an event is emitted and the stream becomes empty
@@ -139,14 +150,36 @@ export function buildModules<T extends AnyModule>(
*/
export function executeModule(
emitter: Emitter,
logger: Logging|undefined,
errHandler: ErrorHandling,
{
module,
task,
}: {
module: Processed<Module>;
task: () => Awaitable<unknown>;
},
onError,
args
}: ExecutePayload,
) {
const onError$ = (err_msg: unknown) => {
if(!onError) {
return throwError(() => SernEmitter.failure(module, err));
}
//Could be promise
const err = onError(err_msg) as CommandError.Response
if(!err) {
const failure = SernEmitter.failure(module, "onError: returned undefined/null");
return throwError(() => failure);
}
if(err.log) {
const { type, message } = err.log;
logger?.[type]({ message });
};
if(err.type === 'fail') {
} else {
}
return EMPTY;
}
return of(module).pipe(
//converting the task into a promise so rxjs can resolve the Awaitable properly
concatMap(() => Result.wrapAsync(async () => task())),
@@ -154,9 +187,8 @@ export function executeModule(
if (result.isOk()) {
emitter.emit('module.activate', SernEmitter.success(module));
return EMPTY;
} else {
return throwError(() => SernEmitter.failure(module, result.error));
}
}
return onError$(result.error);
}),
);
}
@@ -172,7 +204,7 @@ export function executeModule(
*/
export function createResultResolver<
T extends { execute: (...args: any[]) => any; onEvent: ControlPlugin[] },
Args extends { module: T; [key: string]: unknown },
Args extends { module: T; onError: unknown, [key: string]: unknown },
Output,
>(config: {
onStop?: (module: T) => unknown;
@@ -205,9 +237,9 @@ export function callInitPlugins<T extends Processed<AnyModule>>(sernEmitter: Emi
SernEmitter.failure(module, SernError.PluginFailure),
);
},
onNext: ({ module }) => {
onNext: ({ module, onError }) => {
sernEmitter.emit('module.register', SernEmitter.success(module));
return module;
return { module, onError: onError as OnError };
},
}),
);
@@ -219,16 +251,24 @@ export function callInitPlugins<T extends Processed<AnyModule>>(sernEmitter: Emi
*/
export function makeModuleExecutor<
M extends Processed<Module>,
Args extends { module: M; args: unknown[] },
Args extends {
module: M;
args: unknown[];
onError: AnyFunction|undefined
},
>(onStop: (m: M) => unknown) {
const onNext = ({ args, module }: Args) => ({
const onNext = ({ args, module, onError }: Args) => ({
task: () => module.execute(...args),
module,
onError,
args
});
return concatMap(
createResultResolver({
onStop,
createStream: ({ args, module }) => from(module.onEvent).pipe(callPlugin(args)),
createStream: ({ args, module }) =>
from(module.onEvent)
.pipe(callPlugin(args)),
onNext,
}),
);

View File

@@ -13,7 +13,7 @@ import {
import { createInteractionHandler, executeModule, makeModuleExecutor } from './_internal';
import type { DependencyList } from '../types/ioc';
export function interactionHandler([emitter, , , modules, client]: DependencyList) {
export function interactionHandler([emitter, err, log, modules, client]: DependencyList) {
const interactionStream$ = sharedEventStream<Interaction>(client, 'interactionCreate');
const handle = createInteractionHandler(interactionStream$, modules);
@@ -28,6 +28,6 @@ export function interactionHandler([emitter, , , modules, client]: DependencyLis
filterTap(e => emitter.emit('warning', SernEmitter.warning(e))),
makeModuleExecutor(module =>
emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure))),
concatMap(payload => executeModule(emitter, payload)),
concatMap(payload => executeModule(emitter, log, err, payload)),
);
}

View File

@@ -23,7 +23,7 @@ function hasPrefix(prefix: string, content: string) {
}
export function messageHandler(
[emitter, , log, modules, client]: DependencyList,
[emitter, err, log, modules, client]: DependencyList,
defaultPrefix: string | undefined,
) {
if (!defaultPrefix) {
@@ -42,6 +42,6 @@ export function messageHandler(
makeModuleExecutor(module => {
emitter.emit('module.activate', SernEmitter.failure(module, SernError.PluginFailure));
}),
concatMap(payload => executeModule(emitter, payload)),
concatMap(payload => executeModule(emitter, log, err, payload)),
);
}

View File

@@ -17,10 +17,9 @@ export function startReadyEvent(
return concat(ready$, buildModules<AnyModule>(allPaths, moduleManager))
.pipe(callInitPlugins(sEmitter))
.subscribe(module => {
register(moduleManager, module).expect(
SernError.InvalidModuleType + ' ' + util.inspect(module),
);
.subscribe(({ module }) => {
register(moduleManager, module)
.expect(SernError.InvalidModuleType + ' ' + util.inspect(module));
});
}

View File

@@ -4,21 +4,21 @@ import { SernError } from '../core/_internal';
import { buildModules, callInitPlugins, handleCrash, eventDispatcher } from './_internal';
import { Service } from '../core/ioc';
import type { DependencyList } from '../types/ioc';
import type { CommandModule, EventModule, Processed } from '../types/core-modules';
import type { EventModule, OnError, Processed } from '../types/core-modules';
export function eventsHandler(
[emitter, err, log, moduleManager, client]: DependencyList,
allPaths: ObservableInput<string>,
) {
//code smell
const intoDispatcher = (e: Processed<EventModule | CommandModule>) => {
switch (e.type) {
const intoDispatcher = (e: { module: Processed<EventModule>, onError: OnError }) => {
switch (e.module.type) {
case EventType.Sern:
return eventDispatcher(e, emitter);
return eventDispatcher(e.module, e.onError, emitter);
case EventType.Discord:
return eventDispatcher(e, client);
return eventDispatcher(e.module, e.onError, client);
case EventType.External:
return eventDispatcher(e, Service(e.emitter));
return eventDispatcher(e.module, e.onError, Service(e.module.emitter));
default:
throw Error(SernError.InvalidModuleType + ' while creating event handler');
}
@@ -31,7 +31,6 @@ export function eventsHandler(
* Where all events are turned on
*/
mergeAll(),
handleCrash(err, log),
)
handleCrash(err, log))
.subscribe();
}

View File

@@ -17,7 +17,10 @@ import type {
} from 'discord.js';
import { CommandType, Context, EventType } from '../../src/core';
import { AnyCommandPlugin, AnyEventPlugin, ControlPlugin, InitPlugin } from './core-plugin';
import { Awaitable, Args, SlashOptions, SernEventsMapping } from './utility';
import { Awaitable, Args, SlashOptions, SernEventsMapping, AnyFunction } from './utility';
export type OnError = AnyFunction|undefined
export interface CommandMeta {
fullPath: string;

View File

@@ -1,6 +1,9 @@
import { OnError } from "./core-modules";
export interface ImportPayload<T> {
module: T;
absPath: string;
onError: OnError
[key: string]: unknown;
}

View File

@@ -1,6 +1,6 @@
import { CommandInteractionOptionResolver } from 'discord.js';
import { PayloadType } from '../core';
import { AnyModule } from './core-modules';
import type { CommandInteractionOptionResolver, InteractionReplyOptions, MessageReplyOptions } from 'discord.js';
import type { PayloadType } from '../core';
import type { AnyModule } from './core-modules';
export type Awaitable<T> = PromiseLike<T> | T;
@@ -27,3 +27,7 @@ export type Payload =
| { type: PayloadType.Success; module: AnyModule }
| { type: PayloadType.Failure; module?: AnyModule; reason: string | Error }
| { type: PayloadType.Warning; reason: string };
export type ReplyOptions = string | Omit<InteractionReplyOptions, 'fetchReply'> | MessageReplyOptions;

View File

@@ -3,8 +3,6 @@
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"noImplicitAny": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"moduleResolution": "node",
"skipLibCheck": true,

1114
yarn.lock

File diff suppressed because it is too large Load Diff