tiny-essentials 1.19.1 → 1.19.3

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.
@@ -12198,6 +12198,22 @@ const customEncoders = new Map();
12198
12198
  /** @type {Map<any, DecodeFn>} */
12199
12199
  const customDecoders = new Map();
12200
12200
 
12201
+ /** @type {Set<any>} */
12202
+ const customTypesFreezed = new Set([
12203
+ 'string',
12204
+ 'boolean',
12205
+ 'number',
12206
+ 'object',
12207
+ 'array',
12208
+ String,
12209
+ Boolean,
12210
+ Number,
12211
+ Object,
12212
+ Array,
12213
+ BigInt,
12214
+ Symbol,
12215
+ ]);
12216
+
12201
12217
  /**
12202
12218
  * A function that encodes a value into a serializable JSON-compatible format.
12203
12219
  *
@@ -12469,26 +12485,49 @@ class TinyLocalStorage {
12469
12485
 
12470
12486
  ///////////////////////////////////////////////////
12471
12487
 
12488
+ /**
12489
+ * Checks whether a JSON-serializable type is already registered.
12490
+ *
12491
+ * @param {any} type - The type identifier, which can be a string (e.g., `"bigint"`, `"symbol"`)
12492
+ * or a reference to a class/constructor function.
12493
+ * @returns {boolean} True if the type has both an encoder and decoder registered.
12494
+ */
12495
+ static hasJsonType(type) {
12496
+ return (customEncoders.has(type) && customDecoders.has(type)) || customTypesFreezed.has(type);
12497
+ }
12498
+
12472
12499
  /**
12473
12500
  * Registers a new JSON-serializable type with its encoder and decoder.
12474
12501
  *
12475
- * @param {any} type - The type or primitive type name (e.g. `"bigint"`, `"symbol"`, etc).
12476
- * @param {EncodeFn} encodeFn - The function that encodes the value.
12477
- * @param {DecodeFn} decodeFn - An object with `check` and `decode` methods for restoring the value.
12502
+ * @param {any} type - The type identifier, which can be a string (e.g., `"bigint"`, `"symbol"`)
12503
+ * or a reference to a class/constructor function.
12504
+ * @param {EncodeFn} encodeFn - A function that receives a value of this type and returns a JSON-safe representation.
12505
+ * @param {DecodeFn} decodeFn - An object with `check(value)` to identify this type and `decode(value)` to restore it.
12506
+ * @param {boolean} [freezeType=false] - If true, prevents this type from being unregistered later.
12507
+ * @throws {Error} Throws an error if the type is frozen and cannot be registered.
12478
12508
  */
