<commit_before>/*
 * Copyright (c) 2014, Wanadev <http://www.wanadev.fr/>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice, this
 *     list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *   * Neither the name of Wanadev nor the names of its contributors may be used
 *     to endorse or promote products derived from this software without specific
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Authored by: Fabien LOISON <http://flozz.fr/>
 */

/**
 * PhotonUI - Javascript Web User Interface.
 *
 * @module PhotonUI
 * @submodule Input
 * @namespace photonui
 */

var photonui = photonui || {};

/**
 * Numeric field.
 *
 * @class NumericField
 * @constructor
 * @extends photonui.Field
 * @param {Number} params.min The minimum value of the field (optional, default=`null` (no minimum)).
 * @param {Number} params.max The maximum value of the field (optional, default=`null` (no maximum)).
 * @param {Number} params.step The incrementation step when user scroll over the field (optional, default=1).
 * @param {Number} params.decimalDigits The number of digit after the decimal dot (optional, `null` = no limit, default=0 (integer))
 * @param {String} params.decimalSymbol The decimal symbol: dot (`"."`) or comma (`","`) (optional, default=".")
 */
photonui.NumericField = function(params) {
    photonui.Field.call(this, params);

    var params = params || {};

    // Args
    this.min = (params.min !== undefined) ? params.min : null;
    this.max = (params.max !== undefined) ? params.max : null;
    this.step = params.step || 1;
    this.decimalDigits = (params.decimalDigits !== undefined) ? params.decimalDigits : 0;
    this.decimalSymbol = params.decimalSymbol || ".";  // "." or ","

    // Build
    this._buildHtml();
    this._updateAttributes();
    this._bindEvents();

    this._bindEvent("keypress", this._e.field, "keypress", this._onKeypress.bind(this));
    this._bindEvent("keyup", this._e.field, "keyup", this._onKeyup.bind(this));
    this._bindEvent("change", this._e.field, "change", this._onChange.bind(this));
    this._bindEvent("mousewheel", this._e.field, "mousewheel", this._onMouseWheel.bind(this));
    this._bindEvent("mousewheel-firefox", this._e.field, "DOMMouseScroll", this._onMouseWheel.bind(this));
}

photonui.NumericField.prototype = new photonui.Field();

//////////////////////////////////////////
// Getters / Setters                    //
//////////////////////////////////////////

/**
 * Get the minimum value of the field.
 *
 * @method getMin
 * @return {Number} The minumum value or `null` (no minimum).
 */
photonui.NumericField.prototype.getMin = function() {
    return this.min;
}

/**
 * Set the minimum value of the field.
 *
 * @method setMin
 * @param {Number} min The minumum value or `null` (no minimum).
 */
photonui.NumericField.prototype.setMin = function(min) {
    this.min = min;
}

/**
 * Get the maximum value of the field.
 *
 * @method getMax
 * @return {Number} The maximum value or `null` (no maximum).
 */
photonui.NumericField.prototype.getMax = function() {
    return this.max;
}

/**
 * Set the maximum value of the field.
 *
 * @method setMax
 * @param {Number} max The minumum value or `null` (no maximum).
 */
photonui.NumericField.prototype.setMax = function(max) {
    this.max = max;
}

/**
 * Get the incrementation step of the field.
 *
 * @method getStep
 * @return {Number} The step.
 */
photonui.NumericField.prototype.getStep = function() {
    return this.step;
}

/**
 * Set the incrementation step of the field.
 *
 * @method setStep
 * @param {Number} step The step.
 */
photonui.NumericField.prototype.setStep = function(step) {
    this.step = Math.abs(step);
}

/**
 * Get the number of digit after the decimal dot.
 *
 * @method getDecimalDigits
 * @return {Number} The number of digit after the decimal dot or `null` (no limite).
 */
photonui.NumericField.prototype.getDecimalDigits = function() {
    return this.decimalDigits;
}

/**
 * Set the number of digit after the decimal dot.
 *
 * @method setDecimalDigits
 * @param {Number} decimalDigits The number of digit after the decimal dot or `null` (no limit).
 */
photonui.NumericField.prototype.setDecimalDigits = function(decimalDigits) {
    this.decimalDigits = decimalDigits;
}

/**
 * Get the decimal symbol.
 *
 * @method getDecimalSymbol
 * @return {String} The decimal symbol: dot (`"."`) or comma (`","`).
 */
photonui.NumericField.prototype.getDecimalSymbol = function() {
    return this.DecimalSymbol;
}

/**
 * Set the decimal symbol.
 *
 * @method setDecimalSymbol
 * @param {String} decimalSymbol The decimal symbol: dot (`"."`) or comma (`","`).
 */
photonui.NumericField.prototype.setDecimalSymbol = function(decimalSymbol) {
    this.decimalSymbol = decimalSymbol;
}

/**
 * Get the field value.
 *
 * @method getValue
 * @return {String} The value
 */
photonui.NumericField.prototype.getValue = function() {
    return parseFloat(this.value);
}

/**
 * Set the field value.
 *
 * @method setValue
 * @param {String} value The value
 */
photonui.NumericField.prototype.setValue = function(value) {
    this._updateValue(value);
    this._updateFieldValue();
    this._callCallbacks("value-changed", [this.getValue()]);
}

//////////////////////////////////////////
// Private Methods                      //
//////////////////////////////////////////

/**
 * Build the HTML of the text field.
 *
 * @method _buildHtml
 * @private
 */
photonui.NumericField.prototype._buildHtml = function() {
    this._e.field = document.createElement("input");
    this._e.field.className = "photonui-widget photonui-field photonui-field-numeric";
    this._e.field.type = "text";
    this._e.field.name = this.name;
}

/**
 * Update attributes.
 *
 * @method _updateAttributes
 * @private
 */
photonui.NumericField.prototype._updateAttributes = function() {
    photonui.Field.prototype._updateAttributes.call(this);
}

/**
 * Update the value (in the widget).
 *
 * @method _updateValue
 * @private
 * @param {String|Number} value The raw value.
 */
photonui.NumericField.prototype._updateValue = function(value) {
    value = ("" + value).replace(",", "."); // ","
    value = value.replace(/ /g, "");  // remove spaces
    value = parseFloat(value);
    if (isNaN(value)) {
        value = 0;
    }

    if (this.min != null) {
        value = Math.max(this.min, value);
    }

    if (this.max != null) {
        value = Math.min(this.max, value);
    }

    if (this.decimalDigits != null) {
        value = value.toFixed(this.decimalDigits);
    }

    this.value = value;
}

/**
 * Update the value in the html field.
 *
 * @method _updateFieldValue
 * @private
 */
photonui.NumericField.prototype._updateFieldValue = function() {
    this._e.field.value = ("" + this.value).replace(".", this.decimalSymbol);
}

//////////////////////////////////////////
// Internal Events Callbacks            //
//////////////////////////////////////////

photonui.NumericField.prototype._onKeypress = function(event) {
    if (event.charCode == 45) {  // Minus
        if (this._e.field.selectionStart > 0 || (this.min != null && this.min >= 0)) {
            event.preventDefault();
        }
        else if (this.getValue() < 0 && this._e.field.selectionStart - this._e.field.selectionEnd == 0) {
            event.preventDefault();
        }
    }
    else if ((event.charCode == 46 || event.charCode == 44) && (this.decimalDigits > 0 || this.decimalDigits == null)) {  // Dot & Comma
        var value = this._e.field.value;
        if (value.indexOf(".") >= 0 || value.indexOf(",") >= 0) {
            if (this._e.field.selectionStart == this._e.field.selectionEnd) {
                event.preventDefault();
            }
            else {
                var selected = value.substring(this._e.field.selectionStart, this._e.field.selectionEnd);
                if (selected.indexOf(".") < 0 && selected.indexOf(",") < 0) {
                    event.preventDefault();
                }
            }
        }
    }
    else if (event.keyCode == 13) {  // Enter
        this._updateFieldValue();
    }
    else if ((event.charCode < 48 || event.charCode > 57) && event.charCode != 32 && event.charCode != 0) {  // Not (digit, space or special key)
        event.preventDefault();
    }
}

photonui.NumericField.prototype._onKeyup = function(event) {
    this._updateValue(this._e.field.value);
}

photonui.NumericField.prototype._onChange = function(event) {
    this._updateFieldValue();
}

photonui.NumericField.prototype._onMouseWheel = function(event) {
    if (document.activeElement != this._e.field) {
        return;
    }

    var wheelDelta = null;

    // Webkit
    if (event.wheelDeltaY != undefined) {
        wheelDelta = event.wheelDeltaY;
    }
    // MSIE
    if (event.wheelDelta != undefined) {
        wheelDelta = event.wheelDelta;
    }
    // Firefox
    if (event.axis != undefined && event.detail != undefined) {
        if (event.axis == 2) { // Y
            wheelDelta = - event.detail;
        }
    }

    if (wheelDelta != null) {
        if (wheelDelta >= 0) {
            this.setValue(this.getValue() + this.step);
        }
        else {
            this.setValue(this.getValue() - this.step);
        }
        event.preventDefault();
    }
}
<commit_msg>Allow copy/paste in numeric fields<commit_after>/*
 * Copyright (c) 2014, Wanadev <http://www.wanadev.fr/>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice, this
 *     list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *   * Neither the name of Wanadev nor the names of its contributors may be used
 *     to endorse or promote products derived from this software without specific
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Authored by: Fabien LOISON <http://flozz.fr/>
 */

/**
 * PhotonUI - Javascript Web User Interface.
 *
 * @module PhotonUI
 * @submodule Input
 * @namespace photonui
 */

var photonui = photonui || {};

