Clean Up (#499)
* 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:
parent
855a7bbc97
commit
ae07e0672c
45 changed files with 456 additions and 700 deletions
BIN
Images/friendly_robot.png
Normal file
BIN
Images/friendly_robot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7 KiB |
387
README.md
387
README.md
|
@ -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 don’t care how you’re 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.
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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>>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package org.mobilenativefoundation.store.store5.internal.definition
|
||||
|
||||
typealias Converter<Input, Output> = (input: Input) -> Output
|
|
@ -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>>
|
||||
|
|
|
@ -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>() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")))
|
||||
}
|
||||
}
|
|
@ -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")))
|
||||
}
|
||||
}
|
|
@ -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) {
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in a new issue