<commit_before>//.CommonJS
var CSSOM = {
	CSSStyleSheet: require("./CSSStyleSheet").CSSStyleSheet,
	CSSStyleRule: require("./CSSStyleRule").CSSStyleRule,
	CSSImportRule: require("./CSSImportRule").CSSImportRule,
	CSSMediaRule: require("./CSSMediaRule").CSSMediaRule,
	CSSStyleDeclaration: require('./CSSStyleDeclaration').CSSStyleDeclaration,
	CSSKeyframeRule: require('./CSSKeyframeRule').CSSKeyframeRule,
	CSSKeyframesRule: require('./CSSKeyframesRule').CSSKeyframesRule
};
///CommonJS

/**
 * @param {string} token
 */
CSSOM.parse = function parse(token) {

	var i = 0;

	/**
	  "before-selector" or
	  "selector" or
	  "atRule" or
	  "atBlock" or
	  "before-name" or
	  "name" or
	  "before-value" or
	  "value"
	*/
	var state = "before-selector";

	var index;
	var j = i;
	var buffer = "";

	var SIGNIFICANT_WHITESPACE = {
		"selector": true,
		"value": true,
		"atRule": true,
		"importRule-begin": true,
		"importRule": true,
		"atBlock": true
	};

	var styleSheet = new CSSOM.CSSStyleSheet;

	// @type CSSStyleSheet|CSSMediaRule
	var currentScope = styleSheet;

	var selector, name, value, priority="", styleRule, mediaRule, importRule, keyframesRule, keyframeRule;

	for (var character; character = token.charAt(i); i++) {

		switch (character) {

		case " ":
		case "\t":
		case "\r":
		case "\n":
		case "\f":
			if (SIGNIFICANT_WHITESPACE[state]) {
				buffer += character;
			}
			break;

		// String
		case '"':
			j = i + 1;
			index = token.indexOf('"', j) + 1;
			if (!index) {
				throw '" is missing';
			}
			buffer += token.slice(i, index);
			i = index - 1;
			switch (state) {
				case 'before-value':
					state = 'value';
					break;
				case 'importRule-begin':
					state = 'importRule';
					break;
			}
			break;

		case "'":
			j = i + 1;
			index = token.indexOf("'", j) + 1;
			if (!index) {
				throw "' is missing";
			}
			buffer += token.slice(i, index);
			i = index - 1;
			switch (state) {
				case 'before-value':
					state = 'value';
					break;
				case 'importRule-begin':
					state = 'importRule';
					break;
			}
			break;

		// Comment
		case "/":
			if (token.charAt(i + 1) === "*") {
				i += 2;
				index = token.indexOf("*/", i);
				if (index === -1) {
					throw new SyntaxError("Missing */");
				} else {
					i = index + 1;
				}
			} else {
				buffer += character;
			}
			if (state === "importRule-begin") {
				buffer += " ";
				state = "importRule";
			}
			break;

		// At-rule
		case "@":
			if (token.indexOf("@media", i) === i) {
				state = "atBlock";
				mediaRule = new CSSOM.CSSMediaRule;
				mediaRule.__starts = i;
				i += "media".length;
				buffer = "";
				break;
			} else if (token.indexOf("@import", i) === i) {
				state = "importRule-begin";
				i += "import".length;
				buffer += "@import";
				break;
			} else if (token.indexOf("@-webkit-keyframes", i) === i) {
				state = "keyframesRule-begin";
				keyframesRule = new CSSOM.CSSKeyframesRule;
				keyframesRule.__starts = i;
				i += "-webkit-keyframes".length;
				buffer = "";
				break;
			} else if (state === "selector") {
				state = "atRule";
			}
			buffer += character;
			break;

		case "{":
			if (state === "selector" || state === "atRule") {
				styleRule.selectorText = buffer.trim();
				styleRule.style.__starts = i;
				buffer = "";
				state = "before-name";
			} else if (state === "atBlock") {
				mediaRule.media.mediaText = buffer.trim();
				mediaRule.parentRule = currentScope;
				currentScope = mediaRule;
				buffer = "";
				state = "before-selector";
			} else if (state === "keyframesRule-begin") {
				keyframesRule.name = buffer.trim();
				keyframesRule.parentRule = currentScope;
				currentScope = keyframesRule;
				buffer = "";
				state = "keyframeRule-begin";
			} else if (state === "keyframeRule-begin") {
				styleRule = new CSSOM.CSSKeyframeRule;
				styleRule.keyText = buffer.trim();
				styleRule.__starts = i;
				buffer = "";
				state = "before-name";
			}
			break;

		case ":":
			if (state === "name") {
				name = buffer.trim();
				buffer = "";
				state = "before-value";
			} else {
				buffer += character;
			}
			break;

		case '(':
			if (state === 'value') {
				index = token.indexOf(')', i + 1);
				if (index === -1) {
					throw i + ': unclosed "("';
				}
				buffer += token.slice(i, index + 1);
				i = index;
			} else {
				buffer += character;
			}
			break;

		case "!":
			if (state === "value" && token.indexOf("!important", i) === i) {
				priority = "important";
				i += "important".length;
			} else {
				buffer += character;
			}
			break;

		case ";":
			switch (state) {
				case "value":
					styleRule.style.setProperty(name, buffer.trim(), priority);
					priority = "";
					buffer = "";
					state = "before-name";
					break;
				case "atRule":
					buffer = "";
					state = "before-selector";
					break;
				case "importRule":
					importRule = new CSSOM.CSSImportRule;
					importRule.parentRule = currentScope;
					importRule.cssText = buffer + character;
					currentScope.cssRules.push(importRule);
					buffer = "";
					state = "before-selector";
					break;
				default:
					buffer += character;
					break;
			}
			break;

		case "}":
			switch (state) {
				case "value":
					styleRule.style.setProperty(name, buffer.trim(), priority);
					priority = "";
				case "before-name":
				case "name":
					styleRule.__ends = i + 1;
					styleRule.parentRule = currentScope;
					currentScope.cssRules.push(styleRule);
					buffer = "";
					if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
						state = "keyframeRule-begin";
					} else {
						state = "before-selector";
					}
					break;
				case "keyframeRule-begin":
				case "before-selector":
				case "selector":
					// End of media rule.
					// Nesting rules aren't supported yet
					if (!currentScope.parentRule) {
						throw "unexpected }";
					}
					currentScope.__ends = i + 1;
					currentScope.parentRule.cssRules.push(currentScope);
					currentScope = currentScope.parentRule;
					buffer = "";
					state = "before-selector";
					break;
			}
			break;

		default:
			switch (state) {
				case "before-selector":
					state = "selector";
					styleRule = new CSSOM.CSSStyleRule;
					styleRule.__starts = i;
					break;
				case "before-name":
					state = "name";
					break;
				case "before-value":
					state = "value";
					break;
				case "importRule-begin":
					state = "importRule";
					break;
			}
			buffer += character;
			break;
		}
	}

	return styleSheet;
};

//.CommonJS
exports.parse = CSSOM.parse;
///CommonJS
<commit_msg>Remove useless variable<commit_after>//.CommonJS
var CSSOM = {
	CSSStyleSheet: require("./CSSStyleSheet").CSSStyleSheet,
	CSSStyleRule: require("./CSSStyleRule").CSSStyleRule,
	CSSImportRule: require("./CSSImportRule").CSSImportRule,
	CSSMediaRule: require("./CSSMediaRule").CSSMediaRule,
	CSSStyleDeclaration: require('./CSSStyleDeclaration').CSSStyleDeclaration,
	CSSKeyframeRule: require('./CSSKeyframeRule').CSSKeyframeRule,
	CSSKeyframesRule: require('./CSSKeyframesRule').CSSKeyframesRule
};
///CommonJS

/**
 * @param {string} token
 */
CSSOM.parse = function parse(token) {

	var i = 0;

	/**
	  "before-selector" or
	  "selector" or
	  "atRule" or
	  "atBlock" or
	  "before-name" or
	  "name" or
	  "before-value" or
	  "value"
	*/
	var state = "before-selector";

	var index;
	var buffer = "";

	var SIGNIFICANT_WHITESPACE = {
		"selector": true,
		"value": true,
		"atRule": true,
		"importRule-begin": true,
		"importRule": true,
		"atBlock": true
	};

	var styleSheet = new CSSOM.CSSStyleSheet;

	// @type CSSStyleSheet|CSSMediaRule
	var currentScope = styleSheet;

	var selector, name, value, priority="", styleRule, mediaRule, importRule, keyframesRule, keyframeRule;

	for (var character; character = token.charAt(i); i++) {

		switch (character) {

		case " ":
		case "\t":
		case "\r":
		case "\n":
		case "\f":
			if (SIGNIFICANT_WHITESPACE[state]) {
				buffer += character;
			}
			break;

		// String
		case '"':
			index = token.indexOf('"', i + 1) + 1;
			if (!index) {
				throw '" is missing';
			}
			buffer += token.slice(i, index);
			i = index - 1;
			switch (state) {
				case 'before-value':
					state = 'value';
					break;
				case 'importRule-begin':
					state = 'importRule';
					break;
			}
			break;

		case "'":
			index = token.indexOf("'", i + 1) + 1;
			if (!index) {
				throw "' is missing";
			}
			buffer += token.slice(i, index);
			i = index - 1;
			switch (state) {
				case 'before-value':
					state = 'value';
					break;
				case 'importRule-begin':
					state = 'importRule';
					break;
			}
			break;

		// Comment
		case "/":
			if (token.charAt(i + 1) === "*") {
				i += 2;
				index = token.indexOf("*/", i);
				if (index === -1) {
					throw new SyntaxError("Missing */");
				} else {
					i = index + 1;
				}
			} else {
				buffer += character;
			}
			if (state === "importRule-begin") {
				buffer += " ";
				state = "importRule";
			}
			break;

		// At-rule
		case "@":
			if (token.indexOf("@media", i) === i) {
				state = "atBlock";
				mediaRule = new CSSOM.CSSMediaRule;
				mediaRule.__starts = i;
				i += "media".length;
				buffer = "";
				break;
			} else if (token.indexOf("@import", i) === i) {
				state = "importRule-begin";
				i += "import".length;
				buffer += "@import";
				break;
			} else if (token.indexOf("@-webkit-keyframes", i) === i) {
				state = "keyframesRule-begin";
				keyframesRule = new CSSOM.CSSKeyframesRule;
				keyframesRule.__starts = i;
				i += "-webkit-keyframes".length;
				buffer = "";
				break;
			} else if (state === "selector") {
				state = "atRule";
			}
			buffer += character;
			break;

		case "{":
			if (state === "selector" || state === "atRule") {
				styleRule.selectorText = buffer.trim();
				styleRule.style.__starts = i;
				buffer = "";
				state = "before-name";
			} else if (state === "atBlock") {
				mediaRule.media.mediaText = buffer.trim();
				mediaRule.parentRule = currentScope;
				currentScope = mediaRule;
				buffer = "";
				state = "before-selector";
			} else if (state === "keyframesRule-begin") {
				keyframesRule.name = buffer.trim();
				keyframesRule.parentRule = currentScope;
				currentScope = keyframesRule;
				buffer = "";
				state = "keyframeRule-begin";
			} else if (state === "keyframeRule-begin") {
				styleRule = new CSSOM.CSSKeyframeRule;
				styleRule.keyText = buffer.trim();
				styleRule.__starts = i;
				buffer = "";
				state = "before-name";
			}
			break;

		case ":":
			if (state === "name") {
				name = buffer.trim();
				buffer = "";
				state = "before-value";
			} else {
				buffer += character;
			}
			break;

		case '(':
			if (state === 'value') {
				index = token.indexOf(')', i + 1);
				if (index === -1) {
					throw i + ': unclosed "("';
				}
				buffer += token.slice(i, index + 1);
				i = index;
			} else {
				buffer += character;
			}
			break;

		case "!":
			if (state === "value" && token.indexOf("!important", i) === i) {
				priority = "important";
				i += "important".length;
			} else {
				buffer += character;
			}
			break;

		case ";":
			switch (state) {
				case "value":
					styleRule.style.setProperty(name, buffer.trim(), priority);
					priority = "";
					buffer = "";
					state = "before-name";
					break;
				case "atRule":
					buffer = "";
					state = "before-selector";
					break;
				case "importRule":
					importRule = new CSSOM.CSSImportRule;
					importRule.parentRule = currentScope;
					importRule.cssText = buffer + character;
					currentScope.cssRules.push(importRule);
					buffer = "";
					state = "before-selector";
					break;
				default:
					buffer += character;
					break;
			}
			break;

		case "}":
			switch (state) {
				case "value":
					styleRule.style.setProperty(name, buffer.trim(), priority);
					priority = "";
				case "before-name":
				case "name":
					styleRule.__ends = i + 1;
					styleRule.parentRule = currentScope;
					currentScope.cssRules.push(styleRule);
					buffer = "";
					if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
						state = "keyframeRule-begin";
					} else {
						state = "before-selector";
					}
					break;
				case "keyframeRule-begin":
				case "before-selector":
				case "selector":
					// End of media rule.
					// Nesting rules aren't supported yet
					if (!currentScope.parentRule) {
						throw "unexpected }";
					}
					currentScope.__ends = i + 1;
					currentScope.parentRule.cssRules.push(currentScope);
					currentScope = currentScope.parentRule;
					buffer = "";
					state = "before-selector";
					break;
			}
			break;

		default:
			switch (state) {
				case "before-selector":
					state = "selector";
					styleRule = new CSSOM.CSSStyleRule;
					styleRule.__starts = i;
					break;
				case "before-name":
					state = "name";
					break;
				case "before-value":
					state = "value";
					break;
				case "importRule-begin":
					state = "importRule";
					break;
			}
			buffer += character;
			break;
		}
	}

	return styleSheet;
};

