<commit_before>'use strict';

var PeerDictionary = require('./thaliPeerDictionary');
var ThaliMobile = require('../thaliMobile');
var ThaliNotificationAction = require('./thaliNotificationAction.js');
var NotificationBeacons = require('./thaliNotificationBeacons');
var logger = require('../../thalilogger')('thaliNotificationClient');
var assert = require('assert');
var EventEmitter = require('events').EventEmitter;
var util = require('util');

/** @module thaliNotificationClient */

/**
 * @classdesc Data of peerAdvertisesDataForUs event
 * @constructor
 * @param {buffer} keyId The buffer contains the ECDH public key for the
 * peer.
 * @param {string} pskIdentifyField
 * @param {buffer} psk This is the calculated pre-shared key that will be
 * needed to establish a TLS PSK connection.
 * @param {string} hostAddress The IP/DNS address of the peer
 * @param {number} portNumber The TCP/IP port at the hostAddress the peer
 * can be contacted on
 * @param {number} suggestedTCPTimeout Provides a hint to what time out to
 * put on the TCP connection. For some transports a handshake can take quite a
 * long time.
 * @param {module:thaliMobile.connectionTypes} connectionType The type of
 * connection that will be used when connecting to this peer.
 */
function PeerAdvertisesDataForUs (keyId, pskIdentifyField,
                                  psk, hostAddress, portNumber,
                                  suggestedTCPTimeout, connectionType) {
  this.keyId = keyId;
  this.pskIdentifyField = pskIdentifyField;
  this.psk = psk;
  this.hostAddress = hostAddress;
  this.portNumber = portNumber;
  this.suggestedTCPTimeout = suggestedTCPTimeout;
  this.connectionType = connectionType;
}
// jscs:disable maximumLineLength
/**
 * @classdesc Creates a class that can register to receive the {@link
 * module:thaliMobile.event:peerAvailabilityChanged} event. It will listen for
 * the event and upon receiving it, will enqueue an action with the submitted
 * thaliPeerPoolInterface. Once called back by the pool then the callback will
 * issue a HTTP GET request to retrieve the notification beacons for the peer,
 * parse them, see if one matches and if so then fire a {@link
 * module:thaliNotificationClient.event:peerAdvertisesDataForUs}. Callers can
 * listen for the event by using the emitter member.
 *
 * @public
 * @constructor
 * @param {module:thaliPeerPoolInterface~ThaliPeerPoolInterface} thaliPeerPoolInterface
 * Requests to retrieve notification beacons are enqueued on this object in
 * order to make sure we don't overwhelm our bandwidth or native communication
 * capabilities.
 * @param {Crypto.ECDH} ecdhForLocalDevice A Crypto.ECDH object initialized
 * with the local device's public and private keys.
 * @fires module:thaliNotificationClient.event:peerAdvertisesDataForUs
 * @throws {Error} thaliPeerPoolInterface cannot be null
 * @throws {Error} ecdhForLocalDevice cannot be null
 */
// jscs:enable maximumLineLength
function ThaliNotificationClient(thaliPeerPoolInterface, ecdhForLocalDevice) {
  EventEmitter.call(this);
  var self = this;

  assert(thaliPeerPoolInterface,
    ThaliNotificationClient.Errors.PEERPOOL_NOT_NULL);
  assert(ecdhForLocalDevice,
    ThaliNotificationClient.Errors.EDCH_FOR_LOCAL_DEVICE_NOT_NULL);

  this.peerDictionary = null;
  this._thaliPeerPoolInterface = thaliPeerPoolInterface;
  this._ecdhForLocalDevice = ecdhForLocalDevice;
  this._publicKeysToListen = [];
  this._publicKeysToListenHashes = [];

  this._addressBookCallback = function (unencryptedKeyId) {

    if (!Buffer.isBuffer(unencryptedKeyId)) {
      logger.warn('_addressBookCallback: unencryptedKeyId is not an buffer');
      return null;
    }

    for (var i = 0 ; i < self._publicKeysToListenHashes.length ; i++) {
      var pubKeyHash = self._publicKeysToListenHashes[i];
      if (unencryptedKeyId.compare(pubKeyHash) === 0) {
        return self._publicKeysToListen[i];
      }
    }
    return null;
  };
}

util.inherits(ThaliNotificationClient, EventEmitter);

/**
 * A dictionary used to track the state of peers we have received notifications
 * about from {@link module:thaliMobile}.
 *
 * @type {ThaliNotificationClient.PeerDictionary}
 */
ThaliNotificationClient.prototype.peerDictionary = null;

/**
 * This method will cause a listener to be registered on the global singleton
 * {@link module:thaliMobile} object for the {@link
 * module:thaliMobile.event:peerAvailabilityChanged} event.
 *
 * This is not idempotent method since it can be called multiple times with
 * different prioritization lists but repeated start calls MUST only update that
 * list and MUST NOT cause multiple listeners to be registered with thaliMobile.
 *
 * @public
 * @param {Buffer[]} publicKeysToListen Used to decide what peer
 * notifications to pay attention to. This list consists of an array
 * of buffers that contain the serialization of the public ECDH keys of the
 * peers we are interested in syncing with.
 *
 * @throws {Error} Public keys to listen must be an array
 */
ThaliNotificationClient.prototype.start =
  function (publicKeysToListen) {
    var self = this;

    assert(Array.isArray(publicKeysToListen),
      ThaliNotificationClient.Errors.PUBLIC_KEYS_TO_LISTEN_NOT_ARRAY);

    this._publicKeysToListen = [];
    this._publicKeysToListenHashes = [];

    if (publicKeysToListen) {
      this._publicKeysToListen = publicKeysToListen;

      publicKeysToListen.forEach(function (pubKy) {
        self._publicKeysToListenHashes.push(
          NotificationBeacons.createPublicKeyHash(pubKy));
      });
    }

    if (!this.peerDictionary) {
      ThaliMobile.emitter.on('peerAvailabilityChanged',
        this._peerAvailabilityChanged.bind(this));
    }
    this.peerDictionary = new PeerDictionary.PeerDictionary();
  };

/**
 * This method will cause the ThaliNotificationClient to stop listening on
 * {@link module:thaliMobile.event:peerAvailabilityChanged} events and
 * {@link module:thaliNotificationAction.eventEmitter:Resolved} events.
 * It will also clean the peerDictionary object.
 *
 * @public
 */
ThaliNotificationClient.prototype.stop = function () {

  if (this.peerDictionary) {
    ThaliMobile.emitter.removeListener('peerAvailabilityChanged',
      this._peerAvailabilityChanged);

    this.peerDictionary.removeAll();
    this.peerDictionary = null;
  }
};

/**
 * This method handles
 * {@link module:thaliMobile.event:peerAvailabilityChanged}
 * events. New peers will be connected with
 * {@link module:thaliNotificationAction.ThaliNotificationAction} and
 * the action will sort out if we want to communicate with the peer. If
 * we want to communicate with it then ThaliNotificationClient will
 * emit {@link module:thaliNotificationClient.event:peerAdvertisesDataForUs}
 * event.
 *
 * All new actions that we created for incoming peers will be stored
 * in the dictionary. And if we receive an event with null address
 * for peer that already exists in the dictionary this will abort
 * existing action and remove the item from the dictionary.
 *
 * @private
 * @param {module:thaliMobile~discoveryAdvertisingStatus} peerStatus An
 * incoming peer variable.
 */
ThaliNotificationClient.prototype._peerAvailabilityChanged =
  function (peerStatus) {

    // Todo: clean when coordinated tests are done
    console.log('_peerAvailabilityChanged');
    console.log('peerIdentifier:' + peerStatus.peerIdentifier);
    console.log('connectionType:' + peerStatus.connectionType);
    console.log('hostAddress:' + peerStatus.hostAddress);
    console.log('portNumber:' + peerStatus.portNumber);
    console.log('_peerAvailabilityChanged - end');

    if (!peerStatus) {
      logger.warn('_peerAvailabilityChanged: peerStatus is not set');
      return;
    }

    if (!peerStatus.peerIdentifier) {
      logger.warn('_peerAvailabilityChanged: peerIdentifier is not set');
      return;
    }

    if (peerStatus.hostAddress &&
        peerStatus.portNumber &&
        peerStatus.suggestedTCPTimeout &&
        peerStatus.connectionType)
    {
      assert(!this.peerDictionary.exists(peerStatus.peerIdentifier),
        'peerAvailabilityChanged event with the same peerId'+
        ' should not occur. peerIdentifier:' + peerStatus.peerIdentifier);

      var peerConnectionInfo = new PeerDictionary.PeerConnectionInformation(
        peerStatus.connectionType, peerStatus.hostAddress,
        peerStatus.portNumber, peerStatus.suggestedTCPTimeout);

      var peerEntry = new PeerDictionary.NotificationPeerDictionaryEntry(
        PeerDictionary.peerState.CONTROLLED_BY_POOL);

      this._createNewAction(peerEntry, peerStatus.peerIdentifier,
        peerConnectionInfo);
    }
    else if (!peerStatus.hostAddress) {
      // Remove the old peer if it exists.
      this.peerDictionary.remove(peerStatus.peerIdentifier);
    }
  };

// jscs:disable maximumLineLength
/**
 * This function creates a new action and sets connection info into it.
 * Then it enqueues the action in the peer pool and adds the entry into
 * the dictionary.
 *
 * @private
 * @param {module:thaliPeerDictionary~NotificationPeerDictionaryEntry} peerEntry
 * @param {string} peerIdentifier
 * @param {module:thaliPeerDictionary.PeerConnectionInformation} peerConnectionInfo
 */
// jscs:enable maximumLineLength
ThaliNotificationClient.prototype._createNewAction =
  function (peerEntry, peerIdentifier, peerConnectionInfo ) {

    var action = new ThaliNotificationAction(
      peerIdentifier,
      this._ecdhForLocalDevice,
      this._addressBookCallback,
      peerConnectionInfo);

    action.eventEmitter.on(ThaliNotificationAction.Events.Resolved,
      this._resolved.bind(this));

    peerEntry.notificationAction = action;

    var enqueueError = this._thaliPeerPoolInterface.enqueue(action);

    if (!enqueueError) {
      this.peerDictionary.addUpdateEntry(peerIdentifier, peerEntry);
    } else {
      logger.warn('_createAndEnqueueAction: failed to enqueue an item: %s',
        enqueueError.message);
    }
  };

/**
 * This function handles
 * {@link module:thaliNotificationAction.eventEmitter:Resolved} events
 * coming from the actions.
 *
 * @private
 * @param {string} peerId Identifies the peer this action belongs to.
 * @param {module:thaliNotificationAction.ActionResolution} resolution
 * The result of the actions.
 * @param {?module:thaliNotificationBeacons~parseBeaconsResponse} beaconDetails
 * Null if none of the beacons could be validated as being targeted
 * at the local peer or if the beacon came from a remote peer the
 * local peer does not wish to communicate with. If not null then a
 * beacon has been identified to be targeted at the local peer.
 */
