@@ -29,62 +29,123 @@ class DownloadCountsBackend {
2929 final DatastoreDB _db;
3030
3131 late CachedValue <Map <String , int >> _thirtyDaysTotals;
32- var _lastData = (data: < String , int > {}, etag: '' );
32+ var _lastDownloadsData = (data: < String , int > {}, etag: '' );
33+
34+ late CachedValue <Map <String , int >> _trendScores;
35+ var _lastTrendData = (data: < String , int > {}, etag: '' );
3336
3437 DownloadCountsBackend (this ._db) {
3538 _thirtyDaysTotals = CachedValue (
3639 name: 'thirtyDaysTotalDownloadCounts' ,
3740 maxAge: Duration (days: 14 ),
3841 interval: Duration (minutes: 30 ),
3942 updateFn: _updateThirtyDaysTotals);
43+ _trendScores = CachedValue (
44+ name: 'trendScores' ,
45+ maxAge: Duration (days: 14 ),
46+ interval: Duration (minutes: 30 ),
47+ updateFn: _updateTrendScores);
4048 }
4149
4250 Future <Map <String , int >> _updateThirtyDaysTotals () async {
51+ return _fetchAndUpdateCachedData (
52+ fileName: downloadCounts30DaysTotalsFileName,
53+ currentCachedData: _lastDownloadsData,
54+ updateCache: (data) => _lastDownloadsData = data,
55+ errorContext: '30-days total download counts' );
56+ }
57+
58+ Future <Map <String , int >> _updateTrendScores () async {
59+ return _fetchAndUpdateCachedData (
60+ fileName: trendScoreFileName,
61+ currentCachedData: _lastTrendData,
62+ updateCache: (data) => _lastTrendData = data,
63+ errorContext: 'trend scores' );
64+ }
65+
66+ Future <Map <String , int >> _fetchAndUpdateCachedData ({
67+ required String fileName,
68+ required ({Map <String , int > data, String etag}) currentCachedData,
69+ required void Function (({Map <String , int > data, String etag}) newData)
70+ updateCache,
71+ required String errorContext,
72+ }) async {
4373 try {
4474 final info = await storageService
4575 .bucket (activeConfiguration.reportsBucketName! )
46- .infoWithRetry (downloadCounts30DaysTotalsFileName );
76+ .infoWithRetry (fileName );
4777
48- if (_lastData .etag == info.etag) {
49- return _lastData .data;
78+ if (currentCachedData .etag == info.etag) {
79+ return currentCachedData .data;
5080 }
51- final data = (await storageService
52- .bucket (activeConfiguration.reportsBucketName! )
53- .readWithRetry (
54- downloadCounts30DaysTotalsFileName,
55- (input) async => await input
56- .transform (utf8.decoder)
57- .transform (json.decoder)
58- .single as Map <String , dynamic >,
59- ))
60- .cast <String , int >();
61- _lastData = (data: data, etag: info.etag);
81+
82+ final rawData = await storageService
83+ .bucket (activeConfiguration.reportsBucketName! )
84+ .readWithRetry (
85+ fileName,
86+ (input) async => await input
87+ .transform (utf8.decoder)
88+ .transform (json.decoder)
89+ .single,
90+ );
91+
92+ final data = _parseJsonToMapStringInt (rawData, fileName);
93+
94+ final newData = (data: data, etag: info.etag);
95+ updateCache (newData);
6296 return data;
6397 } on FormatException catch (e, st) {
64- logger.severe ('Error loading 30-days total download counts: ' , e, st);
98+ logger.severe ('Error parsing $ errorContext : $ e ' , e, st);
6599 rethrow ;
66100 } on DetailedApiRequestError catch (e, st) {
67101 if (e.status != 404 ) {
68102 logger.severe (
69- 'Failed to load $downloadCounts30DaysTotalsFileName , error : ' ,
70- e,
71- st);
103+ 'Failed to load $fileName ($errorContext ), error : $e ' , e, st);
72104 }
73105 rethrow ;
106+ } on TypeError catch (e, st) {
107+ logger.severe ('Type error during processing $errorContext : $e ' , e, st);
108+ rethrow ;
74109 }
75110 }
76111
112+ Map <String , int > _parseJsonToMapStringInt (dynamic rawJson, String fileName) {
113+ if (rawJson is ! Map ) {
114+ throw FormatException (
115+ 'Expected JSON for $fileName to be a Map, but got ${rawJson .runtimeType }' );
116+ }
117+
118+ final Map <String , int > result = {};
119+ for (final entry in rawJson.entries) {
120+ if (entry.key is ! String ) {
121+ throw FormatException (
122+ 'Expected map keys for $fileName to be String, but found ${entry .key .runtimeType }' );
123+ }
124+ if (entry.value is ! int ) {
125+ throw FormatException (
126+ 'Expected map value for key "${entry .key }" in $fileName to be int, but got ${entry .value .runtimeType }' );
127+ }
128+ result[entry.key as String ] = entry.value as int ;
129+ }
130+ return result;
131+ }
132+
77133 Future <void > start () async {
78134 await _thirtyDaysTotals.update ();
135+ await _trendScores.update ();
79136 }
80137
81138 Future <void > close () async {
82139 await _thirtyDaysTotals.close ();
140+ await _trendScores.close ();
83141 }
84142
85143 int ? lookup30DaysTotalCounts (String package) =>
86144 _thirtyDaysTotals.isAvailable ? _thirtyDaysTotals.value! [package] : null ;
87145
146+ int ? lookupTrendScore (String package) =>
147+ _trendScores.isAvailable ? _trendScores.value! [package] : null ;
148+
88149 Future <CountData ?> lookupDownloadCountData (String pkg) async {
89150 return (await cache.downloadCounts (pkg).get (() async {
90151 final key = _db.emptyKey.append (DownloadCounts , id: pkg);
0 commit comments