@gameroom-js/server
Advanced tools
Comparing version 0.1.1 to 0.1.2
import { Socket } from 'socket.io'; | ||
import { SocketPlus } from './types'; | ||
export declare enum ClientStates { | ||
export declare enum ClientStatus { | ||
JOINING = 0, | ||
@@ -10,6 +10,9 @@ JOINED = 1, | ||
} | ||
export interface ClientState { | ||
clientStatus: ClientStatus; | ||
} | ||
export declare class ClientController { | ||
id: string; | ||
socket: SocketPlus; | ||
clientState: ClientStates; | ||
clientStatus: ClientStatus; | ||
_messageQueue: Array<any>; | ||
@@ -21,2 +24,3 @@ constructor(socket: Socket); | ||
getClientID: () => string; | ||
getClientState: () => ClientState; | ||
} |
@@ -12,2 +12,3 @@ /// <reference types="node" /> | ||
getGameRooms: () => string[]; | ||
getGameRoom: (gameRoomID: string) => GameRoom | undefined; | ||
setListeners: () => Promise<void>; | ||
@@ -14,0 +15,0 @@ joinGameRoomByID: (client: ClientController, gameID: any) => Promise<boolean>; |
/// <reference types="node" /> | ||
import { ClientController } from './ClientController'; | ||
import { EventEmitter } from 'events'; | ||
export declare enum GameRoomState { | ||
export declare enum GameRoomStatus { | ||
CREATING = 0, | ||
@@ -13,6 +13,10 @@ READY = 1, | ||
} | ||
export interface RoomState { | ||
} | ||
export interface GameState { | ||
} | ||
export declare abstract class GameRoom { | ||
id: string; | ||
connectedClients: Map<string, ClientController>; | ||
gameRoomState: GameRoomState; | ||
gameRoomStatus: GameRoomStatus; | ||
autoDispose: boolean; | ||
@@ -23,4 +27,7 @@ _events: EventEmitter; | ||
private onMessageHandlers; | ||
protected onProtocolHandlers: { | ||
[messageType: string]: (client: ClientController) => void; | ||
}; | ||
protected onActionHandlers: { | ||
[messageType: string]: (client: ClientController, message?: any) => void; | ||
[messageType: string]: (client: ClientController, actionType: string) => void; | ||
}; | ||
@@ -32,2 +39,3 @@ protected onTransferHandlers: { | ||
onCreate?(): void | Promise<void>; | ||
onAuth(_client: ClientController): boolean | Promise<boolean>; | ||
onJoin?(client: ClientController, authenticated?: boolean): void | Promise<void>; | ||
@@ -37,8 +45,9 @@ onJoined?(client: ClientController): void | Promise<void>; | ||
onDispose?(): void | Promise<void>; | ||
onAuth(_client: ClientController): boolean | Promise<boolean>; | ||
getRoomState?(): unknown; | ||
getGameState?(): unknown; | ||
onMessage: (messageType: string | number, callback: (...args: any[]) => void) => void; | ||
onAction: (messageType: string | number, callback: (...args: any[]) => void) => void; | ||
onTransfer: (messageType: string | number, callback: (...args: any[]) => void) => void; | ||
getRoomState?(): RoomState; | ||
getGameState?(): GameState; | ||
onProtocol: (protocolType: string, listener: (...args: any[]) => void) => void; | ||
onAction: (actionType: string, listener: (...args: any[]) => void) => void; | ||
onTransfer: (transferType: string, listener: (...args: any[]) => void) => void; | ||
onEvent: (eventName: string | symbol, listener: (...args: any[]) => void) => void; | ||
emitEvent: (eventName: string | symbol, ...args: any) => void; | ||
broadcastRoomState: () => void; | ||
@@ -50,2 +59,3 @@ broadcastGameState: () => void; | ||
private _onLeave; | ||
private registerMessageHandler; | ||
private _onMessage; | ||
@@ -52,0 +62,0 @@ private _shouldDispose; |
export { ClientController } from './ClientController'; | ||
export { ConnectionController } from './ConnectionController'; | ||
export { GameRoom, GameRoomOptions } from './GameRoom'; | ||
export { GameRoom } from './GameRoom'; | ||
export type { GameRoomOptions, RoomState, GameState } from './GameRoom'; |
@@ -9,11 +9,11 @@ 'use strict'; | ||
var ClientStates; | ||
var ClientStatus; | ||
(function (ClientStates) { | ||
ClientStates[ClientStates["JOINING"] = 0] = "JOINING"; | ||
ClientStates[ClientStates["JOINED"] = 1] = "JOINED"; | ||
ClientStates[ClientStates["RECONNECTING"] = 2] = "RECONNECTING"; | ||
ClientStates[ClientStates["LEAVING"] = 3] = "LEAVING"; | ||
ClientStates[ClientStates["REJECTED"] = 4] = "REJECTED"; | ||
})(ClientStates || (ClientStates = {})); | ||
(function (ClientStatus) { | ||
ClientStatus[ClientStatus["JOINING"] = 0] = "JOINING"; | ||
ClientStatus[ClientStatus["JOINED"] = 1] = "JOINED"; | ||
ClientStatus[ClientStatus["RECONNECTING"] = 2] = "RECONNECTING"; | ||
ClientStatus[ClientStatus["LEAVING"] = 3] = "LEAVING"; | ||
ClientStatus[ClientStatus["REJECTED"] = 4] = "REJECTED"; | ||
})(ClientStatus || (ClientStatus = {})); | ||
@@ -23,6 +23,6 @@ class ClientController { | ||
this.send = (message, args, cb) => { | ||
// if clientState is still joining, client may not | ||
// if clientStatus is still joining or reconnecting, client may not | ||
// be ready to receive messages. Queue them up and | ||
// dispatch them after JOINED has been sent | ||
if (this.clientState === ClientStates.JOINING) { | ||
if (this.clientStatus !== ClientStatus.JOINED) { | ||
this._messageQueue.push({ | ||
@@ -54,5 +54,11 @@ message: message, | ||
this.getClientState = () => { | ||
return { | ||
clientStatus: this.clientStatus | ||
}; | ||
}; | ||
this.id = nanoid.nanoid(); | ||
this.socket = socket; | ||
this.clientState = ClientStates.JOINING; | ||
this.clientStatus = ClientStatus.JOINING; | ||
this._messageQueue = []; | ||
@@ -69,2 +75,8 @@ } | ||
this.getGameRoom = gameRoomID => { | ||
const gameRoom = this.gameRooms.get(gameRoomID); | ||
if (gameRoom) return gameRoom; | ||
return undefined; | ||
}; | ||
this.setListeners = async () => { | ||
@@ -117,4 +129,2 @@ this.io.on("connection", async socket => { | ||
this.disposeGameRoom = async gameRoom => { | ||
// inform subscribers that game i_onMessages being deleted so they can remove their references | ||
// actually remove reference to game | ||
this.gameRooms.delete(gameRoom.id); | ||
@@ -149,24 +159,34 @@ console.log(`[${this.constructor.name}]\n\tRemoved gameRoom: ${gameRoom.id}`); | ||
var GameRoomState; | ||
var GameRoomStatus; | ||
(function (GameRoomState) { | ||
GameRoomState[GameRoomState["CREATING"] = 0] = "CREATING"; | ||
GameRoomState[GameRoomState["READY"] = 1] = "READY"; | ||
GameRoomState[GameRoomState["DISPOSING"] = 2] = "DISPOSING"; | ||
})(GameRoomState || (GameRoomState = {})); | ||
(function (GameRoomStatus) { | ||
GameRoomStatus[GameRoomStatus["CREATING"] = 0] = "CREATING"; | ||
GameRoomStatus[GameRoomStatus["READY"] = 1] = "READY"; | ||
GameRoomStatus[GameRoomStatus["DISPOSING"] = 2] = "DISPOSING"; | ||
})(GameRoomStatus || (GameRoomStatus = {})); | ||
class GameRoom { | ||
constructor(options = {}) { | ||
this._events = new events.EventEmitter(); // methods to register callbacks for messages, actions, and transfers | ||
this._events = new events.EventEmitter(); // methods to register listeners for protocol messages, actions, and transfers | ||
this.onMessage = (messageType, callback) => { | ||
this.onMessageHandlers[messageType] = callback; | ||
this.onProtocol = (protocolType, listener) => { | ||
this.onProtocolHandlers[protocolType] = listener; | ||
}; | ||
this.onAction = (messageType, callback) => { | ||
this.onActionHandlers[messageType] = callback; | ||
this.onAction = (actionType, listener) => { | ||
this.onActionHandlers[actionType] = listener; | ||
}; | ||
this.onTransfer = (messageType, callback) => { | ||
this.onTransferHandlers[messageType] = callback; | ||
this.onTransfer = (transferType, listener) => { | ||
this.onTransferHandlers[transferType] = listener; | ||
}; // register listeners for events | ||
this.onEvent = (eventName, listener) => { | ||
this._events.on(eventName, listener); | ||
}; // emit event | ||
this.emitEvent = (eventName, ...args) => { | ||
this._events.emit(eventName, ...args); | ||
}; | ||
@@ -211,3 +231,3 @@ | ||
this._events.once('ready', () => { | ||
this.gameRoomState = GameRoomState.READY; // if game has any queued calls that were queud before it finished creating, execute | ||
this.gameRoomStatus = GameRoomStatus.READY; // if game has any queued calls that were queud before it finished creating, execute | ||
// those now | ||
@@ -226,3 +246,3 @@ | ||
this._events.on('clientJoin', client => { | ||
if (this.gameRoomState !== GameRoomState.READY) { | ||
if (this.gameRoomStatus !== GameRoomStatus.READY) { | ||
this._callQueue.push(this._onJoin.bind(this, client)); | ||
@@ -235,3 +255,3 @@ } else { | ||
this._events.on('clientLeave', client => { | ||
if (this.gameRoomState !== GameRoomState.READY) { | ||
if (this.gameRoomStatus !== GameRoomStatus.READY) { | ||
this._callQueue.push(this._onLeave.bind(this, client)); | ||
@@ -247,12 +267,18 @@ } else { | ||
this._setInitialAutoDisposeTimeout(); | ||
} // register generic message listener for actions | ||
} // register generic protocol message listener for protocol messages | ||
this.onMessage('action', (client, message) => { | ||
if (this.onActionHandlers[message]) { | ||
this.onActionHandlers[message](client, message); | ||
this.registerMessageHandler('protocol', (client, protocolType) => { | ||
if (this.onProtocolHandlers[protocolType]) { | ||
this.onProtocolHandlers[protocolType](client); | ||
} | ||
}); // register generic message listener for actions | ||
this.registerMessageHandler('action', (client, actionType) => { | ||
if (this.onActionHandlers[actionType]) { | ||
this.onActionHandlers[actionType](client, actionType); | ||
} | ||
}); // register generic message listener for transfers | ||
this.onMessage('transfer', (client, data) => { | ||
this.registerMessageHandler('transfer', (client, data) => { | ||
const { | ||
@@ -266,2 +292,9 @@ t: transferType, | ||
} | ||
}); // register basic protocol message listeners | ||
this.onProtocol('FINISHED_JOINING_GAME', client => { | ||
this._onJoined(client); | ||
}); | ||
this.onProtocol('FAILED_JOINING_GAME', client => { | ||
client.clientStatus = ClientStatus.REJECTED; | ||
}); // run onCreate method (if defined by subclass) to register onMessageHandler events | ||
@@ -329,3 +362,3 @@ | ||
console.log(`[${this.constructor.name} - ${this.id}]\n\tClient ${client.id} has finished joining ${this.id}`); | ||
client.clientState = ClientStates.JOINED; // add client to connectedClients | ||
client.clientStatus = ClientStatus.JOINED; // add client to connectedClients | ||
@@ -361,3 +394,3 @@ const clientUserID = client.getClientID(); | ||
try { | ||
client.clientState = ClientStates.LEAVING; | ||
client.clientStatus = ClientStatus.LEAVING; | ||
await this.onLeave(client); | ||
@@ -371,3 +404,3 @@ } catch (e) { | ||
if (client.clientState !== ClientStates.RECONNECTING) { | ||
if (client.clientStatus !== ClientStatus.RECONNECTING) { | ||
const shouldDispose = this._shouldDispose(); | ||
@@ -383,25 +416,27 @@ | ||
this._onMessage = (client, data, cb) => { | ||
this.registerMessageHandler = (messageType, listener) => { | ||
this.onMessageHandlers[messageType] = listener; | ||
}; | ||
this._onMessage = (client, data) => { | ||
console.log(`[${this.constructor.name} - ${this.id}]\n\t[Client ${client.id}]\n\t\t${JSON.stringify(data)}`); | ||
if (data && data.t) { | ||
// protocol messages | ||
if (data.t === 'protocol') { | ||
if (data.m === 'FINISHED_JOINING_GAME') { | ||
this._onJoined(client); | ||
} else if (data.m === 'FAILED_JOINING_GAME') { | ||
client.clientState = ClientStates.REJECTED; | ||
} | ||
if (this.onProtocolHandlers[data.m]) { | ||
this.onProtocolHandlers[data.m](client); | ||
} // action and transfer messages | ||
} else if (data.t === 'game') { | ||
const { | ||
t: messageType, | ||
m: message | ||
m: payload | ||
} = data.m; | ||
if (this.onMessageHandlers[messageType]) { | ||
this.onMessageHandlers[messageType](client, message, cb); | ||
this.onMessageHandlers[messageType](client, payload); | ||
} | ||
} | ||
} | ||
if (cb) eval(cb); | ||
}; | ||
@@ -466,3 +501,3 @@ | ||
this.connectedClients = new Map(); | ||
this.gameRoomState = GameRoomState.CREATING; | ||
this.gameRoomStatus = GameRoomStatus.CREATING; | ||
this.autoDispose = opts.autoDispose; | ||
@@ -472,2 +507,3 @@ this._autoDisposeTimeout = undefined; | ||
this.onMessageHandlers = {}; | ||
this.onProtocolHandlers = {}; | ||
this.onActionHandlers = {}; | ||
@@ -477,5 +513,4 @@ this.onTransferHandlers = {}; | ||
this._init(); | ||
} // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
} | ||
onAuth(_client) { | ||
@@ -482,0 +517,0 @@ // by default, accept all clients unless auth logic is provided |
@@ -1,2 +0,2 @@ | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var nanoid=require("nanoid"),socket_io=require("socket.io"),events=require("events"),ClientStates;!function(t){t[t.JOINING=0]="JOINING",t[t.JOINED=1]="JOINED",t[t.RECONNECTING=2]="RECONNECTING",t[t.LEAVING=3]="LEAVING",t[t.REJECTED=4]="REJECTED"}(ClientStates||(ClientStates={}));class ClientController{constructor(t){this.send=(t,e,s)=>{this.clientState!==ClientStates.JOINING?this.socket.emit(t,e,s):this._messageQueue.push({message:t,args:e,cb:s})},this.on=(t,e)=>{this.socket.on(t,e)},this.sendJoinInitiate=()=>{this.socket.emit("INITIATE_JOIN")},this.getClientID=()=>this.id,this.id=nanoid.nanoid(),this.socket=t,this.clientState=ClientStates.JOINING,this._messageQueue=[]}}class ConnectionController{constructor(t,e){this.getGameRooms=()=>Array.from(this.gameRooms.keys()),this.setListeners=async()=>{this.io.on("connection",async t=>{const e=new ClientController(t);t.handshake.query.gameID&&(this.joinGameRoomByID(e,t.handshake.query.gameID)||(t.emit("JOIN_FAILED","Could not connect to game."),t.emit("message","Could not connect to game."),t.disconnect(!0)))})},this.joinGameRoomByID=async(t,e)=>{const s=this.gameRooms.get(e);return!!s&&(s._events.emit("clientJoin",t),!0)},this.registerGameRoom=t=>{this.gameRooms.set(t.id,t),t._events.once("dispose",this.disposeGameRoom.bind(this,t)),t._events.once("disconnect",()=>t._events.removeAllListeners())},this.disposeGameRoom=async t=>{this.gameRooms.delete(t.id)},this.io=new socket_io.Server(t,e),this.gameRooms=new Map,this.init()}init(){this.setListeners()}}const createID=function(t){t||(t=8);let e="";for(let s=1;s<t+1;s+=8)e+=Math.random().toString(36).substring(2,10);return e.substring(0,t).toUpperCase()};var GameRoomState;!function(t){t[t.CREATING=0]="CREATING",t[t.READY=1]="READY",t[t.DISPOSING=2]="DISPOSING"}(GameRoomState||(GameRoomState={}));class GameRoom{constructor(options={}){this._events=new events.EventEmitter,this.onMessage=(t,e)=>{this.onMessageHandlers[t]=e},this.onAction=(t,e)=>{this.onActionHandlers[t]=e},this.onTransfer=(t,e)=>{this.onTransferHandlers[t]=e},this.broadcastRoomState=()=>{let t;this.getRoomState&&(t=this.getRoomState()),this.connectedClients.forEach(e=>{e.send("updateRoomState",t)})},this.broadcastGameState=()=>{let t;this.getGameState&&(t=this.getGameState()),this.connectedClients.forEach(e=>{e.send("updateGameState",t)})},this._init=async()=>{if(this._events.once("dispose",async()=>{try{await this._dispose()}catch(t){}this._events.emit("disconnect")}),this._events.once("ready",()=>{this.gameRoomState=GameRoomState.READY,this._callQueue.length>0&&(this._callQueue.forEach(t=>{t()}),this._callQueue=[])}),this._events.on("clientJoin",t=>{this.gameRoomState!==GameRoomState.READY?this._callQueue.push(this._onJoin.bind(this,t)):this._onJoin(t)}),this._events.on("clientLeave",t=>{this.gameRoomState!==GameRoomState.READY?this._callQueue.push(this._onLeave.bind(this,t)):this._onLeave(t)}),this.autoDispose&&this._setInitialAutoDisposeTimeout(),this.onMessage("action",(t,e)=>{this.onActionHandlers[e]&&this.onActionHandlers[e](t,e)}),this.onMessage("transfer",(t,e)=>{const{t:s,m:i}=e;this.onTransferHandlers[s]&&this.onTransferHandlers[s](t,i)}),this.onCreate)try{return await this.onCreate(),void this._events.emit("ready")}catch(t){}else this._events.emit("ready")},this._onJoin=async t=>{this._autoDisposeTimeout&&(clearTimeout(this._autoDisposeTimeout),this._autoDisposeTimeout=void 0),t.socket.onLeave=()=>{this._events.emit("clientLeave",t)},t.socket.once("disconnect",t.socket.onLeave);try{const e=await this.onAuth(t);if(!e)throw t.socket.emit("JOIN_FAILED","Cannot authenticate room joining."),new Error("Authentication Failed");t.sendJoinInitiate(),this.onJoin&&await this.onJoin(t,e)}catch(t){}t.on("message",this._onMessage.bind(this,t))},this._onJoined=async t=>{t.clientState=ClientStates.JOINED;const e=t.getClientID();if(e&&this.connectedClients.set(e,t),this.onJoined)try{await this.onJoined(t)}catch(t){}t._messageQueue.length>0&&(t._messageQueue.forEach(e=>t.send(e.message,e.args,e.cb)),t._messageQueue=[])},this._onLeave=async t=>{const e=t.getClientID();if(this.connectedClients.delete(e)&&this.onLeave)try{t.clientState=ClientStates.LEAVING,await this.onLeave(t)}catch(t){}t.clientState!==ClientStates.RECONNECTING&&this._shouldDispose()&&this._resetAutoDisposeTimeout()},this._onMessage=(client,data,cb)=>{if(data&&data.t)if("protocol"===data.t)"FINISHED_JOINING_GAME"===data.m?this._onJoined(client):"FAILED_JOINING_GAME"===data.m&&(client.clientState=ClientStates.REJECTED);else if("game"===data.t){const{t:t,m:e}=data.m;this.onMessageHandlers[t]&&this.onMessageHandlers[t](client,e,cb)}cb&&eval(cb)},this._shouldDispose=()=>!0===this.autoDispose&&0===this.connectedClients.size,this._dispose=async()=>{if(this.onDispose)try{await this.onDispose()}catch(t){}this._autoDisposeTimeout&&(clearInterval(this._autoDisposeTimeout),this._autoDisposeTimeout=void 0)},this._setInitialAutoDisposeTimeout=(t=3e5)=>{this._autoDisposeTimeout=setTimeout(()=>{this._autoDisposeTimeout=void 0,this._events.emit("dispose")},t)},this._resetAutoDisposeTimeout=(t=3e5)=>{void 0!==this._autoDisposeTimeout&&clearTimeout(this._autoDisposeTimeout),this.autoDispose&&(this._autoDisposeTimeout=setTimeout(()=>{this._autoDisposeTimeout=void 0,this._events.emit("dispose")},t))};const opts=Object.assign({gameRoomID:createID(5),autoDispose:!1},options);this.id=opts.gameRoomID,this.connectedClients=new Map,this.gameRoomState=GameRoomState.CREATING,this.autoDispose=opts.autoDispose,this._autoDisposeTimeout=void 0,this._callQueue=[],this.onMessageHandlers={},this.onActionHandlers={},this.onTransferHandlers={},this._init()}onAuth(t){return!0}}exports.ClientController=ClientController,exports.ConnectionController=ConnectionController,exports.GameRoom=GameRoom; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t,e=require("nanoid"),s=require("socket.io"),i=require("events");!function(t){t[t.JOINING=0]="JOINING",t[t.JOINED=1]="JOINED",t[t.RECONNECTING=2]="RECONNECTING",t[t.LEAVING=3]="LEAVING",t[t.REJECTED=4]="REJECTED"}(t||(t={}));class o{constructor(s){this.send=(e,s,i)=>{this.clientStatus===t.JOINED?this.socket.emit(e,s,i):this._messageQueue.push({message:e,args:s,cb:i})},this.on=(t,e)=>{this.socket.on(t,e)},this.sendJoinInitiate=()=>{this.socket.emit("INITIATE_JOIN")},this.getClientID=()=>this.id,this.getClientState=()=>({clientStatus:this.clientStatus}),this.id=e.nanoid(),this.socket=s,this.clientStatus=t.JOINING,this._messageQueue=[]}}const n=function(t){t||(t=8);let e="";for(let s=1;s<t+1;s+=8)e+=Math.random().toString(36).substring(2,10);return e.substring(0,t).toUpperCase()};var a;!function(t){t[t.CREATING=0]="CREATING",t[t.READY=1]="READY",t[t.DISPOSING=2]="DISPOSING"}(a||(a={})),exports.ClientController=o,exports.ConnectionController=class{constructor(t,e){this.getGameRooms=()=>Array.from(this.gameRooms.keys()),this.getGameRoom=t=>{const e=this.gameRooms.get(t);if(e)return e},this.setListeners=async()=>{this.io.on("connection",async t=>{const e=new o(t);t.handshake.query.gameID&&(this.joinGameRoomByID(e,t.handshake.query.gameID)||(t.emit("JOIN_FAILED","Could not connect to game."),t.emit("message","Could not connect to game."),t.disconnect(!0)))})},this.joinGameRoomByID=async(t,e)=>{const s=this.gameRooms.get(e);return!!s&&(s._events.emit("clientJoin",t),!0)},this.registerGameRoom=t=>{this.gameRooms.set(t.id,t),t._events.once("dispose",this.disposeGameRoom.bind(this,t)),t._events.once("disconnect",()=>t._events.removeAllListeners())},this.disposeGameRoom=async t=>{this.gameRooms.delete(t.id)},this.io=new s.Server(t,e),this.gameRooms=new Map,this.init()}init(){this.setListeners()}},exports.GameRoom=class{constructor(e={}){this._events=new i.EventEmitter,this.onProtocol=(t,e)=>{this.onProtocolHandlers[t]=e},this.onAction=(t,e)=>{this.onActionHandlers[t]=e},this.onTransfer=(t,e)=>{this.onTransferHandlers[t]=e},this.onEvent=(t,e)=>{this._events.on(t,e)},this.emitEvent=(t,...e)=>{this._events.emit(t,...e)},this.broadcastRoomState=()=>{let t;this.getRoomState&&(t=this.getRoomState()),this.connectedClients.forEach(e=>{e.send("updateRoomState",t)})},this.broadcastGameState=()=>{let t;this.getGameState&&(t=this.getGameState()),this.connectedClients.forEach(e=>{e.send("updateGameState",t)})},this._init=async()=>{if(this._events.once("dispose",async()=>{try{await this._dispose()}catch(t){}this._events.emit("disconnect")}),this._events.once("ready",()=>{this.gameRoomStatus=a.READY,this._callQueue.length>0&&(this._callQueue.forEach(t=>{t()}),this._callQueue=[])}),this._events.on("clientJoin",t=>{this.gameRoomStatus!==a.READY?this._callQueue.push(this._onJoin.bind(this,t)):this._onJoin(t)}),this._events.on("clientLeave",t=>{this.gameRoomStatus!==a.READY?this._callQueue.push(this._onLeave.bind(this,t)):this._onLeave(t)}),this.autoDispose&&this._setInitialAutoDisposeTimeout(),this.registerMessageHandler("protocol",(t,e)=>{this.onProtocolHandlers[e]&&this.onProtocolHandlers[e](t)}),this.registerMessageHandler("action",(t,e)=>{this.onActionHandlers[e]&&this.onActionHandlers[e](t,e)}),this.registerMessageHandler("transfer",(t,e)=>{const{t:s,m:i}=e;this.onTransferHandlers[s]&&this.onTransferHandlers[s](t,i)}),this.onProtocol("FINISHED_JOINING_GAME",t=>{this._onJoined(t)}),this.onProtocol("FAILED_JOINING_GAME",e=>{e.clientStatus=t.REJECTED}),this.onCreate)try{return await this.onCreate(),void this._events.emit("ready")}catch(t){}else this._events.emit("ready")},this._onJoin=async t=>{this._autoDisposeTimeout&&(clearTimeout(this._autoDisposeTimeout),this._autoDisposeTimeout=void 0),t.socket.onLeave=()=>{this._events.emit("clientLeave",t)},t.socket.once("disconnect",t.socket.onLeave);try{const e=await this.onAuth(t);if(!e)throw t.socket.emit("JOIN_FAILED","Cannot authenticate room joining."),new Error("Authentication Failed");t.sendJoinInitiate(),this.onJoin&&await this.onJoin(t,e)}catch(t){}t.on("message",this._onMessage.bind(this,t))},this._onJoined=async e=>{e.clientStatus=t.JOINED;const s=e.getClientID();if(s&&this.connectedClients.set(s,e),this.onJoined)try{await this.onJoined(e)}catch(t){}e._messageQueue.length>0&&(e._messageQueue.forEach(t=>e.send(t.message,t.args,t.cb)),e._messageQueue=[])},this._onLeave=async e=>{const s=e.getClientID();if(this.connectedClients.delete(s)&&this.onLeave)try{e.clientStatus=t.LEAVING,await this.onLeave(e)}catch(t){}e.clientStatus!==t.RECONNECTING&&this._shouldDispose()&&this._resetAutoDisposeTimeout()},this.registerMessageHandler=(t,e)=>{this.onMessageHandlers[t]=e},this._onMessage=(t,e)=>{if(e&&e.t)if("protocol"===e.t)this.onProtocolHandlers[e.m]&&this.onProtocolHandlers[e.m](t);else if("game"===e.t){const{t:s,m:i}=e.m;this.onMessageHandlers[s]&&this.onMessageHandlers[s](t,i)}},this._shouldDispose=()=>!0===this.autoDispose&&0===this.connectedClients.size,this._dispose=async()=>{if(this.onDispose)try{await this.onDispose()}catch(t){}this._autoDisposeTimeout&&(clearInterval(this._autoDisposeTimeout),this._autoDisposeTimeout=void 0)},this._setInitialAutoDisposeTimeout=(t=3e5)=>{this._autoDisposeTimeout=setTimeout(()=>{this._autoDisposeTimeout=void 0,this._events.emit("dispose")},t)},this._resetAutoDisposeTimeout=(t=3e5)=>{void 0!==this._autoDisposeTimeout&&clearTimeout(this._autoDisposeTimeout),this.autoDispose&&(this._autoDisposeTimeout=setTimeout(()=>{this._autoDisposeTimeout=void 0,this._events.emit("dispose")},t))};const s=Object.assign({gameRoomID:n(5),autoDispose:!1},e);this.id=s.gameRoomID,this.connectedClients=new Map,this.gameRoomStatus=a.CREATING,this.autoDispose=s.autoDispose,this._autoDisposeTimeout=void 0,this._callQueue=[],this.onMessageHandlers={},this.onProtocolHandlers={},this.onActionHandlers={},this.onTransferHandlers={},this._init()}onAuth(t){return!0}}; | ||
//# sourceMappingURL=server.cjs.production.min.js.map |
@@ -5,11 +5,11 @@ import { nanoid } from 'nanoid'; | ||
var ClientStates; | ||
var ClientStatus; | ||
(function (ClientStates) { | ||
ClientStates[ClientStates["JOINING"] = 0] = "JOINING"; | ||
ClientStates[ClientStates["JOINED"] = 1] = "JOINED"; | ||
ClientStates[ClientStates["RECONNECTING"] = 2] = "RECONNECTING"; | ||
ClientStates[ClientStates["LEAVING"] = 3] = "LEAVING"; | ||
ClientStates[ClientStates["REJECTED"] = 4] = "REJECTED"; | ||
})(ClientStates || (ClientStates = {})); | ||
(function (ClientStatus) { | ||
ClientStatus[ClientStatus["JOINING"] = 0] = "JOINING"; | ||
ClientStatus[ClientStatus["JOINED"] = 1] = "JOINED"; | ||
ClientStatus[ClientStatus["RECONNECTING"] = 2] = "RECONNECTING"; | ||
ClientStatus[ClientStatus["LEAVING"] = 3] = "LEAVING"; | ||
ClientStatus[ClientStatus["REJECTED"] = 4] = "REJECTED"; | ||
})(ClientStatus || (ClientStatus = {})); | ||
@@ -19,6 +19,6 @@ class ClientController { | ||
this.send = (message, args, cb) => { | ||
// if clientState is still joining, client may not | ||
// if clientStatus is still joining or reconnecting, client may not | ||
// be ready to receive messages. Queue them up and | ||
// dispatch them after JOINED has been sent | ||
if (this.clientState === ClientStates.JOINING) { | ||
if (this.clientStatus !== ClientStatus.JOINED) { | ||
this._messageQueue.push({ | ||
@@ -50,5 +50,11 @@ message: message, | ||
this.getClientState = () => { | ||
return { | ||
clientStatus: this.clientStatus | ||
}; | ||
}; | ||
this.id = nanoid(); | ||
this.socket = socket; | ||
this.clientState = ClientStates.JOINING; | ||
this.clientStatus = ClientStatus.JOINING; | ||
this._messageQueue = []; | ||
@@ -65,2 +71,8 @@ } | ||
this.getGameRoom = gameRoomID => { | ||
const gameRoom = this.gameRooms.get(gameRoomID); | ||
if (gameRoom) return gameRoom; | ||
return undefined; | ||
}; | ||
this.setListeners = async () => { | ||
@@ -113,4 +125,2 @@ this.io.on("connection", async socket => { | ||
this.disposeGameRoom = async gameRoom => { | ||
// inform subscribers that game i_onMessages being deleted so they can remove their references | ||
// actually remove reference to game | ||
this.gameRooms.delete(gameRoom.id); | ||
@@ -145,24 +155,34 @@ if (process.env.NODE_ENV === 'development') console.log(`[${this.constructor.name}]\n\tRemoved gameRoom: ${gameRoom.id}`); | ||
var GameRoomState; | ||
var GameRoomStatus; | ||
(function (GameRoomState) { | ||
GameRoomState[GameRoomState["CREATING"] = 0] = "CREATING"; | ||
GameRoomState[GameRoomState["READY"] = 1] = "READY"; | ||
GameRoomState[GameRoomState["DISPOSING"] = 2] = "DISPOSING"; | ||
})(GameRoomState || (GameRoomState = {})); | ||
(function (GameRoomStatus) { | ||
GameRoomStatus[GameRoomStatus["CREATING"] = 0] = "CREATING"; | ||
GameRoomStatus[GameRoomStatus["READY"] = 1] = "READY"; | ||
GameRoomStatus[GameRoomStatus["DISPOSING"] = 2] = "DISPOSING"; | ||
})(GameRoomStatus || (GameRoomStatus = {})); | ||
class GameRoom { | ||
constructor(options = {}) { | ||
this._events = new EventEmitter(); // methods to register callbacks for messages, actions, and transfers | ||
this._events = new EventEmitter(); // methods to register listeners for protocol messages, actions, and transfers | ||
this.onMessage = (messageType, callback) => { | ||
this.onMessageHandlers[messageType] = callback; | ||
this.onProtocol = (protocolType, listener) => { | ||
this.onProtocolHandlers[protocolType] = listener; | ||
}; | ||
this.onAction = (messageType, callback) => { | ||
this.onActionHandlers[messageType] = callback; | ||
this.onAction = (actionType, listener) => { | ||
this.onActionHandlers[actionType] = listener; | ||
}; | ||
this.onTransfer = (messageType, callback) => { | ||
this.onTransferHandlers[messageType] = callback; | ||
this.onTransfer = (transferType, listener) => { | ||
this.onTransferHandlers[transferType] = listener; | ||
}; // register listeners for events | ||
this.onEvent = (eventName, listener) => { | ||
this._events.on(eventName, listener); | ||
}; // emit event | ||
this.emitEvent = (eventName, ...args) => { | ||
this._events.emit(eventName, ...args); | ||
}; | ||
@@ -207,3 +227,3 @@ | ||
this._events.once('ready', () => { | ||
this.gameRoomState = GameRoomState.READY; // if game has any queued calls that were queud before it finished creating, execute | ||
this.gameRoomStatus = GameRoomStatus.READY; // if game has any queued calls that were queud before it finished creating, execute | ||
// those now | ||
@@ -222,3 +242,3 @@ | ||
this._events.on('clientJoin', client => { | ||
if (this.gameRoomState !== GameRoomState.READY) { | ||
if (this.gameRoomStatus !== GameRoomStatus.READY) { | ||
this._callQueue.push(this._onJoin.bind(this, client)); | ||
@@ -231,3 +251,3 @@ } else { | ||
this._events.on('clientLeave', client => { | ||
if (this.gameRoomState !== GameRoomState.READY) { | ||
if (this.gameRoomStatus !== GameRoomStatus.READY) { | ||
this._callQueue.push(this._onLeave.bind(this, client)); | ||
@@ -243,12 +263,18 @@ } else { | ||
this._setInitialAutoDisposeTimeout(); | ||
} // register generic message listener for actions | ||
} // register generic protocol message listener for protocol messages | ||
this.onMessage('action', (client, message) => { | ||
if (this.onActionHandlers[message]) { | ||
this.onActionHandlers[message](client, message); | ||
this.registerMessageHandler('protocol', (client, protocolType) => { | ||
if (this.onProtocolHandlers[protocolType]) { | ||
this.onProtocolHandlers[protocolType](client); | ||
} | ||
}); // register generic message listener for actions | ||
this.registerMessageHandler('action', (client, actionType) => { | ||
if (this.onActionHandlers[actionType]) { | ||
this.onActionHandlers[actionType](client, actionType); | ||
} | ||
}); // register generic message listener for transfers | ||
this.onMessage('transfer', (client, data) => { | ||
this.registerMessageHandler('transfer', (client, data) => { | ||
const { | ||
@@ -262,2 +288,9 @@ t: transferType, | ||
} | ||
}); // register basic protocol message listeners | ||
this.onProtocol('FINISHED_JOINING_GAME', client => { | ||
this._onJoined(client); | ||
}); | ||
this.onProtocol('FAILED_JOINING_GAME', client => { | ||
client.clientStatus = ClientStatus.REJECTED; | ||
}); // run onCreate method (if defined by subclass) to register onMessageHandler events | ||
@@ -325,3 +358,3 @@ | ||
if (process.env.NODE_ENV === 'development') console.log(`[${this.constructor.name} - ${this.id}]\n\tClient ${client.id} has finished joining ${this.id}`); | ||
client.clientState = ClientStates.JOINED; // add client to connectedClients | ||
client.clientStatus = ClientStatus.JOINED; // add client to connectedClients | ||
@@ -357,3 +390,3 @@ const clientUserID = client.getClientID(); | ||
try { | ||
client.clientState = ClientStates.LEAVING; | ||
client.clientStatus = ClientStatus.LEAVING; | ||
await this.onLeave(client); | ||
@@ -367,3 +400,3 @@ } catch (e) { | ||
if (client.clientState !== ClientStates.RECONNECTING) { | ||
if (client.clientStatus !== ClientStatus.RECONNECTING) { | ||
const shouldDispose = this._shouldDispose(); | ||
@@ -379,25 +412,27 @@ | ||
this._onMessage = (client, data, cb) => { | ||
this.registerMessageHandler = (messageType, listener) => { | ||
this.onMessageHandlers[messageType] = listener; | ||
}; | ||
this._onMessage = (client, data) => { | ||
if (process.env.NODE_ENV === 'development') console.log(`[${this.constructor.name} - ${this.id}]\n\t[Client ${client.id}]\n\t\t${JSON.stringify(data)}`); | ||
if (data && data.t) { | ||
// protocol messages | ||
if (data.t === 'protocol') { | ||
if (data.m === 'FINISHED_JOINING_GAME') { | ||
this._onJoined(client); | ||
} else if (data.m === 'FAILED_JOINING_GAME') { | ||
client.clientState = ClientStates.REJECTED; | ||
} | ||
if (this.onProtocolHandlers[data.m]) { | ||
this.onProtocolHandlers[data.m](client); | ||
} // action and transfer messages | ||
} else if (data.t === 'game') { | ||
const { | ||
t: messageType, | ||
m: message | ||
m: payload | ||
} = data.m; | ||
if (this.onMessageHandlers[messageType]) { | ||
this.onMessageHandlers[messageType](client, message, cb); | ||
this.onMessageHandlers[messageType](client, payload); | ||
} | ||
} | ||
} | ||
if (cb) eval(cb); | ||
}; | ||
@@ -462,3 +497,3 @@ | ||
this.connectedClients = new Map(); | ||
this.gameRoomState = GameRoomState.CREATING; | ||
this.gameRoomStatus = GameRoomStatus.CREATING; | ||
this.autoDispose = opts.autoDispose; | ||
@@ -468,2 +503,3 @@ this._autoDisposeTimeout = undefined; | ||
this.onMessageHandlers = {}; | ||
this.onProtocolHandlers = {}; | ||
this.onActionHandlers = {}; | ||
@@ -473,5 +509,4 @@ this.onTransferHandlers = {}; | ||
this._init(); | ||
} // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
} | ||
onAuth(_client) { | ||
@@ -478,0 +513,0 @@ // by default, accept all clients unless auth logic is provided |
{ | ||
"version": "0.1.1", | ||
"version": "0.1.2", | ||
"license": "MIT", | ||
@@ -4,0 +4,0 @@ "main": "dist/index.js", |
188
README.md
# @gameroom-js/server | ||
A simple node library for creating multiplayer games. For use in conjunction with [@gameroom-js/client](https://github.com/jbierfeldt/gameroom-js/tree/master/packages/client). | ||
## Overview | ||
--- | ||
A simple library for Node.JS that allows you to rapidly develop stateful, socketed multiplayer games and web applications. For use in conjunction with the client library [@gameroom-js/client](https://github.com/jbierfeldt/gameroom-js/tree/master/packages/client). | ||
The `options` for creating a `GameRoom` are defined as follows: | ||
The three main classes in the gameroom-js server library are `GameRoom`, `ConnectionController`, and `ClientController`. | ||
## Quickstart | ||
#### **`Server.ts`** | ||
```typescript | ||
import express from "express"; | ||
import { ConnectionController } from "@gameroom-js/server"; | ||
import { MyGameRoom } from './MyGameRoom' | ||
// use Express.JS or any other http server library (http, Koa, etc.) to create a server | ||
const app = express(); | ||
const server = app.listen(3000); | ||
// pass the server to a new ConnectionController | ||
const connection = new ConnectionController(server); | ||
// create a GameRoom and register it with the ConnectionController | ||
const defaultGameRoom = new MyGameRoom({ gameRoomID: "Lobby" }); | ||
connection.registerGameRoom(defaultGameRoom); | ||
``` | ||
## GameRoom | ||
`GameRoom` is defined as an abstract class. In order to use it in your game or application, you need to extend it by writing your own subclass which extends it. In doing so, you'll include your own custom logic by adding class properties, methods, and registering listeners. | ||
### Extending GameRoom | ||
#### **`MyGameRoom.ts`** | ||
```typescript | ||
import { GameRoom, GameRoomOptions } from "@gameroom-js/server"; | ||
export interface MyGameRoomOptions extends GameRoomOptions {/*custom options interface*/} | ||
export interface MyRoomState {} | ||
export interface MyGameState {} | ||
export class MyGameRoom extends GameRoom { | ||
constructor(options: MyGameRoomOptions) { | ||
super(options) | ||
// custom properties for game/application logic | ||
// this.myGameState = {...} | ||
// this.clientNameMap = new Map() | ||
// ... | ||
} | ||
// implement the following lifecycle methods with your own logic | ||
async onCreate(): Promise<void> { | ||
// do listener setup in onCreate | ||
this.onMessage() | ||
this.onAction() | ||
this.onTransfer() | ||
this.onEvent() | ||
} | ||
async onAuth(): Promise<void> {/* custom client authentication logic */} | ||
async onJoin(): Promise<void> {/* custom client joining logic */} | ||
async onJoined(): Promise<void> {/* custom client has joined logic */} | ||
async onLeave(): Promise<void> {/* custom client leaving logic */} | ||
async onDispose(): Promise<void> {/* custom room disposal logic */} | ||
// implement the following getter methods for exposing room and game state | ||
getRoomState(): MyRoomState {/* ... */} | ||
getGameState(): MyGameState {/* ... */} | ||
// custom methods with game/application logic | ||
// nextTurn(): void {} | ||
// registerClientName: void {} | ||
// ... | ||
``` | ||
The `GameRoomOptions` interface is defined as follows: | ||
```typescript | ||
export interface GameRoomOptions { | ||
id: string; | ||
autoDispose: boolean; | ||
gameRoomID?: string; // default: random 5 character string (ex. GHT3D) | ||
autoDispose?: boolean; // default: false | ||
} | ||
``` | ||
``` | ||
### Events and Listeners | ||
--- | ||
There are four types of events that your `GameRoom` class can listen for. `Protocol`, `Action`, and `Transfer` are sent by the client. `Event` is used internally within the `GameRoom` and can also be accessed by other parts of the application. (For example, a REST API that also needs to make changes to or access the state of the game.) | ||
All of these listeners should be registered during the `onCreate` lifecycle method, implemented in your custom `GameRoom` class: | ||
#### **`MyGameRoom.ts`** | ||
```typescript | ||
async onCreate(): Promise<void> { | ||
// do listener setup in onCreate | ||
this.onProtocol(protocolType, listener); | ||
this.onAction(actionType, listener); | ||
this.onTransfer(transferType, listener); | ||
this.onEvent(eventName, listener); | ||
} | ||
``` | ||
### Events from the client | ||
The gameroom-js client can send three types of event: `Protocol`, `Action`, and `Transfer`. | ||
- `Protocol` is an event meant to communicate with the server for purposes of handshaking,authentication, or information requests that are separate from game logic. Event includes a `protocolType` string. `Protocol` event listeners are registered via the `onProtocol(protocolType, listener)` method. | ||
- `Action` is an event meant to communicate user interactions to the server. Button presses, clicks, and other interactions with the game or application should be sent as action events. Event includes a `actionType` string. `Action` event listeners are registered via the `onAction(actionType, listener)` method. | ||
- `Transfer` is an event that communicates a piece of information (serialized as a string) to the server. Event includes a `transferType` string as well as a `payload` string. `Transfer` event listeners are registered via the `onTransfer(transferType, listener)` method. | ||
When handling the events via their respective methods, the `clientController` which initiated the event is always sent to the listener as the first argument. This can be used to validate the event or to associate information with a particular client. | ||
```typescript | ||
this.onProtocol("REQUEST_CLIENT_STATE", (client: ClientController) => { | ||
client.send("updateClientState", client.getClientState()); | ||
}); | ||
this.onAction("nextTurnPressed", (client: ClientController) => { | ||
if (client.getClientID() === this.playerWithTurn) { | ||
this.advanceTurn(); | ||
} | ||
}); | ||
this.onTransfer("updateClientName", (client: ClientController, newName: string) => { | ||
this.updateClientName(client, newName); | ||
}); | ||
``` | ||
### Events from the server | ||
The gameroom-js server communicates internally via an `Event` system. Listeners are registered with the `onEvent(eventName, listener)` method. `Events` can be emitted via the `emitEvent(eventName, ...args)` method. | ||
Logic outside of the `GameRoom` can also access these events. A good example of this might be if your game or application has a REST API that might need to send events to the `GameRoom`: | ||
```typescript | ||
const app = express(); | ||
const server = app.listen(3000); | ||
const connection = new ConnectionController(server); | ||
app.post("/nextTurn", (req, res) => { | ||
const gameRoomID = req.body.gameRoomID; | ||
const gameRoom = connection.getGameRoom(gameRoomID); | ||
gameRoom.emitEvent("advanceTurn"); | ||
}) | ||
``` | ||
## ConnectionController | ||
The `ConncectionController` class handles communication between sockets and game rooms. When instantiated, it requires a reference to an http server (such as one created by Express.JS or Koa). gameroom.js uses Socket.IO for websocket communication, so `ConnectionController` optionally receives the [Socket.IO `ServerOptions` object](https://socket.io/docs/v4/server-options/). | ||
#### **`Server.ts`** | ||
```typescript | ||
import express from "express"; | ||
import { ConnectionController } from "@gameroom-js/server"; | ||
// use Express.JS or any other http server library (http, Koa, etc.) to create a server | ||
const app = express(); | ||
const server = app.listen(3000); | ||
// pass the server to a new ConnectionController | ||
// as well as an optional Socket.IO ServerOptions object | ||
const connection = new ConnectionController(server, {pingTimeout: 10000}); | ||
``` | ||
The most important method of `ConnectionController` is `registerGameRoom(gameRoom)`, which registers a new `GameRoom` and allows clients to connect to it. | ||
#### **`Server.ts`** | ||
```typescript | ||
// create a GameRoom and register it with the ConnectionController | ||
const defaultGameRoom = new MyGameRoom({ gameRoomID: "Lobby" }); | ||
connection.registerGameRoom(defaultGameRoom); | ||
``` | ||
`ConnectionController` also has methods for getting references to game rooms that have already been registered. | ||
- `getGameRooms()` returns an array of the `gameRoomID`s of all registered game rooms. | ||
- `getGameRoom(gameRoomID)` returns the reference to the game room that matches the provided `gameRoomID`. Returns `undefined` if no matching game room is registered. | ||
## ClientController | ||
The `ClientController` class is an abstraction on top of the [Socket.IO `socket` instance](https://socket.io/docs/v4/server-api/#socket). It can be extended if you wish to store state or add custom game/application logic, although this is not recommended. (We recommend finding a way to store individual player state with the rest of the GameState, and using an authentication system to reconnect new sockets to previous state.) | ||
To send an event to an individual client, use the `send(message, args, cb)` method. | ||
You can get a client's id via the `getClientID()` method. | ||
If you are storing state in the `ClientController`, you can get this information by extending and calling the `getClientState()` method. |
@@ -5,3 +5,3 @@ import { nanoid } from 'nanoid'; | ||
export enum ClientStates { | ||
export enum ClientStatus { | ||
JOINING, | ||
@@ -14,6 +14,10 @@ JOINED, | ||
export interface ClientState { | ||
clientStatus: ClientStatus; | ||
} | ||
export class ClientController { | ||
id: string; | ||
socket: SocketPlus; | ||
clientState: ClientStates; | ||
clientStatus: ClientStatus; | ||
_messageQueue: Array<any>; | ||
@@ -24,3 +28,3 @@ | ||
this.socket = socket; | ||
this.clientState = ClientStates.JOINING; | ||
this.clientStatus = ClientStatus.JOINING; | ||
this._messageQueue = []; | ||
@@ -30,7 +34,7 @@ } | ||
public send = (message: string, args?: any, cb?: any): void => { | ||
// if clientState is still joining, client may not | ||
// if clientStatus is still joining or reconnecting, client may not | ||
// be ready to receive messages. Queue them up and | ||
// dispatch them after JOINED has been sent | ||
if (this.clientState === ClientStates.JOINING) { | ||
if (this.clientStatus !== ClientStatus.JOINED) { | ||
this._messageQueue.push({ message: message, args: args, cb: cb }); | ||
@@ -56,2 +60,6 @@ return; | ||
}; | ||
public getClientState = (): ClientState => { | ||
return { clientStatus: this.clientStatus }; | ||
}; | ||
} |
@@ -29,2 +29,8 @@ import { Server, ServerOptions } from "socket.io"; | ||
getGameRoom = (gameRoomID: string): GameRoom | undefined => { | ||
const gameRoom = this.gameRooms.get(gameRoomID); | ||
if (gameRoom) return gameRoom | ||
return undefined; | ||
} | ||
setListeners = async (): Promise<void> => { | ||
@@ -80,5 +86,2 @@ this.io.on("connection", async (socket: SocketPlus): Promise<void> => { | ||
disposeGameRoom = async (gameRoom: GameRoom): Promise<void> => { | ||
// inform subscribers that game i_onMessages being deleted so they can remove their references | ||
// actually remove reference to game | ||
this.gameRooms.delete(gameRoom.id); | ||
@@ -85,0 +88,0 @@ if (process.env.NODE_ENV === 'development') console.log(`[${this.constructor.name}]\n\tRemoved gameRoom: ${gameRoom.id}`); |
import { createID } from './utilities'; | ||
import { ClientController, ClientStates } from './ClientController'; | ||
import { ClientController, ClientStatus } from './ClientController'; | ||
import { EventEmitter } from 'events'; | ||
export enum GameRoomState { | ||
export enum GameRoomStatus { | ||
CREATING, | ||
@@ -16,6 +16,10 @@ READY, | ||
// interfaces to be extended | ||
export interface RoomState {} | ||
export interface GameState {} | ||
export abstract class GameRoom { | ||
public id: string; | ||
public connectedClients: Map<string, ClientController>; | ||
public gameRoomState: GameRoomState; | ||
public gameRoomStatus: GameRoomStatus; | ||
@@ -33,8 +37,15 @@ public autoDispose: boolean; | ||
client: ClientController, | ||
message: any, | ||
payload: any, | ||
cb?: any | ||
) => void; | ||
}; | ||
protected onProtocolHandlers: { | ||
[messageType: string]: (client: ClientController) => void; | ||
}; | ||
protected onActionHandlers: { | ||
[messageType: string]: (client: ClientController, message?: any) => void; | ||
[messageType: string]: ( | ||
client: ClientController, | ||
actionType: string | ||
) => void; | ||
}; | ||
@@ -57,3 +68,3 @@ protected onTransferHandlers: { | ||
this.connectedClients = new Map(); | ||
this.gameRoomState = GameRoomState.CREATING; | ||
this.gameRoomStatus = GameRoomStatus.CREATING; | ||
@@ -67,2 +78,4 @@ this.autoDispose = opts.autoDispose; | ||
this.onMessageHandlers = {}; | ||
this.onProtocolHandlers = {}; | ||
this.onActionHandlers = {}; | ||
@@ -76,2 +89,6 @@ this.onTransferHandlers = {}; | ||
public onCreate?(): void | Promise<void>; | ||
public onAuth(_client: ClientController): boolean | Promise<boolean> { | ||
// by default, accept all clients unless auth logic is provided | ||
return true; | ||
} | ||
public onJoin?( | ||
@@ -84,32 +101,40 @@ client: ClientController, | ||
public onDispose?(): void | Promise<void>; | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
public onAuth(_client: ClientController): boolean | Promise<boolean> { | ||
// by default, accept all clients unless auth logic is provided | ||
return true; | ||
} | ||
public getRoomState?(): unknown; | ||
public getGameState?(): unknown; | ||
public getRoomState?(): RoomState; | ||
public getGameState?(): GameState; | ||
// methods to register callbacks for messages, actions, and transfers | ||
public onMessage = ( | ||
messageType: string | number, | ||
callback: (...args: any[]) => void | ||
// methods to register listeners for protocol messages, actions, and transfers | ||
public onProtocol = ( | ||
protocolType: string, | ||
listener: (...args: any[]) => void | ||
) => { | ||
this.onMessageHandlers[messageType] = callback; | ||
this.onProtocolHandlers[protocolType] = listener; | ||
}; | ||
public onAction = ( | ||
messageType: string | number, | ||
callback: (...args: any[]) => void | ||
actionType: string, | ||
listener: (...args: any[]) => void | ||
) => { | ||
this.onActionHandlers[messageType] = callback; | ||
this.onActionHandlers[actionType] = listener; | ||
}; | ||
public onTransfer = ( | ||
messageType: string | number, | ||
callback: (...args: any[]) => void | ||
transferType: string, | ||
listener: (...args: any[]) => void | ||
) => { | ||
this.onTransferHandlers[messageType] = callback; | ||
this.onTransferHandlers[transferType] = listener; | ||
}; | ||
// register listeners for events | ||
public onEvent = ( | ||
eventName: string | symbol, | ||
listener: (...args: any[]) => void | ||
) => { | ||
this._events.on(eventName, listener); | ||
}; | ||
// emit event | ||
public emitEvent = (eventName: string | symbol, ...args: any) => { | ||
this._events.emit(eventName, ...args); | ||
}; | ||
public broadcastRoomState = (): void => { | ||
let roomState: unknown; | ||
let roomState: RoomState; | ||
if (this.getRoomState) { | ||
@@ -124,3 +149,3 @@ roomState = this.getRoomState(); | ||
public broadcastGameState = (): void => { | ||
let gameState: unknown; | ||
let gameState: GameState; | ||
if (this.getGameState) { | ||
@@ -147,3 +172,3 @@ gameState = this.getGameState(); | ||
this._events.once('ready', () => { | ||
this.gameRoomState = GameRoomState.READY; | ||
this.gameRoomStatus = GameRoomStatus.READY; | ||
@@ -162,3 +187,3 @@ // if game has any queued calls that were queud before it finished creating, execute | ||
this._events.on('clientJoin', (client: ClientController) => { | ||
if (this.gameRoomState !== GameRoomState.READY) { | ||
if (this.gameRoomStatus !== GameRoomStatus.READY) { | ||
this._callQueue.push(this._onJoin.bind(this, client)); | ||
@@ -171,3 +196,3 @@ } else { | ||
this._events.on('clientLeave', (client: ClientController) => { | ||
if (this.gameRoomState !== GameRoomState.READY) { | ||
if (this.gameRoomStatus !== GameRoomStatus.READY) { | ||
this._callQueue.push(this._onLeave.bind(this, client)); | ||
@@ -185,17 +210,42 @@ } else { | ||
// register generic protocol message listener for protocol messages | ||
this.registerMessageHandler( | ||
'protocol', | ||
(client: ClientController, protocolType: string) => { | ||
if (this.onProtocolHandlers[protocolType]) { | ||
this.onProtocolHandlers[protocolType](client); | ||
} | ||
} | ||
); | ||
// register generic message listener for actions | ||
this.onMessage('action', (client: ClientController, message: any) => { | ||
if (this.onActionHandlers[message]) { | ||
this.onActionHandlers[message](client, message); | ||
this.registerMessageHandler( | ||
'action', | ||
(client: ClientController, actionType: string) => { | ||
if (this.onActionHandlers[actionType]) { | ||
this.onActionHandlers[actionType](client, actionType); | ||
} | ||
} | ||
}); | ||
); | ||
// register generic message listener for transfers | ||
this.onMessage('transfer', (client: ClientController, data: any) => { | ||
const { t: transferType, m: payload } = data; | ||
if (this.onTransferHandlers[transferType]) { | ||
this.onTransferHandlers[transferType](client, payload); | ||
this.registerMessageHandler( | ||
'transfer', | ||
(client: ClientController, data: { t: string; m: string }) => { | ||
const { t: transferType, m: payload } = data; | ||
if (this.onTransferHandlers[transferType]) { | ||
this.onTransferHandlers[transferType](client, payload); | ||
} | ||
} | ||
); | ||
// register basic protocol message listeners | ||
this.onProtocol('FINISHED_JOINING_GAME', (client: ClientController) => { | ||
this._onJoined(client); | ||
}); | ||
this.onProtocol('FAILED_JOINING_GAME', (client: ClientController) => { | ||
client.clientStatus = ClientStatus.REJECTED; | ||
}); | ||
// run onCreate method (if defined by subclass) to register onMessageHandler events | ||
@@ -272,3 +322,3 @@ if (this.onCreate) { | ||
); | ||
client.clientState = ClientStates.JOINED; | ||
client.clientStatus = ClientStatus.JOINED; | ||
@@ -311,3 +361,3 @@ // add client to connectedClients | ||
try { | ||
client.clientState = ClientStates.LEAVING; | ||
client.clientStatus = ClientStatus.LEAVING; | ||
await this.onLeave(client); | ||
@@ -321,3 +371,3 @@ } catch (e) { | ||
// and should be disposed | ||
if (client.clientState !== ClientStates.RECONNECTING) { | ||
if (client.clientStatus !== ClientStatus.RECONNECTING) { | ||
const shouldDispose = this._shouldDispose(); | ||
@@ -335,3 +385,10 @@ if (shouldDispose) { | ||
private _onMessage = (client: ClientController, data?: any, cb?: any) => { | ||
private registerMessageHandler = ( | ||
messageType: string, | ||
listener: (...args: any[]) => void | ||
) => { | ||
this.onMessageHandlers[messageType] = listener; | ||
}; | ||
private _onMessage = (client: ClientController, data?: any) => { | ||
if (process.env.NODE_ENV === 'development') | ||
@@ -345,17 +402,15 @@ console.log( | ||
if (data && data.t) { | ||
// protocol messages | ||
if (data.t === 'protocol') { | ||
if (data.m === 'FINISHED_JOINING_GAME') { | ||
this._onJoined(client); | ||
} else if (data.m === 'FAILED_JOINING_GAME') { | ||
client.clientState = ClientStates.REJECTED; | ||
if (this.onProtocolHandlers[data.m]) { | ||
this.onProtocolHandlers[data.m](client); | ||
} | ||
// action and transfer messages | ||
} else if (data.t === 'game') { | ||
const { t: messageType, m: message } = data.m; | ||
const { t: messageType, m: payload } = data.m; | ||
if (this.onMessageHandlers[messageType]) { | ||
this.onMessageHandlers[messageType](client, message, cb); | ||
this.onMessageHandlers[messageType](client, payload); | ||
} | ||
} | ||
} | ||
if (cb) eval(cb); | ||
}; | ||
@@ -362,0 +417,0 @@ |
export { ClientController } from './ClientController'; | ||
export { ConnectionController } from './ConnectionController'; | ||
export { GameRoom, GameRoomOptions } from './GameRoom'; | ||
export { GameRoom } from './GameRoom'; | ||
export type {GameRoomOptions, RoomState, GameState} from './GameRoom'; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Uses eval
Supply chain riskPackage uses eval() which is a dangerous function. This prevents the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
160636
1451
190
0