From 71c3e0c7a81199a47f9697fef34148d4d4a1aaf4 Mon Sep 17 00:00:00 2001 From: tophf Date: Thu, 4 Jan 2018 17:04:23 +0300 Subject: [PATCH] extract and improve dummy chrome.storage in FF * chrome.storage.onChanged supported in own pages * values are stored in the background page * chrome.storage in own pages accesses that background storage --- background/background.js | 5 +- background/storage-dummy.js | 78 +++++++++++++++++ js/prefs.js | 27 +----- js/storage-util.js | 170 +++++++++++++++++------------------- manifest.json | 1 + 5 files changed, 161 insertions(+), 120 deletions(-) create mode 100644 background/storage-dummy.js diff --git a/background/background.js b/background/background.js index 8ca67655..6c31287a 100644 --- a/background/background.js +++ b/background/background.js @@ -8,8 +8,7 @@ */ 'use strict'; -// eslint-disable-next-line no-var -var API_METHODS = { +window.API_METHODS = Object.assign(window.API_METHODS || {}, { getStyles, saveStyle, @@ -32,7 +31,7 @@ var API_METHODS = { }); return KEEP_CHANNEL_OPEN; }, -}; +}); // eslint-disable-next-line no-var var browserCommands, contextMenus; diff --git a/background/storage-dummy.js b/background/storage-dummy.js new file mode 100644 index 00000000..5a2de9b2 --- /dev/null +++ b/background/storage-dummy.js @@ -0,0 +1,78 @@ +'use strict'; + +// eslint-disable-next-line no-unused-expressions +(chrome.runtime.id.includes('@temporary') || !('sync' in chrome.storage)) && (() => { + + const listeners = new Set(); + Object.assign(chrome.storage.onChanged, { + addListener: fn => listeners.add(fn), + hasListener: fn => listeners.has(fn), + removeListener: fn => listeners.delete(fn), + }); + + for (const name of ['local', 'sync']) { + const dummy = tryJSONparse(localStorage['dummyStorage.' + name]) || {}; + chrome.storage[name] = { + get(data, cb) { + let result = {}; + if (data === null) { + result = deepCopy(dummy); + } else if (Array.isArray(data)) { + for (const key of data) { + result[key] = dummy[key]; + } + } else if (typeof data === 'object') { + const hasOwnProperty = Object.prototype.hasOwnProperty; + for (const key in data) { + if (hasOwnProperty.call(data, key)) { + const value = dummy[key]; + result[key] = value === undefined ? data[key] : value; + } + } + } else { + result[data] = dummy[data]; + } + if (typeof cb === 'function') cb(result); + }, + set(data, cb) { + const hasOwnProperty = Object.prototype.hasOwnProperty; + const changes = {}; + for (const key in data) { + if (!hasOwnProperty.call(data, key)) continue; + const newValue = data[key]; + changes[key] = {newValue, oldValue: dummy[key]}; + dummy[key] = newValue; + } + localStorage['dummyStorage.' + name] = JSON.stringify(dummy); + if (typeof cb === 'function') cb(); + notify(changes); + }, + remove(keyOrKeys, cb) { + const changes = {}; + for (const key of Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys]) { + changes[key] = {oldValue: dummy[key]}; + delete dummy[key]; + } + localStorage['dummyStorage.' + name] = JSON.stringify(dummy); + if (typeof cb === 'function') cb(); + notify(changes); + }, + }; + } + + window.API_METHODS = Object.assign(window.API_METHODS || {}, { + dummyStorageGet: ({data, name}) => new Promise(resolve => chrome.storage[name].get(data, resolve)), + dummyStorageSet: ({data, name}) => new Promise(resolve => chrome.storage[name].set(data, resolve)), + dummyStorageRemove: ({data, name}) => new Promise(resolve => chrome.storage[name].remove(data, resolve)), + }); + + function notify(changes, name) { + for (const fn of listeners.values()) { + fn(changes, name); + } + sendMessage({ + dummyStorageChanges: changes, + dummyStorageName: name, + }, ignoreChromeError); + } +})(); diff --git a/js/prefs.js b/js/prefs.js index 737055e6..e545d121 100644 --- a/js/prefs.js +++ b/js/prefs.js @@ -270,7 +270,7 @@ var prefs = new function Prefs() { if (BG && BG !== window) return; if (BG === window) { affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false})); - getSync().get('settings', data => importFromSync.call(this, data.settings)); + chromeSync.getValue('settings', settings => importFromSync.call(this, settings)); } chrome.storage.onChanged.addListener((changes, area) => { if (area === 'sync' && 'settings' in changes) { @@ -319,30 +319,7 @@ var prefs = new function Prefs() { } function doSyncSet() { - getSync().set({'settings': values}); - } - - // Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494 - function getSync() { - if ('sync' in chrome.storage && !chrome.runtime.id.includes('@temporary')) { - return chrome.storage.sync; - } - const crappyStorage = {}; - return { - get(key, callback) { - callback(crappyStorage[key] || {}); - }, - set(source, callback) { - for (const property in source) { - if (source.hasOwnProperty(property)) { - crappyStorage[property] = source[property]; - } - } - if (typeof callback === 'function') { - callback(); - } - } - }; + chromeSync.setValue('settings', values); } function importFromSync(synced = {}) { diff --git a/js/storage-util.js b/js/storage-util.js index 8e947a3c..209e211c 100644 --- a/js/storage-util.js +++ b/js/storage-util.js @@ -1,99 +1,85 @@ -/* global LZString loadScript */ +/* global loadScript */ 'use strict'; // eslint-disable-next-line no-var -var [chromeLocal, chromeSync] = [ - chrome.storage.local, - chrome.storage.sync, -].map(storage => { - const wrapper = { - get(options) { - return new Promise(resolve => { - storage.get(options, data => resolve(data)); - }); - }, - set(data) { - return new Promise(resolve => { - storage.set(data, () => resolve(data)); - }); - }, - remove(keyOrKeys) { - return new Promise(resolve => { - storage.remove(keyOrKeys, resolve); - }); - }, - getValue(key) { - return wrapper.get(key).then(data => data[key]); - }, - setValue(key, value) { - return wrapper.set({[key]: value}); - }, - loadLZStringScript() { - return Promise.resolve( - window.LZString || - loadScript('/vendor/lz-string/lz-string-unsafe.js').then(() => { - window.LZString = window.LZStringUnsafe; - })); - }, - getLZValue(key) { - return wrapper.getLZValues([key]).then(data => data[key]); - }, - getLZValues(keys) { - return Promise.all([ - wrapper.get(keys), - wrapper.loadLZStringScript(), - ]).then(([data = {}]) => { - for (const key of keys) { - const value = data[key]; - data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value)); - } - return data; - }); - }, - setLZValue(key, value) { - return wrapper.loadLZStringScript().then(() => - wrapper.set({ - [key]: LZString.compressToUTF16(JSON.stringify(value)), - })); - } - }; - return wrapper; -}); - - -function styleSectionsEqual({sections: a}, {sections: b}) { - if (!a || !b) { - return undefined; +var [chromeLocal, chromeSync] = (() => { + const native = 'sync' in chrome.storage && + !chrome.runtime.id.includes('@temporary'); + if (!native && BG !== window) { + setupOnChangeRelay(); } - if (a.length !== b.length) { - return false; - } - // order of sections should be identical to account for the case of multiple - // sections matching the same URL because the order of rules is part of cascading - return a.every((sectionA, index) => propertiesEqual(sectionA, b[index])); + return [ + createWrapper('local'), + createWrapper('sync'), + ]; - function propertiesEqual(secA, secB) { - for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) { - if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) { - return false; + function createWrapper(name) { + if (!native) createDummyStorage(name); + const storage = chrome.storage[name]; + const wrapper = { + get: data => new Promise(resolve => storage.get(data, resolve)), + set: data => new Promise(resolve => storage.set(data, () => resolve(data))), + remove: data => new Promise(resolve => storage.remove(data, resolve)), + + getValue: key => wrapper.get(key).then(data => data[key]), + setValue: (key, value) => wrapper.set({[key]: value}), + + getLZValue: key => wrapper.getLZValues([key]).then(data => data[key]), + getLZValues: keys => + Promise.all([ + wrapper.get(keys), + loadLZStringScript(), + ]).then(([data = {}, LZString]) => { + for (const key of keys) { + const value = data[key]; + data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value)); + } + return data; + }), + setLZValue: (key, value) => + loadLZStringScript().then(LZString => + wrapper.set({ + [key]: LZString.compressToUTF16(JSON.stringify(value)), + })), + + loadLZStringScript, + }; + return wrapper; + } + + function createDummyStorage(name) { + chrome.storage[name] = { + get: (data, cb) => API.dummyStorageGet({data, name}).then(cb), + set: (data, cb) => API.dummyStorageSet({data, name}).then(cb), + remove: (data, cb) => API.dummyStorageRemove({data, name}).then(cb), + }; + } + + function loadLZStringScript() { + return window.LZString ? + Promise.resolve(window.LZString) : + loadScript('/vendor/lz-string/lz-string-unsafe.js').then(() => + (window.LZString = window.LZString || window.LZStringUnsafe)); + } + + function setupOnChangeRelay() { + const listeners = new Set(); + const onMessage = msg => { + if (!msg.dummyStorageChanges) return; + for (const fn of listeners.values()) { + fn(msg.dummyStorageChanges, msg.dummyStorageName); } - } - return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b); + }; + Object.assign(chrome.storage.onChanged, { + addListener(fn) { + if (!listeners.size) chrome.runtime.onMessage.addListener(onMessage); + listeners.add(fn); + }, + hasListener: fn => listeners.has(fn), + removeListener(fn) { + listeners.delete(fn); + if (!listeners.size) chrome.runtime.onMessage.removeListener(onMessage); + } + }); } - - function equalOrEmpty(a, b, telltale, comparator) { - const typeA = a && typeof a[telltale] === 'function'; - const typeB = b && typeof b[telltale] === 'function'; - return ( - (a === null || a === undefined || (typeA && !a.length)) && - (b === null || b === undefined || (typeB && !b.length)) - ) || typeA && typeB && a.length === b.length && comparator(a, b); - } - - function arrayMirrors(array1, array2) { - return ( - array1.every(el => array2.includes(el)) && - array2.every(el => array1.includes(el)) - ); - } -} +})(); diff --git a/manifest.json b/manifest.json index c573e30a..b249ddfb 100644 --- a/manifest.json +++ b/manifest.json @@ -23,6 +23,7 @@ "js/messaging.js", "js/storage-util.js", "js/sections-equal.js", + "background/storage-dummy.js", "background/storage.js", "js/prefs.js", "js/script-loader.js",