Source: management-system/src/frontend/helpers/bpmn-modeler-events/event-serialization.js

/**
 * Transforms the given context into a JSON serializable form
 * this form contains everything to restore the information on another client to apply the event in the same way
 *
 * @param {string} command the command that is supposed to be executed on another machine
 * @param {object} context the information needed to execute the command
 * @returns {object} - the transformed context
 */
export function preSerialize(command, context) {
  const preSerializedContext = {};
  // prevent sending a force distribution flag to the other clients or we will get an infinite loop
  context.forceDistribution = false;
  // transform the different entries of the context into a form that allows to reproduce the original form on another device
  // e.g. local objects can't be just serialized => replace with object id and then find the object by id on the other device
  Object.entries(context).forEach(([key, info]) => {
    switch (key) {
      // replace all shapes with their ids
      case 'shapes':
        preSerializedContext.shapes = info.map((shape) => shape.id);
        break;
      // allow creation of similar new Root by sending expected type and id
      case 'newRoot':
        preSerializedContext.newRoot = { type: info.type };
        preSerializedContext.newRoot.businessObject = { id: info.id };
        break;
      case 'connection':
        if (command === 'connection.create') {
          // allow creation of similar new connection by sending expected type and id
          preSerializedContext.connection = { type: info.type, id: info.id };
          break;
        }
      // fall through to replacing the connection with its id
      case 'newData':
      case 'shape':
        if (command === 'shape.create' || command === 'shape.replace') {
          // allow creation of similar new shape by sending expected type and all necessary aditional information
          preSerializedContext[key] = {
            type: info.type,
            x: info.x,
            y: info.y,
            height: info.height,
            width: info.width,
            isExpanded: info.isExpanded, // needed for subProcesses
            businessObject: {
              id: info.id,
              cancelActivity: info.businessObject.cancelActivity, // needed for boundary events
              triggeredByEvent: info.businessObject.triggeredByEvent, // needed for event subProcesses
            },
          };
          if (info.isExpanded === false) {
            preSerializedContext[key].collapsed = true; // needed for creation of collapsed subProcesses
          }
          if (info.businessObject) {
            preSerializedContext[key].businessObject.name = info.businessObject.name;
            if (info.businessObject.processRef) {
              preSerializedContext[key].processRef = info.businessObject.processRef.id; // needed for participants
            }
            if (info.businessObject.dataObjectRef) {
              preSerializedContext.dataObjectId = info.businessObject.dataObjectRef.id;
            }
            if (
              info.businessObject.eventDefinitions &&
              info.businessObject.eventDefinitions.length
            ) {
              preSerializedContext[key].eventDefinitionType = // needed for startEvents, boundaryEvents, endEvents
                info.businessObject.eventDefinitions[0].$type;
              preSerializedContext.eventDefinitionId = info.businessObject.eventDefinitions[0].id;
            }
            if (info.businessObject.di && preSerializedContext[key].isExpanded === undefined) {
              preSerializedContext[key].isExpanded = info.businessObject.di.isExpanded; // needed for subProcesses
            }
            if (info.type === 'bpmn:Lane') {
              preSerializedContext.laneSetId = info.businessObject.$parent.id;
            }
            // for when we replace a non subProcess typed activity with a collapsed/expanded subProcess => startEvent implicitly created
            // we need to make sure the start event gets the correct id on other clients
            if (info.businessObject.flowElements && info.businessObject.flowElements.length === 1) {
              if (info.businessObject.flowElements[0].$type === 'bpmn:StartEvent') {
                preSerializedContext.subProcessStartEventId =
                  info.businessObject.flowElements[0].id;
              }
            }
          }
          break;
        }
      // replace all of the following with their id
      case 'source':
      case 'target':
        if (info.businessObject.properties) {
          preSerializedContext.targetPropertyIds = info.businessObject.properties.map(
            (property) => property.id
          );
        }
      case 'newSource':
      case 'newTarget':
      case 'oldShape':
      case 'newParent':
      case 'element':
        // if we change the id we have to make sure to send the old id of the element to change
        if (command === 'element.updateProperties' && context.properties.id) {
          preSerializedContext.element = context.oldProperties.id;
          break;
        }
      case 'host':
        if (!info) {
          break;
        }
      case 'parent':
        preSerializedContext[key] = info.id;
        break;
      case 'properties':
        preSerializedContext[key] = {};
        Object.entries(info).forEach(([propertyKey, propertyValue]) => {
          if (propertyValue && typeof propertyValue === 'object' && propertyValue.id) {
            preSerializedContext[key][propertyKey] = { id: propertyValue.id };
          } else {
            preSerializedContext[key][propertyKey] = propertyValue;
          }
        });
        break;
      // everything else can just be serialized
      default:
        preSerializedContext[key] = info;
        break;
    }
  });

  return preSerializedContext;
}

