<commit_before>/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2012 Google Inc.
 * https://developers.google.com/blockly/
 *
 * 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.
 */

/**
 * @fileoverview Variable input field.
 * @author fraser@google.com (Neil Fraser)
 */
'use strict';

goog.provide('Blockly.FieldVariable');

goog.require('Blockly.FieldDropdown');
goog.require('Blockly.Msg');
goog.require('Blockly.VariableModel');
goog.require('Blockly.Variables');
goog.require('goog.asserts');
goog.require('goog.string');

/**
 * Class for a variable's dropdown field.
 * @param {?string} varname The default name for the variable.  If null,
 *     a unique variable name will be generated.
 * @param {Function=} opt_validator A function that is executed when a new
 *     option is selected.  Its sole argument is the new option value.
 * @param {Array.<string>} opt_variableTypes A list of the types of variables to
 *     include in the dropdown.
 * @extends {Blockly.FieldDropdown}
 * @constructor
 */
Blockly.FieldVariable = function(varname, opt_validator, opt_variableTypes) {
  Blockly.FieldVariable.superClass_.constructor.call(this,
      Blockly.FieldVariable.dropdownCreate, opt_validator);
  this.setValue(varname || '');
  this.addArgType('variable');
  this.variableTypes = opt_variableTypes;
};
goog.inherits(Blockly.FieldVariable, Blockly.FieldDropdown);

/**
 * Install this dropdown on a block.
 */
Blockly.FieldVariable.prototype.init = function() {
  if (this.fieldGroup_) {
    // Dropdown has already been initialized once.
    return;
  }
  Blockly.FieldVariable.superClass_.init.call(this);

  // TODO (1010): Change from init/initModel to initView/initModel
  this.initModel();
};

Blockly.FieldVariable.prototype.initModel = function() {
  if (!this.getValue()) {
    // Variables without names get uniquely named for this workspace.
    var workspace =
        this.sourceBlock_.isInFlyout ?
            this.sourceBlock_.workspace.targetWorkspace :
            this.sourceBlock_.workspace;
    this.setValue(Blockly.Variables.generateUniqueName(workspace));
  }
  // If the selected variable doesn't exist yet, create it.
  // For instance, some blocks in the toolbox have variable dropdowns filled
  // in by default.
  if (!this.sourceBlock_.isInFlyout) {
    // Check if there was exactly one element specified in the
    // variableTypes list. This is the list that specifies which types of
    // variables to include in the dropdown.
    // If there is exactly one element specified, make the variable
    // being created this specified type. Else, default behavior is to create
    // a scalar variable
    if (this.getVariableTypes_().length == 1) {
      this.sourceBlock_.workspace.createVariable(this.getValue(),
        this.getVariableTypes_()[0]);
    } else {
      this.sourceBlock_.workspace.createVariable(this.getValue());
    }
  }
};

/**
 * Attach this field to a block.
 * @param {!Blockly.Block} block The block containing this field.
 */
Blockly.FieldVariable.prototype.setSourceBlock = function(block) {
  goog.asserts.assert(!block.isShadow(),
      'Variable fields are not allowed to exist on shadow blocks.');
  Blockly.FieldVariable.superClass_.setSourceBlock.call(this, block);
};

/**
 * Get the variable's name (use a variableDB to convert into a real name).
 * Unline a regular dropdown, variables are literal and have no neutral value.
 * @return {string} Current text.
 */
Blockly.FieldVariable.prototype.getValue = function() {
  return this.getText();
};

/**
 * Set the variable name.
 * @param {string} value New text.
 */
Blockly.FieldVariable.prototype.setValue = function(value) {
  var newValue = value;
  var newText = value;

  if (this.sourceBlock_) {
    var variable = this.sourceBlock_.workspace.getVariableById(value);
    if (variable) {
      newText = variable.name;
    }
    // TODO(marisaleung): Remove name lookup after converting all Field Variable
    //     instances to use id instead of name.
    else if (variable = this.sourceBlock_.workspace.getVariable(value)) {
      newValue = variable.getId();
    }
    if (Blockly.Events.isEnabled()) {
      Blockly.Events.fire(new Blockly.Events.BlockChange(
          this.sourceBlock_, 'field', this.name, this.value_, newValue));
    }
  }
  this.value_ = newValue;
  this.setText(newText);
};

/**
 * Return a list of variable types to include in the dropdown.
 * @return {!Array.<string>} Array of variable types.
 * @throws {Error} if variableTypes is an empty array.
 * @private
 */
Blockly.FieldVariable.prototype.getVariableTypes_ = function() {
  var variableTypes = this.variableTypes;
  if (variableTypes === null) {
    // If variableTypes is null, return all variable types.
    if (this.sourceBlock_) {
      var workspace = this.sourceBlock_.workspace;
      return workspace.getVariableTypes();
    }
  }
  variableTypes = variableTypes || [''];
  if (variableTypes.length == 0) {
    // Throw an error if variableTypes is an empty list.
    var name = this.getText();
    throw new Error('\'variableTypes\' of field variable ' +
      name + ' was an empty list');
  }
  return variableTypes;
};

/**
 * Return a sorted list of variable names for variable dropdown menus.
 * Include a special option at the end for creating a new variable name.
 * @return {!Array.<string>} Array of variable names.
 * @this {Blockly.FieldVariable}
 */
Blockly.FieldVariable.dropdownCreate = function() {
  var variableModelList = [];
  var name = this.getText();
  // Don't create a new variable if there is nothing selected.
  var createSelectedVariable = name ? true : false;
  var workspace = null;
  if (this.sourceBlock_) {
    workspace = this.sourceBlock_.workspace;
  }
  var isBroadcastType = false;
  if (workspace) {
    var variableTypes = this.getVariableTypes_();
    var variableModelList = [];
    // Get a copy of the list, so that adding rename and new variable options
    // doesn't modify the workspace's list.
    for (var i = 0; i < variableTypes.length; i++) {
      var variableType = variableTypes[i];
      if (variableType == Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE){
        isBroadcastType = true;
      }
      var variables = workspace.getVariablesOfType(variableType);
      variableModelList = variableModelList.concat(variables);
    }
    for (var i = 0; i < variableModelList.length; i++){
      if (createSelectedVariable &&
          goog.string.caseInsensitiveEquals(variableModelList[i].name, name)) {
        createSelectedVariable = false;
        break;
      }
    }
  }
  // Ensure that the currently selected variable is an option.
  if (createSelectedVariable && workspace) {
    var newVar = null;
    if (isBroadcastType && workspace.isFlyout) {
      newVar = workspace.targetWorkspace.createVariable(name, Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE);
    } else {
      newVar = workspace.createVariable(name);
    }
    variableModelList.push(newVar);
  }
  variableModelList.sort(Blockly.VariableModel.compareByName);
  var options = [];
  for (var i = 0; i < variableModelList.length; i++) {
    // Set the uuid as the internal representation of the variable.
    options[i] = [variableModelList[i].name, variableModelList[i].getId()];
  }
  if (isBroadcastType) {
    options.push([Blockly.Msg.NEW_BROADCAST_MESSAGE, Blockly.NEW_BROADCAST_MESSAGE_ID]);
  } else {
    options.push([Blockly.Msg.RENAME_VARIABLE, Blockly.RENAME_VARIABLE_ID]);
    if (Blockly.Msg.DELETE_VARIABLE) {
      options.push([Blockly.Msg.DELETE_VARIABLE.replace('%1', name),
          Blockly.DELETE_VARIABLE_ID]);
    }
  }
  return options;
};

/**
 * Handle the selection of an item in the variable dropdown menu.
 * Special case the 'Rename variable...' and 'Delete variable...' options.
 * In the rename case, prompt the user for a new name.
 * @param {!goog.ui.Menu} menu The Menu component clicked.
 * @param {!goog.ui.MenuItem} menuItem The MenuItem selected within menu.
 */
Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) {
  var id = menuItem.getValue();
  // TODO(marisaleung): change setValue() to take in an id as the parameter.
  // Then remove itemText.
  var itemText;
  if (this.sourceBlock_ && this.sourceBlock_.workspace) {
    var workspace = this.sourceBlock_.workspace;
    var variable = workspace.getVariableById(id);
    // If the item selected is a variable, set itemText to the variable name.
    if (variable) {
      itemText = variable.name;
    }
    else if (id == Blockly.RENAME_VARIABLE_ID) {
      // Rename variable.
      var currentName = this.getText();
      variable = workspace.getVariable(currentName);
      Blockly.Variables.renameVariable(workspace, variable);
      return;
    } else if (id == Blockly.DELETE_VARIABLE_ID) {
      // Delete variable.
      workspace.deleteVariable(this.getText());
      return;
    } else if (id == Blockly.NEW_BROADCAST_MESSAGE_ID) {
      var thisField = this;
      var setName = function(newName) {
        if (newName) {
          thisField.setValue(newName);
        }
      };
      var broadcastMsgWkspc = workspace.isFlyout ? workspace.targetWorkspace : workspace;
      Blockly.Variables.createVariable(broadcastMsgWkspc, setName, Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE);
      return;
    }

    // Call any validation function, and allow it to override.
    itemText = this.callValidator(itemText);
  }
  if (itemText !== null) {
    this.setValue(itemText);
  }
};
<commit_msg>Accomodating implicit deletion. Broadcast blocks in the flyout will now follow scratch 2 behavior of displaying the message that comes first in sorted order (same sort as dropdown)<commit_after>/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2012 Google Inc.
 * https://developers.google.com/blockly/
 *
 * 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.
 */

/**
 * @fileoverview Variable input field.
 * @author fraser@google.com (Neil Fraser)
 */
'use strict';

goog.provide('Blockly.FieldVariable');

goog.require('Blockly.FieldDropdown');
goog.require('Blockly.Msg');
goog.require('Blockly.VariableModel');
goog.require('Blockly.Variables');
goog.require('goog.asserts');
goog.require('goog.string');

/**
 * Class for a variable's dropdown field.
 * @param {?string} varname The default name for the variable.  If null,
 *     a unique variable name will be generated.
 * @param {Function=} opt_validator A function that is executed when a new
 *     option is selected.  Its sole argument is the new option value.
 * @param {Array.<string>} opt_variableTypes A list of the types of variables to
 *     include in the dropdown.
 * @extends {Blockly.FieldDropdown}
 * @constructor
 */
Blockly.FieldVariable = function(varname, opt_validator, opt_variableTypes) {
  Blockly.FieldVariable.superClass_.constructor.call(this,
      Blockly.FieldVariable.dropdownCreate, opt_validator);
  this.setValue(varname || '');
  this.addArgType('variable');
  this.variableTypes = opt_variableTypes;
  this.variableType = (opt_variableTypes && opt_variableTypes.length == 1) ?
    opt_variableTypes[0] : '';
};
goog.inherits(Blockly.FieldVariable, Blockly.FieldDropdown);

