updates to cache clearing, make instance variables in request/response models final

This commit is contained in:
ggurdin 2025-10-27 10:34:05 -04:00
parent e83f153124
commit d9ada39c2c
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
15 changed files with 362 additions and 249 deletions

View file

@ -202,8 +202,6 @@ class IgcController {
clear() {
igcTextData = null;
spanDataController.clearCache();
spanDataController.dispose();
MatrixState.pAnyState.closeAllOverlays(
filter: RegExp(r'span_card_overlay_\d+'),
);

View file

@ -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<void> 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<CurrentITStep?>();
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<CurrentITStep?>();
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<void> 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<ITResponseModel> _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) {

View file

@ -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<SpanDetailsRepoReqAndRes> data;
_SpanDetailsCacheItem({required this.data});
}
import 'package:fluffychat/widgets/future_loading_dialog.dart';
class SpanDataController {
late Choreographer choreographer;
final Map<int, _SpanDetailsCacheItem> _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<SpanDetailsRepoReqAndRes> 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();

View file

@ -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;
}

View file

@ -7,7 +7,7 @@ class ContextualDefinitionRequestModel {
final String fullTextLang;
final String wordLang;
ContextualDefinitionRequestModel({
const ContextualDefinitionRequestModel({
required this.fullText,
required this.word,
required this.feedbackLang,

View file

@ -1,7 +1,7 @@
class ContextualDefinitionResponseModel {
String text;
final String text;
ContextualDefinitionResponseModel({required this.text});
const ContextualDefinitionResponseModel({required this.text});
factory ContextualDefinitionResponseModel.fromJson(
Map<String, dynamic> json,

View file

@ -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<Continuance>? goldContinuances;
final String? goldTranslation;
final List<Continuance>? goldContinuances;
CustomInputRequestModel({
const CustomInputRequestModel({
required this.text,
required this.customInput,
required this.sourceLangCode,
@ -38,7 +40,7 @@ class CustomInputRequestModel {
: null,
);
toJson() => {
Map<String, dynamic> 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 ?? []);
}

View file

@ -17,7 +17,7 @@ class _TranslateCacheItem {
final Future<FullTextTranslationResponseModel> response;
final DateTime timestamp;
_TranslateCacheItem({
const _TranslateCacheItem({
required this.response,
required this.timestamp,
});
@ -87,17 +87,14 @@ class FullTextTranslationRepo {
static Future<FullTextTranslationResponseModel>? _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(

View file

@ -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<String, dynamic> toJson() => {
"text": text,
ModelKey.srcLang: srcLang,

View file

@ -1,14 +1,11 @@
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
class FullTextTranslationResponseModel {
List<String> translations;
final List<String> 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,

View file

@ -17,7 +17,7 @@ class _IgcCacheItem {
final Future<IGCTextData> data;
final DateTime timestamp;
_IgcCacheItem({
const _IgcCacheItem({
required this.data,
required this.timestamp,
});
@ -115,17 +115,16 @@ class IgcRepo {
static Future<IGCTextData>? _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(

View file

@ -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<PreviousMessage> prevMessages;
final String fullText;
final String userL1;
final String userL2;
final bool enableIT;
final bool enableIGC;
final String userId;
final List<PreviousMessage> 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,

View file

@ -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<ITResponseModel> response;
final DateTime timestamp;
const _ITCacheItem({
required this.response,
required this.timestamp,
});
}
class ITRepo {
static Future<ITResponseModel> customInputTranslate(
CustomInputRequestModel initalText,
static final Map<String, _ITCacheItem> _cache = {};
static const Duration _cacheDuration = Duration(minutes: 10);
static Future<Result<ITResponseModel>> 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<ITResponseModel> _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<Result<ITResponseModel>> _getResult(
CustomInputRequestModel request,
Future<ITResponseModel> 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<ITResponseModel>? _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<ITResponseModel> response,
) {
_cache[request.hashCode.toString()] = _ITCacheItem(
response: response,
timestamp: DateTime.now(),
);
}
}

View file

@ -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<Continuance> continuances;
List<Continuance>? goldContinuances;
bool isFinal;
String? translationId;
int payloadId;
final String fullTextTranslation;
final List<Continuance> continuances;
final List<Continuance>? goldContinuances;
final bool isFinal;
final String? translationId;
final int payloadId;
ITResponseModel({
const ITResponseModel({
required this.fullTextTranslation,
required this.continuances,
required this.translationId,

View file

@ -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<SpanDetailsRepoReqAndRes> data;
final DateTime timestamp;
const _SpanDetailsCacheItem({
required this.data,
required this.timestamp,
});
}
class SpanDataRepo {
static Future<SpanDetailsRepoReqAndRes> getSpanDetails(
static final Map<String, _SpanDetailsCacheItem> _cache = {};
static const Duration _cacheDuration = Duration(minutes: 10);
static Future<Result<SpanDetailsRepoReqAndRes>> 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<SpanDetailsRepoReqAndRes> _fetch(
String? accessToken, {
required SpanDetailsRepoReqAndRes request,
}) async {
@ -28,6 +60,46 @@ class SpanDataRepo {
return SpanDetailsRepoReqAndRes.fromJson(json);
}
static Future<Result<SpanDetailsRepoReqAndRes>> _getResult(
SpanDetailsRepoReqAndRes request,
Future<SpanDetailsRepoReqAndRes> 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<SpanDetailsRepoReqAndRes>? _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<SpanDetailsRepoReqAndRes> response,
) {
_cache[request.hashCode.toString()] = _SpanDetailsCacheItem(
data: response,
timestamp: DateTime.now(),
);
}
}
class SpanDetailsRepoReqAndRes {