<commit_before>var _         = require('underscore'),
    ginga     = require('ginga'),
    params    = ginga.params,
    timestamp = require('monotonic-timestamp'),
    queue     = require('queue-async'),
    util      = require('./util');
    // mapper = require('./mapper');

function Resource(roda, name){
  this.roda = roda;
  this.store = roda.db.sublevel(name);

  this._name = name;
  this._clock = this.store.sublevel('clock');
  this._changes = this.store.sublevel('changes');

  this._queues = {};
}
var R = ginga(Resource.prototype);

R.name = function(){
  return this._name;
};

R.queue = require('./queue');
// R.mapper = mapper;

//Read
R.define('get', params('id:string','tx?'), function(ctx, done){
  if(ctx.params.tx){
    if(ctx.params.tx.db !== this.roda.db)
      return next(new Error('Invalid transaction object.'));
    ctx.params.tx.get(ctx.params.id, {
      prefix: this.store
    }, done);
  }else{
    this.store.get(ctx.params.id, done);
  }
});

R.define('read', params(
  'limit:number?','options:object?'
), function(ctx, next){
  var opts = ctx.options = _.extend(
    {}, ctx.params, ctx.params.options
  );
  if('prefix' in opts){
    if('gte' in opts)
      opts.gte = opts.prefix + opts.gte;
    else if('gt' in opts)
      opts.gt = opts.prefix + opts.gt;
    else
      opts.gte = opts.prefix;

    if('lte' in opts)
      opts.lte = opts.prefix + opts.lte;
    else if('lt' in opts)
      opts.lt = opts.prefix + opts.lt;
    else
      opts.lte = opts.prefix + '\xff';
  }
  ctx.stream = this.store.createValueStream(opts);

  next();
}, function(ctx, done){
  var result = [];
  ctx.stream
    .on('data', function(data){
      result.push(data);
    })
    .on('close', function(){
      done(null, result);
    })
    .on('error', done);
});

//Hooks
function extend(ctx, next){
  _.extend(ctx, ctx.params);
  next();
}

R.define('validate', params(
  'result', 'transaction'
), extend, function(ctx, done){
  done(null, ctx.result);
});

R.define('diff', params(
  'current', 'result', 'transaction'
), extend, function(ctx, done){
  done(null, ctx.transaction);
});

//Write

function prepare(ctx, next, end){
  var mid = this.roda.id();

  //prepare result
  ctx.result = _.clone(ctx.params.result || {});

  if('id' in ctx.params)
    ctx.result._id = ctx.params.id;
  else if(!ctx.result._id){
    //generate _id = monotonic timestamp encode + mid
    ctx.result._id = util.encode(timestamp()) + mid;
  }
  var deleted = (!ctx.params.result) && ('id' in ctx.params);

  if(deleted)
    ctx.result._deleted = true;
  else
    delete ctx.result._deleted;

  //init transaction
  if(ctx.params.tx){
    if(ctx.params.tx.db !== this.roda.db)
      return next(new Error('Invalid transaction object.'));
    ctx.transaction = ctx.params.tx;
    //defer if within another transaction
    ctx.transaction.defer(function(cb){
      end(cb);
      next();
    });
  }else{
    ctx.transaction = this.roda.transaction();
    next();
  }
}

function validate(ctx, next){
  this.validate(
    ctx.result,
    ctx.transaction,
    function(err, result){
      if(err) return next(err);

      ctx.result = result;
      next();
    }
  );
}

function current(ctx, next){
  var self = this;

  ctx.transaction.get(ctx.result._id, {
    prefix: this.store
  }, function(err, val){
    //return IO/other errors
    if(err && !err.notFound)
      return next(err);
    //dont proceed if delete non existing item
    if(!val && ctx.result._deleted)
      return next(null);
    if(val){
      ctx.current = val;
      //remove previous change
      ctx.transaction.del(ctx.current._rev, {
        prefix: self._changes
      });
    }
    next();
  });
}

function rev(ctx, next){
  var mid = this.roda.id();
  var self = this;

  ctx.transaction.get(mid, {
    prefix: this._clock
  }, function(err, val){
    //lamport timestamp
    var time = util.trim(util.encode(
      Math.max(
        val ? util.decode(
          util.pad(val, 12)
        ) : 0,
        ctx.current ? util.decode(
          util.pad(ctx.current._rev.slice(8), 12)
        ) : 0
      ) + 1
    ));

    //generate rev id
    ctx.result._rev = mid + time;

    //lamport clock update
    ctx.transaction.put(mid, time, {
      prefix: self._clock
    });

    next();
  });
}

function diff(ctx, next){
  this.diff(
    ctx.current || null,
    ctx.result._deleted ? null: ctx.result,
    ctx.transaction,
    function(err, res){
      if(err) return next(err);
      next();
    }
  );
}

function put(ctx, next){
  ctx.transaction
    .put(ctx.result._id, ctx.result, {
      prefix: this.store
    })
    .put(ctx.result._rev, ctx.result, {
      prefix: this._changes
    });
  next();
}

function del(ctx, next){
  ctx.transaction
    .del(ctx.result._id, {
      prefix: this.store
    })
    .put(ctx.result._rev, ctx.result, {
      prefix: this._changes
    });
  next();
}

function invoke(ctx, done){
  var result = _.clone(ctx.result);
  if(ctx.params.tx){
    //batched not yet committed
    done(null, result);
  }else{
    ctx.transaction.commit(function(err){
      if(err) done(err, null);
      else done(null, result);
    });
  }
}

R.define('put', params('id:string?','result:object','tx?'),
  prepare, validate, current, rev, diff, put, invoke);

R.define('del', params('id:string','tx?'),
  prepare, current, rev, diff, del, invoke);

//Changes
R.define('clock', function(ctx, done){
  var arr = [];
  this._clock.createReadStream()
    .on('data', function(data){
      arr.push(data.key + data.value);
    })
    .on('close', function(){
      done(null, arr.join(','));
    })
    .on('error', done);
});

R.define('changes', params(
  'since:string?','limit:number?'
), function(ctx, done, end){
  var limit = ctx.params.limit;
  var count = 0;
  var result = [];
  var self = this;

  function receive(mid, start, cb){
    if(limit && count >= limit)
      return cb(null, result);
    var opts = {};
    if(limit)
      opts.limit = limit - count;
    if(mid){
      opts.gt = mid + (start || '');
      opts.lt = mid + '~';
    }
    self._changes.createValueStream(opts)
      .on('data', function(data){
        result.push(data);
        count++;
      })
      .on('close', function(){
        cb(null, result);
      })
      .on('error', cb);
  }

  if(!ctx.params.since){
    receive(null, null, done);
  }else{
    this.clock(function(err, data){
      var clock = util.clockObject(data);
      var since = util.clockObject(ctx.params.since);
      var q = queue(1);
      for(var mid in clock){
        if(!since[mid] || since[mid] < clock[mid])
          q.defer(receive, mid, since[mid]);
      }
      q.awaitAll(function(err){
        if(err) return done(err);
        done(null, result);
      });
    });
  }
});

module.exports = Resource;
<commit_msg>safeguard ctx.result in hook diff<commit_after>var _         = require('underscore'),
    ginga     = require('ginga'),
    params    = ginga.params,
    timestamp = require('monotonic-timestamp'),
    queue     = require('queue-async'),
    util      = require('./util');
    // mapper = require('./mapper');

function Resource(roda, name){
  this.roda = roda;
  this.store = roda.db.sublevel(name);

  this._name = name;
  this._clock = this.store.sublevel('clock');
  this._changes = this.store.sublevel('changes');

  this._queues = {};
}
var R = ginga(Resource.prototype);

R.name = function(){
  return this._name;
};

R.queue = require('./queue');
// R.mapper = mapper;

//Read
R.define('get', params('id:string','tx?'), function(ctx, done){
  if(ctx.params.tx){
    if(ctx.params.tx.db !== this.roda.db)
      return next(new Error('Invalid transaction object.'));
    ctx.params.tx.get(ctx.params.id, {
      prefix: this.store
    }, done);
  }else{
    this.store.get(ctx.params.id, done);
  }
});

R.define('read', params(
  'limit:number?','options:object?'
), function(ctx, next){
  var opts = ctx.options = _.extend(
    {}, ctx.params, ctx.params.options
  );
  if('prefix' in opts){
    if('gte' in opts)
      opts.gte = opts.prefix + opts.gte;
    else if('gt' in opts)
      opts.gt = opts.prefix + opts.gt;
    else
      opts.gte = opts.prefix;

    if('lte' in opts)
      opts.lte = opts.prefix + opts.lte;
    else if('lt' in opts)
      opts.lt = opts.prefix + opts.lt;
    else
      opts.lte = opts.prefix + '\xff';
  }
  ctx.stream = this.store.createValueStream(opts);

  next();
}, function(ctx, done){
  var result = [];
  ctx.stream
    .on('data', function(data){
      result.push(data);
    })
    .on('close', function(){
      done(null, result);
    })
    .on('error', done);
});

//Hooks
function extend(ctx, next){
  _.extend(ctx, ctx.params);
  next();
}

R.define('validate', params(
  'result', 'transaction'
), extend, function(ctx, done){
  done(null, ctx.result);
});

