<commit_before>const spawn = require('child_process').spawn;
const Promise = require('promise');

module.exports = function(...args) {
  const {options, next} = getOptions(args);
  const promised = new Promise((resolve, reject) => {
    getLogTextAsync()
      .then((v) => logTextToCommitsAsync(v))
      .then((v) => commitsToMetaAsync(v))
      .then((v) => applyAmendAsync(v))
      .then((v) => metaToJsonAsync(v))
      .then((v) => resolve(JSON.stringify(v)))
      .catch((v) => reject(v));
  });
  if (next) return promised.then((v) => next(null, v), (v) => next(v));
  return promised;
}

function getOptions(args) {
  let first = args.shift() || {};
  if (typeof(first) === 'function') return {options: {}, next: first};
  return {options: first, next: args.shift()};
}

function getLogTextAsync() {
  return new Promise((resolve, reject) => {
    const stdio = ['ignore', 'pipe', process.stderr];
    const task = spawn('git', ['log', '--format=raw'], {stdio});
    let data = '';
    task.stdout.on('data', (v) => data += v);
    task.on('error', (v) => reject(v));
    task.on('close', (code) => {
      if (code !== 0) return reject(`git failed with code ${code}`);
      resolve(data);
    });
  });
}

class Commit {
  constructor() {
    this.hash = '';
    this.msg = '';
  }
  addMsgLine(line) { this.msg += this.msg.length ? "\n" + line : line; }
}

function logTextToCommitsAsync(text) {
  return new Promise((resolve, reject) => {

    const commitList = [];
    const next = () => { let v = new Commit(); commitList.push(v); return v; }
    const cur = () => commitList.slice(-1)[0] || next();

    for(line of text.split(/\r*\n+/)) {
      patternmatch(line, [
        [/^commit ([a-z0-9]+)$/, (hash) => (next().hash = hash)],
        [/^    (.*)$/, (line) => cur().addMsgLine(line)],
      ]);
    }
    resolve(commitList);
  });
}

class Meta {
  constructor(options) {
    this.hash = '';
    this.key = '';
    this.val = '';
    //  Where can be multiple chunks of meta information inside '{}', for
    //  example "{amend:'1';msg:'foo'} some text "{amend:'2';msg:'bar'".
    //  Chunks inside '{}' has same sequence number, so they can be
    //  grouped later with hash and sequence.
    this.seq = 0;
    Object.assign(this, options);
  }
}

function commitsToMetaAsync(commitList) {
  return new Promise((resolve, reject) => {

    let seq = 0;
    const metaList = [];
    const add = (array, v) => { array.push(v); return v; }
    const next = (options) => add(metaList, new Meta(options));
    const cur = () => metaList.slice(-1)[0] || next();

    const State = {
      IDLE: 1,
      KEY: 2,
      VAL: 3,
      META: [State.KEY, State.VAL],
    };
    let state = State.IDLE;

    for (commit of commitList) {
      for (var i = 0; i < commit.msg.length; i ++) {
        const hash = commit.hash;
        patternmatch([commit.msg[i], state], [
          [['{', State.IDLE], () => {
            state = State.KEY;
            next({hash, seq: (seq += 1)});
          }],
          [['}', [State.KEY, State.VAL]], () => state = State.IDLE],
          [[':', State.KEY], () => state = State.VAL],
          [[';', [State.KEY, State.VAL]], () => next({hash, seq})],
          [[null, [State.KEY, State.VAL]], (v) => cur().key += v],
        ]);
      }
    }
    resolve(metaList);
  });
}

function applyAmendAsync(metaList) {
  return new Promise((resolve, reject) => {
    resolve(metaList);
  });
}

function metaToJsonAsync(metaList) {
  return new Promise((resolve, reject) => {
    resolve(metaList);
  });
}

//  Pattern-matching with regexp positional matches passed to handlers.
function patternmatch(rightSeq, pairs) {
  for ([leftSeq, next] of pairs) {
    leftSeq = [].concat(leftSeq);
    rightSeq = [].concat(rightSeq);
    const res = [];
    const compare = (left, right) => {
      //  If test case not specified it's "don't care, match anything".
      if (left === null || left === undefined) return res.push(right), true;
      //  Test case can be an array.
      if (left.some) return left.some((v) => compare(v, right));
      if (!left[Symbol.match]) return left === right;
      const match = left[Symbol.match](right);
      if (!match) return false;
      return res.push(...match.slice(1)), true;
    };
    if (leftSeq.every((v, i) => compare(v, rightSeq[i]))) return next(...res);
  }
}

<commit_msg>multiple keys inside meta support<commit_after>const spawn = require('child_process').spawn;
const Promise = require('promise');

module.exports = function(...args) {
  const {options, next} = getOptions(args);
  const promised = new Promise((resolve, reject) => {
    getLogTextAsync()
      .then((v) => logTextToCommitsAsync(v))
      .then((v) => commitsToMetaAsync(v))
      .then((v) => applyAmendAsync(v))
      .then((v) => metaToJsonAsync(v))
      .then((v) => resolve(JSON.stringify(v)))
      .catch((v) => reject(v));
  });
  if (next) return promised.then((v) => next(null, v), (v) => next(v));
  return promised;
}

function getOptions(args) {
  let first = args.shift() || {};
  if (typeof(first) === 'function') return {options: {}, next: first};
  return {options: first, next: args.shift()};
}

function getLogTextAsync() {
  return new Promise((resolve, reject) => {
    const stdio = ['ignore', 'pipe', process.stderr];
    const task = spawn('git', ['log', '--format=raw'], {stdio});
    let data = '';
    task.stdout.on('data', (v) => data += v);
    task.on('error', (v) => reject(v));
    task.on('close', (code) => {
      if (code !== 0) return reject(`git failed with code ${code}`);
      resolve(data);
    });
  });
}

class Commit {
  constructor() {
    this.hash = '';
    this.msg = '';
  }
  addMsgLine(line) { this.msg += this.msg.length ? "\n" + line : line; }
}

function logTextToCommitsAsync(text) {
  return new Promise((resolve, reject) => {

    const commitList = [];
    const next = () => { let v = new Commit(); commitList.push(v); return v; }
    const cur = () => commitList.slice(-1)[0] || next();

    for(line of text.split(/\r*\n+/)) {
      patternmatch(line, [
        [/^commit ([a-z0-9]+)$/, (hash) => (next().hash = hash)],
        [/^    (.*)$/, (line) => cur().addMsgLine(line)],
      ]);
    }
    resolve(commitList);
  });
}

class Meta {
  constructor(options) {
    this.hash = '';
    this.key = '';
    this.val = '';
    //  Where can be multiple chunks of meta information inside '{}', for
    //  example "{amend:'1';msg:'foo'} some text "{amend:'2';msg:'bar'".
    //  Chunks inside '{}' has same sequence number, so they can be
    //  grouped later with hash and sequence.
    this.seq = 0;
    Object.assign(this, options);
  }
}

function commitsToMetaAsync(commitList) {
  return new Promise((resolve, reject) => {

    let seq = 0;
    const metaList = [];
    const add = (array, v) => { array.push(v); return v; }
    const next = (options) => add(metaList, new Meta(options));
    const cur = () => metaList.slice(-1)[0] || next();

    const State = {
      IDLE: 1,
      KEY: 2,
      VAL: 3,
    };
    let state = State.IDLE;

    for (commit of commitList) {
      for (var i = 0; i < commit.msg.length; i ++) {
        const hash = commit.hash;
        patternmatch([commit.msg[i], state], [
          [['{', State.IDLE], () => {
            state = State.KEY;
            next({hash, seq: (seq += 1)});
          }],
          [[';', [State.KEY, State.VAL]], () => {
            state = State.KEY;
            next({hash, seq})
          }],
          [['}', [State.KEY, State.VAL]], () => state = State.IDLE],
          [[':', State.KEY], () => state = State.VAL],
          [[null, [State.KEY, State.VAL]], (v) => cur().key += v],
        ]);
      }
    }
    resolve(metaList);
  });
}

function applyAmendAsync(metaList) {
  return new Promise((resolve, reject) => {
    resolve(metaList);
  });
}

function metaToJsonAsync(metaList) {
  return new Promise((resolve, reject) => {
    resolve(metaList);
  });
}

//  Pattern-matching with regexp positional matches passed to handlers.
function patternmatch(rightSeq, pairs) {
  for ([leftSeq, next] of pairs) {
    leftSeq = [].concat(leftSeq);
    rightSeq = [].concat(rightSeq);
    const res = [];
    const compare = (left, right) => {
      //  If test case not specified it's "don't care, match anything".
      if (left === null || left === undefined) return res.push(right), true;
      //  Test case can be an array.
      if (left.some) return left.some((v) => compare(v, right));
      if (!left[Symbol.match]) return left === right;
      const match = left[Symbol.match](right);
      if (!match) return false;
      return res.push(...match.slice(1)), true;
    };
    if (leftSeq.every((v, i) => compare(v, rightSeq[i]))) return next(...res);
  }
}

<|endoftext|>


<commit_before>var async = require('async');

// MemcachedTransaction: a wrapper around 3rdEden's node-memcached module to provide transactional behavior.

// currently supported mutations: 'set', 'del', 'touch'

function MemcachedTransaction(client, options) {
	this.queue = {};    // { key: operation }  operation to be executed per key
	this.cache = {};    // guaranteed to be the latest version of a value, if it's in here
	this.client = client;
	this.options = options || {};

	this.debug = null;

	if (this.options.debug) {
		if (this.options.debug === true) {
			this.debug = console.info;
		} else if (typeof this.options.debug === 'function') {
			this.debug = this.options.debug;
		}
	}
}

module.exports = MemcachedTransaction;

MemcachedTransaction.prototype.get = function (key, cb) {
	var cache = this.cache;

	var op = this.queue[key];
	if (op && op.type === 'del') {
		// this value is scheduled to be deleted, so consider it non-existing and return undefined

		if (cb) {
			cb(null, undefined);
		}
		return;
	}

	var value = cache[key];

	if (value !== undefined) {
		// there is a known uptodate value for this key, so return it instantly

		if (cb) {
			cb(null, value);
		}
		return;
	}

	// get the value from memcached and cache it

	if (this.debug) {
		this.debug('MemcachedTransaction: getting key', key);
	}

	this.client.get(key, function (error, value) {
		if (error) {
			if (cb) {
				cb(error);
			}
		} else {
			cache[key] = value;

			if (cb) {
				cb(null, value);
			}
		}
	});
};

