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.
@@ -18860,6 +18860,22 @@ const customEncoders = new Map();
18860
18860
  /** @type {Map<any, DecodeFn>} */
18861
18861
  const customDecoders = new Map();
18862
18862
 
18863
+ /** @type {Set<any>} */
18864
+ const customTypesFreezed = new Set([
18865
+ 'string',
18866
+ 'boolean',
18867
+ 'number',
18868
+ 'object',
18869
+ 'array',
18870
+ String,
18871
+ Boolean,
18872
+ Number,
18873
+ Object,
18874
+ Array,
18875
+ BigInt,
18876
+ Symbol,
18877
+ ]);
18878
+
18863
18879
  /**
18864
18880
  * A function that encodes a value into a serializable JSON-compatible format.
18865
18881
  *
@@ -19131,26 +19147,49 @@ class TinyLocalStorage {
19131
19147
 
19132
19148
  ///////////////////////////////////////////////////
19133
19149
 
19150
+ /**
19151
+ * Checks whether a JSON-serializable type is already registered.
19152
+ *
19153
+ * @param {any} type - The type identifier, which can be a string (e.g., `"bigint"`, `"symbol"`)
19154
+ * or a reference to a class/constructor function.
19155
+ * @returns {boolean} True if the type has both an encoder and decoder registered.
19156
+ */
19157
+ static hasJsonType(type) {
19158
+ return (customEncoders.has(type) && customDecoders.has(type)) || customTypesFreezed.has(type);
19159
+ }
19160
+
19134
19161
  /**
19135
19162
  * Registers a new JSON-serializable type with its encoder and decoder.
19136
19163
  *
19137
- * @param {any} type - The type or primitive type name (e.g. `"bigint"`, `"symbol"`, etc).
19138
- * @param {EncodeFn} encodeFn - The function that encodes the value.
19139
- * @param {DecodeFn} decodeFn - An object with `check` and `decode` methods for restoring the value.
19164
+ * @param {any} type - The type identifier, which can be a string (e.g., `"bigint"`, `"symbol"`)
19165
+ * or a reference to a class/constructor function.
19166
+ * @param {EncodeFn} encodeFn - A function that receives a value of this type and returns a JSON-safe representation.
19167
+ * @param {DecodeFn} decodeFn - An object with `check(value)` to identify this type and `decode(value)` to restore it.
19168
+ * @param {boolean} [freezeType=false] - If true, prevents this type from being unregistered later.
19169
+ * @throws {Error} Throws an error if the type is frozen and cannot be registered.
19140
19170
  */
