/* 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 open().then(database => {
if (!method) {
return database;
if (method === 'putMany') {
return putMany(database, ...args);
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
const transaction = database.transaction(['styles'], mode);
const store = transaction.objectStore('styles');
return storeRequest(store, method, ...args);
function storeRequest(store, method, ...args) {
return new Promise((resolve, reject) => {
const request = store[method](...args);
request.onsuccess = resolve;
request.onerror = reject;
function open() {
const request = indexedDB.open('stylish', 2);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = event => {
if (event.oldVersion === 0) {
event.target.result.createObjectStore('styles', {
keyPath: 'id',
autoIncrement: true,
function putMany(database, items) {
const transaction = database.transaction(['styles'], 'readwrite');
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
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 getMaxId().then(id => {
data.id = id + 1;
return dbExecChromeStorage('put', data);
return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
.then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
case 'putMany': {
const newItems = data.filter(i => !i.id);
const doPut = () =>
chromeLocal.set(data.reduce((o, item) => {
o[STYLE_KEY_PREFIX + item.id] = item;
return o;
}, {}))
.then(() => data.map(d => ({target: {result: d.id}})));
if (newItems.length) {
for (const item of newItems) {
item.id = ++id;
return doPut();
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;
function getMaxId() {
return getAllStyles().then(styles => {
let result = 0;
for (const style of styles) {
if (style.id > result) {
result = style.id;
return result;
})();