R.define('diff', params(
  'current', 'result', 'transaction'
), extend, function(ctx, done){
  done(null, ctx.transaction);
});

//Write

function prepare(ctx, next, end){
  var mid = this.roda.id();

  //prepare result
  ctx.result = _.clone(ctx.params.result || {});

  if('id' in ctx.params)
    ctx.result._id = ctx.params.id;
  else if(!ctx.result._id){
    //generate _id = monotonic timestamp encode + mid
    ctx.result._id = util.encode(timestamp()) + mid;
  }
  var deleted = (!ctx.params.result) && ('id' in ctx.params);

  if(deleted)
    ctx.result._deleted = true;
  else
    delete ctx.result._deleted;

  //init transaction
  if(ctx.params.tx){
    if(ctx.params.tx.db !== this.roda.db)
      return next(new Error('Invalid transaction object.'));
    ctx.transaction = ctx.params.tx;
    //defer if within another transaction
    ctx.transaction.defer(function(cb){
      end(cb);
      next();
    });
  }else{
    ctx.transaction = this.roda.transaction();
    next();
  }
}

function validate(ctx, next){
  this.validate(
    ctx.result,
    ctx.transaction,
    function(err, result){
      if(err) return next(err);

      ctx.result = result;
      next();
    }
  );
}

function current(ctx, next){
  var self = this;

  ctx.transaction.get(ctx.result._id, {
    prefix: this.store
  }, function(err, val){
    //return IO/other errors
    if(err && !err.notFound)
      return next(err);
    //dont proceed if delete non existing item
    if(!val && ctx.result._deleted)
      return next(null);
    if(val){
      ctx.current = val;
      //remove previous change
      ctx.transaction.del(ctx.current._rev, {
        prefix: self._changes
      });
    }
    next();
  });
}

function rev(ctx, next){
  var mid = this.roda.id();
  var self = this;

  ctx.transaction.get(mid, {
    prefix: this._clock
  }, function(err, val){
    //lamport timestamp
    var time = util.trim(util.encode(
      Math.max(
        val ? util.decode(
          util.pad(val, 12)
        ) : 0,
        ctx.current ? util.decode(
          util.pad(ctx.current._rev.slice(8), 12)
        ) : 0
      ) + 1
    ));

    //generate rev id
    ctx.result._rev = mid + time;

    //lamport clock update
    ctx.transaction.put(mid, time, {
      prefix: self._clock
    });

    next();
  });
}

function diff(ctx, next){
  this.diff(
    ctx.current || null,
    ctx.result._deleted ? null: _.clone(ctx.result),
    ctx.transaction,
    function(err, res){
      if(err) return next(err);
      next();
    }
  );
}

function put(ctx, next){
  ctx.transaction
    .put(ctx.result._id, ctx.result, {
      prefix: this.store
    })
    .put(ctx.result._rev, ctx.result, {
      prefix: this._changes
    });
  next();
}

function del(ctx, next){
  ctx.transaction
    .del(ctx.result._id, {
      prefix: this.store
    })
    .put(ctx.result._rev, ctx.result, {
      prefix: this._changes
    });
  next();
}

function invoke(ctx, done){
  var result = _.clone(ctx.result);
  if(ctx.params.tx){
    //batched not yet committed
    done(null, result);
  }else{
    ctx.transaction.commit(function(err){
      if(err) done(err, null);
      else done(null, result);
    });
  }
}

R.define('put', params('id:string?','result:object','tx?'),
  prepare, validate, current, rev, diff, put, invoke);

R.define('del', params('id:string','tx?'),
  prepare, current, rev, diff, del, invoke);

//Changes
R.define('clock', function(ctx, done){
  var arr = [];
  this._clock.createReadStream()
    .on('data', function(data){
      arr.push(data.key + data.value);
    })
    .on('close', function(){
      done(null, arr.join(','));
    })
    .on('error', done);
});

R.define('changes', params(
  'since:string?','limit:number?'
), function(ctx, done, end){
  var limit = ctx.params.limit;
  var count = 0;
  var result = [];
  var self = this;

  function receive(mid, start, cb){
    if(limit && count >= limit)
      return cb(null, result);
    var opts = {};
    if(limit)
      opts.limit = limit - count;
    if(mid){
      opts.gt = mid + (start || '');
      opts.lt = mid + '~';
    }
    self._changes.createValueStream(opts)
      .on('data', function(data){
        result.push(data);
        count++;
      })
      .on('close', function(){
        cb(null, result);
      })
      .on('error', cb);
  }

  if(!ctx.params.since){
    receive(null, null, done);
  }else{
    this.clock(function(err, data){
      var clock = util.clockObject(data);
      var since = util.clockObject(ctx.params.since);
      var q = queue(1);
      for(var mid in clock){
        if(!since[mid] || since[mid] < clock[mid])
          q.defer(receive, mid, since[mid]);
      }
      q.awaitAll(function(err){
        if(err) return done(err);
        done(null, result);
      });
    });
  }
});

module.exports = Resource;
<|endoftext|>


<commit_before>'use strict';

// Load modules

const Stream = require('stream');
const Events = require('events');
const Boom = require('boom');
const Hoek = require('hoek');
const Peekaboo = require('peekaboo');

// Declare internals

const internals = {};

exports = module.exports = internals.Response = function (source, request, options) {

    Events.EventEmitter.call(this);

    options = options || {};

    this.request = request;
    this.statusCode = null;
    this.headers = {};                          // Incomplete as some headers are stored in flags
    this.variety = null;
    this.app = {};
    this.plugins = {};
    this.send = null;                           // Set by reply()
    this.hold = null;                           // Set by reply()

    this.settings = {
        encoding: 'utf8',
        charset: 'utf-8',                       // '-' required by IANA
        ttl: null,
        stringify: null,                        // JSON.stringify options
        passThrough: true,
        varyEtag: false
    };

    this._payload = null;                       // Readable stream
    this._takeover = false;
    this._contentEncoding = null;               // Set during transmit
    this._error = null;                         // The boom object when created from an error

    this._processors = {
        marshal: options.marshal,
        prepare: options.prepare,
        close: options.close
    };

    this._setSource(source, options.variety);
};

Hoek.inherits(internals.Response, Events.EventEmitter);

internals.Response.wrap = function (result, request) {

    return (result instanceof Error ? Boom.wrap(result)
                                    : (result instanceof internals.Response ? result
                                                                            : new internals.Response(result, request)));
};

internals.Response.prototype._setSource = function (source, variety) {

    // Method must not set any headers or other properties as source can change later

    this.variety = variety || 'plain';

    if (source === null ||
        source === undefined ||
        source === '') {

        source = null;
    }
    else if (Buffer.isBuffer(source)) {
        this.variety = 'buffer';
    }
    else if (source instanceof Stream) {
        this.variety = 'stream';
    }
    else if (typeof source === 'object' &&
        typeof source.then === 'function') {                // Promise object

        this.variety = 'promise';
    }

    this.source = source;
};

internals.Response.prototype.code = function (statusCode) {

    Hoek.assert(Hoek.isInteger(statusCode), 'Status code must be an integer');

    this.statusCode = statusCode;
    return this;
};

internals.Response.prototype.header = function (key, value, options) {

    key = key.toLowerCase();
    if (key === 'vary') {
        return this.vary(value);
    }

    return this._header(key, value, options);
};

internals.Response.prototype._header = function (key, value, options) {

    options = options || {};
    const append = options.append || false;
    const separator = options.separator || ',';
    const override = options.override !== false;
    const duplicate = options.duplicate !== false;

    // Ensure key and values do not include non-ascii text

    if (value !== undefined &&
        value !== null) {

        const headerValues = [key].concat(value);
        for (let i = 0; i < headerValues.length; ++i) {
            const header = headerValues[i];
            const buffer = Buffer.isBuffer(header) ? header : new Buffer(header.toString());
            for (let j = 0; j < buffer.length; ++j) {
                Hoek.assert((buffer[j] & 0x7f) === buffer[j], 'Header value cannot contain or convert into non-ascii characters:', key);
            }
        }
    }

    if ((!append && override) ||
        !this.headers[key]) {

        this.headers[key] = value;
    }
    else if (override) {
        if (key === 'set-cookie') {
            this.headers[key] = [].concat(this.headers[key], value);
        }
        else {
            const existing = this.headers[key];
            if (!duplicate) {
                const values = existing.split(separator);
                for (let i = 0; i < values.length; ++i) {
                    if (values[i] === value) {
                        return this;
                    }
                }
            }

            this.headers[key] = existing + separator + value;
        }
    }

    return this;
};

internals.Response.prototype.vary = function (value) {

    if (value === '*') {
        this.headers.vary = '*';
    }
    else if (!this.headers.vary) {
        this.headers.vary = value;
    }
    else if (this.headers.vary !== '*') {
        this._header('vary', value, { append: true, duplicate: false });
    }

    return this;
};

internals.Response.prototype.etag = function (tag, options) {

    Hoek.assert(tag !== '*', 'ETag cannot be *');

    options = options || {};
    this._header('etag', (options.weak ? 'W/' : '') + '"' + tag + '"');
    this.settings.varyEtag = options.vary !== false && !options.weak;       // vary defaults to true
    return this;
};

