* Remove "Representation" from generics

Signed-off-by: mramotar <mramotar@dropbox.com>

* Remove "Representation" from generics

Signed-off-by: mramotar <mramotar@dropbox.com>

* Rename to Validator

Signed-off-by: mramotar <mramotar@dropbox.com>

* Add logo!

Signed-off-by: mramotar <mramotar@dropbox.com>

* Update README.md

Signed-off-by: mramotar <mramotar@dropbox.com>

* Format

Signed-off-by: mramotar <mramotar@dropbox.com>

* Update CI

Signed-off-by: Matt <mramotar@dropbox.com>

* Remove "Representation" from generics

Signed-off-by: mramotar <mramotar@dropbox.com>

* Remove "Representation" from generics

Signed-off-by: mramotar <mramotar@dropbox.com>

* Rename to Validator

Signed-off-by: mramotar <mramotar@dropbox.com>

* Add logo!

Signed-off-by: mramotar <mramotar@dropbox.com>

* Update README.md

Signed-off-by: mramotar <mramotar@dropbox.com>

* Format

Signed-off-by: mramotar <mramotar@dropbox.com>

* Rename generics

Signed-off-by: mramotar <mramotar@dropbox.com>

* Rename input to value

Signed-off-by: mramotar <mramotar@dropbox.com>

* Update README

Signed-off-by: mramotar <mramotar@dropbox.com>

* Update README

Signed-off-by: mramotar <mramotar@dropbox.com>

Signed-off-by: mramotar <mramotar@dropbox.com>
Signed-off-by: Matt <mramotar@dropbox.com>
This commit is contained in:
Matt 2022-12-23 14:04:24 -05:00 committed by mnakhimovich
parent 855a7bbc97
commit ae07e0672c
45 changed files with 456 additions and 700 deletions

BIN
Images/friendly_robot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

387
README.md
View file

