<commit_before>/**
 * Just flat-out fail the test with the given message
 */
function fail(message) {
  throw message;
}

/**
 * Perform an assertion several times. If the assertion passes before the
 * maximum number of iterations, the assertion passes. Otherwise the
 * assertion fails
 * @param f The function to perform (possibly) multiple times
 * @param maxTries The maximum number of attempts
 * @param target The UIATarget (needed for delaying between attempts)
 * @param delay The amount of time to pause between attempts
 */
function retryFunctionWithDelay(f, maxTries, target, delay) {
  var tries = 0;
  var exception = null;
  while (tries < maxTries) {
    try {
      f();
      return;  // if we get here, our function must have passed (no exceptions)
    }
    catch(e) {
      exception = e;
      tries++;
      target.delay(delay);
    }
  }
  throw exception;
}

/**
 * A simplified version of 'retryFunctionWithDelay' with an implicit delay of
 * 0.5 seconds.
 * @param f The function to perform (possibly) multiple times
 * @param maxTries The maximum number of attempts
 * @param target The UIATarget (needed for delaying between attempts)
 */
function retryFunction(f, maxTries, target) {
  retryFunctionWithDelay(f, maxTries, target, 0.5);
}

/**
 * Asserts that the given expression is true and throws an exception with
 * a default message, or the optional +message+ parameter
 */
function assertTrue(expression, message) {
  if (! expression) {
    if (! message) message = "Assertion failed";
    throw message;
  }
}

/**
 * Asserts that the given regular expression matches the result of the
 * given message.
 * @param pattern - the pattern to match
 * @param expression - the expression to match against
 * @param message - an optional string message
 */
function assertMatch(regExp, expression, message) {
  var defMessage = "'" + expression + "' does not match '" + regExp + "'";
  assertTrue(regExp.test(expression), message ? message + ": " + defMessage : defMessage);
}

/**
 * Assert that the +received+ object matches the +expected+ object (using
 * plain ol' ==). If it doesn't, this method throws an exception with either
 * a default message, or the one given as the last (optional) argument
 */
function assertEquals(expected, received, message) {
  var defMessage = "Expected <" + expected + "> but received <" + received + ">";
  assertTrue(expected == received, message ? message + ": " + defMessage : defMessage);
}

/**
 * Asserts that the given expression is false and otherwise throws an
 * exception with a default message, or the optional +message+ parameter
 */
function assertFalse(expression, message) {
  assertTrue(! expression, message);
}

/**
 * Asserts that the given object is null or UIAElementNil (UIAutomation's
 * version of a null stand-in). If the given object is not one of these,
 * an exception is thrown with a default message or the given optional
 * +message+ parameter.
 */
function assertNull(thingie, message) {
  var defMessage = "Expected a null object, but received <" + thingie + ">";
  // TODO: string-matching on UIAElementNil makes my tummy feel bad. Fix it.
  assertTrue(thingie == null || thingie.toString() == "[object UIAElementNil]",
             message ? message + ": " + defMessage : defMessage);
}

/**
 * Asserts that the given object is null or UIAElementNil (UIAutomation's
 * version of a null stand-in). If the given object is not one of these,
 * an exception is thrown with a default message or the given optional
 * +message+ parameter.
 */
function assertNull(thingie, message) {
  var defMessage = "Expected a null object, but received <" + thingie + ">";
  // TODO: string-matching on UIAElementNil makes my tummy feel bad. Fix it.
  assertTrue(thingie == null || thingie.toString() == "[object UIAElementNil]",
             message ? message + ": " + defMessage : defMessage);
}

/**
 * Asserts that the given object is not null or UIAElementNil (UIAutomation's
 * version of a null stand-in). If it is null, an exception is thrown with
 * a default message or the given optional +message+ parameter
 */
function assertNotNull(thingie, message) {
  var defMessage = "Expected not null object";
  assertTrue(thingie != null && thingie.toString() != "[object UIAElementNil]",
             message ? message + ": " + defMessage : defMessage);
}

/**
 * Assert that the given window definition matches the current main window. The
 * window definition is a JavaScript object whose property hierarchy matches
 * the main UIAWindow.  Property names in the given definition that match a
 * method will cause that method to be invoked and the matching to be performed
 * and the result. For example, the UIAWindow exposes all UITableViews through
 * the tableViews() method. You only need to specify a 'tableViews' property to
 * cause the method to be invoked.
 *
 * PROPERTY HIERARCHY Property definitions can be nested as deeply as
 * necessary. Matching is done by traversing the same path in the main
 * UIAWindow as your screen definition. For example, to make assertions about
 * the left and right buttons in a UINavigationBar you can do this:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: { name: "Back" },
 *     rightButton: ( name: "Done" },
 *   }
 * });
 *
 * PROPERTY MATCHERS For each property you wish to make an assertion about, you
 * can specify a string, number regular expression or function. Strings and
 * numbers are matches using the assertEquals() method. Regular expressions are
 * matches using the assertMatch() method.
 *
 * If you specify 'null' for a property, it means you don't care to match.
 * Typically this is done inside of arrays where you need to match the number
 * of elements, but don't necessarily care to make assertions about each one.
 *
 * Functions are given the matching property as the single argument. For
 * example:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: function(button) {
 *       // make custom assertions here
 *     }
 *   }
 * });
 *
 * ARRAYS
 * If a property you want to match is an array (e.g. tableViews()), you can
 * specify one of the above matchers for each element of the array. If the
 * number of provided matchers does not match the number of given elements, the
 * assertion will fail (throw an exception)
 *
 * In any case, you specify another object definition for each property to
 * drill-down into the atomic properties you wish to test. For example:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: { name: "Back" },
 *     rightButton: ( name: "Done" },
 *   },
 *   tableViews: [
 *     {
 *       groups: [
 *         { name: "First Group" },
 *         { name: "Second Group" }
 *       ],
 *       cells: [
 *         { name: "Cell 1" },
 *         { name: "Cell 2" },
 *         { name: "Cell 3" },
 *         { name: "Cell 4" }
 *       ]
 *     }
 *   ]
 * });
 *
 * HANDLING FAILURE If any match fails, an appropriate exception will be
 * thrown. If you are using the test structure provided by tuneup, this will be
 * caught and detailed correctly in Instruments.
 *
 * POST-PROCESSING If your screen definition provides an 'onPass' property that
 * points to a function, that function will be invoked after all matching has
 * been peformed on the current window and all assertions have passed. This
 * means you can assert the structure of your screen and operate on it in one
 * pass:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: { name: "Back" }
 *   },
 *   onPass: function(window) {
 *     var leftButton = window.navigationBar().leftButton();
 *     leftButton.tap();
 *   }
 * });
 */
function assertWindow(window) {
  target = UIATarget.localTarget();
  application = target.frontMostApp();
  mainWindow = application.mainWindow();

  if (window.onPass) {
    var onPass = window.onPass;
    delete window.onPass;
  }

  try {
    assertPropertiesMatch(window, mainWindow, 0);
  }
  catch(badProp) {
    fail("Failed to match " + badProp[0] + ": " + badProp[1]);
  }

  if (onPass) {
    try {
      onPass(mainWindow);
    }
    catch(e) {
      throw "Failed to execute 'onPass' callback: " + e;
    }
  }
};

/**
 * Asserts that the +expected+ object matches the +given+ object by making
 * assertions appropriate based on the type of each property in the
 * +expected+ object. This method will recurse through the structure,
 * applying assertions for each matching property path. See the description
 * for +assertWindow+ for details on the matchers.
 */
function assertPropertiesMatch(expected, given, level) {
  for (var propName in expected) {
    if (propName.match(/~iphone$/) && UIATarget.localTarget().model().match(/^iPhone/) === null) {
      return;
    }
    else if (propName.match(/~ipad$/) && UIATarget.localTarget().model().match(/^iPad/) === null) {
      return;
    }

    if (expected.hasOwnProperty(propName)) {
      try {
        var expectedProp = expected[propName];
        var givenProp = given[propName];

        if (typeof(givenProp) == "function") {
          try {
            // We have to use eval (shudder) because calling functions on
            // UIAutomation objects with () operator crashes
            // See Radar bug 8496138
            givenProp = eval("given." + propName + "()");
          }
          catch (e) {
            UIALogger.logDebug("Unable to evaluate given." + propName + "() against " + given);
            continue;
          }
        }

        // null indicates we don't care to match
        if (expectedProp == null) {
          continue;
        }

        if (givenProp == null) {
          throw propName;
        }

        if (typeof(expectedProp) == "string") {
          assertEquals(expectedProp, givenProp);
        }
        else if (typeof(expectedProp) == "number") {
          assertEquals(expectedProp, givenProp);
        }
        else if (typeof(expectedProp) == "function") {
          if (expectedProp.constructor == RegExp) {
            assertMatch(expectedProp, givenProp);
          }
          else {
            expectedProp(givenProp);
          }
        }
        else if (typeof(expectedProp) == "object") {
          if (expectedProp.constructor == Array) {
            assertEquals(expectedProp.length, givenProp.length, "Incorrect number of elements in array");
            for (var i = 0; i < expectedProp.length; i++) {
              var exp = expectedProp[i];
              var giv = givenProp[i];
              assertPropertiesMatch(exp, giv, level + 1);
            };
          }
          else if (typeof(givenProp) == "object") {
            assertPropertiesMatch(expectedProp, givenProp, level + 1);
          }
          else {
            throw propName;
          }
        }
      }
      catch(e) {
        if (e.constructor == Array) {
          e[0] = propName + "." + e[0];
          throw e;
        }
        else {
          var err = [propName, e];
          throw err;
        }
      }
    }
  }
};

<commit_msg>A variety of syntax fixes for safer JavaScript execution.<commit_after>/**
 * Just flat-out fail the test with the given message
 */
function fail(message) {
  throw message;
}

/**
 * Perform an assertion several times. If the assertion passes before the
 * maximum number of iterations, the assertion passes. Otherwise the
 * assertion fails
 * @param f The function to perform (possibly) multiple times
 * @param maxTries The maximum number of attempts
 * @param target The UIATarget (needed for delaying between attempts)
 * @param delay The amount of time to pause between attempts
 */
function retryFunctionWithDelay(f, maxTries, target, delay) {
  var tries = 0;
  var exception = null;
  while (tries < maxTries) {
    try {
      f();
      return;  // if we get here, our function must have passed (no exceptions)
    }
    catch(e) {
      exception = e;
      tries++;
      target.delay(delay);
    }
  }
  throw exception;
}

/**
 * A simplified version of 'retryFunctionWithDelay' with an implicit delay of
 * 0.5 seconds.
 * @param f The function to perform (possibly) multiple times
 * @param maxTries The maximum number of attempts
 * @param target The UIATarget (needed for delaying between attempts)
 */
function retryFunction(f, maxTries, target) {
  retryFunctionWithDelay(f, maxTries, target, 0.5);
}

/**
 * Asserts that the given expression is true and throws an exception with
 * a default message, or the optional +message+ parameter
 */
function assertTrue(expression, message) {
  if (! expression) {
    if (! message) { message = "Assertion failed"; }
    throw message;
  }
}

/**
 * Asserts that the given regular expression matches the result of the
 * given message.
 * @param pattern - the pattern to match
 * @param expression - the expression to match against
 * @param message - an optional string message
 */
function assertMatch(regExp, expression, message) {
  var defMessage = "'" + expression + "' does not match '" + regExp + "'";
  assertTrue(regExp.test(expression), message ? message + ": " + defMessage : defMessage);
}

/**
 * Assert that the +received+ object matches the +expected+ object (using
 * plain ol' ==). If it doesn't, this method throws an exception with either
 * a default message, or the one given as the last (optional) argument
 */
function assertEquals(expected, received, message) {
  var defMessage = "Expected <" + expected + "> but received <" + received + ">";
  assertTrue(expected == received, message ? message + ": " + defMessage : defMessage);
}

/**
 * Asserts that the given expression is false and otherwise throws an
 * exception with a default message, or the optional +message+ parameter
 */
function assertFalse(expression, message) {
  assertTrue(! expression, message);
}

/**
 * Asserts that the given object is null or UIAElementNil (UIAutomation's
 * version of a null stand-in). If the given object is not one of these,
 * an exception is thrown with a default message or the given optional
 * +message+ parameter.
 */
function assertNull(thingie, message) {
  var defMessage = "Expected a null object, but received <" + thingie + ">";
  // TODO: string-matching on UIAElementNil makes my tummy feel bad. Fix it.
  assertTrue(thingie === null || thingie.toString() == "[object UIAElementNil]",
             message ? message + ": " + defMessage : defMessage);
}

