// Define specific types for better safety and clarity /** Represents the available logging levels. */ export enum LogLevel { error = 0, warn = 1, info = 2, debug = 3, } /** Structure of the data passed to listener callbacks. */ export interface LogEntry { level: LogLevel; message: string; } /** Type signature for listener callback functions. */ export type LogCallback = (entry: LogEntry) => void; // --- Private interface used internally by the class --- interface Listener { callback: LogCallback; level: LogLevel; // Ensure level is always a valid LogLevel internally } /** * A singleton logger class for Zenid applications. */ class ZenidLogger { // Private array to hold listener objects private listeners: Listener[] = []; private stringify(data: unknown): string { if (typeof data === "string") { return data; } else if (data instanceof Error) { return `Error: ${data.message}\nStack: ${data.stack ?? '(stack trace unavailable)'}`; } else if (data === undefined) { return "undefined"; // Or handle as needed } else { try { return JSON.stringify(data); } catch (e) { return "[Unserializable object]"; } } } /** * Logs a message at the specified level. * Internal method called by public level-specific methods. * @param level The severity level of the log message. * @param message The message or data to log. If not a string, it will be JSON.stringified. * @param data Optional additional data to log. */ public log(level: LogLevel, message: unknown, data?:unknown): void { let messageString: string; messageString = this.stringify(message); // Falsy causes (0, '', false, null) still carry information — only skip when absent. if (data !== undefined) { messageString += ` ${this.stringify(data)}`; } // Iterate over a copy of listeners to handle potential modifications during callback execution const currentListeners = [...this.listeners]; for (const listener of currentListeners) { // Type safety ensures listener.level is valid, but comparing numeric values remains if (level <= listener.level) { try { listener.callback({ level: level, message: messageString }); } catch(e) { // Prevent a faulty listener from crashing the logging system zenidLog.error("Log listener callback failed:", e); // Optionally, you could remove the faulty listener here } } } } /** Logs an error message. */ public error(message: unknown, data?: unknown): void { this.log(LogLevel.error, message, data); } /** Logs a warning message. */ public warn(message: unknown, data?: unknown): void { this.log(LogLevel.warn, message, data); } /** Logs an informational message. */ public info(message: unknown, data?: unknown): void { this.log(LogLevel.info, message, data); } /** Logs a debug message. */ public debug(message: unknown, data?: unknown): void { this.log(LogLevel.debug, message, data); } /** * Adds a listener callback that will be invoked for log messages * at or above the specified level. * @param callback The function to call when a log event occurs. * @param minLogLevel The minimum log level for this listener to receive messages. */ public addListener(callback: LogCallback, minLogLevel?: LogLevel): void { const level = minLogLevel ?? LogLevel.info; // Default level is 'info' // Type assertion is safe here because we validated `level` above this.listeners.push({ callback: callback, level: level as LogLevel }); } /** * Removes all registered listeners. */ public clearListeners(): void { this.listeners = []; // Reassign to a new empty array } // --- Singleton Implementation --- private static instance: ZenidLogger; public static defaultConsoleListener: LogCallback = (entry: LogEntry) => { const output = `[${LogLevel[entry.level].toUpperCase()}] ${entry.message}`; switch (entry.level) { case LogLevel.error: console.error(output); break; case LogLevel.warn: console.warn(output); break; case LogLevel.info: console.info(output); // or console.log(output); break; case LogLevel.debug: console.debug(output); // often hidden by default in devtools, consider console.log break; default: // This case should be unreachable due to LogLevel typing console.log(`[unknown] ${entry.message}`); } }; // Private constructor to prevent direct instantiation private constructor() { // Add the default listener. Let's make it listen to 'debug' and above by default. // You can change 'debug' to 'info' if you want less verbose default logging. this.addListener(ZenidLogger.defaultConsoleListener, LogLevel.debug); } /** * Gets the single instance of the ZenidLogger. * @returns The singleton ZenidLogger instance. */ public static getInstance(): ZenidLogger { if (!ZenidLogger.instance) { ZenidLogger.instance = new ZenidLogger(); } return ZenidLogger.instance; } } // --- Export the Singleton Instance --- // This is the single instance you will import and use throughout your application. export const zenidLog = ZenidLogger.getInstance();