/**
 * Install this dropdown on a block.
 */
Blockly.FieldVariable.prototype.init = function() {
  if (this.fieldGroup_) {
    // Dropdown has already been initialized once.
    return;
  }
  Blockly.FieldVariable.superClass_.init.call(this);

  // TODO (1010): Change from init/initModel to initView/initModel
  this.initModel();
};

Blockly.FieldVariable.prototype.initModel = function() {
  if (!this.getValue()) {
    // Variables without names get uniquely named for this workspace.
    var workspace =
        this.sourceBlock_.isInFlyout ?
            this.sourceBlock_.workspace.targetWorkspace :
            this.sourceBlock_.workspace;
    this.setValue(Blockly.Variables.generateUniqueName(workspace));
  }
  // If the selected variable doesn't exist yet, create it.
  // For instance, some blocks in the toolbox have variable dropdowns filled
  // in by default.
  if (!this.sourceBlock_.isInFlyout) {
    // Check if there was exactly one element specified in the
    // variableTypes list. This is the list that specifies which types of
    // variables to include in the dropdown.
    this.sourceBlock_.workspace.createVariable(this.getValue(), this.variableType);
  } else {
    // Using shorter name for this constant
    var broadcastMsgType = Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE;
    var broadcastVars = this.sourceBlock_.workspace.getVariablesOfType(broadcastMsgType);
    if (this.variableType == broadcastMsgType && broadcastVars.length != 0) {
      broadcastVars.sort(Blockly.VariableModel.compareByName);
      this.setValue(broadcastVars[0].name);
    }
  }
};

/**
 * Attach this field to a block.
 * @param {!Blockly.Block} block The block containing this field.
 */
Blockly.FieldVariable.prototype.setSourceBlock = function(block) {
  goog.asserts.assert(!block.isShadow(),
      'Variable fields are not allowed to exist on shadow blocks.');
  Blockly.FieldVariable.superClass_.setSourceBlock.call(this, block);
};

/**
 * Get the variable's name (use a variableDB to convert into a real name).
 * Unline a regular dropdown, variables are literal and have no neutral value.
 * @return {string} Current text.
 */
Blockly.FieldVariable.prototype.getValue = function() {
  return this.getText();
};

/**
 * Set the variable name.
 * @param {string} value New text.
 */
Blockly.FieldVariable.prototype.setValue = function(value) {
  var newValue = value;
  var newText = value;

  if (this.sourceBlock_) {
    var variable = this.sourceBlock_.workspace.getVariableById(value);
    if (variable) {
      newText = variable.name;
    }
    // TODO(marisaleung): Remove name lookup after converting all Field Variable
    //     instances to use id instead of name.
    else if (variable = this.sourceBlock_.workspace.getVariable(value)) {
      newValue = variable.getId();
    }
    if (Blockly.Events.isEnabled()) {
      Blockly.Events.fire(new Blockly.Events.BlockChange(
          this.sourceBlock_, 'field', this.name, this.value_, newValue));
    }
  }
  this.value_ = newValue;
  this.setText(newText);
};

/**
 * Return a list of variable types to include in the dropdown.
 * @return {!Array.<string>} Array of variable types.
 * @throws {Error} if variableTypes is an empty array.
 * @private
 */
Blockly.FieldVariable.prototype.getVariableTypes_ = function() {
  var variableTypes = this.variableTypes;
  if (variableTypes === null) {
    // If variableTypes is null, return all variable types.
    if (this.sourceBlock_) {
      var workspace = this.sourceBlock_.workspace;
      return workspace.getVariableTypes();
    }
  }
  variableTypes = variableTypes || [''];
  if (variableTypes.length == 0) {
    // Throw an error if variableTypes is an empty list.
    var name = this.getText();
    throw new Error('\'variableTypes\' of field variable ' +
      name + ' was an empty list');
  }
  return variableTypes;
};

/**
 * Return a sorted list of variable names for variable dropdown menus.
 * Include a special option at the end for creating a new variable name.
 * @return {!Array.<string>} Array of variable names.
 * @this {Blockly.FieldVariable}
 */
Blockly.FieldVariable.dropdownCreate = function() {
  var variableModelList = [];
  var name = this.getText();
  // Don't create a new variable if there is nothing selected.
  var createSelectedVariable = name ? true : false;
  var workspace = null;
  if (this.sourceBlock_) {
    workspace = this.sourceBlock_.workspace;
  }
  var isBroadcastType = false;
  if (workspace) {
    var variableTypes = this.getVariableTypes_();
    var variableModelList = [];
    // Get a copy of the list, so that adding rename and new variable options
    // doesn't modify the workspace's list.
    for (var i = 0; i < variableTypes.length; i++) {
      var variableType = variableTypes[i];
      if (variableType == Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE){
        isBroadcastType = true;
      }
      var variables = workspace.getVariablesOfType(variableType);
      variableModelList = variableModelList.concat(variables);
    }
    for (var i = 0; i < variableModelList.length; i++){
      if (createSelectedVariable &&
          goog.string.caseInsensitiveEquals(variableModelList[i].name, name)) {
        createSelectedVariable = false;
        break;
      }
    }
  }
  // Ensure that the currently selected variable is an option.
  if (createSelectedVariable && workspace) {
    var newVar = null;
    if (isBroadcastType && workspace.isFlyout) {
      newVar = workspace.targetWorkspace.createVariable(name, Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE);
    } else {
      newVar = workspace.createVariable(name);
    }
    variableModelList.push(newVar);
  }
  variableModelList.sort(Blockly.VariableModel.compareByName);
  var options = [];
  for (var i = 0; i < variableModelList.length; i++) {
    // Set the uuid as the internal representation of the variable.
    options[i] = [variableModelList[i].name, variableModelList[i].getId()];
  }
  if (isBroadcastType) {
    options.push([Blockly.Msg.NEW_BROADCAST_MESSAGE, Blockly.NEW_BROADCAST_MESSAGE_ID]);
  } else {
    options.push([Blockly.Msg.RENAME_VARIABLE, Blockly.RENAME_VARIABLE_ID]);
    if (Blockly.Msg.DELETE_VARIABLE) {
      options.push([Blockly.Msg.DELETE_VARIABLE.replace('%1', name),
          Blockly.DELETE_VARIABLE_ID]);
    }
  }
  return options;
};

/**
 * Handle the selection of an item in the variable dropdown menu.
 * Special case the 'Rename variable...' and 'Delete variable...' options.
 * In the rename case, prompt the user for a new name.
 * @param {!goog.ui.Menu} menu The Menu component clicked.
 * @param {!goog.ui.MenuItem} menuItem The MenuItem selected within menu.
 */
Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) {
  var id = menuItem.getValue();
  // TODO(marisaleung): change setValue() to take in an id as the parameter.
  // Then remove itemText.
  var itemText;
  if (this.sourceBlock_ && this.sourceBlock_.workspace) {
    var workspace = this.sourceBlock_.workspace;
    var variable = workspace.getVariableById(id);
    // If the item selected is a variable, set itemText to the variable name.
    if (variable) {
      itemText = variable.name;
    }
    else if (id == Blockly.RENAME_VARIABLE_ID) {
      // Rename variable.
      var currentName = this.getText();
      variable = workspace.getVariable(currentName);
      Blockly.Variables.renameVariable(workspace, variable);
      return;
    } else if (id == Blockly.DELETE_VARIABLE_ID) {
      // Delete variable.
      workspace.deleteVariable(this.getText());
      return;
    } else if (id == Blockly.NEW_BROADCAST_MESSAGE_ID) {
      var thisField = this;
      var setName = function(newName) {
        if (newName) {
          thisField.setValue(newName);
        }
      };
      var broadcastMsgWkspc = workspace.isFlyout ? workspace.targetWorkspace : workspace;
      Blockly.Variables.createVariable(broadcastMsgWkspc, setName, Blockly.BROADCAST_MESSAGE_VARIABLE_TYPE);
      return;
    }

    // Call any validation function, and allow it to override.
    itemText = this.callValidator(itemText);
  }
  if (itemText !== null) {
    this.setValue(itemText);
  }
};
<|endoftext|>


<commit_before>var cadence = require('cadence')
var Procession = require('procession')

// Utilities.
var coalesce = require('extant')

// Evented stream reading and writing.
var Staccato = require('staccato')

// JSON for use in packets.
var Jacket = require('nascent.jacket')

// Ever increasing serial value with no maximum value.
var Monotonic = require('monotonic').asString

// Orderly destruction of complicated objects.
var Destructible = require('destructible')

var Signal = require('signal')

var Turnstile = require('turnstile/redux')

Turnstile.Queue = require('turnstile/queue')

function Conduit (input, output) {
    this._destructible = new Destructible('conduit')
    this._destructible.markDestroyed(this)
    this._turnstile = new Turnstile
    this._queue = new Turnstile.Queue(this, '_write', this._turnstile)
    this.destroyed = false
    this._input = new Staccato.Readable(input)
    this._destructible.addDestructor('turnstile', this._turnstile, 'pause')
    this._destructible.addDestructor('input', this._input, 'destroy')
    this._output = new Staccato.Writable(output)
    this._destructible.addDestructor('output', this._output, 'destroy')
    this.read = new Procession
    this.write = new Procession
    this.wrote = new Procession
    this.write.pump(this, 'enqueue')
    this._record = new Jacket
    this._closed = new Signal
    this._destructible.addDestructor('closed', this._closed, 'unlatch')
    this.ready = new Signal
    this._destructible.addDestructor('ready', this.ready, 'unlatch')
    this._slices = []
}

Conduit.prototype._write = cadence(function (async, envelope) {
    if (this.destroyed) {
        return []
    }
    envelope = envelope.body
    async(function () {
        if (envelope == null) {
            // TODO And then destroy the conduit.
            this._output.write(JSON.stringify({
                module: 'conduit',
                method: 'trailer',
                body: null
            }) + '\n', async())
            this._closed.unlatch()
        } else {
            var e = envelope
            while (e.body != null && typeof e.body == 'object' && !Buffer.isBuffer(e.body)) {
                e = e.body
            }
            if (Buffer.isBuffer(e.body)) {
                var body = e.body
                e.body = null
                var packet = JSON.stringify({
                    module: 'conduit',
                    method: 'chunk',
                    length: body.length,
                    body: envelope
                }) + '\n'
                e.body = body
                this._output.write(packet, async())
                this._output.write(e.body, async())
            } else {
                this._output.write(JSON.stringify({
                    module: 'conduit',
                    method: 'envelope',
                    body: envelope
                }) + '\n', async())
            }
        }
    }, function () {
        this.wrote.enqueue(envelope, async())
    }, function () {
        return []
    })
})