internals.Response.prototype.type = function (type) {

    this._header('content-type', type);
    return this;
};

internals.Response.prototype.bytes = function (bytes) {

    this._header('content-length', bytes);
    return this;
};

internals.Response.prototype.location = function (uri) {

    this._header('location', uri);
    return this;
};

internals.Response.prototype.created = function (location) {

    Hoek.assert(this.request.method === 'post' || this.request.method === 'put', 'Cannot create resource on GET');

    this.statusCode = 201;
    this.location(location);
    return this;
};

internals.Response.prototype.replacer = function (method) {

    this.settings.stringify = this.settings.stringify || {};
    this.settings.stringify.replacer = method;
    return this;
};

internals.Response.prototype.spaces = function (count) {

    this.settings.stringify = this.settings.stringify || {};
    this.settings.stringify.space = count;
    return this;
};

internals.Response.prototype.suffix = function (suffix) {

    this.settings.stringify = this.settings.stringify || {};
    this.settings.stringify.suffix = suffix;
    return this;
};

internals.Response.prototype.passThrough = function (enabled) {

    this.settings.passThrough = (enabled !== false);    // Defaults to true
    return this;
};

internals.Response.prototype.redirect = function (location) {

    this.statusCode = 302;
    this.location(location);
    this.temporary = this._temporary;
    this.permanent = this._permanent;
    this.rewritable = this._rewritable;
    return this;
};

internals.Response.prototype._temporary = function (isTemporary) {

    this._setTemporary(isTemporary !== false);           // Defaults to true
    return this;
};

internals.Response.prototype._permanent = function (isPermanent) {

    this._setTemporary(isPermanent === false);           // Defaults to true
    return this;
};

internals.Response.prototype._rewritable = function (isRewritable) {

    this._setRewritable(isRewritable !== false);         // Defaults to true
    return this;
};

internals.Response.prototype._isTemporary = function () {

    return this.statusCode === 302 || this.statusCode === 307;
};

internals.Response.prototype._isRewritable = function () {

    return this.statusCode === 301 || this.statusCode === 302;
};

internals.Response.prototype._setTemporary = function (isTemporary) {

    if (isTemporary) {
        if (this._isRewritable()) {
            this.statusCode = 302;
        }
        else {
            this.statusCode = 307;
        }
    }
    else {
        if (this._isRewritable()) {
            this.statusCode = 301;
        }
        else {
            this.statusCode = 308;
        }
    }
};

internals.Response.prototype._setRewritable = function (isRewritable) {

    if (isRewritable) {
        if (this._isTemporary()) {
            this.statusCode = 302;
        }
        else {
            this.statusCode = 301;
        }
    }
    else {
        if (this._isTemporary()) {
            this.statusCode = 307;
        }
        else {
            this.statusCode = 308;
        }
    }
};

internals.Response.prototype.encoding = function (encoding) {

    this.settings.encoding = encoding;
    return this;
};

internals.Response.prototype.charset = function (charset) {

    this.settings.charset = charset || null;
    return this;
};

internals.Response.prototype.ttl = function (ttl) {

    this.settings.ttl = ttl;
    return this;
};

internals.Response.prototype.state = function (name, value, options) {          // options: see Defaults.state

    this.request._setState(name, value, options);
    return this;
};

internals.Response.prototype.unstate = function (name, options) {

    this.request._clearState(name, options);
    return this;
};

internals.Response.prototype.takeover = function () {

    this._takeover = true;
    return this;
};

internals.Response.prototype._prepare = function (data, next) {

    this._passThrough();

    if (this.variety !== 'promise') {
        return this._processPrepare(data, next);
    }

    const onDone = (source) => {

        if (source instanceof Error) {
            return next(Boom.wrap(source), data);
        }

        if (source instanceof internals.Response) {
            return source._processPrepare(data, next);
        }

        this._setSource(source);
        this._passThrough();
        this._processPrepare(data, next);
    };

    this.source.then(onDone, onDone);
};

internals.Response.prototype._passThrough = function () {

    if (this.variety === 'stream' &&
        this.settings.passThrough) {

        if (this.source.statusCode &&
            !this.statusCode) {

            this.statusCode = this.source.statusCode;                        // Stream is an HTTP response
        }

        if (this.source.headers) {
            let headerKeys = Object.keys(this.source.headers);

            if (headerKeys.length) {
                const localHeaders = this.headers;
                this.headers = {};

                for (let i = 0; i < headerKeys.length; ++i) {
                    const key = headerKeys[i];
                    this.header(key.toLowerCase(), Hoek.clone(this.source.headers[key]));     // Clone arrays
                }

                headerKeys = Object.keys(localHeaders);
                for (let i = 0; i < headerKeys.length; ++i) {
                    const key = headerKeys[i];
                    this.header(key, localHeaders[key], { append: key === 'set-cookie' });
                }
            }
        }
    }

    this.statusCode = this.statusCode || 200;
};

internals.Response.prototype._processPrepare = function (data, next) {

    if (!this._processors.prepare) {
        return next(this, data);
    }

    this._processors.prepare(this, (prepared) => {

        return next(prepared, data);
    });
};

internals.Response.prototype._marshal = function (next) {

    if (!this._processors.marshal) {
        return this._streamify(this.source, next);
    }

    this._processors.marshal(this, (err, source) => {

        if (err) {
            return next(err);
        }

        return this._streamify(source, next);
    });
};

internals.Response.prototype._streamify = function (source, next) {

    if (source instanceof Stream) {
        if (typeof source._read !== 'function' || typeof source._readableState !== 'object') {
            return next(Boom.badImplementation('Stream must have a streams2 readable interface'));
        }

        if (source._readableState.objectMode) {
            return next(Boom.badImplementation('Cannot reply with stream in object mode'));
        }

        this._payload = source;
        return next();
    }

    let payload = source;
    if (this.variety === 'plain' &&
        source !== null &&
        typeof source !== 'string') {

        const options = this.settings.stringify || {};
        const space = options.space || this.request.route.settings.json.space;
        const replacer = options.replacer || this.request.route.settings.json.replacer;
        const suffix = options.suffix || this.request.route.settings.json.suffix || '';
        try {
            payload = JSON.stringify(payload, replacer, space);
        }
        catch (err) {
            return next(err);
        }

        if (suffix) {
            payload = payload + suffix;
        }
    }
    else if (this.settings.stringify) {
        return next(Boom.badImplementation('Cannot set formatting options on non object response'));
    }

    this._payload = new internals.Payload(payload, this.settings);
    return next();
};

internals.Response.prototype._tap = function () {

    return (this.listeners('finish').length || this.listeners('peek').length ? new Peekaboo(this) : null);
};

internals.Response.prototype._close = function () {

    if (this._processors.close) {
        this._processors.close(this);
    }

    const stream = this._payload || this.source;
    if (stream instanceof Stream) {
        if (stream.close) {
            stream.close();
        }
        else if (stream.destroy) {
            stream.destroy();
        }
        else {
            const read = () => {

                stream.read();
            };

            const end = () => {

                stream.removeListener('readable', read);
                stream.removeListener('error', end);
                stream.removeListener('end', end);
            };

            stream.on('readable', read);
            stream.once('error', end);
            stream.once('end', end);
        }
    }
};

internals.Response.prototype._isPayloadSupported = function () {

    return (this.request.method !== 'head' && this.statusCode !== 304 && this.statusCode !== 204);
};

internals.Response.Payload = internals.Payload = function (payload, options) {

    Stream.Readable.call(this);
    this._data = payload;
    this._prefix = null;
    this._suffix = null;
    this._sizeOffset = 0;
    this._encoding = options.encoding;
};

Hoek.inherits(internals.Payload, Stream.Readable);

internals.Payload.prototype._read = function (/* size */) {

    if (this._prefix) {
        this.push(this._prefix, this._encoding);
    }

    if (this._data) {
        this.push(this._data, this._encoding);
    }

    if (this._suffix) {
        this.push(this._suffix, this._encoding);
    }

    this.push(null);
};

internals.Payload.prototype.size = function () {

    if (!this._data) {
        return this._sizeOffset;
    }

    return (Buffer.isBuffer(this._data) ? this._data.length : Buffer.byteLength(this._data, this._encoding)) + this._sizeOffset;
};

internals.Payload.prototype.jsonp = function (variable) {

    this._sizeOffset = this._sizeOffset + variable.length + 7;
    this._prefix = '/**/' + variable + '(';                 // '/**/' prefix prevents CVE-2014-4671 security exploit
    this._data = Buffer.isBuffer(this._data) ? this._data : this._data.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
    this._suffix = ');';
};
<commit_msg>Update JSON response stringify for better performance<commit_after>'use strict';

// Load modules

const Stream = require('stream');
const Events = require('events');
const Boom = require('boom');
const Hoek = require('hoek');
const Peekaboo = require('peekaboo');

// Declare internals

const internals = {};

