diff --git a/CHANGELOG.md b/CHANGELOG.md index b2ac8e30cf3..22ca27d6f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## 0.23.0 +## 0.22.9 + +* `Repos`: + * `Cache`: + * Add direct caching repos + ## 0.22.8 * `Common`: diff --git a/gradle.properties b/gradle.properties index a0bc7c4fcb2..8cad02a585c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,4 +16,4 @@ crypto_js_version=4.1.1 group=dev.inmo version=0.23.0 -android_code_version=275 +android_code_version=276 diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullCRUDCacheRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullCRUDCacheRepo.kt new file mode 100644 index 00000000000..1a39813ce40 --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullCRUDCacheRepo.kt @@ -0,0 +1,113 @@ +package dev.inmo.micro_utils.repos.cache.full.direct + +import dev.inmo.micro_utils.common.* +import dev.inmo.micro_utils.coroutines.SmartRWLocker +import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions +import dev.inmo.micro_utils.coroutines.withReadAcquire +import dev.inmo.micro_utils.coroutines.withWriteLock +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.PaginationResult +import dev.inmo.micro_utils.repos.* +import dev.inmo.micro_utils.repos.cache.* +import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode +import dev.inmo.micro_utils.repos.cache.util.actualizeAll +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers + +open class DirectFullReadCRUDCacheRepo( + protected open val parentRepo: ReadCRUDRepo, + protected open val kvCache: KeyValueRepo, + protected val locker: SmartRWLocker = SmartRWLocker(), + protected open val idGetter: (ObjectType) -> IdType +) : ReadCRUDRepo, DirectFullCacheRepo { + protected open suspend fun actualizeAll() { + kvCache.actualizeAll(parentRepo, locker = locker) + } + + override suspend fun getByPagination(pagination: Pagination): PaginationResult = locker.withReadAcquire { + kvCache.values(pagination) + } + + override suspend fun getIdsByPagination(pagination: Pagination): PaginationResult = locker.withReadAcquire { + kvCache.keys(pagination) + } + + override suspend fun count(): Long = locker.withReadAcquire { + kvCache.count() + } + + override suspend fun contains(id: IdType): Boolean = locker.withReadAcquire { + kvCache.contains(id) + } + + override suspend fun getAll(): Map = locker.withReadAcquire { + kvCache.getAll() + } + + override suspend fun getById(id: IdType): ObjectType? = locker.withReadAcquire { + kvCache.get(id) + } + + override suspend fun invalidate() { + actualizeAll() + } +} + +fun ReadCRUDRepo.directlyCached( + kvCache: KeyValueRepo, + locker: SmartRWLocker = SmartRWLocker(), + idGetter: (ObjectType) -> IdType +) = DirectFullReadCRUDCacheRepo(this, kvCache, locker, idGetter) + +open class DirectFullCRUDCacheRepo( + override val parentRepo: CRUDRepo, + kvCache: KeyValueRepo, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), + skipStartInvalidate: Boolean = false, + locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), + idGetter: (ObjectType) -> IdType +) : DirectFullReadCRUDCacheRepo( + parentRepo, + kvCache, + locker, + idGetter +), + WriteCRUDRepo by WriteCRUDCacheRepo( + parentRepo, + kvCache, + scope, + locker, + idGetter + ), + CRUDRepo { + init { + if (!skipStartInvalidate) { + scope.launchSafelyWithoutExceptions { + if (locker.writeMutex.isLocked) { + initialInvalidate() + } else { + invalidate() + } + } + } + } + + protected open suspend fun initialInvalidate() { + try { + kvCache.actualizeAll(parentRepo, locker = null) + } finally { + locker.unlockWrite() + } + } + override suspend fun invalidate() { + actualizeAll() + } +} + +fun CRUDRepo.directFullyCached( + kvCache: KeyValueRepo = MapKeyValueRepo(), + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), + skipStartInvalidate: Boolean = false, + locker: SmartRWLocker = SmartRWLocker(), + idGetter: (ObjectType) -> IdType +) = DirectFullCRUDCacheRepo(this, kvCache, scope, skipStartInvalidate, locker, idGetter) diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullCacheRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullCacheRepo.kt new file mode 100644 index 00000000000..c66179478ad --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullCacheRepo.kt @@ -0,0 +1,13 @@ +package dev.inmo.micro_utils.repos.cache.full.direct + +import dev.inmo.micro_utils.repos.cache.full.FullCacheRepo + +/** + * Repos-inheritors MUST realize their methods via next logic: + * + * * Reloading of data in cache must be reactive (e.g. via Flow) or direct mutation methods usage (override set and + * mutate cache inside, for example) + * * All reading methods must take data from cache via synchronization with [dev.inmo.micro_utils.coroutines.SmartRWLocker] + */ +interface DirectFullCacheRepo : FullCacheRepo { +} \ No newline at end of file diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValueCacheRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValueCacheRepo.kt new file mode 100644 index 00000000000..41b0f66630b --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValueCacheRepo.kt @@ -0,0 +1,177 @@ +package dev.inmo.micro_utils.repos.cache.full.direct + +import dev.inmo.micro_utils.coroutines.SmartRWLocker +import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions +import dev.inmo.micro_utils.coroutines.withReadAcquire +import dev.inmo.micro_utils.coroutines.withWriteLock +import dev.inmo.micro_utils.pagination.Pagination +import dev.inmo.micro_utils.pagination.PaginationResult +import dev.inmo.micro_utils.repos.* +import dev.inmo.micro_utils.repos.cache.full.FullKeyValueCacheRepo +import dev.inmo.micro_utils.repos.cache.full.FullReadKeyValueCacheRepo +import dev.inmo.micro_utils.repos.cache.full.FullWriteKeyValueCacheRepo +import dev.inmo.micro_utils.repos.cache.util.actualizeAll +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +open class DirectFullReadKeyValueCacheRepo( + protected open val parentRepo: ReadKeyValueRepo, + protected open val kvCache: KeyValueRepo, + protected val locker: SmartRWLocker = SmartRWLocker() +) : DirectFullCacheRepo, ReadKeyValueRepo { + protected open suspend fun actualizeAll() { + kvCache.actualizeAll(parentRepo, locker) + } + + override suspend fun get(k: Key): Value? = locker.withReadAcquire { + kvCache.get(k) + } + + override suspend fun values(pagination: Pagination, reversed: Boolean): PaginationResult = locker.withReadAcquire { + kvCache.values(pagination, reversed) + } + + override suspend fun count(): Long = locker.withReadAcquire { + kvCache.count() + } + + override suspend fun contains(key: Key): Boolean = locker.withReadAcquire { + kvCache.contains(key) + } + + override suspend fun getAll(): Map = locker.withReadAcquire { + kvCache.getAll() + } + + override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult = locker.withReadAcquire { + kvCache.keys(pagination, reversed) + } + + override suspend fun keys(v: Value, pagination: Pagination, reversed: Boolean): PaginationResult = locker.withReadAcquire { + kvCache.keys(v, pagination, reversed) + } + + override suspend fun invalidate() { + actualizeAll() + } +} + +fun ReadKeyValueRepo.directlyCached( + kvCache: KeyValueRepo, + locker: SmartRWLocker = SmartRWLocker() +) = DirectFullReadKeyValueCacheRepo(this, kvCache, locker) + +open class DirectFullWriteKeyValueCacheRepo( + protected open val parentRepo: WriteKeyValueRepo, + protected open val kvCache: KeyValueRepo, + protected val locker: SmartRWLocker = SmartRWLocker(), + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), +) : DirectFullCacheRepo, WriteKeyValueRepo by parentRepo { + override val onNewValue: Flow> + get() = parentRepo.onNewValue + override val onValueRemoved: Flow + get() = parentRepo.onValueRemoved + + protected val onNewJob = parentRepo.onNewValue.onEach { + locker.withWriteLock { + kvCache.set(it.first, it.second) + } + }.launchIn(scope) + protected val onRemoveJob = parentRepo.onValueRemoved.onEach { + locker.withWriteLock { + kvCache.unset(it) + } + }.launchIn(scope) + + override suspend fun invalidate() { + locker.withWriteLock { + kvCache.clear() + } + } + + override suspend fun unsetWithValues(toUnset: List) = parentRepo.unsetWithValues(toUnset) +} + +fun WriteKeyValueRepo.directlyCached( + kvCache: KeyValueRepo, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default) +) = DirectFullWriteKeyValueCacheRepo(this, kvCache, scope = scope) + +open class DirectFullKeyValueCacheRepo( + override val parentRepo: KeyValueRepo, + kvCache: KeyValueRepo, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), + skipStartInvalidate: Boolean = false, + locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), +) : DirectFullCacheRepo, + KeyValueRepo , + WriteKeyValueRepo by DirectFullWriteKeyValueCacheRepo( + parentRepo, + kvCache, + locker, + scope + ), + DirectFullReadKeyValueCacheRepo(parentRepo, kvCache, locker) { + init { + if (!skipStartInvalidate) { + scope.launchSafelyWithoutExceptions { + if (locker.writeMutex.isLocked) { + initialInvalidate() + } else { + invalidate() + } + } + } + } + + + protected open suspend fun initialInvalidate() { + try { + kvCache.actualizeAll(parentRepo, locker = null) + } finally { + locker.unlockWrite() + } + } + override suspend fun invalidate() { + kvCache.actualizeAll(parentRepo, locker) + } + + override suspend fun clear() { + parentRepo.clear() + kvCache.clear() + } + + override suspend fun unsetWithValues(toUnset: List) = parentRepo.unsetWithValues(toUnset) + + override suspend fun set(toSet: Map) { + locker.withWriteLock { + parentRepo.set(toSet) + kvCache.set( + toSet.filter { + parentRepo.contains(it.key) + } + ) + } + } + + override suspend fun unset(toUnset: List) { + locker.withWriteLock { + parentRepo.unset(toUnset) + kvCache.unset( + toUnset.filter { + !parentRepo.contains(it) + } + ) + } + } +} + +fun KeyValueRepo.directlyFullyCached( + kvCache: KeyValueRepo = MapKeyValueRepo(), + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), + skipStartInvalidate: Boolean = false, + locker: SmartRWLocker = SmartRWLocker() +) = DirectFullKeyValueCacheRepo(this, kvCache, scope, skipStartInvalidate, locker) diff --git a/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValuesCacheRepo.kt b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValuesCacheRepo.kt new file mode 100644 index 00000000000..a16d3700c9d --- /dev/null +++ b/repos/cache/src/commonMain/kotlin/dev/inmo/micro_utils/repos/cache/full/direct/DirectFullKeyValuesCacheRepo.kt @@ -0,0 +1,243 @@ +package dev.inmo.micro_utils.repos.cache.full.direct + +import dev.inmo.micro_utils.common.* +import dev.inmo.micro_utils.coroutines.SmartRWLocker +import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions +import dev.inmo.micro_utils.coroutines.withReadAcquire +import dev.inmo.micro_utils.coroutines.withWriteLock +import dev.inmo.micro_utils.pagination.* +import dev.inmo.micro_utils.pagination.utils.* +import dev.inmo.micro_utils.repos.* +import dev.inmo.micro_utils.repos.cache.util.ActualizeAllClearMode +import dev.inmo.micro_utils.repos.cache.util.actualizeAll +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* + +open class DirectFullReadKeyValuesCacheRepo( + protected open val parentRepo: ReadKeyValuesRepo, + protected open val kvCache: KeyValueRepo>, + protected val locker: SmartRWLocker = SmartRWLocker(), +) : ReadKeyValuesRepo, DirectFullCacheRepo { + protected open suspend fun actualizeKey(k: Key) { + kvCache.actualizeAll(locker = locker, clearMode = ActualizeAllClearMode.Never) { + mapOf(k to parentRepo.getAll(k)) + } + } + + protected open suspend fun actualizeAll() { + kvCache.actualizeAll(parentRepo, locker = locker) + } + + override suspend fun get(k: Key, pagination: Pagination, reversed: Boolean): PaginationResult { + return locker.withReadAcquire { + kvCache.get(k) ?.paginate( + pagination.let { if (reversed) it.reverse(count(k)) else it } + ) ?.let { + if (reversed) it.copy(results = it.results.reversed()) else it + } + } ?: emptyPaginationResult() + } + + override suspend fun getAll(k: Key, reversed: Boolean): List { + return locker.withReadAcquire { + kvCache.get(k) ?.optionallyReverse(reversed) + } ?: emptyList() + } + + override suspend fun getAll(reverseLists: Boolean): Map> { + return locker.withReadAcquire { + kvCache.getAll().takeIf { it.isNotEmpty() } ?.let { + if (reverseLists) { + it.mapValues { it.value.reversed() } + } else { + it + } + } + } ?: emptyMap() + } + override suspend fun keys(pagination: Pagination, reversed: Boolean): PaginationResult { + return locker.withReadAcquire { + kvCache.keys(pagination, reversed).takeIf { it.results.isNotEmpty() } + } ?: emptyPaginationResult() + } + + override suspend fun count(): Long = locker.withReadAcquire { kvCache.count() } + + override suspend fun count(k: Key): Long = locker.withReadAcquire { kvCache.get(k) ?.size } ?.toLong() ?: 0L + + override suspend fun contains(k: Key, v: Value): Boolean = locker.withReadAcquire { kvCache.get(k) ?.contains(v) } == true + + override suspend fun contains(k: Key): Boolean = locker.withReadAcquire { kvCache.contains(k) } + + override suspend fun keys( + v: Value, + pagination: Pagination, + reversed: Boolean + ): PaginationResult { + val keys = locker.withReadAcquire { + getAllWithNextPaging { kvCache.keys(it) }.filter { kvCache.get(it)?.contains(v) == true } + .optionallyReverse(reversed) + } + val result = if (keys.isNotEmpty()) { + keys.paginate(pagination.optionallyReverse(keys.size, reversed)).takeIf { it.results.isNotEmpty() } + } else { + null + } + + return result ?: emptyPaginationResult() + } + + override suspend fun invalidate() { + actualizeAll() + } +} + +fun ReadKeyValuesRepo.directlyCached( + kvCache: KeyValueRepo>, + locker: SmartRWLocker = SmartRWLocker(), +) = DirectFullReadKeyValuesCacheRepo(this, kvCache, locker) + +open class DirectFullWriteKeyValuesCacheRepo( + parentRepo: WriteKeyValuesRepo, + protected open val kvCache: KeyValueRepo>, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), + protected val locker: SmartRWLocker = SmartRWLocker(), +) : WriteKeyValuesRepo by parentRepo, DirectFullCacheRepo { + protected val onNewJob = parentRepo.onNewValue.onEach { + locker.withWriteLock { + kvCache.set( + it.first, + kvCache.get(it.first) ?.plus(it.second) ?: listOf(it.second) + ) + } + }.launchIn(scope) + protected val onRemoveJob = parentRepo.onValueRemoved.onEach { + locker.withWriteLock { + kvCache.set( + it.first, + kvCache.get(it.first)?.minus(it.second) ?: return@onEach + ) + } + }.launchIn(scope) + + override suspend fun invalidate() { + locker.withWriteLock { + kvCache.clear() + } + } +} + +fun WriteKeyValuesRepo.directlyCached( + kvCache: KeyValueRepo>, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), + locker: SmartRWLocker = SmartRWLocker(), +) = DirectFullWriteKeyValuesCacheRepo(this, kvCache, scope, locker) + +open class DirectFullKeyValuesCacheRepo( + override val parentRepo: KeyValuesRepo, + kvCache: KeyValueRepo>, + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), + skipStartInvalidate: Boolean = false, + locker: SmartRWLocker = SmartRWLocker(writeIsLocked = !skipStartInvalidate), +) : KeyValuesRepo, + DirectFullReadKeyValuesCacheRepo(parentRepo, kvCache, locker), + WriteKeyValuesRepo by parentRepo { + init { + if (!skipStartInvalidate) { + scope.launchSafelyWithoutExceptions { + if (locker.writeMutex.isLocked) { + initialInvalidate() + } else { + invalidate() + } + } + } + } + + override suspend fun clearWithValue(v: Value) { + doAllWithCurrentPaging { + keys(v, it).also { + remove(it.results.associateWith { listOf(v) }) + } + } + } + + protected open suspend fun initialInvalidate() { + try { + kvCache.actualizeAll(parentRepo, locker = null) + } finally { + locker.unlockWrite() + } + } + override suspend fun invalidate() { + kvCache.actualizeAll(parentRepo, locker = locker) + } + + override suspend fun set(toSet: Map>) { + locker.withWriteLock { + parentRepo.set(toSet) + kvCache.set( + toSet.filter { + parentRepo.contains(it.key) + } + ) + } + } + + override suspend fun add(toAdd: Map>) { + locker.withWriteLock { + parentRepo.add(toAdd) + toAdd.forEach { + val filtered = it.value.filter { v -> + parentRepo.contains(it.key, v) + }.ifEmpty { + return@forEach + } + kvCache.set( + it.key, + (kvCache.get(it.key) ?: emptyList()) + filtered + ) + } + } + } + + override suspend fun remove(toRemove: Map>) { + locker.withWriteLock { + parentRepo.remove(toRemove) + toRemove.forEach { + val filtered = it.value.filter { v -> + !parentRepo.contains(it.key, v) + }.ifEmpty { + return@forEach + }.toSet() + val resultList = (kvCache.get(it.key) ?: emptyList()) - filtered + if (resultList.isEmpty()) { + kvCache.unset(it.key) + } else { + kvCache.set( + it.key, + resultList + ) + } + } + } + } + + override suspend fun clear(k: Key) { + locker.withWriteLock { + parentRepo.clear(k) + if (parentRepo.contains(k)) { + return@withWriteLock + } + kvCache.unset(k) + } + } +} + +fun KeyValuesRepo.directlyFullyCached( + kvCache: KeyValueRepo> = MapKeyValueRepo(), + scope: CoroutineScope = CoroutineScope(Dispatchers.Default), + skipStartInvalidate: Boolean = false, + locker: SmartRWLocker = SmartRWLocker(), +) = DirectFullKeyValuesCacheRepo(this, kvCache, scope, skipStartInvalidate, locker)