Conduit.prototype.enqueue = function (envelope, callback) {
    this._queue.enqueue(envelope, callback)
}

Conduit.prototype._pump = cadence(function (async, buffer) {
    async(function () {
        this._parse(coalesce(buffer, new Buffer(0)), async())
        this.ready.unlatch()
    }, function () {
        this._read(async())
    }, function () {
        this._closed.wait(async())
    })
})

Conduit.prototype.listen = cadence(function (async, buffer) {
    this._destructible.addDestructor('shutdown', this, '_shutdown')
    this._queue.turnstile.listen(this._destructible.monitor('turnstile'))
    this._pump(buffer, this._destructible.monitor('pump'))
    this._destructible.completed(async())
})

Conduit.prototype._shutdown = function () {
    this.read.push(null)
}

Conduit.prototype.destroy = function () {
    this._turnstile.close()
}

Conduit.prototype._buffer = cadence(function (async, buffer, start, end) {
    async(function () {
        var length = Math.min(buffer.length - start, this._chunk.length)
        var slice = buffer.slice(start, start + length)
        start += length
        this._chunk.length -= length
        if (this._chunk.length == 0) {
            if (this._slices.length) {
                this._slices.push(slice)
                slice = Buffer.concat(this._slices)
                this._slices.length = 0
            } else {
                slice = new Buffer(slice)
            }
            var envelope = this._chunk.body
            var e = envelope
            while (e.body != null) {
                e = e.body
            }
            e.body = slice
            this._chunk = null
            this._record = new Jacket
            this.read.enqueue(envelope, async())
        } else {
            this._slices.push(new Buffer(slice))
        }
    }, function () {
        return start
    })
})

Conduit.prototype._json = cadence(function (async, buffer, start, end) {
    start = this._record.parse(buffer, start, end)
    async(function () {
        if (this._record.object != null) {
            var envelope = this._record.object
            switch (envelope.method) {
            case 'envelope':
                this.read.enqueue(envelope.body, async())
                break
            case 'chunk':
                this._chunk = this._record.object
                break
            case 'trailer':
                // var socket = this._sockets[envelope.to]
                this.read.enqueue(null, async())
                // delete this._sockets[envelope.to]
                break
            }
            this._record = new Jacket
        }
    }, function () {
        return start
    })
})

Conduit.prototype._parse = cadence(function (async, buffer) {
    var parse = async(function (start) {
        if (start == buffer.length) {
            return [ parse.break ]
        }
        if (this._chunk != null) {
            this._buffer(buffer, start, buffer.length, async())
        } else {
            this._json(buffer, start, buffer.length, async())
        }
    })(0)
})

Conduit.prototype._read = cadence(function (async) {
    var read = async(function () {
        this._input.read(async())
    }, function (buffer) {
        if (buffer == null) {
            return [ read.break ]
        }
        this._parse(buffer, async())
    })()
})

module.exports = Conduit
<commit_msg>100% test coverage.<commit_after>var cadence = require('cadence')
var Procession = require('procession')

// Utilities.
var coalesce = require('extant')

// Evented stream reading and writing.
var Staccato = require('staccato')

// JSON for use in packets.
var Jacket = require('nascent.jacket')

// Ever increasing serial value with no maximum value.
var Monotonic = require('monotonic').asString

// Orderly destruction of complicated objects.
var Destructible = require('destructible')

var Signal = require('signal')

var Turnstile = require('turnstile/redux')

Turnstile.Queue = require('turnstile/queue')

function Conduit (input, output) {
    this._destructible = new Destructible('conduit')
    this._destructible.markDestroyed(this)
    this._turnstile = new Turnstile
    this._queue = new Turnstile.Queue(this, '_write', this._turnstile)
    this.destroyed = false
    this._input = new Staccato.Readable(input)
    this._destructible.addDestructor('turnstile', this._turnstile, 'pause')
    this._destructible.addDestructor('input', this._input, 'destroy')
    this._output = new Staccato.Writable(output)
    this._destructible.addDestructor('output', this._output, 'destroy')
    this.read = new Procession
    this.write = new Procession
    this.wrote = new Procession
    this.write.pump(this, 'enqueue')
    this._record = new Jacket
    this._closed = new Signal
    this._destructible.addDestructor('closed', this._closed, 'unlatch')
    this.ready = new Signal
    this._destructible.addDestructor('ready', this.ready, 'unlatch')
    this._slices = []
}

Conduit.prototype._write = cadence(function (async, envelope) {
    envelope = envelope.body
    async(function () {
        if (envelope == null) {
            // TODO And then destroy the conduit.
            this._output.write(JSON.stringify({
                module: 'conduit',
                method: 'trailer',
                body: null
            }) + '\n', async())
            this._closed.unlatch()
        } else {
            var e = envelope
            while (e.body != null && typeof e.body == 'object' && !Buffer.isBuffer(e.body)) {
                e = e.body
            }
            if (Buffer.isBuffer(e.body)) {
                var body = e.body
                e.body = null
                var packet = JSON.stringify({
                    module: 'conduit',
                    method: 'chunk',
                    length: body.length,
                    body: envelope
                }) + '\n'
                e.body = body
                this._output.write(packet, async())
                this._output.write(e.body, async())
            } else {
                this._output.write(JSON.stringify({
                    module: 'conduit',
                    method: 'envelope',
                    body: envelope
                }) + '\n', async())
            }
        }
    }, function () {
        this.wrote.enqueue(envelope, async())
    }, function () {
        return []
    })
})

Conduit.prototype.enqueue = function (envelope, callback) {
    this._queue.enqueue(envelope, callback)
}

Conduit.prototype._pump = cadence(function (async, buffer) {
    async(function () {
        this._parse(coalesce(buffer, new Buffer(0)), async())
        this.ready.unlatch()
    }, function () {
        this._read(async())
    }, function () {
        this._closed.wait(async())
    })
})

Conduit.prototype.listen = cadence(function (async, buffer) {
    this._destructible.addDestructor('shutdown', this, '_shutdown')
    this._queue.turnstile.listen(this._destructible.monitor('turnstile'))
    this._pump(buffer, this._destructible.monitor('pump'))
    this._destructible.completed(async())
})

Conduit.prototype._shutdown = function () {
    this.read.push(null)
}

Conduit.prototype.destroy = function () {
    this._turnstile.close()
}

Conduit.prototype._buffer = cadence(function (async, buffer, start, end) {
    async(function () {
        var length = Math.min(buffer.length - start, this._chunk.length)
        var slice = buffer.slice(start, start + length)
        start += length
        this._chunk.length -= length
        if (this._chunk.length == 0) {
            if (this._slices.length) {
                this._slices.push(slice)
                slice = Buffer.concat(this._slices)
                this._slices.length = 0
            } else {
                slice = new Buffer(slice)
            }
            var envelope = this._chunk.body
            var e = envelope
            while (e.body != null) {
                e = e.body
            }
            e.body = slice
            this._chunk = null
            this._record = new Jacket
            this.read.enqueue(envelope, async())
        } else {
            this._slices.push(new Buffer(slice))
        }
    }, function () {
        return start
    })
})

Conduit.prototype._json = cadence(function (async, buffer, start, end) {
    start = this._record.parse(buffer, start, end)
    async(function () {
        if (this._record.object != null) {
            var envelope = this._record.object
            switch (envelope.method) {
            case 'envelope':
                this.read.enqueue(envelope.body, async())
                break
            case 'chunk':
                this._chunk = this._record.object
                break
            case 'trailer':
                // var socket = this._sockets[envelope.to]
                this.read.enqueue(null, async())
                // delete this._sockets[envelope.to]
                break
            }
            this._record = new Jacket
        }
    }, function () {
        return start
    })
})

Conduit.prototype._parse = cadence(function (async, buffer) {
    var parse = async(function (start) {
        if (start == buffer.length) {
            return [ parse.break ]
        }
        if (this._chunk != null) {
            this._buffer(buffer, start, buffer.length, async())
        } else {
            this._json(buffer, start, buffer.length, async())
        }
    })(0)
})

Conduit.prototype._read = cadence(function (async) {
    var read = async(function () {
        this._input.read(async())
    }, function (buffer) {
        if (buffer == null) {
            return [ read.break ]
        }
        this._parse(buffer, async())
    })()
})

module.exports = Conduit
<|endoftext|>


<commit_before>"use strict";

var rateLimiter = exports,
    _ = require("underscore")._,
    limitsConfig = require("./limits-config"),
    ipResolver = require("./ipresolver"),
    CounterStore = require("./counter").CounterStore,
    Configuration = require("./limits-config").Configuration,
    httpProxy = require('http-proxy'),
    http = require("http"),
    url = require('url'),
    log4js = require('log4js'),
    EventEmitter = require('events').EventEmitter;

rateLimiter.RateLimiter = RateLimiter;
var logger = log4js.getLogger();

// http://nodejs.org/docs/v0.10.35/api/http.html#http_agent_maxsockets
// in v0.12 maxSockets default was changed to Infinity
http.globalAgent.maxSockets = Infinity;

var defaultOptions = {
    forwardHost: "localhost",
    forwardPort: 80,
    configRefreshInterval: 60000,
    configEndpoint: "/rate_limiter",
    bucketHeaderName: "X-RateLimiter-Bucket"
};

function RateLimiter(options) {
    this.configuration = null;
    this.onConfigurationUpdated = null;

    this.options = this.validateOptions(options);
    log4js.configure(options.log4js);
    this.IPResolver = new ipResolver.IPResolver(options.forwarded_headers);

    this.updateConfig(limitsConfig.defaultConfig);
    this.proxyEvent = new EventEmitter();
    this.counter = new CounterStore();
    this.initProxy();

    this.loadConfig();
    if (this.options.configRefreshInterval > 0) {
        if (this.options.configRefreshInterval < 5000) {
            logger.warn("configRefreshInterval should be >= 5000 (5sec) -> automatic config update turned off");
        } else {
            var _this = this;
            setInterval(function () {
                _this.loadConfig();
            }, this.options.configRefreshInterval);
        }
    }
}

