tiny-essentials 1.25.2 → 1.25.4

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,18 +9,35 @@ const instances = new WeakMap();
9
9
  * @param {MessageEvent<any>} event - Metadata about the message.
10
10
  */
11
11
  /**
12
- * A flexible event routing system for structured communication
13
- * between a parent window and its iframe using `postMessage`.
12
+ * @typedef {object} TinyIframeEventsConfig
13
+ * Configuration object for initializing TinyIframeEvents.
14
+ * @property {HTMLIFrameElement} [targetIframe] - The target iframe element to post messages to. Required if instantiated in the parent window.
15
+ * @property {string} [targetOrigin] - The target origin to restrict messages to. Defaults to `window.location.origin`.
16
+ * @property {string} [secretEventName] - Custom internal name used to validate standard routing messages.
17
+ * @property {string} [handshakeEventName] - Custom internal name used for the initial MessageChannel handshake.
18
+ * @property {string} [readyEventName] - Custom internal name used for the ready event trigger.
19
+ */
20
+ /**
21
+ * @typedef {object} IframeEventBase
22
+ * Internal message structure for routed communication.
23
+ * @property {boolean} [secretIndicator] - Dynamic key based on secretEventName to validate the message.
24
+ * @property {string} eventName - The name of the custom event route.
25
+ * @property {any} payload - The data being sent (can be any type).
26
+ * @property {'iframe' | 'parent'} direction - Indicates the sender: 'iframe' or 'parent'.
27
+ */
28
+ /**
29
+ * A highly secure and flexible event routing system for structured communication
30
+ * between a parent window and its iframe using `MessageChannel`.
14
31
  *
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
32
+ * This class abstracts the complexity of cross-origin communication by establishing
33
+ * a direct, un-interceptable port connection after a secure initial handshake.
21
34
  *
22
- * Use this class when building applications that require modular, event-driven
23
- * communication across embedded frames.
35
+ * Features:
36
+ * - Secure direct pipeline via `MessageChannel`.
37
+ * - Customizable internal event names to avoid collisions.
38
+ * - Symmetrical usage for both parent and iframe.
39
+ * - Queue management for messages sent before the connection is established.
40
+ * - Auto-reconnects and resets ports if the target iframe is reloaded.
24
41
  */
