docs: function aStarBi(graph, options) {
    options = options || {};
    // whether traversal should be considered over oriented graph.
    var oriented = options.oriented;
  
    var heuristic = options.heuristic;
    if (!heuristic) heuristic = defaultSettings.heuristic;
  
    var distance = options.distance;
    if (!distance) distance = defaultSettings.distance;
    var pool = makeSearchStatePool();
  
    return {
      find: find
    };
  
    function find(fromId, toId) {
      // Not sure if we should return NO_PATH or throw. Throw seem to be more
      // helpful to debug errors. So, throwing.
      var from = graph.getNode(fromId);
      if (!from) throw new Error('fromId is not defined in this graph: ' + fromId);
      var to = graph.getNode(toId);
      if (!to) throw new Error('toId is not defined in this graph: ' + toId);
  
      if (from === to) return [from]; // trivial case.
  
      pool.reset();
  
      var callVisitor = oriented ? orientedVisitor : nonOrientedVisitor;
  
      // Maps nodeId to NodeSearchState.
      var nodeState = new Map();
  
      var openSetFrom = new NodeHeap({
        compare: defaultSettings.compareFScore,
        setNodeId: defaultSettings.setHeapIndex
      });
  
      var openSetTo = new NodeHeap({
        compare: defaultSettings.compareFScore,
        setNodeId: defaultSettings.setHeapIndex
      });
  
  
      var startNode = pool.createNewState(from);
      nodeState.set(fromId, startNode);
  
      // For the first node, fScore is completely heuristic.
      startNode.fScore = heuristic(from, to);
      // The cost of going from start to start is zero.
      startNode.distanceToSource = 0;
      openSetFrom.push(startNode);
      startNode.open = BY_FROM;
  
      var endNode = pool.createNewState(to);
      endNode.fScore = heuristic(to, from);
      endNode.distanceToSource = 0;
      openSetTo.push(endNode);
      endNode.open = BY_TO;
  
      // Cost of the best solution found so far. Used for accurate termination
      var lMin = Number.POSITIVE_INFINITY;
      var minFrom;
      var minTo;
  
      var currentSet = openSetFrom;
      var currentOpener = BY_FROM;
  
      while (openSetFrom.length > 0 && openSetTo.length > 0) {
        if (openSetFrom.length < openSetTo.length) {
          // we pick a set with less elements
          currentOpener = BY_FROM;
          currentSet = openSetFrom;
        } else {
          currentOpener = BY_TO;
          currentSet = openSetTo;
        }
  
        var current = currentSet.pop();
  
        // no need to visit this node anymore
        current.closed = true;
  
        if (current.distanceToSource > lMin) continue;
  
        graph.forEachLinkedNode(current.node.id, callVisitor);
  
        if (minFrom && minTo) {
          // This is not necessary the best path, but we are so greedy that we
          // can't resist:
          return reconstructBiDirectionalPath(minFrom, minTo);
        }
      }
  
      return NO_PATH; // No path.
  
      function nonOrientedVisitor(otherNode, link) {
        return visitNode(otherNode, link, current);
      }
  
      function orientedVisitor(otherNode, link) {
        // For oritned graphs we need to reverse graph, when traveling
        // backwards. So, we use non-oriented ngraph's traversal, and 
        // filter link orientation here.
        if (currentOpener === BY_FROM) {
          if (link.fromId === current.node.id) return visitNode(otherNode, link, current)
        } else if (currentOpener === BY_TO) {
          if (link.toId === current.node.id) return visitNode(otherNode, link, current);
        }
      }
  
      function canExit(currentNode) {
        var opener = currentNode.open
        if (opener && opener !== currentOpener) {
          return true;
        }
  
        return false;
      }
  
      function reconstructBiDirectionalPath(a, b) {
        var pathOfNodes = [];
        var aParent = a;
        while(aParent) {
          pathOfNodes.push(aParent.node);
          aParent = aParent.parent;
        }
        var bParent = b;
        while (bParent) {
          pathOfNodes.unshift(bParent.node);
          bParent = bParent.parent
        }
        return pathOfNodes;
      }
  
      function visitNode(otherNode, link, cameFrom) {
        var otherSearchState = nodeState.get(otherNode.id);
        if (!otherSearchState) {
          otherSearchState = pool.createNewState(otherNode);
          nodeState.set(otherNode.id, otherSearchState);
        }
  
        if (otherSearchState.closed) {
          // Already processed this node.
          return;
        }
  
        if (canExit(otherSearchState, cameFrom)) {
          // this node was opened by alternative opener. The sets intersect now,
          // we found an optimal path, that goes through *this* node. However, there
          // is no guarantee that this is the global optimal solution path.
  
          var potentialLMin = otherSearchState.distanceToSource + cameFrom.distanceToSource;
          if (potentialLMin < lMin) {
            minFrom = otherSearchState;
            minTo = cameFrom
            lMin = potentialLMin;
          }
          // we are done with this node.
          return;
        }
  
        var tentativeDistance = cameFrom.distanceToSource + distance(otherSearchState.node, cameFrom.node, link);
  
        if (tentativeDistance >= otherSearchState.distanceToSource) {
          // This would only make our path longer. Ignore this route.
          return;
        }
  
        // Choose target based on current working set:
        var target = (currentOpener === BY_FROM) ? to : from;
        var newFScore = tentativeDistance + heuristic(otherSearchState.node, target);
        if (newFScore >= lMin) {
          // this can't be optimal path, as we have already found a shorter path.
          return;
        }
        otherSearchState.fScore = newFScore;
  
        if (otherSearchState.open === 0) {
          // Remember this node in the current set
          currentSet.push(otherSearchState);
          currentSet.updateItem(otherSearchState.heapIndex);
  
          otherSearchState.open = currentOpener;
        }
  
        // bingo! we found shorter path:
        otherSearchState.parent = cameFrom;
        otherSearchState.distanceToSource = tentativeDistance;
      }
    }
  }