diff --git a/asg-service/src/commonMain/kotlin/io/ashdavies/aggregator/AsgService.kt b/asg-service/src/commonMain/kotlin/io/ashdavies/aggregator/AsgService.kt index 927f593b2..8b19d9c25 100644 --- a/asg-service/src/commonMain/kotlin/io/ashdavies/aggregator/AsgService.kt +++ b/asg-service/src/commonMain/kotlin/io/ashdavies/aggregator/AsgService.kt @@ -19,3 +19,7 @@ public fun AsgService(httpClient: HttpClient): AsgService = object : AsgService return combined.map(transform) } } + +public fun UpcomingConferencesCallable(httpClient: HttpClient): UpcomingConferencesCallable { + return UpcomingConferencesCallable(httpClient, ASG_BASE_URL) +} diff --git a/asg-service/src/commonMain/kotlin/io/ashdavies/aggregator/callable/PastConferencesCallable.kt b/asg-service/src/commonMain/kotlin/io/ashdavies/aggregator/callable/PastConferencesCallable.kt index ec8893c7f..a8553c924 100644 --- a/asg-service/src/commonMain/kotlin/io/ashdavies/aggregator/callable/PastConferencesCallable.kt +++ b/asg-service/src/commonMain/kotlin/io/ashdavies/aggregator/callable/PastConferencesCallable.kt @@ -6,12 +6,13 @@ import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get -internal class PastConferencesCallable( - private val httpClient: HttpClient, - private val baseUrl: String, -) : UnaryCallable> { +internal fun interface PastConferencesCallable : UnaryCallable> - override suspend fun invoke(request: Unit): List = httpClient +internal fun PastConferencesCallable( + httpClient: HttpClient, + baseUrl: String, +) = PastConferencesCallable { _ -> + httpClient .get("https://$baseUrl/conferences/past.json") .body() } diff --git a/asg-service/src/commonMain/kotlin/io/ashdavies/aggregator/callable/UpcomingConferencesCallable.kt b/asg-service/src/commonMain/kotlin/io/ashdavies/aggregator/callable/UpcomingConferencesCallable.kt index e4e91e778..6a7422790 100644 --- a/asg-service/src/commonMain/kotlin/io/ashdavies/aggregator/callable/UpcomingConferencesCallable.kt +++ b/asg-service/src/commonMain/kotlin/io/ashdavies/aggregator/callable/UpcomingConferencesCallable.kt @@ -6,12 +6,13 @@ import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get -internal class UpcomingConferencesCallable( - private val httpClient: HttpClient, - private val baseUrl: String, -) : UnaryCallable> { +public fun interface UpcomingConferencesCallable : UnaryCallable> - override suspend fun invoke(request: Unit): List = httpClient +internal fun UpcomingConferencesCallable( + httpClient: HttpClient, + baseUrl: String, +) = UpcomingConferencesCallable { _ -> + httpClient .get("https://$baseUrl/conferences/upcoming.json") .body() } diff --git a/cloud-run/src/commonMain/kotlin/io/ashdavies/cloud/Identifier.kt b/cloud-run/src/commonMain/kotlin/io/ashdavies/cloud/Identifier.kt index 55869830a..f341c6203 100644 --- a/cloud-run/src/commonMain/kotlin/io/ashdavies/cloud/Identifier.kt +++ b/cloud-run/src/commonMain/kotlin/io/ashdavies/cloud/Identifier.kt @@ -1,27 +1,19 @@ package io.ashdavies.cloud -import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import kotlinx.serialization.serializer import okio.ByteString.Companion.encode -internal interface Identifier : (T) -> String +internal fun interface Identifier : (T) -> String -internal class HashIdentifier( - private val serializer: SerializationStrategy, -) : Identifier { - - private val cache = mutableMapOf() - - override fun invoke(value: T): String = cache.getOrPut(value) { +internal inline fun Identifier( + cache: MutableMap = mutableMapOf(), +) = Identifier { value -> + cache.getOrPut(value) { Json - .encodeToString(serializer, value) + .encodeToString(value) .encode() .md5() .hex() } } - -internal inline fun Identifier(): Identifier { - return HashIdentifier(serializer()) -} diff --git a/conferences-app/build.gradle.kts b/conferences-app/build.gradle.kts index 813810b7a..4521cf075 100644 --- a/conferences-app/build.gradle.kts +++ b/conferences-app/build.gradle.kts @@ -87,6 +87,7 @@ kotlin { commonMain.dependencies { implementation(projects.analytics) implementation(projects.appCheck.appCheckClient) + implementation(projects.asgService) implementation(projects.circuitSupport) implementation(projects.composeMaterial) implementation(projects.httpClient) @@ -116,6 +117,7 @@ kotlin { implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.serialization.core) + implementation(libs.kotlinx.serialization.json) implementation(libs.ktor.client.core) implementation(libs.ktor.client.mock) implementation(libs.ktor.http) diff --git a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventPager.kt b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventPager.kt deleted file mode 100644 index c61062c37..000000000 --- a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventPager.kt +++ /dev/null @@ -1,43 +0,0 @@ -package io.ashdavies.party.events - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.paging.ExperimentalPagingApi -import androidx.paging.InvalidatingPagingSourceFactory -import androidx.paging.Pager -import androidx.paging.PagingConfig -import io.ashdavies.http.LocalHttpClient -import io.ashdavies.party.network.todayAsString -import io.ashdavies.party.sql.rememberLocalQueries - -private const val PLAYGROUND_BASE_URL = "playground.ashdavies.dev" -private const val DEFAULT_PAGE_SIZE = 10 - -@Composable -@ExperimentalPagingApi -internal fun rememberEventPager( - eventsQueries: EventsQueries = rememberLocalQueries { it.eventsQueries }, - eventsCallable: UpcomingEventsCallable = UpcomingEventsCallable( - httpClient = LocalHttpClient.current, - baseUrl = PLAYGROUND_BASE_URL, - ), - initialKey: String = todayAsString(), - pageSize: Int = DEFAULT_PAGE_SIZE, -): Pager = remember(eventsQueries, eventsCallable) { - val pagingSourceFactory = InvalidatingPagingSourceFactory { - EventsPagingSource(eventsQueries) - } - - val remoteMediator = EventsRemoteMediator( - eventsQueries = eventsQueries, - eventsCallable = eventsCallable, - onInvalidate = pagingSourceFactory::invalidate, - ) - - Pager( - config = PagingConfig(pageSize), - initialKey = initialKey, - remoteMediator = remoteMediator, - pagingSourceFactory = pagingSourceFactory, - ) -} diff --git a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsPresenter.kt b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsPresenter.kt index 1572783b2..58ad1df0c 100644 --- a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsPresenter.kt +++ b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsPresenter.kt @@ -8,6 +8,7 @@ import androidx.paging.Pager import androidx.paging.cachedIn import com.slack.circuit.retained.rememberRetained import io.ashdavies.paging.collectAsLazyPagingItems +import io.ashdavies.party.events.paging.rememberEventPager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job diff --git a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/GetUpcomingEventsCallable.kt b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/callable/PagedUpcomingEventsCallable.kt similarity index 74% rename from conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/GetUpcomingEventsCallable.kt rename to conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/callable/PagedUpcomingEventsCallable.kt index 6f3cba83c..fd6ec442b 100644 --- a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/GetUpcomingEventsCallable.kt +++ b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/callable/PagedUpcomingEventsCallable.kt @@ -1,4 +1,4 @@ -package io.ashdavies.party.events +package io.ashdavies.party.events.callable import io.ashdavies.http.UnaryCallable import io.ashdavies.http.throwClientRequestExceptionAs @@ -11,18 +11,19 @@ import io.ashdavies.http.common.models.Event as ApiEvent private const val NETWORK_PAGE_SIZE = 100 +internal fun interface PagedUpcomingEventsCallable : UnaryCallable> + @Serializable internal data class GetEventsRequest( val startAt: String? = null, val limit: Int = NETWORK_PAGE_SIZE, ) -internal class UpcomingEventsCallable( +internal fun PagedUpcomingEventsCallable( httpClient: HttpClient, - private val baseUrl: String, -) : UnaryCallable> { - - private val httpClient = httpClient.config { + baseUrl: String, +): PagedUpcomingEventsCallable { + val errorHandlingHttpClient = httpClient.config { install(HttpCallValidator) { throwClientRequestExceptionAs() } @@ -30,13 +31,13 @@ internal class UpcomingEventsCallable( expectSuccess = true } - override suspend fun invoke(request: GetEventsRequest): List { + return PagedUpcomingEventsCallable { request -> val queryAsString = buildList { if (request.startAt != null) add("startAt=${request.startAt}") add("limit=${request.limit}") }.joinToString("&") - return httpClient + errorHandlingHttpClient .get("https://$baseUrl/events/upcoming?$queryAsString") .body() } diff --git a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/paging/EventPager.kt b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/paging/EventPager.kt new file mode 100644 index 000000000..3939097bc --- /dev/null +++ b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/paging/EventPager.kt @@ -0,0 +1,81 @@ +package io.ashdavies.party.events.paging + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.paging.ExperimentalPagingApi +import androidx.paging.InvalidatingPagingSourceFactory +import androidx.paging.Pager +import androidx.paging.PagingConfig +import io.ashdavies.aggregator.AsgConference +import io.ashdavies.aggregator.UpcomingConferencesCallable +import io.ashdavies.config.RemoteConfig +import io.ashdavies.config.getBoolean +import io.ashdavies.http.LocalHttpClient +import io.ashdavies.http.common.models.EventCfp +import io.ashdavies.party.events.EventsQueries +import io.ashdavies.party.events.callable.PagedUpcomingEventsCallable +import io.ashdavies.party.network.todayAsString +import io.ashdavies.party.sql.rememberLocalQueries +import io.ktor.client.HttpClient +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okio.ByteString.Companion.encode +import io.ashdavies.http.common.models.Event as ApiEvent +import io.ashdavies.party.events.Event as DatabaseEvent + +private const val PLAYGROUND_BASE_URL = "playground.ashdavies.dev" +private const val DEFAULT_PAGE_SIZE = 10 + +private suspend fun RemoteConfig.isPagingEnabled() = getBoolean("paging_enabled") + +@Composable +@ExperimentalPagingApi +internal fun rememberEventPager( + eventsQueries: EventsQueries = rememberLocalQueries { it.eventsQueries }, + eventsCallable: PagedUpcomingEventsCallable = rememberUpcomingEventsCallable(), + initialKey: String = todayAsString(), + pageSize: Int = DEFAULT_PAGE_SIZE, +): Pager = remember(eventsQueries, eventsCallable) { + val pagingSourceFactory = InvalidatingPagingSourceFactory { + EventsPagingSource(eventsQueries) + } + + val remoteMediator = EventsRemoteMediator( + eventsQueries = eventsQueries, + eventsCallable = eventsCallable, + onInvalidate = pagingSourceFactory::invalidate, + ) + + Pager( + config = PagingConfig(pageSize), + initialKey = initialKey, + remoteMediator = remoteMediator, + pagingSourceFactory = pagingSourceFactory, + ) +} + +@Composable +private fun rememberUpcomingEventsCallable( + httpClient: HttpClient = LocalHttpClient.current, + remoteConfig: RemoteConfig = RemoteConfig, +): PagedUpcomingEventsCallable { + val pagedCallable by lazy { PagedUpcomingEventsCallable(httpClient, PLAYGROUND_BASE_URL) } + val asgCallable by lazy { UpcomingConferencesCallable(httpClient) } + + return PagedUpcomingEventsCallable { request -> + when { + remoteConfig.isPagingEnabled() -> pagedCallable(request) + else -> asgCallable(Unit).map(AsgConference::toEvent) + } + } +} + +private fun AsgConference.toEvent(): ApiEvent = ApiEvent( + id = hash(), name = name, website = website, location = location, dateStart = dateStart, + dateEnd = dateEnd, imageUrl = imageUrl, status = status, online = online, + cfp = cfp?.let { EventCfp(start = it.start, end = it.end, site = it.site) }, +) + +private inline fun T.hash() = Json + .encodeToString(this) + .encode().md5().hex() diff --git a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsPagingSource.kt b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/paging/EventsPagingSource.kt similarity index 88% rename from conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsPagingSource.kt rename to conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/paging/EventsPagingSource.kt index 243376496..c929bca9b 100644 --- a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsPagingSource.kt +++ b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/paging/EventsPagingSource.kt @@ -1,7 +1,9 @@ -package io.ashdavies.party.events +package io.ashdavies.party.events.paging import androidx.paging.PagingSource import androidx.paging.PagingState +import io.ashdavies.party.events.Event +import io.ashdavies.party.events.EventsQueries import io.ashdavies.party.network.todayAsString internal class EventsPagingSource(private val queries: EventsQueries) : PagingSource() { diff --git a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsRemoteMediator.kt b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/paging/EventsRemoteMediator.kt similarity index 86% rename from conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsRemoteMediator.kt rename to conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/paging/EventsRemoteMediator.kt index 8a7fe98f1..e3c7dc785 100644 --- a/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/EventsRemoteMediator.kt +++ b/conferences-app/src/commonMain/kotlin/io/ashdavies/party/events/paging/EventsRemoteMediator.kt @@ -1,9 +1,13 @@ -package io.ashdavies.party.events +package io.ashdavies.party.events.paging import androidx.paging.ExperimentalPagingApi import androidx.paging.LoadType import androidx.paging.PagingState import androidx.paging.RemoteMediator +import io.ashdavies.party.events.EventsQueries +import io.ashdavies.party.events.callable.GetEventsError +import io.ashdavies.party.events.callable.GetEventsRequest +import io.ashdavies.party.events.callable.PagedUpcomingEventsCallable import io.ktor.client.network.sockets.SocketTimeoutException import io.ashdavies.http.common.models.Event as ApiEvent import io.ashdavies.party.events.Event as DatabaseEvent @@ -11,7 +15,7 @@ import io.ashdavies.party.events.Event as DatabaseEvent @OptIn(ExperimentalPagingApi::class) internal class EventsRemoteMediator( private val eventsQueries: EventsQueries, - private val eventsCallable: UpcomingEventsCallable, + private val eventsCallable: PagedUpcomingEventsCallable, private val onInvalidate: () -> Unit, ) : RemoteMediator() { @@ -50,7 +54,7 @@ private fun endOfPaginationReached(): RemoteMediator.MediatorResult { return RemoteMediator.MediatorResult.Success(endOfPaginationReached = true) } -private suspend fun UpcomingEventsCallable.result( +private suspend fun PagedUpcomingEventsCallable.result( request: GetEventsRequest, ): CallableResult> = try { CallableResult.Success(invoke(request))