Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cacheOnly option to StoreReadRequest #586

Merged
merged 5 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ package org.mobilenativefoundation.store.store5
* @param skippedCaches List of cache types that should be skipped when retuning the response see [CacheType]
* @param refresh If set to true [Store] will always get fresh value from fetcher while also
* starting the stream from the local [com.dropbox.android.external.store4.impl.SourceOfTruth] and memory cache
*
* @param fetch If set to false, then fetcher will not be used
*/
data class StoreReadRequest<out Key> private constructor(
val key: Key,
private val skippedCaches: Int,
val refresh: Boolean = false,
val fallBackToSourceOfTruth: Boolean = false
val fallBackToSourceOfTruth: Boolean = false,
val fetch: Boolean = true
) {

internal fun shouldSkipCache(type: CacheType) = skippedCaches.and(type.flag) != 0
Expand Down Expand Up @@ -57,7 +58,8 @@ data class StoreReadRequest<out Key> private constructor(
)

/**
* Create a [StoreReadRequest] which will return data from memory/disk caches
* Create a [StoreReadRequest] which will return data from memory/disk caches if present,
* otherwise will hit your fetcher (filling your caches).
* @param refresh if true then return fetcher (new) data as well (updating your caches)
*/
fun <Key> cached(key: Key, refresh: Boolean) = StoreReadRequest(
Expand All @@ -66,6 +68,16 @@ data class StoreReadRequest<out Key> private constructor(
refresh = refresh
)

/**
* Create a [StoreReadRequest] which will return data from memory/disk caches if present,
* otherwise will return [StoreReadResponse.NoNewData]
*/
fun <Key> localOnly(key: Key) = StoreReadRequest(
key = key,
skippedCaches = 0,
fetch = false
)

/**
* Create a [StoreReadRequest] which will return data from disk cache
* @param refresh if true then return fetcher (new) data as well (updating your caches)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package org.mobilenativefoundation.store.store5.impl

import co.touchlab.kermit.CommonWriter
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -89,6 +91,15 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
// if we read a value from cache, dispatch it first
emit(StoreReadResponse.Data(value = it, origin = StoreReadResponseOrigin.Cache))
}

if (sourceOfTruth == null && !request.fetch) {
if (memCache == null) {
logger.w("Local-only request made with no cache or source of truth configured")
}
emit(StoreReadResponse.NoNewData(origin = StoreReadResponseOrigin.Cache))
return@flow
}

val stream: Flow<StoreReadResponse<Output>> = if (sourceOfTruth == null) {
// piggypack only if not specified fresh data AND we emitted a value from the cache
val piggybackOnly = !request.refresh && cachedToEmit != null
Expand All @@ -99,8 +110,19 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
networkLock = null,
piggybackOnly = piggybackOnly
) as Flow<StoreReadResponse<Output>> // when no source of truth Input == Output
} else {
} else if (request.fetch) {
diskNetworkCombined(request, sourceOfTruth)
} else {
val diskLock = CompletableDeferred<Unit>()
diskLock.complete(Unit)
sourceOfTruth.reader(request.key, diskLock).transform { response ->
val data = response.dataOrNull()
if (data == null || validator?.isValid(data) == false) {
emit(StoreReadResponse.NoNewData(origin = response.origin))
} else {
emit(StoreReadResponse.Data(value = data, origin = response.origin))
}
}
}
emitAll(
stream.transform { output: StoreReadResponse<Output> ->
Expand Down Expand Up @@ -312,4 +334,11 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
sourceOfTruth?.reader(key, CompletableDeferred(Unit))?.map { it.dataOrNull() }?.first()

private fun fromMemCache(key: Key) = memCache?.getIfPresent(key)

companion object {
private val logger = Logger.apply {
setLogWriters(listOf(CommonWriter()))
setTag("Store")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package org.mobilenativefoundation.store.store5

import co.touchlab.kermit.Logger
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.mobilenativefoundation.store.store5.impl.extensions.get
import org.mobilenativefoundation.store.store5.util.InMemoryPersister
import org.mobilenativefoundation.store.store5.util.asSourceOfTruth
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.time.Duration

class LocalOnlyTests {
private val testScope = TestScope()

@Test
fun givenEmptyMemoryCacheThenCacheOnlyRequestReturnsNoNewData() = testScope.runTest {
val store = StoreBuilder
.from(Fetcher.of { _: Int -> throw RuntimeException("Fetcher shouldn't be hit") })
.cachePolicy(
MemoryPolicy
.builder<Any, Any>()
.build()
)
.build()
val response = store.stream(StoreReadRequest.localOnly(0)).first()
assertEquals(StoreReadResponse.NoNewData(StoreReadResponseOrigin.Cache), response)
}

@Test
fun givenPrimedMemoryCacheThenCacheOnlyRequestReturnsData() = testScope.runTest {
val fetcherHitCounter = atomic(0)
val store = StoreBuilder
.from(Fetcher.of { _: Int ->
fetcherHitCounter += 1
"result"
})
.cachePolicy(
MemoryPolicy
.builder<Any, Any>()
.build()
)
.build()
val a = store.get(0)
assertEquals("result", a)
assertEquals(1, fetcherHitCounter.value)
val response = store.stream(StoreReadRequest.localOnly(0)).first()
assertEquals("result", response.requireData())
assertEquals(1, fetcherHitCounter.value)
}

@Test
fun givenInvalidMemoryCacheThenCacheOnlyRequestReturnsNoNewData() = testScope.runTest {
val fetcherHitCounter = atomic(0)
val store = StoreBuilder
.from(Fetcher.of { _: Int ->
fetcherHitCounter += 1
"result"
})
.cachePolicy(
MemoryPolicy
.builder<Any, Any>()
.setExpireAfterWrite(Duration.ZERO)
.build()
)
.build()
val a = store.get(0)
assertEquals("result", a)
assertEquals(1, fetcherHitCounter.value)
val response = store.stream(StoreReadRequest.localOnly(0)).first()
assertEquals(StoreReadResponse.NoNewData(StoreReadResponseOrigin.Cache), response)
assertEquals(1, fetcherHitCounter.value)
}

@Test
fun givenEmptyDiskCacheThenCacheOnlyRequestReturnsNoNewData() = testScope.runTest {
val persister = InMemoryPersister<Int, String>()
val store = StoreBuilder
.from(
fetcher = Fetcher.of { _: Int -> throw RuntimeException("Fetcher shouldn't be hit") },
sourceOfTruth = persister.asSourceOfTruth()
)
.disableCache()
.build()
val response = store.stream(StoreReadRequest.localOnly(0)).first()
assertEquals(StoreReadResponse.NoNewData(StoreReadResponseOrigin.SourceOfTruth), response)
}

@Test
fun givenPrimedDiskCacheThenCacheOnlyRequestReturnsData() = testScope.runTest {
val fetcherHitCounter = atomic(0)
val persister = InMemoryPersister<Int, String>()
val store = StoreBuilder
.from(
fetcher = Fetcher.of { _: Int ->
fetcherHitCounter += 1
"result"
},
sourceOfTruth = persister.asSourceOfTruth()
)
.disableCache()
.build()
val a = store.get(0)
assertEquals("result", a)
assertEquals(1, fetcherHitCounter.value)
val response = store.stream(StoreReadRequest.localOnly(0)).first()
assertEquals("result", response.requireData())
assertEquals(StoreReadResponseOrigin.SourceOfTruth, response.origin)
assertEquals(1, fetcherHitCounter.value)
}

@Test
fun givenInvalidDiskCacheThenCacheOnlyRequestReturnsNoNewData() = testScope.runTest {
val fetcherHitCounter = atomic(0)
val persister = InMemoryPersister<Int, String>()
persister.write(0, "result")
val store = StoreBuilder
.from(
fetcher = Fetcher.of { _: Int ->
fetcherHitCounter += 1
"result"
},
sourceOfTruth = persister.asSourceOfTruth()
)
.disableCache()
.validator(Validator.by { false })
.build()
val a = store.get(0)
assertEquals("result", a)
assertEquals(1, fetcherHitCounter.value)
val response = store.stream(StoreReadRequest.localOnly(0)).first()
assertEquals(StoreReadResponse.NoNewData(StoreReadResponseOrigin.SourceOfTruth), response)
assertEquals(1, fetcherHitCounter.value)
}

@Test
fun givenNoCacheThenCacheOnlyRequestReturnsNoNewData() = testScope.runTest {
val store = StoreBuilder
.from(Fetcher.of { _: Int -> throw RuntimeException("Fetcher shouldn't be hit") })
.disableCache()
.build()
val response = store.stream(StoreReadRequest.localOnly(0)).first()
assertTrue(response is StoreReadResponse.NoNewData)
assertEquals(StoreReadResponseOrigin.Cache, response.origin)
}
}