<commit_before>// ======== DB-Model: ========
// "_id" -> ID
// "name" -> string
// "categories" -> [ID_categories]
// "tags" -> list ID_categories
// "description" -> string
// "slug" -> string
// "region" -> ID_region
// "date" -> timestamp     what for?
// "createdby" -> ID_users
// "time_created" -> timestamp
// "time_lastedit" -> timestamp
// "time_lastenrol" -> timestamp
// "roles" -> [role-keys]
// "members" -> [{"user":ID_user,"roles":[role-keys]},"comment":string]
// ===========================

Courses = new Meteor.Collection("Courses");

Courses.allow({
	remove: function (userId, doc) {
		return userId && true;   // allow only if UserId is present
	}
});

function findCourses(params){
	var find = {};

	if (params.member) {
		// courses where given user is member
		find.members = { user: params.member }
	}

	if (Session.get('region')) {
		find.region = Session.get('region')
	}

	if (params.missing=="organisator") {
		// show courses with no organisator
		find['members.roles'] = { $ne: 'team' }
	}

	if (param.search) {
		_.extend(find, {
			$or: [
				// FIXME: Runs unescaped as regex, absolutely not ok
				// ALSO: Not user friendly, do we can have fulltext?
				{ name: { $regex: param.search, $options: 'i' } },
				{ description: { $regex: param.search, $options: 'i' } }
			]
		})
	}

	return Courses.find(find, {sort: {time_lastedit: -1, time_created: -1}});
}

function addRole(course, role, user, comment) {
	// Add the user as member if she's not listed yet
	Courses.update(
		{ _id: course._id, 'members.user': { $ne: user } },
		{ $addToSet: { 'members': { user: user, roles: []} }}
	)

	// Minimongo does not currently support the $ field selector
	// Remove this guard once it does
	if (!Meteor.isClient) {
		Courses.update(
			{ _id: course._id, 'members.user': user },
			{ '$addToSet': { 'members.$.roles': role }, $set:{'members.$.comment' : comment}},
			checkUpdateOne
		)
	}
}

function removeRole(course, role, user) {
	// Minimongo does not currently support the $ field selector
	// Remove this guard once it does
	if (!Meteor.isClient) {
			var result = Courses.update(
				{ _id: course._id, 'members.user': user },
				{ '$pull': { 'members.$.roles': role }},
				checkUpdateOne
			)
	}

	// Housekeeping: Remove members that have no role left
	// Note that we have a race condition here with the addRole() function, blissfully ignoring the unlikely case
	Courses.update(
		{ _id: course._id },
		{ $pull: { members: { roles: { $size: 0 } }}}
	)
}

Meteor.methods({

	change_subscription: function(courseId, role, add, anon, comment) {
		check(role, String)
		check(courseId, String)
		check(add, Boolean)
		check(anon, Boolean)
		check(comment, Match.OneOf(null, String))
		var course = Courses.findOne({_id: courseId})
		if (!course) throw new Meteor.Error(404, "Course not found")
		var userId = false
		var user = Meteor.user();
		var remove = !add
		//See wheter to use an anonId
		if (remove || anon){
			_.each(course.members, function(member){
				if (user.anonId && user.anonId.indexOf(member.user) != -1 && (add || member.roles.indexOf(role) != -1)){
					userId=member.user
				}
			})
		}

		if (anon && !userId){
			userId = new Meteor.Collection.ObjectID()
			userId = 'Anon_' + userId._str
			Meteor.call('insert_anonId', userId)
		}
		if (!anon && !userId){
			userId = user._id
		}

		if (!userId) {
			// Oops
			if (Meteor.isClient) {
				alert('please log in')
				return;
			} else {
				throw new Meteor.Error(401, "please log in")
			}
		}

		if (!course.roles.indexOf(role) == -1) throw new Meteor.Error(404, "No role "+role)

		if (add) {
			addRole(course, role, userId, comment)
			var time = new Date
			Courses.update({_id: courseId}, { $set: {time_lastenrol:time}}, checkUpdateOne)
		} else {
			removeRole(course, role, userId)
		}
	},

	save_course: function(courseId, changes) {
		check(courseId, String)
		check(changes, {
			description: Match.Optional(String),
			categories:  Match.Optional([String]),
			name:        Match.Optional(String),
			region:      Match.Optional(String),
			roles:       Match.Optional(Object)
		})
		var user = Meteor.user()
		if (!user) {
		    if (Meteor.is_client) {
				alert('please log in')
				return;
			} else {
				throw new Meteor.Error(401, "please log in")
			}
		}

		var course;
		var isNew = courseId.length == 0
		if (!isNew) {
			course = Courses.findOne({_id: courseId})
			if (!course) throw new Meteor.Error(404, "Course not found")
		}

 		var mayEdit = isNew || user.isAdmin || Courses.findOne({_id: courseId, members:{$elemMatch: { user: user._id, roles: 'team' }}})
		if (!mayEdit) throw new Meteor.Error(401, "get lost")

		/* Changes we want to perform */
		var set = {}

		_.each(Roles.find().fetch(), function(roletype) {
			var type = roletype.type
			var should_have = roletype.preset || changes.roles && changes.roles[type]
			var have = !isNew && course.roles.indexOf(type) !== -1
			if (have && !should_have) {
				Courses.update(
					{ _id: courseId },
					{ $pull: { roles: type, 'members.roles': type }},
					checkUpdateOne
				)
			}
			if (!have && should_have) {
				if (isNew) {
					set.roles = set.roles || []
					set.roles.push(type)
				} else {
					Courses.update(
						{ _id: courseId },
						{ $addToSet: { roles: type }},
						checkUpdateOne
					)
				}
			}
		})

		if (changes.description) set.description = changes.description.substring(0, 640*1024) /* 640 k ought to be enough for everybody */
		if (changes.categories) set.categories = changes.categories.slice(0, 20)
		if (changes.name) {
		    set.name = changes.name.substring(0, 1000)
		    set.slug = getSlug(set.name);
		}

		set.time_lastedit = new Date
		if (isNew) {
			/* region cannot be changed */
			set.region = Regions.findOne({_id: changes.region})
			if (!set.region) throw new Exception(404, 'region missing')

			courseId = Courses.insert({
				members: [{ user: user._id, roles: [] }],
				createdby: user._id,
				time_created: new Date
			}, checkInsert)
		}

		Courses.update({ _id: courseId }, { $set: set }, checkUpdateOne)

		return course
	}
})

/* Need to find a good place to make these available to all */

function checkInsert(err, id) {
	if (err) throw err
}

function checkUpdateOne(err, aff) {
	if (err) throw err;
	if (aff != 1) throw "Query affected "+aff+" docs, expected 1"
}
<commit_msg>obsolete code<commit_after>// ======== DB-Model: ========
// "_id" -> ID
// "name" -> string
// "categories" -> [ID_categories]
// "tags" -> list ID_categories
// "description" -> string
// "slug" -> string
// "region" -> ID_region
// "date" -> timestamp     what for?
// "createdby" -> ID_users
// "time_created" -> timestamp
// "time_lastedit" -> timestamp
// "time_lastenrol" -> timestamp
// "roles" -> [role-keys]
// "members" -> [{"user":ID_user,"roles":[role-keys]},"comment":string]
// ===========================

Courses = new Meteor.Collection("Courses");

Courses.allow({
	remove: function (userId, doc) {
		return userId && true;   // allow only if UserId is present
	}
});

function addRole(course, role, user, comment) {
	// Add the user as member if she's not listed yet
	Courses.update(
		{ _id: course._id, 'members.user': { $ne: user } },
		{ $addToSet: { 'members': { user: user, roles: []} }}
	)

	// Minimongo does not currently support the $ field selector
	// Remove this guard once it does
	if (!Meteor.isClient) {
		Courses.update(
			{ _id: course._id, 'members.user': user },
			{ '$addToSet': { 'members.$.roles': role }, $set:{'members.$.comment' : comment}},
			checkUpdateOne
		)
	}
}

function removeRole(course, role, user) {
	// Minimongo does not currently support the $ field selector
	// Remove this guard once it does
	if (!Meteor.isClient) {
			var result = Courses.update(
				{ _id: course._id, 'members.user': user },
				{ '$pull': { 'members.$.roles': role }},
				checkUpdateOne
			)
	}

	// Housekeeping: Remove members that have no role left
	// Note that we have a race condition here with the addRole() function, blissfully ignoring the unlikely case
	Courses.update(
		{ _id: course._id },
		{ $pull: { members: { roles: { $size: 0 } }}}
	)
}

