Skip to content
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
11 changes: 11 additions & 0 deletions store/api/store.api
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,17 @@ public final class com/dropbox/android/external/store4/StoreResponse$Loading : c
public fun toString ()Ljava/lang/String;
}

public final class com/dropbox/android/external/store4/StoreResponse$NoNewData : com/dropbox/android/external/store4/StoreResponse {
public fun <init> (Lcom/dropbox/android/external/store4/ResponseOrigin;)V
public final fun component1 ()Lcom/dropbox/android/external/store4/ResponseOrigin;
public final fun copy (Lcom/dropbox/android/external/store4/ResponseOrigin;)Lcom/dropbox/android/external/store4/StoreResponse$NoNewData;
public static synthetic fun copy$default (Lcom/dropbox/android/external/store4/StoreResponse$NoNewData;Lcom/dropbox/android/external/store4/ResponseOrigin;ILjava/lang/Object;)Lcom/dropbox/android/external/store4/StoreResponse$NoNewData;
public fun equals (Ljava/lang/Object;)Z
public fun getOrigin ()Lcom/dropbox/android/external/store4/ResponseOrigin;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/dropbox/android/external/store4/StoreResponseKt {
public static final fun doThrow (Lcom/dropbox/android/external/store4/StoreResponse$Error;)Ljava/lang/Void;
}
Expand Down
12 changes: 9 additions & 3 deletions store/src/main/java/com/dropbox/android/external/store4/Store.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,25 @@ interface Store<Key : Any, Output : Any> {
}

/**
* Helper factory that will return data for [key] if it is cached otherwise will return fresh/network data (updating your caches)
* Helper factory that will return data for [key] if it is cached otherwise will return
* fresh/network data (updating your caches)
*/
suspend fun <Key : Any, Output : Any> Store<Key, Output>.get(key: Key) = stream(
StoreRequest.cached(key, refresh = false)
).filterNot {
it is StoreResponse.Loading
it is StoreResponse.Loading || it is StoreResponse.NoNewData
}.first().requireData()

/**
* Helper factory that will return fresh data for [key] while updating your caches
*
* Note: If the [Fetcher] does not return any data (i.e the returned
* [kotlinx.coroutines.Flow], when collected, is empty). Then store will fall back to local
* data **even** if you explicitly requested fresh data.
* See https://github.com/dropbox/Store/pull/194 for context
*/
suspend fun <Key : Any, Output : Any> Store<Key, Output>.fresh(key: Key) = stream(
StoreRequest.fresh(key)
).filterNot {
it is StoreResponse.Loading
it is StoreResponse.Loading || it is StoreResponse.NoNewData
}.first().requireData()
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ data class StoreRequest<Key> private constructor(
}

/**
* Create a Store Request which will skip all caches and hit your fetcher (filling your caches)
* Create a Store Request which will skip all caches and hit your fetcher
* (filling your caches).
*
* Note: If the [Fetcher] does not return any data (i.e the returned
* [kotlinx.coroutines.Flow], when collected, is empty). Then store will fall back to local
* data **even** if you explicitly requested fresh data.
* See https://github.com/dropbox/Store/pull/194 for context.
*/
fun <Key> fresh(key: Key) = StoreRequest(
key = key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,21 @@ sealed class StoreResponse<out T> {
abstract val origin: ResponseOrigin

/**
* Loading event dispatched by a Pipeline
* Loading event dispatched by [Store] to signal the [Fetcher] is in progress.
*/
data class Loading<T>(override val origin: ResponseOrigin) : StoreResponse<T>()

/**
* Data dispatched by a pipeline
* Data dispatched by [Store]
*/
data class Data<T>(val value: T, override val origin: ResponseOrigin) : StoreResponse<T>()

/**
* No new data event dispatched by Store to signal the [Fetcher] returned no data (i.e the
* returned [kotlinx.coroutines.Flow], when collected, was empty).
*/
data class NoNewData<T>(override val origin: ResponseOrigin) : StoreResponse<T>()

/**
* Error dispatched by a pipeline
*/
Expand Down Expand Up @@ -98,6 +104,7 @@ sealed class StoreResponse<out T> {
internal fun <R> swapType(): StoreResponse<R> = when (this) {
is Error -> this as Error<R>
is Loading -> this as Loading<R>
is NoNewData -> this as NoNewData<R>
is Data -> throw RuntimeException("cannot swap type for StoreResponse.Data")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEmpty

/**
* This class maintains one and only 1 fetcher for a given [Key].
Expand Down Expand Up @@ -81,6 +82,8 @@ internal class FetcherController<Key : Any, Input : Any, Output : Any>(
origin = ResponseOrigin.Fetcher
)
}
}.onEmpty {
emit(StoreResponse.NoNewData(ResponseOrigin.Fetcher))
},
piggybackingDownstream = enablePiggyback,
onEach = { response ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,30 +86,53 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(

override fun stream(request: StoreRequest<Key>): Flow<StoreResponse<Output>> =
flow<StoreResponse<Output>> {
val cached = if (request.shouldSkipCache(CacheType.MEMORY)) {
val cachedToEmit = if (request.shouldSkipCache(CacheType.MEMORY)) {
null
} else {
memCache?.get(request.key)
}

cached?.let {
cachedToEmit?.let {
// if we read a value from cache, dispatch it first
emit(StoreResponse.Data(value = it, origin = ResponseOrigin.Cache))
}
if (sourceOfTruth == null) {
val stream = if (sourceOfTruth == null) {
// piggypack only if not specified fresh data AND we emitted a value from the cache
val piggybackOnly = !request.refresh && cached != null
val piggybackOnly = !request.refresh && cachedToEmit != null
@Suppress("UNCHECKED_CAST")
emitAll(
createNetworkFlow(
request = request,
networkLock = null,
piggybackOnly = piggybackOnly
) as Flow<StoreResponse<Output>> // when no source of truth Input == Output
)

createNetworkFlow(
request = request,
networkLock = null,
piggybackOnly = piggybackOnly
) as Flow<StoreResponse<Output>> // when no source of truth Input == Output
} else {
emitAll(diskNetworkCombined(request, sourceOfTruth))
diskNetworkCombined(request, sourceOfTruth)
}
emitAll(stream.transform {
emit(it)
if (it is StoreResponse.NoNewData && cachedToEmit == null) {
// In the special case where fetcher returned no new data we actually want to
// serve cache data (even if the request specified skipping cache and/or SoT)
//
// For stream(Request.cached(key, refresh=true)) we will return:
// Cache
// Source of truth
// Fetcher - > Loading
// Fetcher - > NoNewData
// (future Source of truth updates)
//
// For stream(Request.fresh(key)) we will return:
// Fetcher - > Loading
// Fetcher - > NoNewData
// Cache
// Source of truth
// (future Source of truth updates)
memCache?.get(request.key)?.let {
emit(StoreResponse.Data(value = it, origin = ResponseOrigin.Cache))
}
}
})
}.onEach {
// whenever a value is dispatched, save it to the memory cache
if (it.origin != ResponseOrigin.Cache) {
Expand Down Expand Up @@ -179,13 +202,17 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
when (it) {
is Either.Left -> {
// left, that is data from network
when (it.value) {
is StoreResponse.Data ->
// unlocking disk only if network sent data so that fresh data request
// never receives disk data by mistake
diskLock.complete(Unit)
else ->
emit(it.value.swapType())
if (it.value is StoreResponse.Data || it.value is StoreResponse.NoNewData) {
// Unlocking disk only if network sent data or reported no new data
// so that fresh data request never receives new fetcher data after
// cached disk data.
// This means that if the user asked for fresh data but the network returned
// no new data we will still unblock disk.
diskLock.complete(Unit)
}

if (it.value !is StoreResponse.Data) {
emit(it.value.swapType())
}
}
is Either.Right -> {
Expand All @@ -194,12 +221,8 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
is StoreResponse.Data -> {
val diskValue = diskData.value
if (diskValue != null) {
emit(
StoreResponse.Data(
value = diskValue,
origin = diskData.origin
)
)
@Suppress("UNCHECKED_CAST")
emit(diskData as StoreResponse<Output>)
}
// If the disk value is null or refresh was requested then allow fetcher
// to start emitting values.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,40 @@ class StoreTest(
verify(persister, never()).read(any())
}

@Test
fun `GIVEN no new data WHEN get THEN returns disk data`() = testScope.runBlockingTest {
val simpleStore = TestStoreBuilder.from(
scope = testScope,
fetcher = fetcher,
persister = persister
).build(storeType)

whenever(fetcher.invoke(barCode)) doReturn
flowOf()

whenever(persister.read(barCode)) doReturn DISK

val value = simpleStore.get(barCode)
assertThat(value).isEqualTo(DISK)
}

@Test
fun `GIVEN no new data WHEN fresh THEN returns disk data`() = testScope.runBlockingTest {
val simpleStore = TestStoreBuilder.from(
scope = testScope,
fetcher = fetcher,
persister = persister
).build(storeType)

whenever(fetcher.invoke(barCode)) doReturn
flowOf()

whenever(persister.read(barCode)) doReturn DISK

val value = simpleStore.fresh(barCode)
assertThat(value).isEqualTo(DISK)
}

companion object {
private const val DISK = "disk"
private const val NETWORK = "fresh"
Expand Down
Loading