<commit_before>
/**
 * Module dependencies.
 */

var EventEmitter = require('events').EventEmitter
  , debug = require('debug')('engine.socket')

/**
 * Module exports.
 */

module.exports = Socket;

/**
 * Client class (abstract).
 *
 * @api private
 */

function Socket (id, server, transport) {
  this.id = id;
  this.server = server;
  this.upgraded = false;
  this.readyState = 'opening';

  // keep track of request that originated the transport
  this.req = transport.req;
  this.setTransport(transport);
  this.onOpen();
}

/**
 * Inherits from EventEmitter.
 */

Socket.prototype.__proto__ = EventEmitter.prototype;

/**
 * Called upon transport considered open.
 *
 * @api private
 */

Socket.prototype.onOpen = function () {
  this.readyState = 'open';

  // sends an `open` packet
  this.transport.send({
      type: 'open'
    , data: JSON.stringify({
          sid: this.id
        , upgrades: this.server.upgrades(this.transport.name)
        , pingTimeout: this.server.pingTimeout
        , pingInterval: this.server.pingInterval
      })
  });

  this.emit('open');
  this.ping();
};

/**
 * Called upon transport packet.
 *
 * @param {Object} packet
 * @api private
 */

Socket.prototype.onPacket = function (packet) {
  if ('open' == this.readyState) {
    switch (packet.type) {
      case 'close':
        this.onClose('server close');
        break;

      case 'pong':
        debug('got pong');
        this.emit('heartbeat');
        this.ping();
        break;

      case 'error':
        this.onClose('parse error');
        break;

      case 'message':
        this.emit('message', packet.data);
        break;
    }
  } else {
    debug('packet received with closed socket');
  }
};

Socket.prototype.onError = function (err) {
  debug('transport error');
  this.onClose('transport error', err);
};

/**
 * Pings a client.
 *
 * @api private
 */

Socket.prototype.ping = function () {
  clearTimeout(this.pingTimeoutTimer);

  var self = this;
  this.pingIntervalTimer = setTimeout(function () {
    debug('writing ping packet - expected pong in %sms', self.server.pingTimeout);
    self.transport.send({ type: 'ping' });
    self.pingTimeoutTimer = setTimeout(function () {
      self.onClose('ping timeout');
    }, self.server.pingTimeout);
  }, this.server.pingInterval);
};

/**
 * Attaches handlers for the given transport.
 *
 * @param {Transport} transport
 * @api private
 */

Socket.prototype.setTransport = function (transport) {
  this.transport = transport;
  this.transport.once('error', this.onError.bind(this));
  this.transport.on('packet', this.onPacket.bind(this));
  this.transport.once('close', this.onClose.bind(this, ['transport close']));
};

/**
 * Upgrades socket to the given transport
 *
 * @param {Transport} transport
 * @api private
 */

Socket.prototype.upgrade = function (transport) {
  // assert: !this.upgraded, 'we cant upgrade twice'
  this.upgraded = true;
  this.clearTransport();

  // the transport is already opened if we're upgrading to it
  // therefore we don't worry about the `open` event
  this.setTransport(transport);
  this.ping();
};

/**
 * Clears listeners and timers associated with current transport.
 *
 * @api private
 */

Socket.prototype.clearTransport = function () {
  clearTimeout(this.pingIntervalTimer);
  clearTimeout(this.pingTimeoutTimer);
};

/**
 * Called upon transport considered closed.
 * Possible reasons: `ping timeout`, `client error`, `parse error`,
 * `transport error`, `client close`, `server close`
 */

Socket.prototype.onClose = function (reason, description) {
  this.clearTransport();
  this.readyState = 'close';
  this.emit('close', reason, description);
};

/**
 * Sends a message packet.
 *
 * @param {String} message
 * @api public
 */

Socket.prototype.send = function (data) {
  this.transport.send({ type: 'message', data: data });
};

/**
 * Closes the socket and underlying transport.
 *
 * @return {Socket} for chaining
 * @api public
 */

Socket.prototype.close = function () {
  // we trigger the `close` event immediately and ignore
  // the transport one, but we still clean up connections
  this.onClose('server close');
  this.transport.close();
};
<commit_msg>Refactored rasons for close<commit_after>
/**
 * Module dependencies.
 */

var EventEmitter = require('events').EventEmitter
  , debug = require('debug')('engine.socket')

/**
 * Module exports.
 */

module.exports = Socket;

/**
 * Client class (abstract).
 *
 * @api private
 */

function Socket (id, server, transport) {
  this.id = id;
  this.server = server;
  this.upgraded = false;
  this.readyState = 'opening';

  // keep track of request that originated the transport
  this.req = transport.req;
  this.setTransport(transport);
  this.onOpen();
}

/**
 * Inherits from EventEmitter.
 */

Socket.prototype.__proto__ = EventEmitter.prototype;

/**
 * Called upon transport considered open.
 *
 * @api private
 */

Socket.prototype.onOpen = function () {
  this.readyState = 'open';

  // sends an `open` packet
  this.transport.send({
      type: 'open'
    , data: JSON.stringify({
          sid: this.id
        , upgrades: this.server.upgrades(this.transport.name)
        , pingTimeout: this.server.pingTimeout
        , pingInterval: this.server.pingInterval
      })
  });

  this.emit('open');
  this.ping();
};

/**
 * Called upon transport packet.
 *
 * @param {Object} packet
 * @api private
 */

Socket.prototype.onPacket = function (packet) {
  if ('open' == this.readyState) {
    switch (packet.type) {
      case 'close':
        this.onClose('server close');
        break;

      case 'pong':
        debug('got pong');
        this.emit('heartbeat');
        this.ping();
        break;

      case 'error':
        this.onClose('parse error');
        break;

      case 'message':
        this.emit('message', packet.data);
        break;
    }
  } else {
    debug('packet received with closed socket');
  }
};

Socket.prototype.onError = function (err) {
  debug('transport error');
  this.onClose('transport error', err);
};

/**
 * Pings a client.
 *
 * @api private
 */

Socket.prototype.ping = function () {
  clearTimeout(this.pingTimeoutTimer);

  var self = this;
  this.pingIntervalTimer = setTimeout(function () {
    debug('writing ping packet - expected pong in %sms', self.server.pingTimeout);
    self.transport.send({ type: 'ping' });
    self.pingTimeoutTimer = setTimeout(function () {
      self.onClose('ping timeout');
    }, self.server.pingTimeout);
  }, this.server.pingInterval);
};

/**
 * Attaches handlers for the given transport.
 *
 * @param {Transport} transport
 * @api private
 */

Socket.prototype.setTransport = function (transport) {
  this.transport = transport;
  this.transport.once('error', this.onError.bind(this));
  this.transport.on('packet', this.onPacket.bind(this));
  this.transport.once('close', this.onClose.bind(this, ['transport close']));
};

/**
 * Upgrades socket to the given transport
 *
 * @param {Transport} transport
 * @api private
 */

Socket.prototype.upgrade = function (transport) {
  // assert: !this.upgraded, 'we cant upgrade twice'
  this.upgraded = true;
  this.clearTransport();

  // the transport is already opened if we're upgrading to it
  // therefore we don't worry about the `open` event
  this.setTransport(transport);
  this.ping();
};

/**
 * Clears listeners and timers associated with current transport.
 *
 * @api private
 */

Socket.prototype.clearTransport = function () {
  clearTimeout(this.pingIntervalTimer);
  clearTimeout(this.pingTimeoutTimer);
};

/**
 * Called upon transport considered closed.
 * Possible reasons: `ping timeout`, `client error`, `parse error`,
 * `transport error`, `server close`, `transport close`
 */

Socket.prototype.onClose = function (reason, description) {
  this.clearTransport();
  this.readyState = 'close';
  this.emit('close', reason, description);
};

/**
 * Sends a message packet.
 *
 * @param {String} message
 * @api public
 */

Socket.prototype.send = function (data) {
  this.transport.send({ type: 'message', data: data });
};

/**
 * Closes the socket and underlying transport.
 *
 * @return {Socket} for chaining
 * @api public
 */

Socket.prototype.close = function () {
  // we trigger the `close` event immediately and ignore
  // the transport one, but we still clean up connections
  this.onClose('server close');
  this.transport.close();
};
<|endoftext|>


<commit_before>/**
 * An abstraction on top of WebSockets and XHR streaming to provide fastest
 * possible connection for peers.
 */
function Socket(server, id, key) {
  if (!(this instanceof Socket)) return new Socket(server, id, key);
  EventEmitter.call(this);

  this._id = id;
  this._server = server;
  this._httpUrl = 'http://' + this._server;
  this._key = key;
};

util.inherits(Socket, EventEmitter);

/** Check in with ID or get one from server. */
Socket.prototype._checkIn = function() {
  // If no ID provided, get a unique ID from server.
  var self = this;
  if (!this._id) {
    try {
      var http = new XMLHttpRequest();
      var url = this._httpUrl + '/id';
      // Set API key if necessary.
      if (!!this._key) {
        url += '/' + this._key;
      }

      // If there's no ID we need to wait for one before trying to init socket.
      http.open('get', url, true);
      http.onreadystatechange = function() {
        if (!self._id && http.readyState > 2 && !!http.responseText) {
          try {
            var response = JSON.parse(http.responseText.split('\n').shift());
            if (!!response.id) {
              self._id = response.id;
              self._startWebSocket();
              self.emit('message', { type: 'ID', id: self._id });
            }
          } catch (e) {
            self._startWebSocket();
          }
        }
        self._handleStream(http, true);
      };
      http.send(null);
    } catch(e) {
      util.log('XMLHttpRequest not available; defaulting to WebSockets');
      this._startWebSocket();
    }
  } else {
    this._startXhrStream();
    this._startWebSocket();
  }
};

/** Start up websocket communications. */
Socket.prototype._startWebSocket = function() {
  if (!!this._socket) {
    return;
  }

  var wsurl = 'ws://' + this._server + '/ws';
  if (!!this._id) {
    wsurl += '?id=' + this._id;
    if (!!this._key) {
      wsurl += '&key=' + this._key;
    }
  } else if (!!this._key) {
    wsurl += '?key=' + this._key;
  }
  this._socket = new WebSocket(wsurl);

  var self = this;
  this._socket.onmessage = function(event) {
    try {
      self.emit('message', JSON.parse(event.data));
    } catch(e) {
      util.log('Invalid server message', event.data);
    }
  };

  // Take care of the queue of connections if necessary and make sure Peer knows
  // socket is open.
  this._socket.onopen = function() {
    util.log('Socket open');
    if (self._id) {
      self.emit('open');
    }
  };
};