Meteor.methods({

	change_subscription: function(courseId, role, add, anon, comment) {
		check(role, String)
		check(courseId, String)
		check(add, Boolean)
		check(anon, Boolean)
		check(comment, Match.OneOf(null, String))
		var course = Courses.findOne({_id: courseId})
		if (!course) throw new Meteor.Error(404, "Course not found")
		var userId = false
		var user = Meteor.user();
		var remove = !add
		//See wheter to use an anonId
		if (remove || anon){
			_.each(course.members, function(member){
				if (user.anonId && user.anonId.indexOf(member.user) != -1 && (add || member.roles.indexOf(role) != -1)){
					userId=member.user
				}
			})
		}

		if (anon && !userId){
			userId = new Meteor.Collection.ObjectID()
			userId = 'Anon_' + userId._str
			Meteor.call('insert_anonId', userId)
		}
		if (!anon && !userId){
			userId = user._id
		}

		if (!userId) {
			// Oops
			if (Meteor.isClient) {
				alert('please log in')
				return;
			} else {
				throw new Meteor.Error(401, "please log in")
			}
		}

		if (!course.roles.indexOf(role) == -1) throw new Meteor.Error(404, "No role "+role)

		if (add) {
			addRole(course, role, userId, comment)
			var time = new Date
			Courses.update({_id: courseId}, { $set: {time_lastenrol:time}}, checkUpdateOne)
		} else {
			removeRole(course, role, userId)
		}
	},

	save_course: function(courseId, changes) {
		check(courseId, String)
		check(changes, {
			description: Match.Optional(String),
			categories:  Match.Optional([String]),
			name:        Match.Optional(String),
			region:      Match.Optional(String),
			roles:       Match.Optional(Object)
		})
		var user = Meteor.user()
		if (!user) {
		    if (Meteor.is_client) {
				alert('please log in')
				return;
			} else {
				throw new Meteor.Error(401, "please log in")
			}
		}

		var course;
		var isNew = courseId.length == 0
		if (!isNew) {
			course = Courses.findOne({_id: courseId})
			if (!course) throw new Meteor.Error(404, "Course not found")
		}

 		var mayEdit = isNew || user.isAdmin || Courses.findOne({_id: courseId, members:{$elemMatch: { user: user._id, roles: 'team' }}})
		if (!mayEdit) throw new Meteor.Error(401, "get lost")

		/* Changes we want to perform */
		var set = {}

		_.each(Roles.find().fetch(), function(roletype) {
			var type = roletype.type
			var should_have = roletype.preset || changes.roles && changes.roles[type]
			var have = !isNew && course.roles.indexOf(type) !== -1
			if (have && !should_have) {
				Courses.update(
					{ _id: courseId },
					{ $pull: { roles: type, 'members.roles': type }},
					checkUpdateOne
				)
			}
			if (!have && should_have) {
				if (isNew) {
					set.roles = set.roles || []
					set.roles.push(type)
				} else {
					Courses.update(
						{ _id: courseId },
						{ $addToSet: { roles: type }},
						checkUpdateOne
					)
				}
			}
		})

		if (changes.description) set.description = changes.description.substring(0, 640*1024) /* 640 k ought to be enough for everybody */
		if (changes.categories) set.categories = changes.categories.slice(0, 20)
		if (changes.name) {
		    set.name = changes.name.substring(0, 1000)
		    set.slug = getSlug(set.name);
		}

		set.time_lastedit = new Date
		if (isNew) {
			/* region cannot be changed */
			set.region = Regions.findOne({_id: changes.region})
			if (!set.region) throw new Exception(404, 'region missing')

			courseId = Courses.insert({
				members: [{ user: user._id, roles: [] }],
				createdby: user._id,
				time_created: new Date
			}, checkInsert)
		}

		Courses.update({ _id: courseId }, { $set: set }, checkUpdateOne)

		return course
	}
})

/* Need to find a good place to make these available to all */

function checkInsert(err, id) {
	if (err) throw err
}

function checkUpdateOne(err, aff) {
	if (err) throw err;
	if (aff != 1) throw "Query affected "+aff+" docs, expected 1"
}
<|endoftext|>


<commit_before>var asyncMap = require('./async-map');

var semver = require('./semver');
var github = require('./github');
var npmWrapper = require('./npm');
var db = require('./db');
var emailWrapper = require('./email');

var running = false;

exports.start = function() {
  // Only allow one instance to run at a time
  if (running) {
    console.log("Check already running.");
    return false;
  }
  console.log("Starting check.");
  running = true;
  var retriesLeft = 2;
  checkUpdates(onDone);
  return true;

  function onDone(error) {
    if (error) {
      console.error(error);
      if (retriesLeft--) {
        return checkUpdates(onDone);
      }
      running = false;
      console.log("Giving up!");
    }
    else {
      running = false;
      console.log("Check finished.");
    }
  }
};

function checkUpdates(callback) {

  // Mapping of unique email address to list of packages
  var emails = {};

  // List of packages to monitor, indexed by user/name string
  // Each entry contains:
  //   dependencies - a map of dependency name to version
  //   devDependencies - a map of dev dependency name to version
  var packages = {};

  // This is a mapping of dependency name to dependent package
  // key is dependency name, value is direct reference to entry in packages hash
  var dependents = {};

  // First we need to load all the subscriptions
  db.readSubscriptions(onSubscriptions);

  function onSubscriptions(error, subscriptions) {
    if (error) return callback(error);

    asyncMap(subscriptions, function (subscription, callback) {
      var user = github.getUserFromUrl(subscription.repourl);
      var repo = github.getRepoFromUrl(subscription.repourl);
      var key = user + "/" + repo;

      // Record this email/package combo
      var watching = emails[subscription.email];
      if (watching) watching.push(key);
      else emails[subscription.email] = [key];

      if (packages[key]) {
        // Someone else is already watching for changes in this project
        return;
      }

      packages[key] = {};

      github.getPackageJson(user, repo, function (error, meta) {
        if (error) return callback(error);
        if (meta.dependencies) {
          packages[key].dependencies = meta.dependencies;
          Object.keys(meta.dependencies).forEach(addLink);
        }
        if (meta.devDependencies) {
          packages[key].devDependencies = meta.devDependencies;
          Object.keys(meta.devDependencies).forEach(addLink);
        }
        callback();
      });

      function addLink(name) {
        if (dependents[name]) dependents[name].push(key);
        else dependents[name] = [key];
      }

    }, onProcessed);
  }

  function onProcessed(error) {
    if (error) return callback(error);
    console.log("Checking %s unique packages with %s total unique dependencies for %s unique email addresses.",
                Object.keys(packages).length,
                Object.keys(dependents).length,
                Object.keys(emails).length);
    npmWrapper.getLatestVersions(Object.keys(dependents), onVersions);
  }

  function onVersions(error, versions) {
    if (error) return callback(error);

    console.info({
      emails: emails,
      dependents: dependents,
      packages: packages,
      versions: versions
    });

    // We now have emails of all the people interested in updates.
    // We also have the list of subscriptions grouped by user/name projects key.
    // We also have the latest version of any npm package that we care about.
    // We also have the dependencies and devDependencies of all subscribed packages.

    // Now the task is to loop through the packages, find dependencies and
    // devDependencies that are out of date.  Record a list of packages with
    // out of data deps.
		var newDependenciesCount = 0;
    var newPackages = {};
    var yesterday = new Date();
    yesterday.setHours(-25);
    Object.keys(packages).forEach(findDependencies);

    function findDependencies(repo) {
      if (!newPackages[repo])
        newPackages[repo] = {};
      newPackages[repo].dependencies = {};
      newPackages[repo].devDependencies = {};

      if (packages[repo].dependencies)
        Object.keys(packages[repo].dependencies).forEach(checkDepVersionFor(packages[repo].dependencies, newPackages[repo].dependencies));
      if (packages[repo].devDependencies)
        Object.keys(packages[repo].devDependencies).forEach(checkDepVersionFor(packages[repo].devDependencies, newPackages[repo].devDependencies));

      function checkDepVersionFor(dep, result) {
        return function (pack) {
          console.info('%s: Date %s >= %s is %s, isNew(%s, %s) is %s', pack, versions[pack].updated, yesterday, versions[pack].updated >= yesterday, versions[pack].latest, dep[pack], semver.isNew(versions[pack].latest, dep[pack]));
          if (versions[pack].updated >= yesterday && semver.isNew(versions[pack].latest, dep[pack])) {
            result[pack] = { old: dep[pack], current: versions[pack].latest };
            newDependenciesCount++;
          }
        };
      }

			console.info('%s has %s dev and %s regular dependencies', repo, Object.keys(newPackages[repo].devDependencies).length, Object.keys(newPackages[repo].dependencies).length);
    }

    console.info('There are %s new dependencies.', newDependenciesCount);

    // Then loop through the email address and for each one, loop through it's
    // packages.  If any of them had out-of-date deps, prepare and send an email.
    // If sending an email is an async operation, you can do it in an asyncMap
    // so that you can catch errors and be notified when they are all done.
    asyncMap(emails, processEmail, onProcessedEmails);

    function processEmail(email, callback) {
      if (error) return callback(error);

      Object.keys(email).forEach(sendEmail);

      function sendEmail(key) {
        emails[key].forEach(function (repo) {
          var newPacks = newPackages[repo];
          var numNewPacks = Object.keys(newPacks.dependencies).length + Object.keys(newPacks.devDependencies).length;
          if (numNewPacks === 0)
            return callback();

          var repourl = github.getUrlFromUserWithRepo(repo);
          console.info('Sending email to ' + key + ' for ' + repourl + ' about potentially ' + numNewPacks + ' packages.');
          emailWrapper.send(key, repourl, newPacks, callback);
        });
      }
    }

    function onProcessedEmails(error) {
      // Once done, call the outer `callback` to release the global lock and let
      // the server log that the action is complete.
      callback(error);
    }
  }

}
<commit_msg>Add more tracing.<commit_after>var asyncMap = require('./async-map');

