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.
|
### Concepts
|
||||||
- Users expect their UI experience to never be compromised (blocked) by new data loads. Whether an
|
|
||||||
application is social, news or business-to-business, users expect a seamless experience both
|
|
||||||
online and offline.
|
|
||||||
- International users expect minimal data downloads as many megabytes of downloaded data can quickly
|
|
||||||
result in astronomical phone bills.
|
|
||||||
|
|
||||||
Store is a Kotlin library for loading data from remote and local sources.
|
- [Store](https://mobilenativefoundation.github.io/Store/store/store/) is a typed repository that returns a flow
|
||||||
|
of [Data](https://github.com/MobileNativeFoundation/Store/blob/main/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt#L39)
|
||||||
|
/[Loading](https://github.com/MobileNativeFoundation/Store/blob/main/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt#L34)
|
||||||
|
/[Error](https://github.com/MobileNativeFoundation/Store/blob/main/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadResponse.kt#L51)
|
||||||
|
from local and network data sources
|
||||||
|
- [MutableStore](https://mobilenativefoundation.github.io/Store/mutable-store/building/overview/) is a mutable repository implementation that allows create **(C)**, read **(R)**,
|
||||||
|
update **(U)**, and delete **(D)** operations for local and network resources
|
||||||
|
- [SourceOfTruth](https://mobilenativefoundation.github.io/Store/mutable-store/building/implementations/source-of-truth/) persists items
|
||||||
|
- [Fetcher](https://mobilenativefoundation.github.io/Store/mutable-store/building/implementations/fetcher/) defines how data will be fetched over network
|
||||||
|
- [Updater](https://mobilenativefoundation.github.io/Store/mutable-store/building/implementations/updater/) defines how local changes will be pushed to network
|
||||||
|
- [Bookkeeper](https://mobilenativefoundation.github.io/Store/mutable-store/building/implementations/bookkeeper/) tracks metadata of local changes and records
|
||||||
|
synchronization failures
|
||||||
|
- [Validator](https://mobilenativefoundation.github.io/Store/mutable-store/building/implementations/validator/) returns whether an item is valid
|
||||||
|
- [Converter](https://mobilenativefoundation.github.io/Store/mutable-store/building/implementations/converter/) converts items
|
||||||
|
between [Network](https://mobilenativefoundation.github.io/Store/mutable-store/building/generics/network)
|
||||||
|
/[Local](https://mobilenativefoundation.github.io/Store/mutable-store/building/generics/sot)
|
||||||
|
/[Output](https://mobilenativefoundation.github.io/Store/mutable-store/building/generics/common) representations
|
||||||
|
|
||||||
### Overview
|
### Including Store In Your Project
|
||||||
|
|
||||||
A Store is responsible for managing a particular data request. When you create an implementation of
|
#### Android
|
||||||
a Store, you provide it with a `Fetcher`, a function that defines how data will be fetched over
|
|
||||||
network. You can also define how your Store will cache data in-memory and on-disk. Since Store
|
|
||||||
returns your data as a `Flow`, threading is a breeze! Once a Store is built, it handles the logic
|
|
||||||
around data flow, allowing your views to use the best data source and ensuring that the newest data
|
|
||||||
is always available for later offline use.
|
|
||||||
|
|
||||||
Store leverages multiple request throttling to prevent excessive calls to the network and disk
|
|
||||||
cache. By utilizing Store, you eliminate the possibility of flooding your network with the same
|
|
||||||
request while adding two layers of caching (memory and disk) as well as ability to add disk as a
|
|
||||||
source of truth where you can modify the disk directly without going through Store (works best with
|
|
||||||
databases that can provide observables sources
|
|
||||||
like [Jetpack Room](https://developer.android.com/jetpack/androidx/releases/room)
|
|
||||||
, [SQLDelight](https://github.com/cashapp/sqldelight)
|
|
||||||
or [Realm](https://realm.io/products/realm-database/))
|
|
||||||
|
|
||||||
### How to include in your project
|
|
||||||
|
|
||||||
Artifacts are hosted on **Maven Central**.
|
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
STORE_VERSION = "5.0.0-alpha03"
|
implementation "org.mobilenativefoundation.store:store5:5.0.0-alpha03"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Android
|
#### Multiplatform (Common, JVM, Native, JS)
|
||||||
|
|
||||||
```groovy
|
|
||||||
implementation "org.mobilenativefoundation.store:store5:$STORE_VERSION"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multiplatform (Common, JVM, Native, JS)
|
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.mobilenativefoundation.store:store5:$STORE_VERSION")
|
implementation("org.mobilenativefoundation.store:store5:5.0.0-alpha03")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fully Configured Store
|
### Getting Started
|
||||||
|
|
||||||
Let's start by looking at what a fully configured Store looks like. We will then walk through
|
#### Building Your First Store
|
||||||
simpler examples showing each piece:
|
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
StoreBuilder
|
StoreBuilder
|
||||||
.from(
|
.from<Key, Network, Output, Local>(fetcher, sourceOfTruth)
|
||||||
fetcher = Fetcher.of { api.fetchSubreddit(it, "10").data.children.map(::toPosts) },
|
.converter(converter)
|
||||||
sourceOfTruth = SourceOfTruth.of(
|
.validator(validator)
|
||||||
reader = db.postDao()::loadPosts,
|
.build(updater, bookkeeper)
|
||||||
writer = db.postDao()::insertPosts,
|
|
||||||
delete = db.postDao()::clearFeed,
|
|
||||||
deleteAll = db.postDao()::clearAllFeeds
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
With the above setup you have:
|
#### Creating
|
||||||
|
|
||||||
+ In-memory caching for rotation
|
##### Request
|
||||||
+ Disk caching for when users are offline
|
|
||||||
+ Throttling of API calls when parallel requests are made for the same resource
|
|
||||||
+ Rich API to ask for data whether you want cached, new or a stream of future data updates.
|
|
||||||
|
|
||||||
And now for the details:
|
|
||||||
|
|
||||||
### Creating a Store
|
|
||||||
|
|
||||||
You create a Store using a builder. The only requirement is to include a `Fetcher` which is just
|
|
||||||
a `typealias` to a function that returns a `Flow<FetcherResult<ReturnType>>`.
|
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val store = StoreBuilder
|
store.write(
|
||||||
.from(Fetcher.ofFlow { articleId -> api.getArticle(articleId) }) // api returns Flow<Article>
|
request = StoreWriteRequest.of<Key, Output, Response>(
|
||||||
.build()
|
key = key,
|
||||||
|
value = value
|
||||||
|
)
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Store uses generic keys as identifiers for data. A key can be any value object that properly
|
##### Response
|
||||||
implements `toString()`, `equals()` and `hashCode()`. When your `Fetcher` function is called, it
|
|
||||||
will be passed a particular `Key` value. Similarly, the key will be used as a primary identifier
|
|
||||||
within caches (Make sure to have a proper `hashCode()`!!).
|
|
||||||
|
|
||||||
Note: We highly recommend using built-in types that implement `equals` and `hashcode` or
|
```text
|
||||||
Kotlin `data` classes for complex keys.
|
1. StoreWriteResponse.Success.Typed<Response>(response)
|
||||||
|
```
|
||||||
|
|
||||||
### Public Interface - Stream
|
#### Reading
|
||||||
|
|
||||||
The primary function provided by a `Store` instance is the `stream` function which has the following
|
##### Request
|
||||||
signature:
|
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
fun stream(request: StoreRequest<Key>): Flow<StoreResponse<Output>>
|
store.stream<Response>(request = StoreReadRequest.cached(key, refresh = false))
|
||||||
```
|
```
|
||||||
|
|
||||||
Each `stream` call receives a `StoreRequest` object, which defines which key to fetch and which data
|
##### Response
|
||||||
sources to utilize.
|
|
||||||
The response is a `Flow` of `StoreResponse`. `StoreResponse` is a Kotlin sealed class that can be
|
|
||||||
either
|
|
||||||
a `Loading`, `Data` or `Error` instance.
|
|
||||||
Each `StoreResponse` includes an `origin` field which specifies where the event is coming from.
|
|
||||||
|
|
||||||
* The `Loading` class only has an `origin` field. This can provide you information like "network is
|
```text
|
||||||
fetching data", which can be a good signal to activate the loading spinner in your UI.
|
1. StoreReadResponse.Data(value, origin = StoreReadResponseOrigin.Cache)
|
||||||
* The `Data` class has a `value` field which includes an instance of the type returned by `Store`.
|
```
|
||||||
* The `Error` class includes an `error` field that contains the exception thrown by the
|
|
||||||
given `origin`.
|
|
||||||
|
|
||||||
When an error happens, `Store` does not throw an exception, instead, it wraps it in
|
#### Updating
|
||||||
a `StoreResponse.Error` type which allows `Flow` to continue so that it can still receive updates
|
|
||||||
that might be triggered by either changes in your data source or subsequent fetch operations.
|
##### Request
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
viewModelScope.launch {
|
store.write(
|
||||||
store.stream(StoreRequest.cached(key = key, refresh = true)).collect { response ->
|
request = StoreWriteRequest.of<Key, Output, Response>(
|
||||||
when (response) {
|
key = key,
|
||||||
is StoreResponse.Loading -> showLoadingSpinner()
|
value = newValue
|
||||||
is StoreResponse.Data -> {
|
)
|
||||||
if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
|
)
|
||||||
updateUI(response.value)
|
|
||||||
}
|
|
||||||
is StoreResponse.Error -> {
|
|
||||||
if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
|
|
||||||
showError(response.error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For convenience, there are `Store.get(key)` and `Store.fresh(key)` extension functions.
|
##### Response
|
||||||
|
|
||||||
* `suspend fun Store.get(key: Key): Value`: This method returns a single value for the given key. If
|
```text
|
||||||
available, it will be returned from the in memory cache or the sourceOfTruth. An error will be
|
1. StoreWriteResponse.Success.Typed<Response>(response)
|
||||||
thrown if no value is available in either the `cache` or `sourceOfTruth`, and the `fetcher` fails
|
```
|
||||||
to load the data from the network.
|
|
||||||
* `suspend fun Store.fresh(key: Key): Value`: This method returns a single value for the given key
|
#### Deleting
|
||||||
that is obtained by querying the fetcher. An error will be thrown if the `fetcher` fails to load
|
|
||||||
the data from the network, regardless of whether any value is available in the `cache`
|
##### Request
|
||||||
or `sourceOfTruth`.
|
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
lifecycleScope.launchWhenStarted {
|
store.clear(key)
|
||||||
val article = store.get(key)
|
|
||||||
updateUI(article)
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The first time you call to `suspend store.get(key)`, the response will be stored in an in-memory
|
### License
|
||||||
cache and in the sourceOfTruth, if provided.
|
|
||||||
All subsequent calls to `store.get(key)` with the same `Key` will retrieve the cached version of the
|
|
||||||
data, minimizing unnecessary data calls. This prevents your app from fetching fresh data over the
|
|
||||||
network (or from another external data source) in situations when doing so would unnecessarily waste
|
|
||||||
bandwidth and battery. A great use case is any time your views are recreated after a rotation, they
|
|
||||||
will be able to request the cached data from your Store. Having this data available can help you
|
|
||||||
avoid the need to retain this in the view layer.
|
|
||||||
|
|
||||||
By default, 100 items will be cached in memory for 24 hours. You
|
|
||||||
may [pass in your own memory policy to override the default policy](#Configuring-In-memory-Cache).
|
|
||||||
|
|
||||||
### Skipping Memory/Disk
|
|
||||||
|
|
||||||
Alternatively, you can call `store.fresh(key)` to get a `suspended result` that skips the memory (
|
|
||||||
and optional disk cache).
|
|
||||||
|
|
||||||
A good use case is overnight background updates use `fresh()` to make sure that calls
|
|
||||||
to `store.get()` will not have to hit the network during normal usage. Another good use case
|
|
||||||
for `fresh()` is when a user wants to pull to refresh.
|
|
||||||
|
|
||||||
Calls to both `fresh()` and `get()` emit one value or throw an error.
|
|
||||||
|
|
||||||
### Stream
|
|
||||||
|
|
||||||
For real-time updates, you may also call `store.stream()` which returns a `Flow<T>` that emits each
|
|
||||||
time a new item is returned from your store. You can think of stream as a way to create reactive
|
|
||||||
streams that update when you db or memory cache updates
|
|
||||||
|
|
||||||
example calls:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
lifecycleScope.launchWhenStarted {
|
|
||||||
store.stream(
|
|
||||||
StoreRequest.cached(
|
|
||||||
3,
|
|
||||||
refresh = false
|
|
||||||
)
|
|
||||||
) //will get cached value followed by any fresh values, refresh will also trigger network call if set to `true` even if the data is available in cache or disk.
|
|
||||||
.collect {}
|
|
||||||
store.stream(StoreRequest.fresh(3)) //skip cache, go directly to fetcher
|
|
||||||
.collect {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Inflight Debouncer
|
|
||||||
|
|
||||||
To prevent duplicate requests for the same data, Store offers an inflight debouncer. If the same
|
|
||||||
request is made as a previous identical request that has not completed, the same response will be
|
|
||||||
returned. This is useful for situations when your app needs to make many async calls for the same
|
|
||||||
data at startup or when users are obsessively pulling to refresh. As an example, The New York Times
|
|
||||||
news app asynchronously calls `ConfigStore.get()` from 12 different places on startup. The first
|
|
||||||
call blocks while all others wait for the data to arrive. We have seen a dramatic decrease in the
|
|
||||||
app's data usage after implementing this inflight logic.
|
|
||||||
|
|
||||||
### Disk as Cache
|
|
||||||
|
|
||||||
Stores can enable disk caching by passing a `SourceOfTruth` into the builder. Whenever a new network
|
|
||||||
request is made, the Store will first write to the disk cache and then read from the disk cache.
|
|
||||||
|
|
||||||
### Disk as Single Source of Truth
|
|
||||||
|
|
||||||
Providing `sourceOfTruth` whose `reader` function can return a `Flow<Value?>` allows you to make
|
|
||||||
Store treat your disk as source of truth.
|
|
||||||
Any changes made on disk, even if it is not made by Store, will update the active `Store` streams.
|
|
||||||
|
|
||||||
This feature, combined with persistence libraries that provide observable
|
|
||||||
queries ([Jetpack Room](https://developer.android.com/jetpack/androidx/releases/room)
|
|
||||||
, [SQLDelight](https://github.com/cashapp/sqldelight)
|
|
||||||
or [Realm](https://realm.io/products/realm-database/))
|
|
||||||
allows you to create offline first applications that can be used without an active network
|
|
||||||
connection while still providing a great user experience.
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
StoreBuilder
|
|
||||||
.from(
|
|
||||||
fetcher = Fetcher.of { api.fetchSubreddit(it, "10").data.children.map(::toPosts) },
|
|
||||||
sourceOfTruth = SourceOfTruth.of(
|
|
||||||
reader = db.postDao()::loadPosts,
|
|
||||||
writer = db.postDao()::insertPosts,
|
|
||||||
delete = db.postDao()::clearFeed,
|
|
||||||
deleteAll = db.postDao()::clearAllFeeds
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
```
|
|
||||||
|
|
||||||
Stores 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
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Copyright (c) 2022 Mobile Native Foundation.
|
Copyright (c) 2022 Mobile Native Foundation.
|
||||||
|
|
|
@ -2,7 +2,7 @@ package org.mobilenativefoundation.store.cache5
|
||||||
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
class CacheBuilder<Key : Any, CommonRepresentation : Any> {
|
class CacheBuilder<Key : Any, Output : Any> {
|
||||||
internal var concurrencyLevel = 4
|
internal var concurrencyLevel = 4
|
||||||
private set
|
private set
|
||||||
internal val initialCapacity = 16
|
internal val initialCapacity = 16
|
||||||
|
@ -14,41 +14,41 @@ class CacheBuilder<Key : Any, CommonRepresentation : Any> {
|
||||||
private set
|
private set
|
||||||
internal var expireAfterWrite: Duration = Duration.INFINITE
|
internal var expireAfterWrite: Duration = Duration.INFINITE
|
||||||
private set
|
private set
|
||||||
internal var weigher: Weigher<Key, CommonRepresentation>? = null
|
internal var weigher: Weigher<Key, Output>? = null
|
||||||
private set
|
private set
|
||||||
internal var ticker: Ticker? = null
|
internal var ticker: Ticker? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun concurrencyLevel(producer: () -> Int): CacheBuilder<Key, CommonRepresentation> = apply {
|
fun concurrencyLevel(producer: () -> Int): CacheBuilder<Key, Output> = apply {
|
||||||
concurrencyLevel = producer.invoke()
|
concurrencyLevel = producer.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun maximumSize(maximumSize: Long): CacheBuilder<Key, CommonRepresentation> = apply {
|
fun maximumSize(maximumSize: Long): CacheBuilder<Key, Output> = apply {
|
||||||
if (maximumSize < 0) {
|
if (maximumSize < 0) {
|
||||||
throw IllegalArgumentException("Maximum size must be non-negative.")
|
throw IllegalArgumentException("Maximum size must be non-negative.")
|
||||||
}
|
}
|
||||||
this.maximumSize = maximumSize
|
this.maximumSize = maximumSize
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expireAfterAccess(duration: Duration): CacheBuilder<Key, CommonRepresentation> = apply {
|
fun expireAfterAccess(duration: Duration): CacheBuilder<Key, Output> = apply {
|
||||||
if (duration.isNegative()) {
|
if (duration.isNegative()) {
|
||||||
throw IllegalArgumentException("Duration must be non-negative.")
|
throw IllegalArgumentException("Duration must be non-negative.")
|
||||||
}
|
}
|
||||||
expireAfterAccess = duration
|
expireAfterAccess = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expireAfterWrite(duration: Duration): CacheBuilder<Key, CommonRepresentation> = apply {
|
fun expireAfterWrite(duration: Duration): CacheBuilder<Key, Output> = apply {
|
||||||
if (duration.isNegative()) {
|
if (duration.isNegative()) {
|
||||||
throw IllegalArgumentException("Duration must be non-negative.")
|
throw IllegalArgumentException("Duration must be non-negative.")
|
||||||
}
|
}
|
||||||
expireAfterWrite = duration
|
expireAfterWrite = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ticker(ticker: Ticker): CacheBuilder<Key, CommonRepresentation> = apply {
|
fun ticker(ticker: Ticker): CacheBuilder<Key, Output> = apply {
|
||||||
this.ticker = ticker
|
this.ticker = ticker
|
||||||
}
|
}
|
||||||
|
|
||||||
fun weigher(maximumWeight: Long, weigher: Weigher<Key, CommonRepresentation>): CacheBuilder<Key, CommonRepresentation> = apply {
|
fun weigher(maximumWeight: Long, weigher: Weigher<Key, Output>): CacheBuilder<Key, Output> = apply {
|
||||||
if (maximumWeight < 0) {
|
if (maximumWeight < 0) {
|
||||||
throw IllegalArgumentException("Maximum weight must be non-negative.")
|
throw IllegalArgumentException("Maximum weight must be non-negative.")
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ class CacheBuilder<Key : Any, CommonRepresentation : Any> {
|
||||||
this.weigher = weigher
|
this.weigher = weigher
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build(): Cache<Key, CommonRepresentation> {
|
fun build(): Cache<Key, Output> {
|
||||||
if (maximumSize != -1L && weigher != null) {
|
if (maximumSize != -1L && weigher != null) {
|
||||||
throw IllegalStateException("Maximum size cannot be combined with weigher.")
|
throw IllegalStateException("Maximum size cannot be combined with weigher.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* See [ofFlow], [of] for easily translating to [FetcherResult] (and
|
||||||
* automatically transforming exceptions into [FetcherResult.Error].
|
* automatically transforming exceptions into [FetcherResult.Error].
|
||||||
*/
|
*/
|
||||||
interface Fetcher<Key : Any, NetworkRepresentation : Any> {
|
interface Fetcher<Key : Any, Network : Any> {
|
||||||
/**
|
/**
|
||||||
* Returns a flow of the item represented by the given [key].
|
* Returns a flow of the item represented by the given [key].
|
||||||
*/
|
*/
|
||||||
operator fun invoke(key: Key): Flow<FetcherResult<NetworkRepresentation>>
|
operator fun invoke(key: Key): Flow<FetcherResult<Network>>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
|
@ -37,9 +37,9 @@ interface Fetcher<Key : Any, NetworkRepresentation : Any> {
|
||||||
*
|
*
|
||||||
* @param flowFactory a factory for a [Flow]ing source of network records.
|
* @param flowFactory a factory for a [Flow]ing source of network records.
|
||||||
*/
|
*/
|
||||||
fun <Key : Any, NetworkRepresentation : Any> ofResultFlow(
|
fun <Key : Any, Network : Any> ofResultFlow(
|
||||||
flowFactory: (Key) -> Flow<FetcherResult<NetworkRepresentation>>
|
flowFactory: (Key) -> Flow<FetcherResult<Network>>
|
||||||
): Fetcher<Key, NetworkRepresentation> = FactoryFetcher(factory = flowFactory)
|
): Fetcher<Key, Network> = FactoryFetcher(factory = flowFactory)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Creates" a [Fetcher] from a non-[Flow] source.
|
* "Creates" a [Fetcher] from a non-[Flow] source.
|
||||||
|
@ -52,9 +52,9 @@ interface Fetcher<Key : Any, NetworkRepresentation : Any> {
|
||||||
*
|
*
|
||||||
* @param fetch a source of network records.
|
* @param fetch a source of network records.
|
||||||
*/
|
*/
|
||||||
fun <Key : Any, NetworkRepresentation : Any> ofResult(
|
fun <Key : Any, Network : Any> ofResult(
|
||||||
fetch: suspend (Key) -> FetcherResult<NetworkRepresentation>
|
fetch: suspend (Key) -> FetcherResult<Network>
|
||||||
): Fetcher<Key, NetworkRepresentation> = ofResultFlow(fetch.asFlow())
|
): Fetcher<Key, Network> = ofResultFlow(fetch.asFlow())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Creates" a [Fetcher] from a [flowFactory] and translate the results to a [FetcherResult].
|
* "Creates" a [Fetcher] from a [flowFactory] and translate the results to a [FetcherResult].
|
||||||
|
@ -68,11 +68,11 @@ interface Fetcher<Key : Any, NetworkRepresentation : Any> {
|
||||||
*
|
*
|
||||||
* @param flowFactory a factory for a [Flow]ing source of network records.
|
* @param flowFactory a factory for a [Flow]ing source of network records.
|
||||||
*/
|
*/
|
||||||
fun <Key : Any, NetworkRepresentation : Any> ofFlow(
|
fun <Key : Any, Network : Any> ofFlow(
|
||||||
flowFactory: (Key) -> Flow<NetworkRepresentation>
|
flowFactory: (Key) -> Flow<Network>
|
||||||
): Fetcher<Key, NetworkRepresentation> = FactoryFetcher { key: Key ->
|
): Fetcher<Key, Network> = FactoryFetcher { key: Key ->
|
||||||
flowFactory(key)
|
flowFactory(key)
|
||||||
.map<NetworkRepresentation, FetcherResult<NetworkRepresentation>> { FetcherResult.Data(it) }
|
.map<Network, FetcherResult<Network>> { FetcherResult.Data(it) }
|
||||||
.catch { throwable: Throwable -> emit(FetcherResult.Error.Exception(throwable)) }
|
.catch { throwable: Throwable -> emit(FetcherResult.Error.Exception(throwable)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,19 +87,19 @@ interface Fetcher<Key : Any, NetworkRepresentation : Any> {
|
||||||
*
|
*
|
||||||
* @param fetch a source of network records.
|
* @param fetch a source of network records.
|
||||||
*/
|
*/
|
||||||
fun <Key : Any, NetworkRepresentation : Any> of(fetch: suspend (key: Key) -> NetworkRepresentation): Fetcher<Key, NetworkRepresentation> =
|
fun <Key : Any, Network : Any> of(fetch: suspend (key: Key) -> Network): Fetcher<Key, Network> =
|
||||||
ofFlow(fetch.asFlow())
|
ofFlow(fetch.asFlow())
|
||||||
|
|
||||||
private fun <Key : Any, NetworkRepresentation : Any> (suspend (key: Key) -> NetworkRepresentation).asFlow() = { key: Key ->
|
private fun <Key : Any, Network : Any> (suspend (key: Key) -> Network).asFlow() = { key: Key ->
|
||||||
flow {
|
flow {
|
||||||
emit(invoke(key))
|
emit(invoke(key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FactoryFetcher<Key : Any, NetworkRepresentation : Any>(
|
private class FactoryFetcher<Key : Any, Network : Any>(
|
||||||
private val factory: (Key) -> Flow<FetcherResult<NetworkRepresentation>>
|
private val factory: (Key) -> Flow<FetcherResult<Network>>
|
||||||
) : Fetcher<Key, NetworkRepresentation> {
|
) : Fetcher<Key, Network> {
|
||||||
override fun invoke(key: Key): Flow<FetcherResult<NetworkRepresentation>> = factory(key)
|
override fun invoke(key: Key): Flow<FetcherResult<Network>> = factory(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package org.mobilenativefoundation.store.store5
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
sealed class FetcherResult<out NetworkRepresentation : Any> {
|
sealed class FetcherResult<out Network : Any> {
|
||||||
data class Data<NetworkRepresentation : Any>(val value: NetworkRepresentation) : FetcherResult<NetworkRepresentation>()
|
data class Data<Network : Any>(val value: Network) : FetcherResult<Network>()
|
||||||
sealed class Error : FetcherResult<Nothing>() {
|
sealed class Error : FetcherResult<Nothing>() {
|
||||||
data class Exception(val error: Throwable) : Error()
|
data class Exception(val error: Throwable) : Error()
|
||||||
data class Message(val message: String) : Error()
|
data class Message(val message: String) : Error()
|
||||||
|
|
|
@ -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
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
interface MutableStore<Key : Any, CommonRepresentation : Any> :
|
interface MutableStore<Key : Any, Output : Any> :
|
||||||
Read.StreamWithConflictResolution<Key, CommonRepresentation>,
|
Read.StreamWithConflictResolution<Key, Output>,
|
||||||
Write<Key, CommonRepresentation>,
|
Write<Key, Output>,
|
||||||
Write.Stream<Key, CommonRepresentation>,
|
Write.Stream<Key, Output>,
|
||||||
Clear.Key<Key>,
|
Clear.Key<Key>,
|
||||||
Clear
|
Clear
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package org.mobilenativefoundation.store.store5
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
data class OnFetcherCompletion<NetworkRepresentation : Any>(
|
data class OnFetcherCompletion<Network : Any>(
|
||||||
val onSuccess: (FetcherResult.Data<NetworkRepresentation>) -> Unit,
|
val onSuccess: (FetcherResult.Data<Network>) -> Unit,
|
||||||
val onFailure: (FetcherResult.Error) -> Unit
|
val onFailure: (FetcherResult.Error) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package org.mobilenativefoundation.store.store5
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
data class OnUpdaterCompletion<NetworkWriteResponse : Any>(
|
data class OnUpdaterCompletion<Response : Any>(
|
||||||
val onSuccess: (UpdaterResult.Success) -> Unit,
|
val onSuccess: (UpdaterResult.Success) -> Unit,
|
||||||
val onFailure: (UpdaterResult.Error) -> Unit
|
val onFailure: (UpdaterResult.Error) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,15 +3,15 @@ package org.mobilenativefoundation.store.store5
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface Read {
|
interface Read {
|
||||||
interface Stream<Key : Any, CommonRepresentation : Any> {
|
interface Stream<Key : Any, Output : Any> {
|
||||||
/**
|
/**
|
||||||
* Return a flow for the given key
|
* Return a flow for the given key
|
||||||
* @param request - see [StoreReadRequest] for configurations
|
* @param request - see [StoreReadRequest] for configurations
|
||||||
*/
|
*/
|
||||||
fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>>
|
fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<Output>>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StreamWithConflictResolution<Key : Any, CommonRepresentation : Any> {
|
interface StreamWithConflictResolution<Key : Any, Output : Any> {
|
||||||
fun <NetworkWriteResponse : Any> stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>>
|
fun <Response : Any> stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<Output>>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,14 +46,14 @@ import kotlin.jvm.JvmName
|
||||||
* transform them to another type when placing them in local storage.
|
* transform them to another type when placing them in local storage.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
interface SourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any> {
|
interface SourceOfTruth<Key : Any, Local : Any> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by [Store] to read records from the source of truth.
|
* Used by [Store] to read records from the source of truth.
|
||||||
*
|
*
|
||||||
* @param key The key to read for.
|
* @param key The key to read for.
|
||||||
*/
|
*/
|
||||||
fun reader(key: Key): Flow<SourceOfTruthRepresentation?>
|
fun reader(key: Key): Flow<Local?>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by [Store] to write records **coming in from the fetcher (network)** to the source of
|
* Used by [Store] to write records **coming in from the fetcher (network)** to the source of
|
||||||
|
@ -66,7 +66,7 @@ interface SourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any> {
|
||||||
*
|
*
|
||||||
* @param key The key to update for.
|
* @param key The key to update for.
|
||||||
*/
|
*/
|
||||||
suspend fun write(key: Key, value: SourceOfTruthRepresentation)
|
suspend fun write(key: Key, value: Local)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used by [Store] to delete records in the source of truth for the given key.
|
* Used by [Store] to delete records in the source of truth for the given key.
|
||||||
|
@ -90,12 +90,12 @@ interface SourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any> {
|
||||||
* @param delete function for deleting records in the source of truth for the given key
|
* @param delete function for deleting records in the source of truth for the given key
|
||||||
* @param deleteAll function for deleting all records in the source of truth
|
* @param deleteAll function for deleting all records in the source of truth
|
||||||
*/
|
*/
|
||||||
fun <Key : Any, SourceOfTruthRepresentation : Any> of(
|
fun <Key : Any, Local : Any> of(
|
||||||
nonFlowReader: suspend (Key) -> SourceOfTruthRepresentation?,
|
nonFlowReader: suspend (Key) -> Local?,
|
||||||
writer: suspend (Key, SourceOfTruthRepresentation) -> Unit,
|
writer: suspend (Key, Local) -> Unit,
|
||||||
delete: (suspend (Key) -> Unit)? = null,
|
delete: (suspend (Key) -> Unit)? = null,
|
||||||
deleteAll: (suspend () -> Unit)? = null
|
deleteAll: (suspend () -> Unit)? = null
|
||||||
): SourceOfTruth<Key, SourceOfTruthRepresentation> = PersistentNonFlowingSourceOfTruth(
|
): SourceOfTruth<Key, Local> = PersistentNonFlowingSourceOfTruth(
|
||||||
realReader = nonFlowReader,
|
realReader = nonFlowReader,
|
||||||
realWriter = writer,
|
realWriter = writer,
|
||||||
realDelete = delete,
|
realDelete = delete,
|
||||||
|
@ -112,12 +112,12 @@ interface SourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any> {
|
||||||
* @param deleteAll function for deleting all records in the source of truth
|
* @param deleteAll function for deleting all records in the source of truth
|
||||||
*/
|
*/
|
||||||
@JvmName("ofFlow")
|
@JvmName("ofFlow")
|
||||||
fun <Key : Any, SourceOfTruthRepresentation : Any> of(
|
fun <Key : Any, Local : Any> of(
|
||||||
reader: (Key) -> Flow<SourceOfTruthRepresentation?>,
|
reader: (Key) -> Flow<Local?>,
|
||||||
writer: suspend (Key, SourceOfTruthRepresentation) -> Unit,
|
writer: suspend (Key, Local) -> Unit,
|
||||||
delete: (suspend (Key) -> Unit)? = null,
|
delete: (suspend (Key) -> Unit)? = null,
|
||||||
deleteAll: (suspend () -> Unit)? = null
|
deleteAll: (suspend () -> Unit)? = null
|
||||||
): SourceOfTruth<Key, SourceOfTruthRepresentation> = PersistentSourceOfTruth(
|
): SourceOfTruth<Key, Local> = PersistentSourceOfTruth(
|
||||||
realReader = reader,
|
realReader = reader,
|
||||||
realWriter = writer,
|
realWriter = writer,
|
||||||
realDelete = delete,
|
realDelete = delete,
|
||||||
|
|
|
@ -31,7 +31,7 @@ package org.mobilenativefoundation.store.store5
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
interface Store<Key : Any, CommonRepresentation : Any> :
|
interface Store<Key : Any, Output : Any> :
|
||||||
Read.Stream<Key, CommonRepresentation>,
|
Read.Stream<Key, Output>,
|
||||||
Clear.Key<Key>,
|
Clear.Key<Key>,
|
||||||
Clear.All
|
Clear.All
|
||||||
|
|
|
@ -22,37 +22,37 @@ import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcherAndSo
|
||||||
/**
|
/**
|
||||||
* Main entry point for creating a [Store].
|
* Main entry point for creating a [Store].
|
||||||
*/
|
*/
|
||||||
interface StoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> {
|
interface StoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any> {
|
||||||
fun build(): Store<Key, CommonRepresentation>
|
fun build(): Store<Key, Output>
|
||||||
|
|
||||||
fun <NetworkWriteResponse : Any> build(
|
fun <Response : Any> build(
|
||||||
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>,
|
updater: Updater<Key, Output, Response>,
|
||||||
bookkeeper: Bookkeeper<Key>
|
bookkeeper: Bookkeeper<Key>
|
||||||
): MutableStore<Key, CommonRepresentation>
|
): MutableStore<Key, Output>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A store multicasts same [CommonRepresentation] value to many consumers (Similar to RxJava.share()), by default
|
* A store multicasts same [Output] value to many consumers (Similar to RxJava.share()), by default
|
||||||
* [Store] will open a global scope for management of shared responses, if instead you'd like to control
|
* [Store] will open a global scope for management of shared responses, if instead you'd like to control
|
||||||
* the scope that sharing/multicasting happens in you can pass a @param [scope]
|
* the scope that sharing/multicasting happens in you can pass a @param [scope]
|
||||||
*
|
*
|
||||||
* @param scope - scope to use for sharing
|
* @param scope - scope to use for sharing
|
||||||
*/
|
*/
|
||||||
fun scope(scope: CoroutineScope): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
|
fun scope(scope: CoroutineScope): StoreBuilder<Key, Network, Output, Local>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* controls eviction policy for a store cache, use [MemoryPolicy.MemoryPolicyBuilder] to configure a TTL
|
* controls eviction policy for a store cache, use [MemoryPolicy.MemoryPolicyBuilder] to configure a TTL
|
||||||
* or size based eviction
|
* or size based eviction
|
||||||
* Example: MemoryPolicy.builder().setExpireAfterWrite(10.seconds).build()
|
* Example: MemoryPolicy.builder().setExpireAfterWrite(10.seconds).build()
|
||||||
*/
|
*/
|
||||||
fun cachePolicy(memoryPolicy: MemoryPolicy<Key, CommonRepresentation>?): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
|
fun cachePolicy(memoryPolicy: MemoryPolicy<Key, Output>?): StoreBuilder<Key, Network, Output, Local>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* by default a Store caches in memory with a default policy of max items = 100
|
* by default a Store caches in memory with a default policy of max items = 100
|
||||||
*/
|
*/
|
||||||
fun disableCache(): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
|
fun disableCache(): StoreBuilder<Key, Network, Output, Local>
|
||||||
|
|
||||||
fun converter(converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>):
|
fun converter(converter: Converter<Network, Output, Local>):
|
||||||
StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
|
StoreBuilder<Key, Network, Output, Local>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@ -61,9 +61,9 @@ interface StoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentat
|
||||||
*
|
*
|
||||||
* @param fetcher a [Fetcher] flow of network records.
|
* @param fetcher a [Fetcher] flow of network records.
|
||||||
*/
|
*/
|
||||||
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any> from(
|
fun <Key : Any, Network : Any, Output : Any> from(
|
||||||
fetcher: Fetcher<Key, NetworkRepresentation>,
|
fetcher: Fetcher<Key, Network>,
|
||||||
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, *> = storeBuilderFromFetcher(fetcher = fetcher)
|
): StoreBuilder<Key, Network, Output, *> = storeBuilderFromFetcher(fetcher = fetcher)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new [StoreBuilder] from a [Fetcher] and a [SourceOfTruth].
|
* Creates a new [StoreBuilder] from a [Fetcher] and a [SourceOfTruth].
|
||||||
|
@ -71,10 +71,10 @@ interface StoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentat
|
||||||
* @param fetcher a function for fetching a flow of network records.
|
* @param fetcher a function for fetching a flow of network records.
|
||||||
* @param sourceOfTruth a [SourceOfTruth] for the store.
|
* @param sourceOfTruth a [SourceOfTruth] for the store.
|
||||||
*/
|
*/
|
||||||
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> from(
|
fun <Key : Any, Network : Any, Output : Any, Local : Any> from(
|
||||||
fetcher: Fetcher<Key, NetworkRepresentation>,
|
fetcher: Fetcher<Key, Network>,
|
||||||
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>
|
sourceOfTruth: SourceOfTruth<Key, Local>
|
||||||
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> =
|
): StoreBuilder<Key, Network, Output, Local> =
|
||||||
storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth)
|
storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* class to represent each response. This allows the flow to keep running even if an error happens
|
||||||
* so that if there is an observable single source of truth, application can keep observing it.
|
* so that if there is an observable single source of truth, application can keep observing it.
|
||||||
*/
|
*/
|
||||||
sealed class StoreReadResponse<out CommonRepresentation> {
|
sealed class StoreReadResponse<out Output> {
|
||||||
/**
|
/**
|
||||||
* Represents the source of the Response.
|
* Represents the source of the Response.
|
||||||
*/
|
*/
|
||||||
|
@ -36,8 +36,8 @@ sealed class StoreReadResponse<out CommonRepresentation> {
|
||||||
/**
|
/**
|
||||||
* Data dispatched by [Store]
|
* Data dispatched by [Store]
|
||||||
*/
|
*/
|
||||||
data class Data<CommonRepresentation>(val value: CommonRepresentation, override val origin: StoreReadResponseOrigin) :
|
data class Data<Output>(val value: Output, override val origin: StoreReadResponseOrigin) :
|
||||||
StoreReadResponse<CommonRepresentation>()
|
StoreReadResponse<Output>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No new data event dispatched by Store to signal the [Fetcher] returned no data (i.e the
|
* No new data event dispatched by Store to signal the [Fetcher] returned no data (i.e the
|
||||||
|
@ -63,7 +63,7 @@ sealed class StoreReadResponse<out CommonRepresentation> {
|
||||||
/**
|
/**
|
||||||
* Returns the available data or throws [NullPointerException] if there is no data.
|
* Returns the available data or throws [NullPointerException] if there is no data.
|
||||||
*/
|
*/
|
||||||
fun requireData(): CommonRepresentation {
|
fun requireData(): Output {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is Data -> value
|
is Data -> value
|
||||||
is Error -> this.doThrow()
|
is Error -> this.doThrow()
|
||||||
|
@ -96,7 +96,7 @@ sealed class StoreReadResponse<out CommonRepresentation> {
|
||||||
/**
|
/**
|
||||||
* If there is data available, returns it; otherwise returns null.
|
* If there is data available, returns it; otherwise returns null.
|
||||||
*/
|
*/
|
||||||
fun dataOrNull(): CommonRepresentation? = when (this) {
|
fun dataOrNull(): Output? = when (this) {
|
||||||
is Data -> value
|
is Data -> value
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,18 +4,18 @@ import kotlinx.datetime.Clock
|
||||||
import org.mobilenativefoundation.store.store5.impl.OnStoreWriteCompletion
|
import org.mobilenativefoundation.store.store5.impl.OnStoreWriteCompletion
|
||||||
import org.mobilenativefoundation.store.store5.impl.RealStoreWriteRequest
|
import org.mobilenativefoundation.store.store5.impl.RealStoreWriteRequest
|
||||||
|
|
||||||
interface StoreWriteRequest<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> {
|
interface StoreWriteRequest<Key : Any, Output : Any, Response : Any> {
|
||||||
val key: Key
|
val key: Key
|
||||||
val input: CommonRepresentation
|
val value: Output
|
||||||
val created: Long
|
val created: Long
|
||||||
val onCompletions: List<OnStoreWriteCompletion>?
|
val onCompletions: List<OnStoreWriteCompletion>?
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun <Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> of(
|
fun <Key : Any, Output : Any, Response : Any> of(
|
||||||
key: Key,
|
key: Key,
|
||||||
input: CommonRepresentation,
|
value: Output,
|
||||||
onCompletions: List<OnStoreWriteCompletion>? = null,
|
onCompletions: List<OnStoreWriteCompletion>? = null,
|
||||||
created: Long = Clock.System.now().toEpochMilliseconds(),
|
created: Long = Clock.System.now().toEpochMilliseconds(),
|
||||||
): StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse> = RealStoreWriteRequest(key, input, created, onCompletions)
|
): StoreWriteRequest<Key, Output, Response> = RealStoreWriteRequest(key, value, created, onCompletions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
sealed class StoreWriteResponse {
|
sealed class StoreWriteResponse {
|
||||||
sealed class Success : StoreWriteResponse() {
|
sealed class Success : StoreWriteResponse() {
|
||||||
data class Typed<NetworkWriteResponse : Any>(val value: NetworkWriteResponse) : Success()
|
data class Typed<Response : Any>(val value: Response) : Success()
|
||||||
data class Untyped(val value: Any) : Success()
|
data class Untyped(val value: Any) : Success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
package org.mobilenativefoundation.store.store5
|
package org.mobilenativefoundation.store.store5
|
||||||
|
|
||||||
typealias PostRequest<Key, CommonRepresentation> = suspend (key: Key, input: CommonRepresentation) -> UpdaterResult
|
typealias PostRequest<Key, Output> = suspend (key: Key, value: Output) -> UpdaterResult
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Posts data to remote data source.
|
* Posts data to remote data source.
|
||||||
* @see [StoreWriteRequest]
|
* @see [StoreWriteRequest]
|
||||||
*/
|
*/
|
||||||
interface Updater<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> {
|
interface Updater<Key : Any, Output : Any, Response : Any> {
|
||||||
/**
|
/**
|
||||||
* Makes HTTP POST request.
|
* Makes HTTP POST request.
|
||||||
*/
|
*/
|
||||||
suspend fun post(key: Key, input: CommonRepresentation): UpdaterResult
|
suspend fun post(key: Key, value: Output): UpdaterResult
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes on network completion.
|
* Executes on network completion.
|
||||||
*/
|
*/
|
||||||
val onCompletion: OnUpdaterCompletion<NetworkWriteResponse>?
|
val onCompletion: OnUpdaterCompletion<Response>?
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun <Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> by(
|
fun <Key : Any, Output : Any, Response : Any> by(
|
||||||
post: PostRequest<Key, CommonRepresentation>,
|
post: PostRequest<Key, Output>,
|
||||||
onCompletion: OnUpdaterCompletion<NetworkWriteResponse>? = null,
|
onCompletion: OnUpdaterCompletion<Response>? = null,
|
||||||
): Updater<Key, CommonRepresentation, NetworkWriteResponse> = RealNetworkUpdater(
|
): Updater<Key, Output, Response> = RealNetworkUpdater(
|
||||||
post, onCompletion
|
post, onCompletion
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class RealNetworkUpdater<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any>(
|
internal class RealNetworkUpdater<Key : Any, Output : Any, Response : Any>(
|
||||||
private val realPost: PostRequest<Key, CommonRepresentation>,
|
private val realPost: PostRequest<Key, Output>,
|
||||||
override val onCompletion: OnUpdaterCompletion<NetworkWriteResponse>?,
|
override val onCompletion: OnUpdaterCompletion<Response>?,
|
||||||
) : Updater<Key, CommonRepresentation, NetworkWriteResponse> {
|
) : Updater<Key, Output, Response> {
|
||||||
override suspend fun post(key: Key, input: CommonRepresentation): UpdaterResult = realPost(key, input)
|
override suspend fun post(key: Key, value: Output): UpdaterResult = realPost(key, value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package org.mobilenativefoundation.store.store5
|
||||||
sealed class UpdaterResult {
|
sealed class UpdaterResult {
|
||||||
|
|
||||||
sealed class Success : UpdaterResult() {
|
sealed class Success : UpdaterResult() {
|
||||||
data class Typed<NetworkWriteResponse : Any>(val value: NetworkWriteResponse) : Success()
|
data class Typed<Response : Any>(val value: Response) : Success()
|
||||||
data class Untyped(val value: Any) : Success()
|
data class Untyped(val value: Any) : Success()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface Write<Key : Any, CommonRepresentation : Any> {
|
interface Write<Key : Any, Output : Any> {
|
||||||
@ExperimentalStoreApi
|
@ExperimentalStoreApi
|
||||||
suspend fun <NetworkWriteResponse : Any> write(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): StoreWriteResponse
|
suspend fun <Response : Any> write(request: StoreWriteRequest<Key, Output, Response>): StoreWriteResponse
|
||||||
interface Stream<Key : Any, CommonRepresentation : Any> {
|
interface Stream<Key : Any, Output : Any> {
|
||||||
@ExperimentalStoreApi
|
@ExperimentalStoreApi
|
||||||
fun <NetworkWriteResponse : Any> stream(requestStream: Flow<StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>>): Flow<StoreWriteResponse>
|
fun <Response : Any> stream(requestStream: Flow<StoreWriteRequest<Key, Output, Response>>): Flow<StoreWriteResponse>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,10 +25,10 @@ import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEmpty
|
import kotlinx.coroutines.flow.onEmpty
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.mobilenativefoundation.store.multicast5.Multicaster
|
import org.mobilenativefoundation.store.multicast5.Multicaster
|
||||||
|
import org.mobilenativefoundation.store.store5.Converter
|
||||||
import org.mobilenativefoundation.store.store5.Fetcher
|
import org.mobilenativefoundation.store.store5.Fetcher
|
||||||
import org.mobilenativefoundation.store.store5.FetcherResult
|
import org.mobilenativefoundation.store.store5.FetcherResult
|
||||||
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||||
import org.mobilenativefoundation.store.store5.StoreConverter
|
|
||||||
import org.mobilenativefoundation.store.store5.StoreReadResponse
|
import org.mobilenativefoundation.store.store5.StoreReadResponse
|
||||||
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
|
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
|
||||||
* fetcher requests receives values dispatched by later requests even if they don't share the
|
* fetcher requests receives values dispatched by later requests even if they don't share the
|
||||||
* request.
|
* request.
|
||||||
*/
|
*/
|
||||||
internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
|
internal class FetcherController<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||||
/**
|
/**
|
||||||
* The [CoroutineScope] to use when collecting from the fetcher
|
* The [CoroutineScope] to use when collecting from the fetcher
|
||||||
*/
|
*/
|
||||||
|
@ -48,14 +48,14 @@ internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonR
|
||||||
/**
|
/**
|
||||||
* The function that provides the actualy fetcher flow when needed
|
* The function that provides the actualy fetcher flow when needed
|
||||||
*/
|
*/
|
||||||
private val realFetcher: Fetcher<Key, NetworkRepresentation>,
|
private val realFetcher: Fetcher<Key, Network>,
|
||||||
/**
|
/**
|
||||||
* [SourceOfTruth] to send the data each time fetcher dispatches a value. Can be `null` if
|
* [SourceOfTruth] to send the data each time fetcher dispatches a value. Can be `null` if
|
||||||
* no [SourceOfTruth] is available.
|
* no [SourceOfTruth] is available.
|
||||||
*/
|
*/
|
||||||
private val sourceOfTruth: SourceOfTruthWithBarrier<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>?,
|
private val sourceOfTruth: SourceOfTruthWithBarrier<Key, Network, Output, Local>?,
|
||||||
|
|
||||||
private val converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null
|
private val converter: Converter<Network, Output, Local>? = null
|
||||||
) {
|
) {
|
||||||
@Suppress("USELESS_CAST", "UNCHECKED_CAST") // needed for multicaster source
|
@Suppress("USELESS_CAST", "UNCHECKED_CAST") // needed for multicaster source
|
||||||
private val fetchers = RefCountedResource(
|
private val fetchers = RefCountedResource(
|
||||||
|
@ -69,7 +69,7 @@ internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonR
|
||||||
StoreReadResponse.Data(
|
StoreReadResponse.Data(
|
||||||
it.value,
|
it.value,
|
||||||
origin = StoreReadResponseOrigin.Fetcher
|
origin = StoreReadResponseOrigin.Fetcher
|
||||||
) as StoreReadResponse<NetworkRepresentation>
|
) as StoreReadResponse<Network>
|
||||||
}
|
}
|
||||||
|
|
||||||
is FetcherResult.Error.Message -> StoreReadResponse.Error.Message(
|
is FetcherResult.Error.Message -> StoreReadResponse.Error.Message(
|
||||||
|
@ -92,9 +92,9 @@ internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonR
|
||||||
*/
|
*/
|
||||||
piggybackingDownstream = true,
|
piggybackingDownstream = true,
|
||||||
onEach = { response ->
|
onEach = { response ->
|
||||||
response.dataOrNull()?.let { networkRepresentation ->
|
response.dataOrNull()?.let { network ->
|
||||||
val input =
|
val input =
|
||||||
networkRepresentation as? CommonRepresentation ?: converter?.fromNetworkRepresentationToCommonRepresentation(networkRepresentation)
|
network as? Output ?: converter?.fromNetworkToOutput(network)
|
||||||
if (input != null) {
|
if (input != null) {
|
||||||
sourceOfTruth?.write(key, input)
|
sourceOfTruth?.write(key, input)
|
||||||
}
|
}
|
||||||
|
@ -102,12 +102,12 @@ internal class FetcherController<Key : Any, NetworkRepresentation : Any, CommonR
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onRelease = { _: Key, multicaster: Multicaster<StoreReadResponse<NetworkRepresentation>> ->
|
onRelease = { _: Key, multicaster: Multicaster<StoreReadResponse<Network>> ->
|
||||||
multicaster.close()
|
multicaster.close()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getFetcher(key: Key, piggybackOnly: Boolean = false): Flow<StoreReadResponse<NetworkRepresentation>> {
|
fun getFetcher(key: Key, piggybackOnly: Boolean = false): Flow<StoreReadResponse<Network>> {
|
||||||
return flow {
|
return flow {
|
||||||
val fetcher = acquireFetcher(key)
|
val fetcher = acquireFetcher(key)
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -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.definition.WriteRequestQueue
|
||||||
import org.mobilenativefoundation.store.store5.internal.result.EagerConflictResolutionResult
|
import org.mobilenativefoundation.store.store5.internal.result.EagerConflictResolutionResult
|
||||||
|
|
||||||
internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
|
internal class RealMutableStore<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||||
private val delegate: RealStore<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>,
|
private val delegate: RealStore<Key, Network, Output, Local>,
|
||||||
private val updater: Updater<Key, CommonRepresentation, *>,
|
private val updater: Updater<Key, Output, *>,
|
||||||
private val bookkeeper: Bookkeeper<Key>,
|
private val bookkeeper: Bookkeeper<Key>,
|
||||||
) : MutableStore<Key, CommonRepresentation>, Clear.Key<Key> by delegate, Clear.All by delegate {
|
) : MutableStore<Key, Output>, Clear.Key<Key> by delegate, Clear.All by delegate {
|
||||||
|
|
||||||
private val storeLock = Mutex()
|
private val storeLock = Mutex()
|
||||||
private val keyToWriteRequestQueue = mutableMapOf<Key, WriteRequestQueue<Key, CommonRepresentation, *>>()
|
private val keyToWriteRequestQueue = mutableMapOf<Key, WriteRequestQueue<Key, Output, *>>()
|
||||||
private val keyToThreadSafety = mutableMapOf<Key, ThreadSafety>()
|
private val keyToThreadSafety = mutableMapOf<Key, ThreadSafety>()
|
||||||
|
|
||||||
override fun <NetworkWriteResponse : Any> stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>> =
|
override fun <Response : Any> stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<Output>> =
|
||||||
flow {
|
flow {
|
||||||
safeInitStore(request.key)
|
safeInitStore(request.key)
|
||||||
|
|
||||||
when (val eagerConflictResolutionResult = tryEagerlyResolveConflicts<NetworkWriteResponse>(request.key)) {
|
when (val eagerConflictResolutionResult = tryEagerlyResolveConflicts<Response>(request.key)) {
|
||||||
is EagerConflictResolutionResult.Error.Exception -> {
|
is EagerConflictResolutionResult.Error.Exception -> {
|
||||||
logger.e(eagerConflictResolutionResult.error.toString())
|
logger.e(eagerConflictResolutionResult.error.toString())
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalStoreApi
|
@ExperimentalStoreApi
|
||||||
override fun <NetworkWriteResponse : Any> stream(requestStream: Flow<StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>>): Flow<StoreWriteResponse> =
|
override fun <Response : Any> stream(requestStream: Flow<StoreWriteRequest<Key, Output, Response>>): Flow<StoreWriteResponse> =
|
||||||
flow {
|
flow {
|
||||||
requestStream
|
requestStream
|
||||||
.onEach { writeRequest ->
|
.onEach { writeRequest ->
|
||||||
|
@ -72,12 +72,12 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
|
||||||
}
|
}
|
||||||
.collect { writeRequest ->
|
.collect { writeRequest ->
|
||||||
val storeWriteResponse = try {
|
val storeWriteResponse = try {
|
||||||
delegate.write(writeRequest.key, writeRequest.input)
|
delegate.write(writeRequest.key, writeRequest.value)
|
||||||
when (val updaterResult = tryUpdateServer(writeRequest)) {
|
when (val updaterResult = tryUpdateServer(writeRequest)) {
|
||||||
is UpdaterResult.Error.Exception -> StoreWriteResponse.Error.Exception(updaterResult.error)
|
is UpdaterResult.Error.Exception -> StoreWriteResponse.Error.Exception(updaterResult.error)
|
||||||
is UpdaterResult.Error.Message -> StoreWriteResponse.Error.Message(updaterResult.message)
|
is UpdaterResult.Error.Message -> StoreWriteResponse.Error.Message(updaterResult.message)
|
||||||
is UpdaterResult.Success.Typed<*> -> {
|
is UpdaterResult.Success.Typed<*> -> {
|
||||||
val typedValue = updaterResult.value as? NetworkWriteResponse
|
val typedValue = updaterResult.value as? Response
|
||||||
if (typedValue == null) {
|
if (typedValue == null) {
|
||||||
StoreWriteResponse.Success.Untyped(updaterResult.value)
|
StoreWriteResponse.Success.Untyped(updaterResult.value)
|
||||||
} else {
|
} else {
|
||||||
|
@ -95,14 +95,14 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalStoreApi
|
@ExperimentalStoreApi
|
||||||
override suspend fun <NetworkWriteResponse : Any> write(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): StoreWriteResponse =
|
override suspend fun <Response : Any> write(request: StoreWriteRequest<Key, Output, Response>): StoreWriteResponse =
|
||||||
stream(flowOf(request)).first()
|
stream(flowOf(request)).first()
|
||||||
|
|
||||||
private suspend fun <NetworkWriteResponse : Any> tryUpdateServer(request: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>): UpdaterResult {
|
private suspend fun <Response : Any> tryUpdateServer(request: StoreWriteRequest<Key, Output, Response>): UpdaterResult {
|
||||||
val updaterResult = postLatest<NetworkWriteResponse>(request.key)
|
val updaterResult = postLatest<Response>(request.key)
|
||||||
|
|
||||||
if (updaterResult is UpdaterResult.Success) {
|
if (updaterResult is UpdaterResult.Success) {
|
||||||
updateWriteRequestQueue<NetworkWriteResponse>(
|
updateWriteRequestQueue<Response>(
|
||||||
key = request.key,
|
key = request.key,
|
||||||
created = request.created,
|
created = request.created,
|
||||||
updaterResult = updaterResult
|
updaterResult = updaterResult
|
||||||
|
@ -115,14 +115,14 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
|
||||||
return updaterResult
|
return updaterResult
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <NetworkWriteResponse : Any> postLatest(key: Key): UpdaterResult {
|
private suspend fun <Response : Any> postLatest(key: Key): UpdaterResult {
|
||||||
val writer = getLatestWriteRequest(key)
|
val writer = getLatestWriteRequest(key)
|
||||||
return when (val updaterResult = updater.post(key, writer.input)) {
|
return when (val updaterResult = updater.post(key, writer.value)) {
|
||||||
is UpdaterResult.Error.Exception -> UpdaterResult.Error.Exception(updaterResult.error)
|
is UpdaterResult.Error.Exception -> UpdaterResult.Error.Exception(updaterResult.error)
|
||||||
is UpdaterResult.Error.Message -> UpdaterResult.Error.Message(updaterResult.message)
|
is UpdaterResult.Error.Message -> UpdaterResult.Error.Message(updaterResult.message)
|
||||||
is UpdaterResult.Success.Untyped -> UpdaterResult.Success.Untyped(updaterResult.value)
|
is UpdaterResult.Success.Untyped -> UpdaterResult.Success.Untyped(updaterResult.value)
|
||||||
is UpdaterResult.Success.Typed<*> -> {
|
is UpdaterResult.Success.Typed<*> -> {
|
||||||
val typedValue = updaterResult.value as? NetworkWriteResponse
|
val typedValue = updaterResult.value as? Response
|
||||||
if (typedValue == null) {
|
if (typedValue == null) {
|
||||||
UpdaterResult.Success.Untyped(updaterResult.value)
|
UpdaterResult.Success.Untyped(updaterResult.value)
|
||||||
} else {
|
} else {
|
||||||
|
@ -133,9 +133,9 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
|
||||||
}
|
}
|
||||||
|
|
||||||
@AnyThread
|
@AnyThread
|
||||||
private suspend fun <NetworkWriteResponse : Any> updateWriteRequestQueue(key: Key, created: Long, updaterResult: UpdaterResult.Success) {
|
private suspend fun <Response : Any> updateWriteRequestQueue(key: Key, created: Long, updaterResult: UpdaterResult.Success) {
|
||||||
val nextWriteRequestQueue = withWriteRequestQueueLock<ArrayDeque<StoreWriteRequest<Key, CommonRepresentation, *>>, NetworkWriteResponse>(key) {
|
val nextWriteRequestQueue = withWriteRequestQueueLock(key) {
|
||||||
val outstandingWriteRequests = ArrayDeque<StoreWriteRequest<Key, CommonRepresentation, *>>()
|
val outstandingWriteRequests = ArrayDeque<StoreWriteRequest<Key, Output, *>>()
|
||||||
|
|
||||||
for (writeRequest in this) {
|
for (writeRequest in this) {
|
||||||
if (writeRequest.created <= created) {
|
if (writeRequest.created <= created) {
|
||||||
|
@ -143,7 +143,7 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
|
||||||
|
|
||||||
val storeWriteResponse = when (updaterResult) {
|
val storeWriteResponse = when (updaterResult) {
|
||||||
is UpdaterResult.Success.Typed<*> -> {
|
is UpdaterResult.Success.Typed<*> -> {
|
||||||
val typedValue = updaterResult.value as? NetworkWriteResponse
|
val typedValue = updaterResult.value as? Response
|
||||||
if (typedValue == null) {
|
if (typedValue == null) {
|
||||||
StoreWriteResponse.Success.Untyped(updaterResult.value)
|
StoreWriteResponse.Success.Untyped(updaterResult.value)
|
||||||
} else {
|
} else {
|
||||||
|
@ -170,10 +170,10 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
|
||||||
}
|
}
|
||||||
|
|
||||||
@AnyThread
|
@AnyThread
|
||||||
private suspend fun <Output : Any, NetworkWriteResponse : Any> withWriteRequestQueueLock(
|
private suspend fun <Result : Any> withWriteRequestQueueLock(
|
||||||
key: Key,
|
key: Key,
|
||||||
block: suspend WriteRequestQueue<Key, CommonRepresentation, *>.() -> Output
|
block: suspend WriteRequestQueue<Key, Output, *>.() -> Result
|
||||||
): Output =
|
): Result =
|
||||||
withThreadSafety(key) {
|
withThreadSafety(key) {
|
||||||
writeRequests.lightswitch.lock(writeRequests.mutex)
|
writeRequests.lightswitch.lock(writeRequests.mutex)
|
||||||
val writeRequestQueue = requireNotNull(keyToWriteRequestQueue[key])
|
val writeRequestQueue = requireNotNull(keyToWriteRequestQueue[key])
|
||||||
|
@ -182,7 +182,7 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getLatestWriteRequest(key: Key): StoreWriteRequest<Key, CommonRepresentation, *> = withThreadSafety(key) {
|
private suspend fun getLatestWriteRequest(key: Key): StoreWriteRequest<Key, Output, *> = withThreadSafety(key) {
|
||||||
writeRequests.mutex.lock()
|
writeRequests.mutex.lock()
|
||||||
val output = requireNotNull(keyToWriteRequestQueue[key]?.last())
|
val output = requireNotNull(keyToWriteRequestQueue[key]?.last())
|
||||||
writeRequests.mutex.unlock()
|
writeRequests.mutex.unlock()
|
||||||
|
@ -208,13 +208,13 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
|
||||||
keyToWriteRequestQueue[key].isNullOrEmpty()
|
keyToWriteRequestQueue[key].isNullOrEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <NetworkWriteResponse : Any> addWriteRequestToQueue(writeRequest: StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>) =
|
private suspend fun <Response : Any> addWriteRequestToQueue(writeRequest: StoreWriteRequest<Key, Output, Response>) =
|
||||||
withWriteRequestQueueLock<Unit, NetworkWriteResponse>(writeRequest.key) {
|
withWriteRequestQueueLock(writeRequest.key) {
|
||||||
add(writeRequest)
|
add(writeRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@AnyThread
|
@AnyThread
|
||||||
private suspend fun <NetworkWriteResponse : Any> tryEagerlyResolveConflicts(key: Key): EagerConflictResolutionResult<NetworkWriteResponse> =
|
private suspend fun <Response : Any> tryEagerlyResolveConflicts(key: Key): EagerConflictResolutionResult<Response> =
|
||||||
withThreadSafety(key) {
|
withThreadSafety(key) {
|
||||||
val latest = delegate.latestOrNull(key)
|
val latest = delegate.latestOrNull(key)
|
||||||
when {
|
when {
|
||||||
|
@ -223,7 +223,7 @@ internal class RealMutableStore<Key : Any, NetworkRepresentation : Any, CommonRe
|
||||||
try {
|
try {
|
||||||
val updaterResult = updater.post(key, latest).also { updaterResult ->
|
val updaterResult = updater.post(key, latest).also { updaterResult ->
|
||||||
if (updaterResult is UpdaterResult.Success) {
|
if (updaterResult is UpdaterResult.Success) {
|
||||||
updateWriteRequestQueue<NetworkWriteResponse>(key = key, created = now(), updaterResult = updaterResult)
|
updateWriteRequestQueue<Response>(key = key, created = now(), updaterResult = updaterResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,16 +19,16 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||||
|
|
||||||
internal class PersistentSourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any>(
|
internal class PersistentSourceOfTruth<Key : Any, Local : Any>(
|
||||||
private val realReader: (Key) -> Flow<SourceOfTruthRepresentation?>,
|
private val realReader: (Key) -> Flow<Local?>,
|
||||||
private val realWriter: suspend (Key, SourceOfTruthRepresentation) -> Unit,
|
private val realWriter: suspend (Key, Local) -> Unit,
|
||||||
private val realDelete: (suspend (Key) -> Unit)? = null,
|
private val realDelete: (suspend (Key) -> Unit)? = null,
|
||||||
private val realDeleteAll: (suspend () -> Unit)? = null
|
private val realDeleteAll: (suspend () -> Unit)? = null
|
||||||
) : SourceOfTruth<Key, SourceOfTruthRepresentation> {
|
) : SourceOfTruth<Key, Local> {
|
||||||
|
|
||||||
override fun reader(key: Key): Flow<SourceOfTruthRepresentation?> = realReader.invoke(key)
|
override fun reader(key: Key): Flow<Local?> = realReader.invoke(key)
|
||||||
|
|
||||||
override suspend fun write(key: Key, value: SourceOfTruthRepresentation) = realWriter(key, value)
|
override suspend fun write(key: Key, value: Local) = realWriter(key, value)
|
||||||
|
|
||||||
override suspend fun delete(key: Key) {
|
override suspend fun delete(key: Key) {
|
||||||
realDelete?.invoke(key)
|
realDelete?.invoke(key)
|
||||||
|
@ -39,20 +39,20 @@ internal class PersistentSourceOfTruth<Key : Any, SourceOfTruthRepresentation :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class PersistentNonFlowingSourceOfTruth<Key : Any, SourceOfTruthRepresentation : Any>(
|
internal class PersistentNonFlowingSourceOfTruth<Key : Any, Local : Any>(
|
||||||
private val realReader: suspend (Key) -> SourceOfTruthRepresentation?,
|
private val realReader: suspend (Key) -> Local?,
|
||||||
private val realWriter: suspend (Key, SourceOfTruthRepresentation) -> Unit,
|
private val realWriter: suspend (Key, Local) -> Unit,
|
||||||
private val realDelete: (suspend (Key) -> Unit)? = null,
|
private val realDelete: (suspend (Key) -> Unit)? = null,
|
||||||
private val realDeleteAll: (suspend () -> Unit)?
|
private val realDeleteAll: (suspend () -> Unit)?
|
||||||
) : SourceOfTruth<Key, SourceOfTruthRepresentation> {
|
) : SourceOfTruth<Key, Local> {
|
||||||
|
|
||||||
override fun reader(key: Key): Flow<SourceOfTruthRepresentation?> =
|
override fun reader(key: Key): Flow<Local?> =
|
||||||
flow {
|
flow {
|
||||||
val sourceOfTruthRepresentation = realReader(key)
|
val sot = realReader(key)
|
||||||
emit(sourceOfTruthRepresentation)
|
emit(sot)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun write(key: Key, value: SourceOfTruthRepresentation) {
|
override suspend fun write(key: Key, value: Local) {
|
||||||
return realWriter(key, value)
|
return realWriter(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,12 +27,12 @@ import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.flow.transform
|
import kotlinx.coroutines.flow.transform
|
||||||
import org.mobilenativefoundation.store.cache5.CacheBuilder
|
import org.mobilenativefoundation.store.cache5.CacheBuilder
|
||||||
import org.mobilenativefoundation.store.store5.CacheType
|
import org.mobilenativefoundation.store.store5.CacheType
|
||||||
|
import org.mobilenativefoundation.store.store5.Converter
|
||||||
import org.mobilenativefoundation.store.store5.ExperimentalStoreApi
|
import org.mobilenativefoundation.store.store5.ExperimentalStoreApi
|
||||||
import org.mobilenativefoundation.store.store5.Fetcher
|
import org.mobilenativefoundation.store.store5.Fetcher
|
||||||
import org.mobilenativefoundation.store.store5.MemoryPolicy
|
import org.mobilenativefoundation.store.store5.MemoryPolicy
|
||||||
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||||
import org.mobilenativefoundation.store.store5.Store
|
import org.mobilenativefoundation.store.store5.Store
|
||||||
import org.mobilenativefoundation.store.store5.StoreConverter
|
|
||||||
import org.mobilenativefoundation.store.store5.StoreReadRequest
|
import org.mobilenativefoundation.store.store5.StoreReadRequest
|
||||||
import org.mobilenativefoundation.store.store5.StoreReadResponse
|
import org.mobilenativefoundation.store.store5.StoreReadResponse
|
||||||
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
|
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
|
||||||
|
@ -40,13 +40,13 @@ import org.mobilenativefoundation.store.store5.impl.operators.Either
|
||||||
import org.mobilenativefoundation.store.store5.impl.operators.merge
|
import org.mobilenativefoundation.store.store5.impl.operators.merge
|
||||||
import org.mobilenativefoundation.store.store5.internal.result.StoreDelegateWriteResult
|
import org.mobilenativefoundation.store.store5.internal.result.StoreDelegateWriteResult
|
||||||
|
|
||||||
internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
|
internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
fetcher: Fetcher<Key, NetworkRepresentation>,
|
fetcher: Fetcher<Key, Network>,
|
||||||
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>? = null,
|
sourceOfTruth: SourceOfTruth<Key, Local>? = null,
|
||||||
converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null,
|
converter: Converter<Network, Output, Local>? = null,
|
||||||
private val memoryPolicy: MemoryPolicy<Key, CommonRepresentation>?
|
private val memoryPolicy: MemoryPolicy<Key, Output>?
|
||||||
) : Store<Key, CommonRepresentation> {
|
) : Store<Key, Output> {
|
||||||
/**
|
/**
|
||||||
* This source of truth is either a real database or an in memory source of truth created by
|
* This source of truth is either a real database or an in memory source of truth created by
|
||||||
* the builder.
|
* the builder.
|
||||||
|
@ -54,13 +54,13 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
|
||||||
* we write the value from fetcher into the disk, we can block reads to avoid sending new data
|
* we write the value from fetcher into the disk, we can block reads to avoid sending new data
|
||||||
* as if it came from the server (the [StoreReadResponse.origin] field).
|
* as if it came from the server (the [StoreReadResponse.origin] field).
|
||||||
*/
|
*/
|
||||||
private val sourceOfTruth: SourceOfTruthWithBarrier<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? =
|
private val sourceOfTruth: SourceOfTruthWithBarrier<Key, Network, Output, Local>? =
|
||||||
sourceOfTruth?.let {
|
sourceOfTruth?.let {
|
||||||
SourceOfTruthWithBarrier(it, converter)
|
SourceOfTruthWithBarrier(it, converter)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val memCache = memoryPolicy?.let {
|
private val memCache = memoryPolicy?.let {
|
||||||
CacheBuilder<Key, CommonRepresentation>().apply {
|
CacheBuilder<Key, Output>().apply {
|
||||||
if (memoryPolicy.hasAccessPolicy) {
|
if (memoryPolicy.hasAccessPolicy) {
|
||||||
expireAfterAccess(memoryPolicy.expireAfterAccess)
|
expireAfterAccess(memoryPolicy.expireAfterAccess)
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
|
||||||
converter = converter
|
converter = converter
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<CommonRepresentation>> =
|
override fun stream(request: StoreReadRequest<Key>): Flow<StoreReadResponse<Output>> =
|
||||||
flow {
|
flow {
|
||||||
val cachedToEmit = if (request.shouldSkipCache(CacheType.MEMORY)) {
|
val cachedToEmit = if (request.shouldSkipCache(CacheType.MEMORY)) {
|
||||||
null
|
null
|
||||||
|
@ -109,7 +109,7 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
|
||||||
request = request,
|
request = request,
|
||||||
networkLock = null,
|
networkLock = null,
|
||||||
piggybackOnly = piggybackOnly
|
piggybackOnly = piggybackOnly
|
||||||
) as Flow<StoreReadResponse<CommonRepresentation>> // when no source of truth Input == Output
|
) as Flow<StoreReadResponse<Output>> // when no source of truth Input == Output
|
||||||
} else {
|
} else {
|
||||||
diskNetworkCombined(request, sourceOfTruth)
|
diskNetworkCombined(request, sourceOfTruth)
|
||||||
}
|
}
|
||||||
|
@ -185,8 +185,8 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
|
||||||
*/
|
*/
|
||||||
private fun diskNetworkCombined(
|
private fun diskNetworkCombined(
|
||||||
request: StoreReadRequest<Key>,
|
request: StoreReadRequest<Key>,
|
||||||
sourceOfTruth: SourceOfTruthWithBarrier<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
|
sourceOfTruth: SourceOfTruthWithBarrier<Key, Network, Output, Local>
|
||||||
): Flow<StoreReadResponse<CommonRepresentation>> {
|
): Flow<StoreReadResponse<Output>> {
|
||||||
val diskLock = CompletableDeferred<Unit>()
|
val diskLock = CompletableDeferred<Unit>()
|
||||||
val networkLock = CompletableDeferred<Unit>()
|
val networkLock = CompletableDeferred<Unit>()
|
||||||
val networkFlow = createNetworkFlow(request, networkLock)
|
val networkFlow = createNetworkFlow(request, networkLock)
|
||||||
|
@ -229,7 +229,7 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
|
||||||
val diskValue = diskData.value
|
val diskValue = diskData.value
|
||||||
if (diskValue != null) {
|
if (diskValue != null) {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
emit(diskData as StoreReadResponse<CommonRepresentation>)
|
emit(diskData as StoreReadResponse<Output>)
|
||||||
}
|
}
|
||||||
// If the disk value is null or refresh was requested then allow fetcher
|
// If the disk value is null or refresh was requested then allow fetcher
|
||||||
// to start emitting values.
|
// to start emitting values.
|
||||||
|
@ -267,7 +267,7 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
|
||||||
request: StoreReadRequest<Key>,
|
request: StoreReadRequest<Key>,
|
||||||
networkLock: CompletableDeferred<Unit>?,
|
networkLock: CompletableDeferred<Unit>?,
|
||||||
piggybackOnly: Boolean = false
|
piggybackOnly: Boolean = false
|
||||||
): Flow<StoreReadResponse<NetworkRepresentation>> {
|
): Flow<StoreReadResponse<Network>> {
|
||||||
return fetcherController
|
return fetcherController
|
||||||
.getFetcher(request.key, piggybackOnly)
|
.getFetcher(request.key, piggybackOnly)
|
||||||
.onStart {
|
.onStart {
|
||||||
|
@ -279,15 +279,15 @@ internal class RealStore<Key : Any, NetworkRepresentation : Any, CommonRepresent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun write(key: Key, input: CommonRepresentation): StoreDelegateWriteResult = try {
|
internal suspend fun write(key: Key, value: Output): StoreDelegateWriteResult = try {
|
||||||
memCache?.put(key, input)
|
memCache?.put(key, value)
|
||||||
sourceOfTruth?.write(key, input)
|
sourceOfTruth?.write(key, value)
|
||||||
StoreDelegateWriteResult.Success
|
StoreDelegateWriteResult.Success
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
StoreDelegateWriteResult.Error.Exception(error)
|
StoreDelegateWriteResult.Error.Exception(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun latestOrNull(key: Key): CommonRepresentation? = fromMemCache(key) ?: fromSourceOfTruth(key)
|
internal suspend fun latestOrNull(key: Key): Output? = fromMemCache(key) ?: fromSourceOfTruth(key)
|
||||||
private suspend fun fromSourceOfTruth(key: Key) = sourceOfTruth?.reader(key, CompletableDeferred(Unit))?.map { it.dataOrNull() }?.first()
|
private suspend fun fromSourceOfTruth(key: Key) = sourceOfTruth?.reader(key, CompletableDeferred(Unit))?.map { it.dataOrNull() }?.first()
|
||||||
private fun fromMemCache(key: Key) = memCache?.getIfPresent(key)
|
private fun fromMemCache(key: Key) = memCache?.getIfPresent(key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,56 +3,56 @@ package org.mobilenativefoundation.store.store5.impl
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import org.mobilenativefoundation.store.store5.Bookkeeper
|
import org.mobilenativefoundation.store.store5.Bookkeeper
|
||||||
|
import org.mobilenativefoundation.store.store5.Converter
|
||||||
import org.mobilenativefoundation.store.store5.Fetcher
|
import org.mobilenativefoundation.store.store5.Fetcher
|
||||||
import org.mobilenativefoundation.store.store5.MemoryPolicy
|
import org.mobilenativefoundation.store.store5.MemoryPolicy
|
||||||
import org.mobilenativefoundation.store.store5.MutableStore
|
import org.mobilenativefoundation.store.store5.MutableStore
|
||||||
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||||
import org.mobilenativefoundation.store.store5.Store
|
import org.mobilenativefoundation.store.store5.Store
|
||||||
import org.mobilenativefoundation.store.store5.StoreBuilder
|
import org.mobilenativefoundation.store.store5.StoreBuilder
|
||||||
import org.mobilenativefoundation.store.store5.StoreConverter
|
|
||||||
import org.mobilenativefoundation.store.store5.StoreDefaults
|
import org.mobilenativefoundation.store.store5.StoreDefaults
|
||||||
import org.mobilenativefoundation.store.store5.Updater
|
import org.mobilenativefoundation.store.store5.Updater
|
||||||
import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore
|
import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore
|
||||||
|
|
||||||
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any> storeBuilderFromFetcher(
|
fun <Key : Any, Network : Any, Output : Any> storeBuilderFromFetcher(
|
||||||
fetcher: Fetcher<Key, NetworkRepresentation>,
|
fetcher: Fetcher<Key, Network>,
|
||||||
sourceOfTruth: SourceOfTruth<Key, *>? = null,
|
sourceOfTruth: SourceOfTruth<Key, *>? = null,
|
||||||
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, *> = RealStoreBuilder(fetcher, sourceOfTruth)
|
): StoreBuilder<Key, Network, Output, *> = RealStoreBuilder(fetcher, sourceOfTruth)
|
||||||
|
|
||||||
fun <Key : Any, CommonRepresentation : Any, NetworkRepresentation : Any, SourceOfTruthRepresentation : Any> storeBuilderFromFetcherAndSourceOfTruth(
|
fun <Key : Any, Output : Any, Network : Any, Local : Any> storeBuilderFromFetcherAndSourceOfTruth(
|
||||||
fetcher: Fetcher<Key, NetworkRepresentation>,
|
fetcher: Fetcher<Key, Network>,
|
||||||
sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>,
|
sourceOfTruth: SourceOfTruth<Key, Local>,
|
||||||
): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> = RealStoreBuilder(fetcher, sourceOfTruth)
|
): StoreBuilder<Key, Network, Output, Local> = RealStoreBuilder(fetcher, sourceOfTruth)
|
||||||
|
|
||||||
internal class RealStoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
|
internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||||
private val fetcher: Fetcher<Key, NetworkRepresentation>,
|
private val fetcher: Fetcher<Key, Network>,
|
||||||
private val sourceOfTruth: SourceOfTruth<Key, SourceOfTruthRepresentation>? = null
|
private val sourceOfTruth: SourceOfTruth<Key, Local>? = null
|
||||||
) : StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
) : StoreBuilder<Key, Network, Output, Local> {
|
||||||
private var scope: CoroutineScope? = null
|
private var scope: CoroutineScope? = null
|
||||||
private var cachePolicy: MemoryPolicy<Key, CommonRepresentation>? = StoreDefaults.memoryPolicy
|
private var cachePolicy: MemoryPolicy<Key, Output>? = StoreDefaults.memoryPolicy
|
||||||
private var converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null
|
private var converter: Converter<Network, Output, Local>? = null
|
||||||
|
|
||||||
override fun scope(scope: CoroutineScope): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
override fun scope(scope: CoroutineScope): StoreBuilder<Key, Network, Output, Local> {
|
||||||
this.scope = scope
|
this.scope = scope
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cachePolicy(memoryPolicy: MemoryPolicy<Key, CommonRepresentation>?): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
override fun cachePolicy(memoryPolicy: MemoryPolicy<Key, Output>?): StoreBuilder<Key, Network, Output, Local> {
|
||||||
cachePolicy = memoryPolicy
|
cachePolicy = memoryPolicy
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun disableCache(): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
override fun disableCache(): StoreBuilder<Key, Network, Output, Local> {
|
||||||
cachePolicy = null
|
cachePolicy = null
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun converter(converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>): StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation> {
|
override fun converter(converter: Converter<Network, Output, Local>): StoreBuilder<Key, Network, Output, Local> {
|
||||||
this.converter = converter
|
this.converter = converter
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun build(): Store<Key, CommonRepresentation> = RealStore(
|
override fun build(): Store<Key, Output> = RealStore(
|
||||||
scope = scope ?: GlobalScope,
|
scope = scope ?: GlobalScope,
|
||||||
sourceOfTruth = sourceOfTruth,
|
sourceOfTruth = sourceOfTruth,
|
||||||
fetcher = fetcher,
|
fetcher = fetcher,
|
||||||
|
@ -60,11 +60,11 @@ internal class RealStoreBuilder<Key : Any, NetworkRepresentation : Any, CommonRe
|
||||||
converter = converter
|
converter = converter
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun <NetworkWriteResponse : Any> build(
|
override fun <UpdaterResult : Any> build(
|
||||||
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>,
|
updater: Updater<Key, Output, UpdaterResult>,
|
||||||
bookkeeper: Bookkeeper<Key>
|
bookkeeper: Bookkeeper<Key>
|
||||||
): MutableStore<Key, CommonRepresentation> =
|
): MutableStore<Key, Output> =
|
||||||
build().asMutableStore<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation, NetworkWriteResponse>(
|
build().asMutableStore<Key, Network, Output, Local, UpdaterResult>(
|
||||||
updater = updater,
|
updater = updater,
|
||||||
bookkeeper = bookkeeper
|
bookkeeper = bookkeeper
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,9 +2,9 @@ package org.mobilenativefoundation.store.store5.impl
|
||||||
|
|
||||||
import org.mobilenativefoundation.store.store5.StoreWriteRequest
|
import org.mobilenativefoundation.store.store5.StoreWriteRequest
|
||||||
|
|
||||||
data class RealStoreWriteRequest<Key : Any, CommonRepresentation : Any, NetworkWriteResponse : Any>(
|
data class RealStoreWriteRequest<Key : Any, Output : Any, Response : Any>(
|
||||||
override val key: Key,
|
override val key: Key,
|
||||||
override val input: CommonRepresentation,
|
override val value: Output,
|
||||||
override val created: Long,
|
override val created: Long,
|
||||||
override val onCompletions: List<OnStoreWriteCompletion>?
|
override val onCompletions: List<OnStoreWriteCompletion>?
|
||||||
) : StoreWriteRequest<Key, CommonRepresentation, NetworkWriteResponse>
|
) : StoreWriteRequest<Key, Output, Response>
|
||||||
|
|
|
@ -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.flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import org.mobilenativefoundation.store.store5.Converter
|
||||||
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||||
import org.mobilenativefoundation.store.store5.StoreConverter
|
|
||||||
import org.mobilenativefoundation.store.store5.StoreReadResponse
|
import org.mobilenativefoundation.store.store5.StoreReadResponse
|
||||||
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
|
import org.mobilenativefoundation.store.store5.StoreReadResponseOrigin
|
||||||
import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
|
import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
|
||||||
|
@ -39,9 +39,9 @@ import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
|
||||||
* dispatching values to downstream while a write is in progress.
|
* dispatching values to downstream while a write is in progress.
|
||||||
*/
|
*/
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any>(
|
internal class SourceOfTruthWithBarrier<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||||
private val delegate: SourceOfTruth<Key, SourceOfTruthRepresentation>,
|
private val delegate: SourceOfTruth<Key, Local>,
|
||||||
private val converter: StoreConverter<NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>? = null,
|
private val converter: Converter<Network, Output, Local>? = null,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Each key has a barrier so that we can block reads while writing.
|
* Each key has a barrier so that we can block reads while writing.
|
||||||
|
@ -59,7 +59,7 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
|
||||||
*/
|
*/
|
||||||
private val versionCounter = atomic(0L)
|
private val versionCounter = atomic(0L)
|
||||||
|
|
||||||
fun reader(key: Key, lock: CompletableDeferred<Unit>): Flow<StoreReadResponse<CommonRepresentation?>> {
|
fun reader(key: Key, lock: CompletableDeferred<Unit>): Flow<StoreReadResponse<Output?>> {
|
||||||
return flow {
|
return flow {
|
||||||
val barrier = barriers.acquire(key)
|
val barrier = barriers.acquire(key)
|
||||||
val readerVersion: Long = versionCounter.incrementAndGet()
|
val readerVersion: Long = versionCounter.incrementAndGet()
|
||||||
|
@ -74,9 +74,9 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val readFlow: Flow<StoreReadResponse<CommonRepresentation?>> = when (barrierMessage) {
|
val readFlow: Flow<StoreReadResponse<Output?>> = when (barrierMessage) {
|
||||||
is BarrierMsg.Open ->
|
is BarrierMsg.Open ->
|
||||||
delegate.reader(key).mapIndexed { index, sourceOfTruthRepresentation ->
|
delegate.reader(key).mapIndexed { index, sourceOfTruth ->
|
||||||
if (index == 0 && messageArrivedAfterMe) {
|
if (index == 0 && messageArrivedAfterMe) {
|
||||||
val firstMsgOrigin = if (writeError == null) {
|
val firstMsgOrigin = if (writeError == null) {
|
||||||
// restarted barrier without an error means write succeeded
|
// restarted barrier without an error means write succeeded
|
||||||
|
@ -89,8 +89,8 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
|
||||||
StoreReadResponseOrigin.SourceOfTruth
|
StoreReadResponseOrigin.SourceOfTruth
|
||||||
}
|
}
|
||||||
|
|
||||||
val value = sourceOfTruthRepresentation as? CommonRepresentation ?: if (sourceOfTruthRepresentation != null) {
|
val value = sourceOfTruth as? Output ?: if (sourceOfTruth != null) {
|
||||||
converter?.fromSourceOfTruthRepresentationToCommonRepresentation(sourceOfTruthRepresentation)
|
converter?.fromLocalToOutput(sourceOfTruth)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -101,11 +101,11 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
|
||||||
} else {
|
} else {
|
||||||
StoreReadResponse.Data(
|
StoreReadResponse.Data(
|
||||||
origin = StoreReadResponseOrigin.SourceOfTruth,
|
origin = StoreReadResponseOrigin.SourceOfTruth,
|
||||||
value = sourceOfTruthRepresentation as? CommonRepresentation
|
value = sourceOfTruth as? Output
|
||||||
?: if (sourceOfTruthRepresentation != null) converter?.fromSourceOfTruthRepresentationToCommonRepresentation(
|
?: if (sourceOfTruth != null) converter?.fromLocalToOutput(
|
||||||
sourceOfTruthRepresentation
|
sourceOfTruth
|
||||||
) else null
|
) else null
|
||||||
) as StoreReadResponse<CommonRepresentation?>
|
) as StoreReadResponse<Output?>
|
||||||
}
|
}
|
||||||
}.catch { throwable ->
|
}.catch { throwable ->
|
||||||
this.emit(
|
this.emit(
|
||||||
|
@ -146,12 +146,12 @@ internal class SourceOfTruthWithBarrier<Key : Any, NetworkRepresentation : Any,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
suspend fun write(key: Key, value: CommonRepresentation) {
|
suspend fun write(key: Key, value: Output) {
|
||||||
val barrier = barriers.acquire(key)
|
val barrier = barriers.acquire(key)
|
||||||
try {
|
try {
|
||||||
barrier.emit(BarrierMsg.Blocked(versionCounter.incrementAndGet()))
|
barrier.emit(BarrierMsg.Blocked(versionCounter.incrementAndGet()))
|
||||||
val writeError = try {
|
val writeError = try {
|
||||||
val input = value as? SourceOfTruthRepresentation ?: converter?.fromCommonRepresentationToSourceOfTruthRepresentation(value)
|
val input = value as? Local ?: converter?.fromOutputToLocal(value)
|
||||||
if (input != null) {
|
if (input != null) {
|
||||||
delegate.write(key, input)
|
delegate.write(key, input)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import org.mobilenativefoundation.store.store5.impl.RealStore
|
||||||
* Helper factory that will return data for [key] if it is cached otherwise will return
|
* Helper factory that will return data for [key] if it is cached otherwise will return
|
||||||
* fresh/network data (updating your caches)
|
* fresh/network data (updating your caches)
|
||||||
*/
|
*/
|
||||||
suspend fun <Key : Any, CommonRepresentation : Any> Store<Key, CommonRepresentation>.get(key: Key) =
|
suspend fun <Key : Any, Output : Any> Store<Key, Output>.get(key: Key) =
|
||||||
stream(StoreReadRequest.cached(key, refresh = false))
|
stream(StoreReadRequest.cached(key, refresh = false))
|
||||||
.filterNot { it is StoreReadResponse.Loading || it is StoreReadResponse.NoNewData }
|
.filterNot { it is StoreReadResponse.Loading || it is StoreReadResponse.NoNewData }
|
||||||
.first()
|
.first()
|
||||||
|
@ -29,18 +29,18 @@ suspend fun <Key : Any, CommonRepresentation : Any> Store<Key, CommonRepresentat
|
||||||
* data **even** if you explicitly requested fresh data.
|
* data **even** if you explicitly requested fresh data.
|
||||||
* See https://github.com/dropbox/Store/pull/194 for context
|
* See https://github.com/dropbox/Store/pull/194 for context
|
||||||
*/
|
*/
|
||||||
suspend fun <Key : Any, CommonRepresentation : Any> Store<Key, CommonRepresentation>.fresh(key: Key) =
|
suspend fun <Key : Any, Output : Any> Store<Key, Output>.fresh(key: Key) =
|
||||||
stream(StoreReadRequest.fresh(key))
|
stream(StoreReadRequest.fresh(key))
|
||||||
.filterNot { it is StoreReadResponse.Loading || it is StoreReadResponse.NoNewData }
|
.filterNot { it is StoreReadResponse.Loading || it is StoreReadResponse.NoNewData }
|
||||||
.first()
|
.first()
|
||||||
.requireData()
|
.requireData()
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any, NetworkWriteResponse : Any> Store<Key, CommonRepresentation>.asMutableStore(
|
fun <Key : Any, Network : Any, Output : Any, Local : Any, Response : Any> Store<Key, Output>.asMutableStore(
|
||||||
updater: Updater<Key, CommonRepresentation, NetworkWriteResponse>,
|
updater: Updater<Key, Output, Response>,
|
||||||
bookkeeper: Bookkeeper<Key>
|
bookkeeper: Bookkeeper<Key>
|
||||||
): MutableStore<Key, CommonRepresentation> {
|
): MutableStore<Key, Output> {
|
||||||
val delegate = this as? RealStore<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>
|
val delegate = this as? RealStore<Key, Network, Output, Local>
|
||||||
?: throw Exception("MutableStore requires Store to be built using StoreBuilder")
|
?: throw Exception("MutableStore requires Store to be built using StoreBuilder")
|
||||||
|
|
||||||
return RealMutableStore(
|
return RealMutableStore(
|
||||||
|
|
|
@ -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
|
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
|
import org.mobilenativefoundation.store.store5.UpdaterResult
|
||||||
|
|
||||||
sealed class EagerConflictResolutionResult<out NetworkWriteResponse : Any> {
|
sealed class EagerConflictResolutionResult<out Response : Any> {
|
||||||
|
|
||||||
sealed class Success<NetworkWriteResponse : Any> : EagerConflictResolutionResult<NetworkWriteResponse>() {
|
sealed class Success<Response : Any> : EagerConflictResolutionResult<Response>() {
|
||||||
object NoConflicts : Success<Nothing>()
|
object NoConflicts : Success<Nothing>()
|
||||||
data class ConflictsResolved<NetworkWriteResponse : Any>(val value: UpdaterResult.Success) : Success<NetworkWriteResponse>()
|
data class ConflictsResolved<Response : Any>(val value: UpdaterResult.Success) : Success<Response>()
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Error : EagerConflictResolutionResult<Nothing>() {
|
sealed class Error : EagerConflictResolutionResult<Nothing>() {
|
||||||
|
|
|
@ -211,6 +211,6 @@ class FetcherResponseTests {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>.buildWithTestScope() =
|
private fun <Key : Any, Network : Any, Output : Any, Local : Any> StoreBuilder<Key, Network, Output, Local>.buildWithTestScope() =
|
||||||
scope(testScope).build()
|
scope(testScope).build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -865,6 +865,6 @@ class FlowStoreTests {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun <Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, SourceOfTruthRepresentation : Any> StoreBuilder<Key, NetworkRepresentation, CommonRepresentation, SourceOfTruthRepresentation>.buildWithTestScope() =
|
private fun <Key : Any, Network : Any, Output : Any, Local : Any> StoreBuilder<Key, Network, Output, Local>.buildWithTestScope() =
|
||||||
scope(testScope).build()
|
scope(testScope).build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,14 @@ import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore
|
import org.mobilenativefoundation.store.store5.impl.extensions.asMutableStore
|
||||||
import org.mobilenativefoundation.store.store5.util.fake.NoteApi
|
import org.mobilenativefoundation.store.store5.util.fake.NotesApi
|
||||||
import org.mobilenativefoundation.store.store5.util.fake.NoteBookkeeping
|
import org.mobilenativefoundation.store.store5.util.fake.NotesBookkeeping
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.CommonNote
|
||||||
|
import org.mobilenativefoundation.store.store5.util.model.NetworkNote
|
||||||
import org.mobilenativefoundation.store.store5.util.model.Note
|
import org.mobilenativefoundation.store.store5.util.model.Note
|
||||||
import org.mobilenativefoundation.store.store5.util.model.NoteCommonRepresentation
|
|
||||||
import org.mobilenativefoundation.store.store5.util.model.NoteData
|
import org.mobilenativefoundation.store.store5.util.model.NoteData
|
||||||
import org.mobilenativefoundation.store.store5.util.model.NoteNetworkRepresentation
|
import org.mobilenativefoundation.store.store5.util.model.NotesWriteResponse
|
||||||
import org.mobilenativefoundation.store.store5.util.model.NoteNetworkWriteResponse
|
import org.mobilenativefoundation.store.store5.util.model.SOTNote
|
||||||
import org.mobilenativefoundation.store.store5.util.model.NoteSourceOfTruthRepresentation
|
|
||||||
import kotlin.test.BeforeTest
|
import kotlin.test.BeforeTest
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
@ -20,22 +20,22 @@ import kotlin.test.assertEquals
|
||||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalStoreApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalStoreApi::class)
|
||||||
class UpdaterTests {
|
class UpdaterTests {
|
||||||
private val testScope = TestScope()
|
private val testScope = TestScope()
|
||||||
private lateinit var api: NoteApi
|
private lateinit var api: NotesApi
|
||||||
private lateinit var bookkeeping: NoteBookkeeping
|
private lateinit var bookkeeping: NotesBookkeeping
|
||||||
|
|
||||||
@BeforeTest
|
@BeforeTest
|
||||||
fun before() {
|
fun before() {
|
||||||
api = NoteApi()
|
api = NotesApi()
|
||||||
bookkeeping = NoteBookkeeping()
|
bookkeeping = NotesBookkeeping()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenEmptyMarketWhenWriteThenSuccessResponsesAndApiUpdated() = testScope.runTest {
|
fun givenEmptyMarketWhenWriteThenSuccessResponsesAndApiUpdated() = testScope.runTest {
|
||||||
val updater = Updater.by<String, NoteCommonRepresentation, NoteNetworkWriteResponse>(
|
val updater = Updater.by<String, CommonNote, NotesWriteResponse>(
|
||||||
post = { key, commonRepresentation ->
|
post = { key, common ->
|
||||||
val networkWriteResponse = api.post(key, commonRepresentation)
|
val response = api.post(key, common)
|
||||||
if (networkWriteResponse.ok) {
|
if (response.ok) {
|
||||||
UpdaterResult.Success.Typed(networkWriteResponse)
|
UpdaterResult.Success.Typed(response)
|
||||||
} else {
|
} else {
|
||||||
UpdaterResult.Error.Message("Failed to sync")
|
UpdaterResult.Error.Message("Failed to sync")
|
||||||
}
|
}
|
||||||
|
@ -48,14 +48,14 @@ class UpdaterTests {
|
||||||
clearAll = bookkeeping::clear
|
clearAll = bookkeeping::clear
|
||||||
)
|
)
|
||||||
|
|
||||||
val store = StoreBuilder.from<String, NoteNetworkRepresentation, NoteCommonRepresentation>(
|
val store = StoreBuilder.from<String, NetworkNote, CommonNote>(
|
||||||
fetcher = Fetcher.ofFlow { key ->
|
fetcher = Fetcher.ofFlow { key ->
|
||||||
val networkRepresentation = NoteNetworkRepresentation(NoteData.Single(Note("$key-id", "$key-title", "$key-content")))
|
val network = NetworkNote(NoteData.Single(Note("$key-id", "$key-title", "$key-content")))
|
||||||
flow { emit(networkRepresentation) }
|
flow { emit(network) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
.asMutableStore<String, NoteNetworkRepresentation, NoteCommonRepresentation, NoteSourceOfTruthRepresentation, NoteNetworkWriteResponse>(
|
.asMutableStore<String, NetworkNote, CommonNote, SOTNote, NotesWriteResponse>(
|
||||||
updater = updater,
|
updater = updater,
|
||||||
bookkeeper = bookkeeper
|
bookkeeper = bookkeeper
|
||||||
)
|
)
|
||||||
|
@ -64,14 +64,14 @@ class UpdaterTests {
|
||||||
val noteTitle = "1-title"
|
val noteTitle = "1-title"
|
||||||
val noteContent = "1-content"
|
val noteContent = "1-content"
|
||||||
val noteData = NoteData.Single(Note(noteKey, noteTitle, noteContent))
|
val noteData = NoteData.Single(Note(noteKey, noteTitle, noteContent))
|
||||||
val writeRequest = StoreWriteRequest.of<String, NoteCommonRepresentation, NoteNetworkWriteResponse>(
|
val writeRequest = StoreWriteRequest.of<String, CommonNote, NotesWriteResponse>(
|
||||||
key = noteKey,
|
key = noteKey,
|
||||||
input = NoteCommonRepresentation(noteData)
|
value = CommonNote(noteData)
|
||||||
)
|
)
|
||||||
|
|
||||||
val storeWriteResponse = store.write(writeRequest)
|
val storeWriteResponse = store.write(writeRequest)
|
||||||
|
|
||||||
assertEquals(StoreWriteResponse.Success.Typed(NoteNetworkWriteResponse(noteKey, true)), storeWriteResponse)
|
assertEquals(StoreWriteResponse.Success.Typed(NotesWriteResponse(noteKey, true)), storeWriteResponse)
|
||||||
assertEquals(NoteNetworkRepresentation(noteData), api.db[noteKey])
|
assertEquals(NetworkNote(noteData), api.db[noteKey])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,9 @@ import org.mobilenativefoundation.store.store5.SourceOfTruth
|
||||||
/**
|
/**
|
||||||
* Only used in FlowStoreTest. We should get rid of it eventually.
|
* Only used in FlowStoreTest. We should get rid of it eventually.
|
||||||
*/
|
*/
|
||||||
class SimplePersisterAsFlowable<Key : Any, SourceOfTruthRepresentation : Any>(
|
class SimplePersisterAsFlowable<Key : Any, Output : Any>(
|
||||||
private val reader: suspend (Key) -> SourceOfTruthRepresentation?,
|
private val reader: suspend (Key) -> Output?,
|
||||||
private val writer: suspend (Key, SourceOfTruthRepresentation) -> Unit,
|
private val writer: suspend (Key, Output) -> Unit,
|
||||||
private val delete: (suspend (Key) -> Unit)? = null
|
private val delete: (suspend (Key) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -23,14 +23,14 @@ class SimplePersisterAsFlowable<Key : Any, SourceOfTruthRepresentation : Any>(
|
||||||
|
|
||||||
private val versionTracker = KeyTracker<Key>()
|
private val versionTracker = KeyTracker<Key>()
|
||||||
|
|
||||||
fun flowReader(key: Key): Flow<SourceOfTruthRepresentation?> = flow {
|
fun flowReader(key: Key): Flow<Output?> = flow {
|
||||||
versionTracker.keyFlow(key).collect {
|
versionTracker.keyFlow(key).collect {
|
||||||
emit(reader(key))
|
emit(reader(key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun flowWriter(key: Key, input: SourceOfTruthRepresentation) {
|
suspend fun flowWriter(key: Key, value: Output) {
|
||||||
writer(key, input)
|
writer(key, value)
|
||||||
versionTracker.invalidate(key)
|
versionTracker.invalidate(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class SimplePersisterAsFlowable<Key : Any, SourceOfTruthRepresentation : Any>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <Key : Any, SourceOfTruthRepresentation : Any> SimplePersisterAsFlowable<Key, SourceOfTruthRepresentation>.asSourceOfTruth() =
|
fun <Key : Any, Output : Any> SimplePersisterAsFlowable<Key, Output>.asSourceOfTruth() =
|
||||||
SourceOfTruth.of(
|
SourceOfTruth.of(
|
||||||
reader = ::flowReader,
|
reader = ::flowReader,
|
||||||
writer = ::flowWriter,
|
writer = ::flowWriter,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package org.mobilenativefoundation.store.store5.util
|
package org.mobilenativefoundation.store.store5.util
|
||||||
|
|
||||||
internal interface TestApi<Key : Any, NetworkRepresentation : Any, CommonRepresentation : Any, NetworkWriteResponse : Any> {
|
internal interface TestApi<Key : Any, Network : Any, Output : Any, Response : Any> {
|
||||||
fun get(key: Key, fail: Boolean = false): NetworkRepresentation?
|
fun get(key: Key, fail: Boolean = false): Network?
|
||||||
fun post(key: Key, value: CommonRepresentation, fail: Boolean = false): NetworkWriteResponse
|
fun post(key: Key, value: Output, fail: Boolean = false): Response
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import org.mobilenativefoundation.store.store5.impl.operators.mapIndexed
|
||||||
* Helper factory that will return [StoreReadResponse.Data] for [key]
|
* Helper factory that will return [StoreReadResponse.Data] for [key]
|
||||||
* if it is cached otherwise will return fresh/network data (updating your caches)
|
* if it is cached otherwise will return fresh/network data (updating your caches)
|
||||||
*/
|
*/
|
||||||
suspend fun <Key : Any, CommonRepresentation : Any> Store<Key, CommonRepresentation>.getData(key: Key) =
|
suspend fun <Key : Any, Output : Any> Store<Key, Output>.getData(key: Key) =
|
||||||
stream(
|
stream(
|
||||||
StoreReadRequest.cached(key, refresh = false)
|
StoreReadRequest.cached(key, refresh = false)
|
||||||
).filterNot {
|
).filterNot {
|
||||||
|
|
|
@ -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
|
package org.mobilenativefoundation.store.store5.util.fake
|
||||||
|
|
||||||
class NoteBookkeeping {
|
class NotesBookkeeping {
|
||||||
private val log: MutableMap<String, Long?> = mutableMapOf()
|
private val log: MutableMap<String, Long?> = mutableMapOf()
|
||||||
fun setLastFailedSync(key: String, timestamp: Long, fail: Boolean = false): Boolean {
|
fun setLastFailedSync(key: String, timestamp: Long, fail: Boolean = false): Boolean {
|
||||||
if (fail) {
|
if (fail) {
|
|
@ -5,20 +5,20 @@ internal sealed class NoteData {
|
||||||
data class Collection(val items: List<Note>) : NoteData()
|
data class Collection(val items: List<Note>) : NoteData()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class NoteNetworkWriteResponse(
|
internal data class NotesWriteResponse(
|
||||||
val key: String,
|
val key: String,
|
||||||
val ok: Boolean
|
val ok: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
internal data class NoteNetworkRepresentation(
|
internal data class NetworkNote(
|
||||||
val data: NoteData? = null
|
val data: NoteData? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
internal data class NoteCommonRepresentation(
|
internal data class CommonNote(
|
||||||
val data: NoteData? = null
|
val data: NoteData? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
internal data class NoteSourceOfTruthRepresentation(
|
internal data class SOTNote(
|
||||||
val data: NoteData? = null
|
val data: NoteData? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue