stylus/manage.js
tophf f4e689721a Improve style caching, cache requests too, add code:false mode
Previously, when a cache was invalidated and every tab/iframe issued a getStyles request, we previous needlessly accessed IndexedDB for each of these requests. It happened because 1) the global cachedStyles was created only at the end of the async DB-reading, 2) and each style record is retrieved asynchronously so the single threaded JS engine interleaved all these operations. It could easily span a few seconds when many tabs are open and you have like 100 styles.

Now, in getStyles: all requests issued while cachedStyles is being populated are queued and invoked at the end.

Now, in filterStyles: all requests are cached using the request's options combined in a string as a key. It also helps on each navigation because we monitor page loading process at different stages: before, when committed, history traversal, requesting applicable styles by a content script. Icon badge update also may issue a copy of the just issued request by one of the navigation listeners.

Now, the caches are invalidated smartly: style add/update/delete/toggle only purges filtering cache, and modifies style cache in-place without re-reading the entire IndexedDB.

Now, code:false mode for manage page that only needs style meta. It reduces the transferred message size 10-100 times thus reducing the overhead caused by to internal JSON-fication in the extensions API.

Also fast&direct getStylesSafe for own pages; code cosmetics
2017-04-18 12:43:28 +03:00

435 lines
13 KiB
JavaScript