/** Start XHR streaming. */
Socket.prototype._startXhrStream = function() {
  try {
    var self = this;

    var http = new XMLHttpRequest();
    var url = this._httpUrl + '/id';
    // Set API key if necessary.
    if (!!this._key) {
      url += '/' + this._key;
    }
    http.open('post', url, true);
    http.setRequestHeader('Content-Type', 'application/json');
    http.onreadystatechange = function() {
      self._handleStream(http);
    };
    http.send(JSON.stringify({ id: this._id }));
  } catch(e) {
    util.log('XMLHttpRequest not available; defaulting to WebSockets');
  }
};

/** Handles onreadystatechange response as a stream. */
Socket.prototype._handleStream = function(http, pad) {
  // 3 and 4 are loading/done state. All others are not relevant.
  if (http.readyState < 3) {
    return;
  } else if (http.readyState == 3 && http.status != 200) {
    return;
  }

  if (this._index === undefined) {
    this._index = pad ? 2 : 1;
  }

  if (http.responseText === null) {
    return;
  }

  var message = http.responseText.split('\n')[this._index];
  if (!!message && http.readyState == 3) {
    this._index += 1;
    try {
      this._handleHTTPErrors(JSON.parse(message));
    } catch(e) {
      util.log('Invalid server message', message);
    }
  } else if (http.readyState == 4) {
    this._index = 1;
  }
};

Socket.prototype._handleHTTPErrors = function(message) {
  switch (message.type) {
    // XHR stream closed by timeout.
    case 'HTTP-END':
      util.log('XHR stream timed out.');
      if (!!this._socket && this._socket.readyState != 1) {
        this._startXhrStream();
      }
      break;
    // XHR stream closed by socket connect.
    case 'HTTP-SOCKET':
      util.log('XHR stream closed, WebSocket connected.');
      break;
    case 'HTTP-ERROR':
      this.emit('error', 'Something went wrong.');
      break;
    default:
      this.emit('message', message);
  }
};

/** Exposed send for DC & Peer. */
Socket.prototype.send = function(data) {
  var type = data.type;
  message = JSON.stringify(data);
  if (!type) {
    this.emit('error', 'Invalid message');
  }

  if (!!this._socket && this._socket.readyState == 1) {
    this._socket.send(message);
  } else {
    var self = this;
    var http = new XMLHttpRequest();
    var url = this._httpUrl + '/' + type.toLowerCase();
    // Set API key if necessary.
    if (!!this._key) {
      url += '/' + this._key;
    }

    http.open('post', url, true);
    http.setRequestHeader('Content-Type', 'application/json');
    http.onload = function() {
      // This happens if destination peer is not available...
      if (http.responseText != 'OK') {
        self.emit('unavailable', data.dst)
      }
    }
    http.send(message);
  }
};

Socket.prototype.close = function() {
  if (!!this._socket && this._socket.readyState == 1) {
    this._socket.close();
  }
};

Socket.prototype.start = function() {
  this._checkIn();
};
<commit_msg>remote unused onload<commit_after>/**
 * An abstraction on top of WebSockets and XHR streaming to provide fastest
 * possible connection for peers.
 */
function Socket(server, id, key) {
  if (!(this instanceof Socket)) return new Socket(server, id, key);
  EventEmitter.call(this);

  this._id = id;
  this._server = server;
  this._httpUrl = 'http://' + this._server;
  this._key = key;
};

util.inherits(Socket, EventEmitter);

/** Check in with ID or get one from server. */
Socket.prototype._checkIn = function() {
  // If no ID provided, get a unique ID from server.
  var self = this;
  if (!this._id) {
    try {
      var http = new XMLHttpRequest();
      var url = this._httpUrl + '/id';
      // Set API key if necessary.
      if (!!this._key) {
        url += '/' + this._key;
      }

      // If there's no ID we need to wait for one before trying to init socket.
      http.open('get', url, true);
      http.onreadystatechange = function() {
        if (!self._id && http.readyState > 2 && !!http.responseText) {
          try {
            var response = JSON.parse(http.responseText.split('\n').shift());
            if (!!response.id) {
              self._id = response.id;
              self._startWebSocket();
              self.emit('message', { type: 'ID', id: self._id });
            }
          } catch (e) {
            self._startWebSocket();
          }
        }
        self._handleStream(http, true);
      };
      http.send(null);
    } catch(e) {
      util.log('XMLHttpRequest not available; defaulting to WebSockets');
      this._startWebSocket();
    }
  } else {
    this._startXhrStream();
    this._startWebSocket();
  }
};

/** Start up websocket communications. */
Socket.prototype._startWebSocket = function() {
  if (!!this._socket) {
    return;
  }

  var wsurl = 'ws://' + this._server + '/ws';
  if (!!this._id) {
    wsurl += '?id=' + this._id;
    if (!!this._key) {
      wsurl += '&key=' + this._key;
    }
  } else if (!!this._key) {
    wsurl += '?key=' + this._key;
  }
  this._socket = new WebSocket(wsurl);

  var self = this;
  this._socket.onmessage = function(event) {
    try {
      self.emit('message', JSON.parse(event.data));
    } catch(e) {
      util.log('Invalid server message', event.data);
    }
  };

  // Take care of the queue of connections if necessary and make sure Peer knows
  // socket is open.
  this._socket.onopen = function() {
    util.log('Socket open');
    if (self._id) {
      self.emit('open');
    }
  };
};

/** Start XHR streaming. */
Socket.prototype._startXhrStream = function() {
  try {
    var self = this;

    var http = new XMLHttpRequest();
    var url = this._httpUrl + '/id';
    // Set API key if necessary.
    if (!!this._key) {
      url += '/' + this._key;
    }
    http.open('post', url, true);
    http.setRequestHeader('Content-Type', 'application/json');
    http.onreadystatechange = function() {
      self._handleStream(http);
    };
    http.send(JSON.stringify({ id: this._id }));
  } catch(e) {
    util.log('XMLHttpRequest not available; defaulting to WebSockets');
  }
};

/** Handles onreadystatechange response as a stream. */
Socket.prototype._handleStream = function(http, pad) {
  // 3 and 4 are loading/done state. All others are not relevant.
  if (http.readyState < 3) {
    return;
  } else if (http.readyState == 3 && http.status != 200) {
    return;
  }

  if (this._index === undefined) {
    this._index = pad ? 2 : 1;
  }

  if (http.responseText === null) {
    return;
  }

  var message = http.responseText.split('\n')[this._index];
  if (!!message && http.readyState == 3) {
    this._index += 1;
    try {
      this._handleHTTPErrors(JSON.parse(message));
    } catch(e) {
      util.log('Invalid server message', message);
    }
  } else if (http.readyState == 4) {
    this._index = 1;
  }
};

Socket.prototype._handleHTTPErrors = function(message) {
  switch (message.type) {
    // XHR stream closed by timeout.
    case 'HTTP-END':
      util.log('XHR stream timed out.');
      if (!!this._socket && this._socket.readyState != 1) {
        this._startXhrStream();
      }
      break;
    // XHR stream closed by socket connect.
    case 'HTTP-SOCKET':
      util.log('XHR stream closed, WebSocket connected.');
      break;
    case 'HTTP-ERROR':
      this.emit('error', 'Something went wrong.');
      break;
    default:
      this.emit('message', message);
  }
};

/** Exposed send for DC & Peer. */
Socket.prototype.send = function(data) {
  var type = data.type;
  message = JSON.stringify(data);
  if (!type) {
    this.emit('error', 'Invalid message');
  }

  if (!!this._socket && this._socket.readyState == 1) {
    this._socket.send(message);
  } else {
    var self = this;
    var http = new XMLHttpRequest();
    var url = this._httpUrl + '/' + type.toLowerCase();
    // Set API key if necessary.
    if (!!this._key) {
      url += '/' + this._key;
    }

    http.open('post', url, true);
    http.setRequestHeader('Content-Type', 'application/json');
    http.send(message);
  }
};

Socket.prototype.close = function() {
  if (!!this._socket && this._socket.readyState == 1) {
    this._socket.close();
  }
};

Socket.prototype.start = function() {
  this._checkIn();
};
<|endoftext|>


<commit_before>/**
 * Module dependencies.
 */

var EventEmitter = require('events').EventEmitter
  , debug = require('debug')('engine:socket')

/**
 * Module exports.
 */

module.exports = Socket;

/**
 * Client class (abstract).
 *
 * @api private
 */

function Socket (id, server, transport) {
  this.id = id;
  this.server = server;
  this.upgraded = false;
  this.readyState = 'opening';
  this.writeBuffer = [];
  this.packetsFn = [];

  this.setTransport(transport);
  this.onOpen();
}

/**
 * Inherits from EventEmitter.
 */

Socket.prototype.__proto__ = EventEmitter.prototype;

/**
 * Accessor for request that originated socket.
 *
 * @api public
 */

Socket.prototype.__defineGetter__('request', function () {
  return this.transport.request;
});

/**
 * Called upon transport considered open.
 *
 * @api private
 */

Socket.prototype.onOpen = function () {
  this.readyState = 'open';

  // sends an `open` packet
  this.transport.sid = this.id;
  this.sendPacket('open', JSON.stringify({
      sid: this.id
    , upgrades: this.getAvailableUpgrades()
    , pingInterval: this.server.pingInterval
    , pingTimeout: this.server.pingTimeout
  }));

  this.emit('open');
  this.setPingTimeout();
};

/**
 * Called upon transport packet.
 *
 * @param {Object} packet
 * @api private
 */

Socket.prototype.onPacket = function (packet) {
  if ('open' == this.readyState) {
    // export packet event
    debug('packet');
    this.emit('packet', packet);

    // Reset ping timeout on any packet, incoming data is a good sign of
    // other side's liveness
    this.setPingTimeout();

    switch (packet.type) {

      case 'ping':
        debug('got ping');
        this.sendPacket('pong');
        this.emit('heartbeat');
        break;

      case 'error':
        this.onClose('parse error');
        break;

      case 'message':
        this.emit('message', null == packet.data ? '' : packet.data);
        break;
    }
  } else {
    debug('packet received with closed socket');
  }
};

/**
 * Called upon transport error.
 *
 * @param {Error} error object
 * @api private
 */

Socket.prototype.onError = function (err) {
  debug('transport error');
  this.onClose('transport error', err);
};

/**
 * Sets and resets ping timeout timer based on client pings.
 *
 * @api private
 */

