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:
|
||||
CodeMirror: false
|
||||
runTryCatch: true
|
||||
getStylesSafe: true
|
||||
getStyles: true
|
||||
updateIcon: true
|
||||
saveStyle: true
|
||||
|
|
38
apply.js
38
apply.js
|
@ -9,20 +9,22 @@ var retiredStyleIds = [];
|
|||
initObserver();
|
||||
requestStyles();
|
||||
|
||||
function requestStyles() {
|
||||
function requestStyles(options = {}) {
|
||||
// If this is a Stylish page (Edit Style or Manage Styles),
|
||||
// we'll request the styles directly to minimize delay and flicker,
|
||||
// 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.)
|
||||
var request = {method: "getStyles", matchUrl: location.href, enabled: true, asHash: true};
|
||||
if (location.href.indexOf(chrome.extension.getURL("")) == 0) {
|
||||
var bg = chrome.extension.getBackgroundPage();
|
||||
if (bg && bg.getStyles) {
|
||||
// apply styles immediately, then proceed with a normal request that will update the icon
|
||||
bg.getStyles(request, applyStyles);
|
||||
}
|
||||
var request = Object.assign({
|
||||
method: "getStyles",
|
||||
matchUrl: location.href,
|
||||
enabled: true,
|
||||
asHash: true,
|
||||
}, 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);
|
||||
|
@ -34,6 +36,10 @@ function applyOnMessage(request, sender, sendResponse) {
|
|||
removeStyle(request.id, document);
|
||||
break;
|
||||
case "styleUpdated":
|
||||
if (request.codeIsUpdated === false) {
|
||||
applyStyleState(request.style.id, request.style.enabled, document);
|
||||
break;
|
||||
}
|
||||
if (request.style.enabled) {
|
||||
retireStyle(request.style.id);
|
||||
// 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) {
|
||||
var e = doc.getElementById("stylus-" + id);
|
||||
delete g_styleElements["stylus-" + id];
|
||||
|
|
|
@ -1,36 +1,21 @@
|
|||
/* 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
|
||||
// why the content script also asks for this stuff.
|
||||
chrome.webNavigation.onCommitted.addListener(webNavigationListener.bind(this, "styleApply"));
|
||||
// Not supported in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1239349
|
||||
if ("onHistoryStateUpdated" in chrome.webNavigation) {
|
||||
chrome.webNavigation.onHistoryStateUpdated.addListener(webNavigationListener.bind(this, "styleReplaceAll"));
|
||||
}
|
||||
chrome.webNavigation.onCommitted.addListener(webNavigationListener.bind(this, 'styleApply'));
|
||||
chrome.webNavigation.onHistoryStateUpdated.addListener(webNavigationListener.bind(this, 'styleReplaceAll'));
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(webNavigationListener.bind(this, null));
|
||||
|
||||
function webNavigationListener(method, data) {
|
||||
// Until Chrome 41, we can't target a frame with a message
|
||||
// (https://developer.chrome.com/extensions/tabs#method-sendMessage)
|
||||
// so a style affecting a page with an iframe will affect the main page as well.
|
||||
// Skip doing this for frames in pre-41 to prevent page flicker.
|
||||
if (data.frameId != 0 && !frameIdMessageable) {
|
||||
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);
|
||||
getStyles({matchUrl: data.url, enabled: true, asHash: true}, styles => {
|
||||
// we can't inject chrome:// and chrome-extension:// pages except our own
|
||||
// that request the styles on their own, so we'll only update the icon
|
||||
if (method && !data.url.startsWith('chrome')) {
|
||||
chrome.tabs.sendMessage(data.tabId, {method, styles}, {frameId: data.frameId});
|
||||
}
|
||||
// main page frame id is 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;
|
||||
case "invalidateCache":
|
||||
if (typeof invalidateCache != "undefined") {
|
||||
invalidateCache(false);
|
||||
invalidateCache(false, request);
|
||||
}
|
||||
break;
|
||||
case "healthCheck":
|
||||
|
|
18
edit.js
18
edit.js
|
@ -1087,18 +1087,10 @@ function init() {
|
|||
}
|
||||
// This is an edit
|
||||
tE("heading", "editStyleHeading", null, false);
|
||||
requestStyle();
|
||||
function requestStyle() {
|
||||
chrome.runtime.sendMessage({method: "getStyles", id: params.id}, function callback(styles) {
|
||||
if (!styles) { // Chrome is starting up and shows edit.html
|
||||
requestStyle();
|
||||
return;
|
||||
}
|
||||
var style = styles[0];
|
||||
styleId = style.id;
|
||||
initWithStyle(style);
|
||||
});
|
||||
}
|
||||
getStylesSafe({id: params.id}).then(styles => {
|
||||
styleId = styles[0].id;
|
||||
initWithStyle(styles[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function initWithStyle(style) {
|
||||
|
@ -1107,7 +1099,7 @@ function initWithStyle(style) {
|
|||
document.getElementById("url").href = style.url;
|
||||
// if this was done in response to an update, we need to clear existing sections
|
||||
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();
|
||||
// after 100ms the sections will be added asynchronously
|
||||
while (new Date().getTime() - queueStart <= 100 && queue.length) {
|
||||
|
|
|
@ -6,13 +6,9 @@ var appliesToExtraTemplate = document.createElement("span");
|
|||
appliesToExtraTemplate.className = "applies-to-extra";
|
||||
appliesToExtraTemplate.innerHTML = " " + t('appliesDisplayTruncatedSuffix');
|
||||
|
||||
chrome.runtime.sendMessage({method: "getStyles"}, showStyles);
|
||||
getStylesSafe({code: false}).then(showStyles);
|
||||
|
||||
function showStyles(styles) {
|
||||
if (!styles) { // Chrome is starting up
|
||||
chrome.runtime.sendMessage({method: "getStyles"}, showStyles);
|
||||
return;
|
||||
}
|
||||
if (!installed) {
|
||||
// "getStyles" message callback is invoked before document is loaded,
|
||||
// postpone the action until DOMContentLoaded is fired
|
||||
|
|
57
messaging.js
57
messaging.js
|
@ -4,12 +4,15 @@ const OWN_ORIGIN = chrome.runtime.getURL('');
|
|||
|
||||
function notifyAllTabs(request) {
|
||||
// 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 => {
|
||||
for (let tab of tabs) {
|
||||
if (request.codeIsUpdated !== false || tab.url.startsWith(OWN_ORIGIN)) {
|
||||
chrome.tabs.sendMessage(tab.id, request);
|
||||
updateIcon(tab);
|
||||
}
|
||||
chrome.tabs.sendMessage(tab.id, request);
|
||||
updateIcon(tab);
|
||||
}
|
||||
});
|
||||
// notify all open popups
|
||||
|
@ -47,57 +50,59 @@ function refreshAllTabs() {
|
|||
function updateIcon(tab, styles) {
|
||||
// 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)
|
||||
if (tab.url == "chrome://newtab/" && tab.status != "complete") {
|
||||
if (tab.url == 'chrome://newtab/' && tab.status != 'complete') {
|
||||
return;
|
||||
}
|
||||
if (styles) {
|
||||
// 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) {
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
getTabRealURL(tab, function(url) {
|
||||
// if we have access to this, call directly. a page sending a message to itself doesn't seem to work right.
|
||||
if (typeof getStyles != "undefined") {
|
||||
getStyles({matchUrl: url, enabled: true}, stylesReceived);
|
||||
getTabRealURL(tab, url => {
|
||||
// if we have access to this, call directly
|
||||
// (Chrome no longer sends messages to the page itself)
|
||||
const options = {method: 'getStyles', matchUrl: url, enabled: true, asHash: true};
|
||||
if (typeof getStyles != 'undefined') {
|
||||
getStyles(options, stylesReceived);
|
||||
} else {
|
||||
chrome.runtime.sendMessage({method: "getStyles", matchUrl: url, enabled: true}, stylesReceived);
|
||||
chrome.runtime.sendMessage(options, stylesReceived);
|
||||
}
|
||||
});
|
||||
|
||||
function stylesReceived(styles) {
|
||||
var disableAll = "disableAll" in styles ? styles.disableAll : prefs.get("disableAll");
|
||||
var postfix = disableAll ? "x" : styles.length == 0 ? "w" : "";
|
||||
let numStyles = styles.length;
|
||||
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({
|
||||
path: {
|
||||
// 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
|
||||
19: "19" + postfix + ".png", 38: "38" + postfix + ".png",
|
||||
19: '19' + postfix + '.png', 38: '38' + postfix + '.png',
|
||||
},
|
||||
tabId: tab.id
|
||||
}, function() {
|
||||
}, () => {
|
||||
// if the tab was just closed an error may occur,
|
||||
// e.g. 'windowPosition' pref updated in edit.js::window.onbeforeunload
|
||||
if (!chrome.runtime.lastError) {
|
||||
var t = prefs.get("show-badge") && styles.length ? ("" + styles.length) : "";
|
||||
chrome.browserAction.setBadgeText({text: t, tabId: tab.id});
|
||||
const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
|
||||
chrome.browserAction.setBadgeText({text, tabId: tab.id});
|
||||
chrome.browserAction.setBadgeBackgroundColor({
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Write new style links
|
||||
|
|
299
storage.js
299
storage.js
|
@ -17,103 +17,226 @@ function getDatabase(ready, error) {
|
|||
}
|
||||
};
|
||||
|
||||
var cachedStyles = null;
|
||||
|
||||
// Let manage/popup/edit reuse background page variables
|
||||
// Note, only "var"-declared variables are visible from another extension page
|
||||
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;
|
||||
}
|
||||
chrome.runtime.sendMessage(Object.assign({method: 'getStyles'}, options), styles => {
|
||||
if (!styles) {
|
||||
resolve(getStylesSafe(options));
|
||||
} else {
|
||||
cachedStyles = chrome.extension.getBackgroundPage().cachedStyles;
|
||||
resolve(styles);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getStyles(options, callback) {
|
||||
if (cachedStyles != null) {
|
||||
callback(filterStyles(cachedStyles, options));
|
||||
if (cachedStyles.list) {
|
||||
callback(filterStyles(options));
|
||||
return;
|
||||
}
|
||||
getDatabase(function(db) {
|
||||
var tx = db.transaction(["styles"], "readonly");
|
||||
var os = tx.objectStore("styles");
|
||||
var all = [];
|
||||
os.openCursor().onsuccess = function(event) {
|
||||
var cursor = event.target.result;
|
||||
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) {
|
||||
var s = cursor.value;
|
||||
const s = cursor.value;
|
||||
s.id = cursor.key;
|
||||
all.push(cursor.value);
|
||||
cursor.continue();
|
||||
} 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{
|
||||
callback(filterStyles(all, options));
|
||||
callback(filterStyles(options));
|
||||
} catch(e){
|
||||
// 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) {
|
||||
chrome.runtime.sendMessage({method: "invalidateCache"});
|
||||
chrome.runtime.sendMessage({method: 'invalidateCache', added, updated, deletedId});
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
styles = styles.filter(function(style) {
|
||||
return style.enabled == enabled;
|
||||
});
|
||||
if (!cachedStyles.list) {
|
||||
return;
|
||||
}
|
||||
if (url != null) {
|
||||
styles = styles.filter(function(style) {
|
||||
return style.url == url;
|
||||
});
|
||||
}
|
||||
if (id != null) {
|
||||
styles = styles.filter(function(style) {
|
||||
return style.id == id;
|
||||
});
|
||||
}
|
||||
if (matchUrl != null) {
|
||||
// Return as a hash from style to applicable sections? Can only be used with matchUrl.
|
||||
var asHash = "asHash" in options ? options.asHash : false;
|
||||
if (asHash) {
|
||||
var h = {disableAll: prefs.get("disableAll", false)};
|
||||
styles.forEach(function(style) {
|
||||
var applicableSections = getApplicableSections(style, matchUrl);
|
||||
if (applicableSections.length > 0) {
|
||||
h[style.id] = applicableSections;
|
||||
}
|
||||
});
|
||||
return h;
|
||||
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);
|
||||
}
|
||||
styles = styles.filter(function(style) {
|
||||
var applicableSections = getApplicableSections(style, matchUrl);
|
||||
return applicableSections.length > 0;
|
||||
});
|
||||
cachedStyles.filters.clear();
|
||||
return;
|
||||
}
|
||||
return styles;
|
||||
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(options = {}) {
|
||||
const t0 = performance.now()
|
||||
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;
|
||||
}
|
||||
|
||||
// add \t after url to prevent collisions (not sure it can actually happen though)
|
||||
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;
|
||||
}
|
||||
|
||||
const styles = id == null
|
||||
? (code ? cachedStyles.list : cachedStyles.noCode)
|
||||
: [code ? cachedStyles.byId.get(id).style : cachedStyles.byId.get(id).noCode];
|
||||
const filtered = asHash ? {} : [];
|
||||
|
||||
for (let i = 0, style; (style = styles[i]); i++) {
|
||||
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 (sections.length) {
|
||||
filtered[style.id] = sections;
|
||||
}
|
||||
} else if (matchUrl == null || sections.length) {
|
||||
filtered.push(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
//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} = {}) {
|
||||
return new Promise(resolve => {
|
||||
getDatabase(db => {
|
||||
const tx = db.transaction(['styles'], 'readwrite');
|
||||
const os = tx.objectStore('styles');
|
||||
|
||||
delete style.method;
|
||||
|
||||
// Update
|
||||
if (style.id) {
|
||||
style.id = Number(style.id);
|
||||
os.get(style.id).onsuccess = eventGet => {
|
||||
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);
|
||||
addMissingStyleTargets(style);
|
||||
os.put(style).onsuccess = eventPut => {
|
||||
style.id = style.id || eventPut.target.result;
|
||||
invalidateCache(notify, {updated: style});
|
||||
if (notify) {
|
||||
notifyAllTabs({method: 'styleUpdated', style, codeIsUpdated});
|
||||
}
|
||||
invalidateCache(notify);
|
||||
resolve(style);
|
||||
};
|
||||
};
|
||||
|
@ -121,6 +244,7 @@ function saveStyle(style, {notify = true} = {}) {
|
|||
}
|
||||
|
||||
// Create
|
||||
delete style.id;
|
||||
style = Object.assign({
|
||||
// Set optional things if they're undefined
|
||||
enabled: true,
|
||||
|
@ -128,23 +252,12 @@ function saveStyle(style, {notify = true} = {}) {
|
|||
md5Url: null,
|
||||
url: null,
|
||||
originalMd5: null,
|
||||
}, style, {
|
||||
// Set other optional things to empty array if they're undefined
|
||||
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;
|
||||
}, style);
|
||||
addMissingStyleTargets(style);
|
||||
os.add(style).onsuccess = event => {
|
||||
invalidateCache(true);
|
||||
// Give it the ID that was generated
|
||||
style.id = event.target.result;
|
||||
invalidateCache(true, {added: style});
|
||||
notifyAllTabs({method: 'styleAdded', style});
|
||||
resolve(style);
|
||||
};
|
||||
|
@ -152,13 +265,25 @@ function saveStyle(style, {notify = true} = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
function enableStyle(id, enabled) {
|
||||
saveStyle({id: id, enabled: enabled}).then(style => {
|
||||
handleUpdate(style);
|
||||
notifyAllTabs({method: "styleUpdated", style});
|
||||
});
|
||||
|
||||
function addMissingStyleTargets(style) {
|
||||
style.sections = (style.sections || []).map(section =>
|
||||
Object.assign({
|
||||
urls: [],
|
||||
urlPrefixes: [],
|
||||
domains: [],
|
||||
regexps: [],
|
||||
}, section)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function enableStyle(id, enabled) {
|
||||
saveStyle({id, enabled})
|
||||
.then(handleUpdate);
|
||||
}
|
||||
|
||||
|
||||
function deleteStyle(id, callback = function (){}) {
|
||||
getDatabase(function(db) {
|
||||
var tx = db.transaction(["styles"], "readwrite");
|
||||
|
@ -166,13 +291,14 @@ function deleteStyle(id, callback = function (){}) {
|
|||
var request = os.delete(Number(id));
|
||||
request.onsuccess = function(event) {
|
||||
handleDelete(id);
|
||||
invalidateCache(true);
|
||||
notifyAllTabs({method: "styleDeleted", id: id});
|
||||
invalidateCache(true, {deletedId: id});
|
||||
notifyAllTabs({method: "styleDeleted", id});
|
||||
callback();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function reportError() {
|
||||
for (i in arguments) {
|
||||
if ("message" in arguments[i]) {
|
||||
|
@ -182,6 +308,7 @@ function reportError() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function fixBoolean(b) {
|
||||
if (typeof b != "undefined") {
|
||||
return b != "false";
|
||||
|
@ -189,6 +316,7 @@ function fixBoolean(b) {
|
|||
return null;
|
||||
}
|
||||
|
||||
|
||||
function getDomains(url) {
|
||||
if (url.indexOf("file:") == 0) {
|
||||
return [];
|
||||
|
@ -202,6 +330,7 @@ function getDomains(url) {
|
|||
return domains;
|
||||
}
|
||||
|
||||
|
||||
function getType(o) {
|
||||
if (typeof o == "undefined" || typeof o == "string") {
|
||||
return typeof o;
|
||||
|
@ -212,7 +341,8 @@ function getType(o) {
|
|||
throw "Not supported - " + o;
|
||||
}
|
||||
|
||||
var namespacePattern = /^\s*(@namespace[^;]+;\s*)+$/;
|
||||
const namespacePattern = /^\s*(@namespace[^;]+;\s*)+$/;
|
||||
|
||||
function getApplicableSections(style, url) {
|
||||
var sections = style.sections.filter(function(section) {
|
||||
return sectionAppliesToUrl(section, url);
|
||||
|
@ -224,6 +354,7 @@ function getApplicableSections(style, url) {
|
|||
return sections;
|
||||
}
|
||||
|
||||
|
||||
function sectionAppliesToUrl(section, url) {
|
||||
// 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) {
|
||||
|
@ -275,6 +406,7 @@ function sectionAppliesToUrl(section, url) {
|
|||
return false;
|
||||
}
|
||||
|
||||
|
||||
function isCheckbox(el) {
|
||||
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) {
|
||||
chrome.runtime.getPackageDirectoryEntry(function(rootDir) {
|
||||
rootDir.getDirectory("codemirror/theme", {create: false}, function(themeDir) {
|
||||
|
@ -481,6 +614,7 @@ function getCodeMirrorThemes(callback) {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
function sessionStorageHash(name) {
|
||||
var hash = {
|
||||
value: {},
|
||||
|
@ -495,6 +629,7 @@ function sessionStorageHash(name) {
|
|||
return hash;
|
||||
}
|
||||
|
||||
|
||||
function deepCopy(obj) {
|
||||
if (!obj || typeof obj != "object") {
|
||||
return obj;
|
||||
|
@ -504,6 +639,7 @@ function deepCopy(obj) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function deepMerge(target, obj1 /* plus any number of object arguments */) {
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
var obj = arguments[i];
|
||||
|
@ -522,6 +658,7 @@ function deepMerge(target, obj1 /* plus any number of object arguments */) {
|
|||
return target;
|
||||
}
|
||||
|
||||
|
||||
function equal(a, b) {
|
||||
if (!a || !b || typeof a != "object" || typeof b != "object") {
|
||||
return a === b;
|
||||
|
@ -537,6 +674,7 @@ function equal(a, b) {
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
function defineReadonlyProperty(obj, key, 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.
|
||||
|
@ -546,7 +684,7 @@ function defineReadonlyProperty(obj, key, value) {
|
|||
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() {
|
||||
if ("sync" in chrome.storage) {
|
||||
return chrome.storage.sync;
|
||||
|
@ -567,6 +705,7 @@ function getSync() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function styleSectionsEqual(styleA, styleB) {
|
||||
if (!styleA.sections || !styleB.sections) {
|
||||
return undefined;
|
||||
|
|
Loading…
Reference in New Issue
Block a user