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.
@@ -4,6 +4,21 @@ import TinyEvents from './TinyEvents.mjs';
4
4
  const customEncoders = new Map();
5
5
  /** @type {Map<any, DecodeFn>} */
6
6
  const customDecoders = new Map();
7
+ /** @type {Set<any>} */
8
+ const customTypesFreezed = new Set([
9
+ 'string',
10
+ 'boolean',
11
+ 'number',
12
+ 'object',
13
+ 'array',
14
+ String,
15
+ Boolean,
16
+ Number,
17
+ Object,
18
+ Array,
19
+ BigInt,
20
+ Symbol,
21
+ ]);
7
22
  /**
8
23
  * A function that encodes a value into a serializable JSON-compatible format.
9
24
  *
@@ -243,25 +258,50 @@ class TinyLocalStorage {
243
258
  return this.#events.getMaxListeners();
244
259
  }
245
260
  ///////////////////////////////////////////////////
261
+ /**
262
+ * Checks whether a JSON-serializable type is already registered.
263
+ *
264
+ * @param {any} type - The type identifier, which can be a string (e.g., `"bigint"`, `"symbol"`)
265
+ * or a reference to a class/constructor function.
266
+ * @returns {boolean} True if the type has both an encoder and decoder registered.
267
+ */
268
+ static hasJsonType(type) {
269
+ return (customEncoders.has(type) && customDecoders.has(type)) || customTypesFreezed.has(type);
270
+ }
246
271
  /**
247
272
  * Registers a new JSON-serializable type with its encoder and decoder.
248
273
  *
249
- * @param {any} type - The type or primitive type name (e.g. `"bigint"`, `"symbol"`, etc).
250
- * @param {EncodeFn} encodeFn - The function that encodes the value.
251
- * @param {DecodeFn} decodeFn - An object with `check` and `decode` methods for restoring the value.
274
+ * @param {any} type - The type identifier, which can be a string (e.g., `"bigint"`, `"symbol"`)
275
+ * or a reference to a class/constructor function.
276
+ * @param {EncodeFn} encodeFn - A function that receives a value of this type and returns a JSON-safe representation.
277
+ * @param {DecodeFn} decodeFn - An object with `check(value)` to identify this type and `decode(value)` to restore it.
278
+ * @param {boolean} [freezeType=false] - If true, prevents this type from being unregistered later.
279
+ * @throws {Error} Throws an error if the type is frozen and cannot be registered.
252
280
  */
