From f5a46f91bb67072211acf406fd2c7bd162a57b9a Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 20 Aug 2025 07:37:47 +0100 Subject: [PATCH 1/4] refactor(country_service): implement time-based caching for improved performance - Introduce _CacheEntry class to hold cached data with expiration time - Replace simple in-memory caching with time-based caching mechanism - Add cache duration constant for aggregated country lists - Update cache handling logic in _cachedEventCountries and _cachedHeadquarterCountries --- lib/src/services/country_service.dart | 32 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/src/services/country_service.dart b/lib/src/services/country_service.dart index 6488a07..86d43df 100644 --- a/lib/src/services/country_service.dart +++ b/lib/src/services/country_service.dart @@ -2,13 +2,30 @@ import 'package:core/core.dart'; import 'package:data_repository/data_repository.dart'; import 'package:logging/logging.dart'; +/// {@template _cache_entry} +/// A simple class to hold cached data along with its expiration time. +/// {@endtemplate} +class _CacheEntry { + /// {@macro _cache_entry} + const _CacheEntry(this.data, this.expiry); + + /// The cached data. + final T data; + + /// The time at which the cached data expires. + final DateTime expiry; + + /// Checks if the cache entry is still valid (not expired). + bool isValid() => DateTime.now().isBefore(expiry); +} + /// {@template country_service} /// A service responsible for retrieving country data, including specialized /// lists like countries associated with headlines or sources. /// /// This service leverages database aggregation for efficient data retrieval -/// and includes basic in-memory caching to optimize performance for frequently -/// requested lists. +/// and includes time-based in-memory caching to optimize performance for +/// frequently requested lists. /// {@endtemplate} class CountryService { /// {@macro country_service} @@ -27,11 +44,12 @@ class CountryService { final DataRepository _sourceRepository; final Logger _log; - // In-memory caches for frequently accessed lists. - // These should be cleared periodically in a real-world application - // or invalidated upon data changes. For this scope, simple caching is used. - List? _cachedEventCountries; - List? _cachedHeadquarterCountries; + // Cache duration for aggregated country lists (e.g., 1 hour). + static const Duration _cacheDuration = Duration(hours: 1); + + // In-memory caches for frequently accessed lists with time-based invalidation. + _CacheEntry>? _cachedEventCountries; + _CacheEntry>? _cachedHeadquarterCountries; /// Retrieves a list of countries based on the provided filter. /// From e4c54f5ab235964882deb6054b1883b5290f2bc9 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 20 Aug 2025 07:38:54 +0100 Subject: [PATCH 2/4] refactor(country): improve country caching mechanism - Introduce _CacheEntry class to enhance caching with expiration - Modify _getEventCountries and _getHeadquarterCountries to use new caching system - Update cache validation logic to check both existence and expiration --- lib/src/services/country_service.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/src/services/country_service.dart b/lib/src/services/country_service.dart index 86d43df..e50e758 100644 --- a/lib/src/services/country_service.dart +++ b/lib/src/services/country_service.dart @@ -109,9 +109,9 @@ class CountryService { /// Uses MongoDB aggregation to efficiently get distinct country IDs /// and then fetches the full Country objects. Results are cached. Future> _getEventCountries() async { - if (_cachedEventCountries != null) { + if (_cachedEventCountries != null && _cachedEventCountries!.isValid()) { _log.finer('Returning cached event countries.'); - return _cachedEventCountries!; + return _cachedEventCountries!.data; } _log.finer('Fetching distinct event countries via aggregation.'); @@ -142,7 +142,10 @@ class CountryService { .map(Country.fromJson) .toList(); - _cachedEventCountries = distinctCountries; + _cachedEventCountries = _CacheEntry( + distinctCountries, + DateTime.now().add(_cacheDuration), + ); _log.info( 'Successfully fetched and cached ${distinctCountries.length} ' 'event countries.', @@ -160,9 +163,10 @@ class CountryService { /// Uses MongoDB aggregation to efficiently get distinct country IDs /// and then fetches the full Country objects. Results are cached. Future> _getHeadquarterCountries() async { - if (_cachedHeadquarterCountries != null) { + if (_cachedHeadquarterCountries != null && + _cachedHeadquarterCountries!.isValid()) { _log.finer('Returning cached headquarter countries.'); - return _cachedHeadquarterCountries!; + return _cachedHeadquarterCountries!.data; } _log.finer('Fetching distinct headquarter countries via aggregation.'); @@ -193,7 +197,10 @@ class CountryService { .map(Country.fromJson) .toList(); - _cachedHeadquarterCountries = distinctCountries; + _cachedHeadquarterCountries = _CacheEntry( + distinctCountries, + DateTime.now().add(_cacheDuration), + ); _log.info( 'Successfully fetched and cached ${distinctCountries.length} ' 'headquarter countries.', From a45102df20a051c1eb1543e62983a909ef684430 Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 20 Aug 2025 07:41:03 +0100 Subject: [PATCH 3/4] refactor(country): extract distinct countries aggregation logic - Create a new private method _getDistinctCountriesFromAggregation for reuse - Modify fetchEventCountries and fetchHeadquarterCountries to use the new method - Improve code readability and maintainability by reducing duplication --- lib/src/services/country_service.dart | 103 +++++++++++++------------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/lib/src/services/country_service.dart b/lib/src/services/country_service.dart index e50e758..30481d9 100644 --- a/lib/src/services/country_service.dart +++ b/lib/src/services/country_service.dart @@ -115,46 +115,20 @@ class CountryService { } _log.finer('Fetching distinct event countries via aggregation.'); - try { - final pipeline = [ - { - r'$match': { - 'status': ContentStatus.active.name, - 'eventCountry.id': {r'$exists': true}, - }, - }, - { - r'$group': { - '_id': r'$eventCountry.id', - 'country': {r'$first': r'$eventCountry'}, - }, - }, - { - r'$replaceRoot': {'newRoot': r'$country'}, - }, - ]; - - final distinctCountriesJson = await _headlineRepository.aggregate( - pipeline: pipeline, - ); - - final distinctCountries = distinctCountriesJson - .map(Country.fromJson) - .toList(); - - _cachedEventCountries = _CacheEntry( - distinctCountries, - DateTime.now().add(_cacheDuration), - ); - _log.info( - 'Successfully fetched and cached ${distinctCountries.length} ' - 'event countries.', - ); - return distinctCountries; - } catch (e, s) { - _log.severe('Failed to fetch event countries via aggregation.', e, s); - throw OperationFailedException('Failed to retrieve event countries: $e'); - } + final distinctCountries = await _getDistinctCountriesFromAggregation( + repository: _headlineRepository, + fieldName: 'eventCountry', + ); + + _cachedEventCountries = _CacheEntry( + distinctCountries, + DateTime.now().add(_cacheDuration), + ); + _log.info( + 'Successfully fetched and cached ${distinctCountries.length} ' + 'event countries.', + ); + return distinctCountries; } /// Fetches a distinct list of countries that are referenced as @@ -170,18 +144,47 @@ class CountryService { } _log.finer('Fetching distinct headquarter countries via aggregation.'); + final distinctCountries = await _getDistinctCountriesFromAggregation( + repository: _sourceRepository, + fieldName: 'headquarters', + ); + + _cachedHeadquarterCountries = _CacheEntry( + distinctCountries, + DateTime.now().add(_cacheDuration), + ); + _log.info( + 'Successfully fetched and cached ${distinctCountries.length} ' + 'headquarter countries.', + ); + return distinctCountries; + } + + /// Helper method to fetch a distinct list of countries from a given + /// repository and field name using MongoDB aggregation. + /// + /// - [repository]: The [DataRepository] to perform the aggregation on. + /// - [fieldName]: The name of the field within the documents that contains + /// the country object (e.g., 'eventCountry', 'headquarters'). + /// + /// Throws [OperationFailedException] for internal errors during data fetch. + Future> _getDistinctCountriesFromAggregation({ + required DataRepository repository, + required String fieldName, + }) async { + _log.finer('Fetching distinct countries for field "$fieldName" via aggregation.'); try { final pipeline = [ { r'$match': { 'status': ContentStatus.active.name, - 'headquarters.id': {r'$exists': true}, + '$fieldName.id': {r'$exists': true}, }, }, { r'$group': { - '_id': r'$headquarters.id', - 'country': {r'$first': r'$headquarters'}, + '_id': '\$$fieldName.id', + 'country': {r'$first': '\$$fieldName'}, }, }, { @@ -189,7 +192,7 @@ class CountryService { }, ]; - final distinctCountriesJson = await _sourceRepository.aggregate( + final distinctCountriesJson = await repository.aggregate( pipeline: pipeline, ); @@ -197,23 +200,19 @@ class CountryService { .map(Country.fromJson) .toList(); - _cachedHeadquarterCountries = _CacheEntry( - distinctCountries, - DateTime.now().add(_cacheDuration), - ); _log.info( - 'Successfully fetched and cached ${distinctCountries.length} ' - 'headquarter countries.', + 'Successfully fetched ${distinctCountries.length} distinct countries ' + 'for field "$fieldName".', ); return distinctCountries; } catch (e, s) { _log.severe( - 'Failed to fetch headquarter countries via aggregation.', + 'Failed to fetch distinct countries for field "$fieldName".', e, s, ); throw OperationFailedException( - 'Failed to retrieve headquarter countries: $e', + 'Failed to retrieve distinct countries for field "$fieldName": $e', ); } } From 22ecbd00a8b1ecec634e2fb7b6e4b25b93ce7e1d Mon Sep 17 00:00:00 2001 From: fulleni Date: Wed, 20 Aug 2025 07:47:39 +0100 Subject: [PATCH 4/4] feat(data_operation_registry): enhance country model filtering - Add support for standard pagination/sorting for country model - Implement conditional logic to handle 'usage' filter for country model - Maintain backwards compatibility for existing specialized queries - Improve code readability and structure in data operation registry --- lib/src/registry/data_operation_registry.dart | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/src/registry/data_operation_registry.dart b/lib/src/registry/data_operation_registry.dart index d90d356..95793eb 100644 --- a/lib/src/registry/data_operation_registry.dart +++ b/lib/src/registry/data_operation_registry.dart @@ -130,17 +130,26 @@ class DataOperationRegistry { pagination: p, ), 'country': (c, uid, f, s, p) async { - // For 'country' model, delegate to CountryService for specialized filtering. - // The CountryService handles the 'usage' filter and returns a List. - // We then wrap this list in a PaginatedResponse for consistency with - // the generic API response structure. - final countryService = c.read(); - final countries = await countryService.getCountries(f); - return PaginatedResponse( - items: countries, - cursor: null, // No cursor for this type of filtered list - hasMore: false, // No more items as it's a complete filtered set - ); + final usage = f?['usage'] as String?; + if (usage != null && usage.isNotEmpty) { + // For 'country' model with 'usage' filter, delegate to CountryService. + // Sorting and pagination are not supported for this specialized query. + final countryService = c.read(); + final countries = await countryService.getCountries(f); + return PaginatedResponse( + items: countries, + cursor: null, // No cursor for this type of filtered list + hasMore: false, // No more items as it's a complete filtered set + ); + } else { + // For standard requests, use the repository which supports pagination/sorting. + return c.read>().readAll( + userId: uid, + filter: f, + sort: s, + pagination: p, + ); + } }, 'language': (c, uid, f, s, p) => c .read>()