Source: management-system/src/backend/shared-electron-server/data/machines.js

import communicationEx from '@proceed/distribution';
const { communication } = communicationEx;
import store from './store.js';
import eventHandler from '../../../frontend/backend-api/event-system/EventHandler.js';
import ExecutionQueue from '../../../shared-frontend-backend/helpers/execution-queue.js';
import {
  checkAvailability,
  getCompleteMachineInformation,
} from '../network/machines/machineInfoRequests.js';
import helpers from '../../../shared-frontend-backend/helpers/javascriptHelpers.js';
const { deepEquals, isSubset } = helpers;
import logger from '../logging.js';

const queue = new ExecutionQueue();

// merge between added and discovered machines
let known = {};

/**
 * Goes through the known machines searching for a machine that matches the given machine in some way
 *
 * @param {Object} machine the machine we want to find a matching machine for
 * @returns {String|undefined} the id of the matching machine in known (or undefined for no matching)
 */
function findMatchingMachineId(machine) {
  const matching = Object.values(known).find((knownMachine) => {
    const idMatch = knownMachine.id === machine.id;

    // user submitted machine matches the network information
    const networkMatch =
      knownMachine.ip === machine.ip &&
      knownMachine.port === machine.port &&
      (!knownMachine.hostname || !machine.hostname);

    // user submitted machine matches hostname
    const hostnameMatch =
      knownMachine.hostname === machine.hostname && (!knownMachine.ip || !machine.ip);

    // machine matches identifying information except id
    const sameInfoDiffId =
      machine.hostname === knownMachine.hostname &&
      machine.ip === knownMachine.ip &&
      machine.port === knownMachine.port;

    // check if machines match in some way
    return idMatch || networkMatch || hostnameMatch || sameInfoDiffId;
  });

  if (matching) {
    return matching.id;
  }
}

/**
 * Handles machines that are added by the user
 *
 * @param {Object} machine contains user defined information about the machine to add
 */
export async function addMachine(addedMachine, fromDiscovery = false) {
  // add flags and info based on where the machine is coming from (discovery/user)
  if (fromDiscovery) {
    addedMachine = { ...addedMachine, discovered: true, status: 'CONNECTED' };
  } else {
    addedMachine = { ...addedMachine, saved: true, status: 'DISCONNECTED' };
  }

  if (!addedMachine.machine) {
    addedMachine.machine = {};
  }

  const { currentlyConnectedEnvironments, hostname } = addedMachine;
  delete addedMachine.currentlyConnectedEnvironments;

  if (currentlyConnectedEnvironments) {
    addedMachine.machine.currentlyConnectedEnvironments = currentlyConnectedEnvironments;
  }
  if (hostname) {
    addedMachine.machine.hostname = hostname;
  }

  await queue.enqueue(async () => {
    // critical area making sure that the list of known machines is not changed while in it

    // check if there is some machine that would match this one
    const matching = findMatchingMachineId(addedMachine);
    let commitedMachine;

    if (matching) {
      let merge;
      if (fromDiscovery) {
        // we expect discovered machines to have preferred/correct information
        merge = { ...known[matching], ...addedMachine };
      } else {
        // we expect user submitted machines to not have "more correct" information than the already known ones
        merge = { ...addedMachine, ...known[matching] };
        merge.optionalName = addedMachine.optionalName;
        merge.saved = true;
      }
      updateMachine(matching, merge);
      commitedMachine = merge;
    } else {
      if (!fromDiscovery) {
        // add user submitted machine to store
        store.add('machines', removeExcessiveInformation(addedMachine));
      }
      known[addedMachine.id] = addedMachine;
      eventHandler.dispatch('machineAdded', { machine: addedMachine });
      commitedMachine = addedMachine;
    }

    // make initial pull for information from machine
    getMachineInformationAndUpdate({ ...commitedMachine });
  });
}

/**
 * Request information from a machine and update the local information on response
 *
 * @param {Object} machine current information about the machine
 */
async function getMachineInformationAndUpdate(machine) {
  try {
    const running = await checkAvailability(machine);

    if (running) {
      machine.status = 'CONNECTED';
      // save id that might be overwritten
      const oldId = machine.id;

      // request up to date info from machine
      machine = await getCompleteMachineInformation(machine);

      updateMachine(oldId, machine);
    } else {
      machine.status = 'DISCONNECTED';
    }
  } catch (err) {
    const { optionalName, hostname, id } = machine;
    logger.debug(`Unable to connect to machine ${optionalName || hostname || id}.`);
    machine.status = 'DISCONNECTED';
  }
}

