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:
Matt Ramotar 2023-05-19 16:41:42 -04:00 committed by GitHub
parent c10f355d24
commit fbcd34fd16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 446 additions and 81 deletions

View file

@ -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

View file

@ -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")
}
}

View file

@ -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"
}

View file

@ -0,0 +1,5 @@
package org.mobilenativefoundation.store.cache5
interface Identifiable<Id : Any> {
val id: Id
}

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

View file

@ -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

View file

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

View file

@ -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,

View file

@ -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(

View file

@ -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.

View file

@ -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!!)

View file

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

View file

@ -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)])
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

View file

@ -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>