UNPKG

@sv443-network/coreutils

Version:

Cross-platform, general-purpose, JavaScript core library for Node, Deno and the browser. Intended to be used in conjunction with `@sv443-network/userutils` and `@sv443-network/djsutils`, but can be used independently as well.

1,195 lines (1,183 loc) 46.3 kB
// lib/math.ts function bitSetHas(bitSet, checkVal) { return (bitSet & checkVal) === checkVal; } function clamp(value, min, max) { if (typeof max !== "number") { max = min; min = 0; } return Math.max(Math.min(value, max), min); } function digitCount(num, withDecimals = true) { num = Number(!["string", "number"].includes(typeof num) ? String(num) : num); if (typeof num === "number" && isNaN(num)) return NaN; const [intPart, decPart] = num.toString().split("."); const intDigits = intPart === "0" ? 1 : Math.floor(Math.log10(Math.abs(Number(intPart))) + 1); const decDigits = withDecimals && decPart ? decPart.length : 0; return intDigits + decDigits; } function formatNumber(number, locale, format) { return number.toLocaleString( locale, format === "short" ? { notation: "compact", compactDisplay: "short", maximumFractionDigits: 1 } : { style: "decimal", maximumFractionDigits: 0 } ); } function mapRange(value, range1min, range1max, range2min, range2max) { if (typeof range2min === "undefined" || typeof range2max === "undefined") { range2max = range1max; range1max = range1min; range2min = range1min = 0; } if (Number(range1min) === 0 && Number(range2min) === 0) return value * (range2max / range1max); return (value - range1min) * ((range2max - range2min) / (range1max - range1min)) + range2min; } function randRange(...args) { let min, max, enhancedEntropy = false; if (typeof args[0] === "number" && typeof args[1] === "number") [min, max] = args; else if (typeof args[0] === "number" && typeof args[1] !== "number") { min = 0; [max] = args; } else throw new TypeError(`Wrong parameter(s) provided - expected (number, boolean|undefined) or (number, number, boolean|undefined) but got (${args.map((a) => typeof a).join(", ")}) instead`); if (typeof args[2] === "boolean") enhancedEntropy = args[2]; else if (typeof args[1] === "boolean") enhancedEntropy = args[1]; min = Number(min); max = Number(max); if (isNaN(min) || isNaN(max)) return NaN; if (min > max) throw new TypeError(`Parameter "min" can't be bigger than "max"`); if (enhancedEntropy) { const uintArr = new Uint8Array(1); crypto.getRandomValues(uintArr); return Number(Array.from( uintArr, (v) => Math.round(mapRange(v, 0, 255, min, max)).toString(10) ).join("")); } else return Math.floor(Math.random() * (max - min + 1)) + min; } function roundFixed(num, fractionDigits) { const scale = 10 ** fractionDigits; return Math.round(num * scale) / scale; } function valsWithin(a, b, dec = 10, withinRange = 0.5) { return Math.abs(roundFixed(a, dec) - roundFixed(b, dec)) <= withinRange; } // lib/array.ts function randomItem(array) { return randomItemIndex(array)[0]; } function randomItemIndex(array) { if (array.length === 0) return [void 0, void 0]; const idx = randRange(array.length - 1); return [array[idx], idx]; } function randomizeArray(array) { const retArray = [...array]; if (array.length === 0) return retArray; for (let i = retArray.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [retArray[i], retArray[j]] = [retArray[j], retArray[i]]; } return retArray; } function takeRandomItem(arr) { var _a; return (_a = takeRandomItemIndex(arr)) == null ? void 0 : _a[0]; } function takeRandomItemIndex(arr) { const [itm, idx] = randomItemIndex(arr); if (idx === void 0) return [void 0, void 0]; arr.splice(idx, 1); return [itm, idx]; } // lib/colors.ts function darkenColor(color, percent, upperCase = false) { var _a; color = color.trim(); const darkenRgb = (r2, g2, b2, percent2) => { r2 = Math.max(0, Math.min(255, r2 - r2 * percent2 / 100)); g2 = Math.max(0, Math.min(255, g2 - g2 * percent2 / 100)); b2 = Math.max(0, Math.min(255, b2 - b2 * percent2 / 100)); return [r2, g2, b2]; }; let r, g, b, a; const isHexCol = color.match(/^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/); if (isHexCol) [r, g, b, a] = hexToRgb(color); else if (color.startsWith("rgb")) { const rgbValues = (_a = color.match(/\d+(\.\d+)?/g)) == null ? void 0 : _a.map(Number); if (!rgbValues) throw new TypeError("Invalid RGB/RGBA color format"); [r, g, b, a] = rgbValues; } else throw new TypeError("Unsupported color format"); [r, g, b] = darkenRgb(r, g, b, percent); if (isHexCol) return rgbToHex(r, g, b, a, color.startsWith("#"), upperCase); else if (color.startsWith("rgba")) return `rgba(${r}, ${g}, ${b}, ${a ?? NaN})`; else if (color.startsWith("rgb")) return `rgb(${r}, ${g}, ${b})`; else throw new TypeError("Unsupported color format"); } function hexToRgb(hex) { hex = (hex.startsWith("#") ? hex.slice(1) : hex).trim(); const a = hex.length === 8 || hex.length === 4 ? parseInt(hex.slice(-(hex.length / 4)), 16) / (hex.length === 8 ? 255 : 15) : void 0; if (!isNaN(Number(a))) hex = hex.slice(0, -(hex.length / 4)); if (hex.length === 3 || hex.length === 4) hex = hex.split("").map((c) => c + c).join(""); const hexInt = parseInt(hex, 16); const r = hexInt >> 16 & 255; const g = hexInt >> 8 & 255; const b = hexInt & 255; return [clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255), typeof a === "number" ? clamp(a, 0, 1) : void 0]; } function lightenColor(color, percent, upperCase = false) { return darkenColor(color, percent * -1, upperCase); } function rgbToHex(red, green, blue, alpha, withHash = true, upperCase = false) { const toHexVal = (n) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0")[upperCase ? "toUpperCase" : "toLowerCase"](); return `${withHash ? "#" : ""}${toHexVal(red)}${toHexVal(green)}${toHexVal(blue)}${alpha ? toHexVal(alpha * 255) : ""}`; } // lib/crypto.ts function abtoa(buf) { return btoa( new Uint8Array(buf).reduce((data, byte) => data + String.fromCharCode(byte), "") ); } function atoab(str) { return Uint8Array.from(atob(str), (c) => c.charCodeAt(0)); } async function compress(input, compressionFormat, outputType = "string") { const byteArray = input instanceof ArrayBuffer ? input : new TextEncoder().encode((input == null ? void 0 : input.toString()) ?? String(input)); const comp = new CompressionStream(compressionFormat); const writer = comp.writable.getWriter(); writer.write(byteArray); writer.close(); const buf = await new Response(comp.readable).arrayBuffer(); return outputType === "arrayBuffer" ? buf : abtoa(buf); } async function decompress(input, compressionFormat, outputType = "string") { const byteArray = input instanceof ArrayBuffer ? input : atoab((input == null ? void 0 : input.toString()) ?? String(input)); const decomp = new DecompressionStream(compressionFormat); const writer = decomp.writable.getWriter(); writer.write(byteArray); writer.close(); const buf = await new Response(decomp.readable).arrayBuffer(); return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf); } async function computeHash(input, algorithm = "SHA-256") { let data; if (typeof input === "string") { const encoder = new TextEncoder(); data = encoder.encode(input); } else data = input; const hashBuffer = await crypto.subtle.digest(algorithm, data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join(""); return hashHex; } function randomId(length = 16, radix = 16, enhancedEntropy = false, randomCase = true) { if (length < 1) throw new RangeError("The length argument must be at least 1"); if (radix < 2 || radix > 36) throw new RangeError("The radix argument must be between 2 and 36"); let arr = []; const caseArr = randomCase ? [0, 1] : [0]; if (enhancedEntropy) { const uintArr = new Uint8Array(length); crypto.getRandomValues(uintArr); arr = Array.from( uintArr, (v) => mapRange(v, 0, 255, 0, radix).toString(radix).substring(0, 1) ); } else { arr = Array.from( { length }, () => Math.floor(Math.random() * radix).toString(radix) ); } if (!arr.some((v) => /[a-zA-Z]/.test(v))) return arr.join(""); return arr.map((v) => caseArr[randRange(0, caseArr.length - 1, enhancedEntropy)] === 1 ? v.toUpperCase() : v).join(""); } // lib/misc.ts async function consumeGen(valGen) { return await (typeof valGen === "function" ? valGen() : valGen); } async function consumeStringGen(strGen) { return typeof strGen === "string" ? strGen : String( typeof strGen === "function" ? await strGen() : strGen ); } async function fetchAdvanced(input, options = {}) { const { timeout = 1e4 } = options; const ctl = new AbortController(); const { signal, ...restOpts } = options; signal == null ? void 0 : signal.addEventListener("abort", () => ctl.abort()); let sigOpts = {}, id = void 0; if (timeout >= 0) { id = setTimeout(() => ctl.abort(), timeout); sigOpts = { signal: ctl.signal }; } try { const res = await fetch(input, { ...restOpts, ...sigOpts }); typeof id !== "undefined" && clearTimeout(id); return res; } catch (err) { typeof id !== "undefined" && clearTimeout(id); throw new Error("Error while calling fetch", { cause: err }); } } function getListLength(listLike, zeroOnInvalid = true) { return "length" in listLike ? listLike.length : "size" in listLike ? listLike.size : "count" in listLike ? listLike.count : zeroOnInvalid ? 0 : NaN; } function pauseFor(time, signal, rejectOnAbort = false) { return new Promise((res, rej) => { const timeout = setTimeout(() => res(), time); signal == null ? void 0 : signal.addEventListener("abort", () => { clearTimeout(timeout); rejectOnAbort ? rej(new Error("The pause was aborted")) : res(); }); }); } function pureObj(obj) { return Object.assign(/* @__PURE__ */ Object.create(null), obj ?? {}); } function setImmediateInterval(callback, interval, signal) { let intervalId; const cleanup = () => clearInterval(intervalId); const loop = () => { if (signal == null ? void 0 : signal.aborted) return cleanup(); callback(); }; signal == null ? void 0 : signal.addEventListener("abort", cleanup); loop(); intervalId = setInterval(loop, interval); } function setImmediateTimeoutLoop(callback, interval, signal) { let timeout; const cleanup = () => clearTimeout(timeout); const loop = async () => { if (signal == null ? void 0 : signal.aborted) return cleanup(); await callback(); timeout = setTimeout(loop, interval); }; signal == null ? void 0 : signal.addEventListener("abort", cleanup); loop(); } function scheduleExit(code = 0, timeout = 0) { if (timeout < 0) throw new TypeError("Timeout must be a non-negative number"); let exit; if (typeof process !== "undefined" && "exit" in process) exit = () => process.exit(code); else if (typeof Deno !== "undefined" && "exit" in Deno) exit = () => Deno.exit(code); else throw new Error("Cannot exit the process, no exit method available"); setTimeout(exit, timeout); } // lib/text.ts function autoPlural(term, num, pluralType = "auto") { if (typeof num !== "number") { if ("length" in num) num = num.length; else if ("size" in num) num = num.size; else if ("count" in num) num = num.count; } if (!["-s", "-ies"].includes(pluralType)) pluralType = "auto"; if (isNaN(num)) num = 2; const pType = pluralType === "auto" ? String(term).endsWith("y") ? "-ies" : "-s" : pluralType; switch (pType) { case "-s": return `${term}${num === 1 ? "" : "s"}`; case "-ies": return `${String(term).slice(0, -1)}${num === 1 ? "y" : "ies"}`; } } function capitalize(text) { return text.charAt(0).toUpperCase() + text.slice(1); } var defaultPbChars = { 100: "\u2588", 75: "\u2593", 50: "\u2592", 25: "\u2591", 0: "\u2500" }; function createProgressBar(percentage, barLength, chars = defaultPbChars) { if (percentage === 100) return chars[100].repeat(barLength); const filledLength = Math.floor(percentage / 100 * barLength); const remainingPercentage = percentage / 10 * barLength - filledLength; let lastBlock = ""; if (remainingPercentage >= 0.75) lastBlock = chars[75]; else if (remainingPercentage >= 0.5) lastBlock = chars[50]; else if (remainingPercentage >= 0.25) lastBlock = chars[25]; const filledBar = chars[100].repeat(filledLength); const emptyBar = chars[0].repeat(barLength - filledLength - (lastBlock ? 1 : 0)); return `${filledBar}${lastBlock}${emptyBar}`; } function insertValues(input, ...values) { return input.replace(/%\d/gm, (match) => { var _a; const argIndex = Number(match.substring(1)) - 1; return (_a = values[argIndex] ?? match) == null ? void 0 : _a.toString(); }); } function joinArrayReadable(array, separators = ", ", lastSeparator = " and ") { const arr = [...array]; if (arr.length === 0) return ""; else if (arr.length === 1) return String(arr[0]); else if (arr.length === 2) return arr.join(lastSeparator); const lastItm = lastSeparator + arr[arr.length - 1]; arr.pop(); return arr.join(separators) + lastItm; } function secsToTimeStr(seconds) { if (seconds < 0) throw new TypeError("Seconds must be a positive number"); const hours = Math.floor(seconds / 3600); const minutes = Math.floor(seconds % 3600 / 60); const secs = Math.floor(seconds % 60); return [ hours ? hours + ":" : "", String(minutes).padStart(minutes > 0 || hours > 0 ? 2 : 1, "0"), ":", String(secs).padStart(secs > 0 || minutes > 0 || hours > 0 ? 2 : 1, "0") ].join(""); } function truncStr(input, length, endStr = "...") { const str = (input == null ? void 0 : input.toString()) ?? String(input); const finalStr = str.length > length ? str.substring(0, length - endStr.length) + endStr : str; return finalStr.length > length ? finalStr.substring(0, length) : finalStr; } // lib/Errors.ts var DatedError = class extends Error { date; constructor(message, options) { super(message, options); this.name = this.constructor.name; this.date = /* @__PURE__ */ new Date(); } }; var ChecksumMismatchError = class extends DatedError { constructor(message, options) { super(message, options); this.name = "ChecksumMismatchError"; } }; var MigrationError = class extends DatedError { constructor(message, options) { super(message, options); this.name = "MigrationError"; } }; var ValidationError = class extends DatedError { constructor(message, options) { super(message, options); this.name = "ValidationError"; } }; // lib/DataStore.ts var dsFmtVer = 1; var DataStore = class { id; formatVersion; defaultData; encodeData; decodeData; compressionFormat = "deflate-raw"; engine; firstInit = true; cachedData; migrations; migrateIds = []; /** * Creates an instance of DataStore to manage a sync & async database that is cached in memory and persistently saved across sessions. * Supports migrating data from older versions to newer ones and populating the cache with default data if no persistent data is found. * * - ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue` if the storageMethod is left as the default of `"GM"` * - ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData` * * @template TData The type of the data that is saved in persistent storage for the currently set format version (will be automatically inferred from `defaultData` if not provided) - **This has to be a JSON-compatible object!** (no undefined, circular references, etc.) * @param opts The options for this DataStore instance */ constructor(opts) { this.id = opts.id; this.formatVersion = opts.formatVersion; this.defaultData = opts.defaultData; this.cachedData = opts.defaultData; this.migrations = opts.migrations; if (opts.migrateIds) this.migrateIds = Array.isArray(opts.migrateIds) ? opts.migrateIds : [opts.migrateIds]; this.encodeData = opts.encodeData; this.decodeData = opts.decodeData; this.engine = typeof opts.engine === "function" ? opts.engine() : opts.engine; if (typeof opts.compressionFormat === "undefined") opts.compressionFormat = "deflate-raw"; if (typeof opts.compressionFormat === "string") { this.encodeData = [opts.compressionFormat, async (data) => await compress(data, opts.compressionFormat, "string")]; this.decodeData = [opts.compressionFormat, async (data) => await compress(data, opts.compressionFormat, "string")]; } else if ("encodeData" in opts && "decodeData" in opts && Array.isArray(opts.encodeData) && Array.isArray(opts.decodeData)) { this.encodeData = [opts.encodeData[0], opts.encodeData[1]]; this.decodeData = [opts.decodeData[0], opts.decodeData[1]]; } else if (opts.compressionFormat === null) { this.encodeData = void 0; this.decodeData = void 0; } else throw new TypeError("Either `compressionFormat` or `encodeData` and `decodeData` have to be set and valid, but not all three at a time. Please refer to the documentation for more info."); this.engine.setDataStoreOptions(opts); } //#region public /** * Loads the data saved in persistent storage into the in-memory cache and also returns a copy of it. * Automatically populates persistent storage with default data if it doesn't contain any data yet. * Also runs all necessary migration functions if the data format has changed since the last time the data was saved. */ async loadData() { try { if (this.firstInit) { this.firstInit = false; const dsVer = Number(await this.engine.getValue("__ds_fmt_ver", 0)); if (isNaN(dsVer) || dsVer < 1) { const oldData = await this.engine.getValue(`_uucfg-${this.id}`, null); if (oldData) { const oldVer = Number(await this.engine.getValue(`_uucfgver-${this.id}`, NaN)); const oldEnc = await this.engine.getValue(`_uucfgenc-${this.id}`, null); const promises = []; const migrateFmt = (oldKey, newKey, value) => { promises.push(this.engine.setValue(newKey, value)); promises.push(this.engine.deleteValue(oldKey)); }; oldData && migrateFmt(`_uucfg-${this.id}`, `__ds-${this.id}-dat`, oldData); !isNaN(oldVer) && migrateFmt(`_uucfgver-${this.id}`, `__ds-${this.id}-ver`, oldVer); typeof oldEnc === "boolean" && migrateFmt(`_uucfgenc-${this.id}`, `__ds-${this.id}-enc`, oldEnc === true ? this.compressionFormat : null); await Promise.allSettled(promises); } await this.engine.setValue("__ds_fmt_ver", dsFmtVer); } } if (this.migrateIds.length > 0) { await this.migrateId(this.migrateIds); this.migrateIds = []; } const gmData = await this.engine.getValue(`__ds-${this.id}-dat`, JSON.stringify(this.defaultData)); let gmFmtVer = Number(await this.engine.getValue(`__ds-${this.id}-ver`, NaN)); if (typeof gmData !== "string") { await this.saveDefaultData(); return { ...this.defaultData }; } const isEncoded = Boolean(await this.engine.getValue(`__ds-${this.id}-enc`, false)); let saveData = false; if (isNaN(gmFmtVer)) { await this.engine.setValue(`__ds-${this.id}-ver`, gmFmtVer = this.formatVersion); saveData = true; } let parsed = await this.engine.deserializeData(gmData, isEncoded); if (gmFmtVer < this.formatVersion && this.migrations) parsed = await this.runMigrations(parsed, gmFmtVer); if (saveData) await this.setData(parsed); return this.cachedData = this.engine.deepCopy(parsed); } catch (err) { console.warn("Error while parsing JSON data, resetting it to the default value.", err); await this.saveDefaultData(); return this.defaultData; } } /** * Returns a copy of the data from the in-memory cache. * Use {@linkcode loadData()} to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage). */ getData() { return this.engine.deepCopy(this.cachedData); } /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */ setData(data) { this.cachedData = data; const useEncoding = this.encodingEnabled(); return new Promise(async (resolve) => { await Promise.allSettled([ this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(data, useEncoding)), this.engine.setValue(`__ds-${this.id}-ver`, this.formatVersion), this.engine.setValue(`__ds-${this.id}-enc`, useEncoding) ]); resolve(); }); } /** Saves the default data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */ async saveDefaultData() { this.cachedData = this.defaultData; const useEncoding = this.encodingEnabled(); await Promise.allSettled([ this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(this.defaultData, useEncoding)), this.engine.setValue(`__ds-${this.id}-ver`, this.formatVersion), this.engine.setValue(`__ds-${this.id}-enc`, useEncoding) ]); } /** * Call this method to clear all persistently stored data associated with this DataStore instance. * The in-memory cache will be left untouched, so you may still access the data with {@linkcode getData()} * Calling {@linkcode loadData()} or {@linkcode setData()} after this method was called will recreate persistent storage with the cached or default data. * * - ⚠️ This requires the additional directive `@grant GM.deleteValue` if the storageMethod is left as the default of `"GM"` */ async deleteData() { await Promise.allSettled([ this.engine.deleteValue(`__ds-${this.id}-dat`), this.engine.deleteValue(`__ds-${this.id}-ver`), this.engine.deleteValue(`__ds-${this.id}-enc`) ]); } /** Returns whether encoding and decoding are enabled for this DataStore instance */ encodingEnabled() { return Boolean(this.encodeData && this.decodeData); } //#region migrations /** * Runs all necessary migration functions consecutively and saves the result to the in-memory cache and persistent storage and also returns it. * This method is automatically called by {@linkcode loadData()} if the data format has changed since the last time the data was saved. * Though calling this method manually is not necessary, it can be useful if you want to run migrations for special occasions like a user importing potentially outdated data that has been previously exported. * * If one of the migrations fails, the data will be reset to the default value if `resetOnError` is set to `true` (default). Otherwise, an error will be thrown and no data will be saved. */ async runMigrations(oldData, oldFmtVer, resetOnError = true) { if (!this.migrations) return oldData; let newData = oldData; const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b)); let lastFmtVer = oldFmtVer; for (const [fmtVer, migrationFunc] of sortedMigrations) { const ver = Number(fmtVer); if (oldFmtVer < this.formatVersion && oldFmtVer < ver) { try { const migRes = migrationFunc(newData); newData = migRes instanceof Promise ? await migRes : migRes; lastFmtVer = oldFmtVer = ver; } catch (err) { if (!resetOnError) throw new MigrationError(`Error while running migration function for format version '${fmtVer}'`, { cause: err }); await this.saveDefaultData(); return this.getData(); } } } await Promise.allSettled([ this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(newData)), this.engine.setValue(`__ds-${this.id}-ver`, lastFmtVer), this.engine.setValue(`__ds-${this.id}-enc`, this.encodingEnabled()) ]); return this.cachedData = { ...newData }; } /** * Tries to migrate the currently saved persistent data from one or more old IDs to the ID set in the constructor. * If no data exist for the old ID(s), nothing will be done, but some time may still pass trying to fetch the non-existent data. */ async migrateId(oldIds) { const ids = Array.isArray(oldIds) ? oldIds : [oldIds]; await Promise.all(ids.map(async (id) => { const data = await this.engine.getValue(`__ds-${id}-dat`, JSON.stringify(this.defaultData)); const fmtVer = Number(await this.engine.getValue(`__ds-${id}-ver`, NaN)); const isEncoded = Boolean(await this.engine.getValue(`__ds-${id}-enc`, false)); if (data === void 0 || isNaN(fmtVer)) return; const parsed = await this.engine.deserializeData(data, isEncoded); await Promise.allSettled([ this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(parsed)), this.engine.setValue(`__ds-${this.id}-ver`, fmtVer), this.engine.setValue(`__ds-${this.id}-enc`, isEncoded), this.engine.deleteValue(`__ds-${id}-dat`), this.engine.deleteValue(`__ds-${id}-ver`), this.engine.deleteValue(`__ds-${id}-enc`) ]); })); } }; // lib/DataStoreEngine.ts var DataStoreEngine = class { dataStoreOptions; // setDataStoreOptions() is called from inside the DataStore constructor to set this value /** Called by DataStore on creation, to pass its options */ setDataStoreOptions(dataStoreOptions) { this.dataStoreOptions = dataStoreOptions; } //#region serialization api /** Serializes the given object to a string, optionally encoded with `options.encodeData` if {@linkcode useEncoding} is set to true */ async serializeData(data, useEncoding) { var _a, _b, _c, _d, _e; const stringData = JSON.stringify(data); if (!useEncoding || !((_a = this.dataStoreOptions) == null ? void 0 : _a.encodeData) || !((_b = this.dataStoreOptions) == null ? void 0 : _b.decodeData)) return stringData; const encRes = (_e = (_d = (_c = this.dataStoreOptions) == null ? void 0 : _c.encodeData) == null ? void 0 : _d[1]) == null ? void 0 : _e.call(_d, stringData); if (encRes instanceof Promise) return await encRes; return encRes; } /** Deserializes the given string to a JSON object, optionally decoded with `options.decodeData` if {@linkcode useEncoding} is set to true */ async deserializeData(data, useEncoding) { var _a, _b, _c; let decRes = ((_a = this.dataStoreOptions) == null ? void 0 : _a.decodeData) && useEncoding ? (_c = (_b = this.dataStoreOptions.decodeData) == null ? void 0 : _b[1]) == null ? void 0 : _c.call(_b, data) : void 0; if (decRes instanceof Promise) decRes = await decRes; return JSON.parse(decRes ?? data); } //#region misc api /** * Copies a JSON-compatible object and loses all its internal references in the process. * Uses [`structuredClone()`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) if available, otherwise falls back to `JSON.parse(JSON.stringify(obj))`. */ deepCopy(obj) { try { if ("structuredClone" in globalThis) return structuredClone(obj); } catch { } return JSON.parse(JSON.stringify(obj)); } }; var BrowserStorageEngine = class extends DataStoreEngine { options; /** * Creates an instance of `BrowserStorageEngine`. * * ⚠️ Requires a DOM environment * ⚠️ Don't reuse this engine across multiple {@linkcode DataStore} instances */ constructor(options) { super(); this.options = { type: "localStorage", ...options }; } //#region storage api /** Fetches a value from persistent storage */ async getValue(name, defaultValue) { return (this.options.type === "localStorage" ? globalThis.localStorage.getItem(name) : globalThis.sessionStorage.getItem(name)) ?? defaultValue; } /** Sets a value in persistent storage */ async setValue(name, value) { if (this.options.type === "localStorage") globalThis.localStorage.setItem(name, String(value)); else globalThis.sessionStorage.setItem(name, String(value)); } /** Deletes a value from persistent storage */ async deleteValue(name) { if (this.options.type === "localStorage") globalThis.localStorage.removeItem(name); else globalThis.sessionStorage.removeItem(name); } }; var fs; var FileStorageEngine = class extends DataStoreEngine { options; /** * Creates an instance of `FileStorageEngine`. * * ⚠️ Requires Node.js or Deno with Node compatibility (v1.31+) * ⚠️ Don't reuse this engine across multiple {@linkcode DataStore} instances */ constructor(options) { super(); this.options = { filePath: (id) => `.ds-${id}`, ...options }; } //#region json file /** Reads the file contents */ async readFile() { var _a, _b, _c; try { if (!fs) fs = (await import("fs/promises")).default; if (!fs) throw new DatedError("FileStorageEngine requires Node.js or Deno with Node compatibility (v1.31+)", { cause: new Error("'node:fs/promises' module not available") }); const path = typeof this.options.filePath === "string" ? this.options.filePath : this.options.filePath(this.dataStoreOptions.id); const data = await fs.readFile(path, "utf-8"); return data ? JSON.parse(await ((_c = (_b = (_a = this.dataStoreOptions) == null ? void 0 : _a.decodeData) == null ? void 0 : _b[1]) == null ? void 0 : _c.call(_b, data)) ?? data) : void 0; } catch { return void 0; } } /** Overwrites the file contents */ async writeFile(data) { var _a, _b, _c; try { if (!fs) fs = (await import("fs/promises")).default; if (!fs) throw new DatedError("FileStorageEngine requires Node.js or Deno with Node compatibility (v1.31+)", { cause: new Error("'node:fs/promises' module not available") }); const path = typeof this.options.filePath === "string" ? this.options.filePath : this.options.filePath(this.dataStoreOptions.id); await fs.mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true }); await fs.writeFile(path, await ((_c = (_b = (_a = this.dataStoreOptions) == null ? void 0 : _a.encodeData) == null ? void 0 : _b[1]) == null ? void 0 : _c.call(_b, JSON.stringify(data))) ?? JSON.stringify(data, void 0, 2), "utf-8"); } catch (err) { console.error("Error writing file:", err); } } //#region storage api /** Fetches a value from persistent storage */ async getValue(name, defaultValue) { const data = await this.readFile(); if (!data) return defaultValue; const value = data == null ? void 0 : data[name]; if (value === void 0) return defaultValue; if (typeof value === "string") return value; return String(value ?? defaultValue); } /** Sets a value in persistent storage */ async setValue(name, value) { let data = await this.readFile(); if (!data) data = {}; data[name] = value; await this.writeFile(data); } /** Deletes a value from persistent storage */ async deleteValue(name) { const data = await this.readFile(); if (!data) return; delete data[name]; await this.writeFile(data); } }; // lib/DataStoreSerializer.ts var DataStoreSerializer = class _DataStoreSerializer { stores; options; constructor(stores, options = {}) { if (!crypto || !crypto.subtle) throw new Error("DataStoreSerializer has to run in a secure context (HTTPS) or in another environment that implements the subtleCrypto API!"); this.stores = stores; this.options = { addChecksum: true, ensureIntegrity: true, ...options }; } /** Calculates the checksum of a string */ async calcChecksum(input) { return computeHash(input, "SHA-256"); } /** * Serializes only a subset of the data stores into a string. * @param stores An array of store IDs or functions that take a store ID and return a boolean * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects */ async serializePartial(stores, useEncoding = true, stringified = true) { const serData = []; for (const storeInst of this.stores.filter((s) => typeof stores === "function" ? stores(s.id) : stores.includes(s.id))) { const data = useEncoding && storeInst.encodingEnabled() ? await storeInst.encodeData[1](JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData()); serData.push({ id: storeInst.id, data, formatVersion: storeInst.formatVersion, encoded: useEncoding && storeInst.encodingEnabled(), checksum: this.options.addChecksum ? await this.calcChecksum(data) : void 0 }); } return stringified ? JSON.stringify(serData) : serData; } /** * Serializes the data stores into a string. * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects */ async serialize(useEncoding = true, stringified = true) { return this.serializePartial(this.stores.map((s) => s.id), useEncoding, stringified); } /** * Deserializes the data exported via {@linkcode serialize()} and imports only a subset into the DataStore instances. * Also triggers the migration process if the data format has changed. */ async deserializePartial(stores, data) { const deserStores = typeof data === "string" ? JSON.parse(data) : data; if (!Array.isArray(deserStores) || !deserStores.every(_DataStoreSerializer.isSerializedDataStoreObj)) throw new TypeError("Invalid serialized data format! Expected an array of SerializedDataStore objects."); for (const storeData of deserStores.filter((s) => typeof stores === "function" ? stores(s.id) : stores.includes(s.id))) { const storeInst = this.stores.find((s) => s.id === storeData.id); if (!storeInst) throw new Error(`DataStore instance with ID "${storeData.id}" not found! Make sure to provide it in the DataStoreSerializer constructor.`); if (this.options.ensureIntegrity && typeof storeData.checksum === "string") { const checksum = await this.calcChecksum(storeData.data); if (checksum !== storeData.checksum) throw new ChecksumMismatchError(`Checksum mismatch for DataStore with ID "${storeData.id}"! Expected: ${storeData.checksum} Has: ${checksum}`); } const decodedData = storeData.encoded && storeInst.encodingEnabled() ? await storeInst.decodeData[1](storeData.data) : storeData.data; if (storeData.formatVersion && !isNaN(Number(storeData.formatVersion)) && Number(storeData.formatVersion) < storeInst.formatVersion) await storeInst.runMigrations(JSON.parse(decodedData), Number(storeData.formatVersion), false); else await storeInst.setData(JSON.parse(decodedData)); } } /** * Deserializes the data exported via {@linkcode serialize()} and imports the data into all matching DataStore instances. * Also triggers the migration process if the data format has changed. */ async deserialize(data) { return this.deserializePartial(this.stores.map((s) => s.id), data); } /** * Loads the persistent data of the DataStore instances into the in-memory cache. * Also triggers the migration process if the data format has changed. * @param stores An array of store IDs or a function that takes the store IDs and returns a boolean - if omitted, all stores will be loaded * @returns Returns a PromiseSettledResult array with the results of each DataStore instance in the format `{ id: string, data: object }` */ async loadStoresData(stores) { return Promise.allSettled( this.getStoresFiltered(stores).map(async (store) => ({ id: store.id, data: await store.loadData() })) ); } /** * Resets the persistent and in-memory data of the DataStore instances to their default values. * @param stores An array of store IDs or a function that takes the store IDs and returns a boolean - if omitted, all stores will be affected */ async resetStoresData(stores) { return Promise.allSettled( this.getStoresFiltered(stores).map((store) => store.saveDefaultData()) ); } /** * Deletes the persistent data of the DataStore instances. * Leaves the in-memory data untouched. * @param stores An array of store IDs or a function that takes the store IDs and returns a boolean - if omitted, all stores will be affected */ async deleteStoresData(stores) { return Promise.allSettled( this.getStoresFiltered(stores).map((store) => store.deleteData()) ); } /** Checks if a given value is an array of SerializedDataStore objects */ static isSerializedDataStoreObjArray(obj) { return Array.isArray(obj) && obj.every((o) => typeof o === "object" && o !== null && "id" in o && "data" in o && "formatVersion" in o && "encoded" in o); } /** Checks if a given value is a SerializedDataStore object */ static isSerializedDataStoreObj(obj) { return typeof obj === "object" && obj !== null && "id" in obj && "data" in obj && "formatVersion" in obj && "encoded" in obj; } /** Returns the DataStore instances whose IDs match the provided array or function */ getStoresFiltered(stores) { return this.stores.filter((s) => typeof stores === "undefined" ? true : Array.isArray(stores) ? stores.includes(s.id) : stores(s.id)); } }; // node_modules/.pnpm/nanoevents@9.1.0/node_modules/nanoevents/index.js var createNanoEvents = () => ({ emit(event, ...args) { for (let callbacks = this.events[event] || [], i = 0, length = callbacks.length; i < length; i++) { callbacks[i](...args); } }, events: {}, on(event, cb) { ; (this.events[event] ||= []).push(cb); return () => { var _a; this.events[event] = (_a = this.events[event]) == null ? void 0 : _a.filter((i) => cb !== i); }; } }); // lib/NanoEmitter.ts var NanoEmitter = class { events = createNanoEvents(); eventUnsubscribes = []; emitterOptions; /** Creates a new instance of NanoEmitter - a lightweight event emitter with helper methods and a strongly typed event map */ constructor(options = {}) { this.emitterOptions = { publicEmit: false, ...options }; } /** * Subscribes to an event and calls the callback when it's emitted. * @param event The event to subscribe to. Use `as "_"` in case your event names aren't thoroughly typed (like when using a template literal, e.g. \`event-${val}\` as "_") * @returns Returns a function that can be called to unsubscribe the event listener * @example ```ts * const emitter = new NanoEmitter<{ * foo: (bar: string) => void; * }>({ * publicEmit: true, * }); * * let i = 0; * const unsub = emitter.on("foo", (bar) => { * // unsubscribe after 10 events: * if(++i === 10) unsub(); * console.log(bar); * }); * * emitter.emit("foo", "bar"); * ``` */ on(event, cb) { let unsub; const unsubProxy = () => { if (!unsub) return; unsub(); this.eventUnsubscribes = this.eventUnsubscribes.filter((u) => u !== unsub); }; unsub = this.events.on(event, cb); this.eventUnsubscribes.push(unsub); return unsubProxy; } /** * Subscribes to an event and calls the callback or resolves the Promise only once when it's emitted. * @param event The event to subscribe to. Use `as "_"` in case your event names aren't thoroughly typed (like when using a template literal, e.g. \`event-${val}\` as "_") * @param cb The callback to call when the event is emitted - if provided or not, the returned Promise will resolve with the event arguments * @returns Returns a Promise that resolves with the event arguments when the event is emitted * @example ```ts * const emitter = new NanoEmitter<{ * foo: (bar: string) => void; * }>(); * * // Promise syntax: * const [bar] = await emitter.once("foo"); * console.log(bar); * * // Callback syntax: * emitter.once("foo", (bar) => console.log(bar)); * ``` */ once(event, cb) { return new Promise((resolve) => { let unsub; const onceProxy = (...args) => { cb == null ? void 0 : cb(...args); unsub == null ? void 0 : unsub(); resolve(args); }; unsub = this.events.on(event, onceProxy); this.eventUnsubscribes.push(unsub); }); } /** * Emits an event on this instance. * ⚠️ Needs `publicEmit` to be set to true in the NanoEmitter constructor or super() call! * @param event The event to emit * @param args The arguments to pass to the event listeners * @returns Returns true if `publicEmit` is true and the event was emitted successfully */ emit(event, ...args) { if (this.emitterOptions.publicEmit) { this.events.emit(event, ...args); return true; } return false; } /** Unsubscribes all event listeners from this instance */ unsubscribeAll() { for (const unsub of this.eventUnsubscribes) unsub(); this.eventUnsubscribes = []; } }; // lib/Debouncer.ts var Debouncer = class extends NanoEmitter { /** * Creates a new debouncer with the specified timeout and edge type. * @param timeout Timeout in milliseconds between letting through calls - defaults to 200 * @param type The edge type to use for the debouncer - see {@linkcode DebouncerType} for details or [the documentation for an explanation and diagram](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#debouncer) - defaults to "immediate" */ constructor(timeout = 200, type = "immediate") { super(); this.timeout = timeout; this.type = type; } /** All registered listener functions and the time they were attached */ listeners = []; /** The currently active timeout */ activeTimeout; /** The latest queued call */ queuedCall; //#region listeners /** Adds a listener function that will be called on timeout */ addListener(fn) { this.listeners.push(fn); } /** Removes the listener with the specified function reference */ removeListener(fn) { const idx = this.listeners.findIndex((l) => l === fn); idx !== -1 && this.listeners.splice(idx, 1); } /** Removes all listeners */ removeAllListeners() { this.listeners = []; } /** Returns all registered listeners */ getListeners() { return this.listeners; } //#region timeout /** Sets the timeout for the debouncer */ setTimeout(timeout) { this.emit("change", this.timeout = timeout, this.type); } /** Returns the current timeout */ getTimeout() { return this.timeout; } /** Whether the timeout is currently active, meaning any latest call to the {@linkcode call()} method will be queued */ isTimeoutActive() { return typeof this.activeTimeout !== "undefined"; } //#region type /** Sets the edge type for the debouncer */ setType(type) { this.emit("change", this.timeout, this.type = type); } /** Returns the current edge type */ getType() { return this.type; } //#region call /** Use this to call the debouncer with the specified arguments that will be passed to all listener functions registered with {@linkcode addListener()} */ call(...args) { const cl = (...a) => { this.queuedCall = void 0; this.emit("call", ...a); this.listeners.forEach((l) => l.call(this, ...a)); }; const setRepeatTimeout = () => { this.activeTimeout = setTimeout(() => { if (this.queuedCall) { this.queuedCall(); setRepeatTimeout(); } else this.activeTimeout = void 0; }, this.timeout); }; switch (this.type) { case "immediate": if (typeof this.activeTimeout === "undefined") { cl(...args); setRepeatTimeout(); } else this.queuedCall = () => cl(...args); break; case "idle": if (this.activeTimeout) clearTimeout(this.activeTimeout); this.activeTimeout = setTimeout(() => { cl(...args); this.activeTimeout = void 0; }, this.timeout); break; default: throw new TypeError(`Invalid debouncer type: ${this.type}`); } } }; function debounce(fn, timeout = 200, type = "immediate") { const debouncer = new Debouncer(timeout, type); debouncer.addListener(fn); const func = (...args) => debouncer.call(...args); func.debouncer = debouncer; return func; } export { BrowserStorageEngine, ChecksumMismatchError, DataStore, DataStoreEngine, DataStoreSerializer, DatedError, Debouncer, FileStorageEngine, MigrationError, NanoEmitter, ValidationError, abtoa, atoab, autoPlural, bitSetHas, capitalize, clamp, compress, computeHash, consumeGen, consumeStringGen, createProgressBar, darkenColor, debounce, decompress, defaultPbChars, digitCount, fetchAdvanced, formatNumber, getListLength, hexToRgb, insertValues, joinArrayReadable, lightenColor, mapRange, pauseFor, pureObj, randRange, randomId, randomItem, randomItemIndex, randomizeArray, rgbToHex, roundFixed, scheduleExit, secsToTimeStr, setImmediateInterval, setImmediateTimeoutLoop, takeRandomItem, takeRandomItemIndex, truncStr, valsWithin }; //# sourceMappingURL=CoreUtils.mjs.map