/* globals styleSectionsEqual */
var lastUpdatedStyleId = null;
var installed;
var appliesToExtraTemplate = document.createElement("span");
appliesToExtraTemplate.className = "applies-to-extra";
appliesToExtraTemplate.innerHTML = " " + t('appliesDisplayTruncatedSuffix');
getStylesSafe({code: false}).then(showStyles);
function showStyles(styles) {
if (!installed) {
// "getStyles" message callback is invoked before document is loaded,
// postpone the action until DOMContentLoaded is fired
document.stylishStyles = styles;
return;
}
styles.sort(function(a, b) { return a.name.localeCompare(b.name)});
styles.forEach(handleUpdate);
if (history.state) {
window.scrollTo(0, history.state.scrollY);
}
}
function createStyleElement(style) {
var e = template.style.cloneNode(true);
e.setAttribute("class", style.enabled ? "enabled" : "disabled");
e.setAttribute("style-id", style.id);
if (style.updateUrl) {
e.setAttribute("style-update-url", style.updateUrl);
}
if (style.md5Url) {
e.setAttribute("style-md5-url", style.md5Url);
}
if (style.originalMd5) {
e.setAttribute("style-original-md5", style.originalMd5);
}
var styleName = e.querySelector(".style-name");
styleName.appendChild(document.createTextNode(style.name));
if (style.url) {
var homepage = template.styleHomepage.cloneNode(true)
homepage.setAttribute("href", style.url);
styleName.appendChild(document.createTextNode(" " ));
styleName.appendChild(homepage);
}
var domains = [];
var urls = [];
var urlPrefixes = [];
var regexps = [];
function add(array, property) {
style.sections.forEach(function(section) {
if (section[property]) {
section[property].filter(function(value) {
return array.indexOf(value) == -1;
}).forEach(function(value) {
array.push(value);
});;
}
});
}
add(domains, 'domains');
add(urls, 'urls');
add(urlPrefixes, 'urlPrefixes');
add(regexps, 'regexps');
var appliesToToShow = [];
if (domains)
appliesToToShow = appliesToToShow.concat(domains);
if (urls)
appliesToToShow = appliesToToShow.concat(urls);
if (urlPrefixes)
appliesToToShow = appliesToToShow.concat(urlPrefixes.map(function(u) { return u + "*"; }));
if (regexps)
appliesToToShow = appliesToToShow.concat(regexps.map(function(u) { return "/" + u + "/"; }));
var appliesToString = "";
var showAppliesToExtra = false;
if (appliesToToShow.length == "")
appliesToString = t('appliesToEverything');
else if (appliesToToShow.length <= 10)
appliesToString = appliesToToShow.join(", ");
else {
appliesToString = appliesToToShow.slice(0, 10).join(", ");
showAppliesToExtra = true;
}
e.querySelector(".applies-to").appendChild(document.createTextNode(t('appliesDisplay', [appliesToString])));
if (showAppliesToExtra) {
e.querySelector(".applies-to").appendChild(appliesToExtraTemplate.cloneNode(true));
}
var editLink = e.querySelector(".style-edit-link");
editLink.setAttribute("href", editLink.getAttribute("href") + style.id);
editLink.addEventListener("click", function(event) {
if (!event.altKey) {
var left = event.button == 0, middle = event.button == 1,
shift = event.shiftKey, ctrl = event.ctrlKey;
var openWindow = left && shift && !ctrl;
var openBackgroundTab = (middle && !shift) || (left && ctrl && !shift);
var openForegroundTab = (middle && shift) || (left && ctrl && shift);
var url = event.target.href || event.target.parentNode.href;
event.preventDefault();
event.stopPropagation();
if (openWindow || openBackgroundTab || openForegroundTab) {
if (openWindow) {
var options = prefs.get("windowPosition");
options.url = url;
chrome.windows.create(options);
} else {
chrome.runtime.sendMessage({
method: "openURL",
url: url,
active: openForegroundTab
});
}
} else {
history.replaceState({scrollY: window.scrollY}, document.title);
getActiveTab(function(tab) {
sessionStorageHash("manageStylesHistory").set(tab.id, url);
location.href = url;
});
}
}
});
e.querySelector(".enable").addEventListener("click", function(event) { enable(event, true); }, false);
e.querySelector(".disable").addEventListener("click", function(event) { enable(event, false); }, false);
e.querySelector(".check-update").addEventListener("click", doCheckUpdate, false);
e.querySelector(".update").addEventListener("click", doUpdate, false);
e.querySelector(".delete").addEventListener("click", doDelete, false);
return e;
}
function enable(event, enabled) {
var id = getId(event);
enableStyle(id, enabled);
}
function doDelete() {
if (!confirm(t('deleteStyleConfirm'))) {
return;
}
var id = getId(event);
deleteStyle(id);
}
function getId(event) {
return getStyleElement(event).getAttribute("style-id");
}
function getStyleElement(event) {
var e = event.target;
while (e) {
if (e.hasAttribute("style-id")) {
return e;
}
e = e.parentNode;
}
return null;
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
switch (request.method) {
case "styleUpdated":
case "styleAdded":
handleUpdate(request.style);
break;
case "styleDeleted":
handleDelete(request.id);
break;
}
});
function handleUpdate(style) {
var element = createStyleElement(style);
var oldElement = installed.querySelector(`[style-id="${style.id}"]`);
if (!oldElement) {
installed.appendChild(element);
return;
}
installed.replaceChild(element, oldElement);
if (style.id == lastUpdatedStyleId) {
lastUpdatedStyleId = null;
element.className = element.className += ' update-done';
element.querySelector('.update-note').innerHTML = t('updateCompleted');
}
}
function handleDelete(id) {
var node = installed.querySelector("[style-id='" + id + "']");
if (node) {
installed.removeChild(node);
}
}
function doCheckUpdate(event) {
checkUpdate(getStyleElement(event));
}
function applyUpdateAll() {
var btnApply = document.getElementById("apply-all-updates");
btnApply.disabled = true;
setTimeout(function() {
btnApply.style.display = "none";
btnApply.disabled = false;
}, 1000);
Array.prototype.forEach.call(document.querySelectorAll(".can-update .update"), function(button) {
button.click();
});
}
function checkUpdateAll() {
var btnCheck = document.getElementById("check-all-updates");
var btnApply = document.getElementById("apply-all-updates");
var noUpdates = document.getElementById("update-all-no-updates");
btnCheck.disabled = true;
btnApply.classList.add("hidden");
noUpdates.classList.add("hidden");
var elements = document.querySelectorAll("[style-update-url]");
var toCheckCount = elements.length;
var updatableCount = 0;
Array.prototype.forEach.call(elements, function(element) {
checkUpdate(element, function(success) {
if (success) {
++updatableCount;
}
if (--toCheckCount == 0) {
btnCheck.disabled = false;
if (updatableCount) {
btnApply.classList.remove("hidden");
} else {
noUpdates.classList.remove("hidden");
setTimeout(function() {
noUpdates.classList.add("hidden");
}, 10000);
}
}
});
});
// notify the automatic updater to reset the next automatic update accordingly
chrome.runtime.sendMessage({
method: 'resetInterval'
});
}
function checkUpdate(element, callback) {
element.querySelector(".update-note").innerHTML = t('checkingForUpdate');
element.className = element.className.replace("checking-update", "").replace("no-update", "").replace("can-update", "") + " checking-update";
var id = element.getAttribute("style-id");
var url = element.getAttribute("style-update-url");
var md5Url = element.getAttribute("style-md5-url");
var originalMd5 = element.getAttribute("style-original-md5");
function handleSuccess(forceUpdate, serverJson) {
chrome.runtime.sendMessage({method: "getStyles", id: id}, function(styles) {
var style = styles[0];
var needsUpdate = false;
if (!forceUpdate && styleSectionsEqual(style, serverJson)) {
handleNeedsUpdate("no", id, serverJson);
} else {
handleNeedsUpdate("yes", id, serverJson);
needsUpdate = true;
}
if (callback) {
callback(needsUpdate);
}
});
}
function handleFailure(status) {
if (status == 0) {
handleNeedsUpdate(t('updateCheckFailServerUnreachable'), id, null);
} else {
handleNeedsUpdate(t('updateCheckFailBadResponseCode', [status]), id, null);
}
if (callback) {
callback(false);
}
}
if (!md5Url || !originalMd5) {
checkUpdateFullCode(url, false, handleSuccess, handleFailure)
} else {
checkUpdateMd5(originalMd5, md5Url, function(needsUpdate) {
if (needsUpdate) {
// If the md5 shows a change we will update regardless of whether the code looks different
checkUpdateFullCode(url, true, handleSuccess, handleFailure);
} else {
handleNeedsUpdate("no", id, null);
if (callback) {
callback(false);
}
}
}, handleFailure);
}
}
function checkUpdateFullCode(url, forceUpdate, successCallback, failureCallback) {
download(url, function(responseText) {
successCallback(forceUpdate, JSON.parse(responseText));
}, failureCallback);
}
function checkUpdateMd5(originalMd5, md5Url, successCallback, failureCallback) {
download(md5Url, function(responseText) {
if (responseText.length != 32) {
failureCallback(-1);
return;
}
successCallback(responseText != originalMd5);
}, failureCallback);
}
function download(url, successCallback, failureCallback) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function (aEvt) {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
successCallback(xhr.responseText)
} else {
failureCallback(xhr.status);
}
}
}
if (url.length > 2000) {
var parts = url.split("?");
xhr.open("POST", parts[0], true);
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xhr.send(parts[1]);
} else {
xhr.open("GET", url, true);
xhr.send();
}
}
function handleNeedsUpdate(needsUpdate, id, serverJson) {
var e = document.querySelector("[style-id='" + id + "']");
e.className = e.className.replace("checking-update", "");
switch (needsUpdate) {
case "yes":
e.className += " can-update";
e.updatedCode = serverJson;
e.querySelector(".update-note").innerHTML = '';
break;
case "no":
e.className += " no-update";
e.querySelector(".update-note").innerHTML = t('updateCheckSucceededNoUpdate');
break;
default:
e.className += " no-update";
e.querySelector(".update-note").innerHTML = needsUpdate;
}
}
function doUpdate(event) {
var element = getStyleElement(event);
var updatedCode = element.updatedCode;
// update everything but name
delete updatedCode.name;
updatedCode.id = element.getAttribute('style-id');
updatedCode.method = "saveStyle";
// updating the UI will be handled by the general update listener
lastUpdatedStyleId = updatedCode.id;
chrome.runtime.sendMessage(updatedCode, function () {});
}
function searchStyles(immediately) {
var query = document.getElementById("search").value.toLocaleLowerCase();
if (query == (searchStyles.lastQuery || "")) {
return;
}
searchStyles.lastQuery = query;
if (immediately) {
doSearch();
} else {
clearTimeout(searchStyles.timeout);
searchStyles.timeout = setTimeout(doSearch, 100);
}
function doSearch() {
chrome.runtime.sendMessage({method: "getStyles"}, function(styles) {
styles.forEach(function(style) {
var el = document.querySelector("[style-id='" + style.id + "']");
if (el) {
el.style.display = !query || isMatchingText(style.name) || isMatchingStyle(style) ? "" : "none";
}
});
});
}
function isMatchingStyle(style) {
return style.sections.some(function(section) {
return Object.keys(section).some(function(key) {
var value = section[key];
switch (typeof value) {
case "string": return isMatchingText(value);
case "object": return value.some(isMatchingText);
}
});
});
}
function isMatchingText(text) {
return text.toLocaleLowerCase().indexOf(query) >= 0;
}
}
function onFilterChange (className, event) {
installed.classList.toggle(className, event.target.checked);
}
function initFilter(className, node) {
node.addEventListener("change", onFilterChange.bind(undefined, className), false);
onFilterChange(className, {target: node});
}
document.addEventListener("DOMContentLoaded", function() {
installed = document.getElementById("installed");
if (document.stylishStyles) {
showStyles(document.stylishStyles);
delete document.stylishStyles;
}
document.getElementById("check-all-updates").addEventListener("click", checkUpdateAll);
document.getElementById("apply-all-updates").addEventListener("click", applyUpdateAll);
document.getElementById("search").addEventListener("input", searchStyles);
searchStyles(true); // re-apply filtering on history Back
setupLivePrefs([
"manage.onlyEnabled",
"manage.onlyEdited",
"show-badge",
"popup.stylesFirst"
]);
initFilter("enabled-only", document.getElementById("manage.onlyEnabled"));
initFilter("edited-only", document.getElementById("manage.onlyEdited"));
});