import { Server , Client , ErrorCode , ClientEvent , Room , RoomClass , IRoomObject , RoomInspector , LobbyRoom , ICache , } from "./internal" ; export interface ISyncRoomTypes { [ name ] : { roomClass : RoomClass ; params : any ; } ; } export interface ICreateRoomData { sessionId : string ; name : string ; options ? : any ; } export interface IJoinRoomData { sessionId : string ; roomId : string ; options ? : any ; } export interface IRoomUpdate { data ? : any ; hostId ? : string ; locked ? : boolean ; } export interface IUpdateRoomData { sessionId : string ; roomId : string ; update : IRoomUpdate ; } export interface ILeaveRoomData { sessionId : string ; roomId : string ; } export interface ICloseRoomData { sessionId : string ; roomId : string ; } export class RoomManager { public roomTypes : ISyncRoomTypes = { } ; public rooms : Map < string , Room < any > > ; public inspector : RoomInspector ; public reservedSeats : Map < string , any > ; public reservationTimeout : number = 3 ; constructor ( public server , public cache ) { this . inspector = new RoomInspector ( server . ipcm , cache ) ; this . rooms = new Map < string , Room < any > > ( ) ; this . reservedSeats = new Map < string , any > ( ) ; } public defineRoomType ( name , roomClass , params ? ) { this . roomTypes [ name ] = { roomClass , params } ; } public setRoomClient ( room , client , trackingParams ? ) { client . onMessage ( ( type , data ) => room . onMessage && room . onMessage ( client , type , data ) ) ; room . clients . set ( client . id , client ) ; if ( room . tracker ) { room . startTracking ( client , trackingParams ) ; } } public async removeRoomClient ( room , client ) { room . clients . delete ( client . id ) ; if ( ! room . clients . size ) { return this . removeRoom ( room ) ; } else { return this . cache . set ( room . id , room . toObject ( ) ) ; } } public async onClientConnected ( client ) { console . debug ( `>> Process ${ this . server . processId }: Client ${ client . id } connect to room` , client . roomId ) ; const room = this . rooms . get ( client . roomId ) ; if ( ! room ) { return client . error ( ErrorCode . RoomNotFound , "Room not found" ) ; } const roomClient = room . clients . get ( client . id ) ; if ( roomClient ) { if ( roomClient . status !== "disconnected" ) { roomClient . error ( ErrorCode . ReconnectError , "Client Reconnected" ) ; } client . on ( ClientEvent . reconnected , async ( ) => { clearTimeout ( timer ) ; this . setRoomClient ( room , client , roomClient . state . trackingParams ) ; client . status = "reconnected" ; } ) ; } else { const reservationId = client . roomId + "-" + client . id ; if ( ! this . reservedSeats . has ( reservationId ) ) { return client . error ( ErrorCode . ReservationExpired , "Reservation expired" ) ; } const { options , disposer , auth } = this . reservedSeats . get ( reservationId ) ; clearTimeout ( disposer ) ; this . reservedSeats . delete ( reservationId ) ; client . on ( ClientEvent . joined , async ( ) => { clearTimeout ( timer ) ; try { room . onJoin && await room . onJoin ( client , options , auth ) ; } catch ( error ) { return client . error ( ErrorCode . JoinError , ( error as any ) ?. message || JSON . stringify ( error ) ) ; } this . setRoomClient ( room , client ) ; client . status = "connected" ; } ) ; } const timer = setTimeout ( ( ) => { client . error ( ErrorCode . ConnectionTimeout , "Connection timeout" ) ; } , this . server . connectionTimeout ) ; client . connected ( ) ; } public async onClientDisconnected ( client , code ) { console . debug ( `>> Process ${ this . server . processId }: Client ${ client . id } disconnect from room` , client . roomId ) ; client . status = "disconnected" ; const room = this . rooms . get ( client . roomId ) ; if ( ! room ) { return client . error ( ErrorCode . RoomNotFound , "Room not found" ) ; } room . stopTracking ( client ) ; if ( room . clients . get ( client . id ) ) { room . onLeave && await room . onLeave ( client , code === 1000 ) ; const roomClient = room . clients . get ( client . id ) ; if ( roomClient && roomClient . status === "connected" ) { return ; } } return this . removeRoomClient ( room , client ) ; } public async reserveSeat ( roomId , sessionId , options , auth ) { console . debug ( `>> Process ${ this . server . processId } requested: Reserve seat for client ${ sessionId } in room` , roomId ) ; const room = this . rooms . get ( roomId ) ; if ( ! room ) { throw new Error ( `Room with id ${ roomId } not found` ) ; } if ( room . clients . has ( sessionId ) ) { throw new Error ( `Client with id ${ sessionId } already in room` ) ; } room . clients . set ( sessionId , null ) ; const disposer = setTimeout ( ( ) => { this . reservedSeats . delete ( roomId + "-" + sessionId ) ; room . clients . delete ( sessionId ) ; if ( ! room . clients . size ) { this . removeRoom ( room ) ; } else { this . cache . set ( roomId , room . toObject ( ) ) ; } } , this . reservationTimeout * 1000 ) ; this . reservedSeats . set ( roomId + "-" + sessionId , { options , disposer , auth } ) ; console . debug ( `>> Process ${ this . server . processId } requested: Save room ${ roomId } to cache` ) ; await this . cache . set ( roomId , room . toObject ( ) ) ; } public async getRoomObject ( roomId = "" ) { console . debug ( `>> Process ${ this . server . processId }: Get room ${ roomId }` ) ; return this . cache . get ( roomId ) ; } public async getAvaliableRooms ( name = [ ] ) { console . debug ( `>> Process ${ this . server . processId }: Get avalible rooms ${ name }` ) ; if ( ! Array . isArray ( name ) ) { name = [ name ] ; } const cachedRooms = await this . cache . findMany ( ) ; const rooms = [ ] ; for ( const room of cachedRooms ) { const pid = String ( room . pid ) ; if ( this . server . ipcm . processId !== pid && ! this . server . ipcm . instances . has ( pid ) ) { await this . cache . remove ( room . id ) ; } else { rooms . push ( room ) ; } } return name . length ? rooms . filter ( ( room ) => name . indexOf ( room . name ) >= 0 ) : rooms ; } public async createRoom ( { sessionId , name , options } , join = true ) { console . debug ( `>> Process ${ this . server . processId } requested: Create room ${ name }` ) ; if ( ! Object . keys ( this . roomTypes ) . includes ( name ) ) { throw new Error ( `Room with name ${ name } not found` ) ; } const { roomClass , params } = this . roomTypes [ name ] ; const room = new roomClass ( { pid : this . server . processId , port : this . server . transport . proxyPort , hostId : sessionId , name , } ) ; console . debug ( `>> Process ${ this . server . processId }: Created room ${ name } with id ${ room . id }` ) ; room . updateCache = ( ) => { if ( ! room . clients . size ) { this . removeRoom ( room ) ; } else { this . cache . set ( room . id , room . toObject ( ) ) ; } } ; room . onCreate && await room . onCreate ( { ... params , options } ) ; if ( room . createState ) { room . state = room . createState ( ) ; } if ( room . createPatchTracker ) { room . tracker = room . createPatchTracker ( room . state ) ; } this . rooms . set ( room . id , room ) ; if ( room instanceof LobbyRoom ) { room . getRooms = ( names ) => this . getAvaliableRooms ( names ) ; this . inspector . subscribe ( room . id , room . watch , ( roomId , data ) => room . onRoomUpdate ( roomId , data ) ) ; } if ( join ) { return this . joinRoom ( { sessionId , roomId : room . id , options } ) ; } await this . cache . set ( room . id , room . toObject ( ) ) ; return room . toObject ( ) ; } public async joinRoom ( { sessionId , roomId , options } ) { console . debug ( `Process ${ this . server . processId } requested: Join room ${ roomId } for client ${ sessionId }` ) ; const room = this . rooms . get ( roomId ) ; if ( ! room ) { throw new Error ( `Cannot join the room with id ${ roomId } - room not found` ) ; } if ( room . locked ) { throw new Error ( `Cannot join the room with id ${ roomId } - room is locked` ) ; } const client = room . clients . get ( sessionId ) ; if ( ! client ) { const auth = room . onAuth && await room . onAuth ( sessionId , options ) ; this . reserveSeat ( roomId , sessionId , options , auth ) ; } else if ( client && client . status !== "disconnected" ) { throw new Error ( `Client ${ sessionId } already in room` ) ; } return room . toObject ( ) ; } public async updateRoom ( { sessionId , roomId , update } ) { const room = this . rooms . get ( roomId ) ; if ( ! room ) { throw new Error ( `Room with id ${ roomId } not found` ) ; } if ( room . hostId && room . hostId !== sessionId ) { throw new Error ( `Only host can update Room with id ${ roomId }` ) ; } room . data = update . data || room . data ; room . locked = update . locked || room . locked ; room . hostId = update . hostId || room . hostId ; await this . cache . set ( room . id , room . toObject ( ) ) ; return room . toObject ( ) ; } public async leaveRoom ( { sessionId , roomId } ) { const room = this . rooms . get ( roomId ) ; if ( ! room ) { throw new Error ( `Room with id ${ roomId } not found` ) ; } const client = room . clients . get ( sessionId ) ; if ( ! client ) { return ; } room . onLeave && room . onLeave ( client , true ) ; room . stopTracking ( client ) ; return this . removeRoomClient ( room , client ) ; } public async closeRoom ( { sessionId , roomId } ) { const room = this . rooms . get ( roomId ) ; if ( ! room ) { throw new Error ( `Room with id ${ roomId } not found` ) ; } if ( room . hostId && room . hostId !== sessionId ) { throw new Error ( `Only host can close Room with id ${ roomId }` ) ; } room . terminated = true ; for ( const client of room . clients . values ( ) ) { if ( ! client ) { continue ; } room . onLeave && room . onLeave ( client , true ) ; client . terminate ( ErrorCode . NormalClose , "Room closed" ) ; room . stopTracking ( client ) ; } room . clients . clear ( ) ; return this . removeRoom ( room ) ; } private async removeRoom ( room ) { room . onClose && await room . onClose ( ) ; await this . cache . remove ( room . id ) ; this . rooms . delete ( room . id ) ; if ( room instanceof LobbyRoom ) { this . inspector . unsubscribe ( room . id , room . watch ) ; } } public async closeRooms ( ) { for ( const room of this . rooms . values ( ) ) { await this . closeRoom ( { sessionId : room . hostId , roomId : room . id } ) ; } } }