@sv443-network/userutils 6.3.0 → 7.0.0
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 +17 -0
- package/README.md +308 -127
- 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 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @sv443-network/userutils
|
|
2
2
|
|
|
3
|
+
## 7.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- fadebf0: Removed the function `insertAfter()` because the DOM API already has the method [`insertAdjacentElement()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement) that has the same functionality and even four positions to pick from.
|
|
8
|
+
To get the same behavior as `insertAfter(refElem, newElem)`, you can use `refElem.insertAdjacentElement("afterend", newElem)`
|
|
9
|
+
|
|
10
|
+
### Minor Changes
|
|
11
|
+
|
|
12
|
+
- ca6ff58: Added option `checkInterval` to SelectorObserver to check on interval instead of on mutation
|
|
13
|
+
- 1e2015c: Added `DataStoreSerializer` class for centralized and much easier and safer de-/serialization of any number of DataStore instances
|
|
14
|
+
- 5190f0b: SelectorObserver's `addListener()` now returns an unsubscribe function to more easily remove a listener
|
|
15
|
+
- e1d467c: Added function `computeHash()` to calculate the hash / checksum of a string
|
|
16
|
+
- 948ac89: DataStore: made `runMigrations`, `encodeData` and `decodeData` public and added `encodingEnabled` method
|
|
17
|
+
- d7cdac0: Made `randomId()` default to using Math.random() and added the parameter `enhancedEntropy` to revert back to the much slower but also much more entropic implementation
|
|
18
|
+
- 287b006: Added ability to change DataStore storage engine from default "GM" to "localStorage" and "sessionStorage"
|
|
19
|
+
|
|
3
20
|
## 6.3.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -26,47 +26,48 @@ View the documentation of previous major releases:
|
|
|
26
26
|
- [**License**](#license)
|
|
27
27
|
- [**Features**](#features)
|
|
28
28
|
- [**DOM:**](#dom)
|
|
29
|
-
- [SelectorObserver](#selectorobserver) - class that manages listeners that are called when selectors are found in the DOM
|
|
30
|
-
- [getUnsafeWindow()](#getunsafewindow) - get the unsafeWindow object or fall back to the regular window object
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
35
|
-
- [
|
|
36
|
-
- [
|
|
37
|
-
- [
|
|
38
|
-
- [
|
|
39
|
-
- [
|
|
40
|
-
- [getSiblingsFrame()](#getsiblingsframe) - returns a frame of an element's siblings, with a given alignment and size
|
|
29
|
+
- [`SelectorObserver`](#selectorobserver) - class that manages listeners that are called when selectors are found in the DOM
|
|
30
|
+
- [`getUnsafeWindow()`](#getunsafewindow) - get the unsafeWindow object or fall back to the regular window object
|
|
31
|
+
- [`addParent()`](#addparent) - add a parent element around another element
|
|
32
|
+
- [`addGlobalStyle()`](#addglobalstyle) - add a global style to the page
|
|
33
|
+
- [`preloadImages()`](#preloadimages) - preload images into the browser cache for faster loading later on
|
|
34
|
+
- [`openInNewTab()`](#openinnewtab) - open a link in a new tab
|
|
35
|
+
- [`interceptEvent()`](#interceptevent) - conditionally intercepts events registered by `addEventListener()` on any given EventTarget object
|
|
36
|
+
- [`interceptWindowEvent()`](#interceptwindowevent) - conditionally intercepts events registered by `addEventListener()` on the window object
|
|
37
|
+
- [`isScrollable()`](#isscrollable) - check if an element has a horizontal or vertical scroll bar
|
|
38
|
+
- [`observeElementProp()`](#observeelementprop) - observe changes to an element's property that can't be observed with MutationObserver
|
|
39
|
+
- [`getSiblingsFrame()`](#getsiblingsframe) - returns a frame of an element's siblings, with a given alignment and size
|
|
41
40
|
- [**Math:**](#math)
|
|
42
|
-
- [clamp()](#clamp) - constrain a number between a min and max value
|
|
43
|
-
- [mapRange()](#maprange) - map a number from one range to the same spot in another range
|
|
44
|
-
- [randRange()](#randrange) - generate a random number between a min and max boundary
|
|
45
|
-
- [randomId()](#randomid) - generate a random ID of a given length and radix
|
|
41
|
+
- [`clamp()`](#clamp) - constrain a number between a min and max value
|
|
42
|
+
- [`mapRange()`](#maprange) - map a number from one range to the same spot in another range
|
|
43
|
+
- [`randRange()`](#randrange) - generate a random number between a min and max boundary
|
|
46
44
|
- [**Misc:**](#misc)
|
|
47
|
-
- [DataStore](#datastore) - class that manages a sync & async persistent JSON database, including data migration
|
|
48
|
-
- [
|
|
49
|
-
- [
|
|
50
|
-
- [
|
|
51
|
-
- [
|
|
52
|
-
- [
|
|
53
|
-
- [
|
|
54
|
-
- [
|
|
45
|
+
- [`DataStore`](#datastore) - class that manages a hybrid sync & async persistent JSON database, including data migration
|
|
46
|
+
- [`DataStoreSerializer`](#datastoreserializer) - class for importing & exporting data of multiple DataStore instances, including compression, checksumming and running migrations
|
|
47
|
+
- [`autoPlural()`](#autoplural) - automatically pluralize a string
|
|
48
|
+
- [`pauseFor()`](#pausefor) - pause the execution of a function for a given amount of time
|
|
49
|
+
- [`debounce()`](#debounce) - call a function only once in a series of calls, after or before a given timeout
|
|
50
|
+
- [`fetchAdvanced()`](#fetchadvanced) - wrapper around the fetch API with a timeout option
|
|
51
|
+
- [`insertValues()`](#insertvalues) - insert values into a string at specified placeholders
|
|
52
|
+
- [`compress()`](#compress) - compress a string with Gzip or Deflate
|
|
53
|
+
- [`decompress()`](#decompress) - decompress a previously compressed string
|
|
54
|
+
- [`computeHash()`](#computehash) - compute the hash / checksum of a string or ArrayBuffer
|
|
55
|
+
- [`randomId()`](#randomid) - generate a random ID of a given length and radix
|
|
55
56
|
- [**Arrays:**](#arrays)
|
|
56
|
-
- [randomItem()](#randomitem) - returns a random item from an array
|
|
57
|
-
- [randomItemIndex()](#randomitemindex) - returns a tuple of a random item and its index from an array
|
|
58
|
-
- [takeRandomItem()](#takerandomitem) - returns a random item from an array and mutates it to remove the item
|
|
59
|
-
- [randomizeArray()](#randomizearray) - returns a copy of the array with its items in a random order
|
|
57
|
+
- [`randomItem()`](#randomitem) - returns a random item from an array
|
|
58
|
+
- [`randomItemIndex()`](#randomitemindex) - returns a tuple of a random item and its index from an array
|
|
59
|
+
- [`takeRandomItem()`](#takerandomitem) - returns a random item from an array and mutates it to remove the item
|
|
60
|
+
- [`randomizeArray()`](#randomizearray) - returns a copy of the array with its items in a random order
|
|
60
61
|
- [**Translation:**](#translation)
|
|
61
|
-
- [tr()](#tr) - simple translation of a string to another language
|
|
62
|
+
- [`tr()`](#tr) - simple translation of a string to another language
|
|
62
63
|
- [tr.addLanguage()](#traddlanguage) - add a language and its translations
|
|
63
64
|
- [tr.setLanguage()](#trsetlanguage) - set the currently active language for translations
|
|
64
65
|
- [tr.getLanguage()](#trgetlanguage) - returns the currently active language
|
|
65
66
|
- [**Utility types for TypeScript:**](#utility-types)
|
|
66
|
-
- [Stringifiable](#stringifiable) - any value that is a string or can be converted to one (implicitly or explicitly)
|
|
67
|
-
- [NonEmptyArray](#nonemptyarray) - any array that should have at least one item
|
|
68
|
-
- [NonEmptyString](#nonemptystring) - any string that should have at least one character
|
|
69
|
-
- [LooseUnion](#looseunion) - a union that gives autocomplete in the IDE but also allows any other value of the same type
|
|
67
|
+
- [`Stringifiable`](#stringifiable) - any value that is a string or can be converted to one (implicitly or explicitly)
|
|
68
|
+
- [`NonEmptyArray`](#nonemptyarray) - any array that should have at least one item
|
|
69
|
+
- [`NonEmptyString`](#nonemptystring) - any string that should have at least one character
|
|
70
|
+
- [`LooseUnion`](#looseunion) - a union that gives autocomplete in the IDE but also allows any other value of the same type
|
|
70
71
|
|
|
71
72
|
<br><br>
|
|
72
73
|
|
|
@@ -80,7 +81,7 @@ View the documentation of previous major releases:
|
|
|
80
81
|
```ts
|
|
81
82
|
import { addGlobalStyle } from "@sv443-network/userutils";
|
|
82
83
|
|
|
83
|
-
// or just import everything (not recommended because
|
|
84
|
+
// or just import everything (not recommended because of worse treeshaking support):
|
|
84
85
|
|
|
85
86
|
import * as UserUtils from "@sv443-network/userutils";
|
|
86
87
|
```
|
|
@@ -145,12 +146,13 @@ new SelectorObserver(baseElementSelector: string, options?: SelectorObserverOpti
|
|
|
145
146
|
```
|
|
146
147
|
|
|
147
148
|
A class that manages listeners that are called when elements at given selectors are found in the DOM.
|
|
148
|
-
|
|
149
|
+
It is useful for userscripts that need to wait for elements to be added to the DOM at an indeterminate point in time before they can be interacted with.
|
|
150
|
+
By default, it uses the MutationObserver API to observe for any element changes, and as such is highly customizable, but can also be configured to run on a fixed interval.
|
|
149
151
|
|
|
150
152
|
The constructor takes a `baseElement`, which is a parent of the elements you want to observe.
|
|
151
153
|
If a selector string is passed instead, it will be used to find the element.
|
|
152
|
-
If you want to observe the entire document, you can pass `document.body`
|
|
153
|
-
|
|
154
|
+
If you want to observe the entire document, you can pass `document.body` - ⚠️ you should only use this to initialize other SelectorObserver instances, and never run continuous listeners on this instance, as the performance impact can be massive!
|
|
155
|
+
|
|
154
156
|
The `options` parameter is optional and will be passed to the MutationObserver that is used internally.
|
|
155
157
|
The MutationObserver options present by default are `{ childList: true, subtree: true }` - you may see the [MutationObserver.observe() documentation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#options) for more information and a list of options.
|
|
156
158
|
For example, if you want to trigger the listeners when certain attributes change, pass `{ attributeFilter: ["class", "data-my-attribute"] }`
|
|
@@ -160,6 +162,7 @@ Additionally, there are the following extra options:
|
|
|
160
162
|
- `enableOnAddListener` - whether to enable the SelectorObserver when a new listener is added (defaults to true)
|
|
161
163
|
- `defaultDebounce` - if set to a number, this debounce will be applied to every listener that doesn't have a custom debounce set (defaults to 0)
|
|
162
164
|
- `defaultDebounceEdge` - can be set to "falling" (default) or "rising", to call the function at (rising) on the very first call and subsequent times after the given debounce time or (falling) the very last call after the debounce time passed with no new calls - [see `debounce()` for more info and a diagram](#debounce)
|
|
165
|
+
- `checkInterval` - if set to a number, the checks will be run on interval instead of on mutation events - in that case all MutationObserverInit props will be ignored
|
|
163
166
|
|
|
164
167
|
⚠️ Make sure to call `enable()` to actually start observing. This will need to be done after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired) **and** as soon as the `baseElement` or `baseElementSelector` is available.
|
|
165
168
|
|
|
@@ -295,7 +298,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
295
298
|
const bazObserver = new SelectorObserver(document.body);
|
|
296
299
|
|
|
297
300
|
// for TypeScript, specify that input elements are returned by the listener:
|
|
298
|
-
bazObserver.addListener<HTMLInputElement>("input", {
|
|
301
|
+
const unsubscribe = bazObserver.addListener<HTMLInputElement>("input", {
|
|
299
302
|
all: true, // use querySelectorAll() instead of querySelector()
|
|
300
303
|
continuous: true, // don't remove the listener after it was called once
|
|
301
304
|
debounce: 50, // debounce the listener by 50ms
|
|
@@ -307,6 +310,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
307
310
|
|
|
308
311
|
bazObserver.enable();
|
|
309
312
|
|
|
313
|
+
window.addEventListener("something", () => {
|
|
314
|
+
// remove the listener after the event "something" was dispatched:
|
|
315
|
+
unsubscribe();
|
|
316
|
+
});
|
|
317
|
+
|
|
310
318
|
|
|
311
319
|
// use a different element as the base:
|
|
312
320
|
|
|
@@ -440,7 +448,7 @@ getUnsafeWindow(): Window
|
|
|
440
448
|
```
|
|
441
449
|
|
|
442
450
|
Returns the unsafeWindow object or falls back to the regular window object if the `@grant unsafeWindow` is not given.
|
|
443
|
-
Userscripts are sandboxed and do not have access to the regular window object, so this function is useful for websites that reject some events that were dispatched by the userscript.
|
|
451
|
+
Userscripts are sandboxed and do not have access to the regular window object, so this function is useful for websites that reject some events that were dispatched by the userscript, or userscripts that need to interact with other userscripts, and more.
|
|
444
452
|
|
|
445
453
|
<details><summary><b>Example - click to view</b></summary>
|
|
446
454
|
|
|
@@ -463,34 +471,6 @@ document.body.dispatchEvent(mouseEvent);
|
|
|
463
471
|
|
|
464
472
|
<br>
|
|
465
473
|
|
|
466
|
-
### insertAfter()
|
|
467
|
-
Usage:
|
|
468
|
-
```ts
|
|
469
|
-
insertAfter(beforeElement: Element, afterElement: Element): Element
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
Inserts the element passed as `afterElement` as a sibling after the passed `beforeElement`.
|
|
473
|
-
The passed `afterElement` will be returned.
|
|
474
|
-
|
|
475
|
-
⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
|
|
476
|
-
|
|
477
|
-
<details><summary><b>Example - click to view</b></summary>
|
|
478
|
-
|
|
479
|
-
```ts
|
|
480
|
-
import { insertAfter } from "@sv443-network/userutils";
|
|
481
|
-
|
|
482
|
-
// insert a <div> as a sibling next to an element
|
|
483
|
-
const beforeElement = document.querySelector("#before");
|
|
484
|
-
const afterElement = document.createElement("div");
|
|
485
|
-
afterElement.innerText = "After";
|
|
486
|
-
|
|
487
|
-
insertAfter(beforeElement, afterElement);
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
</details>
|
|
491
|
-
|
|
492
|
-
<br>
|
|
493
|
-
|
|
494
474
|
### addParent()
|
|
495
475
|
Usage:
|
|
496
476
|
```ts
|
|
@@ -550,7 +530,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
550
530
|
### preloadImages()
|
|
551
531
|
Usage:
|
|
552
532
|
```ts
|
|
553
|
-
preloadImages(urls: string[], rejects?: boolean): Promise<
|
|
533
|
+
preloadImages(urls: string[], rejects?: boolean): Promise<Array<PromiseSettledResult<HTMLImageElement>>>
|
|
554
534
|
```
|
|
555
535
|
|
|
556
536
|
Preloads images into browser cache by creating an invisible `<img>` element for each URL passed.
|
|
@@ -955,36 +935,6 @@ randRange(10); // 7
|
|
|
955
935
|
|
|
956
936
|
</details>
|
|
957
937
|
|
|
958
|
-
<br>
|
|
959
|
-
|
|
960
|
-
### randomId()
|
|
961
|
-
Usage:
|
|
962
|
-
```ts
|
|
963
|
-
randomId(length?: number, radix?: number): string
|
|
964
|
-
```
|
|
965
|
-
|
|
966
|
-
Generates a cryptographically strong random ID of a given length and [radix (base).](https://en.wikipedia.org/wiki/Radix)
|
|
967
|
-
Uses the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) for generating the random numbers.
|
|
968
|
-
⚠️ This is not intended for generating encryption keys, only for generating IDs with a decent amount of entropy!
|
|
969
|
-
|
|
970
|
-
The default length is 16 and the default radix is 16 (hexadecimal).
|
|
971
|
-
You may change the radix to get digits from different numerical systems.
|
|
972
|
-
Use 2 for binary, 8 for octal, 10 for decimal, 16 for hexadecimal and 36 for alphanumeric.
|
|
973
|
-
|
|
974
|
-
<details><summary><b>Example - click to view</b></summary>
|
|
975
|
-
|
|
976
|
-
```ts
|
|
977
|
-
import { randomId } from "@sv443-network/userutils";
|
|
978
|
-
|
|
979
|
-
randomId(); // "1bda419a73629d4f" (length 16, radix 16)
|
|
980
|
-
randomId(10); // "f86cd354a4" (length 10, radix 16)
|
|
981
|
-
randomId(10, 2); // "1010001101" (length 10, radix 2)
|
|
982
|
-
randomId(10, 10); // "0183428506" (length 10, radix 10)
|
|
983
|
-
randomId(10, 36); // "z46jfpa37r" (length 10, radix 36)
|
|
984
|
-
```
|
|
985
|
-
|
|
986
|
-
</details>
|
|
987
|
-
|
|
988
938
|
<br><br>
|
|
989
939
|
|
|
990
940
|
<!-- #SECTION Misc -->
|
|
@@ -996,12 +946,17 @@ Usage:
|
|
|
996
946
|
new DataStore(options: DataStoreOptions)
|
|
997
947
|
```
|
|
998
948
|
|
|
999
|
-
A class that manages a sync & async JSON database that is persistently saved to and loaded from GM storage.
|
|
949
|
+
A class that manages a sync & async JSON database that is persistently saved to and loaded from GM storage, localStorage or sessionStorage.
|
|
1000
950
|
Also supports automatic migration of outdated data formats via provided migration functions.
|
|
1001
951
|
You may create as many instances as you like as long as they have different IDs.
|
|
1002
952
|
|
|
1003
|
-
|
|
1004
|
-
|
|
953
|
+
The class' internal methods are all declared as protected, so you can extend this class and override them if you need to add your own functionality, like changing the location data is stored.
|
|
954
|
+
|
|
955
|
+
If you have multiple DataStore instances and you want to be able to easily and safely export and import their data, take a look at the [DataStoreSerializer](#datastoreserializer) class.
|
|
956
|
+
It combines the data of multiple DataStore instances into a single object that can be exported and imported as a whole by the end user.
|
|
957
|
+
|
|
958
|
+
⚠️ The data is stored as a JSON string, so only JSON-compatible data can be used. Circular structures and complex objects will throw an error on load and save or cause otherwise unexpected behavior.
|
|
959
|
+
⚠️ The directives `@grant GM.getValue` and `@grant GM.setValue` are required if the storageMethod is left as the default of `"GM"`
|
|
1005
960
|
|
|
1006
961
|
The options object has the following properties:
|
|
1007
962
|
| Property | Description |
|
|
@@ -1010,8 +965,9 @@ The options object has the following properties:
|
|
|
1010
965
|
| `defaultData` | The default data to use if no data is saved in persistent storage yet. Until the data is loaded from persistent storage, this will be the data returned by `getData()`. For TypeScript, the type of the data passed here is what will be used for all other methods of the instance. |
|
|
1011
966
|
| `formatVersion` | An incremental version of the data format. If the format of the data is changed in any way, this number should be incremented, in which case all necessary functions of the migrations dictionary will be run consecutively. Never decrement this number or skip numbers. |
|
|
1012
967
|
| `migrations?` | (Optional) A dictionary of functions that can be used to migrate data from older versions of the data to newer ones. The keys of the dictionary should be the format version that the functions can migrate to, from the previous whole integer value. The values should be functions that take the data in the old format and return the data in the new format. The functions will be run in order from the oldest to the newest version. If the current format version is not in the dictionary, no migrations will be run. |
|
|
1013
|
-
| `
|
|
1014
|
-
| `
|
|
968
|
+
| `storageMethod?` | (Optional) The method that is used to store the data. Can be `"GM"` (default), `"localStorage"` or `"sessionStorage"`. If you want to store the data in a different way, you can override the methods of the DataStore class. |
|
|
969
|
+
| `encodeData?` | (Optional, but required when `decodeData` is set) Function that encodes the data before saving - you can use [compress()](#compress) here to save space at the cost of a little bit of performance |
|
|
970
|
+
| `decodeData?` | (Optional, but required when `encodeData` is set) Function that decodes the data when loading - you can use [decompress()](#decompress) here to decode data that was previously compressed with [compress()](#compress) |
|
|
1015
971
|
|
|
1016
972
|
<br>
|
|
1017
973
|
|
|
@@ -1032,10 +988,19 @@ Writes the given data synchronously to the internal cache and asynchronously to
|
|
|
1032
988
|
Writes the default data given in `options.defaultData` synchronously to the internal cache and asynchronously to persistent storage.
|
|
1033
989
|
|
|
1034
990
|
`deleteData(): Promise<void>`
|
|
1035
|
-
Fully deletes the data from persistent storage.
|
|
991
|
+
Fully deletes the data from persistent storage only.
|
|
1036
992
|
The internal cache will be left untouched, so any subsequent calls to `getData()` will return the data that was last loaded.
|
|
1037
993
|
If `loadData()` or `setData()` are called after this, the persistent storage will be populated with the value of `options.defaultData` again.
|
|
994
|
+
This is why you should either immediately repopulate the cache and persistent storage or the page should probably be reloaded or closed after this method is called.
|
|
1038
995
|
⚠️ If you want to use this method, the additional directive `@grant GM.deleteValue` is required.
|
|
996
|
+
|
|
997
|
+
`runMigrations(oldData: any, oldFmtVer: number, resetOnError?: boolean): Promise<TData>`
|
|
998
|
+
Runs all necessary migration functions to migrate the given `oldData` to the latest format.
|
|
999
|
+
If `resetOnError` is set to `false`, the migration will be aborted if an error is thrown and no data will be committed. If it is set to `true` (default) and an error is encountered, it will be suppressed and the `defaultData` will be saved to persistent storage and returned.
|
|
1000
|
+
|
|
1001
|
+
`encodingEnabled(): boolean`
|
|
1002
|
+
Returns `true` if both `options.encodeData` and `options.decodeData` are set, else `false`.
|
|
1003
|
+
Uses TypeScript's type guard notation for easier use in conditional statements.
|
|
1039
1004
|
|
|
1040
1005
|
<br>
|
|
1041
1006
|
|
|
@@ -1052,16 +1017,16 @@ interface MyConfig {
|
|
|
1052
1017
|
qux: string;
|
|
1053
1018
|
}
|
|
1054
1019
|
|
|
1055
|
-
/** Default data */
|
|
1020
|
+
/** Default data returned by getData() calls until setData() is used and also fallback data if something goes wrong */
|
|
1056
1021
|
const defaultData: MyConfig = {
|
|
1057
1022
|
foo: "hello",
|
|
1058
1023
|
bar: 42,
|
|
1059
1024
|
baz: "xyz",
|
|
1060
1025
|
qux: "something",
|
|
1061
1026
|
};
|
|
1062
|
-
/** If any properties are added to, removed from or renamed in MyConfig, increment this number */
|
|
1027
|
+
/** If any properties are added to, removed from, or renamed in the MyConfig type, increment this number */
|
|
1063
1028
|
const formatVersion = 2;
|
|
1064
|
-
/**
|
|
1029
|
+
/** These are functions that migrate outdated data to the latest format - make sure a function exists for every previously used formatVersion and that no numbers are skipped! */
|
|
1065
1030
|
const migrations = {
|
|
1066
1031
|
// migrate from format version 0 to 1
|
|
1067
1032
|
1: (oldData: Record<string, unknown>) => {
|
|
@@ -1073,8 +1038,8 @@ const migrations = {
|
|
|
1073
1038
|
},
|
|
1074
1039
|
// asynchronously migrate from format version 1 to 2
|
|
1075
1040
|
2: async (oldData: Record<string, unknown>) => {
|
|
1076
|
-
// arbitrary async
|
|
1077
|
-
const qux =
|
|
1041
|
+
// using arbitrary async operations for the new format:
|
|
1042
|
+
const qux = await grabQuxDataAsync();
|
|
1078
1043
|
return {
|
|
1079
1044
|
foo: oldData.foo,
|
|
1080
1045
|
bar: oldData.bar,
|
|
@@ -1084,32 +1049,38 @@ const migrations = {
|
|
|
1084
1049
|
},
|
|
1085
1050
|
};
|
|
1086
1051
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1052
|
+
// You probably want to export this instance (or helper functions) so you can use it anywhere in your script:
|
|
1053
|
+
export const manager = new DataStore({
|
|
1054
|
+
/** A unique ID for this instance - choose wisely as changing it is not supported and will result in data loss! */
|
|
1089
1055
|
id: "my-userscript-config",
|
|
1090
|
-
/** Default
|
|
1056
|
+
/** Default, initial and fallback data */
|
|
1091
1057
|
defaultData,
|
|
1092
1058
|
/** The current version of the data format */
|
|
1093
1059
|
formatVersion,
|
|
1094
|
-
/** Data format migration functions */
|
|
1060
|
+
/** Data format migration functions called when the formatVersion is increased */
|
|
1095
1061
|
migrations,
|
|
1062
|
+
/**
|
|
1063
|
+
* Where the data should be stored.
|
|
1064
|
+
* For example, you could use `"sessionStorage"` to make the data be automatically deleted after the browser session is finished, or use `"localStorage"` if you don't have access to GM storage for some reason.
|
|
1065
|
+
*/
|
|
1066
|
+
storageMethod: "localStorage",
|
|
1096
1067
|
|
|
1097
1068
|
// Compression example:
|
|
1098
|
-
// Adding
|
|
1099
|
-
//
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1102
|
-
/**
|
|
1103
|
-
encodeData: (data) => compress(data, "deflate
|
|
1104
|
-
/**
|
|
1105
|
-
decodeData: (data) => decompress(data, "deflate
|
|
1069
|
+
// Adding the following will save space at the cost of a little bit of performance (only for the initial loading and every time new data is saved)
|
|
1070
|
+
// Feel free to use your own functions here, as long as they take in the stringified JSON and return another string, either synchronously or asynchronously
|
|
1071
|
+
// Either both of these properties or none of them should be set
|
|
1072
|
+
|
|
1073
|
+
/** Compresses the data using the "deflate" algorithm and digests it as a string */
|
|
1074
|
+
encodeData: (data) => compress(data, "deflate", "string"),
|
|
1075
|
+
/** Decompresses the "deflate" encoded data as a string */
|
|
1076
|
+
decodeData: (data) => decompress(data, "deflate", "string"),
|
|
1106
1077
|
});
|
|
1107
1078
|
|
|
1108
1079
|
/** Entrypoint of the userscript */
|
|
1109
1080
|
async function init() {
|
|
1110
1081
|
// wait for the data to be loaded from persistent storage
|
|
1111
1082
|
// if no data was saved in persistent storage before or getData() is called before loadData(), the value of options.defaultData will be returned
|
|
1112
|
-
// if the previously saved data needs to be migrated to a newer version, it will happen
|
|
1083
|
+
// if the previously saved data needs to be migrated to a newer version, it will happen inside this function call
|
|
1113
1084
|
const configData = await manager.loadData();
|
|
1114
1085
|
|
|
1115
1086
|
console.log(configData.foo); // "hello"
|
|
@@ -1132,6 +1103,149 @@ init();
|
|
|
1132
1103
|
|
|
1133
1104
|
</details>
|
|
1134
1105
|
|
|
1106
|
+
<br>
|
|
1107
|
+
|
|
1108
|
+
### DataStoreSerializer
|
|
1109
|
+
Usage:
|
|
1110
|
+
```ts
|
|
1111
|
+
new DataStoreSerializer(stores: DataStore[], options: DataStoreSerializerOptions)
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
A class that manages serializing and deserializing (exporting and importing) one to infinite DataStore instances.
|
|
1115
|
+
The serialized data is a JSON string that can be saved to a file, copied to the clipboard, or stored in any other way.
|
|
1116
|
+
Each DataStore instance's settings like data encoding are respected and saved next to the exported data.
|
|
1117
|
+
Also, by default a checksum is calculated and importing data with a mismatching checksum will throw an error.
|
|
1118
|
+
|
|
1119
|
+
The class' internal methods are all declared as protected, so you can extend this class and override them if you need to add your own functionality.
|
|
1120
|
+
|
|
1121
|
+
⚠️ Needs to run in a secure context (HTTPS) due to the use of the SubtleCrypto API.
|
|
1122
|
+
|
|
1123
|
+
The options object has the following properties:
|
|
1124
|
+
| Property | Description |
|
|
1125
|
+
| :-- | :-- |
|
|
1126
|
+
| `addChecksum?` | (Optional) If set to `true` (default), a SHA-256 checksum will be calculated and saved with the serialized data. If set to `false`, no checksum will be calculated and saved. |
|
|
1127
|
+
| `ensureIntegrity?` | (Optional) If set to `true` (default), the checksum will be checked when importing data and an error will be thrown if it doesn't match. If set to `false`, the checksum will not be checked and no error will be thrown. If no checksum property exists on the imported data (for example because it wasn't enabled in a previous data format version), the checksum check will be skipped regardless of this setting. |
|
|
1128
|
+
|
|
1129
|
+
<br>
|
|
1130
|
+
|
|
1131
|
+
#### Methods:
|
|
1132
|
+
`constructor(stores: DataStore[], options?: DataStoreSerializerOptions)`
|
|
1133
|
+
Creates a new DataStoreSerializer instance with the given DataStore instances and options.
|
|
1134
|
+
If no options are passed, the defaults will be used.
|
|
1135
|
+
|
|
1136
|
+
`serialize(): Promise<string>`
|
|
1137
|
+
Serializes all DataStore instances passed in the constructor and returns the serialized data as a JSON string.
|
|
1138
|
+
<details><summary>Click to view the structure of the returned data.</summary>
|
|
1139
|
+
|
|
1140
|
+
```jsonc
|
|
1141
|
+
[
|
|
1142
|
+
{
|
|
1143
|
+
"id": "foo-data", // the ID property given to the DataStore instance
|
|
1144
|
+
"data": "eJyrVkrKTFeyUkrOKM1LLy1WqgUAMvAF6g==", // serialized data (may be compressed / encoded or not)
|
|
1145
|
+
"formatVersion": 2, // the format version of the data
|
|
1146
|
+
"encoded": true, // only set to true if both encodeData and decodeData are set in the DataStore instance
|
|
1147
|
+
"checksum": "420deadbeef69", // property will be missing if addChecksum is set to false
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
// ...
|
|
1151
|
+
}
|
|
1152
|
+
]
|
|
1153
|
+
```
|
|
1154
|
+
</details>
|
|
1155
|
+
|
|
1156
|
+
`deserialize(data: string): Promise<void>`
|
|
1157
|
+
Deserializes the given JSON string and imports the data into the DataStore instances.
|
|
1158
|
+
In the process of importing the data, the migrations will be run, if the `formatVersion` property is lower than the one set on the DataStore instance.
|
|
1159
|
+
|
|
1160
|
+
If `ensureIntegrity` is set to `true` and the checksum doesn't match, an error will be thrown.
|
|
1161
|
+
If `ensureIntegrity` is set to `false`, the checksum check will be skipped entirely.
|
|
1162
|
+
If the `checksum` property is missing on the imported data, the checksum check will also be skipped.
|
|
1163
|
+
If `encoded` is set to `true`, the data will be decoded using the `decodeData` function set on the DataStore instance.
|
|
1164
|
+
|
|
1165
|
+
<br>
|
|
1166
|
+
|
|
1167
|
+
<details><summary><b>Example - click to view</b></summary>
|
|
1168
|
+
|
|
1169
|
+
```ts
|
|
1170
|
+
import { DataStore, DataStoreSerializer, compress, decompress } from "@sv443-network/userutils";
|
|
1171
|
+
|
|
1172
|
+
/** This store doesn't have migrations to run and also has no encodeData and decodeData functions */
|
|
1173
|
+
const fooStore = new DataStore({
|
|
1174
|
+
id: "foo-data",
|
|
1175
|
+
defaultData: {
|
|
1176
|
+
foo: "hello",
|
|
1177
|
+
},
|
|
1178
|
+
formatVersion: 1,
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
/** This store has migrations to run and also has encodeData and decodeData functions */
|
|
1182
|
+
const barStore = new DataStore({
|
|
1183
|
+
id: "bar-data",
|
|
1184
|
+
defaultData: {
|
|
1185
|
+
foo: "hello",
|
|
1186
|
+
},
|
|
1187
|
+
formatVersion: 2,
|
|
1188
|
+
migrations: {
|
|
1189
|
+
2: (oldData) => ({
|
|
1190
|
+
...oldData,
|
|
1191
|
+
bar: "world",
|
|
1192
|
+
}),
|
|
1193
|
+
},
|
|
1194
|
+
encodeData: (data) => compress(data, "deflate", "string"),
|
|
1195
|
+
decodeData: (data) => decompress(data, "deflate", "string"),
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
const serializer = new DataStoreSerializer([fooStore, barStore], {
|
|
1199
|
+
addChecksum: true,
|
|
1200
|
+
ensureIntegrity: true,
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
async function exportMyDataPls() {
|
|
1204
|
+
const serializedData = await serializer.serialize();
|
|
1205
|
+
// create file and download it:
|
|
1206
|
+
const blob = new Blob([serializedData], { type: "application/json" });
|
|
1207
|
+
const url = URL.createObjectURL(blob);
|
|
1208
|
+
const a = document.createElement("a");
|
|
1209
|
+
a.href = url;
|
|
1210
|
+
a.download = `data_export-${new Date().toISOString()}.json`;
|
|
1211
|
+
a.click();
|
|
1212
|
+
a.remove();
|
|
1213
|
+
|
|
1214
|
+
// This function exports an object like this:
|
|
1215
|
+
// [
|
|
1216
|
+
// {
|
|
1217
|
+
// "id": "foo-data",
|
|
1218
|
+
// "data": "{\"foo\":\"hello\"}", // not compressed or encoded because encodeData and decodeData are not set
|
|
1219
|
+
// "formatVersion": 1,
|
|
1220
|
+
// "encoded": false,
|
|
1221
|
+
// "checksum": "420deadbeef69"
|
|
1222
|
+
// },
|
|
1223
|
+
// {
|
|
1224
|
+
// "id": "bar-data",
|
|
1225
|
+
// "data": "eJyrVkrKTFeyUkrOKM1LLy1WqgUAMvAF6g==", // compressed because encodeData and decodeData are set
|
|
1226
|
+
// "formatVersion": 2,
|
|
1227
|
+
// "encoded": true,
|
|
1228
|
+
// "checksum": "69beefdead420"
|
|
1229
|
+
// }
|
|
1230
|
+
// ]
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
async function importMyDataPls() {
|
|
1234
|
+
// grab the data from the file by using the system file picker or any other method
|
|
1235
|
+
const data = await getDataFromSomewhere();
|
|
1236
|
+
|
|
1237
|
+
try {
|
|
1238
|
+
// import the data
|
|
1239
|
+
await serializer.deserialize(data);
|
|
1240
|
+
}
|
|
1241
|
+
catch(err) {
|
|
1242
|
+
console.error(err);
|
|
1243
|
+
alert(`Data import failed: ${err}`);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
```
|
|
1247
|
+
</details>
|
|
1248
|
+
|
|
1135
1249
|
<br><br>
|
|
1136
1250
|
|
|
1137
1251
|
### autoPlural()
|
|
@@ -1366,6 +1480,73 @@ console.log(decompressed); // "Hello, World!"
|
|
|
1366
1480
|
|
|
1367
1481
|
</details>
|
|
1368
1482
|
|
|
1483
|
+
<br>
|
|
1484
|
+
|
|
1485
|
+
### computeHash()
|
|
1486
|
+
Usage:
|
|
1487
|
+
```ts
|
|
1488
|
+
computeHash(input: string | ArrayBuffer, algorithm?: string): Promise<string>
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
Computes a hash / checksum of a string or ArrayBuffer using the specified algorithm ("SHA-256" by default).
|
|
1492
|
+
The algorithm must be supported by the [SubtleCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest).
|
|
1493
|
+
|
|
1494
|
+
⚠️ This function needs to be called in a secure context (HTTPS) due to the use of the SubtleCrypto API.
|
|
1495
|
+
⚠️ If you use this for cryptography, make sure to use a secure algorithm (under no circumstances use SHA-1) and to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) your input data.
|
|
1496
|
+
|
|
1497
|
+
<details><summary><b>Example - click to view</b></summary>
|
|
1498
|
+
|
|
1499
|
+
```ts
|
|
1500
|
+
import { computeHash } from "@sv443-network/userutils";
|
|
1501
|
+
|
|
1502
|
+
async function run() {
|
|
1503
|
+
const hash1 = await computeHash("Hello, World!");
|
|
1504
|
+
const hash2 = await computeHash("Hello, World!");
|
|
1505
|
+
|
|
1506
|
+
console.log(hash1); // dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f
|
|
1507
|
+
console.log(hash1 === hash2); // true (same input = same output)
|
|
1508
|
+
|
|
1509
|
+
const hash3 = await computeHash("Hello, world!"); // lowercase "w"
|
|
1510
|
+
console.log(hash3); // 315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
run();
|
|
1514
|
+
```
|
|
1515
|
+
</details>
|
|
1516
|
+
|
|
1517
|
+
<br>
|
|
1518
|
+
|
|
1519
|
+
### randomId()
|
|
1520
|
+
Usage:
|
|
1521
|
+
```ts
|
|
1522
|
+
randomId(length?: number, radix?: number, enhancedEntropy?: boolean): string
|
|
1523
|
+
```
|
|
1524
|
+
|
|
1525
|
+
Generates a random ID of a given length and [radix (base).](https://en.wikipedia.org/wiki/Radix)
|
|
1526
|
+
|
|
1527
|
+
The default length is 16 and the default radix is 16 (hexadecimal).
|
|
1528
|
+
You may change the radix to get digits from different numerical systems.
|
|
1529
|
+
Use 2 for binary, 8 for octal, 10 for decimal, 16 for hexadecimal and 36 for alphanumeric.
|
|
1530
|
+
|
|
1531
|
+
If `enhancedEntropy` is set to true (false by default), the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) is used for generating the random numbers.
|
|
1532
|
+
Note that this takes MUCH longer, but the generated IDs will have a higher entropy.
|
|
1533
|
+
|
|
1534
|
+
⚠️ Not suitable for generating anything related to cryptography! Use [SubtleCrypto's `generateKey()`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey) for that instead.
|
|
1535
|
+
|
|
1536
|
+
<details><summary><b>Example - click to view</b></summary>
|
|
1537
|
+
|
|
1538
|
+
```ts
|
|
1539
|
+
import { randomId } from "@sv443-network/userutils";
|
|
1540
|
+
|
|
1541
|
+
randomId(); // "1bda419a73629d4f" (length 16, radix 16)
|
|
1542
|
+
randomId(10); // "f86cd354a4" (length 10, radix 16)
|
|
1543
|
+
randomId(10, 2); // "1010001101" (length 10, radix 2)
|
|
1544
|
+
randomId(10, 10); // "0183428506" (length 10, radix 10)
|
|
1545
|
+
randomId(10, 36); // "z46jfpa37r" (length 10, radix 36)
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
</details>
|
|
1549
|
+
|
|
1369
1550
|
<br><br>
|
|
1370
1551
|
|
|
1371
1552
|
<!-- #SECTION Arrays -->
|
|
@@ -1652,8 +1833,9 @@ They don't alter the runtime behavior of the code, but they can be used to make
|
|
|
1652
1833
|
|
|
1653
1834
|
### Stringifiable
|
|
1654
1835
|
This type describes any value that either is a string itself or can be converted to a string.
|
|
1655
|
-
To be considered stringifiable, the object needs to have a `toString()` method that returns a string
|
|
1656
|
-
|
|
1836
|
+
To be considered stringifiable, the object needs to have a `toString()` method that returns a string.
|
|
1837
|
+
Most primitives have this method, but something like undefined or null does not (they can only be used in the `String()` constructor or string interpolation).
|
|
1838
|
+
Having this method allows not just explicit conversion by calling it, but also implicit conversion by passing it into the `String()` constructor or by interpolating it in a template string.
|
|
1657
1839
|
|
|
1658
1840
|
<details><summary><b>Example - click to view</b></summary>
|
|
1659
1841
|
|
|
@@ -1661,7 +1843,7 @@ This method allows not just explicit conversion by calling it, but also implicit
|
|
|
1661
1843
|
import type { Stringifiable } from "@sv443-network/userutils";
|
|
1662
1844
|
|
|
1663
1845
|
function logSomething(value: Stringifiable) {
|
|
1664
|
-
console.log(`Log: ${value}`); // implicit conversion
|
|
1846
|
+
console.log(`Log: ${value}`); // implicit conversion to a string
|
|
1665
1847
|
}
|
|
1666
1848
|
|
|
1667
1849
|
const fooObject = {
|
|
@@ -1675,11 +1857,10 @@ const barObject = {
|
|
|
1675
1857
|
logSomething("foo"); // "Log: foo"
|
|
1676
1858
|
logSomething(42); // "Log: 42"
|
|
1677
1859
|
logSomething(true); // "Log: true"
|
|
1678
|
-
logSomething({}); // "Log: [object Object]"
|
|
1679
1860
|
logSomething(Symbol(1)); // "Log: Symbol(1)"
|
|
1680
1861
|
logSomething(fooObject); // "Log: hello world"
|
|
1681
1862
|
|
|
1682
|
-
logSomething(barObject); // Type
|
|
1863
|
+
logSomething(barObject); // Type error
|
|
1683
1864
|
```
|
|
1684
1865
|
|
|
1685
1866
|
</details>
|