253
- static registerJsonType(type, encodeFn, decodeFn) {
281
+ static registerJsonType(type, encodeFn, decodeFn, freezeType = false) {
282
+ if (customTypesFreezed.has(type))
283
+ throw new Error(`Cannot register type "${type}" because it is frozen.`);
254
284
  customEncoders.set(type, encodeFn);
255
285
  customDecoders.set(type, decodeFn);
286
+ if (freezeType)
287
+ customTypesFreezed.add(type);
256
288
  }
257
289
  /**
258
- * Removes a previously registered custom type from the encoding/decoding system.
290
+ * Unregisters a previously registered custom type from the encoding/decoding system.
259
291
  *
260
- * @param {string} type - The primitive name or constructor reference used in registration.
292
+ * @param {any} type - The primitive name or constructor reference used in the registration.
293
+ * @returns {boolean} Returns true if the type existed and was removed, or false if it wasn't found.
294
+ * @throws {Error} Throws an error if the type is frozen and cannot be unregistered.
261
295
  */
262
296
  static deleteJsonType(type) {
263
- customEncoders.delete(type);
264
- customDecoders.delete(type);
297
+ if (customTypesFreezed.has(type))
298
+ throw new Error(`Cannot remove type "${type}" because it is frozen.`);
299
+ let isDeleted = false;
300
+ if (customEncoders.delete(type))
301
+ isDeleted = true;
302
+ if (customDecoders.delete(type))
303
+ isDeleted = true;
304
+ return isDeleted;
265
305
  }
266
306
  //////////////////////////////////////////////////////
267
307
  /**
@@ -335,16 +375,83 @@ class TinyLocalStorage {
335
375
  //////////////////////////////////////////////////////
336
376
  /** @type {Storage} */
337
377
  #localStorage = window.localStorage;
378
+ /** @type {string|null} */
379
+ #dbKey = null;
380
+ /** @type {number} */
381
+ #version = 0;
338
382
  /** @type {(ev: StorageEvent) => any} */
339
383
  #storageEvent = (ev) => this.emit('storage', ev);
340
384
  /**
341
385
  * Initializes the TinyLocalStorage instance and sets up cross-tab sync.
342
386
  *
343
387
  * Adds listener for the native `storage` event to support tab synchronization.
388
+ * @param {string} [dbName] - Unique database name.
344
389
  */
345
- constructor() {
390
+ constructor(dbName) {
391
+ if (typeof dbName !== 'undefined' && typeof dbName !== 'string')
392
+ throw new TypeError('TinyLocalStorage: dbName must be a string if provided.');
393
+ if (typeof dbName === 'string')
394
+ this.#dbKey = `LSDB::${dbName}`;
346
395
  window.addEventListener('storage', this.#storageEvent);
347
396
  }
397
+ /**
398
+ * Validates that a given key does not conflict with internal database keys.
399
+ *
400
+ * This method is used to prevent accidental overwriting of reserved `LSDB::` keys
401
+ * in `localStorage`, which are used internally by TinyLocalStorage for versioning
402
+ * and data management.
403
+ *
404
+ * @param {string} name - The key to validate before writing to localStorage.
405
+ * @throws {Error} If the key starts with `LSDB::`.
406
+ */
407
+ #isProtectedDbKey(name) {
408
+ if (typeof name === 'string' && name.startsWith('LSDB::'))
409
+ throw new Error(`TinyLocalStorage: Key "${name}" may conflict with reserved dbKeys.`);
410
+ }
411
+ /**
412
+ * Updates the version of the storage and triggers migration if needed.
413
+ *
414
+ * @param {number} version - Desired version of the database.
415
+ * @param {(oldVersion: number, newVersion: number) => void} onUpgrade - Callback to perform migration logic.
416
+ * @throws {Error} If the database key has not been initialized.
417
+ * @throws {TypeError} If `version` is not a valid positive number.
418
+ * @throws {TypeError} If `onUpgrade` is not a function.
419
+ */
420
+ updateStorageVersion(version, onUpgrade) {
421
+ if (typeof this.#dbKey !== 'string')
422
+ throw new Error('TinyLocalStorage: Database key is not initialized. Set a valid dbName in the constructor.');
423
+ if (typeof version !== 'number' || Number.isNaN(version) || version < 1)
424
+ throw new TypeError('TinyLocalStorage: version must be a positive number.');
425
+ if (typeof onUpgrade !== 'function')
426
+ throw new TypeError('TinyLocalStorage: onUpgrade must be a function.');
427
+ // @ts-ignore
428
+ const savedVersion = parseInt(localStorage.getItem(this.#dbKey), 10) || 0;
429
+ if (typeof savedVersion !== 'number' || Number.isNaN(savedVersion) || savedVersion < 0)
430
+ throw new TypeError('TinyLocalStorage: saved version in localStorage is not a valid number.');
431
+ if (version < savedVersion)
432
+ throw new Error(`TinyLocalStorage: Cannot downgrade database version from ${savedVersion} to ${version}.`);
433
+ if (version > savedVersion) {
434
+ onUpgrade(savedVersion, version);
435
+ localStorage.setItem(this.#dbKey, String(version));
436
+ this.#version = version;
437
+ }
438
+ }
439
+ /**
440
+ * Gets the current database key used in localStorage.
441
+ *
442
+ * @returns {string|null} The database key, or null if not set.
443
+ */
444
+ getDbKey() {
445
+ return this.#dbKey;
446
+ }
447
+ /**
448
+ * Gets the current version of the database.
449
+ *
450
+ * @returns {number} The current version number.
451
+ */
452
+ getVersion() {
453
+ return this.#version;
454
+ }
348
455
  /**
349
456
  * Defines a custom storage interface (e.g. `sessionStorage`).
350
457
  *
@@ -352,7 +459,7 @@ class TinyLocalStorage {
352
459
  */
353
460
  setLocalStorage(localstorage) {
354
461
  if (!(localstorage instanceof Storage))
355
- throw new Error('Argument must be a valid instance of Storage.');
462
+ throw new TypeError('Argument must be a valid instance of Storage.');
356
463
  this.#localStorage = localstorage;
357
464
  }
358
465
  /**
@@ -372,7 +479,7 @@ class TinyLocalStorage {
372
479
  */
373
480
  #setJson(name, data) {
374
481
  if (typeof name !== 'string' || !name.length)
375
- throw new Error('Key must be a non-empty string.');
482
+ throw new TypeError('Key must be a non-empty string.');
376
483
  return TinyLocalStorage.encodeSpecialJson(data);
377
484
  }
378
485
  /**
@@ -384,11 +491,12 @@ class TinyLocalStorage {
384
491
  * @param {LocalStorageJsonValue} data - The data to be serialized and stored.
385
492
  */
386
493
  setJson(name, data) {
494
+ this.#isProtectedDbKey(name);
387
495
  if (!isJsonObject(data) &&
388
496
  !Array.isArray(data) &&
389
497
  !(data instanceof Map) &&
390
498
  !(data instanceof Set)) {
391
- throw new Error('The storage value is not a valid JSON-compatible structure.');
499
+ throw new TypeError('The storage value is not a valid JSON-compatible structure.');
392
500
  }
393
501
  const encoded = this.#setJson(name, data);
394
502
  this.emit('setJson', name, data);
@@ -405,7 +513,7 @@ class TinyLocalStorage {
405
513
  */
406
514
  #getJson(name, defaultData) {
407
515
  if (typeof name !== 'string' || !name.length)
408
- throw new Error('Key must be a non-empty string.');
516
+ throw new TypeError('Key must be a non-empty string.');
409
517
  const raw = this.#localStorage.getItem(name);
410
518
  const fallbackTypes = {
411
519
  obj: () => ({}),
@@ -451,8 +559,9 @@ class TinyLocalStorage {
451
559
  * @param {Date} data
452
560
  */
453
561
  setDate(name, data) {
562
+ this.#isProtectedDbKey(name);
454
563
  if (!(data instanceof Date))
455
- throw new Error('Value must be a Date.');
564
+ throw new TypeError('Value must be a Date.');
456
565
  const encoded = this.#setJson(name, data);
457
566
  this.emit('setDate', name, data);
458
567
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -472,8 +581,9 @@ class TinyLocalStorage {
472
581
  * @param {RegExp} data
473
582
  */
474
583
  setRegExp(name, data) {
584
+ this.#isProtectedDbKey(name);
475
585
  if (!(data instanceof RegExp))
476
- throw new Error('Value must be a RegExp.');
586
+ throw new TypeError('Value must be a RegExp.');
477
587
  const encoded = this.#setJson(name, data);
478
588
  this.emit('setRegExp', name, data);
479
589
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -493,8 +603,9 @@ class TinyLocalStorage {
493
603
  * @param {bigint} data
494
604
  */
495
605
  setBigInt(name, data) {
606
+ this.#isProtectedDbKey(name);
496
607
  if (typeof data !== 'bigint')
497
- throw new Error('Value must be a BigInt.');
608
+ throw new TypeError('Value must be a BigInt.');
498
609
  const encoded = this.#setJson(name, data);
499
610
  this.emit('setBigInt', name, data);
500
611
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -515,8 +626,9 @@ class TinyLocalStorage {
515
626
  * @param {symbol} data
516
627
  */
517
628
  setSymbol(name, data) {
629
+ this.#isProtectedDbKey(name);
518
630
  if (typeof data !== 'symbol')
519
- throw new Error('Value must be a Symbol.');
631
+ throw new TypeError('Value must be a Symbol.');
520
632
  const encoded = this.#setJson(name, data);
521
633
  this.emit('setSymbol', name, data);
522
634
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -546,8 +658,9 @@ class TinyLocalStorage {
546
658
  * @param {any} data - The data to store.
547
659
  */
548
660
  setItem(name, data) {
661
+ this.#isProtectedDbKey(name);
549
662
  if (typeof name !== 'string' || !name.length)
550
- throw new Error('Key must be a non-empty string.');
663
+ throw new TypeError('Key must be a non-empty string.');
551
664
  this.emit('setItem', name, data);
552
665
  return this.#localStorage.setItem(name, data);
553
666
  }
@@ -559,7 +672,7 @@ class TinyLocalStorage {
559
672
  */
560
673
  getItem(name) {
561
674
  if (typeof name !== 'string' || !name.length)
562
- throw new Error('Key must be a non-empty string.');
675
+ throw new TypeError('Key must be a non-empty string.');
563
676
  return this.#localStorage.getItem(name);
564
677
  }
565
678
  /**
@@ -569,12 +682,16 @@ class TinyLocalStorage {
569
682
  * @param {string} data - The string to store.
570
683
  */
571
684
  setString(name, data) {
685
+ this.#isProtectedDbKey(name);
572
686
  if (typeof name !== 'string' || !name.length)
573
- throw new Error('Key must be a non-empty string.');
687
+ throw new TypeError('Key must be a non-empty string.');
574
688
  if (typeof data !== 'string')
575
- throw new Error('Value must be a string.');
689
+ throw new TypeError('Value must be a string.');
576
690
  this.emit('setString', name, data);
577
- return this.#localStorage.setItem(name, data);
691
+ return this.#localStorage.setItem(name, JSON.stringify({
692
+ __string__: true,
693
+ value: data,
694
+ }));
578
695
  }
579
696
  /**
580
697
  * Retrieves a string value from `localStorage`.
@@ -584,8 +701,19 @@ class TinyLocalStorage {
584
701
  */
585
702
  getString(name) {
586
703
  if (typeof name !== 'string' || !name.length)
587
- throw new Error('Key must be a non-empty string.');
704
+ throw new TypeError('Key must be a non-empty string.');
588
705
  let value = this.#localStorage.getItem(name);
706
+ try {
707
+ /** @type {{ value: string; __string__: boolean }} */
708
+ // @ts-ignore
709
+ value = JSON.parse(value);
710
+ if (!isJsonObject(value) || !value.__string__ || typeof value.value !== 'string')
711
+ return null;
712
+ value = value.value;
713
+ }
714
+ catch {
715
+ value = null;
716
+ }
589
717
  if (typeof value === 'string')
590
718
  return value;
591
719
  return null;
@@ -597,10 +725,11 @@ class TinyLocalStorage {
597
725
  * @param {number} data - The number to store.
598
726
  */
599
727
  setNumber(name, data) {
728
+ this.#isProtectedDbKey(name);
600
729
  if (typeof name !== 'string' || !name.length)
601
- throw new Error('Key must be a non-empty string.');
730
+ throw new TypeError('Key must be a non-empty string.');
602
731
  if (typeof data !== 'number')
603
- throw new Error('Value must be a number.');
732
+ throw new TypeError('Value must be a number.');
604
733
  this.emit('setNumber', name, data);
605
734
  return this.#localStorage.setItem(name, String(data));
606
735
  }
@@ -612,7 +741,7 @@ class TinyLocalStorage {
612
741
  */
613
742
  getNumber(name) {
614
743
  if (typeof name !== 'string' || !name.length)
615
- throw new Error('Key must be a non-empty string.');
744
+ throw new TypeError('Key must be a non-empty string.');
616
745
  /** @type {number|string|null} */
617
746
  let number = this.#localStorage.getItem(name);
618
747
  if (typeof number === 'number')
@@ -631,10 +760,11 @@ class TinyLocalStorage {
631
760
  * @param {boolean} data - The boolean value to store.
632
761
  */
633
762
  setBool(name, data) {
763
+ this.#isProtectedDbKey(name);
634
764
  if (typeof name !== 'string' || !name.length)
635
- throw new Error('Key must be a non-empty string.');
765
+ throw new TypeError('Key must be a non-empty string.');
636
766
  if (typeof data !== 'boolean')
637
- throw new Error('Value must be a boolean.');
767
+ throw new TypeError('Value must be a boolean.');
638
768
  this.emit('setBool', name, data);
639
769
  return this.#localStorage.setItem(name, String(data));
640
770
  }
@@ -646,7 +776,7 @@ class TinyLocalStorage {
646
776
  */
647
777
  getBool(name) {
648
778
  if (typeof name !== 'string' || !name.length)
649
- throw new Error('Key must be a non-empty string.');
779
+ throw new TypeError('Key must be a non-empty string.');
650
780
  const value = this.#localStorage.getItem(name);
651
781
  if (typeof value === 'boolean')
652
782
  return value;
@@ -665,7 +795,7 @@ class TinyLocalStorage {
665
795
  */
666
796
  removeItem(name) {
667
797
  if (typeof name !== 'string' || !name.length)
668
- throw new Error('Key must be a non-empty string.');
798
+ throw new TypeError('Key must be a non-empty string.');
669
799
  this.emit('removeItem', name);
670
800
  return this.#localStorage.removeItem(name);
671
801
  }
@@ -692,7 +822,7 @@ TinyLocalStorage.registerJsonType(Map, (value, encodeSpecialJson) => ({
692
822
  check: (value) => value.__map__,
693
823
  /** @param {{ data: any[] }} value */
694
824
  decode: (value, decodeSpecialJson) => new Map(value.data.map(([k, v]) => [k, decodeSpecialJson(v)])),
695
- });
825
+ }, true);
696
826
  // Set
697
827
  TinyLocalStorage.registerJsonType(Set, (value, encodeSpecialJson) => ({
698
828
  __set__: true,
@@ -700,7 +830,7 @@ TinyLocalStorage.registerJsonType(Set, (value, encodeSpecialJson) => ({
700
830
  }), {
701
831
  check: (value) => value.__set__,
702
832
  decode: (value, decodeSpecialJson) => new Set(value.data.map(decodeSpecialJson)),
703
- });
833
+ }, true);
704
834
  // Date
705
835
  TinyLocalStorage.registerJsonType(Date, (value) => ({
706
836
  __date__: true,
@@ -708,7 +838,7 @@ TinyLocalStorage.registerJsonType(Date, (value) => ({
708
838
  }), {
709
839
  check: (value) => value.__date__,
710
840
  decode: (value) => new Date(value.value),
711
- });
841
+ }, true);
712
842
  // Regex
713
843
  TinyLocalStorage.registerJsonType(RegExp, (value) => ({
714
844
  __regexp__: true,
@@ -717,7 +847,7 @@ TinyLocalStorage.registerJsonType(RegExp, (value) => ({
717
847
  }), {
718
848
  check: (value) => value.__regexp__,
719
849
  decode: (value) => new RegExp(value.source, value.flags),
720
- });
850
+ }, true);
721
851
  // Big Int
722
852
  TinyLocalStorage.registerJsonType('bigint', (value) => ({
723
853
  __bigint__: true,
@@ -725,7 +855,7 @@ TinyLocalStorage.registerJsonType('bigint', (value) => ({
725
855
  }), {
726
856
  check: (value) => value.__bigint__,
727
857
  decode: (value) => BigInt(value.value),
728
- });
858
+ }, true);
729
859
  // Symbol
730
860
  TinyLocalStorage.registerJsonType('symbol', (value) => ({
731
861
  __symbol__: true,
@@ -736,5 +866,5 @@ TinyLocalStorage.registerJsonType('symbol', (value) => ({
736
866
  const key = value.key;
737
867
  return key != null ? Symbol.for(key) : Symbol();
738
868
  },
739
- });
869
+ }, true);
740
870
  export default TinyLocalStorage;
@@ -32,6 +32,59 @@ console.log(storage.getDate('today') instanceof Date); // true
32
32
 
33
33
  ---
34
34
 
35
+ ## 🏁 `constructor(dbName?: string)`
36
+
37
+ Initializes the `TinyLocalStorage` instance and sets up cross-tab synchronization.
38
+
39
+ ### 🔧 Parameters
40
+
41
+ * `dbName` (optional, `string`) – A unique name for the database. It becomes the base key used internally in `localStorage`.
42
+
43
+ ### 📡 Behavior
44
+
45
+ * Automatically adds a `storage` event listener to support syncing across browser tabs.
46
+
47
+ ---
48
+
49
+ ## 🔁 `updateStorageVersion(version: number, onUpgrade: (oldVersion: number, newVersion: number) => void)`
50
+
51
+ Updates the version of the database. If the new version is higher than the current one, it triggers the provided migration callback.
52
+
53
+ ### 🔧 Parameters
54
+
55
+ * `version` (`number`) – The desired new version of the database. Must be a positive integer.
56
+ * `onUpgrade` (`function`) – A callback function executed during upgrade. Receives `oldVersion` and `newVersion` as arguments.
57
+
58
+ ### ⚠️ Throws
59
+
60
+ * `Error` – If the database key hasn't been initialized via the constructor.
61
+ * `TypeError` – If `version` is invalid (not a number, NaN, or < 1).
62
+ * `TypeError` – If `onUpgrade` is not a function.
63
+ * `TypeError` – If the saved version in `localStorage` is invalid.
64
+ * `Error` – If the provided `version` is lower than the stored version (downgrade is not supported).
65
+
66
+ ---
67
+
68
+ ## 🔑 `getDbKey(): string | null`
69
+
70
+ Returns the current internal database key used in `localStorage`.
71
+
72
+ ### 🔙 Returns
73
+
74
+ * `string | null` – The full storage key prefix (e.g. `LSDB::yourDbName`), or `null` if uninitialized.
75
+
76
+ ---
77
+
78
+ ## 🧮 `getVersion(): number`
79
+
80
+ Returns the current active version of the storage system.
81
+
82
+ ### 🔙 Returns
83
+
84
+ * `number` – The version number currently in use.
85
+
86
+ ---
87
+
35
88
  ## 📦 Storage Methods
36
89
 
37
90
  ### `setJson(key, data)`
@@ -243,7 +296,7 @@ Returns `true` if the browser supports the configured storage backend.
243
296
 
244
297
  ## 🧩 Custom Type Registration
245
298
 
246
- ### `registerJsonType(type, encodeFn, decodeFn)`
299
+ ### `registerJsonType(type, encodeFn, decodeFn, freezeType?)`
247
300
 
248
301
  Registers support for custom types with encoder/decoder logic.
249
302
 
@@ -254,10 +307,15 @@ registerJsonType(
254
307
  decodeFn: {
255
308
  check: (value: any) => boolean,
256
309
  decode: (value: any, decodeSpecialJson: DecodeFn) => any
257
- }
310
+ },
311
+ freezeType?: boolean // default: false
258
312
  ): void
259
313
  ```
260
314
 
315
+ Registers a custom type by associating it with encoder and decoder logic. If `freezeType` is set to `true`, the type becomes immutable and cannot be unregistered or overwritten later.
316
+
317
+ > ⚠️ Throws an error if trying to register a type that is already frozen.
318
+
261
319
  ---
262
320
 
263
321
  ### `deleteJsonType(type)`
@@ -265,10 +323,24 @@ registerJsonType(
265
323
  Unregisters a previously registered custom type.
266
324
 
267
325
  ```ts
268
- deleteJsonType(type: any): void
326
+ deleteJsonType(type: any): boolean
327
+ ```
328
+
329
+ Removes the encoder and decoder associated with the given type. Returns `true` if the type was found and removed, or `false` if it was not registered.
330
+
331
+ > ❌ Throws an error if the type was marked as frozen via `freezeType`.
332
+
333
+ ---
334
+
335
+ ### `hasJsonType(type)`
336
+
337
+ Checks whether a type is currently registered for custom encoding/decoding.
338
+
339
+ ```ts
340
+ hasJsonType(type: any): boolean
269
341
  ```
270
342
 
271
- Removes the encoder and decoder associated with the given type. Useful for cleanup or reconfiguration.
343
+ Returns `true` if the type has both encoder and decoder registered, otherwise `false`.
272
344
 
273
345
  ---
274
346
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-essentials",
3
- "version": "1.19.1",
3
+ "version": "1.19.3",
4
4
  "description": "Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.",
5
5
  "scripts": {
6
6
  "test": "npm run test:mjs && npm run test:cjs && npm run test:js",