mirror of
https://github.com/sern-handler/handler
synced 2026-06-06 01:16:55 +00:00
refactor, add cron types, reinstante module loader
This commit is contained in:
13
package.json
13
package.json
@@ -9,10 +9,6 @@
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.js"
|
||||
},
|
||||
"./internal": {
|
||||
"import": "./dist/_internal.js",
|
||||
"require": "./dist/_internal.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -45,7 +41,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.1",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/node": "~18.17.11",
|
||||
"@typescript-eslint/eslint-plugin": "5.58.0",
|
||||
"@typescript-eslint/parser": "5.59.1",
|
||||
"discord.js": "^14.11.0",
|
||||
@@ -80,10 +76,7 @@
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"semi": [ "error", "always" ],
|
||||
"@typescript-eslint/no-empty-interface": 0,
|
||||
"@typescript-eslint/ban-types": 0,
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
@@ -94,7 +87,7 @@
|
||||
"url": "git+https://github.com/sern-handler/handler.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.16.x"
|
||||
"node": ">= 18.17.x"
|
||||
},
|
||||
"homepage": "https://sern.dev"
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Result } from 'ts-results-es'
|
||||
|
||||
export * from './operators';
|
||||
export * from './functions';
|
||||
|
||||
export type _Module = {
|
||||
|
||||
@@ -72,8 +72,9 @@ async function composeRoot(
|
||||
//container should have no client or logger yet.
|
||||
const hasLogger = conf.exclude?.has('@sern/logger');
|
||||
if (!hasLogger) {
|
||||
__add_container('@sern/logger', new __Services.DefaultLogging);
|
||||
__add_container('@sern/logger', new __Services.DefaultLogging());
|
||||
}
|
||||
__add_container('@sern/errors', new __Services.DefaultErrorHandling());
|
||||
//Build the container based on the callback provided by the user
|
||||
conf.build(container as Container);
|
||||
|
||||
@@ -97,6 +98,7 @@ export async function makeDependencies (conf: ValidDependencyConfig) {
|
||||
if(includeLogger) {
|
||||
__add_container('@sern/logger', new __Services.DefaultLogging);
|
||||
}
|
||||
__add_container('@sern/errors', new __Services.DefaultErrorHandling());
|
||||
await useContainerRaw().ready();
|
||||
} else {
|
||||
await composeRoot(useContainerRaw(), conf);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import path from 'node:path';
|
||||
import { existsSync } from 'fs';
|
||||
import { readdir } from 'fs/promises';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import * as Id from './id'
|
||||
import type { _Module } from './_internal';
|
||||
|
||||
export const parseCallsite = (site: string) => {
|
||||
const pathobj = path.parse(site.replace(/file:\\?/, "")
|
||||
@@ -27,7 +29,6 @@ export const shouldHandle = (pth: string, filenam: string) => {
|
||||
* commonjs, javascript :
|
||||
* ```js
|
||||
* exports = commandModule({ })
|
||||
*
|
||||
* //or
|
||||
* exports.default = commandModule({ })
|
||||
* ```
|
||||
@@ -37,16 +38,33 @@ export const shouldHandle = (pth: string, filenam: string) => {
|
||||
export async function importModule<T>(absPath: string) {
|
||||
let fileModule = await import(absPath);
|
||||
|
||||
let commandModule = fileModule.default;
|
||||
let commandModule: _Module = fileModule.default;
|
||||
|
||||
assert(commandModule , `No export @ ${absPath}. Forgot to ignore with "!"? (!${path.basename(absPath)})?`);
|
||||
if ('default' in commandModule) {
|
||||
commandModule = commandModule.default;
|
||||
commandModule = commandModule.default as _Module;
|
||||
}
|
||||
const p = path.parse(absPath)
|
||||
commandModule.name ??= p.name; commandModule.description ??= "...";
|
||||
commandModule.meta = {
|
||||
//@ts-ignore
|
||||
id: Id.create(commandModule.name, commandModule.type),
|
||||
absPath,
|
||||
};
|
||||
return { module: commandModule } as T;
|
||||
}
|
||||
|
||||
|
||||
export async function* readRecursive(dir: string): AsyncGenerator<string> {
|
||||
const files = await readdir(dir, { withFileTypes: true, recursive: true });
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(file.path, file.name);
|
||||
if(!file.name.startsWith('!') && !file.isDirectory()) {
|
||||
yield fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const fmtFileName = (fileName: string) => path.parse(fileName).name;
|
||||
|
||||
export const filename = (p: string) => fmtFileName(path.basename(p));
|
||||
|
||||
@@ -7,29 +7,16 @@ import type {
|
||||
} from '../types/core-modules';
|
||||
import { type _Module, partitionPlugins } from './_internal';
|
||||
import type { Awaitable } from '../types/utility';
|
||||
import callsites, { type CallSite } from 'callsites';
|
||||
import * as Files from './module-loading'
|
||||
import * as Id from './id'
|
||||
const get_callsite = (css: CallSite[]) => {
|
||||
return css.map(cs => cs.getFileName()).filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.0.0 The wrapper function to define command modules for sern
|
||||
* @param mod
|
||||
*/
|
||||
export function commandModule(mod: InputCommand): _Module {
|
||||
const [onEvent, plugins] = partitionPlugins(mod.plugins);
|
||||
const initCallsite = get_callsite(callsites()).at(-2);
|
||||
if(!initCallsite) throw Error("initCallsite is null");
|
||||
const { name, absPath } = Files.parseCallsite(initCallsite);
|
||||
mod.name ??= name; mod.description ??= '...'
|
||||
//@ts-ignore
|
||||
return {
|
||||
...mod,
|
||||
meta: {
|
||||
id: Id.create(mod.name, mod.type),
|
||||
absPath
|
||||
},
|
||||
onEvent,
|
||||
plugins,
|
||||
};
|
||||
@@ -41,17 +28,10 @@ export function commandModule(mod: InputCommand): _Module {
|
||||
*/
|
||||
export function eventModule(mod: InputEvent): _Module {
|
||||
const [onEvent, plugins] = partitionPlugins(mod.plugins);
|
||||
const initCallsite = get_callsite(callsites()).at(-2);
|
||||
if(!initCallsite) throw Error("initCallsite is null");
|
||||
const { name, absPath } = Files.parseCallsite(initCallsite);
|
||||
mod.name ??= name; mod.description ??= '...'
|
||||
|
||||
//@ts-ignore
|
||||
return {
|
||||
...mod,
|
||||
meta: {
|
||||
id: Id.create(mod.name, mod.type),
|
||||
absPath
|
||||
},
|
||||
plugins,
|
||||
onEvent,
|
||||
};
|
||||
|
||||
@@ -48,16 +48,17 @@ export enum EventType {
|
||||
/**
|
||||
* The EventType for handling discord events
|
||||
*/
|
||||
Discord = 1,
|
||||
Discord,
|
||||
/**
|
||||
* The EventType for handling sern events
|
||||
*/
|
||||
Sern = 2,
|
||||
Sern,
|
||||
/**
|
||||
* The EventType for handling external events.
|
||||
* Could be for example, `process` events, database events
|
||||
*/
|
||||
External = 3,
|
||||
External,
|
||||
Cron
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,13 +14,8 @@ import {
|
||||
pipe
|
||||
} from 'rxjs';
|
||||
import {
|
||||
callPlugin,
|
||||
everyPluginOk,
|
||||
filterMapTo,
|
||||
handleError,
|
||||
type VoidResult,
|
||||
resultPayload,
|
||||
arrayifySource,
|
||||
isAutocomplete,
|
||||
treeSearch,
|
||||
_Module,
|
||||
@@ -39,6 +34,7 @@ import { CommandType } from '../core/structures/enums'
|
||||
import type { Args } from '../types/utility';
|
||||
import { inspect } from 'node:util'
|
||||
import { disposeAll } from '../core/ioc/base';
|
||||
import { arrayifySource, callPlugin, everyPluginOk, filterMapTo, handleError } from '../core/operators';
|
||||
|
||||
|
||||
function contextArgs(wrappable: Message | BaseInteraction, messageArgs?: string[]) {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { Interaction } from 'discord.js';
|
||||
import { mergeMap, merge, concatMap } from 'rxjs';
|
||||
import { PayloadType } from '../core/structures/enums';
|
||||
import { filterTap } from '../core/operators'
|
||||
import { filterTap, sharedEventStream } from '../core/operators'
|
||||
import {
|
||||
isAutocomplete,
|
||||
isCommand,
|
||||
isMessageComponent,
|
||||
isModal,
|
||||
sharedEventStream,
|
||||
resultPayload,
|
||||
} from '../core/_internal';
|
||||
import { createInteractionHandler, executeModule, makeModuleExecutor } from './event-utils';
|
||||
@@ -19,7 +18,7 @@ export function interactionHandler([emitter, err, log, client]: DependencyList)
|
||||
const handle = createInteractionHandler(interactionStream$, modules);
|
||||
|
||||
const interactionHandler$ = merge(handle(isMessageComponent),
|
||||
handle(isAutocomplete),
|
||||
handle(isAutocomplete),
|
||||
handle(isCommand),
|
||||
handle(isModal));
|
||||
return interactionHandler$
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { EMPTY, mergeMap, concatMap } from 'rxjs';
|
||||
import type { Message } from 'discord.js';
|
||||
import { sharedEventStream } from '../core/_internal';
|
||||
import type { DependencyList } from '../types/ioc';
|
||||
import { createMessageHandler, executeModule, makeModuleExecutor } from './event-utils';
|
||||
import { PayloadType, SernError } from '../core/structures/enums'
|
||||
import { resultPayload } from '../core/functions'
|
||||
import { filterTap } from '../core/operators'
|
||||
import { filterTap, sharedEventStream } from '../core/operators'
|
||||
|
||||
/**
|
||||
* Ignores messages from any person / bot except itself
|
||||
* @param prefix
|
||||
@@ -16,7 +16,7 @@ function isNonBot(prefix: string) {
|
||||
|
||||
function hasPrefix(prefix: string, content: string) {
|
||||
const prefixInContent = content.slice(0, prefix.length);
|
||||
return (prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent', }) === 0);
|
||||
return (prefixInContent.localeCompare(prefix, undefined, { sensitivity: 'accent' }) === 0);
|
||||
}
|
||||
|
||||
export function messageHandler(
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ObservableInput, concat, first, fromEvent, ignoreElements, pipe, tap } from 'rxjs';
|
||||
import { concat, first, fromEvent, ignoreElements, pipe, tap } from 'rxjs';
|
||||
import { _Module } from '../core/_internal';
|
||||
import { Logging } from '../core/interfaces';
|
||||
import type { DependencyList } from '../types/ioc';
|
||||
import { callInitPlugins } from './event-utils';
|
||||
|
||||
const once = (log: Logging | undefined) => pipe(
|
||||
tap(() => { log?.info({ message: "Waiting on discord client to be ready..." }) }),
|
||||
first(),
|
||||
ignoreElements())
|
||||
const once = (log: Logging | undefined) =>
|
||||
pipe(tap(() => { log?.info({ message: "Waiting on discord client to be ready..." }) }),
|
||||
first(),
|
||||
ignoreElements())
|
||||
|
||||
export function readyHandler(
|
||||
[sEmitter, , log, client]: DependencyList,
|
||||
@@ -15,7 +15,8 @@ export function readyHandler(
|
||||
//Todo: add module manager on on ready
|
||||
const ready$ = fromEvent(client!, 'ready').pipe(once(log));
|
||||
|
||||
return concat(ready$).pipe(callInitPlugins(sEmitter)).subscribe()
|
||||
return concat(ready$).pipe(callInitPlugins(sEmitter)).subscribe();
|
||||
|
||||
// const validModuleType = module.type >= 0 && module.type <= 1 << 10;
|
||||
// assert.ok(validModuleType,
|
||||
// `Found ${module.name} at ${module.meta.fullPath}, which does not have a valid type`);
|
||||
|
||||
@@ -27,8 +27,7 @@ interface Wrapper {
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function init(wrapper?: Wrapper) {
|
||||
wrapper ??= { commands: "./dist/commands", events: "./dist/events" };
|
||||
export function init(wrapper: Wrapper = { commands: "./dist/commands", events: "./dist/events" }) {
|
||||
const startTime = performance.now();
|
||||
const dependencies = Services('@sern/emitter',
|
||||
'@sern/errors',
|
||||
@@ -52,7 +51,7 @@ export function init(wrapper?: Wrapper) {
|
||||
logger?.info({ message: `sern: registered in ${time} s`, });
|
||||
if(presencePath.exists) {
|
||||
const setPresence = async (p: any) => {
|
||||
return (dependencies[4] as Client).user?.setPresence(p);
|
||||
return (dependencies[3] as Client).user?.setPresence(p);
|
||||
}
|
||||
presenceHandler(presencePath.path, setPresence).subscribe();
|
||||
}
|
||||
|
||||
@@ -163,6 +163,8 @@ export interface EventModuleDefs {
|
||||
[EventType.Sern]: SernEventCommand;
|
||||
[EventType.Discord]: DiscordEventCommand;
|
||||
[EventType.External]: ExternalEventCommand;
|
||||
//TODO
|
||||
[EventType.Cron]: ExternalEventCommand;
|
||||
}
|
||||
|
||||
export interface SernAutocompleteData
|
||||
|
||||
@@ -150,4 +150,8 @@ interface EventArgsMatrix {
|
||||
[PluginType.Control]: unknown[];
|
||||
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
|
||||
};
|
||||
[EventType.Cron]: {
|
||||
[PluginType.Control]: unknown[];
|
||||
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CoreContainer } from '../../src/core/ioc/container';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Disposable, Emitter, Init, Logging } from '../../src/core/interfaces';
|
||||
import * as __Services from '../../src/core/structures/default-services'
|
||||
import { CoreDependencies } from '../../src/types/ioc';
|
||||
|
||||
describe('ioc container', () => {
|
||||
let container: CoreContainer<{}> = new CoreContainer();
|
||||
let dependency: Logging & Init & Disposable;
|
||||
let dependency2: Emitter
|
||||
beforeEach(() => {
|
||||
dependency = {
|
||||
init: vi.fn(),
|
||||
error(): void {},
|
||||
warning(): void {},
|
||||
info(): void {},
|
||||
debug(): void {},
|
||||
dispose: vi.fn()
|
||||
};
|
||||
dependency2 = {
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
emit: 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);
|
||||
});
|
||||
it('should container all core dependencies', async () => {
|
||||
const keys = [
|
||||
'@sern/emitter',
|
||||
'@sern/logger',
|
||||
'@sern/errors',
|
||||
] satisfies (keyof CoreDependencies)[];
|
||||
container.add({
|
||||
'@sern/logger': () => new __Services.DefaultLogging(),
|
||||
'@sern/client': () => new EventEmitter(),
|
||||
});
|
||||
for (const k of keys) {
|
||||
//@ts-expect-error typings for iti are strict
|
||||
expect(() => container.get(k)).not.toThrow();
|
||||
}
|
||||
});
|
||||
it('should init modules', () => {
|
||||
container.upsert({ '@sern/logger': dependency });
|
||||
container.ready();
|
||||
expect(dependency.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.ready();
|
||||
expect(dependency.init).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should init dependency depending on something else', () => {
|
||||
container.add({ '@sern/client': dependency2 });
|
||||
container.upsert((cntr) => ({ '@sern/logger': dependency }));
|
||||
container.ready();
|
||||
expect(dependency.init).toHaveBeenCalledTimes(1);
|
||||
})
|
||||
|
||||
it('should detect a key already exists', () => {
|
||||
container.add({ '@sern/client': dependency2 });
|
||||
expect(container.hasKey('@sern/client')).toBeTruthy()
|
||||
})
|
||||
|
||||
|
||||
it('should detect a key already exists', () => {
|
||||
container.add({ '@sern/client': () => dependency2 });
|
||||
expect(container.hasKey('@sern/client')).toBeTruthy()
|
||||
})
|
||||
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
import { SpyInstance, afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CoreContainer } from '../../src/core/ioc/container';
|
||||
import * as __Services from '../../src/core/structures/default-services';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { commandModule, CommandType } from '../../src';
|
||||
|
||||
function createRandomCommandModules() {
|
||||
return commandModule({
|
||||
type: CommandType.Slash,
|
||||
description: faker.string.alpha(),
|
||||
name: faker.string.alpha({ length: { min: 5, max: 10 }}),
|
||||
execute: vi.fn(),
|
||||
});
|
||||
}
|
||||
describe('services', () => {
|
||||
//@ts-ignore
|
||||
let container: CoreContainer<Dependencies>;
|
||||
let consoleMock: SpyInstance;
|
||||
beforeEach(() => {
|
||||
container = new CoreContainer();
|
||||
container.add({ '@sern/logger': () => new __Services.DefaultLogging() });
|
||||
container.ready();
|
||||
consoleMock = vi.spyOn(container.get('@sern/logger'), 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
consoleMock.mockReset();
|
||||
});
|
||||
|
||||
//todo add more
|
||||
it('error-handling', () => {
|
||||
const errorHandler = container.get('@sern/errors');
|
||||
const lifetime = errorHandler.keepAlive;
|
||||
for (let i = 0; i < lifetime; i++) {
|
||||
if (i == lifetime - 1) {
|
||||
expect(() => errorHandler.updateAlive(new Error('poo'))).toThrowError();
|
||||
} else {
|
||||
expect(() => errorHandler.updateAlive(new Error('poo'))).not.toThrowError();
|
||||
}
|
||||
}
|
||||
});
|
||||
//todo add more, spy on every instance?
|
||||
it('logger', () => {
|
||||
container.get('@sern/logger').error({ message: 'error' });
|
||||
|
||||
expect(consoleMock).toHaveBeenCalledOnce();
|
||||
expect(consoleMock).toHaveBeenLastCalledWith({ message: 'error' });
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
10
yarn.lock
10
yarn.lock
@@ -235,7 +235,7 @@ __metadata:
|
||||
resolution: "@sern/handler@workspace:."
|
||||
dependencies:
|
||||
"@faker-js/faker": ^8.0.1
|
||||
"@types/node": ^18.15.11
|
||||
"@types/node": ~18.17.11
|
||||
"@typescript-eslint/eslint-plugin": 5.58.0
|
||||
"@typescript-eslint/parser": 5.59.1
|
||||
callsites: ^3.1.0
|
||||
@@ -262,10 +262,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^18.15.11":
|
||||
version: 18.17.14
|
||||
resolution: "@types/node@npm:18.17.14"
|
||||
checksum: f96ce1e588426a26cf82440193084f8bbab47bfb3c2e668cf174095f99ce808a20654b2137448c7e88cfd7b6c2b8521ffb6f714f521b3502ac595a0df0bff679
|
||||
"@types/node@npm:~18.17.11":
|
||||
version: 18.17.19
|
||||
resolution: "@types/node@npm:18.17.19"
|
||||
checksum: 6ab47127cd7534511aa199550659d56b44e5a6dbec9df054d0cde279926b4d43f0e6438f92c8392b039ab4e2a85aa0f698b95926430aff860e23bfc36c96576c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user