refactor, add cron types, reinstante module loader

This commit is contained in:
Jacob Nguyen
2024-05-14 12:01:18 -05:00
parent 0a05cbba3f
commit 880311f08c
17 changed files with 60 additions and 233 deletions

View File

@@ -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"
}

View File

@@ -1,4 +0,0 @@

View File

@@ -1,6 +1,5 @@
import type { Result } from 'ts-results-es'
export * from './operators';
export * from './functions';
export type _Module = {

View File

@@ -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);

View File

@@ -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));

View File

@@ -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,
};

View File

@@ -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
}
/**

View File

@@ -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[]) {

View File

@@ -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$

View File

@@ -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(

View File

@@ -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`);

View File

@@ -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();
}

View File

@@ -163,6 +163,8 @@ export interface EventModuleDefs {
[EventType.Sern]: SernEventCommand;
[EventType.Discord]: DiscordEventCommand;
[EventType.External]: ExternalEventCommand;
//TODO
[EventType.Cron]: ExternalEventCommand;
}
export interface SernAutocompleteData

View File

@@ -150,4 +150,8 @@ interface EventArgsMatrix {
[PluginType.Control]: unknown[];
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
};
[EventType.Cron]: {
[PluginType.Control]: unknown[];
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
};
}

View File

@@ -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()
})
});

View File

@@ -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' });
});
});

View File

@@ -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