MemcachedTransaction.prototype.getMulti = function (keys, cb) {
	var cache = this.cache;
	var value, result = {};
	var lookup = [];

	// for each key, check if we really need to fetch it, or if there is a known state

	for (var i = 0, len = keys.length; i < len; i++) {
		var key = keys[i];
		var op = this.queue[key];

		if (op && op.type === 'del') {
			// this value is scheduled to be deleted, so consider it non-existing and return undefined

			result[key] = undefined;
		} else {
			value = cache[key];

			if (value === undefined) {
				// prepare for fetching from memcached

				lookup.push(key);
			} else {
				// there is a known uptodate value for this key, so return it instantly

				result[key] = value;
			}
		}
	}

	// if there is nothing to look up from memcached, return all known results

	if (lookup.length === 0) {
		if (cb) {
			cb(null, result);
		}
		return;
	}

	// get the unknown values from memcached and cache them

	if (this.debug) {
		this.debug('MemcachedTransaction: getting keys', lookup);
	}

	this.client.getMulti(lookup, function (error, values) {
		if (error) {
			if (cb) {
				cb(error);
			}
			return;
		}

		for (var key in values) {
			var value = values[key];

			result[key] = value;
			cache[key] = value;
		}

		if (cb) {
			cb(null, result);
		}
	});
};

MemcachedTransaction.prototype.set = function (key, value, ttl, cb) {
	this.queue[key] = { type: 'set', key: key, value: value, ttl: ttl };
	this.cache[key] = value;

	if (cb) {
		cb();
	}
};

MemcachedTransaction.prototype.del = function (key, cb) {
	this.queue[key] = { type: 'del', key: key };
	delete this.cache[key];

	if (cb) {
		cb();
	}
};

MemcachedTransaction.prototype.touch = function (key, ttl, cb) {
	var op = this.queue[key];

	if (op) {
		switch (op.type) {
		case 'set':
			op.ttl = ttl;
			break;
		case 'del':
			// noop
			break;
		}
	} else {
		this.queue[key] = { type: 'touch', key: key, ttl: ttl };
	}

	if (cb) {
		cb();
	}
};

MemcachedTransaction.prototype._exec = function (op, cb) {
	if (this.options.simulate) {
		console.info('Executing', op);
		if (cb) {
			cb();
		}
		return;
	}

	switch (op.type) {
	case 'set':
		if (this.debug) {
			this.debug('MemcachedTransaction: setting key', op.key, 'to value', op.value, 'with TTL', op.ttl || 0);
		}

		this.client.set(op.key, op.value, op.ttl || 0, cb);
		break;
	case 'del':
		if (this.debug) {
			this.debug('MemcachedTransaction: deleting key', op.key);
		}

		this.client.del(op.key, cb);
		break;
	case 'touch':
		if (this.debug) {
			this.debug('MemcachedTransaction: touching key', op.key, 'with TTL', op.ttl || 0);
		}

		this.client.command(function touch(noreply) {
			return {
				command: ['touch', op.key, op.ttl || 0].join(' '),
				key: op.key,
				type: 'touch',
				callback: cb
			};
		});
		break;
	default:
		if (cb) {
			cb('Unknown operation type: ' + op.type);
		}
		break;
	}
};

MemcachedTransaction.prototype.commit = function (cb) {
	var ops = [];
	for (var key in this.queue) {
		ops.push(this.queue[key]);
	}

	var len = ops.length;

	// hot code paths: length 0 and 1

	if (len === 0) {
		return cb ? cb() : null;
	}

	if (len === 1) {
		return this._exec(ops[0], cb);
	}

	// more than 1 operation

	var that = this;

	async.forEach(
		ops,
		function (op, callback) {
			that._exec(op, callback);
		},
		function (error) {
			if (cb) {
				cb(error);
			}
		}
	);
};

MemcachedTransaction.prototype.rollBack = function (cb) {
	this.queue = {};
	this.cache = {};

	if (cb) {
		cb();
	}
};

<commit_msg>Changed async.forEach to forEachSeries, as tests show it can increase stability.<commit_after>var async = require('async');

// MemcachedTransaction: a wrapper around 3rdEden's node-memcached module to provide transactional behavior.

// currently supported mutations: 'set', 'del', 'touch'

function MemcachedTransaction(client, options) {
	this.queue = {};    // { key: operation }  operation to be executed per key
	this.cache = {};    // guaranteed to be the latest version of a value, if it's in here
	this.client = client;
	this.options = options || {};

	this.debug = null;

	if (this.options.debug) {
		if (this.options.debug === true) {
			this.debug = console.info;
		} else if (typeof this.options.debug === 'function') {
			this.debug = this.options.debug;
		}
	}
}

module.exports = MemcachedTransaction;

MemcachedTransaction.prototype.get = function (key, cb) {
	var cache = this.cache;

	var op = this.queue[key];
	if (op && op.type === 'del') {
		// this value is scheduled to be deleted, so consider it non-existing and return undefined

		if (cb) {
			cb(null, undefined);
		}
		return;
	}

	var value = cache[key];

	if (value !== undefined) {
		// there is a known uptodate value for this key, so return it instantly

		if (cb) {
			cb(null, value);
		}
		return;
	}

	// get the value from memcached and cache it

	if (this.debug) {
		this.debug('MemcachedTransaction: getting key', key);
	}

	this.client.get(key, function (error, value) {
		if (error) {
			if (cb) {
				cb(error);
			}
		} else {
			cache[key] = value;

			if (cb) {
				cb(null, value);
			}
		}
	});
};

MemcachedTransaction.prototype.getMulti = function (keys, cb) {
	var cache = this.cache;
	var value, result = {};
	var lookup = [];

	// for each key, check if we really need to fetch it, or if there is a known state

	for (var i = 0, len = keys.length; i < len; i++) {
		var key = keys[i];
		var op = this.queue[key];

		if (op && op.type === 'del') {
			// this value is scheduled to be deleted, so consider it non-existing and return undefined

			result[key] = undefined;
		} else {
			value = cache[key];

			if (value === undefined) {
				// prepare for fetching from memcached

				lookup.push(key);
			} else {
				// there is a known uptodate value for this key, so return it instantly

				result[key] = value;
			}
		}
	}

	// if there is nothing to look up from memcached, return all known results

	if (lookup.length === 0) {
		if (cb) {
			cb(null, result);
		}
		return;
	}

	// get the unknown values from memcached and cache them

	if (this.debug) {
		this.debug('MemcachedTransaction: getting keys', lookup);
	}

	this.client.getMulti(lookup, function (error, values) {
		if (error) {
			if (cb) {
				cb(error);
			}
			return;
		}

		for (var key in values) {
			var value = values[key];

			result[key] = value;
			cache[key] = value;
		}

		if (cb) {
			cb(null, result);
		}
	});
};

MemcachedTransaction.prototype.set = function (key, value, ttl, cb) {
	this.queue[key] = { type: 'set', key: key, value: value, ttl: ttl };
	this.cache[key] = value;

	if (cb) {
		cb();
	}
};

MemcachedTransaction.prototype.del = function (key, cb) {
	this.queue[key] = { type: 'del', key: key };
	delete this.cache[key];

	if (cb) {
		cb();
	}
};

MemcachedTransaction.prototype.touch = function (key, ttl, cb) {
	var op = this.queue[key];

	if (op) {
		switch (op.type) {
		case 'set':
			op.ttl = ttl;
			break;
		case 'del':
			// noop
			break;
		}
	} else {
		this.queue[key] = { type: 'touch', key: key, ttl: ttl };
	}

	if (cb) {
		cb();
	}
};

MemcachedTransaction.prototype._exec = function (op, cb) {
	if (this.options.simulate) {
		console.info('Executing', op);
		if (cb) {
			cb();
		}
		return;
	}

	switch (op.type) {
	case 'set':
		if (this.debug) {
			this.debug('MemcachedTransaction: setting key', op.key, 'to value', op.value, 'with TTL', op.ttl || 0);
		}

		this.client.set(op.key, op.value, op.ttl || 0, cb);
		break;
	case 'del':
		if (this.debug) {
			this.debug('MemcachedTransaction: deleting key', op.key);
		}

		this.client.del(op.key, cb);
		break;
	case 'touch':
		if (this.debug) {
			this.debug('MemcachedTransaction: touching key', op.key, 'with TTL', op.ttl || 0);
		}

		this.client.command(function touch(noreply) {
			return {
				command: ['touch', op.key, op.ttl || 0].join(' '),
				key: op.key,
				type: 'touch',
				callback: cb
			};
		});
		break;
	default:
		if (cb) {
			cb('Unknown operation type: ' + op.type);
		}
		break;
	}
};

MemcachedTransaction.prototype.commit = function (cb) {
	var ops = [];
	for (var key in this.queue) {
		ops.push(this.queue[key]);
	}

	var len = ops.length;

	// hot code paths: length 0 and 1

	if (len === 0) {
		return cb ? cb() : null;
	}

	if (len === 1) {
		return this._exec(ops[0], cb);
	}

	// more than 1 operation

	var that = this;

	async.forEachSeries(
		ops,
		function (op, callback) {
			that._exec(op, callback);
		},
		function (error) {
			if (cb) {
				cb(error);
			}
		}
	);
};

MemcachedTransaction.prototype.rollBack = function (cb) {
	this.queue = {};
	this.cache = {};

	if (cb) {
		cb();
	}
};

<|endoftext|>


<commit_before>function WindowPage() {
	var inst = window.Page;
	if (inst) {
		// because using instanceof requires reference to the same WindowPage
		if (inst.name == "WindowPage") return inst;
	}
	this.name = "WindowPage";
	this.window = window;

	this.reset();

	this.route = this.chainThenable.bind(this, "route");
	this.build = this.chainThenable.bind(this, "build");
	this.setup = this.chainThenable.bind(this, "setup");
	this.format = this.format.bind(this);

	var state = this.parse();
	state.prerendered = document.documentElement.hasAttribute("prerendered");
	if (state.prerendered) {
		state.imported = true;
		state.built = true;
	}
	this.run(state);
}

WindowPage.QueryString = require('query-string');

WindowPage.prototype.parse = function(str) {
	var dloc = this.window.document.location;
	var loc = new URL(str || "", dloc.toString());
	var obj = {
		pathname: loc.pathname,
		query: WindowPage.QueryString.parse(loc.search),
		hash: loc.hash
	};
	if (obj.hash && obj.hash[0] == "#") obj.hash = obj.hash.substring(1);
	if (loc.port != dloc.port || loc.hostname != dloc.hostname || loc.protocol != dloc.protocol) {
		obj.port = loc.port;
		obj.hostname = loc.hostname;
		obj.protocol = loc.protocol;
	}
	return obj;
};

WindowPage.prototype.format = function(obj) {
	var dloc = this.window.document.location;
	if (obj.path) {
		var parsedPath = this.parse(obj.path);
		obj.pathname = parsedPath.pathname;
		obj.query = parsedPath.query;
		obj.hash = parsedPath.hash;
		delete obj.path;
	}
	if (obj.protocol || obj.hostname || obj.port) {
		var loc = new URL("", dloc.toString());
		// copy only enumerable properties of a URL instance
		for (var k in loc) if (obj[k] !== undefined) loc[k] = obj[k];
		return loc.toString();
	}
	var str = obj.pathname || "";
	var qs = WindowPage.QueryString.stringify(obj.query || {});
	if (qs) str += '?' + qs;
	if (obj.hash) str += '#' + obj.hash;
	return str;
};

