<commit_before>var path = require('path');
var p = require('pfork');
var fs = require('fs');
var fse = require('fs-extra');
var url = require('url');
var extend = require('util')._extend;
var EventEmitter = require('events').EventEmitter;
var pluginMgr = new EventEmitter();
var colors = require('colors/safe');
var comUtil = require('../util');
var logger = require('../util/logger');
var util = require('./util');
var config = require('../config');
var getPluginsSync = require('./get-plugins-sync');
var getPlugin = require('./get-plugins');
var rulesMgr = require('../rules');
var RulesMgr = require('../rules/rules');
var properties = require('../rules/util').properties;
var PLUGIN_MAIN = path.join(__dirname, './load-plugin');
var PLUGIN_URL_RE = /^(?:http|ws)s?:\/\/([\da-z]+)\.local\.whistlejs\.com\//i;
var RULE_VALUE_HEADER = 'x-whistle-rule-value';
var SSL_FLAG_HEADER = 'x-whistle-https';
var FULL_URL_HEADER = 'x-whistle-full-url';
var REAL_URL_HEADER = 'x-whistle-real-url';
var NEXT_RULE_HEADER = 'x-whistle-next-rule';
var REQ_ID_HEADER = 'x-whistle-req-id';
var STATUS_CODE_HEADER = 'x-whistle-status-code';
var LOCAL_HOST_HEADER = 'x-whistle-local-host';
var METHOD_HEADER = 'x-whistle-method';
var INTERVAL = 6000;
var UTF8_OPTIONS = {encoding: 'utf8'};
var plugins = getPluginsSync();
var LOCALHOST = '127.0.0.1';
var conf = {};

Object.keys(config).forEach(function(name) {
	var value = config[name];
	if (typeof value == 'string' || typeof value == 'number') {
		conf[name] = value;
	}
});

pluginMgr.on('updateRules', function() {
	rulesMgr.clearAppend();
	Object.keys(plugins).sort(function(a, b) {
		var p1 = plugins[a];
		var p2 = plugins[b];
		return (p1.mtime > p2.mtime) ? 1 : -1;
	}).forEach(function(name) {
		if (pluginIsDisabled(name.slice(0, -1))) {
			return;
		}
		var plugin = plugins[name];
		if (plugin._rules && !plugin.rulesMgr) {
			plugin.rulesMgr = new RulesMgr();
			plugin.rulesMgr.parse(plugin._rules, plugin.path);
		}
		plugin.rules && rulesMgr.append(plugin.rules, plugin.path);
	});
});
pluginMgr.emit('updateRules');

pluginMgr.updateRules = function() {
	pluginMgr.emit('updateRules');
};

pluginMgr.on('update', function(result) {
	Object.keys(result).forEach(function(name) {
		pluginMgr.stopPlugin(result[name]);
	});
});
pluginMgr.on('uninstall', function(result) {
	Object.keys(result).forEach(function(name) {
		pluginMgr.stopPlugin(result[name]);
	});
});

function showVerbose(oldData, newData) {
	if (!config.debugMode) {
		return;
	}
	var uninstallData, installData, updateData;
	Object.keys(oldData).forEach(function(name) {
		var oldItem = oldData[name];
		var newItem = newData[name];
		if (!newItem) {
			uninstallData = uninstallData || {};
			uninstallData[name] = oldItem;
		} else if (newItem.path != oldItem.path || newItem.mtime != oldItem.mtime) {
			updateData = updateData || {};
			updateData[name] = newItem;
		}
	});

	Object.keys(newData).forEach(function(name) {
		if (!oldData[name]) {
			installData = installData || {};
			installData[name] = newData[name];
		}
	});

	if (uninstallData || installData || updateData) {
		console.log('\n***********[%s] %s has changed***********', comUtil.formatDate(), 'plugins');
	}

	uninstallData && Object.keys(uninstallData).forEach(function(name) {
		console.log(colors.red('[' + comUtil.formatDate(new Date(uninstallData[name].mtime)) + '] [uninstall plugin] ' + name.slice(0, -1)));
	});
	installData && Object.keys(installData).forEach(function(name) {
		console.log(colors.green('[' + comUtil.formatDate(new Date(installData[name].mtime)) + '] [install plugin] ' + name.slice(0, -1)));
	});
	updateData && Object.keys(updateData).forEach(function(name) {
		console.log(colors.yellow('[' + comUtil.formatDate(new Date(updateData[name].mtime)) + '] [update plugin] ' + name.slice(0, -1)));
	});
}

function readPackages(obj, callback) {
	var _plugins = {};
	var count = 0;
	var callbackHandler = function() {
		if (--count <= 0) {
			callback(_plugins);
		}
	};
	Object.keys(obj).forEach(function(name) {
		var pkg = plugins[name];
		var newPkg = obj[name];
		if (!pkg || pkg.path != newPkg.path || pkg.mtime != newPkg.mtime) {
			++count;
			fse.readJson(newPkg.pkgPath, function(err, pkg) {
				if (pkg && pkg.version) {
					newPkg.version = pkg.version;
					newPkg.homepage = util.getHomePageFromPackage(pkg);
					newPkg.description = pkg.description;
					newPkg.moduleName = pkg.name;
					_plugins[name] = newPkg;
					fs.readFile(path.join(path.join(newPkg.path, 'rules.txt')), UTF8_OPTIONS, function(err, rulesText) {
						newPkg.rules = comUtil.trim(rulesText);
						fs.readFile(path.join(path.join(newPkg.path, '_rules.txt')), UTF8_OPTIONS, function(err, rulesText) {
							newPkg._rules = comUtil.trim(rulesText);
							callbackHandler();
						});
					});
				} else {
					callbackHandler();
				}

			});

		} else {
			_plugins[name] = pkg;
		}
	});

	if (count <= 0) {
		callback(_plugins);
	}
}

(function update() {
	setTimeout(function() {
		getPlugin(function(result) {
			readPackages(result, function(_plugins) {
				var updatePlugins, uninstallPlugins;
				Object.keys(plugins).forEach(function(name) {
					var plugin = plugins[name];
					var newPlugin = _plugins[name];
					if (!newPlugin) {
						uninstallPlugins = uninstallPlugins || {};
						uninstallPlugins[name] = plugin;
					} else if (newPlugin.path != plugin.path || newPlugin.mtime != plugin.mtime) {
						updatePlugins = updatePlugins || {};
						updatePlugins[name] = newPlugin;
					}
				});
				showVerbose(plugins, _plugins);
				plugins = _plugins;
				if (uninstallPlugins || updatePlugins) {
					uninstallPlugins && pluginMgr.emit('uninstall', uninstallPlugins);
					updatePlugins && pluginMgr.emit('update', updatePlugins);
					pluginMgr.emit('updateRules');
				}
				update();
			});
		});
	}, INTERVAL);
})();

pluginMgr.RULE_VALUE_HEADER = RULE_VALUE_HEADER;
pluginMgr.SSL_FLAG_HEADER = SSL_FLAG_HEADER;
pluginMgr.FULL_URL_HEADER = FULL_URL_HEADER;
pluginMgr.REAL_URL_HEADER = REAL_URL_HEADER;
pluginMgr.METHOD_HEADER = METHOD_HEADER;
pluginMgr.NEXT_RULE_HEADER = NEXT_RULE_HEADER;
pluginMgr.REQ_ID_HEADER = REQ_ID_HEADER;
pluginMgr.STATUS_CODE_HEADER = STATUS_CODE_HEADER;
pluginMgr.LOCAL_HOST_HEADER = LOCAL_HOST_HEADER;

function loadPlugin(plugin, callback) {
	config.debugMode && console.log(colors.cyan('[' + comUtil.formatDate() + '] [access plugin] ' + plugin.path));
	p.fork({
		name: plugin.moduleName,
		script: PLUGIN_MAIN,
		value: plugin.path,
		RULE_VALUE_HEADER: RULE_VALUE_HEADER,
		SSL_FLAG_HEADER: SSL_FLAG_HEADER,
		FULL_URL_HEADER: FULL_URL_HEADER,
		REAL_URL_HEADER: REAL_URL_HEADER,
		NEXT_RULE_HEADER: NEXT_RULE_HEADER,
		REQ_ID_HEADER: REQ_ID_HEADER,
		STATUS_CODE_HEADER: STATUS_CODE_HEADER,
		LOCAL_HOST_HEADER: LOCAL_HOST_HEADER,
		METHOD_HEADER: METHOD_HEADER,
		debugMode: config.debugMode,
		config: conf
	}, function(err, ports, child) {
		callback && callback(err, ports, child);
		logger.error(err);
		if (config.debugMode) {
			if (err) {
				console.log(colors.red(err));
			} else if (!child.debugMode) {
				child.debugMode = true;
				child.on('data', function(data) {
					if (data && data.type == 'console.log') {
						console.log('[' + comUtil.formatDate() + '] [plugin] [' + plugin.path.substring(plugin.path.lastIndexOf('.') + 1) + ']', data.message);
					}
				});
				child.sendData({
					type: 'console.log',
					status: 'ready'
				});
			}
		}
	});
}

pluginMgr.loadPlugin = loadPlugin;

pluginMgr.stopPlugin = function(plugin) {
	p.kill({
		script: PLUGIN_MAIN,
		value: plugin.path
	}, 10000);
};

pluginMgr.getPlugins = function() {
	return plugins;
};

function pluginIsDisabled(name) {
	if (properties.get('disabledAllPlugins')) {
		return true;
	}
	var disabledPlugins = properties.get('disabledPlugins') || {};
	return disabledPlugins[name];
}

pluginMgr.getPlugin = function(protocol) {
	return pluginIsDisabled(protocol.slice(0, -1)) ? null : plugins[protocol];
};

function getPluginByRuleUrl(ruleUrl) {
	if (!ruleUrl || typeof ruleUrl != 'string') {
		return;
	}
	var index = ruleUrl.indexOf(':');
	if (index == -1) {
		return null;
	}
	var protocol = ruleUrl.substring(0, index + 1);
	return pluginIsDisabled(protocol.slice(0, -1)) ? null : plugins[protocol];
}

