tiny-essentials 1.19.2 → 1.20.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.
Files changed (34) hide show
  1. package/dist/v1/TinyEssentials.js +1009 -22
  2. package/dist/v1/TinyEssentials.min.js +1 -1
  3. package/dist/v1/TinyIframeEvents.js +854 -0
  4. package/dist/v1/TinyIframeEvents.min.js +1 -0
  5. package/dist/v1/TinyLocalStorage.js +109 -21
  6. package/dist/v1/TinyLocalStorage.min.js +1 -1
  7. package/dist/v1/TinyNewWinEvents.js +888 -0
  8. package/dist/v1/TinyNewWinEvents.min.js +1 -0
  9. package/dist/v1/TinyUploadClicker.js +1022 -37
  10. package/dist/v1/TinyUploadClicker.min.js +1 -1
  11. package/dist/v1/build/TinyIframeEvents.cjs +7 -0
  12. package/dist/v1/build/TinyIframeEvents.d.mts +3 -0
  13. package/dist/v1/build/TinyIframeEvents.mjs +2 -0
  14. package/dist/v1/build/TinyNewWinEvents.cjs +7 -0
  15. package/dist/v1/build/TinyNewWinEvents.d.mts +3 -0
  16. package/dist/v1/build/TinyNewWinEvents.mjs +2 -0
  17. package/dist/v1/index.cjs +4 -0
  18. package/dist/v1/index.d.mts +3 -1
  19. package/dist/v1/index.mjs +3 -1
  20. package/dist/v1/libs/TinyIframeEvents.cjs +409 -0
  21. package/dist/v1/libs/TinyIframeEvents.d.mts +193 -0
  22. package/dist/v1/libs/TinyIframeEvents.mjs +348 -0
  23. package/dist/v1/libs/TinyLocalStorage.cjs +109 -21
  24. package/dist/v1/libs/TinyLocalStorage.d.mts +29 -0
  25. package/dist/v1/libs/TinyLocalStorage.mjs +97 -21
  26. package/dist/v1/libs/TinyNewWinEvents.cjs +486 -0
  27. package/dist/v1/libs/TinyNewWinEvents.d.mts +241 -0
  28. package/dist/v1/libs/TinyNewWinEvents.mjs +437 -0
  29. package/docs/v1/README.md +2 -0
  30. package/docs/v1/libs/TinyIframeEvents.md +149 -0
  31. package/docs/v1/libs/TinyLocalStorage.md +53 -0
  32. package/docs/v1/libs/TinyNewWinEvents.md +186 -0
  33. package/docs/v1/libs/TinyNotifyCenter.md +1 -1
  34. package/package.json +1 -1
