docs: function PoolCluster(args) {
    const opts = new PoolClusterOptions(args);
    const nodes = {};
    let cachedPatterns = {};
    let nodeCounter = 0;
  
    /**
     * Add a new pool node to cluster.
     *
     * @param id      identifier
     * @param config  pool configuration
     */
    this.add = (id, config) => {
      let identifier;
      if (typeof id === 'string' || id instanceof String) {
        identifier = id;
        if (nodes[identifier])
          throw new Error("Node identifier '" + identifier + "' already exist !");
      } else {
        identifier = 'PoolNode-' + nodeCounter++;
      }
      const options = new PoolOptions(config);
      const pool = _createPool(options);
      pool.initialize();
      nodes[identifier] = pool;
    };
  
    /**
     * End cluster (and underlying pools).
     *
     * @return {Promise<any[]>}
     */
    this.end = () => {
      cachedPatterns = {};
      const poolEndPromise = [];
      Object.keys(nodes).forEach(pool => {
        poolEndPromise.push(nodes[pool].end());
        delete nodes[pool];
      });
  
      return Promise.all(poolEndPromise);
    };
  
    this.of = (pattern, selector) => {
      return new FilteredPoolCluster(this, pattern, selector);
    };
  
    /**
     * Remove nodes according to pattern.
     *
     * @param pattern  pattern
     */
    this.remove = pattern => {
      if (!pattern)
        throw new Error(
          'pattern parameter in Cluster.remove(pattern)  is mandatory'
        );
  
      const regex = RegExp(pattern);
      Object.keys(nodes).forEach(key => {
        if (regex.test(key)) {
          nodes[key].end();
          delete nodes[key];
          cachedPatterns = {};
        }
      });
    };
  
    /**
     * Get connection from available pools matching pattern, according to selector
     *
     * @param pattern       pattern filter (not mandatory)
     * @param selector      node selector ('RR','RANDOM' or 'ORDER')
     * @return {Promise}
     */
    this.getConnection = (pattern, selector) => {
      return _getConnection(pattern, selector);
    };
  
    /**
     * Force using callback methods.
     */
    this.setCallback = () => {
      this.getConnection = _getConnectionCallback;
      _createPool = _createPoolCallback;
    };
  
    /**
     * Get connection from available pools matching pattern, according to selector
     * with additional parameter to avoid reusing failing node
     *
     * @param pattern       pattern filter (not mandatory)
     * @param selector      node selector ('RR','RANDOM' or 'ORDER')
     * @param avoidNodeKey  failing node
     * @param lastError     last error
     * @return {Promise}
     * @private
     */
    const _getConnection = (pattern, selector, avoidNodeKey, lastError) => {
      const matchingNodeList = _matchingNodes(pattern || /^/);
  
      if (matchingNodeList.length === 0) {
        if (Object.keys(nodes).length === 0)
          return Promise.reject(
            new Error(
              'No node have been added to cluster or nodes have been removed due to too much connection error'
            )
          );
        if (avoidNodeKey === undefined)
          return Promise.reject(
            new Error("No node found for pattern '" + pattern + "'")
          );
        const errMsg =
          "No Connection available for '" +
          pattern +
          "'" +
          (lastError ? '. Last connection error was: ' + lastError.message : '');
        return Promise.reject(new Error(errMsg));
      }
  
      const retry = _getConnection.bind(this, pattern, selector);
      try {
        const nodeKey = _selectPool(matchingNodeList, selector, avoidNodeKey);
        return _handleConnectionError(matchingNodeList, nodeKey, retry);
      } catch (e) {
        return Promise.reject(e);
      }
    };
  
    let _createPool = options => {
      return new Pool(options, false);
    };
  
    const _createPoolCallback = options => {
      return new PoolCallback(options, false);
    };
  
    /**
     * Get connection from available pools matching pattern, according to selector
     * with additional parameter to avoid reusing failing node
     *
     * @param pattern       pattern filter (not mandatory)
     * @param selector      node selector ('RR','RANDOM' or 'ORDER')
     * @param callback      callback function
     * @param avoidNodeKey  failing node
     * @private
     */
    const _getConnectionCallback = (
      pattern,
      selector,
      callback,
      avoidNodeKey
    ) => {
      const matchingNodeList = _matchingNodes(pattern || /^/);
  
      if (matchingNodeList.length === 0) {
        if (Object.keys(nodes).length === 0) {
          callback(
            new Error(
              'No node have been added to cluster or nodes have been removed due to too much connection error'
            )
          );
          return;
        }
        callback(new Error("No node found for pattern '" + pattern + "'"));
        return;
      }
  
      const retry = _getConnectionCallback.bind(
        this,
        pattern,
        selector,
        callback
      );
      try {
        const nodeKey = _selectPool(matchingNodeList, selector, avoidNodeKey);
        _handleConnectionCallbackError(
          matchingNodeList,
          nodeKey,
          retry,
          callback
        );
      } catch (e) {
        callback(e);
      }
    };
  
    /**
     * Selecting nodes according to pattern.
     *
     * @param pattern pattern
     * @return {*}
     * @private
     */
    const _matchingNodes = pattern => {
      if (cachedPatterns[pattern]) return cachedPatterns[pattern];
  
      const regex = RegExp(pattern);
      const matchingNodeList = [];
      Object.keys(nodes).forEach(key => {
        if (regex.test(key)) {
          matchingNodeList.push(key);
        }
      });
  
      cachedPatterns[pattern] = matchingNodeList;
      return matchingNodeList;
    };
  
    /**
     * Select next node to be chosen in nodeList according to selector and failed nodes.
     *
     * @param nodeList        current node list
     * @param selectorParam   selector
     * @param avoidNodeKey    last failing node to avoid selecting this one.
     * @return {Promise}
     * @private
     */
    const _selectPool = (nodeList, selectorParam, avoidNodeKey) => {
      const selector = selectorParam || opts.defaultSelector;
      let retry = 0;
      let selectorFct;
      let nodeKey;
      switch (selector) {
        case 'RR':
          selectorFct = roundRobinSelector;
          break;
  
        case 'RANDOM':
          selectorFct = randomSelector;
          break;
  
        case 'ORDER':
          selectorFct = orderedSelector;
          break;
  
        default:
          throw new Error(
            "Wrong selector value '" +
              selector +
              "'. Possible values are 'RR','RANDOM' or 'ORDER'"
          );
      }
  
      nodeKey = selectorFct(nodeList, retry);
      while (
        (avoidNodeKey === nodeKey || nodes[nodeKey].timeout > Date.now()) &&
        retry < nodeList.length - 1
      ) {
        retry++;
        nodeKey = selectorFct(nodeList);
      }
      return nodeKey;
    };
  
    /**
     * Round robin selector: using nodes one after the other.
     *
     * @param nodeList  node list
     * @return {String}
     */
    const roundRobinSelector = nodeList => {
      let lastRoundRobin = nodeList.lastRrIdx;
      if (lastRoundRobin === undefined) lastRoundRobin = -1;
      if (++lastRoundRobin >= nodeList.length) lastRoundRobin = 0;
      nodeList.lastRrIdx = lastRoundRobin;
      return nodeList[lastRoundRobin];
    };
  
    /**
     * Random selector: use a random node.
     *
     * @param nodeList  node list
     * @return {String}
     */
    const randomSelector = nodeList => {
      let randomIdx = Math.floor(Math.random() * nodeList.length);
      return nodeList[randomIdx];
    };
  
    /**
     * Ordered selector: always use the nodes in sequence, unless failing.
     *
     * @param nodeList  node list
     * @param retry     sequence number if last node is tagged has failing
     * @return {String}
     */
    const orderedSelector = (nodeList, retry) => {
      return nodeList[retry];
    };
  
    /**
     * Connect, or if fail handle retry / set timeout error
     *
     * @param nodeList    current node list
     * @param nodeKey     node name to connect
     * @param retryFct    retry function
     * @return {Promise}
     * @private
     */
    const _handleConnectionError = (nodeList, nodeKey, retryFct) => {
      const node = nodes[nodeKey];
      return node
        .getConnection()
        .then(conn => {
          node.errorCount = 0;
          return Promise.resolve(conn);
        })
        .catch(err => {
          node.errorCount = node.errorCount ? node.errorCount + 1 : 1;
          if (node.errorCount >= opts.removeNodeErrorCount && nodes[nodeKey]) {
            if (opts.restoreNodeTimeout === 0) {
              delete nodes[nodeKey];
              cachedPatterns = {};
              delete nodeList.lastRrIdx;
              //remove node from configuration if not already removed
              node.end().catch(err => {
                // dismiss error
              });
            } else {
              node.timeout = Date.now() + opts.restoreNodeTimeout;
            }
            if (nodeList.length === 0) return Promise.reject(err);
          }
  
          if (opts.canRetry) return retryFct(nodeKey, err);
          return Promise.reject(err);
        });
    };
  
    /**
     * Connect, or if fail handle retry / set timeout error
     *
     * @param nodeList    current node list
     * @param nodeKey     node name to connect
     * @param retryFct    retry function
     * @param callback    callback function
     * @private
     */
    const _handleConnectionCallbackError = (
      nodeList,
      nodeKey,
      retryFct,
      callback
    ) => {
      const node = nodes[nodeKey];
      node.getConnection((err, conn) => {
        if (err) {
          node.errorCount = node.errorCount ? node.errorCount + 1 : 1;
          if (node.errorCount >= opts.removeNodeErrorCount && nodes[nodeKey]) {
            if (opts.restoreNodeTimeout === 0) {
              delete nodes[nodeKey];
              cachedPatterns = {};
              delete nodeList.lastRrIdx;
              //remove node from configuration if not already removed
              node.end(() => {
                //dismiss error
              });
            } else {
              node.timeout = Date.now() + opts.restoreNodeTimeout;
            }
            if (nodeList.length === 0) return Promise.reject(err);
          }
  
          if (opts.canRetry) return retryFct(nodeKey);
          callback(err);
        } else {
          node.errorCount = 0;
          callback(null, conn);
        }
      });
    };
  
    //*****************************************************************
    // internal public testing methods
    //*****************************************************************
  
    function TestMethods() {}
    TestMethods.prototype.getNodes = () => {
      return nodes;
    };
  
    this.__tests = new TestMethods();
  }