@@ -2,13 +2,30 @@ import 'package:core/core.dart';
22import 'package:data_repository/data_repository.dart' ;
33import 'package:logging/logging.dart' ;
44
5+ /// {@template _cache_entry}
6+ /// A simple class to hold cached data along with its expiration time.
7+ /// {@endtemplate}
8+ class _CacheEntry <T > {
9+ /// {@macro _cache_entry}
10+ const _CacheEntry (this .data, this .expiry);
11+
12+ /// The cached data.
13+ final T data;
14+
15+ /// The time at which the cached data expires.
16+ final DateTime expiry;
17+
18+ /// Checks if the cache entry is still valid (not expired).
19+ bool isValid () => DateTime .now ().isBefore (expiry);
20+ }
21+
522/// {@template country_service}
623/// A service responsible for retrieving country data, including specialized
724/// lists like countries associated with headlines or sources.
825///
926/// This service leverages database aggregation for efficient data retrieval
10- /// and includes basic in-memory caching to optimize performance for frequently
11- /// requested lists.
27+ /// and includes time-based in-memory caching to optimize performance for
28+ /// frequently requested lists.
1229/// {@endtemplate}
1330class CountryService {
1431 /// {@macro country_service}
@@ -27,11 +44,12 @@ class CountryService {
2744 final DataRepository <Source > _sourceRepository;
2845 final Logger _log;
2946
30- // In-memory caches for frequently accessed lists.
31- // These should be cleared periodically in a real-world application
32- // or invalidated upon data changes. For this scope, simple caching is used.
33- List <Country >? _cachedEventCountries;
34- List <Country >? _cachedHeadquarterCountries;
47+ // Cache duration for aggregated country lists (e.g., 1 hour).
48+ static const Duration _cacheDuration = Duration (hours: 1 );
49+
50+ // In-memory caches for frequently accessed lists with time-based invalidation.
51+ _CacheEntry <List <Country >>? _cachedEventCountries;
52+ _CacheEntry <List <Country >>? _cachedHeadquarterCountries;
3553
3654 /// Retrieves a list of countries based on the provided filter.
3755 ///
@@ -91,49 +109,26 @@ class CountryService {
91109 /// Uses MongoDB aggregation to efficiently get distinct country IDs
92110 /// and then fetches the full Country objects. Results are cached.
93111 Future <List <Country >> _getEventCountries () async {
94- if (_cachedEventCountries != null ) {
112+ if (_cachedEventCountries != null && _cachedEventCountries ! . isValid () ) {
95113 _log.finer ('Returning cached event countries.' );
96- return _cachedEventCountries! ;
114+ return _cachedEventCountries! .data ;
97115 }
98116
99117 _log.finer ('Fetching distinct event countries via aggregation.' );
100- try {
101- final pipeline = [
102- {
103- r'$match' : {
104- 'status' : ContentStatus .active.name,
105- 'eventCountry.id' : {r'$exists' : true },
106- },
107- },
108- {
109- r'$group' : {
110- '_id' : r'$eventCountry.id' ,
111- 'country' : {r'$first' : r'$eventCountry' },
112- },
113- },
114- {
115- r'$replaceRoot' : {'newRoot' : r'$country' },
116- },
117- ];
118-
119- final distinctCountriesJson = await _headlineRepository.aggregate (
120- pipeline: pipeline,
121- );
122-
123- final distinctCountries = distinctCountriesJson
124- .map (Country .fromJson)
125- .toList ();
126-
127- _cachedEventCountries = distinctCountries;
128- _log.info (
129- 'Successfully fetched and cached ${distinctCountries .length } '
130- 'event countries.' ,
131- );
132- return distinctCountries;
133- } catch (e, s) {
134- _log.severe ('Failed to fetch event countries via aggregation.' , e, s);
135- throw OperationFailedException ('Failed to retrieve event countries: $e ' );
136- }
118+ final distinctCountries = await _getDistinctCountriesFromAggregation (
119+ repository: _headlineRepository,
120+ fieldName: 'eventCountry' ,
121+ );
122+
123+ _cachedEventCountries = _CacheEntry (
124+ distinctCountries,
125+ DateTime .now ().add (_cacheDuration),
126+ );
127+ _log.info (
128+ 'Successfully fetched and cached ${distinctCountries .length } '
129+ 'event countries.' ,
130+ );
131+ return distinctCountries;
137132 }
138133
139134 /// Fetches a distinct list of countries that are referenced as
@@ -142,53 +137,82 @@ class CountryService {
142137 /// Uses MongoDB aggregation to efficiently get distinct country IDs
143138 /// and then fetches the full Country objects. Results are cached.
144139 Future <List <Country >> _getHeadquarterCountries () async {
145- if (_cachedHeadquarterCountries != null ) {
140+ if (_cachedHeadquarterCountries != null &&
141+ _cachedHeadquarterCountries! .isValid ()) {
146142 _log.finer ('Returning cached headquarter countries.' );
147- return _cachedHeadquarterCountries! ;
143+ return _cachedHeadquarterCountries! .data ;
148144 }
149145
150146 _log.finer ('Fetching distinct headquarter countries via aggregation.' );
147+ final distinctCountries = await _getDistinctCountriesFromAggregation (
148+ repository: _sourceRepository,
149+ fieldName: 'headquarters' ,
150+ );
151+
152+ _cachedHeadquarterCountries = _CacheEntry (
153+ distinctCountries,
154+ DateTime .now ().add (_cacheDuration),
155+ );
156+ _log.info (
157+ 'Successfully fetched and cached ${distinctCountries .length } '
158+ 'headquarter countries.' ,
159+ );
160+ return distinctCountries;
161+ }
162+
163+ /// Helper method to fetch a distinct list of countries from a given
164+ /// repository and field name using MongoDB aggregation.
165+ ///
166+ /// - [repository] : The [DataRepository] to perform the aggregation on.
167+ /// - [fieldName] : The name of the field within the documents that contains
168+ /// the country object (e.g., 'eventCountry', 'headquarters').
169+ ///
170+ /// Throws [OperationFailedException] for internal errors during data fetch.
171+ Future <List <Country >> _getDistinctCountriesFromAggregation ({
172+ required DataRepository <dynamic > repository,
173+ required String fieldName,
174+ }) async {
175+ _log.finer ('Fetching distinct countries for field "$fieldName " via aggregation.' );
151176 try {
152177 final pipeline = [
153178 {
154179 r'$match' : {
155180 'status' : ContentStatus .active.name,
156- 'headquarters .id' : {r'$exists' : true },
181+ '$ fieldName .id' : {r'$exists' : true },
157182 },
158183 },
159184 {
160185 r'$group' : {
161- '_id' : r'$headquarters .id' ,
162- 'country' : {r'$first' : r'$headquarters ' },
186+ '_id' : ' \$ $ fieldName .id' ,
187+ 'country' : {r'$first' : ' \$ $ fieldName ' },
163188 },
164189 },
165190 {
166191 r'$replaceRoot' : {'newRoot' : r'$country' },
167192 },
168193 ];
169194
170- final distinctCountriesJson = await _sourceRepository .aggregate (
195+ final distinctCountriesJson = await repository .aggregate (
171196 pipeline: pipeline,
172197 );
173198
174199 final distinctCountries = distinctCountriesJson
175200 .map (Country .fromJson)
176201 .toList ();
177202
178- _cachedHeadquarterCountries = distinctCountries;
179203 _log.info (
180- 'Successfully fetched and cached ${distinctCountries .length } '
181- 'headquarter countries .' ,
204+ 'Successfully fetched ${distinctCountries .length } distinct countries '
205+ 'for field "$ fieldName " .' ,
182206 );
183207 return distinctCountries;
184208 } catch (e, s) {
185209 _log.severe (
186- 'Failed to fetch headquarter countries via aggregation .' ,
210+ 'Failed to fetch distinct countries for field "$ fieldName " .' ,
187211 e,
188212 s,
189213 );
190214 throw OperationFailedException (
191- 'Failed to retrieve headquarter countries: $e ' ,
215+ 'Failed to retrieve distinct countries for field "$ fieldName " : $e ' ,
192216 );
193217 }
194218 }
0 commit comments