/**
 * Numeric field.
 *
 * @class NumericField
 * @constructor
 * @extends photonui.Field
 * @param {Number} params.min The minimum value of the field (optional, default=`null` (no minimum)).
 * @param {Number} params.max The maximum value of the field (optional, default=`null` (no maximum)).
 * @param {Number} params.step The incrementation step when user scroll over the field (optional, default=1).
 * @param {Number} params.decimalDigits The number of digit after the decimal dot (optional, `null` = no limit, default=0 (integer))
 * @param {String} params.decimalSymbol The decimal symbol: dot (`"."`) or comma (`","`) (optional, default=".")
 */
photonui.NumericField = function(params) {
    photonui.Field.call(this, params);

    var params = params || {};

    // Args
    this.min = (params.min !== undefined) ? params.min : null;
    this.max = (params.max !== undefined) ? params.max : null;
    this.step = params.step || 1;
    this.decimalDigits = (params.decimalDigits !== undefined) ? params.decimalDigits : 0;
    this.decimalSymbol = params.decimalSymbol || ".";  // "." or ","

    // Build
    this._buildHtml();
    this._updateAttributes();
    this._bindEvents();

    this._bindEvent("keypress", this._e.field, "keypress", this._onKeypress.bind(this));
    this._bindEvent("keyup", this._e.field, "keyup", this._onKeyup.bind(this));
    this._bindEvent("change", this._e.field, "change", this._onChange.bind(this));
    this._bindEvent("mousewheel", this._e.field, "mousewheel", this._onMouseWheel.bind(this));
    this._bindEvent("mousewheel-firefox", this._e.field, "DOMMouseScroll", this._onMouseWheel.bind(this));
}

photonui.NumericField.prototype = new photonui.Field();

//////////////////////////////////////////
// Getters / Setters                    //
//////////////////////////////////////////

/**
 * Get the minimum value of the field.
 *
 * @method getMin
 * @return {Number} The minumum value or `null` (no minimum).
 */
photonui.NumericField.prototype.getMin = function() {
    return this.min;
}

/**
 * Set the minimum value of the field.
 *
 * @method setMin
 * @param {Number} min The minumum value or `null` (no minimum).
 */
photonui.NumericField.prototype.setMin = function(min) {
    this.min = min;
}

/**
 * Get the maximum value of the field.
 *
 * @method getMax
 * @return {Number} The maximum value or `null` (no maximum).
 */
photonui.NumericField.prototype.getMax = function() {
    return this.max;
}

/**
 * Set the maximum value of the field.
 *
 * @method setMax
 * @param {Number} max The minumum value or `null` (no maximum).
 */
photonui.NumericField.prototype.setMax = function(max) {
    this.max = max;
}

/**
 * Get the incrementation step of the field.
 *
 * @method getStep
 * @return {Number} The step.
 */
photonui.NumericField.prototype.getStep = function() {
    return this.step;
}

/**
 * Set the incrementation step of the field.
 *
 * @method setStep
 * @param {Number} step The step.
 */
photonui.NumericField.prototype.setStep = function(step) {
    this.step = Math.abs(step);
}

/**
 * Get the number of digit after the decimal dot.
 *
 * @method getDecimalDigits
 * @return {Number} The number of digit after the decimal dot or `null` (no limite).
 */
photonui.NumericField.prototype.getDecimalDigits = function() {
    return this.decimalDigits;
}

/**
 * Set the number of digit after the decimal dot.
 *
 * @method setDecimalDigits
 * @param {Number} decimalDigits The number of digit after the decimal dot or `null` (no limit).
 */
photonui.NumericField.prototype.setDecimalDigits = function(decimalDigits) {
    this.decimalDigits = decimalDigits;
}

/**
 * Get the decimal symbol.
 *
 * @method getDecimalSymbol
 * @return {String} The decimal symbol: dot (`"."`) or comma (`","`).
 */
photonui.NumericField.prototype.getDecimalSymbol = function() {
    return this.DecimalSymbol;
}

/**
 * Set the decimal symbol.
 *
 * @method setDecimalSymbol
 * @param {String} decimalSymbol The decimal symbol: dot (`"."`) or comma (`","`).
 */
photonui.NumericField.prototype.setDecimalSymbol = function(decimalSymbol) {
    this.decimalSymbol = decimalSymbol;
}

/**
 * Get the field value.
 *
 * @method getValue
 * @return {String} The value
 */
photonui.NumericField.prototype.getValue = function() {
    return parseFloat(this.value);
}

/**
 * Set the field value.
 *
 * @method setValue
 * @param {String} value The value
 */
photonui.NumericField.prototype.setValue = function(value) {
    this._updateValue(value);
    this._updateFieldValue();
    this._callCallbacks("value-changed", [this.getValue()]);
}

//////////////////////////////////////////
// Private Methods                      //
//////////////////////////////////////////

/**
 * Build the HTML of the text field.
 *
 * @method _buildHtml
 * @private
 */
photonui.NumericField.prototype._buildHtml = function() {
    this._e.field = document.createElement("input");
    this._e.field.className = "photonui-widget photonui-field photonui-field-numeric";
    this._e.field.type = "text";
    this._e.field.name = this.name;
}

/**
 * Update attributes.
 *
 * @method _updateAttributes
 * @private
 */
photonui.NumericField.prototype._updateAttributes = function() {
    photonui.Field.prototype._updateAttributes.call(this);
}

/**
 * Update the value (in the widget).
 *
 * @method _updateValue
 * @private
 * @param {String|Number} value The raw value.
 */
photonui.NumericField.prototype._updateValue = function(value) {
    value = ("" + value).replace(",", "."); // ","
    value = value.replace(/ /g, "");  // remove spaces
    value = parseFloat(value);
    if (isNaN(value)) {
        value = 0;
    }

    if (this.min != null) {
        value = Math.max(this.min, value);
    }

    if (this.max != null) {
        value = Math.min(this.max, value);
    }

    if (this.decimalDigits != null) {
        value = value.toFixed(this.decimalDigits);
    }

    this.value = value;
}

/**
 * Update the value in the html field.
 *
 * @method _updateFieldValue
 * @private
 */
photonui.NumericField.prototype._updateFieldValue = function() {
    this._e.field.value = ("" + this.value).replace(".", this.decimalSymbol);
}

//////////////////////////////////////////
// Internal Events Callbacks            //
//////////////////////////////////////////

photonui.NumericField.prototype._onKeypress = function(event) {
    if (event.ctrlKey) {
        return;
    }
    if (event.charCode == 45) {  // Minus
        if (this._e.field.selectionStart > 0 || (this.min != null && this.min >= 0)) {
            event.preventDefault();
        }
        else if (this.getValue() < 0 && this._e.field.selectionStart - this._e.field.selectionEnd == 0) {
            event.preventDefault();
        }
    }
    else if ((event.charCode == 46 || event.charCode == 44) && (this.decimalDigits > 0 || this.decimalDigits == null)) {  // Dot & Comma
        var value = this._e.field.value;
        if (value.indexOf(".") >= 0 || value.indexOf(",") >= 0) {
            if (this._e.field.selectionStart == this._e.field.selectionEnd) {
                event.preventDefault();
            }
            else {
                var selected = value.substring(this._e.field.selectionStart, this._e.field.selectionEnd);
                if (selected.indexOf(".") < 0 && selected.indexOf(",") < 0) {
                    event.preventDefault();
                }
            }
        }
    }
    else if (event.keyCode == 13) {  // Enter
        this._updateFieldValue();
    }
    else if ((event.charCode < 48 || event.charCode > 57) && event.charCode != 32 && event.charCode != 0) {  // Not (digit, space or special key)
        event.preventDefault();
    }
}

photonui.NumericField.prototype._onKeyup = function(event) {
    var value = this._e.field.value.replace(/[^0-9.,-]*/g, "");
    if (value != this._e.field.value) {
        this._e.field.value = value;
    }
    this._updateValue(this._e.field.value);
}

photonui.NumericField.prototype._onChange = function(event) {
    this._updateFieldValue();
}

photonui.NumericField.prototype._onMouseWheel = function(event) {
    if (document.activeElement != this._e.field) {
        return;
    }

    var wheelDelta = null;

    // Webkit
    if (event.wheelDeltaY != undefined) {
        wheelDelta = event.wheelDeltaY;
    }
    // MSIE
    if (event.wheelDelta != undefined) {
        wheelDelta = event.wheelDelta;
    }
    // Firefox
    if (event.axis != undefined && event.detail != undefined) {
        if (event.axis == 2) { // Y
            wheelDelta = - event.detail;
        }
    }

    if (wheelDelta != null) {
        if (wheelDelta >= 0) {
            this.setValue(this.getValue() + this.step);
        }
        else {
            this.setValue(this.getValue() - this.step);
        }
        event.preventDefault();
    }
}
<|endoftext|>


<commit_before>'use strict';

var StatsD = require('node-statsd').StatsD;
var urlutil = require('url');
var _ = require('lodash');
var onHeaders = require('on-headers');
var logger = require('./logger');

var that = this;
var initialized = false;
var debugMode = false;
var logFunction;
var statsd;
var debugPrefix;

function getInstance(obj) {
  if (!initialized) {
    // Setup default params.
    var obj = obj || {
      debug: true
    };

    // Debug mode?
    if (obj.debug) {
      if (obj.logger) {
        logFunction = obj.logger.debug;
      } else {
        logFunction = logger.debug;
      }

      if (obj.prefix) {
        debugPrefix = obj.prefix;
      }
      debugMode = true;
    }

    // If a url is provided; parse it.
    if (obj.url) {
      if (obj.host || obj.port) {
        throw new Error('Declare only one of: [url, host/port].');
      }
      var parsed = urlutil.parse(obj.url);
      obj.host = parsed.hostname;
      obj.port = Number(parsed.port);
      delete obj.url;
    }

    // Initialize the statsd instance!
    statsd = new StatsD(obj);
    initialized = true;
  } else if (initialized && obj !== undefined && _.keys(obj).length > 0) {
    var message = 'Re-initialization of the statsd helper options - probably not what you want!\n'
                + 'See node-oz-helpers\'s README for further details.';
    throw new Error(message);
  }
  return that;
}