WindowPage.prototype.run = function(state) {
	this.format(state); // converts path if any
	var self = this;
	return this.waitReady().then(function() {
		if (!state.imported) {
			return self.runChain('route', state);
		}
	}).then(function() {
		if (state.imported || !state.document) return;
		self.reset();
		return self.importDocument(state.document).then(function() {
			state.imported = true;
		});
	}).then(function() {
		self.state = state; // this is the new current state
		if (state.setup) return;
		return self.waitUiReady(state).then(function() {
			return self.runChain('setup', state).then(function() {
				state.setup = true;
			});
		});
	}).then(function() {
		// do not run again if setup chain has not been run
		if (state.built && !state.setup) return;
		return self.runChain('build', state).then(function() {
			document.documentElement.setAttribute("prerendered", "true");
			state.built = true;
		});
	});
};

WindowPage.prototype.reset = function() {
	this.chains = {
		route: {thenables: []},
		build: {thenables: []},
		setup: {thenables: []}
	};
};

WindowPage.prototype.runChain = function(name, state) {
	var chain = this.chains[name];
	chain.promise = this.allFn(state, name, chain.thenables);
	return chain.promise.then(function() {
		return state;
	});
};

WindowPage.prototype.chainThenable = function(name, fn) {
	var chain = this.chains[name];
	chain.thenables.push(fn);
	if (chain.promise) {
		chain.promise = this.oneFn(chain.promise, name, fn);
	}
	return this;
};

WindowPage.prototype.catcher = function(name, err, fn) {
	console.error("Uncaught error during", name, err, fn);
};

WindowPage.prototype.oneFn = function(p, name, fn) {
	var catcher = this.catcher.bind(this);
	return p.then(function(state) {
		return Promise.resolve(state).then(fn).catch(function(err) {
			return catcher(name, err, fn);
		}).then(function() {
			return state;
		});
	});
};

WindowPage.prototype.allFn = function(state, name, list) {
	var p = Promise.resolve(state);
	var self = this;
	list.forEach(function(fn) {
		p = self.oneFn(p, name, fn);
	});
	return p;
};

WindowPage.prototype.waitUiReady = function(state) {
	if (document.visibilityState == "prerender") {
		var solve, p = new Promise(function(resolve) { solve = resolve; });
		function vizListener() {
			document.removeEventListener('visibilitychange', vizListener, false);
			solve();
		}
		document.addEventListener('visibilitychange', vizListener, false);
		return p.then(waitImports);
	} else {
		return waitImports();
	}
	function waitImports() {
		var imports = Array.from(document.querySelectorAll('link[rel="import"]'));
		var polyfill = window.HTMLImports;
		var whenReady = (function() {
			var promise;
			return function() {
				if (!promise) promise = new Promise(function(resolve) {
					polyfill.whenReady(function() {
						setTimeout(resolve);
					});
				});
				return promise;
			};
		})();

		return Promise.all(imports.map(function(link) {
			if (link.import && link.import.readyState == "complete") {
				// no need to wait, wether native or polyfill
				return Promise.resolve();
			}
			if (polyfill) {
				// link.onload cannot be trusted
				return whenReady();
			}

			return new Promise(function(resolve, reject) {
				function loadListener() {
					link.removeEventListener('load', loadListener);
					resolve();
				}
				function errorListener() {
					link.removeEventListener('error', errorListener);
					resolve();
				}
				link.addEventListener('load', loadListener);
				link.addEventListener('error', errorListener);
			});
		})).then(function() {
			return state;
		});
	}
};

WindowPage.prototype.waitReady = function() {
	if (this.docReady) return Promise.resolve();
	var solve;
	var p = new Promise(function(resolve) {
		solve = resolve;
	});
	// https://github.com/jquery/jquery/issues/2100
	if (document.readyState == "complete" ||
		(document.readyState != "loading" && !document.documentElement.doScroll)) {
		this.docReady = true;
		setTimeout(solve);
		return p;
	}
	var self = this;
	function listener() {
		document.removeEventListener('DOMContentLoaded', listener);
		window.removeEventListener('load', listener);
		if (self.docReady) return;
		self.docReady = true;
		solve();
	}
	document.addEventListener('DOMContentLoaded', listener);
	window.addEventListener('load', listener);
	return p;
};

WindowPage.prototype.importDocument = function(doc) {
	var scripts = Array.from(doc.querySelectorAll('script')).map(function(node) {
		if (node.type && node.type != "text/javascript") return Promise.resolve({});
		// make sure script is not loaded when inserted into document
		node.type = "text/plain";
		// fetch script content ourselves
		if (node.src) return GET(node.src).then(function(txt) {
			return {src: node.src, txt: txt, node: node};
		}).catch(function(err) {
			console.error("Error loading", node.src, err);
			return {};
		});
		else return Promise.resolve({
			src: "inline", txt: node.textContent, node: node
		});
	});

	var imports = Array.from(doc.querySelectorAll('link[rel="import"]'));
	var cursor;
	imports.forEach(function(link) {
		if (!cursor) {
			cursor = doc.createTextNode("");
			link.parentNode.insertBefore(cursor, link);
		}
		link.remove();
	});

	var root = document.documentElement;
	if (doc.attributes) for (var i=0; i < doc.attributes.length; i++) {
		root.setAttribute(doc.attributes[i].name, doc.attributes[i].value);
	}
	root.replaceChild(document.adoptNode(doc.head), document.head);
	root.replaceChild(document.adoptNode(doc.body), document.body);

	// execute all scripts in their original order as soon as they loaded
	return scripts.reduce(function(sequence, scriptPromise) {
		return sequence.then(function() {
			return scriptPromise;
		}).then(function(obj) {
			if (!obj.txt) return;
			var script = document.createElement("script");
			script.textContent = obj.txt;
			document.head.appendChild(script).remove();
			obj.node.type = "text/javascript";
		});
	}, Promise.resolve()).then(function() {
		imports.forEach(function(link) {
			cursor.parentNode.insertBefore(link, cursor);
		});
	});
};

WindowPage.prototype.push = function(state) {
	return this.historyMethod('push', state);
};

WindowPage.prototype.replace = function(state) {
	return this.historyMethod('replace', state);
};

WindowPage.prototype.historyMethod = function(method, state) {
	var copy = this.parse(typeof state == "string" ? state : this.format(state));
	['document', 'data', 'imported', 'built', 'setup'].forEach(function(k) {
		if (state[k]) copy[k] = state[k];
	});

	var supported = !!this.window.history;
	if (supported && !this.historyListener) {
		this.historyListener = function(e) {
			var state = this.stateFrom(e.state);
			if (!state) return;
			this.run(state);
		}.bind(this);
		this.window.addEventListener('popstate', this.historyListener);
		if (method == "push") {
			var to = this.stateTo(this.state);
			this.window.history.replaceState(to, document.title, to.href);
		}
	}
	return this.run(copy).then(function() {
		this.state = copy;
		var to = this.stateTo(copy);
		if (supported) {
			this.window.history[method + 'State'](to, document.title, to.href);
		}
	}.bind(this));
};

WindowPage.prototype.stateTo = function(state) {
	var to = {
		href: this.format(state)
	};
	if (state.document) to.html = state.document.documentElement.outerHTML;
	if (state.data) to.data = state.data;
	return to;
};

WindowPage.prototype.stateFrom = function(from) {
	if (!from || !from.href) return;
	var state = this.parse(from.href);
	if (from.data) state.data = from.data;
	if (from.html) {
		var doc = document.implementation.createHTMLDocument("");
		doc.open();
		doc.write(from.html);
		doc.close();
		state.document = doc;
	} else {
		state.imported = true;
	}
	return state;
};

window.Page = new WindowPage();

<commit_msg>Ability to queue runs and abort setup<commit_after>function WindowPage() {
	var inst = window.Page;
	if (inst) {
		// because using instanceof requires reference to the same WindowPage
		if (inst.name == "WindowPage") return inst;
	}
	this.name = "WindowPage";
	this.window = window;

	this.reset();

	this.route = this.chainThenable.bind(this, "route");
	this.build = this.chainThenable.bind(this, "build");
	this.setup = this.chainThenable.bind(this, "setup");
	this.format = this.format.bind(this);

	var state = this.parse();
	state.prerendered = document.documentElement.hasAttribute("prerendered");
	if (state.prerendered) {
		state.imported = true;
		state.built = true;
	}
	this.run(state);
}

WindowPage.QueryString = require('query-string');

WindowPage.prototype.parse = function(str) {
	var dloc = this.window.document.location;
	var loc = new URL(str || "", dloc.toString());
	var obj = {
		pathname: loc.pathname,
		query: WindowPage.QueryString.parse(loc.search),
		hash: loc.hash
	};
	if (obj.hash && obj.hash[0] == "#") obj.hash = obj.hash.substring(1);
	if (loc.port != dloc.port || loc.hostname != dloc.hostname || loc.protocol != dloc.protocol) {
		obj.port = loc.port;
		obj.hostname = loc.hostname;
		obj.protocol = loc.protocol;
	}
	return obj;
};

WindowPage.prototype.format = function(obj) {
	var dloc = this.window.document.location;
	if (obj.path) {
		var parsedPath = this.parse(obj.path);
		obj.pathname = parsedPath.pathname;
		obj.query = parsedPath.query;
		obj.hash = parsedPath.hash;
		delete obj.path;
	}
	if (obj.protocol || obj.hostname || obj.port) {
		var loc = new URL("", dloc.toString());
		// copy only enumerable properties of a URL instance
		for (var k in loc) if (obj[k] !== undefined) loc[k] = obj[k];
		return loc.toString();
	}
	var str = obj.pathname || "";
	var qs = WindowPage.QueryString.stringify(obj.query || {});
	if (qs) str += '?' + qs;
	if (obj.hash) str += '#' + obj.hash;
	return str;
};

WindowPage.prototype.run = function(state) {
	this.format(state); // converts path if any
	var self = this;
	if (this.queue) {
		if (this.state && this.state.built && !this.state.setup) {
			this.state.abort = true;
		} else {
			return this.queue.then(function() {
				return self.run(state);
			});
		}
	}
	this.queue = this.waitReady().then(function() {
		if (!state.imported) {
			return self.runChain('route', state);
		}
	}).then(function() {
		if (state.imported || !state.document) return;
		self.reset();
		return self.importDocument(state.document).then(function() {
			state.imported = true;
		});
	}).then(function() {
		self.state = state; // this is the new current state
		// run only once if setup was never run
		if (state.built && !state.setup) return;
		return self.runChain('build', state).then(function() {
			document.documentElement.setAttribute("prerendered", "true");
			state.built = true;
		});
	}).then(function() {
		if (state.setup) return;
		return self.waitUiReady(state).then(function() {
			if (state.abort) return Promise.reject("abort");
			return self.runChain('setup', state).then(function() {
				state.setup = true;
			});
		});
	}).then(function() {
		self.queue = null;
	}).catch(function(err) {
		delete state.abort;
		if (err != "abort") console.error(err);
	});
	return this.queue;
};

