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 isStale;
bool isSuccess;
Future<QueryObserverResult<TData, TError>> Function<TPageData>({
Future<QueryObserverResult<TData, TError>?> Function<TPageData>({
RefetchOptions options,
RefetchableQueryFilters<TPageData> filters,
}) refetch;
+2 -1
View File
@@ -558,7 +558,8 @@ class Query<TQueryFnData extends Map<String, dynamic>, 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) {
@@ -26,12 +26,21 @@ class NotifyOptions {
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>();
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<String, dynamic>,
TQueryData extends Map<String, dynamic>>
extends Subscribable<QueryObserverListener> {
late QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options;
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options;
QueryClient _client;
Query<TQueryFnData, TError, TQueryData>? _currentQuery;
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;
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>?
_currentResultOptions;
@@ -80,12 +94,13 @@ class QueryObserver<
Timer? _refetchInterval;
Duration? _currentRefetchInterval;
/// List of tracked keys/properties of [QueryObserverResult]
late List<String> _trackedProps;
QueryObserver(this._client, options)
: _trackedProps = [],
_previousSelectError = null {
QueryObserver(
this._client,
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? _options,
) : _trackedProps = [],
_previousSelectError = null,
options = _options ?? QueryObserverOptions(),
super() {
this.setOptions(options);
}
@@ -120,7 +135,7 @@ class QueryObserver<
_currentQuery?.removeObserver(this);
}
setOptions(
void setOptions(
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? 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<TData, TError> getOptimisticResult(
@@ -155,7 +188,7 @@ class QueryObserver<
return createResult(query, defaultedOptions);
}
QueryObserverResult<TData, TError> getCurrentResult() {
QueryObserverResult<TData, TError>? getCurrentResult() {
return _currentResult;
}
@@ -229,7 +262,7 @@ class QueryObserver<
}
@protected
Future<QueryObserverResult<TData, TError>> fetch(
Future<QueryObserverResult<TData, TError>?> fetch(
ObserverFetchOptions fetchOptions,
) {
return _executeFetch(fetchOptions).then((val) {
@@ -257,7 +290,7 @@ class QueryObserver<
bool _shouldNotifyListeners(QueryObserverResult<TData, TError> result,
[QueryObserverResult<TData, TError>? 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<TQueryData, TData>(
@@ -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<QueryObserverResult<TData, TError>> refetch<TPageData>({
Future<QueryObserverResult<TData, TError>?> refetch<TPageData>({
RefetchableQueryFilters<TPageData>? filters,
RefetchOptions? options,
}) {
@@ -618,7 +656,7 @@ bool shouldLoadOnMount<
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options,
) {
return (options.enabled != false &&
query.state.dataUpdatedAt != null &&
query.state.dataUpdatedAt == null &&
!(query.state.status == QueryStatus.error &&
options.retryOnMount == false));
}
@@ -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<TListener extends Function> {
@protected
List<TListener> listeners;
Subscribable() : listeners = [];
void Function() subscribe([TListener? listener]) {
listener ??= (() => null) as TListener;
listener ??= placeholder as TListener;
listeners.add(listener);
+2 -4
View File
@@ -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;
@@ -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);
});
});
}