diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c348f7ab..309575e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotli = "0.4.7" +kotli = "0.5.1" kotlin = "1.9.23" kotlinxCoroutines = "1.8.0" logback = "1.5.6" diff --git a/processor/build.gradle b/processor/build.gradle index d8c8dcd5..a1cd5357 100644 --- a/processor/build.gradle +++ b/processor/build.gradle @@ -1,4 +1,4 @@ apply from: "${project.rootDir}/gradle/kotlin/processor.gradle" group = 'com.kotlitecture.kotli' -version = '0.5.0' \ No newline at end of file +version = '0.5.1' \ No newline at end of file diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/Tags.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/Tags.kt new file mode 100644 index 00000000..b58aaa8e --- /dev/null +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/Tags.kt @@ -0,0 +1,37 @@ +package kotli.template.multiplatform.compose + +import kotli.engine.model.FeatureTags + +object Tags { + + val All = listOf( + FeatureTags.Android, + FeatureTags.IOS, + FeatureTags.Web, + FeatureTags.Desktop, + FeatureTags.Server, + ) + + val AllClients = listOf( + FeatureTags.Android, + FeatureTags.IOS, + FeatureTags.Web, + FeatureTags.Desktop, + ) + + val Mobile = listOf( + FeatureTags.Android, + FeatureTags.IOS + ) + + val MobileAndDesktop = listOf( + FeatureTags.Android, + FeatureTags.IOS, + FeatureTags.Desktop, + ) + + val WebAndDesktop = listOf( + FeatureTags.Desktop, + ) + +} \ No newline at end of file diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/analytics/facade/FacadeAnalyticsProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/analytics/facade/FacadeAnalyticsProcessor.kt index e7ecdff2..a1d31322 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/analytics/facade/FacadeAnalyticsProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/analytics/facade/FacadeAnalyticsProcessor.kt @@ -1,10 +1,12 @@ package kotli.template.multiplatform.compose.dataflow.analytics.facade import kotli.engine.BaseFeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.RemoveMarkedLine import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotlin.time.Duration.Companion.minutes object FacadeAnalyticsProcessor : BaseFeatureProcessor() { @@ -12,6 +14,7 @@ object FacadeAnalyticsProcessor : BaseFeatureProcessor() { const val ID = "dataflow.analytics.facade" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 30.minutes.inWholeMilliseconds override fun doRemove(state: TemplateState) { diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/cache/basic/BasicCacheProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/cache/basic/BasicCacheProcessor.kt index b3fa2a3e..0aab4ba4 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/cache/basic/BasicCacheProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/cache/basic/BasicCacheProcessor.kt @@ -2,12 +2,14 @@ package kotli.template.multiplatform.compose.dataflow.cache.basic import kotli.engine.BaseFeatureProcessor import kotli.engine.FeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.VersionCatalogRules import kotli.engine.template.rule.CleanupMarkedLine import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.RemoveMarkedLine import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.common.CommonStatelyProcessor import kotli.template.multiplatform.compose.showcases.datasource.cache.CacheShowcasesProcessor import kotlin.time.Duration.Companion.hours @@ -17,6 +19,7 @@ object BasicCacheProcessor : BaseFeatureProcessor() { const val ID = "dataflow.cache.basic" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 8.hours.inWholeMilliseconds override fun dependencies(): List> = listOf( CacheShowcasesProcessor::class.java, diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/config/facade/FacadeConfigProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/config/facade/FacadeConfigProcessor.kt index 2aef02a0..17d51b8a 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/config/facade/FacadeConfigProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/config/facade/FacadeConfigProcessor.kt @@ -1,10 +1,12 @@ package kotli.template.multiplatform.compose.dataflow.config.facade import kotli.engine.BaseFeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.RemoveMarkedLine import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotlin.time.Duration.Companion.minutes object FacadeConfigProcessor : BaseFeatureProcessor() { @@ -12,6 +14,7 @@ object FacadeConfigProcessor : BaseFeatureProcessor() { const val ID = "dataflow.config.facade" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 30.minutes.inWholeMilliseconds override fun doRemove(state: TemplateState) { diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/database/sqldelight/SqlDelightProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/database/sqldelight/SqlDelightProcessor.kt index 717c5e73..8c9fceaa 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/database/sqldelight/SqlDelightProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/database/sqldelight/SqlDelightProcessor.kt @@ -2,6 +2,7 @@ package kotli.template.multiplatform.compose.dataflow.database.sqldelight import kotli.engine.BaseFeatureProcessor import kotli.engine.FeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.VersionCatalogRules import kotli.engine.template.rule.CleanupMarkedBlock @@ -11,6 +12,7 @@ import kotli.engine.template.rule.RemoveMarkedBlock import kotli.engine.template.rule.RemoveMarkedLine import kotli.engine.template.rule.RenamePackage import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.common.CommonStatelyProcessor import kotli.template.multiplatform.compose.dataflow.paging.cashapp.CashAppPagingProcessor import kotlin.time.Duration.Companion.hours @@ -20,6 +22,7 @@ object SqlDelightProcessor : BaseFeatureProcessor() { const val ID = "dataflow.database.sqldelight" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getWebUrl(state: TemplateState): String = "https://cashapp.github.io/sqldelight/" override fun getIntegrationUrl(state: TemplateState): String = "https://cashapp.github.io/sqldelight/2.0.2/multiplatform_sqlite/" diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/http/ktor/KtorHttpProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/http/ktor/KtorHttpProcessor.kt index ecf67fd8..4bcda5a4 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/http/ktor/KtorHttpProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/http/ktor/KtorHttpProcessor.kt @@ -2,11 +2,13 @@ package kotli.template.multiplatform.compose.dataflow.http.ktor import kotli.engine.BaseFeatureProcessor import kotli.engine.FeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.VersionCatalogRules import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.RemoveMarkedLine import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.common.CommonKtorProcessor import kotlin.time.Duration.Companion.hours @@ -15,6 +17,7 @@ object KtorHttpProcessor : BaseFeatureProcessor() { const val ID = "dataflow.http.ktor" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getWebUrl(state: TemplateState): String = "https://ktor.io" override fun getIntegrationUrl(state: TemplateState): String = "https://ktor.io/docs/client-create-new-application.html" override fun getIntegrationEstimate(state: TemplateState): Long = 2.hours.inWholeMilliseconds diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/keyvalue/settings/SettingsKeyValueProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/keyvalue/settings/SettingsKeyValueProcessor.kt index bdc0ea95..1d99e61e 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/keyvalue/settings/SettingsKeyValueProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/keyvalue/settings/SettingsKeyValueProcessor.kt @@ -2,12 +2,14 @@ package kotli.template.multiplatform.compose.dataflow.keyvalue.settings import kotli.engine.BaseFeatureProcessor import kotli.engine.FeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.VersionCatalogRules import kotli.engine.template.rule.CleanupMarkedLine import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.RemoveMarkedLine import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.dataflow.keyvalue.common.CommonKeyValueProcessor import kotlin.time.Duration.Companion.minutes @@ -16,6 +18,7 @@ object SettingsKeyValueProcessor : BaseFeatureProcessor() { const val ID = "dataflow.keyvalue.settings" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getWebUrl(state: TemplateState): String = "https://github.com/russhwolf/multiplatform-settings" override fun getIntegrationUrl(state: TemplateState): String = "https://github.com/russhwolf/multiplatform-settings?tab=readme-ov-file#no-arg-module" override fun getIntegrationEstimate(state: TemplateState): Long = 30.minutes.inWholeMilliseconds diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/paging/cashapp/CashAppPagingProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/paging/cashapp/CashAppPagingProcessor.kt index 06af807f..79df800c 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/paging/cashapp/CashAppPagingProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/dataflow/paging/cashapp/CashAppPagingProcessor.kt @@ -1,11 +1,13 @@ package kotli.template.multiplatform.compose.dataflow.paging.cashapp import kotli.engine.BaseFeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.VersionCatalogRules import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.RemoveMarkedLine import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotlin.time.Duration.Companion.minutes object CashAppPagingProcessor : BaseFeatureProcessor() { @@ -13,6 +15,7 @@ object CashAppPagingProcessor : BaseFeatureProcessor() { const val ID = "dataflow.paging.jetpack" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getWebUrl(state: TemplateState): String = "https://github.com/cashapp/multiplatform-paging" override fun getIntegrationUrl(state: TemplateState): String = "https://github.com/cashapp/multiplatform-paging?tab=readme-ov-file#usage" override fun getIntegrationEstimate(state: TemplateState): Long = 30.minutes.inWholeMilliseconds diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/essentials/di/koin/KoinProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/essentials/di/koin/KoinProcessor.kt index e65cbf63..cf52392f 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/essentials/di/koin/KoinProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/essentials/di/koin/KoinProcessor.kt @@ -6,11 +6,13 @@ import kotlin.time.Duration.Companion.hours object KoinProcessor : BaseFeatureProcessor() { - const val ID = "essentials.di.koin" + const val ID = "essentials.di.koin" override fun getId(): String = ID override fun getWebUrl(state: TemplateState): String = "https://insert-koin.io" - override fun getIntegrationUrl(state: TemplateState): String = "https://insert-koin.io/docs/reference/koin-mp/kmp" + override fun getIntegrationUrl(state: TemplateState): String = + "https://insert-koin.io/docs/reference/koin-mp/kmp" + override fun getIntegrationEstimate(state: TemplateState): Long = 1.hours.inWholeMilliseconds } \ No newline at end of file diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/testing/logging/kermit/KermitProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/testing/logging/kermit/KermitProcessor.kt index 6f484ebe..2cb07154 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/testing/logging/kermit/KermitProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/testing/logging/kermit/KermitProcessor.kt @@ -1,10 +1,12 @@ package kotli.template.multiplatform.compose.testing.logging.kermit import kotli.engine.BaseFeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.VersionCatalogRules import kotli.engine.template.rule.RemoveMarkedLine import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotlin.time.Duration.Companion.minutes object KermitProcessor : BaseFeatureProcessor() { @@ -12,6 +14,7 @@ object KermitProcessor : BaseFeatureProcessor() { const val ID = "testing.logging.kermit" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getWebUrl(state: TemplateState): String = "https://kermit.touchlab.co" override fun getIntegrationUrl(state: TemplateState): String = "https://kermit.touchlab.co/docs/#getting-started" override fun getIntegrationEstimate(state: TemplateState): Long = 15.minutes.inWholeMilliseconds diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/testing/logging/napier/NapierProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/testing/logging/napier/NapierProcessor.kt index 0db4920c..91fbac6b 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/testing/logging/napier/NapierProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/testing/logging/napier/NapierProcessor.kt @@ -1,10 +1,12 @@ package kotli.template.multiplatform.compose.testing.logging.napier import kotli.engine.BaseFeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.VersionCatalogRules import kotli.engine.template.rule.RemoveMarkedLine import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotlin.time.Duration.Companion.minutes object NapierProcessor : BaseFeatureProcessor() { @@ -12,6 +14,7 @@ object NapierProcessor : BaseFeatureProcessor() { const val ID = "testing.logging.napier" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getWebUrl(state: TemplateState): String = "https://github.com/AAkira/Napier" override fun getIntegrationUrl(state: TemplateState): String = "https://github.com/AAkira/Napier?tab=readme-ov-file#common" diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/loader/data/DataLoaderProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/loader/data/DataLoaderProcessor.kt index 884033f7..629af8d8 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/loader/data/DataLoaderProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/loader/data/DataLoaderProcessor.kt @@ -2,10 +2,12 @@ package kotli.template.multiplatform.compose.userflow.loader.data import kotli.engine.BaseFeatureProcessor import kotli.engine.FeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.RemoveMarkedLine import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.dataflow.config.facade.FacadeConfigProcessor import kotli.template.multiplatform.compose.showcases.feature.loader.data.DataLoaderShowcasesProcessor import kotlin.time.Duration.Companion.hours @@ -15,6 +17,7 @@ object DataLoaderProcessor : BaseFeatureProcessor() { const val ID = "userflow.loader.data" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 1.hours.inWholeMilliseconds override fun dependencies(): List> = listOf( diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/adaptive/AdaptiveNavigationProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/adaptive/AdaptiveNavigationProcessor.kt index d48460b1..f7814dc2 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/adaptive/AdaptiveNavigationProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/adaptive/AdaptiveNavigationProcessor.kt @@ -2,11 +2,13 @@ package kotli.template.multiplatform.compose.userflow.navigation.adaptive import kotli.engine.BaseFeatureProcessor import kotli.engine.FeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.RemoveMarkedLine import kotli.engine.template.rule.ReplaceMarkedText import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.userflow.navigation.NavigationBarProcessor import kotli.template.multiplatform.compose.userflow.navigation.bottom.BottomNavigationProcessor import kotli.template.multiplatform.compose.userflow.navigation.dismissible.DismissibleNavigationProcessor @@ -20,6 +22,7 @@ object AdaptiveNavigationProcessor : BaseFeatureProcessor() { const val ID = "userflow.navigation.adaptive" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 2.hours.inWholeMilliseconds override fun dependencies(): List> = listOf( diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/bottom/BottomNavigationProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/bottom/BottomNavigationProcessor.kt index 91311d54..e61d5f7a 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/bottom/BottomNavigationProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/bottom/BottomNavigationProcessor.kt @@ -1,10 +1,12 @@ package kotli.template.multiplatform.compose.userflow.navigation.bottom import kotli.engine.BaseFeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.RemoveMarkedLine import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotlin.time.Duration.Companion.hours object BottomNavigationProcessor : BaseFeatureProcessor() { @@ -12,6 +14,7 @@ object BottomNavigationProcessor : BaseFeatureProcessor() { const val ID = "userflow.navigation.bottom" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 2.hours.inWholeMilliseconds override fun doRemove(state: TemplateState) { diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/dismissible/DismissibleNavigationProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/dismissible/DismissibleNavigationProcessor.kt index 80a38ce6..9728f824 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/dismissible/DismissibleNavigationProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/dismissible/DismissibleNavigationProcessor.kt @@ -1,10 +1,12 @@ package kotli.template.multiplatform.compose.userflow.navigation.dismissible import kotli.engine.BaseFeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.ReplaceMarkedText import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.userflow.navigation.adaptive.AdaptiveNavigationProcessor import kotlin.time.Duration.Companion.hours @@ -13,6 +15,7 @@ object DismissibleNavigationProcessor : BaseFeatureProcessor() { const val ID = "userflow.navigation.dismissible" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 2.hours.inWholeMilliseconds override fun doApply(state: TemplateState) { diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/modal/ModalNavigationProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/modal/ModalNavigationProcessor.kt index f61621a9..f86924e8 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/modal/ModalNavigationProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/modal/ModalNavigationProcessor.kt @@ -1,10 +1,12 @@ package kotli.template.multiplatform.compose.userflow.navigation.modal import kotli.engine.BaseFeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.ReplaceMarkedText import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.userflow.navigation.adaptive.AdaptiveNavigationProcessor import kotlin.time.Duration.Companion.hours @@ -13,6 +15,7 @@ object ModalNavigationProcessor : BaseFeatureProcessor() { const val ID = "userflow.navigation.modal" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 2.hours.inWholeMilliseconds override fun doApply(state: TemplateState) { diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/permanent/PermanentNavigationProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/permanent/PermanentNavigationProcessor.kt index 7f982ad6..355e6744 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/permanent/PermanentNavigationProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/permanent/PermanentNavigationProcessor.kt @@ -1,10 +1,12 @@ package kotli.template.multiplatform.compose.userflow.navigation.permanent import kotli.engine.BaseFeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.ReplaceMarkedText import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.userflow.navigation.adaptive.AdaptiveNavigationProcessor import kotlin.time.Duration.Companion.hours @@ -13,6 +15,7 @@ object PermanentNavigationProcessor : BaseFeatureProcessor() { const val ID = "userflow.navigation.permanent" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 2.hours.inWholeMilliseconds override fun doApply(state: TemplateState) { diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/rail/RailNavigationProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/rail/RailNavigationProcessor.kt index 1319304a..a2edf03d 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/rail/RailNavigationProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/navigation/rail/RailNavigationProcessor.kt @@ -1,10 +1,12 @@ package kotli.template.multiplatform.compose.userflow.navigation.rail import kotli.engine.BaseFeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.ReplaceMarkedText import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.userflow.navigation.adaptive.AdaptiveNavigationProcessor import kotlin.time.Duration.Companion.hours @@ -13,6 +15,7 @@ object RailNavigationProcessor : BaseFeatureProcessor() { const val ID = "userflow.navigation.rail" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 2.hours.inWholeMilliseconds override fun doApply(state: TemplateState) { diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/theme/change/ChangeThemeProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/theme/change/ChangeThemeProcessor.kt index 6a77b2ca..e3fe42e6 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/theme/change/ChangeThemeProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/theme/change/ChangeThemeProcessor.kt @@ -2,10 +2,12 @@ package kotli.template.multiplatform.compose.userflow.theme.change import kotli.engine.BaseFeatureProcessor import kotli.engine.FeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.RemoveMarkedLine import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.showcases.feature.theme.change.ChangeThemeShowcasesProcessor import kotli.template.multiplatform.compose.userflow.theme.save.SaveThemeProcessor import kotlin.time.Duration.Companion.hours @@ -15,6 +17,7 @@ object ChangeThemeProcessor : BaseFeatureProcessor() { const val ID = "userflow.theme.change" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 1.hours.inWholeMilliseconds override fun dependencies(): List> = listOf( diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/theme/save/SaveThemeProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/theme/save/SaveThemeProcessor.kt index 2c93c7e2..5fc3b33a 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/theme/save/SaveThemeProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/theme/save/SaveThemeProcessor.kt @@ -2,11 +2,13 @@ package kotli.template.multiplatform.compose.userflow.theme.save import kotli.engine.BaseFeatureProcessor import kotli.engine.FeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.RemoveMarkedLine import kotli.engine.template.rule.ReplaceText import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.dataflow.keyvalue.settings.SettingsKeyValueProcessor import kotlin.time.Duration.Companion.hours @@ -15,6 +17,7 @@ object SaveThemeProcessor : BaseFeatureProcessor() { const val ID = "userflow.theme.save" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 2.hours.inWholeMilliseconds override fun dependencies(): List> = listOf( SettingsKeyValueProcessor::class.java diff --git a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/theme/toggle/ToggleThemeProcessor.kt b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/theme/toggle/ToggleThemeProcessor.kt index 3b803642..049f5f7f 100644 --- a/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/theme/toggle/ToggleThemeProcessor.kt +++ b/processor/src/main/kotlin/kotli/template/multiplatform/compose/userflow/theme/toggle/ToggleThemeProcessor.kt @@ -2,10 +2,12 @@ package kotli.template.multiplatform.compose.userflow.theme.toggle import kotli.engine.BaseFeatureProcessor import kotli.engine.FeatureProcessor +import kotli.engine.FeatureTag import kotli.engine.TemplateState import kotli.engine.template.rule.RemoveFile import kotli.engine.template.rule.RemoveMarkedLine import kotli.template.multiplatform.compose.Rules +import kotli.template.multiplatform.compose.Tags import kotli.template.multiplatform.compose.showcases.feature.theme.toggle.ToggleThemeShowcasesProcessor import kotli.template.multiplatform.compose.userflow.theme.save.SaveThemeProcessor import kotlin.time.Duration.Companion.hours @@ -15,6 +17,7 @@ object ToggleThemeProcessor : BaseFeatureProcessor() { const val ID = "userflow.theme.toggle" override fun getId(): String = ID + override fun getTags(): List = Tags.AllClients override fun getIntegrationEstimate(state: TemplateState): Long = 1.hours.inWholeMilliseconds override fun dependencies(): List> = listOf( diff --git a/template/app/src/commonMain/kotlin/kotli/app/presentation/showcases/dataflow/cache/basic/BasicCacheViewModel.kt b/template/app/src/commonMain/kotlin/kotli/app/presentation/showcases/dataflow/cache/basic/BasicCacheViewModel.kt index 928460e6..94e5aca6 100644 --- a/template/app/src/commonMain/kotlin/kotli/app/presentation/showcases/dataflow/cache/basic/BasicCacheViewModel.kt +++ b/template/app/src/commonMain/kotlin/kotli/app/presentation/showcases/dataflow/cache/basic/BasicCacheViewModel.kt @@ -24,7 +24,7 @@ class BasicCacheViewModel( launchAsync { val cacheKey = SimpleCacheKey() val cacheEntry = cacheSource.get(cacheKey, ::getDateAsFormattedString) - cacheEntry.changes().collectLatest(cacheState::set) + cacheEntry.getChanges().collectLatest(cacheState::set) } } diff --git a/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheEntry.kt b/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheEntry.kt index 5e1e0bcb..5147d540 100644 --- a/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheEntry.kt +++ b/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheEntry.kt @@ -1,7 +1,8 @@ package shared.data.source.cache import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update /** * Represents the state of a cache entry. @@ -13,33 +14,40 @@ interface CacheEntry { /** The key associated with this cache entry. */ val key: CacheKey + /** + * Sets the specified value to the given entry. + * + * @param value The value to be stored in the entry. + */ + suspend fun setValue(value: T?) + /** * Retrieves the cached value. * * @return The cached value, or new one if the value is not present in the cache or expired. */ - suspend fun value(): T? + suspend fun getValue(): T? /** * Retrieves the last cached value. * * @return The last cached value, or null if the value is not present in the cache. */ - suspend fun last(): T? + suspend fun getLast(): T? /** * Retrieves a fresh copy of the cached value. * * @return A fresh copy of the cached value, or null if the value is not available. */ - suspend fun fresh(): T? + suspend fun getFresh(): T? /** * Retrieves the last cached value if available, otherwise retrieves a fresh copy of the value. * * @return The last cached value if available, or a fresh copy of the value. Returns null if the value is not present in the cache. */ - suspend fun lastOrFresh() = last() ?: fresh() + suspend fun getLastOrFresh() = getLast() ?: getFresh() /** * Emits the cached value whenever it changes. @@ -47,7 +55,7 @@ interface CacheEntry { * * @return A flow representing the changes to the cached value. */ - suspend fun changes(): Flow + suspend fun getChanges(): Flow companion object { /** @@ -58,11 +66,14 @@ interface CacheEntry { * @return A CacheState instance representing the single cached value. */ fun of(key: CacheKey, value: T): CacheEntry = object : CacheEntry { + private val valueChanges = MutableStateFlow(value) + override val key: CacheKey = key - override suspend fun value(): T? = value - override suspend fun last(): T? = value - override suspend fun fresh(): T? = value - override suspend fun changes(): Flow = flowOf(value) + override suspend fun getValue(): T? = value + override suspend fun getLast(): T? = value + override suspend fun getFresh(): T? = value + override suspend fun getChanges(): Flow = valueChanges + override suspend fun setValue(value: T?) = valueChanges.update { value } } } diff --git a/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheSource.kt b/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheSource.kt index ad1436f4..1d93fd66 100644 --- a/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheSource.kt +++ b/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/CacheSource.kt @@ -50,14 +50,6 @@ interface CacheSource : DataSource { */ fun > remove(key: K) - /** - * Associates the specified value with the specified key in the cache. - * - *@param key The cache key to associate with the value. - * @param value The value to be stored in the cache. - */ - fun put(key: CacheKey, value: T) - /** * Clears all entries from the cache. */ diff --git a/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/InMemoryCacheSource.kt b/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/InMemoryCacheSource.kt index 8303266d..5b56a5ed 100644 --- a/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/InMemoryCacheSource.kt +++ b/template/shared/data/src/commonMain/kotlin/shared/data/source/cache/InMemoryCacheSource.kt @@ -8,9 +8,15 @@ import kotlinx.coroutines.async import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.retry +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import kotlinx.datetime.Clock @@ -24,26 +30,25 @@ import kotlin.reflect.KClass * to present data without delays, but with the ability to update based on expiration and other conditions. * * @param changesRetryInterval The interval, in milliseconds, to retry cache changes. - * @param exceptionRetryInterval The interval, in milliseconds, to retry cache operations in case of exceptions. * @param exceptionRetryCount The maximum number of retries for cache operations in case of exceptions. */ @Suppress("UNCHECKED_CAST") open class InMemoryCacheSource( private val changesRetryInterval: Long = 1000L, - private val exceptionRetryInterval: Long = 3000L, + private val exceptionRetryInterval: Long = 1000L, private val exceptionRetryCount: Int = 10 ) : CacheSource { private val dispatcher = Dispatchers.Default - private val jobs = ConcurrentMutableMap>() - private val cache = ConcurrentMutableMap() - - override fun get(key: CacheKey, valueProvider: suspend () -> T?): CacheEntry = - CacheStateImpl(key, valueProvider) - - override fun put(key: CacheKey, value: T) { - val cacheKey = CacheKeySnapshot(key) - cache[cacheKey] = CacheData(value) + private val jobs = ConcurrentMutableMap, Deferred<*>>() + private val cache = ConcurrentMutableMap, EntryData<*>>() + + override fun get(key: CacheKey, valueProvider: suspend () -> T?): CacheEntry { + val keyData = KeyData(key) + val entryData = cache.computeIfAbsent(keyData) { + EntryData(keyData, valueProvider) + } as CacheEntry + return entryData } override fun clear() { @@ -72,7 +77,7 @@ open class InMemoryCacheSource( } override fun > invalidate(key: K) { - val cacheKey = CacheKeySnapshot(key) + val cacheKey = KeyData(key) jobs.remove(cacheKey)?.cancel() cache[cacheKey]?.invalidate() } @@ -98,110 +103,111 @@ open class InMemoryCacheSource( } override fun > remove(key: K) { - val cacheKey = CacheKeySnapshot(key) + val cacheKey = KeyData(key) jobs.remove(cacheKey)?.cancel() cache.remove(cacheKey) } - private suspend fun getValue( - cacheKey: CacheKeySnapshot, - valueProvider: suspend () -> T? - ): T? { - val job = jobs[cacheKey] - ?.let { deferredJob -> - if (deferredJob.isCancelled) { - jobs.remove(cacheKey)?.cancel() - null - } else { - deferredJob - } - } - ?: run { - if (cacheKey.key.immortal()) { - jobs.computeIfAbsent(cacheKey) { - GlobalScope.async { valueProvider() } - } - } else { - withContext(dispatcher) { - jobs.computeIfAbsent(cacheKey) { - async { valueProvider() } - } - } - } - } - job.invokeOnCompletion { jobs.remove(cacheKey)?.key } + private data class KeyData( + val key: CacheKey, + val type: KClass<*> = key::class + ) - return job.await() as? T - } + private data class EntrySnapshot( + val value: T, + val updateTime: Long = Clock.System.now().toEpochMilliseconds() + ) - private data class CacheData( - val data: Any?, - val createTime: Long = Clock.System.now().toEpochMilliseconds() - ) { + private inner class EntryData( + private val keyData: KeyData, + private val valueProvider: suspend () -> T? + ) : CacheEntry { @Transient - private var invalid: Boolean = false + private var invalidated = false + private val liveChanges by lazy { fetchLiveChanges() } + private val changes = MutableStateFlow?>(null) - fun isValid(ttl: Long): Boolean = when { - invalid -> false - data == null -> false - ttl > 0 -> !isExpired(ttl) - ttl == 0L -> false - else -> true - } + override val key: CacheKey = keyData.key - fun isExpired(ttl: Long): Boolean { - val now = Clock.System.now().toEpochMilliseconds() - return ttl > 0 && createTime + ttl <= now + override suspend fun setValue(value: T?) = changes.update { value?.let(::EntrySnapshot) } + + override suspend fun getFresh(): T? = invalidate().run { getValue() } + + override suspend fun getLast(): T? = changes.value?.value + + override suspend fun getValue(): T? { + return if (!isValid(key.ttl)) { + val newValue = fetchValue() + changes.updateAndGet { newValue?.let(::EntrySnapshot) }?.value + } else { + changes.value?.value + } } + override suspend fun getChanges(): Flow = liveChanges + .flatMapLatest { changes } + .map { snapshot -> snapshot?.value } + .retry { th -> !th.isCancellationException().also { delay(changesRetryInterval) } } + fun invalidate() { - invalid = true + invalidated = true } - } - private inner class CacheStateImpl( - override val key: CacheKey, - private val valueProvider: suspend () -> T? - ) : CacheEntry { + private fun isValid(ttl: Long): Boolean = when { + invalidated -> false + changes.value == null -> false + ttl > 0 -> !isExpired(ttl) + ttl == 0L -> false + else -> true + } - private val cacheKey = CacheKeySnapshot(key) + private fun isExpired(ttl: Long): Boolean { + val updateTime = changes.value?.updateTime ?: return true + val now = Clock.System.now().toEpochMilliseconds() + return ttl > 0 && updateTime + ttl <= now + } - override suspend fun fresh(): T? = cache[cacheKey]?.invalidate().run { value() } - override suspend fun last(): T? = cache[cacheKey]?.data as? T + private suspend fun fetchValue(): T? { + val job = jobs[keyData] + ?.let { deferredJob -> + if (deferredJob.isCancelled) { + jobs.remove(keyData)?.let { null } + } else { + deferredJob + } + } + ?: run { + if (key.immortal()) { + jobs.computeIfAbsent(keyData) { + GlobalScope.async { valueProvider() } + } + } else { + withContext(dispatcher) { + jobs.computeIfAbsent(keyData) { + async { valueProvider() } + } + } + } + } + job.invokeOnCompletion { jobs.remove(keyData)?.let {} } - override suspend fun value(): T? { - val cacheKey = CacheKeySnapshot(key) - val cacheItem = cache[cacheKey] - if (cacheItem == null || !cacheItem.isValid(key.ttl)) { - val data = getValue(cacheKey, valueProvider) ?: return null - cache[cacheKey] = CacheData(data) - return data - } else { - return cacheItem.data as T? - } + return job.await() as? T } - override suspend fun changes(): Flow = flow { - getLastData()?.data - ?.let { data -> data as? T } - ?.let { data -> emit(data) } - + private fun fetchLiveChanges() = flow { + emit(true) var retryAttempt = 0 while (currentCoroutineContext().isActive) { try { - val prev = getLastData() - val next = value() - - if (next != null) { - emit(next) - } + getValue() + val updateTime = changes.value?.updateTime - if (prev?.data != next) { - delay(key.ttl) - } else if (prev != null) { + if (updateTime == null) { + delay(changesRetryInterval) + } else { val now = Clock.System.now().toEpochMilliseconds() - val time = key.ttl - (now - prev.createTime) + val time = key.ttl - (now - updateTime) delay(time) } @@ -210,22 +216,11 @@ open class InMemoryCacheSource( retryAttempt++ when { retryAttempt >= exceptionRetryCount -> throw e - else -> Unit + else -> delay(exceptionRetryInterval) } } } - }.distinctUntilChanged().retry { th -> - !th.isCancellationException().also { - delay(changesRetryInterval) - } - } - - private fun getLastData(): CacheData? = cache[cacheKey] + }.shareIn(GlobalScope, SharingStarted.WhileSubscribed(), 1) } - private data class CacheKeySnapshot( - val key: CacheKey<*>, - val type: KClass<*> = key::class - ) - } \ No newline at end of file diff --git a/template/shared/data/src/jvmTest/kotlin/shared/data/source/cache/InMemoryCacheSourceTest.kt b/template/shared/data/src/jvmTest/kotlin/shared/data/source/cache/InMemoryCacheSourceTest.kt index 9de1a283..219d06db 100644 --- a/template/shared/data/src/jvmTest/kotlin/shared/data/source/cache/InMemoryCacheSourceTest.kt +++ b/template/shared/data/src/jvmTest/kotlin/shared/data/source/cache/InMemoryCacheSourceTest.kt @@ -3,6 +3,9 @@ package shared.data.source.cache import io.ktor.util.collections.ConcurrentSet import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.util.UUID @@ -26,7 +29,7 @@ class InMemoryCacheSourceTest { delay(2.seconds) iteration } - .value() + .getValue() ?.let(cached::add) } } @@ -47,7 +50,7 @@ class InMemoryCacheSourceTest { delay(1.seconds) UUID.randomUUID() } - .value() + .getValue() ?.let(cached::add) } } @@ -55,18 +58,42 @@ class InMemoryCacheSourceTest { assertEquals(1, cached.size) } + @Test + fun `get changes in parallel`() = runBlocking { + val key = UUIDCacheKey(Int.MAX_VALUE) + val iterations = 1000 + val cached = ConcurrentSet() + repeat(iterations) { + launch { + delay(300) + cache + .get(key) { + delay(1.seconds) + UUID.randomUUID() + } + .getChanges() + .filterNotNull() + .take(1) + .first() + .let(cached::add) + } + } + delay(2.seconds) + assertEquals(1, cached.size) + } + @Test fun `check cached state logic`() = runBlocking { val key = UUIDCacheKey(Int.MAX_VALUE, ttl = 100) val entry = cache.get(key) { UUID.randomUUID() } - val value1 = entry.value() - val value1Last = entry.last() + val value1 = entry.getValue() + val value1Last = entry.getLast() delay(100) - val value2 = entry.value() + val value2 = entry.getValue() delay(100) assertNotEquals(value1, value2) assertEquals(value1, value1Last) - assertNotEquals(value2, entry.value()) + assertNotEquals(value2, entry.getValue()) } private data class TestCacheKey(