Add cacheOnly option to StoreReadRequest (#586)
* Add cacheOnly option to StoreReadRequest Signed-off-by: William Brawner <me@wbrawner.com> * Fix doc on StoreReadRequest.cacheOnly Signed-off-by: William Brawner <me@wbrawner.com> * Hit disk caches for cacheOnly requests Signed-off-by: William Brawner <me@wbrawner.com> * Rename cacheOnly to localOnly Signed-off-by: William Brawner <william.p.brawner@aexp.com> * Send NoNewData and log warning for localOnly requests with no local data sources configured Signed-off-by: William Brawner <william.p.brawner@aexp.com> --------- Signed-off-by: William Brawner <me@wbrawner.com> Signed-off-by: William Brawner <william.p.brawner@aexp.com>
This commit is contained in:
parent
3559a786b6
commit
ab4d2e088b
3 changed files with 194 additions and 4 deletions
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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> ->
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue