Source: management-system/src/frontend/backend-api/ms-api-server/process.js

import { listen, request, io, connectionId } from './socket.js';
import restRequest from './rest.js';
import eventHandler from '@/frontend/backend-api/event-system/EventHandler.js';
import { isAuthenticated, isAuthRequired } from '@/frontend/backend-api/index.js';
//import store from '@/frontend/main.js';
import * as browserStorage from './browser-storage.js';

function toInternalFormat(processInformation) {
  const internalFormat = { ...processInformation };
  internalFormat.id = processInformation.definitionId;
  internalFormat.name = processInformation.definitionName;
  delete internalFormat.definitionId;
  delete internalFormat.definitionName;
  return internalFormat;
}

async function getProcessesViaWebsocket() {
  const [backendProcesses] = await request('data_get_processes');
  return backendProcesses;
}

async function getProcessesViaRest() {
  const processes = await restRequest('process', 'noBpmn=true');
  return processes.length ? processes.map((process) => toInternalFormat(process)) : [];
}

/**
 * Pulls process from the backend writes it to the browser storage and returns it
 *
 * @param {String} id the definitions id of the process
 * @param {Boolean} [isUpdate] if the process is known but supposed to be synchronized with the server
 * @returns {Object} The process with the given id
 */
async function pullProcess(id, isUpdate) {
  let process = { ...toInternalFormat(await restRequest(`process/${id}`)), shared: true };

  await observeProcess(id);
  if (isUpdate) {
    process = browserStorage.updateProcessMetaData(id, process);
  } else {
    process = browserStorage.addProcess(process);
  }

  // get user task data
  const userTasks = await restRequest(`process/${id}/user-tasks`, 'withHtml=true');

  browserStorage.saveUserTasksHtml(id, userTasks);
  return process;
}

async function getProcesses(viaWebsocket = false) {
  let backendProcesses = [];

  if ((isAuthRequired && isAuthenticated) || !isAuthRequired) {
    if (viaWebsocket) {
      backendProcesses = await getProcessesViaWebsocket();
    } else {
      backendProcesses = await getProcessesViaRest();
    }

    await Promise.all(
      backendProcesses.map(async (process) => {
        await observeProcess(process.id);
      })
    );

    backendProcesses = backendProcesses.map((p) => ({ ...p, shared: true }));
  }

  // get locally stored processes and merge with the ones from the server
  let localProcesses = Object.values(browserStorage.getProcesses()).map(async (p) => {
    let processData = { ...p.processData };
    if (processData.shared) {
      try {
        processData = await pullProcess(processData.id, true);
      } catch (err) {
        processData = browserStorage.updateProcessMetaData(processData.id, { shared: false });
      }
    }

    delete processData.bpmn;
    return processData;
  });

  localProcesses = await Promise.all(localProcesses);

  // prevent processes from being imported twice if they exist in the local storage and the backend
  backendProcesses = backendProcesses.filter((bP) => !localProcesses.some((lP) => lP.id === bP.id));

  return [...backendProcesses, ...localProcesses];
}

/**
 * Contains process specific connections
 */
const processSockets = {};

/**
 * Unregisters from process namespaces
 *
 * @param {String} processDefinitionsId
 */
async function stopObserving(processDefinitionsId) {
  if (!processSockets[processDefinitionsId]) {
    return;
  }

  const { edit, observe } = processSockets[processDefinitionsId];
  delete processSockets[processDefinitionsId];

  observe.disconnect();
  edit.disconnect();
}

/**
 * Connects to process namespace and sets up handlers for server side events
 *
 * @param {String} processDefinitionsId
 */
export async function observeProcess(processDefinitionsId) {
  // only connect if a connection attempt wasn't previously done
  if (!processSockets[processDefinitionsId]) {
    processSockets[processDefinitionsId] = {};

    const observeSocket = io(
      `https://${window.location.hostname}:33081/process/${processDefinitionsId}/view`,
      { auth: { connectionId } }
    );
    processSockets[processDefinitionsId].observe = observeSocket;

    // check if connection can be established
    processSockets[processDefinitionsId].observeConnect = new Promise((resolve, reject) => {
      observeSocket.on('connect', () => {
        resolve();
      });

      observeSocket.on('connect_error', (err) => {
        reject(`Failed connection to observer socket: ${err.message}`);
      });
    });

    observeSocket.on('bpmn_modeler_event_broadcast', (type, context) => {
      eventHandler.dispatch('processBPMNEvent', {
        processDefinitionsId,
        type,
        context: JSON.parse(context),
      });
    });

    // signals changed bpmn to be stored into local storage (should only happen when the process is not currently open in the editor)
    observeSocket.on('process_xml_updated', (processDefinitionsId, newXml) => {
      if (browserStorage.hasProcess(processDefinitionsId)) {
        browserStorage.updateBPMN(processDefinitionsId, newXml);
      }
    });

    // signals forced bpmn change that has to be pushed into the modeler
    observeSocket.on('process_xml_changed', (processDefinitionsId, newXml) => {
      eventHandler.dispatch('processXmlChanged', { processDefinitionsId, newXml });
    });

    observeSocket.on('user_task_html_changed', (taskId, newHtml) => {
      if (browserStorage.hasProcess(processDefinitionsId)) {
        if (newHtml) {
          browserStorage.saveUserTaskHTML(processDefinitionsId, taskId, newHtml);
        } else {
          browserStorage.deleteUserTaskHTML(processDefinitionsId, taskId);
        }
      }

      eventHandler.dispatch('processTaskHtmlChanged', { processDefinitionsId, taskId, newHtml });
    });

    observeSocket.on('script_changed_event_broadcast', (elId, elType, script, change) => {
      eventHandler.dispatch('processScriptChanged', {
        processDefinitionsId,
        elId,
        elType,
        script,
        change,
      });
    });

    observeSocket.on('element_constraints_changed', (elementId, constraints) => {
      eventHandler.dispatch('elementConstraintsChanged', {
        processDefinitionsId,
        elementId,
        constraints,
      });
    });

    observeSocket.on('element_milestones_changed', (elementId, milestones) => {
      eventHandler.dispatch('elementMilestonesChanged', {
        processDefinitionsId,
        elementId,
        milestones,
      });
    });

    observeSocket.on('element_locations_changed', (elementId, locations) => {
      eventHandler.dispatch('elementLocationsChanged', {
        processDefinitionsId,
        elementId,
        location,
      });
    });

    observeSocket.on('element_resources_changed', (elementId, resources) => {
      eventHandler.dispatch('elementResourcesChanged', {
        processDefinitionsId,
        elementId,
        resources,
      });
    });

    observeSocket.on('process_updated', (oldId, updatedInfo) => {
      if (browserStorage.hasProcess(oldId, updatedInfo)) {
        updatedInfo = browserStorage.updateProcessMetaData(oldId, updatedInfo);
      }

      eventHandler.dispatch('processUpdated', { oldId, updatedInfo });
    });

    const editSocket = io(
      `https://${window.location.hostname}:33081/process/${processDefinitionsId}/edit`,
      { auth: { connectionId } }
    );
    processSockets[processDefinitionsId].edit = editSocket;

    processSockets[processDefinitionsId].editConnect = new Promise((resolve) => {
      editSocket.on('connect', () => {
        resolve();
      });
    });

    observeSocket.on('process_removed', (processDefinitionsId) => {
      // Process is set to local to signal that it isn't stored on the server anymore
      try {
        if (browserStorage.hasProcess(processDefinitionsId)) {
          const updatedInfo = browserStorage.updateProcessMetaData(processDefinitionsId, {
            shared: false,
          });
          eventHandler.dispatch('processUpdated', { oldId: processDefinitionsId, updatedInfo });
        } else {
          eventHandler.dispatch('processRemoved', { processDefinitionsId });
        }
      } catch (err) {}

      stopObserving(processDefinitionsId);
    });
  }

  // wait until the connections were succesful
  await processSockets[processDefinitionsId].observeConnect;
  await processSockets[processDefinitionsId].editConnect;
}

async function getProcess(id) {
  let process;

  if (browserStorage.hasProcess(id)) {
    ({ processData: process } = browserStorage.getProcess(id));
  } else {
    process = toInternalFormat(await restRequest(`process/${id}`));
  }

  return process;
}

/**
 * Will send the process to the backend for authenticated users and shared processes or store it in the browser-storage for unauthenticated users
 *
 * @param {Object} processData
 */
async function addProcess(processData) {
  if (processData.owner) {
    await restRequest('process', undefined, 'POST', 'application/json', processData);
    await observeProcess(processData.id);
  } else {
    // prevent the process to be written to storage and backend when we pulled it from the backend and stored it already
    if (!browserStorage.hasProcess(processData.id)) {
      const processInfo = await browserStorage.addProcess(processData);
      if (processInfo.shared) {
        await restRequest('process', undefined, 'POST', 'application/json', processInfo);
        await observeProcess(processInfo.id);
      }
    }
  }
}

async function pushToBackend(processDefinitionsId) {
  if (browserStorage.hasProcess(processDefinitionsId)) {
    browserStorage.updateProcessMetaData(processDefinitionsId, { shared: true });
    const { processData: process, html: htmlData } =
      browserStorage.getProcess(processDefinitionsId);
    await restRequest('process', undefined, 'POST', 'application/json', { ...process });

    await observeProcess(processDefinitionsId);

    Object.entries(htmlData).forEach(([fileName, html]) => {
      saveUserTaskHTML(processDefinitionsId, fileName, html);
    });
  }
}

async function updateProcess(id, { bpmn }) {
  // non local processes are updated by the backend using events send by the clients
  if (browserStorage.hasProcess(id)) {
    browserStorage.updateBPMN(id, bpmn);
  }
}

async function updateWholeXml(id, newXml) {
  if (browserStorage.hasProcess(id)) {
    browserStorage.updateBPMN(id, newXml);
  }
  if (!browserStorage.isProcessLocal(id)) {
    await restRequest(`process/${id}`, undefined, 'PUT', 'application/json', { bpmn: newXml });
  }
}

/**
 * Sends the new bpmn of a process for the backend to save
 *
 * @param {String} id id of the process
 * @param {String} bpmn
 * @param {String} processChanges changes to the process meta information that should be merged on the server
 */
async function updateProcessViaWebsocket(id, bpmn, processChanges = {}) {
  processSockets[id].edit.emit('data_updateProcess', bpmn, processChanges);
}

async function updateProcessMetaData(processDefinitionsId, metaDataChanges) {
  if (browserStorage.hasProcess(processDefinitionsId)) {
    browserStorage.updateProcessMetaData(processDefinitionsId, metaDataChanges);
  }

  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    await new Promise((resolve) => {
      processSockets[processDefinitionsId].edit.emit(
        'data_process_meta_update',
        metaDataChanges,
        () => resolve() // will be called once the server sends acknowledgement
      );
    });
  }
}

async function updateProcessName(id, newName) {
  if (browserStorage.hasProcess(id)) {
    await browserStorage.updateProcessName(id, newName);
  }

  // namechange is send via the modeler events
  // only use this when the name is updated from outside the modeler (e.g. process view)

  // create modeler event
  if (!browserStorage.isProcessLocal(id)) {
    const type = 'definitions.updateProperties';
    const context = { properties: { name: newName } };

    // broadcast event to other clients
    processSockets[id].edit.emit('bpmn_modeler_event', type, JSON.stringify(context));
  }
}

async function updateProcessDescription(processDefinitionsId, processId, description) {
  if (browserStorage.hasProcess(processDefinitionsId)) {
    await browserStorage.updateProcessDescription(processDefinitionsId, description);
  }

  // description update is send via the modeler events
  // only use this when the description is updated from outside the modeler (e.g. process view)

  // create modeler event
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    const type = 'element.updateDocumentation';
    const context = { elementId: processId, documentation: description };

    // broadcast event to other clients
    processSockets[processDefinitionsId].edit.emit(
      'bpmn_modeler_event',
      type,
      JSON.stringify(context)
    );
  }
}

async function removeProcess(id) {
  if (!browserStorage.isProcessLocal(id)) {
    stopObserving(id);
    // A process won't be removed from the backend if a user that is not logged in deletes it (when authentication is used)
    if ((isAuthRequired && isAuthenticated) || !isAuthRequired)
      await restRequest(`process/${id}`, undefined, 'DELETE');
  }

  if (browserStorage.hasProcess(id)) {
    browserStorage.removeProcess(id);
  }
}

async function observeProcessEditing(processDefinitionsId) {
  // cant observe editing for local processes
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    if (!processSockets[processDefinitionsId]) {
      throw new Error('Not connected to process socket!');
    }

    processSockets[processDefinitionsId].observe.emit('data_observe_modeling');
  }
}

async function stopObservingProcessEditing(processDefinitionsId) {
  // cant observe editing for local processes
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    if (!processSockets[processDefinitionsId]) {
      throw new Error('Not connected to process socket!');
    }

    processSockets[processDefinitionsId].observe.emit('data_stop_observing_modeling');
  }
}

async function blockProcess(processDefinitionsId) {
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    processSockets[processDefinitionsId].edit.emit('data_edit_process');
  }
}

