@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
JavaScript
// 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