<commit_before>var EventEmitter = require('events').EventEmitter;
var util = require('util');
var logger = require('pomelo-logger').getLogger('pomelo', __filename);
var utils = require('../../util/utils');

var FRONTEND_SESSION_FIELDS = ['id', 'frontendId', 'uid', '__sessionService__'];
var EXPORTED_SESSION_FIELDS = ['id', 'frontendId', 'uid', 'settings'];

var ST_INITED = 0;
var ST_CLOSED = 1;

/**
 * Session 服务 维持每一个客户端连接的 session
 *
 * Session 服务 通过 session 组件创建 并且 只在
 * 前端服务端可用. 你可以通过 `app.get('sessionService')` or `app.sessionService`
 * 在前端服务器访问该服务
 *
 * @param {Object} opts constructor parameters
 * @class
 * @constructor
 */
var SessionService = function(opts) {
  opts = opts || {};
  this.singleSession = opts.singleSession;
  this.sessions = {};     // sid -> session
  this.uidMap = {};       // uid -> sessions
};

module.exports = SessionService;

/**
 * 创建并返回内置 session.
 *
 * @param {Integer} sid uniqe id for the internal session
 * @param {String} frontendId frontend server in which the internal session is created
 * @param {Object} socket the underlying socket would be held by the internal session
 *
 * @return {Session}
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.create = function(sid, frontendId, socket) {
  var session = new Session(sid, frontendId, socket, this);
  this.sessions[session.id] = session;

  return session;
};

/**
 * 绑定 session 与用户 id
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.bind = function(sid, uid, cb) {
  var session = this.sessions[sid];

  if(!session) {
    process.nextTick(function() {
      cb(new Error('session does not exist, sid: ' + sid));
    });
    return;
  }

  if(session.uid) {
    if(session.uid === uid) {
      // already bound with the same uid
      cb();
      return;
    }

    // already bound with other uid
    process.nextTick(function() {
      cb(new Error('session has already bound with ' + session.uid));
    });
    return;
  }

  var sessions = this.uidMap[uid];

  if(!!this.singleSession && !!sessions) {
    process.nextTick(function() {
      cb(new Error('singleSession is enabled, and session has already bound with uid: ' + uid));
    });
    return;
  }

  if(!sessions) {
    sessions = this.uidMap[uid] = [];
  }

  for(var i=0, l=sessions.length; i<l; i++) {
    // session has binded with the uid
    if(sessions[i].id === session.id) {
      process.nextTick(cb);
      return;
    }
  }
  sessions.push(session);

  session.bind(uid);

  if(cb) {
    process.nextTick(cb);
  }
};

/**
 * 解除 session 与用户 id 的绑定
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.unbind = function(sid, uid, cb) {
  var session = this.sessions[sid];

  if(!session) {
    process.nextTick(function() {
      cb(new Error('session does not exist, sid: ' + sid));
    });
    return;
  }

  if(!session.uid || session.uid !== uid) {
    process.nextTick(function() {
      cb(new Error('session has not bind with ' + session.uid));
    });
    return;
  }

  var sessions = this.uidMap[uid], sess;
  if(sessions) {
    for(var i=0, l=sessions.length; i<l; i++) {
      sess = sessions[i];
      if(sess.id === sid) {
        sessions.splice(i, 1);
        break;
      }
    }

    if(sessions.length === 0) {
      delete this.uidMap[uid];
    }
  }
  session.unbind(uid);

  if(cb) {
    process.nextTick(cb);
  }
};

/**
 * 通过 session id 获取 session
 *
 * @param {Number} id The session id
 * @return {Session}
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.get = function(sid) {
  return this.sessions[sid];
};

/**
 * 通过用户 id 获取 sessions
 *
 * @param {Number} uid User id associated with the session
 * @return {Array} list of session binded with the uid
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.getByUid = function(uid) {
  return this.uidMap[uid];
};

/**
 * 根据 session id 移除 session
 *
 * @param {Number} sid The session id
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.remove = function(sid) {
  var session = this.sessions[sid];
  if(session) {
    var uid = session.uid;
    delete this.sessions[session.id];

    var sessions = this.uidMap[uid];
    if(!sessions) {
      return;
    }

    for(var i=0, l=sessions.length; i<l; i++) {
      if(sessions[i].id === sid) {
        sessions.splice(i, 1);
        if(sessions.length === 0) {
          delete this.uidMap[uid];
        }
        break;
      }
    }
  }
};

/**
 * Import the key/value into session.
 *
 * @api private
 */
SessionService.prototype.import = function(sid, key, value, cb) {
  var session = this.sessions[sid];
  if(!session) {
    utils.invokeCallback(cb, new Error('session does not exist, sid: ' + sid));
    return;
  }
  session.set(key, value);
  utils.invokeCallback(cb);
};

/**
 * Import new value for the existed session.
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.importAll = function(sid, settings, cb) {
  var session = this.sessions[sid];
  if(!session) {
    utils.invokeCallback(cb, new Error('session does not exist, sid: ' + sid));
    return;
  }

  for(var f in settings) {
    session.set(f, settings[f]);
  }
  utils.invokeCallback(cb);
};

/**
 * Kick all the session offline under the user id.
 *
 * @param {Number}   uid user id asscociated with the session
 * @param {Function} cb  callback function
 *
 * @memberOf SessionService
 */
SessionService.prototype.kick = function(uid, reason, cb) {
  // compatible for old kick(uid, cb);
  if(typeof reason === 'function') {
    cb = reason;
    reason = 'kick';
  }
  var sessions = this.getByUid(uid);

  if(sessions) {
    // notify client
    var sids = [];
    var self = this;
    sessions.forEach(function(session) {
      sids.push(session.id);
    });

    sids.forEach(function(sid) {
      self.sessions[sid].closed(reason);
    });

    process.nextTick(function() {
      utils.invokeCallback(cb);
    });
  } else {
    process.nextTick(function() {
      utils.invokeCallback(cb);
    });
  }
};

/**
 * Kick a user offline by session id.
 *
 * @param {Number}   sid session id
 * @param {Function} cb  callback function
 *
 * @memberOf SessionService
 */
SessionService.prototype.kickBySessionId = function(sid, cb) {
  var session = this.get(sid);

  if(session) {
    // notify client
    session.closed('kick');
    process.nextTick(function() {
      utils.invokeCallback(cb);
    });
  } else {
    process.nextTick(function() {
      utils.invokeCallback(cb);
    });
  }
};

/**
 * Get client remote address by session id.
 *
 * @param {Number}   sid session id
 * @return {Object} remote address of client
 *
 * @memberOf SessionService
 */
 SessionService.prototype.getClientAddressBySessionId = function(sid) {
   var session = this.get(sid);
   if(session) {
      var socket = session.__socket__;
      return socket.remoteAddress;
   } else {
      return null;
   }
 };

/**
 * Send message to the client by session id.
 *
 * @param {String} sid session id
 * @param {Object} msg message to send
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.sendMessage = function(sid, msg) {
  var session = this.sessions[sid];

  if(!session) {
    logger.debug('Fail to send message for non-existing session, sid: ' + sid + ' msg: ' + msg);
    return false;
  }

  return send(this, session, msg);
};

/**
 * Send message to the client by user id.
 *
 * @param {String} uid userId
 * @param {Object} msg message to send
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.sendMessageByUid = function(uid, msg) {
  var sessions = this.uidMap[uid];

  if(!sessions) {
    logger.debug('fail to send message by uid for non-existing session. uid: %j',
        uid);
    return false;
  }

  for(var i=0, l=sessions.length; i<l; i++) {
    send(this, sessions[i], msg);
  }
};

/**
 * Iterate all the session in the session 服务.
 *
 * @param  {Function} cb callback function to fetch session
 * @api private
 */
SessionService.prototype.forEachSession = function(cb) {
  for(var sid in this.sessions) {
    cb(this.sessions[sid]);
  }
};

/**
 * Iterate all the binded session in the session 服务.
 *
 * @param  {Function} cb callback function to fetch session
 * @api private
 */
SessionService.prototype.forEachBindedSession = function(cb) {
  var i, l, sessions;
  for(var uid in this.uidMap) {
    sessions = this.uidMap[uid];
    for(i=0, l=sessions.length; i<l; i++) {
      cb(sessions[i]);
    }
  }
};

/**
 * Get sessions' quantity in specified server.
 *
 */
SessionService.prototype.getSessionsCount = function() {
  return utils.size(this.sessions);
};

/**
 * Send message to the client that associated with the session.
 *
 * @api private
 */
var send = function(service, session, msg) {
  session.send(msg);

  return true;
};

/**
 * Session maintains the relationship between client connection and user information.
 * There is a session associated with each client connection. And it should bind to a
 * user id after the client passes the identification.
 *
 * Session is created in frontend server and should not be accessed in handler.
 * There is a proxy class called BackendSession in backend servers and FrontendSession
 * in frontend servers.
 */
var Session = function(sid, frontendId, socket, service) {
  EventEmitter.call(this);
  this.id = sid;          // r
  this.frontendId = frontendId; // r
  this.uid = null;        // r
  this.settings = {};

  // private
  this.__socket__ = socket;
  this.__sessionService__ = service;
  this.__state__ = ST_INITED;
};

