mirror of
https://github.com/sern-handler/handler
synced 2026-06-21 07:12:15 +00:00
Compare commits
11 Commits
fix/dispos
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb6e8c2cfc | ||
|
|
0eecb08e87 | ||
|
|
c67748c7df | ||
|
|
efee0fdbe2 | ||
|
|
797442ece3 | ||
|
|
513ac8edf4 | ||
|
|
81a0180d05 | ||
|
|
89d7409536 | ||
|
|
aa802f761e | ||
|
|
2414992b73 | ||
|
|
70c6236802 |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,5 +1,33 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [4.2.6](https://github.com/sern-handler/handler/compare/v4.2.5...v4.2.6) (2025-09-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Miscellaneous Chores
|
||||||
|
|
||||||
|
* release 4.2.6 ([#402](https://github.com/sern-handler/handler/issues/402)) ([0eecb08](https://github.com/sern-handler/handler/commit/0eecb08e87e26102030ccf6ef38ddd81051ec373))
|
||||||
|
|
||||||
|
## [4.2.5](https://github.com/sern-handler/handler/compare/v4.2.4...v4.2.5) (2025-08-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* make message module warn rather than throwing ([#399](https://github.com/sern-handler/handler/issues/399)) ([797442e](https://github.com/sern-handler/handler/commit/797442ece3999bf2cb6b5ba0688ce0177e72a22f))
|
||||||
|
|
||||||
|
## [4.2.4](https://github.com/sern-handler/handler/compare/v4.2.3...v4.2.4) (2025-03-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* flat autocomplete ([#395](https://github.com/sern-handler/handler/issues/395)) ([89d7409](https://github.com/sern-handler/handler/commit/89d74095363befddc3222b9e5c89c35e7c6457b9))
|
||||||
|
|
||||||
|
## [4.2.3](https://github.com/sern-handler/handler/compare/v4.2.2...v4.2.3) (2025-03-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* autocomplete sdt.module not present ([#393](https://github.com/sern-handler/handler/issues/393)) ([2414992](https://github.com/sern-handler/handler/commit/2414992b73a40065464b20f2d53826c78fcd3a5f))
|
||||||
|
|
||||||
## [4.2.2](https://github.com/sern-handler/handler/compare/v4.2.1...v4.2.2) (2025-02-03)
|
## [4.2.2](https://github.com/sern-handler/handler/compare/v4.2.1...v4.2.2) (2025-02-03)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ export default commandModule({
|
|||||||
- [SmokinWeed 💨](https://github.com/Peter-MJ-Parker/sern-bud) - A fun bot for a small, but growing server.
|
- [SmokinWeed 💨](https://github.com/Peter-MJ-Parker/sern-bud) - A fun bot for a small, but growing server.
|
||||||
- [Man Nomic](https://github.com/jacoobes/man-nomic) - A simple information bot to provide information to the nomic-ai Discord community.
|
- [Man Nomic](https://github.com/jacoobes/man-nomic) - A simple information bot to provide information to the nomic-ai Discord community.
|
||||||
- [Linear-Discord](https://github.com/sern-handler/linear-discord) - Display and manage a linear dashboard.
|
- [Linear-Discord](https://github.com/sern-handler/linear-discord) - Display and manage a linear dashboard.
|
||||||
|
- [ZenithBot](https://github.com/CodeCraftersHaven/ZenithBot) - A versatile bot coded in TypeScript, designed to enhance server management and user interaction through its robust features.
|
||||||
|
|
||||||
## 💻 CLI
|
## 💻 CLI
|
||||||
|
|
||||||
It is **highly encouraged** to use the [command line interface](https://github.com/sern-handler/cli) for your project. Don't forget to view it.
|
It is **highly encouraged** to use the [command line interface](https://github.com/sern-handler/cli) for your project. Don't forget to view it.
|
||||||
|
|||||||
3897
package-lock.json
generated
Normal file
3897
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@sern/handler",
|
"name": "@sern/handler",
|
||||||
"packageManager": "yarn@3.5.0",
|
"packageManager": "yarn@3.5.0",
|
||||||
"version": "4.2.2",
|
"version": "4.2.6",
|
||||||
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
|
"description": "A complete, customizable, typesafe, & reactive framework for discord bots.",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
@@ -47,8 +47,8 @@
|
|||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
"@typescript-eslint/eslint-plugin": "5.58.0",
|
"@typescript-eslint/eslint-plugin": "5.58.0",
|
||||||
"@typescript-eslint/parser": "5.59.1",
|
"@typescript-eslint/parser": "5.59.1",
|
||||||
"discord.js": "^14.14.1",
|
"discord.js": "^14.22.1",
|
||||||
"eslint": "9.0.0",
|
"eslint": "8.39.0",
|
||||||
"typescript": "5.0.2",
|
"typescript": "5.0.2",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.6.0"
|
||||||
},
|
},
|
||||||
|
|||||||
111
src/cleanup.ts
111
src/cleanup.ts
@@ -1,111 +0,0 @@
|
|||||||
// It's this package but without default console log / error https://github.com/trevorr/async-cleanup
|
|
||||||
|
|
||||||
/** A possibly asynchronous function invoked with the process is about to exit. */
|
|
||||||
export type CleanupListener = () => void | Promise<void>;
|
|
||||||
|
|
||||||
let cleanupListeners: Set<CleanupListener> | undefined;
|
|
||||||
|
|
||||||
/** Registers a new cleanup listener. Adding the same listener more than once has no effect. */
|
|
||||||
export function addCleanupListener(listener: CleanupListener): void {
|
|
||||||
// Install exit listeners on initial cleanup listener
|
|
||||||
if (!cleanupListeners) {
|
|
||||||
installExitListeners();
|
|
||||||
cleanupListeners = new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanupListeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Removes an existing cleanup listener, and returns whether the listener was registered. */
|
|
||||||
export function removeCleanupListener(listener: CleanupListener): boolean {
|
|
||||||
return cleanupListeners != null && cleanupListeners.delete(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Executes all cleanup listeners and then exits the process. Call this instead of `process.exit` to ensure all listeners are fully executed. */
|
|
||||||
export async function exitAfterCleanup(code = 0): Promise<never> {
|
|
||||||
await executeCleanupListeners();
|
|
||||||
process.exit(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Executes all cleanup listeners and then kills the process with the given signal. */
|
|
||||||
export async function killAfterCleanup(signal: ExitSignal): Promise<void> {
|
|
||||||
await executeCleanupListeners();
|
|
||||||
process.kill(process.pid, signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeCleanupListeners(): Promise<void> {
|
|
||||||
if (cleanupListeners) {
|
|
||||||
// Remove exit listeners to restore normal event handling
|
|
||||||
uninstallExitListeners();
|
|
||||||
|
|
||||||
// Clear cleanup listeners to reset state for testing
|
|
||||||
const listeners = cleanupListeners;
|
|
||||||
cleanupListeners = undefined;
|
|
||||||
|
|
||||||
// Call listeners in order added with async listeners running concurrently
|
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
for (const listener of listeners) {
|
|
||||||
try {
|
|
||||||
const promise = listener();
|
|
||||||
if (promise) promises.push(promise);
|
|
||||||
} catch (err) {
|
|
||||||
// console.error("Uncaught exception during cleanup", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all listeners to complete and log any rejections
|
|
||||||
const results = await Promise.allSettled(promises);
|
|
||||||
for (const result of results) {
|
|
||||||
if (result.status === "rejected") {
|
|
||||||
console.error("Unhandled rejection during cleanup", result.reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function beforeExitListener(code: number): void {
|
|
||||||
// console.log(`Exiting with code ${code} due to empty event loop`);
|
|
||||||
void exitAfterCleanup(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
function uncaughtExceptionListener(error: Error): void {
|
|
||||||
// console.error("Exiting with code 1 due to uncaught exception", error);
|
|
||||||
void exitAfterCleanup(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function signalListener(signal: ExitSignal): void {
|
|
||||||
// console.log(`Exiting due to signal ${signal}`);
|
|
||||||
void killAfterCleanup(signal);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listenable signals that terminate the process by default
|
|
||||||
// (except SIGQUIT, which generates a core dump and should not trigger cleanup)
|
|
||||||
// See https://nodejs.org/api/process.html#signal-events
|
|
||||||
const listenedSignals = [
|
|
||||||
"SIGBREAK", // Ctrl-Break on Windows
|
|
||||||
"SIGHUP", // Parent terminal closed
|
|
||||||
"SIGINT", // Terminal interrupt, usually by Ctrl-C
|
|
||||||
"SIGTERM", // Graceful termination
|
|
||||||
"SIGUSR2", // Used by Nodemon
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
/** Signals that can terminate the process. */
|
|
||||||
export type ExitSignal =
|
|
||||||
| typeof listenedSignals[number]
|
|
||||||
| "SIGKILL"
|
|
||||||
| "SIGQUIT"
|
|
||||||
| "SIGSTOP";
|
|
||||||
|
|
||||||
function installExitListeners(): void {
|
|
||||||
process.on("beforeExit", beforeExitListener);
|
|
||||||
process.on("uncaughtException", uncaughtExceptionListener);
|
|
||||||
listenedSignals.forEach((signal) => process.on(signal, signalListener));
|
|
||||||
}
|
|
||||||
|
|
||||||
function uninstallExitListeners(): void {
|
|
||||||
process.removeListener("beforeExit", beforeExitListener);
|
|
||||||
process.removeListener("uncaughtException", uncaughtExceptionListener);
|
|
||||||
listenedSignals.forEach((signal) =>
|
|
||||||
process.removeListener(signal, signalListener)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -33,14 +33,14 @@ export function interactionHandler(deps: UnpackedDependencies, defaultPrefix?: s
|
|||||||
// handles autocomplete
|
// handles autocomplete
|
||||||
if(isAutocomplete(event)) {
|
if(isAutocomplete(event)) {
|
||||||
const lookupTable = module.locals['@sern/lookup-table'] as Map<string, SernAutocompleteData>
|
const lookupTable = module.locals['@sern/lookup-table'] as Map<string, SernAutocompleteData>
|
||||||
const subCommandGroup = event.options.getSubcommandGroup() ?? "",
|
const subCommandGroup = event.options.getSubcommandGroup(false) ?? "",
|
||||||
subCommand = event.options.getSubcommand() ?? "",
|
subCommand = event.options.getSubcommand(false) ?? "",
|
||||||
option = event.options.getFocused(true),
|
option = event.options.getFocused(true),
|
||||||
fullPath = path.posix.join("<parent>", subCommandGroup, subCommand, option.name)
|
fullPath = path.posix.join("<parent>", subCommandGroup, subCommand, option.name)
|
||||||
|
|
||||||
const resolvedModule = (lookupTable.get(fullPath)!.command) as Module
|
const resolvedModule = (lookupTable.get(fullPath)!.command) as Module
|
||||||
payload= { module: resolvedModule , //autocomplete is not a true "module" warning cast!
|
payload= { module: resolvedModule , //autocomplete is not a true "module" warning cast!
|
||||||
args: [event, createSDT(resolvedModule, deps, params)] };
|
args: [event, createSDT(module, deps, params)] };
|
||||||
// either CommandTypes Slash | ContextMessage | ContextUesr
|
// either CommandTypes Slash | ContextMessage | ContextUesr
|
||||||
} else if(isCommand(event)) {
|
} else if(isCommand(event)) {
|
||||||
const sdt = createSDT(module, deps, params)
|
const sdt = createSDT(module, deps, params)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function messageHandler (deps: UnpackedDependencies, defaultPrefix?: stri
|
|||||||
const [prefix] = fmt(message.content, defaultPrefix);
|
const [prefix] = fmt(message.content, defaultPrefix);
|
||||||
let module = mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`) as Module;
|
let module = mg.get(`${prefix}_T`) ?? mg.get(`${prefix}_B`) as Module;
|
||||||
if(!module) {
|
if(!module) {
|
||||||
throw Error('Possibly undefined behavior: could not find a static id to resolve')
|
log?.warning({ message: 'Possibly undefined behavior: could not find a static id to resolve' });
|
||||||
}
|
}
|
||||||
const payload = { module, args: [Context.wrap(message, defaultPrefix), createSDT(module, deps, undefined)] }
|
const payload = { module, args: [Context.wrap(message, defaultPrefix), createSDT(module, deps, undefined)] }
|
||||||
const result = await callPlugins(payload)
|
const result = await callPlugins(payload)
|
||||||
|
|||||||
@@ -1,44 +1,58 @@
|
|||||||
import * as Files from '../core/module-loading'
|
import * as Files from "../core/module-loading";
|
||||||
import { once } from 'node:events';
|
import { once } from "node:events";
|
||||||
import { createLookupTable, resultPayload } from '../core/functions';
|
import { createLookupTable, resultPayload } from "../core/functions";
|
||||||
import { CommandType } from '../core/structures/enums';
|
import { CommandType } from "../core/structures/enums";
|
||||||
import { Module, SernOptionsData } from '../types/core-modules';
|
import { Module, SernOptionsData } from "../types/core-modules";
|
||||||
import type { UnpackedDependencies, Wrapper } from '../types/utility';
|
import type { UnpackedDependencies, Wrapper } from "../types/utility";
|
||||||
import { callInitPlugins } from './event-utils';
|
import { callInitPlugins } from "./event-utils";
|
||||||
import { SernAutocompleteData } from '..';
|
import { SernAutocompleteData } from "..";
|
||||||
|
import { Events } from "discord.js";
|
||||||
|
|
||||||
export default async function(dirs: string | string[], deps : UnpackedDependencies) {
|
export default async function (
|
||||||
const { '@sern/client': client,
|
dirs: string | string[],
|
||||||
'@sern/logger': log,
|
deps: UnpackedDependencies,
|
||||||
'@sern/emitter': sEmitter,
|
) {
|
||||||
'@sern/modules': commands } = deps;
|
const {
|
||||||
log?.info({ message: "Waiting on discord client to be ready..." })
|
"@sern/client": client,
|
||||||
await once(client, "ready");
|
"@sern/logger": log,
|
||||||
log?.info({ message: "Client signaled ready, registering modules" });
|
"@sern/emitter": sEmitter,
|
||||||
|
"@sern/modules": commands,
|
||||||
|
} = deps;
|
||||||
|
log?.info({ message: "Waiting on discord client to be ready..." });
|
||||||
|
await once(client, Events.ClientReady);
|
||||||
|
log?.info({ message: "Client signaled ready, registering modules" });
|
||||||
|
|
||||||
// https://observablehq.com/@ehouais/multiple-promises-as-an-async-generator
|
// https://observablehq.com/@ehouais/multiple-promises-as-an-async-generator
|
||||||
// possibly optimize to concurrently import modules
|
// possibly optimize to concurrently import modules
|
||||||
|
|
||||||
const directories = Array.isArray(dirs) ? dirs : [dirs];
|
const directories = Array.isArray(dirs) ? dirs : [dirs];
|
||||||
|
|
||||||
for (const dir of directories) {
|
for (const dir of directories) {
|
||||||
for await (const path of Files.readRecursive(dir)) {
|
for await (const path of Files.readRecursive(dir)) {
|
||||||
let { module } = await Files.importModule<Module>(path);
|
const { module } = await Files.importModule<Module>(path);
|
||||||
const validType = module.type >= CommandType.Text && module.type <= CommandType.ChannelSelect;
|
const validType =
|
||||||
if(!validType) {
|
module.type >= CommandType.Text &&
|
||||||
throw Error(`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``);
|
module.type <= CommandType.ChannelSelect;
|
||||||
}
|
if (!validType) {
|
||||||
const resultModule = await callInitPlugins(module, deps, true);
|
throw Error(
|
||||||
|
`Found ${module.name} at ${module.meta.absPath}, which has incorrect \`type\``,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const resultModule = await callInitPlugins(module, deps, true);
|
||||||
|
|
||||||
if(module.type === CommandType.Both || module.type === CommandType.Slash) {
|
if (
|
||||||
const options = (Reflect.get(module, 'options') ?? []) as SernOptionsData[];
|
module.type === CommandType.Both ||
|
||||||
const lookupTable = createLookupTable(options)
|
module.type === CommandType.Slash
|
||||||
module.locals['@sern/lookup-table'] = lookupTable;
|
) {
|
||||||
}
|
const options = (Reflect.get(module, "options") ??
|
||||||
// FREEZE! no more writing!!
|
[]) as SernOptionsData[];
|
||||||
commands.set(resultModule.meta.id, Object.freeze(resultModule));
|
const lookupTable = createLookupTable(options);
|
||||||
sEmitter.emit('module.register', resultPayload('success', resultModule));
|
module.locals["@sern/lookup-table"] = lookupTable;
|
||||||
}
|
}
|
||||||
|
// FREEZE! no more writing!!
|
||||||
|
commands.set(resultModule.meta.id, Object.freeze(resultModule));
|
||||||
|
sEmitter.emit("module.register", resultPayload("success", resultModule));
|
||||||
}
|
}
|
||||||
sEmitter.emit('modulesLoaded');
|
}
|
||||||
|
sEmitter.emit("modulesLoaded");
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/sern.ts
10
src/sern.ts
@@ -14,7 +14,6 @@ import { presenceHandler } from './handlers/presence';
|
|||||||
import type { Payload, UnpackedDependencies, Wrapper } from './types/utility';
|
import type { Payload, UnpackedDependencies, Wrapper } from './types/utility';
|
||||||
import type { Presence} from './core/presences';
|
import type { Presence} from './core/presences';
|
||||||
import { registerTasks } from './handlers/tasks';
|
import { registerTasks } from './handlers/tasks';
|
||||||
import { addCleanupListener } from './cleanup';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,12 +76,5 @@ export function init(maybeWrapper: Wrapper = { commands: "./dist/commands" }) {
|
|||||||
})
|
})
|
||||||
.catch(err => { throw err });
|
.catch(err => { throw err });
|
||||||
interactionHandler(deps, maybeWrapper.defaultPrefix);
|
interactionHandler(deps, maybeWrapper.defaultPrefix);
|
||||||
messageHandler(deps, maybeWrapper.defaultPrefix);
|
messageHandler(deps, maybeWrapper.defaultPrefix)
|
||||||
|
|
||||||
addCleanupListener(async () => {
|
|
||||||
const duration = ((performance.now() - startTime) / 1000).toFixed(2)
|
|
||||||
deps['@sern/logger']?.info({ 'message': 'sern is shutting down after '+duration +" seconds" })
|
|
||||||
await useContainerRaw().disposeAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user