function proxy(functionName) {
  return function () {
    // Has initialization taken place?
    if (!initialized) {
      throw new Error('You need to initialize statsd first.');
    }

    if (debugMode) {

      // Associate a prefix with the first argument.
      if (debugPrefix !== undefined && arguments.length > 0) {
        if (debugPrefix.slice(-1) !== '.') {
          arguments['0'] = debugPrefix + '.' + arguments['0'];
        } else {
          arguments['0'] = debugPrefix + arguments['0'];
        }
      }
      var args = [];

      _.forEach(arguments, function (value) {
        if (_.isArray(value)) {
          var o = [];
          _.forEach(value, function (element) {
            o.push("'" + element + "'");
          });
          args.push('[' + o.join(', ') + ']');
        } else if (_.isString(value)) {
          args.push('\'' + value + '\'');
        } else {
          args.push(value);
        }
      });

      var s = 'statsd.' + functionName + '(' + args.join(', ') + ')';
      logFunction(s);
    } else {
      var func = statsd[functionName];
      func.apply(statsd, arguments);
    }
  };
}

/**
 * An connect middleware that sends metrics on the route timings to statsd.
 * @param key the statsd key to use.
 * @returns {measure} the middleware.
 */
function middleware(key) {

  // Make key default to 'requests'.
  if (key === undefined || !_.isString(key)) {
    key = 'requests';
  }

  var that = this;

  function toMillis(hrtime) {
    var ms = hrtime[0] * 1e3 + hrtime[1] * 1e-6;
    return ms.toFixed(0);
  }

  return function middleware(req, res, next) {
    // We cannot get `req.route` nor `req.method` because the request has not been routed by express at this point
    var route;
    var method;
    var extraTags = [];

    onHeaders(res, function () {
      // When the headers are sent (when someone calls res.send) it is safe to assume the request has been routed
      var route = route || req.route.path.replace(/\:/g, '$');
      var method = route || req.method.toLowerCase();
      var tags;

      // add includes to route if existing
      if (req.query.include) {
        route += "include=" + req.query.include;
      }

      for (var i = 1; i < req.measurements.length; i++) {
        tags = [
          'method:' + method,
          'route:' + route,
          'status:' + res.statusCode,
          'part:' + req.measurements[i].key
        ].concat(extraTags);
        that.histogram(key, Number(req.measurements[i].diff), tags);
      }

      var totalDiff = toMillis(process.hrtime(req.measurements[0].time))
      tags = [
        'method:' + method,
        'route:' + route,
        'status:' + res.statusCode
      ].concat(extraTags);
      that.histogram(key + '.total', Number(totalDiff), tags);
    });

    req.measurements = [ {
      key: 'initial',
      time: process.hrtime(),
      diff: 0
    } ];

    req.addTag = function (tag) {
      extraTags.push(tag);
    };

    req.checkpoint = function (key) {
      var last = this.measurements.slice(-1)[0];
      var diff = toMillis(process.hrtime(last.time));
      this.measurements.push({
        key: key,
        time: process.hrtime(),
        diff: diff
      });
    };

    next();
  };
}

// Mostly for debugging purposes.
function getInstanceInfo() {
  return {
    host: statsd.host,
    port: statsd.port
  };
}

// Mostly for debugging purposes.
function reset() {
  initialized = false;
  statsd = undefined;
  debugMode = false;
  logFunction = undefined;
}

/**
 * Exports.
 */
module.exports.getInstance = getInstance;
module.exports.getInstanceInfo = getInstanceInfo;
module.exports.reset = reset;

module.exports.middleware = middleware;

module.exports.timing = proxy('timing');
module.exports.increment = proxy('increment');
module.exports.decrement = proxy('decrement');
module.exports.gauge = proxy('gauge');
module.exports.histogram = proxy('histogram');
module.exports.unique = proxy('unique');
module.exports.set = proxy('set');
module.exports.close = proxy('close');
<commit_msg>add separator<commit_after>'use strict';

var StatsD = require('node-statsd').StatsD;
var urlutil = require('url');
var _ = require('lodash');
var onHeaders = require('on-headers');
var logger = require('./logger');

var that = this;
var initialized = false;
var debugMode = false;
var logFunction;
var statsd;
var debugPrefix;

function getInstance(obj) {
  if (!initialized) {
    // Setup default params.
    var obj = obj || {
      debug: true
    };

    // Debug mode?
    if (obj.debug) {
      if (obj.logger) {
        logFunction = obj.logger.debug;
      } else {
        logFunction = logger.debug;
      }

      if (obj.prefix) {
        debugPrefix = obj.prefix;
      }
      debugMode = true;
    }

    // If a url is provided; parse it.
    if (obj.url) {
      if (obj.host || obj.port) {
        throw new Error('Declare only one of: [url, host/port].');
      }
      var parsed = urlutil.parse(obj.url);
      obj.host = parsed.hostname;
      obj.port = Number(parsed.port);
      delete obj.url;
    }

    // Initialize the statsd instance!
    statsd = new StatsD(obj);
    initialized = true;
  } else if (initialized && obj !== undefined && _.keys(obj).length > 0) {
    var message = 'Re-initialization of the statsd helper options - probably not what you want!\n'
                + 'See node-oz-helpers\'s README for further details.';
    throw new Error(message);
  }
  return that;
}

function proxy(functionName) {
  return function () {
    // Has initialization taken place?
    if (!initialized) {
      throw new Error('You need to initialize statsd first.');
    }

    if (debugMode) {

      // Associate a prefix with the first argument.
      if (debugPrefix !== undefined && arguments.length > 0) {
        if (debugPrefix.slice(-1) !== '.') {
          arguments['0'] = debugPrefix + '.' + arguments['0'];
        } else {
          arguments['0'] = debugPrefix + arguments['0'];
        }
      }
      var args = [];

      _.forEach(arguments, function (value) {
        if (_.isArray(value)) {
          var o = [];
          _.forEach(value, function (element) {
            o.push("'" + element + "'");
          });
          args.push('[' + o.join(', ') + ']');
        } else if (_.isString(value)) {
          args.push('\'' + value + '\'');
        } else {
          args.push(value);
        }
      });

      var s = 'statsd.' + functionName + '(' + args.join(', ') + ')';
      logFunction(s);
    } else {
      var func = statsd[functionName];
      func.apply(statsd, arguments);
    }
  };
}

/**
 * An connect middleware that sends metrics on the route timings to statsd.
 * @param key the statsd key to use.
 * @returns {measure} the middleware.
 */
function middleware(key) {

  // Make key default to 'requests'.
  if (key === undefined || !_.isString(key)) {
    key = 'requests';
  }

  var that = this;

  function toMillis(hrtime) {
    var ms = hrtime[0] * 1e3 + hrtime[1] * 1e-6;
    return ms.toFixed(0);
  }

  return function middleware(req, res, next) {
    // We cannot get `req.route` nor `req.method` because the request has not been routed by express at this point
    var route;
    var method;
    var extraTags = [];

    onHeaders(res, function () {
      // When the headers are sent (when someone calls res.send) it is safe to assume the request has been routed
      var route = route || req.route.path.replace(/\:/g, '$');
      var method = route || req.method.toLowerCase();
      var tags;

      // add includes to route if existing
      if (req.query.include) {
        route += "?include=" + req.query.include;
      }

      for (var i = 1; i < req.measurements.length; i++) {
        tags = [
          'method:' + method,
          'route:' + route,
          'status:' + res.statusCode,
          'part:' + req.measurements[i].key
        ].concat(extraTags);
        that.histogram(key, Number(req.measurements[i].diff), tags);
      }

      var totalDiff = toMillis(process.hrtime(req.measurements[0].time))
      tags = [
        'method:' + method,
        'route:' + route,
        'status:' + res.statusCode
      ].concat(extraTags);
      that.histogram(key + '.total', Number(totalDiff), tags);
    });

    req.measurements = [ {
      key: 'initial',
      time: process.hrtime(),
      diff: 0
    } ];

    req.addTag = function (tag) {
      extraTags.push(tag);
    };

    req.checkpoint = function (key) {
      var last = this.measurements.slice(-1)[0];
      var diff = toMillis(process.hrtime(last.time));
      this.measurements.push({
        key: key,
        time: process.hrtime(),
        diff: diff
      });
    };

    next();
  };
}

// Mostly for debugging purposes.
function getInstanceInfo() {
  return {
    host: statsd.host,
    port: statsd.port
  };
}

// Mostly for debugging purposes.
function reset() {
  initialized = false;
  statsd = undefined;
  debugMode = false;
  logFunction = undefined;
}

/**
 * Exports.
 */
module.exports.getInstance = getInstance;
module.exports.getInstanceInfo = getInstanceInfo;
module.exports.reset = reset;

module.exports.middleware = middleware;

module.exports.timing = proxy('timing');
module.exports.increment = proxy('increment');
module.exports.decrement = proxy('decrement');
module.exports.gauge = proxy('gauge');
module.exports.histogram = proxy('histogram');
module.exports.unique = proxy('unique');
module.exports.set = proxy('set');
module.exports.close = proxy('close');
<|endoftext|>


<commit_before>
// determine if an element can be focused

// http://www.w3.org/TR/html5/editing.html#focus-management

// NOTE: The following known issues exist:
//   Gecko: `svg a[xlink|href]` is not identified as focusable (because SVGElement.prototype.focus is missing)
//   Blink, WebKit: SVGElements that have been made focusable by adding a focus event listener are not identified as focusable

import '../prototype/svgelement.prototype.focus';
import '../prototype/element.prototype.matches';
import getParents from '../get/parents';
import isVisible from './visible';
import isDisabled from './disabled';
import isValidTabindex from './valid-tabindex';
import isValidArea from './valid-area';

