From 6d59a713a319a9f655a1293f52bcb7b2088583c6 Mon Sep 17 00:00:00 2001 From: Zutatensuppe Date: Sat, 15 May 2021 20:04:30 +0200 Subject: [PATCH] dont automatically reconnect, add dc layer with option to reconnect --- public/Communication.js | 115 ++++++++++++++---- public/WsClient.js | 131 --------------------- public/components/ConnectionOverlay.vue.js | 34 ++++++ public/components/HelpOverlay.vue.js | 2 +- public/components/SettingsOverlay.vue.js | 2 +- public/game.js | 57 +++++---- public/style.css | 15 +-- public/views/Game.vue.js | 14 +++ 8 files changed, 179 insertions(+), 191 deletions(-) delete mode 100644 public/WsClient.js create mode 100644 public/components/ConnectionOverlay.vue.js diff --git a/public/Communication.js b/public/Communication.js index 77baff8..3e6be1a 100644 --- a/public/Communication.js +++ b/public/Communication.js @@ -1,35 +1,65 @@ "use strict" -import WsClient from './WsClient.js' +import { logger } from '../common/Util.js' import Protocol from './../common/Protocol.js' -/** @type WsClient */ -let conn +const log = logger('Communication.js') + +const CODE_GOING_AWAY = 1001 +const CODE_CUSTOM_DISCONNECT = 4000 + +const CONN_STATE_NOT_CONNECTED = 0 // not connected yet +const CONN_STATE_DISCONNECTED = 1 // not connected, but was connected before +const CONN_STATE_CONNECTED = 2 // connected +const CONN_STATE_CONNECTING = 3 // connecting +const CONN_STATE_CLOSED = 4 // not connected (closed on purpose) + +/** @type WebSocket */ +let ws let changesCallback = () => {} -let connectionLostCallback = () => {} +let connectionStateChangeCallback = () => {} // TODO: change these to something like on(EVT, cb) function onServerChange(callback) { changesCallback = callback } -function onConnectionLost(callback) { - connectionLostCallback = callback +function onConnectionStateChange(callback) { + connectionStateChangeCallback = callback } -function send(message) { - conn.send(JSON.stringify(message)) +let connectionState = CONN_STATE_NOT_CONNECTED +const setConnectionState = (v) => { + if (connectionState !== v) { + connectionState = v + connectionStateChangeCallback(v) + } } +function send(message) { + if (connectionState === CONN_STATE_CONNECTED) { + try { + ws.send(JSON.stringify(message)) + } catch (e) { + log.info('unable to send message.. maybe because ws is invalid?') + } + } +} + let clientSeq let events function connect(address, gameId, clientId) { clientSeq = 0 events = {} - conn = new WsClient(address, clientId + '|' + gameId) + setConnectionState(CONN_STATE_CONNECTING) return new Promise(resolve => { - conn.connect() - conn.onSocket('message', async ({ data }) => { - const msg = JSON.parse(data) + ws = new WebSocket(address, clientId + '|' + gameId) + ws.onopen = (e) => { + setConnectionState(CONN_STATE_CONNECTED) + connectionStateChangeCallback() + send([Protocol.EV_CLIENT_INIT]) + } + ws.onmessage = (e) => { + const msg = JSON.parse(e.data) const msgType = msg[0] if (msgType === Protocol.EV_SERVER_INIT) { const game = msg[1] @@ -46,22 +76,36 @@ function connect(address, gameId, clientId) { } else { throw `[ 2021-05-09 invalid connect msgType ${msgType} ]` } - }) - conn.onclose(() => { - connectionLostCallback() - }) - send([Protocol.EV_CLIENT_INIT]) + } + + ws.onerror = (e) => { + setConnectionState(CONN_STATE_DISCONNECTED) + throw `[ 2021-05-15 onerror ]` + } + + ws.onclose = (e) => { + if (e.code === CODE_CUSTOM_DISCONNECT || e.code === CODE_GOING_AWAY) { + setConnectionState(CONN_STATE_CLOSED) + } else { + setConnectionState(CONN_STATE_DISCONNECTED) + } + } }) } +// TOOD: change replay stuff function connectReplay(address, gameId, clientId) { clientSeq = 0 events = {} - conn = new WsClient(address, clientId + '|' + gameId) + setConnectionState(CONN_STATE_CONNECTING) return new Promise(resolve => { - conn.connect() - conn.onSocket('message', async ({ data }) => { - const msg = JSON.parse(data) + ws = new WebSocket(address, clientId + '|' + gameId) + ws.onopen = (e) => { + setConnectionState(CONN_STATE_CONNECTED) + send([Protocol.EV_CLIENT_INIT_REPLAY]) + } + ws.onmessage = (e) => { + const msg = JSON.parse(e.data) const msgType = msg[0] if (msgType === Protocol.EV_SERVER_INIT_REPLAY) { const game = msg[1] @@ -70,14 +114,26 @@ function connectReplay(address, gameId, clientId) { } else { throw `[ 2021-05-09 invalid connectReplay msgType ${msgType} ]` } - }) - send([Protocol.EV_CLIENT_INIT_REPLAY]) + } + + ws.onerror = (e) => { + setConnectionState(CONN_STATE_DISCONNECTED) + throw `[ 2021-05-15 onerror ]` + } + + ws.onclose = (e) => { + if (e.code === CODE_CUSTOM_DISCONNECT || e.code === CODE_GOING_AWAY) { + setConnectionState(CONN_STATE_CLOSED) + } else { + setConnectionState(CONN_STATE_DISCONNECTED) + } + } }) } function disconnect() { - if (conn) { - conn.disconnect() + if (ws) { + ws.close(CODE_CUSTOM_DISCONNECT) } clientSeq = 0 events = {} @@ -97,5 +153,12 @@ export default { disconnect, sendClientEvent, onServerChange, - onConnectionLost, + onConnectionStateChange, + CODE_CUSTOM_DISCONNECT, + + CONN_STATE_NOT_CONNECTED, + CONN_STATE_DISCONNECTED, + CONN_STATE_CLOSED, + CONN_STATE_CONNECTED, + CONN_STATE_CONNECTING, } diff --git a/public/WsClient.js b/public/WsClient.js deleted file mode 100644 index 6db8c13..0000000 --- a/public/WsClient.js +++ /dev/null @@ -1,131 +0,0 @@ -"use strict" - -import Time from '../common/Time.js' - -const CODE_CUSTOM_DISCONNECT = 4000 - -/** - * Wrapper around ws that - * - buffers 'send' until a connection is available - * - automatically tries to reconnect on close - */ -export default class WsClient { - // actual ws handle - handle = null - - // timeout for automatic reconnect - reconnectTimeout = null - - // buffer for 'send' - sendBuffer = [] - - constructor(addr, protocols) { - this.addr = addr - this.protocols = protocols - - this.onopen = () => {} - this.onclose = () => {} - this.onmessage = () => {} - - this._on = {} - this.onopen = (e) => { - this._dispatch('socket', 'open', e) - } - this.onmessage = (e) => { - this._dispatch('socket', 'message', e) - if (!!this._on['message']) { - const d = this._parseMessageData(e.data) - if (d.event) { - this._dispatch('message', d.event, d.data) - } - } - } - this.onclose = (e) => { - this._dispatch('socket', 'close', e) - } - } - - send (txt) { - if (this.handle) { - this.handle.send(txt) - } else { - this.sendBuffer.push(txt) - } - } - - connect() { - let ws = new WebSocket(this.addr, this.protocols) - ws.onopen = (e) => { - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout) - } - this.handle = ws - // should have a queue worker - while (this.sendBuffer.length > 0) { - this.handle.send(this.sendBuffer.shift()) - } - this.onopen(e) - } - ws.onmessage = (e) => { - this.onmessage(e) - } - ws.onerror = (e) => { - this.handle = null - this.reconnectTimeout = setTimeout(() => { this.connect() }, 1 * Time.SEC) - this.onclose(e) - } - ws.onclose = (e) => { - this.handle = null - if (e.code !== CODE_CUSTOM_DISCONNECT) { - this.reconnectTimeout = setTimeout(() => { this.connect() }, 1 * Time.SEC) - } - this.onclose(e) - } - } - - disconnect() { - if (this.handle) { - this.handle.close(CODE_CUSTOM_DISCONNECT) - } - } - - onSocket(tag, callback) { - this.addEventListener('socket', tag, callback) - } - - onMessage(tag, callback) { - this.addEventListener('message', tag, callback) - } - - addEventListener(type, tag, callback) { - const tags = Array.isArray(tag) ? tag : [tag] - this._on[type] = this._on[type] || {} - for (const t of tags) { - this._on[type][t] = this._on[type][t] || [] - this._on[type][t].push(callback) - } - } - - _parseMessageData(data) { - try { - const d = JSON.parse(data) - if (d.event) { - return {event: d.event, data: d.data || null} - } - } catch { - } - return {event: null, data: null} - } - - _dispatch(type, tag, ...args) { - const t = this._on[type] || {} - const callbacks = (t[tag] || []) - if (callbacks.length === 0) { - return - } - - for (const callback of callbacks) { - callback(...args) - } - } -} diff --git a/public/components/ConnectionOverlay.vue.js b/public/components/ConnectionOverlay.vue.js new file mode 100644 index 0000000..91ecb82 --- /dev/null +++ b/public/components/ConnectionOverlay.vue.js @@ -0,0 +1,34 @@ +"use strict" + +import Communication from './../Communication.js' + +export default { + name: 'connection-overlay', + template: ` +
+
+
⁉️ LOST CONNECTION ⁉️
+ Reconnect +
+
+
Connecting...
+
+
`, + emits: { + reconnect: null, + }, + props: { + connectionState: Number, + }, + computed: { + lostConnection () { + return this.connectionState === Communication.CONN_STATE_DISCONNECTED + }, + connecting () { + return this.connectionState === Communication.CONN_STATE_CONNECTING + }, + show () { + return this.lostConnection || this.connecting + }, + } +} diff --git a/public/components/HelpOverlay.vue.js b/public/components/HelpOverlay.vue.js index 79f77f2..b8c5966 100644 --- a/public/components/HelpOverlay.vue.js +++ b/public/components/HelpOverlay.vue.js @@ -6,7 +6,7 @@ export default { name: 'help-overlay', template: `
- +
diff --git a/public/components/SettingsOverlay.vue.js b/public/components/SettingsOverlay.vue.js index 1cf8962..92e6662 100644 --- a/public/components/SettingsOverlay.vue.js +++ b/public/components/SettingsOverlay.vue.js @@ -7,7 +7,7 @@ export default { name: 'settings-overlay', template: `
-
⬆️ Move up:
W//🖱️
⬇️ Move down:
S//🖱️
⬅️ Move left:
A//🖱️
+
diff --git a/public/game.js b/public/game.js index c25c54d..a4b4731 100644 --- a/public/game.js +++ b/public/game.js @@ -12,7 +12,7 @@ import fireworksController from './Fireworks.js' import Protocol from '../common/Protocol.js' import Time from '../common/Time.js' -const log = logger('game.js') +// const log = logger('game.js') export const MODE_PLAY = 'play' export const MODE_REPLAY = 'replay' @@ -174,7 +174,14 @@ function EventAdapter (canvas, window, viewport) { } } -export async function main(gameId, clientId, wsAddress, MODE, TARGET_EL, HUD) { +export async function main( + gameId, + clientId, + wsAddress, + MODE, + TARGET_EL, + HUD +) { if (typeof DEBUG === 'undefined') window.DEBUG = false const shouldDrawPlayerText = (player) => { @@ -219,30 +226,37 @@ export async function main(gameId, clientId, wsAddress, MODE, TARGET_EL, HUD) { gameStartTs: null, } - - Communication.onConnectionLost(() => { - log('connection lost ... should reload / hit reconnect button / etc.') + Communication.onConnectionStateChange((state) => { + HUD.setConnectionState(state) }) let TIME - if (MODE === MODE_PLAY) { - const game = await Communication.connect(wsAddress, gameId, clientId) - const gameObject = Util.decodeGame(game) - Game.setGame(gameObject.id, gameObject) - TIME = () => Time.timestamp() - } else if (MODE === MODE_REPLAY) { - const {game, log} = await Communication.connectReplay(wsAddress, gameId, clientId) - const gameObject = Util.decodeGame(game) - Game.setGame(gameObject.id, gameObject) - REPLAY.log = log - REPLAY.lastRealTs = Time.timestamp() - REPLAY.gameStartTs = REPLAY.log[0][REPLAY.log[0].length - 2] - REPLAY.lastGameTs = REPLAY.gameStartTs - TIME = () => REPLAY.lastGameTs - } else { - throw '[ 2020-12-22 MODE invalid, must be play|replay ]' + const connect = async () => { + if (MODE === MODE_PLAY) { + const game = await Communication.connect(wsAddress, gameId, clientId) + const gameObject = Util.decodeGame(game) + Game.setGame(gameObject.id, gameObject) + TIME = () => Time.timestamp() + } else if (MODE === MODE_REPLAY) { + // TODO: change how replay connect is done... + const {game, log} = await Communication.connectReplay(wsAddress, gameId, clientId) + const gameObject = Util.decodeGame(game) + Game.setGame(gameObject.id, gameObject) + REPLAY.log = log + REPLAY.lastRealTs = Time.timestamp() + REPLAY.gameStartTs = REPLAY.log[0][REPLAY.log[0].length - 2] + REPLAY.lastGameTs = REPLAY.gameStartTs + TIME = () => REPLAY.lastGameTs + } else { + throw '[ 2020-12-22 MODE invalid, must be play|replay ]' + } + + // rerender after (re-)connect + RERENDER = true } + await connect() + const TILE_DRAW_OFFSET = Game.getTileDrawOffset(gameId) const TILE_DRAW_SIZE = Game.getTileDrawSize(gameId) const PUZZLE_WIDTH = Game.getPuzzleWidth(gameId) @@ -653,5 +667,6 @@ export async function main(gameId, clientId, wsAddress, MODE, TARGET_EL, HUD) { name: playerName(), }, disconnect: Communication.disconnect, + connect: connect, } } diff --git a/public/style.css b/public/style.css index 294e4a6..23695a9 100644 --- a/public/style.css +++ b/public/style.css @@ -133,7 +133,7 @@ input:focus { background: transparent; } -.help { +.overlay-content { position: absolute; left: 50%; top: 50%; @@ -145,16 +145,9 @@ input:focus { z-index: 1; } -.settings { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%,-50%); - background: var(--bg-color); - padding: 5px; - border: solid 1px black; - box-shadow: 0 0 10px 0 rgba(0,0,0,.7); - z-index: 1; +.connection-lost .overlay-content { + padding: 20px; + text-align: center; } .preview { diff --git a/public/views/Game.vue.js b/public/views/Game.vue.js index 5aeaca5..8185b66 100644 --- a/public/views/Game.vue.js +++ b/public/views/Game.vue.js @@ -4,6 +4,7 @@ import Scores from './../components/Scores.vue.js' import PuzzleStatus from './../components/PuzzleStatus.vue.js' import SettingsOverlay from './../components/SettingsOverlay.vue.js' import PreviewOverlay from './../components/PreviewOverlay.vue.js' +import ConnectionOverlay from './../components/ConnectionOverlay.vue.js' import HelpOverlay from './../components/HelpOverlay.vue.js' import { main, MODE_PLAY } from './../game.js' @@ -15,6 +16,7 @@ export default { Scores, SettingsOverlay, PreviewOverlay, + ConnectionOverlay, HelpOverlay, }, template: `
@@ -22,6 +24,11 @@ export default { + + {}, onNameChange: () => {}, disconnect: () => {}, + connect: () => {}, }, } }, @@ -93,6 +103,7 @@ export default { setDuration: (v) => { this.duration = v }, setPiecesDone: (v) => { this.piecesDone = v }, setPiecesTotal: (v) => { this.piecesTotal = v }, + setConnectionState: (v) => { this.connectionState = v }, togglePreview: () => { this.toggle('preview', false) }, } ) @@ -101,6 +112,9 @@ export default { this.g.disconnect() }, methods: { + reconnect() { + this.g.connect() + }, toggle(overlay, affectsHotkeys) { if (this.overlay === null) { this.overlay = overlay