Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ buildscript {
classpath(libs.maven.publish.plugin)
classpath(libs.kover.plugin)
classpath(libs.atomic.fu.gradle.plugin)
classpath(libs.molecule.gradle.plugin)
}
}

Expand Down
2 changes: 2 additions & 0 deletions cache/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ kotlin {
val commonMain by getting {
dependencies {
api(libs.kotlinx.atomic.fu)
api(project(":core"))
implementation(libs.kotlinx.coroutines.core)
}
}
val jvmMain by getting
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
@file:Suppress("UNCHECKED_CAST")

package org.mobilenativefoundation.store.cache5

import org.mobilenativefoundation.store.core5.KeyProvider
import org.mobilenativefoundation.store.core5.StoreData
import org.mobilenativefoundation.store.core5.StoreKey

/**
* A class that represents a caching system with collection decomposition.
* Manages data with utility functions to get, invalidate, and add items to the cache.
* Depends on [StoreMultiCacheAccessor] for internal data management.
* @see [Cache].
*/
class StoreMultiCache<Id : Any, Key : StoreKey<Id>, Single : StoreData.Single<Id>, Collection : StoreData.Collection<Id, Single>, Output : StoreData<Id>>(
private val keyProvider: KeyProvider<Id, Single>,
singlesCache: Cache<StoreKey.Single<Id>, Single> = CacheBuilder<StoreKey.Single<Id>, Single>().build(),
collectionsCache: Cache<StoreKey.Collection<Id>, Collection> = CacheBuilder<StoreKey.Collection<Id>, Collection>().build(),
) : Cache<Key, Output> {

private val accessor = StoreMultiCacheAccessor(
singlesCache = singlesCache,
collectionsCache = collectionsCache,
)

private fun Key.castSingle() = this as StoreKey.Single<Id>
private fun Key.castCollection() = this as StoreKey.Collection<Id>

private fun StoreKey.Collection<Id>.cast() = this as Key
private fun StoreKey.Single<Id>.cast() = this as Key

override fun getIfPresent(key: Key): Output? {
return when (key) {
is StoreKey.Single<*> -> accessor.getSingle(key.castSingle()) as? Output
is StoreKey.Collection<*> -> accessor.getCollection(key.castCollection()) as? Output
else -> {
throw UnsupportedOperationException(invalidKeyErrorMessage(key))
}
}
}

override fun getOrPut(key: Key, valueProducer: () -> Output): Output {
return when (key) {
is StoreKey.Single<*> -> {
val single = accessor.getSingle(key.castSingle()) as? Output
if (single != null) {
single
} else {
val producedSingle = valueProducer()
put(key, producedSingle)
producedSingle
}
}

is StoreKey.Collection<*> -> {
val collection = accessor.getCollection(key.castCollection()) as? Output
if (collection != null) {
collection
} else {
val producedCollection = valueProducer()
put(key, producedCollection)
producedCollection
}
}

else -> {
throw UnsupportedOperationException(invalidKeyErrorMessage(key))
}
}
}

override fun getAllPresent(keys: List<*>): Map<Key, Output> {
val map = mutableMapOf<Key, Output>()
keys.filterIsInstance<StoreKey<Id>>().forEach { key ->
when (key) {
is StoreKey.Collection<Id> -> {
val collection = accessor.getCollection(key)
collection?.let { map[key.cast()] = it as Output }
}

is StoreKey.Single<Id> -> {
val single = accessor.getSingle(key)
single?.let { map[key.cast()] = it as Output }
}
}
}

return map
}

override fun invalidateAll(keys: List<Key>) {
keys.forEach { key -> invalidate(key) }
}

override fun invalidate(key: Key) {
when (key) {
is StoreKey.Single<*> -> accessor.invalidateSingle(key.castSingle())
is StoreKey.Collection<*> -> accessor.invalidateCollection(key.castCollection())
}
}

override fun putAll(map: Map<Key, Output>) {
map.entries.forEach { (key, value) -> put(key, value) }
}

override fun put(key: Key, value: Output) {
when (key) {
is StoreKey.Single<*> -> {
val single = value as Single
accessor.putSingle(key.castSingle(), single)

val collectionKey = keyProvider.fromSingle(key.castSingle(), single)
val existingCollection = accessor.getCollection(collectionKey)
if (existingCollection != null) {
val updatedItems = existingCollection.items.toMutableList().map {
if (it.id == single.id) {
single
} else {
it
}
}
val updatedCollection = existingCollection.copyWith(items = updatedItems) as Collection
accessor.putCollection(collectionKey, updatedCollection)
}
}

is StoreKey.Collection<*> -> {
val collection = value as Collection
accessor.putCollection(key.castCollection(), collection)

collection.items.forEach {
val single = it as? Single
if (single != null) {
accessor.putSingle(keyProvider.fromCollection(key.castCollection(), single), single)
}
}
}
}
}

override fun invalidateAll() {
accessor.invalidateAll()
}

override fun size(): Long {
return accessor.size()
}

companion object {
fun invalidKeyErrorMessage(key: Any) =
"Expected StoreKey.Single or StoreKey.Collection, but received ${key::class}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package org.mobilenativefoundation.store.cache5

import kotlinx.atomicfu.locks.SynchronizedObject
import kotlinx.atomicfu.locks.synchronized
import org.mobilenativefoundation.store.core5.StoreData
import org.mobilenativefoundation.store.core5.StoreKey

/**
* Responsible for managing and accessing cached data.
* Provides functionality to retrieve, store, and invalidate single items and collections of items.
* All operations are thread-safe, ensuring safe usage across multiple threads.
*
* The thread safety of this class is ensured through the use of synchronized blocks.
* Synchronized blocks guarantee only one thread can execute any of the methods at a time.
* This prevents concurrent modifications and ensures consistency of the data.
*
* @param Id The type of the identifier used for the data.
* @param Collection The type of the data collection.
* @param Single The type of the single data item.
* @property singlesCache The cache used to store single data items.
* @property collectionsCache The cache used to store collections of data items.
*/
class StoreMultiCacheAccessor<Id : Any, Collection : StoreData.Collection<Id, Single>, Single : StoreData.Single<Id>>(
private val singlesCache: Cache<StoreKey.Single<Id>, Single>,
private val collectionsCache: Cache<StoreKey.Collection<Id>, Collection>,
) : SynchronizedObject() {
private val keys = mutableSetOf<StoreKey<Id>>()

/**
* Retrieves a collection of items from the cache using the provided key.
*
* This operation is thread-safe.
*
* @param key The key used to retrieve the collection.
* @return The cached collection or null if it's not present.
*/
fun getCollection(key: StoreKey.Collection<Id>): Collection? = synchronized(this) {
collectionsCache.getIfPresent(key)
}

/**
* Retrieves an individual item from the cache using the provided key.
*
* This operation is thread-safe.
*
* @param key The key used to retrieve the single item.
* @return The cached single item or null if it's not present.
*/
fun getSingle(key: StoreKey.Single<Id>): Single? = synchronized(this) {
singlesCache.getIfPresent(key)
}

/**
* Stores a collection of items in the cache and updates the key set.
*
* This operation is thread-safe.
*
* @param key The key associated with the collection.
* @param collection The collection to be stored in the cache.
*/
fun putCollection(key: StoreKey.Collection<Id>, collection: Collection) = synchronized(this) {
collectionsCache.put(key, collection)
keys.add(key)
}

/**
* Stores an individual item in the cache and updates the key set.
*
* This operation is thread-safe.
*
* @param key The key associated with the single item.
* @param single The single item to be stored in the cache.
*/
fun putSingle(key: StoreKey.Single<Id>, single: Single) = synchronized(this) {
singlesCache.put(key, single)
keys.add(key)
}

/**
* Removes all cache entries and clears the key set.
*
* This operation is thread-safe.
*/
fun invalidateAll() = synchronized(this) {
collectionsCache.invalidateAll()
singlesCache.invalidateAll()
keys.clear()
}

/**
* Removes an individual item from the cache and updates the key set.
*
* This operation is thread-safe.
*
* @param key The key associated with the single item to be invalidated.
*/
fun invalidateSingle(key: StoreKey.Single<Id>) = synchronized(this) {
singlesCache.invalidate(key)
keys.remove(key)
}

/**
* Removes a collection of items from the cache and updates the key set.
*
* This operation is thread-safe.
*
* @param key The key associated with the collection to be invalidated.
*/
fun invalidateCollection(key: StoreKey.Collection<Id>) = synchronized(this) {
collectionsCache.invalidate(key)
keys.remove(key)
}

/**
* Calculates the total count of items in the cache, including both single items and items in collections.
*
* This operation is thread-safe.
*
* @return The total count of items in the cache.
*/
fun size(): Long = synchronized(this) {
var count = 0L
for (key in keys) {
when (key) {
is StoreKey.Single<Id> -> {
val single = singlesCache.getIfPresent(key)
if (single != null) {
count++
}
}

is StoreKey.Collection<Id> -> {
val collection = collectionsCache.getIfPresent(key)
if (collection != null) {
count += collection.items.size
}
}
}
}
count
}
}
Loading