/**
 * Transforms the serialized context back into a form that we can use to trigger an event similar to the original one
 *
 * @param {object} elementRegistry gives us functions to get elements in the modeler (shapes, processes, sequence flows)
 * @param {object} elementFactory gives us functions to create modeler elements
 * @param {object} bpmnFactory gives us functions to create the businessObjects of modeler elements
 * @param {object} command the command we will execute
 * @param {object} context the serialized context
 * @returns {object} - the restored context
 */
export function parse(elementRegistry, elementFactory, bpmnFactory, command, context) {
  const parsedContext = {};

  // restore every context entry to a usable form
  Object.entries(context).forEach(([key, info]) => {
    switch (key) {
      // replace the ids for all shapes with the correct shapes
      case 'shapes':
        parsedContext.shapes = info.map((id) => elementRegistry.get(id));
        break;
      // create a new Root with the correct attributes
      case 'newRoot':
        const bO = bpmnFactory.create(info.type, info.businessObject);
        parsedContext.newRoot = elementFactory.createRoot({ ...info, businessObject: bO });
        break;
      case 'connection':
        if (command === 'connection.create') {
          // create a new connection with the correct attributes

          const businessObject = bpmnFactory.create(info.type, { id: info.id });
          parsedContext.connection = elementFactory.createConnection({ ...info, businessObject });

          if (context.changedBySubprocessId) {
            const element = elementRegistry.get(context.changedBySubprocessId);
            if (element && element.type !== 'bpmn:Process') {
              parsedContext.connection.hidden = true;
            }
          }

          break;
        }
      // fall through to getting the existing connection by id
      case 'newData':
      case 'shape':
        if (command === 'shape.create' || command === 'shape.replace') {
          // create a new shape with the correct attributes
          const businessObject = bpmnFactory.create(info.type, info.businessObject);
          parsedContext[key] = elementFactory.createShape({ ...info, businessObject });
          if (info.processRef) {
            // we want to create a new participant with the correct referenced process (same id as original one)
            const process = bpmnFactory.create('bpmn:Process', {
              id: info.processRef,
            });
            parsedContext[key].businessObject.processRef = process;
          }

          if (context.changedBySubprocessId) {
            const element = elementRegistry.get(context.changedBySubprocessId);
            if (element && element.type !== 'bpmn:Process') {
              parsedContext[key].hidden = true;
            }
          }
          break;
        }
      // fall through to getting the existing element by id
      case 'source':
      case 'target':
      case 'newSource':
      case 'newTarget':
      case 'oldShape':
      case 'newParent':
      case 'element':
      case 'host':
        if (!info) {
          break;
        }
      case 'parent':
        // get modeler element by id
        parsedContext[key] = elementRegistry.get(info);
        break;
      case 'newShape':
        break;
      case 'properties':
        parsedContext[key] = {};
        Object.entries(info).forEach(([propertyKey, propertyValue]) => {
          if (propertyValue && typeof propertyValue === 'object' && propertyValue.id) {
            parsedContext[key][propertyKey] = elementRegistry.get(propertyValue.id).businessObject;
          } else {
            parsedContext[key][propertyKey] = propertyValue;
          }
        });
        break;
      // everything else was just serialized and can be used as is
      default:
        parsedContext[key] = info;
        break;
    }
  });

  return parsedContext;
}