util.inherits(Session, EventEmitter);

/*
 * Export current session as frontend session.
 */
Session.prototype.toFrontendSession = function() {
  return new FrontendSession(this);
};

/**
 * Bind the session with the the uid.
 *
 * @param {Number} uid User id
 * @api public
 */
Session.prototype.bind = function(uid) {
  this.uid = uid;
  this.emit('bind', uid);
};

/**
 * Unbind the session with the the uid.
 *
 * @param {Number} uid User id
 * @api private
 */
Session.prototype.unbind = function(uid) {
  this.uid = null;
  this.emit('unbind', uid);
};

/**
 * Set value for the session.
 *
 * @param {String} key session key
 * @param {Object} value session value
 * @api public
 */
Session.prototype.set = function(key, value) {
  this.settings[key] = value;
};

/**
 * Get value from the session.
 *
 * @param {String} key session key
 * @return {Object} value associated with session key
 * @api public
 */
Session.prototype.get = function(key) {
  return this.settings[key];
};

/**
 * Send message to the session.
 *
 * @param  {Object} msg final message sent to client
 */
Session.prototype.send = function(msg) {
  this.__socket__.send(msg);
};

/**
 * Send message to the session in batch.
 *
 * @param  {Array} msgs list of message
 */
Session.prototype.sendBatch = function(msgs) {
  this.__socket__.sendBatch(msgs);
};

/**
 * Closed callback for the session which would disconnect client in next tick.
 *
 * @api public
 */
Session.prototype.closed = function(reason) {
  logger.debug('session on [%s] is closed with session id: %s', this.frontendId, this.id);
  if(this.__state__ === ST_CLOSED) {
    return;
  }
  this.__state__ = ST_CLOSED;
  this.__sessionService__.remove(this.id);
  this.emit('closed', this.toFrontendSession(), reason);
  this.__socket__.emit('closing', reason);

  var self = this;
  // give a chance to send disconnect message to client

  process.nextTick(function() {
    self.__socket__.disconnect();
  });
};

/**
 * Frontend session for frontend server.
 */
var FrontendSession = function(session) {
  EventEmitter.call(this);
  clone(session, this, FRONTEND_SESSION_FIELDS);
  // deep copy for settings
  this.settings = dclone(session.settings);
  this.__session__ = session;
};

util.inherits(FrontendSession, EventEmitter);

FrontendSession.prototype.bind = function(uid, cb) {
  var self = this;
  this.__sessionService__.bind(this.id, uid, function(err) {
    if(!err) {
      self.uid = uid;
    }
    utils.invokeCallback(cb, err);
  });
};

FrontendSession.prototype.unbind = function(uid, cb) {
  var self = this;
  this.__sessionService__.unbind(this.id, uid, function(err) {
    if(!err) {
      self.uid = null;
    }
    utils.invokeCallback(cb, err);
  });
};

FrontendSession.prototype.set = function(key, value) {
  this.settings[key] = value;
};

FrontendSession.prototype.get = function(key) {
  return this.settings[key];
};

FrontendSession.prototype.push = function(key, cb) {
  this.__sessionService__.import(this.id, key, this.get(key), cb);
};

FrontendSession.prototype.pushAll = function(cb) {
  this.__sessionService__.importAll(this.id, this.settings, cb);
};

FrontendSession.prototype.on = function(event, listener) {
  EventEmitter.prototype.on.call(this, event, listener);
  this.__session__.on(event, listener);
};

/**
 * Export the key/values for serialization.
 *
 * @api private
 */
FrontendSession.prototype.export = function() {
  var res = {};
  clone(this, res, EXPORTED_SESSION_FIELDS);
  return res;
};

var clone = function(src, dest, includes) {
  var f;
  for(var i=0, l=includes.length; i<l; i++) {
    f = includes[i];
    dest[f] = src[f];
  }
};

var dclone = function(src) {
  var res = {};
  for(var f in src) {
    res[f] = src[f];
  }
  return res;
};
<commit_msg>注释 sessionService<commit_after>var EventEmitter = require('events').EventEmitter;
var util = require('util');
var logger = require('pomelo-logger').getLogger('pomelo', __filename);
var utils = require('../../util/utils');

var FRONTEND_SESSION_FIELDS = ['id', 'frontendId', 'uid', '__sessionService__'];
var EXPORTED_SESSION_FIELDS = ['id', 'frontendId', 'uid', 'settings'];

var ST_INITED = 0;
var ST_CLOSED = 1;

/**
 * Session 服务 维持每一个客户端连接的 session
 *
 * Session 服务 通过 session 组件创建 并且 只在
 * 前端服务端可用. 你可以通过 `app.get('sessionService')` or `app.sessionService`
 * 在前端服务器访问该服务
 *
 * @param {Object} opts constructor parameters
 * @class
 * @constructor
 */
var SessionService = function(opts) {
  opts = opts || {};
  this.singleSession = opts.singleSession;
  this.sessions = {};     // sid -> session
  this.uidMap = {};       // uid -> sessions
};

module.exports = SessionService;

/**
 * 创建并返回内置 session.
 *
 * @param {Integer} sid uniqe id for the internal session
 * @param {String} frontendId frontend server in which the internal session is created
 * @param {Object} socket the underlying socket would be held by the internal session
 *
 * @return {Session}
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.create = function(sid, frontendId, socket) {
  var session = new Session(sid, frontendId, socket, this);
  this.sessions[session.id] = session;

  return session;
};

/**
 * 绑定 session 与用户 id
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.bind = function(sid, uid, cb) {
  var session = this.sessions[sid];

  if(!session) {
    process.nextTick(function() {
      cb(new Error('session does not exist, sid: ' + sid));
    });
    return;
  }

  if(session.uid) {
    if(session.uid === uid) {
      // already bound with the same uid
      cb();
      return;
    }

    // already bound with other uid
    process.nextTick(function() {
      cb(new Error('session has already bound with ' + session.uid));
    });
    return;
  }

  var sessions = this.uidMap[uid];

  if(!!this.singleSession && !!sessions) {
    process.nextTick(function() {
      cb(new Error('singleSession is enabled, and session has already bound with uid: ' + uid));
    });
    return;
  }

  if(!sessions) {
    sessions = this.uidMap[uid] = [];
  }

  for(var i=0, l=sessions.length; i<l; i++) {
    // session has binded with the uid
    if(sessions[i].id === session.id) {
      process.nextTick(cb);
      return;
    }
  }
  sessions.push(session);

  session.bind(uid);

  if(cb) {
    process.nextTick(cb);
  }
};

/**
 * 解除 session 与用户 id 的绑定
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.unbind = function(sid, uid, cb) {
  var session = this.sessions[sid];

  if(!session) {
    process.nextTick(function() {
      cb(new Error('session does not exist, sid: ' + sid));
    });
    return;
  }

  if(!session.uid || session.uid !== uid) {
    process.nextTick(function() {
      cb(new Error('session has not bind with ' + session.uid));
    });
    return;
  }

  var sessions = this.uidMap[uid], sess;
  if(sessions) {
    for(var i=0, l=sessions.length; i<l; i++) {
      sess = sessions[i];
      if(sess.id === sid) {
        sessions.splice(i, 1);
        break;
      }
    }

    if(sessions.length === 0) {
      delete this.uidMap[uid];
    }
  }
  session.unbind(uid);

  if(cb) {
    process.nextTick(cb);
  }
};

/**
 * 通过 session id 获取 session
 *
 * @param {Number} id The session id
 * @return {Session}
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.get = function(sid) {
  return this.sessions[sid];
};

/**
 * 通过用户 id 获取 sessions
 *
 * @param {Number} uid User id associated with the session
 * @return {Array} list of session binded with the uid
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.getByUid = function(uid) {
  return this.uidMap[uid];
};

/**
 * 根据 session id 移除 session
 *
 * @param {Number} sid The session id
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.remove = function(sid) {
  var session = this.sessions[sid];
  if(session) {
    var uid = session.uid;
    delete this.sessions[session.id];

    var sessions = this.uidMap[uid];
    if(!sessions) {
      return;
    }

    for(var i=0, l=sessions.length; i<l; i++) {
      if(sessions[i].id === sid) {
        sessions.splice(i, 1);
        if(sessions.length === 0) {
          delete this.uidMap[uid];
        }
        break;
      }
    }
  }
};

/**
 * Import the key/value into session.
 *
 * @api private
 */
SessionService.prototype.import = function(sid, key, value, cb) {
  var session = this.sessions[sid];
  if(!session) {
    utils.invokeCallback(cb, new Error('session does not exist, sid: ' + sid));
    return;
  }
  session.set(key, value);
  utils.invokeCallback(cb);
};

/**
 * Import new value for the existed session.
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.importAll = function(sid, settings, cb) {
  var session = this.sessions[sid];
  if(!session) {
    utils.invokeCallback(cb, new Error('session does not exist, sid: ' + sid));
    return;
  }

  for(var f in settings) {
    session.set(f, settings[f]);
  }
  utils.invokeCallback(cb);
};

/**
 * Kick all the session offline under the user id.
 *
 * @param {Number}   uid user id asscociated with the session
 * @param {Function} cb  callback function
 *
 * @memberOf SessionService
 */