//.CommonJS
exports.parse = CSSOM.parse;
///CommonJS
<|endoftext|>


<commit_before>'use strict';

const jsdom = require('jsdom');
const _ = require('lodash');
const moment = require('moment');
const ms = require('ms');

// Strip out stiff that we'll never need but retain some stuff for easier debugging
const overwrite = (obj, omitKeys, newObj) => Object.assign({}, _.omit(obj, omitKeys), newObj);
const omitKeys = {
  event: ['links', 'season', 'weather'],
  competitor: ['records', 'leaders', 'statistics', 'uid', 'score', 'id', 'type', 'linescores'],
  team: ['links', 'venue', 'logo', 'location', 'color', 'uid', 'id', 'isActive'],
  competition: [
    'status', 'venue', 'odds', 'broadcasts', 'attendance', 'neutralSite',
    'uid', 'conferenceCompetition', 'geoBroadcasts', 'timeValid', 'id', 'headlines'
  ]
};
const parseRaw = (e) => overwrite(e, omitKeys.event, {
  competitions: e.competitions.map((c) => overwrite(c, omitKeys.competition, {
    competitors: c.competitors.map((co) => overwrite(co, omitKeys.competitor, {
      team: _.omit(co.team, omitKeys.team)
    }))
  }))
});

// Get the rank (seed), whether the team was the winner, and a plethora of names
const parseCompetitor = (c) => {
  const shortName = _.unescape(c.team.shortDisplayName);
  const longName = _.unescape(c.team.displayName);
  const teamName = _.unescape(c.team.name);
  const abbrv = _.unescape(c.team.abbreviation);
  const name = _.unescape(c.team.displayName.replace(c.team.name, '').trim());

  return {
    rank: c.curatedRank.current,
    winner: !!c.winner,
    abbrv, // NAU
    shortName, // N Arizona
    longName, // Northern Arizona Lumberjacks
    teamName, // Lumberjacks
    name, // Northern Arizona
    names: [shortName, longName, teamName, name, abbrv].filter(Boolean)
  };
};

// A negative time is possible depending on if a event should've started or
// if the fetch returned stale data. For this case, the time should always be
// 0 meaning a refetch will happen as soon as possible
const parseTime = (time) => {
  const parsed = parseInt(time, 10);
  if (_.isNaN(parsed)) return 0;
  if (parsed <= 0) return 0;
  return parsed || 0;
};

// Try to get the most intelligent guess for how much time remains in the event
// Final period and period length are options that can be passed in to better estimate
const parseTimeRemaining = (s) => {
  const period = parseInt(s.period, 10);
  const finalPeriod = parseInt(s.finalPeriod, 10);

  const clock = ms(`${s.clock}s`);
  const periodLength = typeof s.periodLength === 'string' ? ms(s.periodLength) : s.periodLength;

  // If no options were supplied then return the time left on the clock
  // which could be too early, but its better to err on checking again quickly than not
  if (!finalPeriod || !periodLength || period >= finalPeriod) {
    return clock;
  }

  // Otherwise multiply by the number of periods left
  return (finalPeriod - period) * periodLength + clock;
};

// The status of the event. Whether its complete and the state [pre, post, in]
// and some info about the time depending if its 'pre' or 'in'
const parseStatus = (s) => {
  const state = s.type.state;
  const completed = !!s.type.completed;
  const __now = s.__now;

  return {
    state,
    completed,
    timeRemaining: state === 'in' ? parseTime(parseTimeRemaining(s)) : null,
    timeUntil: state === 'pre' ? parseTime(moment(s.date).diff(moment(__now || void 0))) : null
  };
};

// Look at the notes for the event and determine what region it is for
// This will need to be updated based on the event probably
// TODO: figure out how to update based on event
const parseRegion = (notes) => {
  const matchNote = (regex) => {
    const matched = (notes || []).find((note) => {
      const headline = note.headline;
      const matches = headline && headline.match(regex);
      return matches && matches[1];
    });

    return matched ? matched.headline.match(regex)[1].toLowerCase() : null;
  };

  const regionNote = matchNote(/\b([\w\s]*) region\b/i);
  const finalFourNote = matchNote(/\b(final four)\b/i);
  const championshipNote = matchNote(/\b(national championship)\b/i);

  if (regionNote) return regionNote;
  if (finalFourNote) return finalFourNote;
  if (championshipNote) return championshipNote;

  return null;
};

const parseEvent = (e, options) => {
  const c = e.competitions[0];
  const status = e.status;
  const date = e.date;
  const notes = c.notes;
  const home = _.find(c.competitors, {homeAway: 'home'});
  const away = _.find(c.competitors, {homeAway: 'away'});

  return {
    uid: e.uid,
    id: e.id,
    region: parseRegion(notes),
    status: parseStatus(Object.assign({date}, options, status)),
    home: parseCompetitor(home),
    away: parseCompetitor(away),
    __raw: parseRaw(e)
  };
};

const parseScores = (url, options, cb) => {
  if (!cb) {
    cb = options;
    options = {};
  }

  return jsdom.env(url, {
    // Only process inline JS
    features: {
      FetchExternalResources: false,
      ProcessExternalResources: ['script'],
      SkipExternalResources: true
    },
    done: (err, window) => {
      if (err) return cb(err, null);

      let events;

      try {
        events = window.espn.scoreboardData.events;
        if (!events || !Array.isArray(events)) {
          throw new Error('Events could not be found or is not an array');
        }
      }
      catch (parseErr) {
        return cb(parseErr, null);
      }

      window.close();
      return cb(null, events.map((e) => parseEvent(e, options)));
    }
  });
};

module.exports = parseScores;
<commit_msg>only log games with a winner<commit_after>'use strict';

const jsdom = require('jsdom');
const _ = require('lodash');
const moment = require('moment');
const ms = require('ms');

const STATES = {
  PRE: 'pre',
  POST: 'post',
  IN: 'in'
};

// Strip out stiff that we'll never need but retain some stuff for easier debugging
const overwrite = (obj, omitKeys, newObj) => Object.assign({}, _.omit(obj, omitKeys), newObj);
const omitKeys = {
  event: ['links', 'season', 'weather'],
  competitor: ['records', 'leaders', 'statistics', 'uid', 'score', 'id', 'type', 'linescores'],
  team: ['links', 'venue', 'logo', 'location', 'color', 'uid', 'id', 'isActive'],
  competition: [
    'status', 'venue', 'odds', 'broadcasts', 'attendance', 'neutralSite',
    'uid', 'conferenceCompetition', 'geoBroadcasts', 'timeValid', 'id', 'headlines'
  ]
};
const parseRaw = (e) => overwrite(e, omitKeys.event, {
  competitions: e.competitions.map((c) => overwrite(c, omitKeys.competition, {
    competitors: c.competitors.map((co) => overwrite(co, omitKeys.competitor, {
      team: _.omit(co.team, omitKeys.team)
    }))
  }))
});

// Get the rank (seed), whether the team was the winner, and a plethora of names
const parseCompetitor = (c) => {
  const shortName = _.unescape(c.team.shortDisplayName);
  const longName = _.unescape(c.team.displayName);
  const teamName = _.unescape(c.team.name);
  const abbrv = _.unescape(c.team.abbreviation);
  const name = _.unescape(c.team.displayName.replace(c.team.name, '').trim());

  return {
    rank: c.curatedRank.current,
    winner: c.winner === true,
    abbrv, // NAU
    shortName, // N Arizona
    longName, // Northern Arizona Lumberjacks
    teamName, // Lumberjacks
    name, // Northern Arizona
    names: [shortName, longName, teamName, name, abbrv].filter(Boolean)
  };
};

// A negative time is possible depending on if a event should've started or
// if the fetch returned stale data. For this case, the time should always be
// 0 meaning a refetch will happen as soon as possible
const parseTime = (time) => {
  const parsed = parseInt(time, 10);
  if (_.isNaN(parsed)) return 0;
  if (parsed <= 0) return 0;
  return parsed || 0;
};

// Try to get the most intelligent guess for how much time remains in the event
// Final period and period length are options that can be passed in to better estimate
const parseTimeRemaining = (s) => {
  const period = parseInt(s.period, 10);
  const finalPeriod = parseInt(s.finalPeriod, 10);

  const clock = ms(`${s.clock}s`);
  const periodLength = typeof s.periodLength === 'string' ? ms(s.periodLength) : s.periodLength;

  // If no options were supplied then return the time left on the clock
  // which could be too early, but its better to err on checking again quickly than not
  if (!finalPeriod || !periodLength || period >= finalPeriod) {
    return clock;
  }

  // Otherwise multiply by the number of periods left
  return (finalPeriod - period) * periodLength + clock;
};

// The status of the event. Whether its complete and the state [pre, post, in]
// and some info about the time depending if its 'pre' or 'in'
const parseStatus = (s) => {
  const hasWinner = s.hasWinner;
  let state = s.type.state;
  let completed = !!s.type.completed;
  const __now = s.__now;

  // Fix for what might've led to an incorrect result being saved.
  // If a game is complete but a winner has not been updated yet then reset the game
  // to not being in progress, since it should only ever log games with a winner
  if (state === STATES.POST && completed && !hasWinner) {
    state = STATES.IN;
    completed = false;
  }

  return {
    state,
    completed,
    timeRemaining: state === 'in' ? parseTime(parseTimeRemaining(s)) : null,
    timeUntil: state === 'pre' ? parseTime(moment(s.date).diff(moment(__now || void 0))) : null
  };
};

