diff --git a/packages/fl_query/lib/src/core/models.dart b/packages/fl_query/lib/src/core/models.dart index 357a7d3..e2cfc46 100644 --- a/packages/fl_query/lib/src/core/models.dart +++ b/packages/fl_query/lib/src/core/models.dart @@ -537,7 +537,7 @@ class FetchQueryOptions, TError, /// The time after data is considered stale. /// If the data is fresh it will be returned from the cache. Duration? staleTime; - FetchQueryOptions( + FetchQueryOptions({ ShouldRetryFunction? retry, RetryDelayFunction? retryDelay, Duration? cacheTime, @@ -554,7 +554,7 @@ class FetchQueryOptions, TError, GetNextPageParamFunction? getNextPageParam, bool? defaulted, this.staleTime, - ) : super( + }) : super( retry: retry, retryDelay: retryDelay, cacheTime: cacheTime, diff --git a/packages/fl_query/lib/src/core/query.dart b/packages/fl_query/lib/src/core/query.dart index c3cebf0..55415e9 100644 --- a/packages/fl_query/lib/src/core/query.dart +++ b/packages/fl_query/lib/src/core/query.dart @@ -280,7 +280,7 @@ class Query, TError, data = prevData as TData; } else if (this.options.structuralSharing != false) { // 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 _dispatch(Action( diff --git a/packages/fl_query/lib/src/core/query_cache.dart b/packages/fl_query/lib/src/core/query_cache.dart index 7918576..923fe3b 100644 --- a/packages/fl_query/lib/src/core/query_cache.dart +++ b/packages/fl_query/lib/src/core/query_cache.dart @@ -139,26 +139,31 @@ class QueryCache extends Subscribable { } Query? find< - TQueryFnData extends Map, - TError, - TData extends Map>( - QueryKey queryKey, - QueryFilters queryFilters, - ) { + TQueryFnData extends Map, + TError, + TData extends Map>(QueryKey queryKey, + [QueryFilters? queryFilters]) { + queryFilters ??= QueryFilters(); queryFilters.exact ??= true; - return _queries.firstWhereOrNull((query) => matchQuery(queryFilters, query)) - as Query?; + return _queries.firstWhereOrNull((query) => matchQuery( + queryFilters!, + query, + queryKey, + )) as Query?; } - List findAll(QueryKey? queryKey, [QueryFilters? filters]) { - if (queryKey == null && filters == 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 + List findAll([QueryKey? queryKeys, QueryFilters? filters]) { + return filters == null && queryKeys == null ? _queries - : _queries.where((query) => matchQuery(filters!, query)).toList(); + : _queries + .where( + (query) => matchQuery( + filters ?? QueryFilters(), + query, + queryKeys, + ), + ) + .toList(); } void notify(QueryCacheNotifyEvent event) { @@ -169,12 +174,6 @@ class QueryCache extends Subscribable { }); } - @override - void onSubscribe() {} - - @override - void onUnsubscribe() {} - /// Dummy function just to keep the API similar to react-query void onFocus() {} diff --git a/packages/fl_query/lib/src/core/query_client.dart b/packages/fl_query/lib/src/core/query_client.dart index 5a87d50..ecfed27 100644 --- a/packages/fl_query/lib/src/core/query_client.dart +++ b/packages/fl_query/lib/src/core/query_client.dart @@ -97,10 +97,10 @@ class QueryClient { } List> getQueriesData>({ - QueryKey? queryKey, + QueryKey? queryKeys, QueryFilters? filters, }) { - return getQueryCache().findAll(queryKey, filters).map((query) { + return getQueryCache().findAll(queryKeys, filters).map((query) { return QueryData( data: query.state.data as TData, queryKey: query.queryKey, @@ -130,15 +130,15 @@ class QueryClient { List setQueriesData>({ required DataUpdateFunction updater, - QueryKey? queryKey, + QueryKey? queryKeys, QueryFilters? filters, DateTime? updatedAt, }) { - if (queryKey == null && filters == null) + if (queryKeys == null && filters == null) throw Exception( "[QueryClient.setQueriesData] both `queryKey` & `filters` can't be null at the same time"); return notifyManager - .batch(() => getQueryCache().findAll(queryKey, filters).map( + .batch(() => getQueryCache().findAll(queryKeys, filters).map( (query) => QueryData( queryKey: query.queryKey, data: setQueryData( @@ -164,10 +164,10 @@ class QueryClient { ?.state as QueryState?; } - void removeQueries({QueryKey? queryKey, QueryFilters? filters}) { + void removeQueries({QueryKey? queryKeys, QueryFilters? filters}) { notifyManager.batch( () => { - _queryCache.findAll(queryKey, filters).forEach((query) { + _queryCache.findAll(queryKeys, filters).forEach((query) { _queryCache.remove(query); }) }, @@ -175,7 +175,7 @@ class QueryClient { } Future resetQueries({ - QueryKey? queryKey, + QueryKey? queryKeys, RefetchableQueryFilters? filters, bool? throwOnError, }) { @@ -186,7 +186,7 @@ class QueryClient { }); return notifyManager.batch(() { - _queryCache.findAll(queryKey, filters).forEach((query) { + _queryCache.findAll(queryKeys, filters).forEach((query) { query.reset(); }); return refetchQueries( @@ -197,13 +197,13 @@ class QueryClient { } Future cancelQueries({ - QueryKey? queryKey, + QueryKey? queryKeys, QueryFilters? filters, bool? revert = true, bool? silent, }) { var futures = notifyManager.batch(() => - _queryCache.findAll(queryKey, filters).map((query) => query.cancel( + _queryCache.findAll(queryKeys, filters).map((query) => query.cancel( revert: revert, silent: silent, ))); @@ -211,7 +211,7 @@ class QueryClient { } Future invalidateQueries({ - QueryKey? queryKey, + QueryKey? queryKeys, InvalidateQueryFilters? filters, RefetchOptions? options, }) { @@ -223,7 +223,7 @@ class QueryClient { "inactive": filters?.refetchInactive ?? false, }); return notifyManager.batch(() { - _queryCache.findAll(queryKey, filters).forEach((query) { + _queryCache.findAll(queryKeys, filters).forEach((query) { query.invalidate(); }); return this.refetchQueries( @@ -234,12 +234,12 @@ class QueryClient { } Future refetchQueries({ - QueryKey? queryKey, + QueryKey? queryKeys, RefetchableQueryFilters? filters, RefetchOptions? options, }) { var futures = notifyManager.batch( - () => _queryCache.findAll(queryKey, filters).map( + () => _queryCache.findAll(queryKeys, filters).map( (query) => query.fetch( null, ObserverFetchOptions( diff --git a/packages/fl_query/lib/src/core/subscribable.dart b/packages/fl_query/lib/src/core/subscribable.dart index f8d357e..315cbd1 100644 --- a/packages/fl_query/lib/src/core/subscribable.dart +++ b/packages/fl_query/lib/src/core/subscribable.dart @@ -6,14 +6,14 @@ abstract class Subscribable { Subscribable() : listeners = []; void Function() subscribe([TListener? listener]) { - var callback = listener ?? (() => null); + listener ??= (() => null) as TListener; - listeners.add(callback as TListener); + listeners.add(listener); onSubscribe(); return () { - listeners = listeners.where((x) => x != callback).toList(); + listeners = listeners.where((x) => x != listener).toList(); onUnsubscribe(); }; } @@ -23,8 +23,8 @@ abstract class Subscribable { } @protected - void onSubscribe(); + void onSubscribe() {} @protected - void onUnsubscribe(); + void onUnsubscribe() {} } diff --git a/packages/fl_query/lib/src/core/utils.dart b/packages/fl_query/lib/src/core/utils.dart index 25806ff..04d94a8 100644 --- a/packages/fl_query/lib/src/core/utils.dart +++ b/packages/fl_query/lib/src/core/utils.dart @@ -39,12 +39,20 @@ QueryStatusFilter mapQueryStatusFilter( } } -bool matchQuery(QueryFilters filters, Query query, [QueryKey? queryKey]) { - if (queryKey != null) { - if (filters.exact! && - query.queryHash != hashQueryKeyByOptions(queryKey, query.options)) +bool matchQuery( + QueryFilters filters, + Query query, [ + + /// multiple queryKeys to find the query + QueryKey? queryKeys, +]) { + if (queryKeys != null) { + if (filters.exact == true && + query.queryHash != hashQueryKeyByOptions(queryKeys, query.options)) 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 = mapQueryStatusFilter(filters.active, filters.inactive); diff --git a/packages/fl_query/test/helpers/utils.dart b/packages/fl_query/test/helpers/utils.dart new file mode 100644 index 0000000..b56c6c3 --- /dev/null +++ b/packages/fl_query/test/helpers/utils.dart @@ -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()}"); +} diff --git a/packages/fl_query/test/src/core/query_cache_test.dart b/packages/fl_query/test/src/core/query_cache_test.dart new file mode 100644 index 0000000..60aa816 --- /dev/null +++ b/packages/fl_query/test/src/core/query_cache_test.dart @@ -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 data = {'foo': "foo"}; + queryClient.setQueryData(key, (_) => data); + final query = queryCache.find(key); + await Future.delayed(Duration(milliseconds: 1)); + expect(_event, isA()); + 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()); + 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, dynamic, Map>( + 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); + }); + }); + }); +}