[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:
Mike Nakhimovich 2023-07-05 16:24:37 -04:00 committed by GitHub
parent fbcd34fd16
commit 2499d6e080
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 297 additions and 597 deletions

View file

@ -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() } }

View file

@ -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()

View file

@ -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]!! }

View file

@ -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)
}

View file

@ -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

View file

@ -1,5 +1,6 @@
package org.mobilenativefoundation.store.store5
@ExperimentalStoreApi
interface MutableStore<Key : Any, Output : Any> :
Read.StreamWithConflictResolution<Key, Output>,
Write<Key, Output>,

View file

@ -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
)
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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)
}
}
)

View file

@ -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, *>,

View file

@ -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()
}

View file

@ -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)

View file

@ -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)

View file

@ -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) {

View file

@ -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) {

View file

@ -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()

View file

@ -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

View file

@ -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,

View file

@ -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(

View file

@ -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>>()

View file

@ -22,7 +22,7 @@ class HotFlowStoreTests {
3 to "three-2"
)
val pipeline = StoreBuilder
.from<Int, String, String>(fetcher)
.from(fetcher)
.scope(testScope)
.build()

View file

@ -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)

View file

@ -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
)
}
}

View file

@ -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 {

View file

@ -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>()

View file

@ -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()

View file

@ -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)])
}
}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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
)