Socket.prototype.setPingTimeout = function () {
  var self = this;
  clearTimeout(self.pingTimeoutTimer);
  self.pingTimeoutTimer = setTimeout(function () {
    self.onClose('ping timeout');
  }, self.server.pingInterval + self.server.pingTimeout);
};

/**
 * Attaches handlers for the given transport.
 *
 * @param {Transport} transport
 * @api private
 */

Socket.prototype.setTransport = function (transport) {
  this.transport = transport;
  this.transport.once('error', this.onError.bind(this));
  this.transport.on('packet', this.onPacket.bind(this));
  this.transport.on('drain', this.flush.bind(this));
  this.transport.once('close', this.onClose.bind(this, 'transport close'));
  //this function will manage packet events (also message callbacks)
  this.setupSendCallback();
};

/**
 * Upgrades socket to the given transport
 *
 * @param {Transport} transport
 * @api private
 */

Socket.prototype.maybeUpgrade = function (transport) {
  debug('might upgrade socket transport from "%s" to "%s"'
    , this.transport.name, transport.name);

  // set transport upgrade timer
  var checkInterval;
  var upgradeTimeout = setTimeout(function () {
    debug('client did not complete upgrade - closing transport');
    clearInterval(checkInterval);
    if ('open' == transport.readyState) {
      transport.close();
    }
  }, this.server.upgradeTimeout);

  var self = this;
  function onPacket (packet) {
    if ('ping' == packet.type && 'probe' == packet.data) {
      transport.send([{ type: 'pong', data: 'probe' }]);

      // we force a polling cycle to ensure a fast upgrade
      function check () {
        if ('polling' == self.transport.name && self.transport.writable) {
          debug('writing a noop packet to polling for fast upgrade');
          self.transport.send([{ type: 'noop' }]);
        }
      }

      checkInterval = setInterval(check, 100);
    } else if ('upgrade' == packet.type) {
      debug('got upgrade packet - upgrading');
      self.upgraded = true;
      self.emit('upgrade', transport);
      self.clearTransport();
      self.setTransport(transport);
      self.setPingTimeout();
      self.flush();
      clearInterval(checkInterval);
      clearTimeout(upgradeTimeout);
      transport.removeListener('packet', onPacket);
    } else {
      transport.close();
    }
  }
  transport.on('packet', onPacket);
};

/**
 * Clears listeners and timers associated with current transport.
 *
 * @api private
 */

Socket.prototype.clearTransport = function () {
  // silence further transport errors and prevent uncaught exceptions
  this.transport.on('error', function(){
    debug('error triggered by discarded transport');
  });
  clearTimeout(this.pingIntervalTimer);
  clearTimeout(this.pingTimeoutTimer);
};

/**
 * Called upon transport considered closed.
 * Possible reasons: `ping timeout`, `client error`, `parse error`,
 * `transport error`, `server close`, `transport close`
 */

Socket.prototype.onClose = function (reason, description) {
  if ('closed' != this.readyState) {
    this.packetsFn = [];
    this.clearTransport();
    this.readyState = 'closed';
    this.emit('close', reason, description);
  }
};

/**
 * Setup and manage send callback
 *
 * @api private
 */

Socket.prototype.setupSendCallback = function () {
  var self = this;
  //the message was sent successfully, execute the callback
  this.transport.on('drain', function() {
    if (self.packetsFn.length > 0) {
      var seqFn = self.packetsFn.splice(0,1)[0];
      if ('function' == typeof seqFn) {
        debug('executing send callback');
        seqFn(self.transport);
      }
    }
  });
};

/**
 * Sends a message packet.
 *
 * @param {String} message
 * @param {Function} callback
 * @return {Socket} for chaining
 * @api public
 */

Socket.prototype.send = function (data, callback) {
  this.sendPacket('message', data, callback);
  return this;
};

/**
 * Sends a packet.
 *
 * @param {String} packet type
 * @param {String} optional, data
 * @api private
 */

Socket.prototype.sendPacket = function (type, data, callback) {
  if ('closing' != this.readyState) {
    debug('sending packet "%s" (%s)', type, data);

    var packet = { type: type };
    if (data) packet.data = data;

    // exports packetCreate event
    this.emit('packetCreate', packet);

    this.writeBuffer.push(packet);

    //add send callback to object
    if (callback) {
      this.packetsFn.push(callback);
    }
    this.flush();
  }
};

/**
 * Attempts to flush the packets buffer.
 *
 * @api private
 */

Socket.prototype.flush = function () {
  if ('closed' != this.readyState && this.transport.writable
    && this.writeBuffer.length) {
    debug('flushing buffer to transport');
    this.emit('flush', this.writeBuffer);
    this.server.emit('flush', this, this.writeBuffer);
    var wbuf = this.writeBuffer;
    this.writeBuffer = [];
    this.transport.send(wbuf);
    this.emit('drain');
    this.server.emit('drain', this);
  }
};

/**
 * Get available upgrades for this socket.
 *
 * @api private
 */

Socket.prototype.getAvailableUpgrades = function () {
  var availableUpgrades = [];
  var allUpgrades = this.server.upgrades(this.transport.name);
  for (var i = 0, l = allUpgrades.length; i < l; ++i) {
    var upg = allUpgrades[i];
    if (this.server.transports.indexOf(upg) != -1) {
      availableUpgrades.push(upg);
    }
  }
  return availableUpgrades;
};

/**
 * Closes the socket and underlying transport.
 *
 * @return {Socket} for chaining
 * @api public
 */

Socket.prototype.close = function () {
  if ('open' == this.readyState) {
    this.readyState = 'closing';
    var self = this;
    this.transport.close(function () {
      self.onClose('forced close');
    });
  }
};
<commit_msg>socket: added `Socket#write`<commit_after>/**
 * Module dependencies.
 */

var EventEmitter = require('events').EventEmitter
  , debug = require('debug')('engine:socket')

/**
 * Module exports.
 */

module.exports = Socket;

/**
 * Client class (abstract).
 *
 * @api private
 */

function Socket (id, server, transport) {
  this.id = id;
  this.server = server;
  this.upgraded = false;
  this.readyState = 'opening';
  this.writeBuffer = [];
  this.packetsFn = [];

  this.setTransport(transport);
  this.onOpen();
}

/**
 * Inherits from EventEmitter.
 */

Socket.prototype.__proto__ = EventEmitter.prototype;

/**
 * Accessor for request that originated socket.
 *
 * @api public
 */

Socket.prototype.__defineGetter__('request', function () {
  return this.transport.request;
});

/**
 * Called upon transport considered open.
 *
 * @api private
 */

Socket.prototype.onOpen = function () {
  this.readyState = 'open';

  // sends an `open` packet
  this.transport.sid = this.id;
  this.sendPacket('open', JSON.stringify({
      sid: this.id
    , upgrades: this.getAvailableUpgrades()
    , pingInterval: this.server.pingInterval
    , pingTimeout: this.server.pingTimeout
  }));

  this.emit('open');
  this.setPingTimeout();
};

/**
 * Called upon transport packet.
 *
 * @param {Object} packet
 * @api private
 */

Socket.prototype.onPacket = function (packet) {
  if ('open' == this.readyState) {
    // export packet event
    debug('packet');
    this.emit('packet', packet);

    // Reset ping timeout on any packet, incoming data is a good sign of
    // other side's liveness
    this.setPingTimeout();

    switch (packet.type) {

      case 'ping':
        debug('got ping');
        this.sendPacket('pong');
        this.emit('heartbeat');
        break;

      case 'error':
        this.onClose('parse error');
        break;

      case 'message':
        this.emit('message', null == packet.data ? '' : packet.data);
        break;
    }
  } else {
    debug('packet received with closed socket');
  }
};

/**
 * Called upon transport error.
 *
 * @param {Error} error object
 * @api private
 */

Socket.prototype.onError = function (err) {
  debug('transport error');
  this.onClose('transport error', err);
};

/**
 * Sets and resets ping timeout timer based on client pings.
 *
 * @api private
 */

Socket.prototype.setPingTimeout = function () {
  var self = this;
  clearTimeout(self.pingTimeoutTimer);
  self.pingTimeoutTimer = setTimeout(function () {
    self.onClose('ping timeout');
  }, self.server.pingInterval + self.server.pingTimeout);
};

/**
 * Attaches handlers for the given transport.
 *
 * @param {Transport} transport
 * @api private
 */

Socket.prototype.setTransport = function (transport) {
  this.transport = transport;
  this.transport.once('error', this.onError.bind(this));
  this.transport.on('packet', this.onPacket.bind(this));
  this.transport.on('drain', this.flush.bind(this));
  this.transport.once('close', this.onClose.bind(this, 'transport close'));
  //this function will manage packet events (also message callbacks)
  this.setupSendCallback();
};

/**
 * Upgrades socket to the given transport
 *
 * @param {Transport} transport
 * @api private
 */

Socket.prototype.maybeUpgrade = function (transport) {
  debug('might upgrade socket transport from "%s" to "%s"'
    , this.transport.name, transport.name);

  // set transport upgrade timer
  var checkInterval;
  var upgradeTimeout = setTimeout(function () {
    debug('client did not complete upgrade - closing transport');
    clearInterval(checkInterval);
    if ('open' == transport.readyState) {
      transport.close();
    }
  }, this.server.upgradeTimeout);

  var self = this;
  function onPacket (packet) {
    if ('ping' == packet.type && 'probe' == packet.data) {
      transport.send([{ type: 'pong', data: 'probe' }]);

      // we force a polling cycle to ensure a fast upgrade
      function check () {
        if ('polling' == self.transport.name && self.transport.writable) {
          debug('writing a noop packet to polling for fast upgrade');
          self.transport.send([{ type: 'noop' }]);
        }
      }

      checkInterval = setInterval(check, 100);
    } else if ('upgrade' == packet.type) {
      debug('got upgrade packet - upgrading');
      self.upgraded = true;
      self.emit('upgrade', transport);
      self.clearTransport();
      self.setTransport(transport);
      self.setPingTimeout();
      self.flush();
      clearInterval(checkInterval);
      clearTimeout(upgradeTimeout);
      transport.removeListener('packet', onPacket);
    } else {
      transport.close();
    }
  }
  transport.on('packet', onPacket);
};

/**
 * Clears listeners and timers associated with current transport.
 *
 * @api private
 */

Socket.prototype.clearTransport = function () {
  // silence further transport errors and prevent uncaught exceptions
  this.transport.on('error', function(){
    debug('error triggered by discarded transport');
  });
  clearTimeout(this.pingIntervalTimer);
  clearTimeout(this.pingTimeoutTimer);
};

