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.
@@ -3,6 +3,52 @@ export default TinyIframeEvents;
3
3
  * A function to handle incoming event payloads.
4
4
  */
5
5
  export type handler = (payload: any, event: MessageEvent<any>) => any;
6
+ /**
7
+ * Configuration object for initializing TinyIframeEvents.
8
+ */
9
+ export type TinyIframeEventsConfig = {
10
+ /**
11
+ * - The target iframe element to post messages to. Required if instantiated in the parent window.
12
+ */
13
+ targetIframe?: HTMLIFrameElement | undefined;
14
+ /**
15
+ * - The target origin to restrict messages to. Defaults to `window.location.origin`.
16
+ */
17
+ targetOrigin?: string | undefined;
18
+ /**
19
+ * - Custom internal name used to validate standard routing messages.
20
+ */
21
+ secretEventName?: string | undefined;
22
+ /**
23
+ * - Custom internal name used for the initial MessageChannel handshake.
24
+ */
25
+ handshakeEventName?: string | undefined;
26
+ /**
27
+ * - Custom internal name used for the ready event trigger.
28
+ */
29
+ readyEventName?: string | undefined;
30
+ };
31
+ /**
32
+ * Internal message structure for routed communication.
33
+ */
34
+ export type IframeEventBase = {
35
+ /**
36
+ * - Dynamic key based on secretEventName to validate the message.
37
+ */
38
+ secretIndicator?: boolean | undefined;
39
+ /**
40
+ * - The name of the custom event route.
41
+ */
42
+ eventName: string;
43
+ /**
44
+ * - The data being sent (can be any type).
45
+ */
46
+ payload: any;
47
+ /**
48
+ * - Indicates the sender: 'iframe' or 'parent'.
49
+ */
50
+ direction: "iframe" | "parent";
51
+ };
6
52
  /**
7
53
  * @callback handler
8
54
  * A function to handle incoming event payloads.
@@ -10,32 +56,44 @@ export type handler = (payload: any, event: MessageEvent<any>) => any;
10
56
  * @param {MessageEvent<any>} event - Metadata about the message.
11
57
  */
12
58
  /**
13
- * A flexible event routing system for structured communication
14
- * between a parent window and its iframe using `postMessage`.
59
+ * @typedef {object} TinyIframeEventsConfig
60
+ * Configuration object for initializing TinyIframeEvents.
61
+ * @property {HTMLIFrameElement} [targetIframe] - The target iframe element to post messages to. Required if instantiated in the parent window.
62
+ * @property {string} [targetOrigin] - The target origin to restrict messages to. Defaults to `window.location.origin`.
63
+ * @property {string} [secretEventName] - Custom internal name used to validate standard routing messages.
64
+ * @property {string} [handshakeEventName] - Custom internal name used for the initial MessageChannel handshake.
65
+ * @property {string} [readyEventName] - Custom internal name used for the ready event trigger.
66
+ */
67
+ /**
68
+ * @typedef {object} IframeEventBase
69
+ * Internal message structure for routed communication.
70
+ * @property {boolean} [secretIndicator] - Dynamic key based on secretEventName to validate the message.
71
+ * @property {string} eventName - The name of the custom event route.
72
+ * @property {any} payload - The data being sent (can be any type).
73
+ * @property {'iframe' | 'parent'} direction - Indicates the sender: 'iframe' or 'parent'.
74
+ */
75
+ /**
76
+ * A highly secure and flexible event routing system for structured communication
77
+ * between a parent window and its iframe using `MessageChannel`.
15
78
  *
16
- * This class abstracts the complexity of cross-origin and window-type handling,
17
- * allowing both the iframe and parent to:
18
- * - Send events with arbitrary payloads
19
- * - Listen to specific event names
20
- * - Filter events by origin and source
21
- * - Work symmetrically from both sides with automatic direction handling
79
+ * This class abstracts the complexity of cross-origin communication by establishing
80
+ * a direct, un-interceptable port connection after a secure initial handshake.
22
81
  *
23
- * Use this class when building applications that require modular, event-driven
24
- * communication across embedded frames.
82
+ * Features:
83
+ * - Secure direct pipeline via `MessageChannel`.
84
+ * - Customizable internal event names to avoid collisions.
85
+ * - Symmetrical usage for both parent and iframe.
86
+ * - Queue management for messages sent before the connection is established.
87
+ * - Auto-reconnects and resets ports if the target iframe is reloaded.
25
88
  */
