porter-source
Version:
Messaging Library for Web Extensions
3 lines (2 loc) • 12.1 kB
JavaScript
import{useState as x,useEffect as V,useCallback as O,useRef as M,useMemo as G}from"react";var h=class extends Error{constructor(t,s,r){super(s);this.type=t;this.details=r;this.name="PorterError"}};import B from"webextension-polyfill";var E=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 s=this.queue.length;this.cleanup(),s!==this.queue.length&&this.logger.debug(`Cleaned up ${s-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(s=>e-s.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 R=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 E(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(s){this.logger.error("Error in reconnect callback:",s)}})}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=B.runtime.connect({name:t});let s=new Promise((r,g)=>{var m;let a=setTimeout(()=>g(new h("connection-timeout","Connection timed out waiting for handshake")),this.CONNECTION_TIMEOUT),v=i=>{var p,I;i.action==="porter-handshake"?(this.logger.debug("Received handshake:",i),clearTimeout(a),this.agentInfo=i.payload.info,this.logger.debug("Handshake agent info:",{agentInfo:this.agentInfo}),(p=this.port)==null||p.onMessage.removeListener(v),r()):i.action==="porter-error"&&(clearTimeout(a),(I=this.port)==null||I.onMessage.removeListener(v),this.logger.error("Error:",i),g(new h(i.payload.type,i.payload.message,i.payload.details)))};(m=this.port)==null||m.onMessage.addListener(v)});(e=this.port)==null||e.postMessage({action:"porter-init",payload:{info:this.agentInfo,connectionId:this.connectionId}}),await s,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:s}of e)try{let r={...t};s&&(r.target=s),this.port.postMessage(r),this.logger.debug("Successfully resent queued message:",{message:r})}catch(r){this.logger.error("Failed to process queued message:",r),this.messageQueue.enqueue(t,s),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 w=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,s])=>{this.handlers.has(t)||this.handlers.set(t,[]),this.handlers.get(t).push(s)}),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 s=t.action,r=this.handlers.get(s)||[];r.length>0?(this.logger.debug(`Found ${r.length} handlers for action: ${s}`),r.forEach(g=>g(t))):this.logger.debug(`No handlers for message with action: ${s}`)}post(e,t,s){this.logger.debug("Sending message",{action:t.action,target:s,hasPayload:!!t.payload});try{s&&(t.target=s),e.postMessage(t)}catch(r){throw new h("message-failed","Failed to post message",{originalError:r,message:t,target:s})}}};var o=class o{constructor(e){this.context=e}static getLevel(){var t,s;return((t=o.globalOptions)==null?void 0:t.level)!==void 0?o.globalOptions.level:typeof process!="undefined"&&((s=process.env)==null?void 0:s.PORTER_ENV)==="production"?1:4}static configure(e){o.globalOptions=e,e.level!==void 0&&(o.level=e.level),e.enabled!==void 0&&(o.enabled=e.enabled)}static getLogger(e){return this.instances.has(e)||this.instances.set(e,new o(e)),this.instances.get(e)}error(e,...t){o.enabled&&o.level>=0&&console.error(`[Porter:${this.context}] ${e}`,...t)}warn(e,...t){o.enabled&&o.level>=1&&console.warn(`[Porter:${this.context}] ${e}`,...t)}info(e,...t){o.enabled&&o.level>=2&&console.info(`[Porter:${this.context}] ${e}`,...t)}debug(e,...t){o.enabled&&o.level>=3&&console.debug(`[Porter:${this.context}] ${e}`,...t)}trace(e,...t){o.enabled&&o.level>=4&&console.trace(`[Porter:${this.context}] ${e}`,...t)}};o.level=o.getLevel(),o.enabled=!1,o.instances=new Map;var b=o;var l=class l{constructor(e={}){var r,g;let t=(r=e.namespace)!=null?r:"porter",s=(g=e.agentContext)!=null?g:this.determineContext();e.debug!==void 0&&b.configure({enabled:e.debug}),this.logger=b.getLogger("Agent"),this.connectionManager=new R(t,this.logger),this.messageHandler=new w(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:s}),this.initializeConnection()}static getInstance(e={}){return!l.instance||l.instance.connectionManager.getNamespace()!==e.namespace?l.instance=new l(e):e.debug!==void 0&&b.configure({enabled:e.debug}),l.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 s=this.connectionManager.getPort();if(this.logger.debug("Posting message",{message:e,target:t,port:s}),s)try{this.messageHandler.post(s,e,t)}catch(r){this.logger.error("Failed to post message, queueing for retry",{error:r}),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)}};l.instance=null;var P=l;function L(n){let e=P.getInstance(n);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)}}function H(n){let[e,t]=x(!1),[s,r]=x(!1),[g,a]=x(null),[v,m]=x(null),i=M(null),p=M(null),I=M(null),A=M([]),S=G(()=>({agentContext:n==null?void 0:n.agentContext,namespace:n==null?void 0:n.namespace,debug:n==null?void 0:n.debug}),[n==null?void 0:n.agentContext,n==null?void 0:n.namespace,n==null?void 0:n.debug]),y=M(n==null?void 0:n.onDisconnect),C=M(n==null?void 0:n.onReconnect);y.current=n==null?void 0:n.onDisconnect,C.current=n==null?void 0:n.onReconnect,V(()=>{let c=!0;return(async()=>{try{let{post:u,on:N,getAgentInfo:D,onDisconnect:Q,onReconnect:$}=L(S);if(c){i.current=u,p.current=N,I.current=D,t(!0),r(!1),a(null);let z=Q(()=>{var d;c&&(t(!1),r(!0),m(null),(d=y.current)==null||d.call(y))});A.current.push(z);let U=$(d=>{var T;c&&(t(!0),r(!1),m(d),(T=C.current)==null||T.call(C,d))});A.current.push(U),N({"porter-handshake":d=>{c&&m(d.payload.info)}})}}catch(u){c&&(console.error("[PORTER] initializePorter error ",u),a(u instanceof Error?u:new Error("Failed to connect to Porter")),t(!1),r(!1))}})(),()=>{c=!1,A.current.forEach(u=>u()),A.current=[]}},[S]);let q=O(c=>{if(i.current)try{i.current(c)}catch(f){a(f instanceof Error?f:new Error("Failed to send message"))}else a(new Error("Porter is not connected"))},[]),k=O(c=>{if(p.current)try{p.current(c)}catch(f){a(f instanceof Error?f:new Error("Failed to set message handlers"))}else a(new Error("Porter is not connected"))},[]);return{post:q,on:k,isConnected:e,isReconnecting:s,error:g,agentInfo:v}}export{H as usePorter};
//# sourceMappingURL=index.js.map