/**
 * Asserts that the given object is null or UIAElementNil (UIAutomation's
 * version of a null stand-in). If the given object is not one of these,
 * an exception is thrown with a default message or the given optional
 * +message+ parameter.
 */
function assertNull(thingie, message) {
  var defMessage = "Expected a null object, but received <" + thingie + ">";
  // TODO: string-matching on UIAElementNil makes my tummy feel bad. Fix it.
  assertTrue(thingie === null || thingie.toString() == "[object UIAElementNil]",
             message ? message + ": " + defMessage : defMessage);
}

/**
 * Asserts that the given object is not null or UIAElementNil (UIAutomation's
 * version of a null stand-in). If it is null, an exception is thrown with
 * a default message or the given optional +message+ parameter
 */
function assertNotNull(thingie, message) {
  var defMessage = "Expected not null object";
  assertTrue(thingie !== null && thingie.toString() != "[object UIAElementNil]",
             message ? message + ": " + defMessage : defMessage);
}

/**
 * Assert that the given window definition matches the current main window. The
 * window definition is a JavaScript object whose property hierarchy matches
 * the main UIAWindow.  Property names in the given definition that match a
 * method will cause that method to be invoked and the matching to be performed
 * and the result. For example, the UIAWindow exposes all UITableViews through
 * the tableViews() method. You only need to specify a 'tableViews' property to
 * cause the method to be invoked.
 *
 * PROPERTY HIERARCHY Property definitions can be nested as deeply as
 * necessary. Matching is done by traversing the same path in the main
 * UIAWindow as your screen definition. For example, to make assertions about
 * the left and right buttons in a UINavigationBar you can do this:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: { name: "Back" },
 *     rightButton: ( name: "Done" },
 *   }
 * });
 *
 * PROPERTY MATCHERS For each property you wish to make an assertion about, you
 * can specify a string, number regular expression or function. Strings and
 * numbers are matches using the assertEquals() method. Regular expressions are
 * matches using the assertMatch() method.
 *
 * If you specify 'null' for a property, it means you don't care to match.
 * Typically this is done inside of arrays where you need to match the number
 * of elements, but don't necessarily care to make assertions about each one.
 *
 * Functions are given the matching property as the single argument. For
 * example:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: function(button) {
 *       // make custom assertions here
 *     }
 *   }
 * });
 *
 * ARRAYS
 * If a property you want to match is an array (e.g. tableViews()), you can
 * specify one of the above matchers for each element of the array. If the
 * number of provided matchers does not match the number of given elements, the
 * assertion will fail (throw an exception)
 *
 * In any case, you specify another object definition for each property to
 * drill-down into the atomic properties you wish to test. For example:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: { name: "Back" },
 *     rightButton: ( name: "Done" },
 *   },
 *   tableViews: [
 *     {
 *       groups: [
 *         { name: "First Group" },
 *         { name: "Second Group" }
 *       ],
 *       cells: [
 *         { name: "Cell 1" },
 *         { name: "Cell 2" },
 *         { name: "Cell 3" },
 *         { name: "Cell 4" }
 *       ]
 *     }
 *   ]
 * });
 *
 * HANDLING FAILURE If any match fails, an appropriate exception will be
 * thrown. If you are using the test structure provided by tuneup, this will be
 * caught and detailed correctly in Instruments.
 *
 * POST-PROCESSING If your screen definition provides an 'onPass' property that
 * points to a function, that function will be invoked after all matching has
 * been peformed on the current window and all assertions have passed. This
 * means you can assert the structure of your screen and operate on it in one
 * pass:
 *
 * assertWindow({
 *   navigationBar: {
 *     leftButton: { name: "Back" }
 *   },
 *   onPass: function(window) {
 *     var leftButton = window.navigationBar().leftButton();
 *     leftButton.tap();
 *   }
 * });
 */
function assertWindow(window) {
  target = UIATarget.localTarget();
  application = target.frontMostApp();
  mainWindow = application.mainWindow();

  var onPass = null;
  if (window.onPass) {
    onPass = window.onPass;
    delete window.onPass;
  }

  try {
    assertPropertiesMatch(window, mainWindow, 0);
  }
  catch(badProp) {
    fail("Failed to match " + badProp[0] + ": " + badProp[1]);
  }

  if (onPass) {
    try {
      onPass(mainWindow);
    }
    catch(e) {
      throw "Failed to execute 'onPass' callback: " + e;
    }
  }
};

/**
 * Asserts that the +expected+ object matches the +given+ object by making
 * assertions appropriate based on the type of each property in the
 * +expected+ object. This method will recurse through the structure,
 * applying assertions for each matching property path. See the description
 * for +assertWindow+ for details on the matchers.
 */
function assertPropertiesMatch(expected, given, level) {
  for (var propName in expected) {
    if (propName.match(/~iphone$/) && UIATarget.localTarget().model().match(/^iPhone/) === null) {
      return;
    }
    else if (propName.match(/~ipad$/) && UIATarget.localTarget().model().match(/^iPad/) === null) {
      return;
    }

    if (expected.hasOwnProperty(propName)) {
      try {
        var expectedProp = expected[propName];
        var givenProp = given[propName];

        if (typeof(givenProp) == "function") {
          try {
            // We have to use eval (shudder) because calling functions on
            // UIAutomation objects with () operator crashes
            // See Radar bug 8496138
            givenProp = eval("given." + propName + "()");
          }
          catch (e) {
            UIALogger.logDebug("Unable to evaluate given." + propName + "() against " + given);
            continue;
          }
        }

        // null indicates we don't care to match
        if (expectedProp == null) {
          continue;
        }

        if (givenProp == null) {
          throw propName;
        }

        if (typeof(expectedProp) == "string") {
          assertEquals(expectedProp, givenProp);
        }
        else if (typeof(expectedProp) == "number") {
          assertEquals(expectedProp, givenProp);
        }
        else if (typeof(expectedProp) == "function") {
          if (expectedProp.constructor == RegExp) {
            assertMatch(expectedProp, givenProp);
          }
          else {
            expectedProp(givenProp);
          }
        }
        else if (typeof(expectedProp) == "object") {
          if (expectedProp.constructor == Array) {
            assertEquals(expectedProp.length, givenProp.length, "Incorrect number of elements in array");
            for (var i = 0; i < expectedProp.length; i++) {
              var exp = expectedProp[i];
              var giv = givenProp[i];
              assertPropertiesMatch(exp, giv, level + 1);
            };
          }
          else if (typeof(givenProp) == "object") {
            assertPropertiesMatch(expectedProp, givenProp, level + 1);
          }
          else {
            throw propName;
          }
        }
      }
      catch(e) {
        if (e.constructor == Array) {
          e[0] = propName + "." + e[0];
          throw e;
        }
        else {
          var err = [propName, e];
          throw err;
        }
      }
    }
  }
};

<|endoftext|>


<commit_before>const EventEmitter = require("events").EventEmitter;
const request = require("request");
const mainResolver = require("../modules/audioResolvers/main.js");

class Player extends EventEmitter {
	constructor(guild, data) {
		super();
		this.id = guild.id;
		this.guild = guild;

		this.queue = data.queue || [];
		this.repeat = !!data.repeat;
		this.channel = data.channel || null;
		bot.players.set(this.id, this);
		handlePlayer(this);
	}

	async addQueue(data) {
		if(!this.connection) return false;
		if(this.queue.length >= 2000) {
			let donator = (await r.table("donators").filter({ id: this.guild.ownerID }))[0];
			if(!donator) return __("modules.player.maxQueue", this.guild);
		}

		if(Array.isArray(data)) this.queue = this.queue.concat(data);
		else if(typeof data === "object") this.queue.push(data);
		if(!this.current) this.play();

		if(this.queue.length >= 2000) {
			let donator = (await r.table("donators").filter({ id: this.guild.ownerID }).run())[0];
			if(!donator) {
				this.queue = this.queue.slice(0, (donator ? 10000 : 2000) - 1);
				if(!donator) return __("modules.player.cutOffQueue", this.guild);
			}
		}
		return true;
	}

	async connect(channelID) {
		if(this.connection) return false;

		let connection = this.connection = await bot.joinVoiceChannel(channelID);
		updateStreamCount();

		connection.on("error", err => this.emit("error", err));
		connection.once("disconnect", () => {
			connection.removeAllListeners();
			this.queue = [];
			delete this.connection;
			updateStreamCount();
			this.destroyTimeout = setTimeout(() => this.destroy("inactivity"), 600000);
		});

		if(connection.ready) return true;
		else return new Promise((resolve, reject) => connection.once("ready", resolve(true)));
	}

	destroy(reason) {
		let connection = this.connection;
		if(connection) connection.disconnect();
		bot.players.delete(this.id);

		this.emit("destroy", reason);
		delete this;
		updateStreamCount();
	}

	async play() {
		clearTimeout(this.destroyTimeout);
		let connection = this.connection;
		if(!connection) return;
		else if(this.current && connection.playing) return;

		let song = this.queue[0];
		if(!song && !this.current) {
			this.destroy("no_queue");
			return;
		} else if(!song) {
			setTimeout(() => this.play(), 100);
			return;
		}

		if(this.queue.length > 1) this.queue.shift();
		else this.queue = [];

		if(!song.stream) song = await mainResolver.extract(song);
		if(!song.stream) {
			this.emit("error", new Error(__("modules.player.extractionError", this.guild)));
			this.play();
			return;
		} else if(typeof song.stream === "string" && song.stream === "NO_VALID_FORMATS") {
			this.emit("error", new Error(__("modules.player.noFormats", this.guild)));
			this.play();
			return;
		} else if(typeof song.stream === "string" && song.stream.startsWith("ERROR:")) {
			this.emit("error", song.stream);
			this.play();
			return;
		}

		let volume;
		if(song.live) volume = 1;
		else volume = 0.3;
		let options = {};

		if(song.opus) {
			options.format = "webm";
			options.frameDuration = 20;
		} else {
			options.encoderArgs = ["-af", `volume=${volume}`];
			options.inputArgs = ["-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "2"];
		}
		this.connection.play(song.stream, options);
		this.current = song;
		this.emit("playing", song);
		this.connection.once("end", () => {
			if(this.repeat) this.queue.push(this.current);
			if(this.queue.length === 0) this.destroy("no_queue");
			else setTimeout(() => this.play(), 100);
		});
		if(this.queue[0] && !this.queue[0].stream) this.queue[0] = await mainResolver.extract(this.queue[0]);
	}

	voiceCheck(member) {
		if(!member.voiceState || !member.voiceState.channelID || !this.connection) return false;
		else return member.voiceState.channelID === this.connection.channelID;
	}
}
module.exports = Player;

async function updateStreamCount() {
	let streams = (await process.output({
		type: "globalEval",
		input: () => Array.from(bot.players.values()).filter(player => player.connection).length
	})).results.reduce((a, b) => a + b);
	statsd({ type: "gauge", stat: "streams", value: streams });
}

function handlePlayer(player) {
	let createMessage = async embed => {
		if(!player.channel) return;
		let announcements = (await r.table("settings").filter({ guildID: player.id, name: "musicmessages" }).run())[0];
		if(announcements && announcements.value) return;

		let listening = player.guild.channels.get(player.connection.channelID).voiceMembers
			.filter(member => !member.bot && !member.voiceState.selfDeaf).length;
		if(listening >= 1) player.channel.createMessage({ embed });
	};

	player.on("playing", async song => {
		let embed = {
			description: `**${song.title}**`,
			image: { url: song.thumbnail },
			footer: { text: `ID: ${song.id} | ${__("words.service", this.guild)}: ${song.service}` },
			title: `▶ ${__("phrases.nowPlaying", this.guild)}`
		};
		if(song.duration && !isNaN(song.duration)) embed.description += ` (${bot.utils.secondsToDuration(song.duration)})`;
		createMessage(embed);
	});

	player.on("error", async err => {
		createMessage({
			color: 0xF1C40F,
			description: err.stack || err.message,
			title: `⚠ ${__("phrases.recievedError", this.guild)}`
		});
	});
}
<commit_msg>music stabilization<commit_after>const EventEmitter = require("events").EventEmitter;
const request = require("request");
const mainResolver = require("../modules/audioResolvers/main.js");

class Player extends EventEmitter {
	constructor(guild, data) {
		super();
		this.id = guild.id;
		this.guild = guild;

		this.queue = data.queue || [];
		this.repeat = !!data.repeat;
		this.channel = data.channel || null;
		bot.players.set(this.id, this);
		handlePlayer(this);
	}