exports = module.exports = internals.Response = function (source, request, options) {

    Events.EventEmitter.call(this);

    options = options || {};

    this.request = request;
    this.statusCode = null;
    this.headers = {};                          // Incomplete as some headers are stored in flags
    this.variety = null;
    this.app = {};
    this.plugins = {};
    this.send = null;                           // Set by reply()
    this.hold = null;                           // Set by reply()

    this.settings = {
        encoding: 'utf8',
        charset: 'utf-8',                       // '-' required by IANA
        ttl: null,
        stringify: null,                        // JSON.stringify options
        passThrough: true,
        varyEtag: false
    };

    this._payload = null;                       // Readable stream
    this._takeover = false;
    this._contentEncoding = null;               // Set during transmit
    this._error = null;                         // The boom object when created from an error

    this._processors = {
        marshal: options.marshal,
        prepare: options.prepare,
        close: options.close
    };

    this._setSource(source, options.variety);
};

Hoek.inherits(internals.Response, Events.EventEmitter);

internals.Response.wrap = function (result, request) {

    return (result instanceof Error ? Boom.wrap(result)
                                    : (result instanceof internals.Response ? result
                                                                            : new internals.Response(result, request)));
};

internals.Response.prototype._setSource = function (source, variety) {

    // Method must not set any headers or other properties as source can change later

    this.variety = variety || 'plain';

    if (source === null ||
        source === undefined ||
        source === '') {

        source = null;
    }
    else if (Buffer.isBuffer(source)) {
        this.variety = 'buffer';
    }
    else if (source instanceof Stream) {
        this.variety = 'stream';
    }
    else if (typeof source === 'object' &&
        typeof source.then === 'function') {                // Promise object

        this.variety = 'promise';
    }

    this.source = source;
};

internals.Response.prototype.code = function (statusCode) {

    Hoek.assert(Hoek.isInteger(statusCode), 'Status code must be an integer');

    this.statusCode = statusCode;
    return this;
};

internals.Response.prototype.header = function (key, value, options) {

    key = key.toLowerCase();
    if (key === 'vary') {
        return this.vary(value);
    }

    return this._header(key, value, options);
};

internals.Response.prototype._header = function (key, value, options) {

    options = options || {};
    const append = options.append || false;
    const separator = options.separator || ',';
    const override = options.override !== false;
    const duplicate = options.duplicate !== false;

    // Ensure key and values do not include non-ascii text

    if (value !== undefined &&
        value !== null) {

        const headerValues = [key].concat(value);
        for (let i = 0; i < headerValues.length; ++i) {
            const header = headerValues[i];
            const buffer = Buffer.isBuffer(header) ? header : new Buffer(header.toString());
            for (let j = 0; j < buffer.length; ++j) {
                Hoek.assert((buffer[j] & 0x7f) === buffer[j], 'Header value cannot contain or convert into non-ascii characters:', key);
            }
        }
    }

    if ((!append && override) ||
        !this.headers[key]) {

        this.headers[key] = value;
    }
    else if (override) {
        if (key === 'set-cookie') {
            this.headers[key] = [].concat(this.headers[key], value);
        }
        else {
            const existing = this.headers[key];
            if (!duplicate) {
                const values = existing.split(separator);
                for (let i = 0; i < values.length; ++i) {
                    if (values[i] === value) {
                        return this;
                    }
                }
            }

            this.headers[key] = existing + separator + value;
        }
    }

    return this;
};

internals.Response.prototype.vary = function (value) {

    if (value === '*') {
        this.headers.vary = '*';
    }
    else if (!this.headers.vary) {
        this.headers.vary = value;
    }
    else if (this.headers.vary !== '*') {
        this._header('vary', value, { append: true, duplicate: false });
    }

    return this;
};

internals.Response.prototype.etag = function (tag, options) {

    Hoek.assert(tag !== '*', 'ETag cannot be *');

    options = options || {};
    this._header('etag', (options.weak ? 'W/' : '') + '"' + tag + '"');
    this.settings.varyEtag = options.vary !== false && !options.weak;       // vary defaults to true
    return this;
};

internals.Response.prototype.type = function (type) {

    this._header('content-type', type);
    return this;
};

internals.Response.prototype.bytes = function (bytes) {

    this._header('content-length', bytes);
    return this;
};

internals.Response.prototype.location = function (uri) {

    this._header('location', uri);
    return this;
};

internals.Response.prototype.created = function (location) {

    Hoek.assert(this.request.method === 'post' || this.request.method === 'put', 'Cannot create resource on GET');

    this.statusCode = 201;
    this.location(location);
    return this;
};

internals.Response.prototype.replacer = function (method) {

    this.settings.stringify = this.settings.stringify || {};
    this.settings.stringify.replacer = method;
    return this;
};

internals.Response.prototype.spaces = function (count) {

    this.settings.stringify = this.settings.stringify || {};
    this.settings.stringify.space = count;
    return this;
};

internals.Response.prototype.suffix = function (suffix) {

    this.settings.stringify = this.settings.stringify || {};
    this.settings.stringify.suffix = suffix;
    return this;
};

internals.Response.prototype.passThrough = function (enabled) {

    this.settings.passThrough = (enabled !== false);    // Defaults to true
    return this;
};

internals.Response.prototype.redirect = function (location) {

    this.statusCode = 302;
    this.location(location);
    this.temporary = this._temporary;
    this.permanent = this._permanent;
    this.rewritable = this._rewritable;
    return this;
};

internals.Response.prototype._temporary = function (isTemporary) {

    this._setTemporary(isTemporary !== false);           // Defaults to true
    return this;
};

internals.Response.prototype._permanent = function (isPermanent) {

    this._setTemporary(isPermanent === false);           // Defaults to true
    return this;
};

internals.Response.prototype._rewritable = function (isRewritable) {

    this._setRewritable(isRewritable !== false);         // Defaults to true
    return this;
};

internals.Response.prototype._isTemporary = function () {

    return this.statusCode === 302 || this.statusCode === 307;
};

internals.Response.prototype._isRewritable = function () {

    return this.statusCode === 301 || this.statusCode === 302;
};

internals.Response.prototype._setTemporary = function (isTemporary) {

    if (isTemporary) {
        if (this._isRewritable()) {
            this.statusCode = 302;
        }
        else {
            this.statusCode = 307;
        }
    }
    else {
        if (this._isRewritable()) {
            this.statusCode = 301;
        }
        else {
            this.statusCode = 308;
        }
    }
};

internals.Response.prototype._setRewritable = function (isRewritable) {

    if (isRewritable) {
        if (this._isTemporary()) {
            this.statusCode = 302;
        }
        else {
            this.statusCode = 301;
        }
    }
    else {
        if (this._isTemporary()) {
            this.statusCode = 307;
        }
        else {
            this.statusCode = 308;
        }
    }
};

internals.Response.prototype.encoding = function (encoding) {

    this.settings.encoding = encoding;
    return this;
};

internals.Response.prototype.charset = function (charset) {

    this.settings.charset = charset || null;
    return this;
};

internals.Response.prototype.ttl = function (ttl) {

    this.settings.ttl = ttl;
    return this;
};

internals.Response.prototype.state = function (name, value, options) {          // options: see Defaults.state

    this.request._setState(name, value, options);
    return this;
};

internals.Response.prototype.unstate = function (name, options) {

    this.request._clearState(name, options);
    return this;
};

internals.Response.prototype.takeover = function () {

    this._takeover = true;
    return this;
};

internals.Response.prototype._prepare = function (data, next) {

    this._passThrough();

    if (this.variety !== 'promise') {
        return this._processPrepare(data, next);
    }

    const onDone = (source) => {

        if (source instanceof Error) {
            return next(Boom.wrap(source), data);
        }

        if (source instanceof internals.Response) {
            return source._processPrepare(data, next);
        }

        this._setSource(source);
        this._passThrough();
        this._processPrepare(data, next);
    };

    this.source.then(onDone, onDone);
};

internals.Response.prototype._passThrough = function () {

    if (this.variety === 'stream' &&
        this.settings.passThrough) {

        if (this.source.statusCode &&
            !this.statusCode) {

            this.statusCode = this.source.statusCode;                        // Stream is an HTTP response
        }

        if (this.source.headers) {
            let headerKeys = Object.keys(this.source.headers);

            if (headerKeys.length) {
                const localHeaders = this.headers;
                this.headers = {};

                for (let i = 0; i < headerKeys.length; ++i) {
                    const key = headerKeys[i];
                    this.header(key.toLowerCase(), Hoek.clone(this.source.headers[key]));     // Clone arrays
                }

                headerKeys = Object.keys(localHeaders);
                for (let i = 0; i < headerKeys.length; ++i) {
                    const key = headerKeys[i];
                    this.header(key, localHeaders[key], { append: key === 'set-cookie' });
                }
            }
        }
    }

    this.statusCode = this.statusCode || 200;
};

internals.Response.prototype._processPrepare = function (data, next) {

    if (!this._processors.prepare) {
        return next(this, data);
    }

    this._processors.prepare(this, (prepared) => {

        return next(prepared, data);
    });
};

internals.Response.prototype._marshal = function (next) {

    if (!this._processors.marshal) {
        return this._streamify(this.source, next);
    }

    this._processors.marshal(this, (err, source) => {

        if (err) {
            return next(err);
        }

        return this._streamify(source, next);
    });
};