/**
 * Called upon transport considered closed.
 * Possible reasons: `ping timeout`, `client error`, `parse error`,
 * `transport error`, `server close`, `transport close`
 */

Socket.prototype.onClose = function (reason, description) {
  if ('closed' != this.readyState) {
    this.packetsFn = [];
    this.clearTransport();
    this.readyState = 'closed';
    this.emit('close', reason, description);
  }
};

/**
 * Setup and manage send callback
 *
 * @api private
 */

Socket.prototype.setupSendCallback = function () {
  var self = this;
  //the message was sent successfully, execute the callback
  this.transport.on('drain', function() {
    if (self.packetsFn.length > 0) {
      var seqFn = self.packetsFn.splice(0,1)[0];
      if ('function' == typeof seqFn) {
        debug('executing send callback');
        seqFn(self.transport);
      }
    }
  });
};

/**
 * Sends a message packet.
 *
 * @param {String} message
 * @param {Function} callback
 * @return {Socket} for chaining
 * @api public
 */

Socket.prototype.send =
Socket.prototype.write = function(data, callback){
  this.sendPacket('message', data, callback);
  return this;
};

/**
 * Sends a packet.
 *
 * @param {String} packet type
 * @param {String} optional, data
 * @api private
 */

Socket.prototype.sendPacket = function (type, data, callback) {
  if ('closing' != this.readyState) {
    debug('sending packet "%s" (%s)', type, data);

    var packet = { type: type };
    if (data) packet.data = data;

    // exports packetCreate event
    this.emit('packetCreate', packet);

    this.writeBuffer.push(packet);

    //add send callback to object
    if (callback) {
      this.packetsFn.push(callback);
    }
    this.flush();
  }
};

/**
 * Attempts to flush the packets buffer.
 *
 * @api private
 */

Socket.prototype.flush = function () {
  if ('closed' != this.readyState && this.transport.writable
    && this.writeBuffer.length) {
    debug('flushing buffer to transport');
    this.emit('flush', this.writeBuffer);
    this.server.emit('flush', this, this.writeBuffer);
    var wbuf = this.writeBuffer;
    this.writeBuffer = [];
    this.transport.send(wbuf);
    this.emit('drain');
    this.server.emit('drain', this);
  }
};

/**
 * Get available upgrades for this socket.
 *
 * @api private
 */

Socket.prototype.getAvailableUpgrades = function () {
  var availableUpgrades = [];
  var allUpgrades = this.server.upgrades(this.transport.name);
  for (var i = 0, l = allUpgrades.length; i < l; ++i) {
    var upg = allUpgrades[i];
    if (this.server.transports.indexOf(upg) != -1) {
      availableUpgrades.push(upg);
    }
  }
  return availableUpgrades;
};

/**
 * Closes the socket and underlying transport.
 *
 * @return {Socket} for chaining
 * @api public
 */

Socket.prototype.close = function () {
  if ('open' == this.readyState) {
    this.readyState = 'closing';
    var self = this;
    this.transport.close(function () {
      self.onClose('forced close');
    });
  }
};
<|endoftext|>


<commit_before>/**
 * Module dependencies.
 */

var util = require('./util')
  , transports = require('./transports')
  , EventEmitter = require('./event-emitter')

/**
 * Module exports.
 */

module.exports = Socket;

/**
 * Socket constructor.
 *
 * @param {Object} options
 * @api public
 */

function Socket (opts) {
  if ('string' == typeof opts) {
    var uri = util.parseUri(opts);
    opts = arguments[1] || {};
    opts.host = uri.host;
    opts.secure = uri.scheme == 'https' || uri.scheme == 'wss';
    opts.port = uri.port || (opts.secure ? 443 : 80);
  }

  opts = opts || {};

  this.host = opts.host || opts.hostname || 'localhost';
  this.port = opts.port || 80;
  this.query = opts.query || {};
  this.query.uid = rnd();
  this.upgrade = false !== opts.upgrade;
  this.path = opts.path || '/engine.io'
  this.forceJSONP = !!opts.forceJSONP;
  this.flashPath = opts.flashPath || '';
  this.transports = opts.transports || ['polling', 'websocket', 'flashsocket'];
  this.readyState = '';
  this.writeBuffer = [];
  this.open();
};

/**
 * Inherits from EventEmitter.
 */

util.inherits(Socket, EventEmitter);

/**
 * Creates transport of the given type.
 *
 * @param {String} transport name
 * @return {Transport}
 * @api private
 */

Socket.prototype.createTransport = function (name) {
  // debug: creating transport "%s", name
  var query = clone(this.query)
  query.transport = name;

  if (this.sid) {
    query.sid = this.sid;
  }

  var transport = new transports[name]({
      host: this.host
    , port: this.port
    , secure: this.secure
    , path: this.path
    , query: query
    , forceJSONP: this.forceJSONP
    , flashPath: this.flashPath
  });

  return transport;
};

function clone (obj) {
  var o = {};
  for (var i in obj) {
    if (obj.hasOwnProperty(i)) {
      o[i] = obj[i];
    }
  }
  return o;
}

/**
 * Initializes transport to use and starts probe.
 *
 * @api private
 */

Socket.prototype.open = function () {
  this.readyState = 'opening';
  var transport = this.createTransport(this.transports[0]);
  transport.open();
  this.setTransport(transport);
};

/**
 * Sets the current transport. Disables the existing one (if any).
 *
 * @api private
 */

Socket.prototype.setTransport = function (transport) {
  var self = this;

  if (this.transport) {
    // debug: clearing existing transport
    this.transport.removeAllListeners();
  }

  // set up transport
  this.transport = transport;

  // set up transport listeners
  transport
    .on('drain', function () {
      self.flush();
    })
    .on('packet', function (packet) {
      self.onPacket(packet);
    })
    .on('error', function (e) {
      self.onError(e);
    })
    .on('close', function () {
      self.onClose('transport close');
    })
};

/**
 * Probes a transport.
 *
 * @param {String} transport name
 * @api private
 */

Socket.prototype.probe = function (name) {
  // debug: probing transport "%s", name
  var transport = this.createTransport(name, { probe: 1 })
    , self = this

  transport.once('open', function () {
    // debug: probe transport "%s" opened - pinging, name
    transport.send([{ type: 'ping', data: 'probe' }]);
    transport.once('packet', function (msg) {
      if ('pong' == msg.type && 'probe' == msg.data) {
        // debug: probe transport "%s" pong - upgrading, name
        self.upgrading = true;
        self.emit('upgrading', transport);

        // debug: pausing current transport "%s", self.transport.name
        self.transport.pause(function () {
          if ('closed' == self.readyState || 'closing' == self.readyState) return;
          // debug: changing transport and sending upgrade packet
          self.emit('upgrade', transport);
          self.setTransport(transport);
          transport.send([{ type: 'upgrade' }]);
          transport = null;
          self.upgrading = false;
          self.flush();
        });
      } else {
        // debug: probe transport "%s" failed, name
        var err = new Error('probe error');
        err.transport = transport.name;
        self.emit('error', err);
      }
    });
  });

  transport.open();

  this.once('close', function () {
    if (transport) {
      // debug: socket closed prematurely - aborting probe
      transport.close();
      transport = null;
    }
  });

  this.once('upgrading', function (to) {
    if (transport && to.name != transport.name) {
      // debug: "%s" works - aborting "%s", to.name, transport.name
      transport.close();
      transport = null;
    }
  });
};

/**
 * Called when connection is deemed open.
 *
 * @api public
 */

Socket.prototype.onOpen = function () {
  // debug: socket open
  this.readyState = 'open';
  this.emit('open');
  this.onopen && this.onopen.call(this);
  this.flush();

  if (this.upgrade && this.transport.pause) {
    // debug: starting upgrade probes
    for (var i = 0, l = this.upgrades.length; i < l; i++) {
      this.probe(this.upgrades[i]);
    }
  }
};

/**
 * Handles a packet.
 *
 * @api private
 */

Socket.prototype.onPacket = function (packet) {
  if ('opening' == this.readyState || 'open' == this.readyState) {
    // debug: socket receive: type "%s" | data "%s", packet.type, packet.data
    switch (packet.type) {
      case 'open':
        this.onHandshake(util.parseJSON(packet.data));
        break;

      case 'ping':
        this.sendPacket('pong');
        this.setPingTimeout();
        break;

      case 'error':
        var err = new Error('server error');
        err.code = packet.data;
        this.emit('error', err);
        break;

      case 'message':
        this.emit('message', packet.data);
        this.onmessage && this.onmessage.call(this, packet.data);
        break;
    }
  } else {
    // debug: packet received with socket readyState "%s", this.readyState
  }
};

/**
 * Called upon handshake completion.
 *
 * @param {Object} handshake obj
 * @api private
 */

Socket.prototype.onHandshake = function (data) {
  this.emit('handshake', data);
  this.sid = data.sid;
  this.transport.query.sid = data.sid;
  this.upgrades = data.upgrades;
  this.pingTimeout = data.pingTimeout;
  this.pingInterval = data.pingInterval;
  this.onOpen();
  this.setPingTimeout();
};

/**
 * Clears and sets a ping timeout based on the expected ping interval.
 *
 * @api private
 */

Socket.prototype.setPingTimeout = function () {
  clearTimeout(this.pingTimeoutTimer);
  var self = this;
  this.pingTimeoutTimer = setTimeout(function () {
    self.onClose('ping timeout');
  }, this.pingTimeout);
};

/**
 * Flush write buffers.
 *
 * @api private
 */

Socket.prototype.flush = function () {
  if ('closed' != this.readyState && this.transport.writable
    && !this.upgrading && this.writeBuffer.length) {
    // debug: flushing %d packets in socket, this.writeBuffer.length
    this.transport.send(this.writeBuffer);
    this.writeBuffer = [];
  }
};

/**
 * Sends a message.
 *
 * @param {String} message.
 * @return {Socket} for chaining.
 * @api public
 */

Socket.prototype.send = function (msg) {
  this.sendPacket('message', msg);
  return this;
};

/**
 * Sends a packet.
 *
 * @param {String} packet type.
 * @param {String} data.
 * @api private
 */

Socket.prototype.sendPacket = function (type, data) {
  var packet = { type: type, data: data };
  this.writeBuffer.push(packet);
  this.flush();
};

/**
 * Closes the connection.
 *
 * @api private
 */

Socket.prototype.close = function () {
  if ('opening' == this.readyState || 'open' == this.readyState) {
    this.onClose('forced close');
    // debug: socket closing - telling transport to close
    this.transport.close();
  }

  return this;
};