	async addQueue(data) {
		if(!this.connection) return false;
		if(this.queue.length >= 2000) {
			let donator = (await r.table("donators").filter({ id: this.guild.ownerID }))[0];
			if(!donator) return __("modules.player.maxQueue", this.guild);
		}

		if(Array.isArray(data)) this.queue = this.queue.concat(data);
		else if(typeof data === "object") this.queue.push(data);
		if(!this.current) this.play();

		if(this.queue.length >= 2000) {
			let donator = (await r.table("donators").filter({ id: this.guild.ownerID }).run())[0];
			if(!donator) {
				this.queue = this.queue.slice(0, (donator ? 10000 : 2000) - 1);
				if(!donator) return __("modules.player.cutOffQueue", this.guild);
			}
		}
		return true;
	}

	async connect(channelID) {
		if(this.connection) return false;

		let connection = this.connection = await bot.joinVoiceChannel(channelID);
		updateStreamCount();

		connection.on("error", err => this.emit("error", err));
		connection.once("disconnect", () => {
			connection.removeAllListeners();
			this.queue = [];
			delete this.connection;
			updateStreamCount();
			this.destroyTimeout = setTimeout(() => this.destroy("inactivity"), 600000);
		});

		if(connection.ready) return true;
		else return new Promise((resolve, reject) => connection.once("ready", resolve(true)));
	}

	destroy(reason) {
		let connection = this.connection;
		if(connection) connection.disconnect();
		bot.players.delete(this.id);

		this.emit("destroy", reason);
		delete this;
		updateStreamCount();
	}

	async play() {
		clearTimeout(this.destroyTimeout);
		let connection = this.connection;
		if(!connection) return;
		else if(this.current && connection.playing) return;
		else if(!this.current && connection.playing) connection.stopPlaying();
		else if(!connection.playing && this.current) delete this.current;

		let song = this.queue[0];
		if(!song && !this.current) {
			this.destroy("no_queue");
			return;
		} else if(!song) {
			setTimeout(() => this.play(), 100);
			return;
		}

		if(this.queue.length > 1) this.queue.shift();
		else this.queue = [];

		if(!song.stream) song = await mainResolver.extract(song);
		if(!song.stream) {
			this.emit("error", new Error(__("modules.player.extractionError", this.guild)));
			this.play();
			return;
		} else if(typeof song.stream === "string" && song.stream === "NO_VALID_FORMATS") {
			this.emit("error", new Error(__("modules.player.noFormats", this.guild)));
			this.play();
			return;
		} else if(typeof song.stream === "string" && song.stream.startsWith("ERROR:")) {
			this.emit("error", song.stream);
			this.play();
			return;
		}

		let volume;
		if(song.live) volume = 1;
		else volume = 0.3;
		let options = {};

		// if(song.opus) {
		// 	options.format = "webm";
		// 	options.frameDuration = 20;
		// } else {
		// 	options.encoderArgs = ["-af", `volume=${volume}`];
		// 	options.inputArgs = ["-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "2"];
		// }
		// CPU Usage doesn't matter that much to me :)

		options.encoderArgs = ["-af", `volume=${volume}`];
		options.inputArgs = ["-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "2"];

		this.connection.play(song.stream, options);
		this.current = song;
		this.emit("playing", song);
		this.connection.once("end", () => {
			if(this.repeat) this.queue.push(this.current);
			if(this.queue.length === 0) this.destroy("no_queue");
			else setTimeout(() => this.play(), 100);
		});
		if(this.queue[0] && !this.queue[0].stream) this.queue[0] = await mainResolver.extract(this.queue[0]);
	}

	voiceCheck(member) {
		if(!member.voiceState || !member.voiceState.channelID || !this.connection) return false;
		else return member.voiceState.channelID === this.connection.channelID;
	}
}
module.exports = Player;

async function updateStreamCount() {
	let streams = (await process.output({
		type: "globalEval",
		input: () => Array.from(bot.players.values()).filter(player => player.connection).length
	})).results.reduce((a, b) => a + b);
	statsd({ type: "gauge", stat: "streams", value: streams });
}

function handlePlayer(player) {
	let createMessage = async embed => {
		if(!player.channel) return;
		let announcements = (await r.table("settings").filter({ guildID: player.id, name: "musicmessages" }).run())[0];
		if(announcements && announcements.value) return;

		let listening = player.guild.channels.get(player.connection.channelID).voiceMembers
			.filter(member => !member.bot && !member.voiceState.selfDeaf).length;
		if(listening >= 1) player.channel.createMessage({ embed });
	};

	player.on("playing", async song => {
		let embed = {
			description: `**${song.title}**`,
			image: { url: song.thumbnail },
			footer: { text: `ID: ${song.id} | ${__("words.service", this.guild)}: ${song.service}` },
			title: `▶ ${__("phrases.nowPlaying", this.guild)}`
		};
		if(song.duration && !isNaN(song.duration)) embed.description += ` (${bot.utils.secondsToDuration(song.duration)})`;
		createMessage(embed);
	});

	player.on("error", async err => {
		createMessage({
			color: 0xF1C40F,
			description: err.stack || err.message,
			title: `⚠ ${__("phrases.recievedError", this.guild)}`
		});
	});
}
<|endoftext|>


<commit_before>const Game = require('./game');
const FlashTower = require('./flash-tower');
const SimpleTower = require('./simple-tower');
const HeavyDamageTower = require('./heavy-damage-tower');
const ContinuousFireTower = require('./continuous-fire-tower');
const ImageLoader = require('./image-loader');
const SpriteLoader = require('./sprite-loader');
const AnimationEngine = require('./animation-engine');
const $ = require('jquery');

let game = new Game();
let currentTile = null;
let lastTile = null;
let imageLoader, spriteLoader, images, sprites;
let refSimpleTower, refFlashTower, refHeavyDamageTower, refContinuousFireTower;
let mouseDown = false;
let selectedTower = null;
let mouseEnter = false;
let hoverTower = null;

let canvas = document.getElementById('game-canvas');
let ctx = canvas.getContext('2d');
let simpleTowerButton = document.getElementById('simple-tower');
let flashTowerButton = document.getElementById('flash-tower');
let heavyDamageTowerButton = document.getElementById('heavy-damage-tower');
let continuousFireTowerButton = document.getElementById('continuous-fire-tower');
let startButton = document.getElementById('start');
let fastFowardButton = document.getElementById('ff');
let resumeButton = document.getElementById('play');
let numberOfLives = document.getElementById('number-of-lives');
let moneyAmount = document.getElementById('money-amount');

let towerButtons = $('.tower-button');
let $winScreen = $('#win-screen');
let $loseScreen = $('#lose-screen');
let $fastFowardButton = $('#fast-foward-button');
let $startButton = $('#start-button');
let $nextDifficulty = $('#next-difficulty');
let $nextLevel = $('#next-level');
let $replay = $('#replay');
let $replayLose = $('#replay-lose');
let $sellButton = $('#sell-button');

$(document).mousedown(function() {
  if (eventIsOnCanvas(event)) { mouseDown = true; }
}).mouseup(function() {
  if (eventIsOnCanvas(event)) { mouseDown = false; }
});

$('#simple-tower, #heavy-damage-tower, #continuous-fire-tower, #flash-tower').mouseenter(function() {
  mouseEnter = true;
  hoverTower = createTowerType(this.id);
}).mouseleave(function() {
  mouseEnter = false;
  hoverTower = null;
});

function eventIsOnCanvas(event) {
  let eventPosition = getPosition(event);
  return eventPosition.x >= 0 && eventPosition.x <= 800 &&
         eventPosition.y >= 0 && eventPosition.y <= 500;
}

loadImages();
loadSprites();
createRefTowers();

function loadImages() {
  imageLoader = new ImageLoader();
  images = imageLoader.init(game.currentLevel.stage);
}

function loadSprites() {
  spriteLoader = new SpriteLoader();
  sprites = spriteLoader.sprites;
}

function drawBoard(ctx) {
  for (let { type, x, y } of game.board.tiles) {
    ctx.drawImage(images[type], x, y);
  }
}

function drawEnemies(ctx) {
  for (let enemy of game.retrieveAliveEnemies()) {
    ctx.drawImage(sprites[enemy.type].currentFrame, enemy.x, enemy.y);
  }
}

function drawAnimations(ctx) {
  for (let animation of AnimationEngine.animations) {
    routeAnimation(animation, ctx);
  }
}

function routeAnimation (a, ctx) {
  if (a.nature === 'fire') {
    if(a.tower.isFlashTower()) {
      ctx.drawImage(a.currentFrame, a.tower.x - (a.tower.range / 2), a.tower.y - (a.tower.range / 2));
    } else {
      rotateAndDrawImage(ctx, a);
    }
  } else if (a.nature === 'hit' && a.enemy !== null) {
    if (a.tower.isHeavyDamageTower()) {
      ctx.drawImage(a.currentFrame, a.enemy.x - 5, a.enemy.y - 30);
    } else {
      ctx.drawImage(a.currentFrame, a.enemy.x, a.enemy.y - 11);
    }
  }
}

function rotateAndDrawImage (ctx, animation) {
  let angleInRad = Math.atan2(-((animation.enemy.x + 25) - animation.tower.x),
                              (animation.enemy.y + 25) - animation.tower.y);
  ctx.translate(animation.tower.x, animation.tower.y);
  ctx.rotate(angleInRad);
  ctx.drawImage(animation.currentFrame, -25, 25);
  ctx.rotate(-angleInRad);
  ctx.translate(-animation.tower.x, - animation.tower.y);
}

function drawTowers(ctx) {
  for (let tile of game.getTowerTiles()) {
    ctx.drawImage(sprites[tile.tower.type].currentFrame, tile.x, tile.y);
  }
}

function updateNumberOfLives() { numberOfLives.innerHTML = game.lives; }

function updateMoneyAmount() { moneyAmount.innerHTML = game.monies; }

function updateAnimations() { AnimationEngine.updateAnimations(); }

function createRefTowers() {
  refSimpleTower = new SimpleTower({ x: 0, y:0 });
  refFlashTower = new FlashTower({ x: 0, y:0 });
  refHeavyDamageTower = new HeavyDamageTower({ x: 0, y:0 });
  refContinuousFireTower = new ContinuousFireTower({ x: 0, y:0 });
}

let towerIdTranslator = {
  'simple-tower': refSimpleTower,
  'flash-tower': refFlashTower,
  'heavy-damage-tower': refHeavyDamageTower,
  'continuous-fire-tower': refContinuousFireTower
};

function getTowerPrice($towerButton) {
  return towerIdTranslator[$towerButton.attr('id')].price;
}

function updateTowerButtons() {
  for (let i = 0; i < towerButtons.length; i ++) {
    let $towerButton = $(towerButtons[i]);
    let towerPrice = getTowerPrice($towerButton);
    let towerButtonState = $towerButton.attr('class').split(' ')[1];

    if (game.canAffordTower(towerPrice) && towerButtonState === 'inactive') {
      $towerButton.addClass('active');
      $towerButton.removeClass('inactive');
    } else if (!game.canAffordTower(towerPrice) && towerButtonState === 'active') {
      $towerButton.removeClass('active');
      $towerButton.addClass('inactive');
    }
  }
}

function fireTowers() {
  let shotEvents = [];
  game.getTowers().forEach(tower => shotEvents.push(tower.shoot(game.retrieveAliveEnemiesOnBoard())));
  triggerAnimations(shotEvents);
}

function triggerAnimations(shootEvents) {
  shootEvents.filter(event => event !== undefined)
             .forEach(event => AnimationEngine.triggerAnimation(event));
}

function nextTick() {
  spriteLoader.updateSprites();
  updateTowerButtons();
  game.removeEnemiesOffRight();
  updateNumberOfLives();
  game.redirectEnemies();
  game.retrieveAliveEnemies().forEach(enemy => enemy.move());
  fireTowers();
  updateAnimations();
  game.rewardMonies();
  updateMoneyAmount();
}

// Start game loop - before enemies are called
requestAnimationFrame(startLoop);
let gameNotStarted = true;

function startLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  updateTowerButtons();
  updateNumberOfLives();
  updateMoneyAmount();
  drawBoard(ctx);
  drawTowers(ctx);
  if (mouseDown) { drawTowerRange(ctx); }
  if (mouseEnter) { simulateTowerRange(ctx); }
  if (gameNotStarted) { requestAnimationFrame(startLoop); }
}

//  Main game loop
function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBoard(ctx);
  drawEnemies(ctx);
  drawTowers(ctx);
  drawAnimations(ctx);
  if (mouseDown) { drawTowerRange(ctx); }
  if (mouseEnter) { simulateTowerRange(ctx); }
  nextTick();