@@ -0,0 +1,348 @@
1
+ import { isJsonObject } from '../basics/objChecker.mjs';
2
+ import TinyEvents from './TinyEvents.mjs';
3
+ /** @type {WeakMap<Window, TinyIframeEvents>} */
4
+ const instances = new WeakMap();
5
+ /**
6
+ * @callback handler
7
+ * A function to handle incoming event payloads.
8
+ * @param {any} payload - The data sent by the emitter.
9
+ * @param {MessageEvent<any>} event - Metadata about the message.
10
+ */
11
+ /**
12
+ * A flexible event routing system for structured communication
13
+ * between a parent window and its iframe using `postMessage`.
14
+ *
15
+ * This class abstracts the complexity of cross-origin and window-type handling,
16
+ * allowing both the iframe and parent to:
17
+ * - Send events with arbitrary payloads
18
+ * - Listen to specific event names
19
+ * - Filter events by origin and source
20
+ * - Work symmetrically from both sides with automatic direction handling
21
+ *
22
+ * Use this class when building applications that require modular, event-driven
23
+ * communication across embedded frames.
24
+ */
25
+ class TinyIframeEvents {
26
+ #events = new TinyEvents();
27
+ /**
28
+ * Enables or disables throwing an error when the maximum number of listeners is exceeded.
29
+ *
30
+ * @param {boolean} shouldThrow - If true, an error will be thrown when the max is exceeded.
31
+ */
32
+ setThrowOnMaxListeners(shouldThrow) {
33
+ return this.#events.setThrowOnMaxListeners(shouldThrow);
34
+ }
35
+ /**
36
+ * Checks whether an error will be thrown when the max listener limit is exceeded.
37
+ *
38
+ * @returns {boolean} True if an error will be thrown, false if only a warning is shown.
39
+ */
40
+ getThrowOnMaxListeners() {
41
+ return this.#events.getThrowOnMaxListeners();
42
+ }
43
+ /////////////////////////////////////////////////////////////
44
+ /**
45
+ * Adds a listener to the beginning of the listeners array for the specified event.
46
+ *
47
+ * @param {string} event - Event name.
48
+ * @param {handler} handler - The callback function.
49
+ */
50
+ prependListener(event, handler) {
51
+ return this.#events.prependListener(event, handler);
52
+ }
53
+ /**
54
+ * Adds a one-time listener to the beginning of the listeners array for the specified event.
55
+ *
56
+ * @param {string} event - Event name.
57
+ * @param {handler} handler - The callback function.
58
+ * @returns {handler} - The wrapped handler used internally.
59
+ */
60
+ prependListenerOnce(event, handler) {
61
+ return this.#events.prependListenerOnce(event, handler);
62
+ }
63
+ //////////////////////////////////////////////////////////////////////
64
+ /**
65
+ * Adds a event listener.
66
+ *
67
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
68
+ * @param {handler} handler - Callback function to be called when event fires.
69
+ */
70
+ appendListener(event, handler) {
71
+ return this.#events.appendListener(event, handler);
72
+ }
73
+ /**
74
+ * Registers an event listener that runs only once, then is removed.
75
+ *
76
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
77
+ * @param {handler} handler - The callback function to run on event.
78
+ * @returns {handler} - The wrapped version of the handler.
79
+ */
80
+ appendListenerOnce(event, handler) {
81
+ return this.#events.appendListenerOnce(event, handler);
82
+ }
83
+ /**
84
+ * Adds a event listener.
85
+ *
86
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
87
+ * @param {handler} handler - Callback function to be called when event fires.
88
+ */
89
+ on(event, handler) {
90
+ return this.#events.on(event, handler);
91
+ }
92
+ /**
93
+ * Registers an event listener that runs only once, then is removed.
94
+ *
95
+ * @param {string} event - Event name, such as 'onScrollBoundary' or 'onAutoScroll'.
96
+ * @param {handler} handler - The callback function to run on event.
97
+ * @returns {handler} - The wrapped version of the handler.
98
+ */
99
+ once(event, handler) {
100
+ return this.#events.once(event, handler);
101
+ }
102
+ ////////////////////////////////////////////////////////////////////
103
+ /**
104
+ * Removes a previously registered event listener.
105
+ *
106
+ * @param {string} event - The name of the event to remove the handler from.
107
+ * @param {handler} handler - The specific callback function to remove.
108
+ */
109
+ off(event, handler) {
110
+ return this.#events.off(event, handler);
111
+ }
112
+ /**
113
+ * Removes all event listeners of a specific type from the element.
114
+ *
115
+ * @param {string} event - The event type to remove (e.g. 'onScrollBoundary').
116
+ */
117
+ offAll(event) {
118
+ return this.#events.offAll(event);
119
+ }
120
+ /**
121
+ * Removes all event listeners of all types from the element.
122
+ */
123
+ offAllTypes() {
124
+ return this.#events.offAllTypes();
125
+ }
126
+ ////////////////////////////////////////////////////////////
127
+ /**
128
+ * Returns the number of listeners for a given event.
129
+ *
130
+ * @param {string} event - The name of the event.
131
+ * @returns {number} Number of listeners for the event.
132
+ */
133
+ listenerCount(event) {
134
+ return this.#events.listenerCount(event);
135
+ }
136
+ /**
137
+ * Returns a copy of the array of listeners for the specified event.
138
+ *
139
+ * @param {string} event - The name of the event.
140
+ * @returns {handler[]} Array of listener functions.
141
+ */
142
+ listeners(event) {
143
+ return this.#events.listeners(event);
144
+ }
145
+ /**
146
+ * Returns a copy of the array of listeners for the specified event.
147
+ *
148
+ * @param {string} event - The name of the event.
149
+ * @returns {handler[]} Array of listener functions.
150
+ */
151
+ onceListeners(event) {
152
+ return this.#events.onceListeners(event);
153
+ }
154
+ /**
155
+ * Returns a copy of the internal listeners array for the specified event,
156
+ * including wrapper functions like those used by `.once()`.
157
+ * @param {string | symbol} event - The event name.
158
+ * @returns {handler[]} An array of raw listener functions.
159
+ */
160
+ allListeners(event) {
161
+ return this.#events.allListeners(event);
162
+ }
163
+ /**
164
+ * Returns an array of event names for which there are registered listeners.
165
+ *
166
+ * @returns {string[]} Array of registered event names.
167
+ */
168
+ eventNames() {
169
+ return this.#events.eventNames();
170
+ }
171
+ //////////////////////////////////////////////////////
172
+ /**
173
+ * Sets the maximum number of listeners per event before a warning is shown.
174
+ *
175
+ * @param {number} n - The maximum number of listeners.
176
+ */
177
+ setMaxListeners(n) {
178
+ return this.#events.setMaxListeners(n);
179
+ }
180
+ /**
181
+ * Gets the maximum number of listeners allowed per event.
182
+ *
183
+ * @returns {number} The maximum number of listeners.
184
+ */
185
+ getMaxListeners() {
186
+ return this.#events.getMaxListeners();
187
+ }
188
+ ///////////////////////////////////////////////////
189
+ /** @type {Window} */
190
+ #targetWindow;
191
+ /** @type {string} */
192
+ #targetOrigin;
193
+ /** @type {string} */
194
+ #selfType;
195
+ /** @type {boolean} */
196
+ #isDestroyed = false;
197
+ /** @type {boolean} */
198
+ #ready = false;
199
+ /**
200
+ * @typedef {object} IframeEventBase
201
+ * @property {string} eventName - The name of the custom event route.
202
+ * @property {any} payload - The data being sent (can be any type).
203
+ * @property {'iframe' | 'parent'} direction - Indicates the sender: 'iframe' or 'parent'.
204
+ */
205
+ /**
206
+ * Queue of messages emitted before connection is ready
207
+ * @type {IframeEventBase[]}
208
+ */
209
+ #pendingQueue = [];
210
+ /** @type {string} Internal message type for routed communication */
211
+ #secretEventName = '__tinyIframeEvent__';
212
+ /**
213
+ * Gets the internal secret iframe event name.
214
+ * @returns {string}
215
+ */
216
+ get secretEventName() {
217
+ return this.#secretEventName;
218
+ }
219
+ /**
220
+ * Sets the internal secret iframe event name.
221
+ * @param {string} name
222
+ * @throws {TypeError} If the value is not a string.
223
+ */
224
+ set secretEventName(name) {
225
+ if (typeof name !== 'string')
226
+ throw new TypeError('TinyIframeEvents: secretEventName must be a string.');
227
+ this.#secretEventName = name;
228
+ }
229
+ /**
230
+ * Creates a new TinyIframeEvents instance to manage communication between iframe and parent.
231
+ * Automatically determines the current context (`iframe` or `parent`) based on the `targetWindow`.
232
+ *
233
+ * @param {Object} config - Configuration object.
234
+ * @param {HTMLIFrameElement} [config.targetIframe] - The target window to post messages to. Defaults to `window.parent` (assumes this is inside an iframe).
235
+ * @param {string} [config.targetOrigin] - The target origin to restrict messages to. Defaults to `window.location.origin`.
236
+ */
237
+ constructor({ targetIframe, targetOrigin } = {}) {
238
+ if (typeof targetIframe !== 'undefined' &&
239
+ (!(targetIframe instanceof HTMLIFrameElement) || !targetIframe.contentWindow))
240
+ throw new TypeError(`[TinyIframeEvents] Invalid "targetIframe" provided: expected a HTML Iframe Element, received ${typeof targetIframe}`);
241
+ if (typeof targetOrigin !== 'undefined' && typeof targetOrigin !== 'string')
242
+ throw new TypeError(`[TinyIframeEvents] Invalid "targetOrigin" provided: expected a string, received ${typeof targetOrigin}`);
243
+ this.#targetWindow = targetIframe?.contentWindow ?? window.parent;
244
+ this.#targetOrigin = targetOrigin ?? window.location.origin;
245
+ this.#selfType = !targetIframe ? 'iframe' : 'parent';
246
+ if (instances.has(this.#targetWindow))
247
+ throw new Error('Duplicate window reference.');
248
+ this._boundOnMessage = this.#onMessage.bind(this);
249
+ this._boundOnceMessage = this.#onceMessage.bind(this);
250
+ if (this.#targetWindow.document.readyState === 'complete' ||
251
+ this.#targetWindow.document.readyState === 'interactive')
252
+ this.#onceMessage();
253
+ else {
254
+ this.#targetWindow.addEventListener('load', this._boundOnceMessage, false);
255
+ this.#targetWindow.addEventListener('DOMContentLoaded', this._boundOnceMessage, false);
256
+ }
257
+ window.addEventListener('message', this._boundOnMessage, false);
258
+ instances.set(this.#targetWindow, this);
259
+ }
260
+ /**
261
+ * Marks the communication as ready and flushes any queued messages.
262
+ */
263
+ #onceMessage() {
264
+ if (this.#ready)
265
+ return;
266
+ this.#ready = true;
267
+ this.#flushQueue();
268
+ }
269
+ /**
270
+ * Internal handler for the message event. Filters and dispatches incoming messages.
271
+ *
272
+ * @param {MessageEvent<any>} event - The message event received via `postMessage`.
273
+ */
274
+ #onMessage(event) {
275
+ const { data, source } = event;
276
+ // Reject non-object or unrelated messages
277
+ if (!isJsonObject(data) || !data[this.#secretEventName])
278
+ return;
279
+ const { eventName, payload, direction } = data;
280
+ // Reject if not from the expected window (for security)
281
+ if (source !== this.#targetWindow)
282
+ return;
283
+ // Reject if direction is not meant for this side
284
+ if ((this.#selfType === 'iframe' && direction !== 'iframe') ||
285
+ (this.#selfType === 'parent' && direction !== 'parent'))
286
+ return;
287
+ this.#events.emit(/** @type {string} */ (eventName), payload, event);
288
+ }
289
+ /**
290
+ * Sends an event to the target window.
291
+ *
292
+ * @param {string} eventName - A unique name identifying the event.
293
+ * @param {*} payload - The data to send with the event. Can be any serializable value.
294
+ * @throws {Error} If `eventName` is not a string.
295
+ */
296
+ emit(eventName, payload) {
297
+ if (typeof eventName !== 'string')
298
+ throw new TypeError('Event name must be a string.');
299
+ if (this.#isDestroyed)
300
+ throw new Error('Cannot emit: instance has been destroyed.');
301
+ /** @type {IframeEventBase} */
302
+ const message = {
303
+ [this.#secretEventName]: true,
304
+ eventName,
305
+ payload,
306
+ direction: this.#selfType === 'parent' ? 'iframe' : 'parent',
307
+ };
308
+ if (!this.#ready) {
309
+ this.#pendingQueue.push(message);
310
+ return;
311
+ }
312
+ this.#targetWindow.postMessage(message, this.#targetOrigin);
313
+ }
314
+ /**
315
+ * Sends all pending messages queued before handshake completion.
316
+ *
317
+ * @returns {void}
318
+ */
319
+ #flushQueue() {
320
+ while (this.#pendingQueue.length) {
321
+ const data = this.#pendingQueue.shift();
322
+ if (data)
323
+ this.#targetWindow.postMessage(data, this.#targetOrigin);
324
+ }
325
+ }
326
+ /**
327
+ * Checks if the communication instance has been destroyed.
328
+ *
329
+ * @returns {boolean}
330
+ */
331
+ isDestroyed() {
332
+ return this.#isDestroyed;
333
+ }
334
+ /**
335
+ * Unsubscribes all registered event listeners and removes the message handler.
336
+ * Call this when the instance is no longer needed to prevent memory leaks.
337
+ */
338
+ destroy() {
339
+ this.#isDestroyed = true;
340
+ window.removeEventListener('message', this._boundOnMessage);
341
+ this.#targetWindow.removeEventListener('load', this._boundOnceMessage, false);
342
+ this.#targetWindow.removeEventListener('DOMContentLoaded', this._boundOnceMessage, false);
343
+ this.#events.offAllTypes();
344
+ this.#pendingQueue = [];
345
+ instances.delete(this.#targetWindow);
346
+ }
347
+ }
348
+ export default TinyIframeEvents;
@@ -421,6 +421,12 @@ class TinyLocalStorage {
421
421
  /** @type {Storage} */
422
422
  #localStorage = window.localStorage;
423
423
 
424
+ /** @type {string|null} */
425
+ #dbKey = null;
426
+
427
+ /** @type {number} */
428
+ #version = 0;
429
+
424
430
  /** @type {(ev: StorageEvent) => any} */
425
431
  #storageEvent = (ev) => this.emit('storage', ev);
426
432
 
@@ -428,11 +434,84 @@ class TinyLocalStorage {
428
434
  * Initializes the TinyLocalStorage instance and sets up cross-tab sync.
429
435
  *
430
436
  * Adds listener for the native `storage` event to support tab synchronization.
437
+ * @param {string} [dbName] - Unique database name.
431
438
  */
432
- 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}`;
433
443
  window.addEventListener('storage', this.#storageEvent);
434
444
  }
435
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
+
436
515
  /**
437
516
  * Defines a custom storage interface (e.g. `sessionStorage`).
438
517
  *
@@ -440,7 +519,7 @@ class TinyLocalStorage {
440
519
  */
441
520
  setLocalStorage(localstorage) {
442
521
  if (!(localstorage instanceof Storage))
443
- throw new Error('Argument must be a valid instance of Storage.');
522
+ throw new TypeError('Argument must be a valid instance of Storage.');
444
523
  this.#localStorage = localstorage;
445
524
  }
446
525
 
@@ -462,7 +541,7 @@ class TinyLocalStorage {
462
541
  */
463
542
  #setJson(name, data) {
464
543
  if (typeof name !== 'string' || !name.length)
465
- throw new Error('Key must be a non-empty string.');
544
+ throw new TypeError('Key must be a non-empty string.');
466
545
  return TinyLocalStorage.encodeSpecialJson(data);
467
546
  }
468
547
 
@@ -475,13 +554,14 @@ class TinyLocalStorage {
475
554
  * @param {LocalStorageJsonValue} data - The data to be serialized and stored.
476
555
  */
477
556
  setJson(name, data) {
557
+ this.#isProtectedDbKey(name);
478
558
  if (
479
559
  !objChecker.isJsonObject(data) &&
480
560
  !Array.isArray(data) &&
481
561
  !(data instanceof Map) &&
482
562
  !(data instanceof Set)
483
563
  ) {
484
- 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.');
485
565
  }
486
566
  const encoded = this.#setJson(name, data);
487
567
  this.emit('setJson', name, data);
@@ -499,7 +579,7 @@ class TinyLocalStorage {
499
579
  */
500
580
  #getJson(name, defaultData) {
501
581
  if (typeof name !== 'string' || !name.length)
502
- throw new Error('Key must be a non-empty string.');
582
+ throw new TypeError('Key must be a non-empty string.');
503
583
 
504
584
  const raw = this.#localStorage.getItem(name);
505
585
  const fallbackTypes = {
@@ -553,7 +633,8 @@ class TinyLocalStorage {
553
633
  * @param {Date} data
554
634
  */
555
635
  setDate(name, data) {
556
- 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.');
557
638
  const encoded = this.#setJson(name, data);
558
639
  this.emit('setDate', name, data);
559
640
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -575,7 +656,8 @@ class TinyLocalStorage {
575
656
  * @param {RegExp} data
576
657
  */
577
658
  setRegExp(name, data) {
578
- 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.');
579
661
  const encoded = this.#setJson(name, data);
580
662
  this.emit('setRegExp', name, data);
581
663
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -597,7 +679,8 @@ class TinyLocalStorage {
597
679
  * @param {bigint} data
598
680
  */
599
681
  setBigInt(name, data) {
600
- 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.');
601
684
  const encoded = this.#setJson(name, data);
602
685
  this.emit('setBigInt', name, data);
603
686
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -620,7 +703,8 @@ class TinyLocalStorage {
620
703
  * @param {symbol} data
621
704
  */
622
705
  setSymbol(name, data) {
623
- 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.');
624
708
  const encoded = this.#setJson(name, data);
625
709
  this.emit('setSymbol', name, data);
626
710
  return this.#localStorage.setItem(name, JSON.stringify(encoded));
@@ -653,8 +737,9 @@ class TinyLocalStorage {
653
737
  * @param {any} data - The data to store.
654
738
  */
655
739
  setItem(name, data) {
740
+ this.#isProtectedDbKey(name);
656
741
  if (typeof name !== 'string' || !name.length)
657
- throw new Error('Key must be a non-empty string.');
742
+ throw new TypeError('Key must be a non-empty string.');
658
743
  this.emit('setItem', name, data);
659
744
  return this.#localStorage.setItem(name, data);
660
745
  }
@@ -667,7 +752,7 @@ class TinyLocalStorage {
667
752
  */
668
753
  getItem(name) {
669
754
  if (typeof name !== 'string' || !name.length)
670
- throw new Error('Key must be a non-empty string.');
755
+ throw new TypeError('Key must be a non-empty string.');
671
756
  return this.#localStorage.getItem(name);
672
757
  }
673
758
 
@@ -678,9 +763,10 @@ class TinyLocalStorage {
678
763
  * @param {string} data - The string to store.
679
764
  */
680
765
  setString(name, data) {
766
+ this.#isProtectedDbKey(name);
681
767
  if (typeof name !== 'string' || !name.length)
682
- throw new Error('Key must be a non-empty string.');
683
- 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.');
684
770
 
685
771
  this.emit('setString', name, data);
686
772
  return this.#localStorage.setItem(
@@ -700,7 +786,7 @@ class TinyLocalStorage {
700
786
  */
701
787
  getString(name) {
702
788
  if (typeof name !== 'string' || !name.length)
703
- throw new Error('Key must be a non-empty string.');
789
+ throw new TypeError('Key must be a non-empty string.');
704
790
  let value = this.#localStorage.getItem(name);
705
791
  try {
706
792
  /** @type {{ value: string; __string__: boolean }} */
@@ -722,9 +808,10 @@ class TinyLocalStorage {
722
808
  * @param {number} data - The number to store.
723
809
  */
724
810
  setNumber(name, data) {
811
+ this.#isProtectedDbKey(name);
725
812
  if (typeof name !== 'string' || !name.length)
726
- throw new Error('Key must be a non-empty string.');
727
- 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.');
728
815
  this.emit('setNumber', name, data);
729
816
  return this.#localStorage.setItem(name, String(data));
730
817
  }
@@ -737,7 +824,7 @@ class TinyLocalStorage {
737
824
  */
738
825
  getNumber(name) {
739
826
  if (typeof name !== 'string' || !name.length)
740
- throw new Error('Key must be a non-empty string.');
827
+ throw new TypeError('Key must be a non-empty string.');
741
828
 
742
829
  /** @type {number|string|null} */
743
830
  let number = this.#localStorage.getItem(name);
@@ -756,9 +843,10 @@ class TinyLocalStorage {
756
843
  * @param {boolean} data - The boolean value to store.
757
844
  */
758
845
  setBool(name, data) {
846
+ this.#isProtectedDbKey(name);
759
847
  if (typeof name !== 'string' || !name.length)
760
- throw new Error('Key must be a non-empty string.');
761
- 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.');
762
850
  this.emit('setBool', name, data);
763
851
  return this.#localStorage.setItem(name, String(data));
764
852
  }
@@ -771,7 +859,7 @@ class TinyLocalStorage {
771
859
  */
772
860
  getBool(name) {
773
861
  if (typeof name !== 'string' || !name.length)
774
- throw new Error('Key must be a non-empty string.');
862
+ throw new TypeError('Key must be a non-empty string.');
775
863
 
776
864
  const value = this.#localStorage.getItem(name);
777
865
  if (typeof value === 'boolean') return value;
@@ -790,7 +878,7 @@ class TinyLocalStorage {
790
878
  */
791
879
  removeItem(name) {
792
880
  if (typeof name !== 'string' || !name.length)
793
- throw new Error('Key must be a non-empty string.');
881
+ throw new TypeError('Key must be a non-empty string.');
794
882
 
795
883
  this.emit('removeItem', name);
796
884
  return this.#localStorage.removeItem(name);
@@ -130,6 +130,13 @@ declare class TinyLocalStorage {
130
130
  static deleteJsonType(type: any): boolean;
131
131
  static encodeSpecialJson(value: any): any;
132
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);
133
140
  /**
134
141
  * Enables or disables throwing an error when the maximum number of listeners is exceeded.
135
142
  *
@@ -258,6 +265,28 @@ declare class TinyLocalStorage {
258
265
  * @returns {number} The maximum number of listeners.
259
266
  */
260
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;
261
290
  /**
262
291
  * Defines a custom storage interface (e.g. `sessionStorage`).
263
292
  *