refactor bg updater; add prefs.subscribe()

This commit is contained in:
tophf 2017-04-20 21:27:10 +03:00
parent c52b8c453f
commit 2e60af40f0
7 changed files with 141 additions and 184 deletions

View File

@ -112,7 +112,7 @@ rules:
no-case-declarations: [2]
no-class-assign: [2]
no-cond-assign: [2, except-parens]
no-confusing-arrow: [2, {allowParens: true}]
no-confusing-arrow: [1, {allowParens: true}]
no-const-assign: [2]
no-constant-condition: [0]
no-continue: [0]

View File

@ -137,17 +137,9 @@ for (const id of Object.keys(contextMenus)) {
chrome.contextMenus.create(item, ignoreChromeError);
}
Object.defineProperty(contextMenus, 'updateOnPrefChanged', {
value: changedPrefs => {
for (const id in changedPrefs) {
if (id in contextMenus) {
chrome.contextMenus.update(id, {
checked: changedPrefs[id],
}, ignoreChromeError);
}
}
}
});
prefs.subscribe((id, checked) => {
chrome.contextMenus.update(id, {checked}, ignoreChromeError);
}, Object.keys(contextMenus));
// *************************************************************************
// [re]inject content scripts
@ -282,10 +274,6 @@ function onRuntimeMessage(request, sender, sendResponse) {
() => sendResponse(false));
return KEEP_CHANNEL_OPEN;
case 'prefChanged':
contextMenus.updateOnPrefChanged(request.prefs);
break;
case 'download':
download(request.url)
.then(sendResponse)

View File