internals.Response.prototype._streamify = function (source, next) {

    if (source instanceof Stream) {
        if (typeof source._read !== 'function' || typeof source._readableState !== 'object') {
            return next(Boom.badImplementation('Stream must have a streams2 readable interface'));
        }

        if (source._readableState.objectMode) {
            return next(Boom.badImplementation('Cannot reply with stream in object mode'));
        }

        this._payload = source;
        return next();
    }

    let payload = source;
    if (this.variety === 'plain' &&
        source !== null &&
        typeof source !== 'string') {

        const options = this.settings.stringify || {};
        const space = options.space || this.request.route.settings.json.space;
        const replacer = options.replacer || this.request.route.settings.json.replacer;
        const suffix = options.suffix || this.request.route.settings.json.suffix || '';
        try {
            var arguments = replacer || space ? [payload, replacer, space] : [payload];
            payload = JSON.stringify.apply(JSON, arguments);
        }
        catch (err) {
            return next(err);
        }

        if (suffix) {
            payload = payload + suffix;
        }
    }
    else if (this.settings.stringify) {
        return next(Boom.badImplementation('Cannot set formatting options on non object response'));
    }

    this._payload = new internals.Payload(payload, this.settings);
    return next();
};

internals.Response.prototype._tap = function () {

    return (this.listeners('finish').length || this.listeners('peek').length ? new Peekaboo(this) : null);
};

internals.Response.prototype._close = function () {

    if (this._processors.close) {
        this._processors.close(this);
    }

    const stream = this._payload || this.source;
    if (stream instanceof Stream) {
        if (stream.close) {
            stream.close();
        }
        else if (stream.destroy) {
            stream.destroy();
        }
        else {
            const read = () => {

                stream.read();
            };

            const end = () => {

                stream.removeListener('readable', read);
                stream.removeListener('error', end);
                stream.removeListener('end', end);
            };

            stream.on('readable', read);
            stream.once('error', end);
            stream.once('end', end);
        }
    }
};

internals.Response.prototype._isPayloadSupported = function () {

    return (this.request.method !== 'head' && this.statusCode !== 304 && this.statusCode !== 204);
};

internals.Response.Payload = internals.Payload = function (payload, options) {

    Stream.Readable.call(this);
    this._data = payload;
    this._prefix = null;
    this._suffix = null;
    this._sizeOffset = 0;
    this._encoding = options.encoding;
};

Hoek.inherits(internals.Payload, Stream.Readable);

internals.Payload.prototype._read = function (/* size */) {

    if (this._prefix) {
        this.push(this._prefix, this._encoding);
    }

    if (this._data) {
        this.push(this._data, this._encoding);
    }

    if (this._suffix) {
        this.push(this._suffix, this._encoding);
    }

    this.push(null);
};

internals.Payload.prototype.size = function () {

    if (!this._data) {
        return this._sizeOffset;
    }

    return (Buffer.isBuffer(this._data) ? this._data.length : Buffer.byteLength(this._data, this._encoding)) + this._sizeOffset;
};

internals.Payload.prototype.jsonp = function (variable) {

    this._sizeOffset = this._sizeOffset + variable.length + 7;
    this._prefix = '/**/' + variable + '(';                 // '/**/' prefix prevents CVE-2014-4671 security exploit
    this._data = Buffer.isBuffer(this._data) ? this._data : this._data.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
    this._suffix = ');';
};
<|endoftext|>


<commit_before>import path from 'path';
import React, {Fragment} from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';

import {EnableableOperationPropType} from '../prop-types';
import Tooltip from '../atom/tooltip';
import Commands, {Command} from '../atom/commands';
import AtomTextEditor from '../atom/atom-text-editor';
import {getDataFromGithubUrl} from './issueish-link';
import AggregatedReviewsContainer from '../containers/aggregated-reviews-container';
import EmojiReactionsController from '../controllers/emoji-reactions-controller';
import GithubDotcomMarkdown from './github-dotcom-markdown';
import ErrorView from './error-view';
import PatchPreviewView from './patch-preview-view';
import CheckoutButton from './checkout-button';
import Timeago from './timeago';
import Octicon from '../atom/octicon';
import RefHolder from '../models/ref-holder';
import ResolveReviewThreadMutation from '../mutations/resolve-review-thread';
import UnresolveReviewThreadMutation from '../mutations/unresolve-review-thread';

export default class ReviewsView extends React.Component {
  static propTypes = {
    // Relay results
    relay: PropTypes.shape({
      environment: PropTypes.object.isRequired,
    }).isRequired,
    repository: PropTypes.object.isRequired,
    pullRequest: PropTypes.object.isRequired,

    // Package models
    multiFilePatch: PropTypes.object.isRequired,
    contextLines: PropTypes.number.isRequired,
    checkoutOp: EnableableOperationPropType.isRequired,
    postingToThreadID: PropTypes.string,

    // for the dotcom link in the empty state
    number: PropTypes.number.isRequired,
    repo: PropTypes.string.isRequired,
    owner: PropTypes.string.isRequired,
    workdir: PropTypes.string.isRequired,

    // Atom environment
    workspace: PropTypes.object.isRequired,
    config: PropTypes.object.isRequired,
    commands: PropTypes.object.isRequired,
    tooltips: PropTypes.object.isRequired,

    // Action methods
    openFile: PropTypes.func.isRequired,
    openDiff: PropTypes.func.isRequired,
    openPR: PropTypes.func.isRequired,
    moreContext: PropTypes.func.isRequired,
    lessContext: PropTypes.func.isRequired,
    openIssueish: PropTypes.func.isRequired,
    addSingleComment: PropTypes.func.isRequired,
  }

  constructor(props) {
    super(props);

    this.rootHolder = new RefHolder();
    this.replyHolders = new Map();
    this.state = {
      isRefreshing: false,
      resolvedThreadsMarkedVisible: new Set(),
    };
  }

  render() {
    return (

        {this.renderCommands()}

        <AggregatedReviewsContainer pullRequest={this.props.pullRequest}>
          {({errors, summaries, commentThreads, refetch}) => {
            if (errors && errors.length > 0) {
              return this.renderErrors(errors);
            }

            return (
              <Fragment>
                {this.renderHeader(refetch)}

                  {this.renderReviewSummaries(summaries)}
                  {this.renderReviewCommentThreads(commentThreads)}

              </Fragment>
            );
          }}
        </AggregatedReviewsContainer>

    );
  }

  renderErrors(errors) {
    return errors.map((error, i) => (
      <ErrorView
        key={`error-${i}`}
        title="Pagination error"
        descriptions={[error.stack || error.toString()]}
      />
    ));
  }

  renderCommands() {
    return (
      <Commands registry={this.props.commands} target={this.rootHolder}>
        <Command command="github:more-context" callback={this.props.moreContext} />
        <Command command="github:less-context" callback={this.props.lessContext} />
      </Commands>
    );
  }

  renderHeader(refetch) {
    const refresh = () => {
      if (this.state.isRefreshing) {
        return;
      }
      this.setState({isRefreshing: true});
      refetch(() => {
        this.setState({isRefreshing: false});
      });
    };
    return (

          Reviews for

            {this.props.owner}/{this.props.repo}#{this.props.number}

        <button
          className={cx(
            'github-Reviews-headerButton github-Reviews-clickable icon icon-repo-sync',
            {refreshing: this.state.isRefreshing},
          )}
          onClick={refresh}
        />
        <CheckoutButton
          checkoutOp={this.props.checkoutOp}
          classNamePrefix="github-Reviews-checkoutButton--"
          classNames={['github-Reviews-headerButton']}
        />

    );
  }

  renderEmptyState() {
    const {number, repo, owner} = this.props;
    // todo: make this open the review flow in Atom instead of dotcom
    const pullRequestURL = `https://www.github.com/${owner}/${repo}/pull/${number}/`;
    return (

          This pull request has no reviews

            Start a review

    );
  }

  renderReviewSummaries = summaries => {
    if (summaries.length === 0) {
      return this.renderEmptyState();
    }

    return (

          Summaries

          {summaries.map(this.renderReviewSummary)}

    );
  }

  renderReviewSummary = review => {
    const reviewTypes = type => {
      return {
        APPROVED: {icon: 'icon-check', copy: 'approved these changes'},
        COMMENTED: {icon: 'icon-comment', copy: 'commented'},
        CHANGES_REQUESTED: {icon: 'icon-alert', copy: 'requested changes'},
      }[type] || {icon: '', copy: ''};
    };

    const {icon, copy} = reviewTypes(review.state);

    // filter non actionable empty summary comments from this view
    if (review.state === 'PENDING' || (review.state === 'COMMENTED' && review.bodyHTML === '')) {
      return null;
    }

    const reviewAuthor = review.author ? review.author.login : '';
    return (

          <span className={`github-ReviewSummary-icon icon ${icon}`} />

          <a className="github-ReviewSummary-username" href={`https://github.com/${reviewAuthor}`}>{reviewAuthor}
          {copy}
          <Timeago className="github-ReviewSummary-timeAgo" time={review.submittedAt} displayStyle="short" />

          <GithubDotcomMarkdown
            html={review.bodyHTML}
            switchToIssueish={this.props.openIssueish}
            openIssueishLinkInNewTab={this.openIssueishLinkInNewTab}
          />
          <EmojiReactionsController reactable={review} tooltips={this.props.tooltips} />

    );
  }

