Source: server-1.js

/**
 * @fileoverview This client of the main module can't update the state in the board object directly, so has to use the localStorage.game_mq for that
 * @author Robert Laing
 * @module server
 */

/**
  * @namespace module:server.jsonIn
  * @property {string} action - "player_move" or "game_over"
  * @property {string} game - prefix for localStorage and server database entries
  * @property {integer} move_count - used to id messages to the server so turns aren't inadvertantly skipped
  * @property {Array} [state] - player_move to tell server the current state.
  * @property {Object} [move] - player_move selected by human player, eg {"red": ["move", "a", 1, "b", 2]} or {"red": "noop"}
  * @property {Array} [init] - starting state stored in localStorage to update server utilities
  * @property {Object[]} [moves] - array of moves to provide history of a game played to completion.
  * @property {Object} [goal] - didn't simply call this utility to distinguish from non terminal state values.
 */

/**
  * @namespace module:server.jsonOut
  * @property {string} game - prefix for localStorage and server database entries
  * @property {Array} state - A nested array of bases describing the current state of the board and who's to play
  * @property {Object} last_move - {"red": ["move", "a", 1, "b", 2], "blue": "noop"}
  * @property {boolean} terminal - true if game over
  * @property {Array} [legals] - Possible moves for the human player, used to help explain rules
  * @property {Object} [utility] - final score if game over
  * @property {integer} move_count - used to id messages to the server so turns aren't inadvertantly skipped
 */

let jsonOut;
let mq = [];
let rpcQueue = [];
let busy = false;

/**
 * Exporting jsonOut directly instead of via a getter function caused the client to only see undefined without updates.
 */
function jsonOutGet() {
  return jsonOut;
}

function send(message) {
  mq.push(message);
}

function init() {
  jsonOut = undefined;
  mq = [];
  rpcQueue = [];
  busy = false;
}

function update() {
  const action = mq.shift();
  if (action !== undefined && Object.hasOwn(action[1], "game")) {
    switch (action[0]) {
      case "rpc":
        if (busy) {
          rpcQueue.push(action[1]);
        } else {
          rpc(action[1]);
        }
        break;
    default:
      window.container.textContent = `Unknown state action ${action[1]}`
    }
  }
  if (rpcQueue.length > 0 && !busy) {
    const jsonIn = rpcQueue.shift();
    rpc(jsonIn);
  }
}

/**
 * Private procedure sending JSON to the server and processing response
 * @function module:state~rpc
 * @param {object} jsonIn
 */

async function rpc(jsonIn) {
  try {
    busy = true;
    const response1 = await fetch("/move", {
      method: "POST",
      headers: {"Content-Type": "application/json"},
      body: JSON.stringify(jsonIn)
    });
    if (!response1.ok) {
      throw new Error(`Response status: ${response1.status}`);
    }
    const response2 = response1.clone();
    try {
      jsonOut = await response1.json();
      if (jsonOut.state !== null) {  // hack to avoid null states getting returned
        const moves = JSON.parse(localStorage[`${jsonOut.game}_moves`]);
        moves.push(jsonOut.last_move);
        localStorage[`${jsonOut.game}_moves`] = JSON.stringify(moves);
        localStorage[`${jsonOut.game}_state`] = JSON.stringify(jsonOut.state);
        localStorage[`${jsonOut.game}_legals`] = JSON.stringify(jsonOut.legals);
        // don't wait to request AI player to start thinking when it's its turn without, ie no bothering about animationBusy
        if (jsonOut.last_move) {
          jsonOut.move_count = jsonIn.move_count + 1;
        }
        const player = localStorage[`${jsonOut.game}_player`];
        if (jsonOut.legals && JSON.stringify(JSON.parse(`[{"${player}": "noop"}]`)) === JSON.stringify(jsonOut.legals)) {
          mq.push(["rpc", {
            "action": "player_move",
            "game": jsonOut.game,
            "state": jsonOut.state,
            "move": JSON.parse(`{"${player}": "noop"}`),
            "move_count": jsonOut.move_count
          }]);
        }
        // request server to update game tree if terminal without bothering about animationBusy
        if (jsonOut.terminal) {
          mq.push(["rpc", {
            "action": "game_over",
            "game": jsonOut.game,
            "init": JSON.parse(localStorage[`${jsonOut.game}_init`]),
            "moves": JSON.parse(localStorage[`${jsonOut.game}_moves`]),
            "goal": jsonOut.utility
          }]);
        } 
      } else {
        rpc(jsonIn);  // potential infinite loop here, should include limit
      }
    } catch (error) {
      const textOut = await response2.text();
      if (textOut !== `{"response": "ok"}`) {
        window.container.textContent = `json_error ${textOut} ${JSON.stringify(jsonIn)}`;
      }
    }
  } catch (error) {
      window.container.textContent = `rpc error ${error.name} ${error.message} ${JSON.stringify(jsonIn)}`;
      rpc(jsonIn);
  } finally {
    busy = false;
  }
}

export default Object.freeze({jsonOutGet, send, init, update});