@@ -9,6 +9,9 @@ import kotlinx.coroutines.CoroutineStart
99import kotlinx.coroutines.async
1010import kotlinx.coroutines.awaitAll
1111import kotlinx.coroutines.coroutineScope
12+ import kotlinx.serialization.Serializable
13+ import zed.rainxch.core.data.cache.CacheManager
14+ import zed.rainxch.core.data.cache.CacheTtl
1215import zed.rainxch.core.data.network.executeRequest
1316import zed.rainxch.core.data.services.LocalizationManager
1417import zed.rainxch.core.domain.model.GithubRelease
@@ -29,9 +32,17 @@ import zed.rainxch.details.domain.repository.DetailsRepository
2932class DetailsRepositoryImpl (
3033 private val httpClient : HttpClient ,
3134 private val localizationManager : LocalizationManager ,
32- private val logger : GitHubStoreLogger
35+ private val logger : GitHubStoreLogger ,
36+ private val cacheManager : CacheManager
3337) : DetailsRepository {
3438
39+ @Serializable
40+ private data class CachedReadme (
41+ val content : String ,
42+ val languageCode : String? ,
43+ val path : String
44+ )
45+
3546 private val readmeHelper = ReadmeLocalizationHelper (localizationManager)
3647
3748 private fun RepoByIdNetwork.toGithubRepoSummary (): GithubRepoSummary {
@@ -58,64 +69,132 @@ class DetailsRepositoryImpl(
5869 }
5970
6071 override suspend fun getRepositoryById (id : Long ): GithubRepoSummary {
61- return httpClient.executeRequest<RepoByIdNetwork > {
72+ val cacheKey = " details:repo_id:$id "
73+
74+ cacheManager.get<GithubRepoSummary >(cacheKey)?.let { cached ->
75+ logger.debug(" Cache hit for repo id=$id " )
76+ return cached
77+ }
78+
79+ val result = httpClient.executeRequest<RepoByIdNetwork > {
6280 get(" /repositories/$id " ) {
6381 header(HttpHeaders .Accept , " application/vnd.github+json" )
6482 }
6583 }.getOrThrow().toGithubRepoSummary()
84+
85+ cacheManager.put(cacheKey, result, CacheTtl .REPO_DETAILS )
86+ return result
6687 }
6788
6889 override suspend fun getRepositoryByOwnerAndName (owner : String , name : String ): GithubRepoSummary {
69- return httpClient.executeRequest<RepoByIdNetwork > {
70- get(" /repos/$owner /$name " ) {
71- header(HttpHeaders .Accept , " application/vnd.github+json" )
90+ val cacheKey = " details:repo:$owner /$name "
91+
92+ cacheManager.get<GithubRepoSummary >(cacheKey)?.let { cached ->
93+ logger.debug(" Cache hit for repo $owner /$name " )
94+ return cached
95+ }
96+
97+ return try {
98+ val result = httpClient.executeRequest<RepoByIdNetwork > {
99+ get(" /repos/$owner /$name " ) {
100+ header(HttpHeaders .Accept , " application/vnd.github+json" )
101+ }
102+ }.getOrThrow().toGithubRepoSummary()
103+
104+ cacheManager.put(cacheKey, result, CacheTtl .REPO_DETAILS )
105+ result
106+ } catch (e: Exception ) {
107+ cacheManager.getStale<GithubRepoSummary >(cacheKey)?.let { stale ->
108+ logger.debug(" Network error, using stale cache for $owner /$name " )
109+ return stale
72110 }
73- }.getOrThrow().toGithubRepoSummary()
111+ throw e
112+ }
74113 }
75114
76115 override suspend fun getLatestPublishedRelease (
77116 owner : String ,
78117 repo : String ,
79118 defaultBranch : String
80119 ): GithubRelease ? {
81- val releases = httpClient.executeRequest<List <ReleaseNetwork >> {
82- get(" /repos/$owner /$repo /releases" ) {
83- header(HttpHeaders .Accept , " application/vnd.github+json" )
84- parameter(" per_page" , 10 )
85- }
86- }.getOrNull() ? : return null
120+ val cacheKey = " details:latest_release:$owner /$repo "
87121
88- val latest = releases
89- .asSequence()
90- .filter { (it.draft != true ) && (it.prerelease != true ) }
91- .maxByOrNull { it.publishedAt ? : it.createdAt ? : " " }
92- ? : return null
122+ cacheManager.get<GithubRelease >(cacheKey)?.let { cached ->
123+ logger.debug(" Cache hit for latest release $owner /$repo " )
124+ return cached
125+ }
93126
94- return latest.copy(
95- body = processReleaseBody(latest.body, owner, repo, defaultBranch)
96- ).toDomain()
127+ return try {
128+ val releases = httpClient.executeRequest<List <ReleaseNetwork >> {
129+ get(" /repos/$owner /$repo /releases" ) {
130+ header(HttpHeaders .Accept , " application/vnd.github+json" )
131+ parameter(" per_page" , 10 )
132+ }
133+ }.getOrNull() ? : return null
134+
135+ val latest = releases
136+ .asSequence()
137+ .filter { (it.draft != true ) && (it.prerelease != true ) }
138+ .maxByOrNull { it.publishedAt ? : it.createdAt ? : " " }
139+ ? : return null
140+
141+ val result = latest.copy(
142+ body = processReleaseBody(latest.body, owner, repo, defaultBranch)
143+ ).toDomain()
144+
145+ cacheManager.put(cacheKey, result, CacheTtl .RELEASES )
146+ result
147+ } catch (e: Exception ) {
148+ cacheManager.getStale<GithubRelease >(cacheKey)?.let { stale ->
149+ logger.debug(" Network error, using stale cache for latest release $owner /$repo " )
150+ return stale
151+ }
152+ throw e
153+ }
97154 }
98155
99156 override suspend fun getAllReleases (
100157 owner : String ,
101158 repo : String ,
102159 defaultBranch : String
103160 ): List <GithubRelease > {
104- val releases = httpClient.executeRequest<List <ReleaseNetwork >> {
105- get(" /repos/$owner /$repo /releases" ) {
106- header(HttpHeaders .Accept , " application/vnd.github+json" )
107- parameter(" per_page" , 30 )
161+ val cacheKey = " details:releases:$owner /$repo "
162+
163+ cacheManager.get<List <GithubRelease >>(cacheKey)?.let { cached ->
164+ if (cached.isNotEmpty()) {
165+ logger.debug(" Cache hit for all releases $owner /$repo : ${cached.size} releases" )
166+ return cached
108167 }
109- }.getOrNull() ? : return emptyList()
110-
111- return releases
112- .filter { it.draft != true }
113- .map { release ->
114- release.copy(
115- body = processReleaseBody(release.body, owner, repo, defaultBranch)
116- ).toDomain()
168+ }
169+
170+ return try {
171+ val releases = httpClient.executeRequest<List <ReleaseNetwork >> {
172+ get(" /repos/$owner /$repo /releases" ) {
173+ header(HttpHeaders .Accept , " application/vnd.github+json" )
174+ parameter(" per_page" , 30 )
175+ }
176+ }.getOrNull() ? : return emptyList()
177+
178+ val result = releases
179+ .filter { it.draft != true }
180+ .map { release ->
181+ release.copy(
182+ body = processReleaseBody(release.body, owner, repo, defaultBranch)
183+ ).toDomain()
184+ }
185+ .sortedByDescending { it.publishedAt }
186+
187+ if (result.isNotEmpty()) {
188+ cacheManager.put(cacheKey, result, CacheTtl .RELEASES )
189+ }
190+ result
191+ } catch (e: Exception ) {
192+ cacheManager.getStale<List <GithubRelease >>(cacheKey)?.let { stale ->
193+ logger.debug(" Network error, using stale cache for releases $owner /$repo " )
194+ return stale
117195 }
118- .sortedByDescending { it.publishedAt }
196+ throw e
197+ }
119198 }
120199
121200 private fun processReleaseBody (
@@ -142,6 +221,32 @@ class DetailsRepositoryImpl(
142221 owner : String ,
143222 repo : String ,
144223 defaultBranch : String
224+ ): Triple <String , String ?, String >? {
225+ val cacheKey = " details:readme:$owner /$repo "
226+
227+ cacheManager.get<CachedReadme >(cacheKey)?.let { cached ->
228+ logger.debug(" Cache hit for readme $owner /$repo " )
229+ return Triple (cached.content, cached.languageCode, cached.path)
230+ }
231+
232+ val result = fetchReadmeFromApi(owner, repo, defaultBranch)
233+
234+ if (result != null ) {
235+ val cachedReadme = CachedReadme (
236+ content = result.first,
237+ languageCode = result.second,
238+ path = result.third
239+ )
240+ cacheManager.put(cacheKey, cachedReadme, CacheTtl .README )
241+ }
242+
243+ return result
244+ }
245+
246+ private suspend fun fetchReadmeFromApi (
247+ owner : String ,
248+ repo : String ,
249+ defaultBranch : String
145250 ): Triple <String , String ?, String >? {
146251 val attempts = readmeHelper.generateReadmeAttempts()
147252 val baseUrl = " https://raw.githubusercontent.com/$owner /$repo /$defaultBranch /"
@@ -263,40 +368,76 @@ class DetailsRepositoryImpl(
263368 }
264369
265370 override suspend fun getRepoStats (owner : String , repo : String ): RepoStats {
266- val info = httpClient.executeRequest<RepoInfoNetwork > {
267- get(" /repos/$owner /$repo " ) {
268- header(HttpHeaders .Accept , " application/vnd.github+json" )
269- }
270- }.getOrThrow()
371+ val cacheKey = " details:stats:$owner /$repo "
271372
272- return RepoStats (
273- stars = info.stars,
274- forks = info.forks,
275- openIssues = info.openIssues,
276- )
373+ cacheManager.get<RepoStats >(cacheKey)?.let { cached ->
374+ logger.debug(" Cache hit for repo stats $owner /$repo " )
375+ return cached
376+ }
377+
378+ return try {
379+ val info = httpClient.executeRequest<RepoInfoNetwork > {
380+ get(" /repos/$owner /$repo " ) {
381+ header(HttpHeaders .Accept , " application/vnd.github+json" )
382+ }
383+ }.getOrThrow()
384+
385+ val result = RepoStats (
386+ stars = info.stars,
387+ forks = info.forks,
388+ openIssues = info.openIssues,
389+ )
390+
391+ cacheManager.put(cacheKey, result, CacheTtl .REPO_STATS )
392+ result
393+ } catch (e: Exception ) {
394+ cacheManager.getStale<RepoStats >(cacheKey)?.let { stale ->
395+ logger.debug(" Network error, using stale cache for stats $owner /$repo " )
396+ return stale
397+ }
398+ throw e
399+ }
277400 }
278401
279402 override suspend fun getUserProfile (username : String ): GithubUserProfile {
280- val user = httpClient.executeRequest<UserProfileNetwork > {
281- get(" /users/$username " ) {
282- header(HttpHeaders .Accept , " application/vnd.github+json" )
403+ val cacheKey = " details:profile:$username "
404+
405+ cacheManager.get<GithubUserProfile >(cacheKey)?.let { cached ->
406+ logger.debug(" Cache hit for user profile $username " )
407+ return cached
408+ }
409+
410+ return try {
411+ val user = httpClient.executeRequest<UserProfileNetwork > {
412+ get(" /users/$username " ) {
413+ header(HttpHeaders .Accept , " application/vnd.github+json" )
414+ }
415+ }.getOrThrow()
416+
417+ val result = GithubUserProfile (
418+ id = user.id,
419+ login = user.login,
420+ name = user.name,
421+ bio = user.bio,
422+ avatarUrl = user.avatarUrl,
423+ htmlUrl = user.htmlUrl,
424+ followers = user.followers,
425+ following = user.following,
426+ publicRepos = user.publicRepos,
427+ location = user.location,
428+ company = user.company,
429+ blog = user.blog,
430+ twitterUsername = user.twitterUsername
431+ )
432+
433+ cacheManager.put(cacheKey, result, CacheTtl .USER_PROFILE )
434+ result
435+ } catch (e: Exception ) {
436+ cacheManager.getStale<GithubUserProfile >(cacheKey)?.let { stale ->
437+ logger.debug(" Network error, using stale cache for profile $username " )
438+ return stale
283439 }
284- }.getOrThrow()
285-
286- return GithubUserProfile (
287- id = user.id,
288- login = user.login,
289- name = user.name,
290- bio = user.bio,
291- avatarUrl = user.avatarUrl,
292- htmlUrl = user.htmlUrl,
293- followers = user.followers,
294- following = user.following,
295- publicRepos = user.publicRepos,
296- location = user.location,
297- company = user.company,
298- blog = user.blog,
299- twitterUsername = user.twitterUsername
300- )
440+ throw e
441+ }
301442 }
302- }
443+ }
0 commit comments