restructure data to limit indirect links; breaking change

I found that the game list was wiped out (again) and I suspect that the
issue was that some client pushed an update to the game list before
receiving the current data for the top-level object. That would have
caused it to create new objects for the game and metadata lists
containing only the single newly-created game and store links to them in
the 'games' and 'meta' fields of the top-level object. This data would
not be merged because the new objects have no connection to the original
objects which should have been linked from those fields. The top-level
object would be merged, but only to the extent of updating the links.

To fix this, I moved the 'meta' object to the top level with a fixed
identifier (UUID + '/meta'). This identifier _is_ the object's identity
("soul") so even if a client initializes it with an empty object the new
fields should be merged when syncing resumes. I removed the 'games'
object altogether, since the meta object keeps track of which game IDs
are available. Individual games are assigned "souls" based on their game
IDs (UUID + ('/game/' or '/meta/') + game ID). The resulting structure
looks like:

    'UUID/meta': {
      '12345abcde': <link to 'UUID/meta/12345abcde'>,
      '3a2b5c7e1f': <link to 'UUID/meta/3a2b5c7e1f'>,
      ...
    },
    'UUID/meta/12345abcde': { ... darkName, lightName, moves, etc. ... },
    'UUID/game/12345abcde': { 'board': 'JSON' },
    'UUID/meta/3a2b5c7e1f': { ... darkName, lightName, moves, etc. ... },
    'UUID/game/3a2b5c7e1f': { 'board': 'JSON' },
    ...

Besides being more resilient, this structure should also be more
performant since there are fewer links to traverse to access the game
data. So far in my admittedly limited testing I haven't seen any of the
syncronization issues that plagued the older version.
This commit is contained in:
Jesse D. McDonald 2020-04-04 04:22:56 -05:00
parent b228f1f626
commit 7ebea1d1fc
2 changed files with 29 additions and 68 deletions

View File

