_updateState() {
    let nodeId;
    const deletedNodeIds = [];
    const deletedEdgeIds = {};

    /**
     * Utility function to iterate over clustering nodes only
     * @param {Function} callback  function to call for each cluster node
     */
    const eachClusterNode = (callback) => {
      forEach(this.body.nodes, (node) => {
        if (node.isCluster === true) {
          callback(node);
        }
      });
    };

    //
    // Remove deleted regular nodes from clustering
    //

    // Determine the deleted nodes
    for (nodeId in this.clusteredNodes) {
      if (!Object.prototype.hasOwnProperty.call(this.clusteredNodes, nodeId))
        continue;
      const node = this.body.nodes[nodeId];

      if (node === undefined) {
        deletedNodeIds.push(nodeId);
      }
    }

    // Remove nodes from cluster nodes
    eachClusterNode(function (clusterNode) {
      for (let n = 0; n < deletedNodeIds.length; n++) {
        delete clusterNode.containedNodes[deletedNodeIds[n]];
      }
    });

    // Remove nodes from cluster list
    for (let n = 0; n < deletedNodeIds.length; n++) {
      delete this.clusteredNodes[deletedNodeIds[n]];
    }

    //
    // Remove deleted edges from clustering
    //

    // Add the deleted clustered edges to the list
    forEach(this.clusteredEdges, (edgeId) => {
      const edge = this.body.edges[edgeId];
      if (edge === undefined || !edge.endPointsValid()) {
        deletedEdgeIds[edgeId] = edgeId;
      }
    });

    // Cluster nodes can also contain edges which are not clustered,
    // i.e. nodes 1-2 within cluster with an edge in between.
    // So the cluster nodes also need to be scanned for invalid edges
    eachClusterNode(function (clusterNode) {
      forEach(clusterNode.containedEdges, (edge, edgeId) => {
        if (!edge.endPointsValid() && !deletedEdgeIds[edgeId]) {
          deletedEdgeIds[edgeId] = edgeId;
        }
      });
    });

    // Also scan for cluster edges which need to be removed in the active list.
    // Regular edges have been removed beforehand, so this only picks up the cluster edges.
    forEach(this.body.edges, (edge, edgeId) => {
      // Explicitly scan the contained edges for validity
      let isValid = true;
      const replacedIds = edge.clusteringEdgeReplacingIds;
      if (replacedIds !== undefined) {
        let numValid = 0;

        forEach(replacedIds, (containedEdgeId) => {
          const containedEdge = this.body.edges[containedEdgeId];

          if (containedEdge !== undefined && containedEdge.endPointsValid()) {
            numValid += 1;
          }
        });

        isValid = numValid > 0;
      }

      if (!edge.endPointsValid() || !isValid) {
        deletedEdgeIds[edgeId] = edgeId;
      }
    });

    // Remove edges from cluster nodes
    eachClusterNode((clusterNode) => {
      forEach(deletedEdgeIds, (deletedEdgeId) => {
        delete clusterNode.containedEdges[deletedEdgeId];

        forEach(clusterNode.edges, (edge, m) => {
          if (edge.id === deletedEdgeId) {
            clusterNode.edges[m] = null; // Don't want to directly delete here, because in the loop
            return;
          }

          edge.clusteringEdgeReplacingIds = this._filter(
            edge.clusteringEdgeReplacingIds,
            function (id) {
              return !deletedEdgeIds[id];
            }
          );
        });

        // Clean up the nulls
        clusterNode.edges = this._filter(clusterNode.edges, function (item) {
          return item !== null;
        });
      });
    });

    // Remove from cluster list
    forEach(deletedEdgeIds, (edgeId) => {
      delete this.clusteredEdges[edgeId];
    });

    // Remove cluster edges from active list (this.body.edges).
    // deletedEdgeIds still contains id of regular edges, but these should all
    // be gone when you reach here.
    forEach(deletedEdgeIds, (edgeId) => {
      delete this.body.edges[edgeId];
    });

    //
    // Check changed cluster state of edges
    //

    // Iterating over keys here, because edges may be removed in the loop
    const ids = Object.keys(this.body.edges);
    forEach(ids, (edgeId) => {
      const edge = this.body.edges[edgeId];

      const shouldBeClustered =
        this._isClusteredNode(edge.fromId) || this._isClusteredNode(edge.toId);
      if (shouldBeClustered === this._isClusteredEdge(edge.id)) {
        return; // all is well
      }

      if (shouldBeClustered) {
        // add edge to clustering
        const clusterFrom = this._getClusterNodeForNode(edge.fromId);
        if (clusterFrom !== undefined) {
          this._clusterEdges(this.body.nodes[edge.fromId], edge, clusterFrom);
        }

        const clusterTo = this._getClusterNodeForNode(edge.toId);
        if (clusterTo !== undefined) {
          this._clusterEdges(this.body.nodes[edge.toId], edge, clusterTo);
        }

        // TODO: check that it works for both edges clustered
        //       (This might be paranoia)
      } else {
        delete this._clusterEdges[edgeId];
        this._restoreEdge(edge);
        // This should not be happening, the state should
        // be properly updated at this point.
        //
        // If it *is* reached during normal operation, then we have to implement
        // undo clustering for this edge here.
        // throw new Error('remove edge from clustering not implemented!')
      }
    });

    // Clusters may be nested to any level. Keep on opening until nothing to open
    let changed = false;
    let continueLoop = true;
    while (continueLoop) {
      const clustersToOpen = [];

      // Determine the id's of clusters that need opening
      eachClusterNode(function (clusterNode) {
        const numNodes = Object.keys(clusterNode.containedNodes).length;
        const allowSingle = clusterNode.options.allowSingleNodeCluster === true;
        if ((allowSingle && numNodes < 1) || (!allowSingle && numNodes < 2)) {
          clustersToOpen.push(clusterNode.id);
        }
      });

      // Open them
      for (let n = 0; n < clustersToOpen.length; ++n) {
        this.openCluster(
          clustersToOpen[n],
          {},
          false /* Don't refresh, we're in an refresh/update already */
        );
      }

      continueLoop = clustersToOpen.length > 0;
      changed = changed || continueLoop;
    }

    if (changed) {
      this._updateState(); // Redo this method (recursion possible! should be safe)
    }
  }
Scan all edges for changes in clustering and adjust this if necessary.

Call this (internally) after there has been a change in node or edge data.

Pre: States of this.body.nodes and this.body.edges consistent
Pre: this.clusteredNodes and this.clusteredEdge consistent with containedNodes and containedEdges
     of cluster nodes.