26
89
  declare class TinyIframeEvents {
27
90
  /**
28
- * Creates a new TinyIframeEvents instance to manage communication between iframe and parent.
29
- * Automatically determines the current context (`iframe` or `parent`) based on the `targetWindow`.
91
+ * Creates a new TinyIframeEvents instance to manage secure communication.
92
+ * Automatically establishes a MessageChannel connection between contexts.
30
93
  *
31
- * @param {Object} config - Configuration object.
32
- * @param {HTMLIFrameElement} [config.targetIframe] - The target window to post messages to. Defaults to `window.parent` (assumes this is inside an iframe).
33
- * @param {string} [config.targetOrigin] - The target origin to restrict messages to. Defaults to `window.location.origin`.
94
+ * @param {TinyIframeEventsConfig} config - Configuration object.
34
95
  */
35
- constructor({ targetIframe, targetOrigin }?: {
36
- targetIframe?: HTMLIFrameElement | undefined;
37
- targetOrigin?: string | undefined;
38
- });
96
+ constructor({ targetIframe, targetOrigin, secretEventName, handshakeEventName, readyEventName, }?: TinyIframeEventsConfig);
39
97
  /**
40
98
  * Enables or disables throwing an error when the maximum number of listeners is exceeded.
41
99
  *
@@ -156,6 +214,20 @@ declare class TinyIframeEvents {
156
214
  * @returns {number} The maximum number of listeners.
157
215
  */
158
216
  getMaxListeners(): number;
217
+ get targetWindow(): Window;
218
+ get targetIframeElement(): HTMLIFrameElement | undefined;
219
+ get targetOrigin(): string;
220
+ get selfType(): "iframe" | "parent";
221
+ get ready(): boolean;
222
+ /**
223
+ * Executes the provided callback when the secure MessageChannel connection is fully established.
224
+ * If the connection is already ready, the callback is executed immediately.
225
+ *
226
+ * @param {function(): void} handler - The callback function to execute.
227
+ */
228
+ onReady(handler: () => void): void;
229
+ _boundWindowMessage: (event: MessageEvent<any>) => void;
230
+ _boundPortMessage: (event: MessageEvent<any>) => void;
159
231
  /**
160
232
  * Sets the internal secret iframe event name.
161
233
  * @param {string} name
@@ -163,28 +235,49 @@ declare class TinyIframeEvents {
163
235
  */
164
236
  set secretEventName(name: string);
165
237
  /**
166
- * Gets the internal secret iframe event name.
238
+ * Gets the internal secret iframe event name used for validation.
167
239
  * @returns {string}
168
240
  */
169
241
  get secretEventName(): string;
170
- _boundOnMessage: (event: MessageEvent<any>) => void;
171
- _boundOnceMessage: () => void;
172
242
  /**
173
- * Sends an event to the target window.
243
+ * Sets the internal handshake event name.
244
+ * @param {string} name
245
+ * @throws {TypeError} If the value is not a string.
246
+ */
247
+ set handshakeEventName(name: string);
248
+ /**
249
+ * Gets the internal handshake event name used for establishing the MessageChannel.
250
+ * @returns {string}
251
+ */
252
+ get handshakeEventName(): string;
253
+ /**
254
+ * Sets the internal ready event name.
255
+ * @param {string} name
256
+ * @throws {TypeError} If the value is not a string.
257
+ */
258
+ set readyEventName(name: string);
259
+ /**
260
+ * Gets the internal ready event name used when the connection is fully established.
261
+ * @returns {string}
262
+ */
263
+ get readyEventName(): string;
264
+ /**
265
+ * Sends an event to the target window through the secure MessageChannel.
174
266
  *
175
267
  * @param {string} eventName - A unique name identifying the event.
176
268
  * @param {*} payload - The data to send with the event. Can be any serializable value.
177
- * @throws {Error} If `eventName` is not a string.
269
+ * @throws {TypeError} If `eventName` is not a string.
270
+ * @throws {Error} If instance has been destroyed.
178
271
  */
179
272
  emit(eventName: string, payload: any): void;
180
273
  /**
181
274
  * Checks if the communication instance has been destroyed.
182
275
  *
183
- * @returns {boolean}
276
+ * @returns {boolean} True if destroyed, false otherwise.
184
277
  */
185
278
  isDestroyed(): boolean;
186
279
  /**
187
- * Unsubscribes all registered event listeners and removes the message handler.
280
+ * Unsubscribes all registered event listeners, closes the message port, and cleans up references.
188
281
  * Call this when the instance is no longer needed to prevent memory leaks.
189
282
  */
190
283
  destroy(): void;
@@ -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);
@@ -4,11 +4,11 @@ A minimal and handy module offering a couple of focused utilities for working wi
4
4
 
5
5
  ---
6
6
 
7
- ## `shuffleArray(items: string[]): string[]`
7
+ ## `shuffleArray(items: any[]): any[]`
8
8
 
9
9
  🔄 A tiny utility for shuffling arrays using the good old Fisher–Yates algorithm. Simple, efficient, and perfectly random (as far as JavaScript's `Math.random()` allows, anyway).
10
10
 
11
- - **items** — An array of strings to shuffle.
11
+ - **items** — An array to shuffle.
12
12
  - **Returns** — The same array instance, but now shuffled.
13
13
 
14
14