<commit_before>"use strict";
var SearchParameters = require( "./SearchParameters" );
var SearchResults = require( "./SearchResults" );
var extend = require( "./functions/extend" );
var util = require( "util" );
var events = require( "events" );
var forEach = require( "lodash/collection/forEach" );
var bind = require( "lodash/function/bind" );

/**
 * Initialize a new AlgoliaSearchHelper
 * @class
 * @classdesc The AlgoliaSearchHelper is a class that ease the management of the
 * search. It provides an event based interface for search callbacks :
 *  - change : when the internal search state is changed. This event contains a {SearchParameters} object
 *  - result : when the response is retrieved from Algolia and is processed. This event contains a {SearchResults} object
 *  - error  : when the response is an error. This event contains the error returned by the server.
 * @param  {AlgoliaSearch} client an AlgoliaSearch client
 * @param  {string} index the index name to query
 * @param  {SearchParameters | object} options an object defining the initial config of the search. It doesn't have to be a {SearchParamaters}, just an object containing the properties you need from it.
 */
function AlgoliaSearchHelper( client, index, options ) {
  this.client = client;
  this.index = index;
  this.state = new SearchParameters( options );
}

util.inherits( AlgoliaSearchHelper, events.EventEmitter );

/**
 * Start the search with the parameters set in the state.
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.search = function() {
  this._search();
  return this;
};

/**
 * Sets the query. Also sets the current page to 0.
 * @param  {string} q the user query
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.setQuery = function( q ) {
  this.state = this.state.setQuery( q );

  this.emit( "change", this.state );
  return this;
};

/**
 * Remove all refinements (disjunctive + conjunctive + excludes + numeric filters)
 * @param {string} [name] - If given, name of the facet / attribute on which  we want to remove all refinements
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.clearRefinements = function( name ) {
  this.state = this.state.clearRefinements( name );
  this.emit( "change", this.state );
  return this;
};

/**
 * Ensure a facet refinement exists
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.addDisjunctiveRefine = function( facet, value ) {
  this.state = this.state.addDisjunctiveFacetRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Ensure a facet refinement does not exist
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.removeDisjunctiveRefine = function( facet, value ) {
  this.state = this.state.removeDisjunctiveFacetRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Add a numeric refinement on the given attribute
 * @param  {string} attribute
 * @param  {string} operator
 * @param  {number} value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.addNumericRefinement = function( attribute, operator, value ) {
  this.state = this.state.addNumericRefinement( attribute, operator, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Remove a numeric filter.
 * @param  {string} attribute
 * @param  {string} operator
 * @param  {number} value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.removeNumericRefinement = function( attribute, operator, value ) {
  this.state = this.state.removeNumericRefinement( attribute, operator, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Ensure a facet refinement exists
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.addRefine = function( facet, value ) {
  this.state = this.state.addFacetRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Ensure a facet refinement does not exist
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.removeRefine = function( facet, value ) {
  this.state = this.state.removeFacetRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Ensure a facet exclude exists
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.addExclude = function( facet, value ) {
  this.state = this.state.addExcludeRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Ensure a facet exclude does not exist
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.removeExclude = function( facet, value ) {
  this.state = this.state.removeExcludeRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Toggle refinement state of an exclude
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.toggleExclude = function( facet, value ) {
  this.state = this.state.toggleExcludeFacetRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Toggle refinement state of a facet
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.toggleRefine = function( facet, value ) {
  if( this.state.facets.indexOf( facet ) > -1 ) {
    this.state = this.state.toggleFacetRefinement( facet, value );
  }
  else if( this.state.disjunctiveFacets.indexOf( facet ) > -1 ) {
    this.state = this.state.toggleDisjunctiveFacetRefinement( facet, value );
  }
  else {
    /* eslint-disable */
    console.log( "warning : you're trying to refine the undeclared facet '" + facet +
                "'; add it to the helper options 'facets' or 'disjunctiveFacets'" );
    /* eslint-enable */
    return this;
  }
  this.emit( "change", this.state );
  return this;
};

/**
 * Go to next page
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.nextPage = function() {
  return this.setCurrentPage( this.state.page + 1 );
};

/**
 * Go to previous page
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.previousPage = function() {
  return this.setCurrentPage( this.state.page - 1 );
};

/**
 * Change the current page
 * @param  {integer} page The page number
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.setCurrentPage = function( page ) {
  if( page < 0 ) throw new Error( "Page requested below 0." );

  this.state = this.state.setPage( page );
  this.emit( "change", this.state );
  return this;
};

/**
 * Configure the underlying index name
 * @param {string} name the index name
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.setIndex = function( name ) {
  this.index = name;
  return this;
};

/**
 * Set the whole state ( warning : will erase previous state )
 * @param {SearchParameters} newState the whole new state
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.setState = function( newState ) {
  this.state = new SearchParameters( newState );
  this.emit( "change", this.state );
  return this;
};

/**
 * Check the refinement state of a facet
 * @param  {string}  facet the facet
 * @param  {string}  value the associated value
 * @return {boolean} true if refined
 */
AlgoliaSearchHelper.prototype.isRefined = function( facet, value ) {
  if( this.state.facets.indexOf( facet ) > -1 ) {
    return this.state.isFacetRefined( facet, value );
  }
  else if( this.state.disjunctiveFacets.indexOf( facet ) > -1 ) {
    return this.state.isDisjunctiveFacetRefined( facet, value );
  }
  return false;
};

/**
 * Check the exclude state of a facet
 * @param  {string}  facet the facet
 * @param  {string}  value the associated value
 * @return {boolean} true if refined
 */
AlgoliaSearchHelper.prototype.isExcluded = function( facet, value ) {
  return this.state.isExcludeRefined( facet, value );
};

/**
 * Check the refinement state of the disjunctive facet
 * @param  {string}  facet the facet
 * @param  {string}  value the associated value
 * @return {boolean} true if refined
 */
AlgoliaSearchHelper.prototype.isDisjunctiveRefined = function( facet, value ) {
  return this.state.isDisjunctiveFacetRefined( facet, value );
};

/**
 * Get the underlying configured index name
 */
AlgoliaSearchHelper.prototype.getIndex = function() {
  return this.index;
};

/**
 * Get the currently selected page
 * @return Number the current page
 */
AlgoliaSearchHelper.prototype.getCurrentPage = function() {
  return this.state.page;
};

///////////// PRIVATE

/**
 * Perform the underlying queries
 * @private
 */
AlgoliaSearchHelper.prototype._search = function() {
  var state = this.state;
  var queries = [];

  //One query for the hits
  queries.push( {
    indexName : this.index,
    query : state.query,
    params : this._getHitsSearchParams()
  } );

  //One for each disjunctive facets
  forEach( state.getRefinedDisjunctiveFacets(), function( refinedFacet ) {
    queries.push( {
      indexName : this.index,
      query : state.query,
      params : this._getDisjunctiveFacetSearchParams( refinedFacet )
    } );
  }, this );

  this.client.search( queries, bind( this._handleResponse, this, state ) );
};

/**
 * Transform the response as sent by the server and transform it into a user
 * usable objet that merge the results of all the batch requests.
 * @private
 * @param {SearchParameters} state state used for to generate the request
 * @param {Error} err error if any, null otherwise
 * @param {object} content content of the response
 */
AlgoliaSearchHelper.prototype._handleResponse = function( state, err, content ) {
  if ( err ) {
    this.emit( "error", err );
    return;
  }

  var formattedResponse = new SearchResults( state, content );

  this.emit( "result", formattedResponse );
};

/**
 * Build search parameters used to fetch hits
 * @private
 * @return {object.<string, any>}
 */
AlgoliaSearchHelper.prototype._getHitsSearchParams = function() {
  var facets = this.state.facets.concat( this.state.getUnrefinedDisjunctiveFacets() );
  var facetFilters = this._getFacetFilters();
  var numericFilters = this._getNumericFilters();
  var additionalParams = {
    facets : facets,
    distinct : false
  };

  if( facetFilters.length > 0 ) {
    additionalParams.facetFilters = facetFilters;
    additionalParams.distinct = this.state.distinct || false;
  }

  if( numericFilters.length > 0 ) {
    additionalParams.numericFilters = numericFilters;
  }

  return extend( this.state.getQueryParams(), additionalParams );
};

/**
 * Build search parameters used to fetch a disjunctive facet
 * @private
 * @param  {string} facet the associated facet name
 * @return {object}
 */
AlgoliaSearchHelper.prototype._getDisjunctiveFacetSearchParams = function( facet ) {
  var facetFilters = this._getFacetFilters( facet );
  var numericFilters = this._getNumericFilters( facet );
  var additionalParams = {
    hitsPerPage : 1,
    page : 0,
    attributesToRetrieve : [],
    attributesToHighlight : [],
    attributesToSnippet : [],
    facets : facet,
    distinct : false
  };

  if( numericFilters.length > 0 ) {
    additionalParams.numericFilters = numericFilters;
  }

  if( facetFilters.length > 0 ) {
    additionalParams.facetFilters = facetFilters;
    additionalParams.distinct = this.state.distinct || false;
  }

  return extend( this.state.getQueryParams(), additionalParams );
};

/**
 * Return the numeric filters in an algolia request fashion
 * @private
 * @return {array.<string>} the numeric filters in the algolia format
 */
AlgoliaSearchHelper.prototype._getNumericFilters = function( facetName ) {
  var numericFilters = [];
  forEach( this.state.numericRefinements, function( operators, attribute ) {
    forEach( operators, function( value, operator ) {
      if( facetName !== attribute ) {
        numericFilters.push( attribute + operator + value );
      }
    } );
  } );
  return numericFilters;
};

/**
 * Test if there are some disjunctive refinements on the facet
 * @private
 */
AlgoliaSearchHelper.prototype._hasDisjunctiveRefinements = function( facet ) {
  return this.state.disjunctiveRefinements[ facet ] &&
         this.state.disjunctiveRefinements[ facet ].length > 0;
};

/**
 * Build facetFilters parameter based on current refinements. The array returned
 * contains strings representing the facet filters in the algolia format.
 * @private
 * @param  {string} facet if set, the current disjunctive facet
 * @return {array.<string>}
 */
AlgoliaSearchHelper.prototype._getFacetFilters = function( facet ) {
  var facetFilters = [];

  forEach( this.state.facetsRefinements, function( facetValue, facetName ) {
    facetFilters.push( facetName + ":" + facetValue );
  } );

  forEach( this.state.facetsExcludes, function( facetValues, facetName ) {
    forEach( facetValues, function( facetValue ) {
      facetFilters.push( facetName + ":-" + facetValue );
    } );
  } );

  forEach( this.state.disjunctiveFacetsRefinements, function( facetValues, facetName ) {
    if( facetName === facet || !facetValues || facetValues.length === 0 ) return;
    var orFilters = [];
    forEach( facetValues, function( facetValue ) {
      orFilters.push( facetName + ":" + facetValue );
    } );
    facetFilters.push( orFilters );
  } );

  return facetFilters;
};

module.exports = AlgoliaSearchHelper;
<commit_msg>Set page to 0 when changing currrent index<commit_after>"use strict";
var SearchParameters = require( "./SearchParameters" );
var SearchResults = require( "./SearchResults" );
var extend = require( "./functions/extend" );
var util = require( "util" );
var events = require( "events" );
var forEach = require( "lodash/collection/forEach" );
var bind = require( "lodash/function/bind" );