@ -423,14 +423,14 @@ $(function (){
return name.slice(0, 20) + '…'; return name.slice(0, 20) + '…';
} }
const PacoSakoUUID = 'b425b812-6bdb-11ea-9414-6f946662bac3'; /* V2 release */ const PacoSakoUUID = 'b425b812-6bdb-11ea-9414-6f946662bac3'; /* V2 & V3 releases */
function putState() { function putState() {
const boardElem = $('#cb_board'); const boardElem = $('#cb_board');
const gameId = boardElem.data('gameId'); const gameId = boardElem.data('gameId');
const moves = { past: currentGame.moves, future: currentGame.redoMoves }; const moves = { past: currentGame.moves, future: currentGame.redoMoves };
boardElem.data('last_state', currentGame.moves); boardElem.data('last_state', currentGame.moves);
gun.get(PacoSakoUUID).get('games').get(gameId).put({ board: JSON.stringify(moves) }); gun.get(PacoSakoUUID + '/game/' + gameId).get('board').put(JSON.stringify(moves));
putMeta(); putMeta();
} }
@ -439,21 +439,26 @@ $(function (){
const lightName = $('#cb_light_name').val(); const lightName = $('#cb_light_name').val();
const darkName = $('#cb_dark_name').val(); const darkName = $('#cb_dark_name').val();
const turns = currentGame.countTurns(); const turns = currentGame.countTurns();
let meta = null; const winner = currentGame.winner;
const lastMove = currentGame.lastMove || {};
const lastMeta = lastMove.meta || {};
const status = !winner ? null : (lastMove.took === PS.KING) ? 'mate' : 'ended';
const meta = {
lightName,
darkName,
moves: turns,
timestamp: lastMeta.timestamp || new Date(Gun.state()).getTime(),
status,
};
const metaRef = gun.get(PacoSakoUUID + '/meta/' + gameId).put(meta);
if (lightName !== '' || darkName !== '' || turns !== 0) { if (lightName !== '' || darkName !== '' || turns !== 0) {
const winner = currentGame.winner; gun.get(PacoSakoUUID + '/meta').get(gameId).put(metaRef);
const lastMove = currentGame.lastMove || {}; } else {
const lastMeta = lastMove.meta || {}; gun.get(PacoSakoUUID + '/meta').get(gameId).put(null);
const status = !winner ? null : (lastMove.took === PS.KING) ? 'mate' : 'ended';
meta = {
lightName: lightName,
darkName: darkName,
moves: turns,
timestamp: lastMeta.timestamp || new Date(Gun.state()).getTime(),
status: status,
};
} }
gun.get(PacoSakoUUID).get('meta').put({ [gameId]: meta });
} }
function switchGameId(newId){ function switchGameId(newId){
@ -489,7 +494,7 @@ $(function (){
$('#cb_light_name').val(''); $('#cb_light_name').val('');
$('#cb_dark_name').val(''); $('#cb_dark_name').val('');
cancelGameCallback = gun.get(PacoSakoUUID).get('games').get(newId).onWithCancel(function(d) { cancelGameCallback = gun.get(PacoSakoUUID + '/game/' + newId).onWithCancel(function(d) {
if (d && d.board) { if (d && d.board) {
try { try {
const received = JSON.parse(d.board); const received = JSON.parse(d.board);
@ -526,7 +531,7 @@ $(function (){
} }
}); });
cancelMetaCallback = gun.get(PacoSakoUUID).get('meta').get(newId).onWithCancel(function(d) { cancelMetaCallback = gun.get(PacoSakoUUID + '/meta').get(newId).onWithCancel(function(d) {
d = d || {}; d = d || {};
debug('got meta', d); debug('got meta', d);
$('#cb_board').data('lightName', shortenName(String(d.lightName || 'Light'))); $('#cb_board').data('lightName', shortenName(String(d.lightName || 'Light')));
@ -885,7 +890,7 @@ $(function (){
} }
let cancellers = {}; let cancellers = {};
let cancelAll = gun.get(PacoSakoUUID).get('meta').onWithCancel(function(meta) { let cancelAll = gun.get(PacoSakoUUID + '/meta').onWithCancel(function(meta) {
for (const gameId in meta) { /* use of 'in' here is deliberate */ for (const gameId in meta) { /* use of 'in' here is deliberate */
/* 'gameId' may include extra GUN fields like '_' */ /* 'gameId' may include extra GUN fields like '_' */
if (gameId.match(/^[0-9a-f]{16}$/)) { if (gameId.match(/^[0-9a-f]{16}$/)) {
@ -956,67 +961,22 @@ $(function (){
/* Low-level commands to be run from the JS console */ /* Low-level commands to be run from the JS console */
window.Admin = { window.Admin = {
convertFromV1: function() { convertFromV2: function() {
const PacoSakoUUIDv1 = '7c38edd4-c931-49c8-9f1a-84de560815db'; gun.get(PacoSakoUUID).get('games').map().once(function(d,key){
gun.get(PacoSakoUUIDv1).get('games').map().once(function(d,key){
if (d && d.board) { if (d && d.board) {
debug('converting ' + key); debug('converting ' + key);
const game = new PS.Game(); gun.get(PacoSakoUUID + '/game/' + key).get('board').put(d.board);
let moves = [];
try {
let board = JSON.parse(d.board);
while (board.prior) {
moves.push(board.move);
board = board.prior;
}
moves.reverse();
for (const move of moves) {
if (move.to) {
game.move(move.from, move.to, move.timestamp && { timestamp: move.timestamp });
} else if (move.resign) {
game.resign();
} else {
throw { message: 'unknown move', move: move };
}
}
gun.get(PacoSakoUUID).get('games').get(key).put({ board: JSON.stringify({
past: game.moves,
future: [],
})});
} catch (err) {
debug('conversion of ' + key + ' failed', err, game, moves);
}
} }
}); });
gun.get(PacoSakoUUIDv1).get('meta').map().once(function(d,key){ gun.get(PacoSakoUUID).get('meta').map().once(function(d,key){
if (d) { if (d) {
debug('converting metadata for ' + key); debug('converting metadata for ' + key);
gun.get(PacoSakoUUID).get('meta').get(key).put({ gun.get(PacoSakoUUID + '/meta').get(key).put(d);
lightName: d.lightName,
darkName: d.darkName,
moves: d.moves,
timestamp: d.timestamp,
status: d.status,
});
} }
}); });
}, },
cleanupMissingGames: function() {
gun.get(PacoSakoUUID).get('games').once(function(games){
gun.get(PacoSakoUUID).get('meta').map().once(function(d,key){
if (!(key in games)) {
gun.get(PacoSakoUUID).get('meta').get(key).put(null);
}
});
});
},
getCurrentGame: function() { return currentGame; }, getCurrentGame: function() { return currentGame; },
getVisibleGame: function() { return visibleGame; }, getVisibleGame: function() { return visibleGame; },
setCurrentGame: setCurrentGame, setCurrentGame: setCurrentGame,

View File

@ -8,6 +8,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = { module.exports = {
mode: 'production',
entry: { entry: {
index: './index.js' index: './index.js'
}, },