/**
 * Called upon transport error
 *
 * @api private
 */

Socket.prototype.onError = function (err) {
  this.emit('error', err);
  this.onClose('transport error', err);
};

/**
 * Called upon transport close.
 *
 * @api private
 */

Socket.prototype.onClose = function (reason, desc) {
  if ('closed' != this.readyState) {
    // debug: socket close: "%s", reason
    this.readyState = 'closed';
    this.emit('close', reason, desc);
    this.onclose && this.onclose.call(this);
  }
};

/**
 * Generates a random uid.
 *
 * @api private
 */

function rnd () {
  return String(Math.random()).substr(5) + String(Math.random()).substr(5);
}
<commit_msg>Consistency with server<commit_after>/**
 * Module dependencies.
 */

var util = require('./util')
  , transports = require('./transports')
  , EventEmitter = require('./event-emitter')

/**
 * Module exports.
 */

module.exports = Socket;

/**
 * Socket constructor.
 *
 * @param {Object} options
 * @api public
 */

function Socket (opts) {
  if ('string' == typeof opts) {
    var uri = util.parseUri(opts);
    opts = arguments[1] || {};
    opts.host = uri.host;
    opts.secure = uri.scheme == 'https' || uri.scheme == 'wss';
    opts.port = uri.port || (opts.secure ? 443 : 80);
  }

  opts = opts || {};

  this.host = opts.host || opts.hostname || 'localhost';
  this.port = opts.port || 80;
  this.query = opts.query || {};
  this.query.uid = rnd();
  this.upgrade = false !== opts.upgrade;
  this.path = opts.path || '/engine.io'
  this.forceJSONP = !!opts.forceJSONP;
  this.flashPath = opts.flashPath || '';
  this.transports = opts.transports || ['polling', 'websocket', 'flashsocket'];
  this.readyState = '';
  this.writeBuffer = [];
  this.open();
};

/**
 * Inherits from EventEmitter.
 */

util.inherits(Socket, EventEmitter);

/**
 * Creates transport of the given type.
 *
 * @param {String} transport name
 * @return {Transport}
 * @api private
 */

Socket.prototype.createTransport = function (name) {
  // debug: creating transport "%s", name
  var query = clone(this.query)
  query.transport = name;

  if (this.id) {
    query.sid = this.id;
  }

  var transport = new transports[name]({
      host: this.host
    , port: this.port
    , secure: this.secure
    , path: this.path
    , query: query
    , forceJSONP: this.forceJSONP
    , flashPath: this.flashPath
  });

  return transport;
};

function clone (obj) {
  var o = {};
  for (var i in obj) {
    if (obj.hasOwnProperty(i)) {
      o[i] = obj[i];
    }
  }
  return o;
}

/**
 * Initializes transport to use and starts probe.
 *
 * @api private
 */

Socket.prototype.open = function () {
  this.readyState = 'opening';
  var transport = this.createTransport(this.transports[0]);
  transport.open();
  this.setTransport(transport);
};

/**
 * Sets the current transport. Disables the existing one (if any).
 *
 * @api private
 */

Socket.prototype.setTransport = function (transport) {
  var self = this;

  if (this.transport) {
    // debug: clearing existing transport
    this.transport.removeAllListeners();
  }

  // set up transport
  this.transport = transport;

  // set up transport listeners
  transport
    .on('drain', function () {
      self.flush();
    })
    .on('packet', function (packet) {
      self.onPacket(packet);
    })
    .on('error', function (e) {
      self.onError(e);
    })
    .on('close', function () {
      self.onClose('transport close');
    })
};

/**
 * Probes a transport.
 *
 * @param {String} transport name
 * @api private
 */

Socket.prototype.probe = function (name) {
  // debug: probing transport "%s", name
  var transport = this.createTransport(name, { probe: 1 })
    , self = this

  transport.once('open', function () {
    // debug: probe transport "%s" opened - pinging, name
    transport.send([{ type: 'ping', data: 'probe' }]);
    transport.once('packet', function (msg) {
      if ('pong' == msg.type && 'probe' == msg.data) {
        // debug: probe transport "%s" pong - upgrading, name
        self.upgrading = true;
        self.emit('upgrading', transport);

        // debug: pausing current transport "%s", self.transport.name
        self.transport.pause(function () {
          if ('closed' == self.readyState || 'closing' == self.readyState) return;
          // debug: changing transport and sending upgrade packet
          self.emit('upgrade', transport);
          self.setTransport(transport);
          transport.send([{ type: 'upgrade' }]);
          transport = null;
          self.upgrading = false;
          self.flush();
        });
      } else {
        // debug: probe transport "%s" failed, name
        var err = new Error('probe error');
        err.transport = transport.name;
        self.emit('error', err);
      }
    });
  });

  transport.open();

  this.once('close', function () {
    if (transport) {
      // debug: socket closed prematurely - aborting probe
      transport.close();
      transport = null;
    }
  });

  this.once('upgrading', function (to) {
    if (transport && to.name != transport.name) {
      // debug: "%s" works - aborting "%s", to.name, transport.name
      transport.close();
      transport = null;
    }
  });
};

/**
 * Called when connection is deemed open.
 *
 * @api public
 */

Socket.prototype.onOpen = function () {
  // debug: socket open
  this.readyState = 'open';
  this.emit('open');
  this.onopen && this.onopen.call(this);
  this.flush();

  if (this.upgrade && this.transport.pause) {
    // debug: starting upgrade probes
    for (var i = 0, l = this.upgrades.length; i < l; i++) {
      this.probe(this.upgrades[i]);
    }
  }
};

/**
 * Handles a packet.
 *
 * @api private
 */

Socket.prototype.onPacket = function (packet) {
  if ('opening' == this.readyState || 'open' == this.readyState) {
    // debug: socket receive: type "%s" | data "%s", packet.type, packet.data
    switch (packet.type) {
      case 'open':
        this.onHandshake(util.parseJSON(packet.data));
        break;

      case 'ping':
        this.sendPacket('pong');
        this.setPingTimeout();
        break;

      case 'error':
        var err = new Error('server error');
        err.code = packet.data;
        this.emit('error', err);
        break;

      case 'message':
        this.emit('message', packet.data);
        this.onmessage && this.onmessage.call(this, packet.data);
        break;
    }
  } else {
    // debug: packet received with socket readyState "%s", this.readyState
  }
};

/**
 * Called upon handshake completion.
 *
 * @param {Object} handshake obj
 * @api private
 */

Socket.prototype.onHandshake = function (data) {
  this.emit('handshake', data);
  this.id = data.sid;
  this.transport.query.sid = data.sid;
  this.upgrades = data.upgrades;
  this.pingTimeout = data.pingTimeout;
  this.pingInterval = data.pingInterval;
  this.onOpen();
  this.setPingTimeout();
};

/**
 * Clears and sets a ping timeout based on the expected ping interval.
 *
 * @api private
 */

Socket.prototype.setPingTimeout = function () {
  clearTimeout(this.pingTimeoutTimer);
  var self = this;
  this.pingTimeoutTimer = setTimeout(function () {
    self.onClose('ping timeout');
  }, this.pingTimeout);
};

/**
 * Flush write buffers.
 *
 * @api private
 */

Socket.prototype.flush = function () {
  if ('closed' != this.readyState && this.transport.writable
    && !this.upgrading && this.writeBuffer.length) {
    // debug: flushing %d packets in socket, this.writeBuffer.length
    this.transport.send(this.writeBuffer);
    this.writeBuffer = [];
  }
};

/**
 * Sends a message.
 *
 * @param {String} message.
 * @return {Socket} for chaining.
 * @api public
 */

Socket.prototype.send = function (msg) {
  this.sendPacket('message', msg);
  return this;
};

/**
 * Sends a packet.
 *
 * @param {String} packet type.
 * @param {String} data.
 * @api private
 */

Socket.prototype.sendPacket = function (type, data) {
  var packet = { type: type, data: data };
  this.writeBuffer.push(packet);
  this.flush();
};

/**
 * Closes the connection.
 *
 * @api private
 */

Socket.prototype.close = function () {
  if ('opening' == this.readyState || 'open' == this.readyState) {
    this.onClose('forced close');
    // debug: socket closing - telling transport to close
    this.transport.close();
  }

  return this;
};

/**
 * Called upon transport error
 *
 * @api private
 */

Socket.prototype.onError = function (err) {
  this.emit('error', err);
  this.onClose('transport error', err);
};

/**
 * Called upon transport close.
 *
 * @api private
 */

Socket.prototype.onClose = function (reason, desc) {
  if ('closed' != this.readyState) {
    // debug: socket close: "%s", reason
    this.readyState = 'closed';
    this.emit('close', reason, desc);
    this.onclose && this.onclose.call(this);
  }
};

/**
 * Generates a random uid.
 *
 * @api private
 */

function rnd () {
  return String(Math.random()).substr(5) + String(Math.random()).substr(5);
}
<|endoftext|>


<commit_before>const debug = require('debug');
const events = require('events');
const hash = require('./hash');
const Q = require('q');
const W3CWebSocket = require('websocket').w3cwebsocket;

const debugLog = debug('apprtc-socket');
const errorLog = debug('apprtc-socket:error');
const origin = 'foo://bar';
const url = 'wss://apprtc-ws.webrtc.org:443/ws';
const registrationTimeout = 200; // ms

/**
 * AppRtc socket
 *
 * @constructor
 * @fires AppRtcSocket#ready
 * @fires AppRtcSocket#close
 * @fires AppRtcSocket#error
 * @fires AppRtcSocket#message
 */
class AppRtcSocket extends events.EventEmitter {
  constructor(myId, peerId) {
    super();
    this.myId = myId;
    this.peerId = peerId;
  }

  // send message to peer
  send(data) {
    const message = {};
    message.cmd = 'send';
    message.msg = data;
    this.wsClient.send(JSON.stringify(message));
  }

  // connect to AppRtc server
  connect() {
    // create websocket
    this.wsClient = new W3CWebSocket(url, null, origin);
    // websocket events
    this.wsClient.onopen = this.onOpen.bind(this);
    this.wsClient.onclose = this.onClose.bind(this);
    this.wsClient.onmessage = this.onMessage.bind(this);
    this.wsClient.onerror = this.onFailure.bind(this);
  }

  // connect to AppRtc server, return promise
  connectP() {
    const deferred = Q.defer();
    this.on('ready', () => {
      deferred.resolve();
    });
    this.on('error', (error) => {
      deferred.reject(error);
    });
    this.connect();
    return deferred.promise;
  }

