Skip to content

Commit fbcd34f

Browse files
authored
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>
1 parent c10f355 commit fbcd34f

File tree

21 files changed

+446
-81
lines changed

21 files changed

+446
-81
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## [Unreleased]
44

5+
## [5.0.0-beta01] (2023-05-19)
6+
7+
* Delegate memory cache implementation and provide a hybrid cache with automatic list decomposition as a separate
8+
artifact [#548](https://github.com/MobileNativeFoundation/Store/pull/548)
9+
510
## [5.0.0-alpha06] (2023-05-08)
611

712
* Separate MutableStoreBuilder from
@@ -255,7 +260,9 @@ This is a first alpha release of Store ported to RxJava 2.
255260
* The change log for Store version 1.x can be
256261
found [here](https://github.com/NYTimes/Store/blob/develop/CHANGELOG.md).
257262

258-
[Unreleased]: https://github.com/MobileNativeFoundation/Store/compare/v5.0.0-alpha06...HEAD
263+
[Unreleased]: https://github.com/MobileNativeFoundation/Store/compare/v5.0.0-beta01...HEAD
264+
265+
[5.0.0-beta01]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.0.0-beta01
259266

260267
[5.0.0-alpha06]: https://github.com/MobileNativeFoundation/Store/releases/tag/5.0.0-alpha06
261268

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
3838
#### Android
3939
```kotlin
40-
implementation "org.mobilenativefoundation.store:store5:5.0.0-alpha06"
40+
implementation "org.mobilenativefoundation.store:store5:5.0.0-beta01"
4141
implementation "org.jetbrains.kotlinx:atomicfu:0.18.5"
4242
```
4343

@@ -46,7 +46,7 @@ implementation "org.jetbrains.kotlinx:atomicfu:0.18.5"
4646
```kotlin
4747
commonMain {
4848
dependencies {
49-
implementation("org.mobilenativefoundation.store:store5:5.0.0-alpha06")
49+
implementation("org.mobilenativefoundation.store:store5:5.0.0-beta01")
5050
implementation("org.jetbrains.kotlinx:atomicfu:0.18.5")
5151
}
5252
}

buildSrc/src/main/kotlin/Version.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,6 @@ object Version {
3232
const val navigationCompose = "2.5.2"
3333
const val sqlDelight = "1.5.4"
3434
const val sqlDelightGradlePlugin = sqlDelight
35-
const val store = "5.0.0-alpha06"
35+
const val store = "5.0.0-beta01"
3636
const val truth = "1.1.3"
3737
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.mobilenativefoundation.store.cache5
2+
3+
interface Identifiable<Id : Any> {
4+
val id: Id
5+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
@file:Suppress("UNCHECKED_CAST")
2+
3+
package org.mobilenativefoundation.store.cache5
4+
5+
/**
6+
* Implementation of a cache with collection decomposition.
7+
* Stores and manages the relationship among single items and collections.
8+
* Delegates cache storage and behavior to Guava caches.
9+
*/
10+
class MultiCache<Key : Any, Output : Identifiable<Key>>(
11+
cacheBuilder: CacheBuilder<Key, Output>
12+
) {
13+
private val collectionCacheBuilder = CacheBuilder<Key, Collection<Output>>().apply {
14+
expireAfterAccess(cacheBuilder.expireAfterAccess)
15+
expireAfterWrite(cacheBuilder.expireAfterWrite)
16+
17+
if (cacheBuilder.maximumSize > 0) {
18+
maximumSize(cacheBuilder.maximumSize)
19+
}
20+
// TODO(): Support weigher
21+
}
22+
23+
private val itemKeyToCollectionKey = mutableMapOf<Key, Key>()
24+
25+
private val itemCache: Cache<Key, Output> = cacheBuilder.build()
26+
27+
private val collectionCache: Cache<Key, Collection<Output>> = collectionCacheBuilder.build()
28+
29+
fun getItem(key: Key): Output? {
30+
return itemCache.getIfPresent(key)
31+
}
32+
33+
fun putItem(key: Key, item: Output) {
34+
itemCache.put(key, item)
35+
36+
val collectionKey = itemKeyToCollectionKey[key]
37+
if (collectionKey != null) {
38+
val updatedCollection = collectionCache.getIfPresent(collectionKey)?.map { if (it.id == key) item else it }
39+
if (updatedCollection != null) {
40+
collectionCache.put(collectionKey, updatedCollection)
41+
}
42+
}
43+
}
44+
45+
fun <T : Collection<Output>> getCollection(key: Key): T? {
46+
return collectionCache.getIfPresent(key) as? T
47+
}
48+
49+
fun putCollection(key: Key, items: Collection<Output>) {
50+
collectionCache.put(key, items)
51+
items.forEach { item ->
52+
itemCache.put(item.id, item)
53+
itemKeyToCollectionKey[item.id] = key
54+
}
55+
}
56+
57+
fun invalidateItem(key: Key) {
58+
itemCache.invalidate(key)
59+
}
60+
61+
fun invalidateCollection(key: Key) {
62+
val collection = collectionCache.getIfPresent(key)
63+
collection?.forEach { item ->
64+
invalidateItem(item.id)
65+
}
66+
collectionCache.invalidate(key)
67+
}
68+
69+
fun invalidateAll() {
70+
collectionCache.invalidateAll()
71+
itemCache.invalidateAll()
72+
}
73+
74+
fun size(): Long {
75+
return itemCache.size() + collectionCache.size()
76+
}
77+
}

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ org.gradle.jvmargs=-XX:MaxMetaspaceSize=2G
88

99
# POM file
1010
GROUP=org.mobilenativefoundation.store
11-
VERSION_NAME=5.0.0-alpha06
11+
VERSION_NAME=5.0.0-beta01
1212
POM_PACKAGING=pom
1313
POM_DESCRIPTION = Store5 is a Kotlin Multiplatform network-resilient repository layer
1414

store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreBuilder.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
package org.mobilenativefoundation.store.store5
1717

1818
import kotlinx.coroutines.CoroutineScope
19+
import org.mobilenativefoundation.store.cache5.Cache
1920
import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcher
2021
import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcherAndSourceOfTruth
22+
import org.mobilenativefoundation.store.store5.impl.storeBuilderFromFetcherSourceOfTruthAndMemoryCache
2123

2224
/**
2325
* Main entry point for creating a [Store].
@@ -69,6 +71,17 @@ interface StoreBuilder<Key : Any, Output : Any> {
6971
fun <Key : Any, Input : Any, Output : Any> from(
7072
fetcher: Fetcher<Key, Input>,
7173
sourceOfTruth: SourceOfTruth<Key, Input>
72-
): StoreBuilder<Key, Output> = storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth)
74+
): StoreBuilder<Key, Output> =
75+
storeBuilderFromFetcherAndSourceOfTruth(fetcher = fetcher, sourceOfTruth = sourceOfTruth)
76+
77+
fun <Key : Any, Network : Any, Output : Any, Local : Any> from(
78+
fetcher: Fetcher<Key, Network>,
79+
sourceOfTruth: SourceOfTruth<Key, Local>,
80+
memoryCache: Cache<Key, Output>,
81+
): StoreBuilder<Key, Output> = storeBuilderFromFetcherSourceOfTruthAndMemoryCache(
82+
fetcher,
83+
sourceOfTruth,
84+
memoryCache
85+
)
7386
}
7487
}

store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/StoreReadRequest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ package org.mobilenativefoundation.store.store5
2323
* starting the stream from the local [com.dropbox.android.external.store4.impl.SourceOfTruth] and memory cache
2424
*
2525
*/
26-
data class StoreReadRequest<Key> private constructor(
26+
data class StoreReadRequest<out Key> private constructor(
2727
val key: Key,
2828
private val skippedCaches: Int,
2929
val refresh: Boolean = false,

store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStoreBuilder.kt

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package org.mobilenativefoundation.store.store5.impl
22

33
import kotlinx.coroutines.CoroutineScope
44
import kotlinx.coroutines.GlobalScope
5+
import org.mobilenativefoundation.store.cache5.Cache
6+
import org.mobilenativefoundation.store.cache5.CacheBuilder
57
import org.mobilenativefoundation.store.store5.Bookkeeper
68
import org.mobilenativefoundation.store.store5.Converter
79
import org.mobilenativefoundation.store.store5.Fetcher
@@ -24,9 +26,16 @@ fun <Key : Any, Network : Any, Output : Any, Local : Any> mutableStoreBuilderFro
2426
sourceOfTruth: SourceOfTruth<Key, Local>,
2527
): MutableStoreBuilder<Key, Network, Output, Local> = RealMutableStoreBuilder(fetcher, sourceOfTruth)
2628

29+
fun <Key : Any, Network : Any, Output : Any, Local : Any> mutableStoreBuilderFromFetcherSourceOfTruthAndMemoryCache(
30+
fetcher: Fetcher<Key, Network>,
31+
sourceOfTruth: SourceOfTruth<Key, Local>,
32+
memoryCache: Cache<Key, Output>
33+
): MutableStoreBuilder<Key, Network, Output, Local> = RealMutableStoreBuilder(fetcher, sourceOfTruth, memoryCache)
34+
2735
internal class RealMutableStoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any>(
2836
private val fetcher: Fetcher<Key, Network>,
2937
private val sourceOfTruth: SourceOfTruth<Key, Local>? = null,
38+
private val memoryCache: Cache<Key, Output>? = null
3039
) : MutableStoreBuilder<Key, Network, Output, Local> {
3140
private var scope: CoroutineScope? = null
3241
private var cachePolicy: MemoryPolicy<Key, Output>? = StoreDefaults.memoryPolicy
@@ -62,9 +71,25 @@ internal class RealMutableStoreBuilder<Key : Any, Network : Any, Output : Any, L
6271
scope = scope ?: GlobalScope,
6372
sourceOfTruth = sourceOfTruth,
6473
fetcher = fetcher,
65-
memoryPolicy = cachePolicy,
6674
converter = converter,
67-
validator = validator
75+
validator = validator,
76+
memCache = memoryCache ?: cachePolicy?.let {
77+
CacheBuilder<Key, Output>().apply {
78+
if (cachePolicy!!.hasAccessPolicy) {
79+
expireAfterAccess(cachePolicy!!.expireAfterAccess)
80+
}
81+
if (cachePolicy!!.hasWritePolicy) {
82+
expireAfterWrite(cachePolicy!!.expireAfterWrite)
83+
}
84+
if (cachePolicy!!.hasMaxSize) {
85+
maximumSize(cachePolicy!!.maxSize)
86+
}
87+
88+
if (cachePolicy!!.hasMaxWeight) {
89+
weigher(cachePolicy!!.maxWeight) { key, value -> cachePolicy!!.weigher.weigh(key, value) }
90+
}
91+
}.build()
92+
}
6893
)
6994

7095
override fun <UpdaterResult : Any> build(

store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStore.kt

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,11 @@ import kotlinx.coroutines.flow.map
2525
import kotlinx.coroutines.flow.onEach
2626
import kotlinx.coroutines.flow.onStart
2727
import kotlinx.coroutines.flow.transform
28-
import org.mobilenativefoundation.store.cache5.CacheBuilder
28+
import org.mobilenativefoundation.store.cache5.Cache
2929
import org.mobilenativefoundation.store.store5.CacheType
3030
import org.mobilenativefoundation.store.store5.Converter
3131
import org.mobilenativefoundation.store.store5.ExperimentalStoreApi
3232
import org.mobilenativefoundation.store.store5.Fetcher
33-
import org.mobilenativefoundation.store.store5.MemoryPolicy
3433
import org.mobilenativefoundation.store.store5.SourceOfTruth
3534
import org.mobilenativefoundation.store.store5.Store
3635
import org.mobilenativefoundation.store.store5.StoreReadRequest
@@ -47,7 +46,7 @@ internal class RealStore<Key : Any, Network : Any, Output : Any, Local : Any>(
4746
sourceOfTruth: SourceOfTruth<Key, Local>? = null,
4847
private val converter: Converter<Network, Output, Local>? = null,
4948
private val validator: Validator<Output>?,
50-
private val memoryPolicy: MemoryPolicy<Key, Output>?
49+
private val memCache: Cache<Key, Output>?
5150
) : Store<Key, Output> {
5251
/**
5352
* 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>(
6160
SourceOfTruthWithBarrier(it, converter)
6261
}
6362

64-
private val memCache = memoryPolicy?.let {
65-
CacheBuilder<Key, Output>().apply {
66-
if (memoryPolicy.hasAccessPolicy) {
67-
expireAfterAccess(memoryPolicy.expireAfterAccess)
68-
}
69-
if (memoryPolicy.hasWritePolicy) {
70-
expireAfterWrite(memoryPolicy.expireAfterWrite)
71-
}
72-
if (memoryPolicy.hasMaxSize) {
73-
maximumSize(memoryPolicy.maxSize)
74-
}
75-
76-
if (memoryPolicy.hasMaxWeight) {
77-
weigher(memoryPolicy.maxWeight) { key, value -> memoryPolicy.weigher.weigh(key, value) }
78-
}
79-
}.build()
80-
}
81-
8263
/**
8364
* Fetcher controller maintains 1 and only 1 `Multicaster` for a given key to ensure network
8465
* requests are shared.

store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealStoreBuilder.kt

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ package org.mobilenativefoundation.store.store5.impl
44

55
import kotlinx.coroutines.CoroutineScope
66
import kotlinx.coroutines.GlobalScope
7+
import org.mobilenativefoundation.store.cache5.Cache
8+
import org.mobilenativefoundation.store.cache5.CacheBuilder
79
import org.mobilenativefoundation.store.store5.Converter
810
import org.mobilenativefoundation.store.store5.Fetcher
911
import org.mobilenativefoundation.store.store5.MemoryPolicy
@@ -24,9 +26,16 @@ fun <Key : Any, Input : Any, Output : Any> storeBuilderFromFetcherAndSourceOfTru
2426
sourceOfTruth: SourceOfTruth<Key, *>,
2527
): StoreBuilder<Key, Output> = RealStoreBuilder(fetcher, sourceOfTruth)
2628

29+
fun <Key : Any, Network : Any, Output : Any, Local : Any> storeBuilderFromFetcherSourceOfTruthAndMemoryCache(
30+
fetcher: Fetcher<Key, Network>,
31+
sourceOfTruth: SourceOfTruth<Key, Local>,
32+
memoryCache: Cache<Key, Output>,
33+
): StoreBuilder<Key, Output> = RealStoreBuilder(fetcher, sourceOfTruth, memoryCache)
34+
2735
internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local : Any>(
2836
private val fetcher: Fetcher<Key, Network>,
29-
private val sourceOfTruth: SourceOfTruth<Key, Local>? = null
37+
private val sourceOfTruth: SourceOfTruth<Key, Local>? = null,
38+
private val memoryCache: Cache<Key, Output>? = null
3039
) : StoreBuilder<Key, Output> {
3140
private var scope: CoroutineScope? = null
3241
private var cachePolicy: MemoryPolicy<Key, Output>? = StoreDefaults.memoryPolicy
@@ -57,17 +66,42 @@ internal class RealStoreBuilder<Key : Any, Network : Any, Output : Any, Local :
5766
scope = scope ?: GlobalScope,
5867
sourceOfTruth = sourceOfTruth,
5968
fetcher = fetcher,
60-
memoryPolicy = cachePolicy,
6169
converter = converter,
62-
validator = validator
70+
validator = validator,
71+
memCache = memoryCache ?: cachePolicy?.let {
72+
CacheBuilder<Key, Output>().apply {
73+
if (cachePolicy!!.hasAccessPolicy) {
74+
expireAfterAccess(cachePolicy!!.expireAfterAccess)
75+
}
76+
if (cachePolicy!!.hasWritePolicy) {
77+
expireAfterWrite(cachePolicy!!.expireAfterWrite)
78+
}
79+
if (cachePolicy!!.hasMaxSize) {
80+
maximumSize(cachePolicy!!.maxSize)
81+
}
82+
83+
if (cachePolicy!!.hasMaxWeight) {
84+
weigher(cachePolicy!!.maxWeight) { key, value -> cachePolicy!!.weigher.weigh(key, value) }
85+
}
86+
}.build()
87+
}
6388
)
6489

6590
override fun <Network : Any, Local : Any> toMutableStoreBuilder(): MutableStoreBuilder<Key, Network, Output, Local> {
6691
fetcher as Fetcher<Key, Network>
67-
return if (sourceOfTruth == null) {
92+
return if (sourceOfTruth == null && memoryCache == null) {
6893
mutableStoreBuilderFromFetcher(fetcher)
94+
} else if (memoryCache == null) {
95+
mutableStoreBuilderFromFetcherAndSourceOfTruth<Key, Network, Output, Local>(
96+
fetcher,
97+
sourceOfTruth as SourceOfTruth<Key, Local>
98+
)
6999
} else {
70-
mutableStoreBuilderFromFetcherAndSourceOfTruth<Key, Network, Output, Local>(fetcher, sourceOfTruth as SourceOfTruth<Key, Local>)
100+
mutableStoreBuilderFromFetcherSourceOfTruthAndMemoryCache(
101+
fetcher,
102+
sourceOfTruth as SourceOfTruth<Key, Local>,
103+
memoryCache
104+
)
71105
}.apply {
72106
if (this@RealStoreBuilder.scope != null) {
73107
scope(this@RealStoreBuilder.scope!!)

0 commit comments

Comments
 (0)