Allow use of browser.storage.local as cache storage backend in Firefox

Related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/409

By default `indexedDB` is used in Firefox for purpose of cache storage
backend.

This commit allows to force the use of `browser.storage.local` instead
as cache storage backend. For this to happen, set `cacheStorageAPI` to
`browser.storage.local` in advanced settings.

Additionally, should `indexedDB` not be available for whatever reason,
uBO will automatically fallback to `browser.storage.local`.
This commit is contained in:
Raymond Hill 2019-02-17 15:40:09 -05:00
parent 3b81841dc0
commit 0d369cda21
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
6 changed files with 540 additions and 493 deletions

View File

@ -91,7 +91,6 @@ vAPI.app.restart = function() {
// chrome.storage.local.get(null, function(bin){ console.debug('%o', bin); });
vAPI.storage = chrome.storage.local;
vAPI.cacheStorage = chrome.storage.local;
/******************************************************************************/
/******************************************************************************/

View File

@ -396,13 +396,17 @@ const updateAssetSourceRegistry = function(json, silent) {
const getAssetSourceRegistry = function(callback) {
if ( assetSourceRegistryPromise === undefined ) {
assetSourceRegistryPromise = new Promise(resolve => {
// start of executor
µBlock.cacheStorage.get('assetSourceRegistry', bin => {
assetSourceRegistryPromise = µBlock.cacheStorage.get(
'assetSourceRegistry'
).then(bin => {
if (
bin instanceof Object === false ||
bin.assetSourceRegistry instanceof Object === false
bin instanceof Object &&
bin.assetSourceRegistry instanceof Object
) {
assetSourceRegistry = bin.assetSourceRegistry;
return;
}
return new Promise(resolve => {
api.fetchText(
µBlock.assetsBootstrapLocation || 'assets/assets.json',
details => {
@ -410,12 +414,7 @@ const getAssetSourceRegistry = function(callback) {
resolve();
}
);
return;
}
assetSourceRegistry = bin.assetSourceRegistry;
resolve();
});
// end of executor
});
}
@ -451,16 +450,15 @@ let assetCacheRegistry = {};
const getAssetCacheRegistry = function() {
if ( assetCacheRegistryPromise === undefined ) {
assetCacheRegistryPromise = new Promise(resolve => {
µBlock.cacheStorage.get('assetCacheRegistry', bin => {
assetCacheRegistryPromise = µBlock.cacheStorage.get(
'assetCacheRegistry'
).then(bin => {
if (
bin instanceof Object &&
bin.assetCacheRegistry instanceof Object
) {
assetCacheRegistry = bin.assetCacheRegistry;
}
resolve();
});
});
}
@ -509,8 +507,11 @@ const assetCacheRead = function(assetKey, callback) {
reportBack(bin[internalKey]);
};
getAssetCacheRegistry().then(( ) => {
µBlock.cacheStorage.get(internalKey, onAssetRead);
Promise.all([
getAssetCacheRegistry(),
µBlock.cacheStorage.get(internalKey),
]).then(results => {
onAssetRead(results[1]);
});
};
@ -537,8 +538,8 @@ const assetCacheWrite = function(assetKey, details, callback) {
entry.remoteURL = details.url;
}
µBlock.cacheStorage.set(
{ [internalKey]: content },
details => {
{ [internalKey]: content }
).then(details => {
if (
details instanceof Object &&
typeof details.bytesInUse === 'number'
@ -546,8 +547,7 @@ const assetCacheWrite = function(assetKey, details, callback) {
entry.byteLength = details.bytesInUse;
}
saveAssetCacheRegistry(true);
}
);
});
const result = { assetKey, content };
if ( typeof callback === 'function' ) {
callback(result);
@ -556,9 +556,7 @@ const assetCacheWrite = function(assetKey, details, callback) {
fireNotification('after-asset-updated', result);
};
getAssetCacheRegistry().then(( ) => {
µBlock.cacheStorage.get(internalKey, onReady);
});
getAssetCacheRegistry().then(( ) => onReady());
};
const assetCacheRemove = function(pattern, callback) {

View File

@ -34,78 +34,114 @@
// 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.cacheStorage
// 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 chrome.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.
µBlock.cacheStorage = (function() {
const STORAGE_NAME = 'uBlock0CacheStorage';
// https://bugzilla.mozilla.org/show_bug.cgi?id=1371255
// Firefox-specific: we use indexedDB because chrome.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.
if (
vAPI.webextFlavor.soup.has('firefox') === false &&
µBlock.hiddenSettings.cacheStorageAPI !== 'indexedDB'
) {
// In case IndexedDB was used as cache storage, remove it.
indexedDB.deleteDatabase(STORAGE_NAME);
return vAPI.cacheStorage;
// Default to webext storage. Wrapped into promises if the API does not
// support returning promises.
const promisified = (function() {
try {
return browser.storage.local.get('_') instanceof Promise;
}
let db;
let pendingInitialization;
let dbBytesInUse;
const get = function get(input, callback) {
if ( typeof callback !== 'function' ) { return; }
if ( input === null ) {
return getAllFromDb(callback);
catch(ex) {
}
var toRead, output = {};
if ( typeof input === 'string' ) {
toRead = [ input ];
} else if ( Array.isArray(input) ) {
toRead = input;
} else /* if ( typeof input === 'object' ) */ {
toRead = Object.keys(input);
output = input;
}
return getFromDb(toRead, output, callback);
};
const set = function set(input, callback) {
putToDb(input, callback);
};
const remove = function remove(key, callback) {
deleteFromDb(key, callback);
};
const clear = function clear(callback) {
clearDb(callback);
};
const getBytesInUse = function getBytesInUse(keys, callback) {
getDbSize(callback);
};
return false;
})();
const api = {
get,
set,
remove,
clear,
getBytesInUse,
name: 'browser.storage.local',
get: promisified ?
browser.storage.local.get :
function(keys) {
return new Promise(resolve => {
browser.storage.local.get(keys, bin => {
resolve(bin);
});
});
},
set: promisified ?
browser.storage.local.set :
function(keys) {
return new Promise(resolve => {
browser.storage.local.set(keys, ( ) => {
resolve();
});
});
},
remove: promisified ?
browser.storage.local.remove :
function(keys) {
return new Promise(resolve => {
browser.storage.local.remove(keys, ( ) => {
resolve();
});
});
},
clear: promisified ?
browser.storage.local.clear :
function() {
return new Promise(resolve => {
browser.storage.local.clear(( ) => {
resolve();
});
});
},
getBytesInUse: promisified ?
browser.storage.local.getBytesInUse :
function(keys) {
return new Promise(resolve => {
browser.storage.local.getBytesInUse(keys, count => {
resolve(count);
});
});
},
select: function(backend) {
if ( backend === undefined || backend === 'unset' ) {
backend = vAPI.webextFlavor.soup.has('firefox')
? 'indexedDB'
: 'browser.storage.local';
}
if ( backend === 'indexedDB' ) {
return selectIDB().then(success => {
if ( success ) {
clearWebext();
return 'indexedDB';
}
clearIDB();
return 'browser.storage.local';
});
}
if ( backend === 'browser.storage.local' ) {
clearIDB();
}
return Promise.resolve('browser.storage.local');
},
error: undefined
};
// Reassign API entries to that of indexedDB-based ones
const selectIDB = function() {
let dbPromise;
let dbTimer;
const genericErrorHandler = function(ev) {
let error = ev.target && ev.target.error;
if ( error && error.name === 'QuotaExceededError' ) {
@ -122,14 +158,15 @@
clearTimeout(dbTimer);
dbTimer = undefined;
}
if ( dbPromise === undefined ) { return; }
dbPromise.then(db => {
if ( db instanceof IDBDatabase ) {
db.close();
db = undefined;
}
dbPromise = undefined;
});
};
let dbTimer;
const keepAlive = function() {
if ( dbTimer !== undefined ) {
clearTimeout(dbTimer);
@ -146,17 +183,6 @@
);
};
const getDb = function getDb() {
if ( db === null ) {
return Promise.resolve(null);
}
keepAlive();
if ( db instanceof IDBDatabase ) {
return Promise.resolve(db);
}
if ( pendingInitialization !== undefined ) {
return pendingInitialization;
}
// 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
@ -166,7 +192,13 @@
// necessary when reading the `error` property because we are not
// allowed to read this propery outside of event handlers in newer
// implementation of IDBRequest (my understanding).
pendingInitialization = new Promise(resolve => {
const getDb = function() {
keepAlive();
if ( dbPromise !== undefined ) {
return dbPromise;
}
dbPromise = new Promise(resolve => {
let req;
try {
req = indexedDB.open(STORAGE_NAME, 1);
@ -177,34 +209,31 @@
} catch(ex) {
}
if ( req === undefined ) {
pendingInitialization = undefined;
db = null;
resolve(null);
return;
return resolve(null);
}
req.onupgradeneeded = function(ev) {
req = undefined;
let db = ev.target.result;
const db = ev.target.result;
db.onerror = db.onabort = genericErrorHandler;
let table = db.createObjectStore(STORAGE_NAME, { keyPath: 'key' });
const table = db.createObjectStore(
STORAGE_NAME,
{ keyPath: 'key' }
);
table.createIndex('value', 'value', { unique: false });
};
req.onsuccess = function(ev) {
pendingInitialization = undefined;
req = undefined;
db = ev.target.result;
const db = ev.target.result;
db.onerror = db.onabort = genericErrorHandler;
resolve(db);
};
req.onerror = req.onblocked = function() {
pendingInitialization = undefined;
req = undefined;
db = null;
console.log(this.error);
resolve(null);
};
});
return pendingInitialization;
return dbPromise;
};
const getFromDb = function(keys, keyvalStore, callback) {
@ -224,9 +253,9 @@
})
);
};
getDb().then(( ) => {
getDb().then(db => {
if ( !db ) { return callback(); }
let transaction = db.transaction(STORAGE_NAME);
const transaction = db.transaction(STORAGE_NAME);
transaction.oncomplete =
transaction.onerror =
transaction.onabort = ( ) => {
@ -234,8 +263,8 @@
callback(keyvalStore);
});
};
let table = transaction.objectStore(STORAGE_NAME);
for ( let key of keys ) {
const table = transaction.objectStore(STORAGE_NAME);
for ( const key of keys ) {
let req = table.get(key);
req.onsuccess = gotOne;
req.onerror = noopfn;
@ -245,14 +274,14 @@
};
const visitAllFromDb = function(visitFn) {
getDb().then(( ) => {
getDb().then(db => {
if ( !db ) { return visitFn(); }
let transaction = db.transaction(STORAGE_NAME);
const transaction = db.transaction(STORAGE_NAME);
transaction.oncomplete =
transaction.onerror =
transaction.onabort = ( ) => visitFn();
let table = transaction.objectStore(STORAGE_NAME);
let req = table.openCursor();
const table = transaction.objectStore(STORAGE_NAME);
const req = table.openCursor();
req.onsuccess = function(ev) {
let cursor = ev.target && ev.target.result;
if ( !cursor ) { return; }
@ -265,8 +294,8 @@
const getAllFromDb = function(callback) {
if ( typeof callback !== 'function' ) { return; }
let promises = [];
let keyvalStore = {};
const promises = [];
const keyvalStore = {};
visitAllFromDb(entry => {
if ( entry === undefined ) {
Promise.all(promises).then(( ) => {
@ -287,35 +316,6 @@
});
};
const getDbSize = function(callback) {
if ( typeof callback !== 'function' ) { return; }
if ( typeof dbBytesInUse === 'number' ) {
return Promise.resolve().then(( ) => {
callback(dbBytesInUse);
});
}
const textEncoder = new TextEncoder();
let totalByteLength = 0;
visitAllFromDb(entry => {
if ( entry === undefined ) {
dbBytesInUse = totalByteLength;
return callback(totalByteLength);
}
let value = entry.value;
if ( typeof value === 'string' ) {
totalByteLength += textEncoder.encode(value).byteLength;
} else if ( value instanceof Blob ) {
totalByteLength += value.size;
} else {
totalByteLength += textEncoder.encode(JSON.stringify(value)).byteLength;
}
if ( typeof entry.key === 'string' ) {
totalByteLength += textEncoder.encode(entry.key).byteLength;
}
});
};
// https://github.com/uBlockOrigin/uBlock-issues/issues/141
// Mind that IDBDatabase.transaction() and IDBObjectStore.put()
// can throw:
@ -330,7 +330,8 @@
if ( keys.length === 0 ) { return callback(); }
const promises = [ getDb() ];
const entries = [];
const dontCompress = µBlock.hiddenSettings.cacheStorageCompression !== true;
const dontCompress =
µBlock.hiddenSettings.cacheStorageCompression !== true;
let bytesInUse = 0;
const handleEncodingResult = result => {
if ( typeof result.data === 'string' ) {
@ -354,17 +355,20 @@
µBlock.lz4Codec.encode(key, data).then(handleEncodingResult)
);
}
Promise.all(promises).then(( ) => {
Promise.all(promises).then(results => {
const db = results[0];
if ( !db ) { return callback(); }
const finish = ( ) => {
dbBytesInUse = undefined;
if ( callback === undefined ) { return; }
let cb = callback;
callback = undefined;
cb({ bytesInUse });
};
try {
const transaction = db.transaction(STORAGE_NAME, 'readwrite');
const transaction = db.transaction(
STORAGE_NAME,
'readwrite'
);
transaction.oncomplete =
transaction.onerror =
transaction.onabort = finish;
@ -382,12 +386,11 @@
if ( typeof callback !== 'function' ) {
callback = noopfn;
}
let keys = Array.isArray(input) ? input.slice() : [ input ];
const keys = Array.isArray(input) ? input.slice() : [ input ];
if ( keys.length === 0 ) { return callback(); }
getDb().then(db => {
if ( !db ) { return callback(); }
let finish = ( ) => {
dbBytesInUse = undefined;
if ( callback === undefined ) { return; }
let cb = callback;
callback = undefined;
@ -414,17 +417,16 @@
}
getDb().then(db => {
if ( !db ) { return callback(); }
let finish = ( ) => {
const finish = ( ) => {
disconnect();
indexedDB.deleteDatabase(STORAGE_NAME);
dbBytesInUse = 0;
if ( callback === undefined ) { return; }
let cb = callback;
callback = undefined;
cb();
};
try {
let req = db.transaction(STORAGE_NAME, 'readwrite')
const req = db.transaction(STORAGE_NAME, 'readwrite')
.objectStore(STORAGE_NAME)
.clear();
req.onsuccess = req.onerror = finish;
@ -434,59 +436,76 @@
});
};
// prime the db so that it's ready asap for next access.
getDb(noopfn);
return getDb().then(db => {
if ( !db ) { return false; }
api.name = 'indexedDB';
api.get = function get(keys) {
return 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;
}
getFromDb(toRead, output, bin => resolve(bin));
});
};
api.set = function set(keys) {
return new Promise(resolve => {
putToDb(keys, details => resolve(details));
});
};
api.remove = function remove(keys) {
return new Promise(resolve => {
deleteFromDb(keys, ( ) => resolve());
});
};
api.clear = function clear() {
return new Promise(resolve => {
clearDb(( ) => resolve());
});
};
api.getBytesInUse = function getBytesInUse() {
return Promise.resolve(0);
};
return true;
});
};
// https://github.com/uBlockOrigin/uBlock-issues/issues/328
// Detect whether browser.storage.local was used as cache storage,
// and if so, move cache-related entries to the new storage.
{
const srcStorage = vAPI.cacheStorage;
const desStorage = api;
srcStorage.get(
[ 'assetCacheRegistry', 'assetSourceRegistry' ],
bin => {
// Delete cache-related entries from webext storage.
const clearWebext = function() {
browser.storage.local.get('assetCacheRegistry', bin => {
if (
bin instanceof Object === false ||
bin.assetSourceRegistry instanceof Object === false
bin.assetCacheRegistry instanceof Object === false
) {
return;
}
desStorage.set(bin);
const toRemove = [
'assetCacheRegistry',
'assetSourceRegistry',
'resourcesSelfie',
'selfie'
];
let toMigrate = 0;
const setEntry = function(assetKey, bin) {
if (
bin instanceof Object &&
bin[assetKey] !== undefined
) {
desStorage.set(bin);
}
toMigrate -= 1;
if ( toMigrate === 0 ) {
srcStorage.remove(toRemove);
}
};
for ( const key in bin.assetCacheRegistry ) {
if ( bin.assetCacheRegistry.hasOwnProperty(key) === false ) {
continue;
}
const assetKey = 'cache/' + key;
srcStorage.get(assetKey, setEntry.bind(null, assetKey));
toMigrate += 1;
toRemove.push(assetKey);
}
if ( toMigrate === 0 ) {
srcStorage.remove(toRemove);
if ( bin.assetCacheRegistry.hasOwnProperty(key) ) {
toRemove.push('cache/' + key);
}
}
);
}
browser.storage.local.remove(toRemove);
});
};
const clearIDB = function() {
indexedDB.deleteDatabase(STORAGE_NAME);
};
return api;
}());

View File

@ -848,15 +848,15 @@ var restoreUserData = function(request) {
// Remove all stored data but keep global counts, people can become
// quite attached to numbers
var resetUserData = function() {
const resetUserData = function() {
let count = 3;
let countdown = ( ) => {
const countdown = ( ) => {
count -= 1;
if ( count === 0 ) {
vAPI.app.restart();
}
};
µb.cacheStorage.clear(countdown); // 1
µb.cacheStorage.clear().then(( ) => countdown()); // 1
vAPI.storage.clear(countdown); // 2
µb.saveLocalSettings(countdown); // 3
vAPI.localStorage.removeItem('immediateHiddenSettings');

View File

@ -333,7 +333,7 @@ const fromFetch = function(to, fetched) {
/******************************************************************************/
const onSelectedFilterListsLoaded = function() {
const onSelectedFilterListsReady = function() {
log.info(`List selection ready ${Date.now()-vAPI.T0} ms after launch`);
const fetchableProps = {
@ -371,6 +371,16 @@ const onSelectedFilterListsLoaded = function() {
/******************************************************************************/
const onHiddenSettingsReady = function() {
return µb.cacheStorage.select(
µb.hiddenSettings.cacheStorageAPI
).then(backend => {
log.info(`Backend storage for cache will be ${backend}`);
});
};
/******************************************************************************/
// TODO(seamless migration):
// Eventually selected filter list keys will be loaded as a fetchable
// property. Until then we need to handle backward and forward
@ -379,14 +389,24 @@ const onSelectedFilterListsLoaded = function() {
const onAdminSettingsRestored = function() {
log.info(`Admin settings ready ${Date.now()-vAPI.T0} ms after launch`);
µb.loadSelectedFilterLists(onSelectedFilterListsLoaded);
Promise.all([
µb.loadHiddenSettings().then(( ) =>
onHiddenSettingsReady()
),
µb.loadSelectedFilterLists(),
]).then(( ) =>
onSelectedFilterListsReady()
);
};
/******************************************************************************/
return function() {
// https://github.com/gorhill/uBlock/issues/531
µb.restoreAdminSettings(onAdminSettingsRestored);
µb.restoreAdminSettings().then(( ) => {
onAdminSettingsRestored();
});
};
/******************************************************************************/

View File

@ -50,7 +50,7 @@
countdown += 1;
vAPI.storage.getBytesInUse(null, process);
}
if ( this.cacheStorage !== vAPI.storage ) {
if ( this.cacheStorage.name !== 'browser.storage.local' ) {
countdown += 1;
this.assets.getBytesInUse().then(count => {
process(count);
@ -91,8 +91,13 @@
/******************************************************************************/
µBlock.loadHiddenSettings = function() {
return new Promise(resolve => {
// >>>> start of executor
vAPI.storage.get('hiddenSettings', bin => {
if ( bin instanceof Object === false ) { return; }
if ( bin instanceof Object === false ) {
return resolve();
}
const hs = bin.hiddenSettings;
if ( hs instanceof Object ) {
const hsDefault = this.hiddenSettingsDefault;
@ -110,6 +115,10 @@
this.saveImmediateHiddenSettings();
}
self.log.verbosity = this.hiddenSettings.consoleLogLevel;
resolve();
});
// <<<< end of executor
});
};
@ -195,9 +204,6 @@
);
};
// Do this here to have these hidden settings loaded ASAP.
µBlock.loadHiddenSettings();
/******************************************************************************/
µBlock.savePermanentFirewallRules = function() {
@ -240,24 +246,29 @@
**/
µBlock.loadSelectedFilterLists = function(callback) {
var µb = this;
vAPI.storage.get('selectedFilterLists', function(bin) {
µBlock.loadSelectedFilterLists = function() {
return new Promise(resolve => {
// >>>> start of executor
vAPI.storage.get('selectedFilterLists', bin => {
// Select default filter lists if first-time launch.
if ( !bin || Array.isArray(bin.selectedFilterLists) === false ) {
µb.assets.metadata(function(availableLists) {
µb.saveSelectedFilterLists(
µb.autoSelectRegionalFilterLists(availableLists)
if (
bin instanceof Object === false ||
Array.isArray(bin.selectedFilterLists) === false
) {
this.assets.metadata(function(availableLists) {
this.saveSelectedFilterLists(
this.autoSelectRegionalFilterLists(availableLists)
);
callback();
resolve();
});
return;
}
// TODO: Removes once 1.1.15 is in widespread use.
// https://github.com/gorhill/uBlock/issues/3383
vAPI.storage.remove('remoteBlacklists');
µb.selectedFilterLists = bin.selectedFilterLists;
callback();
this.selectedFilterLists = bin.selectedFilterLists;
resolve();
});
// <<<< end of executor
});
};
@ -1130,16 +1141,16 @@
// necessarily present, i.e. administrators may removed entries which
// values are left to the user's choice.
µBlock.restoreAdminSettings = function(callback) {
// Support for vAPI.adminStorage is optional (webext).
µBlock.restoreAdminSettings = function() {
return new Promise(resolve => {
// >>>> start of executor
if ( vAPI.adminStorage instanceof Object === false ) {
callback();
return;
return resolve();
}
var onRead = function(json) {
var µb = µBlock;
var data;
vAPI.adminStorage.getItem('adminSettings', json => {
let data;
if ( typeof json === 'string' && json !== '' ) {
try {
data = JSON.parse(json);
@ -1148,13 +1159,12 @@
}
}
if ( typeof data !== 'object' || data === null ) {
callback();
return;
if ( data instanceof Object === false ) {
return resolve();
}
var bin = {};
var binNotEmpty = false;
const bin = {};
let binNotEmpty = false;
// Allows an admin to set their own 'assets.json' file, with their own
// set of stock assets.
@ -1164,8 +1174,8 @@
}
if ( typeof data.userSettings === 'object' ) {
for ( var name in µb.userSettings ) {
if ( µb.userSettings.hasOwnProperty(name) === false ) {
for ( const name in this.userSettings ) {
if ( this.userSettings.hasOwnProperty(name) === false ) {
continue;
}
if ( data.userSettings.hasOwnProperty(name) === false ) {
@ -1208,13 +1218,14 @@
}
if ( typeof data.userFilters === 'string' ) {
µb.assets.put(µb.userFiltersPath, data.userFilters);
this.assets.put(this.userFiltersPath, data.userFilters);
}
callback();
};
resolve();
});
vAPI.adminStorage.getItem('adminSettings', onRead);
// <<<< end of executor
});
};
/******************************************************************************/