RateLimiter.prototype = {

    evaluateResult: {
        FORWARD: function (bucket, ip) {
            return function (rl, req, res) {
                if (bucket && ip) {
                    rl.counter.increment(bucket, ip);
                }
                rl.forward(req, res);
                rl.proxyEvent.emit('forward', bucket);
            };
        },
        REJECT: function (reason, errorCode) {
            return function (rl, req, res) {
                rl.reject(req, res, errorCode);
                rl.proxyEvent.emit('reject', reason);
            };
        },
        RESPOND_WITH_HEALTHCHECK: function () {
            return function (rl, req, res) {
                rl.respondWithHealthcheck(req, res);
            };
        }
    },

    loadConfig: function () {
        try {
            var _this = this;
            limitsConfig.loadConfig(this.options.configEndpoint, function (cfg) {
                if (cfg !== null) {
                    _this.updateConfig(cfg);
                } else if (_this.configuration === null) {
                    _this.updateConfig(limitsConfig.defaultConfig);
                } // else use the current config
            });
        } catch (e) {
            logger.error("load/update config failed: " + e);
        }
    },

    updateConfig: function (cfg) {
        this.configuration = new Configuration(cfg);

        if (typeof this.onConfigurationUpdated === "function") {
            this.onConfigurationUpdated();
        }
    },

    initProxy: function () {
        logger.info("New worker is being spawned.");
        var _this = this;
        this.proxy = httpProxy.createProxyServer({});

        this.proxy.on('proxyReq', function (proxyReq, req, res, options) {
            _this.requestForwarded(proxyReq, req, res, options);
        });
        this.proxy.on('proxyRes', function (proxyReq, req, res, options) {
            _this.requestServed(proxyReq, req, res, options);
        });
        this.proxy.on('error', function (err, req, res, options) {
            _this.requestFailed(err, req, res, options);
        });

        var server = http.createServer(function (req, res) {
            var resultMethod = _this.evaluateLimit(req);
            resultMethod(_this, req, res);
        });
        server.listen(_this.options.listenPort, function () {
            logger.info("New worker successfully spawned.");
            if (process.send) {
                logger.info("New worker sends 'online' message.");
                process.send('online');
            }
        });
        this.server = server;

        process.on('SIGTERM', function () {
            _this.terminate(0);
        });

        process.on('message', function (message) {
            if (message === 'shutdown') {
                _this.terminate(0);
            }
        });

        process.on('uncaughtException', function(err) {
            logger.error('uncaughtException handler received: ' + err);
            _this.terminate(1);
        });
    },

    terminate: function (value) {
        this.close(function () {
            logger.info("Old worker proxy process is terminated.");
            process.exit(value);
        });
    },

    close: function (done) {
        logger.info("Old worker proxy is being closed, will serve active requests but no new request will be accepted.");
        if (process.send) {
            logger.info("Old worker proxy sends 'offline' message.");
            process.send('offline');
        }
        this.server.close(function () {
            logger.info("Old worker proxy is closed.");
            done();
        });
    },

    // see: https://gist.github.com/adenes/c70920820faff25ff7a2 evaluate_limits_v3
    evaluateLimit: function (req) {
        try {
            if (this.isRateLimiterHealthcheck(req)) {
                return this.evaluateResult.RESPOND_WITH_HEALTHCHECK();
            }

            if (this.isServiceHealthcheck(req)) {
                return this.evaluateResult.FORWARD();
            }

            if (req.url === this.getConfigEndpoint()) {
                return this.evaluateResult.REJECT("config_endpoint_requested", 404);
            }

            if (this.configuration.maxRequests > 0 && this.counter.getGlobal() >= this.configuration.maxRequests) {
                // hard limit reached
                logger.info("Rejected by global limit: " + this.counter.getGlobal());
                return this.evaluateResult.REJECT("global.request_limit_reached");
            }

            var bucket = this.getMatchingBucket(req);
            req.bucket = bucket;

            var ip = this.IPResolver.resolve(req);
            req.ip = ip;
            var requestCountForBucket = this.counter.get(bucket, ip);
            if (bucket.getMaxRequestsPerIp() > 0 && requestCountForBucket[2] >= bucket.getMaxRequestsPerIp()) {
                // by ip limit reached
                logger.info("Rejected by IP limit for bucket: ", bucket.name, ip);
                return this.evaluateResult.REJECT(bucket.name + ".ip_limit_reached");
            }

            var isGlobalRequestsUnlimited = this.configuration.maxRequestsWithoutBuffer === 0;
            if (isGlobalRequestsUnlimited || this.counter.getGlobal() < this.configuration.maxRequestsWithoutBuffer) {
                // we are below the limits
                return this.evaluateResult.FORWARD(bucket, ip);
            }

            if (bucket.getMaxRequests() > 0 && requestCountForBucket[1] >= this.calculateAvailableRequestsForBucket(bucket)) {
                logger.info("Rejected by bucket limit: ", bucket.name, ip);
                return this.evaluateResult.REJECT(bucket.name + ".request_limit_reached");
            }
            return this.evaluateResult.FORWARD(bucket, ip);

        } catch (e) {
            logger.error("Evaluating limits failed:", e);
            // do nothing, let the request through
        }
        return this.evaluateResult.FORWARD();
    },

    calculateAvailableRequestsForBucket: function (bucket) {
        var remainingRequestCount = this.configuration.maxRequestsWithoutBuffer;
        var sumOfCapacityUnits = 0;
        var _this = this;
        this.configuration.buckets.forEach(function (b) {
            if (_this.counter.getBucketCount(b) >= b.getMaxRequests()) {
                sumOfCapacityUnits += b.capacityUnit;
            } else {
                remainingRequestCount -= _this.counter.getBucketCount(b);
            }
        });

        return Math.ceil(remainingRequestCount / sumOfCapacityUnits * bucket.capacityUnit);
    },

    forward: function (req, res) {
        logger.debug(req.headers['x-forwarded-for'] + " " + req.method + " " + req.url + " " + this.counter.getGlobal());
        this.proxy.web(req, res, {
            target: getForwardUrl(this.options)
        });
    },

    reject: function (req, res, errorCode) {
        errorCode = errorCode || 429;
        res.writeHead(errorCode, "Rejected by the rate limiter");
        res.write("Request has been rejected by the rate limiter");
        res.end();
    },

    respondWithHealthcheck: function (req, res) {
        res.writeHead(200, "Rate-Limiter is running");
        res.write("OK");
        res.end();
    },

    isRateLimiterHealthcheck: function (req) {
        return req.headers["x-rate-limiter"] === "healthcheck";
    },

    isServiceHealthcheck: function (req) {
        return req.url === this.configuration.healthcheckUrl;
    },

    requestForwarded: function (proxyReq, req) {
        if ("bucket" in req) {
            proxyReq.setHeader(this.options.bucketHeaderName, req.bucket.name);
        }
    },

    getMatchingBucket: function (req) {
        for (var i = 0; i < this.configuration.buckets.length; i++) {
            if (this.configuration.buckets[i].matches(req)) {
                return this.configuration.buckets[i];
            }
        }
        throw "Invalid state: no default bucket found";
    },

    requestServed: function (proxyReq, req) {
        this.counter.decrement(req.bucket, req.ip);
        this.proxyEvent.emit('served', req.bucket);
    },

    requestFailed: function (err, req, res, options) {
        logger.error('proxy error', err);

        this.counter.decrement(req.bucket, req.ip);
        this.proxyEvent.emit('failed', err);

        res.writeHead(500, {
            'Content-Type': 'text/plain'
        });

        res.end('An internal error has occurred.');
    },

    getConfigEndpoint: function () {
        return this.options.configEndpoint;
    },

    getConfigRefreshInterval: function () {
        return this.options.configRefreshInterval;
    },

    validateOptions: function (options) {
        options = _.defaults(options || {}, defaultOptions);

        var configUrl = url.parse(options.configEndpoint);
        if (configUrl.protocol === null) {
            configUrl = url.parse(getForwardUrl(options));
            configUrl.pathname = options.configEndpoint;
            options.configEndpoint = url.format(configUrl);
        }

        return options;
    }
};

function getForwardUrl(options) {
    return "http://" + options.forwardHost + ":" + options.forwardPort;
}
<commit_msg>Properly remove event listeners when close shutting down proxy<commit_after>"use strict";

var rateLimiter = exports,
    _ = require("underscore")._,
    limitsConfig = require("./limits-config"),
    ipResolver = require("./ipresolver"),
    CounterStore = require("./counter").CounterStore,
    Configuration = require("./limits-config").Configuration,
    httpProxy = require('http-proxy'),
    http = require("http"),
    url = require('url'),
    log4js = require('log4js'),
    EventEmitter = require('events').EventEmitter;

rateLimiter.RateLimiter = RateLimiter;
var logger = log4js.getLogger();

// http://nodejs.org/docs/v0.10.35/api/http.html#http_agent_maxsockets
// in v0.12 maxSockets default was changed to Infinity
http.globalAgent.maxSockets = Infinity;

var defaultOptions = {
    forwardHost: "localhost",
    forwardPort: 80,
    configRefreshInterval: 60000,
    configEndpoint: "/rate_limiter",
    bucketHeaderName: "X-RateLimiter-Bucket"
};

function RateLimiter(options) {
    this.configuration = null;
    this.onConfigurationUpdated = null;

    this.options = this.validateOptions(options);
    log4js.configure(options.log4js);
    this.IPResolver = new ipResolver.IPResolver(options.forwarded_headers);

    this.updateConfig(limitsConfig.defaultConfig);
    this.proxyEvent = new EventEmitter();
    this.counter = new CounterStore();
    this.initProxy();

    this.loadConfig();
    if (this.options.configRefreshInterval > 0) {
        if (this.options.configRefreshInterval < 5000) {
            logger.warn("configRefreshInterval should be >= 5000 (5sec) -> automatic config update turned off");
        } else {
            var _this = this;
            setInterval(function () {
                _this.loadConfig();
            }, this.options.configRefreshInterval);
        }
    }
}