// jscs:disable maximumLineLength
ThaliNotificationClient.prototype._resolved =
  function (peerId, resolution, beaconDetails) {

    if (!this.peerDictionary) {
      return;
    }

    var entry = this.peerDictionary.get(peerId);

    if (!entry) {
      return;
    }

    switch (resolution) {
      case ThaliNotificationAction.ActionResolution.BEACONS_RETRIEVED_AND_PARSED:
      {
        var connInfo = entry.notificationAction.getConnectionInformation();
        entry.peerState = PeerDictionary.peerState.RESOLVED;
        this.peerDictionary.addUpdateEntry(peerId, entry);

        // todo: this event needs psk additions
        var peerAdvertises = new PeerAdvertisesDataForUs(
          beaconDetails.unencryptedKeyId,
          'pskIdentifyField',
          [1, 2, 3],
          connInfo.getHostAddress(),
          connInfo.getPortNumber(),
          connInfo.getSuggestedTCPTimeout(),
          entry.notificationAction.getConnectionType()
        );

        this.emit(ThaliNotificationClient.Events.PeerAdvertisesDataForUs,
          peerAdvertises);

        break;
      }

      case ThaliNotificationAction.ActionResolution.BEACONS_RETRIEVED_BUT_BAD:
      {
        // This indicates a malfunctioning peer. We need to assume they are bad
        // all up and mark their entry as RESOLVED without taking any further
        // action. This means we will ignore this peerIdentifier
        // in the future.
        entry.peerState = PeerDictionary.peerState.RESOLVED;
        this.peerDictionary.addUpdateEntry(peerId, entry);
        break;
      }

      case ThaliNotificationAction.ActionResolution.HTTP_BAD_RESPONSE:
      case ThaliNotificationAction.ActionResolution.NETWORK_PROBLEM:
      case ThaliNotificationAction.ActionResolution.KILLED:
      {
        // This tells us that the peer is there but not in good shape or
        // ThaliPeerPoolInterface called kill due to resource exhaustion.
        // We will for wait time out specified in the RETRY_TIMEOUTS
        // array and try again.
        var timeOutHandler = function (peerId) {

          entry = this.peerDictionary.get(peerId);
          if (entry && entry.peerState === PeerDictionary.peerState.WAITING) {
            this._createNewAction(
              entry,
              peerId,
              entry.notificationAction.getConnectionInformation());
          } else {
            assert(false, 'unknown state should be WAITING');
          }
        };

        var maxRetries = ThaliNotificationClient.RETRY_TIMEOUTS.length;
        if (entry.retryCounter < maxRetries) {

          entry.peerState = PeerDictionary.peerState.WAITING;
          this.peerDictionary.addUpdateEntry(peerId, entry);

          // Adds 0 - 100 ms random component to keep concurrent tryouts
          // out of sync.
          var timeOut =
            ThaliNotificationClient.RETRY_TIMEOUTS[entry.retryCounter++] +
            Math.floor(Math.random() * 101);

          entry.waitingTimeout = setTimeout(
            timeOutHandler.bind(this, peerId),
            timeOut);
          this.peerDictionary.addUpdateEntry(peerId, entry);
        } else {
          // Gives up after all the timeouts from the RETRY_TIMEOUTS array
          // has been spent.
          entry.peerState = PeerDictionary.peerState.RESOLVED;
          this.peerDictionary.addUpdateEntry(peerId, entry);
        }
        break;
      }
    }
  };
// jscs:enable maximumLineLength
/**
 * We use following timeouts to retry when the action fails with network
 * error.
 *
 * @public
 * @type {number[]}
 */
ThaliNotificationClient.RETRY_TIMEOUTS =
  [100, 300, 600, 1200, 2400 , 4800, 9600];

/**
 * Fired whenever we discover a peer who is looking for us.
 *
 * @public
 * @event module:thaliNotificationClient.event:peerAdvertisesDataForUs
 * @type {PeerAdvertisesDataForUs}
 */
ThaliNotificationClient.Events = {
  PeerAdvertisesDataForUs: 'peerAdvertisesDataForUs'
};

ThaliNotificationClient.Errors = {
  PEERPOOL_NOT_NULL : 'thaliPeerPoolInterface must not be null',
  EDCH_FOR_LOCAL_DEVICE_NOT_NULL : 'ecdhForLocalDevice must not be null',
  PUBLIC_KEYS_TO_LISTEN_NOT_ARRAY: 'Public keys to listen must be an array'
};

module.exports = ThaliNotificationClient;
<commit_msg>Check that peerdictionary still exists in _peerAvailabilityChanged<commit_after>'use strict';

var PeerDictionary = require('./thaliPeerDictionary');
var ThaliMobile = require('../thaliMobile');
var ThaliNotificationAction = require('./thaliNotificationAction.js');
var NotificationBeacons = require('./thaliNotificationBeacons');
var logger = require('../../thalilogger')('thaliNotificationClient');
var assert = require('assert');
var EventEmitter = require('events').EventEmitter;
var util = require('util');

/** @module thaliNotificationClient */

/**
 * @classdesc Data of peerAdvertisesDataForUs event
 * @constructor
 * @param {buffer} keyId The buffer contains the ECDH public key for the
 * peer.
 * @param {string} pskIdentifyField
 * @param {buffer} psk This is the calculated pre-shared key that will be
 * needed to establish a TLS PSK connection.
 * @param {string} hostAddress The IP/DNS address of the peer
 * @param {number} portNumber The TCP/IP port at the hostAddress the peer
 * can be contacted on
 * @param {number} suggestedTCPTimeout Provides a hint to what time out to
 * put on the TCP connection. For some transports a handshake can take quite a
 * long time.
 * @param {module:thaliMobile.connectionTypes} connectionType The type of
 * connection that will be used when connecting to this peer.
 */
function PeerAdvertisesDataForUs (keyId, pskIdentifyField,
                                  psk, hostAddress, portNumber,
                                  suggestedTCPTimeout, connectionType) {
  this.keyId = keyId;
  this.pskIdentifyField = pskIdentifyField;
  this.psk = psk;
  this.hostAddress = hostAddress;
  this.portNumber = portNumber;
  this.suggestedTCPTimeout = suggestedTCPTimeout;
  this.connectionType = connectionType;
}
// jscs:disable maximumLineLength
/**
 * @classdesc Creates a class that can register to receive the {@link
 * module:thaliMobile.event:peerAvailabilityChanged} event. It will listen for
 * the event and upon receiving it, will enqueue an action with the submitted
 * thaliPeerPoolInterface. Once called back by the pool then the callback will
 * issue a HTTP GET request to retrieve the notification beacons for the peer,
 * parse them, see if one matches and if so then fire a {@link
 * module:thaliNotificationClient.event:peerAdvertisesDataForUs}. Callers can
 * listen for the event by using the emitter member.
 *
 * @public
 * @constructor
 * @param {module:thaliPeerPoolInterface~ThaliPeerPoolInterface} thaliPeerPoolInterface
 * Requests to retrieve notification beacons are enqueued on this object in
 * order to make sure we don't overwhelm our bandwidth or native communication
 * capabilities.
 * @param {Crypto.ECDH} ecdhForLocalDevice A Crypto.ECDH object initialized
 * with the local device's public and private keys.
 * @fires module:thaliNotificationClient.event:peerAdvertisesDataForUs
 * @throws {Error} thaliPeerPoolInterface cannot be null
 * @throws {Error} ecdhForLocalDevice cannot be null
 */
// jscs:enable maximumLineLength
function ThaliNotificationClient(thaliPeerPoolInterface, ecdhForLocalDevice) {
  EventEmitter.call(this);
  var self = this;

  assert(thaliPeerPoolInterface,
    ThaliNotificationClient.Errors.PEERPOOL_NOT_NULL);
  assert(ecdhForLocalDevice,
    ThaliNotificationClient.Errors.EDCH_FOR_LOCAL_DEVICE_NOT_NULL);

  this.peerDictionary = null;
  this._thaliPeerPoolInterface = thaliPeerPoolInterface;
  this._ecdhForLocalDevice = ecdhForLocalDevice;
  this._publicKeysToListen = [];
  this._publicKeysToListenHashes = [];

  this._addressBookCallback = function (unencryptedKeyId) {

    if (!Buffer.isBuffer(unencryptedKeyId)) {
      logger.warn('_addressBookCallback: unencryptedKeyId is not an buffer');
      return null;
    }

    for (var i = 0 ; i < self._publicKeysToListenHashes.length ; i++) {
      var pubKeyHash = self._publicKeysToListenHashes[i];
      if (unencryptedKeyId.compare(pubKeyHash) === 0) {
        return self._publicKeysToListen[i];
      }
    }
    return null;
  };
}

util.inherits(ThaliNotificationClient, EventEmitter);

/**
 * A dictionary used to track the state of peers we have received notifications
 * about from {@link module:thaliMobile}.
 *
 * @type {ThaliNotificationClient.PeerDictionary}
 */
ThaliNotificationClient.prototype.peerDictionary = null;

/**
 * This method will cause a listener to be registered on the global singleton
 * {@link module:thaliMobile} object for the {@link
 * module:thaliMobile.event:peerAvailabilityChanged} event.
 *
 * This is not idempotent method since it can be called multiple times with
 * different prioritization lists but repeated start calls MUST only update that
 * list and MUST NOT cause multiple listeners to be registered with thaliMobile.
 *
 * @public
 * @param {Buffer[]} publicKeysToListen Used to decide what peer
 * notifications to pay attention to. This list consists of an array
 * of buffers that contain the serialization of the public ECDH keys of the
 * peers we are interested in syncing with.
 *
 * @throws {Error} Public keys to listen must be an array
 */
ThaliNotificationClient.prototype.start =
  function (publicKeysToListen) {
    var self = this;

    assert(Array.isArray(publicKeysToListen),
      ThaliNotificationClient.Errors.PUBLIC_KEYS_TO_LISTEN_NOT_ARRAY);

    this._publicKeysToListen = [];
    this._publicKeysToListenHashes = [];

    if (publicKeysToListen) {
      this._publicKeysToListen = publicKeysToListen;

      publicKeysToListen.forEach(function (pubKy) {
        self._publicKeysToListenHashes.push(
          NotificationBeacons.createPublicKeyHash(pubKy));
      });
    }

    if (!this.peerDictionary) {
      ThaliMobile.emitter.on('peerAvailabilityChanged',
        this._peerAvailabilityChanged.bind(this));
    }
    this.peerDictionary = new PeerDictionary.PeerDictionary();
  };

/**
 * This method will cause the ThaliNotificationClient to stop listening on
 * {@link module:thaliMobile.event:peerAvailabilityChanged} events and
 * {@link module:thaliNotificationAction.eventEmitter:Resolved} events.
 * It will also clean the peerDictionary object.
 *
 * @public
 */
ThaliNotificationClient.prototype.stop = function () {

  if (this.peerDictionary) {
    ThaliMobile.emitter.removeListener('peerAvailabilityChanged',
      this._peerAvailabilityChanged);

    this.peerDictionary.removeAll();
    this.peerDictionary = null;
  }
};

/**
 * This method handles
 * {@link module:thaliMobile.event:peerAvailabilityChanged}
 * events. New peers will be connected with
 * {@link module:thaliNotificationAction.ThaliNotificationAction} and
 * the action will sort out if we want to communicate with the peer. If
 * we want to communicate with it then ThaliNotificationClient will
 * emit {@link module:thaliNotificationClient.event:peerAdvertisesDataForUs}
 * event.
 *
 * All new actions that we created for incoming peers will be stored
 * in the dictionary. And if we receive an event with null address
 * for peer that already exists in the dictionary this will abort
 * existing action and remove the item from the dictionary.
 *
 * @private
 * @param {module:thaliMobile~discoveryAdvertisingStatus} peerStatus An
 * incoming peer variable.
 */