  // close socket
  close() {
    this.wsClient.close();
  }

  // close socket, return promise
  closeP() {
    const deferred = Q.defer();
    this.on('close', () => {
      deferred.resolve();
    });
    this.on('error', (error) => {
      deferred.reject(error);
    });
    this.close();
    return deferred.promise;
  }

  // connection established
  onOpen() {
    debugLog('connected');
    // send register message
    this.register();
  }

  // send register message to AppRtc server
  register() {
    const message = {};
    message.cmd = 'register';
    message.clientId = this.myId;
    message.roomId = hash(this.myId, this.peerId);
    this.wsClient.send(JSON.stringify(message));
    const self = this;
    this.registrationTimer = setTimeout(() => {
      self.emit('ready');
    }, registrationTimeout);
  }

  // connection closed
  onClose() {
    debugLog('closed');
    // fire close event
    this.emit('close');
  }

  // incoming message
  onMessage(event) {
    // this._log.debug('incoming message')
    if (typeof event.data !== 'string') {
      const binaryErrorMsg = 'not expecting to receive a binary message -- dropping on the floor';
      errorLog(binaryErrorMsg);
      return;
    }
    const messageObject = JSON.parse(event.data);
    if (messageObject.msg !== undefined && messageObject.error !== undefined) {
      this.onIncomingApprtcMessage(messageObject.msg, messageObject.error);
    } else {
      const formatErrorMsg = 'received message does not comply with expected format -- dropping on the floor';
      errorLog(formatErrorMsg);
    }
  }

  // incoming AppRTC message
  onIncomingApprtcMessage(message, error) {
    if (error) {
      const errorMsg = `apprtc error: ${error}`;
      errorLog(errorMsg);
      this.emit('error', errorMsg);
      return;
    }
    this.emit('message', message);
  }

  // error handler
  onFailure(error) {
    // cancel registration timer -- if present
    if (this.registrationTimer) {
      clearTimeout(this.registrationTimer);
    }
    // fire error event
    const errorMsg = `socket error: ${error}`;
    errorLog(errorMsg);
    this.emit('error', errorMsg);
  }
}

module.exports = AppRtcSocket;
<commit_msg>replaced self with lexical-scoped this in arrow function<commit_after>const debug = require('debug');
const events = require('events');
const hash = require('./hash');
const Q = require('q');
const W3CWebSocket = require('websocket').w3cwebsocket;

const debugLog = debug('apprtc-socket');
const errorLog = debug('apprtc-socket:error');
const origin = 'foo://bar';
const url = 'wss://apprtc-ws.webrtc.org:443/ws';
const registrationTimeout = 200; // ms

/**
 * AppRtc socket
 *
 * @constructor
 * @fires AppRtcSocket#ready
 * @fires AppRtcSocket#close
 * @fires AppRtcSocket#error
 * @fires AppRtcSocket#message
 */
class AppRtcSocket extends events.EventEmitter {
  constructor(myId, peerId) {
    super();
    this.myId = myId;
    this.peerId = peerId;
  }

  // send message to peer
  send(data) {
    const message = {};
    message.cmd = 'send';
    message.msg = data;
    this.wsClient.send(JSON.stringify(message));
  }

  // connect to AppRtc server
  connect() {
    // create websocket
    this.wsClient = new W3CWebSocket(url, null, origin);
    // websocket events
    this.wsClient.onopen = this.onOpen.bind(this);
    this.wsClient.onclose = this.onClose.bind(this);
    this.wsClient.onmessage = this.onMessage.bind(this);
    this.wsClient.onerror = this.onFailure.bind(this);
  }

  // connect to AppRtc server, return promise
  connectP() {
    const deferred = Q.defer();
    this.on('ready', () => {
      deferred.resolve();
    });
    this.on('error', (error) => {
      deferred.reject(error);
    });
    this.connect();
    return deferred.promise;
  }

  // close socket
  close() {
    this.wsClient.close();
  }

  // close socket, return promise
  closeP() {
    const deferred = Q.defer();
    this.on('close', () => {
      deferred.resolve();
    });
    this.on('error', (error) => {
      deferred.reject(error);
    });
    this.close();
    return deferred.promise;
  }

  // connection established
  onOpen() {
    debugLog('connected');
    // send register message
    this.register();
  }

  // send register message to AppRtc server
  register() {
    const message = {};
    message.cmd = 'register';
    message.clientId = this.myId;
    message.roomId = hash(this.myId, this.peerId);
    this.wsClient.send(JSON.stringify(message));
    this.registrationTimer = setTimeout(() => {
      this.emit('ready');
    }, registrationTimeout);
  }

  // connection closed
  onClose() {
    debugLog('closed');
    // fire close event
    this.emit('close');
  }

  // incoming message
  onMessage(event) {
    // this._log.debug('incoming message')
    if (typeof event.data !== 'string') {
      const binaryErrorMsg = 'not expecting to receive a binary message -- dropping on the floor';
      errorLog(binaryErrorMsg);
      return;
    }
    const messageObject = JSON.parse(event.data);
    if (messageObject.msg !== undefined && messageObject.error !== undefined) {
      this.onIncomingApprtcMessage(messageObject.msg, messageObject.error);
    } else {
      const formatErrorMsg = 'received message does not comply with expected format -- dropping on the floor';
      errorLog(formatErrorMsg);
    }
  }

  // incoming AppRTC message
  onIncomingApprtcMessage(message, error) {
    if (error) {
      const errorMsg = `apprtc error: ${error}`;
      errorLog(errorMsg);
      this.emit('error', errorMsg);
      return;
    }
    this.emit('message', message);
  }

  // error handler
  onFailure(error) {
    // cancel registration timer -- if present
    if (this.registrationTimer) {
      clearTimeout(this.registrationTimer);
    }
    // fire error event
    const errorMsg = `socket error: ${error}`;
    errorLog(errorMsg);
    this.emit('error', errorMsg);
  }
}

module.exports = AppRtcSocket;
<|endoftext|>


<commit_before>'use strict';

var path = require('path');

function tsifyString(str) {
    return str
        .replace(/[^\w\d _]/g, '')
        .replace(/_/g, ' ')
        .replace(/ (\w)/g, s => s.trim().toUpperCase())
        .replace(/^\w/, s => s.toUpperCase());
}

const INLINE = 0;
const BLOCK = 1;

/**
 * The Type class tracks a single type node (a schema), which could be
 * something as simple as a `number`, or as complicated as a `"type": "object"
 * schema. It is compatible with any allOf/anyOf/oneOf construct.
 */
class Type {
    /**
     * Or Type constructor initialises a bunch of different variables, which
     * need to be set for the conversion methods to function properly. Note
     * that no actual Typescript is written until we get to these conversion
     * methods.
     * @param {Sontyp} st - The Sontyp object that we're working from.
     * @param {Object} object - A valid JSON Schema.
     */
    constructor(st, object) {
        this.st = st;
        this.object = object;

        this.name = this.object.title ? tsifyString(this.object.title) : '';

        this.type = INLINE;
        this.converted = false;

        this.inline = null;
        this.block = '';

        this.hasAdditonals = false;
        this.additionals = '';

        // Initialise allOf, anyOf and oneOf properties.
        if('allOf' in this.object) {
            this.xOf(this.object.anyOf, '&');
        } else if('anyOf' in this.object) {
            this.xOf(this.object.anyOf, '|');
        } else if('oneOf' in this.object) {
            this.xOf(this.object.oneOf, '|');
        }

        // Everything with a title should be block-type.
        if('title' in this.object) {
            this.type = BLOCK;
            this.st.typesDone.push(this);
        }
    }

    /**
     * Since allOf, anyOf and oneOf function mostly in the same way for our
     * expected output, we've put them all in a single method. Here we simply
     * gather the items and set some extra variables. We call these options
     * "additionals".
     * @param {array} options - The array containing the schemas to be matched
     * against.
     * @param {string} separator - The separator to be inserted between the
     * items (like | or &).
     */
    xOf(options, separator) {
        let items = options.map((schema) => this.st.parseSchema(schema));

        this.hasAdditonals = true;
        this.additionalsItems = items;
        this.additionalsSep = ` ${separator} `;
    }

    parseAdditionals() {
        this.additionals = this.additionalsItems
            .map((t) => {
                t.convert();
                return t.inline;
            })
            .filter((t) => {
                return t !== null
            })
            .join(this.additionalsSep);
    }

    /**
     * Start the conversion to Typescript definitions. We make sure this is
     * only runs once, and starting here we can actually write Typescript
     * definitions. An INLINE type can be put as a type expression (if
     * `this.inline` were Bar, you could use it like `{foo: Bar}`), while a
     * BLOCK type has a block-level definition, which should define a type (for
     * example, `type Foo = string`) but should also have an inline type, to
     * reference it.
     */
    convert() {
        if(this.converted) return;

        this.converted = true;

        // In case of additionals, convert them and create the additionals
        // string.
        if(this.hasAdditonals) {
            this.parseAdditionals();
            this.inline = this.additionals;
        }

        if(this.type == INLINE) {
            this.inlinify();
        } else if(this.type == BLOCK) {
            this.blockify();
        }
    }

    inlinify() {
        this.type = INLINE;
    }

    blockify() {
        this.type = BLOCK;

        let add = '';
        if(this.additionals) {
            let add = ` & (${this.additionals})`;

            if(this.name == this.inline) {
                this.block = `type ${this.name} = ${this.additionals};`;
            } else {
                this.block = `type ${this.name} = ${this.inline}${add};`;
            }
            this.inline = this.name;
        } else {
            this.block = `type ${this.name} = ${this.inline};`;
            this.inline = this.name;
        }
    }
}

class TNumber extends Type {
    inlinify() {
        super.inlinify();
        this.inline = 'number';
    }
}
class TString extends Type {
    inlinify() {
        super.inlinify();
        this.inline = 'string';
    }
}
class TBoolean extends Type {
    inlinify() {
        super.inlinify();
        this.inline = 'boolean';
    }
}
class TNull extends Type {
    inlinify() {
        super.inlinify();
        this.inline = null;
    }
}
class TAny extends Type {
    inlinify() {
        super.inlinify();
        this.inline = 'any';
    }
}

/**
 * The Array Type has a property called `items`, which defines a JSON Schema
 * that the items in the array have to conform to. Note that we do not support
 * an `items` property defined as an array, since that wouldn't make sense in
 * Typescript definitions.
 */
