diff --git a/packages/fl_query/lib/src/core/models.dart b/packages/fl_query/lib/src/core/models.dart index 061a18e..6258cd4 100644 --- a/packages/fl_query/lib/src/core/models.dart +++ b/packages/fl_query/lib/src/core/models.dart @@ -1,10 +1,14 @@ +import 'dart:async'; + import 'package:fl_query/src/core/query.dart'; import 'package:fl_query/src/core/query_key.dart'; import 'package:fl_query/src/core/retryer.dart'; typedef QueryMeta = Map; typedef QueryKeyHashFunction = String Function(QueryKey queryKey); -typedef QueryFunction = Function(QueryFunctionContext context); +typedef QueryFunction = FutureOr Function( + QueryFunctionContext context, +); typedef GetPreviousPageParamFunction = Function( TQueryFnData firstPage, List allPages, @@ -64,6 +68,7 @@ class QueryOptions { this.structuralSharing, this.getPreviousPageParam, this.getNextPageParam, + this.behavior, }); QueryOptions.fromJson(Map json) { @@ -144,36 +149,40 @@ class RefetchPageFilters { refetchPage; } -class RefetchQueryFilters - implements QueryFilters, RefetchPageFilters { - @override - bool? active; - @override - bool? exact; - @override - bool? fetching; - @override - bool? inactive; - @override - bool Function(Query query)? predicate; - @override - bool? queryKey; +class RefetchableQueryFilters extends QueryFilters + implements RefetchPageFilters { @override bool Function(TPageData lastPage, int index, List allPages)? refetchPage; - @override - bool? stale; - - RefetchQueryFilters({ - this.active, - this.exact, - this.inactive, - this.predicate, - this.queryKey, - this.stale, - this.fetching, + RefetchableQueryFilters({ + bool? active, + bool? exact, + bool? inactive, + bool Function(Query query)? predicate, + bool? queryKey, + bool? stale, + bool? fetching, this.refetchPage, - }); + }) : super( + active: active, + exact: exact, + fetching: fetching, + inactive: inactive, + predicate: predicate, + queryKey: queryKey, + stale: stale, + ); + + RefetchableQueryFilters.fromJson(Map json) { + active = json["active"]; + exact = json["exact"]; + inactive = json["inactive"]; + queryKey = json["queryKey"]; + stale = json["stale"]; + fetching = json["fetching"]; + predicate = json["predicate"]; + refetchPage = json["refetchPage"]; + } @override Map toJson() { @@ -185,19 +194,62 @@ class RefetchQueryFilters "stale": stale, "fetching": fetching, "predicate": predicate, + "refetchPage": refetchPage, }; } } -class ResultOptions { - bool? throwOnError; -} +class InvalidateQueryFilters + extends RefetchableQueryFilters { + bool? refetchActive; + bool? refetchInactive; -class RefetchOptions implements ResultOptions { - bool? cancelRefetch; + InvalidateQueryFilters({ + bool? active, + bool? exact, + bool? inactive, + bool Function(Query query)? predicate, + bool? queryKey, + bool? stale, + bool? fetching, + bool Function(TPageData lastPage, int index, List allPages)? + refetchPage, + this.refetchActive, + this.refetchInactive, + }) : super( + active: active, + exact: exact, + fetching: fetching, + inactive: inactive, + predicate: predicate, + queryKey: queryKey, + stale: stale, + refetchPage: refetchPage, + ); + + InvalidateQueryFilters.fromJson(Map json) + : super.fromJson(json) { + refetchActive = json["refetchActive"]; + refetchInactive = json["refetchInactive"]; + } @override + Map toJson() { + return { + ...super.toJson(), + "refetchActive": refetchActive, + "refetchInactive": refetchInactive, + }; + } +} + +class RefetchOptions { bool? throwOnError; + bool? cancelRefetch; + RefetchOptions({ + this.cancelRefetch, + this.throwOnError, + }); } enum QueryStatus { @@ -228,7 +280,7 @@ class QueryObserverResult { bool isSuccess; Future> Function({ RefetchOptions options, - RefetchQueryFilters filters, + RefetchableQueryFilters filters, }) refetch; void Function() remove; QueryStatus status; @@ -402,6 +454,48 @@ class QueryObserverOptions structuralSharing: structuralSharing, defaulted: defaulted, ); + + QueryObserverOptions.fromJson(Map json) + : enabled = json["enabled"], + staleTime = json["staleTime"], + refetchInterval = json["refetchInterval"], + refetchIntervalInBackground = json["refetchIntervalInBackground"], + refetchOnReconnect = json["refetchOnReconnect"], + refetchOnMount = json["refetchOnMount"], + retryOnMount = json["retryOnMount"], + onSuccess = json["onSuccess"], + onError = json["onError"], + onSettled = json["onSettled"], + useErrorBoundary = json["useErrorBoundary"], + select = json["select"], + suspense = json["suspense"], + keepPreviousData = json["keepPreviousData"], + placeholderData = json["placeholderData"], + optimisticResults = json["optimisticResults"], + super.fromJson(json); + + @override + Map toJson() { + return { + ...super.toJson(), + "enabled": enabled, + "staleTime": staleTime, + "refetchInterval": refetchInterval, + "refetchIntervalInBackground": refetchIntervalInBackground, + "refetchOnReconnect": refetchOnReconnect, + "refetchOnMount": refetchOnMount, + "retryOnMount": retryOnMount, + "onSuccess": onSuccess, + "onError": onError, + "onSettled": onSettled, + "useErrorBoundary": useErrorBoundary, + "select": select, + "suspense": suspense, + "keepPreviousData": keepPreviousData, + "placeholderData": placeholderData, + "optimisticResults": optimisticResults, + }; + } } class QueryFunctionContext { @@ -417,3 +511,66 @@ class QueryFunctionContext { this.meta, }); } + +class DefaultOptions { + QueryObserverOptions? queries; + // MutationObserverOptions? mutations; + DefaultOptions({ + this.queries, + }); +} + +class FetchQueryOptions + extends QueryOptions { + /** + * The time in milliseconds after data is considered stale. + * If the data is fresh it will be returned from the cache. + */ + Duration? staleTime; + FetchQueryOptions( + ShouldRetryFunction? retry, + RetryDelayFunction? retryDelay, + Duration? cacheTime, + bool Function(TData? oldData, TData newData)? isDataEqual, + QueryFunction? queryFn, + QueryKey? queryKey, + String? queryHash, + QueryKeyHashFunction? queryKeyHashFn, + TData? initialData, + DateTime? initialDataUpdatedAt, + QueryBehavior? behavior, + bool? structuralSharing, + GetPreviousPageParamFunction? getPreviousPageParam, + GetNextPageParamFunction? getNextPageParam, + bool? defaulted, + this.staleTime, + ) : super( + retry: retry, + retryDelay: retryDelay, + cacheTime: cacheTime, + isDataEqual: isDataEqual, + queryFn: queryFn, + queryKey: queryKey, + queryHash: queryHash, + queryKeyHashFn: queryKeyHashFn, + initialData: initialData, + initialDataUpdatedAt: initialDataUpdatedAt, + behavior: behavior, + structuralSharing: structuralSharing, + getPreviousPageParam: getPreviousPageParam, + getNextPageParam: getNextPageParam, + defaulted: defaulted, + ); + + FetchQueryOptions.fromJson(Map json) + : staleTime = json["staleTime"], + super.fromJson(json); + + @override + Map toJson() { + return { + ...super.toJson(), + "staleTime": staleTime, + }; + } +} diff --git a/packages/fl_query/lib/src/core/query_cache.dart b/packages/fl_query/lib/src/core/query_cache.dart index 684b0c8..0130614 100644 --- a/packages/fl_query/lib/src/core/query_cache.dart +++ b/packages/fl_query/lib/src/core/query_cache.dart @@ -73,9 +73,13 @@ class QueryCache extends Subscribable { cache: this, queryKey: queryKey, queryHash: queryHash, - options: client.defaultQueryOptions(options), + options: client.defaultQueryOptions( + QueryObserverOptions.fromJson(options.toJson()), + ), state: state, - defaultOptions: client.getQueryDefaults(queryKey), + defaultOptions: QueryOptions.fromJson( + client.getQueryDefaults(queryKey)?.toJson() ?? {}, + ), meta: options.meta, ); add(query); diff --git a/packages/fl_query/lib/src/core/query_client.dart b/packages/fl_query/lib/src/core/query_client.dart index 58e70c5..7f12eeb 100644 --- a/packages/fl_query/lib/src/core/query_client.dart +++ b/packages/fl_query/lib/src/core/query_client.dart @@ -1,48 +1,371 @@ import 'package:fl_query/src/core/models.dart'; +import 'package:fl_query/src/core/notify_manager.dart'; +import 'package:fl_query/src/core/online_manager.dart'; +import 'package:fl_query/src/core/query.dart'; import 'package:fl_query/src/core/query_cache.dart'; +import 'package:fl_query/src/core/query_key.dart'; +import 'package:fl_query/src/core/query_observer.dart'; +import 'package:fl_query/src/core/utils.dart'; +import 'package:collection/collection.dart'; + +class QueryDefaults { + QueryKey queryKey; + QueryOptions defaultOptions; + QueryDefaults({ + required this.queryKey, + required this.defaultOptions, + }); +} + +class MutationDefaults { + // QueryKey queryKey; + // QueryOptions defaultOptions; + // MutationDefaults({ + // required this.queryKey, + // required this.defaultOptions, + // }); +} + +class QueryData { + QueryKey queryKey; + TData data; + QueryData({ + required this.queryKey, + required this.data, + }); +} class QueryClient { - Object? options; QueryCache _queryCache; - QueryCache _mutationCache; + // QueryCache _mutationCache; + DefaultOptions _defaultOptions; + List _queryDefaults; + // List _mutationDefaults; + void Function()? _unsubscribeFocus; + void Function()? _unsubscribeOnline; + // MutationKey _mutationKey; + // MutationOptions _mutationDefaultOptions; QueryClient({ QueryCache? queryCache, QueryCache? mutationCache, - this.options, + DefaultOptions? defaultOptions, }) : _queryCache = queryCache ?? QueryCache(), - _mutationCache = mutationCache ?? QueryCache() {} + _defaultOptions = defaultOptions ?? DefaultOptions(), + _queryDefaults = []; + /* _mutationDefaults = [], */ + /* _mutationCache = mutationCache ?? QueryCache() */ + + void mount() { + // this.unsubscribeFocus = focusManager.subscribe(() => { + // if (focusManager.isFocused() && onlineManager.isOnline()) { + // this.mutationCache.onFocus() + // this.queryCache.onFocus() + // } + // }) + _unsubscribeOnline = onlineManager.subscribe(() async { + if (/* focusManager.isFocused() && */ await onlineManager.isOnline()) { + // _mutationCache.onOnline(); + _queryCache.onOnline(); + } + }); + } + + void unmount() { + _unsubscribeFocus?.call(); + _unsubscribeOnline?.call(); + } + + int isFetching({QueryKey? queryKey, QueryFilters? filters}) { + filters?.fetching = true; + return _queryCache.findAll(null, filters).length; + } + + // int isMutating([MutationFilters? filters]) { + // return _mutationCache.findAll({ ...filters, fetching: true }).length + // } + + TData? getQueryData( + QueryKey, + queryKey, [ + QueryFilters? filters, + ]) { + return _queryCache + .find(queryKey, filters ?? QueryFilters()) + ?.state + .data; + } + + List> getQueriesData({ + QueryKey? queryKey, + QueryFilters? filters, + }) { + return getQueryCache().findAll(queryKey, filters).map((query) { + return QueryData( + data: query.state.data as TData, + queryKey: query.queryKey, + ); + }).toList(); + } + + TData setQueryData( + QueryKey queryKey, + DataUpdateFunction updater, [ + DateTime? updatedAt, + ]) { + var defaultedOptions = + defaultQueryOptions(QueryObserverOptions(queryKey: queryKey)); + return _queryCache.build(this, defaultedOptions).setData( + updater as Function(dynamic), + updatedAt: updatedAt, + ); + } + + List setQueriesData({ + required DataUpdateFunction updater, + QueryKey? queryKey, + QueryFilters? filters, + DateTime? updatedAt, + }) { + if (queryKey == 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( + (query) => QueryData( + queryKey: query.queryKey, + data: setQueryData( + query.queryKey, + updater, + updatedAt, + ), + ), + )) + .toList(); + } + + QueryState? getQueryState( + QueryKey, + queryKey, [ + QueryFilters? filters, + ]) { + return _queryCache + .find( + queryKey, + filters ?? QueryFilters(), + ) + ?.state as QueryState?; + } + + void removeQueries({QueryKey? queryKey, QueryFilters? filters}) { + notifyManager.batch( + () => { + _queryCache.findAll(queryKey, filters).forEach((query) { + _queryCache.remove(query); + }) + }, + ); + } + + Future resetQueries({ + QueryKey? queryKey, + RefetchableQueryFilters? filters, + bool? throwOnError, + }) { + filters?.active = true; + var refetchFilters = RefetchableQueryFilters.fromJson({ + ...(filters?.toJson() ?? {}), + "active": true, + }); + + return notifyManager.batch(() { + _queryCache.findAll(queryKey, filters).forEach((query) { + query.reset(); + }); + return refetchQueries( + filters: refetchFilters, + options: RefetchOptions(throwOnError: throwOnError), + ); + }); + } + + Future cancelQueries({ + QueryKey? queryKey, + QueryFilters? filters, + bool? revert = true, + bool? silent, + }) { + var futures = notifyManager.batch(() => + _queryCache.findAll(queryKey, filters).map((query) => query.cancel( + revert: revert, + silent: silent, + ))); + return Future.wait(futures).then(noop).catchError(noop); + } + + Future invalidateQueries({ + QueryKey? queryKey, + InvalidateQueryFilters? filters, + RefetchOptions? options, + }) { + var refetchFilters = RefetchableQueryFilters.fromJson({ + ...(filters?.toJson() ?? {}), + // if filters.refetchActive is not provided and filters.active is explicitly false, + // e.g. invalidateQueries({ active: false }), we don't want to refetch active queries + "active": filters?.refetchActive ?? filters?.active ?? true, + "inactive": filters?.refetchInactive ?? false, + }); + return notifyManager.batch(() { + _queryCache.findAll(queryKey, filters).forEach((query) { + query.invalidate(); + }); + return this.refetchQueries( + filters: refetchFilters, + options: options, + ); + }); + } + + Future refetchQueries({ + QueryKey? queryKey, + RefetchableQueryFilters? filters, + RefetchOptions? options, + }) { + var futures = notifyManager.batch( + () => _queryCache.findAll(queryKey, filters).map( + (query) => query.fetch( + null, + ObserverFetchOptions( + cancelRefetch: options?.cancelRefetch, + throwOnError: options?.throwOnError, + meta: {"refetchPage": filters?.refetchPage}, + ), + ), + ), + ); + + var future = Future.wait(futures).then(noop); + + if (options?.throwOnError == false) { + future = future.catchError(noop); + } + + return future; + } + + Future fetchQuery({ + QueryKey? queryKey, + QueryFunction? queryFn, + FetchQueryOptions? options, + }) { + var defaultedOptions = this.defaultQueryOptions( + QueryObserverOptions( + queryFn: queryFn, + queryKey: queryKey, + staleTime: options?.staleTime, + cacheTime: options?.cacheTime, + defaulted: options?.defaulted, + initialData: options?.initialData, + initialDataUpdatedAt: options?.initialDataUpdatedAt, + isDataEqual: options?.isDataEqual, + meta: options?.meta, + queryHash: options?.queryHash, + queryKeyHashFn: options?.queryKeyHashFn, + structuralSharing: options?.structuralSharing, + ), + ); + // returning 0 indicates turing off retry + defaultedOptions.retry ??= (_, __) => 0; + var query = _queryCache.build(this, defaultedOptions); + return query.isStaleByTime(defaultedOptions.staleTime) + ? query.fetch(defaultedOptions) + : Future.value(query.state.data as TData); + } + + Future prefetchQuery({ + QueryKey? queryKey, + QueryFunction? queryFn, + FetchQueryOptions? options, + }) { + return fetchQuery( + queryKey: queryKey, + queryFn: queryFn, + options: options, + ).then(noop).catchError(noop); + } QueryObserverOptions defaultQueryOptions( QueryObserverOptions? - options) {} + options) { + if (options?.defaulted == true) return options!; + var defaultedOptions = + QueryObserverOptions.fromJson({ + ...(_defaultOptions.queries?.toJson() ?? {}), + ...(getQueryDefaults(options?.queryKey)?.toJson() ?? {}), + ...(options?.toJson() ?? {}), + "defaulted": true, + }); + if (defaultedOptions.queryHash == null && + defaultedOptions.queryKey != null) { + defaultedOptions.queryHash = hashQueryKeyByOptions( + defaultedOptions.queryKey!, + defaultedOptions, + ); + } + + return defaultedOptions; + } QueryObserverOptions - defaultQueryObserverOptions( - QueryObserverOptions? - options) { + defaultQueryObserverOptions([ + QueryObserverOptions? options, + ]) { return this.defaultQueryOptions(options); } - fetchQuery() {} - getQueryData() {} - setQueryData() {} - getQueryState() {} - invalidateQueries() {} - refetchQueries() {} - cancelQueries() {} - removeQueries() {} - resetQueries() {} - bool get isFetching => false; - bool get isMutating => false; - getDefaultOptions() {} - setDefaultOptions() {} - getQueryDefaults() {} - setQueryDefaults() {} + DefaultOptions getDefaultOptions() { + return _defaultOptions; + } + + void setDefaultOptions(DefaultOptions options) { + _defaultOptions = options; + } + + QueryObserverOptions? getQueryDefaults([QueryKey? queryKey]) { + return queryKey != null + ? QueryObserverOptions.fromJson((_queryDefaults + .firstWhereOrNull( + (x) => queryKey.key == x.queryKey.key, + ) + ?.defaultOptions) + ?.toJson() ?? + {}) + : null; + } + + void setQueryDefaults(QueryKey queryKey, QueryObserverOptions options) { + var result = _queryDefaults.firstWhereOrNull( + (x) => queryKey.key == x.queryKey.key, + ); + + if (result != null) { + result.defaultOptions = options; + } else { + _queryDefaults + .add(QueryDefaults(queryKey: queryKey, defaultOptions: options)); + } + } + getMutationDefaults() {} setMutationDefaults() {} - QueryCache getQueryCache() {} + QueryCache getQueryCache() { + return _queryCache; + } + getMutationCache() {} - clear() {} + void clear() { + _queryCache.clear(); + // _mutationCache.clear(); + } } diff --git a/packages/fl_query/lib/src/core/query_observer.dart b/packages/fl_query/lib/src/core/query_observer.dart index 289b806..716b9c9 100644 --- a/packages/fl_query/lib/src/core/query_observer.dart +++ b/packages/fl_query/lib/src/core/query_observer.dart @@ -188,13 +188,14 @@ class QueryObserver } Future> getNextResult( - ResultOptions? options) { + bool? throwOnError, + ) { var completer = Completer>(); var unsubscribe; unsubscribe = subscribe((result) { if (!result.isFetching) { unsubscribe?.call(); - if (result.isError && options?.throwOnError == true) { + if (result.isError && throwOnError == true) { if (!completer.isCompleted) completer.completeError(result.error as Object); } else { @@ -588,7 +589,7 @@ class QueryObserver } Future> refetch({ - RefetchQueryFilters? filters, + RefetchableQueryFilters? filters, RefetchOptions? options, }) { return fetch(