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.
@@ -9,6 +9,22 @@ const customEncoders = new Map();
9
9
  /** @type {Map<any, DecodeFn>} */
10
10
  const customDecoders = new Map();
11
11
 
12
+ /** @type {Set<any>} */
13
+ const customTypesFreezed = new Set([
14
+ 'string',
15
+ 'boolean',
16
+ 'number',
17
+ 'object',
18
+ 'array',
19
+ String,
20
+ Boolean,
21
+ Number,
22
+ Object,
23
+ Array,
24
+ BigInt,
25
+ Symbol,
26
+ ]);
27
+
12
28
  /**
13
29
  * A function that encodes a value into a serializable JSON-compatible format.
14
30
  *
@@ -280,26 +296,49 @@ class TinyLocalStorage {
280
296
 
281
297
  ///////////////////////////////////////////////////
282
298
 
299
+ /**
300
+ * Checks whether a JSON-serializable type is already registered.
301
+ *
302
+ * @param {any} type - The type identifier, which can be a string (e.g., `"bigint"`, `"symbol"`)
303
+ * or a reference to a class/constructor function.
304
+ * @returns {boolean} True if the type has both an encoder and decoder registered.
305
+ */
306
+ static hasJsonType(type) {
307
+ return (customEncoders.has(type) && customDecoders.has(type)) || customTypesFreezed.has(type);
308
+ }
309
+
283
310
  /**
284
311
  * Registers a new JSON-serializable type with its encoder and decoder.
285
312
  *
286
- * @param {any} type - The type or primitive type name (e.g. `"bigint"`, `"symbol"`, etc).
287
- * @param {EncodeFn} encodeFn - The function that encodes the value.
288
- * @param {DecodeFn} decodeFn - An object with `check` and `decode` methods for restoring the value.
313
+ * @param {any} type - The type identifier, which can be a string (e.g., `"bigint"`, `"symbol"`)
314
+ * or a reference to a class/constructor function.
315
+ * @param {EncodeFn} encodeFn - A function that receives a value of this type and returns a JSON-safe representation.
316
+ * @param {DecodeFn} decodeFn - An object with `check(value)` to identify this type and `decode(value)` to restore it.
317
+ * @param {boolean} [freezeType=false] - If true, prevents this type from being unregistered later.
318
+ * @throws {Error} Throws an error if the type is frozen and cannot be registered.
289
319
  */