let canFocusSvgMethod = SVGElement.prototype.focus === HTMLElement.prototype.focus;
import canFocusAudioWithoutControls from '../supports/focus-audio-without-controls';
import canFocusVideoWithoutControls from '../supports/focus-video-without-controls';
import canFocusHtml from '../supports/focus-html';
import canFocusLabelTabindex from '../supports/focus-label-tabindex';
import canFocusSvg from '../supports/focus-svg';
import canFocusTable from '../supports/focus-table';
import canFocusFieldset from '../supports/focus-fieldset';
import canFocusSummary from '../supports/focus-summary';
import canFocusImgIsmap from '../supports/focus-img-ismap';
import canFocusImgUsemapTabindex from '../supports/focus-img-usemap-tabindex';
import canFocusScrollContainerWithoutOverflow from '../supports/focus-scroll-container-without-overflow';
import canFocusScrollContainer from '../supports/focus-scroll-container';
import canFocusScrollBody from '../supports/focus-scroll-body';

export default function(element) {
  if (element === document) {
    element = document.documentElement;
  }

  if (!element || element.nodeType !== Node.ELEMENT_NODE) {
    throw new TypeError('is/focusable requires an argument of type Element');
  }

  const nodeName = element.nodeName.toLowerCase();

  // object[usemap] is not focusable in any browser
  if (nodeName === 'object' && element.hasAttribute('usemap')) {
    return false;
  }

  // input[type="hidden"] cannot be focused
  if (nodeName === 'input' && element.type === 'hidden') {
    return false;
  }

  if (nodeName === 'label' && !canFocusLabelTabindex) {
    // there's no way to make an element focusable other than by adding a tabindex,
    // and focus behavior of the label element seems hard-wired to ignore tabindex
    // in some browsers (like Gecko, Blink and WebKit)
    return false;
  }

  if (isDisabled(element)) {
    return false;
  }

  if (nodeName === 'area') {
    return isValidArea(element);
  }

  // elements that are not rendered, cannot be focused
  if (!isVisible(element)) {
    return false;
  }

  if (nodeName === 'input' || nodeName === 'select' || nodeName === 'button' || nodeName === 'textarea') {
    // disabled elements have been filtered out above, using isDisabled()
    return true;
  }

  if (nodeName === 'a' && element.hasAttribute('href')) {
    return true;
  }

  if (nodeName === 'iframe' || nodeName === 'object' || nodeName === 'embed') {
    // browsing context containers
    return true;
  }

  if (element.hasAttribute('contenteditable')) {
    // also see CSS property user-modify below
    return true;
  }

  if (nodeName === 'keygen') {
    return true;
  }

  if (nodeName === 'audio' && (canFocusAudioWithoutControls || element.hasAttribute('controls'))) {
    return true;
  }

  if (nodeName === 'video' && (canFocusVideoWithoutControls || element.hasAttribute('controls'))) {
    return true;
  }

  if (canFocusSummary && nodeName === 'summary') {
    return true;
  }

  if (nodeName === 'img' && element.hasAttribute('usemap') && isValidTabindex(element)) {
    // Gecko, Trident and Edge do not allow an image with an image map and tabindex to be focused,
    // it appears the tabindex is overruled so focus is still forwarded to the <map>
    return canFocusImgUsemapTabindex;
  }

  // TODO: not not identify body as focusable
  if (nodeName === 'body') {
    return true;
  }

  // TODO: not not identify html as focusable
  if (canFocusHtml && nodeName === 'html') {
    // Firefox, IE11 can focus
    return true;
  }

  if (canFocusTable && (nodeName === 'table' || nodeName === 'td')) {
    // IE10-11 can focus
 and
    return true;
  }

  if (canFocusFieldset && nodeName === 'fieldset') {
    // IE10-11 can focus <fieldset>
    return true;
  }

  if (canFocusSvgMethod && canFocusSvg && nodeName === 'svg') {
    // NOTE: in Chrome this would be something like 'svg, svg *,' as *every* svg element with a focus event listener is focusable
    return true;
  }

  if (canFocusSvgMethod && element.matches('svg a[*|href]')) {
    // Namespace problems of [xlink:href] explained in http://stackoverflow.com/a/23047888/515124
    // Firefox cannot focus <svg> child elements from script
    return true;
  }

  // http://www.w3.org/TR/html5/editing.html#sequential-focus-navigation-and-the-tabindex-attribute
  if (isValidTabindex(element)) {
    return true;
  }

  const style = window.getComputedStyle(element, null);
  const userModify = style.webkitUserModify || '';
  if (userModify && userModify.indexOf('write') !== -1) {
    // http://www.w3.org/TR/1999/WD-css3-userint-19990916#user-modify
    // https://github.com/medialize/ally.js/issues/17
    return true;
  }

  if (canFocusImgIsmap && nodeName === 'img' && element.hasAttribute('ismap')) {
    // IE10-11 considers the  in  focusable
    // https://github.com/medialize/ally.js/issues/20
    let hasLinkParent = getParents({context: element}).some(
      parent => parent.nodeName.toLowerCase() === 'a' && parent.hasAttribute('href')
    );

    if (hasLinkParent) {
      return true;
    }
  }

  if (canFocusScrollContainer && (element.offsetHeight < element.scrollHeight || element.offsetWidth < element.scrollWidth)) {
    // scrollable containers are focusable Internet Explorer
    // scrollable containers are tabbable in Firefox
    // https://github.com/medialize/ally.js/issues/21
    if (canFocusScrollContainerWithoutOverflow) {
      return true;
    }
    // Firefox requires proper overflow setting, IE does not
    // https://developer.mozilla.org/en-US/docs/Web/CSS/overflow
    return [
      style.getPropertyValue('overflow'),
      style.getPropertyValue('overflow-x'),
      style.getPropertyValue('overflow-y'),
    ].some(overflow => overflow === 'auto' || overflow === 'scroll');
  }

  const parent = element.parentElement || element.parentNode;
  if (canFocusScrollBody && (parent.offsetHeight < parent.scrollHeight || parent.offsetWidth < parent.scrollWidth)) {
    // scrollable bodies are focusable Internet Explorer
    // https://github.com/medialize/ally.js/issues/21
    return true;
  }

  // NOTE: elements marked as inert are not focusable,
  // but that property is not exposed to the DOM
  // http://www.w3.org/TR/html5/editing.html#inert

  return false;
}
<commit_msg>style(is/focusable): sort support imports alphabetically<commit_after>
// determine if an element can be focused

// http://www.w3.org/TR/html5/editing.html#focus-management

// NOTE: The following known issues exist:
//   Gecko: `svg a[xlink|href]` is not identified as focusable (because SVGElement.prototype.focus is missing)
//   Blink, WebKit: SVGElements that have been made focusable by adding a focus event listener are not identified as focusable

import '../prototype/svgelement.prototype.focus';
import '../prototype/element.prototype.matches';
import getParents from '../get/parents';
import isVisible from './visible';
import isDisabled from './disabled';
import isValidTabindex from './valid-tabindex';
import isValidArea from './valid-area';

let canFocusSvgMethod = SVGElement.prototype.focus === HTMLElement.prototype.focus;
import canFocusAudioWithoutControls from '../supports/focus-audio-without-controls';
import canFocusFieldset from '../supports/focus-fieldset';
import canFocusHtml from '../supports/focus-html';
import canFocusImgIsmap from '../supports/focus-img-ismap';
import canFocusImgUsemapTabindex from '../supports/focus-img-usemap-tabindex';
import canFocusLabelTabindex from '../supports/focus-label-tabindex';
import canFocusScrollBody from '../supports/focus-scroll-body';
import canFocusScrollContainer from '../supports/focus-scroll-container';
import canFocusScrollContainerWithoutOverflow from '../supports/focus-scroll-container-without-overflow';
import canFocusSummary from '../supports/focus-summary';
import canFocusSvg from '../supports/focus-svg';
import canFocusTable from '../supports/focus-table';
import canFocusVideoWithoutControls from '../supports/focus-video-without-controls';

