Add Write + Conflict Resolution (#496)
* Stub Store write Signed-off-by: mramotar <mramotar@dropbox.com> * Format Signed-off-by: mramotar <mramotar@dropbox.com> * Compile Signed-off-by: mramotar <mramotar@dropbox.com> * Fix tests Signed-off-by: mramotar <mramotar@dropbox.com> * Stash M1 Signed-off-by: mramotar <mramotar@dropbox.com> * Make Updater and Bookkeeper optional Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Add conflict resolution Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Cover simple write Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Add MutableStore Signed-off-by: mramotar <mramotar@dropbox.com> * Add RealMutableStore Signed-off-by: mramotar <mramotar@dropbox.com> * Update workflows Signed-off-by: mramotar <mramotar@dropbox.com> * Format Signed-off-by: mramotar <mramotar@dropbox.com> * Remove references to Market Signed-off-by: mramotar <mramotar@dropbox.com> * Remove Converter interface Signed-off-by: mramotar <mramotar@dropbox.com> * Move Converter typealias Signed-off-by: mramotar <mramotar@dropbox.com> * Remove Google copyright Signed-off-by: mramotar <mramotar@dropbox.com> * Update CHANGELOG.md Signed-off-by: mramotar <mramotar@dropbox.com> Signed-off-by: mramotar <mramotar@dropbox.com> Signed-off-by: Matt Ramotar <mramotar@dropbox.com>
This commit is contained in:
parent
461af1a784
commit
1b21081986
67 changed files with 1722 additions and 797 deletions
2
.github/workflows/.ci_test_and_publish.yml
vendored
2
.github/workflows/.ci_test_and_publish.yml
vendored
|
@ -18,6 +18,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
|
||||||
- name: Set up our JDK environment
|
- name: Set up our JDK environment
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v2
|
||||||
|
|
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
|
@ -6,6 +6,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/gradle-build-action@v2
|
||||||
- name: Run check with Gradle Wrapper
|
- name: Run check with Gradle Wrapper
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
* Introduce MutableStore
|
||||||
|
* Implement RealMutableStore with Store delegate
|
||||||
|
* Extract Store and MutableStore methods to use cases
|
||||||
|
|
||||||
## [5.0.0-alpha03] (2022-12-18)
|
## [5.0.0-alpha03] (2022-12-18)
|
||||||
|
|
||||||
|
|
|
@ -61,4 +61,8 @@ object Deps {
|
||||||
const val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Version.kotlinxCoroutines}"
|
const val coroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Version.kotlinxCoroutines}"
|
||||||
const val junit = "junit:junit:${Version.junit}"
|
const val junit = "junit:junit:${Version.junit}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object Touchlab {
|
||||||
|
const val kermit = "co.touchlab:kermit:${Version.kermit}"
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -24,6 +24,7 @@ object Version {
|
||||||
const val junit = "4.13.2"
|
const val junit = "4.13.2"
|
||||||
const val kotlinxCoroutines = "1.6.4"
|
const val kotlinxCoroutines = "1.6.4"
|
||||||
const val kotlinxSerialization = "1.4.0"
|
const val kotlinxSerialization = "1.4.0"
|
||||||
|
const val kermit = "1.2.2"
|
||||||
const val testCore = "1.4.0"
|
const val testCore = "1.4.0"
|
||||||
const val kmmBridge = "0.3.2"
|
const val kmmBridge = "0.3.2"
|
||||||
const val ktlint = "0.39.0"
|
const val ktlint = "0.39.0"
|
||||||
|
|
|
@ -2,7 +2,7 @@ package org.mobilenativefoundation.store.cache5
|
||||||
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
class CacheBuilder<Key : Any, Value : Any> {
|
class CacheBuilder<Key : Any, CommonRepresentation : Any> {
|
||||||
internal var concurrencyLevel = 4
|
internal var concurrencyLevel = 4
|
||||||
private set
|
private set
|
||||||
internal val initialCapacity = 16
|
internal val initialCapacity = 16
|
||||||
|
@ -14,41 +14,41 @@ class CacheBuilder<Key : Any, Value : Any> {
|
||||||
private set
|
private set
|
||||||
internal var expireAfterWrite: Duration = Duration.INFINITE
|
internal var expireAfterWrite: Duration = Duration.INFINITE
|
||||||
private set
|
private set
|
||||||
internal var weigher: Weigher<Key, Value>? = null
|
internal var weigher: Weigher<Key, CommonRepresentation>? = null
|
||||||
private set
|
private set
|
||||||
internal var ticker: Ticker? = null
|
internal var ticker: Ticker? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun concurrencyLevel(producer: () -> Int): CacheBuilder<Key, Value> = apply {
|
fun concurrencyLevel(producer: () -> Int): CacheBuilder<Key, CommonRepresentation> = apply {
|
||||||
concurrencyLevel = producer.invoke()
|
concurrencyLevel = producer.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun maximumSize(maximumSize: Long): CacheBuilder<Key, Value> = apply {
|
fun maximumSize(maximumSize: Long): CacheBuilder<Key, CommonRepresentation> = apply {
|
||||||
if (maximumSize < 0) {
|
if (maximumSize < 0) {
|
||||||
throw IllegalArgumentException("Maximum size must be non-negative.")
|
throw IllegalArgumentException("Maximum size must be non-negative.")
|
||||||
}
|
}
|
||||||
this.maximumSize = maximumSize
|
this.maximumSize = maximumSize
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expireAfterAccess(duration: Duration): CacheBuilder<Key, Value> = apply {
|
fun expireAfterAccess(duration: Duration): CacheBuilder<Key, CommonRepresentation> = apply {
|
||||||
if (duration.isNegative()) {
|
if (duration.isNegative()) {
|
||||||
throw IllegalArgumentException("Duration must be non-negative.")
|
throw IllegalArgumentException("Duration must be non-negative.")
|
||||||
}
|
}
|
||||||
expireAfterAccess = duration
|
expireAfterAccess = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expireAfterWrite(duration: Duration): CacheBuilder<Key, Value> = apply {
|
fun expireAfterWrite(duration: Duration): CacheBuilder<Key, CommonRepresentation> = apply {
|
||||||
if (duration.isNegative()) {
|
if (duration.isNegative()) {
|
||||||
throw IllegalArgumentException("Duration must be non-negative.")
|
throw IllegalArgumentException("Duration must be non-negative.")
|
||||||
}
|
}
|
||||||
expireAfterWrite = duration
|
expireAfterWrite = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ticker(ticker: Ticker): CacheBuilder<Key, Value> = apply {
|
fun ticker(ticker: Ticker): CacheBuilder<Key, CommonRepresentation> = apply {
|
||||||
this.ticker = ticker
|
this.ticker = ticker
|
||||||
}
|
}
|
||||||
|
|
||||||
fun weigher(maximumWeight: Long, weigher: Weigher<Key, Value>): CacheBuilder<Key, Value> = apply {
|
fun weigher(maximumWeight: Long, weigher: Weigher<Key, CommonRepresentation>): CacheBuilder<Key, CommonRepresentation> = apply {
|
||||||
if (maximumWeight < 0) {
|
if (maximumWeight < 0) {
|
||||||
throw IllegalArgumentException("Maximum weight must be non-negative.")
|
throw IllegalArgumentException("Maximum weight must be non-negative.")
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ class CacheBuilder<Key : Any, Value : Any> {
|
||||||
this.weigher = weigher
|
this.weigher = weigher
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build(): Cache<Key, Value> {
|
fun build(): Cache<Key, CommonRepresentation> {
|
||||||
if (maximumSize != -1L && weigher != null) {
|
if (maximumSize != -1L && weigher != null) {
|
||||||
throw IllegalStateException("Maximum size cannot be combined with weigher.")
|
throw IllegalStateException("Maximum size cannot be combined with weigher.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ kotlin {
|
||||||
implementation(serializationCore)
|
implementation(serializationCore)
|
||||||
implementation(dateTime)
|
implementation(dateTime)
|
||||||
}
|
}
|
||||||
|
implementation(Deps.Touchlab.kermit)
|
||||||
implementation(project(":multicast"))
|
implementation(project(":multicast"))
|
||||||
implementation(project(":cache"))
|
implementation(project(":cache"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.RealBookkeeper
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.RealMutableStore
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.extensions.now
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks when local changes fail to sync with network.
|
||||||
|
* @see [RealMutableStore] usage to persist write request failures and eagerly resolve conflicts before completing a read request.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Bookkeeper<Key : Any> {
|
||||||
|
suspend fun getLastFailedSync(key: Key): Long?
|
||||||
|
suspend fun setLastFailedSync(key: Key, timestamp: Long = now()): Boolean
|
||||||
|
suspend fun clear(key: Key): Boolean
|
||||||
|
suspend fun clearAll(): Boolean
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <Key : Any> by(
|
||||||
|
getLastFailedSync: suspend (key: Key) -> Long?,
|
||||||
|
setLastFailedSync: suspend (key: Key, timestamp: Long) -> Boolean,
|
||||||
|
clear: suspend (key: Key) -> Boolean,
|
||||||
|
clearAll: suspend () -> Boolean
|
||||||
|
): Bookkeeper<Key> = RealBookkeeper(getLastFailedSync, setLastFailedSync, clear, clearAll)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
interface Clear {
|
||||||
|
interface Key<Key : Any> {
|
||||||
|
/**
|
||||||
|
* Purge a particular entry from memory and disk cache.
|
||||||
|
* Persistent storage will only be cleared if a delete function was passed to
|
||||||
|
* [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store].
|
||||||
|
*/
|
||||||
|
suspend fun clear(key: Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface All {
|
||||||
|
/**
|
||||||
|
* Purge all entries from memory and disk cache.
|
||||||
|
* Persistent storage will only be cleared if a clear function was passed to
|
||||||
|
* [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store].
|
||||||
|
*/
|
||||||
|
@ExperimentalStoreApi
|
||||||
|
suspend fun clear()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.RealItemValidator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables custom validation of [Store] items.
|
||||||
|
* @see [StoreReadRequest]
|
||||||
|
*/
|
||||||
|
interface ItemValidator<CommonRepresentation : Any> {
|
||||||
|
/**
|
||||||
|
* Determines whether a [Store] item is valid.
|
||||||
|
* If invalid, [MutableStore] will get the latest network value using [Fetcher].
|
||||||
|
* [MutableStore] will not validate network responses.
|
||||||
|
*/
|
||||||
|
suspend fun isValid(item: CommonRepresentation): Boolean
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <CommonRepresentation : Any> by(
|
||||||
|
validator: suspend (item: CommonRepresentation) -> Boolean
|
||||||
|
): ItemValidator<CommonRepresentation> = RealItemValidator(validator)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package org.mobilenativefoundation.store.store5
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
import org.mobilenativefoundation.store.cache5.Cache
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
|
|
||||||
fun interface Weigher<in K : Any, in V : Any> {
|
fun interface Weigher<in K : Any, in V : Any> {
|
||||||
/**
|
/**
|
||||||
|
@ -18,17 +18,10 @@ internal object OneWeigher : Weigher<Any, Any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MemoryPolicy holds all required info to create MemoryCache
|
* Defines behavior of in-memory [Cache].
|
||||||
*
|
* Used by [Store].
|
||||||
*
|
* @see [Store]
|
||||||
* This class is used, in order to define the appropriate parameters for the Memory [com.dropbox.android.external.cache3.Cache]
|
|
||||||
* to be built.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* MemoryPolicy is used by a [Store]
|
|
||||||
* and defines the in-memory cache behavior.
|
|
||||||
*/
|
*/
|
||||||
@ExperimentalTime
|
|
||||||
class MemoryPolicy<in Key : Any, in Value : Any> internal constructor(
|
class MemoryPolicy<in Key : Any, in Value : Any> internal constructor(
|
||||||
val expireAfterWrite: Duration,
|
val expireAfterWrite: Duration,
|
||||||
val expireAfterAccess: Duration,
|
val expireAfterAccess: Duration,
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
interface MutableStore<Key : Any, CommonRepresentation : Any> :
|
||||||
|
Read.StreamWithConflictResolution<Key, CommonRepresentation>,
|
||||||
|
Write<Key, CommonRepresentation>,
|
||||||
|
Write.Stream<Key, CommonRepresentation>,
|
||||||
|
Clear.Key<Key>,
|
||||||
|
Clear
|
|
@ -0,0 +1,6 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
data class OnFetcherCompletion<NetworkRepresentation : Any>(
|
||||||
|
val onSuccess: (FetcherResult.Data<NetworkRepresentation>) -> Unit,
|
||||||
|
val onFailure: (FetcherResult.Error) -> Unit
|
||||||
|
)
|
|
@ -0,0 +1,6 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
data class OnUpdaterCompletion<NetworkWriteResponse : Any>(
|
||||||
|
val onSuccess: (UpdaterResult.Success) -> Unit,
|
||||||
|
val onFailure: (UpdaterResult.Error) -> Unit
|
||||||
|
)
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface Read {
|
||||||
|
interface Stream<Key : Any, CommonRepresentation : Any> {
|
||||||
|
/**
|
||||||
|
* Return a flow for the given key
|
||||||
|
* @param request - see [StoreReadRequest] for configurations
|
||||||
|
*/
|
||||||
|
fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StreamWithConflictResolution<Key : Any, CommonRepresentation : Any> {
|
||||||
|
fun <NetworkWriteResponse : Any> stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>>
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,21 +39,21 @@ import kotlin.jvm.JvmName
|
||||||
* a common flowing API.
|
* a common flowing API.
|
||||||
*
|
*
|
||||||
* A source of truth is usually backed by local storage. It's purpose is to eliminate the need
|
* A source of truth is usually backed by local storage. It's purpose is to eliminate the need
|
||||||
* for waiting on network update before local modifications are available (via [Store.stream]).
|
* for waiting on network update before local modifications are available (via [Store.Stream.read]).
|
||||||
*
|
*
|
||||||
* For maximal flexibility, [writer]'s record type ([Input]] and [reader]'s record type
|
* For maximal flexibility, [writer]'s record type ([Input]] and [reader]'s record type
|
||||||
* ([Output]) are not identical. This allows us to read one type of objects from network and
|
* ([Output]) are not identical. This allows us to read one type of objects from network and
|
||||||
* transform them to another type when placing them in local storage.
|
* transform them to another type when placing them in local storage.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
interface SourceOfTruth<Key, Input, Output> {
|
interface SourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by [Store] to read records from the source of truth.
|
* Used by [Store] to read records from the source of truth.
|
||||||
*
|
*
|
||||||
* @param key The key to read for.
|
* @param key The key to read for.
|
||||||
*/
|
*/
|
||||||
fun reader(key: Key): Flow<Output?>
|
fun reader(key: Key): Flow<SourceOfTruthRepresentation?>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by [Store] to write records **coming in from the fetcher (network)** to the source of
|
* Used by [Store] to write records **coming in from the fetcher (network)** to the source of
|
||||||
|
@ -61,12 +61,12 @@ interface SourceOfTruth<Key, Input, Output> {
|
||||||
*
|
*
|
||||||
* **Note:** [Store] currently does not support updating the source of truth with local user
|
* **Note:** [Store] currently does not support updating the source of truth with local user
|
||||||
* updates (i.e writing record of type [Output]). However, any changes in the local database
|
* updates (i.e writing record of type [Output]). However, any changes in the local database
|
||||||
* will still be visible via [Store.stream] APIs as long as you are using a local storage that
|
* will still be visible via [Store.Stream.read] APIs as long as you are using a local storage that
|
||||||
* supports observability (e.g. Room, SQLDelight, Realm).
|
* supports observability (e.g. Room, SQLDelight, Realm).
|
||||||
*
|
*
|
||||||
* @param key The key to update for.
|
* @param key The key to update for.
|
||||||
*/
|
*/
|
||||||
suspend fun write(key: Key, value: Input)
|
suspend fun write(key: Key, value: SourceOfTruthRepresentation)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by [Store] to delete records in the source of truth for the given key.
|
* Used by [Store] to delete records in the source of truth for the given key.
|
||||||
|
@ -90,12 +90,12 @@ interface SourceOfTruth<Key, Input, Output> {
|
||||||
* @param delete function for deleting records in the source of truth for the given key
|
* @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
|
* @param deleteAll function for deleting all records in the source of truth
|
||||||
*/
|
*/
|
||||||
fun <Key : Any, Input : Any, Output : Any> of(
|
fun <Key : Any, SourceOfTruthRepresentation : Any> of(
|
||||||
nonFlowReader: suspend (Key) -> Output?,
|
nonFlowReader: suspend (Key) -> SourceOfTruthRepresentation?,
|
||||||
writer: suspend (Key, Input) -> Unit,
|
writer: suspend (Key, SourceOfTruthRepresentation) -> Unit,
|
||||||
delete: (suspend (Key) -> Unit)? = null,
|
delete: (suspend (Key) -> Unit)? = null,
|
||||||
deleteAll: (suspend () -> Unit)? = null
|
deleteAll: (suspend () -> Unit)? = null
|
||||||
): SourceOfTruth<Key, Input, Output> = PersistentNonFlowingSourceOfTruth(
|
): SourceOfTruth<Key, SourceOfTruthRepresentation> = PersistentNonFlowingSourceOfTruth(
|
||||||
realReader = nonFlowReader,
|
realReader = nonFlowReader,
|
||||||
realWriter = writer,
|
realWriter = writer,
|
||||||
realDelete = delete,
|
realDelete = delete,
|
||||||
|
@ -112,12 +112,12 @@ interface SourceOfTruth<Key, Input, Output> {
|
||||||
* @param deleteAll function for deleting all records in the source of truth
|
* @param deleteAll function for deleting all records in the source of truth
|
||||||
*/
|
*/
|
||||||
@JvmName("ofFlow")
|
@JvmName("ofFlow")
|
||||||
fun <Key : Any, Input : Any, Output : Any> of(
|
fun <Key : Any, SourceOfTruthRepresentation : Any> of(
|
||||||
reader: (Key) -> Flow<Output?>,
|
reader: (Key) -> Flow<SourceOfTruthRepresentation?>,
|
||||||
writer: suspend (Key, Input) -> Unit,
|
writer: suspend (Key, SourceOfTruthRepresentation) -> Unit,
|
||||||
delete: (suspend (Key) -> Unit)? = null,
|
delete: (suspend (Key) -> Unit)? = null,
|
||||||
deleteAll: (suspend () -> Unit)? = null
|
deleteAll: (suspend () -> Unit)? = null
|
||||||
): SourceOfTruth<Key, Input, Output> = PersistentSourceOfTruth(
|
): SourceOfTruth<Key, SourceOfTruthRepresentation> = PersistentSourceOfTruth(
|
||||||
realReader = reader,
|
realReader = reader,
|
||||||
realWriter = writer,
|
realWriter = writer,
|
||||||
realDelete = delete,
|
realDelete = delete,
|
||||||
|
@ -128,7 +128,7 @@ interface SourceOfTruth<Key, Input, Output> {
|
||||||
/**
|
/**
|
||||||
* The exception provided when a write operation fails in SourceOfTruth.
|
* The exception provided when a write operation fails in SourceOfTruth.
|
||||||
*
|
*
|
||||||
* see [StoreResponse.Error.Exception]
|
* see [StoreReadResponse.Error.Exception]
|
||||||
*/
|
*/
|
||||||
class WriteException(
|
class WriteException(
|
||||||
/**
|
/**
|
||||||
|
@ -169,7 +169,7 @@ interface SourceOfTruth<Key, Input, Output> {
|
||||||
/**
|
/**
|
||||||
* Exception created when a [reader] throws an exception.
|
* Exception created when a [reader] throws an exception.
|
||||||
*
|
*
|
||||||
* see [StoreResponse.Error.Exception]
|
* see [StoreReadResponse.Error.Exception]
|
||||||
*/
|
*/
|
||||||
class ReadException(
|
class ReadException(
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package org.mobilenativefoundation.store.store5
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Store is responsible for managing a particular data request.
|
* A Store is responsible for managing a particular data request.
|
||||||
*
|
*
|
||||||
|
@ -33,26 +31,7 @@ import kotlinx.coroutines.flow.Flow
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
interface Store<Key : Any, Output : Any> {
|
interface Store<Key : Any, CommonRepresentation : Any> :
|
||||||
|
Read.Stream<Key, CommonRepresentation>,
|
||||||
/**
|
Clear.Key<Key>,
|
||||||
* Return a flow for the given key
|
Clear.All
|
||||||
* @param request - see [StoreRequest] for configurations
|
|
||||||
*/
|
|
||||||
fun stream(request: StoreRequest<Key>): Flow<StoreResponse<Output>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Purge a particular entry from memory and disk cache.
|
|
||||||
* Persistent storage will only be cleared if a delete function was passed to
|
|
||||||
* [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store].
|
|
||||||
*/
|
|
||||||
suspend fun clear(key: Key)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Purge all entries from memory and disk cache.
|
|
||||||
* Persistent storage will only be cleared if a deleteAll function was passed to
|
|
||||||
* [StoreBuilder.persister] or [StoreBuilder.nonFlowingPersister] when creating the [Store].
|
|
||||||
*/
|
|
||||||
@ExperimentalStoreApi
|
|
||||||
suspend fun clearAll()
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,36 +16,43 @@
|
||||||
package org.mobilenativefoundation.store.store5
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import org.mobilenativefoundation.store.store5.impl.RealStoreBuilder
|
import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcher
|
||||||
import kotlin.time.ExperimentalTime
|
import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcherAndSourceOfTruth
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry point for creating a [Store].
|
* Main entry point for creating a [Store].
|
||||||
*/
|
*/
|
||||||
interface StoreBuilder<Key : Any, Output : Any> {
|
interface StoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> {
|
||||||
fun build(): Store<Key, Output>
|
fun build(): Store<Key, CommonRepresentation>
|
||||||
|
|
||||||
|
fun <NetworkWriteResponse : Any> build(
|
||||||
|
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>,
|
||||||
|
bookkeeper: Bookkeeper<Key>
|
||||||
|
): MutableStore<Key, CommonRepresentation>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store multicasts same [Output] value to many consumers (Similar to RxJava.share()), by default
|
* A store multicasts same [CommonRepresentation] value to many consumers (Similar to RxJava.share()), by default
|
||||||
* [Store] will open a global scope for management of shared responses, if instead you'd like to control
|
* [Store] will open a global scope for management of shared responses, if instead you'd like to control
|
||||||
* the scope that sharing/multicasting happens in you can pass a @param [scope]
|
* the scope that sharing/multicasting happens in you can pass a @param [scope]
|
||||||
*
|
*
|
||||||
* @param scope - scope to use for sharing
|
* @param scope - scope to use for sharing
|
||||||
*/
|
*/
|
||||||
fun scope(scope: CoroutineScope): StoreBuilder<Key, Output>
|
fun scope(scope: CoroutineScope): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* controls eviction policy for a store cache, use [MemoryPolicy.MemoryPolicyBuilder] to configure a TTL
|
* controls eviction policy for a store cache, use [MemoryPolicy.MemoryPolicyBuilder] to configure a TTL
|
||||||
* or size based eviction
|
* or size based eviction
|
||||||
* Example: MemoryPolicy.builder().setExpireAfterWrite(10.seconds).build()
|
* Example: MemoryPolicy.builder().setExpireAfterWrite(10.seconds).build()
|
||||||
*/
|
*/
|
||||||
@ExperimentalTime
|
fun cachePolicy(memoryPolicy: MemoryPolicy<Key, CommonRepresentation>?): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
|
||||||
fun cachePolicy(memoryPolicy: MemoryPolicy<Key, Output>?): StoreBuilder<Key, Output>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* by default a Store caches in memory with a default policy of max items = 100
|
* by default a Store caches in memory with a default policy of max items = 100
|
||||||
*/
|
*/
|
||||||
fun disableCache(): StoreBuilder<Key, Output>
|
fun disableCache(): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
|
||||||
|
|
||||||
|
fun converter(converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>):
|
||||||
|
StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@ -54,10 +61,9 @@ interface StoreBuilder<Key : Any, Output : Any> {
|
||||||
*
|
*
|
||||||
* @param fetcher a [Fetcher] flow of network records.
|
* @param fetcher a [Fetcher] flow of network records.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalTime::class)
|
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any> from(
|
||||||
fun <Key : Any, Output : Any> from(
|
fetcher: Fetcher<Key, NetworkRepresentation>,
|
||||||
fetcher: Fetcher<Key, Output>
|
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, *> = storeBuilderFromFetcher(fetcher = fetcher)
|
||||||
): StoreBuilder<Key, Output> = RealStoreBuilder(fetcher)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new [StoreBuilder] from a [Fetcher] and a [SourceOfTruth].
|
* Creates a new [StoreBuilder] from a [Fetcher] and a [SourceOfTruth].
|
||||||
|
@ -65,12 +71,10 @@ interface StoreBuilder<Key : Any, Output : Any> {
|
||||||
* @param fetcher a function for fetching a flow of network records.
|
* @param fetcher a function for fetching a flow of network records.
|
||||||
* @param sourceOfTruth a [SourceOfTruth] for the store.
|
* @param sourceOfTruth a [SourceOfTruth] for the store.
|
||||||
*/
|
*/
|
||||||
fun <Key : Any, Input : Any, Output : Any> from(
|
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> from(
|
||||||
fetcher: Fetcher<Key, Input>,
|
fetcher: Fetcher<Key, NetworkRepresentation>,
|
||||||
sourceOfTruth: SourceOfTruth<Key, Input, Output>
|
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>
|
||||||
): StoreBuilder<Key, Output> = RealStoreBuilder(
|
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> =
|
||||||
fetcher = fetcher,
|
storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth)
|
||||||
sourceOfTruth = sourceOfTruth
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
import org.mobilenativefoundation.store.store5.internal.definition.Converter
|
||||||
|
|
||||||
|
interface StoreConverter<NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> {
|
||||||
|
fun fromNetworkRepresentationToCommonRepresentation(networkRepresentation: NetworkRepresentation): CommonRepresentation?
|
||||||
|
fun fromCommonRepresentationToSourceOfTruthRepresentation(commonRepresentation: CommonRepresentation): SourceOfTruthRepresentation?
|
||||||
|
fun fromSourceOfTruthRepresentationToCommonRepresentation(sourceOfTruthRepresentation: SourceOfTruthRepresentation): CommonRepresentation?
|
||||||
|
|
||||||
|
class Builder<NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> {
|
||||||
|
|
||||||
|
private var fromCommonToSourceOfTruth: Converter<CommonRepresentation, SourceOfTruthRepresentation>? = null
|
||||||
|
private var fromNetworkToCommon: Converter<NetworkRepresentation, CommonRepresentation>? = null
|
||||||
|
private var fromSourceOfTruthToCommon: Converter<SourceOfTruthRepresentation, CommonRepresentation>? = null
|
||||||
|
|
||||||
|
fun build(): StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> =
|
||||||
|
RealStoreConverter(fromCommonToSourceOfTruth, fromNetworkToCommon, fromSourceOfTruthToCommon)
|
||||||
|
|
||||||
|
fun fromCommonToSourceOfTruth(converter: Converter<CommonRepresentation, SourceOfTruthRepresentation>): Builder<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
||||||
|
fromCommonToSourceOfTruth = converter
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromSourceOfTruthToCommon(converter: Converter<SourceOfTruthRepresentation, CommonRepresentation>): Builder<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
||||||
|
fromSourceOfTruthToCommon = converter
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromNetworkToCommon(converter: Converter<NetworkRepresentation, CommonRepresentation>): Builder<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
||||||
|
fromNetworkToCommon = converter
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RealStoreConverter<NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
|
||||||
|
private val fromCommonToSourceOfTruth: Converter<CommonRepresentation, SourceOfTruthRepresentation>?,
|
||||||
|
private val fromNetworkToCommon: Converter<NetworkRepresentation, CommonRepresentation>?,
|
||||||
|
private val fromSourceOfTruthToCommon: Converter<SourceOfTruthRepresentation, CommonRepresentation>?
|
||||||
|
) : StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
||||||
|
override fun fromNetworkRepresentationToCommonRepresentation(networkRepresentation: NetworkRepresentation): CommonRepresentation? =
|
||||||
|
fromNetworkToCommon?.invoke(networkRepresentation)
|
||||||
|
|
||||||
|
override fun fromCommonRepresentationToSourceOfTruthRepresentation(commonRepresentation: CommonRepresentation): SourceOfTruthRepresentation? =
|
||||||
|
fromCommonToSourceOfTruth?.invoke(commonRepresentation)
|
||||||
|
|
||||||
|
override fun fromSourceOfTruthRepresentationToCommonRepresentation(sourceOfTruthRepresentation: SourceOfTruthRepresentation): CommonRepresentation? =
|
||||||
|
fromSourceOfTruthToCommon?.invoke(sourceOfTruthRepresentation)
|
||||||
|
}
|
|
@ -1,9 +1,8 @@
|
||||||
package org.mobilenativefoundation.store.store5
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
|
||||||
@ExperimentalTime
|
|
||||||
internal object StoreDefaults {
|
internal object StoreDefaults {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,7 +10,7 @@ internal object StoreDefaults {
|
||||||
*
|
*
|
||||||
* @return memory cache TTL
|
* @return memory cache TTL
|
||||||
*/
|
*/
|
||||||
val cacheTTL: Duration = Duration.hours(24)
|
val cacheTTL: Duration = 24.hours
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache size (default is 100), can be overridden
|
* Cache size (default is 100), can be overridden
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
package org.mobilenativefoundation.store.store5
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.filterNot
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper factory that will return data for [key] if it is cached otherwise will return
|
|
||||||
* fresh/network data (updating your caches)
|
|
||||||
*/
|
|
||||||
suspend fun <Key : Any, Output : Any> Store<Key, Output>.get(key: Key) = stream(
|
|
||||||
StoreRequest.cached(key, refresh = false)
|
|
||||||
).filterNot {
|
|
||||||
it is StoreResponse.Loading || it is StoreResponse.NoNewData
|
|
||||||
}.first().requireData()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper factory that will return fresh data for [key] while updating your caches
|
|
||||||
*
|
|
||||||
* Note: If the [Fetcher] does not return any data (i.e the returned
|
|
||||||
* [kotlinx.coroutines.Flow], when collected, is empty). Then store will fall back to local
|
|
||||||
* data **even** if you explicitly requested fresh data.
|
|
||||||
* See https://github.com/dropbox/Store/pull/194 for context
|
|
||||||
*/
|
|
||||||
suspend fun <Key : Any, Output : Any> Store<Key, Output>.fresh(key: Key) = stream(
|
|
||||||
StoreRequest.fresh(key)
|
|
||||||
).filterNot {
|
|
||||||
it is StoreResponse.Loading || it is StoreResponse.NoNewData
|
|
||||||
}.first().requireData()
|
|
|
@ -23,7 +23,7 @@ package org.mobilenativefoundation.store.store5
|
||||||
* starting the stream from the local [com.dropbox.android.external.store4.impl.SourceOfTruth] and memory cache
|
* starting the stream from the local [com.dropbox.android.external.store4.impl.SourceOfTruth] and memory cache
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
data class StoreRequest<Key> private constructor(
|
data class StoreReadRequest<Key> private constructor(
|
||||||
|
|
||||||
val key: Key,
|
val key: Key,
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ data class StoreRequest<Key> private constructor(
|
||||||
* data **even** if you explicitly requested fresh data.
|
* data **even** if you explicitly requested fresh data.
|
||||||
* See https://github.com/dropbox/Store/pull/194 for context.
|
* See https://github.com/dropbox/Store/pull/194 for context.
|
||||||
*/
|
*/
|
||||||
fun <Key> fresh(key: Key) = StoreRequest(
|
fun <Key> fresh(key: Key) = StoreReadRequest(
|
||||||
key = key,
|
key = key,
|
||||||
skippedCaches = allCaches,
|
skippedCaches = allCaches,
|
||||||
refresh = true
|
refresh = true
|
||||||
|
@ -61,7 +61,7 @@ data class StoreRequest<Key> private constructor(
|
||||||
* Create a Store Request which will return data from memory/disk caches
|
* Create a Store Request which will return data from memory/disk caches
|
||||||
* @param refresh if true then return fetcher (new) data as well (updating your caches)
|
* @param refresh if true then return fetcher (new) data as well (updating your caches)
|
||||||
*/
|
*/
|
||||||
fun <Key> cached(key: Key, refresh: Boolean) = StoreRequest(
|
fun <Key> cached(key: Key, refresh: Boolean) = StoreReadRequest(
|
||||||
key = key,
|
key = key,
|
||||||
skippedCaches = 0,
|
skippedCaches = 0,
|
||||||
refresh = refresh
|
refresh = refresh
|
||||||
|
@ -71,7 +71,7 @@ data class StoreRequest<Key> private constructor(
|
||||||
* Create a Store Request which will return data from disk cache
|
* Create a Store Request which will return data from disk cache
|
||||||
* @param refresh if true then return fetcher (new) data as well (updating your caches)
|
* @param refresh if true then return fetcher (new) data as well (updating your caches)
|
||||||
*/
|
*/
|
||||||
fun <Key> skipMemory(key: Key, refresh: Boolean) = StoreRequest(
|
fun <Key> skipMemory(key: Key, refresh: Boolean) = StoreReadRequest(
|
||||||
key = key,
|
key = key,
|
||||||
skippedCaches = CacheType.MEMORY.flag,
|
skippedCaches = CacheType.MEMORY.flag,
|
||||||
refresh = refresh
|
refresh = refresh
|
|
@ -22,47 +22,48 @@ package org.mobilenativefoundation.store.store5
|
||||||
* class to represent each response. This allows the flow to keep running even if an error happens
|
* class to represent each response. This allows the flow to keep running even if an error happens
|
||||||
* so that if there is an observable single source of truth, application can keep observing it.
|
* so that if there is an observable single source of truth, application can keep observing it.
|
||||||
*/
|
*/
|
||||||
sealed class StoreResponse<out T> {
|
sealed class StoreReadResponse<out CommonRepresentation> {
|
||||||
/**
|
/**
|
||||||
* Represents the source of the Response.
|
* Represents the source of the Response.
|
||||||
*/
|
*/
|
||||||
abstract val origin: ResponseOrigin
|
abstract val origin: StoreReadResponseOrigin
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loading event dispatched by [Store] to signal the [Fetcher] is in progress.
|
* Loading event dispatched by [Store] to signal the [Fetcher] is in progress.
|
||||||
*/
|
*/
|
||||||
data class Loading(override val origin: ResponseOrigin) : StoreResponse<Nothing>()
|
data class Loading(override val origin: StoreReadResponseOrigin) : StoreReadResponse<Nothing>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data dispatched by [Store]
|
* Data dispatched by [Store]
|
||||||
*/
|
*/
|
||||||
data class Data<T>(val value: T, override val origin: ResponseOrigin) : StoreResponse<T>()
|
data class Data<CommonRepresentation>(val value: CommonRepresentation, override val origin: StoreReadResponseOrigin) :
|
||||||
|
StoreReadResponse<CommonRepresentation>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No new data event dispatched by Store to signal the [Fetcher] returned no data (i.e the
|
* No new data event dispatched by Store to signal the [Fetcher] returned no data (i.e the
|
||||||
* returned [kotlinx.coroutines.Flow], when collected, was empty).
|
* returned [kotlinx.coroutines.Flow], when collected, was empty).
|
||||||
*/
|
*/
|
||||||
data class NoNewData(override val origin: ResponseOrigin) : StoreResponse<Nothing>()
|
data class NoNewData(override val origin: StoreReadResponseOrigin) : StoreReadResponse<Nothing>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error dispatched by a pipeline
|
* Error dispatched by a pipeline
|
||||||
*/
|
*/
|
||||||
sealed class Error : StoreResponse<Nothing>() {
|
sealed class Error : StoreReadResponse<Nothing>() {
|
||||||
data class Exception(
|
data class Exception(
|
||||||
val error: Throwable,
|
val error: Throwable,
|
||||||
override val origin: ResponseOrigin
|
override val origin: StoreReadResponseOrigin
|
||||||
) : Error()
|
) : Error()
|
||||||
|
|
||||||
data class Message(
|
data class Message(
|
||||||
val message: String,
|
val message: String,
|
||||||
override val origin: ResponseOrigin
|
override val origin: StoreReadResponseOrigin
|
||||||
) : Error()
|
) : Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the available data or throws [NullPointerException] if there is no data.
|
* Returns the available data or throws [NullPointerException] if there is no data.
|
||||||
*/
|
*/
|
||||||
fun requireData(): T {
|
fun requireData(): CommonRepresentation {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is Data -> value
|
is Data -> value
|
||||||
is Error -> this.doThrow()
|
is Error -> this.doThrow()
|
||||||
|
@ -71,7 +72,7 @@ sealed class StoreResponse<out T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this [StoreResponse] is of type [StoreResponse.Error], throws the exception
|
* If this [StoreReadResponse] is of type [StoreReadResponse.Error], throws the exception
|
||||||
* Otherwise, does nothing.
|
* Otherwise, does nothing.
|
||||||
*/
|
*/
|
||||||
fun throwIfError() {
|
fun throwIfError() {
|
||||||
|
@ -81,7 +82,7 @@ sealed class StoreResponse<out T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this [StoreResponse] is of type [StoreResponse.Error], returns the available error
|
* If this [StoreReadResponse] is of type [StoreReadResponse.Error], returns the available error
|
||||||
* from it. Otherwise, returns `null`.
|
* from it. Otherwise, returns `null`.
|
||||||
*/
|
*/
|
||||||
fun errorMessageOrNull(): String? {
|
fun errorMessageOrNull(): String? {
|
||||||
|
@ -95,13 +96,13 @@ sealed class StoreResponse<out T> {
|
||||||
/**
|
/**
|
||||||
* If there is data available, returns it; otherwise returns null.
|
* If there is data available, returns it; otherwise returns null.
|
||||||
*/
|
*/
|
||||||
fun dataOrNull(): T? = when (this) {
|
fun dataOrNull(): CommonRepresentation? = when (this) {
|
||||||
is Data -> value
|
is Data -> value
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
internal fun <R> swapType(): StoreResponse<R> = when (this) {
|
internal fun <T> swapType(): StoreReadResponse<T> = when (this) {
|
||||||
is Error -> this
|
is Error -> this
|
||||||
is Loading -> this
|
is Loading -> this
|
||||||
is NoNewData -> this
|
is NoNewData -> this
|
||||||
|
@ -110,26 +111,26 @@ sealed class StoreResponse<out T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the origin for a [StoreResponse].
|
* Represents the origin for a [StoreReadResponse].
|
||||||
*/
|
*/
|
||||||
enum class ResponseOrigin {
|
enum class StoreReadResponseOrigin {
|
||||||
/**
|
/**
|
||||||
* [StoreResponse] is sent from the cache
|
* [StoreReadResponse] is sent from the cache
|
||||||
*/
|
*/
|
||||||
Cache,
|
Cache,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [StoreResponse] is sent from the persister
|
* [StoreReadResponse] is sent from the persister
|
||||||
*/
|
*/
|
||||||
SourceOfTruth,
|
SourceOfTruth,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [StoreResponse] is sent from a fetcher,
|
* [StoreReadResponse] is sent from a fetcher,
|
||||||
*/
|
*/
|
||||||
Fetcher
|
Fetcher
|
||||||
}
|
}
|
||||||
|
|
||||||
fun StoreResponse.Error.doThrow(): Nothing = when (this) {
|
fun StoreReadResponse.Error.doThrow(): Nothing = when (this) {
|
||||||
is StoreResponse.Error.Exception -> throw error
|
is StoreReadResponse.Error.Exception -> throw error
|
||||||
is StoreResponse.Error.Message -> throw RuntimeException(message)
|
is StoreReadResponse.Error.Message -> throw RuntimeException(message)
|
||||||
}
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.OnStoreWriteCompletion
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.RealStoreWriteRequest
|
||||||
|
|
||||||
|
interface StoreWriteRequest<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> {
|
||||||
|
val key: Key
|
||||||
|
val input: CommonRepresentation
|
||||||
|
val created: Long
|
||||||
|
val onCompletions: List<OnStoreWriteCompletion>?
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> of(
|
||||||
|
key: Key,
|
||||||
|
input: CommonRepresentation,
|
||||||
|
onCompletions: List<OnStoreWriteCompletion>? = null,
|
||||||
|
created: Long = Clock.System.now().toEpochMilliseconds(),
|
||||||
|
): StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse> = RealStoreWriteRequest(key, input, created, onCompletions)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
sealed class StoreWriteResponse {
|
||||||
|
sealed class Success : StoreWriteResponse() {
|
||||||
|
data class Typed<NetworkWriteResponse : Any>(val value: NetworkWriteResponse) : Success()
|
||||||
|
data class Untyped(val value: Any) : Success()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Error : StoreWriteResponse() {
|
||||||
|
data class Exception(val error: Throwable) : Error()
|
||||||
|
data class Message(val message: String) : Error()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
typealias PostRequest<Key, CommonRepresentation> = suspend (key: Key, input: CommonRepresentation) -> UpdaterResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Posts data to remote data source.
|
||||||
|
* @see [StoreWriteRequest]
|
||||||
|
*/
|
||||||
|
interface Updater<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> {
|
||||||
|
/**
|
||||||
|
* Makes HTTP POST request.
|
||||||
|
*/
|
||||||
|
suspend fun post(key: Key, input: CommonRepresentation): UpdaterResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes on network completion.
|
||||||
|
*/
|
||||||
|
val onCompletion: OnUpdaterCompletion<NetworkWriteResponse>?
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> by(
|
||||||
|
post: PostRequest<Key, CommonRepresentation>,
|
||||||
|
onCompletion: OnUpdaterCompletion<NetworkWriteResponse>? = null,
|
||||||
|
): Updater<Key, CommonRepresentation, NetworkWriteResponse> = RealNetworkUpdater(
|
||||||
|
post, onCompletion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class RealNetworkUpdater<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any>(
|
||||||
|
private val realPost: PostRequest<Key, CommonRepresentation>,
|
||||||
|
override val onCompletion: OnUpdaterCompletion<NetworkWriteResponse>?,
|
||||||
|
) : Updater<Key, CommonRepresentation, NetworkWriteResponse> {
|
||||||
|
override suspend fun post(key: Key, input: CommonRepresentation): UpdaterResult = realPost(key, input)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
sealed class UpdaterResult {
|
||||||
|
|
||||||
|
sealed class Success : UpdaterResult() {
|
||||||
|
data class Typed<NetworkWriteResponse : Any>(val value: NetworkWriteResponse) : Success()
|
||||||
|
data class Untyped(val value: Any) : Success()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Error : UpdaterResult() {
|
||||||
|
data class Exception(val error: Throwable) : Error()
|
||||||
|
data class Message(val message: String) : Error()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface Write<Key : Any, CommonRepresentation : Any> {
|
||||||
|
@ExperimentalStoreApi
|
||||||
|
suspend fun <NetworkWriteResponse : Any> write(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): StoreWriteResponse
|
||||||
|
interface Stream<Key : Any, CommonRepresentation : Any> {
|
||||||
|
@ExperimentalStoreApi
|
||||||
|
fun <NetworkWriteResponse : Any> stream(requestStream: Flow<StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>>): Flow<StoreWriteResponse>
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,9 +27,10 @@ import kotlinx.coroutines.withContext
|
||||||
import org.mobilenativefoundation.store.multicast5.Multicaster
|
import org.mobilenativefoundation.store.multicast5.Multicaster
|
||||||
import org.mobilenativefoundation.store.store5.Fetcher
|
import org.mobilenativefoundation.store.store5.Fetcher
|
||||||
import org.mobilenativefoundation.store.store5.FetcherResult
|
import org.mobilenativefoundation.store.store5.FetcherResult
|
||||||
import org.mobilenativefoundation.store.store5.ResponseOrigin
|
|
||||||
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||||
import org.mobilenativefoundation.store.store5.StoreResponse
|
import org.mobilenativefoundation.store.store5.StoreConverter
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreReadResponse
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class maintains one and only 1 fetcher for a given [Key].
|
* This class maintains one and only 1 fetcher for a given [Key].
|
||||||
|
@ -39,7 +40,7 @@ import org.mobilenativefoundation.store.store5.StoreResponse
|
||||||
* fetcher requests receives values dispatched by later requests even if they don't share the
|
* fetcher requests receives values dispatched by later requests even if they don't share the
|
||||||
* request.
|
* request.
|
||||||
*/
|
*/
|
||||||
internal class FetcherController<Key : Any, Input : Any, Output : Any>(
|
internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
|
||||||
/**
|
/**
|
||||||
* The [CoroutineScope] to use when collecting from the fetcher
|
* The [CoroutineScope] to use when collecting from the fetcher
|
||||||
*/
|
*/
|
||||||
|
@ -47,14 +48,16 @@ internal class FetcherController<Key : Any, Input : Any, Output : Any>(
|
||||||
/**
|
/**
|
||||||
* The function that provides the actualy fetcher flow when needed
|
* The function that provides the actualy fetcher flow when needed
|
||||||
*/
|
*/
|
||||||
private val realFetcher: Fetcher<Key, Input>,
|
private val realFetcher: Fetcher<Key, NetworkRepresentation>,
|
||||||
/**
|
/**
|
||||||
* [SourceOfTruth] to send the data each time fetcher dispatches a value. Can be `null` if
|
* [SourceOfTruth] to send the data each time fetcher dispatches a value. Can be `null` if
|
||||||
* no [SourceOfTruth] is available.
|
* no [SourceOfTruth] is available.
|
||||||
*/
|
*/
|
||||||
private val sourceOfTruth: SourceOfTruthWithBarrier<Key, Input, Output>?,
|
private val sourceOfTruth: SourceOfTruthWithBarrier<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>?,
|
||||||
|
|
||||||
|
private val converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null
|
||||||
) {
|
) {
|
||||||
@Suppress("USELESS_CAST") // needed for multicaster source
|
@Suppress("USELESS_CAST", "UNCHECKED_CAST") // needed for multicaster source
|
||||||
private val fetchers = RefCountedResource(
|
private val fetchers = RefCountedResource(
|
||||||
create = { key: Key ->
|
create = { key: Key ->
|
||||||
Multicaster(
|
Multicaster(
|
||||||
|
@ -62,23 +65,25 @@ internal class FetcherController<Key : Any, Input : Any, Output : Any>(
|
||||||
bufferSize = 0,
|
bufferSize = 0,
|
||||||
source = flow { emitAll(realFetcher(key)) }.map {
|
source = flow { emitAll(realFetcher(key)) }.map {
|
||||||
when (it) {
|
when (it) {
|
||||||
is FetcherResult.Data -> StoreResponse.Data(
|
is FetcherResult.Data -> {
|
||||||
|
StoreReadResponse.Data(
|
||||||
it.value,
|
it.value,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
) as StoreResponse<Input>
|
) as StoreReadResponse<NetworkRepresentation>
|
||||||
|
}
|
||||||
|
|
||||||
is FetcherResult.Error.Message -> StoreResponse.Error.Message(
|
is FetcherResult.Error.Message -> StoreReadResponse.Error.Message(
|
||||||
it.message,
|
it.message,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
|
|
||||||
is FetcherResult.Error.Exception -> StoreResponse.Error.Exception(
|
is FetcherResult.Error.Exception -> StoreReadResponse.Error.Exception(
|
||||||
it.error,
|
it.error,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.onEmpty {
|
}.onEmpty {
|
||||||
emit(StoreResponse.NoNewData(ResponseOrigin.Fetcher))
|
emit(StoreReadResponse.NoNewData(StoreReadResponseOrigin.Fetcher))
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* When enabled, downstream collectors are never closed, instead, they are kept active to
|
* When enabled, downstream collectors are never closed, instead, they are kept active to
|
||||||
|
@ -87,18 +92,22 @@ internal class FetcherController<Key : Any, Input : Any, Output : Any>(
|
||||||
*/
|
*/
|
||||||
piggybackingDownstream = true,
|
piggybackingDownstream = true,
|
||||||
onEach = { response ->
|
onEach = { response ->
|
||||||
response.dataOrNull()?.let { input ->
|
response.dataOrNull()?.let { networkRepresentation ->
|
||||||
|
val input =
|
||||||
|
networkRepresentation as? CommonRepresentation ?: converter?.fromNetworkRepresentationToCommonRepresentation(networkRepresentation)
|
||||||
|
if (input != null) {
|
||||||
sourceOfTruth?.write(key, input)
|
sourceOfTruth?.write(key, input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onRelease = { _: Key, multicaster: Multicaster<StoreResponse<Input>> ->
|
onRelease = { _: Key, multicaster: Multicaster<StoreReadResponse<NetworkRepresentation>> ->
|
||||||
multicaster.close()
|
multicaster.close()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getFetcher(key: Key, piggybackOnly: Boolean = false): Flow<StoreResponse<Input>> {
|
fun getFetcher(key: Key, piggybackOnly: Boolean = false): Flow<StoreReadResponse<NetworkRepresentation>> {
|
||||||
return flow {
|
return flow {
|
||||||
val fetcher = acquireFetcher(key)
|
val fetcher = acquireFetcher(key)
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.impl
|
||||||
|
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreWriteResponse
|
||||||
|
|
||||||
|
data class OnStoreWriteCompletion(
|
||||||
|
val onSuccess: (StoreWriteResponse.Success) -> Unit,
|
||||||
|
val onFailure: (StoreWriteResponse.Error) -> Unit
|
||||||
|
)
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.impl
|
||||||
|
|
||||||
|
import org.mobilenativefoundation.store.store5.Bookkeeper
|
||||||
|
import org.mobilenativefoundation.store.store5.internal.definition.Timestamp
|
||||||
|
|
||||||
|
internal class RealBookkeeper<Key : Any>(
|
||||||
|
private val realGetLastFailedSync: suspend (key: Key) -> Timestamp?,
|
||||||
|
private val realSetLastFailedSync: suspend (key: Key, timestamp: Timestamp) -> Boolean,
|
||||||
|
private val realClear: suspend (key: Key) -> Boolean,
|
||||||
|
private val realClearAll: suspend () -> Boolean
|
||||||
|
) : Bookkeeper<Key> {
|
||||||
|
override suspend fun getLastFailedSync(key: Key): Long? = realGetLastFailedSync(key)
|
||||||
|
|
||||||
|
override suspend fun setLastFailedSync(key: Key, timestamp: Long): Boolean = realSetLastFailedSync(key, timestamp)
|
||||||
|
|
||||||
|
override suspend fun clear(key: Key): Boolean = realClear(key)
|
||||||
|
|
||||||
|
override suspend fun clearAll(): Boolean = realClearAll()
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.impl
|
||||||
|
|
||||||
|
import org.mobilenativefoundation.store.store5.ItemValidator
|
||||||
|
|
||||||
|
internal class RealItemValidator<CommonRepresentation : Any>(
|
||||||
|
private val realValidator: suspend (item: CommonRepresentation) -> Boolean
|
||||||
|
) : ItemValidator<CommonRepresentation> {
|
||||||
|
override suspend fun isValid(item: CommonRepresentation): Boolean = realValidator(item)
|
||||||
|
}
|
|
@ -0,0 +1,266 @@
|
||||||
|
@file:Suppress("UNCHECKED_CAST")
|
||||||
|
|
||||||
|
package org.mobilenativefoundation.store.store5.impl
|
||||||
|
|
||||||
|
import co.touchlab.kermit.CommonWriter
|
||||||
|
import co.touchlab.kermit.Logger
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import org.mobilenativefoundation.store.store5.Bookkeeper
|
||||||
|
import org.mobilenativefoundation.store.store5.Clear
|
||||||
|
import org.mobilenativefoundation.store.store5.ExperimentalStoreApi
|
||||||
|
import org.mobilenativefoundation.store.store5.MutableStore
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreReadRequest
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreReadResponse
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreWriteRequest
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreWriteResponse
|
||||||
|
import org.mobilenativefoundation.store.store5.Updater
|
||||||
|
import org.mobilenativefoundation.store.store5.UpdaterResult
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.extensions.now
|
||||||
|
import org.mobilenativefoundation.store.store5.internal.concurrent.AnyThread
|
||||||
|
import org.mobilenativefoundation.store.store5.internal.concurrent.ThreadSafety
|
||||||
|
import org.mobilenativefoundation.store.store5.internal.definition.WriteRequestQueue
|
||||||
|
import org.mobilenativefoundation.store.store5.internal.result.EagerConflictResolutionResult
|
||||||
|
|
||||||
|
internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
|
||||||
|
private val delegate: RealStore<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>,
|
||||||
|
private val updater: Updater<Key, CommonRepresentation, *>,
|
||||||
|
private val bookkeeper: Bookkeeper<Key>,
|
||||||
|
) : MutableStore<Key, CommonRepresentation>, Clear.Key<Key> by delegate, Clear.All by delegate {
|
||||||
|
|
||||||
|
private val storeLock = Mutex()
|
||||||
|
private val keyToWriteRequestQueue = mutableMapOf<Key, WriteRequestQueue<Key, CommonRepresentation, *>>()
|
||||||
|
private val keyToThreadSafety = mutableMapOf<Key, ThreadSafety>()
|
||||||
|
|
||||||
|
override fun <NetworkWriteResponse : Any> stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>> =
|
||||||
|
flow {
|
||||||
|
safeInitStore(request.key)
|
||||||
|
|
||||||
|
when (val eagerConflictResolutionResult = tryEagerlyResolveConflicts<NetworkWriteResponse>(request.key)) {
|
||||||
|
is EagerConflictResolutionResult.Error.Exception -> {
|
||||||
|
logger.e(eagerConflictResolutionResult.error.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
is EagerConflictResolutionResult.Error.Message -> {
|
||||||
|
logger.e(eagerConflictResolutionResult.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
is EagerConflictResolutionResult.Success.ConflictsResolved -> {
|
||||||
|
logger.d(eagerConflictResolutionResult.value.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
EagerConflictResolutionResult.Success.NoConflicts -> {
|
||||||
|
logger.d(eagerConflictResolutionResult.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate.stream(request).collect { storeReadResponse -> emit(storeReadResponse) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalStoreApi
|
||||||
|
override fun <NetworkWriteResponse : Any> stream(requestStream: Flow<StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>>): Flow<StoreWriteResponse> =
|
||||||
|
flow {
|
||||||
|
requestStream
|
||||||
|
.onEach { writeRequest ->
|
||||||
|
safeInitStore(writeRequest.key)
|
||||||
|
addWriteRequestToQueue(writeRequest)
|
||||||
|
}
|
||||||
|
.collect { writeRequest ->
|
||||||
|
val storeWriteResponse = try {
|
||||||
|
delegate.write(writeRequest.key, writeRequest.input)
|
||||||
|
when (val updaterResult = tryUpdateServer(writeRequest)) {
|
||||||
|
is UpdaterResult.Error.Exception -> StoreWriteResponse.Error.Exception(updaterResult.error)
|
||||||
|
is UpdaterResult.Error.Message -> StoreWriteResponse.Error.Message(updaterResult.message)
|
||||||
|
is UpdaterResult.Success.Typed<*> -> {
|
||||||
|
val typedValue = updaterResult.value as? NetworkWriteResponse
|
||||||
|
if (typedValue == null) {
|
||||||
|
StoreWriteResponse.Success.Untyped(updaterResult.value)
|
||||||
|
} else {
|
||||||
|
StoreWriteResponse.Success.Typed(updaterResult.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is UpdaterResult.Success.Untyped -> StoreWriteResponse.Success.Untyped(updaterResult.value)
|
||||||
|
}
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
StoreWriteResponse.Error.Exception(throwable)
|
||||||
|
}
|
||||||
|
emit(storeWriteResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalStoreApi
|
||||||
|
override suspend fun <NetworkWriteResponse : Any> write(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): StoreWriteResponse =
|
||||||
|
stream(flowOf(request)).first()
|
||||||
|
|
||||||
|
private suspend fun <NetworkWriteResponse : Any> tryUpdateServer(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): UpdaterResult {
|
||||||
|
val updaterResult = postLatest<NetworkWriteResponse>(request.key)
|
||||||
|
|
||||||
|
if (updaterResult is UpdaterResult.Success) {
|
||||||
|
updateWriteRequestQueue<NetworkWriteResponse>(
|
||||||
|
key = request.key,
|
||||||
|
created = request.created,
|
||||||
|
updaterResult = updaterResult
|
||||||
|
)
|
||||||
|
bookkeeper.clear(request.key)
|
||||||
|
} else {
|
||||||
|
bookkeeper.setLastFailedSync(request.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updaterResult
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <NetworkWriteResponse : Any> postLatest(key: Key): UpdaterResult {
|
||||||
|
val writer = getLatestWriteRequest(key)
|
||||||
|
return when (val updaterResult = updater.post(key, writer.input)) {
|
||||||
|
is UpdaterResult.Error.Exception -> UpdaterResult.Error.Exception(updaterResult.error)
|
||||||
|
is UpdaterResult.Error.Message -> UpdaterResult.Error.Message(updaterResult.message)
|
||||||
|
is UpdaterResult.Success.Untyped -> UpdaterResult.Success.Untyped(updaterResult.value)
|
||||||
|
is UpdaterResult.Success.Typed<*> -> {
|
||||||
|
val typedValue = updaterResult.value as? NetworkWriteResponse
|
||||||
|
if (typedValue == null) {
|
||||||
|
UpdaterResult.Success.Untyped(updaterResult.value)
|
||||||
|
} else {
|
||||||
|
UpdaterResult.Success.Typed(updaterResult.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
private suspend fun <NetworkWriteResponse : Any> updateWriteRequestQueue(key: Key, created: Long, updaterResult: UpdaterResult.Success) {
|
||||||
|
val nextWriteRequestQueue = withWriteRequestQueueLock<ArrayDeque<StoreWriteRequest<Key, CommonRepresentation, *>>, NetworkWriteResponse>(key) {
|
||||||
|
val outstandingWriteRequests = ArrayDeque<StoreWriteRequest<Key, CommonRepresentation, *>>()
|
||||||
|
|
||||||
|
for (writeRequest in this) {
|
||||||
|
if (writeRequest.created <= created) {
|
||||||
|
updater.onCompletion?.onSuccess?.invoke(updaterResult)
|
||||||
|
|
||||||
|
val storeWriteResponse = when (updaterResult) {
|
||||||
|
is UpdaterResult.Success.Typed<*> -> {
|
||||||
|
val typedValue = updaterResult.value as? NetworkWriteResponse
|
||||||
|
if (typedValue == null) {
|
||||||
|
StoreWriteResponse.Success.Untyped(updaterResult.value)
|
||||||
|
} else {
|
||||||
|
StoreWriteResponse.Success.Typed(updaterResult.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is UpdaterResult.Success.Untyped -> StoreWriteResponse.Success.Untyped(updaterResult.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeRequest.onCompletions?.forEach { onStoreWriteCompletion ->
|
||||||
|
onStoreWriteCompletion.onSuccess(storeWriteResponse)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outstandingWriteRequests.add(writeRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outstandingWriteRequests
|
||||||
|
}
|
||||||
|
|
||||||
|
withThreadSafety(key) {
|
||||||
|
keyToWriteRequestQueue[key] = nextWriteRequestQueue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
private suspend fun <Output : Any, NetworkWriteResponse : Any> withWriteRequestQueueLock(
|
||||||
|
key: Key,
|
||||||
|
block: suspend WriteRequestQueue<Key, CommonRepresentation, *>.() -> Output
|
||||||
|
): Output =
|
||||||
|
withThreadSafety(key) {
|
||||||
|
writeRequests.lightswitch.lock(writeRequests.mutex)
|
||||||
|
val writeRequestQueue = requireNotNull(keyToWriteRequestQueue[key])
|
||||||
|
val output = writeRequestQueue.block()
|
||||||
|
writeRequests.lightswitch.unlock(writeRequests.mutex)
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getLatestWriteRequest(key: Key): StoreWriteRequest<Key, CommonRepresentation, *> = withThreadSafety(key) {
|
||||||
|
writeRequests.mutex.lock()
|
||||||
|
val output = requireNotNull(keyToWriteRequestQueue[key]?.last())
|
||||||
|
writeRequests.mutex.unlock()
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
private suspend fun <Output : Any?> withThreadSafety(key: Key, block: suspend ThreadSafety.() -> Output): Output {
|
||||||
|
storeLock.lock()
|
||||||
|
val threadSafety = requireNotNull(keyToThreadSafety[key])
|
||||||
|
val output = threadSafety.block()
|
||||||
|
storeLock.unlock()
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun conflictsMightExist(key: Key): Boolean {
|
||||||
|
val lastFailedSync = bookkeeper.getLastFailedSync(key)
|
||||||
|
return lastFailedSync != null || writeRequestsQueueIsEmpty(key).not()
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
private suspend fun writeRequestsQueueIsEmpty(key: Key): Boolean = withThreadSafety(key) {
|
||||||
|
keyToWriteRequestQueue[key].isNullOrEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <NetworkWriteResponse : Any> addWriteRequestToQueue(writeRequest: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>) =
|
||||||
|
withWriteRequestQueueLock<Unit, NetworkWriteResponse>(writeRequest.key) {
|
||||||
|
add(writeRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AnyThread
|
||||||
|
private suspend fun <NetworkWriteResponse : Any> tryEagerlyResolveConflicts(key: Key): EagerConflictResolutionResult<NetworkWriteResponse> =
|
||||||
|
withThreadSafety(key) {
|
||||||
|
val latest = delegate.latestOrNull(key)
|
||||||
|
when {
|
||||||
|
latest == null || conflictsMightExist(key).not() -> EagerConflictResolutionResult.Success.NoConflicts
|
||||||
|
else -> {
|
||||||
|
try {
|
||||||
|
val updaterResult = updater.post(key, latest).also { updaterResult ->
|
||||||
|
if (updaterResult is UpdaterResult.Success) {
|
||||||
|
updateWriteRequestQueue<NetworkWriteResponse>(key = key, created = now(), updaterResult = updaterResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (updaterResult) {
|
||||||
|
is UpdaterResult.Error.Exception -> EagerConflictResolutionResult.Error.Exception(updaterResult.error)
|
||||||
|
is UpdaterResult.Error.Message -> EagerConflictResolutionResult.Error.Message(updaterResult.message)
|
||||||
|
is UpdaterResult.Success -> EagerConflictResolutionResult.Success.ConflictsResolved(updaterResult)
|
||||||
|
}
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
EagerConflictResolutionResult.Error.Exception(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun safeInitWriteRequestQueue(key: Key) = withThreadSafety(key) {
|
||||||
|
if (keyToWriteRequestQueue[key] == null) {
|
||||||
|
keyToWriteRequestQueue[key] = ArrayDeque()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun safeInitThreadSafety(key: Key) = storeLock.withLock {
|
||||||
|
if (keyToThreadSafety[key] == null) {
|
||||||
|
keyToThreadSafety[key] = ThreadSafety()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun safeInitStore(key: Key) {
|
||||||
|
safeInitThreadSafety(key)
|
||||||
|
safeInitWriteRequestQueue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val logger = Logger.apply {
|
||||||
|
setLogWriters(listOf(CommonWriter()))
|
||||||
|
setTag("Store")
|
||||||
|
}
|
||||||
|
private const val UNKNOWN_ERROR = "Unknown error occurred"
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,16 +19,16 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||||
|
|
||||||
internal class PersistentSourceOfTruth<Key, Input, Output>(
|
internal class PersistentSourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any>(
|
||||||
private val realReader: (Key) -> Flow<Output?>,
|
private val realReader: (Key) -> Flow<SourceOfTruthRepresentation?>,
|
||||||
private val realWriter: suspend (Key, Input) -> Unit,
|
private val realWriter: suspend (Key, SourceOfTruthRepresentation) -> Unit,
|
||||||
private val realDelete: (suspend (Key) -> Unit)? = null,
|
private val realDelete: (suspend (Key) -> Unit)? = null,
|
||||||
private val realDeleteAll: (suspend () -> Unit)? = null
|
private val realDeleteAll: (suspend () -> Unit)? = null
|
||||||
) : SourceOfTruth<Key, Input, Output> {
|
) : SourceOfTruth<Key, SourceOfTruthRepresentation> {
|
||||||
|
|
||||||
override fun reader(key: Key): Flow<Output?> = realReader(key)
|
override fun reader(key: Key): Flow<SourceOfTruthRepresentation?> = realReader.invoke(key)
|
||||||
|
|
||||||
override suspend fun write(key: Key, value: Input) = realWriter(key, value)
|
override suspend fun write(key: Key, value: SourceOfTruthRepresentation) = realWriter(key, value)
|
||||||
|
|
||||||
override suspend fun delete(key: Key) {
|
override suspend fun delete(key: Key) {
|
||||||
realDelete?.invoke(key)
|
realDelete?.invoke(key)
|
||||||
|
@ -39,19 +39,22 @@ internal class PersistentSourceOfTruth<Key, Input, Output>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class PersistentNonFlowingSourceOfTruth<Key, Input, Output>(
|
internal class PersistentNonFlowingSourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any>(
|
||||||
private val realReader: suspend (Key) -> Output?,
|
private val realReader: suspend (Key) -> SourceOfTruthRepresentation?,
|
||||||
private val realWriter: suspend (Key, Input) -> Unit,
|
private val realWriter: suspend (Key, SourceOfTruthRepresentation) -> Unit,
|
||||||
private val realDelete: (suspend (Key) -> Unit)? = null,
|
private val realDelete: (suspend (Key) -> Unit)? = null,
|
||||||
private val realDeleteAll: (suspend () -> Unit)?
|
private val realDeleteAll: (suspend () -> Unit)?
|
||||||
) : SourceOfTruth<Key, Input, Output> {
|
) : SourceOfTruth<Key, SourceOfTruthRepresentation> {
|
||||||
|
|
||||||
override fun reader(key: Key): Flow<Output?> =
|
override fun reader(key: Key): Flow<SourceOfTruthRepresentation?> =
|
||||||
flow {
|
flow {
|
||||||
emit(realReader(key))
|
val sourceOfTruthRepresentation = realReader(key)
|
||||||
|
emit(sourceOfTruthRepresentation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun write(key: Key, value: Input) = realWriter(key, value)
|
override suspend fun write(key: Key, value: SourceOfTruthRepresentation) {
|
||||||
|
return realWriter(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun delete(key: Key) {
|
override suspend fun delete(key: Key) {
|
||||||
realDelete?.invoke(key)
|
realDelete?.invoke(key)
|
||||||
|
|
|
@ -19,7 +19,9 @@ import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.emitAll
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.transform
|
import kotlinx.coroutines.flow.transform
|
||||||
|
@ -28,36 +30,37 @@ import org.mobilenativefoundation.store.store5.CacheType
|
||||||
import org.mobilenativefoundation.store.store5.ExperimentalStoreApi
|
import org.mobilenativefoundation.store.store5.ExperimentalStoreApi
|
||||||
import org.mobilenativefoundation.store.store5.Fetcher
|
import org.mobilenativefoundation.store.store5.Fetcher
|
||||||
import org.mobilenativefoundation.store.store5.MemoryPolicy
|
import org.mobilenativefoundation.store.store5.MemoryPolicy
|
||||||
import org.mobilenativefoundation.store.store5.ResponseOrigin
|
|
||||||
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||||
import org.mobilenativefoundation.store.store5.Store
|
import org.mobilenativefoundation.store.store5.Store
|
||||||
import org.mobilenativefoundation.store.store5.StoreRequest
|
import org.mobilenativefoundation.store.store5.StoreConverter
|
||||||
import org.mobilenativefoundation.store.store5.StoreResponse
|
import org.mobilenativefoundation.store.store5.StoreReadRequest
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreReadResponse
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
|
||||||
import org.mobilenativefoundation.store.store5.impl.operators.Either
|
import org.mobilenativefoundation.store.store5.impl.operators.Either
|
||||||
import org.mobilenativefoundation.store.store5.impl.operators.merge
|
import org.mobilenativefoundation.store.store5.impl.operators.merge
|
||||||
import kotlin.time.ExperimentalTime
|
import org.mobilenativefoundation.store.store5.internal.result.StoreDelegateWriteResult
|
||||||
|
|
||||||
@ExperimentalTime
|
internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
|
||||||
internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
fetcher: Fetcher<Key, Input>,
|
fetcher: Fetcher<Key, NetworkRepresentation>,
|
||||||
sourceOfTruth: SourceOfTruth<Key, Input, Output>? = null,
|
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>? = null,
|
||||||
private val memoryPolicy: MemoryPolicy<Key, Output>?
|
converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null,
|
||||||
) : Store<Key, Output> {
|
private val memoryPolicy: MemoryPolicy<Key, CommonRepresentation>?
|
||||||
|
) : Store<Key, CommonRepresentation> {
|
||||||
/**
|
/**
|
||||||
* This source of truth is either a real database or an in memory source of truth created by
|
* This source of truth is either a real database or an in memory source of truth created by
|
||||||
* the builder.
|
* the builder.
|
||||||
* Whatever is given, we always put a [SourceOfTruthWithBarrier] in front of it so that while
|
* Whatever is given, we always put a [SourceOfTruthWithBarrier] in front of it so that while
|
||||||
* we write the value from fetcher into the disk, we can block reads to avoid sending new data
|
* we write the value from fetcher into the disk, we can block reads to avoid sending new data
|
||||||
* as if it came from the server (the [StoreResponse.origin] field).
|
* as if it came from the server (the [StoreReadResponse.origin] field).
|
||||||
*/
|
*/
|
||||||
private val sourceOfTruth: SourceOfTruthWithBarrier<Key, Input, Output>? =
|
private val sourceOfTruth: SourceOfTruthWithBarrier<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? =
|
||||||
sourceOfTruth?.let {
|
sourceOfTruth?.let {
|
||||||
SourceOfTruthWithBarrier(it)
|
SourceOfTruthWithBarrier(it, converter)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val memCache = memoryPolicy?.let {
|
private val memCache = memoryPolicy?.let {
|
||||||
CacheBuilder<Key, Output>().apply {
|
CacheBuilder<Key, CommonRepresentation>().apply {
|
||||||
if (memoryPolicy.hasAccessPolicy) {
|
if (memoryPolicy.hasAccessPolicy) {
|
||||||
expireAfterAccess(memoryPolicy.expireAfterAccess)
|
expireAfterAccess(memoryPolicy.expireAfterAccess)
|
||||||
}
|
}
|
||||||
|
@ -81,10 +84,11 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
||||||
private val fetcherController = FetcherController(
|
private val fetcherController = FetcherController(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
realFetcher = fetcher,
|
realFetcher = fetcher,
|
||||||
sourceOfTruth = this.sourceOfTruth
|
sourceOfTruth = this.sourceOfTruth,
|
||||||
|
converter = converter
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun stream(request: StoreRequest<Key>): Flow<StoreResponse<Output>> =
|
override fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>> =
|
||||||
flow {
|
flow {
|
||||||
val cachedToEmit = if (request.shouldSkipCache(CacheType.MEMORY)) {
|
val cachedToEmit = if (request.shouldSkipCache(CacheType.MEMORY)) {
|
||||||
null
|
null
|
||||||
|
@ -94,7 +98,7 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
||||||
|
|
||||||
cachedToEmit?.let {
|
cachedToEmit?.let {
|
||||||
// if we read a value from cache, dispatch it first
|
// if we read a value from cache, dispatch it first
|
||||||
emit(StoreResponse.Data(value = it, origin = ResponseOrigin.Cache))
|
emit(StoreReadResponse.Data(value = it, origin = StoreReadResponseOrigin.Cache))
|
||||||
}
|
}
|
||||||
val stream = if (sourceOfTruth == null) {
|
val stream = if (sourceOfTruth == null) {
|
||||||
// piggypack only if not specified fresh data AND we emitted a value from the cache
|
// piggypack only if not specified fresh data AND we emitted a value from the cache
|
||||||
|
@ -105,14 +109,14 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
||||||
request = request,
|
request = request,
|
||||||
networkLock = null,
|
networkLock = null,
|
||||||
piggybackOnly = piggybackOnly
|
piggybackOnly = piggybackOnly
|
||||||
) as Flow<StoreResponse<Output>> // when no source of truth Input == Output
|
) as Flow<StoreReadResponse<CommonRepresentation>> // when no source of truth Input == Output
|
||||||
} else {
|
} else {
|
||||||
diskNetworkCombined(request, sourceOfTruth)
|
diskNetworkCombined(request, sourceOfTruth)
|
||||||
}
|
}
|
||||||
emitAll(
|
emitAll(
|
||||||
stream.transform {
|
stream.transform {
|
||||||
emit(it)
|
emit(it)
|
||||||
if (it is StoreResponse.NoNewData && cachedToEmit == null) {
|
if (it is StoreReadResponse.NoNewData && cachedToEmit == null) {
|
||||||
// In the special case where fetcher returned no new data we actually want to
|
// 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)
|
// serve cache data (even if the request specified skipping cache and/or SoT)
|
||||||
//
|
//
|
||||||
|
@ -130,14 +134,14 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
||||||
// Source of truth
|
// Source of truth
|
||||||
// (future Source of truth updates)
|
// (future Source of truth updates)
|
||||||
memCache?.getIfPresent(request.key)?.let {
|
memCache?.getIfPresent(request.key)?.let {
|
||||||
emit(StoreResponse.Data(value = it, origin = ResponseOrigin.Cache))
|
emit(StoreReadResponse.Data(value = it, origin = StoreReadResponseOrigin.Cache))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}.onEach {
|
}.onEach {
|
||||||
// whenever a value is dispatched, save it to the memory cache
|
// whenever a value is dispatched, save it to the memory cache
|
||||||
if (it.origin != ResponseOrigin.Cache) {
|
if (it.origin != StoreReadResponseOrigin.Cache) {
|
||||||
it.dataOrNull()?.let { data ->
|
it.dataOrNull()?.let { data ->
|
||||||
memCache?.put(request.key, data)
|
memCache?.put(request.key, data)
|
||||||
}
|
}
|
||||||
|
@ -150,7 +154,7 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalStoreApi
|
@ExperimentalStoreApi
|
||||||
override suspend fun clearAll() {
|
override suspend fun clear() {
|
||||||
memCache?.invalidateAll()
|
memCache?.invalidateAll()
|
||||||
sourceOfTruth?.deleteAll()
|
sourceOfTruth?.deleteAll()
|
||||||
}
|
}
|
||||||
|
@ -176,13 +180,13 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
||||||
*
|
*
|
||||||
* 2) Request does not want to skip disk cache:
|
* 2) Request does not want to skip disk cache:
|
||||||
* In this case, we first start the disk flow. If disk flow returns `null` or
|
* In this case, we first start the disk flow. If disk flow returns `null` or
|
||||||
* [StoreRequest.refresh] is set to `true`, we enable the fetcher flow.
|
* [StoreReadRequest.refresh] is set to `true`, we enable the fetcher flow.
|
||||||
* This ensures we first get the value from disk and then load from server if necessary.
|
* This ensures we first get the value from disk and then load from server if necessary.
|
||||||
*/
|
*/
|
||||||
private fun diskNetworkCombined(
|
private fun diskNetworkCombined(
|
||||||
request: StoreRequest<Key>,
|
request: StoreReadRequest<Key>,
|
||||||
sourceOfTruth: SourceOfTruthWithBarrier<Key, Input, Output>
|
sourceOfTruth: SourceOfTruthWithBarrier<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
|
||||||
): Flow<StoreResponse<Output>> {
|
): Flow<StoreReadResponse<CommonRepresentation>> {
|
||||||
val diskLock = CompletableDeferred<Unit>()
|
val diskLock = CompletableDeferred<Unit>()
|
||||||
val networkLock = CompletableDeferred<Unit>()
|
val networkLock = CompletableDeferred<Unit>()
|
||||||
val networkFlow = createNetworkFlow(request, networkLock)
|
val networkFlow = createNetworkFlow(request, networkLock)
|
||||||
|
@ -204,7 +208,7 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
||||||
when (it) {
|
when (it) {
|
||||||
is Either.Left -> {
|
is Either.Left -> {
|
||||||
// left, that is data from network
|
// left, that is data from network
|
||||||
if (it.value is StoreResponse.Data || it.value is StoreResponse.NoNewData) {
|
if (it.value is StoreReadResponse.Data || it.value is StoreReadResponse.NoNewData) {
|
||||||
// Unlocking disk only if network sent data or reported no new data
|
// Unlocking disk only if network sent data or reported no new data
|
||||||
// so that fresh data request never receives new fetcher data after
|
// so that fresh data request never receives new fetcher data after
|
||||||
// cached disk data.
|
// cached disk data.
|
||||||
|
@ -213,19 +217,19 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
||||||
diskLock.complete(Unit)
|
diskLock.complete(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (it.value !is StoreResponse.Data) {
|
if (it.value !is StoreReadResponse.Data) {
|
||||||
emit(it.value.swapType<Output>())
|
emit(it.value.swapType())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Either.Right -> {
|
is Either.Right -> {
|
||||||
// right, that is data from disk
|
// right, that is data from disk
|
||||||
when (val diskData = it.value) {
|
when (val diskData = it.value) {
|
||||||
is StoreResponse.Data -> {
|
is StoreReadResponse.Data -> {
|
||||||
val diskValue = diskData.value
|
val diskValue = diskData.value
|
||||||
if (diskValue != null) {
|
if (diskValue != null) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
emit(diskData as StoreResponse<Output>)
|
emit(diskData as StoreReadResponse<CommonRepresentation>)
|
||||||
}
|
}
|
||||||
// If the disk value is null or refresh was requested then allow fetcher
|
// If the disk value is null or refresh was requested then allow fetcher
|
||||||
// to start emitting values.
|
// to start emitting values.
|
||||||
|
@ -234,7 +238,7 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is StoreResponse.Error -> {
|
is StoreReadResponse.Error -> {
|
||||||
// disk sent an error, send it down as well
|
// disk sent an error, send it down as well
|
||||||
emit(diskData)
|
emit(diskData)
|
||||||
|
|
||||||
|
@ -242,7 +246,7 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
||||||
// values since there is nothing to read from disk. If disk sent a write
|
// values since there is nothing to read from disk. If disk sent a write
|
||||||
// error, we should NOT allow fetcher to start emitting values as we
|
// error, we should NOT allow fetcher to start emitting values as we
|
||||||
// should always wait for the read attempt.
|
// should always wait for the read attempt.
|
||||||
if (diskData is StoreResponse.Error.Exception &&
|
if (diskData is StoreReadResponse.Error.Exception &&
|
||||||
diskData.error is SourceOfTruth.ReadException
|
diskData.error is SourceOfTruth.ReadException
|
||||||
) {
|
) {
|
||||||
networkLock.complete(Unit)
|
networkLock.complete(Unit)
|
||||||
|
@ -250,8 +254,8 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
||||||
// for other errors, don't do anything, wait for the read attempt
|
// for other errors, don't do anything, wait for the read attempt
|
||||||
}
|
}
|
||||||
|
|
||||||
is StoreResponse.Loading,
|
is StoreReadResponse.Loading,
|
||||||
is StoreResponse.NoNewData -> {
|
is StoreReadResponse.NoNewData -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -260,18 +264,30 @@ internal class RealStore<Key : Any, Input : Any, Output : Any>(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNetworkFlow(
|
private fun createNetworkFlow(
|
||||||
request: StoreRequest<Key>,
|
request: StoreReadRequest<Key>,
|
||||||
networkLock: CompletableDeferred<Unit>?,
|
networkLock: CompletableDeferred<Unit>?,
|
||||||
piggybackOnly: Boolean = false
|
piggybackOnly: Boolean = false
|
||||||
): Flow<StoreResponse<Input>> {
|
): Flow<StoreReadResponse<NetworkRepresentation>> {
|
||||||
return fetcherController
|
return fetcherController
|
||||||
.getFetcher(request.key, piggybackOnly)
|
.getFetcher(request.key, piggybackOnly)
|
||||||
.onStart {
|
.onStart {
|
||||||
// wait until disk gives us the go
|
// wait until disk gives us the go
|
||||||
networkLock?.await()
|
networkLock?.await()
|
||||||
if (!piggybackOnly) {
|
if (!piggybackOnly) {
|
||||||
emit(StoreResponse.Loading(origin = ResponseOrigin.Fetcher))
|
emit(StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal suspend fun write(key: Key, input: CommonRepresentation): StoreDelegateWriteResult = try {
|
||||||
|
memCache?.put(key, input)
|
||||||
|
sourceOfTruth?.write(key, input)
|
||||||
|
StoreDelegateWriteResult.Success
|
||||||
|
} catch (error: Throwable) {
|
||||||
|
StoreDelegateWriteResult.Error.Exception(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal suspend fun latestOrNull(key: Key): CommonRepresentation? = fromMemCache(key) ?: fromSourceOfTruth(key)
|
||||||
|
private suspend fun fromSourceOfTruth(key: Key) = sourceOfTruth?.reader(key, CompletableDeferred(Unit))?.map { it.dataOrNull() }?.first()
|
||||||
|
private fun fromMemCache(key: Key) = memCache?.getIfPresent(key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,44 +2,70 @@ package org.mobilenativefoundation.store.store5.impl
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import org.mobilenativefoundation.store.store5.Bookkeeper
|
||||||
import org.mobilenativefoundation.store.store5.Fetcher
|
import org.mobilenativefoundation.store.store5.Fetcher
|
||||||
import org.mobilenativefoundation.store.store5.MemoryPolicy
|
import org.mobilenativefoundation.store.store5.MemoryPolicy
|
||||||
|
import org.mobilenativefoundation.store.store5.MutableStore
|
||||||
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||||
import org.mobilenativefoundation.store.store5.Store
|
import org.mobilenativefoundation.store.store5.Store
|
||||||
import org.mobilenativefoundation.store.store5.StoreBuilder
|
import org.mobilenativefoundation.store.store5.StoreBuilder
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreConverter
|
||||||
import org.mobilenativefoundation.store.store5.StoreDefaults
|
import org.mobilenativefoundation.store.store5.StoreDefaults
|
||||||
import kotlin.time.ExperimentalTime
|
import org.mobilenativefoundation.store.store5.Updater
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any> storeBuilderFromFetcher(
|
||||||
internal class RealStoreBuilder<Key : Any, Input : Any, Output : Any>(
|
fetcher: Fetcher<Key, NetworkRepresentation>,
|
||||||
private val fetcher: Fetcher<Key, Input>,
|
sourceOfTruth: SourceOfTruth<Key, *>? = null,
|
||||||
private val sourceOfTruth: SourceOfTruth<Key, Input, Output>? = null
|
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, *> = RealStoreBuilder(fetcher, sourceOfTruth)
|
||||||
) : StoreBuilder<Key, Output> {
|
|
||||||
|
fun <Key : Any, CommonRepresentation : Any, NetworkRepresentation : Any, SourceOfTruthRepresentation : Any> storeBuilderFromFetcherAndSourceOfTruth(
|
||||||
|
fetcher: Fetcher<Key, NetworkRepresentation>,
|
||||||
|
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>,
|
||||||
|
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> = RealStoreBuilder(fetcher, sourceOfTruth)
|
||||||
|
|
||||||
|
internal class RealStoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
|
||||||
|
private val fetcher: Fetcher<Key, NetworkRepresentation>,
|
||||||
|
private val sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>? = null
|
||||||
|
) : StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
private var cachePolicy: MemoryPolicy<Key, Output>? = StoreDefaults.memoryPolicy
|
private var cachePolicy: MemoryPolicy<Key, CommonRepresentation>? = StoreDefaults.memoryPolicy
|
||||||
|
private var converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null
|
||||||
|
|
||||||
override fun scope(scope: CoroutineScope): RealStoreBuilder<Key, Input, Output> {
|
override fun scope(scope: CoroutineScope): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
||||||
this.scope = scope
|
this.scope = scope
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cachePolicy(memoryPolicy: MemoryPolicy<Key, Output>?): RealStoreBuilder<Key, Input, Output> {
|
override fun cachePolicy(memoryPolicy: MemoryPolicy<Key, CommonRepresentation>?): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
||||||
cachePolicy = memoryPolicy
|
cachePolicy = memoryPolicy
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun disableCache(): RealStoreBuilder<Key, Input, Output> {
|
override fun disableCache(): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
||||||
cachePolicy = null
|
cachePolicy = null
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun build(): Store<Key, Output> {
|
override fun converter(converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
||||||
@Suppress("UNCHECKED_CAST")
|
this.converter = converter
|
||||||
return RealStore(
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun build(): Store<Key, CommonRepresentation> = RealStore(
|
||||||
scope = scope ?: GlobalScope,
|
scope = scope ?: GlobalScope,
|
||||||
sourceOfTruth = sourceOfTruth,
|
sourceOfTruth = sourceOfTruth,
|
||||||
fetcher = fetcher,
|
fetcher = fetcher,
|
||||||
memoryPolicy = cachePolicy
|
memoryPolicy = cachePolicy,
|
||||||
|
converter = converter
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun <NetworkWriteResponse : Any> build(
|
||||||
|
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>,
|
||||||
|
bookkeeper: Bookkeeper<Key>
|
||||||
|
): MutableStore<Key, CommonRepresentation> =
|
||||||
|
build().asMutableStore<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation, NetworkWriteResponse>(
|
||||||
|
updater = updater,
|
||||||
|
bookkeeper = bookkeeper
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.impl
|
||||||
|
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreWriteRequest
|
||||||
|
|
||||||
|
data class RealStoreWriteRequest<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any>(
|
||||||
|
override val key: Key,
|
||||||
|
override val input: CommonRepresentation,
|
||||||
|
override val created: Long,
|
||||||
|
override val onCompletions: List<OnStoreWriteCompletion>?
|
||||||
|
) : StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>
|
|
@ -26,19 +26,22 @@ import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import org.mobilenativefoundation.store.store5.ResponseOrigin
|
|
||||||
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||||
import org.mobilenativefoundation.store.store5.StoreResponse
|
import org.mobilenativefoundation.store.store5.StoreConverter
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreReadResponse
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
|
||||||
import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
|
import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a [SourceOfTruth] and blocks reads while a write is in progress.
|
* Wraps a [SourceOfTruth] and blocks reads while a write is in progress.
|
||||||
*
|
*
|
||||||
* Used in the [com.dropbox.android.external.store4.impl.RealStore] implementation to avoid
|
* Used in the [RealStore] implementation to avoid
|
||||||
* dispatching values to downstream while a write is in progress.
|
* dispatching values to downstream while a write is in progress.
|
||||||
*/
|
*/
|
||||||
internal class SourceOfTruthWithBarrier<Key, Input, Output>(
|
@Suppress("UNCHECKED_CAST")
|
||||||
private val delegate: SourceOfTruth<Key, Input, Output>
|
internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
|
||||||
|
private val delegate: SourceOfTruth<Key, SourceOfTruthRepresentation>,
|
||||||
|
private val converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Each key has a barrier so that we can block reads while writing.
|
* Each key has a barrier so that we can block reads while writing.
|
||||||
|
@ -56,7 +59,7 @@ internal class SourceOfTruthWithBarrier<Key, Input, Output>(
|
||||||
*/
|
*/
|
||||||
private val versionCounter = atomic(0L)
|
private val versionCounter = atomic(0L)
|
||||||
|
|
||||||
fun reader(key: Key, lock: CompletableDeferred<Unit>): Flow<StoreResponse<Output?>> {
|
fun reader(key: Key, lock: CompletableDeferred<Unit>): Flow<StoreReadResponse<CommonRepresentation?>> {
|
||||||
return flow {
|
return flow {
|
||||||
val barrier = barriers.acquire(key)
|
val barrier = barriers.acquire(key)
|
||||||
val readerVersion: Long = versionCounter.incrementAndGet()
|
val readerVersion: Long = versionCounter.incrementAndGet()
|
||||||
|
@ -71,38 +74,47 @@ internal class SourceOfTruthWithBarrier<Key, Input, Output>(
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val readFlow: Flow<StoreResponse<Output?>> = when (barrierMessage) {
|
val readFlow: Flow<StoreReadResponse<CommonRepresentation?>> = when (barrierMessage) {
|
||||||
is BarrierMsg.Open ->
|
is BarrierMsg.Open ->
|
||||||
delegate.reader(key).mapIndexed { index, output ->
|
delegate.reader(key).mapIndexed { index, sourceOfTruthRepresentation ->
|
||||||
if (index == 0 && messageArrivedAfterMe) {
|
if (index == 0 && messageArrivedAfterMe) {
|
||||||
val firstMsgOrigin = if (writeError == null) {
|
val firstMsgOrigin = if (writeError == null) {
|
||||||
// restarted barrier without an error means write succeeded
|
// restarted barrier without an error means write succeeded
|
||||||
ResponseOrigin.Fetcher
|
StoreReadResponseOrigin.Fetcher
|
||||||
} else {
|
} else {
|
||||||
// when a write fails, we still get a new reader because
|
// when a write fails, we still get a new reader because
|
||||||
// we've disabled the previous reader before starting the
|
// we've disabled the previous reader before starting the
|
||||||
// write operation. But since write has failed, we should
|
// write operation. But since write has failed, we should
|
||||||
// use the SourceOfTruth as the origin
|
// use the SourceOfTruth as the origin
|
||||||
ResponseOrigin.SourceOfTruth
|
StoreReadResponseOrigin.SourceOfTruth
|
||||||
}
|
}
|
||||||
StoreResponse.Data(
|
|
||||||
|
val value = sourceOfTruthRepresentation as? CommonRepresentation ?: if (sourceOfTruthRepresentation != null) {
|
||||||
|
converter?.fromSourceOfTruthRepresentationToCommonRepresentation(sourceOfTruthRepresentation)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
StoreReadResponse.Data(
|
||||||
origin = firstMsgOrigin,
|
origin = firstMsgOrigin,
|
||||||
value = output
|
value = value
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
value = output
|
value = sourceOfTruthRepresentation as? CommonRepresentation
|
||||||
) as StoreResponse<Output?>
|
?: if (sourceOfTruthRepresentation != null) converter?.fromSourceOfTruthRepresentationToCommonRepresentation(
|
||||||
|
sourceOfTruthRepresentation
|
||||||
|
) else null
|
||||||
|
) as StoreReadResponse<CommonRepresentation?>
|
||||||
}
|
}
|
||||||
}.catch { throwable ->
|
}.catch { throwable ->
|
||||||
this.emit(
|
this.emit(
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
error = SourceOfTruth.ReadException(
|
error = SourceOfTruth.ReadException(
|
||||||
key = key,
|
key = key,
|
||||||
cause = throwable.cause ?: throwable
|
cause = throwable.cause ?: throwable
|
||||||
),
|
),
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -116,8 +128,8 @@ internal class SourceOfTruthWithBarrier<Key, Input, Output>(
|
||||||
// if we have a pending error, make sure to dispatch it first.
|
// if we have a pending error, make sure to dispatch it first.
|
||||||
if (writeError != null) {
|
if (writeError != null) {
|
||||||
emit(
|
emit(
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
error = writeError
|
error = writeError
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -133,12 +145,16 @@ internal class SourceOfTruthWithBarrier<Key, Input, Output>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun write(key: Key, value: Input) {
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
suspend fun write(key: Key, value: CommonRepresentation) {
|
||||||
val barrier = barriers.acquire(key)
|
val barrier = barriers.acquire(key)
|
||||||
try {
|
try {
|
||||||
barrier.emit(BarrierMsg.Blocked(versionCounter.incrementAndGet()))
|
barrier.emit(BarrierMsg.Blocked(versionCounter.incrementAndGet()))
|
||||||
val writeError = try {
|
val writeError = try {
|
||||||
delegate.write(key, value)
|
val input = value as? SourceOfTruthRepresentation ?: converter?.fromCommonRepresentationToSourceOfTruthRepresentation(value)
|
||||||
|
if (input != null) {
|
||||||
|
delegate.write(key, input)
|
||||||
|
}
|
||||||
null
|
null
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
if (throwable !is CancellationException) {
|
if (throwable !is CancellationException) {
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.impl.extensions
|
||||||
|
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
|
||||||
|
internal fun now() = Clock.System.now().toEpochMilliseconds()
|
|
@ -0,0 +1,51 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.impl.extensions
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.filterNot
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import org.mobilenativefoundation.store.store5.Bookkeeper
|
||||||
|
import org.mobilenativefoundation.store.store5.MutableStore
|
||||||
|
import org.mobilenativefoundation.store.store5.Store
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreReadRequest
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreReadResponse
|
||||||
|
import org.mobilenativefoundation.store.store5.Updater
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.RealMutableStore
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.RealStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper factory that will return data for [key] if it is cached otherwise will return
|
||||||
|
* fresh/network data (updating your caches)
|
||||||
|
*/
|
||||||
|
suspend fun <Key : Any, CommonRepresentation : Any> Store<Key, CommonRepresentation>.get(key: Key) =
|
||||||
|
stream(StoreReadRequest.cached(key, refresh = false))
|
||||||
|
.filterNot { it is StoreReadResponse.Loading || it is StoreReadResponse.NoNewData }
|
||||||
|
.first()
|
||||||
|
.requireData()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper factory that will return fresh data for [key] while updating your caches
|
||||||
|
*
|
||||||
|
* Note: If the [Fetcher] does not return any data (i.e the returned
|
||||||
|
* [kotlinx.coroutines.Flow], when collected, is empty). Then store will fall back to local
|
||||||
|
* data **even** if you explicitly requested fresh data.
|
||||||
|
* See https://github.com/dropbox/Store/pull/194 for context
|
||||||
|
*/
|
||||||
|
suspend fun <Key : Any, CommonRepresentation : Any> Store<Key, CommonRepresentation>.fresh(key: Key) =
|
||||||
|
stream(StoreReadRequest.fresh(key))
|
||||||
|
.filterNot { it is StoreReadResponse.Loading || it is StoreReadResponse.NoNewData }
|
||||||
|
.first()
|
||||||
|
.requireData()
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any, NetworkWriteResponse : Any> Store<Key, CommonRepresentation>.asMutableStore(
|
||||||
|
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>,
|
||||||
|
bookkeeper: Bookkeeper<Key>
|
||||||
|
): MutableStore<Key, CommonRepresentation> {
|
||||||
|
val delegate = this as? RealStore<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
|
||||||
|
?: throw Exception("MutableStore requires Store to be built using StoreBuilder")
|
||||||
|
|
||||||
|
return RealMutableStore(
|
||||||
|
delegate = delegate,
|
||||||
|
updater = updater,
|
||||||
|
bookkeeper = bookkeeper
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.internal.concurrent
|
||||||
|
|
||||||
|
annotation class AnyThread
|
|
@ -0,0 +1,33 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.internal.concurrent
|
||||||
|
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locks when first reader starts and unlocks when last reader finishes.
|
||||||
|
* Lightswitch analogy: First one into a room turns on the light (locks the mutex), and the last one out turns off the light (unlocks the mutex).
|
||||||
|
* @property counter Number of readers
|
||||||
|
*/
|
||||||
|
internal class Lightswitch {
|
||||||
|
private var counter = 0
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
suspend fun lock(room: Mutex) {
|
||||||
|
mutex.withLock {
|
||||||
|
counter += 1
|
||||||
|
if (counter == 1) {
|
||||||
|
room.lock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun unlock(room: Mutex) {
|
||||||
|
mutex.withLock {
|
||||||
|
counter -= 1
|
||||||
|
check(counter >= 0)
|
||||||
|
if (counter == 0) {
|
||||||
|
room.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.internal.concurrent
|
||||||
|
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
|
||||||
|
internal data class ThreadSafety(
|
||||||
|
val writeRequests: StoreThreadSafety = StoreThreadSafety(),
|
||||||
|
val readCompletions: StoreThreadSafety = StoreThreadSafety()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class StoreThreadSafety(
|
||||||
|
val mutex: Mutex = Mutex(),
|
||||||
|
val lightswitch: Lightswitch = Lightswitch()
|
||||||
|
)
|
|
@ -0,0 +1,3 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.internal.definition
|
||||||
|
|
||||||
|
typealias Converter<Input, Output> = (input: Input) -> Output
|
|
@ -0,0 +1,3 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.internal.definition
|
||||||
|
|
||||||
|
typealias Timestamp = Long
|
|
@ -0,0 +1,5 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.internal.definition
|
||||||
|
|
||||||
|
import org.mobilenativefoundation.store.store5.StoreWriteRequest
|
||||||
|
|
||||||
|
typealias WriteRequestQueue<Key, CommonRepresentation, NetworkWriteResponse> = ArrayDeque<StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>>
|
|
@ -0,0 +1,16 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.internal.result
|
||||||
|
|
||||||
|
import org.mobilenativefoundation.store.store5.UpdaterResult
|
||||||
|
|
||||||
|
sealed class EagerConflictResolutionResult<out NetworkWriteResponse : Any> {
|
||||||
|
|
||||||
|
sealed class Success<NetworkWriteResponse : Any> : EagerConflictResolutionResult<NetworkWriteResponse>() {
|
||||||
|
object NoConflicts : Success<Nothing>()
|
||||||
|
data class ConflictsResolved<NetworkWriteResponse : Any>(val value: UpdaterResult.Success) : Success<NetworkWriteResponse>()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Error : EagerConflictResolutionResult<Nothing>() {
|
||||||
|
data class Message(val message: String) : Error()
|
||||||
|
data class Exception(val error: Throwable) : Error()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.internal.result
|
||||||
|
|
||||||
|
sealed class StoreDelegateWriteResult {
|
||||||
|
object Success : StoreDelegateWriteResult()
|
||||||
|
sealed class Error : StoreDelegateWriteResult() {
|
||||||
|
data class Message(val error: String) : Error()
|
||||||
|
data class Exception(val error: Throwable) : Error()
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,7 +43,7 @@ class ClearAllStoreTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun callingClearAllOnStoreWithPersisterAndNoInMemoryCacheDeletesAllEntriesFromThePersister() = testScope.runTest {
|
fun callingClearAllOnStoreWithPersisterAndNoInMemoryCacheDeletesAllEntriesFromThePersister() = testScope.runTest {
|
||||||
val store = StoreBuilder.from(
|
val store = StoreBuilder.from<String, Int, Int, Int>(
|
||||||
fetcher = fetcher,
|
fetcher = fetcher,
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
).scope(testScope)
|
).scope(testScope)
|
||||||
|
@ -54,8 +54,8 @@ class ClearAllStoreTests {
|
||||||
val responseOneA = store.getData(key1)
|
val responseOneA = store.getData(key1)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value1
|
value = value1
|
||||||
),
|
),
|
||||||
responseOneA
|
responseOneA
|
||||||
|
@ -63,36 +63,33 @@ class ClearAllStoreTests {
|
||||||
val responseTwoA = store.getData(key2)
|
val responseTwoA = store.getData(key2)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value2
|
value = value2
|
||||||
),
|
),
|
||||||
responseTwoA
|
responseTwoA
|
||||||
)
|
)
|
||||||
|
|
||||||
// should receive data from persister
|
// should receive data from persister
|
||||||
val responseOneB = store.getData(key1)
|
val responseOneB = store.getData(key1)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
value = value1
|
value = value1
|
||||||
),
|
),
|
||||||
responseOneB
|
responseOneB
|
||||||
)
|
)
|
||||||
|
|
||||||
val responseTwoB = store.getData(key2)
|
val responseTwoB = store.getData(key2)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
value = value2
|
value = value2
|
||||||
),
|
),
|
||||||
responseTwoB
|
responseTwoB
|
||||||
)
|
)
|
||||||
|
|
||||||
// clear all entries in store
|
// clear all entries in store
|
||||||
store.clearAll()
|
store.clear()
|
||||||
assertNull(persister.peekEntry(key1))
|
assertNull(persister.peekEntry(key1))
|
||||||
assertNull(persister.peekEntry(key2))
|
assertNull(persister.peekEntry(key2))
|
||||||
|
|
||||||
|
@ -100,8 +97,8 @@ class ClearAllStoreTests {
|
||||||
val responseOneC = store.getData(key1)
|
val responseOneC = store.getData(key1)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value1
|
value = value1
|
||||||
),
|
),
|
||||||
responseOneC
|
responseOneC
|
||||||
|
@ -110,8 +107,8 @@ class ClearAllStoreTests {
|
||||||
val responseTwoC = store.getData(key2)
|
val responseTwoC = store.getData(key2)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value2
|
value = value2
|
||||||
),
|
),
|
||||||
responseTwoC
|
responseTwoC
|
||||||
|
@ -120,21 +117,21 @@ class ClearAllStoreTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun callingClearAllOnStoreWithInMemoryCacheAndNoPersisterDeletesAllEntriesFromTheInMemoryCache() = testScope.runTest {
|
fun callingClearAllOnStoreWithInMemoryCacheAndNoPersisterDeletesAllEntriesFromTheInMemoryCache() = testScope.runTest {
|
||||||
val store = StoreBuilder.from(
|
val store = StoreBuilder.from<String, Int, Int>(
|
||||||
fetcher = fetcher
|
fetcher = fetcher
|
||||||
).scope(testScope).build()
|
).scope(testScope).build()
|
||||||
|
|
||||||
// should receive data from network first time
|
// should receive data from network first time
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value1
|
value = value1
|
||||||
),
|
),
|
||||||
store.getData(key1)
|
store.getData(key1)
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value2
|
value = value2
|
||||||
),
|
),
|
||||||
store.getData(key2)
|
store.getData(key2)
|
||||||
|
@ -142,34 +139,34 @@ class ClearAllStoreTests {
|
||||||
|
|
||||||
// should receive data from cache
|
// should receive data from cache
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.Cache,
|
origin = StoreReadResponseOrigin.Cache,
|
||||||
value = value1
|
value = value1
|
||||||
),
|
),
|
||||||
store.getData(key1)
|
store.getData(key1)
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.Cache,
|
origin = StoreReadResponseOrigin.Cache,
|
||||||
value = value2
|
value = value2
|
||||||
),
|
),
|
||||||
store.getData(key2)
|
store.getData(key2)
|
||||||
)
|
)
|
||||||
|
|
||||||
// clear all entries in store
|
// clear all entries in store
|
||||||
store.clearAll()
|
store.clear()
|
||||||
|
|
||||||
// should fetch data from network again
|
// should fetch data from network again
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value1
|
value = value1
|
||||||
),
|
),
|
||||||
store.getData(key1)
|
store.getData(key1)
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value2
|
value = value2
|
||||||
),
|
),
|
||||||
store.getData(key2)
|
store.getData(key2)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.mobilenativefoundation.store.store5.StoreResponse.Data
|
import org.mobilenativefoundation.store.store5.StoreReadResponse.Data
|
||||||
import org.mobilenativefoundation.store.store5.util.InMemoryPersister
|
import org.mobilenativefoundation.store.store5.util.InMemoryPersister
|
||||||
import org.mobilenativefoundation.store.store5.util.asSourceOfTruth
|
import org.mobilenativefoundation.store.store5.util.asSourceOfTruth
|
||||||
import org.mobilenativefoundation.store.store5.util.getData
|
import org.mobilenativefoundation.store.store5.util.getData
|
||||||
|
@ -24,7 +24,7 @@ class ClearStoreByKeyTests {
|
||||||
fun callingClearWithKeyOnStoreWithPersisterWithNoInMemoryCacheDeletesTheEntryAssociatedWithTheKeyFromThePersister() = testScope.runTest {
|
fun callingClearWithKeyOnStoreWithPersisterWithNoInMemoryCacheDeletesTheEntryAssociatedWithTheKeyFromThePersister() = testScope.runTest {
|
||||||
val key = "key"
|
val key = "key"
|
||||||
val value = 1
|
val value = 1
|
||||||
val store = StoreBuilder.from(
|
val store = StoreBuilder.from<String, Int, Int, Int>(
|
||||||
fetcher = Fetcher.of { value },
|
fetcher = Fetcher.of { value },
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
).scope(testScope)
|
).scope(testScope)
|
||||||
|
@ -34,7 +34,7 @@ class ClearStoreByKeyTests {
|
||||||
// should receive data from network first time
|
// should receive data from network first time
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Data(
|
Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value
|
value = value
|
||||||
),
|
),
|
||||||
store.getData(key)
|
store.getData(key)
|
||||||
|
@ -43,7 +43,7 @@ class ClearStoreByKeyTests {
|
||||||
// should receive data from persister
|
// should receive data from persister
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Data(
|
Data(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
value = value
|
value = value
|
||||||
),
|
),
|
||||||
store.getData(key)
|
store.getData(key)
|
||||||
|
@ -55,7 +55,7 @@ class ClearStoreByKeyTests {
|
||||||
// should fetch data from network again
|
// should fetch data from network again
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Data(
|
Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value
|
value = value
|
||||||
),
|
),
|
||||||
store.getData(key)
|
store.getData(key)
|
||||||
|
@ -66,14 +66,14 @@ class ClearStoreByKeyTests {
|
||||||
fun callingClearWithKeyOStoreWithInMemoryCacheNoPersisterDeletesTheEntryAssociatedWithTheKeyFromTheInMemoryCache() = testScope.runTest {
|
fun callingClearWithKeyOStoreWithInMemoryCacheNoPersisterDeletesTheEntryAssociatedWithTheKeyFromTheInMemoryCache() = testScope.runTest {
|
||||||
val key = "key"
|
val key = "key"
|
||||||
val value = 1
|
val value = 1
|
||||||
val store = StoreBuilder.from<String, Int>(
|
val store = StoreBuilder.from<String, Int, Int>(
|
||||||
fetcher = Fetcher.of { value }
|
fetcher = Fetcher.of { value }
|
||||||
).scope(testScope).build()
|
).scope(testScope).build()
|
||||||
|
|
||||||
// should receive data from network first time
|
// should receive data from network first time
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Data(
|
Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value
|
value = value
|
||||||
),
|
),
|
||||||
store.getData(key)
|
store.getData(key)
|
||||||
|
@ -82,7 +82,7 @@ class ClearStoreByKeyTests {
|
||||||
// should receive data from cache
|
// should receive data from cache
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Data(
|
Data(
|
||||||
origin = ResponseOrigin.Cache,
|
origin = StoreReadResponseOrigin.Cache,
|
||||||
value = value
|
value = value
|
||||||
),
|
),
|
||||||
store.getData(key)
|
store.getData(key)
|
||||||
|
@ -94,7 +94,7 @@ class ClearStoreByKeyTests {
|
||||||
// should fetch data from network again
|
// should fetch data from network again
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Data(
|
Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value
|
value = value
|
||||||
),
|
),
|
||||||
store.getData(key)
|
store.getData(key)
|
||||||
|
@ -107,7 +107,7 @@ class ClearStoreByKeyTests {
|
||||||
val key2 = "key2"
|
val key2 = "key2"
|
||||||
val value1 = 1
|
val value1 = 1
|
||||||
val value2 = 2
|
val value2 = 2
|
||||||
val store = StoreBuilder.from(
|
val store = StoreBuilder.from<String, Int, Int, Int>(
|
||||||
fetcher = Fetcher.of { key ->
|
fetcher = Fetcher.of { key ->
|
||||||
when (key) {
|
when (key) {
|
||||||
key1 -> value1
|
key1 -> value1
|
||||||
|
@ -135,7 +135,7 @@ class ClearStoreByKeyTests {
|
||||||
// getting data for key1 should hit the network again
|
// getting data for key1 should hit the network again
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Data(
|
Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = value1
|
value = value1
|
||||||
),
|
),
|
||||||
store.getData(key1)
|
store.getData(key1)
|
||||||
|
@ -144,7 +144,7 @@ class ClearStoreByKeyTests {
|
||||||
// getting data for key2 should not hit the network
|
// getting data for key2 should not hit the network
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Data(
|
Data(
|
||||||
origin = ResponseOrigin.Cache,
|
origin = StoreReadResponseOrigin.Cache,
|
||||||
value = value2
|
value = value2
|
||||||
),
|
),
|
||||||
store.getData(key2)
|
store.getData(key2)
|
||||||
|
|
|
@ -14,7 +14,7 @@ import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.mobilenativefoundation.store.store5.StoreResponse.Data
|
import org.mobilenativefoundation.store.store5.StoreReadResponse.Data
|
||||||
import org.mobilenativefoundation.store.store5.impl.FetcherController
|
import org.mobilenativefoundation.store.store5.impl.FetcherController
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
@ -26,7 +26,7 @@ class FetcherControllerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun simple() = testScope.runTest {
|
fun simple() = testScope.runTest {
|
||||||
val fetcherController = FetcherController<Int, Int, Int>(
|
val fetcherController = FetcherController<Int, Int, Int, Int>(
|
||||||
scope = testScope,
|
scope = testScope,
|
||||||
realFetcher = Fetcher.ofResultFlow { key: Int ->
|
realFetcher = Fetcher.ofResultFlow { key: Int ->
|
||||||
flow {
|
flow {
|
||||||
|
@ -43,7 +43,7 @@ class FetcherControllerTests {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Data(
|
Data(
|
||||||
value = 9,
|
value = 9,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
received
|
received
|
||||||
)
|
)
|
||||||
|
@ -53,7 +53,7 @@ class FetcherControllerTests {
|
||||||
@Test
|
@Test
|
||||||
fun concurrent() = testScope.runTest {
|
fun concurrent() = testScope.runTest {
|
||||||
var createdCnt = 0
|
var createdCnt = 0
|
||||||
val fetcherController = FetcherController<Int, Int, Int>(
|
val fetcherController = FetcherController<Int, Int, Int, Int>(
|
||||||
scope = testScope,
|
scope = testScope,
|
||||||
realFetcher = Fetcher.ofResultFlow { key: Int ->
|
realFetcher = Fetcher.ofResultFlow { key: Int ->
|
||||||
createdCnt++
|
createdCnt++
|
||||||
|
@ -80,7 +80,7 @@ class FetcherControllerTests {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Data(
|
Data(
|
||||||
value = 9,
|
value = 9,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
it.await()
|
it.await()
|
||||||
)
|
)
|
||||||
|
@ -94,7 +94,7 @@ class FetcherControllerTests {
|
||||||
var createdCnt = 0
|
var createdCnt = 0
|
||||||
val job = SupervisorJob()
|
val job = SupervisorJob()
|
||||||
val scope = TestScope(StandardTestDispatcher() + job)
|
val scope = TestScope(StandardTestDispatcher() + job)
|
||||||
val fetcherController = FetcherController<Int, Int, Int>(
|
val fetcherController = FetcherController<Int, Int, Int, Int>(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
realFetcher = Fetcher.ofResultFlow { key: Int ->
|
realFetcher = Fetcher.ofResultFlow { key: Int ->
|
||||||
createdCnt++
|
createdCnt++
|
||||||
|
|
|
@ -19,14 +19,14 @@ class FetcherResponseTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenAFetcherThatThrowsAnExceptionInInvokeWhenStreamingThenTheExceptionsShouldNotBeCaught() = testScope.runTest {
|
fun givenAFetcherThatThrowsAnExceptionInInvokeWhenStreamingThenTheExceptionsShouldNotBeCaught() = testScope.runTest {
|
||||||
val store = StoreBuilder.from<Int, Int>(
|
val store = StoreBuilder.from<Int, Int, Int>(
|
||||||
Fetcher.ofResult {
|
Fetcher.ofResult {
|
||||||
throw RuntimeException("don't catch me")
|
throw RuntimeException("don't catch me")
|
||||||
}
|
}
|
||||||
).buildWithTestScope()
|
).buildWithTestScope()
|
||||||
|
|
||||||
assertFailsWith<RuntimeException>(message = "don't catch me") {
|
assertFailsWith<RuntimeException>(message = "don't catch me") {
|
||||||
val result = store.stream(StoreRequest.fresh(1)).toList()
|
val result = store.stream(StoreReadRequest.fresh(1)).toList()
|
||||||
assertEquals(0, result.size)
|
assertEquals(0, result.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,9 +35,9 @@ class FetcherResponseTests {
|
||||||
fun givenAFetcherThatEmitsErrorAndDataWhenSteamingThenItCanEmitValueAfterAnError() {
|
fun givenAFetcherThatEmitsErrorAndDataWhenSteamingThenItCanEmitValueAfterAnError() {
|
||||||
val exception = RuntimeException("first error")
|
val exception = RuntimeException("first error")
|
||||||
testScope.runTest {
|
testScope.runTest {
|
||||||
val store = StoreBuilder.from(
|
val store = StoreBuilder.from<Int, String, String>(
|
||||||
fetcher = Fetcher.ofResultFlow { key: Int ->
|
fetcher = Fetcher.ofResultFlow { key: Int ->
|
||||||
flowOf<FetcherResult<String>>(
|
flowOf(
|
||||||
FetcherResult.Error.Exception(exception),
|
FetcherResult.Error.Exception(exception),
|
||||||
FetcherResult.Data("$key")
|
FetcherResult.Data("$key")
|
||||||
)
|
)
|
||||||
|
@ -46,12 +46,12 @@ class FetcherResponseTests {
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
store.stream(
|
store.stream(
|
||||||
StoreRequest.fresh(1)
|
StoreReadRequest.fresh(1)
|
||||||
),
|
),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(ResponseOrigin.Fetcher),
|
StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher),
|
||||||
StoreResponse.Error.Exception(exception, ResponseOrigin.Fetcher),
|
StoreReadResponse.Error.Exception(exception, StoreReadResponseOrigin.Fetcher),
|
||||||
StoreResponse.Data("1", ResponseOrigin.Fetcher)
|
StoreReadResponse.Data("1", StoreReadResponseOrigin.Fetcher)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -61,26 +61,26 @@ class FetcherResponseTests {
|
||||||
fun givenTransformerWhenRawValueThenUnwrappedValueReturnedAndValueIsCached() = testScope.runTest {
|
fun givenTransformerWhenRawValueThenUnwrappedValueReturnedAndValueIsCached() = testScope.runTest {
|
||||||
val fetcher = Fetcher.ofFlow<Int, Int> { flowOf(it * it) }
|
val fetcher = Fetcher.ofFlow<Int, Int> { flowOf(it * it) }
|
||||||
val pipeline = StoreBuilder
|
val pipeline = StoreBuilder
|
||||||
.from(fetcher).buildWithTestScope()
|
.from<Int, Int, Int>(fetcher).buildWithTestScope()
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = 9,
|
value = 9,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = 9,
|
value = 9,
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -98,30 +98,30 @@ class FetcherResponseTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val pipeline = StoreBuilder.from(fetcher)
|
val pipeline = StoreBuilder.from<Int, Int, Int>(fetcher)
|
||||||
.buildWithTestScope()
|
.buildWithTestScope()
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.fresh(3)),
|
pipeline.stream(StoreReadRequest.fresh(3)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Error.Message(
|
StoreReadResponse.Error.Message(
|
||||||
message = "zero",
|
message = "zero",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = 1,
|
value = 1,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -141,30 +141,30 @@ class FetcherResponseTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val pipeline = StoreBuilder
|
val pipeline = StoreBuilder
|
||||||
.from(fetcher)
|
.from<Int, Int, Int>(fetcher)
|
||||||
.buildWithTestScope()
|
.buildWithTestScope()
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.fresh(3)),
|
pipeline.stream(StoreReadRequest.fresh(3)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
error = e,
|
error = e,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = 1,
|
value = 1,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -182,35 +182,35 @@ class FetcherResponseTests {
|
||||||
count - 1
|
count - 1
|
||||||
}
|
}
|
||||||
val pipeline = StoreBuilder
|
val pipeline = StoreBuilder
|
||||||
.from(fetcher = fetcher)
|
.from<Int, Int, Int>(fetcher = fetcher)
|
||||||
.buildWithTestScope()
|
.buildWithTestScope()
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.fresh(3)),
|
pipeline.stream(StoreReadRequest.fresh(3)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
error = e,
|
error = e,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = 1,
|
value = 1,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <Key : Any, Output : Any> StoreBuilder<Key, Output>.buildWithTestScope() =
|
private fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>.buildWithTestScope() =
|
||||||
scope(testScope).build()
|
scope(testScope).build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,9 @@ import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.mobilenativefoundation.store.store5.StoreResponse.Data
|
import org.mobilenativefoundation.store.store5.StoreReadResponse.Data
|
||||||
import org.mobilenativefoundation.store.store5.StoreResponse.Loading
|
import org.mobilenativefoundation.store.store5.StoreReadResponse.Loading
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.extensions.fresh
|
||||||
import org.mobilenativefoundation.store.store5.util.FakeFetcher
|
import org.mobilenativefoundation.store.store5.util.FakeFetcher
|
||||||
import org.mobilenativefoundation.store.store5.util.FakeFlowingFetcher
|
import org.mobilenativefoundation.store.store5.util.FakeFlowingFetcher
|
||||||
import org.mobilenativefoundation.store.store5.util.InMemoryPersister
|
import org.mobilenativefoundation.store.store5.util.InMemoryPersister
|
||||||
|
@ -55,51 +56,51 @@ class FlowStoreTests {
|
||||||
3 to "three-2"
|
3 to "three-2"
|
||||||
)
|
)
|
||||||
val pipeline = StoreBuilder
|
val pipeline = StoreBuilder
|
||||||
.from(fetcher)
|
.from<Int, String, String>(fetcher)
|
||||||
.buildWithTestScope()
|
.buildWithTestScope()
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)).take(2).toList(),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)).take(2).toList(),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)).take(1).toList(),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)).take(1).toList(),
|
||||||
listOf(
|
listOf(
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
pipeline.stream(StoreRequest.fresh(3)).take(2).toList(),
|
pipeline.stream(StoreReadRequest.fresh(3)).take(2).toList(),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)).take(1).toList(),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)).take(1).toList(),
|
||||||
listOf(
|
listOf(
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -112,64 +113,64 @@ class FlowStoreTests {
|
||||||
3 to "three-2"
|
3 to "three-2"
|
||||||
)
|
)
|
||||||
val persister = InMemoryPersister<Int, String>()
|
val persister = InMemoryPersister<Int, String>()
|
||||||
val pipeline = StoreBuilder.from(
|
val pipeline = StoreBuilder.from<Int, String, String, String>(
|
||||||
fetcher = fetcher,
|
fetcher = fetcher,
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
).buildWithTestScope()
|
).buildWithTestScope()
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)),
|
||||||
listOf(
|
listOf(
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
),
|
),
|
||||||
// note that we still get the data from persister as well as we don't listen to
|
// note that we still get the data from persister as well as we don't listen to
|
||||||
// the persister for the cached items unless there is an active stream, which
|
// the persister for the cached items unless there is an active stream, which
|
||||||
// means cache can go out of sync w/ the persister
|
// means cache can go out of sync w/ the persister
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.fresh(3)),
|
pipeline.stream(StoreReadRequest.fresh(3)),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)),
|
||||||
listOf(
|
listOf(
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -183,41 +184,41 @@ class FlowStoreTests {
|
||||||
)
|
)
|
||||||
val persister = InMemoryPersister<Int, String>()
|
val persister = InMemoryPersister<Int, String>()
|
||||||
|
|
||||||
val pipeline = StoreBuilder.from(
|
val pipeline = StoreBuilder.from<Int, String, String, String>(
|
||||||
fetcher = fetcher,
|
fetcher = fetcher,
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
).buildWithTestScope()
|
).buildWithTestScope()
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -229,36 +230,36 @@ class FlowStoreTests {
|
||||||
3 to "three-1",
|
3 to "three-1",
|
||||||
3 to "three-2"
|
3 to "three-2"
|
||||||
)
|
)
|
||||||
val pipeline = StoreBuilder.from(fetcher = fetcher)
|
val pipeline = StoreBuilder.from<Int, String, String>(fetcher = fetcher)
|
||||||
.buildWithTestScope()
|
.buildWithTestScope()
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf
|
listOf
|
||||||
(
|
(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
),
|
),
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -270,31 +271,31 @@ class FlowStoreTests {
|
||||||
3 to "three-1",
|
3 to "three-1",
|
||||||
3 to "three-2"
|
3 to "three-2"
|
||||||
)
|
)
|
||||||
val pipeline = StoreBuilder.from(fetcher = fetcher)
|
val pipeline = StoreBuilder.from<Int, String, String>(fetcher = fetcher)
|
||||||
.buildWithTestScope()
|
.buildWithTestScope()
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.skipMemory(3, refresh = false)),
|
pipeline.stream(StoreReadRequest.skipMemory(3, refresh = false)),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.skipMemory(3, refresh = false)),
|
pipeline.stream(StoreReadRequest.skipMemory(3, refresh = false)),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -308,7 +309,7 @@ class FlowStoreTests {
|
||||||
)
|
)
|
||||||
val persister = InMemoryPersister<Int, String>()
|
val persister = InMemoryPersister<Int, String>()
|
||||||
|
|
||||||
val pipeline = StoreBuilder.from(
|
val pipeline = StoreBuilder.from<Int, String, String, String>(
|
||||||
fetcher = fetcher,
|
fetcher = fetcher,
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
)
|
)
|
||||||
|
@ -316,38 +317,38 @@ class FlowStoreTests {
|
||||||
.buildWithTestScope()
|
.buildWithTestScope()
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.fresh(3)),
|
pipeline.stream(StoreReadRequest.fresh(3)),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -356,7 +357,7 @@ class FlowStoreTests {
|
||||||
@Test
|
@Test
|
||||||
fun diskChangeWhileNetworkIsFlowing_simple() = testScope.runTest {
|
fun diskChangeWhileNetworkIsFlowing_simple() = testScope.runTest {
|
||||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||||
val pipeline = StoreBuilder.from(
|
val pipeline = StoreBuilder.from<Int, String, String, String>(
|
||||||
Fetcher.ofFlow {
|
Fetcher.ofFlow {
|
||||||
flow {
|
flow {
|
||||||
delay(20)
|
delay(20)
|
||||||
|
@ -373,18 +374,18 @@ class FlowStoreTests {
|
||||||
persister.flowWriter(3, "local-1")
|
persister.flowWriter(3, "local-1")
|
||||||
}
|
}
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "local-1",
|
value = "local-1",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
|
|
||||||
)
|
)
|
||||||
|
@ -394,7 +395,7 @@ class FlowStoreTests {
|
||||||
@Test
|
@Test
|
||||||
fun diskChangeWhileNetworkIsFlowing_overwrite() = testScope.runTest {
|
fun diskChangeWhileNetworkIsFlowing_overwrite() = testScope.runTest {
|
||||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||||
val pipeline = StoreBuilder.from(
|
val pipeline = StoreBuilder.from<Int, String, String, String>(
|
||||||
fetcher = Fetcher.ofFlow {
|
fetcher = Fetcher.ofFlow {
|
||||||
flow {
|
flow {
|
||||||
delay(10)
|
delay(10)
|
||||||
|
@ -415,26 +416,26 @@ class FlowStoreTests {
|
||||||
persister.flowWriter(3, "local-2")
|
persister.flowWriter(3, "local-2")
|
||||||
}
|
}
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "local-1",
|
value = "local-1",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "local-2",
|
value = "local-2",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -444,7 +445,7 @@ class FlowStoreTests {
|
||||||
fun errorTest() = testScope.runTest {
|
fun errorTest() = testScope.runTest {
|
||||||
val exception = IllegalArgumentException("wow")
|
val exception = IllegalArgumentException("wow")
|
||||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||||
val pipeline = StoreBuilder.from(
|
val pipeline = StoreBuilder.from<Int, String, String, String>(
|
||||||
Fetcher.of {
|
Fetcher.of {
|
||||||
throw exception
|
throw exception
|
||||||
},
|
},
|
||||||
|
@ -458,34 +459,34 @@ class FlowStoreTests {
|
||||||
persister.flowWriter(3, "local-1")
|
persister.flowWriter(3, "local-1")
|
||||||
}
|
}
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(key = 3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(key = 3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
error = exception,
|
error = exception,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "local-1",
|
value = "local-1",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(key = 3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(key = 3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Data(
|
Data(
|
||||||
value = "local-1",
|
value = "local-1",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
error = exception,
|
error = exception,
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -495,7 +496,7 @@ class FlowStoreTests {
|
||||||
fun givenSourceOfTruthWhenStreamFreshDataReturnsNoDataFromFetcherThenFetchReturnsNoDataAndCachedValuesAreReceived() =
|
fun givenSourceOfTruthWhenStreamFreshDataReturnsNoDataFromFetcherThenFetchReturnsNoDataAndCachedValuesAreReceived() =
|
||||||
testScope.runTest {
|
testScope.runTest {
|
||||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||||
val pipeline = StoreBuilder.from(
|
val pipeline = StoreBuilder.from<Int, String, String, String>(
|
||||||
fetcher = Fetcher.ofFlow { flow {} },
|
fetcher = Fetcher.ofFlow { flow {} },
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
)
|
)
|
||||||
|
@ -506,21 +507,21 @@ class FlowStoreTests {
|
||||||
assertEquals("local-1", firstFetch)
|
assertEquals("local-1", firstFetch)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.fresh(3)),
|
pipeline.stream(StoreReadRequest.fresh(3)),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.NoNewData(
|
StoreReadResponse.NoNewData(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "local-1",
|
value = "local-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "local-1",
|
value = "local-1",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -529,7 +530,7 @@ class FlowStoreTests {
|
||||||
@Test
|
@Test
|
||||||
fun givenSourceOfTruthWhenStreamCachedDataWithRefreshReturnsNoNewDataThenCachedValuesAreReceivedAndFetchReturnsNoData() = testScope.runTest {
|
fun givenSourceOfTruthWhenStreamCachedDataWithRefreshReturnsNoNewDataThenCachedValuesAreReceivedAndFetchReturnsNoData() = testScope.runTest {
|
||||||
val persister = InMemoryPersister<Int, String>().asFlowable()
|
val persister = InMemoryPersister<Int, String>().asFlowable()
|
||||||
val pipeline = StoreBuilder.from(
|
val pipeline = StoreBuilder.from<Int, String, String, String>(
|
||||||
fetcher = Fetcher.ofFlow { flow {} },
|
fetcher = Fetcher.ofFlow { flow {} },
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
)
|
)
|
||||||
|
@ -540,21 +541,21 @@ class FlowStoreTests {
|
||||||
assertEquals("local-1", firstFetch)
|
assertEquals("local-1", firstFetch)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Data(
|
Data(
|
||||||
value = "local-1",
|
value = "local-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "local-1",
|
value = "local-1",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.NoNewData(
|
StoreReadResponse.NoNewData(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -563,7 +564,7 @@ class FlowStoreTests {
|
||||||
@Test
|
@Test
|
||||||
fun givenNoSourceOfTruthWhenStreamFreshDataReturnsNoDataFromFetcherThenFetchReturnsNoDataAndCachedValuesAreReceived() = testScope.runTest {
|
fun givenNoSourceOfTruthWhenStreamFreshDataReturnsNoDataFromFetcherThenFetchReturnsNoDataAndCachedValuesAreReceived() = testScope.runTest {
|
||||||
var createCount = 0
|
var createCount = 0
|
||||||
val pipeline = StoreBuilder.from<Int, String>(
|
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||||
fetcher = Fetcher.ofFlow {
|
fetcher = Fetcher.ofFlow {
|
||||||
if (createCount++ == 0) {
|
if (createCount++ == 0) {
|
||||||
flowOf("remote-1")
|
flowOf("remote-1")
|
||||||
|
@ -578,17 +579,17 @@ class FlowStoreTests {
|
||||||
assertEquals("remote-1", firstFetch)
|
assertEquals("remote-1", firstFetch)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.fresh(3)),
|
pipeline.stream(StoreReadRequest.fresh(3)),
|
||||||
listOf(
|
listOf(
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.NoNewData(
|
StoreReadResponse.NoNewData(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
Data(
|
Data(
|
||||||
value = "remote-1",
|
value = "remote-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -597,7 +598,7 @@ class FlowStoreTests {
|
||||||
@Test
|
@Test
|
||||||
fun givenNoSoTWhenStreamCachedDataWithRefreshReturnsNoNewDataThenCachedValuesAreReceivedAndFetchReturnsNoData() = testScope.runTest {
|
fun givenNoSoTWhenStreamCachedDataWithRefreshReturnsNoNewDataThenCachedValuesAreReceivedAndFetchReturnsNoData() = testScope.runTest {
|
||||||
var createCount = 0
|
var createCount = 0
|
||||||
val pipeline = StoreBuilder.from<Int, String>(
|
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||||
fetcher = Fetcher.ofFlow {
|
fetcher = Fetcher.ofFlow {
|
||||||
if (createCount++ == 0) {
|
if (createCount++ == 0) {
|
||||||
flowOf("remote-1")
|
flowOf("remote-1")
|
||||||
|
@ -612,17 +613,17 @@ class FlowStoreTests {
|
||||||
assertEquals("remote-1", firstFetch)
|
assertEquals("remote-1", firstFetch)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Data(
|
Data(
|
||||||
value = "remote-1",
|
value = "remote-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
),
|
),
|
||||||
Loading(
|
Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.NoNewData(
|
StoreReadResponse.NoNewData(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -634,14 +635,14 @@ class FlowStoreTests {
|
||||||
3 to "three-1",
|
3 to "three-1",
|
||||||
3 to "three-2"
|
3 to "three-2"
|
||||||
)
|
)
|
||||||
val store = StoreBuilder.from(fetcher = fetcher)
|
val store = StoreBuilder.from<Int, String, String>(fetcher = fetcher)
|
||||||
.buildWithTestScope()
|
.buildWithTestScope()
|
||||||
|
|
||||||
val firstFetch = store.fresh(3)
|
val firstFetch = store.fresh(3)
|
||||||
assertEquals("three-1", firstFetch)
|
assertEquals("three-1", firstFetch)
|
||||||
val secondCollect = mutableListOf<StoreResponse<String>>()
|
val secondCollect = mutableListOf<StoreReadResponse<String>>()
|
||||||
val collection = launch {
|
val collection = launch {
|
||||||
store.stream(StoreRequest.cached(3, refresh = false)).collect {
|
store.stream(StoreReadRequest.cached(3, refresh = false)).collect {
|
||||||
secondCollect.add(it)
|
secondCollect.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -651,7 +652,7 @@ class FlowStoreTests {
|
||||||
secondCollect,
|
secondCollect,
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// trigger another fetch from network
|
// trigger another fetch from network
|
||||||
|
@ -665,14 +666,14 @@ class FlowStoreTests {
|
||||||
secondCollect,
|
secondCollect,
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertContains(
|
assertContains(
|
||||||
secondCollect,
|
secondCollect,
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -686,16 +687,16 @@ class FlowStoreTests {
|
||||||
3 to "three-2"
|
3 to "three-2"
|
||||||
)
|
)
|
||||||
val persister = InMemoryPersister<Int, String>()
|
val persister = InMemoryPersister<Int, String>()
|
||||||
val pipeline = StoreBuilder.from(
|
val pipeline = StoreBuilder.from<Int, String, String, String>(
|
||||||
fetcher = fetcher,
|
fetcher = fetcher,
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
).buildWithTestScope()
|
).buildWithTestScope()
|
||||||
|
|
||||||
val firstFetch = pipeline.fresh(3)
|
val firstFetch = pipeline.fresh(3)
|
||||||
assertEquals("three-1", firstFetch)
|
assertEquals("three-1", firstFetch)
|
||||||
val secondCollect = mutableListOf<StoreResponse<String>>()
|
val secondCollect = mutableListOf<StoreReadResponse<String>>()
|
||||||
val collection = launch {
|
val collection = launch {
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)).collect {
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)).collect {
|
||||||
secondCollect.add(it)
|
secondCollect.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -706,14 +707,14 @@ class FlowStoreTests {
|
||||||
secondCollect,
|
secondCollect,
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertContains(
|
assertContains(
|
||||||
secondCollect,
|
secondCollect,
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -728,14 +729,14 @@ class FlowStoreTests {
|
||||||
secondCollect,
|
secondCollect,
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertContains(
|
assertContains(
|
||||||
secondCollect,
|
secondCollect,
|
||||||
Data(
|
Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -743,7 +744,7 @@ class FlowStoreTests {
|
||||||
secondCollect,
|
secondCollect,
|
||||||
Data(
|
Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
collection.cancelAndJoin()
|
collection.cancelAndJoin()
|
||||||
|
@ -757,13 +758,13 @@ class FlowStoreTests {
|
||||||
3 to "three-2",
|
3 to "three-2",
|
||||||
3 to "three-3"
|
3 to "three-3"
|
||||||
)
|
)
|
||||||
val pipeline = StoreBuilder.from(
|
val pipeline = StoreBuilder.from<Int, String, String>(
|
||||||
fetcher = fetcher
|
fetcher = fetcher
|
||||||
).buildWithTestScope()
|
).buildWithTestScope()
|
||||||
|
|
||||||
val fetcher1Collected = mutableListOf<StoreResponse<String>>()
|
val fetcher1Collected = mutableListOf<StoreReadResponse<String>>()
|
||||||
val fetcher1Job = async {
|
val fetcher1Job = async {
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)).collect {
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)).collect {
|
||||||
fetcher1Collected.add(it)
|
fetcher1Collected.add(it)
|
||||||
delay(1_000)
|
delay(1_000)
|
||||||
}
|
}
|
||||||
|
@ -771,36 +772,36 @@ class FlowStoreTests {
|
||||||
testScope.advanceUntilIdle()
|
testScope.advanceUntilIdle()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(
|
listOf(
|
||||||
Loading(origin = ResponseOrigin.Fetcher),
|
Loading(origin = StoreReadResponseOrigin.Fetcher),
|
||||||
Data(origin = ResponseOrigin.Fetcher, value = "three-1")
|
Data(origin = StoreReadResponseOrigin.Fetcher, value = "three-1")
|
||||||
),
|
),
|
||||||
fetcher1Collected
|
fetcher1Collected
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Data(origin = ResponseOrigin.Cache, value = "three-1"),
|
Data(origin = StoreReadResponseOrigin.Cache, value = "three-1"),
|
||||||
Loading(origin = ResponseOrigin.Fetcher),
|
Loading(origin = StoreReadResponseOrigin.Fetcher),
|
||||||
Data(origin = ResponseOrigin.Fetcher, value = "three-2")
|
Data(origin = StoreReadResponseOrigin.Fetcher, value = "three-2")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Data(origin = ResponseOrigin.Cache, value = "three-2"),
|
Data(origin = StoreReadResponseOrigin.Cache, value = "three-2"),
|
||||||
Loading(origin = ResponseOrigin.Fetcher),
|
Loading(origin = StoreReadResponseOrigin.Fetcher),
|
||||||
Data(origin = ResponseOrigin.Fetcher, value = "three-3")
|
Data(origin = StoreReadResponseOrigin.Fetcher, value = "three-3")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
testScope.advanceUntilIdle()
|
testScope.advanceUntilIdle()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(
|
listOf(
|
||||||
Loading(origin = ResponseOrigin.Fetcher),
|
Loading(origin = StoreReadResponseOrigin.Fetcher),
|
||||||
Data(origin = ResponseOrigin.Fetcher, value = "three-1"),
|
Data(origin = StoreReadResponseOrigin.Fetcher, value = "three-1"),
|
||||||
Data(origin = ResponseOrigin.Fetcher, value = "three-2"),
|
Data(origin = StoreReadResponseOrigin.Fetcher, value = "three-2"),
|
||||||
Data(origin = ResponseOrigin.Fetcher, value = "three-3")
|
Data(origin = StoreReadResponseOrigin.Fetcher, value = "three-3")
|
||||||
),
|
),
|
||||||
fetcher1Collected
|
fetcher1Collected
|
||||||
)
|
)
|
||||||
|
@ -815,38 +816,38 @@ class FlowStoreTests {
|
||||||
3 to "three-1",
|
3 to "three-1",
|
||||||
3 to "three-2"
|
3 to "three-2"
|
||||||
)
|
)
|
||||||
val pipeline = StoreBuilder.from(fetcher = fetcher)
|
val pipeline = StoreBuilder.from<Int, String, String>(fetcher = fetcher)
|
||||||
.buildWithTestScope()
|
.buildWithTestScope()
|
||||||
|
|
||||||
val fetcher1Collected = mutableListOf<StoreResponse<String>>()
|
val fetcher1Collected = mutableListOf<StoreReadResponse<String>>()
|
||||||
val fetcher1Job = async {
|
val fetcher1Job = async {
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)).collect {
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)).collect {
|
||||||
fetcher1Collected.add(it)
|
fetcher1Collected.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
testScope.runCurrent()
|
testScope.runCurrent()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(
|
listOf(
|
||||||
Loading(origin = ResponseOrigin.Fetcher),
|
Loading(origin = StoreReadResponseOrigin.Fetcher),
|
||||||
Data(origin = ResponseOrigin.Fetcher, value = "three-1")
|
Data(origin = StoreReadResponseOrigin.Fetcher, value = "three-1")
|
||||||
),
|
),
|
||||||
fetcher1Collected
|
fetcher1Collected
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
Data(origin = ResponseOrigin.Cache, value = "three-1"),
|
Data(origin = StoreReadResponseOrigin.Cache, value = "three-1"),
|
||||||
Loading(origin = ResponseOrigin.Fetcher),
|
Loading(origin = StoreReadResponseOrigin.Fetcher),
|
||||||
Data(origin = ResponseOrigin.Fetcher, value = "three-2")
|
Data(origin = StoreReadResponseOrigin.Fetcher, value = "three-2")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
testScope.runCurrent()
|
testScope.runCurrent()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(
|
listOf(
|
||||||
Loading(origin = ResponseOrigin.Fetcher),
|
Loading(origin = StoreReadResponseOrigin.Fetcher),
|
||||||
Data(origin = ResponseOrigin.Fetcher, value = "three-1"),
|
Data(origin = StoreReadResponseOrigin.Fetcher, value = "three-1"),
|
||||||
Data(origin = ResponseOrigin.Fetcher, value = "three-2")
|
Data(origin = StoreReadResponseOrigin.Fetcher, value = "three-2")
|
||||||
),
|
),
|
||||||
fetcher1Collected
|
fetcher1Collected
|
||||||
)
|
)
|
||||||
|
@ -854,16 +855,16 @@ class FlowStoreTests {
|
||||||
fetcher1Job.cancelAndJoin()
|
fetcher1Job.cancelAndJoin()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun Store<Int, String>.get(request: StoreRequest<Int>) =
|
suspend fun Store<Int, Int>.get(request: StoreReadRequest<Int>) =
|
||||||
this.stream(request).filter { it.dataOrNull() != null }.first()
|
this.stream(request).filter { it.dataOrNull() != null }.first()
|
||||||
|
|
||||||
suspend fun Store<Int, String>.get(key: Int) = get(
|
suspend fun Store<Int, Int>.get(key: Int) = get(
|
||||||
StoreRequest.cached(
|
StoreReadRequest.cached(
|
||||||
key = key,
|
key = key,
|
||||||
refresh = false
|
refresh = false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun <Key : Any, Output : Any> StoreBuilder<Key, Output>.buildWithTestScope() =
|
private fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>.buildWithTestScope() =
|
||||||
scope(testScope).build()
|
scope(testScope).build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,43 +22,43 @@ class HotFlowStoreTests {
|
||||||
3 to "three-2"
|
3 to "three-2"
|
||||||
)
|
)
|
||||||
val pipeline = StoreBuilder
|
val pipeline = StoreBuilder
|
||||||
.from(fetcher)
|
.from<Int, String, String>(fetcher)
|
||||||
.scope(testScope)
|
.scope(testScope)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(
|
pipeline.stream(
|
||||||
StoreRequest.cached(3, refresh = false)
|
StoreReadRequest.cached(3, refresh = false)
|
||||||
),
|
),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Cache
|
origin = StoreReadResponseOrigin.Cache
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.fresh(3)),
|
pipeline.stream(StoreReadRequest.fresh(3)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.mobilenativefoundation.store.store5
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
@ -17,6 +18,7 @@ import org.mobilenativefoundation.store.store5.util.asSourceOfTruth
|
||||||
import org.mobilenativefoundation.store.store5.util.assertEmitsExactly
|
import org.mobilenativefoundation.store.store5.util.assertEmitsExactly
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@FlowPreview
|
@FlowPreview
|
||||||
class SourceOfTruthErrorsTests {
|
class SourceOfTruthErrorsTests {
|
||||||
private val testScope = TestScope()
|
private val testScope = TestScope()
|
||||||
|
@ -29,7 +31,7 @@ class SourceOfTruthErrorsTests {
|
||||||
3 to "b"
|
3 to "b"
|
||||||
)
|
)
|
||||||
val pipeline = StoreBuilder
|
val pipeline = StoreBuilder
|
||||||
.from(
|
.from<Int, String, String, String>(
|
||||||
fetcher = fetcher,
|
fetcher = fetcher,
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
)
|
)
|
||||||
|
@ -40,16 +42,16 @@ class SourceOfTruthErrorsTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.fresh(3)),
|
pipeline.stream(StoreReadRequest.fresh(3)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(ResponseOrigin.Fetcher),
|
StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher),
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
error = WriteException(
|
error = WriteException(
|
||||||
key = 3,
|
key = 3,
|
||||||
value = "a",
|
value = "a",
|
||||||
cause = TestException("i fail")
|
cause = TestException("i fail")
|
||||||
),
|
),
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -63,7 +65,7 @@ class SourceOfTruthErrorsTests {
|
||||||
3 to "b"
|
3 to "b"
|
||||||
)
|
)
|
||||||
val pipeline = StoreBuilder
|
val pipeline = StoreBuilder
|
||||||
.from(
|
.from<Int, String, String, String>(
|
||||||
fetcher = fetcher,
|
fetcher = fetcher,
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
)
|
)
|
||||||
|
@ -75,27 +77,27 @@ class SourceOfTruthErrorsTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = false)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = false)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
error = ReadException(
|
error = ReadException(
|
||||||
key = 3,
|
key = 3,
|
||||||
cause = TestException("null")
|
cause = TestException("null")
|
||||||
),
|
),
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
// after disk fails, we should still invoke fetcher
|
// after disk fails, we should still invoke fetcher
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
// and after fetcher writes the value, it will trigger another read which will also
|
// and after fetcher writes the value, it will trigger another read which will also
|
||||||
// fail
|
// fail
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
error = ReadException(
|
error = ReadException(
|
||||||
key = 3,
|
key = 3,
|
||||||
cause = TestException("a")
|
cause = TestException("a")
|
||||||
),
|
),
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -108,7 +110,7 @@ class SourceOfTruthErrorsTests {
|
||||||
flowOf("a", "b", "c", "d")
|
flowOf("a", "b", "c", "d")
|
||||||
}
|
}
|
||||||
val pipeline = StoreBuilder
|
val pipeline = StoreBuilder
|
||||||
.from(
|
.from<Int, String, String, String>(
|
||||||
fetcher = fetcher,
|
fetcher = fetcher,
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
)
|
)
|
||||||
|
@ -122,107 +124,107 @@ class SourceOfTruthErrorsTests {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
error = WriteException(
|
error = WriteException(
|
||||||
key = 3,
|
key = 3,
|
||||||
value = "a",
|
value = "a",
|
||||||
cause = TestException("a")
|
cause = TestException("a")
|
||||||
),
|
),
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "b",
|
value = "b",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
error = WriteException(
|
error = WriteException(
|
||||||
key = 3,
|
key = 3,
|
||||||
value = "c",
|
value = "c",
|
||||||
cause = TestException("c")
|
cause = TestException("c")
|
||||||
),
|
),
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
// disk flow will restart after a failed write (because we stopped it before the
|
// disk flow will restart after a failed write (because we stopped it before the
|
||||||
// write attempt starts, so we will get the disk value again).
|
// write attempt starts, so we will get the disk value again).
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "b",
|
value = "b",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "d",
|
value = "d",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// @Test
|
||||||
fun givenSourceOfTruthWithFailingWriteWhenAPassiveReaderArrivesThenItShouldReceiveTheNewWriteError() = testScope.runTest {
|
// fun givenSourceOfTruthWithFailingWriteWhenAPassiveReaderArrivesThenItShouldReceiveTheNewWriteError() = testScope.runTest {
|
||||||
val persister = InMemoryPersister<Int, String>()
|
// val persister = InMemoryPersister<Int, String>()
|
||||||
val fetcher = Fetcher.ofFlow { _: Int ->
|
// val fetcher = Fetcher.ofFlow { _: Int ->
|
||||||
flowOf("a", "b", "c", "d")
|
// flowOf("a", "b", "c", "d")
|
||||||
}
|
// }
|
||||||
val pipeline = StoreBuilder
|
// val pipeline = StoreBuilder
|
||||||
.from(
|
// .from<Int, String>(
|
||||||
fetcher = fetcher,
|
// fetcher = fetcher,
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
// sourceOfTruth = persister.asSourceOfTruth()
|
||||||
)
|
// )
|
||||||
.disableCache()
|
// .disableCache()
|
||||||
.scope(testScope)
|
// .scope(testScope)
|
||||||
.build()
|
// .build()
|
||||||
persister.preWriteCallback = { _, value ->
|
// persister.preWriteCallback = { _, value ->
|
||||||
if (value in listOf("a", "c")) {
|
// if (value in listOf("a", "c")) {
|
||||||
delay(50)
|
// delay(50)
|
||||||
throw TestException(value)
|
// throw TestException(value)
|
||||||
} else {
|
// } else {
|
||||||
delay(10)
|
// delay(10)
|
||||||
}
|
// }
|
||||||
value
|
// value
|
||||||
}
|
// }
|
||||||
// keep collection hot
|
// // keep collection hot
|
||||||
val collector = launch {
|
// val collector = launch {
|
||||||
pipeline.stream(
|
// pipeline.stream(
|
||||||
StoreRequest.cached(3, refresh = true)
|
// StoreReadRequest.cached(3, refresh = true)
|
||||||
).toList()
|
// ).toList()
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// miss writes for a and b and let the write operation for c start such that
|
// // miss writes for a and b and let the write operation for c start such that
|
||||||
// we'll catch that write error
|
// // we'll catch that write error
|
||||||
delay(70)
|
// delay(70)
|
||||||
assertEmitsExactly(
|
// assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
// pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
// listOf(
|
||||||
// we wanted the disk value but write failed so we don't get it
|
// // we wanted the disk value but write failed so we don't get it
|
||||||
StoreResponse.Error.Exception(
|
// StoreReadResponse.Error.Exception(
|
||||||
error = WriteException(
|
// error = WriteException(
|
||||||
key = 3,
|
// key = 3,
|
||||||
value = "c",
|
// value = "c",
|
||||||
cause = TestException("c")
|
// cause = TestException("c")
|
||||||
),
|
// ),
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
// origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
// ),
|
||||||
// after the write error, we should get the value on disk
|
// // after the write error, we should get the value on disk
|
||||||
StoreResponse.Data(
|
// StoreReadResponse.Data(
|
||||||
value = "b",
|
// value = "b",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
// origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
// ),
|
||||||
// now we'll unlock the fetcher after disk is read
|
// // now we'll unlock the fetcher after disk is read
|
||||||
StoreResponse.Loading(
|
// StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
// origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
// ),
|
||||||
StoreResponse.Data(
|
// StoreReadResponse.Data(
|
||||||
value = "d",
|
// value = "d",
|
||||||
origin = ResponseOrigin.Fetcher
|
// origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
collector.cancelAndJoin()
|
// collector.cancelAndJoin()
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenSourceOfTruthWithFailingWriteWhenAPassiveReaderArrivesThenItShouldNotGetErrorsHappenedBefore() = testScope.runTest {
|
fun givenSourceOfTruthWithFailingWriteWhenAPassiveReaderArrivesThenItShouldNotGetErrorsHappenedBefore() = testScope.runTest {
|
||||||
|
@ -238,7 +240,7 @@ class SourceOfTruthErrorsTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val pipeline = StoreBuilder
|
val pipeline = StoreBuilder
|
||||||
.from(
|
.from<Int, String, String, String>(
|
||||||
fetcher = fetcher,
|
fetcher = fetcher,
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
)
|
)
|
||||||
|
@ -253,78 +255,78 @@ class SourceOfTruthErrorsTests {
|
||||||
}
|
}
|
||||||
val collector = launch {
|
val collector = launch {
|
||||||
pipeline.stream(
|
pipeline.stream(
|
||||||
StoreRequest.cached(3, refresh = true)
|
StoreReadRequest.cached(3, refresh = true)
|
||||||
).toList() // keep collection hot
|
).toList() // keep collection hot
|
||||||
}
|
}
|
||||||
|
|
||||||
// miss both failures but arrive before d is fetched
|
// miss both failures but arrive before d is fetched
|
||||||
delay(70)
|
delay(70)
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.skipMemory(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.skipMemory(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "b",
|
value = "b",
|
||||||
origin = ResponseOrigin.SourceOfTruth
|
origin = StoreReadResponseOrigin.SourceOfTruth
|
||||||
),
|
),
|
||||||
// don't receive the write exception because technically it started before we
|
// don't receive the write exception because technically it started before we
|
||||||
// started reading
|
// started reading
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "d",
|
value = "d",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
collector.cancelAndJoin()
|
collector.cancelAndJoin()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// @Test
|
||||||
fun givenSourceOfTruthWithFailingWriteWhenAFreshValueReaderArrivesThenItShouldNotGetDiskErrorsFromAPendingWrite() = testScope.runTest {
|
// fun givenSourceOfTruthWithFailingWriteWhenAFreshValueReaderArrivesThenItShouldNotGetDiskErrorsFromAPendingWrite() = testScope.runTest {
|
||||||
val persister = InMemoryPersister<Int, String>()
|
// val persister = InMemoryPersister<Int, String>()
|
||||||
val fetcher = Fetcher.ofFlow<Int, String> {
|
// val fetcher = Fetcher.ofFlow<Int, String> {
|
||||||
flowOf("a", "b", "c", "d")
|
// flowOf("a", "b", "c", "d")
|
||||||
}
|
// }
|
||||||
val pipeline = StoreBuilder
|
// val pipeline = StoreBuilder
|
||||||
.from(
|
// .from<Int, String>(
|
||||||
fetcher = fetcher,
|
// fetcher = fetcher,
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
// sourceOfTruth = persister.asSourceOfTruth()
|
||||||
)
|
// )
|
||||||
.disableCache()
|
// .disableCache()
|
||||||
.scope(testScope)
|
// .scope(testScope)
|
||||||
.build()
|
// .build()
|
||||||
persister.preWriteCallback = { _, value ->
|
// persister.preWriteCallback = { _, value ->
|
||||||
if (value == "c") {
|
// if (value == "c") {
|
||||||
// slow down read so that the new reader arrives
|
// // slow down read so that the new reader arrives
|
||||||
delay(50)
|
// delay(50)
|
||||||
}
|
// }
|
||||||
if (value in listOf("a", "c")) {
|
// if (value in listOf("a", "c")) {
|
||||||
throw TestException(value)
|
// throw TestException(value)
|
||||||
}
|
// }
|
||||||
value
|
// value
|
||||||
}
|
// }
|
||||||
val collector = launch {
|
// val collector = launch {
|
||||||
pipeline.stream(
|
// pipeline.stream(
|
||||||
StoreRequest.cached(3, refresh = true)
|
// StoreReadRequest.cached(3, refresh = true)
|
||||||
).toList() // keep collection hot
|
// ).toList() // keep collection hot
|
||||||
}
|
// }
|
||||||
// miss both failures but arrive before d is fetched
|
// // miss both failures but arrive before d is fetched
|
||||||
delay(20)
|
// delay(20)
|
||||||
assertEmitsExactly(
|
// assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.fresh(3)),
|
// pipeline.stream(StoreReadRequest.fresh(3)),
|
||||||
listOf(
|
// listOf(
|
||||||
StoreResponse.Loading(
|
// StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
// origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
// ),
|
||||||
StoreResponse.Data(
|
// StoreReadResponse.Data(
|
||||||
value = "d",
|
// value = "d",
|
||||||
origin = ResponseOrigin.Fetcher
|
// origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
)
|
// )
|
||||||
collector.cancelAndJoin()
|
// collector.cancelAndJoin()
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenSourceOfTruthWithReadFailureWhenCachedValueReaderArrivesThenFetcherShouldBeCalledToGetANewValue() {
|
fun givenSourceOfTruthWithReadFailureWhenCachedValueReaderArrivesThenFetcherShouldBeCalledToGetANewValue() {
|
||||||
|
@ -332,7 +334,7 @@ class SourceOfTruthErrorsTests {
|
||||||
val persister = InMemoryPersister<Int, String>()
|
val persister = InMemoryPersister<Int, String>()
|
||||||
val fetcher = Fetcher.of { _: Int -> "a" }
|
val fetcher = Fetcher.of { _: Int -> "a" }
|
||||||
val pipeline = StoreBuilder
|
val pipeline = StoreBuilder
|
||||||
.from(
|
.from<Int, String, String, String>(
|
||||||
fetcher = fetcher,
|
fetcher = fetcher,
|
||||||
sourceOfTruth = persister.asSourceOfTruth()
|
sourceOfTruth = persister.asSourceOfTruth()
|
||||||
)
|
)
|
||||||
|
@ -346,21 +348,21 @@ class SourceOfTruthErrorsTests {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.cached(3, refresh = true)),
|
pipeline.stream(StoreReadRequest.cached(3, refresh = true)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
error = ReadException(
|
error = ReadException(
|
||||||
key = 3,
|
key = 3,
|
||||||
cause = TestException("first read")
|
cause = TestException("first read")
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "a",
|
value = "a",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -42,7 +42,7 @@ import kotlin.test.assertNull
|
||||||
class SourceOfTruthWithBarrierTests {
|
class SourceOfTruthWithBarrierTests {
|
||||||
private val testScope = TestScope()
|
private val testScope = TestScope()
|
||||||
private val persister = InMemoryPersister<Int, String>()
|
private val persister = InMemoryPersister<Int, String>()
|
||||||
private val delegate: SourceOfTruth<Int, String, String> =
|
private val delegate: SourceOfTruth<Int, String> =
|
||||||
PersistentSourceOfTruth(
|
PersistentSourceOfTruth(
|
||||||
realReader = { key ->
|
realReader = { key ->
|
||||||
flow {
|
flow {
|
||||||
|
@ -53,13 +53,13 @@ class SourceOfTruthWithBarrierTests {
|
||||||
realDelete = persister::deleteByKey,
|
realDelete = persister::deleteByKey,
|
||||||
realDeleteAll = persister::deleteAll
|
realDeleteAll = persister::deleteAll
|
||||||
)
|
)
|
||||||
private val source = SourceOfTruthWithBarrier(
|
private val source = SourceOfTruthWithBarrier<Int, String, String, String>(
|
||||||
delegate = delegate
|
delegate = delegate
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun simple() = testScope.runTest {
|
fun simple() = testScope.runTest {
|
||||||
val collection = mutableListOf<StoreResponse<String?>>()
|
val collection = mutableListOf<StoreReadResponse<String?>>()
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
source.reader(1, CompletableDeferred(Unit)).take(2).collect {
|
source.reader(1, CompletableDeferred(Unit)).take(2).collect {
|
||||||
|
@ -70,13 +70,13 @@ class SourceOfTruthWithBarrierTests {
|
||||||
source.write(1, "a")
|
source.write(1, "a")
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf<StoreResponse<String?>>(
|
listOf<StoreReadResponse<String?>>(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
value = null
|
value = null
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = "a"
|
value = "a"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -103,7 +103,7 @@ class SourceOfTruthWithBarrierTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun preAndPostWrites() = testScope.runTest {
|
fun preAndPostWrites() = testScope.runTest {
|
||||||
val collection = mutableListOf<StoreResponse<String?>>()
|
val collection = mutableListOf<StoreReadResponse<String?>>()
|
||||||
source.write(1, "a")
|
source.write(1, "a")
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
|
@ -119,13 +119,13 @@ class SourceOfTruthWithBarrierTests {
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf<StoreResponse<String?>>(
|
listOf<StoreReadResponse<String?>>(
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
value = "a"
|
value = "a"
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = "b"
|
value = "b"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -144,8 +144,8 @@ class SourceOfTruthWithBarrierTests {
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
source.reader(1, CompletableDeferred(Unit)),
|
source.reader(1, CompletableDeferred(Unit)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
error = ReadException(
|
error = ReadException(
|
||||||
key = 1,
|
key = 1,
|
||||||
cause = exception
|
cause = exception
|
||||||
|
@ -167,7 +167,7 @@ class SourceOfTruthWithBarrierTests {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
val reader = source.reader(1, CompletableDeferred(Unit))
|
val reader = source.reader(1, CompletableDeferred(Unit))
|
||||||
val collected = mutableListOf<StoreResponse<String?>>()
|
val collected = mutableListOf<StoreReadResponse<String?>>()
|
||||||
val collection = async {
|
val collection = async {
|
||||||
reader.collect {
|
reader.collect {
|
||||||
collected.add(it)
|
collected.add(it)
|
||||||
|
@ -175,8 +175,8 @@ class SourceOfTruthWithBarrierTests {
|
||||||
}
|
}
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
error = ReadException(
|
error = ReadException(
|
||||||
key = 1,
|
key = 1,
|
||||||
cause = exception
|
cause = exception
|
||||||
|
@ -190,17 +190,17 @@ class SourceOfTruthWithBarrierTests {
|
||||||
source.write(1, "a")
|
source.write(1, "a")
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf<StoreResponse<String?>>(
|
listOf<StoreReadResponse<String?>>(
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
error = ReadException(
|
error = ReadException(
|
||||||
key = 1,
|
key = 1,
|
||||||
cause = exception
|
cause = exception
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
// this is fetcher since we are using the write API
|
// this is fetcher since we are using the write API
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = "a"
|
value = "a"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -221,7 +221,7 @@ class SourceOfTruthWithBarrierTests {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
val reader = source.reader(1, CompletableDeferred(Unit))
|
val reader = source.reader(1, CompletableDeferred(Unit))
|
||||||
val collected = mutableListOf<StoreResponse<String?>>()
|
val collected = mutableListOf<StoreReadResponse<String?>>()
|
||||||
val collection = async {
|
val collection = async {
|
||||||
reader.collect {
|
reader.collect {
|
||||||
collected.add(it)
|
collected.add(it)
|
||||||
|
@ -233,20 +233,20 @@ class SourceOfTruthWithBarrierTests {
|
||||||
// make sure collection does not cancel for a write error
|
// make sure collection does not cancel for a write error
|
||||||
assertEquals(true, collection.isActive)
|
assertEquals(true, collection.isActive)
|
||||||
val eventsUntilFailure = listOf(
|
val eventsUntilFailure = listOf(
|
||||||
StoreResponse.Data<String?>(
|
StoreReadResponse.Data<String?>(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
value = null
|
value = null
|
||||||
),
|
),
|
||||||
StoreResponse.Error.Exception(
|
StoreReadResponse.Error.Exception(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
error = WriteException(
|
error = WriteException(
|
||||||
key = 1,
|
key = 1,
|
||||||
value = failValue,
|
value = failValue,
|
||||||
cause = exception
|
cause = exception
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
StoreResponse.Data<String?>(
|
StoreReadResponse.Data<String?>(
|
||||||
origin = ResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
value = null
|
value = null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -257,8 +257,8 @@ class SourceOfTruthWithBarrierTests {
|
||||||
source.write(1, "succeed")
|
source.write(1, "succeed")
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
assertEquals(
|
assertEquals(
|
||||||
eventsUntilFailure + StoreResponse.Data<String?>(
|
eventsUntilFailure + StoreReadResponse.Data<String?>(
|
||||||
origin = ResponseOrigin.Fetcher,
|
origin = StoreReadResponseOrigin.Fetcher,
|
||||||
value = "succeed"
|
value = "succeed"
|
||||||
),
|
),
|
||||||
collected
|
collected
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
class StoreReadResponseTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun requireData() {
|
||||||
|
assertEquals("Foo", StoreReadResponse.Data("Foo", StoreReadResponseOrigin.Fetcher).requireData())
|
||||||
|
|
||||||
|
// should throw
|
||||||
|
assertFailsWith<NullPointerException> {
|
||||||
|
StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher).requireData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun throwIfErrorException() {
|
||||||
|
assertFailsWith<Exception> {
|
||||||
|
StoreReadResponse.Error.Exception(Exception(), StoreReadResponseOrigin.Fetcher).throwIfError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun throwIfErrorMessage() {
|
||||||
|
assertFailsWith<RuntimeException> {
|
||||||
|
StoreReadResponse.Error.Message("test error", StoreReadResponseOrigin.Fetcher).throwIfError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun errorMessageOrNull() {
|
||||||
|
assertFailsWith<Exception>(message = Exception::class.toString()) {
|
||||||
|
StoreReadResponse.Error.Exception(Exception(), StoreReadResponseOrigin.Fetcher).throwIfError()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFailsWith<Exception>(message = "test error message") {
|
||||||
|
StoreReadResponse.Error.Message("test error message", StoreReadResponseOrigin.Fetcher).throwIfError()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNull(StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher).errorMessageOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun swapType() {
|
||||||
|
assertFailsWith<RuntimeException> {
|
||||||
|
StoreReadResponse.Data("Foo", StoreReadResponseOrigin.Fetcher).swapType<String>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,53 +0,0 @@
|
||||||
package org.mobilenativefoundation.store.store5
|
|
||||||
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
import kotlin.test.assertNull
|
|
||||||
|
|
||||||
class StoreResponseTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun requireData() {
|
|
||||||
assertEquals("Foo", StoreResponse.Data("Foo", ResponseOrigin.Fetcher).requireData())
|
|
||||||
|
|
||||||
// should throw
|
|
||||||
assertFailsWith<NullPointerException> {
|
|
||||||
StoreResponse.Loading(ResponseOrigin.Fetcher).requireData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun throwIfErrorException() {
|
|
||||||
assertFailsWith<Exception> {
|
|
||||||
StoreResponse.Error.Exception(Exception(), ResponseOrigin.Fetcher).throwIfError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun throwIfErrorMessage() {
|
|
||||||
assertFailsWith<RuntimeException> {
|
|
||||||
StoreResponse.Error.Message("test error", ResponseOrigin.Fetcher).throwIfError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test()
|
|
||||||
fun errorMessageOrNull() {
|
|
||||||
assertFailsWith<Exception>(message = Exception::class.toString()) {
|
|
||||||
StoreResponse.Error.Exception(Exception(), ResponseOrigin.Fetcher).throwIfError()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertFailsWith<Exception>(message = "test error message") {
|
|
||||||
StoreResponse.Error.Message("test error message", ResponseOrigin.Fetcher).throwIfError()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertNull(StoreResponse.Loading(ResponseOrigin.Fetcher).errorMessageOrNull())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun swapType() {
|
|
||||||
assertFailsWith<RuntimeException> {
|
|
||||||
StoreResponse.Data("Foo", ResponseOrigin.Fetcher).swapType<String>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,13 +4,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.extensions.get
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.Duration.Companion.hours
|
||||||
import kotlin.time.minutes
|
|
||||||
|
|
||||||
@FlowPreview
|
@FlowPreview
|
||||||
@ExperimentalTime
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
class StoreWithInMemoryCacheTests {
|
class StoreWithInMemoryCacheTests {
|
||||||
private val testScope = TestScope()
|
private val testScope = TestScope()
|
||||||
|
@ -18,11 +17,11 @@ class StoreWithInMemoryCacheTests {
|
||||||
@Test
|
@Test
|
||||||
fun storeRequestsCanCompleteWhenInMemoryCacheWithAccessExpiryIsAtTheMaximumSize() = testScope.runTest {
|
fun storeRequestsCanCompleteWhenInMemoryCacheWithAccessExpiryIsAtTheMaximumSize() = testScope.runTest {
|
||||||
val store = StoreBuilder
|
val store = StoreBuilder
|
||||||
.from(Fetcher.of { _: Int -> "result" })
|
.from<Int, String, String>(Fetcher.of { _: Int -> "result" })
|
||||||
.cachePolicy(
|
.cachePolicy(
|
||||||
MemoryPolicy
|
MemoryPolicy
|
||||||
.builder<Any, Any>()
|
.builder<Any, Any>()
|
||||||
.setExpireAfterAccess(10.minutes)
|
.setExpireAfterAccess(1.hours)
|
||||||
.setMaxSize(1)
|
.setMaxSize(1)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,40 +24,40 @@ class StreamWithoutSourceOfTruthTests {
|
||||||
3 to "three-1",
|
3 to "three-1",
|
||||||
3 to "three-2"
|
3 to "three-2"
|
||||||
)
|
)
|
||||||
val pipeline = StoreBuilder.from(fetcher)
|
val pipeline = StoreBuilder.from<Int, String, String>(fetcher)
|
||||||
.scope(testScope)
|
.scope(testScope)
|
||||||
.build()
|
.build()
|
||||||
val twoItemsNoRefresh = async {
|
val twoItemsNoRefresh = async {
|
||||||
pipeline.stream(
|
pipeline.stream(
|
||||||
StoreRequest.cached(3, refresh = false)
|
StoreReadRequest.cached(3, refresh = false)
|
||||||
).take(3).toList()
|
).take(3).toList()
|
||||||
}
|
}
|
||||||
delay(1_000) // make sure the async block starts first
|
delay(1_000) // make sure the async block starts first
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.fresh(3)),
|
pipeline.stream(StoreReadRequest.fresh(3)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
twoItemsNoRefresh.await()
|
twoItemsNoRefresh.await()
|
||||||
|
@ -70,41 +70,41 @@ class StreamWithoutSourceOfTruthTests {
|
||||||
3 to "three-1",
|
3 to "three-1",
|
||||||
3 to "three-2"
|
3 to "three-2"
|
||||||
)
|
)
|
||||||
val pipeline = StoreBuilder.from(fetcher)
|
val pipeline = StoreBuilder.from<Int, String, String>(fetcher)
|
||||||
.scope(testScope)
|
.scope(testScope)
|
||||||
.disableCache()
|
.disableCache()
|
||||||
.build()
|
.build()
|
||||||
val twoItemsNoRefresh = async {
|
val twoItemsNoRefresh = async {
|
||||||
pipeline.stream(
|
pipeline.stream(
|
||||||
StoreRequest.cached(3, refresh = false)
|
StoreReadRequest.cached(3, refresh = false)
|
||||||
).take(3).toList()
|
).take(3).toList()
|
||||||
}
|
}
|
||||||
delay(1_000) // make sure the async block starts first
|
delay(1_000) // make sure the async block starts first
|
||||||
assertEmitsExactly(
|
assertEmitsExactly(
|
||||||
pipeline.stream(StoreRequest.fresh(3)),
|
pipeline.stream(StoreReadRequest.fresh(3)),
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(
|
listOf(
|
||||||
StoreResponse.Loading(
|
StoreReadResponse.Loading(
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "three-1",
|
value = "three-1",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
),
|
),
|
||||||
StoreResponse.Data(
|
StoreReadResponse.Data(
|
||||||
value = "three-2",
|
value = "three-2",
|
||||||
origin = ResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
twoItemsNoRefresh.await()
|
twoItemsNoRefresh.await()
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore
|
||||||
|
import org.mobilenativefoundation.store.store5.util.fake.NoteApi
|
||||||
|
import org.mobilenativefoundation.store.store5.util.fake.NoteBookkeeping
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.Note
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.NoteCommonRepresentation
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.NoteData
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.NoteNetworkRepresentation
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.NoteNetworkWriteResponse
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.NoteSourceOfTruthRepresentation
|
||||||
|
import kotlin.test.BeforeTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalStoreApi::class)
|
||||||
|
class UpdaterTests {
|
||||||
|
private val testScope = TestScope()
|
||||||
|
private lateinit var api: NoteApi
|
||||||
|
private lateinit var bookkeeping: NoteBookkeeping
|
||||||
|
|
||||||
|
@BeforeTest
|
||||||
|
fun before() {
|
||||||
|
api = NoteApi()
|
||||||
|
bookkeeping = NoteBookkeeping()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenEmptyMarketWhenWriteThenSuccessResponsesAndApiUpdated() = testScope.runTest {
|
||||||
|
val updater = Updater.by<String, NoteCommonRepresentation, NoteNetworkWriteResponse>(
|
||||||
|
post = { key, commonRepresentation ->
|
||||||
|
val networkWriteResponse = api.post(key, commonRepresentation)
|
||||||
|
if (networkWriteResponse.ok) {
|
||||||
|
UpdaterResult.Success.Typed(networkWriteResponse)
|
||||||
|
} else {
|
||||||
|
UpdaterResult.Error.Message("Failed to sync")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val bookkeeper = Bookkeeper.by(
|
||||||
|
getLastFailedSync = bookkeeping::getLastFailedSync,
|
||||||
|
setLastFailedSync = bookkeeping::setLastFailedSync,
|
||||||
|
clear = bookkeeping::clear,
|
||||||
|
clearAll = bookkeeping::clear
|
||||||
|
)
|
||||||
|
|
||||||
|
val store = StoreBuilder.from<String, NoteNetworkRepresentation, NoteCommonRepresentation>(
|
||||||
|
fetcher = Fetcher.ofFlow { key ->
|
||||||
|
val networkRepresentation = NoteNetworkRepresentation(NoteData.Single(Note("$key-id", "$key-title", "$key-content")))
|
||||||
|
flow { emit(networkRepresentation) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.asMutableStore<String, NoteNetworkRepresentation, NoteCommonRepresentation, NoteSourceOfTruthRepresentation, NoteNetworkWriteResponse>(
|
||||||
|
updater = updater,
|
||||||
|
bookkeeper = bookkeeper
|
||||||
|
)
|
||||||
|
|
||||||
|
val noteKey = "1-id"
|
||||||
|
val noteTitle = "1-title"
|
||||||
|
val noteContent = "1-content"
|
||||||
|
val noteData = NoteData.Single(Note(noteKey, noteTitle, noteContent))
|
||||||
|
val writeRequest = StoreWriteRequest.of<String, NoteCommonRepresentation, NoteNetworkWriteResponse>(
|
||||||
|
key = noteKey,
|
||||||
|
input = NoteCommonRepresentation(noteData)
|
||||||
|
)
|
||||||
|
|
||||||
|
val storeWriteResponse = store.write(writeRequest)
|
||||||
|
|
||||||
|
assertEquals(StoreWriteResponse.Success.Typed(NoteNetworkWriteResponse(noteKey, true)), storeWriteResponse)
|
||||||
|
assertEquals(NoteNetworkRepresentation(noteData), api.db[noteKey])
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,9 +12,9 @@ import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||||
/**
|
/**
|
||||||
* Only used in FlowStoreTest. We should get rid of it eventually.
|
* Only used in FlowStoreTest. We should get rid of it eventually.
|
||||||
*/
|
*/
|
||||||
class SimplePersisterAsFlowable<Key, Input, Output>(
|
class SimplePersisterAsFlowable<Key : Any, SourceOfTruthRepresentation : Any>(
|
||||||
private val reader: suspend (Key) -> Output?,
|
private val reader: suspend (Key) -> SourceOfTruthRepresentation?,
|
||||||
private val writer: suspend (Key, Input) -> Unit,
|
private val writer: suspend (Key, SourceOfTruthRepresentation) -> Unit,
|
||||||
private val delete: (suspend (Key) -> Unit)? = null
|
private val delete: (suspend (Key) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -23,13 +23,13 @@ class SimplePersisterAsFlowable<Key, Input, Output>(
|
||||||
|
|
||||||
private val versionTracker = KeyTracker<Key>()
|
private val versionTracker = KeyTracker<Key>()
|
||||||
|
|
||||||
fun flowReader(key: Key): Flow<Output?> = flow {
|
fun flowReader(key: Key): Flow<SourceOfTruthRepresentation?> = flow {
|
||||||
versionTracker.keyFlow(key).collect {
|
versionTracker.keyFlow(key).collect {
|
||||||
emit(reader(key))
|
emit(reader(key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun flowWriter(key: Key, input: Input) {
|
suspend fun flowWriter(key: Key, input: SourceOfTruthRepresentation) {
|
||||||
writer(key, input)
|
writer(key, input)
|
||||||
versionTracker.invalidate(key)
|
versionTracker.invalidate(key)
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ class SimplePersisterAsFlowable<Key, Input, Output>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <Key : Any, Input : Any, Output : Any> SimplePersisterAsFlowable<Key, Input, Output>.asSourceOfTruth() =
|
fun <Key : Any, SourceOfTruthRepresentation : Any> SimplePersisterAsFlowable<Key, SourceOfTruthRepresentation>.asSourceOfTruth() =
|
||||||
SourceOfTruth.of(
|
SourceOfTruth.of(
|
||||||
reader = ::flowReader,
|
reader = ::flowReader,
|
||||||
writer = ::flowWriter,
|
writer = ::flowWriter,
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.util
|
||||||
|
|
||||||
|
internal interface TestApi<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> {
|
||||||
|
fun get(key: Key, fail: Boolean = false): NetworkRepresentation?
|
||||||
|
fun post(key: Key, value: CommonRepresentation, fail: Boolean = false): NetworkWriteResponse
|
||||||
|
}
|
|
@ -3,18 +3,21 @@ package org.mobilenativefoundation.store.store5.util
|
||||||
import kotlinx.coroutines.flow.filterNot
|
import kotlinx.coroutines.flow.filterNot
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import org.mobilenativefoundation.store.store5.Store
|
import org.mobilenativefoundation.store.store5.Store
|
||||||
import org.mobilenativefoundation.store.store5.StoreRequest
|
import org.mobilenativefoundation.store.store5.StoreReadRequest
|
||||||
import org.mobilenativefoundation.store.store5.StoreResponse
|
import org.mobilenativefoundation.store.store5.StoreReadResponse
|
||||||
|
import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper factory that will return [StoreResponse.Data] for [key]
|
* Helper factory that will return [StoreReadResponse.Data] for [key]
|
||||||
* if it is cached otherwise will return fresh/network data (updating your caches)
|
* if it is cached otherwise will return fresh/network data (updating your caches)
|
||||||
*/
|
*/
|
||||||
suspend fun <Key : Any, Output : Any> Store<Key, Output>.getData(key: Key) =
|
suspend fun <Key : Any, CommonRepresentation : Any> Store<Key, CommonRepresentation>.getData(key: Key) =
|
||||||
stream(
|
stream(
|
||||||
StoreRequest.cached(key, refresh = false)
|
StoreReadRequest.cached(key, refresh = false)
|
||||||
).filterNot {
|
).filterNot {
|
||||||
it is StoreResponse.Loading
|
it is StoreReadResponse.Loading
|
||||||
|
}.mapIndexed { index, value ->
|
||||||
|
value
|
||||||
}.first().let {
|
}.first().let {
|
||||||
StoreResponse.Data(it.requireData(), it.origin)
|
StoreReadResponse.Data(it.requireData(), it.origin)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.util.fake
|
||||||
|
|
||||||
|
import org.mobilenativefoundation.store.store5.util.TestApi
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.Note
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.NoteCommonRepresentation
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.NoteData
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.NoteNetworkRepresentation
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.NoteNetworkWriteResponse
|
||||||
|
|
||||||
|
internal class NoteApi : TestApi<String, NoteNetworkRepresentation, NoteCommonRepresentation, NoteNetworkWriteResponse> {
|
||||||
|
internal val db = mutableMapOf<String, NoteNetworkRepresentation>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
seed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(key: String, fail: Boolean): NoteNetworkRepresentation? {
|
||||||
|
if (fail) {
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
|
||||||
|
return db[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun post(key: String, value: NoteCommonRepresentation, fail: Boolean): NoteNetworkWriteResponse {
|
||||||
|
if (fail) {
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
|
||||||
|
db[key] = NoteNetworkRepresentation(value.data)
|
||||||
|
|
||||||
|
return NoteNetworkWriteResponse(key, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun seed() {
|
||||||
|
db["1-id"] = NoteNetworkRepresentation(NoteData.Single(Note("1-id", "1-title", "1-content")))
|
||||||
|
db["2-id"] = NoteNetworkRepresentation(NoteData.Single(Note("2-id", "2-title", "2-content")))
|
||||||
|
db["3-id"] = NoteNetworkRepresentation(NoteData.Single(Note("3-id", "3-title", "3-content")))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.util.fake
|
||||||
|
|
||||||
|
class NoteBookkeeping {
|
||||||
|
private val log: MutableMap<String, Long?> = mutableMapOf()
|
||||||
|
fun setLastFailedSync(key: String, timestamp: Long, fail: Boolean = false): Boolean {
|
||||||
|
if (fail) {
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
log[key] = timestamp
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastFailedSync(key: String, fail: Boolean = false): Long? {
|
||||||
|
if (fail) {
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
|
||||||
|
return log[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear(key: String, fail: Boolean = false): Boolean {
|
||||||
|
if (fail) {
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
log.remove(key)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear(fail: Boolean = false): Boolean {
|
||||||
|
if (fail) {
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
log.clear()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package org.mobilenativefoundation.store.store5.util.model
|
||||||
|
|
||||||
|
internal sealed class NoteData {
|
||||||
|
data class Single(val item: Note) : NoteData()
|
||||||
|
data class Collection(val items: List<Note>) : NoteData()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class NoteNetworkWriteResponse(
|
||||||
|
val key: String,
|
||||||
|
val ok: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class NoteNetworkRepresentation(
|
||||||
|
val data: NoteData? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class NoteCommonRepresentation(
|
||||||
|
val data: NoteData? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class NoteSourceOfTruthRepresentation(
|
||||||
|
val data: NoteData? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class Note(
|
||||||
|
val id: String,
|
||||||
|
val title: String,
|
||||||
|
val content: String
|
||||||
|
)
|
Loading…
Reference in a new issue