19141
- static registerJsonType(type, encodeFn, decodeFn) {
19171
+ static registerJsonType(type, encodeFn, decodeFn, freezeType = false) {
19172
+ if (customTypesFreezed.has(type))
19173
+ throw new Error(`Cannot register type "${type}" because it is frozen.`);
19142
19174
  customEncoders.set(type, encodeFn);
19143
19175
  customDecoders.set(type, decodeFn);
19176
+ if (freezeType) customTypesFreezed.add(type);
19144
19177
  }
19145
19178
 
19146
19179
  /**
19147
- * Removes a previously registered custom type from the encoding/decoding system.
19180
+ * Unregisters a previously registered custom type from the encoding/decoding system.
19148
19181
  *
19149
- * @param {string} type - The primitive name or constructor reference used in registration.
19182
+ * @param {any} type - The primitive name or constructor reference used in the registration.
19183
+ * @returns {boolean} Returns true if the type existed and was removed, or false if it wasn't found.
19184
+ * @throws {Error} Throws an error if the type is frozen and cannot be unregistered.
19150
19185
  */
19151
19186
  static deleteJsonType(type) {
19152
- customEncoders.delete(type);
19153
- customDecoders.delete(type);
19187
+ if (customTypesFreezed.has(type))
19188
+ throw new Error(`Cannot remove type "${type}" because it is frozen.`);
19189
+ let isDeleted = false;
19190
+ if (customEncoders.delete(type)) isDeleted = true;
19191
+ if (customDecoders.delete(type)) isDeleted = true;
19192
+ return isDeleted;
19154
19193
  }
19155
19194
 
19156
19195
  //////////////////////////////////////////////////////
@@ -19233,6 +19272,12 @@ class TinyLocalStorage {
19233
19272
  /** @type {Storage} */
19234
19273
  #localStorage = window.localStorage;
19235
19274
 
19275
+ /** @type {string|null} */
19276
+ #dbKey = null;
19277
+
19278
+ /** @type {number} */
19279
+ #version = 0;
19280
+
19236
19281
  /** @type {(ev: StorageEvent) => any} */
19237
19282
  #storageEvent = (ev) => this.emit('storage', ev);
19238
19283
 
@@ -19240,11 +19285,84 @@ class TinyLocalStorage {
19240
19285
  * Initializes the TinyLocalStorage instance and sets up cross-tab sync.
19241
19286
  *
19242
19287
  * Adds listener for the native `storage` event to support tab synchronization.
19288
+ * @param {string} [dbName] - Unique database name.
19243
19289
  */
19244
- constructor() {
19290
+ constructor(dbName) {
19291
+ if (typeof dbName !== 'undefined' && typeof dbName !== 'string')
19292
+ throw new TypeError('TinyLocalStorage: dbName must be a string if provided.');
19293
+ if (typeof dbName === 'string') this.#dbKey = `LSDB::${dbName}`;
19245
19294
  window.addEventListener('storage', this.#storageEvent);
19246
19295
  }
19247
19296
 
19297
+ /**
19298
+ * Validates that a given key does not conflict with internal database keys.
19299
+ *
19300
+ * This method is used to prevent accidental overwriting of reserved `LSDB::` keys
19301
+ * in `localStorage`, which are used internally by TinyLocalStorage for versioning
19302
+ * and data management.
19303
+ *
19304
+ * @param {string} name - The key to validate before writing to localStorage.
19305
+ * @throws {Error} If the key starts with `LSDB::`.
19306
+ */
19307
+ #isProtectedDbKey(name) {
19308
+ if (typeof name === 'string' && name.startsWith('LSDB::'))
19309
+ throw new Error(`TinyLocalStorage: Key "${name}" may conflict with reserved dbKeys.`);
19310
+ }
19311
+
19312
+ /**
19313
+ * Updates the version of the storage and triggers migration if needed.
19314
+ *
19315
+ * @param {number} version - Desired version of the database.
19316
+ * @param {(oldVersion: number, newVersion: number) => void} onUpgrade - Callback to perform migration logic.
19317
+ * @throws {Error} If the database key has not been initialized.
19318
+ * @throws {TypeError} If `version` is not a valid positive number.
19319
+ * @throws {TypeError} If `onUpgrade` is not a function.
19320
+ */
19321
+ updateStorageVersion(version, onUpgrade) {
19322
+ if (typeof this.#dbKey !== 'string')
19323
+ throw new Error(
19324
+ 'TinyLocalStorage: Database key is not initialized. Set a valid dbName in the constructor.',
19325
+ );
19326
+ if (typeof version !== 'number' || Number.isNaN(version) || version < 1)
19327
+ throw new TypeError('TinyLocalStorage: version must be a positive number.');
19328
+ if (typeof onUpgrade !== 'function')
19329
+ throw new TypeError('TinyLocalStorage: onUpgrade must be a function.');
19330
+
19331
+ // @ts-ignore
19332
+ const savedVersion = parseInt(localStorage.getItem(this.#dbKey), 10) || 0;
19333
+ if (typeof savedVersion !== 'number' || Number.isNaN(savedVersion) || savedVersion < 0)
19334
+ throw new TypeError('TinyLocalStorage: saved version in localStorage is not a valid number.');
19335
+
19336
+ if (version < savedVersion)
19337
+ throw new Error(
19338
+ `TinyLocalStorage: Cannot downgrade database version from ${savedVersion} to ${version}.`,
19339
+ );
19340
+
19341
+ if (version > savedVersion) {
19342
+ onUpgrade(savedVersion, version);
19343
+ localStorage.setItem(this.#dbKey, String(version));
19344
+ this.#version = version;
19345
+ }
19346
+ }
19347
+
19348
+ /**
19349
+ * Gets the current database key used in localStorage.
19350
+ *
19351
+ * @returns {string|null} The database key, or null if not set.
19352
+ */
19353
+ getDbKey() {
19354
+ return this.#dbKey;
19355
+ }
19356
+
19357
+ /**
19358
+ * Gets the current version of the database.
19359
+ *
19360
+ * @returns {number} The current version number.
19361
+ */
19362
+ getVersion() {
19363
+ return this.#version;
19364
+ }
19365
+
19248
19366
  /**
19249
19367
  * Defines a custom storage interface (e.g. `sessionStorage`).
19250
19368
  *
@@ -19252,7 +19370,7 @@ class TinyLocalStorage {
19252
19370
  */
19253
19371
  setLocalStorage(localstorage) {
19254
19372
  if (!(localstorage instanceof Storage))
19255
- throw new Error('Argument must be a valid instance of Storage.');
19373
+ throw new TypeError('Argument must be a valid instance of Storage.');
19256
19374
  this.#localStorage = localstorage;
19257
19375
  }
19258
19376
 
@@ -19274,7 +19392,7 @@ class TinyLocalStorage {
19274
19392
  */
19275
19393
  #setJson(name, data) {
19276
19394
  if (typeof name !== 'string' || !name.length)
19277
- throw new Error('Key must be a non-empty string.');
19395
+ throw new TypeError('Key must be a non-empty string.');
19278
19396
  return TinyLocalStorage.encodeSpecialJson(data);
19279
19397
  }
19280
19398
 
@@ -19287,13 +19405,14 @@ class TinyLocalStorage {
19287
19405
  * @param {LocalStorageJsonValue} data - The data to be serialized and stored.
19288
19406
  */
19289
19407
  setJson(name, data) {
19408
+ this.#isProtectedDbKey(name);
19290
19409
  if (
19291
19410
  !isJsonObject(data) &&
19292
19411
  !Array.isArray(data) &&
19293
19412
  !(data instanceof Map) &&
19294
19413
  !(data instanceof Set)
19295
19414
  ) {
19296
- throw new Error('The storage value is not a valid JSON-compatible structure.');
19415
+ throw new TypeError('The storage value is not a valid JSON-compatible structure.');
19297
19416
  }
19298
19417
  const encoded = this.#setJson(name, data);
19299
19418
  this.emit('setJson', name, data);
@@ -19311,7 +19430,7 @@ class TinyLocalStorage {
19311
19430
  */
19312
19431
  #getJson(name, defaultData) {
19313
19432
  if (typeof name !== 'string' || !name.length)
19314
- throw new Error('Key must be a non-empty string.');
19433
+ throw new TypeError('Key must be a non-empty string.');
19315
19434
 
19316
19435
  const raw = this.#localStorage.getItem(name);
19317
19436
  const fallbackTypes = {
@@ -19365,7 +19484,8 @@ class TinyLocalStorage {
19365
19484
  * @param {Date} data
19366
19485
  */
19367
19486
  setDate(name, data) {
19368
- if (!(data instanceof Date)) throw new Error('Value must be a Date.');
19487
+ this.#isProtectedDbKey(name);
19488
+ if (!(data instanceof Date)) throw new TypeError('Value must be a Date.');
19369
19489
  const encoded = this.#setJson(name, data);
19370
19490
  this.emit('setDate', name, data);
19371
19491
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -19387,7 +19507,8 @@ class TinyLocalStorage {
19387
19507
  * @param {RegExp} data
19388
19508
  */
19389
19509
  setRegExp(name, data) {
19390
- if (!(data instanceof RegExp)) throw new Error('Value must be a RegExp.');
19510
+ this.#isProtectedDbKey(name);
19511
+ if (!(data instanceof RegExp)) throw new TypeError('Value must be a RegExp.');
19391
19512
  const encoded = this.#setJson(name, data);
19392
19513
  this.emit('setRegExp', name, data);
19393
19514
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -19409,7 +19530,8 @@ class TinyLocalStorage {
19409
19530
  * @param {bigint} data
19410
19531
  */
19411
19532
  setBigInt(name, data) {
19412
- if (typeof data !== 'bigint') throw new Error('Value must be a BigInt.');
19533
+ this.#isProtectedDbKey(name);
19534
+ if (typeof data !== 'bigint') throw new TypeError('Value must be a BigInt.');
19413
19535
  const encoded = this.#setJson(name, data);
19414
19536
  this.emit('setBigInt', name, data);
19415
19537
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -19432,7 +19554,8 @@ class TinyLocalStorage {
19432
19554
  * @param {symbol} data
19433
19555
  */
19434
19556
  setSymbol(name, data) {
19435
- if (typeof data !== 'symbol') throw new Error('Value must be a Symbol.');
19557
+ this.#isProtectedDbKey(name);
19558
+ if (typeof data !== 'symbol') throw new TypeError('Value must be a Symbol.');
19436
19559
  const encoded = this.#setJson(name, data);
19437
19560
  this.emit('setSymbol', name, data);
19438
19561
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -19465,8 +19588,9 @@ class TinyLocalStorage {
19465
19588
  * @param {any} data - The data to store.
19466
19589
  */
19467
19590
  setItem(name, data) {
19591
+ this.#isProtectedDbKey(name);
19468
19592
  if (typeof name !== 'string' || !name.length)
19469
- throw new Error('Key must be a non-empty string.');
19593
+ throw new TypeError('Key must be a non-empty string.');
19470
19594
  this.emit('setItem', name, data);
19471
19595
  return this.#localStorage.setItem(name, data);
19472
19596
  }
@@ -19479,7 +19603,7 @@ class TinyLocalStorage {
19479
19603
  */
19480
19604
  getItem(name) {
19481
19605
  if (typeof name !== 'string' || !name.length)
19482
- throw new Error('Key must be a non-empty string.');
19606
+ throw new TypeError('Key must be a non-empty string.');
19483
19607
  return this.#localStorage.getItem(name);
19484
19608
  }
19485
19609
 
@@ -19490,12 +19614,19 @@ class TinyLocalStorage {
19490
19614
  * @param {string} data - The string to store.
19491
19615
  */
19492
19616
  setString(name, data) {
19617
+ this.#isProtectedDbKey(name);
19493
19618
  if (typeof name !== 'string' || !name.length)
19494
- throw new Error('Key must be a non-empty string.');
19495
- if (typeof data !== 'string') throw new Error('Value must be a string.');
19619
+ throw new TypeError('Key must be a non-empty string.');
19620
+ if (typeof data !== 'string') throw new TypeError('Value must be a string.');
19496
19621
 
19497
19622
  this.emit('setString', name, data);
19498
- return this.#localStorage.setItem(name, data);
19623
+ return this.#localStorage.setItem(
19624
+ name,
19625
+ JSON.stringify({
19626
+ __string__: true,
19627
+ value: data,
19628
+ }),
19629
+ );
19499
19630
  }
19500
19631
 
19501
19632
  /**
@@ -19506,8 +19637,17 @@ class TinyLocalStorage {
19506
19637
  */
19507
19638
  getString(name) {
19508
19639
  if (typeof name !== 'string' || !name.length)
19509
- throw new Error('Key must be a non-empty string.');
19640
+ throw new TypeError('Key must be a non-empty string.');
19510
19641
  let value = this.#localStorage.getItem(name);
19642
+ try {
19643
+ /** @type {{ value: string; __string__: boolean }} */
19644
+ // @ts-ignore
19645
+ value = JSON.parse(value);
19646
+ if (!isJsonObject(value) || !value.__string__ || typeof value.value !== 'string') return null;
19647
+ value = value.value;
19648
+ } catch {
19649
+ value = null;
19650
+ }
19511
19651
  if (typeof value === 'string') return value;
19512
19652
  return null;
19513
19653
  }
@@ -19519,9 +19659,10 @@ class TinyLocalStorage {
19519
19659
  * @param {number} data - The number to store.
19520
19660
  */
19521
19661
  setNumber(name, data) {
19662
+ this.#isProtectedDbKey(name);
19522
19663
  if (typeof name !== 'string' || !name.length)
19523
- throw new Error('Key must be a non-empty string.');
19524
- if (typeof data !== 'number') throw new Error('Value must be a number.');
19664
+ throw new TypeError('Key must be a non-empty string.');
19665
+ if (typeof data !== 'number') throw new TypeError('Value must be a number.');
19525
19666
  this.emit('setNumber', name, data);
19526
19667
  return this.#localStorage.setItem(name, String(data));
19527
19668
  }
@@ -19534,7 +19675,7 @@ class TinyLocalStorage {
19534
19675
  */
19535
19676
  getNumber(name) {
19536
19677
  if (typeof name !== 'string' || !name.length)
19537
- throw new Error('Key must be a non-empty string.');
19678
+ throw new TypeError('Key must be a non-empty string.');
19538
19679
 
19539
19680
  /** @type {number|string|null} */
19540
19681
  let number = this.#localStorage.getItem(name);
@@ -19553,9 +19694,10 @@ class TinyLocalStorage {
19553
19694
  * @param {boolean} data - The boolean value to store.
19554
19695
  */
19555
19696
  setBool(name, data) {
19697
+ this.#isProtectedDbKey(name);
19556
19698
  if (typeof name !== 'string' || !name.length)
19557
- throw new Error('Key must be a non-empty string.');
19558
- if (typeof data !== 'boolean') throw new Error('Value must be a boolean.');
19699
+ throw new TypeError('Key must be a non-empty string.');
19700
+ if (typeof data !== 'boolean') throw new TypeError('Value must be a boolean.');
19559
19701
  this.emit('setBool', name, data);
19560
19702
  return this.#localStorage.setItem(name, String(data));
19561
19703
  }
@@ -19568,7 +19710,7 @@ class TinyLocalStorage {
19568
19710
  */
19569
19711
  getBool(name) {
19570
19712
  if (typeof name !== 'string' || !name.length)
19571
- throw new Error('Key must be a non-empty string.');
19713
+ throw new TypeError('Key must be a non-empty string.');
19572
19714
 
19573
19715
  const value = this.#localStorage.getItem(name);
19574
19716
  if (typeof value === 'boolean') return value;
@@ -19587,7 +19729,7 @@ class TinyLocalStorage {
19587
19729
  */
19588
19730
  removeItem(name) {
19589
19731
  if (typeof name !== 'string' || !name.length)
19590
- throw new Error('Key must be a non-empty string.');
19732
+ throw new TypeError('Key must be a non-empty string.');
19591
19733
 
19592
19734
  this.emit('removeItem', name);
19593
19735
  return this.#localStorage.removeItem(name);
@@ -19624,6 +19766,7 @@ TinyLocalStorage.registerJsonType(
19624
19766
  decode: (value, decodeSpecialJson) =>
19625
19767
  new Map(value.data.map(([k, v]) => [k, decodeSpecialJson(v)])),
19626
19768
  },
19769
+ true,
19627
19770
  );
19628
19771
 
19629
19772
  // Set
@@ -19637,6 +19780,7 @@ TinyLocalStorage.registerJsonType(
19637
19780
  check: (value) => value.__set__,
19638
19781
  decode: (value, decodeSpecialJson) => new Set(value.data.map(decodeSpecialJson)),
19639
19782
  },
19783
+ true,
19640
19784
  );
19641
19785
 
19642
19786
  // Date
@@ -19650,6 +19794,7 @@ TinyLocalStorage.registerJsonType(
19650
19794
  check: (value) => value.__date__,
19651
19795
  decode: (value) => new Date(value.value),
19652
19796
  },
19797
+ true,
19653
19798
  );
19654
19799
 
19655
19800
  // Regex
@@ -19664,6 +19809,7 @@ TinyLocalStorage.registerJsonType(
19664
19809
  check: (value) => value.__regexp__,
19665
19810
  decode: (value) => new RegExp(value.source, value.flags),
19666
19811
  },
19812
+ true,
19667
19813
  );
19668
19814
 
19669
19815
  // Big Int
@@ -19677,6 +19823,7 @@ TinyLocalStorage.registerJsonType(
19677
19823
  check: (value) => value.__bigint__,
19678
19824
  decode: (value) => BigInt(value.value),
19679
19825
  },
19826
+ true,
19680
19827
  );
19681
19828
 
19682
19829
  // Symbol
@@ -19693,6 +19840,7 @@ TinyLocalStorage.registerJsonType(
19693
19840
  return key != null ? Symbol.for(key) : Symbol();
19694
19841
  },
19695
19842
  },
19843
+ true,
19696
19844
  );
19697
19845
 
19698
19846
  /* harmony default export */ const libs_TinyLocalStorage = (TinyLocalStorage);