TF-4265 Refactor: Move Regex pattern generation to Main Thread to leverage static caching and reduce Isolate initialization overhead.

This commit is contained in:
dab246
2026-01-30 15:36:04 +07:00
committed by Dat H. Pham
parent 55e27c78ed
commit 74c0c75023
5 changed files with 32 additions and 29 deletions
@@ -5,11 +5,11 @@
* When a user composes an email, the system detects if the content implies an attachment (e.g., "Please find attached...") but no file is attached. * When a user composes an email, the system detects if the content implies an attachment (e.g., "Please find attached...") but no file is attached.
* We need a flexible way to add specific keywords (Include) or ignore specific tokens (Exclude) without modifying the source code. * We need a flexible way to add specific keywords (Include) or ignore specific tokens (Exclude) without modifying the source code.
### How to config ### How to configure
1. Configuration File Location 1. Configuration File Location
* The configuration is managed in the JSON file: `configurations\attachment_keywords.json` * The configuration is managed in the JSON file: `configurations/attachment_keywords.json`
* This file must be declared in `pubspec.yaml` under `assets`. * This file must be declared in `pubspec.yaml` under `assets`.
2. JSON Structure 2. JSON Structure
@@ -882,10 +882,7 @@ class ComposerController extends BaseController
} }
final emailContent = await getContentInEditor(); final emailContent = await getContentInEditor();
if (!context.mounted) {
_sendButtonState = ButtonState.enabled;
return;
}
final attachmentKeywords = await validateAttachmentReminder( final attachmentKeywords = await validateAttachmentReminder(
emailSubject: subjectEmail.value ?? '', emailSubject: subjectEmail.value ?? '',
emailContent: emailContent, emailContent: emailContent,
@@ -893,6 +890,7 @@ class ComposerController extends BaseController
if (!context.mounted) { if (!context.mounted) {
logWarning('ComposerController::_prepareToSendMessages: CONTEXT IS NOT MOUNTED'); logWarning('ComposerController::_prepareToSendMessages: CONTEXT IS NOT MOUNTED');
_sendButtonState = ButtonState.enabled;
return; return;
} }
@@ -46,13 +46,12 @@ class AttachmentTextDetector {
"مستند", "مستند",
"ملف", "ملف",
"تقرير", "تقرير",
"ملف",
"إرفاق", "إرفاق",
], ],
}; };
/// Cached RegExp for the default case (when no include list is provided). /// Cached RegExp Pattern for the default case (when no include list is provided).
static RegExp? _cachedDefaultRegExp; static String? _cachedDefaultPattern;
/// Threshold to switch from Sync to Async execution. /// Threshold to switch from Sync to Async execution.
/// ///
/// Value: 20,000 characters (approx. 4-5 pages of text). /// Value: 20,000 characters (approx. 4-5 pages of text).
@@ -118,7 +117,7 @@ class AttachmentTextDetector {
/// Builds the Regex pattern. /// Builds the Regex pattern.
/// Merges [defaultKeywords] with [includeList] to create the search scope. /// Merges [defaultKeywords] with [includeList] to create the search scope.
static RegExp _buildRegExp(List<String> additionalKeywords) { static String _generatePatternString(List<String> additionalKeywords) {
// 1. Get all default keywords // 1. Get all default keywords
final defaultKeywords = _keywordsByLang.values.expand((l) => l).toList(); final defaultKeywords = _keywordsByLang.values.expand((l) => l).toList();
@@ -133,11 +132,16 @@ class AttachmentTextDetector {
// (?![\p{L}]) ensures we don't match substrings inside other words (e.g., "filetage"). // (?![\p{L}]) ensures we don't match substrings inside other words (e.g., "filetage").
final pattern = allKeywords.map(RegExp.escape).join('|'); final pattern = allKeywords.map(RegExp.escape).join('|');
return RegExp( return'($pattern)(?![\\p{L}])';
'($pattern)(?![\\p{L}])', }
unicode: true,
caseSensitive: false, static String _getPattern(List<String> includeList) {
); if (includeList.isNotEmpty) {
return _generatePatternString(includeList);
}
_cachedDefaultPattern ??= _generatePatternString([]);
return _cachedDefaultPattern!;
} }
/// The core logic function. /// The core logic function.
@@ -148,16 +152,11 @@ class AttachmentTextDetector {
final text = params.text; final text = params.text;
if (text.isEmpty) return []; if (text.isEmpty) return [];
RegExp regex; final regex = RegExp(
params.regexPattern,
// Use cached Regex if there are no custom keywords. unicode: true,
if (params.includeList.isEmpty) { caseSensitive: false,
_cachedDefaultRegExp ??= _buildRegExp([]); );
regex = _cachedDefaultRegExp!;
} else {
// Rebuild Regex if Include List is present.
regex = _buildRegExp(params.includeList);
}
final matches = regex.allMatches(text); final matches = regex.allMatches(text);
final result = <String>{}; final result = <String>{};
@@ -199,10 +198,12 @@ class AttachmentTextDetector {
filters.add(ExcludeListFilter(excludeList)); filters.add(ExcludeListFilter(excludeList));
} }
final patternString = _getPattern(includeList);
final params = DetectionParams( final params = DetectionParams(
text: text, text: text,
includeList: includeList, // Passed to build the Regex regexPattern: patternString,
filters: filters, // Passed to filter the matches filters: filters,
); );
// Decision: Run Sync (fast) or Async (safe) // Decision: Run Sync (fast) or Async (safe)
@@ -4,12 +4,12 @@ import 'package:tmail_ui_user/features/composer/presentation/manager/keyword_fil
/// Isolates cannot share memory, so we package everything here. /// Isolates cannot share memory, so we package everything here.
class DetectionParams { class DetectionParams {
final String text; final String text;
final List<String> includeList; // Keywords to ADD to the search. final String regexPattern;
final List<KeywordFilter> filters; // Filters to BLOCK specific matches. final List<KeywordFilter> filters; // Filters to BLOCK specific matches.
DetectionParams({ DetectionParams({
required this.text, required this.text,
required this.includeList, required this.regexPattern,
required this.filters, required this.filters,
}); });
} }
@@ -23,6 +23,10 @@ class ExcludeListFilter with TokenExtractionMixin implements KeywordFilter {
final cleanToken = lowerToken.replaceAll(RegExp(r'[^\w\s]+$'), ''); final cleanToken = lowerToken.replaceAll(RegExp(r'[^\w\s]+$'), '');
if (_excludes.contains(cleanToken)) return false; if (_excludes.contains(cleanToken)) return false;
// Check 3: Block even if it has leading punctuation (e.g., "(file")
final fullyCleanToken = cleanToken.replaceAll(RegExp(r'^[^\w\s]+'), '');
if (_excludes.contains(fullyCleanToken)) return false;
return true; return true;
} }
} }