@sv443-network/userutils 6.2.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <!-- #MARKER Description -->
4
4
  ## UserUtils
5
- Zero-dependency library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, manage persistent user configurations, modify the DOM more easily and more.
5
+ Zero-dependency library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and more.
6
6
 
7
7
  Contains builtin TypeScript declarations. Fully web compatible and supports ESM and CJS imports and global declaration.
8
8
  If you like using this library, please consider [supporting the development ❤️](https://github.com/sponsors/Sv443)
@@ -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
- - [insertAfter()](#insertafter) - insert an element as a sibling after another element
32
- - [addParent()](#addparent) - add a parent element around another element
33
- - [addGlobalStyle()](#addglobalstyle) - add a global style to the page
34
- - [preloadImages()](#preloadimages) - preload images into the browser cache for faster loading later on
35
- - [openInNewTab()](#openinnewtab) - open a link in a new tab
36
- - [interceptEvent()](#interceptevent) - conditionally intercepts events registered by `addEventListener()` on any given EventTarget object
37
- - [interceptWindowEvent()](#interceptwindowevent) - conditionally intercepts events registered by `addEventListener()` on the window object
38
- - [isScrollable()](#isscrollable) - check if an element has a horizontal or vertical scroll bar
39
- - [observeElementProp()](#observeelementprop) - observe changes to an element's property that can't be observed with MutationObserver
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
- - [autoPlural()](#autoplural) - automatically pluralize a string
49
- - [pauseFor()](#pausefor) - pause the execution of a function for a given amount of time
50
- - [debounce()](#debounce) - call a function only once in a series of calls, after or before a given timeout
51
- - [fetchAdvanced()](#fetchadvanced) - wrapper around the fetch API with a timeout option
52
- - [insertValues()](#insertvalues) - insert values into a string at specified placeholders
53
- - [compress()](#compress) - compress a string with Gzip or Deflate
54
- - [decompress()](#decompress) - decompress a previously compressed string
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 this doesn't allow for treeshaking):
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
- This 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.
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"] }`
@@ -159,6 +161,8 @@ Additionally, there are the following extra options:
159
161
  - `disableOnNoListeners` - whether to disable the SelectorObserver when there are no listeners left (defaults to false)
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)
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
162
166
 
163
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.
164
168
 
@@ -177,16 +181,19 @@ The listener will be called immediately if the selector already exists in the DO
177
181
  > This will also include elements that were already found in a previous listener call.
178
182
  > If set to false (default), querySelector() will be used and only the first matching element will be returned.
179
183
 
180
- > If `options.continuous` is set to true, the listener will not be deregistered after it was called once (defaults to false).
184
+ > If `options.continuous` is set to true, this listener will not be deregistered after it was called once (defaults to false).
181
185
  >
182
- > ⚠️ You should keep usage of this option to a minimum, as it will cause the listener to be called every time the selector is *checked for and found* and this can stack up quite quickly.
186
+ > ⚠️ You should keep usage of this option to a minimum, as it will cause this listener to be called every time the selector is *checked for and found* and this can stack up quite quickly.
183
187
  > ⚠️ You should try to only use this option on SelectorObserver instances that are scoped really low in the DOM tree to prevent as many selector checks as possible from being triggered.
184
188
  > ⚠️ I also recommend always setting a debounce time (see constructor or below) if you use this option.
185
189
 
186
- > If `options.debounce` is set to a number above 0, the listener will be debounced by that amount of milliseconds (defaults to 0).
187
- > E.g. if the debounce time is set to 200 and the selector is found twice within 100ms, only the last call of the listener will be executed.
190
+ > If `options.debounce` is set to a number above 0, this listener will be debounced by that amount of milliseconds (defaults to 0).
191
+ > E.g. if the debounce time is set to 200 and the selector is found twice within 100ms, only the last call of this listener will be executed.
192
+
193
+ > `options.debounceEdge` is set to "falling" by default, which means the debounce timer will start after the last call of this listener.
194
+ > If set to "rising", the debounce timer will start after the first call of this listener.
188
195
 
189
- > When using TypeScript, the generic `TElement` can be used to specify the type of the element(s) that the listener will return.
196
+ > When using TypeScript, the generic `TElement` can be used to specify the type of the element(s) that this listener will return.
190
197
  > It will default to HTMLElement if left undefined.
191
198
 
192
199
  <br>
@@ -262,6 +269,9 @@ document.addEventListener("DOMContentLoaded", () => {
262
269
  attributeFilter: ["class", "style", "data-whatever"],
263
270
  // debounce all listeners by 100ms unless specified otherwise:
264
271
  defaultDebounce: 100,
272
+ // "rising" means listeners are called immediately and use the debounce as a timeout between subsequent calls - see the debounce() function for a better explanation
273
+ defaultDebounceEdge: "rising",
274
+ // other settings from the MutationObserver API can be set here too - see https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#options
265
275
  });
266
276
 
267
277
  barObserver.addListener("#my-element", {
@@ -273,6 +283,8 @@ document.addEventListener("DOMContentLoaded", () => {
273
283
  barObserver.addListener("#my-other-element", {
274
284
  // set the debounce higher than provided by the defaultDebounce property:
275
285
  debounce: 250,
286
+ // adjust the debounceEdge back to the default "falling" for this specific listener:
287
+ debounceEdge: "falling",
276
288
  listener: (element) => {
277
289
  console.log("Other element's attributes changed:", element);
278
290
  },
@@ -286,7 +298,7 @@ document.addEventListener("DOMContentLoaded", () => {
286
298
  const bazObserver = new SelectorObserver(document.body);
287
299
 
288
300
  // for TypeScript, specify that input elements are returned by the listener:
289
- bazObserver.addListener<HTMLInputElement>("input", {
301
+ const unsubscribe = bazObserver.addListener<HTMLInputElement>("input", {
290
302
  all: true, // use querySelectorAll() instead of querySelector()
291
303
  continuous: true, // don't remove the listener after it was called once
292
304
  debounce: 50, // debounce the listener by 50ms
@@ -298,6 +310,11 @@ document.addEventListener("DOMContentLoaded", () => {
298
310
 
299
311
  bazObserver.enable();
300
312
 
313
+ window.addEventListener("something", () => {
314
+ // remove the listener after the event "something" was dispatched:
315
+ unsubscribe();
316
+ });
317
+
301
318
 
302
319
  // use a different element as the base:
303
320
 
@@ -431,7 +448,7 @@ getUnsafeWindow(): Window
431
448
  ```
432
449
 
433
450
  Returns the unsafeWindow object or falls back to the regular window object if the `@grant unsafeWindow` is not given.
434
- 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.
435
452
 
436
453
  <details><summary><b>Example - click to view</b></summary>
437
454
 
@@ -454,34 +471,6 @@ document.body.dispatchEvent(mouseEvent);
454
471
 
455
472
  <br>
456
473
 
457
- ### insertAfter()
458
- Usage:
459
- ```ts
460
- insertAfter(beforeElement: Element, afterElement: Element): Element
461
- ```
462
-
463
- Inserts the element passed as `afterElement` as a sibling after the passed `beforeElement`.
464
- The passed `afterElement` will be returned.
465
-
466
- ⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
467
-
468
- <details><summary><b>Example - click to view</b></summary>
469
-
470
- ```ts
471
- import { insertAfter } from "@sv443-network/userutils";
472
-
473
- // insert a <div> as a sibling next to an element
474
- const beforeElement = document.querySelector("#before");
475
- const afterElement = document.createElement("div");
476
- afterElement.innerText = "After";
477
-
478
- insertAfter(beforeElement, afterElement);
479
- ```
480
-
481
- </details>
482
-
483
- <br>
484
-
485
474
  ### addParent()
486
475
  Usage:
487
476
  ```ts
@@ -541,7 +530,7 @@ document.addEventListener("DOMContentLoaded", () => {
541
530
  ### preloadImages()
542
531
  Usage:
543
532
  ```ts
544
- preloadImages(urls: string[], rejects?: boolean): Promise<void>
533
+ preloadImages(urls: string[], rejects?: boolean): Promise<Array<PromiseSettledResult<HTMLImageElement>>>
545
534
  ```
546
535
 
547
536
  Preloads images into browser cache by creating an invisible `<img>` element for each URL passed.
@@ -573,14 +562,14 @@ preloadImages([
573
562
  ### openInNewTab()
574
563
  Usage:
575
564
  ```ts
576
- openInNewTab(url: string): void
565
+ openInNewTab(url: string, background?: boolean): void
577
566
  ```
578
567
 
579
- Creates an invisible anchor with a `_blank` target and clicks it.
580
- Contrary to `window.open()`, this has a lesser chance to get blocked by the browser's popup blocker and doesn't open the URL as a new window.
581
- This function has to be run in response to a user interaction event, else the browser might reject it.
568
+ Tries to use `GM.openInTab` to open the given URL in a new tab, or as a fallback if the grant is not given, creates an invisible anchor element and clicks it.
569
+ If `background` is set to true, the tab will be opened in the background. Leave `undefined` to use the browser's default behavior.
582
570
 
583
- ⚠️ This function needs to be run after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired).
571
+ ⚠️ Needs the `@grant GM.openInTab` directive, otherwise only the fallback behavior will be used and the warning below is extra important:
572
+ ⚠️ For the fallback to work, this function needs to be run in response to a user interaction event, else the browser might reject it.
584
573
 
585
574
  <details><summary><b>Example - click to view</b></summary>
586
575
 
@@ -588,7 +577,8 @@ This function has to be run in response to a user interaction event, else the br
588
577
  import { openInNewTab } from "@sv443-network/userutils";
589
578
 
590
579
  document.querySelector("#my-button").addEventListener("click", () => {
591
- openInNewTab("https://example.org/");
580
+ // open in background:
581
+ openInNewTab("https://example.org/", true);
592
582
  });
593
583
  ```
594
584
 
@@ -945,36 +935,6 @@ randRange(10); // 7
945
935
 
946
936
  </details>
947
937
 
948
- <br>
949
-
950
- ### randomId()
951
- Usage:
952
- ```ts
953
- randomId(length?: number, radix?: number): string
954
- ```
955
-
956
- Generates a cryptographically strong random ID of a given length and [radix (base).](https://en.wikipedia.org/wiki/Radix)
957
- Uses the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) for generating the random numbers.
958
- ⚠️ This is not intended for generating encryption keys, only for generating IDs with a decent amount of entropy!
959
-
960
- The default length is 16 and the default radix is 16 (hexadecimal).
961
- You may change the radix to get digits from different numerical systems.
962
- Use 2 for binary, 8 for octal, 10 for decimal, 16 for hexadecimal and 36 for alphanumeric.
963
-
964
- <details><summary><b>Example - click to view</b></summary>
965
-
966
- ```ts
967
- import { randomId } from "@sv443-network/userutils";
968
-
969
- randomId(); // "1bda419a73629d4f" (length 16, radix 16)
970
- randomId(10); // "f86cd354a4" (length 10, radix 16)
971
- randomId(10, 2); // "1010001101" (length 10, radix 2)
972
- randomId(10, 10); // "0183428506" (length 10, radix 10)
973
- randomId(10, 36); // "z46jfpa37r" (length 10, radix 36)
974
- ```
975
-
976
- </details>
977
-
978
938
  <br><br>
979
939
 
980
940
  <!-- #SECTION Misc -->
@@ -986,12 +946,17 @@ Usage:
986
946
  new DataStore(options: DataStoreOptions)
987
947
  ```
988
948
 
989
- 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.
990
950
  Also supports automatic migration of outdated data formats via provided migration functions.
991
951
  You may create as many instances as you like as long as they have different IDs.
992
952
 
993
- ⚠️ 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.
994
- ⚠️ The directives `@grant GM.getValue` and `@grant GM.setValue` are required for this to work.
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"`
995
960
 
996
961
  The options object has the following properties:
997
962
  | Property | Description |
@@ -1000,8 +965,9 @@ The options object has the following properties:
1000
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. |
1001
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. |
1002
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. |
1003
- | `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 |
1004
- | `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) |
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) |
1005
971
 
1006
972
  <br>
1007
973
 
@@ -1022,10 +988,19 @@ Writes the given data synchronously to the internal cache and asynchronously to
1022
988
  Writes the default data given in `options.defaultData` synchronously to the internal cache and asynchronously to persistent storage.
1023
989
 
1024
990
  `deleteData(): Promise<void>`
1025
- Fully deletes the data from persistent storage.
991
+ Fully deletes the data from persistent storage only.
1026
992
  The internal cache will be left untouched, so any subsequent calls to `getData()` will return the data that was last loaded.
1027
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.
1028
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.
1029
1004
 
1030
1005
  <br>
1031
1006
 
@@ -1042,16 +1017,16 @@ interface MyConfig {
1042
1017
  qux: string;
1043
1018
  }
1044
1019
 
1045
- /** Default data */
1020
+ /** Default data returned by getData() calls until setData() is used and also fallback data if something goes wrong */
1046
1021
  const defaultData: MyConfig = {
1047
1022
  foo: "hello",
1048
1023
  bar: 42,
1049
1024
  baz: "xyz",
1050
1025
  qux: "something",
1051
1026
  };
1052
- /** 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 */
1053
1028
  const formatVersion = 2;
1054
- /** 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! */
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! */
1055
1030
  const migrations = {
1056
1031
  // migrate from format version 0 to 1
1057
1032
  1: (oldData: Record<string, unknown>) => {
@@ -1063,8 +1038,8 @@ const migrations = {
1063
1038
  },
1064
1039
  // asynchronously migrate from format version 1 to 2
1065
1040
  2: async (oldData: Record<string, unknown>) => {
1066
- // arbitrary async operation required for the new format
1067
- const qux = JSON.parse(await (await fetch("https://api.example.org/some-data")).text());
1041
+ // using arbitrary async operations for the new format:
1042
+ const qux = await grabQuxDataAsync();
1068
1043
  return {
1069
1044
  foo: oldData.foo,
1070
1045
  bar: oldData.bar,
@@ -1074,32 +1049,38 @@ const migrations = {
1074
1049
  },
1075
1050
  };
1076
1051
 
1077
- const manager = new DataStore({
1078
- /** A unique ID for this instance - choose wisely as changing it is not supported yet! */
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! */
1079
1055
  id: "my-userscript-config",
1080
- /** Default / fallback data */
1056
+ /** Default, initial and fallback data */
1081
1057
  defaultData,
1082
1058
  /** The current version of the data format */
1083
1059
  formatVersion,
1084
- /** Data format migration functions */
1060
+ /** Data format migration functions called when the formatVersion is increased */
1085
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",
1086
1067
 
1087
1068
  // Compression example:
1088
- // Adding this will save space at the cost of a little bit of performance while initially loading and saving the data
1089
- // Only both of these properties or none of them should be set
1090
- // Everything else will be handled by the DataStore instance
1091
-
1092
- /** Encodes data using the "deflate-raw" algorithm and digests it as a base64 string */
1093
- encodeData: (data) => compress(data, "deflate-raw", "base64"),
1094
- /** Decodes the "deflate-raw" encoded data as a base64 string */
1095
- decodeData: (data) => decompress(data, "deflate-raw", "base64"),
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"),
1096
1077
  });
1097
1078
 
1098
1079
  /** Entrypoint of the userscript */
1099
1080
  async function init() {
1100
1081
  // wait for the data to be loaded from persistent storage
1101
1082
  // if no data was saved in persistent storage before or getData() is called before loadData(), the value of options.defaultData will be returned
1102
- // if the previously saved data needs to be migrated to a newer version, it will happen in this function call
1083
+ // if the previously saved data needs to be migrated to a newer version, it will happen inside this function call
1103
1084
  const configData = await manager.loadData();
1104
1085
 
1105
1086
  console.log(configData.foo); // "hello"
@@ -1122,6 +1103,149 @@ init();
1122
1103
 
1123
1104
  </details>
1124
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
+
1125
1249
  <br><br>
1126
1250
 
1127
1251
  ### autoPlural()
@@ -1356,6 +1480,73 @@ console.log(decompressed); // "Hello, World!"
1356
1480
 
1357
1481
  </details>
1358
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
+
1359
1550
  <br><br>
1360
1551
 
1361
1552
  <!-- #SECTION Arrays -->
@@ -1642,8 +1833,9 @@ They don't alter the runtime behavior of the code, but they can be used to make
1642
1833
 
1643
1834
  ### Stringifiable
1644
1835
  This type describes any value that either is a string itself or can be converted to a string.
1645
- To be considered stringifiable, the object needs to have a `toString()` method that returns a string (all primitive types have this method).
1646
- 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.
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.
1647
1839
 
1648
1840
  <details><summary><b>Example - click to view</b></summary>
1649
1841
 
@@ -1651,7 +1843,7 @@ This method allows not just explicit conversion by calling it, but also implicit
1651
1843
  import type { Stringifiable } from "@sv443-network/userutils";
1652
1844
 
1653
1845
  function logSomething(value: Stringifiable) {
1654
- console.log(`Log: ${value}`); // implicit conversion using `value.toString()`
1846
+ console.log(`Log: ${value}`); // implicit conversion to a string
1655
1847
  }
1656
1848
 
1657
1849
  const fooObject = {
@@ -1665,11 +1857,10 @@ const barObject = {
1665
1857
  logSomething("foo"); // "Log: foo"
1666
1858
  logSomething(42); // "Log: 42"
1667
1859
  logSomething(true); // "Log: true"
1668
- logSomething({}); // "Log: [object Object]"
1669
1860
  logSomething(Symbol(1)); // "Log: Symbol(1)"
1670
1861
  logSomething(fooObject); // "Log: hello world"
1671
1862
 
1672
- logSomething(barObject); // Type Error
1863
+ logSomething(barObject); // Type error
1673
1864
  ```
1674
1865
 
1675
1866
  </details>