dont automatically reconnect, add dc layer with option to reconnect
This commit is contained in:
parent
9f7ac8d111
commit
6d59a713a3
8 changed files with 179 additions and 191 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
34
public/components/ConnectionOverlay.vue.js
Normal file
34
public/components/ConnectionOverlay.vue.js
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
export default {
|
||||
name: 'help-overlay',
|
||||
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 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>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default {
|
|||
name: 'settings-overlay',
|
||||
template: `
|
||||
<div class="overlay transparent" @click="$emit('bgclick')">
|
||||
<table class="settings" @click.stop="">
|
||||
<table class="overlay-content settings" @click.stop="">
|
||||
<tr>
|
||||
<td><label>Background: </label></td>
|
||||
<td><input type="color" v-model="modelValue.background" /></td>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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: `<div id="game">
|
||||
|
|
@ -22,6 +24,11 @@ export default {
|
|||
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
|
||||
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
|
||||
|
||||
<connection-overlay
|
||||
:connectionState="connectionState"
|
||||
@reconnect="reconnect"
|
||||
/>
|
||||
|
||||
<puzzle-status
|
||||
:finished="finished"
|
||||
:duration="duration"
|
||||
|
|
@ -52,6 +59,8 @@ export default {
|
|||
|
||||
overlay: null,
|
||||
|
||||
connectionState: 0,
|
||||
|
||||
g: {
|
||||
player: {
|
||||
background: '',
|
||||
|
|
@ -64,6 +73,7 @@ export default {
|
|||
onColorChange: () => {},
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue