docs: function nba(graph, options) {
    options = options || {};
    // whether traversal should be considered over oriented graph.
    var oriented = options.oriented;
    var quitFast = options.quitFast;
  
    var heuristic = options.heuristic;
    if (!heuristic) heuristic = defaultSettings.heuristic;
  
    var distance = options.distance;
    if (!distance) distance = defaultSettings.distance;
  
    // During stress tests I noticed that garbage collection was one of the heaviest
    // contributors to the algorithm's speed. So I'm using an object pool to recycle nodes.
    var pool = makeNBASearchStatePool();
  
    return {
      /**
       * Finds a path between node `fromId` and `toId`.
       * @returns {Array} of nodes between `toId` and `fromId`. Empty array is returned
       * if no path is found.
       */
      find: find
    };
  
    function find(fromId, toId) {
      // I must apologize for the code duplication. This was the easiest way for me to
      // implement the algorithm fast.
      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);
  
      pool.reset();
  
      // I must also apologize for somewhat cryptic names. The NBA* is bi-directional
      // search algorithm, which means it runs two searches in parallel. One is called
      // forward search and it runs from source node to target, while the other one
      // (backward search) runs from target to source.
  
      // Everywhere where you see `1` it means it's for the forward search. `2` is for 
      // backward search.
  
      // For oriented graph path finding, we need to reverse the graph, so that
      // backward search visits correct link. Obviously we don't want to duplicate
      // the graph, instead we always traverse the graph as non-oriented, and filter
      // edges in `visitN1Oriented/visitN2Oritented`
      var forwardVisitor = oriented ? visitN1Oriented : visitN1;
      var reverseVisitor = oriented ? visitN2Oriented : visitN2;
  
      // Maps nodeId to NBASearchState.
      var nodeState = new Map();
  
      // These two heaps store nodes by their underestimated values.
      var open1Set = new NodeHeap({
        compare: defaultSettings.compareF1Score,
        setNodeId: defaultSettings.setH1
      });
      var open2Set = new NodeHeap({
        compare: defaultSettings.compareF2Score,
        setNodeId: defaultSettings.setH2
      });
  
      // This is where both searches will meet.
      var minNode;
  
      // The smallest path length seen so far is stored here:
      var lMin = Number.POSITIVE_INFINITY;
  
      // We start by putting start/end nodes to the corresponding heaps
      // If variable names like `f1`, `g1` are too confusing, please refer
      // to makeNBASearchStatePool.js file, which has detailed description.
      var startNode = pool.createNewState(from);
      nodeState.set(fromId, startNode); 
      startNode.g1 = 0;
      var f1 = heuristic(from, to);
      startNode.f1 = f1;
      open1Set.push(startNode);
  
      var endNode = pool.createNewState(to);
      nodeState.set(toId, endNode);
      endNode.g2 = 0;
      var f2 = f1; // they should agree originally
      endNode.f2 = f2;
      open2Set.push(endNode)
  
      // the `cameFrom` variable is accessed by both searches, so that we can store parents.
      var cameFrom;
  
      // this is the main algorithm loop:
      while (open2Set.length && open1Set.length) {
        if (open1Set.length < open2Set.length) {
          forwardSearch();
        } else {
          reverseSearch();
        }
  
        if (quitFast && minNode) break;
      }
  
      var path = reconstructPath(minNode);
      return path; // the public API is over
  
      function forwardSearch() {
        cameFrom = open1Set.pop();
        if (cameFrom.closed) {
          return;
        }
  
        cameFrom.closed = true;
  
        if (cameFrom.f1 < lMin && (cameFrom.g1 + f2 - heuristic(from, cameFrom.node)) < lMin) {
          graph.forEachLinkedNode(cameFrom.node.id, forwardVisitor);
        }
  
        if (open1Set.length > 0) {
          // this will be used in reverse search
          f1 = open1Set.peek().f1;
        } 
      }
  
      function reverseSearch() {
        cameFrom = open2Set.pop();
        if (cameFrom.closed) {
          return;
        }
        cameFrom.closed = true;
  
        if (cameFrom.f2 < lMin && (cameFrom.g2 + f1 - heuristic(cameFrom.node, to)) < lMin) {
          graph.forEachLinkedNode(cameFrom.node.id, reverseVisitor);
        }
  
        if (open2Set.length > 0) {
          // this will be used in forward search
          f2 = open2Set.peek().f2;
        }
      }
  
      function visitN1(otherNode, link) {
        var otherSearchState = nodeState.get(otherNode.id);
        if (!otherSearchState) {
          otherSearchState = pool.createNewState(otherNode);
          nodeState.set(otherNode.id, otherSearchState);
        }
  
        if (otherSearchState.closed) return;
  
        var tentativeDistance = cameFrom.g1 + distance(cameFrom.node, otherNode, link);
  
        if (tentativeDistance < otherSearchState.g1) {
          otherSearchState.g1 = tentativeDistance;
          otherSearchState.f1 = tentativeDistance + heuristic(otherSearchState.node, to);
          otherSearchState.p1 = cameFrom;
          if (otherSearchState.h1 < 0) {
            open1Set.push(otherSearchState);
          } else {
            open1Set.updateItem(otherSearchState.h1);
          }
        }
        var potentialMin = otherSearchState.g1 + otherSearchState.g2;
        if (potentialMin < lMin) { 
          lMin = potentialMin;
          minNode = otherSearchState;
        }
      }
  
      function visitN2(otherNode, link) {
        var otherSearchState = nodeState.get(otherNode.id);
        if (!otherSearchState) {
          otherSearchState = pool.createNewState(otherNode);
          nodeState.set(otherNode.id, otherSearchState);
        }
  
        if (otherSearchState.closed) return;
  
        var tentativeDistance = cameFrom.g2 + distance(cameFrom.node, otherNode, link);
  
        if (tentativeDistance < otherSearchState.g2) {
          otherSearchState.g2 = tentativeDistance;
          otherSearchState.f2 = tentativeDistance + heuristic(from, otherSearchState.node);
          otherSearchState.p2 = cameFrom;
          if (otherSearchState.h2 < 0) {
            open2Set.push(otherSearchState);
          } else {
            open2Set.updateItem(otherSearchState.h2);
          }
        }
        var potentialMin = otherSearchState.g1 + otherSearchState.g2;
        if (potentialMin < lMin) {
          lMin = potentialMin;
          minNode = otherSearchState;
        }
      }
  
      function visitN2Oriented(otherNode, link) {
        // we are going backwards, graph needs to be reversed. 
        if (link.toId === cameFrom.node.id) return visitN2(otherNode, link);
      }
      function visitN1Oriented(otherNode, link) {
        // this is forward direction, so we should be coming FROM:
        if (link.fromId === cameFrom.node.id) return visitN1(otherNode, link);
      }
    }
  }