-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: 🧑💻 Add [ProvidersTracker] to monitor alive providers.
- Loading branch information
Showing
3 changed files
with
214 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<OracleService>#2d1c8) | ||
/// [1] = AutoDisposeAsyncNotifierProviderImpl (_archethicOracleUCONotifierProvider:AutoDisposeAsyncNotifierProviderImpl<_ArchethicOracleUCONotifier, ArchethicOracleUCO>#4aa30) | ||
/// [2] = AutoDisposeProvider (apiServiceProvider:AutoDisposeProvider<ApiService>#e4552) | ||
/// ``` | ||
/// | ||
/// ## Filter and READ providers | ||
/// | ||
/// In debug console : | ||
/// | ||
/// ### Command : | ||
/// ```dart | ||
/// aedappfm.ProvidersTracker().byName('oracle').read | ||
/// ``` | ||
/// | ||
/// ### Result : | ||
/// ```dart | ||
/// Set | ||
/// [0] = AutoDisposeProvider (oracleServiceProvider:AutoDisposeProvider<OracleService>#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<Set<ProviderBase<Object?>>> _aliveProviders = | ||
ValueNotifier({}); | ||
|
||
void _addAliveProvider(ProviderBase<Object?> provider) { | ||
_aliveProviders.value = {..._aliveProviders.value, provider}; | ||
} | ||
|
||
void _removeAliveProvider(ProviderBase<Object?> provider) { | ||
_aliveProviders.value = _aliveProviders.value | ||
.where((aliveProvider) => aliveProvider != provider) | ||
.toSet(); | ||
} | ||
|
||
@override | ||
void didAddProvider( | ||
ProviderBase<Object?> provider, | ||
Object? value, | ||
ProviderContainer container, | ||
) { | ||
_addAliveProvider(provider); | ||
} | ||
|
||
@override | ||
void didDisposeProvider(ProviderBase provider, ProviderContainer container) { | ||
_removeAliveProvider(provider); | ||
} | ||
|
||
/// Shows all providers currently alive | ||
Set<ProviderBase<Object?>> get aliveProviders => _aliveProviders.value; | ||
|
||
/// Shows the provider with matching [hashCode] | ||
ProviderBase<Object?>? 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<ProviderBase<Object?>> get read => | ||
tracker._aliveProviders.value.match(matcher); | ||
|
||
/// Creates a [Stream] watching all providers currently alive, filtered according to the [matcher]. | ||
Stream<Set<ProviderBase<Object?>>> get watch { | ||
Set<ProviderBase<Object?>>? previousValue; | ||
late final StreamController<Set<ProviderBase<Object?>>> 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<Set<ProviderBase<Object?>>>( | ||
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<OracleService>#2d1c8) | ||
/// - AutoDisposeAsyncNotifierProviderImpl (_archethicOracleUCONotifierProvider:AutoDisposeAsyncNotifierProviderImpl<_ArchethicOracleUCONotifier, ArchethicOracleUCO>#4aa30) | ||
factory ProviderMatcher.name(String name) => NameProviderMatcher(name); | ||
|
||
bool matches(ProviderBase<Object?> provider) => throw UnimplementedError(); | ||
} | ||
|
||
class NameProviderMatcher implements ProviderMatcher { | ||
const NameProviderMatcher(this.name); | ||
|
||
final String name; | ||
|
||
@override | ||
bool matches(ProviderBase<Object?> provider) => | ||
(provider.name ?? '').toLowerCase().contains(name.toLowerCase()) || | ||
provider.runtimeType | ||
.toString() | ||
.toLowerCase() | ||
.contains(name.toLowerCase()); | ||
} | ||
|
||
extension SetProviderMatchExt on Set<ProviderBase<Object?>> { | ||
Set<ProviderBase<Object?>> match(ProviderMatcher matcher) => where( | ||
(element) => matcher.matches(element), | ||
).toSet(); | ||
} |