<commit_before>

/*!
 * Engine - Master
 * Copyright(c) 2010 LearnBoost <dev@learnboost.com>
 * MIT Licensed
 */

/**
 * Module dependencies.
 */

var Worker = require('./worker')
  , EventEmitter = require('events').EventEmitter
  , binding = process.binding('net')
  , dirname = require('path').dirname
  , resolve = require('path').resolve
  , ENOENT = require('constants').ENOENT
  , net = require('net')
  , fs = require('fs')
  , os = require('os');

/**
 * Start a new `Master` with the given `server`.
 *
 * Options:
 *
 *   - `workers`  Number of workers to spawn, defaults to the number of CPUs
 *   - 'working directory`  Working directory defaulting to '/'
 *   - 'backlog`  Connection backlog, defaulting to 128
 *
 * Events:
 *
 *   - `start`. When the server is starting (pre-spawn)
 *   - `worker`. When a worker is spawned, passing the `worker`
 *   - `listening`. When the server is listening for connections (post-spawn)
 *   - `broadcast`. When master is broadcasting a `cmd` with `args`
 *   - `closing`. When master is gracefully shutting down
 *   - `close`. When master has completed shutting down
 *   - `worker killed`. When a worker has died
 *   - `kill`. When a `signal` is being sent to all workers
 *   - `restart`. Restart requested by REPL or signal
 *
 * @param {http.Server} server
 * @return {Master}
 * @api public
 */

var Master = module.exports = function Master(server) {
  var self = this;
  this.server = server;
  this.plugins = [];
  this.options = { 'working directory': '/', backlog: 128 };
  this.children = [];
  this.env = process.env.NODE_ENV || 'development';
  this.isWorker = !! process.env.ENGINE_MASTER_PID;
  this.pid = process.env.ENGINE_MASTER_PID = process.pid;
  this.cmd = process.argv.slice(1);
  this.dir = dirname(this.cmd[0]);
  this.customFds = [1, 2];
  this.state = 'active';
  this.sockPath = '/tmp/engine.sock'; // TODO: options
  this._server = net.createServer(function(sock){
    sock.setEncoding('ascii');
    sock.on('data', self.frame.bind(self));
  });
};

/**
 * Interit from `EventEmitter.prototype`.
 */

Master.prototype.__proto__ = EventEmitter.prototype;

/**
 * Worker is a receiver.
 */

require('./mixins/receiver')(Master.prototype);

/**
 * Resolve `path` relative to the server file being executed.
 *
 * @param {String} path
 * @return {String}
 * @api public
 */

Master.prototype.resolve = function(path){
  return resolve(this.dir, path);
};

/**
 * Invoke masters's `method` with worker `id`. (called from Worker)
 *
 * @param {Number} id
 * @param {String} method
 * @param {...} args
 * @api private
 */

Master.prototype.call = function(id, method){
  var args = Array.prototype.slice.call(arguments)
    , id = args.shift()
    , method = args.shift();
  this.sock.write(JSON.stringify({
      method: method
    , args: args
    , id: id
  }), 'ascii');
};

/**
 * Defer `http.Server#listen()` call.
 *
 * @param {Number|String} port or unix domain socket path
 * @param {String|Function} host or callback
 * @param {Function} callback
 * @return {Master} for chaining
 * @api public
 */

Master.prototype.listen = function(port, host, callback){
  if (this.isWorker) {
    var worker = new Worker(this);
    this.sock = net.createConnection(this.sockPath);
    this.sock.on('connect', function(){
      worker.start();
    });
  } else {
    this.port = port;
    if ('function' == typeof host) callback = host, host = null;
    this.host = host;
    this.callback = callback;
    this.start();
  }
  return this;
};

/**
 * Set option `key` to `val`.
 *
 * @param {String} key
 * @param {Mixed} val
 * @return {Master} for chaining
 * @api public
 */

Master.prototype.set = function(key, val){
  this.options[key] = val;
  return this;
};

/**
 * Check if `option` has been set.
 *
 * @param {String} option
 * @return {Boolean}
 * @api public
 */

Master.prototype.has = function(option){
  return !! this.options[option];
};

/**
 * Use the given `plugin`.
 *
 * @param {Function} plugin
 * @return {Master} for chaining
 * @api public
 */

Master.prototype.use = function(plugin){
  this.plugins.push(plugin);
  if (!this.isWorker) plugin(this);
  return this;
};

/**
 * Start master process.
 *
 *   - defaults workers to the number of CPUs available
 *   - creates listening socket
 *
 * @api private
 */

Master.prototype.start = function(){
  var self = this;

  // listen sock
  if (this.host) {
    this.fd = binding.socket('tcp' + net.isIP(this.host));
  } else if ('string' == typeof this.port) {
    this.fd = binding.socket('unix');
  } else if (this.port) {
    this.fd = binding.socket('tcp4');
  }

  // signal handlers
  process.on('SIGINT', this.destroy.bind(this));
  process.on('SIGTERM', this.destroy.bind(this));
  process.on('SIGQUIT', this.close.bind(this));
  process.on('SIGUSR2', this.restart.bind(this));
  process.on('SIGHUP', function(){});

  // Default worker to the # of cpus
  this.has('workers') || this.set('workers', os.cpus().length);

  // TCP server for IPC
  this._server.listen(this.sockPath, function(){
    self.emit('start');

    // change working dir
    process.chdir(self.options['working directory']);

    // spawn workers
    self.spawn(self.options.workers);

    // set # of pending workers
    self.starting = true;
    self.pendingWorkers = self.options.workers;
  });
};

/**
 * Spawn `n` workers.
 *
 * @param {Number} n
 * @api private
 */

Master.prototype.spawn = function(n){
  while (n--) this.spawnWorker();
};

/**
 * Spawn a worker with optional `id`.
 *
 * @param {Number} id
 * @return {Worker}
 * @api private
 */

Master.prototype.spawnWorker = function(id){
  var worker = new Worker(this).spawn();

  // id given
  if ('number' == typeof id) {
    this.children[id] = worker;
    worker.id = id;
  // generate an id
  } else {
    var len = this.children.push(worker);
    worker.id = len - 1;
  }

  // TODO: refactor
  var json = JSON.stringify({ method: 'connect', args: worker.id });
  worker.sock.write(json, 'ascii', this.fd);

  // emit
  this.emit('worker', worker);

  return worker;
};

/**
 * Broadcast `cmd` with the given `args` to each worker.
 *
 * @param {String} cmd
 * @param {Mixed} args
 * @api public
 */

Master.prototype.broadcast = function(cmd, args){
  var json = JSON.stringify({ cmd: cmd, args: args });
  this.emit('broadcast', cmd, args);
  this.children.forEach(function(worker){
    worker.sock.write(json, 'ascii');
  }, this);
};

/**
 * Graceful shutdown, wait for all workers
 * to reply before exiting.
 *
 * @api public
 */

Master.prototype.close = function(){
  this.emit('closing');
  this.kill('SIGQUIT');
  this.state = 'graceful shutdown';
  this.pendingDeaths = this.children.length;
};

/**
 * Hard shutdwn, immediately kill all workers.
 *
 * @api public
 */

Master.prototype.destroy = function(){
  this.emit('closing');
  this.kill('SIGKILL');
  this.state = 'hard shutdown';
  this._destroy();
};

/**
 * Gracefully restart all workers, by sending __SIGQUIT__
 * to them, enabling master to re-spawn them.
 *
 * @api public
 */

Master.prototype.restart = function(){
  this.emit('restart');
  this.state = 'restarting';
  this.kill('SIGQUIT');
  this.pendingDeaths = this.children.length;
};
/**
 * Immediate shutdown, used by `Master#close()`
 * and `Master#destroy()` when all workers have shutdown.
 *
 * @api private
 */

Master.prototype._destroy = function(){
  var self = this;
  self._server.close();
  if ('string' == typeof self.port) {
    fs.unlink(self.port, function(){
      self.emit('close');
    });
  } else {
    self.emit('close');
  }
};

/**
 * Worker is connected, on boot
 * wait for all workers to connect,
 * then emit the 'listening' event.
 *
 * @param {Worker} worker
 * @api private
 */

Master.prototype.connect = function(worker){
  var self = this;
  this.emit('worker connected', worker);
  if (this.starting) {
    --this.pendingWorkers || function(){
      // TODO: abstract
      if ('string' == typeof self.port) {
        fs.stat(self.port, function(err){
          if (err) {
            if (ENOENT != err.errno) throw err;
            listen();
          } else {
            fs.unlink(self.port, function(err){
              if (err) throw err;
              listen();
            });
          }
        });
      } else {
        listen();
      }

      // TODO: abstract
      function listen() {
        binding.bind(self.fd, self.port, self.host);
        binding.listen(self.fd, self.options.backlog);
        self.callback && self.callback();
        self.emit('listening');
      }
    }();
  }
};

/**
 * Received worker killed.
 *
 * @api private
 */

Master.prototype.workerKilled = function(worker){
  this.emit('worker killed', worker);
  switch (this.state) {
    case 'hard shutdown':
      // ignore
      break;
    case 'graceful shutdown':
      --this.pendingDeaths || this._destroy();
      break;
    case 'restarting':
      --this.pendingDeaths || (this.state = 'active');
    default:
      this.removeWorker(worker.id);
      this.spawnWorker(worker.id);
  }
};

/**
 * Remove worker `id`.
 *
 * @param {Number} id
 * @api public
 */

Master.prototype.removeWorker = function(id){
  clearTimeout(this.children[id].timeout);
  delete this.children[id];
};

/**
 * Send `sig` to all worker processes, defaults to __SIGTERM__.
 *
 * @param {String} sig
 * @api public
 */

Master.prototype.kill = function(sig){
  var self = this;
  this.emit('kill', sig);
  this.children.forEach(function(worker){
    worker.proc.kill(sig);
    if ('SIGKILL' == sig) return;
    worker.timeout = setTimeout(function(){
      self.emit('worker timeout', worker, sig);
      worker.proc.kill('SIGKILL');
    }, 1000);
  });
};

/**
 * Receive ping from worker, reply with pong.
 *
 * @param {Worker} worker
 * @api private
 */

Master.prototype.ping = function(worker){
  this.emit('pong', worker);
  worker.call('pong');
};
<commit_msg>always remove worker to prevent slow shutdowns (timers)<commit_after>

/*!
 * Engine - Master
 * Copyright(c) 2010 LearnBoost <dev@learnboost.com>
 * MIT Licensed
 */

/**
 * Module dependencies.
 */

var Worker = require('./worker')
  , EventEmitter = require('events').EventEmitter
  , binding = process.binding('net')
  , dirname = require('path').dirname
  , resolve = require('path').resolve
  , ENOENT = require('constants').ENOENT
  , net = require('net')
  , fs = require('fs')
  , os = require('os');

/**
 * Start a new `Master` with the given `server`.
 *
 * Options:
 *
 *   - `workers`  Number of workers to spawn, defaults to the number of CPUs
 *   - 'working directory`  Working directory defaulting to '/'
 *   - 'backlog`  Connection backlog, defaulting to 128
 *
 * Events:
 *
 *   - `start`. When the server is starting (pre-spawn)
 *   - `worker`. When a worker is spawned, passing the `worker`
 *   - `listening`. When the server is listening for connections (post-spawn)
 *   - `broadcast`. When master is broadcasting a `cmd` with `args`
 *   - `closing`. When master is gracefully shutting down
 *   - `close`. When master has completed shutting down
 *   - `worker killed`. When a worker has died
 *   - `kill`. When a `signal` is being sent to all workers
 *   - `restart`. Restart requested by REPL or signal
 *
 * @param {http.Server} server
 * @return {Master}
 * @api public
 */

var Master = module.exports = function Master(server) {
  var self = this;
  this.server = server;
  this.plugins = [];
  this.options = { 'working directory': '/', backlog: 128 };
  this.children = [];
  this.env = process.env.NODE_ENV || 'development';
  this.isWorker = !! process.env.ENGINE_MASTER_PID;
  this.pid = process.env.ENGINE_MASTER_PID = process.pid;
  this.cmd = process.argv.slice(1);
  this.dir = dirname(this.cmd[0]);
  this.customFds = [1, 2];
  this.state = 'active';
  this.sockPath = '/tmp/engine.sock'; // TODO: options
  this._server = net.createServer(function(sock){
    sock.setEncoding('ascii');
    sock.on('data', self.frame.bind(self));
  });
};

