Basic Functions (For userscripts)

Useful functions for myself

Version vom 25.01.2025. Aktuellste Version

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greatest.deepsurf.us/scripts/456034/1526597/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.0
// @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 
     */
	function $() {
		switch(arguments.length) {
			case 2:
				return arguments[0].querySelector(arguments[1]);
				break;
			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 
     */
	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($(`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[]} [selectors] - selector(s) to observe for
     * @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 [root, selectors, attributes, callback] = parseArgs([...arguments], [
			function(args, defaultValues) {
				const arg = args[0];
				return {
					'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
					'object': () => ['root', 'selector', 'attributes', 'callback'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
				}[typeof arg]();
			},
			[1,2],
			[1,2,3],
			[1,2,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 [url, name, 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]();
			},
			[1,2],
			[1,2,3],
			[1,2,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
         */
        /**
         * 被加载函数对象的func函数
         * @callback oFuncBody
         * @param {oFunc} oFunc
         * @returns {*|Promise<*>}
         */
        /**
         * 被加载执行的函数对象
         * @typedef {Object} oFunc
         * @property {string} id - 每次load(每个FuncPool实例)内唯一的标识符
         * @property {checker[]|checker} [checkers] - oFunc执行的条件
         * @property {string[]|string} [detectDom] - 如果提供,开始checker检查前会首先等待其中所有css选择器对应的元素在document中出现
         * @property {string[]|string} [dependencies] - 如果提供,应为其他函数对象的id或者id列表;开始checker检查前会首先等待其中所有指定的函数对象加载完毕
         * @property {boolean} [readonly] - 指定该函数的返回值是否应该被Proxy保护为不可修改对象
         * @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;

                // 已经在函数池中的函数对象,不重复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;
                }

                // 执行函数对象
                const raw_return_value = oFunc.func(oFunc);
                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,使其属性只读
             * 如果传入的不是obj,则直接返回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;
                }
            }
        }
        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
	];
}) ();