LCOV - code coverage report
Current view: top level - repository - remote_adapter_offline.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 115 126 91.3 %
Date: 2021-06-18 12:41:16 Functions: 0 0 -

          Line data    Source code
       1             : part of flutter_data;
       2             : 
       3             : const _offlineAdapterKey = '_offline:keys';
       4             : 
       5             : mixin _RemoteAdapterOffline<T extends DataModel<T>> on _RemoteAdapter<T> {
       6             :   @override
       7             :   @mustCallSuper
       8           1 :   Future<void> onInitialized() async {
       9           2 :     await super.onInitialized();
      10             :     // wipe out orphans
      11           2 :     graph.removeOrphanNodes();
      12             :     // ensure offline nodes exist
      13           2 :     if (!graph._hasNode(_offlineAdapterKey)) {
      14           2 :       graph._addNode(_offlineAdapterKey);
      15             :     }
      16             :   }
      17             : 
      18             :   @override
      19           1 :   FutureOr<R?> sendRequest<R>(
      20             :     final Uri uri, {
      21             :     DataRequestMethod method = DataRequestMethod.GET,
      22             :     Map<String, String>? headers,
      23             :     bool omitDefaultParams = false,
      24             :     DataRequestType requestType = DataRequestType.adhoc,
      25             :     String? key,
      26             :     String? body,
      27             :     OnRawData<R>? onSuccess,
      28             :     OnDataError<R>? onError,
      29             :   }) async {
      30             :     // default key to type#s3mth1ng
      31           2 :     final offlineKey = key ?? DataHelpers.generateKey(internalType);
      32             :     assert(offlineKey.startsWith(internalType));
      33             : 
      34             :     // execute request
      35           2 :     return await super.sendRequest<R>(
      36             :       uri,
      37             :       method: method,
      38             :       headers: headers,
      39             :       requestType: requestType,
      40             :       omitDefaultParams: omitDefaultParams,
      41             :       key: key,
      42             :       body: body,
      43           1 :       onSuccess: (data) {
      44             :         // remove all operations with this
      45             :         // requestType/offlineKey metadata
      46           1 :         OfflineOperation<T>(
      47             :           requestType: requestType,
      48             :           offlineKey: offlineKey,
      49           2 :           request: '${method.toShortString()} $uri',
      50             :           body: body,
      51             :           headers: headers,
      52             :           onSuccess: onSuccess,
      53             :           onError: onError,
      54             :           adapter: this,
      55           1 :         ).remove();
      56             : 
      57             :         // yield
      58           1 :         return onSuccess?.call(data);
      59             :       },
      60           1 :       onError: (e) {
      61           2 :         if (isNetworkError(e.error)) {
      62             :           // queue a new operation if this is
      63             :           // a network error and we're offline
      64           1 :           OfflineOperation<T>(
      65             :             requestType: requestType,
      66             :             offlineKey: offlineKey,
      67           2 :             request: '${method.toShortString()} $uri',
      68             :             body: body,
      69             :             headers: headers,
      70             :             onSuccess: onSuccess,
      71             :             onError: onError,
      72             :             adapter: this,
      73           1 :           ).add();
      74             : 
      75             :           // wrap error in an OfflineException
      76           2 :           e = OfflineException(error: e.error);
      77             : 
      78             :           // call error handler but do not return it
      79           2 :           (onError ?? this.onError).call(e);
      80             : 
      81             :           // instead return a fallback model
      82             :           switch (requestType) {
      83           1 :             case DataRequestType.findAll:
      84           1 :               return findAll(remote: false, syncLocal: false) as Future<R>;
      85           1 :             case DataRequestType.findOne:
      86           1 :             case DataRequestType.save:
      87             :               // call without type (ie 3 not users#3)
      88             :               // key! as we know findOne does pass it
      89           2 :               return findOne(key!.detypify(), remote: false) as Future<R?>;
      90             :             default:
      91             :               return null;
      92             :           }
      93             :         }
      94             : 
      95             :         // if it was not a network error
      96             : 
      97             :         // remove all operations with this
      98             :         // requestType/offlineKey metadata
      99           1 :         OfflineOperation<T>(
     100             :           requestType: requestType,
     101             :           offlineKey: offlineKey,
     102           2 :           request: '${method.toShortString()} $uri',
     103             :           body: body,
     104             :           headers: headers,
     105             :           onSuccess: onSuccess,
     106             :           onError: onError,
     107             :           adapter: this,
     108           1 :         ).remove();
     109             : 
     110             :         // return handler call
     111           2 :         return (onError ?? this.onError).call(e);
     112             :       },
     113             :     );
     114             :   }
     115             : 
     116             :   /// Determines whether [error] was a network error.
     117           1 :   @protected
     118             :   @visibleForTesting
     119             :   bool isNetworkError(error) {
     120             :     // timeouts via http's `connectionTimeout` are
     121             :     // also socket exceptions
     122             :     // we check the exception like this in order not to import `dart:io`
     123           1 :     final _err = error.toString();
     124           1 :     return _err.startsWith('SocketException') ||
     125           1 :         _err.startsWith('Connection closed before full header was received') ||
     126           1 :         _err.startsWith('HandshakeException');
     127             :   }
     128             : 
     129           1 :   @protected
     130             :   @visibleForTesting
     131             :   Set<OfflineOperation<T>> get offlineOperations {
     132           2 :     final node = graph._getNode(_offlineAdapterKey);
     133           3 :     return (node ?? {}).entries.where((e) {
     134             :       // extract type from e.g. _offline:users#4:findOne
     135           5 :       return e.key.split(':')[1].startsWith(internalType);
     136           2 :     }).map((e) {
     137             :       // get first edge value
     138           3 :       final map = json.decode(e.value.first) as Map<String, dynamic>;
     139           1 :       return OfflineOperation.fromJson(map, this);
     140           1 :     }).toSet();
     141             :   }
     142             : }
     143             : 
     144             : /// Represents an offline request that is pending to be retried.
     145             : class OfflineOperation<T extends DataModel<T>> with EquatableMixin {
     146             :   final String offlineKey;
     147             :   final DataRequestType requestType;
     148             :   final String request;
     149             :   final Map<String, String>? headers;
     150             :   final String? body;
     151             :   final OnRawData? onSuccess;
     152             :   final OnDataError? onError;
     153             :   final _RemoteAdapterOffline<T> adapter;
     154             : 
     155           1 :   const OfflineOperation({
     156             :     required this.offlineKey,
     157             :     required this.requestType,
     158             :     required this.request,
     159             :     this.headers,
     160             :     this.body,
     161             :     this.onSuccess,
     162             :     this.onError,
     163             :     required this.adapter,
     164             :   });
     165             : 
     166             :   /// Metadata format:
     167             :   /// _offline:users:d7bcc9a7b72bf90fffd826
     168           5 :   String get metadata => '_offline:${adapter.internalType}:$hash';
     169             : 
     170           1 :   Map<String, dynamic> toJson() {
     171           1 :     return <String, dynamic>{
     172           2 :       't': requestType.toShortString(),
     173           1 :       'r': request,
     174           1 :       'k': offlineKey,
     175           1 :       'b': body,
     176           1 :       'h': headers,
     177             :     };
     178             :   }
     179             : 
     180           1 :   factory OfflineOperation.fromJson(
     181             :       Map<String, dynamic> json, _RemoteAdapterOffline<T> adapter) {
     182           1 :     return OfflineOperation(
     183           2 :       requestType: _getDataRequestType(json['t'] as String),
     184           1 :       request: json['r'] as String,
     185           1 :       offlineKey: json['k'] as String,
     186           1 :       body: json['b'] as String?,
     187             :       headers:
     188           3 :           json['h'] == null ? null : Map<String, String>.from(json['h'] as Map),
     189             :       adapter: adapter,
     190             :     );
     191             :   }
     192             : 
     193           1 :   Uri get uri {
     194           4 :     return request.split(' ').last.asUri;
     195             :   }
     196             : 
     197           1 :   DataRequestMethod get method {
     198             :     return DataRequestMethod.values
     199           7 :         .singleWhere((m) => m.toShortString() == request.split(' ').first);
     200             :   }
     201             : 
     202             :   /// Adds an edge from the `_offlineAdapterKey` to the `key` for save/delete
     203             :   /// and stores header/param metadata. Also stores callbacks.
     204           1 :   void add() {
     205             :     // DO NOT proceed if operation is in queue
     206           3 :     if (!adapter.offlineOperations.contains(this)) {
     207           2 :       final node = json.encode(toJson());
     208             : 
     209           2 :       if (adapter._verbose) {
     210           0 :         print(
     211           0 :             '[flutter_data] [${adapter.internalType}] Adding offline operation with metadata: $metadata');
     212           0 :         print('\n\n');
     213             :       }
     214             : 
     215           4 :       adapter.graph._addEdge(_offlineAdapterKey, node, metadata: metadata);
     216             : 
     217             :       // keep callbacks in memory
     218           8 :       adapter.ref.read(_offlineCallbackProvider).state[metadata] ??= [];
     219           2 :       adapter.ref
     220           2 :           .read(_offlineCallbackProvider)
     221           3 :           .state[metadata]!
     222           4 :           .add([onSuccess, onError]);
     223             :     } else {
     224             :       // trick
     225           2 :       adapter.graph
     226           2 :           ._notify([_offlineAdapterKey, ''], DataGraphEventType.addEdge);
     227             :     }
     228             :   }
     229             : 
     230             :   /// Removes all edges from the `_offlineAdapterKey` for
     231             :   /// current metadata, as well as callbacks from memory.
     232           1 :   void remove() {
     233           4 :     if (adapter.graph._hasEdge(_offlineAdapterKey, metadata: metadata)) {
     234           4 :       adapter.graph._removeEdges(_offlineAdapterKey, metadata: metadata);
     235           2 :       if (adapter._verbose) {
     236           0 :         print(
     237           0 :             '[flutter_data] [${adapter.internalType}] Removing offline operation with metadata: $metadata');
     238           0 :         print('\n\n');
     239             :       }
     240             : 
     241           7 :       adapter.ref.read(_offlineCallbackProvider).state.remove(metadata);
     242             :     }
     243             :   }
     244             : 
     245           1 :   Future<void> retry<R>() async {
     246             :     // look up callbacks (or provide defaults)
     247           7 :     final fns = adapter.ref.read(_offlineCallbackProvider).state[metadata] ??
     248           0 :         [
     249           0 :           [null, null]
     250             :         ];
     251             : 
     252           2 :     for (final pair in fns) {
     253           3 :       await adapter.sendRequest<R>(
     254           1 :         uri,
     255           1 :         method: method,
     256           1 :         headers: headers,
     257           1 :         requestType: requestType,
     258           1 :         key: offlineKey,
     259           1 :         body: body,
     260           1 :         onSuccess: pair.first as OnRawData<R>?,
     261           1 :         onError: pair.last as OnDataError<R>?,
     262             :       );
     263             :     }
     264             :   }
     265             : 
     266             :   /// This getter ONLY makes sense for `findOne` and `save` operations
     267           1 :   T? get model {
     268           1 :     switch (requestType) {
     269           1 :       case DataRequestType.findOne:
     270           0 :         return adapter.localAdapter.findOne(adapter.graph
     271           0 :             .getKeyForId(adapter.internalType, offlineKey.detypify())!);
     272           1 :       case DataRequestType.save:
     273           4 :         return adapter.localAdapter.findOne(offlineKey);
     274             :       default:
     275             :         return null;
     276             :     }
     277             :   }
     278             : 
     279           1 :   @override
     280           6 :   List<Object?> get props => [requestType, request, body, offlineKey, headers];
     281             : 
     282           1 :   @override
     283             :   bool get stringify => true;
     284             : 
     285             :   // generates a unique memory-independent hash of this object
     286           5 :   String get hash => md5.convert(utf8.encode(toString())).toString();
     287             : }
     288             : 
     289             : extension OfflineOperationsX on Set<OfflineOperation<DataModel>> {
     290             :   /// Retries all offline operations for current type.
     291           1 :   FutureOr<void> retry() async {
     292           1 :     if (isNotEmpty) {
     293           4 :       await Future.wait(map((operation) {
     294           1 :         return operation.retry();
     295             :       }));
     296             :     }
     297             :   }
     298             : 
     299             :   /// Removes all offline operations.
     300           1 :   void reset() {
     301           1 :     if (isEmpty) {
     302             :       return;
     303             :     }
     304           2 :     final adapter = first.adapter;
     305             :     // removes node and severs edges
     306           2 :     final node = adapter.graph._getNode(_offlineAdapterKey);
     307           3 :     for (final metadata in (node ?? {}).keys.toImmutableList()) {
     308           2 :       adapter.graph._removeEdges(_offlineAdapterKey, metadata: metadata);
     309             :     }
     310           5 :     adapter.ref.read(_offlineCallbackProvider).state.clear();
     311             :   }
     312             : 
     313             :   /// Filter by [requestType].
     314           1 :   List<OfflineOperation> only(DataRequestType requestType) {
     315           5 :     return where((_) => _.requestType == requestType).toImmutableList();
     316             :   }
     317             : }
     318             : 
     319             : // stores onSuccess/onError function combos
     320           2 : final _offlineCallbackProvider =
     321           3 :     StateProvider<Map<String, List<List<Function?>>>>((_) => {});
     322             : 
     323             : /// Every time there is an offline operation added to/
     324             : /// removed from the queue, this will notify clients
     325             : /// with all pending types (could be none) such that
     326             : /// they can implement their own retry strategy.
     327           0 : final pendingOfflineTypesProvider =
     328             :     StateNotifierProvider<ValueStateNotifier<Set<String>>, Set<String>>((ref) {
     329             :   final _graph = ref.read(graphNotifierProvider);
     330             : 
     331             :   Set<String> _pendingTypes() {
     332             :     final node = _graph._getNode(_offlineAdapterKey)!;
     333             :     // obtain types from metadata e.g. _offline:users#4:findOne
     334             :     return node.keys.map((m) => m.split(':')[1].split('#')[0]).toSet();
     335             :   }
     336             : 
     337             :   final notifier = ValueStateNotifier(<String>{});
     338             :   // emit initial value
     339             :   Timer.run(() {
     340             :     if (notifier.mounted) {
     341             :       notifier.value = _pendingTypes();
     342             :     }
     343             :   });
     344             : 
     345             :   final _dispose = _graph.where((event) {
     346             :     // filter the right events
     347             :     return [DataGraphEventType.addEdge, DataGraphEventType.removeEdge]
     348             :             .contains(event.type) &&
     349             :         event.keys.length == 2 &&
     350             :         event.keys.containsFirst(_offlineAdapterKey);
     351             :   }).addListener((_) {
     352             :     if (notifier.mounted) {
     353             :       // recalculate all pending types
     354             :       notifier.value = _pendingTypes();
     355             :     }
     356             :   });
     357             : 
     358             :   notifier.onDispose = _dispose;
     359             : 
     360             :   return notifier;
     361             : });

Generated by: LCOV version 1.15