* 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.
- 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.
### Concepts
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
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**.
#### Android
```kotlin
STORE_VERSION = "5.0.0-alpha03"
implementation "org.mobilenativefoundation.store:store5:5.0.0-alpha03"
```
### Android
```groovy
implementation "org.mobilenativefoundation.store:store5:$STORE_VERSION"
```
### Multiplatform (Common, JVM, Native, JS)
#### Multiplatform (Common, JVM, Native, JS)
```kotlin
commonMain {
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
simpler examples showing each piece:
#### Building Your First Store
```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()
.from<Key, Network, Output, Local>(fetcher, sourceOfTruth)
.converter(converter)
.validator(validator)
.build(updater, bookkeeper)
```
With the above setup you have:
#### Creating
+ In-memory caching for rotation
+ 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>>`.
##### Request
```kotlin
val store = StoreBuilder
.from(Fetcher.ofFlow { articleId -> api.getArticle(articleId) }) // api returns Flow<Article>
.build()
store.write(
request = StoreWriteRequest.of<Key, Output, Response>(
key = key,
value = value
)
)
```
Store uses generic keys as identifiers for data. A key can be any value object that properly
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()`!!).
##### Response
Note: We highly recommend using built-in types that implement `equals` and `hashcode` or
Kotlin `data` classes for complex keys.
```text
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
signature:
##### Request
```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
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.
##### Response
* The `Loading` class only has an `origin` field. This can provide you information like "network is
fetching data", which can be a good signal to activate the loading spinner in your UI.
* 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`.
```text
1. StoreReadResponse.Data(value, origin = StoreReadResponseOrigin.Cache)
```
When an error happens, `Store` does not throw an exception, instead, it wraps it in
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.
#### Updating
##### Request
```kotlin
viewModelScope.launch {
store.stream(StoreRequest.cached(key = key, refresh = true)).collect { response ->
when (response) {
is StoreResponse.Loading -> showLoadingSpinner()
is StoreResponse.Data -> {
if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
updateUI(response.value)
}
is StoreResponse.Error -> {
if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
showError(response.error)
}
}
}
}
store.write(
request = StoreWriteRequest.of<Key, Output, Response>(
key = key,
value = newValue
)
)
```
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
available, it will be returned from the in memory cache or the sourceOfTruth. An error will be
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
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`
or `sourceOfTruth`.
```text
1. StoreWriteResponse.Success.Typed<Response>(response)
```
#### Deleting
##### Request
```kotlin
lifecycleScope.launchWhenStarted {
val article = store.get(key)
updateUI(article)
}
store.clear(key)
```
The first time you call to `suspend store.get(key)`, the response will be stored in an in-memory
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
### License
```text
Copyright (c) 2022 Mobile Native Foundation.

View file

