js/Logger.js

const fs = require('graceful-fs');
const nwPath = require('path');
const debounce = require('debounce');

/**
 * Redirect the normal log events to a file or array
 * @class
 */
class Logger {
    constructor(options={}) {
        this.options    = options || {};
        this.file       = options.file|| undefined;
        this.output     = [];
        this.baseStack  = this.baseStack||3;
        this.configKey  = options.configKey || "Logger";
        this.config     = options.config || {};
        this.loadConfig();
        if (this.options.truncate) this.truncate()
    }
}

global.__LoggerPooler ||= {}

Logger.getFilePool = function(file) {
    global.__LoggerPooler[file] ||= [];
    return global.__LoggerPooler[file];
}
Logger.resetFilePool = function(file) {
    if (!file) return;
    if (!global.__LoggerPooler[file]) return;
    global.__LoggerPooler[file] = []
}
Logger.appendFilePool = function(file, msg) {
    if (!file) return;
    global.__LoggerPooler[file] ||= [];
    global.__LoggerPooler[file].push(msg);
    return global.__LoggerPooler[file];
}
Logger.consumeFilePool = function(file) {
    if (!file) return;
    const pool = this.getFilePool(file);
    global.__LoggerPooler[file] = [];
    return pool;
}


Logger.prototype.saveConfig = function() {
    this.config = this.config || {};
    localStorage.setItem(this.configKey, JSON.stringify(this.config))
}

Logger.prototype.loadConfig = function() {
    try {
        this.config = JSON.parse(localStorage.getItem(this.configKey))
    } catch (e) {
        this.config = {};
    }

    return this.config;
}

Logger.prototype.setConfig = function(key, value) {
    this.config = this.config || {};
    this.config[key] = value;
    this.saveConfig();
}

Logger.prototype.unsetConfig = function(key, value) {
    this.config = this.config || {};
    delete this.config[key];
    this.saveConfig();
}

Logger.prototype.getConfig = function(key, value) {
    this.config = this.config || {};
    return this.config[key];
}

Logger.prototype.getCallerInfo = function() {
    var err = new Error;
    var stack = err.stack.split("\n");
    if (!stack[this.baseStack]) return "";
    if ( stack[this.baseStack].includes('    at chrome-extension')) {
        stack[this.baseStack] =  stack[this.baseStack].replace("    at ", "");
        return nwPath.basename(stack[this.baseStack]);
    }

    var match = stack[this.baseStack].match(/\((.*?)\)/)
    if (!match) return stack[this.baseStack];
    if (!match[1]) return stack[this.baseStack];

    return nwPath.basename(match[1]);
}

Logger.prototype.getStackCall = function() {
    var err = new Error;
    var stackStr = err.stack;
    stackStr = stackStr.replaceAll(/chrome-extension:\/\/[a-z]+/g, "");
    var stack = stackStr.split("\n").slice(this.baseStack);
    return "\t"+stack.join("\n\t");
}

Logger.prototype.argumentsToArray = function(args) {
	args = Array.prototype.slice.call(args);
	return args;
}
Logger.prototype.getOutput = function() {
    return this.output;
}
Logger.prototype.truncate = async function(file) {
    file = file || this.file;
    if (!file) {
        if (!this.output) return;
        this.output = [];
        return;
    }
    Logger.resetFilePool(file);
    fs.promises.writeFile(file, "Log started at: "+Date()+"\n")
}

// Logger.prototype.commitWriteLog = async function(msg, file) {
//     // should create pooler and debouncer out of this
//     file = file || this.file;
//     if (!file) {
//         if (!Array.isArray(this.output)) return file;
//         this.output.push(msg);
//         // write to output;
//     }
//     await fs.promises.writeFile(file, msg+"\n", {'flag':'a'});
// }

const writePoolToFile = async function(file) {
    let pool = Logger.consumeFilePool(file);
    await fs.promises.writeFile(file, pool.join("\n")+"\n", {'flag':'a'});
}

const debouncedWriteToFile = debounce((file) => writePoolToFile(file), 300);

Logger.prototype.writeLog = async function(msg, file) {
    file = file || this.file;
    if (!file) {
        if (!Array.isArray(this.output)) return;
        this.output.push(msg);
        return;
    }
    Logger.appendFilePool(file, msg+"");
    debouncedWriteToFile(file);
}

