penpal
Version:
Penpal simplifies communication with iframes, workers, and windows by using promise-based methods on top of postMessage.
1 lines • 10.5 kB
JavaScript
var Penpal=function(e){"use strict";var s=class extends Error{code;constructor(e,s){super(s),this.name="PenpalError",this.code=e}},t=e=>({name:e.name,message:e.message,stack:e.stack,penpalCode:e instanceof s?e.code:void 0}),a=Symbol("Reply"),r=class{value;transferables;#e=a;constructor(e,s){this.value=e,this.transferables=s?.transferables}},n="penpal",o=e=>"object"==typeof e&&null!==e,i=e=>"function"==typeof e,l=e=>"SYN"===e.type,d=e=>"ACK1"===e.type,c=e=>"ACK2"===e.type,h=e=>"CALL"===e.type,g=e=>"REPLY"===e.type,m=(e,s=[])=>{const t=[];for(const a of Object.keys(e)){const r=e[a];i(r)?t.push([...s,a]):o(r)&&t.push(...m(r,[...s,a]))}return t},p=e=>e.join("."),u=(e,s,a)=>({namespace:n,channel:e,type:"REPLY",callId:s,isError:!0,...a instanceof Error?{value:t(a),isSerializedErrorInstance:!0}:{value:a}}),f=(e,t,a,l)=>{let d=!1;const c=async c=>{if(d)return;if(!h(c))return;l?.(`Received ${p(c.methodPath)}() call`,c);const{methodPath:g,args:m,id:f}=c;let v,M;try{const e=((e,s)=>{const t=e.reduce(((e,s)=>o(e)?e[s]:void 0),s);return i(t)?t:void 0})(g,t);if(!e)throw new s("METHOD_NOT_FOUND",`Method \`${p(g)}\` is not found.`);let l=await e(...m);l instanceof r&&(M=l.transferables,l=await l.value),v={namespace:n,channel:a,type:"REPLY",callId:f,value:l}}catch(e){v=u(a,f,e)}if(!d)try{l?.(`Sending ${p(g)}() reply`,v),e.sendMessage(v,M)}catch(s){throw"DataCloneError"===s.name&&(v=u(a,f,s),l?.(`Sending ${p(g)}() reply`,v),e.sendMessage(v)),s}};return e.addMessageHandler(c),()=>{d=!0,e.removeMessageHandler(c)}},v=crypto.randomUUID?.bind(crypto)??(()=>new Array(4).fill(0).map((()=>Math.floor(Math.random()*Number.MAX_SAFE_INTEGER).toString(16))).join("-")),M=Symbol("CallOptions"),w=class{transferables;timeout;#e=M;constructor(e){this.transferables=e?.transferables,this.timeout=e?.timeout}},y=new Set(["apply","call","bind"]),E=(e,s,t=[])=>new Proxy(t.length?()=>{}:Object.create(null),{get(a,r){if("then"!==r)return t.length&&y.has(r)?Reflect.get(a,r):E(e,s,[...t,r])},apply:(s,a,r)=>e(t,r)}),C=e=>new s("CONNECTION_DESTROYED",`Method call ${p(e)}() failed due to destroyed connection`),I=(e,t,a)=>{let r=!1;const o=new Map,i=e=>{if(!g(e))return;const{callId:t,value:r,isError:n,isSerializedErrorInstance:i}=e,l=o.get(t);l&&(o.delete(t),a?.(`Received ${p(l.methodPath)}() call`,e),n?l.reject(i?(({name:e,message:t,stack:a,penpalCode:r})=>{const n=r?new s(r,t):new Error(t);return n.name=e,n.stack=a,n})(r):r):l.resolve(r))};e.addMessageHandler(i);return{remoteProxy:E(((i,l)=>{if(r)throw C(i);const d=v(),c=l[l.length-1],h=c instanceof w,{timeout:g,transferables:m}=h?c:{},u=h?l.slice(0,-1):l;return new Promise(((r,l)=>{const c=void 0!==g?window.setTimeout((()=>{o.delete(d),l(new s("METHOD_CALL_TIMEOUT",`Method call ${p(i)}() timed out after ${g}ms`))}),g):void 0;o.set(d,{methodPath:i,resolve:r,reject:l,timeoutId:c});try{const s={namespace:n,channel:t,type:"CALL",id:d,methodPath:i,args:u};a?.(`Sending ${p(i)}() call`,s),e.sendMessage(s,m)}catch(e){l(new s("TRANSMISSION_FAILED",e.message))}}))}),a),destroy:()=>{r=!0,e.removeMessageHandler(i);for(const{methodPath:e,reject:s,timeoutId:t}of o.values())clearTimeout(t),s(C(e));o.clear()}}},O=()=>{let e,s;return{promise:new Promise(((t,a)=>{e=t,s=a})),resolve:e,reject:s}},R=class extends Error{constructor(e){super(`You've hit a bug in Penpal. Please file an issue with the following information: ${e}`)}},N="deprecated-penpal",S=e=>e.join("."),P=e=>new R(`Unexpected message to translate: ${JSON.stringify(e)}`),A=({messenger:e,methods:t,timeout:a,channel:r,log:o})=>{const i=v();let h;const g=[];let p=!1;const u=m(t),{promise:M,resolve:w,reject:y}=O(),E=void 0!==a?setTimeout((()=>{y(new s("CONNECTION_TIMEOUT",`Connection timed out after ${a}ms`))}),a):void 0,C=()=>{for(const e of g)e()},R=()=>{if(p)return;g.push(f(e,t,r,o));const{remoteProxy:s,destroy:a}=I(e,r,o);g.push(a),clearTimeout(E),p=!0,w({remoteProxy:s,destroy:C})},S=()=>{const t={namespace:n,type:"SYN",channel:r,participantId:i};o?.("Sending handshake SYN",t);try{e.sendMessage(t)}catch(e){y(new s("TRANSMISSION_FAILED",e.message))}},P=t=>{l(t)&&(t=>{if(o?.("Received handshake SYN",t),t.participantId===h&&h!==N)return;if(h=t.participantId,S(),!(i>h||h===N))return;const a={namespace:n,channel:r,type:"ACK1",methodPaths:u};o?.("Sending handshake ACK1",a);try{e.sendMessage(a)}catch(e){return void y(new s("TRANSMISSION_FAILED",e.message))}})(t),d(t)&&(t=>{o?.("Received handshake ACK1",t);const a={namespace:n,channel:r,type:"ACK2"};o?.("Sending handshake ACK2",a);try{e.sendMessage(a)}catch(e){return void y(new s("TRANSMISSION_FAILED",e.message))}R()})(t),c(t)&&(e=>{o?.("Received handshake ACK2",e),R()})(t)};return e.addMessageHandler(P),g.push((()=>e.removeMessageHandler(P))),S(),M},b=e=>{let s,t=!1;return(...a)=>(t||(t=!0,s=e(...a)),s)},T=new WeakSet,k=({messenger:e,methods:t={},timeout:a,channel:r,log:i})=>{if(!e)throw new s("INVALID_ARGUMENT","messenger must be defined");if(T.has(e))throw new s("INVALID_ARGUMENT","A messenger can only be used for a single connection");T.add(e);const l=[e.destroy],d=b((s=>{if(s){const s={namespace:n,channel:r,type:"DESTROY"};try{e.sendMessage(s)}catch(e){}}for(const e of l)e();i?.("Connection destroyed")})),c=e=>(e=>o(e)&&e.namespace===n)(e)&&e.channel===r;return{promise:(async()=>{try{e.initialize({log:i,validateReceivedMessage:c}),e.addMessageHandler((e=>{(e=>"DESTROY"===e.type)(e)&&d(!1)}));const{remoteProxy:s,destroy:n}=await A({messenger:e,methods:t,timeout:a,channel:r,log:i});return l.push(n),s}catch(e){throw d(!0),e}})(),destroy:()=>{d(!0)}}},L=class{#s;#t;#a;#r;#n;#o=new Set;#i;#l=!1;constructor({remoteWindow:e,allowedOrigins:t}){if(!e)throw new s("INVALID_ARGUMENT","remoteWindow must be defined");this.#s=e,this.#t=t?.length?t:[window.origin]}initialize=({log:e,validateReceivedMessage:s})=>{this.#a=e,this.#r=s,window.addEventListener("message",this.#d)};sendMessage=(e,s)=>{if(l(e)){const t=this.#c(e);this.#s.postMessage(e,{targetOrigin:t,transfer:s})}else if(d(e)||this.#l){const t=this.#l?(e=>{if(d(e))return{penpal:"synAck",methodNames:e.methodPaths.map(S)};if(h(e))return{penpal:"call",id:e.id,methodName:S(e.methodPath),args:e.args};if(g(e))return e.isError?{penpal:"reply",id:e.callId,resolution:"rejected",...e.isSerializedErrorInstance?{returnValue:e.value,returnValueIsError:!0}:{returnValue:e.value}}:{penpal:"reply",id:e.callId,resolution:"fulfilled",returnValue:e.value};throw P(e)})(e):e,a=this.#c(e);this.#s.postMessage(t,{targetOrigin:a,transfer:s})}else if(c(e)){const{port1:t,port2:a}=new MessageChannel;this.#i=t,t.addEventListener("message",this.#h),t.start();const r=[a,...s||[]],n=this.#c(e);this.#s.postMessage(e,{targetOrigin:n,transfer:r})}else{if(!this.#i)throw new R("Port is undefined");this.#i.postMessage(e,{transfer:s})}};addMessageHandler=e=>{this.#o.add(e)};removeMessageHandler=e=>{this.#o.delete(e)};destroy=()=>{window.removeEventListener("message",this.#d),this.#g(),this.#o.clear()};#m=e=>this.#t.some((s=>s instanceof RegExp?s.test(e):s===e||"*"===s));#c=e=>{if(l(e))return"*";if(!this.#n)throw new R("Concrete remote origin not set");return"null"===this.#n&&this.#t.includes("*")?"*":this.#n};#g=()=>{this.#i?.removeEventListener("message",this.#h),this.#i?.close(),this.#i=void 0};#d=({source:e,origin:s,ports:t,data:a})=>{if(e===this.#s&&((e=>o(e)&&"penpal"in e)(a)&&(this.#a?.("Please upgrade the child window to the latest version of Penpal."),this.#l=!0,a=(e=>{if("syn"===e.penpal)return{namespace:n,channel:void 0,type:"SYN",participantId:N};if("ack"===e.penpal)return{namespace:n,channel:void 0,type:"ACK2"};if("call"===e.penpal)return{namespace:n,channel:void 0,type:"CALL",id:e.id,methodPath:(s=e.methodName,s.split(".")),args:e.args};var s;if("reply"===e.penpal)return"fulfilled"===e.resolution?{namespace:n,channel:void 0,type:"REPLY",callId:e.id,value:e.returnValue}:{namespace:n,channel:void 0,type:"REPLY",callId:e.id,isError:!0,...e.returnValueIsError?{value:e.returnValue,isSerializedErrorInstance:!0}:{value:e.returnValue}};throw P(e)})(a)),this.#r?.(a)))if(this.#m(s)){if(l(a)&&(this.#g(),this.#n=s),c(a)&&!this.#l){if(this.#i=t[0],!this.#i)throw new R("No port received on ACK2");this.#i.addEventListener("message",this.#h),this.#i.start()}for(const e of this.#o)e(a)}else this.#a?.(`Received a message from origin \`${s}\` which did not match allowed origins \`[${this.#t.join(", ")}]\``)};#h=({data:e})=>{if(this.#r?.(e))for(const s of this.#o)s(e)}},D=class{#p;#r;#o=new Set;#i;constructor({worker:e}){if(!e)throw new s("INVALID_ARGUMENT","worker must be defined");this.#p=e}initialize=({validateReceivedMessage:e})=>{this.#r=e,this.#p.addEventListener("message",this.#u)};sendMessage=(e,s)=>{if(l(e)||d(e))this.#p.postMessage(e,{transfer:s});else{if(c(e)){const{port1:t,port2:a}=new MessageChannel;return this.#i=t,t.addEventListener("message",this.#u),t.start(),void this.#p.postMessage(e,{transfer:[a,...s||[]]})}if(!this.#i)throw new R("Port is undefined");this.#i.postMessage(e,{transfer:s})}};addMessageHandler=e=>{this.#o.add(e)};removeMessageHandler=e=>{this.#o.delete(e)};destroy=()=>{this.#p.removeEventListener("message",this.#u),this.#g(),this.#o.clear()};#g=()=>{this.#i?.removeEventListener("message",this.#u),this.#i?.close(),this.#i=void 0};#u=({ports:e,data:s})=>{if(this.#r?.(s)){if(l(s)&&this.#g(),c(s)){if(this.#i=e[0],!this.#i)throw new R("No port received on ACK2");this.#i.addEventListener("message",this.#u),this.#i.start()}for(const e of this.#o)e(s)}}},_=class{#i;#r;#o=new Set;constructor({port:e}){if(!e)throw new s("INVALID_ARGUMENT","port must be defined");this.#i=e}initialize=({validateReceivedMessage:e})=>{this.#r=e,this.#i.addEventListener("message",this.#u),this.#i.start()};sendMessage=(e,s)=>{this.#i?.postMessage(e,{transfer:s})};addMessageHandler=e=>{this.#o.add(e)};removeMessageHandler=e=>{this.#o.delete(e)};destroy=()=>{this.#i.removeEventListener("message",this.#u),this.#i.close(),this.#o.clear()};#u=({data:e})=>{if(this.#r?.(e))for(const s of this.#o)s(e)}},F={ConnectionDestroyed:"CONNECTION_DESTROYED",ConnectionTimeout:"CONNECTION_TIMEOUT",InvalidArgument:"INVALID_ARGUMENT",MethodCallTimeout:"METHOD_CALL_TIMEOUT",MethodNotFound:"METHOD_NOT_FOUND",TransmissionFailed:"TRANSMISSION_FAILED"},U=e=>(...s)=>{console.log(`✍️ %c${e}%c`,"font-weight: bold;","",...s)};return e.CallOptions=w,e.ErrorCode=F,e.PenpalError=s,e.PortMessenger=_,e.Reply=r,e.WindowMessenger=L,e.WorkerMessenger=D,e.connect=k,e.debug=U,e}({});//# sourceMappingURL=penpal.min.js.map