/**
 * Interit from `EventEmitter.prototype`.
 */

Master.prototype.__proto__ = EventEmitter.prototype;

/**
 * Worker is a receiver.
 */

require('./mixins/receiver')(Master.prototype);

/**
 * Resolve `path` relative to the server file being executed.
 *
 * @param {String} path
 * @return {String}
 * @api public
 */

Master.prototype.resolve = function(path){
  return resolve(this.dir, path);
};

/**
 * Invoke masters's `method` with worker `id`. (called from Worker)
 *
 * @param {Number} id
 * @param {String} method
 * @param {...} args
 * @api private
 */

Master.prototype.call = function(id, method){
  var args = Array.prototype.slice.call(arguments)
    , id = args.shift()
    , method = args.shift();
  this.sock.write(JSON.stringify({
      method: method
    , args: args
    , id: id
  }), 'ascii');
};

/**
 * Defer `http.Server#listen()` call.
 *
 * @param {Number|String} port or unix domain socket path
 * @param {String|Function} host or callback
 * @param {Function} callback
 * @return {Master} for chaining
 * @api public
 */

Master.prototype.listen = function(port, host, callback){
  if (this.isWorker) {
    var worker = new Worker(this);
    this.sock = net.createConnection(this.sockPath);
    this.sock.on('connect', function(){
      worker.start();
    });
  } else {
    this.port = port;
    if ('function' == typeof host) callback = host, host = null;
    this.host = host;
    this.callback = callback;
    this.start();
  }
  return this;
};

/**
 * Set option `key` to `val`.
 *
 * @param {String} key
 * @param {Mixed} val
 * @return {Master} for chaining
 * @api public
 */

Master.prototype.set = function(key, val){
  this.options[key] = val;
  return this;
};

/**
 * Check if `option` has been set.
 *
 * @param {String} option
 * @return {Boolean}
 * @api public
 */

Master.prototype.has = function(option){
  return !! this.options[option];
};

/**
 * Use the given `plugin`.
 *
 * @param {Function} plugin
 * @return {Master} for chaining
 * @api public
 */

Master.prototype.use = function(plugin){
  this.plugins.push(plugin);
  if (!this.isWorker) plugin(this);
  return this;
};

/**
 * Start master process.
 *
 *   - defaults workers to the number of CPUs available
 *   - creates listening socket
 *
 * @api private
 */

Master.prototype.start = function(){
  var self = this;

  // listen sock
  if (this.host) {
    this.fd = binding.socket('tcp' + net.isIP(this.host));
  } else if ('string' == typeof this.port) {
    this.fd = binding.socket('unix');
  } else if (this.port) {
    this.fd = binding.socket('tcp4');
  }

  // signal handlers
  process.on('SIGINT', this.destroy.bind(this));
  process.on('SIGTERM', this.destroy.bind(this));
  process.on('SIGQUIT', this.close.bind(this));
  process.on('SIGUSR2', this.restart.bind(this));
  process.on('SIGHUP', function(){});

  // Default worker to the # of cpus
  this.has('workers') || this.set('workers', os.cpus().length);

  // TCP server for IPC
  this._server.listen(this.sockPath, function(){
    self.emit('start');

    // change working dir
    process.chdir(self.options['working directory']);

    // spawn workers
    self.spawn(self.options.workers);

    // set # of pending workers
    self.starting = true;
    self.pendingWorkers = self.options.workers;
  });
};

/**
 * Spawn `n` workers.
 *
 * @param {Number} n
 * @api private
 */

Master.prototype.spawn = function(n){
  while (n--) this.spawnWorker();
};

/**
 * Spawn a worker with optional `id`.
 *
 * @param {Number} id
 * @return {Worker}
 * @api private
 */

Master.prototype.spawnWorker = function(id){
  var worker = new Worker(this).spawn();

  // id given
  if ('number' == typeof id) {
    this.children[id] = worker;
    worker.id = id;
  // generate an id
  } else {
    var len = this.children.push(worker);
    worker.id = len - 1;
  }

  // TODO: refactor
  var json = JSON.stringify({ method: 'connect', args: worker.id });
  worker.sock.write(json, 'ascii', this.fd);

  // emit
  this.emit('worker', worker);

  return worker;
};

/**
 * Broadcast `cmd` with the given `args` to each worker.
 *
 * @param {String} cmd
 * @param {Mixed} args
 * @api public
 */

Master.prototype.broadcast = function(cmd, args){
  var json = JSON.stringify({ cmd: cmd, args: args });
  this.emit('broadcast', cmd, args);
  this.children.forEach(function(worker){
    worker.sock.write(json, 'ascii');
  }, this);
};

/**
 * Graceful shutdown, wait for all workers
 * to reply before exiting.
 *
 * @api public
 */

Master.prototype.close = function(){
  this.emit('closing');
  this.kill('SIGQUIT');
  this.state = 'graceful shutdown';
  this.pendingDeaths = this.children.length;
};

/**
 * Hard shutdwn, immediately kill all workers.
 *
 * @api public
 */

Master.prototype.destroy = function(){
  this.emit('closing');
  this.kill('SIGKILL');
  this.state = 'hard shutdown';
  this._destroy();
};

/**
 * Gracefully restart all workers, by sending __SIGQUIT__
 * to them, enabling master to re-spawn them.
 *
 * @api public
 */

Master.prototype.restart = function(){
  this.emit('restart');
  this.state = 'restarting';
  this.kill('SIGQUIT');
  this.pendingDeaths = this.children.length;
};
/**
 * Immediate shutdown, used by `Master#close()`
 * and `Master#destroy()` when all workers have shutdown.
 *
 * @api private
 */

Master.prototype._destroy = function(){
  var self = this;
  self._server.close();
  if ('string' == typeof self.port) {
    fs.unlink(self.port, function(){
      self.emit('close');
    });
  } else {
    self.emit('close');
  }
};

/**
 * Worker is connected, on boot
 * wait for all workers to connect,
 * then emit the 'listening' event.
 *
 * @param {Worker} worker
 * @api private
 */

Master.prototype.connect = function(worker){
  var self = this;
  this.emit('worker connected', worker);
  if (this.starting) {
    --this.pendingWorkers || function(){
      // TODO: abstract
      if ('string' == typeof self.port) {
        fs.stat(self.port, function(err){
          if (err) {
            if (ENOENT != err.errno) throw err;
            listen();
          } else {
            fs.unlink(self.port, function(err){
              if (err) throw err;
              listen();
            });
          }
        });
      } else {
        listen();
      }

      // TODO: abstract
      function listen() {
        binding.bind(self.fd, self.port, self.host);
        binding.listen(self.fd, self.options.backlog);
        self.callback && self.callback();
        self.emit('listening');
      }
    }();
  }
};

/**
 * Received worker killed.
 *
 * @api private
 */

Master.prototype.workerKilled = function(worker){
  this.emit('worker killed', worker);

  // always remove worker
  this.removeWorker(worker.id);

  // state specifics
  switch (this.state) {
    case 'hard shutdown':
      // ignore
      break;
    case 'graceful shutdown':
      --this.pendingDeaths || this._destroy();
      break;
    case 'restarting':
      --this.pendingDeaths || (this.state = 'active');
    default:
      this.spawnWorker(worker.id);
  }
};

/**
 * Remove worker `id`.
 *
 * @param {Number} id
 * @api public
 */

Master.prototype.removeWorker = function(id){
  clearTimeout(this.children[id].timeout);
  delete this.children[id];
};

/**
 * Send `sig` to all worker processes, defaults to __SIGTERM__.
 *
 * @param {String} sig
 * @api public
 */

Master.prototype.kill = function(sig){
  var self = this;
  this.emit('kill', sig);
  this.children.forEach(function(worker){
    worker.proc.kill(sig);
    if ('SIGKILL' == sig) return;
    worker.timeout = setTimeout(function(){
      self.emit('worker timeout', worker, sig);
      worker.proc.kill('SIGKILL');
    }, 1000);
  });
};

/**
 * Receive ping from worker, reply with pong.
 *
 * @param {Worker} worker
 * @api private
 */

Master.prototype.ping = function(worker){
  this.emit('pong', worker);
  worker.call('pong');
};
<|endoftext|>


<commit_before>var os = require('os'),
    cluster = require('cluster'),
    extend = require('extend'),
    legacy = require('./legacy'),
    ClusterProcess = require('./cluster_process'),
    WorkerWrapper = require('./worker_wrapper'),
    Port = require('./port'),
    Master;

/**
 * @constructor
 * @class Master
 * @augments ClusterProcess
 */
Master = ClusterProcess.create(function Master() {
    Master.__super.apply(this, arguments);

    /**
     * @type {Object}
     * @property {WorkerWrapper} *
     * @public
     * @todo make it private or public immutable
     */
    this.workers = {};

    /**
     * Contains workers queued to restart.
     * @see Master#processRestartQueue
     * @type {WorkerWrapper[]}
     */
    this.restartQueue = [];

    /**
     * If `true` processing Master#restartQueue in progress.
     * @type {Boolean}
     * @private
     */
    this._isRestartQueued = false;

    /**
     * Configuration object to pass to cluster.setupMaster()
     * @type {Object}
     * @private
     */
    this._masterOpts = {};

    this.id = 0;
    this.wid = 0;
    this.pid = process.pid;

    this.on('worker ready', this._onWorkerReady.bind(this));
    this.on('worker state', this._cleanupUnixSockets.bind(this));
    this.on('worker exit', this._checkWorkersAlive.bind(this));

    // @todo make it optional?
    process.on('SIGINT', this._onSignalQuit.bind(this));
    process.on('SIGQUIT', this._onSignalQuit.bind(this));
});

/**
 * Allows same object structure as cluster.setupMaster().
 * This function must be used instead of cluster.setupMaster(),
 * because all calls of cluster.setupMaster() ignored, except first one.
 * An instance of Master will call it, when running.
 * @param {Object} opts
 * @see {@link http://nodejs.org/api/cluster.html#cluster_cluster_setupmaster_settings}
 */
Master.prototype.setup = function(opts) {
    extend(this._masterOpts, opts);
};

/**
 * SIGINT and SIGQUIT handler
 * @private
 */
Master.prototype._onSignalQuit = function() {
    this
        .once('shutdown', function() {
            process.exit(0);
        })
        .shutdown();
};

/**
 * Remove not used unix socket before worker will try to listen it.
 * @param {WorkerWrapper} worker
 * @param {WorkerWrapperState} state
 * @private
 */
Master.prototype._cleanupUnixSockets = function(worker, state) {
    var port = worker.options.port;

    if (this._isRestartQueued ||
        state !== WorkerWrapper.STATES.LAUNCHING ||
        port.family !== Port.UNIX) {
        return;
    }

    var self = this,
        inUse = this.getWorkersArray().some(function(w) {
            return worker.wid !== w.wid &&
                w.isRunning() &&
                port.isEqualTo(w.options.port);
        });

    if ( ! inUse) {
        port.unlink(function(err) {
            if (err) {
                self.emit('error', err);
            }
        });
    }
};

/**
 * Check for alive workers, if no one here, then emit "shutdown".
 * @private
 */
Master.prototype._checkWorkersAlive = function() {
    var workers = this.getWorkersArray(),
        alive = workers.reduce(function(count, w) {
                return w.dead ? count - 1 : count;
            }, workers.length);

    if (alive === 0) {
        this.emit('shutdown');
    }
};

/**
 * @param {WorkerWrapper} worker
 * @private
 */
Master.prototype._onWorkerReady = function(worker) {
    // @todo what if one worker from queue die? Queue processing will be stopped...
    if (this._isRestartQueued &&
        worker.wid === this.restartQueue[0].wid) {
        // worker on top of the queue has been restarted drop it...
        this.restartQueue.shift();
        // ...and take the next or finish queue processing
        this._restartWorkerFromQueue();
    }
};

