LCOV - code coverage report
Current view: top level - repository - remote_adapter_watch.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 141 146 96.6 %
Date: 2022-06-06 11:59:57 Functions: 0 0 -

          Line data    Source code
       1             : part of flutter_data;
       2             : 
       3             : mixin _RemoteAdapterWatch<T extends DataModel<T>> on _RemoteAdapter<T> {
       4           2 :   @protected
       5             :   DataStateNotifier<List<T>?> watchAllNotifier({
       6             :     bool? remote,
       7             :     Map<String, dynamic>? params,
       8             :     Map<String, String>? headers,
       9             :     bool? syncLocal,
      10             :     String? finder,
      11             :     DataRequestLabel? label,
      12             :   }) {
      13           0 :     remote ??= _remote;
      14             :     syncLocal ??= false;
      15             : 
      16           6 :     final maybeFinder = _internalHolder?.finders[finder]?.call(this);
      17           4 :     final finderFn = maybeFinder is DataFinderAll<T> ? maybeFinder : findAll;
      18             : 
      19             :     // we can't use `findAll`'s default internal label
      20             :     // because we need a label to handle events
      21           4 :     label ??= DataRequestLabel('watchAll', type: internalType);
      22             : 
      23           2 :     log(label, 'initializing');
      24             : 
      25             :     // closure to get latest models
      26           2 :     List<T>? _getUpdatedModels() {
      27           4 :       return localAdapter.findAll();
      28             :     }
      29             : 
      30           2 :     final notifier = DataStateNotifier<List<T>?>(
      31           4 :       data: DataState(_getUpdatedModels(), isLoading: remote!),
      32             :     );
      33             : 
      34           4 :     notifier._reloadFn = () async {
      35           2 :       if (!notifier.mounted) {
      36             :         return;
      37             :       }
      38             : 
      39             :       if (remote!) {
      40           2 :         notifier.updateWith(isLoading: true);
      41             :       }
      42             : 
      43           4 :       await finderFn(
      44             :         remote: remote,
      45             :         params: params,
      46             :         headers: headers,
      47             :         syncLocal: syncLocal,
      48             :         label: label,
      49           2 :         onError: (e, label, _) async {
      50             :           try {
      51           3 :             await onError<List<T>>(e, label);
      52           1 :           } on DataException catch (err) {
      53             :             e = err;
      54             :           } catch (_) {
      55             :             rethrow;
      56             :           }
      57           2 :           if (notifier.mounted) {
      58           2 :             notifier.updateWith(isLoading: false, exception: e);
      59             :           }
      60             :           return null;
      61             :         },
      62             :       );
      63             :       if (remote) {
      64             :         // trigger doneLoading to ensure state is updated with isLoading=false
      65           8 :         graph._notify([label.toString()], type: DataGraphEventType.doneLoading);
      66             :       }
      67             :     };
      68             : 
      69             :     // kick off
      70           2 :     notifier.reload();
      71             : 
      72             :     late DelayedStateNotifier<List<DataGraphEvent>> throttledGraph;
      73             : 
      74           6 :     final throttleDuration = read(graphNotifierThrottleDurationProvider);
      75             :     if (throttleDuration != null) {
      76           0 :       throttledGraph = graph.throttle(() => throttleDuration);
      77             :     } else {
      78             :       // if no throttle is required, use map to
      79             :       // convert a single event in a list of events
      80           8 :       throttledGraph = graph.map((_) => [_]);
      81             :     }
      82             : 
      83           2 :     final states = <DataState<List<T>?>>[];
      84             : 
      85           4 :     final dispose = throttledGraph.addListener((events) {
      86           2 :       if (!notifier.mounted) {
      87             :         return;
      88             :       }
      89             : 
      90           4 :       for (final event in events) {
      91             :         // handle done loading
      92           4 :         if (notifier.data.isLoading &&
      93           8 :             event.keys.last == label.toString() &&
      94           4 :             event.type == DataGraphEventType.doneLoading) {
      95           2 :           final models = _getUpdatedModels();
      96           4 :           states.add(DataState(models, isLoading: false, exception: null));
      97             :         }
      98             : 
      99           6 :         if (notifier.data.isLoading == false &&
     100           4 :             event.type.isNode &&
     101           8 :             event.keys.first.startsWith(internalType)) {
     102           2 :           final models = _getUpdatedModels();
     103           2 :           log(label!, 'updated models', logLevel: 2);
     104           4 :           states.add(DataState(
     105             :             models,
     106           4 :             isLoading: notifier.data.isLoading,
     107           4 :             exception: notifier.data.exception,
     108           4 :             stackTrace: notifier.data.stackTrace,
     109             :           ));
     110             :         }
     111             :       }
     112             : 
     113           2 :       _updateFromStates(states, notifier);
     114             :     });
     115             : 
     116           4 :     notifier.onDispose = () {
     117           2 :       log(label!, 'disposing');
     118           2 :       dispose();
     119             :     };
     120             :     return notifier;
     121             :   }
     122             : 
     123           2 :   @protected
     124             :   DataStateNotifier<T?> watchOneNotifier(
     125             :     String key, {
     126             :     bool? remote,
     127             :     Map<String, dynamic>? params,
     128             :     Map<String, String>? headers,
     129             :     AlsoWatch<T>? alsoWatch,
     130             :     String? finder,
     131             :     DataRequestLabel? label,
     132             :   }) {
     133           4 :     final id = graph.getIdForKey(key);
     134             : 
     135           0 :     remote ??= _remote;
     136           7 :     final maybeFinder = _internalHolder?.finders[finder]?.call(this);
     137           4 :     final finderFn = maybeFinder is DataFinderOne<T> ? maybeFinder : findOne;
     138             : 
     139             :     // we can't use `findOne`'s default internal label
     140             :     // because we need a label to handle events
     141             :     label ??=
     142           6 :         DataRequestLabel('watchOne', id: key.detypify(), type: internalType);
     143             : 
     144             :     var alsoWatchPairs = <List<String>>{};
     145             : 
     146             :     // closure to get latest model and watchable relationship pairs
     147           2 :     T? _getUpdatedModel() {
     148           4 :       final model = localAdapter.findOne(key);
     149             :       if (model != null) {
     150             :         // get all metas provided via `alsoWatch`
     151             :         final metas = alsoWatch
     152           2 :             ?.call(RelationshipGraphNode<T>())
     153           1 :             .whereType<RelationshipMeta>();
     154             : 
     155             :         // recursively get applicable watch key pairs for each meta -
     156             :         // from top to bottom (e.g. `p`, `p.familia`, `p.familia.cottage`)
     157             :         alsoWatchPairs = {
     158           1 :           ...?metas
     159           4 :               ?.map((meta) => _getPairsForMeta(meta._top, model))
     160           1 :               .filterNulls
     161           2 :               .expand((_) => _)
     162             :         };
     163             :       } else {
     164             :         // if there is no model nothing should be watched, reset pairs
     165             :         alsoWatchPairs = {};
     166             :       }
     167             :       return model;
     168             :     }
     169             : 
     170           2 :     final notifier = DataStateNotifier<T?>(
     171           4 :       data: DataState(_getUpdatedModel(), isLoading: remote!),
     172             :     );
     173             : 
     174             :     final alsoWatchNames = alsoWatch
     175           2 :             ?.call(RelationshipGraphNode<T>())
     176           1 :             .whereType<RelationshipMeta>()
     177           3 :             .map((m) => m.name) ??
     178             :         {};
     179           2 :     log(label,
     180           6 :         'initializing${alsoWatchNames.isNotEmpty ? ' (and also watching: ${alsoWatchNames.join(', ')})' : ''}');
     181             : 
     182           4 :     notifier._reloadFn = () async {
     183           2 :       if (!notifier.mounted || id == null) return;
     184             : 
     185             :       if (remote!) {
     186           1 :         notifier.updateWith(isLoading: true);
     187             :       }
     188             : 
     189           4 :       final model = await finderFn(
     190             :         id,
     191             :         remote: remote,
     192             :         params: params,
     193             :         headers: headers,
     194             :         label: label,
     195           1 :         onError: (e, label, _) async {
     196             :           try {
     197           1 :             await onError<T>(e, label);
     198           1 :           } on DataException catch (err) {
     199             :             e = err;
     200             :           } catch (_) {
     201             :             rethrow;
     202             :           }
     203           1 :           if (notifier.mounted) {
     204           1 :             notifier.updateWith(isLoading: false, exception: e);
     205             :           }
     206             :           return null;
     207             :         },
     208             :       );
     209             :       // trigger doneLoading to ensure state is updated with isLoading=false
     210           2 :       final modelKey = model?._key;
     211             :       if (remote && modelKey != null) {
     212           4 :         graph._notify([modelKey, label.toString()],
     213             :             type: DataGraphEventType.doneLoading);
     214             :       }
     215             :     };
     216             : 
     217             :     // trigger local + async loading
     218           2 :     notifier.reload();
     219             : 
     220             :     // local buffer useful to reduce amount of notifier updates
     221           4 :     var bufferModel = notifier.data.model;
     222             : 
     223             :     late DelayedStateNotifier<List<DataGraphEvent>> throttledGraph;
     224             : 
     225           6 :     final throttleDuration = read(graphNotifierThrottleDurationProvider);
     226             :     if (throttleDuration != null) {
     227           0 :       throttledGraph = graph.throttle(() => throttleDuration);
     228             :     } else {
     229             :       // if no throttle is required, use map to
     230             :       // convert a single event in a list of events
     231           8 :       throttledGraph = graph.map((_) => [_]);
     232             :     }
     233             : 
     234           2 :     final states = <DataState<T?>>[];
     235             : 
     236             :     // start listening to graph for further changes
     237           4 :     final dispose = throttledGraph.addListener((events) {
     238           2 :       if (!notifier.mounted) return;
     239             : 
     240             :       // get the latest updated model with watchable relationships
     241             :       // (_alsoWatchPairs) in order to determine whether there is
     242             :       // something that will cause an event (with the introduction
     243             :       // of `andEach` even seemingly unrelated models could trigger)
     244           2 :       bufferModel = _getUpdatedModel();
     245             : 
     246           2 :       key = bufferModel?._key ?? key;
     247             : 
     248           4 :       for (final event in events) {
     249           4 :         if (event.keys.contains(key)) {
     250             :           // handle done loading
     251           4 :           if (notifier.data.isLoading &&
     252           4 :               event.keys.last == label.toString() &&
     253           2 :               event.type == DataGraphEventType.doneLoading) {
     254             :             states
     255           2 :                 .add(DataState(bufferModel, isLoading: false, exception: null));
     256             :           }
     257             : 
     258             :           // add/update
     259           4 :           if (event.type == DataGraphEventType.addNode ||
     260           4 :               event.type == DataGraphEventType.updateNode) {
     261           6 :             if (notifier.data.isLoading == false) {
     262           6 :               log(label!, 'added/updated node ${event.keys}', logLevel: 2);
     263           4 :               states.add(DataState(
     264             :                 bufferModel,
     265           4 :                 isLoading: notifier.data.isLoading,
     266           4 :                 exception: notifier.data.exception,
     267           4 :                 stackTrace: notifier.data.stackTrace,
     268             :               ));
     269             :             }
     270             :           }
     271             : 
     272             :           // temporarily restore removed pair so that watchedRelationshipUpdate
     273             :           // has a chance to apply the update
     274           4 :           if (event.type == DataGraphEventType.removeEdge &&
     275           3 :               !event.keys.first.startsWith('id:')) {
     276           2 :             alsoWatchPairs.add(event.keys);
     277             :           }
     278             :         }
     279             : 
     280             :         // handle deletion
     281           4 :         if (event.type == DataGraphEventType.removeNode &&
     282             :             bufferModel == null) {
     283           3 :           log(label!, 'removed node ${event.keys}', logLevel: 2);
     284           2 :           states.add(DataState(
     285             :             null,
     286           2 :             isLoading: notifier.data.isLoading,
     287           2 :             exception: notifier.data.exception,
     288           2 :             stackTrace: notifier.data.stackTrace,
     289             :           ));
     290             :         }
     291             : 
     292             :         // updates on watched relationships condition
     293           4 :         final watchedRelationshipUpdate = event.type.isEdge &&
     294             :             alsoWatchPairs
     295           2 :                 .where((pair) =>
     296           6 :                     pair.sorted().toString() == event.keys.sorted().toString())
     297           1 :                 .isNotEmpty;
     298             : 
     299             :         // updates on watched models (of relationships) condition
     300           4 :         final watchedModelUpdate = event.type.isNode &&
     301             :             alsoWatchPairs
     302           6 :                 .where((pair) => pair.contains(event.keys.first))
     303           2 :                 .isNotEmpty;
     304             : 
     305             :         // if model is loaded and any condition passes, notify update
     306           6 :         if (notifier.data.isLoading == false &&
     307             :             (watchedRelationshipUpdate || watchedModelUpdate)) {
     308           3 :           log(label!, 'relationship update ${event.keys}', logLevel: 2);
     309           2 :           states.add(DataState(
     310             :             bufferModel,
     311           2 :             isLoading: notifier.data.isLoading,
     312           2 :             exception: notifier.data.exception,
     313           2 :             stackTrace: notifier.data.stackTrace,
     314             :           ));
     315             :         }
     316             :       }
     317             : 
     318           2 :       _updateFromStates(states, notifier);
     319             :     });
     320             : 
     321           4 :     notifier.onDispose = () {
     322           2 :       log(label!, 'disposing');
     323           2 :       dispose();
     324             :     };
     325             :     return notifier;
     326             :   }
     327             : 
     328           3 :   void _updateFromStates<S>(
     329             :       List<DataState<S>> states, DataStateNotifier<S> notifier) {
     330           6 :     final mergedState = states.fold<DataState<S>?>(null, (acc, state) {
     331           0 :       return acc == null ? state : acc.merge(state);
     332             :     });
     333           3 :     states.clear();
     334             : 
     335             :     if (mergedState != null) {
     336           3 :       notifier.updateWith(
     337           3 :         model: mergedState.model,
     338           3 :         isLoading: mergedState.isLoading,
     339           3 :         exception: mergedState.exception,
     340           3 :         stackTrace: mergedState.stackTrace,
     341             :       );
     342             :     }
     343             :   }
     344             : 
     345           1 :   Iterable<List<String>> _getPairsForMeta(
     346             :       RelationshipMeta? meta, DataModel model) {
     347             :     // get instance of this relationship
     348           2 :     final relationship = meta?.instance(model);
     349             :     if (relationship == null) return {};
     350             : 
     351             :     return {
     352             :       // include key pairs of (owner, key)
     353           3 :       for (final key in relationship._keys) [relationship._ownerKey!, key],
     354             :       // recursively include key pairs for other requested relationships
     355           1 :       for (final childModel in relationship._iterable)
     356           2 :         _getPairsForMeta(meta!.child, childModel as DataModel)
     357           2 :             .expand((_) => _)
     358           1 :             .toList()
     359             :     };
     360             :   }
     361             : }
     362             : 
     363           6 : final graphNotifierThrottleDurationProvider =
     364           6 :     Provider<Duration?>((ref) => null);

Generated by: LCOV version 1.15