SessionService.prototype.kick = function(uid, reason, cb) {
  // compatible for old kick(uid, cb);
  if(typeof reason === 'function') {
    cb = reason;
    reason = 'kick';
  }
  var sessions = this.getByUid(uid);

  if(sessions) {
    // notify client
    var sids = [];
    var self = this;
    sessions.forEach(function(session) {
      sids.push(session.id);
    });

    sids.forEach(function(sid) {
      self.sessions[sid].closed(reason);
    });

    process.nextTick(function() {
      utils.invokeCallback(cb);
    });
  } else {
    process.nextTick(function() {
      utils.invokeCallback(cb);
    });
  }
};

/**
 * Kick a user offline 通过 session id.
 *
 * @param {Number}   sid session id
 * @param {Function} cb  callback function
 *
 * @memberOf SessionService
 */
SessionService.prototype.kickBySessionId = function(sid, cb) {
  var session = this.get(sid);

  if(session) {
    // notify client
    session.closed('kick');
    process.nextTick(function() {
      utils.invokeCallback(cb);
    });
  } else {
    process.nextTick(function() {
      utils.invokeCallback(cb);
    });
  }
};

/**
 * Get client remote address by session id.
 *
 * @param {Number}   sid session id
 * @return {Object} remote address of client
 *
 * @memberOf SessionService
 */
 SessionService.prototype.getClientAddressBySessionId = function(sid) {
   var session = this.get(sid);
   if(session) {
      var socket = session.__socket__;
      return socket.remoteAddress;
   } else {
      return null;
   }
 };

/**
 * Send message to the client by session id.
 *
 * @param {String} sid session id
 * @param {Object} msg message to send
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.sendMessage = function(sid, msg) {
  var session = this.sessions[sid];

  if(!session) {
    logger.debug('Fail to send message for non-existing session, sid: ' + sid + ' msg: ' + msg);
    return false;
  }

  return send(this, session, msg);
};

/**
 * Send message to the client by user id.
 *
 * @param {String} uid userId
 * @param {Object} msg message to send
 *
 * @memberOf SessionService
 * @api private
 */
SessionService.prototype.sendMessageByUid = function(uid, msg) {
  var sessions = this.uidMap[uid];

  if(!sessions) {
    logger.debug('fail to send message by uid for non-existing session. uid: %j',
        uid);
    return false;
  }

  for(var i=0, l=sessions.length; i<l; i++) {
    send(this, sessions[i], msg);
  }
};

/**
 * Iterate all the session in the session 服务.
 *
 * @param  {Function} cb callback function to fetch session
 * @api private
 */
SessionService.prototype.forEachSession = function(cb) {
  for(var sid in this.sessions) {
    cb(this.sessions[sid]);
  }
};

/**
 * Iterate all the binded session in the session 服务.
 *
 * @param  {Function} cb callback function to fetch session
 * @api private
 */
SessionService.prototype.forEachBindedSession = function(cb) {
  var i, l, sessions;
  for(var uid in this.uidMap) {
    sessions = this.uidMap[uid];
    for(i=0, l=sessions.length; i<l; i++) {
      cb(sessions[i]);
    }
  }
};

/**
 * Get sessions' quantity in specified server.
 *
 */
SessionService.prototype.getSessionsCount = function() {
  return utils.size(this.sessions);
};

/**
 * Send message to the client that associated with the session.
 *
 * @api private
 */
var send = function(service, session, msg) {
  session.send(msg);

  return true;
};

/**
 * Session maintains the relationship between client connection and user information.
 * There is a session associated with each client connection. And it should bind to a
 * user id after the client passes the identification.
 *
 * Session is created in frontend server and should not be accessed in handler.
 * There is a proxy class called BackendSession in backend servers and FrontendSession
 * in frontend servers.
 */
var Session = function(sid, frontendId, socket, service) {
  EventEmitter.call(this);
  this.id = sid;          // r
  this.frontendId = frontendId; // r
  this.uid = null;        // r
  this.settings = {};

  // private
  this.__socket__ = socket;
  this.__sessionService__ = service;
  this.__state__ = ST_INITED;
};

util.inherits(Session, EventEmitter);

/*
 * 导出 当前 session 作为 前端 session.
 */
Session.prototype.toFrontendSession = function() {
  return new FrontendSession(this);
};

/**
 * 绑定 session 与 uid.
 *
 * @param {Number} uid User id
 * @api public
 */
Session.prototype.bind = function(uid) {
  this.uid = uid;
  this.emit('bind', uid);
};

/**
 * 取消 session 与 uid 的绑定.
 *
 * @param {Number} uid User id
 * @api private
 */
Session.prototype.unbind = function(uid) {
  this.uid = null;
  this.emit('unbind', uid);
};

/**
 * 设置 session 上某个 key 的值
 *
 * @param {String} key session key
 * @param {Object} value session value
 * @api public
 */
Session.prototype.set = function(key, value) {
  this.settings[key] = value;
};

/**
 * 获得 session 上某个 key 的值
 *
 * @param {String} key session key
 * @return {Object} value associated with session key
 * @api public
 */
Session.prototype.get = function(key) {
  return this.settings[key];
};

/**
 * Send message to the session.
 *
 * @param  {Object} msg final message sent to client
 */
Session.prototype.send = function(msg) {
  this.__socket__.send(msg);
};

/**
 * Send message to the session in batch.
 *
 * @param  {Array} msgs list of message
 */
Session.prototype.sendBatch = function(msgs) {
  this.__socket__.sendBatch(msgs);
};

/**
 * 关闭 callback for the session 这会断开客户端连接 (in next tick).
 *
 * @api public
 */
Session.prototype.closed = function(reason) {
  logger.debug('session on [%s] is closed with session id: %s', this.frontendId, this.id);
  if(this.__state__ === ST_CLOSED) {
    return;
  }
  this.__state__ = ST_CLOSED;
  this.__sessionService__.remove(this.id);
  this.emit('closed', this.toFrontendSession(), reason);
  this.__socket__.emit('closing', reason);

  var self = this;
  // 给个机会发送断开连接的消息给客户端

  process.nextTick(function() {
    self.__socket__.disconnect();
  });
};

/**
 * 前端 session for 前端 server.
 */
var FrontendSession = function(session) {
  EventEmitter.call(this);
  clone(session, this, FRONTEND_SESSION_FIELDS);
  // deep copy for settings
  this.settings = dclone(session.settings);
  this.__session__ = session;
};

util.inherits(FrontendSession, EventEmitter);

FrontendSession.prototype.bind = function(uid, cb) {
  var self = this;
  this.__sessionService__.bind(this.id, uid, function(err) {
    if(!err) {
      self.uid = uid;
    }
    utils.invokeCallback(cb, err);
  });
};

FrontendSession.prototype.unbind = function(uid, cb) {
  var self = this;
  this.__sessionService__.unbind(this.id, uid, function(err) {
    if(!err) {
      self.uid = null;
    }
    utils.invokeCallback(cb, err);
  });
};

FrontendSession.prototype.set = function(key, value) {
  this.settings[key] = value;
};

FrontendSession.prototype.get = function(key) {
  return this.settings[key];
};

FrontendSession.prototype.push = function(key, cb) {
  this.__sessionService__.import(this.id, key, this.get(key), cb);
};

FrontendSession.prototype.pushAll = function(cb) {
  this.__sessionService__.importAll(this.id, this.settings, cb);
};

FrontendSession.prototype.on = function(event, listener) {
  EventEmitter.prototype.on.call(this, event, listener);
  this.__session__.on(event, listener);
};

/**
 * 导出 key/values for 序列化.
 *
 * @api private
 */
FrontendSession.prototype.export = function() {
  var res = {};
  clone(this, res, EXPORTED_SESSION_FIELDS);
  return res;
};

var clone = function(src, dest, includes) {
  var f;
  for(var i=0, l=includes.length; i<l; i++) {
    f = includes[i];
    dest[f] = src[f];
  }
};

var dclone = function(src) {
  var res = {};
  for(var f in src) {
    res[f] = src[f];
  }
  return res;
};
<|endoftext|>


<commit_before>'use strict';

var React = require('react');
var Tether = require('tether');

var Popover = React.createClass({
  displayName: 'Popover',

  componentWillMount: function() {
    popoverContainer = document.createElement('span');
    popoverContainer.className = 'datepicker__container';

    this._popoverElement = popoverContainer;

    document.querySelector('body').appendChild(this._popoverElement);
  },

  componentDidMount: function() {
    this._renderPopover();
  },

  componentDidUpdate: function() {
    this._renderPopover();
  },

  _popoverComponent: function() {
    var className = this.props.className;
    return (
      React.createElement("div", {className: className},
        this.props.children
      )
    );
  },

  _tetherOptions: function() {
    return {
      element: this._popoverElement,
      target: this.getDOMNode().parentElement,
      attachment: 'top left',
      targetAttachment: 'bottom left',
      targetOffset: '10px 0',
      optimizations: {
        moveElement: false // always moves to <body> anyway!
      },
      constraints: [
        {
          to: 'scrollParent',
          attachment: 'together',
          pin: true
        }
      ]
    };
  },

  _renderPopover: function() {
    React.renderComponent(this._popoverComponent(), this._popoverElement);
    if (this._tether != null) {
      this._tether.setOptions(this._tetherOptions());
    } else {
      this._tether = new Tether(this._tetherOptions());
    }
  },

  componentWillUnmount: function() {
    this._tether.destroy();
    React.unmountComponentAtNode(this._popoverElement);
    if (this._popoverElement.parentNode) {
      this._popoverElement.parentNode.removeChild(this._popoverElement);
    }
  },

  render: function() {
    return React.createElement("span", null);
  }
});