@ -2,7 +2,7 @@ package org.mobilenativefoundation.store.cache5
import kotlin.time.Duration
class CacheBuilder<Key : Any, CommonRepresentation : Any> {
class CacheBuilder<Key : Any, Output : Any> {
internal var concurrencyLevel = 4
private set
internal val initialCapacity = 16
@ -14,41 +14,41 @@ class CacheBuilder<Key : Any, CommonRepresentation : Any> {
private set
internal var expireAfterWrite: Duration = Duration.INFINITE
private set
internal var weigher: Weigher<Key, CommonRepresentation>? = null
internal var weigher: Weigher<Key, Output>? = null
private set
internal var ticker: Ticker? = null
private set
fun concurrencyLevel(producer: () -> Int): CacheBuilder<Key, CommonRepresentation> = apply {
fun concurrencyLevel(producer: () -> Int): CacheBuilder<Key, Output> = apply {
concurrencyLevel = producer.invoke()
}
fun maximumSize(maximumSize: Long): CacheBuilder<Key, CommonRepresentation> = apply {
fun maximumSize(maximumSize: Long): CacheBuilder<Key, Output> = apply {
if (maximumSize < 0) {
throw IllegalArgumentException("Maximum size must be non-negative.")
}
this.maximumSize = maximumSize
}
fun expireAfterAccess(duration: Duration): CacheBuilder<Key, CommonRepresentation> = apply {
fun expireAfterAccess(duration: Duration): CacheBuilder<Key, Output> = apply {
if (duration.isNegative()) {
throw IllegalArgumentException("Duration must be non-negative.")
}
expireAfterAccess = duration
}
fun expireAfterWrite(duration: Duration): CacheBuilder<Key, CommonRepresentation> = apply {
fun expireAfterWrite(duration: Duration): CacheBuilder<Key, Output> = apply {
if (duration.isNegative()) {
throw IllegalArgumentException("Duration must be non-negative.")
}
expireAfterWrite = duration
}
fun ticker(ticker: Ticker): CacheBuilder<Key, CommonRepresentation> = apply {
fun ticker(ticker: Ticker): CacheBuilder<Key, Output> = apply {
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) {
throw IllegalArgumentException("Maximum weight must be non-negative.")
}
@ -57,7 +57,7 @@ class CacheBuilder<Key : Any, CommonRepresentation : Any> {
this.weigher = weigher
}
fun build(): Cache<Key, CommonRepresentation> {
fun build(): Cache<Key, Output> {
if (maximumSize != -1L && weigher != null) {
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
* 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].
*/
operator fun invoke(key: Key): Flow<FetcherResult<NetworkRepresentation>>
operator fun invoke(key: Key): Flow<FetcherResult<Network>>
companion object {
/**
@ -37,9 +37,9 @@ interface Fetcher<Key : Any, NetworkRepresentation : Any> {
*
* @param flowFactory a factory for a [Flow]ing source of network records.
*/
fun <Key : Any, NetworkRepresentation : Any> ofResultFlow(
flowFactory: (Key) -> Flow<FetcherResult<NetworkRepresentation>>
): Fetcher<Key, NetworkRepresentation> = FactoryFetcher(factory = flowFactory)
fun <Key : Any, Network : Any> ofResultFlow(
flowFactory: (Key) -> Flow<FetcherResult<Network>>
): Fetcher<Key, Network> = FactoryFetcher(factory = flowFactory)
/**
* "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.
*/
fun <Key : Any, NetworkRepresentation : Any> ofResult(
fetch: suspend (Key) -> FetcherResult<NetworkRepresentation>
): Fetcher<Key, NetworkRepresentation> = ofResultFlow(fetch.asFlow())
fun <Key : Any, Network : Any> ofResult(
fetch: suspend (Key) -> FetcherResult<Network>
): Fetcher<Key, Network> = ofResultFlow(fetch.asFlow())
/**
* "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.
*/
fun <Key : Any, NetworkRepresentation : Any> ofFlow(
flowFactory: (Key) -> Flow<NetworkRepresentation>
): Fetcher<Key, NetworkRepresentation> = FactoryFetcher { key: Key ->
fun <Key : Any, Network : Any> ofFlow(
flowFactory: (Key) -> Flow<Network>
): Fetcher<Key, Network> = FactoryFetcher { key: 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)) }
}
@ -87,19 +87,19 @@ interface Fetcher<Key : Any, NetworkRepresentation : Any> {
*
* @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())
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 {
emit(invoke(key))
}
}
private class FactoryFetcher<Key : Any, NetworkRepresentation : Any>(
private val factory: (Key) -> Flow<FetcherResult<NetworkRepresentation>>
) : Fetcher<Key, NetworkRepresentation> {
override fun invoke(key: Key): Flow<FetcherResult<NetworkRepresentation>> = factory(key)
private class FactoryFetcher<Key : Any, Network : Any>(
private val factory: (Key) -> Flow<FetcherResult<Network>>
) : Fetcher<Key, Network> {
override fun invoke(key: Key): Flow<FetcherResult<Network>> = factory(key)
}
}
}

View file

@ -1,7 +1,7 @@
package org.mobilenativefoundation.store.store5
sealed class FetcherResult<out NetworkRepresentation : Any> {
data class Data<NetworkRepresentation : Any>(val value: NetworkRepresentation) : FetcherResult<NetworkRepresentation>()
sealed class FetcherResult<out Network : Any> {
data class Data<Network : Any>(val value: Network) : FetcherResult<Network>()
sealed class Error : FetcherResult<Nothing>() {
data class Exception(val error: Throwable) : 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
interface MutableStore<Key : Any, CommonRepresentation : Any> :
Read.StreamWithConflictResolution<Key, CommonRepresentation>,
Write<Key, CommonRepresentation>,
Write.Stream<Key, CommonRepresentation>,
interface MutableStore<Key : Any, Output : Any> :
Read.StreamWithConflictResolution<Key, Output>,
Write<Key, Output>,
Write.Stream<Key, Output>,
Clear.Key<Key>,
Clear

View file

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

View file

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

View file

@ -3,15 +3,15 @@ package org.mobilenativefoundation.store.store5
import kotlinx.coroutines.flow.Flow
interface Read {
interface Stream<Key : Any, CommonRepresentation : Any> {
interface Stream<Key : Any, Output : Any> {
/**
* Return a flow for the given key
* @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> {
fun <NetworkWriteResponse : Any> stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>>
interface StreamWithConflictResolution<Key : Any, Output : Any> {
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.
*
*/
interface SourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any> {
interface SourceOfTruth<Key : Any, Local : Any> {
/**
* Used by [Store] to read records from the source of truth.
*
* @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
@ -66,7 +66,7 @@ interface SourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any> {
*
* @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.
@ -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 deleteAll function for deleting all records in the source of truth
*/
fun <Key : Any, SourceOfTruthRepresentation : Any> of(
nonFlowReader: suspend (Key) -> SourceOfTruthRepresentation?,
writer: suspend (Key, SourceOfTruthRepresentation) -> Unit,
fun <Key : Any, Local : Any> of(
nonFlowReader: suspend (Key) -> Local?,
writer: suspend (Key, Local) -> Unit,
delete: (suspend (Key) -> Unit)? = null,
deleteAll: (suspend () -> Unit)? = null
): SourceOfTruth<Key, SourceOfTruthRepresentation> = PersistentNonFlowingSourceOfTruth(
): SourceOfTruth<Key, Local> = PersistentNonFlowingSourceOfTruth(
realReader = nonFlowReader,
realWriter = writer,
realDelete = delete,
@ -112,12 +112,12 @@ interface SourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any> {
* @param deleteAll function for deleting all records in the source of truth
*/
@JvmName("ofFlow")
fun <Key : Any, SourceOfTruthRepresentation : Any> of(
reader: (Key) -> Flow<SourceOfTruthRepresentation?>,
writer: suspend (Key, SourceOfTruthRepresentation) -> Unit,
fun <Key : Any, Local : Any> of(
reader: (Key) -> Flow<Local?>,
writer: suspend (Key, Local) -> Unit,
delete: (suspend (Key) -> Unit)? = null,
deleteAll: (suspend () -> Unit)? = null
): SourceOfTruth<Key, SourceOfTruthRepresentation> = PersistentSourceOfTruth(
): SourceOfTruth<Key, Local> = PersistentSourceOfTruth(
realReader = reader,
realWriter = writer,
realDelete = delete,

View file

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

View file

@ -22,37 +22,37 @@ import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcherAndSo
/**
* Main entry point for creating a [Store].
*/
interface StoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> {
fun build(): Store<Key, CommonRepresentation>
interface StoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any> {
fun build(): Store<Key, Output>
fun <NetworkWriteResponse : Any> build(
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>,
fun <Response : Any> build(
updater: Updater<Key, Output, Response>,
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
* the scope that sharing/multicasting happens in you can pass a @param [scope]
*
* @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
* or size based eviction
* 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
*/
fun disableCache(): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
fun disableCache(): StoreBuilder<Key, Network, Output, Local>
fun converter(converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>):
StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
fun converter(converter: Converter<Network, Output, Local>):
StoreBuilder<Key, Network, Output, Local>
companion object {
@ -61,9 +61,9 @@ interface StoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentat
*
* @param fetcher a [Fetcher] flow of network records.
*/
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any> from(
fetcher: Fetcher<Key, NetworkRepresentation>,
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, *> = storeBuilderFromFetcher(fetcher = fetcher)
fun <Key : Any, Network : Any, Output : Any> from(
fetcher: Fetcher<Key, Network>,
): StoreBuilder<Key, Network, Output, *> = storeBuilderFromFetcher(fetcher = fetcher)
/**
* 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 sourceOfTruth a [SourceOfTruth] for the store.
*/
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> from(
fetcher: Fetcher<Key, NetworkRepresentation>,
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> =
fun <Key : Any, Network : Any, Output : Any, Local : Any> from(
fetcher: Fetcher<Key, Network>,
sourceOfTruth: SourceOfTruth<Key, Local>
): StoreBuilder<Key, Network, Output, Local> =
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
* 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.
*/
@ -36,8 +36,8 @@ sealed class StoreReadResponse<out CommonRepresentation> {
/**
* Data dispatched by [Store]
*/
data class Data<CommonRepresentation>(val value: CommonRepresentation, override val origin: StoreReadResponseOrigin) :
StoreReadResponse<CommonRepresentation>()
data class Data<Output>(val value: Output, override val origin: StoreReadResponseOrigin) :
StoreReadResponse<Output>()
/**
* 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.
*/
fun requireData(): CommonRepresentation {
fun requireData(): Output {
return when (this) {
is Data -> value
is Error -> this.doThrow()
@ -96,7 +96,7 @@ sealed class StoreReadResponse<out CommonRepresentation> {
/**
* If there is data available, returns it; otherwise returns null.
*/
fun dataOrNull(): CommonRepresentation? = when (this) {
fun dataOrNull(): Output? = when (this) {
is Data -> value
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.RealStoreWriteRequest
interface StoreWriteRequest<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> {
interface StoreWriteRequest<Key : Any, Output : Any, Response : Any> {
val key: Key
val input: CommonRepresentation
val value: Output
val created: Long
val onCompletions: List<OnStoreWriteCompletion>?
companion object {
fun <Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> of(
fun <Key : Any, Output : Any, Response : Any> of(
key: Key,
input: CommonRepresentation,
value: Output,
onCompletions: List<OnStoreWriteCompletion>? = null,
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 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()
}

View file

@ -1,35 +1,35 @@
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.
* @see [StoreWriteRequest]
*/
interface Updater<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> {
interface Updater<Key : Any, Output : Any, Response : Any> {
/**
* Makes HTTP POST request.
*/
suspend fun post(key: Key, input: CommonRepresentation): UpdaterResult
suspend fun post(key: Key, value: Output): UpdaterResult
/**
* Executes on network completion.
*/
val onCompletion: OnUpdaterCompletion<NetworkWriteResponse>?
val onCompletion: OnUpdaterCompletion<Response>?
companion object {
fun <Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> by(
post: PostRequest<Key, CommonRepresentation>,
onCompletion: OnUpdaterCompletion<NetworkWriteResponse>? = null,
): Updater<Key, CommonRepresentation, NetworkWriteResponse> = RealNetworkUpdater(
fun <Key : Any, Output : Any, Response : Any> by(
post: PostRequest<Key, Output>,
onCompletion: OnUpdaterCompletion<Response>? = null,
): Updater<Key, Output, Response> = RealNetworkUpdater(
post, onCompletion
)
}
}
internal class RealNetworkUpdater<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any>(
private val realPost: PostRequest<Key, CommonRepresentation>,
override val onCompletion: OnUpdaterCompletion<NetworkWriteResponse>?,
) : Updater<Key, CommonRepresentation, NetworkWriteResponse> {
override suspend fun post(key: Key, input: CommonRepresentation): UpdaterResult = realPost(key, input)
internal class RealNetworkUpdater<Key : Any, Output : Any, Response : Any>(
private val realPost: PostRequest<Key, Output>,
override val onCompletion: OnUpdaterCompletion<Response>?,
) : Updater<Key, Output, Response> {
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 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()
}

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
interface Write<Key : Any, CommonRepresentation : Any> {
interface Write<Key : Any, Output : Any> {
@ExperimentalStoreApi
suspend fun <NetworkWriteResponse : Any> write(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): StoreWriteResponse
interface Stream<Key : Any, CommonRepresentation : Any> {
suspend fun <Response : Any> write(request: StoreWriteRequest<Key, Output, Response>): StoreWriteResponse
interface Stream<Key : Any, Output : Any> {
@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.withContext
import org.mobilenativefoundation.store.multicast5.Multicaster
import org.mobilenativefoundation.store.store5.Converter
import org.mobilenativefoundation.store.store5.Fetcher
import org.mobilenativefoundation.store.store5.FetcherResult
import org.mobilenativefoundation.store.store5.SourceOfTruth
import org.mobilenativefoundation.store.store5.StoreConverter
import org.mobilenativefoundation.store.store5.StoreReadResponse
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
* 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
*/
@ -48,14 +48,14 @@ internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonR
/**
* 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
* 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
private val fetchers = RefCountedResource(
@ -69,7 +69,7 @@ internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonR
StoreReadResponse.Data(
it.value,
origin = StoreReadResponseOrigin.Fetcher
) as StoreReadResponse<NetworkRepresentation>
) as StoreReadResponse<Network>
}
is FetcherResult.Error.Message -> StoreReadResponse.Error.Message(
@ -92,9 +92,9 @@ internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonR
*/
piggybackingDownstream = true,
onEach = { response ->
response.dataOrNull()?.let { networkRepresentation ->
response.dataOrNull()?.let { network ->
val input =
networkRepresentation as? CommonRepresentation ?: converter?.fromNetworkRepresentationToCommonRepresentation(networkRepresentation)
network as? Output ?: converter?.fromNetworkToOutput(network)
if (input != null) {
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()
}
)
fun getFetcher(key: Key, piggybackOnly: Boolean = false): Flow<StoreReadResponse<NetworkRepresentation>> {
fun getFetcher(key: Key, piggybackOnly: Boolean = false): Flow<StoreReadResponse<Network>> {
return flow {
val fetcher = acquireFetcher(key)
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.result.EagerConflictResolutionResult
internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
private val delegate: RealStore<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>,
private val updater: Updater<Key, CommonRepresentation, *>,
internal class RealMutableStore<Key : Any, Network : Any, Output : Any, Local : Any>(
private val delegate: RealStore<Key, Network, Output, Local>,
private val updater: Updater<Key, Output, *>,
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 keyToWriteRequestQueue = mutableMapOf<Key, WriteRequestQueue<Key, CommonRepresentation, *>>()
private val keyToWriteRequestQueue = mutableMapOf<Key, WriteRequestQueue<Key, Output, *>>()
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 {
safeInitStore(request.key)
when (val eagerConflictResolutionResult = tryEagerlyResolveConflicts<NetworkWriteResponse>(request.key)) {
when (val eagerConflictResolutionResult = tryEagerlyResolveConflicts<Response>(request.key)) {
is EagerConflictResolutionResult.Error.Exception -> {
logger.e(eagerConflictResolutionResult.error.toString())
}
@ -63,7 +63,7 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
}
@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 {
requestStream
.onEach { writeRequest ->
@ -72,12 +72,12 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
}
.collect { writeRequest ->
val storeWriteResponse = try {
delegate.write(writeRequest.key, writeRequest.input)
delegate.write(writeRequest.key, writeRequest.value)
when (val updaterResult = tryUpdateServer(writeRequest)) {
is UpdaterResult.Error.Exception -> StoreWriteResponse.Error.Exception(updaterResult.error)
is UpdaterResult.Error.Message -> StoreWriteResponse.Error.Message(updaterResult.message)
is UpdaterResult.Success.Typed<*> -> {
val typedValue = updaterResult.value as? NetworkWriteResponse
val typedValue = updaterResult.value as? Response
if (typedValue == null) {
StoreWriteResponse.Success.Untyped(updaterResult.value)
} else {
@ -95,14 +95,14 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
}
@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()
private suspend fun <NetworkWriteResponse : Any> tryUpdateServer(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): UpdaterResult {
val updaterResult = postLatest<NetworkWriteResponse>(request.key)
private suspend fun <Response : Any> tryUpdateServer(request: StoreWriteRequest<Key, Output, Response>): UpdaterResult {
val updaterResult = postLatest<Response>(request.key)
if (updaterResult is UpdaterResult.Success) {
updateWriteRequestQueue<NetworkWriteResponse>(
updateWriteRequestQueue<Response>(
key = request.key,
created = request.created,
updaterResult = updaterResult
@ -115,14 +115,14 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
return updaterResult
}
private suspend fun <NetworkWriteResponse : Any> postLatest(key: Key): UpdaterResult {
private suspend fun <Response : Any> postLatest(key: Key): UpdaterResult {
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.Message -> UpdaterResult.Error.Message(updaterResult.message)
is UpdaterResult.Success.Untyped -> UpdaterResult.Success.Untyped(updaterResult.value)
is UpdaterResult.Success.Typed<*> -> {
val typedValue = updaterResult.value as? NetworkWriteResponse
val typedValue = updaterResult.value as? Response
if (typedValue == null) {
UpdaterResult.Success.Untyped(updaterResult.value)
} else {
@ -133,9 +133,9 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
}
@AnyThread
private suspend fun <NetworkWriteResponse : Any> updateWriteRequestQueue(key: Key, created: Long, updaterResult: UpdaterResult.Success) {
val nextWriteRequestQueue = withWriteRequestQueueLock<ArrayDeque<StoreWriteRequest<Key, CommonRepresentation, *>>, NetworkWriteResponse>(key) {
val outstandingWriteRequests = ArrayDeque<StoreWriteRequest<Key, CommonRepresentation, *>>()
private suspend fun <Response : Any> updateWriteRequestQueue(key: Key, created: Long, updaterResult: UpdaterResult.Success) {
val nextWriteRequestQueue = withWriteRequestQueueLock(key) {
val outstandingWriteRequests = ArrayDeque<StoreWriteRequest<Key, Output, *>>()
for (writeRequest in this) {
if (writeRequest.created <= created) {
@ -143,7 +143,7 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
val storeWriteResponse = when (updaterResult) {
is UpdaterResult.Success.Typed<*> -> {
val typedValue = updaterResult.value as? NetworkWriteResponse
val typedValue = updaterResult.value as? Response
if (typedValue == null) {
StoreWriteResponse.Success.Untyped(updaterResult.value)
} else {
@ -170,10 +170,10 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
}
@AnyThread
private suspend fun <Output : Any, NetworkWriteResponse : Any> withWriteRequestQueueLock(
private suspend fun <Result : Any> withWriteRequestQueueLock(
key: Key,
block: suspend WriteRequestQueue<Key, CommonRepresentation, *>.() -> Output
): Output =
block: suspend WriteRequestQueue<Key, Output, *>.() -> Result
): Result =
withThreadSafety(key) {
writeRequests.lightswitch.lock(writeRequests.mutex)
val writeRequestQueue = requireNotNull(keyToWriteRequestQueue[key])
@ -182,7 +182,7 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
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()
val output = requireNotNull(keyToWriteRequestQueue[key]?.last())
writeRequests.mutex.unlock()
@ -208,13 +208,13 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
keyToWriteRequestQueue[key].isNullOrEmpty()
}
private suspend fun <NetworkWriteResponse : Any> addWriteRequestToQueue(writeRequest: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>) =
withWriteRequestQueueLock<Unit, NetworkWriteResponse>(writeRequest.key) {
private suspend fun <Response : Any> addWriteRequestToQueue(writeRequest: StoreWriteRequest<Key, Output, Response>) =
withWriteRequestQueueLock(writeRequest.key) {
add(writeRequest)
}
@AnyThread
private suspend fun <NetworkWriteResponse : Any> tryEagerlyResolveConflicts(key: Key): EagerConflictResolutionResult<NetworkWriteResponse> =
private suspend fun <Response : Any> tryEagerlyResolveConflicts(key: Key): EagerConflictResolutionResult<Response> =
withThreadSafety(key) {
val latest = delegate.latestOrNull(key)
when {
@ -223,7 +223,7 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
try {
val updaterResult = updater.post(key, latest).also { updaterResult ->
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 org.mobilenativefoundation.store.store5.SourceOfTruth
internal class PersistentSourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any>(
private val realReader: (Key) -> Flow<SourceOfTruthRepresentation?>,
private val realWriter: suspend (Key, SourceOfTruthRepresentation) -> Unit,
internal class PersistentSourceOfTruth<Key : Any, Local : Any>(
private val realReader: (Key) -> Flow<Local?>,
private val realWriter: suspend (Key, Local) -> Unit,
private val realDelete: (suspend (Key) -> 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) {
realDelete?.invoke(key)
@ -39,20 +39,20 @@ internal class PersistentSourceOfTruth<Key : Any, SourceOfTruthRepresentation :
}
}
internal class PersistentNonFlowingSourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any>(
private val realReader: suspend (Key) -> SourceOfTruthRepresentation?,
private val realWriter: suspend (Key, SourceOfTruthRepresentation) -> Unit,
internal class PersistentNonFlowingSourceOfTruth<Key : Any, Local : Any>(
private val realReader: suspend (Key) -> Local?,
private val realWriter: suspend (Key, Local) -> Unit,
private val realDelete: (suspend (Key) -> Unit)? = null,
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 {
val sourceOfTruthRepresentation = realReader(key)
emit(sourceOfTruthRepresentation)
val sot = realReader(key)
emit(sot)
}
override suspend fun write(key: Key, value: SourceOfTruthRepresentation) {
override suspend fun write(key: Key, value: Local) {
return realWriter(key, value)
}

View file

@ -27,12 +27,12 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.transform
import org.mobilenativefoundation.store.cache5.CacheBuilder
import org.mobilenativefoundation.store.store5.CacheType
import org.mobilenativefoundation.store.store5.Converter
import org.mobilenativefoundation.store.store5.ExperimentalStoreApi
import org.mobilenativefoundation.store.store5.Fetcher
import org.mobilenativefoundation.store.store5.MemoryPolicy
import org.mobilenativefoundation.store.store5.SourceOfTruth
import org.mobilenativefoundation.store.store5.Store
import org.mobilenativefoundation.store.store5.StoreConverter
import org.mobilenativefoundation.store.store5.StoreReadRequest
import org.mobilenativefoundation.store.store5.StoreReadResponse
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.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,
fetcher: Fetcher<Key, NetworkRepresentation>,
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>? = null,
converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null,
private val memoryPolicy: MemoryPolicy<Key, CommonRepresentation>?
) : Store<Key, CommonRepresentation> {
fetcher: Fetcher<Key, Network>,
sourceOfTruth: SourceOfTruth<Key, Local>? = null,
converter: Converter<Network, Output, Local>? = null,
private val memoryPolicy: MemoryPolicy<Key, Output>?
) : Store<Key, Output> {
/**
* This source of truth is either a real database or an in memory source of truth created by
* 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
* 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 {
SourceOfTruthWithBarrier(it, converter)
}
private val memCache = memoryPolicy?.let {
CacheBuilder<Key, CommonRepresentation>().apply {
CacheBuilder<Key, Output>().apply {
if (memoryPolicy.hasAccessPolicy) {
expireAfterAccess(memoryPolicy.expireAfterAccess)
}
@ -88,7 +88,7 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
converter = converter
)
override fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>> =
override fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<Output>> =
flow {
val cachedToEmit = if (request.shouldSkipCache(CacheType.MEMORY)) {
null
@ -109,7 +109,7 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
request = request,
networkLock = null,
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 {
diskNetworkCombined(request, sourceOfTruth)
}
@ -185,8 +185,8 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
*/
private fun diskNetworkCombined(
request: StoreReadRequest<Key>,
sourceOfTruth: SourceOfTruthWithBarrier<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
): Flow<StoreReadResponse<CommonRepresentation>> {
sourceOfTruth: SourceOfTruthWithBarrier<Key, Network, Output, Local>
): Flow<StoreReadResponse<Output>> {
val diskLock = CompletableDeferred<Unit>()
val networkLock = CompletableDeferred<Unit>()
val networkFlow = createNetworkFlow(request, networkLock)
@ -229,7 +229,7 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
val diskValue = diskData.value
if (diskValue != null) {
@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
// to start emitting values.
@ -267,7 +267,7 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
request: StoreReadRequest<Key>,
networkLock: CompletableDeferred<Unit>?,
piggybackOnly: Boolean = false
): Flow<StoreReadResponse<NetworkRepresentation>> {
): Flow<StoreReadResponse<Network>> {
return fetcherController
.getFetcher(request.key, piggybackOnly)
.onStart {
@ -279,15 +279,15 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
}
}
internal suspend fun write(key: Key, input: CommonRepresentation): StoreDelegateWriteResult = try {
memCache?.put(key, input)
sourceOfTruth?.write(key, input)
internal suspend fun write(key: Key, value: Output): StoreDelegateWriteResult = try {
memCache?.put(key, value)
sourceOfTruth?.write(key, value)
StoreDelegateWriteResult.Success
} catch (error: Throwable) {
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 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.GlobalScope
import org.mobilenativefoundation.store.store5.Bookkeeper
import org.mobilenativefoundation.store.store5.Converter
import org.mobilenativefoundation.store.store5.Fetcher
import org.mobilenativefoundation.store.store5.MemoryPolicy
import org.mobilenativefoundation.store.store5.MutableStore
import org.mobilenativefoundation.store.store5.SourceOfTruth
import org.mobilenativefoundation.store.store5.Store
import org.mobilenativefoundation.store.store5.StoreBuilder
import org.mobilenativefoundation.store.store5.StoreConverter
import org.mobilenativefoundation.store.store5.StoreDefaults
import org.mobilenativefoundation.store.store5.Updater
import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any> storeBuilderFromFetcher(
fetcher: Fetcher<Key, NetworkRepresentation>,
fun <Key : Any, Network : Any, Output : Any> storeBuilderFromFetcher(
fetcher: Fetcher<Key, Network>,
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(
fetcher: Fetcher<Key, NetworkRepresentation>,
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>,
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> = RealStoreBuilder(fetcher, sourceOfTruth)
fun <Key : Any, Output : Any, Network : Any, Local : Any> storeBuilderFromFetcherAndSourceOfTruth(
fetcher: Fetcher<Key, Network>,
sourceOfTruth: SourceOfTruth<Key, Local>,
): StoreBuilder<Key, Network, Output, Local> = RealStoreBuilder(fetcher, sourceOfTruth)
internal class RealStoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
private val fetcher: Fetcher<Key, NetworkRepresentation>,
private val sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>? = null
) : StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any>(
private val fetcher: Fetcher<Key, Network>,
private val sourceOfTruth: SourceOfTruth<Key, Local>? = null
) : StoreBuilder<Key, Network, Output, Local> {
private var scope: CoroutineScope? = null
private var cachePolicy: MemoryPolicy<Key, CommonRepresentation>? = StoreDefaults.memoryPolicy
private var converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null
private var cachePolicy: MemoryPolicy<Key, Output>? = StoreDefaults.memoryPolicy
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
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
return this
}
override fun disableCache(): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
override fun disableCache(): StoreBuilder<Key, Network, Output, Local> {
cachePolicy = null
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
return this
}
override fun build(): Store<Key, CommonRepresentation> = RealStore(
override fun build(): Store<Key, Output> = RealStore(
scope = scope ?: GlobalScope,
sourceOfTruth = sourceOfTruth,
fetcher = fetcher,
@ -60,11 +60,11 @@ internal class RealStoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRe
converter = converter
)
override fun <NetworkWriteResponse : Any> build(
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>,
override fun <UpdaterResult : Any> build(
updater: Updater<Key, Output, UpdaterResult>,
bookkeeper: Bookkeeper<Key>
): MutableStore<Key, CommonRepresentation> =
build().asMutableStore<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation, NetworkWriteResponse>(
): MutableStore<Key, Output> =
build().asMutableStore<Key, Network, Output, Local, UpdaterResult>(
updater = updater,
bookkeeper = bookkeeper
)

View file

@ -2,9 +2,9 @@ package org.mobilenativefoundation.store.store5.impl
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 input: CommonRepresentation,
override val value: Output,
override val created: Long,
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.flowOf
import kotlinx.coroutines.flow.onStart
import org.mobilenativefoundation.store.store5.Converter
import org.mobilenativefoundation.store.store5.SourceOfTruth
import org.mobilenativefoundation.store.store5.StoreConverter
import org.mobilenativefoundation.store.store5.StoreReadResponse
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
@ -39,9 +39,9 @@ import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
* dispatching values to downstream while a write is in progress.
*/
@Suppress("UNCHECKED_CAST")
internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
private val delegate: SourceOfTruth<Key, SourceOfTruthRepresentation>,
private val converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null,
internal class SourceOfTruthWithBarrier<Key : Any, Network : Any, Output : Any, Local : Any>(
private val delegate: SourceOfTruth<Key, Local>,
private val converter: Converter<Network, Output, Local>? = null,
) {
/**
* 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)
fun reader(key: Key, lock: CompletableDeferred<Unit>): Flow<StoreReadResponse<CommonRepresentation?>> {
fun reader(key: Key, lock: CompletableDeferred<Unit>): Flow<StoreReadResponse<Output?>> {
return flow {
val barrier = barriers.acquire(key)
val readerVersion: Long = versionCounter.incrementAndGet()
@ -74,9 +74,9 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
} else {
null
}
val readFlow: Flow<StoreReadResponse<CommonRepresentation?>> = when (barrierMessage) {
val readFlow: Flow<StoreReadResponse<Output?>> = when (barrierMessage) {
is BarrierMsg.Open ->
delegate.reader(key).mapIndexed { index, sourceOfTruthRepresentation ->
delegate.reader(key).mapIndexed { index, sourceOfTruth ->
if (index == 0 && messageArrivedAfterMe) {
val firstMsgOrigin = if (writeError == null) {
// restarted barrier without an error means write succeeded
@ -89,8 +89,8 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
StoreReadResponseOrigin.SourceOfTruth
}
val value = sourceOfTruthRepresentation as? CommonRepresentation ?: if (sourceOfTruthRepresentation != null) {
converter?.fromSourceOfTruthRepresentationToCommonRepresentation(sourceOfTruthRepresentation)
val value = sourceOfTruth as? Output ?: if (sourceOfTruth != null) {
converter?.fromLocalToOutput(sourceOfTruth)
} else {
null
}
@ -101,11 +101,11 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
} else {
StoreReadResponse.Data(
origin = StoreReadResponseOrigin.SourceOfTruth,
value = sourceOfTruthRepresentation as? CommonRepresentation
?: if (sourceOfTruthRepresentation != null) converter?.fromSourceOfTruthRepresentationToCommonRepresentation(
sourceOfTruthRepresentation
value = sourceOfTruth as? Output
?: if (sourceOfTruth != null) converter?.fromLocalToOutput(
sourceOfTruth
) else null
) as StoreReadResponse<CommonRepresentation?>
) as StoreReadResponse<Output?>
}
}.catch { throwable ->
this.emit(
@ -146,12 +146,12 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
}
@Suppress("UNCHECKED_CAST")
suspend fun write(key: Key, value: CommonRepresentation) {
suspend fun write(key: Key, value: Output) {
val barrier = barriers.acquire(key)
try {
barrier.emit(BarrierMsg.Blocked(versionCounter.incrementAndGet()))
val writeError = try {
val input = value as? SourceOfTruthRepresentation ?: converter?.fromCommonRepresentationToSourceOfTruthRepresentation(value)
val input = value as? Local ?: converter?.fromOutputToLocal(value)
if (input != null) {
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
* 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))
.filterNot { it is StoreReadResponse.Loading || it is StoreReadResponse.NoNewData }
.first()
@ -29,18 +29,18 @@ suspend fun <Key : Any, CommonRepresentation : Any> Store<Key, CommonRepresentat
* data **even** if you explicitly requested fresh data.
* See https://github.com/dropbox/Store/pull/194 for context
*/
suspend fun <Key : Any, CommonRepresentation : Any> Store<Key, CommonRepresentation>.fresh(key: Key) =
suspend fun <Key : Any, Output : Any> Store<Key, Output>.fresh(key: Key) =
stream(StoreReadRequest.fresh(key))
.filterNot { it is StoreReadResponse.Loading || it is StoreReadResponse.NoNewData }
.first()
.requireData()
@Suppress("UNCHECKED_CAST")
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any, NetworkWriteResponse : Any> Store<Key, CommonRepresentation>.asMutableStore(
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>,
fun <Key : Any, Network : Any, Output : Any, Local : Any, Response : Any> Store<Key, Output>.asMutableStore(
updater: Updater<Key, Output, Response>,
bookkeeper: Bookkeeper<Key>
): MutableStore<Key, CommonRepresentation> {
val delegate = this as? RealStore<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
): MutableStore<Key, Output> {
val delegate = this as? RealStore<Key, Network, Output, Local>
?: throw Exception("MutableStore requires Store to be built using StoreBuilder")
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
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
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>()
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>() {

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

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

View file

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

View file

@ -1,6 +1,6 @@
package org.mobilenativefoundation.store.store5.util
internal interface TestApi<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> {
fun get(key: Key, fail: Boolean = false): NetworkRepresentation?
fun post(key: Key, value: CommonRepresentation, fail: Boolean = false): NetworkWriteResponse
internal interface TestApi<Key : Any, Network : Any, Output : Any, Response : Any> {
fun get(key: Key, fail: Boolean = false): Network?
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]
* 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(
StoreReadRequest.cached(key, refresh = false)
).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
class NoteBookkeeping {
class NotesBookkeeping {
private val log: MutableMap<String, Long?> = mutableMapOf()
fun setLastFailedSync(key: String, timestamp: Long, fail: Boolean = false): Boolean {
if (fail) {

View file

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