From 70bb78afcb06b4128dc55b41efc26072a1eecd17 Mon Sep 17 00:00:00 2001 From: Jojo Feng Date: Sun, 10 Dec 2023 15:29:40 -0800 Subject: [PATCH] use InterceptorsWrapper for caching. (#358) --- lib/config/constants.dart | 3 ++ lib/cubits/comments/comments_cubit.dart | 6 +-- lib/models/app_exception.dart | 8 ---- .../dio/cache_interceptors_wrapper.dart | 46 +++++++++++++++++++ lib/models/dio/cached_response.dart | 19 ++++++++ lib/models/models.dart | 1 + .../hacker_news_web_repository.dart | 38 ++++++--------- 7 files changed, 84 insertions(+), 37 deletions(-) create mode 100644 lib/models/dio/cache_interceptors_wrapper.dart create mode 100644 lib/models/dio/cached_response.dart diff --git a/lib/config/constants.dart b/lib/config/constants.dart index c93f211..16714f0 100644 --- a/lib/config/constants.dart +++ b/lib/config/constants.dart @@ -83,4 +83,7 @@ abstract class AppDurations { static const Duration oneSecond = Duration(seconds: 1); static const Duration twoSeconds = Duration(seconds: 2); static const Duration tenSeconds = Duration(seconds: 10); + static const Duration sec30 = Duration(seconds: 30); + static const Duration oneMinute = Duration(minutes: 1); + static const Duration twoMinutes = Duration(minutes: 2); } diff --git a/lib/cubits/comments/comments_cubit.dart b/lib/cubits/comments/comments_cubit.dart index 003e500..49e93cc 100644 --- a/lib/cubits/comments/comments_cubit.dart +++ b/lib/cubits/comments/comments_cubit.dart @@ -83,7 +83,7 @@ class CommentsCubit extends Cubit { final Map> _streamSubscriptions = >{}; - static const int _webFetchingCmtCountLowerLimit = 100; + static const int _webFetchingCmtCountLowerLimit = 50; Future get _shouldFetchFromWeb async { final bool isOnWifi = await _isOnWifi; @@ -192,8 +192,6 @@ class CommentsCubit extends Cubit { switch (e.runtimeType) { case RateLimitedWithFallbackException: case PossibleParsingException: - case BrowserNotRunningException: - case DelayNotFinishedException: if (_preferenceCubit.state.devModeEnabled) { onError?.call(e as AppException); } @@ -289,8 +287,6 @@ class CommentsCubit extends Cubit { switch (e.runtimeType) { case RateLimitedException: case PossibleParsingException: - case BrowserNotRunningException: - case DelayNotFinishedException: if (_preferenceCubit.state.devModeEnabled) { onError?.call(e as AppException); } diff --git a/lib/models/app_exception.dart b/lib/models/app_exception.dart index 4187feb..45d6f55 100644 --- a/lib/models/app_exception.dart +++ b/lib/models/app_exception.dart @@ -27,14 +27,6 @@ class PossibleParsingException extends AppException { final int itemId; } -class BrowserNotRunningException extends AppException { - BrowserNotRunningException() : super(message: 'Browser not running...'); -} - -class DelayNotFinishedException extends AppException { - DelayNotFinishedException() : super(message: 'Delay not finished...'); -} - class GenericException extends AppException { GenericException() : super(message: 'Something went wrong...'); } diff --git a/lib/models/dio/cache_interceptors_wrapper.dart b/lib/models/dio/cache_interceptors_wrapper.dart new file mode 100644 index 0000000..9009d83 --- /dev/null +++ b/lib/models/dio/cache_interceptors_wrapper.dart @@ -0,0 +1,46 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:hacki/config/constants.dart'; +import 'package:hacki/models/dio/cached_response.dart'; + +class CacheInterceptorsWrapper extends InterceptorsWrapper { + CacheInterceptorsWrapper() + : super( + onResponse: ( + Response response, + ResponseInterceptorHandler handler, + ) async { + final String key = response.requestOptions.uri.toString(); + + if (response.statusCode == HttpStatus.ok) { + final CachedResponse cachedResponse = + CachedResponse.fromResponse(response); + _cache[key] = cachedResponse; + } + + return handler.next(response); + }, + onRequest: ( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + final String key = options.uri.toString(); + final CachedResponse? cachedResponse = _cache[key]; + + if (cachedResponse != null && + DateTime.now() + .difference(cachedResponse.setDateTime) + .inSeconds < + _delay.inSeconds) { + return handler.resolve(cachedResponse); + } + + return handler.next(options); + }, + ); + + static const Duration _delay = AppDurations.oneMinute; + static final Map> _cache = + >{}; +} diff --git a/lib/models/dio/cached_response.dart b/lib/models/dio/cached_response.dart new file mode 100644 index 0000000..d316872 --- /dev/null +++ b/lib/models/dio/cached_response.dart @@ -0,0 +1,19 @@ +import 'package:dio/dio.dart'; + +class CachedResponse extends Response { + CachedResponse({ + required super.requestOptions, + super.data, + super.statusCode, + }) : setDateTime = DateTime.now(); + + factory CachedResponse.fromResponse(Response response) { + return CachedResponse( + requestOptions: response.requestOptions, + data: response.data, + statusCode: response.statusCode, + ); + } + + final DateTime setDateTime; +} diff --git a/lib/models/models.dart b/lib/models/models.dart index 01fe1a8..f6c8288 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -1,5 +1,6 @@ export 'app_exception.dart'; export 'comments_order.dart'; +export 'dio/cache_interceptors_wrapper.dart'; export 'discoverable_feature.dart'; export 'export_destination.dart'; export 'fetch_mode.dart'; diff --git a/lib/repositories/hacker_news_web_repository.dart b/lib/repositories/hacker_news_web_repository.dart index e858b44..b901b67 100644 --- a/lib/repositories/hacker_news_web_repository.dart +++ b/lib/repositories/hacker_news_web_repository.dart @@ -14,24 +14,17 @@ import 'package:html_unescape/html_unescape.dart'; /// For fetching anything that cannot be fetched through Hacker News API. class HackerNewsWebRepository { HackerNewsWebRepository({ + Dio? dioWithCache, Dio? dio, - }) : _dio = dio ?? Dio(); + }) : _dio = dio ?? Dio(), + _dioWithCache = dioWithCache ?? Dio() + ..interceptors.add(CacheInterceptorsWrapper()); + final Dio _dioWithCache; final Dio _dio; - bool _fetchAllowed = true; - - static const Duration _delay = Duration(seconds: 30); static const Map _headers = { - 'accept': - 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'accept-language': 'en-US,en;q=0.9', - 'cache-control': 'max-age=0', - 'sec-fetch-dest': 'document', - 'sec-fetch-mode': 'navigate', - 'sec-fetch-site': 'same-origin', - 'sec-fetch-user': '?1', - 'upgrade-insecure-requests': '1', + 'accept': 'text/html', 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1', }; @@ -42,6 +35,7 @@ class HackerNewsWebRepository { '#hnmain > tbody > tr:nth-child(3) > td > table > tbody > .athing'; Future> fetchFavorites({required String of}) async { + final bool isOnWifi = await _isOnWifi; final String username = of; final List allIds = []; int page = 1; @@ -52,7 +46,8 @@ class HackerNewsWebRepository { final Uri url = Uri.parse( '''$_favoritesBaseUrl$username${isComment ? '&comments=t' : ''}&p=$page''', ); - final Response response = await _dio.getUri(url); + final Response response = + await (isOnWifi ? _dioWithCache : _dio).getUri(url); /// Due to rate limiting, we have a short break here. await Future.delayed(AppDurations.twoSeconds); @@ -108,12 +103,6 @@ class HackerNewsWebRepository { Stream fetchCommentsStream(Item item) async* { final bool isOnWifi = await _isOnWifi; - - /// If user is on wifi, apply fetch delay. - if (isOnWifi && !_fetchAllowed) { - throw DelayNotFinishedException(); - } - final int itemId = item.id; final int? descendants = item is Story ? item.descendants : null; int parentTextCount = 0; @@ -125,10 +114,14 @@ class HackerNewsWebRepository { headers: _headers, persistentConnection: true, ); - final Response response = await _dio.getUri( + + /// Be more conservative while user is on wifi. + final Response response = + await (isOnWifi ? _dioWithCache : _dio).getUri( url, options: option, ); + final String data = response.data ?? ''; if (page == 1) { @@ -240,9 +233,6 @@ class HackerNewsWebRepository { page++; elements = await fetchElements(page); } - - _fetchAllowed = false; - Timer(_delay, () => _fetchAllowed = true); } static Future get _isOnWifi async {