Greasy Fork is available in English.

Basic Functions (For userscripts)

Useful functions for myself

لا ينبغي أن لا يتم تثبيت هذا السكريت مباشرة. هو مكتبة لسكبتات لتشمل مع التوجيه الفوقية // @require https://update.greasyfork.org/scripts/456034/1532680/Basic%20Functions%20%28For%20userscripts%29.js

// ==UserScript==
// @name               Basic Functions (For userscripts)
// @name:zh-CN         常用函数(用户脚本)
// @name:en            Basic Functions (For userscripts)
// @namespace          PY-DNG Userscripts
// @version            1.4
// @description        Useful functions for myself
// @description:zh-CN  自用函数
// @description:en     Useful functions for myself
// @author             PY-DNG
// @license            GPL-3.0-or-later
// ==/UserScript==

/* eslint-disable no-multi-spaces */
/* eslint-disable no-return-assign */

// Note: version 0.8.2.1 is modified just the license and it's not uploaded to GF yet 23-11-26 15:03
// Note: version 0.8.3.1 is added just the description of parseArgs and has not uploaded to GF yet 24-02-03 18:55

let [
    // Console & Debug
    LogLevel, DoLog, Err, Assert,

    // DOM
    $, $All, $CrE, $AEL, $$CrE, addStyle, detectDom, destroyEvent,

    // Data
    copyProp, copyProps, parseArgs, escJsStr, replaceText,

    // Environment & Browser
    getUrlArgv, dl_browser, dl_GM,

    // Logic & Task
    AsyncManager, queueTask, FunctionLoader, loadFuncs, require, isLoaded
] = (function() {
    const [LogLevel, DoLog] = (function() {
        /**
         * level defination for DoLog function, bigger ones has higher possibility to be printed in console
         * @typedef {Object} LogLevel
         * @property {0} None - 0
         * @property {1} Error - 1
         * @property {2} Success - 2
         * @property {3} Warning - 3
         * @property {4} Info - 4
         */
        /** @type {LogLevel} */
        const LogLevel = {
            None: 0,
            Error: 1,
            Success: 2,
            Warning: 3,
            Info: 4,
        };

        return [LogLevel, DoLog];

        /**
         * @overload
         * @param {String} content - log content
         */
        /**
         * @overload
         * @param {Number} level - level specified in LogLevel object
         * @param {String} content - log content
         */
        /**
         * Logger with level and logger function specification
         * @overload
         * @param {Number} level - level specified in LogLevel object
         * @param {String} content - log content
         * @param {String} logger - which log function to use (in window.console[logger])
         */
        function DoLog() {
            // Get window
            const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;

            const LogLevelMap = {};
            LogLevelMap[LogLevel.None] = {
                prefix: '',
                color: 'color:#ffffff'
            }
            LogLevelMap[LogLevel.Error] = {
                prefix: '[Error]',
                color: 'color:#ff0000'
            }
            LogLevelMap[LogLevel.Success] = {
                prefix: '[Success]',
                color: 'color:#00aa00'
            }
            LogLevelMap[LogLevel.Warning] = {
                prefix: '[Warning]',
                color: 'color:#ffa500'
            }
            LogLevelMap[LogLevel.Info] = {
                prefix: '[Info]',
                color: 'color:#888888'
            }
            LogLevelMap[LogLevel.Elements] = {
                prefix: '[Elements]',
                color: 'color:#000000'
            }

            // Current log level
            DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error

            // Log counter
            DoLog.logCount === undefined && (DoLog.logCount = 0);

            // Get args
            let [level, logContent, logger] = parseArgs([...arguments], [
                [2],
                [1,2],
                [1,2,3]
            ], [LogLevel.Info, 'DoLog initialized.', 'log']);

            let msg = '%c' + LogLevelMap[level].prefix + (typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + (LogLevelMap[level].prefix ? ' ' : '');
            let subst = LogLevelMap[level].color;

            switch (typeof(logContent)) {
                case 'string':
                    msg += '%s';
                    break;
                case 'number':
                    msg += '%d';
                    break;
                default:
                    msg += '%o';
                    break;
            }

            // Log when log level permits
            if (level <= DoLog.logLevel) {
                // Log to console when log level permits
                if (level <= DoLog.logLevel) {
                    if (++DoLog.logCount > 512) {
                        console.clear();
                        DoLog.logCount = 0;
                    }
                    console[logger](msg, subst, logContent);
                }
            }
        }
    }) ();

    /**
     * Throw an error
     * @param {String} msg - the error message
     * @param {Error} [ErrorConstructor=Error] - which error constructor to use, defaulting to Error()
     */
    function Err(msg, ErrorConstructor=Error) {
        throw new ErrorConstructor((typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + msg);
    }

    /**
     * Assert given condition is true-like, otherwise throws given error
     * @param {*} condition
     * @param {string} errmsg
     * @param {Error} [ErrorConstructor=Error]
     */
    function Assert(condition, errmsg, ErrorConstructor=Error) {
        condition || Err(errmsg, ErrorConstructor);
    }

    /**
     * Convenient function to querySelector
     * @overload
     * @param {Element|Document|DocumentFragment} [root] - which target to call querySelector on
     * @param {string} selector - querySelector selector
     * @returns {Element|null}
     */
    function $() {
        switch(arguments.length) {
            case 2:
                return arguments[0].querySelector(arguments[1]);
            default:
                return document.querySelector(arguments[0]);
        }
    }
    /**
     * Convenient function to querySelectorAll
     * @overload
     * @param {Element|Document|DocumentFragment} [root] - which target to call querySelectorAll on
     * @param {string} selector - querySelectorAll selector
     * @returns {NodeList}
     */
    function $All() {
        switch(arguments.length) {
            case 2:
                return arguments[0].querySelectorAll(arguments[1]);
                break;
            default:
                return document.querySelectorAll(arguments[0]);
        }
    }
    /**
     * Convenient function to querySelectorAll
     * @overload
     * @param {Document} [root] - which document to call createElement on
     * @param {string} tagName
     */
    function $CrE() {
        switch(arguments.length) {
            case 2:
                return arguments[0].createElement(arguments[1]);
                break;
            default:
                return document.createElement(arguments[0]);
        }
    }
    /**
     * Convenient function to addEventListener
     * @overload
     * @param {EventTarget} target - which target to call addEventListener on
     * @param {string} type
     * @param {EventListenerOrEventListenerObject | null} callback
     * @param {AddEventListenerOptions | boolean} [options]
     */
    function $AEL(...args) {
        /** @type {EventTarget} */
        const target = args.shift();
        return target.addEventListener.apply(target, args);
    }
    /**
     * @typedef {[type: string, callback: EventListenerOrEventListenerObject | null, options: AddEventListenerOptions | boolean]} $AEL_Arguments
     */
    /**
     * @typedef {Object} $$CrE_Options
     * @property {string} tagName
     * @property {object} [props] - properties set by `element[prop] = value;`
     * @property {object} [attrs] - attributes set by `element.setAttribute(attr, value);`
     * @property {string | string[]} [classes] - class names to be set
     * @property {object} [styles] - styles set by `element[style_name] = style_value;`
     * @property {$AEL_Arguments[]} [listeners] - event listeners added by `$AEL(element, ...listener);`
     */
    /**
     * @overload
     * @param {$$CrE_Options} options
     */
    /**
     * Create configorated element
     * @overload
     * @param {string} tagName
     * @param {object} [props] - properties set by `element[prop] = value;`
     * @param {object} [attrs] - attributes set by `element.setAttribute(attr, value);`
     * @param {string | string[]} [classes] - class names to be set
     * @param {object} [styles] - styles set by `element[style_name] = style_value;`
     * @param {$AEL_Arguments[]} [listeners] - event listeners added by `$AEL(element, ...listener);`
     * @returns {HTMLElement}
     */
    function $$CrE() {
        const [tagName, props, attrs, classes, styles, listeners] = parseArgs([...arguments], [
            function(args, defaultValues) {
                const arg = args[0];
                return {
                    'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
                    'object': () => ['tagName', 'props', 'attrs', 'classes', 'styles', 'listeners'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
                }[typeof arg]();
            },
            [1,2],
            [1,2,3],
            [1,2,3,4],
            [1,2,3,4,5]
        ], ['div', {}, {}, [], {}, []]);
        const elm = $CrE(tagName);
        for (const [name, val] of Object.entries(props)) {
            elm[name] = val;
        }
        for (const [name, val] of Object.entries(attrs)) {
            elm.setAttribute(name, val);
        }
        for (const cls of Array.isArray(classes) ? classes : [classes]) {
            elm.classList.add(cls);
        }
        for (const [name, val] of Object.entries(styles)) {
            elm.style[name] = val;
        }
        for (const listener of listeners) {
            $AEL(elm, ...listener);
        }
        return elm;
    }

    /**
     * @overload
     * @param {string} css - css content
     * @returns {HTMLStyleElement}
     */
    /**
     * @overload
     * @param {string} css - css content
     * @param {string} id - `id` attribute for <style> element
     * @returns {HTMLStyleElement}
     */
    /**
     * Append a style text to document(<head>) with a <style> element \
     * removes existing <style> elements with same id if id provided, so style updates can be done by using one same id
     *
     * Uses `GM_addElement` if `GM_addElement` exists and param `id` not specified. (`GM_addElement` uses id attribute, so specifing id manually when using `GM_addElement` takes no effect) \
     * In another case `GM_addStyle` instead of `GM_addElement` exists, and both `id` and `parentElement` not specified, `GM_addStyle` will be used. \
     * `document.createElement('style')` will be used otherwise.
     * @overload
     * @param {HTMLElement} parentElement - parent element to place <style> element
     * @param {string} css - css content
     * @param {string} id - `id` attribute for <style> element
     * @returns {HTMLStyleElement}
     */
    function addStyle() {
        // Get arguments
        const [parentElement, css, id] = parseArgs([...arguments], [
            [2],
            [2,3],
            [1,2,3]
        ], [null, '', null]);

        if (typeof GM_addElement === 'function' && id === null) {
            return GM_addElement(parentElement, 'style', { textContent: css });
        } else if (typeof GM_addStyle === 'function' && parentElement === null && id === null) {
            return GM_addStyle(css);
        } else {
            // Make <style>
            const style = $CrE('style');
            style.innerText = css;
            id !== null && (style.id = id);
            id !== null && Array.from($All(`style#${id}`)).forEach(elm => elm.remove());

            // Append to parentElement
            (parentElement ?? document.head).appendChild(style);
            return style;
        }
    }

    /**
     * @typedef {Object} detectDom_options
     * @property {Node} root - root target to observe on
     * @property {string | string[]} [selector] - selector(s) to observe for, be aware that in options object it is named selector, but is named selectors in param
     * @property {boolean} [attributes] - whether to observe existing elements' attribute changes
     * @property {function} [callback] - if provided, use callback instead of Promise when selector element found
     */
    /**
     * @overload
     * @param {detectDom_options} options
     * @returns {MutationObserver}
     */
    /**
     * Get callback / resolve promise when specific dom/element appearce in document \
     * uses MutationObserver for implementation \
     * This behavior is different from versions that equals to or older than 0.8.4.2, so be careful when using it.
     * @overload
     * @param {Node} root - root target to observe on
     * @param {string | string[]} [selectors] - selector(s) to observe for
     * @param {boolean} [attributes] - whether to observe existing elements' attribute changes
     * @param {function} [callback] - if provided, use callback instead of Promise when selector element found
     * @returns {MutationObserver}
     */
    function detectDom() {
        let [selectors, root, attributes, callback] = parseArgs([...arguments], [
            function(args, defaultValues) {
                const arg = args[0];
                return {
                    'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
                    'object': () => ['selector', 'root', 'attributes', 'callback'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
                }[typeof arg]();
            },
            [2,1],
            [2,1,3],
            [2,1,3,4],
        ], [[''], document, false, null]);
        !Array.isArray(selectors) && (selectors = [selectors]);

        if (select(root, selectors)) {
            for (const elm of selectAll(root, selectors)) {
                if (callback) {
                    setTimeout(callback.bind(null, elm));
                } else {
                    return Promise.resolve(elm);
                }
            }
        }

        const observer = new MutationObserver(mCallback);
        observer.observe(root, {
            childList: true,
            subtree: true,
            attributes,
        });

        let isPromise = !callback;
        return callback ? observer : new Promise((resolve, reject) => callback = resolve);

        function mCallback(mutationList, observer) {
            const addedNodes = mutationList.reduce((an, mutation) => {
                switch (mutation.type) {
                    case 'childList':
                        an.push(...mutation.addedNodes);
                        break;
                    case 'attributes':
                        an.push(mutation.target);
                        break;
                }
                return an;
            }, []);
            const addedSelectorNodes = addedNodes.reduce((nodes, anode) => {
                if (anode.matches && match(anode, selectors)) {
                    nodes.add(anode);
                }
                const childMatches = anode.querySelectorAll ? selectAll(anode, selectors) : [];
                for (const cm of childMatches) {
                    nodes.add(cm);
                }
                return nodes;
            }, new Set());
            for (const node of addedSelectorNodes) {
                callback(node);
                isPromise && observer.disconnect();
            }
        }

        function selectAll(elm, selectors) {
            !Array.isArray(selectors) && (selectors = [selectors]);
            return selectors.map(selector => [...$All(elm, selector)]).reduce((all, arr) => {
                all.push(...arr);
                return all;
            }, []);
        }

        function select(elm, selectors) {
            const all = selectAll(elm, selectors);
            return all.length ? all[0] : null;
        }

        function match(elm, selectors) {
            return !!elm.matches && selectors.some(selector => elm.matches(selector));
        }
    }

    /**
     * Just stopPropagation and preventDefault
     * @param {Event} e
     */
    function destroyEvent(e) {
        if (!e) {return false;};
        if (!e instanceof Event) {return false;};
        e.stopPropagation();
        e.preventDefault();
    }

    /**
     * copy property value from obj1 to obj2 if exists
     * @param {object} obj1
     * @param {object} obj2
     * @param {string|Symbol} prop
     */
    function copyProp(obj1, obj2, prop) {obj1.hasOwnProperty(prop) && (obj2[prop] = obj1[prop]);}
    /**
     * copy property values from obj1 to obj2 if exists
     * @param {object} obj1
     * @param {object} obj2
     * @param {string|Symbol} [props] - properties to copy, copy all enumerable properties if not specified
     */
    function copyProps(obj1, obj2, props) {(props ?? Object.keys(obj1)).forEach((prop) => (copyProp(obj1, obj2, prop)));}

    /**
     * Argument parser with sorting and defaultValue support \
     * See use cases in other functions
     * @param {Array} args - original arguments' value to be parsed
     * @param {(number[]|function)[]} rules - rules to sort arguments or custom function to parse arguments
     * @param {Array} defaultValues - default values for arguments not provided a value
     * @returns {Array}
     */
    function parseArgs(args, rules, defaultValues=[]) {
        // args and rules should be array, but not just iterable (string is also iterable)
        if (!Array.isArray(args) || !Array.isArray(rules)) {
            throw new TypeError('parseArgs: args and rules should be array')
        }

        // fill rules[0]
        (!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []);

        // max arguments length
        const count = rules.length - 1;

        // args.length must <= count
        if (args.length > count) {
            throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`);
        }

        // rules[i].length should be === i if rules[i] is an array, otherwise it should be a function
        for (let i = 1; i <= count; i++) {
            const rule = rules[i];
            if (Array.isArray(rule)) {
                if (rule.length !== i) {
                    throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`);
                }
                if (!rule.every((num) => (typeof num === 'number' && num <= count))) {
                    throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`);
                }
            } else if (typeof rule !== 'function') {
                throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`)
            }
        }

        // Parse
        const rule = rules[args.length];
        let parsed;
        if (Array.isArray(rule)) {
            parsed = [...defaultValues];
            for (let i = 0; i < rule.length; i++) {
                parsed[rule[i]-1] = args[i];
            }
        } else {
            parsed = rule(args, defaultValues);
        }
        return parsed;
    }

    /**
     * escape str into javascript written format
     * @param {string} str
     * @param {string} [quote]
     * @returns
     */
    function escJsStr(str, quote='"') {
        str = str.replaceAll('\\', '\\\\').replaceAll(quote, '\\' + quote).replaceAll('\t', '\\t');
        str = quote === '`' ? str.replaceAll(/(\$\{[^\}]*\})/g, '\\$1') : str.replaceAll('\r', '\\r').replaceAll('\n', '\\n');
        return quote + str + quote;
    }

    /**
     * Replace given text with no mismatching of replacing replaced text
     *
     * e.g. replaceText('aaaabbbbccccdddd', {'a': 'b', 'b': 'c', 'c': 'd', 'd': 'e'}) === 'bbbbccccddddeeee' \
     *      replaceText('abcdAABBAA', {'BB': 'AA', 'AAAAAA': 'This is a trap!'}) === 'abcdAAAAAA' \
     *      replaceText('abcd{AAAA}BB}', {'{AAAA}': '{BB', '{BBBB}': 'This is a trap!'}) === 'abcd{BBBB}' \
     *      replaceText('abcd', {}) === 'abcd'
     *
     * **Note**: \
     *  replaceText will replace in sort of replacer's iterating sort \
     *  e.g. currently replaceText('abcdAABBAA', {'BBAA': 'TEXT', 'AABB': 'TEXT'}) === 'abcdAATEXT' \
     *  but remember: (As MDN Web Doc said,) Although the keys of an ordinary Object are ordered now, this was \
     *  not always the case, and the order is complex. As a result, it's best not to rely on property order. \
     *  So, don't expect replaceText will treat replacer key-values in any specific sort. Use replaceText to \
     *  replace irrelevance replacer keys only.
     * @param {string} text
     * @param {object} replacer
     * @returns {string}
     */
    function replaceText(text, replacer) {
        if (Object.entries(replacer).length === 0) {return text;}
        const [models, targets] = Object.entries(replacer);
        const len = models.length;
        let text_arr = [{text: text, replacable: true}];
        for (const [model, target] of Object.entries(replacer)) {
            text_arr = replace(text_arr, model, target);
        }
        return text_arr.map((text_obj) => (text_obj.text)).join('');

        function replace(text_arr, model, target) {
            const result_arr = [];
            for (const text_obj of text_arr) {
                if (text_obj.replacable) {
                    const splited = text_obj.text.split(model);
                    for (const part of splited) {
                        result_arr.push({text: part, replacable: true});
                        result_arr.push({text: target, replacable: false});
                    }
                    result_arr.pop();
                } else {
                    result_arr.push(text_obj);
                }
            }
            return result_arr;
        }
    }

    /**
     * @typedef {Object} getUrlArgv_options
     * @property {string} name
     * @property {string} [url]
     * @property {string} [defaultValue]
     * @property {function} [dealFunc] - function that inputs original getUrlArgv result and outputs final return value
     */
    /**
     * @overload
     * @param {Object} getUrlArgv_options
     * @returns
     */
    /**
     * Get a url argument from location.href
     * @param {string} name
     * @param {string} [url]
     * @param {string} [defaultValue]
     * @param {function} [dealFunc] - function that inputs original getUrlArgv result and outputs final return value
     */
    function getUrlArgv() {
        const [name, url, defaultValue, dealFunc] = parseArgs([...arguments], [
            function(args, defaultValues) {
                const arg = args[0];
                return {
                    'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
                    'object': () => ['name', 'url', 'defaultValue', 'dealFunc'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
                }[typeof arg]();
            },
            [2,1],
            [2,1,3],
            [2,1,3,4]
        ], [null, location.href, null, a => a]);

        if (name === null) { return null; }

        const search = new URL(url).search;
        const objSearch = new URLSearchParams(search);
        const raw = objSearch.has(name) ? objSearch.get(name) : defaultValue;
        const argv = dealFunc(raw);

        return argv;
    }

    /**
     * download file from given url by simulating <a download="..." href=""></a> clicks\
     * a common use case is to download Blob objects as file from `URL.createObjectURL`
     * @param {string} url
     * @param {string} filename
     */
    function dl_browser(url, filename) {
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        a.click();
    }

    /**
     * File download function\
     * details looks like the detail of GM_xmlhttpRequest\
     * onload function will be called after file saved to disk
     * @param {object} details
     */
    function dl_GM(details) {
        if (!details.url || !details.name) {return false;};

        // Configure request object
        const requestObj = {
            url: details.url,
            responseType: 'blob',
            onload: function(e) {
                // Save file
                dl_browser(URL.createObjectURL(e.response), details.name);

                // onload callback
                details.onload ? details.onload(e) : function() {};
            }
        }
        if (details.onloadstart       ) {requestObj.onloadstart        = details.onloadstart;};
        if (details.onprogress        ) {requestObj.onprogress         = details.onprogress;};
        if (details.onerror           ) {requestObj.onerror            = details.onerror;};
        if (details.onabort           ) {requestObj.onabort            = details.onabort;};
        if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
        if (details.ontimeout         ) {requestObj.ontimeout          = details.ontimeout;};

        // Send request
        Assert(typeof GM_xmlhttpRequest === 'function', 'GM_xmlhttpRequest should be provided in order to use dl_GM', TypeError);
        GM_xmlhttpRequest(requestObj);
    }

    /**
     * Manager to manager async tasks\
     * This was written when I haven't learnt Promise, so for fluent promise users, just ignore it:)
     *
     * # Usage
     * ```javascript
     * // This simulates a async task, it can be a XMLHttpRequest, some file reading, or so on...
     * function someAsyncTask(callback, duration) {
     *     const result = Math.random();
     *     setTimeout(() => callback(result), duration);
     * }
     *
     * // Do 10 async tasks, and log all results when all async tasks finished
     * const AM = new AsyncManager();
     * const results = [];
     * AM.onfinish = function() {
     *     console.log('All tasks finished!');
     *     console.log(results);
     * }
     *
     * for (let i = 0; i < 10; i++) {
     *     AM.add();
     *     const duration = (Math.random() * 5 + 5) * 1000;
     *     const index = i;
     *     someAsyncTask(result => {
     *         console.log(`Task ${index} finished after ${duration}ms!`);
     *         results[index] = result;
     *     }, duration);
     *     console.log(`Task ${index} started!`);
     * }
     *
     * // Set AM.finishEvent to true after all tasks added, allowing AsyncManager to call onfinish callback
     * ```
     * @constructor
     */
    function AsyncManager() {
        const AM = this;

        // Ongoing tasks count
        this.taskCount = 0;

        // Whether generate finish events
        let finishEvent = false;
        Object.defineProperty(this, 'finishEvent', {
            configurable: true,
            enumerable: true,
            get: () => (finishEvent),
            set: (b) => {
                finishEvent = b;
                b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
            }
        });

        // Add one task
        this.add = () => (++AM.taskCount);

        // Finish one task
        this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
    }

    /**
     * Put tasks in specific queue and order their execution
     * Set `queueTask[queueId].max`, `queueTask[queueId].sleep` to custom queue's max ongoing tasks and sleep time between tasks
     * @param {function} task - task function to run
     * @param {string | Symbol} queueId - identifier to specify a target queue. if provided, given task will be added into specified queue.
     * @returns
     */
    function queueTask(task, queueId='default') {
        init();

        return new Promise((resolve, reject) => {
            queueTask.hasOwnProperty(queueId) || (queueTask[queueId] = { tasks: [], ongoing: 0 });
            queueTask[queueId].tasks.push({task, resolve, reject});
            checkTask(queueId);
        });

        function init() {
            if (!queueTask[queueId]?.initialized) {
                queueTask[queueId] = {
                    // defaults
                    tasks: [],
                    ongoing: 0,
                    max: 3,
                    sleep: 500,

                    // user's pre-sets
                    ...(queueTask[queueId] || {}),

                    // initialized flag
                    initialized: true
                }
            };
        }

        function checkTask() {
            const queue = queueTask[queueId];
            setTimeout(() => {
                if (queue.ongoing < queue.max && queue.tasks.length) {
                    const task = queue.tasks.shift();
                    queue.ongoing++;
                    setTimeout(
                        () => task.task().then(v => {
                            queue.ongoing--;
                            task.resolve(v);
                            checkTask(queueId);
                        }).catch(e => {
                            queue.ongoing--;
                            task.reject(e);
                            checkTask(queueId);
                        }),
                        queue.sleep
                    );
                }
            });
        }
    }

    const [FunctionLoader, loadFuncs, require, isLoaded] = (function() {
        /**
         * 一般用作函数对象oFunc的加载条件,检测当前环境是否适合/需要该oFunc加载
         * @typedef {Object} checker_func
         * @property {string} type - checker's identifier
         * @property {function} func - actual internal judgement implementation
         */
        /**
         * 一般用作函数对象oFunc的加载条件,检测当前环境是否适合/需要该oFunc加载
         * @typedef {Object} checker
         * @property {string} type - checker's identifier
         * @property {*} value - param that goes into checker function
         */
        /**
         * 需要使用的substorage名称
         * @typedef {"GM_setValue" | "GM_getValue" | "GM_listValues" | "GM_deleteValue"} substorage_value
         */
        /**
         * 可以传入params的字符串名称
         * @typedef {'oFunc' | substorage_value} param
         */
        /**
         * 被加载函数对象的func函数
         * @callback oFuncBody
         * @param {oFunc} oFunc
         * @returns {*|Promise<*>}
         */
        /**
         * 被加载执行的函数对象
         * @typedef {Object} oFunc
         * @property {string} id - 每次load(每个FuncPool实例)内唯一的标识符
         * @property {boolean} [disabled] - 为真值时,无论checkers还是detectDom等任何其他条件通过或未通过,均不执行此函数对象;默认为false
         * @property {checker[]|checker} [checkers] - oFunc执行的条件
         * @property {string[]|string} [detectDom] - 如果提供,开始checker检查前会首先等待其中所有css选择器对应的元素在document中出现
         * @property {string[]|string} [dependencies] - 如果提供,应为其他函数对象的id或者id列表;开始checker检查前会首先等待其中所有指定的函数对象加载完毕
         * @property {boolean} [readonly] - 指定该函数的返回值是否应该被Proxy保护为不可修改对象
         * @property {param[]|param} params - 可选,指定传入oFunc.func的参数列表;可以为参数本身或其组成的数组
         * 参数可以为 字符串 或是 其他类型,如果是字符串就传入对应的FunctionLoader提供的内置值(见下),如果是其他类型则按照原样传入
         * - "oFunc":
         *     函数对象本身
         * - "GM_setValue", "GM_getValue", "GM_listValues", "GM_deleteValue":
         *     和脚本管理器提供的函数一致,但是读取和写入的对象是以oFunc.id为键的子空间
         *     比如,GM_getValue("prop") 就相当于调用脚本管理器提供的的 GM_getValue(oFunc.id)["prop"]
         * @property {oFuncBody} func - 实际实现了功能的函数
         * @property {boolean} [STOP] - [调试用] 指定不执行此函数对象
         */

        const registered_checkers = {
			switch: value => value,
			url: value => location.href === value,
			path: value => location.pathname === value,
			regurl: value => !!location.href.match(value),
			regpath: value => !!location.pathname.match(value),
			starturl: value => location.href.startsWith(value),
			startpath: value => location.pathname.startsWith(value),
			func: value => value()
		};

        class FuncPool extends EventTarget {
            static #STILL_LOADING = Symbol('oFunc still loading');
            static FunctionNotFound = Symbol('Function not found');
            static FunctionNotLoaded = Symbol('Function not loaded');

            /** @typedef {symbol|*} return_value */
            /** @type {Map<oFunc, return_value>} */
            #oFuncs = new Map();

            /**
             * 创建新函数池,并加载提供的函数对象
             * @param {oFunc[]|oFunc} [oFuncs] - 可选,需要加载的函数对象或其数组,不提供时默认为空数组
             * @returns {FuncPool}
             */
            constructor(oFuncs=[]) {
                super();
                this.load(oFuncs);
            }

            /**
             * 加载提供的一个或多个函数对象,并将其加入到函数池中
             * @param {oFunc[]|oFunc} [oFuncs] - 可选,需要加载的函数对象或其数组,不提供时默认为空数组
             */
            load(oFuncs=[]) {
                oFuncs = Array.isArray(oFuncs) ? oFuncs : [oFuncs];
                for (const oFunc of oFuncs) {
                    this.#load(oFunc);
                }
            }

            /**
             * 加载一个函数对象,并将其加入到函数池中
             * 当id重复时,直接报错RedeclarationError
             * 异步函数,当彻底load完毕/checkers确定不加载时resolve
             * 当加载完毕时,广播load事件;如果全部加载完毕,还广播all_load事件
             * @param {oFunc} oFunc
             * @returns {Promise<undefined>}
             */
            async #load(oFunc) {
                const that = this;

                // disabled的函数对象,不执行
                if (oFunc.disabled) {
                    return;
                }

                // 已经在函数池中的函数对象,不重复load
                if (this.#oFuncs.has(oFunc)) {
                    return;
                }

                // 检查有无重复id
                for (const o of this.#oFuncs.keys()) {
                    if (o.id === oFunc.id) {
                        throw new RedeclarationError(`Attempts to load oFunc with id already in use: ${oFunc.id}`);
                    }
                }

                // 设置当前返回值为STILL_LOADING
                this.#oFuncs.set(oFunc, FuncPool.#STILL_LOADING);

                // 加载依赖
                const dependencies = Array.isArray(oFunc.dependencies) ? oFunc.dependencies : ( oFunc.dependencies ? [oFunc.dependencies] : [] );
                const promise_deps = Promise.all(dependencies.map(id => new Promise((resolve, reject) => {
                    $AEL(that, 'load', e => e.detail.oFunc.id === id && resolve());
                })));

                // 检测detectDOM中css选择器指定的元素出现
                const selectors = Array.isArray(oFunc.detectDom) ? oFunc.detectDom : ( oFunc.detectDom ? [oFunc.detectDom] : [] );
                const promise_css = Promise.all(selectors.map(selector => detectDom(selector)));

                // 等待上述两项完成
                await Promise.all([promise_deps, promise_css]);

                // 检测checkers加载条件
                const checkers = Array.isArray(oFunc.checkers) ? oFunc.checkers : ( oFunc.checkers ? [oFunc.checkers] : [] );
                if (!testCheckers(checkers)) {
                    return;
                }

                // 处理substorage
                const substorage = FuncPool.#MakeSubStorage(oFunc.id);

                // 处理函数参数
                const builtins = {
                    oFunc,
                    ...substorage
                };
                const params = oFunc.params ? (Array.isArray(oFunc.params) ? oFunc.params : [oFunc.params]) : [];
                const args = params.map(param => typeof param === 'string' ? builtins[param] : param);

                // 执行函数对象
                const raw_return_value = oFunc.func(...args);
                const return_value = await Promise.resolve(raw_return_value);

                // 设置返回值
                this.#oFuncs.set(oFunc, return_value);

                // 广播事件
                this.dispatchEvent(new CustomEvent('load', {
                    detail: {
                        oFunc, id: oFunc.id, return_value
                    }
                }));
                Array.from(this.#oFuncs.values()).every(v => v !== FuncPool.#STILL_LOADING) &&
                    this.dispatchEvent(new CustomEvent('all_load', {}));
            }

            /**
             * 获取指定函数对象的返回值
             * 如果指定的函数对象不存在,返回FunctionNotFound
             * 如果指定的函数对象存在但尚未加载,返回FunctionNotLoaded
             * 如果函数对象指定了readonly为真值,则返回前用Proxy包装返回值,使其不可修改
             * @param {string} id - 函数对象的id
             * @returns {*}
             */
            require(id) {
                for (const [oFunc, return_value] of this.#oFuncs.entries()) {
                    if (oFunc.id === id) {
                        if (return_value === FuncPool.#STILL_LOADING) {
                            return FuncPool.FunctionNotLoaded;
                        } else {
                            return oFunc.readonly ? FuncPool.#MakeReadonlyObj(return_value) : return_value;
                        }
                    }
                }
                return FuncPool.FunctionNotFound;
            }

            isLoaded(id) {
                for (const [oFunc, return_value] of this.#oFuncs.entries()) {
                    if (oFunc.id === id) {
                        if (return_value === FuncPool.#STILL_LOADING) {
                            return false;
                        } else {
                            return true;
                        }
                    }
                    return false;
                }
            }

            /**
             * 以Proxy包装value,使其属性只读
             * 如果传入的不是object,则直接返回value
             * @param {Object} val
             * @returns {Proxy}
             */
            static #MakeReadonlyObj(val) {
                return isObject(val) ? new Proxy(val, {
                    get: function(target, property, receiver) {
                        return FuncPool.#MakeReadonlyObj(target[property]);
                    },
                    set: function(target, property, value, receiver) {},
                    has: function(target, prop) {},
                    setPrototypeOf(target, newProto) {
                        return false;
                    },
                    defineProperty(target, property, descriptor) {
                        return true;
                    },
                    deleteProperty(target, property) {
                        return false;
                    },
                    preventExtensions(target) {
                        return false;
                    }
                }) : val;

                function isObject(value) {
                    return ['object', 'function'].includes(typeof value) && value !== null;
                }
            }

            /**
             * 创建适用于子功能函数的 GM_setValue, GM_getValue, GM_deleteValue 和 GM_listValues \
             * 调用返回的`GM_setValue(str, val)`相当于对脚本管理器提供的GM*函数进行如下调用:
             * ``` javascript
             * const obj = GM_getValue(key, {});
             * if (typeof obj !== 'object' or obj === null) { throw new TypeError(''); }
             * obj[str] = val;
             * GM_setValue(key, obj);
             * ```
             * @param {string} key - 实际调用用户脚本管理器的GM*函数时提供的key,一般是子功能函数id
             * @returns {{ GM_setValue: function, GM_getValue: function, GM_deleteValue: function, GM_listValues: function }}
             */
            static #MakeSubStorage(key) {
                const GM_funcs = {
                    GM_setValue: typeof GM_setValue === 'function' ? GM_setValue : null,
                    GM_getValue: typeof GM_getValue === 'function' ? GM_getValue : null,
                    GM_deleteValue: typeof GM_deleteValue === 'function' ? GM_deleteValue : null,
                    GM_listValues: typeof GM_listValues === 'function' ? GM_listValues : null,
                }
                return {
                    GM_setValue(name, val) {
                        checkGrant(['GM_setValue', 'GM_getValue'], 'GM_setValue');
                        const obj = GM_funcs.GM_getValue(key, {});
                        Assert(isObject(obj), `FunctionLoader: storage item of key ${name} should be an object`, TypeError);
                        obj[name] = val;
                        GM_funcs.GM_setValue(key, obj);
                    },
                    GM_getValue(name, default_value=null) {
                        checkGrant(['GM_getValue'], 'GM_getValue');
                        const obj = GM_funcs.GM_getValue(key, {});
                        return obj.hasOwnProperty(name) ? obj[name] : default_value;
                    },
                    GM_deleteValue(name) {
                        checkGrant(['GM_setValue', 'GM_getValue'], 'GM_deleteValue');
                        const obj = GM_funcs.GM_getValue(key, {});
                        delete obj[name];
                        GM_funcs.GM_setValue(key, obj);
                    },
                    GM_listValues() {
                        checkGrant(['GM_getValue'], 'GM_listValues');
                        const obj = GM_funcs.GM_getValue(key, {});
                        return Object.keys(obj);
                    }
                };

                /**
                 * 检查指定的GM_*函数是否存在,不存在就抛出错误
                 * @param {string|string[]} funcnames
                 * @param {string} calling - 正在调用的GM_函数的名字,输出错误信息时用
                 */
                function checkGrant(funcnames, calling) {
                    Array.isArray(funcnames) || (funcnames = [funcnames]);
                    for (const funcname of funcnames) {
                        Assert(GM_funcs[funcname], `FunctionLoader: @grant ${funcname} in userscript metadata before using ${calling}`, TypeError);
                    }
                }

                function isObject(val) {
                    return typeof val === 'object' && val !== null;
                }
            }
        }
        class RedeclarationError extends TypeError {}
        class CircularDependencyError extends ReferenceError {}


        // 预置的函数池
        const default_pool = new FuncPool();

        /**
         * 在预置的函数池中加载函数对象或其数组
         * @param {oFunc[]|oFunc} oFuncs - 需要执行的函数对象
         * @returns {FuncPool}
         */
        function loadFuncs(oFuncs) {
            default_pool.load(oFuncs);
            return default_pool;
        }

        /**
         * 在预置的函数池中获取函数对象的返回值
         * @param {string} id - 函数对象的字符串id
         * @returns {*}
         */
        function require(id) {
            return default_pool.require(id);
        }

        /**
         * 在预置的函数池中检查指定函数对象是否已经加载完毕(有返回值可用)
         * @param {string} id - 函数对象的字符串id
         * @returns {boolean}
         */
        function isLoaded(id) {
            return default_pool.isLoaded(id);
        }

        /**
         * 测试给定checker是否检测通过
         * 给定多个checker时,checkers之间是 或 关系,有一个checker通过即算作整体通过
         * 注意此函数设计和旧版testChecker的设计不同,旧版中一个checker可以有多个值,还可通过checker.all指定多值之间的关系为 与 还是 或
         * @param {checker[]|checker} [checkers] - 需要检测的checkers
         * @returns {boolean}
         */
        function testCheckers(checkers=[]) {
            checkers = Array.isArray(checkers) ? checkers : [checkers];
            return checkers.length === 0 || checkers.some(checker => !!registered_checkers[checker.type]?.(checker.value));
        }

        /**
         * 注册新checker
         * 如果给定type已经被其他checker占用,则会报错RedeclarationError
         * @param {string} type - checker类名
         * @param {function} func - checker implementation
         */
        function registerChecker(type, func) {
            if (registered_checkers.hasOwnProperty(type)) {
                throw RedeclarationError(`Attempts to register checker with type already in use: ${type}`);
            }
            registered_checkers[type] = func;
        }

        const FunctionLoader = {
            FuncPool,
            testCheckers,
            registerChecker,
            get checkers() {
                return Object.assign({}, registered_checkers);
            },
            Error: {
                RedeclarationError,
                CircularDependencyError
            }
        };
        return [FunctionLoader, loadFuncs, require, isLoaded];
    }) ();

    return [
        // Console & Debug
        LogLevel, DoLog, Err, Assert,

        // DOM
        $, $All, $CrE, $AEL, $$CrE, addStyle, detectDom, destroyEvent,

        // Data
        copyProp, copyProps, parseArgs, escJsStr, replaceText,

        // Environment & Browser
        getUrlArgv, dl_browser, dl_GM,

        // Logic & Task
        AsyncManager, queueTask, FunctionLoader, loadFuncs, require, isLoaded
    ];
}) ();