Source: codemelted.js

// @ts-check
/**
 * @file Implementation of the codemelted.js cross platform module.
 * @author Mark Shaffer
 * @version 0.0.0
 * @license MIT
 */

/**
 * Collection of global namespaces implementing the CodeMelted - Developer
 * cross platform use case modules. Meant for Deno / Web Browser runtime
 * environments. In support of the global namespaces include a set of utility
 * classes that are the result of a namespace function call.
 *
 * <p>The identified classes must be imported. The documented types are global
 * like the namespaces.</p>
 * @module codemelted
 */

// ----------------------------------------------------------------------------
// [async use case] -----------------------------------------------------------
// ----------------------------------------------------------------------------

// TODO: Deno process.

/**
 * Data that is received via the
 * @typedef {object} CAsyncData
 * @property {string} type "data" or "error".
 * @property {any} data The actual data associated with the type.
 */

/**
 * Message handler that receives a JSON object with two fields. "type" which
 * equals either "error" or "data". Then "data" which contains the actual
 * data received.
 * @callback CAsyncWorkerListener
 * @param {CAsyncData} data The data received via the wrapped worker.
 * @returns {void}
 */

/**
 * The task to run as part of the different module async functions.
 * @callback CAsyncTask
 * @param {any} [data] The data to process on the backend.
 * @returns {any} The calculated answer to the task if any.
 */

/**
 * Definition class for a dedicated FIFO thread separated worker /
 * process. Data is queued to this worker via CAsyncWorker.postMessage
 * method and terminated via the CAsyncWorker.terminate method.
 */
export class CAsyncWorker {
    /**
     * Identifies the constructed worker.
     * @type {Worker | undefined}
     */
    #worker = undefined;

    /**
     * Posts dynamic data to the background worker.
     * @param {any} data The data to transmit.
     * @returns {void}
     */
    postMessage(data) {
        this.#worker?.postMessage(data);
    }

    /**
     * Terminates the dedicated background worker.
     * @returns {void}
     */
    terminate() {
        this.#worker?.terminate();
    }

    /**
     * Constructor for the worker.
     * @param {CAsyncWorkerListener} onDataReceived The callback for received data.
     * @param {string} url The URL of the dedicated worker.
     */
    constructor(onDataReceived, url) {
        this.#worker = new Worker(url, {type: "module"});
        this.#worker.onerror = (e) => {
            onDataReceived({type: "error", data: e.error});
        };
        this.#worker.onmessage = (e) => {
            onDataReceived({type: "data", data: e.data});
        }
        this.#worker.onmessageerror = (e) => {
            onDataReceived({type: "error", data: e.data});
        }
    }
}

/**
 * Holds a timer kicked off by the codemelted_async.timer function.
 */
export class CTimer {
    /**
     * The id of the kicked-off timer.
     * @type {number}
     */
    #id;

    /**
     * Stops the running timer.
     */
    stop() {
        clearInterval(this.#id);
    }

    /**
     * Constructor for the object.
     * @param {number} id The interval timer id.
     */
    constructor(id) {
        this.#id = id;
    }
}

/**
 * Implements the Async IO API collecting ways of properly doing asynchronous
 * programming within a Deno or Web Browser runtime.
 * @namespace codemelted.codemelted_async
 */
globalThis["codemelted_async"] = Object.freeze({
    /**
     * Identifies the number of processors to facilitate dedicated workers.
     * @memberof codemelted.codemelted_async
     * @readonly
     * @type {number}
     */
    get hardwareConcurrency() {
        return globalThis.navigator.hardwareConcurrency;
    },

    /**
     * Will sleep an asynchronous task for the specified delay in
     * milliseconds.
     * @memberof codemelted.codemelted_async
     * @param {object} params The named parameters.
     * @param {number} params.delay The specified delay in milliseconds.
     * @returns {Promise<void>}
     */
    sleep: function({delay}) {
        return new Promise((resolve, reject) => {
            try {
                codemelted_json.tryType({type: "number", data: delay});
                if (delay < 0) {
                    throw new SyntaxError("delay must be greater than 0");
                }
                setTimeout(() => resolve(), delay);
            } catch (err) {
                reject(err)
            }
        });
    },

    /**
     * Will process a one off asynchronous task on the main thread.
     * @memberof codemelted.codemelted_async
     * @param {object} params The named parameters.
     * @param {CAsyncTask} params.task The task to run.
     * @param {any} params.data The data to pass to the task.
     * @param {number} params.delay The specified delay in milliseconds to
     * schedule to run the task.
     * @returns {Promise<any>} The calculated result or Error object if a
     * failure occurred.
     */
    task: function({task, data, delay}) {
        return new Promise((resolve, reject) => {
            try {
                codemelted_json.tryType({type: "function",
                    data: task, count: 1});
                codemelted_json.tryType({type: "number", data: delay});
                if (delay < 0) {
                    throw new SyntaxError("delay must be greater than 0");
                }
                setTimeout(() => {
                    const answer = task(data);
                    resolve(answer);
                }, delay);
            } catch (err) {
                reject(err);
            }
        });
    },

    /**
     * Kicks off a timer to schedule tasks on the thread for which it is
     * created calling the task on the interval specified in milliseconds.
     * @memberof codemelted.codemelted_async
     * @param {object} params The named parameters.
     * @param {CAsyncTask} params.task The task to repeat.
     * @param {number} params.interval The interval to repeat the task in
     * milliseconds.
     * @returns {CTimer} The created timer.
     */
    timer: function({task, interval}) {
        codemelted_json.tryType({type: "function", data: task, count: 1});
        codemelted_json.tryType({type: "number", data: interval});
        if (interval < 0) {
            throw new SyntaxError("delay must be greater than 0");
        }
        const id = setInterval(() => task(), interval);
        return new CTimer(id);
    },

    /**
     * Creates the CAsyncWorker dedicated FIFO worker for background work to
     * the client.
     * @memberof codemelted.codemelted_async
     * @param {object} params The named parameters.
     * @param {CAsyncWorkerListener} params.onDataReceived The listener
     * for received data from the worker.
     * @param {string} params.workerUrl The url to the dedicated worker.
     * @returns {CAsyncWorker} The worker created from this namespace.
     */
    worker: function({onDataReceived, workerUrl}) {
        codemelted_json.checkType({type: "function",
            data: onDataReceived, count: 1});
        codemelted_json.checkType({type: "string", data: workerUrl});
        return new CAsyncWorker(onDataReceived, workerUrl);
    }
});

// ----------------------------------------------------------------------------
// [audio use case] -----------------------------------------------------------
// ----------------------------------------------------------------------------

/**
 * UNDER DEVELOPMENT
 * @namespace codemelted.codemelted_audio
 */
globalThis["codemelted_audio"] = Object.freeze({
    //
});

// ----------------------------------------------------------------------------
// [console use case] ---------------------------------------------------------
// ----------------------------------------------------------------------------

/**
 * Provides the console use case function to gather data via a
 * terminal. The actions correspond to the type of input / output
 * that will be interacted with via STDIN and STDOUT.
 * @namespace codemelted.codemelted_console
 */
globalThis["codemelted_console"] = Object.freeze({
    /**
     * Alerts a message to STDOUT with a [Enter] to halt execution.
     * @memberof codemelted.codemelted_console
     * @param {object} params The named parameters.
     * @param {string} params.message The message to display to STDOUT.
     * @returns {void}
     */
    alert: function({message}) {
        codemelted_runtime.tryDeno();
        codemelted_json.tryType({type: "string", data: message});
        globalThis.alert(message);
    },

    /**
     * Prompts a [y/N] to STDOUT with the message as a question.
     * @memberof codemelted.codemelted_console
     * @param {object} params The named parameters.
     * @param {string} params.message The message to display to STDOUT.
     * @returns {boolean} true if y selected, false otherwise.
     */
    confirm: function({message}) {
        codemelted_runtime.tryDeno();
        codemelted_json.tryType({type: "string", data: message});
        return globalThis.confirm(message);
    },

    /**
     * Prompts a list of choices for the user to select from.
     * @memberof codemelted.codemelted_console
     * @param {object} params The named parameters.
     * @param {string} params.message The message to display to STDOUT.
     * @param {string[]} params.choices The choices to select from.
     * @returns {number} The index of the chosen item.
     */
    choose: function({message, choices}) {
        codemelted_runtime.tryDeno();
        codemelted_json.tryType({type: "string", data: message});
        codemelted_json.tryType({type: Array, data: choices});

        let answer = -1;
        do {
            globalThis.console.log();
            globalThis.console.log("-".repeat(message.length));
            globalThis.console.log(message);
            globalThis.console.log("-".repeat(message.length));
            choices.forEach((v, index) => {
                globalThis.console.log(`${index}. ${v}`);
            });
            answer = parseInt(globalThis.prompt(
                `Make a Selection [0 = ${choices.length - 1}]:`
            ) ?? "-1");
            if (isNaN(answer) || answer >= choices.length) {
                console.log(
                    "ERROR: Entered value was invalid. " +
                    "Please try again."
                );
                answer = -1;
            }
        } while (answer === -1);
        return answer;
    },

    /**
     * Prompts for a password not showing the text typed via STDIN.
     * @memberof codemelted.codemelted_console
     * @param {object} params The named parameters.
     * @param {string} params.message The message to display to STDOUT.
     * @returns {string} The typed password.
     */
    password: function({message}) {
        // Setup our variables
        const deno = codemelted_runtime.tryDeno();
        codemelted_json.tryType({type: "string", data: message});
        const buf = new Uint8Array(1);
        const decoder = new TextDecoder();
        let answer = "";
        let done = false;

        // Go prompt for the password
        deno.stdin.setRaw(true);
        globalThis.console.log(`${message}:`);
        do {
            const nread = deno.stdin.readSync(buf);
            if (nread === null) {
                done = true;
            } else if (buf && buf[0] === 0x03) {
                done = true;
            } else if (buf && buf[0] === 13) {
                done = true;
            }
            const text = decoder.decode(buf.subarray(0, nread ?? undefined));
            answer += text;
        } while (!done);
        deno.stdin.setRaw(false);
        return answer;
    },

    /**
     * Prompts to STDOUT and returns the typed message via STDIN.
     * @memberof codemelted.codemelted_console
     * @param {object} params The named parameters.
     * @param {string} params.message The message to display to STDOUT.
     * @returns {string?} The result typed.
     */
    prompt: function({message}) {
        codemelted_runtime.tryDeno();
        codemelted_json.tryType({type: "string", data: message});
        return globalThis.prompt(message);
    },

    /**
     * Write a message to STDOUT.
     * @memberof codemelted.codemelted_console
     * @param {object} params The named parameters.
     * @param {string} params.message The message to display to STDOUT.
     * @returns {void}
     */
    writeln: function({message}) {
        codemelted_runtime.tryDeno();
        codemelted_json.tryType({type: "string", data: message});
        globalThis.console.log(message);
    },
});

// ----------------------------------------------------------------------------
// [database use case] --------------------------------------------------------
// ----------------------------------------------------------------------------

/**
 * UNDER DEVELOPMENT
 * @namespace codemelted.codemelted_database
 */
globalThis["codemelted_database"] = Object.freeze({
    //
});

// ----------------------------------------------------------------------------
// [disk use case] ------------------------------------------------------------
// ----------------------------------------------------------------------------

// TODO: CFileBroker for reading / writing data to a file.

/**
 * Identifies file information based on a codemelted_disk.ls call.
 */
export class CFileInfo {
    /** @type {string} */
    #filename;

