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
This commit is contained in:
tophf 2018-01-04 17:04:23 +03:00
parent bf17c7de94
commit 71c3e0c7a8
5 changed files with 161 additions and 120 deletions

View File

@ -8,8 +8,7 @@
*/ */
'use strict'; 'use strict';
// eslint-disable-next-line no-var window.API_METHODS = Object.assign(window.API_METHODS || {}, {
var API_METHODS = {
getStyles, getStyles,
saveStyle, saveStyle,
@ -32,7 +31,7 @@ var API_METHODS = {
}); });
return KEEP_CHANNEL_OPEN; return KEEP_CHANNEL_OPEN;
}, },
}; });
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var browserCommands, contextMenus; var browserCommands, contextMenus;

View File

@ -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);
}
})();

View File

@ -270,7 +270,7 @@ var prefs = new function Prefs() {
if (BG && BG !== window) return; if (BG && BG !== window) return;
if (BG === window) { if (BG === window) {
affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false})); 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) => { chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'sync' && 'settings' in changes) { if (area === 'sync' && 'settings' in changes) {
@ -319,30 +319,7 @@ var prefs = new function Prefs() {
} }
function doSyncSet() { function doSyncSet() {
getSync().set({'settings': values}); chromeSync.setValue('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();
}
}
};
} }
function importFromSync(synced = {}) { function importFromSync(synced = {}) {

View File

@ -1,99 +1,85 @@
/* global LZString loadScript */ /* global loadScript */
'use strict'; 'use strict';
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var [chromeLocal, chromeSync] = [ var [chromeLocal, chromeSync] = (() => {
chrome.storage.local, const native = 'sync' in chrome.storage &&
chrome.storage.sync, !chrome.runtime.id.includes('@temporary');
].map(storage => { if (!native && BG !== window) {
setupOnChangeRelay();
}
return [
createWrapper('local'),
createWrapper('sync'),
];
function createWrapper(name) {
if (!native) createDummyStorage(name);
const storage = chrome.storage[name];
const wrapper = { const wrapper = {
get(options) { get: data => new Promise(resolve => storage.get(data, resolve)),
return new Promise(resolve => { set: data => new Promise(resolve => storage.set(data, () => resolve(data))),
storage.get(options, data => resolve(data)); remove: data => new Promise(resolve => storage.remove(data, resolve)),
});
}, getValue: key => wrapper.get(key).then(data => data[key]),
set(data) { setValue: (key, value) => wrapper.set({[key]: value}),
return new Promise(resolve => {
storage.set(data, () => resolve(data)); getLZValue: key => wrapper.getLZValues([key]).then(data => data[key]),
}); getLZValues: keys =>
}, Promise.all([
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.get(keys),
wrapper.loadLZStringScript(), loadLZStringScript(),
]).then(([data = {}]) => { ]).then(([data = {}, LZString]) => {
for (const key of keys) { for (const key of keys) {
const value = data[key]; const value = data[key];
data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value)); data[key] = value && tryJSONparse(LZString.decompressFromUTF16(value));
} }
return data; return data;
}); }),
}, setLZValue: (key, value) =>
setLZValue(key, value) { loadLZStringScript().then(LZString =>
return wrapper.loadLZStringScript().then(() =>
wrapper.set({ wrapper.set({
[key]: LZString.compressToUTF16(JSON.stringify(value)), [key]: LZString.compressToUTF16(JSON.stringify(value)),
})); })),
}
loadLZStringScript,
}; };
return wrapper; 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);
}
};
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 styleSectionsEqual({sections: a}, {sections: b}) {
if (!a || !b) {
return undefined;
}
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]));
function propertiesEqual(secA, secB) {
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
return false;
}
}
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
}
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))
);
}
} }
})();

View File

@ -23,6 +23,7 @@
"js/messaging.js", "js/messaging.js",
"js/storage-util.js", "js/storage-util.js",
"js/sections-equal.js", "js/sections-equal.js",
"background/storage-dummy.js",
"background/storage.js", "background/storage.js",
"js/prefs.js", "js/prefs.js",
"js/script-loader.js", "js/script-loader.js",