penpal
Version:
A promise-based library for communicating with iframes via postMessage.
76 lines (75 loc) • 3.04 kB
JavaScript
import createDestructor from '../createDestructor';
import createLogger from '../createLogger';
import { ErrorCode, MessageType, NativeEventType } from '../enums';
import validateWindowIsIframe from './validateWindowIsIframe';
import handleSynAckMessageFactory from './handleSynAckMessageFactory';
import startConnectionTimeout from '../startConnectionTimeout';
const areGlobalsAccessible = () => {
try {
clearTimeout();
}
catch (e) {
return false;
}
return true;
};
/**
* Attempts to establish communication with the parent window.
*/
export default (options = {}) => {
const { parentOrigin = '*', methods = {}, timeout, debug = false } = options;
const log = createLogger(debug);
const destructor = createDestructor();
const { destroy, onDestroy } = destructor;
validateWindowIsIframe();
const handleSynAckMessage = handleSynAckMessageFactory(parentOrigin, methods, destructor, log);
const sendSynMessage = () => {
log('Child: Handshake - Sending SYN');
const synMessage = { penpal: MessageType.Syn };
const parentOriginForSyn = parentOrigin instanceof RegExp ? '*' : parentOrigin;
window.parent.postMessage(synMessage, parentOriginForSyn);
};
const promise = new Promise((resolve, reject) => {
const stopConnectionTimeout = startConnectionTimeout(timeout, destroy);
const handleMessage = (event) => {
// Under niche scenarios, we get into this function after
// the iframe has been removed from the DOM. In Edge, this
// results in "Object expected" errors being thrown when we
// try to access properties on window (global properties).
// For this reason, we try to access a global up front (clearTimeout)
// and if it fails we can assume the iframe has been removed
// and we ignore the message event.
if (!areGlobalsAccessible()) {
return;
}
if (event.source !== parent || !event.data) {
return;
}
if (event.data.penpal === MessageType.SynAck) {
const callSender = handleSynAckMessage(event);
if (callSender) {
window.removeEventListener(NativeEventType.Message, handleMessage);
stopConnectionTimeout();
resolve(callSender);
}
}
};
window.addEventListener(NativeEventType.Message, handleMessage);
sendSynMessage();
onDestroy((error) => {
window.removeEventListener(NativeEventType.Message, handleMessage);
if (!error) {
error = new Error('Connection destroyed');
error.code = ErrorCode.ConnectionDestroyed;
}
reject(error);
});
});
return {
promise,
destroy() {
// Don't allow consumer to pass an error into destroy.
destroy();
}
};
};