ThaliNotificationClient.prototype._peerAvailabilityChanged =
  function (peerStatus) {

    if (!this.peerDictionary) {
      return;
    }
    if (!peerStatus) {
      logger.warn('_peerAvailabilityChanged: peerStatus is not set');
      return;
    }
    if (!peerStatus.peerIdentifier) {
      logger.warn('_peerAvailabilityChanged: peerIdentifier is not set');
      return;
    }

    // Todo: clean when coordinated tests are done
    console.log('_peerAvailabilityChanged');
    console.log('peerIdentifier:' + peerStatus.peerIdentifier);
    console.log('connectionType:' + peerStatus.connectionType);
    console.log('hostAddress:' + peerStatus.hostAddress);
    console.log('portNumber:' + peerStatus.portNumber);
    console.log('_peerAvailabilityChanged - end');

    if (peerStatus.hostAddress &&
        peerStatus.portNumber &&
        peerStatus.suggestedTCPTimeout &&
        peerStatus.connectionType)
    {
      assert(!this.peerDictionary.exists(peerStatus.peerIdentifier),
        'peerAvailabilityChanged event with the same peerId'+
        ' should not occur. peerIdentifier:' + peerStatus.peerIdentifier);

      var peerConnectionInfo = new PeerDictionary.PeerConnectionInformation(
        peerStatus.connectionType, peerStatus.hostAddress,
        peerStatus.portNumber, peerStatus.suggestedTCPTimeout);

      var peerEntry = new PeerDictionary.NotificationPeerDictionaryEntry(
        PeerDictionary.peerState.CONTROLLED_BY_POOL);

      this._createNewAction(peerEntry, peerStatus.peerIdentifier,
        peerConnectionInfo);
    }
    else if (!peerStatus.hostAddress) {
      // Remove the old peer if it exists.
      this.peerDictionary.remove(peerStatus.peerIdentifier);
    }
  };

// jscs:disable maximumLineLength
/**
 * This function creates a new action and sets connection info into it.
 * Then it enqueues the action in the peer pool and adds the entry into
 * the dictionary.
 *
 * @private
 * @param {module:thaliPeerDictionary~NotificationPeerDictionaryEntry} peerEntry
 * @param {string} peerIdentifier
 * @param {module:thaliPeerDictionary.PeerConnectionInformation} peerConnectionInfo
 */
// jscs:enable maximumLineLength
ThaliNotificationClient.prototype._createNewAction =
  function (peerEntry, peerIdentifier, peerConnectionInfo ) {

    var action = new ThaliNotificationAction(
      peerIdentifier,
      this._ecdhForLocalDevice,
      this._addressBookCallback,
      peerConnectionInfo);

    action.eventEmitter.on(ThaliNotificationAction.Events.Resolved,
      this._resolved.bind(this));

    peerEntry.notificationAction = action;

    var enqueueError = this._thaliPeerPoolInterface.enqueue(action);

    if (!enqueueError) {
      this.peerDictionary.addUpdateEntry(peerIdentifier, peerEntry);
    } else {
      logger.warn('_createAndEnqueueAction: failed to enqueue an item: %s',
        enqueueError.message);
    }
  };

/**
 * This function handles
 * {@link module:thaliNotificationAction.eventEmitter:Resolved} events
 * coming from the actions.
 *
 * @private
 * @param {string} peerId Identifies the peer this action belongs to.
 * @param {module:thaliNotificationAction.ActionResolution} resolution
 * The result of the actions.
 * @param {?module:thaliNotificationBeacons~parseBeaconsResponse} beaconDetails
 * Null if none of the beacons could be validated as being targeted
 * at the local peer or if the beacon came from a remote peer the
 * local peer does not wish to communicate with. If not null then a
 * beacon has been identified to be targeted at the local peer.
 */
// jscs:disable maximumLineLength
ThaliNotificationClient.prototype._resolved =
  function (peerId, resolution, beaconDetails) {

    if (!this.peerDictionary) {
      return;
    }

    var entry = this.peerDictionary.get(peerId);

    if (!entry) {
      return;
    }

    switch (resolution) {
      case ThaliNotificationAction.ActionResolution.BEACONS_RETRIEVED_AND_PARSED:
      {
        var connInfo = entry.notificationAction.getConnectionInformation();
        entry.peerState = PeerDictionary.peerState.RESOLVED;
        this.peerDictionary.addUpdateEntry(peerId, entry);

        // todo: this event needs psk additions
        var peerAdvertises = new PeerAdvertisesDataForUs(
          beaconDetails.unencryptedKeyId,
          'pskIdentifyField',
          [1, 2, 3],
          connInfo.getHostAddress(),
          connInfo.getPortNumber(),
          connInfo.getSuggestedTCPTimeout(),
          entry.notificationAction.getConnectionType()
        );

        this.emit(ThaliNotificationClient.Events.PeerAdvertisesDataForUs,
          peerAdvertises);

        break;
      }

      case ThaliNotificationAction.ActionResolution.BEACONS_RETRIEVED_BUT_BAD:
      {
        // This indicates a malfunctioning peer. We need to assume they are bad
        // all up and mark their entry as RESOLVED without taking any further
        // action. This means we will ignore this peerIdentifier
        // in the future.
        entry.peerState = PeerDictionary.peerState.RESOLVED;
        this.peerDictionary.addUpdateEntry(peerId, entry);
        break;
      }

      case ThaliNotificationAction.ActionResolution.HTTP_BAD_RESPONSE:
      case ThaliNotificationAction.ActionResolution.NETWORK_PROBLEM:
      case ThaliNotificationAction.ActionResolution.KILLED:
      {
        // This tells us that the peer is there but not in good shape or
        // ThaliPeerPoolInterface called kill due to resource exhaustion.
        // We will for wait time out specified in the RETRY_TIMEOUTS
        // array and try again.
        var timeOutHandler = function (peerId) {

          entry = this.peerDictionary.get(peerId);
          if (entry && entry.peerState === PeerDictionary.peerState.WAITING) {
            this._createNewAction(
              entry,
              peerId,
              entry.notificationAction.getConnectionInformation());
          } else {
            assert(false, 'unknown state should be WAITING');
          }
        };

        var maxRetries = ThaliNotificationClient.RETRY_TIMEOUTS.length;
        if (entry.retryCounter < maxRetries) {

          entry.peerState = PeerDictionary.peerState.WAITING;
          this.peerDictionary.addUpdateEntry(peerId, entry);

          // Adds 0 - 100 ms random component to keep concurrent tryouts
          // out of sync.
          var timeOut =
            ThaliNotificationClient.RETRY_TIMEOUTS[entry.retryCounter++] +
            Math.floor(Math.random() * 101);

          entry.waitingTimeout = setTimeout(
            timeOutHandler.bind(this, peerId),
            timeOut);
          this.peerDictionary.addUpdateEntry(peerId, entry);
        } else {
          // Gives up after all the timeouts from the RETRY_TIMEOUTS array
          // has been spent.
          entry.peerState = PeerDictionary.peerState.RESOLVED;
          this.peerDictionary.addUpdateEntry(peerId, entry);
        }
        break;
      }
    }
  };
// jscs:enable maximumLineLength
/**
 * We use following timeouts to retry when the action fails with network
 * error.
 *
 * @public
 * @type {number[]}
 */
ThaliNotificationClient.RETRY_TIMEOUTS =
  [100, 300, 600, 1200, 2400 , 4800, 9600];

/**
 * Fired whenever we discover a peer who is looking for us.
 *
 * @public
 * @event module:thaliNotificationClient.event:peerAdvertisesDataForUs
 * @type {PeerAdvertisesDataForUs}
 */
ThaliNotificationClient.Events = {
  PeerAdvertisesDataForUs: 'peerAdvertisesDataForUs'
};

ThaliNotificationClient.Errors = {
  PEERPOOL_NOT_NULL : 'thaliPeerPoolInterface must not be null',
  EDCH_FOR_LOCAL_DEVICE_NOT_NULL : 'ecdhForLocalDevice must not be null',
  PUBLIC_KEYS_TO_LISTEN_NOT_ARRAY: 'Public keys to listen must be an array'
};

module.exports = ThaliNotificationClient;
<|endoftext|>


<commit_before>// ua
$(document).ready(function() {

  // ua detection
  if ($.browser.mozilla) {
    $('body').addClass('ff');
  } else if ($.browser.webkit) {
    $('body').addClass('webkit');
  } else if ($.browser.msie) {
    $('body').addClass('ie');
    if ($.browser.version == "7.0") {
      $('body').addClass('ie7');
    } else if ($.browser.version == "8.0") {
      $('body').addClass('ie8');
    }
  }

  // no widows in wiki body
  if ($('#wiki-wrapper').hasClass('page')) {
    $('#template h1, #template h2, #template h3, #template h4, #template h5, #template h6, #template li, #template p').each(
        function(){
            $(this).html($(this).html().replace(/\s([^\s<]+)\s*$/,' $1'));
        }
    );
  }

  if ($('#minibutton-new-page').length) {
    $('#minibutton-new-page').removeClass('jaws');
    $('#minibutton-new-page').click(function(e) {
      e.preventDefault();
      $.GollumDialog.init({
                            title: 'Create New Page',
                            fields: [
                              {
                                id:   'name',
                                name: 'Page Name',
                                type: 'text'
                              }
                            ],
                            OK: function( res ) {
                              var n = 'New Page';
                              if ( res['name'] )
                                var n = res['name'];
                              n = encodeURIComponent( n );
                              window.location = '/' + n;
                            }
                          });
    });
  }

  if ($('#wiki-wrapper').hasClass('history')) {
    $('#wiki-history td.checkbox input').each(function() {
      $(this).click(highlightChecked);
      if ( $(this).is(':checked') ) {
        nodeSelector.checkNode($(this));
      }
    });

    if ($('.history a.action-compare-revision').length) {
      $('.history a.action-compare-revision').click(function() {
        $("#version-form").submit();
      });
    }
  }

  if ($('#searchbar a#search-submit').length) {
    $.GollumPlaceholder.add($('#searchbar #search-query'));
    $('#searchbar a#search-submit').click(function(e) {
      e.preventDefault();
      $('#searchbar #search-form')[0].submit();
    });
    $('#searchbar #search-form').submit(function(e) {
      $.GollumPlaceholder.clearAll();
      $(this).unbind('submit');
      $(this).submit();
    });
  }

  if ($('#gollum-revert-form').length &&
      $('.gollum-revert-button').length ) {
    $('a.gollum-revert-button').click(function(e) {
      e.preventDefault();
      $('#gollum-revert-form').submit();
    });
  }

});

var nodeSelector = {

  node1: null,
  node2: null,

  selectNodeRange: function( n1, n2 ) {
    if ( nodeSelector.node1 && nodeSelector.node2 ) {
      $('#wiki-history td.selected').removeClass('selected');
      nodeSelector.node1.addClass('selected');
      nodeSelector.node2.addClass('selected');

      // swap the nodes around if they went in reverse
      if ( nodeSelector.nodeComesAfter( nodeSelector.node1,
                                        nodeSelector.node2 ) ) {
        var n = nodeSelector.node1;
        nodeSelector.node1 = nodeSelector.node2;
        nodeSelector.node2 = n;
      }

      var s = true;
      var $nextNode = nodeSelector.node1.next();
      while ( $nextNode ) {
        $nextNode.addClass('selected');
        if ( $nextNode[0] == nodeSelector.node2[0] ) {
          break;
        }
        $nextNode = $nextNode.next();
      }
    }
  },

  nodeComesAfter: function ( n1, n2 ) {
    var s = false;
    $(n1).prevAll().each(function() {
      if ( $(this)[0] == $(n2)[0] ) {
        s = true;
      }
    });
    return s;
  },

  checkNode: function( nodeCheckbox ) {
    var $nodeCheckbox = nodeCheckbox;
    var $node = $(nodeCheckbox).parent().parent();
    // if we're unchecking
     if ( !$nodeCheckbox.is(':checked') ) {

       // remove the range, since we're breaking it
       $('#wiki-history tr.selected').each(function() {
         if ( $(this).find('td.checkbox input').is(':checked') ) {
           return;
         }
         $(this).removeClass('selected');
       });

       // no longer track this
       if ( $node[0] == nodeSelector.node1[0] ) {
         nodeSelector.node1 = null;
         if ( nodeSelector.node2 ) {
           nodeSelector.node1 = nodeSelector.node2;
           nodeSelector.node2 = null;
         }
       } else if ( $node[0] == nodeSelector.node2[0] ) {
         nodeSelector.node2 = null;
       }

     } else {
       if ( !nodeSelector.node1 ) {
         nodeSelector.node1 = $node;
         nodeSelector.node1.addClass('selected');
       } else if ( !nodeSelector.node2 ) {
         // okay, we don't have a node 2 but have a node1
         nodeSelector.node2 = $node;
         nodeSelector.node2.addClass('selected');
         nodeSelector.selectNodeRange( nodeSelector.node1,
                                       nodeSelector.node2 );
       } else {
         // we have two selected already
         $nodeCheckbox[0].checked = false;
       }
     }
  }

};