WindowPage.prototype.reset = function() {
	this.chains = {
		route: {thenables: []},
		build: {thenables: []},
		setup: {thenables: []}
	};
};

WindowPage.prototype.runChain = function(name, state) {
	var chain = this.chains[name];
	chain.promise = this.allFn(state, name, chain.thenables);
	return chain.promise.then(function() {
		return state;
	});
};

WindowPage.prototype.chainThenable = function(name, fn) {
	var chain = this.chains[name];
	chain.thenables.push(fn);
	if (chain.promise) {
		chain.promise = this.oneFn(chain.promise, name, fn);
	}
	return this;
};

WindowPage.prototype.catcher = function(name, err, fn) {
	console.error("Uncaught error during", name, err, fn);
};

WindowPage.prototype.oneFn = function(p, name, fn) {
	var catcher = this.catcher.bind(this);
	return p.then(function(state) {
		return Promise.resolve(state).then(fn).catch(function(err) {
			return catcher(name, err, fn);
		}).then(function() {
			return state;
		});
	});
};

WindowPage.prototype.allFn = function(state, name, list) {
	var p = Promise.resolve(state);
	var self = this;
	list.forEach(function(fn) {
		p = self.oneFn(p, name, fn);
	});
	return p;
};

WindowPage.prototype.waitUiReady = function(state) {
	if (document.visibilityState == "prerender") {
		var solve, p = new Promise(function(resolve) { solve = resolve; });
		function vizListener() {
			document.removeEventListener('visibilitychange', vizListener, false);
			solve();
		}
		document.addEventListener('visibilitychange', vizListener, false);
		return p.then(waitImports);
	} else {
		return waitImports();
	}
	function waitImports() {
		var imports = Array.from(document.querySelectorAll('link[rel="import"]'));
		var polyfill = window.HTMLImports;
		var whenReady = (function() {
			var promise;
			return function() {
				if (!promise) promise = new Promise(function(resolve) {
					polyfill.whenReady(function() {
						setTimeout(resolve);
					});
				});
				return promise;
			};
		})();

		return Promise.all(imports.map(function(link) {
			if (link.import && link.import.readyState == "complete") {
				// no need to wait, wether native or polyfill
				return Promise.resolve();
			}
			if (polyfill) {
				// link.onload cannot be trusted
				return whenReady();
			}

			return new Promise(function(resolve, reject) {
				function loadListener() {
					link.removeEventListener('load', loadListener);
					resolve();
				}
				function errorListener() {
					link.removeEventListener('error', errorListener);
					resolve();
				}
				link.addEventListener('load', loadListener);
				link.addEventListener('error', errorListener);
			});
		})).then(function() {
			return state;
		});
	}
};

WindowPage.prototype.waitReady = function() {
	if (this.docReady) return Promise.resolve();
	var solve;
	var p = new Promise(function(resolve) {
		solve = resolve;
	});
	// https://github.com/jquery/jquery/issues/2100
	if (document.readyState == "complete" ||
		(document.readyState != "loading" && !document.documentElement.doScroll)) {
		this.docReady = true;
		setTimeout(solve);
		return p;
	}
	var self = this;
	function listener() {
		document.removeEventListener('DOMContentLoaded', listener);
		window.removeEventListener('load', listener);
		if (self.docReady) return;
		self.docReady = true;
		solve();
	}
	document.addEventListener('DOMContentLoaded', listener);
	window.addEventListener('load', listener);
	return p;
};

WindowPage.prototype.importDocument = function(doc) {
	var scripts = Array.from(doc.querySelectorAll('script')).map(function(node) {
		if (node.type && node.type != "text/javascript") return Promise.resolve({});
		// make sure script is not loaded when inserted into document
		node.type = "text/plain";
		// fetch script content ourselves
		if (node.src) return GET(node.src).then(function(txt) {
			return {src: node.src, txt: txt, node: node};
		}).catch(function(err) {
			console.error("Error loading", node.src, err);
			return {};
		});
		else return Promise.resolve({
			src: "inline", txt: node.textContent, node: node
		});
	});

	var imports = Array.from(doc.querySelectorAll('link[rel="import"]'));
	var cursor;
	imports.forEach(function(link) {
		if (!cursor) {
			cursor = doc.createTextNode("");
			link.parentNode.insertBefore(cursor, link);
		}
		link.remove();
	});

	var root = document.documentElement;
	if (doc.attributes) for (var i=0; i < doc.attributes.length; i++) {
		root.setAttribute(doc.attributes[i].name, doc.attributes[i].value);
	}
	root.replaceChild(document.adoptNode(doc.head), document.head);
	root.replaceChild(document.adoptNode(doc.body), document.body);

	// execute all scripts in their original order as soon as they loaded
	return scripts.reduce(function(sequence, scriptPromise) {
		return sequence.then(function() {
			return scriptPromise;
		}).then(function(obj) {
			if (!obj.txt) return;
			var script = document.createElement("script");
			script.textContent = obj.txt;
			document.head.appendChild(script).remove();
			obj.node.type = "text/javascript";
		});
	}, Promise.resolve()).then(function() {
		imports.forEach(function(link) {
			cursor.parentNode.insertBefore(link, cursor);
		});
	});
};

WindowPage.prototype.push = function(state) {
	return this.historyMethod('push', state);
};

WindowPage.prototype.replace = function(state) {
	return this.historyMethod('replace', state);
};

WindowPage.prototype.historyMethod = function(method, state) {
	var copy = this.parse(typeof state == "string" ? state : this.format(state));
	['document', 'data', 'imported', 'built', 'setup'].forEach(function(k) {
		if (state[k]) copy[k] = state[k];
	});

	var supported = !!this.window.history;
	if (supported && !this.historyListener) {
		this.historyListener = function(e) {
			var state = this.stateFrom(e.state);
			if (!state) return;
			this.run(state);
		}.bind(this);
		this.window.addEventListener('popstate', this.historyListener);
		if (method == "push") {
			var to = this.stateTo(this.state);
			this.window.history.replaceState(to, document.title, to.href);
		}
	}
	return this.run(copy).then(function() {
		this.state = copy;
		var to = this.stateTo(copy);
		if (supported) {
			this.window.history[method + 'State'](to, document.title, to.href);
		}
	}.bind(this));
};

WindowPage.prototype.stateTo = function(state) {
	var to = {
		href: this.format(state)
	};
	if (state.document) to.html = state.document.documentElement.outerHTML;
	if (state.data) to.data = state.data;
	return to;
};

WindowPage.prototype.stateFrom = function(from) {
	if (!from || !from.href) return;
	var state = this.parse(from.href);
	if (from.data) state.data = from.data;
	if (from.html) {
		var doc = document.implementation.createHTMLDocument("");
		doc.open();
		doc.write(from.html);
		doc.close();
		state.document = doc;
	} else {
		state.imported = true;
	}
	return state;
};

window.Page = new WindowPage();

<|endoftext|>


<commit_before>var gutil = require('gulp-util'),
    through = require('through2'),
    exec = require('child_process').exec;

/**
 * Builds shell command for PHP Code Sniffer according to specified options.
 *
 * @param {Object} opt List of PHP Code Sniffer options.
 * @returns {String} Shell command with all needed flags.
 */
var buildCommand = function(opt) {
    var opt = opt || {};
    var command = opt.bin || 'phpcs';

    if (opt.hasOwnProperty('standard')) {
        command += ' --standard="' + opt.standard + '"';
    }

    if (opt.hasOwnProperty('severity')) {
        command += ' --severity=' + parseInt(opt.severity);
    }

    if (opt.hasOwnProperty('warningSeverity')) {
        command += ' --warning-severity=' + parseInt(opt.warningSeverity);
    }

    if (opt.hasOwnProperty('errorSeverity')) {
        command += ' --error-severity=' + parseInt(opt.errorSeverity);
    }

    if (opt.hasOwnProperty('encoding')) {
        command += ' --encoding="' + opt.encoding + '"';
    }

    if (opt.hasOwnProperty('showSniffCode')) {
        command += ' -s';
    }

    return command;
}

var phpcsPlugin = function(options) {
    return through.obj(function(file, enc, callback) {
        var stream = this;

        if (file.isNull()) {
            stream.push(file);
            callback();

            return;
        }

        if (file.isStream()) {
            stream.emit('error', new gutil.PluginError('gulp-phpcs', 'Streams are not supported'));
            callback();

            return;
        }

        // Run code sniffer
        var phpcs = exec(buildCommand(options), function(error, stdout, stderr) {
            var report = {
                error: false,
                output: ''
            }

            if (error) {
                // Something went wrong. Attache report to the file to allow
                // reporters do their job.
                report.error = error;
                report.output = stdout;
            }

            file.phpcsReport = report;
            stream.push(file);
            callback();
        });

        // Pass content of the file as STDIN to Code Sniffer
        phpcs.stdin.write(file.contents);
        phpcs.stdin.end();
    });
}

// Attach reporters loader to the plugin.
phpcsPlugin.reporter = require('./reporters');

module.exports = phpcsPlugin;
<commit_msg>Added support for STDIN file exclude patterns<commit_after>var gutil = require('gulp-util'),
    through = require('through2'),
    exec = require('child_process').exec;

/**
 * Builds shell command for PHP Code Sniffer according to specified options.
 *
 * @param {Object} opt List of PHP Code Sniffer options.
 * @returns {String} Shell command with all needed flags.
 */
var buildCommand = function(opt) {
    var opt = opt || {};
    var command = opt.bin || 'phpcs';

    if (opt.hasOwnProperty('standard')) {
        command += ' --standard="' + opt.standard + '"';
    }

    if (opt.hasOwnProperty('severity')) {
        command += ' --severity=' + parseInt(opt.severity);
    }

    if (opt.hasOwnProperty('warningSeverity')) {
        command += ' --warning-severity=' + parseInt(opt.warningSeverity);
    }

    if (opt.hasOwnProperty('errorSeverity')) {
        command += ' --error-severity=' + parseInt(opt.errorSeverity);
    }

    if (opt.hasOwnProperty('encoding')) {
        command += ' --encoding="' + opt.encoding + '"';
    }

    if (opt.hasOwnProperty('showSniffCode')) {
        command += ' -s';
    }

    return command;
}

var phpcsPlugin = function(options) {
    return through.obj(function(file, enc, callback) {
        var stream = this;

        if (file.isNull()) {
            stream.push(file);
            callback();

            return;
        }

        if (file.isStream()) {
            stream.emit('error', new gutil.PluginError('gulp-phpcs', 'Streams are not supported'));
            callback();

            return;
        }

        // Run code sniffer
        var phpcs = exec(buildCommand(options), function(error, stdout, stderr) {
            var report = {
                error: false,
                output: ''
            }

            if (error) {
                // Something went wrong. Attache report to the file to allow
                // reporters do their job.
                report.error = error;
                report.output = stdout;
            }

            file.phpcsReport = report;
            stream.push(file);
            callback();
        });

        // Pass the file name to Code Sniffer to support exclude patterns
        phpcs.stdin.write('phpcs_input_file: ' + file.path);

        // Pass content of the file as STDIN to Code Sniffer
        phpcs.stdin.write(file.contents);
        phpcs.stdin.end();
    });
}