var semver = require('./semver');
var github = require('./github');
var npmWrapper = require('./npm');
var db = require('./db');
var emailWrapper = require('./email');

var running = false;

exports.start = function() {
  // Only allow one instance to run at a time
  if (running) {
    console.log("Check already running.");
    return false;
  }
  console.log("Starting check.");
  running = true;
  var retriesLeft = 2;
  checkUpdates(onDone);
  return true;

  function onDone(error) {
    if (error) {
      console.error(error);
      if (retriesLeft--) {
        return checkUpdates(onDone);
      }
      running = false;
      console.log("Giving up!");
    }
    else {
      running = false;
      console.log("Check finished.");
    }
  }
};

function checkUpdates(callback) {

  // Mapping of unique email address to list of packages
  var emails = {};

  // List of packages to monitor, indexed by user/name string
  // Each entry contains:
  //   dependencies - a map of dependency name to version
  //   devDependencies - a map of dev dependency name to version
  var packages = {};

  // This is a mapping of dependency name to dependent package
  // key is dependency name, value is direct reference to entry in packages hash
  var dependents = {};

  // First we need to load all the subscriptions
  db.readSubscriptions(onSubscriptions);

  function onSubscriptions(error, subscriptions) {
    if (error) return callback(error);

    asyncMap(subscriptions, function (subscription, callback) {
      var user = github.getUserFromUrl(subscription.repourl);
      var repo = github.getRepoFromUrl(subscription.repourl);
      var key = user + "/" + repo;

      // Record this email/package combo
      var watching = emails[subscription.email];
      if (watching) watching.push(key);
      else emails[subscription.email] = [key];

      if (packages[key]) {
        // Someone else is already watching for changes in this project
        return;
      }

      packages[key] = {};

      github.getPackageJson(user, repo, function (error, meta) {
        if (error) return callback(error);
        if (meta.dependencies) {
          packages[key].dependencies = meta.dependencies;
          Object.keys(meta.dependencies).forEach(addLink);
        }
        if (meta.devDependencies) {
          packages[key].devDependencies = meta.devDependencies;
          Object.keys(meta.devDependencies).forEach(addLink);
        }
        callback();
      });

      function addLink(name) {
        if (dependents[name]) dependents[name].push(key);
        else dependents[name] = [key];
      }

    }, onProcessed);
  }

  function onProcessed(error) {
    if (error) return callback(error);
    console.log("Checking %s unique packages with %s total unique dependencies for %s unique email addresses.",
                Object.keys(packages).length,
                Object.keys(dependents).length,
                Object.keys(emails).length);
    npmWrapper.getLatestVersions(Object.keys(dependents), onVersions);
  }

  function onVersions(error, versions) {
    if (error) return callback(error);

    console.info({
      emails: emails,
      dependents: dependents,
      packages: packages,
      versions: versions
    });

    // We now have emails of all the people interested in updates.
    // We also have the list of subscriptions grouped by user/name projects key.
    // We also have the latest version of any npm package that we care about.
    // We also have the dependencies and devDependencies of all subscribed packages.

    // Now the task is to loop through the packages, find dependencies and
    // devDependencies that are out of date.  Record a list of packages with
    // out of data deps.
		var newDependenciesCount = 0;
    var newPackages = {};
    var yesterday = new Date();
    yesterday.setHours(-25);
    Object.keys(packages).forEach(findDependencies);

    function findDependencies(repo) {
      if (!newPackages[repo])
        newPackages[repo] = {};
      newPackages[repo].dependencies = {};
      newPackages[repo].devDependencies = {};

      if (packages[repo].dependencies)
        Object.keys(packages[repo].dependencies).forEach(checkDepVersionFor(packages[repo].dependencies, newPackages[repo].dependencies));
      if (packages[repo].devDependencies)
        Object.keys(packages[repo].devDependencies).forEach(checkDepVersionFor(packages[repo].devDependencies, newPackages[repo].devDependencies));

      function checkDepVersionFor(dep, result) {
        return function (pack) {
          console.info('%s: Date %s >= %s is %s, isNew(%s, %s) is %s', pack, versions[pack].updated, yesterday, versions[pack].updated >= yesterday, versions[pack].latest, dep[pack], semver.isNew(versions[pack].latest, dep[pack]));
          if (versions[pack].updated >= yesterday && semver.isNew(versions[pack].latest, dep[pack])) {
            result[pack] = { old: dep[pack], current: versions[pack].latest };
            newDependenciesCount++;
          }
        };
      }

			console.info('%s has %s dev and %s regular dependencies', repo, Object.keys(newPackages[repo].devDependencies).length, Object.keys(newPackages[repo].dependencies).length);
    }

    console.info('There are %s new dependencies.', newDependenciesCount);

    // Then loop through the email address and for each one, loop through it's
    // packages.  If any of them had out-of-date deps, prepare and send an email.
    // If sending an email is an async operation, you can do it in an asyncMap
    // so that you can catch errors and be notified when they are all done.
    asyncMap(emails, processEmail, onProcessedEmails);

    function processEmail(email, callback) {
      if (error) return callback(error);

      Object.keys(email).forEach(sendEmail);

      function sendEmail(key) {
        emails[key].forEach(function (repo) {
          var newPacks = newPackages[repo];
          var numNewPacks = Object.keys(newPacks.dependencies).length + Object.keys(newPacks.devDependencies).length;
          console.info('Email. Key: %s, repo: %s, new packages: %s', key, repo, numNewPacks);
          if (numNewPacks === 0)
            return callback();

          var repourl = github.getUrlFromUserWithRepo(repo);
          console.info('Sending email to ' + key + ' for ' + repourl + ' about potentially ' + numNewPacks + ' packages.');
          emailWrapper.send(key, repourl, newPacks, callback);
        });
      }
    }

    function onProcessedEmails(error) {
      // Once done, call the outer `callback` to release the global lock and let
      // the server log that the action is complete.
      callback(error);
    }
  }

}
<|endoftext|>


<commit_before>var asyncMap = require('./async-map');

var semver = require('./semver');
var github = require('./github');
var npmWrapper = require('./npm');
var db = require('./db');
var emailWrapper = require('./email');

var running = false;

exports.start = function() {
  // Only allow one instance to run at a time
  if (running) {
    console.log("Check already running.");
    return false;
  }
  console.log("Starting check.");
  running = true;
  var retriesLeft = 2;
  checkUpdates(onDone);
  return true;

  function onDone(error) {
    if (error) {
      console.error(error);
      if (retriesLeft--) {
        return checkUpdates(onDone);
      }
      running = false;
      console.log("Giving up!");
    }
    else {
      running = false;
      console.log("Check finished.");
    }
  }
};

