From c0c83869014a4bf3a53e013461db80bf3695c12d Mon Sep 17 00:00:00 2001 From: Chralu Date: Wed, 18 Dec 2024 17:08:00 +0100 Subject: [PATCH] feat: :technologist: Add [ProvidersTracker] to monitor alive providers. --- lib/archethic_dapp_framework_flutter.dart | 3 +- .../utils/providers_logger.dart} | 0 .../application/utils/providers_tracker.dart | 212 ++++++++++++++++++ 3 files changed, 214 insertions(+), 1 deletion(-) rename lib/src/{util/generic/providers_observer.dart => application/utils/providers_logger.dart} (100%) create mode 100644 lib/src/application/utils/providers_tracker.dart diff --git a/lib/archethic_dapp_framework_flutter.dart b/lib/archethic_dapp_framework_flutter.dart index 20d2dee..3c9149f 100644 --- a/lib/archethic_dapp_framework_flutter.dart +++ b/lib/archethic_dapp_framework_flutter.dart @@ -8,6 +8,8 @@ export 'src/application/def_tokens.dart'; export 'src/application/oracle/provider.dart'; export 'src/application/oracle/state.dart'; export 'src/application/ucids_tokens.dart'; +export 'src/application/utils/providers_logger.dart'; +export 'src/application/utils/providers_tracker.dart'; export 'src/application/verified_tokens.dart'; export 'src/application/version.dart'; export 'src/domain/models/ae_token.dart'; @@ -76,7 +78,6 @@ export 'src/util/address_util.dart'; export 'src/util/custom_logs.dart'; export 'src/util/file_util.dart'; export 'src/util/generic/get_it_instance.dart'; -export 'src/util/generic/providers_observer.dart'; export 'src/util/logger_output.dart'; export 'src/util/periodic_future.dart'; export 'src/util/router_util.dart'; diff --git a/lib/src/util/generic/providers_observer.dart b/lib/src/application/utils/providers_logger.dart similarity index 100% rename from lib/src/util/generic/providers_observer.dart rename to lib/src/application/utils/providers_logger.dart diff --git a/lib/src/application/utils/providers_tracker.dart b/lib/src/application/utils/providers_tracker.dart new file mode 100644 index 0000000..0e11b47 --- /dev/null +++ b/lib/src/application/utils/providers_tracker.dart @@ -0,0 +1,212 @@ +import 'dart:async'; +import 'dart:core'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// Keeps track of alive providers. +/// +/// > Usage in production is not recommended. +/// +/// # Howto use +/// ## Register the observer +/// +/// ```dart +/// import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' as aedappfm; +/// +/// runApp( +/// ProviderScope( +/// observers: [ +/// if (kDebugMode) aedappfm.ProvidersTracker(), +/// ], +/// child: const MyApp(), +/// ), +/// ; +/// ``` +/// +/// ## Check all alive providers +/// +/// In debug console, check `ProvidersTracker` content : +/// +/// ### Command : +/// ```dart +/// aedappfm.ProvidersTracker().aliveProviders +/// ``` +/// +/// ### Result : +/// ```dart +/// Set +/// [0] = AutoDisposeProvider (oracleServiceProvider:AutoDisposeProvider#2d1c8) +/// [1] = AutoDisposeAsyncNotifierProviderImpl (_archethicOracleUCONotifierProvider:AutoDisposeAsyncNotifierProviderImpl<_ArchethicOracleUCONotifier, ArchethicOracleUCO>#4aa30) +/// [2] = AutoDisposeProvider (apiServiceProvider:AutoDisposeProvider#e4552) +/// ``` +/// +/// ## Filter and READ providers +/// +/// In debug console : +/// +/// ### Command : +/// ```dart +/// aedappfm.ProvidersTracker().byName('oracle').read +/// ``` +/// +/// ### Result : +/// ```dart +/// Set +/// [0] = AutoDisposeProvider (oracleServiceProvider:AutoDisposeProvider#2d1c8) +/// [1] = AutoDisposeAsyncNotifierProviderImpl (_archethicOracleUCONotifierProvider:AutoDisposeAsyncNotifierProviderImpl<_ArchethicOracleUCONotifier, ArchethicOracleUCO>#4aa30) +/// ``` + +/// ## Filter and WATCH providers +/// +/// In debug console : +/// +/// ### Command : +/// ```dart +/// // watch returns a stream. Here we just log the number of providers whose name matches 'oracle' +/// aedappfm.ProvidersTracker().byName('oracle').watch.forEach((providers) => print('>>> Oracle : ${providers.length}')) +/// ``` +/// +/// ### Result : +/// +/// Each time the alive providers matching 'oracle' changes, we have a log like this : +/// +/// ```dart +/// >>> Oracle : 2 +/// ``` +class ProvidersTracker extends ProviderObserver { + factory ProvidersTracker() { + return _instance ??= ProvidersTracker._(); + } + ProvidersTracker._(); + + static ProvidersTracker? _instance; + + final ValueNotifier>> _aliveProviders = + ValueNotifier({}); + + void _addAliveProvider(ProviderBase provider) { + _aliveProviders.value = {..._aliveProviders.value, provider}; + } + + void _removeAliveProvider(ProviderBase provider) { + _aliveProviders.value = _aliveProviders.value + .where((aliveProvider) => aliveProvider != provider) + .toSet(); + } + + @override + void didAddProvider( + ProviderBase provider, + Object? value, + ProviderContainer container, + ) { + _addAliveProvider(provider); + } + + @override + void didDisposeProvider(ProviderBase provider, ProviderContainer container) { + _removeAliveProvider(provider); + } + + /// Shows all providers currently alive + Set> get aliveProviders => _aliveProviders.value; + + /// Shows the provider with matching [hashCode] + ProviderBase? provider(int hashCode) => _aliveProviders.value + .where( + (element) => element.hashCode == hashCode, + ) + .firstOrNull; + + /// Creates a [ProvidersTrackerMatcher]. + ProvidersTrackerMatcher match(ProviderMatcher matcher) => + ProvidersTrackerMatcher(tracker: this, matcher: matcher); + + /// Creates a [ProvidersTrackerMatcher] which + /// filters providers by name/classname. + /// + /// For more details about the matchin rules, check [NameProviderMatcher]. + ProvidersTrackerMatcher byName(String name) => + match(ProviderMatcher.name(name)); +} + +/// Provides [read] and [watch] methods to monitor +/// currently alive providers filtered with [matcher]. +class ProvidersTrackerMatcher { + ProvidersTrackerMatcher({ + required this.tracker, + required this.matcher, + }); + + final ProviderMatcher matcher; + final ProvidersTracker tracker; + + /// Shows all providers currently alive, filtered according to the [matcher]. + Set> get read => + tracker._aliveProviders.value.match(matcher); + + /// Creates a [Stream] watching all providers currently alive, filtered according to the [matcher]. + Stream>> get watch { + Set>? previousValue; + late final StreamController>> controller; + + void processChange() { + final newValue = tracker._aliveProviders.value.match(matcher); + if (newValue == previousValue) return; + + previousValue = newValue; + controller.add(newValue); + } + + void listen() { + processChange(); + tracker._aliveProviders.addListener(processChange); + } + + void close() { + tracker._aliveProviders.removeListener(processChange); + } + + controller = StreamController>>( + onListen: listen, + onPause: close, + onResume: listen, + onCancel: close, + ); + + return controller.stream; + } +} + +abstract class ProviderMatcher { + /// [name] matcher is case insensitive. Name matching is quite permissive. + /// + /// `ProvidersTracker().aliveProviders('oracle')` would match the following providers : + /// + /// - AutoDisposeProvider (oracleServiceProvider:AutoDisposeProvider#2d1c8) + /// - AutoDisposeAsyncNotifierProviderImpl (_archethicOracleUCONotifierProvider:AutoDisposeAsyncNotifierProviderImpl<_ArchethicOracleUCONotifier, ArchethicOracleUCO>#4aa30) + factory ProviderMatcher.name(String name) => NameProviderMatcher(name); + + bool matches(ProviderBase provider) => throw UnimplementedError(); +} + +class NameProviderMatcher implements ProviderMatcher { + const NameProviderMatcher(this.name); + + final String name; + + @override + bool matches(ProviderBase provider) => + (provider.name ?? '').toLowerCase().contains(name.toLowerCase()) || + provider.runtimeType + .toString() + .toLowerCase() + .contains(name.toLowerCase()); +} + +extension SetProviderMatchExt on Set> { + Set> match(ProviderMatcher matcher) => where( + (element) => matcher.matches(element), + ).toSet(); +}