diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index c71edd86..85549c03 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -20,9 +20,9 @@ import {parse} from "query-string"; import {jwtTokenManager} from "../Services/JWTTokenManager"; import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi"; import {SocketManager, socketManager} from "../Services/SocketManager"; -import {emitInBatch, pongMaxInterval, refresLogoutTimerOnPong, resetPing} from "../Services/IoSocketHelpers"; +import {emitInBatch} from "../Services/IoSocketHelpers"; import {clientEventsEmitter} from "../Services/ClientEventsEmitter"; -import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; +import {ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable"; export class IoSocketController { private nextUserId: number = 1; @@ -110,6 +110,7 @@ export class IoSocketController { this.app.ws('/room', { /* Options */ //compression: uWS.SHARED_COMPRESSOR, + idleTimeout: SOCKET_IDLE_TIMER, maxPayloadLength: 16 * 1024 * 1024, maxBackpressure: 65536, // Maximum 64kB of data in the buffer. //idleTimeout: 10, @@ -239,8 +240,6 @@ export class IoSocketController { // Let's join the room const client = this.initClient(ws); //todo: into the upgrade instead? socketManager.handleJoinRoom(client); - resetPing(client); - refresLogoutTimerOnPong(ws as ExSocketInterface); //get data information and show messages if (ADMIN_API_URL) { @@ -293,9 +292,6 @@ export class IoSocketController { drain: (ws) => { console.log('WebSocket backpressure: ' + ws.getBufferedAmount()); }, - pong(ws) { - refresLogoutTimerOnPong(ws as ExSocketInterface); - }, close: (ws, code, message) => { const Client = (ws as ExSocketInterface); try { diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index 55fd1bb7..3a2ac99e 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -10,6 +10,7 @@ const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_ISS = process.env.JITSI_ISS || ''; const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; +export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed export { SECRET_KEY, diff --git a/back/src/Model/Websocket/ExSocketInterface.ts b/back/src/Model/Websocket/ExSocketInterface.ts index e3d19138..c64a4952 100644 --- a/back/src/Model/Websocket/ExSocketInterface.ts +++ b/back/src/Model/Websocket/ExSocketInterface.ts @@ -25,8 +25,6 @@ export interface ExSocketInterface extends WebSocket, Identificable { emitInBatch: (payload: SubMessage) => void; batchedMessages: BatchMessage; batchTimeout: NodeJS.Timeout|null; - pingTimeout: NodeJS.Timeout|null; - pongTimeout: NodeJS.Timeout|null; disconnecting: boolean, tags: string[], textures: CharacterTexture[], diff --git a/back/src/Services/GaugeManager.ts b/back/src/Services/GaugeManager.ts index f8af822b..80712856 100644 --- a/back/src/Services/GaugeManager.ts +++ b/back/src/Services/GaugeManager.ts @@ -6,8 +6,13 @@ class GaugeManager { private nbClientsPerRoomGauge: Gauge; private nbGroupsPerRoomGauge: Gauge; private nbGroupsPerRoomCounter: Counter; + private nbRoomsGauge: Gauge; constructor() { + this.nbRoomsGauge = new Gauge({ + name: 'workadventure_nb_rooms', + help: 'Number of active rooms' + }); this.nbClientsGauge = new Gauge({ name: 'workadventure_nb_sockets', help: 'Number of connected sockets', @@ -31,6 +36,13 @@ class GaugeManager { }); } + incNbRoomGauge(): void { + this.nbRoomsGauge.inc(); + } + decNbRoomGauge(): void { + this.nbRoomsGauge.dec(); + } + incNbClientPerRoomGauge(roomId: string): void { this.nbClientsGauge.inc(); this.nbClientsPerRoomGauge.inc({ room: roomId }); diff --git a/back/src/Services/IoSocketHelpers.ts b/back/src/Services/IoSocketHelpers.ts index acaa0bb9..9c27c59a 100644 --- a/back/src/Services/IoSocketHelpers.ts +++ b/back/src/Services/IoSocketHelpers.ts @@ -18,22 +18,6 @@ export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): voi socket.batchTimeout = null; }, 100); } - - // If we send a message, we don't need to keep the connection alive - resetPing(socket); -} - -export function resetPing(ws: ExSocketInterface): void { - if (ws.pingTimeout) { - clearTimeout(ws.pingTimeout); - } - ws.pingTimeout = setTimeout(() => { - if (ws.disconnecting) { - return; - } - ws.ping(); - resetPing(ws); - }, 29000); } export function emitError(Client: ExSocketInterface, message: string): void { @@ -49,11 +33,3 @@ export function emitError(Client: ExSocketInterface, message: string): void { console.warn(message); } -export const pongMaxInterval = 30000; // the maximum duration (in ms) between pongs before we shutdown the connexion. - -export function refresLogoutTimerOnPong(ws: ExSocketInterface): void { - if(ws.pongTimeout) clearTimeout(ws.pongTimeout); - ws.pongTimeout = setTimeout(() => { - ws.close(); - }, pongMaxInterval); -} diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 4bd26778..97f008c4 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -351,6 +351,7 @@ export class SocketManager { world.leave(Client); if (world.isEmpty()) { this.Worlds.delete(Client.roomId); + gaugeManager.decNbRoomGauge(); } } //user leave previous room @@ -383,6 +384,7 @@ export class SocketManager { world.tags = data.tags world.policyType = Number(data.policy_type) } + gaugeManager.incNbRoomGauge(); this.Worlds.set(roomId, world); } return Promise.resolve(world) diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 19d011ff..b25e2d76 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -26,6 +26,7 @@ import { QueryJitsiJwtMessage, SendJitsiJwtMessage, CharacterLayerMessage, + PingMessage, SendUserMessage } from "../Messages/generated/messages_pb" @@ -42,6 +43,8 @@ import { } from "./ConnexionModels"; import {BodyResourceDescriptionInterface} from "../Phaser/Entity/body_character"; +const manualPingDelay = 20000; + export class RoomConnection implements RoomConnection { private readonly socket: WebSocket; private userId: number|null = null; @@ -84,7 +87,9 @@ export class RoomConnection implements RoomConnection { this.socket.binaryType = 'arraybuffer'; this.socket.onopen = (ev) => { - //console.log('WS connected'); + //we manually ping every 20s to not be logged out by the server, even when the game is in background. + const pingMessage = new PingMessage(); + setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay); }; this.socket.onmessage = (messageEvent) => { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index d34541dd..91da925b 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -467,35 +467,7 @@ export class GameScene extends ResizableScene implements CenterListener { // From now, this game scene will be notified of reposition events layoutManager.setListener(this); - - this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue) => { - if (newValue === undefined) { - coWebsiteManager.closeCoWebsite(); - } else { - coWebsiteManager.loadCoWebsite(newValue as string); - } - }); - this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => { - if (newValue === undefined) { - this.stopJitsi(); - } else { - if (JITSI_PRIVATE_MODE) { - const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; - - this.connection.emitQueryJitsiJwtMessage(this.instance.replace('/', '-') + "-" + newValue, adminTag); - } else { - this.startJitsi(newValue as string); - } - } - }) - - this.gameMap.onPropertyChange('silent', (newValue, oldValue) => { - if (newValue === undefined || newValue === false || newValue === '') { - this.connection.setSilent(false); - } else { - this.connection.setSilent(true); - } - }); + this.triggerOnMapLayerPropertyChange(); const camera = this.cameras.main; @@ -628,14 +600,46 @@ export class GameScene extends ResizableScene implements CenterListener { this.gameMap.setPosition(event.x, event.y); }) - this.scene.wake(); this.scene.sleep(ReconnectingSceneName); + //init user position and play trigger to check layers properties + this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y); + return connection; }); } + private triggerOnMapLayerPropertyChange(){ + this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue) => { + if (newValue === undefined) { + coWebsiteManager.closeCoWebsite(); + } else { + coWebsiteManager.loadCoWebsite(newValue as string); + } + }); + this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => { + if (newValue === undefined) { + this.stopJitsi(); + } else { + if (JITSI_PRIVATE_MODE) { + const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; + + this.connection.emitQueryJitsiJwtMessage(this.instance.replace('/', '-') + "-" + newValue, adminTag); + } else { + this.startJitsi(newValue as string); + } + } + }) + this.gameMap.onPropertyChange('silent', (newValue, oldValue) => { + if (newValue === undefined || newValue === false || newValue === '') { + this.connection.setSilent(false); + } else { + this.connection.setSilent(true); + } + }); + } + private switchLayoutMode(): void { //if discussion is activated, this layout cannot be activated if(mediaManager.activatedDiscussion){ @@ -718,6 +722,7 @@ export class GameScene extends ResizableScene implements CenterListener { */ //todo: push that into the gameManager private loadNextGame(layer: ITiledMapLayer, mapWidth: number, roomId: string){ + const room = new Room(roomId); gameManager.loadMap(room, this.scene); const exitSceneKey = roomId; diff --git a/front/src/Phaser/UserInput/UserInputManager.ts b/front/src/Phaser/UserInput/UserInputManager.ts index fa7080b5..636783bc 100644 --- a/front/src/Phaser/UserInput/UserInputManager.ts +++ b/front/src/Phaser/UserInput/UserInputManager.ts @@ -40,7 +40,9 @@ export class UserInputManager { initKeyBoardEvent(){ this.KeysCode = [ {event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false) }, + {event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false) }, {event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false) }, + {event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false) }, {event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false) }, {event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false) }, diff --git a/front/src/WebRtc/CoWebsiteManager.ts b/front/src/WebRtc/CoWebsiteManager.ts index 46d03702..70d171a2 100644 --- a/front/src/WebRtc/CoWebsiteManager.ts +++ b/front/src/WebRtc/CoWebsiteManager.ts @@ -16,6 +16,11 @@ class CoWebsiteManager { private opened: iframeStates = iframeStates.closed; private observers = new Array(); + /** + * Quickly going in and out of an iframe trigger can create conflicts between the iframe states. + * So we use this promise to queue up every cowebsite state transition + */ + private currentOperationPromise: Promise = Promise.resolve(); private close(): HTMLDivElement { const cowebsiteDiv = HtmlUtils.getElementByIdOrFail(cowebsiteDivId); @@ -52,7 +57,7 @@ class CoWebsiteManager { const onTimeoutPromise = new Promise((resolve) => { setTimeout(() => resolve(), 2000); }); - Promise.race([onloadPromise, onTimeoutPromise]).then(() => { + this.currentOperationPromise = this.currentOperationPromise.then(() =>Promise.race([onloadPromise, onTimeoutPromise])).then(() => { this.open(); setTimeout(() => { this.fire(); @@ -65,7 +70,7 @@ class CoWebsiteManager { */ public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise): void { const cowebsiteDiv = this.load(); - callback(cowebsiteDiv).then(() => { + this.currentOperationPromise = this.currentOperationPromise.then(() => callback(cowebsiteDiv)).then(() => { this.open(); setTimeout(() => { this.fire(); @@ -74,14 +79,16 @@ class CoWebsiteManager { } public closeCoWebsite(): Promise { - return new Promise((resolve, reject) => { + this.currentOperationPromise = this.currentOperationPromise.then(() => new Promise((resolve, reject) => { + if(this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example const cowebsiteDiv = this.close(); this.fire(); setTimeout(() => { resolve(); setTimeout(() => cowebsiteDiv.innerHTML = '', 500) }, animationTime) - }); + })); + return this.currentOperationPromise; } public getGameSize(): {width: number, height: number} { diff --git a/messages/messages.proto b/messages/messages.proto index 89353825..6e0b47df 100644 --- a/messages/messages.proto +++ b/messages/messages.proto @@ -38,6 +38,10 @@ message CharacterLayerMessage { /*********** CLIENT TO SERVER MESSAGES *************/ +message PingMessage { + +} + message SetPlayerDetailsMessage { string name = 1; repeated string characterLayers = 2;