Logger.prototype.log = async function() {
    var lineInfo = this.getCallerInfo();
    var msg = ""
    try {
        msg = "[INFO]\t"+this.argumentsToArray(arguments).join(" ")+"\t-->"+lineInfo;
    } catch (e) {
        msg = "--- UNABLE TO CAPTURE ---"
    }
    this.writeLog(msg);
}
Logger.prototype.warn = async function() {
    var lineInfo = this.getCallerInfo();
    var msg = "[WARN]\t"+this.argumentsToArray(arguments).join(" ")+"\t-->"+lineInfo;
    this.writeLog(msg);
    this.writeLog(this.getStackCall())
}
Logger.prototype.error = async function() {
    if (typeof arguments[4] == "object") {
        try {
            if (arguments[4].stack) {
                let msg = "[ERROR]\t"+arguments[4].message+` at ${arguments[1]} ${arguments[2]}:${arguments[3]}`;
                this.writeLog(msg);
                this.writeLog("\t"+arguments[4].stack.split("\n").join("\n\t"));
                return;
            }
        } catch (e) {
            // do nothing
        }
    }
    var lineInfo = this.getCallerInfo();
    let msg = "[ERROR]\t"+this.argumentsToArray(arguments).join(" ")+"\t-->"+lineInfo;
    this.writeLog(msg);
    this.writeLog(this.getStackCall())
}
Logger.prototype.debug = async function() {
    var lineInfo = this.getCallerInfo();
    var msg = "[DEBUG]\t"+this.argumentsToArray(arguments).join(" ")+"\t-->"+lineInfo;
    this.writeLog(msg);
}
Logger.prototype.trace = async function() {
    var lineInfo = this.getCallerInfo();
    var msg = "[TRACE]\t"+this.argumentsToArray(arguments).join(" ")+"\t-->"+lineInfo;
    this.writeLog(msg);
    this.writeLog(this.getStackCall())
}


Logger.prototype.replaceConsole = function() {
    if (this.consoleIsReplaced) return;
    this.baseStack = 4;
    var that = this;
    // preserve the original handler
    this.consoleLog     = console.log;
    this.consoleInfo    = console.info;
    this.consoleDebug   = console.debug;
    this.consoleWarn    = console.warn;
    this.consoleError   = console.error;
    this.consoleTrace   = console.trace;
    
    console.log = function() {
		that.log.apply(that, arguments)
	}
    console.info = function() {
		that.log.apply(that, arguments)
	}
    console.debug = function() {
		that.debug.apply(that, arguments)
	}
    console.warn = function() {
		that.warn.apply(that, arguments)
	}
    console.error = function() {
		that.error.apply(that, arguments)
	}
    console.trace = function() {
		that.trace.apply(that, arguments)
	}


    // listen to the unhandled exceptions
    window.onerror = function (message, file, line, col, error) {
        that.error.apply(that, arguments)
        that.consoleError("Error Occured:", arguments);
        return false;
    };
    window.addEventListener("error", function (e) {
        that.error.apply(that, [e?.error?.message])
        that.consoleError("Error", e?.error?.message, e);

        return false;
    })
    window.addEventListener('unhandledrejection', function (e) {
        that.error.apply(that, [e?.reason?.message])
        that.consoleError("Unhandled rejection: " + e?.reason?.message);
    })

    this.consoleIsReplaced = true;
}

Logger.prototype.restoreConsole = function() {
    console.log     = this.consoleLog;
    console.info    = this.consoleInfo;
    console.debug   = this.consoleDebug;
    console.warn    = this.consoleWarn;
    console.error   = this.consoleError;
    console.trace   = this.consoleTrace;
    this.consoleIsReplaced = false;
}

Logger.prototype.loadState = function(force) {
    try {
        if (force) {
            console.log("Force to replace console");
            this.replaceConsole();
        }
        if (nw.App.manifest.localConfig.logToFile) {
            this.replaceConsole();
        }
        if (this.getConfig("replaceConsole")) {
            this.replaceConsole();
        }
        if (this.getConfig("truncate")) {
            this.truncate();
        }
    } catch (e) {
        // do nothing
    }

}

module.exports = Logger;