@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 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
- - [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"] }`
@@ -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<void>
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
- ⚠️ 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.
1004
- ⚠️ 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"`
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
- | `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 |
1014
- | `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) |
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
- /** 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! */
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 operation required for the new format
1077
- 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();
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
- const manager = new DataStore({
1088
- /** 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! */
1089
1055
  id: "my-userscript-config",
1090
- /** Default / fallback data */
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 this will save space at the cost of a little bit of performance while initially loading and saving the data
1099
- // Only both of these properties or none of them should be set
1100
- // Everything else will be handled by the DataStore instance
1101
-
1102
- /** Encodes data using the "deflate-raw" algorithm and digests it as a base64 string */
1103
- encodeData: (data) => compress(data, "deflate-raw", "base64"),
1104
- /** Decodes the "deflate-raw" encoded data as a base64 string */
1105
- 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"),
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 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
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 (all primitive types have this method).
1656
- 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.
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 using `value.toString()`
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 Error
1863
+ logSomething(barObject); // Type error
1683
1864
  ```
1684
1865
 
1685
1866
  </details>