diff --git a/gradle/conventions/settings.gradle.kts b/gradle/conventions/settings.gradle.kts --- a/gradle/conventions/settings.gradle.kts +++ b/gradle/conventions/settings.gradle.kts @@ -1,3 +1,5 @@ +rootProject.name = "conventions" + dependencyResolutionManagement { repositories { mavenCentral() diff --git a/cache/src/commonMain/kotlin/CacheAdapter.kt b/cache/src/commonMain/kotlin/CacheAdapter.kt --- a/cache/src/commonMain/kotlin/CacheAdapter.kt +++ b/cache/src/commonMain/kotlin/CacheAdapter.kt @@ -12,7 +12,7 @@ * and the underlying network APIs. */ class CacheAdapter( - val query: suspend (I) -> Outcome, + private val query: suspend (I) -> Outcome, ) : Cache { override fun get(id: I): ProgressiveFlow = captureProgress { query(id) } @@ -38,4 +38,4 @@ * See [CacheAdapter]. */ fun cache(transform: suspend (I) -> Outcome) = - CacheAdapter { transform(it) } + CacheAdapter(transform) diff --git a/cache/src/commonMain/kotlin/ExpirationCache.kt b/cache/src/commonMain/kotlin/ExpirationCache.kt --- a/cache/src/commonMain/kotlin/ExpirationCache.kt +++ b/cache/src/commonMain/kotlin/ExpirationCache.kt @@ -2,11 +2,10 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Clock import kotlinx.datetime.Instant -import opensavvy.cache.ExpirationCache.Companion.expireAfter import opensavvy.logger.Logger.Companion.trace import opensavvy.logger.loggerFor import opensavvy.progress.done @@ -18,7 +17,7 @@ /** * Cache layer that expires values from the previous layer after a specified [duration][expireAfter]. * - * To add an [ExpirationCache] to a previous layer, use [Cache.expireAfter][ExpirationCache.Companion.expireAfter]: + * To add an [ExpirationCache] to a previous layer, use [Cache.expireAfter][Cache.expireAfter]: * ```kotlin * val cache = Cache.Default * .expireAfter(5.minutes, Job()) @@ -46,14 +45,14 @@ private val log = loggerFor(this) private val lastUpdate = HashMap() - private val lock = Semaphore(1) + private val lock = Mutex() init { expirationScope.launch(CoroutineName("$this")) { while (isActive) { delay(expireAfter) - lock.withPermit { + lock.withLock("checkExpiredValues()") { val now = Clock.System.now() val iterator = lastUpdate.iterator() while (iterator.hasNext()) { @@ -71,7 +70,7 @@ } private suspend fun markAsUpdatedNow(id: I) { - lock.withPermit { + lock.withLock("markAsUpdatedNow($id)") { log.trace(id) { "Updated now:" } lastUpdate[id] = Clock.System.now() } @@ -92,26 +91,26 @@ override suspend fun expire(ids: Collection) { for (ref in ids) - lock.withPermit { + lock.withLock("expire($ids)") { lastUpdate.remove(ref) } upstream.expire(ids) } override suspend fun expireAll() { - lock.withPermit { + lock.withLock("expireAll()") { lastUpdate.clear() } upstream.expireAll() } - companion object { - /** - * Factory function to easily add a [ExpirationCache] layer to an existing cache chain. - * - * @see ExpirationCache - */ - fun Cache.expireAfter(duration: Duration, scope: CoroutineScope) = - ExpirationCache(this, duration, scope) - } + companion object } + +/** + * Factory function to easily add a [ExpirationCache] layer to an existing cache chain. + * + * @see ExpirationCache + */ +fun Cache.expireAfter(duration: Duration, scope: CoroutineScope) = + ExpirationCache(this, duration, scope) diff --git a/cache/src/commonMain/kotlin/MemoryCache.kt b/cache/src/commonMain/kotlin/MemoryCache.kt --- a/cache/src/commonMain/kotlin/MemoryCache.kt +++ b/cache/src/commonMain/kotlin/MemoryCache.kt @@ -2,9 +2,8 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit -import opensavvy.cache.MemoryCache.Companion.cachedInMemory +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import opensavvy.logger.Logger.Companion.trace import opensavvy.logger.loggerFor import opensavvy.state.coroutines.ProgressiveFlow @@ -22,12 +21,7 @@ * This implementation never frees the cache or invalidates elements inside it. * To free memory, add a subsequent layer responsible for it (e.g. [ExpirationCache]). * - * Use the [cachedInMemory] factory for easy cache chaining: - * ```kotlin - * val cache = Cache.Default() - * .cachedInMemory() - * .expireAfter(2.minutes) - * ``` + * Use the [cachedInMemory] factory for easy cache chaining. */ class MemoryCache( private val upstream: Cache, @@ -48,16 +42,16 @@ */ private val cache = HashMap?>>() - private val cacheLock = Semaphore(1) + private val cacheLock = Mutex() private val jobs = HashMap() - private val jobsLock = Semaphore(1) + private val jobsLock = Mutex() /** **UNSAFE**: only call when owning the [cacheLock] */ private fun getUnsafe(id: I) = cache.getOrPut(id) { MutableStateFlow(null) } override fun get(id: I): ProgressiveFlow = flow { - val cached = cacheLock.withPermit { getUnsafe(id) } + val cached = cacheLock.withLock("get($id)") { getUnsafe(id) } .onEach { out -> if (out == null) { // Now, someone should make a request to the previous layer. @@ -73,19 +67,19 @@ }.onEach { log.trace(it) { "Emit value for $id ->" } } private suspend fun attemptTakeResponsibilityPingUpstream(id: I) { - jobsLock.withPermit { + jobsLock.withLock("takeResponsibility($id)") { val job = jobs[id] if (job == null || !job.isActive) { // No one is currently making the request, I'm taking the responsibility to do it val childContext = currentCoroutineContext() + - CoroutineName("$this(for = $id)") + - this.job + CoroutineName("$this(for = $id)") + + this.job jobs[id] = CoroutineScope(childContext).launch { log.trace(id) { "Subscribing to the previous layer for" } - val state = cacheLock.withPermit { getUnsafe(id) } + val state = cacheLock.withLock("upstreamSubscription($id)") { getUnsafe(id) } upstream[id] .onEach { log.trace(it) { "Prev value for $id ->" } } @@ -109,13 +103,13 @@ override suspend fun update(values: Collection>) { log.trace(values) { "update" } - jobsLock.withPermit { + jobsLock.withLock("updateJobs($values)") { for ((id, _) in values) { jobs.remove(id)?.cancel("MemoryCache.expire(refs) was called") } } - cacheLock.withPermit { + cacheLock.withLock("updateCache($values)") { for ((id, value) in values) { getUnsafe(id).value = ProgressiveOutcome.Success(value) } @@ -127,13 +121,13 @@ override suspend fun expire(ids: Collection) { log.trace(ids) { "expire" } - jobsLock.withPermit { + jobsLock.withLock("expireJobs($ids)") { for (id in ids) { jobs.remove(id)?.cancel("MemoryCache.expire(refs) was called") } } - cacheLock.withPermit { + cacheLock.withLock("expireCache($ids)") { for (id in ids) { val cached = cache[id] @@ -156,12 +150,12 @@ override suspend fun expireAll() { log.trace { "expireAll" } - jobsLock.withPermit { + jobsLock.withLock("expireAllJobs()") { jobs.values.forEach { it.cancel("MemoryCache.expireAll() was called") } jobs.clear() } - cacheLock.withPermit { + cacheLock.withLock("expireAllCaches()") { val toRemove = ArrayList() for ((id, cached) in cache) { @@ -182,7 +176,26 @@ upstream.expireAll() } - companion object { - fun Cache.cachedInMemory(job: Job) = MemoryCache(this, job) + /** + * Goes through the entire cache and expires all values for which [predicate] returns `true`. + */ + internal suspend fun expireIf(predicate: (I) -> Boolean) { + log.trace { "expireIf" } + + val targets = cacheLock.withLock("expireIf($predicate)") { + cache.keys.filter(predicate) + } + + expire(targets) } + + companion object } + +/** + * Creates a new cache layer which stores the last queried value for each identifier, and joins requests such that + * multiple subscribers to the same value only start a single request. + * + * For more information, see [MemoryCache]. + */ +fun Cache.cachedInMemory(job: Job) = MemoryCache(this, job) diff --git a/cache/src/commonTest/kotlin/CacheTest.kt b/cache/src/commonTest/kotlin/CacheTest.kt --- a/cache/src/commonTest/kotlin/CacheTest.kt +++ b/cache/src/commonTest/kotlin/CacheTest.kt @@ -5,8 +5,6 @@ import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.runTest -import opensavvy.cache.ExpirationCache.Companion.expireAfter -import opensavvy.cache.MemoryCache.Companion.cachedInMemory import opensavvy.logger.LogLevel import opensavvy.logger.Logger.Companion.debug import opensavvy.logger.Logger.Companion.info diff --git a/cache/src/commonTest/kotlin/ContextualCacheTest.kt b/cache/src/commonTest/kotlin/ContextualCacheTest.kt new file mode 100644 --- /dev/null +++ b/cache/src/commonTest/kotlin/ContextualCacheTest.kt @@ -0,0 +1,176 @@ +package opensavvy.cache + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.job +import kotlinx.coroutines.test.runTest +import opensavvy.cache.contextual.batchingCache +import opensavvy.cache.contextual.cache +import opensavvy.cache.contextual.cachedInMemory +import opensavvy.cache.contextual.expireAfter +import opensavvy.state.arrow.out +import opensavvy.state.coroutines.now +import opensavvy.state.failure.Failure +import opensavvy.state.outcome.success +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.minutes + +@OptIn(ExperimentalCoroutinesApi::class) +class ContextualCacheTest { + + private val data = generateSequence(0) { it + 1 } + + private data class Identifier( + val even: Boolean, + val odd: Boolean, + ) + + private data class Context( + val startAt: Int, + val limit: Int, + ) + + private fun createCache() = cache> { id, context -> + out { + data + .drop(context.startAt) + .filter { id.even || it % 2 != 0 } + .filter { id.odd || it % 2 != 1 } + .take(context.limit) + .toList() + } + } + + @Test + fun read() = runTest { + val cache = createCache() + .cachedInMemory(backgroundScope.coroutineContext.job) + .expireAfter(2.minutes, backgroundScope) + + val expected = (0 until 100).toList().success() + val actual = cache[Identifier(even = true, odd = true), Context(0, 100)].now() + + assertEquals(expected, actual) + } + + @Test + fun expire() = runTest { + val cache = createCache() + .cachedInMemory(backgroundScope.coroutineContext.job) + .expireAfter(2.minutes, backgroundScope) + + // Update with invalid values, so we can notice whether they are expired+queried or not + cache.update(Identifier(even = true, odd = true), Context(0, 100), (0 until 10).toList()) + cache.update(Identifier(even = true, odd = true), Context(10, 100), (0 until 10).toList()) + cache.update(Identifier(even = true, odd = false), Context(0, 100), (0 until 10).toList()) + + run { + val expected = (0 until 10).toList().success() + val actual = cache[Identifier(even = true, odd = true), Context(0, 100)].now() + assertEquals(expected, actual) + } + + run { + val expected = (0 until 10).toList().success() + val actual = cache[Identifier(even = true, odd = true), Context(10, 100)].now() + assertEquals(expected, actual) + } + + run { + val expected = (0 until 10).toList().success() + val actual = cache[Identifier(even = true, odd = false), Context(0, 100)].now() + assertEquals(expected, actual) + } + + // The first value should be updated, but the second one should be unchanged + cache.expire(Identifier(even = true, odd = true), Context(0, 100)) + + run { + val expected = (0 until 100).toList().success() + val actual = cache[Identifier(even = true, odd = true), Context(0, 100)].now() + assertEquals(expected, actual) + } + + run { + val expected = (0 until 10).toList().success() + val actual = cache[Identifier(even = true, odd = true), Context(10, 100)].now() + assertEquals(expected, actual) + } + + run { + val expected = (0 until 10).toList().success() + val actual = cache[Identifier(even = true, odd = false), Context(0, 100)].now() + assertEquals(expected, actual) + } + + // The first and second values should be updated + cache.expire(Identifier(even = true, odd = true)) + + run { + val expected = (0 until 100).toList().success() + val actual = cache[Identifier(even = true, odd = true), Context(0, 100)].now() + assertEquals(expected, actual) + } + + run { + val expected = (10 until 110).toList().success() + val actual = cache[Identifier(even = true, odd = true), Context(10, 100)].now() + assertEquals(expected, actual) + } + + run { + val expected = (0 until 10).toList().success() + val actual = cache[Identifier(even = true, odd = false), Context(0, 100)].now() + assertEquals(expected, actual) + } + + // The last value should be updated + cache.expireAll() + + run { + val expected = (0 until 100).toList().success() + val actual = cache[Identifier(even = true, odd = true), Context(0, 100)].now() + assertEquals(expected, actual) + } + + run { + val expected = (10 until 110).toList().success() + val actual = cache[Identifier(even = true, odd = true), Context(10, 100)].now() + assertEquals(expected, actual) + } + + run { + val expected = (0 until 200 step 2).toList().success() + val actual = cache[Identifier(even = true, odd = false), Context(0, 100)].now() + assertEquals(expected, actual) + } + } + + @Test + fun batching() = runTest { + val initial = createCache() + + @Suppress("RemoveExplicitTypeArguments") // IDEA is wrong, they are necessary + val cache = batchingCache>(backgroundScope) { request -> + for ((id, context) in request) { + emitAll( + initial[id, context] + .map { Triple(id, context, it) } + ) + } + } + + cache.expire(Identifier(even = true, odd = true)) + cache.expire(Identifier(even = true, odd = false), Context(0, 100)) + cache.expireAll() + cache.update(Identifier(even = false, odd = true), Context(0, 1), listOf(1)) + + run { + val expected = (0 until 200 step 2).toList().success() + val actual = cache[Identifier(even = true, odd = false), Context(0, 100)].now() + assertEquals(expected, actual) + } + } +} diff --git a/cache/src/commonTest/kotlin/PassThroughContextTest.kt b/cache/src/commonTest/kotlin/PassThroughContextTest.kt --- a/cache/src/commonTest/kotlin/PassThroughContextTest.kt +++ b/cache/src/commonTest/kotlin/PassThroughContextTest.kt @@ -2,7 +2,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.test.runTest -import opensavvy.cache.MemoryCache.Companion.cachedInMemory import opensavvy.state.failure.Failure import opensavvy.state.outcome.Outcome import kotlin.coroutines.AbstractCoroutineContextElement diff --git a/cache/src/commonMain/kotlin/contextual/ContextualBatchingCacheAdapter.kt b/cache/src/commonMain/kotlin/contextual/ContextualBatchingCacheAdapter.kt new file mode 100644 --- /dev/null +++ b/cache/src/commonMain/kotlin/contextual/ContextualBatchingCacheAdapter.kt @@ -0,0 +1,59 @@ +package opensavvy.cache.contextual + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import opensavvy.cache.BatchingCacheAdapter +import opensavvy.state.coroutines.ProgressiveFlow +import opensavvy.state.failure.Failure +import opensavvy.state.progressive.ProgressiveOutcome + +class ContextualBatchingCacheAdapter( + scope: CoroutineScope, + workers: Int, + queryBatch: (Set>) -> Flow>>, +) : ContextualCache { + + private val batching = BatchingCacheAdapter( + scope, + workers, + ) { + queryBatch(it) + .map { (id, context, value) -> id to context to value } + } + + override fun get(id: I, context: C): ProgressiveFlow = + batching[id to context] + + override suspend fun update(values: Collection>) { + // This cache layer has no state, nothing to do + } + + override suspend fun expire(ids: Collection) { + // This cache layer has no state, nothing to do + } + + override suspend fun expireContextual(ids: Collection>) { + // This cache layer has no state, nothing to do + } + + override suspend fun expireAll() { + // This cache layer has no state, nothing to do + } + +} + +fun batchingCache( + scope: CoroutineScope, + workers: Int = 1, + transform: suspend FlowCollector>>.(Set>) -> Unit, +) = ContextualBatchingCacheAdapter( + scope, + workers, +) { + flow { + transform(it) + } +} diff --git a/cache/src/commonMain/kotlin/contextual/ContextualCache.kt b/cache/src/commonMain/kotlin/contextual/ContextualCache.kt new file mode 100644 --- /dev/null +++ b/cache/src/commonMain/kotlin/contextual/ContextualCache.kt @@ -0,0 +1,100 @@ +package opensavvy.cache.contextual + +import kotlinx.coroutines.flow.Flow +import opensavvy.cache.Cache +import opensavvy.state.coroutines.ProgressiveFlow +import opensavvy.state.failure.Failure + +/** + * Stores information temporarily to avoid unneeded network requests. + * + * Unlike [Cache], a contextual cache stores multiple values per identifier. + * The context decides which value is visible. + * Here are a few examples of context usage: + * - authentication information (different users see different values) + * - paging information (different values are returned depending on the requested page) + * + * The main advantage of using this interface rather than using a compound key in [Cache] is that it is possible to + * expire a value for all contexts. + * + * In all other regards (cache states, cache chaining…), this interface is identical to [Cache]. + * [Cache] can be seen as a specialization of this interface for the case where the context is [Unit]. + * + * @param I The identifier used to request from the cache. + * @param C The context which differentiates between cache results. + * @param F The possible failures when requesting the cache. + * @param T The possible successful value when requesting the cache. + */ +interface ContextualCache { + + /** + * Gets the value associated with an [id] and a [context] in this cache. + * + * This function returns a [Flow] instance synchronously: it is safe to call in synchronous-only areas of the + * program, such as inside the body of a UI component. You can then subscribe to the [Flow] to access the actual + * values. + */ + operator fun get(id: I, context: C): ProgressiveFlow + + /** + * Forces the cache to accept [value] as a more recent value for the given [id] and [context] than whatever it + * was previously storing. + * + * All layers are updated. + */ + suspend fun update(id: I, context: C, value: T) { + update(listOf(Triple(id, context, value))) + } + + /** + * Forces the cache to accept the given [values] as more recent for their associated identifier than whatever + * was previously stored. + * + * All layers are updated. + */ + suspend fun update(values: Collection>) + + /** + * Tells the cache that the values it stores for the given [id] are out of date, no matter the context, + * and should be queried again the next time they are requested. + * + * All layers are updated. + */ + suspend fun expire(id: I) { + expire(listOf(id)) + } + + /** + * Tells the cache that the value it stores for the given [id] and [context] is out of date, + * and should be queried again the next time they are requested. + * + * All layers are updated. + */ + suspend fun expire(id: I, context: C) { + expireContextual(listOf(id to context)) + } + + /** + * Tells the cache that the values it stores for the given [ids] are out of date, no matter the context, + * and should be queried again the next time they are requested. + * + * All layers are updated. + */ + suspend fun expire(ids: Collection) + + /** + * Tells the cache that the values it stores for the given [ids] are out of date, + * and should be queried again the next time they are requested. + * + * All layers are updated. + */ + suspend fun expireContextual(ids: Collection>) + + /** + * Tells the cache that all values are out of date, and should be queried again the next time they are requested. + * + * All layers are updated. + */ + suspend fun expireAll() + +} diff --git a/cache/src/commonMain/kotlin/contextual/ContextualCacheAdapter.kt b/cache/src/commonMain/kotlin/contextual/ContextualCacheAdapter.kt new file mode 100644 --- /dev/null +++ b/cache/src/commonMain/kotlin/contextual/ContextualCacheAdapter.kt @@ -0,0 +1,44 @@ +package opensavvy.cache.contextual + +import opensavvy.state.coroutines.ProgressiveFlow +import opensavvy.state.coroutines.captureProgress +import opensavvy.state.failure.Failure +import opensavvy.state.outcome.Outcome + +/** + * Cache implementation aimed to be the first link in a cache chain. + * + * This is not a valid implementation of a cache (it doesn't do any caching), and only serves as a link + * between caches and the underlying network APIs. + */ +class ContextualCacheAdapter( + private val query: suspend (I, C) -> Outcome, +) : ContextualCache { + override fun get(id: I, context: C): ProgressiveFlow = captureProgress { query(id, context) } + + override suspend fun update(values: Collection>) { + // This cache layer has no state, nothing to do + } + + override suspend fun expire(ids: Collection) { + // This cache layer has no state, nothing to do + } + + override suspend fun expireContextual(ids: Collection>) { + // This cache layer has no state, nothing to do + } + + override suspend fun expireAll() { + // This cache layer has no state, nothing to do + } + + companion object +} + +/** + * Creates a cache layer that intercepts requests. + * + * See [ContextualCacheAdapter]. + */ +fun cache(transform: suspend (I, C) -> Outcome): ContextualCache = + ContextualCacheAdapter(transform) diff --git a/cache/src/commonMain/kotlin/contextual/ContextualExpirationCache.kt b/cache/src/commonMain/kotlin/contextual/ContextualExpirationCache.kt new file mode 100644 --- /dev/null +++ b/cache/src/commonMain/kotlin/contextual/ContextualExpirationCache.kt @@ -0,0 +1,42 @@ +package opensavvy.cache.contextual + +import kotlinx.coroutines.CoroutineScope +import opensavvy.cache.expireAfter +import opensavvy.state.coroutines.ProgressiveFlow +import opensavvy.state.failure.Failure +import kotlin.time.Duration + +class ContextualExpirationCache( + private val upstream: ContextualCache, + duration: Duration, + scope: CoroutineScope, +) : ContextualCache { + + private val cache = ContextualWrapper(upstream) + .expireAfter(duration, scope) + + override fun get(id: I, context: C): ProgressiveFlow = + cache[id to context] + + override suspend fun update(values: Collection>) = + cache.update(values.map { (id, context, value) -> id to context to value }) + + override suspend fun expire(ids: Collection) { + // ExpirationCache doesn't store data, so we can directly expire the upstream + upstream.expire(ids) + } + + override suspend fun expireContextual(ids: Collection>) { + // ExpirationCache doesn't store data, so we can directly expire the upstream + upstream.expireContextual(ids) + } + + override suspend fun expireAll() { + // ExpirationCache doesn't store data, so we can directly expire the upstream + upstream.expireAll() + } + +} + +fun ContextualCache.expireAfter(duration: Duration, scope: CoroutineScope) = + ContextualExpirationCache(this, duration, scope) diff --git a/cache/src/commonMain/kotlin/contextual/ContextualMemoryCache.kt b/cache/src/commonMain/kotlin/contextual/ContextualMemoryCache.kt new file mode 100644 --- /dev/null +++ b/cache/src/commonMain/kotlin/contextual/ContextualMemoryCache.kt @@ -0,0 +1,37 @@ +package opensavvy.cache.contextual + +import kotlinx.coroutines.Job +import opensavvy.cache.cachedInMemory +import opensavvy.state.coroutines.ProgressiveFlow +import opensavvy.state.failure.Failure + +class ContextualMemoryCache( + upstream: ContextualCache, + job: Job, +) : ContextualCache { + + private val cache = ContextualWrapper(upstream) + .cachedInMemory(job) + + override fun get(id: I, context: C): ProgressiveFlow = + cache[id to context] + + override suspend fun update(values: Collection>) = + cache.update(values.map { (id, context, value) -> id to context to value }) + + override suspend fun expire(ids: Collection) { + val filterIds = ids.toSet() + + cache.expireIf { (id, _) -> id in filterIds } + } + + override suspend fun expireContextual(ids: Collection>) = + cache.expire(ids) + + override suspend fun expireAll() = + cache.expireAll() + +} + +fun ContextualCache.cachedInMemory(job: Job) = + ContextualMemoryCache(this, job) diff --git a/cache/src/commonMain/kotlin/contextual/ContextualWrapper.kt b/cache/src/commonMain/kotlin/contextual/ContextualWrapper.kt new file mode 100644 --- /dev/null +++ b/cache/src/commonMain/kotlin/contextual/ContextualWrapper.kt @@ -0,0 +1,34 @@ +package opensavvy.cache.contextual + +import opensavvy.cache.Cache +import opensavvy.state.coroutines.ProgressiveFlow +import opensavvy.state.failure.Failure + +/** + * Implementation of [Cache] for a [ContextualCache]. + */ +internal class ContextualWrapper( + private val upstream: ContextualCache, +) : Cache, F, T> { + + override fun get(id: Pair): ProgressiveFlow { + val (ref, context) = id + return upstream[ref, context] + } + + override suspend fun update(values: Collection, T>>) { + upstream.update(values.map { + val (identifier, value) = it + val (id, context) = identifier + Triple(id, context, value) + }) + } + + override suspend fun expire(ids: Collection>) { + upstream.expireContextual(ids) + } + + override suspend fun expireAll() { + upstream.expireAll() + } +}