export default function(element) {
  if (element === document) {
    element = document.documentElement;
  }

  if (!element || element.nodeType !== Node.ELEMENT_NODE) {
    throw new TypeError('is/focusable requires an argument of type Element');
  }

  const nodeName = element.nodeName.toLowerCase();

  // object[usemap] is not focusable in any browser
  if (nodeName === 'object' && element.hasAttribute('usemap')) {
    return false;
  }

  // input[type="hidden"] cannot be focused
  if (nodeName === 'input' && element.type === 'hidden') {
    return false;
  }

  if (nodeName === 'label' && !canFocusLabelTabindex) {
    // there's no way to make an element focusable other than by adding a tabindex,
    // and focus behavior of the label element seems hard-wired to ignore tabindex
    // in some browsers (like Gecko, Blink and WebKit)
    return false;
  }

  if (isDisabled(element)) {
    return false;
  }

  if (nodeName === 'area') {
    return isValidArea(element);
  }

  // elements that are not rendered, cannot be focused
  if (!isVisible(element)) {
    return false;
  }

  if (nodeName === 'input' || nodeName === 'select' || nodeName === 'button' || nodeName === 'textarea') {
    // disabled elements have been filtered out above, using isDisabled()
    return true;
  }

  if (nodeName === 'a' && element.hasAttribute('href')) {
    return true;
  }

  if (nodeName === 'iframe' || nodeName === 'object' || nodeName === 'embed') {
    // browsing context containers
    return true;
  }

  if (element.hasAttribute('contenteditable')) {
    // also see CSS property user-modify below
    return true;
  }

  if (nodeName === 'keygen') {
    return true;
  }

  if (nodeName === 'audio' && (canFocusAudioWithoutControls || element.hasAttribute('controls'))) {
    return true;
  }

  if (nodeName === 'video' && (canFocusVideoWithoutControls || element.hasAttribute('controls'))) {
    return true;
  }

  if (canFocusSummary && nodeName === 'summary') {
    return true;
  }

  if (nodeName === 'img' && element.hasAttribute('usemap') && isValidTabindex(element)) {
    // Gecko, Trident and Edge do not allow an image with an image map and tabindex to be focused,
    // it appears the tabindex is overruled so focus is still forwarded to the <map>
    return canFocusImgUsemapTabindex;
  }

  // TODO: not not identify body as focusable
  if (nodeName === 'body') {
    return true;
  }

  // TODO: not not identify html as focusable
  if (canFocusHtml && nodeName === 'html') {
    // Firefox, IE11 can focus
    return true;
  }

  if (canFocusTable && (nodeName === 'table' || nodeName === 'td')) {
    // IE10-11 can focus
 and
    return true;
  }

  if (canFocusFieldset && nodeName === 'fieldset') {
    // IE10-11 can focus <fieldset>
    return true;
  }

  if (canFocusSvgMethod && canFocusSvg && nodeName === 'svg') {
    // NOTE: in Chrome this would be something like 'svg, svg *,' as *every* svg element with a focus event listener is focusable
    return true;
  }

  if (canFocusSvgMethod && element.matches('svg a[*|href]')) {
    // Namespace problems of [xlink:href] explained in http://stackoverflow.com/a/23047888/515124
    // Firefox cannot focus <svg> child elements from script
    return true;
  }

  // http://www.w3.org/TR/html5/editing.html#sequential-focus-navigation-and-the-tabindex-attribute
  if (isValidTabindex(element)) {
    return true;
  }

  const style = window.getComputedStyle(element, null);
  const userModify = style.webkitUserModify || '';
  if (userModify && userModify.indexOf('write') !== -1) {
    // http://www.w3.org/TR/1999/WD-css3-userint-19990916#user-modify
    // https://github.com/medialize/ally.js/issues/17
    return true;
  }

  if (canFocusImgIsmap && nodeName === 'img' && element.hasAttribute('ismap')) {
    // IE10-11 considers the  in  focusable
    // https://github.com/medialize/ally.js/issues/20
    let hasLinkParent = getParents({context: element}).some(
      parent => parent.nodeName.toLowerCase() === 'a' && parent.hasAttribute('href')
    );

    if (hasLinkParent) {
      return true;
    }
  }

  if (canFocusScrollContainer && (element.offsetHeight < element.scrollHeight || element.offsetWidth < element.scrollWidth)) {
    // scrollable containers are focusable Internet Explorer
    // scrollable containers are tabbable in Firefox
    // https://github.com/medialize/ally.js/issues/21
    if (canFocusScrollContainerWithoutOverflow) {
      return true;
    }
    // Firefox requires proper overflow setting, IE does not
    // https://developer.mozilla.org/en-US/docs/Web/CSS/overflow
    return [
      style.getPropertyValue('overflow'),
      style.getPropertyValue('overflow-x'),
      style.getPropertyValue('overflow-y'),
    ].some(overflow => overflow === 'auto' || overflow === 'scroll');
  }

  const parent = element.parentElement || element.parentNode;
  if (canFocusScrollBody && (parent.offsetHeight < parent.scrollHeight || parent.offsetWidth < parent.scrollWidth)) {
    // scrollable bodies are focusable Internet Explorer
    // https://github.com/medialize/ally.js/issues/21
    return true;
  }

  // NOTE: elements marked as inert are not focusable,
  // but that property is not exposed to the DOM
  // http://www.w3.org/TR/html5/editing.html#inert

  return false;
}
<|endoftext|>


<commit_before>var dgram = require('dgram'),
    dns   = require('dns');

/**
 * The UDP Client for StatsD
 * @param options
 *   @option host      {String}  The host to connect to default: localhost
 *   @option port      {String|Integer} The port to connect to default: 8125
 *   @option prefix    {String}  An optional prefix to assign to each stat name sent
 *   @option suffix    {String}  An optional suffix to assign to each stat name sent
 *   @option globalize {boolean} An optional boolean to add "statsd" as an object in the global namespace
 *   @option cacheDns  {boolean} An optional option to only lookup the hostname -> ip address once
 *   @option mock      {boolean} An optional boolean indicating this Client is a mock object, no stats are sent.
 * @constructor
 */
var Client = function (host, port, prefix, suffix, globalize, cacheDns, mock) {
  var options = host || {},
         self = this;

  if(arguments.length > 1 || typeof(host) === 'string'){
    options = {
      host      : host,
      port      : port,
      prefix    : prefix,
      suffix    : suffix,
      globalize : globalize,
      cacheDns  : cacheDns,
      mock      : mock === true
    };
  }

  this.host   = options.host || 'localhost';
  this.port   = options.port || 8125;
  this.prefix = options.prefix || '';
  this.suffix = options.suffix || '';
  this.socket = dgram.createSocket('udp4');
  this.mock   = options.mock;

  if(options.cacheDns === true){
    dns.lookup(options.host, function(err, address, family){
      if(err == null){
        self.host = address;
      }
    });
  }

  if(options.globalize){
    global.statsd = this;
  }
};

/**
 * Represents the timing stat
 * @param stat {String|Array} The stat(s) to send
 * @param time {Number} The time in milliseconds to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.timing = function (stat, time, sampleRate, callback) {
  this.sendAll(stat, time, 'ms', sampleRate, callback);
};

/**
 * Increments a stat by a specified amount
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.increment = function (stat, value, sampleRate, callback) {
  this.sendAll(stat, value || 1, 'c', sampleRate, callback);
};

/**
 * Decrements a stat by a specified amount
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.decrement = function (stat, value, sampleRate, callback) {
  this.sendAll(stat, -value || -1, 'c', sampleRate, callback);
};

/**
 * Gauges a stat by a specified amount
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.gauge = function (stat, value, sampleRate, callback) {
  this.sendAll(stat, value, 'g', sampleRate, callback);
};

/**
 * Counts unique values by a specified amount
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.unique =
Client.prototype.set = function (stat, value, sampleRate, callback) {
  this.sendAll(stat, value, 's', sampleRate, callback);
};

/**
 * Checks if stats is an array and sends all stats calling back once all have sent
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.sendAll = function(stat, value, type, sampleRate, callback){
  var completed = 0,
      calledback = false,
      sentBytes = 0,
      self = this;

  /**
   * Gets called once for each callback, when all callbacks return we will
   * call back from the function
   * @private
   */
  function onSend(error, bytes){
    completed += 1;
    if(calledback || typeof callback !== 'function'){
      return;
    }

    if(error){
      calledback = true;
      return callback(error);
    }

    sentBytes += bytes;
    if(completed === stat.length){
      callback(null, sentBytes);
    }
  }

  if(Array.isArray(stat)){
    stat.forEach(function(item){
      self.send(item, value, type, sampleRate, onSend);
    });
  } else {
    this.send(stat, value, type, sampleRate, callback);
  }
};

/**
 * Sends a stat across the wire
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param type {String} The type of message to send to statsd
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.send = function (stat, value, type, sampleRate, callback) {
  var message = this.prefix + stat + this.suffix + ':' + value + '|' + type,
      buf;

  if(sampleRate && sampleRate < 1){
    if(Math.random() < sampleRate){
      message += '|@' + sampleRate;
    } else {
      //don't want to send if we don't meet the sample ratio
      return;
    }
  }

  // Only send this stat if we're not a mock Client.
  if(!this.mock) {
    buf = new Buffer(message);
    this.socket.send(buf, 0, buf.length, this.port, this.host, callback);
  } else {
    if(typeof callback === 'function'){
      callback(null, 0);
    }
  }
};

exports.StatsD = Client;
<commit_msg>Added histogram method<commit_after>var dgram = require('dgram'),
    dns   = require('dns');

/**
 * The UDP Client for StatsD
 * @param options
 *   @option host      {String}  The host to connect to default: localhost
 *   @option port      {String|Integer} The port to connect to default: 8125
 *   @option prefix    {String}  An optional prefix to assign to each stat name sent
 *   @option suffix    {String}  An optional suffix to assign to each stat name sent
 *   @option globalize {boolean} An optional boolean to add "statsd" as an object in the global namespace
 *   @option cacheDns  {boolean} An optional option to only lookup the hostname -> ip address once
 *   @option mock      {boolean} An optional boolean indicating this Client is a mock object, no stats are sent.
 * @constructor
 */
var Client = function (host, port, prefix, suffix, globalize, cacheDns, mock) {
  var options = host || {},
         self = this;

  if(arguments.length > 1 || typeof(host) === 'string'){
    options = {
      host      : host,
      port      : port,
      prefix    : prefix,
      suffix    : suffix,
      globalize : globalize,
      cacheDns  : cacheDns,
      mock      : mock === true
    };
  }

  this.host   = options.host || 'localhost';
  this.port   = options.port || 8125;
  this.prefix = options.prefix || '';
  this.suffix = options.suffix || '';
  this.socket = dgram.createSocket('udp4');
  this.mock   = options.mock;

  if(options.cacheDns === true){
    dns.lookup(options.host, function(err, address, family){
      if(err == null){
        self.host = address;
      }
    });
  }

  if(options.globalize){
    global.statsd = this;
  }
};

/**
 * Represents the timing stat
 * @param stat {String|Array} The stat(s) to send
 * @param time {Number} The time in milliseconds to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.timing = function (stat, time, sampleRate, callback) {
  this.sendAll(stat, time, 'ms', sampleRate, callback);
};

/**
 * Increments a stat by a specified amount
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.increment = function (stat, value, sampleRate, callback) {
  this.sendAll(stat, value || 1, 'c', sampleRate, callback);
};

/**
 * Decrements a stat by a specified amount
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.decrement = function (stat, value, sampleRate, callback) {
  this.sendAll(stat, -value || -1, 'c', sampleRate, callback);
};

/**
 * Represents the histogram stat
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.histogram = function (stat, value, sampleRate, callback) {
  this.sendAll(stat, value, 'h', sampleRate, callback);
};

/**
 * Gauges a stat by a specified amount
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.gauge = function (stat, value, sampleRate, callback) {
  this.sendAll(stat, value, 'g', sampleRate, callback);
};

/**
 * Counts unique values by a specified amount
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.unique =
Client.prototype.set = function (stat, value, sampleRate, callback) {
  this.sendAll(stat, value, 's', sampleRate, callback);
};

/**
 * Checks if stats is an array and sends all stats calling back once all have sent
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.sendAll = function(stat, value, type, sampleRate, callback){
  var completed = 0,
      calledback = false,
      sentBytes = 0,
      self = this;

  /**
   * Gets called once for each callback, when all callbacks return we will
   * call back from the function
   * @private
   */
  function onSend(error, bytes){
    completed += 1;
    if(calledback || typeof callback !== 'function'){
      return;
    }

    if(error){
      calledback = true;
      return callback(error);
    }

    sentBytes += bytes;
    if(completed === stat.length){
      callback(null, sentBytes);
    }
  }

  if(Array.isArray(stat)){
    stat.forEach(function(item){
      self.send(item, value, type, sampleRate, onSend);
    });
  } else {
    this.send(stat, value, type, sampleRate, callback);
  }
};

/**
 * Sends a stat across the wire
 * @param stat {String|Array} The stat(s) to send
 * @param value The value to send
 * @param type {String} The type of message to send to statsd
 * @param sampleRate {Number} The Number of times to sample (0 to 1)
 * @param callback {Function} Callback when message is done being delivered. Optional.
 */
Client.prototype.send = function (stat, value, type, sampleRate, callback) {
  var message = this.prefix + stat + this.suffix + ':' + value + '|' + type,
      buf;

  if(sampleRate && sampleRate < 1){
    if(Math.random() < sampleRate){
      message += '|@' + sampleRate;
    } else {
      //don't want to send if we don't meet the sample ratio
      return;
    }
  }

  // Only send this stat if we're not a mock Client.
  if(!this.mock) {
    buf = new Buffer(message);
    this.socket.send(buf, 0, buf.length, this.port, this.host, callback);
  } else {
    if(typeof callback === 'function'){
      callback(null, 0);
    }
  }
};

exports.StatsD = Client;
<|endoftext|>


<commit_before>// Keeps messages in an intermediate dir first, atomically moves them to the destination one.
//
// Requirements for the entries to be logged:
// 1) Must be JSON.
// 2) Must have the "ms" field with Date.now() output, the milliseconds.
// 3) Logging must be close to real-time: entry "ms" field populated by the caller must be close to "Date.now()".
//    This is important for the fetcher in order to not have to go through extra files.
//
// The easiest way to test this module is to run it as a command-line application.
// Please refer to the shell test for mode details.
//
// Intermediate filename schema: ('tmp:' + random_hash + '.log').
// Destination  filename schema: ('YYYY-MM-DD-HH-MM' + ':' + random_hash + ':' + firstentry_ms + ':' + lastentry_ms + '.log).
//
// Where "random_hash" is five random lowercase latin characters.

var _ = require('underscore');
var assert = require('assert');
var dateformat = require('dateformat');
var fs = require('fs');
var pidlock = require('pidlock');
var mkpath = require('mkpath');
var path = require('path');
var synchronized = require('synchronized');

var prefix = 'DEBUG ';

function safeParseJson(string) {
    try {
        return JSON.parse(string);
    } catch (exception) {
        return null;
    }
};

function generateRandomHash() {
    var result = '';
    for (var i = 0; i < 5; ++i) {
        result += (Math.floor(Math.random() * 26) + 10).toString(36);
    }
    return result;
};

function Impl(params) {
    this.params = _.clone(params);
    if (this.params.verbose) {
        this.log = function(message) {
            console.log(prefix + message);
        };
    } else {
        this.log = function() {
        };
    }
    assert(_.isString(this.params.intermediate_dir));
    assert(fs.existsSync(this.params.intermediate_dir));
    assert(_.isString(this.params.destination_dir));
    assert(fs.existsSync(this.params.destination_dir));
    this.replayIntermediateFiles();
    this.total_consumed = 0;
    this.ensureFileIsOpen();
    this.installTimeBoundaryFlusher();
};

Impl.prototype.ensureFileIsOpen = function() {
    if (_.isUndefined(this.fd)) {
        this.fn = path.join(this.params.intermediate_dir, 'tmp:' + generateRandomHash() + '.log');
        this.log('Intermediate "' + this.fn + '": opening.');
        this.fd = fs.openSync(this.fn, 'a');
        this.log('Intermediate "' + this.fn + '": opened, fd=' + this.fd);
        this.entries = 0;
        this.ms = {
            earliest: null,
            latest: null,
        };
    }
};

Impl.prototype.getFormattedDate = function() {
    return dateformat(Date.now(), 'yyyy-mm-dd-HH-MM');
};

Impl.prototype.ensureFileIsFlushed = function() {
    if (!_.isUndefined(this.fd)) {
        this.log('Intermediate "' + this.fn + '": closing.');
        fs.fsyncSync(this.fd);
        fs.closeSync(this.fd);
        delete this.fd;
        this.log('Intermediate "' + this.fn + '": closed.');
        if (this.entries) {
            var destination_fn = path.join(
                this.params.destination_dir,
                this.getFormattedDate() +
                ':' + generateRandomHash() +
                ':' + this.ms.earliest + ':' + this.ms.latest + '.log');
            this.log('Destination "' + destination_fn + '": atomically renaming.');
            fs.renameSync(this.fn, destination_fn);
            this.log('Destination "' + destination_fn + '": atomically renamed.');
        } else {
            this.log('Intermediate "' + this.fn + '": unlinking empty.');
            fs.unlinkSync(this.fn);
            this.log('Intermediate "' + this.fn + '": unlinked.');
        }
        delete this.fn;
        delete this.entries;
        delete this.ms;
    }
};

Impl.prototype.appendEntry = function(e) {
    assert(_.isObject(e));
    if (!_.isNumber(e.ms)) {
        this.log('NEED_MS_FIELD.');
        return;
    }
    if (Math.abs(e.ms - Date.now()) > this.params.max_time_discrepancy_ms) {
        this.log('LARGE_TIME_DISCREPANCY');
        if (!this.skipped_because_of_time_discrepancy) {
            this.skipped_because_of_time_discrepancy = 0;
        }
        if (!this.skipped_because_of_discrepancy_threshold) {
            this.skipped_because_of_discrepancy_threshold = 1;
        }
        ++this.skipped_because_of_time_discrepancy;
        if (this.skipped_because_of_time_discrepancy >= this.skipped_because_of_discrepancy_threshold) {
            this.skipped_because_of_discrepancy_threshold *= 10;
            this.log(this.skipped_because_of_time_discrepancy + ' total skipped because of time discrepancy.');
        }
        return;
    }
    this.ensureFileIsOpen();
    if (!this.ms.earliest || e.ms < this.ms.earliest) {
        this.ms.earliest = e.ms;
    }
    if (!this.ms.latest || e.ms > this.ms.latest) {
        this.ms.latest = e.ms;
    }
    fs.writeSync(this.fd, JSON.stringify(e) + '\n');
    if (this.params.max_file_age_ms && this.entries === 0) {
        var self = this;
        var fn_to_flush = this.fn;
        setTimeout(
            function() {
                if (self.fn === fn_to_flush) {
                    self.log('Intermediate "' + self.fn + '": flushed by timeout.');
                    self.flush();
                }
            },
            this.params.max_file_age_ms);
    }
    ++this.entries;
    ++this.total_consumed;
    if (this.params.log_frequency && ((this.entries % this.params.log_frequency)) === 0) {
        this.log('Intermediate "' + this.fn + '": ' + this.entries + ' entries.');
    }
    if (this.params.max_entries_per_file && (this.entries > this.params.max_entries_per_file)) {
        this.flush();
    }
};

Impl.prototype.flush = function() {
    this.ensureFileIsFlushed();
    this.ensureFileIsOpen();
};

Impl.prototype.installTimeBoundaryFlusher = function() {
    var self = this;
    this.projected_destination_timestamp = self.getFormattedDate();
    setInterval(
        function() {
            var timestamp = self.getFormattedDate();
            if (timestamp != self.projected_destination_timestamp) {
                self.projected_destination_timestamp = timestamp;
                if (self.entries > 0) {
                    self.log('Time boundary: "' + timestamp + '": flushing.');
                    self.flush();
                } else {
                    self.log('Time boundary: "' + timestamp + '": nothing to flush.');
                }
            }
        },
        1000);
};

Impl.prototype.tearDown = function(callback) {
    this.ensureFileIsFlushed();
    if (_.isFunction(callback)) {
        callback();
    }
};

Impl.prototype.replayIntermediateFiles = function() {
    var self = this;
    var files = fs.readdirSync(this.params.intermediate_dir);
    if (files.length > 0) {
        self.log('Replaying ' + files.length + ' files.');
        _.each(files, function(fn) {
            var full_fn = path.join(self.params.intermediate_dir, fn);
            self.log('Replaying "' + fn + '": begin.');
            self.ms = {
                earliest: null,
                latest: null,
            };
            var lines = {
                total: 0,
                good: 0,
            };
            _.each(fs.readFileSync(full_fn).toString().split('\n'), function(line) {
                if (line) {
                    ++lines.total;
                    var e = safeParseJson(line);
                    if (_.isObject(e) && _.isNumber(e.ms)) {
                        ++lines.good;
                        ++self.entries;
                        self.ensureFileIsOpen();
                        if (!self.ms.earliest || e.ms < self.ms.earliest) {
                            self.ms.earliest = e.ms;
                        }
                        if (!self.ms.latest || e.ms > self.ms.latest) {
                            self.ms.latest = e.ms;
                        }
                        fs.writeSync(self.fd, JSON.stringify(e) + '\n');
                    }
                }
            });
            self.log(
                'Replaying "' + fn + '": ' + lines.good + ' entries parsed' +
                (lines.total === lines.good ? '' : ' (out of ' + lines.total + ' lines)') +
                '.');
            self.ensureFileIsFlushed();
            self.log(
                'Replaying "' + fn + '": done.');
            fs.unlinkSync(full_fn);
        });
    }
};