290
- static registerJsonType(type, encodeFn, decodeFn) {
320
+ static registerJsonType(type, encodeFn, decodeFn, freezeType = false) {
321
+ if (customTypesFreezed.has(type))
322
+ throw new Error(`Cannot register type "${type}" because it is frozen.`);
291
323
  customEncoders.set(type, encodeFn);
292
324
  customDecoders.set(type, decodeFn);
325
+ if (freezeType) customTypesFreezed.add(type);
293
326
  }
294
327
 
295
328
  /**
296
- * Removes a previously registered custom type from the encoding/decoding system.
329
+ * Unregisters a previously registered custom type from the encoding/decoding system.
297
330
  *
298
- * @param {string} type - The primitive name or constructor reference used in registration.
331
+ * @param {any} type - The primitive name or constructor reference used in the registration.
332
+ * @returns {boolean} Returns true if the type existed and was removed, or false if it wasn't found.
333
+ * @throws {Error} Throws an error if the type is frozen and cannot be unregistered.
299
334
  */
300
335
  static deleteJsonType(type) {
301
- customEncoders.delete(type);
302
- customDecoders.delete(type);
336
+ if (customTypesFreezed.has(type))
337
+ throw new Error(`Cannot remove type "${type}" because it is frozen.`);
338
+ let isDeleted = false;
339
+ if (customEncoders.delete(type)) isDeleted = true;
340
+ if (customDecoders.delete(type)) isDeleted = true;
341
+ return isDeleted;
303
342
  }
304
343
 
305
344
  //////////////////////////////////////////////////////
@@ -382,6 +421,12 @@ class TinyLocalStorage {
382
421
  /** @type {Storage} */
383
422
  #localStorage = window.localStorage;
384
423
 
424
+ /** @type {string|null} */
425
+ #dbKey = null;
426
+
427
+ /** @type {number} */
428
+ #version = 0;
429
+
385
430
  /** @type {(ev: StorageEvent) => any} */
386
431
  #storageEvent = (ev) => this.emit('storage', ev);
387
432
 
@@ -389,11 +434,84 @@ class TinyLocalStorage {
389
434
  * Initializes the TinyLocalStorage instance and sets up cross-tab sync.
390
435
  *
391
436
  * Adds listener for the native `storage` event to support tab synchronization.
437
+ * @param {string} [dbName] - Unique database name.
392
438
  */
393
- constructor() {
439
+ constructor(dbName) {
440
+ if (typeof dbName !== 'undefined' && typeof dbName !== 'string')
441
+ throw new TypeError('TinyLocalStorage: dbName must be a string if provided.');
442
+ if (typeof dbName === 'string') this.#dbKey = `LSDB::${dbName}`;
394
443
  window.addEventListener('storage', this.#storageEvent);
395
444
  }
396
445
 
446
+ /**
447
+ * Validates that a given key does not conflict with internal database keys.
448
+ *
449
+ * This method is used to prevent accidental overwriting of reserved `LSDB::` keys
450
+ * in `localStorage`, which are used internally by TinyLocalStorage for versioning
451
+ * and data management.
452
+ *
453
+ * @param {string} name - The key to validate before writing to localStorage.
454
+ * @throws {Error} If the key starts with `LSDB::`.
455
+ */
456
+ #isProtectedDbKey(name) {
457
+ if (typeof name === 'string' && name.startsWith('LSDB::'))
458
+ throw new Error(`TinyLocalStorage: Key "${name}" may conflict with reserved dbKeys.`);
459
+ }
460
+
461
+ /**
462
+ * Updates the version of the storage and triggers migration if needed.
463
+ *
464
+ * @param {number} version - Desired version of the database.
465
+ * @param {(oldVersion: number, newVersion: number) => void} onUpgrade - Callback to perform migration logic.
466
+ * @throws {Error} If the database key has not been initialized.
467
+ * @throws {TypeError} If `version` is not a valid positive number.
468
+ * @throws {TypeError} If `onUpgrade` is not a function.
469
+ */
470
+ updateStorageVersion(version, onUpgrade) {
471
+ if (typeof this.#dbKey !== 'string')
472
+ throw new Error(
473
+ 'TinyLocalStorage: Database key is not initialized. Set a valid dbName in the constructor.',
474
+ );
475
+ if (typeof version !== 'number' || Number.isNaN(version) || version < 1)
476
+ throw new TypeError('TinyLocalStorage: version must be a positive number.');
477
+ if (typeof onUpgrade !== 'function')
478
+ throw new TypeError('TinyLocalStorage: onUpgrade must be a function.');
479
+
480
+ // @ts-ignore
481
+ const savedVersion = parseInt(localStorage.getItem(this.#dbKey), 10) || 0;
482
+ if (typeof savedVersion !== 'number' || Number.isNaN(savedVersion) || savedVersion < 0)
483
+ throw new TypeError('TinyLocalStorage: saved version in localStorage is not a valid number.');
484
+
485
+ if (version < savedVersion)
486
+ throw new Error(
487
+ `TinyLocalStorage: Cannot downgrade database version from ${savedVersion} to ${version}.`,
488
+ );
489
+
490
+ if (version > savedVersion) {
491
+ onUpgrade(savedVersion, version);
492
+ localStorage.setItem(this.#dbKey, String(version));
493
+ this.#version = version;
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Gets the current database key used in localStorage.
499
+ *
500
+ * @returns {string|null} The database key, or null if not set.
501
+ */
502
+ getDbKey() {
503
+ return this.#dbKey;
504
+ }
505
+
506
+ /**
507
+ * Gets the current version of the database.
508
+ *
509
+ * @returns {number} The current version number.
510
+ */
511
+ getVersion() {
512
+ return this.#version;
513
+ }
514
+
397
515
  /**
398
516
  * Defines a custom storage interface (e.g. `sessionStorage`).
399
517
  *
@@ -401,7 +519,7 @@ class TinyLocalStorage {
401
519
  */
402
520
  setLocalStorage(localstorage) {
403
521
  if (!(localstorage instanceof Storage))
404
- throw new Error('Argument must be a valid instance of Storage.');
522
+ throw new TypeError('Argument must be a valid instance of Storage.');
405
523
  this.#localStorage = localstorage;
406
524
  }
407
525
 
@@ -423,7 +541,7 @@ class TinyLocalStorage {
423
541
  */
424
542
  #setJson(name, data) {
425
543
  if (typeof name !== 'string' || !name.length)
426
- throw new Error('Key must be a non-empty string.');
544
+ throw new TypeError('Key must be a non-empty string.');
427
545
  return TinyLocalStorage.encodeSpecialJson(data);
428
546
  }
429
547
 
@@ -436,13 +554,14 @@ class TinyLocalStorage {
436
554
  * @param {LocalStorageJsonValue} data - The data to be serialized and stored.
437
555
  */
438
556
  setJson(name, data) {
557
+ this.#isProtectedDbKey(name);
439
558
  if (
440
559
  !objChecker.isJsonObject(data) &&
441
560
  !Array.isArray(data) &&
442
561
  !(data instanceof Map) &&
443
562
  !(data instanceof Set)
444
563
  ) {
445
- throw new Error('The storage value is not a valid JSON-compatible structure.');
564
+ throw new TypeError('The storage value is not a valid JSON-compatible structure.');
446
565
  }
447
566
  const encoded = this.#setJson(name, data);
448
567
  this.emit('setJson', name, data);
@@ -460,7 +579,7 @@ class TinyLocalStorage {
460
579
  */
461
580
  #getJson(name, defaultData) {
462
581
  if (typeof name !== 'string' || !name.length)
463
- throw new Error('Key must be a non-empty string.');
582
+ throw new TypeError('Key must be a non-empty string.');
464
583
 
465
584
  const raw = this.#localStorage.getItem(name);
466
585
  const fallbackTypes = {
@@ -514,7 +633,8 @@ class TinyLocalStorage {
514
633
  * @param {Date} data
515
634
  */
516
635
  setDate(name, data) {
517
- if (!(data instanceof Date)) throw new Error('Value must be a Date.');
636
+ this.#isProtectedDbKey(name);
637
+ if (!(data instanceof Date)) throw new TypeError('Value must be a Date.');
518
638
  const encoded = this.#setJson(name, data);
519
639
  this.emit('setDate', name, data);
520
640
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -536,7 +656,8 @@ class TinyLocalStorage {
536
656
  * @param {RegExp} data
537
657
  */
538
658
  setRegExp(name, data) {
539
- if (!(data instanceof RegExp)) throw new Error('Value must be a RegExp.');
659
+ this.#isProtectedDbKey(name);
660
+ if (!(data instanceof RegExp)) throw new TypeError('Value must be a RegExp.');
540
661
  const encoded = this.#setJson(name, data);
541
662
  this.emit('setRegExp', name, data);
542
663
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -558,7 +679,8 @@ class TinyLocalStorage {
558
679
  * @param {bigint} data
559
680
  */
560
681
  setBigInt(name, data) {
561
- if (typeof data !== 'bigint') throw new Error('Value must be a BigInt.');
682
+ this.#isProtectedDbKey(name);
683
+ if (typeof data !== 'bigint') throw new TypeError('Value must be a BigInt.');
562
684
  const encoded = this.#setJson(name, data);
563
685
  this.emit('setBigInt', name, data);
564
686
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -581,7 +703,8 @@ class TinyLocalStorage {
581
703
  * @param {symbol} data
582
704
  */
583
705
  setSymbol(name, data) {
584
- if (typeof data !== 'symbol') throw new Error('Value must be a Symbol.');
706
+ this.#isProtectedDbKey(name);
707
+ if (typeof data !== 'symbol') throw new TypeError('Value must be a Symbol.');
585
708
  const encoded = this.#setJson(name, data);
586
709
  this.emit('setSymbol', name, data);
587
710
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -614,8 +737,9 @@ class TinyLocalStorage {
614
737
  * @param {any} data - The data to store.
615
738
  */
616
739
  setItem(name, data) {
740
+ this.#isProtectedDbKey(name);
617
741
  if (typeof name !== 'string' || !name.length)
618
- throw new Error('Key must be a non-empty string.');
742
+ throw new TypeError('Key must be a non-empty string.');
619
743
  this.emit('setItem', name, data);
620
744
  return this.#localStorage.setItem(name, data);
621
745
  }
@@ -628,7 +752,7 @@ class TinyLocalStorage {
628
752
  */
629
753
  getItem(name) {
630
754
  if (typeof name !== 'string' || !name.length)
631
- throw new Error('Key must be a non-empty string.');
755
+ throw new TypeError('Key must be a non-empty string.');
632
756
  return this.#localStorage.getItem(name);
633
757
  }
634
758
 
@@ -639,12 +763,19 @@ class TinyLocalStorage {
639
763
  * @param {string} data - The string to store.
640
764
  */
641
765
  setString(name, data) {
766
+ this.#isProtectedDbKey(name);
642
767
  if (typeof name !== 'string' || !name.length)
643
- throw new Error('Key must be a non-empty string.');
644
- if (typeof data !== 'string') throw new Error('Value must be a string.');
768
+ throw new TypeError('Key must be a non-empty string.');
769
+ if (typeof data !== 'string') throw new TypeError('Value must be a string.');
645
770
 
646
771
  this.emit('setString', name, data);
647
- return this.#localStorage.setItem(name, data);
772
+ return this.#localStorage.setItem(
773
+ name,
774
+ JSON.stringify({
775
+ __string__: true,
776
+ value: data,
777
+ }),
778
+ );
648
779
  }
649
780
 
650
781
  /**
@@ -655,8 +786,17 @@ class TinyLocalStorage {
655
786
  */
656
787
  getString(name) {
657
788
  if (typeof name !== 'string' || !name.length)
658
- throw new Error('Key must be a non-empty string.');
789
+ throw new TypeError('Key must be a non-empty string.');
659
790
  let value = this.#localStorage.getItem(name);
791
+ try {
792
+ /** @type {{ value: string; __string__: boolean }} */
793
+ // @ts-ignore
794
+ value = JSON.parse(value);
795
+ if (!objChecker.isJsonObject(value) || !value.__string__ || typeof value.value !== 'string') return null;
796
+ value = value.value;
797
+ } catch {
798
+ value = null;
799
+ }
660
800
  if (typeof value === 'string') return value;
661
801
  return null;
662
802
  }
@@ -668,9 +808,10 @@ class TinyLocalStorage {
668
808
  * @param {number} data - The number to store.
669
809
  */
670
810
  setNumber(name, data) {
811
+ this.#isProtectedDbKey(name);
671
812
  if (typeof name !== 'string' || !name.length)
672
- throw new Error('Key must be a non-empty string.');
673
- if (typeof data !== 'number') throw new Error('Value must be a number.');
813
+ throw new TypeError('Key must be a non-empty string.');
814
+ if (typeof data !== 'number') throw new TypeError('Value must be a number.');
674
815
  this.emit('setNumber', name, data);
675
816
  return this.#localStorage.setItem(name, String(data));
676
817
  }
@@ -683,7 +824,7 @@ class TinyLocalStorage {
683
824
  */
684
825
  getNumber(name) {
685
826
  if (typeof name !== 'string' || !name.length)
686
- throw new Error('Key must be a non-empty string.');
827
+ throw new TypeError('Key must be a non-empty string.');
687
828
 
688
829
  /** @type {number|string|null} */
689
830
  let number = this.#localStorage.getItem(name);
@@ -702,9 +843,10 @@ class TinyLocalStorage {
702
843
  * @param {boolean} data - The boolean value to store.
703
844
  */
704
845
  setBool(name, data) {
846
+ this.#isProtectedDbKey(name);
705
847
  if (typeof name !== 'string' || !name.length)
706
- throw new Error('Key must be a non-empty string.');
707
- if (typeof data !== 'boolean') throw new Error('Value must be a boolean.');
848
+ throw new TypeError('Key must be a non-empty string.');
849
+ if (typeof data !== 'boolean') throw new TypeError('Value must be a boolean.');
708
850
  this.emit('setBool', name, data);
709
851
  return this.#localStorage.setItem(name, String(data));
710
852
  }
@@ -717,7 +859,7 @@ class TinyLocalStorage {
717
859
  */
718
860
  getBool(name) {
719
861
  if (typeof name !== 'string' || !name.length)
720
- throw new Error('Key must be a non-empty string.');
862
+ throw new TypeError('Key must be a non-empty string.');
721
863
 
722
864
  const value = this.#localStorage.getItem(name);
723
865
  if (typeof value === 'boolean') return value;
@@ -736,7 +878,7 @@ class TinyLocalStorage {
736
878
  */
737
879
  removeItem(name) {
738
880
  if (typeof name !== 'string' || !name.length)
739
- throw new Error('Key must be a non-empty string.');
881
+ throw new TypeError('Key must be a non-empty string.');
740
882
 
741
883
  this.emit('removeItem', name);
742
884
  return this.#localStorage.removeItem(name);
@@ -773,6 +915,7 @@ TinyLocalStorage.registerJsonType(
773
915
  decode: (value, decodeSpecialJson) =>
774
916
  new Map(value.data.map(([k, v]) => [k, decodeSpecialJson(v)])),
775
917
  },
918
+ true,
776
919
  );
777
920
 
778
921
  // Set
@@ -786,6 +929,7 @@ TinyLocalStorage.registerJsonType(
786
929
  check: (value) => value.__set__,
787
930
  decode: (value, decodeSpecialJson) => new Set(value.data.map(decodeSpecialJson)),
788
931
  },
932
+ true,
789
933
  );
790
934
 
791
935
  // Date
@@ -799,6 +943,7 @@ TinyLocalStorage.registerJsonType(
799
943
  check: (value) => value.__date__,
800
944
  decode: (value) => new Date(value.value),
801
945
  },
946
+ true,
802
947
  );
803
948
 
804
949
  // Regex
@@ -813,6 +958,7 @@ TinyLocalStorage.registerJsonType(
813
958
  check: (value) => value.__regexp__,
814
959
  decode: (value) => new RegExp(value.source, value.flags),
815
960
  },
961
+ true,
816
962
  );
817
963
 
818
964
  // Big Int
@@ -826,6 +972,7 @@ TinyLocalStorage.registerJsonType(
826
972
  check: (value) => value.__bigint__,
827
973
  decode: (value) => BigInt(value.value),
828
974
  },
975
+ true,
829
976
  );
830
977
 
831
978
  // Symbol
@@ -842,6 +989,7 @@ TinyLocalStorage.registerJsonType(
842
989
  return key != null ? Symbol.for(key) : Symbol();
843
990
  },
844
991
  },
992
+ true,
845
993
  );
846
994
 
847
995
  module.exports = TinyLocalStorage;
@@ -101,22 +101,42 @@ export type LocalStorageJsonValue = (Record<string | number | symbol, any> | any
101
101
  * This class is suitable for applications that require structured persistence in the browser.
102
102
  */
103
103
  declare class TinyLocalStorage {
104
+ /**
105
+ * Checks whether a JSON-serializable type is already registered.
106
+ *
107
+ * @param {any} type - The type identifier, which can be a string (e.g., `"bigint"`, `"symbol"`)
108
+ * or a reference to a class/constructor function.
109
+ * @returns {boolean} True if the type has both an encoder and decoder registered.
110
+ */
111
+ static hasJsonType(type: any): boolean;
104
112
  /**
105
113
  * Registers a new JSON-serializable type with its encoder and decoder.
106
114
  *
107
- * @param {any} type - The type or primitive type name (e.g. `"bigint"`, `"symbol"`, etc).
108
- * @param {EncodeFn} encodeFn - The function that encodes the value.
109
- * @param {DecodeFn} decodeFn - An object with `check` and `decode` methods for restoring the value.
115
+ * @param {any} type - The type identifier, which can be a string (e.g., `"bigint"`, `"symbol"`)
116
+ * or a reference to a class/constructor function.
117
+ * @param {EncodeFn} encodeFn - A function that receives a value of this type and returns a JSON-safe representation.
118
+ * @param {DecodeFn} decodeFn - An object with `check(value)` to identify this type and `decode(value)` to restore it.
119
+ * @param {boolean} [freezeType=false] - If true, prevents this type from being unregistered later.
120
+ * @throws {Error} Throws an error if the type is frozen and cannot be registered.
110
121
  */
111
- static registerJsonType(type: any, encodeFn: EncodeFn, decodeFn: DecodeFn): void;
122
+ static registerJsonType(type: any, encodeFn: EncodeFn, decodeFn: DecodeFn, freezeType?: boolean): void;
112
123
  /**
113
- * Removes a previously registered custom type from the encoding/decoding system.
124
+ * Unregisters a previously registered custom type from the encoding/decoding system.
114
125
  *
115
- * @param {string} type - The primitive name or constructor reference used in registration.
126
+ * @param {any} type - The primitive name or constructor reference used in the registration.
127
+ * @returns {boolean} Returns true if the type existed and was removed, or false if it wasn't found.
128
+ * @throws {Error} Throws an error if the type is frozen and cannot be unregistered.
116
129
  */
117
- static deleteJsonType(type: string): void;
130
+ static deleteJsonType(type: any): boolean;
118
131
  static encodeSpecialJson(value: any): any;
119
132
  static decodeSpecialJson(value: any): any;
133
+ /**
134
+ * Initializes the TinyLocalStorage instance and sets up cross-tab sync.
135
+ *
136
+ * Adds listener for the native `storage` event to support tab synchronization.
137
+ * @param {string} [dbName] - Unique database name.
138
+ */
139
+ constructor(dbName?: string);
120
140
  /**
121
141
  * Enables or disables throwing an error when the maximum number of listeners is exceeded.
122
142
  *
@@ -245,6 +265,28 @@ declare class TinyLocalStorage {
245
265
  * @returns {number} The maximum number of listeners.
246
266
  */
247
267
  getMaxListeners(): number;
268
+ /**
269
+ * Updates the version of the storage and triggers migration if needed.
270
+ *
271
+ * @param {number} version - Desired version of the database.
272
+ * @param {(oldVersion: number, newVersion: number) => void} onUpgrade - Callback to perform migration logic.
273
+ * @throws {Error} If the database key has not been initialized.
274
+ * @throws {TypeError} If `version` is not a valid positive number.
275
+ * @throws {TypeError} If `onUpgrade` is not a function.
276
+ */
277
+ updateStorageVersion(version: number, onUpgrade: (oldVersion: number, newVersion: number) => void): void;
278
+ /**
279
+ * Gets the current database key used in localStorage.
280
+ *
281
+ * @returns {string|null} The database key, or null if not set.
282
+ */
283
+ getDbKey(): string | null;
284
+ /**
285
+ * Gets the current version of the database.
286
+ *
287
+ * @returns {number} The current version number.
288
+ */
289
+ getVersion(): number;
248
290
  /**
249
291
  * Defines a custom storage interface (e.g. `sessionStorage`).
250
292
  *