From 3f173d70a2a6445ae97f2e23928b9f4f1b5c4a84 Mon Sep 17 00:00:00 2001 From: Jesse McDonald Date: Tue, 28 Apr 2020 19:25:53 -0500 Subject: [PATCH] add an interface to monitor the states of the polling connections --- js/pacosako_io.js | 137 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 119 insertions(+), 18 deletions(-) diff --git a/js/pacosako_io.js b/js/pacosako_io.js index d1c860c..585de5c 100644 --- a/js/pacosako_io.js +++ b/js/pacosako_io.js @@ -16,8 +16,12 @@ const meta = { listeners: {}, nextId: 1, currentRequest: null, + connectionState: 'initializing', }; +const stateListeners = {}; +const stateNextId = 1; + /* One-time request, no caching or polling */ function getGameState(gameId, retries) { if (arguments.length < 2) { @@ -161,6 +165,70 @@ function sendUpdate(gameId, data, retries) { }); } +function onConnectionStateChanged(gameId, callback) { + if (arguments.length < 2) { + callback = gameId; + gameId = undefined; + } + + if (gameId !== undefined) { + const original = callback; + callback = function(cbGameId, cbState) { + if (cbGameId === gameId) { + original(cbState); + } + }; + } + + const cbId = stateNextId; + stateNextId += 1; + stateListeners[cbId] = callback; + + return function clearConnectionStateChanged() { + delete stateListeners[cbId]; + }; +} + +function getConnectionState(gameId) { + if (arguments.length < 1) { + return meta.connectionState; + } else { + return games[gameId] && games[gameId].connectionState; + } +} + +function setConnectionState(game, state) { + let gameId; + + if (arguments.length < 2) { + gameId = 'meta'; + state = game; + game = meta; + } else if (typeof game === 'string') { + gameId = game; + game = gamePollState(game); + } else { + gameId = game.gameId; + } + + if (state !== game.connectionState) { + if (state !== 'initializing' && state !== 'connecting' && state !== 'polling' + && state !== 'failed' && state !== 'stopped') { + throw new TypeError(`invalid connection state: ${state}`); + } + + game.connectionState = state; + + for (const cbId in stateListeners) { + try { + stateListeners[cbId](gameId, state); + } catch(err) { + console.error('uncaught exception in callback', err); + } + } + } +} + function gamePollState(gameId) { if (gameId in games === false) { games[gameId] = { @@ -168,6 +236,7 @@ function gamePollState(gameId) { data: { gameId, modified: 0 }, listeners: [], nextId: 1, + connectionState: 'initializing', }; } @@ -177,15 +246,21 @@ function gamePollState(gameId) { function processGame(gameId, data) { const game = gamePollState(gameId); - if (data && data.modified > game.data.modified) { + if (data && (game.data.modified === 0 || data.modified > game.data.modified)) { + data.gameId = data.gameId || gameId; game.data = data; + for (const cbId in game.listeners) { try { - game.listeners[cbId](data, gameId); + game.listeners[cbId](JSON.parse(JSON.stringify(data)), gameId); } catch(err) { console.error('uncaught exception in callback', err); } } + + if (data.modified !== 0) { + processMeta({ games: [data], modified: 0 }); + } } } @@ -197,24 +272,31 @@ function startGamePoll(gameId, afterTime) { afterTime = game.data.modified; } + if (game.connectionState !== 'polling') { + if (game.connectionState !== 'failed') { + setConnectionState(game, 'connecting'); + } + } + const thisRequest = game.currentRequest = $.ajax({ dataType: 'json', contentType: 'application/json', - url: `${API_BASE}/game/${gameId}/poll/${afterTime}`, + url: `${API_BASE}/game/${gameId}${!afterTime ? '' : `/poll/${afterTime}`}`, cache: false, - timeout: LONG_TIMEOUT, + timeout: afterTime ? LONG_TIMEOUT : SHORT_TIMEOUT, }).done((data, textStatus, jqXHR) => { if (game.currentRequest === thisRequest) { game.currentRequest = null; - if (jqXHR.status == 204) { - startGamePoll(gameId, afterTime); - } else { + if (jqXHR.status !== 204) { + afterTime = data.modified; processGame(gameId, data); - startGamePoll(gameId, data.modified); } + setConnectionState(game, 'polling'); + startGamePoll(gameId, afterTime); } }).fail((jqXHR, textStatus, errorThrown) => { if (game.currentRequest === thisRequest) { + setConnectionState(game, 'failed'); setTimeout(() => { if (game.currentRequest === thisRequest) { game.currentRequest = null; @@ -235,19 +317,28 @@ function stopGamePoll(gameId) { const request = game.currentRequest; if (request !== null) { game.currentRequest = null; + setConnectionState(game, 'stopped'); request.abort(); } } function processMeta(data) { - meta.lastModified = data.modified; + if (data.modified > meta.lastModified) { + meta.lastModified = data.modified; + } for (const game of data.games) { - if (game.gameId in meta.games === false || game.modified > meta.games[game.gameId].modified) { - meta.games[game.gameId] = game; + const gameId = game.gameId; + if (gameId in meta.games === false || game.modified > meta.games[gameId].modified) { + meta.games[gameId] = game; + if (gameId in games === false || games[gameId].data.modified === 0) { + const copy = Object.assign({}, game); + copy.modified = 0; + processGame(gameId, copy); + } for (const cbId in meta.listeners) { try { - meta.listeners[cbId](game, game.gameId); + meta.listeners[cbId](JSON.parse(JSON.stringify(game)), gameId); } catch(err) { console.error('uncaught exception in callback', err); } @@ -262,24 +353,31 @@ function startMetaPoll(afterTime) { afterTime = meta.lastModified; } + if (meta.connectionState !== 'polling') { + if (meta.connectionState !== 'failed') { + setConnectionState('connecting'); + } + } + const thisRequest = meta.currentRequest = $.ajax({ dataType: 'json', contentType: 'application/json', - url: `https://jessemcdonald.info/pacosako/api/games/poll/${afterTime}`, + url: `${API_BASE}/games${!afterTime ? '' : `/poll/${afterTime}`}`, cache: false, - timeout: LONG_TIMEOUT, + timeout: afterTime ? LONG_TIMEOUT : SHORT_TIMEOUT, }).done((data, textStatus, jqXHR) => { if (meta.currentRequest === thisRequest) { meta.currentRequest = null; - if (jqXHR.status == 204) { - startMetaPoll(afterTime); - } else { + if (jqXHR.status !== 204) { + afterTime = data.modified; processMeta(data); - startMetaPoll(data.modified); } + setConnectionState('polling'); + startMetaPoll(afterTime); } }).fail((jqXHR, textStatus, errorThrown) => { if (meta.currentRequest === thisRequest) { + setConnectionState('failed'); setTimeout(() => { if (meta.currentRequest === thisRequest) { meta.currentRequest = null; @@ -295,6 +393,7 @@ function stopMetaPoll() { const request = meta.currentRequest; if (request !== null) { meta.currentRequest = null; + setConnectionState('stopped'); request.abort(); } } @@ -305,6 +404,8 @@ module.exports = { onGameUpdate, onMetaUpdate, sendUpdate, + getConnectionState, + onConnectionStateChanged, }; /* vim:set expandtab sw=3 ts=8: */