function checkUpdates(callback) {

  // Mapping of unique email address to list of packages
  var emails = {};

  // List of packages to monitor, indexed by user/name string
  // Each entry contains:
  //   dependencies - a map of dependency name to version
  //   devDependencies - a map of dev dependency name to version
  var packages = {};

  // This is a mapping of dependency name to dependent package
  // key is dependency name, value is direct reference to entry in packages hash
  var dependents = {};

  // First we need to load all the subscriptions
  db.readSubscriptions(onSubscriptions);

  function onSubscriptions(error, subscriptions) {
    if (error) return callback(error);

    asyncMap(subscriptions, function (subscription, callback) {
      var user = github.getUserFromUrl(subscription.repourl);
      var repo = github.getRepoFromUrl(subscription.repourl);
      var key = user + "/" + repo;

      // Record this email/package combo
      var watching = emails[subscription.email];
      if (watching) watching.push(key);
      else emails[subscription.email] = [key];

      if (packages[key]) {
        // Someone else is already watching for changes in this project
        return;
      }

      packages[key] = {};

      github.getPackageJson(user, repo, function (error, meta) {
        if (error) return callback(error);
        if (meta.dependencies) {
          packages[key].dependencies = meta.dependencies;
          Object.keys(meta.dependencies).forEach(addLink);
        }
        if (meta.devDependencies) {
          packages[key].devDependencies = meta.devDependencies;
          Object.keys(meta.devDependencies).forEach(addLink);
        }
        callback();
      });

      function addLink(name) {
        if (dependents[name]) dependents[name].push(key);
        else dependents[name] = [key];
      }

    }, onProcessed);
  }

  function onProcessed(error) {
    if (error) return callback(error);
    console.log("Checking %s unique packages with %s total unique dependencies for %s unique email addresses.",
                Object.keys(packages).length,
                Object.keys(dependents).length,
                Object.keys(emails).length);
    npmWrapper.getLatestVersions(Object.keys(dependents), onVersions);
  }

  function onVersions(error, versions) {
    if (error) return callback(error);

    console.info({
      emails: emails,
      dependents: dependents,
      packages: packages,
      versions: versions
    });

    // We now have emails of all the people interested in updates.
    // We also have the list of subscriptions grouped by user/name projects key.
    // We also have the latest version of any npm package that we care about.
    // We also have the dependencies and devDependencies of all subscribed packages.

    // Now the task is to loop through the packages, find dependencies and
    // devDependencies that are out of date.  Record a list of packages with
    // out of data deps.
		var newDependenciesCount = 0;
    var newPackages = {};
    var yesterday = new Date();
    yesterday.setHours(-25);
    Object.keys(packages).forEach(findDependencies);

    function findDependencies(repo) {
      if (!newPackages[repo])
        newPackages[repo] = {};
      newPackages[repo].dependencies = {};
      newPackages[repo].devDependencies = {};

      if (packages[repo].dependencies)
        Object.keys(packages[repo].dependencies).forEach(checkDepVersionFor(packages[repo].dependencies, newPackages[repo].dependencies));
      if (packages[repo].devDependencies)
        Object.keys(packages[repo].devDependencies).forEach(checkDepVersionFor(packages[repo].devDependencies, newPackages[repo].devDependencies));

      function checkDepVersionFor(dep, result) {
        return function (pack) {
          console.info('%s: Date %s >= %s is %s, isNew(%s, %s) is %s', pack, versions[pack].updated, yesterday, versions[pack].updated >= yesterday, versions[pack].latest, dep[pack], semver.isNew(versions[pack].latest, dep[pack]));
          if (versions[pack].updated >= yesterday && semver.isNew(versions[pack].latest, dep[pack])) {
            result[pack] = { old: dep[pack], current: versions[pack].latest };
            newDependenciesCount++;
          }
        };
      }
    }

    console.info('There are %s new dependencies.', newDependenciesCount);

    // Then loop through the email address and for each one, loop through it's
    // packages.  If any of them had out-of-date deps, prepare and send an email.
    // If sending an email is an async operation, you can do it in an asyncMap
    // so that you can catch errors and be notified when they are all done.
    asyncMap(emails, processEmail, onProcessedEmails);

    function processEmail(email, callback) {
      if (error) return callback(error);

      Object.keys(email).forEach(sendEmail);

      function sendEmail(key) {
        emails[key].forEach(function (repo) {
          var newPacks = newPackages[repo];
          var numNewPacks = Object.keys(newPacks.dependencies).length + Object.keys(newPacks.devDependencies).length;
          if (numNewPacks === 0)
            return callback();

          var repourl = github.getUrlFromUserWithRepo(repo);
          console.info('Sending email to ' + key + ' for ' + repourl + ' about potentially ' + numNewPacks + ' packages.');
          emailWrapper.send(key, repourl, newPacks, callback);
        });
      }
    }

    function onProcessedEmails(error) {
      // Once done, call the outer `callback` to release the global lock and let
      // the server log that the action is complete.
      callback(error);
    }
  }

}
<commit_msg>Add more trace logs.<commit_after>var asyncMap = require('./async-map');

var semver = require('./semver');
var github = require('./github');
var npmWrapper = require('./npm');
var db = require('./db');
var emailWrapper = require('./email');

var running = false;

exports.start = function() {
  // Only allow one instance to run at a time
  if (running) {
    console.log("Check already running.");
    return false;
  }
  console.log("Starting check.");
  running = true;
  var retriesLeft = 2;
  checkUpdates(onDone);
  return true;

  function onDone(error) {
    if (error) {
      console.error(error);
      if (retriesLeft--) {
        return checkUpdates(onDone);
      }
      running = false;
      console.log("Giving up!");
    }
    else {
      running = false;
      console.log("Check finished.");
    }
  }
};

function checkUpdates(callback) {

  // Mapping of unique email address to list of packages
  var emails = {};

  // List of packages to monitor, indexed by user/name string
  // Each entry contains:
  //   dependencies - a map of dependency name to version
  //   devDependencies - a map of dev dependency name to version
  var packages = {};

  // This is a mapping of dependency name to dependent package
  // key is dependency name, value is direct reference to entry in packages hash
  var dependents = {};

  // First we need to load all the subscriptions
  db.readSubscriptions(onSubscriptions);

  function onSubscriptions(error, subscriptions) {
    if (error) return callback(error);

    asyncMap(subscriptions, function (subscription, callback) {
      var user = github.getUserFromUrl(subscription.repourl);
      var repo = github.getRepoFromUrl(subscription.repourl);
      var key = user + "/" + repo;

      // Record this email/package combo
      var watching = emails[subscription.email];
      if (watching) watching.push(key);
      else emails[subscription.email] = [key];

      if (packages[key]) {
        // Someone else is already watching for changes in this project
        return;
      }

      packages[key] = {};

      github.getPackageJson(user, repo, function (error, meta) {
        if (error) return callback(error);
        if (meta.dependencies) {
          packages[key].dependencies = meta.dependencies;
          Object.keys(meta.dependencies).forEach(addLink);
        }
        if (meta.devDependencies) {
          packages[key].devDependencies = meta.devDependencies;
          Object.keys(meta.devDependencies).forEach(addLink);
        }
        callback();
      });

      function addLink(name) {
        if (dependents[name]) dependents[name].push(key);
        else dependents[name] = [key];
      }

    }, onProcessed);
  }

  function onProcessed(error) {
    if (error) return callback(error);
    console.log("Checking %s unique packages with %s total unique dependencies for %s unique email addresses.",
                Object.keys(packages).length,
                Object.keys(dependents).length,
                Object.keys(emails).length);
    npmWrapper.getLatestVersions(Object.keys(dependents), onVersions);
  }

  function onVersions(error, versions) {
    if (error) return callback(error);

    console.info({
      emails: emails,
      dependents: dependents,
      packages: packages,
      versions: versions
    });

    // We now have emails of all the people interested in updates.
    // We also have the list of subscriptions grouped by user/name projects key.
    // We also have the latest version of any npm package that we care about.
    // We also have the dependencies and devDependencies of all subscribed packages.

    // Now the task is to loop through the packages, find dependencies and
    // devDependencies that are out of date.  Record a list of packages with
    // out of data deps.
		var newDependenciesCount = 0;
    var newPackages = {};
    var yesterday = new Date();
    yesterday.setHours(-25);
    Object.keys(packages).forEach(findDependencies);

    function findDependencies(repo) {
      if (!newPackages[repo])
        newPackages[repo] = {};
      newPackages[repo].dependencies = {};
      newPackages[repo].devDependencies = {};

      if (packages[repo].dependencies)
        Object.keys(packages[repo].dependencies).forEach(checkDepVersionFor(packages[repo].dependencies, newPackages[repo].dependencies));
      if (packages[repo].devDependencies)
        Object.keys(packages[repo].devDependencies).forEach(checkDepVersionFor(packages[repo].devDependencies, newPackages[repo].devDependencies));

      function checkDepVersionFor(dep, result) {
        return function (pack) {
          console.info('%s: Date %s >= %s is %s, isNew(%s, %s) is %s', pack, versions[pack].updated, yesterday, versions[pack].updated >= yesterday, versions[pack].latest, dep[pack], semver.isNew(versions[pack].latest, dep[pack]));
          if (versions[pack].updated >= yesterday && semver.isNew(versions[pack].latest, dep[pack])) {
            result[pack] = { old: dep[pack], current: versions[pack].latest };
            newDependenciesCount++;
          }
        };
      }

			console.info('%s has %s dev and %s regular dependencies', repo, Object.keys(newPackages[repo].devDependencies).length, Object.keys(newPackages[repo].dependencies).length);
    }

    console.info('There are %s new dependencies.', newDependenciesCount);

    // Then loop through the email address and for each one, loop through it's
    // packages.  If any of them had out-of-date deps, prepare and send an email.
    // If sending an email is an async operation, you can do it in an asyncMap
    // so that you can catch errors and be notified when they are all done.
    asyncMap(emails, processEmail, onProcessedEmails);

    function processEmail(email, callback) {
      if (error) return callback(error);

      Object.keys(email).forEach(sendEmail);

      function sendEmail(key) {
        emails[key].forEach(function (repo) {
          var newPacks = newPackages[repo];
          var numNewPacks = Object.keys(newPacks.dependencies).length + Object.keys(newPacks.devDependencies).length;
          if (numNewPacks === 0)
            return callback();

          var repourl = github.getUrlFromUserWithRepo(repo);
          console.info('Sending email to ' + key + ' for ' + repourl + ' about potentially ' + numNewPacks + ' packages.');
          emailWrapper.send(key, repourl, newPacks, callback);
        });
      }
    }

    function onProcessedEmails(error) {
      // Once done, call the outer `callback` to release the global lock and let
      // the server log that the action is complete.
      callback(error);
    }
  }

}
<|endoftext|>


