query_cache related bug fix using query_cache_test

This commit is contained in:
Kingkor Roy Tirtho
2022-02-18 10:36:20 +06:00
parent 0b4c030806
commit 3d3f3381e9
8 changed files with 280 additions and 50 deletions
+2 -2
View File
@@ -537,7 +537,7 @@ class FetchQueryOptions<TQueryFnData extends Map<String, dynamic>, TError,
/// The time after data is considered stale. /// The time after data is considered stale.
/// If the data is fresh it will be returned from the cache. /// If the data is fresh it will be returned from the cache.
Duration? staleTime; Duration? staleTime;
FetchQueryOptions( FetchQueryOptions({
ShouldRetryFunction<TError>? retry, ShouldRetryFunction<TError>? retry,
RetryDelayFunction<TError>? retryDelay, RetryDelayFunction<TError>? retryDelay,
Duration? cacheTime, Duration? cacheTime,
@@ -554,7 +554,7 @@ class FetchQueryOptions<TQueryFnData extends Map<String, dynamic>, TError,
GetNextPageParamFunction<TQueryFnData>? getNextPageParam, GetNextPageParamFunction<TQueryFnData>? getNextPageParam,
bool? defaulted, bool? defaulted,
this.staleTime, this.staleTime,
) : super( }) : super(
retry: retry, retry: retry,
retryDelay: retryDelay, retryDelay: retryDelay,
cacheTime: cacheTime, cacheTime: cacheTime,
+1 -1
View File
@@ -280,7 +280,7 @@ class Query<TQueryFnData extends Map<String, dynamic>, TError,
data = prevData as TData; data = prevData as TData;
} else if (this.options.structuralSharing != false) { } else if (this.options.structuralSharing != false) {
// Structurally share data between prev and new data if needed // Structurally share data between prev and new data if needed
data = replaceEqualDeep(prevData ?? {} as TData, data); data = replaceEqualDeep(prevData, data);
} }
// Set data and mark it as cached // Set data and mark it as cached
_dispatch(Action( _dispatch(Action(
+21 -22
View File
@@ -139,26 +139,31 @@ class QueryCache extends Subscribable<QueryCacheListener> {
} }
Query<TQueryFnData, TError, TData>? find< Query<TQueryFnData, TError, TData>? find<
TQueryFnData extends Map<String, dynamic>, TQueryFnData extends Map<String, dynamic>,
TError, TError,
TData extends Map<String, dynamic>>( TData extends Map<String, dynamic>>(QueryKey queryKey,
QueryKey queryKey, [QueryFilters? queryFilters]) {
QueryFilters queryFilters, queryFilters ??= QueryFilters();
) {
queryFilters.exact ??= true; queryFilters.exact ??= true;
return _queries.firstWhereOrNull((query) => matchQuery(queryFilters, query)) return _queries.firstWhereOrNull((query) => matchQuery(
as Query<TQueryFnData, TError, TData>?; queryFilters!,
query,
queryKey,
)) as Query<TQueryFnData, TError, TData>?;
} }
List<Query> findAll(QueryKey? queryKey, [QueryFilters? filters]) { List<Query> findAll([QueryKey? queryKeys, QueryFilters? filters]) {
if (queryKey == null && filters == null) return filters == null && queryKeys == null
throw Exception(
"[QueryCache.findAll] both `queryKey` & `filters` can't be null");
bool filterIsEmpty =
filters?.toJson().entries.every((map) => map.value == null) ?? false;
return filterIsEmpty
? _queries ? _queries
: _queries.where((query) => matchQuery(filters!, query)).toList(); : _queries
.where(
(query) => matchQuery(
filters ?? QueryFilters(),
query,
queryKeys,
),
)
.toList();
} }
void notify(QueryCacheNotifyEvent event) { void notify(QueryCacheNotifyEvent event) {
@@ -169,12 +174,6 @@ class QueryCache extends Subscribable<QueryCacheListener> {
}); });
} }
@override
void onSubscribe() {}
@override
void onUnsubscribe() {}
/// Dummy function just to keep the API similar to react-query /// Dummy function just to keep the API similar to react-query
void onFocus() {} void onFocus() {}
@@ -97,10 +97,10 @@ class QueryClient {
} }
List<QueryData<TData>> getQueriesData<TData extends Map<String, dynamic>>({ List<QueryData<TData>> getQueriesData<TData extends Map<String, dynamic>>({
QueryKey? queryKey, QueryKey? queryKeys,
QueryFilters? filters, QueryFilters? filters,
}) { }) {
return getQueryCache().findAll(queryKey, filters).map((query) { return getQueryCache().findAll(queryKeys, filters).map((query) {
return QueryData<TData>( return QueryData<TData>(
data: query.state.data as TData, data: query.state.data as TData,
queryKey: query.queryKey, queryKey: query.queryKey,
@@ -130,15 +130,15 @@ class QueryClient {
List<QueryData> setQueriesData<TData extends Map<String, dynamic>>({ List<QueryData> setQueriesData<TData extends Map<String, dynamic>>({
required DataUpdateFunction<TData?, TData> updater, required DataUpdateFunction<TData?, TData> updater,
QueryKey? queryKey, QueryKey? queryKeys,
QueryFilters? filters, QueryFilters? filters,
DateTime? updatedAt, DateTime? updatedAt,
}) { }) {
if (queryKey == null && filters == null) if (queryKeys == null && filters == null)
throw Exception( throw Exception(
"[QueryClient.setQueriesData] both `queryKey` & `filters` can't be null at the same time"); "[QueryClient.setQueriesData] both `queryKey` & `filters` can't be null at the same time");
return notifyManager return notifyManager
.batch(() => getQueryCache().findAll(queryKey, filters).map( .batch(() => getQueryCache().findAll(queryKeys, filters).map(
(query) => QueryData( (query) => QueryData(
queryKey: query.queryKey, queryKey: query.queryKey,
data: setQueryData<TData>( data: setQueryData<TData>(
@@ -164,10 +164,10 @@ class QueryClient {
?.state as QueryState<TData, TError>?; ?.state as QueryState<TData, TError>?;
} }
void removeQueries({QueryKey? queryKey, QueryFilters? filters}) { void removeQueries({QueryKey? queryKeys, QueryFilters? filters}) {
notifyManager.batch( notifyManager.batch(
() => { () => {
_queryCache.findAll(queryKey, filters).forEach((query) { _queryCache.findAll(queryKeys, filters).forEach((query) {
_queryCache.remove(query); _queryCache.remove(query);
}) })
}, },
@@ -175,7 +175,7 @@ class QueryClient {
} }
Future<void> resetQueries<TPageData>({ Future<void> resetQueries<TPageData>({
QueryKey? queryKey, QueryKey? queryKeys,
RefetchableQueryFilters<TPageData>? filters, RefetchableQueryFilters<TPageData>? filters,
bool? throwOnError, bool? throwOnError,
}) { }) {
@@ -186,7 +186,7 @@ class QueryClient {
}); });
return notifyManager.batch(() { return notifyManager.batch(() {
_queryCache.findAll(queryKey, filters).forEach((query) { _queryCache.findAll(queryKeys, filters).forEach((query) {
query.reset(); query.reset();
}); });
return refetchQueries( return refetchQueries(
@@ -197,13 +197,13 @@ class QueryClient {
} }
Future<void> cancelQueries({ Future<void> cancelQueries({
QueryKey? queryKey, QueryKey? queryKeys,
QueryFilters? filters, QueryFilters? filters,
bool? revert = true, bool? revert = true,
bool? silent, bool? silent,
}) { }) {
var futures = notifyManager.batch(() => var futures = notifyManager.batch(() =>
_queryCache.findAll(queryKey, filters).map((query) => query.cancel( _queryCache.findAll(queryKeys, filters).map((query) => query.cancel(
revert: revert, revert: revert,
silent: silent, silent: silent,
))); )));
@@ -211,7 +211,7 @@ class QueryClient {
} }
Future<void> invalidateQueries<TPageData>({ Future<void> invalidateQueries<TPageData>({
QueryKey? queryKey, QueryKey? queryKeys,
InvalidateQueryFilters<TPageData>? filters, InvalidateQueryFilters<TPageData>? filters,
RefetchOptions? options, RefetchOptions? options,
}) { }) {
@@ -223,7 +223,7 @@ class QueryClient {
"inactive": filters?.refetchInactive ?? false, "inactive": filters?.refetchInactive ?? false,
}); });
return notifyManager.batch(() { return notifyManager.batch(() {
_queryCache.findAll(queryKey, filters).forEach((query) { _queryCache.findAll(queryKeys, filters).forEach((query) {
query.invalidate(); query.invalidate();
}); });
return this.refetchQueries( return this.refetchQueries(
@@ -234,12 +234,12 @@ class QueryClient {
} }
Future<void> refetchQueries<TPageData>({ Future<void> refetchQueries<TPageData>({
QueryKey? queryKey, QueryKey? queryKeys,
RefetchableQueryFilters<TPageData>? filters, RefetchableQueryFilters<TPageData>? filters,
RefetchOptions? options, RefetchOptions? options,
}) { }) {
var futures = notifyManager.batch( var futures = notifyManager.batch(
() => _queryCache.findAll(queryKey, filters).map( () => _queryCache.findAll(queryKeys, filters).map(
(query) => query.fetch( (query) => query.fetch(
null, null,
ObserverFetchOptions( ObserverFetchOptions(
@@ -6,14 +6,14 @@ abstract class Subscribable<TListener extends Function> {
Subscribable() : listeners = []; Subscribable() : listeners = [];
void Function() subscribe([TListener? listener]) { void Function() subscribe([TListener? listener]) {
var callback = listener ?? (() => null); listener ??= (() => null) as TListener;
listeners.add(callback as TListener); listeners.add(listener);
onSubscribe(); onSubscribe();
return () { return () {
listeners = listeners.where((x) => x != callback).toList(); listeners = listeners.where((x) => x != listener).toList();
onUnsubscribe(); onUnsubscribe();
}; };
} }
@@ -23,8 +23,8 @@ abstract class Subscribable<TListener extends Function> {
} }
@protected @protected
void onSubscribe(); void onSubscribe() {}
@protected @protected
void onUnsubscribe(); void onUnsubscribe() {}
} }
+13 -5
View File
@@ -39,12 +39,20 @@ QueryStatusFilter mapQueryStatusFilter(
} }
} }
bool matchQuery(QueryFilters filters, Query query, [QueryKey? queryKey]) { bool matchQuery(
if (queryKey != null) { QueryFilters filters,
if (filters.exact! && Query query, [
query.queryHash != hashQueryKeyByOptions(queryKey, query.options))
/// multiple queryKeys to find the query
QueryKey? queryKeys,
]) {
if (queryKeys != null) {
if (filters.exact == true &&
query.queryHash != hashQueryKeyByOptions(queryKeys, query.options))
return false; return false;
else if (query.queryKey.key != queryKey) return false; else if (query.queryKey.key != queryKeys.key &&
!queryKeys.keyAsList.contains(query.queryKey.key) &&
!query.queryKey.keyAsList.contains(queryKeys.key)) return false;
} }
QueryStatusFilter queryStatusFilter = QueryStatusFilter queryStatusFilter =
mapQueryStatusFilter(filters.active, filters.inactive); mapQueryStatusFilter(filters.active, filters.inactive);
@@ -0,0 +1,8 @@
import 'package:fl_query/src/core/core.dart';
import 'package:uuid/uuid.dart';
Uuid uuid = Uuid();
QueryKey queryKey() {
return QueryKey("query_${uuid.v4()}");
}
@@ -0,0 +1,215 @@
import 'package:fl_query/src/core/core.dart';
import 'package:fl_query/src/core/query_cache.dart';
import 'package:test/expect.dart';
import 'package:test/scaffolding.dart';
import '../../helpers/utils.dart';
void main() {
group('QueryCache', () {
late QueryClient queryClient;
late QueryCache queryCache;
setUp(() {
queryClient = new QueryClient();
queryCache = queryClient.getQueryCache();
});
tearDown(() {
queryClient.clear();
});
group('subscribe', () {
test('Should pass the correct query', () async {
final QueryKey key = queryKey();
var _event;
subscriber(event) {
_event ??= event;
}
final unsubscribe = queryCache.subscribe(subscriber);
final Map<String, dynamic> data = {'foo': "foo"};
queryClient.setQueryData(key, (_) => data);
final query = queryCache.find(key);
await Future.delayed(Duration(milliseconds: 1));
expect(_event, isA<QueryCacheNotifyEvent>());
expect(
(_event as QueryCacheNotifyEvent).type,
QueryCacheNotifyEventType.queryAdded,
);
expect(
(_event as QueryCacheNotifyEvent).query,
same(query),
);
unsubscribe();
});
test('Should notify listeners When new query is added', () async {
final key = queryKey();
late bool called;
callback(_) {
called = true;
}
queryCache.subscribe(callback);
queryClient.prefetchQuery(
queryKey: key,
queryFn: (_) async => {'data': "data"},
);
await Future.delayed(Duration(milliseconds: 100));
expect(called, isTrue);
});
test('Should include the queryCache and query When notifying listeners',
() async {
final key = queryKey();
QueryCacheNotifyEvent? _event;
callback(event) => _event ??= event;
queryCache.subscribe(callback);
queryClient.prefetchQuery(
queryKey: key,
queryFn: (_) => {'data': "data"},
);
final query = queryCache.find(key);
await Future.delayed(Duration(milliseconds: 100));
expect(_event, isA<QueryCacheNotifyEvent>());
expect(_event?.type, QueryCacheNotifyEventType.queryAdded);
expect(_event?.query, same(query));
});
test('Should notify subscribers When new query with initialData is added',
() async {
final key = queryKey();
late bool called;
callback(_) => called = true;
queryCache.subscribe(callback);
queryClient
.prefetchQuery<Map<String, dynamic>, dynamic, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => {'data': "Data"},
options: FetchQueryOptions(initialData: {"data": "initial-data"}),
);
await Future.delayed(Duration(milliseconds: 100));
expect(called, isTrue);
});
});
group('find', () {
test('Should filter correctly', () async {
final key = queryKey();
await queryClient.prefetchQuery(
queryKey: key,
queryFn: (_) => {"data": 'data1'},
);
final query = queryCache.find(key);
expect(query, isNotNull);
});
test(
'Should filter correctly When called with exact set to false',
() async {
final key = queryKey();
await queryClient.prefetchQuery(
queryKey: key,
queryFn: (_) => {"data": 'data1'},
);
final query = queryCache.find(key, QueryFilters(exact: false));
expect(query, isNotNull);
},
);
});
group('findAll', () {
test('Should filter correctly', () async {
final key1 = queryKey();
final key2 = queryKey();
final key3 = QueryKey.fromList(['posts', "1"]);
await queryClient.prefetchQuery(
queryKey: key1,
queryFn: (_) => {"data": 'data1'},
);
await queryClient.prefetchQuery(
queryKey: key2,
queryFn: (_) => {"data": 'data2'},
);
await queryClient.prefetchQuery(
queryKey: key3,
queryFn: (_) => {"data": 'data4'},
);
await queryClient.invalidateQueries(queryKeys: key2);
final query1 = queryCache.find(key1);
final query2 = queryCache.find(key2);
final query4 = queryCache.find(key3);
expect(queryCache.findAll(key1), equals([query1]));
expect(queryCache.findAll(), equals([query1, query2, query4]));
expect(
queryCache.findAll(key1, QueryFilters(active: false)),
equals([query1]),
);
expect(
queryCache.findAll(key1, QueryFilters(active: true)), equals([]));
expect(queryCache.findAll(key1, QueryFilters(stale: true)), equals([]));
expect(
queryCache.findAll(key1, QueryFilters(stale: false)),
equals([query1]),
);
expect(
queryCache.findAll(key1, QueryFilters(stale: false, active: true)),
equals([]),
);
expect(
queryCache.findAll(key1, QueryFilters(active: false, stale: false)),
equals([query1]),
);
expect(
queryCache.findAll(
key1,
QueryFilters(active: false, stale: false, exact: true),
),
equals([query1]),
);
expect(queryCache.findAll(key2), equals([query2]));
expect(
queryCache.findAll(key2, QueryFilters(stale: null)),
equals([query2]),
);
expect(
queryCache.findAll(key2, QueryFilters(stale: true)),
equals([query2]),
);
expect(
queryCache.findAll(key2, QueryFilters(stale: false)),
equals([]),
);
expect(
queryCache.findAll(
null,
QueryFilters(predicate: (query) => query == query4),
),
equals([query4]),
);
expect(queryCache.findAll(QueryKey('posts')), equals([query4]));
});
test('Should return all the queries When no filters are defined',
() async {
final key1 = queryKey();
final key2 = queryKey();
await queryClient.prefetchQuery(
queryKey: key1,
queryFn: (_) => {"data": 'data1'},
);
await queryClient.prefetchQuery(
queryKey: key2,
queryFn: (_) {
return {"data": 'data2'};
},
);
expect(queryCache.findAll().length, 2);
});
});
});
}