diff --git a/Images/friendly_robot.png b/Images/friendly_robot.png new file mode 100644 index 0000000..424a3f7 Binary files /dev/null and b/Images/friendly_robot.png differ diff --git a/README.md b/README.md index c004806..233ff76 100644 --- a/README.md +++ b/README.md @@ -1,362 +1,123 @@ -# Store 5 +
+ +

Store5

+
-## Why We Made Store +
+

Full documentation can be found on our website!

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