<commit_before>var __extends = this.__extends || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    __.prototype = b.prototype;
    d.prototype = new __();
};
var BABYLON;
(function (BABYLON) {
    var AnaglyphFreeCamera = (function (_super) {
        __extends(AnaglyphFreeCamera, _super);
        function AnaglyphFreeCamera(name, position, eyeSpace, scene) {
            _super.call(this, name, position, scene);
            this.setSubCameraMode(BABYLON.Camera.SUB_CAMERA_MODE_ANAGLYPH, eyeSpace);
        }
        return AnaglyphFreeCamera;
    })(BABYLON.FreeCamera);
    BABYLON.AnaglyphFreeCamera = AnaglyphFreeCamera;
    var AnaglyphArcRotateCamera = (function (_super) {
        __extends(AnaglyphArcRotateCamera, _super);
        function AnaglyphArcRotateCamera(name, alpha, beta, radius, target, eyeSpace, scene) {
            _super.call(this, name, alpha, beta, radius, target, scene);
            this.setSubCameraMode(BABYLON.Camera.SUB_CAMERA_MODE_ANAGLYPH, eyeSpace);
        }
        return AnaglyphArcRotateCamera;
    })(BABYLON.ArcRotateCamera);
    BABYLON.AnaglyphArcRotateCamera = AnaglyphArcRotateCamera;
    var AnaglyphGamepadCamera = (function (_super) {
        __extends(AnaglyphGamepadCamera, _super);
        function AnaglyphGamepadCamera(name, position, eyeSpace, scene) {
            _super.call(this, name, position, scene);
            this.setSubCameraMode(BABYLON.Camera.SUB_CAMERA_MODE_ANAGLYPH, eyeSpace);
        }
        return AnaglyphGamepadCamera;
    })(BABYLON.GamepadCamera);
    BABYLON.AnaglyphGamepadCamera = AnaglyphGamepadCamera;
})(BABYLON || (BABYLON = {}));
//# sourceMappingURL=babylon.anaglyphCamera.js.map<commit_msg>Delete babylon.anaglyphCamera.js<commit_after><|endoftext|>


<commit_before>'use strict';

// ## NOTE ################################################################ //
//                                                                          //
// History.js works poorly with URLs containing hashes:                     //
//                                                                          //
//    https://github.com/browserstate/history.js/issues/111                 //
//    https://github.com/browserstate/history.js/issues/173                 //
//                                                                          //
// So upon clicks on `/foo#bar` we treat URL and push it to the state as    //
// `/foo` and saving `bar` in the state data, so we could scroll to desired //
// element upon statechange                                                 //
//                                                                          //
// ######################################################################## //

var _ = require('lodash');

var History = window.History; // History.js

// Takes a params hash (can be nested) and tries to parse each string-value as
// number or boolean. Returns a new hash with parsed values.
//
function castParamTypes(inputValue) {
  var parsedValue;

  if (_.isArray(inputValue)) {
    return _.map(inputValue, castParamTypes);

  } else if (_.isObject(inputValue)) {
    parsedValue = {};

    _.forEach(inputValue, function (value, key) {
      parsedValue[key] = castParamTypes(value);
    });

    return parsedValue;

  } else if ('true' === inputValue) {
    return true;

  } else if ('false' === inputValue) {
    return false;

  } else if (/^[0-9\.\-]+$/.test(inputValue)) {
    parsedValue = Number(inputValue);
    return String(parsedValue) === inputValue ? parsedValue : inputValue;

  } else {
    return inputValue;
  }
}

// Returns a normalized URL:
//
//  http://example.com/foo.html  => http://example.com/foo.html
//  /foo.html                    => http://example.com/foo.html
//  //example.com/foo.html       => http://example.com/foo.html
//
// NOTE: History.JS does not plays well with full URLs but without protocols.
//
function normalizeURL(url) {
  var a = document.createElement('a');
  a.href = url;
  return a.href.toString();
}

// Default renderer for `navigate.to` event.
// Used to render content when user clicks a link.
//
function renderNewContent(data, callback) {
  var content = $(N.runtime.render(data.view, data.locals, {
    apiPath: data.apiPath
  })).hide();

  $('#content').fadeOut('fast', function () {
    var offset = data.anchor ? $(data.anchor).offset() : null;

    $(this).replaceWith(content);
    content.fadeIn('fast');

    // To scroll window:
    // - WebKit-based browsers and the quirks mode use `body` element.
    // - Other browsers use `html` element.
    $('html:not(:animated), body:not(:animated)').animate({
      scrollTop:  offset ? offset.top  : 0
    , scrollLeft: offset ? offset.left : 0
    }, 300);

    callback();
  });
}

// Used to render content when user presses Back/Forward buttons.
//
function renderFromHistory(data, callback) {
  var content = $(N.runtime.render(data.view, data.locals, {
    apiPath: data.apiPath
  })).hide();

  $('#content').fadeOut('fast', function () {
    $(this).replaceWith(content);
    content.fadeIn('fast');
    callback();
  });
}

// Ignore next History 'statechange' event if true.
//
// NOTE: The event handler *always* resets this variable to false after each call.
var __dryHistoryChange__ = false;

// Reference to a function to be used on next fire of history 'statechange' event
// to perform content injection/replacement.
//
// NOTE: The event handler *always* resets this variable to `renderFromHistory`
// after each call.
var __renderCallback__ = renderFromHistory;

// Reference to a function to be used by history 'statechange' handler after
// content rendering is done.
//
// NOTE: The event handler *always* resets this variable to null after each call.
var __completeCallback__ = null;

// API path of current page. Updated via `navigate.done` event.
var __currentApiPath__ = null;

