tiny-essentials 1.25.1 → 1.25.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.
@@ -1 +1 @@
1
- (()=>{"use strict";var e={d:(t,n)=>{for(var s in n)e.o(n,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:n[s]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{TinyIframeEvents:()=>r});const n=class{#e=new Map;#t=10;#n=!1;#s(e,t){if("string"==typeof e)return[e];if(Array.isArray(e)&&e.every(e=>"string"==typeof e))return e;throw new TypeError(`${t}(event): event must be a string or string[]`)}setThrowOnMaxListeners(e){if("boolean"!=typeof e)throw new TypeError("setThrowOnMaxListeners(value): value must be a boolean");this.#n=e}getThrowOnMaxListeners(){return this.#n}#r(e,t,{once:n=!1}={}){let s=this.#e.get(e);Array.isArray(s)||(s=[],this.#e.set(e,s)),s.unshift({handler:t,config:{once:n}});const r=this.#t;if(r>0&&s.length>r){const t=`Possible memory leak detected. ${s.length} "${e}" listeners added. Use setMaxListeners() to increase limit.`;if(this.#n)throw new Error(t);console.warn(t)}}prependListener(e,t){const n=this.#s(e,"prependListener");if("function"!=typeof t)throw new TypeError("prependListener(event, handler): handler must be a function");for(const e of n)this.#r(e,t)}prependListenerOnce(e,t){const n=this.#s(e,"prependListenerOnce");if("function"!=typeof t)throw new TypeError("prependListenerOnce(event, handler): handler must be a function");const s=[];for(const e of n){const n=(...s)=>{this.off(e,n),t(...s)};this.#r(e,n,{once:!0}),s.push(n)}return s}#i(e,t,{once:n=!1}={}){const s=this.#s(e,"on");for(const e of s){let s=this.#e.get(e);Array.isArray(s)||(s=[],this.#e.set(e,s)),s.push({handler:t,config:{once:n}});const r=this.#t;if(r>0&&s.length>r){const t=`Possible memory leak detected. ${s.length} "${e}" listeners added. Use setMaxListeners() to increase limit.`;if(this.#n)throw new Error(t);console.warn(t)}}}on(e,t){if("function"!=typeof t)throw new TypeError("on(event, handler): handler must be a function");return this.#i(e,t)}once(e,t){const n=this.#s(e,"once");if("function"!=typeof t)throw new TypeError("once(event, handler): handler must be a function");const s=[];for(const e of n){const n=(...s)=>{this.off(e,n),t(...s)};this.#i(e,n,{once:!0}),s.push(n)}return s}appendListener(e,t){return this.on(e,t)}appendListenerOnce(e,t){return this.once(e,t)}off(e,t){const n=this.#s(e,"off");if("function"!=typeof t)throw new TypeError("off(event, handler): handler must be a function");for(const e of n){const n=this.#e.get(e);if(!Array.isArray(n))continue;const s=n.findIndex(e=>e.handler===t);-1!==s&&n.splice(s,1),0===n.length&&this.#e.delete(e)}}offAll(e){const t=this.#s(e,"offAll");for(const e of t)this.#e.delete(e)}offAllTypes(){this.#e.clear()}listenerCount(e){if("string"!=typeof e)throw new TypeError("listenerCount(event): event name must be a string");const t=this.#e.get(e);return Array.isArray(t)?t.length:0}listeners(e){if("string"!=typeof e)throw new TypeError("listeners(event): event name must be a string");const t=this.#e.get(e);return Array.isArray(t)?[...t].filter(e=>!e.config.once).map(e=>e.handler):[]}onceListeners(e){if("string"!=typeof e)throw new TypeError("onceListeners(event): event name must be a string");const t=this.#e.get(e);return Array.isArray(t)?[...t].filter(e=>e.config.once).map(e=>e.handler):[]}allListeners(e){if("string"!=typeof e)throw new TypeError("allListeners(event): event name must be a string");const t=this.#e.get(e);return Array.isArray(t)?[...t].map(e=>e.handler):[]}eventNames(){return[...this.#e.keys()]}emit(e,...t){const n=this.#s(e,"emit"),s=[];for(const e of n){const n=this.#e.get(e);Array.isArray(n)&&0!==n.length?(n.forEach(e=>e.handler(...t)),s.push(!0)):s.push(!1)}return s}setMaxListeners(e){if(!Number.isInteger(e)||e<0)throw new TypeError("setMaxListeners(n): n must be a non-negative integer");this.#t=e}getMaxListeners(){return this.#t}},s=new WeakMap,r=class{#o=new n;setThrowOnMaxListeners(e){return this.#o.setThrowOnMaxListeners(e)}getThrowOnMaxListeners(){return this.#o.getThrowOnMaxListeners()}prependListener(e,t){return this.#o.prependListener(e,t)}prependListenerOnce(e,t){return this.#o.prependListenerOnce(e,t)}appendListener(e,t){return this.#o.appendListener(e,t)}appendListenerOnce(e,t){return this.#o.appendListenerOnce(e,t)}on(e,t){return this.#o.on(e,t)}once(e,t){return this.#o.once(e,t)}off(e,t){return this.#o.off(e,t)}offAll(e){return this.#o.offAll(e)}offAllTypes(){return this.#o.offAllTypes()}listenerCount(e){return this.#o.listenerCount(e)}listeners(e){return this.#o.listeners(e)}onceListeners(e){return this.#o.onceListeners(e)}allListeners(e){return this.#o.allListeners(e)}eventNames(){return this.#o.eventNames()}setMaxListeners(e){return this.#o.setMaxListeners(e)}getMaxListeners(){return this.#o.getMaxListeners()}#a;#h;#d;#f=!1;#c=!1;#l=[];#p="__tinyIframeEvent__";get secretEventName(){return this.#p}set secretEventName(e){if("string"!=typeof e)throw new TypeError("TinyIframeEvents: secretEventName must be a string.");this.#p=e}constructor({targetIframe:e,targetOrigin:t}={}){if(!(void 0===e||e instanceof HTMLIFrameElement&&e.contentWindow))throw new TypeError('[TinyIframeEvents] Invalid "targetIframe" provided: expected a HTML Iframe Element, received '+typeof e);if(void 0!==t&&"string"!=typeof t)throw new TypeError('[TinyIframeEvents] Invalid "targetOrigin" provided: expected a string, received '+typeof t);if(this.#a=e?.contentWindow??window.parent,this.#h=t??window.location.origin,this.#d=e?"parent":"iframe",s.has(this.#a))throw new Error("Duplicate window reference.");this._boundOnMessage=this.#u.bind(this),this._boundOnceMessage=this.#g.bind(this),"complete"===this.#a.document.readyState||"interactive"===this.#a.document.readyState?this.#g():(this.#a.addEventListener("load",this._boundOnceMessage,!1),this.#a.addEventListener("DOMContentLoaded",this._boundOnceMessage,!1)),window.addEventListener("message",this._boundOnMessage,!1),s.set(this.#a,this)}#g(){this.#c||(this.#c=!0,this.#y())}#u(e){const{data:t,source:n}=e;if(null===(s=t)||"object"!=typeof s||Array.isArray(s)||"[object Object]"!==Object.prototype.toString.call(s)||!t[this.#p])return;var s;const{eventName:r,payload:i,direction:o}=t;n===this.#a&&("iframe"===this.#d&&"iframe"!==o||"parent"===this.#d&&"parent"!==o||this.#o.emit(r,i,e))}emit(e,t){if("string"!=typeof e)throw new TypeError("Event name must be a string.");if(this.#f)throw new Error("Cannot emit: instance has been destroyed.");const n={[this.#p]:!0,eventName:e,payload:t,direction:"parent"===this.#d?"iframe":"parent"};this.#c?this.#a.postMessage(n,this.#h):this.#l.push(n)}#y(){for(;this.#l.length;){const e=this.#l.shift();e&&this.#a.postMessage(e,this.#h)}}isDestroyed(){return this.#f}destroy(){this.#f=!0,window.removeEventListener("message",this._boundOnMessage),this.#a.removeEventListener("load",this._boundOnceMessage,!1),this.#a.removeEventListener("DOMContentLoaded",this._boundOnceMessage,!1),this.#o.offAllTypes(),this.#l=[],s.delete(this.#a)}};window.TinyIframeEvents=t.TinyIframeEvents})();
1
+ (()=>{"use strict";var e={d:(t,n)=>{for(var s in n)e.o(n,s)&&!e.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:n[s]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};function n(e){return null!==e&&"object"==typeof e&&!Array.isArray(e)&&"[object Object]"===Object.prototype.toString.call(e)}e.d(t,{TinyIframeEvents:()=>i});const s=class{#e=new Map;#t=10;#n=!1;#s(e,t){if("string"==typeof e)return[e];if(Array.isArray(e)&&e.every(e=>"string"==typeof e))return e;throw new TypeError(`${t}(event): event must be a string or string[]`)}setThrowOnMaxListeners(e){if("boolean"!=typeof e)throw new TypeError("setThrowOnMaxListeners(value): value must be a boolean");this.#n=e}getThrowOnMaxListeners(){return this.#n}#r(e,t,{once:n=!1}={}){let s=this.#e.get(e);Array.isArray(s)||(s=[],this.#e.set(e,s)),s.unshift({handler:t,config:{once:n}});const r=this.#t;if(r>0&&s.length>r){const t=`Possible memory leak detected. ${s.length} "${e}" listeners added. Use setMaxListeners() to increase limit.`;if(this.#n)throw new Error(t);console.warn(t)}}prependListener(e,t){const n=this.#s(e,"prependListener");if("function"!=typeof t)throw new TypeError("prependListener(event, handler): handler must be a function");for(const e of n)this.#r(e,t)}prependListenerOnce(e,t){const n=this.#s(e,"prependListenerOnce");if("function"!=typeof t)throw new TypeError("prependListenerOnce(event, handler): handler must be a function");const s=[];for(const e of n){const n=(...s)=>{this.off(e,n),t(...s)};this.#r(e,n,{once:!0}),s.push(n)}return s}#i(e,t,{once:n=!1}={}){const s=this.#s(e,"on");for(const e of s){let s=this.#e.get(e);Array.isArray(s)||(s=[],this.#e.set(e,s)),s.push({handler:t,config:{once:n}});const r=this.#t;if(r>0&&s.length>r){const t=`Possible memory leak detected. ${s.length} "${e}" listeners added. Use setMaxListeners() to increase limit.`;if(this.#n)throw new Error(t);console.warn(t)}}}on(e,t){if("function"!=typeof t)throw new TypeError("on(event, handler): handler must be a function");return this.#i(e,t)}once(e,t){const n=this.#s(e,"once");if("function"!=typeof t)throw new TypeError("once(event, handler): handler must be a function");const s=[];for(const e of n){const n=(...s)=>{this.off(e,n),t(...s)};this.#i(e,n,{once:!0}),s.push(n)}return s}appendListener(e,t){return this.on(e,t)}appendListenerOnce(e,t){return this.once(e,t)}off(e,t){const n=this.#s(e,"off");if("function"!=typeof t)throw new TypeError("off(event, handler): handler must be a function");for(const e of n){const n=this.#e.get(e);if(!Array.isArray(n))continue;const s=n.findIndex(e=>e.handler===t);-1!==s&&n.splice(s,1),0===n.length&&this.#e.delete(e)}}offAll(e){const t=this.#s(e,"offAll");for(const e of t)this.#e.delete(e)}offAllTypes(){this.#e.clear()}listenerCount(e){if("string"!=typeof e)throw new TypeError("listenerCount(event): event name must be a string");const t=this.#e.get(e);return Array.isArray(t)?t.length:0}listeners(e){if("string"!=typeof e)throw new TypeError("listeners(event): event name must be a string");const t=this.#e.get(e);return Array.isArray(t)?[...t].filter(e=>!e.config.once).map(e=>e.handler):[]}onceListeners(e){if("string"!=typeof e)throw new TypeError("onceListeners(event): event name must be a string");const t=this.#e.get(e);return Array.isArray(t)?[...t].filter(e=>e.config.once).map(e=>e.handler):[]}allListeners(e){if("string"!=typeof e)throw new TypeError("allListeners(event): event name must be a string");const t=this.#e.get(e);return Array.isArray(t)?[...t].map(e=>e.handler):[]}eventNames(){return[...this.#e.keys()]}emit(e,...t){const n=this.#s(e,"emit"),s=[];for(const e of n){const n=this.#e.get(e);Array.isArray(n)&&0!==n.length?(n.forEach(e=>e.handler(...t)),s.push(!0)):s.push(!1)}return s}setMaxListeners(e){if(!Number.isInteger(e)||e<0)throw new TypeError("setMaxListeners(n): n must be a non-negative integer");this.#t=e}getMaxListeners(){return this.#t}},r=new WeakMap,i=class{#o=new s;setThrowOnMaxListeners(e){return this.#o.setThrowOnMaxListeners(e)}getThrowOnMaxListeners(){return this.#o.getThrowOnMaxListeners()}prependListener(e,t){return this.#o.prependListener(e,t)}prependListenerOnce(e,t){return this.#o.prependListenerOnce(e,t)}appendListener(e,t){return this.#o.appendListener(e,t)}appendListenerOnce(e,t){return this.#o.appendListenerOnce(e,t)}on(e,t){return this.#o.on(e,t)}once(e,t){return this.#o.once(e,t)}off(e,t){return this.#o.off(e,t)}offAll(e){return this.#o.offAll(e)}offAllTypes(){return this.#o.offAllTypes()}listenerCount(e){return this.#o.listenerCount(e)}listeners(e){return this.#o.listeners(e)}onceListeners(e){return this.#o.onceListeners(e)}allListeners(e){return this.#o.allListeners(e)}eventNames(){return this.#o.eventNames()}setMaxListeners(e){return this.#o.setMaxListeners(e)}getMaxListeners(){return this.#o.getMaxListeners()}#a;get targetWindow(){return this.#a}#h;get targetIframeElement(){return this.#h}#d;get targetOrigin(){return this.#d}#f;get selfType(){return this.#f}#l=!1;#p=!1;get ready(){return this.#p}onReady(e){this.#p?e():this.#o.once(this.#m,e)}#c=null;#y=[];#u;#g;#m;#v=null;constructor({targetIframe:e,targetOrigin:t=window.location.origin,secretEventName:n="__tinyIframeEvent__",handshakeEventName:s="__tinyIframeHandshake__",readyEventName:i="__tinyIframeReady__"}={}){if(!(void 0===e||e instanceof HTMLIFrameElement&&e.contentWindow))throw new TypeError('[TinyIframeEvents] Invalid "targetIframe": expected HTMLIFrameElement, received '+typeof e);if("string"!=typeof t)throw new TypeError('[TinyIframeEvents] Invalid "targetOrigin": expected string, received '+typeof t);if(this.#h=e,this.#a=e?.contentWindow??window.parent,this.#d=t,this.#f=e?"parent":"iframe",this.#u=n,this.#g=s,this.#m=i,r.has(this.#a))throw new Error("Duplicate window reference.");this._boundWindowMessage=this.#w.bind(this),this._boundPortMessage=this.#E.bind(this),this.#L(),r.set(this.#a,this)}get secretEventName(){return this.#u}set secretEventName(e){if("string"!=typeof e)throw new TypeError("TinyIframeEvents: secretEventName must be a string.");this.#u=e}get handshakeEventName(){return this.#g}set handshakeEventName(e){if("string"!=typeof e)throw new TypeError("TinyIframeEvents: handshakeEventName must be a string.");this.#g=e}get readyEventName(){return this.#m}set readyEventName(e){if("string"!=typeof e)throw new TypeError("TinyIframeEvents: readyEventName must be a string.");this.#m=e}#L(){"parent"===this.#f?(this.#v=()=>{if(this.#l)return;this.#c&&this.#c.close();const e=new MessageChannel;this.#c=e.port1,this.#c.onmessage=this._boundPortMessage,this.#p=!1,this.#a.postMessage({type:this.#g},this.#d,[e.port2])},this.#h&&(this.#h.addEventListener("load",this.#v),this.#v())):window.addEventListener("message",this._boundWindowMessage,!1)}#w(e){const t=e.data,s=e.ports;n(t)&&t.type===this.#g&&0!==s.length&&(this.#c=s[0],this.#c.onmessage=this._boundPortMessage,window.removeEventListener("message",this._boundWindowMessage),this.#c.postMessage({type:`${this.#g}_ACK`}),this.#T())}#E(e){const t=e.data;if(n(t)&&t.type===`${this.#g}_ACK`)return void this.#T();if(!n(t)||!t[this.#u])return;const s=t.eventName,r=t.payload,i=t.direction;"string"!=typeof s||"iframe"===this.#f&&"iframe"!==i||"parent"===this.#f&&"parent"!==i||this.#o.emit(s,r,e)}#T(){this.#p||(this.#p=!0,this.#b(),this.#o.emit(this.#m))}#b(){for(;this.#y.length>0;){const e=this.#y.shift();e&&this.#c&&this.#c.postMessage(e)}}emit(e,t){if("string"!=typeof e)throw new TypeError("Event name must be a string.");if(this.#l)throw new Error("Cannot emit: instance has been destroyed.");const n={[this.#u]:!0,eventName:e,payload:t,direction:"parent"===this.#f?"iframe":"parent"};this.#p&&this.#c?this.#c.postMessage(n):this.#y.push(n)}isDestroyed(){return this.#l}destroy(){this.#l=!0,this.#p=!1,window.removeEventListener("message",this._boundWindowMessage),this.#h&&this.#v&&this.#h.removeEventListener("load",this.#v),this.#c&&(this.#c.close(),this.#c=null),this.#o.offAllTypes(),this.#y=[],r.delete(this.#a)}};window.TinyIframeEvents=t.TinyIframeEvents})();
@@ -8,8 +8,9 @@
8
8
  * This implementation ensures a uniform distribution of permutations.
9
9
  * Original algorithm source: StackOverflow (link above).
10
10
  *
11
- * @param {any[]} items - The array to shuffle.
12
- * @returns {any[]} The same array instance, now shuffled in place.
11
+ * @template {any[]} T
12
+ * @param {T} items - The array to shuffle.
13
+ * @returns {T} The same array instance, now shuffled in place.
13
14
  */
14
15
  function shuffleArray(items) {
15
16
  let currentIndex = items.length,
@@ -31,9 +32,9 @@ function shuffleArray(items) {
31
32
  /**
32
33
  * Generates an array with repeated phases according to counts.
33
34
  *
34
- * @param {string[]} phases - Array of phase names, e.g., ['Full', 'Half1', 'Half2', 'New'].
35
+ * @param {any[]} phases - Array of phase names, e.g., ['Full', 'Half1', 'Half2', 'New'].
35
36
  * @param {number[]} counts - Array of integers specifying how many times to repeat each phase, e.g., [4,5,5,4].
36
- * @returns {string[]} - Flattened array containing phases repeated according to counts, concatenated in order.
37
+ * @returns {any[]} - Flattened array containing phases repeated according to counts, concatenated in order.
37
38
  */
38
39
  function multiplyArrayBlocks(phases, counts) {
39
40
  // phases: array de strings, cada fase (ex: ['Full', 'Half1', 'Half2', 'New'])
@@ -4,18 +4,19 @@
4
4
  * This implementation ensures a uniform distribution of permutations.
5
5
  * Original algorithm source: StackOverflow (link above).
6
6
  *
7
- * @param {any[]} items - The array to shuffle.
8
- * @returns {any[]} The same array instance, now shuffled in place.
7
+ * @template {any[]} T
8
+ * @param {T} items - The array to shuffle.
9
+ * @returns {T} The same array instance, now shuffled in place.
9
10
  */
10
- export function shuffleArray(items: any[]): any[];
11
+ export function shuffleArray<T extends any[]>(items: T): T;
11
12
  /**
12
13
  * Generates an array with repeated phases according to counts.
13
14
  *
14
- * @param {string[]} phases - Array of phase names, e.g., ['Full', 'Half1', 'Half2', 'New'].
15
+ * @param {any[]} phases - Array of phase names, e.g., ['Full', 'Half1', 'Half2', 'New'].
15
16
  * @param {number[]} counts - Array of integers specifying how many times to repeat each phase, e.g., [4,5,5,4].
16
- * @returns {string[]} - Flattened array containing phases repeated according to counts, concatenated in order.
17
+ * @returns {any[]} - Flattened array containing phases repeated according to counts, concatenated in order.
17
18
  */
18
- export function multiplyArrayBlocks(phases: string[], counts: number[]): string[];
19
+ export function multiplyArrayBlocks(phases: any[], counts: number[]): any[];
19
20
  /**
20
21
  * Diff two class lists.
21
22
  * @param {any[]} oldItems
@@ -5,8 +5,9 @@
5
5
  * This implementation ensures a uniform distribution of permutations.
6
6
  * Original algorithm source: StackOverflow (link above).
7
7
  *
8
- * @param {any[]} items - The array to shuffle.
9
- * @returns {any[]} The same array instance, now shuffled in place.
8
+ * @template {any[]} T
9
+ * @param {T} items - The array to shuffle.
10
+ * @returns {T} The same array instance, now shuffled in place.
10
11
  */
11
12
  export function shuffleArray(items) {
12
13
  let currentIndex = items.length, randomIndex;
@@ -23,9 +24,9 @@ export function shuffleArray(items) {
23
24
  /**
24
25
  * Generates an array with repeated phases according to counts.
25
26
  *
26
- * @param {string[]} phases - Array of phase names, e.g., ['Full', 'Half1', 'Half2', 'New'].
27
+ * @param {any[]} phases - Array of phase names, e.g., ['Full', 'Half1', 'Half2', 'New'].
27
28
  * @param {number[]} counts - Array of integers specifying how many times to repeat each phase, e.g., [4,5,5,4].
28
- * @returns {string[]} - Flattened array containing phases repeated according to counts, concatenated in order.
29
+ * @returns {any[]} - Flattened array containing phases repeated according to counts, concatenated in order.
29
30
  */
30
31
  export function multiplyArrayBlocks(phases, counts) {
31
32
  // phases: array de strings, cada fase (ex: ['Full', 'Half1', 'Half2', 'New'])
@@ -14,18 +14,37 @@ const instances = new WeakMap();
14
14
  */
15
15
 
16
16
  /**
17
- * A flexible event routing system for structured communication
18
- * between a parent window and its iframe using `postMessage`.
17
+ * @typedef {object} TinyIframeEventsConfig
18
+ * Configuration object for initializing TinyIframeEvents.
19
+ * @property {HTMLIFrameElement} [targetIframe] - The target iframe element to post messages to. Required if instantiated in the parent window.
20
+ * @property {string} [targetOrigin] - The target origin to restrict messages to. Defaults to `window.location.origin`.
21
+ * @property {string} [secretEventName] - Custom internal name used to validate standard routing messages.
22
+ * @property {string} [handshakeEventName] - Custom internal name used for the initial MessageChannel handshake.
23
+ * @property {string} [readyEventName] - Custom internal name used for the ready event trigger.
24
+ */
25
+
26
+ /**
27
+ * @typedef {object} IframeEventBase
28
+ * Internal message structure for routed communication.
29
+ * @property {boolean} [secretIndicator] - Dynamic key based on secretEventName to validate the message.
30
+ * @property {string} eventName - The name of the custom event route.
31
+ * @property {any} payload - The data being sent (can be any type).
32
+ * @property {'iframe' | 'parent'} direction - Indicates the sender: 'iframe' or 'parent'.
33
+ */
34
+
35
+ /**
36
+ * A highly secure and flexible event routing system for structured communication
37
+ * between a parent window and its iframe using `MessageChannel`.
19
38
  *
20
- * This class abstracts the complexity of cross-origin and window-type handling,
21
- * allowing both the iframe and parent to:
22
- * - Send events with arbitrary payloads
23
- * - Listen to specific event names
24
- * - Filter events by origin and source
25
- * - Work symmetrically from both sides with automatic direction handling
39
+ * This class abstracts the complexity of cross-origin communication by establishing
40
+ * a direct, un-interceptable port connection after a secure initial handshake.
26
41
  *
27
- * Use this class when building applications that require modular, event-driven
28
- * communication across embedded frames.
42
+ * Features:
43
+ * - Secure direct pipeline via `MessageChannel`.
44
+ * - Customizable internal event names to avoid collisions.
45
+ * - Symmetrical usage for both parent and iframe.
46
+ * - Queue management for messages sent before the connection is established.
47
+ * - Auto-reconnects and resets ports if the target iframe is reloaded.
29
48
  */
30
49
  class TinyIframeEvents {
31
50
  #events = new TinyEvents();
@@ -219,36 +238,120 @@ class TinyIframeEvents {
219
238
  /** @type {Window} */
220
239
  #targetWindow;
221
240
 
241
+ get targetWindow() {
242
+ return this.#targetWindow;
243
+ }
244
+
245
+ /** @type {HTMLIFrameElement | undefined} */
246
+ #targetIframeElement;
247
+
248
+ get targetIframeElement() {
249
+ return this.#targetIframeElement;
250
+ }
251
+
222
252
  /** @type {string} */
223
253
  #targetOrigin;
224
254
 
225
- /** @type {string} */
255
+ get targetOrigin() {
256
+ return this.#targetOrigin;
257
+ }
258
+
259
+ /** @type {'iframe' | 'parent'} */
226
260
  #selfType;
227
261
 
262
+ get selfType() {
263
+ return this.#selfType;
264
+ }
265
+
228
266
  /** @type {boolean} */
229
267
  #isDestroyed = false;
230
268
 
231
269
  /** @type {boolean} */
232
270
  #ready = false;
233
271
 
272
+ get ready() {
273
+ return this.#ready;
274
+ }
275
+
234
276
  /**
235
- * @typedef {object} IframeEventBase
236
- * @property {string} eventName - The name of the custom event route.
237
- * @property {any} payload - The data being sent (can be any type).
238
- * @property {'iframe' | 'parent'} direction - Indicates the sender: 'iframe' or 'parent'.
277
+ * Executes the provided callback when the secure MessageChannel connection is fully established.
278
+ * If the connection is already ready, the callback is executed immediately.
279
+ *
280
+ * @param {function(): void} handler - The callback function to execute.
239
281
  */
282
+ onReady(handler) {
283
+ if (this.#ready) {
284
+ handler();
285
+ } else {
286
+ this.#events.once(this.#readyEventName, handler);
287
+ }
288
+ }
289
+
290
+ /** @type {MessagePort | null} */
291
+ #port = null;
292
+
293
+ /** @type {IframeEventBase[]} */
294
+ #pendingQueue = [];
295
+
296
+ /** @type {string} */
297
+ #secretEventName;
298
+
299
+ /** @type {string} */
300
+ #handshakeEventName;
301
+
302
+ /** @type {string} */
303
+ #readyEventName;
304
+
305
+ /** @type {(() => void) | null} */
306
+ #boundSendPort = null;
240
307
 
241
308
  /**
242
- * Queue of messages emitted before connection is ready
243
- * @type {IframeEventBase[]}
309
+ * Creates a new TinyIframeEvents instance to manage secure communication.
310
+ * Automatically establishes a MessageChannel connection between contexts.
311
+ *
312
+ * @param {TinyIframeEventsConfig} config - Configuration object.
244
313
  */
245
- #pendingQueue = [];
314
+ constructor({
315
+ targetIframe,
316
+ targetOrigin = window.location.origin,
317
+ secretEventName = '__tinyIframeEvent__',
318
+ handshakeEventName = '__tinyIframeHandshake__',
319
+ readyEventName = '__tinyIframeReady__',
320
+ } = {}) {
321
+ if (
322
+ targetIframe !== undefined &&
323
+ (!(targetIframe instanceof HTMLIFrameElement) || !targetIframe.contentWindow)
324
+ ) {
325
+ throw new TypeError(
326
+ `[TinyIframeEvents] Invalid "targetIframe": expected HTMLIFrameElement, received ${typeof targetIframe}`,
327
+ );
328
+ }
329
+
330
+ if (typeof targetOrigin !== 'string') {
331
+ throw new TypeError(
332
+ `[TinyIframeEvents] Invalid "targetOrigin": expected string, received ${typeof targetOrigin}`,
333
+ );
334
+ }
335
+
336
+ this.#targetIframeElement = targetIframe;
337
+ this.#targetWindow = targetIframe?.contentWindow ?? window.parent;
338
+ this.#targetOrigin = targetOrigin;
339
+ this.#selfType = !targetIframe ? 'iframe' : 'parent';
340
+ this.#secretEventName = secretEventName;
341
+ this.#handshakeEventName = handshakeEventName;
342
+ this.#readyEventName = readyEventName;
343
+
344
+ if (instances.has(this.#targetWindow)) throw new Error('Duplicate window reference.');
345
+
346
+ this._boundWindowMessage = this.#onWindowMessage.bind(this);
347
+ this._boundPortMessage = this.#onPortMessage.bind(this);
246
348
 
247
- /** @type {string} Internal message type for routed communication */
248
- #secretEventName = '__tinyIframeEvent__';
349
+ this.#initializeConnection();
350
+ instances.set(this.#targetWindow, this);
351
+ }
249
352
 
250
353
  /**
251
- * Gets the internal secret iframe event name.
354
+ * Gets the internal secret iframe event name used for validation.
252
355
  * @returns {string}
253
356
  */
254
357
  get secretEventName() {
@@ -267,89 +370,160 @@ class TinyIframeEvents {
267
370
  }
268
371
 
269
372
  /**
270
- * Creates a new TinyIframeEvents instance to manage communication between iframe and parent.
271
- * Automatically determines the current context (`iframe` or `parent`) based on the `targetWindow`.
272
- *
273
- * @param {Object} config - Configuration object.
274
- * @param {HTMLIFrameElement} [config.targetIframe] - The target window to post messages to. Defaults to `window.parent` (assumes this is inside an iframe).
275
- * @param {string} [config.targetOrigin] - The target origin to restrict messages to. Defaults to `window.location.origin`.
373
+ * Gets the internal handshake event name used for establishing the MessageChannel.
374
+ * @returns {string}
276
375
  */
277
- constructor({ targetIframe, targetOrigin } = {}) {
278
- if (
279
- typeof targetIframe !== 'undefined' &&
280
- (!(targetIframe instanceof HTMLIFrameElement) || !targetIframe.contentWindow)
281
- )
282
- throw new TypeError(
283
- `[TinyIframeEvents] Invalid "targetIframe" provided: expected a HTML Iframe Element, received ${typeof targetIframe}`,
284
- );
285
- if (typeof targetOrigin !== 'undefined' && typeof targetOrigin !== 'string')
286
- throw new TypeError(
287
- `[TinyIframeEvents] Invalid "targetOrigin" provided: expected a string, received ${typeof targetOrigin}`,
288
- );
289
-
290
- this.#targetWindow = targetIframe?.contentWindow ?? window.parent;
291
- this.#targetOrigin = targetOrigin ?? window.location.origin;
292
- this.#selfType = !targetIframe ? 'iframe' : 'parent';
293
- if (instances.has(this.#targetWindow)) throw new Error('Duplicate window reference.');
376
+ get handshakeEventName() {
377
+ return this.#handshakeEventName;
378
+ }
294
379
 
295
- this._boundOnMessage = this.#onMessage.bind(this);
296
- this._boundOnceMessage = this.#onceMessage.bind(this);
380
+ /**
381
+ * Sets the internal handshake event name.
382
+ * @param {string} name
383
+ * @throws {TypeError} If the value is not a string.
384
+ */
385
+ set handshakeEventName(name) {
386
+ if (typeof name !== 'string')
387
+ throw new TypeError('TinyIframeEvents: handshakeEventName must be a string.');
388
+ this.#handshakeEventName = name;
389
+ }
297
390
 
298
- if (
299
- this.#targetWindow.document.readyState === 'complete' ||
300
- this.#targetWindow.document.readyState === 'interactive'
301
- )
302
- this.#onceMessage();
303
- else {
304
- this.#targetWindow.addEventListener('load', this._boundOnceMessage, false);
305
- this.#targetWindow.addEventListener('DOMContentLoaded', this._boundOnceMessage, false);
306
- }
391
+ /**
392
+ * Gets the internal ready event name used when the connection is fully established.
393
+ * @returns {string}
394
+ */
395
+ get readyEventName() {
396
+ return this.#readyEventName;
397
+ }
307
398
 
308
- window.addEventListener('message', this._boundOnMessage, false);
309
- instances.set(this.#targetWindow, this);
399
+ /**
400
+ * Sets the internal ready event name.
401
+ * @param {string} name
402
+ * @throws {TypeError} If the value is not a string.
403
+ */
404
+ set readyEventName(name) {
405
+ if (typeof name !== 'string')
406
+ throw new TypeError('TinyIframeEvents: readyEventName must be a string.');
407
+ this.#readyEventName = name;
310
408
  }
311
409
 
312
410
  /**
313
- * Marks the communication as ready and flushes any queued messages.
411
+ * Initializes the correct connection strategy based on the current context (parent or iframe).
314
412
  */
315
- #onceMessage() {
316
- if (this.#ready) return;
317
- this.#ready = true;
318
- this.#flushQueue();
413
+ #initializeConnection() {
414
+ if (this.#selfType === 'parent') {
415
+ this.#boundSendPort = () => {
416
+ if (this.#isDestroyed) return;
417
+
418
+ // Closes any existing port to prevent memory leaks during iframe reloads
419
+ if (this.#port) {
420
+ this.#port.close();
421
+ }
422
+
423
+ // Creates a fresh channel for every attempt to prevent clone errors
424
+ const channel = new MessageChannel();
425
+ this.#port = channel.port1;
426
+ this.#port.onmessage = this._boundPortMessage;
427
+
428
+ // Resets ready state to require a new ACK
429
+ this.#ready = false;
430
+
431
+ this.#targetWindow.postMessage({ type: this.#handshakeEventName }, this.#targetOrigin, [
432
+ channel.port2,
433
+ ]);
434
+ };
435
+
436
+ if (this.#targetIframeElement) {
437
+ this.#targetIframeElement.addEventListener('load', this.#boundSendPort);
438
+ this.#boundSendPort();
439
+ }
440
+ } else {
441
+ window.addEventListener('message', this._boundWindowMessage, false);
442
+ }
319
443
  }
320
444
 
321
445
  /**
322
- * Internal handler for the message event. Filters and dispatches incoming messages.
446
+ * Internal handler for the initial window message event (used by iframe to receive the port).
323
447
  *
324
448
  * @param {MessageEvent<any>} event - The message event received via `postMessage`.
325
449
  */
326
- #onMessage(event) {
327
- const { data, source } = event;
450
+ #onWindowMessage(event) {
451
+ const data = event.data;
452
+ const ports = event.ports;
328
453
 
329
- // Reject non-object or unrelated messages
330
- if (!objChecker.isJsonObject(data) || !data[this.#secretEventName]) return;
454
+ if (!objChecker.isJsonObject(data) || data.type !== this.#handshakeEventName || ports.length === 0) return;
455
+
456
+ this.#port = ports[0];
457
+ this.#port.onmessage = this._boundPortMessage;
458
+ window.removeEventListener('message', this._boundWindowMessage);
459
+
460
+ this.#port.postMessage({ type: `${this.#handshakeEventName}_ACK` });
461
+ this.#markReady();
462
+ }
463
+
464
+ /**
465
+ * Internal handler for messages received through the secure MessageChannel port.
466
+ *
467
+ * @param {MessageEvent<any>} event - The message event received via `MessagePort`.
468
+ */
469
+ #onPortMessage(event) {
470
+ /** @type {any} */
471
+ const data = event.data;
331
472
 
332
- const { eventName, payload, direction } = data;
473
+ if (objChecker.isJsonObject(data) && data.type === `${this.#handshakeEventName}_ACK`) {
474
+ this.#markReady();
475
+ return;
476
+ }
333
477
 
334
- // Reject if not from the expected window (for security)
335
- if (source !== this.#targetWindow) return;
478
+ if (!objChecker.isJsonObject(data) || !data[this.#secretEventName]) return;
479
+
480
+ const eventName = data.eventName;
481
+ const payload = data.payload;
482
+ const direction = data.direction;
336
483
 
337
- // Reject if direction is not meant for this side
338
484
  if (
485
+ typeof eventName !== 'string' ||
339
486
  (this.#selfType === 'iframe' && direction !== 'iframe') ||
340
487
  (this.#selfType === 'parent' && direction !== 'parent')
341
- )
488
+ ) {
342
489
  return;
490
+ }
491
+
492
+ this.#events.emit(eventName, payload, event);
493
+ }
343
494
 
344
- this.#events.emit(/** @type {string} */ (eventName), payload, event);
495
+ /**
496
+ * Marks the communication as ready, flushes any queued messages,
497
+ * and triggers the internal ready event for any waiting listeners.
498
+ */
499
+ #markReady() {
500
+ if (this.#ready) return;
501
+ this.#ready = true;
502
+ this.#flushQueue();
503
+ this.#events.emit(this.#readyEventName);
345
504
  }
346
505
 
347
506
  /**
348
- * Sends an event to the target window.
507
+ * Sends all pending messages queued before the secure port was established.
508
+ */
509
+ #flushQueue() {
510
+ while (this.#pendingQueue.length > 0) {
511
+ /** @type {IframeEventBase | undefined} */
512
+ const data = this.#pendingQueue.shift();
513
+
514
+ if (data && this.#port) {
515
+ this.#port.postMessage(data);
516
+ }
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Sends an event to the target window through the secure MessageChannel.
349
522
  *
350
523
  * @param {string} eventName - A unique name identifying the event.
351
524
  * @param {*} payload - The data to send with the event. Can be any serializable value.
352
- * @throws {Error} If `eventName` is not a string.
525
+ * @throws {TypeError} If `eventName` is not a string.
526
+ * @throws {Error} If instance has been destroyed.
353
527
  */
354
528
  emit(eventName, payload) {
355
529
  if (typeof eventName !== 'string') throw new TypeError('Event name must be a string.');
@@ -363,44 +537,42 @@ class TinyIframeEvents {
363
537
  direction: this.#selfType === 'parent' ? 'iframe' : 'parent',
364
538
  };
365
539
 
366
- if (!this.#ready) {
540
+ if (!this.#ready || !this.#port) {
367
541
  this.#pendingQueue.push(message);
368
542
  return;
369
543
  }
370
544
 
371
- this.#targetWindow.postMessage(message, this.#targetOrigin);
372
- }
373
-
374
- /**
375
- * Sends all pending messages queued before handshake completion.
376
- *
377
- * @returns {void}
378
- */
379
- #flushQueue() {
380
- while (this.#pendingQueue.length) {
381
- const data = this.#pendingQueue.shift();
382
- if (data) this.#targetWindow.postMessage(data, this.#targetOrigin);
383
- }
545
+ this.#port.postMessage(message);
384
546
  }
385
547
 
386
548
  /**
387
549
  * Checks if the communication instance has been destroyed.
388
550
  *
389
- * @returns {boolean}
551
+ * @returns {boolean} True if destroyed, false otherwise.
390
552
  */
391
553
  isDestroyed() {
392
554
  return this.#isDestroyed;
393
555
  }
394
556
 
395
557
  /**
396
- * Unsubscribes all registered event listeners and removes the message handler.
558
+ * Unsubscribes all registered event listeners, closes the message port, and cleans up references.
397
559
  * Call this when the instance is no longer needed to prevent memory leaks.
398
560
  */
399
561
  destroy() {
400
562
  this.#isDestroyed = true;
401
- window.removeEventListener('message', this._boundOnMessage);
402
- this.#targetWindow.removeEventListener('load', this._boundOnceMessage, false);
403
- this.#targetWindow.removeEventListener('DOMContentLoaded', this._boundOnceMessage, false);
563
+ this.#ready = false;
564
+
565
+ window.removeEventListener('message', this._boundWindowMessage);
566
+
567
+ if (this.#targetIframeElement && this.#boundSendPort) {
568
+ this.#targetIframeElement.removeEventListener('load', this.#boundSendPort);
569
+ }
570
+
571
+ if (this.#port) {
572
+ this.#port.close();
573
+ this.#port = null;
574
+ }
575
+
404
576
  this.#events.offAllTypes();
405
577
  this.#pendingQueue = [];
406
578
  instances.delete(this.#targetWindow);