pluginMgr.getPluginByRuleUrl = getPluginByRuleUrl;

pluginMgr.getPluginByHomePage = function(url) {
	return PLUGIN_URL_RE.test(url)
				&& plugins[RegExp.$1 + ':'];
};

function getRules(req, port, callback) {
	if (!port) {
		return callback();
	}
	var options = url.parse(req.fullUrl);
    options.headers = extend({}, req.headers);
    options.headers[REQ_ID_HEADER] = req.reqId;
    options.headers[FULL_URL_HEADER] = encodeURIComponent(req.fullUrl);
    options.headers[METHOD_HEADER] = encodeURIComponent(req.method || 'GET');
    var nextRule = rulesMgr.resolveRule(req.fullUrl, 1);
    if (nextRule) {
    	options.headers[NEXT_RULE_HEADER] = encodeURIComponent(nextRule.matcher);
    }
    var localHost = req.rules.host;
    if (localHost) {
    	options.headers[LOCAL_HOST_HEADER] = encodeURIComponent(util.removeProtocol(localHost.matcher, true));
    }
    if (options.protocol == 'https:' || options.protocol == 'wss:') {
        options.headers[SSL_FLAG_HEADER] = 'true';
    }

    options.protocol = 'http:';
    options.host = LOCALHOST;
    options.port = port;
    options.hostname = null;
    var ruleValue = comUtil.getMatcherValue(req.rules.rule);
    if (ruleValue) {
        options.headers[RULE_VALUE_HEADER] = encodeURIComponent(ruleValue);
    }
    delete options.headers.upgrade;
    delete options.headers.connection;
    comUtil.getResponseBody(options, callback);
}

pluginMgr.getRules = function(req, callback) {
	var plugin = req.pluginMgr = getPluginByRuleUrl(comUtil.rule.getUrl(req.rules.rule));
	if (!plugin) {
		return callback();
	}

	loadPlugin(plugin, function(err, ports) {
		getRules(req, ports && ports.rulesPort, function(err, body) {
			if (err || !body) {
				callback(plugin.rulesMgr, plugin);
			} else {
				if (body != plugin.__rules) {
					var rulesMgr = new RulesMgr();
					rulesMgr.parse(body + (plugin._rules ? '\n' + plugin._rules : ''), plugin.path);
					plugin._rulesMgr = rulesMgr;
					plugin.__rules = body;
				}
				callback(plugin._rulesMgr, plugin);
			}
		});
	});
};

function getResRules(req, res, port, callback) {
	if (!port) {
		return callback();
	}
	var options = url.parse(req.fullUrl);
    options.headers = extend({}, res.headers);
    options.headers[FULL_URL_HEADER] = encodeURIComponent(req.fullUrl);
    if (req.realUrl) {
    	options.headers[REAL_URL_HEADER] = encodeURIComponent(req.realUrl);
    }
    options.headers[REQ_ID_HEADER] = req.reqId;
    options.headers[METHOD_HEADER] = encodeURIComponent(req.method || 'GET');
    options.headers[LOCAL_HOST_HEADER] = encodeURIComponent(req.hostIp || LOCALHOST);
    options.headers[STATUS_CODE_HEADER] = encodeURIComponent(res.statusCode == null ? '' : res.statusCode + '');
    if (options.protocol == 'https:' || options.protocol == 'wss:') {
        options.headers[SSL_FLAG_HEADER] = 'true';
    }

    options.protocol = 'http:';
    options.host = LOCALHOST;
    options.port = port;
    options.hostname = null;
    var ruleValue = comUtil.getMatcherValue(req.rules.rule);
    if (ruleValue) {
        options.headers[RULE_VALUE_HEADER] = encodeURIComponent(ruleValue);
    }
    delete options.headers.upgrade;
    delete options.headers.connection;
    comUtil.getResponseBody(options, callback);
}

pluginMgr.getResRules = function(req, res, callback) {
	var plugin = req.pluginMgr;
	if (!plugin) {
		return callback();
	}

	loadPlugin(plugin, function(err, ports) {
		getResRules(req, res, ports && ports.resRulesPort, function(err, body) {
			if (err || !body) {
				callback(plugin.rulesMgr, plugin);
			} else {
				if (body != plugin.__resRules) {
					var rulesMgr = new RulesMgr();
					rulesMgr.parse(body, plugin.path);
					plugin._resRulesMgr = rulesMgr;
					plugin.__resRules = body;
				}
				callback(plugin._resRulesMgr, plugin);
			}
		});
	});
};

module.exports = pluginMgr;

<commit_msg>refactor: rm resolveLocalHost<commit_after>var path = require('path');
var p = require('pfork');
var fs = require('fs');
var fse = require('fs-extra');
var url = require('url');
var extend = require('util')._extend;
var EventEmitter = require('events').EventEmitter;
var pluginMgr = new EventEmitter();
var colors = require('colors/safe');
var comUtil = require('../util');
var logger = require('../util/logger');
var util = require('./util');
var config = require('../config');
var getPluginsSync = require('./get-plugins-sync');
var getPlugin = require('./get-plugins');
var rulesMgr = require('../rules');
var RulesMgr = require('../rules/rules');
var properties = require('../rules/util').properties;
var PLUGIN_MAIN = path.join(__dirname, './load-plugin');
var PLUGIN_URL_RE = /^(?:http|ws)s?:\/\/([\da-z]+)\.local\.whistlejs\.com\//i;
var RULE_VALUE_HEADER = 'x-whistle-rule-value';
var SSL_FLAG_HEADER = 'x-whistle-https';
var FULL_URL_HEADER = 'x-whistle-full-url';
var REAL_URL_HEADER = 'x-whistle-real-url';
var NEXT_RULE_HEADER = 'x-whistle-next-rule';
var REQ_ID_HEADER = 'x-whistle-req-id';
var STATUS_CODE_HEADER = 'x-whistle-status-code';
var LOCAL_HOST_HEADER = 'x-whistle-local-host';
var METHOD_HEADER = 'x-whistle-method';
var INTERVAL = 6000;
var UTF8_OPTIONS = {encoding: 'utf8'};
var plugins = getPluginsSync();
var LOCALHOST = '127.0.0.1';
var conf = {};

Object.keys(config).forEach(function(name) {
	var value = config[name];
	if (typeof value == 'string' || typeof value == 'number') {
		conf[name] = value;
	}
});

pluginMgr.on('updateRules', function() {
	rulesMgr.clearAppend();
	Object.keys(plugins).sort(function(a, b) {
		var p1 = plugins[a];
		var p2 = plugins[b];
		return (p1.mtime > p2.mtime) ? 1 : -1;
	}).forEach(function(name) {
		if (pluginIsDisabled(name.slice(0, -1))) {
			return;
		}
		var plugin = plugins[name];
		if (plugin._rules && !plugin.rulesMgr) {
			plugin.rulesMgr = new RulesMgr();
			plugin.rulesMgr.parse(plugin._rules, plugin.path);
		}
		plugin.rules && rulesMgr.append(plugin.rules, plugin.path);
	});
});
pluginMgr.emit('updateRules');

pluginMgr.updateRules = function() {
	pluginMgr.emit('updateRules');
};

pluginMgr.on('update', function(result) {
	Object.keys(result).forEach(function(name) {
		pluginMgr.stopPlugin(result[name]);
	});
});
pluginMgr.on('uninstall', function(result) {
	Object.keys(result).forEach(function(name) {
		pluginMgr.stopPlugin(result[name]);
	});
});

function showVerbose(oldData, newData) {
	if (!config.debugMode) {
		return;
	}
	var uninstallData, installData, updateData;
	Object.keys(oldData).forEach(function(name) {
		var oldItem = oldData[name];
		var newItem = newData[name];
		if (!newItem) {
			uninstallData = uninstallData || {};
			uninstallData[name] = oldItem;
		} else if (newItem.path != oldItem.path || newItem.mtime != oldItem.mtime) {
			updateData = updateData || {};
			updateData[name] = newItem;
		}
	});

	Object.keys(newData).forEach(function(name) {
		if (!oldData[name]) {
			installData = installData || {};
			installData[name] = newData[name];
		}
	});

	if (uninstallData || installData || updateData) {
		console.log('\n***********[%s] %s has changed***********', comUtil.formatDate(), 'plugins');
	}

	uninstallData && Object.keys(uninstallData).forEach(function(name) {
		console.log(colors.red('[' + comUtil.formatDate(new Date(uninstallData[name].mtime)) + '] [uninstall plugin] ' + name.slice(0, -1)));
	});
	installData && Object.keys(installData).forEach(function(name) {
		console.log(colors.green('[' + comUtil.formatDate(new Date(installData[name].mtime)) + '] [install plugin] ' + name.slice(0, -1)));
	});
	updateData && Object.keys(updateData).forEach(function(name) {
		console.log(colors.yellow('[' + comUtil.formatDate(new Date(updateData[name].mtime)) + '] [update plugin] ' + name.slice(0, -1)));
	});
}

function readPackages(obj, callback) {
	var _plugins = {};
	var count = 0;
	var callbackHandler = function() {
		if (--count <= 0) {
			callback(_plugins);
		}
	};
	Object.keys(obj).forEach(function(name) {
		var pkg = plugins[name];
		var newPkg = obj[name];
		if (!pkg || pkg.path != newPkg.path || pkg.mtime != newPkg.mtime) {
			++count;
			fse.readJson(newPkg.pkgPath, function(err, pkg) {
				if (pkg && pkg.version) {
					newPkg.version = pkg.version;
					newPkg.homepage = util.getHomePageFromPackage(pkg);
					newPkg.description = pkg.description;
					newPkg.moduleName = pkg.name;
					_plugins[name] = newPkg;
					fs.readFile(path.join(path.join(newPkg.path, 'rules.txt')), UTF8_OPTIONS, function(err, rulesText) {
						newPkg.rules = comUtil.trim(rulesText);
						fs.readFile(path.join(path.join(newPkg.path, '_rules.txt')), UTF8_OPTIONS, function(err, rulesText) {
							newPkg._rules = comUtil.trim(rulesText);
							callbackHandler();
						});
					});
				} else {
					callbackHandler();
				}

			});

		} else {
			_plugins[name] = pkg;
		}
	});

	if (count <= 0) {
		callback(_plugins);
	}
}

