From 2ac288397ca599692a4450bfdf511453bfa03bb7 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Fri, 21 Feb 2020 15:34:54 -0500 Subject: [PATCH] Remove usage of synchronous localStorage API Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/899 browser.storage.local is now used to store non-critical local settings. These settings are all collated under the key `localStorage`, and vAPI.localStorage is an API to handle access to these values stored under this key. vAPI.localStorage.getItem() is still synchronous but its purpose is to return internally cached values -- this minimizes code changes throughout uBO. --- platform/chromium/vapi-common.js | 94 +++++++++++++++++++++++--------- src/js/1p-filters.js | 48 +++++++--------- src/js/3p-filters.js | 25 ++------- src/js/dashboard.js | 8 +-- src/js/document-blocked.js | 10 ++-- src/js/logger-ui.js | 6 +- src/js/popup-fenix.js | 36 ++++++------ src/js/popup.js | 36 ++++++------ src/js/start.js | 4 -- 9 files changed, 144 insertions(+), 123 deletions(-) diff --git a/platform/chromium/vapi-common.js b/platform/chromium/vapi-common.js index a791aebd1..805467f01 100644 --- a/platform/chromium/vapi-common.js +++ b/platform/chromium/vapi-common.js @@ -218,48 +218,92 @@ vAPI.closePopup = function() { // background page or auxiliary pages. // This storage is optional, but it is nice to have, for a more polished user // experience. - +// // https://github.com/gorhill/uBlock/issues/2824 // Use a dummy localStorage if for some reasons it's not available. - +// // https://github.com/gorhill/uMatrix/issues/840 // Always use a wrapper to seamlessly handle exceptions +// +// https://github.com/uBlockOrigin/uBlock-issues/issues/899 +// Convert into asynchronous access API. vAPI.localStorage = { - started: false, start: function() { - this.started = true; + if ( this.cache instanceof Object ) { return Promise.resolve(); } + if ( this.promise !== undefined ) { return this.promise; } + this.promise = new Promise(resolve => { + browser.storage.local.get('localStorage', bin => { + if ( + bin instanceof Object === false || + bin.localStorage instanceof Object === false + ) { + this.cache = {}; + const ls = self.localStorage; + for ( let i = 0; i < ls.length; i++ ) { + const key = ls.key(i); + this.cache[key] = ls.getItem(key); + } + //ls.clear(); + browser.storage.local.set({ localStorage: this.cache }); + } else { + try { + this.cache = bin.localStorage; + } catch(ex) { + } + } + if ( this.cache instanceof Object === false ) { + this.cache = {}; + } + this.promise = undefined; + browser.storage.onChanged.addListener((changes, area) => { + if ( + area !== 'local' || + changes instanceof Object === false || + changes.localStorage instanceof Object === false + ) { + return; + } + const newValue = changes.localStorage.newValue; + this.cache = newValue instanceof Object ? newValue : {}; + }); + resolve(); + }); + }); + return this.promise; }, clear: function() { - try { - window.localStorage.clear(); - } catch(ex) { - } + this.cache = {}; + return browser.storage.local.set({ localStorage: this.cache }); }, getItem: function(key) { - if ( this.started === false ) { return null; } - try { - return window.localStorage.getItem(key); - } catch(ex) { + if ( this.cache instanceof Object === false ) { + console.info(`localStorage.getItem('${key}') not ready`); + return null; } - return null; + const value = this.cache[key]; + return value !== undefined ? value : null; + }, + getItemAsync: function(key) { + return this.start().then(( ) => { + const value = this.cache[key]; + return value !== undefined ? value : null; + }); }, removeItem: function(key) { - try { - window.localStorage.removeItem(key); - } catch(ex) { - } + this.setItem(key); }, - setItem: function(key, value) { - if ( this.started === false ) { return; } - try { - window.localStorage.setItem(key, value); - } catch(ex) { - } - } + setItem: function(key, value = undefined) { + return this.start().then(( ) => { + this.cache[key] = value; + return browser.storage.local.set({ localStorage: this.cache }); + }); + }, + promise: undefined, + cache: undefined, }; - +vAPI.localStorage.start(); diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js index 8064f1ac0..1cc6cc3c6 100644 --- a/src/js/1p-filters.js +++ b/src/js/1p-filters.js @@ -45,20 +45,6 @@ let cachedUserFilters = ''; /******************************************************************************/ -// https://github.com/gorhill/uBlock/issues/3706 -// Save/restore cursor position -// -// CoreMirror reference: https://codemirror.net/doc/manual.html#api_selection - -window.addEventListener('beforeunload', ( ) => { - vAPI.localStorage.setItem( - 'myFiltersCursorPosition', - JSON.stringify(cmEditor.getCursor().line) - ); -}); - -/******************************************************************************/ - // This is to give a visual hint that the content of user blacklist has changed. const userFiltersChanged = function(changed) { @@ -71,7 +57,7 @@ const userFiltersChanged = function(changed) { /******************************************************************************/ -const renderUserFilters = async function(first) { +const renderUserFilters = async function() { const details = await vAPI.messaging.send('dashboard', { what: 'readUserFilters', }); @@ -83,18 +69,7 @@ const renderUserFilters = async function(first) { content += '\n'; } cmEditor.setValue(content); - if ( first ) { - cmEditor.clearHistory(); - try { - const line = JSON.parse( - vAPI.localStorage.getItem('myFiltersCursorPosition') - ); - if ( typeof line === 'number' ) { - cmEditor.setCursor(line, 0); - } - } catch(ex) { - } - } + userFiltersChanged(false); }; @@ -224,7 +199,24 @@ uDom('#exportUserFiltersToFile').on('click', exportUserFiltersToFile); uDom('#userFiltersApply').on('click', ( ) => { applyChanges(); }); uDom('#userFiltersRevert').on('click', revertChanges); -renderUserFilters(true); +// https://github.com/gorhill/uBlock/issues/3706 +// Save/restore cursor position +// +// CoreMirror reference: https://codemirror.net/doc/manual.html#api_selection +renderUserFilters().then(( ) => { + cmEditor.clearHistory(); + return vAPI.localStorage.getItemAsync('myFiltersCursorPosition'); +}).then(line => { + if ( typeof line === 'number' ) { + cmEditor.setCursor(line, 0); + } + cmEditor.on('cursorActivity', ( ) => { + const line = cmEditor.getCursor().line; + if ( vAPI.localStorage.getItem('myFiltersCursorPosition') !== line ) { + vAPI.localStorage.setItem('myFiltersCursorPosition', line); + } + }); +}); cmEditor.on('changes', userFiltersChanged); CodeMirror.commands.save = applyChanges; diff --git a/src/js/3p-filters.js b/src/js/3p-filters.js index 31c80273c..ae23ed2d7 100644 --- a/src/js/3p-filters.js +++ b/src/js/3p-filters.js @@ -34,7 +34,7 @@ const reValidExternalList = /[a-z-]+:\/\/\S*\/\S+/; let listDetails = {}; let filteringSettingsHash = ''; -let hideUnusedSet = new Set(); +let hideUnusedSet = new Set([ '*' ]); /******************************************************************************/ @@ -258,10 +258,6 @@ const renderFilterLists = function(soft) { let groupKey = groupKeys[i]; let liGroup = liFromListGroup(groupKey, groups.get(groupKey)); liGroup.setAttribute('data-groupkey', groupKey); - liGroup.classList.toggle( - 'collapsed', - vAPI.localStorage.getItem('collapseGroup' + (i + 1)) === 'y' - ); if ( liGroup.parentElement === null ) { ulLists.appendChild(liGroup); } @@ -548,7 +544,7 @@ const toggleHideUnusedLists = function(which) { .toggleClass('unused', mustHide); vAPI.localStorage.setItem( 'hideUnusedFilterLists', - JSON.stringify(Array.from(hideUnusedSet)) + Array.from(hideUnusedSet) ); }; @@ -571,20 +567,11 @@ uDom('#lists').on('click', '.groupEntry[data-groupkey] > .geDetails', function(e }); // Initialize from saved state. -{ - let aa; - try { - const json = vAPI.localStorage.getItem('hideUnusedFilterLists'); - if ( json !== null ) { - aa = JSON.parse(json); - } - } catch (ex) { +vAPI.localStorage.getItemAsync('hideUnusedFilterLists').then(value => { + if ( Array.isArray(value) ) { + hideUnusedSet = new Set(value); } - if ( Array.isArray(aa) === false ) { - aa = [ '*' ]; - } - hideUnusedSet = new Set(aa); -} +}); /******************************************************************************/ diff --git a/src/js/dashboard.js b/src/js/dashboard.js index b407c498c..2323f8c05 100644 --- a/src/js/dashboard.js +++ b/src/js/dashboard.js @@ -84,10 +84,10 @@ const discardUnsavedData = function(synchronous = false) { const loadDashboardPanel = function(pane = '') { if ( pane === '' ) { - pane = vAPI.localStorage.getItem('dashboardLastVisitedPane'); - if ( pane === null ) { - pane = 'settings.html'; - } + vAPI.localStorage.getItemAsync('dashboardLastVisitedPane').then(value => { + loadDashboardPanel(value !== null ? value : 'settings.html'); + }); + return; } const tabButton = uDom(`[href="#${pane}"]`); if ( !tabButton || tabButton.hasClass('selected') ) { return; } diff --git a/src/js/document-blocked.js b/src/js/document-blocked.js index 746e03557..111e6a165 100644 --- a/src/js/document-blocked.js +++ b/src/js/document-blocked.js @@ -194,10 +194,12 @@ uDom.nodeFromId('why').textContent = details.fs; ); }); - uDom.nodeFromId('theURL').classList.toggle( - 'collapsed', - vAPI.localStorage.getItem('document-blocked-expand-url') !== 'true' - ); + vAPI.localStorage.getItemAsync('document-blocked-expand-url').then(value => { + uDom.nodeFromId('theURL').classList.toggle( + 'collapsed', + value !== 'true' && value !== true + ); + }); })(); /******************************************************************************/ diff --git a/src/js/logger-ui.js b/src/js/logger-ui.js index 9f98c98cf..8424f51d9 100644 --- a/src/js/logger-ui.js +++ b/src/js/logger-ui.js @@ -2675,9 +2675,9 @@ const loggerSettings = (( ) => { linesPerEntry: 4, }; - { + vAPI.localStorage.getItemAsync('loggerSettings').then(value => { try { - const stored = JSON.parse(vAPI.localStorage.getItem('loggerSettings')); + const stored = JSON.parse(value); if ( typeof stored.discard.maxAge === 'number' ) { settings.discard.maxAge = stored.discard.maxAge; } @@ -2695,7 +2695,7 @@ const loggerSettings = (( ) => { } } catch(ex) { } - } + }); const valueFromInput = function(input, def) { let value = parseInt(input.value, 10); diff --git a/src/js/popup-fenix.js b/src/js/popup-fenix.js index 6d503bf91..81d4cb7d2 100644 --- a/src/js/popup-fenix.js +++ b/src/js/popup-fenix.js @@ -30,19 +30,23 @@ /******************************************************************************/ -let popupFontSize = vAPI.localStorage.getItem('popupFontSize'); -if ( typeof popupFontSize === 'string' && popupFontSize !== 'unset' ) { - document.body.style.setProperty('font-size', popupFontSize); -} +let popupFontSize; +vAPI.localStorage.getItemAsync('popupFontSize').then(value => { + if ( typeof value !== 'string' || value === 'unset' ) { return; } + document.body.style.setProperty('font-size', value); + popupFontSize = value; +}); // https://github.com/chrisaljoudi/uBlock/issues/996 -// Experimental: mitigate glitchy popup UI: immediately set the firewall -// pane visibility to its last known state. By default the pane is hidden. -let dfPaneVisibleStored = - vAPI.localStorage.getItem('popupFirewallPane') === 'true'; -if ( dfPaneVisibleStored ) { - document.getElementById('main').classList.add('dfEnabled'); -} +// Experimental: mitigate glitchy popup UI: immediately set the firewall +// pane visibility to its last known state. By default the pane is hidden. +let dfPaneVisibleStored; +vAPI.localStorage.getItemAsync('popupFirewallPane').then(value => { + dfPaneVisibleStored = value === true || value === 'true'; + if ( dfPaneVisibleStored ) { + document.getElementById('panes').classList.add('dfEnabled'); + } +}); /******************************************************************************/ @@ -838,11 +842,8 @@ document.addEventListener( const expandExceptions = new Set(); -(( ) => { +vAPI.localStorage.getItemAsync('popupExpandExceptions').then(exceptions => { try { - const exceptions = JSON.parse( - vAPI.localStorage.getItem('popupExpandExceptions') - ); if ( Array.isArray(exceptions) === false ) { return; } for ( const exception of exceptions ) { expandExceptions.add(exception); @@ -850,13 +851,12 @@ const expandExceptions = new Set(); } catch(ex) { } - -})(); +}); const saveExpandExceptions = function() { vAPI.localStorage.setItem( 'popupExpandExceptions', - JSON.stringify(Array.from(expandExceptions)) + Array.from(expandExceptions) ); }; diff --git a/src/js/popup.js b/src/js/popup.js index 34690a557..d336ab166 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -29,10 +29,12 @@ /******************************************************************************/ -let popupFontSize = vAPI.localStorage.getItem('popupFontSize'); -if ( typeof popupFontSize === 'string' && popupFontSize !== 'unset' ) { - document.body.style.setProperty('font-size', popupFontSize); -} +let popupFontSize; +vAPI.localStorage.getItemAsync('popupFontSize').then(value => { + if ( typeof value !== 'string' || value === 'unset' ) { return; } + document.body.style.setProperty('font-size', value); + popupFontSize = value; +}); // https://github.com/gorhill/uBlock/issues/3032 // Popup panel can be in one of two modes: @@ -48,13 +50,15 @@ if ( } // https://github.com/chrisaljoudi/uBlock/issues/996 -// Experimental: mitigate glitchy popup UI: immediately set the firewall -// pane visibility to its last known state. By default the pane is hidden. -let dfPaneVisibleStored = - vAPI.localStorage.getItem('popupFirewallPane') === 'true'; -if ( dfPaneVisibleStored ) { - document.getElementById('panes').classList.add('dfEnabled'); -} +// Experimental: mitigate glitchy popup UI: immediately set the firewall +// pane visibility to its last known state. By default the pane is hidden. +let dfPaneVisibleStored; +vAPI.localStorage.getItemAsync('popupFirewallPane').then(value => { + dfPaneVisibleStored = value === true || value === 'true'; + if ( dfPaneVisibleStored ) { + document.getElementById('panes').classList.add('dfEnabled'); + } +}); /******************************************************************************/ @@ -931,11 +935,8 @@ document.addEventListener( const expandExceptions = new Set(); -(( ) => { +vAPI.localStorage.getItemAsync('popupExpandExceptions').then(exceptions => { try { - const exceptions = JSON.parse( - vAPI.localStorage.getItem('popupExpandExceptions') - ); if ( Array.isArray(exceptions) === false ) { return; } for ( const exception of exceptions ) { expandExceptions.add(exception); @@ -943,13 +944,12 @@ const expandExceptions = new Set(); } catch(ex) { } - -})(); +}); const saveExpandExceptions = function() { vAPI.localStorage.setItem( 'popupExpandExceptions', - JSON.stringify(Array.from(expandExceptions)) + Array.from(expandExceptions) ); }; diff --git a/src/js/start.js b/src/js/start.js index b6f3be446..d623bfe06 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -316,10 +316,6 @@ if ( selfieIsValid !== true ) { // Start network observers. µb.webRequest.start(); -// https://github.com/uBlockOrigin/uBlock-issues/issues/899 -// Signal that localStorage can be used now that uBO is ready. -vAPI.localStorage.start(); - // Ensure that the resources allocated for decompression purpose (likely // large buffers) are garbage-collectable immediately after launch. // Otherwise I have observed that it may take quite a while before the