UNPKG

tiny-essentials

Version:

Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.

180 lines (179 loc) 7.88 kB
/** * A utility class to manage dynamically adjusted timeouts based on how often * each unique ID is triggered. Also provides polling support for asynchronous conditions. */ class TinyTimeout { /** @type {boolean} Whether this instance has been destroyed. */ #isDestroyed = false; /** @type {boolean} Whether to allow auto-updating an ID's timeout config if `value` changes. */ #allowAutoConfigChange; /** @type {number} The interval time (ms) used to decrement cooldown counters. */ #cooldownWatcherTime; /** @type {NodeJS.Timeout|null} Reference to the internal cooldown interval. */ #cooldownWatcher = null; /** * Internal map that keeps track of how many times each ID has been triggered, * along with the base multiplier used to calculate delays. * * @type {Map<string, { value: number, now: number }>} */ #timeoutFixer = new Map(); /** * Creates a new instance of TinyTimeout. * * @param {Object} [options={}] Optional configuration object. * @param {number} [options.cooldownWatcherTime=5000] Interval in milliseconds for reducing `now` counters. * @param {boolean} [options.allowAutoConfigChange=false] Whether to allow auto value changes for existing IDs. */ constructor({ cooldownWatcherTime = 5000, allowAutoConfigChange = false } = {}) { if (!Number.isFinite(cooldownWatcherTime) || cooldownWatcherTime <= 0) throw new TypeError(`Expected 'cooldownWatcherTime' to be a positive number.`); if (typeof allowAutoConfigChange !== 'boolean') throw new TypeError(`Expected 'allowAutoConfigChange' to be a boolean.`); this.#cooldownWatcherTime = cooldownWatcherTime; this.#allowAutoConfigChange = allowAutoConfigChange; this.setCooldownWatcherTime(cooldownWatcherTime); } /** * Whether this instance has been destroyed and is no longer usable. * * @returns {boolean} */ isDestroyed() { return this.#isDestroyed; } /** * Whether auto config change is enabled. * * @returns {boolean} */ getAllowAutoConfigChange() { return this.#allowAutoConfigChange; } /** * Gets the interval time used for cooldown decrementing. * * @returns {number} */ getCooldownWatcherTime() { return this.#cooldownWatcherTime; } /** * Sets whether to allow auto-updating an ID's timeout config if `value` changes. * * @param {boolean} value */ setAllowAutoConfigChange(value) { if (typeof value !== 'boolean') throw new TypeError(`Expected 'value' to be a boolean.`); this.#allowAutoConfigChange = value; } /** * Sets the cooldown watcher interval time. * Automatically resets the interval if it was already running. * * @param {number} value */ setCooldownWatcherTime(value) { if (this.#isDestroyed) throw new Error('TinyTimeout has been destroyed.'); if (!Number.isFinite(value) || value <= 0) throw new TypeError(`Expected 'value' to be a positive number.`); this.#cooldownWatcherTime = value; if (this.#cooldownWatcher) clearInterval(this.#cooldownWatcher); this.#cooldownWatcher = setInterval(() => { this.#timeoutFixer.forEach((data) => { if (data.now > 0) data.now--; }); }, this.#cooldownWatcherTime); } /** * Schedules a callback using a dynamically adjusted timeout based on usage frequency. * The more often an ID is triggered, the longer the timeout becomes, * scaled by the provided `value`. Optionally, the delay can be limited by `limit`. * * @param {string} id - A unique identifier to track timeout usage. * @param {Function} callback - The function to execute after the delay. * @param {number} value - Base delay multiplier in milliseconds. * @param {number|null} [limit=null] - Optional maximum delay cap. * @returns {number} Handle to the scheduled timeout. * @throws {Error} Throws if the instance has been destroyed or arguments are invalid. */ set(id, callback, value, limit = null) { if (this.#isDestroyed) throw new Error('TinyTimeout has been destroyed.'); if (typeof id !== 'string' || id.trim() === '') throw new TypeError(`Expected 'id' to be a non-empty string.`); if (typeof callback !== 'function') throw new TypeError(`Expected 'callback' to be a function.`); if (!Number.isFinite(value) || value < 0) throw new TypeError(`Expected 'value' to be a non-negative number.`); if (limit !== null && (!Number.isFinite(limit) || limit < 0)) throw new TypeError(`Expected 'limit' to be null or a non-negative number.`); let entry = this.#timeoutFixer.get(id); if (!entry || (this.#allowAutoConfigChange && value !== entry.value)) { entry = { value, now: 0 }; this.#timeoutFixer.set(id, entry); } const delay = entry.value * entry.now; entry.now++; return setTimeout(callback, typeof limit === 'number' ? Math.min(delay, limit) : delay); } /** * Waits until a provided function returns `true`, checking repeatedly at the defined interval. * Useful for polling asynchronous conditions. * * @param {() => boolean} getValue - A function that returns `true` when the condition is met. * @param {number} [checkInterval=100] - How often (in ms) to check the condition. * @returns {Promise<void>} Resolves when the condition is met. * @throws {TypeError} If arguments are invalid. */ static waitForTrue(getValue, checkInterval = 100) { if (typeof getValue !== 'function') throw new TypeError(`Expected 'getValue' to be a function.`); if (!Number.isFinite(checkInterval) || checkInterval <= 0) throw new TypeError(`Expected 'checkInterval' to be a positive number.`); return new Promise((resolve) => { const interval = setInterval(() => { if (getValue()) { clearInterval(interval); resolve(); } }, checkInterval); }); } /** * Instance version of `waitForTrue`, which defaults to using the instance's * cooldownWatcherTime if not explicitly provided. * * @param {() => boolean} getValue - A function that returns `true` when the condition is met. * @param {number|null} [checkInterval=100] - How often (in ms) to check the condition. * @returns {Promise<void>} Resolves when the condition is met. * @throws {Error} If the instance is destroyed or arguments are invalid. */ waitForTrue(getValue, checkInterval = 100) { if (this.#isDestroyed) throw new Error('TinyTimeout has been destroyed.'); if (typeof getValue !== 'function') throw new TypeError(`Expected 'getValue' to be a function.`); if (checkInterval !== null && (!Number.isFinite(checkInterval) || checkInterval <= 0)) throw new TypeError(`Expected 'checkInterval' to be null or a positive number.`); return TinyTimeout.waitForTrue(getValue, checkInterval ?? this.#cooldownWatcherTime); } /** * Cleans up all internal references and stops the cooldown watcher. * After calling this, the instance becomes unusable. */ destroy() { if (this.#isDestroyed) return; this.#isDestroyed = true; if (this.#cooldownWatcher) clearInterval(this.#cooldownWatcher); this.#cooldownWatcher = null; this.#timeoutFixer.clear(); } } export default TinyTimeout;