UNPKG

casement

Version:

An iFrame comms library for giving your app access to the outside world.

211 lines (210 loc) 10.5 kB
export class Peer { constructor() { this.allowSend = false; this.name = 'default'; this.debug = false; this.allowedDomain = ''; } loggy(...args) { if (this.debug) console.info(...args); } warny(...args) { if (this.debug) console.warn(...args); } capitalizeFirstLetter(str) { if (str.length === 0) { return str; // Return empty string if input is empty } const firstLetter = str.charAt(0).toUpperCase(); // Get the first letter and capitalize it const restOfWord = str.slice(1); // Get the remaining letters return firstLetter + restOfWord; } sendAgnostic(isInside, message, actionName) { const here = isInside ? 'inside' : 'outside'; const target = isInside ? window.parent : this.iFrame.contentWindow; if (!this.allowSend) { this.warny(`Casement: Cannot send message from ${here} window. The other side has not yet loaded, or has not yet confirmed that it is ready.`); return; } if (!actionName) { actionName = 'message'; this.loggy('Casement: No actionName was provided. Using default actionName "message".'); } // Send a message without expecting a response target.postMessage({ type: 'casement-message', from: here, name: this.name, message, actionName, }, this.allowedDomain); this.loggy('Casement: Sent message to inside window.'); } ; requestAgnostic(isInside, message, actionName) { const here = isInside ? 'inside' : 'outside'; const there = isInside ? 'outside' : 'inside'; const target = isInside ? window.parent : this.iFrame.contentWindow; if (!this.allowSend) { this.warny(`Casement: Cannot send request from ${here} window. The other side has not yet loaded, or has not yet confirmed that it is ready.`); return; } // Send a message and expect a response return new Promise((resolve) => { // post a message to the parent window const transmissionID = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); target.postMessage({ type: `casement-request`, from: here, name: this.name, transmissionID, message, actionName, }, this.allowedDomain); this.loggy(`Casement: Sent request to ${there} window, ID ${transmissionID}.`); // response handler const handleResponse = (event) => { if (this.domainCheck(event.origin) === false) { this.warny(`Casement: Received response from ${there} window, but the origin (${event.origin}) did not match the allowed domain of ${this.allowedDomain}.`); return; } this.loggy(`Casement: Received response from ${there} window, ID ${transmissionID}.`); if (event.data.type === `casement-response` && event.data.transmissionID === message.transmissionID && event.data.name === this.name && event.data.from === there) { window.removeEventListener('message', handleResponse); resolve(event.data.message); this.loggy(`Casement: Accepted response from ${there} window, ID ${transmissionID}.`); } else { this.warny(`Casement: Received response from ${there} window, but something about the response did not match the request. This is likely a bug.`, event); } }; window.addEventListener('message', handleResponse); this.loggy(`Casement: Listening for response from ${there} window, ID ${transmissionID}.`); }); } on(messageName, handler) { if (!this.onMessage) this.onMessage = []; this.onMessage.push({ name: messageName, callback: handler }); this.loggy(`Casement: Registered handler for message "${messageName}".`); } handleIncomingAgnostic(event, isInside) { const here = isInside ? 'inside' : 'outside'; const there = isInside ? 'outside' : 'inside'; const target = isInside ? window.parent : this.iFrame.contentWindow; this.loggy(`Casement: Incoming parser called.`, event); // Handle incoming messages. No response is needed. if (this.domainCheck(event.origin) === false) { this.warny(`Casement: Received message from ${there} window, but the origin (${event.origin}) did not match the allowed domain of ${this.allowedDomain}.`, event); return; } if (event.data.name !== this.name) { this.warny(`Casement: Received message from ${there} window, but the name (${event.data.name}) did not match the expected name of ${this.name}.`, event); return; } if (event.data.from !== there) { this.warny(`Casement: Received message appears to not come from ${there}. Not handling out of precaution.`, event); return; } switch (event.data.type) { case `casement-ready`: this.handleReadinessAgnostic(event, isInside); break; case `casement-message`: this.loggy(`Casement: Received categorized message from ${there} window.`); let didHandle = false; if (this.onMessage) this.onMessage.forEach((handler) => { if (handler.name === event.data.actionName || handler.name === '*') { this.loggy(`Casement: Accepted categorized message from ${there} window, actionName ${event.data.actionName}.`); handler.callback(event.data.message); didHandle = true; } }); // if there's no handler, warn the user if (!didHandle) this.warny(`Casement: Received a message from ${there} but no handler was set for action name ${event.data.actionName}. Make sure to use the ${this.capitalizeFirstLetter(here)}.on() method to set a handler for this request.`, event); break; // Handle incoming requests, specifically. Call all callbacks. case `casement-request`: this.loggy(`Casement: Received request from ${there} window, ID ${event.data.transmissionID}.`); if (this.onMessage) { target.postMessage({ type: `casement-response`, message: Promise.all(this.onMessage.map((handler) => { if (handler.name === event.data.actionName) { this.loggy(`Casement: Accepted request from ${there} window, actionName ${event.data.actionName}.`); return handler.callback(event.data.message); } else if (handler.name === '*') return handler.callback(event.data.message, event.data.actionName); })), name: this.name, transmissionID: event.data.transmissionID, from: here, }, this.allowedDomain); } else this.warny(`Casement: Received a request from ${there} but no handler was set. Make sure to use the ${this.capitalizeFirstLetter(here)}.on() method to set a handler for this request.`); break; } } handleReadinessAgnostic(event, isInside) { var _a; const here = isInside ? 'inside' : 'outside'; const there = isInside ? 'outside' : 'inside'; const name = event.data.name; this.loggy(`Casement: ${here} readiness handler called.`); // If the message is from the inside, send a ready message back and set the allowSend flag to true. // If the message is from the outside, set the allowSend flag to true. if (this.domainCheck(event.origin) === false) { this.loggy(`Casement: Received ready message from ${there} window, but the origin did not match the allowed domain.`, event); return; } if (name !== this.name) { this.loggy(`Casement: Received ready message from ${here} window, but the name did not match the allowed name.`, event); return; } switch (event.data.from) { // if the message came from the inside, send a ready message back and set the allowSend flag to true case 'inside': this.loggy(`Casement: Received ready message from inside window. Sending one back...`, event); this.allowSend = true; if ((_a = this.iFrame) === null || _a === void 0 ? void 0 : _a.contentWindow) this.iFrame.contentWindow.postMessage({ type: `casement-ready`, from: 'outside', name: this.name }, this.allowedDomain); if (this.onReady) this.onReady(this); break; // if the message came from the outside, set the allowSend flag to true case 'outside': if (event.data.name === this.name) this.loggy(`Casement: Received ready message from outside window. Data channel is now open and both sides can send messages.`, event); this.allowSend = true; if (this.onReady) this.onReady(this); break; default: this.loggy(`Casement: Received a malformed ready message missing a 'from' property.`, event); } } domainCheck(domainRaw) { if (this.allowedDomain === '*') return true; const domain = new URL(domainRaw).origin; if (domain === new URL(this.allowedDomain).origin) return true; else if (domain === window.location.origin) return true; else return false; } }