Skip to content

Commit

Permalink
feat/abstractiti (#340)
Browse files Browse the repository at this point in the history
* progress on better error handling

* wiring onError callback through module loader and resolver

* fix error callbacks not being stored

* update onError to be record

* type alias

* wiring

* seems to work

* update error handling contract and wire more

* add command error builder

* fix merge

* progress on error handling

* naive onError handling, not tested

* progres

* proress

* progress on abstracting away iti

* seems to work

* fix tests

* better typings

* add doc

* abstracting iti

* remove onerror for this pr

* feat: better way to add dependencies

* fix tests
  • Loading branch information
jacoobes authored Dec 15, 2023
1 parent 89f6bbb commit 77fb00d
Show file tree
Hide file tree
Showing 27 changed files with 410 additions and 1,150 deletions.
450 changes: 0 additions & 450 deletions .dependency-cruiser.js

This file was deleted.

6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@sern/handler",
"packageManager": "[email protected]",
"version": "3.1.1",
"version": "3.2.0",
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand All @@ -15,7 +15,6 @@
},
"scripts": {
"watch": "tsup --watch",
"clean-modules": "rimraf node_modules/ && npm install",
"lint": "eslint src/**/*.ts",
"format": "eslint src/**/*.ts --fix",
"build:dev": "tsup --metafile",
Expand Down Expand Up @@ -47,8 +46,7 @@
"@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",
"discord.js": "^14.11.0",
"esbuild": "^0.17.0",
"eslint": "8.39.0",
"prettier": "2.8.8",
Expand Down
6 changes: 5 additions & 1 deletion renovate.json
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
11 changes: 4 additions & 7 deletions src/core/contracts/error-handling.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
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.
* @param error
*/
updateAlive(error: Error): void;

}
11 changes: 8 additions & 3 deletions src/core/contracts/module-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ import type {
} from '../../types/core-modules';
import { CommandType } from '../structures';

interface MetadataAccess {
getMetadata(m: Module): CommandMeta | undefined;
setMetadata(m: Module, c: CommandMeta): void;
}

/**
* @since 2.0.0
* @internal - direct access to the module manager will be removed in version 4
*/
export interface ModuleManager {
export interface ModuleManager extends MetadataAccess {
get(id: string): string | undefined;
getMetadata(m: Module): CommandMeta | undefined;
setMetadata(m: Module, c: CommandMeta): void;

set(id: string, path: string): void;
getPublishableCommands(): Promise<CommandModule[]>;
getByNameCommandType<T extends CommandType>(
Expand Down
6 changes: 3 additions & 3 deletions src/core/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
Expand Down Expand Up @@ -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 };
}
}
}
Expand Down
86 changes: 77 additions & 9 deletions src/core/ioc/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import * as assert from 'assert';
import { composeRoot, useContainer } from './dependency-injection';
import type { DependencyConfiguration } from '../../types/ioc';
import { CoreContainer } from './container';

import { Result } from 'ts-results-es'
import { DefaultServices } from '../_internal';
import { AnyFunction } from '../../types/utility';
//SIDE EFFECT: GLOBAL DI
let containerSubject: CoreContainer<Partial<Dependencies>>;

Expand All @@ -20,17 +22,83 @@ export function useContainerRaw() {
return containerSubject;
}

