diff options
Diffstat (limited to 'src/js/cachestorage.js')
-rw-r--r-- | src/js/cachestorage.js | 983 |
1 files changed, 588 insertions, 395 deletions
diff --git a/src/js/cachestorage.js b/src/js/cachestorage.js index ef056af..19f2dae 100644 --- a/src/js/cachestorage.js +++ b/src/js/cachestorage.js @@ -19,191 +19,439 @@ Home: https://github.com/gorhill/uBlock */ -/* global browser, IDBDatabase, indexedDB */ +/* global indexedDB */ 'use strict'; /******************************************************************************/ import lz4Codec from './lz4.js'; -import µb from './background.js'; import webext from './webext.js'; +import µb from './background.js'; +import { ubolog } from './console.js'; +import * as s14e from './s14e-serializer.js'; /******************************************************************************/ -// The code below has been originally manually imported from: -// Commit: https://github.com/nikrolls/uBlock-Edge/commit/d1538ea9bea89d507219d3219592382eee306134 -// Commit date: 29 October 2016 -// Commit author: https://github.com/nikrolls -// Commit message: "Implement cacheStorage using IndexedDB" - -// The original imported code has been subsequently modified as it was not -// compatible with Firefox. -// (a Promise thing, see https://github.com/dfahlander/Dexie.js/issues/317) -// Furthermore, code to migrate from browser.storage.local to vAPI.storage -// has been added, for seamless migration of cache-related entries into -// indexedDB. - -// https://bugzilla.mozilla.org/show_bug.cgi?id=1371255 -// Firefox-specific: we use indexedDB because browser.storage.local() has -// poor performance in Firefox. -// https://github.com/uBlockOrigin/uBlock-issues/issues/328 -// Use IndexedDB for Chromium as well, to take advantage of LZ4 -// compression. -// https://github.com/uBlockOrigin/uBlock-issues/issues/399 -// Revert Chromium support of IndexedDB, use advanced setting to force -// IndexedDB. -// https://github.com/uBlockOrigin/uBlock-issues/issues/409 -// Allow forcing the use of webext storage on Firefox. - const STORAGE_NAME = 'uBlock0CacheStorage'; +const extensionStorage = webext.storage.local; + +const keysFromGetArg = arg => { + if ( arg === null || arg === undefined ) { return []; } + const type = typeof arg; + if ( type === 'string' ) { return [ arg ]; } + if ( Array.isArray(arg) ) { return arg; } + if ( type !== 'object' ) { return; } + return Object.keys(arg); +}; -// Default to webext storage. -const storageLocal = webext.storage.local; - -let storageReadyResolve; -const storageReadyPromise = new Promise(resolve => { - storageReadyResolve = resolve; -}); - -const cacheStorage = { - name: 'browser.storage.local', - get(...args) { - return storageReadyPromise.then(( ) => - storageLocal.get(...args).catch(reason => { - console.log(reason); - }) - ); - }, - set(...args) { - return storageReadyPromise.then(( ) => - storageLocal.set(...args).catch(reason => { - console.log(reason); - }) - ); - }, - remove(...args) { - return storageReadyPromise.then(( ) => - storageLocal.remove(...args).catch(reason => { - console.log(reason); - }) - ); - }, - clear(...args) { - return storageReadyPromise.then(( ) => - storageLocal.clear(...args).catch(reason => { - console.log(reason); - }) - ); - }, - select: function(selectedBackend) { - let actualBackend = selectedBackend; - if ( actualBackend === undefined || actualBackend === 'unset' ) { - actualBackend = vAPI.webextFlavor.soup.has('firefox') - ? 'indexedDB' - : 'browser.storage.local'; - } - if ( actualBackend === 'indexedDB' ) { - return selectIDB().then(success => { - if ( success || selectedBackend === 'indexedDB' ) { - clearWebext(); - storageReadyResolve(); - return 'indexedDB'; +let fastCache = 'indexedDB'; + +/******************************************************************************* + * + * Extension storage + * + * Always available. + * + * */ + +const cacheStorage = (( ) => { + + const exGet = (api, wanted, outbin) => { + return api.get(wanted).then(inbin => { + inbin = inbin || {}; + const found = Object.keys(inbin); + Object.assign(outbin, inbin); + if ( found.length === wanted.length ) { return; } + const missing = []; + for ( const key of wanted ) { + if ( outbin.hasOwnProperty(key) ) { continue; } + missing.push(key); + } + return missing; + }); + }; + + const compress = async (bin, key, data) => { + const µbhs = µb.hiddenSettings; + const after = await s14e.serializeAsync(data, { + compress: µbhs.cacheStorageCompression, + compressThreshold: µbhs.cacheStorageCompressionThreshold, + multithreaded: µbhs.cacheStorageMultithread, + }); + bin[key] = after; + }; + + const decompress = async (bin, key) => { + const data = bin[key]; + if ( s14e.isSerialized(data) === false ) { return; } + const µbhs = µb.hiddenSettings; + const isLarge = data.length >= µbhs.cacheStorageCompressionThreshold; + bin[key] = await s14e.deserializeAsync(data, { + multithreaded: isLarge && µbhs.cacheStorageMultithread || 1, + }); + }; + + const api = { + get(argbin) { + const outbin = {}; + return exGet( + cacheAPIs[fastCache], + keysFromGetArg(argbin), + outbin + ).then(wanted => { + if ( wanted === undefined ) { return; } + return exGet(extensionStorage, wanted, outbin); + }).then(wanted => { + if ( wanted === undefined ) { return; } + if ( argbin instanceof Object === false ) { return; } + if ( Array.isArray(argbin) ) { return; } + for ( const key of wanted ) { + if ( argbin.hasOwnProperty(key) === false ) { continue; } + outbin[key] = argbin[key]; + } + }).then(( ) => { + const promises = []; + for ( const key of Object.keys(outbin) ) { + promises.push(decompress(outbin, key)); } - clearIDB(); - storageReadyResolve(); - return 'browser.storage.local'; + return Promise.all(promises).then(( ) => outbin); + }).catch(reason => { + ubolog(reason); }); - } - if ( actualBackend === 'browser.storage.local' ) { - clearIDB(); - } - storageReadyResolve(); - return Promise.resolve('browser.storage.local'); - - }, - error: undefined -}; + }, + + async keys(regex) { + const results = await Promise.all([ + cacheAPIs[fastCache].keys(regex), + extensionStorage.get(null).catch(( ) => {}), + ]); + const keys = new Set(results[0]); + const bin = results[1] || {}; + for ( const key of Object.keys(bin) ) { + if ( regex && regex.test(key) === false ) { continue; } + keys.add(key); + } + return keys; + }, + + async set(rawbin) { + const keys = Object.keys(rawbin); + if ( keys.length === 0 ) { return; } + const serializedbin = {}; + const promises = []; + for ( const key of keys ) { + promises.push(compress(serializedbin, key, rawbin[key])); + } + await Promise.all(promises); + cacheAPIs[fastCache].set(rawbin, serializedbin); + return extensionStorage.set(serializedbin).catch(reason => { + ubolog(reason); + }); + }, -// Not all platforms support getBytesInUse -if ( storageLocal.getBytesInUse instanceof Function ) { - cacheStorage.getBytesInUse = function(...args) { - return storageLocal.getBytesInUse(...args).catch(reason => { - console.log(reason); - }); + remove(...args) { + cacheAPIs[fastCache].remove(...args); + return extensionStorage.remove(...args).catch(reason => { + ubolog(reason); + }); + }, + + clear(...args) { + cacheAPIs[fastCache].clear(...args); + return extensionStorage.clear(...args).catch(reason => { + ubolog(reason); + }); + }, + + select(api) { + if ( cacheAPIs.hasOwnProperty(api) === false ) { return fastCache; } + fastCache = api; + for ( const k of Object.keys(cacheAPIs) ) { + if ( k === api ) { continue; } + cacheAPIs[k]['clear'](); + } + return fastCache; + }, }; -} -// Reassign API entries to that of indexedDB-based ones -const selectIDB = async function() { - let db; - let dbPromise; + // Not all platforms support getBytesInUse + if ( extensionStorage.getBytesInUse instanceof Function ) { + api.getBytesInUse = function(...args) { + return extensionStorage.getBytesInUse(...args).catch(reason => { + ubolog(reason); + }); + }; + } - const noopfn = function () { + return api; +})(); + +/******************************************************************************* + * + * Cache API + * + * Purpose is to mirror cache-related items from extension storage, as its + * read/write operations are faster. May not be available/populated in + * private/incognito mode. + * + * */ + +const cacheAPI = (( ) => { + const caches = globalThis.caches; + let cacheStoragePromise; + + const getAPI = ( ) => { + if ( cacheStoragePromise !== undefined ) { return cacheStoragePromise; } + cacheStoragePromise = new Promise(resolve => { + if ( typeof caches !== 'object' || caches === null ) { + ubolog('CacheStorage API not available'); + resolve(null); + return; + } + resolve(caches.open(STORAGE_NAME)); + }).catch(reason => { + ubolog(reason); + return null; + }); + return cacheStoragePromise; }; - const disconnect = function() { - dbTimer.off(); - if ( db instanceof IDBDatabase ) { - db.close(); - db = undefined; + const urlPrefix = 'https://ublock0.invalid/'; + + const keyToURL = key => + `${urlPrefix}${encodeURIComponent(key)}`; + + const urlToKey = url => + decodeURIComponent(url.slice(urlPrefix.length)); + + // Cache API is subject to quota so we will use it only for what is key + // performance-wise + const shouldCache = bin => { + const out = {}; + for ( const key of Object.keys(bin) ) { + if ( key.startsWith('cache/' ) ) { + if ( /^cache\/(compiled|selfie)\//.test(key) === false ) { continue; } + } + out[key] = bin[key]; } + if ( Object.keys(out).length !== 0 ) { return out; } }; - const dbTimer = vAPI.defer.create(( ) => { - disconnect(); - }); + const getOne = async key => { + const cache = await getAPI(); + if ( cache === null ) { return; } + return cache.match(keyToURL(key)).then(response => { + if ( response === undefined ) { return; } + return response.text(); + }).then(text => { + if ( text === undefined ) { return; } + return { key, text }; + }).catch(reason => { + ubolog(reason); + }); + }; - const keepAlive = function() { - dbTimer.offon(Math.max( - µb.hiddenSettings.autoUpdateAssetFetchPeriod * 2 * 1000, - 180000 - )); + const getAll = async ( ) => { + const cache = await getAPI(); + if ( cache === null ) { return; } + return cache.keys().then(requests => { + const promises = []; + for ( const request of requests ) { + promises.push(getOne(urlToKey(request.url))); + } + return Promise.all(promises); + }).then(responses => { + const bin = {}; + for ( const response of responses ) { + if ( response === undefined ) { continue; } + bin[response.key] = response.text; + } + return bin; + }).catch(reason => { + ubolog(reason); + }); }; - // https://github.com/gorhill/uBlock/issues/3156 - // I have observed that no event was fired in Tor Browser 7.0.7 + - // medium security level after the request to open the database was - // created. When this occurs, I have also observed that the `error` - // property was already set, so this means uBO can detect here whether - // the database can be opened successfully. A try-catch block is - // necessary when reading the `error` property because we are not - // allowed to read this property outside of event handlers in newer - // implementation of IDBRequest (my understanding). + const setOne = async (key, text) => { + if ( text === undefined ) { return removeOne(key); } + const blob = new Blob([ text ], { type: 'text/plain;charset=utf-8'}); + const cache = await getAPI(); + if ( cache === null ) { return; } + return cache + .put(keyToURL(key), new Response(blob)) + .catch(reason => { + ubolog(reason); + }); + }; - const getDb = function() { - keepAlive(); - if ( db !== undefined ) { - return Promise.resolve(db); - } - if ( dbPromise !== undefined ) { - return dbPromise; - } - dbPromise = new Promise(resolve => { - let req; - try { - req = indexedDB.open(STORAGE_NAME, 1); - if ( req.error ) { - console.log(req.error); - req = undefined; + const removeOne = async key => { + const cache = await getAPI(); + if ( cache === null ) { return; } + return cache.delete(keyToURL(key)).catch(reason => { + ubolog(reason); + }); + }; + + return { + async get(arg) { + const keys = keysFromGetArg(arg); + if ( keys === undefined ) { return; } + if ( keys.length === 0 ) { + return getAll(); + } + const bin = {}; + const toFetch = keys.slice(); + const hasDefault = typeof arg === 'object' && Array.isArray(arg) === false; + for ( let i = 0; i < toFetch.length; i++ ) { + const key = toFetch[i]; + if ( hasDefault && arg[key] !== undefined ) { + bin[key] = arg[key]; } - } catch(ex) { + toFetch[i] = getOne(key); } - if ( req === undefined ) { - db = null; - dbPromise = undefined; - return resolve(null); + const responses = await Promise.all(toFetch); + for ( const response of responses ) { + if ( response === undefined ) { continue; } + const { key, text } = response; + if ( typeof key !== 'string' ) { continue; } + if ( typeof text !== 'string' ) { continue; } + bin[key] = text; } - req.onupgradeneeded = function(ev) { - // https://github.com/uBlockOrigin/uBlock-issues/issues/2725 - // If context Firefox + incognito mode, fall back to - // browser.storage.local for cache storage purpose. - if ( - vAPI.webextFlavor.soup.has('firefox') && - browser.extension.inIncognitoContext === true - ) { - return req.onerror(); + if ( Object.keys(bin).length === 0 ) { return; } + return bin; + }, + + async keys(regex) { + const cache = await getAPI(); + if ( cache === null ) { return []; } + return cache.keys().then(requests => + requests.map(r => urlToKey(r.url)) + .filter(k => regex === undefined || regex.test(k)) + ).catch(( ) => []); + }, + + async set(rawbin, serializedbin) { + const bin = shouldCache(serializedbin); + if ( bin === undefined ) { return; } + const keys = Object.keys(bin); + const promises = []; + for ( const key of keys ) { + promises.push(setOne(key, bin[key])); + } + return Promise.all(promises); + }, + + remove(keys) { + const toRemove = []; + if ( typeof keys === 'string' ) { + toRemove.push(removeOne(keys)); + } else if ( Array.isArray(keys) ) { + for ( const key of keys ) { + toRemove.push(removeOne(key)); } + } + return Promise.all(toRemove); + }, + + async clear() { + if ( typeof caches !== 'object' || caches === null ) { return; } + return globalThis.caches.delete(STORAGE_NAME).catch(reason => { + ubolog(reason); + }); + }, + + shutdown() { + cacheStoragePromise = undefined; + return this.clear(); + }, + }; +})(); + +/******************************************************************************* + * + * In-memory storage + * + * */ + +const memoryStorage = (( ) => { + + const sessionStorage = vAPI.sessionStorage; + + // This should help speed up loading from suspended state in Firefox for + // Android. + // 20240228 Observation: Slows down loading from suspended state in + // Firefox desktop. Could be different in Firefox for Android. + const shouldCache = bin => { + const out = {}; + for ( const key of Object.keys(bin) ) { + if ( key.startsWith('cache/compiled/') ) { continue; } + out[key] = bin[key]; + } + if ( Object.keys(out).length !== 0 ) { return out; } + }; + + return { + get(...args) { + return sessionStorage.get(...args).then(bin => { + return bin; + }).catch(reason => { + ubolog(reason); + }); + }, + + async keys(regex) { + const bin = await this.get(null); + const keys = []; + for ( const key of Object.keys(bin || {}) ) { + if ( regex && regex.test(key) === false ) { continue; } + keys.push(key); + } + return keys; + }, + + async set(rawbin, serializedbin) { + const bin = shouldCache(serializedbin); + if ( bin === undefined ) { return; } + return sessionStorage.set(bin).catch(reason => { + ubolog(reason); + }); + }, + + remove(...args) { + return sessionStorage.remove(...args).catch(reason => { + ubolog(reason); + }); + }, + + clear(...args) { + return sessionStorage.clear(...args).catch(reason => { + ubolog(reason); + }); + }, + + shutdown() { + return this.clear(); + }, + }; +})(); + +/******************************************************************************* + * + * IndexedDB + * + * Deprecated, exists only for the purpose of migrating from older versions. + * + * */ + +const idbStorage = (( ) => { + let dbPromise; + + const getDb = function() { + if ( dbPromise !== undefined ) { return dbPromise; } + dbPromise = new Promise(resolve => { + const req = indexedDB.open(STORAGE_NAME, 1); + req.onupgradeneeded = ev => { if ( ev.oldVersion === 1 ) { return; } try { const db = ev.target.result; @@ -212,35 +460,44 @@ const selectIDB = async function() { req.onerror(); } }; - req.onsuccess = function(ev) { + req.onsuccess = ev => { if ( resolve === undefined ) { return; } - req = undefined; - db = ev.target.result; - dbPromise = undefined; - resolve(db); + resolve(ev.target.result || null); resolve = undefined; }; - req.onerror = req.onblocked = function() { + req.onerror = req.onblocked = ( ) => { if ( resolve === undefined ) { return; } - req = undefined; - console.log(this.error); - db = null; - dbPromise = undefined; + ubolog(req.error); resolve(null); resolve = undefined; }; - vAPI.defer.once(5000).then(( ) => { + vAPI.defer.once(10000).then(( ) => { if ( resolve === undefined ) { return; } - db = null; - dbPromise = undefined; resolve(null); resolve = undefined; }); + }).catch(reason => { + ubolog(`idbStorage() / getDb() failed: ${reason}`); + return null; }); return dbPromise; }; - const fromBlob = function(data) { + // Cache API is subject to quota so we will use it only for what is key + // performance-wise + const shouldCache = bin => { + const out = {}; + for ( const key of Object.keys(bin) ) { + if ( key.startsWith('cache/' ) ) { + if ( /^cache\/(compiled|selfie)\//.test(key) === false ) { continue; } + } + out[key] = bin[key]; + } + if ( Object.keys(out).length === 0 ) { return; } + return out; + }; + + const fromBlob = data => { if ( data instanceof Blob === false ) { return Promise.resolve(data); } @@ -253,277 +510,213 @@ const selectIDB = async function() { }); }; - const toBlob = function(data) { - const value = data instanceof Uint8Array - ? new Blob([ data ]) - : data; - return Promise.resolve(value); + const decompress = (key, value) => { + return lz4Codec.decode(value, fromBlob).then(value => { + return { key, value }; + }); }; - const compress = function(store, key, data) { - return lz4Codec.encode(data, toBlob).then(value => { - store.push({ key, value }); + const getAllEntries = async function() { + const db = await getDb(); + if ( db === null ) { return []; } + return new Promise(resolve => { + const entries = []; + const transaction = db.transaction(STORAGE_NAME, 'readonly'); + transaction.oncomplete = + transaction.onerror = + transaction.onabort = ( ) => { + resolve(Promise.all(entries)); + }; + const table = transaction.objectStore(STORAGE_NAME); + const req = table.openCursor(); + req.onsuccess = ev => { + const cursor = ev.target && ev.target.result; + if ( !cursor ) { return; } + const { key, value } = cursor.value; + if ( value instanceof Blob ) { + entries.push(decompress(key, value)); + } else { + entries.push({ key, value }); + } + cursor.continue(); + }; + }).catch(reason => { + ubolog(`idbStorage() / getAllEntries() failed: ${reason}`); + return []; }); }; - const decompress = function(store, key, data) { - return lz4Codec.decode(data, fromBlob).then(data => { - store[key] = data; + const getAllKeys = async function(regex) { + const db = await getDb(); + if ( db === null ) { return []; } + return new Promise(resolve => { + const keys = []; + const transaction = db.transaction(STORAGE_NAME, 'readonly'); + transaction.oncomplete = + transaction.onerror = + transaction.onabort = ( ) => { + resolve(keys); + }; + const table = transaction.objectStore(STORAGE_NAME); + const req = table.openCursor(); + req.onsuccess = ev => { + const cursor = ev.target && ev.target.result; + if ( !cursor ) { return; } + if ( regex && regex.test(cursor.key) === false ) { return; } + keys.push(cursor.key); + cursor.continue(); + }; + }).catch(reason => { + ubolog(`idbStorage() / getAllKeys() failed: ${reason}`); + return []; }); }; - const getFromDb = async function(keys, keyvalStore, callback) { - if ( typeof callback !== 'function' ) { return; } - if ( keys.length === 0 ) { return callback(keyvalStore); } - const promises = []; - const gotOne = function() { - if ( typeof this.result !== 'object' ) { return; } - const { key, value } = this.result; - keyvalStore[key] = value; - if ( value instanceof Blob === false ) { return; } - promises.push(decompress(keyvalStore, key, value)); - }; - try { - const db = await getDb(); - if ( !db ) { return callback(); } + const getEntries = async function(keys) { + const db = await getDb(); + if ( db === null ) { return []; } + return new Promise(resolve => { + const entries = []; + const gotOne = ev => { + const { result } = ev.target; + if ( typeof result !== 'object' ) { return; } + if ( result === null ) { return; } + const { key, value } = result; + if ( value instanceof Blob ) { + entries.push(decompress(key, value)); + } else { + entries.push({ key, value }); + } + }; const transaction = db.transaction(STORAGE_NAME, 'readonly'); transaction.oncomplete = transaction.onerror = - transaction.onabort = ( ) => { - Promise.all(promises).then(( ) => { - callback(keyvalStore); - }); + transaction.onabort = ( ) => { + resolve(Promise.all(entries)); }; const table = transaction.objectStore(STORAGE_NAME); for ( const key of keys ) { const req = table.get(key); req.onsuccess = gotOne; - req.onerror = noopfn; + req.onerror = ( ) => { }; } - } - catch(reason) { - console.info(`cacheStorage.getFromDb() failed: ${reason}`); - callback(); - } - }; - - const visitAllFromDb = async function(visitFn) { - const db = await getDb(); - if ( !db ) { return visitFn(); } - const transaction = db.transaction(STORAGE_NAME, 'readonly'); - transaction.oncomplete = - transaction.onerror = - transaction.onabort = ( ) => visitFn(); - const table = transaction.objectStore(STORAGE_NAME); - const req = table.openCursor(); - req.onsuccess = function(ev) { - let cursor = ev.target && ev.target.result; - if ( !cursor ) { return; } - let entry = cursor.value; - visitFn(entry); - cursor.continue(); - }; - }; - - const getAllFromDb = function(callback) { - if ( typeof callback !== 'function' ) { return; } - const promises = []; - const keyvalStore = {}; - visitAllFromDb(entry => { - if ( entry === undefined ) { - Promise.all(promises).then(( ) => { - callback(keyvalStore); - }); - return; - } - const { key, value } = entry; - keyvalStore[key] = value; - if ( entry.value instanceof Blob === false ) { return; } - promises.push(decompress(keyvalStore, key, value)); }).catch(reason => { - console.info(`cacheStorage.getAllFromDb() failed: ${reason}`); - callback(); + ubolog(`idbStorage() / getEntries() failed: ${reason}`); + return []; }); }; - // https://github.com/uBlockOrigin/uBlock-issues/issues/141 - // Mind that IDBDatabase.transaction() and IDBObjectStore.put() - // can throw: - // https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/transaction - // https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/put - - const putToDb = async function(keyvalStore, callback) { - if ( typeof callback !== 'function' ) { - callback = noopfn; + const getAll = async ( ) => { + const entries = await getAllEntries(); + const outbin = {}; + for ( const { key, value } of entries ) { + outbin[key] = value; } - const keys = Object.keys(keyvalStore); - if ( keys.length === 0 ) { return callback(); } - const promises = [ getDb() ]; - const entries = []; - const dontCompress = - µb.hiddenSettings.cacheStorageCompression !== true; - for ( const key of keys ) { - const value = keyvalStore[key]; - const isString = typeof value === 'string'; - if ( isString === false || dontCompress ) { - entries.push({ key, value }); - continue; + return outbin; + }; + + const setEntries = async inbin => { + const keys = Object.keys(inbin); + if ( keys.length === 0 ) { return; } + const db = await getDb(); + if ( db === null ) { return; } + return new Promise(resolve => { + const entries = []; + for ( const key of keys ) { + entries.push({ key, value: inbin[key] }); } - promises.push(compress(entries, key, value)); - } - const finish = ( ) => { - if ( callback === undefined ) { return; } - let cb = callback; - callback = undefined; - cb(); - }; - try { - const results = await Promise.all(promises); - const db = results[0]; - if ( !db ) { return callback(); } - const transaction = db.transaction( - STORAGE_NAME, - 'readwrite' - ); + const transaction = db.transaction(STORAGE_NAME, 'readwrite'); transaction.oncomplete = transaction.onerror = - transaction.onabort = finish; + transaction.onabort = ( ) => { + resolve(); + }; const table = transaction.objectStore(STORAGE_NAME); for ( const entry of entries ) { table.put(entry); } - } catch (ex) { - finish(); - } + }).catch(reason => { + ubolog(`idbStorage() / setEntries() failed: ${reason}`); + }); }; - const deleteFromDb = async function(input, callback) { - if ( typeof callback !== 'function' ) { - callback = noopfn; - } - const keys = Array.isArray(input) ? input.slice() : [ input ]; - if ( keys.length === 0 ) { return callback(); } - const finish = ( ) => { - if ( callback === undefined ) { return; } - let cb = callback; - callback = undefined; - cb(); - }; - try { - const db = await getDb(); - if ( !db ) { return callback(); } + const deleteEntries = async arg => { + const keys = Array.isArray(arg) ? arg.slice() : [ arg ]; + if ( keys.length === 0 ) { return; } + const db = await getDb(); + if ( db === null ) { return; } + return new Promise(resolve => { const transaction = db.transaction(STORAGE_NAME, 'readwrite'); transaction.oncomplete = transaction.onerror = - transaction.onabort = finish; + transaction.onabort = ( ) => { + resolve(); + }; const table = transaction.objectStore(STORAGE_NAME); for ( const key of keys ) { table.delete(key); } - } catch (ex) { - finish(); - } - }; - - const clearDb = async function(callback) { - if ( typeof callback !== 'function' ) { - callback = noopfn; - } - try { - const db = await getDb(); - if ( !db ) { return callback(); } - const transaction = db.transaction(STORAGE_NAME, 'readwrite'); - transaction.oncomplete = - transaction.onerror = - transaction.onabort = ( ) => { - callback(); - }; - transaction.objectStore(STORAGE_NAME).clear(); - } - catch(reason) { - console.info(`cacheStorage.clearDb() failed: ${reason}`); - callback(); - } + }).catch(reason => { + ubolog(`idbStorage() / deleteEntries() failed: ${reason}`); + }); }; - await getDb(); - if ( !db ) { return false; } - - cacheStorage.name = 'indexedDB'; - cacheStorage.get = function get(keys) { - return storageReadyPromise.then(( ) => - new Promise(resolve => { - if ( keys === null ) { - return getAllFromDb(bin => resolve(bin)); - } - let toRead, output = {}; - if ( typeof keys === 'string' ) { - toRead = [ keys ]; - } else if ( Array.isArray(keys) ) { - toRead = keys; - } else /* if ( typeof keys === 'object' ) */ { - toRead = Object.keys(keys); - output = keys; + return { + async get(argbin) { + const keys = keysFromGetArg(argbin); + if ( keys === undefined ) { return; } + if ( keys.length === 0 ) { return getAll(); } + const entries = await getEntries(keys); + const outbin = {}; + for ( const { key, value } of entries ) { + outbin[key] = value; + } + if ( argbin instanceof Object && Array.isArray(argbin) === false ) { + for ( const key of keys ) { + if ( outbin.hasOwnProperty(key) ) { continue; } + outbin[key] = argbin[key]; } - getFromDb(toRead, output, bin => resolve(bin)); - }) - ); - }; - cacheStorage.set = function set(keys) { - return storageReadyPromise.then(( ) => - new Promise(resolve => { - putToDb(keys, details => resolve(details)); - }) - ); - }; - cacheStorage.remove = function remove(keys) { - return storageReadyPromise.then(( ) => - new Promise(resolve => { - deleteFromDb(keys, ( ) => resolve()); - }) - ); - }; - cacheStorage.clear = function clear() { - return storageReadyPromise.then(( ) => - new Promise(resolve => { - clearDb(( ) => resolve()); - }) - ); - }; - cacheStorage.getBytesInUse = function getBytesInUse() { - return Promise.resolve(0); + } + return outbin; + }, + + async set(rawbin) { + const bin = shouldCache(rawbin); + if ( bin === undefined ) { return; } + return setEntries(bin); + }, + + keys(...args) { + return getAllKeys(...args); + }, + + remove(...args) { + return deleteEntries(...args); + }, + + clear() { + return getDb().then(db => { + if ( db === null ) { return; } + db.close(); + indexedDB.deleteDatabase(STORAGE_NAME); + }).catch(reason => { + ubolog(`idbStorage.clear() failed: ${reason}`); + }); + }, + + async shutdown() { + await this.clear(); + dbPromise = undefined; + }, }; - return true; -}; +})(); -// https://github.com/uBlockOrigin/uBlock-issues/issues/328 -// Delete cache-related entries from webext storage. -const clearWebext = async function() { - let bin; - try { - bin = await webext.storage.local.get('assetCacheRegistry'); - } catch(ex) { - console.error(ex); - } - if ( bin instanceof Object === false ) { return; } - if ( bin.assetCacheRegistry instanceof Object === false ) { return; } - const toRemove = [ - 'assetCacheRegistry', - 'assetSourceRegistry', - ]; - for ( const key in bin.assetCacheRegistry ) { - if ( bin.assetCacheRegistry.hasOwnProperty(key) ) { - toRemove.push('cache/' + key); - } - } - webext.storage.local.remove(toRemove); -}; +/******************************************************************************/ -const clearIDB = function() { - try { - indexedDB.deleteDatabase(STORAGE_NAME); - } catch(ex) { - } +const cacheAPIs = { + 'indexedDB': idbStorage, + 'cacheAPI': cacheAPI, + 'browser.storage.session': memoryStorage, }; /******************************************************************************/ |