(function update() {
	setTimeout(function() {
		getPlugin(function(result) {
			readPackages(result, function(_plugins) {
				var updatePlugins, uninstallPlugins;
				Object.keys(plugins).forEach(function(name) {
					var plugin = plugins[name];
					var newPlugin = _plugins[name];
					if (!newPlugin) {
						uninstallPlugins = uninstallPlugins || {};
						uninstallPlugins[name] = plugin;
					} else if (newPlugin.path != plugin.path || newPlugin.mtime != plugin.mtime) {
						updatePlugins = updatePlugins || {};
						updatePlugins[name] = newPlugin;
					}
				});
				showVerbose(plugins, _plugins);
				plugins = _plugins;
				if (uninstallPlugins || updatePlugins) {
					uninstallPlugins && pluginMgr.emit('uninstall', uninstallPlugins);
					updatePlugins && pluginMgr.emit('update', updatePlugins);
					pluginMgr.emit('updateRules');
				}
				update();
			});
		});
	}, INTERVAL);
})();

pluginMgr.RULE_VALUE_HEADER = RULE_VALUE_HEADER;
pluginMgr.SSL_FLAG_HEADER = SSL_FLAG_HEADER;
pluginMgr.FULL_URL_HEADER = FULL_URL_HEADER;
pluginMgr.REAL_URL_HEADER = REAL_URL_HEADER;
pluginMgr.METHOD_HEADER = METHOD_HEADER;
pluginMgr.NEXT_RULE_HEADER = NEXT_RULE_HEADER;
pluginMgr.REQ_ID_HEADER = REQ_ID_HEADER;
pluginMgr.STATUS_CODE_HEADER = STATUS_CODE_HEADER;
pluginMgr.LOCAL_HOST_HEADER = LOCAL_HOST_HEADER;

function loadPlugin(plugin, callback) {
	config.debugMode && console.log(colors.cyan('[' + comUtil.formatDate() + '] [access plugin] ' + plugin.path));
	p.fork({
		name: plugin.moduleName,
		script: PLUGIN_MAIN,
		value: plugin.path,
		RULE_VALUE_HEADER: RULE_VALUE_HEADER,
		SSL_FLAG_HEADER: SSL_FLAG_HEADER,
		FULL_URL_HEADER: FULL_URL_HEADER,
		REAL_URL_HEADER: REAL_URL_HEADER,
		NEXT_RULE_HEADER: NEXT_RULE_HEADER,
		REQ_ID_HEADER: REQ_ID_HEADER,
		STATUS_CODE_HEADER: STATUS_CODE_HEADER,
		LOCAL_HOST_HEADER: LOCAL_HOST_HEADER,
		METHOD_HEADER: METHOD_HEADER,
		debugMode: config.debugMode,
		config: conf
	}, function(err, ports, child) {
		callback && callback(err, ports, child);
		logger.error(err);
		if (config.debugMode) {
			if (err) {
				console.log(colors.red(err));
			} else if (!child.debugMode) {
				child.debugMode = true;
				child.on('data', function(data) {
					if (data && data.type == 'console.log') {
						console.log('[' + comUtil.formatDate() + '] [plugin] [' + plugin.path.substring(plugin.path.lastIndexOf('.') + 1) + ']', data.message);
					}
				});
				child.sendData({
					type: 'console.log',
					status: 'ready'
				});
			}
		}
	});
}

pluginMgr.loadPlugin = loadPlugin;

pluginMgr.stopPlugin = function(plugin) {
	p.kill({
		script: PLUGIN_MAIN,
		value: plugin.path
	}, 10000);
};

pluginMgr.getPlugins = function() {
	return plugins;
};

function pluginIsDisabled(name) {
	if (properties.get('disabledAllPlugins')) {
		return true;
	}
	var disabledPlugins = properties.get('disabledPlugins') || {};
	return disabledPlugins[name];
}

pluginMgr.getPlugin = function(protocol) {
	return pluginIsDisabled(protocol.slice(0, -1)) ? null : plugins[protocol];
};

function getPluginByRuleUrl(ruleUrl) {
	if (!ruleUrl || typeof ruleUrl != 'string') {
		return;
	}
	var index = ruleUrl.indexOf(':');
	if (index == -1) {
		return null;
	}
	var protocol = ruleUrl.substring(0, index + 1);
	return pluginIsDisabled(protocol.slice(0, -1)) ? null : plugins[protocol];
}

pluginMgr.getPluginByRuleUrl = getPluginByRuleUrl;

pluginMgr.getPluginByHomePage = function(url) {
	return PLUGIN_URL_RE.test(url)
				&& plugins[RegExp.$1 + ':'];
};

function getRules(req, port, callback) {
	if (!port) {
		return callback();
	}
	var options = url.parse(req.fullUrl);
    options.headers = extend({}, req.headers);
    options.headers[REQ_ID_HEADER] = req.reqId;
    options.headers[FULL_URL_HEADER] = encodeURIComponent(req.fullUrl);
    options.headers[METHOD_HEADER] = encodeURIComponent(req.method || 'GET');
    var nextRule = rulesMgr.resolveRule(req.fullUrl, 1);
    if (nextRule) {
    	options.headers[NEXT_RULE_HEADER] = encodeURIComponent(nextRule.matcher);
    }
    var localHost = req.rules.host;
    if (localHost) {
    	options.headers[LOCAL_HOST_HEADER] = encodeURIComponent(comUtil.removeProtocol(localHost.matcher, true));
    }
    if (options.protocol == 'https:' || options.protocol == 'wss:') {
        options.headers[SSL_FLAG_HEADER] = 'true';
    }

    options.protocol = 'http:';
    options.host = LOCALHOST;
    options.port = port;
    options.hostname = null;
    var ruleValue = comUtil.getMatcherValue(req.rules.rule);
    if (ruleValue) {
        options.headers[RULE_VALUE_HEADER] = encodeURIComponent(ruleValue);
    }
    delete options.headers.upgrade;
    delete options.headers.connection;
    comUtil.getResponseBody(options, callback);
}

pluginMgr.getRules = function(req, callback) {
	var plugin = req.pluginMgr = getPluginByRuleUrl(comUtil.rule.getUrl(req.rules.rule));
	if (!plugin) {
		return callback();
	}

	loadPlugin(plugin, function(err, ports) {
		getRules(req, ports && ports.rulesPort, function(err, body) {
			if (err || !body) {
				callback(plugin.rulesMgr, plugin);
			} else {
				if (body != plugin.__rules) {
					var rulesMgr = new RulesMgr();
					rulesMgr.parse(body + (plugin._rules ? '\n' + plugin._rules : ''), plugin.path);
					plugin._rulesMgr = rulesMgr;
					plugin.__rules = body;
				}
				callback(plugin._rulesMgr, plugin);
			}
		});
	});
};

function getResRules(req, res, port, callback) {
	if (!port) {
		return callback();
	}
	var options = url.parse(req.fullUrl);
    options.headers = extend({}, res.headers);
    options.headers[FULL_URL_HEADER] = encodeURIComponent(req.fullUrl);
    if (req.realUrl) {
    	options.headers[REAL_URL_HEADER] = encodeURIComponent(req.realUrl);
    }
    options.headers[REQ_ID_HEADER] = req.reqId;
    options.headers[METHOD_HEADER] = encodeURIComponent(req.method || 'GET');
    options.headers[LOCAL_HOST_HEADER] = encodeURIComponent(req.hostIp || LOCALHOST);
    options.headers[STATUS_CODE_HEADER] = encodeURIComponent(res.statusCode == null ? '' : res.statusCode + '');
    if (options.protocol == 'https:' || options.protocol == 'wss:') {
        options.headers[SSL_FLAG_HEADER] = 'true';
    }

    options.protocol = 'http:';
    options.host = LOCALHOST;
    options.port = port;
    options.hostname = null;
    var ruleValue = comUtil.getMatcherValue(req.rules.rule);
    if (ruleValue) {
        options.headers[RULE_VALUE_HEADER] = encodeURIComponent(ruleValue);
    }
    delete options.headers.upgrade;
    delete options.headers.connection;
    comUtil.getResponseBody(options, callback);
}

pluginMgr.getResRules = function(req, res, callback) {
	var plugin = req.pluginMgr;
	if (!plugin) {
		return callback();
	}

	loadPlugin(plugin, function(err, ports) {
		getResRules(req, res, ports && ports.resRulesPort, function(err, body) {
			if (err || !body) {
				callback(plugin.rulesMgr, plugin);
			} else {
				if (body != plugin.__resRules) {
					var rulesMgr = new RulesMgr();
					rulesMgr.parse(body, plugin.path);
					plugin._resRulesMgr = rulesMgr;
					plugin.__resRules = body;
				}
				callback(plugin._resRulesMgr, plugin);
			}
		});
	});
};

module.exports = pluginMgr;

<|endoftext|>


<commit_before>var tileRadius = 20;
var boardSize = 10;

var tileData = [
  // letter, value, frequency
  ['A', 0, 7],
  ['B', 2, 3],
  ['C', 2, 3],
  ['D', 1, 3],
  ['E', 0, 10],
  ['F', 2, 3],
  ['G', 2, 4],
  ['H', 3, 2],
  ['I', 0, 7],
  ['J', 4, 1],
  ['K', 3, 2],
  ['L', 0, 4],
  ['M', 2, 3],
  ['N', 1, 4],
  ['O', 0, 7],
  ['P', 2, 3],
  ['Q', 4, 1],
  ['R', 1, 5],
  ['S', 1, 5],
  ['T', 1, 5],
  ['U', 1, 4],
  ['V', 4, 1],
  ['W', 3, 3],
  ['X', 4, 1],
  ['Y', 3, 2],
  ['Z', 4, 1],
  [' ', 0, 2]
];

// MODELS