// Look at the notes for the event and determine what region it is for
// This will need to be updated based on the event probably
// TODO: figure out how to update based on event
const parseRegion = (notes) => {
  const matchNote = (regex) => {
    const matched = (notes || []).find((note) => {
      const headline = note.headline;
      const matches = headline && headline.match(regex);
      return matches && matches[1];
    });

    return matched ? matched.headline.match(regex)[1].toLowerCase() : null;
  };

  const regionNote = matchNote(/\b([\w\s]*) region\b/i);
  const finalFourNote = matchNote(/\b(final four)\b/i);
  const championshipNote = matchNote(/\b(national championship)\b/i);

  if (regionNote) return regionNote;
  if (finalFourNote) return finalFourNote;
  if (championshipNote) return championshipNote;

  return null;
};

const parseEvent = (e, options) => {
  const c = e.competitions[0];
  const status = e.status;
  const date = e.date;
  const notes = c.notes;
  const home = _.find(c.competitors, {homeAway: 'home'});
  const away = _.find(c.competitors, {homeAway: 'away'});
  const hasWinner = home.winner === true || away.winner === true;

  return {
    uid: e.uid,
    id: e.id,
    region: parseRegion(notes),
    status: parseStatus(Object.assign({date, hasWinner}, options, status)),
    home: parseCompetitor(home),
    away: parseCompetitor(away),
    __raw: parseRaw(e)
  };
};

const parseScores = (url, options, cb) => {
  if (!cb) {
    cb = options;
    options = {};
  }

  return jsdom.env(url, {
    // Only process inline JS
    features: {
      FetchExternalResources: false,
      ProcessExternalResources: ['script'],
      SkipExternalResources: true
    },
    done: (err, window) => {
      if (err) return cb(err, null);

      let events;

      try {
        events = window.espn.scoreboardData.events;
        if (!events || !Array.isArray(events)) {
          throw new Error('Events could not be found or is not an array');
        }
      }
      catch (parseErr) {
        return cb(parseErr, null);
      }

      window.close();
      return cb(null, events.map((e) => parseEvent(e, options)));
    }
  });
};

module.exports = parseScores;
<|endoftext|>


<commit_before>const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

exports.setupCSS = function setupCSS(path) {
  return {
    module: {
      loaders: [
        {
          test: /\.css$/,
          loaders: ['style', 'css'],
          include: path
        }
      ]
    }
  };
}

exports.devServer = function devServer(options) {
  return {
    devServer: {
      // Enable history API fallback so HTML5 History API based
      // routing works. This is a good default that will come
      // in handy in more complicated setups.
      historyApiFallback: true,
      hot: true,
      inline: true,
      progress: true,

      // Display only errors to reduce the amount of output.
      stats: 'errors-only',

      // Parse host and port from env to allow customization.
      //
      // If you use Vagrant or Cloud9, set
      // host: process.env.HOST || '0.0.0.0';
      //
      // 0.0.0.0 is available to all network devices
      // unlike default localhost
      host: options.host,
      port: options.port
    },
  };
}

exports.dontParse = function dontParse(options) {
  const alias = {};
  alias[options.name] = options.path;

  return {
    module: {
      noParse: [
        options.path
      ]
    },
    resolve: {
      alias: alias
    }
  };
}

exports.setEnvironment = function setEnvironment(options) {
  const env = {};
  env[options.key] = JSON.stringify(options.value);

  return {
    plugins: [
      new webpack.DefinePlugin(env)
    ]
  };
}

exports.extractCSS = function extractCSS(path) {
  return {
    module: {
      loaders: [
        // Extract CSS during build
        {
          test: /\.css$/,
          loader: ExtractTextPlugin.extract('style', 'css'),
          include: path
        }
      ]
    },
    plugins: [
      // Output extracted CSS to a file
      new ExtractTextPlugin('[name].[chunkhash].css')
    ]
  };
}

exports.extractBundle = function extractBundle(options) {
  const entry = {};
  entry[options.name] = options.entries;

  return {
    // Define vendor entry point needed for splitting.
    entry: entry,
    plugins: [
      // Extract bundle and manifest files. Manifest is
      // needed for reliable caching.
      new webpack.optimize.CommonsChunkPlugin({
        names: [options.name, 'manifest']
      })
    ]
  };
}

exports.clean = function clean(path) {
  return {
    plugins: [
      new CleanWebpackPlugin([path])
    ]
  };
}

exports.minify = function minify() {
  return {
    plugins: [
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false
        }
      })
    ]
  };
}
<commit_msg>extractBundle - Extract only modules belonging to the passed chunk<commit_after>const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

exports.setupCSS = function setupCSS(path) {
  return {
    module: {
      loaders: [
        {
          test: /\.css$/,
          loaders: ['style', 'css'],
          include: path
        }
      ]
    }
  };
}

exports.devServer = function devServer(options) {
  return {
    devServer: {
      // Enable history API fallback so HTML5 History API based
      // routing works. This is a good default that will come
      // in handy in more complicated setups.
      historyApiFallback: true,
      hot: true,
      inline: true,
      progress: true,

      // Display only errors to reduce the amount of output.
      stats: 'errors-only',

      // Parse host and port from env to allow customization.
      //
      // If you use Vagrant or Cloud9, set
      // host: process.env.HOST || '0.0.0.0';
      //
      // 0.0.0.0 is available to all network devices
      // unlike default localhost
      host: options.host,
      port: options.port
    },
  };
}

exports.dontParse = function dontParse(options) {
  const alias = {};
  alias[options.name] = options.path;

  return {
    module: {
      noParse: [
        options.path
      ]
    },
    resolve: {
      alias: alias
    }
  };
}

exports.setEnvironment = function setEnvironment(options) {
  const env = {};
  env[options.key] = JSON.stringify(options.value);

  return {
    plugins: [
      new webpack.DefinePlugin(env)
    ]
  };
}

exports.extractCSS = function extractCSS(path) {
  return {
    module: {
      loaders: [
        // Extract CSS during build
        {
          test: /\.css$/,
          loader: ExtractTextPlugin.extract('style', 'css'),
          include: path
        }
      ]
    },
    plugins: [
      // Output extracted CSS to a file
      new ExtractTextPlugin('[name].[chunkhash].css')
    ]
  };
}

exports.extractBundle = function extractBundle(options) {
  const entry = {};
  entry[options.name] = options.entries;

  return {
    // Define vendor entry point needed for splitting.
    entry: entry,
    plugins: [
      // Extract bundle and manifest files. Manifest is
      // needed for reliable caching.
      new webpack.optimize.CommonsChunkPlugin({
        names: [options.name, 'manifest'],

        // options.name modules only
        minChunks: Infinity
      })
    ]
  };
}

exports.clean = function clean(path) {
  return {
    plugins: [
      new CleanWebpackPlugin([path])
    ]
  };
}

exports.minify = function minify() {
  return {
    plugins: [
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false
        }
      })
    ]
  };
}
<|endoftext|>


<commit_before>// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// copies of recommendations and selected stops, stored as arrays
// for synchronous updating
let recs = [];
let stops = [];

if (document.readyState === 'loading') {  // Loading hasn't finished yet
  document.addEventListener('DOMContentLoaded', load);
} else {  // `DOMContentLoaded` has already fired
  load();
}

/** Used to restore stops and recommendations upon load or refresh */
function load(){
  getRecsOnload();
  getStopsOnload();
}

function initMap() {
  let directionsService = new google.maps.DirectionsService();
  let directionsRenderer = new google.maps.DirectionsRenderer();
  let start = new google.maps.LatLng(37.7699298, -122.4469157);
  let end = new google.maps.LatLng(37.7683909618184, -122.51089453697205);
  let mapOptions = {
    zoom: 14,
    center: start
  }
  let map = new google.maps.Map(document.getElementById('map'), mapOptions);
  directionsRenderer.setMap(map);
  document.getElementById("route").addEventListener("click", function() {
    calcRoute(directionsService, directionsRenderer, start, end);
  });
}

/** Displays route overtop the map */
function calcRoute(directionsService, directionsRenderer, start, end) {
  let request = {
    origin:  start,
    destination: end,
    travelMode: 'DRIVING'
  };
  directionsService.route(request, function(response, status) {
    if (status == 'OK') {
      directionsRenderer.setDirections(response);
    } else {
      window.alert("Could not calculate route due to: " + status);
    }
  });
}

/** Clear the stops panel in the html */
function clearStops(){
  const stopList = document.getElementById('stop-list');
  if(stopList != null){
    stopList.innerText = ""; // clear list
  }
}

/** Render stop list  */
function renderStops(stop, fromDatastore){
  const stopList = document.getElementById('stop-list');
  // add to stop array in the js
  if(fromDatastore){
    stops.push(stop);
  }
  let btn = document.createElement('button');
  btn.id = stop.id;
  btn.innerText = stop.placename;
  btn.className =  "btn rec-btn";
  btn.addEventListener("click", function() {
    deleteFromStops(stop);
  });
  stopList.appendChild(btn);
}

/** Get the new list of stops from datastore onload */
function getStopsOnload(){
  clearStops();
  fetch('/api/stop')
  .then(response => response.json())
  .then((stopsResponse) => {
    stopsResponse.forEach((stop)=>{
      renderStops(stop, true);
    });
  })
}

/** Get the new list of stops locally */
function getStopsList(){
  clearStops();
  // re-render list synchronously
  stops.forEach((stop)=>{
    renderStops(stop, false);
  })
}

/** Add stop locally and to datastore */
function addToStops(stop){
  deleteFromRecs(stop);
  // add to stops array locally in js
  stops.push(stop);
  getStopsList();

  // add to datastore
  const params = new URLSearchParams();
  params.append("text", stop.placename);
  params.append("action", "add");
  fetch('/api/stop', {method: 'POST', body: params});
}

/** Delete stop locally and from datastore*/
function deleteFromStops(stop){
  addToRecs(stop);
  console.log("delete " + stop.placename);
  // delete from stops array locally in js
  stops = stops.filter(function(stopObj){
    return stopObj.placename != stop.placename;
  })
  getStopsList();

  // delete from datastore
  const params = new URLSearchParams();
  params.append("text", stop.placename);
  params.append("action", "remove");
  fetch('/api/stop', {method: 'POST', body: params});
}

/** Clear the recommendations panel in the html */
function clearRecs(){
  const recList = document.getElementById('rec-list');
  if(recList != null){
    recList.innerText = ""; // clear list
  }
}

/** Render recommendations list */
function renderRecs(rec, fromDatastore){
  const recsList = document.getElementById('rec-list');
  // populate local js array initially
  if(fromDatastore){
    recs.push(rec);
  }
  var btn = document.createElement('button');
  btn.id = rec.id;
  btn.innerText = rec.placename;
  btn.className =  "btn rec-btn";
  btn.addEventListener("click", function() {
    addToStops(rec);
  });
  recsList.appendChild(btn);
}

/** Get the new list of recommendations from servlet onload */
function getRecsOnload() {
  clearRecs();
  fetch('/api/recs')
  .then(response => response.json())
  .then((recommendations) => {
    recommendations.forEach((rec)=>{
      renderRecs(rec, true);
    })
  })
}

