From 2e60af40f06591afaadd837bbd9f6a1d6b80d52e Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 20 Apr 2017 21:27:10 +0300 Subject: [PATCH] refactor bg updater; add prefs.subscribe() --- .eslintrc | 2 +- background.js | 18 +---- manage.js | 5 +- messaging.js | 4 + options/index.js | 72 ++++++++---------- prefs.js | 38 +++++++--- update.js | 186 +++++++++++++++++++---------------------------- 7 files changed, 141 insertions(+), 184 deletions(-) diff --git a/.eslintrc b/.eslintrc index b20ffc9d..8cc63436 100644 --- a/.eslintrc +++ b/.eslintrc @@ -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] diff --git a/background.js b/background.js index 1162cc08..415de5ae 100644 --- a/background.js +++ b/background.js @@ -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) diff --git a/manage.js b/manage.js index 464f47ca..0af0d7f1 100644 --- a/manage.js +++ b/manage.js @@ -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) { diff --git a/messaging.js b/messaging.js index c0020345..b688270a 100644 --- a/messaging.js +++ b/messaging.js @@ -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)); } diff --git a/options/index.js b/options/index.js index 6da43d07..2d0b5808 100644 --- a/options/index.js +++ b/options/index.js @@ -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'); + }); +} diff --git a/prefs.js b/prefs.js index 17510093..1c981e63 100644 --- a/prefs.js +++ b/prefs.js @@ -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) { diff --git a/update.js b/update.js index 3ff46ebf..02214985 100644 --- a/update.js +++ b/update.js @@ -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']);