stylus/background/db.js
eight 5646b8a0d9
Add: improve import performance (#547)
* Add: implement db putMany

* Fix: putMany returns a list of ids
2018-11-11 14:04:22 +08:00

192 lines
5.6 KiB
JavaScript

/* global tryCatch chromeLocal ignoreChromeError */
/* 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() {
// 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)
// test localStorage
const fallbackSet = localStorage.dbInChromeStorage;
if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) {
useChromeStorage();
return Promise.resolve();
}
if (fallbackSet === 'false') {
useIndexedDB();
return Promise.resolve();
}
// test storage.local
return chromeLocal.get('dbInChromeStorage')
.then(data =>
data && data.dbInChromeStorage && Promise.reject())
.then(() =>
tryCatch(dbExecIndexedDB, 'getAllKeys', IDBKeyRange.lowerBound(1), 1) ||
Promise.reject())
.then(({target}) => (
(target.result || [])[0] ?
Promise.reject('ok') :
dbExecIndexedDB('put', {id: -1})))
.then(() =>
dbExecIndexedDB('get', -1))
.then(({target}) => (
(target.result || {}).id === -1 ?
dbExecIndexedDB('delete', -1) :
Promise.reject()))
.then(() =>
Promise.reject('ok'))
.catch(result => {
if (result === 'ok') {
useIndexedDB();
} else {
useChromeStorage();
}
});
}
function useChromeStorage() {
exec = dbExecChromeStorage;
chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError);
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() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('stylish', 2);
request.onsuccess = () => resolve(request.result);
request.onerror = reject;
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');
const store = transaction.objectStore('styles');
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) {
return getMaxId().then(id => {
for (const item of newItems) {
item.id = ++id;
}
return doPut();
});
}
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;
});
}
}
})();