@sv443-network/coreutils 1.0.0 → 2.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,13 +1,30 @@
1
1
  # @sv443-network/coreutils
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 273fa5a: Removed the boolean property `__ds-${id}-enc` from the `DataStore` and `DataStoreEngine` classes.
8
+ Now the key `__ds-${id}-enf` will hold the encoding format identifier string, or `null` if not set (will get created on the next write call).
9
+ This will make it possible to switch the encoding format without compatibility issues.
10
+ This functionality is not officially supported yet, but can be achieved manually by calling the storage API methods of `storeInstance.engine`
11
+
12
+ ### Minor Changes
13
+
14
+ - 29fb048: Added `overflowVal()` to conform a value to an over- & undeflowing range
15
+ - 91cbe9c: Added optional abstract method `DataStoreEngine.deleteStorage()` for deleting the data storage container itself. If implemented in subclasses, it will be called from the method `DataStore.deleteData()`
16
+ - 3cae1cb: Added `dataStoreOptions` constructor prop to the DataStoreEngine subclasses to enable them to be used standalone.
17
+ - f6465b5: Added method `NanoEmitter.onMulti()` to listen to when multiple events have emitted, with fine grained options
18
+
3
19
  ## 1.0.0
4
20
 
5
21
  This is the initial release of CoreUtils. Most features have been originally ported from [version 9.4.1 of `@sv443-network/userutils`](https://github.com/Sv443-Network/UserUtils/tree/v9.4.1)
6
22
  Parts of the code have been overhauled, and some features have been added:
23
+
7
24
  - Additions:
8
25
  - Added [`capitalize()`](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#function-capitalize) to capitalize the first letter of a string.
9
- - Added [`setImmediateInterval()`](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#function-setimmediateinterval) to set an interval that runs immediately, then again on a fixed *interval.*
10
- - Added [`setImmediateTimeoutLoop()`](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#function-setimmediatetimeoutloop) to set a recursive `setTimeout()` loop with a fixed *delay.*
26
+ - Added [`setImmediateInterval()`](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#function-setimmediateinterval) to set an interval that runs immediately, then again on a fixed _interval._
27
+ - Added [`setImmediateTimeoutLoop()`](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#function-setimmediatetimeoutloop) to set a recursive `setTimeout()` loop with a fixed _delay._
11
28
  - Added [`takeRandomItemIndex()`](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#function-takerandomitemindex), as a mutating counterpart to [`randomItemIndex()`](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#function-randomitemindex)
12
29
  - Added [`truncStr()`](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#function-truncstr) to truncate a string to a given length, optionally adding an ellipsis.
13
30
  - Added [`valsWithin()`](https://github.com/Sv443-Network/CoreUtils/blob/main/docs.md#function-valswithin) to check if two values, rounded at the given decimal, are within a certain range of each other.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  Cross-platform, general-purpose, JavaScript core library for Node, Deno and the browser.
5
5
  Intended to be used in conjunction with [`@sv443-network/userutils`](https://github.com/Sv443-Network/UserUtils) and [`@sv443-network/djsutils`](https://github.com/Sv443-Network/DJSUtils), but can be used independently as well.
6
6
 
7
- ### [Documentation](./docs.md#readme) • [Features](#features) • [Installation](#installation) • [License](#license) • [Changelog](./CHANGELOG.md)
7
+ ### [Documentation](./docs.md#readme) • [Features](#features) • [Installation](#installation) • [License](./LICENSE.txt) • [Changelog](./CHANGELOG.md)
8
8
 
9
9
  </div>
10
10
  <br>
@@ -34,12 +34,14 @@ Intended to be used in conjunction with [`@sv443-network/userutils`](https://git
34
34
  - 🟧 [`class DataStore`](./docs.md#class-datastore) - The main class for the data store
35
35
  - 🔷 [`type DataStoreOptions`](./docs.md#type-datastoreoptions) - Options for the data store
36
36
  - 🔷 [`type DataMigrationsDict`](./docs.md#type-datamigrationsdict) - Dictionary of data migration functions
37
+ - 🔷 [`type DataStoreData`](./docs.md#type-datastoredata) - The type of the serializable data
37
38
  - 🟧 [`class DataStoreSerializer`](./docs.md#class-datastoreserializer) - Serializes and deserializes data for multiple DataStore instances
38
39
  - 🔷 [`type DataStoreSerializerOptions`](./docs.md#type-datastoreserializeroptions) - Options for the DataStoreSerializer
39
40
  - 🔷 [`type LoadStoresDataResult`](./docs.md#type-loadstoresdataresult) - Result of calling [`loadStoresData()`](./docs.md#datastoreserializer-loadstoresdata)
40
41
  - 🔷 [`type SerializedDataStore`](./docs.md#type-serializeddatastore) - Meta object and serialized data of a DataStore instance
41
42
  - 🔷 [`type StoreFilter`](./docs.md#type-storefilter) - Filter for selecting data stores
42
43
  - 🟧 [`class DataStoreEngine`](./docs.md#class-datastoreengine) - Base class for DataStore storage engines, which handle the data storage
44
+ - 🔷 [`type DataStoreEngineDSOptions`](./docs.md#type-datastoreenginedsoptions) - Reduced version of [`DataStoreOptions`](./docs.md#type-datastoreoptions)
43
45
  - [Storage Engines:](./docs.md#storage-engines)
44
46
  - 🟧 [`class BrowserStorageEngine`](./docs.md#class-browserstorageengine) - Storage engine for browser environments (localStorage, sessionStorage)
45
47
  - 🔷 [`type BrowserStorageEngineOptions`](./docs.md#browserstorageengineoptions) - Options for the browser storage engine
@@ -63,6 +65,7 @@ Intended to be used in conjunction with [`@sv443-network/userutils`](https://git
63
65
  - 🟣 [`function formatNumber()`](./docs.md#function-formatnumber) - Formats a number to a string using the given locale and format identifier
64
66
  - 🔷 [`type NumberFormat`](./docs.md#type-numberformat) - Number format identifier
65
67
  - 🟣 [`function mapRange()`](./docs.md#function-maprange) - Maps a number from one range to another
68
+ - 🟣 [`function overflowVal()`](./docs.md#function-overflowVal) - Makes sure a number is in a range by over- & underflowing it
66
69
  - 🟣 [`function randRange()`](./docs.md#function-randrange) - Returns a random number in the given range
67
70
  - 🟣 [`function roundFixed()`](./docs.md#function-roundfixed) - Rounds the given number to the given number of decimal places
68
71
  - 🟣 [`function valsWithin()`](./docs.md#function-valswithin) - Checks if the given numbers are within a certain range of each other
@@ -90,7 +93,7 @@ Intended to be used in conjunction with [`@sv443-network/userutils`](https://git
90
93
  - 🟩 [`const defaultPbChars`](./docs.md#const-defaultpbchars) - Default characters for the progress bar
91
94
  - 🔷 [`type ProgressBarChars`](./docs.md#type-progressbarchars) - Type for the progress bar characters object
92
95
  - 🟣 [`function joinArrayReadable()`](./docs.md#function-joinarrayreadable) - Joins the given array into a string, using the given separators and last separator
93
- - 🟣 [`function secsToTimeStr()`](./docs.md#function-sectostimestr) - Turns the given number of seconds into a string in the format `(hh:)mm:ss` with intelligent zero-padding
96
+ - 🟣 [`function secsToTimeStr()`](./docs.md#function-secstotimestr) - Turns the given number of seconds into a string in the format `(hh:)mm:ss` with intelligent zero-padding
94
97
  - 🟣 [`function truncStr()`](./docs.md#function-truncstr) - Truncates the given string to the given length
95
98
  <!-- - *[**TieredCache:**](./docs.md#tieredcache)
96
99
  - 🟧 *[`class TieredCache`](./docs.md#class-tieredcache) - A multi-tier cache that uses multiple storage engines with different expiration times
@@ -122,7 +125,7 @@ Intended to be used in conjunction with [`@sv443-network/userutils`](https://git
122
125
  > 🟣 = function
123
126
  > 🟧 = class
124
127
  > 🔷 = type
125
- > 🔶 = const
128
+ > 🟩 = const
126
129
 
127
130
  <br>
128
131
 
@@ -135,11 +138,11 @@ yarn add @sv443-network/coreutils
135
138
  npx jsr install @sv443-network/coreutils
136
139
  deno add jsr:@sv443-network/coreutils
137
140
  ```
138
- - If you are in a DOM environment, you can include the UMD bundle using your favorite CDN:
141
+ - If you are in a DOM environment, you can include the UMD bundle using your favorite CDN (after inserting the version number):
139
142
  ```html
140
- <script src="https://cdn.jsdelivr.net/npm/@sv443-network/coreutils@latest/dist/CoreUtils.min.umd.js"></script>
141
- <script src="https://unpkg.com/@sv443-network/coreutils@latest/dist/CoreUtils.min.umd.js"></script>
142
- <script src="https://esm.sh/@sv443-network/coreutils@latest/dist/CoreUtils.min.umd.js"></script>
143
+ <script src="https://cdn.jsdelivr.net/npm/@sv443-network/coreutils@INSERT_VERSION_HERE/dist/CoreUtils.min.umd.js"></script>
144
+ <script src="https://unpkg.com/@sv443-network/coreutils@INSERT_VERSION_HERE/dist/CoreUtils.min.umd.js"></script>
145
+ <script src="https://esm.sh/@sv443-network/coreutils@INSERT_VERSION_HERE/dist/CoreUtils.min.umd.js"></script>
143
146
  ```
144
147
  - Then, import parts of the library as needed:
145
148
  ```ts
@@ -164,5 +167,18 @@ const CoreUtils = require("@sv443-network/coreutils");
164
167
  // - to make the global variable `CoreUtils` available, import this file:
165
168
  // "@sv443-network/coreutils/dist/CoreUtils.min.umd.js"
166
169
  // - or import the library on your HTML page:
167
- // <script src="https://cdn.jsdelivr.net/npm/@sv443-network/coreutils@latest/dist/CoreUtils.min.umd.js"></script>
170
+ // <script src="https://cdn.jsdelivr.net/npm/@sv443-network/coreutils@INSERT_VERSION_HERE/dist/CoreUtils.min.umd.js"></script>
171
+ // (make sure to insert the version number above. Use 'latest' (not recommended) or a specific version, e.g. '9.4.3')
168
172
  ```
173
+
174
+ <br><br>
175
+
176
+ <div align="center" style="text-align: center;">
177
+
178
+ Made with ❤️ by [Sv443](https://github.com/Sv443)
179
+ If you like this userscript, please consider [supporting the development](https://github.com/sponsors/Sv443)
180
+
181
+ © 2025 Sv443 & Sv443 Network
182
+ [MIT license](./LICENSE.txt)
183
+
184
+ </div>
@@ -65,6 +65,7 @@ __export(lib_exports, {
65
65
  joinArrayReadable: () => joinArrayReadable,
66
66
  lightenColor: () => lightenColor,
67
67
  mapRange: () => mapRange,
68
+ overflowVal: () => overflowVal,
68
69
  pauseFor: () => pauseFor,
69
70
  pureObj: () => pureObj,
70
71
  randRange: () => randRange,
@@ -128,6 +129,19 @@ function mapRange(value, range1min, range1max, range2min, range2max) {
128
129
  return value * (range2max / range1max);
129
130
  return (value - range1min) * ((range2max - range2min) / (range1max - range1min)) + range2min;
130
131
  }
132
+ function overflowVal(value, minOrMax, max) {
133
+ const min = typeof max === "number" ? minOrMax : 0;
134
+ max = typeof max === "number" ? max : minOrMax;
135
+ if (min > max)
136
+ throw new RangeError(`Parameter "min" can't be bigger than "max"`);
137
+ if (isNaN(value) || isNaN(min) || isNaN(max) || !isFinite(value) || !isFinite(min) || !isFinite(max))
138
+ return NaN;
139
+ if (value >= min && value <= max)
140
+ return value;
141
+ const range = max - min + 1;
142
+ const wrappedValue = ((value - min) % range + range) % range + min;
143
+ return wrappedValue;
144
+ }
131
145
  function randRange(...args) {
132
146
  let min, max, enhancedEntropy = false;
133
147
  if (typeof args[0] === "number" && typeof args[1] === "number")
@@ -223,10 +237,8 @@ function darkenColor(color, percent, upperCase = false) {
223
237
  return rgbToHex(r, g, b, a, color.startsWith("#"), upperCase);
224
238
  else if (color.startsWith("rgba"))
225
239
  return `rgba(${r}, ${g}, ${b}, ${a ?? NaN})`;
226
- else if (color.startsWith("rgb"))
227
- return `rgb(${r}, ${g}, ${b})`;
228
240
  else
229
- throw new TypeError("Unsupported color format");
241
+ return `rgb(${r}, ${g}, ${b})`;
230
242
  }
231
243
  function hexToRgb(hex) {
232
244
  hex = (hex.startsWith("#") ? hex.slice(1) : hex).trim();
@@ -259,22 +271,22 @@ function atoab(str) {
259
271
  return Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
260
272
  }
261
273
  async function compress(input, compressionFormat, outputType = "string") {
262
- const byteArray = input instanceof ArrayBuffer ? input : new TextEncoder().encode((input == null ? void 0 : input.toString()) ?? String(input));
274
+ const byteArray = input instanceof Uint8Array ? input : new TextEncoder().encode((input == null ? void 0 : input.toString()) ?? String(input));
263
275
  const comp = new CompressionStream(compressionFormat);
264
276
  const writer = comp.writable.getWriter();
265
277
  writer.write(byteArray);
266
278
  writer.close();
267
- const buf = await new Response(comp.readable).arrayBuffer();
268
- return outputType === "arrayBuffer" ? buf : abtoa(buf);
279
+ const uintArr = new Uint8Array(await new Response(comp.readable).arrayBuffer());
280
+ return outputType === "arrayBuffer" ? uintArr : abtoa(uintArr);
269
281
  }
270
282
  async function decompress(input, compressionFormat, outputType = "string") {
271
- const byteArray = input instanceof ArrayBuffer ? input : atoab((input == null ? void 0 : input.toString()) ?? String(input));
283
+ const byteArray = input instanceof Uint8Array ? input : atoab((input == null ? void 0 : input.toString()) ?? String(input));
272
284
  const decomp = new DecompressionStream(compressionFormat);
273
285
  const writer = decomp.writable.getWriter();
274
286
  writer.write(byteArray);
275
287
  writer.close();
276
- const buf = await new Response(decomp.readable).arrayBuffer();
277
- return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
288
+ const uintArr = new Uint8Array(await new Response(decomp.readable).arrayBuffer());
289
+ return outputType === "arrayBuffer" ? uintArr : new TextDecoder().decode(uintArr);
278
290
  }
279
291
  async function computeHash(input, algorithm = "SHA-256") {
280
292
  let data;
@@ -520,7 +532,14 @@ var DataStore = class {
520
532
  decodeData;
521
533
  compressionFormat = "deflate-raw";
522
534
  engine;
535
+ options;
536
+ /**
537
+ * Whether all first-init checks should be done.
538
+ * This includes migrating the internal DataStore format, migrating data from the UserUtils format, and anything similar.
539
+ * This is set to `true` by default. Create a subclass and set it to `false` before calling {@linkcode loadData()} if you want to explicitly skip these checks.
540
+ */
523
541
  firstInit = true;
542
+ /** In-memory cached copy of the data that is saved in persistent storage used for synchronous read access. */
524
543
  cachedData;
525
544
  migrations;
526
545
  migrateIds = [];
@@ -528,13 +547,13 @@ var DataStore = class {
528
547
  * Creates an instance of DataStore to manage a sync & async database that is cached in memory and persistently saved across sessions.
529
548
  * Supports migrating data from older versions to newer ones and populating the cache with default data if no persistent data is found.
530
549
  *
531
- * - ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue` if the storageMethod is left as the default of `"GM"`
532
550
  * - ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
533
551
  *
534
552
  * @template TData The type of the data that is saved in persistent storage for the currently set format version (will be automatically inferred from `defaultData` if not provided) - **This has to be a JSON-compatible object!** (no undefined, circular references, etc.)
535
553
  * @param opts The options for this DataStore instance
536
554
  */
537
555
  constructor(opts) {
556
+ var _a;
538
557
  this.id = opts.id;
539
558
  this.formatVersion = opts.formatVersion;
540
559
  this.defaultData = opts.defaultData;
@@ -545,8 +564,9 @@ var DataStore = class {
545
564
  this.encodeData = opts.encodeData;
546
565
  this.decodeData = opts.decodeData;
547
566
  this.engine = typeof opts.engine === "function" ? opts.engine() : opts.engine;
567
+ this.options = opts;
548
568
  if (typeof opts.compressionFormat === "undefined")
549
- opts.compressionFormat = "deflate-raw";
569
+ opts.compressionFormat = ((_a = opts.encodeData) == null ? void 0 : _a[0]) ?? "deflate-raw";
550
570
  if (typeof opts.compressionFormat === "string") {
551
571
  this.encodeData = [opts.compressionFormat, async (data) => await compress(data, opts.compressionFormat, "string")];
552
572
  this.decodeData = [opts.compressionFormat, async (data) => await compress(data, opts.compressionFormat, "string")];
@@ -581,9 +601,14 @@ var DataStore = class {
581
601
  promises.push(this.engine.setValue(newKey, value));
582
602
  promises.push(this.engine.deleteValue(oldKey));
583
603
  };
584
- oldData && migrateFmt(`_uucfg-${this.id}`, `__ds-${this.id}-dat`, oldData);
585
- !isNaN(oldVer) && migrateFmt(`_uucfgver-${this.id}`, `__ds-${this.id}-ver`, oldVer);
586
- typeof oldEnc === "boolean" && migrateFmt(`_uucfgenc-${this.id}`, `__ds-${this.id}-enc`, oldEnc === true ? this.compressionFormat : null);
604
+ if (oldData)
605
+ migrateFmt(`_uucfg-${this.id}`, `__ds-${this.id}-dat`, oldData);
606
+ if (!isNaN(oldVer))
607
+ migrateFmt(`_uucfgver-${this.id}`, `__ds-${this.id}-ver`, oldVer);
608
+ if (typeof oldEnc === "boolean")
609
+ migrateFmt(`_uucfgenc-${this.id}`, `__ds-${this.id}-enf`, oldEnc === true ? Boolean(this.compressionFormat) || null : null);
610
+ else
611
+ promises.push(this.engine.setValue(`__ds-${this.id}-enf`, this.compressionFormat));
587
612
  await Promise.allSettled(promises);
588
613
  }
589
614
  await this.engine.setValue("__ds_fmt_ver", dsFmtVer);
@@ -593,21 +618,22 @@ var DataStore = class {
593
618
  await this.migrateId(this.migrateIds);
594
619
  this.migrateIds = [];
595
620
  }
596
- const gmData = await this.engine.getValue(`__ds-${this.id}-dat`, JSON.stringify(this.defaultData));
597
- let gmFmtVer = Number(await this.engine.getValue(`__ds-${this.id}-ver`, NaN));
598
- if (typeof gmData !== "string") {
621
+ const storedData = await this.engine.getValue(`__ds-${this.id}-dat`, JSON.stringify(this.defaultData));
622
+ let storedFmtVer = Number(await this.engine.getValue(`__ds-${this.id}-ver`, NaN));
623
+ if (typeof storedData !== "string") {
599
624
  await this.saveDefaultData();
600
625
  return { ...this.defaultData };
601
626
  }
602
- const isEncoded = Boolean(await this.engine.getValue(`__ds-${this.id}-enc`, false));
627
+ const encodingFmt = String(await this.engine.getValue(`__ds-${this.id}-enf`, null));
628
+ const isEncoded = encodingFmt !== "null" && encodingFmt !== "false";
603
629
  let saveData = false;
604
- if (isNaN(gmFmtVer)) {
605
- await this.engine.setValue(`__ds-${this.id}-ver`, gmFmtVer = this.formatVersion);
630
+ if (isNaN(storedFmtVer)) {
631
+ await this.engine.setValue(`__ds-${this.id}-ver`, storedFmtVer = this.formatVersion);
606
632
  saveData = true;
607
633
  }
608
- let parsed = await this.engine.deserializeData(gmData, isEncoded);
609
- if (gmFmtVer < this.formatVersion && this.migrations)
610
- parsed = await this.runMigrations(parsed, gmFmtVer);
634
+ let parsed = await this.engine.deserializeData(storedData, isEncoded);
635
+ if (storedFmtVer < this.formatVersion && this.migrations)
636
+ parsed = await this.runMigrations(parsed, storedFmtVer);
611
637
  if (saveData)
612
638
  await this.setData(parsed);
613
639
  return this.cachedData = this.engine.deepCopy(parsed);
@@ -627,12 +653,11 @@ var DataStore = class {
627
653
  /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
628
654
  setData(data) {
629
655
  this.cachedData = data;
630
- const useEncoding = this.encodingEnabled();
631
656
  return new Promise(async (resolve) => {
632
657
  await Promise.allSettled([
633
- this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(data, useEncoding)),
658
+ this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(data, this.encodingEnabled())),
634
659
  this.engine.setValue(`__ds-${this.id}-ver`, this.formatVersion),
635
- this.engine.setValue(`__ds-${this.id}-enc`, useEncoding)
660
+ this.engine.setValue(`__ds-${this.id}-enf`, this.compressionFormat)
636
661
  ]);
637
662
  resolve();
638
663
  });
@@ -640,30 +665,29 @@ var DataStore = class {
640
665
  /** Saves the default data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
641
666
  async saveDefaultData() {
642
667
  this.cachedData = this.defaultData;
643
- const useEncoding = this.encodingEnabled();
644
668
  await Promise.allSettled([
645
- this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(this.defaultData, useEncoding)),
669
+ this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(this.defaultData, this.encodingEnabled())),
646
670
  this.engine.setValue(`__ds-${this.id}-ver`, this.formatVersion),
647
- this.engine.setValue(`__ds-${this.id}-enc`, useEncoding)
671
+ this.engine.setValue(`__ds-${this.id}-enf`, this.compressionFormat)
648
672
  ]);
649
673
  }
650
674
  /**
651
- * Call this method to clear all persistently stored data associated with this DataStore instance.
675
+ * Call this method to clear all persistently stored data associated with this DataStore instance, including the storage container (if supported by the DataStoreEngine).
652
676
  * The in-memory cache will be left untouched, so you may still access the data with {@linkcode getData()}
653
- * Calling {@linkcode loadData()} or {@linkcode setData()} after this method was called will recreate persistent storage with the cached or default data.
654
- *
655
- * - ⚠️ This requires the additional directive `@grant GM.deleteValue` if the storageMethod is left as the default of `"GM"`
677
+ * Calling {@linkcode loadData()} or {@linkcode setData()} after this method was called will recreate persistent storage with the cached or default data.
656
678
  */
657
679
  async deleteData() {
680
+ var _a, _b;
658
681
  await Promise.allSettled([
659
682
  this.engine.deleteValue(`__ds-${this.id}-dat`),
660
683
  this.engine.deleteValue(`__ds-${this.id}-ver`),
661
- this.engine.deleteValue(`__ds-${this.id}-enc`)
684
+ this.engine.deleteValue(`__ds-${this.id}-enf`)
662
685
  ]);
686
+ await ((_b = (_a = this.engine).deleteStorage) == null ? void 0 : _b.call(_a));
663
687
  }
664
688
  /** Returns whether encoding and decoding are enabled for this DataStore instance */
665
689
  encodingEnabled() {
666
- return Boolean(this.encodeData && this.decodeData);
690
+ return Boolean(this.encodeData && this.decodeData) && this.compressionFormat !== null || Boolean(this.compressionFormat);
667
691
  }
668
692
  //#region migrations
669
693
  /**
@@ -697,7 +721,7 @@ var DataStore = class {
697
721
  await Promise.allSettled([
698
722
  this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(newData)),
699
723
  this.engine.setValue(`__ds-${this.id}-ver`, lastFmtVer),
700
- this.engine.setValue(`__ds-${this.id}-enc`, this.encodingEnabled())
724
+ this.engine.setValue(`__ds-${this.id}-enf`, this.compressionFormat)
701
725
  ]);
702
726
  return this.cachedData = { ...newData };
703
727
  }
@@ -708,19 +732,24 @@ var DataStore = class {
708
732
  async migrateId(oldIds) {
709
733
  const ids = Array.isArray(oldIds) ? oldIds : [oldIds];
710
734
  await Promise.all(ids.map(async (id) => {
711
- const data = await this.engine.getValue(`__ds-${id}-dat`, JSON.stringify(this.defaultData));
712
- const fmtVer = Number(await this.engine.getValue(`__ds-${id}-ver`, NaN));
713
- const isEncoded = Boolean(await this.engine.getValue(`__ds-${id}-enc`, false));
735
+ const [data, fmtVer, isEncoded] = await (async () => {
736
+ const [d, f, e] = await Promise.all([
737
+ this.engine.getValue(`__ds-${id}-dat`, JSON.stringify(this.defaultData)),
738
+ this.engine.getValue(`__ds-${id}-ver`, NaN),
739
+ this.engine.getValue(`__ds-${id}-enf`, null)
740
+ ]);
741
+ return [d, Number(f), Boolean(e) && String(e) !== "null"];
742
+ })();
714
743
  if (data === void 0 || isNaN(fmtVer))
715
744
  return;
716
745
  const parsed = await this.engine.deserializeData(data, isEncoded);
717
746
  await Promise.allSettled([
718
747
  this.engine.setValue(`__ds-${this.id}-dat`, await this.engine.serializeData(parsed)),
719
748
  this.engine.setValue(`__ds-${this.id}-ver`, fmtVer),
720
- this.engine.setValue(`__ds-${this.id}-enc`, isEncoded),
749
+ this.engine.setValue(`__ds-${this.id}-enf`, this.compressionFormat),
721
750
  this.engine.deleteValue(`__ds-${id}-dat`),
722
751
  this.engine.deleteValue(`__ds-${id}-ver`),
723
- this.engine.deleteValue(`__ds-${id}-enc`)
752
+ this.engine.deleteValue(`__ds-${id}-enf`)
724
753
  ]);
725
754
  }));
726
755
  }
@@ -730,7 +759,11 @@ var DataStore = class {
730
759
  var DataStoreEngine = class {
731
760
  dataStoreOptions;
732
761
  // setDataStoreOptions() is called from inside the DataStore constructor to set this value
733
- /** Called by DataStore on creation, to pass its options */
762
+ constructor(options) {
763
+ if (options)
764
+ this.dataStoreOptions = options;
765
+ }
766
+ /** Called by DataStore on creation, to pass its options. Only call this if you are using this instance standalone! */
734
767
  setDataStoreOptions(dataStoreOptions) {
735
768
  this.dataStoreOptions = dataStoreOptions;
736
769
  }
@@ -738,6 +771,7 @@ var DataStoreEngine = class {
738
771
  /** Serializes the given object to a string, optionally encoded with `options.encodeData` if {@linkcode useEncoding} is set to true */
739
772
  async serializeData(data, useEncoding) {
740
773
  var _a, _b, _c, _d, _e;
774
+ this.ensureDataStoreOptions();
741
775
  const stringData = JSON.stringify(data);
742
776
  if (!useEncoding || !((_a = this.dataStoreOptions) == null ? void 0 : _a.encodeData) || !((_b = this.dataStoreOptions) == null ? void 0 : _b.decodeData))
743
777
  return stringData;
@@ -749,12 +783,20 @@ var DataStoreEngine = class {
749
783
  /** Deserializes the given string to a JSON object, optionally decoded with `options.decodeData` if {@linkcode useEncoding} is set to true */
750
784
  async deserializeData(data, useEncoding) {
751
785
  var _a, _b, _c;
786
+ this.ensureDataStoreOptions();
752
787
  let decRes = ((_a = this.dataStoreOptions) == null ? void 0 : _a.decodeData) && useEncoding ? (_c = (_b = this.dataStoreOptions.decodeData) == null ? void 0 : _b[1]) == null ? void 0 : _c.call(_b, data) : void 0;
753
788
  if (decRes instanceof Promise)
754
789
  decRes = await decRes;
755
790
  return JSON.parse(decRes ?? data);
756
791
  }
757
792
  //#region misc api
793
+ /** Throws an error if the DataStoreOptions are not set or invalid */
794
+ ensureDataStoreOptions() {
795
+ if (!this.dataStoreOptions)
796
+ throw new DatedError("DataStoreEngine must be initialized with DataStore options before use. If you are using this instance standalone, set them in the constructor or call `setDataStoreOptions()` with the DataStore options.");
797
+ if (!this.dataStoreOptions.id)
798
+ throw new DatedError("DataStoreEngine must be initialized with a valid DataStore ID");
799
+ }
758
800
  /**
759
801
  * Copies a JSON-compatible object and loses all its internal references in the process.
760
802
  * Uses [`structuredClone()`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) if available, otherwise falls back to `JSON.parse(JSON.stringify(obj))`.
@@ -773,11 +815,11 @@ var BrowserStorageEngine = class extends DataStoreEngine {
773
815
  /**
774
816
  * Creates an instance of `BrowserStorageEngine`.
775
817
  *
776
- * ⚠️ Requires a DOM environment
777
- * ⚠️ Don't reuse this engine across multiple {@linkcode DataStore} instances
818
+ * - ⚠️ Requires a DOM environment
819
+ * - ⚠️ Don't reuse engines across multiple {@linkcode DataStore} instances
778
820
  */
779
821
  constructor(options) {
780
- super();
822
+ super(options == null ? void 0 : options.dataStoreOptions);
781
823
  this.options = {
782
824
  type: "localStorage",
783
825
  ...options
@@ -809,11 +851,11 @@ var FileStorageEngine = class extends DataStoreEngine {
809
851
  /**
810
852
  * Creates an instance of `FileStorageEngine`.
811
853
  *
812
- * ⚠️ Requires Node.js or Deno with Node compatibility (v1.31+)
813
- * ⚠️ Don't reuse this engine across multiple {@linkcode DataStore} instances
854
+ * - ⚠️ Requires Node.js or Deno with Node compatibility (v1.31+)
855
+ * - ⚠️ Don't reuse engines across multiple {@linkcode DataStore} instances
814
856
  */
815
857
  constructor(options) {
816
- super();
858
+ super(options == null ? void 0 : options.dataStoreOptions);
817
859
  this.options = {
818
860
  filePath: (id) => `.ds-${id}`,
819
861
  ...options
@@ -822,30 +864,32 @@ var FileStorageEngine = class extends DataStoreEngine {
822
864
  //#region json file
823
865
  /** Reads the file contents */
824
866
  async readFile() {
825
- var _a, _b, _c;
867
+ var _a, _b, _c, _d;
868
+ this.ensureDataStoreOptions();
826
869
  try {
827
870
  if (!fs)
828
- fs = (await import("fs/promises")).default;
871
+ fs = (_a = await import("fs/promises")) == null ? void 0 : _a.default;
829
872
  if (!fs)
830
873
  throw new DatedError("FileStorageEngine requires Node.js or Deno with Node compatibility (v1.31+)", { cause: new Error("'node:fs/promises' module not available") });
831
874
  const path = typeof this.options.filePath === "string" ? this.options.filePath : this.options.filePath(this.dataStoreOptions.id);
832
875
  const data = await fs.readFile(path, "utf-8");
833
- return data ? JSON.parse(await ((_c = (_b = (_a = this.dataStoreOptions) == null ? void 0 : _a.decodeData) == null ? void 0 : _b[1]) == null ? void 0 : _c.call(_b, data)) ?? data) : void 0;
876
+ return data ? JSON.parse(await ((_d = (_c = (_b = this.dataStoreOptions) == null ? void 0 : _b.decodeData) == null ? void 0 : _c[1]) == null ? void 0 : _d.call(_c, data)) ?? data) : void 0;
834
877
  } catch {
835
878
  return void 0;
836
879
  }
837
880
  }
838
881
  /** Overwrites the file contents */
839
882
  async writeFile(data) {
840
- var _a, _b, _c;
883
+ var _a, _b, _c, _d;
884
+ this.ensureDataStoreOptions();
841
885
  try {
842
886
  if (!fs)
843
- fs = (await import("fs/promises")).default;
887
+ fs = (_a = await import("fs/promises")) == null ? void 0 : _a.default;
844
888
  if (!fs)
845
889
  throw new DatedError("FileStorageEngine requires Node.js or Deno with Node compatibility (v1.31+)", { cause: new Error("'node:fs/promises' module not available") });
846
890
  const path = typeof this.options.filePath === "string" ? this.options.filePath : this.options.filePath(this.dataStoreOptions.id);
847
891
  await fs.mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true });
848
- await fs.writeFile(path, await ((_c = (_b = (_a = this.dataStoreOptions) == null ? void 0 : _a.encodeData) == null ? void 0 : _b[1]) == null ? void 0 : _c.call(_b, JSON.stringify(data))) ?? JSON.stringify(data, void 0, 2), "utf-8");
892
+ await fs.writeFile(path, await ((_d = (_c = (_b = this.dataStoreOptions) == null ? void 0 : _b.encodeData) == null ? void 0 : _c[1]) == null ? void 0 : _d.call(_c, JSON.stringify(data))) ?? JSON.stringify(data, void 0, 2), "utf-8");
849
893
  } catch (err) {
850
894
  console.error("Error writing file:", err);
851
895
  }
@@ -879,6 +923,21 @@ var FileStorageEngine = class extends DataStoreEngine {
879
923
  delete data[name];
880
924
  await this.writeFile(data);
881
925
  }
926
+ /** Deletes the file that contains the data of this DataStore. */
927
+ async deleteStorage() {
928
+ var _a;
929
+ this.ensureDataStoreOptions();
930
+ try {
931
+ if (!fs)
932
+ fs = (_a = await import("fs/promises")) == null ? void 0 : _a.default;
933
+ if (!fs)
934
+ throw new DatedError("FileStorageEngine requires Node.js or Deno with Node compatibility (v1.31+)", { cause: new Error("'node:fs/promises' module not available") });
935
+ const path = typeof this.options.filePath === "string" ? this.options.filePath : this.options.filePath(this.dataStoreOptions.id);
936
+ await fs.unlink(path);
937
+ } catch (err) {
938
+ console.error("Error deleting file:", err);
939
+ }
940
+ }
882
941
  };
883
942
 
884
943
  // lib/DataStoreSerializer.ts
@@ -906,14 +965,16 @@ var DataStoreSerializer = class _DataStoreSerializer {
906
965
  * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
907
966
  */
908
967
  async serializePartial(stores, useEncoding = true, stringified = true) {
968
+ var _a;
909
969
  const serData = [];
910
970
  for (const storeInst of this.stores.filter((s) => typeof stores === "function" ? stores(s.id) : stores.includes(s.id))) {
911
- const data = useEncoding && storeInst.encodingEnabled() ? await storeInst.encodeData[1](JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
971
+ const encoded = Boolean(useEncoding && storeInst.encodingEnabled() && ((_a = storeInst.encodeData) == null ? void 0 : _a[1]));
972
+ const data = encoded ? await storeInst.encodeData[1](JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
912
973
  serData.push({
913
974
  id: storeInst.id,
914
975
  data,
915
976
  formatVersion: storeInst.formatVersion,
916
- encoded: useEncoding && storeInst.encodingEnabled(),
977
+ encoded,
917
978
  checksum: this.options.addChecksum ? await this.calcChecksum(data) : void 0
918
979
  });
919
980
  }
@@ -1037,6 +1098,7 @@ var NanoEmitter = class {
1037
1098
  ...options
1038
1099
  };
1039
1100
  }
1101
+ //#region on
1040
1102
  /**
1041
1103
  * Subscribes to an event and calls the callback when it's emitted.
1042
1104
  * @param event The event to subscribe to. Use `as "_"` in case your event names aren't thoroughly typed (like when using a template literal, e.g. \`event-${val}\` as "_")
@@ -1070,6 +1132,7 @@ var NanoEmitter = class {
1070
1132
  this.eventUnsubscribes.push(unsub);
1071
1133
  return unsubProxy;
1072
1134
  }
1135
+ //#region once
1073
1136
  /**
1074
1137
  * Subscribes to an event and calls the callback or resolves the Promise only once when it's emitted.
1075
1138
  * @param event The event to subscribe to. Use `as "_"` in case your event names aren't thoroughly typed (like when using a template literal, e.g. \`event-${val}\` as "_")
@@ -1100,9 +1163,90 @@ var NanoEmitter = class {
1100
1163
  this.eventUnsubscribes.push(unsub);
1101
1164
  });
1102
1165
  }
1166
+ //#region onMulti
1167
+ /**
1168
+ * Allows subscribing to multiple events and calling the callback only when one of, all of, or a subset of the events are emitted, either continuously or only once.
1169
+ * @param options An object or array of objects with the following properties:
1170
+ * `callback` (required) is the function that will be called when the conditions are met.
1171
+ *
1172
+ * Set `once` to true to call the callback only once for the first event (or set of events) that match the criteria, then stop listening.
1173
+ * If `signal` is provided, the subscription will be aborted when the given signal is aborted.
1174
+ *
1175
+ * If `oneOf` is used, the callback will be called when any of the matching events are emitted.
1176
+ * If `allOf` is used, the callback will be called after all of the matching events are emitted at least once, then any time any of them are emitted.
1177
+ * You may use a combination of the above two options, but at least one of them must be provided.
1178
+ *
1179
+ * @returns Returns a function that can be called to unsubscribe all listeners created by this call. Alternatively, pass an `AbortSignal` to all options objects to achieve the same effect or for finer control.
1180
+ */
1181
+ onMulti(options) {
1182
+ const allUnsubs = [];
1183
+ const unsubAll = () => {
1184
+ for (const unsub of allUnsubs)
1185
+ unsub();
1186
+ allUnsubs.splice(0, allUnsubs.length);
1187
+ this.eventUnsubscribes = this.eventUnsubscribes.filter((u) => !allUnsubs.includes(u));
1188
+ };
1189
+ for (const opts of Array.isArray(options) ? options : [options]) {
1190
+ const optsWithDefaults = {
1191
+ allOf: [],
1192
+ oneOf: [],
1193
+ once: false,
1194
+ ...opts
1195
+ };
1196
+ const {
1197
+ oneOf,
1198
+ allOf,
1199
+ once,
1200
+ signal,
1201
+ callback
1202
+ } = optsWithDefaults;
1203
+ if (signal == null ? void 0 : signal.aborted)
1204
+ return unsubAll;
1205
+ const curEvtUnsubs = [];
1206
+ const checkUnsubAllEvt = (force = false) => {
1207
+ if (!(signal == null ? void 0 : signal.aborted) && !force)
1208
+ return;
1209
+ for (const unsub of curEvtUnsubs)
1210
+ unsub();
1211
+ curEvtUnsubs.splice(0, curEvtUnsubs.length);
1212
+ this.eventUnsubscribes = this.eventUnsubscribes.filter((u) => !curEvtUnsubs.includes(u));
1213
+ };
1214
+ for (const event of oneOf) {
1215
+ const unsub = this.events.on(event, (...args) => {
1216
+ checkUnsubAllEvt();
1217
+ callback(event, ...args);
1218
+ if (once)
1219
+ checkUnsubAllEvt(true);
1220
+ });
1221
+ curEvtUnsubs.push(unsub);
1222
+ }
1223
+ const allOfEmitted = /* @__PURE__ */ new Set();
1224
+ const checkAllOf = (event, ...args) => {
1225
+ checkUnsubAllEvt();
1226
+ allOfEmitted.add(event);
1227
+ if (allOfEmitted.size === allOf.length) {
1228
+ callback(event, ...args);
1229
+ if (once)
1230
+ checkUnsubAllEvt(true);
1231
+ }
1232
+ };
1233
+ for (const event of allOf) {
1234
+ const unsub = this.events.on(event, (...args) => {
1235
+ checkUnsubAllEvt();
1236
+ checkAllOf(event, ...args);
1237
+ });
1238
+ curEvtUnsubs.push(unsub);
1239
+ }
1240
+ if (oneOf.length === 0 && allOf.length === 0)
1241
+ throw new TypeError("NanoEmitter.onMulti(): Either `oneOf` or `allOf` or both must be provided in the options");
1242
+ allUnsubs.push(() => checkUnsubAllEvt(true));
1243
+ }
1244
+ return unsubAll;
1245
+ }
1246
+ //#region emit
1103
1247
  /**
1104
1248
  * Emits an event on this instance.
1105
- * ⚠️ Needs `publicEmit` to be set to true in the NanoEmitter constructor or super() call!
1249
+ * - ⚠️ Needs `publicEmit` to be set to true in the NanoEmitter constructor or super() call!
1106
1250
  * @param event The event to emit
1107
1251
  * @param args The arguments to pass to the event listeners
1108
1252
  * @returns Returns true if `publicEmit` is true and the event was emitted successfully
@@ -1114,6 +1258,7 @@ var NanoEmitter = class {
1114
1258
  }
1115
1259
  return false;
1116
1260
  }
1261
+ //#region unsubscribeAll
1117
1262
  /** Unsubscribes all event listeners from this instance */
1118
1263
  unsubscribeAll() {
1119
1264
  for (const unsub of this.eventUnsubscribes)