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