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
This commit is contained in:
parent
df59fca29c
commit
f4e689721a
|
@ -12,6 +12,7 @@ env:
|
||||||
globals:
|
globals:
|
||||||
CodeMirror: false
|
CodeMirror: false
|
||||||
runTryCatch: true
|
runTryCatch: true
|
||||||
|
getStylesSafe: true
|
||||||
getStyles: true
|
getStyles: true
|
||||||
updateIcon: true
|
updateIcon: true
|
||||||
saveStyle: true
|
saveStyle: true
|
||||||
|
|
38
apply.js
38
apply.js
|
@ -9,21 +9,23 @@ var retiredStyleIds = [];
|
||||||
initObserver();
|
initObserver();
|
||||||
requestStyles();
|
requestStyles();
|
||||||
|
|
||||||
function requestStyles() {
|
function requestStyles(options = {}) {
|
||||||
// If this is a Stylish page (Edit Style or Manage Styles),
|
// If this is a Stylish page (Edit Style or Manage Styles),
|
||||||
// we'll request the styles directly to minimize delay and flicker,
|
// we'll request the styles directly to minimize delay and flicker,
|
||||||
// unless Chrome still starts up and the background page isn't fully loaded.
|
// unless Chrome still starts up and the background page isn't fully loaded.
|
||||||
// (Note: in this case the function may be invoked again from applyStyles.)
|
// (Note: in this case the function may be invoked again from applyStyles.)
|
||||||
var request = {method: "getStyles", matchUrl: location.href, enabled: true, asHash: true};
|
var request = Object.assign({
|
||||||
if (location.href.indexOf(chrome.extension.getURL("")) == 0) {
|
method: "getStyles",
|
||||||
var bg = chrome.extension.getBackgroundPage();
|
matchUrl: location.href,
|
||||||
if (bg && bg.getStyles) {
|
enabled: true,
|
||||||
// apply styles immediately, then proceed with a normal request that will update the icon
|
asHash: true,
|
||||||
bg.getStyles(request, applyStyles);
|
}, options);
|
||||||
}
|
if (typeof getStylesSafe !== 'undefined') {
|
||||||
}
|
getStylesSafe(request).then(applyStyles);
|
||||||
|
} else {
|
||||||
chrome.runtime.sendMessage(request, applyStyles);
|
chrome.runtime.sendMessage(request, applyStyles);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(applyOnMessage);
|
chrome.runtime.onMessage.addListener(applyOnMessage);
|
||||||
|
|
||||||
|
@ -34,6 +36,10 @@ function applyOnMessage(request, sender, sendResponse) {
|
||||||
removeStyle(request.id, document);
|
removeStyle(request.id, document);
|
||||||
break;
|
break;
|
||||||
case "styleUpdated":
|
case "styleUpdated":
|
||||||
|
if (request.codeIsUpdated === false) {
|
||||||
|
applyStyleState(request.style.id, request.style.enabled, document);
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (request.style.enabled) {
|
if (request.style.enabled) {
|
||||||
retireStyle(request.style.id);
|
retireStyle(request.style.id);
|
||||||
// fallthrough to "styleAdded"
|
// fallthrough to "styleAdded"
|
||||||
|
@ -92,6 +98,20 @@ function disableAll(disable) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyStyleState(id, enabled, doc) {
|
||||||
|
var e = doc.getElementById("stylus-" + id);
|
||||||
|
if (!e) {
|
||||||
|
if (enabled) {
|
||||||
|
requestStyles({id});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
e.sheet.disabled = !enabled;
|
||||||
|
getDynamicIFrames(doc).forEach(function(iframe) {
|
||||||
|
applyStyleState(id, iframe.contentDocument);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function removeStyle(id, doc) {
|
function removeStyle(id, doc) {
|
||||||
var e = doc.getElementById("stylus-" + id);
|
var e = doc.getElementById("stylus-" + id);
|
||||||
delete g_styleElements["stylus-" + id];
|
delete g_styleElements["stylus-" + id];
|
||||||
|
|
|
@ -1,36 +1,21 @@
|
||||||
/* globals wildcardAsRegExp, KEEP_CHANNEL_OPEN */
|
/* globals wildcardAsRegExp, KEEP_CHANNEL_OPEN */
|
||||||
|
|
||||||
var frameIdMessageable;
|
|
||||||
runTryCatch(function() {
|
|
||||||
chrome.tabs.sendMessage(0, {}, {frameId: 0}, function() {
|
|
||||||
var clearError = chrome.runtime.lastError;
|
|
||||||
frameIdMessageable = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// This happens right away, sometimes so fast that the content script isn't even ready. That's
|
// This happens right away, sometimes so fast that the content script isn't even ready. That's
|
||||||
// why the content script also asks for this stuff.
|
// why the content script also asks for this stuff.
|
||||||
chrome.webNavigation.onCommitted.addListener(webNavigationListener.bind(this, "styleApply"));
|
chrome.webNavigation.onCommitted.addListener(webNavigationListener.bind(this, 'styleApply'));
|
||||||
// Not supported in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1239349
|
chrome.webNavigation.onHistoryStateUpdated.addListener(webNavigationListener.bind(this, 'styleReplaceAll'));
|
||||||
if ("onHistoryStateUpdated" in chrome.webNavigation) {
|
|
||||||
chrome.webNavigation.onHistoryStateUpdated.addListener(webNavigationListener.bind(this, "styleReplaceAll"));
|
|
||||||
}
|
|
||||||
chrome.webNavigation.onBeforeNavigate.addListener(webNavigationListener.bind(this, null));
|
chrome.webNavigation.onBeforeNavigate.addListener(webNavigationListener.bind(this, null));
|
||||||
|
|
||||||
function webNavigationListener(method, data) {
|
function webNavigationListener(method, data) {
|
||||||
// Until Chrome 41, we can't target a frame with a message
|
getStyles({matchUrl: data.url, enabled: true, asHash: true}, styles => {
|
||||||
// (https://developer.chrome.com/extensions/tabs#method-sendMessage)
|
// we can't inject chrome:// and chrome-extension:// pages except our own
|
||||||
// so a style affecting a page with an iframe will affect the main page as well.
|
// that request the styles on their own, so we'll only update the icon
|
||||||
// Skip doing this for frames in pre-41 to prevent page flicker.
|
if (method && !data.url.startsWith('chrome')) {
|
||||||
if (data.frameId != 0 && !frameIdMessageable) {
|
chrome.tabs.sendMessage(data.tabId, {method, styles}, {frameId: data.frameId});
|
||||||
return;
|
|
||||||
}
|
|
||||||
getStyles({matchUrl: data.url, enabled: true, asHash: true}, function(styleHash) {
|
|
||||||
if (method) {
|
|
||||||
chrome.tabs.sendMessage(data.tabId, {method: method, styles: styleHash},
|
|
||||||
frameIdMessageable ? {frameId: data.frameId} : undefined);
|
|
||||||
}
|
}
|
||||||
|
// main page frame id is 0
|
||||||
if (data.frameId == 0) {
|
if (data.frameId == 0) {
|
||||||
updateIcon({id: data.tabId, url: data.url}, styleHash);
|
updateIcon({id: data.tabId, url: data.url}, styles);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -70,7 +55,7 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
||||||
return KEEP_CHANNEL_OPEN;
|
return KEEP_CHANNEL_OPEN;
|
||||||
case "invalidateCache":
|
case "invalidateCache":
|
||||||
if (typeof invalidateCache != "undefined") {
|
if (typeof invalidateCache != "undefined") {
|
||||||
invalidateCache(false);
|
invalidateCache(false, request);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "healthCheck":
|
case "healthCheck":
|
||||||
|
|
16
edit.js
16
edit.js
|
@ -1087,19 +1087,11 @@ function init() {
|
||||||
}
|
}
|
||||||
// This is an edit
|
// This is an edit
|
||||||
tE("heading", "editStyleHeading", null, false);
|
tE("heading", "editStyleHeading", null, false);
|
||||||
requestStyle();
|
getStylesSafe({id: params.id}).then(styles => {
|
||||||
function requestStyle() {
|
styleId = styles[0].id;
|
||||||
chrome.runtime.sendMessage({method: "getStyles", id: params.id}, function callback(styles) {
|
initWithStyle(styles[0]);
|
||||||
if (!styles) { // Chrome is starting up and shows edit.html
|
|
||||||
requestStyle();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var style = styles[0];
|
|
||||||
styleId = style.id;
|
|
||||||
initWithStyle(style);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function initWithStyle(style) {
|
function initWithStyle(style) {
|
||||||
document.getElementById("name").value = style.name;
|
document.getElementById("name").value = style.name;
|
||||||
|
@ -1107,7 +1099,7 @@ function initWithStyle(style) {
|
||||||
document.getElementById("url").href = style.url;
|
document.getElementById("url").href = style.url;
|
||||||
// if this was done in response to an update, we need to clear existing sections
|
// if this was done in response to an update, we need to clear existing sections
|
||||||
getSections().forEach(function(div) { div.remove(); });
|
getSections().forEach(function(div) { div.remove(); });
|
||||||
var queue = style.sections.length ? style.sections : [{code: ""}];
|
var queue = style.sections.length ? style.sections.slice() : [{code: ""}];
|
||||||
var queueStart = new Date().getTime();
|
var queueStart = new Date().getTime();
|
||||||
// after 100ms the sections will be added asynchronously
|
// after 100ms the sections will be added asynchronously
|
||||||
while (new Date().getTime() - queueStart <= 100 && queue.length) {
|
while (new Date().getTime() - queueStart <= 100 && queue.length) {
|
||||||
|
|
|
@ -6,13 +6,9 @@ var appliesToExtraTemplate = document.createElement("span");
|
||||||
appliesToExtraTemplate.className = "applies-to-extra";
|
appliesToExtraTemplate.className = "applies-to-extra";
|
||||||
appliesToExtraTemplate.innerHTML = " " + t('appliesDisplayTruncatedSuffix');
|
appliesToExtraTemplate.innerHTML = " " + t('appliesDisplayTruncatedSuffix');
|
||||||
|
|
||||||
chrome.runtime.sendMessage({method: "getStyles"}, showStyles);
|
getStylesSafe({code: false}).then(showStyles);
|
||||||
|
|
||||||
function showStyles(styles) {
|
function showStyles(styles) {
|
||||||
if (!styles) { // Chrome is starting up
|
|
||||||
chrome.runtime.sendMessage({method: "getStyles"}, showStyles);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!installed) {
|
if (!installed) {
|
||||||
// "getStyles" message callback is invoked before document is loaded,
|
// "getStyles" message callback is invoked before document is loaded,
|
||||||
// postpone the action until DOMContentLoaded is fired
|
// postpone the action until DOMContentLoaded is fired
|
||||||
|
|
53
messaging.js
53
messaging.js
|
@ -4,13 +4,16 @@ const OWN_ORIGIN = chrome.runtime.getURL('');
|
||||||
|
|
||||||
function notifyAllTabs(request) {
|
function notifyAllTabs(request) {
|
||||||
// list all tabs including chrome-extension:// which can be ours
|
// list all tabs including chrome-extension:// which can be ours
|
||||||
|
if (request.codeIsUpdated === false && request.style) {
|
||||||
|
request = Object.assign({}, request, {
|
||||||
|
style: getStyleWithNoCode(request.style)
|
||||||
|
});
|
||||||
|
}
|
||||||
chrome.tabs.query({}, tabs => {
|
chrome.tabs.query({}, tabs => {
|
||||||
for (let tab of tabs) {
|
for (let tab of tabs) {
|
||||||
if (request.codeIsUpdated !== false || tab.url.startsWith(OWN_ORIGIN)) {
|
|
||||||
chrome.tabs.sendMessage(tab.id, request);
|
chrome.tabs.sendMessage(tab.id, request);
|
||||||
updateIcon(tab);
|
updateIcon(tab);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// notify all open popups
|
// notify all open popups
|
||||||
const reqPopup = Object.assign({}, request, {method: 'updatePopup', reason: request.method});
|
const reqPopup = Object.assign({}, request, {method: 'updatePopup', reason: request.method});
|
||||||
|
@ -47,57 +50,59 @@ function refreshAllTabs() {
|
||||||
function updateIcon(tab, styles) {
|
function updateIcon(tab, styles) {
|
||||||
// while NTP is still loading only process the request for its main frame with a real url
|
// while NTP is still loading only process the request for its main frame with a real url
|
||||||
// (but when it's loaded we should process style toggle requests from popups, for example)
|
// (but when it's loaded we should process style toggle requests from popups, for example)
|
||||||
if (tab.url == "chrome://newtab/" && tab.status != "complete") {
|
if (tab.url == 'chrome://newtab/' && tab.status != 'complete') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (styles) {
|
if (styles) {
|
||||||
// check for not-yet-existing tabs e.g. omnibox instant search
|
// check for not-yet-existing tabs e.g. omnibox instant search
|
||||||
chrome.tabs.get(tab.id, function() {
|
chrome.tabs.get(tab.id, () => {
|
||||||
if (!chrome.runtime.lastError) {
|
if (!chrome.runtime.lastError) {
|
||||||
// for 'styles' asHash:true fake the length by counting numeric ids manually
|
|
||||||
if (styles.length === undefined) {
|
|
||||||
styles.length = 0;
|
|
||||||
for (var id in styles) {
|
|
||||||
styles.length += id.match(/^\d+$/) ? 1 : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stylesReceived(styles);
|
stylesReceived(styles);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getTabRealURL(tab, function(url) {
|
getTabRealURL(tab, url => {
|
||||||
// if we have access to this, call directly. a page sending a message to itself doesn't seem to work right.
|
// if we have access to this, call directly
|
||||||
if (typeof getStyles != "undefined") {
|
// (Chrome no longer sends messages to the page itself)
|
||||||
getStyles({matchUrl: url, enabled: true}, stylesReceived);
|
const options = {method: 'getStyles', matchUrl: url, enabled: true, asHash: true};
|
||||||
|
if (typeof getStyles != 'undefined') {
|
||||||
|
getStyles(options, stylesReceived);
|
||||||
} else {
|
} else {
|
||||||
chrome.runtime.sendMessage({method: "getStyles", matchUrl: url, enabled: true}, stylesReceived);
|
chrome.runtime.sendMessage(options, stylesReceived);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function stylesReceived(styles) {
|
function stylesReceived(styles) {
|
||||||
var disableAll = "disableAll" in styles ? styles.disableAll : prefs.get("disableAll");
|
let numStyles = styles.length;
|
||||||
var postfix = disableAll ? "x" : styles.length == 0 ? "w" : "";
|
if (numStyles === undefined) {
|
||||||
|
// for 'styles' asHash:true fake the length by counting numeric ids manually
|
||||||
|
numStyles = 0;
|
||||||
|
for (let id of Object.keys(styles)) {
|
||||||
|
numStyles += id.match(/^\d+$/) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
|
||||||
|
const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : '';
|
||||||
chrome.browserAction.setIcon({
|
chrome.browserAction.setIcon({
|
||||||
path: {
|
path: {
|
||||||
// Material Design 2016 new size is 16px
|
// Material Design 2016 new size is 16px
|
||||||
16: "16" + postfix + ".png", 32: "32" + postfix + ".png",
|
16: '16' + postfix + '.png', 32: '32' + postfix + '.png',
|
||||||
// Chromium forks or non-chromium browsers may still use the traditional 19px
|
// Chromium forks or non-chromium browsers may still use the traditional 19px
|
||||||
19: "19" + postfix + ".png", 38: "38" + postfix + ".png",
|
19: '19' + postfix + '.png', 38: '38' + postfix + '.png',
|
||||||
},
|
},
|
||||||
tabId: tab.id
|
tabId: tab.id
|
||||||
}, function() {
|
}, () => {
|
||||||
// if the tab was just closed an error may occur,
|
// if the tab was just closed an error may occur,
|
||||||
// e.g. 'windowPosition' pref updated in edit.js::window.onbeforeunload
|
// e.g. 'windowPosition' pref updated in edit.js::window.onbeforeunload
|
||||||
if (!chrome.runtime.lastError) {
|
if (!chrome.runtime.lastError) {
|
||||||
var t = prefs.get("show-badge") && styles.length ? ("" + styles.length) : "";
|
const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
|
||||||
chrome.browserAction.setBadgeText({text: t, tabId: tab.id});
|
chrome.browserAction.setBadgeText({text, tabId: tab.id});
|
||||||
chrome.browserAction.setBadgeBackgroundColor({
|
chrome.browserAction.setBadgeBackgroundColor({
|
||||||
color: prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal')
|
color: prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//console.log("Tab " + tab.id + " (" + tab.url + ") badge text set to '" + t + "'.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3
popup.js
3
popup.js
|
@ -19,7 +19,8 @@ function updatePopUp(url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.runtime.sendMessage({method: "getStyles", matchUrl: url}, showStyles);
|
getStylesSafe({matchUrl: url}).then(showStyles);
|
||||||
|
|
||||||
document.querySelector("#find-styles a").href = "https://userstyles.org/styles/browse/all/" + encodeURIComponent("file" === urlWillWork[1] ? "file:" : url);
|
document.querySelector("#find-styles a").href = "https://userstyles.org/styles/browse/all/" + encodeURIComponent("file" === urlWillWork[1] ? "file:" : url);
|
||||||
|
|
||||||
// Write new style links
|
// Write new style links
|
||||||
|
|
287
storage.js
287
storage.js
|
@ -17,83 +17,203 @@ function getDatabase(ready, error) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var cachedStyles = null;
|
|
||||||
function getStyles(options, callback) {
|
// Let manage/popup/edit reuse background page variables
|
||||||
if (cachedStyles != null) {
|
// Note, only "var"-declared variables are visible from another extension page
|
||||||
callback(filterStyles(cachedStyles, options));
|
var cachedStyles = ((bg) => bg && bg.cache || {
|
||||||
|
bg,
|
||||||
|
list: null,
|
||||||
|
noCode: null,
|
||||||
|
byId: new Map(),
|
||||||
|
filters: new Map(),
|
||||||
|
mutex: {
|
||||||
|
inProgress: false,
|
||||||
|
onDone: [],
|
||||||
|
},
|
||||||
|
})(chrome.extension.getBackgroundPage());
|
||||||
|
|
||||||
|
|
||||||
|
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
|
||||||
|
function getStylesSafe(options) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (cachedStyles.bg) {
|
||||||
|
getStyles(options, resolve);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getDatabase(function(db) {
|
chrome.runtime.sendMessage(Object.assign({method: 'getStyles'}, options), styles => {
|
||||||
var tx = db.transaction(["styles"], "readonly");
|
if (!styles) {
|
||||||
var os = tx.objectStore("styles");
|
resolve(getStylesSafe(options));
|
||||||
var all = [];
|
} else {
|
||||||
os.openCursor().onsuccess = function(event) {
|
cachedStyles = chrome.extension.getBackgroundPage().cachedStyles;
|
||||||
var cursor = event.target.result;
|
resolve(styles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getStyles(options, callback) {
|
||||||
|
if (cachedStyles.list) {
|
||||||
|
callback(filterStyles(options));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cachedStyles.mutex.inProgress) {
|
||||||
|
cachedStyles.mutex.onDone.push({options, callback});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cachedStyles.mutex.inProgress = true;
|
||||||
|
|
||||||
|
const t0 = performance.now()
|
||||||
|
getDatabase(db => {
|
||||||
|
const tx = db.transaction(['styles'], 'readonly');
|
||||||
|
const os = tx.objectStore('styles');
|
||||||
|
const all = [];
|
||||||
|
os.openCursor().onsuccess = event => {
|
||||||
|
const cursor = event.target.result;
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
var s = cursor.value;
|
const s = cursor.value;
|
||||||
s.id = cursor.key;
|
s.id = cursor.key;
|
||||||
all.push(cursor.value);
|
all.push(cursor.value);
|
||||||
cursor.continue();
|
cursor.continue();
|
||||||
} else {
|
} else {
|
||||||
cachedStyles = all;
|
cachedStyles.list = all;
|
||||||
|
cachedStyles.noCode = [];
|
||||||
|
for (let style of all) {
|
||||||
|
const noCode = getStyleWithNoCode(style);
|
||||||
|
cachedStyles.noCode.push(noCode);
|
||||||
|
cachedStyles.byId.set(style.id, {style, noCode});
|
||||||
|
}
|
||||||
|
//console.log('%s getStyles %s, invoking cached callbacks: %o', (performance.now() - t0).toFixed(1), JSON.stringify(options), cache.mutex.onDone.map(e => JSON.stringify(e.options)))
|
||||||
try{
|
try{
|
||||||
callback(filterStyles(all, options));
|
callback(filterStyles(options));
|
||||||
} catch(e){
|
} catch(e){
|
||||||
// no error in console, it works
|
// no error in console, it works
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cachedStyles.mutex.inProgress = false;
|
||||||
|
for (let {options, callback} of cachedStyles.mutex.onDone) {
|
||||||
|
callback(filterStyles(options));
|
||||||
|
}
|
||||||
|
cachedStyles.mutex.onDone = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, null);
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function invalidateCache(andNotify) {
|
|
||||||
cachedStyles = null;
|
function getStyleWithNoCode(style) {
|
||||||
|
const stripped = Object.assign({}, style, {sections: []});
|
||||||
|
for (let section of style.sections) {
|
||||||
|
stripped.sections.push(Object.assign({}, section, {code: null}));
|
||||||
|
}
|
||||||
|
return stripped;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function invalidateCache(andNotify, {added, updated, deletedId} = {}) {
|
||||||
|
// prevent double-add on echoed invalidation
|
||||||
|
const cached = added && cachedStyles.byId.get(added.id);
|
||||||
|
if (cached) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (andNotify) {
|
if (andNotify) {
|
||||||
chrome.runtime.sendMessage({method: "invalidateCache"});
|
chrome.runtime.sendMessage({method: 'invalidateCache', added, updated, deletedId});
|
||||||
}
|
}
|
||||||
|
if (!cachedStyles.list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
const cached = cachedStyles.byId.get(updated.id);
|
||||||
|
if (cached) {
|
||||||
|
Object.assign(cached.style, updated);
|
||||||
|
Object.assign(cached.noCode, getStyleWithNoCode(updated));
|
||||||
|
//console.log('cache: updated', updated);
|
||||||
|
}
|
||||||
|
cachedStyles.filters.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (added) {
|
||||||
|
const noCode = getStyleWithNoCode(added);
|
||||||
|
cachedStyles.list.push(added);
|
||||||
|
cachedStyles.noCode.push(noCode);
|
||||||
|
cachedStyles.byId.set(added.id, {style: added, noCode});
|
||||||
|
//console.log('cache: added', added);
|
||||||
|
cachedStyles.filters.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (deletedId != undefined) {
|
||||||
|
const deletedStyle = (cachedStyles.byId.get(deletedId) || {}).style;
|
||||||
|
if (deletedStyle) {
|
||||||
|
const cachedIndex = cachedStyles.list.indexOf(deletedStyle);
|
||||||
|
cachedStyles.list.splice(cachedIndex, 1);
|
||||||
|
cachedStyles.noCode.splice(cachedIndex, 1);
|
||||||
|
cachedStyles.byId.delete(deletedId);
|
||||||
|
//console.log('cache: deleted', deletedStyle);
|
||||||
|
cachedStyles.filters.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cachedStyles.list = null;
|
||||||
|
cachedStyles.noCode = null;
|
||||||
|
//console.log('cache cleared');
|
||||||
|
cachedStyles.filters.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterStyles(styles, options) {
|
|
||||||
var enabled = fixBoolean(options.enabled);
|
|
||||||
var url = "url" in options ? options.url : null;
|
|
||||||
var id = "id" in options ? Number(options.id) : null;
|
|
||||||
var matchUrl = "matchUrl" in options ? options.matchUrl : null;
|
|
||||||
|
|
||||||
if (enabled != null) {
|
function filterStyles(options = {}) {
|
||||||
styles = styles.filter(function(style) {
|
const t0 = performance.now()
|
||||||
return style.enabled == enabled;
|
const enabled = fixBoolean(options.enabled);
|
||||||
});
|
const url = 'url' in options ? options.url : null;
|
||||||
|
const id = 'id' in options ? Number(options.id) : null;
|
||||||
|
const matchUrl = 'matchUrl' in options ? options.matchUrl : null;
|
||||||
|
const code = 'code' in options ? options.code : true;
|
||||||
|
const asHash = 'asHash' in options ? options.asHash : false;
|
||||||
|
|
||||||
|
if (enabled == null
|
||||||
|
&& url == null
|
||||||
|
&& id == null
|
||||||
|
&& matchUrl == null
|
||||||
|
&& asHash != true) {
|
||||||
|
//console.log('%c%s filterStyles SKIPPED LOOP %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options))
|
||||||
|
return code ? cachedStyles.list : cachedStyles.noCode;
|
||||||
}
|
}
|
||||||
if (url != null) {
|
|
||||||
styles = styles.filter(function(style) {
|
// add \t after url to prevent collisions (not sure it can actually happen though)
|
||||||
return style.url == url;
|
const cacheKey = '' + enabled + url + '\t' + id + matchUrl + '\t' + code + asHash;
|
||||||
});
|
const cached = cachedStyles.filters.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
//console.log('%c%s filterStyles REUSED RESPONSE %s', 'color:gray', (performance.now() - t0).toFixed(1), JSON.stringify(options))
|
||||||
|
return asHash
|
||||||
|
? Object.assign({disableAll: prefs.get('disableAll', false)}, cached)
|
||||||
|
: cached;
|
||||||
}
|
}
|
||||||
if (id != null) {
|
|
||||||
styles = styles.filter(function(style) {
|
const styles = id == null
|
||||||
return style.id == id;
|
? (code ? cachedStyles.list : cachedStyles.noCode)
|
||||||
});
|
: [code ? cachedStyles.byId.get(id).style : cachedStyles.byId.get(id).noCode];
|
||||||
}
|
const filtered = asHash ? {} : [];
|
||||||
if (matchUrl != null) {
|
|
||||||
// Return as a hash from style to applicable sections? Can only be used with matchUrl.
|
for (let i = 0, style; (style = styles[i]); i++) {
|
||||||
var asHash = "asHash" in options ? options.asHash : false;
|
if ((enabled == null || style.enabled == enabled)
|
||||||
|
&& (url == null || style.url == url)
|
||||||
|
&& (id == null || style.id == id)) {
|
||||||
|
const sections = (asHash || matchUrl != null) && getApplicableSections(style, matchUrl);
|
||||||
if (asHash) {
|
if (asHash) {
|
||||||
var h = {disableAll: prefs.get("disableAll", false)};
|
if (sections.length) {
|
||||||
styles.forEach(function(style) {
|
filtered[style.id] = sections;
|
||||||
var applicableSections = getApplicableSections(style, matchUrl);
|
|
||||||
if (applicableSections.length > 0) {
|
|
||||||
h[style.id] = applicableSections;
|
|
||||||
}
|
}
|
||||||
});
|
} else if (matchUrl == null || sections.length) {
|
||||||
return h;
|
filtered.push(style);
|
||||||
}
|
}
|
||||||
styles = styles.filter(function(style) {
|
|
||||||
var applicableSections = getApplicableSections(style, matchUrl);
|
|
||||||
return applicableSections.length > 0;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return styles;
|
|
||||||
}
|
}
|
||||||
|
//console.log('%s filterStyles %s', (performance.now() - t0).toFixed(1), JSON.stringify(options))
|
||||||
|
cachedStyles.filters.set(cacheKey, filtered);
|
||||||
|
return asHash
|
||||||
|
? Object.assign({disableAll: prefs.get('disableAll', false)}, filtered)
|
||||||
|
: filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function saveStyle(style, {notify = true} = {}) {
|
function saveStyle(style, {notify = true} = {}) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
|
@ -101,19 +221,22 @@ function saveStyle(style, {notify = true} = {}) {
|
||||||
const tx = db.transaction(['styles'], 'readwrite');
|
const tx = db.transaction(['styles'], 'readwrite');
|
||||||
const os = tx.objectStore('styles');
|
const os = tx.objectStore('styles');
|
||||||
|
|
||||||
|
delete style.method;
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
if (style.id) {
|
if (style.id) {
|
||||||
style.id = Number(style.id);
|
style.id = Number(style.id);
|
||||||
os.get(style.id).onsuccess = eventGet => {
|
os.get(style.id).onsuccess = eventGet => {
|
||||||
const oldStyle = Object.assign({}, eventGet.target.result);
|
const oldStyle = Object.assign({}, eventGet.target.result);
|
||||||
const codeIsUpdated = !styleSectionsEqual(style, oldStyle);
|
const codeIsUpdated = 'sections' in style && !styleSectionsEqual(style, oldStyle);
|
||||||
style = Object.assign(oldStyle, style);
|
style = Object.assign(oldStyle, style);
|
||||||
|
addMissingStyleTargets(style);
|
||||||
os.put(style).onsuccess = eventPut => {
|
os.put(style).onsuccess = eventPut => {
|
||||||
style.id = style.id || eventPut.target.result;
|
style.id = style.id || eventPut.target.result;
|
||||||
|
invalidateCache(notify, {updated: style});
|
||||||
if (notify) {
|
if (notify) {
|
||||||
notifyAllTabs({method: 'styleUpdated', style, codeIsUpdated});
|
notifyAllTabs({method: 'styleUpdated', style, codeIsUpdated});
|
||||||
}
|
}
|
||||||
invalidateCache(notify);
|
|
||||||
resolve(style);
|
resolve(style);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -121,6 +244,7 @@ function saveStyle(style, {notify = true} = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
|
delete style.id;
|
||||||
style = Object.assign({
|
style = Object.assign({
|
||||||
// Set optional things if they're undefined
|
// Set optional things if they're undefined
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -128,23 +252,12 @@ function saveStyle(style, {notify = true} = {}) {
|
||||||
md5Url: null,
|
md5Url: null,
|
||||||
url: null,
|
url: null,
|
||||||
originalMd5: null,
|
originalMd5: null,
|
||||||
}, style, {
|
}, style);
|
||||||
// Set other optional things to empty array if they're undefined
|
addMissingStyleTargets(style);
|
||||||
sections: style.sections.map(section =>
|
|
||||||
Object.assign({
|
|
||||||
urls: [],
|
|
||||||
urlPrefixes: [],
|
|
||||||
domains: [],
|
|
||||||
regexps: [],
|
|
||||||
}, section)
|
|
||||||
),
|
|
||||||
})
|
|
||||||
// Make sure it's not null - that makes indexeddb sad
|
|
||||||
delete style.id;
|
|
||||||
os.add(style).onsuccess = event => {
|
os.add(style).onsuccess = event => {
|
||||||
invalidateCache(true);
|
|
||||||
// Give it the ID that was generated
|
// Give it the ID that was generated
|
||||||
style.id = event.target.result;
|
style.id = event.target.result;
|
||||||
|
invalidateCache(true, {added: style});
|
||||||
notifyAllTabs({method: 'styleAdded', style});
|
notifyAllTabs({method: 'styleAdded', style});
|
||||||
resolve(style);
|
resolve(style);
|
||||||
};
|
};
|
||||||
|
@ -152,13 +265,25 @@ function saveStyle(style, {notify = true} = {}) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableStyle(id, enabled) {
|
|
||||||
saveStyle({id: id, enabled: enabled}).then(style => {
|
function addMissingStyleTargets(style) {
|
||||||
handleUpdate(style);
|
style.sections = (style.sections || []).map(section =>
|
||||||
notifyAllTabs({method: "styleUpdated", style});
|
Object.assign({
|
||||||
});
|
urls: [],
|
||||||
|
urlPrefixes: [],
|
||||||
|
domains: [],
|
||||||
|
regexps: [],
|
||||||
|
}, section)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function enableStyle(id, enabled) {
|
||||||
|
saveStyle({id, enabled})
|
||||||
|
.then(handleUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function deleteStyle(id, callback = function (){}) {
|
function deleteStyle(id, callback = function (){}) {
|
||||||
getDatabase(function(db) {
|
getDatabase(function(db) {
|
||||||
var tx = db.transaction(["styles"], "readwrite");
|
var tx = db.transaction(["styles"], "readwrite");
|
||||||
|
@ -166,13 +291,14 @@ function deleteStyle(id, callback = function (){}) {
|
||||||
var request = os.delete(Number(id));
|
var request = os.delete(Number(id));
|
||||||
request.onsuccess = function(event) {
|
request.onsuccess = function(event) {
|
||||||
handleDelete(id);
|
handleDelete(id);
|
||||||
invalidateCache(true);
|
invalidateCache(true, {deletedId: id});
|
||||||
notifyAllTabs({method: "styleDeleted", id: id});
|
notifyAllTabs({method: "styleDeleted", id});
|
||||||
callback();
|
callback();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function reportError() {
|
function reportError() {
|
||||||
for (i in arguments) {
|
for (i in arguments) {
|
||||||
if ("message" in arguments[i]) {
|
if ("message" in arguments[i]) {
|
||||||
|
@ -182,6 +308,7 @@ function reportError() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function fixBoolean(b) {
|
function fixBoolean(b) {
|
||||||
if (typeof b != "undefined") {
|
if (typeof b != "undefined") {
|
||||||
return b != "false";
|
return b != "false";
|
||||||
|
@ -189,6 +316,7 @@ function fixBoolean(b) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getDomains(url) {
|
function getDomains(url) {
|
||||||
if (url.indexOf("file:") == 0) {
|
if (url.indexOf("file:") == 0) {
|
||||||
return [];
|
return [];
|
||||||
|
@ -202,6 +330,7 @@ function getDomains(url) {
|
||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getType(o) {
|
function getType(o) {
|
||||||
if (typeof o == "undefined" || typeof o == "string") {
|
if (typeof o == "undefined" || typeof o == "string") {
|
||||||
return typeof o;
|
return typeof o;
|
||||||
|
@ -212,7 +341,8 @@ function getType(o) {
|
||||||
throw "Not supported - " + o;
|
throw "Not supported - " + o;
|
||||||
}
|
}
|
||||||
|
|
||||||
var namespacePattern = /^\s*(@namespace[^;]+;\s*)+$/;
|
const namespacePattern = /^\s*(@namespace[^;]+;\s*)+$/;
|
||||||
|
|
||||||
function getApplicableSections(style, url) {
|
function getApplicableSections(style, url) {
|
||||||
var sections = style.sections.filter(function(section) {
|
var sections = style.sections.filter(function(section) {
|
||||||
return sectionAppliesToUrl(section, url);
|
return sectionAppliesToUrl(section, url);
|
||||||
|
@ -224,6 +354,7 @@ function getApplicableSections(style, url) {
|
||||||
return sections;
|
return sections;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function sectionAppliesToUrl(section, url) {
|
function sectionAppliesToUrl(section, url) {
|
||||||
// only http, https, file, and chrome-extension allowed
|
// only http, https, file, and chrome-extension allowed
|
||||||
if (url.indexOf("http") != 0 && url.indexOf("file") != 0 && url.indexOf("chrome-extension") != 0 && url.indexOf("ftp") != 0) {
|
if (url.indexOf("http") != 0 && url.indexOf("file") != 0 && url.indexOf("chrome-extension") != 0 && url.indexOf("ftp") != 0) {
|
||||||
|
@ -275,6 +406,7 @@ function sectionAppliesToUrl(section, url) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function isCheckbox(el) {
|
function isCheckbox(el) {
|
||||||
return el.nodeName.toLowerCase() == "input" && "checkbox" == el.type.toLowerCase();
|
return el.nodeName.toLowerCase() == "input" && "checkbox" == el.type.toLowerCase();
|
||||||
}
|
}
|
||||||
|
@ -462,6 +594,7 @@ var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function getCodeMirrorThemes(callback) {
|
function getCodeMirrorThemes(callback) {
|
||||||
chrome.runtime.getPackageDirectoryEntry(function(rootDir) {
|
chrome.runtime.getPackageDirectoryEntry(function(rootDir) {
|
||||||
rootDir.getDirectory("codemirror/theme", {create: false}, function(themeDir) {
|
rootDir.getDirectory("codemirror/theme", {create: false}, function(themeDir) {
|
||||||
|
@ -481,6 +614,7 @@ function getCodeMirrorThemes(callback) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function sessionStorageHash(name) {
|
function sessionStorageHash(name) {
|
||||||
var hash = {
|
var hash = {
|
||||||
value: {},
|
value: {},
|
||||||
|
@ -495,6 +629,7 @@ function sessionStorageHash(name) {
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function deepCopy(obj) {
|
function deepCopy(obj) {
|
||||||
if (!obj || typeof obj != "object") {
|
if (!obj || typeof obj != "object") {
|
||||||
return obj;
|
return obj;
|
||||||
|
@ -504,6 +639,7 @@ function deepCopy(obj) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function deepMerge(target, obj1 /* plus any number of object arguments */) {
|
function deepMerge(target, obj1 /* plus any number of object arguments */) {
|
||||||
for (var i = 1; i < arguments.length; i++) {
|
for (var i = 1; i < arguments.length; i++) {
|
||||||
var obj = arguments[i];
|
var obj = arguments[i];
|
||||||
|
@ -522,6 +658,7 @@ function deepMerge(target, obj1 /* plus any number of object arguments */) {
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function equal(a, b) {
|
function equal(a, b) {
|
||||||
if (!a || !b || typeof a != "object" || typeof b != "object") {
|
if (!a || !b || typeof a != "object" || typeof b != "object") {
|
||||||
return a === b;
|
return a === b;
|
||||||
|
@ -537,6 +674,7 @@ function equal(a, b) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function defineReadonlyProperty(obj, key, value) {
|
function defineReadonlyProperty(obj, key, value) {
|
||||||
var copy = deepCopy(value);
|
var copy = deepCopy(value);
|
||||||
// In ES6, freezing a literal is OK (it returns the same value), but in previous versions it's an exception.
|
// In ES6, freezing a literal is OK (it returns the same value), but in previous versions it's an exception.
|
||||||
|
@ -546,7 +684,7 @@ function defineReadonlyProperty(obj, key, value) {
|
||||||
Object.defineProperty(obj, key, {value: copy, configurable: true})
|
Object.defineProperty(obj, key, {value: copy, configurable: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Polyfill, can be removed when Firefox gets this - https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
|
// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
|
||||||
function getSync() {
|
function getSync() {
|
||||||
if ("sync" in chrome.storage) {
|
if ("sync" in chrome.storage) {
|
||||||
return chrome.storage.sync;
|
return chrome.storage.sync;
|
||||||
|
@ -567,6 +705,7 @@ function getSync() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function styleSectionsEqual(styleA, styleB) {
|
function styleSectionsEqual(styleA, styleB) {
|
||||||
if (!styleA.sections || !styleB.sections) {
|
if (!styleA.sections || !styleB.sections) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user