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.
- package/changelog/1/25/2.md +9 -0
- package/dist/v1/TinyEssentials.min.js +1 -1
- package/dist/v1/TinyIframeEvents.min.js +1 -1
- package/dist/v1/basics/array.cjs +5 -4
- package/dist/v1/basics/array.d.mts +7 -6
- package/dist/v1/basics/array.mjs +5 -4
- package/dist/v1/libs/TinyIframeEvents.cjs +266 -94
- package/dist/v1/libs/TinyIframeEvents.d.mts +119 -26
- package/dist/v1/libs/TinyIframeEvents.mjs +224 -86
- package/docs/v1/basics/array.md +2 -2
- package/docs/v1/libs/TinyIframeEvents.md +61 -32
- package/package.json +1 -1
|
@@ -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:()=>
|
|
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})();
|
package/dist/v1/basics/array.cjs
CHANGED
|
@@ -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
|
-
* @
|
|
12
|
-
* @
|
|
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 {
|
|
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 {
|
|
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
|
-
* @
|
|
8
|
-
* @
|
|
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:
|
|
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 {
|
|
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 {
|
|
17
|
+
* @returns {any[]} - Flattened array containing phases repeated according to counts, concatenated in order.
|
|
17
18
|
*/
|
|
18
|
-
export function multiplyArrayBlocks(phases:
|
|
19
|
+
export function multiplyArrayBlocks(phases: any[], counts: number[]): any[];
|
|
19
20
|
/**
|
|
20
21
|
* Diff two class lists.
|
|
21
22
|
* @param {any[]} oldItems
|
package/dist/v1/basics/array.mjs
CHANGED
|
@@ -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
|
-
* @
|
|
9
|
-
* @
|
|
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 {
|
|
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 {
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
|
21
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* @
|
|
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
|
-
*
|
|
243
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
*
|
|
271
|
-
*
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
*
|
|
411
|
+
* Initializes the correct connection strategy based on the current context (parent or iframe).
|
|
314
412
|
*/
|
|
315
|
-
#
|
|
316
|
-
if (this.#
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
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
|
-
#
|
|
327
|
-
const
|
|
450
|
+
#onWindowMessage(event) {
|
|
451
|
+
const data = event.data;
|
|
452
|
+
const ports = event.ports;
|
|
328
453
|
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
473
|
+
if (objChecker.isJsonObject(data) && data.type === `${this.#handshakeEventName}_ACK`) {
|
|
474
|
+
this.#markReady();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
333
477
|
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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.#
|
|
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
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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);
|