From 5175d66eb5e2930598e133ca36897ecc5614e840 Mon Sep 17 00:00:00 2001 From: zombieb Date: Wed, 25 Jun 2025 02:42:05 -0400 Subject: [PATCH] version 1.3!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --- .gitignore | 1 - README.md | 60 +++++++++++++++- deno.json | 6 +- mod.ts | 196 ++++++++++++++++++++++++++++++++++++++++++++++------- test.ts | 44 ++++++++++++ 5 files changed, 279 insertions(+), 28 deletions(-) delete mode 100644 .gitignore create mode 100644 test.ts diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0c2fc0c..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -test.ts \ No newline at end of file diff --git a/README.md b/README.md index 5f337a8..bf7fab7 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ log.n("Something happened"); Disable a type of message globally: ```ts -import Logging, { MessageTypeVisibility } from "./mod.ts"; +import Logging, { MessageTypeVisibility } from "@proxnet/undead-logging"; // .. logging source "Main" is in the scope MessageTypeVisibility.Error = false; @@ -45,4 +45,62 @@ log.error("I am not visible"); // output: // 2024-11-14T01:21:40.350Z Main [INFO] I am visible +``` + +## Event Listeners +Event listeners are called when a message is logged on any logging source + +An event callback can either receive messages as either: +* Whole lines, including formatting, colors, excluding newlines +or +* Individual `msg` (string), `type` (MessageType), `source` (string), and `time` (Date) arguments + +Callback ran when a message is logged anywhere: +```ts +LoggingListeners.onmsg('basic', msg => { + // `msg` is a string containing the entire line w/ formatting +}); +``` + +Callback ran when a message is logged anywhere (this time components are split up): +```ts +LoggingListeners.onmsg('type', (msg, type, source, time) => { + // msg: string + // type: MessageType - import { MessageType } from "@proxnet/undead-logging";) + // source: string + // time: Date +}); +``` + +Remove callback: +```ts +const cb = msg => { /* do something with msg */ }; +LoggingListeners.onmsg('basic', cb); +LoggingListeners.offmsg('basic', cb); +``` + +## Time display modes + +You can display three different formats for time: +* UTC +* Unix time (in milliseconds) +* Local [process] time (in seconds, from the `performance` API) + +Set the time format: +```ts +import { LoggingConfiguration, TimeFormat } from "@proxnet/undead-logging"; + +LoggingConfiguration.timeFormat = TimeFormat.Local +// or +LoggingConfiguration.timeFormat = TimeFormat.Utc +``` + +## (advanced) Logging timing + +You can control when log functions are executed using `LoggingConfiguration.logTiming`. + +Logs are sent synchronously by default. You can optionally defer logs with `setImmediate` using `LogTiming.Deferred` +```ts +LoggingConfiguration.logTiming = LogTiming.Sync +LoggingConfiguration.logTiming = LogTiming.Deferred ``` \ No newline at end of file diff --git a/deno.json b/deno.json index a2feefe..6096327 100644 --- a/deno.json +++ b/deno.json @@ -1,10 +1,14 @@ { + "tasks": { + "debug": "deno run -A test.ts true", + "plain": "deno run -A test.ts" + }, "imports": { "@std/assert": "jsr:@std/assert@1", "chalk": "npm:chalk@^5.3.0" }, "exports": "./mod.ts", - "version": "1.2.0", + "version": "1.3.0", "name": "@proxnet/undead-logging", "license": "MIT" } diff --git a/mod.ts b/mod.ts index e072f98..701600c 100644 --- a/mod.ts +++ b/mod.ts @@ -1,14 +1,25 @@ import chalk from "chalk"; +import { setImmediate } from "node:timers"; + +type ChalkFunction = (...msgs: unknown[]) => string; +type PrimitiveItems = unknown[]; + +interface UnknownConversion { + condition: (arg: unknown) => boolean; + converter: (arg: T) => string; +} /** * A source for pretty and cool and fun logging */ class Logging { - /** Can I be seen by the console? */ + /** Can I be seen by the console and listeners? */ visible: boolean /** What is my name? */ source: string + /** Should I use bright colors over dull ones? */ + bright: boolean = true; /** * Create a logging source @@ -21,10 +32,90 @@ class Logging { * @param silent Set to false to log a message when the logger instantiates. Useful for debugging. * @returns A source for logging messages to the console. Functions for info, warnings, errors, debug statements, and network events are provided and have shorthands. */ - constructor(source: string, silent?: boolean) { + constructor(source: string, silent?: boolean, bright?: boolean) { this.visible = true; this.source = source; - if (typeof silent == 'boolean' && !silent) this.info(`Instantiated module logging`); + if (typeof bright == 'boolean') this.bright = bright; + + if (typeof silent == 'boolean' && !silent) this.info(`Instantiated logging for ${this.source}`); + } + + #log(type: MessageType, chalkColor: ChalkFunction, ...msgs: PrimitiveItems) { + if (!this.visible) return; + const func = () => { + + let typestr: string; + switch (type) { + case MessageType.Info: + typestr = '[INFO]'; + break; + case MessageType.Warn: + typestr = '[WARN]'; + break; + case MessageType.Error: + typestr = '[ERROR]'; + break; + case MessageType.Debug: + typestr = '[DEBUG]'; + break; + case MessageType.Network: + typestr = '[NETWORK]'; + break; + default: + typestr = '[UNKNOWN]'; + break; + } + + const time = new Date(); + let timeFormatted; + switch (LoggingConfiguration.timeFormat) { + case TimeFormat.Unix: + timeFormatted = time.getTime(); + break; + case TimeFormat.Local: + timeFormatted = performance.now(); + break; + default: + timeFormatted = time.toISOString(); + } + + const conversions = [ + { + condition: arg => arg instanceof Error, + converter: arg => arg.stack || arg.message + } as UnknownConversion, + { + condition: arg => arg == null, + converter: arg => JSON.stringify(arg) + } as UnknownConversion, + { + condition: arg => typeof arg == 'object', + converter: arg => JSON.stringify(arg) + } as UnknownConversion, + ]; + function convertToString(arg: unknown) { + for (const conversion of conversions) + if (conversion.condition(arg)) + return (conversion.converter as (arg: unknown) => string)(arg); + // gpt-4o idea cuz my brain has the `stupid` type + + return String(arg); // fallback + } + + const str = msgs.map(val => convertToString(val)).join(' '); + const msg = chalk.gray(`${timeFormatted} `) + chalkColor(chalk.inverse(`${this.source} ${typestr}`) + ` ${str}`); + console.log(msg); + + try { + LoggingListeners.emit('basic', msg); + LoggingListeners.emit('type', str, type, this.source, time); + } catch (err) { + console.error(`(FALLBACK ERROR) Callback failed! Stack: ${(err as Error).stack}`); + } + } + + if (LoggingConfiguration.logTiming == LogTiming.Sync) func(); + else setImmediate(func); } /** @@ -32,7 +123,7 @@ class Logging { * @param type The kind of message to log * @param msgs Your message(s) */ - log(type: MessageType, ...msgs: string[]) { + log(type: MessageType, ...msgs: PrimitiveItems) { if (type == MessageType.Info) this.i(...msgs); if (type == MessageType.Warn) this.w(...msgs); if (type == MessageType.Error) this.e(...msgs); @@ -41,48 +132,43 @@ class Logging { } /** Logging Function */ - i(...msgs: string[]) {this.info(...msgs);} + i(...msgs: PrimitiveItems) {this.info(...msgs);} /** Logging Function */ - info(...msgs: string[]) { + info(...msgs: PrimitiveItems) { if (!MessageTypeVisibility.Info) return; - if (this.visible !== true) return; - console.log(chalk.gray(`${new Date().toISOString()} `) + chalk.bgWhite.black(`${this.source} [INFO]`) + chalk.whiteBright(' ' + msgs.join(' '))); + this.#log(MessageType.Info, chalk.whiteBright, ...msgs); } /** Logging Function */ - w(...msgs: string[]) {this.warn(...msgs);} + w(...msgs: PrimitiveItems) {this.warn(...msgs);} /** Logging Function */ - warn(...msgs: string[]) { + warn(...msgs: PrimitiveItems) { if (!MessageTypeVisibility.Warn) return; - if (this.visible !== true) return; - console.warn(chalk.gray(`${new Date().toISOString()} `) + chalk.bgYellow.black(`${this.source} [WARN]`) + chalk.yellowBright(' ' + msgs.join(' '))); + this.#log(MessageType.Warn, chalk.yellowBright, ...msgs); } /** Logging Function */ - e(...msgs: string[]) {this.error(...msgs);} + e(...msgs: PrimitiveItems) {this.error(...msgs);} /** Logging Function */ - error(...msgs: string[]) { + error(...msgs: PrimitiveItems) { if (!MessageTypeVisibility.Error) return; - if (this.visible !== true) return; - console.error(chalk.gray(`${new Date().toISOString()} `) + chalk.bgRed.black(`${this.source} [ERROR]`) + chalk.redBright(' ' + msgs.join(' '))); + this.#log(MessageType.Error, chalk.redBright, ...msgs); } /** Logging Function */ - d(...msgs: string[]) {this.debug(...msgs);} + d(...msgs: PrimitiveItems) {this.debug(...msgs);} /** Logging Function */ - debug(...msgs: string[]) { + debug(...msgs: PrimitiveItems) { if (!MessageTypeVisibility.Debug) return; - if (this.visible !== true) return; - console.debug(chalk.gray(`${new Date().toISOString()} `) + chalk.bgGreen.black(`${this.source} [DEBUG]`) + chalk.greenBright(' ' + msgs.join(' '))); + this.#log(MessageType.Debug, chalk.greenBright, ...msgs); } /** Logging Function */ - n(...msgs: string[]) {this.network(...msgs);} + n(...msgs: PrimitiveItems) {this.network(...msgs);} /** Logging Function */ - network(...msgs: string[]) { + network(...msgs: PrimitiveItems) { if (!MessageTypeVisibility.Network) return; - if (this.visible !== true) return; - console.log(chalk.gray(`${new Date().toISOString()} `) + chalk.bgCyan.black(`${this.source} [NETWORK]`) + chalk.cyanBright(' ' + msgs.join(' '))); + this.#log(MessageType.Network, chalk.cyanBright, ...msgs); } } @@ -105,5 +191,65 @@ const MessageTypeVisibility = { Network: true } -export { MessageType, MessageTypeVisibility }; +// 'On Message' event listeners +type BasicListener = (msg: string) => unknown; +type TypeListener = (msg: string, type: MessageType, source: string, time: Date) => unknown; +type Listener = BasicListener | TypeListener; +type ListenerType = 'basic' | 'type'; + +const basicListeners = new Set(); +const typeListeners = new Set(); + +class ListenersBase { + onmsg(type: 'basic', cb: BasicListener): void + onmsg(type: 'type', cb: TypeListener): void + onmsg(type: ListenerType, cb: Listener) { + if (type == 'basic') basicListeners.add(cb); + else if (type == 'type') typeListeners.add(cb); + } + + offmsg(type: 'basic', cb: BasicListener): void + offmsg(type: 'type', cb: TypeListener): void + offmsg(type: ListenerType, cb: Listener) { + if (type == 'basic') basicListeners.delete(cb); + else if (type == 'type') typeListeners.delete(cb); + } + + emit(type: 'basic', msg: string, msgtype?: MessageType, source?: string, time?: Date): void + emit(type: 'type', msg: string, msgtype: MessageType, source: string, time: Date): void + emit(type: ListenerType, msg: string, msgtype: MessageType, source: string, time: Date) { + if (type == 'basic') + for (const cb of basicListeners) (cb as BasicListener)(msg); + if (type == 'type') + for (const cb of typeListeners) (cb as TypeListener)(msg, msgtype, source, time); + } +} +const LoggingListeners = new ListenersBase(); + +enum LogTiming { + Sync, + Deferred +} +enum TimeFormat { + Utc, + Unix, + Local +} + +class LoggingConfigurationBase { + + #timing: LogTiming = LogTiming.Sync; + + #timeType: TimeFormat = TimeFormat.Utc; + + get timeFormat() { return this.#timeType } + set timeFormat(data) { this.#timeType = data } + + get logTiming() { return this.#timing; } + set logTiming(data) { this.#timing = data } + +} +const LoggingConfiguration = new LoggingConfigurationBase(); + +export { MessageType, TimeFormat, LogTiming, MessageTypeVisibility, LoggingListeners, LoggingConfiguration }; export default Logging; \ No newline at end of file diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..b598e76 --- /dev/null +++ b/test.ts @@ -0,0 +1,44 @@ +import Logging, { LoggingListeners, LoggingConfiguration, MessageType, MessageTypeVisibility, TimeFormat, LogTiming } from "@proxnet/undead-logging"; +import { setImmediate } from "node:timers/promises"; + +const debug = Deno.args[0] == 'true'; +if (debug) { + LoggingListeners.onmsg('basic', msg => { + console.debug(`[d] ${msg}`); + }); + LoggingListeners.onmsg('type', (msg, type, source, time) => { + console.debug(`[D] M:'${msg}' T:${type} S:'${source}' TM:${time.getTime()}`); + }); +} + +const log = new Logging("Test1"); + +log.i("Hello World!"); +log.visible = false; +log.e('I should not be visible.'); +log.visible = true; +log.e('Now I should be!'); + +const logg = new Logging("Test2"); + +logg.w(`Uh oh..`); +logg.d(`Here's some info that tells you about that warning:`, new Error('Uh oh..')); + +LoggingConfiguration.timeFormat = TimeFormat.Unix; +logg.i(`Unix time!`); + +LoggingConfiguration.timeFormat = TimeFormat.Local; +logg.i(`Process time!`); + +LoggingConfiguration.timeFormat = TimeFormat.Utc; +LoggingConfiguration.logTiming = LogTiming.Deferred; +logg.log(MessageType.Network, "Deferred mode!"); + +LoggingConfiguration.timeFormat = TimeFormat.Local; +setImmediate(() => { + // all should be local mode + MessageTypeVisibility.Error = false; + logg.error("I can't be seen!"); + log.error('Same!'); + log.n("I *can* be seen!"); +}); \ No newline at end of file