@ -1,362 +1,123 @@
# Store 5 <div align="center">
<img src="Images/friendly_robot.png" width="120"/>
<h1 style="font-size:48px">Store5</h1>
</div>
## Why We Made Store <div align="center">
<h4>Full documentation can be found on our <a href="https://mobilenativefoundation.github.io/Store/">website</a>!</h4>
</div>
- Modern software needs data representations to be fluid and always available. ### Concepts
- Users expect their UI experience to never be compromised (blocked) by new data loads. Whether an
application is social, news or business-to-business, users expect a seamless experience both
online and offline.
- International users expect minimal data downloads as many megabytes of downloaded data can quickly
result in astronomical phone bills.
Store is a Kotlin library for loading data from remote and local sources. - [Store](https://mobilenativefoundation.github.io/Store/store/store/) is a typed repository that returns a flow
of [Data](https://github.com/MobileNativeFoundation/Store/blob/main/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt#L39)
/[Loading](https://github.com/MobileNativeFoundation/Store/blob/main/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt#L34)
/[Error](https://github.com/MobileNativeFoundation/Store/blob/main/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt#L51)
from local and network data sources
- [MutableStore](https://mobilenativefoundation.github.io/Store/mutable-store/building/overview/) is a mutable repository implementation that allows create **(C)**, read **(R)**,
update **(U)**, and delete **(D)** operations for local and network resources
- [SourceOfTruth](https://mobilenativefoundation.github.io/Store/mutable-store/building/implementations/source-of-truth/) persists items
- [Fetcher](https://mobilenativefoundation.github.io/Store/mutable-store/building/implementations/fetcher/) defines how data will be fetched over network
- [Updater](https://mobilenativefoundation.github.io/Store/mutable-store/building/implementations/updater/) defines how local changes will be pushed to network
- [Bookkeeper](https://mobilenativefoundation.github.io/Store/mutable-store/building/implementations/bookkeeper/) tracks metadata of local changes and records
synchronization failures
- [Validator](https://mobilenativefoundation.github.io/Store/mutable-store/building/implementations/validator/) returns whether an item is valid
- [Converter](https://mobilenativefoundation.github.io/Store/mutable-store/building/implementations/converter/) converts items
between [Network](https://mobilenativefoundation.github.io/Store/mutable-store/building/generics/network)
/[Local](https://mobilenativefoundation.github.io/Store/mutable-store/building/generics/sot)
/[Output](https://mobilenativefoundation.github.io/Store/mutable-store/building/generics/common) representations
### Overview ### Including Store In Your Project
A Store is responsible for managing a particular data request. When you create an implementation of #### Android
a Store, you provide it with a `Fetcher`, a function that defines how data will be fetched over
network. You can also define how your Store will cache data in-memory and on-disk. Since Store
returns your data as a `Flow`, threading is a breeze! Once a Store is built, it handles the logic
around data flow, allowing your views to use the best data source and ensuring that the newest data
is always available for later offline use.
Store leverages multiple request throttling to prevent excessive calls to the network and disk
cache. By utilizing Store, you eliminate the possibility of flooding your network with the same
request while adding two layers of caching (memory and disk) as well as ability to add disk as a
source of truth where you can modify the disk directly without going through Store (works best with
databases that can provide observables sources
like [Jetpack Room](https://developer.android.com/jetpack/androidx/releases/room)
, [SQLDelight](https://github.com/cashapp/sqldelight)
or [Realm](https://realm.io/products/realm-database/))
### How to include in your project
Artifacts are hosted on **Maven Central**.
```kotlin ```kotlin
STORE_VERSION = "5.0.0-alpha03" implementation "org.mobilenativefoundation.store:store5:5.0.0-alpha03"
``` ```
### Android #### Multiplatform (Common, JVM, Native, JS)
```groovy
implementation "org.mobilenativefoundation.store:store5:$STORE_VERSION"
```
### Multiplatform (Common, JVM, Native, JS)
```kotlin ```kotlin
commonMain { commonMain {
dependencies { dependencies {
implementation("org.mobilenativefoundation.store:store5:$STORE_VERSION") implementation("org.mobilenativefoundation.store:store5:5.0.0-alpha03")
} }
} }
``` ```
### Fully Configured Store ### Getting Started
Let's start by looking at what a fully configured Store looks like. We will then walk through #### Building Your First Store
simpler examples showing each piece:
```kotlin ```kotlin
StoreBuilder StoreBuilder
.from( .from<Key, Network, Output, Local>(fetcher, sourceOfTruth)
fetcher = Fetcher.of { api.fetchSubreddit(it, "10").data.children.map(::toPosts) }, .converter(converter)
sourceOfTruth = SourceOfTruth.of( .validator(validator)
reader = db.postDao()::loadPosts, .build(updater, bookkeeper)
writer = db.postDao()::insertPosts,
delete = db.postDao()::clearFeed,
deleteAll = db.postDao()::clearAllFeeds
)
).build()
``` ```
With the above setup you have: #### Creating
+ In-memory caching for rotation ##### Request
+ Disk caching for when users are offline
+ Throttling of API calls when parallel requests are made for the same resource
+ Rich API to ask for data whether you want cached, new or a stream of future data updates.
And now for the details:
### Creating a Store
You create a Store using a builder. The only requirement is to include a `Fetcher` which is just
a `typealias` to a function that returns a `Flow<FetcherResult<ReturnType>>`.
```kotlin ```kotlin
val store = StoreBuilder store.write(
.from(Fetcher.ofFlow { articleId -> api.getArticle(articleId) }) // api returns Flow<Article> request = StoreWriteRequest.of<Key, Output, Response>(
.build() key = key,
value = value
)
)
``` ```
Store uses generic keys as identifiers for data. A key can be any value object that properly ##### Response
implements `toString()`, `equals()` and `hashCode()`. When your `Fetcher` function is called, it
will be passed a particular `Key` value. Similarly, the key will be used as a primary identifier
within caches (Make sure to have a proper `hashCode()`!!).
Note: We highly recommend using built-in types that implement `equals` and `hashcode` or ```text
Kotlin `data` classes for complex keys. 1. StoreWriteResponse.Success.Typed<Response>(response)
```
### Public Interface - Stream #### Reading
The primary function provided by a `Store` instance is the `stream` function which has the following ##### Request
signature:
```kotlin ```kotlin
fun stream(request: StoreRequest<Key>): Flow<StoreResponse<Output>> store.stream<Response>(request = StoreReadRequest.cached(key, refresh = false))
``` ```
Each `stream` call receives a `StoreRequest` object, which defines which key to fetch and which data ##### Response
sources to utilize.
The response is a `Flow` of `StoreResponse`. `StoreResponse` is a Kotlin sealed class that can be
either
a `Loading`, `Data` or `Error` instance.
Each `StoreResponse` includes an `origin` field which specifies where the event is coming from.
* The `Loading` class only has an `origin` field. This can provide you information like "network is ```text
fetching data", which can be a good signal to activate the loading spinner in your UI. 1. StoreReadResponse.Data(value, origin = StoreReadResponseOrigin.Cache)
* The `Data` class has a `value` field which includes an instance of the type returned by `Store`. ```
* The `Error` class includes an `error` field that contains the exception thrown by the
given `origin`.
When an error happens, `Store` does not throw an exception, instead, it wraps it in #### Updating
a `StoreResponse.Error` type which allows `Flow` to continue so that it can still receive updates
that might be triggered by either changes in your data source or subsequent fetch operations. ##### Request
```kotlin ```kotlin
viewModelScope.launch { store.write(
store.stream(StoreRequest.cached(key = key, refresh = true)).collect { response -> request = StoreWriteRequest.of<Key, Output, Response>(
when (response) { key = key,
is StoreResponse.Loading -> showLoadingSpinner() value = newValue
is StoreResponse.Data -> { )
if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner() )
updateUI(response.value)
}
is StoreResponse.Error -> {
if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
showError(response.error)
}
}
}
}
``` ```
For convenience, there are `Store.get(key)` and `Store.fresh(key)` extension functions. ##### Response
* `suspend fun Store.get(key: Key): Value`: This method returns a single value for the given key. If ```text
available, it will be returned from the in memory cache or the sourceOfTruth. An error will be 1. StoreWriteResponse.Success.Typed<Response>(response)
thrown if no value is available in either the `cache` or `sourceOfTruth`, and the `fetcher` fails ```
to load the data from the network.
* `suspend fun Store.fresh(key: Key): Value`: This method returns a single value for the given key #### Deleting
that is obtained by querying the fetcher. An error will be thrown if the `fetcher` fails to load
the data from the network, regardless of whether any value is available in the `cache` ##### Request
or `sourceOfTruth`.
```kotlin ```kotlin
lifecycleScope.launchWhenStarted { store.clear(key)
val article = store.get(key)
updateUI(article)
}
``` ```
The first time you call to `suspend store.get(key)`, the response will be stored in an in-memory ### License
cache and in the sourceOfTruth, if provided.
All subsequent calls to `store.get(key)` with the same `Key` will retrieve the cached version of the
data, minimizing unnecessary data calls. This prevents your app from fetching fresh data over the
network (or from another external data source) in situations when doing so would unnecessarily waste
bandwidth and battery. A great use case is any time your views are recreated after a rotation, they
will be able to request the cached data from your Store. Having this data available can help you
avoid the need to retain this in the view layer.
By default, 100 items will be cached in memory for 24 hours. You
may [pass in your own memory policy to override the default policy](#Configuring-In-memory-Cache).
### Skipping Memory/Disk
Alternatively, you can call `store.fresh(key)` to get a `suspended result` that skips the memory (
and optional disk cache).
A good use case is overnight background updates use `fresh()` to make sure that calls
to `store.get()` will not have to hit the network during normal usage. Another good use case
for `fresh()` is when a user wants to pull to refresh.
Calls to both `fresh()` and `get()` emit one value or throw an error.
### Stream
For real-time updates, you may also call `store.stream()` which returns a `Flow<T>` that emits each
time a new item is returned from your store. You can think of stream as a way to create reactive
streams that update when you db or memory cache updates
example calls:
```kotlin
lifecycleScope.launchWhenStarted {
store.stream(
StoreRequest.cached(
3,
refresh = false
)
) //will get cached value followed by any fresh values, refresh will also trigger network call if set to `true` even if the data is available in cache or disk.
.collect {}
store.stream(StoreRequest.fresh(3)) //skip cache, go directly to fetcher
.collect {}
}
```
### Inflight Debouncer
To prevent duplicate requests for the same data, Store offers an inflight debouncer. If the same
request is made as a previous identical request that has not completed, the same response will be
returned. This is useful for situations when your app needs to make many async calls for the same
data at startup or when users are obsessively pulling to refresh. As an example, The New York Times
news app asynchronously calls `ConfigStore.get()` from 12 different places on startup. The first
call blocks while all others wait for the data to arrive. We have seen a dramatic decrease in the
app's data usage after implementing this inflight logic.
### Disk as Cache
Stores can enable disk caching by passing a `SourceOfTruth` into the builder. Whenever a new network
request is made, the Store will first write to the disk cache and then read from the disk cache.
### Disk as Single Source of Truth
Providing `sourceOfTruth` whose `reader` function can return a `Flow<Value?>` allows you to make
Store treat your disk as source of truth.
Any changes made on disk, even if it is not made by Store, will update the active `Store` streams.
This feature, combined with persistence libraries that provide observable
queries ([Jetpack Room](https://developer.android.com/jetpack/androidx/releases/room)
, [SQLDelight](https://github.com/cashapp/sqldelight)
or [Realm](https://realm.io/products/realm-database/))
allows you to create offline first applications that can be used without an active network
connection while still providing a great user experience.
```kotlin
StoreBuilder
.from(
fetcher = Fetcher.of { api.fetchSubreddit(it, "10").data.children.map(::toPosts) },
sourceOfTruth = SourceOfTruth.of(
reader = db.postDao()::loadPosts,
writer = db.postDao()::insertPosts,
delete = db.postDao()::clearFeed,
deleteAll = db.postDao()::clearAllFeeds
)
).build()
```
Stores dont care how youre storing or retrieving your data from disk. As a result, you can use
Stores with object storage or any database (Realm, SQLite, CouchDB, Firebase etc). Technically,
there is nothing stopping you from implementing an in-memory cache for the "sourceOfTruth"
implementation and instead have two levels of in-memory caching--one with inflated and one with
deflated models, allowing for sharing of the “sourceOfTruth” cache data between stores.
If using SQLite we recommend working
with [Room](https://developer.android.com/topic/libraries/architecture/room) which returns a `Flow`
from a query
The above builder is how we recommend working with data on Android. With the above setup you have:
+ Memory caching with TTL & Size policies
+ Disk caching with simple integration with Room
+ In-flight request management
+ Ability to get cached data or bust through your caches (`get()` vs. `fresh()`)
+ Ability to listen for any new emissions from network (stream)
+ Structured Concurrency through APIs build on Coroutines and Kotlin Flow
### Configuring in-memory Cache
You can configure in-memory cache with the `MemoryPolicy`:
```kotlin
StoreBuilder
.from(
fetcher = Fetcher.of { api.fetchSubreddit(it, "10").data.children.map(::toPosts) },
sourceOfTruth = SourceOfTruth.of(
reader = db.postDao()::loadPosts,
writer = db.postDao()::insertPosts,
delete = db.postDao()::clearFeed,
deleteAll = db.postDao()::clearAllFeeds
)
).cachePolicy(
MemoryPolicy.builder<Any, Any>()
.setMaxSize(10)
.setExpireAfterAccess(10.minutes) // or setExpireAfterWrite(10.minutes)
.build()
).build()
```
* `setMaxSize(maxSize: Long)` sets the maximum number of entries to be kept in the cache before
starting to evict the least recently used items.
* `setExpireAfterAccess(expireAfterAccess: Duration)` sets the maximum time an entry can live in the
cache since the last access, where "access" means reading the cache, adding a new cache entry, and
replacing an existing entry with a new one. This duration is also known as **time-to-idle (TTI)**.
* `setExpireAfterWrite(expireAfterWrite: Duration)` sets the maximum time an entry can live in the
cache since the last write, where "write" means adding a new cache entry and replacing an existing
entry with a new one. This duration is also known as **time-to-live (TTL)**.
Note that `setExpireAfterAccess` and `setExpireAfterWrite` **cannot** both be set at the same time.
### Clearing store entries
You can delete a specific entry by key from a store, or clear all entries in a store.
#### Store with no sourceOfTruth
```kotlin
val store = StoreBuilder
.from(
fetcher = Fetcher.of { key: String ->
api.fetchData(key)
}).build()
```
The following will clear the entry associated with the key from the in-memory cache:
```kotlin
store.clear("10")
```
The following will clear all entries from the in-memory cache:
```kotlin
store.clearAll()
```
#### Store with sourceOfTruth
When store has a sourceOfTruth, you'll need to provide the `delete` and `deleteAll` functions
for `clear(key)` and `clearAll()` to work:
```kotlin
StoreBuilder
.from(
fetcher = Fetcher.of { api.fetchData(key) },
sourceOfTruth = SourceOfTruth.of(
reader = dao::loadData,
writer = dao::writeData,
delete = dao::clearDataByKey,
deleteAll = dao::clearAllData
)
).build()
```
The following will clear the entry associated with the key from both the in-memory cache and the
sourceOfTruth:
```kotlin
store.clear("myKey")
```
The following will clear all entries from both the in-memory cache and the sourceOfTruth:
```kotlin
store.clearAll()
```
## License
```text ```text
Copyright (c) 2022 Mobile Native Foundation. Copyright (c) 2022 Mobile Native Foundation.

View file

@ -2,7 +2,7 @@ package org.mobilenativefoundation.store.cache5
import kotlin.time.Duration import kotlin.time.Duration
class CacheBuilder<Key : Any, CommonRepresentation : Any> { class CacheBuilder<Key : Any, Output : 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, CommonRepresentation : 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, CommonRepresentation>? = null internal var weigher: Weigher<Key, Output>? = 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, CommonRepresentation> = apply { fun concurrencyLevel(producer: () -> Int): CacheBuilder<Key, Output> = apply {
concurrencyLevel = producer.invoke() concurrencyLevel = producer.invoke()
} }
fun maximumSize(maximumSize: Long): CacheBuilder<Key, CommonRepresentation> = apply { fun maximumSize(maximumSize: Long): CacheBuilder<Key, Output> = 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, CommonRepresentation> = apply { fun expireAfterAccess(duration: Duration): CacheBuilder<Key, Output> = 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, CommonRepresentation> = apply { fun expireAfterWrite(duration: Duration): CacheBuilder<Key, Output> = 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, CommonRepresentation> = apply { fun ticker(ticker: Ticker): CacheBuilder<Key, Output> = apply {
this.ticker = ticker this.ticker = ticker
} }
fun weigher(maximumWeight: Long, weigher: Weigher<Key, CommonRepresentation>): CacheBuilder<Key, CommonRepresentation> = apply { fun weigher(maximumWeight: Long, weigher: Weigher<Key, Output>): CacheBuilder<Key, Output> = 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, CommonRepresentation : Any> {
this.weigher = weigher this.weigher = weigher
} }
fun build(): Cache<Key, CommonRepresentation> { fun build(): Cache<Key, Output> {
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.")
} }

View file

@ -0,0 +1,47 @@
package org.mobilenativefoundation.store.store5
interface Converter<Network : Any, Output : Any, Local : Any> {
fun fromNetworkToOutput(network: Network): Output?
fun fromOutputToLocal(common: Output): Local?
fun fromLocalToOutput(sourceOfTruth: Local): Output?
class Builder<Network : Any, Output : Any, Local : Any> {
private var fromOutputToLocal: ((value: Output) -> Local)? = null
private var fromNetworkToOutput: ((value: Network) -> Output)? = null
private var fromLocalToOutput: ((value: Local) -> Output)? = null
fun build(): Converter<Network, Output, Local> =
RealConverter(fromOutputToLocal, fromNetworkToOutput, fromLocalToOutput)
fun fromOutputToLocal(converter: (value: Output) -> Local): Builder<Network, Output, Local> {
fromOutputToLocal = converter
return this
}
fun fromLocalToOutput(converter: (value: Local) -> Output): Builder<Network, Output, Local> {
fromLocalToOutput = converter
return this
}
fun fromNetworkToOutput(converter: (value: Network) -> Output): Builder<Network, Output, Local> {
fromNetworkToOutput = converter
return this
}
}
}
private class RealConverter<Network : Any, Output : Any, Local : Any>(
private val fromOutputToLocal: ((value: Output) -> Local)?,
private val fromNetworkToOutput: ((value: Network) -> Output)?,
private val fromLocalToOutput: ((value: Local) -> Output)?,
) : Converter<Network, Output, Local> {
override fun fromNetworkToOutput(network: Network): Output? =
fromNetworkToOutput?.invoke(network)
override fun fromOutputToLocal(common: Output): Local? =
fromOutputToLocal?.invoke(common)
override fun fromLocalToOutput(sourceOfTruth: Local): Output? =
fromLocalToOutput?.invoke(sourceOfTruth)
}

View file

@ -19,11 +19,11 @@ import org.mobilenativefoundation.store.store5.Fetcher.Companion.ofResult
* See [ofFlow], [of] for easily translating to [FetcherResult] (and * See [ofFlow], [of] for easily translating to [FetcherResult] (and
* automatically transforming exceptions into [FetcherResult.Error]. * automatically transforming exceptions into [FetcherResult.Error].
*/ */
interface Fetcher<Key : Any, NetworkRepresentation : Any> { interface Fetcher<Key : Any, Network : Any> {
/** /**
* Returns a flow of the item represented by the given [key]. * Returns a flow of the item represented by the given [key].
*/ */
operator fun invoke(key: Key): Flow<FetcherResult<NetworkRepresentation>> operator fun invoke(key: Key): Flow<FetcherResult<Network>>
companion object { companion object {
/** /**
@ -37,9 +37,9 @@ interface Fetcher<Key : Any, NetworkRepresentation : Any> {
* *
* @param flowFactory a factory for a [Flow]ing source of network records. * @param flowFactory a factory for a [Flow]ing source of network records.
*/ */
fun <Key : Any, NetworkRepresentation : Any> ofResultFlow( fun <Key : Any, Network : Any> ofResultFlow(
flowFactory: (Key) -> Flow<FetcherResult<NetworkRepresentation>> flowFactory: (Key) -> Flow<FetcherResult<Network>>
): Fetcher<Key, NetworkRepresentation> = FactoryFetcher(factory = flowFactory) ): Fetcher<Key, Network> = FactoryFetcher(factory = flowFactory)
/** /**
* "Creates" a [Fetcher] from a non-[Flow] source. * "Creates" a [Fetcher] from a non-[Flow] source.
@ -52,9 +52,9 @@ interface Fetcher<Key : Any, NetworkRepresentation : Any> {
* *
* @param fetch a source of network records. * @param fetch a source of network records.
*/ */
fun <Key : Any, NetworkRepresentation : Any> ofResult( fun <Key : Any, Network : Any> ofResult(
fetch: suspend (Key) -> FetcherResult<NetworkRepresentation> fetch: suspend (Key) -> FetcherResult<Network>
): Fetcher<Key, NetworkRepresentation> = ofResultFlow(fetch.asFlow()) ): Fetcher<Key, Network> = ofResultFlow(fetch.asFlow())
/** /**
* "Creates" a [Fetcher] from a [flowFactory] and translate the results to a [FetcherResult]. * "Creates" a [Fetcher] from a [flowFactory] and translate the results to a [FetcherResult].
@ -68,11 +68,11 @@ interface Fetcher<Key : Any, NetworkRepresentation : Any> {
* *
* @param flowFactory a factory for a [Flow]ing source of network records. * @param flowFactory a factory for a [Flow]ing source of network records.
*/ */
fun <Key : Any, NetworkRepresentation : Any> ofFlow( fun <Key : Any, Network : Any> ofFlow(
flowFactory: (Key) -> Flow<NetworkRepresentation> flowFactory: (Key) -> Flow<Network>
): Fetcher<Key, NetworkRepresentation> = FactoryFetcher { key: Key -> ): Fetcher<Key, Network> = FactoryFetcher { key: Key ->
flowFactory(key) flowFactory(key)
.map<NetworkRepresentation, FetcherResult<NetworkRepresentation>> { FetcherResult.Data(it) } .map<Network, FetcherResult<Network>> { FetcherResult.Data(it) }
.catch { throwable: Throwable -> emit(FetcherResult.Error.Exception(throwable)) } .catch { throwable: Throwable -> emit(FetcherResult.Error.Exception(throwable)) }
} }
@ -87,19 +87,19 @@ interface Fetcher<Key : Any, NetworkRepresentation : Any> {
* *
* @param fetch a source of network records. * @param fetch a source of network records.
*/ */
fun <Key : Any, NetworkRepresentation : Any> of(fetch: suspend (key: Key) -> NetworkRepresentation): Fetcher<Key, NetworkRepresentation> = fun <Key : Any, Network : Any> of(fetch: suspend (key: Key) -> Network): Fetcher<Key, Network> =
ofFlow(fetch.asFlow()) ofFlow(fetch.asFlow())
private fun <Key : Any, NetworkRepresentation : Any> (suspend (key: Key) -> NetworkRepresentation).asFlow() = { key: Key -> private fun <Key : Any, Network : Any> (suspend (key: Key) -> Network).asFlow() = { key: Key ->
flow { flow {
emit(invoke(key)) emit(invoke(key))
} }
} }
private class FactoryFetcher<Key : Any, NetworkRepresentation : Any>( private class FactoryFetcher<Key : Any, Network : Any>(
private val factory: (Key) -> Flow<FetcherResult<NetworkRepresentation>> private val factory: (Key) -> Flow<FetcherResult<Network>>
) : Fetcher<Key, NetworkRepresentation> { ) : Fetcher<Key, Network> {
override fun invoke(key: Key): Flow<FetcherResult<NetworkRepresentation>> = factory(key) override fun invoke(key: Key): Flow<FetcherResult<Network>> = factory(key)
} }
} }
} }

View file

@ -1,7 +1,7 @@
package org.mobilenativefoundation.store.store5 package org.mobilenativefoundation.store.store5
sealed class FetcherResult<out NetworkRepresentation : Any> { sealed class FetcherResult<out Network : Any> {
data class Data<NetworkRepresentation : Any>(val value: NetworkRepresentation) : FetcherResult<NetworkRepresentation>() data class Data<Network : Any>(val value: Network) : FetcherResult<Network>()
sealed class Error : FetcherResult<Nothing>() { sealed class Error : FetcherResult<Nothing>() {
data class Exception(val error: Throwable) : Error() data class Exception(val error: Throwable) : Error()
data class Message(val message: String) : Error() data class Message(val message: String) : Error()

View file

@ -1,22 +0,0 @@
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)
}
}

View file

@ -1,8 +1,8 @@
package org.mobilenativefoundation.store.store5 package org.mobilenativefoundation.store.store5
interface MutableStore<Key : Any, CommonRepresentation : Any> : interface MutableStore<Key : Any, Output : Any> :
Read.StreamWithConflictResolution<Key, CommonRepresentation>, Read.StreamWithConflictResolution<Key, Output>,
Write<Key, CommonRepresentation>, Write<Key, Output>,
Write.Stream<Key, CommonRepresentation>, Write.Stream<Key, Output>,
Clear.Key<Key>, Clear.Key<Key>,
Clear Clear

View file

@ -1,6 +1,6 @@
package org.mobilenativefoundation.store.store5 package org.mobilenativefoundation.store.store5
data class OnFetcherCompletion<NetworkRepresentation : Any>( data class OnFetcherCompletion<Network : Any>(
val onSuccess: (FetcherResult.Data<NetworkRepresentation>) -> Unit, val onSuccess: (FetcherResult.Data<Network>) -> Unit,
val onFailure: (FetcherResult.Error) -> Unit val onFailure: (FetcherResult.Error) -> Unit
) )

View file

@ -1,6 +1,6 @@
package org.mobilenativefoundation.store.store5 package org.mobilenativefoundation.store.store5
data class OnUpdaterCompletion<NetworkWriteResponse : Any>( data class OnUpdaterCompletion<Response : Any>(
val onSuccess: (UpdaterResult.Success) -> Unit, val onSuccess: (UpdaterResult.Success) -> Unit,
val onFailure: (UpdaterResult.Error) -> Unit val onFailure: (UpdaterResult.Error) -> Unit
) )

View file

@ -3,15 +3,15 @@ package org.mobilenativefoundation.store.store5
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface Read { interface Read {
interface Stream<Key : Any, CommonRepresentation : Any> { interface Stream<Key : Any, Output : Any> {
/** /**
* Return a flow for the given key * Return a flow for the given key
* @param request - see [StoreReadRequest] for configurations * @param request - see [StoreReadRequest] for configurations
*/ */
fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>> fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<Output>>
} }
interface StreamWithConflictResolution<Key : Any, CommonRepresentation : Any> { interface StreamWithConflictResolution<Key : Any, Output : Any> {
fun <NetworkWriteResponse : Any> stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>> fun <Response : Any> stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<Output>>
} }
} }

View file

@ -46,14 +46,14 @@ import kotlin.jvm.JvmName
* 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 : Any, SourceOfTruthRepresentation : Any> { interface SourceOfTruth<Key : Any, Local : 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<SourceOfTruthRepresentation?> fun reader(key: Key): Flow<Local?>
/** /**
* 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
@ -66,7 +66,7 @@ interface SourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any> {
* *
* @param key The key to update for. * @param key The key to update for.
*/ */
suspend fun write(key: Key, value: SourceOfTruthRepresentation) suspend fun write(key: Key, value: Local)
/** /**
* 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 : Any, SourceOfTruthRepresentation : Any> {
* @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, SourceOfTruthRepresentation : Any> of( fun <Key : Any, Local : Any> of(
nonFlowReader: suspend (Key) -> SourceOfTruthRepresentation?, nonFlowReader: suspend (Key) -> Local?,
writer: suspend (Key, SourceOfTruthRepresentation) -> Unit, writer: suspend (Key, Local) -> Unit,
delete: (suspend (Key) -> Unit)? = null, delete: (suspend (Key) -> Unit)? = null,
deleteAll: (suspend () -> Unit)? = null deleteAll: (suspend () -> Unit)? = null
): SourceOfTruth<Key, SourceOfTruthRepresentation> = PersistentNonFlowingSourceOfTruth( ): SourceOfTruth<Key, Local> = PersistentNonFlowingSourceOfTruth(
realReader = nonFlowReader, realReader = nonFlowReader,
realWriter = writer, realWriter = writer,
realDelete = delete, realDelete = delete,
@ -112,12 +112,12 @@ interface SourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any> {
* @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, SourceOfTruthRepresentation : Any> of( fun <Key : Any, Local : Any> of(
reader: (Key) -> Flow<SourceOfTruthRepresentation?>, reader: (Key) -> Flow<Local?>,
writer: suspend (Key, SourceOfTruthRepresentation) -> Unit, writer: suspend (Key, Local) -> Unit,
delete: (suspend (Key) -> Unit)? = null, delete: (suspend (Key) -> Unit)? = null,
deleteAll: (suspend () -> Unit)? = null deleteAll: (suspend () -> Unit)? = null
): SourceOfTruth<Key, SourceOfTruthRepresentation> = PersistentSourceOfTruth( ): SourceOfTruth<Key, Local> = PersistentSourceOfTruth(
realReader = reader, realReader = reader,
realWriter = writer, realWriter = writer,
realDelete = delete, realDelete = delete,

View file

@ -31,7 +31,7 @@ package org.mobilenativefoundation.store.store5
* } * }
* *
*/ */
interface Store<Key : Any, CommonRepresentation : Any> : interface Store<Key : Any, Output : Any> :
Read.Stream<Key, CommonRepresentation>, Read.Stream<Key, Output>,
Clear.Key<Key>, Clear.Key<Key>,
Clear.All Clear.All

View file

@ -22,37 +22,37 @@ import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcherAndSo
/** /**
* Main entry point for creating a [Store]. * Main entry point for creating a [Store].
*/ */
interface StoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> { interface StoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any> {
fun build(): Store<Key, CommonRepresentation> fun build(): Store<Key, Output>
fun <NetworkWriteResponse : Any> build( fun <Response : Any> build(
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>, updater: Updater<Key, Output, Response>,
bookkeeper: Bookkeeper<Key> bookkeeper: Bookkeeper<Key>
): MutableStore<Key, CommonRepresentation> ): MutableStore<Key, Output>
/** /**
* A store multicasts same [CommonRepresentation] value to many consumers (Similar to RxJava.share()), by default * A store multicasts same [Output] 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, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> fun scope(scope: CoroutineScope): StoreBuilder<Key, Network, Output, Local>
/** /**
* 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()
*/ */
fun cachePolicy(memoryPolicy: MemoryPolicy<Key, CommonRepresentation>?): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> fun cachePolicy(memoryPolicy: MemoryPolicy<Key, Output>?): StoreBuilder<Key, Network, Output, Local>
/** /**
* 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, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> fun disableCache(): StoreBuilder<Key, Network, Output, Local>
fun converter(converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>): fun converter(converter: Converter<Network, Output, Local>):
StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> StoreBuilder<Key, Network, Output, Local>
companion object { companion object {
@ -61,9 +61,9 @@ interface StoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentat
* *
* @param fetcher a [Fetcher] flow of network records. * @param fetcher a [Fetcher] flow of network records.
*/ */
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any> from( fun <Key : Any, Network : Any, Output : Any> from(
fetcher: Fetcher<Key, NetworkRepresentation>, fetcher: Fetcher<Key, Network>,
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, *> = storeBuilderFromFetcher(fetcher = fetcher) ): StoreBuilder<Key, Network, Output, *> = storeBuilderFromFetcher(fetcher = fetcher)
/** /**
* Creates a new [StoreBuilder] from a [Fetcher] and a [SourceOfTruth]. * Creates a new [StoreBuilder] from a [Fetcher] and a [SourceOfTruth].
@ -71,10 +71,10 @@ interface StoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentat
* @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, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> from( fun <Key : Any, Network : Any, Output : Any, Local : Any> from(
fetcher: Fetcher<Key, NetworkRepresentation>, fetcher: Fetcher<Key, Network>,
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation> sourceOfTruth: SourceOfTruth<Key, Local>
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> = ): StoreBuilder<Key, Network, Output, Local> =
storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth) storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth)
} }
} }

View file

@ -1,49 +0,0 @@
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)
}

View file

@ -22,7 +22,7 @@ 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 StoreReadResponse<out CommonRepresentation> { sealed class StoreReadResponse<out Output> {
/** /**
* Represents the source of the Response. * Represents the source of the Response.
*/ */
@ -36,8 +36,8 @@ sealed class StoreReadResponse<out CommonRepresentation> {
/** /**
* Data dispatched by [Store] * Data dispatched by [Store]
*/ */
data class Data<CommonRepresentation>(val value: CommonRepresentation, override val origin: StoreReadResponseOrigin) : data class Data<Output>(val value: Output, override val origin: StoreReadResponseOrigin) :
StoreReadResponse<CommonRepresentation>() StoreReadResponse<Output>()
/** /**
* 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
@ -63,7 +63,7 @@ sealed class StoreReadResponse<out CommonRepresentation> {
/** /**
* 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(): CommonRepresentation { fun requireData(): Output {
return when (this) { return when (this) {
is Data -> value is Data -> value
is Error -> this.doThrow() is Error -> this.doThrow()
@ -96,7 +96,7 @@ sealed class StoreReadResponse<out CommonRepresentation> {
/** /**
* If there is data available, returns it; otherwise returns null. * If there is data available, returns it; otherwise returns null.
*/ */
fun dataOrNull(): CommonRepresentation? = when (this) { fun dataOrNull(): Output? = when (this) {
is Data -> value is Data -> value
else -> null else -> null
} }

View file

@ -4,18 +4,18 @@ import kotlinx.datetime.Clock
import org.mobilenativefoundation.store.store5.impl.OnStoreWriteCompletion import org.mobilenativefoundation.store.store5.impl.OnStoreWriteCompletion
import org.mobilenativefoundation.store.store5.impl.RealStoreWriteRequest import org.mobilenativefoundation.store.store5.impl.RealStoreWriteRequest
interface StoreWriteRequest<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> { interface StoreWriteRequest<Key : Any, Output : Any, Response : Any> {
val key: Key val key: Key
val input: CommonRepresentation val value: Output
val created: Long val created: Long
val onCompletions: List<OnStoreWriteCompletion>? val onCompletions: List<OnStoreWriteCompletion>?
companion object { companion object {
fun <Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> of( fun <Key : Any, Output : Any, Response : Any> of(
key: Key, key: Key,
input: CommonRepresentation, value: Output,
onCompletions: List<OnStoreWriteCompletion>? = null, onCompletions: List<OnStoreWriteCompletion>? = null,
created: Long = Clock.System.now().toEpochMilliseconds(), created: Long = Clock.System.now().toEpochMilliseconds(),
): StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse> = RealStoreWriteRequest(key, input, created, onCompletions) ): StoreWriteRequest<Key, Output, Response> = RealStoreWriteRequest(key, value, created, onCompletions)
} }
} }

View file

@ -2,7 +2,7 @@ package org.mobilenativefoundation.store.store5
sealed class StoreWriteResponse { sealed class StoreWriteResponse {
sealed class Success : StoreWriteResponse() { sealed class Success : StoreWriteResponse() {
data class Typed<NetworkWriteResponse : Any>(val value: NetworkWriteResponse) : Success() data class Typed<Response : Any>(val value: Response) : Success()
data class Untyped(val value: Any) : Success() data class Untyped(val value: Any) : Success()
} }

View file

@ -1,35 +1,35 @@
package org.mobilenativefoundation.store.store5 package org.mobilenativefoundation.store.store5
typealias PostRequest<Key, CommonRepresentation> = suspend (key: Key, input: CommonRepresentation) -> UpdaterResult typealias PostRequest<Key, Output> = suspend (key: Key, value: Output) -> UpdaterResult
/** /**
* Posts data to remote data source. * Posts data to remote data source.
* @see [StoreWriteRequest] * @see [StoreWriteRequest]
*/ */
interface Updater<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> { interface Updater<Key : Any, Output : Any, Response : Any> {
/** /**
* Makes HTTP POST request. * Makes HTTP POST request.
*/ */
suspend fun post(key: Key, input: CommonRepresentation): UpdaterResult suspend fun post(key: Key, value: Output): UpdaterResult
/** /**
* Executes on network completion. * Executes on network completion.
*/ */
val onCompletion: OnUpdaterCompletion<NetworkWriteResponse>? val onCompletion: OnUpdaterCompletion<Response>?
companion object { companion object {
fun <Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> by( fun <Key : Any, Output : Any, Response : Any> by(
post: PostRequest<Key, CommonRepresentation>, post: PostRequest<Key, Output>,
onCompletion: OnUpdaterCompletion<NetworkWriteResponse>? = null, onCompletion: OnUpdaterCompletion<Response>? = null,
): Updater<Key, CommonRepresentation, NetworkWriteResponse> = RealNetworkUpdater( ): Updater<Key, Output, Response> = RealNetworkUpdater(
post, onCompletion post, onCompletion
) )
} }
} }
internal class RealNetworkUpdater<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any>( internal class RealNetworkUpdater<Key : Any, Output : Any, Response : Any>(
private val realPost: PostRequest<Key, CommonRepresentation>, private val realPost: PostRequest<Key, Output>,
override val onCompletion: OnUpdaterCompletion<NetworkWriteResponse>?, override val onCompletion: OnUpdaterCompletion<Response>?,
) : Updater<Key, CommonRepresentation, NetworkWriteResponse> { ) : Updater<Key, Output, Response> {
override suspend fun post(key: Key, input: CommonRepresentation): UpdaterResult = realPost(key, input) override suspend fun post(key: Key, value: Output): UpdaterResult = realPost(key, value)
} }

View file

@ -3,7 +3,7 @@ package org.mobilenativefoundation.store.store5
sealed class UpdaterResult { sealed class UpdaterResult {
sealed class Success : UpdaterResult() { sealed class Success : UpdaterResult() {
data class Typed<NetworkWriteResponse : Any>(val value: NetworkWriteResponse) : Success() data class Typed<Response : Any>(val value: Response) : Success()
data class Untyped(val value: Any) : Success() data class Untyped(val value: Any) : Success()
} }

View file

@ -0,0 +1,22 @@
package org.mobilenativefoundation.store.store5
import org.mobilenativefoundation.store.store5.impl.RealValidator
/**
* Enables custom validation of [Store] items.
* @see [StoreReadRequest]
*/
interface Validator<Output : 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: Output): Boolean
companion object {
fun <Output : Any> by(
validator: suspend (item: Output) -> Boolean
): Validator<Output> = RealValidator(validator)
}
}

View file

@ -2,11 +2,11 @@ package org.mobilenativefoundation.store.store5
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface Write<Key : Any, CommonRepresentation : Any> { interface Write<Key : Any, Output : Any> {
@ExperimentalStoreApi @ExperimentalStoreApi
suspend fun <NetworkWriteResponse : Any> write(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): StoreWriteResponse suspend fun <Response : Any> write(request: StoreWriteRequest<Key, Output, Response>): StoreWriteResponse
interface Stream<Key : Any, CommonRepresentation : Any> { interface Stream<Key : Any, Output : Any> {
@ExperimentalStoreApi @ExperimentalStoreApi
fun <NetworkWriteResponse : Any> stream(requestStream: Flow<StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>>): Flow<StoreWriteResponse> fun <Response : Any> stream(requestStream: Flow<StoreWriteRequest<Key, Output, Response>>): Flow<StoreWriteResponse>
} }
} }

View file

@ -25,10 +25,10 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEmpty import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.mobilenativefoundation.store.multicast5.Multicaster import org.mobilenativefoundation.store.multicast5.Multicaster
import org.mobilenativefoundation.store.store5.Converter
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.SourceOfTruth import org.mobilenativefoundation.store.store5.SourceOfTruth
import org.mobilenativefoundation.store.store5.StoreConverter
import org.mobilenativefoundation.store.store5.StoreReadResponse import org.mobilenativefoundation.store.store5.StoreReadResponse
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
@ -40,7 +40,7 @@ import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
* 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, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>( internal class FetcherController<Key : Any, Network : Any, Output : Any, Local : Any>(
/** /**
* The [CoroutineScope] to use when collecting from the fetcher * The [CoroutineScope] to use when collecting from the fetcher
*/ */
@ -48,14 +48,14 @@ internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonR
/** /**
* 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, NetworkRepresentation>, private val realFetcher: Fetcher<Key, Network>,
/** /**
* [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, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>?, private val sourceOfTruth: SourceOfTruthWithBarrier<Key, Network, Output, Local>?,
private val converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null private val converter: Converter<Network, Output, Local>? = null
) { ) {
@Suppress("USELESS_CAST", "UNCHECKED_CAST") // needed for multicaster source @Suppress("USELESS_CAST", "UNCHECKED_CAST") // needed for multicaster source
private val fetchers = RefCountedResource( private val fetchers = RefCountedResource(
@ -69,7 +69,7 @@ internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonR
StoreReadResponse.Data( StoreReadResponse.Data(
it.value, it.value,
origin = StoreReadResponseOrigin.Fetcher origin = StoreReadResponseOrigin.Fetcher
) as StoreReadResponse<NetworkRepresentation> ) as StoreReadResponse<Network>
} }
is FetcherResult.Error.Message -> StoreReadResponse.Error.Message( is FetcherResult.Error.Message -> StoreReadResponse.Error.Message(
@ -92,9 +92,9 @@ internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonR
*/ */
piggybackingDownstream = true, piggybackingDownstream = true,
onEach = { response -> onEach = { response ->
response.dataOrNull()?.let { networkRepresentation -> response.dataOrNull()?.let { network ->
val input = val input =
networkRepresentation as? CommonRepresentation ?: converter?.fromNetworkRepresentationToCommonRepresentation(networkRepresentation) network as? Output ?: converter?.fromNetworkToOutput(network)
if (input != null) { if (input != null) {
sourceOfTruth?.write(key, input) sourceOfTruth?.write(key, input)
} }
@ -102,12 +102,12 @@ internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonR
} }
) )
}, },
onRelease = { _: Key, multicaster: Multicaster<StoreReadResponse<NetworkRepresentation>> -> onRelease = { _: Key, multicaster: Multicaster<StoreReadResponse<Network>> ->
multicaster.close() multicaster.close()
} }
) )
fun getFetcher(key: Key, piggybackOnly: Boolean = false): Flow<StoreReadResponse<NetworkRepresentation>> { fun getFetcher(key: Key, piggybackOnly: Boolean = false): Flow<StoreReadResponse<Network>> {
return flow { return flow {
val fetcher = acquireFetcher(key) val fetcher = acquireFetcher(key)
try { try {

View file

@ -1,9 +0,0 @@
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)
}

View file

@ -27,21 +27,21 @@ import org.mobilenativefoundation.store.store5.internal.concurrent.ThreadSafety
import org.mobilenativefoundation.store.store5.internal.definition.WriteRequestQueue import org.mobilenativefoundation.store.store5.internal.definition.WriteRequestQueue
import org.mobilenativefoundation.store.store5.internal.result.EagerConflictResolutionResult import org.mobilenativefoundation.store.store5.internal.result.EagerConflictResolutionResult
internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>( internal class RealMutableStore<Key : Any, Network : Any, Output : Any, Local : Any>(
private val delegate: RealStore<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>, private val delegate: RealStore<Key, Network, Output, Local>,
private val updater: Updater<Key, CommonRepresentation, *>, private val updater: Updater<Key, Output, *>,
private val bookkeeper: Bookkeeper<Key>, private val bookkeeper: Bookkeeper<Key>,
) : MutableStore<Key, CommonRepresentation>, Clear.Key<Key> by delegate, Clear.All by delegate { ) : MutableStore<Key, Output>, Clear.Key<Key> by delegate, Clear.All by delegate {
private val storeLock = Mutex() private val storeLock = Mutex()
private val keyToWriteRequestQueue = mutableMapOf<Key, WriteRequestQueue<Key, CommonRepresentation, *>>() private val keyToWriteRequestQueue = mutableMapOf<Key, WriteRequestQueue<Key, Output, *>>()
private val keyToThreadSafety = mutableMapOf<Key, ThreadSafety>() private val keyToThreadSafety = mutableMapOf<Key, ThreadSafety>()
override fun <NetworkWriteResponse : Any> stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>> = override fun <Response : Any> stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<Output>> =
flow { flow {
safeInitStore(request.key) safeInitStore(request.key)
when (val eagerConflictResolutionResult = tryEagerlyResolveConflicts<NetworkWriteResponse>(request.key)) { when (val eagerConflictResolutionResult = tryEagerlyResolveConflicts<Response>(request.key)) {
is EagerConflictResolutionResult.Error.Exception -> { is EagerConflictResolutionResult.Error.Exception -> {
logger.e(eagerConflictResolutionResult.error.toString()) logger.e(eagerConflictResolutionResult.error.toString())
} }
@ -63,7 +63,7 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
} }
@ExperimentalStoreApi @ExperimentalStoreApi
override fun <NetworkWriteResponse : Any> stream(requestStream: Flow<StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>>): Flow<StoreWriteResponse> = override fun <Response : Any> stream(requestStream: Flow<StoreWriteRequest<Key, Output, Response>>): Flow<StoreWriteResponse> =
flow { flow {
requestStream requestStream
.onEach { writeRequest -> .onEach { writeRequest ->
@ -72,12 +72,12 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
} }
.collect { writeRequest -> .collect { writeRequest ->
val storeWriteResponse = try { val storeWriteResponse = try {
delegate.write(writeRequest.key, writeRequest.input) delegate.write(writeRequest.key, writeRequest.value)
when (val updaterResult = tryUpdateServer(writeRequest)) { when (val updaterResult = tryUpdateServer(writeRequest)) {
is UpdaterResult.Error.Exception -> StoreWriteResponse.Error.Exception(updaterResult.error) is UpdaterResult.Error.Exception -> StoreWriteResponse.Error.Exception(updaterResult.error)
is UpdaterResult.Error.Message -> StoreWriteResponse.Error.Message(updaterResult.message) is UpdaterResult.Error.Message -> StoreWriteResponse.Error.Message(updaterResult.message)
is UpdaterResult.Success.Typed<*> -> { is UpdaterResult.Success.Typed<*> -> {
val typedValue = updaterResult.value as? NetworkWriteResponse val typedValue = updaterResult.value as? Response
if (typedValue == null) { if (typedValue == null) {
StoreWriteResponse.Success.Untyped(updaterResult.value) StoreWriteResponse.Success.Untyped(updaterResult.value)
} else { } else {
@ -95,14 +95,14 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
} }
@ExperimentalStoreApi @ExperimentalStoreApi
override suspend fun <NetworkWriteResponse : Any> write(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): StoreWriteResponse = override suspend fun <Response : Any> write(request: StoreWriteRequest<Key, Output, Response>): StoreWriteResponse =
stream(flowOf(request)).first() stream(flowOf(request)).first()
private suspend fun <NetworkWriteResponse : Any> tryUpdateServer(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): UpdaterResult { private suspend fun <Response : Any> tryUpdateServer(request: StoreWriteRequest<Key, Output, Response>): UpdaterResult {
val updaterResult = postLatest<NetworkWriteResponse>(request.key) val updaterResult = postLatest<Response>(request.key)
if (updaterResult is UpdaterResult.Success) { if (updaterResult is UpdaterResult.Success) {
updateWriteRequestQueue<NetworkWriteResponse>( updateWriteRequestQueue<Response>(
key = request.key, key = request.key,
created = request.created, created = request.created,
updaterResult = updaterResult updaterResult = updaterResult
@ -115,14 +115,14 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
return updaterResult return updaterResult
} }
private suspend fun <NetworkWriteResponse : Any> postLatest(key: Key): UpdaterResult { private suspend fun <Response : Any> postLatest(key: Key): UpdaterResult {
val writer = getLatestWriteRequest(key) val writer = getLatestWriteRequest(key)
return when (val updaterResult = updater.post(key, writer.input)) { return when (val updaterResult = updater.post(key, writer.value)) {
is UpdaterResult.Error.Exception -> UpdaterResult.Error.Exception(updaterResult.error) is UpdaterResult.Error.Exception -> UpdaterResult.Error.Exception(updaterResult.error)
is UpdaterResult.Error.Message -> UpdaterResult.Error.Message(updaterResult.message) is UpdaterResult.Error.Message -> UpdaterResult.Error.Message(updaterResult.message)
is UpdaterResult.Success.Untyped -> UpdaterResult.Success.Untyped(updaterResult.value) is UpdaterResult.Success.Untyped -> UpdaterResult.Success.Untyped(updaterResult.value)
is UpdaterResult.Success.Typed<*> -> { is UpdaterResult.Success.Typed<*> -> {
val typedValue = updaterResult.value as? NetworkWriteResponse val typedValue = updaterResult.value as? Response
if (typedValue == null) { if (typedValue == null) {
UpdaterResult.Success.Untyped(updaterResult.value) UpdaterResult.Success.Untyped(updaterResult.value)
} else { } else {
@ -133,9 +133,9 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
} }
@AnyThread @AnyThread
private suspend fun <NetworkWriteResponse : Any> updateWriteRequestQueue(key: Key, created: Long, updaterResult: UpdaterResult.Success) { private suspend fun <Response : Any> updateWriteRequestQueue(key: Key, created: Long, updaterResult: UpdaterResult.Success) {
val nextWriteRequestQueue = withWriteRequestQueueLock<ArrayDeque<StoreWriteRequest<Key, CommonRepresentation, *>>, NetworkWriteResponse>(key) { val nextWriteRequestQueue = withWriteRequestQueueLock(key) {
val outstandingWriteRequests = ArrayDeque<StoreWriteRequest<Key, CommonRepresentation, *>>() val outstandingWriteRequests = ArrayDeque<StoreWriteRequest<Key, Output, *>>()
for (writeRequest in this) { for (writeRequest in this) {
if (writeRequest.created <= created) { if (writeRequest.created <= created) {
@ -143,7 +143,7 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
val storeWriteResponse = when (updaterResult) { val storeWriteResponse = when (updaterResult) {
is UpdaterResult.Success.Typed<*> -> { is UpdaterResult.Success.Typed<*> -> {
val typedValue = updaterResult.value as? NetworkWriteResponse val typedValue = updaterResult.value as? Response
if (typedValue == null) { if (typedValue == null) {
StoreWriteResponse.Success.Untyped(updaterResult.value) StoreWriteResponse.Success.Untyped(updaterResult.value)
} else { } else {
@ -170,10 +170,10 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
} }
@AnyThread @AnyThread
private suspend fun <Output : Any, NetworkWriteResponse : Any> withWriteRequestQueueLock( private suspend fun <Result : Any> withWriteRequestQueueLock(
key: Key, key: Key,
block: suspend WriteRequestQueue<Key, CommonRepresentation, *>.() -> Output block: suspend WriteRequestQueue<Key, Output, *>.() -> Result
): Output = ): Result =
withThreadSafety(key) { withThreadSafety(key) {
writeRequests.lightswitch.lock(writeRequests.mutex) writeRequests.lightswitch.lock(writeRequests.mutex)
val writeRequestQueue = requireNotNull(keyToWriteRequestQueue[key]) val writeRequestQueue = requireNotNull(keyToWriteRequestQueue[key])
@ -182,7 +182,7 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
output output
} }
private suspend fun getLatestWriteRequest(key: Key): StoreWriteRequest<Key, CommonRepresentation, *> = withThreadSafety(key) { private suspend fun getLatestWriteRequest(key: Key): StoreWriteRequest<Key, Output, *> = withThreadSafety(key) {
writeRequests.mutex.lock() writeRequests.mutex.lock()
val output = requireNotNull(keyToWriteRequestQueue[key]?.last()) val output = requireNotNull(keyToWriteRequestQueue[key]?.last())
writeRequests.mutex.unlock() writeRequests.mutex.unlock()
@ -208,13 +208,13 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
keyToWriteRequestQueue[key].isNullOrEmpty() keyToWriteRequestQueue[key].isNullOrEmpty()
} }
private suspend fun <NetworkWriteResponse : Any> addWriteRequestToQueue(writeRequest: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>) = private suspend fun <Response : Any> addWriteRequestToQueue(writeRequest: StoreWriteRequest<Key, Output, Response>) =
withWriteRequestQueueLock<Unit, NetworkWriteResponse>(writeRequest.key) { withWriteRequestQueueLock(writeRequest.key) {
add(writeRequest) add(writeRequest)
} }
@AnyThread @AnyThread
private suspend fun <NetworkWriteResponse : Any> tryEagerlyResolveConflicts(key: Key): EagerConflictResolutionResult<NetworkWriteResponse> = private suspend fun <Response : Any> tryEagerlyResolveConflicts(key: Key): EagerConflictResolutionResult<Response> =
withThreadSafety(key) { withThreadSafety(key) {
val latest = delegate.latestOrNull(key) val latest = delegate.latestOrNull(key)
when { when {
@ -223,7 +223,7 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
try { try {
val updaterResult = updater.post(key, latest).also { updaterResult -> val updaterResult = updater.post(key, latest).also { updaterResult ->
if (updaterResult is UpdaterResult.Success) { if (updaterResult is UpdaterResult.Success) {
updateWriteRequestQueue<NetworkWriteResponse>(key = key, created = now(), updaterResult = updaterResult) updateWriteRequestQueue<Response>(key = key, created = now(), updaterResult = updaterResult)
} }
} }

View file

@ -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 : Any, SourceOfTruthRepresentation : Any>( internal class PersistentSourceOfTruth<Key : Any, Local : Any>(
private val realReader: (Key) -> Flow<SourceOfTruthRepresentation?>, private val realReader: (Key) -> Flow<Local?>,
private val realWriter: suspend (Key, SourceOfTruthRepresentation) -> Unit, private val realWriter: suspend (Key, Local) -> 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, SourceOfTruthRepresentation> { ) : SourceOfTruth<Key, Local> {
override fun reader(key: Key): Flow<SourceOfTruthRepresentation?> = realReader.invoke(key) override fun reader(key: Key): Flow<Local?> = realReader.invoke(key)
override suspend fun write(key: Key, value: SourceOfTruthRepresentation) = realWriter(key, value) override suspend fun write(key: Key, value: Local) = realWriter(key, value)
override suspend fun delete(key: Key) { override suspend fun delete(key: Key) {
realDelete?.invoke(key) realDelete?.invoke(key)
@ -39,20 +39,20 @@ internal class PersistentSourceOfTruth<Key : Any, SourceOfTruthRepresentation :
} }
} }
internal class PersistentNonFlowingSourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any>( internal class PersistentNonFlowingSourceOfTruth<Key : Any, Local : Any>(
private val realReader: suspend (Key) -> SourceOfTruthRepresentation?, private val realReader: suspend (Key) -> Local?,
private val realWriter: suspend (Key, SourceOfTruthRepresentation) -> Unit, private val realWriter: suspend (Key, Local) -> 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, SourceOfTruthRepresentation> { ) : SourceOfTruth<Key, Local> {
override fun reader(key: Key): Flow<SourceOfTruthRepresentation?> = override fun reader(key: Key): Flow<Local?> =
flow { flow {
val sourceOfTruthRepresentation = realReader(key) val sot = realReader(key)
emit(sourceOfTruthRepresentation) emit(sot)
} }
override suspend fun write(key: Key, value: SourceOfTruthRepresentation) { override suspend fun write(key: Key, value: Local) {
return realWriter(key, value) return realWriter(key, value)
} }

View file

@ -27,12 +27,12 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transform
import org.mobilenativefoundation.store.cache5.CacheBuilder import org.mobilenativefoundation.store.cache5.CacheBuilder
import org.mobilenativefoundation.store.store5.CacheType import org.mobilenativefoundation.store.store5.CacheType
import org.mobilenativefoundation.store.store5.Converter
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.SourceOfTruth import org.mobilenativefoundation.store.store5.SourceOfTruth
import org.mobilenativefoundation.store.store5.Store import org.mobilenativefoundation.store.store5.Store
import org.mobilenativefoundation.store.store5.StoreConverter
import org.mobilenativefoundation.store.store5.StoreReadRequest import org.mobilenativefoundation.store.store5.StoreReadRequest
import org.mobilenativefoundation.store.store5.StoreReadResponse import org.mobilenativefoundation.store.store5.StoreReadResponse
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
@ -40,13 +40,13 @@ 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 org.mobilenativefoundation.store.store5.internal.result.StoreDelegateWriteResult import org.mobilenativefoundation.store.store5.internal.result.StoreDelegateWriteResult
internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>( internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
scope: CoroutineScope, scope: CoroutineScope,
fetcher: Fetcher<Key, NetworkRepresentation>, fetcher: Fetcher<Key, Network>,
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>? = null, sourceOfTruth: SourceOfTruth<Key, Local>? = null,
converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null, converter: Converter<Network, Output, Local>? = null,
private val memoryPolicy: MemoryPolicy<Key, CommonRepresentation>? private val memoryPolicy: MemoryPolicy<Key, Output>?
) : Store<Key, CommonRepresentation> { ) : Store<Key, Output> {
/** /**
* 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.
@ -54,13 +54,13 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
* 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 [StoreReadResponse.origin] field). * as if it came from the server (the [StoreReadResponse.origin] field).
*/ */
private val sourceOfTruth: SourceOfTruthWithBarrier<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = private val sourceOfTruth: SourceOfTruthWithBarrier<Key, Network, Output, Local>? =
sourceOfTruth?.let { sourceOfTruth?.let {
SourceOfTruthWithBarrier(it, converter) SourceOfTruthWithBarrier(it, converter)
} }
private val memCache = memoryPolicy?.let { private val memCache = memoryPolicy?.let {
CacheBuilder<Key, CommonRepresentation>().apply { CacheBuilder<Key, Output>().apply {
if (memoryPolicy.hasAccessPolicy) { if (memoryPolicy.hasAccessPolicy) {
expireAfterAccess(memoryPolicy.expireAfterAccess) expireAfterAccess(memoryPolicy.expireAfterAccess)
} }
@ -88,7 +88,7 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
converter = converter converter = converter
) )
override fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>> = override fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<Output>> =
flow { flow {
val cachedToEmit = if (request.shouldSkipCache(CacheType.MEMORY)) { val cachedToEmit = if (request.shouldSkipCache(CacheType.MEMORY)) {
null null
@ -109,7 +109,7 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
request = request, request = request,
networkLock = null, networkLock = null,
piggybackOnly = piggybackOnly piggybackOnly = piggybackOnly
) as Flow<StoreReadResponse<CommonRepresentation>> // when no source of truth Input == Output ) as Flow<StoreReadResponse<Output>> // when no source of truth Input == Output
} else { } else {
diskNetworkCombined(request, sourceOfTruth) diskNetworkCombined(request, sourceOfTruth)
} }
@ -185,8 +185,8 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
*/ */
private fun diskNetworkCombined( private fun diskNetworkCombined(
request: StoreReadRequest<Key>, request: StoreReadRequest<Key>,
sourceOfTruth: SourceOfTruthWithBarrier<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> sourceOfTruth: SourceOfTruthWithBarrier<Key, Network, Output, Local>
): Flow<StoreReadResponse<CommonRepresentation>> { ): Flow<StoreReadResponse<Output>> {
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)
@ -229,7 +229,7 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
val diskValue = diskData.value val diskValue = diskData.value
if (diskValue != null) { if (diskValue != null) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
emit(diskData as StoreReadResponse<CommonRepresentation>) emit(diskData as StoreReadResponse<Output>)
} }
// 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.
@ -267,7 +267,7 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
request: StoreReadRequest<Key>, request: StoreReadRequest<Key>,
networkLock: CompletableDeferred<Unit>?, networkLock: CompletableDeferred<Unit>?,
piggybackOnly: Boolean = false piggybackOnly: Boolean = false
): Flow<StoreReadResponse<NetworkRepresentation>> { ): Flow<StoreReadResponse<Network>> {
return fetcherController return fetcherController
.getFetcher(request.key, piggybackOnly) .getFetcher(request.key, piggybackOnly)
.onStart { .onStart {
@ -279,15 +279,15 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
} }
} }
internal suspend fun write(key: Key, input: CommonRepresentation): StoreDelegateWriteResult = try { internal suspend fun write(key: Key, value: Output): StoreDelegateWriteResult = try {
memCache?.put(key, input) memCache?.put(key, value)
sourceOfTruth?.write(key, input) sourceOfTruth?.write(key, value)
StoreDelegateWriteResult.Success StoreDelegateWriteResult.Success
} catch (error: Throwable) { } catch (error: Throwable) {
StoreDelegateWriteResult.Error.Exception(error) StoreDelegateWriteResult.Error.Exception(error)
} }
internal suspend fun latestOrNull(key: Key): CommonRepresentation? = fromMemCache(key) ?: fromSourceOfTruth(key) internal suspend fun latestOrNull(key: Key): Output? = fromMemCache(key) ?: fromSourceOfTruth(key)
private suspend fun fromSourceOfTruth(key: Key) = sourceOfTruth?.reader(key, CompletableDeferred(Unit))?.map { it.dataOrNull() }?.first() private suspend fun fromSourceOfTruth(key: Key) = sourceOfTruth?.reader(key, CompletableDeferred(Unit))?.map { it.dataOrNull() }?.first()
private fun fromMemCache(key: Key) = memCache?.getIfPresent(key) private fun fromMemCache(key: Key) = memCache?.getIfPresent(key)
} }

View file

@ -3,56 +3,56 @@ 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.Bookkeeper
import org.mobilenativefoundation.store.store5.Converter
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.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 org.mobilenativefoundation.store.store5.Updater import org.mobilenativefoundation.store.store5.Updater
import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any> storeBuilderFromFetcher( fun <Key : Any, Network : Any, Output : Any> storeBuilderFromFetcher(
fetcher: Fetcher<Key, NetworkRepresentation>, fetcher: Fetcher<Key, Network>,
sourceOfTruth: SourceOfTruth<Key, *>? = null, sourceOfTruth: SourceOfTruth<Key, *>? = null,
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, *> = RealStoreBuilder(fetcher, sourceOfTruth) ): StoreBuilder<Key, Network, Output, *> = RealStoreBuilder(fetcher, sourceOfTruth)
fun <Key : Any, CommonRepresentation : Any, NetworkRepresentation : Any, SourceOfTruthRepresentation : Any> storeBuilderFromFetcherAndSourceOfTruth( fun <Key : Any, Output : Any, Network : Any, Local : Any> storeBuilderFromFetcherAndSourceOfTruth(
fetcher: Fetcher<Key, NetworkRepresentation>, fetcher: Fetcher<Key, Network>,
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>, sourceOfTruth: SourceOfTruth<Key, Local>,
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> = RealStoreBuilder(fetcher, sourceOfTruth) ): StoreBuilder<Key, Network, Output, Local> = RealStoreBuilder(fetcher, sourceOfTruth)
internal class RealStoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>( internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any>(
private val fetcher: Fetcher<Key, NetworkRepresentation>, private val fetcher: Fetcher<Key, Network>,
private val sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>? = null private val sourceOfTruth: SourceOfTruth<Key, Local>? = null
) : StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> { ) : StoreBuilder<Key, Network, Output, Local> {
private var scope: CoroutineScope? = null private var scope: CoroutineScope? = null
private var cachePolicy: MemoryPolicy<Key, CommonRepresentation>? = StoreDefaults.memoryPolicy private var cachePolicy: MemoryPolicy<Key, Output>? = StoreDefaults.memoryPolicy
private var converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null private var converter: Converter<Network, Output, Local>? = null
override fun scope(scope: CoroutineScope): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> { override fun scope(scope: CoroutineScope): StoreBuilder<Key, Network, Output, Local> {
this.scope = scope this.scope = scope
return this return this
} }
override fun cachePolicy(memoryPolicy: MemoryPolicy<Key, CommonRepresentation>?): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> { override fun cachePolicy(memoryPolicy: MemoryPolicy<Key, Output>?): StoreBuilder<Key, Network, Output, Local> {
cachePolicy = memoryPolicy cachePolicy = memoryPolicy
return this return this
} }
override fun disableCache(): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> { override fun disableCache(): StoreBuilder<Key, Network, Output, Local> {
cachePolicy = null cachePolicy = null
return this return this
} }
override fun converter(converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> { override fun converter(converter: Converter<Network, Output, Local>): StoreBuilder<Key, Network, Output, Local> {
this.converter = converter this.converter = converter
return this return this
} }
override fun build(): Store<Key, CommonRepresentation> = RealStore( override fun build(): Store<Key, Output> = RealStore(
scope = scope ?: GlobalScope, scope = scope ?: GlobalScope,
sourceOfTruth = sourceOfTruth, sourceOfTruth = sourceOfTruth,
fetcher = fetcher, fetcher = fetcher,
@ -60,11 +60,11 @@ internal class RealStoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRe
converter = converter converter = converter
) )
override fun <NetworkWriteResponse : Any> build( override fun <UpdaterResult : Any> build(
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>, updater: Updater<Key, Output, UpdaterResult>,
bookkeeper: Bookkeeper<Key> bookkeeper: Bookkeeper<Key>
): MutableStore<Key, CommonRepresentation> = ): MutableStore<Key, Output> =
build().asMutableStore<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation, NetworkWriteResponse>( build().asMutableStore<Key, Network, Output, Local, UpdaterResult>(
updater = updater, updater = updater,
bookkeeper = bookkeeper bookkeeper = bookkeeper
) )

View file

@ -2,9 +2,9 @@ package org.mobilenativefoundation.store.store5.impl
import org.mobilenativefoundation.store.store5.StoreWriteRequest import org.mobilenativefoundation.store.store5.StoreWriteRequest
data class RealStoreWriteRequest<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any>( data class RealStoreWriteRequest<Key : Any, Output : Any, Response : Any>(
override val key: Key, override val key: Key,
override val input: CommonRepresentation, override val value: Output,
override val created: Long, override val created: Long,
override val onCompletions: List<OnStoreWriteCompletion>? override val onCompletions: List<OnStoreWriteCompletion>?
) : StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse> ) : StoreWriteRequest<Key, Output, Response>

View file

@ -0,0 +1,9 @@
package org.mobilenativefoundation.store.store5.impl
import org.mobilenativefoundation.store.store5.Validator
internal class RealValidator<Output : Any>(
private val realValidator: suspend (item: Output) -> Boolean
) : Validator<Output> {
override suspend fun isValid(item: Output): Boolean = realValidator(item)
}

View file

@ -26,8 +26,8 @@ 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.Converter
import org.mobilenativefoundation.store.store5.SourceOfTruth import org.mobilenativefoundation.store.store5.SourceOfTruth
import org.mobilenativefoundation.store.store5.StoreConverter
import org.mobilenativefoundation.store.store5.StoreReadResponse import org.mobilenativefoundation.store.store5.StoreReadResponse
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
@ -39,9 +39,9 @@ import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
* dispatching values to downstream while a write is in progress. * dispatching values to downstream while a write is in progress.
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>( internal class SourceOfTruthWithBarrier<Key : Any, Network : Any, Output : Any, Local : Any>(
private val delegate: SourceOfTruth<Key, SourceOfTruthRepresentation>, private val delegate: SourceOfTruth<Key, Local>,
private val converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null, private val converter: Converter<Network, Output, Local>? = 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.
@ -59,7 +59,7 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
*/ */
private val versionCounter = atomic(0L) private val versionCounter = atomic(0L)
fun reader(key: Key, lock: CompletableDeferred<Unit>): Flow<StoreReadResponse<CommonRepresentation?>> { fun reader(key: Key, lock: CompletableDeferred<Unit>): Flow<StoreReadResponse<Output?>> {
return flow { return flow {
val barrier = barriers.acquire(key) val barrier = barriers.acquire(key)
val readerVersion: Long = versionCounter.incrementAndGet() val readerVersion: Long = versionCounter.incrementAndGet()
@ -74,9 +74,9 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
} else { } else {
null null
} }
val readFlow: Flow<StoreReadResponse<CommonRepresentation?>> = when (barrierMessage) { val readFlow: Flow<StoreReadResponse<Output?>> = when (barrierMessage) {
is BarrierMsg.Open -> is BarrierMsg.Open ->
delegate.reader(key).mapIndexed { index, sourceOfTruthRepresentation -> delegate.reader(key).mapIndexed { index, sourceOfTruth ->
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
@ -89,8 +89,8 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
StoreReadResponseOrigin.SourceOfTruth StoreReadResponseOrigin.SourceOfTruth
} }
val value = sourceOfTruthRepresentation as? CommonRepresentation ?: if (sourceOfTruthRepresentation != null) { val value = sourceOfTruth as? Output ?: if (sourceOfTruth != null) {
converter?.fromSourceOfTruthRepresentationToCommonRepresentation(sourceOfTruthRepresentation) converter?.fromLocalToOutput(sourceOfTruth)
} else { } else {
null null
} }
@ -101,11 +101,11 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
} else { } else {
StoreReadResponse.Data( StoreReadResponse.Data(
origin = StoreReadResponseOrigin.SourceOfTruth, origin = StoreReadResponseOrigin.SourceOfTruth,
value = sourceOfTruthRepresentation as? CommonRepresentation value = sourceOfTruth as? Output
?: if (sourceOfTruthRepresentation != null) converter?.fromSourceOfTruthRepresentationToCommonRepresentation( ?: if (sourceOfTruth != null) converter?.fromLocalToOutput(
sourceOfTruthRepresentation sourceOfTruth
) else null ) else null
) as StoreReadResponse<CommonRepresentation?> ) as StoreReadResponse<Output?>
} }
}.catch { throwable -> }.catch { throwable ->
this.emit( this.emit(
@ -146,12 +146,12 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
suspend fun write(key: Key, value: CommonRepresentation) { suspend fun write(key: Key, value: Output) {
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 {
val input = value as? SourceOfTruthRepresentation ?: converter?.fromCommonRepresentationToSourceOfTruthRepresentation(value) val input = value as? Local ?: converter?.fromOutputToLocal(value)
if (input != null) { if (input != null) {
delegate.write(key, input) delegate.write(key, input)
} }

View file

@ -15,7 +15,7 @@ import org.mobilenativefoundation.store.store5.impl.RealStore
* Helper factory that will return data for [key] if it is cached otherwise will return * Helper factory that will return data for [key] if it is cached otherwise will return
* fresh/network data (updating your caches) * fresh/network data (updating your caches)
*/ */
suspend fun <Key : Any, CommonRepresentation : Any> Store<Key, CommonRepresentation>.get(key: Key) = suspend fun <Key : Any, Output : Any> Store<Key, Output>.get(key: Key) =
stream(StoreReadRequest.cached(key, refresh = false)) stream(StoreReadRequest.cached(key, refresh = false))
.filterNot { it is StoreReadResponse.Loading || it is StoreReadResponse.NoNewData } .filterNot { it is StoreReadResponse.Loading || it is StoreReadResponse.NoNewData }
.first() .first()
@ -29,18 +29,18 @@ suspend fun <Key : Any, CommonRepresentation : Any> Store<Key, CommonRepresentat
* 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
*/ */
suspend fun <Key : Any, CommonRepresentation : Any> Store<Key, CommonRepresentation>.fresh(key: Key) = suspend fun <Key : Any, Output : Any> Store<Key, Output>.fresh(key: Key) =
stream(StoreReadRequest.fresh(key)) stream(StoreReadRequest.fresh(key))
.filterNot { it is StoreReadResponse.Loading || it is StoreReadResponse.NoNewData } .filterNot { it is StoreReadResponse.Loading || it is StoreReadResponse.NoNewData }
.first() .first()
.requireData() .requireData()
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any, NetworkWriteResponse : Any> Store<Key, CommonRepresentation>.asMutableStore( fun <Key : Any, Network : Any, Output : Any, Local : Any, Response : Any> Store<Key, Output>.asMutableStore(
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>, updater: Updater<Key, Output, Response>,
bookkeeper: Bookkeeper<Key> bookkeeper: Bookkeeper<Key>
): MutableStore<Key, CommonRepresentation> { ): MutableStore<Key, Output> {
val delegate = this as? RealStore<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> val delegate = this as? RealStore<Key, Network, Output, Local>
?: throw Exception("MutableStore requires Store to be built using StoreBuilder") ?: throw Exception("MutableStore requires Store to be built using StoreBuilder")
return RealMutableStore( return RealMutableStore(

View file

@ -1,3 +0,0 @@
package org.mobilenativefoundation.store.store5.internal.definition
typealias Converter<Input, Output> = (input: Input) -> Output

View file

@ -2,4 +2,4 @@ package org.mobilenativefoundation.store.store5.internal.definition
import org.mobilenativefoundation.store.store5.StoreWriteRequest import org.mobilenativefoundation.store.store5.StoreWriteRequest
typealias WriteRequestQueue<Key, CommonRepresentation, NetworkWriteResponse> = ArrayDeque<StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>> typealias WriteRequestQueue<Key, Output, Response> = ArrayDeque<StoreWriteRequest<Key, Output, Response>>

View file

@ -2,11 +2,11 @@ package org.mobilenativefoundation.store.store5.internal.result
import org.mobilenativefoundation.store.store5.UpdaterResult import org.mobilenativefoundation.store.store5.UpdaterResult
sealed class EagerConflictResolutionResult<out NetworkWriteResponse : Any> { sealed class EagerConflictResolutionResult<out Response : Any> {
sealed class Success<NetworkWriteResponse : Any> : EagerConflictResolutionResult<NetworkWriteResponse>() { sealed class Success<Response : Any> : EagerConflictResolutionResult<Response>() {
object NoConflicts : Success<Nothing>() object NoConflicts : Success<Nothing>()
data class ConflictsResolved<NetworkWriteResponse : Any>(val value: UpdaterResult.Success) : Success<NetworkWriteResponse>() data class ConflictsResolved<Response : Any>(val value: UpdaterResult.Success) : Success<Response>()
} }
sealed class Error : EagerConflictResolutionResult<Nothing>() { sealed class Error : EagerConflictResolutionResult<Nothing>() {

View file

@ -211,6 +211,6 @@ class FetcherResponseTests {
) )
} }
private fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>.buildWithTestScope() = private fun <Key : Any, Network : Any, Output : Any, Local : Any> StoreBuilder<Key, Network, Output, Local>.buildWithTestScope() =
scope(testScope).build() scope(testScope).build()
} }

View file

@ -865,6 +865,6 @@ class FlowStoreTests {
) )
) )
private fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>.buildWithTestScope() = private fun <Key : Any, Network : Any, Output : Any, Local : Any> StoreBuilder<Key, Network, Output, Local>.buildWithTestScope() =
scope(testScope).build() scope(testScope).build()
} }

View file

@ -5,14 +5,14 @@ import kotlinx.coroutines.flow.flow
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.asMutableStore import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore
import org.mobilenativefoundation.store.store5.util.fake.NoteApi import org.mobilenativefoundation.store.store5.util.fake.NotesApi
import org.mobilenativefoundation.store.store5.util.fake.NoteBookkeeping import org.mobilenativefoundation.store.store5.util.fake.NotesBookkeeping
import org.mobilenativefoundation.store.store5.util.model.CommonNote
import org.mobilenativefoundation.store.store5.util.model.NetworkNote
import org.mobilenativefoundation.store.store5.util.model.Note 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.NoteData
import org.mobilenativefoundation.store.store5.util.model.NoteNetworkRepresentation import org.mobilenativefoundation.store.store5.util.model.NotesWriteResponse
import org.mobilenativefoundation.store.store5.util.model.NoteNetworkWriteResponse import org.mobilenativefoundation.store.store5.util.model.SOTNote
import org.mobilenativefoundation.store.store5.util.model.NoteSourceOfTruthRepresentation
import kotlin.test.BeforeTest import kotlin.test.BeforeTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -20,22 +20,22 @@ import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalStoreApi::class) @OptIn(ExperimentalCoroutinesApi::class, ExperimentalStoreApi::class)
class UpdaterTests { class UpdaterTests {
private val testScope = TestScope() private val testScope = TestScope()
private lateinit var api: NoteApi private lateinit var api: NotesApi
private lateinit var bookkeeping: NoteBookkeeping private lateinit var bookkeeping: NotesBookkeeping
@BeforeTest @BeforeTest
fun before() { fun before() {
api = NoteApi() api = NotesApi()
bookkeeping = NoteBookkeeping() bookkeeping = NotesBookkeeping()
} }
@Test @Test
fun givenEmptyMarketWhenWriteThenSuccessResponsesAndApiUpdated() = testScope.runTest { fun givenEmptyMarketWhenWriteThenSuccessResponsesAndApiUpdated() = testScope.runTest {
val updater = Updater.by<String, NoteCommonRepresentation, NoteNetworkWriteResponse>( val updater = Updater.by<String, CommonNote, NotesWriteResponse>(
post = { key, commonRepresentation -> post = { key, common ->
val networkWriteResponse = api.post(key, commonRepresentation) val response = api.post(key, common)
if (networkWriteResponse.ok) { if (response.ok) {
UpdaterResult.Success.Typed(networkWriteResponse) UpdaterResult.Success.Typed(response)
} else { } else {
UpdaterResult.Error.Message("Failed to sync") UpdaterResult.Error.Message("Failed to sync")
} }
@ -48,14 +48,14 @@ class UpdaterTests {
clearAll = bookkeeping::clear clearAll = bookkeeping::clear
) )
val store = StoreBuilder.from<String, NoteNetworkRepresentation, NoteCommonRepresentation>( val store = StoreBuilder.from<String, NetworkNote, CommonNote>(
fetcher = Fetcher.ofFlow { key -> fetcher = Fetcher.ofFlow { key ->
val networkRepresentation = NoteNetworkRepresentation(NoteData.Single(Note("$key-id", "$key-title", "$key-content"))) val network = NetworkNote(NoteData.Single(Note("$key-id", "$key-title", "$key-content")))
flow { emit(networkRepresentation) } flow { emit(network) }
} }
) )
.build() .build()
.asMutableStore<String, NoteNetworkRepresentation, NoteCommonRepresentation, NoteSourceOfTruthRepresentation, NoteNetworkWriteResponse>( .asMutableStore<String, NetworkNote, CommonNote, SOTNote, NotesWriteResponse>(
updater = updater, updater = updater,
bookkeeper = bookkeeper bookkeeper = bookkeeper
) )
@ -64,14 +64,14 @@ class UpdaterTests {
val noteTitle = "1-title" val noteTitle = "1-title"
val noteContent = "1-content" val noteContent = "1-content"
val noteData = NoteData.Single(Note(noteKey, noteTitle, noteContent)) val noteData = NoteData.Single(Note(noteKey, noteTitle, noteContent))
val writeRequest = StoreWriteRequest.of<String, NoteCommonRepresentation, NoteNetworkWriteResponse>( val writeRequest = StoreWriteRequest.of<String, CommonNote, NotesWriteResponse>(
key = noteKey, key = noteKey,
input = NoteCommonRepresentation(noteData) value = CommonNote(noteData)
) )
val storeWriteResponse = store.write(writeRequest) val storeWriteResponse = store.write(writeRequest)
assertEquals(StoreWriteResponse.Success.Typed(NoteNetworkWriteResponse(noteKey, true)), storeWriteResponse) assertEquals(StoreWriteResponse.Success.Typed(NotesWriteResponse(noteKey, true)), storeWriteResponse)
assertEquals(NoteNetworkRepresentation(noteData), api.db[noteKey]) assertEquals(NetworkNote(noteData), api.db[noteKey])
} }
} }

View file

@ -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 : Any, SourceOfTruthRepresentation : Any>( class SimplePersisterAsFlowable<Key : Any, Output : Any>(
private val reader: suspend (Key) -> SourceOfTruthRepresentation?, private val reader: suspend (Key) -> Output?,
private val writer: suspend (Key, SourceOfTruthRepresentation) -> Unit, private val writer: suspend (Key, Output) -> Unit,
private val delete: (suspend (Key) -> Unit)? = null private val delete: (suspend (Key) -> Unit)? = null
) { ) {
@ -23,14 +23,14 @@ class SimplePersisterAsFlowable<Key : Any, SourceOfTruthRepresentation : Any>(
private val versionTracker = KeyTracker<Key>() private val versionTracker = KeyTracker<Key>()
fun flowReader(key: Key): Flow<SourceOfTruthRepresentation?> = flow { fun flowReader(key: Key): Flow<Output?> = flow {
versionTracker.keyFlow(key).collect { versionTracker.keyFlow(key).collect {
emit(reader(key)) emit(reader(key))
} }
} }
suspend fun flowWriter(key: Key, input: SourceOfTruthRepresentation) { suspend fun flowWriter(key: Key, value: Output) {
writer(key, input) writer(key, value)
versionTracker.invalidate(key) versionTracker.invalidate(key)
} }
@ -42,7 +42,7 @@ class SimplePersisterAsFlowable<Key : Any, SourceOfTruthRepresentation : Any>(
} }
} }
fun <Key : Any, SourceOfTruthRepresentation : Any> SimplePersisterAsFlowable<Key, SourceOfTruthRepresentation>.asSourceOfTruth() = fun <Key : Any, Output : Any> SimplePersisterAsFlowable<Key, Output>.asSourceOfTruth() =
SourceOfTruth.of( SourceOfTruth.of(
reader = ::flowReader, reader = ::flowReader,
writer = ::flowWriter, writer = ::flowWriter,

View file

@ -1,6 +1,6 @@
package org.mobilenativefoundation.store.store5.util package org.mobilenativefoundation.store.store5.util
internal interface TestApi<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> { internal interface TestApi<Key : Any, Network : Any, Output : Any, Response : Any> {
fun get(key: Key, fail: Boolean = false): NetworkRepresentation? fun get(key: Key, fail: Boolean = false): Network?
fun post(key: Key, value: CommonRepresentation, fail: Boolean = false): NetworkWriteResponse fun post(key: Key, value: Output, fail: Boolean = false): Response
} }

View file

@ -11,7 +11,7 @@ import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
* Helper factory that will return [StoreReadResponse.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, CommonRepresentation : Any> Store<Key, CommonRepresentation>.getData(key: Key) = suspend fun <Key : Any, Output : Any> Store<Key, Output>.getData(key: Key) =
stream( stream(
StoreReadRequest.cached(key, refresh = false) StoreReadRequest.cached(key, refresh = false)
).filterNot { ).filterNot {

View file

@ -1,40 +0,0 @@
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")))
}
}

View file

@ -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.CommonNote
import org.mobilenativefoundation.store.store5.util.model.NetworkNote
import org.mobilenativefoundation.store.store5.util.model.Note
import org.mobilenativefoundation.store.store5.util.model.NoteData
import org.mobilenativefoundation.store.store5.util.model.NotesWriteResponse
internal class NotesApi : TestApi<String, NetworkNote, CommonNote, NotesWriteResponse> {
internal val db = mutableMapOf<String, NetworkNote>()
init {
seed()
}
override fun get(key: String, fail: Boolean): NetworkNote? {
if (fail) {
throw Exception()
}
return db[key]
}
override fun post(key: String, value: CommonNote, fail: Boolean): NotesWriteResponse {
if (fail) {
throw Exception()
}
db[key] = NetworkNote(value.data)
return NotesWriteResponse(key, true)
}
private fun seed() {
db["1-id"] = NetworkNote(NoteData.Single(Note("1-id", "1-title", "1-content")))
db["2-id"] = NetworkNote(NoteData.Single(Note("2-id", "2-title", "2-content")))
db["3-id"] = NetworkNote(NoteData.Single(Note("3-id", "3-title", "3-content")))
}
}

View file

@ -1,6 +1,6 @@
package org.mobilenativefoundation.store.store5.util.fake package org.mobilenativefoundation.store.store5.util.fake
class NoteBookkeeping { class NotesBookkeeping {
private val log: MutableMap<String, Long?> = mutableMapOf() private val log: MutableMap<String, Long?> = mutableMapOf()
fun setLastFailedSync(key: String, timestamp: Long, fail: Boolean = false): Boolean { fun setLastFailedSync(key: String, timestamp: Long, fail: Boolean = false): Boolean {
if (fail) { if (fail) {

View file

@ -5,20 +5,20 @@ internal sealed class NoteData {
data class Collection(val items: List<Note>) : NoteData() data class Collection(val items: List<Note>) : NoteData()
} }
internal data class NoteNetworkWriteResponse( internal data class NotesWriteResponse(
val key: String, val key: String,
val ok: Boolean val ok: Boolean
) )
internal data class NoteNetworkRepresentation( internal data class NetworkNote(
val data: NoteData? = null val data: NoteData? = null
) )
internal data class NoteCommonRepresentation( internal data class CommonNote(
val data: NoteData? = null val data: NoteData? = null
) )
internal data class NoteSourceOfTruthRepresentation( internal data class SOTNote(
val data: NoteData? = null val data: NoteData? = null
) )