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) + '…';
}
const PacoSakoUUID = 'b425b812-6bdb-11ea-9414-6f946662bac3'; /* V2 release */
const PacoSakoUUID = 'b425b812-6bdb-11ea-9414-6f946662bac3'; /* V2 & V3 releases */
function putState() {
const boardElem = $('#cb_board');
const gameId = boardElem.data('gameId');
const moves = { past: currentGame.moves, future: currentGame.redoMoves };
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();
}
@ -439,21 +439,26 @@ $(function (){
const lightName = $('#cb_light_name').val();
const darkName = $('#cb_dark_name').val();
const turns = currentGame.countTurns();
let meta = null;
if (lightName !== '' || darkName !== '' || turns !== 0) {
const winner = currentGame.winner;
const lastMove = currentGame.lastMove || {};
const lastMeta = lastMove.meta || {};
const status = !winner ? null : (lastMove.took === PS.KING) ? 'mate' : 'ended';
meta = {
lightName: lightName,
darkName: darkName,
const meta = {
lightName,
darkName,
moves: turns,
timestamp: lastMeta.timestamp || new Date(Gun.state()).getTime(),
status: status,
status,
};
const metaRef = gun.get(PacoSakoUUID + '/meta/' + gameId).put(meta);
if (lightName !== '' || darkName !== '' || turns !== 0) {
gun.get(PacoSakoUUID + '/meta').get(gameId).put(metaRef);
} else {
gun.get(PacoSakoUUID + '/meta').get(gameId).put(null);
}
gun.get(PacoSakoUUID).get('meta').put({ [gameId]: meta });
}
function switchGameId(newId){
@ -489,7 +494,7 @@ $(function (){
$('#cb_light_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) {
try {
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 || {};
debug('got meta', d);
$('#cb_board').data('lightName', shortenName(String(d.lightName || 'Light')));
@ -885,7 +890,7 @@ $(function (){
}
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 */
/* 'gameId' may include extra GUN fields like '_' */
if (gameId.match(/^[0-9a-f]{16}$/)) {
@ -956,67 +961,22 @@ $(function (){
/* Low-level commands to be run from the JS console */
window.Admin = {
convertFromV1: function() {
const PacoSakoUUIDv1 = '7c38edd4-c931-49c8-9f1a-84de560815db';
gun.get(PacoSakoUUIDv1).get('games').map().once(function(d,key){
convertFromV2: function() {
gun.get(PacoSakoUUID).get('games').map().once(function(d,key){
if (d && d.board) {
debug('converting ' + key);
const game = new PS.Game();
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(PacoSakoUUID + '/game/' + key).get('board').put(d.board);
}
});
gun.get(PacoSakoUUIDv1).get('meta').map().once(function(d,key){
gun.get(PacoSakoUUID).get('meta').map().once(function(d,key){
if (d) {
debug('converting metadata for ' + key);
gun.get(PacoSakoUUID).get('meta').get(key).put({
lightName: d.lightName,
darkName: d.darkName,
moves: d.moves,
timestamp: d.timestamp,
status: d.status,
});
gun.get(PacoSakoUUID + '/meta').get(key).put(d);
}
});
},
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; },
getVisibleGame: function() { return visibleGame; },
setCurrentGame: setCurrentGame,

View File

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