25
42
  class TinyIframeEvents {
26
43
  #events = new TinyEvents();
@@ -188,29 +205,87 @@ class TinyIframeEvents {
188
205
  ///////////////////////////////////////////////////
189
206
  /** @type {Window} */
190
207
  #targetWindow;
208
+ get targetWindow() {
209
+ return this.#targetWindow;
210
+ }
211
+ /** @type {HTMLIFrameElement | undefined} */
212
+ #targetIframeElement;
213
+ get targetIframeElement() {
214
+ return this.#targetIframeElement;
215
+ }
191
216
  /** @type {string} */
192
217
  #targetOrigin;
193
- /** @type {string} */
218
+ get targetOrigin() {
219
+ return this.#targetOrigin;
220
+ }
221
+ /** @type {'iframe' | 'parent'} */
194
222
  #selfType;
223
+ get selfType() {
224
+ return this.#selfType;
225
+ }
195
226
  /** @type {boolean} */
196
227
  #isDestroyed = false;
197
228
  /** @type {boolean} */
198
229
  #ready = false;
230
+ get ready() {
231
+ return this.#ready;
232
+ }
199
233
  /**
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'.
234
+ * Executes the provided callback when the secure MessageChannel connection is fully established.
235
+ * If the connection is already ready, the callback is executed immediately.
236
+ *
237
+ * @param {function(): void} handler - The callback function to execute.
204
238
  */
239
+ onReady(handler) {
240
+ if (this.#ready) {
241
+ handler();
242
+ }
243
+ else {
244
+ this.#events.once(this.#readyEventName, handler);
245
+ }
246
+ }
247
+ /** @type {MessagePort | null} */
248
+ #port = null;
249
+ /** @type {IframeEventBase[]} */
250
+ #pendingQueue = [];
251
+ /** @type {string} */
252
+ #secretEventName;
253
+ /** @type {string} */
254
+ #handshakeEventName;
255
+ /** @type {string} */
256
+ #readyEventName;
257
+ /** @type {(() => void) | null} */
258
+ #boundSendPort = null;
205
259
  /**
206
- * Queue of messages emitted before connection is ready
207
- * @type {IframeEventBase[]}
260
+ * Creates a new TinyIframeEvents instance to manage secure communication.
261
+ * Automatically establishes a MessageChannel connection between contexts.
262
+ *
263
+ * @param {TinyIframeEventsConfig} config - Configuration object.
208
264
  */
209
- #pendingQueue = [];
210
- /** @type {string} Internal message type for routed communication */
211
- #secretEventName = '__tinyIframeEvent__';
265
+ constructor({ targetIframe, targetOrigin = window.location.origin, secretEventName = '__tinyIframeEvent__', handshakeEventName = '__tinyIframeHandshake__', readyEventName = '__tinyIframeReady__', } = {}) {
266
+ if (targetIframe !== undefined &&
267
+ (!(targetIframe instanceof HTMLIFrameElement) || !targetIframe.contentWindow)) {
268
+ throw new TypeError(`[TinyIframeEvents] Invalid "targetIframe": expected HTMLIFrameElement, received ${typeof targetIframe}`);
269
+ }
270
+ if (typeof targetOrigin !== 'string') {
271
+ throw new TypeError(`[TinyIframeEvents] Invalid "targetOrigin": expected string, received ${typeof targetOrigin}`);
272
+ }
273
+ this.#targetIframeElement = targetIframe;
274
+ this.#targetWindow = targetIframe?.contentWindow ?? window.parent;
275
+ this.#targetOrigin = targetOrigin;
276
+ this.#selfType = !targetIframe ? 'iframe' : 'parent';
277
+ this.#secretEventName = secretEventName;
278
+ this.#handshakeEventName = handshakeEventName;
279
+ this.#readyEventName = readyEventName;
280
+ if (instances.has(this.#targetWindow))
281
+ throw new Error('Duplicate window reference.');
282
+ this._boundWindowMessage = this.#onWindowMessage.bind(this);
283
+ this._boundPortMessage = this.#onPortMessage.bind(this);
284
+ this.#initializeConnection();
285
+ instances.set(this.#targetWindow, this);
286
+ }
212
287
  /**
213
- * Gets the internal secret iframe event name.
288
+ * Gets the internal secret iframe event name used for validation.
214
289
  * @returns {string}
215
290
  */
216
291
  get secretEventName() {
@@ -227,71 +302,140 @@ class TinyIframeEvents {
227
302
  this.#secretEventName = name;
228
303
  }
229
304
  /**
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();
305
+ * Gets the internal handshake event name used for establishing the MessageChannel.
306
+ * @returns {string}
307
+ */
308
+ get handshakeEventName() {
309
+ return this.#handshakeEventName;
310
+ }
311
+ /**
312
+ * Sets the internal handshake event name.
313
+ * @param {string} name
314
+ * @throws {TypeError} If the value is not a string.
315
+ */
316
+ set handshakeEventName(name) {
317
+ if (typeof name !== 'string')
318
+ throw new TypeError('TinyIframeEvents: handshakeEventName must be a string.');
319
+ this.#handshakeEventName = name;
320
+ }
321
+ /**
322
+ * Gets the internal ready event name used when the connection is fully established.
323
+ * @returns {string}
324
+ */
325
+ get readyEventName() {
326
+ return this.#readyEventName;
327
+ }
328
+ /**
329
+ * Sets the internal ready event name.
330
+ * @param {string} name
331
+ * @throws {TypeError} If the value is not a string.
332
+ */
333
+ set readyEventName(name) {
334
+ if (typeof name !== 'string')
335
+ throw new TypeError('TinyIframeEvents: readyEventName must be a string.');
336
+ this.#readyEventName = name;
337
+ }
338
+ /**
339
+ * Initializes the correct connection strategy based on the current context (parent or iframe).
340
+ */
341
+ #initializeConnection() {
342
+ if (this.#selfType === 'parent') {
343
+ this.#boundSendPort = () => {
344
+ if (this.#isDestroyed)
345
+ return;
346
+ // Closes any existing port to prevent memory leaks during iframe reloads
347
+ if (this.#port) {
348
+ this.#port.close();
349
+ }
350
+ // Creates a fresh channel for every attempt to prevent clone errors
351
+ const channel = new MessageChannel();
352
+ this.#port = channel.port1;
353
+ this.#port.onmessage = this._boundPortMessage;
354
+ // Resets ready state to require a new ACK
355
+ this.#ready = false;
356
+ this.#targetWindow.postMessage({ type: this.#handshakeEventName }, this.#targetOrigin, [
357
+ channel.port2,
358
+ ]);
359
+ };
360
+ if (this.#targetIframeElement) {
361
+ this.#targetIframeElement.addEventListener('load', this.#boundSendPort);
362
+ this.#boundSendPort();
363
+ }
364
+ }
253
365
  else {
254
- this.#targetWindow.addEventListener('load', this._boundOnceMessage, false);
255
- this.#targetWindow.addEventListener('DOMContentLoaded', this._boundOnceMessage, false);
366
+ window.addEventListener('message', this._boundWindowMessage, false);
256
367
  }
257
- window.addEventListener('message', this._boundOnMessage, false);
258
- instances.set(this.#targetWindow, this);
259
368
  }
260
369
  /**
261
- * Marks the communication as ready and flushes any queued messages.
370
+ * Internal handler for the initial window message event (used by iframe to receive the port).
371
+ *
372
+ * @param {MessageEvent<any>} event - The message event received via `postMessage`.
262
373
  */
263
- #onceMessage() {
264
- if (this.#ready)
374
+ #onWindowMessage(event) {
375
+ const data = event.data;
376
+ const ports = event.ports;
377
+ if (!isJsonObject(data) || data.type !== this.#handshakeEventName || ports.length === 0)
265
378
  return;
266
- this.#ready = true;
267
- this.#flushQueue();
379
+ this.#port = ports[0];
380
+ this.#port.onmessage = this._boundPortMessage;
381
+ window.removeEventListener('message', this._boundWindowMessage);
382
+ this.#port.postMessage({ type: `${this.#handshakeEventName}_ACK` });
383
+ this.#markReady();
268
384
  }
269
385
  /**
270
- * Internal handler for the message event. Filters and dispatches incoming messages.
386
+ * Internal handler for messages received through the secure MessageChannel port.
271
387
  *
272
- * @param {MessageEvent<any>} event - The message event received via `postMessage`.
388
+ * @param {MessageEvent<any>} event - The message event received via `MessagePort`.
273
389
  */
274
- #onMessage(event) {
275
- const { data, source } = event;
276
- // Reject non-object or unrelated messages
390
+ #onPortMessage(event) {
391
+ /** @type {any} */
392
+ const data = event.data;
393
+ if (isJsonObject(data) && data.type === `${this.#handshakeEventName}_ACK`) {
394
+ this.#markReady();
395
+ return;
396
+ }
277
397
  if (!isJsonObject(data) || !data[this.#secretEventName])
278
398
  return;
279
- const { eventName, payload, direction } = data;
280
- // Reject if not from the expected window (for security)
281
- if (source !== this.#targetWindow)
399
+ const eventName = data.eventName;
400
+ const payload = data.payload;
401
+ const direction = data.direction;
402
+ if (typeof eventName !== 'string' ||
403
+ (this.#selfType === 'iframe' && direction !== 'iframe') ||
404
+ (this.#selfType === 'parent' && direction !== 'parent')) {
282
405
  return;
283
- // Reject if direction is not meant for this side
284
- if ((this.#selfType === 'iframe' && direction !== 'iframe') ||
285
- (this.#selfType === 'parent' && direction !== 'parent'))
406
+ }
407
+ this.#events.emit(eventName, payload, event);
408
+ }
409
+ /**
410
+ * Marks the communication as ready, flushes any queued messages,
411
+ * and triggers the internal ready event for any waiting listeners.
412
+ */
413
+ #markReady() {
414
+ if (this.#ready)
286
415
  return;
287
- this.#events.emit(/** @type {string} */ (eventName), payload, event);
416
+ this.#ready = true;
417
+ this.#flushQueue();
418
+ this.#events.emit(this.#readyEventName);
288
419
  }
289
420
  /**
290
- * Sends an event to the target window.
421
+ * Sends all pending messages queued before the secure port was established.
422
+ */
423
+ #flushQueue() {
424
+ while (this.#pendingQueue.length > 0) {
425
+ /** @type {IframeEventBase | undefined} */
426
+ const data = this.#pendingQueue.shift();
427
+ if (data && this.#port) {
428
+ this.#port.postMessage(data);
429
+ }
430
+ }
431
+ }
432
+ /**
433
+ * Sends an event to the target window through the secure MessageChannel.
291
434
  *
292
435
  * @param {string} eventName - A unique name identifying the event.
293
436
  * @param {*} payload - The data to send with the event. Can be any serializable value.
294
- * @throws {Error} If `eventName` is not a string.
437
+ * @throws {TypeError} If `eventName` is not a string.
438
+ * @throws {Error} If instance has been destroyed.
295
439
  */
296
440
  emit(eventName, payload) {
297
441
  if (typeof eventName !== 'string')
@@ -305,41 +449,35 @@ class TinyIframeEvents {
305
449
  payload,
306
450
  direction: this.#selfType === 'parent' ? 'iframe' : 'parent',
307
451
  };
308
- if (!this.#ready) {
452
+ if (!this.#ready || !this.#port) {
309
453
  this.#pendingQueue.push(message);
310
454
  return;
311
455
  }
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
- }
456
+ this.#port.postMessage(message);
325
457
  }
326
458
  /**
327
459
  * Checks if the communication instance has been destroyed.
328
460
  *
329
- * @returns {boolean}
461
+ * @returns {boolean} True if destroyed, false otherwise.
330
462
  */
331
463
  isDestroyed() {
332
464
  return this.#isDestroyed;
333
465
  }
334
466
  /**
335
- * Unsubscribes all registered event listeners and removes the message handler.
467
+ * Unsubscribes all registered event listeners, closes the message port, and cleans up references.
336
468
  * Call this when the instance is no longer needed to prevent memory leaks.
337
469
  */
338
470
  destroy() {
339
471
  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);
472
+ this.#ready = false;
473
+ window.removeEventListener('message', this._boundWindowMessage);
474
+ if (this.#targetIframeElement && this.#boundSendPort) {
475
+ this.#targetIframeElement.removeEventListener('load', this.#boundSendPort);
476
+ }
477
+ if (this.#port) {
478
+ this.#port.close();
479
+ this.#port = null;
480
+ }
343
481
  this.#events.offAllTypes();
344
482
  this.#pendingQueue = [];
345
483
  instances.delete(this.#targetWindow);
package/docs/v1/README.md CHANGED
@@ -62,6 +62,7 @@ This folder contains the core scripts we have worked on so far. Each file is a m
62
62
  - 🎨 **[TinyColorValidator](./libs/TinyColorValidator.md)** — A comprehensive CSS color validation and parsing utility supporting HEX, HEXA, RGB, RGBA, HSL, HSLA, HWB, Lab, LCH, standard HTML color names, and special keywords, with automatic type detection and parsing.
63
63
  * 📝 **[TinyTextDiffer](./libs/TinyTextDiffer.md)** — A granular text comparison utility using the LCS algorithm to detect additions, deletions, and unchanged segments between multiple history versions, returning a clean, parseable diff structure.
64
64
  * 🕒 **[TinyAnalogClock](./libs/TinyAnalogClock.md)** — A lightweight analog clock engine for managing time-based rotations, supporting custom offsets, smooth transitions, and easy binding to CSS variables rendering.
65
+ * 🔍 **[TinyArrayComparator](./libs/TinyArrayComparator.md)** — A lightweight, highly optimized JavaScript utility class designed to compare two arrays and efficiently detect which items were **added** or **deleted**.
65
66
 
66
67
  ### 3. **`fileManager/`**
67
68
  * 📁 **[Main](./fileManager/main.md)** — A Node.js file/directory utility module with support for JSON, backups, renaming, size analysis, and more.
@@ -0,0 +1,101 @@
1
+ # 🔍 TinyArrayComparator
2
+
3
+ A lightweight, highly optimized JavaScript utility class designed to compare two arrays and efficiently detect which items were **added** or **deleted**.
4
+
5
+ ## ✨ Features
6
+
7
+ - **Fast Comparison:** Uses a Map and hash-based lookups for $O(N)$ performance.
8
+ - **Stateful Design:** Store your base array once and compare it multiple times.
9
+ - **Zero Dependencies:** Pure Vanilla JavaScript.
10
+ - **Deep Compatibility:** Safely hashes strings, numbers, arrays, and deep objects.
11
+ - **Clean Architecture:** Built as an ES6 Module with strict JSDoc typing and input validation.
12
+
13
+ ---
14
+
15
+ ## 🚀 Quick Start
16
+
17
+ ### 1. Import the Class
18
+ Since this project uses modern ES6 modules, simply import it into your working file.
19
+
20
+ ```javascript
21
+ import TinyArrayComparator from './TinyArrayComparator.js';
22
+ ```
23
+
24
+ ### 2. Prepare Your Arrays
25
+ Set up the initial state (`oldArray`) and the modified state (`newArray`) that you want to compare.
26
+
27
+ ```javascript
28
+ const oldList = [
29
+ { id: 1, role: 'admin' },
30
+ { id: 2, role: 'user' },
31
+ { id: 3, role: 'moderator' }
32
+ ];
33
+
34
+ const newList = [
35
+ { id: 1, role: 'admin' }, // Unchanged
36
+ { id: 3, role: 'moderator' }, // Unchanged
37
+ { id: 4, role: 'guest' } // Added
38
+ // User ID 2 was deleted
39
+ ];
40
+ ```
41
+
42
+ ### 3. Initialize and Compare
43
+ Instantiate the class passing the initial array. Then, call the `compare` method with your new array.
44
+
45
+ ```javascript
46
+ const comparator = new TinyArrayComparator(oldList);
47
+ const results = comparator.compare(newList);
48
+
49
+ console.log(results);
50
+ ```
51
+
52
+ **Output:**
53
+ ```json
54
+ [
55
+ {
56
+ "item": { "id": 4, "role": "guest" },
57
+ "status": "added"
58
+ },
59
+ {
60
+ "item": { "id": 2, "role": "user" },
61
+ "status": "deleted"
62
+ }
63
+ ]
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 🛠️ API Reference
69
+
70
+ ### `constructor([oldArray])`
71
+ Creates a new comparator instance.
72
+ * **`oldArray`** `(Array<any>)` *(Optional)*: The initial state of the array to be stored. Throws a `TypeError` if the provided value is not an Array.
73
+
74
+ ### `compare(newArray)`
75
+ Evaluates differences between the internally stored `oldArray` and the provided `newArray`. Throws a `TypeError` if the provided value is not an Array.
76
+ * **`newArray`** `(Array<any>)`: The new array containing current modifications.
77
+ * **Returns:** An array of objects. Each object contains the original `item` and a `status` string (either `'added'` or `'deleted'`).
78
+
79
+ ### `get oldArray` / `set oldArray(value)`
80
+ Allows you to retrieve or update the base array stored inside the instance.
81
+ * **Throws:** `TypeError` if attempting to set a non-array value.
82
+
83
+ ### `static generateHash(item)`
84
+ A static helper method if you ever need to generate a unique base36 hash string for an item outside the comparison flow.
85
+ * **`item`** `(any)`: The target object, string, or number to hash.
86
+ * **Returns:** A `string` representing the unique hash.
87
+
88
+ ```javascript
89
+ const myHash = TinyArrayComparator.generateHash({ pony: 'Pudding' });
90
+ console.log(myHash); // Returns a base36 string like "1b4x9z"
91
+ ```
92
+
93
+ ---
94
+
95
+ ## 💡 Best Practices
96
+
97
+ * **Order Doesn't Matter:** Because this utility relies on value-hashing, the index order of items inside the arrays does not affect the comparison. It strictly checks for the existence of the values.
98
+ * **Object References:** The returned array preserves the exact references to the items passed in the parameters, allowing you to seamlessly integrate the output back into your application logic.
99
+ * **Reusability:** You can easily update the base state by assigning a new array to `comparator.oldArray = [...]` without creating a new instance.
100
+
101
+ Happy coding! 💻✨