module.exports = Popover;
<commit_msg>Fix leaking variable<commit_after>'use strict';

var React = require('react');
var Tether = require('tether');

var Popover = React.createClass({
  displayName: 'Popover',

  componentWillMount: function() {
    var popoverContainer = document.createElement('span');
    popoverContainer.className = 'datepicker__container';

    this._popoverElement = popoverContainer;

    document.querySelector('body').appendChild(this._popoverElement);
  },

  componentDidMount: function() {
    this._renderPopover();
  },

  componentDidUpdate: function() {
    this._renderPopover();
  },

  _popoverComponent: function() {
    var className = this.props.className;
    return (
      React.createElement("div", {className: className},
        this.props.children
      )
    );
  },

  _tetherOptions: function() {
    return {
      element: this._popoverElement,
      target: this.getDOMNode().parentElement,
      attachment: 'top left',
      targetAttachment: 'bottom left',
      targetOffset: '10px 0',
      optimizations: {
        moveElement: false // always moves to <body> anyway!
      },
      constraints: [
        {
          to: 'scrollParent',
          attachment: 'together',
          pin: true
        }
      ]
    };
  },

  _renderPopover: function() {
    React.renderComponent(this._popoverComponent(), this._popoverElement);
    if (this._tether != null) {
      this._tether.setOptions(this._tetherOptions());
    } else {
      this._tether = new Tether(this._tetherOptions());
    }
  },

  componentWillUnmount: function() {
    this._tether.destroy();
    React.unmountComponentAtNode(this._popoverElement);
    if (this._popoverElement.parentNode) {
      this._popoverElement.parentNode.removeChild(this._popoverElement);
    }
  },

  render: function() {
    return React.createElement("span", null);
  }
});

module.exports = Popover;
<|endoftext|>


<commit_before>var _ = require('lodash'),
    util = require('../util'),

    authMap = {
        basic: 'basicAuth',
        digest: 'digestAuth',
        hawk: 'hawkAuth',
        oauth1: 'oAuth1',
        awsv4: 'awsSigV4',
        noauth: 'normal'
    },

    modeMap = {
        urlencoded: 'urlencoded',
        formdata: 'params',
        raw: 'raw'
    },

    Builders = function ConversionBuilder (options) {
        this.options = options || {};
    };

_.extend(Builders.prototype, {

    /**
     * Constructs a monolithic raw HTTP header block from a V2 header array
     *
     * @param item
     * @returns {*|string}
     */
    headers: function (item) {
        var headers = item.request.header;
        return _.map(_.object(_.pluck(headers, 'key'), _.pluck(headers, 'value')), function (value, key) {
            return key + ': ' + value;
        }).join('\n');
    },

    /**
     * Detects the data mode from a given Postman Collection V2 item
     *
     * @param item
     * @returns {*|number|string}
     */
    dataMode: function (item) {
        return modeMap[item.request.body.mode];
    },

    /**
     * Returns the appropriate request data based on the data mode.
     *
     * @param item
     * @returns {*}
     */
    data: function (item) {
        var mode = _.get(item, 'request.body.mode');
        if (mode === 'raw' || !mode) {
            return [];
        }
        return item.request.body[mode] ? item.request.body[mode] : [];
    },

    /**
     * In case of raw request bodies, this constructs the proper raw data from a V2 item.
     *
     * @param item
     * @returns {*}
     */
    rawModeData: function (item) {
        return (item.request.body.mode === 'raw') ? item.request.body.raw : '';
    },

    /**
     * Creates an object of path-variables and their values from a V2 item
     *
     * @param item
     * @returns {*}
     */
    pathVariables: function (item) {
        var variable = _.get(item, 'request.url.variable');

        if (!variable) {
            return;
        }
        return _.object(_.pluck(variable, 'id'), _.pluck(variable, 'value'));
    },

    /**
     * Creates a V1 URL from a V2 item
     *
     * @param item
     */
    url: function (item) {
        var url = _.get(item, 'request.url');

        if (_.isString(url)) {
            return url;
        }

        return util.urlunparse(url);
    },

    /**
     * Extracts tests from a V2 collection
     *
     * @param item
     * @returns {string}
     */
    tests: function (item) {
        // Nothing to do if the item has no associated events
        if (!item.event) {
            return;
        }

        var events = _.filter(item.event, {listen: 'test'});
        return _.map(events, function (event) {
            var tests = _.get(event, 'script.exec');
            if (_.isArray(tests)) {
                tests = tests.join('\n');
            }
            return tests;
        }).join('\n');
    },

    /**
     * Extracts the pre-request script from an Item
     *
     * @param item
     * @returns {string}
     */
    preRequestScript: function (item) {
        // Nothing to do if the item has no associated events
        if (!item.event) {
            return;
        }

        var events = _.filter(item.event, {listen: 'prerequest'});
        return _.map(events, function (event) {
            var tests = _.get(event, 'script.exec');
            if (_.isArray(tests)) {
                tests = tests.join('\n');
            }
            return tests;
        }).join('\n');
    },

    /**
     * Converts a V2 cookie to a V1 cookie.
     *
     * @param cookieV2
     * @returns {{expirationDate: *, hostOnly: *, httpOnly: *,
         *          domain: (any), path: (any), secure: *, session: *, value: *, name: *}}
     */
    cookie: function (cookieV2) {
        return {
            expirationDate: cookieV2.expires,
            hostOnly: cookieV2.hostOnly,
            httpOnly: cookieV2.httpOnly,
            domain: cookieV2.domain,
            path: cookieV2.path,
            secure: cookieV2.secure,
            session: cookieV2.session,
            value: cookieV2.value,
            name: cookieV2.name
        };
    },

    /**
     * Converts a V2 response object to a V1 response
     *
     * @param responseV2
     * @returns {{}}
     */
    response: function (responseV2) {
        var self = this,
            response = {},
            originalRequest = responseV2.originalRequest || responseV2.request;
        // the true in the next line ensures that we don't recursively go on processing responses in a request.
        response.request = originalRequest ? self.request({request: originalRequest}, undefined, true) : undefined;
        response.id = util.uid();
        response.name = responseV2.name;
        response.status = responseV2.status;
        response.responseCode = {
            code: responseV2.code,
            name: responseV2.status,
            // TODO: get a list of descriptions
            detail: ''
        };
        response.time = responseV2.responseTime;
        response.headers = responseV2.header;
        response.cookies = _.map(responseV2.cookie, self.cookie);
        response.text = responseV2.body;
        response.rawDataType = 'text';
        return response;
    },

    /**
     * Extracts the array of responses from a V2 Item.
     *
     * @param item
     * @returns {Array}
     */
    responses: function (item) {
        var self = this,
            responses = [];
        if (!item.response) {
            return;
        }
        _.each(item.response, function (response) {
            responses.push(self.response(response, item));
        });
        return responses;
    },

    /**
     * Converts a V2 request to a V1 request.
     *
     * @param item
     * @param collectionId
     * @param skipResponses
     * @returns {{id: *, name: *, description: (*|string|builders.description), url: *, collectionId: *, method: *,
         *          currentHelper: *, helperAttributes: *}|*}
     */
    request: function (item, collectionId, skipResponses) {
        // TODO: Support for path variables
        var units = ['headers', 'dataMode', 'data', 'rawModeData', 'pathVariables',
                'tests', 'preRequestScript', 'url'],
            self = this,
            request,
            currentHelper,
            helperAttributes,
            auth = _.get(item, 'request.auth');

        !skipResponses && units.push('responses');

        // Extract helpers
        if (auth && auth.type) {
            currentHelper = authMap[auth.type];
            if (util.authMappersFromCurrent[currentHelper]) {
                helperAttributes = util.authMappersFromCurrent[currentHelper](auth[auth.type]);
            }
            else {
                helperAttributes = {};
            }
        }
        request = {
            // jscs:disable
            id: item.id || item._postman_id || util.uid(), // jshint ignore:line
            // jscs:enable
            name: item.name,
            description: item.description,
            collectionId: collectionId,
            method: item.request ? item.request.method : undefined,
            currentHelper: currentHelper,
            helperAttributes: helperAttributes
        };
        _.map(units, function (unit) {
            request[unit] = self[unit](item);
        });
        return request;
    },

    /**
     * Creates a V1 compatible array of requests from a Postman V2 collection.
     *
     * @param collectionV2
     */
    /* jshint ignore:start */ // jscs:disable
    requests: function (collectionV2) {
        var self = this,
            flattenedItems = _.flatten(_.map(collectionV2.item, function (item) {
                return (item.item) ? item.item : item;
            })),
            requests = [];
        _.each(flattenedItems, function (item) {
            var requestV1 = self.request(item, (collectionV2.info.id || collectionV2.info._postman_id));
            requests.push(requestV1);
        });
        return requests;
    },
    /* jshint ignore:end */ // jscs:enable

    /**
     * Creates a V1 compatible array of solo requestIds from a Postman collection V2
     *
     * @param collectionV2
     */
    order: function (collectionV2) {
        var allItems = _.isArray(collectionV2.item) ? collectionV2.item : [collectionV2.item];

        return _.filter(_.map(allItems, function (item) {
            if (!item.item) {
                return item.id;
            }
        }));
    },

    /**
     * Creates an array of V1 compatible folders from a V2 collection
     *
     * @param collectionV2
     */
    folders: function (collectionV2) {
        return _.map(_.filter(collectionV2.item, function (element) {
            return !!element.item;
        }), function (folder) {
            return {
                /* jshint ignore:start */ // jscs:disable
                id: folder._postman_id || folder.id || util.uid(),
                /* jshint ignore:end */ // jscs:enable
                name: folder.name,
                description: folder.description,
                order: _.map(folder.item, 'id')
            };
        });
    }
});