module.exports.spawn = function(config, callback) {
    assert(_.isFunction(callback));
    var lockdir = config.storer_lockdir || config.storer_workdir;
    assert(_.isString(lockdir) && lockdir !== '');
    mkpath.sync(lockdir);
    console.log('Lock ("' + lockdir + '" / "' + config.storer_lockname + '"): acquiring.');
    pidlock.guard(lockdir, config.storer_lockname, function(error, data, cleanup) {
        if (error) {
            console.log('Lock ("' + lockdir + '" / "' + config.storer_lockname + '"): busy.');
        } else {
            console.log('Lock ("' + lockdir + '" / "' + config.storer_lockname + '"): acquired.');

            var intermediate_dir = config.storer_intermediate_dir || path.join(config.storer_workdir, '/intermediate');
            console.log('Intermediate dir "' + intermediate_dir + '".');
            mkpath.sync(intermediate_dir);
            var destination_dir = config.storer_destination_dir || path.join(config.storer_workdir, '/destination');
            console.log('Destination dir "' + destination_dir + '".');
            mkpath.sync(destination_dir);

            var impl = new Impl({
                verbose: config.verbose,
                intermediate_dir: intermediate_dir,
                destination_dir: destination_dir,
                log_frequency: config.storer_log_frequency,
                max_time_discrepancy_ms: config.storer_max_time_discrepancy_ms,
                max_entries_per_file: config.storer_max_entries_per_file,
                max_file_age_ms: config.storer_max_file_age_ms,
            });

            function tearDown(callback) {
                console.log('Lock ("' + lockdir + '" / "' + config.storer_lockname + '"): releasing.');
                cleanup();
                console.log('Lock ("' + lockdir + '" / "' + config.storer_lockname + '"): releases.');
                if (_.isFunction(callback)) {
                    callback();
                }
            };

            callback({
                push: function(entry, callback) {
                    var json;
                    if (_.isString(entry)) {
                        if (config.storer_debug && entry === 'STOP') {
                            tearDown(function() {
                                process.exit(0);
                            });
                            return;
                        } else if (config.storer_debug && entry === 'CONFIG') {
                            console.log(JSON.stringify(config, null, 2));
                            if (_.isFunction(callback)) {
                                setTimeout(callback, 0);
                            }
                            return;
                        } else if (config.storer_debug && entry === 'CREATE') {
                            impl.ensureFileIsOpen();
                            if (_.isFunction(callback)) {
                                setTimeout(callback, 0);
                            }
                            return;
                        } else if (config.storer_debug && entry === 'FLUSH') {
                            impl.ensureFileIsFlushed();
                            if (_.isFunction(callback)) {
                                setTimeout(callback, 0);
                            }
                            return;
                        } else if (config.storer_debug && entry === 'STATUS') {
                            console.log(JSON.stringify(impl, null, 2));
                            if (_.isFunction(callback)) {
                                setTimeout(callback, 0);
                            }
                            return;
                        }
                        json = safeParseJson(entry);
                    } else {
                        json = entry;
                    }
                    if (_.isObject(json)) {
                        impl.appendEntry(json);
                    } else {
                        impl.log('INVALID_JSON');
                    }
                    if (_.isFunction(callback)) {
                        setTimeout(callback, 0);
                    }
                },
                shutdown: function(callback) {
                    impl.tearDown();
                    tearDown(callback);
                }
            });
        }
    });
};

if (require.main === module) {
    var config = require('./config').fromCommandLine();
    module.exports.spawn(config, function(storer) {
        var rl = require('readline').createInterface(process.stdin, process.stdout);
        var lock;
        rl.on('line', function(line) {
            synchronized(lock, function(callback) {
                storer.push(line, callback);
            });
        });
        rl.on('close', function() {
            synchronized(lock, function(callback) {
                console.log('Tearing down.');
                storer.shutdown(function() {
                    console.log('Done.');
                    process.exit(0);
                });
            });
        });
    });
}
<commit_msg>Fixed saving replayed entries.<commit_after>// Keeps messages in an intermediate dir first, atomically moves them to the destination one.
//
// Requirements for the entries to be logged:
// 1) Must be JSON.
// 2) Must have the "ms" field with Date.now() output, the milliseconds.
// 3) Logging must be close to real-time: entry "ms" field populated by the caller must be close to "Date.now()".
//    This is important for the fetcher in order to not have to go through extra files.
//
// The easiest way to test this module is to run it as a command-line application.
// Please refer to the shell test for mode details.
//
// Intermediate filename schema: ('tmp:' + random_hash + '.log').
// Destination  filename schema: ('YYYY-MM-DD-HH-MM' + ':' + random_hash + ':' + firstentry_ms + ':' + lastentry_ms + '.log).
//
// Where "random_hash" is five random lowercase latin characters.

var _ = require('underscore');
var assert = require('assert');
var dateformat = require('dateformat');
var fs = require('fs');
var pidlock = require('pidlock');
var mkpath = require('mkpath');
var path = require('path');
var synchronized = require('synchronized');

var prefix = 'DEBUG ';

function safeParseJson(string) {
    try {
        return JSON.parse(string);
    } catch (exception) {
        return null;
    }
};

function generateRandomHash() {
    var result = '';
    for (var i = 0; i < 5; ++i) {
        result += (Math.floor(Math.random() * 26) + 10).toString(36);
    }
    return result;
};

function Impl(params) {
    this.params = _.clone(params);
    if (this.params.verbose) {
        this.log = function(message) {
            console.log(prefix + message);
        };
    } else {
        this.log = function() {
        };
    }
    assert(_.isString(this.params.intermediate_dir));
    assert(fs.existsSync(this.params.intermediate_dir));
    assert(_.isString(this.params.destination_dir));
    assert(fs.existsSync(this.params.destination_dir));
    this.replayIntermediateFiles();
    this.total_consumed = 0;
    this.ensureFileIsOpen();
    this.installTimeBoundaryFlusher();
};

Impl.prototype.ensureFileIsOpen = function() {
    if (_.isUndefined(this.fd)) {
        this.fn = path.join(this.params.intermediate_dir, 'tmp:' + generateRandomHash() + '.log');
        this.log('Intermediate "' + this.fn + '": opening.');
        this.fd = fs.openSync(this.fn, 'a');
        this.log('Intermediate "' + this.fn + '": opened, fd=' + this.fd);
        this.entries = 0;
        this.ms = {
            earliest: null,
            latest: null,
        };
    }
};

Impl.prototype.getFormattedDate = function() {
    return dateformat(Date.now(), 'yyyy-mm-dd-HH-MM');
};

Impl.prototype.ensureFileIsFlushed = function() {
    if (!_.isUndefined(this.fd)) {
        this.log('Intermediate "' + this.fn + '": closing.');
        fs.fsyncSync(this.fd);
        fs.closeSync(this.fd);
        delete this.fd;
        this.log('Intermediate "' + this.fn + '": closed.');
        if (this.entries) {
            var destination_fn = path.join(
                this.params.destination_dir,
                this.getFormattedDate() +
                ':' + generateRandomHash() +
                ':' + this.ms.earliest + ':' + this.ms.latest + '.log');
            this.log('Destination "' + destination_fn + '": atomically renaming.');
            fs.renameSync(this.fn, destination_fn);
            this.log('Destination "' + destination_fn + '": atomically renamed.');
        } else {
            this.log('Intermediate "' + this.fn + '": unlinking empty.');
            fs.unlinkSync(this.fn);
            this.log('Intermediate "' + this.fn + '": unlinked.');
        }
        delete this.fn;
        delete this.entries;
        delete this.ms;
    }
};

Impl.prototype.appendEntry = function(e) {
    assert(_.isObject(e));
    if (!_.isNumber(e.ms)) {
        this.log('NEED_MS_FIELD.');
        return;
    }
    if (Math.abs(e.ms - Date.now()) > this.params.max_time_discrepancy_ms) {
        this.log('LARGE_TIME_DISCREPANCY');
        if (!this.skipped_because_of_time_discrepancy) {
            this.skipped_because_of_time_discrepancy = 0;
        }
        if (!this.skipped_because_of_discrepancy_threshold) {
            this.skipped_because_of_discrepancy_threshold = 1;
        }
        ++this.skipped_because_of_time_discrepancy;
        if (this.skipped_because_of_time_discrepancy >= this.skipped_because_of_discrepancy_threshold) {
            this.skipped_because_of_discrepancy_threshold *= 10;
            this.log(this.skipped_because_of_time_discrepancy + ' total skipped because of time discrepancy.');
        }
        return;
    }
    this.ensureFileIsOpen();
    if (!this.ms.earliest || e.ms < this.ms.earliest) {
        this.ms.earliest = e.ms;
    }
    if (!this.ms.latest || e.ms > this.ms.latest) {
        this.ms.latest = e.ms;
    }
    fs.writeSync(this.fd, JSON.stringify(e) + '\n');
    if (this.params.max_file_age_ms && this.entries === 0) {
        var self = this;
        var fn_to_flush = this.fn;
        setTimeout(
            function() {
                if (self.fn === fn_to_flush) {
                    self.log('Intermediate "' + self.fn + '": flushed by timeout.');
                    self.flush();
                }
            },
            this.params.max_file_age_ms);
    }
    ++this.entries;
    ++this.total_consumed;
    if (this.params.log_frequency && ((this.entries % this.params.log_frequency)) === 0) {
        this.log('Intermediate "' + this.fn + '": ' + this.entries + ' entries.');
    }
    if (this.params.max_entries_per_file && (this.entries > this.params.max_entries_per_file)) {
        this.flush();
    }
};

Impl.prototype.flush = function() {
    this.ensureFileIsFlushed();
    this.ensureFileIsOpen();
};