    /**
     * The name of the file on disk.
     * @readonly
     * @type {string}
     */
    get filename() { return this.#filename; }

    /** @type {boolean} */
    #isDirectory;

    /**
     * Is a directory or not.
     * @readonly
     * @type {boolean}
     */
    get isDirectory() { return this.#isDirectory; }

    /** @type {boolean} */
    #isFile;

    /**
     * Is a file or not.
     * @readonly
     * @type {boolean}
     */
    get isFile() { return this.#isFile; }

    /** @type {boolean} */
    #isSymLink;

     /**
      * If symbolic link or not.
     * @readonly
     * @type {boolean}
     */
     get isSymLink() { return this.#isSymLink; }

    /** @type {number} */
    #size;

    /**
     * Size of file on disk.
     * @readonly
     * @type {number}
     */
    get size() { return this.#size; }

    /**
     * Constructor for the class.
     * @param {string} filename The filename to include full path.
     * @param {boolean} isDirectory true if directory, false otherwise.
     * @param {boolean} isFile true if file, false otherwise.
     * @param {boolean} isSymLink true if symbolic link, false otherwise.
     * @param {number} size Size of the file in bytes on disk.
     */
    constructor(filename, isDirectory, isFile, isSymLink, size) {
        this.#filename = filename;
        this.#isDirectory = isDirectory;
        this.#isFile = isFile;
        this.#isSymLink = isSymLink;
        this.#size = size;
    }
}

/**
 * Provides the ability to manage items on disk. This includes file
 * manipulation, reading / writing files, and opening files for additional
 * work. Only supported on the Deno runtime and will throw a SyntaxError if
 * attempting to run within a Web Browser.
 * @namespace codemelted.codemelted_disk
 */
globalThis["codemelted_disk"] = Object.freeze({
    /**
     * Copies a file / directory from its currently source location to the
     * specified destination.
     * @memberof codemelted.codemelted_disk
     * @param {object} params The named parameters.
     * @param {string} params.src The source item to copy.
     * @param {string} params.dest The destination of where to copy the item.
     * @returns {boolean} true if carried out, false otherwise.
     */
    cp: function({src, dest}) {
        try {
            const deno = codemelted_runtime.tryDeno();
            codemelted_json.tryType({type: "string", data: src});
            codemelted_json.tryType({type: "string", data: dest});
            deno.copyFileSync(src, dest);
            return true;
        } catch (err) {
            if (err instanceof SyntaxError) {
                throw err;
            }
            return false;
        }
    },

    /**
     * List the files in the specified source location.
     * @memberof codemelted.codemelted_disk
     * @param {object} params The named parameters.
     * @param {string} params.path The path to list.
     * @returns {CFileInfo[]?} Array of files found.
     */
    ls: function({path}) {
        try {
            const deno = codemelted_runtime.tryDeno();
            codemelted_json.tryType({type: "string", data: path});
            const dirList = deno.readDirSync(path);
            const fileInfoList = [];
            for (const dirEntry of dirList) {
                const fileInfo = deno.lstatSync(
                    `${path}/${dirEntry.name}`
                );
                fileInfoList.push(new CFileInfo(
                    dirEntry.name,
                    fileInfo.isDirectory,
                    fileInfo.isFile,
                    fileInfo.isSymlink,
                    fileInfo.size
                ));
            }
            return fileInfoList;
        } catch (err) {
            if (err instanceof SyntaxError) {
                throw err;
            }
            return null;
        }
    },

    /**
     * Makes a directory at the specified location.
     * @memberof codemelted.codemelted_disk
     * @param {object} params The named parameters.
     * @param {string} params.path The source item to create.
     * @returns {boolean}
     */
    mkdir: function({path}) {
        try {
            const deno = codemelted_runtime.tryDeno();
            codemelted_json.tryType({type: "string", data: path});
            deno.mkdirSync(path);
            return true;
        } catch (err) {
            if (err instanceof SyntaxError) {
                throw err;
            }
            return false;
        }
    },

    /**
     * Identifies the path separator for files on disk.
     * @memberof codemelted.codemelted_disk
     * @readonly
     * @type {string}
     */
    get pathSeparator() {
        return codemelted_runtime.osName === "windows"
            ? "\\"
            : "/"
    },

    /**
     * Reads a file to disk.
     * @memberof codemelted.codemelted_disk
     * @param {object} params The named parameters.
     * @param {string} params.filename The filename to write.
     * @param {boolean} params.isTextFile true if text file, false if binary.
     * @returns {string | Uint8Array | null}
     */
    readFile: function({filename, isTextFile}) {
        try {
            const deno = codemelted_runtime.tryDeno();
            codemelted_json.tryType({type: "string", data: filename});
            codemelted_json.tryType({type: "boolean", data: isTextFile});
            if (isTextFile) {
                return deno.readTextFileSync(filename);
            } else {
                return deno.readFileSync(filename);
            }
        } catch (err) {
            if (err instanceof SyntaxError) {
                throw err;
            }
            return null;
        }
    },

    /**
     * Removes a file or directory at the specified location.
     * @memberof codemelted.codemelted_disk
     * @param {object} params The named parameters.
     * @param {string} params.path The source item to create.
     * @returns {boolean}
     */
    rm: function({path}) {
        try {
            const deno = codemelted_runtime.tryDeno();
            codemelted_json.tryType({type: "string", data: path});
            deno.removeSync(path);
            return true;
        } catch (err) {
            if (err instanceof SyntaxError) {
                throw err;
            }
            return false;
        }
    },

    /**
     * Identifies the temp directory on disk. Null is returned if on
     * web browser.
     * @memberof codemelted.codemelted_disk
     * @readonly
     * @type {string?}
     */
    get tempPath() {
        return codemelted_runtime.isDeno
            ? codemelted_runtime.tryDeno().env.get("TMPDIR") ||
              codemelted_runtime.tryDeno().env.get("TMP") ||
              codemelted_runtime.tryDeno().env.get("TEMP") ||
              "/tmp"
            : null;
    },

    /**
     * Writes a file to disk.
     * @memberof codemelted.codemelted_disk
     * @param {object} params The named parameters.
     * @param {string} params.filename The filename to write.
     * @param {string | Uint8Array} params.data The data to write.
     * @param {boolean} params.append true to append the data, false
     * to overwrite the file.
     * @returns {boolean} true if successful, false otherwise.
     */
    writeFile: function({filename, data, append}) {
        try {
            const deno = codemelted_runtime.tryDeno();
            codemelted_json.tryType({type: "string", data: filename});
            codemelted_json.tryType({type: "boolean", data: append});
            if (codemelted_json.checkType({type: "string", data: data})) {
                // @ts-ignore: checked it is string previous line.
                deno.writeTextFileSync(filename, data, append)
            } else if (codemelted_json.checkType({
                    type: Uint8Array, data: data})) {
                // @ts-ignore: checked it is string previous line.
                deno.writeFileSync(filename, data, append);
            } else {
                throw SyntaxError("data is not of an expected type");
            }
            return true;
        } catch (err) {
            if (err instanceof SyntaxError) {
                throw err;
            }
            return false;
        }
    },
});

// ----------------------------------------------------------------------------
// [firebase use case] --------------------------------------------------------
// ----------------------------------------------------------------------------

/**
 * UNDER DEVELOPMENT
 * @namespace codemelted.codemelted_firebase
 */
globalThis["codemelted_firebase"] = Object.freeze({
    //
});

// ----------------------------------------------------------------------------
// [game use case] ------------------------------------------------------------
// ----------------------------------------------------------------------------

/**
 * UNDER DEVELOPMENT
 * @namespace codemelted.codemelted_game
 */
globalThis["codemelted_game"] = Object.freeze({
    //
});

// ----------------------------------------------------------------------------
// [hardware use case] --------------------------------------------------------
// ----------------------------------------------------------------------------

/**
 * UNDER DEVELOPMENT
 * @namespace codemelted.codemelted_hardware
 */
globalThis["codemelted_hardware"] = Object.freeze({
    //
});

// ----------------------------------------------------------------------------
// [json use case] ------------------------------------------------------------
// ----------------------------------------------------------------------------

// TODO: CArray object, CObject object like flutter.

/**
 * Defines a set of utility methods for performing data conversions for JSON
 * along with data validation.
 * @namespace codemelted.codemelted_json
 */
globalThis["codemelted_json"] = Object.freeze({
    /**
     * Determines if the specified object has the specified property.
     * @memberof codemelted.codemelted_json
     * @param {object} params The named parameters.
     * @param {object} params.obj The object to check for the key.
     * @param {string} params.key The property to check for.
     * @returns {boolean} true if property was found, false otherwise.
     */
    checkHasProperty: function({ obj, key }) {
        return Object.hasOwn(obj, key);
    },

    /**
     * Utility to check parameters of a function to ensure they are of an
     * expected type.
     * @memberof codemelted.codemelted_json
     * @param {object} params The named parameters.
     * @param {string | any} params.type The specified type to check the data
     * against.
     * @param {any} params.data The parameter to be checked.
     * @param {number} [params.count] Checks the data parameter function
     * signature to ensure the appropriate number of parameters are
     * specified.
     * @returns {boolean} true if it meets the expectations, false otherwise.
     */
    checkType: function({ type, data, count = undefined }) {
        const isExpectedType = typeof type !== "string"
            ? (data instanceof type)
            // deno-lint-ignore valid-typeof
            : typeof data === type;
        return typeof count === "number"
            ? isExpectedType && data.length === count
            : isExpectedType;
    },

    /**
     * Checks for a valid URL.
     * @memberof codemelted.codemelted_json
     * @param {object} params The named parameters.
     * @param {string} params.data String to parse to see if it is a valid URL.
     * @returns {boolean} true if valid, false otherwise.
     */
    checkValidUrl: function({ data }) {
        let url = undefined;
        try {
            url = new URL(data);
        }
        catch (_ex) {
            url = undefined;
        }
        return codemelted_json.checkType({ type: URL, data: url });
    },

    /**
     * Converts a string to a JavaScript object.
     * @memberof codemelted.codemelted_json
     * @param {object} params The named parameters.
     * @param {string} params.data The data to parse.
     * @returns {object | null} Object or null if the parse failed.
     */
    jsonParse: function({ data }) {
        try {
            return JSON.parse(data);
        }
        catch (_ex) {
            return null;
        }
    },

    /**
     * Converts a JavaScript object into a string.
     * @memberof codemelted.codemelted_json
     * @param {object} params The named parameters.
     * @param {object} params.data An object with valid JSON attributes.
     * @returns {string | null} The string representation or null if the
     * stringify failed.
     */
    jsonStringify: function({ data }) {
        try {
            return JSON.stringify(data);
        }
        catch (_ex) {
            return null;
        }
    },

    /**
     * Determines if the specified object has the specified property.
     * @memberof codemelted.codemelted_json
     * @param {object} params The named parameters.
     * @param {object} params.obj The object to check for the key.
     * @param {string} params.key The property to check for.
     * @returns {void}
     * @throws {SyntaxError} if the property is not found.
     */
    tryHasProperty: function({ obj, key }) {
        if (!this.checkHasProperty({ obj: obj, key: key })) {
            throw new SyntaxError(`${obj} does not have property ${key}`);
        }
    },

    /**
     * Utility to check parameters of a function to ensure they are of an
     * expected type.
     * @memberof codemelted.codemelted_json
     * @param {object} params The named parameters.
     * @param {string | any} params.type he specified type to check the data
     * against.
     * @param {any} params.data The parameter to be checked.
     * @param {number} [params.count] Checks the data parameter function
     * signature to ensure the appropriate number of parameters are
     * specified.
     * @returns {void}
     * @throws {SyntaxError} if the type was not as expected
     */
    tryType: function({ type, data, count = undefined }) {
        if (!this.checkType({ type: type, data: data, count: count })) {
            throw new SyntaxError(`${data} parameter is not of type ${type} or does not ` +
                `contain expected ${count} for function`);
        }
    },

    /**
     * Checks for a valid URL.
     * @memberof codemelted.codemelted_json
     * @param {object} params The named parameters.
     * @param {string} params.data String to parse to see if it is a valid URL.
     * @returns {void}
     * @throws {SyntaxError} If v is not a valid url.
     */
    tryValidUrl: function({ data }) {
        if (!this.checkValidUrl({ data: data })) {
            throw new SyntaxError(`'${data}' is not a valid url`);
        }
    },
});

// ----------------------------------------------------------------------------
// [logger use case] ----------------------------------------------------------
// ----------------------------------------------------------------------------

/**
 * Wraps the handled logged event for later processing.
 */
export class CLogRecord {
    /** @type {Date} */
    #time;
    /**
     * The time of the event.
     * @type {Date}
     */
    get time() { return this.#time; }

    /** @type {number} */
    #level;
    /**
     * The log level of the event.
     * @type {number}
     */
    get level() { return this.#level; }

    /** @type {any}*/
    #data;
    /**
     * The data associated with the event.
     * @readonly
     * @type {any}
     */
    get data() { return this.#data; }

    /** @type {string?} */
    #stackTrace = null;
    /**
     * A stack trace associated with the data.
     * @readonly
     * @type {string?}
     */
    get stackTrace() { return this.#stackTrace; }

    /**
     * Formatted log record
     * @returns {string}
     */
    toString() {
        const levelStr = this.level === 0
            ? "debug"
            : this.level === 1
                ? "info"
                : this.level === 2
                    ? "warning"
                    : "error";
        let msg = `${this.time.toISOString()} [${levelStr}]: ${this.data}`;
        msg = this.stackTrace != null ? `${msg}\n${this.stackTrace}` : msg;
        return msg;
    }

    /**
     * Constructor for the class
     * @param {number} level The level of the logged event.
     * @param {any} data The data associated with it.
     */
    constructor(level, data) {
        this.#time = new Date();
        this.#level = level;
        this.#data = data;
        if (codemelted_json.checkType({type: Error, data: data})) {
            this.#stackTrace = data.stack;
        }
    }
}

/**
 * Handler to support the codemelted_flutter module for post processing of a
 * logged event.
 * @callback COnLogEventListener
 * @param {CLogRecord} record The log record that was received.
 * @returns {void}
 */

/**
 * The currently set module log level.
 * @type {number}
 * @private
 */
let _logLevel = 1; // The information setting

/**
 * The module holder for the codemelted_logger.onLogEvent property.
 * @type {COnLogEventListener?}
 * @private
 */
let _onLogEventHandler = null;

/**
 * Sets up the logging facility for the module. Events logged are sent to
 * the runtime console with the [onLogEvent] handling more advanced
 * logging if set.
 * @namespace codemelted.codemelted_logger
 */
globalThis["codemelted_logger"] = Object.freeze({
    /**
     * debug log level.
     * @memberof codemelted.codemelted_logger
     * @readonly
     * @type {number}
     */
    get debugLevel() { return 0; },

    /**
     * info log level.
     * @memberof codemelted.codemelted_logger
     * @readonly
     * @type {number}
     */
    get infoLevel() { return 1; },

    /**
     * warning log level.
     * @memberof codemelted.codemelted_logger
     * @readonly
     * @type {number}
     */
    get warningLevel() { return 2; },

    /**
     * error log level.
     * @memberof codemelted.codemelted_logger
     * @readonly
     * @type {number}
     */
    get errorLevel() { return 3; },

    /**
     * off log level.
     * @memberof codemelted.codemelted_logger
     * @readonly
     * @type {number}
     */
    get offLevel() { return 4; },

    /**
     * The current log level for the module as set by the namespaced
     * constants.
     * @memberof codemelted.codemelted_logger
     * @type {number}
     */
    set level(v) {
        codemelted_json.checkType({type: "number", data: v});
        if (v < this.debugLevel || v > this.offLevel) {
            throw new SyntaxError("level not set in valid range");
        }
        _logLevel = v;
    },
    get level() { return _logLevel; },

    /**
     * Sets / gets the listener for post processing of log events once they
     * are handled by the module.
     * @memberof codemelted.codemelted_logger
     * @type {COnLogEventListener?}
     */
    set onLogEvent(v) {
        if (v) {
            codemelted_json.checkType({type: "function", data: v, count: 1});
        }
        _onLogEventHandler = v == undefined ? null : v;
    },
    get onLogEvent() { return _onLogEventHandler; },

    /**
     * Will log debug level messages via the module.
     * @memberof codemelted.codemelted_logger
     * @param {object} params The named parameters
     * @param {any} params.data The data to log.
     * @returns {void}
     */
    debug: function({data}) {
        if (this.level <= this.debugLevel && this.level != this.offLevel) {
            const record = new CLogRecord(this.debugLevel, data);
            console.log(data.toString());
            if (this.onLogEvent) {
                this.onLogEvent(record);
            }
        }
    },

    /**
     * Will log info level messages via the module.
     * @memberof codemelted.codemelted_logger
     * @param {object} params The named parameters
     * @param {any} params.data The data to log.
     * @returns {void}
     */
    info: function({data}) {
        if (this.level <= this.infoLevel && this.level != this.offLevel) {
            const record = new CLogRecord(this.infoLevel, data);
            console.info(data.toString());
            if (this.onLogEvent) {
                this.onLogEvent(record);
            }
        }
    },

    /**
     * Will log warning level messages via the module.
     * @memberof codemelted.codemelted_logger
     * @param {object} params The named parameters
     * @param {any} params.data The data to log.
     * @returns {void}
     */
    warning: function({data}) {
        if (this.level <= this.warningLevel && this.level != this.offLevel) {
            const record = new CLogRecord(this.warningLevel, data);
            console.warn(data.toString());
            if (this.onLogEvent) {
                this.onLogEvent(record);
            }
        }
    },

    /**
     * Will log error level messages via the module.
     * @memberof codemelted.codemelted_logger
     * @param {object} params The named parameters
     * @param {any} params.data The data to log.
     * @returns {void}
     */
    error: function({data}) {
        if (this.level <= this.errorLevel && this.level != this.offLevel) {
            const record = new CLogRecord(this.errorLevel, data);
            console.error(data.toString());
            if (this.onLogEvent) {
                this.onLogEvent(record);
            }
        }
    },
});

// ----------------------------------------------------------------------------
// [math use case] ------------------------------------------------------------
// ----------------------------------------------------------------------------

/**
 * Provides a math utility API with a collection of mathematical formulas I
 * have either had to use, research, or just found on the Internet.
 * @namespace codemelted.codemelted_math
 */
globalThis["codemelted_math"] = Object.freeze({
    /**
     * Converts celsius to fahrenheit.
     * @memberof codemelted.codemelted_math
     * @param {object} params The named parameters
     * @param {number} params.temp The temperature to convert.
     * @returns {number} The converted temperature
     */
    celsiusToFahrenheit: function({temp}) {
        return (temp * (9 / 5)) + 32;
    },

    /**
     * Converts celsius to kelvin.
     * @memberof codemelted.codemelted_math
     * @param {object} params The named parameters
     * @param {number} params.temp The temperature to convert.
     * @returns {number} The converted temperature
     */
    celsiusToKelvin: function({temp}) {
        return temp + 273.15;
    },

    /**
     * Converts fahrenheit to celsius
     * @memberof codemelted.codemelted_math
     * @param {object} params The named parameters
     * @param {number} params.temp The temperature to convert.
     * @returns {number} The converted temperature
     */
    fahrenheitToCelsius: function({temp}) {
        return (temp - 32) * (5 / 9);
    },

    /**
     * Converts fahrenheit to kelvin
     * @memberof codemelted.codemelted_math
     * @param {object} params The named parameters
     * @param {number} params.temp The temperature to convert.
     * @returns {number} The converted temperature
     */
    fahrenheitToKelvin: function({temp}) {
        return (temp - 32) * (5 / 9) + 273.15;
    },

    /**
     * Calculates the distance in meters between two WGS84 points.
     * @memberof codemelted.codemelted_math
     * @param {object} params The named parameters
     * @param {number} params.startLatitude The starting latitude coordinate.
     * @param {number} params.startLongitude The starting longitude
     * coordinate.
     * @param {number} params.endLatitude The ending latitude coordinate.
     * @param {number} params.endLongitude The ending longitude
     * coordinate.
     * @returns {number} The calculated distance.
     */
    geodeticDistance: function({
        startLatitude,
        startLongitude,
        endLatitude,
        endLongitude,
      }) {
        // Convert degrees to radians
        const lat1 = startLatitude * Math.PI / 180.0;
        const lon1 = startLongitude * Math.PI / 180.0;

        const lat2 = endLatitude * Math.PI / 180.0;
        const lon2 = endLongitude * Math.PI / 180.0;

        // radius of earth in metres
        const r = 6378100;

        // P
        const rho1 = r * Math.cos(lat1);
        const z1 = r * Math.sin(lat1);
        const x1 = rho1 * Math.cos(lon1);
        const y1 = rho1 * Math.sin(lon1);

        // Q
        const rho2 = r * Math.cos(lat2);
        const z2 = r * Math.sin(lat2);
        const x2 = rho2 * Math.cos(lon2);
        const y2 = rho2 * Math.sin(lon2);

        // Dot product
        const dot = (x1 * x2 + y1 * y2 + z1 * z2);
        const cosTheta = dot / (r * r);
        const theta = Math.acos(cosTheta);

        // Distance in meters
        return r * theta;
    },

    /**
     * Calculates the geodetic heading WGS84 to true north represented as 0
     * and rotating around 360 degrees.
     * @memberof codemelted.codemelted_math
     * @param {object} params The named parameters
     * @param {number} params.startLatitude The starting latitude coordinate.
     * @param {number} params.startLongitude The starting longitude
     * coordinate.
     * @param {number} params.endLatitude The ending latitude coordinate.
     * @param {number} params.endLongitude The ending longitude
     * coordinate.
     * @returns {number} The calculated heading.
     */
    geodeticHeading: function({
        startLatitude,
        startLongitude,
        endLatitude,
        endLongitude,
    }) {
        // Get the initial data from our variables:
        const lat1 = startLatitude * (Math.PI / 180);
        const lon1 = startLongitude * (Math.PI / 180);
        const lat2 = endLatitude * (Math.PI / 180);
        const lon2 = endLongitude * (Math.PI / 180);

        // Set up our calculations
        const y = Math.sin(lon2 - lon1) * Math.cos(lat2);
        const x = Math.cos(lat1) * Math.sin(lat2) -
            Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);
        let rtnval = Math.atan2(y, x) * (180 / Math.PI);
        rtnval = (rtnval + 360) % 360;
        return rtnval;
    },

    /**
     * Calculates the speed between two points in meters per second.
     * @memberof codemelted.codemelted_math
     * @param {object} params The named parameters
     * @param {number} params.startMilliseconds The starting time in
     * milliseconds.
     * @param {number} params.startLatitude The starting latitude coordinate.
     * @param {number} params.startLongitude The starting longitude
     * coordinate.
     * @param {number} params.endMilliseconds The ending time in milliseconds.
     * @param {number} params.endLatitude The ending latitude coordinate.
     * @param {number} params.endLongitude The ending longitude
     * coordinate.
     * @returns {number} The calculated heading.
     */
    geodeticSpeed: function({
        startMilliseconds,
        startLatitude,
        startLongitude,
        endMilliseconds,
        endLatitude,
        endLongitude,
    }) {
        // Get the lat / lon for start / end positions
        const distMeters = this.geodeticDistance({
            startLatitude: startLatitude,
            startLongitude: startLongitude,
            endLatitude: endLatitude,
            endLongitude: endLongitude,
        });

        const timeS = (endMilliseconds - startMilliseconds) / 1000.0;
        return distMeters / timeS;
    },

    /**
     * Converts kelvin to celsius.
     * @memberof codemelted.codemelted_math
     * @param {object} params The named parameters
     * @param {number} params.temp The temperature to convert.
     * @returns {number} The converted temperature
     */
    kelvinToCelsius: function({temp}) {
        return temp - 273.15;
    },

    /**
     * Converts kelvin to fahrenheit.
     * @memberof codemelted.codemelted_math
     * @param {object} params The named parameters
     * @param {number} params.temp The temperature to convert.
     * @returns {number} The converted temperature
     */
    kelvinToFahrenheit: function({temp}) {
        return (temp - 273.15) * (9 / 5) + 32;
    },
});

// ----------------------------------------------------------------------------
// [network use case] ---------------------------------------------------------
// ----------------------------------------------------------------------------

// TODO: HTTP Server, WebSocket Client / Server, WebRTC

/**
 * Message handler that receives a JSON object with two fields. "type" which
 * equals either "error" or "data". Then "data" which contains the actual
 * data received.
 * @callback COnMessageListener
 * @param {object} data The object identifying the type of data and the
 * actual data.
 */

/**
 * Wrapper object for the Broadcast Channel API. Constructed via the
 * codemelted_network.broadcastChannel and provides the bindings for
 * working with the channel.
 */
export class CBroadcastChannel {
    /** @type {BroadcastChannel} */
    #channel;

    /**
     * Closes the channel.
     * @returns {void}
     */
    close() {
        this.#channel.close();
    }

    /**
     * Broadcasts a message via the channel
     * @param {any} data The data to communicate.
     * @returns {void}
     */
    postMessage(data) {
        this.#channel.postMessage(data);
    }

    /**
     * Constructor for the class.
     * @param {string} name The url that
     * @param {COnMessageListener} onMessage The receiver for messages.
     */
    constructor(name, onMessage) {
        this.#channel = new BroadcastChannel(name);
        this.#channel.onmessage = (e) => {
            onMessage({
                type: "message",
                data: e.data,
            });
        };
        this.#channel.onmessageerror = (e) => {
            onMessage({
                type: "error",
                data: e.data,
            });
        };
    }
}

/**
 * The EventSource interface is web content's interface to server-sent
 * events. An EventSource instance opens a persistent connection to an
 * HTTP server, which sends events in text/event-stream format. The
 * connection remains open until closed.
 */
export class CEventSource {
    /** @type {EventSource} */
    #eventSource;

    /**
     * Identifies an connecting state for the object.
     * @readonly
     * @type {number}
     */
    static get CONNECTING() { return 0; }

    /**
     * Identifies an open state for the object.
     * @readonly
     * @type {number}
     */
    static get OPEN() { return 1; }

     /**
     * Identifies an closed state for the object.
     * @readonly
     * @type {number}
     */
     static get CLOSED() { return 2; }

    /**
     * Determines the current state of the object. Once in a CLOSED state
     * you will need to get a new object.
     * @readonly
     * @type {number}
     */
    get state() { return this.#eventSource.readyState; }

    /**
     * Closes the server sent event receiver.
     */
    close() {
        this.#eventSource.close();
    }

    /**
     * Constructor for the class.
     * @param {string} url The url to the server streaming the events.
     * @param {boolean} withCredentials True if using CORS, false otherwise.
     * @param {COnMessageListener} onMessage The callback for received messages.
     */
    constructor(url, withCredentials, onMessage) {
        this.#eventSource = new EventSource(
            url,
            {withCredentials: withCredentials}
        );
        this.#eventSource.onerror = (e) => {
            onMessage({
                type: "error",
                data: e
            });
        };
        this.#eventSource.onmessage = (e) => {
            onMessage({
                type: "data",
                data: e.data,
            });
        }
    }
}

export class CFetchResponse {
    /** @type {any} */
    #data;

    /**
     * The retrieved with the fetch request. May be null.
     * @readonly
     * @type {any}
     */
    get data() { return this.#data; }

    /**
     * Will treat the data as a Blob object.
     * @readonly
     * @type {Blob?}
     */
    get asBlob() {
        return this.#data instanceof Blob
            ? this.#data
            : null;
    }

    /**
     * Will treat the data as a Blob object.
     * @readonly
     * @type {FormData?}
     */
    get asFormData() {
        return this.#data instanceof FormData
            ? this.#data
            : null;
    }

    /**
     * Will treat the data as a JavaScript object.
     * @readonly
     * @type {object?}
     */
    get asObject() {
        return typeof this.#data === "object"
            ? this.#data
            : null;
    }

    /**
     * Will treat the data as a string.
     * @readonly
     * @type {string?}
     */
    get asString() {
        return typeof this.#data === "string"
            ? this.#data
            : null;
    }

    /** @type {number} */
    #status;

    /**
     * The HTTP status code of the request.
     * @readonly
     * @type {number}
     */
    get status() { return this.#status; }

    /** @type {string} */
    #statusText;

    /**
     * The HTTP status text of the request.
     * @readonly
     * @type {string}
     */
    get statusText() { return this.#statusText; }

    /**
     * Constructor for the object.
     * @param {number} status The HTTP status of the fetch.
     * @param {string} statusText The associated text.
     * @param {any} data The data retrieved.
     */
    constructor(status, statusText, data) {
        this.#status = status;
        this.#statusText = statusText ? statusText : "";
        this.#data = data;
    }
}

/**
 * Event listener for messages received from different loaded pages.
 * @callback COnWindowMessageListener
 * @param {string} origin Where the message originated.
 * @param {any} data The data received from the other window.
 */

/**
 * Wrapper object for receiving and posting messages between pages.
 */
export class CWindowMessenger {
    /** @type {number} */
    static #count = 0;

    /**
     * Removes the callback from the window object.
     */
    close() {
        CWindowMessenger.#count = 0;
        globalThis.onmessage = null;
    }

    /**
     * Sends data to another window.
     * @param {object} params The named parameters
     * @param {string} [params.targetOrigin] Who the message is for.
     * @param {any} params.data The data to send.
     */
    postMessage({targetOrigin, data}) {
        if (targetOrigin) {
            globalThis.postMessage(data, targetOrigin);
        } else {
            globalThis.postMessage(data);
        }
    }

    /**
     * Constructor for the class.
     * @param {COnWindowMessageListener} onMessage The item to parse
     * received messages.
     */
    constructor(onMessage) {
        if (CWindowMessenger.#count >= 1) {
            throw new SyntaxError(
                "Only one CWindowMessenger object can exist"
            );
        }
        CWindowMessenger.#count += 1;
        globalThis.onmessage = (e) => {
            onMessage(e.origin, e.data);
        }
    }
}

/**
 * Provides the collection of all network related items from the Deno / Web
 * Browser APIs. Any communication you need to make with a backend server
 * are handled via this namespace. If you need to build a backend server this
 * namespace will also provide it.
 * @namespace codemelted.codemelted_network
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API
 * @see https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
 * @see https://developer.mozilla.org/en-US/docs/Web/API/EventSource
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
 * @see https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
 */
globalThis["codemelted_network"] = Object.freeze({
    /**
     * Sends an HTTP POST request containing a small amount of data to a web
     * server. It's intended to be used for sending analytics data to a web
     * server, and avoids some of the problems with legacy techniques for
     * sending analytics, such as the use of XMLHttpRequest.
     * @memberof codemelted.codemelted_network
     * @param {object} params The named parameters
     * @param {string} params.url The url hosting the POST
     * @param {BodyInit} [params.data] The data to send.
     * @throws {SyntaxError} This is only available on Web Browser runtime.
     */
    beacon: function({url, data}) {
        codemelted_json.tryType({type: "string", data: url});
        return codemelted_runtime.tryWeb().navigator.sendBeacon(url, data);
    },

    /**
     * Constructs a CBroadcastChannel object for posting messages between
     * pages within the same domain.
     * @memberof codemelted.codemelted_network
     * @param {object} params The named parameters
     * @param {string} params.name The name of the broadcast channel.
     * @param {COnMessageListener} params.onMessage The listener for received
     * messages.
     * @returns {CBroadcastChannel}
     * @throws {SyntaxError} if attempting to utilizing this object on
     * Deno runtime.
     */
    broadcastChannel: function({name, onMessage}) {
        codemelted_runtime.tryDeno();
        codemelted_json.tryType({type: "string", data: name});
        codemelted_json.tryType({type: "function",
            data: onMessage, count: 1});
        return new CBroadcastChannel(name, onMessage);
    },

    /**
     * Implements the ability to fetch a server's REST API endpoint to retrieve
     * and manage data. The actions for the REST API are controlled via the
     * specified action value with optional items to pass to the
     * endpoint. The result is a CFetchResponse wrapping the REST API endpoint
     * response to the request.
     * @memberof codemelted.codemelted_network
     * @param {object} params The named parameters
     * @param {string} params.action "get", "post", "put", "delete".
     * @param {string} params.url The server's hosting API endpoint.
     * @param {boolean} [params.adAuctionHeaders] See RequestInit reference.
     * @param {object} [params.body] See RequestInit reference.
     * @param {string} [params.cache] See RequestInit reference.
     * @param {string} [params.credentials] See RequestInit reference.
     * @param {object} [params.headers] See RequestInit reference.
     * @param {string} [params.integrity] See RequestInit reference.
     * @param {boolean} [params.keepalive] See RequestInit reference.
     * @param {string} [params.mode] See RequestInit reference.
     * @param {string} [params.priority] See RequestInit reference.
     * @param {string} [params.redirect] See RequestInit reference.
     * @param {string} [params.referrer] See RequestInit reference.
     * @param {string} [params.referrerPolicy] See RequestInit reference.
     * @param {AbortSignal} [params.signal] See RequestInit reference.
     * @see https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
     * @returns {Promise<CFetchResponse>} The result of the fetch.
     */
    fetch: async function({
        action,
        url,
        adAuctionHeaders,
        body,
        cache,
        credentials,
        headers,
        integrity,
        keepalive,
        mode,
        priority,
        redirect,
        referrer,
        referrerPolicy,
        signal,
    }) {
        try {
            // Setup the request
            const request = {
                "method": action.toUpperCase()
            };
            if (adAuctionHeaders) {
                Object.defineProperty(
                    request,
                    "adAuctionHeaders",
                    adAuctionHeaders
                );
            }
            if (body) {
                Object.defineProperty(
                    request,
                    "body",
                    body
                );
            }
            if (cache) {
                Object.defineProperty(
                    request,
                    "cache",
                    cache
                );
            }
            if (credentials) {
                Object.defineProperty(
                    request,
                    "credentials",
                    credentials
                );
            }
            if (headers) {
                Object.defineProperty(
                    request,
                    "headers",
                    headers
                );
            }
            if (integrity) {
                Object.defineProperty(
                    request,
                    "integrity",
                    integrity
                );
            }
            if (keepalive) {
                Object.defineProperty(
                    request,
                    "keepalive",
                    keepalive
                );
            }
            if (mode) {
                Object.defineProperty(
                    request,
                    "mode",
                    mode
                );
            }
            if (priority) {
                Object.defineProperty(
                    request,
                    "priority",
                    priority
                );
            }
            if (redirect) {
                Object.defineProperty(
                    request,
                    "redirect",
                    redirect
                );
            }
            if (referrer) {
                Object.defineProperty(
                    request,
                    "referrer",
                    referrer
                );
            }
            if (referrerPolicy) {
                Object.defineProperty(
                    request,
                    "referrerPolicy",
                    referrerPolicy
                );
            }
            if (signal) {
                Object.defineProperty(
                    request,
                    "signal",
                    signal
                );
            }

            // Now go fetch the data
            const resp = await globalThis.fetch(url, request);
            const contentType = resp.headers.get("Content-Type");
            const status = resp.status;
            const statusText = resp.statusText;
            const data = contentType?.toLowerCase().includes("application/json")
                ? await resp.json()
                : contentType?.toLowerCase().includes("form-data")
                    ? await resp.formData()
                    : contentType?.toLowerCase().includes("application/octet-stream")
                        ? await resp.blob()
                        : contentType?.toLowerCase().includes("text/")
                            ? await resp.text()
                            : "";
            return new CFetchResponse(status, statusText, data);
        } catch (err) {
            return new CFetchResponse(
                418,
                `I'm a teapot. ${err.message}`,
                null
            );
        }

    },

    /**
     * Determines if a connection to the Internet exists. Will return null
     * on Deno runtime.
     * @memberof codemelted.codemelted_network
     * @readonly
     * @type {boolean?}
     */
    get online() {
        return codemelted_runtime.isWeb
            ? globalThis.navigator.onLine
            : null;
    },

    /**
     * Constructs a CEventSource object to open a dedicated connection to a
     * HTTP server to receive messages only.
     * @memberof codemelted.codemelted_network
     * @param {object} params The named parameters
     * @param {string} params.url The URL to open the connection to.
     * @param {boolean} [params.withCredentials = false] Whether to open
     * the connection with credentials.
     * @param {COnMessageListener} params.onMessage The listener for received
     * messages.
     * @return {CEventSource} The constructed object.
     */
    serverSentEvents: function({url, withCredentials = false, onMessage}) {
        return new CEventSource(url, withCredentials, onMessage);
    },

    /**
     * Constructs a CWindowMessenger object to allow for communication
     * between pages.
     * @memberof codemelted.codemelted_network
     * @param {object} params The named parameters
     * @param {COnWindowMessageListener} params.onMessage The listener for
     * received data.
     * @returns {CWindowMessenger} The window messenger.
     * @throws {SyntaxError} If you call this within a Deno runtime.
     */
    windowMessenger: function({onMessage}) {
        codemelted_runtime.tryWeb();
        codemelted_json.tryType({
            type: "function",
            data: onMessage,
            count: 2
        });
        return new CWindowMessenger(onMessage);
    },
});

// ----------------------------------------------------------------------------
// [runtime use case] ---------------------------------------------------------
// ----------------------------------------------------------------------------

/**
 * Optional parameter for the mailto scheme to facilitate translating the
 * more complicated URL.
 */
export class CMailToParams {
    /**
     * Formatted URL of the object
     * @returns {string}
     */
    toString() {
        let url = "";

        // Go format the mailto part of the url
        url += `${this.mailto};`;
        url = url.substring(0, url.length - 1);

        // Go format the cc part of the url
        let delimiter = "?";
        if (this.cc.length > 0) {
          url += `${delimiter}cc=`;
          delimiter = "&";
          for (const e in this.cc) {
            url += `${e};`;
          }
          url = url.substring(0, url.length - 1);
        }

        // Go format the bcc part of the url
        if (this.bcc.length > 0) {
          url += `${delimiter}bcc=`;
          delimiter = "&";
          for (const e in this.bcc) {
            url += `${e};`;
          }
          url = url.substring(0, url.length - 1);
        }

        // Go format the subject part
        if (this.subject.trim().length > 0) {
          url += `${delimiter}subject=${this.subject.trim()}`;
          delimiter = "&";
        }

        // Go format the body part
        if (this.body.trim().length > 0) {
          url += `${delimiter}body=${this.body.trim()}`;
          delimiter = "&";
        }

        return url;
    }

    /**
     * Constructor for the class.
     * @param {object} params The named parameters
     * @param {string} params.mailto Where to email the message.
     * @param {string[]} [params.cc=[]] Who to CC on the email.
     * @param {string[]} [params.bcc=[]] Who to BCC on the email.
     * @param {string} [params.subject=""] The subject of the email.
     * @param {string} [params.body=""] The body of the email.
     */
    constructor({
        mailto,
        cc = [],
        bcc = [],
        subject = "",
        body = ""
    }) {
        codemelted_json.checkType({type: "string", data: mailto});
        codemelted_json.checkType({type: "array", data: cc});
        codemelted_json.checkType({type: "array", data: bcc});
        codemelted_json.checkType({type: "string", data: subject});
        codemelted_json.checkType({type: "string", data: body});
        this.mailto = mailto;
        this.cc = cc;
        this.bcc = bcc;
        this.subject = subject;
        this.body = body;
    }
}

/**
 * The result of the codemelted_runtime.share function call.
 */
export class CShareResult {
    /**
     * The share was successful.
     * @readonly
     * @type {number}
     */
    static get success() { return 0; }

    /**
     * The user canceled the share operation or there are no share targets
     * available.
     * @readonly
     * @type {number}
     */
    static get abort() { return 1; }

    /**
     * There was a problem starting the share target or transmitting the data.
     * The specified share data cannot be validated.
     * @readonly
     * @type {number}
     */
    static get dataIssue() { return 2; }

    /**
     * The document is not fully active, or other sharing operations are in
     * progress.
     * @readonly
     * @type {number}
     */
    static get invalidState() { return 3; }

    /**
     * The user canceled the share operation or there are no share targets
     * available.
     * @readonly
     * @type {number}
     */
    static get notAllowed() { return 4; }

    /**
     * Unknown error encountered.
     * @readonly
     * @type {number}
     */
    static get unknown() { return 5; }

    /** @type {number} */
    #status = 0;

    /**
     * Provides the status of the transaction. The codes are constants on this
     * object.
     * @readonly
     * @type {number}
     */
    get status() { return this.#status; }

    /** @type {string} */
    #message = "";

    /**
     * Provides the message associated with any status code that is not 0.
     * @readonly
     * @type {string}
     */
    get message() { return this.#message; }

    /**
     * Constructor for the class.
     * @param {any} ex The exception if one occurred or undefined if the share
     * was successful.
     */
    constructor(ex) {
        if (!ex) {
            this.#status = CShareResult.success;
            this.#message = "";
        } else if (ex instanceof TypeError) {
            this.#status = CShareResult.dataIssue;
            this.#message = ex.message;
        } else if (ex instanceof DOMException) {
            if (ex.name.includes("AbortError")) {
                this.#status = CShareResult.abort;
            } else if (ex.name.includes("DataError")) {
                this.#status = CShareResult.dataIssue;
            } else if (ex.name.includes("InvalidStateError")) {
                this.#status = CShareResult.invalidState;
            } else if (ex.name.includes("NotAllowedError")) {
                this.#status = CShareResult.notAllowed;
            } else {
                this.#status = CShareResult.unknown;
            }
            this.#message = ex.toString();
        }
    }
}

/**
 * Binds a series of properties and utility methods that are specific to the
 * deno / web runtimes.
 * @namespace codemelted.codemelted_runtime
 * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
 * @see https://docs.deno.com/api/deno/
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Window
 */
globalThis["codemelted_runtime"] = Object.freeze({
    /**
     * Adds an event listener either to the global object or to the specified
     * event target.
     * @memberof codemelted.codemelted_runtime
     * @param {object} params The named parameters.
     * @param {string} params.type A case-sensitive string representing the
     * event type to listen for.
     * @param {EventListener} params.listener Callback function to handle the
     * event fired with optional receipt of an event object.
     * @param {EventTarget} [params.obj] The optional EventTarget to attach
     * the event vs. globalThis event listener.
     * @returns {void}
     */
    addEventListener: function({type, listener, obj}) {
        codemelted_json.tryType({type: "string", data: type});
        codemelted_json.tryType({type: "function", data: listener});
        if (!obj) {
            globalThis.addEventListener(type, listener);
        }
        else {
            obj.addEventListener(type, listener);
        }
    },

    /**
     * Gets the name of the browser your page is running.
     * @memberof codemelted.codemelted_runtime
     * @readonly
     * @type {string}
     */
    get browserName() {
        const browserName = this.isWeb
            ? globalThis.navigator.userAgent.toLowerCase()
            : "deno";
        if (browserName.includes("firefox/")) {
            return "firefox";
        } else if (browserName.includes("opr/")
                || browserName.includes("presto/")) {
            return "opera";
        } else if (browserName.includes("mobile/")
                || browserName.includes("version/")) {
            return "safari";
        } else if (browserName.includes("edg/")) {
            return "edge";
        } else if (browserName.includes("chrome/")) {
            return "chrome";
        }

        return browserName == "deno" ? "deno" : "unknown";
    },

    /**
     * Copies data to the system clipboard. Only available on the
     * Web Browser runtime.
     * @memberof codemelted.codemelted_runtime
     * @param {object} params The named parameters.
     * @param {string} params.data The data to copy to the clipboard.
     * @returns {Promise<void>} Of the copy action.
     * @throws {SyntaxError} If attempted on the Deno runtime.
     */
    copyToClipboard: function({data}) {
        return this.tryWeb().navigator.clipboard.writeText(data);
    },

    /**
     * Parses the environment for the given specified key.
     * @memberof codemelted.codemelted_runtime
     * @param {object} params The named parameters.
     * @param {string} params.key The items to search for.
     * @returns {string?} The value associated with the key or null if not
     * found.
     */
    environment: function({key}) {
        if (this.isDeno) {
            const data = this.tryDeno().env.get(key)
            return !data ? null : data;
        }

        const urlParams = new globalThis.URLSearchParams(
            globalThis.location.search
        );
        return urlParams.get(key);
    },

    /**
     * Gets the newline character specific to the operating system.
     * @memberof codemelted.codemelted_runtime
     * @readonly
     * @type {string}
     */
    get eol() {
        return this.osName === "windows" ? "\r\n" : "\n";
    },

    /**
     * Gets the hostname your page is running
     * @memberof codemelted.codemelted_runtime
     * @readonly
     * @type {string}
     */
    get hostname() {
        return this.isDeno
            ? this.tryDeno().hostname()
            : this.tryWeb().location.hostname;
    },

    /**
     * Gets the href of where your page is loaded.
     * @memberof codemelted.codemelted_runtime
     * @readonly
     * @type {string}
     */
    get href() {
        return this.isWeb
            ? this.tryWeb().location.href
            : "";
    },

    /**
     * Determines if the runtime is Deno.
     * @memberof codemelted.codemelted_runtime
     * @readonly
     * @type {boolean}
     */
    get isDeno() {
        return typeof globalThis.Deno === "undefined";
    },

    /**
     * Identifies if you are on desktop or not.
     * @memberof codemelted.codemelted_runtime
     * @readonly
     * @type {boolean}
     */
    get isDesktop() {
        return this.isDeno;
    },

    /**
     * Identifies if you are on mobile or not.
     * @memberof codemelted.codemelted_runtime
     * @readonly
     * @type {boolean}
     */
    get isMobile() {
        const osName = this.osName;
        return osName.includes("android") ||
            osName.includes("ios");
    },

    /**
     * Determines if the running page is an installed Progressive Web App.
     * @memberof codemelted.codemelted_runtime
     * @readonly
     * @type {boolean}
     */
    get isPWA() {
        if (this.isWeb) {
            const queries = [
                '(display-mode: fullscreen)',
                '(display-mode: standalone)',
                '(display-mode: minimal-ui),'
            ];
            let pwaDetected = false;
            for (const query in queries) {
                pwaDetected = pwaDetected || globalThis.matchMedia(query).matches;
            }
            return pwaDetected;
        }
        return false;
    },

    /**
     * Determines if the runtime is Web Browser.
     * @memberof codemelted.codemelted_runtime
     * @readonly
     * @type {boolean}
     */
    get isWeb() {
        return !this.isDeno;
    },

    /**
     * Loads a specified resource into a new or existing browsing context
     * (that is, a tab, a window, or an iframe) under a specified name. These
     * are based on the different scheme supported protocol items.
     * @memberof codemelted.codemelted_runtime
     * @param {object} params The named parameters.
     * @param {string} params.scheme Either "file:", "http://",
     * "https://", "mailto:", "tel:", or "sms:".
     * @param {boolean} [params.popupWindow = false] true to open a new
     * popup browser window. false to utilize the _target for browser
     * behavior.
     * @param {CMailToParams} [params.mailtoParams] Object to assist in the
     * mailto: scheme URL construction.
     * @param {string} [params.url] The url to utilize with the scheme.
     * @param {string} [params.target = "_blank"] The target to utilize when
     * opening the scheme. Only valid when not utilizing popupWindow.
     * @param {number} [params.width] The width to open the window with.
     * @param {number} [params.height] The height to open the window with.
     * @returns {Promise<Window | null>} The result of the transaction.
     */
    open: function({scheme, popupWindow = false, mailtoParams, url,
            target = "_blank", width, height}) {
        return new Promise((resolve, reject) => {
            try {
                let urlToLaunch = "";
                if (scheme === "file:" ||
                    scheme === "http://" ||
                    scheme === "https://" ||
                    scheme === "sms:" ||
                    scheme === "tel:") {
                  urlToLaunch = `${scheme}${url}`;
                } else if (scheme === "mailto:") {
                  urlToLaunch = mailtoParams != null
                      ? `mailto:${mailtoParams.toString()}`
                      : `mailto:${url}`;
                } else {
                    throw new SyntaxError("Invalid scheme specified");
                }

                let rtnval = null;
                if (popupWindow) {
                    const w = width ?? 900.0;
                    const h = height ?? 600.0;
                    const top = (this.tryWeb().screen.height - h) / 2;
                    const left = (this.tryWeb().screen.width - w) / 2;
                    const settings = "toolbar=no, location=no, " +
                        "directories=no, status=no, menubar=no, " +
                        "scrollbars=no, resizable=yes, copyhistory=no, " +
                        `width=${w}, height=${h}, top=${top}, left=${left}`;
                    rtnval = this.tryWeb().open(
                      urlToLaunch,
                      "_blank",
                      settings,
                    );
                  }

                  rtnval = this.tryWeb().open(urlToLaunch, target);
                  resolve(rtnval);
            } catch (err) {
                reject(err);
            }
        });
    },

    /**
     * Gets the name of the operating system your page is running.
     * @memberof codemelted.codemelted_runtime
     * @readonly
     * @type {string}
     */
    get osName() {
        const osName = this.isDeno
            ? this.tryDeno().build.os
            : this.tryWeb().navigator.userAgent.toLowerCase();

        if (osName.includes("android")) {
            return "android";
        } else if (osName.includes("ios") || osName.includes("iphone")
            || osName.includes("ipad")) {
            return "ios";
        } else if (osName.includes("linux")) {
            return "linux";
        } else if (osName.includes("mac")) {
            return "macos";
        } else if (osName.includes("windows")) {
            return "windows";
        }

        return "unknown";
    },

    /**
     * Will open a print dialog when running in a Web Browser.
     * @memberof codemelted.codemelted_runtime
     * @returns {void}
     * @throws {SyntaxError} if you attempt to call this in Deno runtime.
     */
    print: function() {
        this.tryWeb().print();
    },

    /**
     * Removes an event listener either to the global object or to the
     * specified event target.
     * @memberof codemelted.codemelted_runtime
     * @param {object} params The named parameters.
     * @param {string} params.type A case-sensitive string representing the
     * event type to listen for.
     * @param {EventListener} params.listener Callback function to handle the
     * event fired with optional receipt of an event object.
     * @param {EventTarget} [params.obj] The optional EventTarget to attach
     * the event vs. globalThis event listener.
     * @returns {void}
     */
    removeEventListener: function({type, listener, obj}) {
        codemelted_json.tryType({type: "string", data: type});
        codemelted_json.tryType({type: "function", data: listener});
        if (!obj) {
            globalThis.removeEventListener(type, listener);
        }
        else {
            obj.removeEventListener(type, listener);
        }
    },

    /**
     * Provides the ability to share items via the share services. You specify
     * options via the shareData object parameters. Only available on the web
     * browser runtime.
     * @memberof codemelted.codemelted_runtime
     * @param {object} params The named parameters.
     * @param {object} params.shareData The object specifying the data to
     * share.
     * @returns {Promise<CShareResult>} The result of the call.
     */
    share: async function({shareData}) {
        try {
            await this.tryWeb().navigator.share(shareData);
            return new CShareResult(undefined);
        } catch (err) {
            return new CShareResult(err);
        }
    },

    /**
     * Determines if we are running in a Deno Runtime or not.
     * @memberof codemelted.codemelted_runtime
     * @returns {Deno} namespace reference.
     * @throws {SyntaxError} If not running in a Deno Runtime.
     */
    tryDeno: function() {
        if (!this.isDeno) {
            throw new SyntaxError("Not Running in a Deno Runtime.");
        }
        return globalThis.Deno;
    },

    /**
     * Determines if we are running in a Web Browser environment or not.
     * @memberof codemelted.codemelted_runtime
     * @returns {Window} Reference to the browser window.
     * @throws {SyntaxError} If not running in a browser environment.
     */
    tryWeb: function() {
        if (!this.isDeno) {
            throw new SyntaxError("Not Running in a Web Runtime.");
        }
        return globalThis.window;
    },
});

// ----------------------------------------------------------------------------
// [storage use case] ---------------------------------------------------------
// ----------------------------------------------------------------------------

/**
 * Provides the ability to manage a key / value pair within the targeted
 * runtime environment. The storage methods of "local" and "session" are
 * supported on both Deno and Web Browsers. "cookie" method is only supported
 * on Web Browser and will result in a SyntaxError if called in a Deno
 * runtime.
 * @namespace codemelted.codemelted_storage
 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
 */
globalThis["codemelted_storage"] = Object.freeze({
    /**
     * Clears the specified storage method.
     * @memberof codemelted.codemelted_storage
     * @param {object} params The named parameters.
     * @param {string} [params.method="local"] Values of cookie, local, or
     * session.
     * @returns {void}
     */
    clear: function({method = "local"}) {
        if (method === "local") {
            globalThis.localStorage.clear();
        } else if (method === "session") {
            globalThis.sessionStorage.clear();
        } else if (method === "cookie") {
            codemelted_runtime.tryWeb().document.cookie = "";
        }

        throw new SyntaxError("Invalid method specified");
    },

    /**
     * Gets data from the identified method via the specified key.
     * @memberof codemelted.codemelted_storage
     * @param {object} params The named parameters.
     * @param {string} [params.method="local"] Values of cookie, local, or
     * session.
     * @param {string} params.key The key field to retrieve.
     * @returns {string?} The associated with the key or null if not found.
     */
    getItem: function({method = "local", key}) {
        codemelted_json.tryType({type: "string", data: key});
        if (method === "local") {
            return globalThis.localStorage.getItem(key);
        } else if (method === "session") {
            return globalThis.sessionStorage.getItem(key);
        } else if (method === "cookie") {
            const name = `${key}=`;
            const ca = codemelted_runtime.tryWeb().document.cookie.split(';');
            for (let i = 0; i < ca.length; i++) {
              let c = ca[i];
              while (c[0] == " ") {
                c = c.substring(1);
              }
              if (c.indexOf(name) == 0) {
                return c.substring(name.length, c.length);
              }
            }
            return null;
        }

        throw new SyntaxError("Invalid method specified");
    },

    /**
     * Total items stored in the identified method.
     * @memberof codemelted.codemelted_storage
     * @param {object} params The named parameters.
     * @param {string} [params.method="local"] Values of cookie, local, or
     * session.
     * @returns {number} The number of key / value pairs stored within the
     * storage method.
     */
    length: function({method = "local"}) {
        if (method === "local") {
            return globalThis.localStorage.length;
        } else if (method === "session") {
            return globalThis.localStorage.length;
        } else if (method === "cookie") {
            const ca = codemelted_runtime.tryWeb().document.cookie.split(";");
            return ca.length;
        }

        throw new SyntaxError("Invalid method specified");
    },

    /**
     * Gets the key from the index from the identified storage method.
     * @memberof codemelted.codemelted_storage
     * @param {object} params The named parameters.
     * @param {string} [params.method="local"] Values of cookie, local, or
     * session.
     * @param {number} params.index The index of where the key exists in
     * storage.
     * @returns {String?} The name of the key or null if the index goes
     * beyond the stored length.
     */
    key: function({method = "local", index}) {
        codemelted_json.tryType({type: "number", data: index});
        if (method === "local") {
            return globalThis.localStorage.key(index);
        } else if (method === "session") {
            return globalThis.sessionStorage.key(index);
        } else if (method === "cookie") {
            const ca = codemelted_runtime.tryWeb().document.cookie.split(";");
            if (ca.length >= index) {
                return null;
            }
            const key = ca[index].split("=");
            return key[0];
        }

        throw new SyntaxError("Invalid method specified");
    },

    /**
     * Removes an item by key from the identified storage method.
     * @memberof codemelted.codemelted_storage
     * @param {object} params The named parameters.
     * @param {string} [params.method="local"] Values of cookie, local, or
     * session.
     * @param {string} params.key The key field to remove.
     * @returns {void}
     */
    removeItem: function({method = "local", key}) {
        codemelted_json.tryType({type: "string", data: key});
        if (method === "local") {
            globalThis.localStorage.removeItem(key);
        } else if (method === "session") {
            globalThis.sessionStorage.removeItem(key);
        } else if (method === "cookie") {
            codemelted_runtime.tryWeb().document.cookie =
                `${key}=; expires=01 Jan 1970 00:00:00; path=/;`;
        }

        throw new SyntaxError("Invalid method specified");
    },

    /**
     * Sets a value by the identified key within the identified storage
     * method.
     * @memberof codemelted.codemelted_storage
     * @param {object} params The named parameters.
     * @param {string} [params.method="local"] Values of cookie, local, or
     * session.
     * @param {string} params.key The identified key to associate with
     * the value.
     * @param {string} params.value The value to set.
     * @returns {void}
     */
    setItem: function({method = "local", key, value}) {
        codemelted_json.tryType({type: "string", data: key});
        codemelted_json.tryType({type: "string", data: value});
        if (method === "local") {
            globalThis.localStorage.setItem(key, value);
        } else if (method === "session") {
            globalThis.sessionStorage.setItem(key, value);
        } else if (method === "cookie") {
            const d = new Date();
            d.setTime(d.getTime() + (365 * 24 * 60 * 60 * 1000));
            const expires = "expires="+ d.toUTCString();
            codemelted_runtime.tryWeb().document.cookie =
                `${key}=${value}; ${expires}; path=/`;
        }

        throw new SyntaxError("Invalid method specified");
    }
});

// ----------------------------------------------------------------------------
// [ui use case] --------------------------------------------------------------
// ----------------------------------------------------------------------------

/**
 * UNDER DEVELOPMENT
 * @namespace codemelted.codemelted_ui
 */
globalThis["codemelted_ui"] = Object.freeze({
    //
});