/** Get the new list of recommendations locally */
function getRecsList(){
  clearRecs();
  // re-render list synchronously
  recs.forEach((rec)=>{
      renderRecs(rec, false);
  })
}

/** Add recommendation locally*/
function addToRecs(rec){
  recs.push(rec);
  getRecsList();
  const params = new URLSearchParams();
  params.append("text", rec.placename);
}

/** Delete recommendation locally */
function deleteFromRecs(rec){
  recs = recs.filter(function(r){
    return r.placename != rec.placename;
  })
  getRecsList();
}

/* exported initMap */
/* global google */

<commit_msg>address comments regarding duplicate code, etc.<commit_after>// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// copies of recommendations and selected stops, stored as arrays
// for synchronous updating
let recs = [];
let stops = [];

if (document.readyState === 'loading') {  // Loading hasn't finished yet
  document.addEventListener('DOMContentLoaded', loadData);
} else {  // `DOMContentLoaded` has already fired
  loadData();
}

/** Used to restore stops and recommendations upon load or refresh */
function loadData(){
  getRecsOnload();
  getStopsOnload();
}

function initMap() {
  let directionsService = new google.maps.DirectionsService();
  let directionsRenderer = new google.maps.DirectionsRenderer();
  let start = new google.maps.LatLng(37.7699298, -122.4469157);
  let end = new google.maps.LatLng(37.7683909618184, -122.51089453697205);
  let mapOptions = {
    zoom: 14,
    center: start
  }
  let map = new google.maps.Map(document.getElementById('map'), mapOptions);
  directionsRenderer.setMap(map);
  document.getElementById("route").addEventListener("click", function() {
    calcRoute(directionsService, directionsRenderer, start, end);
  });
}

/**
 * Displays route containing waypoints overtop the map.
 * TODO: Add in code from routeDir to change route in real-time
 * based on what's in the schedule panel
 * @param {DirectionsService} directionsService object that communicates with the GMaps API service
 * @param {DirectionsRenderer} directionsRenderer object that renders display results on the map
 * @param {String} start starting point of route
 * @param {String} end ending point of route. TODO: In the final implementation, just have
 * start as start and end are the same
 */
function calcRoute(directionsService, directionsRenderer, start, end) {
  let request = {
    origin:  start,
    destination: end,
    travelMode: 'DRIVING'
  };
  directionsService.route(request, function(response, status) {
    if (status == 'OK') {
      directionsRenderer.setDirections(response);
    } else {
      window.alert("Could not calculate route due to: " + status);
    }
  });
}

/** Clear the stops panel in the html */
function clearStops(){
  const stopList = document.getElementById('stop-list');
  if(stopList != null){
    stopList.innerText = ""; // clear list
  }
}

/** Render stop list  */
function renderStop(stop){
  const stopList = document.getElementById('stop-list');
  // add to stop array in the js

  let btn = document.createElement('button');
  btn.id = stop.id;
  btn.innerText = stop.placename;
  btn.className =  "btn rec-btn";
  btn.addEventListener("click", function() {
    deleteFromStops(stop);
  });
  stopList.appendChild(btn);
}

/** Get the new list of stops from datastore onload */
function getStopsOnload(){
  console.log("getstopsOnloda");
  clearStops();
  fetch('/api/stop')
  .then(response => response.json())
  .then((stopsResponse) => {
    stopsResponse.forEach((stop)=>{
      stops.push(stop);
      // renderStops(stop);
    });
    renderStopsList();
  });

}

/** Get the new list of stops locally */
function renderStopsList(){
  console.log("renderstoplist");
  clearStops();
  // re-render list synchronously
  stops.forEach((stop)=>{
    renderStop(stop);
  })
}

/** Add stop locally and to datastore */
function addToStops(stop){
  deleteFromRecs(stop);
  // add to stops array locally in js
  stops.push(stop);
  renderStopsList();

  // add to datastore
  const params = new URLSearchParams();
  params.append("text", stop.placename);
  params.append("action", "add");
  fetch('/api/stop', {method: 'POST', body: params});
}

/** Delete stop locally and from datastore*/
function deleteFromStops(stop){
  addToRecs(stop);
  console.log("delete " + stop.placename);
  // delete from stops array locally in js
  stops = stops.filter(function(stopObj){
    return stopObj.placename != stop.placename;
  })
  renderStopsList();

  // delete from datastore
  const params = new URLSearchParams();
  params.append("text", stop.placename);
  params.append("action", "remove");
  fetch('/api/stop', {method: 'POST', body: params});
}

/** Clear the recommendations panel in the html */
function clearRecs(){
  const recList = document.getElementById('rec-list');
  if(recList != null){
    recList.innerText = ""; // clear list
  }
}

/** Render recommendations list */
function renderRec(rec){
  console.log("renderrec");
  const recsList = document.getElementById('rec-list');
  let btn = document.createElement('button');
  btn.id = rec.id;
  btn.innerText = rec.placename;
  btn.className =  "btn rec-btn";
  btn.addEventListener("click", function() {
    addToStops(rec);
  });
  recsList.appendChild(btn);
}

/** Get the new list of recommendations from servlet onload */
function getRecsOnload() {
  console.log("getRecsOnloda");
  clearRecs();
  fetch('/api/recs')
  .then(response => response.json())
  .then((recommendations) => {
    recommendations.forEach((rec)=>{
      recs.push(rec);
      console.log("pushed")
      // renderRecs(rec);
    })
    renderRecsList();
  })

}

/** Get the new list of recommendations locally */
function renderRecsList(){
  console.log("renderrecslist");
  clearRecs();
  // re-render list synchronously
  console.log(recs);
  recs.forEach((rec)=>{
    console.log("in foreach");
    renderRec(rec);
  })
}

/** Add recommendation locally*/
function addToRecs(rec){
  recs.push(rec);
  renderRecsList();
}

/** Delete recommendation locally */
function deleteFromRecs(rec){
  recs = recs.filter(function(r){
    return r.placename != rec.placename;
  })
  renderRecsList();
}

/* exported initMap */
/* global google */

<|endoftext|>


<commit_before>'use strict';

var path = require('path');
var util = require('./util');
var vinylFS = require('vinyl-fs');
var tornado = require('tornado-repl');

var Stack = tornado.Stack;
var Runtime = tornado.Runtime;

// String to function mapping failed, handle it
//
Stack.prototype.onHandleNotFound = function(next){
  var path = next.match || next.path;
  var message = 'no task found for `'+path+'`.\n'+
    'Set one with `task(\''+path+'\', [Function])`';

  if(this.runtime.repl){
    util.log('Warning:', message);
    this.log = false;
    return next();
  }

  throw new Error(message);
};

// Errors anywhere in the stack
//
Stack.prototype.onHandleError = function(error, next){
  if(!(error instanceof Error)){ return next(); }
  var file = error.stack.match(/\(?(\S+:\d+:\d+)\)?/);
  file = path.resolve(process.cwd(), file[1]);

  util.log('Error in \'%s\' after %s at %s\n%s',
    util.color.cyan(next.match),
    util.color.time(next.time),
    util.color.file(file),
    error.stack
  );

  next.error = error;
  if(!this.runtime.repl){
    process.exit(1);
  }
  next();
};

// Make a logger that looks like gulp's
//
Stack.prototype.onHandle = function(next){
  if(next.error || !this.log || /^-/.test(next.match)){
    return ; // ^ skip logging for errors, silent or flags
  }

  if(!this.configFile){
    this.configFile = process.argv[1];
    this.runtime.set({configFile: this.configFile});
    util.log('Using', util.color.file(process.argv[1]));
  }

  var host = '';
  if(this.host){
    host = '(' + util.color.green(this.host.path) + ') ';
  }

  if(!this.time){
    util.log('%sStarted \'%s\' in %s',
      host,
      util.color.cyan(this.path),
      util.color.bold(this.wait ? 'series' : 'parallel')
    );
    this.time = process.hrtime();
  } else if(this.queue && next.match && next.time){
    util.log('%sFinished \'%s\' after %s',
      host, util.color.cyan(next.match), util.color.time(next.time)
    );
  }

  if(!next.time){ next.time = process.hrtime(); }
  if(this.queue || this.time.end){ return ; }

  this.time.end = util.color.time(this.time);
  util.log('%sFinished \'%s\' after %s',
    host, util.color.cyan(this.path), this.time.end
  );
};

/*
# runtime.src
```js
function src(string|array glob[, object opt])
```
Same as [vinylFs.src][m-vinly-fs-src]
*/

Runtime.prototype.src = vinylFS.src;

/*
# runtime.dest
```js
function dest(string|function folder[, object opt])
```
Same as [vinylFs.dest][m-vinly-fs-dest]
*/
Runtime.prototype.dest = vinylFS.dest;

/*
# runtime.task
```js
function task(string name[, string|array dep, function handle])
```
_arguments_
 - are same as [gulp.task][m-gulp-task] aside of `dep`
 - `dep` type array or string, tasks to run before this one
 if given as a string they must be space separated

_returns_ differences with [gulp.task][m-gulp-task]
 - the function of the task `name`, if it was set before
 - this otherwise (just as [gulp.task][m-gulp-task])
*/

Runtime.prototype.task = function(name, dep, handle){
  var handleType = util.type(handle || dep);

  if(typeof name !== 'string'){
    throw new TypeError(
      'task(string name, [string|array dep, function handle])\n'+
      '- Tasks require a string `name`'
    );
  } else if(!handleType.match(/function|array|string/)){
    throw new TypeError(
      'task(string name, [string|array dep, function handle])\n' +
      'New tasks need at lest 2 arguments\n'+
      ' - deps: dependencies to run before this one'+
      ' - handle: function that runs the task\n'+
      '\n'
    );
  }

  handle = handleType.function || function(next){ next(); };
  // TODO: checking handle completion should be more robust
  //
  if(!handle.length && !/return/.test(handle.toString())){
    name = util.color.cyan(name);
    var error = new Error(
      'task `'+name+'` doesn\'t give a hint of its completion\n'+
      'Meaning: the task function does not return nor uses a callback\n'+
      'Solution:'+
      ' - return a stream, promise or observable\n'+
      ' - use the callback that is passed as first argument\n--'
    );
    var file = error.stack.match(/([^()]+:\d+:\d+)/g)[1];
    error.stack = error.stack.replace(file, util.color.file(file)) + '\n';
    throw error;
  }

  var depType = util.type(dep);
  if(!depType.match(/array|string/) || !dep.length){
    // no dependencies
    return this.set(name, handle);
  }

  dep = depType.string || dep.join(' ');
  return this.set(name, {
    dep: dep.trim().split(/[ ]+/),
    handle: this.stack(dep, handle, {wait: true})
  });
};

