@gitorial/sync
Version:
Universal sync library for real-time tutorial state synchronization between websites and VS Code extensions with built-in relay server orchestration
2 lines (1 loc) • 37.7 kB
JavaScript
import{EventEmitter as z}from"events";var I=class{constructor(){this.emitter=new z}on(e,t){return this.emitter.on(e,t),this}off(e,t){return this.emitter.off(e,t),this}emit(e,...t){return this.emitter.emit(e,...t)}once(e,t){return this.emitter.once(e,t),this}removeAllListeners(e){return this.emitter.removeAllListeners(e),this}};var v=class{constructor(){this.events=new Map}on(e,t){return this.events.has(e)||this.events.set(e,new Set),this.events.get(e).add(t),this}off(e,t){let n=this.events.get(e);return n&&(n.delete(t),n.size===0&&this.events.delete(e)),this}emit(e,...t){let n=this.events.get(e);if(!n||n.size===0)return!1;for(let s of n)try{s(...t)}catch(i){console.error("EventEmitter error:",i)}return!0}once(e,t){let n=(...s)=>{this.off(e,n),t(...s)};return this.on(e,n),this}removeAllListeners(e){return e?this.events.delete(e):this.events.clear(),this}};function p(){return typeof process<"u"&&process.versions&&process.versions.node?new I:new v}var F=(r=>(r.DISCONNECTED="disconnected",r.CONNECTING="connecting",r.CONNECTED="connected",r.GIVEN_AWAY_CONTROL="given_away_control",r.TAKEN_BACK_CONTROL="taken_back_control",r.ERROR="error",r))(F||{});var g=(u=>(u.CONNECTION_FAILED="connection_failed",u.CONNECTION_LOST="connection_lost",u.INVALID_MESSAGE="invalid_message",u.SERVER_ERROR="server_error",u.TIMEOUT="timeout",u.MAX_RECONNECT_ATTEMPTS_EXCEEDED="max_reconnect_attempts_exceeded",u.PROTOCOL_VERSION="protocol_version",u.INVALID_STATE_TRANSITION="invalid_state_transition",u.INVALID_OPERATION="invalid_operation",u))(g||{}),a=class extends Error{constructor(t,n,s){super(n);this.type=t;this.originalError=s;this.name="SyncClientError"}};var W=(C=>(C.CONNECTION_STATUS_CHANGED="connectionStatusChanged",C.TUTORIAL_STATE_UPDATED="tutorialStateUpdated",C.ERROR="error",C.CLIENT_ID_ASSIGNED="clientIdAssigned",C.CLIENT_CONNECTED="clientConnected",C.CLIENT_DISCONNECTED="clientDisconnected",C.PEER_CONTROL_OFFERED="peerControlOffered",C.PEER_CONTROL_ACCEPTED="peerControlAccepted",C.PEER_CONTROL_DECLINED="peerControlDeclined",C.PEER_CONTROL_RETURNED="peerControlReturned",C))(W||{});var _=(l=>(l.STATE_UPDATE="state_update",l.REQUEST_SYNC="request_sync",l.CLIENT_CONNECTED="client_connected",l.CLIENT_DISCONNECTED="client_disconnected",l.OFFER_CONTROL="offer_control",l.ACCEPT_CONTROL="accept_control",l.DECLINE_CONTROL="decline_control",l.RETURN_CONTROL="return_control",l.ERROR="error",l.PROTOCOL_HANDSHAKE="protocol_handshake",l.PROTOCOL_ACK="protocol_ack",l.REQUEST_CONTROL="request_control",l.RELEASE_CONTROL="release_control",l.CONFIRM_TRANSFER="confirm_transfer",l.ROLE_CHANGED="role_changed",l.COORDINATE_SYNC_DIRECTION="coordinate_sync_direction",l.ASSIGN_SYNC_DIRECTION="assign_sync_direction",l))(_||{});var L=(s=>(s.UNINITIALIZED="uninitialized",s.CONNECTED="connected",s.PASSIVE="passive",s.ACTIVE="active",s))(L||{}),Z=(s=>(s.IDLE="idle",s.REQUESTING="requesting",s.TRANSFERRING="transferring",s.SYNCHRONIZED="synchronized",s))(Z||{}),b=(n=>(n.FIRST_COME_FIRST_SERVED="first_come_first_served",n.DENY_BOTH="deny_both",n.USER_CHOICE="user_choice",n))(b||{}),M=class{static canSendTutorialState(e){return e==="passive"}static canRequestTutorialState(e){return e==="active"}static canChooseRole(e){return e==="connected"}static canOfferControl(e){return e==="active"||e==="passive"}static canReleaseControl(e){return e==="active"||e==="passive"}static canRequestControl(e){return e==="connected"||e==="active"||e==="passive"}static canAcceptControlOffer(e){return e==="active"||e==="passive"}static canDisconnect(e){return e!=="uninitialized"}static isConnected(e){return e==="connected"||e==="passive"||e==="active"}static getValidTransitions(e){switch(e){case"uninitialized":return["connected"];case"connected":return["passive","active","uninitialized"];case"passive":return["active","connected","uninitialized"];case"active":return["passive","connected","uninitialized"];default:return[]}}},G=class{constructor(){this.currentRole="uninitialized"}getCurrentRole(){return this.currentRole}canTransitionTo(e){return M.getValidTransitions(this.currentRole).includes(e)}transitionTo(e){return this.canTransitionTo(e)?(this.currentRole=e,!0):!1}reset(){this.currentRole="uninitialized"}};var $=(h=>(h.CONNECTING="connecting",h.CONNECTED_IDLE="connected_idle",h.INITIALIZING_PULL="initializing_pull",h.INITIALIZING_PUSH="initializing_push",h.ACTIVE="active",h.PASSIVE="passive",h.DISCONNECTED="disconnected",h))($||{}),S=class{static canSendTutorialState(e){return e==="active"}static canRequestSync(e){return e==="initializing_pull"||e==="active"}static canChooseSyncDirection(e){return e==="connected_idle"}static canOfferControlTransfer(e){return e==="active"}static canDisconnect(e){return e!=="disconnected"}static canConnect(e){return e==="disconnected"}static getValidTransitions(e){switch(e){case"disconnected":return["connecting"];case"connecting":return["connected_idle","disconnected"];case"connected_idle":return["initializing_pull","initializing_push","active","passive","disconnected"];case"initializing_pull":return["active","disconnected"];case"initializing_push":return["passive","disconnected"];case"active":return["passive","disconnected"];case"passive":return["active","disconnected"];default:return[]}}},y=class{constructor(){this.currentPhase="disconnected"}getCurrentPhase(){return this.currentPhase}canTransitionTo(e){return S.getValidTransitions(this.currentPhase).includes(e)}transitionTo(e){return this.canTransitionTo(e)?(this.currentPhase=e,!0):!1}reset(){this.currentPhase="disconnected"}};import Y from"ws";var T=class{async connect(e){return this.socket=new Y(e),this.messageHandler&&this.socket.on("message",t=>this.messageHandler(JSON.parse(t.toString()))),this.errorHandler&&this.socket.on("error",this.errorHandler),this.closeHandler&&this.socket.on("close",this.closeHandler),this.openHandler&&this.socket.on("open",this.openHandler),new Promise((t,n)=>{this.socket.on("open",()=>t(this)),this.socket.on("error",s=>n(s))})}send(e){this.socket.send(JSON.stringify(e))}close(){this.socket.close()}onMessage(e){this.messageHandler=e,this.socket&&this.socket.on("message",t=>e(JSON.parse(t.toString())))}onError(e){this.errorHandler=e,this.socket&&this.socket.on("error",e)}onClose(e){this.closeHandler=e,this.socket&&this.socket.on("close",e)}onOpen(e){this.openHandler=e,this.socket&&this.socket.on("open",e)}};var R=class{async connect(e){return this.socket=new globalThis.WebSocket(e),this.messageHandler&&(this.socket.onmessage=t=>this.messageHandler(JSON.parse(t.data))),this.errorHandler&&(this.socket.onerror=this.errorHandler),this.closeHandler&&(this.socket.onclose=this.closeHandler),this.openHandler&&(this.socket.onopen=this.openHandler),new Promise((t,n)=>{this.socket.onopen=()=>{this.openHandler&&this.openHandler(),t(this)},this.socket.onerror=s=>{this.errorHandler&&this.errorHandler(s),n(s)}})}send(e){this.socket.send(JSON.stringify(e))}close(){this.socket.close()}onMessage(e){this.messageHandler=e,this.socket&&(this.socket.onmessage=t=>e(JSON.parse(t.data)))}onError(e){this.errorHandler=e,this.socket&&(this.socket.onerror=e)}onClose(e){this.closeHandler=e,this.socket&&(this.socket.onclose=e)}onOpen(e){this.openHandler=e,this.socket&&(this.socket.onopen=e)}};function k(){return typeof process<"u"&&process.versions&&process.versions.node?new T:new R}var N=class{constructor(e){this.socket=null;this.connectionStatus="disconnected";this.currentSessionId=null;this.reconnectAttempts=0;this.reconnectTimer=null;this.isConnecting=!1;this.connectionTimeout=null;this.config=e,this.eventEmitter=p()}on(e,t){return this.eventEmitter.on(e,t),this}off(e,t){return this.eventEmitter.off(e,t),this}emit(e,...t){return this.eventEmitter.emit(e,...t)}once(e,t){return this.eventEmitter.once(e,t),this}removeAllListeners(e){return this.eventEmitter.removeAllListeners(e),this}async connect(e){this.currentSessionId=e;let t=`${this.config.wsUrl}?session=${e}`;if(this.isConnecting)throw new a("connection_failed","Connection already in progress");this.isConnecting=!0,this.setStatus("connecting");try{this.socket=k(),this.socket.onOpen(()=>{this.clearConnectionTimeout(),this.isConnecting=!1,this.reconnectAttempts=0,this.setStatus("connected"),this.emit("connected")}),this.socket.onMessage(n=>{try{let s=n;if(!s.type)throw new Error("Message missing type field");this.emit("message",s)}catch(s){console.warn("Invalid message received:",n,s),this.handleError(new a("invalid_message",`Invalid message received: ${s}`))}}),this.socket.onClose(()=>{this.clearConnectionTimeout(),this.isConnecting=!1,this.setStatus("disconnected"),this.config.autoReconnect&&this.reconnectAttempts<this.config.maxReconnectAttempts?this.scheduleReconnect():this.emit("disconnected")}),this.socket.onError(n=>{this.clearConnectionTimeout(),this.isConnecting=!1;let s=new a("connection_failed","WebSocket connection failed");this.handleError(s)}),this.connectionTimeout=setTimeout(()=>{this.cleanup();let n=new a("timeout","Connection timeout");throw this.handleError(n),n},this.config.connectionTimeout),await this.socket.connect(t)}catch(n){this.isConnecting=!1,this.cleanup();let s=new a("connection_failed",`Failed to create WebSocket connection: ${n}`);throw this.handleError(s),s}}disconnect(){this.clearReconnectTimer(),this.clearConnectionTimeout(),this.cleanup(),this.setStatus("disconnected"),this.emit("disconnected")}sendMessage(e){if(!this.socket||!this.isConnected())throw new a("connection_failed","Not connected to relay server");try{this.socket.send(e)}catch(t){this.handleError(new a("invalid_message",`Failed to send message: ${t}`))}}isConnected(){return this.socket!==null&&this.connectionStatus==="connected"}getStatus(){return this.connectionStatus}getCurrentSessionId(){return this.currentSessionId}setStatus(e){this.connectionStatus!==e&&(this.connectionStatus=e,this.emit("statusChanged",e))}handleError(e){this.emit("error",e)}scheduleReconnect(){this.reconnectTimer&&clearTimeout(this.reconnectTimer),this.reconnectAttempts++,this.setStatus("connecting"),this.reconnectTimer=setTimeout(async()=>{try{this.currentSessionId&&await this.connect(this.currentSessionId)}catch{this.handleError(new a("connection_failed","Reconnection failed")),this.reconnectAttempts<this.config.maxReconnectAttempts?this.scheduleReconnect():(this.setStatus("disconnected"),this.emit("disconnected"))}},this.config.reconnectDelay)}clearReconnectTimer(){this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null)}clearConnectionTimeout(){this.connectionTimeout&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=null)}cleanup(){if(this.clearConnectionTimeout(),this.socket){try{this.socket.close()}catch{}this.socket=null}}};var D=class{constructor(e){this.config=e}async createSession(e){let t=await fetch(`${this.config.baseUrl}${this.config.sessionEndpoint}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({metadata:e})});if(!t.ok)throw new a("connection_failed","Failed to create session");return await t.json()}async getSessionInfo(e){if(!e)return null;let t=await fetch(`${this.config.baseUrl}${this.config.sessionEndpoint}/${e}`,{method:"GET",headers:{"Content-Type":"application/json"}});if(!t.ok){if(t.status===404)return null;throw new a("connection_failed","Failed to get session info")}return await t.json()}async deleteSession(e){return e?(await fetch(`${this.config.baseUrl}${this.config.sessionEndpoint}/${e}`,{method:"DELETE",headers:{"Content-Type":"application/json"}})).ok:!1}async listSessions(){let e=await fetch(`${this.config.baseUrl}${this.config.sessionEndpoint}`,{method:"GET",headers:{"Content-Type":"application/json"}});if(!e.ok)throw new a("connection_failed","Failed to list sessions");return await e.json()}};var c=1;var O=class{constructor(e,t){this.connectionManager=e;this.lastSynchronizedState=null;this.eventEmitter=p(),this.clientId=t,this.setupMessageHandling()}on(e,t){return this.eventEmitter.on(e,t),this}off(e,t){return this.eventEmitter.off(e,t),this}emit(e,...t){return this.eventEmitter.emit(e,...t)}once(e,t){return this.eventEmitter.once(e,t),this}removeAllListeners(e){return this.eventEmitter.removeAllListeners(e),this}broadcastTutorialState(e){this.lastSynchronizedState=e,this.sendMessage({type:"state_update",clientId:this.clientId,data:e,timestamp:Date.now(),protocol_version:1})}requestStateSync(){this.sendMessage({type:"request_sync",clientId:this.clientId,data:{},timestamp:Date.now(),protocol_version:1})}sendControlRequest(e){this.sendMessage({type:"request_control",clientId:this.clientId,data:e,timestamp:Date.now(),protocol_version:1})}offerRoleSwitch(){let e={tutorialState:this.lastSynchronizedState,metadata:{transferTimestamp:Date.now(),fromClientId:this.clientId,toClientId:"other",stateChecksum:this.generateStateChecksum(this.lastSynchronizedState)}};this.sendMessage({type:"offer_control",clientId:this.clientId,data:e,timestamp:Date.now(),protocol_version:1})}announceRoleChange(e){this.sendMessage({type:"role_changed",clientId:this.clientId,data:{role:e,timestamp:Date.now()},timestamp:Date.now(),protocol_version:1})}releaseControl(){this.sendMessage({type:"release_control",clientId:this.clientId,data:{timestamp:Date.now()},timestamp:Date.now(),protocol_version:1})}requestSyncDirectionCoordination(e,t){let n={preferredDirection:e,reason:t};this.sendMessage({type:"coordinate_sync_direction",clientId:this.clientId,data:n,timestamp:Date.now(),protocol_version:1})}setupMessageHandling(){this.connectionManager.on("message",e=>{this.handleIncomingMessage(e)})}handleIncomingMessage(e){switch(e.type){case"state_update":this.handleStateUpdate(e);break;case"request_sync":this.handleSyncRequest(e);break;case"request_control":this.handleControlRequest(e);break;case"offer_control":this.handleControlOffer(e);break;case"accept_control":this.handleControlAccept(e);break;case"decline_control":this.handleControlDecline(e);break;case"release_control":this.handleControlRelease(e);break;case"confirm_transfer":this.handleTransferConfirm(e);break;case"role_changed":this.handleRoleChanged(e);break;case"client_connected":this.handleClientConnected(e);break;case"client_disconnected":this.handleClientDisconnected(e);break;case"error":this.handleServerError(e);break;case"assign_sync_direction":this.handleSyncDirectionAssignment(e);break;default:console.warn("Unknown message type received:",e.type)}}handleStateUpdate(e){"data"in e&&(this.lastSynchronizedState=e.data,this.emit("tutorialStateReceived",e.data))}handleSyncRequest(e){this.lastSynchronizedState&&this.broadcastTutorialState(this.lastSynchronizedState)}handleControlRequest(e){if("clientId"in e&&"data"in e){let t=e.data;this.emit("controlRequested",{fromClientId:e.clientId,request:t,accept:()=>this.acceptControlTransfer(e.clientId),decline:()=>this.declineControlTransfer(e.clientId)})}}handleControlOffer(e){if("clientId"in e&&"data"in e){let t=e.data;this.emit("controlOffered",{fromClientId:e.clientId,state:t.tutorialState,accept:()=>this.acceptControlTransfer(e.clientId,t),decline:()=>this.declineControlTransfer(e.clientId)})}}handleControlAccept(e){if("clientId"in e&&"data"in e){if(e.clientId==="relay-server"&&e.data?.granted){this.emit("controlTransferConfirmed");return}this.emit("controlAccepted",e.clientId),this.sendMessage({type:"confirm_transfer",clientId:this.clientId,data:{toClientId:e.clientId,timestamp:Date.now()},timestamp:Date.now(),protocol_version:1})}}handleControlDecline(e){}handleControlRelease(e){}handleTransferConfirm(e){this.emit("controlTransferConfirmed")}handleRoleChanged(e){if("data"in e){let t=e.data}}handleClientConnected(e){"clientId"in e&&this.emit("clientConnected",e.clientId)}handleClientDisconnected(e){"clientId"in e&&this.emit("clientDisconnected",e.clientId)}handleServerError(e){if("data"in e)throw new a("server_error",e.data?.message||"Relay server error")}handleSyncDirectionAssignment(e){if("data"in e){let t=e.data;this.emit("syncDirectionAssigned",t)}}acceptControlTransfer(e,t){t&&(this.lastSynchronizedState=t.tutorialState),this.sendMessage({type:"accept_control",clientId:this.clientId,data:{fromClientId:e,timestamp:Date.now()},timestamp:Date.now(),protocol_version:1})}declineControlTransfer(e){this.sendMessage({type:"decline_control",clientId:this.clientId,data:{fromClientId:e,timestamp:Date.now()},timestamp:Date.now(),protocol_version:1})}sendMessage(e){this.connectionManager.sendMessage(e)}generateStateChecksum(e){return`checksum_${Date.now()}_${JSON.stringify(e).length}`}getCurrentState(){return this.lastSynchronizedState}updateCurrentState(e){this.lastSynchronizedState=e}};var x=class{constructor(e){this.eventEmitter=p(),this.config={connectionTimeout:e.connectionTimeout??5e3,autoReconnect:e.autoReconnect??!0,maxReconnectAttempts:e.maxReconnectAttempts??5,reconnectDelay:e.reconnectDelay??2e3,sessionEndpoint:e.sessionEndpoint,baseUrl:e.baseUrl,wsUrl:e.wsUrl},this.clientId=`client_${Math.random().toString(36).substring(2,15)}`,this.syncPhaseStateMachine=new y,this.connectionManager=new N({wsUrl:this.config.wsUrl,connectionTimeout:this.config.connectionTimeout,autoReconnect:this.config.autoReconnect,maxReconnectAttempts:this.config.maxReconnectAttempts,reconnectDelay:this.config.reconnectDelay}),this.sessionManager=new D({baseUrl:this.config.baseUrl,sessionEndpoint:this.config.sessionEndpoint}),this.messageDispatcher=new O(this.connectionManager,this.clientId),this.setupEventHandlers()}on(e,t){return this.eventEmitter.on(e,t),this}off(e,t){return this.eventEmitter.off(e,t),this}emit(e,...t){return this.eventEmitter.emit(e,...t)}once(e,t){return this.eventEmitter.once(e,t),this}removeAllListeners(e){return this.eventEmitter.removeAllListeners(e),this}async createSessionAndConnect(e){this.enforceValidTransition("connecting","Cannot create session while not disconnected");let t=await this.sessionManager.createSession(e);return await this.connectToSession(t.id),t}async connectToSession(e){this.enforceValidTransition("connecting","Cannot connect while not disconnected"),this.transitionToPhase("connecting","Establishing connection");try{await this.connectionManager.connect(e),this.transitionToPhase("connected_idle","Connection established")}catch(t){throw this.transitionToPhase("disconnected","Connection failed"),t}}async getSessionInfo(){let e=this.connectionManager.getCurrentSessionId();return e?this.sessionManager.getSessionInfo(e):null}disconnect(){this.syncPhaseStateMachine.getCurrentPhase()!=="disconnected"&&this.connectionManager.disconnect()}getCurrentSyncPhase(){return this.syncPhaseStateMachine.getCurrentPhase()}isConnectedIdle(){return this.getCurrentSyncPhase()==="connected_idle"}isActive(){return this.getCurrentSyncPhase()==="active"}isPassive(){return this.getCurrentSyncPhase()==="passive"}isConnecting(){let e=this.getCurrentSyncPhase();return e==="connecting"||e==="initializing_pull"||e==="initializing_push"}async pullStateFromPeer(){this.enforcePermission(S.canChooseSyncDirection(this.getCurrentSyncPhase()),"Can only choose sync direction when connected idle"),this.transitionToPhase("initializing_pull","Initializing pull from peer");try{this.messageDispatcher.requestSyncDirectionCoordination("ACTIVE","Client wants to pull state from peer")}catch(e){throw this.transitionToPhase("connected_idle","Pull initialization failed"),e}}async pushStateToPeer(e){this.enforcePermission(S.canChooseSyncDirection(this.getCurrentSyncPhase()),"Can only choose sync direction when connected idle"),this.transitionToPhase("initializing_push","Initializing push to peer");try{this.messageDispatcher.requestSyncDirectionCoordination("PASSIVE","Client wants to push state to peer"),e&&this.messageDispatcher.updateCurrentState(e)}catch(t){throw this.transitionToPhase("connected_idle","Push initialization failed"),t}}sendTutorialState(e){this.enforcePermission(S.canSendTutorialState(this.getCurrentSyncPhase()),"Only active clients can send tutorial state"),this.messageDispatcher.broadcastTutorialState(e)}requestTutorialState(){this.enforcePermission(S.canRequestSync(this.getCurrentSyncPhase()),"Only active or initializing pull clients can request state"),this.messageDispatcher.requestStateSync()}getLastTutorialState(){return this.messageDispatcher.getCurrentState()}offerControlToPeer(){this.enforcePermission(S.canOfferControlTransfer(this.getCurrentSyncPhase()),"Only active clients can offer control transfer"),this.messageDispatcher.offerRoleSwitch()}acceptControlTransfer(){this.isPassive()&&this.transitionToPhase("active","Accepted control transfer from peer")}releaseControl(){this.isActive()&&this.transitionToPhase("passive","Released control to peer")}isConnected(){return this.connectionManager.isConnected()}getConnectionStatus(){return this.connectionManager.getStatus()}getCurrentSessionId(){return this.connectionManager.getCurrentSessionId()}getClientId(){return this.clientId}async listAvailableSessions(){return this.sessionManager.listSessions()}async deleteCurrentSession(){let e=this.connectionManager.getCurrentSessionId();return e?(this.disconnect(),this.sessionManager.deleteSession(e)):!1}transitionToPhase(e,t){let n=this.syncPhaseStateMachine.getCurrentPhase();if(this.syncPhaseStateMachine.transitionTo(e)){let s={clientId:this.clientId,previousPhase:n,newPhase:e,timestamp:Date.now(),reason:t};this.emit("syncPhaseChanged",s)}else throw new a("invalid_state_transition",`Invalid sync phase transition from ${n} to ${e}`)}enforceValidTransition(e,t){if(!this.syncPhaseStateMachine.canTransitionTo(e))throw new a("invalid_state_transition",t)}enforcePermission(e,t){if(!e)throw new a("invalid_operation",t)}setupEventHandlers(){this.connectionManager.on("connected",()=>{this.emit("connected")}),this.connectionManager.on("disconnected",()=>{this.syncPhaseStateMachine.getCurrentPhase()!=="disconnected"&&this.transitionToPhase("disconnected","Connection lost"),this.emit("disconnected")}),this.connectionManager.on("statusChanged",e=>{this.emit("connectionStatusChanged",e)}),this.connectionManager.on("error",e=>{this.emit("error",e)}),this.messageDispatcher.on("tutorialStateReceived",e=>{this.emit("tutorialStateUpdated",e)}),this.messageDispatcher.on("controlRequested",e=>{this.emit("controlRequested",e)}),this.messageDispatcher.on("controlOffered",e=>{let t={...e,accept:()=>{e.accept(),this.acceptControlTransfer()}};this.emit("controlOffered",t)}),this.messageDispatcher.on("controlAccepted",e=>{this.isActive()&&this.transitionToPhase("passive",`Control transferred to ${e}`)}),this.messageDispatcher.on("controlTransferConfirmed",()=>{this.isPassive()&&this.transitionToPhase("active","Control transfer confirmed")}),this.messageDispatcher.on("syncDirectionAssigned",e=>{let t=this.getCurrentSyncPhase();e.assignedDirection==="ACTIVE"?t==="initializing_pull"?this.transitionToPhase("active",e.reason):t==="connected_idle"&&this.transitionToPhase("active",e.reason):e.assignedDirection==="PASSIVE"&&(t==="initializing_push"?this.transitionToPhase("passive",e.reason):t==="connected_idle"&&this.transitionToPhase("passive",e.reason))}),this.messageDispatcher.on("clientConnected",e=>{this.emit("clientConnected",e)}),this.messageDispatcher.on("clientDisconnected",e=>{this.emit("clientDisconnected",e)})}};import{EventEmitter as te}from"events";import{WebSocket as H}from"ws";var d=[];for(let o=0;o<256;++o)d.push((o+256).toString(16).slice(1));function K(o,e=0){return(d[o[e+0]]+d[o[e+1]]+d[o[e+2]]+d[o[e+3]]+"-"+d[o[e+4]]+d[o[e+5]]+"-"+d[o[e+6]]+d[o[e+7]]+"-"+d[o[e+8]]+d[o[e+9]]+"-"+d[o[e+10]]+d[o[e+11]]+d[o[e+12]]+d[o[e+13]]+d[o[e+14]]+d[o[e+15]]).toLowerCase()}import{randomFillSync as J}from"crypto";var P=new Uint8Array(256),A=P.length;function w(){return A>P.length-16&&(J(P),A=0),P.slice(A,A+=16)}import{randomUUID as Q}from"crypto";var V={randomUUID:Q};function j(o,e,t){if(V.randomUUID&&!e&&!o)return V.randomUUID();o=o||{};let n=o.random??o.rng?.()??w();if(n.length<16)throw new Error("Random bytes length must be >= 16");if(n[6]=n[6]&15|64,n[8]=n[8]&63|128,e){if(t=t||0,t<0||t+16>e.length)throw new RangeError(`UUID byte range ${t}:${t+15} is out of buffer bounds`);for(let s=0;s<16;++s)e[t+s]=n[s];return e}return K(n)}var U=j;import{EventEmitter as B}from"events";var E=class extends B{constructor(t=30*60*1e3,n="first_come_first_served"){super();this.sessions=new Map;this.defaultSessionTimeoutMs=t,this.defaultConflictResolution=n}create(t={}){let n=t.sessionId||this.generateSessionId(),s=new Date,i=new Date(s.getTime()+(t.expiresIn||this.defaultSessionTimeoutMs)),r={id:n,createdAt:s,expiresAt:i,lastActivity:s,metadata:t.metadata,status:"active",activeClientId:null,roleTransferInProgress:!1,conflictResolution:t.conflictResolution||this.defaultConflictResolution,clientConnections:new Set};return this.sessions.set(n,r),this.toSessionData(r)}get(t){let n=this.sessions.get(t);return!n||n.status!=="active"?null:this.toSessionData(n)}getInternal(t){return this.sessions.get(t)||null}updateActivity(t){let n=this.sessions.get(t);return!n||n.status!=="active"?!1:(n.lastActivity=new Date,!0)}updateMetadata(t,n){let s=this.sessions.get(t);return!s||s.status!=="active"?!1:(s.metadata=n,!0)}delete(t){let n=this.sessions.get(t);return n?(n.status="deleted",this.sessions.delete(t),this.emit("sessionDeleted",t),!0):!1}list(){return Array.from(this.sessions.values()).filter(t=>t.status==="active").map(t=>this.toSessionData(t))}getExpiredSessions(){let t=new Date,n=[];for(let[s,i]of this.sessions.entries())i.status==="active"&&t>i.expiresAt&&n.push(s);return n}markExpired(t){let n=this.sessions.get(t);return!n||n.status!=="active"?!1:(n.status="expired",this.emit("sessionExpired",t),!0)}getActiveCount(){return Array.from(this.sessions.values()).filter(t=>t.status==="active").length}clear(){this.sessions.clear()}toSessionData(t){return{id:t.id,createdAt:t.createdAt,expiresAt:t.expiresAt,clientCount:t.clientConnections.size,lastActivity:t.lastActivity,metadata:t.metadata,activeClientId:t.activeClientId,status:t.status}}generateSessionId(){return Math.random().toString(36).substring(2,15)+Math.random().toString(36).substring(2,15)}on(t,n){return super.on(t,n)}emit(t,...n){return super.emit(t,...n)}};import{EventEmitter as X}from"events";var m=class extends X{constructor(t,n=60*1e3){super();this.cleanupTimer=null;this.isRunning=!1;this.sessionStore=t,this.cleanupIntervalMs=n,this.sessionStore.on("sessionExpired",s=>{this.emit("sessionExpired",s)}),this.sessionStore.on("sessionDeleted",s=>{this.emit("sessionDeleted",s)})}start(){this.isRunning||(this.startCleanupTimer(),this.isRunning=!0,console.log("\u{1F504} SessionLifecycleManager started"))}stop(){this.isRunning&&(this.stopCleanupTimer(),this.isRunning=!1,console.log("\u23F9\uFE0F SessionLifecycleManager stopped"))}isActive(){return this.isRunning}cleanupExpiredSessions(){let t=this.sessionStore.getExpiredSessions(),n=0;for(let s of t)this.sessionStore.markExpired(s)&&(n++,console.log(`\u23F0 Session expired: ${s}`));return n}startCleanupTimer(){this.cleanupTimer=setInterval(()=>{this.cleanupExpiredSessions()},this.cleanupIntervalMs)}stopCleanupTimer(){this.cleanupTimer&&(clearInterval(this.cleanupTimer),this.cleanupTimer=null)}on(t,n){return super.on(t,n)}emit(t,...n){return super.emit(t,...n)}};import{EventEmitter as ee}from"events";var f=class extends ee{constructor(){super();this.connections=new Map;this.sessionConnections=new Map}addConnection(t,n){return this.connections.has(n.id)?!1:(this.connections.set(n.id,n),this.sessionConnections.has(t)||this.sessionConnections.set(t,new Set),this.sessionConnections.get(t).add(n.id),this.emit("clientConnected",t,n.id),!0)}removeConnection(t){let n=this.connections.get(t);if(!n)return!1;let s=n.sessionId;this.connections.delete(t);let i=this.sessionConnections.get(s);return i&&(i.delete(t),i.size===0&&this.sessionConnections.delete(s)),n.socket.readyState===n.socket.OPEN&&n.socket.close(),this.emit("clientDisconnected",s,t),!0}getConnection(t){return this.connections.get(t)||null}getSessionConnections(t){let n=this.sessionConnections.get(t);if(!n)return[];let s=[];for(let i of n){let r=this.connections.get(i);r&&s.push(r)}return s}updateConnectionActivity(t){let n=this.connections.get(t);return n?(n.lastPing=new Date,!0):!1}setConnectionRole(t,n){let s=this.connections.get(t);if(!s)return!1;let i=s.role;return s.role=n,s.lastRoleChange=new Date,console.log(`\u{1F504} Connection ${t} role changed: ${i} \u2192 ${n}`),!0}findActiveConnection(t){return this.getSessionConnections(t).find(s=>s.role==="active")||null}findConnectionByClientId(t,n){return this.getSessionConnections(t).find(i=>i.clientId===n)||null}getConnectionCount(t){let n=this.sessionConnections.get(t);return n?n.size:0}closeAllConnections(t){let n=this.getSessionConnections(t);for(let i of n)i.socket.readyState===i.socket.OPEN&&i.socket.close();let s=this.sessionConnections.get(t);if(s){for(let i of s)this.connections.delete(i);this.sessionConnections.delete(t)}}getAllConnections(){return Array.from(this.connections.values())}getStats(){return{totalConnections:this.connections.size,activeSessions:this.sessionConnections.size,connectionsPerSession:Array.from(this.sessionConnections.entries()).map(([t,n])=>({sessionId:t,connectionCount:n.size}))}}clear(){for(let t of this.connections.values())t.socket.readyState===t.socket.OPEN&&t.socket.close();this.connections.clear(),this.sessionConnections.clear()}on(t,n){return super.on(t,n)}emit(t,...n){return super.emit(t,...n)}};var q=class extends te{constructor(t={}){super();this.pingTimer=null;this.isRunning=!1;this.config={sessionTimeoutMs:t.sessionTimeoutMs??30*60*1e3,pingIntervalMs:t.pingIntervalMs??30*1e3,cleanupIntervalMs:t.cleanupIntervalMs??60*1e3,enableRoleManagement:t.enableRoleManagement??!0,defaultConflictResolution:t.defaultConflictResolution??"first_come_first_served"},this.sessionStore=new E(this.config.sessionTimeoutMs,this.config.defaultConflictResolution),this.lifecycleManager=new m(this.sessionStore,this.config.cleanupIntervalMs),this.connectionManager=new f,this.setupEventForwarding()}start(){this.isRunning||(this.lifecycleManager.start(),this.startPingTimer(),this.isRunning=!0,console.log("\u{1F680} RelaySessionOrchestrator started with role management"))}stop(){this.isRunning&&(this.lifecycleManager.stop(),this.stopPingTimer(),this.connectionManager.clear(),this.sessionStore.clear(),this.isRunning=!1,console.log("\u{1F6D1} RelaySessionOrchestrator stopped"))}createSession(t={}){let n=this.sessionStore.create(t);return this.emit("sessionCreated",n.id),n}getSession(t){return this.sessionStore.get(t)}listSessions(){return this.sessionStore.list()}deleteSession(t){return this.connectionManager.closeAllConnections(t),this.sessionStore.delete(t)}handleUpgrade(t,n,s){if(!this.sessionStore.getInternal(t))return console.log(`\u274C Session not found: ${t}`),n.close(1008,"Session not found"),!1;let r={id:this.generateConnectionId(),sessionId:t,socket:n,connectedAt:new Date,lastPing:new Date,role:"connected",lastRoleChange:new Date};return this.connectionManager.addConnection(t,r)?(this.sessionStore.updateActivity(t),this.setupWebSocketHandlers(r),console.log(`\u2705 Client connected to session ${t}: ${r.id}`),!0):(console.log(`\u274C Failed to add connection: ${r.id}`),n.close(1011,"Failed to add connection"),!1)}getStats(){return{sessions:{active:this.sessionStore.getActiveCount(),total:this.sessionStore.list().length},connections:this.connectionManager.getStats(),lifecycle:{isRunning:this.isRunning,lifecycleManagerActive:this.lifecycleManager.isActive()}}}setupEventForwarding(){this.lifecycleManager.on("sessionExpired",t=>{this.connectionManager.closeAllConnections(t),this.emit("sessionExpired",t)}),this.lifecycleManager.on("sessionDeleted",t=>{this.emit("sessionDeleted",t)}),this.connectionManager.on("clientConnected",(t,n)=>{this.emit("clientConnected",t,n)}),this.connectionManager.on("clientDisconnected",(t,n)=>{this.emit("clientDisconnected",t,n)})}setupWebSocketHandlers(t){t.socket.on("message",n=>{this.handleWebSocketMessage(t,n)}),t.socket.on("close",()=>{this.handleWebSocketClose(t)}),t.socket.on("error",n=>{this.handleWebSocketError(t,n)}),t.socket.on("pong",()=>{this.connectionManager.updateConnectionActivity(t.id)})}handleWebSocketMessage(t,n){try{let s=JSON.parse(n.toString());switch(this.connectionManager.updateConnectionActivity(t.id),this.sessionStore.updateActivity(t.sessionId),s.type){case"role_changed":this.handleRoleMessage(t,s);break;case"request_control":this.handleControlRequest(t,s);break;case"offer_control":this.handleControlOffer(t,s);break;case"accept_control":this.handleControlAccept(t,s);break;case"decline_control":this.handleControlDecline(t,s);break;case"release_control":this.handleControlRelease(t,s);break;case"coordinate_sync_direction":this.handleSyncDirectionCoordination(t,s);break;default:this.routeMessage(t,s);break}}catch(s){console.error(`\u274C Error handling message from ${t.id}:`,s)}}handleRoleMessage(t,n){if(n.type==="role_changed"&&"data"in n&&n.data?.role){let s=n.data.role;this.connectionManager.setConnectionRole(t.id,s),this.emit("roleChanged",t.sessionId,t.id,s)}this.routeMessage(t,n)}handleControlRequest(t,n){let s=this.sessionStore.getInternal(t.sessionId);if(!s)return;let i=this.connectionManager.findActiveConnection(t.sessionId);i?i.id===t.id?this.sendToConnection(t,{type:"confirm_transfer",clientId:t.clientId||t.id,data:{reason:"Already active"},timestamp:Date.now(),protocol_version:1}):this.resolveControlConflict(t,i,s)?(this.connectionManager.setConnectionRole(i.id,"passive"),this.connectionManager.setConnectionRole(t.id,"active"),s.activeClientId=t.clientId||t.id,this.sendToConnection(t,{type:"confirm_transfer",clientId:t.clientId||t.id,data:{reason:"Control transferred"},timestamp:Date.now(),protocol_version:1}),this.sendToConnection(i,{type:"error",clientId:i.clientId||i.id,data:{reason:"Control transferred to another client"},timestamp:Date.now(),protocol_version:1}),this.emit("controlTransferred",t.sessionId,i.id,t.id)):this.sendToConnection(t,{type:"error",clientId:t.clientId||t.id,data:{reason:"Another client is active"},timestamp:Date.now(),protocol_version:1}):(this.connectionManager.setConnectionRole(t.id,"active"),s.activeClientId=t.clientId||t.id,this.sendToConnection(t,{type:"confirm_transfer",clientId:t.clientId||t.id,data:{reason:"No active client"},timestamp:Date.now(),protocol_version:1}),this.emit("controlTransferred",t.sessionId,"none",t.id))}handleControlOffer(t,n){this.routeMessage(t,n)}handleControlAccept(t,n){let s=this.sessionStore.getInternal(t.sessionId);if(!s)return;let i=this.connectionManager.findActiveConnection(t.sessionId);i&&this.connectionManager.setConnectionRole(i.id,"passive"),this.connectionManager.setConnectionRole(t.id,"active"),s.activeClientId=t.clientId||t.id,this.emit("controlTransferred",t.sessionId,i?.id||"none",t.id),this.routeMessage(t,n)}handleControlDecline(t,n){this.routeMessage(t,n)}handleControlRelease(t,n){let s=this.sessionStore.getInternal(t.sessionId);s&&(t.role==="active"&&(this.connectionManager.setConnectionRole(t.id,"connected"),s.activeClientId=null,this.emit("controlTransferred",t.sessionId,t.id,"none")),this.routeMessage(t,n))}handleSyncDirectionCoordination(t,n){let s=this.sessionStore.getInternal(t.sessionId);if(!s||!("data"in n))return;let i=n.data;if(this.connectionManager.getSessionConnections(t.sessionId).length===1)this.assignSyncDirection(t,i.preferredDirection,"Single client session");else{this.assignSyncDirection(t,i.preferredDirection,"Sync direction coordination");let h=i.preferredDirection==="ACTIVE"?"PASSIVE":"ACTIVE";this.assignOtherClientsDirection(s,t,h,"Complementary sync direction")}}assignSyncDirection(t,n,s){let i=n==="ACTIVE"?"active":"passive";this.connectionManager.setConnectionRole(t.id,i);let r={assignedDirection:n,reason:s};this.sendToConnection(t,{type:"assign_sync_direction",clientId:t.clientId||t.id,data:r,timestamp:Date.now(),protocol_version:1})}assignOtherClientsDirection(t,n,s,i){let r=this.connectionManager.getSessionConnections(t.id);for(let h of r)h.id!==n.id&&this.assignSyncDirection(h,s,i)}resolveControlConflict(t,n,s){switch(s.conflictResolution){case"first_come_first_served":return!1;case"deny_both":return!1;case"user_choice":return!1;default:return!1}}routeMessage(t,n){let s=this.connectionManager.getSessionConnections(t.sessionId);for(let i of s)i.id!==t.id&&i.socket.readyState===H.OPEN&&this.sendToConnection(i,n)}handleWebSocketClose(t){console.log(`\u{1F50C} Client disconnected: ${t.id}`);let n=this.sessionStore.getInternal(t.sessionId);n&&t.role==="active"&&(n.activeClientId=null),this.connectionManager.removeConnection(t.id)}handleWebSocketError(t,n){console.error(`\u274C WebSocket error for ${t.id}:`,n)}startPingTimer(){this.pingTimer=setInterval(()=>{this.pingClients()},this.config.pingIntervalMs)}stopPingTimer(){this.pingTimer&&(clearInterval(this.pingTimer),this.pingTimer=null)}pingClients(){let t=this.connectionManager.getAllConnections();for(let n of t)n.socket.readyState===H.OPEN&&n.socket.ping()}sendToConnection(t,n){t.socket.readyState===H.OPEN&&t.socket.send(JSON.stringify(n))}generateConnectionId(){return U()}on(t,n){return super.on(t,n)}emit(t,...n){return super.emit(t,...n)}};export{L as ClientRole,b as ConflictResolution,f as ConnectionManager,F as ConnectionStatus,x as RelayClient,q as RelaySessionOrchestrator,M as RolePermissions,G as RoleStateMachine,Z as RoleTransferState,c as SYNC_PROTOCOL_VERSION,m as SessionLifecycleManager,E as SessionStore,a as SyncClientError,W as SyncClientEvent,g as SyncErrorType,_ as SyncMessageType,$ as SyncPhase,S as SyncPhasePermissions,y as SyncPhaseStateMachine,k as createWebSocketClient};