Skip to content

Commit 32ca8a0

Browse files
committed
refactor(core): Handle rate limit exceptions and improve starred repos sync
This commit introduces centralized handling for `RateLimitException` across various features and refactors the starred repositories synchronization logic. When a GitHub API rate limit is exceeded, the app will now fail gracefully instead of crashing. This affects app updates, repository searching, and fetching starred repositories. Additionally, the synchronization behavior for starred repositories has been updated. The sync threshold has been extended to 24 hours to reduce unnecessary API calls. A "Sign In" button has been added to the "Starred" screen, guiding unauthenticated users to log in to view their starred repositories. - **refactor(core)**: Propagate `RateLimitException` from data sources up to the view models in the search, apps, and starred features to prevent crashes. - **feat(starred)**: Added a `OnSingInClick` action and navigation to the authentication screen from the "Starred" page for unauthenticated users. - **refactor(starred)**: Extended the sync threshold for starred repositories from 6 to 24 hours.
1 parent 9972274 commit 32ca8a0

9 files changed

Lines changed: 44 additions & 5 deletions

File tree

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ fun AppNavigation(
184184
)
185185
)
186186
},
187+
onNavigateToAuthentication = {
188+
navController.navigate(
189+
GithubStoreGraph.AuthenticationScreen
190+
)
191+
},
187192
onNavigateToDeveloperProfile = { username ->
188193
navController.navigate(
189194
GithubStoreGraph.DeveloperProfileScreen(

core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/StarredRepositoryImpl.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import zed.rainxch.core.data.local.db.dao.StarredRepoDao
2626
import zed.rainxch.core.data.mappers.toDomain
2727
import zed.rainxch.core.data.mappers.toEntity
2828
import zed.rainxch.core.domain.model.Platform
29+
import zed.rainxch.core.domain.model.RateLimitException
2930
import zed.rainxch.core.domain.repository.StarredRepository
31+
import kotlin.coroutines.cancellation.CancellationException
3032
import kotlin.time.Clock
3133
import kotlin.time.ExperimentalTime
3234
import kotlin.time.Instant
@@ -39,7 +41,7 @@ class StarredRepositoryImpl(
3941
) : StarredRepository {
4042

4143
companion object {
42-
private const val SYNC_THRESHOLD_MS = 6 * 60 * 60 * 1000L // 6 hours
44+
private const val SYNC_THRESHOLD_MS = 24 * 60 * 60 * 1000L // 24 hours
4345
}
4446

4547
override fun getAllStarred(): Flow<List<zed.rainxch.core.domain.model.StarredRepository>> {
@@ -99,7 +101,6 @@ class StarredRepositoryImpl(
99101
val now = Clock.System.now().toEpochMilliseconds()
100102
val starredRepos = mutableListOf<zed.rainxch.core.domain.model.StarredRepository>()
101103

102-
// Process in parallel to avoid sequential N+1 delays
103104
coroutineScope {
104105
val semaphore = Semaphore(25)
105106
val deferredResults = allRepos.map { repo ->
@@ -145,6 +146,10 @@ class StarredRepositoryImpl(
145146
starredRepoDao.replaceAllStarred(starredRepos.map { it.toEntity() })
146147

147148
Result.success(Unit)
149+
} catch (e: RateLimitException) {
150+
throw e
151+
} catch (e: CancellationException) {
152+
throw e
148153
} catch (e: Exception) {
149154
Logger.e(e) { "Failed to sync starred repos" }
150155
Result.failure(e)
@@ -185,6 +190,10 @@ class StarredRepositoryImpl(
185190
}
186191

187192
relevantAssets.isNotEmpty()
193+
} catch (e: RateLimitException) {
194+
throw e
195+
} catch (e: CancellationException) {
196+
throw e
188197
} catch (e: Exception) {
189198
Logger.w(e) { "Failed to check valid assets for $owner/$repo" }
190199
false

feature/apps/data/src/commonMain/kotlin/zed/rainxch/apps/data/repository/AppsRepositoryImpl.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import zed.rainxch.core.data.network.executeRequest
1313
import zed.rainxch.core.domain.logging.GitHubStoreLogger
1414
import zed.rainxch.core.domain.model.GithubRelease
1515
import zed.rainxch.core.domain.model.InstalledApp
16+
import zed.rainxch.core.domain.model.RateLimitException
1617
import zed.rainxch.core.domain.repository.InstalledAppsRepository
1718
import zed.rainxch.core.domain.utils.AppLauncher
1819

@@ -53,13 +54,15 @@ class AppsRepositoryImpl(
5354
header(HttpHeaders.Accept, "application/vnd.github+json")
5455
parameter("per_page", 10)
5556
}
56-
}.getOrNull() ?: return null
57+
}.getOrThrow()
5758

5859
releases
5960
.asSequence()
6061
.filter { (it.draft != true) && (it.prerelease != true) }
6162
.maxByOrNull { it.publishedAt ?: it.createdAt ?: "" }
6263
?.toDomain()
64+
} catch (e: RateLimitException) {
65+
throw e
6366
} catch (e: Exception) {
6467
logger.error("Failed to fetch latest release for $owner/$repo: ${e.message}")
6568
null

feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import zed.rainxch.apps.presentation.model.UpdateAllProgress
2222
import zed.rainxch.apps.presentation.model.UpdateState
2323
import zed.rainxch.core.domain.logging.GitHubStoreLogger
2424
import zed.rainxch.core.domain.model.InstalledApp
25+
import zed.rainxch.core.domain.model.RateLimitException
2526
import zed.rainxch.core.domain.network.Downloader
2627
import zed.rainxch.core.domain.repository.InstalledAppsRepository
2728
import zed.rainxch.core.domain.system.Installer
@@ -289,9 +290,11 @@ class AppsViewModel(
289290
cleanupUpdate(app.packageName, app.latestAssetName)
290291
updateAppState(app.packageName, UpdateState.Idle)
291292
throw e
293+
} catch (e: RateLimitException) {
294+
logger.debug("Rate limited during update for ${app.packageName}")
295+
updateAppState(app.packageName, UpdateState.Idle)
292296
} catch (e: Exception) {
293297
logger.error("Update failed for ${app.packageName}: ${e.message}")
294-
e.printStackTrace()
295298
cleanupUpdate(app.packageName, app.latestAssetName)
296299
updateAppState(
297300
app.packageName,

feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import zed.rainxch.core.data.mappers.toSummary
2626
import zed.rainxch.core.data.network.executeRequest
2727
import zed.rainxch.core.domain.model.GithubRepoSummary
2828
import zed.rainxch.core.domain.model.PaginatedDiscoveryRepositories
29+
import zed.rainxch.core.domain.model.RateLimitException
2930
import zed.rainxch.domain.model.ProgrammingLanguage
3031
import zed.rainxch.domain.model.SearchPlatform
3132
import zed.rainxch.domain.repository.SearchRepository
@@ -169,6 +170,8 @@ class SearchRepositoryImpl(
169170
)
170171
}
171172
}
173+
} catch (e: RateLimitException) {
174+
throw e
172175
} catch (e: CancellationException) {
173176
throw e
174177
}

feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.update
1515
import kotlinx.coroutines.launch
1616
import org.jetbrains.compose.resources.getString
1717
import zed.rainxch.core.domain.logging.GitHubStoreLogger
18+
import zed.rainxch.core.domain.model.RateLimitException
1819
import zed.rainxch.core.domain.repository.FavouritesRepository
1920
import zed.rainxch.core.domain.repository.InstalledAppsRepository
2021
import zed.rainxch.core.domain.repository.StarredRepository
@@ -225,6 +226,8 @@ class SearchViewModel(
225226
_state.update {
226227
it.copy(isLoading = false, isLoadingMore = false)
227228
}
229+
} catch (e: RateLimitException) {
230+
logger.debug("Rate limit exceed: ${e.message}")
228231
} catch (e: CancellationException) {
229232
logger.debug("Search cancelled (expected): ${e.message}")
230233
} catch (e: Exception) {

feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposAction.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ sealed interface StarredReposAction {
66
data object OnNavigateBackClick : StarredReposAction
77
data object OnRefresh : StarredReposAction
88
data object OnRetrySync : StarredReposAction
9+
data object OnSingInClick : StarredReposAction
910
data object OnDismissError : StarredReposAction
1011
data class OnRepositoryClick(val repository: StarredRepositoryUi) : StarredReposAction
1112
data class OnDeveloperProfileClick(val username: String) : StarredReposAction

feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposRoot.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ fun StarredReposRoot(
5858
onNavigateBack: () -> Unit,
5959
onNavigateToDetails: (repoId: Long) -> Unit,
6060
onNavigateToDeveloperProfile: (username: String) -> Unit,
61+
onNavigateToAuthentication: () -> Unit,
6162
viewModel: StarredReposViewModel = koinViewModel()
6263
) {
6364
val state by viewModel.state.collectAsStateWithLifecycle()
@@ -69,6 +70,7 @@ fun StarredReposRoot(
6970
StarredReposAction.OnNavigateBackClick -> onNavigateBack()
7071
is StarredReposAction.OnRepositoryClick -> onNavigateToDetails(action.repository.repoId)
7172
is StarredReposAction.OnDeveloperProfileClick -> onNavigateToDeveloperProfile(action.username)
73+
StarredReposAction.OnSingInClick -> onNavigateToAuthentication()
7274
else -> viewModel.onAction(action)
7375
}
7476
}
@@ -104,6 +106,10 @@ fun StarredScreen(
104106
title = stringResource(Res.string.sign_in_required),
105107
message = stringResource(Res.string.sign_in_with_github_for_stars),
106108
icon = Icons.Default.Star,
109+
actionText = stringResource(Res.string.sign_in_with_github),
110+
onActionClick = {
111+
onAction(StarredReposAction.OnSingInClick)
112+
},
107113
modifier = Modifier.align(Alignment.Center)
108114
)
109115
}
@@ -121,7 +127,9 @@ fun StarredScreen(
121127
icon = Icons.Default.Star,
122128
actionText = if (state.errorMessage != null) stringResource(Res.string.retry) else null,
123129
onActionClick = if (state.errorMessage != null) {
124-
{ onAction(StarredReposAction.OnRetrySync) }
130+
{
131+
onAction(StarredReposAction.OnRetrySync)
132+
}
125133
} else null,
126134
modifier = Modifier.align(Alignment.Center)
127135
)

feature/starred/presentation/src/commonMain/kotlin/zed/rainxch/starred/presentation/StarredReposViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ class StarredReposViewModel (
136136
// Handled in composable
137137
}
138138

139+
is StarredReposAction.OnSingInClick -> {
140+
// Handled in composable
141+
}
142+
139143
StarredReposAction.OnRefresh -> {
140144
syncStarredRepos(forceRefresh = true)
141145
}

0 commit comments

Comments
 (0)