function highlightOn() {
  $(this).parent().parent().animate({
                                      backgroundColor: '#ffffea',
                                      duration: 400
                                    });
}

function highlightOff() {
  var color = '#ebf2f6';
  if ($(this).parent().parent().hasClass('alt-row')) {
    color = '#f3f7fa';
  }
  $(this).parent().parent().animate({
                                      backgroundColor: color,
                                      duration: 400
                                    });
}

function highlightChecked() {
 nodeSelector.checkNode($(this));
}

function initMathJax() {

}
<commit_msg>Remove widon't code<commit_after>// ua
$(document).ready(function() {

  // ua detection
  if ($.browser.mozilla) {
    $('body').addClass('ff');
  } else if ($.browser.webkit) {
    $('body').addClass('webkit');
  } else if ($.browser.msie) {
    $('body').addClass('ie');
    if ($.browser.version == "7.0") {
      $('body').addClass('ie7');
    } else if ($.browser.version == "8.0") {
      $('body').addClass('ie8');
    }
  }

  if ($('#minibutton-new-page').length) {
    $('#minibutton-new-page').removeClass('jaws');
    $('#minibutton-new-page').click(function(e) {
      e.preventDefault();
      $.GollumDialog.init({
                            title: 'Create New Page',
                            fields: [
                              {
                                id:   'name',
                                name: 'Page Name',
                                type: 'text'
                              }
                            ],
                            OK: function( res ) {
                              var n = 'New Page';
                              if ( res['name'] )
                                var n = res['name'];
                              n = encodeURIComponent( n );
                              window.location = '/' + n;
                            }
                          });
    });
  }

  if ($('#wiki-wrapper').hasClass('history')) {
    $('#wiki-history td.checkbox input').each(function() {
      $(this).click(highlightChecked);
      if ( $(this).is(':checked') ) {
        nodeSelector.checkNode($(this));
      }
    });

    if ($('.history a.action-compare-revision').length) {
      $('.history a.action-compare-revision').click(function() {
        $("#version-form").submit();
      });
    }
  }

  if ($('#searchbar a#search-submit').length) {
    $.GollumPlaceholder.add($('#searchbar #search-query'));
    $('#searchbar a#search-submit').click(function(e) {
      e.preventDefault();
      $('#searchbar #search-form')[0].submit();
    });
    $('#searchbar #search-form').submit(function(e) {
      $.GollumPlaceholder.clearAll();
      $(this).unbind('submit');
      $(this).submit();
    });
  }

  if ($('#gollum-revert-form').length &&
      $('.gollum-revert-button').length ) {
    $('a.gollum-revert-button').click(function(e) {
      e.preventDefault();
      $('#gollum-revert-form').submit();
    });
  }

});

var nodeSelector = {

  node1: null,
  node2: null,

  selectNodeRange: function( n1, n2 ) {
    if ( nodeSelector.node1 && nodeSelector.node2 ) {
      $('#wiki-history td.selected').removeClass('selected');
      nodeSelector.node1.addClass('selected');
      nodeSelector.node2.addClass('selected');

      // swap the nodes around if they went in reverse
      if ( nodeSelector.nodeComesAfter( nodeSelector.node1,
                                        nodeSelector.node2 ) ) {
        var n = nodeSelector.node1;
        nodeSelector.node1 = nodeSelector.node2;
        nodeSelector.node2 = n;
      }

      var s = true;
      var $nextNode = nodeSelector.node1.next();
      while ( $nextNode ) {
        $nextNode.addClass('selected');
        if ( $nextNode[0] == nodeSelector.node2[0] ) {
          break;
        }
        $nextNode = $nextNode.next();
      }
    }
  },

  nodeComesAfter: function ( n1, n2 ) {
    var s = false;
    $(n1).prevAll().each(function() {
      if ( $(this)[0] == $(n2)[0] ) {
        s = true;
      }
    });
    return s;
  },

  checkNode: function( nodeCheckbox ) {
    var $nodeCheckbox = nodeCheckbox;
    var $node = $(nodeCheckbox).parent().parent();
    // if we're unchecking
     if ( !$nodeCheckbox.is(':checked') ) {

       // remove the range, since we're breaking it
       $('#wiki-history tr.selected').each(function() {
         if ( $(this).find('td.checkbox input').is(':checked') ) {
           return;
         }
         $(this).removeClass('selected');
       });

       // no longer track this
       if ( $node[0] == nodeSelector.node1[0] ) {
         nodeSelector.node1 = null;
         if ( nodeSelector.node2 ) {
           nodeSelector.node1 = nodeSelector.node2;
           nodeSelector.node2 = null;
         }
       } else if ( $node[0] == nodeSelector.node2[0] ) {
         nodeSelector.node2 = null;
       }

     } else {
       if ( !nodeSelector.node1 ) {
         nodeSelector.node1 = $node;
         nodeSelector.node1.addClass('selected');
       } else if ( !nodeSelector.node2 ) {
         // okay, we don't have a node 2 but have a node1
         nodeSelector.node2 = $node;
         nodeSelector.node2.addClass('selected');
         nodeSelector.selectNodeRange( nodeSelector.node1,
                                       nodeSelector.node2 );
       } else {
         // we have two selected already
         $nodeCheckbox[0].checked = false;
       }
     }
  }

};

function highlightOn() {
  $(this).parent().parent().animate({
                                      backgroundColor: '#ffffea',
                                      duration: 400
                                    });
}

function highlightOff() {
  var color = '#ebf2f6';
  if ($(this).parent().parent().hasClass('alt-row')) {
    color = '#f3f7fa';
  }
  $(this).parent().parent().animate({
                                      backgroundColor: color,
                                      duration: 400
                                    });
}

function highlightChecked() {
 nodeSelector.checkNode($(this));
}

function initMathJax() {

}
<|endoftext|>


<commit_before>function updateFollowButtons(){
    $.getJSON("/getfollowing", function(data){
        var followlist = data['JSON_follow_list'];
        console.log(followlist)
        $(".followButton").each(function(){
            for (item in followlist) {
                if ($(this).closest('tr').attr('id') === followlist[item]) {
                    $(this).addClass('following');
                    $(this).text('Following');
                }
                // else {
                //     $(this).removeClass('following');
                //     $(this).removeClass('unfollow');
                //     $(this).text('Follow');
                // }
            }
        });
    });
}