module.exports = {
    input: '2.0.0',
    output: '1.0.0',
    Builders: Builders,

    convert: function (collection, options, callback) {
        var units = ['order', 'folders', 'requests'],
            builders = new Builders(options),
            newCollection = {
                // jscs:disable
                id: collection.info._postman_id, // jshint ignore:line
                // jscs:enable
                name: collection.info.name,
                description: collection.info.description
            };

        try {
            units.map(function (unit) {
                newCollection[unit] = builders[unit](collection);
            });
        }
        catch (e) {
            if (callback) {
                return callback(e, null);
            }
            throw e;
        }

        if (callback) {
            return callback(null, newCollection);
        }
        return newCollection;
    }
};
<commit_msg>populate collection ids if they don't already exist<commit_after>var _ = require('lodash'),
    util = require('../util'),

    authMap = {
        basic: 'basicAuth',
        digest: 'digestAuth',
        hawk: 'hawkAuth',
        oauth1: 'oAuth1',
        awsv4: 'awsSigV4',
        noauth: 'normal'
    },

    modeMap = {
        urlencoded: 'urlencoded',
        formdata: 'params',
        raw: 'raw'
    },

    populateIds = function (currentItem) {
        !currentItem.id && (currentItem.id = util.uid());
        currentItem.item && currentItem.item.length && (currentItem.item = _.map(currentItem.item, function (item) {
            if (!item.id) {
                item = _.merge({
                    id: util.uid()
                }, item);
            }
            item.item && (item.item = _.map(item.item, populateIds));
            return item;
        }));
        return currentItem;
    },

    Builders = function ConversionBuilder (options) {
        this.options = options || {};
    };

_.extend(Builders.prototype, {

    /**
     * Constructs a monolithic raw HTTP header block from a V2 header array
     *
     * @param item
     * @returns {*|string}
     */
    headers: function (item) {
        var headers = item.request.header;
        return _.map(_.object(_.pluck(headers, 'key'), _.pluck(headers, 'value')), function (value, key) {
            return key + ': ' + value;
        }).join('\n');
    },

    /**
     * Detects the data mode from a given Postman Collection V2 item
     *
     * @param item
     * @returns {*|number|string}
     */
    dataMode: function (item) {
        return modeMap[item.request.body.mode];
    },

    /**
     * Returns the appropriate request data based on the data mode.
     *
     * @param item
     * @returns {*}
     */
    data: function (item) {
        var mode = _.get(item, 'request.body.mode');
        if (mode === 'raw' || !mode) {
            return [];
        }
        return item.request.body[mode] ? item.request.body[mode] : [];
    },

    /**
     * In case of raw request bodies, this constructs the proper raw data from a V2 item.
     *
     * @param item
     * @returns {*}
     */
    rawModeData: function (item) {
        return (item.request.body.mode === 'raw') ? item.request.body.raw : '';
    },

    /**
     * Creates an object of path-variables and their values from a V2 item
     *
     * @param item
     * @returns {*}
     */
    pathVariables: function (item) {
        var variable = _.get(item, 'request.url.variable');

        if (!variable) {
            return;
        }
        return _.object(_.pluck(variable, 'id'), _.pluck(variable, 'value'));
    },

    /**
     * Creates a V1 URL from a V2 item
     *
     * @param item
     */
    url: function (item) {
        var url = _.get(item, 'request.url');

        if (_.isString(url)) {
            return url;
        }

        return util.urlunparse(url);
    },

    /**
     * Extracts tests from a V2 collection
     *
     * @param item
     * @returns {string}
     */
    tests: function (item) {
        // Nothing to do if the item has no associated events
        if (!item.event) {
            return;
        }

        var events = _.filter(item.event, {listen: 'test'});
        return _.map(events, function (event) {
            var tests = _.get(event, 'script.exec');
            if (_.isArray(tests)) {
                tests = tests.join('\n');
            }
            return tests;
        }).join('\n');
    },

    /**
     * Extracts the pre-request script from an Item
     *
     * @param item
     * @returns {string}
     */
    preRequestScript: function (item) {
        // Nothing to do if the item has no associated events
        if (!item.event) {
            return;
        }

        var events = _.filter(item.event, {listen: 'prerequest'});
        return _.map(events, function (event) {
            var tests = _.get(event, 'script.exec');
            if (_.isArray(tests)) {
                tests = tests.join('\n');
            }
            return tests;
        }).join('\n');
    },

    /**
     * Converts a V2 cookie to a V1 cookie.
     *
     * @param cookieV2
     * @returns {{expirationDate: *, hostOnly: *, httpOnly: *,
         *          domain: (any), path: (any), secure: *, session: *, value: *, name: *}}
     */
    cookie: function (cookieV2) {
        return {
            expirationDate: cookieV2.expires,
            hostOnly: cookieV2.hostOnly,
            httpOnly: cookieV2.httpOnly,
            domain: cookieV2.domain,
            path: cookieV2.path,
            secure: cookieV2.secure,
            session: cookieV2.session,
            value: cookieV2.value,
            name: cookieV2.name
        };
    },

    /**
     * Converts a V2 response object to a V1 response
     *
     * @param responseV2
     * @returns {{}}
     */
    response: function (responseV2) {
        var self = this,
            response = {},
            originalRequest = responseV2.originalRequest || responseV2.request;
        // the true in the next line ensures that we don't recursively go on processing responses in a request.
        response.request = originalRequest ? self.request({request: originalRequest}, undefined, true) : undefined;
        response.id = util.uid();
        response.name = responseV2.name;
        response.status = responseV2.status;
        response.responseCode = {
            code: responseV2.code,
            name: responseV2.status,
            // TODO: get a list of descriptions
            detail: ''
        };
        response.time = responseV2.responseTime;
        response.headers = responseV2.header;
        response.cookies = _.map(responseV2.cookie, self.cookie);
        response.text = responseV2.body;
        response.rawDataType = 'text';
        return response;
    },

    /**
     * Extracts the array of responses from a V2 Item.
     *
     * @param item
     * @returns {Array}
     */
    responses: function (item) {
        var self = this,
            responses = [];
        if (!item.response) {
            return;
        }
        _.each(item.response, function (response) {
            responses.push(self.response(response, item));
        });
        return responses;
    },

    /**
     * Converts a V2 request to a V1 request.
     *
     * @param item
     * @param collectionId
     * @param skipResponses
     * @returns {{id: *, name: *, description: (*|string|builders.description), url: *, collectionId: *, method: *,
         *          currentHelper: *, helperAttributes: *}|*}
     */
    request: function (item, collectionId, skipResponses) {
        // TODO: Support for path variables
        var units = ['headers', 'dataMode', 'data', 'rawModeData', 'pathVariables',
                'tests', 'preRequestScript', 'url'],
            self = this,
            request,
            currentHelper,
            helperAttributes,
            auth = _.get(item, 'request.auth');

        !skipResponses && units.push('responses');

        // Extract helpers
        if (auth && auth.type) {
            currentHelper = authMap[auth.type];
            if (util.authMappersFromCurrent[currentHelper]) {
                helperAttributes = util.authMappersFromCurrent[currentHelper](auth[auth.type]);
            }
            else {
                helperAttributes = {};
            }
        }
        request = {
            // jscs:disable
            id: item.id || item._postman_id || util.uid(), // jshint ignore:line
            // jscs:enable
            name: item.name,
            description: item.description,
            collectionId: collectionId,
            method: item.request ? item.request.method : undefined,
            currentHelper: currentHelper,
            helperAttributes: helperAttributes
        };
        _.map(units, function (unit) {
            request[unit] = self[unit](item);
        });
        return request;
    },

    /**
     * Creates a V1 compatible array of requests from a Postman V2 collection.
     *
     * @param collectionV2
     */
    /* jshint ignore:start */ // jscs:disable
    requests: function (collectionV2) {
        var self = this,
            flattenedItems = _.flatten(_.map(collectionV2.item, function (item) {
                return (item.item) ? item.item : item;
            })),
            requests = [];
        _.each(flattenedItems, function (item) {
            var requestV1 = self.request(item, (collectionV2.info.id || collectionV2.info._postman_id));
            requests.push(requestV1);
        });
        return requests;
    },
    /* jshint ignore:end */ // jscs:enable

    /**
     * Creates a V1 compatible array of solo requestIds from a Postman collection V2
     *
     * @param collectionV2
     */
    order: function (collectionV2) {
        var allItems = _.isArray(collectionV2.item) ? collectionV2.item : [collectionV2.item];

        return _.filter(_.map(allItems, function (item) {
            if (!item.item) {
                return item.id;
            }
        }));
    },

    /**
     * Creates an array of V1 compatible folders from a V2 collection
     *
     * @param collectionV2
     */
    folders: function (collectionV2) {
        return _.map(_.filter(collectionV2.item, function (element) {
            return !!element.item;
        }), function (folder) {
            return {
                /* jshint ignore:start */ // jscs:disable
                id: folder._postman_id || folder.id || util.uid(),
                /* jshint ignore:end */ // jscs:enable
                name: folder.name,
                description: folder.description,
                order: _.map(folder.item, 'id')
            };
        });
    }
});

