diff --git a/src/MancalaApp.tsx b/src/MancalaApp.tsx index b35675d..27e2a36 100644 --- a/src/MancalaApp.tsx +++ b/src/MancalaApp.tsx @@ -16,6 +16,7 @@ import { Theme } from './theme/Theme'; import LobyPage from './routes/LobyPage'; import swal from 'sweetalert'; import { ConnectionState } from './rtmt/rtmt'; +import Util from './util/Util'; const context = initContext(); @@ -27,56 +28,42 @@ const MancalaApp: FunctionComponent = () => { const [theme, setTheme] = useState(context.themeManager.theme); - const onConnectionDone = () => { - setConnetionState("connected"); - }; - const onConnectionLost = () => { - setConnetionState("reconnecting"); - connectToServer(); - }; - const onConnectionError = (event: Event) => { - setConnetionState("error"); - connectToServer(); - }; - const onThemeChange = (theme: Theme) => { - setTheme(theme) + const onConnectionError = (event: Event) => console.error("(RTMT) Connection Error: ", event); + + const onConnectionChange = (_connectionState: ConnectionState) => setConnetionState(_connectionState); + + const onThemeChange = (theme: Theme) => setTheme(theme); + + const connectRTMT = (userKey: string) => { + const rtmt = context.rtmt as RTMTWS; + rtmt.on("error", onConnectionError); + rtmt.on("connectionchange", onConnectionChange) + rtmt.connectWebSocket(userKey) + return rtmt; } - const connectToServer = async () => { - try { - const userKey = await context.userKeyStore.getUserKey(); + + const loadUserKeyAndConnectServer = () => { + context.userKeyStore.getUserKey().then((userKey: string) => { setUserKey(userKey); - (context.rtmt as RTMTWS).initWebSocket(userKey); - } catch (error) { + connectRTMT(userKey); + }).catch((error) => { //TODO: check if it is network error! swal(context.texts.Error + "!", context.texts.ErrorWhenRetrievingInformation, "error"); console.error(error); - } - }; + }); + } + + const disposeApp = () => { + context.rtmt?.dispose(); + context.themeManager.off("themechange", onThemeChange); + } React.useEffect(() => { - context.rtmt.on("open", onConnectionDone); - context.rtmt.on("close", onConnectionLost); - context.rtmt.on("error", onConnectionError); + loadUserKeyAndConnectServer(); context.themeManager.on("themechange", onThemeChange); - setConnetionState("connecting"); - connectToServer(); - return () => { - context.rtmt.dispose(); - context.themeManager.off("themechange", onThemeChange); - }; + return () => disposeApp(); }, []); - const showConnectionState = connectionState != "connected"; - const floatingPanelColor = context.themeManager.theme.boardColor; - const connectionStateText = () => { - const map: { [key: string]: string } = { - connecting: context.texts.Connecting, - connected: context.texts.Connected, - error: context.texts.CannotConnect, - reconnecting: context.texts.ConnectingAgain, - }; - return map[connectionState]; - }; const textColorOnBoard = getColorByBrightness( context.themeManager.theme.boardColor, context.themeManager.theme.textColor, @@ -96,8 +83,8 @@ const MancalaApp: FunctionComponent = () => { - - {connectionStateText()} + + {Util.getTextByConnectionState(context, connectionState)} ); diff --git a/src/const/config.ts b/src/const/config.ts index 2068795..e7ac3e3 100644 --- a/src/const/config.ts +++ b/src/const/config.ts @@ -17,3 +17,4 @@ export const server: Server = useLocalServer ? { wsServerAdress: "wss://segin.one/mancala-backend-beta/", }; +export const RTMT_WS_PING_INTERVAL = 1000, RTMT_WS_PING_INTERVAL_BUFFER_TIME = 2000; \ No newline at end of file diff --git a/src/routes/GamePage.tsx b/src/routes/GamePage.tsx index 331d841..83b7254 100644 --- a/src/routes/GamePage.tsx +++ b/src/routes/GamePage.tsx @@ -138,7 +138,7 @@ const GamePage: FunctionComponent<{ const checkHasAnOngoingAction = () => hasOngoingAction; const onLeaveGameClick = () => { - if(Util.checkConnectionAndMaybeAlert(context)) return; + if (Util.checkConnectionAndMaybeAlert(context)) return; swal({ title: context.texts.AreYouSureToLeaveGame, icon: "warning", @@ -153,7 +153,7 @@ const GamePage: FunctionComponent<{ }; const onNewGameClick = () => { - if(Util.checkConnectionAndMaybeAlert(context)) return; + if (Util.checkConnectionAndMaybeAlert(context)) return; navigate("/loby") }; @@ -161,6 +161,7 @@ const GamePage: FunctionComponent<{ if (!game || isSpectator || !userKey) { return; } + if (Util.checkConnectionAndMaybeAlert(context)) return; if (game.mancalaGame.getPlayerIdByIndex(index) !== userKey) { notyf.error(context.texts.UCanOnlyPlayYourOwnPits); return; @@ -174,7 +175,6 @@ const GamePage: FunctionComponent<{ notyf.error(context.texts.UMustWaitUntilCurrentMoveComplete); return; } - if(Util.checkConnectionAndMaybeAlert(context)) return; if (!boardViewModel) return; //TODO: this check should be in mancala.js if (pit.stoneCount === 0) { diff --git a/src/rtmt/rtmt.ts b/src/rtmt/rtmt.ts index 499a8a0..3b92b2a 100644 --- a/src/rtmt/rtmt.ts +++ b/src/rtmt/rtmt.ts @@ -5,7 +5,7 @@ export type OnMessage = (message : Object) => any export type ConnectionState = "none" | "connecting" | "error" | "connected" | "closed" | "reconnecting"; -export type RtmtEventTypes = "open" | "close" | "connected" | "error" | "disconnected" | "message"; +export type RtmtEventTypes = "open" | "close" | "connected" | "error" | "disconnected" | "message" | "connectionchange"; export interface RTMT extends EventEmitter2 { get connectionState() : ConnectionState; diff --git a/src/rtmt/rtmt_websocket.ts b/src/rtmt/rtmt_websocket.ts index 2117618..6aaf1bd 100644 --- a/src/rtmt/rtmt_websocket.ts +++ b/src/rtmt/rtmt_websocket.ts @@ -1,63 +1,91 @@ import { decode, encode } from "./encode_decode_message"; import { channel_ping, channel_pong } from "../const/channel_names"; -import { server } from "../const/config"; +import { RTMT_WS_PING_INTERVAL, RTMT_WS_PING_INTERVAL_BUFFER_TIME, server } from "../const/config"; import EventEmitter2, { Listener } from "eventemitter2"; import { Bytes, ConnectionState, RTMT, RtmtEventTypes } from "./rtmt"; -const PING_INTERVAL = 15000, PING_INTERVAL_BUFFER_TIME = 1000; const MESSAGE_CHANNEL_PREFIX = "message_channel"; export class RTMTWS extends EventEmitter2 implements RTMT { - private webSocket: WebSocket; + private webSocket?: WebSocket; private pingTimeout?: number = undefined; private _connectionState: ConnectionState = "none"; - - constructor() { - super(); - } + private userKey: string; get connectionState(): ConnectionState { return this._connectionState; } - public initWebSocket(userKey: string) { - this._connectionState = this._connectionState !== "none" ? "reconnecting" : "connecting"; - const url = server.wsServerAdress + "?userKey=" + userKey; + protected setConnectionState(connectionState: ConnectionState) { + this._connectionState = connectionState; + this.emit("connectionchange", this._connectionState); + } + + private createWebSocket() { + const url = server.wsServerAdress + "?userKey=" + this.userKey; const webSocket = new WebSocket(url); - webSocket.onopen = () => { - console.info("(RTMT) WebSocket has opened"); - this.webSocket = webSocket; - this.heartbeat(); - this._connectionState = "connected"; - this.emit("open"); - }; - webSocket.onclose = () => { - console.info("(RTMT) WebSocket has closed"); - //this.WebSocket = undefined - clearTimeout(this.pingTimeout); - this._connectionState = "closed"; - this.emit("close"); - }; - webSocket.onmessage = (event: MessageEvent) => { - const { channel, message } = decode(event.data); - this.onMessage(channel, message); - }; - webSocket.onerror = (error) => { - console.error(error); - this._connectionState = "error"; - this.emit("error", error); - } + webSocket.onopen = () => this.onWebSocketOpen(webSocket); + webSocket.onclose = () => this.onWebSocketClose(webSocket); + webSocket.onmessage = (event: MessageEvent) => this.onWebSocketMessage(webSocket, event); + webSocket.onerror = (error: any) => this.onWebSocketError(webSocket, error); + } + + private disposeWebSocket() { + if (!this.webSocket) return; + this.webSocket.onopen = () => { }; + this.webSocket.onclose = () => { }; + this.webSocket.onmessage = (event: MessageEvent) => { }; + this.webSocket.onerror = (error: any) => { }; + this.webSocket = undefined; + } + + public connectWebSocket(userKey: string) { + this.setConnectionState("connecting"); + this.userKey = userKey; + this.createWebSocket(); + } + + private reconnectWebSocket() { + this.setConnectionState("reconnecting"); + this.disposeWebSocket(); + setTimeout(() => this.createWebSocket(), 1000); + } + + protected onWebSocketOpen(webSocket: WebSocket) { + this.webSocket = webSocket; + this.setConnectionState("connected"); + console.info("(RTMT) WebSocket has opened"); + this.heartbeat(); + this.emit("open"); + } + + protected onWebSocketMessage(webSocket: WebSocket, event: MessageEvent) { + const { channel, message } = decode(event.data); + this.onMessage(channel, message); + } + + protected onWebSocketError(webSocket: WebSocket, error: any) { + this.setConnectionState("error"); + console.error(error); + this.emit("error", error); + } + + protected onWebSocketClose(webSocket: WebSocket) { + this.setConnectionState("closed"); + console.info("(RTMT) WebSocket has closed"); + //this.WebSocket = undefined + clearTimeout(this.pingTimeout); + this.emit("close"); + this.reconnectWebSocket(); } private heartbeat() { clearTimeout(this.pingTimeout); - // Use `WebSocket#terminate()`, which immediately destroys the connection, - // instead of `WebSocket#close()`, which waits for the close timer. - // Delay should be equal to the interval at which your server - // sends out pings plus a conservative assumption of the latency. this.pingTimeout = setTimeout(() => { + if (!this.webSocket) return; this.webSocket.close(); - }, PING_INTERVAL + PING_INTERVAL_BUFFER_TIME); + this.onWebSocketClose(this.webSocket); + }, RTMT_WS_PING_INTERVAL + RTMT_WS_PING_INTERVAL_BUFFER_TIME); } public sendMessage(channel: string, message: Object) { @@ -96,7 +124,7 @@ export class RTMTWS extends EventEmitter2 implements RTMT { } public dispose() { - this.webSocket.close(); + this.disposeWebSocket(); this.removeAllListeners(); } } diff --git a/src/util/Util.ts b/src/util/Util.ts index cf27332..9529d11 100644 --- a/src/util/Util.ts +++ b/src/util/Util.ts @@ -1,4 +1,5 @@ import { Context } from "../context/context"; +import { ConnectionState } from "../rtmt/rtmt"; import notyf from "./Notyf"; export default class Util { @@ -17,4 +18,15 @@ export default class Util { } return false; } + + public static getTextByConnectionState(context: Context, connectionState: ConnectionState): string { + const map: { [key: string]: string } = { + connecting: context.texts.Connecting, + connected: context.texts.Connected, + error: context.texts.CannotConnect, + closed: context.texts.ConnectingAgain, + reconnecting: context.texts.ConnectingAgain, + }; + return map[connectionState]; + } }