Skip to content

Commit 8cc8edd

Browse files
authored
Support Paging (#550)
* Add Molecule and Paging libraries, update Kotlin version and enable Compose UIKit * Second stab * Clean up Signed-off-by: mramotar_dbx <mramotar@dropbox.com> * Remove StoreState and use StoreReadResponse Signed-off-by: mramotar_dbx <mramotar@dropbox.com> * Rename to launchPagingStore Signed-off-by: mramotar_dbx <mramotar@dropbox.com> * Remove log statements Signed-off-by: mramotar_dbx <mramotar@dropbox.com> * Rename to InsertionStrategy Signed-off-by: mramotar_dbx <mramotar@dropbox.com> * Move StoreKey and StoreData to core Signed-off-by: mramotar_dbx <mramotar@dropbox.com> * Make StoreMultiCacheAccessor thread safe Signed-off-by: mramotar_dbx <mramotar@dropbox.com> * Rename KeyProvider methods * Remove MultiCache and Identifiable * Add REPLACE InsertionStrategy * Add AndroidManifest.xml * Fix formatting errors * Add design_doc.md * Fix build.gradle.kts Signed-off-by: mramotar <mramotar@dropbox.com> * Update versions Signed-off-by: mramotar <mramotar@dropbox.com> --------- Signed-off-by: mramotar_dbx <mramotar@dropbox.com> Signed-off-by: mramotar <mramotar@dropbox.com>
1 parent 4f34a8b commit 8cc8edd

File tree

37 files changed

+1394
-267
lines changed

37 files changed

+1394
-267
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ buildscript {
2020
classpath(libs.maven.publish.plugin)
2121
classpath(libs.kover.plugin)
2222
classpath(libs.atomic.fu.gradle.plugin)
23+
classpath(libs.molecule.gradle.plugin)
2324
}
2425
}
2526

