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