RateLimiter.prototype = {

    evaluateResult: {
        FORWARD: function (bucket, ip) {
            return function (rl, req, res) {
                if (bucket && ip) {
                    rl.counter.increment(bucket, ip);
                }
                rl.forward(req, res);
                rl.proxyEvent.emit('forward', bucket);
            };
        },
        REJECT: function (reason, errorCode) {
            return function (rl, req, res) {
                rl.reject(req, res, errorCode);
                rl.proxyEvent.emit('reject', reason);
            };
        },
        RESPOND_WITH_HEALTHCHECK: function () {
            return function (rl, req, res) {
                rl.respondWithHealthcheck(req, res);
            };
        }
    },

    loadConfig: function () {
        try {
            var _this = this;
            limitsConfig.loadConfig(this.options.configEndpoint, function (cfg) {
                if (cfg !== null) {
                    _this.updateConfig(cfg);
                } else if (_this.configuration === null) {
                    _this.updateConfig(limitsConfig.defaultConfig);
                } // else use the current config
            });
        } catch (e) {
            logger.error("load/update config failed: " + e);
        }
    },

    updateConfig: function (cfg) {
        this.configuration = new Configuration(cfg);

        if (typeof this.onConfigurationUpdated === "function") {
            this.onConfigurationUpdated();
        }
    },

    initProxy: function () {
        logger.info("New worker is being spawned.");
        var _this = this;
        this.proxy = httpProxy.createProxyServer({});

        this.proxy.on('proxyReq', function (proxyReq, req, res, options) {
            _this.requestForwarded(proxyReq, req, res, options);
        });
        this.proxy.on('proxyRes', function (proxyReq, req, res, options) {
            _this.requestServed(proxyReq, req, res, options);
        });
        this.proxy.on('error', function (err, req, res, options) {
            _this.requestFailed(err, req, res, options);
        });

        var server = http.createServer(function (req, res) {
            var resultMethod = _this.evaluateLimit(req);
            resultMethod(_this, req, res);
        });
        server.listen(_this.options.listenPort, function () {
            logger.info("New worker successfully spawned.");
            if (process.send) {
                logger.info("New worker sends 'online' message.");
                process.send('online');
            }
        });
        this.server = server;

        this.sigtermEventHandler = function () {
            _this.terminate(0);
        };
        process.on('SIGTERM', this.sigtermEventHandler);

        this.messageEventHandler = function (message) {
            if (message === 'shutdown') {
                _this.terminate(0);
            }
        };
        process.on('message', this.messageEventHandler);

        this.uncaughtExceptionEventHandler = function(err) {
            logger.error('uncaughtException handler received: ' + err);
            _this.terminate(1);
        };
        process.on('uncaughtException', this.uncaughtExceptionEventHandler);
    },

    terminate: function (value) {
        this.close(function () {
            logger.info("Old worker proxy process is terminated.");
            process.exit(value);
        });
    },

    close: function (done) {
        logger.info("Old worker proxy is being closed, will serve active requests but no new request will be accepted.");
        if (process.send) {
            logger.info("Old worker proxy sends 'offline' message.");
            process.send('offline');
        }

        var self = this;
        this.server.close(function () {
            logger.info("Old worker proxy is closed.");
            process.removeListener('SIGTERM', self.sigtermEventHandler);
            process.removeListener('message', self.messageEventHandler);
            process.removeListener('uncaughtException', self.uncaughtExceptionEventHandler);
            done();
        });
    },

    // see: https://gist.github.com/adenes/c70920820faff25ff7a2 evaluate_limits_v3
    evaluateLimit: function (req) {
        try {
            if (this.isRateLimiterHealthcheck(req)) {
                return this.evaluateResult.RESPOND_WITH_HEALTHCHECK();
            }

            if (this.isServiceHealthcheck(req)) {
                return this.evaluateResult.FORWARD();
            }

            if (req.url === this.getConfigEndpoint()) {
                return this.evaluateResult.REJECT("config_endpoint_requested", 404);
            }

            if (this.configuration.maxRequests > 0 && this.counter.getGlobal() >= this.configuration.maxRequests) {
                // hard limit reached
                logger.info("Rejected by global limit: " + this.counter.getGlobal());
                return this.evaluateResult.REJECT("global.request_limit_reached");
            }

            var bucket = this.getMatchingBucket(req);
            req.bucket = bucket;

            var ip = this.IPResolver.resolve(req);
            req.ip = ip;
            var requestCountForBucket = this.counter.get(bucket, ip);
            if (bucket.getMaxRequestsPerIp() > 0 && requestCountForBucket[2] >= bucket.getMaxRequestsPerIp()) {
                // by ip limit reached
                logger.info("Rejected by IP limit for bucket: ", bucket.name, ip);
                return this.evaluateResult.REJECT(bucket.name + ".ip_limit_reached");
            }

            var isGlobalRequestsUnlimited = this.configuration.maxRequestsWithoutBuffer === 0;
            if (isGlobalRequestsUnlimited || this.counter.getGlobal() < this.configuration.maxRequestsWithoutBuffer) {
                // we are below the limits
                return this.evaluateResult.FORWARD(bucket, ip);
            }

            if (bucket.getMaxRequests() > 0 && requestCountForBucket[1] >= this.calculateAvailableRequestsForBucket(bucket)) {
                logger.info("Rejected by bucket limit: ", bucket.name, ip);
                return this.evaluateResult.REJECT(bucket.name + ".request_limit_reached");
            }
            return this.evaluateResult.FORWARD(bucket, ip);

        } catch (e) {
            logger.error("Evaluating limits failed:", e);
            // do nothing, let the request through
        }
        return this.evaluateResult.FORWARD();
    },

    calculateAvailableRequestsForBucket: function (bucket) {
        var remainingRequestCount = this.configuration.maxRequestsWithoutBuffer;
        var sumOfCapacityUnits = 0;
        var _this = this;
        this.configuration.buckets.forEach(function (b) {
            if (_this.counter.getBucketCount(b) >= b.getMaxRequests()) {
                sumOfCapacityUnits += b.capacityUnit;
            } else {
                remainingRequestCount -= _this.counter.getBucketCount(b);
            }
        });

        return Math.ceil(remainingRequestCount / sumOfCapacityUnits * bucket.capacityUnit);
    },

    forward: function (req, res) {
        logger.debug(req.headers['x-forwarded-for'] + " " + req.method + " " + req.url + " " + this.counter.getGlobal());
        this.proxy.web(req, res, {
            target: getForwardUrl(this.options)
        });
    },

    reject: function (req, res, errorCode) {
        errorCode = errorCode || 429;
        res.writeHead(errorCode, "Rejected by the rate limiter");
        res.write("Request has been rejected by the rate limiter");
        res.end();
    },

    respondWithHealthcheck: function (req, res) {
        res.writeHead(200, "Rate-Limiter is running");
        res.write("OK");
        res.end();
    },

    isRateLimiterHealthcheck: function (req) {
        return req.headers["x-rate-limiter"] === "healthcheck";
    },

    isServiceHealthcheck: function (req) {
        return req.url === this.configuration.healthcheckUrl;
    },

    requestForwarded: function (proxyReq, req) {
        if ("bucket" in req) {
            proxyReq.setHeader(this.options.bucketHeaderName, req.bucket.name);
        }
    },

    getMatchingBucket: function (req) {
        for (var i = 0; i < this.configuration.buckets.length; i++) {
            if (this.configuration.buckets[i].matches(req)) {
                return this.configuration.buckets[i];
            }
        }
        throw "Invalid state: no default bucket found";
    },

    requestServed: function (proxyReq, req) {
        this.counter.decrement(req.bucket, req.ip);
        this.proxyEvent.emit('served', req.bucket);
    },

    requestFailed: function (err, req, res, options) {
        logger.error('proxy error', err);

        this.counter.decrement(req.bucket, req.ip);
        this.proxyEvent.emit('failed', err);

        res.writeHead(500, {
            'Content-Type': 'text/plain'
        });

        res.end('An internal error has occurred.');
    },

    getConfigEndpoint: function () {
        return this.options.configEndpoint;
    },

    getConfigRefreshInterval: function () {
        return this.options.configRefreshInterval;
    },

    validateOptions: function (options) {
        options = _.defaults(options || {}, defaultOptions);

        var configUrl = url.parse(options.configEndpoint);
        if (configUrl.protocol === null) {
            configUrl = url.parse(getForwardUrl(options));
            configUrl.pathname = options.configEndpoint;
            options.configEndpoint = url.format(configUrl);
        }

        return options;
    }
};

function getForwardUrl(options) {
    return "http://" + options.forwardHost + ":" + options.forwardPort;
}
<|endoftext|>


<commit_before>
//
// Copyright 2016 Kary Foundation, Inc.
//   Author: Pouya Kary <k@karyfoundation.org>
//

//
// ─── CONSTANTS ──────────────────────────────────────────────────────────────────
//

    // regex-sequence.quartet
    const sequenceRegEx = /^(?:\(.*\)|\[.*\])$/;

//
// ─── TO UNICODE ─────────────────────────────────────────────────────────────────
//

    function quartetUnicodify ( text ) {
        let result = [ ];
        for ( let character of text ) {
            result.push( getUnicodeCharacter( character ) );
        }
        return result.join('');
    }

//
// ─── GET UNICODE CHARACTER ──────────────────────────────────────────────────────
//

    function getUnicodeCharacter ( character ) {
        let num = character.charCodeAt( 0 ).toString( 16 ).toUpperCase( );
        let size = 4 - num.length;
        for ( let index = 0; index < size; index++ ) num = '0' + num;
        return '\\u' + num;
    }

//
// ─── SEQUENCE MAKER ─────────────────────────────────────────────────────────────
//

    function quartetSequence ( code ) {
        if ( sequenceRegEx.test( code ) ) return code;
        return ( quartetGetStringLength( code ) <= 1 )? code : '(?:' + code + ')';
    }

//
// ─── ENCODE TEXT ────────────────────────────────────────────────────────────────
//

    function quartetEncodeText ( code ) {
        let result = [ ];
        for ( let character of code ) {
            switch ( character ) {
                case '.':
                case '\\':
                case '+':
                case '*':
                case '?':
                case '[':
                case '^':
                case ']':
                case '$':
                case '(':
                case ')':
                case '{':
                case '}':
                case '=':
                case '!':
                case '<':
                case '>':
                case '|':
                case ':':
                case '-':
                    result.push( `\\${ character }` );
                    break;
                case ' ':
                    result.push( ' ');
                    break;
                default:
                    result.push( character );
            }
        }
        return quartetEncodeHTML( result.join('') );
    }

//
// ─── SPACE ENCODE ───────────────────────────────────────────────────────────────
//

    function quartetSpaceEncode ( text ) {
        return text.replace(/ /g, ' ');
    }

//
// ─── GET STRING LENGTH ──────────────────────────────────────────────────────────
//

    function quartetGetStringLength ( text ) {
        return text.replace(' ', '').length;
    }

//
// ─── SPACE DECODE ───────────────────────────────────────────────────────────────
//

    function quartetSpaceDecode ( text ) {
        return text.replace( /\\u0020/g, ' ' );
    }

//
// ─── MAKE ALPHABET ──────────────────────────────────────────────────────────────
//

    function quartetAlphabet ( text ) {
        return quartetEncodeHTML( ( text.length === 1 )? text[ 0 ] : '[' + text.join('') + ']' );
    }

//
// ─── ENCODE FOR HTML ────────────────────────────────────────────────────────────
//

    function quartetEncodeHTML ( text ) {
        return text.replace( /<|>/, match => {
            switch ( match ) {
                case '<':
                    return '<';
                case '>':
                    return '>';
            }
        });
    }

// ────────────────────────────────────────────────────────────────────────────────
<commit_msg>fixed quartetAlphabet<commit_after>
//
// Copyright 2016 Kary Foundation, Inc.
//   Author: Pouya Kary <k@karyfoundation.org>
//

//
// ─── CONSTANTS ──────────────────────────────────────────────────────────────────
//

    // regex-sequence.quartet
    const sequenceRegEx = /^(?:\(.*\)|\[.*\])$/;

