Some fixes. TODO: game.mode must be analyze when score is not '*' ==> simplify Board...
[vchess.git] / client / src / utils / storage.js
index 937d8c7..2e7a84f 100644 (file)
-// TODO: general methods to access/retrieve from storage, to be generalized
-// https://developer.mozilla.org/fr/docs/Web/API/API_IndexedDB
-// https://dexie.org/
+import { extractTime } from "@/utils/timeControl";
+
+// TODO: show game structure
+//const newItem = [
+//     { gameId: "", players: [], timeControl: "", clocks: [] }
+//];
+
+function dbOperation(callback)
+{
+  let db = null;
+  let DBOpenRequest = window.indexedDB.open("vchess", 4);
+
+  DBOpenRequest.onerror = function(event) {
+    alert("Database error: " + event.target.errorCode);
+  };
+
+  DBOpenRequest.onsuccess = function(event) {
+    db = DBOpenRequest.result;
+    callback(db);
+    db.close();
+  };
+
+  DBOpenRequest.onupgradeneeded = function(event) {
+    let db = event.target.result;
+    db.onerror = function(event) {
+      alert("Error while loading database: " + event.target.errorCode);
+    };
+    // Create objectStore for vchess->games
+    db.createObjectStore("games", { keyPath: "gameId" });
+  }
+}
+
+// Optional callback to get error status
+function addGame(game, callback)
+{
+  dbOperation((db) => {
+    let transaction = db.transaction(["games"], "readwrite");
+    if (callback)
+    {
+      transaction.oncomplete = function() {
+        callback({}); //everything's fine
+      }
+      transaction.onerror = function() {
+        callback({errmsg: "addGame failed: " + transaction.error});
+      };
+    }
+    let objectStore = transaction.objectStore("games");
+    objectStore.add(game);
+  });
+}
+
+// Clear current live game from localStorage
+function clear() {
+  localStorage.removeItem("gameInfo");
+  localStorage.removeItem("gameState");
+}
+
+// Current live game:
+function getCurrent()
+{
+  return Object.assign({},
+    JSON.parse(localStorage.getItem("gameInfo")),
+    JSON.parse(localStorage.getItem("gameState")));
+}
+
+// Only called internally after a score update
+function transferToDb()
+{
+  addGame(getCurrent(), (err) => {
+    if (!!err.errmsg)
+      return err;
+    clear();
+  });
+}
 
 export const GameStorage =
 {
-  init: function(myid, oppid, gameId, variant, mycolor, fenStart)
+  // localStorage:
+  init: function(o)
   {
-    localStorage.setItem("myid", myid);
-    localStorage.setItem("gameId", gameId);
-    localStorage.setItem("vname", variant);
-    localStorage.setItem("mycolor", mycolor);
-    localStorage.setItem("fenStart", fenStart);
-    localStorage.setItem("moves", []);
+    // Extract times (in [milli]seconds), set clocks, store in localStorage
+    const tc = extractTime(o.timeControl);
+
+    // game infos: constant
+    const gameInfo =
+    {
+      gameId: o.gameId,
+      vname: o.vname,
+      fenStart: o.fenStart,
+      players: o.players,
+      timeControl: o.timeControl,
+      increment: tc.increment,
+      mode: "live", //function for live games only
+    };
+
+    // game state: will be updated
+    const gameState =
+    {
+      fen: o.fenStart,
+      moves: [],
+      clocks: [...Array(o.players.length)].fill(tc.mainTime),
+      initime: (o.initime ? Date.now() : undefined),
+      score: "*",
+    };
+
+    localStorage.setItem("gameInfo", JSON.stringify(gameInfo));
+    localStorage.setItem("gameState", JSON.stringify(gameState));
+  },
+
+  getInitime: function()
+  {
+    const gameState = JSON.parse(localStorage.getItem("gameState"));
+    return gameState.initime;
   },
 
+  // localStorage:
   // TODO: also option to takeback a move ?
-  update: function(move)
+  // NOTE: for live games only (all on server for corr)
+  update: function(o) //colorIdx, move, fen, addTime, initime, score
   {
-    let moves = JSON.parse(localStorage.getItem("moves"));
-    moves.push(move);
-    localStorage.setItem("moves", JSON.stringify(moves));
+    let gameState = JSON.parse(localStorage.getItem("gameState"));
+    if (!!o.move)
+    {
+      gameState.moves.push(o.move);
+      gameState.fen = o.fen;
+      if (!!o.addTime) //NaN if first move in game
+        gameState.clocks[o.colorIdx] += o.addTime;
+    }
+    if (!!o.initime) //just a flag (true)
+      gameState.initime = Date.now();
+    if (!!o.score)
+      gameState.score = o.score;
+    localStorage.setItem("gameState", JSON.stringify(gameState));
+    if (!!o.score && o.score != "*")
+      transferToDb(); //game is over
   },
 
-  // "computer mode" clearing is done through the menu
-  clear: function()
+  // indexedDB:
+  // Since DB requests are asynchronous, require a callback using the result
+  // TODO: option for remote retrieval (third arg, or just "gameRef")
+  getLocal: function(gameId, callback)
   {
-    // TODO: refresh, and implement "transfert" function (to indexedDB)
-    delete localStorage["myid"];
-    delete localStorage["oppid"];
-    delete localStorage["gameId"];
-    delete localStorage["variant"];
-    delete localStorage["mycolor"];
-    delete localStorage["fenStart"];
-    delete localStorage["moves"];
+    dbOperation((db) => {
+      let objectStore = db.transaction('games').objectStore('games');
+      if (!gameId) //retrieve all
+      {
+        let games = [];
+        objectStore.openCursor().onsuccess = function(event) {
+          let cursor = event.target.result;
+          // if there is still another cursor to go, keep running this code
+          if (cursor)
+          {
+            games.push(cursor.value);
+            cursor.continue();
+          }
+          else
+            callback(games);
+        }
+      }
+      else //just one game
+      {
+        objectStore.get(gameId).onsuccess = function(event) {
+          callback(event.target.result);
+        }
+      }
+    });
   },
 
-  get: function(gameRef)
+  // Delete a game in indexedDB
+  remove: function(gameId, callback)
+  {
+    dbOperation((db) => {
+      let transaction = db.transaction(["games"], "readwrite");
+      if (callback)
+      {
+        transaction.oncomplete = function() {
+          callback({}); //everything's fine
+        }
+        transaction.onerror = function() {
+          callback({errmsg: "game removal failed: " + transaction.error});
+        };
+      }
+      transaction.objectStore("games").delete(gameId);
+    });
+  },
+
+  // Retrieve any live game from its identifiers (remote or not, running or not)
+  // NOTE: need callback because result might be obtained asynchronously
+  get: function(gameRef, callback)
   {
     const gid = gameRef.id;
     const rid = gameRef.rid; //may be blank
-    let game = {};
-    if (localStorage.getItem("gameId") === gid)
+    if (!!rid)
     {
-      // Retrieve running game from localStorage
-      game.score = localStorage.getItem("score");
-      game.mycolor = localStorage.getItem("mycolor");
-      game.fenStart = localStorage.getItem("fenStart");
-      game.fen = localStorage.getItem("fen");
-      game.moves = JSON.parse(localStorage.getItem("moves"));
-      game.players = JSON.parse(localStorage.getItem("players"));
-      game.started = JSON.parse(localStorage.getItem("started"));
-      game.clocks = JSON.parse(localStorage.getItem("clocks"));
-      game.timeControl = localStorage.getItem("timeControl");
-      game.increment = localStorage.getItem("increment");
-      game.mode = "live";
+      // TODO: send request to server which forward to user sid == rid,
+      // need to listen to "remote game" event in main hall ?
+      return callback({}); //means "the game will arrive later" (TODO...)
     }
-    else
+
+    const gameInfoStr = localStorage.getItem("gameInfo");
+    if (gameInfoStr)
     {
-      // Find the game in indexedDB, on server or remotely: TODO
+      const gameInfo = JSON.parse(gameInfoStr);
+      if (gameInfo.gameId == gid)
+      {
+        const gameState = JSON.parse(localStorage.getItem("gameState"));
+        return callback(Object.assign({}, gameInfo, gameState));
+      }
     }
-    return game;
+
+    // Game is local and not running
+    GameStorage.getLocal(gid, callback);
   },
 };