class TArray extends Type {
    inlinify() {
        super.inlinify();

        let itemsType = this.st.parseSchema(this.object.items);
        itemsType.convert();

        // If there's any spaces in the type of the output (for example, if we
        // have a type like `string | number`, we should put parentheses around
        // it to make sure we're actually defining an array with items of
        // _either_ type, not the first time or an array of the last type.
        if(itemsType.inline !== null && itemsType.inline.indexOf(' ') !== -1) {
            this.inline = '(' + itemsType.inline + ')[]';
        } else {
            this.inline = itemsType.inline + '[]';
        }
    }
}

/**
 * What I've called a "TypeList" is pretty much just a simple array of types,
 * like `["string", "number"] and so on.
 */
class TTypeList extends Type {
    inlinify() {
        super.inlinify();

        // TODO: the types should be parsed!
        this.inline = this.object.type.join(' | ');
    }
}

/**
 * This is our most complex type, since it can have a lot of edge-cases. An
 * object type has a property called `properties`, which list several
 * properties that we can expect to see in a valid object. We try to be as
 * type-strict as possible, so we assume that `additionalProperties` is set to
 * `false`.
 */
class TObject extends Type {
    /**
     * The only special thing our constructor does is create a list of types,
     * and parse them. This is so we can later more easily generate definitions
     * from them.
     */
    constructor(st, object) {
        super(st, object);

        this.types = [];

        for(let name in this.object.properties) {
            this.types.push({
                name: name,
                type: this.st.parseSchema(this.object.properties[name]),
                required: this.object.required && this.object.required.indexOf(name) > -1
            });
        }
    }

    /**
     * Since both our `inlinify` and `blockify` methods have mostly identical
     * output (`{property: Type; propertwo: Twope}`), we have extracted
     * creating that into this helper method.
     */
    createTypedef() {
        let typedef = `{\n`;

        for(let t of this.types) {
            // One of the two hardest thing in CompSci.
            // http://martinfowler.com/bliki/TwoHardThings.html
            let optionalityOperator = '?';
            if(t.required) optionalityOperator = '';

            t.type.convert();
            typedef += `${t.name}${optionalityOperator}: ${t.type.inline};\n`;
        }

        typedef += `}`;
        return typedef;
    }

    /**
     * An inline object type is very simple, since it's just the exact output
     * of the `createTypedef` method.
     */
    inlinify() {
        super.inlinify();

        this.inline = this.createTypedef();
        this.block = '';
    }

    /**
     * The block object type is a little tougher. We may not be able to use an
     * interface if we have additionals, but we should prefer it for
     * readability.
     */
    blockify() {
        this.inline = this.name;

        let inlineDef = this.createTypedef();

        if(this.additionals) {
            this.block = [
                `type ${this.name} = ${inlineDef} & (${this.additionals})`
            ].join('\n');
        } else {
            this.block = `interface ${this.name} ${inlineDef}\n\n`;
        }

        this.type = BLOCK;
    }
}

/**
 * The Reference type exists to prevent recursivity problems (a circular type
 * definition, in particular). It stores a reference to another type, which
 * should be added to the Sontyp object's `refsDone` array. We don't actually
 * have to do anything with it until we start _converting_. This way we make
 * sure to never pass a referenced object twice, or get stuck in a loop.
 */
class TReference extends Type {
    convert() {
        if(this.converted) return;

        this.converted = true;
        let referenced = this.st.refsDone[this.object.$ref];

        if(!referenced.converted) {
            referenced.convert();
            referenced.blockify();

            this.st.typesDone.push(referenced);
        }

        this.inline = referenced.inline;
    }
}

/**
 * The Sontyp parser object. It's fairly simple, but requires some shared
 * variables between its separate little parsers, so it's a class.
 */
class Sontyp {
    constructor(root) {
        this.root = root;
        this.typesDone = [];
        this.titledTypesDone = {};

        this.schemasTodo = [];
        this.refsDone = {};
    }

    /**
     * A schema is really anything with a type value or some xOf.
     * @param {object} obj - The schema.
     */
    addSchema(schema) {
        this.schemasTodo.push(schema);
    }

    /**
     * Since json-schema does this cool thing called references, we need to be
     * able to resolve the paths in those and load things from these paths.
     * (really just calls Sontyp.parseSchema)
     * @param {string} ref - The path to the thing.
     * @return {Type}
     */
    addByRef(ref) {
        if(this.root) {
            ref = path.join(this.root, ref);
        }
        if(ref in this.refsDone) {
            return;
        }

        let fs = require("fs");
        let file = fs.readFileSync(ref, {encoding: "utf-8"});

        let obj = JSON.parse(file);
        this.parseSchema(obj);
    }

    /**
     * Since json-schema has a fairly complex typing system, we have this
     * separate function to parse anything that could be described as a type
     * definition - a schema.
     * @param {object} schema - The schema.
     * @return {Type} An instance of one of the different possible Type
     * subclasses.
     */
    parseSchema(schema) {
        let type;

        // If we've already had this schema (the id isn't new), we can simply
        // pass the Type generated then.
        if('id' in schema && schema.id in this.refsDone) {
            return this.refsDone[schema.id];
        }

        // A reference is a special case, the only one which needs to do
        // something else than simply initiate a new Type.
        if('$ref' in schema) {
            this.addByRef(schema.$ref);
            type = new TReference(this, schema);

        // The rest is simple Type initiations, sometimes with some advanced
        // logic, like the TypeList.
        } else if(schema.type && typeof schema.type !== 'string' && schema.type.length) {
            type = new TTypeList(this, schema);

        // Base types
        } else if(schema.type == 'string') {
            type = new TString(this, schema);
        } else if(schema.type == 'number' || schema.type == 'integer') {
            type = new TNumber(this, schema);
        } else if(schema.type == 'boolean') {
            type = new TBoolean(this, schema);
        } else if(schema.type == 'null') {
            type = new TNull(this, schema);

        // Array and objects check for the `items` and `properties` properties
        // as well as a defined type.
        } else if(schema.type == 'array' || 'items' in schema) {
            type = new TArray(this, schema);
        } else if('properties' in schema) {
            type = new TObject(this, schema);

        // And if we have a simple xOf, we just initiate a base Type, that'll
        // work.
        } else if('anyOf' in schema || 'allOf' in schema || 'oneOf' in schema) {
            type = new Type(this, schema);
        }

        // Save the schema in the `refsDone` array if we can reference it.
        if('id' in schema) {
            this.refsDone[schema.id] = type;
        }

        if(typeof type === 'undefined') {
            type = new TAny(this, schema);
            console.log('Sontyp Warning: "any" type detected in schema:\n',
                        schema);
        }

        return type;
    }

    /**
     * The parse method works in three steps:
     * [1] Parse the schemas.
     * [2] Convert the schemas.
     * [3] Generate the text output.
     */
    parse() {
        // [1]
        while(this.schemasTodo.length > 0) {
            let schema = this.schemasTodo.pop();
            let type = this.parseSchema(schema);

            this.typesDone.push(type);
        }

        // [2]
        let typeNames = [];
        let finalTypes = [];
        while(this.typesDone.length > 0) {
            let type = this.typesDone.pop();
            type.convert();

            // We save typeNames so we don't output the same type several
            // times. Bad schema definitions may break from this.
            if(type.type == BLOCK && typeNames.indexOf(type.inline) < 0) {
                typeNames.push(type.inline);
                finalTypes.push(type.block);
            }
        }

        // [3]
        return finalTypes.join('\n\n');
    }
}

function sontyp(json, root) {
    var s = new Sontyp(root);
    s.addSchema(json);
    return s.parse();
}

module.exports = {
    sontyp: sontyp,
    Sontyp: Sontyp
};
module.exports.gulpSontyp = require('./gulp-sontyp');
<commit_msg>Added a try/catch and  custom error message for a common missing reference error<commit_after>'use strict';

var path = require('path');

function tsifyString(str) {
    return str
        .replace(/[^\w\d _]/g, '')
        .replace(/_/g, ' ')
        .replace(/ (\w)/g, s => s.trim().toUpperCase())
        .replace(/^\w/, s => s.toUpperCase());
}

const INLINE = 0;
const BLOCK = 1;

/**
 * The Type class tracks a single type node (a schema), which could be
 * something as simple as a `number`, or as complicated as a `"type": "object"
 * schema. It is compatible with any allOf/anyOf/oneOf construct.
 */
class Type {
    /**
     * Or Type constructor initialises a bunch of different variables, which
     * need to be set for the conversion methods to function properly. Note
     * that no actual Typescript is written until we get to these conversion
     * methods.
     * @param {Sontyp} st - The Sontyp object that we're working from.
     * @param {Object} object - A valid JSON Schema.
     */
    constructor(st, object) {
        this.st = st;
        this.object = object;

        this.name = this.object.title ? tsifyString(this.object.title) : '';

        this.type = INLINE;
        this.converted = false;

        this.inline = null;
        this.block = '';

        this.hasAdditonals = false;
        this.additionals = '';

        // Initialise allOf, anyOf and oneOf properties.
        if('allOf' in this.object) {
            this.xOf(this.object.anyOf, '&');
        } else if('anyOf' in this.object) {
            this.xOf(this.object.anyOf, '|');
        } else if('oneOf' in this.object) {
            this.xOf(this.object.oneOf, '|');
        }

        // Everything with a title should be block-type.
        if('title' in this.object) {
            this.type = BLOCK;
            this.st.typesDone.push(this);
        }
    }

    /**
     * Since allOf, anyOf and oneOf function mostly in the same way for our
     * expected output, we've put them all in a single method. Here we simply
     * gather the items and set some extra variables. We call these options
     * "additionals".
     * @param {array} options - The array containing the schemas to be matched
     * against.
     * @param {string} separator - The separator to be inserted between the
     * items (like | or &).
     */
    xOf(options, separator) {
        let items = options.map((schema) => this.st.parseSchema(schema));

        this.hasAdditonals = true;
        this.additionalsItems = items;
        this.additionalsSep = ` ${separator} `;
    }

    parseAdditionals() {
        this.additionals = this.additionalsItems
            .map((t) => {
                t.convert();
                return t.inline;
            })
            .filter((t) => {
                return t !== null
            })
            .join(this.additionalsSep);
    }

