mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' of https://github.com/city-unit/SillyTavern into feature/exorcism
This commit is contained in:
@ -1,19 +1,57 @@
|
||||
import { getContext } from "./extensions.js";
|
||||
import { getRequestHeaders } from "../script.js";
|
||||
import { isMobile } from "./RossAscends-mods.js";
|
||||
|
||||
/**
|
||||
* Pagination status string template.
|
||||
* @type {string}
|
||||
*/
|
||||
export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> of <%= totalNumber %>';
|
||||
|
||||
/**
|
||||
* Navigation options for pagination.
|
||||
* @enum {number}
|
||||
*/
|
||||
export const navigation_option = { none: 0, previous: 1, last: 2, };
|
||||
|
||||
/**
|
||||
* Determines if a value is unique in an array.
|
||||
* @param {any} value Current value.
|
||||
* @param {number} index Current index.
|
||||
* @param {any} array The array being processed.
|
||||
* @returns {boolean} True if the value is unique, false otherwise.
|
||||
*/
|
||||
export function onlyUnique(value, index, array) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string only contains digits.
|
||||
* @param {string} str The string to check.
|
||||
* @returns {boolean} True if the string only contains digits, false otherwise.
|
||||
* @example
|
||||
* isDigitsOnly('123'); // true
|
||||
* isDigitsOnly('abc'); // false
|
||||
*/
|
||||
export function isDigitsOnly(str) {
|
||||
return /^\d+$/.test(str);
|
||||
}
|
||||
|
||||
// Increase delay on touch screens
|
||||
/**
|
||||
* Gets a drag delay for sortable elements. This is to prevent accidental drags when scrolling.
|
||||
* @returns {number} The delay in milliseconds. 50ms for desktop, 750ms for mobile.
|
||||
*/
|
||||
export function getSortableDelay() {
|
||||
return navigator.maxTouchPoints > 0 ? 750 : 100;
|
||||
return isMobile() ? 750 : 50;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rearranges an array in a random order.
|
||||
* @param {any[]} array The array to shuffle.
|
||||
* @returns {any[]} The shuffled array.
|
||||
* @example
|
||||
* shuffle([1, 2, 3]); // [2, 3, 1]
|
||||
*/
|
||||
export function shuffle(array) {
|
||||
let currentIndex = array.length,
|
||||
randomIndex;
|
||||
@ -29,6 +67,12 @@ export function shuffle(array) {
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file to the user's devices.
|
||||
* @param {BlobPart} content File content to download.
|
||||
* @param {string} fileName File name.
|
||||
* @param {string} contentType File content type.
|
||||
*/
|
||||
export function download(content, fileName, contentType) {
|
||||
const a = document.createElement("a");
|
||||
const file = new Blob([content], { type: contentType });
|
||||
@ -37,22 +81,38 @@ export function download(content, fileName, contentType) {
|
||||
a.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a file by URL and parses its contents as data URI.
|
||||
* @param {string} url The URL to fetch.
|
||||
* @param {any} params Fetch parameters.
|
||||
* @returns {Promise<string>} A promise that resolves to the data URI.
|
||||
*/
|
||||
export async function urlContentToDataUri(url, params) {
|
||||
const response = await fetch(url, params);
|
||||
const blob = await response.blob();
|
||||
return await new Promise(callback => {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function () { callback(this.result); };
|
||||
return await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
resolve(String(reader.result));
|
||||
};
|
||||
reader.onerror = function (error) {
|
||||
reject(error);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the file's text.
|
||||
* @param {Blob} file The file to read.
|
||||
* @returns {Promise<string>} A promise that resolves to the file's text.
|
||||
*/
|
||||
export function getFileText(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file);
|
||||
reader.onload = function () {
|
||||
resolve(reader.result);
|
||||
resolve(String(reader.result));
|
||||
};
|
||||
reader.onerror = function (error) {
|
||||
reject(error);
|
||||
@ -60,6 +120,10 @@ export function getFileText(file) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the file's array buffer.
|
||||
* @param {Blob} file The file to read.
|
||||
*/
|
||||
export function getFileBuffer(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@ -73,12 +137,17 @@ export function getFileBuffer(file) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the base64 encoded string of a file.
|
||||
* @param {Blob} file The file to read.
|
||||
* @returns {Promise<string>} A promise that resolves to the base64 encoded string.
|
||||
*/
|
||||
export function getBase64Async(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = function () {
|
||||
resolve(reader.result);
|
||||
resolve(String(reader.result));
|
||||
};
|
||||
reader.onerror = function (error) {
|
||||
reject(error);
|
||||
@ -86,15 +155,26 @@ export function getBase64Async(file) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a file blob as a JSON object.
|
||||
* @param {Blob} file The file to read.
|
||||
* @returns {Promise<any>} A promise that resolves to the parsed JSON object.
|
||||
*/
|
||||
export async function parseJsonFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = event => resolve(JSON.parse(event.target.result));
|
||||
fileReader.onerror = error => reject(error);
|
||||
fileReader.readAsText(file);
|
||||
fileReader.onload = event => resolve(JSON.parse(String(event.target.result)));
|
||||
fileReader.onerror = error => reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a hash code for a string.
|
||||
* @param {string} str The string to hash.
|
||||
* @param {number} [seed=0] The seed to use for the hash.
|
||||
* @returns {number} The hash code.
|
||||
*/
|
||||
export function getStringHash(str, seed = 0) {
|
||||
if (typeof str !== 'string') {
|
||||
return 0;
|
||||
@ -114,6 +194,12 @@ export function getStringHash(str, seed = 0) {
|
||||
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
|
||||
* @param {function} func The function to debounce.
|
||||
* @param {number} [timeout=300] The timeout in milliseconds.
|
||||
* @returns {function} The debounced function.
|
||||
*/
|
||||
export function debounce(func, timeout = 300) {
|
||||
let timer;
|
||||
return (...args) => {
|
||||
@ -122,6 +208,12 @@ export function debounce(func, timeout = 300) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a throttled function that only invokes func at most once per every limit milliseconds.
|
||||
* @param {function} func The function to throttle.
|
||||
* @param {number} [limit=300] The limit in milliseconds.
|
||||
* @returns {function} The throttled function.
|
||||
*/
|
||||
export function throttle(func, limit = 300) {
|
||||
let lastCall;
|
||||
return (...args) => {
|
||||
@ -133,6 +225,11 @@ export function throttle(func, limit = 300) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an element is in the viewport.
|
||||
* @param {Element} el The element to check.
|
||||
* @returns {boolean} True if the element is in the viewport, false otherwise.
|
||||
*/
|
||||
export function isElementInViewport(el) {
|
||||
if (typeof jQuery === "function" && el instanceof jQuery) {
|
||||
el = el[0];
|
||||
@ -146,6 +243,12 @@ export function isElementInViewport(el) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a name that is unique among the names that exist.
|
||||
* @param {string} name The name to check.
|
||||
* @param {{ (y: any): boolean; }} exists Function to check if name exists.
|
||||
* @returns {string} A unique name.
|
||||
*/
|
||||
export function getUniqueName(name, exists) {
|
||||
let i = 1;
|
||||
let baseName = name;
|
||||
@ -156,18 +259,48 @@ export function getUniqueName(name, exists) {
|
||||
return name;
|
||||
}
|
||||
|
||||
export const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||
export const isSubsetOf = (a, b) => (Array.isArray(a) && Array.isArray(b)) ? b.every(val => a.includes(val)) : false;
|
||||
/**
|
||||
* Returns a promise that resolves after the specified number of milliseconds.
|
||||
* @param {number} ms The number of milliseconds to wait.
|
||||
* @returns {Promise<void>} A promise that resolves after the specified number of milliseconds.
|
||||
*/
|
||||
export function delay(ms) {
|
||||
return new Promise((res) => setTimeout(res, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an array is a subset of another array.
|
||||
* @param {any[]} a Array A
|
||||
* @param {any[]} b Array B
|
||||
* @returns {boolean} True if B is a subset of A, false otherwise.
|
||||
*/
|
||||
export function isSubsetOf(a, b) {
|
||||
return (Array.isArray(a) && Array.isArray(b)) ? b.every(val => a.includes(val)) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the trailing number in a string.
|
||||
* @param {string} str The string to process.
|
||||
* @returns {string} The string with the trailing number incremented by 1.
|
||||
* @example
|
||||
* incrementString('Hello, world! 1'); // 'Hello, world! 2'
|
||||
*/
|
||||
export function incrementString(str) {
|
||||
// Find the trailing number or it will match the empty string
|
||||
const count = str.match(/\d*$/);
|
||||
|
||||
// Take the substring up until where the integer was matched
|
||||
// Concatenate it to the matched count incremented by 1
|
||||
return str.substr(0, count.index) + (++count[0]);
|
||||
return str.substring(0, count.index) + (Number(count[0]) + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a string using the specified arguments.
|
||||
* @param {string} format The format string.
|
||||
* @returns {string} The formatted string.
|
||||
* @example
|
||||
* stringFormat('Hello, {0}!', 'world'); // 'Hello, world!'
|
||||
*/
|
||||
export function stringFormat(format) {
|
||||
const args = Array.prototype.slice.call(arguments, 1);
|
||||
return format.replace(/{(\d+)}/g, function (match, number) {
|
||||
@ -178,7 +311,11 @@ export function stringFormat(format) {
|
||||
});
|
||||
};
|
||||
|
||||
// Save the caret position in a contenteditable element
|
||||
/**
|
||||
* Save the caret position in a contenteditable element.
|
||||
* @param {Element} element The element to save the caret position of.
|
||||
* @returns {{ start: number, end: number }} An object with the start and end offsets of the caret.
|
||||
*/
|
||||
export function saveCaretPosition(element) {
|
||||
// Get the current selection
|
||||
const selection = window.getSelection();
|
||||
@ -207,7 +344,11 @@ export function saveCaretPosition(element) {
|
||||
return position;
|
||||
}
|
||||
|
||||
// Restore the caret position in a contenteditable element
|
||||
/**
|
||||
* Restore the caret position in a contenteditable element.
|
||||
* @param {Element} element The element to restore the caret position of.
|
||||
* @param {{ start: any; end: any; }} position An object with the start and end offsets of the caret.
|
||||
*/
|
||||
export function restoreCaretPosition(element, position) {
|
||||
// If the position is null, do nothing
|
||||
if (!position) {
|
||||
@ -234,6 +375,11 @@ export async function resetScrollHeight(element) {
|
||||
$(element).css('height', $(element).prop('scrollHeight') + 3 + 'px');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the height of an element to its scroll height.
|
||||
* @param {JQuery<HTMLElement>} element The element to initialize the scroll height of.
|
||||
* @returns {Promise<void>} A promise that resolves when the scroll height has been initialized.
|
||||
*/
|
||||
export async function initScrollHeight(element) {
|
||||
await delay(1);
|
||||
|
||||
@ -250,15 +396,27 @@ export async function initScrollHeight(element) {
|
||||
//resetScrollHeight(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares elements by their CSS order property. Used for sorting.
|
||||
* @param {any} a The first element.
|
||||
* @param {any} b The second element.
|
||||
* @returns {number} A negative number if a is before b, a positive number if a is after b, or 0 if they are equal.
|
||||
*/
|
||||
export function sortByCssOrder(a, b) {
|
||||
const _a = Number($(a).css('order'));
|
||||
const _b = Number($(b).css('order'));
|
||||
return _a - _b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims a string to the end of a nearest sentence.
|
||||
* @param {string} input The string to trim.
|
||||
* @param {boolean} include_newline Whether to include a newline character in the trimmed string.
|
||||
* @returns {string} The trimmed string.
|
||||
* @example
|
||||
* end_trim_to_sentence('Hello, world! I am from'); // 'Hello, world!'
|
||||
*/
|
||||
export function end_trim_to_sentence(input, include_newline = false) {
|
||||
// inspired from https://github.com/kaihordewebui/kaihordewebui.github.io/blob/06b95e6b7720eb85177fbaf1a7f52955d7cdbc02/index.html#L4853-L4867
|
||||
|
||||
const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '!', '?', '”', ')', '】', '】', '’', '」', '】']); // extend this as you see fit
|
||||
let last = -1;
|
||||
|
||||
@ -283,6 +441,15 @@ export function end_trim_to_sentence(input, include_newline = false) {
|
||||
return input.substring(0, last + 1).trimEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of occurrences of a character in a string.
|
||||
* @param {string} string The string to count occurrences in.
|
||||
* @param {string} character The character to count occurrences of.
|
||||
* @returns {number} The number of occurrences of the character in the string.
|
||||
* @example
|
||||
* countOccurrences('Hello, world!', 'l'); // 3
|
||||
* countOccurrences('Hello, world!', 'x'); // 0
|
||||
*/
|
||||
export function countOccurrences(string, character) {
|
||||
let count = 0;
|
||||
|
||||
@ -295,6 +462,14 @@ export function countOccurrences(string, character) {
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a number is odd.
|
||||
* @param {number} number The number to check.
|
||||
* @returns {boolean} True if the number is odd, false otherwise.
|
||||
* @example
|
||||
* isOdd(3); // true
|
||||
* isOdd(4); // false
|
||||
*/
|
||||
export function isOdd(number) {
|
||||
return number % 2 !== 0;
|
||||
}
|
||||
@ -335,6 +510,12 @@ export function timestampToMoment(timestamp) {
|
||||
return moment.invalid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two moment objects for sorting.
|
||||
* @param {*} a The first moment object.
|
||||
* @param {*} b The second moment object.
|
||||
* @returns {number} A negative number if a is before b, a positive number if a is after b, or 0 if they are equal.
|
||||
*/
|
||||
export function sortMoments(a, b) {
|
||||
if (a.isBefore(b)) {
|
||||
return 1;
|
||||
@ -345,14 +526,21 @@ export function sortMoments(a, b) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Split string to parts no more than length in size */
|
||||
export function splitRecursive(input, length, delimitiers = ['\n\n', '\n', ' ', '']) {
|
||||
const delim = delimitiers[0] ?? '';
|
||||
/** Split string to parts no more than length in size.
|
||||
* @param {string} input The string to split.
|
||||
* @param {number} length The maximum length of each part.
|
||||
* @param {string[]} delimiters The delimiters to use when splitting the string.
|
||||
* @returns {string[]} The split string.
|
||||
* @example
|
||||
* splitRecursive('Hello, world!', 3); // ['Hel', 'lo,', 'wor', 'ld!']
|
||||
*/
|
||||
export function splitRecursive(input, length, delimiters = ['\n\n', '\n', ' ', '']) {
|
||||
const delim = delimiters[0] ?? '';
|
||||
const parts = input.split(delim);
|
||||
|
||||
const flatParts = parts.flatMap(p => {
|
||||
if (p.length < length) return p;
|
||||
return splitRecursive(input, length, delimitiers.slice(1));
|
||||
return splitRecursive(input, length, delimiters.slice(1));
|
||||
});
|
||||
|
||||
// Merge short chunks
|
||||
@ -376,6 +564,13 @@ export function splitRecursive(input, length, delimitiers = ['\n\n', '\n', ' ',
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid data URL.
|
||||
* @param {string} str The string to check.
|
||||
* @returns {boolean} True if the string is a valid data URL, false otherwise.
|
||||
* @example
|
||||
* isDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...'); // true
|
||||
*/
|
||||
export function isDataURL(str) {
|
||||
const regex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)*;?)?(base64)?,([a-z0-9!$&',()*+;=\-_%.~:@\/?#]+)?$/i;
|
||||
return regex.test(str);
|
||||
@ -390,6 +585,13 @@ export function getCharaFilename(chid) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts words from a string.
|
||||
* @param {string} value The string to extract words from.
|
||||
* @returns {string[]} The extracted words.
|
||||
* @example
|
||||
* extractAllWords('Hello, world!'); // ['hello', 'world']
|
||||
*/
|
||||
export function extractAllWords(value) {
|
||||
const words = [];
|
||||
|
||||
@ -404,21 +606,45 @@ export function extractAllWords(value) {
|
||||
return words;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a string for use in a regular expression.
|
||||
* @param {string} string The string to escape.
|
||||
* @returns {string} The escaped string.
|
||||
* @example
|
||||
* escapeRegex('^Hello$'); // '\\^Hello\\$'
|
||||
*/
|
||||
export function escapeRegex(string) {
|
||||
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an interface for rate limiting function calls.
|
||||
*/
|
||||
export class RateLimiter {
|
||||
constructor(intervalMillis) {
|
||||
this._intervalMillis = intervalMillis;
|
||||
this._lastResolveTime = 0;
|
||||
this._pendingResolve = Promise.resolve();
|
||||
/**
|
||||
* Creates a new RateLimiter.
|
||||
* @param {number} interval The interval in milliseconds.
|
||||
* @example
|
||||
* const rateLimiter = new RateLimiter(1000);
|
||||
* rateLimiter.waitForResolve().then(() => {
|
||||
* console.log('Waited 1000ms');
|
||||
* });
|
||||
*/
|
||||
constructor(interval) {
|
||||
this.interval = interval;
|
||||
this.lastResolveTime = 0;
|
||||
this.pendingResolve = Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the remaining time in the interval.
|
||||
* @param {AbortSignal} abortSignal An optional AbortSignal to abort the wait.
|
||||
* @returns {Promise<void>} A promise that resolves when the remaining time has elapsed.
|
||||
*/
|
||||
_waitRemainingTime(abortSignal) {
|
||||
const currentTime = Date.now();
|
||||
const elapsedTime = currentTime - this._lastResolveTime;
|
||||
const remainingTime = Math.max(0, this._intervalMillis - elapsedTime);
|
||||
const elapsedTime = currentTime - this.lastResolveTime;
|
||||
const remainingTime = Math.max(0, this.interval - elapsedTime);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@ -434,19 +660,29 @@ export class RateLimiter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the next interval to elapse.
|
||||
* @param {AbortSignal} abortSignal An optional AbortSignal to abort the wait.
|
||||
* @returns {Promise<void>} A promise that resolves when the next interval has elapsed.
|
||||
*/
|
||||
async waitForResolve(abortSignal) {
|
||||
await this._pendingResolve;
|
||||
this._pendingResolve = this._waitRemainingTime(abortSignal);
|
||||
await this.pendingResolve;
|
||||
this.pendingResolve = this._waitRemainingTime(abortSignal);
|
||||
|
||||
// Update the last resolve time
|
||||
this._lastResolveTime = Date.now() + this._intervalMillis;
|
||||
console.debug(`RateLimiter.waitForResolve() ${this._lastResolveTime}`);
|
||||
this.lastResolveTime = Date.now() + this.interval;
|
||||
console.debug(`RateLimiter.waitForResolve() ${this.lastResolveTime}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Taken from https://github.com/LostRuins/lite.koboldai.net/blob/main/index.html
|
||||
//import tavern png data. adapted from png-chunks-extract under MIT license
|
||||
//accepts png input data, and returns the extracted JSON
|
||||
/**
|
||||
* Extracts a JSON object from a PNG file.
|
||||
* Taken from https://github.com/LostRuins/lite.koboldai.net/blob/main/index.html
|
||||
* Adapted from png-chunks-extract under MIT license
|
||||
* @param {Uint8Array} data The PNG data to extract the JSON from.
|
||||
* @param {string} identifier The identifier to look for in the PNG tEXT data.
|
||||
* @returns {object} The extracted JSON object.
|
||||
*/
|
||||
export function extractDataFromPng(data, identifier = 'chara') {
|
||||
console.log("Attempting PNG import...");
|
||||
let uint8 = new Uint8Array(4);
|
||||
@ -629,6 +865,13 @@ export function loadFileToDocument(url, type) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a thumbnail from a data URL.
|
||||
* @param {string} dataUrl The data URL encoded data of the image.
|
||||
* @param {number} maxWidth The maximum width of the thumbnail.
|
||||
* @param {number} maxHeight The maximum height of the thumbnail.
|
||||
* @returns {Promise<string>} A promise that resolves to the thumbnail data URL.
|
||||
*/
|
||||
export function createThumbnail(dataUrl, maxWidth, maxHeight) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
@ -664,6 +907,13 @@ export function createThumbnail(dataUrl, maxWidth, maxHeight) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a condition to be true. Throws an error if the condition is not true within the timeout.
|
||||
* @param {{ (): boolean; }} condition The condition to wait for.
|
||||
* @param {number} [timeout=1000] The timeout in milliseconds.
|
||||
* @param {number} [interval=100] The interval in milliseconds.
|
||||
* @returns {Promise<void>} A promise that resolves when the condition is true.
|
||||
*/
|
||||
export async function waitUntilCondition(condition, timeout = 1000, interval = 100) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
@ -681,6 +931,12 @@ export async function waitUntilCondition(condition, timeout = 1000, interval = 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a UUID v4 string.
|
||||
* @returns {string} A UUID v4 string.
|
||||
* @example
|
||||
* uuidv4(); // '3e2fd9e1-0a7a-4f6d-9aaf-8a7a4babe7eb'
|
||||
*/
|
||||
export function uuidv4() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
@ -689,6 +945,11 @@ export function uuidv4() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones an object using JSON serialization.
|
||||
* @param {any} obj The object to clone.
|
||||
* @returns {any} A deep clone of the object.
|
||||
*/
|
||||
export function deepClone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
Reference in New Issue
Block a user