/**
 * Initialize a new AlgoliaSearchHelper
 * @class
 * @classdesc The AlgoliaSearchHelper is a class that ease the management of the
 * search. It provides an event based interface for search callbacks :
 *  - change : when the internal search state is changed. This event contains a {SearchParameters} object
 *  - result : when the response is retrieved from Algolia and is processed. This event contains a {SearchResults} object
 *  - error  : when the response is an error. This event contains the error returned by the server.
 * @param  {AlgoliaSearch} client an AlgoliaSearch client
 * @param  {string} index the index name to query
 * @param  {SearchParameters | object} options an object defining the initial config of the search. It doesn't have to be a {SearchParamaters}, just an object containing the properties you need from it.
 */
function AlgoliaSearchHelper( client, index, options ) {
  this.client = client;
  this.index = index;
  this.state = new SearchParameters( options );
}

util.inherits( AlgoliaSearchHelper, events.EventEmitter );

/**
 * Start the search with the parameters set in the state.
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.search = function() {
  this._search();
  return this;
};

/**
 * Sets the query. Also sets the current page to 0.
 * @param  {string} q the user query
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.setQuery = function( q ) {
  this.state = this.state.setQuery( q );

  this.emit( "change", this.state );
  return this;
};

/**
 * Remove all refinements (disjunctive + conjunctive + excludes + numeric filters)
 * @param {string} [name] - If given, name of the facet / attribute on which  we want to remove all refinements
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.clearRefinements = function( name ) {
  this.state = this.state.clearRefinements( name );
  this.emit( "change", this.state );
  return this;
};

/**
 * Ensure a facet refinement exists
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.addDisjunctiveRefine = function( facet, value ) {
  this.state = this.state.addDisjunctiveFacetRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Ensure a facet refinement does not exist
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.removeDisjunctiveRefine = function( facet, value ) {
  this.state = this.state.removeDisjunctiveFacetRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Add a numeric refinement on the given attribute
 * @param  {string} attribute
 * @param  {string} operator
 * @param  {number} value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.addNumericRefinement = function( attribute, operator, value ) {
  this.state = this.state.addNumericRefinement( attribute, operator, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Remove a numeric filter.
 * @param  {string} attribute
 * @param  {string} operator
 * @param  {number} value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.removeNumericRefinement = function( attribute, operator, value ) {
  this.state = this.state.removeNumericRefinement( attribute, operator, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Ensure a facet refinement exists
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.addRefine = function( facet, value ) {
  this.state = this.state.addFacetRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Ensure a facet refinement does not exist
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.removeRefine = function( facet, value ) {
  this.state = this.state.removeFacetRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Ensure a facet exclude exists
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.addExclude = function( facet, value ) {
  this.state = this.state.addExcludeRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Ensure a facet exclude does not exist
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.removeExclude = function( facet, value ) {
  this.state = this.state.removeExcludeRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Toggle refinement state of an exclude
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.toggleExclude = function( facet, value ) {
  this.state = this.state.toggleExcludeFacetRefinement( facet, value );
  this.emit( "change", this.state );
  return this;
};

/**
 * Toggle refinement state of a facet
 * @param  {string} facet the facet to refine
 * @param  {string} value the associated value
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.toggleRefine = function( facet, value ) {
  if( this.state.facets.indexOf( facet ) > -1 ) {
    this.state = this.state.toggleFacetRefinement( facet, value );
  }
  else if( this.state.disjunctiveFacets.indexOf( facet ) > -1 ) {
    this.state = this.state.toggleDisjunctiveFacetRefinement( facet, value );
  }
  else {
    /* eslint-disable */
    console.log( "warning : you're trying to refine the undeclared facet '" + facet +
                "'; add it to the helper options 'facets' or 'disjunctiveFacets'" );
    /* eslint-enable */
    return this;
  }
  this.emit( "change", this.state );
  return this;
};

/**
 * Go to next page
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.nextPage = function() {
  return this.setCurrentPage( this.state.page + 1 );
};

/**
 * Go to previous page
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.previousPage = function() {
  return this.setCurrentPage( this.state.page - 1 );
};

/**
 * Change the current page
 * @param  {integer} page The page number
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.setCurrentPage = function( page ) {
  if( page < 0 ) throw new Error( "Page requested below 0." );

  this.state = this.state.setPage( page );
  this.emit( "change", this.state );
  return this;
};

/**
 * Configure the underlying index name
 * @param {string} name the index name
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.setIndex = function( name ) {
  this.index = name;
  this.setCurrentPage( 0 );
  return this;
};

/**
 * Set the whole state ( warning : will erase previous state )
 * @param {SearchParameters} newState the whole new state
 * @return {AlgoliaSearchHelper}
 */
AlgoliaSearchHelper.prototype.setState = function( newState ) {
  this.state = new SearchParameters( newState );
  this.emit( "change", this.state );
  return this;
};

/**
 * Check the refinement state of a facet
 * @param  {string}  facet the facet
 * @param  {string}  value the associated value
 * @return {boolean} true if refined
 */
AlgoliaSearchHelper.prototype.isRefined = function( facet, value ) {
  if( this.state.facets.indexOf( facet ) > -1 ) {
    return this.state.isFacetRefined( facet, value );
  }
  else if( this.state.disjunctiveFacets.indexOf( facet ) > -1 ) {
    return this.state.isDisjunctiveFacetRefined( facet, value );
  }
  return false;
};

/**
 * Check the exclude state of a facet
 * @param  {string}  facet the facet
 * @param  {string}  value the associated value
 * @return {boolean} true if refined
 */
AlgoliaSearchHelper.prototype.isExcluded = function( facet, value ) {
  return this.state.isExcludeRefined( facet, value );
};

/**
 * Check the refinement state of the disjunctive facet
 * @param  {string}  facet the facet
 * @param  {string}  value the associated value
 * @return {boolean} true if refined
 */
AlgoliaSearchHelper.prototype.isDisjunctiveRefined = function( facet, value ) {
  return this.state.isDisjunctiveFacetRefined( facet, value );
};

/**
 * Get the underlying configured index name
 */
AlgoliaSearchHelper.prototype.getIndex = function() {
  return this.index;
};

/**
 * Get the currently selected page
 * @return Number the current page
 */
AlgoliaSearchHelper.prototype.getCurrentPage = function() {
  return this.state.page;
};

///////////// PRIVATE

/**
 * Perform the underlying queries
 * @private
 */
AlgoliaSearchHelper.prototype._search = function() {
  var state = this.state;
  var queries = [];

  //One query for the hits
  queries.push( {
    indexName : this.index,
    query : state.query,
    params : this._getHitsSearchParams()
  } );

  //One for each disjunctive facets
  forEach( state.getRefinedDisjunctiveFacets(), function( refinedFacet ) {
    queries.push( {
      indexName : this.index,
      query : state.query,
      params : this._getDisjunctiveFacetSearchParams( refinedFacet )
    } );
  }, this );

  this.client.search( queries, bind( this._handleResponse, this, state ) );
};

/**
 * Transform the response as sent by the server and transform it into a user
 * usable objet that merge the results of all the batch requests.
 * @private
 * @param {SearchParameters} state state used for to generate the request
 * @param {Error} err error if any, null otherwise
 * @param {object} content content of the response
 */
AlgoliaSearchHelper.prototype._handleResponse = function( state, err, content ) {
  if ( err ) {
    this.emit( "error", err );
    return;
  }

  var formattedResponse = new SearchResults( state, content );

  this.emit( "result", formattedResponse );
};

/**
 * Build search parameters used to fetch hits
 * @private
 * @return {object.<string, any>}
 */
AlgoliaSearchHelper.prototype._getHitsSearchParams = function() {
  var facets = this.state.facets.concat( this.state.getUnrefinedDisjunctiveFacets() );
  var facetFilters = this._getFacetFilters();
  var numericFilters = this._getNumericFilters();
  var additionalParams = {
    facets : facets,
    distinct : false
  };

  if( facetFilters.length > 0 ) {
    additionalParams.facetFilters = facetFilters;
    additionalParams.distinct = this.state.distinct || false;
  }

  if( numericFilters.length > 0 ) {
    additionalParams.numericFilters = numericFilters;
  }

  return extend( this.state.getQueryParams(), additionalParams );
};

/**
 * Build search parameters used to fetch a disjunctive facet
 * @private
 * @param  {string} facet the associated facet name
 * @return {object}
 */
AlgoliaSearchHelper.prototype._getDisjunctiveFacetSearchParams = function( facet ) {
  var facetFilters = this._getFacetFilters( facet );
  var numericFilters = this._getNumericFilters( facet );
  var additionalParams = {
    hitsPerPage : 1,
    page : 0,
    attributesToRetrieve : [],
    attributesToHighlight : [],
    attributesToSnippet : [],
    facets : facet,
    distinct : false
  };

  if( numericFilters.length > 0 ) {
    additionalParams.numericFilters = numericFilters;
  }

  if( facetFilters.length > 0 ) {
    additionalParams.facetFilters = facetFilters;
    additionalParams.distinct = this.state.distinct || false;
  }

  return extend( this.state.getQueryParams(), additionalParams );
};

/**
 * Return the numeric filters in an algolia request fashion
 * @private
 * @return {array.<string>} the numeric filters in the algolia format
 */
AlgoliaSearchHelper.prototype._getNumericFilters = function( facetName ) {
  var numericFilters = [];
  forEach( this.state.numericRefinements, function( operators, attribute ) {
    forEach( operators, function( value, operator ) {
      if( facetName !== attribute ) {
        numericFilters.push( attribute + operator + value );
      }
    } );
  } );
  return numericFilters;
};

/**
 * Test if there are some disjunctive refinements on the facet
 * @private
 */
AlgoliaSearchHelper.prototype._hasDisjunctiveRefinements = function( facet ) {
  return this.state.disjunctiveRefinements[ facet ] &&
         this.state.disjunctiveRefinements[ facet ].length > 0;
};

/**
 * Build facetFilters parameter based on current refinements. The array returned
 * contains strings representing the facet filters in the algolia format.
 * @private
 * @param  {string} facet if set, the current disjunctive facet
 * @return {array.<string>}
 */
AlgoliaSearchHelper.prototype._getFacetFilters = function( facet ) {
  var facetFilters = [];

  forEach( this.state.facetsRefinements, function( facetValue, facetName ) {
    facetFilters.push( facetName + ":" + facetValue );
  } );

  forEach( this.state.facetsExcludes, function( facetValues, facetName ) {
    forEach( facetValues, function( facetValue ) {
      facetFilters.push( facetName + ":-" + facetValue );
    } );
  } );

  forEach( this.state.disjunctiveFacetsRefinements, function( facetValues, facetName ) {
    if( facetName === facet || !facetValues || facetValues.length === 0 ) return;
    var orFilters = [];
    forEach( facetValues, function( facetValue ) {
      orFilters.push( facetName + ":" + facetValue );
    } );
    facetFilters.push( orFilters );
  } );

  return facetFilters;
};