    /**
     * Start the conversion to Typescript definitions. We make sure this is
     * only runs once, and starting here we can actually write Typescript
     * definitions. An INLINE type can be put as a type expression (if
     * `this.inline` were Bar, you could use it like `{foo: Bar}`), while a
     * BLOCK type has a block-level definition, which should define a type (for
     * example, `type Foo = string`) but should also have an inline type, to
     * reference it.
     */
    convert() {
        if(this.converted) return;

        this.converted = true;

        // In case of additionals, convert them and create the additionals
        // string.
        if(this.hasAdditonals) {
            this.parseAdditionals();
            this.inline = this.additionals;
        }

        if(this.type == INLINE) {
            this.inlinify();
        } else if(this.type == BLOCK) {
            this.blockify();
        }
    }

    inlinify() {
        this.type = INLINE;
    }

    blockify() {
        this.type = BLOCK;

        let add = '';
        if(this.additionals) {
            let add = ` & (${this.additionals})`;

            if(this.name == this.inline) {
                this.block = `type ${this.name} = ${this.additionals};`;
            } else {
                this.block = `type ${this.name} = ${this.inline}${add};`;
            }
            this.inline = this.name;
        } else {
            this.block = `type ${this.name} = ${this.inline};`;
            this.inline = this.name;
        }
    }
}

class TNumber extends Type {
    inlinify() {
        super.inlinify();
        this.inline = 'number';
    }
}
class TString extends Type {
    inlinify() {
        super.inlinify();
        this.inline = 'string';
    }
}
class TBoolean extends Type {
    inlinify() {
        super.inlinify();
        this.inline = 'boolean';
    }
}
class TNull extends Type {
    inlinify() {
        super.inlinify();
        this.inline = null;
    }
}
class TAny extends Type {
    inlinify() {
        super.inlinify();
        this.inline = 'any';
    }
}

/**
 * The Array Type has a property called `items`, which defines a JSON Schema
 * that the items in the array have to conform to. Note that we do not support
 * an `items` property defined as an array, since that wouldn't make sense in
 * Typescript definitions.
 */
class TArray extends Type {
    inlinify() {
        super.inlinify();

        let itemsType = this.st.parseSchema(this.object.items);
        itemsType.convert();

        // If there's any spaces in the type of the output (for example, if we
        // have a type like `string | number`, we should put parentheses around
        // it to make sure we're actually defining an array with items of
        // _either_ type, not the first time or an array of the last type.
        if(itemsType.inline !== null && itemsType.inline.indexOf(' ') !== -1) {
            this.inline = '(' + itemsType.inline + ')[]';
        } else {
            this.inline = itemsType.inline + '[]';
        }
    }
}

/**
 * What I've called a "TypeList" is pretty much just a simple array of types,
 * like `["string", "number"] and so on.
 */
class TTypeList extends Type {
    inlinify() {
        super.inlinify();

        // TODO: the types should be parsed!
        this.inline = this.object.type.join(' | ');
    }
}

/**
 * This is our most complex type, since it can have a lot of edge-cases. An
 * object type has a property called `properties`, which list several
 * properties that we can expect to see in a valid object. We try to be as
 * type-strict as possible, so we assume that `additionalProperties` is set to
 * `false`.
 */
class TObject extends Type {
    /**
     * The only special thing our constructor does is create a list of types,
     * and parse them. This is so we can later more easily generate definitions
     * from them.
     */
    constructor(st, object) {
        super(st, object);

        this.types = [];

        for(let name in this.object.properties) {
            this.types.push({
                name: name,
                type: this.st.parseSchema(this.object.properties[name]),
                required: this.object.required && this.object.required.indexOf(name) > -1
            });
        }
    }

    /**
     * Since both our `inlinify` and `blockify` methods have mostly identical
     * output (`{property: Type; propertwo: Twope}`), we have extracted
     * creating that into this helper method.
     */
    createTypedef() {
        let typedef = `{\n`;

        for(let t of this.types) {
            // One of the two hardest thing in CompSci.
            // http://martinfowler.com/bliki/TwoHardThings.html
            let optionalityOperator = '?';
            if(t.required) optionalityOperator = '';

            t.type.convert();
            typedef += `${t.name}${optionalityOperator}: ${t.type.inline};\n`;
        }

        typedef += `}`;
        return typedef;
    }

    /**
     * An inline object type is very simple, since it's just the exact output
     * of the `createTypedef` method.
     */
    inlinify() {
        super.inlinify();

        this.inline = this.createTypedef();
        this.block = '';
    }

    /**
     * The block object type is a little tougher. We may not be able to use an
     * interface if we have additionals, but we should prefer it for
     * readability.
     */
    blockify() {
        this.inline = this.name;

        let inlineDef = this.createTypedef();

        if(this.additionals) {
            this.block = [
                `type ${this.name} = ${inlineDef} & (${this.additionals})`
            ].join('\n');
        } else {
            this.block = `interface ${this.name} ${inlineDef}\n\n`;
        }

        this.type = BLOCK;
    }
}

/**
 * The Reference type exists to prevent recursivity problems (a circular type
 * definition, in particular). It stores a reference to another type, which
 * should be added to the Sontyp object's `refsDone` array. We don't actually
 * have to do anything with it until we start _converting_. This way we make
 * sure to never pass a referenced object twice, or get stuck in a loop.
 */
class TReference extends Type {
    convert() {
        if(this.converted) return;

        this.converted = true;
        let referenced = this.st.refsDone[this.object.$ref];

        try{
            if(!referenced.converted) {
                referenced.convert();
                referenced.blockify();

                this.st.typesDone.push(referenced);
            }

            this.inline = referenced.inline;
        } catch(error){
            console.log(error);
            console.log('Sontyp Error: Check if this referenced type exists:');
            console.log(this.object);
            console.log('');
        }
    }
}

/**
 * The Sontyp parser object. It's fairly simple, but requires some shared
 * variables between its separate little parsers, so it's a class.
 */
class Sontyp {
    constructor(root) {
        this.root = root;
        this.typesDone = [];
        this.titledTypesDone = {};

        this.schemasTodo = [];
        this.refsDone = {};
    }

    /**
     * A schema is really anything with a type value or some xOf.
     * @param {object} obj - The schema.
     */
    addSchema(schema) {
        this.schemasTodo.push(schema);
    }

    /**
     * Since json-schema does this cool thing called references, we need to be
     * able to resolve the paths in those and load things from these paths.
     * (really just calls Sontyp.parseSchema)
     * @param {string} ref - The path to the thing.
     * @return {Type}
     */
    addByRef(ref) {
        if(this.root) {
            ref = path.join(this.root, ref);
        }
        if(ref in this.refsDone) {
            return;
        }

        let fs = require("fs");
        let file = fs.readFileSync(ref, {encoding: "utf-8"});

        let obj = JSON.parse(file);
        this.parseSchema(obj);
    }

    /**
     * Since json-schema has a fairly complex typing system, we have this
     * separate function to parse anything that could be described as a type
     * definition - a schema.
     * @param {object} schema - The schema.
     * @return {Type} An instance of one of the different possible Type
     * subclasses.
     */
    parseSchema(schema) {
        let type;

        // If we've already had this schema (the id isn't new), we can simply
        // pass the Type generated then.
        if('id' in schema && schema.id in this.refsDone) {
            return this.refsDone[schema.id];
        }

        // A reference is a special case, the only one which needs to do
        // something else than simply initiate a new Type.
        if('$ref' in schema) {
            this.addByRef(schema.$ref);
            type = new TReference(this, schema);

        // The rest is simple Type initiations, sometimes with some advanced
        // logic, like the TypeList.
        } else if(schema.type && typeof schema.type !== 'string' && schema.type.length) {
            type = new TTypeList(this, schema);

        // Base types
        } else if(schema.type == 'string') {
            type = new TString(this, schema);
        } else if(schema.type == 'number' || schema.type == 'integer') {
            type = new TNumber(this, schema);
        } else if(schema.type == 'boolean') {
            type = new TBoolean(this, schema);
        } else if(schema.type == 'null') {
            type = new TNull(this, schema);

        // Array and objects check for the `items` and `properties` properties
        // as well as a defined type.
        } else if(schema.type == 'array' || 'items' in schema) {
            type = new TArray(this, schema);
        } else if('properties' in schema) {
            type = new TObject(this, schema);

        // And if we have a simple xOf, we just initiate a base Type, that'll
        // work.
        } else if('anyOf' in schema || 'allOf' in schema || 'oneOf' in schema) {
            type = new Type(this, schema);
        }

        // Save the schema in the `refsDone` array if we can reference it.
        if('id' in schema) {
            this.refsDone[schema.id] = type;
        }

        if(typeof type === 'undefined') {
            type = new TAny(this, schema);
            console.log('Sontyp Warning: "any" type detected in schema:\n',
                        schema);
        }

        return type;
    }

    /**
     * The parse method works in three steps:
     * [1] Parse the schemas.
     * [2] Convert the schemas.
     * [3] Generate the text output.
     */
    parse() {
        // [1]
        while(this.schemasTodo.length > 0) {
            let schema = this.schemasTodo.pop();
            let type = this.parseSchema(schema);

            this.typesDone.push(type);
        }

        // [2]
        let typeNames = [];
        let finalTypes = [];
        while(this.typesDone.length > 0) {
            let type = this.typesDone.pop();
            type.convert();

            // We save typeNames so we don't output the same type several
            // times. Bad schema definitions may break from this.
            if(type.type == BLOCK && typeNames.indexOf(type.inline) < 0) {
                typeNames.push(type.inline);
                finalTypes.push(type.block);
            }
        }

        // [3]
        return finalTypes.join('\n\n');
    }
}

function sontyp(json, root) {
    var s = new Sontyp(root);
    s.addSchema(json);
    return s.parse();
}

module.exports = {
    sontyp: sontyp,
    Sontyp: Sontyp
};
module.exports.gulpSontyp = require('./gulp-sontyp');
<|endoftext|>


<commit_before>/**
 * source.js
 * Andrea Tino - 2015
 */

/**
 * Extracts parts from the source code basing on line number and column number.
 * _src: Source code to provide
 */
module.exports = function(_src) {
  var src = null;

  return {

  };
};
<commit_msg>Update source.js<commit_after>/**
 * source.js
 * Andrea Tino - 2015
 */

/**
 * Extracts parts from the source code basing on line number and column number.
 * _src: Source code to provide
 */
module.exports = function(_src) {
  var validateSrc = function(src) {
    if (!src) {
      throw 'Error: src cannot be null or undefined!';
    }
    return src;
  };

  var src = validateSrc(_src);

  return {
    /**
     * Extracts source code at provided line number and col number.
     * rs: Row start.
     * re: Row end.
     * cs: Col start.
     * ce: Col end.
     */
    at: function(rs, re, cs, ce) {

    }
  };
};
<|endoftext|>