From d9ada39c2cdb4eb0cb6e5c50743bfc84273f3cec Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 27 Oct 2025 10:34:05 -0400 Subject: [PATCH] updates to cache clearing, make instance variables in request/response models final --- .../controllers/igc_controller.dart | 2 - .../controllers/it_controller.dart | 177 ++++++++---------- .../controllers/span_data_controller.dart | 71 ++----- lib/pangea/choreographer/models/it_step.dart | 27 +++ .../contextual_definition_request_model.dart | 2 +- .../contextual_definition_response_model.dart | 4 +- .../repo/custom_input_request_model.dart | 48 ++++- .../repo/full_text_translation_repo.dart | 17 +- .../full_text_translation_request_model.dart | 20 +- .../full_text_translation_response_model.dart | 11 +- lib/pangea/choreographer/repo/igc_repo.dart | 40 ++-- .../choreographer/repo/igc_request_model.dart | 25 ++- .../repo/interactive_translation_repo.dart | 79 +++++++- .../choreographer/repo/it_response_model.dart | 14 +- .../choreographer/repo/span_data_repo.dart | 74 +++++++- 15 files changed, 362 insertions(+), 249 deletions(-) diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 0f644bd97..0dd7836ae 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -202,8 +202,6 @@ class IgcController { clear() { igcTextData = null; - spanDataController.clearCache(); - spanDataController.dispose(); MatrixState.pAnyState.closeAllOverlays( filter: RegExp(r'span_card_overlay_\d+'), ); diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 62f5144d5..1b3075fab 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -3,17 +3,19 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; +import 'package:async/async.dart'; import 'package:http/http.dart' as http; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart'; import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart'; +import 'package:fluffychat/pangea/choreographer/repo/interactive_translation_repo.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../models/it_step.dart'; import '../repo/custom_input_request_model.dart'; -import '../repo/interactive_translation_repo.dart'; import '../repo/it_response_model.dart'; import 'choreographer.dart'; @@ -116,80 +118,73 @@ class ITController { // used 1) at very beginning (with custom input = null) // and 2) if they make direct edits to the text field Future getTranslationData(bool useCustomInput) async { - try { - choreographer.startLoading(); + final String currentText = choreographer.currentText; - final String currentText = choreographer.currentText; + if (sourceText == null) await _setSourceText(); - if (sourceText == null) await _setSourceText(); + if (useCustomInput && currentITStep != null) { + completedITSteps.add( + ITStep( + currentITStep!.continuances, + customInput: currentText, + ), + ); + } - if (useCustomInput && currentITStep != null) { - completedITSteps.add( - ITStep( - currentITStep!.continuances, - customInput: currentText, - ), - ); - } + currentITStep = null; - currentITStep = null; + // During first IT step, next step will not be set + if (nextITStep == null) { + final res = await ITRepo.get(_request(currentText)).timeout( + const Duration(seconds: 10), + onTimeout: () { + return Result.error( + TimeoutException("ITRepo.get timed out after 10 seconds"), + ); + }, + ); - // During first IT step, next step will not be set - if (nextITStep == null) { - final ITResponseModel res = await _customInputTranslation(currentText) - .timeout(const Duration(seconds: 10)); - if (sourceText == null) return; - - if (res.goldContinuances != null && res.goldContinuances!.isNotEmpty) { - goldRouteTracker = GoldRouteTracker( - res.goldContinuances!, - sourceText!, + if (res.isError) { + if (_willOpen) { + choreographer.errorService.setErrorAndLock( + ChoreoError(raw: res.asError), ); } - - currentITStep = CurrentITStep( - sourceText: sourceText!, - currentText: currentText, - responseModel: res, - storedGoldContinuances: goldRouteTracker.continuances, - ); - - _addPayloadId(res); - } else { - currentITStep = await nextITStep!.future; + return; } - if (isTranslationDone) { - nextITStep = null; - closeIT(); - } else { - nextITStep = Completer(); - final nextStep = await _getNextTranslationData(); - nextITStep?.complete(nextStep); + if (sourceText == null) { + return; } - } catch (e, s) { - debugger(when: kDebugMode); - if (e is! http.Response) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "currentText": choreographer.currentText, - "sourceText": sourceText, - "currentITStepPayloadID": currentITStep?.payloadId, - }, - level: - e is TimeoutException ? SentryLevel.warning : SentryLevel.error, + + final result = res.result!; + if (result.goldContinuances != null && + result.goldContinuances!.isNotEmpty) { + goldRouteTracker = GoldRouteTracker( + result.goldContinuances!, + sourceText!, ); } - if (_willOpen) { - choreographer.errorService.setErrorAndLock( - ChoreoError(raw: e), - ); - } - } finally { - choreographer.stopLoading(); + currentITStep = CurrentITStep( + sourceText: sourceText!, + currentText: currentText, + responseModel: result, + storedGoldContinuances: goldRouteTracker.continuances, + ); + + _addPayloadId(result); + } else { + currentITStep = await nextITStep!.future; + } + + if (isTranslationDone) { + nextITStep = null; + closeIT(); + } else { + nextITStep = Completer(); + final nextStep = await _getNextTranslationData(); + nextITStep?.complete(nextStep); } } @@ -210,42 +205,28 @@ class ITController { return null; } - try { - final String currentText = choreographer.currentText; - final String nextText = - goldRouteTracker.continuances[completedITSteps.length].text; + final String currentText = choreographer.currentText; + final String nextText = + goldRouteTracker.continuances[completedITSteps.length].text; - final ITResponseModel res = - await _customInputTranslation(currentText + nextText); - if (sourceText == null) return null; + final res = await ITRepo.get( + _request(currentText + nextText), + ); - return CurrentITStep( - sourceText: sourceText!, - currentText: nextText, - responseModel: res, - storedGoldContinuances: goldRouteTracker.continuances, - ); - } catch (e, s) { - debugger(when: kDebugMode); - if (e is! http.Response) { - ErrorHandler.logError( - e: e, - s: s, - data: { - "sourceText": sourceText, - "currentITStepPayloadID": currentITStep?.payloadId, - "continuances": - goldRouteTracker.continuances.map((e) => e.toJson()), - }, - ); - } + if (sourceText == null) return null; + if (res.isError) { choreographer.errorService.setErrorAndLock( - ChoreoError(raw: e), + ChoreoError(raw: res.asError), ); - } finally { - choreographer.stopLoading(); + return null; } - return null; + + return CurrentITStep( + sourceText: sourceText!, + currentText: nextText, + responseModel: res.result!, + storedGoldContinuances: goldRouteTracker.continuances, + ); } Future onEditSourceTextSubmit(String newSourceText) async { @@ -277,7 +258,6 @@ class ITController { ChoreoError(raw: err), ); } finally { - choreographer.stopLoading(); choreographer.textController.setSystemText( "", EditType.other, @@ -285,10 +265,7 @@ class ITController { } } - Future _customInputTranslation(String textInput) async { - return ITRepo.customInputTranslate( - CustomInputRequestModel( - //this should be set by this time + CustomInputRequestModel _request(String textInput) => CustomInputRequestModel( text: sourceText!, customInput: textInput, sourceLangCode: sourceLangCode, @@ -297,9 +274,7 @@ class ITController { roomId: choreographer.roomId!, goldTranslation: goldRouteTracker.fullTranslation, goldContinuances: goldRouteTracker.continuances, - ), - ); - } + ); //maybe we store IT data in the same format? make a specific kind of match? void selectTranslation(int chosenIndex) { diff --git a/lib/pangea/choreographer/controllers/span_data_controller.dart b/lib/pangea/choreographer/controllers/span_data_controller.dart index 91dde7a5a..b6b3070db 100644 --- a/lib/pangea/choreographer/controllers/span_data_controller.dart +++ b/lib/pangea/choreographer/controllers/span_data_controller.dart @@ -9,35 +9,11 @@ import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/models/span_data.dart'; import 'package:fluffychat/pangea/choreographer/repo/span_data_repo.dart'; import 'package:fluffychat/pangea/choreographer/utils/text_normalization_util.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; - -class _SpanDetailsCacheItem { - Future data; - - _SpanDetailsCacheItem({required this.data}); -} +import 'package:fluffychat/widgets/future_loading_dialog.dart'; class SpanDataController { late Choreographer choreographer; - final Map _cache = {}; - Timer? _cacheClearTimer; - - SpanDataController(this.choreographer) { - _initializeCacheClearing(); - } - - void _initializeCacheClearing() { - const duration = Duration(minutes: 2); - _cacheClearTimer = Timer.periodic(duration, (Timer t) => clearCache()); - } - - void clearCache() { - _cache.clear(); - } - - void dispose() { - _cacheClearTimer?.cancel(); - } + SpanDataController(this.choreographer); SpanData? _getSpan(int matchIndex) { if (choreographer.igc.igcTextData == null || @@ -80,41 +56,20 @@ class SpanDataController { }) async { final SpanData? span = _getSpan(matchIndex); if (span == null || (isNormalizationError(matchIndex) && !force)) return; - - final req = SpanDetailsRepoReqAndRes( - userL1: choreographer.l1LangCode!, - userL2: choreographer.l2LangCode!, - enableIGC: choreographer.igcEnabled, - enableIT: choreographer.itEnabled, - span: span, + final response = await SpanDataRepo.get( + choreographer.accessToken, + request: SpanDetailsRepoReqAndRes( + userL1: choreographer.l1LangCode!, + userL2: choreographer.l2LangCode!, + enableIGC: choreographer.igcEnabled, + enableIT: choreographer.itEnabled, + span: span, + ), ); - final int cacheKey = req.hashCode; - /// Retrieves the [SpanDetailsRepoReqAndRes] response from the cache if it exists, - /// otherwise makes an API call to get the response and stores it in the cache. - Future response; - if (_cache.containsKey(cacheKey)) { - response = _cache[cacheKey]!.data; - } else { - response = SpanDataRepo.getSpanDetails( - choreographer.accessToken, - request: SpanDetailsRepoReqAndRes( - userL1: choreographer.l1LangCode!, - userL2: choreographer.l2LangCode!, - enableIGC: choreographer.igcEnabled, - enableIT: choreographer.itEnabled, - span: span, - ), - ); - _cache[cacheKey] = _SpanDetailsCacheItem(data: response); - } - - try { + if (response.result != null) { choreographer.igc.igcTextData!.matches[matchIndex].match = - (await response).span; - } catch (err, s) { - ErrorHandler.logError(e: err, s: s, data: req.toJson()); - _cache.remove(cacheKey); + response.result!.span; } choreographer.setState(); diff --git a/lib/pangea/choreographer/models/it_step.dart b/lib/pangea/choreographer/models/it_step.dart index 93011d898..5e3ab7ede 100644 --- a/lib/pangea/choreographer/models/it_step.dart +++ b/lib/pangea/choreographer/models/it_step.dart @@ -165,4 +165,31 @@ class Continuance { return null; } } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Continuance && + runtimeType == other.runtimeType && + probability == other.probability && + level == other.level && + text == other.text && + description == other.description && + indexSavedByServer == other.indexSavedByServer && + wasClicked == other.wasClicked && + inDictionary == other.inDictionary && + hasInfo == other.hasInfo && + gold == other.gold; + + @override + int get hashCode => + probability.hashCode ^ + level.hashCode ^ + text.hashCode ^ + description.hashCode ^ + indexSavedByServer.hashCode ^ + wasClicked.hashCode ^ + inDictionary.hashCode ^ + hasInfo.hashCode ^ + gold.hashCode; } diff --git a/lib/pangea/choreographer/repo/contextual_definition_request_model.dart b/lib/pangea/choreographer/repo/contextual_definition_request_model.dart index 46e6f064a..71376d9e7 100644 --- a/lib/pangea/choreographer/repo/contextual_definition_request_model.dart +++ b/lib/pangea/choreographer/repo/contextual_definition_request_model.dart @@ -7,7 +7,7 @@ class ContextualDefinitionRequestModel { final String fullTextLang; final String wordLang; - ContextualDefinitionRequestModel({ + const ContextualDefinitionRequestModel({ required this.fullText, required this.word, required this.feedbackLang, diff --git a/lib/pangea/choreographer/repo/contextual_definition_response_model.dart b/lib/pangea/choreographer/repo/contextual_definition_response_model.dart index 424955d35..7b3cd613a 100644 --- a/lib/pangea/choreographer/repo/contextual_definition_response_model.dart +++ b/lib/pangea/choreographer/repo/contextual_definition_response_model.dart @@ -1,7 +1,7 @@ class ContextualDefinitionResponseModel { - String text; + final String text; - ContextualDefinitionResponseModel({required this.text}); + const ContextualDefinitionResponseModel({required this.text}); factory ContextualDefinitionResponseModel.fromJson( Map json, diff --git a/lib/pangea/choreographer/repo/custom_input_request_model.dart b/lib/pangea/choreographer/repo/custom_input_request_model.dart index 03355af1b..2ef92400e 100644 --- a/lib/pangea/choreographer/repo/custom_input_request_model.dart +++ b/lib/pangea/choreographer/repo/custom_input_request_model.dart @@ -1,18 +1,20 @@ +import 'package:flutter/foundation.dart'; + import 'package:fluffychat/pangea/choreographer/models/it_step.dart'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; class CustomInputRequestModel { - String text; - String customInput; - String sourceLangCode; - String targetLangCode; - String userId; - String roomId; + final String text; + final String customInput; + final String sourceLangCode; + final String targetLangCode; + final String userId; + final String roomId; - String? goldTranslation; - List? goldContinuances; + final String? goldTranslation; + final List? goldContinuances; - CustomInputRequestModel({ + const CustomInputRequestModel({ required this.text, required this.customInput, required this.sourceLangCode, @@ -38,7 +40,7 @@ class CustomInputRequestModel { : null, ); - toJson() => { + Map toJson() => { 'text': text, 'custom_input': customInput, ModelKey.srcLang: sourceLangCode, @@ -50,4 +52,30 @@ class CustomInputRequestModel { ? List.from(goldContinuances!.map((e) => e.toJson())) : null, }; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CustomInputRequestModel && + other.text == text && + other.customInput == customInput && + other.sourceLangCode == sourceLangCode && + other.targetLangCode == targetLangCode && + other.userId == userId && + other.roomId == roomId && + other.goldTranslation == goldTranslation && + listEquals(other.goldContinuances, goldContinuances); + } + + @override + int get hashCode => + text.hashCode ^ + customInput.hashCode ^ + sourceLangCode.hashCode ^ + targetLangCode.hashCode ^ + userId.hashCode ^ + roomId.hashCode ^ + goldTranslation.hashCode ^ + Object.hashAll(goldContinuances ?? []); } diff --git a/lib/pangea/choreographer/repo/full_text_translation_repo.dart b/lib/pangea/choreographer/repo/full_text_translation_repo.dart index ca8c5ff10..5ad4714d4 100644 --- a/lib/pangea/choreographer/repo/full_text_translation_repo.dart +++ b/lib/pangea/choreographer/repo/full_text_translation_repo.dart @@ -17,7 +17,7 @@ class _TranslateCacheItem { final Future response; final DateTime timestamp; - _TranslateCacheItem({ + const _TranslateCacheItem({ required this.response, required this.timestamp, }); @@ -87,17 +87,14 @@ class FullTextTranslationRepo { static Future? _getCached( FullTextTranslationRequestModel request, ) { - final cached = _cache[request.hashCode.toString()]; - if (cached == null) { - return null; + final cacheKeys = [..._cache.keys]; + for (final key in cacheKeys) { + if (DateTime.now().difference(_cache[key]!.timestamp) >= _cacheDuration) { + _cache.remove(key); + } } - if (DateTime.now().difference(cached.timestamp) < _cacheDuration) { - return cached.response; - } - - _cache.remove(request.hashCode.toString()); - return null; + return _cache[request.hashCode.toString()]?.response; } static void _setCached( diff --git a/lib/pangea/choreographer/repo/full_text_translation_request_model.dart b/lib/pangea/choreographer/repo/full_text_translation_request_model.dart index 39e046e71..dd5c4cf0e 100644 --- a/lib/pangea/choreographer/repo/full_text_translation_request_model.dart +++ b/lib/pangea/choreographer/repo/full_text_translation_request_model.dart @@ -1,16 +1,16 @@ import 'package:fluffychat/pangea/common/constants/model_keys.dart'; class FullTextTranslationRequestModel { - String text; - String? srcLang; - String tgtLang; - String userL1; - String userL2; - bool? deepL; - int? offset; - int? length; + final String text; + final String? srcLang; + final String tgtLang; + final String userL1; + final String userL2; + final bool? deepL; + final int? offset; + final int? length; - FullTextTranslationRequestModel({ + const FullTextTranslationRequestModel({ required this.text, this.srcLang, required this.tgtLang, @@ -21,8 +21,6 @@ class FullTextTranslationRequestModel { this.length, }); - //PTODO throw error for null - Map toJson() => { "text": text, ModelKey.srcLang: srcLang, diff --git a/lib/pangea/choreographer/repo/full_text_translation_response_model.dart b/lib/pangea/choreographer/repo/full_text_translation_response_model.dart index 15e791101..ebba1fea1 100644 --- a/lib/pangea/choreographer/repo/full_text_translation_response_model.dart +++ b/lib/pangea/choreographer/repo/full_text_translation_response_model.dart @@ -1,14 +1,11 @@ import 'package:fluffychat/pangea/common/constants/model_keys.dart'; class FullTextTranslationResponseModel { - List translations; + final List translations; + final String source; + final String? deepL; - /// detected source - /// PTODO - - String source; - String? deepL; - - FullTextTranslationResponseModel({ + const FullTextTranslationResponseModel({ required this.translations, required this.source, required this.deepL, diff --git a/lib/pangea/choreographer/repo/igc_repo.dart b/lib/pangea/choreographer/repo/igc_repo.dart index 99e16c067..862505ca9 100644 --- a/lib/pangea/choreographer/repo/igc_repo.dart +++ b/lib/pangea/choreographer/repo/igc_repo.dart @@ -17,7 +17,7 @@ class _IgcCacheItem { final Future data; final DateTime timestamp; - _IgcCacheItem({ + const _IgcCacheItem({ required this.data, required this.timestamp, }); @@ -115,17 +115,16 @@ class IgcRepo { static Future? _getCached( IGCRequestModel request, ) { - final cached = _igcCache[request.hashCode.toString()]; - if (cached == null) { - return null; + final cacheKeys = [..._igcCache.keys]; + for (final key in cacheKeys) { + if (_igcCache[key]! + .timestamp + .isBefore(DateTime.now().subtract(_cacheDuration))) { + _igcCache.remove(key); + } } - if (DateTime.now().difference(cached.timestamp) < _cacheDuration) { - return cached.data; - } - - _igcCache.remove(request.hashCode.toString()); - return null; + return _igcCache[request.hashCode.toString()]?.data; } static void _setCached( @@ -149,22 +148,19 @@ class IgcRepo { static PangeaMatch? _getCachedIgnoredSpan( PangeaMatch match, ) { + final cacheKeys = [..._ignoredMatchCache.keys]; + for (final key in cacheKeys) { + final entry = _ignoredMatchCache[key]!; + if (DateTime.now().difference(entry.timestamp) >= _cacheDuration) { + _ignoredMatchCache.remove(key); + } + } + final cacheEntry = _IgnoredMatchCacheItem( match: match, timestamp: DateTime.now(), ); - - final cached = _ignoredMatchCache[cacheEntry.hashCode.toString()]; - if (cached == null) { - return null; - } - - if (DateTime.now().difference(cached.timestamp) < _cacheDuration) { - return cached.match; - } - - _ignoredMatchCache.remove(cacheEntry.hashCode.toString()); - return null; + return _ignoredMatchCache[cacheEntry.hashCode.toString()]?.match; } static void _setCachedIgnoredSpan( diff --git a/lib/pangea/choreographer/repo/igc_request_model.dart b/lib/pangea/choreographer/repo/igc_request_model.dart index 731b12f50..dc14b82fe 100644 --- a/lib/pangea/choreographer/repo/igc_request_model.dart +++ b/lib/pangea/choreographer/repo/igc_request_model.dart @@ -3,15 +3,15 @@ import 'dart:convert'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; class IGCRequestModel { - String fullText; - String userL1; - String userL2; - bool enableIT; - bool enableIGC; - String userId; - List prevMessages; + final String fullText; + final String userL1; + final String userL2; + final bool enableIT; + final bool enableIGC; + final String userId; + final List prevMessages; - IGCRequestModel({ + const IGCRequestModel({ required this.fullText, required this.userL1, required this.userL2, @@ -54,18 +54,17 @@ class IGCRequestModel { enableIT, enableIGC, userId, - Object.hashAll(prevMessages), ); } /// Previous text/audio message sent in chat /// Contain message content, sender, and timestamp class PreviousMessage { - String content; - String sender; - DateTime timestamp; + final String content; + final String sender; + final DateTime timestamp; - PreviousMessage({ + const PreviousMessage({ required this.content, required this.sender, required this.timestamp, diff --git a/lib/pangea/choreographer/repo/interactive_translation_repo.dart b/lib/pangea/choreographer/repo/interactive_translation_repo.dart index cc213224d..87a815deb 100644 --- a/lib/pangea/choreographer/repo/interactive_translation_repo.dart +++ b/lib/pangea/choreographer/repo/interactive_translation_repo.dart @@ -1,27 +1,98 @@ import 'dart:convert'; +import 'package:async/async.dart'; import 'package:http/http.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../common/network/requests.dart'; import '../../common/network/urls.dart'; import 'custom_input_request_model.dart'; import 'it_response_model.dart'; +class _ITCacheItem { + final Future response; + final DateTime timestamp; + + const _ITCacheItem({ + required this.response, + required this.timestamp, + }); +} + class ITRepo { - static Future customInputTranslate( - CustomInputRequestModel initalText, + static final Map _cache = {}; + static const Duration _cacheDuration = Duration(minutes: 10); + + static Future> get( + CustomInputRequestModel request, + ) { + final cached = _getCached(request); + if (cached != null) { + return _getResult(request, cached); + } + + final future = _fetch(request); + _setCached(request, future); + return _getResult(request, future); + } + + static Future _fetch( + CustomInputRequestModel request, ) async { final Requests req = Requests( choreoApiKey: Environment.choreoApiKey, accessToken: MatrixState.pangeaController.userController.accessToken, ); final Response res = - await req.post(url: PApiUrls.firstStep, body: initalText.toJson()); + await req.post(url: PApiUrls.firstStep, body: request.toJson()); + + if (res.statusCode != 200) { + throw Exception('Failed to load interactive translation'); + } final json = jsonDecode(utf8.decode(res.bodyBytes).toString()); - return ITResponseModel.fromJson(json); } + + static Future> _getResult( + CustomInputRequestModel request, + Future future, + ) async { + try { + final res = await future; + return Result.value(res); + } catch (e, s) { + _cache.remove(request.hashCode.toString()); + ErrorHandler.logError( + e: e, + s: s, + data: request.toJson(), + ); + return Result.error(e); + } + } + + static Future? _getCached( + CustomInputRequestModel request, + ) { + final cacheKeys = [..._cache.keys]; + for (final key in cacheKeys) { + if (DateTime.now().difference(_cache[key]!.timestamp) >= _cacheDuration) { + _cache.remove(key); + } + } + return _cache[request.hashCode.toString()]?.response; + } + + static void _setCached( + CustomInputRequestModel request, + Future response, + ) { + _cache[request.hashCode.toString()] = _ITCacheItem( + response: response, + timestamp: DateTime.now(), + ); + } } diff --git a/lib/pangea/choreographer/repo/it_response_model.dart b/lib/pangea/choreographer/repo/it_response_model.dart index dd0a14939..b507bef60 100644 --- a/lib/pangea/choreographer/repo/it_response_model.dart +++ b/lib/pangea/choreographer/repo/it_response_model.dart @@ -6,14 +6,14 @@ import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart' import 'package:fluffychat/pangea/choreographer/models/it_step.dart'; class ITResponseModel { - String fullTextTranslation; - List continuances; - List? goldContinuances; - bool isFinal; - String? translationId; - int payloadId; + final String fullTextTranslation; + final List continuances; + final List? goldContinuances; + final bool isFinal; + final String? translationId; + final int payloadId; - ITResponseModel({ + const ITResponseModel({ required this.fullTextTranslation, required this.continuances, required this.translationId, diff --git a/lib/pangea/choreographer/repo/span_data_repo.dart b/lib/pangea/choreographer/repo/span_data_repo.dart index 3789cd6d2..63322d503 100644 --- a/lib/pangea/choreographer/repo/span_data_repo.dart +++ b/lib/pangea/choreographer/repo/span_data_repo.dart @@ -1,16 +1,48 @@ import 'dart:convert'; +import 'package:async/async.dart'; import 'package:collection/collection.dart'; import 'package:http/http.dart'; import 'package:fluffychat/pangea/choreographer/models/span_data.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import '../../common/constants/model_keys.dart'; import '../../common/network/requests.dart'; import '../../common/network/urls.dart'; +class _SpanDetailsCacheItem { + final Future data; + final DateTime timestamp; + + const _SpanDetailsCacheItem({ + required this.data, + required this.timestamp, + }); +} + class SpanDataRepo { - static Future getSpanDetails( + static final Map _cache = {}; + static const Duration _cacheDuration = Duration(minutes: 10); + + static Future> get( + String? accessToken, { + required SpanDetailsRepoReqAndRes request, + }) async { + final cached = _getCached(request); + if (cached != null) { + return _getResult(request, cached); + } + + final future = _fetch( + accessToken, + request: request, + ); + _setCached(request, future); + return _getResult(request, future); + } + + static Future _fetch( String? accessToken, { required SpanDetailsRepoReqAndRes request, }) async { @@ -28,6 +60,46 @@ class SpanDataRepo { return SpanDetailsRepoReqAndRes.fromJson(json); } + + static Future> _getResult( + SpanDetailsRepoReqAndRes request, + Future future, + ) async { + try { + final res = await future; + return Result.value(res); + } catch (e, s) { + _cache.remove(request.hashCode.toString()); + ErrorHandler.logError( + e: e, + s: s, + data: request.toJson(), + ); + return Result.error(e); + } + } + + static Future? _getCached( + SpanDetailsRepoReqAndRes request, + ) { + final cacheKeys = [..._cache.keys]; + for (final key in cacheKeys) { + if (DateTime.now().difference(_cache[key]!.timestamp) >= _cacheDuration) { + _cache.remove(key); + } + } + return _cache[request.hashCode.toString()]?.data; + } + + static void _setCached( + SpanDetailsRepoReqAndRes request, + Future response, + ) { + _cache[request.hashCode.toString()] = _SpanDetailsCacheItem( + data: response, + timestamp: DateTime.now(), + ); + } } class SpanDetailsRepoReqAndRes {