var Game = Backbone.Model.extend({
  initialize: function () {
    this.total = 0;
    this.selected = new Tiles();
    this.board = new Board({ game: this, tiles: this.makeTiles(tileData) });
  },

  makeTiles: function (tileData) {
    var game = this;
    return _.shuffle(_.flatten(_.map(tileData, function (args) {
      return function (letter, value, freq) {
        return _.map(_.range(freq), function (i) {
          return new Tile({ game: game, letter: letter, value: value });
        })}.apply(game, args);
    })));
  },

  select: function (tile) {
    this.selected.add(tile);
  },

  unselect: function (tile) {
    this.selected.remove(tile);
  },

  reset: function () {
    while (this.selected.length > 0) {
      var tile = this.selected.at(0);
      tile.origin.placeTile(tile);
    }
  },

  getWord: function () {
    tiles = this.selected;
    if (tiles.length > 1) {
      // Determine if the tiles are in a line.
      var origin = tiles.at(0).space;
      var space = tiles.at(1).space
      var Δr = space.r - origin.r, Δc = space.c - origin.c;
      if (Δr >= -1 && Δr <= 1 && Δc >= -1 && Δc <= 1) {
        for (var i = 2; i < tiles.length; i++) {
          var next = tiles.at(i).space;
          if (next.r - space.r != Δr || next.c - space.c != Δc) return;
          space = next;
        }
        return tiles.pluck('letter').join('');
      }
    }
  },

  score: function () {
    var sum = 0;
    this.selected.each(function (tile) { sum += tile.value; });
    return sum * this.selected.length;
  },

  scoreWord: function () {
    this.total += this.score();
    // Copy to a fresh array first, rather than iterating over the
    // collection (which gets mutated during the iteration).
    var tiles = this.selected.toArray();
    _.invoke(tiles, 'remove');
  }
});

var Board = Backbone.Model.extend({
  initialize: function (attrs) {
    this.game = attrs.game;
    this.spaces = this.makeSpaces();
    this.placeTiles(attrs.tiles);
  },

  makeSpaces: function () {
    var board = this;
    return _.map(_.range(boardSize), function (r) {
      return _.map(_.range(boardSize), function (c) {
        return new Space({ board: board, r: r, c: c });
      });
    });
  },

  spaceAt: function (r, c) {
    return this.spaces[r][c];
  },

  placeTiles: function (tiles) {
    var board = this;
    _.each(_.range(boardSize), function (r) {
      _.each(_.range(boardSize), function (c) {
        // Leave the center four spaces blank.
        if (r < boardSize/2 - 1 || r > boardSize/2 ||
            c < boardSize/2 - 1 || c > boardSize/2) {
          var tile = tiles.pop();
          var space = board.spaceAt(r, c);
          tile.origin = space;
          space.placeTile(tile);
          tile.board = board;
        }
      });
    });
  }
});

var Space = Backbone.Model.extend({
  initialize: function (attrs) {
    this.board = attrs.board;
    this.r = attrs.r;
    this.c = attrs.c;
  },

  placeTile: function (tile) {
    var oldSpace = tile.space;
    if (oldSpace) oldSpace.tile = null;
    this.tile = tile;
    tile.moveTo(this);
    tile.updateSelection();
  },

  isBetween: function (s1, s2) {
    // TO DO: I suspect there is a simpler/quicker way to compute this.
    return (s1.r - s2.r)*(s1.c - this.c) == (s1.c - s2.c)*(s1.r - this.r) &&
      (s1.r <= this.r && this.r <= s2.r || s2.r <= this.r && this.r <= s1.r) &&
      (s1.c <= this.c && this.c <= s2.c || s2.c <= this.c && this.c <= s1.c);
  }
});

var Tile = Backbone.Model.extend({
  initialize: function (attrs) {
    this.game = attrs.game;
    this.letter = attrs.letter;
    this.value = attrs.value;
  },

  legalSpaces: function () {
    var tile = this;
    var board = this.board;
    var r0 = this.origin.r;
    var c0 = this.origin.c;
    var spaces = [];
    if (!this.origin.tile) {
      // A tile can't move back to its origin if it would be in the
      // way of another tile that's already moved.
      if (!this.game.selected.any(function (movedTile) {
        return tile != movedTile &&
          tile.origin.isBetween(movedTile.origin, movedTile.space);
      })) {
        spaces.push(this.origin);
      }
    }
    _.each([-1, 0, 1], function (Δr) {
      _.each([-1, 0, 1], function (Δc) {
        if (Δr || Δc) {
          for (var i = 1; i < boardSize; i++) {
            var r = r0 + i*Δr;
            var c = c0 + i*Δc;
            if (r < 0 || r >= boardSize || c < 0 || c >= boardSize) break;
            var space = board.spaceAt(r, c);
            if (space == tile.origin) break;
            var tileInSpace = space.tile;
            if (tileInSpace && tileInSpace != tile) break;
            spaces.push(space);
          }
        }
      });
    });
    return spaces;
  },

  moveTo: function (space) {
    this.space = space;
    this.set('space', space);
  },

  isSelected: function () {
    return this.game.selected.include(this);
  },

  updateSelection: function () {
    if (this.space == this.origin) {
      this.unselect();
    } else {
      this.select();
    }
  },

  select: function () {
    this.game.select(this);
  },

  unselect: function () {
    this.game.unselect(this);
  },

  remove: function () {
    this.unselect();
    this.space.tile = null;
    this.moveTo(null);
  }
});

var Tiles = Backbone.Collection.extend({
  model: Tile,

  comparator: function (a, b) {
    if (a == b) return 0;
    var as = a.space, bs = b.space;
    return (as.r < bs.r || as.r == bs.r && as.c < bs.c) ? -1 : 1;
  }
});

// VIEWS

var GameView = Backbone.View.extend({
  initialize: function (attrs) {
    var game = this.model;
    game.view = this;

    this.$el.width(tileRadius*boardSize*3);
    this.$el.height(tileRadius*boardSize*3);

    new BoardView({ model: game.board, parent: this });

    game.selected.on('add remove change', this.render, this);
  },

  render: function () {
    var word = this.model.getWord();
    $('#word').text(word || '');
    $('#score').text(word ? '= ' + this.model.score() : '');
    $('#scoreButton').attr('disabled', !word);
    $('#total').text(this.model.total);
    return this;
  }
});

var BoardView = Backbone.View.extend({
  id: 'board',

  initialize: function (opts) {
    var board = this.model;
    var view = board.view = this;

    this.$el.appendTo(opts.parent.$el);
    this.$el.width(tileRadius*boardSize*3);
    this.$el.height(tileRadius*boardSize*3);

    _.each(_.range(boardSize), function (r) {
      _.each(_.range(boardSize), function (c) {
        new SpaceView({ model: board.spaceAt(r, c), parent: view });
      });
    });
  }
});

function drawCircle(canvas, p) {
  p.radius = p.radius || tileRadius;
  p.width = p.height = 2*p.radius;
  p.strokeStyle = p.strokeStyle || 'black';
  canvas.drawEllipse(p);
}

var SpaceView = Backbone.View.extend({
  tagName: 'canvas',
  className: 'space',

  initialize: function (opts) {
    var space = this.model;
    space.view = this;
    this.$el.data('model', space);

    this.$el.appendTo(opts.parent.$el);
    this.$el.attr('width', tileRadius*3);
    this.$el.attr('height', tileRadius*3);
    drawCircle(this.$el, { x: tileRadius*1.5, y: tileRadius*1.5 });

    this.$el.position({
      my: 'left top', at: 'left top', of: opts.parent.$el,
      collision: 'none',
      offset: space.c*tileRadius*3 + ' ' + space.r*tileRadius*3
    });

    if (space.tile) {
      new TileView({ model: space.tile, parent: space.board.game.view});
    }
  }
});

