diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b8bd0ba82..c56303fa450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 0.16.8 + +* `Versions`: + * `Ktor`: `2.2.2` -> `2.2.3` +* `Ktor`: + * `Client` + * Fixes in `HttpClient.uniUpload` + * `Server` + * Fixes in `PartData.FileItem.download` +* `Repos`: + * `Cache`: + * New type of caches: `FallbackCacheRepo` + * Fixes in `Write*` variants of cached repos + * New type `ActionWrapper` + * New `AutoRecache*` classes for all types of repos as `FallbackCacheRepo`s + * `Common`: + * New transformations for key-value and key-values vice-verse + * Fixes in `FileReadKeyValueRepo` + ## 0.16.7 * `Common`: diff --git a/gradle.properties b/gradle.properties index bba345e0759..ecd020b6e6a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,5 +14,5 @@ crypto_js_version=4.1.1 # Project data group=dev.inmo -version=0.16.7 -android_code_version=175 +version=0.16.8 +android_code_version=176 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1f43dec4c89..51869c69f1c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ jb-dokka = "1.7.20" klock = "3.4.0" uuid = "0.6.0" -ktor = "2.2.2" +ktor = "2.2.3" gh-release = "2.4.1" diff --git a/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualUniUpload.kt b/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualUniUpload.kt index 572785ca371..8031c347565 100644 --- a/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualUniUpload.kt +++ b/ktor/client/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/client/ActualUniUpload.kt @@ -17,8 +17,11 @@ import io.ktor.http.HttpStatusCode import io.ktor.http.Parameters import io.ktor.http.content.PartData import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.SerializationStrategy import kotlinx.serialization.StringFormat import kotlinx.serialization.encodeToString +import kotlinx.serialization.serializer import java.io.File /** @@ -29,6 +32,7 @@ import java.io.File * in case you wish to pass other source of multipart binary data than regular file * @suppress */ +@OptIn(InternalSerializationApi::class) actual suspend fun HttpClient.uniUpload( url: String, data: Map, @@ -60,7 +64,7 @@ actual suspend fun HttpClient.uniUpload( ) else -> append( k, - stringFormat.encodeToString(v) + stringFormat.encodeToString(v::class.serializer() as SerializationStrategy, v) ) } } diff --git a/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/DownloadFileItem.kt b/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/DownloadFileItem.kt index 2056a6e704b..998fef99797 100644 --- a/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/DownloadFileItem.kt +++ b/ktor/server/src/jvmMain/kotlin/dev/inmo/micro_utils/ktor/server/DownloadFileItem.kt @@ -10,8 +10,8 @@ import java.io.File fun PartData.FileItem.download(target: File) { provider().use { input -> - target.outputStream().use { - input.copyTo(it.asOutput()) + target.outputStream().asOutput().use { + input.copyTo(it) } } } diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/FallbackCacheRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/FallbackCacheRepo.kt new file mode 100644 index 00000000000..a1130e8f645 --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/FallbackCacheRepo.kt @@ -0,0 +1,7 @@ +package dev.inmo.micro_utils.repos.cache + +/** + * Any inheritor of this should work with next logic: try to take data from their original repo, if successful - save data to internal + * [dev.inmo.micro_utils.repos.cache.cache.FullKVCache] or try to take data from that internal cache + */ +interface FallbackCacheRepo : CacheRepo diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/ActionWrapper.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/ActionWrapper.kt new file mode 100644 index 00000000000..11a96e71978 --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/ActionWrapper.kt @@ -0,0 +1,37 @@ +package dev.inmo.micro_utils.repos.cache.fallback + +import dev.inmo.micro_utils.coroutines.runCatchingSafely +import kotlinx.coroutines.withTimeout + +/** + * Realizations should [wrap] the work with some conditions like retries on exceptions, calling timeout, etc. + * + * @see Timeouted + * @see Direct + */ +interface ActionWrapper { + /** + * Should execute [block] to take the result [T], but may return failure in case when something went wrong. + * This method should never throw any [Exception] + */ + suspend fun wrap(block: suspend () -> T): Result + + /** + * This type of [ActionWrapper]s will use [withTimeout]([timeoutMillis]) and if original call + * will not return anything in that timeout just return [Result] with failure + */ + class Timeouted(private val timeoutMillis: Long) : ActionWrapper { + override suspend fun wrap(block: suspend () -> T): Result = runCatchingSafely { + withTimeout(timeoutMillis) { + block() + } + } + } + /** + * It is passthrough variant of [ActionWrapper] which will just call incoming block with wrapping into [runCatchingSafely] + */ + object Direct : ActionWrapper { + + override suspend fun wrap(block: suspend () -> T): Result = runCatchingSafely { block() } + } +} diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/crud/AutoRecacheCRUDRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/crud/AutoRecacheCRUDRepo.kt new file mode 100644 index 00000000000..cc28b5245d7 --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/crud/AutoRecacheCRUDRepo.kt @@ -0,0 +1,36 @@ +package dev.inmo.micro_utils.repos.cache.fallback.crud + +import dev.inmo.micro_utils.repos.CRUDRepo +import dev.inmo.micro_utils.repos.WriteCRUDRepo +import dev.inmo.micro_utils.repos.cache.cache.FullKVCache +import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper +import kotlinx.coroutines.CoroutineScope +import kotlin.time.Duration.Companion.seconds + +open class AutoRecacheCRUDRepo( + originalRepo: CRUDRepo, + scope: CoroutineScope, + kvCache: FullKVCache = FullKVCache(), + recacheDelay: Long = 60.seconds.inWholeMilliseconds, + actionWrapper: ActionWrapper = ActionWrapper.Direct, + idGetter: (RegisteredObject) -> Id +) : AutoRecacheReadCRUDRepo( + originalRepo, + scope, + kvCache, + recacheDelay, + actionWrapper, + idGetter +), + WriteCRUDRepo by AutoRecacheWriteCRUDRepo(originalRepo, scope, kvCache, idGetter), + CRUDRepo { + + constructor( + originalRepo: CRUDRepo, + scope: CoroutineScope, + originalCallTimeoutMillis: Long, + kvCache: FullKVCache = FullKVCache(), + recacheDelay: Long = 60.seconds.inWholeMilliseconds, + idGetter: (RegisteredObject) -> Id + ) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis), idGetter) +} diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/crud/AutoRecacheReadCRUDRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/crud/AutoRecacheReadCRUDRepo.kt new file mode 100644 index 00000000000..edca199fe97 --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/crud/AutoRecacheReadCRUDRepo.kt @@ -0,0 +1,86 @@ +package dev.inmo.micro_utils.repos.cache.fallback.crud + +import dev.inmo.micro_utils.coroutines.runCatchingSafely +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.PaginationResult +import dev.inmo.micro_utils.repos.ReadCRUDRepo +import dev.inmo.micro_utils.repos.cache.cache.FullKVCache +import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper +import dev.inmo.micro_utils.repos.cache.util.actualizeAll +import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo +import dev.inmo.micro_utils.repos.set +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +open class AutoRecacheReadCRUDRepo( + protected open val originalRepo: ReadCRUDRepo, + protected val scope: CoroutineScope, + protected val kvCache: FullKVCache = FullKVCache(), + protected val recacheDelay: Long = 60.seconds.inWholeMilliseconds, + protected val actionWrapper: ActionWrapper = ActionWrapper.Direct, + protected val idGetter: (RegisteredObject) -> Id +) : ReadCRUDRepo, FallbackCacheRepo { + val autoUpdateJob = scope.launch { + while (isActive) { + actualizeAll() + + delay(recacheDelay) + } + } + + constructor( + originalRepo: ReadCRUDRepo, + scope: CoroutineScope, + originalCallTimeoutMillis: Long, + kvCache: FullKVCache = FullKVCache(), + recacheDelay: Long = 60.seconds.inWholeMilliseconds, + idGetter: (RegisteredObject) -> Id + ) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis), idGetter) + + protected open suspend fun actualizeAll(): Result { + return runCatchingSafely { + kvCache.actualizeAll(originalRepo) + } + } + + override suspend fun contains(id: Id): Boolean = actionWrapper.wrap { + originalRepo.contains(id) + }.getOrElse { + kvCache.contains(id) + } + + override suspend fun count(): Long = actionWrapper.wrap { + originalRepo.count() + }.getOrElse { + kvCache.count() + } + + override suspend fun getByPagination( + pagination: Pagination + ): PaginationResult = actionWrapper.wrap { + originalRepo.getByPagination(pagination) + }.getOrNull() ?.also { + it.results.forEach { + kvCache.set(idGetter(it), it) + } + } ?: kvCache.values(pagination) + + override suspend fun getIdsByPagination( + pagination: Pagination + ): PaginationResult = actionWrapper.wrap { + originalRepo.getIdsByPagination(pagination) + }.getOrElse { kvCache.keys(pagination) } + + override suspend fun getById(id: Id): RegisteredObject? = actionWrapper.wrap { + originalRepo.getById(id) + }.getOrNull() ?.also { + kvCache.set(idGetter(it), it) + } ?: kvCache.get(id) + + override suspend fun invalidate() { + actualizeAll() + } +} diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/crud/AutoRecacheWriteCRUDRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/crud/AutoRecacheWriteCRUDRepo.kt new file mode 100644 index 00000000000..ca15411096e --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/crud/AutoRecacheWriteCRUDRepo.kt @@ -0,0 +1,65 @@ +package dev.inmo.micro_utils.repos.cache.fallback.crud + +import dev.inmo.micro_utils.coroutines.plus +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.repos.UpdatedValuePair +import dev.inmo.micro_utils.repos.WriteCRUDRepo +import dev.inmo.micro_utils.repos.cache.cache.FullKVCache +import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo +import dev.inmo.micro_utils.repos.set +import dev.inmo.micro_utils.repos.unset +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +open class AutoRecacheWriteCRUDRepo( + protected val originalRepo: WriteCRUDRepo, + protected val scope: CoroutineScope, + protected val kvCache: FullKVCache = FullKVCache(), + protected val idGetter: (RegisteredObject) -> Id +) : WriteCRUDRepo, FallbackCacheRepo { + override val deletedObjectsIdsFlow: Flow + get() = (originalRepo.deletedObjectsIdsFlow + kvCache.onValueRemoved).distinctUntilChanged() + override val newObjectsFlow: Flow + get() = (originalRepo.newObjectsFlow + kvCache.onNewValue.map { it.second }).distinctUntilChanged() + override val updatedObjectsFlow: Flow + get() = originalRepo.updatedObjectsFlow + + private val onRemovingUpdatesListeningJob = originalRepo.deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(scope) { + kvCache.unset(it) + } + + private val onNewAndUpdatedObjectsListeningJob = merge( + originalRepo.newObjectsFlow, + originalRepo.updatedObjectsFlow, + ).subscribeSafelyWithoutExceptions(scope) { + kvCache.set(idGetter(it), it) + } + + override suspend fun update( + values: List> + ): List = originalRepo.update(values).onEach { + kvCache.set(idGetter(it), it) + } + + override suspend fun update( + id: Id, + value: InputObject + ): RegisteredObject? = originalRepo.update(id, value) ?.also { + kvCache.set(idGetter(it), it) + } + + override suspend fun deleteById(ids: List) = originalRepo.deleteById(ids).also { + kvCache.unset(ids) + } + + override suspend fun create(values: List): List = originalRepo.create(values).onEach { + kvCache.set(idGetter(it), it) + } + + override suspend fun invalidate() { + kvCache.clear() + } +} diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalue/AutoRecacheKeyValueRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalue/AutoRecacheKeyValueRepo.kt new file mode 100644 index 00000000000..e1864829e3c --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalue/AutoRecacheKeyValueRepo.kt @@ -0,0 +1,42 @@ +package dev.inmo.micro_utils.repos.cache.fallback.keyvalue + +import dev.inmo.micro_utils.repos.KeyValueRepo +import dev.inmo.micro_utils.repos.WriteKeyValueRepo +import dev.inmo.micro_utils.repos.cache.cache.FullKVCache +import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper +import kotlinx.coroutines.CoroutineScope +import kotlin.time.Duration.Companion.seconds + +open class AutoRecacheKeyValueRepo( + override val originalRepo: KeyValueRepo, + scope: CoroutineScope, + kvCache: FullKVCache = FullKVCache(), + recacheDelay: Long = 60.seconds.inWholeMilliseconds, + actionWrapper: ActionWrapper = ActionWrapper.Direct, + idGetter: (RegisteredObject) -> Id +) : AutoRecacheReadKeyValueRepo ( + originalRepo, + scope, + kvCache, + recacheDelay, + actionWrapper, + idGetter +), + WriteKeyValueRepo by AutoRecacheWriteKeyValueRepo(originalRepo, scope, kvCache), + KeyValueRepo { + + constructor( + originalRepo: KeyValueRepo, + scope: CoroutineScope, + originalCallTimeoutMillis: Long, + kvCache: FullKVCache = FullKVCache(), + recacheDelay: Long = 60.seconds.inWholeMilliseconds, + idGetter: (RegisteredObject) -> Id + ) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis), idGetter) + + override suspend fun unsetWithValues(toUnset: List) = originalRepo.unsetWithValues( + toUnset + ).also { + kvCache.unsetWithValues(toUnset) + } +} diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalue/AutoRecacheReadKeyValueRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalue/AutoRecacheReadKeyValueRepo.kt new file mode 100644 index 00000000000..74711f920c0 --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalue/AutoRecacheReadKeyValueRepo.kt @@ -0,0 +1,96 @@ +package dev.inmo.micro_utils.repos.cache.fallback.keyvalue + +import dev.inmo.micro_utils.coroutines.runCatchingSafely +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.PaginationResult +import dev.inmo.micro_utils.repos.ReadKeyValueRepo +import dev.inmo.micro_utils.repos.cache.cache.FullKVCache +import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper +import dev.inmo.micro_utils.repos.cache.util.actualizeAll +import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo +import dev.inmo.micro_utils.repos.set +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +open class AutoRecacheReadKeyValueRepo( + protected open val originalRepo: ReadKeyValueRepo, + protected val scope: CoroutineScope, + protected val kvCache: FullKVCache = FullKVCache(), + protected val recacheDelay: Long = 60.seconds.inWholeMilliseconds, + protected val actionWrapper: ActionWrapper = ActionWrapper.Direct, + protected val idGetter: (RegisteredObject) -> Id +) : ReadKeyValueRepo, FallbackCacheRepo { + val autoUpdateJob = scope.launch { + while (isActive) { + actualizeAll() + + delay(recacheDelay) + } + } + + constructor( + originalRepo: ReadKeyValueRepo, + scope: CoroutineScope, + originalCallTimeoutMillis: Long, + kvCache: FullKVCache = FullKVCache(), + recacheDelay: Long = 60.seconds.inWholeMilliseconds, + idGetter: (RegisteredObject) -> Id + ) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis), idGetter) + + protected open suspend fun actualizeAll(): Result { + return runCatchingSafely { + kvCache.actualizeAll(originalRepo) + } + } + + override suspend fun contains(key: Id): Boolean = actionWrapper.wrap { + originalRepo.contains(key) + }.getOrElse { + kvCache.contains(key) + } + + override suspend fun count(): Long = actionWrapper.wrap { + originalRepo.count() + }.getOrElse { + kvCache.count() + } + + override suspend fun get(k: Id): RegisteredObject? = actionWrapper.wrap { + originalRepo.get(k) + }.getOrNull() ?.also { + kvCache.set(k, it) + } ?: kvCache.get(k) + + override suspend fun values( + pagination: Pagination, + reversed: Boolean + ): PaginationResult = actionWrapper.wrap { + originalRepo.values(pagination, reversed) + }.getOrNull() ?.also { + it.results.forEach { + kvCache.set(idGetter(it), it) + } + } ?: kvCache.values(pagination, reversed) + + override suspend fun keys( + pagination: Pagination, + reversed: Boolean + ): PaginationResult = actionWrapper.wrap { + originalRepo.keys(pagination, reversed) + }.getOrElse { kvCache.keys(pagination, reversed) } + + override suspend fun keys( + v: RegisteredObject, + pagination: Pagination, + reversed: Boolean + ): PaginationResult = actionWrapper.wrap { + originalRepo.keys(v, pagination, reversed) + }.getOrElse { kvCache.keys(v, pagination, reversed) } + + override suspend fun invalidate() { + actualizeAll() + } +} diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalue/AutoRecacheWriteKeyValueRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalue/AutoRecacheWriteKeyValueRepo.kt new file mode 100644 index 00000000000..8a725629b36 --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalue/AutoRecacheWriteKeyValueRepo.kt @@ -0,0 +1,54 @@ +package dev.inmo.micro_utils.repos.cache.fallback.keyvalue + +import dev.inmo.micro_utils.coroutines.plus +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.repos.WriteKeyValueRepo +import dev.inmo.micro_utils.repos.cache.cache.FullKVCache +import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo +import dev.inmo.micro_utils.repos.set +import dev.inmo.micro_utils.repos.unset +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +open class AutoRecacheWriteKeyValueRepo( + protected val originalRepo: WriteKeyValueRepo, + protected val scope: CoroutineScope, + protected val kvCache: FullKVCache = FullKVCache() +) : WriteKeyValueRepo, FallbackCacheRepo { + override val onValueRemoved: Flow + get() = (originalRepo.onValueRemoved + kvCache.onValueRemoved).distinctUntilChanged() + + override val onNewValue: Flow> + get() = (originalRepo.onNewValue + kvCache.onNewValue).distinctUntilChanged() + + private val onRemovingUpdatesListeningJob = originalRepo.onValueRemoved.subscribeSafelyWithoutExceptions(scope) { + kvCache.unset(it) + } + + private val onNewAndUpdatedObjectsListeningJob = originalRepo.onNewValue.subscribeSafelyWithoutExceptions(scope) { + kvCache.set(it.first, it.second) + } + + override suspend fun unsetWithValues(toUnset: List) = originalRepo.unsetWithValues( + toUnset + ).also { + kvCache.unsetWithValues(toUnset) + } + + override suspend fun unset(toUnset: List) = originalRepo.unset( + toUnset + ).also { + kvCache.unset(toUnset) + } + + override suspend fun set(toSet: Map) = originalRepo.set( + toSet + ).also { + kvCache.set(toSet) + } + + override suspend fun invalidate() { + kvCache.clear() + } +} diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalues/AutoRecacheKeyValueRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalues/AutoRecacheKeyValueRepo.kt new file mode 100644 index 00000000000..c51f852932c --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalues/AutoRecacheKeyValueRepo.kt @@ -0,0 +1,37 @@ +package dev.inmo.micro_utils.repos.cache.fallback.keyvalues + +import dev.inmo.micro_utils.repos.KeyValuesRepo +import dev.inmo.micro_utils.repos.WriteKeyValuesRepo +import dev.inmo.micro_utils.repos.cache.cache.FullKVCache +import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper +import kotlinx.coroutines.CoroutineScope +import kotlin.time.Duration.Companion.seconds + +open class AutoRecacheKeyValuesRepo( + override val originalRepo: KeyValuesRepo, + scope: CoroutineScope, + kvCache: FullKVCache> = FullKVCache(), + recacheDelay: Long = 60.seconds.inWholeMilliseconds, + actionWrapper: ActionWrapper = ActionWrapper.Direct +) : AutoRecacheReadKeyValuesRepo ( + originalRepo, + scope, + kvCache, + recacheDelay, + actionWrapper +), + WriteKeyValuesRepo by AutoRecacheWriteKeyValuesRepo(originalRepo, scope, kvCache), + KeyValuesRepo { + + constructor( + originalRepo: KeyValuesRepo, + scope: CoroutineScope, + originalCallTimeoutMillis: Long, + kvCache: FullKVCache> = FullKVCache(), + recacheDelay: Long = 60.seconds.inWholeMilliseconds + ) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis)) + + override suspend fun clearWithValue(v: RegisteredObject) { + super.clearWithValue(v) + } +} diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalues/AutoRecacheReadKeyValuesRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalues/AutoRecacheReadKeyValuesRepo.kt new file mode 100644 index 00000000000..20b521771b2 --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalues/AutoRecacheReadKeyValuesRepo.kt @@ -0,0 +1,145 @@ +package dev.inmo.micro_utils.repos.cache.fallback.keyvalues + +import dev.inmo.micro_utils.coroutines.runCatchingSafely +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.PaginationResult +import dev.inmo.micro_utils.pagination.changeResultsUnchecked +import dev.inmo.micro_utils.pagination.createPaginationResult +import dev.inmo.micro_utils.pagination.emptyPaginationResult +import dev.inmo.micro_utils.pagination.firstIndex +import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging +import dev.inmo.micro_utils.pagination.utils.optionallyReverse +import dev.inmo.micro_utils.pagination.utils.paginate +import dev.inmo.micro_utils.repos.ReadKeyValuesRepo +import dev.inmo.micro_utils.repos.cache.cache.FullKVCache +import dev.inmo.micro_utils.repos.cache.fallback.ActionWrapper +import dev.inmo.micro_utils.repos.cache.util.actualizeAll +import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo +import dev.inmo.micro_utils.repos.set +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +open class AutoRecacheReadKeyValuesRepo( + protected open val originalRepo: ReadKeyValuesRepo, + protected val scope: CoroutineScope, + protected val kvCache: FullKVCache> = FullKVCache(), + protected val recacheDelay: Long = 60.seconds.inWholeMilliseconds, + protected val actionWrapper: ActionWrapper = ActionWrapper.Direct +) : ReadKeyValuesRepo, FallbackCacheRepo { + val autoUpdateJob = scope.launch { + while (isActive) { + actualizeAll() + + delay(recacheDelay) + } + } + + constructor( + originalRepo: ReadKeyValuesRepo, + scope: CoroutineScope, + originalCallTimeoutMillis: Long, + kvCache: FullKVCache> = FullKVCache(), + recacheDelay: Long = 60.seconds.inWholeMilliseconds + ) : this(originalRepo, scope, kvCache, recacheDelay, ActionWrapper.Timeouted(originalCallTimeoutMillis)) + + protected open suspend fun actualizeAll(): Result { + return runCatchingSafely { + kvCache.actualizeAll(originalRepo) + } + } + + override suspend fun contains(k: Id): Boolean = actionWrapper.wrap { + originalRepo.contains(k) + }.getOrElse { + kvCache.contains(k) + } + + override suspend fun count(): Long = actionWrapper.wrap { + originalRepo.count() + }.getOrElse { + kvCache.count() + } + + override suspend fun keys( + v: RegisteredObject, + pagination: Pagination, + reversed: Boolean + ): PaginationResult = actionWrapper.wrap { + originalRepo.keys(v, pagination, reversed) + }.getOrElse { + val results = mutableListOf() + + val toSkip = pagination.firstIndex + var count = 0 + + doForAllWithNextPaging { + kvCache.keys(pagination, reversed).also { + it.results.forEach { + if (kvCache.get(it) ?.contains(v) == true) { + count++ + if (count < toSkip || results.size >= pagination.size) { + return@forEach + } else { + results.add(it) + } + } + } + } + } + + return@getOrElse results.createPaginationResult( + pagination, + count.toLong() + ) + } + + override suspend fun keys( + pagination: Pagination, + reversed: Boolean + ): PaginationResult = actionWrapper.wrap { + originalRepo.keys(pagination, reversed) + }.getOrElse { kvCache.keys(pagination, reversed) } + + override suspend fun get( + k: Id, + pagination: Pagination, + reversed: Boolean + ): PaginationResult = actionWrapper.wrap { + originalRepo.get(k, pagination, reversed) + }.getOrNull() ?.also { + it.results.forEach { + kvCache.set(k, ((kvCache.get(k) ?: return@also) + it).distinct()) + } + } ?: kvCache.get(k) ?.run { + paginate(pagination.optionallyReverse(size, reversed)).let { + if (reversed) { + it.changeResultsUnchecked( + it.results.reversed() + ) + } else { + it + } + } + } ?: emptyPaginationResult() + + override suspend fun count(k: Id): Long = actionWrapper.wrap { + originalRepo.count(k) + }.getOrElse { + kvCache.get(k) ?.size ?.toLong() ?: 0L + } + + override suspend fun contains(k: Id, v: RegisteredObject): Boolean { + return (actionWrapper.wrap { + originalRepo.contains(k, v) + }.getOrNull() ?.also { + kvCache.set(k, ((kvCache.get(k) ?: return@also) + v).distinct()) + }) ?: (kvCache.get(k) ?.contains(v) == true) + } + + override suspend fun invalidate() { + actualizeAll() + } +} diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalues/AutoRecacheWriteKeyValuesRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalues/AutoRecacheWriteKeyValuesRepo.kt new file mode 100644 index 00000000000..73cf0551472 --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/fallback/keyvalues/AutoRecacheWriteKeyValuesRepo.kt @@ -0,0 +1,86 @@ +package dev.inmo.micro_utils.repos.cache.fallback.keyvalues + +import dev.inmo.micro_utils.coroutines.plus +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.pagination.FirstPagePagination +import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging +import dev.inmo.micro_utils.repos.WriteKeyValuesRepo +import dev.inmo.micro_utils.repos.cache.cache.FullKVCache +import dev.inmo.micro_utils.repos.cache.FallbackCacheRepo +import dev.inmo.micro_utils.repos.set +import dev.inmo.micro_utils.repos.unset +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +open class AutoRecacheWriteKeyValuesRepo( + protected val originalRepo: WriteKeyValuesRepo, + protected val scope: CoroutineScope, + protected val kvCache: FullKVCache> = FullKVCache() +) : WriteKeyValuesRepo, FallbackCacheRepo { + override val onValueRemoved: Flow> + get() = originalRepo.onValueRemoved + + override val onNewValue: Flow> + get() = originalRepo.onNewValue + override val onDataCleared: Flow + get() = (originalRepo.onDataCleared + kvCache.onValueRemoved).distinctUntilChanged() + + private val onDataClearedListeningJob = originalRepo.onDataCleared.subscribeSafelyWithoutExceptions(scope) { + kvCache.unset(it) + } + + private val onRemovingUpdatesListeningJob = originalRepo.onValueRemoved.subscribeSafelyWithoutExceptions(scope) { + kvCache.set( + it.first, + (kvCache.get( + it.first + ) ?: return@subscribeSafelyWithoutExceptions) - it.second + ) + } + + private val onNewAndUpdatedObjectsListeningJob = originalRepo.onNewValue.subscribeSafelyWithoutExceptions(scope) { + kvCache.set( + it.first, + (kvCache.get( + it.first + ) ?: return@subscribeSafelyWithoutExceptions) + it.second + ) + } + + override suspend fun clearWithValue(v: RegisteredObject) { + originalRepo.clearWithValue(v) + doForAllWithNextPaging(FirstPagePagination(kvCache.count().takeIf { it < Int.MAX_VALUE } ?.toInt() ?: Int.MAX_VALUE)) { + kvCache.keys(it).also { + it.results.forEach { id -> + kvCache.get(id) ?.takeIf { it.contains(v) } ?.let { + kvCache.unset(id) + } + } + } + } + } + + override suspend fun clear(k: Id) { + originalRepo.clear(k) + kvCache.unset(k) + } + + override suspend fun remove(toRemove: Map>) { + originalRepo.remove(toRemove) + toRemove.forEach { (k, v) -> + kvCache.set(k, (kvCache.get(k) ?: return@forEach) - v) + } + } + + override suspend fun add(toAdd: Map>) { + originalRepo.add(toAdd) + toAdd.forEach { (k, v) -> + kvCache.set(k, (kvCache.get(k) ?: return@forEach) + v) + } + } + + override suspend fun invalidate() { + kvCache.clear() + } +} diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullCRUDCacheRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullCRUDCacheRepo.kt index 7878d897668..0b18d436001 100644 --- a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullCRUDCacheRepo.kt +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullCRUDCacheRepo.kt @@ -3,11 +3,9 @@ package dev.inmo.micro_utils.repos.cache.full import dev.inmo.micro_utils.common.* import dev.inmo.micro_utils.pagination.Pagination import dev.inmo.micro_utils.pagination.PaginationResult -import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging import dev.inmo.micro_utils.repos.* import dev.inmo.micro_utils.repos.cache.* import dev.inmo.micro_utils.repos.cache.cache.FullKVCache -import dev.inmo.micro_utils.repos.cache.cache.KVCache import dev.inmo.micro_utils.repos.cache.util.actualizeAll import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullKeyValueCacheRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullKeyValueCacheRepo.kt index 635e5754724..81927bb1b80 100644 --- a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullKeyValueCacheRepo.kt +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullKeyValueCacheRepo.kt @@ -9,6 +9,7 @@ import dev.inmo.micro_utils.repos.cache.util.actualizeAll import dev.inmo.micro_utils.repos.pagination.getAll import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* open class FullReadKeyValueCacheRepo( @@ -80,7 +81,7 @@ fun ReadKeyValueRepo.cached( ) = FullReadKeyValueCacheRepo(this, kvCache) open class FullWriteKeyValueCacheRepo( - protected open val parentRepo: WriteKeyValueRepo, + parentRepo: WriteKeyValueRepo, protected open val kvCache: FullKVCache, scope: CoroutineScope = CoroutineScope(Dispatchers.Default) ) : WriteKeyValueRepo by parentRepo, FullCacheRepo { @@ -98,7 +99,7 @@ fun WriteKeyValueRepo.caching( ) = FullWriteKeyValueCacheRepo(this, kvCache, scope) open class FullKeyValueCacheRepo( - override val parentRepo: KeyValueRepo, + protected open val parentRepo: KeyValueRepo, kvCache: FullKVCache, scope: CoroutineScope = CoroutineScope(Dispatchers.Default) ) : FullWriteKeyValueCacheRepo(parentRepo, kvCache, scope), diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullKeyValuesCacheRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullKeyValuesCacheRepo.kt index e91ef6dbcb1..fc4bf34db0b 100644 --- a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullKeyValuesCacheRepo.kt +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/FullKeyValuesCacheRepo.kt @@ -112,7 +112,7 @@ fun ReadKeyValuesRepo.cached( ) = FullReadKeyValuesCacheRepo(this, kvCache) open class FullWriteKeyValuesCacheRepo( - protected open val parentRepo: WriteKeyValuesRepo, + parentRepo: WriteKeyValuesRepo, protected open val kvCache: FullKVCache>, scope: CoroutineScope = CoroutineScope(Dispatchers.Default) ) : WriteKeyValuesRepo by parentRepo, FullCacheRepo { @@ -140,7 +140,7 @@ fun WriteKeyValuesRepo.caching( ) = FullWriteKeyValuesCacheRepo(this, kvCache, scope) open class FullKeyValuesCacheRepo( - override val parentRepo: KeyValuesRepo, + protected open val parentRepo: KeyValuesRepo, kvCache: FullKVCache>, scope: CoroutineScope = CoroutineScope(Dispatchers.Default) ) : FullWriteKeyValuesCacheRepo(parentRepo, kvCache, scope), diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/util/ActualizeAll.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/util/ActualizeAll.kt index a657d6b8b45..61287183ff7 100644 --- a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/util/ActualizeAll.kt +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/util/ActualizeAll.kt @@ -2,60 +2,52 @@ package dev.inmo.micro_utils.repos.cache.util import dev.inmo.micro_utils.pagination.FirstPagePagination import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging +import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging import dev.inmo.micro_utils.repos.ReadCRUDRepo import dev.inmo.micro_utils.repos.ReadKeyValueRepo import dev.inmo.micro_utils.repos.ReadKeyValuesRepo import dev.inmo.micro_utils.repos.cache.cache.KVCache +import dev.inmo.micro_utils.repos.pagination.getAll import dev.inmo.micro_utils.repos.set suspend inline fun KVCache.actualizeAll( + clear: Boolean = true, getAll: () -> Map ) { - clear() - set(getAll()) + set( + getAll().also { + if (clear) { + clear() + } + } + ) } suspend inline fun KVCache.actualizeAll( - repo: ReadKeyValueRepo + repo: ReadKeyValueRepo, + clear: Boolean = true, ) { - clear() - val count = repo.count().takeIf { it < Int.MAX_VALUE } ?.toInt() ?: Int.MAX_VALUE - val initPagination = FirstPagePagination(count) - doForAllWithNextPaging(initPagination) { - keys(it).also { - set( - it.results.mapNotNull { k -> repo.get(k) ?.let { k to it } } - ) - } + actualizeAll(clear) { + repo.getAll { keys(it) }.toMap() } } suspend inline fun KVCache>.actualizeAll( - repo: ReadKeyValuesRepo + repo: ReadKeyValuesRepo, + clear: Boolean = true, ) { - clear() - val count = repo.count().takeIf { it < Int.MAX_VALUE } ?.toInt() ?: Int.MAX_VALUE - val initPagination = FirstPagePagination(count) - doForAllWithNextPaging(initPagination) { - keys(it).also { - set( - it.results.associateWith { k -> repo.getAll(k) } - ) - } + actualizeAll(clear) { + repo.getAll { keys(it) }.toMap() } } suspend inline fun KVCache.actualizeAll( - repo: ReadCRUDRepo + repo: ReadCRUDRepo, + clear: Boolean = true, ) { - clear() - val count = repo.count().takeIf { it < Int.MAX_VALUE } ?.toInt() ?: Int.MAX_VALUE - val initPagination = FirstPagePagination(count) - doForAllWithNextPaging(initPagination) { - keys(it).also { - set( - it.results.mapNotNull { k -> repo.getById(k) ?.let { k to it } } - ) - } + actualizeAll(clear) { + repo.getAllByWithNextPaging { + getIdsByPagination(it) + }.mapNotNull { it to (repo.getById(it) ?: return@mapNotNull null) }.toMap() } } diff --git a/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kv/Factories.kt b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kv/Factories.kt new file mode 100644 index 00000000000..5edb5476565 --- /dev/null +++ b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kv/Factories.kt @@ -0,0 +1,12 @@ +package dev.inmo.micro_utils.repos.transforms.kv + +import dev.inmo.micro_utils.repos.KeyValueRepo +import dev.inmo.micro_utils.repos.KeyValuesRepo +import dev.inmo.micro_utils.repos.ReadKeyValueRepo +import dev.inmo.micro_utils.repos.ReadKeyValuesRepo +import kotlin.js.JsName +import kotlin.jvm.JvmName + +fun ReadKeyValuesRepo.asReadKeyValueRepo() = ReadKeyValueFromKeyValuesRepo(this) + +fun KeyValuesRepo.asKeyValueRepo() = KeyValueFromKeyValuesRepo(this) diff --git a/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kv/KeyValueFromKeyValuesRepo.kt b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kv/KeyValueFromKeyValuesRepo.kt new file mode 100644 index 00000000000..235ceafd701 --- /dev/null +++ b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kv/KeyValueFromKeyValuesRepo.kt @@ -0,0 +1,41 @@ +package dev.inmo.micro_utils.repos.transforms.kv + +import dev.inmo.micro_utils.pagination.FirstPagePagination +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.PaginationResult +import dev.inmo.micro_utils.pagination.changeResults +import dev.inmo.micro_utils.pagination.changeResultsUnchecked +import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging +import dev.inmo.micro_utils.pagination.utils.optionallyReverse +import dev.inmo.micro_utils.pagination.utils.paginate +import dev.inmo.micro_utils.repos.KeyValueRepo +import dev.inmo.micro_utils.repos.KeyValuesRepo +import dev.inmo.micro_utils.repos.ReadKeyValueRepo +import dev.inmo.micro_utils.repos.ReadKeyValuesRepo +import dev.inmo.micro_utils.repos.transforms.kvs.ReadKeyValuesFromKeyValueRepo +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge + +open class KeyValueFromKeyValuesRepo( + private val original: KeyValuesRepo +) : KeyValueRepo>, ReadKeyValueFromKeyValuesRepo(original) { + override val onNewValue: Flow>> = merge( + original.onNewValue, + original.onValueRemoved + ).mapNotNull { + it.first to (get(it.first) ?: return@mapNotNull null) + } + override val onValueRemoved: Flow = original.onDataCleared + + override suspend fun unset(toUnset: List) { + toUnset.forEach { + original.clear(it) + } + } + + override suspend fun set(toSet: Map>) { + original.set(toSet) + } +} diff --git a/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kv/ReadKeyValueFromKeyValuesRepo.kt b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kv/ReadKeyValueFromKeyValuesRepo.kt new file mode 100644 index 00000000000..9efbdd36403 --- /dev/null +++ b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kv/ReadKeyValueFromKeyValuesRepo.kt @@ -0,0 +1,67 @@ +package dev.inmo.micro_utils.repos.transforms.kv + +import dev.inmo.micro_utils.pagination.FirstPagePagination +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.PaginationResult +import dev.inmo.micro_utils.pagination.changeResults +import dev.inmo.micro_utils.pagination.changeResultsUnchecked +import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging +import dev.inmo.micro_utils.pagination.utils.optionallyReverse +import dev.inmo.micro_utils.pagination.utils.paginate +import dev.inmo.micro_utils.repos.ReadKeyValueRepo +import dev.inmo.micro_utils.repos.ReadKeyValuesRepo +import dev.inmo.micro_utils.repos.transforms.kvs.ReadKeyValuesFromKeyValueRepo + +open class ReadKeyValueFromKeyValuesRepo( + private val original: ReadKeyValuesRepo +) : ReadKeyValueRepo> { + override suspend fun get(k: Key): List? = original.getAll(k) + + override suspend fun values( + pagination: Pagination, + reversed: Boolean + ): PaginationResult> { + val keys = keys(pagination, reversed) + return keys.changeResults( + keys.results.mapNotNull { + get(it) + } + ) + } + + override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult { + return original.keys(pagination, reversed) + } + + override suspend fun count(): Long { + return original.count() + } + + override suspend fun contains(key: Key): Boolean { + return original.contains(key) + } + + override suspend fun keys(v: List, pagination: Pagination, reversed: Boolean): PaginationResult { + val keys = mutableSetOf() + + doForAllWithNextPaging(FirstPagePagination(count().toInt())) { + original.keys(it).also { + it.results.forEach { + val values = get(it) ?: return@forEach + if (values.containsAll(v) && v.containsAll(values)) { + keys.add(it) + } + } + } + } + + val paginated = keys.paginate( + pagination.optionallyReverse(keys.count(), reversed) + ) + return if (reversed) { + paginated.changeResultsUnchecked(paginated.results.reversed()) + } else { + paginated + } + } +} diff --git a/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kvs/Factories.kt b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kvs/Factories.kt new file mode 100644 index 00000000000..92c6abb1795 --- /dev/null +++ b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kvs/Factories.kt @@ -0,0 +1,21 @@ +package dev.inmo.micro_utils.repos.transforms.kvs + +import dev.inmo.micro_utils.repos.KeyValueRepo +import dev.inmo.micro_utils.repos.ReadKeyValueRepo +import kotlin.js.JsName +import kotlin.jvm.JvmName + + +fun > ReadKeyValueRepo.asReadKeyValuesRepo() = ReadKeyValuesFromKeyValueRepo(this) + +fun > KeyValueRepo.asKeyValuesRepo( + listToValuesIterable: suspend (List) -> VI +): KeyValuesFromKeyValueRepo = KeyValuesFromKeyValueRepo(this, listToValuesIterable) + +@JvmName("asListKeyValuesRepo") +@JsName("asListKeyValuesRepo") +fun KeyValueRepo>.asKeyValuesRepo(): KeyValuesFromKeyValueRepo> = asKeyValuesRepo { it } + +@JvmName("asSetKeyValuesRepo") +@JsName("asSetKeyValuesRepo") +fun KeyValueRepo>.asKeyValuesRepo(): KeyValuesFromKeyValueRepo> = asKeyValuesRepo { it.toSet() } diff --git a/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kvs/KeyValuesFromKeyValueRepo.kt b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kvs/KeyValuesFromKeyValueRepo.kt new file mode 100644 index 00000000000..7473599f14f --- /dev/null +++ b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kvs/KeyValuesFromKeyValueRepo.kt @@ -0,0 +1,77 @@ +package dev.inmo.micro_utils.repos.transforms.kvs + +import dev.inmo.micro_utils.pagination.FirstPagePagination +import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging +import dev.inmo.micro_utils.repos.KeyValueRepo +import dev.inmo.micro_utils.repos.KeyValuesRepo +import dev.inmo.micro_utils.repos.unset +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlin.js.JsName +import kotlin.jvm.JvmName + +open class KeyValuesFromKeyValueRepo>( + private val original: KeyValueRepo, + private val listToValuesIterable: suspend (List) -> ValuesIterable +) : KeyValuesRepo, ReadKeyValuesFromKeyValueRepo(original) { + private val _onNewValue = MutableSharedFlow>() + private val _onValueRemoved = MutableSharedFlow>() + override val onNewValue: Flow> = _onNewValue.asSharedFlow() + override val onValueRemoved: Flow> = _onValueRemoved.asSharedFlow() + override val onDataCleared: Flow = original.onValueRemoved + + override suspend fun clearWithValue(v: Value) { + val keys = mutableSetOf() + + doForAllWithNextPaging(FirstPagePagination(count().toInt())) { + original.keys(it).also { + it.results.forEach { + if (contains(it, v)) { + keys.add(it) + } + } + } + } + + original.unset(keys.toList()) + } + + override suspend fun clear(k: Key) { + original.unset(k) + } + + override suspend fun remove(toRemove: Map>) { + original.set( + toRemove.mapNotNull { (k, removing) -> + val exists = original.get(k) ?: return@mapNotNull null + k to listToValuesIterable(exists - removing).also { + if (it.firstOrNull() == null) { + original.unset(k) + return@mapNotNull null + } + } + }.toMap() + ) + toRemove.forEach { (k, v) -> + v.forEach { + _onValueRemoved.emit(k to it) + } + } + } + + override suspend fun add(toAdd: Map>) { + original.set( + toAdd.mapNotNull { (k, adding) -> + val exists = original.get(k) ?: emptyList() + k to listToValuesIterable(exists + adding) + }.toMap() + ) + toAdd.forEach { (k, v) -> + v.forEach { + _onNewValue.emit(k to it) + } + } + } + +} diff --git a/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kvs/ReadKeyValuesFromKeyValueRepo.kt b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kvs/ReadKeyValuesFromKeyValueRepo.kt new file mode 100644 index 00000000000..21491740076 --- /dev/null +++ b/repos/common/src/commonMain/kotlin/dev/inmo/micro_utils/repos/transforms/kvs/ReadKeyValuesFromKeyValueRepo.kt @@ -0,0 +1,72 @@ +package dev.inmo.micro_utils.repos.transforms.kvs + +import dev.inmo.micro_utils.pagination.FirstPagePagination +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.PaginationResult +import dev.inmo.micro_utils.pagination.changeResultsUnchecked +import dev.inmo.micro_utils.pagination.emptyPaginationResult +import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging +import dev.inmo.micro_utils.pagination.utils.optionallyReverse +import dev.inmo.micro_utils.pagination.utils.paginate +import dev.inmo.micro_utils.repos.ReadKeyValueRepo +import dev.inmo.micro_utils.repos.ReadKeyValuesRepo + +open class ReadKeyValuesFromKeyValueRepo>( + private val original: ReadKeyValueRepo +) : ReadKeyValuesRepo { + override suspend fun get( + k: Key, + pagination: Pagination, + reversed: Boolean + ): PaginationResult { + val iterable = original.get(k) ?: return emptyPaginationResult(pagination) + val paginated = iterable.paginate( + pagination.optionallyReverse(iterable.count(), reversed) + ) + return if (reversed) { + paginated.changeResultsUnchecked(paginated.results.reversed()) + } else { + paginated + } + } + + override suspend fun keys( + pagination: Pagination, + reversed: Boolean + ): PaginationResult = original.keys(pagination, reversed) + + override suspend fun count(): Long = original.count() + + override suspend fun count(k: Key): Long = original.get(k) ?.count() ?.toLong() ?: 0L + + override suspend fun contains(k: Key, v: Value): Boolean = original.get(k) ?.contains(v) == true + + override suspend fun contains(k: Key): Boolean = original.contains(k) + + override suspend fun keys( + v: Value, + pagination: Pagination, + reversed: Boolean + ): PaginationResult { + val keys = mutableSetOf() + + doForAllWithNextPaging(FirstPagePagination(count().toInt())) { + original.keys(it).also { + it.results.forEach { + if (contains(it, v)) { + keys.add(it) + } + } + } + } + + val paginated = keys.paginate( + pagination.optionallyReverse(keys.count(), reversed) + ) + return if (reversed) { + paginated.changeResultsUnchecked(paginated.results.reversed()) + } else { + paginated + } + } +} diff --git a/repos/common/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/FileKeyValueRepo.kt b/repos/common/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/FileKeyValueRepo.kt index d753d1f9f32..92909c7949f 100644 --- a/repos/common/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/FileKeyValueRepo.kt +++ b/repos/common/src/jvmMain/kotlin/dev/inmo/micro_utils/repos/FileKeyValueRepo.kt @@ -31,11 +31,19 @@ class FileReadKeyValueRepo( override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult { val count = count() val resultPagination = if (reversed) pagination.reverse(count) else pagination - val filesPaths = folder.list() ?.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndexExclusive) ?: return emptyPaginationResult() - if (reversed) { - filesPaths.reverse() + val filesList = folder.list() + val files: Array = if (resultPagination.firstIndex < count) { + val filesPaths = filesList.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndexExclusive.coerceAtMost(filesList.size)) + + if (reversed) { + filesPaths.reversedArray() + } else { + filesPaths + } + } else { + emptyArray() } - return filesPaths.map { File(folder, it) }.createPaginationResult( + return files.map { File(folder, it) }.createPaginationResult( resultPagination, count ) @@ -44,11 +52,21 @@ class FileReadKeyValueRepo( override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult { val count = count() val resultPagination = if (reversed) pagination.reverse(count) else pagination - val filesPaths = folder.list() ?.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndexExclusive) ?: return emptyPaginationResult() - if (reversed) { - filesPaths.reverse() + val filesList = folder.list() + + val files: Array = if (resultPagination.firstIndex < count) { + val filesPaths = filesList.copyOfRange(resultPagination.firstIndex, resultPagination.lastIndexExclusive.coerceAtMost(filesList.size)) + + if (reversed) { + filesPaths.reversedArray() + } else { + filesPaths + } + } else { + emptyArray() } - return filesPaths.toList().createPaginationResult( + + return files.toList().createPaginationResult( resultPagination, count )