dont automatically reconnect, add dc layer with option to reconnect

This commit is contained in:
Zutatensuppe 2021-05-15 20:04:30 +02:00
parent 9f7ac8d111
commit 6d59a713a3
8 changed files with 179 additions and 191 deletions

View file

@ -1,35 +1,65 @@
"use strict" "use strict"
import WsClient from './WsClient.js' import { logger } from '../common/Util.js'
import Protocol from './../common/Protocol.js' import Protocol from './../common/Protocol.js'
/** @type WsClient */ const log = logger('Communication.js')
let conn
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 changesCallback = () => {}
let connectionLostCallback = () => {} let connectionStateChangeCallback = () => {}
// TODO: change these to something like on(EVT, cb) // TODO: change these to something like on(EVT, cb)
function onServerChange(callback) { function onServerChange(callback) {
changesCallback = callback changesCallback = callback
} }
function onConnectionLost(callback) { function onConnectionStateChange(callback) {
connectionLostCallback = callback connectionStateChangeCallback = callback
} }
function send(message) { let connectionState = CONN_STATE_NOT_CONNECTED
conn.send(JSON.stringify(message)) 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 clientSeq
let events let events
function connect(address, gameId, clientId) { function connect(address, gameId, clientId) {
clientSeq = 0 clientSeq = 0
events = {} events = {}
conn = new WsClient(address, clientId + '|' + gameId) setConnectionState(CONN_STATE_CONNECTING)
return new Promise(resolve => { return new Promise(resolve => {
conn.connect() ws = new WebSocket(address, clientId + '|' + gameId)
conn.onSocket('message', async ({ data }) => { ws.onopen = (e) => {
const msg = JSON.parse(data) setConnectionState(CONN_STATE_CONNECTED)
connectionStateChangeCallback()
send([Protocol.EV_CLIENT_INIT])
}
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
const msgType = msg[0] const msgType = msg[0]
if (msgType === Protocol.EV_SERVER_INIT) { if (msgType === Protocol.EV_SERVER_INIT) {
const game = msg[1] const game = msg[1]
@ -46,22 +76,36 @@ function connect(address, gameId, clientId) {
} else { } else {
throw `[ 2021-05-09 invalid connect msgType ${msgType} ]` throw `[ 2021-05-09 invalid connect msgType ${msgType} ]`
} }
}) }
conn.onclose(() => {
connectionLostCallback() ws.onerror = (e) => {
}) setConnectionState(CONN_STATE_DISCONNECTED)
send([Protocol.EV_CLIENT_INIT]) 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) { function connectReplay(address, gameId, clientId) {
clientSeq = 0 clientSeq = 0
events = {} events = {}
conn = new WsClient(address, clientId + '|' + gameId) setConnectionState(CONN_STATE_CONNECTING)
return new Promise(resolve => { return new Promise(resolve => {
conn.connect() ws = new WebSocket(address, clientId + '|' + gameId)
conn.onSocket('message', async ({ data }) => { ws.onopen = (e) => {
const msg = JSON.parse(data) setConnectionState(CONN_STATE_CONNECTED)
send([Protocol.EV_CLIENT_INIT_REPLAY])
}
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
const msgType = msg[0] const msgType = msg[0]
if (msgType === Protocol.EV_SERVER_INIT_REPLAY) { if (msgType === Protocol.EV_SERVER_INIT_REPLAY) {
const game = msg[1] const game = msg[1]
@ -70,14 +114,26 @@ function connectReplay(address, gameId, clientId) {
} else { } else {
throw `[ 2021-05-09 invalid connectReplay msgType ${msgType} ]` 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() { function disconnect() {
if (conn) { if (ws) {
conn.disconnect() ws.close(CODE_CUSTOM_DISCONNECT)
} }
clientSeq = 0 clientSeq = 0
events = {} events = {}
@ -97,5 +153,12 @@ export default {
disconnect, disconnect,
sendClientEvent, sendClientEvent,
onServerChange, onServerChange,
onConnectionLost, onConnectionStateChange,
CODE_CUSTOM_DISCONNECT,
CONN_STATE_NOT_CONNECTED,
CONN_STATE_DISCONNECTED,
CONN_STATE_CLOSED,
CONN_STATE_CONNECTED,
CONN_STATE_CONNECTING,
} }

View file

@ -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)
}
}
}

View file

@ -0,0 +1,34 @@
"use strict"
import Communication from './../Communication.js'
export default {
name: 'connection-overlay',
template: `
<div class="overlay connection-lost" v-if="show">
<div class="overlay-content" v-if="lostConnection">
<div> LOST CONNECTION </div>
<span class="btn" @click="$emit('reconnect')">Reconnect</span>
</div>
<div class="overlay-content" v-if="connectionState === 3">
<div>Connecting...</div>
</div>
</div>`,
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
},
}
}

View file

@ -6,7 +6,7 @@
export default { export default {
name: 'help-overlay', name: 'help-overlay',
template: `<div class="overlay transparent" @click="$emit('bgclick')"> template: `<div class="overlay transparent" @click="$emit('bgclick')">
<table class="help" @click.stop=""> <table class="overlay-content help" @click.stop="">
<tr><td> Move up:</td><td><div><kbd>W</kbd>/<kbd></kbd>/🖱</div></td></tr> <tr><td> Move up:</td><td><div><kbd>W</kbd>/<kbd></kbd>/🖱</div></td></tr>
<tr><td> Move down:</td><td><div><kbd>S</kbd>/<kbd></kbd>/🖱</div></td></tr> <tr><td> Move down:</td><td><div><kbd>S</kbd>/<kbd></kbd>/🖱</div></td></tr>
<tr><td> Move left:</td><td><div><kbd>A</kbd>/<kbd></kbd>/🖱</div></td></tr> <tr><td> Move left:</td><td><div><kbd>A</kbd>/<kbd></kbd>/🖱</div></td></tr>

View file

@ -7,7 +7,7 @@ export default {
name: 'settings-overlay', name: 'settings-overlay',
template: ` template: `
<div class="overlay transparent" @click="$emit('bgclick')"> <div class="overlay transparent" @click="$emit('bgclick')">
<table class="settings" @click.stop=""> <table class="overlay-content settings" @click.stop="">
<tr> <tr>
<td><label>Background: </label></td> <td><label>Background: </label></td>
<td><input type="color" v-model="modelValue.background" /></td> <td><input type="color" v-model="modelValue.background" /></td>

View file

@ -12,7 +12,7 @@ import fireworksController from './Fireworks.js'
import Protocol from '../common/Protocol.js' import Protocol from '../common/Protocol.js'
import Time from '../common/Time.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_PLAY = 'play'
export const MODE_REPLAY = 'replay' 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 if (typeof DEBUG === 'undefined') window.DEBUG = false
const shouldDrawPlayerText = (player) => { const shouldDrawPlayerText = (player) => {
@ -219,18 +226,19 @@ export async function main(gameId, clientId, wsAddress, MODE, TARGET_EL, HUD) {
gameStartTs: null, gameStartTs: null,
} }
Communication.onConnectionStateChange((state) => {
Communication.onConnectionLost(() => { HUD.setConnectionState(state)
log('connection lost ... should reload / hit reconnect button / etc.')
}) })
let TIME let TIME
const connect = async () => {
if (MODE === MODE_PLAY) { if (MODE === MODE_PLAY) {
const game = await Communication.connect(wsAddress, gameId, clientId) const game = await Communication.connect(wsAddress, gameId, clientId)
const gameObject = Util.decodeGame(game) const gameObject = Util.decodeGame(game)
Game.setGame(gameObject.id, gameObject) Game.setGame(gameObject.id, gameObject)
TIME = () => Time.timestamp() TIME = () => Time.timestamp()
} else if (MODE === MODE_REPLAY) { } else if (MODE === MODE_REPLAY) {
// TODO: change how replay connect is done...
const {game, log} = await Communication.connectReplay(wsAddress, gameId, clientId) const {game, log} = await Communication.connectReplay(wsAddress, gameId, clientId)
const gameObject = Util.decodeGame(game) const gameObject = Util.decodeGame(game)
Game.setGame(gameObject.id, gameObject) Game.setGame(gameObject.id, gameObject)
@ -243,6 +251,12 @@ export async function main(gameId, clientId, wsAddress, MODE, TARGET_EL, HUD) {
throw '[ 2020-12-22 MODE invalid, must be play|replay ]' 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_OFFSET = Game.getTileDrawOffset(gameId)
const TILE_DRAW_SIZE = Game.getTileDrawSize(gameId) const TILE_DRAW_SIZE = Game.getTileDrawSize(gameId)
const PUZZLE_WIDTH = Game.getPuzzleWidth(gameId) const PUZZLE_WIDTH = Game.getPuzzleWidth(gameId)
@ -653,5 +667,6 @@ export async function main(gameId, clientId, wsAddress, MODE, TARGET_EL, HUD) {
name: playerName(), name: playerName(),
}, },
disconnect: Communication.disconnect, disconnect: Communication.disconnect,
connect: connect,
} }
} }

View file

@ -133,7 +133,7 @@ input:focus {
background: transparent; background: transparent;
} }
.help { .overlay-content {
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
@ -145,16 +145,9 @@ input:focus {
z-index: 1; z-index: 1;
} }
.settings { .connection-lost .overlay-content {
position: absolute; padding: 20px;
left: 50%; text-align: center;
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;
} }
.preview { .preview {

View file

@ -4,6 +4,7 @@ import Scores from './../components/Scores.vue.js'
import PuzzleStatus from './../components/PuzzleStatus.vue.js' import PuzzleStatus from './../components/PuzzleStatus.vue.js'
import SettingsOverlay from './../components/SettingsOverlay.vue.js' import SettingsOverlay from './../components/SettingsOverlay.vue.js'
import PreviewOverlay from './../components/PreviewOverlay.vue.js' import PreviewOverlay from './../components/PreviewOverlay.vue.js'
import ConnectionOverlay from './../components/ConnectionOverlay.vue.js'
import HelpOverlay from './../components/HelpOverlay.vue.js' import HelpOverlay from './../components/HelpOverlay.vue.js'
import { main, MODE_PLAY } from './../game.js' import { main, MODE_PLAY } from './../game.js'
@ -15,6 +16,7 @@ export default {
Scores, Scores,
SettingsOverlay, SettingsOverlay,
PreviewOverlay, PreviewOverlay,
ConnectionOverlay,
HelpOverlay, HelpOverlay,
}, },
template: `<div id="game"> template: `<div id="game">
@ -22,6 +24,11 @@ export default {
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" /> <preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" /> <help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
<connection-overlay
:connectionState="connectionState"
@reconnect="reconnect"
/>
<puzzle-status <puzzle-status
:finished="finished" :finished="finished"
:duration="duration" :duration="duration"
@ -52,6 +59,8 @@ export default {
overlay: null, overlay: null,
connectionState: 0,
g: { g: {
player: { player: {
background: '', background: '',
@ -64,6 +73,7 @@ export default {
onColorChange: () => {}, onColorChange: () => {},
onNameChange: () => {}, onNameChange: () => {},
disconnect: () => {}, disconnect: () => {},
connect: () => {},
}, },
} }
}, },
@ -93,6 +103,7 @@ export default {
setDuration: (v) => { this.duration = v }, setDuration: (v) => { this.duration = v },
setPiecesDone: (v) => { this.piecesDone = v }, setPiecesDone: (v) => { this.piecesDone = v },
setPiecesTotal: (v) => { this.piecesTotal = v }, setPiecesTotal: (v) => { this.piecesTotal = v },
setConnectionState: (v) => { this.connectionState = v },
togglePreview: () => { this.toggle('preview', false) }, togglePreview: () => { this.toggle('preview', false) },
} }
) )
@ -101,6 +112,9 @@ export default {
this.g.disconnect() this.g.disconnect()
}, },
methods: { methods: {
reconnect() {
this.g.connect()
},
toggle(overlay, affectsHotkeys) { toggle(overlay, affectsHotkeys) {
if (this.overlay === null) { if (this.overlay === null) {
this.overlay = overlay this.overlay = overlay