Make it easier to work with lists (#548)
* Make it possible to provide memory cache Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Format Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Add HybridCache Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Fix putList Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Enable memory cache delegation with Guava as default Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Add MutableStoreWithHybridCacheTests Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Support all cache methods Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Rename to multicache Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Refactor from list decomposition to collection decomposition Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Remove ReactiveCircus/android-emulator-runner Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Remove MemoryCache Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Update .ci_test_and_publish.yml Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Update .ci_test_and_publish.yml Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Update .ci_test_and_publish.yml Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Update .ci_test_and_publish.yml Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Format Signed-off-by: Matt Ramotar <mramotar@dropbox.com> * Prepare for release 5.0.0-beta01 Signed-off-by: Matt Ramotar <mramotar@dropbox.com> --------- Signed-off-by: Matt Ramotar <mramotar@dropbox.com>
This commit is contained in:
parent
c10f355d24
commit
fbcd34fd16
21 changed files with 446 additions and 81 deletions
|
@ -2,6 +2,11 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [5.0.0-beta01] (2023-05-19)
|
||||
|
||||
* Delegate memory cache implementation and provide a hybrid cache with automatic list decomposition as a separate
|
||||
artifact [#548](https://github.com/MobileNativeFoundation/Store/pull/548)
|
||||
|
||||
## [5.0.0-alpha06] (2023-05-08)
|
||||
|
||||
* Separate MutableStoreBuilder from
|
||||
|
@ -255,7 +260,9 @@ This is a first alpha release of Store ported to RxJava 2.
|
|||
* The change log for Store version 1.x can be
|
||||
found [here](https://github.com/NYTimes/Store/blob/develop/CHANGELOG.md).
|
||||
|
||||
[Unreleased]: https://github.com/MobileNativeFoundation/Store/compare/v5.0.0-alpha06...HEAD
|
||||
[Unreleased]: https://github.com/MobileNativeFoundation/Store/compare/v5.0.0-beta01...HEAD
|
||||
|
||||
[5.0.0-beta01]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.0.0-beta01
|
||||
|
||||
[5.0.0-alpha06]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.0.0-alpha06
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
#### Android
|
||||
```kotlin
|
||||
implementation "org.mobilenativefoundation.store:store5:5.0.0-alpha06"
|
||||
implementation "org.mobilenativefoundation.store:store5:5.0.0-beta01"
|
||||
implementation "org.jetbrains.kotlinx:atomicfu:0.18.5"
|
||||
```
|
||||
|
||||
|
@ -46,7 +46,7 @@ implementation "org.jetbrains.kotlinx:atomicfu:0.18.5"
|
|||
```kotlin
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation("org.mobilenativefoundation.store:store5:5.0.0-alpha06")
|
||||
implementation("org.mobilenativefoundation.store:store5:5.0.0-beta01")
|
||||
implementation("org.jetbrains.kotlinx:atomicfu:0.18.5")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,6 @@ object Version {
|
|||
const val navigationCompose = "2.5.2"
|
||||
const val sqlDelight = "1.5.4"
|
||||
const val sqlDelightGradlePlugin = sqlDelight
|
||||
const val store = "5.0.0-alpha06"
|
||||
const val store = "5.0.0-beta01"
|
||||
const val truth = "1.1.3"
|
||||
}
|
5
cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Identifiable.kt
vendored
Normal file
5
cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Identifiable.kt
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
package org.mobilenativefoundation.store.cache5
|
||||
|
||||
interface Identifiable<Id : Any> {
|
||||
val id: Id
|
||||
}
|
77
cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/MultiCache.kt
vendored
Normal file
77
cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/MultiCache.kt
vendored
Normal file
|
@ -0,0 +1,77 @@
|
|||
@file:Suppress("UNCHECKED_CAST")
|
||||
|
||||
package org.mobilenativefoundation.store.cache5
|
||||
|
||||
/**
|
||||
* Implementation of a cache with collection decomposition.
|
||||
* Stores and manages the relationship among single items and collections.
|
||||
* Delegates cache storage and behavior to Guava caches.
|
||||
*/
|
||||
class MultiCache<Key : Any, Output : Identifiable<Key>>(
|
||||
cacheBuilder: CacheBuilder<Key, Output>
|
||||
) {
|
||||
private val collectionCacheBuilder = CacheBuilder<Key, Collection<Output>>().apply {
|
||||
expireAfterAccess(cacheBuilder.expireAfterAccess)
|
||||
expireAfterWrite(cacheBuilder.expireAfterWrite)
|
||||
|
||||
if (cacheBuilder.maximumSize > 0) {
|
||||
maximumSize(cacheBuilder.maximumSize)
|
||||
}
|
||||
// TODO(): Support weigher
|
||||
}
|
||||
|
||||
private val itemKeyToCollectionKey = mutableMapOf<Key, Key>()
|
||||
|
||||
private val itemCache: Cache<Key, Output> = cacheBuilder.build()
|
||||
|
||||
private val collectionCache: Cache<Key, Collection<Output>> = collectionCacheBuilder.build()
|
||||
|
||||
fun getItem(key: Key): Output? {
|
||||
return itemCache.getIfPresent(key)
|
||||
}
|
||||
|
||||
fun putItem(key: Key, item: Output) {
|
||||
itemCache.put(key, item)
|
||||
|
||||
val collectionKey = itemKeyToCollectionKey[key]
|
||||
if (collectionKey != null) {
|
||||
val updatedCollection = collectionCache.getIfPresent(collectionKey)?.map { if (it.id == key) item else it }
|
||||
if (updatedCollection != null) {
|
||||
collectionCache.put(collectionKey, updatedCollection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Collection<Output>> getCollection(key: Key): T? {
|
||||
return collectionCache.getIfPresent(key) as? T
|
||||
}
|
||||
|
||||
fun putCollection(key: Key, items: Collection<Output>) {
|
||||
collectionCache.put(key, items)
|
||||
items.forEach { item ->
|
||||
itemCache.put(item.id, item)
|
||||
itemKeyToCollectionKey[item.id] = key
|
||||
}
|
||||
}
|
||||
|
||||
fun invalidateItem(key: Key) {
|
||||
itemCache.invalidate(key)
|
||||
}
|
||||
|
||||
fun invalidateCollection(key: Key) {
|
||||
val collection = collectionCache.getIfPresent(key)
|
||||
collection?.forEach { item ->
|
||||
invalidateItem(item.id)
|
||||
}
|
||||
collectionCache.invalidate(key)
|
||||
}
|
||||
|
||||
fun invalidateAll() {
|
||||
collectionCache.invalidateAll()
|
||||
itemCache.invalidateAll()
|
||||
}
|
||||
|
||||
fun size(): Long {
|
||||
return itemCache.size() + collectionCache.size()
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ org.gradle.jvmargs=-XX:MaxMetaspaceSize=2G
|
|||
|
||||
# POM file
|
||||
GROUP=org.mobilenativefoundation.store
|
||||
VERSION_NAME=5.0.0-alpha06
|
||||
VERSION_NAME=5.0.0-beta01
|
||||
POM_PACKAGING=pom
|
||||
POM_DESCRIPTION = Store5 is a Kotlin Multiplatform network-resilient repository layer
|
||||
|
||||
|
|
|
@ -16,8 +16,10 @@
|
|||
package org.mobilenativefoundation.store.store5
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.mobilenativefoundation.store.cache5.Cache
|
||||
import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcher
|
||||
import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcherAndSourceOfTruth
|
||||
import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcherSourceOfTruthAndMemoryCache
|
||||
|
||||
/**
|
||||
* Main entry point for creating a [Store].
|
||||
|
@ -69,6 +71,17 @@ interface StoreBuilder<Key : Any, Output : Any> {
|
|||
fun <Key : Any, Input : Any, Output : Any> from(
|
||||
fetcher: Fetcher<Key, Input>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Input>
|
||||
): StoreBuilder<Key, Output> = storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth)
|
||||
): StoreBuilder<Key, Output> =
|
||||
storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth)
|
||||
|
||||
fun <Key : Any, Network : Any, Output : Any, Local : Any> from(
|
||||
fetcher: Fetcher<Key, Network>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Local>,
|
||||
memoryCache: Cache<Key, Output>,
|
||||
): StoreBuilder<Key, Output> = storeBuilderFromFetcherSourceOfTruthAndMemoryCache(
|
||||
fetcher,
|
||||
sourceOfTruth,
|
||||
memoryCache
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ package org.mobilenativefoundation.store.store5
|
|||
* starting the stream from the local [com.dropbox.android.external.store4.impl.SourceOfTruth] and memory cache
|
||||
*
|
||||
*/
|
||||
data class StoreReadRequest<Key> private constructor(
|
||||
data class StoreReadRequest<out Key> private constructor(
|
||||
val key: Key,
|
||||
private val skippedCaches: Int,
|
||||
val refresh: Boolean = false,
|
||||
|
|
|
@ -2,6 +2,8 @@ package org.mobilenativefoundation.store.store5.impl
|
|||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import org.mobilenativefoundation.store.cache5.Cache
|
||||
import org.mobilenativefoundation.store.cache5.CacheBuilder
|
||||
import org.mobilenativefoundation.store.store5.Bookkeeper
|
||||
import org.mobilenativefoundation.store.store5.Converter
|
||||
import org.mobilenativefoundation.store.store5.Fetcher
|
||||
|
@ -24,9 +26,16 @@ fun <Key : Any, Network : Any, Output : Any, Local : Any> mutableStoreBuilderFro
|
|||
sourceOfTruth: SourceOfTruth<Key, Local>,
|
||||
): MutableStoreBuilder<Key, Network, Output, Local> = RealMutableStoreBuilder(fetcher, sourceOfTruth)
|
||||
|
||||
fun <Key : Any, Network : Any, Output : Any, Local : Any> mutableStoreBuilderFromFetcherSourceOfTruthAndMemoryCache(
|
||||
fetcher: Fetcher<Key, Network>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Local>,
|
||||
memoryCache: Cache<Key, Output>
|
||||
): MutableStoreBuilder<Key, Network, Output, Local> = RealMutableStoreBuilder(fetcher, sourceOfTruth, memoryCache)
|
||||
|
||||
internal class RealMutableStoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||
private val fetcher: Fetcher<Key, Network>,
|
||||
private val sourceOfTruth: SourceOfTruth<Key, Local>? = null,
|
||||
private val memoryCache: Cache<Key, Output>? = null
|
||||
) : MutableStoreBuilder<Key, Network, Output, Local> {
|
||||
private var scope: CoroutineScope? = null
|
||||
private var cachePolicy: MemoryPolicy<Key, Output>? = StoreDefaults.memoryPolicy
|
||||
|
@ -62,9 +71,25 @@ internal class RealMutableStoreBuilder<Key : Any, Network : Any, Output : Any, L
|
|||
scope = scope ?: GlobalScope,
|
||||
sourceOfTruth = sourceOfTruth,
|
||||
fetcher = fetcher,
|
||||
memoryPolicy = cachePolicy,
|
||||
converter = converter,
|
||||
validator = validator
|
||||
validator = validator,
|
||||
memCache = memoryCache ?: cachePolicy?.let {
|
||||
CacheBuilder<Key, Output>().apply {
|
||||
if (cachePolicy!!.hasAccessPolicy) {
|
||||
expireAfterAccess(cachePolicy!!.expireAfterAccess)
|
||||
}
|
||||
if (cachePolicy!!.hasWritePolicy) {
|
||||
expireAfterWrite(cachePolicy!!.expireAfterWrite)
|
||||
}
|
||||
if (cachePolicy!!.hasMaxSize) {
|
||||
maximumSize(cachePolicy!!.maxSize)
|
||||
}
|
||||
|
||||
if (cachePolicy!!.hasMaxWeight) {
|
||||
weigher(cachePolicy!!.maxWeight) { key, value -> cachePolicy!!.weigher.weigh(key, value) }
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
)
|
||||
|
||||
override fun <UpdaterResult : Any> build(
|
||||
|
|
|
@ -25,12 +25,11 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import org.mobilenativefoundation.store.cache5.CacheBuilder
|
||||
import org.mobilenativefoundation.store.cache5.Cache
|
||||
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.StoreReadRequest
|
||||
|
@ -47,7 +46,7 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
|
|||
sourceOfTruth: SourceOfTruth<Key, Local>? = null,
|
||||
private val converter: Converter<Network, Output, Local>? = null,
|
||||
private val validator: Validator<Output>?,
|
||||
private val memoryPolicy: MemoryPolicy<Key, Output>?
|
||||
private val memCache: Cache<Key, Output>?
|
||||
) : Store<Key, Output> {
|
||||
/**
|
||||
* This source of truth is either a real database or an in memory source of truth created by
|
||||
|
@ -61,24 +60,6 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
|
|||
SourceOfTruthWithBarrier(it, converter)
|
||||
}
|
||||
|
||||
private val memCache = memoryPolicy?.let {
|
||||
CacheBuilder<Key, Output>().apply {
|
||||
if (memoryPolicy.hasAccessPolicy) {
|
||||
expireAfterAccess(memoryPolicy.expireAfterAccess)
|
||||
}
|
||||
if (memoryPolicy.hasWritePolicy) {
|
||||
expireAfterWrite(memoryPolicy.expireAfterWrite)
|
||||
}
|
||||
if (memoryPolicy.hasMaxSize) {
|
||||
maximumSize(memoryPolicy.maxSize)
|
||||
}
|
||||
|
||||
if (memoryPolicy.hasMaxWeight) {
|
||||
weigher(memoryPolicy.maxWeight) { key, value -> memoryPolicy.weigher.weigh(key, value) }
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetcher controller maintains 1 and only 1 `Multicaster` for a given key to ensure network
|
||||
* requests are shared.
|
||||
|
|
|
@ -4,6 +4,8 @@ package org.mobilenativefoundation.store.store5.impl
|
|||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import org.mobilenativefoundation.store.cache5.Cache
|
||||
import org.mobilenativefoundation.store.cache5.CacheBuilder
|
||||
import org.mobilenativefoundation.store.store5.Converter
|
||||
import org.mobilenativefoundation.store.store5.Fetcher
|
||||
import org.mobilenativefoundation.store.store5.MemoryPolicy
|
||||
|
@ -24,9 +26,16 @@ fun <Key : Any, Input : Any, Output : Any> storeBuilderFromFetcherAndSourceOfTru
|
|||
sourceOfTruth: SourceOfTruth<Key, *>,
|
||||
): StoreBuilder<Key, Output> = RealStoreBuilder(fetcher, sourceOfTruth)
|
||||
|
||||
fun <Key : Any, Network : Any, Output : Any, Local : Any> storeBuilderFromFetcherSourceOfTruthAndMemoryCache(
|
||||
fetcher: Fetcher<Key, Network>,
|
||||
sourceOfTruth: SourceOfTruth<Key, Local>,
|
||||
memoryCache: Cache<Key, Output>,
|
||||
): StoreBuilder<Key, Output> = RealStoreBuilder(fetcher, sourceOfTruth, memoryCache)
|
||||
|
||||
internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any>(
|
||||
private val fetcher: Fetcher<Key, Network>,
|
||||
private val sourceOfTruth: SourceOfTruth<Key, Local>? = null
|
||||
private val sourceOfTruth: SourceOfTruth<Key, Local>? = null,
|
||||
private val memoryCache: Cache<Key, Output>? = null
|
||||
) : StoreBuilder<Key, Output> {
|
||||
private var scope: CoroutineScope? = null
|
||||
private var cachePolicy: MemoryPolicy<Key, Output>? = StoreDefaults.memoryPolicy
|
||||
|
@ -57,17 +66,42 @@ internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local :
|
|||
scope = scope ?: GlobalScope,
|
||||
sourceOfTruth = sourceOfTruth,
|
||||
fetcher = fetcher,
|
||||
memoryPolicy = cachePolicy,
|
||||
converter = converter,
|
||||
validator = validator
|
||||
validator = validator,
|
||||
memCache = memoryCache ?: cachePolicy?.let {
|
||||
CacheBuilder<Key, Output>().apply {
|
||||
if (cachePolicy!!.hasAccessPolicy) {
|
||||
expireAfterAccess(cachePolicy!!.expireAfterAccess)
|
||||
}
|
||||
if (cachePolicy!!.hasWritePolicy) {
|
||||
expireAfterWrite(cachePolicy!!.expireAfterWrite)
|
||||
}
|
||||
if (cachePolicy!!.hasMaxSize) {
|
||||
maximumSize(cachePolicy!!.maxSize)
|
||||
}
|
||||
|
||||
if (cachePolicy!!.hasMaxWeight) {
|
||||
weigher(cachePolicy!!.maxWeight) { key, value -> cachePolicy!!.weigher.weigh(key, value) }
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
)
|
||||
|
||||
override fun <Network : Any, Local : Any> toMutableStoreBuilder(): MutableStoreBuilder<Key, Network, Output, Local> {
|
||||
fetcher as Fetcher<Key, Network>
|
||||
return if (sourceOfTruth == null) {
|
||||
return if (sourceOfTruth == null && memoryCache == null) {
|
||||
mutableStoreBuilderFromFetcher(fetcher)
|
||||
} else if (memoryCache == null) {
|
||||
mutableStoreBuilderFromFetcherAndSourceOfTruth<Key, Network, Output, Local>(
|
||||
fetcher,
|
||||
sourceOfTruth as SourceOfTruth<Key, Local>
|
||||
)
|
||||
} else {
|
||||
mutableStoreBuilderFromFetcherAndSourceOfTruth<Key, Network, Output, Local>(fetcher, sourceOfTruth as SourceOfTruth<Key, Local>)
|
||||
mutableStoreBuilderFromFetcherSourceOfTruthAndMemoryCache(
|
||||
fetcher,
|
||||
sourceOfTruth as SourceOfTruth<Key, Local>,
|
||||
memoryCache
|
||||
)
|
||||
}.apply {
|
||||
if (this@RealStoreBuilder.scope != null) {
|
||||
scope(this@RealStoreBuilder.scope!!)
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
package org.mobilenativefoundation.store.store5
|
||||
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.mobilenativefoundation.store.cache5.CacheBuilder
|
||||
import org.mobilenativefoundation.store.cache5.MultiCache
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NoteCollections
|
||||
import org.mobilenativefoundation.store.store5.util.fake.Notes
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesApi
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesDatabase
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesMemoryCache
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesKey
|
||||
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.SOTNote
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class MutableStoreWithMultiCacheTests {
|
||||
private val testScope = TestScope()
|
||||
private lateinit var api: NotesApi
|
||||
private lateinit var database: NotesDatabase
|
||||
|
||||
@BeforeTest
|
||||
fun before() {
|
||||
api = NotesApi()
|
||||
database = NotesDatabase()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenEmptyStoreWhenListFromFetcherThenListIsDecomposed() = testScope.runTest {
|
||||
val memoryCache = NotesMemoryCache(MultiCache<String, Note>(CacheBuilder()))
|
||||
|
||||
val store = StoreBuilder.from<NotesKey, NetworkNote, NoteData, SOTNote>(
|
||||
fetcher = Fetcher.of<NotesKey, NetworkNote> { key -> api.get(key) },
|
||||
sourceOfTruth = SourceOfTruth.of<NotesKey, SOTNote>(
|
||||
nonFlowReader = { key -> database.get(key) },
|
||||
writer = { key, note -> database.put(key, note) },
|
||||
delete = null,
|
||||
deleteAll = null
|
||||
),
|
||||
memoryCache = memoryCache
|
||||
).toMutableStoreBuilder<NetworkNote, SOTNote>()
|
||||
.converter(
|
||||
Converter.Builder<NetworkNote, NoteData, SOTNote>()
|
||||
.fromLocalToOutput { local -> local.data!! }
|
||||
.fromNetworkToOutput { network -> network.data!! }
|
||||
.fromOutputToLocal { output -> SOTNote(output, Long.MAX_VALUE) }
|
||||
.build()
|
||||
).build(
|
||||
updater = Updater.by(
|
||||
post = { _, _ -> UpdaterResult.Error.Exception(Exception()) }
|
||||
)
|
||||
)
|
||||
|
||||
val freshRequest = StoreReadRequest.fresh(NotesKey.Collection(NoteCollections.Keys.OneAndTwo))
|
||||
|
||||
val freshStream = store.stream<UpdaterResult>(freshRequest)
|
||||
|
||||
val actualResultFromFreshStream = freshStream.take(2).toList()
|
||||
val expectedResultFromFreshStream = listOf(
|
||||
StoreReadResponse.Loading(StoreReadResponseOrigin.Fetcher()),
|
||||
StoreReadResponse.Data(NoteCollections.OneAndTwo, StoreReadResponseOrigin.Fetcher())
|
||||
)
|
||||
|
||||
assertEquals(expectedResultFromFreshStream, actualResultFromFreshStream)
|
||||
|
||||
val singleFromMemoryCache = memoryCache.getIfPresent(NotesKey.Single(Notes.One.id))
|
||||
assertIs<NoteData.Single>(singleFromMemoryCache)
|
||||
assertEquals(singleFromMemoryCache.item, Notes.One)
|
||||
|
||||
val cachedRequest = StoreReadRequest.cached(NotesKey.Single(Notes.One.id), refresh = true)
|
||||
val cachedStream = store.stream<UpdaterResult>(cachedRequest)
|
||||
val actualResultFromCachedStream = cachedStream.take(1).toList()
|
||||
val expectedResultFromCachedStream = listOf(
|
||||
StoreReadResponse.Data(NoteData.Single(Notes.One), StoreReadResponseOrigin.Cache)
|
||||
)
|
||||
|
||||
assertEquals(expectedResultFromCachedStream, actualResultFromCachedStream)
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import org.mobilenativefoundation.store.store5.util.fake.NotesApi
|
|||
import org.mobilenativefoundation.store.store5.util.fake.NotesBookkeeping
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesConverterProvider
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesDatabase
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesKey
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesUpdaterProvider
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesValidator
|
||||
import org.mobilenativefoundation.store.store5.util.model.CommonNote
|
||||
|
@ -55,7 +56,7 @@ class UpdaterTests {
|
|||
clearAll = bookkeeping::clear
|
||||
)
|
||||
|
||||
val store = MutableStoreBuilder.from<String, NetworkNote, CommonNote, SOTNote>(
|
||||
val store = MutableStoreBuilder.from<NotesKey, NetworkNote, CommonNote, SOTNote>(
|
||||
fetcher = Fetcher.of { key -> api.get(key, ttl = ttl) },
|
||||
sourceOfTruth = SourceOfTruth.of(
|
||||
nonFlowReader = { key -> notes.get(key) },
|
||||
|
@ -71,7 +72,7 @@ class UpdaterTests {
|
|||
bookkeeper = bookkeeper
|
||||
)
|
||||
|
||||
val readRequest = StoreReadRequest.fresh(Notes.One.id)
|
||||
val readRequest = StoreReadRequest.fresh(NotesKey.Single(Notes.One.id))
|
||||
|
||||
val stream = store.stream<NotesWriteResponse>(readRequest)
|
||||
|
||||
|
@ -80,27 +81,40 @@ class UpdaterTests {
|
|||
stream,
|
||||
listOf(
|
||||
StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()),
|
||||
StoreReadResponse.Data(CommonNote(NoteData.Single(Notes.One), ttl = ttl), StoreReadResponseOrigin.Fetcher())
|
||||
StoreReadResponse.Data(
|
||||
CommonNote(NoteData.Single(Notes.One), ttl = ttl),
|
||||
StoreReadResponseOrigin.Fetcher
|
||||
()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val newNote = Notes.One.copy(title = "New Title-1")
|
||||
val writeRequest = StoreWriteRequest.of<String, CommonNote, NotesWriteResponse>(
|
||||
key = Notes.One.id,
|
||||
val writeRequest = StoreWriteRequest.of<NotesKey, CommonNote, NotesWriteResponse>(
|
||||
key = NotesKey.Single(Notes.One.id),
|
||||
value = CommonNote(NoteData.Single(newNote))
|
||||
)
|
||||
|
||||
val storeWriteResponse = store.write(writeRequest)
|
||||
|
||||
// Write is success
|
||||
assertEquals(StoreWriteResponse.Success.Typed(NotesWriteResponse(Notes.One.id, true)), storeWriteResponse)
|
||||
assertEquals(
|
||||
StoreWriteResponse.Success.Typed(NotesWriteResponse(NotesKey.Single(Notes.One.id), true)),
|
||||
storeWriteResponse
|
||||
)
|
||||
|
||||
val cachedReadRequest = StoreReadRequest.cached(Notes.One.id, refresh = false)
|
||||
val cachedReadRequest = StoreReadRequest.cached(NotesKey.Single(Notes.One.id), refresh = false)
|
||||
val cachedStream = store.stream<NotesWriteResponse>(cachedReadRequest)
|
||||
|
||||
// Cache + SOT are updated
|
||||
val firstResponse = cachedStream.first()
|
||||
assertEquals(StoreReadResponse.Data(CommonNote(NoteData.Single(newNote), ttl = null), StoreReadResponseOrigin.Cache), firstResponse)
|
||||
assertEquals(
|
||||
StoreReadResponse.Data(
|
||||
CommonNote(NoteData.Single(newNote), ttl = null),
|
||||
StoreReadResponseOrigin.Cache
|
||||
),
|
||||
firstResponse
|
||||
)
|
||||
|
||||
val secondResponse = cachedStream.take(2).last()
|
||||
assertIs<StoreReadResponse.Data<CommonNote>>(secondResponse)
|
||||
|
@ -112,8 +126,11 @@ class UpdaterTests {
|
|||
assertNotNull(secondResponse.value.ttl)
|
||||
|
||||
// API is updated
|
||||
assertEquals(StoreWriteResponse.Success.Typed(NotesWriteResponse(Notes.One.id, true)), storeWriteResponse)
|
||||
assertEquals(NetworkNote(NoteData.Single(newNote), ttl = null), api.db[Notes.One.id])
|
||||
assertEquals(
|
||||
StoreWriteResponse.Success.Typed(NotesWriteResponse(NotesKey.Single(Notes.One.id), true)),
|
||||
storeWriteResponse
|
||||
)
|
||||
assertEquals(NetworkNote(NoteData.Single(newNote), ttl = null), api.db[NotesKey.Single(Notes.One.id)])
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -130,7 +147,7 @@ class UpdaterTests {
|
|||
clearAll = bookkeeping::clear
|
||||
)
|
||||
|
||||
val store = MutableStoreBuilder.from<String, NetworkNote, CommonNote, SOTNote>(
|
||||
val store = MutableStoreBuilder.from<NotesKey, NetworkNote, CommonNote, SOTNote>(
|
||||
fetcher = Fetcher.of { key -> api.get(key, ttl = ttl) },
|
||||
sourceOfTruth = SourceOfTruth.of(
|
||||
nonFlowReader = { key -> notes.get(key) },
|
||||
|
@ -146,7 +163,7 @@ class UpdaterTests {
|
|||
bookkeeper = bookkeeper
|
||||
)
|
||||
|
||||
val readRequest = StoreReadRequest.fresh(Notes.One.id)
|
||||
val readRequest = StoreReadRequest.fresh(NotesKey.Single(Notes.One.id))
|
||||
|
||||
val stream = store.stream<NotesWriteResponse>(readRequest)
|
||||
|
||||
|
@ -155,11 +172,15 @@ class UpdaterTests {
|
|||
stream,
|
||||
listOf(
|
||||
StoreReadResponse.Loading(origin = StoreReadResponseOrigin.Fetcher()),
|
||||
StoreReadResponse.Data(CommonNote(NoteData.Single(Notes.One), ttl = ttl), StoreReadResponseOrigin.Fetcher())
|
||||
StoreReadResponse.Data(
|
||||
CommonNote(NoteData.Single(Notes.One), ttl = ttl),
|
||||
StoreReadResponseOrigin.Fetcher
|
||||
()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val cachedReadRequest = StoreReadRequest.cached(Notes.One.id, refresh = false)
|
||||
val cachedReadRequest = StoreReadRequest.cached(NotesKey.Single(Notes.One.id), refresh = false)
|
||||
val cachedStream = store.stream<NotesWriteResponse>(cachedReadRequest)
|
||||
|
||||
// Cache + SOT are updated
|
||||
|
@ -170,7 +191,11 @@ class UpdaterTests {
|
|||
assertEmitsExactly(
|
||||
cachedStream,
|
||||
listOf(
|
||||
StoreReadResponse.Data(CommonNote(NoteData.Single(Notes.One), ttl = ttl), StoreReadResponseOrigin.Fetcher())
|
||||
StoreReadResponse.Data(
|
||||
CommonNote(NoteData.Single(Notes.One), ttl = ttl),
|
||||
StoreReadResponseOrigin.Fetcher
|
||||
()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -187,7 +212,7 @@ class UpdaterTests {
|
|||
clearAll = bookkeeping::clear
|
||||
)
|
||||
|
||||
val store = MutableStoreBuilder.from<String, NetworkNote, CommonNote, SOTNote>(
|
||||
val store = MutableStoreBuilder.from<NotesKey, NetworkNote, CommonNote, SOTNote>(
|
||||
fetcher = Fetcher.ofFlow { key ->
|
||||
val network = api.get(key)
|
||||
flow { emit(network) }
|
||||
|
@ -207,13 +232,16 @@ class UpdaterTests {
|
|||
)
|
||||
|
||||
val newNote = Notes.One.copy(title = "New Title-1")
|
||||
val writeRequest = StoreWriteRequest.of<String, CommonNote, NotesWriteResponse>(
|
||||
key = Notes.One.id,
|
||||
val writeRequest = StoreWriteRequest.of<NotesKey, CommonNote, NotesWriteResponse>(
|
||||
key = NotesKey.Single(Notes.One.id),
|
||||
value = CommonNote(NoteData.Single(newNote))
|
||||
)
|
||||
val storeWriteResponse = store.write(writeRequest)
|
||||
|
||||
assertEquals(StoreWriteResponse.Success.Typed(NotesWriteResponse(Notes.One.id, true)), storeWriteResponse)
|
||||
assertEquals(NetworkNote(NoteData.Single(newNote)), api.db[Notes.One.id])
|
||||
assertEquals(
|
||||
StoreWriteResponse.Success.Typed(NotesWriteResponse(NotesKey.Single(Notes.One.id), true)),
|
||||
storeWriteResponse
|
||||
)
|
||||
assertEquals(NetworkNote(NoteData.Single(newNote)), api.db[NotesKey.Single(Notes.One.id)])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package org.mobilenativefoundation.store.store5.util.fake
|
||||
|
||||
import org.mobilenativefoundation.store.store5.util.model.NoteData
|
||||
|
||||
internal object NoteCollections {
|
||||
object Keys {
|
||||
const val OneAndTwo = "ONE_AND_TWO"
|
||||
}
|
||||
val OneAndTwo = NoteData.Collection(listOf(Notes.One, Notes.Two))
|
||||
}
|
|
@ -6,14 +6,14 @@ import org.mobilenativefoundation.store.store5.util.model.NetworkNote
|
|||
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>()
|
||||
internal class NotesApi : TestApi<NotesKey, NetworkNote, CommonNote, NotesWriteResponse> {
|
||||
internal val db = mutableMapOf<NotesKey, NetworkNote>()
|
||||
|
||||
init {
|
||||
seed()
|
||||
}
|
||||
|
||||
override fun get(key: String, fail: Boolean, ttl: Long?): NetworkNote {
|
||||
override fun get(key: NotesKey, fail: Boolean, ttl: Long?): NetworkNote {
|
||||
if (fail) {
|
||||
throw Exception()
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ internal class NotesApi : TestApi<String, NetworkNote, CommonNote, NotesWriteRes
|
|||
}
|
||||
}
|
||||
|
||||
override fun post(key: String, value: CommonNote, fail: Boolean): NotesWriteResponse {
|
||||
override fun post(key: NotesKey, value: CommonNote, fail: Boolean): NotesWriteResponse {
|
||||
if (fail) {
|
||||
throw Exception()
|
||||
}
|
||||
|
@ -37,15 +37,16 @@ internal class NotesApi : TestApi<String, NetworkNote, CommonNote, NotesWriteRes
|
|||
}
|
||||
|
||||
private fun seed() {
|
||||
db[Notes.One.id] = NetworkNote(NoteData.Single(Notes.One))
|
||||
db[Notes.Two.id] = NetworkNote(NoteData.Single(Notes.Two))
|
||||
db[Notes.Three.id] = NetworkNote(NoteData.Single(Notes.Three))
|
||||
db[Notes.Four.id] = NetworkNote(NoteData.Single(Notes.Four))
|
||||
db[Notes.Five.id] = NetworkNote(NoteData.Single(Notes.Five))
|
||||
db[Notes.Six.id] = NetworkNote(NoteData.Single(Notes.Six))
|
||||
db[Notes.Seven.id] = NetworkNote(NoteData.Single(Notes.Seven))
|
||||
db[Notes.Eight.id] = NetworkNote(NoteData.Single(Notes.Eight))
|
||||
db[Notes.Nine.id] = NetworkNote(NoteData.Single(Notes.Nine))
|
||||
db[Notes.Ten.id] = NetworkNote(NoteData.Single(Notes.Ten))
|
||||
db[NotesKey.Single(Notes.One.id)] = NetworkNote(NoteData.Single(Notes.One))
|
||||
db[NotesKey.Single(Notes.Two.id)] = NetworkNote(NoteData.Single(Notes.Two))
|
||||
db[NotesKey.Single(Notes.Three.id)] = NetworkNote(NoteData.Single(Notes.Three))
|
||||
db[NotesKey.Single(Notes.Four.id)] = NetworkNote(NoteData.Single(Notes.Four))
|
||||
db[NotesKey.Single(Notes.Five.id)] = NetworkNote(NoteData.Single(Notes.Five))
|
||||
db[NotesKey.Single(Notes.Six.id)] = NetworkNote(NoteData.Single(Notes.Six))
|
||||
db[NotesKey.Single(Notes.Seven.id)] = NetworkNote(NoteData.Single(Notes.Seven))
|
||||
db[NotesKey.Single(Notes.Eight.id)] = NetworkNote(NoteData.Single(Notes.Eight))
|
||||
db[NotesKey.Single(Notes.Nine.id)] = NetworkNote(NoteData.Single(Notes.Nine))
|
||||
db[NotesKey.Single(Notes.Ten.id)] = NetworkNote(NoteData.Single(Notes.Ten))
|
||||
db[NotesKey.Collection(NoteCollections.Keys.OneAndTwo)] = NetworkNote(NoteCollections.OneAndTwo)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package org.mobilenativefoundation.store.store5.util.fake
|
||||
|
||||
class NotesBookkeeping {
|
||||
private val log: MutableMap<String, Long?> = mutableMapOf()
|
||||
fun setLastFailedSync(key: String, timestamp: Long, fail: Boolean = false): Boolean {
|
||||
private val log: MutableMap<NotesKey, Long?> = mutableMapOf()
|
||||
fun setLastFailedSync(key: NotesKey, timestamp: Long, fail: Boolean = false): Boolean {
|
||||
if (fail) {
|
||||
throw Exception()
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ class NotesBookkeeping {
|
|||
return true
|
||||
}
|
||||
|
||||
fun getLastFailedSync(key: String, fail: Boolean = false): Long? {
|
||||
fun getLastFailedSync(key: NotesKey, fail: Boolean = false): Long? {
|
||||
if (fail) {
|
||||
throw Exception()
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ class NotesBookkeeping {
|
|||
return log[key]
|
||||
}
|
||||
|
||||
fun clear(key: String, fail: Boolean = false): Boolean {
|
||||
fun clear(key: NotesKey, fail: Boolean = false): Boolean {
|
||||
if (fail) {
|
||||
throw Exception()
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ package org.mobilenativefoundation.store.store5.util.fake
|
|||
import org.mobilenativefoundation.store.store5.util.model.SOTNote
|
||||
|
||||
internal class NotesDatabase {
|
||||
private val db: MutableMap<String, SOTNote?> = mutableMapOf()
|
||||
fun put(key: String, input: SOTNote, fail: Boolean = false): Boolean {
|
||||
private val db: MutableMap<NotesKey, SOTNote?> = mutableMapOf()
|
||||
fun put(key: NotesKey, input: SOTNote, fail: Boolean = false): Boolean {
|
||||
if (fail) {
|
||||
throw Exception()
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ internal class NotesDatabase {
|
|||
return true
|
||||
}
|
||||
|
||||
fun get(key: String, fail: Boolean = false): SOTNote? {
|
||||
fun get(key: NotesKey, fail: Boolean = false): SOTNote? {
|
||||
if (fail) {
|
||||
throw Exception()
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ internal class NotesDatabase {
|
|||
return db[key]
|
||||
}
|
||||
|
||||
fun clear(key: String, fail: Boolean = false): Boolean {
|
||||
fun clear(key: NotesKey, fail: Boolean = false): Boolean {
|
||||
if (fail) {
|
||||
throw Exception()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package org.mobilenativefoundation.store.store5.util.fake
|
||||
|
||||
sealed class NotesKey {
|
||||
data class Single(val id: String) : NotesKey()
|
||||
data class Collection(val id: String) : NotesKey()
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package org.mobilenativefoundation.store.store5.util.fake
|
||||
|
||||
import org.mobilenativefoundation.store.cache5.Cache
|
||||
import org.mobilenativefoundation.store.cache5.MultiCache
|
||||
import org.mobilenativefoundation.store.store5.util.model.Note
|
||||
import org.mobilenativefoundation.store.store5.util.model.NoteData
|
||||
|
||||
internal class NotesMemoryCache(private val delegate: MultiCache<String, Note>) : Cache<NotesKey, NoteData> {
|
||||
override fun getIfPresent(key: NotesKey): NoteData? = when (key) {
|
||||
is NotesKey.Collection -> {
|
||||
val items = delegate.getCollection<List<Note>>(key.id)
|
||||
if (items != null) {
|
||||
NoteData.Collection(items)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
is NotesKey.Single -> {
|
||||
val item = delegate.getItem(key.id)
|
||||
if (item != null) {
|
||||
NoteData.Single(item)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOrPut(key: NotesKey, valueProducer: () -> NoteData): NoteData {
|
||||
val collection = getIfPresent(key)
|
||||
return if (collection == null) {
|
||||
val noteData = valueProducer()
|
||||
put(key, noteData)
|
||||
noteData
|
||||
} else {
|
||||
collection
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAllPresent(keys: List<*>): Map<NotesKey, NoteData> {
|
||||
val map = mutableMapOf<NotesKey, NoteData>()
|
||||
|
||||
keys.filterIsInstance<NotesKey>().forEach { key ->
|
||||
val noteData = getIfPresent(key)
|
||||
if (noteData != null) {
|
||||
map[key] = noteData
|
||||
}
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
override fun invalidateAll() {
|
||||
delegate.invalidateAll()
|
||||
}
|
||||
|
||||
override fun size(): Long {
|
||||
return delegate.size()
|
||||
}
|
||||
|
||||
override fun invalidateAll(keys: List<NotesKey>) {
|
||||
keys.forEach { key ->
|
||||
invalidate(key)
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate(key: NotesKey) = when (key) {
|
||||
is NotesKey.Collection -> delegate.invalidateCollection(key.id)
|
||||
is NotesKey.Single -> delegate.invalidateItem(key.id)
|
||||
}
|
||||
|
||||
override fun putAll(map: Map<NotesKey, NoteData>) {
|
||||
map.entries.forEach { (key, noteData) ->
|
||||
put(key, noteData)
|
||||
}
|
||||
}
|
||||
|
||||
override fun put(key: NotesKey, value: NoteData) = when (key) {
|
||||
is NotesKey.Collection -> {
|
||||
require(value is NoteData.Collection)
|
||||
delegate.putCollection(key.id, value.items)
|
||||
}
|
||||
|
||||
is NotesKey.Single -> {
|
||||
require(value is NoteData.Single)
|
||||
delegate.putItem(key.id, value.item)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import org.mobilenativefoundation.store.store5.util.model.CommonNote
|
|||
import org.mobilenativefoundation.store.store5.util.model.NotesWriteResponse
|
||||
|
||||
internal class NotesUpdaterProvider(private val api: NotesApi) {
|
||||
fun provide(): Updater<String, CommonNote, NotesWriteResponse> = Updater.by(
|
||||
fun provide(): Updater<NotesKey, CommonNote, NotesWriteResponse> = Updater.by(
|
||||
post = { key, input ->
|
||||
val response = api.post(key, input)
|
||||
if (response.ok) {
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package org.mobilenativefoundation.store.store5.util.model
|
||||
|
||||
import org.mobilenativefoundation.store.cache5.Identifiable
|
||||
import org.mobilenativefoundation.store.store5.util.fake.NotesKey
|
||||
|
||||
internal sealed class NoteData {
|
||||
data class Single(val item: Note) : NoteData()
|
||||
data class Collection(val items: List<Note>) : NoteData()
|
||||
}
|
||||
|
||||
internal data class NotesWriteResponse(
|
||||
val key: String,
|
||||
val key: NotesKey,
|
||||
val ok: Boolean
|
||||
)
|
||||
|
||||
|
@ -26,7 +29,7 @@ internal data class SOTNote(
|
|||
)
|
||||
|
||||
internal data class Note(
|
||||
val id: String,
|
||||
override val id: String,
|
||||
val title: String,
|
||||
val content: String
|
||||
)
|
||||
) : Identifiable<String>
|
||||
|
|
Loading…
Reference in a new issue