@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) • 38.8 kB
JavaScript
"use strict";var se=Object.create;var v=Object.defineProperty;var ie=Object.getOwnPropertyDescriptor;var oe=Object.getOwnPropertyNames;var re=Object.getPrototypeOf,ae=Object.prototype.hasOwnProperty;var ce=(i,e)=>{for(var t in e)v(i,t,{get:e[t],enumerable:!0})},z=(i,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of oe(e))!ae.call(i,s)&&s!==t&&v(i,s,{get:()=>e[s],enumerable:!(n=ie(e,s))||n.enumerable});return i};var le=(i,e,t)=>(t=i!=null?se(re(i)):{},z(e||!i||!i.__esModule?v(t,"default",{value:i,enumerable:!0}):t,i)),de=i=>z(v({},"__esModule",{value:!0}),i);var Ce={};ce(Ce,{ClientRole:()=>D,ConflictResolution:()=>O,ConnectionManager:()=>f,ConnectionStatus:()=>H,RelayClient:()=>k,RelaySessionOrchestrator:()=>U,RolePermissions:()=>N,RoleStateMachine:()=>q,RoleTransferState:()=>Y,SYNC_PROTOCOL_VERSION:()=>c,SessionLifecycleManager:()=>m,SessionStore:()=>E,SyncClientError:()=>a,SyncClientEvent:()=>Z,SyncErrorType:()=>g,SyncMessageType:()=>R,SyncPhase:()=>F,SyncPhasePermissions:()=>u,SyncPhaseStateMachine:()=>I,createWebSocketClient:()=>_});module.exports=de(Ce);var W=require("events"),y=class{constructor(){this.emitter=new W.EventEmitter}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 T=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(o){console.error("EventEmitter error:",o)}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 y:new T}var H=(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))(H||{});var g=(S=>(S.CONNECTION_FAILED="connection_failed",S.CONNECTION_LOST="connection_lost",S.INVALID_MESSAGE="invalid_message",S.SERVER_ERROR="server_error",S.TIMEOUT="timeout",S.MAX_RECONNECT_ATTEMPTS_EXCEEDED="max_reconnect_attempts_exceeded",S.PROTOCOL_VERSION="protocol_version",S.INVALID_STATE_TRANSITION="invalid_state_transition",S.INVALID_OPERATION="invalid_operation",S))(g||{}),a=class extends Error{constructor(t,n,s){super(n);this.type=t;this.originalError=s;this.name="SyncClientError"}};var Z=(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))(Z||{});var R=(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))(R||{});var D=(s=>(s.UNINITIALIZED="uninitialized",s.CONNECTED="connected",s.PASSIVE="passive",s.ACTIVE="active",s))(D||{}),Y=(s=>(s.IDLE="idle",s.REQUESTING="requesting",s.TRANSFERRING="transferring",s.SYNCHRONIZED="synchronized",s))(Y||{}),O=(n=>(n.FIRST_COME_FIRST_SERVED="first_come_first_served",n.DENY_BOTH="deny_both",n.USER_CHOICE="user_choice",n))(O||{}),N=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[]}}},q=class{constructor(){this.currentRole="uninitialized"}getCurrentRole(){return this.currentRole}canTransitionTo(e){return N.getValidTransitions(this.currentRole).includes(e)}transitionTo(e){return this.canTransitionTo(e)?(this.currentRole=e,!0):!1}reset(){this.currentRole="uninitialized"}};var F=(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))(F||{}),u=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[]}}},I=class{constructor(){this.currentPhase="disconnected"}getCurrentPhase(){return this.currentPhase}canTransitionTo(e){return u.getValidTransitions(this.currentPhase).includes(e)}transitionTo(e){return this.canTransitionTo(e)?(this.currentPhase=e,!0):!1}reset(){this.currentPhase="disconnected"}};var J=le(require("ws")),A=class{async connect(e){return this.socket=new J.default(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 P=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 _(){return typeof process<"u"&&process.versions&&process.versions.node?new A:new P}var M=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=_(),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 L=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 b=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){var t;if("clientId"in e&&"data"in e){if(e.clientId==="relay-server"&&((t=e.data)!=null&&t.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){var t;if("data"in e)throw new a("server_error",((t=e.data)==null?void 0:t.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 k=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 I,this.connectionManager=new M({wsUrl:this.config.wsUrl,connectionTimeout:this.config.connectionTimeout,autoReconnect:this.config.autoReconnect,maxReconnectAttempts:this.config.maxReconnectAttempts,reconnectDelay:this.config.reconnectDelay}),this.sessionManager=new L({baseUrl:this.config.baseUrl,sessionEndpoint:this.config.sessionEndpoint}),this.messageDispatcher=new b(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(u.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(u.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(u.canSendTutorialState(this.getCurrentSyncPhase()),"Only active clients can send tutorial state"),this.messageDispatcher.broadcastTutorialState(e)}requestTutorialState(){this.enforcePermission(u.canRequestSync(this.getCurrentSyncPhase()),"Only active or initializing pull clients can request state"),this.messageDispatcher.requestStateSync()}getLastTutorialState(){return this.messageDispatcher.getCurrentState()}offerControlToPeer(){this.enforcePermission(u.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)})}};var ne=require("events"),V=require("ws");var d=[];for(let i=0;i<256;++i)d.push((i+256).toString(16).slice(1));function Q(i,e=0){return(d[i[e+0]]+d[i[e+1]]+d[i[e+2]]+d[i[e+3]]+"-"+d[i[e+4]]+d[i[e+5]]+"-"+d[i[e+6]]+d[i[e+7]]+"-"+d[i[e+8]]+d[i[e+9]]+"-"+d[i[e+10]]+d[i[e+11]]+d[i[e+12]]+d[i[e+13]]+d[i[e+14]]+d[i[e+15]]).toLowerCase()}var j=require("crypto"),w=new Uint8Array(256),x=w.length;function G(){return x>w.length-16&&((0,j.randomFillSync)(w),x=0),w.slice(x,x+=16)}var B=require("crypto"),$={randomUUID:B.randomUUID};function he(i,e,t){var s;if($.randomUUID&&!e&&!i)return $.randomUUID();i=i||{};let n=i.random??((s=i.rng)==null?void 0:s.call(i))??G();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 o=0;o<16;++o)e[t+o]=n[o];return e}return Q(n)}var K=he;var X=require("events");var E=class extends X.EventEmitter{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,o=new Date(s.getTime()+(t.expiresIn||this.defaultSessionTimeoutMs)),r={id:n,createdAt:s,expiresAt:o,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,o]of this.sessions.entries())o.status==="active"&&t>o.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)}};var ee=require("events"),m=class extends ee.EventEmitter{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)}};var te=require("events");var f=class extends te.EventEmitter{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 o=this.sessionConnections.get(s);return o&&(o.delete(t),o.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 o of n){let r=this.connections.get(o);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 o=s.role;return s.role=n,s.lastRoleChange=new Date,console.log(`\u{1F504} Connection ${t} role changed: ${o} \u2192 ${n}`),!0}findActiveConnection(t){return this.getSessionConnections(t).find(s=>s.role==="active")||null}findConnectionByClientId(t,n){return this.getSessionConnections(t).find(o=>o.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 o of n)o.socket.readyState===o.socket.OPEN&&o.socket.close();let s=this.sessionConnections.get(t);if(s){for(let o of s)this.connections.delete(o);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 U=class extends ne.EventEmitter{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){var s;if(n.type==="role_changed"&&"data"in n&&((s=n.data)!=null&&s.role)){let o=n.data.role;this.connectionManager.setConnectionRole(t.id,o),this.emit("roleChanged",t.sessionId,t.id,o)}this.routeMessage(t,n)}handleControlRequest(t,n){let s=this.sessionStore.getInternal(t.sessionId);if(!s)return;let o=this.connectionManager.findActiveConnection(t.sessionId);o?o.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,o,s)?(this.connectionManager.setConnectionRole(o.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(o,{type:"error",clientId:o.clientId||o.id,data:{reason:"Control transferred to another client"},timestamp:Date.now(),protocol_version:1}),this.emit("controlTransferred",t.sessionId,o.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 o=this.connectionManager.findActiveConnection(t.sessionId);o&&this.connectionManager.setConnectionRole(o.id,"passive"),this.connectionManager.setConnectionRole(t.id,"active"),s.activeClientId=t.clientId||t.id,this.emit("controlTransferred",t.sessionId,(o==null?void 0:o.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 o=n.data;if(this.connectionManager.getSessionConnections(t.sessionId).length===1)this.assignSyncDirection(t,o.preferredDirection,"Single client session");else{this.assignSyncDirection(t,o.preferredDirection,"Sync direction coordination");let h=o.preferredDirection==="ACTIVE"?"PASSIVE":"ACTIVE";this.assignOtherClientsDirection(s,t,h,"Complementary sync direction")}}assignSyncDirection(t,n,s){let o=n==="ACTIVE"?"active":"passive";this.connectionManager.setConnectionRole(t.id,o);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,o){let r=this.connectionManager.getSessionConnections(t.id);for(let h of r)h.id!==n.id&&this.assignSyncDirection(h,s,o)}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 o of s)o.id!==t.id&&o.socket.readyState===V.WebSocket.OPEN&&this.sendToConnection(o,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===V.WebSocket.OPEN&&n.socket.ping()}sendToConnection(t,n){t.socket.readyState===V.WebSocket.OPEN&&t.socket.send(JSON.stringify(n))}generateConnectionId(){return K()}on(t,n){return super.on(t,n)}emit(t,...n){return super.emit(t,...n)}};0&&(module.exports={ClientRole,ConflictResolution,ConnectionManager,ConnectionStatus,RelayClient,RelaySessionOrchestrator,RolePermissions,RoleStateMachine,RoleTransferState,SYNC_PROTOCOL_VERSION,SessionLifecycleManager,SessionStore,SyncClientError,SyncClientEvent,SyncErrorType,SyncMessageType,SyncPhase,SyncPhasePermissions,SyncPhaseStateMachine,createWebSocketClient});