//
// ─── TO UNICODE ─────────────────────────────────────────────────────────────────
//

    function quartetUnicodify ( text ) {
        let result = [ ];
        for ( let character of text ) {
            result.push( getUnicodeCharacter( character ) );
        }
        return result.join('');
    }

//
// ─── GET UNICODE CHARACTER ──────────────────────────────────────────────────────
//

    function getUnicodeCharacter ( character ) {
        let num = character.charCodeAt( 0 ).toString( 16 ).toUpperCase( );
        let size = 4 - num.length;
        for ( let index = 0; index < size; index++ ) num = '0' + num;
        return '\\u' + num;
    }

//
// ─── SEQUENCE MAKER ─────────────────────────────────────────────────────────────
//

    function quartetSequence ( code ) {
        if ( sequenceRegEx.test( code ) ) return code;
        return ( quartetGetStringLength( code ) <= 1 )? code : '(?:' + code + ')';
    }

//
// ─── ENCODE TEXT ────────────────────────────────────────────────────────────────
//

    function quartetEncodeText ( code ) {
        let result = [ ];
        for ( let character of code ) {
            switch ( character ) {
                case '.':
                case '\\':
                case '+':
                case '*':
                case '?':
                case '[':
                case '^':
                case ']':
                case '$':
                case '(':
                case ')':
                case '{':
                case '}':
                case '=':
                case '!':
                case '<':
                case '>':
                case '|':
                case ':':
                case '-':
                    result.push( `\\${ character }` );
                    break;
                case ' ':
                    result.push( ' ');
                    break;
                default:
                    result.push( character );
            }
        }
        return quartetEncodeHTML( result.join('') );
    }

//
// ─── SPACE ENCODE ───────────────────────────────────────────────────────────────
//

    function quartetSpaceEncode ( text ) {
        return text.replace(/ /g, ' ');
    }

//
// ─── GET STRING LENGTH ──────────────────────────────────────────────────────────
//

    function quartetGetStringLength ( text ) {
        return text.replace(' ', '').length;
    }

//
// ─── SPACE DECODE ───────────────────────────────────────────────────────────────
//

    function quartetSpaceDecode ( text ) {
        return text.replace( /\\u0020/g, ' ' );
    }

//
// ─── MAKE ALPHABET ──────────────────────────────────────────────────────────────
//

    function quartetAlphabet ( sigma ) {
        if ( sigma.length === 0 ) return '';
        return quartetEncodeHTML( ( sigma.length === 1 )? sigma[ 0 ] : '[' + sigma.join('') + ']' );
    }

//
// ─── ENCODE FOR HTML ────────────────────────────────────────────────────────────
//

    function quartetEncodeHTML ( text ) {
        return text.replace( /<|>/, match => {
            switch ( match ) {
                case '<':
                    return '<';
                case '>':
                    return '>';
            }
        });
    }

// ────────────────────────────────────────────────────────────────────────────────
<|endoftext|>


<commit_before>/**
 * @class Relation
 *
 * In graph terminology a relation represents a directed edge, a
 * reference from one asset to another. For the purpose of being able
 * to treat all relations equally, there's a subclass for each
 * supported relation type, encapsulating the details of how to
 * retrieve, update, and (optionally) inline the asset being pointed
 * to.
 *
 * These are some examples of included subclasses:
 *
 *    - `relations.HtmlAnchor`         An anchor tag in an HTML document `<a href='...'>`.
 *    - `relations.HtmlImage`          An `<img src='...'>` tag in an HTML document.
 *    - `relations.CssImport`          An `@import` declaration in a CSS asset.
 *    - `relations.CacheManifestEntry` A line in a cache manifest.
 */
var _ = require('underscore'),
    extendWithGettersAndSetters = require('../util/extendWithGettersAndSetters'),
    passError = require('passerror'),
    urlTools = require('../util/urlTools'),
    uniqueId = require('../util/uniqueId'),
    query = require('../query');

/**
 * new Relation(options)
 * =====================
 *
 * Create a new Relation instance. For existing assets the
 * instantiation of relations happens automatically if you use the
 * `populate` transform. You only need to create relations manually
 * when you need to introduce new ones.
 *
 * Note that the base Relation class should be considered
 * abstract. Please instantiate the appropriate subclass.
 *
 * Options:
 *
 *  - `from` The source asset of the relation.
 *  - `to`   The target asset of the relation, or an asset configuration
 *           object if the target asset hasn't yet been resolved and created.
 */
function Relation(config) {
    if (config.hrefType) {
        this._hrefType = config.hrefType;
        delete config.hrefType;
    }
    _.extend(this, config);
    this.id = uniqueId();
}

