From 721c85f8b618485552b47165d211a314d3f49cad Mon Sep 17 00:00:00 2001
From: Benoit Lavenier <benoit.lavenier@e-is.pro>
Date: Fri, 6 Mar 2020 12:22:33 +0100
Subject: [PATCH] [fix] Disable cache storage in local store, by default - see
 issue #885 [enh] Settings: add an option to enable/disable local storage of
 cache

---
 www/i18n/locale-en-GB.json           |   2 +
 www/i18n/locale-en.json              |   2 +
 www/i18n/locale-es-ES.json           |   2 +
 www/i18n/locale-fr-FR.json           |   6 +-
 www/js/platform.js                   |  14 +++-
 www/js/services/cache-services.js    | 101 ++++++++++++++++++---------
 www/js/services/http-services.js     |   2 +-
 www/js/services/settings-services.js |   5 ++
 www/js/services/wot-services.js      |   8 +--
 www/templates/settings/settings.html |  20 +++++-
 10 files changed, 117 insertions(+), 45 deletions(-)

diff --git a/www/i18n/locale-en-GB.json b/www/i18n/locale-en-GB.json
index a9e6b89b..7b4f7c56 100644
--- a/www/i18n/locale-en-GB.json
+++ b/www/i18n/locale-en-GB.json
@@ -140,6 +140,8 @@
     "PEER": "Duniter peer address",
     "PEER_SHORT": "Peer address",
     "PEER_CHANGED_TEMPORARY": "Address used temporarily",
+    "PERSIST_CACHE": "Keep navigation data (experimental)",
+    "PERSIST_CACHE_HELP": "Allows faster navigation, locally retaining the data received, for use from one session to another.",
     "USE_LOCAL_STORAGE": "Enable local storage",
     "USE_LOCAL_STORAGE_HELP": "Allows you to save your settings",
     "WALLETS_SETTINGS": "My wallets",
diff --git a/www/i18n/locale-en.json b/www/i18n/locale-en.json
index bfca9588..101eb868 100644
--- a/www/i18n/locale-en.json
+++ b/www/i18n/locale-en.json
@@ -140,6 +140,8 @@
     "PEER": "Duniter peer address",
     "PEER_SHORT": "Peer address",
     "PEER_CHANGED_TEMPORARY": "Address used temporarily",
+    "PERSIST_CACHE": "Keep navigation data (experimental)",
+    "PERSIST_CACHE_HELP": "Allows faster navigation, locally retaining the data received, for use from one session to another.",
     "USE_LOCAL_STORAGE": "Enable local storage",
     "USE_LOCAL_STORAGE_HELP": "Allows you to save your settings",
     "WALLETS_SETTINGS": "My wallets",
diff --git a/www/i18n/locale-es-ES.json b/www/i18n/locale-es-ES.json
index aa1c119b..f57fa6ae 100644
--- a/www/i18n/locale-es-ES.json
+++ b/www/i18n/locale-es-ES.json
@@ -134,6 +134,8 @@
     "NETWORK_SETTINGS": "Red",
     "PEER": "Dirección del nodo Duniter",
     "PEER_CHANGED_TEMPORARY": "Dirección utilizada temporalmente",
+    "PERSIST_CACHE": "Mantener datos de navegación (experimental)",
+    "PERSIST_CACHE_HELP": "Permite una navegación más rápida, conservando localmente los datos recibidos, para usar de una sesión a otra.",
     "USE_LOCAL_STORAGE": "Activar el almacenamiento local",
     "USE_LOCAL_STORAGE_HELP": "Permitir el ahorro de almacenamiento local",
     "WALLETS_SETTINGS": "Mis monederos",
diff --git a/www/i18n/locale-fr-FR.json b/www/i18n/locale-fr-FR.json
index 1f472e58..3ed72166 100644
--- a/www/i18n/locale-fr-FR.json
+++ b/www/i18n/locale-fr-FR.json
@@ -137,9 +137,11 @@
     "DISPLAY_DIVIDER": "Affichage",
     "STORAGE_DIVIDER": "Stockage",
     "NETWORK_SETTINGS": "Réseau",
-    "PEER": "Adresse du nœud Duniter",
-    "PEER_SHORT": "Adresse du nœud",
+    "PEER": "Nœud Duniter",
+    "PEER_SHORT": "Nœud Duniter",
     "PEER_CHANGED_TEMPORARY": "Adresse utilisée temporairement",
