Skip to content

Commit

Permalink
Make paging an opt in feature (#1148)
Browse files Browse the repository at this point in the history
* Make paging an opt in feature

* Change to when comparison

---------

Co-authored-by: Ashley Davies <[email protected]>
  • Loading branch information
ashdavies and ashdavies authored Sep 2, 2024
1 parent c7b3813 commit 5b29a70
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Unit, List<AsgConference>> {
internal fun interface PastConferencesCallable : UnaryCallable<Unit, List<AsgConference>>

override suspend fun invoke(request: Unit): List<AsgConference> = httpClient
internal fun PastConferencesCallable(
httpClient: HttpClient,
baseUrl: String,
) = PastConferencesCallable { _ ->
httpClient
.get("https://$baseUrl/conferences/past.json")
.body()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Unit, List<AsgConference>> {
public fun interface UpcomingConferencesCallable : UnaryCallable<Unit, List<AsgConference>>

override suspend fun invoke(request: Unit): List<AsgConference> = httpClient
internal fun UpcomingConferencesCallable(
httpClient: HttpClient,
baseUrl: String,
) = UpcomingConferencesCallable { _ ->
httpClient
.get("https://$baseUrl/conferences/upcoming.json")
.body()
}
22 changes: 7 additions & 15 deletions cloud-run/src/commonMain/kotlin/io/ashdavies/cloud/Identifier.kt
Original file line number Diff line number Diff line change
@@ -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 : Any> : (T) -> String
internal fun interface Identifier<T : Any> : (T) -> String

internal class HashIdentifier<T : Any>(
private val serializer: SerializationStrategy<T>,
) : Identifier<T> {

private val cache = mutableMapOf<T, String>()

override fun invoke(value: T): String = cache.getOrPut(value) {
internal inline fun <reified T : Any> Identifier(
cache: MutableMap<T, String> = mutableMapOf(),
) = Identifier<T> { value ->
cache.getOrPut(value) {
Json
.encodeToString(serializer, value)
.encodeToString(value)
.encode()
.md5()
.hex()
}
}

internal inline fun <reified T : Any> Identifier(): Identifier<T> {
return HashIdentifier(serializer())
}
2 changes: 2 additions & 0 deletions conferences-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,32 +11,33 @@ import io.ashdavies.http.common.models.Event as ApiEvent

private const val NETWORK_PAGE_SIZE = 100

internal fun interface PagedUpcomingEventsCallable : UnaryCallable<GetEventsRequest, List<ApiEvent>>

@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<GetEventsRequest, List<ApiEvent>> {

private val httpClient = httpClient.config {
baseUrl: String,
): PagedUpcomingEventsCallable {
val errorHandlingHttpClient = httpClient.config {
install(HttpCallValidator) {
throwClientRequestExceptionAs<GetEventsError>()
}

expectSuccess = true
}

override suspend fun invoke(request: GetEventsRequest): List<ApiEvent> {
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()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, DatabaseEvent> = 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 <reified T : Any> T.hash() = Json
.encodeToString(this)
.encode().md5().hex()
Original file line number Diff line number Diff line change
@@ -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<String, Event>() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
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

@OptIn(ExperimentalPagingApi::class)
internal class EventsRemoteMediator(
private val eventsQueries: EventsQueries,
private val eventsCallable: UpcomingEventsCallable,
private val eventsCallable: PagedUpcomingEventsCallable,
private val onInvalidate: () -> Unit,
) : RemoteMediator<String, DatabaseEvent>() {

Expand Down Expand Up @@ -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<List<ApiEvent>> = try {
CallableResult.Success(invoke(request))
Expand Down

0 comments on commit 5b29a70

Please sign in to comment.