12479
- static registerJsonType(type, encodeFn, decodeFn) {
12509
+ static registerJsonType(type, encodeFn, decodeFn, freezeType = false) {
12510
+ if (customTypesFreezed.has(type))
12511
+ throw new Error(`Cannot register type "${type}" because it is frozen.`);
12480
12512
  customEncoders.set(type, encodeFn);
12481
12513
  customDecoders.set(type, decodeFn);
12514
+ if (freezeType) customTypesFreezed.add(type);
12482
12515
  }
12483
12516
 
12484
12517
  /**
12485
- * Removes a previously registered custom type from the encoding/decoding system.
12518
+ * Unregisters a previously registered custom type from the encoding/decoding system.
12486
12519
  *
12487
- * @param {string} type - The primitive name or constructor reference used in registration.
12520
+ * @param {any} type - The primitive name or constructor reference used in the registration.
12521
+ * @returns {boolean} Returns true if the type existed and was removed, or false if it wasn't found.
12522
+ * @throws {Error} Throws an error if the type is frozen and cannot be unregistered.
12488
12523
  */
12489
12524
  static deleteJsonType(type) {
12490
- customEncoders.delete(type);
12491
- customDecoders.delete(type);
12525
+ if (customTypesFreezed.has(type))
12526
+ throw new Error(`Cannot remove type "${type}" because it is frozen.`);
12527
+ let isDeleted = false;
12528
+ if (customEncoders.delete(type)) isDeleted = true;
12529
+ if (customDecoders.delete(type)) isDeleted = true;
12530
+ return isDeleted;
12492
12531
  }
12493
12532
 
12494
12533
  //////////////////////////////////////////////////////
@@ -12571,6 +12610,12 @@ class TinyLocalStorage {
12571
12610
  /** @type {Storage} */
12572
12611
  #localStorage = window.localStorage;
12573
12612
 
12613
+ /** @type {string|null} */
12614
+ #dbKey = null;
12615
+
12616
+ /** @type {number} */
12617
+ #version = 0;
12618
+
12574
12619
  /** @type {(ev: StorageEvent) => any} */
12575
12620
  #storageEvent = (ev) => this.emit('storage', ev);
12576
12621
 
@@ -12578,11 +12623,84 @@ class TinyLocalStorage {
12578
12623
  * Initializes the TinyLocalStorage instance and sets up cross-tab sync.
12579
12624
  *
12580
12625
  * Adds listener for the native `storage` event to support tab synchronization.
12626
+ * @param {string} [dbName] - Unique database name.
12581
12627
  */
12582
- constructor() {
12628
+ constructor(dbName) {
12629
+ if (typeof dbName !== 'undefined' && typeof dbName !== 'string')
12630
+ throw new TypeError('TinyLocalStorage: dbName must be a string if provided.');
12631
+ if (typeof dbName === 'string') this.#dbKey = `LSDB::${dbName}`;
12583
12632
  window.addEventListener('storage', this.#storageEvent);
12584
12633
  }
12585
12634
 
12635
+ /**
12636
+ * Validates that a given key does not conflict with internal database keys.
12637
+ *
12638
+ * This method is used to prevent accidental overwriting of reserved `LSDB::` keys
12639
+ * in `localStorage`, which are used internally by TinyLocalStorage for versioning
12640
+ * and data management.
12641
+ *
12642
+ * @param {string} name - The key to validate before writing to localStorage.
12643
+ * @throws {Error} If the key starts with `LSDB::`.
12644
+ */
12645
+ #isProtectedDbKey(name) {
12646
+ if (typeof name === 'string' && name.startsWith('LSDB::'))
12647
+ throw new Error(`TinyLocalStorage: Key "${name}" may conflict with reserved dbKeys.`);
12648
+ }
12649
+
12650
+ /**
12651
+ * Updates the version of the storage and triggers migration if needed.
12652
+ *
12653
+ * @param {number} version - Desired version of the database.
12654
+ * @param {(oldVersion: number, newVersion: number) => void} onUpgrade - Callback to perform migration logic.
12655
+ * @throws {Error} If the database key has not been initialized.
12656
+ * @throws {TypeError} If `version` is not a valid positive number.
12657
+ * @throws {TypeError} If `onUpgrade` is not a function.
12658
+ */
12659
+ updateStorageVersion(version, onUpgrade) {
12660
+ if (typeof this.#dbKey !== 'string')
12661
+ throw new Error(
12662
+ 'TinyLocalStorage: Database key is not initialized. Set a valid dbName in the constructor.',
12663
+ );
12664
+ if (typeof version !== 'number' || Number.isNaN(version) || version < 1)
12665
+ throw new TypeError('TinyLocalStorage: version must be a positive number.');
12666
+ if (typeof onUpgrade !== 'function')
12667
+ throw new TypeError('TinyLocalStorage: onUpgrade must be a function.');
12668
+
12669
+ // @ts-ignore
12670
+ const savedVersion = parseInt(localStorage.getItem(this.#dbKey), 10) || 0;
12671
+ if (typeof savedVersion !== 'number' || Number.isNaN(savedVersion) || savedVersion < 0)
12672
+ throw new TypeError('TinyLocalStorage: saved version in localStorage is not a valid number.');
12673
+
12674
+ if (version < savedVersion)
12675
+ throw new Error(
12676
+ `TinyLocalStorage: Cannot downgrade database version from ${savedVersion} to ${version}.`,
12677
+ );
12678
+
12679
+ if (version > savedVersion) {
12680
+ onUpgrade(savedVersion, version);
12681
+ localStorage.setItem(this.#dbKey, String(version));
12682
+ this.#version = version;
12683
+ }
12684
+ }
12685
+
12686
+ /**
12687
+ * Gets the current database key used in localStorage.
12688
+ *
12689
+ * @returns {string|null} The database key, or null if not set.
12690
+ */
12691
+ getDbKey() {
12692
+ return this.#dbKey;
12693
+ }
12694
+
12695
+ /**
12696
+ * Gets the current version of the database.
12697
+ *
12698
+ * @returns {number} The current version number.
12699
+ */
12700
+ getVersion() {
12701
+ return this.#version;
12702
+ }
12703
+
12586
12704
  /**
12587
12705
  * Defines a custom storage interface (e.g. `sessionStorage`).
12588
12706
  *
@@ -12590,7 +12708,7 @@ class TinyLocalStorage {
12590
12708
  */
12591
12709
  setLocalStorage(localstorage) {
12592
12710
  if (!(localstorage instanceof Storage))
12593
- throw new Error('Argument must be a valid instance of Storage.');
12711
+ throw new TypeError('Argument must be a valid instance of Storage.');
12594
12712
  this.#localStorage = localstorage;
12595
12713
  }
12596
12714
 
@@ -12612,7 +12730,7 @@ class TinyLocalStorage {
12612
12730
  */
12613
12731
  #setJson(name, data) {
12614
12732
  if (typeof name !== 'string' || !name.length)
12615
- throw new Error('Key must be a non-empty string.');
12733
+ throw new TypeError('Key must be a non-empty string.');
12616
12734
  return TinyLocalStorage.encodeSpecialJson(data);
12617
12735
  }
12618
12736
 
@@ -12625,13 +12743,14 @@ class TinyLocalStorage {
12625
12743
  * @param {LocalStorageJsonValue} data - The data to be serialized and stored.
12626
12744
  */
12627
12745
  setJson(name, data) {
12746
+ this.#isProtectedDbKey(name);
12628
12747
  if (
12629
12748
  !isJsonObject(data) &&
12630
12749
  !Array.isArray(data) &&
12631
12750
  !(data instanceof Map) &&
12632
12751
  !(data instanceof Set)
12633
12752
  ) {
12634
- throw new Error('The storage value is not a valid JSON-compatible structure.');
12753
+ throw new TypeError('The storage value is not a valid JSON-compatible structure.');
12635
12754
  }
12636
12755
  const encoded = this.#setJson(name, data);
12637
12756
  this.emit('setJson', name, data);
@@ -12649,7 +12768,7 @@ class TinyLocalStorage {
12649
12768
  */
12650
12769
  #getJson(name, defaultData) {
12651
12770
  if (typeof name !== 'string' || !name.length)
12652
- throw new Error('Key must be a non-empty string.');
12771
+ throw new TypeError('Key must be a non-empty string.');
12653
12772
 
12654
12773
  const raw = this.#localStorage.getItem(name);
12655
12774
  const fallbackTypes = {
@@ -12703,7 +12822,8 @@ class TinyLocalStorage {
12703
12822
  * @param {Date} data
12704
12823
  */
12705
12824
  setDate(name, data) {
12706
- if (!(data instanceof Date)) throw new Error('Value must be a Date.');
12825
+ this.#isProtectedDbKey(name);
12826
+ if (!(data instanceof Date)) throw new TypeError('Value must be a Date.');
12707
12827
  const encoded = this.#setJson(name, data);
12708
12828
  this.emit('setDate', name, data);
12709
12829
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -12725,7 +12845,8 @@ class TinyLocalStorage {
12725
12845
  * @param {RegExp} data
12726
12846
  */
12727
12847
  setRegExp(name, data) {
12728
- if (!(data instanceof RegExp)) throw new Error('Value must be a RegExp.');
12848
+ this.#isProtectedDbKey(name);
12849
+ if (!(data instanceof RegExp)) throw new TypeError('Value must be a RegExp.');
12729
12850
  const encoded = this.#setJson(name, data);
12730
12851
  this.emit('setRegExp', name, data);
12731
12852
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -12747,7 +12868,8 @@ class TinyLocalStorage {
12747
12868
  * @param {bigint} data
12748
12869
  */
12749
12870
  setBigInt(name, data) {
12750
- if (typeof data !== 'bigint') throw new Error('Value must be a BigInt.');
12871
+ this.#isProtectedDbKey(name);
12872
+ if (typeof data !== 'bigint') throw new TypeError('Value must be a BigInt.');
12751
12873
  const encoded = this.#setJson(name, data);
12752
12874
  this.emit('setBigInt', name, data);
12753
12875
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -12770,7 +12892,8 @@ class TinyLocalStorage {
12770
12892
  * @param {symbol} data
12771
12893
  */
12772
12894
  setSymbol(name, data) {
12773
- if (typeof data !== 'symbol') throw new Error('Value must be a Symbol.');
12895
+ this.#isProtectedDbKey(name);
12896
+ if (typeof data !== 'symbol') throw new TypeError('Value must be a Symbol.');
12774
12897
  const encoded = this.#setJson(name, data);
12775
12898
  this.emit('setSymbol', name, data);
12776
12899
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -12803,8 +12926,9 @@ class TinyLocalStorage {
12803
12926
  * @param {any} data - The data to store.
12804
12927
  */
12805
12928
  setItem(name, data) {
12929
+ this.#isProtectedDbKey(name);
12806
12930
  if (typeof name !== 'string' || !name.length)
12807
- throw new Error('Key must be a non-empty string.');
12931
+ throw new TypeError('Key must be a non-empty string.');
12808
12932
  this.emit('setItem', name, data);
12809
12933
  return this.#localStorage.setItem(name, data);
12810
12934
  }
@@ -12817,7 +12941,7 @@ class TinyLocalStorage {
12817
12941
  */
12818
12942
  getItem(name) {
12819
12943
  if (typeof name !== 'string' || !name.length)
12820
- throw new Error('Key must be a non-empty string.');
12944
+ throw new TypeError('Key must be a non-empty string.');
12821
12945
  return this.#localStorage.getItem(name);
12822
12946
  }
12823
12947
 
@@ -12828,12 +12952,19 @@ class TinyLocalStorage {
12828
12952
  * @param {string} data - The string to store.
12829
12953
  */
12830
12954
  setString(name, data) {
12955
+ this.#isProtectedDbKey(name);
12831
12956
  if (typeof name !== 'string' || !name.length)
12832
- throw new Error('Key must be a non-empty string.');
12833
- if (typeof data !== 'string') throw new Error('Value must be a string.');
12957
+ throw new TypeError('Key must be a non-empty string.');
12958
+ if (typeof data !== 'string') throw new TypeError('Value must be a string.');
12834
12959
 
12835
12960
  this.emit('setString', name, data);
12836
- return this.#localStorage.setItem(name, data);
12961
+ return this.#localStorage.setItem(
12962
+ name,
12963
+ JSON.stringify({
12964
+ __string__: true,
12965
+ value: data,
12966
+ }),
12967
+ );
12837
12968
  }
12838
12969
 
12839
12970
  /**
@@ -12844,8 +12975,17 @@ class TinyLocalStorage {
12844
12975
  */
12845
12976
  getString(name) {
12846
12977
  if (typeof name !== 'string' || !name.length)
12847
- throw new Error('Key must be a non-empty string.');
12978
+ throw new TypeError('Key must be a non-empty string.');
12848
12979
  let value = this.#localStorage.getItem(name);
12980
+ try {
12981
+ /** @type {{ value: string; __string__: boolean }} */
12982
+ // @ts-ignore
12983
+ value = JSON.parse(value);
12984
+ if (!isJsonObject(value) || !value.__string__ || typeof value.value !== 'string') return null;
12985
+ value = value.value;
12986
+ } catch {
12987
+ value = null;
12988
+ }
12849
12989
  if (typeof value === 'string') return value;
12850
12990
  return null;
12851
12991
  }
@@ -12857,9 +12997,10 @@ class TinyLocalStorage {
12857
12997
  * @param {number} data - The number to store.
12858
12998
  */
12859
12999
  setNumber(name, data) {
13000
+ this.#isProtectedDbKey(name);
12860
13001
  if (typeof name !== 'string' || !name.length)
12861
- throw new Error('Key must be a non-empty string.');
12862
- if (typeof data !== 'number') throw new Error('Value must be a number.');
13002
+ throw new TypeError('Key must be a non-empty string.');
13003
+ if (typeof data !== 'number') throw new TypeError('Value must be a number.');
12863
13004
  this.emit('setNumber', name, data);
12864
13005
  return this.#localStorage.setItem(name, String(data));
12865
13006
  }
@@ -12872,7 +13013,7 @@ class TinyLocalStorage {
12872
13013
  */
12873
13014
  getNumber(name) {
12874
13015
  if (typeof name !== 'string' || !name.length)
12875
- throw new Error('Key must be a non-empty string.');
13016
+ throw new TypeError('Key must be a non-empty string.');
12876
13017
 
12877
13018
  /** @type {number|string|null} */
12878
13019
  let number = this.#localStorage.getItem(name);
@@ -12891,9 +13032,10 @@ class TinyLocalStorage {
12891
13032
  * @param {boolean} data - The boolean value to store.
12892
13033
  */
12893
13034
  setBool(name, data) {
13035
+ this.#isProtectedDbKey(name);
12894
13036
  if (typeof name !== 'string' || !name.length)
12895
- throw new Error('Key must be a non-empty string.');
12896
- if (typeof data !== 'boolean') throw new Error('Value must be a boolean.');
13037
+ throw new TypeError('Key must be a non-empty string.');
13038
+ if (typeof data !== 'boolean') throw new TypeError('Value must be a boolean.');
12897
13039
  this.emit('setBool', name, data);
12898
13040
  return this.#localStorage.setItem(name, String(data));
12899
13041
  }
@@ -12906,7 +13048,7 @@ class TinyLocalStorage {
12906
13048
  */
12907
13049
  getBool(name) {
12908
13050
  if (typeof name !== 'string' || !name.length)
12909
- throw new Error('Key must be a non-empty string.');
13051
+ throw new TypeError('Key must be a non-empty string.');
12910
13052
 
12911
13053
  const value = this.#localStorage.getItem(name);
12912
13054
  if (typeof value === 'boolean') return value;
@@ -12925,7 +13067,7 @@ class TinyLocalStorage {
12925
13067
  */
12926
13068
  removeItem(name) {
12927
13069
  if (typeof name !== 'string' || !name.length)
12928
- throw new Error('Key must be a non-empty string.');
13070
+ throw new TypeError('Key must be a non-empty string.');
12929
13071
 
12930
13072
  this.emit('removeItem', name);
12931
13073
  return this.#localStorage.removeItem(name);
@@ -12962,6 +13104,7 @@ TinyLocalStorage.registerJsonType(
12962
13104
  decode: (value, decodeSpecialJson) =>
12963
13105
  new Map(value.data.map(([k, v]) => [k, decodeSpecialJson(v)])),
12964
13106
  },
13107
+ true,
12965
13108
  );
12966
13109
 
12967
13110
  // Set
@@ -12975,6 +13118,7 @@ TinyLocalStorage.registerJsonType(
12975
13118
  check: (value) => value.__set__,
12976
13119
  decode: (value, decodeSpecialJson) => new Set(value.data.map(decodeSpecialJson)),
12977
13120
  },
13121
+ true,
12978
13122
  );
12979
13123
 
12980
13124
  // Date
@@ -12988,6 +13132,7 @@ TinyLocalStorage.registerJsonType(
12988
13132
  check: (value) => value.__date__,
12989
13133
  decode: (value) => new Date(value.value),
12990
13134
  },
13135
+ true,
12991
13136
  );
12992
13137
 
12993
13138
  // Regex
@@ -13002,6 +13147,7 @@ TinyLocalStorage.registerJsonType(
13002
13147
  check: (value) => value.__regexp__,
13003
13148
  decode: (value) => new RegExp(value.source, value.flags),
13004
13149
  },
13150
+ true,
13005
13151
  );
13006
13152
 
13007
13153
  // Big Int
@@ -13015,6 +13161,7 @@ TinyLocalStorage.registerJsonType(
13015
13161
  check: (value) => value.__bigint__,
13016
13162
  decode: (value) => BigInt(value.value),
13017
13163
  },
13164
+ true,
13018
13165
  );
13019
13166
 
13020
13167
  // Symbol
@@ -13031,6 +13178,7 @@ TinyLocalStorage.registerJsonType(
13031
13178
  return key != null ? Symbol.for(key) : Symbol();
13032
13179
  },
13033
13180
  },
13181
+ true,
13034
13182
  );
13035
13183
 
13036
13184
  /* harmony default export */ const libs_TinyLocalStorage = ((/* unused pure expression or super */ null && (TinyLocalStorage)));