@ -499,10 +499,7 @@ function checkUpdateAll() {
let updatesFound = false;
let checked = 0;
processQueue();
// notify the automatic updater to reset the next automatic update accordingly
chrome.runtime.sendMessage({
method: 'resetInterval'
});
BG.updater.resetInterval();
function processQueue(status) {
if (status === true) {

View File

@ -203,6 +203,10 @@ function debounce(fn, delay, ...args) {
timers.delete(fn);
fn(...args);
});
debounce.unregister = debounce.unregister || (fn => {
clearTimeout(timers.get(fn));
timers.delete(fn);
});
clearTimeout(timers.get(fn));
timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
}

View File

@ -22,54 +22,13 @@ document.onclick = e => {
// prevent double-triggering in case a sub-element was clicked
e.stopPropagation();
function check() {
let total = 0;
let checked = 0;
let updated = 0;
$('#update-progress').style.width = 0;
$('#updates-installed').dataset.value = '';
document.body.classList.add('update-in-progress');
const maxWidth = $('#update-progress').parentElement.clientWidth;
function showProgress() {
$('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px';
$('#updates-installed').dataset.value = updated || '';
}
function done() {
document.body.classList.remove('update-in-progress');
}
BG.update.perform((cmd, value) => {
switch (cmd) {
case 'count':
total = value;
if (!total) {
done();
}
break;
case 'single-updated':
updated++;
// fallthrough
case 'single-skipped':
checked++;
if (total && checked === total) {
done();
}
break;
}
showProgress();
});
// notify the automatic updater to reset the next automatic update accordingly
chrome.runtime.sendMessage({
method: 'resetInterval'
});
}
switch (target.dataset.cmd) {
case 'open-manage':
openURL({url: '/manage.html'});
break;
case 'check-updates':
check();
checkUpdates();
break;
case 'open-keyboard':
@ -84,3 +43,32 @@ document.onclick = e => {
break;
}
};
function checkUpdates() {
let total = 0;
let checked = 0;
let updated = 0;
const installed = $('#updates-installed');
const progress = $('#update-progress');
const maxWidth = progress.parentElement.clientWidth;
progress.style.width = 0;
installed.dataset.value = '';
document.body.classList.add('update-in-progress');
BG.updater.checkAllStyles((state, value) => {
switch (state) {
case BG.updater.COUNT:
total = value;
break;
case BG.updater.UPDATED:
updated++;
// fallthrough
case BG.updater.SKIPPED:
checked++;
break;
}
progress.style.width = Math.round(checked / total * maxWidth) + 'px';
installed.dataset.value = updated || '';
}).then(() => {
document.body.classList.remove('update-in-progress');
});
}

View File

@ -60,6 +60,11 @@ var prefs = new function Prefs() {
'badgeNormal',
];
const onChange = {
any: new Set(),
specific: new Map(),
};
// coalesce multiple pref changes in broadcast
let broadcastPrefs = {};
@ -101,16 +106,26 @@ var prefs = new function Prefs() {
}
values[key] = value;
defineReadonlyProperty(this.readOnlyValues, key, value);
const hasChanged = !equal(value, oldValue);
if (BG && BG != window) {
BG.prefs.set(key, BG.deepCopy(value), {noBroadcast, noSync});
} else {
localStorage[key] = typeof defaults[key] == 'object'
? JSON.stringify(value)
: value;
if (!noBroadcast && !equal(value, oldValue)) {
if (!noBroadcast && hasChanged) {
this.broadcast(key, value, {noSync});
}
}
if (hasChanged) {
const listener = onChange.specific.get(key);
if (listener) {
listener(key, value);
}
for (const listener of onChange.any.values()) {
listener(key, value);
}
}
},
remove: key => this.set(key, undefined),
@ -124,6 +139,16 @@ var prefs = new function Prefs() {
debounce(doSyncSet);
}
},
subscribe(listener, keys) {
if (keys) {
for (const key of keys) {
onChange.specific.set(key, listener);
}
} else {
onChange.any.add(listener);
}
},
});
// Unlike sync, HTML5 localStorage is ready at browser startup
@ -289,15 +314,8 @@ function setupLivePrefs(IDs) {
updateElement({id, element, force: true});
element.addEventListener('change', onChange);
}
chrome.runtime.onMessage.addListener(msg => {
if (msg.prefs) {
for (const id in msg.prefs) {
if (id in checkedProps) {
updateElement({id, value: msg.prefs[id]});
}
}
}
});
prefs.subscribe((id, value) => updateElement({id, value}), IDs);
function onChange() {
const value = this[checkedProps[this.id]];
if (prefs.get(this.id) != value) {

186
update.js
View File

@ -1,118 +1,80 @@
/* eslint brace-style: 1, arrow-parens: 1, space-before-function-paren: 1, arrow-body-style: 1 */
/* globals getStyles, saveStyle */
/* globals getStyles, saveStyle, styleSectionsEqual */
'use strict';
// TODO: refactor to make usable in manage::Updater
var update = {
fetch: (resource, callback) => {
let req = new XMLHttpRequest();
let [url, data] = resource.split('?');
req.open('POST', url, true);
req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
req.onload = () => callback(req.responseText);
req.onerror = req.ontimeout = () => callback();
req.send(data);
},
md5Check: (style, callback, skipped) => {
let req = new XMLHttpRequest();
req.open('GET', style.md5Url, true);
req.onload = () => {
let md5 = req.responseText;
if (md5 && md5 !== style.originalMd5) {
callback(style);
}
else {
skipped(`"${style.name}" style is up-to-date`);
}
};
req.onerror = req.ontimeout = () => skipped('Error validating MD5 checksum');
req.send();
},
list: (callback) => {
getStyles({}, (styles) => callback(styles.filter(style => style.updateUrl)));
},
perform: (observe = function () {}) => {
// TODO: use sectionsAreEqual
// from install.js
function arraysAreEqual (a, b) {
// treat empty array and undefined as equivalent
if (typeof a === 'undefined') {
return (typeof b === 'undefined') || (b.length === 0);
}
if (typeof b === 'undefined') {
return (typeof a === 'undefined') || (a.length === 0);
}
if (a.length !== b.length) {
return false;
}
return a.every(function (entry) {
return b.indexOf(entry) !== -1;
// eslint-disable-next-line no-var
var updater = {
COUNT: 'count',
UPDATED: 'updated',
SKIPPED: 'skipped',
SKIPPED_SAME_MD5: 'up-to-date: MD5 is unchanged',
SKIPPED_SAME_CODE: 'up-to-date: code sections are unchanged',
SKIPPED_ERROR_MD5: 'error: MD5 is invalid',
SKIPPED_ERROR_JSON: 'error: JSON is invalid',
DONE: 'done',
lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
checkAllStyles(observe = () => {}) {
updater.resetInterval();
return new Promise(resolve => {
getStyles({}, styles => {
styles = styles.filter(style => style.updateUrl);
observe(updater.COUNT, styles.length);
Promise.all(styles.map(style =>
updater.checkStyle(style)
.then(saveStyle)
.then(saved => observe(updater.UPDATED, saved))
.catch(err => observe(updater.SKIPPED, style, err))
)).then(() => {
observe(updater.DONE);
resolve();
});
});
}
// from install.js
function sectionsAreEqual(a, b) {
if (a.code !== b.code) {
return false;
}
return ['urls', 'urlPrefixes', 'domains', 'regexps'].every(function (attribute) {
return arraysAreEqual(a[attribute], b[attribute]);
});
}
update.list(styles => {
observe('count', styles.length);
styles.forEach(style => update.md5Check(style, style => update.fetch(style.updateUrl, response => {
if (response) {
let json = JSON.parse(response);
if (json.sections.length === style.sections.length) {
if (json.sections.every((section) => {
return style.sections.some(installedSection => sectionsAreEqual(section, installedSection));
})) {
return observe('single-skipped', '2'); // everything is the same
}
json.method = 'saveStyle';
json.id = style.id;
saveStyle(json).then(style => {
observe('single-updated', style.name);
});
}
else {
return observe('single-skipped', '3'); // style sections mismatch
}
}
}), () => observe('single-skipped', '1')));
});
}
};
// automatically update all user-styles if "updateInterval" pref is set
window.setTimeout(function () {
let id;
function run () {
update.perform(/*(cmd, value) => console.log(cmd, value)*/);
reset();
}
function reset () {
window.clearTimeout(id);
let interval = prefs.get('updateInterval');
// if interval === 0 => automatic update is disabled
},
checkStyle(style) {
return download(style.md5Url)
.then(md5 =>
!md5 || md5.length != 32 ? Promise.reject(updater.SKIPPED_ERROR_MD5) :
md5 == style.originalMd5 ? Promise.reject(updater.SKIPPED_SAME_MD5) :
style.updateUrl)
.then(download)
.then(text => tryJSONparse(text))
.then(json =>
!updater.styleJSONseemsValid(json) ? Promise.reject(updater.SKIPPED_ERROR_JSON) :
styleSectionsEqual(json, style) ? Promise.reject(updater.SKIPPED_SAME_CODE) :
// keep the local name as it could've been customized by the user
Object.assign(json, {
id: style.id,
name: null,
}));
},
styleJSONseemsValid(json) {
return json
&& json.sections
&& json.sections.length
&& typeof json.sections.every == 'function'
&& typeof json.sections[0].code == 'string';
},
schedule() {
const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
if (interval) {
/* console.log('next update', interval); */
id = window.setTimeout(run, interval * 60 * 60 * 1000);
const elapsed = Math.max(0, Date.now() - updater.lastUpdateTime);
debounce(updater.checkAllStyles, Math.max(10e3, interval - elapsed));
} else if (debounce.timers) {
debounce.unregister(updater.checkAllStyles);
}
}
if (prefs.get('updateInterval')) {
run();
}
chrome.runtime.onMessage.addListener(request => {
// when user has changed the predefined time interval in the settings page
if (request.method === 'prefChanged' && 'updateInterval' in request.prefs) {
reset();
}
// when user just manually checked for updates
if (request.method === 'resetInterval') {
reset();
}
});
}, 10000);
},
resetInterval() {
localStorage.lastUpdateTime = updater.lastUpdateTime = Date.now();
updater.schedule();
},
};
updater.schedule();
prefs.subscribe(updater.schedule, ['updateInterval']);