async function unblockProcess(processDefinitionsId) {
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    processSockets[processDefinitionsId].edit.emit('data_stop_editing_process');
  }
}

async function blockTask(processDefinitionsId, taskId) {
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    processSockets[processDefinitionsId].edit.emit('data_blockTask', taskId);
  }
}

async function unblockTask(processDefinitionsId, taskId) {
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    processSockets[processDefinitionsId].edit.emit('data_unblockTask', taskId);
  }
}

async function broadcastBPMNEvents(processDefinitionsId, type, context) {
  // prevent updates for local processes from being broadcasted
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    processSockets[processDefinitionsId].edit.emit(
      'bpmn_modeler_event',
      type,
      JSON.stringify(context)
    );
  }
}

async function broadcastScriptChangeEvent(processDefinitionsId, elId, elType, script, change) {
  // prevent updates for local processes from being broadcasted
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    processSockets[processDefinitionsId].edit.emit(
      'script_changed_event',
      elId,
      elType,
      script,
      JSON.stringify(change)
    );
  }
}

async function updateConstraints(processDefinitionsId, elementId, constraints) {
  // prevent updates for local processes from being broadcasted
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    processSockets[processDefinitionsId].edit.emit(
      'data_updateConstraints',
      elementId,
      constraints
    );
  }
}

async function updateMilestones(processDefinitionsId, elementId, milestones) {
  // prevent updates for local processes from being broadcasted
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    processSockets[processDefinitionsId].edit.emit('data_updateMilestones', elementId, milestones);
  }
}

async function updateLocations(processDefinitionsId, elementId, locations) {
  // prevent updates for local processes from being broadcasted
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    processSockets[processDefinitionsId].edit.emit('data_updateResources', elementId, locations);
  }
}

async function updateResources(processDefinitionsId, elementId, resources) {
  // prevent updates for local processes from being broadcasted
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    processSockets[processDefinitionsId].edit.emit('data_updateResources', elementId, resources);
  }
}

async function getUserTasksHTML(processDefinitionsId) {
  let taskIdHTMLMap;

  if (browserStorage.hasProcess(processDefinitionsId)) {
    taskIdHTMLMap = browserStorage.getUserTasksHTML(processDefinitionsId);
  } else {
    taskIdHTMLMap = await restRequest(
      `process/${processDefinitionsId}/user-tasks`,
      'withHtml=true'
    );
  }

  return taskIdHTMLMap;
}

async function saveUserTaskHTML(definitionsId, taskFileName, html) {
  if (browserStorage.hasProcess(definitionsId)) {
    browserStorage.saveUserTaskHTML(definitionsId, taskFileName, html);
  }

  if (!browserStorage.isProcessLocal(definitionsId)) {
    processSockets[definitionsId].edit.emit('data_saveUserTaskHTML', taskFileName, html);
  }
}

async function deleteUserTaskHTML(processDefinitionsId, taskFileName) {
  if (browserStorage.hasProcess(processDefinitionsId)) {
    browserStorage.deleteUserTaskHTML(processDefinitionsId, taskFileName);
  }

  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    processSockets[processDefinitionsId].edit.emit('data_deleteUserTaskHTML', taskFileName);
  }
}

async function saveScriptTaskJS(processDefinitionsId, taskId, js) {
  if (browserStorage.hasProcess(processDefinitionsId)) {
    browserStorage.saveScriptTaskJS(processDefinitionsId, taskId, js);
  }
  if (!browserStorage.isProcessLocal(processDefinitionsId)) {
    processSockets[processDefinitionsId].edit.emit('data_saveScriptTaskJS', taskId, js);
  }
}

export function setProcessesListener() {
  listen('process_added', async (process) => {
    // await pullProcess(process.id);
    // eventHandler.dispatch('processAdded', { process });
    await observeProcess(process.id);
  });
}

export default {
  getProcesses,
  getProcess,
  addProcess,
  updateProcess,
  updateWholeXml,
  updateProcessViaWebsocket,
  updateProcessMetaData,
  updateProcessName,
  updateProcessDescription,
  removeProcess,
  observeProcessEditing,
  stopObservingProcessEditing,
  blockProcess,
  unblockProcess,
  blockTask,
  unblockTask,
  broadcastBPMNEvents,
  broadcastScriptChangeEvent,
  updateConstraints,
  updateMilestones,
  updateLocations,
  updateResources,
  getUserTasksHTML,
  saveUserTaskHTML,
  deleteUserTaskHTML,
  saveScriptTaskJS,
  pullProcess,
  pushToBackend,
};