mirror of
https://github.com/sern-handler/handler
synced 2026-06-06 01:16:55 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
395f75bcda |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
||||
open_collective: sern
|
||||
2
.github/workflows/continuous-integration.yml
vendored
2
.github/workflows/continuous-integration.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 # v4
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
commit-message: "style: pretty please"
|
||||
branch: prettier
|
||||
|
||||
4
.github/workflows/dependency-review.yml
vendored
4
.github/workflows/dependency-review.yml
vendored
@@ -15,6 +15,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@0efb1d1d84fc9633afcdaad14c485cbbc90ef46c # v2
|
||||
uses: actions/dependency-review-action@v2
|
||||
|
||||
6
.github/workflows/npm-publish.yml
vendored
6
.github/workflows/npm-publish.yml
vendored
@@ -10,13 +10,13 @@ jobs:
|
||||
test-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
|
||||
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 17
|
||||
- run: yarn --immutable
|
||||
- run: yarn build:prod
|
||||
- uses: JS-DevTools/npm-publish@0f451a94170d1699fd50710966d48fb26194d939 # v1
|
||||
- uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
access: "public"
|
||||
|
||||
2
.github/workflows/release-please.yml
vendored
2
.github/workflows/release-please.yml
vendored
@@ -6,7 +6,7 @@ jobs:
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: google-github-actions/release-please-action@ca6063f4ed81b55db15b8c42d1b6f7925866342d # v3
|
||||
- uses: google-github-actions/release-please-action@v3
|
||||
with:
|
||||
release-type: node
|
||||
package-name: release-please-action
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -18,9 +18,9 @@ jobs:
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -95,5 +95,3 @@ dist
|
||||
.yalc
|
||||
|
||||
yalc.lock
|
||||
|
||||
*.svg
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [3.1.0](https://github.com/sern-handler/handler/compare/v3.0.2...v3.1.0) (2023-09-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add guaranteed `channelId` and `userId` getters to `Context` ([#320](https://github.com/sern-handler/handler/issues/320)) ([50253ca](https://github.com/sern-handler/handler/commit/50253ca322e7d6dbd2313139c0187a1028f71109))
|
||||
* dispose hooks (deprecate useContainerRaw) ([#323](https://github.com/sern-handler/handler/issues/323)) ([26ccd11](https://github.com/sern-handler/handler/commit/26ccd118ff8cbcde94158a4d09fc0df18da9f254))
|
||||
|
||||
## [3.0.2](https://github.com/sern-handler/handler/compare/v3.0.1...v3.0.2) (2023-08-06)
|
||||
|
||||
|
||||
|
||||
15
README.md
15
README.md
@@ -77,14 +77,16 @@ export default commandModule({
|
||||
</details>
|
||||
<details open><summary>index.ts</summary>
|
||||
|
||||
|
||||
```ts
|
||||
```ts
|
||||
import { Client, GatewayIntentBits } from 'discord.js';
|
||||
import { Sern, single } from '@sern/handler';
|
||||
import { Sern, single, type Dependencies } from '@sern/handler';
|
||||
|
||||
//client has been declared previously
|
||||
//Version 3
|
||||
await makeDependencies({
|
||||
|
||||
interface MyDependencies extends Dependencies {
|
||||
'@sern/client': Singleton<Client>;
|
||||
}
|
||||
export const useContainer = Sern.makeDependencies<MyDependencies>({
|
||||
build: root => root
|
||||
.add({ '@sern/client': single(() => client) })
|
||||
});
|
||||
@@ -94,6 +96,9 @@ Sern.init({
|
||||
defaultPrefix: '!', // removing defaultPrefix will shut down text commands
|
||||
commands: 'src/commands',
|
||||
// events: 'src/events' (optional),
|
||||
containerConfig : {
|
||||
get: useContainer
|
||||
}
|
||||
});
|
||||
|
||||
client.login("YOUR_BOT_TOKEN_HERE");
|
||||
|
||||
1484
dependency-graph.svg
Normal file
1484
dependency-graph.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 119 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@sern/handler",
|
||||
"packageManager": "yarn@3.5.0",
|
||||
"version": "3.1.0",
|
||||
"version": "3.0.2",
|
||||
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -19,7 +19,7 @@
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"format": "eslint src/**/*.ts --fix",
|
||||
"build:dev": "tsup --metafile",
|
||||
"build:prod": "tsup ",
|
||||
"build:prod": "tsup --minify",
|
||||
"prepare": "npm run build:prod",
|
||||
"pretty": "prettier --write .",
|
||||
"tdd": "vitest",
|
||||
@@ -40,7 +40,7 @@
|
||||
"dependencies": {
|
||||
"iti": "^0.6.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"ts-results-es": "^4.0.0"
|
||||
"ts-results-es": "^3.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.1",
|
||||
|
||||
@@ -7,4 +7,3 @@ export type { VoidResult } from '../types/core-plugin';
|
||||
export { SernError } from './structures/enums';
|
||||
export { ModuleStore } from './structures/module-store';
|
||||
export * as DefaultServices from './structures/services';
|
||||
export { useContainerRaw } from './ioc/base'
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { Awaitable } from '../../types/utility';
|
||||
|
||||
/**
|
||||
* Represents a Disposable contract.
|
||||
* Let dependencies implement this to dispose and cleanup.
|
||||
*/
|
||||
export interface Disposable {
|
||||
dispose(): Awaitable<unknown>;
|
||||
}
|
||||
@@ -4,4 +4,3 @@ export * from './module-manager';
|
||||
export * from './module-store';
|
||||
export * from './init';
|
||||
export * from './emitter';
|
||||
export * from './disposable'
|
||||
|
||||
@@ -7,10 +7,8 @@ import { CoreContainer } from './container';
|
||||
let containerSubject: CoreContainer<Partial<Dependencies>>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Returns the underlying data structure holding all dependencies.
|
||||
* Exposes methods from iti
|
||||
* Use the Service API. The container should be readonly
|
||||
*/
|
||||
export function useContainerRaw() {
|
||||
assert.ok(
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import { Container } from 'iti';
|
||||
import { Disposable, SernEmitter } from '../';
|
||||
import { SernEmitter } from '../';
|
||||
import { isAsyncFunction } from 'node:util/types';
|
||||
|
||||
import * as assert from 'node:assert';
|
||||
import { Subject } from 'rxjs';
|
||||
import { DefaultServices, ModuleStore } from '../_internal';
|
||||
import * as Hooks from './hooks'
|
||||
|
||||
|
||||
/**
|
||||
* A semi-generic container that provides error handling, emitter, and module store.
|
||||
* For the handler to operate correctly, The only user provided dependency needs to be @sern/client
|
||||
* Provides all the defaults for sern to function properly.
|
||||
* The only user provided dependency needs to be @sern/client
|
||||
*/
|
||||
export class CoreContainer<T extends Partial<Dependencies>> extends Container<T, {}> {
|
||||
private ready$ = new Subject<void>();
|
||||
private ready$ = new Subject<never>();
|
||||
private beenCalled = new Set<PropertyKey>();
|
||||
constructor() {
|
||||
super();
|
||||
assert.ok(!this.isReady(), 'Listening for dispose & init should occur prior to sern being ready.');
|
||||
|
||||
const { unsubscribe } = Hooks.createInitListener(this);
|
||||
this.ready$
|
||||
.subscribe({ complete: unsubscribe });
|
||||
this.listenForInsertions();
|
||||
|
||||
(this as Container<{}, {}>)
|
||||
.add({
|
||||
@@ -34,27 +32,36 @@ export class CoreContainer<T extends Partial<Dependencies>> extends Container<T,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
override async disposeAll() {
|
||||
|
||||
const otherDisposables = Object
|
||||
.entries(this._context)
|
||||
.flatMap(([key, value]) =>
|
||||
'dispose' in value
|
||||
? [key]
|
||||
: []);
|
||||
|
||||
for(const key of otherDisposables) {
|
||||
this.addDisposer({ [key]: (dep: Disposable) => dep.dispose() } as never);
|
||||
}
|
||||
await super.disposeAll()
|
||||
}
|
||||
ready() {
|
||||
this.ready$.complete();
|
||||
this.ready$.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CoreDependencies, DependencyConfiguration, IntoDependencies } from '../../types/ioc';
|
||||
import { DefaultServices } from '../_internal';
|
||||
import { SernError, DefaultServices } from '../_internal';
|
||||
import { useContainerRaw } from './base';
|
||||
import { CoreContainer } from './container';
|
||||
|
||||
@@ -66,7 +66,12 @@ export async function composeRoot(
|
||||
}
|
||||
//Build the container based on the callback provided by the user
|
||||
conf.build(container as CoreContainer<Omit<CoreDependencies, '@sern/client'>>);
|
||||
|
||||
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.' });
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { CoreContainer } from "./container"
|
||||
|
||||
interface HookEvent {
|
||||
key : PropertyKey
|
||||
newContainer: any
|
||||
}
|
||||
type HookName = 'init';
|
||||
|
||||
export const createInitListener = (coreContainer : CoreContainer<any>) => {
|
||||
const initCalled = new Set<PropertyKey>();
|
||||
const hasCallableMethod = createPredicate(initCalled);
|
||||
const unsubscribe = coreContainer.on('containerUpserted', async (event) => {
|
||||
|
||||
if(isNotHookable(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(hasCallableMethod('init', event)) {
|
||||
await event.newContainer?.init();
|
||||
initCalled.add(event.key);
|
||||
}
|
||||
|
||||
});
|
||||
return { unsubscribe };
|
||||
}
|
||||
|
||||
const isNotHookable = (hk: HookEvent) => {
|
||||
return typeof hk.newContainer !== 'object'
|
||||
|| Array.isArray(hk.newContainer)
|
||||
|| hk.newContainer === null;
|
||||
}
|
||||
|
||||
const createPredicate = <T extends HookEvent>(called: Set<PropertyKey>) => {
|
||||
return (hookName: HookName, event: T) => {
|
||||
const hasMethod = Reflect.has(event.newContainer!, hookName);
|
||||
const beenCalledOnce = !called.has(event.key)
|
||||
|
||||
return hasMethod && beenCalledOnce
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export { makeDependencies } from './base';
|
||||
export { useContainerRaw, makeDependencies } from './base';
|
||||
export { Service, Services, single, transient } from './dependency-injection';
|
||||
|
||||
@@ -33,7 +33,6 @@ export async function importModule<T>(absPath: string) {
|
||||
.wrap(() => module.getInstance())
|
||||
.unwrapOr(module) as T;
|
||||
}
|
||||
|
||||
export async function defaultModuleLoader<T extends Module>(absPath: string): ModuleResult<T> {
|
||||
let module = await importModule<T>(absPath);
|
||||
assert(module, `Found an undefined module: ${absPath}`);
|
||||
@@ -54,7 +53,7 @@ export function buildModuleStream<T extends Module>(
|
||||
return from(input).pipe(mergeMap(defaultModuleLoader<T>));
|
||||
}
|
||||
|
||||
export const getFullPathTree = (dir: string) => readPaths(resolve(dir));
|
||||
export const getFullPathTree = (dir: string, mode: boolean) => readPaths(resolve(dir), mode);
|
||||
|
||||
export const filename = (path: string) => fmtFileName(basename(path));
|
||||
|
||||
@@ -71,18 +70,22 @@ async function deriveFileInfo(dir: string, file: string) {
|
||||
base: basename(file),
|
||||
};
|
||||
}
|
||||
async function* readPaths(dir: string): AsyncGenerator<string> {
|
||||
async function* readPaths(dir: string, shouldDebug: boolean): AsyncGenerator<string> {
|
||||
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)) {
|
||||
yield* readPaths(fullPath);
|
||||
if (isSkippable(base)) {
|
||||
if (shouldDebug) console.info(`ignored directory: ${fullPath}`);
|
||||
} else {
|
||||
yield* readPaths(fullPath, shouldDebug);
|
||||
}
|
||||
} else {
|
||||
if (!isSkippable(base)) {
|
||||
if (isSkippable(base)) {
|
||||
if (shouldDebug) console.info(`ignored: ${fullPath}`);
|
||||
} else {
|
||||
yield 'file:///' + fullPath;
|
||||
}
|
||||
}
|
||||
@@ -95,30 +98,38 @@ async function* readPaths(dir: string): AsyncGenerator<string> {
|
||||
const requir = createRequire(import.meta.url);
|
||||
|
||||
export function loadConfig(wrapper: Wrapper | 'file'): Wrapper {
|
||||
if (wrapper !== 'file') {
|
||||
return wrapper;
|
||||
}
|
||||
console.log('Experimental loading of sern.config.json');
|
||||
const config = requir(resolve('sern.config.json'));
|
||||
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]!);
|
||||
|
||||
const makePath = (dir: PropertyKey) =>
|
||||
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);
|
||||
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 {
|
||||
defaultPrefix: config.defaultPrefix,
|
||||
commands: commandsPath,
|
||||
events: eventsPath,
|
||||
};
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export const arrayifySource = map(src => (Array.isArray(src) ? (src as unknown[]
|
||||
* Checks if the stream of results is all ok.
|
||||
*/
|
||||
export const everyPluginOk: OperatorFunction<VoidResult, boolean> = pipe(
|
||||
every(result => result.isOk()),
|
||||
every(result => result.ok),
|
||||
defaultIfEmpty(true),
|
||||
);
|
||||
|
||||
@@ -74,10 +74,10 @@ export function handleError<C>(crashHandler: ErrorHandling, logging?: Logging) {
|
||||
export const filterTap = <K, R>(onErr: (e: R) => void): OperatorFunction<Result<K, R>, K> =>
|
||||
pipe(
|
||||
concatMap(result => {
|
||||
if(result.isOk()) {
|
||||
return of(result.value)
|
||||
if(result.ok) {
|
||||
return of(result.val)
|
||||
}
|
||||
onErr(result.error);
|
||||
onErr(result.val);
|
||||
return EMPTY
|
||||
|
||||
})
|
||||
|
||||
@@ -31,73 +31,44 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
|
||||
}
|
||||
|
||||
public get id(): Snowflake {
|
||||
return safeUnwrap(this.ctx
|
||||
.map(m => m.id)
|
||||
.mapErr(i => i.id));
|
||||
return this.ctx.val.id;
|
||||
}
|
||||
|
||||
public get channel() {
|
||||
return safeUnwrap(this.ctx
|
||||
.map(m => m.channel)
|
||||
.mapErr(i => i.channel));
|
||||
return this.ctx.val.channel;
|
||||
}
|
||||
|
||||
public get channelId(): Snowflake {
|
||||
return safeUnwrap(this.ctx
|
||||
.map(m => m.channelId)
|
||||
.mapErr(i => i.channelId));
|
||||
}
|
||||
|
||||
/**
|
||||
* If context is holding a message, message.author
|
||||
* else, interaction.user
|
||||
*/
|
||||
public get user(): User {
|
||||
return safeUnwrap(this.ctx
|
||||
.map(m => m.author)
|
||||
.mapErr(i => i.user));
|
||||
}
|
||||
|
||||
public get userId(): Snowflake {
|
||||
return this.user.id;
|
||||
return safeUnwrap(this.ctx.map(m => m.author).mapErr(i => i.user));
|
||||
}
|
||||
|
||||
public get createdTimestamp(): number {
|
||||
return safeUnwrap(this.ctx
|
||||
.map(m => m.createdTimestamp)
|
||||
.mapErr(i => i.createdTimestamp));
|
||||
return this.ctx.val.createdTimestamp;
|
||||
}
|
||||
|
||||
public get guild() {
|
||||
return safeUnwrap(this.ctx
|
||||
.map(m => m.guild)
|
||||
.mapErr(i => i.guild));
|
||||
return this.ctx.val.guild;
|
||||
}
|
||||
|
||||
public get guildId() {
|
||||
return safeUnwrap(this.ctx
|
||||
.map(m => m.guildId)
|
||||
.mapErr(i => i.guildId));
|
||||
return this.ctx.val.guildId;
|
||||
}
|
||||
/*
|
||||
* interactions can return APIGuildMember if the guild it is emitted from is not cached
|
||||
*/
|
||||
public get member() {
|
||||
return safeUnwrap(this.ctx
|
||||
.map(m => m.member)
|
||||
.mapErr(i => i.member));
|
||||
return this.ctx.val.member;
|
||||
}
|
||||
|
||||
public get client(): Client {
|
||||
return safeUnwrap(this.ctx
|
||||
.map(m => m.client)
|
||||
.mapErr(i => i.client));
|
||||
return this.ctx.val.client;
|
||||
}
|
||||
|
||||
public get inGuild(): boolean {
|
||||
return safeUnwrap(this.ctx
|
||||
.map(m => m.inGuild())
|
||||
.mapErr(i => i.inGuild()));
|
||||
return this.ctx.val.inGuild();
|
||||
}
|
||||
|
||||
public async reply(content: ReplyOptions) {
|
||||
@@ -120,8 +91,5 @@ export class Context extends CoreContext<Message, ChatInputCommandInteraction> {
|
||||
}
|
||||
|
||||
function safeUnwrap<T>(res: Result<T, T>) {
|
||||
if(res.isOk()) {
|
||||
return res.expect("Tried unwrapping message field: " + res)
|
||||
}
|
||||
return res.expectErr("Tried unwrapping interaction field" + res)
|
||||
return res.val;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as assert from 'node:assert';
|
||||
*/
|
||||
export abstract class CoreContext<M, I> {
|
||||
protected constructor(protected ctx: Either<M, I>) {
|
||||
assert.ok(typeof ctx === 'object' && ctx != null);
|
||||
assert.ok(typeof ctx.val === 'object' && ctx.val != null);
|
||||
}
|
||||
get message(): M {
|
||||
return this.ctx.expect(SernError.MismatchEvent);
|
||||
@@ -17,7 +17,7 @@ export abstract class CoreContext<M, I> {
|
||||
}
|
||||
|
||||
public isMessage(): this is CoreContext<M, never> {
|
||||
return this.ctx.isOk();
|
||||
return this.ctx.ok;
|
||||
}
|
||||
|
||||
public isSlash(): this is CoreContext<never, I> {
|
||||
|
||||
@@ -21,9 +21,8 @@ import {
|
||||
handleError,
|
||||
SernError,
|
||||
VoidResult,
|
||||
useContainerRaw,
|
||||
} from '../core/_internal';
|
||||
import { Emitter, ErrorHandling, Logging, ModuleManager } from '../core';
|
||||
import { Emitter, ErrorHandling, Logging, ModuleManager, useContainerRaw } from '../core';
|
||||
import { contextArgs, createDispatcher, dispatchMessage } from './dispatchers';
|
||||
import { ObservableInput, pipe } from 'rxjs';
|
||||
import { SernEmitter } from '../core';
|
||||
@@ -151,11 +150,11 @@ export function executeModule(
|
||||
//converting the task into a promise so rxjs can resolve the Awaitable properly
|
||||
concatMap(() => Result.wrapAsync(async () => task())),
|
||||
concatMap(result => {
|
||||
if (result.isOk()) {
|
||||
if (result.ok) {
|
||||
emitter.emit('module.activate', SernEmitter.success(module));
|
||||
return EMPTY;
|
||||
} else {
|
||||
return throwError(() => SernEmitter.failure(module, result.error));
|
||||
return throwError(() => SernEmitter.failure(module, result.val));
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -183,7 +182,7 @@ export function createResultResolver<
|
||||
const task$ = config.createStream(args);
|
||||
return task$.pipe(
|
||||
tap(result => {
|
||||
result.isErr() && config.onStop?.(args.module);
|
||||
result.err && config.onStop?.(args.module);
|
||||
}),
|
||||
everyPluginOk,
|
||||
filterMapTo(() => config.onNext(args)),
|
||||
|
||||
@@ -50,7 +50,4 @@ export {
|
||||
CommandExecutable,
|
||||
} from './core/modules';
|
||||
|
||||
export {
|
||||
useContainerRaw
|
||||
} from './core/_internal'
|
||||
export { controller } from './sern';
|
||||
|
||||
13
src/sern.ts
13
src/sern.ts
@@ -27,12 +27,13 @@ export function init(maybeWrapper: Wrapper | 'file') {
|
||||
const dependencies = useDependencies();
|
||||
const logger = dependencies[2],
|
||||
errorHandler = dependencies[1];
|
||||
const mode = isDevMode(wrapper.mode ?? process.env.MODE);
|
||||
|
||||
if (wrapper.events !== undefined) {
|
||||
eventsHandler(dependencies, Files.getFullPathTree(wrapper.events));
|
||||
eventsHandler(dependencies, Files.getFullPathTree(wrapper.events, mode));
|
||||
}
|
||||
//Ready event: load all modules and when finished, time should be taken and logged
|
||||
startReadyEvent(dependencies, Files.getFullPathTree(wrapper.commands)).add(() => {
|
||||
startReadyEvent(dependencies, Files.getFullPathTree(wrapper.commands, mode)).add(() => {
|
||||
const time = ((performance.now() - startTime) / 1000).toFixed(2);
|
||||
dependencies[0].emit('modulesLoaded');
|
||||
logger?.info({
|
||||
@@ -46,6 +47,14 @@ export function init(maybeWrapper: Wrapper | 'file') {
|
||||
merge(messages$, interactions$).pipe(handleCrash(errorHandler, logger)).subscribe();
|
||||
}
|
||||
|
||||
function isDevMode(mode: string | undefined) {
|
||||
console.info(`Detected mode: "${mode}"`);
|
||||
if (mode === undefined) {
|
||||
console.info('No mode found in process.env, assuming DEV');
|
||||
}
|
||||
return mode === 'DEV' || mode == undefined;
|
||||
}
|
||||
|
||||
function useDependencies() {
|
||||
return Services(
|
||||
'@sern/emitter',
|
||||
|
||||
@@ -10,9 +10,8 @@ export interface Wrapper {
|
||||
events?: string;
|
||||
/**
|
||||
* Overload to enable mode in case developer does not use a .env file.
|
||||
* @deprecated - https://github.com/sern-handler/handler/pull/325
|
||||
*/
|
||||
mode?: string
|
||||
mode?: 'DEV' | 'PROD';
|
||||
/*
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { describe, vi, it, expect } from'vitest'
|
||||
import { Context } from '../../src';
|
||||
import { faker } from '@faker-js/faker'
|
||||
describe('Context', () => {
|
||||
// Mocked message and interaction objects for testing
|
||||
const mockMessage = {
|
||||
id: 'messageId',
|
||||
channel: 'channelId',
|
||||
channelId: 'channelId',
|
||||
interaction: {
|
||||
id: faker.string.uuid()
|
||||
},
|
||||
author: { id: 'userId' },
|
||||
createdTimestamp: 1234567890,
|
||||
guild: 'guildId',
|
||||
guildId: 'guildId',
|
||||
member: { id: 'memberId' },
|
||||
client: { id: 'clientId' },
|
||||
inGuild: vi.fn().mockReturnValue(true),
|
||||
reply: vi.fn(),
|
||||
};
|
||||
|
||||
const mockInteraction = {
|
||||
id: 'interactionId',
|
||||
user: { id: 'userId' },
|
||||
channel: 'channelId',
|
||||
channelId: 'channelId',
|
||||
createdTimestamp: 1234567890,
|
||||
guild: 'guildId',
|
||||
guildId: 'guildId',
|
||||
fetchReply: vi.fn().mockResolvedValue({}),
|
||||
member: { id: 'memberId' },
|
||||
client: { id: 'clientId' },
|
||||
isChatInputCommand: vi.fn().mockResolvedValue(true),
|
||||
inGuild: vi.fn().mockReturnValue(true),
|
||||
reply: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
|
||||
it('should create a context from a message', () => {
|
||||
//@ts-ignore
|
||||
const context = Context.wrap(mockMessage);
|
||||
expect(context).toBeDefined();
|
||||
expect(context.id).toBe('messageId');
|
||||
});
|
||||
it('should throw error if accessing interaction as message', () => {
|
||||
//@ts-ignore
|
||||
const context = Context.wrap(mockMessage);
|
||||
expect(context).toBeDefined();
|
||||
expect(() => context.interaction)
|
||||
.toThrowError('You cannot use message when an interaction fired or vice versa');
|
||||
|
||||
})
|
||||
it('should throw error if accessing message as interaction', () => {
|
||||
//@ts-ignore
|
||||
const context = Context.wrap(mockInteraction);
|
||||
expect(context).toBeDefined();
|
||||
expect(() => context.message)
|
||||
.toThrowError('You cannot use message when an interaction fired or vice versa');
|
||||
|
||||
})
|
||||
|
||||
it('should create a context from an interaction', () => {
|
||||
//@ts-ignore
|
||||
const context = Context.wrap(mockInteraction);
|
||||
expect(context).toBeDefined();
|
||||
expect(context.id).toBe('interactionId');
|
||||
});
|
||||
|
||||
it('should reply to a context with a message', async () => {
|
||||
//@ts-ignore
|
||||
const context = Context.wrap(mockMessage);
|
||||
const replyOptions = { content: 'Hello, world!' };
|
||||
await context.reply(replyOptions);
|
||||
expect(mockMessage.reply).toHaveBeenCalledWith(replyOptions);
|
||||
});
|
||||
|
||||
it('should reply to a context with an interaction', async () => {
|
||||
//@ts-ignore
|
||||
const context = Context.wrap(mockInteraction);
|
||||
const replyOptions = { content: 'Hello, world!' };
|
||||
await context.reply(replyOptions);
|
||||
expect(mockInteraction.reply).toHaveBeenCalledWith(replyOptions);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,36 +1,23 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CoreContainer } from '../../src/core/ioc/container';
|
||||
import { CoreDependencies } from '../../src/core/ioc';
|
||||
import { EventEmitter } from 'events';
|
||||
import { DefaultLogging, Disposable, Init, Logging } from '../../src/core';
|
||||
import { CoreDependencies } from '../../src/types/ioc';
|
||||
import { DefaultLogging, Init, Logging } from '../../src/core';
|
||||
|
||||
describe('ioc container', () => {
|
||||
let container: CoreContainer<{}> = new CoreContainer();
|
||||
let dependency: Logging & Init & Disposable;
|
||||
let container: CoreContainer<{}>;
|
||||
let initDependency: Logging & Init;
|
||||
beforeEach(() => {
|
||||
dependency = {
|
||||
initDependency = {
|
||||
init: vi.fn(),
|
||||
error(): void {},
|
||||
warning(): void {},
|
||||
info(): void {},
|
||||
debug(): void {},
|
||||
dispose: vi.fn()
|
||||
};
|
||||
container = new CoreContainer();
|
||||
});
|
||||
const wait = (seconds: number) => new Promise((resolve) => setTimeout(resolve, seconds));
|
||||
class DB implements Init, Disposable {
|
||||
public connected = false
|
||||
constructor() {}
|
||||
async init() {
|
||||
this.connected = true
|
||||
await wait(10)
|
||||
}
|
||||
async dispose() {
|
||||
await wait(20)
|
||||
this.connected = false
|
||||
}
|
||||
}
|
||||
|
||||
it('should be ready after calling container.ready()', () => {
|
||||
container.ready();
|
||||
expect(container.isReady()).toBe(true);
|
||||
@@ -52,35 +39,14 @@ describe('ioc container', () => {
|
||||
}
|
||||
});
|
||||
it('should init modules', () => {
|
||||
container.upsert({ '@sern/logger': dependency });
|
||||
container.upsert({ '@sern/logger': initDependency });
|
||||
container.ready();
|
||||
expect(dependency.init).to.toHaveBeenCalledOnce();
|
||||
expect(initDependency.init).to.toHaveBeenCalledOnce();
|
||||
});
|
||||
it('should dispose modules', async () => {
|
||||
|
||||
container.upsert({ '@sern/logger': dependency })
|
||||
|
||||
container.ready();
|
||||
// We need to access the dependency at least once to be able to dispose of it.
|
||||
container.get('@sern/logger' as never);
|
||||
await container.disposeAll();
|
||||
expect(dependency.dispose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should init and dispose', async () => {
|
||||
container.add({ db: new DB() })
|
||||
container.ready()
|
||||
const db = container.get('db' as never) as DB
|
||||
expect(db.connected).toBeTruthy()
|
||||
|
||||
await container.disposeAll();
|
||||
|
||||
expect(db.connected).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should not lazy module', () => {
|
||||
container.upsert({ '@sern/logger': () => dependency });
|
||||
container.upsert({ '@sern/logger': () => initDependency });
|
||||
container.ready();
|
||||
expect(dependency.init).toHaveBeenCalledTimes(0);
|
||||
expect(initDependency.init).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ describe('module-loading', () => {
|
||||
const filename = Files.fmtFileName(name+'.'+extension);
|
||||
expect(filename).toBe(name)
|
||||
})
|
||||
|
||||
|
||||
// todo: handle commands with multiple extensions
|
||||
// it('should properly extract filename from file, nested multiple', () => {
|
||||
|
||||
@@ -39,14 +39,14 @@ describe('services', () => {
|
||||
.map((path, i) => `${path}/${modules[i]}.js`);
|
||||
|
||||
const metadata: CommandMeta[] = modules.map((cm, i) => ({
|
||||
id: Id.create(cm.name!, cm.type),
|
||||
id: Id.create(cm.name, cm.type),
|
||||
isClass: false,
|
||||
fullPath: `${paths[i]}/${cm.name}.js`,
|
||||
}));
|
||||
const moduleManager = container.get('@sern/modules');
|
||||
let i = 0;
|
||||
for (const m of modules) {
|
||||
moduleManager.set(Id.create(m.name!, m.type), paths[i]);
|
||||
moduleManager.set(Id.create(m.name, m.type), paths[i]);
|
||||
moduleManager.setMetadata(m, metadata[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ function createRandomCommandModules() {
|
||||
CommandType.Button,
|
||||
];
|
||||
return commandModule({
|
||||
type: faker.helpers.uniqueArray(randomCommandType, 1)[0],
|
||||
type: randomCommandType[Math.floor(Math.random() * randomCommandType.length)],
|
||||
description: faker.string.alpha(),
|
||||
name: faker.string.alpha(),
|
||||
execute: () => {},
|
||||
|
||||
@@ -4,7 +4,7 @@ const shared = {
|
||||
external: ['discord.js', 'iti'],
|
||||
platform: 'node',
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
sourcemap: false,
|
||||
treeshake: {
|
||||
moduleSideEffects: false,
|
||||
correctVarValueBeforeDeclaration: true, //need this to treeshake esm discord.js empty import
|
||||
@@ -17,8 +17,33 @@ export default defineConfig([
|
||||
target: 'node18',
|
||||
tsconfig: './tsconfig.json',
|
||||
outDir: './dist',
|
||||
minify: false,
|
||||
splitting: true,
|
||||
dts: true,
|
||||
...shared,
|
||||
},
|
||||
]);
|
||||
// {
|
||||
// format: 'cjs',
|
||||
// esbuildPlugins: [ifdefPlugin({ variables: { MODE: 'cjs' }, verbose: true })],
|
||||
// splitting: false,
|
||||
// target: 'node18',
|
||||
// tsconfig: './tsconfig-cjs.json',
|
||||
// outDir: './dist/cjs',
|
||||
// outExtension() {
|
||||
// return {
|
||||
// js: '.cjs',
|
||||
// };
|
||||
// },
|
||||
// async onSuccess() {
|
||||
// console.log('writing json commonjs');
|
||||
// await writeFile('./dist/cjs/package.json', JSON.stringify({ type: 'commonjs' }));
|
||||
// },
|
||||
// ...shared,
|
||||
// },
|
||||
// {
|
||||
// dts: {
|
||||
// only: true,
|
||||
// },
|
||||
// entry: ['src/index.ts'],
|
||||
// outDir: 'dist',
|
||||
// },
|
||||
]);
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -619,7 +619,7 @@ __metadata:
|
||||
iti: ^0.6.0
|
||||
prettier: 2.8.8
|
||||
rxjs: ^7.8.0
|
||||
ts-results-es: ^4.0.0
|
||||
ts-results-es: ^3.6.1
|
||||
tsup: ^6.7.0
|
||||
typescript: 5.0.2
|
||||
vitest: latest
|
||||
@@ -3844,10 +3844,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-results-es@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "ts-results-es@npm:4.0.0"
|
||||
checksum: 32a7059491e36d06c5a1084fe9be8021a0beb2d94a94b0c3fa85dc3e96561bf34fb8fd60ebe661064c9fc2bafcf437b6b65f119e8d7497af7f76cda9d9a2a945
|
||||
"ts-results-es@npm:^3.6.1":
|
||||
version: 3.6.1
|
||||
resolution: "ts-results-es@npm:3.6.1"
|
||||
checksum: af0d93ee4d3bd9e99a5fd4ac4b0ad090aef0a61e1f38ee596cfebe8d47090b34a2557d3778e00b4aae7c74962133805275ffffe56716e4d747fa559a926d9ced
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user