module.exports = AlgoliaSearchHelper;
<|endoftext|>


<commit_before>/*jshint globalstrict:false, strict:false, maxlen: 500 */
/*global assertEqual, assertTrue, AQL_EXPLAIN */

////////////////////////////////////////////////////////////////////////////////
/// @brief tests for optimizer rule use indexes
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2014-2014 triagens GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Max Neunhoeffer
/// @author Copyright 2014, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////

var internal = require("internal");
var jsunity = require("jsunity");
var helper = require("org/arangodb/aql-helper");
var removeAlwaysOnClusterRules = helper.removeAlwaysOnClusterRules;

////////////////////////////////////////////////////////////////////////////////
/// @brief test suite
////////////////////////////////////////////////////////////////////////////////

function optimizerRuleUseIndexRangeTester () {
  var ruleName = "use-indexes";
  var collBaseName = "UTUseIndexRange";
  var collNames = ["NoInd", "SkipInd", "HashInd", "BothInd"];
  var collNoInd;
  var collSkipInd;
  var collHashInd;
  var collBothInd;

  // various choices to control the optimizer:
  var paramNone     = { optimizer: { rules: [ "-all" ] } };
  var paramEnabled  = { optimizer: { rules: [ "-all", "+" + ruleName ] } };
  var paramAll      = { optimizer: { rules: [ "+all" ] } };
  var paramEnabledAllPlans = { optimizer: { rules: [ "-all", "+" + ruleName ]},
                               allPlans: true };
  var paramAllAllPlans = { optimizer: { rules: [ "+all" ]}, allPlans: true };

  return {

////////////////////////////////////////////////////////////////////////////////
/// @brief set up
////////////////////////////////////////////////////////////////////////////////

    setUp : function () {
      var n = collNames.map(function(x) { return collBaseName + x; });
      var colls = [];
      for (var i = 0; i < n.length; i++) {
        var collName = n[i], coll;
        internal.db._drop(collName);
        coll = internal.db._create(collName);
        for (var j = 0; j < 10; j++) {
          coll.insert({"a":j, "s":"s"+j});
        }
        colls.push(coll);
      }
      collNoInd = colls[0];
      collSkipInd = colls[1];
      collSkipInd.ensureSkiplist("a");
      collHashInd = colls[2];
      collHashInd.ensureHashIndex("a");
      collBothInd = colls[3];
      collBothInd.ensureHashIndex("a");
      collBothInd.ensureSkiplist("a");
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief tear down
////////////////////////////////////////////////////////////////////////////////

    tearDown : function () {
      var n = collNames.map(function(x) { return collBaseName + x; });
      for (var i = 0; i < n.length; i++) {
        internal.db._drop(n[i]);
      }
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that rule has no effect when explicitly disabled
////////////////////////////////////////////////////////////////////////////////

    testRuleDisabled : function () {
      var queries = [
        "FOR i IN UTUseIndexRangeNoInd FILTER i.a >= 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a >= 2 RETURN i",
        "FOR i IN UTUseIndexRangeHashInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a == 2 RETURN i",
      ];

      queries.forEach(function(query) {
        var result = AQL_EXPLAIN(query, { }, paramNone);
        assertEqual([ ], removeAlwaysOnClusterRules(result.plan.rules), query);
      });
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that rule has no effect
////////////////////////////////////////////////////////////////////////////////

    testRuleNoEffect : function () {
      var queries = [
        "FOR i IN UTUseIndexRangeNoInd FILTER i.a >= 2 RETURN i",
        "FOR i IN UTUseIndexRangeNoInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeHashInd FILTER i.a >= 2 RETURN i"
      ];

      queries.forEach(function(query) {
        var result = AQL_EXPLAIN(query, { }, paramEnabled);
        assertEqual([ ], removeAlwaysOnClusterRules(result.plan.rules), query);
      });
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that rule has an effect
////////////////////////////////////////////////////////////////////////////////

    testRuleHasEffect : function () {
      var queries = [
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a >= 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a < 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a < 2 && i.a > 0 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a < 2 && i.a > 3 RETURN i",
        "FOR i IN UTUseIndexRangeHashInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a <= 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a > 2 RETURN i"
      ];

      queries.forEach(function(query) {
        var result = AQL_EXPLAIN(query, { }, paramEnabled);
        assertEqual([ ruleName ], removeAlwaysOnClusterRules(result.plan.rules),
                    query);
      });
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that rule has an effect
////////////////////////////////////////////////////////////////////////////////

    testRuleHasEffectWithAll : function () {
      var queries = [
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a >= 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a < 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a < 2 && i.a > 0 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a < 2 && i.a > 3 RETURN i",
        "FOR i IN UTUseIndexRangeHashInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a <= 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a > 2 RETURN i"
      ];

      queries.forEach(function(query) {
        var result = AQL_EXPLAIN(query, { }, paramAll);
        assertTrue(result.plan.rules.indexOf(ruleName) >= 0, query);
      });
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that plan explosion does not happen
////////////////////////////////////////////////////////////////////////////////

    testRuleHasNoPlanExplosion : function () {
      var query =
        "LET A = (FOR a IN UTUseIndexRangeBothInd FILTER a.a == 2 RETURN a) "+
        "LET B = (FOR b IN UTUseIndexRangeBothInd FILTER b.a == 2 RETURN b) "+
        "LET C = (FOR c IN UTUseIndexRangeBothInd FILTER c.a == 2 RETURN c) "+
        "LET D = (FOR d IN UTUseIndexRangeBothInd FILTER d.a == 2 RETURN d) "+
        "LET E = (FOR e IN UTUseIndexRangeBothInd FILTER e.a == 2 RETURN e) "+
        "LET F = (FOR f IN UTUseIndexRangeBothInd FILTER f.a == 2 RETURN f) "+
        "LET G = (FOR g IN UTUseIndexRangeBothInd FILTER g.a == 2 RETURN g) "+
        "LET H = (FOR h IN UTUseIndexRangeBothInd FILTER h.a == 2 RETURN h) "+
        "LET I = (FOR i IN UTUseIndexRangeBothInd FILTER i.a == 2 RETURN i) "+
        "LET J = (FOR j IN UTUseIndexRangeBothInd FILTER j.a == 2 RETURN j) "+
        "FOR k IN UTUseIndexRangeBothInd FILTER k.a == 2 "+
        "  RETURN {A:A, B:B, C:C, D:D, E:E, F:F, G:G, H:H, I:I, J:J, K:k}";

      var result = AQL_EXPLAIN(query, { }, paramEnabledAllPlans);
      assertTrue(result.plans.length < 40, query);
      result = AQL_EXPLAIN(query, { }, paramAllAllPlans);
      assertTrue(result.plans.length < 40, query);
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that plan explosion does not happen
////////////////////////////////////////////////////////////////////////////////

    testRuleMakesAll : function () {
      var query =
        "LET A = (FOR a IN UTUseIndexRangeBothInd FILTER a.a == 2 RETURN a) "+
        "LET B = (FOR b IN UTUseIndexRangeBothInd FILTER b.a == 2 RETURN b) "+
        "LET C = (FOR c IN UTUseIndexRangeBothInd FILTER c.a == 2 RETURN c) "+
        "FOR k IN UTUseIndexRangeBothInd FILTER k.a == 2 "+
        "  RETURN {A:A, B:B, C:C, K:k}";

      var result = AQL_EXPLAIN(query, { }, paramEnabledAllPlans);
      assertTrue(result.plans.length === 1, query);
      result = AQL_EXPLAIN(query, { }, paramAllAllPlans);
      assertTrue(result.plans.length === 1, query);
    }

  };
}

////////////////////////////////////////////////////////////////////////////////
/// @brief executes the test suite
////////////////////////////////////////////////////////////////////////////////

jsunity.run(optimizerRuleUseIndexRangeTester);

return jsunity.done();

// Local Variables:
// mode: outline-minor
// outline-regexp: "^\\(/// @brief\\|/// @addtogroup\\|// --SECTION--\\|/// @page\\|/// @}\\)"
// End:
<commit_msg>Fixed the use-index-range test<commit_after>/*jshint globalstrict:false, strict:false, maxlen: 500 */
/*global assertEqual, assertTrue, AQL_EXPLAIN */

////////////////////////////////////////////////////////////////////////////////
/// @brief tests for optimizer rule use indexes
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2014-2014 triagens GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Max Neunhoeffer
/// @author Copyright 2014, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////

var internal = require("internal");
var jsunity = require("jsunity");
var helper = require("org/arangodb/aql-helper");
var removeAlwaysOnClusterRules = helper.removeAlwaysOnClusterRules;

////////////////////////////////////////////////////////////////////////////////
/// @brief test suite
////////////////////////////////////////////////////////////////////////////////

function optimizerRuleUseIndexRangeTester () {
  var ruleName = "use-indexes";
  var collBaseName = "UTUseIndexRange";
  var collNames = ["NoInd", "SkipInd", "HashInd", "BothInd"];
  var collNoInd;
  var collSkipInd;
  var collHashInd;
  var collBothInd;

  // various choices to control the optimizer:
  var paramNone     = { optimizer: { rules: [ "-all" ] } };
  var paramEnabled  = { optimizer: { rules: [ "-all", "+" + ruleName ] } };
  var paramAll      = { optimizer: { rules: [ "+all" ] } };
  var paramEnabledAllPlans = { optimizer: { rules: [ "-all", "+" + ruleName ]},
                               allPlans: true };
  var paramAllAllPlans = { optimizer: { rules: [ "+all" ]}, allPlans: true };

  return {

////////////////////////////////////////////////////////////////////////////////
/// @brief set up
////////////////////////////////////////////////////////////////////////////////

    setUp : function () {
      var n = collNames.map(function(x) { return collBaseName + x; });
      var colls = [];
      for (var i = 0; i < n.length; i++) {
        var collName = n[i], coll;
        internal.db._drop(collName);
        coll = internal.db._create(collName);
        for (var j = 0; j < 10; j++) {
          coll.insert({"a":j, "s":"s"+j});
        }
        colls.push(coll);
      }
      collNoInd = colls[0];
      collSkipInd = colls[1];
      collSkipInd.ensureSkiplist("a");
      collHashInd = colls[2];
      collHashInd.ensureHashIndex("a");
      collBothInd = colls[3];
      collBothInd.ensureHashIndex("a");
      collBothInd.ensureSkiplist("a");
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief tear down
////////////////////////////////////////////////////////////////////////////////

    tearDown : function () {
      var n = collNames.map(function(x) { return collBaseName + x; });
      for (var i = 0; i < n.length; i++) {
        internal.db._drop(n[i]);
      }
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that rule has no effect when explicitly disabled
////////////////////////////////////////////////////////////////////////////////

    testRuleDisabled : function () {
      var queries = [
        "FOR i IN UTUseIndexRangeNoInd FILTER i.a >= 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a >= 2 RETURN i",
        "FOR i IN UTUseIndexRangeHashInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a == 2 RETURN i",
      ];

      queries.forEach(function(query) {
        var result = AQL_EXPLAIN(query, { }, paramNone);
        assertEqual([ ], removeAlwaysOnClusterRules(result.plan.rules), query);
      });
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that rule has no effect
////////////////////////////////////////////////////////////////////////////////

    testRuleNoEffect : function () {
      var queries = [
        "FOR i IN UTUseIndexRangeNoInd FILTER i.a >= 2 RETURN i",
        "FOR i IN UTUseIndexRangeNoInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeHashInd FILTER i.a >= 2 RETURN i"
      ];

      queries.forEach(function(query) {
        var result = AQL_EXPLAIN(query, { }, paramEnabled);
        assertEqual([ ], removeAlwaysOnClusterRules(result.plan.rules), query);
      });
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that rule has an effect
////////////////////////////////////////////////////////////////////////////////

    testRuleHasEffect : function () {
      var queries = [
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a >= 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a < 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a < 2 && i.a > 0 RETURN i",
        "FOR i IN UTUseIndexRangeHashInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a <= 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a > 2 RETURN i"
      ];

      queries.forEach(function(query) {
        var result = AQL_EXPLAIN(query, { }, paramEnabled);
        assertEqual([ ruleName ], removeAlwaysOnClusterRules(result.plan.rules),
                    query);
      });
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that rule has an effect
////////////////////////////////////////////////////////////////////////////////

    testRuleHasEffectWithAll : function () {
      var queries = [
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a >= 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a < 2 RETURN i",
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a < 2 && i.a > 0 RETURN i",
        "FOR i IN UTUseIndexRangeHashInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a == 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a <= 2 RETURN i",
        "FOR i IN UTUseIndexRangeBothInd FILTER i.a > 2 RETURN i"
      ];

      queries.forEach(function(query) {
        var result = AQL_EXPLAIN(query, { }, paramAll);
        assertTrue(result.plan.rules.indexOf(ruleName) >= 0, query);
      });
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that impossible conditions are detected and optimized
////////////////////////////////////////////////////////////////////////////////

    testImpossibleRangesAreDetected : function () {
      var queries = [
        "FOR i IN UTUseIndexRangeSkipInd FILTER i.a < 2 && i.a > 3 RETURN i"
      ];

      queries.forEach(function(query) {
        var result = AQL_EXPLAIN(query, { }, paramAll);
        var foundNoResults = false;
        for (var t in result.plan.nodes) {
          if (result.plan.nodes[t].type === "NoResultsNode") {
            foundNoResults = true;
          }
        }
        assertTrue(foundNoResults, query);
      });
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that plan explosion does not happen
////////////////////////////////////////////////////////////////////////////////

    testRuleHasNoPlanExplosion : function () {
      var query =
        "LET A = (FOR a IN UTUseIndexRangeBothInd FILTER a.a == 2 RETURN a) "+
        "LET B = (FOR b IN UTUseIndexRangeBothInd FILTER b.a == 2 RETURN b) "+
        "LET C = (FOR c IN UTUseIndexRangeBothInd FILTER c.a == 2 RETURN c) "+
        "LET D = (FOR d IN UTUseIndexRangeBothInd FILTER d.a == 2 RETURN d) "+
        "LET E = (FOR e IN UTUseIndexRangeBothInd FILTER e.a == 2 RETURN e) "+
        "LET F = (FOR f IN UTUseIndexRangeBothInd FILTER f.a == 2 RETURN f) "+
        "LET G = (FOR g IN UTUseIndexRangeBothInd FILTER g.a == 2 RETURN g) "+
        "LET H = (FOR h IN UTUseIndexRangeBothInd FILTER h.a == 2 RETURN h) "+
        "LET I = (FOR i IN UTUseIndexRangeBothInd FILTER i.a == 2 RETURN i) "+
        "LET J = (FOR j IN UTUseIndexRangeBothInd FILTER j.a == 2 RETURN j) "+
        "FOR k IN UTUseIndexRangeBothInd FILTER k.a == 2 "+
        "  RETURN {A:A, B:B, C:C, D:D, E:E, F:F, G:G, H:H, I:I, J:J, K:k}";

      var result = AQL_EXPLAIN(query, { }, paramEnabledAllPlans);
      assertTrue(result.plans.length < 40, query);
      result = AQL_EXPLAIN(query, { }, paramAllAllPlans);
      assertTrue(result.plans.length < 40, query);
    },

////////////////////////////////////////////////////////////////////////////////
/// @brief test that plan explosion does not happen
////////////////////////////////////////////////////////////////////////////////

    testRuleMakesAll : function () {
      var query =
        "LET A = (FOR a IN UTUseIndexRangeBothInd FILTER a.a == 2 RETURN a) "+
        "LET B = (FOR b IN UTUseIndexRangeBothInd FILTER b.a == 2 RETURN b) "+
        "LET C = (FOR c IN UTUseIndexRangeBothInd FILTER c.a == 2 RETURN c) "+
        "FOR k IN UTUseIndexRangeBothInd FILTER k.a == 2 "+
        "  RETURN {A:A, B:B, C:C, K:k}";

      var result = AQL_EXPLAIN(query, { }, paramEnabledAllPlans);
      assertTrue(result.plans.length === 1, query);
      result = AQL_EXPLAIN(query, { }, paramAllAllPlans);
      assertTrue(result.plans.length === 1, query);
    }

  };
}

////////////////////////////////////////////////////////////////////////////////
/// @brief executes the test suite
////////////////////////////////////////////////////////////////////////////////

jsunity.run(optimizerRuleUseIndexRangeTester);

return jsunity.done();

// Local Variables:
// mode: outline-minor
// outline-regexp: "^\\(/// @brief\\|/// @addtogroup\\|// --SECTION--\\|/// @page\\|/// @}\\)"
// End:
<|endoftext|>


<commit_before>var WebAudiox	= WebAudiox	|| {}

WebAudiox.AbsoluteNormalizer	= function(){
	var maxThreshold	= -Infinity;
	var minThreshold	= +Infinity;
	this.update	= function(value){
		// TODO make be good to smooth those values over time, thus it would forget
		// it would be the adaptative
		// and this one being absolute
		if( value < minThreshold ) minThreshold	= value
		if( value > maxThreshold ) maxThreshold = value
		// to avoid division by zero
		if( maxThreshold === minThreshold )	return value;
		// compute normalized value
		var normalized	= (value - minThreshold) / (maxThreshold-minThreshold);
		// return the just built normalized value between [0, 1]
		return normalized;
	}
}

var WebAudiox	= WebAudiox	|| {}

// TODO to rewrite with a simple weight average on a history array
// - simple and no magic involved

WebAudiox.AdaptativeNormalizer	= function(factorForMin, factorForMax){
	var minThreshold	= 0;
	var maxThreshold	= 1;
	this.update	= function(value){
		// smooth adapatation
		var smoothOut	= 0.01
		var smoothIn	= 0.01
		if( value < minThreshold )	minThreshold += (value-minThreshold)*smoothOut
		else				minThreshold += (value-minThreshold)*smoothIn
		if( value > maxThreshold )	maxThreshold += (value-maxThreshold)*smoothOut
		else				maxThreshold += (value-maxThreshold)*smoothIn
		// ensure bound are respected
		if( value < minThreshold ) value = minThreshold
		if( value > maxThreshold ) value = maxThreshold
		// to avoid division by zero
		if( maxThreshold === minThreshold )	return value;
		// compute normalized value
console.log(minThreshold.toFixed(10),maxThreshold.toFixed(10))
		var normalized	= (value - minThreshold) / (maxThreshold-minThreshold);
		// return the just built normalized value between [0, 1]
		return normalized;
	}
}

// @namespace defined WebAudiox name space
var WebAudiox	= WebAudiox	|| {}

/**
 * do a average on a ByteFrequencyData from an analyser node
 * @param  {[type]} analyser the analyser node
 * @param  {Number} width    how many elements of the array will be considered
 * @param  {Number} offset   the index of the element to consider
 * @return {Number}          the ByteFrequency average
 */
WebAudiox.analyserAverage	= function(analyser, width, offset){
	// handle paramerter
	width		= width !== undefined ? width	: analyser.frequencyBinCount;
	offset		= offset !== undefined ? offset	: 0;
	// inint variable
	var freqByte	= new Uint8Array(analyser.frequencyBinCount);
	// get the frequency data
	analyser.getByteFrequencyData(freqByte);
	// compute the sum
	var sum	= 0;
	for(var i = offset; i < offset+width; i++){
		sum	+= freqByte[i];
	}
	// complute the amplitude
	var amplitude	= sum / (width*256-1);
	// return ampliture
	return amplitude;
}
var WebAudiox	= WebAudiox	|| {}

WebAudiox.ByteToNormalizedFloat32Array	= function(srcArray, dstArray){
	var ratio	= srcArray.length / dstArray.length
	for(var i = 0; i < dstArray.length; i++){
		var first	= Math.round((i+0) * ratio)
		var last	= Math.round((i+1) * ratio)
		last		= Math.min(srcArray.length-1, last)
		for(var j = first, sum = 0; j <= last; j++){
			sum	+= srcArray[j]/256;
		}
		dstArray[i]	= sum/(last-first+1);
	}
}
var WebAudiox	= WebAudiox	|| {}

/**
 * generate buffer with jsfx.js
 * @param  {AudioContext} context the WebAudio API context
 * @param  {Array} lib     parameter for jsfx
 * @return {[type]}         the just built buffer
 */
WebAudiox.getBufferFromJsfx	= function(context, lib){
	var params	= jsfxlib.arrayToParams(lib);
	var data	= jsfx.generate(params);
	var buffer	= context.createBuffer(1, data.length, 44100);
	var fArray	= buffer.getChannelData(0);
	for(var i = 0; i < fArray.length; i++){
		fArray[i]	= data[i];
	}
	return buffer;
}
/**
 * @namespace definition of WebAudiox
 * @type {object}
 */
var WebAudiox	= WebAudiox	|| {}

/**
 * definition of a lineOut
 * @constructor
 * @param  {AudioContext} context WebAudio API context
 */
WebAudiox.LineOut	= function(context){
	// init this.destination
	this.destination= context.destination

	// this.destination to support muteWithVisibility
	var visibilityGain	= context.createGain()
	visibilityGain.connect(this.destination)
	muteWithVisibility(visibilityGain)
	this.destination= visibilityGain

	// this.destination to support webAudiox.toggleMute() and webAudiox.isMuted
	var muteGain	= context.createGain()
	muteGain.connect(this.destination)
	this.destination= muteGain
	this.isMuted	= false
	this.toggeMute= function(){
		this.isMuted		= this.isMuted ? false : true;
		muteGain.gain.value	= this.isMuted ? 0 : 1;
	}.bind(this)

	//  to support webAudiox.volume
	var volumeNode	= context.createGain()
	volumeNode.connect( this.destination )
	this.destination= volumeNode
	Object.defineProperty(this, 'volume', {
		get : function(){
			return volumeNode.gain.value;
		},
                set : function(value){

                	volumeNode.gain.value	= value;
		}
	});

	return;

	//////////////////////////////////////////////////////////////////////////////////
	//		muteWithVisibility helper					//
	//////////////////////////////////////////////////////////////////////////////////
	/**
	 * mute a gainNode when the page isnt visible
	 * @param  {Node} gainNode the gainNode to mute/unmute
	 */
	function muteWithVisibility(gainNode){
		// shim to handle browser vendor
		var eventStr	= (document.hidden !== undefined	? 'visibilitychange'	:
			(document.mozHidden	!== undefined		? 'mozvisibilitychange'	:
			(document.msHidden	!== undefined		? 'msvisibilitychange'	:
			(document.webkitHidden	!== undefined		? 'webkitvisibilitychange' :
			console.assert(false, "Page Visibility API unsupported")
		))));
		var documentStr	= (document.hidden !== undefined ? 'hidden' :
			(document.mozHidden	!== undefined ? 'mozHidden' :
			(document.msHidden	!== undefined ? 'msHidden' :
			(document.webkitHidden	!== undefined ? 'webkitHidden' :
			console.assert(false, "Page Visibility API unsupported")
		))));
		// event handler for visibilitychange event
		var callback	= function(){
			var isHidden	= document[documentStr] ? true : false
			gainNode.gain.value	= isHidden ? 0 : 1
		}.bind(this)
		// bind the event itself
		document.addEventListener(eventStr, callback, false)
		// destructor
		this.destroy	= function(){
			document.removeEventListener(eventStr, callback, false)
		}
	}
}
var WebAudiox	= WebAudiox	|| {}

/**
 * Helper to load a buffer
 *
 * @param  {AudioContext} context the WebAudio API context
 * @param  {String} url     the url of the sound to load
 * @param  {Function} onLoad  callback to notify when the buffer is loaded and decoded
 * @param  {Function} onError callback to notify when an error occured
 */
WebAudiox.loadBuffer	= function(context, url, onLoad, onError){
	var request	= new XMLHttpRequest()
	request.open('GET', url, true)
	request.responseType	= 'arraybuffer'
	// counter inProgress request
	WebAudiox.loadBuffer.inProgressCount++
	request.onload	= function(){
		context.decodeAudioData(request.response, function(buffer){
			// counter inProgress request
			WebAudiox.loadBuffer.inProgressCount--
			// notify the callback
			onLoad && onLoad(buffer)
			// notify
			WebAudiox.loadBuffer.onLoad(context, url, buffer)
		}, function(){
			// notify the callback
			onError && onError()
			// counter inProgress request
			WebAudiox.loadBuffer.inProgressCount--
		})
	}
	request.send()
}

/**
 * global onLoad callback. it is notified everytime .loadBuffer() load something
 * @param  {AudioContext} context the WebAudio API context
 * @param  {String} url     the url of the sound to load
 * @param {[type]} buffer the just loaded buffer
 */
WebAudiox.loadBuffer.onLoad	= function(context, url, buffer){}

/**
 * counter of all the .loadBuffer in progress. usefull to know is all your sounds
 * as been loaded
 * @type {Number}
 */
WebAudiox.loadBuffer.inProgressCount	= 0

/**
 * shim to get AudioContext
 */
window.AudioContext	= window.AudioContext || window.webkitAudioContext;
// @namespace
var WebAudiox	= WebAudiox	|| {}

/**
 * update webaudio context listener with three.Object3D position
 *
 * @constructor
 * @param  {AudioContext} context  the webaudio api context
 * @param  {THREE.Object3D} object3d the object for the listenre
 */
WebAudiox.ListenerObject3DUpdater	= function(context, object3d){
	var prevPosition= null
	this.update	= function(delta, now){
		// ensure object3d.matrixWorld is up to date
		object3d.updateMatrixWorld()
		// get matrixWorld
		var matrixWorld	= object3d.matrixWorld
		////////////////////////////////////////////////////////////////////////
		// set position
		var position	= new THREE.Vector3().getPositionFromMatrix(matrixWorld)
		context.listener.setPosition(position.x, position.y, position.z)

		////////////////////////////////////////////////////////////////////////
		// set orientation
		var mOrientation= matrixWorld.clone();
		// zero the translation
		mOrientation.setPosition({x : 0, y: 0, z: 0});
		// Compute Front vector: Multiply the 0,0,1 vector by the world matrix and normalize the result.
		var vFront= new THREE.Vector3(0,0,1);
		vFront.applyMatrix4(mOrientation)
		vFront.normalize();
		// Compute UP vector: Multiply the 0,-1,0 vector by the world matrix and normalize the result.
		var vUp= new THREE.Vector3(0,-1, 0);
		vUp.applyMatrix4(mOrientation)
		vUp.normalize();
		// Set panner orientation
		context.listener.setOrientation(vFront.x, vFront.y, vFront.z, vUp.x, vUp.y, vUp.z);

		////////////////////////////////////////////////////////////////////////
		// set velocity
		if( prevPosition === null ){
			prevPosition	= new THREE.Vector3().getPositionFromMatrix(matrixWorld);
		}else{
			var position	= new THREE.Vector3().getPositionFromMatrix(matrixWorld);
			var velocity	= position.clone().sub(prevPosition).divideScalar(delta);
			prevPosition.copy(position)
			context.listener.setVelocity(velocity.x, velocity.y, velocity.z);
		}
	}
}

/**
 * update panner position based on a object3d position
 *
 * @constructor
 * @param  {[type]} panner   the panner node to update
 * @param  {THREE.Object3D} object3d the object from which we take the position
 */
WebAudiox.PannerObject3DUpdater	= function(panner, object3d){
	var prevPosition= null
	this.update	= function(delta, now){
		// ensure object3d.matrixWorld is up to date
		object3d.updateMatrixWorld()
		// get matrixWorld
		var matrixWorld	= object3d.matrixWorld

		////////////////////////////////////////////////////////////////////////
		// set position
		var position	= new THREE.Vector3().getPositionFromMatrix(matrixWorld)
		panner.setPosition(position.x, position.y, position.z)

		////////////////////////////////////////////////////////////////////////
		// set orientation
		var vOrientation= new THREE.Vector3(0,0,1);
		var mOrientation= matrixWorld.clone();
		// zero the translation
		mOrientation.setPosition({x : 0, y: 0, z: 0});
		// Multiply the 0,0,1 vector by the world matrix and normalize the result.
		vOrientation.applyMatrix4(mOrientation)
		vOrientation.normalize();
		// Set panner orientation
		panner.setOrientation(vOrientation.x, vOrientation.y, vOrientation.z);

		////////////////////////////////////////////////////////////////////////
		// set velocity
		if( prevPosition === null ){
			prevPosition	= new THREE.Vector3().getPositionFromMatrix(matrixWorld);
		}else{
			var position	= new THREE.Vector3().getPositionFromMatrix(matrixWorld);
			var velocity	= position.clone().sub(prevPosition).divideScalar(delta);
			prevPosition.copy( position )
			panner.setVelocity(velocity.x, velocity.y, velocity.z);
		}
	}
}

<commit_msg>remove old build<commit_after><|endoftext|>


<commit_before>#!/usr/bin/env node
var fs = require('fs')
var GitHubSlugger = require('github-slugger')
var markybars = require('./lib/markybars.js')
var path = require('path')
var pullOrClone = require('./lib/pull-or-clone.js')
var join = path.join
var sh = require('shelljs')
var resolve = require('path').resolve

var awesomePath = join('tmp/awesome-standard')
var buildPath = 'build'
var logoPath = join(buildPath, 'docs')
var demoPath = join('tmp/standard-demo')
var mdPath = join('tmp/markdown')
var stdPath = join('tmp/standard')
var stdDocsPath = join('tmp/standard/docs')
var page = join('layout/page.html')
var demoPage = join('layout/demo.html')

var partials = {
  toc: join('layout/partials/toc.html'),
  ga: join('layout/partials/ga.html')
}

var slugger = new GitHubSlugger()

if (!sh.which('git')) {
  sh.echo('Sorry, this script requires git')
  sh.exit(1)
}

pullOrClone('https://github.com/standard/standard', stdPath)
pullOrClone('https://github.com/standard/awesome-standard', awesomePath)
pullOrClone('https://github.com/flet/standard-demo', demoPath)

sh.rm('-rf', buildPath)
sh.rm('-rf', mdPath)

sh.mkdir(buildPath)
sh.mkdir('-p', logoPath)
sh.mkdir(mdPath)

sh.cp('-f', join('markdown/*.md'), mdPath)
sh.cp('-f', join(stdPath, '*.md'), mdPath)
sh.cp('-f', join(stdDocsPath, '*.md'), mdPath)
sh.cp('-f', join(awesomePath, 'README.md'), join(mdPath, 'awesome.md'))
sh.cp('-R', join(stdDocsPath, 'logos'), logoPath)
var genPage = markybars.compile(page, partials)
var genDemo = markybars.compile(demoPage, partials)

var files = sh.ls(mdPath)
files.forEach(function (file) {
  var fileData = fs.readFileSync(join(mdPath, file), 'utf8')
  var name = path.parse(file).name
  var gen = name === 'demo' ? genDemo : genPage

  var htmlData = gen({data: fileData, name: name.toLowerCase()})
  var fileName = slugger.slug(name) + '.html'

  fs.writeFileSync(join(buildPath, fileName), htmlData, 'utf8')
})

sh.find(buildPath)
  .filter(function (file) { return file.match(/\.html$/) })
  .forEach(function (f) {
    // replace all RULES.md instances in links with rules.html
    sh.sed('-i', /"(.*?)RULES(.*?)\.md/g, '"rules$2.html', f)

    sh.sed('-i', /"\.\.\/README.md/g, '"index.html', f)
    sh.sed('-i', /"(.*?)README(.*?)\.md/g, '"readme$2.html', f)

    sh.sed('-i', /"(docs\/|\.\.\/)?webstorm(.*?)\.md/g, '"webstorm$2.html', f)
  })

// rename files to be internet friendly
sh.mv('-f', join(buildPath, 'readme.html'), join(buildPath, 'index.html'))

// once everything is built, copy it to dist
sh.rm('dist/*.html')
sh.cp('-R', buildPath + '/', resolve(__dirname, 'dist'))

// copy standard-demo bundle.js to dist
sh.cp('-f', join(demoPath, 'bundle.js'), resolve(__dirname, 'dist', 'standard-demo.js'))

// copy favicons to dist
sh.cp('-R', 'favicons', resolve(__dirname, 'dist'))

// copy main.css to dist
sh.cp('layout/main.css', resolve(__dirname, 'dist'))

// remove tmp dir
sh.rm('-rf', 'tmp')
<commit_msg>standard<commit_after>#!/usr/bin/env node
var fs = require('fs')
var GitHubSlugger = require('github-slugger')
var markybars = require('./lib/markybars.js')
var path = require('path')
var pullOrClone = require('./lib/pull-or-clone.js')
var join = path.join
var sh = require('shelljs')
var resolve = require('path').resolve

var awesomePath = join('tmp/awesome-standard')
var buildPath = 'build'
var logoPath = join(buildPath, 'docs')
var demoPath = join('tmp/standard-demo')
var mdPath = join('tmp/markdown')
var stdPath = join('tmp/standard')
var stdDocsPath = join('tmp/standard/docs')
var page = join('layout/page.html')
var demoPage = join('layout/demo.html')

var partials = {
  toc: join('layout/partials/toc.html'),
  ga: join('layout/partials/ga.html')
}

var slugger = new GitHubSlugger()

if (!sh.which('git')) {
  sh.echo('Sorry, this script requires git')
  sh.exit(1)
}

pullOrClone('https://github.com/standard/standard', stdPath)
pullOrClone('https://github.com/standard/awesome-standard', awesomePath)
pullOrClone('https://github.com/flet/standard-demo', demoPath)

sh.rm('-rf', buildPath)
sh.rm('-rf', mdPath)

sh.mkdir(buildPath)
sh.mkdir('-p', logoPath)
sh.mkdir(mdPath)

sh.cp('-f', join('markdown/*.md'), mdPath)
sh.cp('-f', join(stdPath, '*.md'), mdPath)
sh.cp('-f', join(stdDocsPath, '*.md'), mdPath)
sh.cp('-f', join(awesomePath, 'README.md'), join(mdPath, 'awesome.md'))
sh.cp('-R', join(stdDocsPath, 'logos'), logoPath)
var genPage = markybars.compile(page, partials)
var genDemo = markybars.compile(demoPage, partials)

var files = sh.ls(mdPath)
files.forEach(function (file) {
  var fileData = fs.readFileSync(join(mdPath, file), 'utf8')
  var name = path.parse(file).name
  var gen = name === 'demo' ? genDemo : genPage

  var htmlData = gen({ data: fileData, name: name.toLowerCase() })
  var fileName = slugger.slug(name) + '.html'

  fs.writeFileSync(join(buildPath, fileName), htmlData, 'utf8')
})

sh.find(buildPath)
  .filter(function (file) { return file.match(/\.html$/) })
  .forEach(function (f) {
    // replace all RULES.md instances in links with rules.html
    sh.sed('-i', /"(.*?)RULES(.*?)\.md/g, '"rules$2.html', f)

    sh.sed('-i', /"\.\.\/README.md/g, '"index.html', f)
    sh.sed('-i', /"(.*?)README(.*?)\.md/g, '"readme$2.html', f)

    sh.sed('-i', /"(docs\/|\.\.\/)?webstorm(.*?)\.md/g, '"webstorm$2.html', f)
  })

// rename files to be internet friendly
sh.mv('-f', join(buildPath, 'readme.html'), join(buildPath, 'index.html'))

// once everything is built, copy it to dist
sh.rm('dist/*.html')
sh.cp('-R', buildPath + '/', resolve(__dirname, 'dist'))

// copy standard-demo bundle.js to dist
sh.cp('-f', join(demoPath, 'bundle.js'), resolve(__dirname, 'dist', 'standard-demo.js'))

// copy favicons to dist
sh.cp('-R', 'favicons', resolve(__dirname, 'dist'))

// copy main.css to dist
sh.cp('layout/main.css', resolve(__dirname, 'dist'))

// remove tmp dir
sh.rm('-rf', 'tmp')
<|endoftext|>


<commit_before>var height = 500;
var width = 750;
var margin = {left: 0, right: 0, top: 15, bottom: 15};
var topLevel = 10;

var svg = d3.select('#recursion')
  .append('svg')
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")

var levelToHeight = d3.scale.linear()
    .domain([topLevel, 0])
    .range([0, height]);

var tree = {i: topLevel, left: 0, right: width};
tree.parent = tree;

addChildren(tree);

function addChildren(obj){
  if (obj.i === 0 || obj.i === 1){
    obj.val = obj.i;
    return obj;
  }

  var mid = (obj.left + obj.right)/2;
  obj.children = [
      addChildren({i: obj.i - 1, parent: obj, left: obj.left, right: mid}),
      addChildren({i: obj.i - 2, parent: obj, left: mid,      right: obj.right}),
    ];

  obj.val = d3.sum(obj.children, ƒ('val'))
  return obj;
}

function drawCircle(obj){
  var childDrawn = false;
  svg.append('circle')
      .attr('r', 10)
      .on('mouseover', function(){
        if (childDrawn) return;
        if (!obj.children) return;
        childDrawn = true;
        drawCircle(obj.children[0])
        drawCircle(obj.children[1])
        d3.select(this).style('fill', 'lightgrey')
      })
      .attr('cx', objToX(obj.parent))
      .attr('cy', objToY(obj.parent))
      .style('pointer-events', 'none')
      .style('fill', 'steelblue')
    .transition()
      .attr('cx', objToX(obj))
      .attr('cy', objToY(obj))
      .each('end', function(){
        d3.select(this).style('pointer-events', 'all')
      })

  svg.append('line')
      .attr(
        { x1: objToX(obj.parent),
          y1: objToY(obj.parent),
          x2: objToX(obj.parent),
          y2: objToY(obj.parent) })
    .transition()
      .attr(
        { x2: objToX(obj),
          y2: objToY(obj) })
      .style({stroke: 'black', "stroke-width": 3});

}

function objToX(obj){ return (obj.left + obj.right)/2; }
function objToY(obj){ return levelToHeight(obj.i); }

drawCircle(tree);<commit_msg>removes obj function<commit_after>var height = 500;
var width = 750;
var margin = {left: 0, right: 0, top: 15, bottom: 15};
var topLevel = 10;

var svg = d3.select('#recursion')
  .append('svg')
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")

var levelToHeight = d3.scale.linear()
    .domain([topLevel, 0])
    .range([0, height]);

var tree = {i: topLevel, left: 0, right: width};
tree.parent = tree;

addChildren(tree);

function addChildren(obj){
  obj.x = (obj.left + obj.right)/2;
  obj.y = levelToHeight(obj.i);

  if (obj.i === 0 || obj.i === 1){
    obj.val = obj.i;
    return obj;
  }

  var mid = (obj.left + obj.right)/2;
  obj.children = [
      addChildren({i: obj.i - 1, parent: obj, left: obj.left, right: mid}),
      addChildren({i: obj.i - 2, parent: obj, left: mid,      right: obj.right}),
    ];

  obj.val = d3.sum(obj.children, ƒ('val'))
  return obj;
}

function drawCircle(obj){
  var childDrawn = false;
  svg.append('circle')
      .attr('r', 10)
      .on('mouseover', function(){
        if (childDrawn) return;
        if (!obj.children) return;
        childDrawn = true;
        drawCircle(obj.children[0])
        drawCircle(obj.children[1])
        d3.select(this).style('fill', 'lightgrey')
      })
      .attr('cx', obj.parent.x)
      .attr('cy', obj.parent.y)
      .style('pointer-events', 'none')
      .style('fill', 'steelblue')
    .transition()
      .attr('cx', obj.x)
      .attr('cy', obj.y)
      .each('end', function(){
        d3.select(this).style('pointer-events', 'all')
      })

  svg.append('line')
      .attr(
        { x1: obj.parent.x,
          y1: obj.parent.y,
          x2: obj.parent.x,
          y2: obj.parent.y })
    .transition()
      .attr(
        { x2: obj.x,
          y2: obj.y })
      .style({stroke: 'black', "stroke-width": 3});

}

function objToX(obj){ return (obj.left + obj.right)/2; }
function objToY(obj){ return levelToHeight(obj.i); }

drawCircle(tree);<|endoftext|>


<commit_before>/*    Copyright 2021 Firewalla LLC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);

const Sensor = require('./Sensor.js').Sensor;

const f = require('../net2/Firewalla.js');

const userConfigFolder = f.getUserConfigFolder();
const dnsmasqConfigFolder = `${userConfigFolder}/dnsmasq`;
const systemConfigFile = `${dnsmasqConfigFolder}/clash_system.conf`;

const fs = require('fs');
const Promise = require('bluebird');
Promise.promisifyAll(fs);

const DNSMASQ = require('../extension/dnsmasq/dnsmasq.js');
const dnsmasq = new DNSMASQ();

const exec = require('child-process-promise').exec;

const sysManager = require('../net2/SysManager.js');

const extensionManager = require('./ExtensionManager.js');

const featureName = "clash";
const policyKeyName = "clash";

const clash = require('../extension/clash_tun/clash_tun.js');

const platformLoader = require('../platform/PlatformLoader.js');
const platform = platformLoader.getPlatform();

class ClashPlugin extends Sensor {
  async run() {
    if(platform.getName() !== 'gold') {
      return;
    }

    this.adminSystemSwitch = false;
    await exec(`mkdir -p ${dnsmasqConfigFolder}`);
    extensionManager.registerExtension(policyKeyName, this, {
      applyPolicy: this.applyPolicy
    });

    this.hookFeature(featureName);

  }

  async applyAll(options = {}) {
    log.info("Applying...");
    const config = await this.getFeatureConfig();
    clash.config = Object.assign({}, config,  {dns: sysManager.myDefaultDns()});
    await clash.start();
    this.ready = true;

    await this.applyClash();
  }

  async applyPolicy(host, ip, policy) {
    log.info("Applying clash policy:", ip, policy);
    if (ip === '0.0.0.0') {
      // not supported
      return;
    }

    if (!host)
      return;

    // only support Host
    if (host.constructor.name !== "Host") {
      return;
    }

    const macAddress = host && host.o && host.o.mac;
    if (!macAddress) {
      return;
    }

    if (policy === false) {
      await exec(`sudo ipset -! add fw_clash_whitelist_mac ${macAddress}`).catch(() => {});
    } else {
      await exec(`sudo ipset -! del fw_clash_whitelist_mac ${macAddress}`).catch(() => {});
    }
  }

  async applyClash() {
    if (!this.ready) {
      log.info("Service clash is not ready.");
      return;
    }

    if (this.adminSystemSwitch) {
      return this.systemStart();
    } else {
      return this.systemStop();
    }
  }

  async systemStart() {
    log.info("Starting Clash at global level...");
    const entry = "server=127.0.0.1#9953\n";
    await fs.writeFileAsync(systemConfigFile, entry);
    await dnsmasq.scheduleRestartDNSService();

    await clash.redirectTraffic();
  }

  async systemStop() {
    log.info("Stopping Clash at global level...");
    await fs.unlinkAsync(systemConfigFile).catch(() => undefined);
    dnsmasq.scheduleRestartDNSService();

    await clash.stop();
  }

  // global on/off
  async globalOn(options) {
    this.adminSystemSwitch = true;
    await this.applyAll(options);
  }

  async globalOff() {
    this.adminSystemSwitch = false;
    await clash.stop();
    await this.applyClash();
  }

}

module.exports = ClashPlugin;
<commit_msg>remove clash restriction<commit_after>/*    Copyright 2021 Firewalla LLC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);

const Sensor = require('./Sensor.js').Sensor;

const f = require('../net2/Firewalla.js');

const userConfigFolder = f.getUserConfigFolder();
const dnsmasqConfigFolder = `${userConfigFolder}/dnsmasq`;
const systemConfigFile = `${dnsmasqConfigFolder}/clash_system.conf`;

const fs = require('fs');
const Promise = require('bluebird');
Promise.promisifyAll(fs);

const DNSMASQ = require('../extension/dnsmasq/dnsmasq.js');
const dnsmasq = new DNSMASQ();

const exec = require('child-process-promise').exec;

const sysManager = require('../net2/SysManager.js');

const extensionManager = require('./ExtensionManager.js');

const featureName = "clash";
const policyKeyName = "clash";

const clash = require('../extension/clash_tun/clash_tun.js');

const platformLoader = require('../platform/PlatformLoader.js');
const platform = platformLoader.getPlatform();

class ClashPlugin extends Sensor {
  async run() {
    this.adminSystemSwitch = false;
    await exec(`mkdir -p ${dnsmasqConfigFolder}`);
    extensionManager.registerExtension(policyKeyName, this, {
      applyPolicy: this.applyPolicy
    });

    this.hookFeature(featureName);

  }

  async applyAll(options = {}) {
    log.info("Applying...");
    const config = await this.getFeatureConfig();
    clash.config = Object.assign({}, config,  {dns: sysManager.myDefaultDns()});
    await clash.start();
    this.ready = true;

    await this.applyClash();
  }

  async applyPolicy(host, ip, policy) {
    log.info("Applying clash policy:", ip, policy);
    if (ip === '0.0.0.0') {
      // not supported
      return;
    }

    if (!host)
      return;

    // only support Host
    if (host.constructor.name !== "Host") {
      return;
    }

    const macAddress = host && host.o && host.o.mac;
    if (!macAddress) {
      return;
    }

    if (policy === false) {
      await exec(`sudo ipset -! add fw_clash_whitelist_mac ${macAddress}`).catch(() => {});
    } else {
      await exec(`sudo ipset -! del fw_clash_whitelist_mac ${macAddress}`).catch(() => {});
    }
  }

  async applyClash() {
    if (!this.ready) {
      log.info("Service clash is not ready.");
      return;
    }

    if (this.adminSystemSwitch) {
      return this.systemStart();
    } else {
      return this.systemStop();
    }
  }

  async systemStart() {
    log.info("Starting Clash at global level...");
    const entry = "server=127.0.0.1#9953\n";
    await fs.writeFileAsync(systemConfigFile, entry);
    await dnsmasq.scheduleRestartDNSService();

    await clash.redirectTraffic();
  }

  async systemStop() {
    log.info("Stopping Clash at global level...");
    await fs.unlinkAsync(systemConfigFile).catch(() => undefined);
    dnsmasq.scheduleRestartDNSService();

    await clash.stop();
  }

  // global on/off
  async globalOn(options) {
    this.adminSystemSwitch = true;
    await this.applyAll(options);
  }

  async globalOff() {
    this.adminSystemSwitch = false;
    await clash.stop();
    await this.applyClash();
  }

}

module.exports = ClashPlugin;
<|endoftext|>


<commit_before>import Bluebird, {promisify} from 'bluebird'
import Collection from 'xo-collection'
import createDebug from 'debug'
import forEach from 'lodash.foreach'
import startsWith from 'lodash.startswith'
import {BaseError} from 'make-error'
import {
  createClient as createXmlRpcClient,
  createSecureClient as createSecureXmlRpcClient
} from 'xmlrpc'
import {EventEmitter} from 'events'

const debug = createDebug('xen-api')

// ===================================================================

// http://www.gnu.org/software/libc/manual/html_node/Error-Codes.html
const NETWORK_ERRORS = {
  // Connection has been closed outside of our control.
  ECONNRESET: true,

  // Connection has been aborted locally.
  ECONNABORTED: true,

  // Host is up but refuses connection (typically: no such service).
  ECONNREFUSED: true,

  // TODO: ??
  EINVAL: true,

  // Host is not reachable (does not respond).
  EHOSTUNREACH: true,

  // Connection configured timed out has been reach.
  ETIMEDOUT: true
}

const isNetworkError = (error) => NETWORK_ERRORS[error.code]

// -------------------------------------------------------------------

const XAPI_NETWORK_ERRORS = {
  HOST_STILL_BOOTING: true,
  HOST_HAS_NO_MANAGEMENT_IP: true
}

const isXapiNetworkError = (error) => XAPI_NETWORK_ERRORS[error.code]

// -------------------------------------------------------------------

const areEventsLost = (error) => error.code === 'EVENTS_LOST'

const isHostSlave = (error) => error.code === 'HOST_IS_SLAVE'

const isSessionInvalid = (error) => error.code === 'SESSION_INVALID'

// -------------------------------------------------------------------

class XapiError extends BaseError {
  constructor (error) {
    super(error[0])

    this.code = error[0]
    this.params = error.slice(1)
  }
}

export const wrapError = error => new XapiError(error)

// ===================================================================

const URL_RE = /^(http(s)?:\/\/)?([^/]+?)(?::([0-9]+))?(?:\/.*)?$/
function parseUrl (url) {
  const matches = URL_RE.exec(url)
  if (!matches) {
    throw new Error('invalid URL: ' + url)
  }

  const [, protocol, , host, port] = matches
  let [, , isSecure] = matches

  if (!protocol) {
    isSecure = true
  }

  return {
    isSecure: Boolean(isSecure),
    host,
    port: port !== undefined ?
      +port :
      isSecure ? 443 : 80
  }
}

// -------------------------------------------------------------------

const noop = () => {}

// -------------------------------------------------------------------

const notConnectedPromise = Bluebird.reject(new Error('not connected'))

// Does nothing but avoid a Bluebird message error.
notConnectedPromise.catch(noop)

// ===================================================================

const MAX_TRIES = 5

// -------------------------------------------------------------------

export class Xapi extends EventEmitter {
  constructor (opts) {
    super()

    this._url = parseUrl(opts.url)
    this._auth = opts.auth

    this._sessionId = notConnectedPromise

    this._init()

    this._pool = null
    this._objectsByRefs = Object.create(null)
    this._objectsByRefs['OpaqueRef:NULL'] = null
    this._objects = new Collection()
    this._objects.getId = (object) => object.$id

    this._fromToken = ''
    this.on('connected', this._watchEvents)
    this.on('disconnected', () => {
      this._fromToken = ''
      this._objects.clear()
    })
  }

  get sessionId () {
    if (this.status !== 'connected') {
      throw new Error('sessionId is only available when connected')
    }

    return this._sessionId.value()
  }

  get status () {
    const {_sessionId: sessionId} = this

    if (sessionId.isFulfilled()) {
      return 'connected'
    }

    if (sessionId.isPending()) {
      return 'connecting'
    }

    return 'disconnected'
  }

  get _humanId () {
    return `${this._auth.user}@${this._url.host}`
  }

  connect () {
    const {status} = this

    if (status === 'connected') {
      return Bluebird.reject(new Error('already connected'))
    }

    if (status === 'connecting') {
      return Bluebird.reject(new Error('already connecting'))
    }

    this._sessionId = this._transportCall('session.login_with_password', [
      this._auth.user,
      this._auth.password
    ])

    return this._sessionId.then(() => {
      debug('%s: connected', this._humanId)

      this.emit('connected')
    })
  }

  disconnect () {
    const {status} = this

    if (status === 'disconnected') {
      return Bluebird.reject('already disconnected')
    }

    if (status === 'connecting') {
      return this._sessionId.cancel().catch(Bluebird.CancellationError, () => {
        debug('%s: disconnected', this._humanId)

        this.emit('disconnected')
      })
    }

    this._sessionId = notConnectedPromise

    return Bluebird.resolve().then(() => {
      debug('%s: disconnected', this._humanId)

      this.emit('disconnected')
    })
  }

  // High level calls.
  call (method, ...args) {
    return this._sessionCall(method, args)
  }

  // Nice getter which returns the object for a given $id (internal to
  // this lib), UUID (unique identifier that some objects have) or
  // opaque reference (internal to XAPI).
  getObject (idOrUuidOrRef, defaultValue) {
    const object = (
      // if there is an UUID, it is also the $id.
      this.objects.all[idOrUuidOrRef] ||
      this._objectsByRefs[idOrUuidOrRef]
    )

    if (object) return object

    if (arguments.length > 1) return defaultValue

    throw new Error('there is not object can be matched to ' + idOrUuidOrRef)
  }

  // Returns the object for a given opaque reference (internal to
  // XAPI).
  getObjectByRef (ref, defaultValue) {
    const object = this._objectsByRefs[ref]

    if (object) return object

    if (arguments.length > 1) return defaultValue

    throw new Error('there is no object with the ref ' + ref)
  }

  // Returns the object for a given UUID (unique identifier that some
  // objects have).
  getObjectByUuid (uuid, defaultValue) {
    // Objects ids are already UUIDs if they have one.
    const object = this.objects.all[uuid]

    if (object) return object

    if (arguments.length > 1) return defaultValue

    throw new Error('there is no object with the UUID ' + uuid)
  }

  get pool () {
    return this._pool
  }

  get objects () {
    return this._objects
  }

  // Medium level call: handle session errors.
  _sessionCall (method, args) {
    if (startsWith(method, 'session.')) {
      return Bluebird.reject(
        new Error('session.*() methods are disabled from this interface')
      )
    }

    return this._sessionId.then((sessionId) => {
      return this._transportCall(method, [sessionId].concat(args))
    }).catch(isSessionInvalid, () => {
      // XAPI is sometimes reinitialized and sessions are lost.
      // Try to login again.
      debug('%s: the session has been reinitialized', this._humanId)

      this._sessionId = notConnectedPromise

      return this._sessionCall(method, args)
    })
  }

  // Low level call: handle transport errors.
  _transportCall (method, args, tries = 1) {
    debug('%s: %s(...)', this._humanId, method)

    return this._rawCall(method, args)
      .catch(isNetworkError, isXapiNetworkError, error => {
        debug('%s: network error %s', this._humanId, error.code)

        if (!(tries < MAX_TRIES)) {
          debug('%s too many network errors (%s), give up', this._humanId, tries)

          throw error
        }

        // TODO: ability to cancel the connection
        // TODO: ability to force immediate reconnection
        // TODO: implement back-off

        return Bluebird.delay(5e3).then(() => {
          // TODO: handling not responding host.

          return this._transportCall(method, args, tries + 1)
        })
      })
      .catch(isHostSlave, ({params: [master]}) => {
        debug('%s: host is slave, attempting to connect at %s', this._humanId, master)

        this._url.host = master
        this._init()

        return this._transportCall(method, args)
      })
  }

  // Lowest level call: do not handle any errors.
  _rawCall (method, args) {
    return this._xmlRpcCall(method, args)
      .then(result => {
        const {Status: status} = result

        // Return the plain result if it does not have a valid XAPI
        // format.
        if (!status) {
          return result
        }

        if (status === 'Success') {
          return result.Value
        }

        wrapError(result.ErrorDescription)
      })
      .cancellable()
  }

  _init () {
    const {isSecure, host, port} = this._url

    const client = (isSecure ?
      createSecureXmlRpcClient :
      createXmlRpcClient
    )({
      host,
      port,
      rejectUnauthorized: false,
      timeout: 10
    })

    this._xmlRpcCall = promisify(client.methodCall, client)
  }

  _normalizeObject (type, ref, object) {
    const {_objectsByRefs: objectsByRefs} = this
    const REF_RE = /^OpaqueRef:/

    forEach(object, function resolveIfLink (value, key, object) {
      if (typeof value === 'string' && REF_RE.test(value)) {
        Object.defineProperty(object, key, {
          enumerable: true,
          get: () => objectsByRefs[value]
        })
      } else if (typeof value === 'object') {
        forEach(value, resolveIfLink)
      }
    })

    object.$id = object.uuid || ref
    object.$ref = ref
    object.$type = type

    Object.defineProperty(object, '$pool', {
      enumerable: true,
      get: () => this._pool
    })
  }

  _watchEvents () {
    this.call(
      'event.from', ['*'], this._fromToken, 1e3 + 0.1
    ).then(({token, events}) => {
      this._fromToken = token

      const {
        _objects: objects,
        _objectsByRefs: objectsByRefs
      } = this

      forEach(events, event => {
        const {operation: op} = event

        const {ref} = event
        if (op === 'del') {
          const object = objectsByRefs[ref]

          if (object) {
            objects.remove(object.$id)
            delete objectsByRefs[ref]
          }
        } else {
          const {class: type, snapshot: object} = event

          this._normalizeObject(type, ref, object)
          objects.set(object)
          objectsByRefs[ref] = object

          if (object.$type === 'pool') {
            this._pool = object
          }
        }
      })
    }).catch(areEventsLost, () => {
      this._objects.clear()
    }).then(() => {
      this._watchEvents()
    }).catch(Bluebird.CancellationError, noop)
  }
}

// ===================================================================

// The default value is a factory function.
export const createClient = (opts) => new Xapi(opts)
<commit_msg>Bypass Xapi#objects getter internally for perf.<commit_after>import Bluebird, {promisify} from 'bluebird'
import Collection from 'xo-collection'
import createDebug from 'debug'
import forEach from 'lodash.foreach'
import startsWith from 'lodash.startswith'
import {BaseError} from 'make-error'
import {
  createClient as createXmlRpcClient,
  createSecureClient as createSecureXmlRpcClient
} from 'xmlrpc'
import {EventEmitter} from 'events'

const debug = createDebug('xen-api')

// ===================================================================

// http://www.gnu.org/software/libc/manual/html_node/Error-Codes.html
const NETWORK_ERRORS = {
  // Connection has been closed outside of our control.
  ECONNRESET: true,

  // Connection has been aborted locally.
  ECONNABORTED: true,

  // Host is up but refuses connection (typically: no such service).
  ECONNREFUSED: true,

  // TODO: ??
  EINVAL: true,

  // Host is not reachable (does not respond).
  EHOSTUNREACH: true,

  // Connection configured timed out has been reach.
  ETIMEDOUT: true
}

const isNetworkError = (error) => NETWORK_ERRORS[error.code]

// -------------------------------------------------------------------

const XAPI_NETWORK_ERRORS = {
  HOST_STILL_BOOTING: true,
  HOST_HAS_NO_MANAGEMENT_IP: true
}

const isXapiNetworkError = (error) => XAPI_NETWORK_ERRORS[error.code]

// -------------------------------------------------------------------

const areEventsLost = (error) => error.code === 'EVENTS_LOST'

const isHostSlave = (error) => error.code === 'HOST_IS_SLAVE'

const isSessionInvalid = (error) => error.code === 'SESSION_INVALID'

// -------------------------------------------------------------------

class XapiError extends BaseError {
  constructor (error) {
    super(error[0])

    this.code = error[0]
    this.params = error.slice(1)
  }
}

export const wrapError = error => new XapiError(error)

// ===================================================================

const URL_RE = /^(http(s)?:\/\/)?([^/]+?)(?::([0-9]+))?(?:\/.*)?$/
function parseUrl (url) {
  const matches = URL_RE.exec(url)
  if (!matches) {
    throw new Error('invalid URL: ' + url)
  }

  const [, protocol, , host, port] = matches
  let [, , isSecure] = matches

  if (!protocol) {
    isSecure = true
  }

  return {
    isSecure: Boolean(isSecure),
    host,
    port: port !== undefined ?
      +port :
      isSecure ? 443 : 80
  }
}

// -------------------------------------------------------------------

const noop = () => {}

// -------------------------------------------------------------------

const notConnectedPromise = Bluebird.reject(new Error('not connected'))

// Does nothing but avoid a Bluebird message error.
notConnectedPromise.catch(noop)

// ===================================================================

const MAX_TRIES = 5

// -------------------------------------------------------------------

export class Xapi extends EventEmitter {
  constructor (opts) {
    super()

    this._url = parseUrl(opts.url)
    this._auth = opts.auth

    this._sessionId = notConnectedPromise

    this._init()

    this._pool = null
    this._objectsByRefs = Object.create(null)
    this._objectsByRefs['OpaqueRef:NULL'] = null
    this._objects = new Collection()
    this._objects.getId = (object) => object.$id

    this._fromToken = ''
    this.on('connected', this._watchEvents)
    this.on('disconnected', () => {
      this._fromToken = ''
      this._objects.clear()
    })
  }

  get sessionId () {
    if (this.status !== 'connected') {
      throw new Error('sessionId is only available when connected')
    }

    return this._sessionId.value()
  }

  get status () {
    const {_sessionId: sessionId} = this

    if (sessionId.isFulfilled()) {
      return 'connected'
    }

    if (sessionId.isPending()) {
      return 'connecting'
    }

    return 'disconnected'
  }

  get _humanId () {
    return `${this._auth.user}@${this._url.host}`
  }

  connect () {
    const {status} = this

    if (status === 'connected') {
      return Bluebird.reject(new Error('already connected'))
    }

    if (status === 'connecting') {
      return Bluebird.reject(new Error('already connecting'))
    }

    this._sessionId = this._transportCall('session.login_with_password', [
      this._auth.user,
      this._auth.password
    ])

    return this._sessionId.then(() => {
      debug('%s: connected', this._humanId)

      this.emit('connected')
    })
  }

  disconnect () {
    const {status} = this

    if (status === 'disconnected') {
      return Bluebird.reject('already disconnected')
    }

    if (status === 'connecting') {
      return this._sessionId.cancel().catch(Bluebird.CancellationError, () => {
        debug('%s: disconnected', this._humanId)

        this.emit('disconnected')
      })
    }

    this._sessionId = notConnectedPromise

    return Bluebird.resolve().then(() => {
      debug('%s: disconnected', this._humanId)

      this.emit('disconnected')
    })
  }

  // High level calls.
  call (method, ...args) {
    return this._sessionCall(method, args)
  }

  // Nice getter which returns the object for a given $id (internal to
  // this lib), UUID (unique identifier that some objects have) or
  // opaque reference (internal to XAPI).
  getObject (idOrUuidOrRef, defaultValue) {
    const object = (
      // if there is an UUID, it is also the $id.
      this._objects.all[idOrUuidOrRef] ||
      this._objectsByRefs[idOrUuidOrRef]
    )

    if (object) return object

    if (arguments.length > 1) return defaultValue

    throw new Error('there is not object can be matched to ' + idOrUuidOrRef)
  }

  // Returns the object for a given opaque reference (internal to
  // XAPI).
  getObjectByRef (ref, defaultValue) {
    const object = this._objectsByRefs[ref]

    if (object) return object

    if (arguments.length > 1) return defaultValue

    throw new Error('there is no object with the ref ' + ref)
  }

  // Returns the object for a given UUID (unique identifier that some
  // objects have).
  getObjectByUuid (uuid, defaultValue) {
    // Objects ids are already UUIDs if they have one.
    const object = this._objects.all[uuid]

    if (object) return object

    if (arguments.length > 1) return defaultValue

    throw new Error('there is no object with the UUID ' + uuid)
  }

  get pool () {
    return this._pool
  }

  get objects () {
    return this._objects
  }

  // Medium level call: handle session errors.
  _sessionCall (method, args) {
    if (startsWith(method, 'session.')) {
      return Bluebird.reject(
        new Error('session.*() methods are disabled from this interface')
      )
    }

    return this._sessionId.then((sessionId) => {
      return this._transportCall(method, [sessionId].concat(args))
    }).catch(isSessionInvalid, () => {
      // XAPI is sometimes reinitialized and sessions are lost.
      // Try to login again.
      debug('%s: the session has been reinitialized', this._humanId)

      this._sessionId = notConnectedPromise

      return this._sessionCall(method, args)
    })
  }

  // Low level call: handle transport errors.
  _transportCall (method, args, tries = 1) {
    debug('%s: %s(...)', this._humanId, method)

    return this._rawCall(method, args)
      .catch(isNetworkError, isXapiNetworkError, error => {
        debug('%s: network error %s', this._humanId, error.code)

        if (!(tries < MAX_TRIES)) {
          debug('%s too many network errors (%s), give up', this._humanId, tries)

          throw error
        }

        // TODO: ability to cancel the connection
        // TODO: ability to force immediate reconnection
        // TODO: implement back-off

        return Bluebird.delay(5e3).then(() => {
          // TODO: handling not responding host.

          return this._transportCall(method, args, tries + 1)
        })
      })
      .catch(isHostSlave, ({params: [master]}) => {
        debug('%s: host is slave, attempting to connect at %s', this._humanId, master)

        this._url.host = master
        this._init()

        return this._transportCall(method, args)
      })
  }

  // Lowest level call: do not handle any errors.
  _rawCall (method, args) {
    return this._xmlRpcCall(method, args)
      .then(result => {
        const {Status: status} = result

        // Return the plain result if it does not have a valid XAPI
        // format.
        if (!status) {
          return result
        }

        if (status === 'Success') {
          return result.Value
        }

        wrapError(result.ErrorDescription)
      })
      .cancellable()
  }

  _init () {
    const {isSecure, host, port} = this._url

    const client = (isSecure ?
      createSecureXmlRpcClient :
      createXmlRpcClient
    )({
      host,
      port,
      rejectUnauthorized: false,
      timeout: 10
    })

    this._xmlRpcCall = promisify(client.methodCall, client)
  }

  _normalizeObject (type, ref, object) {
    const {_objectsByRefs: objectsByRefs} = this
    const REF_RE = /^OpaqueRef:/

    forEach(object, function resolveIfLink (value, key, object) {
      if (typeof value === 'string' && REF_RE.test(value)) {
        Object.defineProperty(object, key, {
          enumerable: true,
          get: () => objectsByRefs[value]
        })
      } else if (typeof value === 'object') {
        forEach(value, resolveIfLink)
      }
    })

    object.$id = object.uuid || ref
    object.$ref = ref
    object.$type = type

    Object.defineProperty(object, '$pool', {
      enumerable: true,
      get: () => this._pool
    })
  }

  _watchEvents () {
    this.call(
      'event.from', ['*'], this._fromToken, 1e3 + 0.1
    ).then(({token, events}) => {
      this._fromToken = token

      const {
        _objects: objects,
        _objectsByRefs: objectsByRefs
      } = this

      forEach(events, event => {
        const {operation: op} = event

        const {ref} = event
        if (op === 'del') {
          const object = objectsByRefs[ref]

          if (object) {
            objects.remove(object.$id)
            delete objectsByRefs[ref]
          }
        } else {
          const {class: type, snapshot: object} = event

          this._normalizeObject(type, ref, object)
          objects.set(object)
          objectsByRefs[ref] = object

          if (object.$type === 'pool') {
            this._pool = object
          }
        }
      })
    }).catch(areEventsLost, () => {
      this._objects.clear()
    }).then(() => {
      this._watchEvents()
    }).catch(Bluebird.CancellationError, noop)
  }
}

// ===================================================================

// The default value is a factory function.
export const createClient = (opts) => new Xapi(opts)
<|endoftext|>