/*
# runtime.watch
```js
function watch(string|array glob[, string|array|object opt, function cb])
```

Same as [gulp.watch][m-gulp-watch] (run tasks when files change).
Additional features:
  - Files can be reloaded on the `require.cache`
  - Tasks can be run in series directly if so specified

_arguments_ differences with [gulp.watch][m-gulp-watch]
 - `tasks` can be a string, space separated tasks will be dispatched as written
 - when `opt` is an object, it can have non mandatory properties below
  - `opt.wait` type boolean, if `opt.tasks` should run in series or not
  - `opt.tasks` type array|string, tasks to run after change and before reload
  - `opt.reload` type boolean, wether or not to reload a file

_defaults_
 - `opt.wait` to `true`
 - `opt.reload` to `undefined`

_returns_
 - a watcher, same as [gulp.watch][m-gulp-watch]

> Notes:
- The callback arguments are:
  - a gaze event
  - the `opt` argument given
- Only files with a registered `require.extension` will be reloaded
- When a file is deleted on disk, the reload will be skipped but the
callback is still invoked

```javascript
var gulp = require('gulp-runtime').create();

var watcher;
watcher = gulp.watch('app/*.js', 'minify browserSync');

gulp.watch('app/*.*', {
  wait: true,  // run tasks after change in series
  tasks: 'sass css jsx browserSync',
  reload: true // will only reload app/*.js files (server, db, etc)
}, function(ev, opt){
  var reloaded = opt.reload && ev.type !== 'delete';
  console.log('%s was %s', ev.path, ev.type, reloaded ? 'and reloaded' : '');
});
```
**/
Runtime.prototype.watch = function(glob, opt, fn){
  var cb = util.type(fn || opt).function;
  var o = cb !== fn && util.type(fn).plainObject
    || cb !== opt && util.type(opt).plainObject
    || {tasks: opt, wait: true};

  var reload = o.reload && function reloadHandle(ev){
    if(ev.type !== 'delete' && require.extension[path.extname(ev.path)]){
      delete require.cache[ev.path];
      require(ev.path);
    }
    if(cb){ cb(ev, o); }
  };

  var tasks = util.type(o && o.tasks || o);
  if(tasks.string || tasks.array){
    tasks = tasks.string || tasks.array.join(' ');
    return vinylFS.watch(glob, null, reload
      ? this.stack(tasks, reload, o)
      : this.stack(tasks, o)
    );
  }

  return vinylFS.watch(glob, o, reload || function onChange(ev){
    if(cb){ cb(ev, o); }
  });
};

// export back tornado
//
exports = module.exports = tornado;
<commit_msg>preventing circular dependencies in tasks<commit_after>'use strict';

var path = require('path');
var util = require('./util');
var vinylFS = require('vinyl-fs');
var tornado = require('tornado-repl');

var Stack = tornado.Stack;
var Runtime = tornado.Runtime;

// String to function mapping failed, handle it
//
Stack.prototype.onHandleNotFound = function(next){
  var path = next.match || next.path;
  var message = 'no task found for `'+path+'`\n'+
    'Set one with `task(\''+path+'\', [Function])`';

  if(this.runtime.repl){
    util.log('Warning:', message);
    this.log = false;
    return next();
  }

  throw new Error(message);
};

// Errors anywhere in the stack
//
Stack.prototype.onHandleError = function(error, next){
  if(!(error instanceof Error)){ return next(); }
  var file = error.stack.match(/\(?(\S+:\d+:\d+)\)?/);
  file = path.resolve(process.cwd(), file[1]);

  util.log('Error in \'%s\' after %s at %s\n%s',
    util.color.cyan(next.match),
    util.color.time(next.time),
    util.color.file(file),
    error.stack
  );

  next.error = error;
  if(!this.runtime.repl){
    process.exit(1);
  }
  next();
};

// Make a logger that looks like gulp's
//
Stack.prototype.onHandle = function(next){
  if(next.error || !this.log || /^-/.test(next.match)){
    return ; // ^ skip logging for errors, silent or flags
  }

  if(!this.configFile){
    this.configFile = process.argv[1];
    this.runtime.set({configFile: this.configFile});
    util.log('Using', util.color.file(process.argv[1]));
  }

  var host = '';
  if(this.host){
    host = '(' + util.color.green(this.host.path) + ') ';
  }

  if(!this.time){
    util.log('%sStarted \'%s\' in %s',
      host,
      util.color.cyan(this.path),
      util.color.bold(this.wait ? 'series' : 'parallel')
    );
    this.time = process.hrtime();
  } else if(this.queue && next.match && next.time){
    util.log('%sFinished \'%s\' after %s',
      host, util.color.cyan(next.match), util.color.time(next.time)
    );
  }

  if(!next.time){ next.time = process.hrtime(); }
  if(this.queue || this.time.end){ return ; }

  this.time.end = util.color.time(this.time);
  util.log('%sFinished \'%s\' after %s',
    host, util.color.cyan(this.path), this.time.end
  );
};

/*
# runtime.src
```js
function src(string|array glob[, object opt])
```
Same as [vinylFs.src][m-vinly-fs-src]
*/

Runtime.prototype.src = vinylFS.src;

/*
# runtime.dest
```js
function dest(string|function folder[, object opt])
```
Same as [vinylFs.dest][m-vinly-fs-dest]
*/
Runtime.prototype.dest = vinylFS.dest;

/*
# runtime.task
```js
function task(string name[, string|array dep, function handle])
```
_arguments_
 - are same as [gulp.task][m-gulp-task] aside of `dep`
 - `dep` type array or string, tasks to run before this one
 if given as a string they must be space separated

_returns_ differences with [gulp.task][m-gulp-task]
 - the function of the task `name`, if it was set before
 - this otherwise (just as [gulp.task][m-gulp-task])
*/

Runtime.prototype.task = function(name, deps, handle){
  var handleType = util.type(handle || deps);

  if(typeof name !== 'string'){
    throw new TypeError(
      'task(string name, [string|array dep, function handle])\n'+
      '- Tasks require a string `name`'
    );
  } else if(!handleType.match(/function|array|string/)){
    throw new TypeError(
      'task(string name, [string|array dep, function handle])\n' +
      'New tasks need at lest 2 arguments\n'+
      ' - deps: dependencies to run before this one'+
      ' - handle: function that runs the task\n'+
      '\n'
    );
  }

  // TODO: checking handle completion should be more robust
  handle = handleType.function || function(next){ next(); };
  if(!handle.length && !/return/.test(handle.toString())){
    name = util.color.cyan(name);
    var error = new Error(
      'task `'+name+'` doesn\'t give a hint of its completion\n'+
      'Meaning: it doesn\'t return nor uses a callback\n'+
      'Solution:\n'+
      ' - return a stream, promise or observable\n'+
      ' - use the callback that is passed as first argument\n--'
    );
    util.color.callersPath(error);
    throw error;
  }

  if(!handle.name && !handle.displayName){
    handle.displayName = name;
  }

  var depsType = util.type(deps);
  if(!depsType.match(/array|string/) || !deps.length){
    return this.set(name, handle);
  }

  this.set(name);
  deps = depsType.array || deps.trim().split(/[ ]+/);

  // prevent circular dependencies
  // since we have path-to-regex mapping
  // TODO: do not allow path with params to be deps?
  //
  deps.forEach(function isDepCircular(dep){
    var node = this.get(dep, {ref: true});
    if(node.path !== name){ return ; }

    var regex = RegExp(this.regex.filter(function(re){
      return re.path === name;
    })[0].source);

    dep = util.color.red(dep);
    name = util.color.cyan(name);
    regex = util.color.yellow(regex);

    var error = new Error(
      'task `'+name+'` dependency `'+dep+'` is circular\n'+
      'Meaning: if is run it will call itself\n'+
      'Solution: rewrite it so they don\'t match\n'+
      '- path: ' + name + '\n'+
      '- deps: ' + deps + '\n'+
      '- regex: ' + JSON.stringify(regex) +'\n--'
    );

    // mark file on the stack
    util.color.callersPath(error, 2);
    throw error;
  }, this);

  return this.set(name, {
    dep: deps,
    handle: this.stack(deps.join(' '), handle, {wait: true})
  });
};

/*
# runtime.watch
```js
function watch(string|array glob[, string|array|object opt, function cb])
```

Same as [gulp.watch][m-gulp-watch] (run tasks when files change).
Additional features:
  - Files can be reloaded on the `require.cache`
  - Tasks can be run in series directly if so specified

_arguments_ differences with [gulp.watch][m-gulp-watch]
 - `tasks` can be a string, space separated tasks will be dispatched as written
 - when `opt` is an object, it can have non mandatory properties below
  - `opt.wait` type boolean, if `opt.tasks` should run in series or not
  - `opt.tasks` type array|string, tasks to run after change and before reload
  - `opt.reload` type boolean, wether or not to reload a file

_defaults_
 - `opt.wait` to `true`
 - `opt.reload` to `undefined`

_returns_
 - a watcher, same as [gulp.watch][m-gulp-watch]

> Notes:
- The callback arguments are:
  - a gaze event
  - the `opt` argument given
- Only files with a registered `require.extension` will be reloaded
- When a file is deleted on disk, the reload will be skipped but the
callback is still invoked

```javascript
var gulp = require('gulp-runtime').create();

var watcher;
watcher = gulp.watch('app/*.js', 'minify browserSync');

gulp.watch('app/*.*', {
  wait: true,  // run tasks after change in series
  tasks: 'sass css jsx browserSync',
  reload: true // will only reload app/*.js files (server, db, etc)
}, function(ev, opt){
  var reloaded = opt.reload && ev.type !== 'delete';
  console.log('%s was %s', ev.path, ev.type, reloaded ? 'and reloaded' : '');
});
```
**/
Runtime.prototype.watch = function(glob, opt, fn){
  var cb = util.type(fn || opt).function;
  var o = cb !== fn && util.type(fn).plainObject
    || cb !== opt && util.type(opt).plainObject
    || {tasks: opt, wait: true};

  var reload = o.reload && function reloadHandle(ev){
    if(ev.type !== 'delete' && require.extension[path.extname(ev.path)]){
      delete require.cache[ev.path];
      require(ev.path);
    }
    if(cb){ cb(ev, o); }
  };

  var tasks = util.type(o && o.tasks || o);
  if(tasks.string || tasks.array){
    tasks = tasks.string || tasks.array.join(' ');
    return vinylFS.watch(glob, null, reload
      ? this.stack(tasks, reload, o)
      : this.stack(tasks, o)
    );
  }

  return vinylFS.watch(glob, o, reload || function onChange(ev){
    if(cb){ cb(ev, o); }
  });
};

// export back tornado
//
exports = module.exports = tornado;
<|endoftext|>


<commit_before>'use strict';

var fs = require('fs') ;
var path = require('path') ;
var _ = require('lodash') ;
var allPossibleTargets = [] ;
var dest = [] ;
var debug ;
var config ;
var pingDown = true ;

function createTargets( config ) {
  allPossibleTargets = _.filter( config.targets, function(t) { return t.enabled === true; }) ;
}
function isValidTarget( target ) {
  return _.find( allPossibleTargets, function(t) {return t.address === target.address && t.port === target.port; }) ;
}

function pingTarget( app, target ) {
  var dest =  'sip:' + target.address + ':' + (target.port || 5060) ;
  if( pingDown ) {
    console.log('not pinging target %s because pinging has been temporarily turned off: ', dest) ;
    return;
  }

  //start a timer - if we don't get a response within a second we'll mark it down
  var timeoutHandler = setTimeout( function() {
    //if we get here we timed out
    if( !target.offline ) {
        target.offline = true ;
        console.error('taking target %s offline because it did not respond to ping: ', dest) ;
    }
  }, 1500 ) ;

  //send the OPTIONS ping
  console.log('OPTIONS %s', dest) ;
  app.request({
      uri: dest,
      method: 'OPTIONS',
      headers: {
        'User-Agent': 'drachtio simple proxy'
      }
  }, function( err, req ) {
    if( err ) {
      setTimeout( function() {
        if( isValidTarget( target ) ) {
          pingTarget( app, target);
        }
        else {
          console.log('stop sending OPTIONS ping to %s; it has been removed from the configuration file', dest) ;
        }
      }, config.optionsPingInterval) ;
      return console.error('Error pinging %s', target) ;
    }

    req.on('response', function(res){

      console.log('%s: %d %s', dest, res.status, res.reason) ;
      clearTimeout( timeoutHandler ) ;
      if( 200 === res.status ) {
        if( target.offline ) {
           target.offline = false ;
           console.log('bringing target back online: %s', dest) ;
        }
      }
      setTimeout( function() {
        if( isValidTarget( target ) ) {
          pingTarget( app, target);
        }
        else {
          console.log('stop sending OPTIONS ping to %s; it has been removed from the configuration file', dest) ;
        }
      }, config.optionsPingInterval) ;
    }) ;
  }) ;
}

