diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 778814576534e00e449e40e3be69ba7685e4044e..dcd586c641bf1a82c0506b93e7cff9c077910199 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,9 @@ ksp = "2.1.10-1.0.31" kotlinx-serialization = "1.8.0" ktor = "3.1.1" cache4k = "0.14.0" -statelyConcurrentCollections = "2.1.0" +# Pinned. Do not upgrade from 2.0.5 until Koin migrates to using a newer version of Stately >= 2.1.0 +# See for more info: https://github.com/sqldelight/sqldelight/issues/4357#issuecomment-1839905700 +stately = "2.0.5" slf4j = "2.0.17" kotlinx-io = "0.7.0" @@ -18,8 +20,8 @@ datetime = "0.6.2" sandwich = "2.1.0" arrow = "2.0.0" # Main Koin version - for core, android deps -koin = "4.0.2" -koinTest = "4.0.2" +koin = "4.1.0-Beta5" +koinTest = "4.1.0-Beta5" gradleBuildConfigPlugin = "5.5.4" akkurate = "0.11.0" exposed = "0.56.0" @@ -54,12 +56,14 @@ compileSdk = "34" ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } ktor-client-serialization = { group = "io.ktor", name = "ktor-client-serialization", version.ref = "ktor" } +ktor-client-encoding = { group = "io.ktor", name = "ktor-client-encoding", version.ref = "ktor" } ktor-client-java = { group = "io.ktor", name = "ktor-client-java", version.ref = "ktor" } ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" } ktor-test = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" } cache4k = { group = "io.github.reactivecircus.cache4k", name = "cache4k", version.ref = "cache4k" } -statelyConcurrentCollections = { group = "co.touchlab", name = "stately-concurrent-collections", version.ref = "statelyConcurrentCollections" } -slf4j = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } +statelyCommon = { module = "co.touchlab:stately-common", version.ref = "stately" } +statelyIsolate = { module = "co.touchlab:stately-isolate", version.ref = "stately" } +statelyIsoCollections = { module = "co.touchlab:stately-iso-collections", version.ref = "stately" } slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } @@ -119,7 +123,7 @@ klaxon = { module = "com.beust:klaxon", version.ref = "klaxon" } konsist = { module = "com.lemonappdev:konsist", version.ref = "konsist" } [bundles] -ktor = ["ktor-client-logging", "ktor-client-serialization", "kotlinx-serialization-json"] +ktor = ["ktor-client-logging", "ktor-client-serialization", "ktor-client-encoding", "kotlinx-serialization-json"] sandwich = ["sandwich", "sandwich-ktor", "sandwich-ktorfit"] [plugins] diff --git a/temerity/build.gradle.kts b/temerity/build.gradle.kts index 0a4a0f62af75b1a686de1f479579e2cb8e7e0fe2..6d69a307da2b2084c212ff4b5cd2594f055cf26a 100644 --- a/temerity/build.gradle.kts +++ b/temerity/build.gradle.kts @@ -106,8 +106,8 @@ kotlin { implementation(libs.ktorfit.lib) implementation(libs.kotlinx.coroutines.core) api(libs.kermit) - // TODO: Re-add cache4k when updated for Kotlin 2.0.21 - implementation(libs.statelyConcurrentCollections) + implementation(libs.cache4k) + implementation(libs.statelyCommon) implementation(libs.kmpIo) implementation(libs.kotlinx.io) implementation(libs.kotlinSemver) @@ -127,12 +127,18 @@ kotlin { implementation(libs.ktor.client.java) implementation(libs.slf4j.api) implementation(libs.jansi) + // See https://github.com/sqldelight/sqldelight/issues/4357#issuecomment-1839905700 + implementation(libs.statelyIsolate) + implementation(libs.statelyIsoCollections) } } val androidMain by getting { dependencies { implementation(libs.ktor.client.android) // TODO: Add logging for Android + // See https://github.com/sqldelight/sqldelight/issues/4357#issuecomment-1839905700 + implementation(libs.statelyIsolate) + implementation(libs.statelyIsoCollections) } } val jvmTest by getting { diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemClientConfig.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemClientConfig.kt index 4b2ef17f062902a894325c97f44f6ee2fdd809fa..a31ac918f9c15e6c42b80a57783b7388dd3ec99d 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemClientConfig.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/TemClientConfig.kt @@ -30,13 +30,16 @@ import kotlin.time.Duration * * @property serviceUrl Specifies URL of platform instance to make API calls to * @property serviceToken Specifies the authorization token to use for API calls - * @property optDebugEnabled Specifies whether debug logging should be enabled + * @property optLoggingEnabled Specifies whether logging should be enabled + * @property optDebugEnabled Specifies whether debugging mode should be enabled + * @property optSupportKtxNotebook Specifies whether Kotlin notebook support should be enabled * * @property optExpectSuccess Specifies whether to expect successful responses by default * @property optUseWebTimeout Specifies whether to use a timeout other than default for web requests * @property optWebTimeoutDuration Specifies the timeout [kotlin.time.Duration] to use when making web requests * @property optCacheTimeoutDuration Specifies the timeout [kotlin.time.Duration] to use when storing cache entries. Affects keep-alive time for cached instance data like user role type fields. * @property optThreadCount Specifies the number of threads to use for each client instance. Defaults to 2 at a minimum + * @property optEnableCompression Specifies whether to turn on compression for HTTP requests. Enabled by default. */ @TemDsl public class TemClientConfig { @@ -59,6 +62,8 @@ public class TemClientConfig { public var optThreadCount: Int? = null + public var optEnableCompression: Boolean = true + /** * Configures the HttpClient with the provided block. * @param () -> HttpClient The block to configure the HttpClient. @@ -80,7 +85,7 @@ public class TemClientConfig { /** * Creates an custom [HttpClient] with the specified [io.ktor.client.engine.HttpClientEngineFactory] and optional [block] configuration. - * Note that the Temerity config will be added afterwards. + * Note that the Temerity config will be added afterward. */ public fun <T : HttpClientEngineConfig> httpClient( engineFactory: HttpClientEngineFactory<T>, diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt index 685947d248b9561c27d7318e8bffebc1369dbe59..422a74f29750e060454fd5e6a3a6898b67b901bc 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt @@ -17,7 +17,6 @@ */ package edu.ucsc.its.temerity.core -import co.touchlab.stately.collections.ConcurrentMutableMap import com.skydoves.sandwich.ApiResponse import com.skydoves.sandwich.StatusCode.NoContent import com.skydoves.sandwich.getOrThrow @@ -54,10 +53,13 @@ import edu.ucsc.its.temerity.model.User import edu.ucsc.its.temerity.model.UserGroup import edu.ucsc.its.temerity.model.UserRecordingSession import edu.ucsc.its.temerity.model.UserUpdate +import io.github.reactivecircus.cache4k.Cache import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig import io.ktor.client.engine.HttpClientEngine import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.compression.ContentEncoding +import io.ktor.client.plugins.compression.ContentEncodingConfig import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.logging.LogLevel.ALL import io.ktor.client.plugins.logging.Logging @@ -216,7 +218,7 @@ public class Temerity internal constructor( private var libraryLogger: KermitLogger private var platformApi: PlatformApi - private var cachedUserRoleList: ConcurrentMutableMap<Int, String> + private var cachedUserRoleList: Cache<Int, String> private var libraryCoroutineDispatcher: CoroutineDispatcher private var libraryCoroutineScope: CoroutineScope private var jsonProcessingDispatcher: CoroutineDispatcher @@ -251,7 +253,9 @@ public class Temerity internal constructor( parametersOf(config, webRequestDispatcher) } - cachedUserRoleList = ConcurrentMutableMap() + cachedUserRoleList = Cache.Builder<Int, String>() + .expireAfterWrite(config.optCacheTimeoutDuration ?: 15.minutes) + .build() } /** @@ -344,7 +348,7 @@ public class Temerity internal constructor( json.decodeFromString<List<User>>(returnedUsersResponse) } val roleTypes = returnedUserList.map { it.userType }.distinct() - cachedUserRoleList.clear() + cachedUserRoleList.invalidateAll() roleTypes.forEach { cachedUserRoleList.put(it.hashCode(), it) } @@ -355,7 +359,7 @@ public class Temerity internal constructor( if (refresh) { refreshCachedUserRoles() } - val returnedUserRoles = cachedUserRoleList.values.toList() + val returnedUserRoles = cachedUserRoleList.asMap().values.toList() returnedUserRoles.ifEmpty { refreshCachedUserRoles() } @@ -693,8 +697,17 @@ internal fun buildHttpClient( } } + if (config.optEnableCompression) { + install(ContentEncoding) { + mode = ContentEncodingConfig.Mode.CompressRequest + deflate(1.0F) + gzip(1.0F) + } + } + config.optHttpClientConfigBlock?.invoke(this) } + return config.optHttpClientBuilder?.invoke()?.config(defaultConfig) ?: HttpClient(httpClientEngine, defaultConfig) } diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt index 5f61a923904ca0d4e6f143dc254f76f62181308d..c62a65f86cbb33dfbc95c6da26bb5f4f2df2a5eb 100644 --- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt +++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdReportTests.kt @@ -18,6 +18,7 @@ package edu.ucsc.its.temerity.test import edu.ucsc.its.temerity.model.AuditLogSortOrder.NEW_FIRST +import edu.ucsc.its.temerity.model.DeviceRecordingSession import edu.ucsc.its.temerity.model.EventType.AUTOMATED_SESSION_FAILED_TO_START import edu.ucsc.its.temerity.model.EventType.AUTOMATED_SESSION_MONITOR import edu.ucsc.its.temerity.model.EventType.CAPTURE_ERROR