UNPKG

iframe.io

Version:

Easy and friendly API to connect and interact between content window and its containing iframe

561 lines (560 loc) 23.8 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); function newObject(data) { return JSON.parse(JSON.stringify(data)); } function getMessageSize(data) { try { return JSON.stringify(data).length; } catch (_a) { return 0; } } function sanitizePayload(payload, maxSize) { if (!payload) return payload; var size = getMessageSize(payload); if (size > maxSize) { throw new Error("Message size ".concat(size, " exceeds limit ").concat(maxSize)); } // Basic sanitization - remove functions and undefined values return JSON.parse(JSON.stringify(payload)); } var ackId = function () { var rmin = 100000, rmax = 999999; var timestamp = Date.now(); var random = Math.floor(Math.random() * (rmax - rmin + 1) + rmin); return "".concat(timestamp, "_").concat(random); }; var IOF = /** @class */ (function () { function IOF(options) { if (options === void 0) { options = {}; } this.messageQueue = []; this.messageRateTracker = []; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; if (options && typeof options !== 'object') throw new Error('Invalid Options'); this.options = __assign({ debug: false, heartbeatInterval: 30000, connectionTimeout: 10000, maxMessageSize: 1024 * 1024, maxMessagesPerSecond: 100, autoReconnect: true, messageQueueSize: 50 }, options); this.Events = {}; this.peer = { type: 'IFRAME', connected: false }; if (options.type) this.peer.type = options.type.toUpperCase(); } IOF.prototype.debug = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } this.options.debug && console.debug.apply(console, args); }; IOF.prototype.isConnected = function () { return !!this.peer.connected && !!this.peer.source; }; // Enhanced connection health monitoring IOF.prototype.startHeartbeat = function () { var _this = this; if (!this.options.heartbeatInterval) return; this.heartbeatTimer = setInterval(function () { if (_this.isConnected()) { var now = Date.now(); // Check if peer is still responsive if (_this.peer.lastHeartbeat && (now - _this.peer.lastHeartbeat) > (_this.options.heartbeatInterval * 2)) { _this.debug("[".concat(_this.peer.type, "] Heartbeat timeout detected")); _this.handleConnectionLoss(); return; } // Send heartbeat try { _this.emit('__heartbeat', { timestamp: now }); } catch (error) { _this.debug("[".concat(_this.peer.type, "] Heartbeat send failed:"), error); _this.handleConnectionLoss(); } } }, this.options.heartbeatInterval); }; IOF.prototype.stopHeartbeat = function () { if (!this.heartbeatTimer) return; clearInterval(this.heartbeatTimer); this.heartbeatTimer = undefined; }; // Handle connection loss and potential reconnection IOF.prototype.handleConnectionLoss = function () { if (!this.peer.connected) return; this.peer.connected = false; this.stopHeartbeat(); this.fire('disconnect', { reason: 'CONNECTION_LOST' }); this.options.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts && this.attemptReconnection(); }; IOF.prototype.attemptReconnection = function () { var _this = this; if (this.reconnectTimer) return; this.reconnectAttempts++; var delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 30000); // Exponential backoff, max 30s this.debug("[".concat(this.peer.type, "] Attempting reconnection ").concat(this.reconnectAttempts, "/").concat(this.maxReconnectAttempts, " in ").concat(delay, "ms")); this.fire('reconnecting', { attempt: this.reconnectAttempts, delay: delay }); this.reconnectTimer = setTimeout(function () { _this.reconnectTimer = undefined; // Re-initiate connection for WINDOW type _this.peer.type === 'WINDOW' && _this.peer.source && _this.peer.origin && _this.emit('ping'); // For IFRAME type, just wait for incoming connection // Set timeout for this reconnection attempt setTimeout(function () { if (!_this.peer.connected) { _this.reconnectAttempts < _this.maxReconnectAttempts ? _this.attemptReconnection() : _this.fire('reconnection_failed', { attempts: _this.reconnectAttempts }); } }, _this.options.connectionTimeout); }, delay); }; // Message rate limiting IOF.prototype.checkRateLimit = function () { if (!this.options.maxMessagesPerSecond) return true; var now = Date.now(), aSecondAgo = now - 1000; // Clean old entries this.messageRateTracker = this.messageRateTracker.filter(function (timestamp) { return timestamp > aSecondAgo; }); // Check if limit exceeded if (this.messageRateTracker.length >= this.options.maxMessagesPerSecond) { this.fire('error', { type: 'RATE_LIMIT_EXCEEDED', limit: this.options.maxMessagesPerSecond, current: this.messageRateTracker.length }); return false; } this.messageRateTracker.push(now); return true; }; // Queue messages when not connected IOF.prototype.queueMessage = function (_event, payload, fn) { if (this.messageQueue.length >= this.options.messageQueueSize) { // Remove oldest message var removed = this.messageQueue.shift(); this.debug("[".concat(this.peer.type, "] Message queue full, removed oldest message:"), removed === null || removed === void 0 ? void 0 : removed._event); } this.messageQueue.push({ _event: _event, payload: payload, fn: fn, timestamp: Date.now() }); this.debug("[".concat(this.peer.type, "] Queued message: ").concat(_event, " (queue size: ").concat(this.messageQueue.length, ")")); }; // Process queued messages when connection is established IOF.prototype.processMessageQueue = function () { var _this = this; if (!this.isConnected() || this.messageQueue.length === 0) return; this.debug("[".concat(this.peer.type, "] Processing ").concat(this.messageQueue.length, " queued messages")); var queue = __spreadArray([], this.messageQueue, true); this.messageQueue = []; queue.forEach(function (message) { try { _this.emit(message._event, message.payload, message.fn); } catch (error) { _this.debug("[".concat(_this.peer.type, "] Failed to send queued message:"), error); } }); }; /** * Establish a connection with an iframe containing * in the current window */ IOF.prototype.initiate = function (contentWindow, iframeOrigin) { var _this = this; if (!contentWindow || !iframeOrigin) throw new Error('Invalid Connection initiation arguments'); if (this.peer.type === 'IFRAME') throw new Error('Expect IFRAME to <listen> and WINDOW to <initiate> a connection'); // Clean up existing listener if any this.cleanup(); this.peer.source = contentWindow; this.peer.origin = iframeOrigin; this.peer.connected = false; this.reconnectAttempts = 0; this.messageListener = function (_a) { var origin = _a.origin, data = _a.data, source = _a.source; try { // Enhanced security: check valid message structure if (origin !== _this.peer.origin || !source || typeof data !== 'object' || !data.hasOwnProperty('_event')) return; var _b = data, _event = _b._event, payload = _b.payload, cid = _b.cid, timestamp = _b.timestamp; // Handle heartbeat responses if (_event === '__heartbeat_response') { _this.peer.lastHeartbeat = Date.now(); return; } // Handle heartbeat requests if (_event === '__heartbeat') { _this.emit('__heartbeat_response', { timestamp: Date.now() }); _this.peer.lastHeartbeat = Date.now(); return; } _this.debug("[".concat(_this.peer.type, "] Message: ").concat(_event), payload || ''); // Handshake or availability check events if (_event == 'pong') { // Content Window is connected to iframe _this.peer.connected = true; _this.reconnectAttempts = 0; _this.peer.lastHeartbeat = Date.now(); _this.startHeartbeat(); _this.fire('connect'); _this.processMessageQueue(); return _this.debug("[".concat(_this.peer.type, "] connected")); } // Fire available event listeners _this.fire(_event, payload, cid); } catch (error) { _this.debug("[".concat(_this.peer.type, "] Message handling error:"), error); _this.fire('error', { type: 'MESSAGE_HANDLING_ERROR', error: error instanceof Error ? error.message : String(error), origin: origin }); } }; window.addEventListener('message', this.messageListener, false); this.debug("[".concat(this.peer.type, "] Initiate connection: IFrame origin <").concat(iframeOrigin, ">")); this.emit('ping'); return this; }; /** * Listening to connection from the content window */ IOF.prototype.listen = function (hostOrigin) { var _this = this; this.peer.type = 'IFRAME'; // iframe.io connection listener is automatically set as IFRAME this.peer.connected = false; this.reconnectAttempts = 0; this.debug("[".concat(this.peer.type, "] Listening to connect").concat(hostOrigin ? ": Host <".concat(hostOrigin, ">") : '')); // Clean up existing listener if any this.cleanup(); this.messageListener = function (_a) { var origin = _a.origin, data = _a.data, source = _a.source; try { // Enhanced security: check host origin where event must only come from if (hostOrigin && hostOrigin !== origin) { _this.fire('error', { type: 'INVALID_ORIGIN', expected: hostOrigin, received: origin }); return; } // Enhanced security: check valid message structure if (!source || typeof data !== 'object' || !data.hasOwnProperty('_event')) return; // Define peer source window and origin if (!_this.peer.source) { _this.peer = __assign(__assign({}, _this.peer), { source: source, origin: origin }); _this.debug("[".concat(_this.peer.type, "] Connect to ").concat(origin)); } // Origin different from handshaked source origin else if (origin !== _this.peer.origin) { _this.fire('error', { type: 'ORIGIN_MISMATCH', expected: _this.peer.origin, received: origin }); return; } var _event = data._event, payload = data.payload, cid = data.cid, timestamp = data.timestamp; // Handle heartbeat responses if (_event === '__heartbeat_response') { _this.peer.lastHeartbeat = Date.now(); return; } // Handle heartbeat requests if (_event === '__heartbeat') { _this.emit('__heartbeat_response', { timestamp: Date.now() }); _this.peer.lastHeartbeat = Date.now(); return; } _this.debug("[".concat(_this.peer.type, "] Message: ").concat(_event), payload || ''); // Handshake or availability check events if (_event == 'ping') { _this.emit('pong'); // Iframe is connected to content window _this.peer.connected = true; _this.reconnectAttempts = 0; _this.peer.lastHeartbeat = Date.now(); _this.startHeartbeat(); _this.fire('connect'); _this.processMessageQueue(); return _this.debug("[".concat(_this.peer.type, "] connected")); } // Fire available event listeners _this.fire(_event, payload, cid); } catch (error) { _this.debug("[".concat(_this.peer.type, "] Message handling error:"), error); _this.fire('error', { type: 'MESSAGE_HANDLING_ERROR', error: error instanceof Error ? error.message : String(error), origin: origin }); } }; window.addEventListener('message', this.messageListener, false); return this; }; IOF.prototype.fire = function (_event, payload, cid) { var _this = this; // Volatile event - check if any listeners exist if (!this.Events[_event] && !this.Events[_event + '--@once']) return this.debug("[".concat(this.peer.type, "] No <").concat(_event, "> listener defined")); var ackFn = cid ? function (error) { var args = []; for (var _i = 1; _i < arguments.length; _i++) { args[_i - 1] = arguments[_i]; } _this.emit("".concat(_event, "--").concat(cid, "--@ack"), { error: error || false, args: args }); return; } : undefined; var listeners = []; if (this.Events[_event + '--@once']) { // Once triggable event _event += '--@once'; listeners = this.Events[_event]; // Delete once event listeners after fired delete this.Events[_event]; } else listeners = this.Events[_event]; // Fire listeners with error handling listeners.forEach(function (fn) { try { payload !== undefined ? fn(payload, ackFn) : fn(ackFn); } catch (error) { _this.debug("[".concat(_this.peer.type, "] Listener error for ").concat(_event, ":"), error); _this.fire('error', { type: 'LISTENER_ERROR', event: _event, error: error instanceof Error ? error.message : String(error) }); } }); }; IOF.prototype.emit = function (_event, payload, fn) { // Check rate limiting if (!this.checkRateLimit()) return this; // Queue message if not connected (except for connection-related events) if (!this.isConnected() && !['ping', 'pong', '__heartbeat', '__heartbeat_response'].includes(_event)) { this.queueMessage(_event, payload, fn); return this; } if (!this.peer.source) { this.fire('error', { type: 'NO_CONNECTION', event: _event }); return this; } if (typeof payload == 'function') { fn = payload; payload = undefined; } try { // Enhanced security: sanitize and validate payload var sanitizedPayload = payload ? sanitizePayload(payload, this.options.maxMessageSize) : payload; // Acknowledge event listener var cid = void 0; if (typeof fn === 'function') { var ackFunction_1 = fn; cid = ackId(); this.once("".concat(_event, "--").concat(cid, "--@ack"), function (_a) { var error = _a.error, args = _a.args; return ackFunction_1.apply(void 0, __spreadArray([error], args, false)); }); } var messageData = { _event: _event, payload: sanitizedPayload, cid: cid, timestamp: Date.now(), size: getMessageSize(sanitizedPayload) }; this.peer.source.postMessage(newObject(messageData), this.peer.origin); } catch (error) { this.debug("[".concat(this.peer.type, "] Emit error:"), error); this.fire('error', { type: 'EMIT_ERROR', event: _event, error: error instanceof Error ? error.message : String(error) }); // Call acknowledgment with error if provided if (typeof fn === 'function') { fn(error instanceof Error ? error.message : String(error)); } } return this; }; IOF.prototype.on = function (_event, fn) { // Add Event listener if (!this.Events[_event]) this.Events[_event] = []; this.Events[_event].push(fn); this.debug("[".concat(this.peer.type, "] New <").concat(_event, "> listener on")); return this; }; IOF.prototype.once = function (_event, fn) { // Add Once Event listener _event += '--@once'; if (!this.Events[_event]) this.Events[_event] = []; this.Events[_event].push(fn); this.debug("[".concat(this.peer.type, "] New <").concat(_event, " once> listener on")); return this; }; IOF.prototype.off = function (_event, fn) { // Remove Event listener if (fn && this.Events[_event]) { // Remove specific listener if provided var index = this.Events[_event].indexOf(fn); if (index > -1) { this.Events[_event].splice(index, 1); // Remove event array if empty if (this.Events[_event].length === 0) delete this.Events[_event]; } } // Remove all listeners for event else delete this.Events[_event]; typeof fn == 'function' && fn(); this.debug("[".concat(this.peer.type, "] <").concat(_event, "> listener off")); return this; }; IOF.prototype.removeListeners = function (fn) { // Clear all event listeners this.Events = {}; typeof fn == 'function' && fn(); this.debug("[".concat(this.peer.type, "] All listeners removed")); return this; }; IOF.prototype.emitAsync = function (_event, payload) { var _this = this; return new Promise(function (resolve, reject) { try { _this.emit(_event, payload, function (error) { var args = []; for (var _i = 1; _i < arguments.length; _i++) { args[_i - 1] = arguments[_i]; } error ? reject(new Error(typeof error === 'string' ? error : 'Ack error')) : resolve(args.length === 0 ? undefined : args.length === 1 ? args[0] : args); }); } catch (error) { reject(error); } }); }; IOF.prototype.onceAsync = function (_event) { var _this = this; return new Promise(function (resolve) { return _this.once(_event, resolve); }); }; IOF.prototype.connectAsync = function (timeout) { var _this = this; if (timeout === void 0) { timeout = 5000; } return new Promise(function (resolve, reject) { if (_this.isConnected()) return resolve(); var timeoutId = setTimeout(function () { _this.off('connect', connectHandler); reject(new Error('Connection timeout')); }, timeout); var connectHandler = function () { clearTimeout(timeoutId); resolve(); }; _this.once('connect', connectHandler); }); }; // Clean up all resources IOF.prototype.cleanup = function () { if (this.messageListener) { window.removeEventListener('message', this.messageListener); this.messageListener = undefined; } this.stopHeartbeat(); if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = undefined; } }; IOF.prototype.disconnect = function (fn) { // Clean disconnect method this.cleanup(); this.peer.connected = false; this.peer.source = undefined; this.peer.origin = undefined; this.peer.lastHeartbeat = undefined; this.messageQueue = []; this.messageRateTracker = []; this.reconnectAttempts = 0; this.removeListeners(); typeof fn == 'function' && fn(); this.debug("[".concat(this.peer.type, "] Disconnected")); return this; }; // Get connection statistics IOF.prototype.getStats = function () { return { connected: this.isConnected(), peerType: this.peer.type, origin: this.peer.origin, lastHeartbeat: this.peer.lastHeartbeat, queuedMessages: this.messageQueue.length, reconnectAttempts: this.reconnectAttempts, activeListeners: Object.keys(this.Events).length, messageRate: this.messageRateTracker.length }; }; // Clear message queue manually IOF.prototype.clearQueue = function () { var queueSize = this.messageQueue.length; this.messageQueue = []; this.debug("[".concat(this.peer.type, "] Cleared ").concat(queueSize, " queued messages")); return this; }; return IOF; }()); exports.default = IOF;