Relation.prototype = {
    /**
     * relation.from (Asset)
     * =====================
     *
     * The source asset of the relation.
     */

    /**
     * relation.to (Asset or asset config object)
     * ==========================================
     *
     * The target asset of the relation. If the relation hasn't yet
     * been resolved, it can also be a relative url string or an asset
     * configuration object.
     */

    /**
     * relation.href (getter/setter)
     * =============================
     *
     * Get or set the href of the relation. The relation must be
     * attached to an asset.
     *
     * What is actually retrieved or updated depends on the relation
     * type. For `HtmlImage` the `src` attribute of the HTML element
     * is changed, for `CssImport` the parsed representation of
     * the @import rule is updated, etc.
     *
     * Most of the time you don't need to think about this property,
     * as the href is automatically updated when the url of the source
     * or target asset is changed, or an intermediate asset is
     * inlined.
     *
     * @api public
     */

    /**
     * relation.refreshHref
     * ====================
     *
     * Update `href` of a relation to make sure it points at the
     * current url of its target asset.
     *
     * It's not necessary to call this function manually as long as
     * the source and target assets of the relation have only been
     * moved by having their `url` property changed (the recommended
     * way), but some transforms will need this after some low-level
     * surgery, such as attaching an existing relation to a different
     * asset.
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    refreshHref: function () {
        // if (this.to.isInline) won't work because relation.to might be unresolved and thus not an Asset instance:
        if (this.to && this.to.url) {
            var hrefType = this.hrefType,
                href;
            if (hrefType === 'rootRelative') {
                href = urlTools.buildRootRelativeUrl(this.baseAsset.nonInlineAncestor.url, this.to.url, this.assetGraph && this.assetGraph.root);
            } else if (hrefType === 'relative') {
                href = urlTools.buildRelativeUrl(this.baseAsset.nonInlineAncestor.url, this.to.url);
            } else if (hrefType === 'protocolRelative') {
                href = urlTools.buildProtocolRelativeUrl(this.baseAsset.nonInlineAncestor.url, this.to.url);
            } else {
                // Absolute
                href = this.to.url;
            }
            if (this.href !== href) {
                this.href = href;
                this.from.markDirty();
            }
        }
        return this;
    },

    /**
     * relation.hrefType (getter/setter)
     * =================================
     *
     * Either 'absolute', 'rootRelative', 'protocolRelative', or 'relative'. Decides what "degree" of
     * relative url refreshHref tries to issue.
     */

    get hrefType() {
        if (!this._hrefType) {
            var href = this.href;
            if (/^\/\//.test(href)) {
                this._hrefType = 'protocolRelative';
            } else if (/^\//.test(href)) {
                this._hrefType = 'rootRelative';
            } else if (/^[a-z\+]+:/i.test(href)) {
                this._hrefType = 'absolute';
            } else {
                this._hrefType = 'relative';
            }
        }
        return this._hrefType;
    },

    set hrefType(hrefType) {
        if (hrefType !== this._hrefType) {
            this._hrefType = hrefType;
            this.refreshHref();
        }
    },

    /**
     * relation.inline()
     * =================
     *
     * Inline the relation. This is only supported by certain relation
     * types and will produce different results depending on the type
     * (`data:` url, inline script, inline stylesheet...).
     *
     * Will make a clone of the target asset if it has more incoming
     * relations than this one.
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    inline: function () {
        if (this.to.incomingRelations.length !== 1) {
            // This isn't the only incoming relation to the asset, clone before inlining.
            this.to.clone(this);
        }
        if (!this.to.isInline) {
            if (this.assetGraph) {
                var affectedRelations = [].concat(this.assetGraph._objInBaseAssetPaths[this.to.id]);
                affectedRelations.forEach(function (affectedRelation) {
                    affectedRelation._unregisterBaseAssetPath();
                }, this);
                this.to.url = null;
                affectedRelations.forEach(function (affectedRelation) {
                    affectedRelation._registerBaseAssetPath();
                    affectedRelation.refreshHref();
                }, this);
            } else {
                this.to.url = null;
            }
        }
        return this;
    },

    /**
     * relation.isRelation (boolean)
     * =============================
     *
     * Property that's true for all relation instances. Avoids
     * reliance on the `instanceof` operator.
     */
    isRelation: true,

    /**
     * relation.attach(asset, position[, adjacentRelation])
     * ====================================================
     *
     * Attaches the relation to an asset.
     *
     * The ordering of certain relation types is significant
     * (`HtmlScript`, for instance), so it's important that the order
     * isn't scrambled in the indices. Therefore the caller must
     * explicitly specify a position at which to insert the object.
     *
     * @param {Asset} asset The asset to attach the relation to.
     * @param {String} position "first", "last", "before", or "after".
     * @param {Relation} adjacentRelation The adjacent relation, mandatory if the position is "before" or "after".
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    attach: function (asset, position, adjacentRelation) {
        this.from = asset;
        this.from.markDirty();
        if (this.from.assetGraph && !this.assetGraph) {
            this.from.assetGraph.addRelation(this, position, adjacentRelation);
        }
        if (this.to && this.to.url) {
            this.refreshHref();
        }
        return this;
    },

    /**
     * relation.detach()
     * =================
     *
     * Detaches the relation from the asset it is currently attached
     * to. If the relation is currently part of a graph, it will
     * removed from it.
     *
     * Detaching implies that the tag/statement/declaration
     * representing the relation is physically removed from the
     * referring asset. Not all relation types support this.
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    detach: function () {
        this.from.markDirty();
        if (this.assetGraph) {
            this.assetGraph.removeRelation(this);
        }
        return this;
    },

    /**
     * relation.remove()
     * =================
     *
     * Removes the relation from the graph it's currently part
     * of. Doesn't detach the relation (compare with
     * `relation.detach()`).
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    remove: function () {
        if (!this.assetGraph) {
            throw new Error("relation.remove(): Not in a graph");
        }
        this.assetGraph.removeRelation(this);
        return this;
    },

    /**
     * relation.baseAssetQuery (Object)
     * ================================
     *
     * Subclass-specific query object used for finding the base asset
     * for the relation (the asset whose url should be the basis for
     * resolving the href of the relation). This is usually the first
     * non-inline asset, but for some relation types it's the first
     * Html document.
     *
     * You shouldn't need to worry about this.
     *
     * @api public
     */
    baseAssetQuery: {isInline: false},

    /**
     * relation.baseAsset (getter)
     * ===========================
     *
     * Find the asset from which the url of the relation is to be
     * resolved. This is usually the first non-inline containing
     * asset, but for some relation types it's the first Html ancestor
     * -- infamously `CssAlphaImageLoader` and `CssBehavior`, but also
     * `JavaScriptGetStaticUrl`.
     *
     * The relation doesn't have to be in the graph as long as the
     * source asset is, so this can be used during population of the
     * graph.
     *
     * @return {Asset} The base asset for the relation.
     * @api public
     */
    get baseAsset() {
        if (!this.from || ! this.from.assetGraph || !(this.from.id in this.from.assetGraph.idIndex)) {
            throw new Error("Relation.baseAsset getter: The 'from' asset of the relation is not in the graph: " + this.from);
        }
        var baseAssetPath = this.from.baseAssetSubstitute ? this._computeBaseAssetPath()[0] : this.baseAssetPath;
        if (baseAssetPath) {
            return this.baseAssetPath[0];
        }
    },

    _computeBaseAssetPath: function () {
        var baseAssetMatcher = this.from.assetGraph.constructor.query.queryObjToMatcherFunction(this.baseAssetQuery),
            queue = [[this.from.baseAssetSubstitute || this.from]],
            seenAssets = {};
        // Reverse breadth-first search
        while (queue.length) {
            var assetPath = queue.shift();
            if (baseAssetMatcher(assetPath[0])) {
                return assetPath;
            }
            seenAssets[assetPath[0].id] = true;

            // Add all yet-unseen assets reachable via incoming relations to the end of the queue:
            assetPath[0].incomingRelations.forEach(function (incomingRelation) {
                var sourceAsset = incomingRelation.from.baseAssetSubstitute || incomingRelation.from;
                if (!(sourceAsset.id in seenAssets)) {
                    queue.push([sourceAsset, incomingRelation].concat(assetPath));
                }
            });
        }
    },

    /**
     * relation.baseAssetPath (getter)
     * ===============================
     *
     * Helper for the relation.baseAsset getter.
     *
     * @return {Array} The complete path to the base asset: [baseAsset, intermediateRelation, asset, ...]. For normal non-inline assets that will just be [baseAsset].
     * @api public
     */
    get baseAssetPath() {
        if (!this.from || !this.from.assetGraph) {
            throw new Error("Relation.baseAssetPath getter: The 'from' asset of the relation is not in the graph: " + this.from);
        }
        if (!('_baseAssetPath' in this)) {
            this._baseAssetPath = this._computeBaseAssetPath() || null;
        }
        return this._baseAssetPath;
    },

    /**
     * relation._registerBaseAssetPath()
     * =================================
     *
     * Register the base asset path of the relation (save it in the
     * internal index).
     *
     * @return {Boolean} Whether the base asset path could be computed.
     * @api private
     */
    _registerBaseAssetPath: function () {
        if (!this.assetGraph) {
            throw new Error("relation._registerBaseAssetPath(): Not in a graph");
        }
        if (!this.baseAssetPath) {
            this.assetGraph._relationsWithNoBaseAsset.push(this);
            return false;
        } else {
            this.baseAssetPath.forEach(function (obj) {
                this.assetGraph._objInBaseAssetPaths[obj.id].push(this);
            }, this);
            return true;
        }
    },

    resolve: function (cb) {
        if (!this.from.assetGraph) {
            throw new Error("relation.resolve(): Source asset not in a graph");
        }
        var baseAsset = this.baseAsset;
        if (baseAsset) {
            this.from.assetGraph.resolveAssetConfig(this.to, baseAsset.nonInlineAncestor.url, passError(cb, function (resolvedAssetConfigs) {
                if (!_.isArray(resolvedAssetConfigs)) {
                    resolvedAssetConfigs = [resolvedAssetConfigs];
                }
                if (resolvedAssetConfigs.length === 0) {
                    this.from.assetGraph.emit('warn', new Error('The relation ' + this.toString() + ' multiplied to nothing'));
                    this.remove();
                    cb(null, []);
                } else if (resolvedAssetConfigs.length === 1) {
                    this.to = resolvedAssetConfigs[0];
                    this.refreshHref();
                    cb(null, [this]);
                } else {
                    var multipliedRelations = [];
                    resolvedAssetConfigs.forEach(function (resolvedAssetConfig) {
                        var multipliedRelation = new this.constructor({to: resolvedAssetConfig});
                        multipliedRelation.attach(this.from, 'before', this);
                        multipliedRelations.push(multipliedRelation);
                    }, this);
                    this.detach();
                    cb(null, multipliedRelations);
                }
            }.bind(this)));
        } else {
            process.nextTick(function () {
                cb(new Error('No base asset found'));
            });
        }
    },

    /**
     * relation._unregisterBaseAssetPath()
     * ===================================
     *
     * Remove the the base asset path of the relation from the
     * internal indices.
     *
     * @api private
     */
    _unregisterBaseAssetPath: function () {
        if (!this.assetGraph) {
            throw new Error("relation._unregisterBaseAssetPath(): Not in a graph");
        }
        if (!this.baseAssetPath) {
            this.assetGraph._relationsWithNoBaseAsset.splice(this.assetGraph._relationsWithNoBaseAsset.indexOf(this), 1);
        } else {
            this.baseAssetPath.forEach(function (obj) {
                var indexInBaseAssetPathsForOtherObj = this.assetGraph._objInBaseAssetPaths[obj.id].indexOf(this);
                if (indexInBaseAssetPathsForOtherObj === -1) {
                    throw new Error('relation._unregisterBaseAssetPath: Assertion failure: Relation ' + this + ' is no longer in _objInBaseAssetPaths[' + obj + ']');
                }
                this.assetGraph._objInBaseAssetPaths[obj.id].splice(indexInBaseAssetPathsForOtherObj, 1);
            }, this);
        }
        delete this._baseAssetPath;
    },

    /**
     * relation.toString()
     * ===================
     *
     * Get a brief text containing the type, id of the relation. Will
     * also contain the `.toString()` of the relation's source and
     * target assets if available.
     *
     * @return {String} The string, eg. "[HtmlAnchor/141: [Html/40 file:///foo/bar/index.html] => [Html/76 file:///foo/bar/otherpage.html]]"
     * @api public
     */
    toString: function () {
        return "[" + this.type + "/" + this.id + ": " + ((this.from && this.to) ? this.from.toString() + " => " + (this.to.isAsset ? this.to.toString() : this.to.url || this.to.type || '?') : "unattached") + "]";
    }
};

module.exports = Relation;
<commit_msg>Relation: Report the containing asset when warning about a wildcard relation that expanded to nothing.<commit_after>/**
 * @class Relation
 *
 * In graph terminology a relation represents a directed edge, a
 * reference from one asset to another. For the purpose of being able
 * to treat all relations equally, there's a subclass for each
 * supported relation type, encapsulating the details of how to
 * retrieve, update, and (optionally) inline the asset being pointed
 * to.
 *
 * These are some examples of included subclasses:
 *
 *    - `relations.HtmlAnchor`         An anchor tag in an HTML document `<a href='...'>`.
 *    - `relations.HtmlImage`          An `<img src='...'>` tag in an HTML document.
 *    - `relations.CssImport`          An `@import` declaration in a CSS asset.
 *    - `relations.CacheManifestEntry` A line in a cache manifest.
 */
var _ = require('underscore'),
    extendWithGettersAndSetters = require('../util/extendWithGettersAndSetters'),
    passError = require('passerror'),
    urlTools = require('../util/urlTools'),
    uniqueId = require('../util/uniqueId'),
    query = require('../query');

/**
 * new Relation(options)
 * =====================
 *
 * Create a new Relation instance. For existing assets the
 * instantiation of relations happens automatically if you use the
 * `populate` transform. You only need to create relations manually
 * when you need to introduce new ones.
 *
 * Note that the base Relation class should be considered
 * abstract. Please instantiate the appropriate subclass.
 *
 * Options:
 *
 *  - `from` The source asset of the relation.
 *  - `to`   The target asset of the relation, or an asset configuration
 *           object if the target asset hasn't yet been resolved and created.
 */
function Relation(config) {
    if (config.hrefType) {
        this._hrefType = config.hrefType;
        delete config.hrefType;
    }
    _.extend(this, config);
    this.id = uniqueId();
}