/**
 * Handles machines that are removed by the user
 *
 * @param {String} machineId id of the machine to remove
 */
export async function removeMachine(machineId) {
  await updateMachine(machineId, { saved: false });
}

export async function updateMachine(machineId, updatedInfo) {
  await queue.enqueue(async () => {
    if (known[machineId]) {
      // we don't allow changing the hostname, ip, port or id of a connected machine
      if (known[machineId].status === 'CONNECTED') {
        delete updatedInfo.ip;
        delete updatedInfo.port;
        delete updatedInfo.hostname;
        delete updatedInfo.id;
      }

      // check if we are actually getting new info
      if (!isSubset(known[machineId], updatedInfo)) {
        let merge = { ...known[machineId], ...updatedInfo };

        // update in store if necessary
        updateStoreIfInfoChanged(known[machineId], merge);

        if (!merge.saved && !merge.discovered) {
          // machine is neither in discovery nor in the store => remove
          delete known[machineId];
          eventHandler.dispatch('machineRemoved', { machineId });
        } else {
          // check if id changed so we can update entry in known
          if (updatedInfo.id && updatedInfo.id !== machineId) {
            delete known[machineId];

            // if we change id we might actually allready know the machine with the new id
            // Scenario: discovery adds machine with id 'a' and local network address, user adds same machine but with id 'b' and its open address
            // first request to the machine adds new information and updates with id 'a' => we need to consider the existing information while updating
            if (known[updatedInfo.id]) {
              // make clients remove the machine with the old id and update the one with the correct id
              eventHandler.dispatch('machineRemoved', { machineId });
              merge = { ...known[updatedInfo.id], ...merge };
              machineId = updatedInfo.id;
            }

            known[updatedInfo.id] = merge;
          } else {
            // just update the information and emit change event
            known[machineId] = merge;
          }
          eventHandler.dispatch('machineUpdated', { oldId: machineId, updatedInfo: merge });
        }
      }
    }
  });
}

/**
 * Checks if a change of machine information should trigger a mutation of the backend machine store
 *
 * @param {Object} oldInfo contains the previously known information about a machine
 * @param {Object} newInfo contains the newly available information about a machine
 */
function updateStoreIfInfoChanged(oldInfo, newInfo) {
  // check if we have to make changes in the store
  if (!oldInfo.saved) {
    // not previously stored
    if (newInfo.saved) {
      // should be stored now
      store.add('machines', removeExcessiveInformation(newInfo));
    }
  } else {
    // was already stored
    if (newInfo.saved) {
      // check if store needs to be updated
      const minified = removeExcessiveInformation(newInfo);
      if (!deepEquals(minified, removeExcessiveInformation(oldInfo))) {
        store.update('machines', oldInfo.id, minified);
      }
    } else {
      // remove from store
      store.remove('machines', oldInfo.id);
    }
  }
}

communication.onMachineDiscovery(async (added) => {
  await addMachine(added, true);
});

communication.onMachineUnpublishing(async (removed) => {
  await updateMachine(removed.id, { discovered: false, status: 'DISCONNECTED', online: false });
});

/**
 * Returns an object that contains only the most vital information about an object that we would like to store
 *
 * which is: id, ip, port, hostname, name, optionalName
 *
 * @param {Object} machineInfo a machine information object
 * @returns {Object} - an object that contains a subset of the original object information
 */
function removeExcessiveInformation({ id, ip, port, hostname, optionalName }) {
  return { id, ip, port, hostname, optionalName };
}

export function getMachines() {
  return Object.values(known);
}

export function clearMachines() {
  known = {};
}

function init() {
  queue.enqueue(async () => {
    // wait a second for every system to be started
    // we try to call the stored machines to see if they are online so we need the engine to be completely up and running
    await new Promise((resolve) =>
      setTimeout(() => {
        resolve();
      }, 1000)
    );
    const machines = store.get('machines');
    machines.forEach((machine) => {
      addMachine(machine);
    });
  });
}

// initialize with the machines that are in the store
init();