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);
|