function stopPinging() {
  console.log('stopping OPTIONS pings..') ;
  pingDown = true ;
}

function startPinging( app, config ) {
  console.log('start OPTIONS pings to targets at %d ms interval: ', config.optionsPingInterval, JSON.stringify(allPossibleTargets)) ;
  pingDown = false ;
  allPossibleTargets.forEach( function(t) {pingTarget(app,t); }) ;
}

module.exports = function(app) {
  debug = app.debug ;
  config = app.config ;

  //min allowed ping interval is 2 seconds
  config.optionsPingInterval = Math.max(config.optionsPingInterval || 5000, 2000) ;
  console.log('options ping interval will be %d milliseconds', config.optionsPingInterval) ;

  createTargets(config) ;
  startPinging(app, config) ;

  //watch config file for changes - we allow the user to dynamically add or remove targets
  var configPath = path.resolve(__dirname) + '/../config.js' ;
  fs.watchFile(configPath, function () {
    try {
      console.log('config.js was just modified...') ;
      stopPinging() ;

      delete require.cache[require.resolve(configPath)] ;
      config = require(configPath) ;
      createTargets( config ) ;

      console.log('proxy targets are now: ',
        _.map( allPossibleTargets, function(t) { return _.pick(t, ['address','port','enabled','optionsPing']);}));

      startPinging(app, config) ;

    } catch( err ) {
      console.error('Error re-reading config.js after modification; check to ensure there are no syntax errors: ', err) ;
      startPinging(app, config) ;
    }
  }) ;

  app.invite( function(req,res) {

    //filter out offline servers
    var onlineServers = _.filter( allPossibleTargets, function(t) { return !t.offline; }) ;

    if( 0 === onlineServers.length ) {
      console.error('there are no servers online currently') ;
      return res.send(480) ;
    }

    var dest = _.map(  onlineServers, function(t) { return t.address + ':' + (t.port || 5060); }) ;

    debug('proxy destinations are: ', dest) ;

    //finally....here's the magic: proxy the request, attempting destinations in order until we connect or all fail
    req.proxy({
      remainInDialog: false,
      destination: dest
    }, function(err, results) {
      if( err ) console.error('Error proxying: ', err) ;

      //this is voluminous, but if you want to know what happened all the detail is here..
      debug('proxy result: ', JSON.stringify(results)) ;
    }) ;

    //round-robin the next starting server in the list
    var item = allPossibleTargets.shift() ;
    allPossibleTargets.push( item ) ;
  }) ;
} ;

<commit_msg>fix bug where re-lading the config file causes each server to get pinged multiple times each cycle<commit_after>'use strict';

var fs = require('fs') ;
var path = require('path') ;
var _ = require('lodash') ;
var allPossibleTargets = [] ;
var dest = [] ;
var debug ;
var config ;

function createTargets( config ) {
  allPossibleTargets = _.filter( config.targets, function(t) { return t.enabled === true; }) ;
}
function isValidTarget( target ) {
  return _.find( allPossibleTargets, function(t) {return t.address === target.address && t.port === target.port; }) ;
}

function pingTarget( app, target ) {
  var dest =  'sip:' + target.address + ':' + (target.port || 5060) ;
  if( target.markedForDeath ) {
    console.log('removing target %s due to re-loading config file: ', dest) ;
    return;
  }

  //start a timer - if we don't get a response within a second we'll mark it down
  var timeoutHandler = setTimeout( function() {
    //if we get here we timed out
    if( !target.offline ) {
        target.offline = true ;
        console.error('taking target %s offline because it did not respond to ping: ', dest) ;
    }
  }, 1500 ) ;

  //send the OPTIONS ping
  console.log('OPTIONS %s', dest) ;
  app.request({
      uri: dest,
      method: 'OPTIONS',
      headers: {
        'User-Agent': 'drachtio simple proxy'
      }
  }, function( err, req ) {
    if( err ) {
      setTimeout( function() {
        if( isValidTarget( target )  && !target.markedForDeath ) {
          pingTarget( app, target);
        }
        else {
          console.log('stop sending OPTIONS ping to %s; it has been removed from the configuration file', dest) ;
        }
      }, config.optionsPingInterval) ;
      return console.error('Error pinging %s', target) ;
    }

    req.on('response', function(res){

      console.log('%s: %d %s', dest, res.status, res.reason) ;
      clearTimeout( timeoutHandler ) ;
      if( 200 === res.status ) {
        if( target.offline ) {
           target.offline = false ;
           console.log('bringing target back online: %s', dest) ;
        }
      }
      setTimeout( function() {
        if( isValidTarget( target ) && !target.markedForDeath ) {
          pingTarget( app, target);
        }
        else {
          console.log('stop sending OPTIONS ping to %s; it has been removed from the configuration file', dest) ;
        }
      }, config.optionsPingInterval) ;
    }) ;
  }) ;
}

function stopPinging() {
  console.log('stopping OPTIONS pings..') ;
  allPossibleTargets.forEach(function(t) { t.markedForDeath = true; }) ;
}

function startPinging( app, config ) {
  console.log('start OPTIONS pings to targets at %d ms interval: ', config.optionsPingInterval, JSON.stringify(allPossibleTargets)) ;
  allPossibleTargets.forEach( function(t) {pingTarget(app,t); }) ;
}

module.exports = function(app) {
  debug = app.debug ;
  config = app.config ;

  //min allowed ping interval is 2 seconds
  config.optionsPingInterval = Math.max(config.optionsPingInterval || 5000, 2000) ;
  console.log('options ping interval will be %d milliseconds', config.optionsPingInterval) ;

  createTargets(config) ;
  startPinging(app, config) ;

  //watch config file for changes - we allow the user to dynamically add or remove targets
  var configPath = path.resolve(__dirname) + '/../config.js' ;
  fs.watchFile(configPath, function () {
    try {
      console.log('config.js was just modified...') ;
      stopPinging() ;

      delete require.cache[require.resolve(configPath)] ;
      config = require(configPath) ;
      createTargets( config ) ;

      console.log('proxy targets are now: ',
        _.map( allPossibleTargets, function(t) { return _.pick(t, ['address','port','enabled','optionsPing']);}));

      startPinging(app, config) ;

    } catch( err ) {
      console.error('Error re-reading config.js after modification; check to ensure there are no syntax errors: ', err) ;
      startPinging(app, config) ;
    }
  }) ;

  app.invite( function(req,res) {

    //filter out offline servers
    var onlineServers = _.filter( allPossibleTargets, function(t) { return !t.offline; }) ;

    if( 0 === onlineServers.length ) {
      console.error('there are no servers online currently') ;
      return res.send(480) ;
    }

    var dest = _.map(  onlineServers, function(t) { return t.address + ':' + (t.port || 5060); }) ;

    debug('proxy destinations are: ', dest) ;

    //finally....here's the magic: proxy the request, attempting destinations in order until we connect or all fail
    req.proxy({
      remainInDialog: false,
      destination: dest
    }, function(err, results) {
      if( err ) console.error('Error proxying: ', err) ;

      //this is voluminous, but if you want to know what happened all the detail is here..
      debug('proxy result: ', JSON.stringify(results)) ;
    }) ;

    //round-robin the next starting server in the list
    var item = allPossibleTargets.shift() ;
    allPossibleTargets.push( item ) ;
  }) ;
} ;

<|endoftext|>


<commit_before>let chainables = require("./chainables"); // Chainables
let sequences = require("./sequences"); // Start sequences, non-chainable

function makeChainableIterator(name, ...args) {
	return new Proxy(chainables[name](...args), handler);
}

function makeSequence(name, ...args) {
	return new Proxy(sequences[name](...args), handler);
}

let handler = {
	getOwnPropertyDescriptor(target, prop) {
		return Reflect.getOwnPropertyDescriptor(target, prop)
		    || Reflect.getOwnPropertyDescriptor(chainables, prop);
	},
	has(target, prop) {
		return Reflect.has(target, prop) || chainables.hasOwnProperty(prop);
	},
	get(target, prop, receiver) {
		if (!Reflect.apply(Object.prototype.hasOwnProperty, target, [prop])
		  && Reflect.apply(Object.prototype.hasOwnProperty, chainables, [prop])) {
			return makeChainableIterator.bind(this, prop, target);
		}

		if (target[prop] instanceof Function) {
			return () => target[prop]();
		} else {
			return target[prop];
		}
	},
	ownKeys(target) {
		return [...Reflect.ownKeys(target), ...Reflect.ownKeys(chainables)];
	}
};

module.exports = {
	makeChainableIterator,
	makeSequence,
	handler
};
<commit_msg>Use Reflect.get method in proxy get handler<commit_after>let chainables = require("./chainables"); // Chainables
let sequences = require("./sequences"); // Start sequences, non-chainable

function makeChainableIterator(name, ...args) {
	return new Proxy(chainables[name](...args), handler);
}

function makeSequence(name, ...args) {
	return new Proxy(sequences[name](...args), handler);
}

let handler = {
	getOwnPropertyDescriptor(target, prop) {
		return Reflect.getOwnPropertyDescriptor(target, prop)
		    || Reflect.getOwnPropertyDescriptor(chainables, prop);
	},
	has(target, prop) {
		return Reflect.has(target, prop) || chainables.hasOwnProperty(prop);
	},
	get(target, prop, receiver) {
		if (!Reflect.apply(Object.prototype.hasOwnProperty, target, [prop])
		  && Reflect.apply(Object.prototype.hasOwnProperty, chainables, [prop])) {
			return makeChainableIterator.bind(this, prop, target);
		}

		if (target[prop] instanceof Function) {
			return () => target[prop]();
		} else {
			return Reflect.get(target, prop, receiver);
		}
	},
	ownKeys(target) {
		return [...Reflect.ownKeys(target), ...Reflect.ownKeys(chainables)];
	}
};

module.exports = {
	makeChainableIterator,
	makeSequence,
	handler
};
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2015 by Greg Reimer <gregreimer@gmail.com>
 * MIT License. See mit-license.txt for more info.
 */

import http from 'http'
import Cycle from './cycle'
import cheerio from 'cheerio'
import querystring from 'querystring'
import RoutePattern from 'route-pattern'
import isTypeXml from './is-xml'
import { EventEmitter } from 'events'
import co from 'co'
import adapt from 'ugly-adapter'
import { SNISpoofer } from './sni-spoofer'
import net from 'net'
import https from 'https'

function isAsync(fun) {
  return fun.length >= 3
}

function asyncIntercept(opts, intercept) {
  let origIntercept = intercept
  if (!isAsync(intercept)) {
    intercept = function(req, resp, done) {
      let result = origIntercept.call(this, req, resp)
      if (result && typeof result.next === 'function') {
        co(result).then(() => {
          done()
        }, done)
      } else if (result && typeof result.then === 'function') {
        result.then(() => {
          done()
        }, done)
      } else {
        done()
      }
    }
  }
  return intercept
}