  if (game.checkStatus() === 'ongoing') {
    requestAnimationFrame(gameLoop);
  } else if (game.checkStatus() === 'lose') {
    requestAnimationFrame(gameLose);
  } else if (game.checkStatus() === 'win') {
    requestAnimationFrame(gameWin);
  }
}

function gameLose() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  $(canvas).hide();
  $fastFowardButton.hide();
  $loseScreen.show();
}

function gameWin() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  $(canvas).hide();
  $fastFowardButton.hide();
  $winScreen.show();
  showStars();

  if (game.currentLevel.difficulty === 3) {
    $nextDifficulty.hide();
  } else {
    $nextDifficulty.show();
  }
}

function showStars() {
  let stars = game.determineNumberOfStars();
  for (var i = 1; i <= stars; i++) { $('#star-' + i).show(); }
}

function hideAllStars() {
  for (var i = 1; i <= 3; i++) { $('#star-' + i).hide(); }
}

function getPosition(event) {
  return {
    x: event.pageX - canvas.offsetLeft,
    y: event.pageY - canvas.offsetTop
  };
}

function selectTile(event) {
  let clickPosition = getPosition(event);
  lastTile = currentTile;
  currentTile = game.board.getClickedTile(clickPosition.x, clickPosition.y);

  if (tileIsABuildTile(currentTile) && (tileIsAHighlightedBuildTile(lastTile) || tileIsASelectedBuildTile(lastTile)) && currentTile.vacant) {
    lastTile.type = 'buildTile';
    currentTile.type = 'buildTileHighlighted';
    $('#sell-button').hide();
  } else if ((tileIsAHighlightedBuildTile(lastTile) || tileIsASelectedBuildTile(lastTile)) && !tileIsABuildTile(currentTile)) {
    lastTile.type = 'buildTile';
    $('#sell-button').hide();
  } else if (tileIsABuildTile(currentTile) && currentTile.vacant) {
    currentTile.type = 'buildTileHighlighted';
    $('#sell-button').hide();
  } else if (tileIsABuildTile(currentTile)) {
    currentTile.type = 'buildTileSelected';
    if (tileIsAHighlightedBuildTile(lastTile)) { lastTile.type = 'buildTile'; }
    $('#sell-button').show();
  }
}

function assignOrSelectTower() {
  let tower = selectedTower || createTowerType(this.id);

  if (!tileIsAHighlightedBuildTile(currentTile)) {
    selectedTower = createTowerType(this.id);
  } else if (tileIsAHighlightedBuildTile(currentTile) && tower && canAffordTower(tower.price)) {
    tower.x = currentTile.centerX();
    tower.y = currentTile.centerY();
    currentTile.addTower(tower);
    currentTile.type = 'buildTile';
    selectedTower = null;
    game.monies -= tower.price;
  }
}

function createTowerType(id) {
  switch (id) {
    case 'simple-tower':
      return new SimpleTower({x: 0, y:0 });
    case 'flash-tower':
      return new FlashTower({x: 0, y:0 });
    case 'heavy-damage-tower':
      return new HeavyDamageTower({x: 0, y:0 });
    case 'continuous-fire-tower':
      return new ContinuousFireTower({x: 0, y:0 });
  }
}

function canAffordTower(price) {
  return game.monies >= price;
}

function tileIsABuildTile(tile) {
  return tile && tile.type === 'buildTile';
}

function tileIsAHighlightedBuildTile(tile) {
  return tile && tile.type === 'buildTileHighlighted';
}

function tileIsASelectedBuildTile(tile) {
  return tile && tile.type === 'buildTileSelected';
}

function tileHasATower(tile) {
  return tile.tower !== undefined;
}

function drawTowerRange(ctx) {
  if (tileIsABuildTile(currentTile) && tileHasATower(currentTile)) {
    ctx.beginPath();
    ctx.arc(currentTile.centerX(), currentTile.centerY(),
            currentTile.tower.range, 0, 2 * Math.PI, false);
    ctx.stroke();
  }
}

function simulateTowerRange(ctx) {
  if (tileIsAHighlightedBuildTile(currentTile)) {
    ctx.beginPath();
    ctx.arc(currentTile.centerX(), currentTile.centerY(),
            hoverTower.range, 0, 2 * Math.PI, false);
    ctx.stroke();
  }
}

function moveToNextLevel() {
  resetScreen();
  game.loadNextLevel();
  loadImages();
  game.refreshGameObjectValues();
  requestAnimationFrame(startLoop);
}

function moveToNextDifficulty() {
  resetScreen();
  game.loadNextDifficulty();
  game.refreshGameObjectValues();
  requestAnimationFrame(startLoop);
}

function resetLevel() {
  resetScreen();
  game.resetLevel();
  game.refreshGameObjectValues();
  requestAnimationFrame(startLoop);
}

function resetScreen() {
  hideAllStars();
  $winScreen.hide();
  $loseScreen.hide();
  $(canvas).show();
  resetParameters();
  resetButtons();
}

function resetParameters() {
  gameNotStarted = true;
  currentTile = null;
  lastTile = null;
}

canvas.addEventListener("mousedown", selectTile);
canvas.addEventListener("mousedown", assignOrSelectTower);
simpleTowerButton.addEventListener("click", assignOrSelectTower);
flashTowerButton.addEventListener("click", assignOrSelectTower);
heavyDamageTowerButton.addEventListener("click", assignOrSelectTower);
continuousFireTowerButton.addEventListener("click", assignOrSelectTower);
startButton.addEventListener("click", function() {
  gameNotStarted = false;
  replaceStartButtonWithFastFoward();
  requestAnimationFrame(gameLoop);
});

$sellButton.on("click", function() {
  game.monies += (currentTile.tower.price / 2);
  currentTile.tower = null;
  currentTile.vacant = true;
  currentTile.type = 'buildTile';
  $sellButton.hide();
});

fastFowardButton.addEventListener('click', function() {
  game.fastFoward();
});

resumeButton.addEventListener('click', function() {
  game.refreshGameObjectValues();
});

function replaceStartButtonWithFastFoward() {
  $startButton.hide();
  $fastFowardButton.show();
}

function resetButtons() {
  $startButton.show();
  $fastFowardButton.hide();
}

$nextLevel.click(moveToNextLevel);
$nextDifficulty.click(moveToNextDifficulty);
$replay.click(resetLevel);
$replayLose.click(resetLevel);
<commit_msg>Get all of the engine methods to use the reference towers instead of createTowerType<commit_after>const Game = require('./game');
const FlashTower = require('./flash-tower');
const SimpleTower = require('./simple-tower');
const HeavyDamageTower = require('./heavy-damage-tower');
const ContinuousFireTower = require('./continuous-fire-tower');
const ImageLoader = require('./image-loader');
const SpriteLoader = require('./sprite-loader');
const AnimationEngine = require('./animation-engine');
const $ = require('jquery');

let game = new Game();
let currentTile = null;
let lastTile = null;
let imageLoader, spriteLoader, images, sprites;
let refSimpleTower, refFlashTower, refHeavyDamageTower, refContinuousFireTower;
let mouseDown = false;
let selectedTower = null;
let mouseEnter = false;
let hoverTower = null;

let canvas = document.getElementById('game-canvas');
let ctx = canvas.getContext('2d');
let simpleTowerButton = document.getElementById('simple-tower');
let flashTowerButton = document.getElementById('flash-tower');
let heavyDamageTowerButton = document.getElementById('heavy-damage-tower');
let continuousFireTowerButton = document.getElementById('continuous-fire-tower');
let startButton = document.getElementById('start');
let fastFowardButton = document.getElementById('ff');
let resumeButton = document.getElementById('play');
let numberOfLives = document.getElementById('number-of-lives');
let moneyAmount = document.getElementById('money-amount');

let towerButtons = $('.tower-button');
let $winScreen = $('#win-screen');
let $loseScreen = $('#lose-screen');
let $fastFowardButton = $('#fast-foward-button');
let $startButton = $('#start-button');
let $nextDifficulty = $('#next-difficulty');
let $nextLevel = $('#next-level');
let $replay = $('#replay');
let $replayLose = $('#replay-lose');
let $sellButton = $('#sell-button');

$(document).mousedown(function() {
  if (eventIsOnCanvas(event)) { mouseDown = true; }
}).mouseup(function() {
  if (eventIsOnCanvas(event)) { mouseDown = false; }
});

$('#simple-tower, #heavy-damage-tower, #continuous-fire-tower, #flash-tower').mouseenter(function() {
  mouseEnter = true;
  hoverTower = towerIdTranslator[this.id];
}).mouseleave(function() {
  mouseEnter = false;
  hoverTower = null;
});

function eventIsOnCanvas(event) {
  let eventPosition = getPosition(event);
  return eventPosition.x >= 0 && eventPosition.x <= 800 &&
         eventPosition.y >= 0 && eventPosition.y <= 500;
}

loadImages();
loadSprites();
createRefTowers();

function loadImages() {
  imageLoader = new ImageLoader();
  images = imageLoader.init(game.currentLevel.stage);
}

function loadSprites() {
  spriteLoader = new SpriteLoader();
  sprites = spriteLoader.sprites;
}

function drawBoard(ctx) {
  for (let { type, x, y } of game.board.tiles) {
    ctx.drawImage(images[type], x, y);
  }
}

function drawEnemies(ctx) {
  for (let enemy of game.retrieveAliveEnemies()) {
    ctx.drawImage(sprites[enemy.type].currentFrame, enemy.x, enemy.y);
  }
}

function drawAnimations(ctx) {
  for (let animation of AnimationEngine.animations) {
    routeAnimation(animation, ctx);
  }
}

function routeAnimation (a, ctx) {
  if (a.nature === 'fire') {
    if(a.tower.isFlashTower()) {
      ctx.drawImage(a.currentFrame, a.tower.x - (a.tower.range / 2), a.tower.y - (a.tower.range / 2));
    } else {
      rotateAndDrawImage(ctx, a);
    }
  } else if (a.nature === 'hit' && a.enemy !== null) {
    if (a.tower.isHeavyDamageTower()) {
      ctx.drawImage(a.currentFrame, a.enemy.x - 5, a.enemy.y - 30);
    } else {
      ctx.drawImage(a.currentFrame, a.enemy.x, a.enemy.y - 11);
    }
  }
}

function rotateAndDrawImage (ctx, animation) {
  let angleInRad = Math.atan2(-((animation.enemy.x + 25) - animation.tower.x),
                              (animation.enemy.y + 25) - animation.tower.y);
  ctx.translate(animation.tower.x, animation.tower.y);
  ctx.rotate(angleInRad);
  ctx.drawImage(animation.currentFrame, -25, 25);
  ctx.rotate(-angleInRad);
  ctx.translate(-animation.tower.x, - animation.tower.y);
}

function drawTowers(ctx) {
  for (let tile of game.getTowerTiles()) {
    ctx.drawImage(sprites[tile.tower.type].currentFrame, tile.x, tile.y);
  }
}

function updateNumberOfLives() { numberOfLives.innerHTML = game.lives; }

function updateMoneyAmount() { moneyAmount.innerHTML = game.monies; }

function updateAnimations() { AnimationEngine.updateAnimations(); }

function createRefTowers() {
  refSimpleTower = new SimpleTower({ x: 0, y:0 });
  refFlashTower = new FlashTower({ x: 0, y:0 });
  refHeavyDamageTower = new HeavyDamageTower({ x: 0, y:0 });
  refContinuousFireTower = new ContinuousFireTower({ x: 0, y:0 });
}

let towerIdTranslator = {
  'simple-tower': refSimpleTower,
  'flash-tower': refFlashTower,
  'heavy-damage-tower': refHeavyDamageTower,
  'continuous-fire-tower': refContinuousFireTower
};

function getTowerPrice($towerButton) {
  return towerIdTranslator[$towerButton.attr('id')].price;
}

function updateTowerButtons() {
  for (let i = 0; i < towerButtons.length; i ++) {
    let $towerButton = $(towerButtons[i]);
    let towerPrice = getTowerPrice($towerButton);
    let towerButtonState = $towerButton.attr('class').split(' ')[1];

    if (game.canAffordTower(towerPrice) && towerButtonState === 'inactive') {
      $towerButton.addClass('active');
      $towerButton.removeClass('inactive');
    } else if (!game.canAffordTower(towerPrice) && towerButtonState === 'active') {
      $towerButton.removeClass('active');
      $towerButton.addClass('inactive');
    }
  }
}

