fixed the long existing bug of null fields overrriding the fields in Map of notifyOptions

This commit is contained in:
Kingkor Roy Tirtho
2022-03-26 10:47:43 +06:00
parent 2c9960a8c3
commit 06e15c3096
6 changed files with 157 additions and 48 deletions
+1 -1
View File
@@ -281,7 +281,7 @@ class QueryObserverResult<TData extends Map<String, dynamic>, TError> {
bool isRefetching; bool isRefetching;
bool isStale; bool isStale;
bool isSuccess; bool isSuccess;
Future<QueryObserverResult<TData, TError>> Function<TPageData>({ Future<QueryObserverResult<TData, TError>?> Function<TPageData>({
RefetchOptions options, RefetchOptions options,
RefetchableQueryFilters<TPageData> filters, RefetchableQueryFilters<TPageData> filters,
}) refetch; }) refetch;
+2 -1
View File
@@ -558,7 +558,8 @@ class Query<TQueryFnData extends Map<String, dynamic>, TError,
bool isStale() { bool isStale() {
return (this.state.isInvalidated || return (this.state.isInvalidated ||
this.state.dataUpdatedAt == null || this.state.dataUpdatedAt == null ||
_observers.any((observer) => observer.getCurrentResult().isStale)); _observers
.any((observer) => observer.getCurrentResult()?.isStale == true));
} }
bool isStaleByTime(Duration? staleTime) { bool isStaleByTime(Duration? staleTime) {
@@ -26,12 +26,21 @@ class NotifyOptions {
NotifyOptions({this.cache, this.listeners, this.onError, this.onSuccess}); NotifyOptions({this.cache, this.listeners, this.onError, this.onSuccess});
Map<String, dynamic> toJson() { /// [safe] default `true`- if it's true then there'll be no key
/// containing null value
Map<String, dynamic> toJson([bool safe = true]) {
final Map<String, dynamic> data = new Map<String, dynamic>(); final Map<String, dynamic> data = new Map<String, dynamic>();
data['cache'] = this.cache; if (safe) {
data['listeners'] = this.listeners; if (this.cache != null) data['cache'] = this.cache;
data['onError'] = this.onError; if (this.listeners != null) data['listeners'] = this.listeners;
data['onSuccess'] = this.onSuccess; 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; return data;
} }
@@ -65,11 +74,16 @@ class QueryObserver<
TData extends Map<String, dynamic>, TData extends Map<String, dynamic>,
TQueryData extends Map<String, dynamic>> TQueryData extends Map<String, dynamic>>
extends Subscribable<QueryObserverListener> { extends Subscribable<QueryObserverListener> {
late QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options; QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options;
QueryClient _client; QueryClient _client;
Query<TQueryFnData, TError, TQueryData>? _currentQuery; Query<TQueryFnData, TError, TQueryData>? _currentQuery;
late QueryState<TQueryData, TError> _currentQueryInitialState; late QueryState<TQueryData, TError> _currentQueryInitialState;
late QueryObserverResult<TData, TError> _currentResult; QueryObserverResult<TData, TError>? _currentResult;
/// List of tracked keys/properties of [QueryObserverResult]
late List<String> _trackedProps;
QueryState<TQueryData, TError>? _currentResultState; QueryState<TQueryData, TError>? _currentResultState;
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>?
_currentResultOptions; _currentResultOptions;
@@ -80,12 +94,13 @@ class QueryObserver<
Timer? _refetchInterval; Timer? _refetchInterval;
Duration? _currentRefetchInterval; Duration? _currentRefetchInterval;
/// List of tracked keys/properties of [QueryObserverResult] QueryObserver(
late List<String> _trackedProps; this._client,
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? _options,
QueryObserver(this._client, options) ) : _trackedProps = [],
: _trackedProps = [], _previousSelectError = null,
_previousSelectError = null { options = _options ?? QueryObserverOptions(),
super() {
this.setOptions(options); this.setOptions(options);
} }
@@ -120,7 +135,7 @@ class QueryObserver<
_currentQuery?.removeObserver(this); _currentQuery?.removeObserver(this);
} }
setOptions( void setOptions(
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? options, [ QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? options, [
NotifyOptions? notifyOptions, NotifyOptions? notifyOptions,
]) { ]) {
@@ -143,6 +158,24 @@ class QueryObserver<
_executeFetch(); _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<TData, TError> getOptimisticResult( QueryObserverResult<TData, TError> getOptimisticResult(
@@ -155,7 +188,7 @@ class QueryObserver<
return createResult(query, defaultedOptions); return createResult(query, defaultedOptions);
} }
QueryObserverResult<TData, TError> getCurrentResult() { QueryObserverResult<TData, TError>? getCurrentResult() {
return _currentResult; return _currentResult;
} }
@@ -229,7 +262,7 @@ class QueryObserver<
} }
@protected @protected
Future<QueryObserverResult<TData, TError>> fetch( Future<QueryObserverResult<TData, TError>?> fetch(
ObserverFetchOptions fetchOptions, ObserverFetchOptions fetchOptions,
) { ) {
return _executeFetch(fetchOptions).then((val) { return _executeFetch(fetchOptions).then((val) {
@@ -257,7 +290,7 @@ class QueryObserver<
bool _shouldNotifyListeners(QueryObserverResult<TData, TError> result, bool _shouldNotifyListeners(QueryObserverResult<TData, TError> result,
[QueryObserverResult<TData, TError>? prevResult]) { [QueryObserverResult<TData, TError>? prevResult]) {
if (prevResult == null) return true; if (prevResult == null) return true;
if (!options.notifyOnChangeProps && if (options.notifyOnChangeProps == false &&
options.notifyOnChangePropsExclusions == null) { options.notifyOnChangePropsExclusions == null) {
return true; return true;
} }
@@ -292,20 +325,25 @@ class QueryObserver<
_currentResultState = _currentQuery?.state; _currentResultState = _currentQuery?.state;
_currentResultOptions = this.options; _currentResultOptions = this.options;
final isSameMap =
shallowEqualMap(_currentResult?.toJson(), prevResult?.toJson());
// Only notify if something has changed // Only notify if something has changed
if (shallowEqualMap(_currentResult.toJson(), prevResult?.toJson())) { if (isSameMap) {
return; return;
} }
NotifyOptions defaultNotifyOptions = NotifyOptions(cache: true); NotifyOptions defaultNotifyOptions = NotifyOptions(cache: true);
if (notifyOptions?.listeners != false && if (notifyOptions?.listeners != false &&
_shouldNotifyListeners(_currentResult, prevResult)) { _currentResult != null &&
_shouldNotifyListeners(_currentResult!, prevResult)) {
defaultNotifyOptions.listeners = true; defaultNotifyOptions.listeners = true;
} }
_notify(NotifyOptions.fromJson({ final mergedNotifyOptions = {
...defaultNotifyOptions.toJson(), ...defaultNotifyOptions.toJson(),
...(notifyOptions?.toJson() ?? {}), ...(notifyOptions?.toJson() ?? {}),
})); };
_notify(NotifyOptions.fromJson(mergedNotifyOptions));
} }
void _updateQuery() { void _updateQuery() {
@@ -407,7 +445,7 @@ class QueryObserver<
try { try {
data = options.select?.call(state.data); data = options.select?.call(state.data);
if (options.structuralSharing != false) { if (options.structuralSharing != false) {
data = replaceEqualDeep(prevResult.data, data); data = replaceEqualDeep(prevResult?.data, data);
} }
if (options.select != null && data != null) { if (options.select != null && data != null) {
_previousSelect = SelectQuery<TQueryData, TData>( _previousSelect = SelectQuery<TQueryData, TData>(
@@ -427,7 +465,7 @@ class QueryObserver<
} }
// Use query data // Use query data
else { else {
data = state.data as TData; data = state.data as TData?;
} }
if (options.placeholderData != null && if (options.placeholderData != null &&
@@ -435,9 +473,9 @@ class QueryObserver<
(status == QueryStatus.loading || status == QueryStatus.idle)) { (status == QueryStatus.loading || status == QueryStatus.idle)) {
var placeholderData; var placeholderData;
if (prevResult.isPlaceholderData == true && if (prevResult?.isPlaceholderData == true &&
options.placeholderData == prevResultOptions?.placeholderData) { options.placeholderData == prevResultOptions?.placeholderData) {
placeholderData = prevResult.data; placeholderData = prevResult?.data;
} else { } else {
placeholderData = options.placeholderData; placeholderData = options.placeholderData;
if (options.select != null && placeholderData != null) { if (options.select != null && placeholderData != null) {
@@ -445,7 +483,7 @@ class QueryObserver<
placeholderData = options.select?.call(placeholderData); placeholderData = options.select?.call(placeholderData);
if (options.structuralSharing != false) { if (options.structuralSharing != false) {
placeholderData = placeholderData =
replaceEqualDeep(prevResult.data, placeholderData); replaceEqualDeep(prevResult?.data, placeholderData);
} }
_previousSelectError = null; _previousSelectError = null;
} catch (selectError) { } catch (selectError) {
@@ -497,18 +535,18 @@ class QueryObserver<
void _notify(NotifyOptions notifyOptions) { void _notify(NotifyOptions notifyOptions) {
notifyManager.batch(() { notifyManager.batch(() {
// First trigger the configuration callbacks // First trigger the configuration callbacks
if (notifyOptions.onSuccess == true) { if (notifyOptions.onSuccess == true && _currentResult != null) {
this.options.onSuccess?.call(_currentResult.data!); this.options.onSuccess?.call(_currentResult!.data!);
this.options.onSettled?.call(_currentResult.data!); this.options.onSettled?.call(_currentResult!.data!);
} else if (notifyOptions.onError == true) { } else if (notifyOptions.onError == true && _currentResult != null) {
this.options.onError?.call(_currentResult.error!); this.options.onError?.call(_currentResult!.error!);
this.options.onSettled?.call(null, _currentResult.error!); this.options.onSettled?.call(null, _currentResult!.error!);
} }
// Then trigger the listeners // Then trigger the listeners
if (notifyOptions.listeners == true) { if (notifyOptions.listeners == true && _currentResult != null) {
this.listeners.forEach((listener) { this.listeners.forEach((listener) {
listener(_currentResult); listener(_currentResult!);
}); });
} }
@@ -526,7 +564,7 @@ class QueryObserver<
Duration? _computeRefetchInterval() { Duration? _computeRefetchInterval() {
return this.options.refetchInterval != null && _currentQuery != null return this.options.refetchInterval != null && _currentQuery != null
? this.options.refetchInterval!(_currentResult.data, _currentQuery!) ? this.options.refetchInterval!(_currentResult?.data, _currentQuery!)
: null; : null;
} }
@@ -537,22 +575,22 @@ class QueryObserver<
void _updateStaleTimeout() { void _updateStaleTimeout() {
_clearStaleTimeout(); _clearStaleTimeout();
if (_currentResult.isStale || if (_currentResult?.isStale == true ||
options.staleTime == null || options.staleTime == null ||
_currentResult.dataUpdatedAt == null) return; _currentResult?.dataUpdatedAt == null) return;
// The timeout is sometimes triggered 1 ms before the stale time // The timeout is sometimes triggered 1 ms before the stale time
// expiration. To mitigate this issue we always add 1 ms to the // expiration. To mitigate this issue we always add 1 ms to the
// timeout. // timeout.
Duration time = Duration( Duration time = Duration(
milliseconds: milliseconds:
timeUntilStale(_currentResult.dataUpdatedAt!, this.options.staleTime) timeUntilStale(_currentResult!.dataUpdatedAt!, this.options.staleTime)
.inMilliseconds + .inMilliseconds +
1, 1,
); );
_staleTimeout = Timer(time, () { _staleTimeout = Timer(time, () {
if (!_currentResult.isStale) { if (!_currentResult!.isStale) {
this.updateResult(); this.updateResult();
} }
}); });
@@ -595,7 +633,7 @@ class QueryObserver<
_currentQuery?.removeObserver(this); _currentQuery?.removeObserver(this);
} }
Future<QueryObserverResult<TData, TError>> refetch<TPageData>({ Future<QueryObserverResult<TData, TError>?> refetch<TPageData>({
RefetchableQueryFilters<TPageData>? filters, RefetchableQueryFilters<TPageData>? filters,
RefetchOptions? options, RefetchOptions? options,
}) { }) {
@@ -618,7 +656,7 @@ bool shouldLoadOnMount<
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options, QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options,
) { ) {
return (options.enabled != false && return (options.enabled != false &&
query.state.dataUpdatedAt != null && query.state.dataUpdatedAt == null &&
!(query.state.status == QueryStatus.error && !(query.state.status == QueryStatus.error &&
options.retryOnMount == false)); options.retryOnMount == false));
} }
@@ -1,12 +1,16 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
//? using a single argument due to TypeCast Error cause queryObserver
//? listeners
void placeholder(a1) {}
abstract class Subscribable<TListener extends Function> { abstract class Subscribable<TListener extends Function> {
@protected @protected
List<TListener> listeners; List<TListener> listeners;
Subscribable() : listeners = []; Subscribable() : listeners = [];
void Function() subscribe([TListener? listener]) { void Function() subscribe([TListener? listener]) {
listener ??= (() => null) as TListener; listener ??= placeholder as TListener;
listeners.add(listener); listeners.add(listener);
+2 -4
View File
@@ -91,10 +91,8 @@ bool shallowEqualMap(Map? a, Map? b) {
return false; return false;
} }
for (var item in a!.entries) { for (final item in a!.entries) {
var aVal = item.value; if (a[item.key] != b?[item.key]) return false;
var bVal = b?[item.key];
if (aVal != bVal) return false;
} }
return true; return true;
@@ -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<QueryObserverResult> 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);
});
});
}