/**
 * Repeat WorkerWrapper events on Master and add 'worker ' prefix to event names
 * so for example 'online' became 'worker online'
 * @private
 * @param {WorkerWrapper} worker
 */
Master.prototype._proxyWorkerEvents = function(worker) {
    WorkerWrapper.EVENTS
        .forEach(function(eventName) {
            worker.on(eventName, this.emit.bind(this, 'worker ' + eventName, worker));
        }, this);
};

/**
 * @returns {WorkerWrapper[]} workers array
 */
Master.prototype.getWorkersArray = function() {
    if ( ! this._workersArrayCache) {
        this._workersArrayCache = Object.keys(this.workers).map(function(wid) {
            return this.workers[wid];
        }, this);
    }

    return this._workersArrayCache;
};

/**
 * Add worker to the pool
 * @param {WorkerWrapper} worker
 * @returns {Master} self
 * @public
 */
Master.prototype.add = function(worker) {
    // invalidate Master#getWorkersArray cache
    this._workersArrayCache = null;

    this.workers[worker.wid] = worker;
    this._proxyWorkerEvents(worker);

    return this;
};

/**
 * Iterate over workers in the pool.
 * @param {Function} fn
 * @public
 * @returns {Master} self
 *
 * @description Shortcut for:
 *      master.getWorkersArray().forEach(function(worker) {
 *          // fn
 *      }, master);
 */
Master.prototype.forEach = function(fn) {
    this.getWorkersArray().forEach(function(worker) {
        fn.call(this, worker);
    }, this);

    return this;
};

/**
 * Configure cluster
 * @override ClusterProcess
 * @private
 */
Master.prototype._onConfigured = function() {
    Master.__super.prototype._onConfigured.apply(this, arguments);

    var // WorkerWrapper options
        forkTimeout = this.config.get('control.forkTimeout'),
        stopTimeout = this.config.get('control.stopTimeout'),
        exitThreshold = this.config.get('control.exitThreshold'),
        allowedSequentialDeaths = this.config.get('control.allowedSequentialDeaths'),
        debugPort = this.config.get('debug.port'),

        // workers and groups count
        i = 0,
        count = this.config.get('workers', os.cpus().length),
        isServerPortSet = this.config.has('server.port'),
        port,
        groups = this.config.get('server.groups', 1),
        group = 0,
        workersPerGroup = Math.floor(count / groups),
        workersInGroup = 0;

    if (isServerPortSet) {
        port = new Port(this.config.get('server.port'));
    }

    // remove `--debug` option from passed to master
    this.setup({
        execArgv : process.execArgv.filter(function(s) {
            return ! /^\-\-debug/.test(s);
        })
    });

    // create pool of workers
    while (count > i++) {
        this.add(new WorkerWrapper(this, {
            forkTimeout : forkTimeout,
            stopTimeout : stopTimeout,
            exitThreshold : exitThreshold,
            allowedSequentialDeaths : allowedSequentialDeaths,
            debugPort : debugPort && debugPort + i - 1,
            port : isServerPortSet ? port.next(group) : 0,
            _maxListeners : this._maxListeners
        }));

        // groups > 1, current group is full and
        // last workers can form at least more one group
        if (groups > 1 &&
            ++workersInGroup >= workersPerGroup &&
            count - (group + 1) * workersPerGroup >= workersPerGroup) {
            workersInGroup = 0;
            group++;
        }
    }
};

/**
 * @param {Number[]} wids Array of `WorkerWrapper#wid` values
 * @param {String} event wait for
 * @param {Function} callback
 * @public
 * @returns {Master} self
 */
Master.prototype.waitForWorkers = function(wids, event, callback) {
    var self = this;

    function onWorkerState(worker) {
        var idx = wids.indexOf(worker.wid);

        if (idx > -1) {
            wids.splice(idx, 1);
        }

        if (wids.length === 0) {
            self.removeListener(event, onWorkerState);
            callback.call(self);
        }
    }

    if (wids.length > 0) {
        this.on(event, onWorkerState);
    } else {
        legacy.setImmediate(callback.bind(self));
    }

    return this;
};

/**
 * @event Master#running
 */

/**
 * Fork workers.
 * Execution will be delayed until Master became configured
 * (`configured` event fired).
 * @method
 * @returns {Master} self
 * @public
 * @fires Master#running then workers spawned and ready.
 *
 * @example
 *      // file: master.js
 *      var master = require('luster');
 *
 *      master
 *          .configure({ app : 'worker' })
 *          .run();
 *
 *      // there is master is still not running anyway
 *      // it will run immediate once configured and
 *      // current thread execution done
 */
Master.prototype.run = Master.whenInitialized(function() {
    cluster.setupMaster(this._masterOpts);

    this.waitForWorkers(
        this.getWorkersArray().map(function(worker) {
            worker.run();

            return worker.wid;
        }),
        'worker ready',
        function() {
            this.emit('running');
        });

    return this;
});

/**
 * @event Master#restarted
 */

/**
 * Hard workers restart: all workers will be restarted at same time.
 * @public
 * @returns {Master} self
 * @fires Master#restarted then workers spawned and ready.
 */
Master.prototype.restart = function() {
    this.waitForWorkers(
        this.getWorkersArray().map(function(worker) {
            worker.restart();

            return worker.wid;
        }),
        'worker ready',
        function() {
            this.emit('restarted');
        });

    return this;
};

/**
 * Workers will be restarted one by one.
 * @public
 */
Master.prototype.softRestart = function() {
    this.forEach(function(worker) {
        this.restartQueue.push(worker);
    });
    this.processRestartQueue();
};

/**
 * Start processing restartQueue if it isn't started already.
 * @see event:Master#'worker state'('running')
 * @public
 */
Master.prototype.processRestartQueue = function() {
    if ( ! this._isRestartQueued) {
        this._isRestartQueued = true;
        this._restartWorkerFromQueue();
    }
};

/**
 * Restart worker from the top of the Master#restartQueue
 * @private
 */
Master.prototype._restartWorkerFromQueue = function() {
    if (this.restartQueue.length === 0) {
        this.emit('restarted');
        this._isRestartQueued = false;
    } else {
        this.restartQueue[0].restart();
    }
};

/**
 * @override
 * @see ClusterProcess
 * @private
 */
Master.prototype._setupIPCMessagesHandler = function() {
    this.on('worker message', this._onMessage.bind(this));
};

/**
 * RPC to all workers
 * @method
 * @param {String} name of called command in the worker
 * @param {*} ...args
 * @public
 */
Master.prototype.remoteCallToAll = function(name) {
    /* jshint unused:false */
    var args = Array.prototype.slice.call(arguments, 0);

    this.forEach(function(worker) {
        if (worker.ready) {
            worker.remoteCall.apply(worker, args);
        } else {
            worker.on('ready', function(a) {
                worker.remoteCall.apply(worker, args);
            }.bind(worker, args));
        }
    });
};

/**
 * Broadcast event to all workers.
 * @method
 * @param {String} event of called command in the worker
 * @param {*} ...args
 * @public
 */
Master.prototype.broadcastEventToAll = function() {
    this.forEach(function(worker) {
        if (worker.ready) {
            worker.broadcastEvent.apply(worker, arguments);
        }
    });
};

/**
 * Emit event on master and all workers in "ready" state.
 * @method
 * @param {String} event of called command in the worker
 * @param {*} ...args
 * @public
 */
Master.prototype.emitToAll = function() {
    this.emit.apply(this, arguments);
    this.broadcastEventToAll.apply(this, arguments);
};

/**
 * @event Master#shutdown
 */

/**
 * Stop all workers and emit `Master#shutdown` event after successful shutdown of all workers.
 * @fires Master#shutdown
 * @returns {Master}
 */
Master.prototype.shutdown = function() {
    var stoppedWorkers = [];

    this.forEach(function(worker) {
        if (worker.isRunning()) {
            worker.stop();
            stoppedWorkers.push(worker.wid);
        }
    });

    this.waitForWorkers(
        stoppedWorkers,
        'worker exit',
        function() {
            this.emit('shutdown');
        });

    return this;
};

module.exports = Master;
<commit_msg>fix Master#broadcastToAll()<commit_after>var os = require('os'),
    cluster = require('cluster'),
    extend = require('extend'),
    legacy = require('./legacy'),
    ClusterProcess = require('./cluster_process'),
    WorkerWrapper = require('./worker_wrapper'),
    Port = require('./port'),
    Master;

/**
 * @constructor
 * @class Master
 * @augments ClusterProcess
 */
Master = ClusterProcess.create(function Master() {
    Master.__super.apply(this, arguments);

    /**
     * @type {Object}
     * @property {WorkerWrapper} *
     * @public
     * @todo make it private or public immutable
     */
    this.workers = {};

    /**
     * Contains workers queued to restart.
     * @see Master#processRestartQueue
     * @type {WorkerWrapper[]}
     */
    this.restartQueue = [];

    /**
     * If `true` processing Master#restartQueue in progress.
     * @type {Boolean}
     * @private
     */
    this._isRestartQueued = false;

    /**
     * Configuration object to pass to cluster.setupMaster()
     * @type {Object}
     * @private
     */
    this._masterOpts = {};

    this.id = 0;
    this.wid = 0;
    this.pid = process.pid;

    this.on('worker ready', this._onWorkerReady.bind(this));
    this.on('worker state', this._cleanupUnixSockets.bind(this));
    this.on('worker exit', this._checkWorkersAlive.bind(this));

    // @todo make it optional?
    process.on('SIGINT', this._onSignalQuit.bind(this));
    process.on('SIGQUIT', this._onSignalQuit.bind(this));
});

/**
 * Allows same object structure as cluster.setupMaster().
 * This function must be used instead of cluster.setupMaster(),
 * because all calls of cluster.setupMaster() ignored, except first one.
 * An instance of Master will call it, when running.
 * @param {Object} opts
 * @see {@link http://nodejs.org/api/cluster.html#cluster_cluster_setupmaster_settings}
 */
Master.prototype.setup = function(opts) {
    extend(this._masterOpts, opts);
};

/**
 * SIGINT and SIGQUIT handler
 * @private
 */
Master.prototype._onSignalQuit = function() {
    this
        .once('shutdown', function() {
            process.exit(0);
        })
        .shutdown();
};

/**
 * Remove not used unix socket before worker will try to listen it.
 * @param {WorkerWrapper} worker
 * @param {WorkerWrapperState} state
 * @private
 */
Master.prototype._cleanupUnixSockets = function(worker, state) {
    var port = worker.options.port;

    if (this._isRestartQueued ||
        state !== WorkerWrapper.STATES.LAUNCHING ||
        port.family !== Port.UNIX) {
        return;
    }

    var self = this,
        inUse = this.getWorkersArray().some(function(w) {
            return worker.wid !== w.wid &&
                w.isRunning() &&
                port.isEqualTo(w.options.port);
        });

    if ( ! inUse) {
        port.unlink(function(err) {
            if (err) {
                self.emit('error', err);
            }
        });
    }
};

/**
 * Check for alive workers, if no one here, then emit "shutdown".
 * @private
 */
Master.prototype._checkWorkersAlive = function() {
    var workers = this.getWorkersArray(),
        alive = workers.reduce(function(count, w) {
                return w.dead ? count - 1 : count;
            }, workers.length);

    if (alive === 0) {
        this.emit('shutdown');
    }
};

/**
 * @param {WorkerWrapper} worker
 * @private
 */
Master.prototype._onWorkerReady = function(worker) {
    // @todo what if one worker from queue die? Queue processing will be stopped...
    if (this._isRestartQueued &&
        worker.wid === this.restartQueue[0].wid) {
        // worker on top of the queue has been restarted drop it...
        this.restartQueue.shift();
        // ...and take the next or finish queue processing
        this._restartWorkerFromQueue();
    }
};

/**
 * Repeat WorkerWrapper events on Master and add 'worker ' prefix to event names
 * so for example 'online' became 'worker online'
 * @private
 * @param {WorkerWrapper} worker
 */
Master.prototype._proxyWorkerEvents = function(worker) {
    WorkerWrapper.EVENTS
        .forEach(function(eventName) {
            worker.on(eventName, this.emit.bind(this, 'worker ' + eventName, worker));
        }, this);
};

/**
 * @returns {WorkerWrapper[]} workers array
 */
Master.prototype.getWorkersArray = function() {
    if ( ! this._workersArrayCache) {
        this._workersArrayCache = Object.keys(this.workers).map(function(wid) {
            return this.workers[wid];
        }, this);
    }

    return this._workersArrayCache;
};

/**
 * Add worker to the pool
 * @param {WorkerWrapper} worker
 * @returns {Master} self
 * @public
 */
Master.prototype.add = function(worker) {
    // invalidate Master#getWorkersArray cache
    this._workersArrayCache = null;

    this.workers[worker.wid] = worker;
    this._proxyWorkerEvents(worker);

    return this;
};

/**
 * Iterate over workers in the pool.
 * @param {Function} fn
 * @public
 * @returns {Master} self
 *
 * @description Shortcut for:
 *      master.getWorkersArray().forEach(function(worker) {
 *          // fn
 *      }, master);
 */
Master.prototype.forEach = function(fn) {
    this.getWorkersArray().forEach(function(worker) {
        fn.call(this, worker);
    }, this);

    return this;
};

/**
 * Configure cluster
 * @override ClusterProcess
 * @private
 */
Master.prototype._onConfigured = function() {
    Master.__super.prototype._onConfigured.apply(this, arguments);

    var // WorkerWrapper options
        forkTimeout = this.config.get('control.forkTimeout'),
        stopTimeout = this.config.get('control.stopTimeout'),
        exitThreshold = this.config.get('control.exitThreshold'),
        allowedSequentialDeaths = this.config.get('control.allowedSequentialDeaths'),
        debugPort = this.config.get('debug.port'),

        // workers and groups count
        i = 0,
        count = this.config.get('workers', os.cpus().length),
        isServerPortSet = this.config.has('server.port'),
        port,
        groups = this.config.get('server.groups', 1),
        group = 0,
        workersPerGroup = Math.floor(count / groups),
        workersInGroup = 0;

    if (isServerPortSet) {
        port = new Port(this.config.get('server.port'));
    }

    // remove `--debug` option from passed to master
    this.setup({
        execArgv : process.execArgv.filter(function(s) {
            return ! /^\-\-debug/.test(s);
        })
    });

    // create pool of workers
    while (count > i++) {
        this.add(new WorkerWrapper(this, {
            forkTimeout : forkTimeout,
            stopTimeout : stopTimeout,
            exitThreshold : exitThreshold,
            allowedSequentialDeaths : allowedSequentialDeaths,
            debugPort : debugPort && debugPort + i - 1,
            port : isServerPortSet ? port.next(group) : 0,
            _maxListeners : this._maxListeners
        }));

        // groups > 1, current group is full and
        // last workers can form at least more one group
        if (groups > 1 &&
            ++workersInGroup >= workersPerGroup &&
            count - (group + 1) * workersPerGroup >= workersPerGroup) {
            workersInGroup = 0;
            group++;
        }
    }
};

/**
 * @param {Number[]} wids Array of `WorkerWrapper#wid` values
 * @param {String} event wait for
 * @param {Function} callback
 * @public
 * @returns {Master} self
 */
Master.prototype.waitForWorkers = function(wids, event, callback) {
    var self = this;

    function onWorkerState(worker) {
        var idx = wids.indexOf(worker.wid);

        if (idx > -1) {
            wids.splice(idx, 1);
        }

        if (wids.length === 0) {
            self.removeListener(event, onWorkerState);
            callback.call(self);
        }
    }

    if (wids.length > 0) {
        this.on(event, onWorkerState);
    } else {
        legacy.setImmediate(callback.bind(self));
    }

    return this;
};

/**
 * @event Master#running
 */

/**
 * Fork workers.
 * Execution will be delayed until Master became configured
 * (`configured` event fired).
 * @method
 * @returns {Master} self
 * @public
 * @fires Master#running then workers spawned and ready.
 *
 * @example
 *      // file: master.js
 *      var master = require('luster');
 *
 *      master
 *          .configure({ app : 'worker' })
 *          .run();
 *
 *      // there is master is still not running anyway
 *      // it will run immediate once configured and
 *      // current thread execution done
 */
Master.prototype.run = Master.whenInitialized(function() {
    cluster.setupMaster(this._masterOpts);

    this.waitForWorkers(
        this.getWorkersArray().map(function(worker) {
            worker.run();

            return worker.wid;
        }),
        'worker ready',
        function() {
            this.emit('running');
        });

    return this;
});

/**
 * @event Master#restarted
 */

/**
 * Hard workers restart: all workers will be restarted at same time.
 * @public
 * @returns {Master} self
 * @fires Master#restarted then workers spawned and ready.
 */
Master.prototype.restart = function() {
    this.waitForWorkers(
        this.getWorkersArray().map(function(worker) {
            worker.restart();

            return worker.wid;
        }),
        'worker ready',
        function() {
            this.emit('restarted');
        });

    return this;
};

/**
 * Workers will be restarted one by one.
 * @public
 */
Master.prototype.softRestart = function() {
    this.forEach(function(worker) {
        this.restartQueue.push(worker);
    });
    this.processRestartQueue();
};

/**
 * Start processing restartQueue if it isn't started already.
 * @see event:Master#'worker state'('running')
 * @public
 */
Master.prototype.processRestartQueue = function() {
    if ( ! this._isRestartQueued) {
        this._isRestartQueued = true;
        this._restartWorkerFromQueue();
    }
};

/**
 * Restart worker from the top of the Master#restartQueue
 * @private
 */
Master.prototype._restartWorkerFromQueue = function() {
    if (this.restartQueue.length === 0) {
        this.emit('restarted');
        this._isRestartQueued = false;
    } else {
        this.restartQueue[0].restart();
    }
};

/**
 * @override
 * @see ClusterProcess
 * @private
 */
Master.prototype._setupIPCMessagesHandler = function() {
    this.on('worker message', this._onMessage.bind(this));
};

/**
 * RPC to all workers
 * @method
 * @param {String} name of called command in the worker
 * @param {*} ...args
 * @public
 */
Master.prototype.remoteCallToAll = function(name) {
    /* jshint unused:false */
    var args = Array.prototype.slice.call(arguments, 0);

    this.forEach(function(worker) {
        if (worker.ready) {
            worker.remoteCall.apply(worker, args);
        } else {
            worker.on('ready', function(a) {
                worker.remoteCall.apply(worker, args);
            }.bind(worker, args));
        }
    });
};

/**
 * Broadcast event to all workers.
 * @method
 * @param {String} event of called command in the worker
 * @param {*} ...args
 * @public
 */
Master.prototype.broadcastEventToAll = function() {
    var args = Array.prototype.slice.call(arguments, 0);
    this.forEach(function(worker) {
        if (worker.ready) {
            worker.broadcastEvent.apply(worker, args);
        }
    });
};

/**
 * Emit event on master and all workers in "ready" state.
 * @method
 * @param {String} event of called command in the worker
 * @param {*} ...args
 * @public
 */
Master.prototype.emitToAll = function() {
    this.emit.apply(this, arguments);
    this.broadcastEventToAll.apply(this, arguments);
};

/**
 * @event Master#shutdown
 */

/**
 * Stop all workers and emit `Master#shutdown` event after successful shutdown of all workers.
 * @fires Master#shutdown
 * @returns {Master}
 */
Master.prototype.shutdown = function() {
    var stoppedWorkers = [];

    this.forEach(function(worker) {
        if (worker.isRunning()) {
            worker.stop();
            stoppedWorkers.push(worker.wid);
        }
    });

    this.waitForWorkers(
        stoppedWorkers,
        'worker exit',
        function() {
            this.emit('shutdown');
        });

    return this;
};

module.exports = Master;
<|endoftext|>


<commit_before>'use strict';
/* jshint camelcase: false */

/**
 * nodecached server: memory functions.
 * (C) 2013 Alex Fernández.
 */

// requires
var Log = require('log');
var token = require('./token.js');
var packageJson = require('../package.json');
var testing = require('testing');

// globals
var log = new Log('info');

/**
 * A record to store in memory. Params:
 *	- value: the value to store.
 *	- expirationSeconds: the number of seconds that the item must live, at most.
 *	- flags: to store with the value.
 */
var Record = function(value, expirationSeconds, flags)
{
	// self-reference
	var self = this;

	// attributes
	self.flags = flags;
	self.expiration = 0;
	self.value = value;

	// init
	if (expirationSeconds)
	{
		self.expiration = Date.now() + 1000 * expirationSeconds;
	}

	/**
	 * Find out if the record is still valid.
	 */
	self.isValid = function()
	{
		if (!self.expiration)
		{
			return true;
		}
		log.debug('Now: %s, expiration: %s', Date.now(), self.expiration);
		return (Date.now() < self.expiration);
	};
};

/**
 * Test record creation.
 */
function testRecord(callback)
{
	var record = new Record('b', 1, null);
	testing.assert(record.isValid(), 'Should be valid', callback);
	record = new Record('b', -1, null);
	testing.assert(!record.isValid(), 'Should not be valid', callback);
	testing.success(callback);
}

/**
 * An in-memory cache object.
 * Options can be an integer defining the size of the cache in items.
 * Otherwise it is an object that may hold:
 *	- maxSizeMb: max size of the cache in MB.
 * Compatible with jscache API: https://github.com/monsur/jscache.
 */
exports.Cache = function(options)
{
	// self-reference
	var self = this;

	// attributes
	var records = {};
	var maxRecords;
	var maxSizeMb;
	var totalItems = 0;

	// init
	init();

	/**
	 * Init memory structures.
	 */
	function init()
	{
		if (!options)
		{
			return;
		}
		var size = parseInt(options, 10);
		if (size)
		{
			maxRecords = size;
			return;
		}
		maxSizeMb = options.maxSizeMb;
	}

	/**
	 * Get a record from the cache. Params:
	 *	- key: the key to look up.
	 * Returns the current record (with flags, expiration and value), or null if not in the cache.
	 */
	self.getRecord = function(key)
	{
		if (!(key in records))
		{
			return null;
		}
		var record = records[key];
		if (!record.isValid())
		{
			return null;
		}
		return record;
	};

	/**
	 * Set a record in the cache. Params:
	 *	- key: the key to look up.
	 *	- value: to store.
	 *	- expirationSeconds: time to live, or 0 for unlimited.
	 *	- flags: for the value.
	 * Returns true if the item was stored.
	 */
	self.setRecord = function(key, value, expirationSeconds, flags)
	{
		var record = new Record(value, expirationSeconds, flags);
		log.debug('storing: %j', record);
		records[key] = record;
		totalItems++;
		process.nextTick(purge);
		return true;
	};

	/**
	 * Delete a record from the cache. Params:
	 *	- key: the key to look up.
	 * Returns true if the item was deleted, false if not in the cache.
	 */
	self.deleteRecord = function(key)
	{
		if (!(key in records))
		{
			return false;
		}
		delete records[key];
		process.nextTick(purge);
		return true;
	};

	/**
	 * Return an object with stats. Params:
	 *	- type: of stats wanted, ignored by now.
	 */
	self.stats = function(type)
	{
		if (type)
		{
			log.error('Unsupported stats type %s', type);
		}
		return {
			pid: process.pid,
			uptime: process.uptime(),
			time: Math.floor(Date.now() / 1000),
			version: self.version(),
			curr_items: records.countProperties(),
			total_items: totalItems,
			bytes: process.memoryUsage().rss,
			max_bytes: maxSizeMb * 1024 * 1024,
			tcpport: options.port,
			num_threads: 1,
			cas_enabled: 'no',
			evictions: 'on',
		};
	};

	/**
	 * Return the current version string.
	 */
	self.version = function()
	{
		return packageJson.name + '-' + packageJson.version;
	};

	/**
	 * Get item directly. Params:
	 *	- key: the key to look up.
	 * Part of jscache API.
	 */
	self.getItem = function(key)
	{
		var record = records[key];
		log.debug('Got record: %j', record);
		if (!record)
		{
			return null;
		}
		return record.value;
	};

	/**
	 * Set an item directly. Params:
	 *	- key: the key to look up.
	 *	- value: to set.
	 *	- itemOptions: may contain:
	 *		- expirationAbsolute: the time to expire.
	 *		- expirationSeconds: seconds to expire.
	 * Part of jscache API.
	 */
	self.setItem = function(key, value, itemOptions)
	{
		var lifetime = 0;
		if (itemOptions.expirationSeconds)
		{
			lifetime = itemOptions.expirationSeconds;
		}
		if (itemOptions.expirationAbsolute)
		{
			lifetime = (Date.now() - itemOptions.expirationAbsolute.getTime()) / 1000;
		}
		log.debug('storing: %j', value);
		self.setRecord(key, value, lifetime, 0);
	};

	/**
	 * Remove an item. Params:
	 *	- key: the key to look up.
	 * Part of jscache API.
	 */
	self.removeItem = function(key)
	{
		return self.deleteRecord(key);
	};

	/**
	 * Purge memory, if needed.
	 */
	function purge()
	{
		if (maxRecords)
		{
			purgeRecords();
		}
		if (maxSizeMb)
		{
			purgeMemory();
		}
	}

	function purgeRecords()
	{
		if (records.countProperties() < maxRecords)
		{
			return;
		}
		for (var key in records)
		{
			delete records[key];
			if (records.countProperties() < maxRecords)
			{
				return;
			}
		}
	}

	function purgeMemory()
	{
		var usageMb = process.memoryUsage().rss / 1024 / 1024;
		if (usageMb < maxSizeMb)
		{
			return;
		}
		log.info('Memory used %s, max %s: purging', usageMb, maxSizeMb);
		for (var key in records)
		{
			if (!records[key].isValid())
			{
				delete records[key];
			}
		}
		if (usageMb < maxSizeMb)
		{
			return;
		}
		for (key in records)
		{
			delete records[key];
			if (usageMb < maxSizeMb)
			{
				return;
			}
		}
	}
};