function fireTowers() {
  let shotEvents = [];
  game.getTowers().forEach(tower => shotEvents.push(tower.shoot(game.retrieveAliveEnemiesOnBoard())));
  triggerAnimations(shotEvents);
}

function triggerAnimations(shootEvents) {
  shootEvents.filter(event => event !== undefined)
             .forEach(event => AnimationEngine.triggerAnimation(event));
}

function nextTick() {
  spriteLoader.updateSprites();
  updateTowerButtons();
  game.removeEnemiesOffRight();
  updateNumberOfLives();
  game.redirectEnemies();
  game.retrieveAliveEnemies().forEach(enemy => enemy.move());
  fireTowers();
  updateAnimations();
  game.rewardMonies();
  updateMoneyAmount();
}

// Start game loop - before enemies are called
requestAnimationFrame(startLoop);
let gameNotStarted = true;

function startLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  updateTowerButtons();
  updateNumberOfLives();
  updateMoneyAmount();
  drawBoard(ctx);
  drawTowers(ctx);
  if (mouseDown) { drawTowerRange(ctx); }
  if (mouseEnter) { simulateTowerRange(ctx); }
  if (gameNotStarted) { requestAnimationFrame(startLoop); }
}

//  Main game loop
function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBoard(ctx);
  drawEnemies(ctx);
  drawTowers(ctx);
  drawAnimations(ctx);
  if (mouseDown) { drawTowerRange(ctx); }
  if (mouseEnter) { simulateTowerRange(ctx); }
  nextTick();

  if (game.checkStatus() === 'ongoing') {
    requestAnimationFrame(gameLoop);
  } else if (game.checkStatus() === 'lose') {
    requestAnimationFrame(gameLose);
  } else if (game.checkStatus() === 'win') {
    requestAnimationFrame(gameWin);
  }
}

function gameLose() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  $(canvas).hide();
  $fastFowardButton.hide();
  $loseScreen.show();
}

function gameWin() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  $(canvas).hide();
  $fastFowardButton.hide();
  $winScreen.show();
  showStars();

  if (game.currentLevel.difficulty === 3) {
    $nextDifficulty.hide();
  } else {
    $nextDifficulty.show();
  }
}

function showStars() {
  let stars = game.determineNumberOfStars();
  for (var i = 1; i <= stars; i++) { $('#star-' + i).show(); }
}

function hideAllStars() {
  for (var i = 1; i <= 3; i++) { $('#star-' + i).hide(); }
}

function getPosition(event) {
  return {
    x: event.pageX - canvas.offsetLeft,
    y: event.pageY - canvas.offsetTop
  };
}

function selectTile(event) {
  let clickPosition = getPosition(event);
  lastTile = currentTile;
  currentTile = game.board.getClickedTile(clickPosition.x, clickPosition.y);

  if (tileIsABuildTile(currentTile) && (tileIsAHighlightedBuildTile(lastTile) || tileIsASelectedBuildTile(lastTile)) && currentTile.vacant) {
    lastTile.type = 'buildTile';
    currentTile.type = 'buildTileHighlighted';
    $('#sell-button').hide();
  } else if ((tileIsAHighlightedBuildTile(lastTile) || tileIsASelectedBuildTile(lastTile)) && !tileIsABuildTile(currentTile)) {
    lastTile.type = 'buildTile';
    $('#sell-button').hide();
  } else if (tileIsABuildTile(currentTile) && currentTile.vacant) {
    currentTile.type = 'buildTileHighlighted';
    $('#sell-button').hide();
  } else if (tileIsABuildTile(currentTile)) {
    currentTile.type = 'buildTileSelected';
    if (tileIsAHighlightedBuildTile(lastTile)) { lastTile.type = 'buildTile'; }
    $('#sell-button').show();
  }
}

function assignOrSelectTower() {
  let tower = selectedTower || towerIdTranslator[this.id];

  if (!tileIsAHighlightedBuildTile(currentTile)) {
    selectedTower = towerIdTranslator[this.id];
  } else if (tileIsAHighlightedBuildTile(currentTile) && tower && canAffordTower(tower.price)) {
    tower.x = currentTile.centerX();
    tower.y = currentTile.centerY();
    currentTile.addTower(tower);
    currentTile.type = 'buildTile';
    selectedTower = null;
    game.monies -= tower.price;
  }
}

function canAffordTower(price) {
  return game.monies >= price;
}

function tileIsABuildTile(tile) {
  return tile && tile.type === 'buildTile';
}

function tileIsAHighlightedBuildTile(tile) {
  return tile && tile.type === 'buildTileHighlighted';
}

function tileIsASelectedBuildTile(tile) {
  return tile && tile.type === 'buildTileSelected';
}

function tileHasATower(tile) {
  return tile.tower !== undefined;
}

function drawTowerRange(ctx) {
  if (tileIsABuildTile(currentTile) && tileHasATower(currentTile)) {
    ctx.beginPath();
    ctx.arc(currentTile.centerX(), currentTile.centerY(),
            currentTile.tower.range, 0, 2 * Math.PI, false);
    ctx.stroke();
  }
}

function simulateTowerRange(ctx) {
  if (tileIsAHighlightedBuildTile(currentTile)) {
    ctx.beginPath();
    ctx.arc(currentTile.centerX(), currentTile.centerY(),
            hoverTower.range, 0, 2 * Math.PI, false);
    ctx.stroke();
  }
}

function moveToNextLevel() {
  resetScreen();
  game.loadNextLevel();
  loadImages();
  game.refreshGameObjectValues();
  requestAnimationFrame(startLoop);
}

function moveToNextDifficulty() {
  resetScreen();
  game.loadNextDifficulty();
  game.refreshGameObjectValues();
  requestAnimationFrame(startLoop);
}

function resetLevel() {
  resetScreen();
  game.resetLevel();
  game.refreshGameObjectValues();
  requestAnimationFrame(startLoop);
}

function resetScreen() {
  hideAllStars();
  $winScreen.hide();
  $loseScreen.hide();
  $(canvas).show();
  resetParameters();
  resetButtons();
}

function resetParameters() {
  gameNotStarted = true;
  currentTile = null;
  lastTile = null;
}

canvas.addEventListener("mousedown", selectTile);
canvas.addEventListener("mousedown", assignOrSelectTower);
simpleTowerButton.addEventListener("click", assignOrSelectTower);
flashTowerButton.addEventListener("click", assignOrSelectTower);
heavyDamageTowerButton.addEventListener("click", assignOrSelectTower);
continuousFireTowerButton.addEventListener("click", assignOrSelectTower);
startButton.addEventListener("click", function() {
  gameNotStarted = false;
  replaceStartButtonWithFastFoward();
  requestAnimationFrame(gameLoop);
});

$sellButton.on("click", function() {
  game.monies += (currentTile.tower.price / 2);
  currentTile.tower = null;
  currentTile.vacant = true;
  currentTile.type = 'buildTile';
  $sellButton.hide();
});

fastFowardButton.addEventListener('click', function() {
  game.fastFoward();
});

resumeButton.addEventListener('click', function() {
  game.refreshGameObjectValues();
});

function replaceStartButtonWithFastFoward() {
  $startButton.hide();
  $fastFowardButton.show();
}

function resetButtons() {
  $startButton.show();
  $fastFowardButton.hide();
}

$nextLevel.click(moveToNextLevel);
$nextDifficulty.click(moveToNextDifficulty);
$replay.click(resetLevel);
$replayLose.click(resetLevel);
<|endoftext|>


<commit_before>// By http://ejohn.org/blog/fast-javascript-maxmin/
// Convert to prototype functions?

Array.max = function(arr){
	return Math.max.apply(Math, arr);
};

Array.min = function(arr){
	return Math.min.apply(Math, arr);
};

String.prototype.contains = Array.prototype.contains = function(sub) {
	return ~this.indexOf(sub);
}

String.prototype.repeat = function(nr) {
	return Array(nr).join(this);
}

Number.prototype.pad = function(nr) {
	return "0".repeat(nr) + this).slice(-nr);
}

<commit_msg>Added hyphenate() and fix for setTimeout/setInterval<commit_after>// By http://ejohn.org/blog/fast-javascript-maxmin/
// Convert to prototype functions?

Array.max = function(arr){
	return Math.max.apply(Math, arr);
};

Array.min = function(arr){
	return Math.min.apply(Math, arr);
};

String.prototype.contains = Array.prototype.contains = function(sub) {
	return ~this.indexOf(sub);
};

String.prototype.repeat = function(nr) {
	return Array(nr).join(this);
};

Number.prototype.pad = function(nr) {
	return "0".repeat(nr) + this).slice(-nr);
};

// Via: http://es5.github.com/#x15.5.4.11
String.prototype.hyphenate = function() {
	return this.replace(/[A-Z]/g,'-$&').toLowerCase();
};

// Adds support for extra parameters for setInterval/setTimeout for browsers missing it
// Via: http://webreflection.blogspot.com/2011/05/settimeout-and-setinterval-with-extra.html
// Only missing in IE browsers
setTimeout(function(one) {
	// only if not supported ...
	if (one) {
		return;
	}

	var slice = [].slice;

	// trap original versions
	var Timeout = setTimeout;
	var Interval = setInterval;

	// create a delegate
	var delegate = function (callback, args) {
		args = slice.call(args, 2);
		return function() {
			callback.apply(null, args);
		};
	};

	// redefine original versions
	setTimeout = function (callback, delay) {
		return Timeout(delegate(callback, arguments), delay);
	};

	setInterval = function (callback, delay) {
		return Interval(delegate(callback, arguments), delay);
	};
}, 0, 1);
<|endoftext|>


<commit_before>function each(obj,cb){
    for(var key in obj){
        if(obj.hasOwnProperty(key)){
            cb.call(obj, obj[key], key)
        }
    }
}

function normalize(s){
    return s.split('_').map(function(val,ind){
        return capitalize(val)
    }).join('')
}

function capitalize(s){
    return s[0].toUpperCase() + s.substr(1).toLowerCase()
}

function without(val,array){
    return array.filter(function(v){return v != val})
}

module.exports = {
    normalize : normalize,
    capitalize : capitalize,
    without : without,
    each : each
}
<commit_msg>Without supports arrays Added countTokens<commit_after>function each(obj,cb){
    for(var key in obj){
        if(obj.hasOwnProperty(key)){
            cb.call(obj, obj[key], key)
        }
    }
}

function normalize(s){
    return s.split('_').map(function(val,ind){
        return capitalize(val)
    }).join('')
}

function capitalize(s){
    return s[0].toUpperCase() + s.substr(1).toLowerCase()
}

function without(val,array){
    if(!val.filter) val = [val]
    return array.filter(function(v){return array.indexOf(val) > -1})
}

function countTokens(string){
    var res = string.match(/%[jsd]/g)
    return res ? res.length : 0
}

module.exports = {
    normalize : normalize,
    capitalize : capitalize,
    without : without,
    each : each,
    countTokens : countTokens
}
<|endoftext|>


