From b1543e30756d6845ad7c407e3a04a180541d7ef8 Mon Sep 17 00:00:00 2001 From: DatLag Date: Fri, 3 May 2024 14:36:11 +0200 Subject: [PATCH] added caching and fallback request --- .../aniflow/anilist/AiringTodayRepository.kt | 36 +++++++++++- .../dev/datlag/aniflow/anilist/Cache.kt | 57 ------------------- .../anilist/PopularNextSeasonRepository.kt | 34 ++++++++++- .../anilist/PopularSeasonRepository.kt | 34 ++++++++++- .../aniflow/anilist/TrendingRepository.kt | 34 ++++++++++- composeApp/build.gradle.kts | 3 + .../aniflow/module/PlatformModule.android.kt | 8 +++ .../datlag/aniflow/module/NetworkModule.kt | 9 +++ .../dev/datlag/aniflow/other/Constants.kt | 1 + .../aniflow/module/PlatformModule.ios.kt | 4 -- gradle/libs.versions.toml | 2 + 11 files changed, 153 insertions(+), 69 deletions(-) diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt index ca2728c..7813454 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/AiringTodayRepository.kt @@ -10,6 +10,7 @@ import kotlin.time.Duration.Companion.hours class AiringTodayRepository( private val apolloClient: ApolloClient, + private val fallbackClient: ApolloClient, private val nsfw: Flow = flowOf(false), ) { @@ -24,8 +25,39 @@ class AiringTodayRepository( private val airingPreFilter = query.transform { return@transform emitAll(apolloClient.query(it.toGraphQL()).toFlow()) } - val airing = combine(airingPreFilter, nsfw) { q, n -> - State.fromGraphQL(q.data, n) + private val fallbackPreFilter = query.transform { + return@transform emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) + } + private val fallbackQuery = combine(fallbackPreFilter, nsfw.distinctUntilChanged()) { q, n -> + val data = q.data + if (data == null) { + if (q.hasErrors()) { + State.fromGraphQL(data, n) + } else { + null + } + } else { + State.fromGraphQL(data, n) + } + }.filterNotNull() + + val airing = combine(airingPreFilter, nsfw.distinctUntilChanged()) { q, n -> + val data = q.data + if (data == null) { + if (q.hasErrors()) { + State.fromGraphQL(data, n) + } else { + null + } + } else { + State.fromGraphQL(data, n) + } + }.filterNotNull().transform { + return@transform if (it is State.Error) { + emitAll(fallbackQuery) + } else { + emit(it) + } } fun nextPage() = page.getAndUpdate { diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt index d641923..79f2850 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/Cache.kt @@ -8,27 +8,6 @@ import dev.datlag.tooling.async.suspendCatching import kotlin.time.Duration.Companion.hours internal object Cache { - private val trendingAnime = InMemoryKache( - maxSize = 5L * 1024 * 1024, - ) { - strategy = KacheStrategy.LRU - expireAfterWriteDuration = 2.hours - } - - private val airing = InMemoryKache( - maxSize = 5L * 1024 * 1024 - ) { - strategy = KacheStrategy.LRU - expireAfterWriteDuration = 1.hours - } - - private val season = InMemoryKache( - maxSize = 5L * 1024 * 1024 - ) { - strategy = KacheStrategy.LRU - expireAfterWriteDuration = 2.hours - } - private val medium = InMemoryKache( maxSize = 10L * 1024 * 1024 ) { @@ -43,42 +22,6 @@ internal object Cache { expireAfterWriteDuration = 2.hours } - suspend fun getTrending(key: TrendingQuery): TrendingQuery.Data? { - return suspendCatching { - trendingAnime.getIfAvailable(key) - }.getOrNull() - } - - suspend fun setTrending(key: TrendingQuery, data: TrendingQuery.Data): TrendingQuery.Data { - return suspendCatching { - trendingAnime.put(key, data) - }.getOrNull() ?: data - } - - suspend fun getAiring(key: AiringQuery): AiringQuery.Data? { - return suspendCatching { - airing.getIfAvailable(key) - }.getOrNull() - } - - suspend fun setAiring(key: AiringQuery, data: AiringQuery.Data): AiringQuery.Data { - return suspendCatching { - airing.put(key, data) - }.getOrNull() ?: data - } - - suspend fun getSeason(key: SeasonQuery): SeasonQuery.Data? { - return suspendCatching { - season.getIfAvailable(key) - }.getOrNull() - } - - suspend fun setSeason(key: SeasonQuery, data: SeasonQuery.Data): SeasonQuery.Data { - return suspendCatching { - season.put(key, data) - }.getOrNull() ?: data - } - suspend fun getMedium(key: MediumQuery): Medium? { return suspendCatching { medium.getIfAvailable(key) diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt index dea95f0..c53e697 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularNextSeasonRepository.kt @@ -12,6 +12,7 @@ import kotlinx.datetime.Clock class PopularNextSeasonRepository( private val apolloClient: ApolloClient, + private val fallbackClient: ApolloClient, private val nsfw: Flow = flowOf(false), ) { @@ -28,11 +29,40 @@ class PopularNextSeasonRepository( year = year ) } + private val fallbackQuery = query.transform { + return@transform emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) + }.mapNotNull { + val data = it.data + if (data == null) { + if (it.hasErrors()) { + SeasonState.fromGraphQL(data) + } else { + null + } + } else { + SeasonState.fromGraphQL(data) + } + } val popularNextSeason = query.transform { return@transform emitAll(apolloClient.query(it.toGraphQL()).toFlow()) - }.map { - SeasonState.fromGraphQL(it.data) + }.mapNotNull { + val data = it.data + if (data == null) { + if (it.hasErrors()) { + SeasonState.fromGraphQL(data) + } else { + null + } + } else { + SeasonState.fromGraphQL(data) + } + }.transform { + return@transform if (it is SeasonState.Error) { + emitAll(fallbackQuery) + } else { + emit(it) + } } fun nextPage() = page.getAndUpdate { diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt index e473da1..a1f9f22 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/PopularSeasonRepository.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.* class PopularSeasonRepository( private val apolloClient: ApolloClient, + private val fallbackClient: ApolloClient, private val nsfw: Flow = flowOf(false), ) { @@ -21,11 +22,40 @@ class PopularSeasonRepository( nsfw = n ) } + private val fallbackQuery = query.transform { + return@transform emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) + }.mapNotNull { + val data = it.data + if (data == null) { + if (it.hasErrors()) { + SeasonState.fromGraphQL(data) + } else { + null + } + } else { + SeasonState.fromGraphQL(data) + } + } val popularThisSeason = query.transform { return@transform emitAll(apolloClient.query(it.toGraphQL()).toFlow()) - }.map { - SeasonState.fromGraphQL(it.data) + }.mapNotNull { + val data = it.data + if (data == null) { + if (it.hasErrors()) { + SeasonState.fromGraphQL(data) + } else { + null + } + } else { + SeasonState.fromGraphQL(data) + } + }.transform { + return@transform if (it is SeasonState.Error) { + emitAll(fallbackQuery) + } else { + emit(it) + } } fun nextPage() = page.getAndUpdate { diff --git a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt index 74144b2..89b85cc 100644 --- a/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt +++ b/anilist/src/commonMain/kotlin/dev/datlag/aniflow/anilist/TrendingRepository.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.* class TrendingRepository( private val apolloClient: ApolloClient, + private val fallbackClient: ApolloClient, private val nsfw: Flow = flowOf(false), ) { @@ -21,11 +22,40 @@ class TrendingRepository( nsfw = n ) } + private val fallbackQuery = query.transform { + return@transform emitAll(fallbackClient.query(it.toGraphQL()).toFlow()) + }.mapNotNull { + val data = it.data + if (data == null) { + if (it.hasErrors()) { + State.fromGraphQL(data) + } else { + null + } + } else { + State.fromGraphQL(data) + } + } val trending = query.transform { return@transform emitAll(apolloClient.query(it.toGraphQL()).toFlow()) - }.map { - State.fromGraphQL(it.data) + }.mapNotNull { + val data = it.data + if (data == null) { + if (it.hasErrors()) { + State.fromGraphQL(data) + } else { + null + } + } else { + State.fromGraphQL(data) + } + }.transform { + return@transform if (it is State.Error) { + emitAll(fallbackQuery) + } else { + emit(it) + } } fun nextPage() = page.getAndUpdate { diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index ddf232b..7dfe004 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -110,6 +110,9 @@ kotlin { implementation(libs.kache) implementation(libs.markdown.renderer) + implementation(libs.apollo.cache) + implementation(libs.apollo.cache.sql) + implementation("dev.datlag.sheets-compose-dialogs:rating:2.0.0-SNAPSHOT") implementation("dev.datlag.sheets-compose-dialogs:option:2.0.0-SNAPSHOT") diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/module/PlatformModule.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/module/PlatformModule.android.kt index 98029e2..d0fd305 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/module/PlatformModule.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/module/PlatformModule.android.kt @@ -6,6 +6,9 @@ import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.okio.OkioStorage import coil3.ImageLoader import coil3.request.allowHardware +import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory +import com.apollographql.apollo3.cache.normalized.api.NormalizedCacheFactory +import com.apollographql.apollo3.cache.normalized.sql.SqlNormalizedCacheFactory import dev.datlag.aniflow.BuildKonfig import dev.datlag.aniflow.Sekret import dev.datlag.aniflow.firebase.FirebaseFactory @@ -120,6 +123,11 @@ actual object PlatformModule { bindSingleton { BurningSeriesResolver(context = instance()) } + bindSingleton(Constants.AniList.CACHE_FACTORY) { + MemoryCacheFactory(maxSizeBytes = 25 * 1024 * 1024).chain( + SqlNormalizedCacheFactory(context = instance(), name = "anilist.db") + ) + } } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt index cd26277..e769b92 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -10,6 +10,9 @@ import coil3.svg.SvgDecoder import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.api.http.HttpRequest import com.apollographql.apollo3.api.http.HttpResponse +import com.apollographql.apollo3.cache.normalized.api.MemoryCacheFactory +import com.apollographql.apollo3.cache.normalized.api.NormalizedCacheFactory +import com.apollographql.apollo3.cache.normalized.normalizedCache import com.apollographql.apollo3.network.http.HttpInterceptor import com.apollographql.apollo3.network.http.HttpInterceptorChain import de.jensklingenberg.ktorfit.Ktorfit @@ -81,12 +84,14 @@ data object NetworkModule { return chain.proceed(req) } }) + .normalizedCache(instance(Constants.AniList.CACHE_FACTORY)) .build() } bindSingleton(Constants.AniList.FALLBACK_APOLLO_CLIENT) { ApolloClient.Builder() .dispatcher(ioDispatcher()) .serverUrl(Constants.AniList.SERVER_URL) + .normalizedCache(instance(Constants.AniList.CACHE_FACTORY)) .build() } bindSingleton { @@ -119,6 +124,7 @@ data object NetworkModule { TrendingRepository( apolloClient = instance(Constants.AniList.APOLLO_CLIENT), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), nsfw = appSettings.adultContent ) } @@ -127,6 +133,7 @@ data object NetworkModule { AiringTodayRepository( apolloClient = instance(Constants.AniList.APOLLO_CLIENT), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), nsfw = appSettings.adultContent ) } @@ -135,6 +142,7 @@ data object NetworkModule { PopularSeasonRepository( apolloClient = instance(Constants.AniList.APOLLO_CLIENT), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), nsfw = appSettings.adultContent ) } @@ -143,6 +151,7 @@ data object NetworkModule { PopularNextSeasonRepository( apolloClient = instance(Constants.AniList.APOLLO_CLIENT), + fallbackClient = instance(Constants.AniList.FALLBACK_APOLLO_CLIENT), nsfw = appSettings.adultContent ) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/Constants.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/Constants.kt index 002fd71..85b1f94 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/Constants.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/Constants.kt @@ -7,6 +7,7 @@ data object Constants { data object AniList { const val SERVER_URL = "https://graphql.anilist.co/" + const val CACHE_FACTORY = "AniListCacheFactory" const val APOLLO_CLIENT = "AniListApolloClient" const val FALLBACK_APOLLO_CLIENT = "FallbackAniListApolloClient" diff --git a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/module/PlatformModule.ios.kt b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/module/PlatformModule.ios.kt index 5cbadcf..786a266 100644 --- a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/module/PlatformModule.ios.kt +++ b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/module/PlatformModule.ios.kt @@ -13,7 +13,6 @@ import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json import org.kodein.di.* -import org.publicvalue.multiplatform.oidc.appsupport.IosCodeAuthFlowFactory actual object PlatformModule { @@ -51,9 +50,6 @@ actual object PlatformModule { localLogger = instanceOrNull() ) } - bindEagerSingleton { - IosCodeAuthFlowFactory() - } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b6b6591..f672dca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,6 +57,8 @@ android-credentials-play-services = { group = "androidx.credentials", name = "cr activity = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" } activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" } apollo = { group = "com.apollographql.apollo3", name = "apollo-runtime", version.ref = "apollo" } +apollo-cache = { group = "com.apollographql.apollo3", name = "apollo-normalized-cache", version.ref = "apollo" } +apollo-cache-sql = { group = "com.apollographql.apollo3", name = "apollo-normalized-cache-sqlite", version.ref = "apollo" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } atomicfu = { group = "org.jetbrains.kotlinx", name = "atomicfu-gradle-plugin", version.ref = "atomicfu" } coil = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil" }