/**
 * Test to get, set and delete a test record.
 */
function testGetRecord(callback)
{
	var key = 'test#' + token.create();
	var value = {a: 'b'};
	var cache = new exports.Cache();
	testing.assertEquals(cache.getRecord(key), null, 'Could get record before setting it', callback);
	testing.assert(cache.setRecord(key, value), 'Could not set record', callback);
	var result = cache.getRecord(key);
	testing.assert(result, 'Could not get record', callback);
	testing.assertEquals(result.value, value, 'Got different record', callback);
	testing.assert(cache.deleteRecord(key), 'Could not delete record', callback);
	testing.assertEquals(cache.getRecord(key), null, 'Could get record after deleting it', callback);
	testing.success(callback);
}

/**
 * Test the stats function.
 */
function testStats(callback)
{
	var key = 'test#' + token.create();
	var value = {a: 'b'};
	var cache = new exports.Cache();
	testing.assert(cache.setRecord(key, value), 'Could not set record 1', callback);
	testing.assert(cache.setRecord(key, value), 'Could not set record 2', callback);
	var stats = cache.stats();
	testing.assert(stats, 'Could not get stats', callback);
	testing.assertEquals(stats.curr_items, 1, 'Invalid curr_items', callback);
	testing.assertEquals(stats.total_items, 2, 'Invalid total_items', callback);
	testing.success(callback);
}

/**
 * Test other functions.
 */
function testOther(callback)
{
	var cache = new exports.Cache();
	testing.assert(cache.version().startsWith('nodecached'), 'Invalid version string', callback);
	testing.success(callback);
}

/**
 * Get the default cache, or create it if necessary.
 */
function getDefaultCache()
{
	if (!getDefaultCache.cache)
	{
		log.debug('Creating default cache');
		getDefaultCache.cache = new exports.Cache(0);
	}
	return getDefaultCache.cache;
}

/**
 * Get a value from the cache. Params:
 *	- key: to look up.
 *	- callback: function(error, result) to call with the value.
 * Part of the node-memcached API.
 */
exports.get = function(key, callback)
{
	if (!callback)
	{
		log.error('Missing callback in get()');
		return;
	}
	callback(null, getDefaultCache().getItem(key));
};

/**
 * Set a value in the cache. Params:
 *	- key: to look up.
 *	- value: to store.
 *	- lifetime: seconds to live, 0 for always.
 *	- callback: function(error, result) called with always null, true.
 * Part of the node-memcached API.
 */
exports.set = function(key, value, lifetime, callback)
{
	var options = {
		expirationSeconds: lifetime,
	};
	getDefaultCache().setItem(key, value, options);
	if (callback)
	{
		callback(null, true);
	}
};

/**
 * Delete a value. Params:
 *	- key: to look up.
 *	- value: to store.
 *	- lifetime: seconds to live, 0 for always.
 *	- callback: function(error, result) where result is true if deleted.
 * Part of the node-memcached API.
 */
exports.delete = function(key, callback)
{
	var deleted = getDefaultCache().removeItem(key);
	if (callback)
	{
		callback(null, deleted);
	}
};

/**
 * Test the node-memcached API.
 */
function testNodeMemcachedAPI(callback)
{
	var key = 'test#' + token.create();
	var value = {a: 'b'};
	exports.get(key, function(error, result)
	{
		testing.check(error, 'Error getting first', callback);
		testing.assert(!result, 'Should not get', callback);
		exports.set(key, value, 10, function(error, result)
		{
			testing.check(error, 'Error setting', callback);
			testing.assert(result, 'Could not set', callback);
			exports.get(key, function(error, result)
			{
				testing.check(error, 'Error getting', callback);
				testing.assert(result, 'Could not get', callback);
				testing.assertEquals(result, value, 'Different get', callback);
				exports.delete(key, function(error, result)
				{
					testing.check(error, 'Error deleting', callback);
					testing.assert(result, 'Could not delete', callback);
					exports.get(key, function(error, result)
					{
						testing.check(error, 'Error getting last', callback);
						testing.assert(!result, 'Should not get last', callback);
						testing.success(callback);
					});
				});
			});
		});
	});
}

/**
 * Run all tests.
 */
exports.test = function(callback)
{
	log.debug('Running tests');
	testing.run({
		testRecord: testRecord,
		testGetRecord: testGetRecord,
		testNodeMemcached: testNodeMemcachedAPI,
		testStats: testStats,
		testOther: testOther,
	}, callback);
};

// run tests if invoked directly
if (__filename == process.argv[1])
{
	exports.test(testing.show);
}

<commit_msg>Support absolute seconds as expiration time.<commit_after>'use strict';
/* jshint camelcase: false */

/**
 * nodecached server: memory functions.
 * (C) 2013 Alex Fernández.
 */

// requires
var Log = require('log');
var token = require('./token.js');
var packageJson = require('../package.json');
var testing = require('testing');

// globals
var log = new Log('info');

// constants
var MAX_RELATIVE_SECONDS = 60*60*24*30;

/**
 * A record to store in memory. Params:
 *	- value: the value to store.
 *	- expirationSeconds: the number of seconds that the item must live, at most.
 *	- flags: to store with the value.
 */
var Record = function(value, expirationSeconds, flags)
{
	// self-reference
	var self = this;

	// attributes
	self.flags = flags;
	self.expiration = 0;
	self.value = value;

	// init
	if (expirationSeconds)
	{
		if (expirationSeconds < MAX_RELATIVE_SECONDS)
		{
			self.expiration = Date.now() + 1000 * expirationSeconds;
		}
		else
		{
			self.expiration = 1000 * expirationSeconds;
		}
	}

	/**
	 * Find out if the record is still valid.
	 */
	self.isValid = function()
	{
		if (!self.expiration)
		{
			return true;
		}
		log.debug('Now: %s, expiration: %s', Date.now(), self.expiration);
		return (Date.now() < self.expiration);
	};
};

/**
 * Test record creation.
 */
function testRecord(callback)
{
	var record = new Record('b', 1, null);
	testing.assert(record.isValid(), 'Should be valid', callback);
	record = new Record('b', -1, null);
	testing.assert(!record.isValid(), 'Should not be valid', callback);
	testing.success(callback);
}

/**
 * An in-memory cache object.
 * Options can be an integer defining the size of the cache in items.
 * Otherwise it is an object that may hold:
 *	- maxSizeMb: max size of the cache in MB.
 * Compatible with jscache API: https://github.com/monsur/jscache.
 */
exports.Cache = function(options)
{
	// self-reference
	var self = this;

	// attributes
	var records = {};
	var maxRecords;
	var maxSizeMb;
	var totalItems = 0;

	// init
	init();

	/**
	 * Init memory structures.
	 */
	function init()
	{
		if (!options)
		{
			return;
		}
		var size = parseInt(options, 10);
		if (size)
		{
			maxRecords = size;
			return;
		}
		maxSizeMb = options.maxSizeMb;
	}

	/**
	 * Get a record from the cache. Params:
	 *	- key: the key to look up.
	 * Returns the current record (with flags, expiration and value), or null if not in the cache.
	 */
	self.getRecord = function(key)
	{
		if (!(key in records))
		{
			return null;
		}
		var record = records[key];
		if (!record.isValid())
		{
			return null;
		}
		return record;
	};

	/**
	 * Set a record in the cache. Params:
	 *	- key: the key to look up.
	 *	- value: to store.
	 *	- expirationSeconds: time to live, or 0 for unlimited.
	 *	- flags: for the value.
	 * Returns true if the item was stored.
	 */
	self.setRecord = function(key, value, expirationSeconds, flags)
	{
		var record = new Record(value, expirationSeconds, flags);
		log.debug('storing: %j', record);
		records[key] = record;
		totalItems++;
		process.nextTick(purge);
		return true;
	};

	/**
	 * Delete a record from the cache. Params:
	 *	- key: the key to look up.
	 * Returns true if the item was deleted, false if not in the cache.
	 */
	self.deleteRecord = function(key)
	{
		if (!(key in records))
		{
			return false;
		}
		delete records[key];
		process.nextTick(purge);
		return true;
	};

	/**
	 * Return an object with stats. Params:
	 *	- type: of stats wanted, ignored by now.
	 */
	self.stats = function(type)
	{
		if (type)
		{
			log.error('Unsupported stats type %s', type);
		}
		return {
			pid: process.pid,
			uptime: process.uptime(),
			time: Math.floor(Date.now() / 1000),
			version: self.version(),
			curr_items: records.countProperties(),
			total_items: totalItems,
			bytes: process.memoryUsage().rss,
			max_bytes: maxSizeMb * 1024 * 1024,
			tcpport: options.port,
			num_threads: 1,
			cas_enabled: 'no',
			evictions: 'on',
		};
	};

	/**
	 * Return the current version string.
	 */
	self.version = function()
	{
		return packageJson.name + '-' + packageJson.version;
	};

	/**
	 * Get item directly. Params:
	 *	- key: the key to look up.
	 * Part of jscache API.
	 */
	self.getItem = function(key)
	{
		var record = records[key];
		log.debug('Got record: %j', record);
		if (!record)
		{
			return null;
		}
		return record.value;
	};

	/**
	 * Set an item directly. Params:
	 *	- key: the key to look up.
	 *	- value: to set.
	 *	- itemOptions: may contain:
	 *		- expirationAbsolute: the time to expire.
	 *		- expirationSeconds: seconds to expire.
	 * Part of jscache API.
	 */
	self.setItem = function(key, value, itemOptions)
	{
		var lifetime = 0;
		if (itemOptions.expirationSeconds)
		{
			lifetime = itemOptions.expirationSeconds;
		}
		if (itemOptions.expirationAbsolute)
		{
			lifetime = (Date.now() - itemOptions.expirationAbsolute.getTime()) / 1000;
		}
		log.debug('storing: %j', value);
		self.setRecord(key, value, lifetime, 0);
	};

	/**
	 * Remove an item. Params:
	 *	- key: the key to look up.
	 * Part of jscache API.
	 */
	self.removeItem = function(key)
	{
		return self.deleteRecord(key);
	};

	/**
	 * Purge memory, if needed.
	 */
	function purge()
	{
		if (maxRecords)
		{
			purgeRecords();
		}
		if (maxSizeMb)
		{
			purgeMemory();
		}
	}

	function purgeRecords()
	{
		if (records.countProperties() < maxRecords)
		{
			return;
		}
		for (var key in records)
		{
			delete records[key];
			if (records.countProperties() < maxRecords)
			{
				return;
			}
		}
	}

	function purgeMemory()
	{
		var usageMb = process.memoryUsage().rss / 1024 / 1024;
		if (usageMb < maxSizeMb)
		{
			return;
		}
		log.info('Memory used %s, max %s: purging', usageMb, maxSizeMb);
		for (var key in records)
		{
			if (!records[key].isValid())
			{
				delete records[key];
			}
		}
		if (usageMb < maxSizeMb)
		{
			return;
		}
		for (key in records)
		{
			delete records[key];
			if (usageMb < maxSizeMb)
			{
				return;
			}
		}
	}
};

