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.
- package/dist/v1/TinyEssentials.js +1009 -22
- package/dist/v1/TinyEssentials.min.js +1 -1
- package/dist/v1/TinyIframeEvents.js +854 -0
- package/dist/v1/TinyIframeEvents.min.js +1 -0
- package/dist/v1/TinyLocalStorage.js +109 -21
- package/dist/v1/TinyLocalStorage.min.js +1 -1
- package/dist/v1/TinyNewWinEvents.js +888 -0
- package/dist/v1/TinyNewWinEvents.min.js +1 -0
- package/dist/v1/TinyUploadClicker.js +1022 -37
- package/dist/v1/TinyUploadClicker.min.js +1 -1
- package/dist/v1/build/TinyIframeEvents.cjs +7 -0
- package/dist/v1/build/TinyIframeEvents.d.mts +3 -0
- package/dist/v1/build/TinyIframeEvents.mjs +2 -0
- package/dist/v1/build/TinyNewWinEvents.cjs +7 -0
- package/dist/v1/build/TinyNewWinEvents.d.mts +3 -0
- package/dist/v1/build/TinyNewWinEvents.mjs +2 -0
- package/dist/v1/index.cjs +4 -0
- package/dist/v1/index.d.mts +3 -1
- package/dist/v1/index.mjs +3 -1
- package/dist/v1/libs/TinyIframeEvents.cjs +409 -0
- package/dist/v1/libs/TinyIframeEvents.d.mts +193 -0
- package/dist/v1/libs/TinyIframeEvents.mjs +348 -0
- package/dist/v1/libs/TinyLocalStorage.cjs +109 -21
- package/dist/v1/libs/TinyLocalStorage.d.mts +29 -0
- package/dist/v1/libs/TinyLocalStorage.mjs +97 -21
- package/dist/v1/libs/TinyNewWinEvents.cjs +486 -0
- package/dist/v1/libs/TinyNewWinEvents.d.mts +241 -0
- package/dist/v1/libs/TinyNewWinEvents.mjs +437 -0
- package/docs/v1/README.md +2 -0
- package/docs/v1/libs/TinyIframeEvents.md +149 -0
- package/docs/v1/libs/TinyLocalStorage.md +53 -0
- package/docs/v1/libs/TinyNewWinEvents.md +186 -0
- package/docs/v1/libs/TinyNotifyCenter.md +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
683
|
-
if (typeof data !== 'string') throw new
|
|
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
|
|
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
|
|
727
|
-
if (typeof data !== 'number') throw new
|
|
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
|
|
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
|
|
761
|
-
if (typeof data !== 'boolean') throw new
|
|
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
|
|
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
|
|
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
|
*
|