diff --git a/packages/fl_query/lib/src/core/models.dart b/packages/fl_query/lib/src/core/models.dart index e2cfc46..c7c3b0d 100644 --- a/packages/fl_query/lib/src/core/models.dart +++ b/packages/fl_query/lib/src/core/models.dart @@ -281,7 +281,7 @@ class QueryObserverResult, TError> { bool isRefetching; bool isStale; bool isSuccess; - Future> Function({ + Future?> Function({ RefetchOptions options, RefetchableQueryFilters filters, }) refetch; diff --git a/packages/fl_query/lib/src/core/query.dart b/packages/fl_query/lib/src/core/query.dart index 55415e9..7da3e25 100644 --- a/packages/fl_query/lib/src/core/query.dart +++ b/packages/fl_query/lib/src/core/query.dart @@ -558,7 +558,8 @@ class Query, TError, bool isStale() { return (this.state.isInvalidated || this.state.dataUpdatedAt == null || - _observers.any((observer) => observer.getCurrentResult().isStale)); + _observers + .any((observer) => observer.getCurrentResult()?.isStale == true)); } bool isStaleByTime(Duration? staleTime) { diff --git a/packages/fl_query/lib/src/core/query_observer.dart b/packages/fl_query/lib/src/core/query_observer.dart index fe2ff10..7ecc37a 100644 --- a/packages/fl_query/lib/src/core/query_observer.dart +++ b/packages/fl_query/lib/src/core/query_observer.dart @@ -26,12 +26,21 @@ class NotifyOptions { NotifyOptions({this.cache, this.listeners, this.onError, this.onSuccess}); - Map toJson() { + /// [safe] default `true`- if it's true then there'll be no key + /// containing null value + Map toJson([bool safe = true]) { final Map data = new Map(); - data['cache'] = this.cache; - data['listeners'] = this.listeners; - data['onError'] = this.onError; - data['onSuccess'] = this.onSuccess; + if (safe) { + if (this.cache != null) data['cache'] = this.cache; + if (this.listeners != null) data['listeners'] = this.listeners; + if (this.onError != null) data['onError'] = this.onError; + if (this.onSuccess != null) data['onSuccess'] = this.onSuccess; + } else { + data['cache'] = this.cache; + data['listeners'] = this.listeners; + data['onError'] = this.onError; + data['onSuccess'] = this.onSuccess; + } return data; } @@ -65,11 +74,16 @@ class QueryObserver< TData extends Map, TQueryData extends Map> extends Subscribable { - late QueryObserverOptions options; + QueryObserverOptions options; QueryClient _client; Query? _currentQuery; + late QueryState _currentQueryInitialState; - late QueryObserverResult _currentResult; + QueryObserverResult? _currentResult; + + /// List of tracked keys/properties of [QueryObserverResult] + late List _trackedProps; + QueryState? _currentResultState; QueryObserverOptions? _currentResultOptions; @@ -80,12 +94,13 @@ class QueryObserver< Timer? _refetchInterval; Duration? _currentRefetchInterval; - /// List of tracked keys/properties of [QueryObserverResult] - late List _trackedProps; - - QueryObserver(this._client, options) - : _trackedProps = [], - _previousSelectError = null { + QueryObserver( + this._client, + QueryObserverOptions? _options, + ) : _trackedProps = [], + _previousSelectError = null, + options = _options ?? QueryObserverOptions(), + super() { this.setOptions(options); } @@ -120,7 +135,7 @@ class QueryObserver< _currentQuery?.removeObserver(this); } - setOptions( + void setOptions( QueryObserverOptions? options, [ NotifyOptions? notifyOptions, ]) { @@ -143,6 +158,24 @@ class QueryObserver< _executeFetch(); } ; + + this.updateResult(notifyOptions); + if (mounted && + (_currentQuery != prevQuery || + this.options.enabled != prevOptions.enabled || + this.options.staleTime != prevOptions.staleTime)) { + _updateStaleTimeout(); + } + + final nextRefetchInterval = _computeRefetchInterval(); + + // Update refetch interval if needed + if (mounted && + (_currentQuery != prevQuery || + this.options.enabled != prevOptions.enabled || + nextRefetchInterval != _currentRefetchInterval)) { + _updateRefetchInterval(nextRefetchInterval); + } } QueryObserverResult getOptimisticResult( @@ -155,7 +188,7 @@ class QueryObserver< return createResult(query, defaultedOptions); } - QueryObserverResult getCurrentResult() { + QueryObserverResult? getCurrentResult() { return _currentResult; } @@ -229,7 +262,7 @@ class QueryObserver< } @protected - Future> fetch( + Future?> fetch( ObserverFetchOptions fetchOptions, ) { return _executeFetch(fetchOptions).then((val) { @@ -257,7 +290,7 @@ class QueryObserver< bool _shouldNotifyListeners(QueryObserverResult result, [QueryObserverResult? prevResult]) { if (prevResult == null) return true; - if (!options.notifyOnChangeProps && + if (options.notifyOnChangeProps == false && options.notifyOnChangePropsExclusions == null) { return true; } @@ -292,20 +325,25 @@ class QueryObserver< _currentResultState = _currentQuery?.state; _currentResultOptions = this.options; + final isSameMap = + shallowEqualMap(_currentResult?.toJson(), prevResult?.toJson()); // Only notify if something has changed - if (shallowEqualMap(_currentResult.toJson(), prevResult?.toJson())) { + if (isSameMap) { return; } NotifyOptions defaultNotifyOptions = NotifyOptions(cache: true); if (notifyOptions?.listeners != false && - _shouldNotifyListeners(_currentResult, prevResult)) { + _currentResult != null && + _shouldNotifyListeners(_currentResult!, prevResult)) { defaultNotifyOptions.listeners = true; } - _notify(NotifyOptions.fromJson({ + final mergedNotifyOptions = { ...defaultNotifyOptions.toJson(), ...(notifyOptions?.toJson() ?? {}), - })); + }; + + _notify(NotifyOptions.fromJson(mergedNotifyOptions)); } void _updateQuery() { @@ -407,7 +445,7 @@ class QueryObserver< try { data = options.select?.call(state.data); if (options.structuralSharing != false) { - data = replaceEqualDeep(prevResult.data, data); + data = replaceEqualDeep(prevResult?.data, data); } if (options.select != null && data != null) { _previousSelect = SelectQuery( @@ -427,7 +465,7 @@ class QueryObserver< } // Use query data else { - data = state.data as TData; + data = state.data as TData?; } if (options.placeholderData != null && @@ -435,9 +473,9 @@ class QueryObserver< (status == QueryStatus.loading || status == QueryStatus.idle)) { var placeholderData; - if (prevResult.isPlaceholderData == true && + if (prevResult?.isPlaceholderData == true && options.placeholderData == prevResultOptions?.placeholderData) { - placeholderData = prevResult.data; + placeholderData = prevResult?.data; } else { placeholderData = options.placeholderData; if (options.select != null && placeholderData != null) { @@ -445,7 +483,7 @@ class QueryObserver< placeholderData = options.select?.call(placeholderData); if (options.structuralSharing != false) { placeholderData = - replaceEqualDeep(prevResult.data, placeholderData); + replaceEqualDeep(prevResult?.data, placeholderData); } _previousSelectError = null; } catch (selectError) { @@ -497,18 +535,18 @@ class QueryObserver< void _notify(NotifyOptions notifyOptions) { notifyManager.batch(() { // First trigger the configuration callbacks - if (notifyOptions.onSuccess == true) { - this.options.onSuccess?.call(_currentResult.data!); - this.options.onSettled?.call(_currentResult.data!); - } else if (notifyOptions.onError == true) { - this.options.onError?.call(_currentResult.error!); - this.options.onSettled?.call(null, _currentResult.error!); + if (notifyOptions.onSuccess == true && _currentResult != null) { + this.options.onSuccess?.call(_currentResult!.data!); + this.options.onSettled?.call(_currentResult!.data!); + } else if (notifyOptions.onError == true && _currentResult != null) { + this.options.onError?.call(_currentResult!.error!); + this.options.onSettled?.call(null, _currentResult!.error!); } // Then trigger the listeners - if (notifyOptions.listeners == true) { + if (notifyOptions.listeners == true && _currentResult != null) { this.listeners.forEach((listener) { - listener(_currentResult); + listener(_currentResult!); }); } @@ -526,7 +564,7 @@ class QueryObserver< Duration? _computeRefetchInterval() { return this.options.refetchInterval != null && _currentQuery != null - ? this.options.refetchInterval!(_currentResult.data, _currentQuery!) + ? this.options.refetchInterval!(_currentResult?.data, _currentQuery!) : null; } @@ -537,22 +575,22 @@ class QueryObserver< void _updateStaleTimeout() { _clearStaleTimeout(); - if (_currentResult.isStale || + if (_currentResult?.isStale == true || options.staleTime == null || - _currentResult.dataUpdatedAt == null) return; + _currentResult?.dataUpdatedAt == null) return; // The timeout is sometimes triggered 1 ms before the stale time // expiration. To mitigate this issue we always add 1 ms to the // timeout. Duration time = Duration( milliseconds: - timeUntilStale(_currentResult.dataUpdatedAt!, this.options.staleTime) + timeUntilStale(_currentResult!.dataUpdatedAt!, this.options.staleTime) .inMilliseconds + 1, ); _staleTimeout = Timer(time, () { - if (!_currentResult.isStale) { + if (!_currentResult!.isStale) { this.updateResult(); } }); @@ -595,7 +633,7 @@ class QueryObserver< _currentQuery?.removeObserver(this); } - Future> refetch({ + Future?> refetch({ RefetchableQueryFilters? filters, RefetchOptions? options, }) { @@ -618,7 +656,7 @@ bool shouldLoadOnMount< QueryObserverOptions options, ) { return (options.enabled != false && - query.state.dataUpdatedAt != null && + query.state.dataUpdatedAt == null && !(query.state.status == QueryStatus.error && options.retryOnMount == false)); } diff --git a/packages/fl_query/lib/src/core/subscribable.dart b/packages/fl_query/lib/src/core/subscribable.dart index 315cbd1..118a6b1 100644 --- a/packages/fl_query/lib/src/core/subscribable.dart +++ b/packages/fl_query/lib/src/core/subscribable.dart @@ -1,12 +1,16 @@ import 'package:meta/meta.dart'; +//? using a single argument due to TypeCast Error cause queryObserver +//? listeners +void placeholder(a1) {} + abstract class Subscribable { @protected List listeners; Subscribable() : listeners = []; void Function() subscribe([TListener? listener]) { - listener ??= (() => null) as TListener; + listener ??= placeholder as TListener; listeners.add(listener); diff --git a/packages/fl_query/lib/src/core/utils.dart b/packages/fl_query/lib/src/core/utils.dart index 04d94a8..cfdda17 100644 --- a/packages/fl_query/lib/src/core/utils.dart +++ b/packages/fl_query/lib/src/core/utils.dart @@ -91,10 +91,8 @@ bool shallowEqualMap(Map? a, Map? b) { return false; } - for (var item in a!.entries) { - var aVal = item.value; - var bVal = b?[item.key]; - if (aVal != bVal) return false; + for (final item in a!.entries) { + if (a[item.key] != b?[item.key]) return false; } return true; diff --git a/packages/fl_query/test/src/core/query_observer_test.dart b/packages/fl_query/test/src/core/query_observer_test.dart new file mode 100644 index 0000000..1764feb --- /dev/null +++ b/packages/fl_query/test/src/core/query_observer_test.dart @@ -0,0 +1,68 @@ +import 'package:fl_query/src/core/core.dart'; +import 'package:test/test.dart'; + +import '../../helpers/utils.dart'; + +void main() { + group('QueryObserver', () { + late QueryClient queryClient; + + setUp(() { + queryClient = QueryClient(); + queryClient.mount(); + }); + + tearDown(() { + queryClient.clear(); + }); + + test('should trigger a fetch when subscribed', () async { + final key = queryKey(); + int calls = 0; + queryFn(context) { + calls++; + return {"data": "data1"}; + } + + final observer = QueryObserver( + queryClient, + QueryObserverOptions(queryKey: key, queryFn: queryFn), + ); + final unsubscribe = observer.subscribe(); + await Future.delayed(Duration(milliseconds: 1)); + unsubscribe(); + expect(calls, 1); + }); + + test('should notify when switching query', () async { + final key1 = queryKey(); + final key2 = queryKey(); + final List results = []; + final observer = QueryObserver( + queryClient, + QueryObserverOptions( + queryKey: key1, + queryFn: (_) => {"data": 1}, + ), + ); + final unsubscribe = observer.subscribe((result) { + results.add(result); + }); + await Future.delayed(Duration(milliseconds: 1)); + observer.setOptions( + QueryObserverOptions(queryKey: key2, queryFn: (_) => {"data": 2}), + ); + await Future.delayed(Duration(milliseconds: 2)); + unsubscribe(); + expect(results.length, 4); + expect(results[0].data, isNull); + expect(results[0].status, QueryStatus.loading); + expect(results[1].data, {"data": 1}); + expect(results[1].status, QueryStatus.success); + expect(results[2].data, isNull); + expect(results[2].status, QueryStatus.loading); + expect(results[3].data, {"data": 2}); + expect(results[3].status, QueryStatus.success); + }); + }); +}