/**
 * Test to get, set and delete a test record.
 */
function testGetRecord(callback)
{
	var key = 'test#' + token.create();
	var value = {a: 'b'};
	var cache = new exports.Cache();
	testing.assertEquals(cache.getRecord(key), null, 'Could get record before setting it', callback);
	testing.assert(cache.setRecord(key, value), 'Could not set record', callback);
	var result = cache.getRecord(key);
	testing.assert(result, 'Could not get record', callback);
	testing.assertEquals(result.value, value, 'Got different record', callback);
	testing.assert(cache.deleteRecord(key), 'Could not delete record', callback);
	testing.assertEquals(cache.getRecord(key), null, 'Could get record after deleting it', callback);
	testing.success(callback);
}

/**
 * Test the stats function.
 */
function testStats(callback)
{
	var key = 'test#' + token.create();
	var value = {a: 'b'};
	var cache = new exports.Cache();
	testing.assert(cache.setRecord(key, value), 'Could not set record 1', callback);
	testing.assert(cache.setRecord(key, value), 'Could not set record 2', callback);
	var stats = cache.stats();
	testing.assert(stats, 'Could not get stats', callback);
	testing.assertEquals(stats.curr_items, 1, 'Invalid curr_items', callback);
	testing.assertEquals(stats.total_items, 2, 'Invalid total_items', callback);
	testing.success(callback);
}

/**
 * Test other functions.
 */
function testOther(callback)
{
	var cache = new exports.Cache();
	testing.assert(cache.version().startsWith('nodecached'), 'Invalid version string', callback);
	testing.success(callback);
}

/**
 * Get the default cache, or create it if necessary.
 */
function getDefaultCache()
{
	if (!getDefaultCache.cache)
	{
		log.debug('Creating default cache');
		getDefaultCache.cache = new exports.Cache(0);
	}
	return getDefaultCache.cache;
}

/**
 * Get a value from the cache. Params:
 *	- key: to look up.
 *	- callback: function(error, result) to call with the value.
 * Part of the node-memcached API.
 */
exports.get = function(key, callback)
{
	if (!callback)
	{
		log.error('Missing callback in get()');
		return;
	}
	callback(null, getDefaultCache().getItem(key));
};

/**
 * Set a value in the cache. Params:
 *	- key: to look up.
 *	- value: to store.
 *	- lifetime: seconds to live, 0 for always.
 *	- callback: function(error, result) called with always null, true.
 * Part of the node-memcached API.
 */
exports.set = function(key, value, lifetime, callback)
{
	var options = {
		expirationSeconds: lifetime,
	};
	getDefaultCache().setItem(key, value, options);
	if (callback)
	{
		callback(null, true);
	}
};

/**
 * Delete a value. Params:
 *	- key: to look up.
 *	- value: to store.
 *	- lifetime: seconds to live, 0 for always.
 *	- callback: function(error, result) where result is true if deleted.
 * Part of the node-memcached API.
 */
exports.delete = function(key, callback)
{
	var deleted = getDefaultCache().removeItem(key);
	if (callback)
	{
		callback(null, deleted);
	}
};

/**
 * Test the node-memcached API.
 */
function testNodeMemcachedAPI(callback)
{
	var key = 'test#' + token.create();
	var value = {a: 'b'};
	exports.get(key, function(error, result)
	{
		testing.check(error, 'Error getting first', callback);
		testing.assert(!result, 'Should not get', callback);
		exports.set(key, value, 10, function(error, result)
		{
			testing.check(error, 'Error setting', callback);
			testing.assert(result, 'Could not set', callback);
			exports.get(key, function(error, result)
			{
				testing.check(error, 'Error getting', callback);
				testing.assert(result, 'Could not get', callback);
				testing.assertEquals(result, value, 'Different get', callback);
				exports.delete(key, function(error, result)
				{
					testing.check(error, 'Error deleting', callback);
					testing.assert(result, 'Could not delete', callback);
					exports.get(key, function(error, result)
					{
						testing.check(error, 'Error getting last', callback);
						testing.assert(!result, 'Should not get last', callback);
						testing.success(callback);
					});
				});
			});
		});
	});
}

/**
 * Run all tests.
 */
exports.test = function(callback)
{
	log.debug('Running tests');
	testing.run({
		testRecord: testRecord,
		testGetRecord: testGetRecord,
		testNodeMemcached: testNodeMemcachedAPI,
		testStats: testStats,
		testOther: testOther,
	}, callback);
};

// run tests if invoked directly
if (__filename == process.argv[1])
{
	exports.test(testing.show);
}

<|endoftext|>


<commit_before>/**
 * 	Deze functie leest een bestand in het geheugen.
 *
 * 	@param {string} [filename]
 * 		De naam van het bestand.
 *
 * 	@returns {object | null}
 * 		Een object met twee properties, 'file' en 'blob'. 'file' is een
 *  	Titanium.Filesystem.File object, 'blob' is de data van het bestand.
 * 		De functie returned null als het mislukt om de file te lezen.
 */
var getFile = function(filename) {
	var file = Titanium.Filesystem.getFile(Titanium.Filesystem.resourcesDirectory, filename)
	  , blob;

	// Leest het bestand in het geheugen
	if (file && file.exists()) {
		blob = file.read();
	} else { // Als het fout gaat
		file = null;
	  	blob = null;
		Titanium.API.warn('Functie Data.getFile: Het bestand \'' + filename +'\' bestaat niet');
		return null;
	}

	// Als blob nu nog null is, dan is er iets mis gegaan.
	if (!blob) {
		file = null;
		Titanium.API.warn('Functie Data.getFile: Het inlezen van het bestand \'' + filename +'\' is mislukt');
		return null
	}

	return {
		file: file
	  , blob: blob
	};
}

/**
 * 	Deze functie leest en parsed een XML bestand uit de Resources/XML folder.
 *
 * 	@param {string} [filename]
 * 		De naam van het XML bestand.
 *
 * 	@returns {Titanium.XML.Document | null}
 * 		Een Titanium.XML.Document object met de data geladen uit het aangegeven
 *      XML bestand.
 *      De functie returned null als het laden en/of parsen van het XML bestand
 *      fout is gegaan.
 *
 * 	@example :: Het bestand 'data.xml' laden en parsen
 * 		var XMLdata = Data.getXML('data.xml');
 */
var getXML = function(filename) {
	var XMLdata
	  , XMLdocument

	// Check of er een geldige filename is opgegeven, anders geef fout.
	if (!filename) {
		Titanium.API.warn('Functie Data.getXML: parameter filename mag niet null/undefined zijn');
		return null;
	}

	// Probeer het bestand in het geheugen te halen.
	XMLdata = getFile(filename);

	// Probeer het XMLbestand te parsen, als het mislukt cleanup doen en null returnen
	if (XMLdata && XMLdata.blob) {
		try {
			XMLdocument = Titanium.XML.parseString(XMLdata.blob.text);
		} catch (e) {
			Titanium.API.warn('Functie Data.getXML: Het parsen van het XML bestand \'' + XMLDIR + filename +'\' is mislukt')
			XMLdata.file = null;
			XMLdata.blob = null;
			return null;
		}
	} else {
		Titanium.API.warn('Functie Data.getXML: Het is mislukt om \'' + XMLDIR + filename + '\' uit te lezen');
		return null;
	}

	// Als alles goed is gegaan, dan hebben we nu een XML.Document object, die returnen we.
	return XMLdocument;
};

/**
 * 	Deze functie leest en parsed een JSON bestand uit de Resources/JSON folder.
 *
 * 	@param {string} [filename]
 * 		De naam van het JSON bestand.
 *
 * 	@returns {object | null}
 * 		Een object met de data geladen uit het aangegeven JSON bestand.
 *      De functie returned null als het laden en/of parsen van het JSON bestand
 *      fout is gegaan.
 *
 * 	@example :: Het bestand 'data.json' laden en parsen
 * 		var JSONdata = Data.getJSON('data.json');
 */
var getJSON = function(filename) {
	var JSONdata
      , JSONobject;

	if (!filename) {
		Titanium.API.warn('Functie Data.getJSON: parameter filename mag niet null/undefined zijn');
		return null;
    }

    // Probeer het bestand in het geheugen te halen.
    JSONdata = getFile(filename)

    // Probeer de JSON te parsen, als het mislukt cleanup doen en null returnen
    if (JSONdata && JSONdata.blob) {
    	try {
    		JSONobject = JSON.parse(JSONdata.blob.text);
    	} catch (e) {
    		Titanium.API.warn('Functie Data.getJSON: Het parsen van het JSON bestand \'' + JSONDIR + filename +'\' is mislukt');
    		JSONdata.file = null;
    		JSONdata.blob = null;
    		return null;
    	}
    } else {
    	Titanium.API.warn('Functie Data.getJSON: Het is mislukt om \'' + JSONDIR + filename +'\' uit te lezen');
    	return null;
    }

    return JSONdata;
};

exports.getFile = getFile;
exports.getXML  = getXML;
exports.getJSON = getJSON;<commit_msg>uh...<commit_after>/**
 * 	Deze functie leest een bestand in het geheugen.
 *
 * 	@param {string} [filename]
 * 		De naam van het bestand.
 *
 * 	@returns {object | null}
 * 		Een object met twee properties, 'file' en 'blob'. 'file' is een
 *  	Titanium.Filesystem.File object, 'blob' is de data van het bestand.
 * 		De functie returned null als het mislukt om de file te lezen.
 */
var getFile = function(filename) {
	var file = Titanium.Filesystem.getFile(Titanium.Filesystem.resourcesDirectory, filename)
	  , blob;

	// Leest het bestand in het geheugen
	if (file && file.exists()) {
		blob = file.read();
	} else { // Als het fout gaat
		file = null;
	  	blob = null;
		Titanium.API.warn('Functie Data.getFile: Het bestand \'' + filename +'\' bestaat niet');
		return null;
	}

	// Als blob nu nog null is, dan is er iets mis gegaan.
	if (!blob) {
		file = null;
		Titanium.API.warn('Functie Data.getFile: Het inlezen van het bestand \'' + filename +'\' is mislukt');
		return null
	}

	return {
		file: file
	  , blob: blob
	};
}

/**
 * 	Deze functie leest en parsed een XML bestand uit de Resources/XML folder.
 *
 * 	@param {string} [filename]
 * 		De naam van het XML bestand.
 *
 * 	@returns {Titanium.XML.Document | null}
 * 		Een Titanium.XML.Document object met de data geladen uit het aangegeven
 *      XML bestand.
 *      De functie returned null als het laden en/of parsen van het XML bestand
 *      fout is gegaan.
 *
 * 	@example :: Het bestand 'data.xml' laden en parsen
 * 		var XMLdata = Data.getXML('data.xml');
 */
var getXML = function(filename) {
	var XMLdata
	  , XMLdocument

	// Check of er een geldige filename is opgegeven, anders geef fout.
	if (!filename) {
		Titanium.API.warn('Functie Data.getXML: parameter filename mag niet null/undefined zijn');
		return null;
	}

	// Probeer het bestand in het geheugen te halen.
	XMLdata = getFile(filename);

	// Probeer het XMLbestand te parsen, als het mislukt cleanup doen en null returnen
	if (XMLdata && XMLdata.blob) {
		try {
			XMLdocument = Titanium.XML.parseString(XMLdata.blob.text);
		} catch (e) {
			Titanium.API.warn('Functie Data.getXML: Het parsen van het XML bestand \'' + XMLDIR + filename +'\' is mislukt')
			XMLdata.file = null;
			XMLdata.blob = null;
			return null;
		}
	} else {
		Titanium.API.warn('Functie Data.getXML: Het is mislukt om \'' + XMLDIR + filename + '\' uit te lezen');
		return null;
	}

	// Als alles goed is gegaan, dan hebben we nu een XML.Document object, die returnen we.
	return XMLdocument;
};