function filterIntercept(opts, intercept) {
  if (opts.filter) {
    let origIntercept = intercept
    intercept = function(req, resp, done) {
      if (opts.filter(req, resp)) {
        origIntercept.apply(this, arguments)
      } else {
        done()
      }
    }
  }
  return intercept
}

function asIntercept(opts, intercept) {
  if (opts.as) {
    let origIntercept = intercept
    intercept = function(req, resp, done) {
      let args = arguments
      let r = opts.phase === 'request' ? req : resp
      r._load().then(() => {
        asHandlers[opts.as](r)
        origIntercept.apply(this, args)
        done()
      }).catch(done)
    }
  }
  return intercept
}

let otherIntercept = (() => {
  let ctPatt = /;.*$/
  function test(tester, testee, isUrl) {
    if (tester === undefined) { return true }
    if (tester instanceof RegExp) { return tester.test(testee) }
    if (isUrl) { return getUrlTester(tester)(testee) }
    return tester == testee // intentional double-equals
  }
  return function(opts, intercept) {
    let isReq = opts.phase === 'request' || opts.phase === 'request-sent'
    return function(req, resp, done) {
      let reqContentType = req.headers['content-type']
      let respContentType = resp.headers['content-type']
      let reqMimeType = reqContentType ? reqContentType.replace(ctPatt, '') : undefined
      let respMimeType = respContentType ? respContentType.replace(ctPatt, '') : undefined
      let contentType, mimeType
      contentType = isReq ? reqContentType : respContentType
      mimeType = isReq ? reqMimeType : respMimeType
      let isMatch = 1

      isMatch &= test(opts.contentType, contentType)
      isMatch &= test(opts.mimeType, mimeType)
      isMatch &= test(opts.requestContentType, reqContentType)
      isMatch &= test(opts.responseContentType, respContentType)
      isMatch &= test(opts.requestMimeType, reqMimeType)
      isMatch &= test(opts.responseMimeType, respMimeType)
      isMatch &= test(opts.protocol, req.protocol)
      isMatch &= test(opts.host, req.headers.host)
      isMatch &= test(opts.hostname, req.hostname)
      isMatch &= test(opts.port, req.port)
      isMatch &= test(opts.method, req.method)
      isMatch &= test(opts.url, req.url, true)
      isMatch &= test(opts.fullUrl, req.fullUrl(), true)
      if (isMatch) {
        intercept.apply(this, arguments)
      } else {
        done()
      }
    }
  }
})()

export default class Proxy extends EventEmitter {

  constructor(opts = {}) {
    super()

    if (opts.reverse) {
      let reverse = opts.reverse
      if (!/^https?:\/\/[^:]+(:\d+)?$/.test(reverse)) {
        throw new Error(`invalid value for reverse: "${opts.reverse}"`)
      }
      this._reverse = reverse
    }

    if (opts.upstreamProxy) {
      let proxy = opts.upstreamProxy
      if (!/^https?:\/\//.test(proxy)) {
        proxy = 'http://' + proxy
      }
      if (!/^https?:\/\/[^:]+:\d+$/.test(proxy)) {
        throw new Error(`invalid value for upstreamProxy: "${opts.upstreamProxy}"`)
      }
      this._upstreamProxy = proxy
    }

    this._intercepts = Object.freeze({
      'request': [],
      'request-sent': [],
      'response': [],
      'response-sent': [],
    })

    this._server = http.createServer((fromClient, toClient) => {

      let cycle = new Cycle(this)
        , req = cycle._request
        , resp = cycle._response

      cycle.on('log', log => this.emit('log', log))

      co.call(this, function*() {
        req._setHttpSource(fromClient, opts.reverse)
        try { yield this._runIntercepts('request', cycle) }
        catch(ex) { this._emitError(ex, 'request') }
        let partiallyFulfilledRequest = yield cycle._sendToServer()
        try { yield this._runIntercepts('request-sent', cycle) }
        catch(ex) { this._emitError(ex, 'request-sent') }
        if (partiallyFulfilledRequest === undefined) {
          this.emit('log', {
            level: 'debug',
            message: `server fetch skipped for ${req.fullUrl()}`,
          })
        } else {
          let responseFromServer = yield partiallyFulfilledRequest.receive()
          resp._setHttpSource(responseFromServer)
        }
        try { yield this._runIntercepts('response', cycle) }
        catch(ex) { this._emitError(ex, 'response') }
        yield cycle._sendToClient(toClient)
        try { yield this._runIntercepts('response-sent', cycle) }
        catch(ex) { this._emitError(ex, 'response-sent') }
      }).catch(ex => {
        this.emit('error', ex)
        this.emit('log', {
          level: 'error',
          message: ex.message,
          error: ex,
        })
      })
    })

    this._server.on('error', err => {
      this.emit('error', err)
      this.emit('log', {
        level: 'error',
        message: 'proxy server error: ' + err.message,
        error: err,
      })
    })

    if (opts.certAuthority) {

      let { key, cert } = opts.certAuthority
        , spoofer = new SNISpoofer(key, cert)
        , SNICallback = spoofer.callback()
        , cxnEstablished = new Buffer(`HTTP/1.1 200 Connection Established\r\n\r\n`, 'ascii')

      spoofer.on('error', err => this.emit('error', err))
      spoofer.on('generate', serverName => {
        this.emit('log', {
          level: 'info',
          message: `generated fake credentials for ${serverName}`,
        })
      })

      this._server.on('connect', (request, clientSocket, head) => {
        let addr = this._tlsSpoofingServer.address()
        let serverSocket = net.connect(addr.port, addr.address, () => {
          clientSocket.write(cxnEstablished)
          serverSocket.write(head)
          clientSocket
          .pipe(serverSocket)
          .pipe(clientSocket)
        })
      })

      this._tlsSpoofingServer = https.createServer({
        key,
        cert,
        SNICallback,
      }, (fromClient, toClient) => {
        let shp = 'https://' + fromClient.headers.host
          , fullUrl = shp + fromClient.url
          , addr = this._server.address()
        let toServer = http.request({
          host: addr.address,
          port: addr.port,
          method: fromClient.method,
          path: fullUrl,
          headers: fromClient.headers,
        }, fromServer => {
          toClient.writeHead(fromServer.statusCode, fromServer.headers)
          fromServer.pipe(toClient)
        })
        fromClient.pipe(toServer)
      })
    }
  }

  listen(port) {
    // TODO: test bogus port
    this._server.listen.apply(this._server, arguments)
    let message = 'proxy listening on ' + port
    if (this._reverse) {
      message += ', reverse ' + this._reverse
    }
    this.emit('log', {
      level: 'info',
      message: message,
    })
    if (this._tlsSpoofingServer) {
      this._tlsSpoofingServer.listen(0)
    }
    return this
  }

  intercept(opts, intercept) {
    // TODO: test string versus object
    // TODO: test opts is undefined
    if (typeof opts === 'string') {
      opts = { phase: opts }
    }
    let phase = opts.phase
    if (!this._intercepts.hasOwnProperty(phase)) {
      throw new Error(phase ? 'invalid phase ' + phase : 'missing phase')
    }
    if (opts.as) {
      if (!asHandlers[opts.as]) {
        // TODO: test bogus as
        throw new Error('invalid as: ' + opts.as)
      }
      if (phase === 'request-sent' || phase === 'response-sent') {
        // TODO: test intercept as in read only phase
        throw new Error('cannot intercept ' + opts.as + ' in phase ' + phase)
      }
    }
    intercept = asyncIntercept(opts, intercept)
    intercept = asIntercept(opts, intercept) // TODO: test asIntercept this, args, async
    intercept = filterIntercept(opts, intercept) // TODO: test filterIntercept this, args, async
    intercept = otherIntercept(opts, intercept) // TODO: test otherIntercept this, args, async
    this._intercepts[phase].push(intercept)
  }

  close() {
    this._server.close.apply(this._server, arguments)
  }

  address() {
    return this._server.address.apply(this._server, arguments)
  }

  log(events, cb) {
    let listenTo = {}
    events.split(/\s/)
    .map(s => s.trim())
    .filter(s => !!s)
    .forEach(s => listenTo[s] = true)
    let writable
    if (!cb) {
      writable = process.stderr
    } else if (cb.write) {
      writable = cb
    }
    this.on('log', log => {
      if (!listenTo[log.level]) { return }
      let message = log.error ? log.error.stack : log.message
      if (writable) {
        writable.write(log.level.toUpperCase() + ': ' + message + '\n')
      } else if (typeof cb === 'function') {
        cb(log)
      }
    })
  }

  _emitError(ex, phase) {
    this.emit('log', {
      level: 'error',
      message: `${phase} phase error: ${ex.message}`,
      error: ex,
    })
  }

  _runIntercepts(phase, cycle) {

    let req = cycle._request
      , resp = cycle._response
      , self = this
      , intercepts = this._intercepts[phase]

    return co(function*() {
      cycle._setPhase(phase)
      for (let intercept of intercepts) {
        let t = setTimeout(() => {
          self.emit('log', {
            level: 'debug',
            message: 'an async ' + phase + ' intercept is taking a long time: ' + req.fullUrl(),
          })
        }, 5000)
        yield adapt.method(intercept, 'call', cycle, req, resp)
        clearTimeout(t)
      }
    })
  }
}

// TODO: test direct url string comparison, :id tags, wildcard, regexp
// TODO: test line direct url string comparison, :id tags, wildcard
let getUrlTester = (() => {
  let sCache = {},
    rCache = {}
  return testUrl => {
    if (testUrl instanceof RegExp) {
      if (!rCache[testUrl]) {
        rCache[testUrl] = u => testUrl.test(u)
      }
      return rCache[testUrl]
    } else {
      if (!sCache[testUrl]) {
        if (!testUrl) {
          sCache[testUrl] = u => testUrl === u
        } else {
          let pattern = RoutePattern.fromString(testUrl)
          sCache[testUrl] = u => pattern.matches(u)
        }
      }
      return sCache[testUrl]
    }
  }
})()

// TODO: test all five for both requet and response
let asHandlers = {
  '$': r => {
    // TODO: test to ensure that parse errors here propagate to error log.
    // TODO: test to ensure that parse errors here fail gracefully.
    let contentType = r.headers['content-type']
    let isXml = isTypeXml(contentType)
    r.$ = cheerio.load(r._source.toString(), { xmlMode: isXml })
  },
  'json': r => {
    // TODO: test to ensure that parse errors here propagate to error log.
    // TODO: test to ensure that parse errors here fail gracefully.
    r.json = JSON.parse(r._source.toString())
  },
  'params': r => {
    // TODO: test to ensure that parse errors here propagate to error log.
    // TODO: test to ensure that parse errors here fail gracefully.
    r.params = querystring.parse(r._source.toString())
  },
  'buffer': () => {},
  'string': () => {},
}
<commit_msg>If tls option passed, start proxy as an https server<commit_after>/*
 * Copyright (c) 2015 by Greg Reimer <gregreimer@gmail.com>
 * MIT License. See mit-license.txt for more info.
 */

import http from 'http'
import Cycle from './cycle'
import cheerio from 'cheerio'
import querystring from 'querystring'
import RoutePattern from 'route-pattern'
import isTypeXml from './is-xml'
import { EventEmitter } from 'events'
import co from 'co'
import adapt from 'ugly-adapter'
import { SNISpoofer } from './sni-spoofer'
import net from 'net'
import https from 'https'