+    "PERSIST_CACHE": "Conserver les données de navigation (expérimental)",
+    "PERSIST_CACHE_HELP": "Permet une navigation plus rapide, en conservant localement les données reçues, pour les utiliser d'une session à l'autre.",
     "USE_LOCAL_STORAGE": "Activer le stockage local",
     "USE_LOCAL_STORAGE_HELP": "Permet de sauvegarder vos paramètres",
     "WALLETS_SETTINGS": "Mes portefeuilles",
diff --git a/www/js/platform.js b/www/js/platform.js
index 02c2de56..f2e30f8e 100644
--- a/www/js/platform.js
+++ b/www/js/platform.js
@@ -44,10 +44,20 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
     $animateProvider.classNameFilter( /\banimate-/ );
   })
 
-  // Configure cache (used by HTTP requests) default max age
+  // Configure cache (used by HTTP requests) default options
   .config(function (CacheFactoryProvider, csConfig) {
     'ngInject';
-    angular.extend(CacheFactoryProvider.defaults, { maxAge: csConfig.cacheTimeMs || 60 * 1000 /*1min*/});
+
+    angular.extend(CacheFactoryProvider.defaults, {
+      // Fixed options:
+      recycleFreq: 60 * 1000, // Scan expired items every 1min
+      storagePrefix: 'caches.', // Override storage key prefix
+      capacity: 100, // Force to use a LRU cache, to avoid size exceed max
+
+      // Options overwritten by the csCache service:
+      maxAge: csConfig.cacheTimeMs || 60 * 1000, // from config if exists, or 1min
+      storageMode: 'memory' // Do NOT use local Storage by default
+    });
   })
 
   // Configure screen size detection
diff --git a/www/js/services/cache-services.js b/www/js/services/cache-services.js
index a1e4f8aa..9650fdb2 100644
--- a/www/js/services/cache-services.js
+++ b/www/js/services/cache-services.js
@@ -1,6 +1,6 @@
 angular.module('cesium.cache.services', ['angular-cache'])
 