/**
 * 	Deze functie leest en parsed een JSON bestand uit de Resources/JSON folder.
 *
 * 	@param {string} [filename]
 * 		De naam van het JSON bestand.
 *
 * 	@returns {object | null}
 * 		Een object met de data geladen uit het aangegeven JSON bestand.
 *      De functie returned null als het laden en/of parsen van het JSON bestand
 *      fout is gegaan.
 *
 * 	@example :: Het bestand 'data.json' laden en parsen
 * 		var JSONdata = Data.getJSON('data.json');
 */
var getJSON = function(filename) {
	var JSONdata
      , JSONobject;

	if (!filename) {
		Titanium.API.warn('Functie Data.getJSON: parameter filename mag niet null/undefined zijn');
		return null;
    }

    // Probeer het bestand in het geheugen te halen.
    JSONdata = getFile(filename)

    // Probeer de JSON te parsen, als het mislukt cleanup doen en null returnen
    if (JSONdata && JSONdata.blob) {
    	try {
    		JSONobject = JSON.parse(JSONdata.blob.text);
    	} catch (e) {
    		Titanium.API.warn('Functie Data.getJSON: Het parsen van het JSON bestand \'' + JSONDIR + filename +'\' is mislukt');
    		JSONdata.file = null;
    		JSONdata.blob = null;
    		return null;
    	}
    } else {
    	Titanium.API.warn('Functie Data.getJSON: Het is mislukt om \'' + JSONDIR + filename +'\' uit te lezen');
    	return null;
    }

    return JSONobject;
};

exports.getFile = getFile;
exports.getXML  = getXML;
exports.getJSON = getJSON;<|endoftext|>


<commit_before>const symClassType = Symbol('classType');

global.getInterface = function (definition) {
    // todo: use definition to check if all methods are implemented

    let mixin = (superclass) => class extends superclass {};
    Object.setPrototypeOf(mixin, {
        symClassType: 'Interface',
        [Symbol.hasInstance]: (o) => {
            let mixins = o.constructor['_applied_interfaces'];
            if (! mixins) {
                return false;
            }

            return mixins.indexOf(mixin) != -1;
        }
    });

    return mixin;
};

global.getTrait = function (definition) {
    let mixin = (superclass) => {
        let trait = class extends superclass {};
        for (let prop of Object.getOwnPropertyNames(definition.prototype)) {
            if (prop === 'constructor') {
                continue;
            }

            trait.prototype[prop] = definition.prototype[prop];
        }

        return trait;
    };

    Object.setPrototypeOf(mixin, {
        symClassType: 'Trait',
    });

    return mixin;
};

global.mix = function (superclass, ...mixins) {
    superclass = superclass || class {};
    superclass = mixins.reduce((a, b) => b(a), superclass);

    mixins = mixins.filter(i => i[symClassType] !== 'Interface');

    let mixed = (s => class extends s {})(superclass);
    Object.defineProperty(mixed.constructor, '_applied_interfaces', {
        value: [superclass, ...mixins],
        enumerable: false
    });

    return mixed;
};

global.implementationOf = function(...interfaces) {
    return global.mix(undefined, ...interfaces);
};
<commit_msg>fixed mixins<commit_after>const symClassType = Symbol('classType');

global.getInterface = function (definition) {
    // todo: use definition to check if all methods are implemented

    let mixin = (superclass) => class extends superclass {};
    Object.setPrototypeOf(mixin, {
        symClassType: 'Interface',
        [Symbol.hasInstance]: (o) => {
            let mixins = o.constructor['_applied_interfaces'];
            if (! mixins) {
                return false;
            }

            return mixins.indexOf(mixin) != -1;
        }
    });

    return mixin;
};

global.getTrait = function (definition) {
    let mixin = (superclass) => {
        let trait = class extends superclass {};
        for (let prop of Object.getOwnPropertyNames(definition.prototype)) {
            if (prop === 'constructor') {
                continue;
            }

            trait.prototype[prop] = definition.prototype[prop];
        }

        return trait;
    };

    Object.setPrototypeOf(mixin, {
        symClassType: 'Trait',
    });

    return mixin;
};

global.mix = function (superclass, ...mixins) {
    superclass = superclass || class {};
    superclass = mixins.reduce((a, b) => b(a), superclass);

    mixins = mixins.filter(i => i[symClassType] !== 'Interface');

    let mixed = (s => class extends s {})(superclass);
    Object.defineProperty(mixed, '_applied_interfaces', {
        value: [superclass, ...mixins],
        enumerable: false
    });

    return mixed;
};

global.implementationOf = function(...interfaces) {
    return global.mix(undefined, ...interfaces);
};
<|endoftext|>


<commit_before>import React, { Component, PureComponent } from 'react';
import PropTypes from 'prop-types';
import Select from 'select';
import Checkbox from 'checkbox';
import SKUContainer from './SKUContainer';

const noop = res => res;

class SKUGroup extends (PureComponent || Component) {
  constructor(props) {
    super(props);
    this.state = {
      newSKUText: '',
      hasSKUImage: props.sku.leaf
        ? props.sku.leaf.some(item => item.img_url)
        : false
    };
  }

  filterHandler = (item, keyword) => {
    let { maxSKUTextLength } = this.context;
    if (maxSKUTextLength && maxSKUTextLength > 0) {
      keyword = keyword.substring(0, maxSKUTextLength);
    }
    return item.text.indexOf(keyword) > -1;
  };

  // 选择sku
  selectSKUHandler = (evt, sku) => {
    let { index, onSKUChange } = this.props;
    let { optionValue, optionText } = this.context;
    sku.leaf = [];
    if (sku[optionValue] === this.props.sku[optionValue]) return;
    if (sku[optionValue]) {
      onSKUChange(sku, index);
      return;
    }
    this.createSKU(sku[optionText], index);
  };

  // 创建sku
  createSKU(text) {
    let { sku, index, onSKUChange } = this.props;
    let { onCreateGroup, optionValue, optionText } = this.context;
    onCreateGroup(text).then(data => {
      if (data > 0) {
        sku[optionValue] = data;
        sku[optionText] = text;
        this.setState({
          newSKUText: ''
        });
        onSKUChange(sku, index);
      }
    });
  }

  asyncFilterSKU = keyword => {
    let { skuTree } = this.props;
    let { optionText, maxSKUTextLength } = this.context;
    if (maxSKUTextLength && maxSKUTextLength > 0) {
      keyword = keyword.substring(0, maxSKUTextLength);
    }
    if (skuTree.some(item => item[optionText] === keyword)) return;
    this.setState({
      newSKUText: keyword
    });
  };

  onSKULeafChange = leaf => {
    let { sku, index, onSKUChange } = this.props;
    sku.leaf = leaf;
    onSKUChange(sku, index);
  };

  rebuildSKULeaf(sku, index) {
    let { subGroup } = this.state;
    if (subGroup[index]) {
      subGroup[index].leaf = [].concat(sku);
    }
    this.setState({ subGroup });
    this.props.onChange(subGroup);
  }

  checkSKUImage = evt => {
    let { sku, index, onSKUChange } = this.props;
    let isShow = evt.target.checked;
    sku.is_show = isShow;
    sku.leaf = sku.leaf.map(item => {
      item.is_show = isShow;
      return item;
    });
    this.setState({
      hasSKUImage: isShow
    });
    onSKUChange(sku, index);
  };

  handleReset = () => {
    this.setState({
      newSKUText: ''
    });
  };

  render() {
    let { sku, index, skuTree } = this.props;
    let { optionValue, optionText } = this.context;
    let { newSKUText, hasSKUImage } = this.state;

    const prefix = `${this.context.prefix}-group`;

    if (newSKUText) {
      skuTree = [].concat(skuTree);
      if (skuTree[0][optionValue] === 0) {
        skuTree[0][optionText] = newSKUText;
      } else {
        let newSKU = {};
        newSKU[optionValue] = 0;
        newSKU[optionText] = newSKUText;
        skuTree.unshift(newSKU);
      }
    }

    return (

          <Select
            open={typeof sku[optionValue] === 'undefined'}
            optionValue={optionValue}
            data={skuTree}
            onChange={this.selectSKUHandler}
            filter={this.filterHandler}
            onAsyncFilter={this.asyncFilterSKU}
            onOpen={this.handleReset}
            value={sku[optionValue]}
          />
          {index === 0
            ? <Checkbox checked={hasSKUImage} onChange={this.checkSKUImage}>
                添加规格图片
              </Checkbox>
            : ''}

            ×

        <SKUContainer
          sku={{ ...sku }}
          hasSKUImage={hasSKUImage}
          onSKULeafChange={this.onSKULeafChange}
        />
        {hasSKUImage
          ?

目前只支持为第一个规格设置不同的规格图片

设置后，用户选择不同规格会显示不同图片

建议尺寸：640 x 640像素

          :
}

    );
  }
}

SKUGroup.contextTypes = {
  prefix: PropTypes.string,
  maxSKUTextLength: PropTypes.number,
  optionValue: PropTypes.string,
  optionText: PropTypes.string,
  onCreateGroup: PropTypes.func
};

SKUGroup.propTypes = {
  index: PropTypes.number,
  sku: PropTypes.object.isRequired,
  onSKUDelete: PropTypes.func,
  onSKUChange: PropTypes.func
};

SKUGroup.defaultProps = {
  index: 0,
  data: {},
  onSKUDelete: noop,
  onSKUChange: noop
};

export default SKUGroup;
<commit_msg>fix: 同名sku bug<commit_after>import React, { Component, PureComponent } from 'react';
import PropTypes from 'prop-types';
import Select from 'select';
import Checkbox from 'checkbox';
import SKUContainer from './SKUContainer';

const noop = res => res;

class SKUGroup extends (PureComponent || Component) {
  constructor(props) {
    super(props);
    this.state = {
      newSKUText: '',
      hasSKUImage: props.sku.leaf
        ? props.sku.leaf.some(item => item.img_url)
        : false
    };
  }

  filterHandler = (item, keyword) => {
    let { maxSKUTextLength } = this.context;
    if (maxSKUTextLength && maxSKUTextLength > 0) {
      keyword = keyword.substring(0, maxSKUTextLength);
    }
    return item.text.indexOf(keyword) > -1;
  };

  // 选择sku
  selectSKUHandler = (evt, sku) => {
    let { index, onSKUChange } = this.props;
    let { optionValue, optionText } = this.context;
    sku.leaf = [];
    if (sku[optionValue] === this.props.sku[optionValue]) return;
    if (sku[optionValue]) {
      onSKUChange(sku, index);
      return;
    }
    this.createSKU(sku[optionText], index);
  };

  // 创建sku
  createSKU(text) {
    let { sku, index, onSKUChange } = this.props;
    let { onCreateGroup, optionValue, optionText } = this.context;
    onCreateGroup(text).then(data => {
      if (data > 0) {
        sku[optionValue] = data;
        sku[optionText] = text;
        this.setState({
          newSKUText: ''
        });
        onSKUChange(sku, index);
      }
    });
  }

  asyncFilterSKU = keyword => {
    let { skuTree } = this.props;
    let { optionText, maxSKUTextLength } = this.context;
    if (maxSKUTextLength && maxSKUTextLength > 0) {
      keyword = keyword.substring(0, maxSKUTextLength);
    }
    if (skuTree.some(item => item[optionText] === keyword)) return;
    this.setState({
      newSKUText: keyword
    });
  };

  onSKULeafChange = leaf => {
    let { sku, index, onSKUChange } = this.props;
    sku.leaf = leaf;
    onSKUChange(sku, index);
  };

  rebuildSKULeaf(sku, index) {
    let { subGroup } = this.state;
    if (subGroup[index]) {
      subGroup[index].leaf = [].concat(sku);
    }
    this.setState({ subGroup });
    this.props.onChange(subGroup);
  }

