@noxfly/noxus
Version:
Simulate lightweight HTTP-like requests between renderer and main process in Electron applications with MessagePort, with structured and modular design.
431 lines (359 loc) • 13.7 kB
text/typescript
/**
* @copyright 2025 NoxFly
* @license MIT
* @author NoxFly
*/
import * as fs from 'fs';
import * as path from 'path';
/**
* Logger is a utility class for logging messages to the console.
*/
export type LogLevel =
| 'debug'
| 'comment'
| 'log'
| 'info'
| 'warn'
| 'error'
| 'critical'
;
interface FileLogState {
queue: string[];
isWriting: boolean;
}
/**
* Returns a formatted timestamp for logging.
*/
function getPrettyTimestamp(): string {
const now = new Date();
return `${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`
+ ` ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
}
/**
* Generates a log prefix for the console output.
* @param callee - The name of the function or class that is logging the message.
* @param messageType - The type of message being logged (e.g., 'log', 'info', 'warn', 'error', 'debug').
* @param color - The color to use for the log message.
* @returns A formatted string that includes the timestamp, process ID, message type, and callee name.
*/
function getLogPrefix(callee: string, messageType: string, color?: string): string {
const timestamp = getPrettyTimestamp();
const spaces = " ".repeat(10 - messageType.length);
let colReset = Logger.colors.initial;
let colCallee = Logger.colors.yellow;
if(color === undefined) {
color = "";
colReset = "";
colCallee = "";
}
return `${color}[APP] ${process.pid} - ${colReset}`
+ `${timestamp}${spaces}`
+ `${color}${messageType.toUpperCase()}${colReset} `
+ `${colCallee}[${callee}]${colReset}`;
}
/**
* Formats an object into a string representation for logging.
* It converts the object to JSON and adds indentation for readability.
* @param prefix - The prefix to use for the formatted object.
* @param arg - The object to format.
* @returns A formatted string representation of the object, with each line prefixed by the specified prefix.
*/
function formatObject(prefix: string, arg: object, enableColor: boolean = true): string {
const json = JSON.stringify(arg, null, 2);
let colStart = "";
let colLine = "";
let colReset = "";
if(enableColor) {
colStart = Logger.colors.darkGrey;
colLine = Logger.colors.grey;
colReset = Logger.colors.initial;
}
const prefixedJson = json
.split('\n')
.map((line, idx) => idx === 0 ? `${colStart}${line}` : `${prefix} ${colLine}${line}`)
.join('\n') + colReset;
return prefixedJson;
}
/**
* Formats the arguments for logging.
* It colors strings and formats objects with indentation.
* This function is used to prepare the arguments for console output.
* @param prefix - The prefix to use for the formatted arguments.
* @param args - The arguments to format.
* @param color - The color to use for the formatted arguments.
* @returns An array of formatted arguments, where strings are colored and objects are formatted with indentation.
*/
function formattedArgs(prefix: string, args: any[], color?: string): any[] {
let colReset = Logger.colors.initial;
if(color === undefined) {
color = "";
colReset = "";
}
return args.map(arg => {
if(typeof arg === "string") {
return `${color}${arg}${colReset}`;
}
else if(typeof arg === "object") {
return formatObject(prefix, arg, color !== "");
}
return arg;
});
}
/**
* Gets the name of the caller function or class from the stack trace.
* This function is used to determine the context of the log message.
* @returns The name of the caller function or class.
*/
function getCallee(): string {
const stack = new Error().stack?.split('\n') ?? [];
const caller = stack[3]
?.trim()
.match(/at (.+?)(?:\..+)? .+$/)
?.[1]
?.replace("Object", "")
.replace(/^_/, "")
|| "App";
return caller;
}
/**
* Checks if the current log level allows logging the specified level.
* This function compares the current log level with the specified level to determine if logging should occur.
* @param level - The log level to check.
* @returns A boolean indicating whether the log level is enabled.
*/
function canLog(level: LogLevel): boolean {
return logLevels.has(level);
}
/**
* Writes a log message to a file asynchronously to avoid blocking the event loop.
* It batches messages if writing is already in progress.
* @param filepath - The path to the log file.
*/
function processLogQueue(filepath: string): void {
const state = fileStates.get(filepath);
if(!state || state.isWriting || state.queue.length === 0) {
return;
}
state.isWriting = true;
// Optimization: Grab all pending messages to write in one go
const messagesToWrite = state.queue.join('\n') + '\n';
state.queue = []; // Clear the queue immediately
const dir = path.dirname(filepath);
// Using async IO to allow other operations
fs.mkdir(dir, { recursive: true }, (err) => {
if(err) {
console.error(`[Logger] Failed to create directory ${dir}`, err);
state.isWriting = false;
return;
}
fs.appendFile(filepath, messagesToWrite, { encoding: "utf-8" }, (err) => {
state.isWriting = false;
if(err) {
console.error(`[Logger] Failed to write log to ${filepath}`, err);
}
// If new messages arrived while we were writing, process them now
if(state.queue.length > 0) {
processLogQueue(filepath);
}
});
});
}
/**
* Adds a message to the file queue and triggers processing.
*/
function enqueue(filepath: string, message: string): void {
if(!fileStates.has(filepath)) {
fileStates.set(filepath, { queue: [], isWriting: false });
}
const state = fileStates.get(filepath)!;
state.queue.push(message);
processLogQueue(filepath);
}
/**
*
*/
function output(level: LogLevel, args: any[]): void {
if(!canLog(level)) {
return;
}
const callee = getCallee();
{
const prefix = getLogPrefix(callee, level, logLevelColors[level]);
const data = formattedArgs(prefix, args, logLevelColors[level]);
logLevelChannel[level](prefix, ...data);
}
{
const prefix = getLogPrefix(callee, level);
const data = formattedArgs(prefix, args);
const filepath = fileSettings.get(level)?.filepath;
if(filepath) {
const message = prefix + " " + data.join(" ").replace(/\x1b\[[0-9;]*m/g, ''); // Remove ANSI codes
enqueue(filepath, message);
}
}
}
export namespace Logger {
/**
* Sets the log level for the logger.
* This function allows you to change the log level dynamically at runtime.
* This won't affect the startup logs.
*
* If the parameter is a single LogLevel, all log levels with equal or higher severity will be enabled.
* If the parameter is an array of LogLevels, only the specified levels will be enabled.
*
* @param level Sets the log level for the logger.
*/
export function setLogLevel(level: LogLevel | LogLevel[]): void {
logLevels.clear();
if(Array.isArray(level)) {
for(const lvl of level) {
logLevels.add(lvl);
}
}
else {
const targetRank = logLevelRank[level];
for(const [lvl, rank] of Object.entries(logLevelRank) as [LogLevel, number][]) {
if(rank >= targetRank) {
logLevels.add(lvl);
}
}
}
}
/**
* Logs a message to the console with log level LOG.
* This function formats the message with a timestamp, process ID, and the name of the caller function or class.
* It uses different colors for different log levels to enhance readability.
* @param args The arguments to log.
*/
export function log(...args: any[]): void {
output("log", args);
}
/**
* Logs a message to the console with log level INFO.
* This function formats the message with a timestamp, process ID, and the name of the caller function or class.
* It uses different colors for different log levels to enhance readability.
* @param args The arguments to log.
*/
export function info(...args: any[]): void {
output("info", args);
}
/**
* Logs a message to the console with log level WARN.
* This function formats the message with a timestamp, process ID, and the name of the caller function or class.
* It uses different colors for different log levels to enhance readability.
* @param args The arguments to log.
*/
export function warn(...args: any[]): void {
output("warn", args);
}
/**
* Logs a message to the console with log level ERROR.
* This function formats the message with a timestamp, process ID, and the name of the caller function or class.
* It uses different colors for different log levels to enhance readability.
* @param args The arguments to log.
*/
export function error(...args: any[]): void {
output("error", args);
}
/**
* Logs a message to the console with log level ERROR and a grey color scheme.
*/
export function errorStack(...args: any[]): void {
output("error", args);
}
/**
* Logs a message to the console with log level DEBUG.
* This function formats the message with a timestamp, process ID, and the name of the caller function or class.
* It uses different colors for different log levels to enhance readability.
* @param args The arguments to log.
*/
export function debug(...args: any[]): void {
output("debug", args);
}
/**
* Logs a message to the console with log level COMMENT.
* This function formats the message with a timestamp, process ID, and the name of the caller function or class.
* It uses different colors for different log levels to enhance readability.
* @param args The arguments to log.
*/
export function comment(...args: any[]): void {
output("comment", args);
}
/**
* Logs a message to the console with log level CRITICAL.
* This function formats the message with a timestamp, process ID, and the name of the caller function or class.
* It uses different colors for different log levels to enhance readability.
* @param args The arguments to log.
*/
export function critical(...args: any[]): void {
output("critical", args);
}
/**
* Enables logging to a file output for the specified log levels.
* @param filepath The path to the log file.
* @param levels The log levels to enable file logging for. Defaults to all levels.
*/
export function enableFileLogging(filepath: string, levels: LogLevel[] = ["debug", "comment", "log", "info", "warn", "error", "critical"]): void {
for(const level of levels) {
fileSettings.set(level, { filepath });
}
}
/**
* Disables logging to a file output for the specified log levels.
* @param levels The log levels to disable file logging for. Defaults to all levels.
*/
export function disableFileLogging(levels: LogLevel[] = ["debug", "comment", "log", "info", "warn", "error", "critical"]): void {
for(const level of levels) {
fileSettings.delete(level);
}
}
export const colors = {
black: "\x1b[0;30m",
grey: "\x1b[0;37m",
red: "\x1b[0;31m",
green: "\x1b[0;32m",
brown: "\x1b[0;33m",
blue: "\x1b[0;34m",
purple: "\x1b[0;35m",
darkGrey: "\x1b[1;30m",
lightRed: "\x1b[1;31m",
lightGreen: "\x1b[1;32m",
yellow: "\x1b[1;33m",
lightBlue: "\x1b[1;34m",
magenta: "\x1b[1;35m",
cyan: "\x1b[1;36m",
white: "\x1b[1;37m",
initial: "\x1b[0m"
};
}
const fileSettings: Map<LogLevel, { filepath: string }> = new Map();
const fileStates: Map<string, FileLogState> = new Map(); // filepath -> state
const logLevels: Set<LogLevel> = new Set();
const logLevelRank: Record<LogLevel, number> = {
debug: 0,
comment: 1,
log: 2,
info: 3,
warn: 4,
error: 5,
critical: 6,
};
const logLevelColors: Record<LogLevel, string> = {
debug: Logger.colors.purple,
comment: Logger.colors.grey,
log: Logger.colors.green,
info: Logger.colors.blue,
warn: Logger.colors.brown,
error: Logger.colors.red,
critical: Logger.colors.lightRed,
};
const logLevelChannel: Record<LogLevel, (message?: any, ...optionalParams: any[]) => void> = {
debug: console.debug,
comment: console.debug,
log: console.log,
info: console.info,
warn: console.warn,
error: console.error,
critical: console.error,
};
Logger.setLogLevel("debug");