From 45b6f92316ca1496e47462eb67aea64f43f8d9c1 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 11 Feb 2023 13:05:59 +0600 Subject: [PATCH] feat: infinite query implementation --- .../lib/src/collections/default_configs.dart | 5 +- .../lib/src/collections/json_config.dart | 9 + .../lib/src/collections/refresh_config.dart | 11 + .../lib/src/collections/retry_config.dart | 11 + .../fl_query/lib/src/core/infinite_query.dart | 247 ++++++++++++++++++ packages/fl_query/lib/src/core/query.dart | 42 ++- packages/fl_query/lib/src/core/retryer.dart | 13 +- 7 files changed, 300 insertions(+), 38 deletions(-) create mode 100644 packages/fl_query/lib/src/collections/json_config.dart create mode 100644 packages/fl_query/lib/src/collections/refresh_config.dart create mode 100644 packages/fl_query/lib/src/collections/retry_config.dart create mode 100644 packages/fl_query/lib/src/core/infinite_query.dart diff --git a/packages/fl_query/lib/src/collections/default_configs.dart b/packages/fl_query/lib/src/collections/default_configs.dart index 0e5a4a6..98e33f8 100644 --- a/packages/fl_query/lib/src/collections/default_configs.dart +++ b/packages/fl_query/lib/src/collections/default_configs.dart @@ -1,5 +1,5 @@ -import 'package:fl_query/src/core/query.dart'; -import 'package:fl_query/src/core/retryer.dart'; +import 'package:fl_query/src/collections/refresh_config.dart'; +import 'package:fl_query/src/collections/retry_config.dart'; abstract class DefaultConstants { static const RetryConfig retryConfig = RetryConfig( @@ -11,5 +11,6 @@ abstract class DefaultConstants { static const RefreshConfig refreshConfig = RefreshConfig( staleDuration: Duration(seconds: 10), refreshInterval: Duration(seconds: 5), + refreshOnMount: true, ); } diff --git a/packages/fl_query/lib/src/collections/json_config.dart b/packages/fl_query/lib/src/collections/json_config.dart new file mode 100644 index 0000000..762b80d --- /dev/null +++ b/packages/fl_query/lib/src/collections/json_config.dart @@ -0,0 +1,9 @@ +class JsonConfig { + final Map Function(T data) toJson; + final T Function(Map json) fromJson; + + const JsonConfig({ + required this.toJson, + required this.fromJson, + }); +} diff --git a/packages/fl_query/lib/src/collections/refresh_config.dart b/packages/fl_query/lib/src/collections/refresh_config.dart new file mode 100644 index 0000000..1d8613e --- /dev/null +++ b/packages/fl_query/lib/src/collections/refresh_config.dart @@ -0,0 +1,11 @@ +class RefreshConfig { + final Duration staleDuration; + final Duration refreshInterval; + final bool refreshOnMount; + + const RefreshConfig({ + required this.staleDuration, + required this.refreshInterval, + required this.refreshOnMount, + }); +} diff --git a/packages/fl_query/lib/src/collections/retry_config.dart b/packages/fl_query/lib/src/collections/retry_config.dart new file mode 100644 index 0000000..e88cb87 --- /dev/null +++ b/packages/fl_query/lib/src/collections/retry_config.dart @@ -0,0 +1,11 @@ +class RetryConfig { + final int maxRetries; + final Duration retryDelay; + final Duration timeout; + + const RetryConfig({ + required this.maxRetries, + required this.retryDelay, + required this.timeout, + }); +} diff --git a/packages/fl_query/lib/src/core/infinite_query.dart b/packages/fl_query/lib/src/core/infinite_query.dart new file mode 100644 index 0000000..79e678e --- /dev/null +++ b/packages/fl_query/lib/src/core/infinite_query.dart @@ -0,0 +1,247 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:fl_query/src/collections/default_configs.dart'; +import 'package:fl_query/src/collections/json_config.dart'; +import 'package:fl_query/src/collections/refresh_config.dart'; +import 'package:fl_query/src/collections/retry_config.dart'; +import 'package:fl_query/src/core/retryer.dart'; +import 'package:flutter/material.dart'; +import 'package:hive_flutter/adapters.dart'; +import 'package:mutex/mutex.dart'; +import 'package:state_notifier/state_notifier.dart'; + +typedef InfiniteQueryFn = FutureOr Function(P page); +typedef InfiniteQueryNextPage = P? Function( + P lastPage, + List pages, +); + +class InfiniteQueryPage { + final P page; + final T? data; + final E? error; + + final DateTime updatedAt; + final Duration staleDuration; + + const InfiniteQueryPage({ + required this.page, + this.data, + this.error, + required this.updatedAt, + required this.staleDuration, + }); + + bool get isStale => DateTime.now().difference(updatedAt) > staleDuration; + + InfiniteQueryPage copyWith({ + T? data, + E? error, + }) { + return InfiniteQueryPage( + page: page, + updatedAt: DateTime.now(), + staleDuration: staleDuration, + data: data ?? this.data, + error: error ?? this.error, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is InfiniteQueryPage && other.page == page; + } + + @override + int get hashCode => page.hashCode; +} + +class InfiniteQueryState { + final Set> pages; + final InfiniteQueryFn queryFn; + final InfiniteQueryNextPage nextPage; + + const InfiniteQueryState({ + required this.pages, + required this.queryFn, + required this.nextPage, + }); + + P get lastPage => pages.last.page; + P? get getNextPage => nextPage(lastPage, pages.map((e) => e.data!).toList()); + bool get hasNextPage => getNextPage != null; + + InfiniteQueryState copyWith({ + Set>? pages, + InfiniteQueryFn? queryFn, + InfiniteQueryNextPage? nextPage, + }) { + return InfiniteQueryState( + pages: pages ?? this.pages, + queryFn: queryFn ?? this.queryFn, + nextPage: nextPage ?? this.nextPage, + ); + } +} + +class InfiniteQuery + extends StateNotifier> with Retryer { + final ValueKey key; + final RetryConfig retryConfig; + final RefreshConfig refreshConfig; + final JsonConfig? jsonConfig; + + InfiniteQuery( + this.key, + InfiniteQueryFn queryFn, { + required InfiniteQueryNextPage nextPage, + required P initialParam, + this.retryConfig = DefaultConstants.retryConfig, + this.refreshConfig = DefaultConstants.refreshConfig, + this.jsonConfig, + }) : super(InfiniteQueryState( + pages: { + InfiniteQueryPage( + page: initialParam, + updatedAt: DateTime.now(), + staleDuration: refreshConfig.staleDuration, + ), + }, + queryFn: queryFn, + nextPage: nextPage, + )) { + if (jsonConfig != null) { + _mutex.protect(() async { + final json = await _box.get(key.value); + if (json != null) { + state = state.copyWith( + pages: json.map( + (key, value) => MapEntry( + key as P, + jsonConfig!.fromJson(value), + ), + ), + ); + } + }); + + Timer.periodic(refreshConfig.refreshInterval, (_) async { + await Future.wait( + state.pages.map((page) async { + if (page.isStale) { + return await refresh(page.page); + } + }), + ); + }); + } + } + + final _mutex = Mutex(); + final _box = Hive.lazyBox("cache"); + + List get pages => state.pages.map((e) => e.data).whereType().toList(); + List get errors => state.pages.map((e) => e.error).whereType().toList(); + P get lastPage => state.lastPage; + + bool get isLoadingPage => !hasPageData && !hasPageError && _mutex.isLocked; + bool get isRefreshingPage => (hasPageData || hasPageError) && _mutex.isLocked; + + bool get hasPages => pages.isNotEmpty; + bool get hasErrors => errors.isNotEmpty; + + bool get hasPageData => state.pages.last.data != null; + bool get hasPageError => state.pages.last.error != null; + + bool get hasNextPage => state.hasNextPage; + + Future _operation(P page) { + return _mutex.protect(() async { + retryOperation( + () => state.queryFn(page), + config: retryConfig, + onSuccessful: (data) async { + state = state.copyWith( + pages: { + ...state.pages, + state.pages + .firstWhere( + (e) => e.page == page, + orElse: () => InfiniteQueryPage( + page: page, + updatedAt: DateTime.now(), + staleDuration: refreshConfig.staleDuration, + ), + ) + .copyWith(data: data), + }, + ); + if (jsonConfig != null) { + await _box.put( + key.value, + state.pages.map( + (e) => MapEntry( + e.page, + e.data != null ? jsonConfig!.toJson(e.data!) : null, + ), + ), + ); + } + }, + onFailed: (error) { + state = state.copyWith( + pages: { + ...state.pages, + state.pages + .firstWhere( + (e) => e.page == page, + orElse: () => InfiniteQueryPage( + page: page, + updatedAt: DateTime.now(), + staleDuration: refreshConfig.staleDuration, + ), + ) + .copyWith(error: error), + }, + ); + }, + ); + }); + } + + Future fetch() async { + final lastPage = state.lastPage; + if (_mutex.isLocked || hasPageData || hasPageError) + return state.pages.last.data; + return await _operation(lastPage).then((_) => state.pages.last.data); + } + + Future refresh([P? page]) async { + page ??= lastPage; + if (_mutex.isLocked) + return state.pages.firstWhereOrNull((e) => e.page == page)?.data; + return await _operation(page!).then((_) { + return state.pages.firstWhereOrNull((e) => e.page == page)?.data; + }); + } + + Future?> refreshAll() async { + if (_mutex.isLocked) return pages; + return await Future.wait( + state.pages.map((e) => _operation(e.page)), + ).then((_) => pages); + } + + Future fetchNext() async { + final nextPage = state.getNextPage; + if (_mutex.isLocked || nextPage != null) { + return state.pages.firstWhereOrNull((e) => e.page == nextPage)?.data; + } + return await _operation(nextPage!).then((_) { + return state.pages.firstWhereOrNull((e) => e.page == nextPage)?.data; + }); + } +} diff --git a/packages/fl_query/lib/src/core/query.dart b/packages/fl_query/lib/src/core/query.dart index 11549e5..d7d6bb9 100644 --- a/packages/fl_query/lib/src/core/query.dart +++ b/packages/fl_query/lib/src/core/query.dart @@ -1,6 +1,9 @@ import 'dart:async'; import 'package:fl_query/src/collections/default_configs.dart'; +import 'package:fl_query/src/collections/json_config.dart'; +import 'package:fl_query/src/collections/refresh_config.dart'; +import 'package:fl_query/src/collections/retry_config.dart'; import 'package:fl_query/src/core/retryer.dart'; import 'package:flutter/cupertino.dart'; import 'package:hive_flutter/adapters.dart'; @@ -12,7 +15,7 @@ typedef QueryFn = FutureOr Function(); class QueryState { final T? data; final E? error; - final QueryFn? queryFn; + final QueryFn queryFn; final DateTime updatedAt; final Duration staleDuration; @@ -20,7 +23,7 @@ class QueryState { const QueryState({ this.data, this.error, - this.queryFn, + required this.queryFn, required this.updatedAt, required this.staleDuration, }); @@ -44,26 +47,6 @@ class QueryState { } } -class JsonConfig { - final Map Function(T data) toJson; - final T Function(Map json) fromJson; - - const JsonConfig({ - required this.toJson, - required this.fromJson, - }); -} - -class RefreshConfig { - final Duration staleDuration; - final Duration refreshInterval; - - const RefreshConfig({ - required this.staleDuration, - required this.refreshInterval, - }); -} - class Query extends StateNotifier> with Retryer { final ValueKey key; @@ -112,10 +95,13 @@ class Query extends StateNotifier> bool get hasData => state.data != null; bool get hasError => state.error != null; + T? get data => state.data; + E? get error => state.error; + Future _operate() { return _mutex.protect(() async { retryOperation( - state.queryFn!, + state.queryFn, config: retryConfig, onSuccessful: (T? data) { state = state.copyWith(data: data); @@ -134,7 +120,7 @@ class Query extends StateNotifier> } Future fetch() async { - if (_mutex.isLocked || hasData) return state.data; + if (_mutex.isLocked || hasData || hasError) return state.data; return _operate().then((_) => state.data); } @@ -142,4 +128,12 @@ class Query extends StateNotifier> if (_mutex.isLocked) return state.data; return _operate().then((_) => state.data); } + + void updateQueryFn(QueryFn queryFn) { + state = state.copyWith(queryFn: queryFn); + } + + void setData(T data) { + state = state.copyWith(data: data); + } } diff --git a/packages/fl_query/lib/src/core/retryer.dart b/packages/fl_query/lib/src/core/retryer.dart index 2f3c166..59aacbe 100644 --- a/packages/fl_query/lib/src/core/retryer.dart +++ b/packages/fl_query/lib/src/core/retryer.dart @@ -1,19 +1,8 @@ import 'dart:async'; +import 'package:fl_query/src/collections/retry_config.dart'; import 'package:flutter/foundation.dart'; -class RetryConfig { - final int maxRetries; - final Duration retryDelay; - final Duration timeout; - - const RetryConfig({ - required this.maxRetries, - required this.retryDelay, - required this.timeout, - }); -} - mixin Retryer { VoidCallback retryOperation( FutureOr Function() operation, {