removed old implementation
This commit is contained in:
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"editor.tokenColorCustomizations": {
|
||||
"comments": "",
|
||||
"textMateRules": []
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# fl-query
|
||||
|
||||
Flutter Query, the asynchronous data fetching & invalidation library for Flutter
|
||||
@@ -1,29 +0,0 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
@@ -1,10 +0,0 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b
|
||||
channel: stable
|
||||
|
||||
project_type: package
|
||||
@@ -1,3 +0,0 @@
|
||||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
||||
@@ -1 +0,0 @@
|
||||
TODO: Add your license here.
|
||||
@@ -1,39 +0,0 @@
|
||||
<!--
|
||||
This README describes the package. If you publish this package to pub.dev,
|
||||
this README's contents appear on the landing page for your package.
|
||||
|
||||
For information about how to write a good package README, see the guide for
|
||||
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
||||
|
||||
For general information about developing packages, see the Dart guide for
|
||||
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
||||
and the Flutter guide for
|
||||
[developing packages and plugins](https://flutter.dev/developing-packages).
|
||||
-->
|
||||
|
||||
TODO: Put a short description of the package here that helps potential users
|
||||
know whether this package might be useful for them.
|
||||
|
||||
## Features
|
||||
|
||||
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||
|
||||
## Getting started
|
||||
|
||||
TODO: List prerequisites and provide or point to information on how to
|
||||
start using the package.
|
||||
|
||||
## Usage
|
||||
|
||||
TODO: Include short and useful examples for package users. Add longer examples
|
||||
to `/example` folder.
|
||||
|
||||
```dart
|
||||
const like = 'sample';
|
||||
```
|
||||
|
||||
## Additional information
|
||||
|
||||
TODO: Tell users more about the package: where to find more information, how to
|
||||
contribute to the package, how to file issues, what response they can expect
|
||||
from the package authors, and more.
|
||||
@@ -1,4 +0,0 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -1,6 +0,0 @@
|
||||
# Files and directories created by pub.
|
||||
.dart_tool/
|
||||
.packages
|
||||
|
||||
# Conventional directory for build output.
|
||||
build/
|
||||
@@ -1,3 +0,0 @@
|
||||
## 1.0.0
|
||||
|
||||
- Initial version.
|
||||
@@ -1 +0,0 @@
|
||||
A simple command-line application.
|
||||
@@ -1,30 +0,0 @@
|
||||
# This file configures the static analysis results for your project (errors,
|
||||
# warnings, and lints).
|
||||
#
|
||||
# This enables the 'recommended' set of lints from `package:lints`.
|
||||
# This set helps identify many issues that may lead to problems when running
|
||||
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||
# style and format.
|
||||
#
|
||||
# If you want a smaller set of lints you can change this to specify
|
||||
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||
# (the recommended set includes the core lints).
|
||||
# The core lints are also what is used by pub.dev for scoring packages.
|
||||
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
# Uncomment the following section to specify additional rules.
|
||||
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
|
||||
# analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
|
||||
# For more information about the core and recommended set of lints, see
|
||||
# https://dart.dev/go/core-lints
|
||||
|
||||
# For additional information about configuring this file, see
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'package:fl_query/fl_query.dart';
|
||||
|
||||
var todos = [
|
||||
{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false},
|
||||
{
|
||||
"userId": 1,
|
||||
"id": 2,
|
||||
"title": "quis ut nam facilis et officia qui",
|
||||
"completed": false
|
||||
},
|
||||
{"userId": 1, "id": 3, "title": "fugiat veniam minus", "completed": false},
|
||||
{"userId": 1, "id": 4, "title": "et porro tempora", "completed": true},
|
||||
{
|
||||
"userId": 1,
|
||||
"id": 5,
|
||||
"title": "laboriosam mollitia et enim quasi adipisci quia provident illum",
|
||||
"completed": false
|
||||
},
|
||||
{
|
||||
"userId": 1,
|
||||
"id": 6,
|
||||
"title": "qui ullam ratione quibusdam voluptatem quia omnis",
|
||||
"completed": false
|
||||
},
|
||||
];
|
||||
|
||||
void main() async {
|
||||
try {
|
||||
var key = QueryKey("TEST");
|
||||
QueryClient queryClient = QueryClient();
|
||||
queryClient.mount();
|
||||
var data = await queryClient
|
||||
.fetchQuery<Map<String, dynamic>, dynamic, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (context) {
|
||||
return Future.value(todos.first);
|
||||
},
|
||||
);
|
||||
print("======FETCHED DATA======");
|
||||
print(data);
|
||||
print("======CACHED DATA======");
|
||||
print(queryClient.getQueryData(key));
|
||||
queryClient.setQueryData<Map<String, dynamic>>(key, (prevData) {
|
||||
return {
|
||||
...(prevData) ?? {},
|
||||
"title": "Yehi aloh heh",
|
||||
"completed": true,
|
||||
};
|
||||
});
|
||||
print("======CACHED DATA======");
|
||||
print(queryClient.getQueryData(key));
|
||||
print("======STATE======");
|
||||
print(queryClient.getQueryState(key)?.toJson());
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.2"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
fl_query:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
hive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hive
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.13.4"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
internet_connection_checker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: internet_connection_checker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.1+3"
|
||||
lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lints
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
sdks:
|
||||
dart: ">=2.15.1 <3.0.0"
|
||||
@@ -1,16 +0,0 @@
|
||||
name: example
|
||||
description: A simple command-line application.
|
||||
version: 1.0.0
|
||||
# homepage: https://www.example.com
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=2.15.1 <3.0.0'
|
||||
|
||||
|
||||
dependencies:
|
||||
fl_query:
|
||||
path: ../
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^1.0.0
|
||||
@@ -1,4 +0,0 @@
|
||||
library fl_query;
|
||||
|
||||
export 'package:fl_query/src/core/core.dart';
|
||||
// export 'package:fl_query/src/core/framework.dart';
|
||||
@@ -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);
|
||||
@@ -1,31 +0,0 @@
|
||||
name: fl_query
|
||||
description: A new Flutter package project.
|
||||
version: 0.0.1
|
||||
homepage: https://github.com/KRTirtho/fl-query
|
||||
|
||||
environment:
|
||||
sdk: ">=2.15.1 <3.0.0"
|
||||
# flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
# flutter:
|
||||
# sdk: flutter
|
||||
meta: ^1.3.0
|
||||
path: ^1.8.0
|
||||
hive: ^2.0.0
|
||||
http: ^0.13.0
|
||||
collection: ^1.15.0
|
||||
uuid: ^3.0.1
|
||||
internet_connection_checker: ^0.0.1+3
|
||||
|
||||
dev_dependencies:
|
||||
async: ^2.5.0
|
||||
mockito: ^5.0.0
|
||||
test: ^1.18.2
|
||||
coverage: ^1.0.3
|
||||
http_parser: ^4.0.0
|
||||
lints: ^1.0.1
|
||||
build_runner: ^2.1.7
|
||||
|
||||
# The following section is specific to Flutter.
|
||||
# flutter:
|
||||
@@ -1,51 +0,0 @@
|
||||
import 'package:fl_query/src/core/core.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
Uuid uuid = Uuid();
|
||||
|
||||
QueryKey queryKey() {
|
||||
return QueryKey("query_${uuid.v4()}");
|
||||
}
|
||||
|
||||
class SpyFn<T extends Function()> {
|
||||
int _calls = 0;
|
||||
late T _customFn;
|
||||
int get calls => _calls;
|
||||
T get customFn => _customFn;
|
||||
|
||||
SpyFn();
|
||||
|
||||
SpyFn.withFn(this._customFn);
|
||||
|
||||
fn([Function()? cb]) {
|
||||
_calls++;
|
||||
return () => cb?.call();
|
||||
}
|
||||
|
||||
fn1([Function()? cb]) {
|
||||
_calls++;
|
||||
return (p0) => cb?.call();
|
||||
}
|
||||
|
||||
fn2([Function()? cb]) {
|
||||
_calls++;
|
||||
return (p0, p1) => cb?.call();
|
||||
}
|
||||
|
||||
fn3([Function()? cb]) {
|
||||
_calls++;
|
||||
return (p0, p1, p2) => cb?.call();
|
||||
}
|
||||
|
||||
fn4([Function()? cb]) {
|
||||
_calls++;
|
||||
return (p0, p1, p2, p3) => cb?.call();
|
||||
}
|
||||
|
||||
fn5([Function()? cb]) {
|
||||
_calls++;
|
||||
return (p0, p1, p2, p3, p4) => cb?.call();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sleep(int ms) => Future.delayed(Duration(milliseconds: ms));
|
||||
@@ -1,68 +0,0 @@
|
||||
import 'package:fl_query/src/core/notify_manager.dart';
|
||||
import 'package:test/expect.dart';
|
||||
import 'package:test/scaffolding.dart';
|
||||
|
||||
class SpyNotifyManager extends NotifyManager {
|
||||
SpyNotifyManager() : super();
|
||||
|
||||
int flushCall = 0;
|
||||
|
||||
@override
|
||||
void flush() {
|
||||
super.flush();
|
||||
flushCall++;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
group("NotifyManager", () {
|
||||
test(
|
||||
"Should call _notifyFn in schedule When no callback is batched",
|
||||
() async {
|
||||
final NotifyManager notifyManager = NotifyManager();
|
||||
int called = 0;
|
||||
notifyManager.schedule(() => called++);
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
expect(called, equals(1));
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"Should call default _batchNotifyFn even When multiple level deep callbacks are registered",
|
||||
() async {
|
||||
final NotifyManager notifyManager = NotifyManager();
|
||||
int level1 = 0;
|
||||
int level2 = 0;
|
||||
int level3 = 0;
|
||||
callback() async {
|
||||
await Future.delayed(Duration(milliseconds: 20));
|
||||
level3++;
|
||||
}
|
||||
|
||||
notifyManager.batch(() {
|
||||
notifyManager.batch(() {
|
||||
notifyManager.schedule(callback);
|
||||
level2++;
|
||||
});
|
||||
level1++;
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 30));
|
||||
expect(level1, equals(1));
|
||||
expect(level2, equals(1));
|
||||
expect(level3, equals(1));
|
||||
},
|
||||
timeout: Timeout(Duration(minutes: 2)),
|
||||
);
|
||||
|
||||
test("Should flush When Exception is thrown in a batched callback", () {
|
||||
final SpyNotifyManager notifyManager = SpyNotifyManager();
|
||||
try {
|
||||
notifyManager.batch(() {
|
||||
throw Exception("Damn an exception");
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
expect(notifyManager.flushCall, equals(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/src/core/online_manager.dart';
|
||||
import 'package:internet_connection_checker/internet_connection_checker.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:test/expect.dart';
|
||||
import 'package:test/scaffolding.dart';
|
||||
import './online_manager_test.mocks.dart';
|
||||
|
||||
@GenerateMocks([InternetConnectionChecker])
|
||||
void main() {
|
||||
group('OnlineManager', () {
|
||||
late OnlineManager onlineManagerTest;
|
||||
late StreamController<InternetConnectionStatus> statusController;
|
||||
late MockInternetConnectionChecker connectionChecker;
|
||||
setUp(() {
|
||||
statusController = StreamController<InternetConnectionStatus>.broadcast();
|
||||
statusController.add(InternetConnectionStatus.connected);
|
||||
connectionChecker = MockInternetConnectionChecker();
|
||||
when(connectionChecker.hasConnection)
|
||||
.thenAnswer((_) => Future.value(true));
|
||||
when(connectionChecker.hasListeners)
|
||||
.thenReturn(statusController.hasListener);
|
||||
when(connectionChecker.onStatusChange)
|
||||
.thenAnswer((_) => statusController.stream);
|
||||
onlineManagerTest = OnlineManager(connectionChecker);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
statusController.close();
|
||||
});
|
||||
|
||||
test(
|
||||
'isOnline Should return true When InternetConnectionChecker.hasConnection is true',
|
||||
() async {
|
||||
bool online = await onlineManagerTest.isOnline();
|
||||
expect(online, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"setEventListener Should use _online property When setOnline sets _online = false",
|
||||
() async {
|
||||
int count = 0;
|
||||
|
||||
setup(void Function(bool?) setOnline) {
|
||||
Timer(Duration(milliseconds: 20), () {
|
||||
count++;
|
||||
setOnline(false);
|
||||
});
|
||||
return () {};
|
||||
}
|
||||
|
||||
onlineManagerTest.setEventListener(setup);
|
||||
await Future.delayed(Duration(milliseconds: 30));
|
||||
expect(count, equals(1));
|
||||
onlineManagerTest.isOnline().then((online) {
|
||||
expect(online, isFalse);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'setEventListener Should call previous remove handler When replacing an event listener',
|
||||
() {
|
||||
int cb1calls = 0;
|
||||
int cb2calls = 0;
|
||||
onlineManagerTest.setEventListener((_) => () => cb1calls++);
|
||||
onlineManagerTest.setEventListener((_) => () => cb2calls++);
|
||||
expect(cb1calls, equals(1));
|
||||
expect(cb2calls, equals(0));
|
||||
},
|
||||
);
|
||||
test(
|
||||
'Should replace default window listener When a new event listener is set',
|
||||
() {
|
||||
// Should set the default event listener with window event listeners
|
||||
final unsubscribe = onlineManagerTest.subscribe();
|
||||
verify(connectionChecker.onStatusChange.listen).called(1);
|
||||
// Should replace the window default event listener by a new one
|
||||
// and it should call window.removeEventListener twice
|
||||
onlineManagerTest.setEventListener((online) {
|
||||
return () => null;
|
||||
});
|
||||
expect(connectionChecker.hasListeners, isFalse);
|
||||
unsubscribe();
|
||||
},
|
||||
);
|
||||
|
||||
test('Should cancel StreamSubscription When last listener unsubscribes',
|
||||
() {
|
||||
final unsubscribe1 = onlineManager.subscribe(() => null);
|
||||
final unsubscribe2 = onlineManager.subscribe(() => null);
|
||||
|
||||
verify(connectionChecker.onStatusChange.listen).called(1);
|
||||
unsubscribe1();
|
||||
expect(connectionChecker.hasListeners, isTrue);
|
||||
unsubscribe2();
|
||||
expect(connectionChecker.hasListeners, isFalse);
|
||||
}, skip: true);
|
||||
|
||||
test('should keep setup function even if last listener unsubscribes', () {
|
||||
int count = 0;
|
||||
onlineManager.setEventListener((_) => () => count++);
|
||||
|
||||
final unsubscribe1 = onlineManagerTest.subscribe(() => null);
|
||||
expect(count, equals(1));
|
||||
unsubscribe1();
|
||||
|
||||
final unsubscribe2 = onlineManager.subscribe(() => null);
|
||||
expect(count, equals(2));
|
||||
unsubscribe2();
|
||||
}, skip: true);
|
||||
});
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// Mocks generated by Mockito 5.1.0 from annotations
|
||||
// in fl_query/test/src/core/online_manager_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
import 'dart:async' as _i3;
|
||||
|
||||
import 'package:internet_connection_checker/internet_connection_checker.dart'
|
||||
as _i2;
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
|
||||
class _FakeDuration_0 extends _i1.Fake implements Duration {}
|
||||
|
||||
class _FakeAddressCheckResult_1 extends _i1.Fake
|
||||
implements _i2.AddressCheckResult {}
|
||||
|
||||
/// A class which mocks [InternetConnectionChecker].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockInternetConnectionChecker extends _i1.Mock
|
||||
implements _i2.InternetConnectionChecker {
|
||||
MockInternetConnectionChecker() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
List<_i2.AddressCheckOptions> get addresses =>
|
||||
(super.noSuchMethod(Invocation.getter(#addresses),
|
||||
returnValue: <_i2.AddressCheckOptions>[])
|
||||
as List<_i2.AddressCheckOptions>);
|
||||
@override
|
||||
set addresses(List<_i2.AddressCheckOptions>? _addresses) =>
|
||||
super.noSuchMethod(Invocation.setter(#addresses, _addresses),
|
||||
returnValueForMissingStub: null);
|
||||
@override
|
||||
Duration get checkInterval =>
|
||||
(super.noSuchMethod(Invocation.getter(#checkInterval),
|
||||
returnValue: _FakeDuration_0()) as Duration);
|
||||
@override
|
||||
set checkInterval(Duration? _checkInterval) =>
|
||||
super.noSuchMethod(Invocation.setter(#checkInterval, _checkInterval),
|
||||
returnValueForMissingStub: null);
|
||||
@override
|
||||
_i3.Future<bool> get hasConnection =>
|
||||
(super.noSuchMethod(Invocation.getter(#hasConnection),
|
||||
returnValue: Future<bool>.value(false)) as _i3.Future<bool>);
|
||||
@override
|
||||
_i3.Future<_i2.InternetConnectionStatus> get connectionStatus =>
|
||||
(super.noSuchMethod(Invocation.getter(#connectionStatus),
|
||||
returnValue: Future<_i2.InternetConnectionStatus>.value(
|
||||
_i2.InternetConnectionStatus.connected))
|
||||
as _i3.Future<_i2.InternetConnectionStatus>);
|
||||
@override
|
||||
_i3.Stream<_i2.InternetConnectionStatus> get onStatusChange =>
|
||||
(super.noSuchMethod(Invocation.getter(#onStatusChange),
|
||||
returnValue: Stream<_i2.InternetConnectionStatus>.empty())
|
||||
as _i3.Stream<_i2.InternetConnectionStatus>);
|
||||
@override
|
||||
bool get hasListeners =>
|
||||
(super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false)
|
||||
as bool);
|
||||
@override
|
||||
bool get isActivelyChecking =>
|
||||
(super.noSuchMethod(Invocation.getter(#isActivelyChecking),
|
||||
returnValue: false) as bool);
|
||||
@override
|
||||
_i3.Future<_i2.AddressCheckResult> isHostReachable(
|
||||
_i2.AddressCheckOptions? options) =>
|
||||
(super.noSuchMethod(Invocation.method(#isHostReachable, [options]),
|
||||
returnValue: Future<_i2.AddressCheckResult>.value(
|
||||
_FakeAddressCheckResult_1()))
|
||||
as _i3.Future<_i2.AddressCheckResult>);
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
import 'package:fl_query/src/core/core.dart';
|
||||
import 'package:fl_query/src/core/query_cache.dart';
|
||||
import 'package:fl_query/src/core/retryer.dart';
|
||||
import 'package:test/expect.dart';
|
||||
import 'package:test/scaffolding.dart';
|
||||
|
||||
import '../../helpers/utils.dart';
|
||||
|
||||
void main() {
|
||||
group('QueryCache', () {
|
||||
late QueryClient queryClient;
|
||||
late QueryCache queryCache;
|
||||
|
||||
setUp(() {
|
||||
queryClient = new QueryClient();
|
||||
queryCache = queryClient.getQueryCache();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
queryClient.clear();
|
||||
});
|
||||
group('subscribe', () {
|
||||
test('Should pass the correct query', () async {
|
||||
final QueryKey key = queryKey();
|
||||
var _event;
|
||||
subscriber(event) {
|
||||
_event ??= event;
|
||||
}
|
||||
|
||||
final unsubscribe = queryCache.subscribe(subscriber);
|
||||
final Map<String, dynamic> data = {'foo': "foo"};
|
||||
queryClient.setQueryData(key, (_) => data);
|
||||
final query = queryCache.find(key);
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
expect(_event, isA<QueryCacheNotifyEvent>());
|
||||
expect(
|
||||
(_event as QueryCacheNotifyEvent).type,
|
||||
QueryCacheNotifyEventType.queryAdded,
|
||||
);
|
||||
expect(
|
||||
(_event as QueryCacheNotifyEvent).query,
|
||||
same(query),
|
||||
);
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
test('Should notify listeners When new query is added', () async {
|
||||
final key = queryKey();
|
||||
late bool called;
|
||||
callback(_) {
|
||||
called = true;
|
||||
}
|
||||
|
||||
queryCache.subscribe(callback);
|
||||
queryClient.prefetchQuery(
|
||||
queryKey: key,
|
||||
queryFn: (_) async => {'data': "data"},
|
||||
);
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
|
||||
expect(called, isTrue);
|
||||
});
|
||||
|
||||
test('Should include the queryCache and query When notifying listeners',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
QueryCacheNotifyEvent? _event;
|
||||
callback(event) => _event ??= event;
|
||||
queryCache.subscribe(callback);
|
||||
queryClient.prefetchQuery(
|
||||
queryKey: key,
|
||||
queryFn: (_) => {'data': "data"},
|
||||
);
|
||||
final query = queryCache.find(key);
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
expect(_event, isA<QueryCacheNotifyEvent>());
|
||||
expect(_event?.type, QueryCacheNotifyEventType.queryAdded);
|
||||
expect(_event?.query, same(query));
|
||||
});
|
||||
|
||||
test('Should notify subscribers When new query with initialData is added',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
late bool called;
|
||||
callback(_) => called = true;
|
||||
queryCache.subscribe(callback);
|
||||
queryClient
|
||||
.prefetchQuery<Map<String, dynamic>, dynamic, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => {'data': "Data"},
|
||||
options: FetchQueryOptions(initialData: {"data": "initial-data"}),
|
||||
);
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
expect(called, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('find', () {
|
||||
test('Should filter correctly', () async {
|
||||
final key = queryKey();
|
||||
await queryClient.prefetchQuery(
|
||||
queryKey: key,
|
||||
queryFn: (_) => {"data": 'data1'},
|
||||
);
|
||||
final query = queryCache.find(key);
|
||||
expect(query, isNotNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'Should filter correctly When called with exact set to false',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
await queryClient.prefetchQuery(
|
||||
queryKey: key,
|
||||
queryFn: (_) => {"data": 'data1'},
|
||||
);
|
||||
final query = queryCache.find(key, QueryFilters(exact: false));
|
||||
expect(query, isNotNull);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('findAll', () {
|
||||
test('Should filter correctly', () async {
|
||||
final key1 = queryKey();
|
||||
final key2 = queryKey();
|
||||
final key3 = QueryKey.fromList(['posts', "1"]);
|
||||
await queryClient.prefetchQuery(
|
||||
queryKey: key1,
|
||||
queryFn: (_) => {"data": 'data1'},
|
||||
);
|
||||
await queryClient.prefetchQuery(
|
||||
queryKey: key2,
|
||||
queryFn: (_) => {"data": 'data2'},
|
||||
);
|
||||
await queryClient.prefetchQuery(
|
||||
queryKey: key3,
|
||||
queryFn: (_) => {"data": 'data4'},
|
||||
);
|
||||
await queryClient.invalidateQueries(queryKeys: key2);
|
||||
final query1 = queryCache.find(key1);
|
||||
final query2 = queryCache.find(key2);
|
||||
final query4 = queryCache.find(key3);
|
||||
|
||||
expect(queryCache.findAll(key1), equals([query1]));
|
||||
expect(queryCache.findAll(), equals([query1, query2, query4]));
|
||||
expect(
|
||||
queryCache.findAll(key1, QueryFilters(active: false)),
|
||||
equals([query1]),
|
||||
);
|
||||
expect(
|
||||
queryCache.findAll(key1, QueryFilters(active: true)), equals([]));
|
||||
expect(queryCache.findAll(key1, QueryFilters(stale: true)), equals([]));
|
||||
expect(
|
||||
queryCache.findAll(key1, QueryFilters(stale: false)),
|
||||
equals([query1]),
|
||||
);
|
||||
expect(
|
||||
queryCache.findAll(key1, QueryFilters(stale: false, active: true)),
|
||||
equals([]),
|
||||
);
|
||||
expect(
|
||||
queryCache.findAll(key1, QueryFilters(active: false, stale: false)),
|
||||
equals([query1]),
|
||||
);
|
||||
expect(
|
||||
queryCache.findAll(
|
||||
key1,
|
||||
QueryFilters(active: false, stale: false, exact: true),
|
||||
),
|
||||
equals([query1]),
|
||||
);
|
||||
|
||||
expect(queryCache.findAll(key2), equals([query2]));
|
||||
expect(
|
||||
queryCache.findAll(key2, QueryFilters(stale: null)),
|
||||
equals([query2]),
|
||||
);
|
||||
expect(
|
||||
queryCache.findAll(key2, QueryFilters(stale: true)),
|
||||
equals([query2]),
|
||||
);
|
||||
expect(
|
||||
queryCache.findAll(key2, QueryFilters(stale: false)),
|
||||
equals([]),
|
||||
);
|
||||
|
||||
expect(
|
||||
queryCache.findAll(
|
||||
null,
|
||||
QueryFilters(predicate: (query) => query == query4),
|
||||
),
|
||||
equals([query4]),
|
||||
);
|
||||
expect(queryCache.findAll(QueryKey('posts')), equals([query4]));
|
||||
});
|
||||
|
||||
test('Should return all the queries When no filters are defined',
|
||||
() async {
|
||||
final key1 = queryKey();
|
||||
final key2 = queryKey();
|
||||
await queryClient.prefetchQuery(
|
||||
queryKey: key1,
|
||||
queryFn: (_) => {"data": 'data1'},
|
||||
);
|
||||
await queryClient.prefetchQuery(
|
||||
queryKey: key2,
|
||||
queryFn: (_) {
|
||||
return {"data": 'data2'};
|
||||
},
|
||||
);
|
||||
expect(queryCache.findAll().length, 2);
|
||||
});
|
||||
});
|
||||
|
||||
group('QueryCacheConfig.onError', () {
|
||||
test('should be called when a query errors', () async {
|
||||
final key = queryKey();
|
||||
var errorArg;
|
||||
var queryArg;
|
||||
onError(error, query) {
|
||||
errorArg = error;
|
||||
queryArg = query;
|
||||
}
|
||||
|
||||
final testCache = new QueryCache(onError: onError);
|
||||
final testClient = new QueryClient(queryCache: testCache);
|
||||
await testClient
|
||||
.prefetchQuery<Map<String, dynamic>, dynamic, Map<String, dynamic>>(
|
||||
queryKey: key, queryFn: (_) => Future.error('error'));
|
||||
final query = testCache.find(key);
|
||||
expect(errorArg, equals("error"));
|
||||
expect(queryArg, equals(query));
|
||||
});
|
||||
});
|
||||
|
||||
group('QueryCacheConfig.onSuccess', () {
|
||||
test('should be called when a query is successful', () async {
|
||||
final key = queryKey();
|
||||
var dataArg;
|
||||
var queryArg;
|
||||
onData(data, query) {
|
||||
dataArg = data;
|
||||
queryArg = query;
|
||||
}
|
||||
|
||||
final testCache = new QueryCache(onData: onData);
|
||||
final testClient = new QueryClient(queryCache: testCache);
|
||||
await testClient
|
||||
.prefetchQuery<Map<String, dynamic>, dynamic, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => Future.value({"data": 5}),
|
||||
);
|
||||
final query = testCache.find(key);
|
||||
expect(dataArg, equals({"data": 5}));
|
||||
expect(queryArg, equals(query));
|
||||
});
|
||||
});
|
||||
group('QueryCache.add', () {
|
||||
test('should not try to add a query already added to the cache',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
final hash = key.key;
|
||||
|
||||
await queryClient.prefetchQuery(
|
||||
queryKey: key, queryFn: (_) => {"data": 'data1'});
|
||||
|
||||
// Directly add the query from the cache
|
||||
// to simulate a race condition
|
||||
final query = queryCache.queriesMap[hash] as Query;
|
||||
|
||||
// No error should be thrown when trying to add the query
|
||||
queryCache.add(query);
|
||||
expect(queryCache.queries.length, 1);
|
||||
|
||||
// Clean-up to avoid an error when queryClient.clear()
|
||||
queryCache.remove(query);
|
||||
});
|
||||
});
|
||||
|
||||
group('QueryCache.remove', () {
|
||||
test('should not try to remove a query already removed from the cache',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
final hash = key.key;
|
||||
|
||||
await queryClient.prefetchQuery(
|
||||
queryKey: key, queryFn: (_) => {"data": 'data1'});
|
||||
|
||||
// Directly remove the query from the cache
|
||||
// to simulate a race condition
|
||||
final query = queryCache.queriesMap[hash] as Query;
|
||||
queryCache.queriesMap.remove(hash);
|
||||
|
||||
// No error should be thrown when trying to remove the query
|
||||
expect(() => queryCache.remove(query), isNot(throwsException));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,912 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_query/src/core/core.dart';
|
||||
import 'package:fl_query/src/core/query_observer.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../helpers/utils.dart';
|
||||
|
||||
typedef QueryFn = FutureOr<Map<String, dynamic>> Function(
|
||||
QueryFunctionContext<dynamic>);
|
||||
|
||||
void main() {
|
||||
group('QueryObserver', () {
|
||||
late QueryClient queryClient;
|
||||
|
||||
setUp(() {
|
||||
queryClient = QueryClient();
|
||||
queryClient.mount();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
test('should trigger a fetch when subscribed', () async {
|
||||
final key = queryKey();
|
||||
;
|
||||
int calls = 0;
|
||||
queryFn(context) {
|
||||
calls++;
|
||||
return {"data": "data1"};
|
||||
}
|
||||
|
||||
final observer = QueryObserver(
|
||||
queryClient,
|
||||
QueryObserverOptions(queryKey: key, queryFn: queryFn),
|
||||
);
|
||||
final unsubscribe = observer.subscribe();
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
unsubscribe();
|
||||
expect(calls, 1);
|
||||
});
|
||||
|
||||
test('should notify when switching query', () async {
|
||||
final key1 = queryKey();
|
||||
final key2 = queryKey();
|
||||
final List<QueryObserverResult> results = [];
|
||||
final observer = QueryObserver(
|
||||
queryClient,
|
||||
QueryObserverOptions(
|
||||
queryKey: key1,
|
||||
queryFn: (_) => {"data": 1},
|
||||
),
|
||||
);
|
||||
final unsubscribe = observer.subscribe((result) {
|
||||
results.add(result);
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
observer.setOptions(
|
||||
QueryObserverOptions(queryKey: key2, queryFn: (_) => {"data": 2}),
|
||||
);
|
||||
await Future.delayed(Duration(milliseconds: 2));
|
||||
unsubscribe();
|
||||
expect(results.length, 4);
|
||||
expect(results[0].data, isNull);
|
||||
expect(results[0].status, QueryStatus.loading);
|
||||
expect(results[1].data, {"data": 1});
|
||||
expect(results[1].status, QueryStatus.success);
|
||||
expect(results[2].data, isNull);
|
||||
expect(results[2].status, QueryStatus.loading);
|
||||
expect(results[3].data, {"data": 2});
|
||||
expect(results[3].status, QueryStatus.success);
|
||||
});
|
||||
|
||||
test('should be able to fetch with a selector', () async {
|
||||
final key = queryKey();
|
||||
;
|
||||
final observer = QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => {"count": 1},
|
||||
select: (data) => ({"myCount": data?["count"]}),
|
||||
));
|
||||
QueryObserverResult? observerResult;
|
||||
final unsubscribe = observer.subscribe((result) {
|
||||
observerResult = result;
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
unsubscribe();
|
||||
expect(
|
||||
observerResult?.data,
|
||||
equals({"myCount": 1}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should be able to fetch with a selector using the fetch method',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
;
|
||||
final observer = QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => {"count": 1},
|
||||
select: (data) => ({"myCount": data?["count"]}),
|
||||
));
|
||||
final observerResult = await observer.refetch();
|
||||
expect(observerResult?.data, equals({"myCount": 1}));
|
||||
});
|
||||
|
||||
test('should run the selector again if the data changed', () async {
|
||||
final key = queryKey();
|
||||
;
|
||||
int count = 0;
|
||||
final observer = QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => Map.from({"count": count}),
|
||||
select: (data) {
|
||||
count++;
|
||||
return Map.from({"myCount": data?["count"]});
|
||||
},
|
||||
));
|
||||
final observerResult1 = await observer.refetch();
|
||||
final observerResult2 = await observer.refetch();
|
||||
expect(count, 2);
|
||||
expect(observerResult1?.data, equals({"myCount": 0}));
|
||||
expect(observerResult2?.data, equals({"myCount": 1}));
|
||||
});
|
||||
|
||||
test('should run the selector again if the selector changed', () async {
|
||||
final key = queryKey();
|
||||
;
|
||||
int count = 0;
|
||||
final List<QueryObserverResult> results = [];
|
||||
final queryFn = (_) => ({"count": 1});
|
||||
select1(data) {
|
||||
count++;
|
||||
return {"myCount": data?["count"]};
|
||||
}
|
||||
|
||||
select2(_data) {
|
||||
count++;
|
||||
return {"myCount": 99};
|
||||
}
|
||||
|
||||
final observer = new QueryObserver(
|
||||
queryClient,
|
||||
QueryObserverOptions(
|
||||
queryKey: key,
|
||||
queryFn: queryFn,
|
||||
select: select1,
|
||||
));
|
||||
final unsubscribe = observer.subscribe((result) {
|
||||
results.add(result);
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
observer.setOptions(QueryObserverOptions(
|
||||
queryKey: key,
|
||||
queryFn: queryFn,
|
||||
select: select2,
|
||||
));
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
//! Currently causing an extra call for refetch
|
||||
//! select shouldn't be called when refetch is called
|
||||
await observer.refetch();
|
||||
unsubscribe();
|
||||
expect(count, 2);
|
||||
expect(results.length, 5);
|
||||
expect(results.first.status, QueryStatus.loading);
|
||||
expect(results.first.isFetching, isTrue);
|
||||
expect(results.first.data, isNull);
|
||||
expect(results[1].status, QueryStatus.success);
|
||||
expect(results[1].isFetching, false);
|
||||
expect(results[1].data, {"myCount": 1});
|
||||
expect(results[2].status, QueryStatus.success);
|
||||
expect(results[2].isFetching, false);
|
||||
expect(results[2].data, {"myCount": 99});
|
||||
expect(results[3].status, QueryStatus.success);
|
||||
expect(results[3].isFetching, true);
|
||||
expect(results[3].data, {"myCount": 99});
|
||||
expect(results.last.status, QueryStatus.success);
|
||||
expect(results.last.isFetching, false);
|
||||
expect(results.last.data, {"myCount": 99});
|
||||
});
|
||||
test(
|
||||
'should not run the selector again if the data and selector did not change',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
;
|
||||
int count = 0;
|
||||
final List<QueryObserverResult> results = [];
|
||||
final queryFn = (_) => {"count": 1};
|
||||
select(data) {
|
||||
count++;
|
||||
return {"myCount": data["count"]};
|
||||
}
|
||||
|
||||
final observer = new QueryObserver(
|
||||
queryClient,
|
||||
QueryObserverOptions(
|
||||
queryKey: key,
|
||||
queryFn: queryFn,
|
||||
select: select,
|
||||
));
|
||||
final unsubscribe = observer.subscribe((result) {
|
||||
results.add(result);
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
observer.setOptions(QueryObserverOptions(
|
||||
queryKey: key,
|
||||
queryFn: queryFn,
|
||||
select: select,
|
||||
));
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
await observer.refetch();
|
||||
unsubscribe();
|
||||
expect(count, 1);
|
||||
expect(results.length, 4);
|
||||
expect(results.first.status, QueryStatus.loading);
|
||||
expect(results.first.isFetching, isTrue);
|
||||
expect(results.first.data, isNull);
|
||||
expect(results[1].status, QueryStatus.success);
|
||||
expect(results[1].isFetching, false);
|
||||
expect(results[1].data, {"myCount": 1});
|
||||
expect(results[2].status, QueryStatus.success);
|
||||
expect(results[2].isFetching, true);
|
||||
expect(results[2].data, {"myCount": 1});
|
||||
expect(results.last.status, QueryStatus.success);
|
||||
expect(results.last.isFetching, false);
|
||||
expect(results.last.data, {"myCount": 1});
|
||||
});
|
||||
|
||||
test('should not run the selector again if the data did not change',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
;
|
||||
int count = 0;
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => {"count": 1},
|
||||
select: (data) {
|
||||
count++;
|
||||
return {"myCount": data?["count"]};
|
||||
},
|
||||
),
|
||||
);
|
||||
final observerResult1 = await observer.refetch();
|
||||
final observerResult2 = await observer.refetch();
|
||||
expect(count, 1);
|
||||
expect(observerResult1?.data, equals({"myCount": 1}));
|
||||
expect(observerResult2?.data, equals({"myCount": 1}));
|
||||
});
|
||||
|
||||
test('should always run the selector again if selector throws an error',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
;
|
||||
final List<QueryObserverResult> results = [];
|
||||
select(data) {
|
||||
throw new Exception('selector error');
|
||||
}
|
||||
|
||||
queryFn(_) => ({"count": 1});
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: queryFn,
|
||||
select: select,
|
||||
));
|
||||
final unsubscribe = observer.subscribe((result) {
|
||||
results.add(result);
|
||||
});
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
await observer.refetch();
|
||||
unsubscribe();
|
||||
expect(results.length, 5);
|
||||
expect(results.first.status, QueryStatus.loading);
|
||||
expect(results.first.isFetching, isTrue);
|
||||
expect(results.first.data, isNull);
|
||||
expect(results[1].status, QueryStatus.error);
|
||||
expect(results[1].isFetching, false);
|
||||
expect(results[1].data, isNull);
|
||||
expect(results[2].status, QueryStatus.error);
|
||||
expect(results[2].isFetching, true);
|
||||
expect(results[2].data, isNull);
|
||||
expect(results[3].status, QueryStatus.error);
|
||||
expect(results[3].isFetching, false);
|
||||
expect(results[3].data, isNull);
|
||||
expect(results.last.status, QueryStatus.error);
|
||||
expect(results.last.isFetching, false);
|
||||
expect(results.last.data, isNull);
|
||||
});
|
||||
|
||||
test('should structurally share the selector', () async {
|
||||
final key = queryKey();
|
||||
;
|
||||
int count = 0;
|
||||
final observer = new QueryObserver(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => {"count": ++count},
|
||||
select: (data) {
|
||||
return {"myCount": 1};
|
||||
},
|
||||
));
|
||||
final observerResult1 = await observer.refetch();
|
||||
final observerResult2 = await observer.refetch();
|
||||
expect(count, 2);
|
||||
expect(observerResult1?.data, isNotNull);
|
||||
expect(observerResult1?.data, equals(observerResult2?.data));
|
||||
});
|
||||
|
||||
test('should not trigger a fetch when subscribed and disabled', () async {
|
||||
int count = 0;
|
||||
final key = queryKey();
|
||||
;
|
||||
final observer = new QueryObserver(
|
||||
queryClient,
|
||||
QueryObserverOptions(
|
||||
queryKey: key,
|
||||
queryFn: (_) {
|
||||
count++;
|
||||
return {"data": null};
|
||||
},
|
||||
enabled: false,
|
||||
));
|
||||
final unsubscribe = observer.subscribe();
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
unsubscribe();
|
||||
expect(count, 0);
|
||||
});
|
||||
|
||||
test('should not trigger a fetch when not subscribed', () async {
|
||||
int count = 0;
|
||||
final key = queryKey();
|
||||
;
|
||||
new QueryObserver(
|
||||
queryClient,
|
||||
QueryObserverOptions(
|
||||
queryKey: key,
|
||||
queryFn: (_) {
|
||||
count++;
|
||||
return {"data": null};
|
||||
},
|
||||
));
|
||||
await Future.delayed(Duration(milliseconds: 1));
|
||||
expect(count, 0);
|
||||
});
|
||||
|
||||
test('should be able to watch a query without defining a query function',
|
||||
() async {
|
||||
int count = 0;
|
||||
int subscribeCount = 0;
|
||||
final key = queryKey();
|
||||
;
|
||||
queryFn(_) {
|
||||
count++;
|
||||
return {"data": null};
|
||||
}
|
||||
|
||||
final observer = QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: queryFn,
|
||||
enabled: false,
|
||||
));
|
||||
|
||||
final unsubscribe = observer.subscribe((_) {
|
||||
subscribeCount++;
|
||||
});
|
||||
await queryClient.fetchQuery(queryKey: key, queryFn: queryFn);
|
||||
unsubscribe();
|
||||
expect(count, 1);
|
||||
expect(subscribeCount, 2);
|
||||
});
|
||||
|
||||
test('should accept unresolved query config in update function', () async {
|
||||
final key = queryKey();
|
||||
;
|
||||
final observer = QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
enabled: false,
|
||||
));
|
||||
|
||||
final List<QueryObserverResult> results = [];
|
||||
final unsubscribe = observer.subscribe((x) {
|
||||
results.add(x);
|
||||
});
|
||||
observer.setOptions(
|
||||
QueryObserverOptions(
|
||||
enabled: false,
|
||||
staleTime: Duration(milliseconds: 10),
|
||||
),
|
||||
);
|
||||
int count = 0;
|
||||
|
||||
queryFn(_) {
|
||||
count++;
|
||||
return {"data": null};
|
||||
}
|
||||
|
||||
await queryClient.fetchQuery(queryKey: key, queryFn: queryFn);
|
||||
await sleep(100);
|
||||
unsubscribe();
|
||||
expect(count, 1);
|
||||
expect(results.length, 3);
|
||||
expect(results[0].isStale, true);
|
||||
expect(results[1].isStale, false);
|
||||
expect(results[2].isStale, true);
|
||||
});
|
||||
|
||||
test('should be able to handle multiple subscribers', () async {
|
||||
final key = queryKey();
|
||||
|
||||
int count = 0;
|
||||
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
enabled: false,
|
||||
));
|
||||
final List<QueryObserverResult> results1 = [];
|
||||
final List<QueryObserverResult> results2 = [];
|
||||
final unsubscribe1 = observer.subscribe((x) {
|
||||
results1.add(x);
|
||||
});
|
||||
final unsubscribe2 = observer.subscribe((x) {
|
||||
results2.add(x);
|
||||
});
|
||||
await queryClient
|
||||
.fetchQuery<Map<String, dynamic>, dynamic, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) {
|
||||
count++;
|
||||
return {"data": false};
|
||||
},
|
||||
);
|
||||
await sleep(50);
|
||||
unsubscribe1();
|
||||
unsubscribe2();
|
||||
expect(count, 1);
|
||||
expect(results1.length, 2);
|
||||
expect(results2.length, 2);
|
||||
expect(results1[0].data?["data"], isNull);
|
||||
expect(results1[1].data?["data"], isFalse);
|
||||
expect(results2[0].data?["data"], isNull);
|
||||
expect(results2[1].data?["data"], isFalse);
|
||||
});
|
||||
|
||||
test('should be able to resolve a promise', () async {
|
||||
final key = queryKey();
|
||||
int count = 0;
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
enabled: false,
|
||||
));
|
||||
QueryObserverResult<Map<String, dynamic>, dynamic>? value;
|
||||
observer.getNextResult().then((x) {
|
||||
value = x;
|
||||
});
|
||||
queryClient
|
||||
.prefetchQuery<Map<String, dynamic>, dynamic, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) {
|
||||
count++;
|
||||
return {"data": "a data"};
|
||||
});
|
||||
await sleep(50);
|
||||
expect(count, 1);
|
||||
expect(value?.data?["data"], "a data");
|
||||
});
|
||||
|
||||
test('should be able to resolve a promise with an error', () async {
|
||||
final key = queryKey();
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
enabled: false,
|
||||
));
|
||||
var error;
|
||||
await observer.getNextResult(true).catchError((e) async {
|
||||
error = e;
|
||||
return e;
|
||||
});
|
||||
await queryClient
|
||||
.prefetchQuery<Map<String, dynamic>, dynamic, Map<String, dynamic>>(
|
||||
queryKey: key, queryFn: (_) => Future.error('reject'));
|
||||
await sleep(50);
|
||||
expect(error, 'reject');
|
||||
}, skip: true);
|
||||
|
||||
test('should stop retry when unsubscribing', () async {
|
||||
int count = 0;
|
||||
final key = queryKey();
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) {
|
||||
count++;
|
||||
return Future.error({"data": 'reject'});
|
||||
},
|
||||
retry: (_, __) => 10,
|
||||
retryDelay: (_, __) => 50,
|
||||
),
|
||||
);
|
||||
final unsubscribe = observer.subscribe();
|
||||
await sleep(70);
|
||||
unsubscribe();
|
||||
await sleep(200);
|
||||
expect(count, 2);
|
||||
}, skip: true);
|
||||
|
||||
test('should clear interval when unsubscribing to a refetchInterval query',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
|
||||
final fetchData =
|
||||
(_) => Future<Map<String, dynamic>>.error({'data': null});
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: fetchData,
|
||||
cacheTime: Duration.zero,
|
||||
refetchInterval: (_, __) => Duration(milliseconds: 1),
|
||||
));
|
||||
final unsubscribe = observer.subscribe();
|
||||
// @ts-expect-error
|
||||
expect(observer.refetchInterval, isNotNull);
|
||||
unsubscribe();
|
||||
// @ts-expect-error
|
||||
expect(observer.refetchInterval, isNull);
|
||||
await sleep(10);
|
||||
expect(queryClient.getQueryCache().find(key), isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'uses placeholderData as non-cache data when loading a query with no data',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => {'data': 'data'},
|
||||
placeholderData: {"data": 'placeholder'},
|
||||
));
|
||||
final result = observer.getCurrentResult();
|
||||
expect(result?.status, QueryStatus.success);
|
||||
|
||||
final List<QueryObserverResult> results = [];
|
||||
|
||||
final unsubscribe = observer.subscribe((x) {
|
||||
results.add(x);
|
||||
});
|
||||
|
||||
await sleep(10);
|
||||
unsubscribe();
|
||||
|
||||
expect(results.length, 2);
|
||||
expect(results.first.status, QueryStatus.success);
|
||||
expect(results.first.data, equals({"data": 'placeholder'}));
|
||||
expect(results.last.status, QueryStatus.success);
|
||||
expect(results.last.data, equals({'data': 'data'}));
|
||||
});
|
||||
|
||||
test(
|
||||
'the retryer should not throw an error when reject if the retrier is already resolved',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
int count = 0;
|
||||
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) {
|
||||
count++;
|
||||
return Future.error({'reject': count});
|
||||
},
|
||||
retry: (_, __) => 1,
|
||||
retryDelay: (_, __) => 20,
|
||||
));
|
||||
|
||||
final unsubscribe = observer.subscribe();
|
||||
|
||||
// Simulate a race condition when an unsubscribe and a retry occur.
|
||||
await sleep(20);
|
||||
unsubscribe();
|
||||
|
||||
// A second reject is triggered for the retry
|
||||
// but the retryer has already set isResolved to true
|
||||
// so it does nothing and no error is thrown
|
||||
|
||||
// Should not log an error
|
||||
queryClient.clear();
|
||||
await sleep(40);
|
||||
expect(true, true);
|
||||
// expect(consoleMock).not.toHaveBeenNthCalledWith(1, 'reject 1')
|
||||
|
||||
// consoleMock.mockRestore()
|
||||
}, skip: true);
|
||||
|
||||
test('getCurrentQuery should return the current query', () async {
|
||||
final key = queryKey();
|
||||
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key, queryFn: (_) => {'data': 'data'}));
|
||||
|
||||
expect(observer.getCurrentQuery().queryKey, key);
|
||||
});
|
||||
|
||||
test('should throw an error if throwOnError option is true', () async {
|
||||
final key = queryKey();
|
||||
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => Future.error({'error': 'error'}),
|
||||
retry: (_, __) => 0,
|
||||
));
|
||||
|
||||
var error = null;
|
||||
try {
|
||||
await observer.refetch(options: RefetchOptions(throwOnError: true));
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error, equals({'error': 'error'}));
|
||||
});
|
||||
|
||||
test(
|
||||
'should not refetch in background if refetchIntervalInBackground is false',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
final spy = SpyFn();
|
||||
|
||||
// focusManager.setFocused(false)
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: spy.fn1(),
|
||||
refetchIntervalInBackground: false,
|
||||
refetchInterval: (_, __) => Duration(milliseconds: 10),
|
||||
));
|
||||
|
||||
final unsubscribe = observer.subscribe();
|
||||
await sleep(30);
|
||||
|
||||
expect(spy.calls, 1);
|
||||
|
||||
// Clean-up
|
||||
unsubscribe();
|
||||
// focusManager.setFocused(true)
|
||||
});
|
||||
|
||||
test(
|
||||
'should not use replaceEqualDeep for select value when structuralSharing option is true',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
|
||||
final data = {"value": 'data'};
|
||||
final selectedData = {"value": 'data'};
|
||||
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => data,
|
||||
select: (_) => data,
|
||||
));
|
||||
|
||||
final unsubscribe = observer.subscribe();
|
||||
|
||||
await sleep(10);
|
||||
expect(observer.getCurrentResult()?.data, data);
|
||||
|
||||
observer.setOptions(QueryObserverOptions(
|
||||
queryKey: key,
|
||||
queryFn: (_) => data,
|
||||
structuralSharing: false,
|
||||
select: (_) => selectedData,
|
||||
));
|
||||
|
||||
await observer.refetch(filters: RefetchableQueryFilters(queryKey: key));
|
||||
expect(observer.getCurrentResult()?.data, selectedData);
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
test('select function error using placeholderdata should log an error', () {
|
||||
final key = queryKey();
|
||||
|
||||
QueryObserver<Map<String, dynamic>, dynamic, Map<String, dynamic>,
|
||||
Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => {"data": 'data'},
|
||||
placeholderData: {"data": 'placeholderdata'},
|
||||
select: (_) {
|
||||
throw new Exception('error');
|
||||
},
|
||||
));
|
||||
|
||||
expect(true, true);
|
||||
// expect(consoleMock).toHaveBeenNthCalledWith(1, new Error('error'))
|
||||
|
||||
// consoleMock.mockRestore()
|
||||
});
|
||||
|
||||
test(
|
||||
'should not use replaceEqualDeep for select value when structuralSharing option is true and placeholderdata is defined',
|
||||
() {
|
||||
final key = queryKey();
|
||||
|
||||
final data = {"value": 'data'};
|
||||
final selectedData1 = {"value": 'data'};
|
||||
final selectedData2 = {"value": 'data'};
|
||||
final placeholderData1 = {"value": 'data'};
|
||||
final placeholderData2 = {"value": 'data'};
|
||||
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => data,
|
||||
select: (_) => data,
|
||||
));
|
||||
|
||||
observer.setOptions(QueryObserverOptions(
|
||||
queryKey: key,
|
||||
queryFn: (_) => data,
|
||||
select: (_) {
|
||||
return selectedData1;
|
||||
},
|
||||
placeholderData: placeholderData1,
|
||||
));
|
||||
observer.setOptions(QueryObserverOptions(
|
||||
queryKey: key,
|
||||
queryFn: (_) => data,
|
||||
select: (_) {
|
||||
return selectedData2;
|
||||
},
|
||||
placeholderData: placeholderData2,
|
||||
structuralSharing: false,
|
||||
));
|
||||
|
||||
expect(observer.getCurrentResult()?.data, equals(selectedData2));
|
||||
});
|
||||
|
||||
test(
|
||||
'should not use an undefined value returned by select as placeholderdata',
|
||||
() {
|
||||
final key = queryKey();
|
||||
|
||||
final data = {"value": 'data'};
|
||||
final selectedData = {"value": 'data'};
|
||||
final placeholderData1 = {"value": 'data'};
|
||||
final placeholderData2 = {"value": 'data'};
|
||||
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => data,
|
||||
select: (_) => data,
|
||||
));
|
||||
|
||||
observer.setOptions(QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => data,
|
||||
select: (_) {
|
||||
return selectedData;
|
||||
},
|
||||
placeholderData: placeholderData1,
|
||||
));
|
||||
|
||||
expect(observer.getCurrentResult()?.isPlaceholderData, isTrue);
|
||||
|
||||
observer.setOptions(QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: (_) => data,
|
||||
select: (_) {
|
||||
return {};
|
||||
},
|
||||
placeholderData: placeholderData2,
|
||||
));
|
||||
|
||||
expect(observer.getCurrentResult()?.isPlaceholderData, isFalse);
|
||||
});
|
||||
|
||||
test(
|
||||
'updateResult should not notify cache listeners if cache option is false',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
|
||||
final data1 = {"value": 'data 1'};
|
||||
final data2 = {"value": 'data 2'};
|
||||
|
||||
await queryClient.prefetchQuery(queryKey: key, queryFn: (_) => data1);
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(queryKey: key));
|
||||
await queryClient.prefetchQuery(queryKey: key, queryFn: (_) => data2);
|
||||
|
||||
final spy = SpyFn();
|
||||
final unsubscribe = queryClient.getQueryCache().subscribe(spy.fn1());
|
||||
observer.updateResult(NotifyOptions(cache: false));
|
||||
|
||||
expect(spy.calls, 0);
|
||||
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
test(
|
||||
'should not notify observer when the stale timeout expires and the current result is stale',
|
||||
() async {
|
||||
final key = queryKey();
|
||||
final queryFn = (_) => {'data': "data"};
|
||||
|
||||
await queryClient.prefetchQuery(queryKey: key, queryFn: queryFn);
|
||||
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryClient,
|
||||
QueryObserverOptions<Map<String, dynamic>, dynamic,
|
||||
Map<String, dynamic>, Map<String, dynamic>>(
|
||||
queryKey: key,
|
||||
queryFn: queryFn,
|
||||
staleTime: Duration(milliseconds: 20),
|
||||
));
|
||||
|
||||
final spy = SpyFn();
|
||||
final unsubscribe = observer.subscribe(spy.fn1());
|
||||
await queryClient.refetchQueries(queryKeys: key);
|
||||
await sleep(10);
|
||||
|
||||
// Force isStale to true
|
||||
// because no use case has been found to reproduce this condition
|
||||
// @ts-ignore
|
||||
// observer.getCurrentResult().isStale = true;
|
||||
await sleep(30);
|
||||
expect(spy.calls, 0);
|
||||
unsubscribe();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
import 'package:fl_query/src/core/utils.dart';
|
||||
import 'package:test/expect.dart';
|
||||
import 'package:test/scaffolding.dart';
|
||||
|
||||
void main() {
|
||||
group('core/utils', () {
|
||||
group('replaceEqualDeep', () {
|
||||
test(
|
||||
'Should return the previous value When the next value is an equal primitive',
|
||||
() {
|
||||
expect(replaceEqualDeep(1, 1), equals(1));
|
||||
expect(replaceEqualDeep('1', '1'), equals('1'));
|
||||
expect(replaceEqualDeep(true, true), true);
|
||||
expect(replaceEqualDeep(false, false), false);
|
||||
expect(replaceEqualDeep(null, null), null);
|
||||
});
|
||||
test(
|
||||
'Should return the next value When the previous value is a different value',
|
||||
() {
|
||||
expect(replaceEqualDeep(1, 0), equals(0));
|
||||
expect(replaceEqualDeep(1, 2), equals(2));
|
||||
expect(replaceEqualDeep('1', '2'), equals('2'));
|
||||
expect(replaceEqualDeep(true, false), equals(false));
|
||||
expect(replaceEqualDeep(false, true), equals(true));
|
||||
});
|
||||
|
||||
test(
|
||||
'Should return the next value When the previous value is a different type',
|
||||
() {
|
||||
final array = [1];
|
||||
final object = {"a": "a"};
|
||||
expect(replaceEqualDeep(0, null), equals(null));
|
||||
expect(replaceEqualDeep(null, 0), equals(0));
|
||||
expect(replaceEqualDeep(2, null), equals(null));
|
||||
expect(replaceEqualDeep(null, 2), equals(2));
|
||||
expect(replaceEqualDeep({}, null), equals(null));
|
||||
expect(replaceEqualDeep([], null), equals(null));
|
||||
expect(replaceEqualDeep(array, object), equals(object));
|
||||
expect(replaceEqualDeep(object, array), equals(array));
|
||||
});
|
||||
|
||||
test(
|
||||
'Should return the previous value When the next value is an equal array',
|
||||
() {
|
||||
final prev = [1, 2];
|
||||
final next = [1, 2];
|
||||
expect(replaceEqualDeep(prev, next), equals(prev));
|
||||
});
|
||||
|
||||
test(
|
||||
'Should return a copy When the previous value is a different array subset',
|
||||
() {
|
||||
final prev = [1, 2];
|
||||
final next = [1, 2, 3];
|
||||
final result = replaceEqualDeep(prev, next);
|
||||
expect(result, equals(next));
|
||||
expect(result, isNot(equals(prev)));
|
||||
});
|
||||
|
||||
test(
|
||||
'Should return the previous value When the next value is an equal empty array',
|
||||
() {
|
||||
final prev = [];
|
||||
final next = [];
|
||||
expect(replaceEqualDeep(prev, next), equals(prev));
|
||||
});
|
||||
|
||||
test(
|
||||
'Should return the previous value When the next value is an equal empty object',
|
||||
() {
|
||||
final prev = {};
|
||||
final next = {};
|
||||
expect(replaceEqualDeep(prev, next), equals(prev));
|
||||
});
|
||||
|
||||
test(
|
||||
'Should return the previous value When the next value is an equal object',
|
||||
() {
|
||||
final prev = {"a": 'a'};
|
||||
final next = {"a": 'a'};
|
||||
expect(replaceEqualDeep(prev, next), equals(prev));
|
||||
});
|
||||
|
||||
test('Should replace different values in objects', () {
|
||||
final prev = {
|
||||
"a": {"b": 'b'},
|
||||
"c": 'c'
|
||||
};
|
||||
final next = {
|
||||
"a": {"b": 'b'},
|
||||
"c": 'd'
|
||||
};
|
||||
final result = replaceEqualDeep(prev, next);
|
||||
expect(result, equals(next));
|
||||
expect(result, isNot(prev));
|
||||
expect(result["a"], equals(prev["a"]));
|
||||
expect(result["c"], equals(next["c"]));
|
||||
});
|
||||
|
||||
test('Should replace different values in arrays', () {
|
||||
final prev = [
|
||||
1,
|
||||
{"a": 'a'},
|
||||
{
|
||||
"b": {"b": 'b'}
|
||||
},
|
||||
[1]
|
||||
];
|
||||
final next = [
|
||||
1,
|
||||
{"a": 'a'},
|
||||
{
|
||||
"b": {"b": 'c'}
|
||||
},
|
||||
[1]
|
||||
];
|
||||
final result = replaceEqualDeep(prev, next);
|
||||
expect(result, equals(next));
|
||||
expect(result, isNot(prev));
|
||||
expect(result[0], prev[0]);
|
||||
expect(result[1], prev[1]);
|
||||
expect((result[2] as Map)["b"]["b"], (next[2] as Map)["b"]["b"]);
|
||||
expect(result[3], prev[3]);
|
||||
});
|
||||
|
||||
test(
|
||||
'Should replace different values in arrays When the next value is a subset',
|
||||
() {
|
||||
final prev = [
|
||||
{"a": 'a'},
|
||||
{"b": 'b'},
|
||||
{"c": 'c'}
|
||||
];
|
||||
final next = [
|
||||
{"a": 'a'},
|
||||
{"b": 'b'}
|
||||
];
|
||||
final result = replaceEqualDeep(prev, next);
|
||||
expect(result, equals(next));
|
||||
expect(result, isNot(prev));
|
||||
expect(result[0], prev[0]);
|
||||
expect(result[1], prev[1]);
|
||||
expect(() => result[2], throwsRangeError);
|
||||
});
|
||||
|
||||
test(
|
||||
'Should replace different values in arrays When the next value is a superset',
|
||||
() {
|
||||
final prev = [
|
||||
{"a": 'a'},
|
||||
{"b": 'b'}
|
||||
];
|
||||
final next = [
|
||||
{"a": 'a'},
|
||||
{"b": 'b'},
|
||||
{"c": 'c'}
|
||||
];
|
||||
final result = replaceEqualDeep(prev, next);
|
||||
expect(result, equals(next));
|
||||
expect(result[0], equals(prev[0]));
|
||||
expect(result[1], equals(prev[1]));
|
||||
expect(result[2], equals(next[2]));
|
||||
});
|
||||
|
||||
test('Should copy objects which are not arrays or objects', () {
|
||||
final prev = [
|
||||
{"a": 'a'},
|
||||
{"b": 'b'},
|
||||
{"c": 'c'},
|
||||
1
|
||||
];
|
||||
final next = [
|
||||
{"a": 'a'},
|
||||
Map(),
|
||||
{"c": 'c'},
|
||||
2
|
||||
];
|
||||
final result = replaceEqualDeep(prev, next);
|
||||
expect(result[0], equals(prev[0]));
|
||||
expect(result[1], equals(next[1]));
|
||||
expect(result[2], equals(prev[2]));
|
||||
expect(result[3], equals(next[3]));
|
||||
});
|
||||
|
||||
test('Should support equal objects which are not arrays or objects', () {
|
||||
final map = new Map();
|
||||
final prev = [
|
||||
map,
|
||||
[1]
|
||||
];
|
||||
final next = [
|
||||
map,
|
||||
[1]
|
||||
];
|
||||
final result = replaceEqualDeep(prev, next);
|
||||
expect(result, equals(prev));
|
||||
});
|
||||
|
||||
test('Should support non equal objects which are not arrays or objects',
|
||||
() {
|
||||
final map1 = new Map();
|
||||
final map2 = new Map();
|
||||
final prev = [
|
||||
map1,
|
||||
[1]
|
||||
];
|
||||
final next = [
|
||||
map2,
|
||||
[1]
|
||||
];
|
||||
final result = replaceEqualDeep(prev, next);
|
||||
expect(result[0], equals(next[0]));
|
||||
expect(result[1], equals(prev[1]));
|
||||
});
|
||||
|
||||
test('Should replace all parent objects if some nested value changes',
|
||||
() {
|
||||
final prev = {
|
||||
"todo": {
|
||||
"id": '1',
|
||||
"meta": {"createdAt": 0},
|
||||
"state": {"done": false},
|
||||
},
|
||||
"otherTodo": {
|
||||
"id": '2',
|
||||
"meta": {"createdAt": 0},
|
||||
"state": {"done": true},
|
||||
},
|
||||
};
|
||||
final next = {
|
||||
"todo": {
|
||||
"id": '1',
|
||||
"meta": {"createdAt": 0},
|
||||
"state": {"done": true},
|
||||
},
|
||||
"otherTodo": {
|
||||
"id": '2',
|
||||
"meta": {"createdAt": 0},
|
||||
"state": {"done": true},
|
||||
},
|
||||
};
|
||||
final result = replaceEqualDeep(prev, next);
|
||||
expect(result, equals(next));
|
||||
expect(result["todo"] == prev["todo"], isFalse);
|
||||
expect(result["todo"] == next["todo"], isFalse);
|
||||
expect(result["todo"]["id"], equals((next["todo"] as Map)["id"]));
|
||||
expect(result["todo"]["meta"], equals((prev["todo"] as Map)["meta"]));
|
||||
expect(
|
||||
result["todo"]["state"],
|
||||
equals((next["todo"] as Map)["state"]),
|
||||
);
|
||||
expect(
|
||||
result["todo"]["state"]["done"],
|
||||
(next["todo"] as Map)["state"]["done"],
|
||||
);
|
||||
expect(result["otherTodo"], prev["otherTodo"]);
|
||||
});
|
||||
|
||||
test('Should replace all parent arrays if some nested value changes', () {
|
||||
final Map<String, List<Map>> prev = {
|
||||
"todos": [
|
||||
{
|
||||
"id": '1',
|
||||
"meta": {"createdAt": 0},
|
||||
"state": {"done": false}
|
||||
},
|
||||
{
|
||||
"id": '2',
|
||||
"meta": {"createdAt": 0},
|
||||
"state": {"done": true}
|
||||
},
|
||||
],
|
||||
};
|
||||
final Map<String, List<Map>> next = {
|
||||
"todos": [
|
||||
{
|
||||
"id": '1',
|
||||
"meta": {"createdAt": 0},
|
||||
"state": {"done": true}
|
||||
},
|
||||
{
|
||||
"id": '2',
|
||||
"meta": {"createdAt": 0},
|
||||
"state": {"done": true}
|
||||
},
|
||||
],
|
||||
};
|
||||
final result = replaceEqualDeep(prev, next);
|
||||
expect(result, equals(next));
|
||||
expect(result["todos"][0], isNot(equals(prev["todos"]?.first)));
|
||||
expect(
|
||||
result["todos"][0]?["id"],
|
||||
equals(next["todos"]?.first["id"]),
|
||||
);
|
||||
expect(
|
||||
result["todos"][0]?["meta"],
|
||||
equals(prev["todos"]?.first["meta"]),
|
||||
);
|
||||
expect(result["todos"][0]?["state"]["done"],
|
||||
next["todos"]?.first["state"]["done"]);
|
||||
expect(result["todos"][1], equals(prev["todos"]?[1]));
|
||||
});
|
||||
});
|
||||
|
||||
group(
|
||||
'matchMutation',
|
||||
() => {
|
||||
// test('should return false if mutationKey options is undefined', () => {
|
||||
// const filters = { mutationKey: 'key1' };
|
||||
// const queryClient = new QueryClient();
|
||||
// const mutation = new Mutation({
|
||||
// mutationId: 1,
|
||||
// mutationCache: queryClient.getMutationCache(),
|
||||
// options: {},
|
||||
// })
|
||||
// expect(matchMutation(filters, mutation)).toBeFalsy()
|
||||
// })
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user