module.exports = {
    input: '2.0.0',
    output: '1.0.0',
    Builders: Builders,

    convert: function (collection, options, callback) {
        var units = ['order', 'folders', 'requests'],
            builders = new Builders(options),
            newCollection = {
                // jscs:disable
                id: collection.info._postman_id, // jshint ignore:line
                // jscs:enable
                name: collection.info.name,
                description: collection.info.description
            };

        // ensure that each item has an ID
        collection = populateIds(collection);
        try {
            units.map(function (unit) {
                newCollection[unit] = builders[unit](collection);
            });
        }
        catch (e) {
            if (callback) {
                return callback(e, null);
            }
            throw e;
        }

        if (callback) {
            return callback(null, newCollection);
        }
        return newCollection;
    }
};
<|endoftext|>


<commit_before>let fs, watch, http, STDIN, STDOUT;
if (process.env.NODE_ENV === "test") {
    const mocks = require("./process-mocks");
    fs = mocks.fs;
    watch = mocks.watch;
    http = mocks.http;
    STDIN  = mocks.stdio.stdin;
    STDOUT = mocks.stdio.stdout;
} else {
    fs = require("fs");
    watch = require("node-watch");
    http = require("http");
    // STDIN = process.stdin;
    // STDOUT = process.stdout;
    //
    // FIXME this resolves an issue with process.std{in,out} but breaks windows
    // compatibility
    STDIN = fs.createReadStream("/dev/stdin");
    STDIN.fd = 0; // careful...
    STDOUT = fs.createWriteStream("/dev/stdout");
    STDOUT.fd = 1;
}

import path from "path";
import EventEmitter from "events";

function myStatSync(stdioStream, path) {
    if (path === "-") return fs.fstatSync(stdioStream.fd);
    return fs.statSync(path);
}

function ensureDir(dir) {
    try { fs.mkdirSync(dir); }
    catch (e) {
        if (e.code === "EEXIST") {
            if (!fs.statSync(dir).isDirectory()) throw e;
            return;
        }
        if (e.code !== "ENOENT") throw e;

        ensureDir(path.dirname(dir));
        fs.mkdirSync(dir);
    }
}

function dirProvider(output) {
    // FIXME this will place outputs outside the output directory if the inputs
    // are oudside PWD
    return inpath => {
        let outpath = path.join(output, path.relative(process.cwd(), inpath));
        let outdir = path.dirname(outpath);

        // TODO can this be moved elsewhere to avoid synchronous IO?
        ensureDir(outdir);

        return fs.createWriteStream(outpath);
    };
}

function fileProvider(output) {
    ensureDir(path.dirname(output));
    return inpath => output === "-" ? STDOUT : fs.createWriteStream(output);
}

class MyEventEmitter extends EventEmitter {
    constructor(...args) {
        super(...args);
        this.hasEnded = false;
    }

    emit(event, ...args) {
        if (this.hasEnded) return;
        if (event === "end" || event === "error") {
            this.hasEnded = true;
            super.emit(event, ...args);
            return;
        }
        super.emit(event, ...args);
    }
}

class ReaddirJobEmitter extends MyEventEmitter {
    constructor({ dir, streamRegistry, recursive, outstreamProvider }) {
        super();

        process.nextTick(() => {
            let awaitCount = 0;
            const complete = () => {
                if (--awaitCount === 0) this.emit("end");
            };

            fs.readdir(dir, (err, files) => {
                if (err != null) return this.emit("error", err);

                awaitCount += files.length;

                for (let file of files) {
                    file = path.normalize(path.join(dir, file));
                    fs.stat(file, (err, stats) => {
                        if (err != null) return this.emit("error", err);

                        if (stats.isDirectory()) {
                            if (recursive) {
                                let subdirEmitter = new ReaddirJobEmitter(
                                    { dir: file, streamRegistry, recursive, outstreamProvider });
                                subdirEmitter.on("job", job => this.emit("job", job));
                                subdirEmitter.on("error", error => this.emit("error", error));
                                subdirEmitter.on("end", complete);
                            } else {
                                complete();
                            }
                        } else {
                            if (streamRegistry[file]) streamRegistry[file].end();
                            let outstream = streamRegistry[file] = outstreamProvider(file);
                            this.emit("job", { in: fs.createReadStream(file), out: outstream });
                            complete();
                        }
                    });
                }
            });
        });
    }
}

class SingleJobEmitter extends MyEventEmitter {
    constructor({ file, streamRegistry, outstreamProvider }) {
        super();

        file = path.normalize(file);
        if (streamRegistry[file]) streamRegistry[file].end();
        let outstream = streamRegistry[file] = outstreamProvider(file);
        let instream = file === "-" ? STDIN : fs.createReadStream(file);

        process.nextTick(() => {
            this.emit("job", { in: instream, out: outstream });
            this.emit("end");
        });
    }
}

class NullJobEmitter extends MyEventEmitter {
    constructor() {
        super();

        process.nextTick(() => this.emit("end"));
    }
}

class WatchJobEmitter extends MyEventEmitter {
    constructor({ file, streamRegistry, recursive, outstreamProvider }) {
        super();

        let watcher = watch(file, { recursive, followSymLinks: true });

        watcher.on("error", err => this.emit("error", err));
        watcher.on("end", () => this.emit("end"));
        watcher.on("change", file => {
            file = path.normalize(file);
            fs.stat(file, (err, stats) => {
                if (err) return this.emit("error", err);
                if (stats.isDirectory()) return;
                if (streamRegistry[file]) streamRegistry[file].end();
                let outstream = streamRegistry[file] = outstreamProvider(file);
                let instream = fs.createReadStream(file);
                this.emit("job", { in: instream, out: outstream });
            });
        });
    }
}

class MergedJobEmitter extends MyEventEmitter {
    constructor(...jobEmitters) {
        super();

        let ncomplete = 0;

        for (let jobEmitter of jobEmitters) {
            jobEmitter.on("error", err => this.emit("error", err));
            jobEmitter.on("job", job => this.emit("job", job));
            jobEmitter.on("end", () => {
                if (++ncomplete === jobEmitters.length) this.emit("end");
            });
        }
    }
}

class ConcattedJobEmitter extends MyEventEmitter {
    constructor(emitterFn, ...emitterFns) {
        super();

        let emitter = emitterFn();

        if (emitterFns.length === 0) {
            emitter.on("error", err => this.emit("error", err));
            emitter.on("job", job => this.emit("job", job));
            emitter.on("end", () => this.emit("end"));
        } else {
            emitter.on("error", err => this.emit("error", err));
            emitter.on("job", job => this.emit("job", job));
            emitter.on("end", () => {
                let restEmitter = new ConcattedJobEmitter(...emitterFns);
                restEmitter.on("error", err => this.emit("error", err));
                restEmitter.on("job", job => this.emit("job", job));
                restEmitter.on("end", () => this.emit("end"));
            });
        }
    }
}

function makeJobEmitter(inputs, { recursive, outstreamProvider, streamRegistry, watch }) {
    let emitter = new EventEmitter();

    let emitterFns = [];
    let watcherFns = [];

    for (let input of inputs) {
        if (input === "-") {
            emitterFns.push(
                () => new SingleJobEmitter({ file: input, outstreamProvider, streamRegistry }));
            watcherFns.push(
                () => new NullJobEmitter());

            startEmitting();
        } else {
            fs.stat(input, (err, stats) => {
                if (err != null) return emitter.emit("error", err);

                if (stats.isDirectory()) {
                    emitterFns.push(
                        () => new ReaddirJobEmitter({ dir: input, recursive, outstreamProvider, streamRegistry }));
                    watcherFns.push(
                        () => new WatchJobEmitter({ file: input, recursive, outstreamProvider, streamRegistry }));
                } else {
                    emitterFns.push(
                        () => new SingleJobEmitter({ file: input, outstreamProvider, streamRegistry }));
                    watcherFns.push(
                        () => new WatchJobEmitter({ file: input, recursive, outstreamProvider, streamRegistry }));
                }

                startEmitting();
            });
        }
    }

    function startEmitting() {
        if (emitterFns.length !== inputs.length) return;

        let source = new MergedJobEmitter(...emitterFns.map(f => f()));

        if (watch) {
            source = new ConcattedJobEmitter(() => source,
                                             () => new MergedJobEmitter(...watcherFns.map(f => f())));
        }

        source.on("job", job => emitter.emit("job", job));
        source.on("error", err => emitter.emit("error", err));
        source.on("end", () => emitter.emit("end"));
    }

    return emitter;
}