// Performs RPC navigation to the specified page. Allowed options:
//
//    options.href
//    options.apiPath
//    options.params
//
// `href` and `apiPath` parameters are calculated from each other.
// So they are mutually exclusive.
//
N.wire.on('navigate.to', function navigate_to(options, callback) {
  var match, href, anchor, apiPath, params, errorReport;

  if ('string' === typeof options) {
    options = { href: options };
  }

  if (options.href) {
    href   = normalizeURL(options.href).split('#')[0];
    anchor = normalizeURL(options.href).slice(href.length) || '';

    match = _.find(N.runtime.router.matchAll(href), function (match) {
      return _.has(match.meta.methods, 'get');
    });

    // It's an external link or 404 error if route is not matched. So perform
    // regular page requesting via HTTP.
    if (!match) {
      window.location = href + anchor;
      callback();
      return;
    }

    apiPath = match.meta.methods.get;
    params = castParamTypes(match.params || {});

  } else if (options.apiPath) {
    apiPath = options.apiPath;
    params  = options.params || {};
    href    = normalizeURL(N.runtime.router.linkTo(apiPath, params));
    anchor  = options.anchor || '';

    if (!href) {
      errorReport = 'Invalid parameters passed to `navigate.to` event: ' +
                    JSON.stringify(options);

      window.alert(errorReport);
      callback(new Error(errorReport));
      return;
    }

  } else {
    errorReport = 'Not enough parameters for `navigate.to` event. ' +
                  'Need `href` or `apiPath` at least: ' +
                  JSON.stringify(options);

    window.alert(errorReport);
    callback(new Error(errorReport));
    return;
  }

  // Add anchor hash-prefix if not exists.
  if (anchor && '#' !== anchor.charAt(0)) {
    anchor = '#' + anchor;
  }

  // Stop here if base URL (all except anchor) haven't changed.
  if (href === (location.protocol + '//' + location.host + location.pathname)) {

    // Update anchor if it's changed.
    if (location.hash !== anchor) {
      location.hash = anchor;
    }

    callback();
    return;
  }

  // Fallback for old browsers.
  if (!History.enabled) {
    window.location = href + anchor;
    callback();
    return;
  }

  // History is enabled - try RPC navigation.
  N.io.rpc(apiPath, params, function (err, response) {
    if (err && N.io.REDIRECT === err.code) {
      var redirectUrl = document.createElement('a');

      // Tricky way to parse URL.
      redirectUrl.href = err.head.Location;

      // Note, that we try to keep anchor, if exists.
      // That's important for moved threads and last pages redirects.
      redirectUrl.hash = anchor || window.location.hash;

      // If protocol is changed, we must completely reload the page to keep
      // Same-origin policy for RPC.
      // - port check not required, because port depends on protocol.
      // - domain check not required, because RPC is available on all domains
      //   (it uses relative path)
      if (redirectUrl.protocol !== location.protocol) {
        window.location = redirectUrl.href;
        callback();
      } else {
        N.wire.emit('navigate.to', { href: redirectUrl.href }, callback);
      }
      return;
    }

    if (err) {
      // Can't deal via RPC - try HTTP. This might be:
      //
      // - Either a generic error, e.g. authorization / bad params / fuckup
      //   so we redirect user to show him an error page.
      //
      // - Version mismatch, so we call action by HTTP to update client.
      window.location = href + anchor;
      callback();
      return;
    }

    /*
    if (response.layout !== N.runtime.layout) {
      // Layout was changed - perform normal page loading.
      //
      // TODO: Prevent double page requesting. The server should not perform
      // database queries on RPC when the client is not intending to use the
      // response data. Like in this situation.
      window.location = href + anchor;
      callback();
      return;
    }
    */

    N.loader.loadAssets((response.view || apiPath).split('.')[0], function () {
      var state = {
        apiPath: apiPath
      , anchor:  anchor
      , view:    response.view   || apiPath
    //, layout:  response.layout || null
      , locals:  response.data   || {}
      };

      // Set one-use callbacks for history 'statechange' handler.
      // The handler will reset these to defaults (`renderFromHistory` and null).
      __renderCallback__   = renderNewContent;
      __completeCallback__ = callback;

      History.pushState(state, response.data.head.title, href);
    });
  });
});

// Replace current History state without data fetching and rendering.
//
//   options.href  - full url of new history state. (required)
//   options.title - new page title. (required)
//   options.data  - data for history renderer; it will be used when user will
//                   return to this page using history navigation. (optional)
//
N.wire.on('navigate.replace', function navigate_replace(options, callback) {
  __dryHistoryChange__ = true;
  __completeCallback__ = callback;

  History.replaceState(options.data || {}, options.title, options.href);
});

//
// Bind History's statechange handler. It fires when:
//
// - User presses `Back` or `Forward` button in his browser.
// - User clicks a link.
// - User clicks "More threads/posts/etc" button.
//

if (History.enabled) {
  History.Adapter.bind(window, 'statechange', function () {
    var state    = History.getState()
      , render   = __renderCallback__
      , complete = __completeCallback__;

    // Dry history change - just reset the flag and callback parameters for next
    // history state change, and invoke complete callback.
    if (__dryHistoryChange__) {
      __dryHistoryChange__ = false;
      __renderCallback__   = renderFromHistory;
      __completeCallback__ = null;

      if (complete) {
        complete();
      }
      return;
    }

    // We have no state data for the initial page (received via HTTP responder).
    // So request that data via RPC and place into History.
    if (_.isEmpty(state.data)) {
      var match = _.find(N.runtime.router.matchAll(state.url), function (match) {
        return _.has(match.meta.methods, 'get');
      });

      // Can't match initial URL by some reason - just reload the page.
      // This is internal error. Must not happen on normal Nodeca workflow.
      if (!match) {
        window.location = state.url;
        return;
      }

      // Retrieve data.
      N.io.rpc(match.meta.methods.get, castParamTypes(match.params || {}), function (err, response) {
        var a, data, url;

        // Simple way to parse URL in browser.
        a = document.createElement('a');
        a.href = state.url;

        // Result state data.
        data = {
          apiPath: match.meta.methods.get
        , anchor:  a.hash
        , view:    response.view   || match.meta.methods.get
      //, layout:  response.layout || null
        , locals:  response.data   || {}
        };

        // State URL without anchor. (due to a problem in History.js; see above)
        url = a.protocol + '//' + a.host + a.pathname;

        // We terminate here, but History.replaceState will trigger 'statechange'
        // again, and we will continue with next part of code.
        History.replaceState(data, state.title, url);
      });
      return;
    }

    // Restore callbacks to defaults. It's needed to ensure using right renderer
    // on regular history state changes - when user clicks back/forward buttons
    // in his browser.
    __renderCallback__   = renderFromHistory;
    __completeCallback__ = null;

    var exitEventData = { apiPath: __currentApiPath__, url: state.url }
      , doneEventData = { apiPath: state.data.apiPath, url: state.url };

    // Invoke exit handlers.
    N.wire.emit(['navigate.exit:' + __currentApiPath__, 'navigate.exit'], exitEventData, function (err) {
      if (err) {
        N.logger.error('%s', err); // Log error, but not stop.
      }

      // Clear old raw response data. It's collected by view templates.
      N.runtime.page_data = {};

      render(state.data, function () {
        // Invoke done-handlers.
        N.wire.emit(['navigate.done', 'navigate.done:' + state.data.apiPath], doneEventData, function (err) {
          if (err) {
            N.logger.error('%s', err); // Log error, but not stop.
          }

          if (complete) {
            complete();
          }
        });
      });
    });
  });
}

//
// __currentApiPath__ updater.
//

N.wire.on('navigate.done', { priority: -999 }, function apipath_set(data) {
  __currentApiPath__ = data.apiPath;
});

//
// Bind global a.click handler.
//
// NOTE: This handler must have *the lowest* priority. So:
//
// - It must be binded to `document`, since root DOM node has the lowest
//   priotity on event bubbling.
//
// - External libraries like Bootstrap must be placed *before* Nodeca client
//   code to ensure right order of handlers.
//

N.wire.once('navigate.done', { priority: 999 }, function navigate_click_handler() {
  $(document).on('click', 'a', function (event) {
    var $this = $(this);

    if ($this.attr('target') || event.isDefaultPrevented()) {
      // skip links that have `target` attribute specified
      // and clicks that were already handled
      return;
    }

    // Continue as normal for cmd clicks etc
    if (2 === event.which || event.metaKey) {
      return;
    }

    if ('#' === $this.attr('href')) {
      // Prevent clicks on special "button"-links.
      event.preventDefault();
      return;
    }

    N.wire.emit('navigate.to', $this.attr('href'), function (err) {
      if (err) {
        N.logger.error('%s', err);
      }
    });

    event.preventDefault();
  });
});
<commit_msg>Rolled back revert, done by mistake<commit_after>'use strict';

// ## NOTE ################################################################ //
//                                                                          //
// History.js works poorly with URLs containing hashes:                     //
//                                                                          //
//    https://github.com/browserstate/history.js/issues/111                 //
//    https://github.com/browserstate/history.js/issues/173                 //
//                                                                          //
// So upon clicks on `/foo#bar` we treat URL and push it to the state as    //
// `/foo` and saving `bar` in the state data, so we could scroll to desired //
// element upon statechange                                                 //
//                                                                          //
// ######################################################################## //

var _ = require('lodash');

var History = window.History; // History.js

// Takes a params hash (can be nested) and tries to parse each string-value as
// number or boolean. Returns a new hash with parsed values.
//
function castParamTypes(inputValue) {
  var parsedValue;

  if (_.isArray(inputValue)) {
    return _.map(inputValue, castParamTypes);

  } else if (_.isObject(inputValue)) {
    parsedValue = {};

    _.forEach(inputValue, function (value, key) {
      parsedValue[key] = castParamTypes(value);
    });

    return parsedValue;

  } else if ('true' === inputValue) {
    return true;

  } else if ('false' === inputValue) {
    return false;

  } else if (/^[0-9\.\-]+$/.test(inputValue)) {
    parsedValue = Number(inputValue);
    return String(parsedValue) === inputValue ? parsedValue : inputValue;

  } else {
    return inputValue;
  }
}

