LCOV - code coverage report
Current view: top level - repository - remote_adapter.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 225 237 94.9 %
Date: 2022-05-06 22:54:19 Functions: 0 0 -

          Line data    Source code
       1             : part of flutter_data;
       2             : 
       3             : /// An adapter base class for all remote operations for type [T].
       4             : ///
       5             : /// Includes:
       6             : ///
       7             : ///  - Remote methods such as [_RemoteAdapter.findAll] or [_RemoteAdapter.save]
       8             : ///  - Configuration methods and getters like [_RemoteAdapter.baseUrl] or [_RemoteAdapter.urlForFindAll]
       9             : ///  - Serialization methods like [_RemoteAdapterSerialization.serialize]
      10             : ///  - Watch methods such as [Repository.watchOneNotifier]
      11             : ///  - Access to the [_RemoteAdapter.graph] for subclasses or mixins
      12             : ///
      13             : /// This class is meant to be extended via mixing in new adapters.
      14             : /// This can be done with the [DataRepository] annotation on a [DataModel] class:
      15             : ///
      16             : /// ```
      17             : /// @JsonSerializable()
      18             : /// @DataRepository([MyAppAdapter])
      19             : /// class Todo with DataModel<Todo> {
      20             : ///   @override
      21             : ///   final int? id;
      22             : ///   final String title;
      23             : ///   final bool completed;
      24             : ///
      25             : ///   Todo({this.id, required this.title, this.completed = false});
      26             : /// }
      27             : /// ```
      28             : ///
      29             : /// Identity in this layer is enforced by IDs.
      30             : class RemoteAdapter<T extends DataModel<T>> = _RemoteAdapter<T>
      31             :     with _RemoteAdapterSerialization<T>, _RemoteAdapterWatch<T>;
      32             : 
      33             : abstract class _RemoteAdapter<T extends DataModel<T>> with _Lifecycle {
      34           1 :   @protected
      35             :   _RemoteAdapter(this.localAdapter, [this._internalHolder]);
      36             : 
      37             :   @protected
      38             :   @visibleForTesting
      39             :   @nonVirtual
      40             :   final LocalAdapter<T> localAdapter;
      41             : 
      42             :   /// A [GraphNotifier] instance also available to adapters
      43           1 :   @protected
      44             :   @nonVirtual
      45           2 :   GraphNotifier get graph => localAdapter.graph;
      46             : 
      47             :   // None of these fields below can be late finals as they might be re-initialized
      48             :   Map<String, RemoteAdapter>? _adapters;
      49             :   bool? _remote;
      50             :   Reader? _read;
      51             : 
      52             :   /// All adapters for the relationship subgraph of [T] and their relationships.
      53             :   ///
      54             :   /// This [Map] is typically required when initializing new models, and passed as-is.
      55           1 :   @protected
      56             :   @nonVirtual
      57           1 :   Map<String, RemoteAdapter> get adapters => _adapters!;
      58             : 
      59             :   /// Give access to the dependency injection system
      60           1 :   @nonVirtual
      61           1 :   Reader get read => _read!;
      62             : 
      63             :   /// INTERNAL: DO NOT USE
      64           1 :   @visibleForTesting
      65             :   @protected
      66             :   @nonVirtual
      67           1 :   String get internalType => DataHelpers.getType<T>();
      68             : 
      69             :   /// The pluralized and downcased [DataHelpers.getType<T>] version of type [T]
      70             :   /// by default.
      71             :   ///
      72             :   /// Example: [T] as `Post` has a [type] of `posts`.
      73           2 :   String get type => internalType;
      74             : 
      75             :   /// ONLY FOR FLUTTER DATA INTERNAL USE
      76             :   Watcher? internalWatch;
      77             :   final InternalHolder<T>? _internalHolder;
      78             : 
      79             :   /// Turn verbosity on or off.
      80             :   // ignore: prefer_final_fields
      81             :   bool _verbose = false;
      82             : 
      83           1 :   bool get autoInitializeModels => true;
      84             : 
      85             :   /// Returns the base URL for this type [T].
      86             :   ///
      87             :   /// Typically used in a generic adapter (i.e. one shared by all types)
      88             :   /// so it should be e.g. `http://jsonplaceholder.typicode.com/`
      89             :   ///
      90             :   /// For specific paths to this type [T], see [urlForFindAll], [urlForFindOne], etc
      91           1 :   @protected
      92             :   String get baseUrl => 'https://override-base-url-in-adapter/';
      93             : 
      94             :   /// Returns URL for [findAll]. Defaults to [type].
      95           1 :   @protected
      96           2 :   String urlForFindAll(Map<String, dynamic> params) => '$type';
      97             : 
      98             :   /// Returns HTTP method for [findAll]. Defaults to `GET`.
      99           1 :   @protected
     100             :   DataRequestMethod methodForFindAll(Map<String, dynamic> params) =>
     101             :       DataRequestMethod.GET;
     102             : 
     103             :   /// Returns URL for [findOne]. Defaults to [type]/[id].
     104           1 :   @protected
     105           2 :   String urlForFindOne(id, Map<String, dynamic> params) => '$type/$id';
     106             : 
     107             :   /// Returns HTTP method for [findOne]. Defaults to `GET`.
     108           1 :   @protected
     109             :   DataRequestMethod methodForFindOne(id, Map<String, dynamic> params) =>
     110             :       DataRequestMethod.GET;
     111             : 
     112             :   /// Returns URL for [save]. Defaults to [type]/[id] (if [id] is present).
     113           1 :   @protected
     114             :   String urlForSave(id, Map<String, dynamic> params) =>
     115           3 :       id != null ? '$type/$id' : type;
     116             : 
     117             :   /// Returns HTTP method for [save]. Defaults to `PATCH` if [id] is present,
     118             :   /// or `POST` otherwise.
     119           1 :   @protected
     120             :   DataRequestMethod methodForSave(id, Map<String, dynamic> params) =>
     121             :       id != null ? DataRequestMethod.PATCH : DataRequestMethod.POST;
     122             : 
     123             :   /// Returns URL for [delete]. Defaults to [type]/[id].
     124           1 :   @protected
     125           2 :   String urlForDelete(id, Map<String, dynamic> params) => '$type/$id';
     126             : 
     127             :   /// Returns HTTP method for [delete]. Defaults to `DELETE`.
     128           1 :   @protected
     129             :   DataRequestMethod methodForDelete(id, Map<String, dynamic> params) =>
     130             :       DataRequestMethod.DELETE;
     131             : 
     132             :   /// A [Map] representing default HTTP query parameters. Defaults to empty.
     133             :   ///
     134             :   /// It can return a [Future], so that adapters overriding this method
     135             :   /// have a chance to call async methods.
     136             :   ///
     137             :   /// Example:
     138             :   /// ```
     139             :   /// @override
     140             :   /// FutureOr<Map<String, dynamic>> get defaultParams async {
     141             :   ///   final token = await _localStorage.get('token');
     142             :   ///   return await super.defaultParams..addAll({'token': token});
     143             :   /// }
     144             :   /// ```
     145           1 :   @protected
     146           1 :   FutureOr<Map<String, dynamic>> get defaultParams => {};
     147             : 
     148             :   /// A [Map] representing default HTTP headers.
     149             :   ///
     150             :   /// Initial default is: `{'Content-Type': 'application/json'}`.
     151             :   ///
     152             :   /// It can return a [Future], so that adapters overriding this method
     153             :   /// have a chance to call async methods.
     154             :   ///
     155             :   /// Example:
     156             :   /// ```
     157             :   /// @override
     158             :   /// FutureOr<Map<String, String>> get defaultHeaders async {
     159             :   ///   final token = await _localStorage.get('token');
     160             :   ///   return await super.defaultHeaders..addAll({'Authorization': token});
     161             :   /// }
     162             :   /// ```
     163           1 :   @protected
     164             :   FutureOr<Map<String, String>> get defaultHeaders =>
     165           1 :       {'Content-Type': 'application/json'};
     166             : 
     167             :   // lifecycle methods
     168             : 
     169             :   @mustCallSuper
     170           1 :   Future<void> onInitialized() async {
     171             :     // wipe out orphans
     172           2 :     graph.removeOrphanNodes();
     173             :     // ensure offline nodes exist
     174           2 :     if (!graph.hasNode(_offlineAdapterKey)) {
     175           2 :       graph.addNode(_offlineAdapterKey);
     176             :     }
     177             :   }
     178             : 
     179             :   @mustCallSuper
     180             :   @nonVirtual
     181           1 :   Future<RemoteAdapter<T>> initialize(
     182             :       {bool? remote,
     183             :       required Map<String, RemoteAdapter> adapters,
     184             :       required Reader read}) async {
     185           1 :     if (isInitialized) return this as RemoteAdapter<T>;
     186             : 
     187             :     // initialize attributes
     188           1 :     _adapters = adapters;
     189           1 :     _remote = remote ?? true;
     190           1 :     _read = read;
     191             : 
     192           3 :     await localAdapter.initialize();
     193             : 
     194             :     // hook for clients
     195           2 :     await onInitialized();
     196             : 
     197             :     return this as RemoteAdapter<T>;
     198             :   }
     199             : 
     200           1 :   @override
     201           2 :   bool get isInitialized => localAdapter.isInitialized;
     202             : 
     203             :   /// ONLY FOR FLUTTER DATA INTERNAL USE
     204           0 :   Future<void> internalInitializeModels() async {
     205           0 :     final models = localAdapter.findAll();
     206             :     if (models != null) {
     207           0 :       for (final model in models) {
     208           0 :         model.init(save: false);
     209             :       }
     210             :     }
     211             :   }
     212             : 
     213           1 :   @override
     214             :   void dispose() {
     215           2 :     localAdapter.dispose();
     216             :   }
     217             : 
     218             :   // serialization interface
     219             : 
     220             :   /// Returns a [DeserializedData] object when deserializing a given [data].
     221             :   @protected
     222             :   @visibleForTesting
     223             :   DeserializedData<T> deserialize(Object? data);
     224             : 
     225             :   /// Returns a serialized version of a model of [T],
     226             :   /// as a [Map<String, dynamic>] ready to be JSON-encoded.
     227             :   @protected
     228             :   @visibleForTesting
     229             :   Map<String, dynamic> serialize(T model);
     230             : 
     231             :   // caching
     232             : 
     233             :   /// Returns whether calling [findAll] should trigger a remote call.
     234             :   ///
     235             :   /// Meant to be overriden. Defaults to [remote].
     236           1 :   @protected
     237             :   bool shouldLoadRemoteAll(
     238             :     bool remote,
     239             :     Map<String, dynamic> params,
     240             :     Map<String, String> headers,
     241             :   ) =>
     242             :       remote;
     243             : 
     244             :   /// Returns whether calling [findOne] should initiate an HTTP call.
     245             :   ///
     246             :   /// Meant to be overriden. Defaults to [remote].
     247           1 :   @protected
     248             :   bool shouldLoadRemoteOne(
     249             :     Object? id,
     250             :     bool remote,
     251             :     Map<String, dynamic> params,
     252             :     Map<String, String> headers,
     253             :   ) =>
     254             :       remote;
     255             : 
     256             :   // remote implementation
     257             : 
     258           1 :   Future<List<T>?> findAll({
     259             :     bool? remote,
     260             :     bool? background,
     261             :     Map<String, dynamic>? params,
     262             :     Map<String, String>? headers,
     263             :     bool? syncLocal,
     264             :     OnSuccessAll<T>? onSuccess,
     265             :     OnErrorAll<T>? onError,
     266             :     DataRequestLabel? label,
     267             :   }) async {
     268           1 :     remote ??= _remote;
     269             :     background ??= false;
     270             :     syncLocal ??= false;
     271           3 :     params = await defaultParams & params;
     272           3 :     headers = await defaultHeaders & headers;
     273             : 
     274           2 :     label = DataRequestLabel('findAll', type: internalType, withParent: label);
     275             : 
     276             :     late List<T>? models;
     277             : 
     278           1 :     if (!shouldLoadRemoteAll(remote!, params, headers) || background) {
     279           3 :       models = localAdapter.findAll()?.toImmutableList();
     280             :       if (models != null) {
     281           1 :         log(label,
     282           2 :             'returned ${models.toShortLog()} from local storage${background ? ' and loading in the background' : ''}');
     283             :       }
     284             :       if (!background) {
     285             :         return models;
     286             :       }
     287             :     }
     288             : 
     289           4 :     log(label, 'request ${params.isNotEmpty ? 'with $params' : ''}');
     290             : 
     291           1 :     final future = sendRequest<List<T>>(
     292           5 :       baseUrl.asUri / urlForFindAll(params) & params,
     293           1 :       method: methodForFindAll(params),
     294             :       headers: headers,
     295             :       label: label,
     296           1 :       onSuccess: (data, label) async {
     297             :         if (syncLocal!) {
     298           3 :           await localAdapter.clear();
     299             :         }
     300           2 :         onSuccess ??= (data, label, _) => this.onSuccess<List<T>>(data, label);
     301           1 :         return onSuccess!.call(data, label, this as RemoteAdapter<T>);
     302             :       },
     303           1 :       onError: (e, label) async {
     304           2 :         onError ??= (e, label, _) => this.onError<List<T>>(e, label);
     305           1 :         return onError!.call(e, label, this as RemoteAdapter<T>);
     306             :       },
     307             :     );
     308             : 
     309             :     if (background && models != null) {
     310             :       // ignore: unawaited_futures
     311           0 :       future.then((_) => Future.value(_));
     312             :       return models;
     313             :     } else {
     314           2 :       return await future ?? <T>[];
     315             :     }
     316             :   }
     317             : 
     318           1 :   Future<T?> findOne(
     319             :     Object id, {
     320             :     bool? remote,
     321             :     bool? background,
     322             :     Map<String, dynamic>? params,
     323             :     Map<String, String>? headers,
     324             :     OnSuccessOne<T>? onSuccess,
     325             :     OnErrorOne<T>? onError,
     326             :     DataRequestLabel? label,
     327             :   }) async {
     328           1 :     remote ??= _remote;
     329             :     background ??= false;
     330           3 :     params = await defaultParams & params;
     331           3 :     headers = await defaultHeaders & headers;
     332             : 
     333           1 :     final resolvedId = _resolveId(id);
     334             :     late T? model;
     335             : 
     336           1 :     label = DataRequestLabel('findOne',
     337           2 :         type: internalType, id: resolvedId?.toString(), withParent: label);
     338             : 
     339           1 :     if (!shouldLoadRemoteOne(id, remote!, params, headers) || background) {
     340           3 :       final key = graph.getKeyForId(internalType, resolvedId,
     341           1 :           keyIfAbsent: id is T ? id._key : null);
     342           2 :       model = localAdapter.findOne(key);
     343             :       // model?._initialize(adapters);
     344             :       if (model != null) {
     345           1 :         log(label,
     346           1 :             'returned from local storage${background ? ' and loading in the background' : ''}');
     347             :       }
     348             :       if (!background) {
     349             :         return model;
     350             :       }
     351             :     }
     352             : 
     353           4 :     log(label, 'request ${params.isNotEmpty ? 'with $params' : ''}');
     354             : 
     355           1 :     final future = sendRequest(
     356           5 :       baseUrl.asUri / urlForFindOne(id, params) & params,
     357           1 :       method: methodForFindOne(id, params),
     358             :       headers: headers,
     359             :       label: label,
     360           1 :       onSuccess: (data, label) {
     361           2 :         onSuccess ??= (data, label, _) => this.onSuccess<T>(data, label);
     362           1 :         return onSuccess!.call(data, label, this as RemoteAdapter<T>);
     363             :       },
     364           1 :       onError: (e, label) async {
     365           2 :         onError ??= (e, label, _) => this.onError<T>(e, label);
     366           1 :         return onError!.call(e, label, this as RemoteAdapter<T>);
     367             :       },
     368             :     );
     369             : 
     370             :     if (background && model != null) {
     371             :       // ignore: unawaited_futures
     372           0 :       future.then((_) => Future.value(_));
     373             :       return model;
     374             :     } else {
     375           1 :       return await future;
     376             :     }
     377             :   }
     378             : 
     379           0 :   FutureOr<T?> onSuccessOne(Object? data, DataRequestLabel? label) =>
     380           0 :       onSuccess<T>(data, label);
     381             : 
     382           1 :   Future<T> save(
     383             :     T model, {
     384             :     bool? remote,
     385             :     Map<String, dynamic>? params,
     386             :     Map<String, String>? headers,
     387             :     OnSuccessOne<T>? onSuccess,
     388             :     OnErrorOne<T>? onError,
     389             :     DataRequestLabel? label,
     390             :   }) async {
     391           1 :     remote ??= _remote;
     392             : 
     393           3 :     params = await defaultParams & params;
     394           3 :     headers = await defaultHeaders & headers;
     395             : 
     396             :     // ensure model is saved
     397           4 :     await localAdapter.save(model._key, model);
     398             : 
     399           1 :     label = DataRequestLabel('save',
     400           1 :         type: internalType,
     401           2 :         id: model.id?.toString(),
     402             :         model: model,
     403             :         withParent: label);
     404             : 
     405           1 :     if (remote == false) {
     406           1 :       log(label, 'saved in local storage only');
     407             :       return model;
     408             :     }
     409             : 
     410           1 :     final serialized = serialize(model);
     411           1 :     final body = json.encode(serialized);
     412             : 
     413           1 :     log(label, 'requesting');
     414           2 :     final result = await sendRequest<T>(
     415           6 :       baseUrl.asUri / urlForSave(model.id, params) & params,
     416           2 :       method: methodForSave(model.id, params),
     417             :       headers: headers,
     418             :       body: body,
     419             :       label: label,
     420           1 :       onSuccess: (data, label) {
     421           2 :         onSuccess ??= (data, label, _) => this.onSuccess<T>(data, label);
     422           1 :         return onSuccess!.call(data, label, this as RemoteAdapter<T>);
     423             :       },
     424           1 :       onError: (e, label) async {
     425           2 :         onError ??= (e, label, _) => this.onError<T>(e, label);
     426           1 :         return onError!.call(e, label, this as RemoteAdapter<T>);
     427             :       },
     428             :     );
     429             :     return result ?? model;
     430             :   }
     431             : 
     432           1 :   Future<T?> delete(
     433             :     Object model, {
     434             :     bool? remote,
     435             :     Map<String, dynamic>? params,
     436             :     Map<String, String>? headers,
     437             :     OnSuccessOne<T>? onSuccess,
     438             :     OnErrorOne<T>? onError,
     439             :     DataRequestLabel? label,
     440             :   }) async {
     441           1 :     remote ??= _remote;
     442             : 
     443           3 :     params = await defaultParams & params;
     444           3 :     headers = await defaultHeaders & headers;
     445             : 
     446           1 :     final id = _resolveId(model);
     447           1 :     final key = keyForModelOrId(model);
     448             : 
     449           1 :     label = DataRequestLabel('delete',
     450           2 :         type: internalType, id: id.toString(), withParent: label);
     451             : 
     452             :     if (key != null) {
     453           1 :       if (remote == false) {
     454           1 :         log(label, 'deleted in local storage only');
     455             :       }
     456           3 :       await localAdapter.delete(key);
     457             :     }
     458             : 
     459           1 :     if (remote == true && id != null) {
     460           1 :       log(label, 'requesting');
     461           2 :       return await sendRequest(
     462           5 :         baseUrl.asUri / urlForDelete(id, params) & params,
     463           1 :         method: methodForDelete(id, params),
     464             :         headers: headers,
     465             :         label: label,
     466           1 :         onSuccess: (data, label) {
     467           2 :           onSuccess ??= (data, label, _) => this.onSuccess<T>(data, label);
     468           1 :           return onSuccess!.call(data, label, this as RemoteAdapter<T>);
     469             :         },
     470           1 :         onError: (e, label) async {
     471           0 :           onError ??= (e, label, _) => this.onError<T>(e, label);
     472           1 :           return onError!.call(e, label, this as RemoteAdapter<T>);
     473             :         },
     474             :       );
     475             :     }
     476             :     return null;
     477             :   }
     478             : 
     479           3 :   Future<void> clear() => localAdapter.clear();
     480             : 
     481             :   // http
     482             : 
     483             :   /// An [http.Client] used to make an HTTP request.
     484             :   ///
     485             :   /// This getter returns a new client every time
     486             :   /// as by default they are used once and then closed.
     487           0 :   @protected
     488             :   @visibleForTesting
     489           0 :   http.Client get httpClient => http.Client();
     490             : 
     491             :   /// The function used to perform an HTTP request and return an [R].
     492             :   ///
     493             :   /// **IMPORTANT**:
     494             :   ///  - [uri] takes the FULL `Uri` including query parameters
     495             :   ///  - [headers] does NOT include ANY defaults such as [defaultHeaders]
     496             :   ///  (unless you omit the argument, in which case defaults will be included)
     497             :   ///
     498             :   /// Example:
     499             :   ///
     500             :   /// ```
     501             :   /// await sendRequest(
     502             :   ///   baseUrl.asUri + 'token' & await defaultParams & {'a': 1},
     503             :   ///   headers: await defaultHeaders & {'a': 'b'},
     504             :   ///   onSuccess: (data) => data['token'] as String,
     505             :   /// );
     506             :   /// ```
     507             :   ///
     508             :   ///ignore: comment_references
     509             :   /// To build the URI you can use [String.asUri], [Uri.+] and [Uri.&].
     510             :   ///
     511             :   /// To merge headers and params with their defaults you can use the helper
     512             :   /// [Map<String, dynamic>.&].
     513             :   ///
     514             :   /// In addition, [onSuccess] is supplied to post-process the
     515             :   /// data in JSON format. Deserialization and initialization
     516             :   /// typically occur in this function.
     517             :   ///
     518             :   /// [onError] can also be supplied to override [_RemoteAdapter.onError].
     519             :   @protected
     520             :   @visibleForTesting
     521           1 :   Future<R?> sendRequest<R>(
     522             :     final Uri uri, {
     523             :     DataRequestMethod method = DataRequestMethod.GET,
     524             :     Map<String, String>? headers,
     525             :     String? body,
     526             :     _OnSuccessGeneric<R>? onSuccess,
     527             :     _OnErrorGeneric<R>? onError,
     528             :     bool omitDefaultParams = false,
     529             :     DataRequestLabel? label,
     530             :   }) async {
     531             :     // defaults
     532           2 :     headers ??= await defaultHeaders;
     533             :     final _params =
     534           3 :         omitDefaultParams ? <String, dynamic>{} : await defaultParams;
     535             : 
     536           2 :     label ??= DataRequestLabel('adhoc', type: internalType);
     537           0 :     onSuccess ??= this.onSuccess;
     538           1 :     onError ??= this.onError;
     539             : 
     540             :     http.Response? response;
     541             :     Object? data;
     542             :     Object? error;
     543             :     StackTrace? stackTrace;
     544             : 
     545           4 :     final _client = _isTesting ? read(httpClientProvider)! : httpClient;
     546             : 
     547             :     try {
     548           3 :       final request = http.Request(method.toShortString(), uri & _params);
     549           2 :       request.headers.addAll(headers);
     550             :       if (body != null) {
     551           1 :         request.body = body;
     552             :       }
     553           2 :       final stream = await _client.send(request);
     554           2 :       response = await http.Response.fromStream(stream);
     555             :     } catch (err, stack) {
     556             :       error = err;
     557             :       stackTrace = stack;
     558             :     } finally {
     559           1 :       _client.close();
     560             :     }
     561             : 
     562             :     // response handling
     563             : 
     564             :     try {
     565           2 :       if (response?.body.isNotEmpty ?? false) {
     566           2 :         data = json.decode(response!.body);
     567             :       }
     568           1 :     } on FormatException catch (e) {
     569             :       error = e;
     570             :     }
     571             : 
     572           1 :     final code = response?.statusCode;
     573             : 
     574           2 :     if (error == null && code != null && code >= 200 && code < 300) {
     575           1 :       return onSuccess(data, label);
     576             :     } else {
     577           1 :       if (isOfflineError(error)) {
     578             :         // queue a new operation if:
     579             :         //  - this is a network error and we're offline
     580             :         //  - the request was not a find
     581           1 :         if (method != DataRequestMethod.GET) {
     582           1 :           OfflineOperation<T>(
     583           2 :             httpRequest: '${method.toShortString()} $uri',
     584             :             label: label,
     585             :             body: body,
     586             :             headers: headers,
     587             :             onSuccess: onSuccess as _OnSuccessGeneric<T>,
     588             :             onError: onError as _OnErrorGeneric<T>,
     589             :             adapter: this as RemoteAdapter<T>,
     590           1 :           ).add();
     591             :         }
     592             : 
     593             :         // wrap error in an OfflineException
     594           1 :         final offlineException = OfflineException(error: error!);
     595             : 
     596             :         // call error handler but do not return it
     597             :         // (this gives the user the chance to present
     598             :         // a UI element to retry fetching, for example)
     599           1 :         onError(offlineException, label);
     600             : 
     601             :         // instead return a fallback model from local storage
     602           1 :         switch (label.kind) {
     603           1 :           case 'findAll':
     604           1 :             return findAll(remote: false) as Future<R?>;
     605           1 :           case 'findOne':
     606           1 :           case 'save':
     607           1 :             return label.model as R?;
     608             :           default:
     609             :             return null;
     610             :         }
     611             :       }
     612             : 
     613             :       // if it was not a network error
     614             :       // remove all operations with this request
     615           1 :       OfflineOperation.remove(label, this as RemoteAdapter<T>);
     616             : 
     617           1 :       final e = DataException(error ?? data!,
     618             :           stackTrace: stackTrace, statusCode: code);
     619           3 :       log(label, e.error.toString());
     620           2 :       return await onError(e, label);
     621             :     }
     622             :   }
     623             : 
     624           1 :   FutureOr<R?> onSuccess<R>(Object? data, DataRequestLabel? label) async {
     625             :     // remove all operations with this label
     626           1 :     OfflineOperation.remove(label!, this as RemoteAdapter);
     627             : 
     628           2 :     if (label.kind == 'save') {
     629           1 :       if (label.model == null) {
     630             :         return null;
     631             :       }
     632           1 :       var model = label.model as T;
     633             : 
     634             :       if (data == null) {
     635             :         // return original model if response was empty
     636             :         return model as R?;
     637             :       }
     638             : 
     639             :       // deserialize already inits models
     640             :       // if model had a key already, reuse it
     641           1 :       final deserialized = deserialize(data as Map<String, dynamic>);
     642           3 :       model = deserialized.model!.was(model).saveLocal();
     643             : 
     644           1 :       log(label, 'saved in local storage and remote');
     645             :       return model as R?;
     646             :     }
     647             : 
     648           2 :     if (label.kind == 'delete') {
     649           1 :       log(label, 'deleted in local storage and remote');
     650             :       return null;
     651             :     }
     652             : 
     653           1 :     final deserialized = deserialize(data);
     654           1 :     deserialized._log(this as RemoteAdapter, label);
     655             : 
     656           2 :     final isFindAll = label.kind.startsWith('findAll');
     657           2 :     final isFindOne = label.kind.startsWith('findOne');
     658           2 :     final isAdHoc = label.kind == 'adhoc';
     659             : 
     660           1 :     if (isFindAll || (isAdHoc && deserialized.model == null)) {
     661           4 :       for (final model in [...deserialized.models, ...deserialized.included]) {
     662           1 :         model.saveLocal();
     663             :       }
     664           1 :       return deserialized.models as R?;
     665             :     }
     666             : 
     667           1 :     if (isFindOne || (isAdHoc && deserialized.model != null)) {
     668           4 :       for (final model in [...deserialized.models, ...deserialized.included]) {
     669           1 :         model.saveLocal();
     670             :       }
     671           1 :       return deserialized.model as R?;
     672             :     }
     673             : 
     674             :     return null;
     675             :   }
     676             : 
     677             :   /// Implements global request error handling.
     678             :   ///
     679             :   /// Defaults to throw [e] unless it is an HTTP 404
     680             :   /// or an `OfflineException`.
     681             :   ///
     682             :   /// NOTE: `onError` arguments throughout the API are used
     683             :   /// to override this default behavior.
     684           1 :   @protected
     685             :   @visibleForTesting
     686             :   FutureOr<R?> onError<R>(
     687             :     DataException e,
     688             :     DataRequestLabel? label,
     689             :   ) {
     690           3 :     if (e.statusCode == 404 || e is OfflineException) {
     691             :       return null;
     692             :     }
     693             :     throw e;
     694             :   }
     695             : 
     696             :   /// Logs messages for a specific label when `verbose` is `true`.
     697           1 :   @protected
     698             :   void log(DataRequestLabel label, String message) {
     699           1 :     if (_verbose) {
     700           1 :       final now = DateTime.now();
     701             :       final timestamp =
     702           7 :           '${now.second.toString().padLeft(2, '0')}:${now.millisecond.toString().padLeft(3, '0')}';
     703           5 :       print('$timestamp ${' ' * label.indentation * 2}[$label] $message');
     704             :     }
     705             :   }
     706             : 
     707             :   // offline
     708             : 
     709             :   /// Determines whether [error] was an offline error.
     710           1 :   @protected
     711             :   @visibleForTesting
     712             :   bool isOfflineError(Object? error) {
     713             :     // timeouts via http's `connectionTimeout` are
     714             :     // also socket exceptions
     715             :     // we check the exception like this in order not to import `dart:io`
     716           1 :     final _err = error.toString();
     717           1 :     return _err.startsWith('SocketException') ||
     718           1 :         _err.startsWith('Connection closed before full header was received') ||
     719           1 :         _err.startsWith('HandshakeException');
     720             :   }
     721             : 
     722           1 :   @protected
     723             :   @visibleForTesting
     724             :   @nonVirtual
     725             :   Set<OfflineOperation<T>> get offlineOperations {
     726           2 :     final node = graph._getNode(_offlineAdapterKey)!;
     727           1 :     return node.entries
     728           2 :         .map((e) {
     729             :           // extract type from e.g. _offline:findOne/users#3@d7bcc9
     730           3 :           final label = DataRequestLabel.parse(e.key.denamespace());
     731           3 :           if (label.type == internalType) {
     732             :             // get first edge value
     733           3 :             final map = json.decode(e.value.first) as Map<String, dynamic>;
     734           1 :             return OfflineOperation<T>.fromJson(
     735             :                 label, map, this as RemoteAdapter<T>);
     736             :           }
     737             :         })
     738           1 :         .filterNulls
     739           1 :         .toSet();
     740             :   }
     741             : 
     742           1 :   Object? _resolveId(Object obj) {
     743           2 :     return obj is T ? obj.id : obj;
     744             :   }
     745             : 
     746           1 :   @protected
     747             :   @visibleForTesting
     748             :   @nonVirtual
     749             :   String? keyForModelOrId(Object model) {
     750           1 :     if (model is T) {
     751           1 :       return model._key;
     752             :     } else {
     753           1 :       final id = _resolveId(model);
     754             :       if (id != null) {
     755           3 :         return graph.getKeyForId(internalType, id,
     756           1 :             keyIfAbsent: DataHelpers.generateKey<T>())!;
     757             :       } else {
     758             :         return null;
     759             :       }
     760             :     }
     761             :   }
     762             : 
     763           1 :   bool get _isTesting {
     764           3 :     return read(httpClientProvider) != null;
     765             :   }
     766             : }
     767             : 
     768             : /// When this provider is non-null it will override
     769             : /// all [_RemoteAdapter.httpClient] overrides;
     770             : /// it is useful for providing a mock client for testing
     771           3 : final httpClientProvider = Provider<http.Client?>((_) => null);

Generated by: LCOV version 1.15