From b18b0db5befc479cee067565d05cfea696feacc7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?=
 <baris@nodebb.org>
Date: Mon, 8 May 2017 20:36:43 -0400
Subject: [PATCH] notifications.js style

---
 src/notifications.js | 817 +++++++++++++++++++++----------------------
 src/start.js         |   2 +-
 2 files changed, 409 insertions(+), 410 deletions(-)

diff --git a/src/notifications.js b/src/notifications.js
index 9034fc54c8..bb002238e6 100644
--- a/src/notifications.js
+++ b/src/notifications.js
@@ -15,500 +15,499 @@ var batch = require('./batch');
 var plugins = require('./plugins');
 var utils = require('./utils');
 
-(function (Notifications) {
-	Notifications.init = function () {
-		winston.verbose('[notifications.init] Registering jobs.');
-		new cron('*/30 * * * *', Notifications.prune, null, true);
-	};
-
-	Notifications.get = function (nid, callback) {
-		Notifications.getMultiple([nid], function (err, notifications) {
-			callback(err, Array.isArray(notifications) && notifications.length ? notifications[0] : null);
-		});
-	};
+var Notifications = module.exports;
+
+Notifications.startJobs = function () {
+	winston.verbose('[notifications.init] Registering jobs.');
+	new cron('*/30 * * * *', Notifications.prune, null, true);
+};
+
+Notifications.get = function (nid, callback) {
+	Notifications.getMultiple([nid], function (err, notifications) {
+		callback(err, Array.isArray(notifications) && notifications.length ? notifications[0] : null);
+	});
+};
+
+Notifications.getMultiple = function (nids, callback) {
+	if (!nids.length) {
+		return setImmediate(callback, null, []);
+	}
+	var keys = nids.map(function (nid) {
+		return 'notifications:' + nid;
+	});
+
+	var notifications;
+
+	async.waterfall([
+		function (next) {
+			db.getObjects(keys, next);
+		},
+		function (_notifications, next) {
+			notifications = _notifications;
+			var userKeys = notifications.map(function (notification) {
+				return notification && notification.from;
+			});
 
-	Notifications.getMultiple = function (nids, callback) {
-		if (!nids.length) {
-			return setImmediate(callback, null, []);
-		}
-		var keys = nids.map(function (nid) {
-			return 'notifications:' + nid;
-		});
+			User.getUsersFields(userKeys, ['username', 'userslug', 'picture'], next);
+		},
+		function (usersData, next) {
+			notifications.forEach(function (notification, index) {
+				if (notification) {
+					notification.datetimeISO = utils.toISOString(notification.datetime);
 
-		var notifications;
+					if (notification.bodyLong) {
+						notification.bodyLong = S(notification.bodyLong).escapeHTML().s;
+					}
 
-		async.waterfall([
-			function (next) {
-				db.getObjects(keys, next);
-			},
-			function (_notifications, next) {
-				notifications = _notifications;
-				var userKeys = notifications.map(function (notification) {
-					return notification && notification.from;
-				});
+					notification.user = usersData[index];
+					if (notification.user) {
+						notification.image = notification.user.picture || null;
+						if (notification.user.username === '[[global:guest]]') {
+							notification.bodyShort = notification.bodyShort.replace(/([\s\S]*?),[\s\S]*?,([\s\S]*?)/, '$1, [[global:guest]], $2');
+						}
+					} else if (notification.image === 'brand:logo' || !notification.image) {
+						notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png';
+					}
+				}
+			});
 
-				User.getUsersFields(userKeys, ['username', 'userslug', 'picture'], next);
-			},
-			function (usersData, next) {
-				notifications.forEach(function (notification, index) {
-					if (notification) {
-						notification.datetimeISO = utils.toISOString(notification.datetime);
+			next(null, notifications);
+		},
+	], callback);
+};
+
+Notifications.filterExists = function (nids, callback) {
+	async.waterfall([
+		function (next) {
+			db.isSortedSetMembers('notifications', nids, next);
+		},
+		function (exists, next) {
+			nids = nids.filter(function (notifId, idx) {
+				return exists[idx];
+			});
 
-						if (notification.bodyLong) {
-							notification.bodyLong = S(notification.bodyLong).escapeHTML().s;
-						}
+			next(null, nids);
+		},
+	], callback);
+};
 
-						notification.user = usersData[index];
-						if (notification.user) {
-							notification.image = notification.user.picture || null;
-							if (notification.user.username === '[[global:guest]]') {
-								notification.bodyShort = notification.bodyShort.replace(/([\s\S]*?),[\s\S]*?,([\s\S]*?)/, '$1, [[global:guest]], $2');
-							}
-						} else if (notification.image === 'brand:logo' || !notification.image) {
-							notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png';
-						}
-					}
-				});
+Notifications.findRelated = function (mergeIds, set, callback) {
+	// A related notification is one in a zset that has the same mergeId
+	var _nids;
 
-				next(null, notifications);
-			},
-		], callback);
-	};
+	async.waterfall([
+		async.apply(db.getSortedSetRevRange, set, 0, -1),
+		function (nids, next) {
+			_nids = nids;
 
-	Notifications.filterExists = function (nids, callback) {
-		async.waterfall([
-			function (next) {
-				db.isSortedSetMembers('notifications', nids, next);
-			},
-			function (exists, next) {
-				nids = nids.filter(function (notifId, idx) {
-					return exists[idx];
-				});
+			var keys = nids.map(function (nid) {
+				return 'notifications:' + nid;
+			});
 
-				next(null, nids);
-			},
-		], callback);
-	};
+			db.getObjectsFields(keys, ['mergeId'], next);
+		},
+	], function (err, sets) {
+		if (err) {
+			return callback(err);
+		}
 
-	Notifications.findRelated = function (mergeIds, set, callback) {
-		// A related notification is one in a zset that has the same mergeId
-		var _nids;
+		sets = sets.map(function (set) {
+			return set.mergeId;
+		});
 
-		async.waterfall([
-			async.apply(db.getSortedSetRevRange, set, 0, -1),
-			function (nids, next) {
-				_nids = nids;
+		callback(null, _nids.filter(function (nid, idx) {
+			return mergeIds.indexOf(sets[idx]) !== -1;
+		}));
+	});
+};
 
-				var keys = nids.map(function (nid) {
-					return 'notifications:' + nid;
-				});
+Notifications.create = function (data, callback) {
+	if (!data.nid) {
+		return callback(new Error('no-notification-id'));
+	}
+	data.importance = data.importance || 5;
+	db.getObject('notifications:' + data.nid, function (err, oldNotification) {
+		if (err) {
+			return callback(err);
+		}
 
-				db.getObjectsFields(keys, ['mergeId'], next);
-			},
-		], function (err, sets) {
-			if (err) {
-				return callback(err);
+		if (oldNotification) {
+			if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) {
+				return callback(null, null);
 			}
+		}
 
-			sets = sets.map(function (set) {
-				return set.mergeId;
-			});
-
-			callback(null, _nids.filter(function (nid, idx) {
-				return mergeIds.indexOf(sets[idx]) !== -1;
-			}));
+		var now = Date.now();
+		data.datetime = now;
+		async.parallel([
+			function (next) {
+				db.sortedSetAdd('notifications', now, data.nid, next);
+			},
+			function (next) {
+				db.setObject('notifications:' + data.nid, data, next);
+			},
+		], function (err) {
+			callback(err, data);
 		});
-	};
+	});
+};
 
-	Notifications.create = function (data, callback) {
-		if (!data.nid) {
-			return callback(new Error('no-notification-id'));
-		}
-		data.importance = data.importance || 5;
-		db.getObject('notifications:' + data.nid, function (err, oldNotification) {
-			if (err) {
-				return callback(err);
-			}
+Notifications.push = function (notification, uids, callback) {
+	callback = callback || function () {};
 
-			if (oldNotification) {
-				if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) {
-					return callback(null, null);
-				}
-			}
+	if (!notification || !notification.nid) {
+		return callback();
+	}
 
-			var now = Date.now();
-			data.datetime = now;
-			async.parallel([
-				function (next) {
-					db.sortedSetAdd('notifications', now, data.nid, next);
-				},
-				function (next) {
-					db.setObject('notifications:' + data.nid, data, next);
-				},
-			], function (err) {
-				callback(err, data);
-			});
+	if (!Array.isArray(uids)) {
+		uids = [uids];
+	}
+
+	uids = uids.filter(function (uid, index, array) {
+		return parseInt(uid, 10) && array.indexOf(uid) === index;
+	});
+
+	if (!uids.length) {
+		return callback();
+	}
+
+	setTimeout(function () {
+		batch.processArray(uids, function (uids, next) {
+			pushToUids(uids, notification, next);
+		}, { interval: 1000 }, function (err) {
+			if (err) {
+				winston.error(err.stack);
+			}
 		});
-	};
+	}, 1000);
+
+	callback();
+};
+
+function pushToUids(uids, notification, callback) {
+	var oneWeekAgo = Date.now() - 604800000;
+	var unreadKeys = [];
+	var readKeys = [];
+
+	async.waterfall([
+		function (next) {
+			plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }, next);
+		},
+		function (data, next) {
+			uids = data.uids;
+			notification = data.notification;
+
+			uids.forEach(function (uid) {
+				unreadKeys.push('uid:' + uid + ':notifications:unread');
+				readKeys.push('uid:' + uid + ':notifications:read');
+			});
 
-	Notifications.push = function (notification, uids, callback) {
-		callback = callback || function () {};
+			db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid, next);
+		},
+		function (next) {
+			db.sortedSetsRemove(readKeys, notification.nid, next);
+		},
+		function (next) {
+			db.sortedSetsRemoveRangeByScore(unreadKeys, '-inf', oneWeekAgo, next);
+		},
+		function (next) {
+			db.sortedSetsRemoveRangeByScore(readKeys, '-inf', oneWeekAgo, next);
+		},
+		function (next) {
+			var websockets = require('./socket.io');
+			if (websockets.server) {
+				uids.forEach(function (uid) {
+					websockets.in('uid_' + uid).emit('event:new_notification', notification);
+				});
+			}
 
-		if (!notification || !notification.nid) {
-			return callback();
+			plugins.fireHook('action:notification.pushed', { notification: notification, uids: uids });
+			next();
+		},
+	], callback);
+}
+
+Notifications.pushGroup = function (notification, groupName, callback) {
+	callback = callback || function () {};
+	groups.getMembers(groupName, 0, -1, function (err, members) {
+		if (err || !Array.isArray(members) || !members.length) {
+			return callback(err);
 		}
 
-		if (!Array.isArray(uids)) {
-			uids = [uids];
+		Notifications.push(notification, members, callback);
+	});
+};
+
+Notifications.pushGroups = function (notification, groupNames, callback) {
+	callback = callback || function () {};
+	groups.getMembersOfGroups(groupNames, function (err, groupMembers) {
+		if (err) {
+			return callback(err);
 		}
 
-		uids = uids.filter(function (uid, index, array) {
-			return parseInt(uid, 10) && array.indexOf(uid) === index;
-		});
+		var members = _.unique(_.flatten(groupMembers));
 
-		if (!uids.length) {
-			return callback();
-		}
+		Notifications.push(notification, members, callback);
+	});
+};
 
-		setTimeout(function () {
-			batch.processArray(uids, function (uids, next) {
-				pushToUids(uids, notification, next);
-			}, { interval: 1000 }, function (err) {
-				if (err) {
-					winston.error(err.stack);
-				}
-			});
-		}, 1000);
+Notifications.rescind = function (nid, callback) {
+	callback = callback || function () {};
 
-		callback();
-	};
+	async.parallel([
+		async.apply(db.sortedSetRemove, 'notifications', nid),
+		async.apply(db.delete, 'notifications:' + nid),
+	], function (err) {
+		if (err) {
+			winston.error('Encountered error rescinding notification (' + nid + '): ' + err.message);
+		} else {
+			winston.verbose('[notifications/rescind] Rescinded notification "' + nid + '"');
+		}
 
-	function pushToUids(uids, notification, callback) {
-		var oneWeekAgo = Date.now() - 604800000;
-		var unreadKeys = [];
-		var readKeys = [];
+		callback(err, nid);
+	});
+};
 
-		async.waterfall([
-			function (next) {
-				plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }, next);
-			},
-			function (data, next) {
-				uids = data.uids;
-				notification = data.notification;
+Notifications.markRead = function (nid, uid, callback) {
+	callback = callback || function () {};
+	if (!parseInt(uid, 10) || !nid) {
+		return callback();
+	}
+	Notifications.markReadMultiple([nid], uid, callback);
+};
 
-				uids.forEach(function (uid) {
-					unreadKeys.push('uid:' + uid + ':notifications:unread');
-					readKeys.push('uid:' + uid + ':notifications:read');
-				});
+Notifications.markUnread = function (nid, uid, callback) {
+	callback = callback || function () {};
+	if (!parseInt(uid, 10) || !nid) {
+		return callback();
+	}
 
-				db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid, next);
-			},
-			function (next) {
-				db.sortedSetsRemove(readKeys, notification.nid, next);
-			},
-			function (next) {
-				db.sortedSetsRemoveRangeByScore(unreadKeys, '-inf', oneWeekAgo, next);
-			},
-			function (next) {
-				db.sortedSetsRemoveRangeByScore(readKeys, '-inf', oneWeekAgo, next);
-			},
-			function (next) {
-				var websockets = require('./socket.io');
-				if (websockets.server) {
-					uids.forEach(function (uid) {
-						websockets.in('uid_' + uid).emit('event:new_notification', notification);
-					});
-				}
+	db.getObject('notifications:' + nid, function (err, notification) {
+		if (err || !notification) {
+			return callback(err || new Error('[[error:no-notification]]'));
+		}
+		notification.datetime = notification.datetime || Date.now();
 
-				plugins.fireHook('action:notification.pushed', { notification: notification, uids: uids });
-				next();
-			},
+		async.parallel([
+			async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid),
+			async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid),
 		], callback);
+	});
+};
+
+Notifications.markReadMultiple = function (nids, uid, callback) {
+	callback = callback || function () {};
+	nids = nids.filter(Boolean);
+	if (!Array.isArray(nids) || !nids.length) {
+		return callback();
 	}
 
-	Notifications.pushGroup = function (notification, groupName, callback) {
-		callback = callback || function () {};
-		groups.getMembers(groupName, 0, -1, function (err, members) {
-			if (err || !Array.isArray(members) || !members.length) {
-				return callback(err);
-			}
+	var notificationKeys = nids.map(function (nid) {
+		return 'notifications:' + nid;
+	});
 
-			Notifications.push(notification, members, callback);
-		});
-	};
+	async.waterfall([
+		async.apply(db.getObjectsFields, notificationKeys, ['mergeId']),
+		function (mergeIds, next) {
+			// Isolate mergeIds and find related notifications
+			mergeIds = mergeIds.map(function (set) {
+				return set.mergeId;
+			}).reduce(function (memo, mergeId, idx, arr) {
+				if (mergeId && idx === arr.indexOf(mergeId)) {
+					memo.push(mergeId);
+				}
+				return memo;
+			}, []);
 
-	Notifications.pushGroups = function (notification, groupNames, callback) {
-		callback = callback || function () {};
-		groups.getMembersOfGroups(groupNames, function (err, groupMembers) {
-			if (err) {
-				return callback(err);
-			}
+			Notifications.findRelated(mergeIds, 'uid:' + uid + ':notifications:unread', next);
+		},
+		function (relatedNids, next) {
+			notificationKeys = _.union(nids, relatedNids).map(function (nid) {
+				return 'notifications:' + nid;
+			});
+
+			db.getObjectsFields(notificationKeys, ['nid', 'datetime'], next);
+		},
+	], function (err, notificationData) {
+		if (err) {
+			return callback(err);
+		}
 
-			var members = _.unique(_.flatten(groupMembers));
+		// Filter out notifications that didn't exist
+		notificationData = notificationData.filter(function (notification) {
+			return notification && notification.nid;
+		});
 
-			Notifications.push(notification, members, callback);
+		// Extract nid
+		nids = notificationData.map(function (notification) {
+			return notification.nid;
 		});
-	};
 
-	Notifications.rescind = function (nid, callback) {
-		callback = callback || function () {};
+		var datetimes = notificationData.map(function (notification) {
+			return (notification && notification.datetime) || Date.now();
+		});
 
 		async.parallel([
-			async.apply(db.sortedSetRemove, 'notifications', nid),
-			async.apply(db.delete, 'notifications:' + nid),
+			function (next) {
+				db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next);
+			},
+			function (next) {
+				db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next);
+			},
 		], function (err) {
-			if (err) {
-				winston.error('Encountered error rescinding notification (' + nid + '): ' + err.message);
-			} else {
-				winston.verbose('[notifications/rescind] Rescinded notification "' + nid + '"');
-			}
-
-			callback(err, nid);
+			callback(err);
 		});
-	};
+	});
+};
 
-	Notifications.markRead = function (nid, uid, callback) {
-		callback = callback || function () {};
-		if (!parseInt(uid, 10) || !nid) {
-			return callback();
+Notifications.markAllRead = function (uid, callback) {
+	db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) {
+		if (err) {
+			return callback(err);
 		}
-		Notifications.markReadMultiple([nid], uid, callback);
-	};
 
-	Notifications.markUnread = function (nid, uid, callback) {
-		callback = callback || function () {};
-		if (!parseInt(uid, 10) || !nid) {
+		if (!Array.isArray(nids) || !nids.length) {
 			return callback();
 		}
 
-		db.getObject('notifications:' + nid, function (err, notification) {
-			if (err || !notification) {
-				return callback(err || new Error('[[error:no-notification]]'));
-			}
-			notification.datetime = notification.datetime || Date.now();
+		Notifications.markReadMultiple(nids, uid, callback);
+	});
+};
 
-			async.parallel([
-				async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid),
-				async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid),
-			], callback);
-		});
-	};
+Notifications.prune = function () {
+	var	week = 604800000;
+
+	var	cutoffTime = Date.now() - week;
+
+	db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function (err, nids) {
+		if (err) {
+			return winston.error(err.message);
+		}
 
-	Notifications.markReadMultiple = function (nids, uid, callback) {
-		callback = callback || function () {};
-		nids = nids.filter(Boolean);
 		if (!Array.isArray(nids) || !nids.length) {
-			return callback();
+			return;
 		}
 
-		var notificationKeys = nids.map(function (nid) {
+		var	keys = nids.map(function (nid) {
 			return 'notifications:' + nid;
 		});
 
-		async.waterfall([
-			async.apply(db.getObjectsFields, notificationKeys, ['mergeId']),
-			function (mergeIds, next) {
-				// Isolate mergeIds and find related notifications
-				mergeIds = mergeIds.map(function (set) {
-					return set.mergeId;
-				}).reduce(function (memo, mergeId, idx, arr) {
-					if (mergeId && idx === arr.indexOf(mergeId)) {
-						memo.push(mergeId);
-					}
-					return memo;
-				}, []);
-
-				Notifications.findRelated(mergeIds, 'uid:' + uid + ':notifications:unread', next);
+		async.parallel([
+			function (next) {
+				db.sortedSetRemove('notifications', nids, next);
 			},
-			function (relatedNids, next) {
-				notificationKeys = _.union(nids, relatedNids).map(function (nid) {
-					return 'notifications:' + nid;
-				});
-
-				db.getObjectsFields(notificationKeys, ['nid', 'datetime'], next);
+			function (next) {
+				db.deleteAll(keys, next);
 			},
-		], function (err, notificationData) {
+		], function (err) {
 			if (err) {
-				return callback(err);
+				return winston.error('Encountered error pruning notifications: ' + err.message);
 			}
-
-			// Filter out notifications that didn't exist
-			notificationData = notificationData.filter(function (notification) {
-				return notification && notification.nid;
-			});
-
-			// Extract nid
-			nids = notificationData.map(function (notification) {
-				return notification.nid;
-			});
-
-			var datetimes = notificationData.map(function (notification) {
-				return (notification && notification.datetime) || Date.now();
-			});
-
-			async.parallel([
-				function (next) {
-					db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next);
-				},
-				function (next) {
-					db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next);
-				},
-			], function (err) {
-				callback(err);
-			});
 		});
-	};
-
-	Notifications.markAllRead = function (uid, callback) {
-		db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) {
-			if (err) {
-				return callback(err);
+	});
+};
+
+Notifications.merge = function (notifications, callback) {
+	// When passed a set of notification objects, merge any that can be merged
+	var mergeIds = [
+		'notifications:upvoted_your_post_in',
+		'notifications:user_started_following_you',
+		'notifications:user_posted_to',
+		'notifications:user_flagged_post_in',
+		'notifications:user_flagged_user',
+		'new_register',
+	];
+	var isolated;
+	var differentiators;
+	var differentiator;
+	var modifyIndex;
+	var set;
+
+	notifications = mergeIds.reduce(function (notifications, mergeId) {
+		isolated = notifications.filter(function (notifObj) {
+			if (!notifObj || !notifObj.hasOwnProperty('mergeId')) {
+				return false;
 			}
 
-			if (!Array.isArray(nids) || !nids.length) {
-				return callback();
-			}
-
-			Notifications.markReadMultiple(nids, uid, callback);
+			return notifObj.mergeId.split('|')[0] === mergeId;
 		});
-	};
-
-	Notifications.prune = function () {
-		var	week = 604800000;
 
-		var	cutoffTime = Date.now() - week;
-
-		db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function (err, nids) {
-			if (err) {
-				return winston.error(err.message);
-			}
+		if (isolated.length <= 1) {
+			return notifications;	// Nothing to merge
+		}
 
-			if (!Array.isArray(nids) || !nids.length) {
-				return;
+		// Each isolated mergeId may have multiple differentiators, so process each separately
+		differentiators = isolated.reduce(function (cur, next) {
+			differentiator = next.mergeId.split('|')[1] || 0;
+			if (cur.indexOf(differentiator) === -1) {
+				cur.push(differentiator);
 			}
 
-			var	keys = nids.map(function (nid) {
-				return 'notifications:' + nid;
-			});
-
-			async.parallel([
-				function (next) {
-					db.sortedSetRemove('notifications', nids, next);
-				},
-				function (next) {
-					db.deleteAll(keys, next);
-				},
-			], function (err) {
-				if (err) {
-					return winston.error('Encountered error pruning notifications: ' + err.message);
-				}
-			});
-		});
-	};
-
-	Notifications.merge = function (notifications, callback) {
-		// When passed a set of notification objects, merge any that can be merged
-		var mergeIds = [
-			'notifications:upvoted_your_post_in',
-			'notifications:user_started_following_you',
-			'notifications:user_posted_to',
-			'notifications:user_flagged_post_in',
-			'notifications:user_flagged_user',
-			'new_register',
-		];
-		var isolated;
-		var differentiators;
-		var differentiator;
-		var modifyIndex;
-		var set;
-
-		notifications = mergeIds.reduce(function (notifications, mergeId) {
-			isolated = notifications.filter(function (notifObj) {
-				if (!notifObj || !notifObj.hasOwnProperty('mergeId')) {
-					return false;
-				}
-
-				return notifObj.mergeId.split('|')[0] === mergeId;
-			});
+			return cur;
+		}, []);
 
-			if (isolated.length <= 1) {
-				return notifications;	// Nothing to merge
+		differentiators.forEach(function (differentiator) {
+			if (differentiator === 0 && differentiators.length === 1) {
+				set = isolated;
+			} else {
+				set = isolated.filter(function (notifObj) {
+					return notifObj.mergeId === (mergeId + '|' + differentiator);
+				});
 			}
 
-			// Each isolated mergeId may have multiple differentiators, so process each separately
-			differentiators = isolated.reduce(function (cur, next) {
-				differentiator = next.mergeId.split('|')[1] || 0;
-				if (cur.indexOf(differentiator) === -1) {
-					cur.push(differentiator);
-				}
+			modifyIndex = notifications.indexOf(set[0]);
+			if (modifyIndex === -1 || set.length === 1) {
+				return notifications;
+			}
 
-				return cur;
-			}, []);
+			switch (mergeId) {
+			// intentional fall-through
+			case 'notifications:upvoted_your_post_in':
+			case 'notifications:user_started_following_you':
+			case 'notifications:user_posted_to':
+			case 'notifications:user_flagged_post_in':
+			case 'notifications:user_flagged_user':
+				var usernames = set.map(function (notifObj) {
+					return notifObj && notifObj.user && notifObj.user.username;
+				}).filter(function (username, idx, array) {
+					return array.indexOf(username) === idx;
+				});
+				var numUsers = usernames.length;
 
-			differentiators.forEach(function (differentiator) {
-				if (differentiator === 0 && differentiators.length === 1) {
-					set = isolated;
-				} else {
-					set = isolated.filter(function (notifObj) {
-						return notifObj.mergeId === (mergeId + '|' + differentiator);
-					});
-				}
+				var title = S(notifications[modifyIndex].topicTitle || '').decodeHTMLEntities().s;
+				var titleEscaped = title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
+				titleEscaped = titleEscaped ? (', ' + titleEscaped) : '';
 
-				modifyIndex = notifications.indexOf(set[0]);
-				if (modifyIndex === -1 || set.length === 1) {
-					return notifications;
+				if (numUsers === 2) {
+					notifications[modifyIndex].bodyShort = '[[' + mergeId + '_dual, ' + usernames.join(', ') + titleEscaped + ']]';
+				} else if (numUsers > 2) {
+					notifications[modifyIndex].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers - 1) + titleEscaped + ']]';
 				}
 
-				switch (mergeId) {
-				// intentional fall-through
-				case 'notifications:upvoted_your_post_in':
-				case 'notifications:user_started_following_you':
-				case 'notifications:user_posted_to':
-				case 'notifications:user_flagged_post_in':
-				case 'notifications:user_flagged_user':
-					var usernames = set.map(function (notifObj) {
-						return notifObj && notifObj.user && notifObj.user.username;
-					}).filter(function (username, idx, array) {
-						return array.indexOf(username) === idx;
-					});
-					var numUsers = usernames.length;
-
-					var title = S(notifications[modifyIndex].topicTitle || '').decodeHTMLEntities().s;
-					var titleEscaped = title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
-					titleEscaped = titleEscaped ? (', ' + titleEscaped) : '';
-
-					if (numUsers === 2) {
-						notifications[modifyIndex].bodyShort = '[[' + mergeId + '_dual, ' + usernames.join(', ') + titleEscaped + ']]';
-					} else if (numUsers > 2) {
-						notifications[modifyIndex].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers - 1) + titleEscaped + ']]';
-					}
+				notifications[modifyIndex].path = set[set.length - 1].path;
+				break;
 
-					notifications[modifyIndex].path = set[set.length - 1].path;
-					break;
+			case 'new_register':
+				notifications[modifyIndex].bodyShort = '[[notifications:' + mergeId + '_multiple, ' + set.length + ']]';
+				break;
+			}
 
-				case 'new_register':
-					notifications[modifyIndex].bodyShort = '[[notifications:' + mergeId + '_multiple, ' + set.length + ']]';
-					break;
+			// Filter out duplicates
+			notifications = notifications.filter(function (notifObj, idx) {
+				if (!notifObj || !notifObj.mergeId) {
+					return true;
 				}
 
-				// Filter out duplicates
-				notifications = notifications.filter(function (notifObj, idx) {
-					if (!notifObj || !notifObj.mergeId) {
-						return true;
-					}
-
-					return !(notifObj.mergeId === (mergeId + (differentiator ? '|' + differentiator : '')) && idx !== modifyIndex);
-				});
+				return !(notifObj.mergeId === (mergeId + (differentiator ? '|' + differentiator : '')) && idx !== modifyIndex);
 			});
-
-			return notifications;
-		}, notifications);
-
-		plugins.fireHook('filter:notifications.merge', {
-			notifications: notifications,
-		}, function (err, data) {
-			callback(err, data.notifications);
 		});
-	};
-}(exports));
 
+		return notifications;
+	}, notifications);
+
+	plugins.fireHook('filter:notifications.merge', {
+		notifications: notifications,
+	}, function (err, data) {
+		callback(err, data.notifications);
+	});
+};
diff --git a/src/start.js b/src/start.js
index 2a6e53f507..8d4e465a55 100644
--- a/src/start.js
+++ b/src/start.js
@@ -48,7 +48,7 @@ start.start = function () {
 			require('./socket.io').init(webserver.server);
 
 			if (nconf.get('isPrimary') === 'true' && !nconf.get('jobsDisabled')) {
-				require('./notifications').init();
+				require('./notifications').startJobs();
 				require('./user').startJobs();
 			}