penpal
Version:
A promise-based library for communicating with iframes via postMessage.
1 lines • 7.26 kB
JavaScript
var Penpal=function(){"use strict";var e;(function(e){e["Call"]="call";e["Reply"]="reply";e["Syn"]="syn";e["SynAck"]="synAck";e["Ack"]="ack"})(e||(e={}));var n;(function(e){e["Fulfilled"]="fulfilled";e["Rejected"]="rejected"})(n||(n={}));var t;(function(e){e["ConnectionDestroyed"]="ConnectionDestroyed";e["ConnectionTimeout"]="ConnectionTimeout";e["NoIframeSrc"]="NoIframeSrc"})(t||(t={}));var o;(function(e){e["DataCloneError"]="DataCloneError"})(o||(o={}));var r;(function(e){e["Message"]="message"})(r||(r={}));var c=(e,n)=>{const t=[];let o=false;return{destroy(r){if(!o){o=true;n("".concat(e,": Destroying connection"));t.forEach(e=>{e(r)})}},onDestroy(e){o?e():t.push(e)}}};var s=e=>(...n)=>{if(e){console.log("[Penpal]",...n)}};const i={"http:":"80","https:":"443"};const a=/^(https?:)?\/\/([^/:]+)?(:(\d+))?/;const d=["file:","data:"];var l=e=>{if(e&&d.find(n=>e.startsWith(n))){return"null"}const n=document.location;const t=a.exec(e);let o;let r;let c;if(t){o=t[1]?t[1]:n.protocol;r=t[2];c=t[4]}else{o=n.protocol;r=n.hostname;c=n.port}const s=c&&c!==i[o]?":".concat(c):"";return"".concat(o,"//").concat(r).concat(s)};const u=({name:e,message:n,stack:t})=>({name:e,message:n,stack:t});const f=e=>{const n=new Error;Object.keys(e).forEach(t=>n[t]=e[t]);return n};var g=(t,c,s)=>{const{localName:i,local:a,remote:d,originForSending:l,originForReceiving:f}=t;let g=false;const m=t=>{if(t.source!==d||t.data.penpal!==e.Call){return}if(f!=="*"&&t.origin!==f){s("".concat(i," received message from origin ").concat(t.origin," which did not match expected origin ").concat(f));return}const r=t.data;const{methodName:a,args:m,id:p}=r;s("".concat(i,": Received ").concat(a,"() call"));const h=t=>r=>{s("".concat(i,": Sending ").concat(a,"() reply"));if(g){s("".concat(i,": Unable to send ").concat(a,"() reply due to destroyed connection"));return}const c={penpal:e.Reply,id:p,resolution:t,returnValue:r};if(t===n.Rejected&&r instanceof Error){c.returnValue=u(r);c.returnValueIsError=true}try{d.postMessage(c,l)}catch(t){if(t.name===o.DataCloneError){const o={penpal:e.Reply,id:p,resolution:n.Rejected,returnValue:u(t),returnValueIsError:true};d.postMessage(o,l)}throw t}};new Promise(e=>e(c[a].apply(c,m))).then(h(n.Fulfilled),h(n.Rejected))};a.addEventListener(r.Message,m);return()=>{g=true;a.removeEventListener(r.Message,m)}};let m=0;var p=()=>++m;const h=".";const v=e=>e?e.split(h):[];const y=e=>e.join(h);const w=(e,n)=>{const t=v(n||"");t.push(e);return y(t)};const C=(e,n,t)=>{const o=v(n);o.reduce((e,n,r)=>{if(typeof e[n]==="undefined"){e[n]={}}if(r===o.length-1){e[n]=t}return e[n]},e);return e};const E=(e,n)=>{const t={};Object.keys(e).forEach(o=>{const r=e[o];const c=w(o,n);if(typeof r==="object"){Object.assign(t,E(r,c))}if(typeof r==="function"){t[c]=r}});return t};const k=e=>{const n={};for(const t in e){C(n,t,e[t])}return n};var R=(o,c,s,i,a)=>{const{localName:d,local:l,remote:u,originForSending:g,originForReceiving:m}=c;let h=false;a("".concat(d,": Connecting call sender"));const v=o=>(...c)=>{a("".concat(d,": Sending ").concat(o,"() call"));let s;try{if(u.closed){s=true}}catch(e){s=true}if(s){i()}if(h){const e=new Error("Unable to send ".concat(o,"() call due ")+"to destroyed connection");e.code=t.ConnectionDestroyed;throw e}return new Promise((t,s)=>{const i=p();const h=c=>{if(c.source!==u||c.data.penpal!==e.Reply||c.data.id!==i){return}if(m!=="*"&&c.origin!==m){a("".concat(d," received message from origin ").concat(c.origin," which did not match expected origin ").concat(m));return}const g=c.data;a("".concat(d,": Received ").concat(o,"() reply"));l.removeEventListener(r.Message,h);let p=g.returnValue;if(g.returnValueIsError){p=f(p)}(g.resolution===n.Fulfilled?t:s)(p)};l.addEventListener(r.Message,h);const v={penpal:e.Call,id:i,methodName:o,args:c};u.postMessage(v,g)})};const y=s.reduce((e,n)=>{e[n]=v(n);return e},{});Object.assign(o,k(y));return()=>{h=true}};var S=(e,n,t,o,r)=>{const{destroy:c,onDestroy:s}=o;let i;let a;const d={};return o=>{if(n!=="*"&&o.origin!==n){r("Parent: Handshake - Received ACK message from origin ".concat(o.origin," which did not match expected origin ").concat(n));return}r("Parent: Handshake - Received ACK");const l={localName:"Parent",local:window,remote:o.source,originForSending:t,originForReceiving:n};if(i){i()}i=g(l,e,r);s(i);if(a){a.forEach(e=>{delete d[e]})}a=o.data.methodNames;const u=R(d,l,a,c,r);s(u);return d}};var N=(n,t,o,r)=>c=>{if(o!=="*"&&c.origin!==o){n("Parent: Handshake - Received SYN message from origin ".concat(c.origin," which did not match expected origin ").concat(o));return}n("Parent: Handshake - Received SYN, responding with SYN-ACK");const s={penpal:e.SynAck,methodNames:Object.keys(t)};c.source.postMessage(s,r)};const M=6e4;var A=(e,n)=>{const{destroy:t,onDestroy:o}=n;const r=setInterval(()=>{if(!e.isConnected){clearInterval(r);t()}},M);o(()=>{clearInterval(r)})};var P=(e,n)=>{let o;if(e!==undefined){o=window.setTimeout(()=>{const o=new Error("Connection timed out after ".concat(e,"ms"));o.code=t.ConnectionTimeout;n(o)},e)}return()=>{clearTimeout(o)}};var j=e=>{if(!e.src&&!e.srcdoc){const e=new Error("Iframe must have src or srcdoc property defined.");e.code=t.NoIframeSrc;throw e}};var D=n=>{let{iframe:t,methods:o={},childOrigin:i,timeout:a,debug:d=false}=n;const u=s(d);const f=c("Parent",u);const{onDestroy:g,destroy:m}=f;if(!i){j(t);i=l(t.src)}const p=i==="null"?"*":i;const h=E(o);const v=N(u,h,i,p);const y=S(h,i,p,f,u);const w=new Promise((n,o)=>{const c=P(a,m);const s=o=>{if(o.source!==t.contentWindow||!o.data){return}if(o.data.penpal===e.Syn){v(o);return}if(o.data.penpal===e.Ack){const e=y(o);if(e){c();n(e)}return}};window.addEventListener(r.Message,s);u("Parent: Awaiting handshake");A(t,f);g(e=>{window.removeEventListener(r.Message,s);if(e){o(e)}})});return{promise:w,destroy(){m()}}};var b=(n,t,o,r)=>{const{destroy:c,onDestroy:s}=o;return o=>{let i=n instanceof RegExp?n.test(o.origin):n==="*"||n===o.origin;if(!i){r("Child: Handshake - Received SYN-ACK from origin ".concat(o.origin," which did not match expected origin ").concat(n));return}r("Child: Handshake - Received SYN-ACK, responding with ACK");const a=o.origin==="null"?"*":o.origin;const d={penpal:e.Ack,methodNames:Object.keys(t)};window.parent.postMessage(d,a);const l={localName:"Child",local:window,remote:window.parent,originForSending:a,originForReceiving:o.origin};const u=g(l,t,r);s(u);const f={};const m=R(f,l,o.data.methodNames,c,r);s(m);return f}};const F=()=>{try{clearTimeout()}catch(e){return false}return true};var I=(n={})=>{const{parentOrigin:t="*",methods:o={},timeout:i,debug:a=false}=n;const d=s(a);const l=c("Child",d);const{destroy:u,onDestroy:f}=l;const g=E(o);const m=b(t,g,l,d);const p=()=>{d("Child: Handshake - Sending SYN");const n={penpal:e.Syn};const o=t instanceof RegExp?"*":t;window.parent.postMessage(n,o)};const h=new Promise((n,t)=>{const o=P(i,u);const c=t=>{if(!F()){return}if(t.source!==parent||!t.data){return}if(t.data.penpal===e.SynAck){const e=m(t);if(e){window.removeEventListener(r.Message,c);o();n(e)}}};window.addEventListener(r.Message,c);p();f(e=>{window.removeEventListener(r.Message,c);if(e){t(e)}})});return{promise:h,destroy(){u()}}};var L={connectToChild:D,connectToParent:I,ErrorCode:t};return L}();