  renderReviewCommentThreads = commentThreads => {
    if (commentThreads.length === 0) {
      return null;
    }

    const resolvedThreads = commentThreads.filter(pair => pair.thread.isResolved).length;

    return (

          Comments

              Resolved
              {' '}{resolvedThreads}{' '}
              of
              {' '}{commentThreads.length}

            <progress className="github-Reviews-progessBar" value={resolvedThreads} max={commentThreads.length} />

          {commentThreads.map(this.renderReviewCommentThread)}

    );
  }

  renderReviewCommentThread = commentThread => {
    const {comments, thread} = commentThread;
    const rootComment = comments[0];
    if (!rootComment) {
      return null;
    }
    const {dir, base} = path.parse(rootComment.path);
    // TODO: fixme
    const lineNumber = rootComment.position;

    const refJumpToFileButton = new RefHolder();
    const jumpToFileDisabledLabel = 'Checkout this pull request to enable Jump To File.';

    const threadVisible = !thread.isResolved || this.state.resolvedThreadsMarkedVisible.has(thread.id);
    const positionText = lineNumber ? lineNumber : 'outdated';
    const navButtonClasses = cx('github-Review-navButton icon icon-code', {outdated: !lineNumber});

    return (
      <details className="github-Review" key={`review-comment-${rootComment.id}`}>

          {dir && {dir}}
          {dir ? '/' : ''}{base}
          {positionText}

          <Timeago className="github-Review-referenceTimeAgo" time={rootComment.createdAt} displayStyle="short" />

          <button className={navButtonClasses}
            data-path={rootComment.path} data-line={lineNumber} data-diff={rootComment.diffHunk}
            onClick={this.openFile} disabled={this.props.checkoutOp.isEnabled()}
            ref={refJumpToFileButton.setter}>
            Jump To File
          </button>
          <button className={navButtonClasses}
            data-path={rootComment.path} data-line={lineNumber}
            onClick={this.openDiff}>
            Open Diff
          </button>
          {this.props.checkoutOp.isEnabled() &&
            <Tooltip
              manager={this.props.tooltips}
              target={refJumpToFileButton}
              title={jumpToFileDisabledLabel}
              showDelay={200}
            />
          }

        {rootComment.position !== null && (
          <PatchPreviewView
            multiFilePatch={this.props.multiFilePatch}
            fileName={rootComment.path}
            diffRow={rootComment.position}
            maxRowCount={this.props.contextLines}
            config={this.props.config}
          />
        )}

        {thread.isResolved && this.renderResolvedHeader(thread, threadVisible)}
        {threadVisible && this.renderThread({thread, comments})}

    );
  }

  renderResolvedHeader = (thread, visible) => {
    return (
      <Fragment>

 Marked as resolved by @{thread.resolvedBy.login}

        {
          visible ?
            <button onClick={() => this.collapseThread(thread.id)}> Hide conversation </button> :
            <button onClick={() => this.uncollapseThread(thread.id)}> Show conversation </button>
        }
      </Fragment>
    );
  }

  renderThread = ({thread, comments}) => {
    let replyHolder = this.replyHolders.get(thread.id);
    if (!replyHolder) {
      replyHolder = new RefHolder();
      this.replyHolders.set(thread.id, replyHolder);
    }
    const lastComment = comments[comments.length - 1];
    const isPosting = this.props.postingToThreadID !== null;

    return (
      <Fragment>

          {comments.map(this.renderComment)}

            <AtomTextEditor
              placeholderText="Reply..."
              lineNumberGutterVisible={false}
              softWrapped={true}
              autoHeight={true}
              readOnly={isPosting}
              refModel={replyHolder}
            />

          <button
            className="github-Review-replyButton btn"
            title="Add your comment"
            disabled={isPosting}
            onClick={() => this.submitReply(replyHolder, thread, lastComment)}>
            Comment
          </button>
          {this.renderResolveButton(thread)}

      </Fragment>
    );
  }

  renderResolveButton = thread => {
    if (thread.isResolved) {
      return (
        <button
          className="github-Review-resolveButton btn btn-primary icon icon-check"
          title="Unresolve conversation"
          onClick={() => this.unresolveReviewThread(thread)}>
          Unresolve conversation
        </button>
      );
    } else {
      return (
        <button
          className="github-Review-resolveButton btn btn-primary icon icon-check"
          title="Resolve conversation"
          onClick={() => this.resolveReviewThread(thread)}>
          Resolve conversation
        </button>
      );
    }
  }

  renderComment = comment => {
    if (comment.isMinimized) {
      return (

          <Octicon icon={'fold'} className="github-Review-icon" />
          This comment was hidden

      );
    }

    return (

          <a className="github-Review-username" href={`https://github.com/${comment.author.login}`}>{comment.author.login}

            <Timeago displayStyle="long" time={comment.createdAt} />

          <GithubDotcomMarkdown
            html={comment.bodyHTML}
            switchToIssueish={this.props.openIssueish}
            openIssueishLinkInNewTab={this.openIssueishLinkInNewTab}
          />
          <EmojiReactionsController reactable={comment} tooltips={this.props.tooltips} />

    );
  }

  collapseThread = threadId => {
    const resolvedThreadsMarkedVisible = this.state.resolvedThreadsMarkedVisible;
    resolvedThreadsMarkedVisible.delete(threadId);
    this.setState({resolvedThreadsMarkedVisible});
  }

  uncollapseThread = threadId => {
    const resolvedThreadsMarkedVisible = this.state.resolvedThreadsMarkedVisible;
    resolvedThreadsMarkedVisible.add(threadId);
    this.setState({resolvedThreadsMarkedVisible});
  }

  openFile = evt => {
    if (!this.props.checkoutOp.isEnabled()) {
      const target = evt.currentTarget;
      this.props.openFile(target.dataset.path, target.dataset.diff);
    }
  }

  openDiff = evt => {
    const target = evt.currentTarget;
    this.props.openDiff(target.dataset.path, parseInt(target.dataset.line, 10));
  }

  openIssueishLinkInNewTab = evt => {
    const {repoOwner, repoName, issueishNumber} = getDataFromGithubUrl(evt.target.dataset.url);
    return this.props.openIssueish(repoOwner, repoName, issueishNumber);
  }

  resolveReviewThread = thread => {
    if (thread.viewerCanResolve) {
      this.collapseThread(thread.id);
      const onCompleted = () => this.collapseThread(thread.id);
      const onError = () => this.uncollapseThread(thread.id);
      ResolveReviewThreadMutation(thread.id, this.props.relay.environment, onCompleted, onError);
    }
  }

  unresolveReviewThread = thread => {
    if (thread.viewerCanUnresolve) {
      UnresolveReviewThreadMutation(thread.id, this.props.relay.environment);
    }
  }

  submitReply(replyHolder, thread, lastComment) {
    const body = replyHolder.map(editor => editor.getText()).getOr('');
    return this.props.addSingleComment(body, thread.id, lastComment.id);
  }
}
<commit_msg>Clear the reply editor's contents after the comment has been submitted<commit_after>import path from 'path';
import React, {Fragment} from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';

import {EnableableOperationPropType} from '../prop-types';
import Tooltip from '../atom/tooltip';
import Commands, {Command} from '../atom/commands';
import AtomTextEditor from '../atom/atom-text-editor';
import {getDataFromGithubUrl} from './issueish-link';
import AggregatedReviewsContainer from '../containers/aggregated-reviews-container';
import EmojiReactionsController from '../controllers/emoji-reactions-controller';
import GithubDotcomMarkdown from './github-dotcom-markdown';
import ErrorView from './error-view';
import PatchPreviewView from './patch-preview-view';
import CheckoutButton from './checkout-button';
import Timeago from './timeago';
import Octicon from '../atom/octicon';
import RefHolder from '../models/ref-holder';
import ResolveReviewThreadMutation from '../mutations/resolve-review-thread';
import UnresolveReviewThreadMutation from '../mutations/unresolve-review-thread';

export default class ReviewsView extends React.Component {
  static propTypes = {
    // Relay results
    relay: PropTypes.shape({
      environment: PropTypes.object.isRequired,
    }).isRequired,
    repository: PropTypes.object.isRequired,
    pullRequest: PropTypes.object.isRequired,

    // Package models
    multiFilePatch: PropTypes.object.isRequired,
    contextLines: PropTypes.number.isRequired,
    checkoutOp: EnableableOperationPropType.isRequired,
    postingToThreadID: PropTypes.string,

    // for the dotcom link in the empty state
    number: PropTypes.number.isRequired,
    repo: PropTypes.string.isRequired,
    owner: PropTypes.string.isRequired,
    workdir: PropTypes.string.isRequired,

    // Atom environment
    workspace: PropTypes.object.isRequired,
    config: PropTypes.object.isRequired,
    commands: PropTypes.object.isRequired,
    tooltips: PropTypes.object.isRequired,

    // Action methods
    openFile: PropTypes.func.isRequired,
    openDiff: PropTypes.func.isRequired,
    openPR: PropTypes.func.isRequired,
    moreContext: PropTypes.func.isRequired,
    lessContext: PropTypes.func.isRequired,
    openIssueish: PropTypes.func.isRequired,
    addSingleComment: PropTypes.func.isRequired,
  }