// Attach reporters loader to the plugin.
phpcsPlugin.reporter = require('./reporters');

module.exports = phpcsPlugin;
<|endoftext|>


<commit_before>'use strict';
const Orchestrator = require('orchestrator');
const execa = require('execa');
const template = require('lodash.template');
const templateSettings = require('lodash.templatesettings');
const chalk = require('chalk');
const trimNewlines = require('trim-newlines');
const util = require('./lib/Util');
const prettyMs = require('pretty-ms');

class Gue extends Orchestrator {

  constructor(...args) {
    super(...args);
    this.exitCode = 0;
    this.options = {};
  }

  task(name, deps, func) {
    super.add(name, deps, func);
  }

  log(message, taskname, duration) {
    if (!taskname && !duration) {
      this._log('clean', message);
    } else {
      this._log('normal', message, taskname, duration);
    }
  }

  errLog(message, taskname, duration) {
    this._log('error', message, taskname, duration);
  }

  shell(command, values) {

    const lodashVars = (values && typeof values !== undefined) ? values :
      this.options;

    templateSettings.interpolate = /{{([\s\S]+?)}}/g;
    const compiledCmd = template(command);
    return execa.shell(compiledCmd(lodashVars), {env: {FORCE_COLOR: 'true'}})
      .then((result) => {
        result.stdout = trimNewlines(result.stdout);
        this.log(result.stdout);
        return result.stdout;
      })
      .catch((result) => {
        this.exitCode = 1;
        result.stderr = trimNewlines(result.stderr);
        this.log(trimNewlines(result.stdout));
        return Promise.reject(result);
      });
  }

  setOption(name, value) {
    this.options[name] = value;
  }

  taskList() {
    return Object.keys(this.tasks);
  }

  // Display the list of tasks to run
  // removes default if it exists
  runList() {
    let myArr = this.seq;
    let indexToRemove = myArr.indexOf('default');
    if (indexToRemove > 0) {
      myArr.splice(indexToRemove, 1);
    }
    return myArr;
  }

  _log(type, message, taskname, duration) {
    let composedMessage = '';

    if (!message || message === '') {
      return;
    }

    if (taskname && taskname !== undefined) {
      composedMessage += chalk.bold.green(
        util.leftPad('[' + taskname + '] ', 3 + util.maxLen(this.runList())));
    }

    if (type === 'error') {
      composedMessage += chalk.red(message);
    } else if (type === 'normal') {
      composedMessage += chalk.cyan(message);
    } else if (type === 'clean') {
      composedMessage += message;
    } else {
      composedMessage += message;
    }

    if (duration !== undefined && duration > 0.0) {
      composedMessage += ' ' + chalk.white(prettyMs(duration));
    }
    console.log(composedMessage);
  }

}
module.exports = new Gue();
<commit_msg>Create _shell and replace existing shell<commit_after>'use strict';
const Orchestrator = require('orchestrator');
const execa = require('execa');
const template = require('lodash.template');
const templateSettings = require('lodash.templatesettings');
const chalk = require('chalk');
const trimNewlines = require('trim-newlines');
const util = require('./lib/Util');
const prettyMs = require('pretty-ms');

class Gue extends Orchestrator {

  constructor(...args) {
    super(...args);
    this.exitCode = 0;
    this.options = {};
  }

  task(name, deps, func) {
    super.add(name, deps, func);
  }

  log(message, taskname, duration) {
    if (!taskname && !duration) {
      this._log('clean', message);
    } else {
      this._log('normal', message, taskname, duration);
    }
  }

  errLog(message, taskname, duration) {
    this._log('error', message, taskname, duration);
  }

  shell(command, value) {
    return this._shell(command, value);
  }

  _shell(command, values) {

    const lodashVars = (values && typeof values !== undefined) ? values :
      this.options;

    templateSettings.interpolate = /{{([\s\S]+?)}}/g;
    const compiledCmd = template(command);
    return execa.shell(compiledCmd(lodashVars), {env: {FORCE_COLOR: 'true'}})
      .then((result) => {
        result.stdout = trimNewlines(result.stdout);
        this.log(result.stdout);
        return result.stdout;
      })
      .catch((result) => {
        this.exitCode = 1;
        result.stderr = trimNewlines(result.stderr);
        this.log(trimNewlines(result.stdout));
        return Promise.reject(result);
      });
  }

  setOption(name, value) {
    this.options[name] = value;
  }

  taskList() {
    return Object.keys(this.tasks);
  }

  // Display the list of tasks to run
  // removes default if it exists
  runList() {
    let myArr = this.seq;
    let indexToRemove = myArr.indexOf('default');
    if (indexToRemove > 0) {
      myArr.splice(indexToRemove, 1);
    }
    return myArr;
  }

  _log(type, message, taskname, duration) {
    let composedMessage = '';

    if (!message || message === '') {
      return;
    }

    if (taskname && taskname !== undefined) {
      composedMessage += chalk.bold.green(
        util.leftPad('[' + taskname + '] ', 3 + util.maxLen(this.runList())));
    }

    if (type === 'error') {
      composedMessage += chalk.red(message);
    } else if (type === 'normal') {
      composedMessage += chalk.cyan(message);
    } else if (type === 'clean') {
      composedMessage += message;
    } else {
      composedMessage += message;
    }

    if (duration !== undefined && duration > 0.0) {
      composedMessage += ' ' + chalk.white(prettyMs(duration));
    }
    console.log(composedMessage);
  }

}
module.exports = new Gue();
<|endoftext|>


<commit_before>var _= require('underscore'),
    mw= require('./middleware'),
    ut= require('./util'),
    async= require('async'),
    request= require('request'),
    express= require('express'),
    EventEmitter= require('events').EventEmitter;

// @see http://www.cs.cornell.edu/~asdas/research/dsn02-swim.pdf
        http://www.cs.ucsb.edu/~ravenben/classes/papers/aodv-wmcsa99.pdf

module.exports= function (localNode,opts)
{
    opts= _.defaults(opts || {},{ base: '/swim',
                                  period_length: 3000,
                                  ping_timeout: 1000,
                                  failing_timeout: 9000,
                                  message_ttl: 10000,
                                  pingreq_nodes: 2,
                                  tune_gossip: 2,
                                  gossip_messages: 10 });

    var app= express(),
        swim= new EventEmitter(),
        T1= opts.period_length,                  // period length
        PING_TIMEOUT= opts.ping_timeout,         // timeout of a ping request
        FAILING_TIMEOUT= opts.failing_timeout,   // timeout of a suspected state before failing a node
        MESSAGE_TTL= opts.message_ttl,           // the time a message should be kept in the message queue
        k= opts.pingreq_nodes,                   // number of random nodes to select for a ping-req
        lambda= opts.tune_gossip,                // tune maximum message retransmission (keep it "small")
        MAX_MESSAGES= opts.gossip_messages;      // max piggybacked messages per request

    if (PING_TIMEOUT*3>T1) throw('quote: "which is chosen smaller than the protocol period...'+
                                 'Note that the protocol period has to be at least three times'+
                                 ' the round-trip estimate"');

    // todo:
    // adapt ping_timeout avg(response time)

    var periodSeq= 0,
        incSeq= 0,
        messageSeq= 0,
        membershipUpdates= [],
        servers= (function(servers){ servers[localNode]= { string: localNode, inc: 0 }; return servers; })({}),
        group= {
           join: function (server)
           {
              swim.emit('join',server);
              delete servers[server.string];
              servers[server.string]= server;
           },
           leave: function (string)
           {
              swim.emit('leave',servers[string]);
              delete servers[string];
           },
           suspect: function (string)
           {
              servers[string].suspect= true;
              swim.emit('suspect',servers[string]);
           },
           fail: function (string)
           {
              servers[string].failed= true;
              swim.emit('fail',servers[string]);
           },
           alive: function (string)
           {
              servers[string].suspected= servers[string].failed= false;
              swim.emit('alive',servers[string]);
           },
           inc: function (string)
           {
              return (servers[string]||{}).inc;
           },
           find: function (string)
           {
              return servers[string];
           },
           nodes: function ()
           {
              return _.pluck(_.filter(_.values(servers),function (s) { return !s.failed; }),'string');
           }
        },
        piggyback= function (seq,target)
        {
           var max= Math.round(Math.log(group.nodes().length)*lambda),
               messages= _.filter(membershipUpdates,function (upd)
                        {
                            return upd.message.source!=target
                                && upd.counter<max;
                        });

           messages= _.sortBy(messages,'counter').slice(0,MAX_MESSAGES);

           messages.forEach(function (upd)
           {
               ++upd.counter;

               if (upd.counter>=max)
                 upd.rmTimeout= setTimeout(function ()
                 {
                    ut.rm(membershipUpdates,upd);
                 },MESSAGE_TTL);
           });

           messages= _.pluck(messages,'message');

           return { seq: seq, sender: localNode, messages: messages };
        },
        processMessages= function (ack)
        {
           if (!ack) return;

           if (ack.messages)
           ack.messages.forEach(function (message)
           {
              if (message.source==localNode) return; // ignore my messages

              if (_.filter(membershipUpdates,
                    function (upd) { return upd.message.source==message.source
                                          &&upd.message.id==message.id }).length)
                return; // ignore known messages

              console.log('swim','receive',message);

              if (message.emit!==undefined)
                try
                {
                    swim.emit(message.type,message.emit);
                }
                catch (ex)
                {
                   console.log('swim','emit error',ex,ex.stack);
                }
              else
                receive[message.type](message.subject,message.inc);

              membershipUpdates.unshift({ message: message, counter: 0 });
           });
        },
        rnodes= function (n,suspect)
        {
           var nodes= {},
               others= _.without(group.nodes(),localNode,suspect);

           if (others.length<=n)
             return others;
           else
           while (_.keys(nodes).length < n)
           {
              var rnode= others[_.random(0,others.length-1)];
              nodes[rnode]= true;
           }

           return _.keys(nodes);
        },
        ping= function (node,seq,cb)
        {
               request.post({ timeout: PING_TIMEOUT,
                                  uri: 'http://'+node+opts.base+'/ping/'+seq,
                                 body: JSON.stringify(piggyback(seq,node)) },
               function (err, res, body)
               {
                   if (err)
                     cb(err);
                   else
                   if (res.statusCode!=200)
                     cb({ code: res.statusCode, message: processMessages(ut.json(body)) });
                   else
                     cb(null,processMessages(ut.json(body)));
               });
        },
        pingReq= function (node,target,seq,cb)
        {
               request.post({ timeout: PING_TIMEOUT,
                                  uri: 'http://'+node+opts.base+'/ping-req/'+target+'/'+seq,
                                 body: JSON.stringify(piggyback(seq,node)) },
               function (err, res, body)
               {
                   if (err)
                     cb(err);
                   else
                   if (res.statusCode!=200)
                     cb({ code: 200, message: res.statusCode < 300 ? processMessages(ut.json(body)) : body });
                   else
                     cb(null,processMessages(ut.json(body)));
               });
        },
        enqueueMessage= function (m)
        {
               var upd={ message: m, counter: 0 };

               membershipUpdates.unshift(upd);

               console.log('swim','send',upd.message);
        },
        sendMessage= function (type,subject)
        {
               var server= group.find(subject);

               if (subject!=localNode&&!server) return;

               enqueueMessage({ source: localNode, id: messageSeq++,
                                  type: type, subject: subject,
                                  inc: subject==localNode ? incSeq++ : server.inc });
        },
        receive= {
            join: function (subject,inc)
            {
                group.join({ string: subject, inc: inc });
            },
            leave: function (subject,inc)
            {
                if (inc>=ring.inc(subject));
                  group.leave(subject);
            },
            alive: function (subject,inc)
            {
                var server= group.find(subject);

                if (server&&inc>server.inc)
                {
                  server.inc= inc;
                  server.suspected= clearTimeout(server.suspected);
                }
                else
                  group.alive(subject);
            },
            fail: function (subject,inc) // (confirm)
            {
                if (localNode==subject)
                  sendMessage('alive',localNode);
                else
                  group.fail(subject);
            },
            suspect: function (subject,inc)
            {
                if (localNode==subject)
                    sendMessage('alive',localNode);
                else
                {
                    var server= group.find(subject);

                    if (!server||server.failed) return;

                    if (server.suspected)
                    {
                       if (inc>server.inc)
                       {
                           clearTimeout(server.suspected);
                           server.suspected= setTimeout(function ()
                           {
                               group.fail(subject);
                               sendMessage('fail',subject);
                           },FAILING_TIMEOUT);
                       }
                    }
                    else
                    if (inc>=server.inc)
                      server.suspected= setTimeout(function ()
                      {
                          group.fail(subject);
                          sendMessage('fail',subject);
                      },FAILING_TIMEOUT);
                }
            }
        },
        pingStack= [],
        periodInterval= setInterval(function ()
        {
           periodSeq++;

           var Mj= pingStack.pop();

           if (!Mj)
             Mj= (pingStack= _.shuffle(_.without(group.nodes(),localNode))).pop();

           console.log('swim','period',periodSeq,Mj);

           if (!Mj) return; // disabled we need more nodes on the cluster

           ping(Mj,periodSeq,function (err, ack)
           {
              if (err)
              {
                 var Mr= rnodes(k,Mj), errors= [];

                 async.forEach(Mr,
                 function (node,done)
                 {
                    pingReq(node,Mj,periodSeq,function (err, ack)
                    {
                       if (err)
                         errors.push({ node: node, err: err });

                       done(ack);
                    });
                 },
                 function (ack)
                 {
                    if (!ack)
                    {
                      receive.suspect(Mj,group.inc(Mj));
                      sendMessage('suspect',Mj);
                    }
                 });
              }
              else
                 console.log('swim','ping','OK',Mj,ack);
           });
        },T1);

    app.post('/ping/:seq', mw.json, function (req, res)
    {
        processMessages(req.json);

        res.send(piggyback(req.params.seq,req.json.sender));
    });

    app.post('/ping-req/:target/:seq', mw.json, function (req, res)
    {
        processMessages(req.json);

        ping(req.params.target,req.params.seq,function (err, ack)
        {
            if (err)
              res.status(504).send(piggyback(req.params.seq,req.json.sender));
            else
            {
              processMessages(ack);
              res.send(piggyback(ack.seq,req.json.sender));
            }
        });
    });

    // call on the joining node
    app.post('/join', mw.text, function (req, res)
    {
        group.join({ string: req.text, inc: 0 });
        sendMessage('join',localNode);
        res.end();
    });

    // call on the leaving node
    app.delete('/leave', function (req, res)
    {
        sendMessage('leave',localNode);
        res.end();
    });

    app.get('/nodes', function (req, res)
    {
        res.send(group.nodes());
    });

    swim.send= function (event,message)
    {
       enqueueMessage({ type: event, emit: message });
       swim.emit(event,message);
    };

    app.swim= swim;

    return app;
};
<commit_msg>initial<commit_after>var _= require('underscore'),
    mw= require('./middleware'),
    ut= require('./util'),
    async= require('async'),
    request= require('request'),
    express= require('express'),
    EventEmitter= require('events').EventEmitter;