Impl.prototype.installTimeBoundaryFlusher = function() {
    var self = this;
    this.projected_destination_timestamp = self.getFormattedDate();
    setInterval(
        function() {
            var timestamp = self.getFormattedDate();
            if (timestamp != self.projected_destination_timestamp) {
                self.projected_destination_timestamp = timestamp;
                if (self.entries > 0) {
                    self.log('Time boundary: "' + timestamp + '": flushing.');
                    self.flush();
                } else {
                    self.log('Time boundary: "' + timestamp + '": nothing to flush.');
                }
            }
        },
        1000);
};

Impl.prototype.tearDown = function(callback) {
    this.ensureFileIsFlushed();
    if (_.isFunction(callback)) {
        callback();
    }
};

Impl.prototype.replayIntermediateFiles = function() {
    var self = this;
    var files = fs.readdirSync(this.params.intermediate_dir);
    if (files.length > 0) {
        self.log('Replaying ' + files.length + ' files.');
        self.entries = 0;
        _.each(files, function(fn) {
            var full_fn = path.join(self.params.intermediate_dir, fn);
            self.log('Replaying "' + fn + '": begin.');
            self.ms = {
                earliest: null,
                latest: null,
            };
            var lines = {
                total: 0,
                good: 0,
            };
            _.each(fs.readFileSync(full_fn).toString().split('\n'), function(line) {
                if (line) {
                    ++lines.total;
                    var e = safeParseJson(line);
                    if (_.isObject(e) && _.isNumber(e.ms)) {
                        ++lines.good;
                        ++self.entries;
                        self.ensureFileIsOpen();
                        if (!self.ms.earliest || e.ms < self.ms.earliest) {
                            self.ms.earliest = e.ms;
                        }
                        if (!self.ms.latest || e.ms > self.ms.latest) {
                            self.ms.latest = e.ms;
                        }
                        fs.writeSync(self.fd, JSON.stringify(e) + '\n');
                        ++self.entries;
                    }
                }
            });
            self.log(
                'Replaying "' + fn + '": ' + lines.good + ' entries parsed' +
                (lines.total === lines.good ? '' : ' (out of ' + lines.total + ' lines)') +
                '.');
            self.ensureFileIsFlushed();
            self.log(
                'Replaying "' + fn + '": done.');
            fs.unlinkSync(full_fn);
        });
    }
};

module.exports.spawn = function(config, callback) {
    assert(_.isFunction(callback));
    var lockdir = config.storer_lockdir || config.storer_workdir;
    assert(_.isString(lockdir) && lockdir !== '');
    mkpath.sync(lockdir);
    console.log('Lock ("' + lockdir + '" / "' + config.storer_lockname + '"): acquiring.');
    pidlock.guard(lockdir, config.storer_lockname, function(error, data, cleanup) {
        if (error) {
            console.log('Lock ("' + lockdir + '" / "' + config.storer_lockname + '"): busy.');
        } else {
            console.log('Lock ("' + lockdir + '" / "' + config.storer_lockname + '"): acquired.');

            var intermediate_dir = config.storer_intermediate_dir || path.join(config.storer_workdir, '/intermediate');
            console.log('Intermediate dir "' + intermediate_dir + '".');
            mkpath.sync(intermediate_dir);
            var destination_dir = config.storer_destination_dir || path.join(config.storer_workdir, '/destination');
            console.log('Destination dir "' + destination_dir + '".');
            mkpath.sync(destination_dir);

            var impl = new Impl({
                verbose: config.verbose,
                intermediate_dir: intermediate_dir,
                destination_dir: destination_dir,
                log_frequency: config.storer_log_frequency,
                max_time_discrepancy_ms: config.storer_max_time_discrepancy_ms,
                max_entries_per_file: config.storer_max_entries_per_file,
                max_file_age_ms: config.storer_max_file_age_ms,
            });

            function tearDown(callback) {
                console.log('Lock ("' + lockdir + '" / "' + config.storer_lockname + '"): releasing.');
                cleanup();
                console.log('Lock ("' + lockdir + '" / "' + config.storer_lockname + '"): releases.');
                if (_.isFunction(callback)) {
                    callback();
                }
            };

            callback({
                push: function(entry, callback) {
                    var json;
                    if (_.isString(entry)) {
                        if (config.storer_debug && entry === 'STOP') {
                            tearDown(function() {
                                process.exit(0);
                            });
                            return;
                        } else if (config.storer_debug && entry === 'CONFIG') {
                            console.log(JSON.stringify(config, null, 2));
                            if (_.isFunction(callback)) {
                                setTimeout(callback, 0);
                            }
                            return;
                        } else if (config.storer_debug && entry === 'CREATE') {
                            impl.ensureFileIsOpen();
                            if (_.isFunction(callback)) {
                                setTimeout(callback, 0);
                            }
                            return;
                        } else if (config.storer_debug && entry === 'FLUSH') {
                            impl.ensureFileIsFlushed();
                            if (_.isFunction(callback)) {
                                setTimeout(callback, 0);
                            }
                            return;
                        } else if (config.storer_debug && entry === 'STATUS') {
                            console.log(JSON.stringify(impl, null, 2));
                            if (_.isFunction(callback)) {
                                setTimeout(callback, 0);
                            }
                            return;
                        }
                        json = safeParseJson(entry);
                    } else {
                        json = entry;
                    }
                    if (_.isObject(json)) {
                        impl.appendEntry(json);
                    } else {
                        impl.log('INVALID_JSON');
                    }
                    if (_.isFunction(callback)) {
                        setTimeout(callback, 0);
                    }
                },
                shutdown: function(callback) {
                    impl.tearDown();
                    tearDown(callback);
                }
            });
        }
    });
};

if (require.main === module) {
    var config = require('./config').fromCommandLine();
    module.exports.spawn(config, function(storer) {
        var rl = require('readline').createInterface(process.stdin, process.stdout);
        var lock;
        rl.on('line', function(line) {
            synchronized(lock, function(callback) {
                storer.push(line, callback);
            });
        });
        rl.on('close', function() {
            synchronized(lock, function(callback) {
                console.log('Tearing down.');
                storer.shutdown(function() {
                    console.log('Done.');
                    process.exit(0);
                });
            });
        });
    });
}
<|endoftext|>


<commit_before>/**
  This view handles the rendering of a topic list

  @class ListTopicsView
  @extends Discourse.View
  @namespace Discourse
  @uses Discourse.LoadMore
  @module Discourse
**/
Discourse.ListTopicsView = Discourse.View.extend(Discourse.LoadMore, {
  templateName: 'list/topics',
  categoryBinding: 'controller.controllers.list.category',
  canCreateTopicBinding: 'controller.controllers.list.canCreateTopic',
  listBinding: 'controller.model',
  loadedMore: false,
  currentTopicId: null,
  eyelineSelector: '.topic-list-item',

  topicTrackingState: function() {
    return Discourse.TopicTrackingState.current();
  }.property(),

  didInsertElement: function() {
    this._super();
    Em.run.schedule('afterRender', function() {
      $('html, body').scrollTop(0);
    });
  },

  hasTopics: Em.computed.gt('list.topics.length', 0),
  showTable: Em.computed.or('hasTopics', 'topicTrackingState.hasIncoming'),

  updateTitle: function(){
    Discourse.notifyTitle(this.get('topicTrackingState.incomingCount'));
  }.observes('topicTrackingState.incomingCount'),

  loadMore: function() {
    var listTopicsView = this;
    Discourse.notifyTitle(0);
    listTopicsView.get('controller').loadMore().then(function (hasMoreResults) {
      Em.run.schedule('afterRender', function() {
        listTopicsView.saveScrollPosition();
      });
      if (!hasMoreResults) {
        listTopicsView.get('eyeline').flushRest();
      }
    });
  },

  // Remember where we were scrolled to
  saveScrollPosition: function() {
    Discourse.Session.current('topicListScrollPosition', $(window).scrollTop());
  },

  // When the topic list is scrolled
  scrolled: function(e) {
    this._super();
    this.saveScrollPosition();
  }

});

<commit_msg>oh my this convenience method sure is not convenient<commit_after>/**
  This view handles the rendering of a topic list

  @class ListTopicsView
  @extends Discourse.View
  @namespace Discourse
  @uses Discourse.LoadMore
  @module Discourse
**/
Discourse.ListTopicsView = Discourse.View.extend(Discourse.LoadMore, {
  templateName: 'list/topics',
  categoryBinding: 'controller.controllers.list.category',
  canCreateTopicBinding: 'controller.controllers.list.canCreateTopic',
  listBinding: 'controller.model',
  loadedMore: false,
  currentTopicId: null,
  eyelineSelector: '.topic-list-item',

  topicTrackingState: function() {
    return Discourse.TopicTrackingState.current();
  }.property(),

  didInsertElement: function() {
    this._super();
    Em.run.schedule('afterRender', function() {
      $('html, body').scrollTop(0);
    });
  },

  hasTopics: Em.computed.gt('list.topics.length', 0),
  showTable: Em.computed.or('hasTopics', 'topicTrackingState.hasIncoming'),

  updateTitle: function(){
    Discourse.notifyTitle(this.get('topicTrackingState.incomingCount'));
  }.observes('topicTrackingState.incomingCount'),

  loadMore: function() {
    var listTopicsView = this;
    Discourse.notifyTitle(0);
    listTopicsView.get('controller').loadMore().then(function (hasMoreResults) {
      Em.run.schedule('afterRender', function() {
        listTopicsView.saveScrollPosition();
      });
      if (!hasMoreResults) {
        listTopicsView.get('eyeline').flushRest();
      }
    });
  },

  // Remember where we were scrolled to
  saveScrollPosition: function() {
    Discourse.Session.current().set('topicListScrollPosition', $(window).scrollTop());
  },

  // When the topic list is scrolled
  scrolled: function(e) {
    this._super();
    this.saveScrollPosition();
  }

});

<|endoftext|>