export default function run(client, { steps, template, fields, watch, recursive, inputs, output }) {
    if (inputs.length === 0) inputs = [ "-" ];

    let params = steps ? { steps: require(steps) } : { template_id: template };
    params.fields = fields;

    let outstat = myStatSync(STDOUT, output);
    if (!outstat.isDirectory()) {
        if (inputs.length > 1) throw new Error();
        if (myStatSync(STDIN, inputs[0]).isDirectory()) throw new Error();
    }

    let outstreamProvider = outstat.isDirectory() ? dirProvider(output) : fileProvider(output);
    let streamRegistry = {};

    let emitter = makeJobEmitter(inputs, { recursive, watch, outstreamProvider, streamRegistry });

    emitter.on("job", job => {
        console.error("GOT JOB", job.in.path, job.out.path);
        let superceded = false;
        job.out.on("finish", () => superceded = true);

        client.addStream("in", job.in);

        client.createAssembly({ params }, (err, result) => {
            if (err != null) return console.error(err);

            if (superceded) return;

            client.getAssembly(result.assembly_id, function callback(err, result) {
                if (err != null) return console.error(err);

                if (superceded) return;

                if (result.ok !== "ASSEMBLY_COMPLETED") {
                    client.getAssembly(result.assembly_id, callback);
                    return;
                }

                let resulturl = result.results[Object.keys(result.results)[0]][0].url;

                http.get(resulturl, res => {
                    if (res.statusCode !== 200) {
                        console.error(new Error(`Server returned http status ${res.statusCode}`));
                        return;
                    }

                    if (superceded) return;

                    res.pipe(job.out);
                    res.on("end", () => console.error("COMPLETED", job.in.path, job.out.path));
                    job.out.on("finish", () => res.unpipe()); // TODO is this done automatically?
                }).on("error", console.error);
            });
        });
    });

    emitter.on("error", err => {
        console.error(err);
    });
}
<commit_msg>fix file names in output directories<commit_after>let fs, watch, http, STDIN, STDOUT;
if (process.env.NODE_ENV === "test") {
    const mocks = require("./process-mocks");
    fs = mocks.fs;
    watch = mocks.watch;
    http = mocks.http;
    STDIN  = mocks.stdio.stdin;
    STDOUT = mocks.stdio.stdout;
} else {
    fs = require("fs");
    watch = require("node-watch");
    http = require("http");
    // STDIN = process.stdin;
    // STDOUT = process.stdout;
    //
    // FIXME this resolves an issue with process.std{in,out} but breaks windows
    // compatibility
    STDIN = fs.createReadStream("/dev/stdin");
    STDIN.fd = 0; // careful...
    STDOUT = fs.createWriteStream("/dev/stdout");
    STDOUT.fd = 1;
}

import path from "path";
import EventEmitter from "events";

function myStatSync(stdioStream, path) {
    if (path === "-") return fs.fstatSync(stdioStream.fd);
    return fs.statSync(path);
}

function ensureDir(dir) {
    try { fs.mkdirSync(dir); }
    catch (e) {
        if (e.code === "EEXIST") {
            if (!fs.statSync(dir).isDirectory()) throw e;
            return;
        }
        if (e.code !== "ENOENT") throw e;

        ensureDir(path.dirname(dir));
        fs.mkdirSync(dir);
    }
}

function dirProvider(output) {
    return (inpath, indir=process.cwd()) => {
        let relpath = path.relative(indir, inpath)
        // if inpath is outside indir, ensure that outpath will still be inside
        // output
        relpath = relpath.replace(/^(\.\.\/)+/, "");
        let outpath = path.join(output, relpath);
        let outdir = path.dirname(outpath);

        // TODO can this be moved elsewhere to avoid synchronous IO?
        ensureDir(outdir);

        return fs.createWriteStream(outpath);
    };
}

function fileProvider(output) {
    ensureDir(path.dirname(output));
    return inpath => output === "-" ? STDOUT : fs.createWriteStream(output);
}

class MyEventEmitter extends EventEmitter {
    constructor(...args) {
        super(...args);
        this.hasEnded = false;
    }

    emit(event, ...args) {
        if (this.hasEnded) return;
        if (event === "end" || event === "error") {
            this.hasEnded = true;
            super.emit(event, ...args);
            return;
        }
        super.emit(event, ...args);
    }
}

class ReaddirJobEmitter extends MyEventEmitter {
    constructor({ dir, streamRegistry, recursive, outstreamProvider, topdir=dir }) {
        super();

        process.nextTick(() => {
            let awaitCount = 0;
            const complete = () => {
                if (--awaitCount === 0) this.emit("end");
            };

            fs.readdir(dir, (err, files) => {
                if (err != null) return this.emit("error", err);

                awaitCount += files.length;

                for (let file of files) {
                    file = path.normalize(path.join(dir, file));
                    fs.stat(file, (err, stats) => {
                        if (err != null) return this.emit("error", err);

                        if (stats.isDirectory()) {
                            if (recursive) {
                                let subdirEmitter = new ReaddirJobEmitter(
                                    { dir: file, streamRegistry, recursive, outstreamProvider, topdir });
                                subdirEmitter.on("job", job => this.emit("job", job));
                                subdirEmitter.on("error", error => this.emit("error", error));
                                subdirEmitter.on("end", complete);
                            } else {
                                complete();
                            }
                        } else {
                            if (streamRegistry[file]) streamRegistry[file].end();
                            let outstream = streamRegistry[file] = outstreamProvider(file, topdir);
                            this.emit("job", { in: fs.createReadStream(file), out: outstream });
                            complete();
                        }
                    });
                }
            });
        });
    }
}

class SingleJobEmitter extends MyEventEmitter {
    constructor({ file, streamRegistry, outstreamProvider }) {
        super();

        file = path.normalize(file);
        if (streamRegistry[file]) streamRegistry[file].end();
        let outstream = streamRegistry[file] = outstreamProvider(file);
        let instream = file === "-" ? STDIN : fs.createReadStream(file);

        process.nextTick(() => {
            this.emit("job", { in: instream, out: outstream });
            this.emit("end");
        });
    }
}

class NullJobEmitter extends MyEventEmitter {
    constructor() {
        super();

        process.nextTick(() => this.emit("end"));
    }
}

class WatchJobEmitter extends MyEventEmitter {
    constructor({ file, streamRegistry, recursive, outstreamProvider }) {
        super();

        let watcher = watch(file, { recursive, followSymLinks: true });

        watcher.on("error", err => this.emit("error", err));
        watcher.on("end", () => this.emit("end"));
        watcher.on("change", file => {
            file = path.normalize(file);
            fs.stat(file, (err, stats) => {
                if (err) return this.emit("error", err);
                if (stats.isDirectory()) return;
                if (streamRegistry[file]) streamRegistry[file].end();
                let outstream = streamRegistry[file] = outstreamProvider(file);
                let instream = fs.createReadStream(file);
                this.emit("job", { in: instream, out: outstream });
            });
        });
    }
}

class MergedJobEmitter extends MyEventEmitter {
    constructor(...jobEmitters) {
        super();

        let ncomplete = 0;

        for (let jobEmitter of jobEmitters) {
            jobEmitter.on("error", err => this.emit("error", err));
            jobEmitter.on("job", job => this.emit("job", job));
            jobEmitter.on("end", () => {
                if (++ncomplete === jobEmitters.length) this.emit("end");
            });
        }
    }
}

class ConcattedJobEmitter extends MyEventEmitter {
    constructor(emitterFn, ...emitterFns) {
        super();

        let emitter = emitterFn();

        if (emitterFns.length === 0) {
            emitter.on("error", err => this.emit("error", err));
            emitter.on("job", job => this.emit("job", job));
            emitter.on("end", () => this.emit("end"));
        } else {
            emitter.on("error", err => this.emit("error", err));
            emitter.on("job", job => this.emit("job", job));
            emitter.on("end", () => {
                let restEmitter = new ConcattedJobEmitter(...emitterFns);
                restEmitter.on("error", err => this.emit("error", err));
                restEmitter.on("job", job => this.emit("job", job));
                restEmitter.on("end", () => this.emit("end"));
            });
        }
    }
}

