paco_sako/js/pacosako_io.js

311 lines
8.4 KiB
JavaScript

'use strict';
const API_BASE = `https://jessemcdonald.info/pacosako/api`;
const SHORT_TIMEOUT = 5000/*ms*/;
const LONG_TIMEOUT = 90000/*ms*/;
const RETRIES = 3;
const RETRY_DELAY = 1000/*ms*/;
const games = {};
const meta = {
games: {},
lastModified: 0,
listeners: {},
nextId: 1,
currentRequest: null,
};
/* One-time request, no caching or polling */
function getGameState(gameId, retries) {
if (arguments.length < 2) {
retries = RETRIES;
}
return new Promise((resolve, reject) => {
$.ajax({
dataType: 'json',
contentType: 'application/json',
url: `${API_BASE}/game/${gameId}`,
cache: false,
timeout: SHORT_TIMEOUT,
}).done((data, textStatus, jqXHR) => {
resolve(data);
}).fail((jqXHR, textStatus, errorThrown) => {
if ((!jqXHR.status || jqXHR.status < 400 || jqXHR.status > 499) && retries > 0) {
setTimeout(() => {
getGameState(gameId, retries - 1).then(resolve, reject);
}, RETRY_DELAY);
} else {
reject({ status: jqXHR.status, textStatus, errorThrown });
}
});
});
}
/* One-time request, no caching or polling */
function getGameMeta(gameId, retries) {
if (arguments.length < 2) {
retries = RETRIES;
}
return new Promise((resolve, reject) => {
$.ajax({
dataType: 'json',
contentType: 'application/json',
url: `${API_BASE}/meta/${gameId}`,
cache: false,
timeout: SHORT_TIMEOUT,
}).done((data, textStatus, jqXHR) => {
resolve(data);
}).fail((jqXHR, textStatus, errorThrown) => {
if ((!jqXHR.status || jqXHR.status < 400 || jqXHR.status > 499) && retries > 0) {
setTimeout(() => {
getGameState(gameId, retries - 1).then(resolve, reject);
}, RETRY_DELAY);
} else {
reject({ status: jqXHR.status, textStatus, errorThrown });
}
});
});
}
function onGameUpdate(gameId, callback) {
const game = gamePollState(gameId);
if (Object.keys(game.listeners).length === 0) {
startGamePoll(gameId, game.data.modified);
}
const cbId = game.nextId;
game.nextId += 1;
game.listeners[cbId] = callback;
/* delay the initial run of the callback to the microtask queue */
(Promise.resolve()).then(() => {
if (cbId in game.listeners) {
try {
callback(game.data, gameId);
} catch(err) {
console.error('uncaught exception in callback', err);
}
}
});
return function clearGameUpdate(keepPolling) {
if (cbId in game.listeners) {
delete game.listeners[cbId];
if (!keepPolling && Object.keys(game.listeners).length === 0) {
stopGamePoll(gameId);
}
}
}
}
function onMetaUpdate(callback) {
if (Object.keys(meta.listeners).length === 0) {
startMetaPoll(meta.lastModified);
}
const cbId = meta.nextId;
meta.nextId += 1;
meta.listeners[cbId] = callback;
/* delay the initial run of the callback to the microtask queue */
(Promise.resolve()).then(() => {
if (cbId in meta.listeners) {
for (const gameId in meta.games) {
try {
callback(meta.games[gameId], gameId);
} catch(err) {
console.error('uncaught exception in callback', err);
}
}
}
});
return function clearMetaUpdate(keepPolling) {
if (cbId in meta.listeners) {
delete meta.listeners[cbId];
if (!keepPolling && Object.keys(meta.listeners).length === 0) {
stopMetaPoll();
}
}
}
}
function sendUpdate(gameId, data, retries) {
if (arguments.length < 3) {
retries = RETRIES;
}
return new Promise((resolve, reject) => {
$.ajax({
dataType: 'json',
contentType: 'application/json',
url: `${API_BASE}/game/${gameId}`,
method: 'POST',
cache: false,
data: JSON.stringify(data),
timeout: SHORT_TIMEOUT,
}).done((responseData, textStatus, jqXHR) => {
resolve({ status: jqXHR.status, data: responseData });
}).fail((jqXHR, textStatus, errorThrown) => {
if ((!jqXHR.status || jqXHR.status < 400 || jqXHR.status > 499) && retries > 0) {
setTimeout(() => {
getGameState(gameId, retries - 1).then(resolve, reject);
}, RETRY_DELAY);
} else {
reject({ status: jqXHR.status, textStatus, errorThrown });
}
});
});
}
function gamePollState(gameId) {
if (gameId in games === false) {
games[gameId] = {
currentRequest: null,
data: { gameId, modified: 0 },
listeners: [],
nextId: 1,
};
}
return games[gameId];
}
function processGame(gameId, data) {
const game = gamePollState(gameId);
if (data && data.modified > game.data.modified) {
game.data = data;
for (const cbId in game.listeners) {
try {
game.listeners[cbId](data, gameId);
} catch(err) {
console.error('uncaught exception in callback', err);
}
}
}
}
function startGamePoll(gameId, afterTime) {
const game = gamePollState(gameId);
if (game.currentRequest === null) {
if (afterTime === undefined) {
afterTime = game.data.modified;
}
const thisRequest = game.currentRequest = $.ajax({
dataType: 'json',
contentType: 'application/json',
url: `${API_BASE}/game/${gameId}/poll/${afterTime}`,
cache: false,
timeout: LONG_TIMEOUT,
}).done((data, textStatus, jqXHR) => {
if (game.currentRequest === thisRequest) {
game.currentRequest = null;
if (jqXHR.status == 204) {
startGamePoll(gameId, afterTime);
} else {
processGame(gameId, data);
startGamePoll(gameId, data.modified);
}
}
}).fail((jqXHR, textStatus, errorThrown) => {
if (game.currentRequest === thisRequest) {
setTimeout(() => {
if (game.currentRequest === thisRequest) {
game.currentRequest = null;
startGamePoll(gameId, 0);
}
}, RETRY_DELAY);
}
});
}
}
function stopGamePoll(gameId) {
if (gameId in games === false) {
return;
}
const game = games[gameId];
const request = game.currentRequest;
if (request !== null) {
game.currentRequest = null;
request.abort();
}
}
function processMeta(data) {
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;
for (const cbId in meta.listeners) {
try {
meta.listeners[cbId](game, game.gameId);
} catch(err) {
console.error('uncaught exception in callback', err);
}
}
}
}
}
function startMetaPoll(afterTime) {
if (meta.currentRequest === null) {
if (afterTime === undefined) {
afterTime = meta.lastModified;
}
const thisRequest = meta.currentRequest = $.ajax({
dataType: 'json',
contentType: 'application/json',
url: `https://jessemcdonald.info/pacosako/api/games/poll/${afterTime}`,
cache: false,
timeout: LONG_TIMEOUT,
}).done((data, textStatus, jqXHR) => {
if (meta.currentRequest === thisRequest) {
meta.currentRequest = null;
if (jqXHR.status == 204) {
startMetaPoll(afterTime);
} else {
processMeta(data);
startMetaPoll(data.modified);
}
}
}).fail((jqXHR, textStatus, errorThrown) => {
if (meta.currentRequest === thisRequest) {
setTimeout(() => {
if (meta.currentRequest === thisRequest) {
meta.currentRequest = null;
startMetaPoll(0);
}
}, RETRY_DELAY);
}
});
}
}
function stopMetaPoll() {
const request = meta.currentRequest;
if (request !== null) {
meta.currentRequest = null;
request.abort();
}
}
module.exports = {
getGameState,
getGameMeta,
onGameUpdate,
onMetaUpdate,
sendUpdate,
};
/* vim:set expandtab sw=3 ts=8: */