fixed the long existing bug of null fields overrriding the fields in Map of notifyOptions
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user