// @see http://www.cs.cornell.edu/~asdas/research/dsn02-swim.pdf
        http://www.cs.ucsb.edu/~ravenben/classes/papers/aodv-wmcsa99.pdf

module.exports= function (localNode,opts)
{
    opts= _.defaults(opts || {},{ base: '/swim',
                                  period_length: 3000,
                                  ping_timeout: 1000,
                                  failing_timeout: 9000,
                                  message_ttl: 10000,
                                  pingreq_nodes: 2,
                                  tune_gossip: 2,
                                  gossip_messages: 10 });

    var app= express(),
        swim= new EventEmitter(),
        T1= opts.period_length,                  // period length
        PING_TIMEOUT= opts.ping_timeout,         // timeout of a ping request
        FAILING_TIMEOUT= opts.failing_timeout,   // timeout of a suspected state before failing a node
        MESSAGE_TTL= opts.message_ttl,           // the time a message should be kept in the message queue
        k= opts.pingreq_nodes,                   // number of random nodes to select for a ping-req
        lambda= opts.tune_gossip,                // tune maximum message retransmission (keep it "small")
        MAX_MESSAGES= opts.gossip_messages;      // max piggybacked messages per request

    if (PING_TIMEOUT*3>T1) throw('quote: "which is chosen smaller than the protocol period...'+
                                 'Note that the protocol period has to be at least three times'+
                                 ' the round-trip estimate"');

    // todo:
    // adapt ping_timeout avg(response time)

    var periodSeq= 0,
        incSeq= 0,
        messageSeq= 0,
        membershipUpdates= [],
        servers= (function(servers){ servers[localNode]= { string: localNode, inc: 0 }; return servers; })({}),
        group= {
           join: function (server)
           {
              swim.emit('join',server);
              delete servers[server.string];
              servers[server.string]= server;
           },
           leave: function (string)
           {
              swim.emit('leave',servers[string]);
              delete servers[string];
           },
           suspect: function (string)
           {
              servers[string].suspect= true;
              swim.emit('suspect',servers[string]);
           },
           fail: function (string)
           {
              servers[string].failed= true;
              swim.emit('fail',servers[string]);
           },
           alive: function (string)
           {
              servers[string].suspected= servers[string].failed= false;
              swim.emit('alive',servers[string]);
           },
           inc: function (string)
           {
              return (servers[string]||{}).inc;
           },
           find: function (string)
           {
              return servers[string];
           },
           nodes: function ()
           {
              return _.pluck(_.filter(_.values(servers),function (s) { return !s.failed; }),'string');
           }
        },
        piggyback= function (seq,target)
        {
           var max= Math.round(Math.log(group.nodes().length)*lambda),
               messages= _.filter(membershipUpdates,function (upd)
                        {
                            return upd.message.source!=target
                                && upd.counter<max;
                        });

           messages= _.sortBy(messages,'counter').slice(0,MAX_MESSAGES);

           messages.forEach(function (upd)
           {
               ++upd.counter;

               if (upd.counter>=max)
                 upd.rmTimeout= setTimeout(function ()
                 {
                    ut.rm(membershipUpdates,upd);
                 },MESSAGE_TTL);
           });

           messages= _.pluck(messages,'message');

           return { seq: seq, sender: localNode, messages: messages };
        },
        processMessages= function (ack)
        {
           if (!ack) return;

           if (ack.messages)
           ack.messages.forEach(function (message)
           {
              if (message.source==localNode) return; // ignore my messages

              if (_.filter(membershipUpdates,
                    function (upd) { return upd.message.source==message.source
                                          &&upd.message.id==message.id }).length)
                return; // ignore known messages

              console.log('swim','receive',message);

              if (message.emit!==undefined)
                try
                {
                    swim.emit(message.type,message.emit);
                }
                catch (ex)
                {
                   console.log('swim','emit error',ex,ex.stack);
                }
              else
                receive[message.type](message.subject,message.inc);

              membershipUpdates.unshift({ message: message, counter: 0 });
           });
        },
        rnodes= function (n,suspect)
        {
           var nodes= {},
               others= _.without(group.nodes(),localNode,suspect);

           if (others.length<=n)
             return others;
           else
           while (_.keys(nodes).length < n)
           {
              var rnode= others[_.random(0,others.length-1)];
              nodes[rnode]= true;
           }

           return _.keys(nodes);
        },
        ping= function (node,seq,cb)
        {
               request.post({ timeout: PING_TIMEOUT,
                                  uri: 'http://'+node+opts.base+'/ping/'+seq,
                                 body: JSON.stringify(piggyback(seq,node)) },
               function (err, res, body)
               {
                   if (err)
                     cb(err);
                   else
                   if (res.statusCode!=200)
                     cb({ code: res.statusCode, message: processMessages(ut.json(body)) });
                   else
                     cb(null,processMessages(ut.json(body)));
               });
        },
        pingReq= function (node,target,seq,cb)
        {
               request.post({ timeout: PING_TIMEOUT,
                                  uri: 'http://'+node+opts.base+'/ping-req/'+target+'/'+seq,
                                 body: JSON.stringify(piggyback(seq,node)) },
               function (err, res, body)
               {
                   if (err)
                     cb(err);
                   else
                   if (res.statusCode!=200)
                     cb({ code: 200, message: res.statusCode < 300 ? processMessages(ut.json(body)) : body });
                   else
                     cb(null,processMessages(ut.json(body)));
               });
        },
        enqueueMessage= function (m)
        {
               var upd={ message: m, counter: 0 };

               membershipUpdates.unshift(upd);

               console.log('swim','send',upd.message);
        },
        sendMessage= function (type,subject)
        {
               var server= group.find(subject);

               if (subject!=localNode&&!server) return;

               enqueueMessage({ source: localNode, id: messageSeq++,
                                  type: type, subject: subject,
                                  inc: subject==localNode ? incSeq++ : server.inc });
        },
        receive= {
            join: function (subject,inc)
            {
                group.join({ string: subject, inc: inc });
                sendMessage('alive',localNode);
            },
            leave: function (subject,inc)
            {
                if (inc>=ring.inc(subject));
                  group.leave(subject);
            },
            alive: function (subject,inc)
            {
                var server= group.find(subject);

                if (server)
                {
                    if (inc>server.inc)
                    {
                      server.inc= inc;
                      server.suspected= clearTimeout(server.suspected);
                    }
                    else
                      group.alive(subject);
                }
                else
                  group.join({ string: subject, inc: inc });
            },
            fail: function (subject,inc) // (confirm)
            {
                if (localNode==subject)
                  sendMessage('alive',localNode);
                else
                  group.fail(subject);
            },
            suspect: function (subject,inc)
            {
                if (localNode==subject)
                    sendMessage('alive',localNode);
                else
                {
                    var server= group.find(subject);

                    if (!server||server.failed) return;

                    if (server.suspected)
                    {
                       if (inc>server.inc)
                       {
                           clearTimeout(server.suspected);
                           server.suspected= setTimeout(function ()
                           {
                               group.fail(subject);
                               sendMessage('fail',subject);
                           },FAILING_TIMEOUT);
                       }
                    }
                    else
                    if (inc>=server.inc)
                      server.suspected= setTimeout(function ()
                      {
                          group.fail(subject);
                          sendMessage('fail',subject);
                      },FAILING_TIMEOUT);
                }
            }
        },
        pingStack= [],
        periodInterval= setInterval(function ()
        {
           periodSeq++;

           var Mj= pingStack.pop();

           if (!Mj)
             Mj= (pingStack= _.shuffle(_.without(group.nodes(),localNode))).pop();

           console.log('swim','period',periodSeq,Mj);

           if (!Mj) return; // disabled we need more nodes on the cluster

           ping(Mj,periodSeq,function (err, ack)
           {
              if (err)
              {
                 var Mr= rnodes(k,Mj), errors= [];

                 async.forEach(Mr,
                 function (node,done)
                 {
                    pingReq(node,Mj,periodSeq,function (err, ack)
                    {
                       if (err)
                         errors.push({ node: node, err: err });

                       done(ack);
                    });
                 },
                 function (ack)
                 {
                    if (!ack)
                    {
                      receive.suspect(Mj,group.inc(Mj));
                      sendMessage('suspect',Mj);
                    }
                 });
              }
              else
                 console.log('swim','ping','OK',Mj,ack);
           });
        },T1);

    app.post('/ping/:seq', mw.json, function (req, res)
    {
        processMessages(req.json);

        res.send(piggyback(req.params.seq,req.json.sender));
    });

    app.post('/ping-req/:target/:seq', mw.json, function (req, res)
    {
        processMessages(req.json);

        ping(req.params.target,req.params.seq,function (err, ack)
        {
            if (err)
              res.status(504).send(piggyback(req.params.seq,req.json.sender));
            else
            {
              processMessages(ack);
              res.send(piggyback(ack.seq,req.json.sender));
            }
        });
    });

    // call on the joining node
    app.post('/join', mw.text, function (req, res)
    {
        group.join({ string: req.text, inc: 0 });
        sendMessage('join',localNode);
        res.end();
    });

    // call on the leaving node
    app.delete('/leave', function (req, res)
    {
        sendMessage('leave',localNode);
        res.end();
    });

    app.get('/nodes', function (req, res)
    {
        res.send(group.nodes());
    });

    swim.send= function (event,message)
    {
       enqueueMessage({ type: event, emit: message });
       swim.emit(event,message);
    };

    app.swim= swim;

    return app;
};
<|endoftext|>


