crann
Version:
Effortless State Synchronization for Web Extensions
3 lines (2 loc) • 30.3 kB
JavaScript
import{createContext as ee,useContext as te,useState as F,useEffect as Q,useRef as q,useCallback as ne}from"react";import Z from"webextension-polyfill";var T=(c=>(c.ContentScript="contentscript",c.Extension="extension",c.Popup="popup",c.Sidepanel="sidepanel",c.Devtools="devtools",c.Options="options",c.Unknown="unknown",c))(T||{});var u=class extends Error{constructor(t,n,o){super(n);this.type=t;this.details=o;this.name="PorterError"}};function z(){return typeof ServiceWorkerGlobalScope!="undefined"&&self instanceof ServiceWorkerGlobalScope}var d=class d{constructor(e){this.context=e}static getLevel(){var t,n,o;return((t=d.globalOptions)==null?void 0:t.level)!==void 0?d.globalOptions.level:typeof process!="undefined"&&(((n=process.env)==null?void 0:n.NODE_ENV)==="production"||((o=process.env)==null?void 0:o.PORTER_ENV)==="production")?1:4}static configure(e){d.globalOptions=e,e.level!==void 0&&(d.level=e.level),e.enabled!==void 0&&(d.enabled=e.enabled)}static getLogger(e){return this.instances.has(e)||this.instances.set(e,new d(e)),this.instances.get(e)}error(e,...t){d.enabled&&d.level>=0&&console.error(`[Porter:${this.context}] ${e}`,...t)}warn(e,...t){d.enabled&&d.level>=1&&console.warn(`[Porter:${this.context}] ${e}`,...t)}info(e,...t){d.enabled&&d.level>=2&&console.info(`[Porter:${this.context}] ${e}`,...t)}debug(e,...t){d.enabled&&d.level>=3&&console.debug(`[Porter:${this.context}] ${e}`,...t)}trace(e,...t){d.enabled&&d.level>=4&&console.trace(`[Porter:${this.context}] ${e}`,...t)}};d.level=d.getLevel(),d.enabled=!1,d.instances=new Map;var A=d;import K from"webextension-polyfill";import{v4 as W}from"uuid";var w=class{constructor(e){this.logger=e;this.agents=new Map;this.agentsInfo=new Map;this.eventHandlers=new Map;this.eventHandlers.set("agentSetup",new Set),this.eventHandlers.set("agentMessage",new Set),this.eventHandlers.set("agentDisconnect",new Set)}addAgent(e,t){var g,h;this.logger.debug("Adding agent",{context:t,port:e});let n=this.identifyConnectionSource(e);if(!n){this.logger.error("Cannot add agent that did not have a sender");return}let o=n.context,r=n.tabId||-1,a=n.frameId||0;this.logger.debug("Determined context for new agent",{determinedContext:o,tabId:r,frameId:a});let c=Array.from(this.agentsInfo.values()).filter(p=>p.location.context===o&&p.location.tabId===r&&p.location.frameId===a);c.length>0&&this.logger.debug("Adding agent: Found existing similar agent.",{tabAgentsInfo:c});let l=((h=(g=this.getAgentByLocation({context:o,tabId:r,frameId:a}))==null?void 0:g.info)==null?void 0:h.id)||W();this.logger.debug(`Adding agent with id: ${l}`),this.agents.set(l,e);let f={id:l,location:{context:o,tabId:r,frameId:a},createdAt:Date.now(),lastActiveAt:Date.now()};this.agentsInfo.set(l,f),this.logger.debug(`Constructed agent info: ${JSON.stringify(f)}`),e.onMessage.addListener(p=>this.emit("agentMessage",p,f));let i={port:e,info:f};return e.onDisconnect.addListener(()=>{this.emit("agentDisconnect",f),this.logger.debug("Agent disconnected, removing from manager. ",{agentInfo:f}),this.removeAgent(l)}),this.emit("agentSetup",i),this.logger.debug("Setup complete for adding agent. ",{agentInfo:f}),l}getAgentByLocation(e){let{context:t,tabId:n,frameId:o}=e,r=Array.from(this.agentsInfo.entries()).find(([f,i])=>i.location.context===t&&i.location.tabId===n&&i.location.frameId===o);if(r===void 0)return this.logger.error("No agent found for location. ",{location:e}),null;let a=r[0],c=this.agents.get(a),l=this.agentsInfo.get(a);return!c||!l?(this.logger.error("No agent found for location. ",{location:e}),null):{port:c,info:l}}getAgentsByContext(e){return Array.from(this.agentsInfo.entries()).filter(([n,o])=>o.location.context===e).map(([n,o])=>({port:this.agents.get(n),info:o}))}getAllAgents(){return Array.from(this.agentsInfo.entries()).map(([t,n])=>({port:this.agents.get(t),info:n}))}queryAgents(e){return Array.from(this.agentsInfo.entries()).filter(([n,o])=>{let r=e.context?o.location.context===e.context:!0,a=e.tabId?o.location.tabId===e.tabId:!0,c=e.frameId?o.location.frameId===e.frameId:!0;return r&&a&&c}).map(([n,o])=>({port:this.agents.get(n),info:o}))}getAgentById(e){let t=this.agents.get(e),n=this.agentsInfo.get(e);return!t||!n?(this.logger.error("No agent found for agentId. ",{id:e}),null):{port:t,info:n}}getAllAgentsInfo(){return Array.from(this.agentsInfo.values())}hasPort(e){return!!Array.from(this.agents.values()).find(n=>n.name===e.name)}removeAgent(e){this.agents.has(e)&&this.agentsInfo.has(e)?(this.agents.delete(e),this.agentsInfo.delete(e)):this.logger.error("No agent found to remove. ",{agentId:e})}printAgents(){let e=Array.from(this.agents.entries()),t=Array.from(this.agentsInfo.entries());this.logger.debug("Current agents:",{allAgents:e,allAgentsInfo:t})}on(e,t){let n=this.eventHandlers.get(e);n&&n.add(t)}emit(e,...t){let n=this.eventHandlers.get(e);n==null||n.forEach(o=>o(...t))}identifyConnectionSource(e){var h,p,I,m,y,b,C;let t=e.sender;if(!t)return this.logger.error("Cannot add agent that did not have a sender"),null;let n=K.runtime.getManifest(),o=((h=n==null?void 0:n.side_panel)==null?void 0:h.default_path)||"",r=n.options_page||"",a=((p=n.action)==null?void 0:p.default_popup)||"",c=n.devtools_page||"",l=((I=n.chrome_url_overrides)==null?void 0:I.newtab)||"",f=((m=n.chrome_url_overrides)==null?void 0:m.bookmarks)||"",i=((y=n.chrome_url_overrides)==null?void 0:y.history)||"",g={sidepanel:o?o.split("/").pop():"sidepanel.html",options:r?r.split("/").pop():"options.html",popup:a?a.split("/").pop():"popup.html",devtools:c?c.split("/").pop():"devtools.html",newtab:l?l.split("/").pop():"newtab.html",bookmarks:f?f.split("/").pop():"bookmarks.html",history:i?i.split("/").pop():"history.html"};if(t.tab&&t.url&&!t.url.includes("extension://"))return{context:"contentscript",tabId:t.tab.id,frameId:t.frameId||0,url:t.url,portName:e.name};if(t.url&&t.url.includes("extension://")){let V=new URL(t.url).pathname.split("/").pop();for(let[B,S]of Object.entries(g))if(V===S)return{context:B,tabId:((b=t.tab)==null?void 0:b.id)||0,frameId:t.frameId||0,url:t.url,portName:e.name};return{context:"unknown",tabId:((C=t.tab)==null?void 0:C.id)||0,frameId:t.frameId||0,url:t.url,portName:e.name}}return{context:"unknown",tabId:0,url:t.url,portName:e.name}}};var P=class{constructor(e,t,n){this.agentOperations=e;this.namespace=t;this.logger=n}handleConnection(e){try{if(this.logger.info("New connection request:",e.name),!e.name)throw new u("invalid-context","Port name not provided");if(!e.name||!e.name.startsWith(this.namespace+":"))throw new u("invalid-context",`Invalid namespace or port name format. port name was ${(e==null?void 0:e.name)||"port undefined"} but namespace is ${this.namespace}`);e.onMessage.addListener(this.handleInitMessage.bind(this,e)),setTimeout(()=>{if(!this.agentOperations.hasPort(e))try{e.disconnect()}catch(t){this.logger.error("Failed to disconnect port:",t)}},5e3)}catch(t){this.handleConnectionError(e,t)}}handleInitMessage(e,t){if(t.action==="porter-init")try{e.onMessage.removeListener(this.handleInitMessage.bind(this,e));let{connectionId:n}=t.payload;if(!n)throw new u("invalid-context","Missing context or connection ID. Message was: "+JSON.stringify(t));let o=this.agentOperations.addAgent(e);if(!o)throw new u("invalid-context","Failed to add agent");let r=this.agentOperations.getAgentById(o);r&&this.confirmConnection(r),this.agentOperations.printAgents()}catch(n){this.handleConnectionError(e,n)}}handleConnectionError(e,t){let n=t instanceof u?t:new u("connection-failed",t instanceof Error?t.message:"Unknown connection error",{originalError:t});this.logger.error("Connection handling failed: ",{porterError:n});try{e.postMessage({action:"porter-error",payload:{error:n}})}catch(o){this.logger.error("Failed to send error message: ",{error:o})}try{e.disconnect()}catch(o){this.logger.error("Failed to disconnect port: ",{error:o})}}confirmConnection(e){if(this.logger.debug("Sending confirmation message back to initiator ",{agent:e}),!e.port)throw new u("invalid-port","Agent port is undefined when confirming connection");e.port.postMessage({action:"porter-handshake",payload:{info:e.info,currentConnections:this.agentOperations.getAllAgentsInfo()}})}};var E=class{constructor(e,t){this.agentOperations=e;this.logger=t;this.eventListeners=new Map;this.messageListeners=new Set;this.initializationHandler={"porter-messages-established":(n,o)=>{var a;if(!o||!o.id)return;let r=(a=this.agentOperations.getAgentById(o.id))==null?void 0:a.info;if(!r){this.logger.error("No agent info found for agent id: ",o.id);return}this.logger.debug("internalHandlers, established message received: ",o.id,n),this.emitEvent("onMessagesSet",r)}}}async post(e,t){return new Promise((n,o)=>{try{this.logger.debug("Post request received:",{action:e.action,target:t});let r=setTimeout(()=>{let a=new Error("Message posting timed out");this.logger.error("Post timeout:",a),o(a)},5e3);t===void 0?this.broadcastMessage(e):X(t)?this.postToLocation(e,t):J(t)?this.postToContext(e,t):typeof t=="string"?this.postToId(e,t):this.postToTab(e,t),clearTimeout(r),n()}catch(r){let a=r instanceof Error?r.message:"Unknown error";this.logger.error("Failed to post message:",a),o(new Error(`Failed to post message: ${a}`))}})}broadcastMessage(e){this.logger.info("Broadcasting message to all agents: ",e),this.agentOperations.getAllAgents().forEach(t=>{t.port&&t.port.postMessage(e)})}postToTab(e,t){let n=this.agentOperations.queryAgents({context:"contentscript",tabId:t});if(n.length===0){throw this.logger.warn("post: No agents found for tab: ",t),new u("message-failed",`Failed to post message to tabId ${t}`,{originalError:e});return}n.forEach(o=>{o.port&&this.postToPort(e,o.port)})}postToLocation(e,t){this.agentOperations.queryAgents(t).forEach(o=>{if(!o.port)throw new u("invalid-target","No port found for agent",{agentInfo:o.info});this.postToPort(e,o.port)})}postToContext(e,t){this.agentOperations.queryAgents({context:t}).forEach(o=>{if(!o.port)throw new u("invalid-target","No port found for agent",{agentInfo:o.info});this.postToPort(e,o.port)})}postToPort(e,t){try{t.postMessage(e)}catch(n){throw new u("message-failed","Failed to post message to port",{originalError:n,message:e})}}postToId(e,t){let n=this.agentOperations.getAgentById(t);if(!(n!=null&&n.port))throw new u("invalid-target",`No agent found for key: ${t}`);this.postToPort(e,n.port)}onMessage(e){Array.from(this.messageListeners).find(o=>JSON.stringify(o.config)===JSON.stringify(e))&&this.logger.warn(`Listener with same config already exists: ${JSON.stringify(e)}`);let n={config:e,listener:o=>{let r=e[o.message.action];if(r){this.logger.debug("onMessage, calling handler ",{event:o});let{message:a,...c}=o;r(a,c)}else this.logger.debug("onMessage, no handler found ",{event:o})}};return this.messageListeners.add(n),()=>{this.messageListeners.delete(n)}}on(e){return this.onMessage(e)}handleIncomingMessage(e,t){this.logger.debug("Received message",{message:e,info:t}),this.emitMessage({...t,message:e})}emitEvent(e,t){var n;this.logger.debug("emitting event: ",e,t),(n=this.eventListeners.get(e))==null||n.forEach(o=>o(t))}emitMessage(e){if(this.logger.debug("Dispatching incoming message to subscribers",{messageEvent:e}),e.message.action.startsWith("porter-")){let n=this.initializationHandler[e.message.action];if(n){this.logger.debug("Internal message being handled",{messageEvent:e});let{message:o,...r}=e;n(o,r);return}}e.message.target&&(this.logger.debug("Relaying message to target:",e.message.target),this.post(e.message,e.message.target));let t=0;this.logger.trace("Processing message with registered handlers");for(let{listener:n,config:o}of this.messageListeners)o[e.message.action]&&(n(e),t++,this.logger.debug("Message handled by registered listener: ",{listener:n,config:o}));t===0?this.logger.warn("No handler found for message:",e.message.action):this.logger.debug(`Message handled by ${t} registered listeners`)}addListener(e,t){return this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t),()=>{var n;(n=this.eventListeners.get(e))==null||n.delete(t)}}handleDisconnect(e){this.messageListeners.forEach(t=>{t.config[e.id]&&this.messageListeners.delete(t)}),this.logger.info("Agent disconnected:",{info:e}),this.emitEvent("onDisconnect",e)}handleConnect(e){this.logger.info("Agent connected:",{info:e}),this.emitEvent("onConnect",e)}onConnect(e){return this.addListener("onConnect",e)}onMessagesSet(e){return this.addListener("onMessagesSet",e)}onDisconnect(e){return this.addListener("onDisconnect",e)}};function X(s){return typeof s=="object"&&s!==null&&"context"in s&&"tabId"in s&&"frameId"in s}function J(s){return typeof s=="string"&&Object.values(T).includes(s)}var v=class v{constructor(e,t){if((t==null?void 0:t.debug)!==void 0&&A.configure({enabled:t.debug}),this.logger=A.getLogger("SW"),this.namespace=e||"porter",e||this.logger.error('No namespace provided, defaulting to "porter"'),this.agentManager=new w(this.logger),this.messageHandler=new E(this.agentManager,this.logger),this.connectionManager=new P(this.agentManager,this.namespace,this.logger),this.logger.info(`Constructing Porter with namespace: ${this.namespace}`),!z())throw new u("invalid-context","Can only create in a service worker");this.agentManager.on("agentMessage",(n,o)=>{this.messageHandler.handleIncomingMessage(n,o)}),this.agentManager.on("agentDisconnect",n=>{this.messageHandler.handleDisconnect(n)}),this.agentManager.on("agentSetup",n=>{this.logger.debug("Handling agent setup",{agent:n}),this.messageHandler.handleConnect(n.info),this.connectionManager.confirmConnection(n)}),Z.runtime.onConnect.addListener(this.connectionManager.handleConnection.bind(this.connectionManager))}static getInstance(e="porter",t){return v.staticLogger.debug(`Getting instance for namespace: ${e}`),v.instances.has(e)?(t==null?void 0:t.debug)!==void 0&&A.configure({enabled:t.debug}):(v.staticLogger.info(`Creating new instance for namespace: ${e}`),v.instances.set(e,new v(e,t))),v.instances.get(e)}post(e,t){return this.messageHandler.post(e,t)}onMessage(e){return this.messageHandler.onMessage(e)}on(e){return this.messageHandler.on(e)}onConnect(e){return this.messageHandler.onConnect(e)}onDisconnect(e){return this.messageHandler.onDisconnect(e)}onMessagesSet(e){return this.messageHandler.onMessagesSet(e)}getInfo(e){var t;return((t=this.agentManager.getAgentById(e))==null?void 0:t.info)||null}getAgentById(e){return this.agentManager.getAgentById(e)}getAgentByLocation(e){return this.agentManager.getAgentByLocation(e)}queryAgents(e){return this.agentManager.queryAgents(e)}};v.instances=new Map,v.staticLogger=A.getLogger("SW");var G=v;import Y from"webextension-polyfill";var L=class{constructor(e){this.queue=[];this.maxQueueSize=1e3;this.maxMessageAge=5*60*1e3;this.logger=e,this.logger.debug("MessageQueue initialized",{maxQueueSize:this.maxQueueSize,maxMessageAge:`${this.maxMessageAge/1e3} seconds`})}enqueue(e,t){let n=this.queue.length;this.cleanup(),n!==this.queue.length&&this.logger.debug(`Cleaned up ${n-this.queue.length} old messages`),this.queue.length>=this.maxQueueSize&&(this.logger.warn("Message queue is full, dropping oldest message",{queueSize:this.queue.length,maxSize:this.maxQueueSize}),this.queue.shift()),this.queue.push({message:e,target:t,timestamp:Date.now()}),this.logger.debug("Message queued",{queueSize:this.queue.length,message:e,target:t,timestamp:new Date().toISOString()})}dequeue(){let e=[...this.queue];return this.queue=[],this.logger.info(`Dequeued ${e.length} messages`,{oldestMessage:e[0]?new Date(e[0].timestamp).toISOString():null,newestMessage:e[e.length-1]?new Date(e[e.length-1].timestamp).toISOString():null}),e}isEmpty(){return this.queue.length===0}cleanup(){let e=Date.now(),t=this.queue.length;this.queue=this.queue.filter(n=>e-n.timestamp<this.maxMessageAge),t!==this.queue.length&&this.logger.debug(`Cleaned up ${t-this.queue.length} expired messages`,{remaining:this.queue.length,maxAge:`${this.maxMessageAge/1e3} seconds`})}};var D=class{constructor(e,t){this.namespace=e;this.CONNECTION_TIMEOUT=5e3;this.RECONNECT_INTERVAL=1e3;this.connectionTimer=null;this.reconnectTimer=null;this.agentInfo=null;this.port=null;this.isReconnecting=!1;this.reconnectAttemptCount=0;this.disconnectCallbacks=new Set;this.reconnectCallbacks=new Set;this.connectionId=`${Date.now()}-${Math.random().toString(36).substring(2,9)}`,this.logger=t,this.messageQueue=new L(t)}onDisconnect(e){return this.disconnectCallbacks.add(e),()=>{this.disconnectCallbacks.delete(e)}}onReconnect(e){return this.reconnectCallbacks.add(e),()=>{this.reconnectCallbacks.delete(e)}}emitDisconnect(){this.logger.debug("Emitting disconnect event",{callbackCount:this.disconnectCallbacks.size}),this.disconnectCallbacks.forEach(e=>{try{e()}catch(t){this.logger.error("Error in disconnect callback:",t)}})}emitReconnect(e){this.logger.debug("Emitting reconnect event",{callbackCount:this.reconnectCallbacks.size,info:e}),this.reconnectCallbacks.forEach(t=>{try{t(e)}catch(n){this.logger.error("Error in reconnect callback:",n)}})}async initializeConnection(){var e;try{this.connectionTimer&&clearTimeout(this.connectionTimer);let t=`${this.namespace}:${this.connectionId}`;this.logger.debug("Connecting new port with name: ",{portName:t}),this.port=Y.runtime.connect({name:t});let n=new Promise((o,r)=>{var l;let a=setTimeout(()=>r(new u("connection-timeout","Connection timed out waiting for handshake")),this.CONNECTION_TIMEOUT),c=f=>{var i,g;f.action==="porter-handshake"?(this.logger.debug("Received handshake:",f),clearTimeout(a),this.agentInfo=f.payload.info,this.logger.debug("Handshake agent info:",{agentInfo:this.agentInfo}),(i=this.port)==null||i.onMessage.removeListener(c),o()):f.action==="porter-error"&&(clearTimeout(a),(g=this.port)==null||g.onMessage.removeListener(c),this.logger.error("Error:",f),r(new u(f.payload.type,f.payload.message,f.payload.details)))};(l=this.port)==null||l.onMessage.addListener(c)});(e=this.port)==null||e.postMessage({action:"porter-init",payload:{info:this.agentInfo,connectionId:this.connectionId}}),await n,await this.processQueuedMessages()}catch(t){throw this.logger.error("Connection initialization failed:",t),this.handleDisconnect(this.port),t}}async processQueuedMessages(){if(!this.port||this.messageQueue.isEmpty())return;let e=this.messageQueue.dequeue();this.logger.info(`Processing ${e.length} queued messages after reconnection`);for(let{message:t,target:n}of e)try{let o={...t};n&&(o.target=n),this.port.postMessage(o),this.logger.debug("Successfully resent queued message:",{message:o})}catch(o){this.logger.error("Failed to process queued message:",o),this.messageQueue.enqueue(t,n),this.logger.debug("Re-queued failed message for retry")}}getPort(){return this.port}getAgentInfo(){return this.agentInfo}getNamespace(){return this.namespace}handleDisconnect(e){this.logger.info("Port disconnected",{portName:e.name,connectionId:this.connectionId,queuedMessages:this.messageQueue.isEmpty()?0:"some"}),this.port=null,this.agentInfo=null,this.emitDisconnect(),this.isReconnecting||this.startReconnectionAttempts()}startReconnectionAttempts(){this.isReconnecting=!0,this.reconnectAttemptCount=0,this.reconnectTimer&&clearInterval(this.reconnectTimer),this.logger.info("Starting reconnection attempts",{interval:this.RECONNECT_INTERVAL,queuedMessages:this.messageQueue.isEmpty()?0:"some"}),this.reconnectTimer=setInterval(async()=>{this.reconnectAttemptCount++;try{this.logger.debug(`Reconnection attempt ${this.reconnectAttemptCount}`),await this.initializeConnection(),this.isReconnecting=!1,this.reconnectTimer&&(clearInterval(this.reconnectTimer),this.reconnectTimer=null),this.logger.info("Reconnection successful",{attempts:this.reconnectAttemptCount,queuedMessages:this.messageQueue.isEmpty()?0:"some"}),this.agentInfo&&this.emitReconnect(this.agentInfo)}catch(e){this.logger.debug(`Reconnection attempt ${this.reconnectAttemptCount} failed:`,e)}},this.RECONNECT_INTERVAL)}queueMessage(e,t){this.messageQueue.enqueue(e,t),this.logger.debug("Message queued for retry",{message:e,target:t,queueSize:this.messageQueue.isEmpty()?0:"some"})}};var R=class{constructor(e){this.logger=e;this.MAX_QUEUE_SIZE=1e3;this.MESSAGE_TIMEOUT=3e4;this.messageQueue=[];this.handlers=new Map}handleMessage(e,t){if(this.logger.debug("handleMessage, message: ",t),this.handlers.size===0){if(this.messageQueue.length>=this.MAX_QUEUE_SIZE){this.logger.warn("Message queue full, dropping message:",t);return}this.logger.warn("No message handlers configured yet, queueing message: ",t),this.messageQueue.push({message:t,timestamp:Date.now()});return}this.processMessage(e,t)}onMessage(e){this.logger.debug("Setting message handlers from config: ",e),this.handlers.clear(),this.on(e),this.processQueuedMessages()}on(e){this.logger.debug("Adding message handlers from config: ",e),Object.entries(e).forEach(([t,n])=>{this.handlers.has(t)||this.handlers.set(t,[]),this.handlers.get(t).push(n)}),this.processQueuedMessages()}processQueuedMessages(){for(;this.messageQueue.length>0;){let e=this.messageQueue[0];if(Date.now()-e.timestamp>this.MESSAGE_TIMEOUT){this.logger.warn("Message timeout, dropping message: ",this.messageQueue.shift());continue}this.processMessage(null,e.message),this.messageQueue.shift()}}processMessage(e,t){let n=t.action,o=this.handlers.get(n)||[];o.length>0?(this.logger.debug(`Found ${o.length} handlers for action: ${n}`),o.forEach(r=>r(t))):this.logger.debug(`No handlers for message with action: ${n}`)}post(e,t,n){this.logger.debug("Sending message",{action:t.action,target:n,hasPayload:!!t.payload});try{n&&(t.target=n),e.postMessage(t)}catch(o){throw new u("message-failed","Failed to post message",{originalError:o,message:t,target:n})}}};var M=class M{constructor(e={}){var o,r;let t=(o=e.namespace)!=null?o:"porter",n=(r=e.agentContext)!=null?r:this.determineContext();e.debug!==void 0&&A.configure({enabled:e.debug}),this.logger=A.getLogger("Agent"),this.connectionManager=new D(t,this.logger),this.messageHandler=new R(this.logger),this.connectionManager.onReconnect(a=>{this.logger.info("Reconnected, re-wiring port listeners",{info:a}),this.setupPortListeners()}),this.logger.info("Initializing with options: ",{options:e,context:n}),this.initializeConnection()}static getInstance(e={}){return!M.instance||M.instance.connectionManager.getNamespace()!==e.namespace?M.instance=new M(e):e.debug!==void 0&&A.configure({enabled:e.debug}),M.instance}async initializeConnection(){await this.connectionManager.initializeConnection(),this.setupPortListeners()}setupPortListeners(){let e=this.connectionManager.getPort();e?(this.logger.debug("Setting up port listeners"),e.onMessage.addListener(t=>this.messageHandler.handleMessage(e,t)),e.onDisconnect.addListener(t=>this.connectionManager.handleDisconnect(t))):this.logger.warn("Cannot setup port listeners: no port available")}onMessage(e){this.messageHandler.onMessage(e);let t=this.connectionManager.getPort();t==null||t.postMessage({action:"porter-messages-established"})}on(e){this.messageHandler.on(e);let t=this.connectionManager.getPort();t==null||t.postMessage({action:"porter-messages-established"})}post(e,t){let n=this.connectionManager.getPort();if(this.logger.debug("Posting message",{message:e,target:t,port:n}),n)try{this.messageHandler.post(n,e,t)}catch(o){this.logger.error("Failed to post message, queueing for retry",{error:o}),this.connectionManager.queueMessage(e,t)}else this.logger.debug("No port found, queueing message",{message:e,target:t}),this.connectionManager.queueMessage(e,t)}determineContext(){return window.location.protocol.includes("extension")?"extension":"contentscript"}getAgentInfo(){return this.connectionManager.getAgentInfo()||null}onDisconnect(e){return this.connectionManager.onDisconnect(e)}onReconnect(e){return this.connectionManager.onReconnect(e)}};M.instance=null;var U=M;function k(s){let e=U.getInstance(s);return{type:"agent",post:e.post.bind(e),onMessage:e.onMessage.bind(e),on:e.on.bind(e),getAgentInfo:e.getAgentInfo.bind(e),onDisconnect:e.onDisconnect.bind(e),onReconnect:e.onReconnect.bind(e)}}import{useState as Mt,useEffect as St,useCallback as Tt,useRef as wt,useMemo as Pt}from"react";var _=class extends Error{constructor(e){super(e),this.name="CrannError",Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}};var O=class extends _{constructor(t,n){super(`${n==="store"?"Store":"Agent"} "${t}" has been ${n==="store"?"destroyed":"disconnected"} and cannot be used.`);this.storeName=t;this.entity=n;this.name="LifecycleError"}};var N=class{constructor(e,t={}){this._agentInfo=null;this._isConnected=!1;this._isDisconnected=!1;this.subscribers=new Set;this.readyCallbacks=new Set;this.disconnectCallbacks=new Set;this.reconnectCallbacks=new Set;var n;this.config=e,this.options=t,this._state=this.buildDefaultState(),this.readyPromise=new Promise(o=>{this.readyResolve=o}),this.porter=k({namespace:e.name,debug:(n=t.debug)!=null?n:!1}),this.setupMessageHandlers(),this.actionsProxy=this.createActionsProxy()}ready(){return this.assertNotDisconnected(),this.readyPromise}onReady(e){return this.assertNotDisconnected(),this._isConnected&&setTimeout(()=>e(this._state),0),this.readyCallbacks.add(e),()=>{this.readyCallbacks.delete(e)}}getState(){return this.assertNotDisconnected(),{...this._state}}get state(){return this.getState()}async setState(e){this.assertNotDisconnected(),this._state={...this._state,...e},this.porter.post({action:"setState",payload:{state:e}})}subscribe(e,t){this.assertNotDisconnected();let n,o;typeof e=="function"?o=e:(n=e,o=t);let r={keys:n,callback:o};return this.subscribers.add(r),()=>{this.subscribers.delete(r)}}get actions(){return this.assertNotDisconnected(),this.actionsProxy}getInfo(){return this._agentInfo?{id:this._agentInfo.id,tabId:this._agentInfo.location.tabId,frameId:this._agentInfo.location.frameId,context:this._agentInfo.location.context}:null}onDisconnect(e){return this.assertNotDisconnected(),this.disconnectCallbacks.add(e),()=>{this.disconnectCallbacks.delete(e)}}onReconnect(e){return this.assertNotDisconnected(),this.reconnectCallbacks.add(e),()=>{this.reconnectCallbacks.delete(e)}}disconnect(){this._isDisconnected||(this._isDisconnected=!0,this._isConnected=!1,this.subscribers.clear(),this.readyCallbacks.clear(),this.disconnectCallbacks.clear(),this.reconnectCallbacks.clear())}setupMessageHandlers(){this.porter.on({initialState:e=>{let{state:t,info:n}=e.payload;this._state=t,this._agentInfo=n,this._isConnected=!0,this.readyResolve(this._state),this.readyCallbacks.forEach(o=>{try{o(this._state)}catch(r){console.error("[Crann Agent] Error in onReady callback:",r)}}),this.notifySubscribers(t)},stateUpdate:e=>{let{state:t}=e.payload;this._state={...this._state,...t},this.notifySubscribers(t)}}),this.porter.onDisconnect(()=>{this._isConnected=!1,this.disconnectCallbacks.forEach(e=>{try{e()}catch(t){console.error("[Crann Agent] Error in onDisconnect callback:",t)}})}),this.porter.onReconnect(e=>{this._agentInfo=e,this._isConnected=!0,this.reconnectCallbacks.forEach(t=>{try{t(this._state)}catch(n){console.error("[Crann Agent] Error in onReconnect callback:",n)}})})}notifySubscribers(e){this.subscribers.forEach(t=>{if(!(t.keys&&!t.keys.some(o=>o in e)))try{t.callback(e,this._state)}catch(n){console.error("[Crann Agent] Error in subscriber callback:",n)}})}createActionsProxy(){let e=new Map,t=0;return this.porter.on({rpcResult:n=>{let{callId:o,result:r,error:a,success:c}=n.payload,l=e.get(o);l&&(e.delete(o),c?l.resolve(r):l.reject(new Error(a)))}}),new Proxy({},{get:(n,o)=>async(...r)=>{let a=`${t++}`;return new Promise((c,l)=>{e.set(a,{resolve:c,reject:l}),this.porter.post({action:"rpc",payload:{callId:a,actionName:o,args:r}})})}})}buildDefaultState(){let e={};for(let[t,n]of Object.entries(this.config))t==="name"||t==="version"||t==="actions"||typeof n=="object"&&n!==null&&"default"in n&&(e[t]=n.default);return e}assertNotDisconnected(){if(this._isDisconnected)throw new O(this.config.name,"agent")}};function H(s,e){return new N(s,e)}import{jsx as ie}from"react/jsx-runtime";function oe(s,e={}){let t=null,n=ee(null);function o(){return t||(t=H(s,{debug:e.debug})),t}function r(){let i=te(n),[g,h]=F(i!=null?i:t);return Q(()=>{g||h(o())},[g]),i!=null?i:g}function a(){let i=r(),[g,h]=F(!1);return Q(()=>{if(!i)return;i.getInfo()&&h(!0);let I=i.onReady(()=>{h(!0)}),m=i.onDisconnect(()=>{h(!1)}),y=i.onReconnect(()=>{h(!0)});return()=>{I(),m(),y()}},[i]),g}function c(i){let g=r(),h=typeof i=="function",p=h?i:b=>b[i],[I,m]=F(()=>{if(!g){let b=se(s);return p(b)}return p(g.getState())}),y=q(I);if(Q(()=>{if(!g)return;let b=g.getState(),C=p(b);return j(C,y.current)||(y.current=C,m(C)),g.subscribe((V,B)=>{let S=p(B);j(S,y.current)||(y.current=S,m(S))})},[g,p]),!h){let b=i,C=ne(async $=>{g&&await g.setState({[b]:$})},[g,b]);return[I,C]}return I}function l(){let i=r(),g=q(null);g.current=i;let h=q({}),p=q(null);return p.current||(p.current=new Proxy({},{get:(I,m)=>(h.current[m]||(h.current[m]=async(...y)=>{let b=g.current;if(!b)throw new Error(`Cannot call action "${m}" before agent is connected`);if(process.env.NODE_ENV!=="production"&&y.length>0){let C=y[0];re(C)&&console.warn(`[Crann] Action "${m}" was called with what looks like a DOM event. Events are not serializable and will cause the action to fail silently. Use onClick={() => ${m}()} instead of onClick={${m}}`)}return b.actions[m](...y)}),h.current[m])})),p.current}return{useCrannState:c,useCrannActions:l,useCrannReady:a,useAgent:r,CrannProvider:({agent:i,children:g})=>{let h=i!=null?i:o();return ie(n.Provider,{value:h,children:g})}}}function re(s){if(!s||typeof s!="object")return!1;let e=s;return"nativeEvent"in e&&"type"in e||"target"in e&&"currentTarget"in e&&"type"in e||"preventDefault"in e&&typeof e.preventDefault=="function"}function se(s){let e={};for(let[t,n]of Object.entries(s))t==="name"||t==="version"||t==="actions"||typeof n=="object"&&n!==null&&"default"in n&&(e[t]=n.default);return e}function j(s,e){if(s===e)return!0;if(typeof s!="object"||typeof e!="object"||s===null||e===null)return!1;let t=Object.keys(s),n=Object.keys(e);if(t.length!==n.length)return!1;for(let o of t)if(s[o]!==e[o])return!1;return!0}export{oe as createCrannHooks};
//# sourceMappingURL=react.js.map