  constructor(props) {
    super(props);

    this.rootHolder = new RefHolder();
    this.replyHolders = new Map();
    this.state = {
      isRefreshing: false,
      resolvedThreadsMarkedVisible: new Set(),
    };
  }

  render() {
    return (

        {this.renderCommands()}

        <AggregatedReviewsContainer pullRequest={this.props.pullRequest}>
          {({errors, summaries, commentThreads, refetch}) => {
            if (errors && errors.length > 0) {
              return this.renderErrors(errors);
            }

            return (
              <Fragment>
                {this.renderHeader(refetch)}

                  {this.renderReviewSummaries(summaries)}
                  {this.renderReviewCommentThreads(commentThreads)}

              </Fragment>
            );
          }}
        </AggregatedReviewsContainer>

    );
  }

  renderErrors(errors) {
    return errors.map((error, i) => (
      <ErrorView
        key={`error-${i}`}
        title="Pagination error"
        descriptions={[error.stack || error.toString()]}
      />
    ));
  }

  renderCommands() {
    return (
      <Commands registry={this.props.commands} target={this.rootHolder}>
        <Command command="github:more-context" callback={this.props.moreContext} />
        <Command command="github:less-context" callback={this.props.lessContext} />
      </Commands>
    );
  }

  renderHeader(refetch) {
    const refresh = () => {
      if (this.state.isRefreshing) {
        return;
      }
      this.setState({isRefreshing: true});
      refetch(() => {
        this.setState({isRefreshing: false});
      });
    };
    return (

          Reviews for

            {this.props.owner}/{this.props.repo}#{this.props.number}

        <button
          className={cx(
            'github-Reviews-headerButton github-Reviews-clickable icon icon-repo-sync',
            {refreshing: this.state.isRefreshing},
          )}
          onClick={refresh}
        />
        <CheckoutButton
          checkoutOp={this.props.checkoutOp}
          classNamePrefix="github-Reviews-checkoutButton--"
          classNames={['github-Reviews-headerButton']}
        />

    );
  }

  renderEmptyState() {
    const {number, repo, owner} = this.props;
    // todo: make this open the review flow in Atom instead of dotcom
    const pullRequestURL = `https://www.github.com/${owner}/${repo}/pull/${number}/`;
    return (

          This pull request has no reviews

            Start a review

    );
  }

  renderReviewSummaries = summaries => {
    if (summaries.length === 0) {
      return this.renderEmptyState();
    }

    return (

          Summaries

          {summaries.map(this.renderReviewSummary)}

    );
  }

  renderReviewSummary = review => {
    const reviewTypes = type => {
      return {
        APPROVED: {icon: 'icon-check', copy: 'approved these changes'},
        COMMENTED: {icon: 'icon-comment', copy: 'commented'},
        CHANGES_REQUESTED: {icon: 'icon-alert', copy: 'requested changes'},
      }[type] || {icon: '', copy: ''};
    };

    const {icon, copy} = reviewTypes(review.state);

    // filter non actionable empty summary comments from this view
    if (review.state === 'PENDING' || (review.state === 'COMMENTED' && review.bodyHTML === '')) {
      return null;
    }

    const reviewAuthor = review.author ? review.author.login : '';
    return (

          <span className={`github-ReviewSummary-icon icon ${icon}`} />

          <a className="github-ReviewSummary-username" href={`https://github.com/${reviewAuthor}`}>{reviewAuthor}
          {copy}
          <Timeago className="github-ReviewSummary-timeAgo" time={review.submittedAt} displayStyle="short" />

          <GithubDotcomMarkdown
            html={review.bodyHTML}
            switchToIssueish={this.props.openIssueish}
            openIssueishLinkInNewTab={this.openIssueishLinkInNewTab}
          />
          <EmojiReactionsController reactable={review} tooltips={this.props.tooltips} />

    );
  }

  renderReviewCommentThreads = commentThreads => {
    if (commentThreads.length === 0) {
      return null;
    }

    const resolvedThreads = commentThreads.filter(pair => pair.thread.isResolved).length;

    return (

          Comments

              Resolved
              {' '}{resolvedThreads}{' '}
              of
              {' '}{commentThreads.length}

            <progress className="github-Reviews-progessBar" value={resolvedThreads} max={commentThreads.length} />

          {commentThreads.map(this.renderReviewCommentThread)}

    );
  }

  renderReviewCommentThread = commentThread => {
    const {comments, thread} = commentThread;
    const rootComment = comments[0];
    if (!rootComment) {
      return null;
    }
    const {dir, base} = path.parse(rootComment.path);
    // TODO: fixme
    const lineNumber = rootComment.position;

    const refJumpToFileButton = new RefHolder();
    const jumpToFileDisabledLabel = 'Checkout this pull request to enable Jump To File.';

    const threadVisible = !thread.isResolved || this.state.resolvedThreadsMarkedVisible.has(thread.id);
    const positionText = lineNumber ? lineNumber : 'outdated';
    const navButtonClasses = cx('github-Review-navButton icon icon-code', {outdated: !lineNumber});

    return (
      <details className="github-Review" key={`review-comment-${rootComment.id}`}>

          {dir && {dir}}
          {dir ? '/' : ''}{base}
          {positionText}

          <Timeago className="github-Review-referenceTimeAgo" time={rootComment.createdAt} displayStyle="short" />

          <button className={navButtonClasses}
            data-path={rootComment.path} data-line={lineNumber} data-diff={rootComment.diffHunk}
            onClick={this.openFile} disabled={this.props.checkoutOp.isEnabled()}
            ref={refJumpToFileButton.setter}>
            Jump To File
          </button>
          <button className={navButtonClasses}
            data-path={rootComment.path} data-line={lineNumber}
            onClick={this.openDiff}>
            Open Diff
          </button>
          {this.props.checkoutOp.isEnabled() &&
            <Tooltip
              manager={this.props.tooltips}
              target={refJumpToFileButton}
              title={jumpToFileDisabledLabel}
              showDelay={200}
            />
          }

        {rootComment.position !== null && (
          <PatchPreviewView
            multiFilePatch={this.props.multiFilePatch}
            fileName={rootComment.path}
            diffRow={rootComment.position}
            maxRowCount={this.props.contextLines}
            config={this.props.config}
          />
        )}

        {thread.isResolved && this.renderResolvedHeader(thread, threadVisible)}
        {threadVisible && this.renderThread({thread, comments})}

    );
  }

  renderResolvedHeader = (thread, visible) => {
    return (
      <Fragment>

 Marked as resolved by @{thread.resolvedBy.login}

        {
          visible ?
            <button onClick={() => this.collapseThread(thread.id)}> Hide conversation </button> :
            <button onClick={() => this.uncollapseThread(thread.id)}> Show conversation </button>
        }
      </Fragment>
    );
  }

  renderThread = ({thread, comments}) => {
    let replyHolder = this.replyHolders.get(thread.id);
    if (!replyHolder) {
      replyHolder = new RefHolder();
      this.replyHolders.set(thread.id, replyHolder);
    }
    const lastComment = comments[comments.length - 1];
    const isPosting = this.props.postingToThreadID !== null;

    return (
      <Fragment>

          {comments.map(this.renderComment)}

            <AtomTextEditor
              placeholderText="Reply..."
              lineNumberGutterVisible={false}
              softWrapped={true}
              autoHeight={true}
              readOnly={isPosting}
              refModel={replyHolder}
            />

          <button
            className="github-Review-replyButton btn"
            title="Add your comment"
            disabled={isPosting}
            onClick={() => this.submitReply(replyHolder, thread, lastComment)}>
            Comment
          </button>
          {this.renderResolveButton(thread)}

      </Fragment>
    );
  }

  renderResolveButton = thread => {
    if (thread.isResolved) {
      return (
        <button
          className="github-Review-resolveButton btn btn-primary icon icon-check"
          title="Unresolve conversation"
          onClick={() => this.unresolveReviewThread(thread)}>
          Unresolve conversation
        </button>
      );
    } else {
      return (
        <button
          className="github-Review-resolveButton btn btn-primary icon icon-check"
          title="Resolve conversation"
          onClick={() => this.resolveReviewThread(thread)}>
          Resolve conversation
        </button>
      );
    }
  }

  renderComment = comment => {
    if (comment.isMinimized) {
      return (

          <Octicon icon={'fold'} className="github-Review-icon" />
          This comment was hidden

      );
    }

    return (

          <a className="github-Review-username" href={`https://github.com/${comment.author.login}`}>{comment.author.login}

            <Timeago displayStyle="long" time={comment.createdAt} />

          <GithubDotcomMarkdown
            html={comment.bodyHTML}
            switchToIssueish={this.props.openIssueish}
            openIssueishLinkInNewTab={this.openIssueishLinkInNewTab}
          />
          <EmojiReactionsController reactable={comment} tooltips={this.props.tooltips} />

    );
  }

  collapseThread = threadId => {
    const resolvedThreadsMarkedVisible = this.state.resolvedThreadsMarkedVisible;
    resolvedThreadsMarkedVisible.delete(threadId);
    this.setState({resolvedThreadsMarkedVisible});
  }