<commit_before>import {ContainerPlugin, Events} from 'clappr'
import get from 'lodash.get'

const REPORT_EVENT = 'clappr:stats:report'

export default class ClapprStats extends ContainerPlugin {
  get name() { return 'clappr_stats' }

  get _playbackName() {return this.container.playback.name}
  get _playbackType() {return this.container.getPlaybackType()}
  _now() {
    const hasPerformanceSupport = performance && typeof(performance.now) === 'function'
    return (hasPerformanceSupport)?performance.now():new Date()
  }
  _inc(counter) {this._metrics.counters[counter] += 1}
  _timerHasStarted(timer){return this[`_start${timer}`] !== undefined}
  _start(timer) {this[`_start${timer}`] = this._now()}
  _stop(timer) {this._metrics.timers[timer] += this._now() - this[`_start${timer}`]}
  _defaultReport(metrics) {console.log(metrics)} //eslint-disable-line no-console

  constructor(container) {
    super(container)
    this._runEach = get(container, 'options.clapprStats.runEach', 5000)
    this._onReport = get(container, 'options.clapprStats.onReport', this._defaultReport)
    this._uriToMeasureLatency = get(container, 'options.clapprStats.uriToMeasureLatency')

    this._metrics = {
      counters: {
        play: 0, pause: 0, error: 0, buffering: 0, decodedFrames: 0, droppedFrames: 0,
        fps: 0, changeLevel: 0, seek: 0, fullscreen: 0, dvrUsage: 0
      },
      timers: {
        startup: 0, watch: 0, pause: 0, buffering: 0, session: 0, latency: 0
      },
      extra: {
        playbackName: '', playbackType: '', bitratesHistory: [], bitrateMean: 0,
        bitrateVariance: 0, bitrateStandardDeviation: 0, bitrateMostUsed: 0,
        buffersize: 0
      }
    }

    this.on(REPORT_EVENT, this._onReport)
  }

  bindEvents() {
    this.listenTo(this.container, Events.CONTAINER_BITRATE, this.onBitrate)
    this.listenTo(this.container, Events.CONTAINER_STOP, this.stopReporting)
    this.listenTo(this.container, Events.CONTAINER_ENDED, this.stopReporting)
    this.listenToOnce(this.container.playback, Events.PLAYBACK_PLAY_INTENT, this.startTimers)
    this.listenToOnce(this.container, Events.CONTAINER_PLAY, this.onFirstPlaying)
    this.listenTo(this.container, Events.CONTAINER_PLAY, this.onPlay)
    this.listenTo(this.container, Events.CONTAINER_PAUSE, this.onPause)
    this.listenTo(this.container, Events.CONTAINER_TIMEUPDATE, (e) => e.current > 0 && this.onContainerUpdateWhilePlaying())
    this.listenToOnce(this.container, Events.CONTAINER_STATE_BUFFERING, this.onBuffering)
    this.listenTo(this.container, Events.CONTAINER_SEEK, () => this._inc('seek'))
    this.listenTo(this.container, Events.CONTAINER_ERROR, () => this._inc('error'))
    this.listenTo(this.container, Events.CONTAINER_FULLSCREEN, () => this._inc('fullscreen'))
    this.listenTo(this.container, Events.CONTAINER_PLAYBACKDVRSTATECHANGED, (dvrInUse) => {dvrInUse && this._inc('dvrUsage')})
    this.listenTo(this.container.playback, Events.PLAYBACK_PROGRESS, this.onProgress)
  }

  onBitrate(newBitrate) {
    var bitrate = parseInt(get(newBitrate, 'bitrate', 0), 10)
    var now = this._now()

    if (this._metrics.extra.bitratesHistory.length > 0) {
      var beforeLast = this._metrics.extra.bitratesHistory[this._metrics.extra.bitratesHistory.length-1]
      beforeLast.end = now
      beforeLast.time = now - beforeLast.start
    }

    this._metrics.extra.bitratesHistory.push({start: this._now(), bitrate: bitrate})

    this._inc('changeLevel')
  }

  stopReporting() {
    this._buildReport()
    clearInterval(this._intervalId)
  }

  startTimers() {
    this._intervalId = setInterval(this._buildReport.bind(this), this._runEach)
    this._start('session')
    this._start('startup')
  }

  onFirstPlaying() {
    this._start('watch')
    this._stop('startup')
  }

  playAfterPause() {
    this._stop('pause')
    this._start('watch')
  }

  onPlay() {
    this._inc('play')
  }

  onPause() {
    this._start('pause')
    this._inc('pause')
    this.listenToOnce(this.container, Events.CONTAINER_PLAY, this.playAfterPause)
  }

  onContainerUpdateWhilePlaying() {
    this._stop('watch')
    this._start('watch')
  }

  onBuffering() {
    this._inc('buffering')
    this._start('buffering')
    this.listenToOnce(this.container, Events.CONTAINER_STATE_BUFFERFULL, this.onBufferfull)
  }

  onBufferfull() {
    this._stop('buffering')
    this.listenToOnce(this.container, Events.CONTAINER_STATE_BUFFERING, this.onBuffering)
  }

  onProgress(progress) {
    this._metrics.extra.buffersize = progress.current * 1000
  }

  _buildReport() {
    this._stop('session')
    this._start('session')

    this._metrics.extra.playbackName = this._playbackName
    this._metrics.extra.playbackType = this._playbackType

    this._calculateBitrates()
    this._fetchFPS()
    this._measureLatency()

    this.trigger(REPORT_EVENT, JSON.parse(JSON.stringify(this._metrics)))
  }

  _fetchFPS() {
    // flashls ??? - hls.droppedFramesl hls.stream.bufferLength (seconds)
    // hls ??? (use the same?)
    const fetchFPS = {
      'html5_video': this._html5FetchFPS,
      'hls': this._html5FetchFPS,
      'dash_shaka_playback': this._html5FetchFPS
    }

    fetchFPS[this._playbackName] && fetchFPS[this._playbackName].call(this)
  }

  _calculateBitrates() {
    var bitrates = this._metrics.extra.bitratesHistory.map((x) => x.bitrate)

    this._metrics.extra.bitrateMean = bitrates.reduce((a,b) => a + b, 0) / bitrates.length

    this._metrics.extra.bitrateVariance = bitrates.map((n) => {
      return Math.pow(n - this._metrics.extra.bitrateMean, 2)
    }).reduce((a,b) => a + b, 0) / bitrates.length

    this._metrics.extra.bitrateStandardDeviation = Math.sqrt(this._metrics.extra.bitrateVariance)
    this._metrics.extra.bitrateMostUsed = this._metrics.extra.bitratesHistory.sort((a,b) => a.time < b.time)[0]
  }

  _html5FetchFPS() {
    const videoTag = this.container.playback.el
    const decodedFrames = videoTag.webkitDecodedFrameCount || videoTag.mozDecodedFrames || 0
    const droppedFrames = (videoTag.webkitDroppedFrameCount || (videoTag.mozParsedFrames - videoTag.mozDecodedFrames)) || 0
    const decodedFramesLastTime = decodedFrames - (this._lastDecodedFramesCount || 0)

    this._metrics.counters.decodedFrames = decodedFrames
    this._metrics.counters.droppedFrames = droppedFrames
    this._metrics.counters.fps = decodedFramesLastTime / (this._runEach / 1000)

    this._lastDecodedFramesCount = decodedFrames
  }

  // originally from https://www.smashingmagazine.com/2011/11/analyzing-network-characteristics-using-javascript-and-the-dom-part-1/
  _measureLatency() {
    if (this._uriToMeasureLatency) {
      var t=[], n=2, rtt
      var ld = () => {
        t.push(this._now())
        if(t.length > n)
          done()
        else {
          var img = new Image
          img.onload = ld
          img.src=this._uriToMeasureLatency + '?' + Math.random()
          + '=' + this._now()
        }
      }
      var done = () => {
        rtt=t[2]-t[1]
        this._metrics.timers.latency = rtt
      }
      ld()
    }
  }
}
<commit_msg>counts pause time as non watch time fixes #5<commit_after>import {ContainerPlugin, Events} from 'clappr'
import get from 'lodash.get'