function isAsync(fun) {
  return fun.length >= 3
}

function asyncIntercept(opts, intercept) {
  let origIntercept = intercept
  if (!isAsync(intercept)) {
    intercept = function(req, resp, done) {
      let result = origIntercept.call(this, req, resp)
      if (result && typeof result.next === 'function') {
        co(result).then(() => {
          done()
        }, done)
      } else if (result && typeof result.then === 'function') {
        result.then(() => {
          done()
        }, done)
      } else {
        done()
      }
    }
  }
  return intercept
}

function filterIntercept(opts, intercept) {
  if (opts.filter) {
    let origIntercept = intercept
    intercept = function(req, resp, done) {
      if (opts.filter(req, resp)) {
        origIntercept.apply(this, arguments)
      } else {
        done()
      }
    }
  }
  return intercept
}

function asIntercept(opts, intercept) {
  if (opts.as) {
    let origIntercept = intercept
    intercept = function(req, resp, done) {
      let args = arguments
      let r = opts.phase === 'request' ? req : resp
      r._load().then(() => {
        asHandlers[opts.as](r)
        origIntercept.apply(this, args)
        done()
      }).catch(done)
    }
  }
  return intercept
}

let otherIntercept = (() => {
  let ctPatt = /;.*$/
  function test(tester, testee, isUrl) {
    if (tester === undefined) { return true }
    if (tester instanceof RegExp) { return tester.test(testee) }
    if (isUrl) { return getUrlTester(tester)(testee) }
    return tester == testee // intentional double-equals
  }
  return function(opts, intercept) {
    let isReq = opts.phase === 'request' || opts.phase === 'request-sent'
    return function(req, resp, done) {
      let reqContentType = req.headers['content-type']
      let respContentType = resp.headers['content-type']
      let reqMimeType = reqContentType ? reqContentType.replace(ctPatt, '') : undefined
      let respMimeType = respContentType ? respContentType.replace(ctPatt, '') : undefined
      let contentType, mimeType
      contentType = isReq ? reqContentType : respContentType
      mimeType = isReq ? reqMimeType : respMimeType
      let isMatch = 1

      isMatch &= test(opts.contentType, contentType)
      isMatch &= test(opts.mimeType, mimeType)
      isMatch &= test(opts.requestContentType, reqContentType)
      isMatch &= test(opts.responseContentType, respContentType)
      isMatch &= test(opts.requestMimeType, reqMimeType)
      isMatch &= test(opts.responseMimeType, respMimeType)
      isMatch &= test(opts.protocol, req.protocol)
      isMatch &= test(opts.host, req.headers.host)
      isMatch &= test(opts.hostname, req.hostname)
      isMatch &= test(opts.port, req.port)
      isMatch &= test(opts.method, req.method)
      isMatch &= test(opts.url, req.url, true)
      isMatch &= test(opts.fullUrl, req.fullUrl(), true)
      if (isMatch) {
        intercept.apply(this, arguments)
      } else {
        done()
      }
    }
  }
})()

export default class Proxy extends EventEmitter {

  constructor(opts = {}) {
    super()

    if (opts.reverse) {
      let reverse = opts.reverse
      if (!/^https?:\/\/[^:]+(:\d+)?$/.test(reverse)) {
        throw new Error(`invalid value for reverse: "${opts.reverse}"`)
      }
      this._reverse = reverse
    }

    if (opts.upstreamProxy) {
      let proxy = opts.upstreamProxy
      if (!/^https?:\/\//.test(proxy)) {
        proxy = 'http://' + proxy
      }
      if (!/^https?:\/\/[^:]+:\d+$/.test(proxy)) {
        throw new Error(`invalid value for upstreamProxy: "${opts.upstreamProxy}"`)
      }
      this._upstreamProxy = proxy
    }

    this._intercepts = Object.freeze({
      'request': [],
      'request-sent': [],
      'response': [],
      'response-sent': [],
    })

    let createServer = opts.tls
      ? https.createServer.bind(https, opts.tls)
      : http.createServer.bind(http)

    this._server = createServer((fromClient, toClient) => {

      let cycle = new Cycle(this)
        , req = cycle._request
        , resp = cycle._response

      cycle.on('log', log => this.emit('log', log))

      co.call(this, function*() {
        req._setHttpSource(fromClient, opts.reverse)
        try { yield this._runIntercepts('request', cycle) }
        catch(ex) { this._emitError(ex, 'request') }
        let partiallyFulfilledRequest = yield cycle._sendToServer()
        try { yield this._runIntercepts('request-sent', cycle) }
        catch(ex) { this._emitError(ex, 'request-sent') }
        if (partiallyFulfilledRequest === undefined) {
          this.emit('log', {
            level: 'debug',
            message: `server fetch skipped for ${req.fullUrl()}`,
          })
        } else {
          let responseFromServer = yield partiallyFulfilledRequest.receive()
          resp._setHttpSource(responseFromServer)
        }
        try { yield this._runIntercepts('response', cycle) }
        catch(ex) { this._emitError(ex, 'response') }
        yield cycle._sendToClient(toClient)
        try { yield this._runIntercepts('response-sent', cycle) }
        catch(ex) { this._emitError(ex, 'response-sent') }
      }).catch(ex => {
        this.emit('error', ex)
        this.emit('log', {
          level: 'error',
          message: ex.message,
          error: ex,
        })
      })
    })

    this._server.on('error', err => {
      this.emit('error', err)
      this.emit('log', {
        level: 'error',
        message: 'proxy server error: ' + err.message,
        error: err,
      })
    })

    if (opts.certAuthority) {

      let { key, cert } = opts.certAuthority
        , spoofer = new SNISpoofer(key, cert)
        , SNICallback = spoofer.callback()
        , cxnEstablished = new Buffer(`HTTP/1.1 200 Connection Established\r\n\r\n`, 'ascii')

      spoofer.on('error', err => this.emit('error', err))
      spoofer.on('generate', serverName => {
        this.emit('log', {
          level: 'info',
          message: `generated fake credentials for ${serverName}`,
        })
      })

      this._server.on('connect', (request, clientSocket, head) => {
        let addr = this._tlsSpoofingServer.address()
        let serverSocket = net.connect(addr.port, addr.address, () => {
          clientSocket.write(cxnEstablished)
          serverSocket.write(head)
          clientSocket
          .pipe(serverSocket)
          .pipe(clientSocket)
        })
      })

      this._tlsSpoofingServer = https.createServer({
        key,
        cert,
        SNICallback,
      }, (fromClient, toClient) => {
        let shp = 'https://' + fromClient.headers.host
          , fullUrl = shp + fromClient.url
          , addr = this._server.address()
        let toServer = http.request({
          host: addr.address,
          port: addr.port,
          method: fromClient.method,
          path: fullUrl,
          headers: fromClient.headers,
        }, fromServer => {
          toClient.writeHead(fromServer.statusCode, fromServer.headers)
          fromServer.pipe(toClient)
        })
        fromClient.pipe(toServer)
      })
    }
  }

  listen(port) {
    // TODO: test bogus port
    this._server.listen.apply(this._server, arguments)
    let message = 'proxy listening on ' + port
    if (this._reverse) {
      message += ', reverse ' + this._reverse
    }
    this.emit('log', {
      level: 'info',
      message: message,
    })
    if (this._tlsSpoofingServer) {
      this._tlsSpoofingServer.listen(0)
    }
    return this
  }

  intercept(opts, intercept) {
    // TODO: test string versus object
    // TODO: test opts is undefined
    if (typeof opts === 'string') {
      opts = { phase: opts }
    }
    let phase = opts.phase
    if (!this._intercepts.hasOwnProperty(phase)) {
      throw new Error(phase ? 'invalid phase ' + phase : 'missing phase')
    }
    if (opts.as) {
      if (!asHandlers[opts.as]) {
        // TODO: test bogus as
        throw new Error('invalid as: ' + opts.as)
      }
      if (phase === 'request-sent' || phase === 'response-sent') {
        // TODO: test intercept as in read only phase
        throw new Error('cannot intercept ' + opts.as + ' in phase ' + phase)
      }
    }
    intercept = asyncIntercept(opts, intercept)
    intercept = asIntercept(opts, intercept) // TODO: test asIntercept this, args, async
    intercept = filterIntercept(opts, intercept) // TODO: test filterIntercept this, args, async
    intercept = otherIntercept(opts, intercept) // TODO: test otherIntercept this, args, async
    this._intercepts[phase].push(intercept)
  }

  close() {
    this._server.close.apply(this._server, arguments)
  }

  address() {
    return this._server.address.apply(this._server, arguments)
  }

  log(events, cb) {
    let listenTo = {}
    events.split(/\s/)
    .map(s => s.trim())
    .filter(s => !!s)
    .forEach(s => listenTo[s] = true)
    let writable
    if (!cb) {
      writable = process.stderr
    } else if (cb.write) {
      writable = cb
    }
    this.on('log', log => {
      if (!listenTo[log.level]) { return }
      let message = log.error ? log.error.stack : log.message
      if (writable) {
        writable.write(log.level.toUpperCase() + ': ' + message + '\n')
      } else if (typeof cb === 'function') {
        cb(log)
      }
    })
  }

  _emitError(ex, phase) {
    this.emit('log', {
      level: 'error',
      message: `${phase} phase error: ${ex.message}`,
      error: ex,
    })
  }

  _runIntercepts(phase, cycle) {

    let req = cycle._request
      , resp = cycle._response
      , self = this
      , intercepts = this._intercepts[phase]

    return co(function*() {
      cycle._setPhase(phase)
      for (let intercept of intercepts) {
        let t = setTimeout(() => {
          self.emit('log', {
            level: 'debug',
            message: 'an async ' + phase + ' intercept is taking a long time: ' + req.fullUrl(),
          })
        }, 5000)
        yield adapt.method(intercept, 'call', cycle, req, resp)
        clearTimeout(t)
      }
    })
  }
}

// TODO: test direct url string comparison, :id tags, wildcard, regexp
// TODO: test line direct url string comparison, :id tags, wildcard
let getUrlTester = (() => {
  let sCache = {},
    rCache = {}
  return testUrl => {
    if (testUrl instanceof RegExp) {
      if (!rCache[testUrl]) {
        rCache[testUrl] = u => testUrl.test(u)
      }
      return rCache[testUrl]
    } else {
      if (!sCache[testUrl]) {
        if (!testUrl) {
          sCache[testUrl] = u => testUrl === u
        } else {
          let pattern = RoutePattern.fromString(testUrl)
          sCache[testUrl] = u => pattern.matches(u)
        }
      }
      return sCache[testUrl]
    }
  }
})()

// TODO: test all five for both requet and response
let asHandlers = {
  '$': r => {
    // TODO: test to ensure that parse errors here propagate to error log.
    // TODO: test to ensure that parse errors here fail gracefully.
    let contentType = r.headers['content-type']
    let isXml = isTypeXml(contentType)
    r.$ = cheerio.load(r._source.toString(), { xmlMode: isXml })
  },
  'json': r => {
    // TODO: test to ensure that parse errors here propagate to error log.
    // TODO: test to ensure that parse errors here fail gracefully.
    r.json = JSON.parse(r._source.toString())
  },
  'params': r => {
    // TODO: test to ensure that parse errors here propagate to error log.
    // TODO: test to ensure that parse errors here fail gracefully.
    r.params = querystring.parse(r._source.toString())
  },
  'buffer': () => {},
  'string': () => {},
}
<|endoftext|>