  checkSKUImage = evt => {
    let { sku, index, onSKUChange } = this.props;
    let isShow = evt.target.checked;
    sku.is_show = isShow;
    sku.leaf = sku.leaf.map(item => {
      item.is_show = isShow;
      return item;
    });
    this.setState({
      hasSKUImage: isShow
    });
    onSKUChange(sku, index);
  };

  handleReset = () => {
    this.setState({
      newSKUText: ''
    });
  };

  render() {
    let { sku, index, skuTree } = this.props;
    let { optionValue, optionText } = this.context;
    let { newSKUText, hasSKUImage } = this.state;

    const prefix = `${this.context.prefix}-group`;

    if (newSKUText) {
      skuTree = [].concat(skuTree);
      if (skuTree[0][optionValue] === 0) {
        skuTree[0][optionText] = newSKUText;
      } else {
        let newSKU = {};
        newSKU[optionValue] = 0;
        newSKU[optionText] = newSKUText;
        skuTree.unshift(newSKU);
      }
    }

    return (

          <Select
            open={typeof sku[optionValue] === 'undefined'}
            optionValue={optionValue}
            data={skuTree}
            onChange={this.selectSKUHandler}
            filter={this.filterHandler}
            onAsyncFilter={this.asyncFilterSKU}
            onOpen={this.handleReset}
            value={sku[optionValue] || ''}
          />
          {index === 0
            ? <Checkbox checked={hasSKUImage} onChange={this.checkSKUImage}>
                添加规格图片
              </Checkbox>
            : ''}

            ×

        <SKUContainer
          sku={{ ...sku }}
          hasSKUImage={hasSKUImage}
          onSKULeafChange={this.onSKULeafChange}
        />
        {hasSKUImage
          ?

目前只支持为第一个规格设置不同的规格图片

设置后，用户选择不同规格会显示不同图片

建议尺寸：640 x 640像素

          :
}

    );
  }
}

SKUGroup.contextTypes = {
  prefix: PropTypes.string,
  maxSKUTextLength: PropTypes.number,
  optionValue: PropTypes.string,
  optionText: PropTypes.string,
  onCreateGroup: PropTypes.func
};

SKUGroup.propTypes = {
  index: PropTypes.number,
  sku: PropTypes.object.isRequired,
  onSKUDelete: PropTypes.func,
  onSKUChange: PropTypes.func
};

SKUGroup.defaultProps = {
  index: 0,
  data: {},
  onSKUDelete: noop,
  onSKUChange: noop
};

export default SKUGroup;
<|endoftext|>


<commit_before>import _ from 'highland'
import constructor from './constructor'
import sinon from 'sinon'
import assert from 'assert'

import logger from '../stream-utils/logger'
import duplexStub from '../stream-utils/duplex-stub'
import asLine from '../stream-utils/as-line'

export default (tape) => {

  tape('connects to bb', { timeout: 1000}, (t) => {
    t.plan(3)

    let world = makeWorld()

    constructor(
      world.stubs.net, world.stubs.guid, '192.168.0.1:1234', 4567)
    t.ok(world.stubs.server.listen.calledWith(4567))
    t.ok(world.stubs.net.connect.calledWith(1234, '192.168.0.1'))
    world.stubs.clientSocket.push(asLine({
      event: {
        topic: "myTopic",
        body: {
          hello: 123
        }
      }
    }))
    world.stubs
      .boilerBaySocket
      .await(1,
        'send myTopic ' + world.state.guidToGenerate + ' {"hello":123}\n',
        t.pass)
  })

  tape('converts bb errors to cv errors', { timeout: 1000 }, (t) => {
    t.plan(1)
    let world = makeWorld()
    constructor(world.stubs.net, world.stubs.guid, '192.168.0.1:1234', 4567)
    world.stubs.boilerBaySocket.push(asLine('error some-code oh my god'))
    world.stubs.clientSocket
      .await(
        "{\"error\":{\"code\":\"some-code\",\"message\":\"oh my god\"}}\n",
        t.pass)
  })

  tape('consume', { timeout:1000 }, (t) => {
    t.plan(1)
    let world = makeWorld()
    constructor(world.stubs.net, world.stubs.guid, '192.168.0.1:1234', 4567)
    world.stubs.clientSocket.push(asLine({
      consume: {
        offsetReset: 'smallest',
        topic: 'my_fine_topic'
        // leaving group undefined
      },
    }))
    world.stubs.boilerBaySocket
      .await("consume my_fine_topic " +
        world.state.guidToGenerate + " smallest\n", t.pass)

  })

  tape('next', {timeout:1000}, (t) => {
    t.plan(1)
    let world = makeWorld()
    constructor(world.stubs.net, world.stubs.guid, '192.168.0.1:1234', 4567)
    world.stubs.clientSocket.push(asLine({
      next: true
    }))
    world.stubs.boilerBaySocket.await("next\n", t.pass)
  })

  tape('handles two messages in one push', {timeout: 1000}, (t) => {
    t.plan(1)
    let world = makeWorld()
    constructor(world.stubs.net, world.stubs.guid, '192.168.0.1:1234', 4567)
    world.stubs.clientSocket.push(
      asLine({ next: true }) + asLine({ next: true }))
    world.stubs.boilerBaySocket.await(2, "next\n", t.pass)
  })

  tape('commit', {timeout:1000}, (t) => {
    t.plan(1)
    let world = makeWorld()
    constructor(world.stubs.net, world.stubs.guid, '192.168.0.1:1234', 4567)
    world.stubs.clientSocket.push(asLine({
      commit: true
    }))
    world.stubs.boilerBaySocket.await("commit\n", t.pass)

  })

  tape('msg', {timeout: 1000}, (t) => {
    t.plan(2)
    let world = makeWorld()
    constructor(world.stubs.net, world.stubs.guid, '192.168.0.1:1234', 4567)
    world.stubs.boilerBaySocket.push(asLine('ready'))
    world.stubs.boilerBaySocket.push('msg ' + asLine({ hello: 123 }))
    world.stubs.clientSocket.await(
      "{\"message\":{\"hello\":123}}\n", t.pass)
    world.stubs.clientSocket.awaitNot("undefined\n", t.pass)
  })

  tape('consume-started', (t) => {
    t.plan(1)
    let world = makeWorld()
    constructor(world.stubs.net, world.stubs.guid, '192.168.0.1:1234', 4567)
    world.stubs.boilerBaySocket.push('consume-started\n')
    world.stubs.clientSocket.await(
      "{\"consumeStarted\":true}\n",
      t.pass
    )
  })

  tape('commit-ok', {timeout:1000}, (t) => {
    t.plan(1)
    let world = makeWorld()
    constructor(world.stubs.net, world.stubs.guid, '192.168.0.1:1234', 4567)
    world.stubs.boilerBaySocket.push('commit-ok\n')
    world.stubs.clientSocket.await("{\"commitOK\":true}\n", t.pass)
  })

  let makeWorld = () => {

    let state = {
      guidToGenerate: Math.floor(Math.random()*10000000).toString()
    }

    let boilerBaySocketStub = duplexStub()
    let clientSocketStub    = duplexStub()
    let serverStub          = {
      listen: sinon.stub()
    }

    let netStub = {
      connect: sinon.spy(() => boilerBaySocketStub),
      createServer: (callback) => {
        callback(clientSocketStub)
        return serverStub
      },
    }
    let guidStub = () => state.guidToGenerate

    let api = {
      state,
      stubs: {
        net: netStub,
        guid: guidStub,
        server: serverStub,
        clientSocket: clientSocketStub,
        boilerBaySocket: boilerBaySocketStub
      }
    }
    return api

  }

}
<commit_msg>world -> simulation<commit_after>import _ from 'highland'
import constructor from './constructor'
import sinon from 'sinon'
import assert from 'assert'

import logger from '../stream-utils/logger'
import duplexStub from '../stream-utils/duplex-stub'
import asLine from '../stream-utils/as-line'

export default (tape) => {

  tape('connects to bb', { timeout: 1000}, (t) => {
    t.plan(3)

    let sim = makeSimulation()

    constructor(
      sim.net, sim.guid, '192.168.0.1:1234', 4567)
    t.ok(sim.server.listen.calledWith(4567))
    t.ok(sim.net.connect.calledWith(1234, '192.168.0.1'))
    sim.clientSocket.push(asLine({
      event: {
        topic: "myTopic",
        body: {
          hello: 123
        }
      }
    }))
    sim
      .boilerBaySocket
      .await(1,
        'send myTopic ' + sim.guidToGenerate + ' {"hello":123}\n',
        t.pass)
  })

  tape('converts bb errors to cv errors', { timeout: 1000 }, (t) => {
    t.plan(1)
    let sim = makeSimulation()
    constructor(sim.net, sim.guid, '192.168.0.1:1234', 4567)
    sim.boilerBaySocket.push(asLine('error some-code oh my god'))
    sim.clientSocket
      .await(
        "{\"error\":{\"code\":\"some-code\",\"message\":\"oh my god\"}}\n",
        t.pass)
  })

  tape('consume', { timeout:1000 }, (t) => {
    t.plan(1)
    let sim = makeSimulation()
    constructor(sim.net, sim.guid, '192.168.0.1:1234', 4567)
    sim.clientSocket.push(asLine({
      consume: {
        offsetReset: 'smallest',
        topic: 'my_fine_topic'
        // leaving group undefined
      },
    }))
    sim.boilerBaySocket
      .await("consume my_fine_topic " +
        sim.guidToGenerate + " smallest\n", t.pass)

  })

  tape('next', {timeout:1000}, (t) => {
    t.plan(1)
    let sim = makeSimulation()
    constructor(sim.net, sim.guid, '192.168.0.1:1234', 4567)
    sim.clientSocket.push(asLine({
      next: true
    }))
    sim.boilerBaySocket.await("next\n", t.pass)
  })

  tape('handles two messages in one push', {timeout: 1000}, (t) => {
    t.plan(1)
    let sim = makeSimulation()
    constructor(sim.net, sim.guid, '192.168.0.1:1234', 4567)
    sim.clientSocket.push(
      asLine({ next: true }) + asLine({ next: true }))
    sim.boilerBaySocket.await(2, "next\n", t.pass)
  })

  tape('commit', {timeout:1000}, (t) => {
    t.plan(1)
    let sim = makeSimulation()
    constructor(sim.net, sim.guid, '192.168.0.1:1234', 4567)
    sim.clientSocket.push(asLine({
      commit: true
    }))
    sim.boilerBaySocket.await("commit\n", t.pass)

  })

  tape('msg', {timeout: 1000}, (t) => {
    t.plan(2)
    let sim = makeSimulation()
    constructor(sim.net, sim.guid, '192.168.0.1:1234', 4567)
    sim.boilerBaySocket.push(asLine('ready'))
    sim.boilerBaySocket.push('msg ' + asLine({ hello: 123 }))
    sim.clientSocket.await(
      "{\"message\":{\"hello\":123}}\n", t.pass)
    sim.clientSocket.awaitNot("undefined\n", t.pass)
  })

  tape('consume-started', (t) => {
    t.plan(1)
    let sim = makeSimulation()
    constructor(sim.net, sim.guid, '192.168.0.1:1234', 4567)
    sim.boilerBaySocket.push('consume-started\n')
    sim.clientSocket.await(
      "{\"consumeStarted\":true}\n",
      t.pass
    )
  })

  tape('commit-ok', {timeout:1000}, (t) => {
    t.plan(1)
    let sim = makeSimulation()
    constructor(sim.net, sim.guid, '192.168.0.1:1234', 4567)
    sim.boilerBaySocket.push('commit-ok\n')
    sim.clientSocket.await("{\"commitOK\":true}\n", t.pass)
  })

  let makeSimulation = () => {

    let simulation = {
      guidToGenerate: Math.floor(Math.random()*10000000).toString(),
      net: {
        connect: sinon.spy(() => simulation.boilerBaySocket),
        createServer: (callback) => {
          callback(simulation.clientSocket)
          return simulation.server
        },
      },
      guid: () => simulation.guidToGenerate,
      server: {
        listen: sinon.stub()
      },
      clientSocket: duplexStub(),
      boilerBaySocket: duplexStub()
    }

    return simulation
  }

}
<|endoftext|>