const REPORT_EVENT = 'clappr:stats:report'

export default class ClapprStats extends ContainerPlugin {
  get name() { return 'clappr_stats' }

  get _playbackName() {return this.container.playback.name}
  get _playbackType() {return this.container.getPlaybackType()}
  _now() {
    const hasPerformanceSupport = performance && typeof(performance.now) === 'function'
    return (hasPerformanceSupport)?performance.now():new Date()
  }
  _inc(counter) {this._metrics.counters[counter] += 1}
  _timerHasStarted(timer){return this[`_start${timer}`] !== undefined}
  _start(timer) {this[`_start${timer}`] = this._now()}
  _stop(timer) {this._metrics.timers[timer] += this._now() - this[`_start${timer}`]}
  _defaultReport(metrics) {console.log(metrics)} //eslint-disable-line no-console

  constructor(container) {
    super(container)
    this._runEach = get(container, 'options.clapprStats.runEach', 5000)
    this._onReport = get(container, 'options.clapprStats.onReport', this._defaultReport)
    this._uriToMeasureLatency = get(container, 'options.clapprStats.uriToMeasureLatency')

    this._metrics = {
      counters: {
        play: 0, pause: 0, error: 0, buffering: 0, decodedFrames: 0, droppedFrames: 0,
        fps: 0, changeLevel: 0, seek: 0, fullscreen: 0, dvrUsage: 0
      },
      timers: {
        startup: 0, watch: 0, pause: 0, buffering: 0, session: 0, latency: 0
      },
      extra: {
        playbackName: '', playbackType: '', bitratesHistory: [], bitrateMean: 0,
        bitrateVariance: 0, bitrateStandardDeviation: 0, bitrateMostUsed: 0,
        buffersize: 0
      }
    }

    this.on(REPORT_EVENT, this._onReport)
  }

  bindEvents() {
    this.listenTo(this.container, Events.CONTAINER_BITRATE, this.onBitrate)
    this.listenTo(this.container, Events.CONTAINER_STOP, this.stopReporting)
    this.listenTo(this.container, Events.CONTAINER_ENDED, this.stopReporting)
    this.listenToOnce(this.container.playback, Events.PLAYBACK_PLAY_INTENT, this.startTimers)
    this.listenToOnce(this.container, Events.CONTAINER_PLAY, this.onFirstPlaying)
    this.listenTo(this.container, Events.CONTAINER_PLAY, this.onPlay)
    this.listenTo(this.container, Events.CONTAINER_PAUSE, this.onPause)
    this.listenTo(this.container, Events.CONTAINER_TIMEUPDATE, (e) => e.current > 0 && this.onContainerUpdateWhilePlaying())
    this.listenToOnce(this.container, Events.CONTAINER_STATE_BUFFERING, this.onBuffering)
    this.listenTo(this.container, Events.CONTAINER_SEEK, () => this._inc('seek'))
    this.listenTo(this.container, Events.CONTAINER_ERROR, () => this._inc('error'))
    this.listenTo(this.container, Events.CONTAINER_FULLSCREEN, () => this._inc('fullscreen'))
    this.listenTo(this.container, Events.CONTAINER_PLAYBACKDVRSTATECHANGED, (dvrInUse) => {dvrInUse && this._inc('dvrUsage')})
    this.listenTo(this.container.playback, Events.PLAYBACK_PROGRESS, this.onProgress)
  }

  onBitrate(newBitrate) {
    var bitrate = parseInt(get(newBitrate, 'bitrate', 0), 10)
    var now = this._now()

    if (this._metrics.extra.bitratesHistory.length > 0) {
      var beforeLast = this._metrics.extra.bitratesHistory[this._metrics.extra.bitratesHistory.length-1]
      beforeLast.end = now
      beforeLast.time = now - beforeLast.start
    }

    this._metrics.extra.bitratesHistory.push({start: this._now(), bitrate: bitrate})

    this._inc('changeLevel')
  }

  stopReporting() {
    this._buildReport()
    clearInterval(this._intervalId)
  }

  startTimers() {
    this._intervalId = setInterval(this._buildReport.bind(this), this._runEach)
    this._start('session')
    this._start('startup')
  }

  onFirstPlaying() {
    this._start('watch')
    this._stop('startup')
  }

  playAfterPause() {
    this._stop('pause')
    this._start('watch')
  }

  onPlay() {
    this._inc('play')
  }

  onPause() {
    this._stop('watch')
    this._start('pause')
    this._inc('pause')
    this.listenToOnce(this.container, Events.CONTAINER_PLAY, this.playAfterPause)
  }

  onContainerUpdateWhilePlaying() {
    this._stop('watch')
    this._start('watch')
  }

  onBuffering() {
    this._inc('buffering')
    this._start('buffering')
    this.listenToOnce(this.container, Events.CONTAINER_STATE_BUFFERFULL, this.onBufferfull)
  }

  onBufferfull() {
    this._stop('buffering')
    this.listenToOnce(this.container, Events.CONTAINER_STATE_BUFFERING, this.onBuffering)
  }

  onProgress(progress) {
    this._metrics.extra.buffersize = progress.current * 1000
  }

  _buildReport() {
    this._stop('session')
    this._start('session')

    this._metrics.extra.playbackName = this._playbackName
    this._metrics.extra.playbackType = this._playbackType

    this._calculateBitrates()
    this._fetchFPS()
    this._measureLatency()

    this.trigger(REPORT_EVENT, JSON.parse(JSON.stringify(this._metrics)))
  }

  _fetchFPS() {
    // flashls ??? - hls.droppedFramesl hls.stream.bufferLength (seconds)
    // hls ??? (use the same?)
    const fetchFPS = {
      'html5_video': this._html5FetchFPS,
      'hls': this._html5FetchFPS,
      'dash_shaka_playback': this._html5FetchFPS
    }

    fetchFPS[this._playbackName] && fetchFPS[this._playbackName].call(this)
  }

  _calculateBitrates() {
    var bitrates = this._metrics.extra.bitratesHistory.map((x) => x.bitrate)

    this._metrics.extra.bitrateMean = bitrates.reduce((a,b) => a + b, 0) / bitrates.length

    this._metrics.extra.bitrateVariance = bitrates.map((n) => {
      return Math.pow(n - this._metrics.extra.bitrateMean, 2)
    }).reduce((a,b) => a + b, 0) / bitrates.length

    this._metrics.extra.bitrateStandardDeviation = Math.sqrt(this._metrics.extra.bitrateVariance)
    this._metrics.extra.bitrateMostUsed = this._metrics.extra.bitratesHistory.sort((a,b) => a.time < b.time)[0]
  }

  _html5FetchFPS() {
    const videoTag = this.container.playback.el
    const decodedFrames = videoTag.webkitDecodedFrameCount || videoTag.mozDecodedFrames || 0
    const droppedFrames = (videoTag.webkitDroppedFrameCount || (videoTag.mozParsedFrames - videoTag.mozDecodedFrames)) || 0
    const decodedFramesLastTime = decodedFrames - (this._lastDecodedFramesCount || 0)

    this._metrics.counters.decodedFrames = decodedFrames
    this._metrics.counters.droppedFrames = droppedFrames
    this._metrics.counters.fps = decodedFramesLastTime / (this._runEach / 1000)

    this._lastDecodedFramesCount = decodedFrames
  }

  // originally from https://www.smashingmagazine.com/2011/11/analyzing-network-characteristics-using-javascript-and-the-dom-part-1/
  _measureLatency() {
    if (this._uriToMeasureLatency) {
      var t=[], n=2, rtt
      var ld = () => {
        t.push(this._now())
        if(t.length > n)
          done()
        else {
          var img = new Image
          img.onload = ld
          img.src=this._uriToMeasureLatency + '?' + Math.random()
          + '=' + this._now()
        }
      }
      var done = () => {
        rtt=t[2]-t[1]
        this._metrics.timers.latency = rtt
      }
      ld()
    }
  }
}
<|endoftext|>


<commit_before>'use strict';

/* eslint-env node */

module.exports = {
  name: 'ember-cli-eslint',

  // TODO: Disable this (or set it to return false) before committing
  isDevelopingAddon: function() {
    return false;
  },

  // instructs ember-cli-qunit and ember-cli-mocha to
  // disable their lintTree implementations (which use JSHint)
  isDefaultJSLinter: true,

  init() {
    this._super.init && this._super.init.apply(this, arguments);

    var VersionChecker = require('ember-cli-version-checker');
    var checker = new VersionChecker(this);

    if (checker.for('ember-cli-qunit', 'npm').satisfies('*')) {
      this._testGenerator = 'qunit';
    } else if (checker.for('ember-cli-mocha', 'npm').satisfies('*')) {
      this._testGenerator = 'mocha';
    }
  },

  included: function (app) {
    this._super.included.apply(this, arguments);
    this.options = app.options.eslint || {};
  },

  lintTree: function(type, tree) {
    var ui = this.ui;

    if (type === 'templates') {
      return undefined;
    }

    var ESLint = require('broccoli-lint-eslint');

    return ESLint.create(tree, {
      testGenerator: this.options.testGenerator || this._testGenerator,
      group: (this.options.group !== false) ? type : undefined,

      console: {
        log: function(message) {
          ui.writeLine(message);
        },

        error: function(message) {
          ui.writeLine(message, 'ERROR');
        }
      }
    });
  }
};
<commit_msg>Allow passthrough of rulesDir option to broccoli-lint-eslint<commit_after>'use strict';

/* eslint-env node */

module.exports = {
  name: 'ember-cli-eslint',

  // TODO: Disable this (or set it to return false) before committing
  isDevelopingAddon: function() {
    return false;
  },

  // instructs ember-cli-qunit and ember-cli-mocha to
  // disable their lintTree implementations (which use JSHint)
  isDefaultJSLinter: true,

  init() {
    this._super.init && this._super.init.apply(this, arguments);

    var VersionChecker = require('ember-cli-version-checker');
    var checker = new VersionChecker(this);

    if (checker.for('ember-cli-qunit', 'npm').satisfies('*')) {
      this._testGenerator = 'qunit';
    } else if (checker.for('ember-cli-mocha', 'npm').satisfies('*')) {
      this._testGenerator = 'mocha';
    }
  },

  included: function (app) {
    this._super.included.apply(this, arguments);
    this.options = app.options.eslint || {};
  },

  lintTree: function(type, tree) {
    var ui = this.ui;

    if (type === 'templates') {
      return undefined;
    }

    var ESLint = require('broccoli-lint-eslint');

    return ESLint.create(tree, {
      testGenerator: this.options.testGenerator || this._testGenerator,
      group: (this.options.group !== false) ? type : undefined,

      options: {
        rulesDir: this.options.rulesDir || 'eslint_rules'
      },

      console: {
        log: function(message) {
          ui.writeLine(message);
        },

        error: function(message) {
          ui.writeLine(message, 'ERROR');
        }
      }
    });
  }
};
<|endoftext|>