  uncollapseThread = threadId => {
    const resolvedThreadsMarkedVisible = this.state.resolvedThreadsMarkedVisible;
    resolvedThreadsMarkedVisible.add(threadId);
    this.setState({resolvedThreadsMarkedVisible});
  }

  openFile = evt => {
    if (!this.props.checkoutOp.isEnabled()) {
      const target = evt.currentTarget;
      this.props.openFile(target.dataset.path, target.dataset.diff);
    }
  }

  openDiff = evt => {
    const target = evt.currentTarget;
    this.props.openDiff(target.dataset.path, parseInt(target.dataset.line, 10));
  }

  openIssueishLinkInNewTab = evt => {
    const {repoOwner, repoName, issueishNumber} = getDataFromGithubUrl(evt.target.dataset.url);
    return this.props.openIssueish(repoOwner, repoName, issueishNumber);
  }

  resolveReviewThread = thread => {
    if (thread.viewerCanResolve) {
      this.collapseThread(thread.id);
      const onCompleted = () => this.collapseThread(thread.id);
      const onError = () => this.uncollapseThread(thread.id);
      ResolveReviewThreadMutation(thread.id, this.props.relay.environment, onCompleted, onError);
    }
  }

  unresolveReviewThread = thread => {
    if (thread.viewerCanUnresolve) {
      UnresolveReviewThreadMutation(thread.id, this.props.relay.environment);
    }
  }

  submitReply(replyHolder, thread, lastComment) {
    const body = replyHolder.map(editor => editor.getText()).getOr('');
    const didSubmitComment = () => replyHolder.map(editor => editor.setText('', {bypassReadOnly: true}));

    return this.props.addSingleComment(body, thread.id, lastComment.id, {didSubmitComment}).catch(err => {
      // Restore the failed comment if it fails to post
      replyHolder.map(editor => editor.setText(body));
      return Promise.reject(err);
    });
  }
}
<|endoftext|>


<commit_before>'use strict';
let event = require('./event');
let container = require('./container');
let recorder = require('./recorder');
let getParamNames = require('./utils').getParamNames;
let isGenerator = require('./utils').isGenerator;

var resumeTest = module.exports.resumeTest = function (res, done, error, returnStatement) {
  recorder.add('create new promises queue for generator', function (data) {
    recorder.session.start('generator'); // creating a new promise chain
    try {
      let resume = res.next(data);
      if (resume.done) {
        done();
      } else {
        resumeTest(res, done, error, returnStatement);
      }
    } catch (err) {
      recorder.throw(err); // catch error in test and rethrow into promise
    }
    recorder.catch(error); // handle all errors in promise chain
    if (returnStatement) {
      return recorder.promise();
    }
  });
};

/**
 * Wraps test function, injects support objects from container,
 * starts promise chain with recorder, performs before/after hooks
 * through event system.
 */
module.exports.test = (test) => {
  let testFn = test.fn;
  if (!testFn) {
    return test;
  }

  test.steps = [];

  test.fn = function (done) {
    recorder.errHandler(function (err) {
      recorder.session.start('teardown');
      event.emit(event.test.failed, test, err);
      recorder.add(() => done(err));
    });

    try {
      event.emit(event.test.started, test);
      let res = testFn.apply(test, getInjectedArguments(testFn, test));
      if (isGenerator(testFn)) {
        try {
          res.next(); // running test
        } catch (err) {
          event.emit(event.test.failed, test, err);
          done(err);
          return test;
        }
        recorder.catch(); // catching possible errors in promises
        resumeTest(res, () => {
          recorder.add('fire test.passed', () => event.emit(event.test.passed, test));
          recorder.add('finish generator with no error', () => done()); // finish him
        });
      }
    } catch (err) {
      recorder.throw(err);
    } finally {
      if (!isGenerator(testFn)) {
        recorder.add('fire test.passed', () => event.emit(event.test.passed, test));
        recorder.add('finish test', () => done());
        recorder.catch();
      }
    }
  };
  return test;
};

/**
 * Injects arguments to function from controller
 */
module.exports.injected = function (fn, suite, hookName) {
  return function () {
    try {
      recorder.startUnlessRunning();
      fn.apply(this, getInjectedArguments(fn));
    } catch (err) {
      recorder.throw(err);
    }
    recorder.catch((err) => {
      event.emit(event.test.failed, suite, err); // emit
      if (hookName === 'after') event.emit(event.test.after, suite);
      if (hookName === 'afterSuite') event.emit(event.suite.after, suite);
      throw err;
    });
    return recorder.promise();
  };
};

/**
 * Starts promise chain, so helpers could enqueue their hooks
 */
module.exports.setup = function () {
  recorder.startUnlessRunning();
  event.emit(event.test.before);
};

module.exports.teardown = function () {
  recorder.startUnlessRunning();
  event.emit(event.test.after);
};

module.exports.suiteSetup = function (suite) {
  recorder.startUnlessRunning();
  event.emit(event.suite.before, suite);
};

module.exports.suiteTeardown = function (suite) {
  recorder.startUnlessRunning();
  event.emit(event.suite.after, suite);
};

function getInjectedArguments(fn, test) {
  let testArguments = [];
  let params = getParamNames(fn) || [];
  let objects = container.support();
  for (var key in params) {
    let obj = params[key];
    if (test.inject[obj]) {
      testArguments.push(test.inject[obj]);
      continue;
    }
    if (!objects[obj]) {
      throw new Error(`Object of type ${obj} is not defined in container`);
    }
    testArguments.push(container.support(obj));
  }

  return testArguments;
}
<commit_msg>fixed Cannot read property 'inject' of undefined (#618)<commit_after>'use strict';
let event = require('./event');
let container = require('./container');
let recorder = require('./recorder');
let getParamNames = require('./utils').getParamNames;
let isGenerator = require('./utils').isGenerator;

var resumeTest = module.exports.resumeTest = function (res, done, error, returnStatement) {
  recorder.add('create new promises queue for generator', function (data) {
    recorder.session.start('generator'); // creating a new promise chain
    try {
      let resume = res.next(data);
      if (resume.done) {
        done();
      } else {
        resumeTest(res, done, error, returnStatement);
      }
    } catch (err) {
      recorder.throw(err); // catch error in test and rethrow into promise
    }
    recorder.catch(error); // handle all errors in promise chain
    if (returnStatement) {
      return recorder.promise();
    }
  });
};

/**
 * Wraps test function, injects support objects from container,
 * starts promise chain with recorder, performs before/after hooks
 * through event system.
 */
module.exports.test = (test) => {
  let testFn = test.fn;
  if (!testFn) {
    return test;
  }

  test.steps = [];

  test.fn = function (done) {
    recorder.errHandler(function (err) {
      recorder.session.start('teardown');
      event.emit(event.test.failed, test, err);
      recorder.add(() => done(err));
    });

    try {
      event.emit(event.test.started, test);
      let res = testFn.apply(test, getInjectedArguments(testFn, test));
      if (isGenerator(testFn)) {
        try {
          res.next(); // running test
        } catch (err) {
          event.emit(event.test.failed, test, err);
          done(err);
          return test;
        }
        recorder.catch(); // catching possible errors in promises
        resumeTest(res, () => {
          recorder.add('fire test.passed', () => event.emit(event.test.passed, test));
          recorder.add('finish generator with no error', () => done()); // finish him
        });
      }
    } catch (err) {
      recorder.throw(err);
    } finally {
      if (!isGenerator(testFn)) {
        recorder.add('fire test.passed', () => event.emit(event.test.passed, test));
        recorder.add('finish test', () => done());
        recorder.catch();
      }
    }
  };
  return test;
};

/**
 * Injects arguments to function from controller
 */
module.exports.injected = function (fn, suite, hookName) {
  return function () {
    try {
      recorder.startUnlessRunning();
      fn.apply(this, getInjectedArguments(fn));
    } catch (err) {
      recorder.throw(err);
    }
    recorder.catch((err) => {
      event.emit(event.test.failed, suite, err); // emit
      if (hookName === 'after') event.emit(event.test.after, suite);
      if (hookName === 'afterSuite') event.emit(event.suite.after, suite);
      throw err;
    });
    return recorder.promise();
  };
};

/**
 * Starts promise chain, so helpers could enqueue their hooks
 */
module.exports.setup = function () {
  recorder.startUnlessRunning();
  event.emit(event.test.before);
};

module.exports.teardown = function () {
  recorder.startUnlessRunning();
  event.emit(event.test.after);
};

module.exports.suiteSetup = function (suite) {
  recorder.startUnlessRunning();
  event.emit(event.suite.before, suite);
};

module.exports.suiteTeardown = function (suite) {
  recorder.startUnlessRunning();
  event.emit(event.suite.after, suite);
};

function getInjectedArguments(fn, test) {
  let testArguments = [];
  let params = getParamNames(fn) || [];
  let objects = container.support();
  for (var key in params) {
    let obj = params[key];
    if (test && test.inject && test.inject[obj]) {
      testArguments.push(test.inject[obj]);
      continue;
    }
    if (!objects[obj]) {
      throw new Error(`Object of type ${obj} is not defined in container`);
    }
    testArguments.push(container.support(obj));
  }

  return testArguments;
}
<|endoftext|>