From 0b4c030806152116396ba921dd9e445972c9a7d4 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 14 Feb 2022 12:02:30 +0600 Subject: [PATCH] utils tests created TQueryFnData, TData & TQueryData now only can be Maps or subclass of Map --- packages/fl_query/example/bin/example.dart | 5 +- packages/fl_query/lib/src/core/models.dart | 26 +- packages/fl_query/lib/src/core/query.dart | 19 +- .../fl_query/lib/src/core/query_cache.dart | 20 +- .../fl_query/lib/src/core/query_client.dart | 64 ++-- .../fl_query/lib/src/core/query_observer.dart | 116 ++++--- packages/fl_query/lib/src/core/retryer.dart | 4 +- packages/fl_query/lib/src/core/utils.dart | 81 +++-- .../fl_query/test/src/core/utils_test.dart | 320 ++++++++++++++++++ 9 files changed, 533 insertions(+), 122 deletions(-) create mode 100644 packages/fl_query/test/src/core/utils_test.dart diff --git a/packages/fl_query/example/bin/example.dart b/packages/fl_query/example/bin/example.dart index c4b83df..8240894 100644 --- a/packages/fl_query/example/bin/example.dart +++ b/packages/fl_query/example/bin/example.dart @@ -29,7 +29,8 @@ void main() async { var key = QueryKey("TEST"); QueryClient queryClient = QueryClient(); queryClient.mount(); - var data = await queryClient.fetchQuery( + var data = await queryClient + .fetchQuery, dynamic, Map>( queryKey: key, queryFn: (context) { return Future.value(todos.first); @@ -39,7 +40,7 @@ void main() async { print(data); print("======CACHED DATA======"); print(queryClient.getQueryData(key)); - queryClient.setQueryData(key, (prevData) { + queryClient.setQueryData>(key, (prevData) { return { ...(prevData) ?? {}, "title": "Yehi aloh heh", diff --git a/packages/fl_query/lib/src/core/models.dart b/packages/fl_query/lib/src/core/models.dart index a880959..357a7d3 100644 --- a/packages/fl_query/lib/src/core/models.dart +++ b/packages/fl_query/lib/src/core/models.dart @@ -9,16 +9,19 @@ typedef QueryKeyHashFunction = String Function(QueryKey queryKey); typedef QueryFunction = FutureOr Function( QueryFunctionContext context, ); -typedef GetPreviousPageParamFunction = Function( +typedef GetPreviousPageParamFunction> + = Function( TQueryFnData firstPage, List allPages, ); -typedef GetNextPageParamFunction = Function( +typedef GetNextPageParamFunction> + = Function( TQueryFnData firstPage, List allPages, ); -class QueryOptions { +class QueryOptions, TError, + TData extends Map> { ShouldRetryFunction? retry; RetryDelayFunction? retryDelay; Duration? cacheTime; @@ -259,7 +262,7 @@ enum QueryStatus { success, } -class QueryObserverResult { +class QueryObserverResult, TError> { TData? data; DateTime? dataUpdatedAt; TError? error; @@ -373,7 +376,11 @@ class QueryObserverResult { } } -typedef RefetchIntervalFunction +typedef RefetchIntervalFunction< + TQueryFnData extends Map, + TError, + TQueryData extends Map, + TData extends Map> = Duration? Function( TData? data, Query query, @@ -391,7 +398,11 @@ enum RefetchOnMount { always, } -class QueryObserverOptions +class QueryObserverOptions< + TQueryFnData extends Map, + TError, + TData extends Map, + TQueryData extends Map> extends QueryOptions { bool? enabled; Duration? staleTime; @@ -520,7 +531,8 @@ class DefaultOptions { }); } -class FetchQueryOptions +class FetchQueryOptions, TError, + TData extends Map> extends QueryOptions { /// The time after data is considered stale. /// If the data is fresh it will be returned from the cache. diff --git a/packages/fl_query/lib/src/core/query.dart b/packages/fl_query/lib/src/core/query.dart index a4d16a0..c3cebf0 100644 --- a/packages/fl_query/lib/src/core/query.dart +++ b/packages/fl_query/lib/src/core/query.dart @@ -17,7 +17,8 @@ class FetchOptions { FetchOptions({this.cancelRefetch, this.meta}); } -class FetchContext { +class FetchContext, TError, + TData extends Map> { FutureOr Function() fetchFn; FetchOptions? fetchOptions; QueryOptions options; @@ -35,12 +36,13 @@ class FetchContext { }); } -class QueryBehavior { +class QueryBehavior, TError, + TData extends Map> { void Function(FetchContext context) onFetch; QueryBehavior({required this.onFetch}); } -class QueryState { +class QueryState, TError> { TData? data; TError? error; QueryStatus status; @@ -120,7 +122,7 @@ class SetStateOptions { } } -class Action { +class Action, TError> { ActionType type; Object? meta; TData? data; @@ -160,7 +162,8 @@ class Action { } } -class Query { +class Query, TError, + TData extends Map> { QueryKey queryKey; String queryHash; late QueryOptions options; @@ -277,7 +280,7 @@ class Query { 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 ?? {} as TData, data); } // Set data and mark it as cached _dispatch(Action( @@ -360,7 +363,7 @@ class Query { var observer = _observers.firstWhereOrNull((x) => x.options.queryFn != null); if (observer != null) { - _setOptions(QueryOptions( + _setOptions(QueryOptions( queryKey: observer.options.queryKey, queryKeyHashFn: observer.options.queryKeyHashFn, cacheTime: observer.options.cacheTime, @@ -368,7 +371,7 @@ class Query { queryFn: observer.options.queryFn as QueryFunction, queryHash: observer.options.queryHash, - initialData: observer.options.initialData, + initialData: observer.options.initialData as TData?, initialDataUpdatedAt: observer.options.initialDataUpdatedAt, meta: observer.options.meta, structuralSharing: observer.options.structuralSharing, diff --git a/packages/fl_query/lib/src/core/query_cache.dart b/packages/fl_query/lib/src/core/query_cache.dart index 2b0e288..7918576 100644 --- a/packages/fl_query/lib/src/core/query_cache.dart +++ b/packages/fl_query/lib/src/core/query_cache.dart @@ -59,7 +59,10 @@ class QueryCache extends Subscribable { _queriesMap = {}, super(); - Query build( + Query build< + TQueryFnData extends Map, + TError, + TData extends Map>( QueryClient client, QueryOptions options, [ QueryState? state, @@ -124,8 +127,10 @@ class QueryCache extends Subscribable { }); } - Query? get( - String queryHash) { + Query? get< + TQueryFnData extends Map, + TError, + TData extends Map>(String queryHash) { return _queriesMap[queryHash] as Query?; } @@ -133,13 +138,16 @@ class QueryCache extends Subscribable { return _queries; } - Query? find( + Query? find< + TQueryFnData extends Map, + TError, + TData extends Map>( QueryKey queryKey, QueryFilters queryFilters, ) { queryFilters.exact ??= true; - return _queries - .firstWhereOrNull((query) => matchQuery(queryFilters, query)); + return _queries.firstWhereOrNull((query) => matchQuery(queryFilters, query)) + as Query?; } List findAll(QueryKey? queryKey, [QueryFilters? filters]) { diff --git a/packages/fl_query/lib/src/core/query_client.dart b/packages/fl_query/lib/src/core/query_client.dart index 0c6d61c..5a87d50 100644 --- a/packages/fl_query/lib/src/core/query_client.dart +++ b/packages/fl_query/lib/src/core/query_client.dart @@ -26,7 +26,7 @@ class MutationDefaults { // }); } -class QueryData { +class QueryData> { QueryKey queryKey; TData data; QueryData({ @@ -85,17 +85,18 @@ class QueryClient { // return _mutationCache.findAll({ ...filters, fetching: true }).length // } - TData? getQueryData( + TData? getQueryData>( QueryKey queryKey, [ QueryFilters? filters, ]) { return _queryCache - .find(queryKey, filters ?? QueryFilters()) + .find>( + queryKey, filters ?? QueryFilters()) ?.state - .data; + .data as TData?; } - List> getQueriesData({ + List> getQueriesData>({ QueryKey? queryKey, QueryFilters? filters, }) { @@ -107,26 +108,27 @@ class QueryClient { }).toList(); } - TData setQueryData( + TData setQueryData>( QueryKey queryKey, DataUpdateFunction updater, [ DateTime? updatedAt, ]) { - final QueryOptions defaultedOptions = - QueryOptions.fromJson( - defaultQueryOptions( - QueryObserverOptions( - queryKey: queryKey)) + final QueryOptions, dynamic, TData> defaultedOptions = + QueryOptions, dynamic, TData>.fromJson( + defaultQueryOptions, dynamic, TData, + Map>( + QueryObserverOptions, dynamic, TData, + Map>(queryKey: queryKey)) .toJson()); return _queryCache - .build(this, defaultedOptions) + .build, dynamic, TData>(this, defaultedOptions) .setData( updater, updatedAt: updatedAt, ); } - List setQueriesData({ + List setQueriesData>({ required DataUpdateFunction updater, QueryKey? queryKey, QueryFilters? filters, @@ -149,12 +151,13 @@ class QueryClient { .toList(); } - QueryState? getQueryState( + QueryState? + getQueryState, TError>( QueryKey queryKey, [ QueryFilters? filters, ]) { return _queryCache - .find( + .find>( queryKey, filters ?? QueryFilters(), ) @@ -257,13 +260,15 @@ class QueryClient { return future; } - Future fetchQuery({ + Future fetchQuery, TError, + TData extends Map>({ QueryKey? queryKey, QueryFunction? queryFn, FetchQueryOptions? options, }) { var defaultedOptions = this.defaultQueryOptions( - QueryObserverOptions( + QueryObserverOptions, dynamic, Map, + TData>( queryFn: queryFn, queryKey: queryKey, staleTime: options?.staleTime, @@ -280,28 +285,33 @@ class QueryClient { ); // returning 0 indicates turing off retry defaultedOptions.retry ??= (_, __) => 0; - var query = _queryCache.build(this, defaultedOptions); + var query = _queryCache.build, dynamic, TData>( + this, defaultedOptions); return query.isStaleByTime(defaultedOptions.staleTime) ? query.fetch(defaultedOptions) : Future.value(query.state.data as TData); } - Future prefetchQuery({ + Future prefetchQuery, TError, + TData extends Map>({ QueryKey? queryKey, QueryFunction? queryFn, FetchQueryOptions? options, }) { - return fetchQuery( + return fetchQuery( queryKey: queryKey, queryFn: queryFn, options: options, ).then(noop).catchError(noop); } - QueryObserverOptions - defaultQueryOptions( - QueryObserverOptions? - options) { + QueryObserverOptions defaultQueryOptions< + TQueryFnData extends Map, + TError, + TData extends Map, + TQueryData extends Map>( + QueryObserverOptions? options) { if (options?.defaulted == true) return options!; var defaultedOptions = QueryObserverOptions.fromJson({ @@ -322,7 +332,11 @@ class QueryClient { } QueryObserverOptions - defaultQueryObserverOptions([ + defaultQueryObserverOptions< + TQueryFnData extends Map, + TError, + TData extends Map, + TQueryData extends Map>([ QueryObserverOptions? options, ]) { return this.defaultQueryOptions(options); diff --git a/packages/fl_query/lib/src/core/query_observer.dart b/packages/fl_query/lib/src/core/query_observer.dart index 716b9c9..fe2ff10 100644 --- a/packages/fl_query/lib/src/core/query_observer.dart +++ b/packages/fl_query/lib/src/core/query_observer.dart @@ -15,8 +15,8 @@ import 'package:fl_query/src/core/subscribable.dart'; import 'package:fl_query/src/core/utils.dart'; import 'package:meta/meta.dart'; -typedef QueryObserverListener = void Function( - QueryObserverResult result); +typedef QueryObserverListener, TError> = void + Function(QueryObserverResult result); class NotifyOptions { bool? cache; @@ -52,13 +52,18 @@ class ObserverFetchOptions extends FetchOptions { }) : super(cancelRefetch: cancelRefetch, meta: meta); } -class SelectQuery { +class SelectQuery, + TData extends Map> { TData Function(TQueryData data) fn; TData result; SelectQuery(this.fn, this.result); } -class QueryObserver +class QueryObserver< + TQueryFnData extends Map, + TError, + TData extends Map, + TQueryData extends Map> extends Subscribable { late QueryObserverOptions options; QueryClient _client; @@ -70,7 +75,7 @@ class QueryObserver _currentResultOptions; QueryObserverResult? _previousQueryResult; Exception? _previousSelectError; - SelectQuery? _previousSelect; + SelectQuery? _previousSelect; Timer? _staleTimeout; Timer? _refetchInterval; Duration? _currentRefetchInterval; @@ -119,8 +124,8 @@ class QueryObserver QueryObserverOptions? options, [ NotifyOptions? notifyOptions, ]) { - var prevOptions = this.options; - var prevQuery = _currentQuery; + final prevOptions = this.options; + final prevQuery = _currentQuery; this.options = this._client.defaultQueryObserverOptions(options); @@ -190,7 +195,7 @@ class QueryObserver Future> getNextResult( bool? throwOnError, ) { - var completer = Completer>(); + final completer = Completer>(); var unsubscribe; unsubscribe = subscribe((result) { if (!result.isFetching) { @@ -215,8 +220,8 @@ class QueryObserver Future> fetchOptimistic( QueryObserverOptions options) { - var defaultedOptions = _client.defaultQueryObserverOptions(options); - var query = _client.getQueryCache().build(_client, defaultedOptions); + final defaultedOptions = _client.defaultQueryObserverOptions(options); + final query = _client.getQueryCache().build(_client, defaultedOptions); return query.fetch().then((val) { return createResult(query, defaultedOptions); @@ -269,7 +274,7 @@ class QueryObserver Map prevResultMap = prevResult.toJson(); return resultMap.keys.any((key) { - var changed = resultMap[key] != prevResultMap[key]; + final changed = resultMap[key] != prevResultMap[key]; bool? isIncluded = includedProps?.any((x) => x == key); bool isExcluded = options.notifyOnChangePropsExclusions?.any((x) => x == key) ?? false; @@ -280,7 +285,7 @@ class QueryObserver } void updateResult([NotifyOptions? notifyOptions]) { - QueryObserverResult? prevResult = _currentResult; + final QueryObserverResult? prevResult = _currentResult; if (_currentQuery != null) _currentResult = this.createResult(_currentQuery!, this.options); @@ -288,7 +293,7 @@ class QueryObserver _currentResultOptions = this.options; // Only notify if something has changed - if (shallowEqualMap(_currentResult.toJson(), prevResult.toJson())) { + if (shallowEqualMap(_currentResult.toJson(), prevResult?.toJson())) { return; } NotifyOptions defaultNotifyOptions = NotifyOptions(cache: true); @@ -304,11 +309,12 @@ class QueryObserver } void _updateQuery() { - var query = this._client.getQueryCache().build(this._client, this.options); + final query = + this._client.getQueryCache().build(this._client, this.options); if (query == _currentQuery) return; - var prevQuery = _currentQuery; + final prevQuery = _currentQuery; _currentQuery = query; _currentQueryInitialState = query.state; _previousQueryResult = _currentResult; @@ -339,22 +345,22 @@ class QueryObserver Query query, QueryObserverOptions options, ) { - var prevQuery = _currentQuery; - var prevOptions = this.options; - var prevResult = _currentResult; - var prevResultState = _currentResultState; - var prevResultOptions = _currentResultOptions; - bool queryChange = query != prevQuery; - var queryInitialState = + final prevQuery = _currentQuery; + final prevOptions = this.options; + final prevResult = _currentResult; + final prevResultState = _currentResultState; + final prevResultOptions = _currentResultOptions; + final bool queryChange = query != prevQuery; + final queryInitialState = queryChange ? query.state : _currentQueryInitialState; - var prevQueryResult = queryChange ? _currentResult : _previousQueryResult; + final prevQueryResult = queryChange ? _currentResult : _previousQueryResult; - var state = query.state; - var dataUpdatedAt = state.dataUpdatedAt; - var error = state.error; - var errorUpdatedAt = state.errorUpdatedAt; - var isFetching = state.isFetching; - var status = state.status; + final state = query.state; + DateTime? dataUpdatedAt = state.dataUpdatedAt; + TError? error = state.error; + DateTime? errorUpdatedAt = state.errorUpdatedAt; + bool isFetching = state.isFetching; + QueryStatus status = state.status; bool isPreviousData = false; bool isPlaceholderData = false; @@ -362,9 +368,9 @@ class QueryObserver // Optimistically set result in fetching state if needed if (options.optimisticResults == true) { - var mounted = hasListeners(); + final bool mounted = hasListeners(); - var fetchOnMount = !mounted && shouldFetchOnMount(query, options); + final bool fetchOnMount = !mounted && shouldFetchOnMount(query, options); bool fetchOptionally = mounted && prevQuery != null && @@ -401,7 +407,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( @@ -429,7 +435,7 @@ 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; } else { @@ -439,7 +445,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) { @@ -459,7 +465,8 @@ class QueryObserver } } - QueryObserverResult result = QueryObserverResult( + final QueryObserverResult result = + QueryObserverResult( status: status, dataUpdatedAt: dataUpdatedAt, isLoading: status == QueryStatus.loading, @@ -602,7 +609,11 @@ class QueryObserver } } -bool shouldLoadOnMount( +bool shouldLoadOnMount< + TQueryFnData extends Map, + TError, + TData extends Map, + TQueryData extends Map>( Query query, QueryObserverOptions options, ) { @@ -612,7 +623,11 @@ bool shouldLoadOnMount( options.retryOnMount == false)); } -bool shouldRefetchOnMount( +bool shouldRefetchOnMount< + TQueryFnData extends Map, + TError, + TData extends Map, + TQueryData extends Map>( Query query, QueryObserverOptions options, ) { @@ -623,7 +638,11 @@ bool shouldRefetchOnMount( isStale(query, options)))); } -bool shouldFetchOnMount( +bool shouldFetchOnMount< + TQueryFnData extends Map, + TError, + TData extends Map, + TQueryData extends Map>( Query query, QueryObserverOptions options, ) { @@ -631,17 +650,26 @@ bool shouldFetchOnMount( shouldRefetchOnMount(query, options)); } -bool shouldFetchOnReconnect( +bool shouldFetchOnReconnect< + TQueryFnData extends Map, + TError, + TData extends Map, + TQueryData extends Map>( Query query, QueryObserverOptions options, ) { return (options.enabled != false && (options.refetchOnReconnect == RefetchOnReconnect.always || (options.refetchOnReconnect != RefetchOnReconnect.off && - isStale(query, options)))); + isStale( + query, options)))); } -bool shouldFetchOptionally( +bool shouldFetchOptionally< + TQueryFnData extends Map, + TError, + TData extends Map, + TQueryData extends Map>( Query query, Query prevQuery, QueryObserverOptions options, @@ -653,7 +681,11 @@ bool shouldFetchOptionally( isStale(query, options)); } -bool isStale( +bool isStale< + TQueryFnData extends Map, + TError, + TData extends Map, + TQueryData extends Map>( Query query, QueryObserverOptions options, ) { diff --git a/packages/fl_query/lib/src/core/retryer.dart b/packages/fl_query/lib/src/core/retryer.dart index 2370da0..523b9f0 100644 --- a/packages/fl_query/lib/src/core/retryer.dart +++ b/packages/fl_query/lib/src/core/retryer.dart @@ -36,9 +36,9 @@ bool isCancelledError(value) { } typedef OnError = void Function(TError error); -typedef OnData = void Function(TData data); +typedef OnData> = void Function(TData data); -class Retryer { +class Retryer, TError> { late void Function({bool? revert, bool? silent}) cancel; late void Function() cancelRetry; late void Function() continueRetry; diff --git a/packages/fl_query/lib/src/core/utils.dart b/packages/fl_query/lib/src/core/utils.dart index 74c6f0b..25806ff 100644 --- a/packages/fl_query/lib/src/core/utils.dart +++ b/packages/fl_query/lib/src/core/utils.dart @@ -1,6 +1,7 @@ import 'package:fl_query/src/core/models.dart'; import 'package:fl_query/src/core/query.dart'; import 'package:fl_query/src/core/query_key.dart'; +import 'package:collection/collection.dart'; /// Default query keys hash function. /// Dummy function just to fill the gaps for original react-query like @@ -95,41 +96,61 @@ bool shallowEqualMap(Map? a, Map? b) { /// If not, it will replace any deeply equal children of `b` with those /// of `a`\ /// This can be used for structural sharing between JSON values for example. -/// `a` & `b` can only be Type of [Iterable] or [Map] -T replaceEqualDeep(T a, T b) { +/// `a` & `b` can only be Type of [List] or [Map] +replaceEqualDeep(a, b) { if (a == b) { return a; } - bool isList = (a is Iterable && b is Iterable); - if (isList || (a is Map && b is Map)) { - int aSize = isList ? a.length : (a as Map).keys.length; - List bItems = (isList ? b : (b as Map).keys).toList(); - int bSize = bItems.length; - var copy; - - int equalItems = 0; - - for (int i = 0; i < bSize; i++) { - var key = isList ? i : bItems[i]; - if (isList) { - copy ??= []; - copy[key] = replaceEqualDeep((a as List)[key], (b as List)[key]); - if (copy[key] == a[key]) { - equalItems++; - } - } else { - copy ??= {}; - copy[key] = replaceEqualDeep((a as Map)[key], (b as Map)[key]); - if (copy[key] == a[key]) { - equalItems++; - } - } - } - - return aSize == bSize && equalItems == aSize ? a : copy as T; + int aSize; + List bItems; + int bSize; + int equalItems = 0; + onEqual() => equalItems++; + var copy; + if (a is List && b is List) { + aSize = a.length; + bItems = b; + bSize = bItems.length; + copy = replaceEqualDeepList(a, b, onEqual); + } else if (a is Map && b is Map) { + aSize = a.keys.length; + bItems = b.keys.toList(); + bSize = bItems.length; + copy = replaceEqualDeepMap(a, b, onEqual); + } else { + return b; } - return b; + return aSize == bSize && equalItems == aSize ? a : copy; +} + +Map replaceEqualDeepMap(Map a, Map b, void Function() onEqual) { + final copy = Map.from(a); + copy.clear(); + for (final bEntry in b.entries) { + final aItem = a[bEntry.key]; + copy[bEntry.key] = + aItem != null ? replaceEqualDeep(aItem, bEntry.value) : bEntry.value; + if (copy[bEntry.key] == aItem) { + onEqual(); + } + } + return copy; +} + +List replaceEqualDeepList(List a, List b, void Function() onEqual) { + final copy = List.of(a, growable: true); + copy.clear(); + for (final bEntry in b.asMap().entries) { + final aItem = a.firstWhereIndexedOrNull((i, _) => i == bEntry.key); + final result = + aItem != null ? replaceEqualDeep(aItem, bEntry.value) : bEntry.value; + copy.add(result); + if (copy.last == aItem) { + onEqual(); + } + } + return copy; } Duration timeUntilStale(DateTime updatedAt, [Duration? staleTime]) => diff --git a/packages/fl_query/test/src/core/utils_test.dart b/packages/fl_query/test/src/core/utils_test.dart new file mode 100644 index 0000000..77aa934 --- /dev/null +++ b/packages/fl_query/test/src/core/utils_test.dart @@ -0,0 +1,320 @@ +import 'package:fl_query/src/core/utils.dart'; +import 'package:test/expect.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + group('core/utils', () { + group('replaceEqualDeep', () { + test( + 'Should return the previous value When the next value is an equal primitive', + () { + expect(replaceEqualDeep(1, 1), equals(1)); + expect(replaceEqualDeep('1', '1'), equals('1')); + expect(replaceEqualDeep(true, true), true); + expect(replaceEqualDeep(false, false), false); + expect(replaceEqualDeep(null, null), null); + }); + test( + 'Should return the next value When the previous value is a different value', + () { + expect(replaceEqualDeep(1, 0), equals(0)); + expect(replaceEqualDeep(1, 2), equals(2)); + expect(replaceEqualDeep('1', '2'), equals('2')); + expect(replaceEqualDeep(true, false), equals(false)); + expect(replaceEqualDeep(false, true), equals(true)); + }); + + test( + 'Should return the next value When the previous value is a different type', + () { + final array = [1]; + final object = {"a": "a"}; + expect(replaceEqualDeep(0, null), equals(null)); + expect(replaceEqualDeep(null, 0), equals(0)); + expect(replaceEqualDeep(2, null), equals(null)); + expect(replaceEqualDeep(null, 2), equals(2)); + expect(replaceEqualDeep({}, null), equals(null)); + expect(replaceEqualDeep([], null), equals(null)); + expect(replaceEqualDeep(array, object), equals(object)); + expect(replaceEqualDeep(object, array), equals(array)); + }); + + test( + 'Should return the previous value When the next value is an equal array', + () { + final prev = [1, 2]; + final next = [1, 2]; + expect(replaceEqualDeep(prev, next), equals(prev)); + }); + + test( + 'Should return a copy When the previous value is a different array subset', + () { + final prev = [1, 2]; + final next = [1, 2, 3]; + final result = replaceEqualDeep(prev, next); + expect(result, equals(next)); + expect(result, isNot(equals(prev))); + }); + + test( + 'Should return the previous value When the next value is an equal empty array', + () { + final prev = []; + final next = []; + expect(replaceEqualDeep(prev, next), equals(prev)); + }); + + test( + 'Should return the previous value When the next value is an equal empty object', + () { + final prev = {}; + final next = {}; + expect(replaceEqualDeep(prev, next), equals(prev)); + }); + + test( + 'Should return the previous value When the next value is an equal object', + () { + final prev = {"a": 'a'}; + final next = {"a": 'a'}; + expect(replaceEqualDeep(prev, next), equals(prev)); + }); + + test('Should replace different values in objects', () { + final prev = { + "a": {"b": 'b'}, + "c": 'c' + }; + final next = { + "a": {"b": 'b'}, + "c": 'd' + }; + final result = replaceEqualDeep(prev, next); + expect(result, equals(next)); + expect(result, isNot(prev)); + expect(result["a"], equals(prev["a"])); + expect(result["c"], equals(next["c"])); + }); + + test('Should replace different values in arrays', () { + final prev = [ + 1, + {"a": 'a'}, + { + "b": {"b": 'b'} + }, + [1] + ]; + final next = [ + 1, + {"a": 'a'}, + { + "b": {"b": 'c'} + }, + [1] + ]; + final result = replaceEqualDeep(prev, next); + expect(result, equals(next)); + expect(result, isNot(prev)); + expect(result[0], prev[0]); + expect(result[1], prev[1]); + expect((result[2] as Map)["b"]["b"], (next[2] as Map)["b"]["b"]); + expect(result[3], prev[3]); + }); + + test( + 'Should replace different values in arrays When the next value is a subset', + () { + final prev = [ + {"a": 'a'}, + {"b": 'b'}, + {"c": 'c'} + ]; + final next = [ + {"a": 'a'}, + {"b": 'b'} + ]; + final result = replaceEqualDeep(prev, next); + expect(result, equals(next)); + expect(result, isNot(prev)); + expect(result[0], prev[0]); + expect(result[1], prev[1]); + expect(() => result[2], throwsRangeError); + }); + + test( + 'Should replace different values in arrays When the next value is a superset', + () { + final prev = [ + {"a": 'a'}, + {"b": 'b'} + ]; + final next = [ + {"a": 'a'}, + {"b": 'b'}, + {"c": 'c'} + ]; + final result = replaceEqualDeep(prev, next); + expect(result, equals(next)); + expect(result[0], equals(prev[0])); + expect(result[1], equals(prev[1])); + expect(result[2], equals(next[2])); + }); + + test('Should copy objects which are not arrays or objects', () { + final prev = [ + {"a": 'a'}, + {"b": 'b'}, + {"c": 'c'}, + 1 + ]; + final next = [ + {"a": 'a'}, + Map(), + {"c": 'c'}, + 2 + ]; + final result = replaceEqualDeep(prev, next); + expect(result[0], equals(prev[0])); + expect(result[1], equals(next[1])); + expect(result[2], equals(prev[2])); + expect(result[3], equals(next[3])); + }); + + test('Should support equal objects which are not arrays or objects', () { + final map = new Map(); + final prev = [ + map, + [1] + ]; + final next = [ + map, + [1] + ]; + final result = replaceEqualDeep(prev, next); + expect(result, equals(prev)); + }); + + test('Should support non equal objects which are not arrays or objects', + () { + final map1 = new Map(); + final map2 = new Map(); + final prev = [ + map1, + [1] + ]; + final next = [ + map2, + [1] + ]; + final result = replaceEqualDeep(prev, next); + expect(result[0], equals(next[0])); + expect(result[1], equals(prev[1])); + }); + + test('Should replace all parent objects if some nested value changes', + () { + final prev = { + "todo": { + "id": '1', + "meta": {"createdAt": 0}, + "state": {"done": false}, + }, + "otherTodo": { + "id": '2', + "meta": {"createdAt": 0}, + "state": {"done": true}, + }, + }; + final next = { + "todo": { + "id": '1', + "meta": {"createdAt": 0}, + "state": {"done": true}, + }, + "otherTodo": { + "id": '2', + "meta": {"createdAt": 0}, + "state": {"done": true}, + }, + }; + final result = replaceEqualDeep(prev, next); + expect(result, equals(next)); + expect(result["todo"] == prev["todo"], isFalse); + expect(result["todo"] == next["todo"], isFalse); + expect(result["todo"]["id"], equals((next["todo"] as Map)["id"])); + expect(result["todo"]["meta"], equals((prev["todo"] as Map)["meta"])); + expect( + result["todo"]["state"], + equals((next["todo"] as Map)["state"]), + ); + expect( + result["todo"]["state"]["done"], + (next["todo"] as Map)["state"]["done"], + ); + expect(result["otherTodo"], prev["otherTodo"]); + }); + + test('Should replace all parent arrays if some nested value changes', () { + final Map> prev = { + "todos": [ + { + "id": '1', + "meta": {"createdAt": 0}, + "state": {"done": false} + }, + { + "id": '2', + "meta": {"createdAt": 0}, + "state": {"done": true} + }, + ], + }; + final Map> next = { + "todos": [ + { + "id": '1', + "meta": {"createdAt": 0}, + "state": {"done": true} + }, + { + "id": '2', + "meta": {"createdAt": 0}, + "state": {"done": true} + }, + ], + }; + final result = replaceEqualDeep(prev, next); + expect(result, equals(next)); + expect(result["todos"][0], isNot(equals(prev["todos"]?.first))); + expect( + result["todos"][0]?["id"], + equals(next["todos"]?.first["id"]), + ); + expect( + result["todos"][0]?["meta"], + equals(prev["todos"]?.first["meta"]), + ); + expect(result["todos"][0]?["state"]["done"], + next["todos"]?.first["state"]["done"]); + expect(result["todos"][1], equals(prev["todos"]?[1])); + }); + }); + + group( + 'matchMutation', + () => { + // test('should return false if mutationKey options is undefined', () => { + // const filters = { mutationKey: 'key1' }; + // const queryClient = new QueryClient(); + // const mutation = new Mutation({ + // mutationId: 1, + // mutationCache: queryClient.getMutationCache(), + // options: {}, + // }) + // expect(matchMutation(filters, mutation)).toBeFalsy() + // }) + }); + }); +}