[WIP] split read and write SOT types (#560)
* split read and write SOT types * all but 2 tests passing * remove extranous parameterized type on simple store factory * pr review * pr review * Passing except for UpdaterTests (#565) Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * mark mutablestore experimental --------- Signed-off-by: Matt Ramotar <mramotar@dropbox.com> Co-authored-by: Matt Ramotar <mramotar@dropbox.com>
This commit is contained in:
parent
fbcd34fd16
commit
2499d6e080
34 changed files with 297 additions and 597 deletions
|
@ -19,12 +19,12 @@ import org.mobilenativefoundation.store.store5.SourceOfTruth
|
|||
* @param deleteAll function for deleting all records in the source of truth
|
||||
*
|
||||
*/
|
||||
fun <Key : Any, Local : Any> SourceOfTruth.Companion.ofMaybe(
|
||||
reader: (Key) -> Maybe<Local>,
|
||||
writer: (Key, Local) -> Completable,
|
||||
delete: ((Key) -> Completable)? = null,
|
||||
deleteAll: (() -> Completable)? = null
|
||||
): SourceOfTruth<Key, Local> {
|
||||
fun <Key : Any, Local : Any, Output:Any> SourceOfTruth.Companion.ofMaybe(
|
||||
reader: (Key) -> Maybe<Output>,
|
||||
writer: (Key, Local) -> Completable,
|
||||
delete: ((Key) -> Completable)? = null,
|
||||
deleteAll: (() -> Completable)? = null
|
||||
): SourceOfTruth<Key, Local, Output> {
|
||||
val deleteFun: (suspend (Key) -> Unit)? =
|
||||
if (delete != null) { key -> delete(key).await() } else null
|
||||
val deleteAllFun: (suspend () -> Unit)? = deleteAll?.let { { deleteAll().await() } }
|
||||
|
@ -46,12 +46,12 @@ fun <Key : Any, Local : Any> SourceOfTruth.Companion.ofMaybe(
|
|||
* @param deleteAll function for deleting all records in the source of truth
|
||||
*
|
||||
*/
|
||||
fun <Key : Any, Local : Any> SourceOfTruth.Companion.ofFlowable(
|
||||
reader: (Key) -> Flowable<Local>,
|
||||
fun <Key : Any, Local : Any, Output:Any> SourceOfTruth.Companion.ofFlowable(
|
||||
reader: (Key) -> Flowable<Output>,
|
||||
writer: (Key, Local) -> Completable,
|
||||
delete: ((Key) -> Completable)? = null,
|
||||
deleteAll: (() -> Completable)? = null
|
||||
): SourceOfTruth<Key, Local> {
|
||||
): SourceOfTruth<Key, Local, Output> {
|
||||
val deleteFun: (suspend (Key) -> Unit)? =
|
||||
if (delete != null) { key -> delete(key).await() } else null
|
||||
val deleteAllFun: (suspend () -> Unit)? = deleteAll?.let { { deleteAll().await() } }
|
||||
|
|
|
@ -30,7 +30,7 @@ class HotRxSingleStoreTest {
|
|||
3 to FetcherResult.Data("three-1"),
|
||||
3 to FetcherResult.Data("three-2")
|
||||
)
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(Fetcher.ofResultSingle<Int, String> { fetcher.fetch(it) })
|
||||
val pipeline = StoreBuilder.from(Fetcher.ofResultSingle<Int, String> { fetcher.fetch(it) })
|
||||
.scope(testScope)
|
||||
.build()
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class RxFlowableStoreTest {
|
|||
BackpressureStrategy.BUFFER
|
||||
)
|
||||
},
|
||||
sourceOfTruth = SourceOfTruth.ofFlowable<Int, String>(
|
||||
sourceOfTruth = SourceOfTruth.ofFlowable<Int, String, String>(
|
||||
reader = {
|
||||
if (fakeDisk[it] != null)
|
||||
Flowable.fromCallable { fakeDisk[it]!! }
|
||||
|
|
|
@ -1,47 +1,45 @@
|
|||
package org.mobilenativefoundation.store.store5
|
||||
|
||||
interface Converter<Network : Any, Output : Any, Local : Any> {
|
||||
fun fromNetworkToOutput(network: Network): Output?
|
||||
fun fromOutputToLocal(output: Output): Local?
|
||||
fun fromLocalToOutput(local: Local): Output?
|
||||
/**
|
||||
* Converter is a utility interface that can be used to convert a network or output model to a local model.
|
||||
* Network to Local conversion is needed when the network model is different what you are saving in
|
||||
* your Source of Truth.
|
||||
* Output to Local conversion is needed when you are doing local writes in a MutableStore
|
||||
* @param Network The network data source model type. This is the type used in [Fetcher]
|
||||
* @param Output The common model type emitted from Store, typically the type returend from your Source of Truth
|
||||
* @param Local The local data source model type. This is the type used to save to your Source of Truth
|
||||
*/
|
||||
interface Converter<Network : Any, Local : Any, Output : Any> {
|
||||
fun fromNetworkToLocal(network: Network): Local
|
||||
fun fromOutputToLocal(output: Output): Local
|
||||
|
||||
class Builder<Network : Any, Output : Any, Local : Any> {
|
||||
class Builder<Network : Any, Local : Any, Output : Any> {
|
||||
|
||||
private var fromOutputToLocal: ((output: Output) -> Local)? = null
|
||||
private var fromNetworkToOutput: ((network: Network) -> Output)? = null
|
||||
private var fromLocalToOutput: ((local: Local) -> Output)? = null
|
||||
lateinit var fromOutputToLocal: ((output: Output) -> Local)
|
||||
lateinit var fromNetworkToLocal: ((network: Network) -> Local)
|
||||
|
||||
fun build(): Converter<Network, Output, Local> =
|
||||
RealConverter(fromOutputToLocal, fromNetworkToOutput, fromLocalToOutput)
|
||||
fun build(): Converter<Network, Local, Output> =
|
||||
RealConverter(fromOutputToLocal, fromNetworkToLocal)
|
||||
|
||||
fun fromOutputToLocal(converter: (output: Output) -> Local): Builder<Network, Output, Local> {
|
||||
fun fromOutputToLocal(converter: (output: Output) -> Local): Builder<Network, Local, Output> {
|
||||
fromOutputToLocal = converter
|
||||
return this
|
||||
}
|
||||
|
||||
fun fromLocalToOutput(converter: (local: Local) -> Output): Builder<Network, Output, Local> {
|
||||
fromLocalToOutput = converter
|
||||
return this
|
||||
}
|
||||
|
||||
fun fromNetworkToOutput(converter: (network: Network) -> Output): Builder<Network, Output, Local> {
|
||||
fromNetworkToOutput = converter
|
||||
fun fromNetworkToLocal(converter: (network: Network) -> Local): Builder<Network, Local, Output> {
|
||||
fromNetworkToLocal = converter
|
||||
return this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RealConverter<Network : Any, Output : Any, Local : Any>(
|
||||
private val fromOutputToLocal: ((output: Output) -> Local)?,
|
||||
private val fromNetworkToOutput: ((network: Network) -> Output)?,
|
||||
private val fromLocalToOutput: ((local: Local) -> Output)?,
|
||||
) : Converter<Network, Output, Local> {
|
||||
override fun fromNetworkToOutput(network: Network): Output? =
|
||||
fromNetworkToOutput?.invoke(network)
|
||||
private class RealConverter<Network : Any, Local : Any, Output : Any>(
|
||||
private val fromOutputToLocal: ((output: Output) -> Local),
|
||||
private val fromNetworkToLocal: ((network: Network) -> Local),
|
||||
) : Converter<Network, Local, Output> {
|
||||
override fun fromNetworkToLocal(network: Network): Local =
|
||||
fromNetworkToLocal.invoke(network)
|
||||
|
||||
override fun fromOutputToLocal(output: Output): Local? =
|
||||
fromOutputToLocal?.invoke(output)
|
||||
|
||||
override fun fromLocalToOutput(local: Local): Output? =
|
||||
fromLocalToOutput?.invoke(local)
|
||||
override fun fromOutputToLocal(output: Output): Local =
|
||||
fromOutputToLocal.invoke(output)
|
||||
}
|
||||
|
|
|
@ -145,7 +145,7 @@ interface Fetcher<Key : Any, Network : Any> {
|
|||
* Use instead of [of] if implementing fallback mechanisms.
|
||||
* @param name Unique name to enable differentiation of fetchers
|
||||
*/
|
||||
fun <Key : Any, Network : Any> ofWithFallback(
|
||||
fun <Key : Any, Network : Any> withFallback(
|
||||
name: String,
|
||||
fallback: Fetcher<Key, Network>,
|
||||
fetch: suspend (key: Key) -> Network
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.mobilenativefoundation.store.store5
|
||||
|
||||
@ExperimentalStoreApi
|
||||
interface MutableStore<Key : Any, Output : Any> :
|
||||
Read.StreamWithConflictResolution<Key, Output>,
|
||||
Write<Key, Output>,
|
||||
|
|
|
@ -3,7 +3,7 @@ package org.mobilenativefoundation.store.store5
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.mobilenativefoundation.store.store5.impl.mutableStoreBuilderFromFetcherAndSourceOfTruth
|
||||
|
||||
interface MutableStoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any> {
|
||||
interface MutableStoreBuilder<Key : Any, Network : Any, Local : Any, Output : Any> {
|
||||
|
||||
fun <Response : Any> build(
|
||||
updater: Updater<Key, Output, Response>,
|
||||
|
@ -17,24 +17,21 @@ interface MutableStoreBuilder<Key : Any, Network : Any, Output : Any, Local : An
|
|||
*
|
||||
* @param scope - scope to use for sharing
|
||||
*/
|
||||
fun scope(scope: CoroutineScope): MutableStoreBuilder<Key, Network, Output, Local>
|
||||
fun scope(scope: CoroutineScope): MutableStoreBuilder<Key, Network, Local, Output>
|
||||
|
||||
/**
|
||||
* controls eviction policy for a store cache, use [MemoryPolicy.MemoryPolicyBuilder] to configure a TTL
|
||||
* or size based eviction
|
||||
* Example: MemoryPolicy.builder().setExpireAfterWrite(10.seconds).build()
|
||||
*/
|
||||
fun cachePolicy(memoryPolicy: MemoryPolicy<Key, Output>?): MutableStoreBuilder<Key, Network, Output, Local>
|
||||
fun cachePolicy(memoryPolicy: MemoryPolicy<Key, Output>?): MutableStoreBuilder<Key, Network, Local, Output>
|
||||
|
||||
/**
|
||||
* by default a Store caches in memory with a default policy of max items = 100
|
||||
*/
|
||||
fun disableCache(): MutableStoreBuilder<Key, Network, Output, Local>
|
||||
fun disableCache(): MutableStoreBuilder<Key, Network, Local, Output>
|
||||
|
||||
fun converter(converter: Converter<Network, Output, Local>):
|
||||
MutableStoreBuilder<Key, Network, Output, Local>
|
||||
|
||||
fun validator(validator: Validator<Output>): MutableStoreBuilder<Key, Network, Output, Local>
|
||||
fun validator(validator: Validator<Output>): MutableStoreBuilder<Key, Network, Local, Output, >
|
||||
|
||||
companion object {
|
||||
/**
|
||||
|
@ -43,10 +40,14 @@ interface MutableStoreBuilder<Key : Any, Network : Any, Output : Any, Local : An
|
|||
* @param fetcher a function for fetching a flow of network records.
|
||||
* @param sourceOfTruth a [SourceOfTruth] for the store.
|
||||
*/
|
||||
fun <Key : Any, Network : Any, Output : Any, Local : Any> from(
|
||||
fun <Key : Any, Network : Any, Local : Any, Output : Any,> from(
|
||||
fetcher: Fetcher<Key, Network>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Local>
|
||||
): MutableStoreBuilder<Key, Network, Output, Local> =
|
||||
mutableStoreBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth)
|
||||
sourceOfTruth: SourceOfTruth<Key, Local, Output>,
|
||||
converter: Converter<Network, Local, Output>
|
||||
): MutableStoreBuilder<Key, Network, Local, Output> =
|
||||
mutableStoreBuilderFromFetcherAndSourceOfTruth(
|
||||
fetcher = fetcher, sourceOfTruth = sourceOfTruth,
|
||||
converter = converter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,14 +46,14 @@ import kotlin.jvm.JvmName
|
|||
* transform them to another type when placing them in local storage.
|
||||
*
|
||||
*/
|
||||
interface SourceOfTruth<Key : Any, Local : Any> {
|
||||
interface SourceOfTruth<Key : Any, Local : Any, Output : Any> {
|
||||
|
||||
/**
|
||||
* Used by [Store] to read records from the source of truth.
|
||||
*
|
||||
* @param key The key to read for.
|
||||
*/
|
||||
fun reader(key: Key): Flow<Local?>
|
||||
fun reader(key: Key): Flow<Output?>
|
||||
|
||||
/**
|
||||
* Used by [Store] to write records **coming in from the fetcher (network)** to the source of
|
||||
|
@ -90,12 +90,12 @@ interface SourceOfTruth<Key : Any, Local : Any> {
|
|||
* @param delete function for deleting records in the source of truth for the given key
|
||||
* @param deleteAll function for deleting all records in the source of truth
|
||||
*/
|
||||
fun <Key : Any, Local : Any> of(
|
||||
nonFlowReader: suspend (Key) -> Local?,
|
||||
fun <Key : Any, Local : Any, Output : Any> of(
|
||||
nonFlowReader: suspend (Key) -> Output?,
|
||||
writer: suspend (Key, Local) -> Unit,
|
||||
delete: (suspend (Key) -> Unit)? = null,
|
||||
deleteAll: (suspend () -> Unit)? = null
|
||||
): SourceOfTruth<Key, Local> = PersistentNonFlowingSourceOfTruth(
|
||||
): SourceOfTruth<Key, Local, Output> = PersistentNonFlowingSourceOfTruth(
|
||||
realReader = nonFlowReader,
|
||||
realWriter = writer,
|
||||
realDelete = delete,
|
||||
|
@ -112,12 +112,12 @@ interface SourceOfTruth<Key : Any, Local : Any> {
|
|||
* @param deleteAll function for deleting all records in the source of truth
|
||||
*/
|
||||
@JvmName("ofFlow")
|
||||
fun <Key : Any, Local : Any> of(
|
||||
reader: (Key) -> Flow<Local?>,
|
||||
fun <Key : Any, Local : Any, Output : Any> of(
|
||||
reader: (Key) -> Flow<Output?>,
|
||||
writer: suspend (Key, Local) -> Unit,
|
||||
delete: (suspend (Key) -> Unit)? = null,
|
||||
deleteAll: (suspend () -> Unit)? = null
|
||||
): SourceOfTruth<Key, Local> = PersistentSourceOfTruth(
|
||||
): SourceOfTruth<Key, Local, Output> = PersistentSourceOfTruth(
|
||||
realReader = reader,
|
||||
realWriter = writer,
|
||||
realDelete = delete,
|
||||
|
|
|
@ -27,7 +27,7 @@ import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcherSourc
|
|||
interface StoreBuilder<Key : Any, Output : Any> {
|
||||
fun build(): Store<Key, Output>
|
||||
|
||||
fun <Network : Any, Local : Any> toMutableStoreBuilder(): MutableStoreBuilder<Key, Network, Output, Local>
|
||||
fun <Network : Any, Local : Any> toMutableStoreBuilder(converter: Converter<Network, Local, Output>): MutableStoreBuilder<Key, Network, Local, Output>
|
||||
|
||||
/**
|
||||
* A store multicasts same [Output] value to many consumers (Similar to RxJava.share()), by default
|
||||
|
@ -58,9 +58,9 @@ interface StoreBuilder<Key : Any, Output : Any> {
|
|||
*
|
||||
* @param fetcher a [Fetcher] flow of network records.
|
||||
*/
|
||||
fun <Key : Any, Input : Any, Output : Any> from(
|
||||
fun <Key : Any, Input : Any> from(
|
||||
fetcher: Fetcher<Key, Input>,
|
||||
): StoreBuilder<Key, Output> = storeBuilderFromFetcher(fetcher = fetcher)
|
||||
): StoreBuilder<Key, Input> = storeBuilderFromFetcher(fetcher = fetcher)
|
||||
|
||||
/**
|
||||
* Creates a new [StoreBuilder] from a [Fetcher] and a [SourceOfTruth].
|
||||
|
@ -70,13 +70,13 @@ interface StoreBuilder<Key : Any, Output : Any> {
|
|||
*/
|
||||
fun <Key : Any, Input : Any, Output : Any> from(
|
||||
fetcher: Fetcher<Key, Input>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Input>
|
||||
sourceOfTruth: SourceOfTruth<Key, Input, Output>
|
||||
): StoreBuilder<Key, Output> =
|
||||
storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth)
|
||||
|
||||
fun <Key : Any, Network : Any, Output : Any, Local : Any> from(
|
||||
fun <Key : Any, Network : Any, Output : Any> from(
|
||||
fetcher: Fetcher<Key, Network>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Local>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Network, Output>,
|
||||
memoryCache: Cache<Key, Output>,
|
||||
): StoreBuilder<Key, Output> = storeBuilderFromFetcherSourceOfTruthAndMemoryCache(
|
||||
fetcher,
|
||||
|
|
|
@ -40,6 +40,7 @@ import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
|
|||
* fetcher requests receives values dispatched by later requests even if they don't share the
|
||||
* request.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal class FetcherController<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||
/**
|
||||
* The [CoroutineScope] to use when collecting from the fetcher
|
||||
|
@ -55,7 +56,17 @@ internal class FetcherController<Key : Any, Network : Any, Output : Any, Local :
|
|||
*/
|
||||
private val sourceOfTruth: SourceOfTruthWithBarrier<Key, Network, Output, Local>?,
|
||||
|
||||
private val converter: Converter<Network, Output, Local>? = null
|
||||
private val converter: Converter<Network, Local, Output> = object :
|
||||
Converter<Network, Local, Output> {
|
||||
|
||||
override fun fromNetworkToLocal(network: Network): Local {
|
||||
return network as Local
|
||||
}
|
||||
|
||||
override fun fromOutputToLocal(output: Output): Local {
|
||||
throw IllegalStateException("Not used")
|
||||
}
|
||||
}
|
||||
) {
|
||||
@Suppress("USELESS_CAST", "UNCHECKED_CAST") // needed for multicaster source
|
||||
private val fetchers = RefCountedResource(
|
||||
|
@ -94,10 +105,9 @@ internal class FetcherController<Key : Any, Network : Any, Output : Any, Local :
|
|||
*/
|
||||
piggybackingDownstream = true,
|
||||
onEach = { response ->
|
||||
response.dataOrNull()?.let { network ->
|
||||
val output = converter?.fromNetworkToOutput(network)
|
||||
val input = output ?: network
|
||||
sourceOfTruth?.write(key, input as Output)
|
||||
response.dataOrNull()?.let { network: Network ->
|
||||
val local: Local = converter.fromNetworkToLocal(network)
|
||||
sourceOfTruth?.write(key, local)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.mobilenativefoundation.store.store5.internal.concurrent.ThreadSafety
|
|||
import org.mobilenativefoundation.store.store5.internal.definition.WriteRequestQueue
|
||||
import org.mobilenativefoundation.store.store5.internal.result.EagerConflictResolutionResult
|
||||
|
||||
@ExperimentalStoreApi
|
||||
internal class RealMutableStore<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||
private val delegate: RealStore<Key, Network, Output, Local>,
|
||||
private val updater: Updater<Key, Output, *>,
|
||||
|
|
|
@ -17,56 +17,58 @@ import org.mobilenativefoundation.store.store5.Updater
|
|||
import org.mobilenativefoundation.store.store5.Validator
|
||||
import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore
|
||||
|
||||
fun <Key : Any, Network : Any, Output : Any, Local : Any> mutableStoreBuilderFromFetcher(
|
||||
// we don't have a source of truth and can use a dummy converter
|
||||
fun <Key : Any, Network : Any, Local : Any, Output : Any> mutableStoreBuilderFromFetcher(
|
||||
fetcher: Fetcher<Key, Network>,
|
||||
): MutableStoreBuilder<Key, Network, Output, Local> = RealMutableStoreBuilder(fetcher)
|
||||
converter: Converter<Network, Local, Output>
|
||||
): MutableStoreBuilder<Key, Network, Local, Output> =
|
||||
RealMutableStoreBuilder(fetcher, converter = converter)
|
||||
|
||||
fun <Key : Any, Network : Any, Output : Any, Local : Any> mutableStoreBuilderFromFetcherAndSourceOfTruth(
|
||||
fun <Key : Any, Network : Any, Local : Any, Output : Any> mutableStoreBuilderFromFetcherAndSourceOfTruth(
|
||||
fetcher: Fetcher<Key, Network>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Local>,
|
||||
): MutableStoreBuilder<Key, Network, Output, Local> = RealMutableStoreBuilder(fetcher, sourceOfTruth)
|
||||
sourceOfTruth: SourceOfTruth<Key, Local, Output>,
|
||||
converter: Converter<Network, Local, Output>
|
||||
): MutableStoreBuilder<Key, Network, Local, Output> =
|
||||
RealMutableStoreBuilder(fetcher, sourceOfTruth, converter = converter)
|
||||
|
||||
fun <Key : Any, Network : Any, Output : Any, Local : Any> mutableStoreBuilderFromFetcherSourceOfTruthAndMemoryCache(
|
||||
fetcher: Fetcher<Key, Network>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Local>,
|
||||
memoryCache: Cache<Key, Output>
|
||||
): MutableStoreBuilder<Key, Network, Output, Local> = RealMutableStoreBuilder(fetcher, sourceOfTruth, memoryCache)
|
||||
sourceOfTruth: SourceOfTruth<Key, Local, Output>,
|
||||
memoryCache: Cache<Key, Output>,
|
||||
converter: Converter<Network, Local, Output>,
|
||||
): MutableStoreBuilder<Key, Network, Local, Output> =
|
||||
RealMutableStoreBuilder(fetcher, sourceOfTruth, memoryCache, converter = converter)
|
||||
|
||||
internal class RealMutableStoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||
internal class RealMutableStoreBuilder<Key : Any, Network : Any, Local : Any, Output : Any>(
|
||||
private val fetcher: Fetcher<Key, Network>,
|
||||
private val sourceOfTruth: SourceOfTruth<Key, Local>? = null,
|
||||
private val memoryCache: Cache<Key, Output>? = null
|
||||
) : MutableStoreBuilder<Key, Network, Output, Local> {
|
||||
private val sourceOfTruth: SourceOfTruth<Key, Local, Output>? = null,
|
||||
private val memoryCache: Cache<Key, Output>? = null,
|
||||
private val converter: Converter<Network, Local, Output>
|
||||
) : MutableStoreBuilder<Key, Network, Local, Output> {
|
||||
private var scope: CoroutineScope? = null
|
||||
private var cachePolicy: MemoryPolicy<Key, Output>? = StoreDefaults.memoryPolicy
|
||||
private var converter: Converter<Network, Output, Local>? = null
|
||||
private var validator: Validator<Output>? = null
|
||||
|
||||
override fun scope(scope: CoroutineScope): MutableStoreBuilder<Key, Network, Output, Local> {
|
||||
override fun scope(scope: CoroutineScope): MutableStoreBuilder<Key, Network, Local, Output> {
|
||||
this.scope = scope
|
||||
return this
|
||||
}
|
||||
|
||||
override fun cachePolicy(memoryPolicy: MemoryPolicy<Key, Output>?): MutableStoreBuilder<Key, Network, Output, Local> {
|
||||
override fun cachePolicy(memoryPolicy: MemoryPolicy<Key, Output>?): MutableStoreBuilder<Key, Network, Local, Output> {
|
||||
cachePolicy = memoryPolicy
|
||||
return this
|
||||
}
|
||||
|
||||
override fun disableCache(): MutableStoreBuilder<Key, Network, Output, Local> {
|
||||
override fun disableCache(): MutableStoreBuilder<Key, Network, Local, Output> {
|
||||
cachePolicy = null
|
||||
return this
|
||||
}
|
||||
|
||||
override fun validator(validator: Validator<Output>): MutableStoreBuilder<Key, Network, Output, Local> {
|
||||
override fun validator(validator: Validator<Output>): MutableStoreBuilder<Key, Network, Local, Output> {
|
||||
this.validator = validator
|
||||
return this
|
||||
}
|
||||
|
||||
override fun converter(converter: Converter<Network, Output, Local>): MutableStoreBuilder<Key, Network, Output, Local> {
|
||||
this.converter = converter
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): Store<Key, Output> = RealStore(
|
||||
scope = scope ?: GlobalScope,
|
||||
sourceOfTruth = sourceOfTruth,
|
||||
|
@ -86,7 +88,12 @@ internal class RealMutableStoreBuilder<Key : Any, Network : Any, Output : Any, L
|
|||
}
|
||||
|
||||
if (cachePolicy!!.hasMaxWeight) {
|
||||
weigher(cachePolicy!!.maxWeight) { key, value -> cachePolicy!!.weigher.weigh(key, value) }
|
||||
weigher(cachePolicy!!.maxWeight) { key, value ->
|
||||
cachePolicy!!.weigher.weigh(
|
||||
key,
|
||||
value
|
||||
)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
|
|
@ -19,14 +19,14 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.flow
|
||||
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||
|
||||
internal class PersistentSourceOfTruth<Key : Any, Local : Any>(
|
||||
private val realReader: (Key) -> Flow<Local?>,
|
||||
internal class PersistentSourceOfTruth<Key : Any, Local : Any, Output : Any>(
|
||||
private val realReader: (Key) -> Flow<Output?>,
|
||||
private val realWriter: suspend (Key, Local) -> Unit,
|
||||
private val realDelete: (suspend (Key) -> Unit)? = null,
|
||||
private val realDeleteAll: (suspend () -> Unit)? = null
|
||||
) : SourceOfTruth<Key, Local> {
|
||||
) : SourceOfTruth<Key, Local, Output> {
|
||||
|
||||
override fun reader(key: Key): Flow<Local?> = realReader.invoke(key)
|
||||
override fun reader(key: Key): Flow<Output?> = realReader.invoke(key)
|
||||
|
||||
override suspend fun write(key: Key, value: Local) = realWriter(key, value)
|
||||
|
||||
|
@ -39,14 +39,14 @@ internal class PersistentSourceOfTruth<Key : Any, Local : Any>(
|
|||
}
|
||||
}
|
||||
|
||||
internal class PersistentNonFlowingSourceOfTruth<Key : Any, Local : Any>(
|
||||
private val realReader: suspend (Key) -> Local?,
|
||||
internal class PersistentNonFlowingSourceOfTruth<Key : Any, Local : Any, Output : Any>(
|
||||
private val realReader: suspend (Key) -> Output?,
|
||||
private val realWriter: suspend (Key, Local) -> Unit,
|
||||
private val realDelete: (suspend (Key) -> Unit)? = null,
|
||||
private val realDeleteAll: (suspend () -> Unit)?
|
||||
) : SourceOfTruth<Key, Local> {
|
||||
) : SourceOfTruth<Key, Local, Output> {
|
||||
|
||||
override fun reader(key: Key): Flow<Local?> =
|
||||
override fun reader(key: Key): Flow<Output?> =
|
||||
flow {
|
||||
val sot = realReader(key)
|
||||
emit(sot)
|
||||
|
|
|
@ -43,8 +43,8 @@ import org.mobilenativefoundation.store.store5.internal.result.StoreDelegateWrit
|
|||
internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||
scope: CoroutineScope,
|
||||
fetcher: Fetcher<Key, Network>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Local>? = null,
|
||||
private val converter: Converter<Network, Output, Local>? = null,
|
||||
sourceOfTruth: SourceOfTruth<Key, Local, Output>? = null,
|
||||
private val converter: Converter<Network, Local, Output>,
|
||||
private val validator: Validator<Output>?,
|
||||
private val memCache: Cache<Key, Output>?
|
||||
) : Store<Key, Output> {
|
||||
|
@ -79,17 +79,16 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
|
|||
} else {
|
||||
val output = memCache?.getIfPresent(request.key)
|
||||
when {
|
||||
output == null -> null
|
||||
validator?.isValid(output) == false -> null
|
||||
output == null || validator?.isValid(output) == false -> null
|
||||
else -> output
|
||||
}
|
||||
}
|
||||
|
||||
cachedToEmit?.let {
|
||||
cachedToEmit?.let { it: Output ->
|
||||
// if we read a value from cache, dispatch it first
|
||||
emit(StoreReadResponse.Data(value = it, origin = StoreReadResponseOrigin.Cache))
|
||||
}
|
||||
val stream = if (sourceOfTruth == null) {
|
||||
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
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
@ -103,44 +102,27 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
|
|||
diskNetworkCombined(request, sourceOfTruth)
|
||||
}
|
||||
emitAll(
|
||||
stream.transform { output ->
|
||||
val data = output.dataOrNull()
|
||||
val shouldSkipValidation =
|
||||
validator == null || data == null || output.origin is StoreReadResponseOrigin.Fetcher
|
||||
if (data != null && !shouldSkipValidation && validator?.isValid(data) == false) {
|
||||
fetcherController.getFetcher(request.key, false).collect { storeReadResponse ->
|
||||
val network = storeReadResponse.dataOrNull()
|
||||
if (network != null) {
|
||||
val newOutput = converter?.fromNetworkToOutput(network) ?: network as? Output
|
||||
if (newOutput != null) {
|
||||
emit(StoreReadResponse.Data(newOutput, origin = storeReadResponse.origin))
|
||||
} else {
|
||||
emit(StoreReadResponse.NoNewData(origin = storeReadResponse.origin))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emit(output)
|
||||
if (output is StoreReadResponse.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?.getIfPresent(request.key)?.let {
|
||||
emit(StoreReadResponse.Data(value = it, origin = StoreReadResponseOrigin.Cache))
|
||||
}
|
||||
stream.transform { output: StoreReadResponse<Output> ->
|
||||
emit(output)
|
||||
if (output is StoreReadResponse.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?.getIfPresent(request.key)?.let {
|
||||
emit(StoreReadResponse.Data(value = it, origin = StoreReadResponseOrigin.Cache))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -230,7 +212,7 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
|
|||
diskLock.complete(Unit)
|
||||
}
|
||||
|
||||
if (it.value !is StoreReadResponse.Data && !fallBackToSourceOfTruth) {
|
||||
if (it.value !is StoreReadResponse.Data) {
|
||||
emit(it.value.swapType())
|
||||
}
|
||||
}
|
||||
|
@ -248,14 +230,17 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
|
|||
}
|
||||
|
||||
val diskValue = diskData.value
|
||||
val isValid = diskValue?.let { it1 -> validator?.isValid(it1) } == true
|
||||
if (diskValue != null) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val output =
|
||||
diskData.copy(origin = responseOriginWithFetcherName) as StoreReadResponse<Output>
|
||||
emit(output)
|
||||
}
|
||||
// If the disk value is null or refresh was requested then allow fetcher
|
||||
// to start emitting values.
|
||||
// If the disk value is null
|
||||
// or refresh was requested
|
||||
// or the disk value is not valid
|
||||
// then allow fetcher to start emitting values.
|
||||
if (request.refresh || diskData.value == null) {
|
||||
networkLock.complete(Unit)
|
||||
}
|
||||
|
@ -304,7 +289,7 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
|
|||
|
||||
internal suspend fun write(key: Key, value: Output): StoreDelegateWriteResult = try {
|
||||
memCache?.put(key, value)
|
||||
sourceOfTruth?.write(key, value)
|
||||
sourceOfTruth?.write(key, converter.fromOutputToLocal(value))
|
||||
StoreDelegateWriteResult.Success
|
||||
} catch (error: Throwable) {
|
||||
StoreDelegateWriteResult.Error.Exception(error)
|
||||
|
|
|
@ -16,30 +16,39 @@ import org.mobilenativefoundation.store.store5.StoreBuilder
|
|||
import org.mobilenativefoundation.store.store5.StoreDefaults
|
||||
import org.mobilenativefoundation.store.store5.Validator
|
||||
|
||||
fun <Key : Any, Input : Any, Output : Any> storeBuilderFromFetcher(
|
||||
fetcher: Fetcher<Key, Input>,
|
||||
sourceOfTruth: SourceOfTruth<Key, *>? = null,
|
||||
): StoreBuilder<Key, Output> = RealStoreBuilder(fetcher, sourceOfTruth)
|
||||
|
||||
fun <Key : Any, Input : Any, Output : Any> storeBuilderFromFetcherAndSourceOfTruth(
|
||||
fetcher: Fetcher<Key, Input>,
|
||||
sourceOfTruth: SourceOfTruth<Key, *>,
|
||||
): StoreBuilder<Key, Output> = RealStoreBuilder(fetcher, sourceOfTruth)
|
||||
|
||||
fun <Key : Any, Network : Any, Output : Any, Local : Any> storeBuilderFromFetcherSourceOfTruthAndMemoryCache(
|
||||
fun <Key : Any, Network : Any, Output : Any> storeBuilderFromFetcher(
|
||||
fetcher: Fetcher<Key, Network>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Local>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Network, Output>? = null,
|
||||
): StoreBuilder<Key, Output> =
|
||||
RealStoreBuilder<Key, Network, Output, Network>(fetcher, sourceOfTruth)
|
||||
|
||||
fun <Key : Any, Network : Any, Output : Any> storeBuilderFromFetcherAndSourceOfTruth(
|
||||
fetcher: Fetcher<Key, Network>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Network, Output>,
|
||||
): StoreBuilder<Key, Output> =
|
||||
RealStoreBuilder<Key, Network, Output, Network>(fetcher, sourceOfTruth)
|
||||
|
||||
fun <Key : Any, Network : Any, Output : Any> storeBuilderFromFetcherSourceOfTruthAndMemoryCache(
|
||||
fetcher: Fetcher<Key, Network>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Network, Output>,
|
||||
memoryCache: Cache<Key, Output>,
|
||||
): StoreBuilder<Key, Output> = RealStoreBuilder(fetcher, sourceOfTruth, memoryCache)
|
||||
): StoreBuilder<Key, Output> =
|
||||
RealStoreBuilder(fetcher, sourceOfTruth, memoryCache)
|
||||
|
||||
internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||
private val fetcher: Fetcher<Key, Network>,
|
||||
private val sourceOfTruth: SourceOfTruth<Key, Local>? = null,
|
||||
private val memoryCache: Cache<Key, Output>? = null
|
||||
private val sourceOfTruth: SourceOfTruth<Key, Local, Output>? = null,
|
||||
private val memoryCache: Cache<Key, Output>? = null,
|
||||
private val converter: Converter<Network, Local, Output> = object :
|
||||
Converter<Network, Local, Output> {
|
||||
override fun fromOutputToLocal(output: Output): Local =
|
||||
throw IllegalStateException("non mutable store never call this function")
|
||||
|
||||
override fun fromNetworkToLocal(network: Network): Local = network as Local
|
||||
}
|
||||
) : StoreBuilder<Key, Output> {
|
||||
private var scope: CoroutineScope? = null
|
||||
private var cachePolicy: MemoryPolicy<Key, Output>? = StoreDefaults.memoryPolicy
|
||||
private var converter: Converter<Network, Output, Local>? = null
|
||||
private var validator: Validator<Output>? = null
|
||||
|
||||
override fun scope(scope: CoroutineScope): StoreBuilder<Key, Output> {
|
||||
|
@ -62,7 +71,7 @@ internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local :
|
|||
return this
|
||||
}
|
||||
|
||||
override fun build(): Store<Key, Output> = RealStore(
|
||||
override fun build(): Store<Key, Output> = RealStore<Key, Network, Output, Local>(
|
||||
scope = scope ?: GlobalScope,
|
||||
sourceOfTruth = sourceOfTruth,
|
||||
fetcher = fetcher,
|
||||
|
@ -81,26 +90,33 @@ internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local :
|
|||
}
|
||||
|
||||
if (cachePolicy!!.hasMaxWeight) {
|
||||
weigher(cachePolicy!!.maxWeight) { key, value -> cachePolicy!!.weigher.weigh(key, value) }
|
||||
weigher(cachePolicy!!.maxWeight) { key, value ->
|
||||
cachePolicy!!.weigher.weigh(
|
||||
key,
|
||||
value
|
||||
)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
)
|
||||
|
||||
override fun <Network : Any, Local : Any> toMutableStoreBuilder(): MutableStoreBuilder<Key, Network, Output, Local> {
|
||||
override fun <Network : Any, Local : Any> toMutableStoreBuilder(converter: Converter<Network, Local, Output>): MutableStoreBuilder<Key, Network, Local, Output> {
|
||||
fetcher as Fetcher<Key, Network>
|
||||
return if (sourceOfTruth == null && memoryCache == null) {
|
||||
mutableStoreBuilderFromFetcher(fetcher)
|
||||
mutableStoreBuilderFromFetcher(fetcher, converter)
|
||||
} else if (memoryCache == null) {
|
||||
mutableStoreBuilderFromFetcherAndSourceOfTruth<Key, Network, Output, Local>(
|
||||
mutableStoreBuilderFromFetcherAndSourceOfTruth(
|
||||
fetcher,
|
||||
sourceOfTruth as SourceOfTruth<Key, Local>
|
||||
sourceOfTruth as SourceOfTruth<Key, Local, Output>,
|
||||
converter
|
||||
)
|
||||
} else {
|
||||
mutableStoreBuilderFromFetcherSourceOfTruthAndMemoryCache(
|
||||
fetcher,
|
||||
sourceOfTruth as SourceOfTruth<Key, Local>,
|
||||
memoryCache
|
||||
sourceOfTruth as SourceOfTruth<Key, Local, Output>,
|
||||
memoryCache,
|
||||
converter
|
||||
)
|
||||
}.apply {
|
||||
if (this@RealStoreBuilder.scope != null) {
|
||||
|
|
|
@ -40,8 +40,8 @@ import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
|
|||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal class SourceOfTruthWithBarrier<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||
private val delegate: SourceOfTruth<Key, Local>,
|
||||
private val converter: Converter<Network, Output, Local>? = null,
|
||||
private val delegate: SourceOfTruth<Key, Local, Output>,
|
||||
private val converter: Converter<Network, Local, Output>? = null,
|
||||
) {
|
||||
/**
|
||||
* Each key has a barrier so that we can block reads while writing.
|
||||
|
@ -76,7 +76,7 @@ internal class SourceOfTruthWithBarrier<Key : Any, Network : Any, Output : Any,
|
|||
}
|
||||
val readFlow: Flow<StoreReadResponse<Output?>> = when (barrierMessage) {
|
||||
is BarrierMsg.Open ->
|
||||
delegate.reader(key).mapIndexed { index, local ->
|
||||
delegate.reader(key).mapIndexed { index, local: Output? ->
|
||||
if (index == 0 && messageArrivedAfterMe) {
|
||||
val firstMsgOrigin = if (writeError == null) {
|
||||
// restarted barrier without an error means write succeeded
|
||||
|
@ -88,25 +88,15 @@ internal class SourceOfTruthWithBarrier<Key : Any, Network : Any, Output : Any,
|
|||
// use the SourceOfTruth as the origin
|
||||
StoreReadResponseOrigin.SourceOfTruth
|
||||
}
|
||||
|
||||
val output = when {
|
||||
local != null -> converter?.fromLocalToOutput(local) ?: local as? Output
|
||||
else -> null
|
||||
}
|
||||
|
||||
StoreReadResponse.Data(
|
||||
origin = firstMsgOrigin,
|
||||
value = output
|
||||
value = local
|
||||
)
|
||||
} else {
|
||||
val output = when {
|
||||
local != null -> converter?.fromLocalToOutput(local) ?: local as? Output
|
||||
else -> null
|
||||
}
|
||||
|
||||
StoreReadResponse.Data(
|
||||
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||
value = output
|
||||
value = local
|
||||
) as StoreReadResponse<Output?>
|
||||
}
|
||||
}.catch { throwable ->
|
||||
|
@ -148,14 +138,12 @@ internal class SourceOfTruthWithBarrier<Key : Any, Network : Any, Output : Any,
|
|||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
suspend fun write(key: Key, value: Output) {
|
||||
suspend fun write(key: Key, value: Local) {
|
||||
val barrier = barriers.acquire(key)
|
||||
try {
|
||||
barrier.emit(BarrierMsg.Blocked(versionCounter.incrementAndGet()))
|
||||
val writeError = try {
|
||||
val local = converter?.fromOutputToLocal(value)
|
||||
val input = local ?: value
|
||||
delegate.write(key, input as Local)
|
||||
delegate.write(key, value)
|
||||
null
|
||||
} catch (throwable: Throwable) {
|
||||
if (throwable !is CancellationException) {
|
||||
|
|
|
@ -43,7 +43,7 @@ class ClearAllStoreTests {
|
|||
|
||||
@Test
|
||||
fun callingClearAllOnStoreWithPersisterAndNoInMemoryCacheDeletesAllEntriesFromThePersister() = testScope.runTest {
|
||||
val store = StoreBuilder.from<String, Int, Int>(
|
||||
val store = StoreBuilder.from(
|
||||
fetcher = fetcher,
|
||||
sourceOfTruth = persister.asSourceOfTruth()
|
||||
).scope(testScope)
|
||||
|
@ -118,7 +118,7 @@ class ClearAllStoreTests {
|
|||
@Test
|
||||
fun callingClearAllOnStoreWithInMemoryCacheAndNoPersisterDeletesAllEntriesFromTheInMemoryCache() =
|
||||
testScope.runTest {
|
||||
val store = StoreBuilder.from<String, Int, Int>(
|
||||
val store = StoreBuilder.from(
|
||||
fetcher = fetcher
|
||||
).scope(testScope).build()
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ class ClearStoreByKeyTests {
|
|||
fun callingClearWithKeyOnStoreWithPersisterWithNoInMemoryCacheDeletesTheEntryAssociatedWithTheKeyFromThePersister() = testScope.runTest {
|
||||
val key = "key"
|
||||
val value = 1
|
||||
val store = StoreBuilder.from<String, Int, Int>(
|
||||
val store = StoreBuilder.from(
|
||||
fetcher = Fetcher.of { value },
|
||||
sourceOfTruth = persister.asSourceOfTruth()
|
||||
).scope(testScope)
|
||||
|
@ -66,7 +66,7 @@ class ClearStoreByKeyTests {
|
|||
fun callingClearWithKeyOStoreWithInMemoryCacheNoPersisterDeletesTheEntryAssociatedWithTheKeyFromTheInMemoryCache() = testScope.runTest {
|
||||
val key = "key"
|
||||
val value = 1
|
||||
val store = StoreBuilder.from<String, Int, Int>(
|
||||
val store = StoreBuilder.from(
|
||||
fetcher = Fetcher.of { value }
|
||||
).scope(testScope).build()
|
||||
|
||||
|
@ -107,7 +107,7 @@ class ClearStoreByKeyTests {
|
|||
val key2 = "key2"
|
||||
val value1 = 1
|
||||
val value2 = 2
|
||||
val store = StoreBuilder.from<String, Int, Int>(
|
||||
val store = StoreBuilder.from(
|
||||
fetcher = Fetcher.of { key ->
|
||||
when (key) {
|
||||
key1 -> value1
|
||||
|
|
|
@ -35,13 +35,13 @@ class FallbackTests {
|
|||
val fail = false
|
||||
|
||||
val hardcodedPagesFetcher = Fetcher.of<String, Page> { key -> hardcodedPages.get(key) }
|
||||
val secondaryApiFetcher = Fetcher.ofWithFallback<String, Page>(
|
||||
val secondaryApiFetcher = Fetcher.withFallback(
|
||||
secondaryApi.name,
|
||||
hardcodedPagesFetcher
|
||||
) { key -> secondaryApi.get(key) }
|
||||
|
||||
val store = StoreBuilder.from<String, Page, Page>(
|
||||
fetcher = Fetcher.ofWithFallback(api.name, secondaryApiFetcher) { key -> api.fetch(key, fail, ttl) },
|
||||
val store = StoreBuilder.from(
|
||||
fetcher = Fetcher.withFallback(api.name, secondaryApiFetcher) { key -> api.fetch(key, fail, ttl) },
|
||||
sourceOfTruth = SourceOfTruth.of(
|
||||
nonFlowReader = { key -> pagesDatabase.get(key) },
|
||||
writer = { key, page -> pagesDatabase.put(key, page) },
|
||||
|
@ -68,13 +68,13 @@ class FallbackTests {
|
|||
val fail = true
|
||||
|
||||
val hardcodedPagesFetcher = Fetcher.of<String, Page> { key -> hardcodedPages.get(key) }
|
||||
val secondaryApiFetcher = Fetcher.ofWithFallback<String, Page>(
|
||||
val secondaryApiFetcher = Fetcher.withFallback(
|
||||
secondaryApi.name,
|
||||
hardcodedPagesFetcher
|
||||
) { key -> secondaryApi.get(key) }
|
||||
|
||||
val store = StoreBuilder.from<String, Page, Page>(
|
||||
fetcher = Fetcher.ofWithFallback(api.name, secondaryApiFetcher) { key -> api.fetch(key, fail, ttl) },
|
||||
val store = StoreBuilder.from(
|
||||
fetcher = Fetcher.withFallback(api.name, secondaryApiFetcher) { key -> api.fetch(key, fail, ttl) },
|
||||
sourceOfTruth = SourceOfTruth.of(
|
||||
nonFlowReader = { key -> pagesDatabase.get(key) },
|
||||
writer = { key, page -> pagesDatabase.put(key, page) },
|
||||
|
@ -106,10 +106,10 @@ class FallbackTests {
|
|||
|
||||
val hardcodedPagesFetcher = Fetcher.of<String, Page> { key -> hardcodedPages.get(key) }
|
||||
val throwingSecondaryApiFetcher =
|
||||
Fetcher.ofWithFallback<String, Page>(secondaryApi.name, hardcodedPagesFetcher) { throw Exception() }
|
||||
Fetcher.withFallback(secondaryApi.name, hardcodedPagesFetcher) { throw Exception() }
|
||||
|
||||
val store = StoreBuilder.from<String, Page, Page>(
|
||||
fetcher = Fetcher.ofWithFallback(api.name, throwingSecondaryApiFetcher) { key ->
|
||||
val store = StoreBuilder.from(
|
||||
fetcher = Fetcher.withFallback(api.name, throwingSecondaryApiFetcher) { key ->
|
||||
api.fetch(
|
||||
key,
|
||||
fail,
|
||||
|
|
|
@ -19,7 +19,7 @@ class FetcherResponseTests {
|
|||
|
||||
@Test
|
||||
fun givenAFetcherThatThrowsAnExceptionInInvokeWhenStreamingThenTheExceptionsShouldNotBeCaught() = testScope.runTest {
|
||||
val store = StoreBuilder.from<Int, Int, Int>(
|
||||
val store = StoreBuilder.from(
|
||||
Fetcher.ofResult {
|
||||
throw RuntimeException("don't catch me")
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ class FetcherResponseTests {
|
|||
fun givenAFetcherThatEmitsErrorAndDataWhenSteamingThenItCanEmitValueAfterAnError() {
|
||||
val exception = RuntimeException("first error")
|
||||
testScope.runTest {
|
||||
val store = StoreBuilder.from<Int, String, String>(
|
||||
val store = StoreBuilder.from(
|
||||
fetcher = Fetcher.ofResultFlow { key: Int ->
|
||||
flowOf(
|
||||
FetcherResult.Error.Exception(exception),
|
||||
|
@ -61,7 +61,7 @@ class FetcherResponseTests {
|
|||
fun givenTransformerWhenRawValueThenUnwrappedValueReturnedAndValueIsCached() = testScope.runTest {
|
||||
val fetcher = Fetcher.ofFlow<Int, Int> { flowOf(it * it) }
|
||||
val pipeline = StoreBuilder
|
||||
.from<Int, Int, Int>(fetcher).buildWithTestScope()
|
||||
.from(fetcher).buildWithTestScope()
|
||||
|
||||
assertEmitsExactly(
|
||||
pipeline.stream(StoreReadRequest.cached(3, refresh = false)),
|
||||
|
@ -98,7 +98,7 @@ class FetcherResponseTests {
|
|||
}
|
||||
}
|
||||
}
|
||||
val pipeline = StoreBuilder.from<Int, Int, Int>(fetcher)
|
||||
val pipeline = StoreBuilder.from(fetcher)
|
||||
.buildWithTestScope()
|
||||
|
||||
assertEmitsExactly(
|
||||
|
@ -141,7 +141,7 @@ class FetcherResponseTests {
|
|||
}
|
||||
}
|
||||
val pipeline = StoreBuilder
|
||||
.from<Int, Int, Int>(fetcher)
|
||||
.from(fetcher)
|
||||
.buildWithTestScope()
|
||||
|
||||
assertEmitsExactly(
|
||||
|
@ -182,7 +182,7 @@ class FetcherResponseTests {
|
|||
count - 1
|
||||
}
|
||||
val pipeline = StoreBuilder
|
||||
.from<Int, Int, Int>(fetcher = fetcher)
|
||||
.from(fetcher = fetcher)
|
||||
.buildWithTestScope()
|
||||
|
||||
assertEmitsExactly(
|
||||
|
|
|
@ -56,7 +56,7 @@ class FlowStoreTests {
|
|||
3 to "three-2"
|
||||
)
|
||||
val pipeline = StoreBuilder
|
||||
.from<Int, String, String>(fetcher)
|
||||
.from(fetcher)
|
||||
.buildWithTestScope()
|
||||
|
||||
assertEquals(
|
||||
|
@ -113,7 +113,7 @@ class FlowStoreTests {
|
|||
3 to "three-2"
|
||||
)
|
||||
val persister = InMemoryPersister<Int, String>()
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||
val pipeline = StoreBuilder.from(
|
||||
fetcher = fetcher,
|
||||
sourceOfTruth = persister.asSourceOfTruth()
|
||||
).buildWithTestScope()
|
||||
|
@ -184,7 +184,7 @@ class FlowStoreTests {
|
|||
)
|
||||
val persister = InMemoryPersister<Int, String>()
|
||||
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||
val pipeline = StoreBuilder.from(
|
||||
fetcher = fetcher,
|
||||
sourceOfTruth = persister.asSourceOfTruth()
|
||||
).buildWithTestScope()
|
||||
|
@ -230,7 +230,7 @@ class FlowStoreTests {
|
|||
3 to "three-1",
|
||||
3 to "three-2"
|
||||
)
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(fetcher = fetcher)
|
||||
val pipeline = StoreBuilder.from(fetcher = fetcher)
|
||||
.buildWithTestScope()
|
||||
|
||||
assertEmitsExactly(
|
||||
|
@ -271,7 +271,7 @@ class FlowStoreTests {
|
|||
3 to "three-1",
|
||||
3 to "three-2"
|
||||
)
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(fetcher = fetcher)
|
||||
val pipeline = StoreBuilder.from(fetcher = fetcher)
|
||||
.buildWithTestScope()
|
||||
|
||||
assertEmitsExactly(
|
||||
|
@ -309,7 +309,7 @@ class FlowStoreTests {
|
|||
)
|
||||
val persister = InMemoryPersister<Int, String>()
|
||||
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||
val pipeline = StoreBuilder.from(
|
||||
fetcher = fetcher,
|
||||
sourceOfTruth = persister.asSourceOfTruth()
|
||||
)
|
||||
|
@ -357,7 +357,7 @@ class FlowStoreTests {
|
|||
@Test
|
||||
fun diskChangeWhileNetworkIsFlowing_simple() = testScope.runTest {
|
||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||
val pipeline = StoreBuilder.from(
|
||||
Fetcher.ofFlow {
|
||||
flow {
|
||||
delay(20)
|
||||
|
@ -395,7 +395,7 @@ class FlowStoreTests {
|
|||
@Test
|
||||
fun diskChangeWhileNetworkIsFlowing_overwrite() = testScope.runTest {
|
||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||
val pipeline = StoreBuilder.from(
|
||||
fetcher = Fetcher.ofFlow {
|
||||
flow {
|
||||
delay(10)
|
||||
|
@ -445,7 +445,7 @@ class FlowStoreTests {
|
|||
fun errorTest() = testScope.runTest {
|
||||
val exception = IllegalArgumentException("wow")
|
||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||
val pipeline = StoreBuilder.from(
|
||||
Fetcher.of {
|
||||
throw exception
|
||||
},
|
||||
|
@ -496,7 +496,7 @@ class FlowStoreTests {
|
|||
fun givenSourceOfTruthWhenStreamFreshDataReturnsNoDataFromFetcherThenFetchReturnsNoDataAndCachedValuesAreReceived() =
|
||||
testScope.runTest {
|
||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||
val pipeline = StoreBuilder.from(
|
||||
fetcher = Fetcher.ofFlow { flow {} },
|
||||
sourceOfTruth = persister.asSourceOfTruth()
|
||||
)
|
||||
|
@ -530,7 +530,7 @@ class FlowStoreTests {
|
|||
@Test
|
||||
fun givenSourceOfTruthWhenStreamCachedDataWithRefreshReturnsNoNewDataThenCachedValuesAreReceivedAndFetchReturnsNoData() = testScope.runTest {
|
||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||
val pipeline = StoreBuilder.from(
|
||||
fetcher = Fetcher.ofFlow { flow {} },
|
||||
sourceOfTruth = persister.asSourceOfTruth()
|
||||
)
|
||||
|
@ -564,7 +564,7 @@ class FlowStoreTests {
|
|||
@Test
|
||||
fun givenNoSourceOfTruthWhenStreamFreshDataReturnsNoDataFromFetcherThenFetchReturnsNoDataAndCachedValuesAreReceived() = testScope.runTest {
|
||||
var createCount = 0
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||
val pipeline = StoreBuilder.from(
|
||||
fetcher = Fetcher.ofFlow {
|
||||
if (createCount++ == 0) {
|
||||
flowOf("remote-1")
|
||||
|
@ -598,7 +598,7 @@ class FlowStoreTests {
|
|||
@Test
|
||||
fun givenNoSoTWhenStreamCachedDataWithRefreshReturnsNoNewDataThenCachedValuesAreReceivedAndFetchReturnsNoData() = testScope.runTest {
|
||||
var createCount = 0
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||
val pipeline = StoreBuilder.from(
|
||||
fetcher = Fetcher.ofFlow {
|
||||
if (createCount++ == 0) {
|
||||
flowOf("remote-1")
|
||||
|
@ -635,7 +635,7 @@ class FlowStoreTests {
|
|||
3 to "three-1",
|
||||
3 to "three-2"
|
||||
)
|
||||
val store = StoreBuilder.from<Int, String, String>(fetcher = fetcher)
|
||||
val store = StoreBuilder.from(fetcher = fetcher)
|
||||
.buildWithTestScope()
|
||||
|
||||
val firstFetch = store.fresh(3)
|
||||
|
@ -687,7 +687,7 @@ class FlowStoreTests {
|
|||
3 to "three-2"
|
||||
)
|
||||
val persister = InMemoryPersister<Int, String>()
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||
val pipeline = StoreBuilder.from(
|
||||
fetcher = fetcher,
|
||||
sourceOfTruth = persister.asSourceOfTruth()
|
||||
).buildWithTestScope()
|
||||
|
@ -758,7 +758,7 @@ class FlowStoreTests {
|
|||
3 to "three-2",
|
||||
3 to "three-3"
|
||||
)
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||
val pipeline = StoreBuilder.from(
|
||||
fetcher = fetcher
|
||||
).buildWithTestScope()
|
||||
|
||||
|
@ -816,7 +816,7 @@ class FlowStoreTests {
|
|||
3 to "three-1",
|
||||
3 to "three-2"
|
||||
)
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(fetcher = fetcher)
|
||||
val pipeline = StoreBuilder.from(fetcher = fetcher)
|
||||
.buildWithTestScope()
|
||||
|
||||
val fetcher1Collected = mutableListOf<StoreReadResponse<String>>()
|
||||
|
|
|
@ -22,7 +22,7 @@ class HotFlowStoreTests {
|
|||
3 to "three-2"
|
||||
)
|
||||
val pipeline = StoreBuilder
|
||||
.from<Int, String, String>(fetcher)
|
||||
.from(fetcher)
|
||||
.scope(testScope)
|
||||
.build()
|
||||
|
||||
|
|
|
@ -10,17 +10,17 @@ import org.mobilenativefoundation.store.store5.util.fake.NoteCollections
|
|||
import org.mobilenativefoundation.store.store5.util.fake.Notes
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesApi
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesDatabase
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesMemoryCache
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesKey
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesMemoryCache
|
||||
import org.mobilenativefoundation.store.store5.util.model.InputNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.NetworkNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.Note
|
||||
import org.mobilenativefoundation.store.store5.util.model.NoteData
|
||||
import org.mobilenativefoundation.store.store5.util.model.SOTNote
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
@OptIn(ExperimentalStoreApi::class)
|
||||
class MutableStoreWithMultiCacheTests {
|
||||
private val testScope = TestScope()
|
||||
private lateinit var api: NotesApi
|
||||
|
@ -34,31 +34,33 @@ class MutableStoreWithMultiCacheTests {
|
|||
|
||||
@Test
|
||||
fun givenEmptyStoreWhenListFromFetcherThenListIsDecomposed() = testScope.runTest {
|
||||
val memoryCache = NotesMemoryCache(MultiCache<String, Note>(CacheBuilder()))
|
||||
val memoryCache =
|
||||
NotesMemoryCache(MultiCache(CacheBuilder()))
|
||||
|
||||
val store = StoreBuilder.from<NotesKey, NetworkNote, NoteData, SOTNote>(
|
||||
fetcher = Fetcher.of<NotesKey, NetworkNote> { key -> api.get(key) },
|
||||
sourceOfTruth = SourceOfTruth.of<NotesKey, SOTNote>(
|
||||
nonFlowReader = { key -> database.get(key) },
|
||||
writer = { key, note -> database.put(key, note) },
|
||||
val converter: Converter<NetworkNote, NetworkNote, NoteData> =
|
||||
Converter.Builder<NetworkNote, NetworkNote, NoteData>()
|
||||
.fromNetworkToLocal { network: NetworkNote -> network }
|
||||
.fromOutputToLocal { output: NoteData -> NetworkNote(output, Long.MAX_VALUE) }
|
||||
.build()
|
||||
val store = StoreBuilder.from(
|
||||
fetcher = Fetcher.of { key -> api.get(key) },
|
||||
sourceOfTruth = SourceOfTruth.of(
|
||||
nonFlowReader = { key -> database.get(key)!!.data },
|
||||
writer = { key, note -> database.put(key, InputNote(note.data, Long.MAX_VALUE)) },
|
||||
delete = null,
|
||||
deleteAll = null
|
||||
),
|
||||
memoryCache = memoryCache
|
||||
).toMutableStoreBuilder<NetworkNote, SOTNote>()
|
||||
.converter(
|
||||
Converter.Builder<NetworkNote, NoteData, SOTNote>()
|
||||
.fromLocalToOutput { local -> local.data!! }
|
||||
.fromNetworkToOutput { network -> network.data!! }
|
||||
.fromOutputToLocal { output -> SOTNote(output, Long.MAX_VALUE) }
|
||||
.build()
|
||||
).build(
|
||||
updater = Updater.by(
|
||||
post = { _, _ -> UpdaterResult.Error.Exception(Exception()) }
|
||||
)
|
||||
).toMutableStoreBuilder(
|
||||
converter
|
||||
).build(
|
||||
updater = Updater.by(
|
||||
post = { _, _ -> UpdaterResult.Error.Exception(Exception()) }
|
||||
)
|
||||
)
|
||||
|
||||
val freshRequest = StoreReadRequest.fresh(NotesKey.Collection(NoteCollections.Keys.OneAndTwo))
|
||||
val freshRequest =
|
||||
StoreReadRequest.fresh(NotesKey.Collection(NoteCollections.Keys.OneAndTwo))
|
||||
|
||||
val freshStream = store.stream<UpdaterResult>(freshRequest)
|
||||
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
package org.mobilenativefoundation.store.store5
|
||||
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.mobilenativefoundation.store.store5.util.fake.fallback.HardcodedPages
|
||||
import org.mobilenativefoundation.store.store5.util.fake.fallback.Page
|
||||
import org.mobilenativefoundation.store.store5.util.fake.fallback.PagesDatabase
|
||||
import org.mobilenativefoundation.store.store5.util.fake.fallback.PrimaryPagesApi
|
||||
import org.mobilenativefoundation.store.store5.util.fake.fallback.SecondaryPagesApi
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class SourceOfTruthFallbackTests {
|
||||
private val testScope = TestScope()
|
||||
private lateinit var api: PrimaryPagesApi
|
||||
private lateinit var secondaryApi: SecondaryPagesApi
|
||||
private lateinit var hardcodedPages: HardcodedPages
|
||||
private lateinit var pagesDatabase: PagesDatabase
|
||||
|
||||
@BeforeTest
|
||||
fun before() {
|
||||
api = PrimaryPagesApi()
|
||||
secondaryApi = SecondaryPagesApi()
|
||||
hardcodedPages = HardcodedPages()
|
||||
pagesDatabase = PagesDatabase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNonEmptyStoreAndSourceOfTruthAsFallbackWhenFailureFromPrimaryApiThenStoreReadResponseOfSourceOfTruthResult() =
|
||||
testScope.runTest {
|
||||
|
||||
val sourceOfTruth = SourceOfTruth.of<String, Page>(
|
||||
nonFlowReader = { key -> pagesDatabase.get(key) },
|
||||
writer = { key, page -> pagesDatabase.put(key, page) },
|
||||
delete = null,
|
||||
deleteAll = null
|
||||
)
|
||||
|
||||
val ttl = null
|
||||
var fail = false
|
||||
val store = StoreBuilder.from<String, Page, Page>(
|
||||
fetcher = Fetcher.of { key -> api.fetch(key, fail, ttl) },
|
||||
sourceOfTruth = sourceOfTruth
|
||||
).build()
|
||||
|
||||
val responsesWithEmptyStore = store.stream(StoreReadRequest.fresh("1")).take(2).toList()
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()),
|
||||
StoreReadResponse.Data(Page.Data("1", null), StoreReadResponseOrigin.Fetcher())
|
||||
),
|
||||
responsesWithEmptyStore
|
||||
)
|
||||
fail = true
|
||||
val responsesWithNonEmptyStore =
|
||||
store.stream(StoreReadRequest.freshWithFallBackToSourceOfTruth("1")).take(2).toList()
|
||||
assertEquals(
|
||||
listOf(
|
||||
StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()),
|
||||
StoreReadResponse.Data(Page.Data("1", null), StoreReadResponseOrigin.SourceOfTruth)
|
||||
),
|
||||
responsesWithNonEmptyStore
|
||||
)
|
||||
}
|
||||
}
|
|
@ -42,7 +42,7 @@ import kotlin.test.assertNull
|
|||
class SourceOfTruthWithBarrierTests {
|
||||
private val testScope = TestScope()
|
||||
private val persister = InMemoryPersister<Int, String>()
|
||||
private val delegate: SourceOfTruth<Int, String> =
|
||||
private val delegate: SourceOfTruth<Int, String, String> =
|
||||
PersistentSourceOfTruth(
|
||||
realReader = { key ->
|
||||
flow {
|
||||
|
|
|
@ -17,7 +17,7 @@ class StoreWithInMemoryCacheTests {
|
|||
@Test
|
||||
fun storeRequestsCanCompleteWhenInMemoryCacheWithAccessExpiryIsAtTheMaximumSize() = testScope.runTest {
|
||||
val store = StoreBuilder
|
||||
.from<Int, String, String>(Fetcher.of { _: Int -> "result" })
|
||||
.from(Fetcher.of { _: Int -> "result" })
|
||||
.cachePolicy(
|
||||
MemoryPolicy
|
||||
.builder<Any, Any>()
|
||||
|
|
|
@ -24,7 +24,7 @@ class StreamWithoutSourceOfTruthTests {
|
|||
3 to "three-1",
|
||||
3 to "three-2"
|
||||
)
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(fetcher)
|
||||
val pipeline = StoreBuilder.from(fetcher)
|
||||
.scope(testScope)
|
||||
.build()
|
||||
val twoItemsNoRefresh = async {
|
||||
|
@ -70,7 +70,7 @@ class StreamWithoutSourceOfTruthTests {
|
|||
3 to "three-1",
|
||||
3 to "three-2"
|
||||
)
|
||||
val pipeline = StoreBuilder.from<Int, String, String>(fetcher)
|
||||
val pipeline = StoreBuilder.from(fetcher)
|
||||
.scope(testScope)
|
||||
.disableCache()
|
||||
.build()
|
||||
|
|
|
@ -1,247 +0,0 @@
|
|||
package org.mobilenativefoundation.store.store5
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.mobilenativefoundation.store.store5.impl.extensions.inHours
|
||||
import org.mobilenativefoundation.store.store5.util.assertEmitsExactly
|
||||
import org.mobilenativefoundation.store.store5.util.fake.Notes
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesApi
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesBookkeeping
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesConverterProvider
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesDatabase
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesKey
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesUpdaterProvider
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesValidator
|
||||
import org.mobilenativefoundation.store.store5.util.model.CommonNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.NetworkNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.NoteData
|
||||
import org.mobilenativefoundation.store.store5.util.model.NotesWriteResponse
|
||||
import org.mobilenativefoundation.store.store5.util.model.SOTNote
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalStoreApi::class)
|
||||
class UpdaterTests {
|
||||
private val testScope = TestScope()
|
||||
private lateinit var api: NotesApi
|
||||
private lateinit var bookkeeping: NotesBookkeeping
|
||||
private lateinit var notes: NotesDatabase
|
||||
|
||||
@BeforeTest
|
||||
fun before() {
|
||||
api = NotesApi()
|
||||
bookkeeping = NotesBookkeeping()
|
||||
notes = NotesDatabase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNonEmptyMarketWhenWriteThenStoredAndAPIUpdated() = testScope.runTest {
|
||||
val ttl = inHours(1)
|
||||
|
||||
val converter = NotesConverterProvider().provide()
|
||||
val validator = NotesValidator()
|
||||
val updater = NotesUpdaterProvider(api).provide()
|
||||
val bookkeeper = Bookkeeper.by(
|
||||
getLastFailedSync = bookkeeping::getLastFailedSync,
|
||||
setLastFailedSync = bookkeeping::setLastFailedSync,
|
||||
clear = bookkeeping::clear,
|
||||
clearAll = bookkeeping::clear
|
||||
)
|
||||
|
||||
val store = MutableStoreBuilder.from<NotesKey, NetworkNote, CommonNote, SOTNote>(
|
||||
fetcher = Fetcher.of { key -> api.get(key, ttl = ttl) },
|
||||
sourceOfTruth = SourceOfTruth.of(
|
||||
nonFlowReader = { key -> notes.get(key) },
|
||||
writer = { key, sot -> notes.put(key, sot) },
|
||||
delete = { key -> notes.clear(key) },
|
||||
deleteAll = { notes.clear() }
|
||||
)
|
||||
)
|
||||
.converter(converter)
|
||||
.validator(validator)
|
||||
.build(
|
||||
updater = updater,
|
||||
bookkeeper = bookkeeper
|
||||
)
|
||||
|
||||
val readRequest = StoreReadRequest.fresh(NotesKey.Single(Notes.One.id))
|
||||
|
||||
val stream = store.stream<NotesWriteResponse>(readRequest)
|
||||
|
||||
// Read is success
|
||||
assertEmitsExactly(
|
||||
stream,
|
||||
listOf(
|
||||
StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()),
|
||||
StoreReadResponse.Data(
|
||||
CommonNote(NoteData.Single(Notes.One), ttl = ttl),
|
||||
StoreReadResponseOrigin.Fetcher
|
||||
()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val newNote = Notes.One.copy(title = "New Title-1")
|
||||
val writeRequest = StoreWriteRequest.of<NotesKey, CommonNote, NotesWriteResponse>(
|
||||
key = NotesKey.Single(Notes.One.id),
|
||||
value = CommonNote(NoteData.Single(newNote))
|
||||
)
|
||||
|
||||
val storeWriteResponse = store.write(writeRequest)
|
||||
|
||||
// Write is success
|
||||
assertEquals(
|
||||
StoreWriteResponse.Success.Typed(NotesWriteResponse(NotesKey.Single(Notes.One.id), true)),
|
||||
storeWriteResponse
|
||||
)
|
||||
|
||||
val cachedReadRequest = StoreReadRequest.cached(NotesKey.Single(Notes.One.id), refresh = false)
|
||||
val cachedStream = store.stream<NotesWriteResponse>(cachedReadRequest)
|
||||
|
||||
// Cache + SOT are updated
|
||||
val firstResponse = cachedStream.first()
|
||||
assertEquals(
|
||||
StoreReadResponse.Data(
|
||||
CommonNote(NoteData.Single(newNote), ttl = null),
|
||||
StoreReadResponseOrigin.Cache
|
||||
),
|
||||
firstResponse
|
||||
)
|
||||
|
||||
val secondResponse = cachedStream.take(2).last()
|
||||
assertIs<StoreReadResponse.Data<CommonNote>>(secondResponse)
|
||||
val data = secondResponse.value.data
|
||||
assertIs<NoteData.Single>(data)
|
||||
assertNotNull(data)
|
||||
assertEquals(newNote, data.item)
|
||||
assertEquals(StoreReadResponseOrigin.SourceOfTruth, secondResponse.origin)
|
||||
assertNotNull(secondResponse.value.ttl)
|
||||
|
||||
// API is updated
|
||||
assertEquals(
|
||||
StoreWriteResponse.Success.Typed(NotesWriteResponse(NotesKey.Single(Notes.One.id), true)),
|
||||
storeWriteResponse
|
||||
)
|
||||
assertEquals(NetworkNote(NoteData.Single(newNote), ttl = null), api.db[NotesKey.Single(Notes.One.id)])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNonEmptyMarketWithValidatorWhenInvalidThenSuccessOriginatingFromFetcher() = testScope.runTest {
|
||||
val ttl = inHours(1)
|
||||
|
||||
val converter = NotesConverterProvider().provide()
|
||||
val validator = NotesValidator(expiration = inHours(12))
|
||||
val updater = NotesUpdaterProvider(api).provide()
|
||||
val bookkeeper = Bookkeeper.by(
|
||||
getLastFailedSync = bookkeeping::getLastFailedSync,
|
||||
setLastFailedSync = bookkeeping::setLastFailedSync,
|
||||
clear = bookkeeping::clear,
|
||||
clearAll = bookkeeping::clear
|
||||
)
|
||||
|
||||
val store = MutableStoreBuilder.from<NotesKey, NetworkNote, CommonNote, SOTNote>(
|
||||
fetcher = Fetcher.of { key -> api.get(key, ttl = ttl) },
|
||||
sourceOfTruth = SourceOfTruth.of(
|
||||
nonFlowReader = { key -> notes.get(key) },
|
||||
writer = { key, sot -> notes.put(key, sot) },
|
||||
delete = { key -> notes.clear(key) },
|
||||
deleteAll = { notes.clear() }
|
||||
)
|
||||
)
|
||||
.converter(converter)
|
||||
.validator(validator)
|
||||
.build(
|
||||
updater = updater,
|
||||
bookkeeper = bookkeeper
|
||||
)
|
||||
|
||||
val readRequest = StoreReadRequest.fresh(NotesKey.Single(Notes.One.id))
|
||||
|
||||
val stream = store.stream<NotesWriteResponse>(readRequest)
|
||||
|
||||
// Fetch is success and validator is not used
|
||||
assertEmitsExactly(
|
||||
stream,
|
||||
listOf(
|
||||
StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()),
|
||||
StoreReadResponse.Data(
|
||||
CommonNote(NoteData.Single(Notes.One), ttl = ttl),
|
||||
StoreReadResponseOrigin.Fetcher
|
||||
()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val cachedReadRequest = StoreReadRequest.cached(NotesKey.Single(Notes.One.id), refresh = false)
|
||||
val cachedStream = store.stream<NotesWriteResponse>(cachedReadRequest)
|
||||
|
||||
// Cache + SOT are updated
|
||||
// But item is invalid
|
||||
// So we do not emit value in cache or SOT
|
||||
// Instead we get latest from network even though refresh = false
|
||||
|
||||
assertEmitsExactly(
|
||||
cachedStream,
|
||||
listOf(
|
||||
StoreReadResponse.Data(
|
||||
CommonNote(NoteData.Single(Notes.One), ttl = ttl),
|
||||
StoreReadResponseOrigin.Fetcher
|
||||
()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenEmptyMarketWhenWriteThenSuccessResponsesAndApiUpdated() = testScope.runTest {
|
||||
val converter = NotesConverterProvider().provide()
|
||||
val validator = NotesValidator()
|
||||
val updater = NotesUpdaterProvider(api).provide()
|
||||
val bookkeeper = Bookkeeper.by(
|
||||
getLastFailedSync = bookkeeping::getLastFailedSync,
|
||||
setLastFailedSync = bookkeeping::setLastFailedSync,
|
||||
clear = bookkeeping::clear,
|
||||
clearAll = bookkeeping::clear
|
||||
)
|
||||
|
||||
val store = MutableStoreBuilder.from<NotesKey, NetworkNote, CommonNote, SOTNote>(
|
||||
fetcher = Fetcher.ofFlow { key ->
|
||||
val network = api.get(key)
|
||||
flow { emit(network) }
|
||||
},
|
||||
sourceOfTruth = SourceOfTruth.of(
|
||||
nonFlowReader = { key -> notes.get(key) },
|
||||
writer = { key, sot -> notes.put(key, sot) },
|
||||
delete = { key -> notes.clear(key) },
|
||||
deleteAll = { notes.clear() }
|
||||
)
|
||||
)
|
||||
.converter(converter)
|
||||
.validator(validator)
|
||||
.build(
|
||||
updater = updater,
|
||||
bookkeeper = bookkeeper
|
||||
)
|
||||
|
||||
val newNote = Notes.One.copy(title = "New Title-1")
|
||||
val writeRequest = StoreWriteRequest.of<NotesKey, CommonNote, NotesWriteResponse>(
|
||||
key = NotesKey.Single(Notes.One.id),
|
||||
value = CommonNote(NoteData.Single(newNote))
|
||||
)
|
||||
val storeWriteResponse = store.write(writeRequest)
|
||||
|
||||
assertEquals(
|
||||
StoreWriteResponse.Success.Typed(NotesWriteResponse(NotesKey.Single(Notes.One.id), true)),
|
||||
storeWriteResponse
|
||||
)
|
||||
assertEquals(NetworkNote(NoteData.Single(newNote)), api.db[NotesKey.Single(Notes.One.id)])
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
package org.mobilenativefoundation.store.store5.util.fake
|
||||
|
||||
import org.mobilenativefoundation.store.store5.util.TestApi
|
||||
import org.mobilenativefoundation.store.store5.util.model.CommonNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.InputNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.NetworkNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.NoteData
|
||||
import org.mobilenativefoundation.store.store5.util.model.NotesWriteResponse
|
||||
|
||||
internal class NotesApi : TestApi<NotesKey, NetworkNote, CommonNote, NotesWriteResponse> {
|
||||
internal class NotesApi : TestApi<NotesKey, NetworkNote, InputNote, NotesWriteResponse> {
|
||||
internal val db = mutableMapOf<NotesKey, NetworkNote>()
|
||||
|
||||
init {
|
||||
|
@ -26,7 +26,7 @@ internal class NotesApi : TestApi<NotesKey, NetworkNote, CommonNote, NotesWriteR
|
|||
}
|
||||
}
|
||||
|
||||
override fun post(key: NotesKey, value: CommonNote, fail: Boolean): NotesWriteResponse {
|
||||
override fun post(key: NotesKey, value: InputNote, fail: Boolean): NotesWriteResponse {
|
||||
if (fail) {
|
||||
throw Exception()
|
||||
}
|
||||
|
|
|
@ -2,14 +2,19 @@ package org.mobilenativefoundation.store.store5.util.fake
|
|||
|
||||
import org.mobilenativefoundation.store.store5.Converter
|
||||
import org.mobilenativefoundation.store.store5.impl.extensions.inHours
|
||||
import org.mobilenativefoundation.store.store5.util.model.CommonNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.InputNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.NetworkNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.SOTNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.OutputNote
|
||||
|
||||
internal class NotesConverterProvider {
|
||||
fun provide(): Converter<NetworkNote, CommonNote, SOTNote> = Converter.Builder<NetworkNote, CommonNote, SOTNote>()
|
||||
.fromLocalToOutput { value -> CommonNote(data = value.data, ttl = value.ttl) }
|
||||
.fromOutputToLocal { value -> SOTNote(data = value.data, ttl = value.ttl ?: inHours(12)) }
|
||||
.fromNetworkToOutput { value -> CommonNote(data = value.data, ttl = value.ttl) }
|
||||
.build()
|
||||
fun provide(): Converter<NetworkNote, InputNote, OutputNote> =
|
||||
Converter.Builder<NetworkNote, InputNote, OutputNote>()
|
||||
.fromOutputToLocal { value -> InputNote(data = value.data, ttl = value.ttl) }
|
||||
.fromNetworkToLocal { value: NetworkNote ->
|
||||
InputNote(
|
||||
data = value.data,
|
||||
ttl = value.ttl ?: inHours(12)
|
||||
)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
package org.mobilenativefoundation.store.store5.util.fake
|
||||
|
||||
import org.mobilenativefoundation.store.store5.util.model.SOTNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.InputNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.OutputNote
|
||||
|
||||
internal class NotesDatabase {
|
||||
private val db: MutableMap<NotesKey, SOTNote?> = mutableMapOf()
|
||||
fun put(key: NotesKey, input: SOTNote, fail: Boolean = false): Boolean {
|
||||
private val db: MutableMap<NotesKey, OutputNote?> = mutableMapOf()
|
||||
fun put(key: NotesKey, input: InputNote, fail: Boolean = false): Boolean {
|
||||
if (fail) {
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
db[key] = input
|
||||
db[key] = OutputNote(input.data, input.ttl ?: 0)
|
||||
return true
|
||||
}
|
||||
|
||||
fun get(key: NotesKey, fail: Boolean = false): SOTNote? {
|
||||
fun get(key: NotesKey, fail: Boolean = false): OutputNote? {
|
||||
if (fail) {
|
||||
throw Exception()
|
||||
}
|
||||
|
|
|
@ -2,13 +2,14 @@ package org.mobilenativefoundation.store.store5.util.fake
|
|||
|
||||
import org.mobilenativefoundation.store.store5.Updater
|
||||
import org.mobilenativefoundation.store.store5.UpdaterResult
|
||||
import org.mobilenativefoundation.store.store5.util.model.CommonNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.InputNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.NotesWriteResponse
|
||||
import org.mobilenativefoundation.store.store5.util.model.OutputNote
|
||||
|
||||
internal class NotesUpdaterProvider(private val api: NotesApi) {
|
||||
fun provide(): Updater<NotesKey, CommonNote, NotesWriteResponse> = Updater.by(
|
||||
fun provide(): Updater<NotesKey, OutputNote, NotesWriteResponse> = Updater.by(
|
||||
post = { key, input ->
|
||||
val response = api.post(key, input)
|
||||
val response = api.post(key, InputNote(input.data, input.ttl ?: 0))
|
||||
if (response.ok) {
|
||||
UpdaterResult.Success.Typed(response)
|
||||
} else {
|
||||
|
|
|
@ -2,10 +2,10 @@ package org.mobilenativefoundation.store.store5.util.fake
|
|||
|
||||
import org.mobilenativefoundation.store.store5.Validator
|
||||
import org.mobilenativefoundation.store.store5.impl.extensions.now
|
||||
import org.mobilenativefoundation.store.store5.util.model.CommonNote
|
||||
import org.mobilenativefoundation.store.store5.util.model.OutputNote
|
||||
|
||||
internal class NotesValidator(private val expiration: Long = now()) : Validator<CommonNote> {
|
||||
override suspend fun isValid(item: CommonNote): Boolean = when {
|
||||
internal class NotesValidator(private val expiration: Long = now()) : Validator<OutputNote> {
|
||||
override suspend fun isValid(item: OutputNote): Boolean = when {
|
||||
item.ttl == null -> true
|
||||
else -> item.ttl > expiration
|
||||
}
|
||||
|
|
|
@ -18,12 +18,12 @@ internal data class NetworkNote(
|
|||
val ttl: Long? = null,
|
||||
)
|
||||
|
||||
internal data class CommonNote(
|
||||
internal data class InputNote(
|
||||
val data: NoteData? = null,
|
||||
val ttl: Long? = null,
|
||||
)
|
||||
|
||||
internal data class SOTNote(
|
||||
internal data class OutputNote(
|
||||
val data: NoteData? = null,
|
||||
val ttl: Long
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue