@@ -29,62 +29,123 @@ class DownloadCountsBackend {
29
29
final DatastoreDB _db;
30
30
31
31
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: '' );
33
36
34
37
DownloadCountsBackend (this ._db) {
35
38
_thirtyDaysTotals = CachedValue (
36
39
name: 'thirtyDaysTotalDownloadCounts' ,
37
40
maxAge: Duration (days: 14 ),
38
41
interval: Duration (minutes: 30 ),
39
42
updateFn: _updateThirtyDaysTotals);
43
+ _trendScores = CachedValue (
44
+ name: 'trendScores' ,
45
+ maxAge: Duration (days: 14 ),
46
+ interval: Duration (minutes: 30 ),
47
+ updateFn: _updateTrendScores);
40
48
}
41
49
42
50
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 {
43
73
try {
44
74
final info = await storageService
45
75
.bucket (activeConfiguration.reportsBucketName! )
46
- .infoWithRetry (downloadCounts30DaysTotalsFileName );
76
+ .infoWithRetry (fileName );
47
77
48
- if (_lastData .etag == info.etag) {
49
- return _lastData .data;
78
+ if (currentCachedData .etag == info.etag) {
79
+ return currentCachedData .data;
50
80
}
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);
62
96
return data;
63
97
} 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);
65
99
rethrow ;
66
100
} on DetailedApiRequestError catch (e, st) {
67
101
if (e.status != 404 ) {
68
102
logger.severe (
69
- 'Failed to load $downloadCounts30DaysTotalsFileName , error : ' ,
70
- e,
71
- st);
103
+ 'Failed to load $fileName ($errorContext ), error : $e ' , e, st);
72
104
}
73
105
rethrow ;
106
+ } on TypeError catch (e, st) {
107
+ logger.severe ('Type error during processing $errorContext : $e ' , e, st);
108
+ rethrow ;
74
109
}
75
110
}
76
111
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
+
77
133
Future <void > start () async {
78
134
await _thirtyDaysTotals.update ();
135
+ await _trendScores.update ();
79
136
}
80
137
81
138
Future <void > close () async {
82
139
await _thirtyDaysTotals.close ();
140
+ await _trendScores.close ();
83
141
}
84
142
85
143
int ? lookup30DaysTotalCounts (String package) =>
86
144
_thirtyDaysTotals.isAvailable ? _thirtyDaysTotals.value! [package] : null ;
87
145
146
+ int ? lookupTrendScore (String package) =>
147
+ _trendScores.isAvailable ? _trendScores.value! [package] : null ;
148
+
88
149
Future <CountData ?> lookupDownloadCountData (String pkg) async {
89
150
return (await cache.downloadCounts (pkg).get (() async {
90
151
final key = _db.emptyKey.append (DownloadCounts , id: pkg);
0 commit comments