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:
tophf 2017-03-18 01:50:35 +03:00
parent df59fca29c
commit f4e689721a
8 changed files with 299 additions and 160 deletions

View File

@ -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

View File

@ -9,20 +9,22 @@ 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];

View File

@ -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
View File

@ -1087,18 +1087,10 @@ 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) {
@ -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) {

View File

@ -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

View File

@ -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 + "'.");
} }
} }

View File

@ -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

View File

@ -17,103 +17,226 @@ 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 => {
getDatabase(db => { getDatabase(db => {
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;