-.factory('csCache', function($http, $window, csSettings, CacheFactory) {
+.factory('csCache', function($rootScope, $http, $window, csSettings, CacheFactory) {
   'ngInject';
 
   var
@@ -10,54 +10,78 @@ angular.module('cesium.cache.services', ['angular-cache'])
       MEDIUM: 5  * 60 * 1000 /*5 min*/,
       SHORT: csSettings.defaultSettings.cacheTimeMs // around 1min
     },
+    storageMode = getSettingsStorageMode(),
     cacheNames = []
   ;
 
-  function getOrCreateCache(prefix, maxAge, onExpire){
-    prefix = prefix || 'csCache-';
-    maxAge = maxAge || constants.SHORT;
-    var cacheName = prefix + (maxAge / 1000);
+  function getSettingsStorageMode(settings) {
+    settings = settings || csSettings.data;
+    return settings && settings.useLocalStorage && settings.persistCache && $window.localStorage ? 'localStorage' : 'memory';
+  }
 
-    // FIXME: enable this when cache is cleaning on rollback
-    var storageMode = csSettings.data.useLocalStorage && $window.localStorage ? 'localStorage' : 'memory';
+  function fillStorageOptions(options) {
+    options = options || {};
+    options.storageMode = getSettingsStorageMode();
+    options.deleteOnExpire = (options.storageMode === 'localStorage' || options.onExpire) ? 'aggressive' : 'passive';
+    options.cacheFlushInterval = options.deleteOnExpire === 'passive' ?
+      (60 * 60 * 1000) : // If passive mode, remove all items every hour
+      null;
+    return options;
+  }
 
-    if (!onExpire) {
-      if (!cacheNames[cacheName]) {
-        cacheNames[cacheName] = true;
-        console.debug("[cache] Creating cache {0}...".format(cacheName));
+  function onSettingsChanged(settings) {
+    var newStorageMode = getSettingsStorageMode(settings)
+    var hasChanged = (newStorageMode !== storageMode);
+    if (hasChanged) {
+      storageMode = newStorageMode;
+      console.debug("[cache] Updating caches with {storageMode: {0}}".format(storageMode));
+      if (storageMode === 'memory') {
+        clearAllCaches();
       }
-      return CacheFactory.get(cacheName) ||
-        CacheFactory.createCache(cacheName, {
-          maxAge: maxAge,
-          deleteOnExpire: 'passive',
-          //cacheFlushInterval: 60 * 60 * 1000, //  clear itself every hour
-          recycleFreq: Math.max(maxAge - 1000, 5 * 60 * 1000 /*5min*/),
-          storageMode: storageMode
-        });
+      _.forEach(_.keys(cacheNames), function(cacheName) {
+        var cache = CacheFactory.get(cacheName);
+        if (cache) {
+          cache.setOptions({storageMode: storageMode});
+        }
+      });
     }
-    else {
+  }
+
+  function getOrCreateCache(prefix, maxAge, onExpire){
+    prefix = prefix || '';
+    maxAge = maxAge || constants.SHORT;
+    var cacheName = prefix + ((maxAge / 1000) + 's');
+
+    // If onExpire fn, generate a new cache key
+    var cache;
+    if (onExpire && typeof onExpire == 'function') {
       var counter = 1;
-      while(CacheFactory.get(cacheName + counter)) {
+      while (CacheFactory.get(cacheName + counter)) {
         counter++;
       }
       cacheName = cacheName + counter;
-      if (!cacheNames[cacheName]) {
-        cacheNames[cacheName] = true;
-      }
-      console.debug("[cache] Creating cache {0} with 'onExpire' option...".format(cacheName));
-      return CacheFactory.createCache(cacheName, {
-          maxAge: maxAge,
-          deleteOnExpire: 'aggressive',
-          //cacheFlushInterval: 60 * 60 * 1000, // This cache will clear itself every hour
-          recycleFreq: maxAge,
-          onExpire: onExpire,
-          storageMode: storageMode
-        });
     }
+    else {
+      cache = CacheFactory.get(cacheName);
+    }
+
+    // Add to cache names map
+    if (!cacheNames[cacheName]) cacheNames[cacheName] = true;
+
+    // Already exists: use it
+    if (cache) return cache;
+
+    // Not exists yet: create a new cache
+    var options = fillStorageOptions({
+      maxAge: maxAge,
+      onExpire: onExpire || null
+    });
+    console.debug("[cache] Creating cache {{0}} with {storageMode: {1}}...".format(cacheName, options.storageMode));
+    return CacheFactory.createCache(cacheName, options);
   }
 
   function clearAllCaches() {
-    console.debug("[cache] cleaning all caches");
+    console.debug("[cache] Cleaning all caches...");
     _.forEach(_.keys(cacheNames), function(cacheName) {
       var cache = CacheFactory.get(cacheName);
       if (cache) {
@@ -77,6 +101,15 @@ angular.module('cesium.cache.services', ['angular-cache'])
     });
   }
 
+  function addListeners() {
+    listeners = [
+      // Listen if node changed
+      csSettings.api.data.on.changed($rootScope, onSettingsChanged, this)
+    ];
+  }
+
+  addListeners();
+
   return {
     get: getOrCreateCache,
     clear: clearFromPrefix,
diff --git a/www/js/services/http-services.js b/www/js/services/http-services.js
index b332d635..32030a16 100644
--- a/www/js/services/http-services.js
+++ b/www/js/services/http-services.js
@@ -102,7 +102,7 @@ angular.module('cesium.http.services', ['cesium.cache.services'])
         };
         if (autoRefresh) { // redo the request if need
           config.cache = csCache.get(cachePrefix, maxAge, function (key, value, done) {
-              console.debug('[http] Refreshing cache for ['+key+'] ');
+              console.debug('[http] Refreshing cache for {{0}} '.format(key));
               $http.get(key, config)
                 .success(function (data) {
                   config.cache.put(key, data);
diff --git a/www/js/services/settings-services.js b/www/js/services/settings-services.js
index d279f379..c7cf9ad9 100644
--- a/www/js/services/settings-services.js
+++ b/www/js/services/settings-services.js
@@ -69,6 +69,7 @@ angular.module('cesium.settings.services', ['ngApi', 'cesium.config'])
     useRelative: false,
     useLocalStorage: !!$window.localStorage, // override to false if no device
     useLocalStorageEncryption: false,
+    persistCache: false, // disable by default (waiting resolution of issue #885)
     walletHistoryTimeSecond: 30 * 24 * 60 * 60 /*30 days*/,
     walletHistorySliceSecond: 5 * 24 * 60 * 60 /*download using 5 days slice*/,
     walletHistoryAutoRefresh: true, // override to false if device
@@ -300,6 +301,9 @@ angular.module('cesium.settings.services', ['ngApi', 'cesium.config'])
     api.locale.raise.changed(locale);
   },
 
+  isStarted = function() {
+    return started;
+  },
 
   ready = function() {
     if (started) return $q.when();
@@ -341,6 +345,7 @@ angular.module('cesium.settings.services', ['ngApi', 'cesium.config'])
   //start();
 
   return {
+    isStarted: isStarted,
     ready: ready,
     start: start,
     data: data,
diff --git a/www/js/services/wot-services.js b/www/js/services/wot-services.js
index 94b656df..87faa4c2 100644
--- a/www/js/services/wot-services.js
+++ b/www/js/services/wot-services.js
@@ -2,7 +2,7 @@
 angular.module('cesium.wot.services', ['ngApi', 'cesium.bma.services', 'cesium.crypto.services', 'cesium.utils.services',
   'cesium.settings.services'])
 
-.factory('csWot', function($rootScope, $q, $timeout, BMA, Api, CacheFactory, csConfig, csCurrency, csSettings, csCache) {
+.factory('csWot', function($rootScope, $q, $timeout, BMA, Api, CacheFactory, UIUtils, csConfig, csCurrency, csSettings, csCache) {
   'ngInject';
 
 
@@ -681,17 +681,17 @@ angular.module('cesium.wot.services', ['ngApi', 'cesium.bma.services', 'cesium.c
       if (pubkey) {
         data = (options.cache !== false) ? identityCache.get(pubkey) : null;
         if (data && (!uid || data.uid === uid) && (!options.blockUid || data.blockUid === options.blockUid)) {
-          console.debug("[wot] Identity " + pubkey.substring(0, 8) + " found in cache");
+          console.debug("[wot] Identity {{0}} found in cache".format(pubkey.substring(0, 8)));
           return $q.when(data);
         }
-        console.debug("[wot] Loading identity " + pubkey.substring(0, 8) + "...");
+        console.debug("[wot] Loading identity {{0}}...".format(pubkey.substring(0, 8)));
         data = {
           pubkey: pubkey,
           uid: uid
         };
       }
       else {
-        console.debug("[wot] Loading identity from uid " + uid);
+        console.debug("[wot] Loading identity from uid {{0}}...".format(uid));
         data = {
           uid: uid
         };
diff --git a/www/templates/settings/settings.html b/www/templates/settings/settings.html
index b5e93a0a..9c11718c 100644
--- a/www/templates/settings/settings.html
+++ b/www/templates/settings/settings.html
@@ -90,6 +90,20 @@
           </label>
         </div>
 
+        <!-- Persist cache ? -->
+        <div class="item item-text-wrap item-toggle dark hidden-xs hidden-sm">
+          <div class="input-label" ng-class="{'gray': !formData.useLocalStorage}"
+               ng-bind-html="'SETTINGS.PERSIST_CACHE' | translate"></div>
+          <h4 class="gray" ng-bind-html="'SETTINGS.PERSIST_CACHE_HELP' | translate"></h4>
+          <label class="toggle toggle-royal">
+            <input type="checkbox" ng-model="formData.persistCache" ng-if="formData.useLocalStorage">
+            <input type="checkbox" ng-model="formData.useLocalStorage" ng-if="!formData.useLocalStorage" disabled>
+            <div class="track">
+              <div class="handle"></div>
+            </div>
+          </label>
+        </div>
+
         <!-- Allow extension here -->
         <cs-extension-point name="common"></cs-extension-point>
 
@@ -104,7 +118,8 @@
           <h4 class="gray text-wrap" ng-bind-html="'SETTINGS.REMEMBER_ME_HELP' | translate"></h4>
 
           <label class="toggle toggle-royal">
-            <input type="checkbox" ng-model="formData.rememberMe" ng-disabled="!formData.useLocalStorage">
+            <input type="checkbox" ng-model="formData.rememberMe" ng-if="formData.useLocalStorage">
+            <input type="checkbox" ng-model="formData.useLocalStorage" ng-if="!formData.useLocalStorage" disabled>
             <div class="track">
               <div class="handle"></div>
             </div>
@@ -145,7 +160,8 @@
             <h4 class="gray text-wrap" ng-bind-html="'SETTINGS.USE_WALLETS_ENCRYPTION_HELP' | translate">
             </h4>
             <label class="toggle toggle-royal">
-              <input type="checkbox" ng-model="formData.useLocalStorageEncryption" ng-disabled="!formData.useLocalStorage">
+              <input type="checkbox" ng-model="formData.useLocalStorageEncryption" ng-if="formData.useLocalStorage">
+              <input type="checkbox" ng-model="formData.useLocalStorage" ng-if="!formData.useLocalStorage" disabled>
               <div class="track">
                 <div class="handle"></div>
               </div>
-- 
GitLab