Skip to content

Commit

Permalink
feat: 🧑‍💻 Add [ProvidersTracker] to monitor alive providers.
Browse files Browse the repository at this point in the history
  • Loading branch information
Chralu committed Dec 18, 2024
1 parent 29984d9 commit c8c8a2c
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 1 deletion.
3 changes: 2 additions & 1 deletion lib/archethic_dapp_framework_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down
212 changes: 212 additions & 0 deletions lib/src/application/utils/providers_tracker.dart
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();
}

0 comments on commit c8c8a2c

Please sign in to comment.