@sv443-network/coreutils 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE.txt +21 -0
- package/README.md +141 -0
- package/dist/CoreUtils.cjs +1213 -0
- package/dist/CoreUtils.min.cjs +4 -0
- package/dist/CoreUtils.min.mjs +4 -0
- package/dist/CoreUtils.min.umd.js +46 -0
- package/dist/CoreUtils.mjs +1180 -0
- package/dist/CoreUtils.umd.js +1255 -0
- package/dist/lib/DataStore.d.ts +159 -0
- package/dist/lib/DataStoreEngine.d.ts +89 -0
- package/dist/lib/DataStoreSerializer.d.ts +116 -0
- package/dist/lib/Debouncer.d.ts +86 -0
- package/dist/lib/Errors.d.ts +21 -0
- package/dist/lib/NanoEmitter.d.ts +71 -0
- package/dist/lib/TieredCache.d.ts +86 -0
- package/dist/lib/Translate.d.ts +65 -0
- package/dist/lib/array.d.ts +19 -0
- package/dist/lib/colors.d.ts +27 -0
- package/dist/lib/crypto.d.ts +34 -0
- package/dist/lib/index.d.ts +17 -0
- package/dist/lib/math.d.ts +61 -0
- package/dist/lib/misc.d.ts +65 -0
- package/dist/lib/text.d.ts +41 -0
- package/dist/lib/types.d.ts +44 -0
- package/package.json +86 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
// lib/math.ts
|
|
2
|
+
function bitSetHas(bitSet, checkVal) {
|
|
3
|
+
return (bitSet & checkVal) === checkVal;
|
|
4
|
+
}
|
|
5
|
+
function clamp(value, min, max) {
|
|
6
|
+
if (typeof max !== "number") {
|
|
7
|
+
max = min;
|
|
8
|
+
min = 0;
|
|
9
|
+
}
|
|
10
|
+
return Math.max(Math.min(value, max), min);
|
|
11
|
+
}
|
|
12
|
+
function digitCount(num, withDecimals = true) {
|
|
13
|
+
num = Number(!["string", "number"].includes(typeof num) ? String(num) : num);
|
|
14
|
+
if (typeof num === "number" && isNaN(num))
|
|
15
|
+
return NaN;
|
|
16
|
+
const [intPart, decPart] = num.toString().split(".");
|
|
17
|
+
const intDigits = intPart === "0" ? 1 : Math.floor(Math.log10(Math.abs(Number(intPart))) + 1);
|
|
18
|
+
const decDigits = withDecimals && decPart ? decPart.length : 0;
|
|
19
|
+
return intDigits + decDigits;
|
|
20
|
+
}
|
|
21
|
+
function formatNumber(number, locale, format) {
|
|
22
|
+
return number.toLocaleString(
|
|
23
|
+
locale,
|
|
24
|
+
format === "short" ? {
|
|
25
|
+
notation: "compact",
|
|
26
|
+
compactDisplay: "short",
|
|
27
|
+
maximumFractionDigits: 1
|
|
28
|
+
} : {
|
|
29
|
+
style: "decimal",
|
|
30
|
+
maximumFractionDigits: 0
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function mapRange(value, range1min, range1max, range2min, range2max) {
|
|
35
|
+
if (typeof range2min === "undefined" || typeof range2max === "undefined") {
|
|
36
|
+
range2max = range1max;
|
|
37
|
+
range1max = range1min;
|
|
38
|
+
range2min = range1min = 0;
|
|
39
|
+
}
|
|
40
|
+
if (Number(range1min) === 0 && Number(range2min) === 0)
|
|
41
|
+
return value * (range2max / range1max);
|
|
42
|
+
return (value - range1min) * ((range2max - range2min) / (range1max - range1min)) + range2min;
|
|
43
|
+
}
|
|
44
|
+
function randRange(...args) {
|
|
45
|
+
let min, max, enhancedEntropy = false;
|
|
46
|
+
if (typeof args[0] === "number" && typeof args[1] === "number")
|
|
47
|
+
[min, max] = args;
|
|
48
|
+
else if (typeof args[0] === "number" && typeof args[1] !== "number") {
|
|
49
|
+
min = 0;
|
|
50
|
+
[max] = args;
|
|
51
|
+
} else
|
|
52
|
+
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`);
|
|
53
|
+
if (typeof args[2] === "boolean")
|
|
54
|
+
enhancedEntropy = args[2];
|
|
55
|
+
else if (typeof args[1] === "boolean")
|
|
56
|
+
enhancedEntropy = args[1];
|
|
57
|
+
min = Number(min);
|
|
58
|
+
max = Number(max);
|
|
59
|
+
if (isNaN(min) || isNaN(max))
|
|
60
|
+
return NaN;
|
|
61
|
+
if (min > max)
|
|
62
|
+
throw new TypeError(`Parameter "min" can't be bigger than "max"`);
|
|
63
|
+
if (enhancedEntropy) {
|
|
64
|
+
const uintArr = new Uint8Array(1);
|
|
65
|
+
crypto.getRandomValues(uintArr);
|
|
66
|
+
return Number(Array.from(
|
|
67
|
+
uintArr,
|
|
68
|
+
(v) => Math.round(mapRange(v, 0, 255, min, max)).toString(10)
|
|
69
|
+
).join(""));
|
|
70
|
+
} else
|
|
71
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
72
|
+
}
|
|
73
|
+
function roundFixed(num, fractionDigits) {
|
|
74
|
+
const scale = 10 ** fractionDigits;
|
|
75
|
+
return Math.round(num * scale) / scale;
|
|
76
|
+
}
|
|
77
|
+
function valsWithin(a, b, dec = 10, withinRange = 0.5) {
|
|
78
|
+
return Math.abs(roundFixed(a, dec) - roundFixed(b, dec)) <= withinRange;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// lib/array.ts
|
|
82
|
+
function randomItem(array) {
|
|
83
|
+
return randomItemIndex(array)[0];
|
|
84
|
+
}
|
|
85
|
+
function randomItemIndex(array) {
|
|
86
|
+
if (array.length === 0)
|
|
87
|
+
return [void 0, void 0];
|
|
88
|
+
const idx = randRange(array.length - 1);
|
|
89
|
+
return [array[idx], idx];
|
|
90
|
+
}
|
|
91
|
+
function randomizeArray(array) {
|
|
92
|
+
const retArray = [...array];
|
|
93
|
+
if (array.length === 0)
|
|
94
|
+
return retArray;
|
|
95
|
+
for (let i = retArray.length - 1; i > 0; i--) {
|
|
96
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
97
|
+
[retArray[i], retArray[j]] = [retArray[j], retArray[i]];
|
|
98
|
+
}
|
|
99
|
+
return retArray;
|
|
100
|
+
}
|
|
101
|
+
function takeRandomItem(arr) {
|
|
102
|
+
var _a;
|
|
103
|
+
return (_a = takeRandomItemIndex(arr)) == null ? void 0 : _a[0];
|
|
104
|
+
}
|
|
105
|
+
function takeRandomItemIndex(arr) {
|
|
106
|
+
const [itm, idx] = randomItemIndex(arr);
|
|
107
|
+
if (idx === void 0)
|
|
108
|
+
return [void 0, void 0];
|
|
109
|
+
arr.splice(idx, 1);
|
|
110
|
+
return [itm, idx];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// lib/colors.ts
|
|
114
|
+
function darkenColor(color, percent, upperCase = false) {
|
|
115
|
+
var _a;
|
|
116
|
+
color = color.trim();
|
|
117
|
+
const darkenRgb = (r2, g2, b2, percent2) => {
|
|
118
|
+
r2 = Math.max(0, Math.min(255, r2 - r2 * percent2 / 100));
|
|
119
|
+
g2 = Math.max(0, Math.min(255, g2 - g2 * percent2 / 100));
|
|
120
|
+
b2 = Math.max(0, Math.min(255, b2 - b2 * percent2 / 100));
|
|
121
|
+
return [r2, g2, b2];
|
|
122
|
+
};
|
|
123
|
+
let r, g, b, a;
|
|
124
|
+
const isHexCol = color.match(/^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/);
|
|
125
|
+
if (isHexCol)
|
|
126
|
+
[r, g, b, a] = hexToRgb(color);
|
|
127
|
+
else if (color.startsWith("rgb")) {
|
|
128
|
+
const rgbValues = (_a = color.match(/\d+(\.\d+)?/g)) == null ? void 0 : _a.map(Number);
|
|
129
|
+
if (!rgbValues)
|
|
130
|
+
throw new TypeError("Invalid RGB/RGBA color format");
|
|
131
|
+
[r, g, b, a] = rgbValues;
|
|
132
|
+
} else
|
|
133
|
+
throw new TypeError("Unsupported color format");
|
|
134
|
+
[r, g, b] = darkenRgb(r, g, b, percent);
|
|
135
|
+
if (isHexCol)
|
|
136
|
+
return rgbToHex(r, g, b, a, color.startsWith("#"), upperCase);
|
|
137
|
+
else if (color.startsWith("rgba"))
|
|
138
|
+
return `rgba(${r}, ${g}, ${b}, ${a ?? NaN})`;
|
|
139
|
+
else if (color.startsWith("rgb"))
|
|
140
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
141
|
+
else
|
|
142
|
+
throw new TypeError("Unsupported color format");
|
|
143
|
+
}
|
|
144
|
+
function hexToRgb(hex) {
|
|
145
|
+
hex = (hex.startsWith("#") ? hex.slice(1) : hex).trim();
|
|
146
|
+
const a = hex.length === 8 || hex.length === 4 ? parseInt(hex.slice(-(hex.length / 4)), 16) / (hex.length === 8 ? 255 : 15) : void 0;
|
|
147
|
+
if (!isNaN(Number(a)))
|
|
148
|
+
hex = hex.slice(0, -(hex.length / 4));
|
|
149
|
+
if (hex.length === 3 || hex.length === 4)
|
|
150
|
+
hex = hex.split("").map((c) => c + c).join("");
|
|
151
|
+
const hexInt = parseInt(hex, 16);
|
|
152
|
+
const r = hexInt >> 16 & 255;
|
|
153
|
+
const g = hexInt >> 8 & 255;
|
|
154
|
+
const b = hexInt & 255;
|
|
155
|
+
return [clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255), typeof a === "number" ? clamp(a, 0, 1) : void 0];
|
|
156
|
+
}
|
|
157
|
+
function lightenColor(color, percent, upperCase = false) {
|
|
158
|
+
return darkenColor(color, percent * -1, upperCase);
|
|
159
|
+
}
|
|
160
|
+
function rgbToHex(red, green, blue, alpha, withHash = true, upperCase = false) {
|
|
161
|
+
const toHexVal = (n) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0")[upperCase ? "toUpperCase" : "toLowerCase"]();
|
|
162
|
+
return `${withHash ? "#" : ""}${toHexVal(red)}${toHexVal(green)}${toHexVal(blue)}${alpha ? toHexVal(alpha * 255) : ""}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// lib/crypto.ts
|
|
166
|
+
function abtoa(buf) {
|
|
167
|
+
return btoa(
|
|
168
|
+
new Uint8Array(buf).reduce((data, byte) => data + String.fromCharCode(byte), "")
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
function atoab(str) {
|
|
172
|
+
return Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
|
|
173
|
+
}
|
|
174
|
+
async function compress(input, compressionFormat, outputType = "string") {
|
|
175
|
+
const byteArray = input instanceof ArrayBuffer ? input : new TextEncoder().encode((input == null ? void 0 : input.toString()) ?? String(input));
|
|
176
|
+
const comp = new CompressionStream(compressionFormat);
|
|
177
|
+
const writer = comp.writable.getWriter();
|
|
178
|
+
writer.write(byteArray);
|
|
179
|
+
writer.close();
|
|
180
|
+
const buf = await new Response(comp.readable).arrayBuffer();
|
|
181
|
+
return outputType === "arrayBuffer" ? buf : abtoa(buf);
|
|
182
|
+
}
|
|
183
|
+
async function decompress(input, compressionFormat, outputType = "string") {
|
|
184
|
+
const byteArray = input instanceof ArrayBuffer ? input : atoab((input == null ? void 0 : input.toString()) ?? String(input));
|
|
185
|
+
const decomp = new DecompressionStream(compressionFormat);
|
|
186
|
+
const writer = decomp.writable.getWriter();
|
|
187
|
+
writer.write(byteArray);
|
|
188
|
+
writer.close();
|
|
189
|
+
const buf = await new Response(decomp.readable).arrayBuffer();
|
|
190
|
+
return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
|
|
191
|
+
}
|
|
192
|
+
async function computeHash(input, algorithm = "SHA-256") {
|
|
193
|
+
let data;
|
|
194
|
+
if (typeof input === "string") {
|
|
195
|
+
const encoder = new TextEncoder();
|
|
196
|
+
data = encoder.encode(input);
|
|
197
|
+
} else
|
|
198
|
+
data = input;
|
|
199
|
+
const hashBuffer = await crypto.subtle.digest(algorithm, data);
|
|
200
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
201
|
+
const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
202
|
+
return hashHex;
|
|
203
|
+
}
|
|
204
|
+
function randomId(length = 16, radix = 16, enhancedEntropy = false, randomCase = true) {
|
|
205
|
+
if (length < 1)
|
|
206
|
+
throw new RangeError("The length argument must be at least 1");
|
|
207
|
+
if (radix < 2 || radix > 36)
|
|
208
|
+
throw new RangeError("The radix argument must be between 2 and 36");
|
|
209
|
+
let arr = [];
|
|
210
|
+
const caseArr = randomCase ? [0, 1] : [0];
|
|
211
|
+
if (enhancedEntropy) {
|
|
212
|
+
const uintArr = new Uint8Array(length);
|
|
213
|
+
crypto.getRandomValues(uintArr);
|
|
214
|
+
arr = Array.from(
|
|
215
|
+
uintArr,
|
|
216
|
+
(v) => mapRange(v, 0, 255, 0, radix).toString(radix).substring(0, 1)
|
|
217
|
+
);
|
|
218
|
+
} else {
|
|
219
|
+
arr = Array.from(
|
|
220
|
+
{ length },
|
|
221
|
+
() => Math.floor(Math.random() * radix).toString(radix)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (!arr.some((v) => /[a-zA-Z]/.test(v)))
|
|
225
|
+
return arr.join("");
|
|
226
|
+
return arr.map((v) => caseArr[randRange(0, caseArr.length - 1, enhancedEntropy)] === 1 ? v.toUpperCase() : v).join("");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// lib/misc.ts
|
|
230
|
+
async function consumeGen(valGen) {
|
|
231
|
+
return await (typeof valGen === "function" ? valGen() : valGen);
|
|
232
|
+
}
|
|
233
|
+
async function consumeStringGen(strGen) {
|
|
234
|
+
return typeof strGen === "string" ? strGen : String(
|
|
235
|
+
typeof strGen === "function" ? await strGen() : strGen
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
async function fetchAdvanced(input, options = {}) {
|
|
239
|
+
const { timeout = 1e4 } = options;
|
|
240
|
+
const ctl = new AbortController();
|
|
241
|
+
const { signal, ...restOpts } = options;
|
|
242
|
+
signal == null ? void 0 : signal.addEventListener("abort", () => ctl.abort());
|
|
243
|
+
let sigOpts = {}, id = void 0;
|
|
244
|
+
if (timeout >= 0) {
|
|
245
|
+
id = setTimeout(() => ctl.abort(), timeout);
|
|
246
|
+
sigOpts = { signal: ctl.signal };
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const res = await fetch(input, {
|
|
250
|
+
...restOpts,
|
|
251
|
+
...sigOpts
|
|
252
|
+
});
|
|
253
|
+
typeof id !== "undefined" && clearTimeout(id);
|
|
254
|
+
return res;
|
|
255
|
+
} catch (err) {
|
|
256
|
+
typeof id !== "undefined" && clearTimeout(id);
|
|
257
|
+
throw new Error("Error while calling fetch", { cause: err });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function getListLength(listLike, zeroOnInvalid = true) {
|
|
261
|
+
return "length" in listLike ? listLike.length : "size" in listLike ? listLike.size : "count" in listLike ? listLike.count : zeroOnInvalid ? 0 : NaN;
|
|
262
|
+
}
|
|
263
|
+
function pauseFor(time, signal, rejectOnAbort = false) {
|
|
264
|
+
return new Promise((res, rej) => {
|
|
265
|
+
const timeout = setTimeout(() => res(), time);
|
|
266
|
+
signal == null ? void 0 : signal.addEventListener("abort", () => {
|
|
267
|
+
clearTimeout(timeout);
|
|
268
|
+
rejectOnAbort ? rej(new Error("The pause was aborted")) : res();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function pureObj(obj) {
|
|
273
|
+
return Object.assign(/* @__PURE__ */ Object.create(null), obj ?? {});
|
|
274
|
+
}
|
|
275
|
+
function setImmediateInterval(callback, interval, signal) {
|
|
276
|
+
let intervalId;
|
|
277
|
+
const cleanup = () => clearInterval(intervalId);
|
|
278
|
+
const loop = () => {
|
|
279
|
+
if (signal == null ? void 0 : signal.aborted)
|
|
280
|
+
return cleanup();
|
|
281
|
+
callback();
|
|
282
|
+
};
|
|
283
|
+
signal == null ? void 0 : signal.addEventListener("abort", cleanup);
|
|
284
|
+
loop();
|
|
285
|
+
intervalId = setInterval(loop, interval);
|
|
286
|
+
}
|
|
287
|
+
function setImmediateTimeoutLoop(callback, interval, signal) {
|
|
288
|
+
let timeout;
|
|
289
|
+
const cleanup = () => clearTimeout(timeout);
|
|
290
|
+
const loop = async () => {
|
|
291
|
+
if (signal == null ? void 0 : signal.aborted)
|
|
292
|
+
return cleanup();
|
|
293
|
+
await callback();
|
|
294
|
+
timeout = setTimeout(loop, interval);
|
|
295
|
+
};
|
|
296
|
+
signal == null ? void 0 : signal.addEventListener("abort", cleanup);
|
|
297
|
+
loop();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// lib/text.ts
|
|
301
|
+
function autoPlural(term, num, pluralType = "auto") {
|
|
302
|
+
if (typeof num !== "number") {
|
|
303
|
+
if ("length" in num)
|
|
304
|
+
num = num.length;
|
|
305
|
+
else if ("size" in num)
|
|
306
|
+
num = num.size;
|
|
307
|
+
else if ("count" in num)
|
|
308
|
+
num = num.count;
|
|
309
|
+
}
|
|
310
|
+
if (!["-s", "-ies"].includes(pluralType))
|
|
311
|
+
pluralType = "auto";
|
|
312
|
+
if (isNaN(num))
|
|
313
|
+
num = 2;
|
|
314
|
+
const pType = pluralType === "auto" ? String(term).endsWith("y") ? "-ies" : "-s" : pluralType;
|
|
315
|
+
switch (pType) {
|
|
316
|
+
case "-s":
|
|
317
|
+
return `${term}${num === 1 ? "" : "s"}`;
|
|
318
|
+
case "-ies":
|
|
319
|
+
return `${String(term).slice(0, -1)}${num === 1 ? "y" : "ies"}`;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function capitalize(text) {
|
|
323
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
324
|
+
}
|
|
325
|
+
var defaultPbChars = {
|
|
326
|
+
100: "\u2588",
|
|
327
|
+
75: "\u2593",
|
|
328
|
+
50: "\u2592",
|
|
329
|
+
25: "\u2591",
|
|
330
|
+
0: "\u2500"
|
|
331
|
+
};
|
|
332
|
+
function createProgressBar(percentage, barLength, chars = defaultPbChars) {
|
|
333
|
+
if (percentage === 100)
|
|
334
|
+
return chars[100].repeat(barLength);
|
|
335
|
+
const filledLength = Math.floor(percentage / 100 * barLength);
|
|
336
|
+
const remainingPercentage = percentage / 10 * barLength - filledLength;
|
|
337
|
+
let lastBlock = "";
|
|
338
|
+
if (remainingPercentage >= 0.75)
|
|
339
|
+
lastBlock = chars[75];
|
|
340
|
+
else if (remainingPercentage >= 0.5)
|
|
341
|
+
lastBlock = chars[50];
|
|
342
|
+
else if (remainingPercentage >= 0.25)
|
|
343
|
+
lastBlock = chars[25];
|
|
344
|
+
const filledBar = chars[100].repeat(filledLength);
|
|
345
|
+
const emptyBar = chars[0].repeat(barLength - filledLength - (lastBlock ? 1 : 0));
|
|
346
|
+
return `${filledBar}${lastBlock}${emptyBar}`;
|
|
347
|
+
}
|
|
348
|
+
function insertValues(input, ...values) {
|
|
349
|
+
return input.replace(/%\d/gm, (match) => {
|
|
350
|
+
var _a;
|
|
351
|
+
const argIndex = Number(match.substring(1)) - 1;
|
|
352
|
+
return (_a = values[argIndex] ?? match) == null ? void 0 : _a.toString();
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function joinArrayReadable(array, separators = ", ", lastSeparator = " and ") {
|
|
356
|
+
const arr = [...array];
|
|
357
|
+
if (arr.length === 0)
|
|
358
|
+
return "";
|
|
359
|
+
else if (arr.length === 1)
|
|
360
|
+
return String(arr[0]);
|
|
361
|
+
else if (arr.length === 2)
|
|
362
|
+
return arr.join(lastSeparator);
|
|
363
|
+
const lastItm = lastSeparator + arr[arr.length - 1];
|
|
364
|
+
arr.pop();
|
|
365
|
+
return arr.join(separators) + lastItm;
|
|
366
|
+
}
|
|
367
|
+
function secsToTimeStr(seconds) {
|
|
368
|
+
if (seconds < 0)
|
|
369
|
+
throw new TypeError("Seconds must be a positive number");
|
|
370
|
+
const hours = Math.floor(seconds / 3600);
|
|
371
|
+
const minutes = Math.floor(seconds % 3600 / 60);
|
|
372
|
+
const secs = Math.floor(seconds % 60);
|
|
373
|
+
return [
|
|
374
|
+
hours ? hours + ":" : "",
|
|
375
|
+
String(minutes).padStart(minutes > 0 || hours > 0 ? 2 : 1, "0"),
|
|
376
|
+
":",
|
|
377
|
+
String(secs).padStart(secs > 0 || minutes > 0 || hours > 0 ? 2 : 1, "0")
|
|
378
|
+
].join("");
|
|
379
|
+
}
|
|
380
|
+
function truncStr(input, length, endStr = "...") {
|
|
381
|
+
const str = (input == null ? void 0 : input.toString()) ?? String(input);
|
|
382
|
+
const finalStr = str.length > length ? str.substring(0, length - endStr.length) + endStr : str;
|
|
383
|
+
return finalStr.length > length ? finalStr.substring(0, length) : finalStr;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// lib/Errors.ts
|
|
387
|
+
var DatedError = class extends Error {
|
|
388
|
+
date;
|
|
389
|
+
constructor(message, options) {
|
|
390
|
+
super(message, options);
|
|
391
|
+
this.name = this.constructor.name;
|
|
392
|
+
this.date = /* @__PURE__ */ new Date();
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
var ChecksumMismatchError = class extends DatedError {
|
|
396
|
+
constructor(message, options) {
|
|
397
|
+
super(message, options);
|
|
398
|
+
this.name = "ChecksumMismatchError";
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
var MigrationError = class extends DatedError {
|
|
402
|
+
constructor(message, options) {
|
|
403
|
+
super(message, options);
|
|
404
|
+
this.name = "MigrationError";
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
var ValidationError = class extends DatedError {
|
|
408
|
+
constructor(message, options) {
|
|
409
|
+
super(message, options);
|
|
410
|
+
this.name = "ValidationError";
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// lib/DataStore.ts
|
|
415
|
+
var dsFmtVer = 1;
|
|
416
|
+
var DataStore = class {
|
|
417
|
+
id;
|
|
418
|
+
formatVersion;
|
|
419
|
+
defaultData;
|
|
420
|
+
encodeData;
|
|
421
|
+
decodeData;
|
|
422
|
+
compressionFormat = "deflate-raw";
|
|
423
|
+
engine;
|
|
424
|
+
firstInit = true;
|
|
425
|
+
cachedData;
|
|
426
|
+
migrations;
|
|
427
|
+
migrateIds = [];
|
|
428
|
+
/**
|
|
429
|
+
* Creates an instance of DataStore to manage a sync & async database that is cached in memory and persistently saved across sessions.
|
|
430
|
+
* Supports migrating data from older versions to newer ones and populating the cache with default data if no persistent data is found.
|
|
431
|
+
*
|
|
432
|
+
* - ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue` if the storageMethod is left as the default of `"GM"`
|
|
433
|
+
* - ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
|
|
434
|
+
*
|
|
435
|
+
* @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.)
|
|
436
|
+
* @param opts The options for this DataStore instance
|
|
437
|
+
*/
|
|
438
|
+
constructor(opts) {
|
|
439
|
+
this.id = opts.id;
|
|
440
|
+
this.formatVersion = opts.formatVersion;
|
|
441
|
+
this.defaultData = opts.defaultData;
|
|
442
|
+
this.cachedData = opts.defaultData;
|
|
443
|
+
this.migrations = opts.migrations;
|
|
444
|
+
if (opts.migrateIds)
|
|
445
|
+
this.migrateIds = Array.isArray(opts.migrateIds) ? opts.migrateIds : [opts.migrateIds];
|
|
446
|
+
this.encodeData = opts.encodeData;
|
|
447
|
+
this.decodeData = opts.decodeData;
|
|
448
|
+
this.engine = typeof opts.engine === "function" ? opts.engine() : opts.engine;
|
|
449
|
+
if (typeof opts.compressionFormat === "undefined")
|
|
450
|
+
opts.compressionFormat = "deflate-raw";
|
|
451
|
+
if (typeof opts.compressionFormat === "string") {
|
|
452
|
+
this.encodeData = [opts.compressionFormat, async (data) => await compress(data, opts.compressionFormat, "string")];
|
|
453
|
+
this.decodeData = [opts.compressionFormat, async (data) => await compress(data, opts.compressionFormat, "string")];
|
|
454
|
+
} else if ("encodeData" in opts && "decodeData" in opts && Array.isArray(opts.encodeData) && Array.isArray(opts.decodeData)) {
|
|
455
|
+
this.encodeData = [opts.encodeData[0], opts.encodeData[1]];
|
|
456
|
+
this.decodeData = [opts.decodeData[0], opts.decodeData[1]];
|
|
457
|
+
} else if (opts.compressionFormat === null) {
|
|
458
|
+
this.encodeData = void 0;
|
|
459
|
+
this.decodeData = void 0;
|
|
460
|
+
} else
|
|
461
|
+
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.");
|
|
462
|
+
this.engine.setDataStoreOptions(opts);
|
|
463
|
+
}
|
|
464
|
+
//#region public
|
|
465
|
+
/**
|
|
466
|
+
* Loads the data saved in persistent storage into the in-memory cache and also returns a copy of it.
|
|
467
|
+
* Automatically populates persistent storage with default data if it doesn't contain any data yet.
|
|
468
|
+
* Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
|
|
469
|
+
*/
|
|
470
|
+
async loadData() {
|
|
471
|
+
try {
|
|
472
|
+
if (this.firstInit) {
|
|
473
|
+
this.firstInit = false;
|
|
474
|
+
const dsVer = Number(await this.engine.getValue("__ds_fmt_ver", 0));
|
|
475
|
+
if (isNaN(dsVer) || dsVer < 1) {
|
|
476
|
+
const oldData = await this.engine.getValue(`_uucfg-${this.id}`, null);
|
|
477
|
+
if (oldData) {
|
|
478
|
+
const oldVer = Number(await this.engine.getValue(`_uucfgver-${this.id}`, NaN));
|
|
479
|
+
const oldEnc = await this.engine.getValue(`_uucfgenc-${this.id}`, null);
|
|
480
|
+
const promises = [];
|
|
481
|
+
const migrateFmt = (oldKey, newKey, value) => {
|
|
482
|
+
promises.push(this.engine.setValue(newKey, value));
|
|
483
|
+
promises.push(this.engine.deleteValue(oldKey));
|
|
484
|
+
};
|
|
485
|
+
oldData && migrateFmt(`_uucfg-${this.id}`, `__ds-${this.id}-dat`, oldData);
|
|
486
|
+
!isNaN(oldVer) && migrateFmt(`_uucfgver-${this.id}`, `__ds-${this.id}-ver`, oldVer);
|
|
487
|
+
typeof oldEnc === "boolean" && migrateFmt(`_uucfgenc-${this.id}`, `__ds-${this.id}-enc`, oldEnc === true ? this.compressionFormat : null);
|
|
488
|
+
await Promise.allSettled(promises);
|
|
489
|
+
}
|
|
490
|
+
await this.engine.setValue("__ds_fmt_ver", dsFmtVer);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (this.migrateIds.length > 0) {
|
|
494
|
+
await this.migrateId(this.migrateIds);
|
|
495
|
+
this.migrateIds = [];
|
|
496
|
+
}
|
|
497
|
+
const gmData = await this.engine.getValue(`__ds-${this.id}-dat`, JSON.stringify(this.defaultData));
|
|
498
|
+
let gmFmtVer = Number(await this.engine.getValue(`__ds-${this.id}-ver`, NaN));
|
|
499
|
+
if (typeof gmData !== "string") {
|
|
500
|
+
await this.saveDefaultData();
|
|
501
|
+
return { ...this.defaultData };
|
|
502
|
+
}
|
|
503
|
+
const isEncoded = Boolean(await this.engine.getValue(`__ds-${this.id}-enc`, false));
|
|
504
|
+
let saveData = false;
|
|
505
|
+
if (isNaN(gmFmtVer)) {
|
|
506
|
+
await this.engine.setValue(`__ds-${this.id}-ver`, gmFmtVer = this.formatVersion);
|
|
507
|
+
saveData = true;
|
|
508
|
+
}
|
|
509
|
+
let parsed = await this.engine.deserializeData(gmData, isEncoded);
|
|
510
|
+
if (gmFmtVer < this.formatVersion && this.migrations)
|
|
511
|
+
parsed = await this.runMigrations(parsed, gmFmtVer);
|
|
512
|
+
if (saveData)
|
|
513
|
+
await this.setData(parsed);
|
|
514
|
+
return this.cachedData = this.engine.deepCopy(parsed);
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.warn("Error while parsing JSON data, resetting it to the default value.", err);
|
|
517
|
+
await this.saveDefaultData();
|
|
518
|
+
return this.defaultData;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Returns a copy of the data from the in-memory cache.
|
|
523
|
+
* Use {@linkcode loadData()} to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage).
|
|
524
|
+
*/
|
|
525
|
+
getData() {
|
|
526
|
+
return this.engine.deepCopy(this.cachedData);
|
|
527
|
+
}
|
|
528
|
+
/** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
|
|
529
|
+
setData(data) {
|
|
530
|
+
this.cachedData = data;
|
|
531
|
+
const useEncoding = this.encodingEnabled();
|
|
532
|
+
return new Promise(async (resolve) => {
|
|
533
|
+
await Promise.allSettled([
|
|
534
|
+
this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(data, useEncoding)),
|
|
535
|
+
this.engine.setValue(`__ds-${this.id}-ver`, this.formatVersion),
|
|
536
|
+
this.engine.setValue(`__ds-${this.id}-enc`, useEncoding)
|
|
537
|
+
]);
|
|
538
|
+
resolve();
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
/** Saves the default data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
|
|
542
|
+
async saveDefaultData() {
|
|
543
|
+
this.cachedData = this.defaultData;
|
|
544
|
+
const useEncoding = this.encodingEnabled();
|
|
545
|
+
await Promise.allSettled([
|
|
546
|
+
this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(this.defaultData, useEncoding)),
|
|
547
|
+
this.engine.setValue(`__ds-${this.id}-ver`, this.formatVersion),
|
|
548
|
+
this.engine.setValue(`__ds-${this.id}-enc`, useEncoding)
|
|
549
|
+
]);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Call this method to clear all persistently stored data associated with this DataStore instance.
|
|
553
|
+
* The in-memory cache will be left untouched, so you may still access the data with {@linkcode getData()}
|
|
554
|
+
* Calling {@linkcode loadData()} or {@linkcode setData()} after this method was called will recreate persistent storage with the cached or default data.
|
|
555
|
+
*
|
|
556
|
+
* - ⚠️ This requires the additional directive `@grant GM.deleteValue` if the storageMethod is left as the default of `"GM"`
|
|
557
|
+
*/
|
|
558
|
+
async deleteData() {
|
|
559
|
+
await Promise.allSettled([
|
|
560
|
+
this.engine.deleteValue(`__ds-${this.id}-dat`),
|
|
561
|
+
this.engine.deleteValue(`__ds-${this.id}-ver`),
|
|
562
|
+
this.engine.deleteValue(`__ds-${this.id}-enc`)
|
|
563
|
+
]);
|
|
564
|
+
}
|
|
565
|
+
/** Returns whether encoding and decoding are enabled for this DataStore instance */
|
|
566
|
+
encodingEnabled() {
|
|
567
|
+
return Boolean(this.encodeData && this.decodeData);
|
|
568
|
+
}
|
|
569
|
+
//#region migrations
|
|
570
|
+
/**
|
|
571
|
+
* Runs all necessary migration functions consecutively and saves the result to the in-memory cache and persistent storage and also returns it.
|
|
572
|
+
* This method is automatically called by {@linkcode loadData()} if the data format has changed since the last time the data was saved.
|
|
573
|
+
* 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.
|
|
574
|
+
*
|
|
575
|
+
* 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.
|
|
576
|
+
*/
|
|
577
|
+
async runMigrations(oldData, oldFmtVer, resetOnError = true) {
|
|
578
|
+
if (!this.migrations)
|
|
579
|
+
return oldData;
|
|
580
|
+
let newData = oldData;
|
|
581
|
+
const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
|
|
582
|
+
let lastFmtVer = oldFmtVer;
|
|
583
|
+
for (const [fmtVer, migrationFunc] of sortedMigrations) {
|
|
584
|
+
const ver = Number(fmtVer);
|
|
585
|
+
if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
|
|
586
|
+
try {
|
|
587
|
+
const migRes = migrationFunc(newData);
|
|
588
|
+
newData = migRes instanceof Promise ? await migRes : migRes;
|
|
589
|
+
lastFmtVer = oldFmtVer = ver;
|
|
590
|
+
} catch (err) {
|
|
591
|
+
if (!resetOnError)
|
|
592
|
+
throw new MigrationError(`Error while running migration function for format version '${fmtVer}'`, { cause: err });
|
|
593
|
+
await this.saveDefaultData();
|
|
594
|
+
return this.getData();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
await Promise.allSettled([
|
|
599
|
+
this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(newData)),
|
|
600
|
+
this.engine.setValue(`__ds-${this.id}-ver`, lastFmtVer),
|
|
601
|
+
this.engine.setValue(`__ds-${this.id}-enc`, this.encodingEnabled())
|
|
602
|
+
]);
|
|
603
|
+
return this.cachedData = { ...newData };
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Tries to migrate the currently saved persistent data from one or more old IDs to the ID set in the constructor.
|
|
607
|
+
* 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.
|
|
608
|
+
*/
|
|
609
|
+
async migrateId(oldIds) {
|
|
610
|
+
const ids = Array.isArray(oldIds) ? oldIds : [oldIds];
|
|
611
|
+
await Promise.all(ids.map(async (id) => {
|
|
612
|
+
const data = await this.engine.getValue(`__ds-${id}-dat`, JSON.stringify(this.defaultData));
|
|
613
|
+
const fmtVer = Number(await this.engine.getValue(`__ds-${id}-ver`, NaN));
|
|
614
|
+
const isEncoded = Boolean(await this.engine.getValue(`__ds-${id}-enc`, false));
|
|
615
|
+
if (data === void 0 || isNaN(fmtVer))
|
|
616
|
+
return;
|
|
617
|
+
const parsed = await this.engine.deserializeData(data, isEncoded);
|
|
618
|
+
await Promise.allSettled([
|
|
619
|
+
this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(parsed)),
|
|
620
|
+
this.engine.setValue(`__ds-${this.id}-ver`, fmtVer),
|
|
621
|
+
this.engine.setValue(`__ds-${this.id}-enc`, isEncoded),
|
|
622
|
+
this.engine.deleteValue(`__ds-${id}-dat`),
|
|
623
|
+
this.engine.deleteValue(`__ds-${id}-ver`),
|
|
624
|
+
this.engine.deleteValue(`__ds-${id}-enc`)
|
|
625
|
+
]);
|
|
626
|
+
}));
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// lib/DataStoreEngine.ts
|
|
631
|
+
var DataStoreEngine = class {
|
|
632
|
+
dataStoreOptions;
|
|
633
|
+
// setDataStoreOptions() is called from inside the DataStore constructor to set this value
|
|
634
|
+
/** Called by DataStore on creation, to pass its options */
|
|
635
|
+
setDataStoreOptions(dataStoreOptions) {
|
|
636
|
+
this.dataStoreOptions = dataStoreOptions;
|
|
637
|
+
}
|
|
638
|
+
//#region serialization api
|
|
639
|
+
/** Serializes the given object to a string, optionally encoded with `options.encodeData` if {@linkcode useEncoding} is set to true */
|
|
640
|
+
async serializeData(data, useEncoding) {
|
|
641
|
+
var _a, _b, _c, _d, _e;
|
|
642
|
+
const stringData = JSON.stringify(data);
|
|
643
|
+
if (!useEncoding || !((_a = this.dataStoreOptions) == null ? void 0 : _a.encodeData) || !((_b = this.dataStoreOptions) == null ? void 0 : _b.decodeData))
|
|
644
|
+
return stringData;
|
|
645
|
+
const encRes = (_e = (_d = (_c = this.dataStoreOptions) == null ? void 0 : _c.encodeData) == null ? void 0 : _d[1]) == null ? void 0 : _e.call(_d, stringData);
|
|
646
|
+
if (encRes instanceof Promise)
|
|
647
|
+
return await encRes;
|
|
648
|
+
return encRes;
|
|
649
|
+
}
|
|
650
|
+
/** Deserializes the given string to a JSON object, optionally decoded with `options.decodeData` if {@linkcode useEncoding} is set to true */
|
|
651
|
+
async deserializeData(data, useEncoding) {
|
|
652
|
+
var _a, _b, _c;
|
|
653
|
+
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;
|
|
654
|
+
if (decRes instanceof Promise)
|
|
655
|
+
decRes = await decRes;
|
|
656
|
+
return JSON.parse(decRes ?? data);
|
|
657
|
+
}
|
|
658
|
+
//#region misc api
|
|
659
|
+
/**
|
|
660
|
+
* Copies a JSON-compatible object and loses all its internal references in the process.
|
|
661
|
+
* Uses [`structuredClone()`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) if available, otherwise falls back to `JSON.parse(JSON.stringify(obj))`.
|
|
662
|
+
*/
|
|
663
|
+
deepCopy(obj) {
|
|
664
|
+
try {
|
|
665
|
+
if ("structuredClone" in globalThis)
|
|
666
|
+
return structuredClone(obj);
|
|
667
|
+
} catch {
|
|
668
|
+
}
|
|
669
|
+
return JSON.parse(JSON.stringify(obj));
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
var BrowserStorageEngine = class extends DataStoreEngine {
|
|
673
|
+
options;
|
|
674
|
+
/**
|
|
675
|
+
* Creates an instance of `BrowserStorageEngine`.
|
|
676
|
+
*
|
|
677
|
+
* ⚠️ Requires a DOM environment
|
|
678
|
+
* ⚠️ Don't reuse this engine across multiple {@linkcode DataStore} instances
|
|
679
|
+
*/
|
|
680
|
+
constructor(options) {
|
|
681
|
+
super();
|
|
682
|
+
this.options = {
|
|
683
|
+
type: "localStorage",
|
|
684
|
+
...options
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
//#region storage api
|
|
688
|
+
/** Fetches a value from persistent storage */
|
|
689
|
+
async getValue(name, defaultValue) {
|
|
690
|
+
return (this.options.type === "localStorage" ? globalThis.localStorage.getItem(name) : globalThis.sessionStorage.getItem(name)) ?? defaultValue;
|
|
691
|
+
}
|
|
692
|
+
/** Sets a value in persistent storage */
|
|
693
|
+
async setValue(name, value) {
|
|
694
|
+
if (this.options.type === "localStorage")
|
|
695
|
+
globalThis.localStorage.setItem(name, String(value));
|
|
696
|
+
else
|
|
697
|
+
globalThis.sessionStorage.setItem(name, String(value));
|
|
698
|
+
}
|
|
699
|
+
/** Deletes a value from persistent storage */
|
|
700
|
+
async deleteValue(name) {
|
|
701
|
+
if (this.options.type === "localStorage")
|
|
702
|
+
globalThis.localStorage.removeItem(name);
|
|
703
|
+
else
|
|
704
|
+
globalThis.sessionStorage.removeItem(name);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
var fs;
|
|
708
|
+
var FileStorageEngine = class extends DataStoreEngine {
|
|
709
|
+
options;
|
|
710
|
+
/**
|
|
711
|
+
* Creates an instance of `FileStorageEngine`.
|
|
712
|
+
*
|
|
713
|
+
* ⚠️ Requires Node.js or Deno with Node compatibility
|
|
714
|
+
* ⚠️ Don't reuse this engine across multiple {@linkcode DataStore} instances
|
|
715
|
+
*/
|
|
716
|
+
constructor(options) {
|
|
717
|
+
super();
|
|
718
|
+
this.options = {
|
|
719
|
+
filePath: (id) => `.ds-${id}`,
|
|
720
|
+
...options
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
//#region json file
|
|
724
|
+
/** Reads the file contents */
|
|
725
|
+
async readFile() {
|
|
726
|
+
var _a, _b, _c;
|
|
727
|
+
try {
|
|
728
|
+
if (!fs)
|
|
729
|
+
fs = (await import("node:fs/promises")).default;
|
|
730
|
+
const path = typeof this.options.filePath === "string" ? this.options.filePath : this.options.filePath(this.dataStoreOptions.id);
|
|
731
|
+
const data = await fs.readFile(path, "utf-8");
|
|
732
|
+
if (!data)
|
|
733
|
+
return void 0;
|
|
734
|
+
return 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);
|
|
735
|
+
} catch {
|
|
736
|
+
return void 0;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/** Overwrites the file contents */
|
|
740
|
+
async writeFile(data) {
|
|
741
|
+
var _a, _b, _c;
|
|
742
|
+
try {
|
|
743
|
+
if (!fs)
|
|
744
|
+
fs = (await import("node:fs/promises")).default;
|
|
745
|
+
const path = typeof this.options.filePath === "string" ? this.options.filePath : this.options.filePath(this.dataStoreOptions.id);
|
|
746
|
+
await fs.mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true });
|
|
747
|
+
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");
|
|
748
|
+
} catch (err) {
|
|
749
|
+
console.error("Error writing file:", err);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
//#region storage api
|
|
753
|
+
/** Fetches a value from persistent storage */
|
|
754
|
+
async getValue(name, defaultValue) {
|
|
755
|
+
const data = await this.readFile();
|
|
756
|
+
if (!data)
|
|
757
|
+
return defaultValue;
|
|
758
|
+
const value = data == null ? void 0 : data[name];
|
|
759
|
+
if (value === void 0)
|
|
760
|
+
return defaultValue;
|
|
761
|
+
if (typeof value === "string")
|
|
762
|
+
return value;
|
|
763
|
+
return String(value ?? defaultValue);
|
|
764
|
+
}
|
|
765
|
+
/** Sets a value in persistent storage */
|
|
766
|
+
async setValue(name, value) {
|
|
767
|
+
let data = await this.readFile();
|
|
768
|
+
if (!data)
|
|
769
|
+
data = {};
|
|
770
|
+
data[name] = value;
|
|
771
|
+
await this.writeFile(data);
|
|
772
|
+
}
|
|
773
|
+
/** Deletes a value from persistent storage */
|
|
774
|
+
async deleteValue(name) {
|
|
775
|
+
const data = await this.readFile();
|
|
776
|
+
if (!data)
|
|
777
|
+
return;
|
|
778
|
+
delete data[name];
|
|
779
|
+
await this.writeFile(data);
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
// lib/DataStoreSerializer.ts
|
|
784
|
+
var DataStoreSerializer = class _DataStoreSerializer {
|
|
785
|
+
stores;
|
|
786
|
+
options;
|
|
787
|
+
constructor(stores, options = {}) {
|
|
788
|
+
if (!crypto || !crypto.subtle)
|
|
789
|
+
throw new Error("DataStoreSerializer has to run in a secure context (HTTPS) or in another environment that implements the subtleCrypto API!");
|
|
790
|
+
this.stores = stores;
|
|
791
|
+
this.options = {
|
|
792
|
+
addChecksum: true,
|
|
793
|
+
ensureIntegrity: true,
|
|
794
|
+
...options
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
/** Calculates the checksum of a string */
|
|
798
|
+
async calcChecksum(input) {
|
|
799
|
+
return computeHash(input, "SHA-256");
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Serializes only a subset of the data stores into a string.
|
|
803
|
+
* @param stores An array of store IDs or functions that take a store ID and return a boolean
|
|
804
|
+
* @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
|
|
805
|
+
* @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
|
|
806
|
+
*/
|
|
807
|
+
async serializePartial(stores, useEncoding = true, stringified = true) {
|
|
808
|
+
const serData = [];
|
|
809
|
+
for (const storeInst of this.stores.filter((s) => typeof stores === "function" ? stores(s.id) : stores.includes(s.id))) {
|
|
810
|
+
const data = useEncoding && storeInst.encodingEnabled() ? await storeInst.encodeData[1](JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
|
|
811
|
+
serData.push({
|
|
812
|
+
id: storeInst.id,
|
|
813
|
+
data,
|
|
814
|
+
formatVersion: storeInst.formatVersion,
|
|
815
|
+
encoded: useEncoding && storeInst.encodingEnabled(),
|
|
816
|
+
checksum: this.options.addChecksum ? await this.calcChecksum(data) : void 0
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
return stringified ? JSON.stringify(serData) : serData;
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Serializes the data stores into a string.
|
|
823
|
+
* @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
|
|
824
|
+
* @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
|
|
825
|
+
*/
|
|
826
|
+
async serialize(useEncoding = true, stringified = true) {
|
|
827
|
+
return this.serializePartial(this.stores.map((s) => s.id), useEncoding, stringified);
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Deserializes the data exported via {@linkcode serialize()} and imports only a subset into the DataStore instances.
|
|
831
|
+
* Also triggers the migration process if the data format has changed.
|
|
832
|
+
*/
|
|
833
|
+
async deserializePartial(stores, data) {
|
|
834
|
+
const deserStores = typeof data === "string" ? JSON.parse(data) : data;
|
|
835
|
+
if (!Array.isArray(deserStores) || !deserStores.every(_DataStoreSerializer.isSerializedDataStoreObj))
|
|
836
|
+
throw new TypeError("Invalid serialized data format! Expected an array of SerializedDataStore objects.");
|
|
837
|
+
for (const storeData of deserStores.filter((s) => typeof stores === "function" ? stores(s.id) : stores.includes(s.id))) {
|
|
838
|
+
const storeInst = this.stores.find((s) => s.id === storeData.id);
|
|
839
|
+
if (!storeInst)
|
|
840
|
+
throw new Error(`DataStore instance with ID "${storeData.id}" not found! Make sure to provide it in the DataStoreSerializer constructor.`);
|
|
841
|
+
if (this.options.ensureIntegrity && typeof storeData.checksum === "string") {
|
|
842
|
+
const checksum = await this.calcChecksum(storeData.data);
|
|
843
|
+
if (checksum !== storeData.checksum)
|
|
844
|
+
throw new ChecksumMismatchError(`Checksum mismatch for DataStore with ID "${storeData.id}"!
|
|
845
|
+
Expected: ${storeData.checksum}
|
|
846
|
+
Has: ${checksum}`);
|
|
847
|
+
}
|
|
848
|
+
const decodedData = storeData.encoded && storeInst.encodingEnabled() ? await storeInst.decodeData[1](storeData.data) : storeData.data;
|
|
849
|
+
if (storeData.formatVersion && !isNaN(Number(storeData.formatVersion)) && Number(storeData.formatVersion) < storeInst.formatVersion)
|
|
850
|
+
await storeInst.runMigrations(JSON.parse(decodedData), Number(storeData.formatVersion), false);
|
|
851
|
+
else
|
|
852
|
+
await storeInst.setData(JSON.parse(decodedData));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Deserializes the data exported via {@linkcode serialize()} and imports the data into all matching DataStore instances.
|
|
857
|
+
* Also triggers the migration process if the data format has changed.
|
|
858
|
+
*/
|
|
859
|
+
async deserialize(data) {
|
|
860
|
+
return this.deserializePartial(this.stores.map((s) => s.id), data);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Loads the persistent data of the DataStore instances into the in-memory cache.
|
|
864
|
+
* Also triggers the migration process if the data format has changed.
|
|
865
|
+
* @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
|
|
866
|
+
* @returns Returns a PromiseSettledResult array with the results of each DataStore instance in the format `{ id: string, data: object }`
|
|
867
|
+
*/
|
|
868
|
+
async loadStoresData(stores) {
|
|
869
|
+
return Promise.allSettled(
|
|
870
|
+
this.getStoresFiltered(stores).map(async (store) => ({
|
|
871
|
+
id: store.id,
|
|
872
|
+
data: await store.loadData()
|
|
873
|
+
}))
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Resets the persistent and in-memory data of the DataStore instances to their default values.
|
|
878
|
+
* @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
|
|
879
|
+
*/
|
|
880
|
+
async resetStoresData(stores) {
|
|
881
|
+
return Promise.allSettled(
|
|
882
|
+
this.getStoresFiltered(stores).map((store) => store.saveDefaultData())
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Deletes the persistent data of the DataStore instances.
|
|
887
|
+
* Leaves the in-memory data untouched.
|
|
888
|
+
* @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
|
|
889
|
+
*/
|
|
890
|
+
async deleteStoresData(stores) {
|
|
891
|
+
return Promise.allSettled(
|
|
892
|
+
this.getStoresFiltered(stores).map((store) => store.deleteData())
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
/** Checks if a given value is an array of SerializedDataStore objects */
|
|
896
|
+
static isSerializedDataStoreObjArray(obj) {
|
|
897
|
+
return Array.isArray(obj) && obj.every((o) => typeof o === "object" && o !== null && "id" in o && "data" in o && "formatVersion" in o && "encoded" in o);
|
|
898
|
+
}
|
|
899
|
+
/** Checks if a given value is a SerializedDataStore object */
|
|
900
|
+
static isSerializedDataStoreObj(obj) {
|
|
901
|
+
return typeof obj === "object" && obj !== null && "id" in obj && "data" in obj && "formatVersion" in obj && "encoded" in obj;
|
|
902
|
+
}
|
|
903
|
+
/** Returns the DataStore instances whose IDs match the provided array or function */
|
|
904
|
+
getStoresFiltered(stores) {
|
|
905
|
+
return this.stores.filter((s) => typeof stores === "undefined" ? true : Array.isArray(stores) ? stores.includes(s.id) : stores(s.id));
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
// node_modules/.pnpm/nanoevents@9.1.0/node_modules/nanoevents/index.js
|
|
910
|
+
var createNanoEvents = () => ({
|
|
911
|
+
emit(event, ...args) {
|
|
912
|
+
for (let callbacks = this.events[event] || [], i = 0, length = callbacks.length; i < length; i++) {
|
|
913
|
+
callbacks[i](...args);
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
events: {},
|
|
917
|
+
on(event, cb) {
|
|
918
|
+
;
|
|
919
|
+
(this.events[event] ||= []).push(cb);
|
|
920
|
+
return () => {
|
|
921
|
+
var _a;
|
|
922
|
+
this.events[event] = (_a = this.events[event]) == null ? void 0 : _a.filter((i) => cb !== i);
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// lib/NanoEmitter.ts
|
|
928
|
+
var NanoEmitter = class {
|
|
929
|
+
events = createNanoEvents();
|
|
930
|
+
eventUnsubscribes = [];
|
|
931
|
+
emitterOptions;
|
|
932
|
+
/** Creates a new instance of NanoEmitter - a lightweight event emitter with helper methods and a strongly typed event map */
|
|
933
|
+
constructor(options = {}) {
|
|
934
|
+
this.emitterOptions = {
|
|
935
|
+
publicEmit: false,
|
|
936
|
+
...options
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Subscribes to an event and calls the callback when it's emitted.
|
|
941
|
+
* @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 "_")
|
|
942
|
+
* @returns Returns a function that can be called to unsubscribe the event listener
|
|
943
|
+
* @example ```ts
|
|
944
|
+
* const emitter = new NanoEmitter<{
|
|
945
|
+
* foo: (bar: string) => void;
|
|
946
|
+
* }>({
|
|
947
|
+
* publicEmit: true,
|
|
948
|
+
* });
|
|
949
|
+
*
|
|
950
|
+
* let i = 0;
|
|
951
|
+
* const unsub = emitter.on("foo", (bar) => {
|
|
952
|
+
* // unsubscribe after 10 events:
|
|
953
|
+
* if(++i === 10) unsub();
|
|
954
|
+
* console.log(bar);
|
|
955
|
+
* });
|
|
956
|
+
*
|
|
957
|
+
* emitter.emit("foo", "bar");
|
|
958
|
+
* ```
|
|
959
|
+
*/
|
|
960
|
+
on(event, cb) {
|
|
961
|
+
let unsub;
|
|
962
|
+
const unsubProxy = () => {
|
|
963
|
+
if (!unsub)
|
|
964
|
+
return;
|
|
965
|
+
unsub();
|
|
966
|
+
this.eventUnsubscribes = this.eventUnsubscribes.filter((u) => u !== unsub);
|
|
967
|
+
};
|
|
968
|
+
unsub = this.events.on(event, cb);
|
|
969
|
+
this.eventUnsubscribes.push(unsub);
|
|
970
|
+
return unsubProxy;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Subscribes to an event and calls the callback or resolves the Promise only once when it's emitted.
|
|
974
|
+
* @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 "_")
|
|
975
|
+
* @param cb The callback to call when the event is emitted - if provided or not, the returned Promise will resolve with the event arguments
|
|
976
|
+
* @returns Returns a Promise that resolves with the event arguments when the event is emitted
|
|
977
|
+
* @example ```ts
|
|
978
|
+
* const emitter = new NanoEmitter<{
|
|
979
|
+
* foo: (bar: string) => void;
|
|
980
|
+
* }>();
|
|
981
|
+
*
|
|
982
|
+
* // Promise syntax:
|
|
983
|
+
* const [bar] = await emitter.once("foo");
|
|
984
|
+
* console.log(bar);
|
|
985
|
+
*
|
|
986
|
+
* // Callback syntax:
|
|
987
|
+
* emitter.once("foo", (bar) => console.log(bar));
|
|
988
|
+
* ```
|
|
989
|
+
*/
|
|
990
|
+
once(event, cb) {
|
|
991
|
+
return new Promise((resolve) => {
|
|
992
|
+
let unsub;
|
|
993
|
+
const onceProxy = (...args) => {
|
|
994
|
+
cb == null ? void 0 : cb(...args);
|
|
995
|
+
unsub == null ? void 0 : unsub();
|
|
996
|
+
resolve(args);
|
|
997
|
+
};
|
|
998
|
+
unsub = this.events.on(event, onceProxy);
|
|
999
|
+
this.eventUnsubscribes.push(unsub);
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Emits an event on this instance.
|
|
1004
|
+
* ⚠️ Needs `publicEmit` to be set to true in the NanoEmitter constructor or super() call!
|
|
1005
|
+
* @param event The event to emit
|
|
1006
|
+
* @param args The arguments to pass to the event listeners
|
|
1007
|
+
* @returns Returns true if `publicEmit` is true and the event was emitted successfully
|
|
1008
|
+
*/
|
|
1009
|
+
emit(event, ...args) {
|
|
1010
|
+
if (this.emitterOptions.publicEmit) {
|
|
1011
|
+
this.events.emit(event, ...args);
|
|
1012
|
+
return true;
|
|
1013
|
+
}
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
/** Unsubscribes all event listeners from this instance */
|
|
1017
|
+
unsubscribeAll() {
|
|
1018
|
+
for (const unsub of this.eventUnsubscribes)
|
|
1019
|
+
unsub();
|
|
1020
|
+
this.eventUnsubscribes = [];
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
// lib/Debouncer.ts
|
|
1025
|
+
var Debouncer = class extends NanoEmitter {
|
|
1026
|
+
/**
|
|
1027
|
+
* Creates a new debouncer with the specified timeout and edge type.
|
|
1028
|
+
* @param timeout Timeout in milliseconds between letting through calls - defaults to 200
|
|
1029
|
+
* @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"
|
|
1030
|
+
*/
|
|
1031
|
+
constructor(timeout = 200, type = "immediate") {
|
|
1032
|
+
super();
|
|
1033
|
+
this.timeout = timeout;
|
|
1034
|
+
this.type = type;
|
|
1035
|
+
}
|
|
1036
|
+
/** All registered listener functions and the time they were attached */
|
|
1037
|
+
listeners = [];
|
|
1038
|
+
/** The currently active timeout */
|
|
1039
|
+
activeTimeout;
|
|
1040
|
+
/** The latest queued call */
|
|
1041
|
+
queuedCall;
|
|
1042
|
+
//#region listeners
|
|
1043
|
+
/** Adds a listener function that will be called on timeout */
|
|
1044
|
+
addListener(fn) {
|
|
1045
|
+
this.listeners.push(fn);
|
|
1046
|
+
}
|
|
1047
|
+
/** Removes the listener with the specified function reference */
|
|
1048
|
+
removeListener(fn) {
|
|
1049
|
+
const idx = this.listeners.findIndex((l) => l === fn);
|
|
1050
|
+
idx !== -1 && this.listeners.splice(idx, 1);
|
|
1051
|
+
}
|
|
1052
|
+
/** Removes all listeners */
|
|
1053
|
+
removeAllListeners() {
|
|
1054
|
+
this.listeners = [];
|
|
1055
|
+
}
|
|
1056
|
+
/** Returns all registered listeners */
|
|
1057
|
+
getListeners() {
|
|
1058
|
+
return this.listeners;
|
|
1059
|
+
}
|
|
1060
|
+
//#region timeout
|
|
1061
|
+
/** Sets the timeout for the debouncer */
|
|
1062
|
+
setTimeout(timeout) {
|
|
1063
|
+
this.emit("change", this.timeout = timeout, this.type);
|
|
1064
|
+
}
|
|
1065
|
+
/** Returns the current timeout */
|
|
1066
|
+
getTimeout() {
|
|
1067
|
+
return this.timeout;
|
|
1068
|
+
}
|
|
1069
|
+
/** Whether the timeout is currently active, meaning any latest call to the {@linkcode call()} method will be queued */
|
|
1070
|
+
isTimeoutActive() {
|
|
1071
|
+
return typeof this.activeTimeout !== "undefined";
|
|
1072
|
+
}
|
|
1073
|
+
//#region type
|
|
1074
|
+
/** Sets the edge type for the debouncer */
|
|
1075
|
+
setType(type) {
|
|
1076
|
+
this.emit("change", this.timeout, this.type = type);
|
|
1077
|
+
}
|
|
1078
|
+
/** Returns the current edge type */
|
|
1079
|
+
getType() {
|
|
1080
|
+
return this.type;
|
|
1081
|
+
}
|
|
1082
|
+
//#region call
|
|
1083
|
+
/** Use this to call the debouncer with the specified arguments that will be passed to all listener functions registered with {@linkcode addListener()} */
|
|
1084
|
+
call(...args) {
|
|
1085
|
+
const cl = (...a) => {
|
|
1086
|
+
this.queuedCall = void 0;
|
|
1087
|
+
this.emit("call", ...a);
|
|
1088
|
+
this.listeners.forEach((l) => l.call(this, ...a));
|
|
1089
|
+
};
|
|
1090
|
+
const setRepeatTimeout = () => {
|
|
1091
|
+
this.activeTimeout = setTimeout(() => {
|
|
1092
|
+
if (this.queuedCall) {
|
|
1093
|
+
this.queuedCall();
|
|
1094
|
+
setRepeatTimeout();
|
|
1095
|
+
} else
|
|
1096
|
+
this.activeTimeout = void 0;
|
|
1097
|
+
}, this.timeout);
|
|
1098
|
+
};
|
|
1099
|
+
switch (this.type) {
|
|
1100
|
+
case "immediate":
|
|
1101
|
+
if (typeof this.activeTimeout === "undefined") {
|
|
1102
|
+
cl(...args);
|
|
1103
|
+
setRepeatTimeout();
|
|
1104
|
+
} else
|
|
1105
|
+
this.queuedCall = () => cl(...args);
|
|
1106
|
+
break;
|
|
1107
|
+
case "idle":
|
|
1108
|
+
if (this.activeTimeout)
|
|
1109
|
+
clearTimeout(this.activeTimeout);
|
|
1110
|
+
this.activeTimeout = setTimeout(() => {
|
|
1111
|
+
cl(...args);
|
|
1112
|
+
this.activeTimeout = void 0;
|
|
1113
|
+
}, this.timeout);
|
|
1114
|
+
break;
|
|
1115
|
+
default:
|
|
1116
|
+
throw new TypeError(`Invalid debouncer type: ${this.type}`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
function debounce(fn, timeout = 200, type = "immediate") {
|
|
1121
|
+
const debouncer = new Debouncer(timeout, type);
|
|
1122
|
+
debouncer.addListener(fn);
|
|
1123
|
+
const func = (...args) => debouncer.call(...args);
|
|
1124
|
+
func.debouncer = debouncer;
|
|
1125
|
+
return func;
|
|
1126
|
+
}
|
|
1127
|
+
export {
|
|
1128
|
+
BrowserStorageEngine,
|
|
1129
|
+
ChecksumMismatchError,
|
|
1130
|
+
DataStore,
|
|
1131
|
+
DataStoreEngine,
|
|
1132
|
+
DataStoreSerializer,
|
|
1133
|
+
DatedError,
|
|
1134
|
+
Debouncer,
|
|
1135
|
+
FileStorageEngine,
|
|
1136
|
+
MigrationError,
|
|
1137
|
+
NanoEmitter,
|
|
1138
|
+
ValidationError,
|
|
1139
|
+
abtoa,
|
|
1140
|
+
atoab,
|
|
1141
|
+
autoPlural,
|
|
1142
|
+
bitSetHas,
|
|
1143
|
+
capitalize,
|
|
1144
|
+
clamp,
|
|
1145
|
+
compress,
|
|
1146
|
+
computeHash,
|
|
1147
|
+
consumeGen,
|
|
1148
|
+
consumeStringGen,
|
|
1149
|
+
createProgressBar,
|
|
1150
|
+
darkenColor,
|
|
1151
|
+
debounce,
|
|
1152
|
+
decompress,
|
|
1153
|
+
defaultPbChars,
|
|
1154
|
+
digitCount,
|
|
1155
|
+
fetchAdvanced,
|
|
1156
|
+
formatNumber,
|
|
1157
|
+
getListLength,
|
|
1158
|
+
hexToRgb,
|
|
1159
|
+
insertValues,
|
|
1160
|
+
joinArrayReadable,
|
|
1161
|
+
lightenColor,
|
|
1162
|
+
mapRange,
|
|
1163
|
+
pauseFor,
|
|
1164
|
+
pureObj,
|
|
1165
|
+
randRange,
|
|
1166
|
+
randomId,
|
|
1167
|
+
randomItem,
|
|
1168
|
+
randomItemIndex,
|
|
1169
|
+
randomizeArray,
|
|
1170
|
+
rgbToHex,
|
|
1171
|
+
roundFixed,
|
|
1172
|
+
secsToTimeStr,
|
|
1173
|
+
setImmediateInterval,
|
|
1174
|
+
setImmediateTimeoutLoop,
|
|
1175
|
+
takeRandomItem,
|
|
1176
|
+
takeRandomItemIndex,
|
|
1177
|
+
truncStr,
|
|
1178
|
+
valsWithin
|
|
1179
|
+
};
|
|
1180
|
+
//# sourceMappingURL=CoreUtils.mjs.map
|