@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,226 lines (1,209 loc) • 55.1 kB
JavaScript
/* umd */
(function (g, f) {
if ("object" == typeof exports && "object" == typeof module) {
module.exports = f();
} else if ("function" == typeof define && define.amd) {
define("CoreUtils", [], f);
} else if ("object" == typeof exports) {
exports["CoreUtils"] = f();
} else {
g["CoreUtils"] = f();
}
}(this, () => {
var exports = {};
var module = { exports };
(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("CoreUtils",f)}else {g["CoreUtils"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports};
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __pow = Math.pow;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __objRest = (source, exclude) => {
var target = {};
for (var prop in source)
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
target[prop] = source[prop];
if (source != null && __getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(source)) {
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
target[prop] = source[prop];
}
return target;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// lib/index.ts
var lib_exports = {};
__export(lib_exports, {
BrowserStorageEngine: () => BrowserStorageEngine,
ChecksumMismatchError: () => ChecksumMismatchError,
DataStore: () => DataStore,
DataStoreEngine: () => DataStoreEngine,
DataStoreSerializer: () => DataStoreSerializer,
DatedError: () => DatedError,
Debouncer: () => Debouncer,
FileStorageEngine: () => FileStorageEngine,
MigrationError: () => MigrationError,
NanoEmitter: () => NanoEmitter,
ValidationError: () => ValidationError,
abtoa: () => abtoa,
atoab: () => atoab,
autoPlural: () => autoPlural,
bitSetHas: () => bitSetHas,
capitalize: () => capitalize,
clamp: () => clamp,
compress: () => compress,
computeHash: () => computeHash,
consumeGen: () => consumeGen,
consumeStringGen: () => consumeStringGen,
createProgressBar: () => createProgressBar,
darkenColor: () => darkenColor,
debounce: () => debounce,
decompress: () => decompress,
defaultPbChars: () => defaultPbChars,
digitCount: () => digitCount,
fetchAdvanced: () => fetchAdvanced,
formatNumber: () => formatNumber,
getListLength: () => getListLength,
hexToRgb: () => hexToRgb,
insertValues: () => insertValues,
joinArrayReadable: () => joinArrayReadable,
lightenColor: () => lightenColor,
mapRange: () => mapRange,
pauseFor: () => pauseFor,
pureObj: () => pureObj,
randRange: () => randRange,
randomId: () => randomId,
randomItem: () => randomItem,
randomItemIndex: () => randomItemIndex,
randomizeArray: () => randomizeArray,
rgbToHex: () => rgbToHex,
roundFixed: () => roundFixed,
scheduleExit: () => scheduleExit,
secsToTimeStr: () => secsToTimeStr,
setImmediateInterval: () => setImmediateInterval,
setImmediateTimeoutLoop: () => setImmediateTimeoutLoop,
takeRandomItem: () => takeRandomItem,
takeRandomItemIndex: () => takeRandomItemIndex,
truncStr: () => truncStr,
valsWithin: () => valsWithin
});
module.exports = __toCommonJS(lib_exports);
// 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 = __pow(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 != null ? 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));
}
function compress(input, compressionFormat, outputType = "string") {
return __async(this, null, function* () {
var _a;
const byteArray = input instanceof ArrayBuffer ? input : new TextEncoder().encode((_a = input == null ? void 0 : input.toString()) != null ? _a : String(input));
const comp = new CompressionStream(compressionFormat);
const writer = comp.writable.getWriter();
writer.write(byteArray);
writer.close();
const buf = yield new Response(comp.readable).arrayBuffer();
return outputType === "arrayBuffer" ? buf : abtoa(buf);
});
}
function decompress(input, compressionFormat, outputType = "string") {
return __async(this, null, function* () {
var _a;
const byteArray = input instanceof ArrayBuffer ? input : atoab((_a = input == null ? void 0 : input.toString()) != null ? _a : String(input));
const decomp = new DecompressionStream(compressionFormat);
const writer = decomp.writable.getWriter();
writer.write(byteArray);
writer.close();
const buf = yield new Response(decomp.readable).arrayBuffer();
return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
});
}
function computeHash(input, algorithm = "SHA-256") {
return __async(this, null, function* () {
let data;
if (typeof input === "string") {
const encoder = new TextEncoder();
data = encoder.encode(input);
} else
data = input;
const hashBuffer = yield 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
function consumeGen(valGen) {
return __async(this, null, function* () {
return yield typeof valGen === "function" ? valGen() : valGen;
});
}
function consumeStringGen(strGen) {
return __async(this, null, function* () {
return typeof strGen === "string" ? strGen : String(
typeof strGen === "function" ? yield strGen() : strGen
);
});
}
function fetchAdvanced(_0) {
return __async(this, arguments, function* (input, options = {}) {
const { timeout = 1e4 } = options;
const ctl = new AbortController();
const _a = options, { signal } = _a, restOpts = __objRest(_a, ["signal"]);
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 = yield fetch(input, __spreadValues(__spreadValues({}, 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 != 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(null, null, function* () {
if (signal == null ? void 0 : signal.aborted)
return cleanup();
yield 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, _b;
const argIndex = Number(match.substring(1)) - 1;
return (_b = (_a = values[argIndex]) != null ? _a : match) == null ? void 0 : _b.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 = "...") {
var _a;
const str = (_a = input == null ? void 0 : input.toString()) != null ? _a : 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 {
constructor(message, options) {
super(message, options);
__publicField(this, "date");
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 {
/**
* 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) {
__publicField(this, "id");
__publicField(this, "formatVersion");
__publicField(this, "defaultData");
__publicField(this, "encodeData");
__publicField(this, "decodeData");
__publicField(this, "compressionFormat", "deflate-raw");
__publicField(this, "engine");
__publicField(this, "firstInit", true);
__publicField(this, "cachedData");
__publicField(this, "migrations");
__publicField(this, "migrateIds", []);
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, (data) => __async(this, null, function* () {
return yield compress(data, opts.compressionFormat, "string");
})];
this.decodeData = [opts.compressionFormat, (data) => __async(this, null, function* () {
return yield 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.
*/
loadData() {
return __async(this, null, function* () {
try {
if (this.firstInit) {
this.firstInit = false;
const dsVer = Number(yield this.engine.getValue("__ds_fmt_ver", 0));
if (isNaN(dsVer) || dsVer < 1) {
const oldData = yield this.engine.getValue(`_uucfg-${this.id}`, null);
if (oldData) {
const oldVer = Number(yield this.engine.getValue(`_uucfgver-${this.id}`, NaN));
const oldEnc = yield 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);
yield Promise.allSettled(promises);
}
yield this.engine.setValue("__ds_fmt_ver", dsFmtVer);
}
}
if (this.migrateIds.length > 0) {
yield this.migrateId(this.migrateIds);
this.migrateIds = [];
}
const gmData = yield this.engine.getValue(`__ds-${this.id}-dat`, JSON.stringify(this.defaultData));
let gmFmtVer = Number(yield this.engine.getValue(`__ds-${this.id}-ver`, NaN));
if (typeof gmData !== "string") {
yield this.saveDefaultData();
return __spreadValues({}, this.defaultData);
}
const isEncoded = Boolean(yield this.engine.getValue(`__ds-${this.id}-enc`, false));
let saveData = false;
if (isNaN(gmFmtVer)) {
yield this.engine.setValue(`__ds-${this.id}-ver`, gmFmtVer = this.formatVersion);
saveData = true;
}
let parsed = yield this.engine.deserializeData(gmData, isEncoded);
if (gmFmtVer < this.formatVersion && this.migrations)
parsed = yield this.runMigrations(parsed, gmFmtVer);
if (saveData)
yield 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);
yield 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((resolve) => __async(this, null, function* () {
yield Promise.allSettled([
this.engine.setValue(`__ds-${this.id}-dat`, yield 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 */
saveDefaultData() {
return __async(this, null, function* () {
this.cachedData = this.defaultData;
const useEncoding = this.encodingEnabled();
yield Promise.allSettled([
this.engine.setValue(`__ds-${this.id}-dat`, yield 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"`
*/
deleteData() {
return __async(this, null, function* () {
yield 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.
*/
runMigrations(oldData, oldFmtVer, resetOnError = true) {
return __async(this, null, function* () {
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 ? yield migRes : migRes;
lastFmtVer = oldFmtVer = ver;
} catch (err) {
if (!resetOnError)
throw new MigrationError(`Error while running migration function for format version '${fmtVer}'`, { cause: err });
yield this.saveDefaultData();
return this.getData();
}
}
}
yield Promise.allSettled([
this.engine.setValue(`__ds-${this.id}-dat`, yield this.engine.serializeData(newData)),
this.engine.setValue(`__ds-${this.id}-ver`, lastFmtVer),
this.engine.setValue(`__ds-${this.id}-enc`, this.encodingEnabled())
]);
return this.cachedData = __spreadValues({}, 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.
*/
migrateId(oldIds) {
return __async(this, null, function* () {
const ids = Array.isArray(oldIds) ? oldIds : [oldIds];
yield Promise.all(ids.map((id) => __async(this, null, function* () {
const data = yield this.engine.getValue(`__ds-${id}-dat`, JSON.stringify(this.defaultData));
const fmtVer = Number(yield this.engine.getValue(`__ds-${id}-ver`, NaN));
const isEncoded = Boolean(yield this.engine.getValue(`__ds-${id}-enc`, false));
if (data === void 0 || isNaN(fmtVer))
return;
const parsed = yield this.engine.deserializeData(data, isEncoded);
yield Promise.allSettled([
this.engine.setValue(`__ds-${this.id}-dat`, yield 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 {
constructor() {
__publicField(this, "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 */
serializeData(data, useEncoding) {
return __async(this, null, function* () {
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 yield encRes;
return encRes;
});
}
/** Deserializes the given string to a JSON object, optionally decoded with `options.decodeData` if {@linkcode useEncoding} is set to true */
deserializeData(data, useEncoding) {
return __async(this, null, function* () {
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 = yield decRes;
return JSON.parse(decRes != null ? 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 (e) {
}
return JSON.parse(JSON.stringify(obj));
}
};
var BrowserStorageEngine = class extends DataStoreEngine {
/**
* Creates an instance of `BrowserStorageEngine`.
*
* ⚠️ Requires a DOM environment
* ⚠️ Don't reuse this engine across multiple {@linkcode DataStore} instances
*/
constructor(options) {
super();
__publicField(this, "options");
this.options = __spreadValues({
type: "localStorage"
}, options);
}
//#region storage api
/** Fetches a value from persistent storage */
getValue(name, defaultValue) {
return __async(this, null, function* () {
var _a;
return (_a = this.options.type === "localStorage" ? globalThis.localStorage.getItem(name) : globalThis.sessionStorage.getItem(name)) != null ? _a : defaultValue;
});
}
/** Sets a value in persistent storage */
setValue(name, value) {
return __async(this, null, function* () {
if (this.options.type === "localStorage")
globalThis.localStorage.setItem(name, String(value));
else
globalThis.sessionStorage.setItem(name, String(value));
});
}
/** Deletes a value from persistent storage */
deleteValue(name) {
return __async(this, null, function* () {
if (this.options.type === "localStorage")
globalThis.localStorage.removeItem(name);
else
globalThis.sessionStorage.removeItem(name);
});
}
};
var fs;
var FileStorageEngine = class extends DataStoreEngine {
/**
* 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();
__publicField(this, "options");
this.options = __spreadValues({
filePath: (id) => `.ds-${id}`
}, options);
}
//#region json file
/** Reads the file contents */
readFile() {
return __async(this, null, function* () {
var _a, _b, _c, _d;
try {
if (!fs)
fs = (yield 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 = yield fs.readFile(path, "utf-8");
return data ? JSON.parse((_d = yield (_c = (_b = (_a = this.dataStoreOptions) == null ? void 0 : _a.decodeData) == null ? void 0 : _b[1]) == null ? void 0 : _c.call(_b, data)) != null ? _d : data) : void 0;
} catch (e) {
return void 0;
}
});
}
/** Overwrites the file contents */
writeFile(data) {
return __async(this, null, function* () {
var _a, _b, _c, _d;
try {
if (!fs)
fs = (yield 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);
yield fs.mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true });
yield fs.writeFile(path, (_d = yield (_c = (_b = (_a = this.dataStoreOptions) == null ? void 0 : _a.encodeData) == null ? void 0 : _b[1]) == null ? void 0 : _c.call(_b, JSON.stringify(data))) != null ? _d : 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 */
getValue(name, defaultValue) {
return __async(this, null, function* () {
const data = yield 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 != null ? value : defaultValue);
});
}
/** Sets a value in persistent storage */
setValue(name, value) {
return __async(this, null, function* () {
let data = yield this.readFile();
if (!data)
data = {};
data[name] = value;
yield this.writeFile(data);
});
}
/** Deletes a value from persistent storage */
deleteValue(name) {
return __async(this, null, function* () {
const data = yield this.readFile();
if (!data)
return;
delete data[name];
yield this.writeFile(data);
});
}
};
// lib/DataStoreSerializer.ts
var DataStoreSerializer = class _DataStoreSerializer {
constructor(stores, options = {}) {
__publicField(this, "stores");
__publicField(this, "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 = __spreadValues({
addChecksum: true,
ensureIntegrity: true
}, options);
}
/** Calculates the checksum of a string */
calcChecksum(input) {
return __async(this, null, function* () {
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
*/
serializePartial(stores, useEncoding = true, stringified = true) {
return __async(this, null, function* () {
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() ? yield 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 ? yield 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
*/
serialize(useEncoding = true, stringified = true) {
return __async(this, null, function* () {
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.
*/
deserializePartial(stores, data) {
return __async(this, null, function* () {
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 = yield 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() ? yield storeInst.decodeData[1](storeData.data) : storeData.data;
if (storeData.formatVersion && !isNaN(Number(storeData.formatVersion)) && Number(storeData.formatVersion) < storeInst.formatVersion)
yield storeInst.runMigrations(JSON.parse(decodedData), Number(storeData.formatVersion), false);
else
yield 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.
*/
deserialize(data) {
return __async(this, null, function* () {
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 }`
*/
loadStoresData(stores) {
return __async(this, null, function* () {
return Promise.allSettled(
this.getStoresFiltered(stores).map((store) => __async(this, null, function* () {
return {
id: store.id,
data: yield 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
*/
resetStoresData(stores) {
return __async(this, null, function* () {
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
*/
deleteStoresData(stores) {
return __async(this, null, function* () {
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) {
var _a;
;
((_a = this.events)[event] || (_a[event] = [])).push(cb);
return () => {
var _a2;
this.events[event] = (_a2 = this.events[event]) == null ? void 0 : _a2.filter((i) => cb !== i);
};
}
});
// lib/NanoEmitter.ts
var NanoEmitter = class {
/** Creates a new instance of NanoEmitter - a lightweight event emitter with helper methods and a strongly typed event map */
constructor(options = {}) {
__publicField(this, "events", createNanoEvents());
__publicField(this, "eventUnsubscribes", []);
__publicField(this, "emitterOptions");
this.emitterOptions = __spreadValues({
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(..