@powertoys/relay
Version:
Tab 2 tab communication on web applications
3 lines (2 loc) • 8.66 kB
JavaScript
import{jsx as e}from"react/jsx-runtime";import{createContext as t,useRef as s,useEffect as a,useContext as n,useState as r,useCallback as o}from"react";class i{subscribers=[];subscribe(e){return this.subscribers.push(e),()=>{const t=this.subscribers.indexOf(e);-1!==t&&this.subscribers.splice(t,1)}}next(e){this.subscribers.forEach((t=>{try{t.next(e)}catch(e){t.error?t.error(e):console.error("Unhandled error in subscriber:",e)}}))}error(e){this.subscribers.forEach((t=>{t.error&&t.error(e)}))}complete(){this.subscribers.forEach((e=>{e.complete&&e.complete()})),this.subscribers=[]}}let c=null;class l{port=null;tabInfo=null;heartbeatInterval=null;requestIdCounter=0;pendingRequests=new Map;handlers;isInitialized=!1;options;status="idle";channel;workerUrl;events$=new i;tabList$=new i;constructor(e){this.channel=new BroadcastChannel("relay-channel"),this.channel.onmessage=this.handleChannelMessage.bind(this),this.channel.postMessage("KNOCK"),setTimeout((()=>{"idle"===this.status&&(this.status="creating",this.channel.postMessage("CREATING"),setTimeout((()=>this.initialize()),300))}),Math.min(2e3*Math.random(),300)),this.options={heartbeatInterval:5e3,...e},this.handlers=e.handlers??{}}handleChannelMessage(e){const t=e.data;switch(t){case"KNOCK":switch(this.status){case"creating":this.channel.postMessage("CREATING");break;case"connected":this.channel.postMessage(this.workerUrl)}break;case"CREATING":if(this.isInitialized)return;this.status="waiting";break;default:t.startsWith("blob:")&&!this.isInitialized&&(this.status="connecting",this.workerUrl=t,this.initialize())}}async initialize(){if(!this.isInitialized&&"idle"!==this.status&&"waiting"!==this.status&&("connecting"!==this.status||this.workerUrl)){"creating"!==this.status||this.workerUrl||(this.workerUrl=function(){{const e=new Blob(['!function(){"use strict";const e=self,t=new Map,a=new Map;function o(e){console.warn("Unregistering client:",e);const o=a.get(e);if(t.delete(e),a.delete(e),o&&o.port)try{o.port.close()}catch(e){console.error("Error closing port:",e)}r(),console.log(`Tab unregistered. Current tabs: ${a.size}`)}function s(){return Array.from(a.values()).map((e=>({tabId:e.tabId,tabName:e.tabName,lastSeen:e.lastHeartbeat})))}function r(){const e=s();t.forEach((t=>{t.postMessage({type:"TAB_LIST",tabs:e})}))}console.log(`Relay worker started at ${(new Date).toISOString()}`),e.onconnect=function(e){const n=e.ports[0];let c=null,l=!0;console.log("New connection to worker established"),n.onmessage=function(e){const b=e.data;if(l)switch(b.type){case"REGISTER":if(a.has(b.tabId)&&(console.log("Tab already registered, cleaning up old connection"),o(b.tabId)),c=b.tabId,"*"===c)return console.error("Tab ID cannot be *, use * to broadcast to all clients"),void n.close();const e={tabId:c,tabName:b.tabName,port:n,lastHeartbeat:Date.now(),isActive:!0};!function(e,a){t.forEach((t=>{t.postMessage({type:"REGISTRATION",tabId:e,tabName:a})}))}(c,b.tabName),t.set(c,n),a.set(c,e),r();break;case"TEST_URL":n.postMessage({type:"TEST_URL_RESPONSE",tabs:s().length}),n.close();break;case"HEARTBEAT":if(b.tabId&&a.has(b.tabId)){const e=a.get(b.tabId);e.lastHeartbeat=Date.now(),a.set(b.tabId,e)}else console.log("heartbeat received for unknown tab",b.tabId);break;case"GET_TAB_LIST":n.postMessage({type:"TAB_LIST",tabs:s()});break;case"REQUEST_ACTION":const i=t.get(b.targetTabId);if(i&&c)i.postMessage({type:"ACTION_REQUEST",action:b.action,requestId:b.requestId,requestorId:c,payload:b.payload});else if(c)if("*"===b.targetTabId){Array.from(t.values()).filter((e=>e!==n)).forEach((e=>{e.postMessage({type:"ACTION_REQUEST",action:b.action,requestId:b.requestId,requestorId:c,payload:b.payload})}))}else n.postMessage({type:"ACTION_ERROR",requestId:b.requestId,error:"Target tab not available"});break;case"ACTION_RESPONSE":const d=t.get(b.requestorId);d&&d.postMessage({type:"ACTION_RESULT",requestId:b.requestId,result:b.result});break;case"UNREGISTER":console.log("Explicit unregister:",b.tabId),b.tabId&&(o(b.tabId),b.tabId===c&&(l=!1))}else console.log("Port is not active, ignoring message")},n.onmessageerror=function(){console.error("Message error on port"),console.log(`Port closed for tab ${c}`),c&&l&&(o(c),l=!1)},n.start()},setInterval((function(){const e=Date.now();let t=!1;a.forEach(((a,s)=>{e-a.lastHeartbeat>15e3&&(console.log(`Stale connection detected for tab ${s}, last seen ${(e-a.lastHeartbeat)/1e3}s ago`),o(s),t=!0)})),t&&r()}),1e4),setInterval((()=>{0===a.size&&self.close()}),1e4)}();\n//# sourceMappingURL=relay.worker.js.map\n'],{type:"application/javascript"});return URL.createObjectURL(e)}}(),this.channel.postMessage(this.workerUrl));try{if(this.options.getTabInfo)this.tabInfo=await this.options.getTabInfo();else{const e=crypto.randomUUID();this.tabInfo={tabId:e,tabName:`Tab ${e.slice(-8)}`}}if(!this.workerUrl)return;c=new SharedWorker(this.workerUrl),this.port=c.port,this.port.onmessage=this.handleWorkerMessage.bind(this),this.port.start(),this.status="connected",this.sendMessage({type:"REGISTER",tabId:this.tabInfo.tabId,tabName:this.tabInfo.tabName}),this.heartbeatInterval=window.setInterval((()=>{this.sendHeartbeat()}),this.options.heartbeatInterval),this.isInitialized=!0,this.events$.next({type:"connected",tabInfo:this.tabInfo}),this.getTabList(),window.addEventListener("beforeunload",this.cleanup.bind(this))}catch(e){throw console.error("Failed to initialize tab communicator:",e),e}}}sendHeartbeat(){this.tabInfo&&this.sendMessage({type:"HEARTBEAT",tabId:this.tabInfo.tabId})}getTabList(){this.sendMessage({type:"GET_TAB_LIST"})}requestAction(e,t,s,a,n){if(!this.port||!this.tabInfo)return n&&n("Not connected to shared worker"),null;const r=`req_${this.tabInfo.tabId}_${++this.requestIdCounter}`;return this.pendingRequests.set(r,{targetTabId:e,action:t,payload:s,onSuccess:a,onError:n}),this.sendMessage({type:"REQUEST_ACTION",targetTabId:e,action:t,requestId:r,payload:s}),r}async handleActionRequest(e){if(!this.port)return;const{requestId:t,requestorId:s,action:a,payload:n}=e;try{let e;if(!this.handlers[a])throw new Error(`Unknown action: ${a}`);e=await this.handlers[a](n,t,s),this.sendMessage({type:"ACTION_RESPONSE",requestId:t,requestorId:s,result:e})}catch(e){this.sendMessage({type:"ACTION_RESPONSE",requestId:t,requestorId:s,result:{error:e instanceof Error?e.message:"Unknown error"}})}}handleWorkerMessage(e){const t=e.data;switch(t.type){case"TAB_LIST":this.tabList$.next(t.tabs),this.events$.next({type:"tabListUpdated",tabs:t.tabs});break;case"REGISTRATION":this.events$.next({type:"tabRegistered",tabId:t.tabId,tabName:t.tabName});break;case"ACTION_RESULT":const e=this.pendingRequests.get(t.requestId);e&&(this.pendingRequests.delete(t.requestId),this.events$.next({type:"actionResult",requestId:t.requestId,result:t.result}),e.onSuccess&&e.onSuccess(t.result));break;case"ACTION_ERROR":const s=this.pendingRequests.get(t.requestId);s&&s.onError&&(this.pendingRequests.delete(t.requestId),s.onError(t.error));break;case"ACTION_REQUEST":this.handleActionRequest(t)}}sendMessage(e){this.port&&this.port.postMessage(e)}cleanup(){this.workerUrl&&URL.revokeObjectURL(this.workerUrl),console.warn(`Cleaning up Relay instance ${this.tabInfo?.tabId}`),this.heartbeatInterval&&(clearInterval(this.heartbeatInterval),this.heartbeatInterval=null),this.port&&this.tabInfo&&(this.sendMessage({type:"UNREGISTER",tabId:this.tabInfo.tabId}),this.port.close(),this.port=null),c=null,this.pendingRequests.clear(),this.requestIdCounter=0,this.isInitialized=!1,this.events$.next({type:"disconnected"})}onEvent(e){return this.events$.subscribe(e)}onTabListUpdated(e){return this.tabList$.subscribe(e)}getTabInfo(){return this.tabInfo}isConnected(){return this.isInitialized}}const b=t(null);function h({relay:t,children:n}){const r=s(!1);return a((()=>(r.current||(t.initialize().catch(console.error),r.current=!0),()=>{"production"!==process.env.NODE_ENV&&r.current||t.cleanup()})),[t]),e(b.Provider,{value:t,children:n})}function d(){const e=n(b);if(!e)throw new Error("useRelay must be used within a RelayProvider");return e}function u(){const e=d(),[t,s]=r(e.getTabInfo());return a((()=>e.onEvent({next:e=>{"connected"===e.type?s(e.tabInfo):"disconnected"===e.type&&s(null)}})),[e]),t}function I(){const e=d(),[t,s]=r([]);return a((()=>{const t=e.onTabListUpdated({next:e=>{s(e)}});return e.getTabList(),t}),[e]),t}function p(){const e=d();return o(((t,s,a,n,r)=>new Promise(((o,i)=>{e.requestAction(t,s,a,(e=>{n&&n(e),o(e)}),(e=>{r&&r(e),i(new Error(e))}))}))),[e])}export{i as Observable,l as Relay,h as RelayProvider,d as useRelay,p as useRequestAction,u as useTabInfo,I as useTabList};
//# sourceMappingURL=index.esm.js.map