Initial Support
This commit is contained in:
@@ -1,4 +1,19 @@
|
||||
library fl_query;
|
||||
|
||||
export 'package:fl_query/src/core/core.dart';
|
||||
// export 'package:fl_query/src/core/framework.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FlQueryScope extends StatefulWidget {
|
||||
final Widget child;
|
||||
const FlQueryScope({required this.child, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<FlQueryScope> createState() => _FlQueryScopeState();
|
||||
}
|
||||
|
||||
class _FlQueryScopeState extends State<FlQueryScope> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Theme.of(context);
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum QueryStatus {
|
||||
failed,
|
||||
succeed,
|
||||
pending,
|
||||
refetching;
|
||||
}
|
||||
|
||||
typedef QueryTaskFunction<T> = FutureOr<T> Function(String);
|
||||
|
||||
typedef QueryListener<T> = FutureOr<void> Function(T);
|
||||
|
||||
typedef ListenerUnsubscriber = void Function();
|
||||
|
||||
class Query<T> extends ChangeNotifier {
|
||||
// all params
|
||||
final String queryKey;
|
||||
QueryTaskFunction<T> task;
|
||||
final int retries;
|
||||
final Duration retryDelay;
|
||||
|
||||
// all properties
|
||||
T? data;
|
||||
dynamic error;
|
||||
QueryStatus status;
|
||||
int retryAttempts = 0;
|
||||
late DateTime updatedAt;
|
||||
int refetchCount = 0;
|
||||
|
||||
@protected
|
||||
bool fetched = false;
|
||||
|
||||
final QueryListener<T>? _onData;
|
||||
final QueryListener<dynamic>? _onError;
|
||||
|
||||
Query({
|
||||
required this.queryKey,
|
||||
required this.task,
|
||||
this.retries = 3,
|
||||
this.retryDelay = const Duration(milliseconds: 200),
|
||||
T? initialData,
|
||||
QueryListener<T>? onData,
|
||||
QueryListener<dynamic>? onError,
|
||||
}) : status = QueryStatus.pending,
|
||||
data = initialData,
|
||||
_onData = onData,
|
||||
_onError = onError;
|
||||
|
||||
// all getters & setters
|
||||
bool get hasData => data != null && error == null;
|
||||
bool get hasError =>
|
||||
status == QueryStatus.failed && error != null && data == null;
|
||||
bool get isLoading =>
|
||||
status == QueryStatus.pending && data == null && error == null;
|
||||
bool get isRefetching =>
|
||||
status == QueryStatus.refetching && data == null && error == null;
|
||||
bool get isSucceeded => status == QueryStatus.succeed && data != null;
|
||||
|
||||
// all methods
|
||||
Future<void> _execute({bool isFetch = true}) async {
|
||||
try {
|
||||
retryAttempts = 0;
|
||||
status = isFetch ? QueryStatus.pending : QueryStatus.refetching;
|
||||
data = await task(queryKey);
|
||||
updatedAt = DateTime.now();
|
||||
status = QueryStatus.succeed;
|
||||
_onData?.call(data!);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
status = QueryStatus.failed;
|
||||
error = e;
|
||||
_onError?.call(e);
|
||||
notifyListeners();
|
||||
// retrying for retry count if failed for the first time
|
||||
while (retryAttempts <= retries) {
|
||||
await Future.delayed(retryDelay);
|
||||
try {
|
||||
data = await task(queryKey);
|
||||
status = QueryStatus.succeed;
|
||||
_onData?.call(data!);
|
||||
notifyListeners();
|
||||
break;
|
||||
} catch (e) {
|
||||
status = QueryStatus.failed;
|
||||
error = e;
|
||||
retryAttempts++;
|
||||
_onError?.call(e);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<T?> fetch() async {
|
||||
return _execute().then((_) {
|
||||
fetched = true;
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
Future<T?> refetch() {
|
||||
refetchCount++;
|
||||
return _execute(isFetch: false).then((_) => data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:fl_query/query.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class QueryBowlScope extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Duration? staleTime;
|
||||
const QueryBowlScope({
|
||||
required this.child,
|
||||
this.staleTime,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<QueryBowlScope> createState() => _QueryBowlScopeState();
|
||||
}
|
||||
|
||||
class _QueryBowlScopeState extends State<QueryBowlScope> {
|
||||
late Set<Query> queries;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
queries = {};
|
||||
}
|
||||
|
||||
void updateQueries() {
|
||||
setState(() {
|
||||
queries = Set.from(queries);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QueryBowl(
|
||||
onUpdate: updateQueries,
|
||||
queries: queries,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class QueryBowl extends InheritedWidget {
|
||||
final Set<Query> queries;
|
||||
final Duration staleTime;
|
||||
final void Function() onUpdate;
|
||||
|
||||
const QueryBowl({
|
||||
required Widget child,
|
||||
required this.onUpdate,
|
||||
required this.queries,
|
||||
this.staleTime = const Duration(minutes: 5),
|
||||
Key? key,
|
||||
}) : super(child: child, key: key);
|
||||
|
||||
listenToQueryUpdate() {
|
||||
for (final query in queries) {
|
||||
query.addListener(onUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
void disposeListeners() {
|
||||
for (final query in queries) {
|
||||
query.removeListener(onUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
Future<T?> fetchQuery<T>(Query<T> query) async {
|
||||
final prevQuery =
|
||||
queries.firstWhereOrNull((q) => q.queryKey == query.queryKey);
|
||||
if (prevQuery is Query<T>) {
|
||||
if (!prevQuery.hasData) {
|
||||
return prevQuery.fetched
|
||||
? await prevQuery.refetch()
|
||||
: await prevQuery.fetch();
|
||||
}
|
||||
return prevQuery.data;
|
||||
}
|
||||
queries.add(query);
|
||||
disposeListeners();
|
||||
listenToQueryUpdate();
|
||||
return await query.fetch();
|
||||
}
|
||||
|
||||
Query<T>? getQuery<T>(String queryKey) {
|
||||
return queries.firstWhereOrNull(
|
||||
(query) => query.queryKey == queryKey && query is Query<T>)
|
||||
as Query<T>?;
|
||||
}
|
||||
|
||||
static QueryBowl of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<QueryBowl>()!;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(QueryBowl oldWidget) {
|
||||
return oldWidget.staleTime != staleTime || oldWidget.queries != queries;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:fl_query/query.dart';
|
||||
import 'package:fl_query/query_bowl.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class QueryBuilder<T> extends StatefulWidget {
|
||||
final Widget Function(BuildContext, Query<T>) builder;
|
||||
final QueryTaskFunction<T> task;
|
||||
final String queryKey;
|
||||
const QueryBuilder({
|
||||
required this.builder,
|
||||
required this.task,
|
||||
required this.queryKey,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<QueryBuilder<T>> createState() => _QueryBuilderState<T>();
|
||||
}
|
||||
|
||||
class _QueryBuilderState<T> extends State<QueryBuilder<T>> {
|
||||
late Query<T> query;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
query = Query<T>(queryKey: widget.queryKey, task: widget.task);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await QueryBowl.of(context).fetchQuery(query);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final queryRT = QueryBowl.of(context).getQuery<T>(widget.queryKey) ?? query;
|
||||
return widget.builder(context, queryRT);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
export 'package:fl_query/src/core/retryer.dart' show CancelledError;
|
||||
export 'package:fl_query/src/core/query_cache.dart' show QueryCache;
|
||||
export 'package:fl_query/src/core/query_client.dart' show QueryClient;
|
||||
export 'package:fl_query/src/core/query_observer.dart' show QueryObserver;
|
||||
export 'package:fl_query/src/core/query_key.dart';
|
||||
// export 'package:fl_query/src/core/queriesObserver.dart' show QueriesObserver;
|
||||
// export 'package:fl_query/src/core/infiniteQueryObserver.dart' show InfiniteQueryObserver;
|
||||
// export 'package:fl_query/src/core/mutationCache.dart' show MutationCache;
|
||||
// export 'package:fl_query/src/core/mutationObserver.dart' show MutationObserver;
|
||||
// export 'package:fl_query/src/core/logger.dart' show setLogger;
|
||||
export 'package:fl_query/src/core/notify_manager.dart' show notifyManager;
|
||||
// export 'package:fl_query/src/core/focusManager.dart' show focusManager;
|
||||
export 'package:fl_query/src/core/online_manager.dart' show onlineManager;
|
||||
export 'package:fl_query/src/core/utils.dart' show hashQueryKey;
|
||||
export 'package:fl_query/src/core/retryer.dart' show isCancelledError;
|
||||
// export 'package:fl_query/src/core/hydration.dart' show dehydrate, DehydrateOptions, DehydratedState, HydrateOptions, ShouldDehydrateMutationFunction, ShouldDehydrateQueryFunction;
|
||||
|
||||
export 'package:fl_query/src/core/models.dart';
|
||||
export 'package:fl_query/src/core/query.dart' show Query;
|
||||
// export type { Mutation } from './mutation'
|
||||
// export type { Logger } from './logger'
|
||||
@@ -1,596 +0,0 @@
|
||||
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<T> = Map<String, T>;
|
||||
typedef QueryKeyHashFunction = String Function(QueryKey queryKey);
|
||||
typedef QueryFunction<T, TPageParam> = FutureOr<T> Function(
|
||||
QueryFunctionContext<TPageParam> context,
|
||||
);
|
||||
typedef GetPreviousPageParamFunction<TQueryFnData extends Map<String, dynamic>>
|
||||
= Function(
|
||||
TQueryFnData firstPage,
|
||||
List<TQueryFnData> allPages,
|
||||
);
|
||||
typedef GetNextPageParamFunction<TQueryFnData extends Map<String, dynamic>>
|
||||
= Function(
|
||||
TQueryFnData firstPage,
|
||||
List<TQueryFnData> allPages,
|
||||
);
|
||||
|
||||
class QueryOptions<TQueryFnData extends Map<String, dynamic>, TError,
|
||||
TData extends Map<String, dynamic>> {
|
||||
ShouldRetryFunction<TError>? retry;
|
||||
RetryDelayFunction<TError>? retryDelay;
|
||||
Duration? cacheTime;
|
||||
bool Function(TData? oldData, TData newData)? isDataEqual;
|
||||
QueryFunction<TQueryFnData, dynamic>? queryFn;
|
||||
QueryKey? queryKey;
|
||||
|
||||
/// Basically [QueryKey.key] in short form
|
||||
String? queryHash;
|
||||
QueryKeyHashFunction? queryKeyHashFn;
|
||||
TData? initialData;
|
||||
DateTime? initialDataUpdatedAt;
|
||||
QueryBehavior<TQueryFnData, TError, TData>? behavior;
|
||||
|
||||
/// Set this to `false` to disable structural sharing between query results\
|
||||
/// Defaults to `true`.
|
||||
bool? structuralSharing;
|
||||
|
||||
/// This function can be set to automatically get the previous cursor for infinite queries.
|
||||
/// The result will also be used to determine the value of `hasPreviousPage`.
|
||||
GetPreviousPageParamFunction<TQueryFnData>? getPreviousPageParam;
|
||||
|
||||
/// This function can be set to automatically get the next cursor for
|
||||
/// infinite queries.
|
||||
/// The result will also be used to determine the value of
|
||||
/// `hasNextPage`.
|
||||
GetNextPageParamFunction<TQueryFnData>? getNextPageParam;
|
||||
bool? defaulted;
|
||||
|
||||
/// Additional payload to be stored on each query.
|
||||
/// Use this property to pass information that can be used in other places.
|
||||
QueryMeta? meta;
|
||||
|
||||
QueryOptions({
|
||||
this.retry,
|
||||
this.retryDelay,
|
||||
this.queryKey,
|
||||
this.queryKeyHashFn,
|
||||
this.cacheTime,
|
||||
this.isDataEqual,
|
||||
this.queryFn,
|
||||
this.defaulted,
|
||||
this.initialData,
|
||||
this.initialDataUpdatedAt,
|
||||
this.meta,
|
||||
this.queryHash,
|
||||
this.structuralSharing,
|
||||
this.getPreviousPageParam,
|
||||
this.getNextPageParam,
|
||||
this.behavior,
|
||||
});
|
||||
|
||||
QueryOptions.fromJson(Map<String, dynamic> json) {
|
||||
queryKey = json["queryKey"];
|
||||
queryKeyHashFn = json["queryKeyHashFn"];
|
||||
cacheTime = json["cacheTime"];
|
||||
isDataEqual = json["isDataEqual"];
|
||||
queryFn = json["queryFn"];
|
||||
queryHash = json["queryHash"];
|
||||
initialData = json["initialData"];
|
||||
initialDataUpdatedAt = json["initialDataUpdatedAt"];
|
||||
meta = json["meta"];
|
||||
structuralSharing = json["structuralSharing"];
|
||||
defaulted = json["defaulted"];
|
||||
retry = json["retry"];
|
||||
retryDelay = json["retryDelay"];
|
||||
behavior = json["behavior"];
|
||||
getPreviousPageParam = json["getPreviousPageParam"];
|
||||
getNextPageParam = json["getNextPageParam"];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"queryKey": queryKey,
|
||||
"queryKeyHashFn": queryKeyHashFn,
|
||||
"cacheTime": cacheTime,
|
||||
"isDataEqual": isDataEqual,
|
||||
"queryFn": queryFn,
|
||||
"queryHash": queryHash,
|
||||
"initialData": initialData,
|
||||
"initialDataUpdatedAt": initialDataUpdatedAt,
|
||||
"meta": meta,
|
||||
"structuralSharing": structuralSharing,
|
||||
"defaulted": defaulted,
|
||||
"retry": retry,
|
||||
"retryDelay": retryDelay,
|
||||
"behavior": behavior,
|
||||
"getPreviousPageParam": getPreviousPageParam,
|
||||
"getNextPageParam": getNextPageParam,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class QueryFilters {
|
||||
bool? active;
|
||||
bool? exact;
|
||||
bool? inactive;
|
||||
bool Function(Query query)? predicate;
|
||||
QueryKey? queryKey;
|
||||
bool? stale;
|
||||
bool? fetching;
|
||||
|
||||
QueryFilters({
|
||||
this.active,
|
||||
this.exact,
|
||||
this.inactive,
|
||||
this.predicate,
|
||||
this.queryKey,
|
||||
this.stale,
|
||||
this.fetching,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"active": active,
|
||||
"exact": exact,
|
||||
"inactive": inactive,
|
||||
"queryKey": queryKey,
|
||||
"stale": stale,
|
||||
"fetching": fetching,
|
||||
"predicate": predicate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RefetchPageFilters<TPageData> {
|
||||
bool Function(TPageData lastPage, int index, List<TPageData> allPages)?
|
||||
refetchPage;
|
||||
}
|
||||
|
||||
class RefetchableQueryFilters<TPageData> extends QueryFilters
|
||||
implements RefetchPageFilters<TPageData> {
|
||||
@override
|
||||
bool Function(TPageData lastPage, int index, List<TPageData> allPages)?
|
||||
refetchPage;
|
||||
RefetchableQueryFilters({
|
||||
bool? active,
|
||||
bool? exact,
|
||||
bool? inactive,
|
||||
bool Function(Query query)? predicate,
|
||||
QueryKey? 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<String, dynamic> 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<String, dynamic> toJson() {
|
||||
return {
|
||||
"active": active,
|
||||
"exact": exact,
|
||||
"inactive": inactive,
|
||||
"queryKey": queryKey,
|
||||
"stale": stale,
|
||||
"fetching": fetching,
|
||||
"predicate": predicate,
|
||||
"refetchPage": refetchPage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidateQueryFilters<TPageData>
|
||||
extends RefetchableQueryFilters<TPageData> {
|
||||
bool? refetchActive;
|
||||
bool? refetchInactive;
|
||||
|
||||
InvalidateQueryFilters({
|
||||
bool? active,
|
||||
bool? exact,
|
||||
bool? inactive,
|
||||
bool Function(Query query)? predicate,
|
||||
QueryKey? queryKey,
|
||||
bool? stale,
|
||||
bool? fetching,
|
||||
bool Function(TPageData lastPage, int index, List<TPageData> 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<String, dynamic> json)
|
||||
: super.fromJson(json) {
|
||||
refetchActive = json["refetchActive"];
|
||||
refetchInactive = json["refetchInactive"];
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
...super.toJson(),
|
||||
"refetchActive": refetchActive,
|
||||
"refetchInactive": refetchInactive,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RefetchOptions {
|
||||
bool? throwOnError;
|
||||
bool? cancelRefetch;
|
||||
RefetchOptions({
|
||||
this.cancelRefetch,
|
||||
this.throwOnError,
|
||||
});
|
||||
}
|
||||
|
||||
enum QueryStatus {
|
||||
idle,
|
||||
loading,
|
||||
error,
|
||||
success,
|
||||
}
|
||||
|
||||
class QueryObserverResult<TData extends Map<String, dynamic>, TError> {
|
||||
TData? data;
|
||||
DateTime? dataUpdatedAt;
|
||||
TError? error;
|
||||
DateTime? errorUpdatedAt;
|
||||
int failureCount;
|
||||
bool isError;
|
||||
bool isFetched;
|
||||
bool isFetchedAfterMount;
|
||||
bool isFetching;
|
||||
bool isIdle;
|
||||
bool isLoading;
|
||||
bool isLoadingError;
|
||||
bool isPlaceholderData;
|
||||
bool isPreviousData;
|
||||
bool isRefetchError;
|
||||
bool isRefetching;
|
||||
bool isStale;
|
||||
bool isSuccess;
|
||||
Future<QueryObserverResult<TData, TError>?> Function<TPageData>({
|
||||
RefetchOptions options,
|
||||
RefetchableQueryFilters<TPageData> filters,
|
||||
}) refetch;
|
||||
void Function() remove;
|
||||
QueryStatus status;
|
||||
|
||||
QueryObserverResult({
|
||||
required this.failureCount,
|
||||
required this.isError,
|
||||
required this.isFetched,
|
||||
required this.isFetchedAfterMount,
|
||||
required this.isFetching,
|
||||
required this.isIdle,
|
||||
required this.isLoading,
|
||||
required this.isLoadingError,
|
||||
required this.isPlaceholderData,
|
||||
required this.isPreviousData,
|
||||
required this.isRefetchError,
|
||||
required this.isRefetching,
|
||||
required this.isStale,
|
||||
required this.isSuccess,
|
||||
required this.refetch,
|
||||
required this.remove,
|
||||
required this.status,
|
||||
this.data,
|
||||
this.error,
|
||||
this.dataUpdatedAt,
|
||||
this.errorUpdatedAt,
|
||||
}) {
|
||||
String errorLabel =
|
||||
"[QueryObserverResult.QueryObserverResult] status = `$status` but parent has wrong set of properties";
|
||||
if (status == QueryStatus.idle &&
|
||||
(data != null ||
|
||||
error != null ||
|
||||
isError ||
|
||||
!isIdle ||
|
||||
isLoading ||
|
||||
isLoadingError ||
|
||||
isRefetchError ||
|
||||
isSuccess)) throw Exception(errorLabel);
|
||||
|
||||
if (status == QueryStatus.loading &&
|
||||
(data != null ||
|
||||
error != null ||
|
||||
isError ||
|
||||
isIdle ||
|
||||
!isLoading ||
|
||||
isLoadingError ||
|
||||
isRefetchError ||
|
||||
isSuccess != false)) throw Exception(errorLabel);
|
||||
|
||||
if (status == QueryStatus.error &&
|
||||
((!(error is TError)) || !isError || isIdle || isLoading || isSuccess))
|
||||
throw Exception(errorLabel);
|
||||
|
||||
if (status == QueryStatus.success &&
|
||||
(!(data is TData) ||
|
||||
error != null ||
|
||||
isError ||
|
||||
isIdle ||
|
||||
isLoading ||
|
||||
isLoadingError ||
|
||||
isRefetchError ||
|
||||
!isSuccess)) throw Exception(errorLabel);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {
|
||||
'data': this.data,
|
||||
'dataUpdatedAt': dataUpdatedAt,
|
||||
'error': error,
|
||||
'errorUpdatedAt': errorUpdatedAt,
|
||||
'failureCount': failureCount,
|
||||
'isError': isError,
|
||||
'isFetched': isFetched,
|
||||
'isFetchedAfterMount': isFetchedAfterMount,
|
||||
'isFetching': isFetching,
|
||||
'isIdle': isIdle,
|
||||
'isLoading': isLoading,
|
||||
'isLoadingError': isLoadingError,
|
||||
'isPlaceholderData': isPlaceholderData,
|
||||
'isPreviousData': isPreviousData,
|
||||
'isRefetchError': isRefetchError,
|
||||
'isRefetching': isRefetching,
|
||||
'isStale': isStale,
|
||||
'isSuccess': isSuccess,
|
||||
'refetch': refetch,
|
||||
'remove': remove,
|
||||
'status': status,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
typedef RefetchIntervalFunction<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TQueryData extends Map<String, dynamic>,
|
||||
TData extends Map<String, dynamic>>
|
||||
= Duration? Function(
|
||||
TData? data,
|
||||
Query<TQueryFnData, TError, TQueryData> query,
|
||||
);
|
||||
|
||||
enum RefetchOnReconnect {
|
||||
on,
|
||||
off,
|
||||
always,
|
||||
}
|
||||
|
||||
enum RefetchOnMount {
|
||||
on,
|
||||
off,
|
||||
always,
|
||||
}
|
||||
|
||||
class QueryObserverOptions<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>,
|
||||
TQueryData extends Map<String, dynamic>>
|
||||
extends QueryOptions<TQueryFnData, TError, TQueryData> {
|
||||
bool? enabled;
|
||||
Duration? staleTime;
|
||||
RefetchIntervalFunction<TQueryFnData, TError, TQueryData, TData>?
|
||||
refetchInterval;
|
||||
bool? refetchIntervalInBackground;
|
||||
RefetchOnReconnect? refetchOnReconnect;
|
||||
RefetchOnMount? refetchOnMount;
|
||||
bool? retryOnMount;
|
||||
OnData? onSuccess;
|
||||
OnError? onError;
|
||||
void Function(TData? data, [TError? error])? onSettled;
|
||||
bool Function(TError error)? useErrorBoundary;
|
||||
TData Function(TQueryData? data)? select;
|
||||
bool? suspense;
|
||||
bool? keepPreviousData;
|
||||
TQueryData? placeholderData;
|
||||
bool? optimisticResults;
|
||||
/*List<String>|'tracked'?*/ dynamic notifyOnChangeProps;
|
||||
List<String>? notifyOnChangePropsExclusions;
|
||||
|
||||
QueryObserverOptions({
|
||||
this.enabled,
|
||||
this.staleTime,
|
||||
this.refetchInterval,
|
||||
this.refetchIntervalInBackground,
|
||||
this.refetchOnReconnect,
|
||||
this.refetchOnMount,
|
||||
this.retryOnMount,
|
||||
this.onSuccess,
|
||||
this.onError,
|
||||
this.onSettled,
|
||||
this.useErrorBoundary,
|
||||
this.select,
|
||||
this.suspense,
|
||||
this.keepPreviousData,
|
||||
this.placeholderData,
|
||||
this.optimisticResults,
|
||||
QueryKey? queryKey,
|
||||
QueryKeyHashFunction? queryKeyHashFn,
|
||||
Duration? cacheTime,
|
||||
bool Function(TQueryData? oldData, TQueryData newData)? isDataEqual,
|
||||
QueryFunction<TQueryFnData, dynamic>? queryFn,
|
||||
String? queryHash,
|
||||
TQueryData? initialData,
|
||||
DateTime? initialDataUpdatedAt,
|
||||
QueryMeta? meta,
|
||||
bool? structuralSharing,
|
||||
bool? defaulted,
|
||||
ShouldRetryFunction<TError>? retry,
|
||||
RetryDelayFunction<TError>? retryDelay,
|
||||
QueryBehavior<TQueryFnData, TError, TQueryData>? behavior,
|
||||
GetPreviousPageParamFunction<TQueryFnData>? getPreviousPageParam,
|
||||
GetNextPageParamFunction<TQueryFnData>? getNextPageParam,
|
||||
}) : super(
|
||||
queryKey: queryKey,
|
||||
queryKeyHashFn: queryKeyHashFn,
|
||||
cacheTime: cacheTime,
|
||||
isDataEqual: isDataEqual,
|
||||
queryFn: queryFn,
|
||||
queryHash: queryHash,
|
||||
initialData: initialData,
|
||||
initialDataUpdatedAt: initialDataUpdatedAt,
|
||||
meta: meta,
|
||||
structuralSharing: structuralSharing,
|
||||
defaulted: defaulted,
|
||||
behavior: behavior,
|
||||
getNextPageParam: getNextPageParam,
|
||||
getPreviousPageParam: getPreviousPageParam,
|
||||
retry: retry,
|
||||
retryDelay: retryDelay,
|
||||
);
|
||||
|
||||
QueryObserverOptions.fromJson(Map<String, dynamic> 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<String, dynamic> 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<TPageParam> {
|
||||
QueryKey queryKey;
|
||||
/* AbortSignal */ dynamic? signal;
|
||||
TPageParam? pageParam;
|
||||
QueryMeta? meta;
|
||||
|
||||
QueryFunctionContext({
|
||||
required this.queryKey,
|
||||
this.signal,
|
||||
this.pageParam,
|
||||
this.meta,
|
||||
});
|
||||
}
|
||||
|
||||
class DefaultOptions<TError> {
|
||||
QueryObserverOptions<dynamic, TError, dynamic, dynamic>? queries;
|
||||
// MutationObserverOptions<dynamic, TError, dynamic>? mutations;
|
||||
DefaultOptions({
|
||||
this.queries,
|
||||
});
|
||||
}
|
||||
|
||||
class FetchQueryOptions<TQueryFnData extends Map<String, dynamic>, TError,
|
||||
TData extends Map<String, dynamic>>
|
||||
extends QueryOptions<TQueryFnData, TError, TData> {
|
||||
/// The time after data is considered stale.
|
||||
/// If the data is fresh it will be returned from the cache.
|
||||
Duration? staleTime;
|
||||
FetchQueryOptions({
|
||||
ShouldRetryFunction<TError>? retry,
|
||||
RetryDelayFunction<TError>? retryDelay,
|
||||
Duration? cacheTime,
|
||||
bool Function(TData? oldData, TData newData)? isDataEqual,
|
||||
QueryFunction<TQueryFnData, dynamic>? queryFn,
|
||||
QueryKey? queryKey,
|
||||
String? queryHash,
|
||||
QueryKeyHashFunction? queryKeyHashFn,
|
||||
TData? initialData,
|
||||
DateTime? initialDataUpdatedAt,
|
||||
QueryBehavior<TQueryFnData, TError, TData>? behavior,
|
||||
bool? structuralSharing,
|
||||
GetPreviousPageParamFunction<TQueryFnData>? getPreviousPageParam,
|
||||
GetNextPageParamFunction<TQueryFnData>? 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<String, dynamic> json)
|
||||
: staleTime = json["staleTime"],
|
||||
super.fromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
...super.toJson(),
|
||||
"staleTime": staleTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// TYPES
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
typedef NotifyCallback = void Function();
|
||||
|
||||
typedef NotifyFunction = void Function(void Function() callback);
|
||||
|
||||
typedef BatchNotifyFunction = void Function(void Function() callback);
|
||||
|
||||
class NotifyManager {
|
||||
List<NotifyCallback> _queue;
|
||||
int _transactions;
|
||||
late NotifyFunction _notifyFn;
|
||||
late BatchNotifyFunction _batchNotifyFn;
|
||||
|
||||
NotifyManager()
|
||||
: _queue = [],
|
||||
_transactions = 0 {
|
||||
_notifyFn = (void Function() callback) {
|
||||
callback();
|
||||
};
|
||||
|
||||
_batchNotifyFn = (void Function() callback) {
|
||||
callback();
|
||||
};
|
||||
}
|
||||
|
||||
T batch<T>(T Function() callback) {
|
||||
final T result;
|
||||
_transactions++;
|
||||
try {
|
||||
result = callback();
|
||||
} finally {
|
||||
_transactions--;
|
||||
if (_transactions == 0) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void schedule(NotifyCallback callback) {
|
||||
if (_transactions > 0) {
|
||||
_queue.add(callback);
|
||||
} else {
|
||||
scheduleMicrotask(() {
|
||||
_notifyFn(callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// All calls to the wrapped function will be batched.
|
||||
T batchCalls<T extends void Function(List? args)>(T callback) {
|
||||
void fn(List? args) {
|
||||
schedule(() {
|
||||
callback(args);
|
||||
});
|
||||
}
|
||||
|
||||
;
|
||||
return fn as T;
|
||||
}
|
||||
|
||||
void flush() {
|
||||
var queue = _queue;
|
||||
_queue = [];
|
||||
if (queue.isNotEmpty) {
|
||||
scheduleMicrotask(() {
|
||||
_batchNotifyFn(() {
|
||||
queue.forEach((fn) {
|
||||
_notifyFn(fn);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
///Use this method to set a custom notify function.
|
||||
void setNotifyFunction(NotifyFunction fn) {
|
||||
_notifyFn = fn;
|
||||
}
|
||||
|
||||
/// Use this method to set a custom function to batch notifications
|
||||
/// together into a single tick.
|
||||
/// By default React Query will use the batch function provided by
|
||||
/// ReactDOM or React Native.
|
||||
void setBatchNotifyFunction(BatchNotifyFunction fn) {
|
||||
_batchNotifyFn = fn;
|
||||
}
|
||||
}
|
||||
|
||||
// SINGLETON
|
||||
|
||||
NotifyManager notifyManager = new NotifyManager();
|
||||
@@ -1,72 +0,0 @@
|
||||
import 'package:fl_query/src/core/subscribable.dart';
|
||||
import 'package:internet_connection_checker/internet_connection_checker.dart';
|
||||
|
||||
typedef SetupFn = void Function()? Function(
|
||||
void Function([bool? online]) setOnline);
|
||||
|
||||
class OnlineManager extends Subscribable {
|
||||
bool? _online;
|
||||
void Function()? _cleanup;
|
||||
SetupFn? _setup;
|
||||
|
||||
OnlineManager([InternetConnectionChecker? connectionChecker]) {
|
||||
connectionChecker ??= InternetConnectionChecker();
|
||||
_setup = (listener) {
|
||||
var subscription =
|
||||
connectionChecker!.onStatusChange.listen((status) => listener());
|
||||
return () {
|
||||
subscription.cancel();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void onSubscribe() {
|
||||
if (_cleanup == null) {
|
||||
setEventListener(_setup!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onUnsubscribe() {
|
||||
if (!hasListeners()) {
|
||||
_cleanup?.call();
|
||||
_cleanup = null;
|
||||
}
|
||||
}
|
||||
|
||||
void setEventListener(SetupFn setup) {
|
||||
_setup = setup;
|
||||
_cleanup?.call();
|
||||
_cleanup = setup(([bool? online]) {
|
||||
if (online != null) {
|
||||
setOnline(online);
|
||||
} else {
|
||||
onOnline();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setOnline(bool? online) {
|
||||
_online = online;
|
||||
if (online != null && online) {
|
||||
onOnline();
|
||||
}
|
||||
}
|
||||
|
||||
void onOnline() {
|
||||
listeners.forEach((listener) {
|
||||
listener();
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> isOnline() {
|
||||
if (_online != null) {
|
||||
return Future.value(_online!);
|
||||
}
|
||||
|
||||
return InternetConnectionChecker().hasConnection;
|
||||
}
|
||||
}
|
||||
|
||||
OnlineManager onlineManager = OnlineManager();
|
||||
@@ -1,666 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_query/src/core/models.dart';
|
||||
import 'package:fl_query/src/core/notify_manager.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/retryer.dart';
|
||||
import 'package:fl_query/src/core/utils.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class FetchOptions {
|
||||
bool? cancelRefetch;
|
||||
dynamic meta;
|
||||
FetchOptions({this.cancelRefetch, this.meta});
|
||||
}
|
||||
|
||||
class FetchContext<TQueryFnData extends Map<String, dynamic>, TError,
|
||||
TData extends Map<String, dynamic>> {
|
||||
FutureOr<TQueryFnData> Function() fetchFn;
|
||||
FetchOptions? fetchOptions;
|
||||
QueryOptions<TQueryFnData, TError, TData> options;
|
||||
QueryKey queryKey;
|
||||
QueryState<TData, TError> state;
|
||||
QueryMeta? meta;
|
||||
|
||||
FetchContext({
|
||||
required this.fetchFn,
|
||||
required this.options,
|
||||
required this.queryKey,
|
||||
required this.state,
|
||||
this.meta,
|
||||
this.fetchOptions,
|
||||
});
|
||||
}
|
||||
|
||||
class QueryBehavior<TQueryFnData extends Map<String, dynamic>, TError,
|
||||
TData extends Map<String, dynamic>> {
|
||||
void Function(FetchContext<TQueryFnData, TError, TData> context) onFetch;
|
||||
QueryBehavior({required this.onFetch});
|
||||
}
|
||||
|
||||
class QueryState<TData extends Map<String, dynamic>, TError> {
|
||||
TData? data;
|
||||
TError? error;
|
||||
QueryStatus status;
|
||||
DateTime? dataUpdatedAt;
|
||||
int dataUpdateCount;
|
||||
DateTime? errorUpdatedAt;
|
||||
int errorUpdateCount;
|
||||
int fetchFailureCount;
|
||||
dynamic fetchMeta;
|
||||
bool isFetching;
|
||||
bool isInvalidated;
|
||||
bool isPaused;
|
||||
|
||||
QueryState({
|
||||
required this.status,
|
||||
required this.dataUpdatedAt,
|
||||
required this.dataUpdateCount,
|
||||
required this.errorUpdatedAt,
|
||||
required this.errorUpdateCount,
|
||||
required this.fetchFailureCount,
|
||||
required this.fetchMeta,
|
||||
required this.isFetching,
|
||||
required this.isInvalidated,
|
||||
required this.isPaused,
|
||||
this.data,
|
||||
this.error,
|
||||
});
|
||||
|
||||
QueryState.fromJson(Map<String, dynamic> json)
|
||||
: data = json["data"],
|
||||
error = json["error"],
|
||||
status = json["status"],
|
||||
dataUpdatedAt = json["dataUpdatedAt"],
|
||||
dataUpdateCount = json["dataUpdateCount"],
|
||||
errorUpdatedAt = json["errorUpdatedAt"],
|
||||
errorUpdateCount = json["errorUpdateCount"],
|
||||
fetchFailureCount = json["fetchFailureCount"],
|
||||
fetchMeta = json["fetchMeta"],
|
||||
isFetching = json["isFetching"],
|
||||
isInvalidated = json["isInvalidated"],
|
||||
isPaused = json["isPaused"];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"data": data,
|
||||
"error": error,
|
||||
"status": status,
|
||||
"dataUpdatedAt": dataUpdatedAt,
|
||||
"dataUpdateCount": dataUpdateCount,
|
||||
"errorUpdatedAt": errorUpdatedAt,
|
||||
"errorUpdateCount": errorUpdateCount,
|
||||
"fetchFailureCount": fetchFailureCount,
|
||||
"fetchMeta": fetchMeta,
|
||||
"isFetching": isFetching,
|
||||
"isInvalidated": isInvalidated,
|
||||
"isPaused": isPaused,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
failed,
|
||||
fetch,
|
||||
success,
|
||||
error,
|
||||
invalidate,
|
||||
pause,
|
||||
resume,
|
||||
setState,
|
||||
}
|
||||
|
||||
class SetStateOptions {
|
||||
Object? meta;
|
||||
SetStateOptions({this.meta});
|
||||
Map<String, dynamic> toJson() {
|
||||
return {"meta": meta};
|
||||
}
|
||||
}
|
||||
|
||||
class Action<TData extends Map<String, dynamic>, TError> {
|
||||
ActionType type;
|
||||
Object? meta;
|
||||
TData? data;
|
||||
DateTime? dataUpdatedAt;
|
||||
TError? error;
|
||||
QueryState<TData, TError>? state;
|
||||
SetStateOptions? setStateOptions;
|
||||
|
||||
Action(
|
||||
this.type, {
|
||||
this.meta,
|
||||
this.data,
|
||||
this.dataUpdatedAt,
|
||||
this.error,
|
||||
this.state,
|
||||
this.setStateOptions,
|
||||
}) {
|
||||
if (type == ActionType.error && error == null)
|
||||
throw Exception(
|
||||
"[Action.Action] property `error` can't be null when `type` = `$type`");
|
||||
|
||||
if (type == ActionType.setState && state == null)
|
||||
throw Exception(
|
||||
"[Action.Action] property `state` can't be null when `type` = `$type`");
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"type": type,
|
||||
"meta": meta,
|
||||
"data": data,
|
||||
"dataUpdatedAt": dataUpdatedAt,
|
||||
"error": error,
|
||||
"state": state,
|
||||
"setStateOptions": setStateOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Query<TQueryFnData extends Map<String, dynamic>, TError,
|
||||
TData extends Map<String, dynamic>> {
|
||||
QueryKey queryKey;
|
||||
String queryHash;
|
||||
late QueryOptions<TQueryFnData, TError, TData> options;
|
||||
late QueryState<TData, TError> initialState;
|
||||
QueryState<TData, TError>? revertState;
|
||||
late QueryState<TData, TError> state;
|
||||
Duration? cacheTime;
|
||||
QueryMeta? meta;
|
||||
|
||||
QueryCache _cache;
|
||||
// Future<TData>? _future;
|
||||
Completer<TData>? _completer;
|
||||
Timer? _gcTimeout;
|
||||
Retryer<TData, TError>? _retryer;
|
||||
List<QueryObserver> _observers;
|
||||
QueryOptions<TQueryFnData, TError, TData>? _defaultOptions;
|
||||
bool _abortSignalConsumed;
|
||||
bool _hadObservers;
|
||||
|
||||
Query({
|
||||
required this.queryKey,
|
||||
required this.queryHash,
|
||||
required QueryCache cache,
|
||||
QueryOptions<TQueryFnData, TError, TData>? options,
|
||||
QueryOptions<TQueryFnData, TError, TData>? defaultOptions,
|
||||
QueryState<TData, TError>? state,
|
||||
QueryMeta? meta,
|
||||
}) : _abortSignalConsumed = false,
|
||||
_hadObservers = false,
|
||||
_defaultOptions = defaultOptions,
|
||||
_observers = [],
|
||||
_cache = cache {
|
||||
_setOptions(options);
|
||||
initialState = state ?? _getDefaultState(this.options);
|
||||
this.state = initialState;
|
||||
this.meta = meta;
|
||||
_scheduleGc();
|
||||
}
|
||||
|
||||
void _scheduleGc() {
|
||||
this._clearGcTimeout();
|
||||
if (this.cacheTime != null) {
|
||||
_gcTimeout = Timer(cacheTime!, () {
|
||||
this._optionalRemove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _clearGcTimeout() {
|
||||
_gcTimeout?.cancel();
|
||||
_gcTimeout = null;
|
||||
}
|
||||
|
||||
void _optionalRemove() {
|
||||
if (_observers.isEmpty) {
|
||||
if (state.isFetching) {
|
||||
if (_hadObservers) {
|
||||
_scheduleGc();
|
||||
}
|
||||
} else {
|
||||
_cache.remove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _setOptions(QueryOptions<TQueryFnData, TError, TData>? options) {
|
||||
this.options = QueryOptions.fromJson({
|
||||
...(_defaultOptions?.toJson() ?? {}),
|
||||
...(options?.toJson() ?? {}),
|
||||
});
|
||||
meta = options?.meta;
|
||||
|
||||
/// Default to [5 minutes] if cache time isn't set
|
||||
cacheTime = Duration(
|
||||
milliseconds: max(
|
||||
cacheTime?.inMilliseconds ?? 0,
|
||||
this.options.cacheTime?.inMilliseconds ?? 5 * 60 * 1000,
|
||||
));
|
||||
}
|
||||
|
||||
QueryState<TData, TError> _getDefaultState(
|
||||
QueryOptions<TQueryFnData, TError, TData> options) {
|
||||
var data = options.initialData;
|
||||
bool hasData = data != null;
|
||||
|
||||
DateTime? initialDataUpdatedAt =
|
||||
hasData ? options.initialDataUpdatedAt : null;
|
||||
|
||||
return QueryState(
|
||||
data: data,
|
||||
dataUpdateCount: 0,
|
||||
dataUpdatedAt: hasData ? initialDataUpdatedAt ?? DateTime.now() : null,
|
||||
error: null,
|
||||
errorUpdateCount: 0,
|
||||
errorUpdatedAt: null,
|
||||
fetchFailureCount: 0,
|
||||
fetchMeta: null,
|
||||
isFetching: false,
|
||||
isInvalidated: false,
|
||||
isPaused: false,
|
||||
status: hasData ? QueryStatus.success : QueryStatus.idle,
|
||||
);
|
||||
}
|
||||
|
||||
TData setData(
|
||||
DataUpdateFunction<TData?, TData> updater, {
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
try {
|
||||
var prevData = this.state.data;
|
||||
var data = updater(prevData);
|
||||
// Use prev data if an isDataEqual function is defined and returns `true`
|
||||
if (this.options.isDataEqual?.call(prevData, data) == true) {
|
||||
data = prevData as TData;
|
||||
} else if (this.options.structuralSharing != false) {
|
||||
// Structurally share data between prev and new data if needed
|
||||
final merged =
|
||||
Map<String, dynamic>.from(replaceEqualDeep(prevData, data));
|
||||
data = merged as TData;
|
||||
}
|
||||
// Set data and mark it as cached
|
||||
_dispatch(Action(
|
||||
ActionType.success,
|
||||
data: data,
|
||||
dataUpdatedAt: updatedAt,
|
||||
));
|
||||
return data;
|
||||
} catch (e, stack) {
|
||||
print("[Query.setData] $e");
|
||||
print(stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void setState(
|
||||
QueryState<TData, TError> state, [
|
||||
SetStateOptions? setStateOptions,
|
||||
]) {
|
||||
_dispatch(Action(
|
||||
ActionType.setState,
|
||||
state: state,
|
||||
setStateOptions: setStateOptions,
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> cancel({bool? revert, bool? silent}) {
|
||||
// var future = _future;
|
||||
_retryer?.cancel(revert: revert, silent: silent);
|
||||
if (_completer != null && !_completer!.isCompleted) {
|
||||
_completer!.completeError("Cancelled Job", StackTrace.current);
|
||||
return _completer!.future.then(noop).catchError(noop);
|
||||
}
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
destroy();
|
||||
setState(initialState);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
_clearGcTimeout();
|
||||
cancel(silent: true);
|
||||
}
|
||||
|
||||
bool isActive() {
|
||||
return _observers.any((observer) => observer.options.enabled != false);
|
||||
}
|
||||
|
||||
bool isFetching() {
|
||||
return this.state.isFetching;
|
||||
}
|
||||
|
||||
Future<TData> fetch([
|
||||
QueryOptions<TQueryFnData, TError, TData>? options,
|
||||
ObserverFetchOptions? fetchOptions,
|
||||
]) {
|
||||
if (this.state.isFetching) {
|
||||
if (this.state.dataUpdatedAt != null &&
|
||||
fetchOptions?.cancelRefetch == true) {
|
||||
// Silently cancel current fetch if the user wants to cancel re-fetches
|
||||
this.cancel(silent: true);
|
||||
} else if (_completer != null) {
|
||||
// make sure that retries that were potentially cancelled due to unmounts can continue
|
||||
_retryer?.continueRetry();
|
||||
// Return current promise if we are already fetching
|
||||
return _completer!.future;
|
||||
}
|
||||
}
|
||||
|
||||
// Update config if passed, otherwise the config from the last execution is used
|
||||
if (options != null) {
|
||||
_setOptions(options);
|
||||
}
|
||||
|
||||
// Use the options from the first observer with a query function if no function is found.
|
||||
// This can happen when the query is hydrated or created with setQueryData.
|
||||
if (this.options.queryFn == null) {
|
||||
final observer =
|
||||
_observers.firstWhereOrNull((x) => x.options.queryFn != null);
|
||||
if (observer != null) {
|
||||
_setOptions(QueryOptions<TQueryFnData, TError, TData>(
|
||||
queryKey: observer.options.queryKey,
|
||||
queryKeyHashFn: observer.options.queryKeyHashFn,
|
||||
cacheTime: observer.options.cacheTime,
|
||||
isDataEqual: observer.options.isDataEqual,
|
||||
queryFn:
|
||||
observer.options.queryFn as QueryFunction<TQueryFnData, dynamic>,
|
||||
queryHash: observer.options.queryHash,
|
||||
initialData: observer.options.initialData as TData?,
|
||||
initialDataUpdatedAt: observer.options.initialDataUpdatedAt,
|
||||
meta: observer.options.meta,
|
||||
structuralSharing: observer.options.structuralSharing,
|
||||
defaulted: observer.options.defaulted,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
QueryFunctionContext queryFnContext = QueryFunctionContext(
|
||||
queryKey: queryKey,
|
||||
meta: meta,
|
||||
);
|
||||
|
||||
/// !!LANGUAGE LIMITATION!! There's no equivalent of [AbortController]
|
||||
/// the [get] can be implemented using Dart's getter but it'd be
|
||||
/// useless since there's no equivalent of AbortController.
|
||||
/// Have to find a better way to control ABORTION
|
||||
|
||||
// Object.defineProperty(queryFnContext, 'signal', {
|
||||
// enumerable: true,
|
||||
// get: () {
|
||||
// if (abortController) {
|
||||
// this.abortSignalConsumed = true
|
||||
// return abortController.signal
|
||||
// }
|
||||
// return undefined
|
||||
// },
|
||||
// })
|
||||
|
||||
// Create fetch function
|
||||
FutureOr<TQueryFnData> fetchFn() {
|
||||
if (this.options.queryFn == null) {
|
||||
return Future.error('Missing queryFn');
|
||||
}
|
||||
_abortSignalConsumed = false;
|
||||
return options!.queryFn!.call(queryFnContext);
|
||||
}
|
||||
|
||||
// Trigger behavior hook
|
||||
FetchContext<TQueryFnData, TError, TData> context =
|
||||
FetchContext<TQueryFnData, TError, TData>(
|
||||
fetchOptions: fetchOptions,
|
||||
options: this.options,
|
||||
queryKey: queryKey,
|
||||
state: this.state,
|
||||
fetchFn: fetchFn,
|
||||
meta: this.meta,
|
||||
);
|
||||
|
||||
this.options.behavior?.onFetch(context);
|
||||
// Store state in case the current fetch needs to be reverted
|
||||
this.revertState = this.state;
|
||||
|
||||
// Set to fetching state if not already in it
|
||||
if (!this.state.isFetching ||
|
||||
this.state.fetchMeta != context.fetchOptions?.meta) {
|
||||
_dispatch(Action(ActionType.fetch, meta: context.fetchOptions?.meta));
|
||||
}
|
||||
|
||||
_retryer = Retryer(
|
||||
fn: context.fetchFn as FutureOr<TData> Function(),
|
||||
// abort: abortController?.abort?.bind(abortController),
|
||||
onSuccess: (data) {
|
||||
this.setData((_) => data);
|
||||
|
||||
// Notify cache callback
|
||||
_cache.onData?.call(data, this);
|
||||
if (_completer?.isCompleted == false) _completer?.complete(data);
|
||||
// Remove query after fetching if cache time is 0
|
||||
if (this.cacheTime == null || this.cacheTime == Duration.zero) {
|
||||
_optionalRemove();
|
||||
}
|
||||
},
|
||||
onError: (TError error) {
|
||||
// Optimistically update state if needed
|
||||
if (!(isCancelledError(error) && (error as dynamic)?.silent == true)) {
|
||||
_dispatch(Action(ActionType.error, error: error));
|
||||
}
|
||||
|
||||
if (!isCancelledError(error)) {
|
||||
// Notify cache callback
|
||||
_cache.onError?.call(error, this);
|
||||
|
||||
// Log error
|
||||
// getLogger().error(error);
|
||||
}
|
||||
|
||||
// Remove query after fetching if cache time is 0
|
||||
if (this.cacheTime == null || this.cacheTime == Duration.zero) {
|
||||
_optionalRemove();
|
||||
}
|
||||
if (_completer?.isCompleted == false)
|
||||
_completer?.completeError(
|
||||
error ?? "Retry Failed", StackTrace.current);
|
||||
},
|
||||
onFail: (failureCount, error) {
|
||||
_dispatch(Action(ActionType.failed));
|
||||
},
|
||||
onPause: () {
|
||||
_dispatch(Action(ActionType.pause));
|
||||
},
|
||||
onContinue: () {
|
||||
_dispatch(Action(ActionType.resume));
|
||||
},
|
||||
retry: context.options.retry,
|
||||
retryDelay: context.options.retryDelay,
|
||||
);
|
||||
|
||||
this._completer = _retryer!.completer;
|
||||
return this._completer!.future;
|
||||
}
|
||||
|
||||
void _dispatch(Action<TData, TError> action) {
|
||||
this.state = this.reducer(this.state, action);
|
||||
|
||||
notifyManager.batch(() {
|
||||
_observers.forEach((observer) {
|
||||
observer.onQueryUpdate(action);
|
||||
});
|
||||
_cache.notify(QueryCacheNotifyEvent(
|
||||
QueryCacheNotifyEventType.queryUpdated,
|
||||
this,
|
||||
action: action,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
void addObserver(QueryObserver observer) {
|
||||
if (_observers.indexOf(observer) == -1) {
|
||||
_observers.add(observer);
|
||||
_hadObservers = true;
|
||||
|
||||
// Stop the query from being garbage collected
|
||||
_clearGcTimeout();
|
||||
|
||||
_cache.notify(QueryCacheNotifyEvent(
|
||||
QueryCacheNotifyEventType.observerAdded,
|
||||
this,
|
||||
observer: observer,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void removeObserver(QueryObserver observer) {
|
||||
if (_observers.indexOf(observer) != -1) {
|
||||
_observers = _observers.where((x) => x != observer).toList();
|
||||
|
||||
if (_observers.isEmpty) {
|
||||
// If the transport layer does not support cancellation
|
||||
// we'll let the query continue so the result can be cached
|
||||
if (_retryer != null) {
|
||||
if (_retryer?.isTransportCancelable == true || _abortSignalConsumed) {
|
||||
_retryer?.cancel(revert: true);
|
||||
} else {
|
||||
_retryer?.cancelRetry();
|
||||
}
|
||||
}
|
||||
|
||||
if (cacheTime != null) {
|
||||
_scheduleGc();
|
||||
} else {
|
||||
_cache.remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
_cache.notify(QueryCacheNotifyEvent(
|
||||
QueryCacheNotifyEventType.observerRemoved,
|
||||
this,
|
||||
observer: observer,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
int getObserversCount() {
|
||||
return _observers.length;
|
||||
}
|
||||
|
||||
void invalidate() {
|
||||
if (!this.state.isInvalidated) {
|
||||
_dispatch(Action(ActionType.invalidate));
|
||||
}
|
||||
}
|
||||
|
||||
bool isStale() {
|
||||
return (this.state.isInvalidated ||
|
||||
this.state.dataUpdatedAt == null ||
|
||||
_observers
|
||||
.any((observer) => observer.getCurrentResult()?.isStale == true));
|
||||
}
|
||||
|
||||
bool isStaleByTime(Duration? staleTime) {
|
||||
return (this.state.isInvalidated ||
|
||||
this.state.dataUpdatedAt == null ||
|
||||
timeUntilStale(this.state.dataUpdatedAt!, staleTime) == Duration.zero);
|
||||
}
|
||||
|
||||
void onOnline() {
|
||||
var observer = _observers
|
||||
.firstWhereOrNull((x) => x.shouldFetchCurrentQueryOnReconnect());
|
||||
|
||||
if (observer != null) {
|
||||
observer.refetch();
|
||||
}
|
||||
|
||||
// Continue fetch if currently paused
|
||||
_retryer?.continueFn();
|
||||
}
|
||||
|
||||
@protected
|
||||
QueryState<TData, TError> reducer(
|
||||
QueryState<TData, TError> state,
|
||||
Action<TData, TError> action,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case ActionType.failed:
|
||||
return QueryState.fromJson({
|
||||
...state.toJson(),
|
||||
"fetchFailureCount": state.fetchFailureCount + 1,
|
||||
});
|
||||
case ActionType.fetch:
|
||||
return QueryState.fromJson({
|
||||
...state.toJson(),
|
||||
"fetchFailureCount": 0,
|
||||
"fetchMeta": action.meta,
|
||||
"isFetching": true,
|
||||
"isPaused": false,
|
||||
if (state.dataUpdatedAt == null)
|
||||
...({
|
||||
"error": null,
|
||||
"status": QueryStatus.loading,
|
||||
})
|
||||
});
|
||||
case ActionType.success:
|
||||
return QueryState.fromJson({
|
||||
...state.toJson(),
|
||||
"data": action.data,
|
||||
"dataUpdateCount": state.dataUpdateCount + 1,
|
||||
"dataUpdatedAt": action.dataUpdatedAt ?? DateTime.now(),
|
||||
"error": null,
|
||||
"fetchFailureCount": 0,
|
||||
"isFetching": false,
|
||||
"isInvalidated": false,
|
||||
"isPaused": false,
|
||||
"status": QueryStatus.success,
|
||||
});
|
||||
case ActionType.error:
|
||||
var error = action.error as dynamic;
|
||||
if (isCancelledError(error) &&
|
||||
error?.revert == true &&
|
||||
revertState != null) {
|
||||
return QueryState.fromJson(revertState!.toJson());
|
||||
}
|
||||
|
||||
return QueryState.fromJson({
|
||||
...state.toJson(),
|
||||
"error": error as TError,
|
||||
"errorUpdateCount": state.errorUpdateCount + 1,
|
||||
"errorUpdatedAt": DateTime.now(),
|
||||
"fetchFailureCount": state.fetchFailureCount + 1,
|
||||
"isFetching": false,
|
||||
"isPaused": false,
|
||||
"status": QueryStatus.error,
|
||||
});
|
||||
case ActionType.invalidate:
|
||||
return QueryState.fromJson({
|
||||
...state.toJson(),
|
||||
"isInvalidated": true,
|
||||
});
|
||||
case ActionType.pause:
|
||||
return QueryState.fromJson({
|
||||
...state.toJson(),
|
||||
"isPaused": true,
|
||||
});
|
||||
case ActionType.resume:
|
||||
return QueryState.fromJson({
|
||||
...state.toJson(),
|
||||
"isPaused": false,
|
||||
});
|
||||
case ActionType.setState:
|
||||
return QueryState.fromJson({
|
||||
...state.toJson(),
|
||||
...(action.state?.toJson() ?? {}),
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import 'package:fl_query/src/core/models.dart';
|
||||
import 'package:fl_query/src/core/notify_manager.dart';
|
||||
import 'package:fl_query/src/core/query.dart';
|
||||
import 'package:fl_query/src/core/query_client.dart';
|
||||
import 'package:fl_query/src/core/query_key.dart';
|
||||
import 'package:fl_query/src/core/subscribable.dart';
|
||||
import 'package:fl_query/src/core/utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
enum QueryCacheNotifyEventType {
|
||||
observerResultsUpdated,
|
||||
observerRemoved,
|
||||
observerAdded,
|
||||
queryUpdated,
|
||||
queryRemoved,
|
||||
queryAdded
|
||||
}
|
||||
|
||||
class QueryCacheNotifyEvent {
|
||||
Query query;
|
||||
Object? observer;
|
||||
Object? action;
|
||||
QueryCacheNotifyEventType type;
|
||||
QueryCacheNotifyEvent(
|
||||
this.type,
|
||||
this.query, {
|
||||
this.observer,
|
||||
this.action,
|
||||
}) {
|
||||
if ([
|
||||
QueryCacheNotifyEventType.observerAdded,
|
||||
QueryCacheNotifyEventType.observerRemoved
|
||||
].contains(type) &&
|
||||
observer == null)
|
||||
throw Exception(
|
||||
"[QueryCacheNotifyEvent.constructor] property `observer` can't be `null` for `QueryCacheNotifyEventType.observerAdded` & `QueryCacheNotifyEventType.observerRemoved`");
|
||||
if (type == QueryCacheNotifyEventType.queryUpdated && action == null)
|
||||
throw Exception(
|
||||
"[QueryCacheNotifyEvent.constructor] property `action` can't be `null` for `QueryCacheNotifyEventType.queryUpdated`");
|
||||
}
|
||||
}
|
||||
|
||||
typedef QueryCacheListener = void Function(QueryCacheNotifyEvent? event);
|
||||
typedef QueryCacheOnError = void Function(dynamic error, Query query);
|
||||
typedef QueryCacheOnData = void Function(dynamic data, Query query);
|
||||
typedef QueryHashMap = Map<String, Query>;
|
||||
|
||||
class QueryCache extends Subscribable<QueryCacheListener> {
|
||||
List<Query> _queries;
|
||||
QueryHashMap _queriesMap;
|
||||
|
||||
QueryCacheOnError? onError;
|
||||
QueryCacheOnData? onData;
|
||||
|
||||
QueryCache({
|
||||
this.onData,
|
||||
this.onError,
|
||||
}) : _queries = [],
|
||||
_queriesMap = {},
|
||||
super();
|
||||
|
||||
Query<TQueryFnData, TError, TData> build<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>>(
|
||||
QueryClient client,
|
||||
QueryOptions<TQueryFnData, TError, TData> options, [
|
||||
QueryState<TData, TError>? state,
|
||||
]) {
|
||||
QueryKey queryKey = options.queryKey!;
|
||||
String queryHash =
|
||||
options.queryHash ?? hashQueryKeyByOptions(queryKey, options);
|
||||
Query<TQueryFnData, TError, TData>? query =
|
||||
get<TQueryFnData, TError, TData>(queryHash);
|
||||
|
||||
if (query == null) {
|
||||
query = Query(
|
||||
cache: this,
|
||||
queryKey: queryKey,
|
||||
queryHash: queryHash,
|
||||
options: client.defaultQueryOptions(
|
||||
QueryObserverOptions.fromJson(options.toJson()),
|
||||
),
|
||||
state: state,
|
||||
defaultOptions: QueryOptions.fromJson(
|
||||
client.getQueryDefaults(queryKey)?.toJson() ?? {},
|
||||
),
|
||||
meta: options.meta,
|
||||
);
|
||||
add(query);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
QueryHashMap get queriesMap => _queriesMap;
|
||||
List<Query> get queries => _queries;
|
||||
|
||||
void add(Query query) {
|
||||
if (!_queriesMap.containsKey(query.queryHash)) {
|
||||
_queriesMap[query.queryHash] = query;
|
||||
_queries.add(query);
|
||||
notify(
|
||||
QueryCacheNotifyEvent(
|
||||
QueryCacheNotifyEventType.queryAdded,
|
||||
query,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void remove(Query query) {
|
||||
Query? queryInMap = _queriesMap[query.queryHash];
|
||||
if (queryInMap == null) return;
|
||||
query.destroy();
|
||||
_queries = _queries.where((x) => x != query).toList();
|
||||
if (queryInMap == query) {
|
||||
_queriesMap.remove(query.queryHash);
|
||||
}
|
||||
notify(QueryCacheNotifyEvent(
|
||||
QueryCacheNotifyEventType.queryRemoved,
|
||||
query,
|
||||
));
|
||||
}
|
||||
|
||||
void clear() {
|
||||
notifyManager.batch(() {
|
||||
for (var query in _queries) {
|
||||
remove(query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Query<TQueryFnData, TError, TData>? get<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>>(String queryHash) {
|
||||
return _queriesMap[queryHash] as Query<TQueryFnData, TError, TData>?;
|
||||
}
|
||||
|
||||
List<Query> getAll() {
|
||||
return _queries;
|
||||
}
|
||||
|
||||
Query<TQueryFnData, TError, TData>? find<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>>(QueryKey queryKey,
|
||||
[QueryFilters? queryFilters]) {
|
||||
queryFilters ??= QueryFilters();
|
||||
queryFilters.exact ??= true;
|
||||
return _queries.firstWhereOrNull((query) => matchQuery(
|
||||
queryFilters!,
|
||||
query,
|
||||
queryKey,
|
||||
)) as Query<TQueryFnData, TError, TData>?;
|
||||
}
|
||||
|
||||
List<Query> findAll([QueryKey? queryKeys, QueryFilters? filters]) {
|
||||
return filters == null && queryKeys == null
|
||||
? _queries
|
||||
: _queries
|
||||
.where(
|
||||
(query) => matchQuery(
|
||||
filters ?? QueryFilters(),
|
||||
query,
|
||||
queryKeys,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
void notify(QueryCacheNotifyEvent event) {
|
||||
notifyManager.batch(() {
|
||||
for (final listener in listeners) {
|
||||
listener(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Dummy function just to keep the API similar to react-query
|
||||
void onFocus() {}
|
||||
|
||||
void onOnline() {
|
||||
notifyManager.batch(() {
|
||||
_queries.forEach((query) {
|
||||
query.onOnline();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
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<TData extends Map<String, dynamic>> {
|
||||
QueryKey queryKey;
|
||||
TData data;
|
||||
QueryData({
|
||||
required this.queryKey,
|
||||
required this.data,
|
||||
});
|
||||
}
|
||||
|
||||
class QueryClient {
|
||||
QueryCache _queryCache;
|
||||
// QueryCache _mutationCache;
|
||||
DefaultOptions _defaultOptions;
|
||||
List<QueryDefaults> _queryDefaults;
|
||||
// List<MutationDefaults> _mutationDefaults;
|
||||
void Function()? _unsubscribeFocus;
|
||||
void Function()? _unsubscribeOnline;
|
||||
// MutationKey _mutationKey;
|
||||
// MutationOptions<any, any, any, any> _mutationDefaultOptions;
|
||||
|
||||
QueryClient({
|
||||
QueryCache? queryCache,
|
||||
QueryCache? mutationCache,
|
||||
DefaultOptions? defaultOptions,
|
||||
}) : _queryCache = queryCache ?? 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<TData extends Map<String, dynamic>>(
|
||||
QueryKey queryKey, [
|
||||
QueryFilters? filters,
|
||||
]) {
|
||||
return _queryCache
|
||||
.find<TData, dynamic, Map<String, dynamic>>(
|
||||
queryKey, filters ?? QueryFilters())
|
||||
?.state
|
||||
.data as TData?;
|
||||
}
|
||||
|
||||
List<QueryData<TData>> getQueriesData<TData extends Map<String, dynamic>>({
|
||||
QueryKey? queryKeys,
|
||||
QueryFilters? filters,
|
||||
}) {
|
||||
return getQueryCache().findAll(queryKeys, filters).map((query) {
|
||||
return QueryData<TData>(
|
||||
data: query.state.data as TData,
|
||||
queryKey: query.queryKey,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
TData setQueryData<TData extends Map<String, dynamic>>(
|
||||
QueryKey queryKey,
|
||||
DataUpdateFunction<TData?, TData> updater, [
|
||||
DateTime? updatedAt,
|
||||
]) {
|
||||
final QueryOptions<Map<String, dynamic>, dynamic, TData> defaultedOptions =
|
||||
QueryOptions<Map<String, dynamic>, dynamic, TData>.fromJson(
|
||||
defaultQueryOptions<Map<String, dynamic>, dynamic, TData,
|
||||
Map<String, dynamic>>(
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic, TData,
|
||||
Map<String, dynamic>>(queryKey: queryKey))
|
||||
.toJson());
|
||||
return _queryCache
|
||||
.build<Map<String, dynamic>, dynamic, TData>(this, defaultedOptions)
|
||||
.setData(
|
||||
updater,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
List<QueryData> setQueriesData<TData extends Map<String, dynamic>>({
|
||||
required DataUpdateFunction<TData?, TData> updater,
|
||||
QueryKey? queryKeys,
|
||||
QueryFilters? filters,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
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(queryKeys, filters).map(
|
||||
(query) => QueryData(
|
||||
queryKey: query.queryKey,
|
||||
data: setQueryData<TData>(
|
||||
query.queryKey,
|
||||
updater,
|
||||
updatedAt,
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
QueryState<TData, TError>?
|
||||
getQueryState<TData extends Map<String, dynamic>, TError>(
|
||||
QueryKey queryKey, [
|
||||
QueryFilters? filters,
|
||||
]) {
|
||||
return _queryCache
|
||||
.find<TData, TError, Map<String, dynamic>>(
|
||||
queryKey,
|
||||
filters ?? QueryFilters(),
|
||||
)
|
||||
?.state as QueryState<TData, TError>?;
|
||||
}
|
||||
|
||||
void removeQueries({QueryKey? queryKeys, QueryFilters? filters}) {
|
||||
notifyManager.batch(
|
||||
() => {
|
||||
_queryCache.findAll(queryKeys, filters).forEach((query) {
|
||||
_queryCache.remove(query);
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> resetQueries<TPageData>({
|
||||
QueryKey? queryKeys,
|
||||
RefetchableQueryFilters<TPageData>? filters,
|
||||
bool? throwOnError,
|
||||
}) {
|
||||
filters?.active = true;
|
||||
var refetchFilters = RefetchableQueryFilters<TPageData>.fromJson({
|
||||
...(filters?.toJson() ?? {}),
|
||||
"active": true,
|
||||
});
|
||||
|
||||
return notifyManager.batch(() {
|
||||
_queryCache.findAll(queryKeys, filters).forEach((query) {
|
||||
query.reset();
|
||||
});
|
||||
return refetchQueries(
|
||||
filters: refetchFilters,
|
||||
options: RefetchOptions(throwOnError: throwOnError),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> cancelQueries({
|
||||
QueryKey? queryKeys,
|
||||
QueryFilters? filters,
|
||||
bool? revert = true,
|
||||
bool? silent,
|
||||
}) {
|
||||
var futures = notifyManager.batch(() =>
|
||||
_queryCache.findAll(queryKeys, filters).map((query) => query.cancel(
|
||||
revert: revert,
|
||||
silent: silent,
|
||||
)));
|
||||
return Future.wait(futures).then(noop).catchError(noop);
|
||||
}
|
||||
|
||||
Future<void> invalidateQueries<TPageData>({
|
||||
QueryKey? queryKeys,
|
||||
InvalidateQueryFilters<TPageData>? filters,
|
||||
RefetchOptions? options,
|
||||
}) {
|
||||
var refetchFilters = RefetchableQueryFilters<TPageData>.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(queryKeys, filters).forEach((query) {
|
||||
query.invalidate();
|
||||
});
|
||||
return this.refetchQueries(
|
||||
filters: refetchFilters,
|
||||
options: options,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> refetchQueries<TPageData>({
|
||||
QueryKey? queryKeys,
|
||||
RefetchableQueryFilters<TPageData>? filters,
|
||||
RefetchOptions? options,
|
||||
}) {
|
||||
var futures = notifyManager.batch(
|
||||
() => _queryCache.findAll(queryKeys, 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<TData> fetchQuery<TQueryFnData extends Map<String, dynamic>, TError,
|
||||
TData extends Map<String, dynamic>>({
|
||||
QueryKey? queryKey,
|
||||
QueryFunction<TQueryFnData, dynamic>? queryFn,
|
||||
FetchQueryOptions<TQueryFnData, TError, TData>? options,
|
||||
}) {
|
||||
final defaultedOptions = this.defaultQueryOptions(
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic, Map<String, dynamic>,
|
||||
TData>(
|
||||
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;
|
||||
final query = _queryCache.build<Map<String, dynamic>, dynamic, TData>(
|
||||
this, defaultedOptions);
|
||||
return query.isStaleByTime(defaultedOptions.staleTime)
|
||||
? query.fetch(defaultedOptions)
|
||||
: Future.value(query.state.data as TData);
|
||||
}
|
||||
|
||||
Future<void> prefetchQuery<TQueryFnData extends Map<String, dynamic>, TError,
|
||||
TData extends Map<String, dynamic>>({
|
||||
QueryKey? queryKey,
|
||||
QueryFunction<TQueryFnData, dynamic>? queryFn,
|
||||
FetchQueryOptions<TQueryFnData, TError, TData>? options,
|
||||
}) {
|
||||
return fetchQuery<TQueryFnData, dynamic, TData>(
|
||||
queryKey: queryKey,
|
||||
queryFn: queryFn,
|
||||
options: options,
|
||||
).then(noop).catchError(noop);
|
||||
}
|
||||
|
||||
QueryObserverOptions<TQueryFnData, TError, TData,
|
||||
TQueryData> defaultQueryOptions<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>,
|
||||
TQueryData extends Map<String, dynamic>>(
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? options) {
|
||||
if (options?.defaulted == true) return options!;
|
||||
final defaultedOptions =
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>.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<TQueryFnData, TError, TData, TQueryData>
|
||||
defaultQueryObserverOptions<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>,
|
||||
TQueryData extends Map<String, dynamic>>([
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? options,
|
||||
]) {
|
||||
return this.defaultQueryOptions(options);
|
||||
}
|
||||
|
||||
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() {}
|
||||
// getMutationCache() {}
|
||||
|
||||
QueryCache getQueryCache() {
|
||||
return _queryCache;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_queryCache.clear();
|
||||
// _mutationCache.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/// Used for defining a unique identifier for a specific query
|
||||
/// that can be used to read/modify/delete the query from the
|
||||
/// store
|
||||
class QueryKey {
|
||||
List<String> _key;
|
||||
QueryKey(String key) : _key = [key];
|
||||
|
||||
QueryKey.fromList(List<String> key) : _key = key;
|
||||
QueryKey.parse(String keyStr) : _key = keyStr.split(".");
|
||||
|
||||
String get key => _key.map((k) => k.replaceAll(".", "")).join(".");
|
||||
List<String> get keyAsList => _key;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'QueryKey("$key")';
|
||||
}
|
||||
}
|
||||
@@ -1,740 +0,0 @@
|
||||
/// `TQueryData`, `TQueryFnData`, `TData` should be [Map]s for shallow/deep equality checks
|
||||
/// Or these can be data classes that have `toJson` method & `fromJson`
|
||||
/// constructor. This also requires the data-class to be passed to the
|
||||
/// [Query] constructor parameters e.g ([dataType])
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/src/core/models.dart';
|
||||
import 'package:fl_query/src/core/notify_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_client.dart';
|
||||
import 'package:fl_query/src/core/retryer.dart';
|
||||
import 'package:fl_query/src/core/subscribable.dart';
|
||||
import 'package:fl_query/src/core/utils.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
typedef QueryObserverListener<TData extends Map<String, dynamic>, TError> = void
|
||||
Function(QueryObserverResult<TData, TError> result);
|
||||
|
||||
class NotifyOptions {
|
||||
bool? cache;
|
||||
bool? listeners;
|
||||
bool? onError;
|
||||
bool? onSuccess;
|
||||
|
||||
NotifyOptions({this.cache, this.listeners, this.onError, this.onSuccess});
|
||||
|
||||
/// [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>();
|
||||
if (safe) {
|
||||
if (this.cache != null) data['cache'] = this.cache;
|
||||
if (this.listeners != null) data['listeners'] = this.listeners;
|
||||
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;
|
||||
}
|
||||
|
||||
NotifyOptions.fromJson(Map<String, dynamic> json) {
|
||||
cache = json['cache'];
|
||||
listeners = json['listeners'];
|
||||
onError = json['onError'];
|
||||
onSuccess = json['onSuccess'];
|
||||
}
|
||||
}
|
||||
|
||||
class ObserverFetchOptions extends FetchOptions {
|
||||
bool? throwOnError;
|
||||
ObserverFetchOptions({
|
||||
this.throwOnError,
|
||||
bool? cancelRefetch,
|
||||
dynamic meta,
|
||||
}) : super(cancelRefetch: cancelRefetch, meta: meta);
|
||||
}
|
||||
|
||||
class SelectQuery<TQueryData extends Map<String, dynamic>,
|
||||
TData extends Map<String, dynamic>> {
|
||||
TData Function(TQueryData data) fn;
|
||||
TData result;
|
||||
SelectQuery(this.fn, this.result);
|
||||
}
|
||||
|
||||
class QueryObserver<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>,
|
||||
TQueryData extends Map<String, dynamic>>
|
||||
extends Subscribable<QueryObserverListener> {
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options;
|
||||
QueryClient _client;
|
||||
Query<TQueryFnData, TError, TQueryData>? _currentQuery;
|
||||
|
||||
late QueryState<TQueryData, TError> _currentQueryInitialState;
|
||||
QueryObserverResult<TData, TError>? _currentResult;
|
||||
|
||||
/// List of tracked keys/properties of [QueryObserverResult]
|
||||
late List<String> _trackedProps;
|
||||
|
||||
QueryState<TQueryData, TError>? _currentResultState;
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>?
|
||||
_currentResultOptions;
|
||||
QueryObserverResult<TData, TError>? _previousQueryResult;
|
||||
Exception? _previousSelectError;
|
||||
SelectQuery<TQueryData, TData>? _previousSelect;
|
||||
Timer? _staleTimeout;
|
||||
Timer? _refetchInterval;
|
||||
Duration? _currentRefetchInterval;
|
||||
|
||||
@protected
|
||||
Timer? get refetchInterval => _refetchInterval;
|
||||
|
||||
QueryObserver(
|
||||
this._client,
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? _options,
|
||||
) : _trackedProps = [],
|
||||
_previousSelectError = null,
|
||||
options = _options ?? QueryObserverOptions(),
|
||||
super() {
|
||||
this.setOptions(options);
|
||||
}
|
||||
|
||||
bool shouldFetchCurrentQueryOnReconnect() {
|
||||
return shouldFetchOnReconnect(_currentQuery!, this.options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onSubscribe() {
|
||||
if (listeners.length == 1) {
|
||||
_currentQuery?.addObserver(this);
|
||||
|
||||
if (_currentQuery != null &&
|
||||
shouldFetchOnMount(_currentQuery!, options)) {
|
||||
_executeFetch();
|
||||
}
|
||||
|
||||
_updateTimers();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onUnsubscribe() {
|
||||
if (listeners.isEmpty) {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
listeners = [];
|
||||
_clearTimers();
|
||||
_currentQuery?.removeObserver(this);
|
||||
}
|
||||
|
||||
void setOptions(
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? options, [
|
||||
NotifyOptions? notifyOptions,
|
||||
]) {
|
||||
final prevOptions = this.options;
|
||||
final prevQuery = _currentQuery;
|
||||
|
||||
this.options = this._client.defaultQueryObserverOptions(options);
|
||||
|
||||
this.options.queryKey ??= prevOptions.queryKey;
|
||||
|
||||
_updateQuery();
|
||||
|
||||
bool mounted = hasListeners();
|
||||
|
||||
if (mounted &&
|
||||
_currentQuery != null &&
|
||||
prevQuery != null &&
|
||||
shouldFetchOptionally(
|
||||
_currentQuery!, prevQuery, this.options, prevOptions)) {
|
||||
_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(
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options,
|
||||
) {
|
||||
final defaultedOptions = _client.defaultQueryObserverOptions(options);
|
||||
|
||||
final query = _client.getQueryCache().build(_client, defaultedOptions);
|
||||
|
||||
return createResult(query, defaultedOptions);
|
||||
}
|
||||
|
||||
QueryObserverResult<TData, TError>? getCurrentResult() {
|
||||
return _currentResult;
|
||||
}
|
||||
|
||||
/// There's nothing similar to JS [defineProperty] in dart native
|
||||
/// objects thus modifying the underlying property `get` method is
|
||||
/// impossible so [trackProp] can't be implemented at the moment
|
||||
/// At least not following this procedure
|
||||
QueryObserverResult<TData, TError> trackResult(
|
||||
QueryObserverResult<TData, TError> result,
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>
|
||||
defaultedOptions,
|
||||
) {
|
||||
// final Map<String, dynamic> trackedResult = <String, dynamic>{};
|
||||
// const trackProp = (key: keyof QueryObserverResult) => {
|
||||
// if (!this.trackedProps.includes(key)) {
|
||||
// this.trackedProps.push(key)
|
||||
// }
|
||||
// }
|
||||
// Object.keys(result).forEach(key => {
|
||||
// Object.defineProperty(trackedResult, key, {
|
||||
// configurable: false,
|
||||
// enumerable: true,
|
||||
// get: () => {
|
||||
// trackProp(key as keyof QueryObserverResult)
|
||||
// return result[key as keyof QueryObserverResult]
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
// if (defaultedOptions.useErrorBoundary || defaultedOptions.suspense) {
|
||||
// trackProp('error')
|
||||
// }
|
||||
// return trackedResult
|
||||
|
||||
throw UnimplementedError("COULD NOT IMPLEMENT DUE TO LANGUAGE LIMITATIONS");
|
||||
}
|
||||
|
||||
Future<QueryObserverResult<TData, TError>> getNextResult([
|
||||
bool? throwOnError,
|
||||
]) {
|
||||
final completer = Completer<QueryObserverResult<TData, TError>>();
|
||||
var unsubscribe;
|
||||
unsubscribe = subscribe((result) {
|
||||
if (!result.isFetching) {
|
||||
unsubscribe?.call();
|
||||
if (result.isError && throwOnError == true) {
|
||||
if (!completer.isCompleted) completer.completeError(result.error);
|
||||
} else {
|
||||
if (!completer.isCompleted)
|
||||
completer.complete(
|
||||
result as QueryObserverResult<TData, TError>,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Query<TQueryFnData, TError, TQueryData> getCurrentQuery() {
|
||||
return _currentQuery!;
|
||||
}
|
||||
|
||||
Future<QueryObserverResult<TData, TError>> fetchOptimistic(
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options) {
|
||||
final defaultedOptions = _client.defaultQueryObserverOptions(options);
|
||||
final query = _client.getQueryCache().build(_client, defaultedOptions);
|
||||
|
||||
return query.fetch().then((val) {
|
||||
return createResult(query, defaultedOptions);
|
||||
});
|
||||
}
|
||||
|
||||
@protected
|
||||
Future<QueryObserverResult<TData, TError>?> fetch(
|
||||
ObserverFetchOptions fetchOptions,
|
||||
) {
|
||||
return _executeFetch(fetchOptions).then((val) {
|
||||
updateResult();
|
||||
return _currentResult;
|
||||
});
|
||||
}
|
||||
|
||||
Future<TQueryData?> _executeFetch([ObserverFetchOptions? fetchOptions]) {
|
||||
// Make sure we reference the latest query as the current one might have been removed
|
||||
_updateQuery();
|
||||
// Fetch
|
||||
Future<TQueryData?> future = _currentQuery!.fetch(
|
||||
this.options,
|
||||
fetchOptions,
|
||||
);
|
||||
|
||||
if (fetchOptions?.throwOnError != null) {
|
||||
future = future.catchError((e) => e);
|
||||
}
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
bool _shouldNotifyListeners(QueryObserverResult<TData, TError> result,
|
||||
[QueryObserverResult<TData, TError>? prevResult]) {
|
||||
if (prevResult == null) return true;
|
||||
if (options.notifyOnChangeProps == false &&
|
||||
options.notifyOnChangePropsExclusions == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.notifyOnChangeProps == 'tracked' && _trackedProps.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String>? includedProps = options.notifyOnChangeProps == 'tracked'
|
||||
? _trackedProps
|
||||
: options.notifyOnChangeProps;
|
||||
|
||||
Map<String, dynamic> resultMap = result.toJson();
|
||||
Map<String, dynamic> prevResultMap = prevResult.toJson();
|
||||
|
||||
return resultMap.keys.any((key) {
|
||||
final changed = resultMap[key] != prevResultMap[key];
|
||||
bool? isIncluded = includedProps?.any((x) => x == key);
|
||||
bool isExcluded =
|
||||
options.notifyOnChangePropsExclusions?.any((x) => x == key) ?? false;
|
||||
return changed &&
|
||||
!isExcluded &&
|
||||
(includedProps == null || isIncluded == true);
|
||||
});
|
||||
}
|
||||
|
||||
void updateResult([NotifyOptions? notifyOptions]) {
|
||||
final QueryObserverResult<TData, TError>? prevResult = _currentResult;
|
||||
|
||||
if (_currentQuery != null)
|
||||
_currentResult = this.createResult(_currentQuery!, this.options);
|
||||
_currentResultState = _currentQuery?.state;
|
||||
_currentResultOptions = this.options;
|
||||
|
||||
final isSameMap =
|
||||
shallowEqualMap(_currentResult?.toJson(), prevResult?.toJson());
|
||||
// Only notify if something has changed
|
||||
if (isSameMap) {
|
||||
return;
|
||||
}
|
||||
NotifyOptions defaultNotifyOptions = NotifyOptions(cache: true);
|
||||
if (notifyOptions?.listeners != false &&
|
||||
_currentResult != null &&
|
||||
_shouldNotifyListeners(_currentResult!, prevResult)) {
|
||||
defaultNotifyOptions.listeners = true;
|
||||
}
|
||||
|
||||
final mergedNotifyOptions = {
|
||||
...defaultNotifyOptions.toJson(),
|
||||
...(notifyOptions?.toJson() ?? {}),
|
||||
};
|
||||
|
||||
_notify(NotifyOptions.fromJson(mergedNotifyOptions));
|
||||
}
|
||||
|
||||
void _updateQuery() {
|
||||
final query =
|
||||
this._client.getQueryCache().build(this._client, this.options);
|
||||
|
||||
if (query == _currentQuery) return;
|
||||
|
||||
final prevQuery = _currentQuery;
|
||||
_currentQuery = query;
|
||||
_currentQueryInitialState = query.state;
|
||||
_previousQueryResult = _currentResult;
|
||||
|
||||
if (hasListeners()) {
|
||||
prevQuery?.removeObserver(this);
|
||||
query.addObserver(this);
|
||||
}
|
||||
}
|
||||
|
||||
void onQueryUpdate(Action<TData, TError> action) {
|
||||
final NotifyOptions notifyOptions = NotifyOptions();
|
||||
|
||||
if (action.type == 'success') {
|
||||
notifyOptions.onSuccess = true;
|
||||
} else if (action.type == 'error' && !isCancelledError(action.error)) {
|
||||
notifyOptions.onError = true;
|
||||
}
|
||||
|
||||
updateResult(notifyOptions);
|
||||
|
||||
if (this.hasListeners()) {
|
||||
_updateTimers();
|
||||
}
|
||||
}
|
||||
|
||||
QueryObserverResult<TData, TError> createResult(
|
||||
Query<TQueryFnData, TError, TQueryData> query,
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options,
|
||||
) {
|
||||
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;
|
||||
final prevQueryResult = queryChange ? _currentResult : _previousQueryResult;
|
||||
|
||||
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;
|
||||
TData? data;
|
||||
|
||||
// Optimistically set result in fetching state if needed
|
||||
if (options.optimisticResults == true) {
|
||||
final bool mounted = hasListeners();
|
||||
|
||||
final bool fetchOnMount = !mounted && shouldFetchOnMount(query, options);
|
||||
|
||||
bool fetchOptionally = mounted &&
|
||||
prevQuery != null &&
|
||||
shouldFetchOptionally(query, prevQuery, options, prevOptions);
|
||||
|
||||
if (fetchOnMount || fetchOptionally) {
|
||||
isFetching = true;
|
||||
if (dataUpdatedAt == null) {
|
||||
status = QueryStatus.loading;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep previous data if needed
|
||||
if (prevQueryResult != null &&
|
||||
options.keepPreviousData == true &&
|
||||
state.dataUpdateCount == 0 &&
|
||||
prevQueryResult.isSuccess == true &&
|
||||
status != QueryStatus.error) {
|
||||
data = prevQueryResult.data;
|
||||
dataUpdatedAt = prevQueryResult.dataUpdatedAt;
|
||||
status = prevQueryResult.status;
|
||||
isPreviousData = true;
|
||||
}
|
||||
|
||||
// Select data if needed
|
||||
else if (options.select != null && state.data != null) {
|
||||
print("prevResult != null ${prevResult != null}");
|
||||
print(
|
||||
"state.data == prevResultState?.data | ${state.data} == ${prevResultState?.data} | ${shallowEqualMap(state.data, prevResultState?.data)}");
|
||||
print(
|
||||
"options.select == _previousSelect?.fn ${options.select == _previousSelect?.fn}");
|
||||
print("_previousSelectError == null ${_previousSelectError == null}");
|
||||
if (prevResult != null &&
|
||||
shallowEqualMap(state.data, prevResultState?.data) &&
|
||||
options.select == _previousSelect?.fn &&
|
||||
_previousSelectError == null) {
|
||||
data = _previousSelect?.result;
|
||||
} else {
|
||||
try {
|
||||
data = options.select?.call(state.data);
|
||||
if (options.structuralSharing != false) {
|
||||
data = Map<String, dynamic>.from(
|
||||
replaceEqualDeep(prevResult?.data, data)) as TData;
|
||||
}
|
||||
if (options.select != null && data != null) {
|
||||
_previousSelect = SelectQuery<TQueryData, TData>(
|
||||
options.select!,
|
||||
data,
|
||||
);
|
||||
}
|
||||
_previousSelectError = null;
|
||||
} catch (selectError) {
|
||||
// getLogger().error(selectError);
|
||||
error = selectError as TError;
|
||||
_previousSelectError = selectError as Exception;
|
||||
errorUpdatedAt = DateTime.now();
|
||||
status = QueryStatus.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use query data
|
||||
else {
|
||||
data = state.data as TData?;
|
||||
}
|
||||
|
||||
if (options.placeholderData != null &&
|
||||
data == null &&
|
||||
(status == QueryStatus.loading || status == QueryStatus.idle)) {
|
||||
var placeholderData;
|
||||
|
||||
if (prevResult?.isPlaceholderData == true &&
|
||||
options.placeholderData == prevResultOptions?.placeholderData) {
|
||||
placeholderData = prevResult?.data;
|
||||
} else {
|
||||
placeholderData = options.placeholderData;
|
||||
if (options.select != null && placeholderData != null) {
|
||||
try {
|
||||
placeholderData = options.select?.call(placeholderData);
|
||||
if (options.structuralSharing != false) {
|
||||
placeholderData =
|
||||
replaceEqualDeep(prevResult?.data, placeholderData);
|
||||
}
|
||||
_previousSelectError = null;
|
||||
} catch (selectError) {
|
||||
// getLogger().error(selectError);
|
||||
error = selectError as TError;
|
||||
_previousSelectError = selectError as Exception;
|
||||
errorUpdatedAt = DateTime.now();
|
||||
status = QueryStatus.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (placeholderData != null) {
|
||||
status = QueryStatus.success;
|
||||
data = placeholderData as TData;
|
||||
isPlaceholderData = true;
|
||||
}
|
||||
}
|
||||
|
||||
final QueryObserverResult<TData, TError> result =
|
||||
QueryObserverResult<TData, TError>(
|
||||
status: status,
|
||||
dataUpdatedAt: dataUpdatedAt,
|
||||
isLoading: status == QueryStatus.loading,
|
||||
isSuccess: status == QueryStatus.success,
|
||||
isError: status == QueryStatus.error,
|
||||
isIdle: status == QueryStatus.idle,
|
||||
data: data,
|
||||
error: error,
|
||||
failureCount: state.fetchFailureCount,
|
||||
isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0,
|
||||
isFetchedAfterMount:
|
||||
state.dataUpdateCount > queryInitialState.dataUpdateCount ||
|
||||
state.errorUpdateCount > queryInitialState.errorUpdateCount,
|
||||
isFetching: isFetching,
|
||||
isRefetching: isFetching && status != QueryStatus.loading,
|
||||
isLoadingError:
|
||||
status == QueryStatus.error && state.dataUpdatedAt == null,
|
||||
isPlaceholderData: isPlaceholderData,
|
||||
isPreviousData: isPreviousData,
|
||||
isRefetchError: status == 'error' && state.dataUpdatedAt != 0,
|
||||
isStale: isStale(query, options),
|
||||
refetch: this.refetch,
|
||||
remove: this.remove,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
void _notify(NotifyOptions notifyOptions) {
|
||||
notifyManager.batch(() {
|
||||
// First trigger the configuration callbacks
|
||||
if (notifyOptions.onSuccess == true && _currentResult != null) {
|
||||
this.options.onSuccess?.call(_currentResult!.data!);
|
||||
this.options.onSettled?.call(_currentResult!.data!);
|
||||
} else if (notifyOptions.onError == true && _currentResult != null) {
|
||||
this.options.onError?.call(_currentResult!.error!);
|
||||
this.options.onSettled?.call(null, _currentResult!.error!);
|
||||
}
|
||||
|
||||
// Then trigger the listeners
|
||||
if (notifyOptions.listeners == true && _currentResult != null) {
|
||||
this.listeners.forEach((listener) {
|
||||
listener(_currentResult!);
|
||||
});
|
||||
}
|
||||
|
||||
// Then the cache listeners
|
||||
if (notifyOptions.cache == true && _currentQuery != null) {
|
||||
_client.getQueryCache().notify(
|
||||
QueryCacheNotifyEvent(
|
||||
QueryCacheNotifyEventType.observerResultsUpdated,
|
||||
_currentQuery as Query,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Duration? _computeRefetchInterval() {
|
||||
return this.options.refetchInterval != null && _currentQuery != null
|
||||
? this.options.refetchInterval!(_currentResult?.data, _currentQuery!)
|
||||
: null;
|
||||
}
|
||||
|
||||
void _updateTimers() {
|
||||
_updateStaleTimeout();
|
||||
_updateRefetchInterval(_computeRefetchInterval());
|
||||
}
|
||||
|
||||
void _updateStaleTimeout() {
|
||||
_clearStaleTimeout();
|
||||
if (_currentResult?.isStale == true ||
|
||||
options.staleTime == null ||
|
||||
_currentResult?.dataUpdatedAt == null) return;
|
||||
|
||||
// The timeout is sometimes triggered 1 ms before the stale time
|
||||
// expiration. To mitigate this issue we always add 1 ms to the
|
||||
// timeout.
|
||||
Duration time = Duration(
|
||||
milliseconds:
|
||||
timeUntilStale(_currentResult!.dataUpdatedAt!, this.options.staleTime)
|
||||
.inMilliseconds +
|
||||
1,
|
||||
);
|
||||
|
||||
_staleTimeout = Timer(time, () {
|
||||
if (!_currentResult!.isStale) {
|
||||
this.updateResult();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updateRefetchInterval(Duration? nextInterval) {
|
||||
_clearRefetchInterval();
|
||||
|
||||
_currentRefetchInterval = nextInterval;
|
||||
|
||||
if (this.options.enabled == false ||
|
||||
_currentRefetchInterval == null ||
|
||||
_currentRefetchInterval == Duration.zero) return;
|
||||
|
||||
_refetchInterval = Timer.periodic(_currentRefetchInterval!, (t) {
|
||||
if (this.options.refetchIntervalInBackground == true) {
|
||||
_executeFetch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _clearTimers() {
|
||||
_clearStaleTimeout();
|
||||
_clearRefetchInterval();
|
||||
}
|
||||
|
||||
void _clearStaleTimeout() {
|
||||
_staleTimeout?.cancel();
|
||||
_staleTimeout = null;
|
||||
}
|
||||
|
||||
void _clearRefetchInterval() {
|
||||
_refetchInterval?.cancel();
|
||||
_refetchInterval = null;
|
||||
}
|
||||
|
||||
void remove() {
|
||||
_client.getQueryCache().remove(_currentQuery as Query);
|
||||
_clearTimers();
|
||||
_currentQuery?.removeObserver(this);
|
||||
}
|
||||
|
||||
Future<QueryObserverResult<TData, TError>?> refetch<TPageData>({
|
||||
RefetchableQueryFilters<TPageData>? filters,
|
||||
RefetchOptions? options,
|
||||
}) {
|
||||
return fetch(
|
||||
ObserverFetchOptions(
|
||||
cancelRefetch: options?.cancelRefetch,
|
||||
meta: filters?.toJson(),
|
||||
throwOnError: options?.throwOnError,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool shouldLoadOnMount<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>,
|
||||
TQueryData extends Map<String, dynamic>>(
|
||||
Query<TQueryFnData, TError, TQueryData> query,
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options,
|
||||
) {
|
||||
return (options.enabled != false &&
|
||||
query.state.dataUpdatedAt == null &&
|
||||
!(query.state.status == QueryStatus.error &&
|
||||
options.retryOnMount == false));
|
||||
}
|
||||
|
||||
bool shouldRefetchOnMount<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>,
|
||||
TQueryData extends Map<String, dynamic>>(
|
||||
Query<TQueryFnData, TError, TQueryData> query,
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options,
|
||||
) {
|
||||
return (options.enabled != false &&
|
||||
query.state.dataUpdatedAt != null &&
|
||||
(options.refetchOnMount == RefetchOnMount.always ||
|
||||
(options.refetchOnMount != RefetchOnMount.off &&
|
||||
isStale(query, options))));
|
||||
}
|
||||
|
||||
bool shouldFetchOnMount<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>,
|
||||
TQueryData extends Map<String, dynamic>>(
|
||||
Query<TQueryFnData, TError, TQueryData> query,
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options,
|
||||
) {
|
||||
return (shouldLoadOnMount(query, options) ||
|
||||
shouldRefetchOnMount(query, options));
|
||||
}
|
||||
|
||||
bool shouldFetchOnReconnect<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>,
|
||||
TQueryData extends Map<String, dynamic>>(
|
||||
Query<TQueryFnData, TError, TQueryData> query,
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options,
|
||||
) {
|
||||
return (options.enabled != false &&
|
||||
(options.refetchOnReconnect == RefetchOnReconnect.always ||
|
||||
(options.refetchOnReconnect != RefetchOnReconnect.off &&
|
||||
isStale<TQueryFnData, TError, TData, TQueryData>(
|
||||
query, options))));
|
||||
}
|
||||
|
||||
bool shouldFetchOptionally<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>,
|
||||
TQueryData extends Map<String, dynamic>>(
|
||||
Query<TQueryFnData, TError, TQueryData> query,
|
||||
Query<TQueryFnData, TError, TQueryData> prevQuery,
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options,
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> prevOptions,
|
||||
) {
|
||||
return (options.enabled != false &&
|
||||
(query != prevQuery || prevOptions.enabled == false) &&
|
||||
(options.suspense != true || query.state.status != QueryStatus.error) &&
|
||||
isStale(query, options));
|
||||
}
|
||||
|
||||
bool isStale<
|
||||
TQueryFnData extends Map<String, dynamic>,
|
||||
TError,
|
||||
TData extends Map<String, dynamic>,
|
||||
TQueryData extends Map<String, dynamic>>(
|
||||
Query<TQueryFnData, TError, TQueryData> query,
|
||||
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData> options,
|
||||
) {
|
||||
return query.isStaleByTime(options.staleTime);
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'dart:math' show pow, min;
|
||||
|
||||
import 'package:fl_query/src/core/online_manager.dart';
|
||||
|
||||
typedef ShouldRetryFunction<TError> = int Function(
|
||||
int failureCount,
|
||||
TError error,
|
||||
);
|
||||
typedef RetryDelayFunction<TError> = double Function(
|
||||
int failureCount,
|
||||
TError error,
|
||||
);
|
||||
|
||||
double defaultRetryDelay(int failureCount) {
|
||||
return min(pow(1000 * 2, failureCount), 30000).toDouble();
|
||||
}
|
||||
|
||||
abstract class Cancelable {
|
||||
void cancel();
|
||||
}
|
||||
|
||||
bool isCancelable(value) {
|
||||
return value is Cancelable;
|
||||
}
|
||||
|
||||
class CancelledError {
|
||||
bool? revert;
|
||||
bool? silent;
|
||||
CancelledError({this.revert, this.silent});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "CancelledError(revert: $revert, silent: $silent)";
|
||||
}
|
||||
}
|
||||
|
||||
bool isCancelledError(value) {
|
||||
return value is CancelledError;
|
||||
}
|
||||
|
||||
typedef OnError<TError> = void Function(TError error);
|
||||
typedef OnData<TData extends Map<String, dynamic>> = void Function(TData data);
|
||||
|
||||
class Retryer<TData extends Map<String, dynamic>, TError> {
|
||||
late void Function({bool? revert, bool? silent}) cancel;
|
||||
late void Function() cancelRetry;
|
||||
late void Function() continueRetry;
|
||||
late void Function() continueFn;
|
||||
// late Future<TData> future;
|
||||
late Completer<TData> completer;
|
||||
int failureCount;
|
||||
bool isPaused;
|
||||
bool isResolved;
|
||||
bool isTransportCancelable;
|
||||
|
||||
// config options for the retryer
|
||||
FutureOr<TData> Function() fn;
|
||||
void Function()? _abort;
|
||||
OnError<TError>? onError;
|
||||
OnData<TData>? onSuccess;
|
||||
void Function(int failureCount, TError error)? onFail;
|
||||
void Function()? onPause;
|
||||
void Function()? onContinue;
|
||||
ShouldRetryFunction<TError>? retry;
|
||||
RetryDelayFunction<TError>? retryDelay;
|
||||
|
||||
Retryer({
|
||||
required this.fn,
|
||||
void Function()? abort,
|
||||
this.onError,
|
||||
this.onSuccess,
|
||||
this.onFail,
|
||||
this.onPause,
|
||||
this.onContinue,
|
||||
this.retry,
|
||||
this.retryDelay,
|
||||
}) : _abort = abort,
|
||||
failureCount = 0,
|
||||
isPaused = false,
|
||||
isResolved = false,
|
||||
isTransportCancelable = false {
|
||||
bool cancelRetry = false;
|
||||
void Function({bool? revert, bool? silent})? cancelFn;
|
||||
void Function([dynamic value])? continueFn;
|
||||
cancel = ({bool? revert, bool? silent}) {
|
||||
cancelFn?.call();
|
||||
};
|
||||
|
||||
this.cancelRetry = () {
|
||||
cancelRetry = true;
|
||||
};
|
||||
|
||||
this.continueRetry = () {
|
||||
cancelRetry = false;
|
||||
};
|
||||
|
||||
this.continueFn = () => continueFn?.call();
|
||||
|
||||
completer = Completer<TData>();
|
||||
|
||||
// this.future = completer.future;
|
||||
|
||||
resolve(value) {
|
||||
if (!this.isResolved) {
|
||||
this.isResolved = true;
|
||||
onSuccess?.call(value);
|
||||
continueFn?.call();
|
||||
if (!completer.isCompleted) completer.complete(value);
|
||||
}
|
||||
}
|
||||
|
||||
reject(value) {
|
||||
if (!this.isResolved) {
|
||||
this.isResolved = true;
|
||||
onError?.call(value);
|
||||
continueFn?.call();
|
||||
if (!completer.isCompleted) completer.completeError(value);
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
Completer pauseCompleter = Completer();
|
||||
if (!pauseCompleter.isCompleted) continueFn = pauseCompleter.complete;
|
||||
this.isPaused = true;
|
||||
onPause?.call();
|
||||
return pauseCompleter.future.then((val) {
|
||||
continueFn = null;
|
||||
this.isPaused = false;
|
||||
onContinue?.call();
|
||||
});
|
||||
}
|
||||
|
||||
run() {
|
||||
// Do nothing if already resolved
|
||||
if (this.isResolved) {
|
||||
return;
|
||||
}
|
||||
var promiseOrValue;
|
||||
|
||||
// Execute query
|
||||
try {
|
||||
promiseOrValue = fn();
|
||||
} catch (error) {
|
||||
promiseOrValue = Future.error(error);
|
||||
}
|
||||
|
||||
// Create callback to cancel this fetch
|
||||
cancelFn = ({bool? revert, bool? silent}) {
|
||||
if (!this.isResolved) {
|
||||
reject(new CancelledError(revert: revert, silent: silent));
|
||||
|
||||
abort?.call();
|
||||
|
||||
// Cancel transport if supported
|
||||
if (isCancelable(promiseOrValue)) {
|
||||
try {
|
||||
promiseOrValue.cancel();
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check if the transport layer support cancellation
|
||||
this.isTransportCancelable = isCancelable(promiseOrValue);
|
||||
Future.value(promiseOrValue).then(resolve).catchError((error) {
|
||||
// Stop if the fetch is already resolved
|
||||
if (this.isResolved) return;
|
||||
// Do we need to retry the request?
|
||||
int _retry = retry?.call(failureCount, error) ?? 3;
|
||||
double _retryDelay = retryDelay?.call(failureCount, error) ??
|
||||
defaultRetryDelay(failureCount);
|
||||
bool shouldRetry = _retry > 0 && _retry > failureCount;
|
||||
if (cancelRetry || !shouldRetry) {
|
||||
// We are done if the query does not need to be retried
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
this.failureCount++;
|
||||
|
||||
// Notify on fail
|
||||
onFail?.call(this.failureCount, error);
|
||||
Future.delayed(Duration(milliseconds: _retryDelay.toInt()))
|
||||
.then((val) async {
|
||||
if (!await onlineManager.isOnline()) {
|
||||
return pause();
|
||||
}
|
||||
}).then((val) {
|
||||
if (cancelRetry) {
|
||||
reject(error);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Start loop
|
||||
run();
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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> {
|
||||
@protected
|
||||
List<TListener> listeners;
|
||||
Subscribable() : listeners = [];
|
||||
|
||||
void Function() subscribe([TListener? listener]) {
|
||||
listener ??= placeholder as TListener;
|
||||
|
||||
listeners.add(listener);
|
||||
|
||||
onSubscribe();
|
||||
|
||||
return () {
|
||||
listeners = listeners.where((x) => x != listener).toList();
|
||||
onUnsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
bool hasListeners() {
|
||||
return listeners.isNotEmpty;
|
||||
}
|
||||
|
||||
@protected
|
||||
void onSubscribe() {}
|
||||
|
||||
@protected
|
||||
void onUnsubscribe() {}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
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';
|
||||
import 'dart:math';
|
||||
|
||||
/// Default query keys hash function.
|
||||
/// Dummy function just to fill the gaps for original react-query like
|
||||
/// function body signatures
|
||||
/// It is not required as a Standardized [QueryKey] data-class is used to
|
||||
/// create the queryKey
|
||||
String hashQueryKeyByOptions(
|
||||
QueryKey queryKey,
|
||||
QueryOptions? options,
|
||||
) {
|
||||
return options?.queryKeyHashFn?.call(queryKey) ?? queryKey.key;
|
||||
}
|
||||
|
||||
enum QueryStatusFilter {
|
||||
all,
|
||||
active,
|
||||
inactive,
|
||||
none,
|
||||
}
|
||||
|
||||
QueryStatusFilter mapQueryStatusFilter(
|
||||
bool? active,
|
||||
bool? inactive,
|
||||
) {
|
||||
if ((active == true && inactive == true) ||
|
||||
(active == null && inactive == null)) {
|
||||
return QueryStatusFilter.all;
|
||||
} else if (active == false && inactive == false) {
|
||||
return QueryStatusFilter.none;
|
||||
} else {
|
||||
// At this point, active|inactive can only be true|false or false|true
|
||||
// so, when only one value is provided, the missing one has to be the negated value
|
||||
bool isActive = active ?? !(inactive ?? false);
|
||||
return isActive ? QueryStatusFilter.active : QueryStatusFilter.inactive;
|
||||
}
|
||||
}
|
||||
|
||||
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 != queryKeys.key &&
|
||||
!queryKeys.keyAsList.contains(query.queryKey.key) &&
|
||||
!query.queryKey.keyAsList.contains(queryKeys.key)) return false;
|
||||
}
|
||||
QueryStatusFilter queryStatusFilter =
|
||||
mapQueryStatusFilter(filters.active, filters.inactive);
|
||||
|
||||
if (queryStatusFilter == QueryStatusFilter.none) {
|
||||
return false;
|
||||
} else if (queryStatusFilter != QueryStatusFilter.all) {
|
||||
bool isActive = query.isActive();
|
||||
if (queryStatusFilter == QueryStatusFilter.active && !isActive) {
|
||||
return false;
|
||||
}
|
||||
if (queryStatusFilter == QueryStatusFilter.inactive && isActive) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.stale != null && query.isStale() != filters.stale) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.fetching != null && query.isFetching() != filters.fetching) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.predicate != null && !filters.predicate!(query)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void noop([e]) => null;
|
||||
|
||||
bool shallowEqualMap(Map? a, Map? b) {
|
||||
if ((a != null && b == null) || (b != null && a == null)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final item in a!.entries) {
|
||||
if (a[item.key] != b?[item.key]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// This function returns `a` if `b` is deeply equal\
|
||||
/// 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 [List] or [Map]
|
||||
replaceEqualDeep(a, b) {
|
||||
if (a == b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
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 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]) {
|
||||
return Duration(
|
||||
milliseconds: max<int>(
|
||||
updatedAt
|
||||
.add(staleTime ?? Duration.zero)
|
||||
.difference(DateTime.now())
|
||||
.inMilliseconds,
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
typedef DataUpdateFunction<TInput, TOutput> = TOutput Function(TInput input);
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:fl_query/query.dart';
|
||||
|
||||
Future<void> callQueryListeners<T>(Set<QueryListener<T>> listeners, T data) {
|
||||
return Future.wait(listeners.map(
|
||||
(listener) => Future.value(listener(data)),
|
||||
));
|
||||
// for (final listener in listeners) {
|
||||
// await listener(data);
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user