<commit_before>const datePickerStyle = {

  // wrapper of entire component
  style: {
    borderRadius: 2,
    width: 267,
    textAlign: 'center',
    boxSizing: 'border-box',
    backgroundColor: 'white',

    /*
     To avoid any kind of flickering the user won't get feedback
     for selecting the button text
     */
    WebkitUserSelect: 'none',
    MozUserSelect: 'none',
    MsUserSelect: 'none',
    userSelect: 'none',

    MsTouchAction: 'manipulation',
    touchAction: 'manipulation',

    /*
     Prevent flickering while tapping on WebKit
     http://stackoverflow.com/a/3516243/837709
     */
    WebkitTapHighlightColor: 'transparent',

    transition: 'color 0.1s',
    transitionTimingFunction: 'ease-out',
  },

  disabledStyle: {
    backgroundColor: '#D8D8D8',
  },

  readOnlyStyle: {
  },

  hoverStyle: {
  },

  focusStyle: {
    boxShadow: '0 0 0 2px rgba(140, 224, 255, 0.6) inset',
    outline: 0,
  },

  disabledHoverStyle: {
    backgroundColor: '#E1E9EC',
  },

  // nav-bar at top for month navigation
  navBarStyle: {
    height: 30,
    lineHeight: '30px',
    border: '1px solid #ccc',
  },

  // left button in nav-bar to go to previous month
  prevMonthNavStyle: {
    cursor: 'pointer',
    float: 'left',

    /*
     To avoid any kind of flickering the user won't get feedback
     for selecting the button text
     */
    WebkitUserSelect: 'none',
    MozUserSelect: 'none',
    MsUserSelect: 'none',
    userSelect: 'none',

    /* This button can only be pressed */
    MsTouchAction: 'manipulation',
    touchAction: 'manipulation',

    /*
     Prevent flickering while tapping on WebKit
     http://stackoverflow.com/a/3516243/837709
     */
    WebkitTapHighlightColor: 'transparent',
  },

  hoverPrevMonthNavStyle: {
  },

  activePrevMonthNavStyle: {
  },

  // right button in nav-bar to go to previous month
  nextMonthNavStyle: {
    cursor: 'pointer',
    float: 'right',

    /*
     To avoid any kind of flickering the user won't get feedback
     for selecting the button text
     */
    WebkitUserSelect: 'none',
    MozUserSelect: 'none',
    MsUserSelect: 'none',
    userSelect: 'none',

    /* This button can only be pressed */
    MsTouchAction: 'manipulation',
    touchAction: 'manipulation',

    /*
     Prevent flickering while tapping on WebKit
     http://stackoverflow.com/a/3516243/837709
     */
    WebkitTapHighlightColor: 'transparent',
  },

  hoverNextMonthNavStyle: {
  },

  activeNextMonthNavStyle: {
  },

  // styling for month label on top of calendar
  monthLabelStyle: {
    fontSize: 15,
    width: 125,
    boxSizing: 'border-box',

    /*
     User should be able to copy date.
     */
    WebkitUserSelect: 'initial',
    MozUserSelect: 'initial',
    MsUserSelect: 'initial',
    userSelect: 'initial',
  },

  // styling for entire grid of week-header and weeks
  weekGroupStyle: {
    boxSizing: 'border-box',
    overflow: 'auto',
    paddingBottom: 1,
  },

  weekHeaderStyle: {
    overflow: 'auto',
    boxSizing: 'border-box',
    boxShadow: '1px 0 0 0 #EEE inset, -1px 0 0 0 #EEE inset',
  },

  // styling for week's day label
  dayLabelStyle: {
    width: 39,
    height: 30,
    padding: 0,
    marginRight: -1,
    lineHeight: '30px',
    color: '#716D6D',
    display: 'block',
    float: 'left',
    boxSizing: 'border-box',

    /*
     User should be able to copy date.
     */
    WebkitUserSelect: 'initial',
    MozUserSelect: 'initial',
    MsUserSelect: 'initial',
    userSelect: 'initial',
  },

  disabledDayLabelStyle: {
    color: '#C1BABA',
  },

  weekendLabelStyle: {
    backgroundColor: '#F0F0F0',
  },

  // styling for individual day
  dayStyle: {
    width: 39,
    height: 30,
    lineHeight: '30px',
    cursor: 'pointer',
    border: '1px solid #EEE',
    color: '#716D6D',
    display: 'block',
    float: 'left',
    padding: 0,
    marginRight: -1,
    marginBottom: -1,
    boxSizing: 'border-box',
    position: 'relative',
    zIndex: 100,

    /*
     User should be able to copy date.
     */
    WebkitUserSelect: 'initial',
    MozUserSelect: 'initial',
    MsUserSelect: 'initial',
    userSelect: 'initial',

    /* This button can only be pressed */
    MsTouchAction: 'manipulation',
    touchAction: 'manipulation',

    /*
     Prevent flickering while tapping on WebKit
     http://stackoverflow.com/a/3516243/837709
     */
    WebkitTapHighlightColor: 'transparent',

    transition: 'color 0.1s',
    transitionTimingFunction: 'ease-out',
  },

  disabledDayStyle: {
    color: '#C1BABA',
    cursor: 'auto',
  },

  readOnlyDayStyle: {
    cursor: 'auto',
  },

  hoverDayStyle: {
    border: '1px solid #EEE',
    background: '#EEE',
    color: '#616161',
  },

  activeDayStyle: {
    boxShadow: 'none',
    border: '1px solid #EEE',
    borderTop: '1px solid #BDBDBD',
    background: '#E0E0E0',
    color: '#424242',
  },

  focusDayStyle: {
    border: '1px solid rgba(140, 224, 255, 0.8)',
    zIndex: 200,
  },

  disabledHoverDayStyle: {
    cursor: 'auto',
  },

  todayStyle: {
    color: '#2C87A9',
  },

  weekendStyle: {
    backgroundColor: '#F0F0F0',
  },

  selectedDayStyle: {
    boxShadow: 'none',
    border: '1px solid #EEE',
    borderTop: '1px solid #BDBDBD',
    background: '#E0E0E0',
    color: '#424242',
  },

  otherMonthDayStyle: {
    color: '#BDBDBD',
  },
};

export default datePickerStyle;
<commit_msg>style(DatePicker): align border-color<commit_after>const datePickerStyle = {

  // wrapper of entire component
  style: {
    borderRadius: 2,
    width: 267,
    textAlign: 'center',
    boxSizing: 'border-box',
    backgroundColor: 'white',

    /*
     To avoid any kind of flickering the user won't get feedback
     for selecting the button text
     */
    WebkitUserSelect: 'none',
    MozUserSelect: 'none',
    MsUserSelect: 'none',
    userSelect: 'none',

    MsTouchAction: 'manipulation',
    touchAction: 'manipulation',

    /*
     Prevent flickering while tapping on WebKit
     http://stackoverflow.com/a/3516243/837709
     */
    WebkitTapHighlightColor: 'transparent',

    transition: 'color 0.1s',
    transitionTimingFunction: 'ease-out',
  },

  disabledStyle: {
    backgroundColor: '#D8D8D8',
  },

  readOnlyStyle: {
  },

  hoverStyle: {
  },

  focusStyle: {
    boxShadow: '0 0 0 2px rgba(140, 224, 255, 0.6) inset',
    outline: 0,
  },

  disabledHoverStyle: {
    backgroundColor: '#E1E9EC',
  },

  // nav-bar at top for month navigation
  navBarStyle: {
    height: 30,
    lineHeight: '30px',
    border: '1px solid #E0E0E0',
  },

  // left button in nav-bar to go to previous month
  prevMonthNavStyle: {
    cursor: 'pointer',
    float: 'left',

    /*
     To avoid any kind of flickering the user won't get feedback
     for selecting the button text
     */
    WebkitUserSelect: 'none',
    MozUserSelect: 'none',
    MsUserSelect: 'none',
    userSelect: 'none',

    /* This button can only be pressed */
    MsTouchAction: 'manipulation',
    touchAction: 'manipulation',

    /*
     Prevent flickering while tapping on WebKit
     http://stackoverflow.com/a/3516243/837709
     */
    WebkitTapHighlightColor: 'transparent',
  },

  hoverPrevMonthNavStyle: {
  },

  activePrevMonthNavStyle: {
  },

  // right button in nav-bar to go to previous month
  nextMonthNavStyle: {
    cursor: 'pointer',
    float: 'right',

    /*
     To avoid any kind of flickering the user won't get feedback
     for selecting the button text
     */
    WebkitUserSelect: 'none',
    MozUserSelect: 'none',
    MsUserSelect: 'none',
    userSelect: 'none',

    /* This button can only be pressed */
    MsTouchAction: 'manipulation',
    touchAction: 'manipulation',

    /*
     Prevent flickering while tapping on WebKit
     http://stackoverflow.com/a/3516243/837709
     */
    WebkitTapHighlightColor: 'transparent',
  },

  hoverNextMonthNavStyle: {
  },

  activeNextMonthNavStyle: {
  },

  // styling for month label on top of calendar
  monthLabelStyle: {
    fontSize: 15,
    width: 125,
    boxSizing: 'border-box',

    /*
     User should be able to copy date.
     */
    WebkitUserSelect: 'initial',
    MozUserSelect: 'initial',
    MsUserSelect: 'initial',
    userSelect: 'initial',
  },

  // styling for entire grid of week-header and weeks
  weekGroupStyle: {
    boxSizing: 'border-box',
    overflow: 'auto',
    paddingBottom: 1,
  },

  weekHeaderStyle: {
    overflow: 'auto',
    boxSizing: 'border-box',
    boxShadow: '1px 0 0 0 #E0E0E0 inset, -1px 0 0 0 #E0E0E0 inset',
  },

  // styling for week's day label
  dayLabelStyle: {
    width: 39,
    height: 30,
    padding: 0,
    marginRight: -1,
    lineHeight: '30px',
    color: '#716D6D',
    display: 'block',
    float: 'left',
    boxSizing: 'border-box',

    /*
     User should be able to copy date.
     */
    WebkitUserSelect: 'initial',
    MozUserSelect: 'initial',
    MsUserSelect: 'initial',
    userSelect: 'initial',
  },

  disabledDayLabelStyle: {
    color: '#C1BABA',
  },

  weekendLabelStyle: {
    backgroundColor: '#F0F0F0',
  },

  // styling for individual day
  dayStyle: {
    width: 39,
    height: 30,
    lineHeight: '30px',
    cursor: 'pointer',
    border: '1px solid #E0E0E0',
    color: '#716D6D',
    display: 'block',
    float: 'left',
    padding: 0,
    marginRight: -1,
    marginBottom: -1,
    boxSizing: 'border-box',
    position: 'relative',
    zIndex: 100,

    /*
     User should be able to copy date.
     */
    WebkitUserSelect: 'initial',
    MozUserSelect: 'initial',
    MsUserSelect: 'initial',
    userSelect: 'initial',

    /* This button can only be pressed */
    MsTouchAction: 'manipulation',
    touchAction: 'manipulation',

    /*
     Prevent flickering while tapping on WebKit
     http://stackoverflow.com/a/3516243/837709
     */
    WebkitTapHighlightColor: 'transparent',

    transition: 'color 0.1s',
    transitionTimingFunction: 'ease-out',
  },

  disabledDayStyle: {
    color: '#C1BABA',
    cursor: 'auto',
  },

  readOnlyDayStyle: {
    cursor: 'auto',
  },

  hoverDayStyle: {
    border: '1px solid #E0E0E0',
    background: '#E0E0E0',
    color: '#616161',
  },

  activeDayStyle: {
    boxShadow: 'none',
    border: '1px solid #E0E0E0',
    borderTop: '1px solid #BDBDBD',
    background: '#E0E0E0',
    color: '#424242',
  },

  focusDayStyle: {
    border: '1px solid rgba(140, 224, 255, 0.8)',
    zIndex: 200,
  },

  disabledHoverDayStyle: {
    cursor: 'auto',
  },

  todayStyle: {
    color: '#2C87A9',
  },

  weekendStyle: {
    backgroundColor: '#F0F0F0',
  },

  selectedDayStyle: {
    boxShadow: 'none',
    border: '1px solid #E0E0E0',
    borderTop: '1px solid #BDBDBD',
    background: '#E0E0E0',
    color: '#424242',
  },

  otherMonthDayStyle: {
    color: '#BDBDBD',
  },
};

export default datePickerStyle;
<|endoftext|>


<commit_before>var
  redis = require("redis")
  , _ = require('underscore')._
  , neuron = require('neuron')
  , zlib = require('zlib')
  , winston = require('./logging').winston
  , oauth = require("./oauth")
  , oauth2 = require('./oauth2')
;

require('date-utils');

// Connect to redis
var client = redis.createClient(GLOBAL.config.redis.port, GLOBAL.config.redis.address);
if (GLOBAL.config.redis.auth) {
	client.auth(GLOBAL.config.redis.auth, function (err) {
		if (err) {
			// handle err;
		}
	});
}

//
// ## Recursive function to remove unwanted elements from API response
//
var filter = function( source, map) {
	if (_.isArray(source)) {
		_.each(source, function(item, index) { filter( item, map[0]); });
	} else {
		if (_.isString(source) || map === true || _.isUndefined(map)) return 0;
		_.each(source, function(obj, key, source) {
			if (_.isUndefined(map[key])) {
				delete source[key];
			} else {
				filter( obj, map[key]);
			}
		});
	};
}

//
// ## Perform scheduled refresh
//
exports.refresh = function(api, key, bid, bundle) {

	winston.info('exports.refresh: ' + api);
	// We're forcing a refresh of the content so run the api.code
	api.resource( api.params, api.credentials, function( err, res ) {
		if ( err ) {

			// We got an error so set our output object to be the error and expire immediately
			api.expires = ( new Date() );
  			var tout = {
  				expires: api.expires,
  				result: err,
  				iid: bid+key,
  				cname: key,
  				scheduled: true
  			};

		} else {

  			winston.event('Get data for ' + bid + ' from ' + key + ', ' + res.size);

  			// Perform cleanup function on API response
  			if (_.has(api, 'cleanup')) {
	  			res = api.cleanup(res);
  			}

			// Filter the response
  			if (_.has(api, 'filter')) {
	  			filter ( res, api.filter );
  			}

			// Build the stored response
  			api.expires = ( new Date() ).addSeconds( api.cacheduration );
  			bundle[key] = api;

  			var tout = {
  				expires: api.expires,
  				result: res,
  				iid: api.iid,
  				cname: key,
  				scheduled: true
  			};

	   		// Save response to Redis
	   		client.set(bid+key, JSON.stringify(tout));

	   		// Update the bundle
	   		client.del('bid'+bid);
		}
  	});

}

