"use strict" import { ClientEvent, EncodedGame, GameEvent, ReplayData, ServerEvent } from '../common/Types' import Util, { logger } from '../common/Util' import Protocol from './../common/Protocol' 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) let ws: WebSocket let missedMessages: ServerEvent[] = [] let changesCallback = (msg: ServerEvent) => { missedMessages.push(msg) } let missedStateChanges: Array = [] let connectionStateChangeCallback = (state: number) => { missedStateChanges.push(state) } function onServerChange(callback: (msg: ServerEvent) => void): void { changesCallback = callback for (const missedMessage of missedMessages) { changesCallback(missedMessage) } missedMessages = [] } function onConnectionStateChange(callback: (state: number) => void): void { connectionStateChangeCallback = callback for (const missedStateChange of missedStateChanges) { connectionStateChangeCallback(missedStateChange) } missedStateChanges = [] } let connectionState = CONN_STATE_NOT_CONNECTED const setConnectionState = (state: number): void => { if (connectionState !== state) { connectionState = state connectionStateChangeCallback(state) } } function send(message: ClientEvent): void { 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: number let events: Record function connect( address: string, gameId: string, clientId: string ): Promise { clientSeq = 0 events = {} setConnectionState(CONN_STATE_CONNECTING) return new Promise(resolve => { ws = new WebSocket(address, clientId + '|' + gameId) ws.onopen = () => { setConnectionState(CONN_STATE_CONNECTED) send([Protocol.EV_CLIENT_INIT]) } ws.onmessage = (e: MessageEvent) => { const msg: ServerEvent = JSON.parse(e.data) const msgType = msg[0] if (msgType === Protocol.EV_SERVER_INIT) { const game = msg[1] resolve(game) } else if (msgType === Protocol.EV_SERVER_EVENT) { const msgClientId = msg[1] const msgClientSeq = msg[2] if (msgClientId === clientId && events[msgClientSeq]) { delete events[msgClientSeq] // we have already calculated that change locally. probably return } changesCallback(msg) } else { throw `[ 2021-05-09 invalid connect msgType ${msgType} ]` } } ws.onerror = () => { setConnectionState(CONN_STATE_DISCONNECTED) throw `[ 2021-05-15 onerror ]` } ws.onclose = (e: CloseEvent) => { if (e.code === CODE_CUSTOM_DISCONNECT || e.code === CODE_GOING_AWAY) { setConnectionState(CONN_STATE_CLOSED) } else { setConnectionState(CONN_STATE_DISCONNECTED) } } }) } async function requestReplayData( gameId: string, offset: number, size: number ): Promise { const args = { gameId, offset, size } const res = await fetch(`/api/replay-data${Util.asQueryArgs(args)}`) const json: ReplayData = await res.json() return json } function disconnect(): void { if (ws) { ws.close(CODE_CUSTOM_DISCONNECT) } clientSeq = 0 events = {} } function sendClientEvent(evt: GameEvent): void { // when sending event, increase number of sent events // and add the event locally clientSeq++; events[clientSeq] = evt send([Protocol.EV_CLIENT_EVENT, clientSeq, events[clientSeq]]) } export default { connect, requestReplayData, disconnect, sendClientEvent, onServerChange, onConnectionStateChange, CODE_CUSTOM_DISCONNECT, CONN_STATE_NOT_CONNECTED, CONN_STATE_DISCONNECTED, CONN_STATE_CLOSED, CONN_STATE_CONNECTED, CONN_STATE_CONNECTING, }