cache/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ kotlin {
4444
val commonMain by getting {
4545
dependencies {
4646
api(libs.kotlinx.atomic.fu)
47+
api(project(":core"))
48+
implementation(libs.kotlinx.coroutines.core)
4749
}
4850
}
4951
val jvmMain by getting

cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/Identifiable.kt

Lines changed: 0 additions & 5 deletions
This file was deleted.

cache/src/commonMain/kotlin/org/mobilenativefoundation/store/cache5/MultiCache.kt

Lines changed: 0 additions & 77 deletions
This file was deleted.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
@file:Suppress("UNCHECKED_CAST")
2+
3+
package org.mobilenativefoundation.store.cache5
4+
5+
import org.mobilenativefoundation.store.core5.KeyProvider
6+
import org.mobilenativefoundation.store.core5.StoreData
7+
import org.mobilenativefoundation.store.core5.StoreKey
8+
9+
/**
10+
* A class that represents a caching system with collection decomposition.
11+
* Manages data with utility functions to get, invalidate, and add items to the cache.
12+
* Depends on [StoreMultiCacheAccessor] for internal data management.
13+
* @see [Cache].
14+
*/
15+
class StoreMultiCache<Id : Any, Key : StoreKey<Id>, Single : StoreData.Single<Id>, Collection : StoreData.Collection<Id, Single>, Output : StoreData<Id>>(
16+
private val keyProvider: KeyProvider<Id, Single>,
17+
singlesCache: Cache<StoreKey.Single<Id>, Single> = CacheBuilder<StoreKey.Single<Id>, Single>().build(),
18+
collectionsCache: Cache<StoreKey.Collection<Id>, Collection> = CacheBuilder<StoreKey.Collection<Id>, Collection>().build(),
19+
) : Cache<Key, Output> {
20+
21+
private val accessor = StoreMultiCacheAccessor(
22+
singlesCache = singlesCache,
23+
collectionsCache = collectionsCache,
24+
)
25+
26+
private fun Key.castSingle() = this as StoreKey.Single<Id>
27+
private fun Key.castCollection() = this as StoreKey.Collection<Id>
28+
29+
private fun StoreKey.Collection<Id>.cast() = this as Key
30+
private fun StoreKey.Single<Id>.cast() = this as Key
31+
32+
override fun getIfPresent(key: Key): Output? {
33+
return when (key) {
34+
is StoreKey.Single<*> -> accessor.getSingle(key.castSingle()) as? Output
35+
is StoreKey.Collection<*> -> accessor.getCollection(key.castCollection()) as? Output
36+
else -> {
37+
throw UnsupportedOperationException(invalidKeyErrorMessage(key))
38+
}
39+
}
40+
}
41+
42+
override fun getOrPut(key: Key, valueProducer: () -> Output): Output {
43+
return when (key) {
44+
is StoreKey.Single<*> -> {
45+
val single = accessor.getSingle(key.castSingle()) as? Output
46+
if (single != null) {
47+
single
48+
} else {
49+
val producedSingle = valueProducer()
50+
put(key, producedSingle)
51+
producedSingle
52+
}
53+
}
54+
55+
is StoreKey.Collection<*> -> {
56+
val collection = accessor.getCollection(key.castCollection()) as? Output
57+
if (collection != null) {
58+
collection
59+
} else {
60+
val producedCollection = valueProducer()
61+
put(key, producedCollection)
62+
producedCollection
63+
}
64+
}
65+
66+
else -> {
67+
throw UnsupportedOperationException(invalidKeyErrorMessage(key))
68+
}
69+
}
70+
}
71+
72+
override fun getAllPresent(keys: List<*>): Map<Key, Output> {
73+
val map = mutableMapOf<Key, Output>()
74+
keys.filterIsInstance<StoreKey<Id>>().forEach { key ->
75+
when (key) {
76+
is StoreKey.Collection<Id> -> {
77+
val collection = accessor.getCollection(key)
78+
collection?.let { map[key.cast()] = it as Output }
79+
}
80+
81+
is StoreKey.Single<Id> -> {
82+
val single = accessor.getSingle(key)
83+
single?.let { map[key.cast()] = it as Output }
84+
}
85+
}
86+
}
87+
88+
return map
89+
}
90+
91+
override fun invalidateAll(keys: List<Key>) {
92+
keys.forEach { key -> invalidate(key) }
93+
}
94+
95+
override fun invalidate(key: Key) {
96+
when (key) {
97+
is StoreKey.Single<*> -> accessor.invalidateSingle(key.castSingle())
98+
is StoreKey.Collection<*> -> accessor.invalidateCollection(key.castCollection())
99+
}
100+
}
101+
102+
override fun putAll(map: Map<Key, Output>) {
103+
map.entries.forEach { (key, value) -> put(key, value) }
104+
}
105+
106+
override fun put(key: Key, value: Output) {
107+
when (key) {
108+
is StoreKey.Single<*> -> {
109+
val single = value as Single
110+
accessor.putSingle(key.castSingle(), single)
111+
112+
val collectionKey = keyProvider.fromSingle(key.castSingle(), single)
113+
val existingCollection = accessor.getCollection(collectionKey)
114+
if (existingCollection != null) {
115+
val updatedItems = existingCollection.items.toMutableList().map {
116+
if (it.id == single.id) {
117+
single
118+
} else {
119+
it
120+
}
121+
}
122+
val updatedCollection = existingCollection.copyWith(items = updatedItems) as Collection
123+
accessor.putCollection(collectionKey, updatedCollection)
124+
}
125+
}
126+
127+
is StoreKey.Collection<*> -> {
128+
val collection = value as Collection
129+
accessor.putCollection(key.castCollection(), collection)
130+
131+
collection.items.forEach {
132+
val single = it as? Single
133+
if (single != null) {
134+
accessor.putSingle(keyProvider.fromCollection(key.castCollection(), single), single)
135+
}
136+
}
137+
}
138+
}
139+
}
140+
141+
override fun invalidateAll() {
142+
accessor.invalidateAll()
143+
}
144+
145+
override fun size(): Long {
146+
return accessor.size()
147+
}
148+
149+
companion object {
150+
fun invalidKeyErrorMessage(key: Any) =
151+
"Expected StoreKey.Single or StoreKey.Collection, but received ${key::class}"
152+
}
153+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package org.mobilenativefoundation.store.cache5
2+
3+
import kotlinx.atomicfu.locks.SynchronizedObject
4+
import kotlinx.atomicfu.locks.synchronized
5+
import org.mobilenativefoundation.store.core5.StoreData
6+
import org.mobilenativefoundation.store.core5.StoreKey
7+
8+
/**
9+
* Responsible for managing and accessing cached data.
10+
* Provides functionality to retrieve, store, and invalidate single items and collections of items.
11+
* All operations are thread-safe, ensuring safe usage across multiple threads.
12+
*
13+
* The thread safety of this class is ensured through the use of synchronized blocks.
14+
* Synchronized blocks guarantee only one thread can execute any of the methods at a time.
15+
* This prevents concurrent modifications and ensures consistency of the data.
16+
*
17+
* @param Id The type of the identifier used for the data.
18+
* @param Collection The type of the data collection.
19+
* @param Single The type of the single data item.
20+
* @property singlesCache The cache used to store single data items.
21+
* @property collectionsCache The cache used to store collections of data items.
22+
*/
23+
class StoreMultiCacheAccessor<Id : Any, Collection : StoreData.Collection<Id, Single>, Single : StoreData.Single<Id>>(
24+
private val singlesCache: Cache<StoreKey.Single<Id>, Single>,
25+
private val collectionsCache: Cache<StoreKey.Collection<Id>, Collection>,
26+
) : SynchronizedObject() {
27+
private val keys = mutableSetOf<StoreKey<Id>>()
28+
29+
/**
30+
* Retrieves a collection of items from the cache using the provided key.
31+
*
32+
* This operation is thread-safe.
33+
*
34+
* @param key The key used to retrieve the collection.
35+
* @return The cached collection or null if it's not present.
36+
*/
37+
fun getCollection(key: StoreKey.Collection<Id>): Collection? = synchronized(this) {
38+
collectionsCache.getIfPresent(key)
39+
}
40+
41+
/**
42+
* Retrieves an individual item from the cache using the provided key.
43+
*
44+
* This operation is thread-safe.
45+
*
46+
* @param key The key used to retrieve the single item.
47+
* @return The cached single item or null if it's not present.
48+
*/
49+
fun getSingle(key: StoreKey.Single<Id>): Single? = synchronized(this) {
50+
singlesCache.getIfPresent(key)
51+
}
52+
53+
/**
54+
* Stores a collection of items in the cache and updates the key set.
55+
*
56+
* This operation is thread-safe.
57+
*
58+
* @param key The key associated with the collection.
59+
* @param collection The collection to be stored in the cache.
60+
*/
61+
fun putCollection(key: StoreKey.Collection<Id>, collection: Collection) = synchronized(this) {
62+
collectionsCache.put(key, collection)
63+
keys.add(key)
64+
}
65+
66+
/**
67+
* Stores an individual item in the cache and updates the key set.
68+
*
69+
* This operation is thread-safe.
70+
*
71+
* @param key The key associated with the single item.
72+
* @param single The single item to be stored in the cache.
73+
*/
74+
fun putSingle(key: StoreKey.Single<Id>, single: Single) = synchronized(this) {
75+
singlesCache.put(key, single)
76+
keys.add(key)
77+
}
78+
79+
/**
80+
* Removes all cache entries and clears the key set.
81+
*
82+
* This operation is thread-safe.
83+
*/
84+
fun invalidateAll() = synchronized(this) {
85+
collectionsCache.invalidateAll()
86+
singlesCache.invalidateAll()
87+
keys.clear()
88+
}
89+
90+
/**
91+
* Removes an individual item from the cache and updates the key set.
92+
*
93+
* This operation is thread-safe.
94+
*
95+
* @param key The key associated with the single item to be invalidated.
96+
*/
97+
fun invalidateSingle(key: StoreKey.Single<Id>) = synchronized(this) {
98+
singlesCache.invalidate(key)
99+
keys.remove(key)
100+
}
101+
102+
/**
103+
* Removes a collection of items from the cache and updates the key set.
104+
*
105+
* This operation is thread-safe.
106+
*
107+
* @param key The key associated with the collection to be invalidated.
108+
*/
109+
fun invalidateCollection(key: StoreKey.Collection<Id>) = synchronized(this) {
110+
collectionsCache.invalidate(key)
111+
keys.remove(key)
112+
}
113+
114+
/**
115+
* Calculates the total count of items in the cache, including both single items and items in collections.
116+
*
117+
* This operation is thread-safe.
118+
*
119+
* @return The total count of items in the cache.
120+
*/
121+
fun size(): Long = synchronized(this) {
122+
var count = 0L
123+
for (key in keys) {
124+
when (key) {
125+
is StoreKey.Single<Id> -> {
126+
val single = singlesCache.getIfPresent(key)
127+
if (single != null) {
128+
count++
129+
}
130+
}
131+
132+
is StoreKey.Collection<Id> -> {
133+
val collection = collectionsCache.getIfPresent(key)
134+
if (collection != null) {
135+
count += collection.items.size
136+
}
137+
}
138+
}
139+
}
140+
count
141+
}
142+
}

0 commit comments

Comments
 (0)