var TileView = Backbone.View.extend({
  className: 'tile',

  initialize: function (opts) {
    var tile = this.model;
    var view = tile.view = this;
    this.$el.data('model', tile);

    this.$el.appendTo(opts.parent.$el);
    this.$el.width(tileRadius*3);
    this.$el.height(tileRadius*3);

    var canvas = $('<canvas />').appendTo(this.$el);
    canvas.attr('width', tileRadius*3);
    canvas.attr('height', tileRadius*3);
    canvas.addLayer({
      method: 'drawEllipse',
      x: tileRadius*1.5,
      y: tileRadius*1.5,
      width: tileRadius*2,
      height: tileRadius*2,
      strokeStyle: 'black',
      fillStyle: 'tan'
    });
    canvas.drawLayers();

    var label = $('
').appendTo(this.$el);
    $('
' + tile.letter + '
').appendTo(label);
    if (tile.value) {
      $('' + tile.value + '').appendTo(label);
    }
    label.position({ my: 'center', at: 'center', of: canvas });

    this.render();

    this.model.on('add remove change', this.render, this);

    // Handle clicking on a tile without dragging.
    this.$el.mouseup(function (e) {
      if (!tile.isSelected()) {
        tile.select();
      } else {
        tile.updateSelection();
      }
    });

    this.$el.draggable({
      distance: 5,
      stack: '.tile',
      revert: function (space) {
        if (!space) {
          tile.updateSelection();
          return true;
        } else {
          space.data('model').placeTile(tile);
          // This might not result in an event, if dropped on/near the
          // same space, but we still need to move it back to the
          // center of its space, so just call render directly.
          view.render();
          return false;
        }
      },
      revertDuration: 200,
      start: function (e) {
        _.each(tile.legalSpaces(), function (space) {
          space.view.$el.droppable();
        });
      },
      stop: function (e) {
        // This bypasses the mouseup handler, because selection is
        // updated in the revert function.
        e.stopImmediatePropagation();
        $('.space').droppable('destroy');
      }
    });
  },

  render: function () {
    var space = this.model.space;
    if (space) {
      this.$el.position({ my: 'left top', at: 'left top', of: space.view.$el,
                          collision: 'none' });
    } else {
      this.$el.css('visibility', 'hidden');
    }

    var canvas = this.$('canvas');
    if (this.model.isSelected()) {
      if (canvas.getLayers().length == 1) {
        canvas.addLayer({
          method: 'drawEllipse',
          strokeStyle: 'red',
          strokeWidth: 5,
          x: tileRadius*1.5,
          y: tileRadius*1.5,
          width: (tileRadius+3)*2,
          height: (tileRadius+3)*2
        });
        canvas.drawLayers();
      }
    } else {
      if (canvas.getLayers().length > 1) {
        canvas.removeLayer(1);
        canvas.drawLayers();
      }
    }

    return this;
  }
});

var game;
var view;

function main() {
  game = new Game();
  view = new GameView({ model: game, el: $('#wordsearch') });
}

$(main);
<commit_msg>Fix some punctuation.<commit_after>var tileRadius = 20;
var boardSize = 10;

var tileData = [
  // letter, value, frequency
  ['A', 0, 7],
  ['B', 2, 3],
  ['C', 2, 3],
  ['D', 1, 3],
  ['E', 0, 10],
  ['F', 2, 3],
  ['G', 2, 4],
  ['H', 3, 2],
  ['I', 0, 7],
  ['J', 4, 1],
  ['K', 3, 2],
  ['L', 0, 4],
  ['M', 2, 3],
  ['N', 1, 4],
  ['O', 0, 7],
  ['P', 2, 3],
  ['Q', 4, 1],
  ['R', 1, 5],
  ['S', 1, 5],
  ['T', 1, 5],
  ['U', 1, 4],
  ['V', 4, 1],
  ['W', 3, 3],
  ['X', 4, 1],
  ['Y', 3, 2],
  ['Z', 4, 1],
  [' ', 0, 2]
];

// MODELS

var Game = Backbone.Model.extend({
  initialize: function () {
    this.total = 0;
    this.selected = new Tiles();
    this.board = new Board({ game: this, tiles: this.makeTiles(tileData) });
  },

  makeTiles: function (tileData) {
    var game = this;
    return _.shuffle(_.flatten(_.map(tileData, function (args) {
      return function (letter, value, freq) {
        return _.map(_.range(freq), function (i) {
          return new Tile({ game: game, letter: letter, value: value });
        });
      }.apply(game, args);
    })));
  },

  select: function (tile) {
    this.selected.add(tile);
  },

  unselect: function (tile) {
    this.selected.remove(tile);
  },

  reset: function () {
    while (this.selected.length > 0) {
      var tile = this.selected.at(0);
      tile.origin.placeTile(tile);
    }
  },

  getWord: function () {
    tiles = this.selected;
    if (tiles.length > 1) {
      // Determine if the tiles are in a line.
      var origin = tiles.at(0).space;
      var space = tiles.at(1).space
      var Δr = space.r - origin.r, Δc = space.c - origin.c;
      if (Δr >= -1 && Δr <= 1 && Δc >= -1 && Δc <= 1) {
        for (var i = 2; i < tiles.length; i++) {
          var next = tiles.at(i).space;
          if (next.r - space.r != Δr || next.c - space.c != Δc) return;
          space = next;
        }
        return tiles.pluck('letter').join('');
      }
    }
  },

  score: function () {
    var sum = 0;
    this.selected.each(function (tile) { sum += tile.value; });
    return sum * this.selected.length;
  },

  scoreWord: function () {
    this.total += this.score();
    // Copy to a fresh array first, rather than iterating over the
    // collection (which gets mutated during the iteration).
    var tiles = this.selected.toArray();
    _.invoke(tiles, 'remove');
  }
});

var Board = Backbone.Model.extend({
  initialize: function (attrs) {
    this.game = attrs.game;
    this.spaces = this.makeSpaces();
    this.placeTiles(attrs.tiles);
  },

  makeSpaces: function () {
    var board = this;
    return _.map(_.range(boardSize), function (r) {
      return _.map(_.range(boardSize), function (c) {
        return new Space({ board: board, r: r, c: c });
      });
    });
  },

  spaceAt: function (r, c) {
    return this.spaces[r][c];
  },

  placeTiles: function (tiles) {
    var board = this;
    _.each(_.range(boardSize), function (r) {
      _.each(_.range(boardSize), function (c) {
        // Leave the center four spaces blank.
        if (r < boardSize/2 - 1 || r > boardSize/2 ||
            c < boardSize/2 - 1 || c > boardSize/2) {
          var tile = tiles.pop();
          var space = board.spaceAt(r, c);
          tile.origin = space;
          space.placeTile(tile);
          tile.board = board;
        }
      });
    });
  }
});

var Space = Backbone.Model.extend({
  initialize: function (attrs) {
    this.board = attrs.board;
    this.r = attrs.r;
    this.c = attrs.c;
  },

  placeTile: function (tile) {
    var oldSpace = tile.space;
    if (oldSpace) oldSpace.tile = null;
    this.tile = tile;
    tile.moveTo(this);
    tile.updateSelection();
  },

  isBetween: function (s1, s2) {
    // TO DO: I suspect there is a simpler/quicker way to compute this.
    return (s1.r - s2.r)*(s1.c - this.c) == (s1.c - s2.c)*(s1.r - this.r) &&
      (s1.r <= this.r && this.r <= s2.r || s2.r <= this.r && this.r <= s1.r) &&
      (s1.c <= this.c && this.c <= s2.c || s2.c <= this.c && this.c <= s1.c);
  }
});

var Tile = Backbone.Model.extend({
  initialize: function (attrs) {
    this.game = attrs.game;
    this.letter = attrs.letter;
    this.value = attrs.value;
  },

  legalSpaces: function () {
    var tile = this;
    var board = this.board;
    var r0 = this.origin.r;
    var c0 = this.origin.c;
    var spaces = [];
    if (!this.origin.tile) {
      // A tile can't move back to its origin if it would be in the
      // way of another tile that's already moved.
      if (!this.game.selected.any(function (movedTile) {
        return tile != movedTile &&
          tile.origin.isBetween(movedTile.origin, movedTile.space);
      })) {
        spaces.push(this.origin);
      }
    }
    _.each([-1, 0, 1], function (Δr) {
      _.each([-1, 0, 1], function (Δc) {
        if (Δr || Δc) {
          for (var i = 1; i < boardSize; i++) {
            var r = r0 + i*Δr;
            var c = c0 + i*Δc;
            if (r < 0 || r >= boardSize || c < 0 || c >= boardSize) break;
            var space = board.spaceAt(r, c);
            if (space == tile.origin) break;
            var tileInSpace = space.tile;
            if (tileInSpace && tileInSpace != tile) break;
            spaces.push(space);
          }
        }
      });
    });
    return spaces;
  },

  moveTo: function (space) {
    this.space = space;
    this.set('space', space);
  },

  isSelected: function () {
    return this.game.selected.include(this);
  },

  updateSelection: function () {
    if (this.space == this.origin) {
      this.unselect();
    } else {
      this.select();
    }
  },

  select: function () {
    this.game.select(this);
  },

  unselect: function () {
    this.game.unselect(this);
  },

  remove: function () {
    this.unselect();
    this.space.tile = null;
    this.moveTo(null);
  }
});

var Tiles = Backbone.Collection.extend({
  model: Tile,

  comparator: function (a, b) {
    if (a == b) return 0;
    var as = a.space, bs = b.space;
    return (as.r < bs.r || as.r == bs.r && as.c < bs.c) ? -1 : 1;
  }
});

// VIEWS

var GameView = Backbone.View.extend({
  initialize: function (attrs) {
    var game = this.model;
    game.view = this;

    this.$el.width(tileRadius*boardSize*3);
    this.$el.height(tileRadius*boardSize*3);

    new BoardView({ model: game.board, parent: this });

    game.selected.on('add remove change', this.render, this);
  },

  render: function () {
    var word = this.model.getWord();
    $('#word').text(word || '');
    $('#score').text(word ? '= ' + this.model.score() : '');
    $('#scoreButton').attr('disabled', !word);
    $('#total').text(this.model.total);
    return this;
  }
});

var BoardView = Backbone.View.extend({
  id: 'board',

  initialize: function (opts) {
    var board = this.model;
    var view = board.view = this;

    this.$el.appendTo(opts.parent.$el);
    this.$el.width(tileRadius*boardSize*3);
    this.$el.height(tileRadius*boardSize*3);

    _.each(_.range(boardSize), function (r) {
      _.each(_.range(boardSize), function (c) {
        new SpaceView({ model: board.spaceAt(r, c), parent: view });
      });
    });
  }
});

function drawCircle(canvas, p) {
  p.radius = p.radius || tileRadius;
  p.width = p.height = 2*p.radius;
  p.strokeStyle = p.strokeStyle || 'black';
  canvas.drawEllipse(p);
}

var SpaceView = Backbone.View.extend({
  tagName: 'canvas',
  className: 'space',

  initialize: function (opts) {
    var space = this.model;
    space.view = this;
    this.$el.data('model', space);

    this.$el.appendTo(opts.parent.$el);
    this.$el.attr('width', tileRadius*3);
    this.$el.attr('height', tileRadius*3);
    drawCircle(this.$el, { x: tileRadius*1.5, y: tileRadius*1.5 });

    this.$el.position({
      my: 'left top', at: 'left top', of: opts.parent.$el,
      collision: 'none',
      offset: space.c*tileRadius*3 + ' ' + space.r*tileRadius*3
    });

    if (space.tile) {
      new TileView({ model: space.tile, parent: space.board.game.view});
    }
  }
});

var TileView = Backbone.View.extend({
  className: 'tile',

  initialize: function (opts) {
    var tile = this.model;
    var view = tile.view = this;
    this.$el.data('model', tile);

    this.$el.appendTo(opts.parent.$el);
    this.$el.width(tileRadius*3);
    this.$el.height(tileRadius*3);

    var canvas = $('<canvas />').appendTo(this.$el);
    canvas.attr('width', tileRadius*3);
    canvas.attr('height', tileRadius*3);
    canvas.addLayer({
      method: 'drawEllipse',
      x: tileRadius*1.5,
      y: tileRadius*1.5,
      width: tileRadius*2,
      height: tileRadius*2,
      strokeStyle: 'black',
      fillStyle: 'tan'
    });
    canvas.drawLayers();

    var label = $('
').appendTo(this.$el);
    $('
' + tile.letter + '
').appendTo(label);
    if (tile.value) {
      $('' + tile.value + '').appendTo(label);
    }
    label.position({ my: 'center', at: 'center', of: canvas });

    this.render();

    this.model.on('add remove change', this.render, this);

    // Handle clicking on a tile without dragging.
    this.$el.mouseup(function (e) {
      if (!tile.isSelected()) {
        tile.select();
      } else {
        tile.updateSelection();
      }
    });

    this.$el.draggable({
      distance: 5,
      stack: '.tile',
      revert: function (space) {
        if (!space) {
          tile.updateSelection();
          return true;
        } else {
          space.data('model').placeTile(tile);
          // This might not result in an event, if dropped on/near the
          // same space, but we still need to move it back to the
          // center of its space, so just call render directly.
          view.render();
          return false;
        }
      },
      revertDuration: 200,
      start: function (e) {
        _.each(tile.legalSpaces(), function (space) {
          space.view.$el.droppable();
        });
      },
      stop: function (e) {
        // This bypasses the mouseup handler, because selection is
        // updated in the revert function.
        e.stopImmediatePropagation();
        $('.space').droppable('destroy');
      }
    });
  },

  render: function () {
    var space = this.model.space;
    if (space) {
      this.$el.position({ my: 'left top', at: 'left top', of: space.view.$el,
                          collision: 'none' });
    } else {
      this.$el.css('visibility', 'hidden');
    }

    var canvas = this.$('canvas');
    if (this.model.isSelected()) {
      if (canvas.getLayers().length == 1) {
        canvas.addLayer({
          method: 'drawEllipse',
          strokeStyle: 'red',
          strokeWidth: 5,
          x: tileRadius*1.5,
          y: tileRadius*1.5,
          width: (tileRadius+3)*2,
          height: (tileRadius+3)*2
        });
        canvas.drawLayers();
      }
    } else {
      if (canvas.getLayers().length > 1) {
        canvas.removeLayer(1);
        canvas.drawLayers();
      }
    }

    return this;
  }
});

var game;
var view;

function main() {
  game = new Game();
  view = new GameView({ model: game, el: $('#wordsearch') });
}

$(main);
<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');
var times = require('../times');
var levels = require('../levels');

function init() {

  var upbat = {
    name: 'upbat'
    , label: 'Uploader Battery'
    , pluginType: 'pill-status'
    , pillFlip: true
  };

  upbat.getPrefs = function getPrefs(sbx) {
    return {
      warn: sbx.extendedSettings.warn ? sbx.extendedSettings.warn : 30
      , urgent: sbx.extendedSettings.urgent ? sbx.extendedSettings.urgent : 20
      , enableAlerts: sbx.extendedSettings.enableAlerts
    };
  };

  upbat.setProperties = function setProperties (sbx) {
    sbx.offerProperty('upbat', function setUpbat2 ( ) {
      return upbat.analyzeData(sbx);
    });
  };

  function byBattery (status) {
    return status.uploader.battery;
  }

  upbat.analyzeData = function analyzeData (sbx) {

    var prefs = upbat.getPrefs(sbx);

    var recentMins = 30;
    var recentMills = sbx.time - times.mins(recentMins).msecs;

    var recentData = _.filter(sbx.data.devicestatus, function eachStatus (status) {
      return ('uploader' in status) && sbx.entryMills(status) <= sbx.time && sbx.entryMills(status) >= recentMills;
    });

    var result = {
      level: undefined
      , display: '?%'
      , status: undefined
      , devices: {}
    };

    function getDevice (status) {
      var uri = status.device || 'uploader';
      var device = result.devices[uri];

      if (!device) {
        device = {
          //TODO: regex to look for any uri schemes, such as: xdrip://phone
          name: uri.indexOf('openaps://') === 0 ? uri.substring('openaps://'.length) : uri
          , uri: uri
          , statuses: [ ]
        };

        result.devices[uri] = device;
      }
      return device;
    }

    function analyzeStatus (status) {

      var uploaderStatus = status.uploader;

      var battery = uploaderStatus.battery;
      var voltage = uploaderStatus.batteryVoltage;
      var voltageDisplay;

      if (voltage) {
        if (voltage > 1000) {
          voltage = voltage / 1000;
        }
        voltageDisplay = voltage.toFixed(3) + 'v';
      }

      if (battery || voltage) {
        uploaderStatus.value = battery || voltage;

        if (battery) {
          uploaderStatus.battery = battery;
        }

        if (voltage) {
          uploaderStatus.voltage = voltage;
          uploaderStatus.voltageDisplay = voltageDisplay;
        }

        uploaderStatus.display = battery ? battery + '%' : voltageDisplay;

        if (battery >= 95) {
          uploaderStatus.level = 100;
        } else if (battery < 95 && battery >= 55) {
          uploaderStatus.level = 75;
        } else if (battery < 55 && battery >= 30) {
          uploaderStatus.level = 50;
        } else {
          uploaderStatus.level = 25;
        }

        if (battery <= prefs.warn && battery > prefs.urgent) {
          uploaderStatus.notification = levels.WARN;
        } else if (battery <= prefs.urgent) {
          uploaderStatus.notification = levels.URGENT;
        }

      }
    }

    _.forEach(recentData, function eachRecentStatus (status) {
      analyzeStatus(status);
      var device = getDevice(status);
      device.statuses.push(_.pick(status, ['uploader', 'created_at', 'mills', '_id']));
    });

    var recentLowests = [ ];
    _.forEach(result.devices, function eachDevice (device) {
      device.statuses = _.sortBy(device.statuses, function (status) {
        return sbx.entryMills(status);
      }).reverse();
      var first = _.first(device.statuses);
      var recent = sbx.entryMills(first) - times.mins(10).msecs;
      var recentLowest = _.chain(device.statuses)
        .filter(function isRecent (status) {
          return sbx.entryMills(status) > recent;
        })
        .minBy(byBattery)
        .value();

      device.min = recentLowest.uploader;
      recentLowests.push(recentLowest);
    });

    var min = _.minBy(recentLowests, byBattery);

    if (min && min.uploader) {
      result.level = min.uploader.level;
      result.display = min.uploader.display;
      result.status = levels.toStatusClass(min.uploader.notification);
      result.min = min.uploader;
    }

    return result;
  };

  upbat.checkNotifications = function checkNotifications(sbx) {
    var prefs = upbat.getPrefs(sbx);

    var prop = sbx.properties.upbat;
    if (!prop || !prefs.enableAlerts) { return; }

    if (prop.notification && prop.notification >= levels.WARN) {
      var message = _.map(_.values(prop.devices), function toMessage (device) {
        var info = [
          device.name
          , device.min.display
        ];

        if (device.min && device.min.battery && device.min.voltageDisplay) {
          info.push('(' + device.min.voltageDisplay + ')');
        }

        return info.join(' ');
      }).join('; ');

      sbx.notifications.requestNotify({
        level: prop.notification
        , title: levels.toDisplay(prop.notification) + ' Uploader Battery is Low'
        , message: message
        , pushoverSound: 'echo'
        , group: 'Uploader Battery'
        , plugin: upbat
        , debug: prop
      });
    }
  };

  upbat.updateVisualisation = function updateVisualisation (sbx) {
    var prop = sbx.properties.upbat;

    var infos = null;

    if (_.values(prop.devices).length > 1) {
      infos = _.map(_.values(prop.devices), function toInfo (device) {
        var info = {
          label: device.name
          , value: device.min.display
        };

        if (device.min && device.min.battery && device.min.voltageDisplay) {
          info.value += ' (' + device.min.voltageDisplay + ')';
        }

        return info;
      });
    } else if (prop.min && prop.min.battery && prop.min.voltageDisplay) {
      infos = [{label: 'Voltage', value: prop.min.voltageDisplay}];
    }

    sbx.pluginBase.updatePillText(upbat, {
      value: prop && prop.display
      , labelClass: prop && prop.level && 'icon-battery-' + prop.level
      , pillClass: prop && prop.status
      , info: infos
      , hide: !(prop && prop.min && prop.min.value && prop.min.value >= 0)
    });
  };

  return upbat;

}

module.exports = init;<commit_msg>use prop.min.notification for notification, after restructing prop<commit_after>'use strict';

var _ = require('lodash');
var times = require('../times');
var levels = require('../levels');

function init() {

  var upbat = {
    name: 'upbat'
    , label: 'Uploader Battery'
    , pluginType: 'pill-status'
    , pillFlip: true
  };

  upbat.getPrefs = function getPrefs(sbx) {
    return {
      warn: sbx.extendedSettings.warn ? sbx.extendedSettings.warn : 30
      , urgent: sbx.extendedSettings.urgent ? sbx.extendedSettings.urgent : 20
      , enableAlerts: sbx.extendedSettings.enableAlerts
    };
  };

  upbat.setProperties = function setProperties (sbx) {
    sbx.offerProperty('upbat', function setUpbat2 ( ) {
      return upbat.analyzeData(sbx);
    });
  };

  function byBattery (status) {
    return status.uploader.battery;
  }

  upbat.analyzeData = function analyzeData (sbx) {

    var prefs = upbat.getPrefs(sbx);

    var recentMins = 30;
    var recentMills = sbx.time - times.mins(recentMins).msecs;

    var recentData = _.filter(sbx.data.devicestatus, function eachStatus (status) {
      return ('uploader' in status) && sbx.entryMills(status) <= sbx.time && sbx.entryMills(status) >= recentMills;
    });

    var result = {
      level: undefined
      , display: '?%'
      , status: undefined
      , devices: {}
    };

    function getDevice (status) {
      var uri = status.device || 'uploader';
      var device = result.devices[uri];

      if (!device) {
        device = {
          //TODO: regex to look for any uri schemes, such as: xdrip://phone
          name: uri.indexOf('openaps://') === 0 ? uri.substring('openaps://'.length) : uri
          , uri: uri
          , statuses: [ ]
        };

        result.devices[uri] = device;
      }
      return device;
    }

    function analyzeStatus (status) {

      var uploaderStatus = status.uploader;

      var battery = uploaderStatus.battery;
      var voltage = uploaderStatus.batteryVoltage;
      var voltageDisplay;

      if (voltage) {
        if (voltage > 1000) {
          voltage = voltage / 1000;
        }
        voltageDisplay = voltage.toFixed(3) + 'v';
      }

      if (battery || voltage) {
        uploaderStatus.value = battery || voltage;

        if (battery) {
          uploaderStatus.battery = battery;
        }

        if (voltage) {
          uploaderStatus.voltage = voltage;
          uploaderStatus.voltageDisplay = voltageDisplay;
        }

        uploaderStatus.display = battery ? battery + '%' : voltageDisplay;

        if (battery >= 95) {
          uploaderStatus.level = 100;
        } else if (battery < 95 && battery >= 55) {
          uploaderStatus.level = 75;
        } else if (battery < 55 && battery >= 30) {
          uploaderStatus.level = 50;
        } else {
          uploaderStatus.level = 25;
        }

        if (battery <= prefs.warn && battery > prefs.urgent) {
          uploaderStatus.notification = levels.WARN;
        } else if (battery <= prefs.urgent) {
          uploaderStatus.notification = levels.URGENT;
        }

      }
    }

    _.forEach(recentData, function eachRecentStatus (status) {
      analyzeStatus(status);
      var device = getDevice(status);
      device.statuses.push(_.pick(status, ['uploader', 'created_at', 'mills', '_id']));
    });

    var recentLowests = [ ];
    _.forEach(result.devices, function eachDevice (device) {
      device.statuses = _.sortBy(device.statuses, function (status) {
        return sbx.entryMills(status);
      }).reverse();
      var first = _.first(device.statuses);
      var recent = sbx.entryMills(first) - times.mins(10).msecs;
      var recentLowest = _.chain(device.statuses)
        .filter(function isRecent (status) {
          return sbx.entryMills(status) > recent;
        })
        .minBy(byBattery)
        .value();

      device.min = recentLowest.uploader;
      recentLowests.push(recentLowest);
    });

    var min = _.minBy(recentLowests, byBattery);

    if (min && min.uploader) {
      result.level = min.uploader.level;
      result.display = min.uploader.display;
      result.status = levels.toStatusClass(min.uploader.notification);
      result.min = min.uploader;
    }

    return result;
  };

  upbat.checkNotifications = function checkNotifications(sbx) {
    var prefs = upbat.getPrefs(sbx);

    var prop = sbx.properties.upbat;
    if (!prop || !prefs.enableAlerts) { return; }

    if (prop.min && prop.min.notification && prop.min.notification >= levels.WARN) {
      var message = _.map(_.values(prop.devices), function toMessage (device) {
        var info = [
          device.name
          , device.min.display
        ];

        if (device.min && device.min.battery && device.min.voltageDisplay) {
          info.push('(' + device.min.voltageDisplay + ')');
        }

        return info.join(' ');
      }).join('; ');

      sbx.notifications.requestNotify({
        level: prop.min.notification
        , title: levels.toDisplay(prop.min.notification) + ' Uploader Battery is Low'
        , message: message
        , pushoverSound: 'echo'
        , group: 'Uploader Battery'
        , plugin: upbat
        , debug: prop
      });
    }
  };

  upbat.updateVisualisation = function updateVisualisation (sbx) {
    var prop = sbx.properties.upbat;

    var infos = null;

    if (_.values(prop.devices).length > 1) {
      infos = _.map(_.values(prop.devices), function toInfo (device) {
        var info = {
          label: device.name
          , value: device.min.display
        };

        if (device.min && device.min.battery && device.min.voltageDisplay) {
          info.value += ' (' + device.min.voltageDisplay + ')';
        }

        return info;
      });
    } else if (prop.min && prop.min.battery && prop.min.voltageDisplay) {
      infos = [{label: 'Voltage', value: prop.min.voltageDisplay}];
    }

    sbx.pluginBase.updatePillText(upbat, {
      value: prop && prop.display
      , labelClass: prop && prop.level && 'icon-battery-' + prop.level
      , pillClass: prop && prop.status
      , info: infos
      , hide: !(prop && prop.min && prop.min.value && prop.min.value >= 0)
    });
  };

  return upbat;

}

module.exports = init;<|endoftext|>


<commit_before>var redis = require('./redis');
var queue = require('./queue');
var utility = require('./utility');
var config = require('./config');
var getReplayUrl = require('./getReplayUrl');
var db = require('./db');
var insertMatch = require('./queries').insertMatch;
var buildSets = require('./buildSets');
var bodyParser = require('body-parser');
var express = require('express');
var moment = require('moment');
var app = express();
var port = config.PORT || config.WORK_PORT;
var active_jobs = {};
var pooled_jobs = {};
var startedAt = moment();
var memwatch = require('memwatch-next');
var hd = new memwatch.HeapDiff();
memwatch.on('leak', function(info) {
    console.error(info);
});
memwatch.on('stats', function(stats) {
    var diff = hd.end();
    console.error(JSON.stringify(diff));
    hd = new memwatch.HeapDiff();
    console.error(stats);
});
buildSets(db, redis, function(err) {
    if (err) {
        throw err;
    }
    var pool_size = 200;
    queue.parse.process(pool_size, function(job, cb) {
        //save the callback for this job
        job.cb = cb;
        //put it in the pool
        pooled_jobs[job.jobId] = job;
        console.log('loaded job %s into pool, %s jobs in pool, %s jobs active', job.jobId, Object.keys(pooled_jobs).length, Object.keys(active_jobs).length);
    });
    start();
});

function start() {
    app.use(bodyParser.json({
        limit: '1mb'
    }));
    app.get("/", function(req, res) {
        res.json({
            started_at: startedAt.format(),
            started_ago: startedAt.fromNow()
        });
    });
    app.get('/parse', function(req, res) {
        if (config.RETRIEVER_SECRET && req.query.key !== config.RETRIEVER_SECRET) {
            return res.status(500).json({
                error: "invalid key"
            });
        }
        var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
        console.log('client %s requested work', ip);
        var job = pooled_jobs[Object.keys(pooled_jobs)[0]];
        if (!job) {
            console.log('no work available');
            return res.status(500).json({
                error: "no work available"
            });
        }
        delete pooled_jobs[job.jobId];
        active_jobs[job.jobId] = job;
        var match = job.data.payload;
        job.submitWork = function submitWork(parsed_data) {
            if (parsed_data.error) {
                return job.exit(parsed_data.error);
            }
            delete parsed_data.key;
            delete parsed_data.jobId;
            //extend match object with parsed data, keep existing data if key conflict (match_id)
            //match.players was deleted earlier during insertion of api data
            for (var key in parsed_data) {
                match[key] = match[key] || parsed_data[key];
            }
            match.parse_status = 2;
            return insertMatch(db, redis, queue, match, {
                type: "parsed"
            }, job.exit);
        };
        job.exit = function exit(err) {
            clearTimeout(job.expire);
            delete active_jobs[job.jobId];
            return job.cb(err);
        };
        //get the replay url and save it
        return getReplayUrl(db, redis, match, function(err) {
            if (err) {
                res.status(500).json({
                    error: "failed to get replay url"
                });
                return job.exit(err);
            }
            else {
                job.expire = setTimeout(function() {
                    console.log('job %s expired', job.jobId);
                    return job.exit("timeout");
                }, 180 * 1000);
                console.log('server sent jobid %s', job.jobId);
                return res.json({
                    jobId: job.jobId,
                    data: job.data
                });
            }
        });
    });
    app.post('/parse', function(req, res) {
        //validate request
        if (config.RETRIEVER_SECRET && req.body.key !== config.RETRIEVER_SECRET) {
            return res.status(500).json({
                error: "invalid key"
            });
        }
        console.log('received submitted work');
        if (active_jobs[req.body.jobId]) {
            var job = active_jobs[req.body.jobId];
            job.submitWork(req.body);
            return res.json({
                error: null
            });
        }
        else {
            return res.status(500).json({
                error: "no active job with this jobid"
            });
        }
    });
    var server = app.listen(port, function() {
        var host = server.address().address;
        console.log('[WORKSERVER] listening at http://%s:%s', host, port);
    });
}
<commit_msg>move getreplayurl<commit_after>var redis = require('./redis');
var queue = require('./queue');
var utility = require('./utility');
var config = require('./config');
var getReplayUrl = require('./getReplayUrl');
var db = require('./db');
var insertMatch = require('./queries').insertMatch;
var buildSets = require('./buildSets');
var bodyParser = require('body-parser');
var express = require('express');
var moment = require('moment');
var app = express();
var port = config.PORT || config.WORK_PORT;
var active_jobs = {};
var pooled_jobs = {};
var startedAt = moment();
var memwatch = require('memwatch-next');
var hd = new memwatch.HeapDiff();
memwatch.on('leak', function(info) {
    console.error(info);
});
memwatch.on('stats', function(stats) {
    var diff = hd.end();
    console.error(JSON.stringify(diff));
    hd = new memwatch.HeapDiff();
    console.error(stats);
});
buildSets(db, redis, function(err) {
    if (err) {
        throw err;
    }
    var pool_size = 200;
    queue.parse.process(pool_size, function(job, cb) {
        //save the callback for this job
        job.cb = cb;
        var match = job.data.payload;
        //get the replay url and save it to db
        return getReplayUrl(db, redis, match, function(err) {
            if (err) {
                return cb(err);
            }
            //put it in the pool
            pooled_jobs[job.jobId] = job;
            console.log('loaded job %s into pool, %s jobs in pool, %s jobs active', job.jobId, Object.keys(pooled_jobs).length, Object.keys(active_jobs).length);
        });
    });
    start();
});

function start() {
    app.use(bodyParser.json({
        limit: '1mb'
    }));
    app.get("/", function(req, res) {
        res.json({
            started_at: startedAt.format(),
            started_ago: startedAt.fromNow()
        });
    });
    app.get('/parse', function(req, res) {
        if (config.RETRIEVER_SECRET && req.query.key !== config.RETRIEVER_SECRET) {
            return res.status(500).json({
                error: "invalid key"
            });
        }
        var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
        console.log('client %s requested work', ip);
        var job = pooled_jobs[Object.keys(pooled_jobs)[0]];
        if (!job) {
            console.log('no work available');
            return res.status(500).json({
                error: "no work available"
            });
        }
        delete pooled_jobs[job.jobId];
        active_jobs[job.jobId] = job;
        var match = job.data.payload;
        job.submitWork = function submitWork(parsed_data) {
            if (parsed_data.error) {
                return job.exit(parsed_data.error);
            }
            delete parsed_data.key;
            delete parsed_data.jobId;
            //extend match object with parsed data, keep existing data if key conflict (match_id)
            //match.players was deleted earlier during insertion of api data
            for (var key in parsed_data) {
                match[key] = match[key] || parsed_data[key];
            }
            match.parse_status = 2;
            return insertMatch(db, redis, queue, match, {
                type: "parsed"
            }, job.exit);
        };
        job.exit = function exit(err) {
            clearTimeout(job.expire);
            delete active_jobs[job.jobId];
            return job.cb(err);
        };
        job.expire = setTimeout(function() {
            console.log('job %s expired', job.jobId);
            return job.exit("timeout");
        }, 180 * 1000);
        console.log('server sent jobid %s', job.jobId);
        return res.json({
            jobId: job.jobId,
            data: job.data
        });
    });
    app.post('/parse', function(req, res) {
        //validate request
        if (config.RETRIEVER_SECRET && req.body.key !== config.RETRIEVER_SECRET) {
            return res.status(500).json({
                error: "invalid key"
            });
        }
        console.log('received submitted work');
        if (active_jobs[req.body.jobId]) {
            var job = active_jobs[req.body.jobId];
            job.submitWork(req.body);
            return res.json({
                error: null
            });
        }
        else {
            return res.status(500).json({
                error: "no active job with this jobid"
            });
        }
    });
    var server = app.listen(port, function() {
        var host = server.address().address;
        console.log('[WORKSERVER] listening at http://%s:%s', host, port);
    });
}
<|endoftext|>


<commit_before>import Emitter from "../emitter";
const STATUS = {
  PLAYING: "PLAYING",
  STOPPED: "STOPPED",
  PAUSED: "PAUSED"
};

export default class ElementSource {
  on(eventType, cb) {
    return this._emitter.on(eventType, cb);
  }

  constructor(context, destination) {
    this._emitter = new Emitter();
    this._context = context;
    this._destination = destination;
    this._audio = document.createElement("audio");
    this._audio.crossOrigin = "anonymous";
    this._stalled = false;
    this._status = STATUS.STOPPED;

    this._audio.addEventListener("suspend", () => {
      this._setStalled(true);
    });

    this._audio.addEventListener("durationchange", () => {
      this._emitter.trigger("loaded");
      this._setStalled(false);
    });

    this._audio.addEventListener("ended", () => {
      this._emitter.trigger("ended");
      this._setStatus(STATUS.STOPPED);
    });

    // TODO: Throttle to 50 (if needed)
    this._audio.addEventListener("timeupdate", () => {
      this._emitter.trigger("positionChange");
    });

    this._audio.addEventListener("error", e => {
      switch (this._audio.error.code) {
        case 1:
          // The fetching of the associated resource was aborted by the user's request.
          console.error("MEDIA_ERR_ABORTED", e);
          break;
        case 2:
          console.error("MEDIA_ERR_NETWORK", e);
          // Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.
          break;
        case 3:
          // Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.

          // There is a bug in Chrome where improperly terminated mp3s can cuase this error.
          // https://bugs.chromium.org/p/chromium/issues/detail?id=794782
          // Related: Commit f44e826c83c74fef04c2c448af30cfb353b28312
          console.error("PIPELINE_ERROR_DECODE", e);
          break;
        case 4:
          console.error("MEDIA_ERR_SRC_NOT_SUPPORTED", e);
          // The associated resource or media provider object (such as a MediaStream) has been found to be unsuitable.
          break;
      }
      // Rather than just geting stuck in this error state, we can just pretend this is
      // the end of the track.

      this._emitter.trigger("ended");
      this._setStatus(STATUS.STOPPED);
    });

    this._source = this._context.createMediaElementSource(this._audio);
    this._source.connect(destination);
  }

  _setStalled(stalled) {
    this._stalled = stalled;
    this._emitter.trigger("stallChanged");
  }

  disconnect() {
    this._source.disconnect();
  }

  // Async for now, for compatibility with BufferAudioSource
  // TODO: This does not need to be async
  async loadUrl(url) {
    this._audio.src = url;
  }

  async play() {
    if (this._status !== STATUS.PAUSED) {
      this.seekToTime(0);
    }
    try {
      await this._audio.play();
    } catch (err) {
      //
    }
    this._setStatus(STATUS.PLAYING);
  }

  pause() {
    this._audio.pause();
    this._setStatus(STATUS.PAUSED);
  }

  stop() {
    this._audio.pause();
    this._audio.currentTime = 0;
    this._setStatus(STATUS.STOPPED);
  }

  seekToTime(time) {
    // Make sure we are within range
    // TODO: Use clamp
    time = Math.min(time, this.getDuration());
    time = Math.max(time, 0);
    this._audio.currentTime = time;
    this._emitter.trigger("positionChange");
  }

  getStalled() {
    return this._stalled;
  }

  getStatus() {
    return this._status;
  }

  getDuration() {
    const { duration } = this._audio;
    // Safari on iOS currently has a strange behavior where it reports
    // the duration as infinity. For now, 0 is better even though it's still wrong.
    return isNaN(duration) || duration === Infinity ? 0 : duration;
  }

  getTimeElapsed() {
    return this._audio.currentTime;
  }

  getNumberOfChannels() {
    return this._source.channelCount;
  }

  getSampleRate() {
    // This is a lie. This is the sample rate of the context, not the
    // underlying source media.
    return this._context.sampleRate;
  }

  _setStatus(status) {
    this._status = status;
    this._emitter.trigger("statusChange");
  }
}
<commit_msg>Document infinite duration bug better<commit_after>import Emitter from "../emitter";
const STATUS = {
  PLAYING: "PLAYING",
  STOPPED: "STOPPED",
  PAUSED: "PAUSED"
};

export default class ElementSource {
  on(eventType, cb) {
    return this._emitter.on(eventType, cb);
  }

  constructor(context, destination) {
    this._emitter = new Emitter();
    this._context = context;
    this._destination = destination;
    this._audio = document.createElement("audio");
    this._audio.crossOrigin = "anonymous";
    this._stalled = false;
    this._status = STATUS.STOPPED;

    this._audio.addEventListener("suspend", () => {
      this._setStalled(true);
    });

    this._audio.addEventListener("durationchange", () => {
      this._emitter.trigger("loaded");
      this._setStalled(false);
    });

    this._audio.addEventListener("ended", () => {
      this._emitter.trigger("ended");
      this._setStatus(STATUS.STOPPED);
    });

    // TODO: Throttle to 50 (if needed)
    this._audio.addEventListener("timeupdate", () => {
      this._emitter.trigger("positionChange");
    });

    this._audio.addEventListener("error", e => {
      switch (this._audio.error.code) {
        case 1:
          // The fetching of the associated resource was aborted by the user's request.
          console.error("MEDIA_ERR_ABORTED", e);
          break;
        case 2:
          console.error("MEDIA_ERR_NETWORK", e);
          // Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.
          break;
        case 3:
          // Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.

          // There is a bug in Chrome where improperly terminated mp3s can cuase this error.
          // https://bugs.chromium.org/p/chromium/issues/detail?id=794782
          // Related: Commit f44e826c83c74fef04c2c448af30cfb353b28312
          console.error("PIPELINE_ERROR_DECODE", e);
          break;
        case 4:
          console.error("MEDIA_ERR_SRC_NOT_SUPPORTED", e);
          // The associated resource or media provider object (such as a MediaStream) has been found to be unsuitable.
          break;
      }
      // Rather than just geting stuck in this error state, we can just pretend this is
      // the end of the track.

      this._emitter.trigger("ended");
      this._setStatus(STATUS.STOPPED);
    });

    this._source = this._context.createMediaElementSource(this._audio);
    this._source.connect(destination);
  }

  _setStalled(stalled) {
    this._stalled = stalled;
    this._emitter.trigger("stallChanged");
  }

  disconnect() {
    this._source.disconnect();
  }

  // Async for now, for compatibility with BufferAudioSource
  // TODO: This does not need to be async
  async loadUrl(url) {
    this._audio.src = url;
  }

  async play() {
    if (this._status !== STATUS.PAUSED) {
      this.seekToTime(0);
    }
    try {
      await this._audio.play();
    } catch (err) {
      //
    }
    this._setStatus(STATUS.PLAYING);
  }

  pause() {
    this._audio.pause();
    this._setStatus(STATUS.PAUSED);
  }

  stop() {
    this._audio.pause();
    this._audio.currentTime = 0;
    this._setStatus(STATUS.STOPPED);
  }

  seekToTime(time) {
    // Make sure we are within range
    // TODO: Use clamp
    time = Math.min(time, this.getDuration());
    time = Math.max(time, 0);
    this._audio.currentTime = time;
    this._emitter.trigger("positionChange");
  }

  getStalled() {
    return this._stalled;
  }

  getStatus() {
    return this._status;
  }

  getDuration() {
    const { duration } = this._audio;
    // Safari on iOS currently has a strange behavior where it reports
    // the duration as infinity if an Accept-Ranges header is not returned.
    // For now, 0 is better even though it's still wrong.
    return isNaN(duration) || duration === Infinity ? 0 : duration;
  }

  getTimeElapsed() {
    return this._audio.currentTime;
  }

  getNumberOfChannels() {
    return this._source.channelCount;
  }

  getSampleRate() {
    // This is a lie. This is the sample rate of the context, not the
    // underlying source media.
    return this._context.sampleRate;
  }

  _setStatus(status) {
    this._status = status;
    this._emitter.trigger("statusChange");
  }
}
<|endoftext|>