stylus/background/db.js
2018-11-09 10:08:38 +08:00

181 lines
5.2 KiB
JavaScript

/* global chromeLocal ignoreChromeError workerUtil */
/* exported db */
/*
Initialize a database. There are some problems using IndexedDB in Firefox:
https://www.reddit.com/r/firefox/comments/74wttb/note_to_firefox_webextension_developers_who_use/
Some of them are fixed in FF59:
https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_use_indexeddb_when/
*/
'use strict';
const db = (() => {
let exec;
const preparing = prepare();
return {
exec: (...args) =>
preparing.then(() => exec(...args))
};
function prepare() {
return shouldUseIndexedDB().then(
ok => {
if (ok) {
useIndexedDB();
} else {
useChromeStorage();
}
},
err => {
useChromeStorage(err);
}
);
}
function shouldUseIndexedDB() {
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
// which, once detected on the first run, is remembered in chrome.storage.local
// for reliablility and in localStorage for fast synchronous access
// (FF may block localStorage depending on its privacy options)
if (typeof indexedDB === 'undefined') {
return Promise.reject(new Error('indexedDB is undefined'));
}
// test localStorage
const fallbackSet = localStorage.dbInChromeStorage;
if (fallbackSet === 'true') {
return Promise.resolve(false);
}
if (fallbackSet === 'false') {
return Promise.resolve(true);
}
// test storage.local
return chromeLocal.get('dbInChromeStorage')
.then(data => {
if (data && data.dbInChromeStorage) {
return false;
}
return testDBSize()
.then(ok => ok || testDBMutation());
});
}
function testDBSize() {
return dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1)
.then(event => (
event.target.result &&
event.target.result.length &&
event.target.result[0]
));
}
function testDBMutation() {
return dbExecIndexedDB('put', {id: -1})
.then(() => dbExecIndexedDB('get', -1))
.then(event => {
if (!event.target.result) {
throw new Error('failed to get previously put item');
}
if (event.target.result.id !== -1) {
throw new Error('item id is wrong');
}
return dbExecIndexedDB('delete', -1);
})
.then(() => true);
}
function useChromeStorage(err) {
exec = dbExecChromeStorage;
chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError);
if (err) {
chromeLocal.setValue('dbInChromeStorageReason', workerUtil.cloneError(err));
console.warn('Failed to access indexedDB. Switched to storage API.', err);
}
localStorage.dbInChromeStorage = 'true';
}
function useIndexedDB() {
exec = dbExecIndexedDB;
chromeLocal.set({dbInChromeStorage: false}, ignoreChromeError);
localStorage.dbInChromeStorage = 'false';
}
function dbExecIndexedDB(method, ...args) {
return new Promise((resolve, reject) => {
Object.assign(indexedDB.open('stylish', 2), {
onsuccess(event) {
const database = event.target.result;
if (!method) {
resolve(database);
} else {
const transaction = database.transaction(['styles'], 'readwrite');
const store = transaction.objectStore('styles');
try {
Object.assign(store[method](...args), {
onsuccess: event => resolve(event, store, transaction, database),
onerror: reject,
});
} catch (err) {
reject(err);
}
}
},
onerror(event) {
console.warn(event.target.error || event.target.errorCode);
reject(event);
},
onupgradeneeded(event) {
if (event.oldVersion === 0) {
event.target.result.createObjectStore('styles', {
keyPath: 'id',
autoIncrement: true,
});
}
},
});
});
}
function dbExecChromeStorage(method, data) {
const STYLE_KEY_PREFIX = 'style-';
switch (method) {
case 'get':
return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
.then(result => ({target: {result}}));
case 'put':
if (!data.id) {
return getAllStyles().then(styles => {
data.id = 1;
for (const style of styles) {
data.id = Math.max(data.id, style.id + 1);
}
return dbExecChromeStorage('put', data);
});
}
return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
.then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
case 'delete':
return chromeLocal.remove(STYLE_KEY_PREFIX + data);
case 'getAll':
return getAllStyles()
.then(styles => ({target: {result: styles}}));
}
return Promise.reject();
function getAllStyles() {
return chromeLocal.get(null).then(storage => {
const styles = [];
for (const key in storage) {
if (key.startsWith(STYLE_KEY_PREFIX) &&
Number(key.substr(STYLE_KEY_PREFIX.length))) {
styles.push(storage[key]);
}
}
return styles;
});
}
}
})();