//
// ## Retrieve the requested bundle
//
exports.fulfill = function ( myRes, ip, bid, callback, gzip, override ) {

	winston.info('exports.fulfill: ' + bid);

	var bundle = GLOBAL.bundles[bid];

	if (!callback && bundle && bundle.callback) {
		callback = bundle.callback;
	}

	// If the user requested a bundle that is not defined
	if (typeof bundle === 'undefined') {
		myRes.writeHead(404);
		myRes.end();
		return false;
	}

	// Count the number of queries in this bundle so we know when we are ready to respond
	var queriesInThisBundle = _.size(bundle),
		thisResponse = {};

	// cleanup is not an API request
	if(_.has(bundle, 'cleanup')) {
		queriesInThisBundle--;
	}

	// callback is not an API request
	if(_.has(bundle, 'callback')) {
		queriesInThisBundle--;
	}

	// If override was not passed
	if( _.isUndefined( override )) {

		// Retrieve bundle response from Redis
		client.get('bid'+bid, function ( err, doc ) {

			if ( err || doc === null ) {
				// There was an error so force refresh on bundle
				exports.fulfill( myRes, ip, bid, callback, gzip, true );

			} else {

				jDoc = JSON.parse( doc );

				// If there is a valid expiration date for the bundle
				jDoc.expires = new Date(jDoc.expires);
				if ( 'expires' in jDoc && _.isDate(jDoc.expires) ) {
					jDoc.secleft = jDoc.expires.getSecondsBetween( new Date() ) * -1;
				} else {
					// This should never happen
					jDoc.secleft = 0;
				}

				if (Number(jDoc.secleft) < 0 ) {

					// The bundle has expired. Force a refresh
					exports.fulfill( myRes, ip, bid, callback, gzip, true );

				} else {

					//Respond with the cached data
					jDoc.fromcache = true;
					var responseType = callback ? 'application/javascript' : 'application/json';
					var responseHeaders = {'Content-Type': responseType, 'vary': 'Accept-Encoding', 'max-age': jDoc.secleft, 'cache-control': 'public, max-age='+jDoc.secleft+', no-transform', "Expires": jDoc.expires };
					doc = JSON.stringify(jDoc);

					// If a callback name was passed, use it. Otherwise, just output the object
					var tbuf = new Buffer(  ( !callback ) ? doc : callback + '('  + doc +  ');');

					if (gzip) {

						zlib.gzip(tbuf.toString(), function(err, zbuf) {
						  if (!err) {
						    winston.event('Send gzipped response for ' + bid +', ' + zbuf.toString().length + ', ' + ip);
						    myRes.writeHead(200, {'Content-Type': responseType, 'vary': 'Accept-Encoding', 'max-age': jDoc.secleft, 'cache-control': 'public, max-age='+jDoc.secleft+', no-transform', "Expires": jDoc.expires, 'content-encoding': 'gzip' });
						    myRes.end(zbuf);
						  }
						});
					} else {
						myRes.writeHead(200, responseHeaders);
						winston.event('Send response for ' + bid +', ' + doc.length + ', ' + ip);
						myRes.end(tbuf);
					}

				}
			}
		});

	} else {

		// ### Override was passed so we are forcing a refresh on the bundle
		var manager = new neuron.JobManager();

		manager.on('empty', function (job) {

			winston.info('manager:empty');

			if (job.name === 'finishRequest' && queriesInThisBundle === 0) {
				var done = true;

				_.each(manager.job, function(job, key) {
					if (_.size(job.waiting)  !== 0 || _.size(job.running) !== 0 || job.queue.length !== 0) {
        				done = false;
        			}
        		});

        		if (done) manager.enqueue('composeResponse', bid);
			}
		});

		manager.addJob('fulfillPart', {
			work: function(api, bid, key, override, cachedPart) {

				winston.info('manager:fulfillPart: ' + bid + '.' + key + ', override: '+override);

				var self = this;

				if ( _.isUndefined( override ) ) {

					// Load the cached api response from Redis
			  		client.get(bid+key, function (err, doc) {
			  			if (err || doc === null){
			  				self.finished = true;
			  				manager.enqueue('fulfillPart', api, bid, key, true );
			  			} else {

				  			doc = JSON.parse( doc );
				  			doc.expires = new Date(doc.expires);
				  			if ( ('expires' in doc) && _.isDate(doc.expires) ) {
				  				var secleft = doc.expires.getSecondsBetween( new Date() ) * -1;
				  			}
				  			if (secleft < 0) {
				  				self.finished = true;
				  				manager.enqueue('fulfillPart', api, bid, key, true, doc );
				  			} else {
				  				doc.fromcache = true;
				  				manager.enqueue('finishRequest', doc );
				  				self.finished = true;
				  			}
				  		}
				  	});

				} else {

					if (_.has( api, 'auth')) {

						winston.info('Bundle uses auth type ' + api.auth.type);
						// If the API request object has an auth scheme defined
						if (api.auth.type == 'oauth') {
							oauth.authorize (api, bid, key, function( result, authParams ) {
								if (result === true) {
									api.params = _.extend(api.params, authParams);
									manager.enqueue('startRequest', api, key, cachedPart, bid);
								} else {
									manager.enqueue('finishRequest', result );
								}
								self.finished = true;
							});
						} else if (api.auth.type == 'oauth2') {
							oauth2.authorize (api, bid, key, function( result, authParams ) {
								if (result === true) {
									api.params = _.extend(api.params, authParams);
									manager.enqueue('startRequest', api, key, cachedPart, bid);
								} else {
									manager.enqueue('finishRequest', result );
								}
								self.finished = true;
							});
						} else {
							winston.error('auth type ' + api.auth.type + ' not recognized');
						}
					} else {
						// Authentication is not needed
						self.finished = true;
						manager.enqueue('startRequest', api, key, cachedPart, bid);
					}

				}

			}
		});

		manager.addJob('startRequest', {
			work: function( api, key, cachedPart, bid ) {

				winston.info('manager:startRequest: ' + key);

				var self = this;

				if (_.has( api, 'timeout') && _.isObject(cachedPart)) {
					self.timeout = setTimeout(function(self) {
						if(_.isObject(cachedPart)) {
							cachedPart.timeout = true;
							cachedPart.fromcache = true;
						} else {
							cachedPart= {
								"cname": key,
								"timeout" : true,
								"fromcache" : false
							};
						}
						manager.enqueue('finishRequest', cachedPart );
						self.finished = true;
					}, api.timeout, self)
				}

				api.resource( api.params, api.credentials, function( err, res ) {
		  			clearTimeout(self.timeout)
		  			delete self.timeout;

		  			if ( err ) {

		  				api.expires = ( new Date() );
			  			tout = {};
			  			tout.cname = key;
			  			tout.expires = api.expires;
			  			tout.result =  _.isUndefined(cachedPart) ? {} : cachedPart;
			  			tout.fromcache = true;
			  			tout.err = err;
			  			winston.error('Problem retrieving data for ' + bid + ' from ' + key + ': ' + JSON.stringify(err));

		  			} else {

			  			winston.event('Get data for ' + bid + ' from ' + key + ', ' + res.size);

			  			// Perform cleanup function on API response
			  			if (_.has(api, 'cleanup')) {
				  			res = api.cleanup(res);
			  			}

			  			// Filter the response
			  			if (_.has(api, 'filter')) {
				  			filter ( res, api.filter );
			  			}

			  			// Build the stored response
			  			api.expires = ( new Date() ).addSeconds( api.cacheduration );
			  			bundle[key] = api;
			  			//client.set('bundle'+bid, JSON.stringify(bundle));
			  			var tout = {
			  				expires: api.expires,
			  				result: res,
			  				iid: api.iid,
			  				cname: key
			  			};

				   		// Save response to Redis
				   		client.set(bid+key, JSON.stringify(tout));
				   	}
					manager.enqueue('finishRequest', tout );
	  				self.finished = true;
			  	});
			}
		})

		manager.addJob('finishRequest', {
			work: function(apiResponse) {

				winston.info('manager:finishRequest');

				queriesInThisBundle--;

				if (_.has(apiResponse, 'redirect')) thisResponse.redirect = apiResponse.redirect;
				thisResponse[apiResponse.cname] = apiResponse;

			  	this.finished = true;
			}
		});

		manager.addJob('composeResponse', {
			work: function() {

				winston.info('manager:composeResponse');

				// Update the expiration date on the bundle
				var tout = {
					expires: _.min( thisResponse, function( val ) { return val.expires } ).expires,
				};

				if (_.has( thisResponse, 'redirect')) tout.redirect = thisResponse.redirect;

				// Insert api responses into bundle
				_.each( thisResponse, function( val, idx ) {
					tout[val.cname] = val;
				});

				// Perform cleanup function on bundle
			  	if (_.has(bundle, 'cleanup')) {
				  	tout = bundle.cleanup(tout);
				}

				// Save cached bundle in Redis
				client.set('bid'+bid, JSON.stringify(tout));

				// Determine the seconds left before expiry
				if ( 'expires' in tout && _.isDate(tout.expires) ) {
					tout.secleft = tout.expires.getSecondsBetween( new Date() ) * -1;
				} else {
					tout.secleft = 3600;
				}

				manager.enqueue('sendResponse', tout);
				this.finished = true;

			}
		});

		manager.addJob('sendResponse', {
			work: function(doc) {

				winston.info('manager:sendResponse');

				if (_.has(doc, 'redirect')) {
					myRes.statusCode = 302;
					myRes.setHeader("Location", doc.redirect);
					myRes.end();
					this.finished = true;
				} else {

					// Send the results

					var responseType = callback ? 'application/javascript' : 'application/json';
					var responseHeaders = {'Content-Type': responseType, 'vary': 'Accept-Encoding', 'max-age': doc.secleft, 'cache-control': 'public, max-age='+doc.secleft+', no-transform', "Expires": doc.expires };

					// If a callback name was passed, use it. Otherwise, just output the object
					var tbuf = new Buffer(  ( !callback ) ? JSON.stringify( doc ) : callback + '('  + JSON.stringify( doc ) +  ');');

					if (gzip) {

						zlib.gzip(tbuf.toString(), function(err, zbuf) {
						  if (!err) {
						    winston.event('Send gzipped response for ' + bid +', ' + zbuf.toString().length + ', ' + ip);
						    myRes.writeHead(200, {'Content-Type': responseType, 'max-age': doc.secleft, 'cache-control': 'public, max-age='+doc.secleft+', no-transform', "Expires": doc.expires, 'content-encoding': 'gzip' });
						    myRes.end(zbuf);
						    this.finished = true;
						  }
						});
					} else {
						winston.event('Send response for ' + bid +', ' + tbuf.length + ', ' + ip);
						myRes.writeHead(200, responseHeaders);
						myRes.end(tbuf);
						this.finished = true;
					}
				}
			}
		});

		_.each( bundle, function( api, key ) {
			if (key !== 'cleanup' && key !== 'callback') {
				if (_.isUndefined(api, 'credentials')) {
					api.credentials = {};
				}
				manager.enqueue('fulfillPart', api, bid, key);
			}
		});

	}
}<commit_msg>Improve some comments and add lastModified value to bundle<commit_after>var
  redis = require("redis")
  , _ = require('underscore')._
  , neuron = require('neuron')
  , zlib = require('zlib')
  , winston = require('./logging').winston
  , oauth = require("./oauth")
  , oauth2 = require('./oauth2')
;

require('date-utils');

// Connect to redis
var client = redis.createClient(GLOBAL.config.redis.port, GLOBAL.config.redis.address);
if (GLOBAL.config.redis.auth) {
	client.auth(GLOBAL.config.redis.auth, function (err) {
		if (err) {
			// handle err;
		}
	});
}

//
// ## Recursive function to remove unwanted elements from API response
//
var filter = function( source, map) {
	if (_.isArray(source)) {
		_.each(source, function(item, index) { filter( item, map[0]); });
	} else {
		if (_.isString(source) || map === true || _.isUndefined(map)) return 0;
		_.each(source, function(obj, key, source) {
			if (_.isUndefined(map[key])) {
				delete source[key];
			} else {
				filter( obj, map[key]);
			}
		});
	};
}