//call this whenever a new followBUtton appears on the DOM//
function followButtonClickListeners(event){
    event.preventDefault();
    $button = $(this);
    var id = $(this).closest('tr').attr('id')
    console.log(id)
    if($button.hasClass('following')){

        $.post("/unfollow", {"id": id}, function(data){
            console.log(data);
            $button.removeClass('following');
            $button.removeClass('unfollow');
            $button.text('Follow');
            if ($button.closest('table').attr('id') === "following-list"){
                $button.closest('tr').remove();
                $("#followers-list").find("#" + id).find("button").removeClass("following").text("Follow");
            }
            else {
                $("#following-list").find("#" + id).remove();
            }
        })
    } else {
            $.post("/follow", {'id': id}, function(data) {
            console.log("following ", data["following"]);
            var following = data["following"]
            $button.addClass('following');
            $button.text('Following');
            $("#following-list tr:last").after("
" + following + "	<button class='btn followButton following'>Following</button>
")
        })
    }
}

/// Call this on any event that changes the number of ///
///songs, following or followers of a user ///
function update_user_profile() {
        $.ajax({
            url:'updateprofile',
            dataType:'json',
            success:function(data) {
                var song_count = data['song_count']
                var following_count = data['following_count']
                var followers_count = data['followers_count']
                $("#profile_songs").html(song_count)
                $("#profile_followers").html(following_count)
                $("#profile_following").html(followers_count)
              }
        })
    }

function check_song_count() {
    var num_songs = $("#profile_songs").text()
    if (num_songs === "0")    {
         $("#syncModal").modal('show')
         $("#sync_button").on('click', function() {
            $.getJSON("/seed", function(data){
                if (data['status'] === "redirect") {
                    $(location).attr('href', '/')
                }
                else {
                    update_user_profile()

                }
            })
        })
    }
}

$(document).ready(function(){
    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
    })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

    ga('create', 'UA-66616301-1', 'auto');
    ga('send', 'pageview');

    /// On page load checks if the user has any songs, and makes sure they sync ///
    check_song_count()

    /// Sets the size of the profile text
    var profile_height = $("#profile").height()
    $("#profile_stats td").css(({"font-size":5*profile_height/10,"height":5*profile_height/10}))
    $("#profile_stats_names td").css(({"font-size":3*profile_height/10,"height":3*profile_height/10}))

    // console.log(profile_height)

    $("#following").on("click", function(event){
        event.preventDefault();
        $("#following-button").addClass("disabled").prop('disabled', true);
        $("#followers-button").addClass("active").prop('disabled', false);
        $("#followers-list").hide();
        updateFollowButtons();
    });
    $("#following-button").on("click", function() {
        if ($(this).hasClass("active")){
            $(this).removeClass("active");
            $(this).addClass("disabled").prop('disabled', true);
            $("#followers-button").removeClass("disabled");
            $("#followers-button").addClass("active").prop('disabled', false);
            $("#followers-list").hide();
            $("#following-list").show();
            updateFollowButtons()
        }
        else {
            $(this).removeClass("disabled");
            $(this).addClass("active").prop('disabled', false);
            $("#followers-button").removeClass("active");
            $("#followers-button").addClass("disabled").prop('disabled', true);
            updateFollowButtons()
        }
    })

    $("#followers-button").on("click", function() {
        if ($(this).hasClass("active")){
            $(this).removeClass("active");
            $(this).addClass("disabled").prop('disabled', true);
            $("#following-button").removeClass("disabled");
            $("#following-button").addClass("active").prop('disabled', false);
            $("#following-list").hide();
            $("#followers-list").show();
            updateFollowButtons();
        }
        else {
            $(this).removeClass("disabled");
            $(this).addClass("active").prop('disabled', false);
            $("#following-button").removeClass("active");
            $("#following-button").addClass("disabled").prop('disabled', true);
            updateFollowButtons();
        }
    })

    $.getJSON("/get_minifeed", function(data){
        var all_posts = data['all_posts'];
        var count = 0;
        for (post in all_posts) {
            $("#mini-feed").append("

posted by: " + all_posts[post].user + "

" + all_posts[post].content + "

");
            count += 1;
        }
    });

    $(document).ajaxStart(function(){
        $('#loading').html('')
    });
    $(document).ajaxComplete(function(){
        $("#loading").empty();
    });

    //// Syncs the users playlist,
    $("#sync").on("click", function(event){
        event.preventDefault();
        $.getJSON("/seed", function(data){
            if (data['status'] === "redirect") {
                $(location).attr('href', '/')
            }
            else {
                update_user_profile()
            }
        })
    })
    $("#user-search-results").on('click', 'button.followButton', function(event){
        event.preventDefault();
        $button = $(this);
        var id = $(this).closest('tr').attr('id')
        if($button.hasClass('following')){

            $.post("/unfollow", {"id": id}, function(data){
                $button.removeClass('following');
                $button.removeClass('unfollow');
                $button.text('Follow');
                if ($button.closest('table').attr('id') === "following-list"){
                    $button.closest('tr').remove();
                    $("#followers-list").find("#" + id).find("button").removeClass("following").text("Follow");
                }
                else {
                    $("#following-list").find("#" + id).remove();
                }
            })
        } else {
                $.post("/follow", {'id': id}, function(data) {
                var following = data["following"]
                $button.addClass('following');
                $button.text('Following');
                $("#following-list tr:last").after("
" + following + "	<button class='btn followButton following'>Following</button>
")

            })
        }
    });
    $("#user-search-results").on("mouseenter", "button.followButton", function(){
        console.log("got it")
        $button = $(this)
        if($button.hasClass('following')) {
            $button.addClass('unfollow');
            $button.text('Unfollow');
        }
    });

    $("#user-search-results").on("mouseleave", "button.followButton", function(){
        if($button.hasClass('following')) {
            $button.removeClass('unfollow');
            $button.text('Following');
        }
    });

    $("#createpost").on("click", function() {
        $("#searchresult_list").empty();
    })
    $("#song-search-button").on("click", function(event) {
        event.preventDefault();
        $("#searchresult_list").empty();
        var query = $("[name=search_query]").val();
        $.getJSON("search", {"search_query": query}, function(data){
            var count = 0;
            for (item in data["search_result"]) {
                $("#searchresult_list").append("
" +data["search_result"][item] + "<button class=select-song>select</button>
");
                count += 1;
            }
        })
    })
    $("#searchresult_list").on("click", ".select-song", function() {
        var text = $(this).parent().text();
        var substring = text.substring(0, text.length - 6);
        $("#searchresult_list").empty();
        $("#song-search-button").hide();
        $("[name=search_query]").val(substring);
    })
    $("#songsearch").on("submit", function(event) {
        event.preventDefault();
        var track_name = $("[name=search_query]").val()
        $.getJSON("track_uri", {"track_name": track_name}, function(data){
            var track_uri = data['track_uri'];
        $("#song-reference").empty().append("");
        $("#song-comment").append($("#comment").val());
        });
    })
    $("#post-it-button").on("click", function() {
        var comment = $("#song-comment").text();
        var src = $("#song-reference")[0].firstChild.src;
        var track_uri = src.replace("https://embed.spotify.com/?uri=","")
        $.post("create_post", {"comment": comment, "track_uri": track_uri}, function(data) {
        })
        $("#comment").val('');
        $("[name=search_query]").val('');
    })
    $("#usersearch").on("submit", function(event) {
        event.preventDefault();
        $("#user-search-results").empty();
        var usernameQuery = $("[name=user_query]").val();
        $.getJSON("find_user", {"usernameQuery": usernameQuery}, function(data){
            var userList = data["search_result"];
            console.log(userList)
            if (userList === "No results found..." || userList === "Please input a username.") {
                $("#user-search-results").append("
" + userList + "
")
            }
            else {
                var $searchdiv = $("#user-search");
                //

                for (var i in userList) {
                    $("#user-search-results").append("
" + userList[i] + "	<button class='btn followButton'>Follow</button>
");
                }
            }
        });
        updateFollowButtons();
    });

    /////// Generates playlists ///////
        /// Sets the initial size of the playlist div
    $(".timelineplaylist").css("height","20em")

    var num_songs = parseInt($("#profile_songs").text())

    if (num_songs > 0) {
        /// Personal ///
        var number_songs = 10
        var follow = 0
        var recency_effect = 10
        var rating_effect = 10
        var duplicate_artist = 10
        var existing_playlist = 10
        var post_data = {"type":"your_home","number_songs":number_songs, "follow":follow, "recency_effect":recency_effect,"rating_effect":rating_effect,"duplicate_artist":duplicate_artist, "existing_playlist": existing_playlist}

        $.post('playlist', post_data, function(data) {
                var your_uris = data['track_uris']
                var cover_art = data['cover_art']
                $("#yourplaylist_image").append('')
                $("#yourplaylist_image").css("width","80%")
                $("#yourplaylist_image_image").css("width","100%");

                /// setting the position of the buttons
                var div_width = $("#yourplaylist").width()
                var imageheight =  $("#yourplaylist_image").width();
                var buttons_width = $("#yourplaylist_buttons").width()
                var buttons_centre = (div_width - imageheight) + 0.5 * imageheight - 0.5 * buttons_width
                $("#yourplaylist_buttons").css({"top":imageheight,"left":buttons_centre})

                ///Resets the size of the playlist div according to the size of the image ///
                $(".timelineplaylist").css("height",imageheight*1.2)

                var string = ""
                for (var song in your_uris) {
                    string += your_uris[song] + ','
                }
                string = string.substring(0, string.length - 1);
                $('#yourplaylist_playlist').css("width","80%");
                var pw = $('#yourplaylist_playlist').width();
                $('#yourplaylist_playlist').css({'height':pw+'px'});

                $('#yourplaylist_playlist').html('')

                /// When the user clicks on the image of the playlist ///
                $('#yourplaylist img').on('click', function() {
                    $(this).fadeOut(2000)
                    $("#yourplaylist_buttons").css("display","inline")
                    /// Bind the like save and dislike events to the button
                    $("#yourplaylist_like").on("click", function() {
                        $.post("rating", {"uris": your_uris, "decision": "like"}, function(data){
                        })
                    })
                    $("#yourplaylist_dislike").on("click", function() {
                        $.post("rating", {"uris": your_uris, "decision": "dislike"}, function(data){
                        })
                    })
                    $("#yourplaylist_save").on("click", function() {
                        $.post("saveplaylist", {"uris": your_uris}, function(data){
                        })
                    })
                })

            })
        }

    /// Friends ///
    var num_following = parseInt($("#profile_following").text())

    if (num_following > 0) {
        var number_songs2 = 10
        var follow2 = 10
        var recency_effect2 = 10
        var rating_effect2 = 10
        var duplicate_artist2 = 5
        var existing_playlist2 = 9
        var post_data2 = {"type":"your_friends","number_songs":number_songs2, "follow":follow2, "recency_effect":recency_effect2,"rating_effect":rating_effect2,"duplicate_artist":duplicate_artist2, "existing_playlist": existing_playlist2}

        $.post('playlist', post_data2, function(data) {
                var friends_uris = data['track_uris']
                var cover_art = data['cover_art']
                $("#friendsplaylist_image").html('');
                $("#friendsplaylist_image").css("width","80%");
                $("#friendsplaylist_image_image").css("width","100%");

                /// setting the position of the buttons
                var div_width = $("#friendsplaylist").width()
                var imageheight =  $("#friendsplaylist_image").width();
                var buttons_width = $("#friendsplaylist_buttons").width()
                var buttons_centre = (div_width - imageheight) + 0.5 * imageheight - 0.5 * buttons_width
                $("#friendsplaylist_buttons").css({"top":imageheight,"left":buttons_centre})

                var string = ""
                for (var song in friends_uris) {
                    string += friends_uris[song] + ','
                }
                string = string.substring(0, string.length - 1);
                $('#friendsplaylist_playlist').css("width","80%");
                var pw = $('#friendsplaylist_playlist').width();
                $('#friendsplaylist_playlist').css({'height':pw+'px'});
                $('#friendsplaylist_playlist').html('')

                /// When the user clicks on the image of the playlist ///
                $('#friendsplaylist img').on('click', function() {
                    $(this).fadeOut(2000)
                    $("#friendsplaylist_buttons").css("display","inline")
                    /// Bind the like save and dislike events to the button
                    $("#friendsplaylist_like").on("click", function() {
                        $.post("rating", {"uris": friends_uris, "decision": "like"}, function(data){
                        })
                    })
                    $("#friendsplaylist_dislike").on("click", function() {
                        $.post("rating", {"uris": friends_uris, "decision": "dislike"}, function(data){
                        })
                    })
                    $("#friendsplaylist_save").on("click", function() {
                        $.post("saveplaylist", {"uris": friends_uris}, function(data){
                        })
                    })

                })

            })

    }

$('#testpopover').popover({
    html:true,
    title:"header",
    content: function() {
        return ''
    }

})

})

<commit_msg>follow buttons working well<commit_after>function updateFollowButtons(){
    $.getJSON("/getfollowing", function(data){
        var followlist = data['JSON_follow_list'];
        console.log(followlist)
        $(".followButton").each(function(){
            for (item in followlist) {
                if ($(this).closest('tr').attr('id') === followlist[item]) {
                    $(this).addClass('following');
                    $(this).text('Following');
                }
            }
        });
    });
}

/// Call this on any event that changes the number of ///
///songs, following or followers of a user ///
function update_user_profile() {
        $.ajax({
            url:'updateprofile',
            dataType:'json',
            success:function(data) {
                var song_count = data['song_count']
                var following_count = data['following_count']
                var followers_count = data['followers_count']
                $("#profile_songs").html(song_count)
                $("#profile_followers").html(followers_count)
                $("#profile_following").html(following_count)
              }
        })
    }

function check_song_count() {
    var num_songs = $("#profile_songs").text()
    if (num_songs === "0")    {
         $("#syncModal").modal('show')
         $("#sync_button").on('click', function() {
            $.getJSON("/seed", function(data){
                if (data['status'] === "redirect") {
                    $(location).attr('href', '/')
                }
                else {
                    update_user_profile()

                }
            })
        })
    }
}

$(document).ready(function(){
    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
    })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

    ga('create', 'UA-66616301-1', 'auto');
    ga('send', 'pageview');

    /// On page load checks if the user has any songs, and makes sure they sync ///
    check_song_count()

    /// Sets the size of the profile text
    var profile_height = $("#profile").height()
    $("#profile_stats td").css(({"font-size":5*profile_height/10,"height":5*profile_height/10}))
    $("#profile_stats_names td").css(({"font-size":3*profile_height/10,"height":3*profile_height/10}))

    // console.log(profile_height)

    $("#following").on("click", function(event){
        event.preventDefault();
        $("#following-button").addClass("disabled").prop('disabled', true);
        $("#followers-button").addClass("active").prop('disabled', false);
        $("#followers-list").hide();
        updateFollowButtons();
    });
    $("#following-button").on("click", function() {
        if ($(this).hasClass("active")){
            $(this).removeClass("active");
            $(this).addClass("disabled").prop('disabled', true);
            $("#followers-button").removeClass("disabled");
            $("#followers-button").addClass("active").prop('disabled', false);
            $("#followers-list").hide();
            $("#following-list").show();
            updateFollowButtons()
        }
        else {
            $(this).removeClass("disabled");
            $(this).addClass("active").prop('disabled', false);
            $("#followers-button").removeClass("active");
            $("#followers-button").addClass("disabled").prop('disabled', true);
            updateFollowButtons()
        }
    })

    $("#followers-button").on("click", function() {
        if ($(this).hasClass("active")){
            $(this).removeClass("active");
            $(this).addClass("disabled").prop('disabled', true);
            $("#following-button").removeClass("disabled");
            $("#following-button").addClass("active").prop('disabled', false);
            $("#following-list").hide();
            $("#followers-list").show();
            updateFollowButtons();
        }
        else {
            $(this).removeClass("disabled");
            $(this).addClass("active").prop('disabled', false);
            $("#following-button").removeClass("active");
            $("#following-button").addClass("disabled").prop('disabled', true);
            updateFollowButtons();
        }
    })

    $.getJSON("/get_minifeed", function(data){
        var all_posts = data['all_posts'];
        var count = 0;
        for (post in all_posts) {
            $("#mini-feed").append("

posted by: " + all_posts[post].user + "

" + all_posts[post].content + "

");
            count += 1;
        }
    });

    $(document).ajaxStart(function(){
        $('#loading').html('')
    });
    $(document).ajaxComplete(function(){
        $("#loading").empty();
    });

    //// Syncs the users playlist,
    $("#sync").on("click", function(event){
        event.preventDefault();
        $.getJSON("/seed", function(data){
            if (data['status'] === "redirect") {
                $(location).attr('href', '/')
            }
            else {
                update_user_profile()
            }
        })
    })
    $("#mini-feed, #user-search-results, #following-list, #followers-list").on('click', 'button.followButton', function(event){
        event.preventDefault();
        $button = $(this);
        var id = $(this).closest('tr').attr('id')
        if($button.hasClass('following')){

            $.post("/unfollow", {"id": id}, function(data){
                $button.removeClass('following');
                $button.removeClass('unfollow');
                $button.text('Follow');
                if ($button.closest('table').attr('id') === "following-list"){
                    $button.closest('tr').remove();
                    $("#followers-list").find("#" + id).find("button").removeClass("following").text("Follow");
                }
                else {
                    $("#following-list").find("#" + id).remove();
                }
                update_user_profile();
            })
        } else {
                $.post("/follow", {'id': id}, function(data) {
                var following = data["following"]
                $button.addClass('following');
                $button.text('Following');
                $("#following-list").append("
" + following + "	<button class='btn followButton following'>Following</button>
")
                update_user_profile();
                });
            }
        });
    $("#mini-feed, #user-search-results, #following-list, #followers-list").on("mouseenter", "button.followButton", function(){
        console.log("got it")
        $button = $(this)
        if($button.hasClass('following')) {
            $button.addClass('unfollow');
            $button.text('Unfollow');
        }
    });

    $("#mini-feed, #user-search-results, #following-list, #followers-list").on("mouseleave", "button.followButton", function(){
        if($button.hasClass('following')) {
            $button.removeClass('unfollow');
            $button.text('Following');
        }
    });

    $("#createpost").on("click", function() {
        $("#searchresult_list").empty();
    })
    $("#song-search-button").on("click", function(event) {
        event.preventDefault();
        $("#searchresult_list").empty();
        var query = $("[name=search_query]").val();
        $.getJSON("search", {"search_query": query}, function(data){
            var count = 0;
            for (item in data["search_result"]) {
                $("#searchresult_list").append("
" +data["search_result"][item] + "<button class=select-song>select</button>
");
                count += 1;
            }
        })
    })
    $("#searchresult_list").on("click", ".select-song", function() {
        var text = $(this).parent().text();
        var substring = text.substring(0, text.length - 6);
        $("#searchresult_list").empty();
        $("#song-search-button").hide();
        $("[name=search_query]").val(substring);
    })
    $("#songsearch").on("submit", function(event) {
        event.preventDefault();
        var track_name = $("[name=search_query]").val()
        $.getJSON("track_uri", {"track_name": track_name}, function(data){
            var track_uri = data['track_uri'];
        $("#song-reference").empty().append("");
        $("#song-comment").append($("#comment").val());
        });
    })
    $("#post-it-button").on("click", function() {
        var comment = $("#song-comment").text();
        var src = $("#song-reference")[0].firstChild.src;
        var track_uri = src.replace("https://embed.spotify.com/?uri=","")
        $.post("create_post", {"comment": comment, "track_uri": track_uri}, function(data) {
        })
        $("#comment").val('');
        $("[name=search_query]").val('');
    })
    $("#usersearch").on("submit", function(event) {
        event.preventDefault();
        $("#user-search-results").empty();
        var usernameQuery = $("[name=user_query]").val();
        $.getJSON("find_user", {"usernameQuery": usernameQuery}, function(data){
            var userList = data["search_result"];
            console.log(userList)
            if (userList === "No results found..." || userList === "Please input a username.") {
                $("#user-search-results").append("
" + userList + "
")
            }
            else {
                var $searchdiv = $("#user-search");
                //

                for (var i in userList) {
                    $("#user-search-results").append("
" + userList[i] + "	<button class='btn followButton'>Follow</button>
");
                }
            }
        });
        updateFollowButtons();
    });

    /////// Generates playlists ///////
        /// Sets the initial size of the playlist div
    $(".timelineplaylist").css("height","20em")

    var num_songs = parseInt($("#profile_songs").text())

    if (num_songs > 0) {
        /// Personal ///
        var number_songs = 10
        var follow = 0
        var recency_effect = 10
        var rating_effect = 10
        var duplicate_artist = 10
        var existing_playlist = 10
        var post_data = {"type":"your_home","number_songs":number_songs, "follow":follow, "recency_effect":recency_effect,"rating_effect":rating_effect,"duplicate_artist":duplicate_artist, "existing_playlist": existing_playlist}

        $.post('playlist', post_data, function(data) {
                var your_uris = data['track_uris']
                var cover_art = data['cover_art']
                $("#yourplaylist_image").append('')
                $("#yourplaylist_image").css("width","80%")
                $("#yourplaylist_image_image").css("width","100%");

                /// setting the position of the buttons
                var div_width = $("#yourplaylist").width()
                var imageheight =  $("#yourplaylist_image").width();
                var buttons_width = $("#yourplaylist_buttons").width()
                var buttons_centre = (div_width - imageheight) + 0.5 * imageheight - 0.5 * buttons_width
                $("#yourplaylist_buttons").css({"top":imageheight,"left":buttons_centre})

                ///Resets the size of the playlist div according to the size of the image ///
                $(".timelineplaylist").css("height",imageheight*1.2)

                var string = ""
                for (var song in your_uris) {
                    string += your_uris[song] + ','
                }
                string = string.substring(0, string.length - 1);
                $('#yourplaylist_playlist').css("width","80%");
                var pw = $('#yourplaylist_playlist').width();
                $('#yourplaylist_playlist').css({'height':pw+'px'});

                $('#yourplaylist_playlist').html('')

                /// When the user clicks on the image of the playlist ///
                $('#yourplaylist img').on('click', function() {
                    $(this).fadeOut(2000)
                    $("#yourplaylist_buttons").css("display","inline")
                    /// Bind the like save and dislike events to the button
                    $("#yourplaylist_like").on("click", function() {
                        $.post("rating", {"uris": your_uris, "decision": "like"}, function(data){
                        })
                    })
                    $("#yourplaylist_dislike").on("click", function() {
                        $.post("rating", {"uris": your_uris, "decision": "dislike"}, function(data){
                        })
                    })
                    $("#yourplaylist_save").on("click", function() {
                        $.post("saveplaylist", {"uris": your_uris}, function(data){
                        })
                    })
                })

            })
        }

    /// Friends ///
    var num_following = parseInt($("#profile_following").text())

    if (num_following > 0) {
        var number_songs2 = 10
        var follow2 = 10
        var recency_effect2 = 10
        var rating_effect2 = 10
        var duplicate_artist2 = 5
        var existing_playlist2 = 9
        var post_data2 = {"type":"your_friends","number_songs":number_songs2, "follow":follow2, "recency_effect":recency_effect2,"rating_effect":rating_effect2,"duplicate_artist":duplicate_artist2, "existing_playlist": existing_playlist2}

        $.post('playlist', post_data2, function(data) {
                var friends_uris = data['track_uris']
                var cover_art = data['cover_art']
                $("#friendsplaylist_image").html('');
                $("#friendsplaylist_image").css("width","80%");
                $("#friendsplaylist_image_image").css("width","100%");

                /// setting the position of the buttons
                var div_width = $("#friendsplaylist").width()
                var imageheight =  $("#friendsplaylist_image").width();
                var buttons_width = $("#friendsplaylist_buttons").width()
                var buttons_centre = (div_width - imageheight) + 0.5 * imageheight - 0.5 * buttons_width
                $("#friendsplaylist_buttons").css({"top":imageheight,"left":buttons_centre})

                var string = ""
                for (var song in friends_uris) {
                    string += friends_uris[song] + ','
                }
                string = string.substring(0, string.length - 1);
                $('#friendsplaylist_playlist').css("width","80%");
                var pw = $('#friendsplaylist_playlist').width();
                $('#friendsplaylist_playlist').css({'height':pw+'px'});
                $('#friendsplaylist_playlist').html('')

                /// When the user clicks on the image of the playlist ///
                $('#friendsplaylist img').on('click', function() {
                    $(this).fadeOut(2000)
                    $("#friendsplaylist_buttons").css("display","inline")
                    /// Bind the like save and dislike events to the button
                    $("#friendsplaylist_like").on("click", function() {
                        $.post("rating", {"uris": friends_uris, "decision": "like"}, function(data){
                        })
                    })
                    $("#friendsplaylist_dislike").on("click", function() {
                        $.post("rating", {"uris": friends_uris, "decision": "dislike"}, function(data){
                        })
                    })
                    $("#friendsplaylist_save").on("click", function() {
                        $.post("saveplaylist", {"uris": friends_uris}, function(data){
                        })
                    })

                })

            })

    }

$('#testpopover').popover({
    html:true,
    title:"header",
    content: function() {
        return ''
    }

})

})

<|endoftext|>


<commit_before>var common = require('../common-tap.js')
var test = require('tap').test
var npm = require('../../lib/npm.js')
var mkdirp = require('mkdirp')
var rimraf = require('rimraf')
var path = require('path')
var mr = require('npm-registry-mock')

var osenv = require('osenv')

var requireInject = require('require-inject')

var PKG_DIR = path.resolve(__dirname, 'update-examples')
var CACHE_DIR = path.resolve(PKG_DIR, 'cache')

// ** constant templates for mocks **
var DEFAULT_PKG = {
  'name': 'update-examples',
  'version': '1.2.3',
  'dependencies': {
    'dep1': '*'
  }
}

var DEP_PKG = {
  name: 'dep1',
  version: '1.1.1',
  _from: '^1.1.1'
}

var INSTALLED = {
  dependencies: {
    'dep1': '1.1.1'
  }
}

var DEP1_REGISTRY = { name: 'dep1',
  'dist-tags': { latest: '1.2.2' },
  versions: {
    '1.2.2': { version: '1.2.2' },
    '1.2.1': { version: '1.2.1' },
    '1.2.0': { version: '1.2.0' },
    '1.1.2': { version: '1.1.2' },
    '1.1.1': { version: '1.1.1' },
    '1.0.0': { version: '1.0.0' },
    '0.4.1': { version: '0.4.1' },
    '0.4.0': { version: '0.4.0' },
    '0.2.0': { version: '0.2.0' }
  }
}

var registryMocks = {
  'get': {
    '/dep1': [200, DEP1_REGISTRY]
  }
}

// ** dynamic mocks, cloned from templates and modified **
var mockServer
var mockDepJson = clone(DEP_PKG)
var mockInstalled = clone(INSTALLED)
var mockParentJson = clone(DEFAULT_PKG)

// target
var installAskedFor

function clone (a) {
  return extend({}, a)
}

function extend (a, b) {
  for (var key in b) {
    a[key] = b[key]
  }
  return a
}

function resetPackage (options) {
  rimraf.sync(CACHE_DIR)
  mkdirp.sync(CACHE_DIR)

  installAskedFor = undefined

  mockParentJson = clone(DEFAULT_PKG)
  mockInstalled = clone(INSTALLED)
  mockDepJson = clone(DEP_PKG)

  if (options.wanted) {
    mockParentJson.dependencies.dep1 = options.wanted
    mockDepJson._from = options.wanted
  }

  if (options.installed) {
    mockInstalled.dependencies.dep1 = options.installed
    mockDepJson.version = options.installed
  }
}

function mockReadInstalled (dir, opts, cb) {
  cb(null, mockInstalled)
}

function mockReadJson (file, cb) {
  cb(null, file.match(/dep1/) ? mockDepJson : mockParentJson)
}

function mockCommand (npm, name, fn) {
  delete npm.commands[name]
  npm.commands[name] = fn
}

test('setup', function (t) {
  process.chdir(osenv.tmpdir())
  mkdirp.sync(PKG_DIR)
  process.chdir(PKG_DIR)

  resetPackage({})

  mr({ port: common.port, mocks: registryMocks }, function (er, server) {
    npm.load({ cache: CACHE_DIR,
      registry: common.registry,
    cwd: PKG_DIR }, function (err) {
        t.ifError(err, 'started server')
        mockServer = server

        mockCommand(npm, 'install', function mockInstall (where, what, cb) {
          installAskedFor = what
          cb(null)
        })

        mockCommand(npm, 'outdated', requireInject('../../lib/outdated', {
          'read-installed': mockReadInstalled,
          'read-package-json': mockReadJson
        }))

        t.end()
      })
  })
})

test('update caret dependency to latest', function (t) {
  resetPackage({ wanted: '^1.1.1' })

  npm.commands.update([], function (err) {
    t.ifError(err)
    t.equal('dep1@1.2.2', installAskedFor, 'should want to install dep@1.2.2')
    t.end()
  })
})

test('update tilde dependency to latest', function (t) {
  resetPackage({ wanted: '~1.1.1' })

  npm.commands.update([], function (err) {
    t.ifError(err)
    t.equal('dep1@1.1.2', installAskedFor, 'should want to install dep@1.1.2')
    t.end()
  })
})

test('hold tilde dependency at wanted (#6441)', function (t) {
  resetPackage({ wanted: '~1.1.2', installed: '1.1.2' })

  npm.commands.update([], function (err) {
    t.ifError(err)
    t.notOk(installAskedFor, 'should not want to install anything')
    t.end()
  })
})

test('update old caret dependency with no newer', function (t) {
  resetPackage({ wanted: '^0.2.0', installed: '^0.2.0' })

  npm.commands.update([], function (err) {
    t.ifError(err)
    t.equal('dep1@0.2.0', installAskedFor, 'should want to install dep@0.2.0')
    t.end()
  })
})

test('update old caret dependency with newer', function (t) {
  resetPackage({ wanted: '^0.4.0', installed: '^0.4.0' })

  npm.commands.update([], function (err) {
    t.ifError(err)
    t.equal('dep1@0.4.1', installAskedFor, 'should want to install dep@0.4.1')
    t.end()
  })
})

test('cleanup', function (t) {
  mockServer.close()

  process.chdir(osenv.tmpdir())
  rimraf.sync(PKG_DIR)

  t.end()
})
<commit_msg>test: update-examples: add assertions to setup to track its completion<commit_after>var common = require('../common-tap.js')
var test = require('tap').test
var npm = require('../../lib/npm.js')
var mkdirp = require('mkdirp')
var rimraf = require('rimraf')
var path = require('path')
var mr = require('npm-registry-mock')

var osenv = require('osenv')

var requireInject = require('require-inject')

var PKG_DIR = path.resolve(__dirname, 'update-examples')
var CACHE_DIR = path.resolve(PKG_DIR, 'cache')

// ** constant templates for mocks **
var DEFAULT_PKG = {
  'name': 'update-examples',
  'version': '1.2.3',
  'dependencies': {
    'dep1': '*'
  }
}

var DEP_PKG = {
  name: 'dep1',
  version: '1.1.1',
  _from: '^1.1.1'
}

var INSTALLED = {
  dependencies: {
    'dep1': '1.1.1'
  }
}

var DEP1_REGISTRY = { name: 'dep1',
  'dist-tags': { latest: '1.2.2' },
  versions: {
    '1.2.2': { version: '1.2.2' },
    '1.2.1': { version: '1.2.1' },
    '1.2.0': { version: '1.2.0' },
    '1.1.2': { version: '1.1.2' },
    '1.1.1': { version: '1.1.1' },
    '1.0.0': { version: '1.0.0' },
    '0.4.1': { version: '0.4.1' },
    '0.4.0': { version: '0.4.0' },
    '0.2.0': { version: '0.2.0' }
  }
}

var registryMocks = {
  'get': {
    '/dep1': [200, DEP1_REGISTRY]
  }
}

// ** dynamic mocks, cloned from templates and modified **
var mockServer
var mockDepJson = clone(DEP_PKG)
var mockInstalled = clone(INSTALLED)
var mockParentJson = clone(DEFAULT_PKG)

// target
var installAskedFor

function clone (a) {
  return extend({}, a)
}

function extend (a, b) {
  for (var key in b) {
    a[key] = b[key]
  }
  return a
}

function resetPackage (options) {
  rimraf.sync(CACHE_DIR)
  mkdirp.sync(CACHE_DIR)

  installAskedFor = undefined

  mockParentJson = clone(DEFAULT_PKG)
  mockInstalled = clone(INSTALLED)
  mockDepJson = clone(DEP_PKG)

  if (options.wanted) {
    mockParentJson.dependencies.dep1 = options.wanted
    mockDepJson._from = options.wanted
  }

  if (options.installed) {
    mockInstalled.dependencies.dep1 = options.installed
    mockDepJson.version = options.installed
  }
}

function mockReadInstalled (dir, opts, cb) {
  cb(null, mockInstalled)
}

function mockReadJson (file, cb) {
  cb(null, file.match(/dep1/) ? mockDepJson : mockParentJson)
}

function mockCommand (npm, name, fn) {
  delete npm.commands[name]
  npm.commands[name] = fn
}

test('setup', function (t) {
  t.plan(5)
  process.chdir(osenv.tmpdir())
  mkdirp.sync(PKG_DIR)
  process.chdir(PKG_DIR)
  t.pass('made '+PKG_DIR)

  resetPackage({})

  mr({ port: common.port, mocks: registryMocks }, function (er, server) {
    t.pass('mock registry active')
    npm.load({ cache: CACHE_DIR,
      registry: common.registry,
    cwd: PKG_DIR }, function (err) {
        t.ifError(err, 'started server')
        mockServer = server

        t.pass('npm.load complete')

        mockCommand(npm, 'install', function mockInstall (where, what, cb) {
          installAskedFor = what
          cb(null)
        })

        mockCommand(npm, 'outdated', requireInject('../../lib/outdated', {
          'read-installed': mockReadInstalled,
          'read-package-json': mockReadJson
        }))

        t.pass('mocks configured')
        t.end()
      })
  })
})

test('update caret dependency to latest', function (t) {
  resetPackage({ wanted: '^1.1.1' })

  npm.commands.update([], function (err) {
    t.ifError(err)
    t.equal('dep1@1.2.2', installAskedFor, 'should want to install dep@1.2.2')
    t.end()
  })
})

test('update tilde dependency to latest', function (t) {
  resetPackage({ wanted: '~1.1.1' })

  npm.commands.update([], function (err) {
    t.ifError(err)
    t.equal('dep1@1.1.2', installAskedFor, 'should want to install dep@1.1.2')
    t.end()
  })
})

test('hold tilde dependency at wanted (#6441)', function (t) {
  resetPackage({ wanted: '~1.1.2', installed: '1.1.2' })

  npm.commands.update([], function (err) {
    t.ifError(err)
    t.notOk(installAskedFor, 'should not want to install anything')
    t.end()
  })
})

test('update old caret dependency with no newer', function (t) {
  resetPackage({ wanted: '^0.2.0', installed: '^0.2.0' })

  npm.commands.update([], function (err) {
    t.ifError(err)
    t.equal('dep1@0.2.0', installAskedFor, 'should want to install dep@0.2.0')
    t.end()
  })
})

test('update old caret dependency with newer', function (t) {
  resetPackage({ wanted: '^0.4.0', installed: '^0.4.0' })

  npm.commands.update([], function (err) {
    t.ifError(err)
    t.equal('dep1@0.4.1', installAskedFor, 'should want to install dep@0.4.1')
    t.end()
  })
})

test('cleanup', function (t) {
  mockServer.close()

  process.chdir(osenv.tmpdir())
  rimraf.sync(PKG_DIR)

  t.end()
})
<|endoftext|>


<commit_before>var Game = function() {
  this.lives = 5;
  this.score = 0;
  this.speed = 1;
  this.words = [];
  this.activeWords = [];
  this.correctWords = [];
  this.startTime = 0;
  this.keystrokes = 0;
  this.textVisible = false;
};

Game.prototype.playSplash = function() {
  var audioSplash = document.createElement("audio");
  audioSplash.src = "/splash1.mp3";
  audioSplash.play();
};

Game.prototype.playPlop = function() {
  var audioPlop = document.createElement("audio");
  audioPlop.src = "/plop.mp3";
  audioPlop.play();
};

Game.prototype.playOver = function() {
  var audioOver = document.createElement("audio");
  audioOver.src = "/gameover.mp3";
  audioOver.play();
};

Game.prototype.getWords = function() {
  return $.ajax({
    url: "/game-words",
    method: "get",
    data: {level: localStorage.level}
  }).done(function(response) {
    this.words = response;
  }.bind(this));
};

Game.prototype.drawStart = function() {
  this.ctx.beginPath();
  this.ctx.rect(175, 275, 150, 60);
  this.ctx.strokeStyle = "#0095DD";
  this.ctx.stroke();
  this.ctx.closePath();
  this.ctx.font = "30px Arial";
  this.ctx.fillStyle = "#0095DD";
  this.ctx.fillText("START", 200, 315);

  this.startGame();
};

Game.prototype.startGame = function() {
  this.canvas.addEventListener('click', function(event) {
    var x = event.pageX - this.canvas.offsetLeft;
    var y = event.pageY - this.canvas.offsetTop;

    if (y > 275 && y < 335 && x > 175 && x < 325) {
      this.input.focus();
      this.addWord();
      this.startTime = new Date().getTime();
      setInterval(this.addWord.bind(this), 1000);
      this.drawGame();
    }
  }.bind(this));
};

Game.prototype.drawGame = function() {
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  this.drawLives();
  this.drawScore();

  if (this.lives === 0) {
    this.drawGameOver();
    this.drawRestart();
  } else if (this.activeWords.length === 0 && this.words.length === 0) {
    this.drawWin();
    this.drawContinue();
    this.drawRestart();
  } else {
    this.drawWord();
    this.collisionTest();

    for (var i in this.activeWords) this.activeWords[i].y += this.speed;

    requestAnimationFrame(this.drawGame.bind(this));
  }
};

Game.prototype.collisionTest = function() {
  for (var i in this.activeWords) {
    if (this.activeWords[i].y > this.canvas.height - 110) {
      this.playSplash();
      this.activeWords.splice(i, 1);
      this.lives -= 1;
    }
  }
};

Game.prototype.checkSpelling = function() {
  var inputValue = $("#inputText").val();
  for (var i in this.activeWords) {
    if (this.activeWords[i].text === inputValue.trim()) {
      this.playPlop();
      this.score += this.activeWords[i].points;
      var correctWord = this.activeWords.splice(i, 1)[0];
      this.correctWords.push(correctWord);
      return $("#inputText").val("");
    }
  }
};

Game.prototype.addWord = function() {
  if (this.words.length > 0) {
    var word = this.words.shift();
    this.activeWords.push(word);
  }
};

Game.prototype.drawLives = function() {
  this.ctx.font = "20px Arial";
  this.ctx.fillStyle = "black";
  this.ctx.fillText("LIVES: " + this.lives, 400, 570);
};

Game.prototype.drawScore = function() {
  this.ctx.font = "20px Arial";
  this.ctx.fillStyle = "black";
  this.ctx.fillText("SCORE: " + this.score, 20, 570);
};

Game.prototype.drawWord = function() {
  this.ctx.font = "20px Arial";
  this.ctx.fillStyle = "#0095DD";
  var words = this.activeWords;
  for (var i in words) {
    this.ctx.fillText(words[i].text, words[i].x, words[i].y);
  }
};

Game.prototype.drawWin = function() {
  this.ctx.font = "30px Arial";
  this.ctx.fillStyle = "#0095DD";
  this.activeWords = [];
  this.ctx.fillText("LEVEL COMPLETE", 120, 200);
  this.saveGame();
};

Game.prototype.drawGameOver = function() {
  var that = this;
  this.ctx.font = "30px 'Press Start 2P'";
  this.ctx.fillStyle = "#0095DD";
  this.activeWords = [];
  this.interval = setInterval(function() { that.doBlink(); }, 800);
  // this.playOver();
  this.saveGame();
};

Game.prototype.doBlink = function() {
  if(this.textVisible) {
    this.ctx.clearRect(0,0, 500, 300);
    this.textVisible = false;
  }
  else {
    this.ctx.font = "30px 'Press Start 2P'";
    this.ctx.fillText("GAME OVER", 115, 299);
    this.textVisible = true;
  }
};

Game.prototype.drawRestart = function() {
  this.ctx.beginPath();
  this.ctx.rect(200, 325, 100, 40);
  this.ctx.strokeStyle = "#0095DD";
  this.ctx.stroke();
  this.ctx.closePath();
  this.ctx.font = "16px Arial";
  this.ctx.fillStyle = "#0095DD";
  this.ctx.fillText("RESTART", 214, 350);

  this.canvas.addEventListener('click', function(event) {
    var x = event.pageX - this.canvas.offsetLeft;
    var y = event.pageY - this.canvas.offsetTop;

    if (y > 325 && y < 365 && x > 200 && x < 300) {
      // create new game instead of reloading everything
      document.location.reload();
    }
  }.bind(this));
};

Game.prototype.drawContinue = function() {
  this.ctx.beginPath();
  this.ctx.rect(200, 250, 100, 40);
  this.ctx.strokeStyle = "#0095DD";
  this.ctx.stroke();
  this.ctx.closePath();
  this.ctx.font = "16px Arial";
  this.ctx.fillStyle = "#0095DD";
  this.ctx.fillText("CONTINUE", 208, 275);

  this.nextLevel();
};

Game.prototype.nextLevel = function() {
  this.canvas.addEventListener('click', function(event) {
    var x = event.pageX - this.canvas.offsetLeft;
    var y = event.pageY - this.canvas.offsetTop;
    if (y > 250 && y < 290 && x > 200 && x < 300) {
      if (parseInt(localStorage.level) < 6) {
        localStorage.setItem("level", parseInt(localStorage.level) + 1);
      }
      document.location.reload();
    }
  }.bind(this));
};

Game.prototype.saveGame = function() {
  var endTime = new Date().getTime();
  var totalTime = (endTime - this.startTime) / 60000;
  var totalLetters = this.correctWords.reduce(function(total, word) {
    return total + word.text.length;
  }, 0);
  var normalizeWords = totalLetters / 5;
  var wpm = normalizeWords / totalTime;
  var accuracy = (totalLetters / this.keystrokes) * 100;
  var score = this.score;
  var level = localStorage.level;

  if (wpm > 0) {
    $.ajax({
      url: "/games",
      method: "post",
      data: {game: {score: score, wpm: wpm, accuracy: accuracy, time: totalTime, level: level}}
    });
  }
};
<commit_msg>Update game.js<commit_after>var Game = function() {
  this.lives = 5;
  this.score = 0;
  this.speed = 1;
  this.words = [];
  this.activeWords = [];
  this.correctWords = [];
  this.startTime = 0;
  this.keystrokes = 0;
  this.textVisible = false;
};

Game.prototype.playSplash = function() {
  var audioSplash = document.createElement("audio");
  audioSplash.src = "/splash1.mp3";
  audioSplash.play();
};

Game.prototype.playPlop = function() {
  var audioPlop = document.createElement("audio");
  audioPlop.src = "/plop.mp3";
  audioPlop.play();
};

Game.prototype.playOver = function() {
  var audioOver = document.createElement("audio");
  audioOver.src = "/gameover.mp3";
  audioOver.play();
};

Game.prototype.getWords = function() {
  return $.ajax({
    url: "/game-words",
    method: "get",
    data: {level: localStorage.level}
  }).done(function(response) {
    this.words = response;
  }.bind(this));
};

Game.prototype.drawStart = function() {
  this.ctx.beginPath();
  this.ctx.rect(175, 275, 150, 60);
  this.ctx.strokeStyle = "#0095DD";
  this.ctx.stroke();
  this.ctx.closePath();
  this.ctx.font = "30px Arial";
  this.ctx.fillStyle = "#0095DD";
  this.ctx.fillText("START", 200, 315);

  this.startGame();
};

Game.prototype.startGame = function() {
  this.canvas.addEventListener('click', function(event) {
    var x = event.pageX - this.canvas.offsetLeft;
    var y = event.pageY - this.canvas.offsetTop;

    if (y > 275 && y < 335 && x > 175 && x < 325) {
      this.input.focus();
      this.addWord();
      this.startTime = new Date().getTime();
      setInterval(this.addWord.bind(this), 1000);
      this.drawGame();
    }
  }.bind(this));
};

Game.prototype.drawGame = function() {
  this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  this.drawLives();
  this.drawScore();

  if (this.lives === 0) {
    this.drawGameOver();
    this.drawRestart();
  } else if (this.activeWords.length === 0 && this.words.length === 0) {
    this.drawWin();
    this.drawContinue();
    this.drawRestart();
  } else {
    this.drawWord();
    this.collisionTest();

    for (var i in this.activeWords) this.activeWords[i].y += this.speed;

    requestAnimationFrame(this.drawGame.bind(this));
  }
};

Game.prototype.collisionTest = function() {
  for (var i in this.activeWords) {
    if (this.activeWords[i].y > this.canvas.height - 110) {
      this.playSplash();
      this.activeWords.splice(i, 1);
      this.lives -= 1;
    }
  }
};

Game.prototype.checkSpelling = function() {
  var inputValue = $("#inputText").val();
  for (var i in this.activeWords) {
    if (this.activeWords[i].text === inputValue.trim()) {
      this.playPlop();
      this.score += this.activeWords[i].points;
      var correctWord = this.activeWords.splice(i, 1)[0];
      this.correctWords.push(correctWord);
      return $("#inputText").val("");
    }
  }
};

Game.prototype.addWord = function() {
  if (this.words.length > 0) {
    var word = this.words.shift();
    this.activeWords.push(word);
  }
};

Game.prototype.drawLives = function() {
  this.ctx.font = "20px Arial";
  this.ctx.fillStyle = "black";
  this.ctx.fillText("LIVES: " + this.lives, 400, 570);
};

Game.prototype.drawScore = function() {
  this.ctx.font = "20px Arial";
  this.ctx.fillStyle = "black";
  this.ctx.fillText("SCORE: " + this.score, 20, 570);
};

Game.prototype.drawWord = function() {
  this.ctx.font = "20px Arial";
  this.ctx.fillStyle = "#0095DD";
  var words = this.activeWords;
  for (var i in words) {
    this.ctx.fillText(words[i].text, words[i].x, words[i].y);
  }
};

Game.prototype.drawWin = function() {
  this.ctx.font = "30px Arial";
  this.ctx.fillStyle = "#0095DD";
  this.activeWords = [];
  this.ctx.fillText("LEVEL COMPLETE", 120, 200);
  this.saveGame();
};

Game.prototype.drawGameOver = function() {
  var that = this;
  this.ctx.font = "30px 'Press Start 2P'";
  this.ctx.fillStyle = "#0095DD";
  this.activeWords = [];
  this.interval = setInterval(function() { that.doBlink(); }, 800);

  // Need to pick a better gameover sound
  // this.playOver();
  this.saveGame();
};

Game.prototype.doBlink = function() {
  if(this.textVisible) {
    this.ctx.clearRect(0,0, 500, 300);
    this.textVisible = false;
  }
  else {
    this.ctx.font = "30px 'Press Start 2P'";
    this.ctx.fillText("GAME OVER", 115, 299);
    this.textVisible = true;
  }
};

Game.prototype.drawRestart = function() {
  this.ctx.beginPath();
  this.ctx.rect(200, 325, 100, 40);
  this.ctx.strokeStyle = "#0095DD";
  this.ctx.stroke();
  this.ctx.closePath();
  this.ctx.font = "16px Arial";
  this.ctx.fillStyle = "#0095DD";
  this.ctx.fillText("RESTART", 214, 350);

  this.canvas.addEventListener('click', function(event) {
    var x = event.pageX - this.canvas.offsetLeft;
    var y = event.pageY - this.canvas.offsetTop;

    if (y > 325 && y < 365 && x > 200 && x < 300) {
      // create new game instead of reloading everything
      document.location.reload();
    }
  }.bind(this));
};

Game.prototype.drawContinue = function() {
  this.ctx.beginPath();
  this.ctx.rect(200, 250, 100, 40);
  this.ctx.strokeStyle = "#0095DD";
  this.ctx.stroke();
  this.ctx.closePath();
  this.ctx.font = "16px Arial";
  this.ctx.fillStyle = "#0095DD";
  this.ctx.fillText("CONTINUE", 208, 275);

  this.nextLevel();
};

Game.prototype.nextLevel = function() {
  this.canvas.addEventListener('click', function(event) {
    var x = event.pageX - this.canvas.offsetLeft;
    var y = event.pageY - this.canvas.offsetTop;
    if (y > 250 && y < 290 && x > 200 && x < 300) {
      if (parseInt(localStorage.level) < 6) {
        localStorage.setItem("level", parseInt(localStorage.level) + 1);
      }
      document.location.reload();
    }
  }.bind(this));
};

Game.prototype.saveGame = function() {
  var endTime = new Date().getTime();
  var totalTime = (endTime - this.startTime) / 60000;
  var totalLetters = this.correctWords.reduce(function(total, word) {
    return total + word.text.length;
  }, 0);
  var normalizeWords = totalLetters / 5;
  var wpm = normalizeWords / totalTime;
  var accuracy = (totalLetters / this.keystrokes) * 100;
  var score = this.score;
  var level = localStorage.level;

  if (wpm > 0) {
    $.ajax({
      url: "/games",
      method: "post",
      data: {game: {score: score, wpm: wpm, accuracy: accuracy, time: totalTime, level: level}}
    });
  }
};
<|endoftext|>