@sv443-network/userutils 6.3.0 → 7.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 +23 -0
- package/README.md +309 -128
- package/dist/index.global.js +236 -59
- package/dist/index.js +235 -58
- package/dist/index.mjs +234 -58
- package/dist/lib/DataStore.d.ts +46 -17
- package/dist/lib/DataStoreSerializer.d.ts +44 -0
- package/dist/lib/SelectorObserver.d.ts +10 -5
- package/dist/lib/dom.d.ts +3 -11
- package/dist/lib/index.d.ts +9 -7
- package/dist/lib/math.d.ts +0 -8
- package/dist/lib/misc.d.ts +19 -18
- package/dist/lib/translation.d.ts +1 -1
- package/dist/lib/types.d.ts +17 -0
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -77,14 +77,6 @@ function randRange(...args) {
|
|
|
77
77
|
throw new TypeError(`Parameter "min" can't be bigger than "max"`);
|
|
78
78
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
79
79
|
}
|
|
80
|
-
function randomId(length = 16, radix = 16) {
|
|
81
|
-
const arr = new Uint8Array(length);
|
|
82
|
-
crypto.getRandomValues(arr);
|
|
83
|
-
return Array.from(
|
|
84
|
-
arr,
|
|
85
|
-
(v) => mapRange(v, 0, 255, 0, radix).toString(radix).substring(0, 1)
|
|
86
|
-
).join("");
|
|
87
|
-
}
|
|
88
80
|
|
|
89
81
|
// lib/array.ts
|
|
90
82
|
function randomItem(array) {
|
|
@@ -120,28 +112,32 @@ var DataStore = class {
|
|
|
120
112
|
* Creates an instance of DataStore to manage a sync & async database that is cached in memory and persistently saved across sessions.
|
|
121
113
|
* Supports migrating data from older versions to newer ones and populating the cache with default data if no persistent data is found.
|
|
122
114
|
*
|
|
123
|
-
* ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
|
|
115
|
+
* ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue` if the storageMethod is left as the default of `"GM"`
|
|
124
116
|
* ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
|
|
125
117
|
*
|
|
126
|
-
* @template TData The type of the data that is saved in persistent storage (will be automatically inferred from `
|
|
118
|
+
* @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.)
|
|
127
119
|
* @param options The options for this DataStore instance
|
|
128
|
-
|
|
120
|
+
*/
|
|
129
121
|
constructor(options) {
|
|
130
122
|
__publicField(this, "id");
|
|
131
123
|
__publicField(this, "formatVersion");
|
|
132
124
|
__publicField(this, "defaultData");
|
|
133
|
-
__publicField(this, "cachedData");
|
|
134
|
-
__publicField(this, "migrations");
|
|
135
125
|
__publicField(this, "encodeData");
|
|
136
126
|
__publicField(this, "decodeData");
|
|
127
|
+
__publicField(this, "storageMethod");
|
|
128
|
+
__publicField(this, "cachedData");
|
|
129
|
+
__publicField(this, "migrations");
|
|
130
|
+
var _a;
|
|
137
131
|
this.id = options.id;
|
|
138
132
|
this.formatVersion = options.formatVersion;
|
|
139
133
|
this.defaultData = options.defaultData;
|
|
140
134
|
this.cachedData = options.defaultData;
|
|
141
135
|
this.migrations = options.migrations;
|
|
136
|
+
this.storageMethod = (_a = options.storageMethod) != null ? _a : "GM";
|
|
142
137
|
this.encodeData = options.encodeData;
|
|
143
138
|
this.decodeData = options.decodeData;
|
|
144
139
|
}
|
|
140
|
+
//#region public
|
|
145
141
|
/**
|
|
146
142
|
* Loads the data saved in persistent storage into the in-memory cache and also returns it.
|
|
147
143
|
* Automatically populates persistent storage with default data if it doesn't contain any data yet.
|
|
@@ -150,19 +146,25 @@ var DataStore = class {
|
|
|
150
146
|
loadData() {
|
|
151
147
|
return __async(this, null, function* () {
|
|
152
148
|
try {
|
|
153
|
-
const gmData = yield
|
|
154
|
-
let gmFmtVer = Number(yield
|
|
149
|
+
const gmData = yield this.getValue(`_uucfg-${this.id}`, JSON.stringify(this.defaultData));
|
|
150
|
+
let gmFmtVer = Number(yield this.getValue(`_uucfgver-${this.id}`, NaN));
|
|
155
151
|
if (typeof gmData !== "string") {
|
|
156
152
|
yield this.saveDefaultData();
|
|
157
153
|
return __spreadValues({}, this.defaultData);
|
|
158
154
|
}
|
|
159
|
-
const isEncoded = yield
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
const isEncoded = Boolean(yield this.getValue(`_uucfgenc-${this.id}`, false));
|
|
156
|
+
let saveData = false;
|
|
157
|
+
if (isNaN(gmFmtVer)) {
|
|
158
|
+
yield this.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
|
|
159
|
+
saveData = true;
|
|
160
|
+
}
|
|
162
161
|
let parsed = yield this.deserializeData(gmData, isEncoded);
|
|
163
162
|
if (gmFmtVer < this.formatVersion && this.migrations)
|
|
164
163
|
parsed = yield this.runMigrations(parsed, gmFmtVer);
|
|
165
|
-
|
|
164
|
+
if (saveData)
|
|
165
|
+
yield this.setData(parsed);
|
|
166
|
+
this.cachedData = __spreadValues({}, parsed);
|
|
167
|
+
return this.cachedData;
|
|
166
168
|
} catch (err) {
|
|
167
169
|
console.warn("Error while parsing JSON data, resetting it to the default value.", err);
|
|
168
170
|
yield this.saveDefaultData();
|
|
@@ -173,19 +175,20 @@ var DataStore = class {
|
|
|
173
175
|
/**
|
|
174
176
|
* Returns a copy of the data from the in-memory cache.
|
|
175
177
|
* Use {@linkcode loadData()} to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage).
|
|
178
|
+
* @param deepCopy Whether to return a deep copy of the data (default: `false`) - only necessary if your data object is nested and may have a bigger performance impact if enabled
|
|
176
179
|
*/
|
|
177
|
-
getData() {
|
|
178
|
-
return this.deepCopy(this.cachedData);
|
|
180
|
+
getData(deepCopy = false) {
|
|
181
|
+
return deepCopy ? this.deepCopy(this.cachedData) : __spreadValues({}, this.cachedData);
|
|
179
182
|
}
|
|
180
183
|
/** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
|
|
181
184
|
setData(data) {
|
|
182
185
|
this.cachedData = data;
|
|
183
|
-
const useEncoding =
|
|
186
|
+
const useEncoding = this.encodingEnabled();
|
|
184
187
|
return new Promise((resolve) => __async(this, null, function* () {
|
|
185
188
|
yield Promise.all([
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
this.setValue(`_uucfg-${this.id}`, yield this.serializeData(data, useEncoding)),
|
|
190
|
+
this.setValue(`_uucfgver-${this.id}`, this.formatVersion),
|
|
191
|
+
this.setValue(`_uucfgenc-${this.id}`, useEncoding)
|
|
189
192
|
]);
|
|
190
193
|
resolve();
|
|
191
194
|
}));
|
|
@@ -194,12 +197,12 @@ var DataStore = class {
|
|
|
194
197
|
saveDefaultData() {
|
|
195
198
|
return __async(this, null, function* () {
|
|
196
199
|
this.cachedData = this.defaultData;
|
|
197
|
-
const useEncoding =
|
|
200
|
+
const useEncoding = this.encodingEnabled();
|
|
198
201
|
return new Promise((resolve) => __async(this, null, function* () {
|
|
199
202
|
yield Promise.all([
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
this.setValue(`_uucfg-${this.id}`, yield this.serializeData(this.defaultData, useEncoding)),
|
|
204
|
+
this.setValue(`_uucfgver-${this.id}`, this.formatVersion),
|
|
205
|
+
this.setValue(`_uucfgenc-${this.id}`, useEncoding)
|
|
203
206
|
]);
|
|
204
207
|
resolve();
|
|
205
208
|
}));
|
|
@@ -215,14 +218,25 @@ var DataStore = class {
|
|
|
215
218
|
deleteData() {
|
|
216
219
|
return __async(this, null, function* () {
|
|
217
220
|
yield Promise.all([
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
+
this.deleteValue(`_uucfg-${this.id}`),
|
|
222
|
+
this.deleteValue(`_uucfgver-${this.id}`),
|
|
223
|
+
this.deleteValue(`_uucfgenc-${this.id}`)
|
|
221
224
|
]);
|
|
222
225
|
});
|
|
223
226
|
}
|
|
224
|
-
/**
|
|
225
|
-
|
|
227
|
+
/** Returns whether encoding and decoding are enabled for this DataStore instance */
|
|
228
|
+
encodingEnabled() {
|
|
229
|
+
return Boolean(this.encodeData && this.decodeData);
|
|
230
|
+
}
|
|
231
|
+
//#region migrations
|
|
232
|
+
/**
|
|
233
|
+
* Runs all necessary migration functions consecutively and saves the result to the in-memory cache and persistent storage and also returns it.
|
|
234
|
+
* This method is automatically called by {@linkcode loadData()} if the data format has changed since the last time the data was saved.
|
|
235
|
+
* 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.
|
|
236
|
+
*
|
|
237
|
+
* 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.
|
|
238
|
+
*/
|
|
239
|
+
runMigrations(oldData, oldFmtVer, resetOnError = true) {
|
|
226
240
|
return __async(this, null, function* () {
|
|
227
241
|
if (!this.migrations)
|
|
228
242
|
return oldData;
|
|
@@ -237,6 +251,8 @@ var DataStore = class {
|
|
|
237
251
|
newData = migRes instanceof Promise ? yield migRes : migRes;
|
|
238
252
|
lastFmtVer = oldFmtVer = ver;
|
|
239
253
|
} catch (err) {
|
|
254
|
+
if (!resetOnError)
|
|
255
|
+
throw new Error(`Error while running migration function for format version '${fmtVer}'`);
|
|
240
256
|
console.error(`Error while running migration function for format version '${fmtVer}' - resetting to the default value.`, err);
|
|
241
257
|
yield this.saveDefaultData();
|
|
242
258
|
return this.getData();
|
|
@@ -244,18 +260,19 @@ var DataStore = class {
|
|
|
244
260
|
}
|
|
245
261
|
}
|
|
246
262
|
yield Promise.all([
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
263
|
+
this.setValue(`_uucfg-${this.id}`, yield this.serializeData(newData)),
|
|
264
|
+
this.setValue(`_uucfgver-${this.id}`, lastFmtVer),
|
|
265
|
+
this.setValue(`_uucfgenc-${this.id}`, this.encodingEnabled())
|
|
250
266
|
]);
|
|
251
|
-
return newData;
|
|
267
|
+
return this.cachedData = __spreadValues({}, newData);
|
|
252
268
|
});
|
|
253
269
|
}
|
|
270
|
+
//#region serialization
|
|
254
271
|
/** Serializes the data using the optional this.encodeData() and returns it as a string */
|
|
255
272
|
serializeData(data, useEncoding = true) {
|
|
256
273
|
return __async(this, null, function* () {
|
|
257
274
|
const stringData = JSON.stringify(data);
|
|
258
|
-
if (!this.
|
|
275
|
+
if (!this.encodingEnabled() || !useEncoding)
|
|
259
276
|
return stringData;
|
|
260
277
|
const encRes = this.encodeData(stringData);
|
|
261
278
|
if (encRes instanceof Promise)
|
|
@@ -266,16 +283,131 @@ var DataStore = class {
|
|
|
266
283
|
/** Deserializes the data using the optional this.decodeData() and returns it as a JSON object */
|
|
267
284
|
deserializeData(data, useEncoding = true) {
|
|
268
285
|
return __async(this, null, function* () {
|
|
269
|
-
let decRes = this.
|
|
286
|
+
let decRes = this.encodingEnabled() && useEncoding ? this.decodeData(data) : void 0;
|
|
270
287
|
if (decRes instanceof Promise)
|
|
271
288
|
decRes = yield decRes;
|
|
272
289
|
return JSON.parse(decRes != null ? decRes : data);
|
|
273
290
|
});
|
|
274
291
|
}
|
|
275
|
-
|
|
292
|
+
//#region misc
|
|
293
|
+
/** Copies a JSON-compatible object and loses all its internal references in the process */
|
|
276
294
|
deepCopy(obj) {
|
|
277
295
|
return JSON.parse(JSON.stringify(obj));
|
|
278
296
|
}
|
|
297
|
+
//#region storage
|
|
298
|
+
/** Gets a value from persistent storage - can be overwritten in a subclass if you want to use something other than GM storage */
|
|
299
|
+
getValue(name, defaultValue) {
|
|
300
|
+
return __async(this, null, function* () {
|
|
301
|
+
var _a, _b;
|
|
302
|
+
switch (this.storageMethod) {
|
|
303
|
+
case "localStorage":
|
|
304
|
+
return (_a = localStorage.getItem(name)) != null ? _a : defaultValue;
|
|
305
|
+
case "sessionStorage":
|
|
306
|
+
return (_b = sessionStorage.getItem(name)) != null ? _b : defaultValue;
|
|
307
|
+
default:
|
|
308
|
+
return GM.getValue(name, defaultValue);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Sets a value in persistent storage - can be overwritten in a subclass if you want to use something other than GM storage.
|
|
314
|
+
* The default storage engines will stringify all passed values like numbers or booleans, so be aware of that.
|
|
315
|
+
*/
|
|
316
|
+
setValue(name, value) {
|
|
317
|
+
return __async(this, null, function* () {
|
|
318
|
+
switch (this.storageMethod) {
|
|
319
|
+
case "localStorage":
|
|
320
|
+
return localStorage.setItem(name, String(value));
|
|
321
|
+
case "sessionStorage":
|
|
322
|
+
return sessionStorage.setItem(name, String(value));
|
|
323
|
+
default:
|
|
324
|
+
return GM.setValue(name, String(value));
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
/** Deletes a value from persistent storage - can be overwritten in a subclass if you want to use something other than GM storage */
|
|
329
|
+
deleteValue(name) {
|
|
330
|
+
return __async(this, null, function* () {
|
|
331
|
+
switch (this.storageMethod) {
|
|
332
|
+
case "localStorage":
|
|
333
|
+
return localStorage.removeItem(name);
|
|
334
|
+
case "sessionStorage":
|
|
335
|
+
return sessionStorage.removeItem(name);
|
|
336
|
+
default:
|
|
337
|
+
return GM.deleteValue(name);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// lib/DataStoreSerializer.ts
|
|
344
|
+
var DataStoreSerializer = class {
|
|
345
|
+
constructor(stores, options = {}) {
|
|
346
|
+
__publicField(this, "stores");
|
|
347
|
+
__publicField(this, "options");
|
|
348
|
+
if (!getUnsafeWindow().crypto || !getUnsafeWindow().crypto.subtle)
|
|
349
|
+
throw new Error("DataStoreSerializer has to run in a secure context (HTTPS)!");
|
|
350
|
+
this.stores = stores;
|
|
351
|
+
this.options = __spreadValues({
|
|
352
|
+
addChecksum: true,
|
|
353
|
+
ensureIntegrity: true
|
|
354
|
+
}, options);
|
|
355
|
+
}
|
|
356
|
+
/** Calculates the checksum of a string */
|
|
357
|
+
calcChecksum(input) {
|
|
358
|
+
return __async(this, null, function* () {
|
|
359
|
+
return computeHash(input, "SHA-256");
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/** Serializes a DataStore instance */
|
|
363
|
+
serializeStore(storeInst) {
|
|
364
|
+
return __async(this, null, function* () {
|
|
365
|
+
const data = storeInst.encodingEnabled() ? yield storeInst.encodeData(JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
|
|
366
|
+
const checksum = this.options.addChecksum ? yield this.calcChecksum(data) : void 0;
|
|
367
|
+
return {
|
|
368
|
+
id: storeInst.id,
|
|
369
|
+
data,
|
|
370
|
+
formatVersion: storeInst.formatVersion,
|
|
371
|
+
encoded: storeInst.encodingEnabled(),
|
|
372
|
+
checksum
|
|
373
|
+
};
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
/** Serializes the data stores into a string */
|
|
377
|
+
serialize() {
|
|
378
|
+
return __async(this, null, function* () {
|
|
379
|
+
const serData = [];
|
|
380
|
+
for (const store of this.stores)
|
|
381
|
+
serData.push(yield this.serializeStore(store));
|
|
382
|
+
return JSON.stringify(serData);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Deserializes the data exported via {@linkcode serialize()} and imports it into the DataStore instances.
|
|
387
|
+
* Also triggers the migration process if the data format has changed.
|
|
388
|
+
*/
|
|
389
|
+
deserialize(serializedData) {
|
|
390
|
+
return __async(this, null, function* () {
|
|
391
|
+
const deserStores = JSON.parse(serializedData);
|
|
392
|
+
for (const storeData of deserStores) {
|
|
393
|
+
const storeInst = this.stores.find((s) => s.id === storeData.id);
|
|
394
|
+
if (!storeInst)
|
|
395
|
+
throw new Error(`DataStore instance with ID "${storeData.id}" not found! Make sure to provide it in the DataStoreSerializer constructor.`);
|
|
396
|
+
if (this.options.ensureIntegrity && typeof storeData.checksum === "string") {
|
|
397
|
+
const checksum = yield this.calcChecksum(storeData.data);
|
|
398
|
+
if (checksum !== storeData.checksum)
|
|
399
|
+
throw new Error(`Checksum mismatch for DataStore with ID "${storeData.id}"!
|
|
400
|
+
Expected: ${storeData.checksum}
|
|
401
|
+
Has: ${checksum}`);
|
|
402
|
+
}
|
|
403
|
+
const decodedData = storeData.encoded && storeInst.encodingEnabled() ? yield storeInst.decodeData(storeData.data) : storeData.data;
|
|
404
|
+
if (storeData.formatVersion && !isNaN(Number(storeData.formatVersion)) && Number(storeData.formatVersion) < storeInst.formatVersion)
|
|
405
|
+
yield storeInst.runMigrations(JSON.parse(decodedData), Number(storeData.formatVersion), false);
|
|
406
|
+
else
|
|
407
|
+
yield storeInst.setData(JSON.parse(decodedData));
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
279
411
|
};
|
|
280
412
|
|
|
281
413
|
// lib/dom.ts
|
|
@@ -286,11 +418,6 @@ function getUnsafeWindow() {
|
|
|
286
418
|
return window;
|
|
287
419
|
}
|
|
288
420
|
}
|
|
289
|
-
function insertAfter(beforeElement, afterElement) {
|
|
290
|
-
var _a;
|
|
291
|
-
(_a = beforeElement.parentNode) == null ? void 0 : _a.insertBefore(afterElement, beforeElement.nextSibling);
|
|
292
|
-
return afterElement;
|
|
293
|
-
}
|
|
294
421
|
function addParent(element, newParent) {
|
|
295
422
|
const oldParent = element.parentNode;
|
|
296
423
|
if (!oldParent)
|
|
@@ -437,9 +564,14 @@ function fetchAdvanced(_0) {
|
|
|
437
564
|
id = setTimeout(() => controller.abort(), timeout);
|
|
438
565
|
signalOpts = { signal: controller.signal };
|
|
439
566
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
567
|
+
try {
|
|
568
|
+
const res = yield fetch(input, __spreadValues(__spreadValues({}, options), signalOpts));
|
|
569
|
+
id && clearTimeout(id);
|
|
570
|
+
return res;
|
|
571
|
+
} catch (err) {
|
|
572
|
+
id && clearTimeout(id);
|
|
573
|
+
throw err;
|
|
574
|
+
}
|
|
443
575
|
});
|
|
444
576
|
}
|
|
445
577
|
function insertValues(input, ...values) {
|
|
@@ -479,8 +611,38 @@ function ab2str(buf) {
|
|
|
479
611
|
function str2ab(str) {
|
|
480
612
|
return Uint8Array.from(getUnsafeWindow().atob(str), (c) => c.charCodeAt(0));
|
|
481
613
|
}
|
|
614
|
+
function computeHash(input, algorithm = "SHA-256") {
|
|
615
|
+
return __async(this, null, function* () {
|
|
616
|
+
let data;
|
|
617
|
+
if (typeof input === "string") {
|
|
618
|
+
const encoder = new TextEncoder();
|
|
619
|
+
data = encoder.encode(input);
|
|
620
|
+
} else
|
|
621
|
+
data = input;
|
|
622
|
+
const hashBuffer = yield crypto.subtle.digest(algorithm, data);
|
|
623
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
624
|
+
const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
625
|
+
return hashHex;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
function randomId(length = 16, radix = 16, enhancedEntropy = false) {
|
|
629
|
+
if (enhancedEntropy) {
|
|
630
|
+
const arr = new Uint8Array(length);
|
|
631
|
+
crypto.getRandomValues(arr);
|
|
632
|
+
return Array.from(
|
|
633
|
+
arr,
|
|
634
|
+
(v) => mapRange(v, 0, 255, 0, radix).toString(radix).substring(0, 1)
|
|
635
|
+
).join("");
|
|
636
|
+
}
|
|
637
|
+
return Array.from(
|
|
638
|
+
{ length },
|
|
639
|
+
() => Math.floor(Math.random() * radix).toString(radix)
|
|
640
|
+
).join("");
|
|
641
|
+
}
|
|
482
642
|
|
|
483
643
|
// lib/SelectorObserver.ts
|
|
644
|
+
var domLoaded = false;
|
|
645
|
+
document.addEventListener("DOMContentLoaded", () => domLoaded = true);
|
|
484
646
|
var SelectorObserver = class {
|
|
485
647
|
constructor(baseElement, options = {}) {
|
|
486
648
|
__publicField(this, "enabled", false);
|
|
@@ -491,7 +653,6 @@ var SelectorObserver = class {
|
|
|
491
653
|
__publicField(this, "listenerMap");
|
|
492
654
|
this.baseElement = baseElement;
|
|
493
655
|
this.listenerMap = /* @__PURE__ */ new Map();
|
|
494
|
-
this.observer = new MutationObserver(() => this.checkAllSelectors());
|
|
495
656
|
const _a = options, {
|
|
496
657
|
defaultDebounce,
|
|
497
658
|
defaultDebounceEdge,
|
|
@@ -513,11 +674,21 @@ var SelectorObserver = class {
|
|
|
513
674
|
disableOnNoListeners: disableOnNoListeners != null ? disableOnNoListeners : false,
|
|
514
675
|
enableOnAddListener: enableOnAddListener != null ? enableOnAddListener : true
|
|
515
676
|
};
|
|
677
|
+
if (typeof this.customOptions.checkInterval !== "number") {
|
|
678
|
+
this.observer = new MutationObserver(() => this.checkAllSelectors());
|
|
679
|
+
} else {
|
|
680
|
+
this.checkAllSelectors();
|
|
681
|
+
setInterval(() => this.checkAllSelectors(), this.customOptions.checkInterval);
|
|
682
|
+
}
|
|
516
683
|
}
|
|
684
|
+
/** Call to check all selectors in the {@linkcode listenerMap} using {@linkcode checkSelector()} */
|
|
517
685
|
checkAllSelectors() {
|
|
686
|
+
if (!this.enabled || !domLoaded)
|
|
687
|
+
return;
|
|
518
688
|
for (const [selector, listeners] of this.listenerMap.entries())
|
|
519
689
|
this.checkSelector(selector, listeners);
|
|
520
690
|
}
|
|
691
|
+
/** Checks if the element(s) with the given {@linkcode selector} exist in the DOM and calls the respective {@linkcode listeners} accordingly */
|
|
521
692
|
checkSelector(selector, listeners) {
|
|
522
693
|
var _a;
|
|
523
694
|
if (!this.enabled)
|
|
@@ -549,9 +720,6 @@ var SelectorObserver = class {
|
|
|
549
720
|
this.disable();
|
|
550
721
|
}
|
|
551
722
|
}
|
|
552
|
-
debounce(func, time, edge = "falling") {
|
|
553
|
-
return debounce(func, time, edge);
|
|
554
|
-
}
|
|
555
723
|
/**
|
|
556
724
|
* Starts observing the children of the base element for changes to the given {@linkcode selector} according to the set {@linkcode options}
|
|
557
725
|
* @param selector The selector to observe
|
|
@@ -560,11 +728,16 @@ var SelectorObserver = class {
|
|
|
560
728
|
* @param [options.all] Whether to use `querySelectorAll()` instead - default is false
|
|
561
729
|
* @param [options.continuous] Whether to call the listener continuously instead of just once - default is false
|
|
562
730
|
* @param [options.debounce] Whether to debounce the listener to reduce calls to `querySelector` or `querySelectorAll` - set undefined or <=0 to disable (default)
|
|
731
|
+
* @returns Returns a function that can be called to remove this listener more easily
|
|
563
732
|
*/
|
|
564
733
|
addListener(selector, options) {
|
|
565
|
-
options = __spreadValues({
|
|
734
|
+
options = __spreadValues({
|
|
735
|
+
all: false,
|
|
736
|
+
continuous: false,
|
|
737
|
+
debounce: 0
|
|
738
|
+
}, options);
|
|
566
739
|
if (options.debounce && options.debounce > 0 || this.customOptions.defaultDebounce && this.customOptions.defaultDebounce > 0) {
|
|
567
|
-
options.listener =
|
|
740
|
+
options.listener = debounce(
|
|
568
741
|
options.listener,
|
|
569
742
|
options.debounce || this.customOptions.defaultDebounce,
|
|
570
743
|
options.debounceEdge || this.customOptions.defaultDebounceEdge
|
|
@@ -577,13 +750,15 @@ var SelectorObserver = class {
|
|
|
577
750
|
if (this.enabled === false && this.customOptions.enableOnAddListener)
|
|
578
751
|
this.enable();
|
|
579
752
|
this.checkSelector(selector, [options]);
|
|
753
|
+
return () => this.removeListener(selector, options);
|
|
580
754
|
}
|
|
581
755
|
/** Disables the observation of the child elements */
|
|
582
756
|
disable() {
|
|
757
|
+
var _a;
|
|
583
758
|
if (!this.enabled)
|
|
584
759
|
return;
|
|
585
760
|
this.enabled = false;
|
|
586
|
-
this.observer.disconnect();
|
|
761
|
+
(_a = this.observer) == null ? void 0 : _a.disconnect();
|
|
587
762
|
}
|
|
588
763
|
/**
|
|
589
764
|
* Enables or reenables the observation of the child elements.
|
|
@@ -591,11 +766,12 @@ var SelectorObserver = class {
|
|
|
591
766
|
* @returns Returns true when the observation was enabled, false otherwise (e.g. when the base element wasn't found)
|
|
592
767
|
*/
|
|
593
768
|
enable(immediatelyCheckSelectors = true) {
|
|
769
|
+
var _a;
|
|
594
770
|
const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement;
|
|
595
771
|
if (this.enabled || !baseElement)
|
|
596
772
|
return false;
|
|
597
773
|
this.enabled = true;
|
|
598
|
-
this.observer.observe(baseElement, this.observerOptions);
|
|
774
|
+
(_a = this.observer) == null ? void 0 : _a.observe(baseElement, this.observerOptions);
|
|
599
775
|
if (immediatelyCheckSelectors)
|
|
600
776
|
this.checkAllSelectors();
|
|
601
777
|
return true;
|
|
@@ -665,4 +841,4 @@ tr.getLanguage = () => {
|
|
|
665
841
|
return curLang;
|
|
666
842
|
};
|
|
667
843
|
|
|
668
|
-
export { DataStore, SelectorObserver, addGlobalStyle, addParent, autoPlural, clamp, compress, debounce, decompress, fetchAdvanced, getSiblingsFrame, getUnsafeWindow,
|
|
844
|
+
export { DataStore, DataStoreSerializer, SelectorObserver, addGlobalStyle, addParent, autoPlural, clamp, compress, computeHash, debounce, decompress, fetchAdvanced, getSiblingsFrame, getUnsafeWindow, insertValues, interceptEvent, interceptWindowEvent, isScrollable, mapRange, observeElementProp, openInNewTab, pauseFor, preloadImages, randRange, randomId, randomItem, randomItemIndex, randomizeArray, takeRandomItem, tr };
|
package/dist/lib/DataStore.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="greasemonkey" />
|
|
1
2
|
/** Function that takes the data in the old format and returns the data in the new format. Also supports an asynchronous migration. */
|
|
2
3
|
type MigrationFunc = (oldData: any) => any | Promise<any>;
|
|
3
4
|
/** Dictionary of format version numbers and the function that migrates to them from the previous whole integer */
|
|
@@ -10,7 +11,7 @@ export type DataStoreOptions<TData> = {
|
|
|
10
11
|
* The default data object to use if no data is saved in persistent storage yet.
|
|
11
12
|
* Until the data is loaded from persistent storage with `loadData()`, this will be the data returned by `getData()`
|
|
12
13
|
*
|
|
13
|
-
* ⚠️ This has to be an object that can be serialized to JSON
|
|
14
|
+
* ⚠️ This has to be an object that can be serialized to JSON using `JSON.stringify()`, so no functions or circular references are allowed, they will cause unexpected behavior.
|
|
14
15
|
*/
|
|
15
16
|
defaultData: TData;
|
|
16
17
|
/**
|
|
@@ -28,6 +29,12 @@ export type DataStoreOptions<TData> = {
|
|
|
28
29
|
* If the current format version is not in the dictionary, no migrations will be run.
|
|
29
30
|
*/
|
|
30
31
|
migrations?: DataMigrationsDict;
|
|
32
|
+
/**
|
|
33
|
+
* Where the data should be saved (`"GM"` by default).
|
|
34
|
+
* The protected methods `getValue` , `setValue` and `deleteValue` are used to interact with the storage.
|
|
35
|
+
* If you want to use a different storage method, you can extend the class and overwrite these methods.
|
|
36
|
+
*/
|
|
37
|
+
storageMethod?: "GM" | "localStorage" | "sessionStorage";
|
|
31
38
|
} & ({
|
|
32
39
|
/**
|
|
33
40
|
* Function to use to encode the data prior to saving it in persistent storage.
|
|
@@ -50,32 +57,36 @@ export type DataStoreOptions<TData> = {
|
|
|
50
57
|
decodeData?: never;
|
|
51
58
|
});
|
|
52
59
|
/**
|
|
53
|
-
* Manages a
|
|
60
|
+
* Manages a hybrid synchronous & asynchronous persistent JSON database that is cached in memory and persistently saved across sessions using [GM storage.](https://wiki.greasespot.net/GM.setValue)
|
|
54
61
|
* Supports migrating data from older format versions to newer ones and populating the cache with default data if no persistent data is found.
|
|
55
62
|
*
|
|
56
|
-
*
|
|
63
|
+
* All methods are at least `protected`, so you can easily extend this class and overwrite them to use a different storage method or to add additional functionality.
|
|
64
|
+
* Remember that you can call `super.methodName()` in the subclass to access the original method.
|
|
65
|
+
*
|
|
66
|
+
* ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue` if the storageMethod is left as the default of `"GM"`
|
|
57
67
|
* ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
|
|
58
68
|
*
|
|
59
|
-
* @template TData The type of the data that is saved in persistent storage (will be automatically inferred from `defaultData`) -
|
|
69
|
+
* @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.)
|
|
60
70
|
*/
|
|
61
|
-
export declare class DataStore<TData =
|
|
71
|
+
export declare class DataStore<TData extends object = object> {
|
|
62
72
|
readonly id: string;
|
|
63
73
|
readonly formatVersion: number;
|
|
64
74
|
readonly defaultData: TData;
|
|
75
|
+
readonly encodeData: DataStoreOptions<TData>["encodeData"];
|
|
76
|
+
readonly decodeData: DataStoreOptions<TData>["decodeData"];
|
|
77
|
+
readonly storageMethod: Required<DataStoreOptions<TData>>["storageMethod"];
|
|
65
78
|
private cachedData;
|
|
66
79
|
private migrations?;
|
|
67
|
-
private encodeData;
|
|
68
|
-
private decodeData;
|
|
69
80
|
/**
|
|
70
81
|
* Creates an instance of DataStore to manage a sync & async database that is cached in memory and persistently saved across sessions.
|
|
71
82
|
* Supports migrating data from older versions to newer ones and populating the cache with default data if no persistent data is found.
|
|
72
83
|
*
|
|
73
|
-
* ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
|
|
84
|
+
* ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue` if the storageMethod is left as the default of `"GM"`
|
|
74
85
|
* ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
|
|
75
86
|
*
|
|
76
|
-
* @template TData The type of the data that is saved in persistent storage (will be automatically inferred from `
|
|
87
|
+
* @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.)
|
|
77
88
|
* @param options The options for this DataStore instance
|
|
78
|
-
|
|
89
|
+
*/
|
|
79
90
|
constructor(options: DataStoreOptions<TData>);
|
|
80
91
|
/**
|
|
81
92
|
* Loads the data saved in persistent storage into the in-memory cache and also returns it.
|
|
@@ -86,8 +97,9 @@ export declare class DataStore<TData = any> {
|
|
|
86
97
|
/**
|
|
87
98
|
* Returns a copy of the data from the in-memory cache.
|
|
88
99
|
* Use {@linkcode loadData()} to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage).
|
|
100
|
+
* @param deepCopy Whether to return a deep copy of the data (default: `false`) - only necessary if your data object is nested and may have a bigger performance impact if enabled
|
|
89
101
|
*/
|
|
90
|
-
getData(): TData;
|
|
102
|
+
getData(deepCopy?: boolean): TData;
|
|
91
103
|
/** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
|
|
92
104
|
setData(data: TData): Promise<void>;
|
|
93
105
|
/** Saves the default data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
|
|
@@ -100,13 +112,30 @@ export declare class DataStore<TData = any> {
|
|
|
100
112
|
* ⚠️ This requires the additional directive `@grant GM.deleteValue`
|
|
101
113
|
*/
|
|
102
114
|
deleteData(): Promise<void>;
|
|
103
|
-
/**
|
|
104
|
-
|
|
115
|
+
/** Returns whether encoding and decoding are enabled for this DataStore instance */
|
|
116
|
+
encodingEnabled(): this is Required<Pick<DataStoreOptions<TData>, "encodeData" | "decodeData">>;
|
|
117
|
+
/**
|
|
118
|
+
* Runs all necessary migration functions consecutively and saves the result to the in-memory cache and persistent storage and also returns it.
|
|
119
|
+
* This method is automatically called by {@linkcode loadData()} if the data format has changed since the last time the data was saved.
|
|
120
|
+
* 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.
|
|
121
|
+
*
|
|
122
|
+
* 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.
|
|
123
|
+
*/
|
|
124
|
+
runMigrations(oldData: any, oldFmtVer: number, resetOnError?: boolean): Promise<TData>;
|
|
105
125
|
/** Serializes the data using the optional this.encodeData() and returns it as a string */
|
|
106
|
-
|
|
126
|
+
protected serializeData(data: TData, useEncoding?: boolean): Promise<string>;
|
|
107
127
|
/** Deserializes the data using the optional this.decodeData() and returns it as a JSON object */
|
|
108
|
-
|
|
109
|
-
/** Copies a JSON-compatible object and loses its internal references */
|
|
110
|
-
|
|
128
|
+
protected deserializeData(data: string, useEncoding?: boolean): Promise<TData>;
|
|
129
|
+
/** Copies a JSON-compatible object and loses all its internal references in the process */
|
|
130
|
+
protected deepCopy<T>(obj: T): T;
|
|
131
|
+
/** Gets a value from persistent storage - can be overwritten in a subclass if you want to use something other than GM storage */
|
|
132
|
+
protected getValue<TValue extends GM.Value = string>(name: string, defaultValue: TValue): Promise<string | TValue>;
|
|
133
|
+
/**
|
|
134
|
+
* Sets a value in persistent storage - can be overwritten in a subclass if you want to use something other than GM storage.
|
|
135
|
+
* The default storage engines will stringify all passed values like numbers or booleans, so be aware of that.
|
|
136
|
+
*/
|
|
137
|
+
protected setValue(name: string, value: GM.Value): Promise<void>;
|
|
138
|
+
/** Deletes a value from persistent storage - can be overwritten in a subclass if you want to use something other than GM storage */
|
|
139
|
+
protected deleteValue(name: string): Promise<void>;
|
|
111
140
|
}
|
|
112
141
|
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type DataStore } from "./index.js";
|
|
2
|
+
export type DataStoreSerializerOptions = {
|
|
3
|
+
/** Whether to add a checksum to the exported data */
|
|
4
|
+
addChecksum?: boolean;
|
|
5
|
+
/** Whether to ensure the integrity of the data when importing it (unless the checksum property doesn't exist) */
|
|
6
|
+
ensureIntegrity?: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** Serialized data of a DataStore instance */
|
|
9
|
+
export type SerializedDataStore = {
|
|
10
|
+
/** The ID of the DataStore instance */
|
|
11
|
+
id: string;
|
|
12
|
+
/** The serialized data */
|
|
13
|
+
data: string;
|
|
14
|
+
/** The format version of the data */
|
|
15
|
+
formatVersion: number;
|
|
16
|
+
/** Whether the data is encoded */
|
|
17
|
+
encoded: boolean;
|
|
18
|
+
/** The checksum of the data - key is not present for data without a checksum */
|
|
19
|
+
checksum?: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Allows for easy serialization and deserialization of multiple DataStore instances.
|
|
23
|
+
*
|
|
24
|
+
* All methods are at least `protected`, so you can easily extend this class and overwrite them to use a different storage method or to add additional functionality.
|
|
25
|
+
* Remember that you can call `super.methodName()` in the subclass to access the original method.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ Needs to run in a secure context (HTTPS) due to the use of the SubtleCrypto API if checksumming is enabled.
|
|
28
|
+
*/
|
|
29
|
+
export declare class DataStoreSerializer {
|
|
30
|
+
protected stores: DataStore[];
|
|
31
|
+
protected options: Required<DataStoreSerializerOptions>;
|
|
32
|
+
constructor(stores: DataStore[], options?: DataStoreSerializerOptions);
|
|
33
|
+
/** Calculates the checksum of a string */
|
|
34
|
+
protected calcChecksum(input: string): Promise<string>;
|
|
35
|
+
/** Serializes a DataStore instance */
|
|
36
|
+
protected serializeStore(storeInst: DataStore): Promise<SerializedDataStore>;
|
|
37
|
+
/** Serializes the data stores into a string */
|
|
38
|
+
serialize(): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Deserializes the data exported via {@linkcode serialize()} and imports it into the DataStore instances.
|
|
41
|
+
* Also triggers the migration process if the data format has changed.
|
|
42
|
+
*/
|
|
43
|
+
deserialize(serializedData: string): Promise<void>;
|
|
44
|
+
}
|