function makeJobEmitter(inputs, { recursive, outstreamProvider, streamRegistry, watch }) {
    let emitter = new EventEmitter();

    let emitterFns = [];
    let watcherFns = [];

    for (let input of inputs) {
        if (input === "-") {
            emitterFns.push(
                () => new SingleJobEmitter({ file: input, outstreamProvider, streamRegistry }));
            watcherFns.push(
                () => new NullJobEmitter());

            startEmitting();
        } else {
            fs.stat(input, (err, stats) => {
                if (err != null) return emitter.emit("error", err);

                if (stats.isDirectory()) {
                    emitterFns.push(
                        () => new ReaddirJobEmitter({ dir: input, recursive, outstreamProvider, streamRegistry }));
                    watcherFns.push(
                        () => new WatchJobEmitter({ file: input, recursive, outstreamProvider, streamRegistry }));
                } else {
                    emitterFns.push(
                        () => new SingleJobEmitter({ file: input, outstreamProvider, streamRegistry }));
                    watcherFns.push(
                        () => new WatchJobEmitter({ file: input, recursive, outstreamProvider, streamRegistry }));
                }

                startEmitting();
            });
        }
    }

    function startEmitting() {
        if (emitterFns.length !== inputs.length) return;

        let source = new MergedJobEmitter(...emitterFns.map(f => f()));

        if (watch) {
            source = new ConcattedJobEmitter(() => source,
                                             () => new MergedJobEmitter(...watcherFns.map(f => f())));
        }

        source.on("job", job => emitter.emit("job", job));
        source.on("error", err => emitter.emit("error", err));
        source.on("end", () => emitter.emit("end"));
    }

    return emitter;
}

export default function run(client, { steps, template, fields, watch, recursive, inputs, output }) {
    if (inputs.length === 0) inputs = [ "-" ];

    let params = steps ? { steps: require(steps) } : { template_id: template };
    params.fields = fields;

    let outstat = myStatSync(STDOUT, output);
    if (!outstat.isDirectory()) {
        if (inputs.length > 1) throw new Error();
        if (myStatSync(STDIN, inputs[0]).isDirectory()) throw new Error();
    }

    let outstreamProvider = outstat.isDirectory() ? dirProvider(output) : fileProvider(output);
    let streamRegistry = {};

    let emitter = makeJobEmitter(inputs, { recursive, watch, outstreamProvider, streamRegistry });

    emitter.on("job", job => {
        console.error("GOT JOB", job.in.path, job.out.path);
        let superceded = false;
        job.out.on("finish", () => superceded = true);

        client.addStream("in", job.in);

        client.createAssembly({ params }, (err, result) => {
            if (err != null) return console.error(err);

            if (superceded) return;

            client.getAssembly(result.assembly_id, function callback(err, result) {
                if (err != null) return console.error(err);

                if (superceded) return;

                if (result.ok !== "ASSEMBLY_COMPLETED") {
                    client.getAssembly(result.assembly_id, callback);
                    return;
                }

                let resulturl = result.results[Object.keys(result.results)[0]][0].url;

                http.get(resulturl, res => {
                    if (res.statusCode !== 200) {
                        console.error(new Error(`Server returned http status ${res.statusCode}`));
                        return;
                    }

                    if (superceded) return;

                    res.pipe(job.out);
                    res.on("end", () => console.error("COMPLETED", job.in.path, job.out.path));
                    job.out.on("finish", () => res.unpipe()); // TODO is this done automatically?
                }).on("error", console.error);
            });
        });
    });

    emitter.on("error", err => {
        console.error(err);
    });
}
<|endoftext|>


<commit_before>function Promise(value) {
	this.value = value
	this.deferreds = []
}

Promise.prototype.isFulfilled = false
Promise.prototype.isRejected = false

Promise.prototype.then = function (onFulfill, onReject) {
	var promise = new Promise,
	    deferred = defer(promise, onFulfill, onReject)
	if (this.isFulfilled || this.isRejected) {
		resolve(deferred, this.isFulfilled ? Promise.SUCCESS : Promise.FAILURE, this.value)
	} else {
		this.deferreds.push(deferred)
	}

	return promise
}

Promise.prototype.fulfill = function (value) {
	if (this.isFulfilled || this.isRejected) return
	var isResolved = false
	try {
		if (value === this) throw new TypeError('Can\'t resolve a promise with itself.')
		if (isObject(value) || isFunction(value)) {
			var then = value.then,
			    self = this
			if (isFunction(then)) {
				then.call(value, function (val) {
					if (!isResolved) {
						isResolved = true
						self.fulfill(val)
					}
				}, function (err) {
					if (!isResolved) {
						isResolved = true
						self.reject(err)
					}
				})
				return
			}
		}
		this.isFulfilled = true
		this.complete(value)
	} catch (e) {
		if (!isResolved) {
			this.reject(e)
		}
	}
}

Promise.prototype.reject = function (error) {
	if (this.isFulfilled || this.isRejected) return
	this.isRejected = true
	this.complete(error)
}

Promise.prototype.complete = function (value) {
	this.value = value
	var type = this.isFulfilled ? Promise.SUCCESS : Promise.FAILURE
	for (var i = 0; i < this.deferreds.length; ++i) {
		resolve(this.deferreds[i], type, value)
	}
	this.deferreds = undefined
}

Promise.SUCCESS = 'fulfill'
Promise.FAILURE = 'reject'

Promise.all = function () {
	var args = [].slice.call(arguments.length === 1 && Array.isArray(arguments[0]) ?
	        arguments[0] :
	        arguments),
	    promise = new Promise,
	    remaining = args.length

	for (var i = 0; i < args.length; ++i) {
		resolve(i, args[i])
	}

	return promise

  function reject(err) {
		promise.reject(err)
 	}

	function fulfill(val) {
		resolve(i, val)
	}

	function resolve(i, value) {
		if (isObject(value) && isFunction(value.then)) {
			value.then(fulfill, reject)
			return
		}
		args[i] = value
		if (--remaining === 0) {
			promise.fulfill(args)
		}
	}
}

function resolve(deferred, type, value) {
	var fn = deferred[type],
	    promise = deferred.promise
	if (isFunction(fn)) {
		nextTick(function () {
			try {
				value = fn(value)
				promise.fulfill(value)
			} catch (e) {
				promise.reject(e)
			}
		})
	} else {
		promise[type](value)
	}
}

function defer(promise, fulfill, reject) {
	return {
		promise: promise,
		fulfill: fulfill,
		reject: reject
	}
}

function isObject(obj) {
	return obj && typeof obj === 'object'
}

function isFunction(fn) {
	return fn && typeof fn === 'function'
}
<commit_msg>formatting<commit_after>function Promise(value) {
	this.value = value
	this.deferreds = []
}

Promise.prototype.isFulfilled = false
Promise.prototype.isRejected = false

Promise.prototype.then = function (onFulfill, onReject) {
	var promise = new Promise,
	    deferred = defer(promise, onFulfill, onReject)
	if (this.isFulfilled || this.isRejected) {
		resolve(deferred, this.isFulfilled ? Promise.SUCCESS : Promise.FAILURE, this.value)
	} else {
		this.deferreds.push(deferred)
	}

	return promise
}

Promise.prototype.fulfill = function (value) {
	if (this.isFulfilled || this.isRejected) return
	var isResolved = false
	try {
		if (value === this) throw new TypeError('Can\'t resolve a promise with itself.')
		if (isObject(value) || isFunction(value)) {
			var then = value.then,
			    self = this
			if (isFunction(then)) {
				then.call(value, function (val) {
					if (!isResolved) {
						isResolved = true
						self.fulfill(val)
					}
				}, function (err) {
					if (!isResolved) {
						isResolved = true
						self.reject(err)
					}
				})
				return
			}
		}
		this.isFulfilled = true
		this.complete(value)
	} catch (e) {
		if (!isResolved) {
			this.reject(e)
		}
	}
}

Promise.prototype.reject = function (error) {
	if (this.isFulfilled || this.isRejected) return
	this.isRejected = true
	this.complete(error)
}

Promise.prototype.complete = function (value) {
	this.value = value
	var type = this.isFulfilled ? Promise.SUCCESS : Promise.FAILURE
	for (var i = 0; i < this.deferreds.length; ++i) {
		resolve(this.deferreds[i], type, value)
	}
	this.deferreds = undefined
}

Promise.SUCCESS = 'fulfill'
Promise.FAILURE = 'reject'

Promise.all = function () {
	var args = [].slice.call(arguments.length === 1 && Array.isArray(arguments[0]) ?
	        arguments[0] :
	        arguments),
	    promise = new Promise,
	    remaining = args.length

	for (var i = 0; i < args.length; ++i) {
		resolve(i, args[i])
	}

	return promise

	function reject(err) {
		promise.reject(err)
	}

	function fulfill(val) {
		resolve(i, val)
	}

	function resolve(i, value) {
		if (isObject(value) && isFunction(value.then)) {
			value.then(fulfill, reject)
			return
		}
		args[i] = value
		if (--remaining === 0) {
			promise.fulfill(args)
		}
	}
}

function resolve(deferred, type, value) {
	var fn = deferred[type],
	    promise = deferred.promise
	if (isFunction(fn)) {
		nextTick(function () {
			try {
				value = fn(value)
				promise.fulfill(value)
			} catch (e) {
				promise.reject(e)
			}
		})
	} else {
		promise[type](value)
	}
}

function defer(promise, fulfill, reject) {
	return {
		promise: promise,
		fulfill: fulfill,
		reject: reject
	}
}

function isObject(obj) {
	return obj && typeof obj === 'object'
}

function isFunction(fn) {
	return fn && typeof fn === 'function'
}
<|endoftext|>