@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,216 lines (1,198 loc) • 56.5 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 };
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
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);
// 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,
overflowVal: () => overflowVal,
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 overflowVal(value, minOrMax, max) {
const min = typeof max === "number" ? minOrMax : 0;
max = typeof max === "number" ? max : minOrMax;
if (min > max)
throw new RangeError(`Parameter "min" can't be bigger than "max"`);
if (isNaN(value) || isNaN(min) || isNaN(max) || !isFinite(value) || !isFinite(min) || !isFinite(max))
return NaN;
if (value >= min && value <= max)
return value;
const range = max - min + 1;
const wrappedValue = ((value - min) % range + range) % range + min;
return wrappedValue;
}
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
return `rgb(${r}, ${g}, ${b})`;
}
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 Uint8Array ? 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 uintArr = new Uint8Array(await new Response(comp.readable).arrayBuffer());
return outputType === "arrayBuffer" ? uintArr : abtoa(uintArr);
}
async function decompress(input, compressionFormat, outputType = "string") {
const byteArray = input instanceof Uint8Array ? 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 uintArr = new Uint8Array(await new Response(decomp.readable).arrayBuffer());
return outputType === "arrayBuffer" ? uintArr : new TextDecoder().decode(uintArr);
}
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;
options;
/**
* Whether all first-init checks should be done.
* This includes migrating the internal DataStore format, migrating data from the UserUtils format, and anything similar.
* This is set to `true` by default. Create a subclass and set it to `false` before calling {@linkcode loadData()} if you want to explicitly skip these checks.
*/
firstInit = true;
/** In-memory cached copy of the data that is saved in persistent storage used for synchronous read access. */
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.
*
* - ⚠️ 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) {
var _a;
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;
this.options = opts;
if (typeof opts.compressionFormat === "undefined")
opts.compressionFormat = ((_a = opts.encodeData) == null ? void 0 : _a[0]) ?? "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));
};
if (oldData)
migrateFmt(`_uucfg-${this.id}`, `__ds-${this.id}-dat`, oldData);
if (!isNaN(oldVer))
migrateFmt(`_uucfgver-${this.id}`, `__ds-${this.id}-ver`, oldVer);
if (typeof oldEnc === "boolean")
migrateFmt(`_uucfgenc-${this.id}`, `__ds-${this.id}-enf`, oldEnc === true ? Boolean(this.compressionFormat) || null : null);
else
promises.push(this.engine.setValue(`__ds-${this.id}-enf`, this.compressionFormat));
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 storedData = await this.engine.getValue(`__ds-${this.id}-dat`, JSON.stringify(this.defaultData));
let storedFmtVer = Number(await this.engine.getValue(`__ds-${this.id}-ver`, NaN));
if (typeof storedData !== "string") {
await this.saveDefaultData();
return { ...this.defaultData };
}
const encodingFmt = String(await this.engine.getValue(`__ds-${this.id}-enf`, null));
const isEncoded = encodingFmt !== "null" && encodingFmt !== "false";
let saveData = false;
if (isNaN(storedFmtVer)) {
await this.engine.setValue(`__ds-${this.id}-ver`, storedFmtVer = this.formatVersion);
saveData = true;
}
let parsed = await this.engine.deserializeData(storedData, isEncoded);
if (storedFmtVer < this.formatVersion && this.migrations)
parsed = await this.runMigrations(parsed, storedFmtVer);
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;
return new Promise(async (resolve) => {
await Promise.allSettled([
this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(data, this.encodingEnabled())),
this.engine.setValue(`__ds-${this.id}-ver`, this.formatVersion),
this.engine.setValue(`__ds-${this.id}-enf`, this.compressionFormat)
]);
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;
await Promise.allSettled([
this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(this.defaultData, this.encodingEnabled())),
this.engine.setValue(`__ds-${this.id}-ver`, this.formatVersion),
this.engine.setValue(`__ds-${this.id}-enf`, this.compressionFormat)
]);
}
/**
* Call this method to clear all persistently stored data associated with this DataStore instance, including the storage container (if supported by the DataStoreEngine).
* 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.
*/
async deleteData() {
var _a, _b;
await Promise.allSettled([
this.engine.deleteValue(`__ds-${this.id}-dat`),
this.engine.deleteValue(`__ds-${this.id}-ver`),
this.engine.deleteValue(`__ds-${this.id}-enf`)
]);
await ((_b = (_a = this.engine).deleteStorage) == null ? void 0 : _b.call(_a));
}
/** Returns whether encoding and decoding are enabled for this DataStore instance */
encodingEnabled() {
return Boolean(this.encodeData && this.decodeData) && this.compressionFormat !== null || Boolean(this.compressionFormat);
}
//#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}-enf`, this.compressionFormat)
]);
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, fmtVer, isEncoded] = await (async () => {
const [d, f, e] = await Promise.all([
this.engine.getValue(`__ds-${id}-dat`, JSON.stringify(this.defaultData)),
this.engine.getValue(`__ds-${id}-ver`, NaN),
this.engine.getValue(`__ds-${id}-enf`, null)
]);
return [d, Number(f), Boolean(e) && String(e) !== "null"];
})();
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}-enf`, this.compressionFormat),
this.engine.deleteValue(`__ds-${id}-dat`),
this.engine.deleteValue(`__ds-${id}-ver`),
this.engine.deleteValue(`__ds-${id}-enf`)
]);
}));
}
};
// lib/DataStoreEngine.ts
var DataStoreEngine = class {
dataStoreOptions;
// setDataStoreOptions() is called from inside the DataStore constructor to set this value
constructor(options) {
if (options)
this.dataStoreOptions = options;
}
/** Called by DataStore on creation, to pass its options. Only call this if you are using this instance standalone! */
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;
this.ensureDataStoreOptions();
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;
this.ensureDataStoreOptions();
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
/** Throws an error if the DataStoreOptions are not set or invalid */
ensureDataStoreOptions() {
if (!this.dataStoreOptions)
throw new DatedError("DataStoreEngine must be initialized with DataStore options before use. If you are using this instance standalone, set them in the constructor or call `setDataStoreOptions()` with the DataStore options.");
if (!this.dataStoreOptions.id)
throw new DatedError("DataStoreEngine must be initialized with a valid DataStore ID");
}
/**
* 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 engines across multiple {@linkcode DataStore} instances
*/
constructor(options) {
super(options == null ? void 0 : options.dataStoreOptions);
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 engines across multiple {@linkcode DataStore} instances
*/
constructor(options) {
super(options == null ? void 0 : options.dataStoreOptions);
this.options = {
filePath: (id) => `.ds-${id}`,
...options
};
}
//#region json file
/** Reads the file contents */
async readFile() {
var _a, _b, _c, _d;
this.ensureDataStoreOptions();
try {
if (!fs)
fs = (_a = await import("fs/promises")) == null ? void 0 : _a.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 ((_d = (_c = (_b = this.dataStoreOptions) == null ? void 0 : _b.decodeData) == null ? void 0 : _c[1]) == null ? void 0 : _d.call(_c, data)) ?? data) : void 0;
} catch {
return void 0;
}
}
/** Overwrites the file contents */
async writeFile(data) {
var _a, _b, _c, _d;
this.ensureDataStoreOptions();
try {
if (!fs)
fs = (_a = await import("fs/promises")) == null ? void 0 : _a.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 ((_d = (_c = (_b = this.dataStoreOptions) == null ? void 0 : _b.encodeData) == null ? void 0 : _c[1]) == null ? void 0 : _d.call(_c, 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);
}
/** Deletes the file that contains the data of this DataStore. */
async deleteStorage() {
var _a;
this.ensureDataStoreOptions();
try {
if (!fs)
fs = (_a = await import("fs/promises")) == null ? void 0 : _a.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.unlink(path);
} catch (err) {
console.error("Error deleting file:", err);
}
}
};
// 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) {
var _a;
const serData = [];
for (const storeInst of this.stores.filter((s) => typeof stores === "function" ? stores(s.id) : stores.includes(s.id))) {
const encoded = Boolean(useEncoding && storeInst.encodingEnabled() && ((_a = storeInst.encodeData) == null ? void 0 : _a[1]));
const data = encoded ? await storeInst.encodeData[1](JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
serData.push({
id: storeInst.id,
data,
formatVersion: storeInst.formatVersion,
encoded,
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
};
}
//#region on
/**
* 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;
}
//#region once
/**
* 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);
});
}
//#region onMulti
/**
* Allows subscribing to multiple events and calling the callback only when one of, all of, or a subset of the events are emitted, either continuously or only once.
* @param options An object or array of objects with the following properties:
* `callback` (required) is the function that will be called when the conditions are met.
*
* Set `once` to true to call the callback only once for the first event (or set of events) that match the criteria, then stop listening.
* If `signal` is provided, the subscription will be aborted when the given signal is aborted.
*
* If `oneOf` is used, the callback will be called when any of the matching events are emitted.
* If `allOf` is used, the callback will be called after all of the matching events are emitted at least once, then any time any of them are emitted.
* You may use a combination of the above two options, but at least one of them must be provided.
*
* @returns Returns a function that can be called to unsubscribe all listeners created by this call. Alternatively, pass an `AbortSignal` to all options objects to achieve the same effect or for finer control.
*/
onMulti(options) {
const allUnsubs = [];
const unsubAll = () => {
for (const unsub of allUnsubs)
unsub();
allUnsubs.splice(0, allUnsubs.length);
this.eventUnsubscribes = this.eventUnsubscribes.filter((u) => !allUnsubs.includes(u));
};
for (const opts of Array.isArray(options) ? options : [options]) {
const optsWithDefaults = {
allOf: [],
oneOf: [],
once: false,
...opts
};
const {
oneOf,