diff --git a/app/config.json b/app/config.json index 216de473fe58cdd5ad2c663167750d7511bcdb64..af183ae1715c9c8fe04ceaf42b3da88266c0101a 100644 --- a/app/config.json +++ b/app/config.json @@ -9,7 +9,7 @@ "timeout": 30000, "timeWarningExpireMembership": 5184000, "timeWarningExpire": 7776000, - "minPeerCountAtStartup": 10, + "minConsensusPeerCount": 10, "keepAuthIdle": 600, "useLocalStorage": true, "useRelative": false, @@ -138,7 +138,7 @@ "timeout": 30000, "timeWarningExpireMembership": 5184000, "timeWarningExpire": 7776000, - "minPeerCountAtStartup": 2, + "minConsensusPeerCount": -1, "useLocalStorage": true, "useRelative": false, "expertMode": true, diff --git a/www/js/config-test.js b/www/js/config-test.js index 8576b66a435d531c7ccf20cf7b4c40c8293363f0..d8aa2c45957c43b449133e60353d8bbe1db4afb4 100644 --- a/www/js/config-test.js +++ b/www/js/config-test.js @@ -18,7 +18,7 @@ angular.module("cesium.config", []) "timeout": 30000, "timeWarningExpireMembership": 5184000, "timeWarningExpire": 7776000, - "minPeerCountAtStartup": 2, + "minConsensusPeerCount": 2, "useLocalStorage": true, "useRelative": false, "expertMode": true, @@ -80,9 +80,9 @@ angular.module("cesium.config", []) "defaultCountry": "France" } }, - "version": "1.7.7", - "build": "2023-08-17T13:11:36.358Z", + "version": "1.7.8", + "build": "2023-08-17T17:00:38.095Z", "newIssueUrl": "https://git.duniter.org/clients/cesium-grp/cesium/issues/new" }) -; \ No newline at end of file +; diff --git a/www/js/config.js b/www/js/config.js index 97b7d307c1204a8cb4f7429bb723dbe6675ad08a..c4b79c10ba1f8ca486387b1045e7831f444e809a 100644 --- a/www/js/config.js +++ b/www/js/config.js @@ -18,7 +18,7 @@ angular.module("cesium.config", []) "timeout": 30000, "timeWarningExpireMembership": 5184000, "timeWarningExpire": 7776000, - "minPeerCountAtStartup": 10, + "minConsensusPeerCount": 10, "keepAuthIdle": 600, "useLocalStorage": true, "useRelative": false, @@ -152,4 +152,4 @@ angular.module("cesium.config", []) "newIssueUrl": "https://git.duniter.org/clients/cesium-grp/cesium/issues/new" }) -; \ No newline at end of file +; diff --git a/www/js/controllers/app-controllers.js b/www/js/controllers/app-controllers.js index 2a58c669214f50b617d492cb8d4322974cf8ab58..87508125545f1ba216396cac2ebb1ccc88a1f638 100644 --- a/www/js/controllers/app-controllers.js +++ b/www/js/controllers/app-controllers.js @@ -403,8 +403,11 @@ function AppController($scope, $rootScope, $state, $ionicSideMenuDelegate, $q, $ console.info('[app] Trying to parse as uri: ', uri); var fromHomeState = $state.current && $state.current.name === 'app.home'; - // Parse the URI - return BMA.uri.parse(uri) + return (!csPlatform.isStarted() ? csPlatform.ready() : $q.when()) + // Parse the URI + .then(function() { + return BMA.uri.parse(uri); + }) .then(function(res) { if (!res) throw {message: 'ERROR.UNKNOWN_URI_FORMAT'}; // Continue @@ -469,7 +472,7 @@ function AppController($scope, $rootScope, $state, $ionicSideMenuDelegate, $q, $ reject(err); return; } - console.error("[home] Error while handle uri {" + uri + "': ", err); + console.error("[home] Error while handle uri '{0}'".format(uri), JSON.stringify(err)); return UIUtils.onError(uri)(err); }); }; diff --git a/www/js/controllers/network-controllers.js b/www/js/controllers/network-controllers.js index df86a0141c37f46e45c3d4ce990551e4269f090e..3ec5405591c3c875c95962481eff5c7cbd4eeec1 100644 --- a/www/js/controllers/network-controllers.js +++ b/www/js/controllers/network-controllers.js @@ -48,7 +48,7 @@ angular.module('cesium.network.controllers', ['cesium.services']) ; function NetworkLookupController($scope, $state, $location, $ionicPopover, $window, $translate, - BMA, UIUtils, csConfig, csSettings, csCurrency, csNetwork, csWot) { + BMA, Device, UIUtils, csConfig, csSettings, csCurrency, csNetwork, csWot) { 'ngInject'; $scope.networkStarted = false; @@ -149,7 +149,8 @@ function NetworkLookupController($scope, $state, $location, $ionicPopover, $win asc : $scope.search.asc }, expertMode: $scope.expertMode, - timeout: angular.isDefined($scope.timeout) ? $scope.timeout : Device.network.timeout() + timeout: angular.isDefined($scope.timeout) ? $scope.timeout : Device.network.timeout(), + withSandboxes: $scope.expertMode && Device.isDesktop() // SKip sandboxes if mobile }; return options; }; diff --git a/www/js/platform.js b/www/js/platform.js index b31f7a23bb65b71574d7e7a3a8cd243885a9091e..89d3d76794cc2f601dc569daa7915e77bbce30b1 100644 --- a/www/js/platform.js +++ b/www/js/platform.js @@ -194,19 +194,19 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] if (!alive) return false; var now = Date.now(); - console.info("[platform] Checking peer [{0}] is well synchronized...".format(BMA.server)); + console.info('[platform] Checking peer [{0}] is well synchronized...'.format(BMA.server)); api.start.raise.message('NETWORK.INFO.ANALYZING_NETWORK'); var askUserConfirmation = csSettings.data.expertMode; - var minConsensusPeerCount = csSettings.data.minPeerCountAtStartup || -1; + var minConsensusPeerCount = csSettings.data.minConsensusPeerCount || -1; - return csNetwork.getSynchronizedBmaPeers(BMA) + return csNetwork.getSynchronizedBmaPeers(BMA, {autoRefresh: false}) .then(function(peers) { var consensusBlockNumber = peers.length ? peers[0].currentNumber : undefined; var consensusPeerCount = peers.length; - // Not enough synchronized peers found (e.g. an isolated peer). Should never occur. + // Not enough peers on main consensus (e.g. an isolated peer). Should never occur. if (!consensusPeerCount || (minConsensusPeerCount > 0 && consensusPeerCount < minConsensusPeerCount)) { console.warn("[platform] Not enough BMA peers on the main consensus block: {0} found. Will peek another peer...".format(consensusPeerCount)); // Retry using another fallback peer @@ -214,26 +214,25 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services'] .then(checkBmaNodeSynchronized); // Loop } - // Filter compatible peers - peers = peers && peers.reduce(function(res, peer) { + // Filter on compatible peers + peers = peers.reduce(function(res, peer) { if (!peer.compatible) return res; // Serialize to JSON, then append return res.concat(peer.toJSON()); }, []); - console.info("[platform] Found {0}/{1} BMA peers, synchronized and compatible, in {2}ms".format(peers.length, consensusPeerCount, Date.now() - now)); + console.info("[platform] Keep {0}/{1} BMA peers, synchronized and compatible, in {2}ms".format(peers.length, consensusPeerCount, Date.now() - now)); // Try to find the current peer in synchronized peers var synchronized = false; - peers = peers && _.filter(peers, function(peer) { + peers = _.filter(peers, function(peer) { if (BMA.url !== peer.url) return true; synchronized = true; return false; }); - // Saving other peers to settings - console.debug("[platform] Saving {0} BMA peers in settings, for a later use".format(peers.length)); - csSettings.data.network.peers = peers; + // Saving others peers to settings + csSettings.savePeers(peers); // OK (current BMA node is sync and compatible): continue if (synchronized) { diff --git a/www/js/services/bma-services.js b/www/js/services/bma-services.js index 566fee59b763eb38e7e3145d85ac1d11116eeb94..a09fc57b42e0ff9976c2a2ecb6613f63d6e69d87 100644 --- a/www/js/services/bma-services.js +++ b/www/js/services/bma-services.js @@ -1118,15 +1118,21 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium. var matches = exports.regexp.PUBKEY_WITH_CHECKSUM.exec(result.pubkey); pubkey = matches[1]; var checksum = matches[2]; - var expectedChecksum = csCrypto.util.pkChecksum(pubkey); - if (checksum !== expectedChecksum) { - console.warn("[BMA.parse] Detecting a pubkey {{0}} with checksum {{1}}, but expecting checksum is {{2}}".format( - pubkey, checksum, expectedChecksum - )); - throw {message: 'ERROR.PUBKEY_INVALID_CHECKSUM'}; - } - result.pubkey = pubkey; - result.pubkeyChecksum = checksum; + return csCrypto.ready() + .then(function() { + return csCrypto.util.pkChecksum(pubkey); + }) + .then(function(expectedChecksum){ + if (checksum !== expectedChecksum) { + console.warn("[BMA.parse] Detecting a pubkey {{0}} with checksum {{1}}, but expecting checksum is {{2}}".format( + pubkey, checksum, expectedChecksum + )); + throw {message: 'ERROR.PUBKEY_INVALID_CHECKSUM'}; + } + result.pubkey = pubkey; + result.pubkeyChecksum = checksum; + return result; + }); } return result; }); @@ -1221,7 +1227,7 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium. url: csHttp.getUrl(host, port, path, useSsl), node: { summary: csHttp.getWithCache(host, port, path + '/node/summary', useSsl, csCache.constants.MEDIUM, false/*autoRefresh*/, timeout), - sandboxes: csHttp.get(host, port, path + '/node/sandboxes', useSsl, Math.max(1000, timeout)), // sandboxes request can be long + sandboxes: csHttp.get(host, port, path + '/node/sandboxes', useSsl, timeout ? Math.max(timeout * 2, 3000) : undefined), // /!\ sandboxes request can be long }, network: { peering: { diff --git a/www/js/services/device-services.js b/www/js/services/device-services.js index ab1536ba3cc16dc880053cc66376ca2db96b0652..25d1b13ed055d586cff190ce975120769f9dee5a 100644 --- a/www/js/services/device-services.js +++ b/www/js/services/device-services.js @@ -150,45 +150,61 @@ angular.module('cesium.device.services', ['cesium.utils.services', 'cesium.setti } }; exports.network = { + constants: { + NONE: 'none', + CELL: 'cellular', + CELL_2G: '2g', + CELL_3G: '3g', + CELL_4G: '4g', + CELL_5G: '5g', + ETHERNET: 'ethernet', + WIFI: 'wifi', + UNKOWN: 'unknown', + }, connectionType: function () { + var type; try { // If mobile: use the Cordova network plugin if (exports.network.enable) { - return navigator.connection.type || 'unknown'; + type = navigator.connection.type || Connection.CELL_2G; + console.debug('[device] Network plugin connection type: {0}'.format(type)); + return type; } // Continue using browser API if (navigator.onLine === false) { - return 'none'; + return exports.network.constants.NONE; } var connection = navigator.connection; - var connectionType = connection && connection.effectiveType || 'unknown'; + type = connection && connection.effectiveType || exports.network.constants.UNKNOWN; // Si la vitesse de liaison descendante est de 0 mais que le type est '4g', cela signifie probablement que nous sommes hors ligne if (connection && connection.downlink === 0) { //console.debug('[device] Navigator connection type: none (downlink=0)'); - return 'none'; + return exports.network.constants.NONE; } else { - //console.debug('[device] Navigator connection type: ' + connectionType); - switch (connectionType) { + //console.debug('[device] Navigator connection type: ' + type); + switch (type) { case 'slow-2g': case '2g': - return 'cell_2g'; + return exports.network.constants.CELL_2G; case '3g': - return 'cell_3g'; + return exports.network.constants.CELL_3G; case '4g': - if (exports.isDesktop()) return 'ethernet'; - return 'cell_4g'; + if (exports.isDesktop()) return exports.network.constants.ETHERNET; + return exports.network.constants.CELL_4G; case '5g': - if (exports.isDesktop()) return 'ethernet'; - return 'cell_5g'; + if (exports.isDesktop()) return exports.network.constants.ETHERNET; + return exports.network.constants.CELL_5G; case 'unknown': - if (exports.isDesktop()) return 'ethernet'; - return 'unknown'; + if (exports.isDesktop()) return exports.network.constants.ETHERNET; + return exports.network.constants.UNKNOWN; + case 'none': + return exports.network.constants.NONE; default: - return connectionType; + return type; } } @@ -228,30 +244,25 @@ angular.module('cesium.device.services', ['cesium.utils.services', 'cesium.setti var connectionType = exports.network.connectionType(); switch (connectionType) { - case 'ethernet': - timeout = 1000; // 1 s - break; - case 'wifi': - timeout = 2000; - break; - case 'cell_5g': - timeout = 3000; - break; - case 'cell': // (e.g. iOS) - case 'cell_4g': + case exports.network.constants.ETHERNET: + case exports.network.constants.WIFI: + case exports.network.constants.CELL: // (e.g. iOS) + case exports.network.constants.CELL_4G: + case exports.network.constants.CELL_5G: timeout = 4000; // 4s break; - case 'cell_3g': + case exports.network.constants.CELL_3G: timeout = 5000; // 5s break; - case 'cell_2g': + case exports.network.constants.CELL_2G: timeout = 10000; // 10s break; - case 'none': + case exports.network.constants.NONE: timeout = 0; break; - case 'unknown': + case exports.network.constants.UNKNOWN: default: + console.warn('[device] Fallback to default timeout ({0}ms) - connectionType: {1}'.format(defaultTimeout, connectionType)); timeout = defaultTimeout; break; } @@ -452,7 +463,7 @@ angular.module('cesium.device.services', ['cesium.utils.services', 'cesium.setti var paths = (modelPath || '').split('.'); var property = paths.length && paths[paths.length - 1]; paths.reduce(function (res, path) { - if (path == property) { + if (path === property) { res[property] = value; return; } @@ -518,7 +529,6 @@ angular.module('cesium.device.services', ['cesium.utils.services', 'cesium.setti exports.isDesktop = function () { if (!angular.isDefined(cache.isDesktop)) { try { - cache.isDesktop = !exports.enable && ( exports.isUbuntu() || exports.isWindows() || @@ -581,22 +591,45 @@ angular.module('cesium.device.services', ['cesium.utils.services', 'cesium.setti window.open = cordova.InAppBrowser.open; } - // Add network listeners + // Add network listeners, using cordova network plugin if (exports.network.enable) { + // Override constants, because it depends on OS version + exports.network.constants.NONE = Connection.NONE; + exports.network.constants.CELL = Connection.CELL; + exports.network.constants.CELL_2G = Connection.CELL_2G; + exports.network.constants.CELL_3G = Connection.CELL_3G; + exports.network.constants.CELL_4G = Connection.CELL_4G; + exports.network.constants.WIFI = Connection.WIFI; + exports.network.constants.ETHERNET = Connection.ETHERNET; + exports.network.constants.UNKNOWN = Connection.UNKNOWN; + + + var previousConnectionType; document.addEventListener('offline', function () { console.info('[device] Network is offline'); api.network.raise.offline(); + previousConnectionType = 'none'; }, false); document.addEventListener('online', function () { console.info('[device] Network is online'); - api.network.raise.online(); + if (!previousConnectionType || previousConnectionType === 'none') { + api.network.raise.online(); + previousConnectionType = exports.network.connectionType(); + } + else { + var connectionType = exports.network.connectionType(); + if (connectionType !== previousConnectionType) { + console.info('[device] Network connection type changed: ' + connectionType); + api.network.raise.changed(connectionType); + } + } }, false); } } else { console.debug('[device] Ionic platform ready - no device detected.'); - // Add network listeners + // Add network listeners, using browser events window.addEventListener('offline', function () { console.info('[device] Network is offline'); api.network.raise.offline(); diff --git a/www/js/services/network-services.js b/www/js/services/network-services.js index 501dba84cbe508f18d4efd8dec173a227afaa9dd..b440e660ab126db98dbfd05ccc1d034500eb5e3d 100644 --- a/www/js/services/network-services.js +++ b/www/js/services/network-services.js @@ -43,8 +43,11 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', searchingPeersOnNetwork: false, difficulties: null, ws2pHeads: null, - timeout: csConfig.timeout, - startTime: null + timeout: csConfig.timeout, // Max timeout + startTime: null, + autoRefresh: true, + flushIntervalMs: 1000, + withSandboxes: null }, // Return the block uid @@ -91,14 +94,16 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', data.searchingPeersOnNetwork = false; data.difficulties = null; data.ws2pHeads = null; - data.timeout = data.timeout || getDefaultTimeout(); + data.timeout = data.timeout || getDeviceTimeout(); // Keep it is already set data.startTime = null; + data.autoRefresh = true; + data.flushIntervalMs = null; }, /** * Compute a timeout, depending on connection type (wifi, ethernet, cell, etc.) */ - getDefaultTimeout = function () { + getDeviceTimeout = function () { // Using timeout from settings if (csSettings.data.expertMode && csSettings.data.timeout > 0) { console.debug('[network] Using user defined timeout: {0}ms'.format(csSettings.data.timeout)); @@ -148,7 +153,7 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', if (err && err.ucode === BMA.errorCodes.HTTP_LIMITATION) { return $timeout(function() { if (remainingTime() > 0) return loadW2spHeads(); - }, 3000); + }, BMA.constants.LIMIT_REQUEST_DELAY); } console.error(err); // can occur on duniter v1.6 data.ws2pHeads = {}; @@ -166,10 +171,10 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', }) .catch(function(err) { // When too many request, retry in 3s - if (err && err.ucode == BMA.errorCodes.HTTP_LIMITATION) { + if (err && err.ucode === BMA.errorCodes.HTTP_LIMITATION) { return $timeout(function() { if (remainingTime() > 0) return loadDifficulties(); - }, 3000); + }, BMA.constants.LIMIT_REQUEST_DELAY); } console.error(err); data.difficulties = {}; @@ -201,8 +206,9 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', sortPeers(true/*update main buid*/); console.debug('[network] [#{0}] {1} peer(s) found.'.format(pid, data.peers.length)); + } - }, 1000); + }, data.flushIntervalMs || 1000); var initJobs = [ // Load uids @@ -545,7 +551,7 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', return $q.when(peer); } - var timeout = Math.max(data.timeout / 2, remainingTime()); // >= 500ms + var timeout = Math.max(1000, remainingTime()); // >= 500ms peer.api = peer.api || BMA.lightInstance(peer.getHost(), peer.getPort(), peer.getPath(), peer.isSsl(), timeout); // Get current block @@ -625,7 +631,7 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', } // Get sandboxes - if (data.expertMode) { + if (data.expertMode && data.withSandboxes !== false) { jobs.push(peer.api.node.sandboxes() .catch(function(err) { return {}; // continue @@ -665,18 +671,21 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', flushNewPeersAndSort = function(newPeers, updateMainBuid) { newPeers = newPeers || data.newPeers; if (!newPeers.length) return; - var ids = _.map(data.peers, function(peer){ - return peer.id; - }); + var ids = _.pluck(data.peers, 'id'); var hasUpdates = false; var newPeersAdded = 0; _.forEach(newPeers.splice(0), function(peer) { - if (!ids[peer.id]) { + if (!ids.includes(peer.id)) { data.peers.push(peer); - ids[peer.id] = peer; + ids.push(peer.id); hasUpdates = true; newPeersAdded++; } + else { + // Skip if exists + //data.peers[ids.indexOf(peer.id)] = peer; + //hasUpdates = true; + } }); if (hasUpdates) { console.debug('[network] Flushing {0} new peers...'.format(newPeersAdded)); @@ -873,8 +882,11 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', data.filter = options.filter ? angular.merge(data.filter, options.filter) : data.filter; data.sort = options.sort ? angular.merge(data.sort, options.sort) : data.sort; data.expertMode = angular.isDefined(options.expertMode) ? options.expertMode : data.expertMode; - data.timeout = angular.isDefined(options.timeout) ? options.timeout : getDefaultTimeout(); + data.timeout = angular.isDefined(options.timeout) ? options.timeout : getDeviceTimeout(); // Fore recompute data.startTime = Date.now(); + data.autoRefresh = angular.isDefined(options.autoRefresh) ? data.autoRefresh : true; + data.flushIntervalMs = angular.isDefined(options.flushIntervalMs) ? options.flushIntervalMs : 1000; + data.withSandboxes = angular.isDefined(options.withSandboxes) ? options.withSandboxes : true; // Init a min block number var mainBlockNumber = data.mainBlock && buidBlockNumber(data.mainBlock.buid); @@ -892,9 +904,10 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', }) .then(function() { var now = Date.now(); - console.info('[network] [#{0}] Starting from [{1}{2}] {ssl: {3}}'.format(pid, bma.server, bma.path, bma.useSsl)); + console.info('[network] [#{0}] Starting from [{1}{2}] {ssl: {3}, expertMode: {4}, sandboxes: {5}}'.format(pid, bma.server, bma.path, bma.useSsl, data.expertMode, data.withSandboxes)); - addListeners(); + // Start listener to auto refresh peers + if (data.autoRefresh) addListeners(); return loadPeers() .then(function(peers){ @@ -910,7 +923,7 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', close = function(pid) { if (data.bma) { - console.info(pid > 0 ? '[network] [#{0}] Stopping...'.format(pid) : '[network] Stopping...'); + console.info(pid > 0 ? '[network] [#{0}] Stopping'.format(pid) : '[network] Stopping'); removeListeners(); resetData(); } @@ -959,34 +972,85 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', options.filter.ssl = isHttpsMode ? true : undefined /*= all */; options.filter.online = true; options.filter.expertMode = false; // Difficulties not need - options.timeout = angular.isDefined(options.timeout) ? options.timeout : getDefaultTimeout(); + options.timeout = angular.isDefined(options.timeout) ? options.timeout : getDeviceTimeout(); + options.minConsensusPeerCount = options.minConsensusPeerCount > 0 ? options.minConsensusPeerCount : -1; + options.autoRefresh = options.autoRefresh === true; // false by default (dont need to detecte changes, using websockets) + options.flushIntervalMs = angular.isDefined(options.flushIntervalMs) ? options.flushIntervalMs : 1000; + options.withSandboxes = angular.isDefined(options.withSandboxes) ? options.withSandboxes : false; + + // Compute minConsensusPeerCount, from statistics + if (options.minConsensusPeerCount <= 0) { + // Init using default config value + options.minConsensusPeerCount = csSettings.data.minConsensusPeerCount > 0 ? csSettings.data.minConsensusPeerCount : -1; + + // Override with stats value (if greater) + var avgPeerCount = csSettings.stats.computeAvgPeerCount(options); + var avgPeerCount80pct = avgPeerCount > 0 && Math.floor(avgPeerCount * 0.8) || -1; + if (avgPeerCount80pct > options.minConsensusPeerCount) { + console.debug('[network] Using computed minConsensusPeerCount={0} (=80% of average peer count {1})'.format(avgPeerCount80pct, avgPeerCount)); + options.minConsensusPeerCount = avgPeerCount80pct; + } + } var wasStarted = isStarted(); var pid = wasStarted ? data.pid : data.pid + 1; + console.info('[network] [#{0}] Getting synchronized BMA peers... {timeout: {1}, minConsensusPeerCount: {2}, flushIntervalMs: {3}}'.format(pid, + options.timeout, options.minConsensusPeerCount, options.flushIntervalMs)); - var now = Date.now(); - console.info('[network] [#{0}] Getting synchronized BMA peers... {timeout: {1}}'.format(pid, options.timeout)); + var filterPeers = function(peers) { + var peerUrls = []; + return peers.reduce(function(res, peer) { + // Exclude if not BMA or not on the main consensus block + if (!peer || !peer.isBma() || !peer.hasMainConsensusBlock) return res; - return startIfNeed(bma, options) - .then(function(data){ - var peerUrls = []; - var peers = data && data.peers.reduce(function(res, peer) { - // Exclude if not BMA or not on the main consensus block - if (!peer || !peer.isBma() || !peer.hasMainConsensusBlock) return res; + // Fill some properties compatible + peer.compatible = isCompatible(peer); + peer.url = peer.getUrl(); + + // Clean unused properties (e.g. the API, created by BMA.lightInstance()) + delete peer.api; - // Fill some properties compatible - peer.compatible = isCompatible(peer); - peer.url = peer.getUrl(); + // Remove duplicate + if (peerUrls.includes(peer.url)) return res; + peerUrls.push(peer.url); - // Clean unused properties (e.g. the API, created by BMA.lightInstance()) - delete peer.api; + return res.concat(peer); + }, []); + }; - // Remove duplicate - if (peerUrls.includes(peer.url)) return res; - peerUrls.push(peer.url); - return res.concat(peer); - }, []); + var deferred = $q.defer(); + var checkEnoughListener = null; + + // Stop when enough peers, on the main consensus block + if (options.minConsensusPeerCount > 0) { + checkEnoughListener = api.data.on.changed($rootScope, function(data) { + console.debug('[network] [#{0}] {1} peers'.format(pid, data.peers.length)); + var peers = filterPeers(data.peers); + var consensusPeerCount = peers.length; + // Stop here, if reach minConsensusPeerCount + if (consensusPeerCount >= options.minConsensusPeerCount) { + if (checkEnoughListener) { + checkEnoughListener(); + checkEnoughListener = null; + + console.debug('[network] [#{0}] Found enough peers on main consensus - in {1}ms (timeout {2}ms) '.format(pid, Date.now() - data.startTime, options.timeout)); + deferred.resolve(peers); + } + } + }); + } + + // Start network scan + startIfNeed(bma, options) + .then(function(data) { + var peers = filterPeers(data.peers); + deferred.resolve(peers); + }) + .catch(deferred.reject); + + return deferred.promise + .then(function(peers){ // Log if (peers && peers.length > 0) { @@ -995,18 +1059,24 @@ angular.module('cesium.network.services', ['ngApi', 'cesium.currency.services', peers.length, data.peers.length, mainConsensusBlock, - Date.now() - now)); + Date.now() - data.startTime)); } else { - console.warn('[network] [#{0}] No synchronized BMA peers found - in {1}ms'.format(pid, Date.now() - now)); + console.warn('[network] [#{0}] No synchronized BMA peers found - in {1}ms'.format(pid, Date.now() - data.startTime)); } + // Stop network if (!wasStarted) close(pid); + if (checkEnoughListener) checkEnoughListener(); + return peers; }) .catch(function(err) { console.error('[network] [#{0}] Error while getting synchronized BMA peers'.format(pid), err); + if (!wasStarted) close(pid); + if (checkEnoughListener) checkEnoughListener(); + throw err; }); }, diff --git a/www/js/services/settings-services.js b/www/js/services/settings-services.js index 81ceb5e5a25c7c6f9fe0f71d0455895cfa6663dc..ff47d939a413b90c8c20428de44ed8bcce39b9e6 100644 --- a/www/js/services/settings-services.js +++ b/www/js/services/settings-services.js @@ -1,7 +1,7 @@ angular.module('cesium.settings.services', ['ngApi', 'cesium.config']) -.factory('csSettings', function($rootScope, $q, $window, Api, localStorage, $translate, csConfig) { +.factory('csSettings', function($rootScope, $q, $window, $timeout, Api, localStorage, $translate, csConfig) { 'ngInject'; // Define app locales @@ -76,7 +76,7 @@ angular.module('cesium.settings.services', ['ngApi', 'cesium.config']) timeWarningExpire: 2592000 * 3 /*=3 mois*/, minVersion: '1.8.0', minVersionAtStartup: '1.8.7', // use for node auto-selection - minPeerCountAtStartup: 10, // use for node auto-selection (avoid to start if no few peers found) + minConsensusPeerCount: 10, // use for node auto-selection (avoid to start if no few peers found) sourceUrl: 'https://git.duniter.org/clients/cesium-grp/cesium', sourceLicenseUrl: 'https://git.duniter.org/clients/cesium-grp/cesium/-/raw/master/LICENSE', newIssueUrl: "https://git.duniter.org/clients/cesium-grp/cesium/issues/new", @@ -107,7 +107,10 @@ angular.module('cesium.settings.services', ['ngApi', 'cesium.config']) blockValidityWindow: 6, network: { // Synchronized BMA peers found - peers: [] + peers: [], + stats: [], + statsWindowSecond: (csConfig.network && csConfig.network.statsWindowSecond || 10 * 24 * 60 * 60), // 10 days + statsPeriodSecond: 5 * 60 // 5 min }, helptip: { enable: true, @@ -302,6 +305,85 @@ angular.module('cesium.settings.services', ['ngApi', 'cesium.config']) }); } + /** + * Save synchronized BMA peers, after a network scan + * @param newData + */ + function savePeers(peers, options) { + if (!peers) return; // skip empty + + console.debug("[settings] Saving {0} BMA peers...".format(peers.length)); + + data.network = data.network || {}; + data.network.peers = peers; + + // Update peer stats + var statsWindowSecond = options && options.statsWindowSecond || data.network.statsWindowSecond; + if (statsWindowSecond > 0) { + + // Clean stats outside the stats window + var now = Date.now(); + var minTime = now - statsWindowSecond * 1000; + data.network.stats = _.filter(data.network.stats || [], function(stat) { + return stat.time > minTime; + }); + + var currentStat = { + time: now, + peerCount: peers.length + 1 /*current BMA peer*/ + }; + + // If previous stats is recent (< 5min) + // this is used to avoid too many stats, but keep the must fresh value + var previousStats = data.network.stats.length ? data.network.stats[data.network.stats.length-1] : undefined; + var previousAgeSecond = previousStats && (now - previousStats.time) / 1000; + var statsPeriodSecond = data.network.statsPeriodSecond || 5 * 60; // 5 min; + if (previousAgeSecond && previousAgeSecond < statsPeriodSecond) { + if (currentStat.peerCount === previousStats.peerCount) { + console.debug("[settings] Skip network stats (recent stats exists)"); + } + // Replace it peer count change + else { + angular.merge(previousStats, currentStat); + } + } + else { + // Insert + data.network.stats.push(currentStat); + } + } + + // Storing, with a delay 2s + $timeout(store, 2000); + } + + function computeAvgPeerCount(options) { + if (!data.network || !data.network.stats || !data.network.stats.length) return undefined; // Cannot compute + + var statsWindowSecond = options && options.statsWindowSecond || data.network.statsWindowSecond || -1; + if (statsWindowSecond > data.network.statsWindowSecond) { + console.warn('[settings] Peer stats windows ({0}s) should not be greater than the collected windows ({1}s)'.format( + statsWindowSecond, + data.statsWindowSecond + )); + statsWindowSecond = data.network.statsWindowSecond; + } + + var now = Date.now(); + var minTime = now - statsWindowSecond * 1000; + + // Select stats inside the expected window + var stats = _.filter(data.network.stats || [], function(stat) { + return stat.time > minTime && stat.peerCount > 1; + }); + + if (!stats.length) return undefined; // Not enough stats to compute something + + // Compute the AVG(peerCount) + var sum = _.reduce(stats, function(sum, stat) { return sum + stat.peerCount; }, 0); + return Math.floor(sum / stats.length); + } + function getLicenseUrl() { var locale = data.locale && data.locale.id || csConfig.defaultLanguage || 'en'; return (csConfig.license) ? @@ -408,6 +490,10 @@ angular.module('cesium.settings.services', ['ngApi', 'cesium.config']) restore: restore, getLicenseUrl: getLicenseUrl, getFeedUrl: getFeedUrl, + savePeers: savePeers, + stats: { + computeAvgPeerCount: computeAvgPeerCount + }, defaultSettings: defaultSettings, // api extension api: api,