//
// ## Perform scheduled refresh
//
exports.refresh = function(api, key, bid, bundle) {

	winston.info('exports.refresh: ' + api);
	// We're forcing a refresh of the content so run the api.code
	api.resource( api.params, api.credentials, function( err, res ) {
		if ( err ) {

			// We got an error so set our output object to be the error and expire immediately
			api.expires = ( new Date() );
  			var tout = {
  				expires: api.expires,
  				result: err,
  				iid: bid+key,
  				cname: key,
  				scheduled: true
  			};

		} else {

  			winston.event('Get data for ' + bid + ' from ' + key + ', ' + res.size);

  			// Perform cleanup function on API response
  			if (_.has(api, 'cleanup')) {
	  			res = api.cleanup(res);
  			}

			// Filter the response
  			if (_.has(api, 'filter')) {
	  			filter ( res, api.filter );
  			}

			// Build the stored response
  			api.expires = ( new Date() ).addSeconds( api.cacheduration );
  			bundle[key] = api;

  			var tout = {
  				expires: api.expires,
  				result: res,
  				iid: api.iid,
  				cname: key,
  				scheduled: true
  			};

	   		// Save the API response to Redis
	   		client.set(bid+key, JSON.stringify(tout));

	   		// Delete the cached bundle. It will be rebuilt the next time a user requests it.
	   		client.del('bid'+bid);
		}
  	});

}

//
// ## Retrieve the requested bundle
//
exports.fulfill = function ( myRes, ip, bid, callback, gzip, override ) {

	winston.info('exports.fulfill: ' + bid);

	var bundle = GLOBAL.bundles[bid];

	if (!callback && bundle && bundle.callback) {
		callback = bundle.callback;
	}

	// If the user requested a bundle that is not defined
	if (typeof bundle === 'undefined') {
		myRes.writeHead(404);
		myRes.end();
		return false;
	}

	// Count the number of queries in this bundle so we know when we are ready to respond
	var queriesInThisBundle = _.size(bundle),
		thisResponse = {};

	// cleanup is not an API request
	if(_.has(bundle, 'cleanup')) {
		queriesInThisBundle--;
	}

	// callback is not an API request
	if(_.has(bundle, 'callback')) {
		queriesInThisBundle--;
	}

	// If override was not passed
	if( _.isUndefined( override )) {

		// Retrieve bundle response from Redis
		client.get('bid'+bid, function ( err, doc ) {

			if ( err || doc === null ) {
				// There was an error so force refresh on bundle
				exports.fulfill( myRes, ip, bid, callback, gzip, true );

			} else {

				jDoc = JSON.parse( doc );

				// If there is a valid expiration date for the bundle
				jDoc.expires = new Date(jDoc.expires);
				if ( 'expires' in jDoc && _.isDate(jDoc.expires) ) {
					jDoc.secleft = jDoc.expires.getSecondsBetween( new Date() ) * -1;
				} else {
					// This should never happen
					jDoc.secleft = 0;
				}

				if (Number(jDoc.secleft) < 0 ) {

					// The bundle has expired. Force a refresh
					exports.fulfill( myRes, ip, bid, callback, gzip, true );

				} else {

					//Respond with the cached data
					jDoc.fromcache = true;
					var responseType = callback ? 'application/javascript' : 'application/json';
					var responseHeaders = {'Content-Type': responseType, 'vary': 'Accept-Encoding', 'max-age': jDoc.secleft, 'cache-control': 'public, max-age='+jDoc.secleft+', no-transform', "Expires": jDoc.expires };
					doc = JSON.stringify(jDoc);

					// If a callback name was passed, use it. Otherwise, just output the object
					var tbuf = new Buffer(  ( !callback ) ? doc : callback + '('  + doc +  ');');

					if (gzip) {

						zlib.gzip(tbuf.toString(), function(err, zbuf) {
						  if (!err) {
						    winston.event('Send gzipped response for ' + bid +', ' + zbuf.toString().length + ', ' + ip);
						    myRes.writeHead(200, {'Content-Type': responseType, 'vary': 'Accept-Encoding', 'max-age': jDoc.secleft, 'cache-control': 'public, max-age='+jDoc.secleft+', no-transform', "Expires": jDoc.expires, 'content-encoding': 'gzip' });
						    myRes.end(zbuf);
						  }
						});
					} else {
						myRes.writeHead(200, responseHeaders);
						winston.event('Send response for ' + bid +', ' + doc.length + ', ' + ip);
						myRes.end(tbuf);
					}

				}
			}
		});

	} else {

		// ### Override was passed so we are forcing a refresh on the bundle
		var manager = new neuron.JobManager();

		manager.on('empty', function (job) {

			winston.info('manager:empty');

			if (job.name === 'finishRequest' && queriesInThisBundle === 0) {
				var done = true;

				_.each(manager.job, function(job, key) {
					if (_.size(job.waiting)  !== 0 || _.size(job.running) !== 0 || job.queue.length !== 0) {
        				done = false;
        			}
        		});

        		if (done) manager.enqueue('composeResponse', bid);
			}
		});

		manager.addJob('fulfillPart', {
			work: function(api, bid, key, override, cachedPart) {

				winston.info('manager:fulfillPart: ' + bid + '.' + key + ', override: '+override);

				var self = this;

				if ( _.isUndefined( override ) ) {

					// Load the cached api response from Redis
			  		client.get(bid+key, function (err, doc) {
			  			if (err || doc === null){
			  				self.finished = true;
			  				manager.enqueue('fulfillPart', api, bid, key, true );
			  			} else {

				  			doc = JSON.parse( doc );
				  			doc.expires = new Date(doc.expires);
				  			if ( ('expires' in doc) && _.isDate(doc.expires) ) {
				  				var secleft = doc.expires.getSecondsBetween( new Date() ) * -1;
				  			}
				  			if (secleft < 0) {
				  				self.finished = true;
				  				manager.enqueue('fulfillPart', api, bid, key, true, doc );
				  			} else {
				  				doc.fromcache = true;
				  				manager.enqueue('finishRequest', doc );
				  				self.finished = true;
				  			}
				  		}
				  	});

				} else {

					if (_.has( api, 'auth')) {

						winston.info('Bundle uses auth type ' + api.auth.type);
						// If the API request object has an auth scheme defined
						if (api.auth.type == 'oauth') {
							oauth.authorize (api, bid, key, function( result, authParams ) {
								if (result === true) {
									api.params = _.extend(api.params, authParams);
									manager.enqueue('startRequest', api, key, cachedPart, bid);
								} else {
									manager.enqueue('finishRequest', result );
								}
								self.finished = true;
							});
						} else if (api.auth.type == 'oauth2') {
							oauth2.authorize (api, bid, key, function( result, authParams ) {
								if (result === true) {
									api.params = _.extend(api.params, authParams);
									manager.enqueue('startRequest', api, key, cachedPart, bid);
								} else {
									manager.enqueue('finishRequest', result );
								}
								self.finished = true;
							});
						} else {
							winston.error('auth type ' + api.auth.type + ' not recognized');
						}
					} else {
						// Authentication is not needed
						self.finished = true;
						manager.enqueue('startRequest', api, key, cachedPart, bid);
					}

				}

			}
		});

		manager.addJob('startRequest', {
			work: function( api, key, cachedPart, bid ) {

				winston.info('manager:startRequest: ' + key);

				var self = this;

				if (_.has( api, 'timeout') && _.isObject(cachedPart)) {
					self.timeout = setTimeout(function(self) {
						if(_.isObject(cachedPart)) {
							cachedPart.timeout = true;
							cachedPart.fromcache = true;
						} else {
							cachedPart= {
								"cname": key,
								"timeout" : true,
								"fromcache" : false
							};
						}
						manager.enqueue('finishRequest', cachedPart );
						self.finished = true;
					}, api.timeout, self)
				}

				api.resource( api.params, api.credentials, function( err, res ) {
		  			clearTimeout(self.timeout)
		  			delete self.timeout;

		  			if ( err ) {

		  				api.expires = ( new Date() );
			  			tout = {};
			  			tout.cname = key;
			  			tout.expires = api.expires;
			  			tout.result =  _.isUndefined(cachedPart) ? {} : cachedPart;
			  			tout.fromcache = true;
			  			tout.err = err;
			  			winston.error('Problem retrieving data for ' + bid + ' from ' + key + ': ' + JSON.stringify(err));

		  			} else {

			  			winston.event('Get data for ' + bid + ' from ' + key + ', ' + res.size);

			  			// Perform cleanup function on API response
			  			if (_.has(api, 'cleanup')) {
				  			res = api.cleanup(res);
			  			}

			  			// Filter the response
			  			if (_.has(api, 'filter')) {
				  			filter ( res, api.filter );
			  			}

			  			// Build the stored response
			  			api.expires = ( new Date() ).addSeconds( api.cacheduration );
			  			bundle[key] = api;
			  			//client.set('bundle'+bid, JSON.stringify(bundle));
			  			var tout = {
			  				expires: api.expires,
			  				result: res,
			  				iid: api.iid,
			  				cname: key
			  			};

				   		// Save the API response to Redis
				   		client.set(bid+key, JSON.stringify(tout));
				   	}
					manager.enqueue('finishRequest', tout );
	  				self.finished = true;
			  	});
			}
		})

		manager.addJob('finishRequest', {
			work: function(apiResponse) {

				winston.info('manager:finishRequest');

				queriesInThisBundle--;

				if (_.has(apiResponse, 'redirect')) thisResponse.redirect = apiResponse.redirect;
				thisResponse[apiResponse.cname] = apiResponse;

			  	this.finished = true;
			}
		});

		manager.addJob('composeResponse', {
			work: function() {

				winston.info('manager:composeResponse');

				// Update the expiration date on the bundle
				var tout = {
					expires: _.min( thisResponse, function( val ) { return val.expires } ).expires,
					lastModified: new Date()
				};

				if (_.has( thisResponse, 'redirect')) tout.redirect = thisResponse.redirect;

				// Insert api responses into bundle
				_.each( thisResponse, function( val, idx ) {
					tout[val.cname] = val;
				});

				// Perform cleanup function on bundle
			  	if (_.has(bundle, 'cleanup')) {
				  	tout = bundle.cleanup(tout);
				}

				// Save cached bundle in Redis
				client.set('bid'+bid, JSON.stringify(tout));

				// Determine the seconds left before expiry
				if ( 'expires' in tout && _.isDate(tout.expires) ) {
					tout.secleft = tout.expires.getSecondsBetween( new Date() ) * -1;
				} else {
					tout.secleft = 3600;
				}

				manager.enqueue('sendResponse', tout);
				this.finished = true;

			}
		});

		manager.addJob('sendResponse', {
			work: function(doc) {

				winston.info('manager:sendResponse');

				if (_.has(doc, 'redirect')) {
					myRes.statusCode = 302;
					myRes.setHeader("Location", doc.redirect);
					myRes.end();
					this.finished = true;
				} else {

					// Send the results

					var responseType = callback ? 'application/javascript' : 'application/json';
					var responseHeaders = {'Content-Type': responseType, 'vary': 'Accept-Encoding', 'max-age': doc.secleft, 'cache-control': 'public, max-age='+doc.secleft+', no-transform', "Expires": doc.expires };

					// If a callback name was passed, use it. Otherwise, just output the object
					var tbuf = new Buffer(  ( !callback ) ? JSON.stringify( doc ) : callback + '('  + JSON.stringify( doc ) +  ');');

					if (gzip) {

						zlib.gzip(tbuf.toString(), function(err, zbuf) {
						  if (!err) {
						    winston.event('Send gzipped response for ' + bid +', ' + zbuf.toString().length + ', ' + ip);
						    myRes.writeHead(200, {'Content-Type': responseType, 'max-age': doc.secleft, 'cache-control': 'public, max-age='+doc.secleft+', no-transform', "Expires": doc.expires, 'content-encoding': 'gzip' });
						    myRes.end(zbuf);
						    this.finished = true;
						  }
						});
					} else {
						winston.event('Send response for ' + bid +', ' + tbuf.length + ', ' + ip);
						myRes.writeHead(200, responseHeaders);
						myRes.end(tbuf);
						this.finished = true;
					}
				}
			}
		});

		_.each( bundle, function( api, key ) {
			if (key !== 'cleanup' && key !== 'callback') {
				if (_.isUndefined(api, 'credentials')) {
					api.credentials = {};
				}
				manager.enqueue('fulfillPart', api, bid, key);
			}
		});

	}
}<|endoftext|>