// Returns a normalized URL:
//
//  http://example.com/foo.html  => http://example.com/foo.html
//  /foo.html                    => http://example.com/foo.html
//  //example.com/foo.html       => http://example.com/foo.html
//
// NOTE: History.JS does not plays well with full URLs but without protocols.
//
function normalizeURL(url) {
  var a = document.createElement('a');
  a.href = url;
  return a.href.toString();
}

// Default renderer for `navigate.to` event.
// Used to render content when user clicks a link.
//
function renderNewContent(data, callback) {
  var content = $(N.runtime.render(data.view, data.locals, {
    apiPath: data.apiPath
  })).hide();

  $('#content').fadeOut('fast', function () {
    var offset = data.anchor ? $(data.anchor).offset() : null;

    $(this).replaceWith(content);
    content.fadeIn('fast');

    // To scroll window:
    // - WebKit-based browsers and the quirks mode use `body` element.
    // - Other browsers use `html` element.
    $('html:not(:animated), body:not(:animated)').animate({
      scrollTop:  offset ? offset.top  : 0
    , scrollLeft: offset ? offset.left : 0
    }, 300);

    callback();
  });
}

// Used to render content when user presses Back/Forward buttons.
//
function renderFromHistory(data, callback) {
  var content = $(N.runtime.render(data.view, data.locals, {
    apiPath: data.apiPath
  })).hide();

  $('#content').fadeOut('fast', function () {
    $(this).replaceWith(content);
    content.fadeIn('fast');
    callback();
  });
}

// Ignore next History 'statechange' event if true.
//
// NOTE: The event handler *always* resets this variable to false after each call.
var __dryHistoryChange__ = false;

// Reference to a function to be used on next fire of history 'statechange' event
// to perform content injection/replacement.
//
// NOTE: The event handler *always* resets this variable to `renderFromHistory`
// after each call.
var __renderCallback__ = renderFromHistory;

// Reference to a function to be used by history 'statechange' handler after
// content rendering is done.
//
// NOTE: The event handler *always* resets this variable to null after each call.
var __completeCallback__ = null;

// API path of current page. Updated via `navigate.done` event.
var __currentApiPath__ = null;
var __currentParams__  = {};

// Performs RPC navigation to the specified page. Allowed options:
//
//    options.href
//    options.apiPath
//    options.params
//
// `href` and `apiPath` parameters are calculated from each other.
// So they are mutually exclusive.
//
N.wire.on('navigate.to', function navigate_to(options, callback) {
  var match, href, anchor, apiPath, params, errorReport;

  if ('string' === typeof options) {
    options = { href: options };
  }

  if (options.href) {
    href   = normalizeURL(options.href).split('#')[0];
    anchor = normalizeURL(options.href).slice(href.length) || '';

    match = _.find(N.runtime.router.matchAll(href), function (match) {
      return _.has(match.meta.methods, 'get');
    });

    // It's an external link or 404 error if route is not matched. So perform
    // regular page requesting via HTTP.
    if (!match) {
      window.location = href + anchor;
      callback();
      return;
    }

    apiPath = match.meta.methods.get;
    params = castParamTypes(match.params || {});

  } else if (options.apiPath) {
    apiPath = options.apiPath;
    params  = options.params || {};
    href    = normalizeURL(N.runtime.router.linkTo(apiPath, params));
    anchor  = options.anchor || '';

    if (!href) {
      errorReport = 'Invalid parameters passed to `navigate.to` event: ' +
                    JSON.stringify(options);

      window.alert(errorReport);
      callback(new Error(errorReport));
      return;
    }

  } else {
    errorReport = 'Not enough parameters for `navigate.to` event. ' +
                  'Need `href` or `apiPath` at least: ' +
                  JSON.stringify(options);

    window.alert(errorReport);
    callback(new Error(errorReport));
    return;
  }

  // Add anchor hash-prefix if not exists.
  if (anchor && '#' !== anchor.charAt(0)) {
    anchor = '#' + anchor;
  }

  // Stop here if base URL (all except anchor) haven't changed.
  if (href === (location.protocol + '//' + location.host + location.pathname)) {

    // Update anchor if it's changed.
    if (location.hash !== anchor) {
      location.hash = anchor;
    }

    callback();
    return;
  }

  // Fallback for old browsers.
  if (!History.enabled) {
    window.location = href + anchor;
    callback();
    return;
  }

  // History is enabled - try RPC navigation.
  N.io.rpc(apiPath, params, function (err, response) {
    if (err && N.io.REDIRECT === err.code) {
      var redirectUrl = document.createElement('a');

      // Tricky way to parse URL.
      redirectUrl.href = err.head.Location;

      // Note, that we try to keep anchor, if exists.
      // That's important for moved threads and last pages redirects.
      redirectUrl.hash = anchor || window.location.hash;

      // If protocol is changed, we must completely reload the page to keep
      // Same-origin policy for RPC.
      // - port check not required, because port depends on protocol.
      // - domain check not required, because RPC is available on all domains
      //   (it uses relative path)
      if (redirectUrl.protocol !== location.protocol) {
        window.location = redirectUrl.href;
        callback();
      } else {
        N.wire.emit('navigate.to', { href: redirectUrl.href }, callback);
      }
      return;
    }

    if (err) {
      // Can't deal via RPC - try HTTP. This might be:
      //
      // - Either a generic error, e.g. authorization / bad params / fuckup
      //   so we redirect user to show him an error page.
      //
      // - Version mismatch, so we call action by HTTP to update client.
      window.location = href + anchor;
      callback();
      return;
    }

    /*
    if (response.layout !== N.runtime.layout) {
      // Layout was changed - perform normal page loading.
      //
      // TODO: Prevent double page requesting. The server should not perform
      // database queries on RPC when the client is not intending to use the
      // response data. Like in this situation.
      window.location = href + anchor;
      callback();
      return;
    }
    */

    N.loader.loadAssets((response.view || apiPath).split('.')[0], function () {
      var state = {
        apiPath: apiPath
      , params:  params
      , anchor:  anchor
      , view:    response.view   || apiPath
    //, layout:  response.layout || null
      , locals:  response.data   || {}
      };

      // Set one-use callbacks for history 'statechange' handler.
      // The handler will reset these to defaults (`renderFromHistory` and null).
      __renderCallback__   = renderNewContent;
      __completeCallback__ = callback;

      History.pushState(state, response.data.head.title, href);
    });
  });
});

// Replace current History state without data fetching and rendering.
//
//   options.href  - full url of new history state. (required)
//   options.title - new page title. (required)
//   options.data  - data for history renderer; it will be used when user will
//                   return to this page using history navigation. (optional)
//
N.wire.on('navigate.replace', function navigate_replace(options, callback) {
  __dryHistoryChange__ = true;
  __completeCallback__ = callback;

  History.replaceState(options.data || {}, options.title, options.href);
});

//
// Bind History's statechange handler. It fires when:
//
// - User presses `Back` or `Forward` button in his browser.
// - User clicks a link.
// - User clicks "More threads/posts/etc" button.
//

if (History.enabled) {
  History.Adapter.bind(window, 'statechange', function () {
    var state    = History.getState()
      , render   = __renderCallback__
      , complete = __completeCallback__;

    // Dry history change - just reset the flag and callback parameters for next
    // history state change, and invoke complete callback.
    if (__dryHistoryChange__) {
      __dryHistoryChange__ = false;
      __renderCallback__   = renderFromHistory;
      __completeCallback__ = null;

      if (complete) {
        complete();
      }
      return;
    }

    // We have no state data for the initial page (received via HTTP responder).
    // So request that data via RPC and place into History.
    if (_.isEmpty(state.data)) {
      var match = _.find(N.runtime.router.matchAll(state.url), function (match) {
        return _.has(match.meta.methods, 'get');
      });

      // Can't match initial URL by some reason - just reload the page.
      // This is internal error. Must not happen on normal Nodeca workflow.
      if (!match) {
        window.location = state.url;
        return;
      }

      // Retrieve data.
      N.io.rpc(match.meta.methods.get, castParamTypes(match.params || {}), function (err, response) {
        var a, data, url;

        // Simple way to parse URL in browser.
        a = document.createElement('a');
        a.href = state.url;

        // Result state data.
        data = {
          apiPath: match.meta.methods.get
        , params:  castParamTypes(match.params || {})
        , anchor:  a.hash
        , view:    response.view   || match.meta.methods.get
      //, layout:  response.layout || null
        , locals:  response.data   || {}
        };

        // State URL without anchor. (due to a problem in History.js; see above)
        url = a.protocol + '//' + a.host + a.pathname;

        // We terminate here, but History.replaceState will trigger 'statechange'
        // again, and we will continue with next part of code.
        History.replaceState(data, state.title, url);
      });
      return;
    }

    // Restore callbacks to defaults. It's needed to ensure using right renderer
    // on regular history state changes - when user clicks back/forward buttons
    // in his browser.
    __renderCallback__   = renderFromHistory;
    __completeCallback__ = null;

    var exitEventData = { apiPath: __currentApiPath__, params: __currentParams__, url: state.url }
      , doneEventData = { apiPath: state.data.apiPath, params: state.data.params, url: state.url };

    // Invoke exit handlers.
    N.wire.emit(['navigate.exit:' + __currentApiPath__, 'navigate.exit'], exitEventData, function (err) {
      if (err) {
        N.logger.error('%s', err); // Log error, but not stop.
      }

      // Clear old raw response data. It's collected by view templates.
      N.runtime.page_data = {};

      render(state.data, function () {
        // Invoke done-handlers.
        N.wire.emit(['navigate.done', 'navigate.done:' + state.data.apiPath], doneEventData, function (err) {
          if (err) {
            N.logger.error('%s', err); // Log error, but not stop.
          }

          if (complete) {
            complete();
          }
        });
      });
    });
  });
}

