feat(details): manual refresh via /v1/repo/{owner}/{name}/refresh#510
Conversation
…n Android, Ctrl/Cmd+R on desktop
…et across all locales
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughAdds a manual refresh flow for repository details: backend POST endpoint with refresh-specific 429 parsing and new exceptions, a domain RefreshException enum/type, repository refresh API and cache invalidation, ViewModel/UI wiring (pull-to-refresh, keyboard, overflow menu), platform support flags, localized strings, and updated release notes. ChangesManual Repository Refresh Feature
Sequence DiagramsequenceDiagram
participant User
participant UI as DetailsScreen
participant VM as DetailsViewModel
participant Repo as DetailsRepository
participant API as BackendApiClient
participant Backend as Backend Service
User->>UI: Pull / Click refresh / Press Ctrl+R
UI->>VM: onAction(Refresh)
VM->>VM: check cooldown & isRefreshing
alt cooldown active
VM-->>UI: emit OnRefreshError(COOLDOWN, retryAfterSeconds)
UI-->>User: show cooldown snackbar
else allowed
VM->>Repo: refreshRepository(owner,name)
Repo->>API: refreshRepo(owner,name)
API->>Backend: POST /repo/{owner}/{name}/refresh
alt 2xx
Backend-->>API: BackendRepoResponse
API-->>Repo: success
Repo-->>VM: GithubRepoSummary
VM-->>UI: update state with refreshed data
else 429
Backend-->>API: 429 with body/header
API->>Repo: throws RefreshCooldownException or RefreshBudgetExhaustedException
Repo-->>VM: RefreshException(kind, retryAfterSeconds)
VM-->>UI: emit OnRefreshError(kind, retryAfterSeconds)
else 404/410
Backend-->>API: 404/410
API->>Repo: throws RepoNotFoundException/RepoArchivedException
Repo-->>VM: RefreshException(NOT_FOUND/ARCHIVED)
VM-->>UI: emit OnRefreshError(kind)
else other
API-->>Repo: BackendException
Repo-->>VM: RefreshException(UPSTREAM/GENERIC)
VM-->>UI: emit OnRefreshError(GENERIC)
end
end
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/isPullToRefreshSupported.kt (1)
1-3: ⚖️ Poor tradeoffAlign the package with the repo convention.
zed.rainxch.core.presentation.utilsadds a third segment that doesn't fit the repo'szed.rainxch.{module}.{layer}package pattern. Please move this expect helper into the presentation layer package and keep the platform actuals aligned as well. As per coding guidelines, package names must follow the patternzed.rainxch.{module}.{layer}.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/isPullToRefreshSupported.kt` around lines 1 - 3, The expect declaration is in the wrong package; change its package from zed.rainxch.core.presentation.utils to zed.rainxch.core.presentation and update all matching platform actual implementations to the same package so the expect/actual linkage resolves; specifically move the expect fun isPullToRefreshSupported() into package zed.rainxch.core.presentation and ensure each actual isPullToRefreshSupported in platform source sets uses that same package.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt`:
- Around line 447-461: In parseRefreshErrorBody, remove the early return that
checks response.contentLength() == 0L so we don't skip parsing when
Content-Length is absent (e.g., chunked responses); instead always call
response.bodyAsText(), keep the existing text.isBlank() guard and JSON parsing
logic, and preserve the try/catch/return behavior that yields error and
retry_after from the JsonObject (refer to the parseRefreshErrorBody function and
its use of response.contentLength(), response.bodyAsText(),
Json.parseToJsonElement, and the extraction of "error" and "retry_after").
In `@core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml`:
- Around line 831-837: The French strings containing escaped apostrophes (e.g.,
details_refresh_more_options, details_refresh_snackbar_cooldown,
details_refresh_snackbar_budget_exhausted, details_refresh_snackbar_not_found,
details_refresh_snackbar_upstream, details_refresh_snackbar_generic) currently
include backslashes before apostrophes (\' ) which render literally; update each
string value to remove the backslashes and use a plain apostrophe (') or a
proper XML entity (') so the UI shows correct French text (for example
change "d\'actualiser" to "d'actualiser").
In `@core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml`:
- Around line 866-873: The Turkish strings details_refresh_snackbar_archived and
details_refresh_snackbar_not_found contain escaped backslashes before
apostrophes (GitHub\'da) which render literally; edit those entries to remove
the backslashes so they read GitHub'da (plain apostrophe) instead of GitHub\'da,
leaving all other strings unchanged.
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt`:
- Around line 535-541: The handlers that call onAction(DetailsAction.Refresh)
(notably the onPreviewKeyEvent Ctrl/Cmd+R branch and the pull-to-refresh
callback) must guard against dispatching while a refresh is already running:
check state.isRefreshing and only call onAction(DetailsAction.Refresh) when
false, returning/consuming the event accordingly; update the onPreviewKeyEvent
lambda and the pull-to-refresh refresh handler to first test state.isRefreshing
and skip/return true (or no-op) if a refresh is in progress.
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt`:
- Around line 2653-2656: When computing the new selectedRelease from
freshReleases, first try to preserve the current selection by finding an item in
freshReleases that matches the existing _state.value.selectedRelease (e.g.,
compare a stable unique identity like id/version), and only if no match is found
fall back to the existing logic (first non-prerelease via
isEffectivelyPreRelease() or firstOrNull). Update the block that builds
selectedRelease to search freshReleases for a match against
_state.value.selectedRelease before using the first stable/first release
fallback.
- Around line 2633-2645: The async blocks that build releasesDeferred and
statsDeferred are using runCatching which swallows CancellationException; update
both blocks in refresh() to use explicit try/catch around
detailsRepository.getAllReleases(...) and detailsRepository.getRepoStats(...)
respectively, catching Throwable but rethrowing CancellationException
immediately (and only handling non-cancellation errors), so cancellations
propagate to the outer try/catch and preserve structured concurrency; keep the
same return/result wrapping used elsewhere in the codebase after converting
runCatching to the try/catch+rethrow pattern.
---
Nitpick comments:
In
`@core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/isPullToRefreshSupported.kt`:
- Around line 1-3: The expect declaration is in the wrong package; change its
package from zed.rainxch.core.presentation.utils to
zed.rainxch.core.presentation and update all matching platform actual
implementations to the same package so the expect/actual linkage resolves;
specifically move the expect fun isPullToRefreshSupported() into package
zed.rainxch.core.presentation and ensure each actual isPullToRefreshSupported in
platform source sets uses that same package.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: eeba3ccb-af68-4343-92c7-4b627aa11173
📒 Files selected for processing (38)
core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/RefreshException.ktcore/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/utils/isPullToRefreshSupported.android.ktcore/presentation/src/commonMain/composeResources/files/whatsnew/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ar/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/es/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/hi/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/it/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ja/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/pl/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ru/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/16.jsoncore/presentation/src/commonMain/composeResources/values-ar/strings-ar.xmlcore/presentation/src/commonMain/composeResources/values-bn/strings-bn.xmlcore/presentation/src/commonMain/composeResources/values-es/strings-es.xmlcore/presentation/src/commonMain/composeResources/values-fr/strings-fr.xmlcore/presentation/src/commonMain/composeResources/values-hi/strings-hi.xmlcore/presentation/src/commonMain/composeResources/values-it/strings-it.xmlcore/presentation/src/commonMain/composeResources/values-ja/strings-ja.xmlcore/presentation/src/commonMain/composeResources/values-ko/strings-ko.xmlcore/presentation/src/commonMain/composeResources/values-pl/strings-pl.xmlcore/presentation/src/commonMain/composeResources/values-ru/strings-ru.xmlcore/presentation/src/commonMain/composeResources/values-tr/strings-tr.xmlcore/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xmlcore/presentation/src/commonMain/composeResources/values/strings.xmlcore/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/isPullToRefreshSupported.ktcore/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/utils/isPullToRefreshSupported.jvm.ktfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.ktfeature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/repository/DetailsRepository.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsEvent.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt
| private suspend fun parseRefreshErrorBody( | ||
| response: io.ktor.client.statement.HttpResponse, | ||
| ): Pair<String?, Long?> { | ||
| val contentLength = response.contentLength() ?: 0L | ||
| if (contentLength == 0L) return null to null | ||
| return try { | ||
| val text = response.bodyAsText() | ||
| if (text.isBlank()) return null to null | ||
| val obj = Json.parseToJsonElement(text) as? kotlinx.serialization.json.JsonObject | ||
| ?: return null to null | ||
| val error = (obj["error"] as? kotlinx.serialization.json.JsonPrimitive)?.contentOrNull | ||
| val retryAfter = (obj["retry_after"] as? kotlinx.serialization.json.JsonPrimitive) | ||
| ?.contentOrNull | ||
| ?.toLongOrNull() | ||
| error to retryAfter |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does Ktor HttpResponse.contentLength()return null when the response omits theContent-Length header, such as with chunked transfer encoding?
💡 Result:
Yes. Ktor’s HttpResponse.contentLength (and the underlying parsing it relies on) returns null when the response does not include a valid Content-Length header, which is the case for responses that omit Content-Length (e.g., when the transfer uses Transfer-Encoding: chunked).
Citations:
- 1: https://api.ktor.io/ktor-client-core/io.ktor.client.statement/-http-response/index.html
- 2: https://api.ktor.io/ktor-http/io.ktor.http/content-length.html
- 3: https://api.ktor.io/3.1.x/ktor-http/io.ktor.http.content/-outgoing-content/content-length.html
- 4: Content-Length not specified with ktor-html ktorio/ktor#345
Remove the Content-Length gate from refresh-429 parsing.
The current implementation returns null to null for responses that omit the Content-Length header (e.g., with chunked transfer encoding), even when a valid JSON body is present. This silently skips the error and retry_after fields, collapsing budget_exhausted into the generic 429 path and breaking the distinct snackbar/cooldown UX.
The isBlank() check on the parsed body is sufficient; remove the contentLength() gate entirely.
Suggested fix
private suspend fun parseRefreshErrorBody(
response: io.ktor.client.statement.HttpResponse,
): Pair<String?, Long?> {
- val contentLength = response.contentLength() ?: 0L
- if (contentLength == 0L) return null to null
return try {
val text = response.bodyAsText()
if (text.isBlank()) return null to null
val obj = Json.parseToJsonElement(text) as? kotlinx.serialization.json.JsonObject
?: return null to null🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt`
around lines 447 - 461, In parseRefreshErrorBody, remove the early return that
checks response.contentLength() == 0L so we don't skip parsing when
Content-Length is absent (e.g., chunked responses); instead always call
response.bodyAsText(), keep the existing text.isBlank() guard and JSON parsing
logic, and preserve the try/catch/return behavior that yields error and
retry_after from the JsonObject (refer to the parseRefreshErrorBody function and
its use of response.contentLength(), response.bodyAsText(),
Json.parseToJsonElement, and the extraction of "error" and "retry_after").
| <string name="details_refresh_more_options">Plus d\'options</string> | ||
| <string name="details_refresh_snackbar_cooldown">Vous venez d\'actualiser — réessayez dans %1$d s.</string> | ||
| <string name="details_refresh_snackbar_budget_exhausted">Service d\'actualisation occupé — réessayez dans %1$d s.</string> | ||
| <string name="details_refresh_snackbar_archived">Ce dépôt est archivé sur GitHub.</string> | ||
| <string name="details_refresh_snackbar_not_found">Ce dépôt n\'existe plus sur GitHub.</string> | ||
| <string name="details_refresh_snackbar_upstream">Échec de l\'actualisation. Réessayez bientôt.</string> | ||
| <string name="details_refresh_snackbar_generic">Échec de l\'actualisation. Réessayez.</string> |
There was a problem hiding this comment.
Remove the escaped apostrophes from the French refresh copy.
\' is rendered literally in XML text, so these strings will show backslashes in the UI. Please replace them with normal apostrophes.
Fix
- <string name="details_refresh_more_options">Plus d\'options</string>
- <string name="details_refresh_snackbar_cooldown">Vous venez d\'actualiser — réessayez dans %1$d s.</string>
- <string name="details_refresh_snackbar_budget_exhausted">Service d\'actualisation occupé — réessayez dans %1$d s.</string>
- <string name="details_refresh_snackbar_not_found">Ce dépôt n\'existe plus sur GitHub.</string>
- <string name="details_refresh_snackbar_upstream">Échec de l\'actualisation. Réessayez bientôt.</string>
+ <string name="details_refresh_more_options">Plus d’options</string>
+ <string name="details_refresh_snackbar_cooldown">Vous venez d’actualiser — réessayez dans %1$d s.</string>
+ <string name="details_refresh_snackbar_budget_exhausted">Service d’actualisation occupé — réessayez dans %1$d s.</string>
+ <string name="details_refresh_snackbar_not_found">Ce dépôt n’existe plus sur GitHub.</string>
+ <string name="details_refresh_snackbar_upstream">Échec de l’actualisation. Réessayez bientôt.</string>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml`
around lines 831 - 837, The French strings containing escaped apostrophes (e.g.,
details_refresh_more_options, details_refresh_snackbar_cooldown,
details_refresh_snackbar_budget_exhausted, details_refresh_snackbar_not_found,
details_refresh_snackbar_upstream, details_refresh_snackbar_generic) currently
include backslashes before apostrophes (\' ) which render literally; update each
string value to remove the backslashes and use a plain apostrophe (') or a
proper XML entity (') so the UI shows correct French text (for example
change "d\'actualiser" to "d'actualiser").
| <string name="details_refresh_cooldown">Yenile (%1$dsn)</string> | ||
| <string name="details_refresh_more_options">Diğer seçenekler</string> | ||
| <string name="details_refresh_snackbar_cooldown">Az önce yenilediniz — %1$d sn sonra tekrar deneyin.</string> | ||
| <string name="details_refresh_snackbar_budget_exhausted">Yenileme hizmeti meşgul — %1$d sn sonra tekrar deneyin.</string> | ||
| <string name="details_refresh_snackbar_archived">Bu depo GitHub\'da arşivlenmiş.</string> | ||
| <string name="details_refresh_snackbar_not_found">Bu depo artık GitHub\'da bulunmuyor.</string> | ||
| <string name="details_refresh_snackbar_upstream">Yenileme başarısız. Birazdan tekrar deneyin.</string> | ||
| <string name="details_refresh_snackbar_generic">Yenileme başarısız. Tekrar deneyin.</string> |
There was a problem hiding this comment.
Remove the escaped apostrophes from the Turkish refresh copy.
The backslashes in GitHub\'da are literal in XML text, so they will show up to users. Please switch these to plain apostrophes.
Fix
- <string name="details_refresh_snackbar_archived">Bu depo GitHub\'da arşivlenmiş.</string>
- <string name="details_refresh_snackbar_not_found">Bu depo artık GitHub\'da bulunmuyor.</string>
+ <string name="details_refresh_snackbar_archived">Bu depo GitHub'da arşivlenmiş.</string>
+ <string name="details_refresh_snackbar_not_found">Bu depo artık GitHub'da bulunmuyor.</string>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml`
around lines 866 - 873, The Turkish strings details_refresh_snackbar_archived
and details_refresh_snackbar_not_found contain escaped backslashes before
apostrophes (GitHub\'da) which render literally; edit those entries to remove
the backslashes so they read GitHub'da (plain apostrophe) instead of GitHub\'da,
leaving all other strings unchanged.
| .onPreviewKeyEvent { event -> | ||
| if (event.type == KeyEventType.KeyDown && | ||
| (event.isMetaPressed || event.isCtrlPressed) && | ||
| event.key == Key.R | ||
| ) { | ||
| onAction(DetailsAction.Refresh) | ||
| true |
There was a problem hiding this comment.
Prevent duplicate refresh dispatch while a refresh is already in progress.
Line 540 and Line 557 send DetailsAction.Refresh unconditionally. Unlike the overflow menu, these paths ignore state.isRefreshing, so users can enqueue overlapping refreshes via Ctrl/Cmd+R or pull-to-refresh.
💡 Suggested fix
val pullEnabled = remember { isPullToRefreshSupported() }
+ val canTriggerRefresh = !state.isRefreshing
ScrollbarContainer(
@@
.onPreviewKeyEvent { event ->
if (event.type == KeyEventType.KeyDown &&
(event.isMetaPressed || event.isCtrlPressed) &&
- event.key == Key.R
+ event.key == Key.R &&
+ canTriggerRefresh
) {
onAction(DetailsAction.Refresh)
true
@@
PullToRefreshHost(
enabled = pullEnabled,
isRefreshing = state.isRefreshing,
- onRefresh = { onAction(DetailsAction.Refresh) },
+ onRefresh = {
+ if (canTriggerRefresh) onAction(DetailsAction.Refresh)
+ },
) {Also applies to: 554-558
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt`
around lines 535 - 541, The handlers that call onAction(DetailsAction.Refresh)
(notably the onPreviewKeyEvent Ctrl/Cmd+R branch and the pull-to-refresh
callback) must guard against dispatching while a refresh is already running:
check state.isRefreshing and only call onAction(DetailsAction.Refresh) when
false, returning/consuming the event accordingly; update the onPreviewKeyEvent
lambda and the pull-to-refresh refresh handler to first test state.isRefreshing
and skip/return true (or no-op) if a refresh is in progress.
…is absent (chunked responses)
…cancellation in deferred fetches
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml`:
- Line 868: The Hindi cooldown string details_refresh_cooldown currently renders
as "रीफ़्रेश (%1$dसे)" without spacing; update the string value for
details_refresh_cooldown to include a separator and preserve the %1$d
placeholder — e.g., change to "रीफ़्रेश (%1$d से)" or use the full unit
"रीफ़्रेश (%1$d सेकंड)". Ensure the placeholder stays exactly "%1$d" and
spacing/word choice reads naturally in Hindi.
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt`:
- Around line 212-216: The current flow writes the new repo cache via
cacheManager.put but immediately invalidates dependent keys
(cacheManager.invalidate for "details:repo_id:${result.id}",
"details:stats:$owner/$name", "details:latest_release:$owner/$name",
"details:releases:$owner/$name") before the VM refresh repopulates those
dependent payloads; change the flow so dependent caches are only evicted after
their replacements are persisted (or return the refreshed dependent payloads
from the same refresh call). Concretely: perform the follow-up fetches (or
obtain the refreshed stats/releases) and persist them with cacheManager.put
first, then call cacheManager.invalidate for the dependent keys (or skip
invalidate if you replaced them atomically), and adjust
DetailsRepositoryImpl::refresh / DetailsViewModel.refresh to return or await the
refreshed dependent data so eviction happens only after successful writes.
In
`@feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt`:
- Around line 2660-2695: The refresh path updates selectedRelease but doesn't
update selectedReleaseCategory, causing UI/filter desync; update
selectedReleaseCategory whenever you set selectedRelease in the refresh flow by
resolving the category from the chosen selectedRelease (re-use the same logic
used in retryReleases() that maps a Release -> selectedReleaseCategory), e.g.
compute newCategory from the selectedRelease (or fallback logic used there) and
set selectedReleaseCategory = newCategory inside the same _state.update block
where selectedRelease is assigned.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f7092855-45fd-441e-a1b6-14f83de879a9
📒 Files selected for processing (38)
core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/RefreshException.ktcore/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/utils/isPullToRefreshSupported.android.ktcore/presentation/src/commonMain/composeResources/files/whatsnew/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ar/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/es/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/hi/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/it/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ja/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/pl/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ru/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/16.jsoncore/presentation/src/commonMain/composeResources/values-ar/strings-ar.xmlcore/presentation/src/commonMain/composeResources/values-bn/strings-bn.xmlcore/presentation/src/commonMain/composeResources/values-es/strings-es.xmlcore/presentation/src/commonMain/composeResources/values-fr/strings-fr.xmlcore/presentation/src/commonMain/composeResources/values-hi/strings-hi.xmlcore/presentation/src/commonMain/composeResources/values-it/strings-it.xmlcore/presentation/src/commonMain/composeResources/values-ja/strings-ja.xmlcore/presentation/src/commonMain/composeResources/values-ko/strings-ko.xmlcore/presentation/src/commonMain/composeResources/values-pl/strings-pl.xmlcore/presentation/src/commonMain/composeResources/values-ru/strings-ru.xmlcore/presentation/src/commonMain/composeResources/values-tr/strings-tr.xmlcore/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xmlcore/presentation/src/commonMain/composeResources/values/strings.xmlcore/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/utils/isPullToRefreshSupported.ktcore/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/utils/isPullToRefreshSupported.jvm.ktfeature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.ktfeature/details/domain/src/commonMain/kotlin/zed/rainxch/details/domain/repository/DetailsRepository.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsEvent.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsState.ktfeature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt
| cacheManager.put(cacheKey, result, REPO_DETAILS) | ||
| cacheManager.invalidate("details:repo_id:${result.id}") | ||
| cacheManager.invalidate("details:stats:$owner/$name") | ||
| cacheManager.invalidate("details:latest_release:$owner/$name") | ||
| cacheManager.invalidate("details:releases:$owner/$name") |
There was a problem hiding this comment.
Avoid evicting dependent caches before the replacements exist.
These invalidations run before DetailsViewModel.refresh() has successfully repopulated releases/stats. If either follow-up fetch fails, the screen keeps stale in-memory values for the current session, but the last-known-good persisted cache is gone for the next load/offline fallback. Consider delaying these invalidations until the replacement payloads have been stored, or returning the dependent refreshed data from the same refresh call.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/DetailsRepositoryImpl.kt`
around lines 212 - 216, The current flow writes the new repo cache via
cacheManager.put but immediately invalidates dependent keys
(cacheManager.invalidate for "details:repo_id:${result.id}",
"details:stats:$owner/$name", "details:latest_release:$owner/$name",
"details:releases:$owner/$name") before the VM refresh repopulates those
dependent payloads; change the flow so dependent caches are only evicted after
their replacements are persisted (or return the refreshed dependent payloads
from the same refresh call). Concretely: perform the follow-up fetches (or
obtain the refreshed stats/releases) and persist them with cacheManager.put
first, then call cacheManager.invalidate for the dependent keys (or skip
invalidate if you replaced them atomically), and adjust
DetailsRepositoryImpl::refresh / DetailsViewModel.refresh to return or await the
refreshed dependent data so eviction happens only after successful writes.
…(postposition collision)
…ew selection to avoid filter desync
Summary
Wires the freshly-shipped backend
POST /v1/repo/{owner}/{name}/refreshendpoint into the Details screen so users can force-fresh repo metadata + latest release on demand.How it surfaces
MANUALinstalls.isPullToRefreshSupported()expect/actual). Desktop skips the gesture — no natural mouse mapping — and instead picks up:cooldownorbudget_exhausted, the menu item disables and shows a liveRefresh (Ns)countdown driven by aLaunchedEffectticker; a Snackbar surfaces the human-readable reason.archived(410),not_found(404),github_unreachable(502), andbudget_exhausted(429-with-budget-body).Architecture
BackendApiClient.refreshRepo(owner, name)— POSTs the new endpoint, attachesX-GitHub-Tokenif signed in, parses the 429 body to discriminatecooldownvsbudget_exhausted, and returns typed exceptions.RefreshException(kind, retryAfterSeconds)+RefreshErrorenum so the presentation layer doesn't depend oncore/datainternals.DetailsRepositoryImpl.refreshRepositorycalls the backend, busts the local repo / stats / latest-release / releases caches on success, and re-throws data-layer exceptions asRefreshExceptionfor the ViewModel.DetailsViewModel.refresh()short-circuits while a client-side cooldown is active, otherwise replacesrepository/stats/allReleaseswith the fresh response, then re-runsgetAllReleases+getRepoStatsto consume the busted caches. No fallback to GitHub direct (per backend doc §7).Test plan
X-GitHub-Token).Summary by CodeRabbit
New Features
Localization