From 68ad019ba593ace0141a14920d45b091b9e8adb5 Mon Sep 17 00:00:00 2001 From: clangenb <37865735+clangenb@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:22:15 +0200 Subject: [PATCH] Handle multiple network endpoints and choose a healthy one with offchain indexing enabled (#1693) * add simple `EndpointManager` class * [EndpointManager] minor fixes * [EndpointManager] Extract EndpointChecker * [EndpointManager] fmt * [EndpointManager] fix: change abstract class to mixin * minor fixes * [config/networks] add endpoints and rename them * [api] use `EndpointManager` in API * [api] check for offchain indexing in the healthcheck * [dart_api] proper condition of offchain indexing enablement log. * [dart_api] remove unused code * [EndpointManager] randomize order of healthcheck. * [const/network] better naming convention for address field * [EndpointManager] make randomization optional * fmt * update pubspec_overrides.yaml --- app/lib/config/networks/networks.dart | 41 +- app/lib/service/substrate_api/api.dart | 47 ++- .../service/substrate_api/core/dart_api.dart | 24 +- .../core/reconnecting_ws_provider.dart | 3 +- app/pubspec.lock | 7 + app/pubspec.yaml | 2 + app/pubspec_overrides.yaml | 4 +- .../endpoint_manager/analysis_options.yaml | 7 + .../lib/endpoint_manager.dart | 71 ++++ packages/endpoint_manager/pubspec.lock | 373 ++++++++++++++++++ packages/endpoint_manager/pubspec.yaml | 13 + .../test/endpoint_manager_test.dart | 40 ++ 12 files changed, 589 insertions(+), 43 deletions(-) create mode 100644 packages/endpoint_manager/analysis_options.yaml create mode 100644 packages/endpoint_manager/lib/endpoint_manager.dart create mode 100644 packages/endpoint_manager/pubspec.lock create mode 100644 packages/endpoint_manager/pubspec.yaml create mode 100644 packages/endpoint_manager/test/endpoint_manager_test.dart diff --git a/app/lib/config/networks/networks.dart b/app/lib/config/networks/networks.dart index 561903406..57559a70c 100644 --- a/app/lib/config/networks/networks.dart +++ b/app/lib/config/networks/networks.dart @@ -1,12 +1,16 @@ import 'dart:io'; import 'package:encointer_wallet/config/consts.dart'; +import 'package:ew_endpoint_manager/endpoint_manager.dart'; -class NetworkEndpoint { - NetworkEndpoint({required this.name, required this.address}); +class NetworkEndpoint with Endpoint { + NetworkEndpoint({required this.name, required String address}) : _address = address; final String name; - final String address; + final String _address; + + @override + String address() => _address; } const String gesellId = 'nctr-gsl'; @@ -75,15 +79,13 @@ enum Network { }; } - /// Exists for simple reverse compatibility. - /// Will be remove in the course of https://github.com/encointer/encointer-wallet-flutter/issues/1603. - String value() { + String defaultEndpoint() { return switch (this) { - encointerKusama => networkEndpoints().first.address, - encointerRococo => networkEndpoints().first.address, - gesell => networkEndpoints().first.address, + encointerKusama => networkEndpoints().first.address(), + encointerRococo => networkEndpoints().first.address(), + gesell => networkEndpoints().first.address(), // only dev network refers to the local one - gesellDev => networkEndpoints().first.address, + gesellDev => networkEndpoints().first.address(), }; } @@ -98,31 +100,26 @@ enum Network { } List gesellEndpoints() { - return [ - NetworkEndpoint(name: 'Encointer Gesell (Hosted by Encointer Association)', address: 'wss://gesell.encointer.org') - ]; + return [NetworkEndpoint(name: 'Encointer Association', address: 'wss://gesell.encointer.org')]; } List gesellDevEndpoints() { return [ - NetworkEndpoint( - name: 'Encointer Gesell Local DevNet', - address: 'ws://${Platform.isAndroid ? androidLocalHost : iosLocalHost}:9944') + NetworkEndpoint(name: 'Local DevNet', address: 'ws://${Platform.isAndroid ? androidLocalHost : iosLocalHost}:9944') ]; } List rococoEndpoints() { return [ - NetworkEndpoint( - name: 'Encointer Lietaer on Rococo (Hosted by Encointer Association)', - address: 'wss://rococo.api.encointer.org') + NetworkEndpoint(name: 'Encointer Association', address: 'wss://rococo.api.encointer.org'), ]; } List kusamaEndpoints() { return [ - NetworkEndpoint( - name: 'Encointer Network on Kusama (Hosted by Encointer Association)', - address: 'wss://kusama.api.encointer.org') + NetworkEndpoint(name: 'Encointer Association', address: 'wss://kusama.api.encointer.org'), + NetworkEndpoint(name: 'Dwellir', address: 'wss://encointer-kusama-rpc.dwellir.com'), + NetworkEndpoint(name: 'IBP1', address: 'wss://sys.ibp.network/encointer-kusama'), + NetworkEndpoint(name: 'IBP2', address: 'wss://sys.dotters.network/encointer-kusama'), ]; } diff --git a/app/lib/service/substrate_api/api.dart b/app/lib/service/substrate_api/api.dart index a73d9b366..d1712963c 100644 --- a/app/lib/service/substrate_api/api.dart +++ b/app/lib/service/substrate_api/api.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:encointer_wallet/config/networks/networks.dart'; import 'package:encointer_wallet/store/app.dart'; import 'package:encointer_wallet/mocks/ipfs_api.dart'; import 'package:encointer_wallet/service/ipfs/ipfs_api.dart'; @@ -9,6 +10,7 @@ import 'package:encointer_wallet/service/substrate_api/chain_api.dart'; import 'package:encointer_wallet/service/substrate_api/core/dart_api.dart'; import 'package:encointer_wallet/service/substrate_api/encointer/encointer_api.dart'; import 'package:encointer_wallet/service/substrate_api/core/reconnecting_ws_provider.dart'; +import 'package:ew_endpoint_manager/endpoint_manager.dart'; import 'package:ew_http/ew_http.dart'; import 'package:ew_polkadart/ew_polkadart.dart'; import 'package:encointer_wallet/service/log/log_service.dart'; @@ -18,6 +20,31 @@ import 'package:encointer_wallet/service/log/log_service.dart'; /// `late final` because it will be initialized exactly once in lib/app.dart. late Api webApi; +class NetworkEndpointChecker with EndpointChecker { + // Trivial check if we can connect to an endpoint. + @override + Future checkHealth(NetworkEndpoint endpoint) async { + Log.d('[NetworkEndpointChecker] Checking health of: ${endpoint.address()}', 'Api'); + + final provider = WsProvider(Uri.parse(endpoint.address())); + final ready = await provider.ready(); + + Log.d('[NetworkEndpointChecker] Endpoint ${endpoint.address()} ready: $ready', 'Api'); + + if (!ready) { + await provider.disconnect(); + return false; + } + + final offchainIndexing = await SubstrateDartApi(provider).offchainIndexingEnabled(); + Log.d('[NetworkEndpointChecker] Endpoint ${endpoint.address()} offchainIndexingEnabled: $offchainIndexing', 'Api'); + + await provider.disconnect(); + // only allow nodes that have offchain indexing enabled + return offchainIndexing; + } +} + class Api { Api( this.store, @@ -34,7 +61,9 @@ class Api { EwHttp ewHttp, { bool isIntegrationTest = false, }) { - final provider = ReconnectingWsProvider(Uri.parse(store.settings.currentNetwork.value()), autoConnect: false); + // Initialize with default endpoint, will check for healthiness later. + final provider = + ReconnectingWsProvider(Uri.parse(store.settings.currentNetwork.defaultEndpoint()), autoConnect: false); return Api( store, provider, @@ -79,13 +108,17 @@ class Api { }); } - Future _connect() { - Log.d('[webApi] Connecting to endpoint: ${store.settings.currentNetwork.value()}', 'Api'); + Future _connect() async { + Log.p('[webApi] Looking for a healthy endpoint...', 'Api'); + final manager = + EndpointManager.withEndpoints(NetworkEndpointChecker(), store.settings.currentNetwork.networkEndpoints()); + final endpoint = await manager.pollHealthyEndpoint(randomize: true); + + Log.p('[webApi] Connecting to healthy endpoint: ${endpoint.address()}', 'Api'); store.settings.setNetworkLoading(true); - final endpoint = store.settings.currentNetwork.value(); - return provider.connectToNewEndpoint(Uri.parse(endpoint)).then((voidValue) async { + return provider.connectToNewEndpoint(Uri.parse(endpoint.address())).then((voidValue) async { Log.p('[webApi] channel is ready...'); if (await isConnected()) { return _onConnected(); @@ -99,7 +132,7 @@ class Api { } Future _onConnected() async { - Log.d('[webApi] Connected to endpoint: ${store.settings.currentNetwork.value()}', 'Api'); + Log.d('[webApi] Connected to endpoint: ${provider.url}', 'Api'); if (store.account.currentAddress.isNotEmpty) { await store.encointer.initializeUninitializedStores(store.account.currentAddress); @@ -118,7 +151,7 @@ class Api { store.settings.setNetworkLoading(false); - Log.d('[webApi] Obtained basic network data: ${store.settings.currentNetwork.value()}'); + Log.d('[webApi] Obtained basic network data: ${provider.url}'); // need to do this from here as we can't access instance fields in constructor. account.setFetchAccountData(fetchAccountData); diff --git a/app/lib/service/substrate_api/core/dart_api.dart b/app/lib/service/substrate_api/core/dart_api.dart index 58fe498ad..9d1beef40 100644 --- a/app/lib/service/substrate_api/core/dart_api.dart +++ b/app/lib/service/substrate_api/core/dart_api.dart @@ -13,26 +13,15 @@ class SubstrateDartApi { /// Websocket client used to connect to the node. final Provider _provider; - /// The rpc methods exposed by the connected node. - RpcMethods? _rpc; - - /// Address of the node we connect to including ws(s). - String? _endpoint; - /// Returns the rpc nodes of the connected node or an empty list otherwise. Future rpcMethods() async { return rpc>('rpc_methods', []).then(RpcMethods.fromJson); } - /// Gets address of the node we connect to including ws(s). - String? get endpoint => _endpoint; - Future connect(String endpoint) async { try { - _rpc = await rpc>('rpc_methods', []).then(RpcMethods.fromJson); - // Sanity check that we are running against valid node with offchain indexing enabled - if (!_rpc!.methods!.contains('encointer_getReputations')) { + if (!(await offchainIndexingEnabled())) { Log.d( "rpc_methods does not contain 'getReputations'. Are the following flags passed" " to the node? \n '--enable-offchain-indexing true --rpc-methods unsafe'", @@ -44,6 +33,17 @@ class SubstrateDartApi { } } + Future offchainIndexingEnabled() async { + try { + // Check reputation of Alice. This will return an exception if offchain + // indexing is disabled. + await rpc>('encointer_getReputations', ['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY']); + return Future.value(true); + } catch (e) { + return Future.value(false); + } + } + /// Queries the rpc of the node. /// /// Hints: diff --git a/app/lib/service/substrate_api/core/reconnecting_ws_provider.dart b/app/lib/service/substrate_api/core/reconnecting_ws_provider.dart index ab4aaa7c1..a68a9a8bb 100644 --- a/app/lib/service/substrate_api/core/reconnecting_ws_provider.dart +++ b/app/lib/service/substrate_api/core/reconnecting_ws_provider.dart @@ -10,11 +10,12 @@ class ReconnectingWsProvider extends Provider { autoConnect: autoConnect, ); - final Uri url; + Uri url; WsProvider provider; Future connectToNewEndpoint(Uri url) async { await disconnect(); + this.url = url; provider = WsProvider(url); await provider.ready(); } diff --git a/app/pubspec.lock b/app/pubspec.lock index 67f1c69e2..60e5117ab 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -408,6 +408,13 @@ packages: relative: true source: path version: "0.1.0+1" + ew_endpoint_manager: + dependency: "direct main" + description: + path: "../packages/endpoint_manager" + relative: true + source: path + version: "0.1.0+1" ew_http: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 7180cef57..174cee9d4 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -76,6 +76,8 @@ dependencies: pointycastle: ^3.7.3 connectivity_plus: ^5.0.2 timezone: ^0.9.2 + ew_endpoint_manager: + path: ../packages/endpoint_manager ew_encointer_utils: path: ../packages/ew_encointer_utils/ ew_storage: diff --git a/app/pubspec_overrides.yaml b/app/pubspec_overrides.yaml index 8e65c5bd5..4ce0233ed 100644 --- a/app/pubspec_overrides.yaml +++ b/app/pubspec_overrides.yaml @@ -1,7 +1,9 @@ -# melos_managed_dependency_overrides: ew_encointer_utils,ew_http,ew_keyring,ew_polkadart,ew_primitives,ew_storage,ew_substrate_fixed,polkadart,polkadart_scale_codec,substrate_metadata,polkadart_keyring +# melos_managed_dependency_overrides: ew_encointer_utils,ew_http,ew_keyring,ew_polkadart,ew_primitives,ew_storage,ew_substrate_fixed,polkadart,polkadart_scale_codec,substrate_metadata,polkadart_keyring,ew_endpoint_manager dependency_overrides: ew_encointer_utils: path: ../packages/ew_encointer_utils + ew_endpoint_manager: + path: ../packages/endpoint_manager ew_http: path: ../packages/ew_http ew_keyring: diff --git a/packages/endpoint_manager/analysis_options.yaml b/packages/endpoint_manager/analysis_options.yaml new file mode 100644 index 000000000..3ff4f2b81 --- /dev/null +++ b/packages/endpoint_manager/analysis_options.yaml @@ -0,0 +1,7 @@ +include: package:very_good_analysis/analysis_options.3.1.0.yaml + +linter: + rules: + public_member_api_docs: false + lines_longer_than_80_chars: false + sort_pub_dependencies: false \ No newline at end of file diff --git a/packages/endpoint_manager/lib/endpoint_manager.dart b/packages/endpoint_manager/lib/endpoint_manager.dart new file mode 100644 index 000000000..dbd2df859 --- /dev/null +++ b/packages/endpoint_manager/lib/endpoint_manager.dart @@ -0,0 +1,71 @@ +import 'dart:math'; + +mixin Endpoint { + String address(); +} + +mixin EndpointChecker { + Future checkHealth(E endpoint); +} + +class EndpointManager { + EndpointManager(this._checker); + + EndpointManager.withEndpoints(this._checker, List endpoints) { + for (final e in endpoints) { + this.endpoints[e.address()] = e; + } + } + + Map endpoints = {}; + + final EndpointChecker _checker; + + void addEndpoint(E endpoint) { + endpoints[endpoint.address()] = endpoint; + } + + void removeEndpoint(E endpoint) { + endpoints.remove(endpoint.address()); + } + + List getEndpoints() { + return endpoints.values.toList(); + } + + /// Returns the first endpoint that is healthy where the checks are optionally run in random order. + /// + /// Will return null if all endpoints are unhealthy. + Future getHealthyEndpoint({bool randomize = false}) { + final values = endpoints.values.toList(); + + if (randomize) { + values.shuffle(Random()); + } + + return firstWhereAsync(values, _checker.checkHealth); + } + + /// Returns a future that completes once a healthy endpoint has been found. + Future pollHealthyEndpoint({bool randomize = false}) async { + E? endpoint; + + while ((endpoint = await getHealthyEndpoint(randomize: randomize)) == null) { + await Future.delayed(const Duration(seconds: 5)); + } + + return endpoint!; + } +} + +Future firstWhereAsync( + Iterable items, + Future Function(T) test, +) async { + for (final item in items) { + if (await test(item).timeout(const Duration(seconds: 2), onTimeout: () => false)) { + return item; + } + } + return null; +} diff --git a/packages/endpoint_manager/pubspec.lock b/packages/endpoint_manager/pubspec.lock new file mode 100644 index 000000000..494688fb7 --- /dev/null +++ b/packages/endpoint_manager/pubspec.lock @@ -0,0 +1,373 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "36a321c3d2cbe01cbcb3540a87b8843846e0206df3e691fa7b23e19e78de6d49" + url: "https://pub.dev" + source: hosted + version: "65.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: dfe03b90ec022450e22513b5e5ca1f01c0c01de9c3fba2f7fd233cb57a6b9a07 + url: "https://pub.dev" + source: hosted + version: "6.3.0" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: ac86d3abab0f165e4b8f561280ff4e066bceaac83c424dd19f1ae2c2fcd12ca9 + url: "https://pub.dev" + source: hosted + version: "1.7.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + url: "https://pub.dev" + source: hosted + version: "1.24.9" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + url: "https://pub.dev" + source: hosted + version: "0.5.9" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + very_good_analysis: + dependency: "direct dev" + description: + name: very_good_analysis + sha256: "9ae7f3a3bd5764fb021b335ca28a34f040cd0ab6eec00a1b213b445dae58a4b8" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.2.0 <4.0.0" diff --git a/packages/endpoint_manager/pubspec.yaml b/packages/endpoint_manager/pubspec.yaml new file mode 100644 index 000000000..016dea37a --- /dev/null +++ b/packages/endpoint_manager/pubspec.yaml @@ -0,0 +1,13 @@ +name: ew_endpoint_manager +description: Manages multiple endpoints and chooses a current one +version: 0.1.0+1 +publish_to: none + +environment: + sdk: ">=3.2.0 <4.0.0" + +dependencies: + +dev_dependencies: + test: ^1.24.9 + very_good_analysis: ^5.1.0 diff --git a/packages/endpoint_manager/test/endpoint_manager_test.dart b/packages/endpoint_manager/test/endpoint_manager_test.dart new file mode 100644 index 000000000..b2b4b9cdc --- /dev/null +++ b/packages/endpoint_manager/test/endpoint_manager_test.dart @@ -0,0 +1,40 @@ +import 'package:ew_endpoint_manager/endpoint_manager.dart'; +import 'package:test/test.dart'; + +class TestEndpoint implements Endpoint { + TestEndpoint(this._address, {required this.healthy}); + + final String _address; + + bool healthy; + + @override + String address() { + return _address; + } +} + +final testEndpoints = [ + TestEndpoint('address1', healthy: false), + TestEndpoint('address2', healthy: true), + TestEndpoint('address3', healthy: false), + TestEndpoint('address4', healthy: true), +]; + +class TestEndpointChecker implements EndpointChecker { + @override + Future checkHealth(TestEndpoint endpoint) { + return Future.value(endpoint.healthy); + } +} + +void main() { + group('EndpointManager', () { + test('Returns first healthy Endpoint', () async { + final manager = EndpointManager.withEndpoints(TestEndpointChecker(), testEndpoints); + final endpoint = await manager.getHealthyEndpoint(randomize: false); + + expect(endpoint!.address(), 'address2'); + }); + }); +}