Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package zed.rainxch.githubstore.app.di

import org.koin.core.module.dsl.viewModel
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
import zed.rainxch.apps.presentation.AppsViewModel
Expand All @@ -18,7 +19,37 @@ val viewModelsModule =
module {
viewModelOf(::AppsViewModel)
viewModelOf(::AuthenticationViewModel)
viewModelOf(::DetailsViewModel)
viewModel { params ->
// Indexed access because `ownerParam` and `repoParam` are both
// Strings — positional `params.get()` would silently pick the
// first matching by type and could swap the two if Koin ever
// changes its resolution order.
DetailsViewModel(
repositoryId = params.get(0),
ownerParam = params.get(1),
repoParam = params.get(2),
isComingFromUpdate = params.get(3),
detailsRepository = get(),
downloader = get(),
installer = get(),
platform = get(),
helper = get(),
shareManager = get(),
installedAppsRepository = get(),
favouritesRepository = get(),
starredRepository = get(),
packageMonitor = get(),
syncInstalledAppsUseCase = get(),
translationRepository = get(),
logger = get(),
tweaksRepository = get(),
seenReposRepository = get(),
installationManager = get(),
attestationVerifier = get(),
downloadOrchestrator = get(),
telemetryRepository = get(),
)
}
viewModelOf(::DeveloperProfileViewModel)
viewModelOf(::FavouritesViewModel)
viewModelOf(::HomeViewModel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ import zed.rainxch.core.data.repository.AuthenticationStateImpl
import zed.rainxch.core.data.repository.FavouritesRepositoryImpl
import zed.rainxch.core.data.repository.InstalledAppsRepositoryImpl
import zed.rainxch.core.data.repository.ProxyRepositoryImpl
import zed.rainxch.core.data.repository.DeviceIdentityRepositoryImpl
import zed.rainxch.core.data.repository.RateLimitRepositoryImpl
import zed.rainxch.core.data.repository.SearchHistoryRepositoryImpl
import zed.rainxch.core.data.repository.TelemetryRepositoryImpl
import zed.rainxch.core.data.repository.SeenReposRepositoryImpl
import zed.rainxch.core.data.repository.StarredRepositoryImpl
import zed.rainxch.core.data.repository.TweaksRepositoryImpl
Expand All @@ -42,13 +44,15 @@ import zed.rainxch.core.domain.model.ProxyConfig
import zed.rainxch.core.domain.network.ProxyTester
import zed.rainxch.core.domain.system.DownloadOrchestrator
import zed.rainxch.core.domain.repository.AuthenticationState
import zed.rainxch.core.domain.repository.DeviceIdentityRepository
import zed.rainxch.core.domain.repository.FavouritesRepository
import zed.rainxch.core.domain.repository.InstalledAppsRepository
import zed.rainxch.core.domain.repository.ProxyRepository
import zed.rainxch.core.domain.repository.RateLimitRepository
import zed.rainxch.core.domain.repository.SearchHistoryRepository
import zed.rainxch.core.domain.repository.SeenReposRepository
import zed.rainxch.core.domain.repository.StarredRepository
import zed.rainxch.core.domain.repository.TelemetryRepository
import zed.rainxch.core.domain.repository.TweaksRepository
import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase

Expand Down Expand Up @@ -143,6 +147,23 @@ val coreModule =
BackendApiClient()
}

single<DeviceIdentityRepository> {
DeviceIdentityRepositoryImpl(
preferences = get(),
)
}

single<TelemetryRepository> {
TelemetryRepositoryImpl(
backendApiClient = get(),
deviceIdentity = get(),
tweaksRepository = get(),
platform = get(),
appScope = get(),
logger = get(),
)
}

// Application-scoped download / install orchestrator. Lives
// for the process lifetime so downloads survive screen
// navigation. ViewModels are observers, never owners.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package zed.rainxch.core.data.dto

import kotlinx.serialization.Serializable

@Serializable
data class EventRequest(
val deviceId: String,
val platform: String,
val appVersion: String? = null,
val eventType: String,
val repoId: Long? = null,
val queryHash: String? = null,
val resultCount: Int? = null,
val success: Boolean? = null,
val errorCode: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ import io.ktor.http.isSuccess
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import io.ktor.client.plugins.timeout
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import zed.rainxch.core.data.dto.BackendExploreResponse
import zed.rainxch.core.data.dto.BackendRepoResponse
import zed.rainxch.core.data.dto.BackendSearchResponse
import zed.rainxch.core.data.dto.EventRequest
import kotlin.coroutines.cancellation.CancellationException

class BackendApiClient {
Expand Down Expand Up @@ -104,6 +110,22 @@ class BackendApiClient {
}
}

suspend fun postEvents(events: List<EventRequest>): Result<Unit> =
safeCall {
val response = httpClient.post("events") {
contentType(ContentType.Application.Json)
setBody(events)
}
when {
response.status == HttpStatusCode.NoContent || response.status.isSuccess() ->
Result.success(Unit)
response.status == HttpStatusCode.TooManyRequests ->
Result.failure(RateLimitedException())
else ->
Result.failure(BackendException("HTTP ${response.status.value}"))
}
}

private inline fun <T> safeCall(block: () -> Result<T>): Result<T> =
try {
block()
Expand All @@ -119,3 +141,5 @@ class BackendApiClient {
}

class BackendException(message: String) : Exception(message)

class RateLimitedException : Exception("Rate limited by backend (429)")
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package zed.rainxch.core.data.repository

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
import zed.rainxch.core.domain.repository.DeviceIdentityRepository

@OptIn(ExperimentalUuidApi::class)
class DeviceIdentityRepositoryImpl(
private val preferences: DataStore<Preferences>,
) : DeviceIdentityRepository {

// Serialises the read-check-generate-write sequence so two concurrent
// first callers can't each mint a different UUID and race to persist
// it. DataStore's `edit` alone is atomic per-write but doesn't cover
// the read-then-conditionally-write pattern we need here.
private val deviceIdMutex = Mutex()

override suspend fun getDeviceId(): String =
deviceIdMutex.withLock {
val existing = preferences.data.first()[DEVICE_ID_KEY]
if (!existing.isNullOrBlank()) return existing

val generated = Uuid.random().toString()
preferences.edit { it[DEVICE_ID_KEY] = generated }
generated
}

override suspend fun resetDeviceId(): String =
deviceIdMutex.withLock {
val next = Uuid.random().toString()
preferences.edit { it[DEVICE_ID_KEY] = next }
next
}

private companion object {
private val DEVICE_ID_KEY = stringPreferencesKey("anonymous_device_id")
}
}
Loading