/**
* @since 2.0.0
* @param conf a configuration for creating your project dependencies
*/
export async function makeDependencies<const T extends Dependencies>(
conf: DependencyConfiguration,
) {
const dependencyBuilder = (container: any, excluded: string[]) => {
type Insertable =
| ((container: CoreContainer<Dependencies>) => unknown )
| Record<PropertyKey, unknown>
return {
/**
* Insert a dependency into your container.
* Supply the correct key and dependency
*/
add(key: keyof Dependencies, v: Insertable) {
Result
.wrap(() => container.add({ [key]: v}))
.expect("Failed to add " + key);
},
/**
* Exclude any dependencies from being added.
* Warning: this could lead to bad errors if not used correctly
*/
exclude(...keys: (keyof Dependencies)[]) {
keys.forEach(key => excluded.push(key));
},
/**
* @param key the key of the dependency
* @param v The dependency to swap out.
* Swap out a preexisting dependency.
*/
swap(key: keyof Dependencies, v: Insertable) {
Result
.wrap(() => container.upsert({ [key]: v }))
.expect("Failed to update " + key);
},
/**
* @param key the key of the dependency
* @param cleanup Provide cleanup for the dependency at key. First parameter is the dependency itself
* @example
* ```ts
* addDisposer('dbConnection', (dbConnection) => dbConnection.end())
* ```
* Swap out a preexisting dependency.
*/
addDisposer(key: keyof Dependencies, cleanup: AnyFunction) {
Result
.wrap(() => container.addDisposer({ [key] : cleanup }))
.expect("Failed to addDisposer for" + key);
}
};
};

type CallbackBuilder = (c: ReturnType<typeof dependencyBuilder>) => any

type ValidDependencyConfig =
| CallbackBuilder
| DependencyConfiguration;

export const insertLogger = (containerSubject: CoreContainer<any>) => {
containerSubject
.upsert({'@sern/logger': () => new DefaultServices.DefaultLogging});
}
export async function makeDependencies<const T extends Dependencies>
(conf: ValidDependencyConfig) {
//Until there are more optional dependencies, just check if the logger exists
//SIDE EFFECT
containerSubject = new CoreContainer();
await composeRoot(containerSubject, conf);
if(typeof conf === 'function') {
const excluded: string[] = [];
conf(dependencyBuilder(containerSubject, excluded));
if(!excluded.includes('@sern/logger')) {
assert.ok(!containerSubject.getTokens()['@sern/logger'])
insertLogger(containerSubject);
}
containerSubject.ready();
} else {
composeRoot(containerSubject, conf);
}

return useContainer<T>();
}



14 changes: 4 additions & 10 deletions src/core/ioc/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ export class CoreContainer<T extends Partial<Dependencies>> extends Container<T,
.subscribe({ complete: unsubscribe });

(this as Container<{}, {}>)
.add({
'@sern/errors': () => new DefaultServices.DefaultErrorHandling(),
'@sern/emitter': () => new SernEmitter(),
'@sern/store': () => new ModuleStore(),
})
.add({ '@sern/errors': () => new DefaultServices.DefaultErrorHandling(),
'@sern/emitter': () => new SernEmitter(),
'@sern/store': () => new ModuleStore() })
.add(ctx => {
return {
'@sern/modules': () =>
Expand All @@ -34,19 +32,15 @@ export class CoreContainer<T extends Partial<Dependencies>> extends Container<T,
});
}


isReady() {

return this.ready$.closed;
}
override async disposeAll() {

const otherDisposables = Object
.entries(this._context)
.flatMap(([key, value]) =>
'dispose' in value
? [key]
: []);
'dispose' in value ? [key] : []);

for(const key of otherDisposables) {
this.addDisposer({ [key]: (dep: Disposable) => dep.dispose() } as never);
Expand Down
9 changes: 3 additions & 6 deletions src/core/ioc/dependency-injection.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { CoreDependencies, DependencyConfiguration, IntoDependencies } from '../../types/ioc';
import { DefaultServices } from '../_internal';
import { useContainerRaw } from './base';
import { insertLogger, useContainerRaw } from './base';
import { CoreContainer } from './container';

/**
Expand Down Expand Up @@ -53,16 +52,14 @@ export function Services<const T extends (keyof Dependencies)[]>(...keys: [...T]
* Finally, update the containerSubject with the new container state
* @param conf
*/
export async function composeRoot(
export function composeRoot(
container: CoreContainer<Partial<Dependencies>>,
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(),
});
insertLogger(container);
}
//Build the container based on the callback provided by the user
conf.build(container as CoreContainer<Omit<CoreDependencies, '@sern/client'>>);
Expand Down
19 changes: 11 additions & 8 deletions src/core/module-loading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,21 @@ 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;

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() }))
.unwrapOr({ module: commandModule }) as T;
}

export async function defaultModuleLoader<T extends Module>(absPath: string): ModuleResult<T> {
let module = await importModule<T>(absPath);
let { module } = await importModule<{ module: T }>(absPath);
assert(module, `Found an undefined module: ${absPath}`);
return { module, absPath };
}
Expand All @@ -51,7 +53,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));
Expand Down
41 changes: 41 additions & 0 deletions src/core/structures/command-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { ReplyOptions } from "../../types/utility";
import type { Logging } from "../contracts";

export interface Response {
type: 'fail' | 'continue';
body?: ReplyOptions;
log?: { type: keyof Logging; message: unknown }
}

export const of = () => {
const payload = {
type: 'fail',
body: undefined,
log : undefined
} as Record<PropertyKey, unknown>

return {
/**
* @param {'fail' | 'continue'} p a status to determine if the error will
* terminate your application or continue. Warning and
*/
status: (p: 'fail' | 'continue') => {
payload.type = p;
return payload;
},
/**
* @param {keyof Logging} type Determine to log to logger[type].
* @param {T} message the message to log
*
* Log this error with the logger.
*/
log: <T=string>(type: keyof Logging, message: T) => {
payload.log = { type, message };
return payload;
},
reply: (bodyContent: ReplyOptions) => {
payload.body = bodyContent;
return payload;
}
};
}
6 changes: 3 additions & 3 deletions src/core/structures/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()),
),
);
}
Expand Down
1 change: 1 addition & 0 deletions src/core/structures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './context';
export * from './sern-emitter';
export * from './services';
export * from './module-store';
export * as CommandError from './command-error';
Loading

0 comments on commit 77fb00d

Please sign in to comment.