Relation.prototype = {
    /**
     * relation.from (Asset)
     * =====================
     *
     * The source asset of the relation.
     */

    /**
     * relation.to (Asset or asset config object)
     * ==========================================
     *
     * The target asset of the relation. If the relation hasn't yet
     * been resolved, it can also be a relative url string or an asset
     * configuration object.
     */

    /**
     * relation.href (getter/setter)
     * =============================
     *
     * Get or set the href of the relation. The relation must be
     * attached to an asset.
     *
     * What is actually retrieved or updated depends on the relation
     * type. For `HtmlImage` the `src` attribute of the HTML element
     * is changed, for `CssImport` the parsed representation of
     * the @import rule is updated, etc.
     *
     * Most of the time you don't need to think about this property,
     * as the href is automatically updated when the url of the source
     * or target asset is changed, or an intermediate asset is
     * inlined.
     *
     * @api public
     */

    /**
     * relation.refreshHref
     * ====================
     *
     * Update `href` of a relation to make sure it points at the
     * current url of its target asset.
     *
     * It's not necessary to call this function manually as long as
     * the source and target assets of the relation have only been
     * moved by having their `url` property changed (the recommended
     * way), but some transforms will need this after some low-level
     * surgery, such as attaching an existing relation to a different
     * asset.
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    refreshHref: function () {
        // if (this.to.isInline) won't work because relation.to might be unresolved and thus not an Asset instance:
        if (this.to && this.to.url) {
            var hrefType = this.hrefType,
                href;
            if (hrefType === 'rootRelative') {
                href = urlTools.buildRootRelativeUrl(this.baseAsset.nonInlineAncestor.url, this.to.url, this.assetGraph && this.assetGraph.root);
            } else if (hrefType === 'relative') {
                href = urlTools.buildRelativeUrl(this.baseAsset.nonInlineAncestor.url, this.to.url);
            } else if (hrefType === 'protocolRelative') {
                href = urlTools.buildProtocolRelativeUrl(this.baseAsset.nonInlineAncestor.url, this.to.url);
            } else {
                // Absolute
                href = this.to.url;
            }
            if (this.href !== href) {
                this.href = href;
                this.from.markDirty();
            }
        }
        return this;
    },

    /**
     * relation.hrefType (getter/setter)
     * =================================
     *
     * Either 'absolute', 'rootRelative', 'protocolRelative', or 'relative'. Decides what "degree" of
     * relative url refreshHref tries to issue.
     */

    get hrefType() {
        if (!this._hrefType) {
            var href = this.href;
            if (/^\/\//.test(href)) {
                this._hrefType = 'protocolRelative';
            } else if (/^\//.test(href)) {
                this._hrefType = 'rootRelative';
            } else if (/^[a-z\+]+:/i.test(href)) {
                this._hrefType = 'absolute';
            } else {
                this._hrefType = 'relative';
            }
        }
        return this._hrefType;
    },

    set hrefType(hrefType) {
        if (hrefType !== this._hrefType) {
            this._hrefType = hrefType;
            this.refreshHref();
        }
    },

    /**
     * relation.inline()
     * =================
     *
     * Inline the relation. This is only supported by certain relation
     * types and will produce different results depending on the type
     * (`data:` url, inline script, inline stylesheet...).
     *
     * Will make a clone of the target asset if it has more incoming
     * relations than this one.
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    inline: function () {
        if (this.to.incomingRelations.length !== 1) {
            // This isn't the only incoming relation to the asset, clone before inlining.
            this.to.clone(this);
        }
        if (!this.to.isInline) {
            if (this.assetGraph) {
                var affectedRelations = [].concat(this.assetGraph._objInBaseAssetPaths[this.to.id]);
                affectedRelations.forEach(function (affectedRelation) {
                    affectedRelation._unregisterBaseAssetPath();
                }, this);
                this.to.url = null;
                affectedRelations.forEach(function (affectedRelation) {
                    affectedRelation._registerBaseAssetPath();
                    affectedRelation.refreshHref();
                }, this);
            } else {
                this.to.url = null;
            }
        }
        return this;
    },

    /**
     * relation.isRelation (boolean)
     * =============================
     *
     * Property that's true for all relation instances. Avoids
     * reliance on the `instanceof` operator.
     */
    isRelation: true,

    /**
     * relation.attach(asset, position[, adjacentRelation])
     * ====================================================
     *
     * Attaches the relation to an asset.
     *
     * The ordering of certain relation types is significant
     * (`HtmlScript`, for instance), so it's important that the order
     * isn't scrambled in the indices. Therefore the caller must
     * explicitly specify a position at which to insert the object.
     *
     * @param {Asset} asset The asset to attach the relation to.
     * @param {String} position "first", "last", "before", or "after".
     * @param {Relation} adjacentRelation The adjacent relation, mandatory if the position is "before" or "after".
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    attach: function (asset, position, adjacentRelation) {
        this.from = asset;
        this.from.markDirty();
        if (this.from.assetGraph && !this.assetGraph) {
            this.from.assetGraph.addRelation(this, position, adjacentRelation);
        }
        if (this.to && this.to.url) {
            this.refreshHref();
        }
        return this;
    },

    /**
     * relation.detach()
     * =================
     *
     * Detaches the relation from the asset it is currently attached
     * to. If the relation is currently part of a graph, it will
     * removed from it.
     *
     * Detaching implies that the tag/statement/declaration
     * representing the relation is physically removed from the
     * referring asset. Not all relation types support this.
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    detach: function () {
        this.from.markDirty();
        if (this.assetGraph) {
            this.assetGraph.removeRelation(this);
        }
        return this;
    },

    /**
     * relation.remove()
     * =================
     *
     * Removes the relation from the graph it's currently part
     * of. Doesn't detach the relation (compare with
     * `relation.detach()`).
     *
     * @return {Relation} The relation itself (chaining-friendly).
     * @api public
     */
    remove: function () {
        if (!this.assetGraph) {
            throw new Error("relation.remove(): Not in a graph");
        }
        this.assetGraph.removeRelation(this);
        return this;
    },

    /**
     * relation.baseAssetQuery (Object)
     * ================================
     *
     * Subclass-specific query object used for finding the base asset
     * for the relation (the asset whose url should be the basis for
     * resolving the href of the relation). This is usually the first
     * non-inline asset, but for some relation types it's the first
     * Html document.
     *
     * You shouldn't need to worry about this.
     *
     * @api public
     */
    baseAssetQuery: {isInline: false},

    /**
     * relation.baseAsset (getter)
     * ===========================
     *
     * Find the asset from which the url of the relation is to be
     * resolved. This is usually the first non-inline containing
     * asset, but for some relation types it's the first Html ancestor
     * -- infamously `CssAlphaImageLoader` and `CssBehavior`, but also
     * `JavaScriptGetStaticUrl`.
     *
     * The relation doesn't have to be in the graph as long as the
     * source asset is, so this can be used during population of the
     * graph.
     *
     * @return {Asset} The base asset for the relation.
     * @api public
     */
    get baseAsset() {
        if (!this.from || ! this.from.assetGraph || !(this.from.id in this.from.assetGraph.idIndex)) {
            throw new Error("Relation.baseAsset getter: The 'from' asset of the relation is not in the graph: " + this.from);
        }
        var baseAssetPath = this.from.baseAssetSubstitute ? this._computeBaseAssetPath()[0] : this.baseAssetPath;
        if (baseAssetPath) {
            return this.baseAssetPath[0];
        }
    },

    _computeBaseAssetPath: function () {
        var baseAssetMatcher = this.from.assetGraph.constructor.query.queryObjToMatcherFunction(this.baseAssetQuery),
            queue = [[this.from.baseAssetSubstitute || this.from]],
            seenAssets = {};
        // Reverse breadth-first search
        while (queue.length) {
            var assetPath = queue.shift();
            if (baseAssetMatcher(assetPath[0])) {
                return assetPath;
            }
            seenAssets[assetPath[0].id] = true;

            // Add all yet-unseen assets reachable via incoming relations to the end of the queue:
            assetPath[0].incomingRelations.forEach(function (incomingRelation) {
                var sourceAsset = incomingRelation.from.baseAssetSubstitute || incomingRelation.from;
                if (!(sourceAsset.id in seenAssets)) {
                    queue.push([sourceAsset, incomingRelation].concat(assetPath));
                }
            });
        }
    },

    /**
     * relation.baseAssetPath (getter)
     * ===============================
     *
     * Helper for the relation.baseAsset getter.
     *
     * @return {Array} The complete path to the base asset: [baseAsset, intermediateRelation, asset, ...]. For normal non-inline assets that will just be [baseAsset].
     * @api public
     */
    get baseAssetPath() {
        if (!this.from || !this.from.assetGraph) {
            throw new Error("Relation.baseAssetPath getter: The 'from' asset of the relation is not in the graph: " + this.from);
        }
        if (!('_baseAssetPath' in this)) {
            this._baseAssetPath = this._computeBaseAssetPath() || null;
        }
        return this._baseAssetPath;
    },

    /**
     * relation._registerBaseAssetPath()
     * =================================
     *
     * Register the base asset path of the relation (save it in the
     * internal index).
     *
     * @return {Boolean} Whether the base asset path could be computed.
     * @api private
     */
    _registerBaseAssetPath: function () {
        if (!this.assetGraph) {
            throw new Error("relation._registerBaseAssetPath(): Not in a graph");
        }
        if (!this.baseAssetPath) {
            this.assetGraph._relationsWithNoBaseAsset.push(this);
            return false;
        } else {
            this.baseAssetPath.forEach(function (obj) {
                this.assetGraph._objInBaseAssetPaths[obj.id].push(this);
            }, this);
            return true;
        }
    },

    resolve: function (cb) {
        if (!this.from.assetGraph) {
            throw new Error("relation.resolve(): Source asset not in a graph");
        }
        var baseAsset = this.baseAsset;
        if (baseAsset) {
            this.from.assetGraph.resolveAssetConfig(this.to, baseAsset.nonInlineAncestor.url, passError(cb, function (resolvedAssetConfigs) {
                if (!_.isArray(resolvedAssetConfigs)) {
                    resolvedAssetConfigs = [resolvedAssetConfigs];
                }
                if (resolvedAssetConfigs.length === 0) {
                    this.from.assetGraph.emit('warn', new Error('The relation ' + this.toString() + ' multiplied to nothing, containing asset: ' + this.from.urlOrDescription));
                    this.remove();
                    cb(null, []);
                } else if (resolvedAssetConfigs.length === 1) {
                    this.to = resolvedAssetConfigs[0];
                    this.refreshHref();
                    cb(null, [this]);
                } else {
                    var multipliedRelations = [];
                    resolvedAssetConfigs.forEach(function (resolvedAssetConfig) {
                        var multipliedRelation = new this.constructor({to: resolvedAssetConfig});
                        multipliedRelation.attach(this.from, 'before', this);
                        multipliedRelations.push(multipliedRelation);
                    }, this);
                    this.detach();
                    cb(null, multipliedRelations);
                }
            }.bind(this)));
        } else {
            process.nextTick(function () {
                cb(new Error('No base asset found'));
            });
        }
    },

    /**
     * relation._unregisterBaseAssetPath()
     * ===================================
     *
     * Remove the the base asset path of the relation from the
     * internal indices.
     *
     * @api private
     */
    _unregisterBaseAssetPath: function () {
        if (!this.assetGraph) {
            throw new Error("relation._unregisterBaseAssetPath(): Not in a graph");
        }
        if (!this.baseAssetPath) {
            this.assetGraph._relationsWithNoBaseAsset.splice(this.assetGraph._relationsWithNoBaseAsset.indexOf(this), 1);
        } else {
            this.baseAssetPath.forEach(function (obj) {
                var indexInBaseAssetPathsForOtherObj = this.assetGraph._objInBaseAssetPaths[obj.id].indexOf(this);
                if (indexInBaseAssetPathsForOtherObj === -1) {
                    throw new Error('relation._unregisterBaseAssetPath: Assertion failure: Relation ' + this + ' is no longer in _objInBaseAssetPaths[' + obj + ']');
                }
                this.assetGraph._objInBaseAssetPaths[obj.id].splice(indexInBaseAssetPathsForOtherObj, 1);
            }, this);
        }
        delete this._baseAssetPath;
    },

    /**
     * relation.toString()
     * ===================
     *
     * Get a brief text containing the type, id of the relation. Will
     * also contain the `.toString()` of the relation's source and
     * target assets if available.
     *
     * @return {String} The string, eg. "[HtmlAnchor/141: [Html/40 file:///foo/bar/index.html] => [Html/76 file:///foo/bar/otherpage.html]]"
     * @api public
     */
    toString: function () {
        return "[" + this.type + "/" + this.id + ": " + ((this.from && this.to) ? this.from.toString() + " => " + (this.to.isAsset ? this.to.toString() : this.to.url || this.to.type || '?') : "unattached") + "]";
    }
};

module.exports = Relation;
<|endoftext|>