//
// __currentApiPath__ updater.
//

N.wire.on('navigate.done', { priority: -999 }, function apipath_set(data) {
  __currentApiPath__ = data.apiPath;
  __currentParams__  = data.params;
});

//
// Bind global a.click handler.
//
// NOTE: This handler must have *the lowest* priority. So:
//
// - It must be binded to `document`, since root DOM node has the lowest
//   priotity on event bubbling.
//
// - External libraries like Bootstrap must be placed *before* Nodeca client
//   code to ensure right order of handlers.
//

N.wire.once('navigate.done', { priority: 999 }, function navigate_click_handler() {
  $(document).on('click', 'a', function (event) {
    var $this = $(this);

    if ($this.attr('target') || event.isDefaultPrevented()) {
      // skip links that have `target` attribute specified
      // and clicks that were already handled
      return;
    }

    // Continue as normal for cmd clicks etc
    if (2 === event.which || event.metaKey) {
      return;
    }

    if ('#' === $this.attr('href')) {
      // Prevent clicks on special "button"-links.
      event.preventDefault();
      return;
    }

    N.wire.emit('navigate.to', $this.attr('href'), function (err) {
      if (err) {
        N.logger.error('%s', err);
      }
    });

    event.preventDefault();
  });
});
<|endoftext|>


<commit_before>import EventsNormalizer from 'Parser/Core/EventsNormalizer';

import SPELLS from 'common/SPELLS';

const MAX_DELAY = 20000;
const CBT_DELAY = 15000;

/*
* Cloudburst Totem had some weird behaviour (even if it rarely happens),
* because of that you can have a cast event without heal events.
* This happens if everyone in range is at 100% HP, it will just not heal at all.
*
* That throws off the CooldownThroughputTracker, gathering all events
* until the CBT after that explodes and giving results that are completely wrong.
*
* This Normalizer creates a 100% overhealed healing event where the heal would have been,
* if no healing events happened within 20 seconds after casting the totem.
 */

class CloudburstNormalizer extends EventsNormalizer {

  on_initialized() {
    this.active = this.combatants.selected.hasTalent(SPELLS.CLOUDBURST_TOTEM_TALENT.id);
  }

  fabricatedEvent = null;

  normalize(events) {
    const fixedEvents = [];
    events.forEach((event, eventIndex) => {

      if(this.fabricatedEvent) {
        if(event.timestamp >= this.fabricatedEvent.timestamp) {
          fixedEvents.push(this.fabricatedEvent);
          this.fabricatedEvent = null;
        }
      }

      fixedEvents.push(event);

      if (event.type === 'cast' && event.ability.guid === SPELLS.CLOUDBURST_TOTEM_TALENT.id) {
        const castTimestamp = event.timestamp;

        // Look ahead through the events to see if there is an CLOUDBURST_TOTEM_HEAL within a 20 second period
        for (let nextEventIndex = eventIndex; nextEventIndex < events.length-1; nextEventIndex += 1) {
          const nextEvent = events[nextEventIndex];

          if ((nextEvent.timestamp - castTimestamp) > MAX_DELAY) {
            // No CLOUDBURST_TOTEM_HEAL found within the period, meaning this cast wasn't able to find targets and did not have any healing events -> create a 100% overheal event
            const newTimestamp = event.timestamp+CBT_DELAY;

            this.fabricatedEvent = {
              timestamp: newTimestamp,
              type: "heal",
              sourceID: event.sourceID,
              targetID: event.sourceID,
              sourceIsFriendly: true,
              targetIsFriendly: true,
              ability: {
                abilityIcon: SPELLS.CLOUDBURST_TOTEM_HEAL.icon,
                guid: SPELLS.CLOUDBURST_TOTEM_HEAL.id,
                name: SPELLS.CLOUDBURST_TOTEM_HEAL.name,
                type: 8,
              },
              amount: 0,
              overheal: 1,
              hitType: 1,
              hitPoints: event.maxHitPoints,
              maxHitPoints: event.maxHitPoints,
              __fabricated: true,
            };
            break;
          } else if (nextEvent.type === 'heal' && nextEvent.ability.guid === SPELLS.CLOUDBURST_TOTEM_HEAL.id) {
            // CLOUDBURST_TOTEM_HEAL found, this was fine
            break;
          }
        }
      }
    });

    return fixedEvents;
  }

}
export default CloudburstNormalizer;
<commit_msg>use recall timestamp if found<commit_after>import EventsNormalizer from 'Parser/Core/EventsNormalizer';

import SPELLS from 'common/SPELLS';

const MAX_DELAY = 20000;
const CBT_DELAY = 15000;

/*
* Cloudburst Totem had some weird behaviour (even if it rarely happens),
* because of that you can have a cast event without heal events.
* This happens if everyone in range is at 100% HP, it will just not heal at all.
*
* That throws off the CooldownThroughputTracker, gathering all events
* until the CBT after that explodes and giving results that are completely wrong.
*
* This Normalizer creates a 100% overhealed healing event where the heal would have been,
* if no healing events happened within 20 seconds after casting the totem.
 */

class CloudburstNormalizer extends EventsNormalizer {

  on_initialized() {
    this.active = this.combatants.selected.hasTalent(SPELLS.CLOUDBURST_TOTEM_TALENT.id);
  }

  fabricatedEvent = null;
  recallTimestamp = null;

  normalize(events) {
    const fixedEvents = [];
    events.forEach((event, eventIndex) => {

      if(this.fabricatedEvent) {
        if(event.timestamp >= this.fabricatedEvent.timestamp) {
          fixedEvents.push(this.fabricatedEvent);
          this.fabricatedEvent = null;
        }
      }

      fixedEvents.push(event);

      if (event.type === 'cast' && event.ability.guid === SPELLS.CLOUDBURST_TOTEM_TALENT.id) {
        const castTimestamp = event.timestamp;

        // Look ahead through the events to see if there is an CLOUDBURST_TOTEM_HEAL within a 20 second period
        for (let nextEventIndex = eventIndex; nextEventIndex < events.length-1; nextEventIndex += 1) {
          const nextEvent = events[nextEventIndex];

          if ((nextEvent.timestamp - castTimestamp) > MAX_DELAY) {
            // No CLOUDBURST_TOTEM_HEAL found within the period, meaning this cast wasn't able to find targets and did not have any healing events -> create a 100% overheal event
            const newTimestamp = (this.recallTimestamp) ? this.recallTimestamp : event.timestamp+CBT_DELAY;

            this.fabricatedEvent = {
              timestamp: newTimestamp,
              type: "heal",
              sourceID: event.sourceID,
              targetID: event.sourceID,
              sourceIsFriendly: true,
              targetIsFriendly: true,
              ability: {
                abilityIcon: SPELLS.CLOUDBURST_TOTEM_HEAL.icon,
                guid: SPELLS.CLOUDBURST_TOTEM_HEAL.id,
                name: SPELLS.CLOUDBURST_TOTEM_HEAL.name,
                type: 8,
              },
              amount: 0,
              overheal: 1,
              hitType: 1,
              hitPoints: event.maxHitPoints,
              maxHitPoints: event.maxHitPoints,
              __fabricated: true,
            };
            break;
          } else if (nextEvent.type === 'cast' && nextEvent.ability.guid === SPELLS.CLOUDBURST_TOTEM_RECALL.id) {
            this.recallTimestamp = nextEvent.timestamp;
            continue;
          } else if (nextEvent.type === 'heal' && nextEvent.ability.guid === SPELLS.CLOUDBURST_TOTEM_HEAL.id) {
            // CLOUDBURST_TOTEM_HEAL found, this was fine
            break;
          }
        }
      }
    });

    return fixedEvents;
  }

}
export default CloudburstNormalizer;
<|endoftext|>