Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,8 @@ public class FileCodec<T : @Serializable Any>(

SystemFileSystem.atomicMove(source = tempFile, destination = file)
}

override fun id(): Any {
return this.file.toString() + this.serializer.descriptor.serialName
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ public class StorageCodec<T : @Serializable Any>(

override suspend fun decode(): T? = storage[key]
?.let { format.decodeFromString(serializer, it) }

override fun id(): Any {
return storage.toString() + serializer.descriptor.serialName
}
}
6 changes: 6 additions & 0 deletions kstore/src/commonMain/kotlin/io/github/xxfast/kstore/Codec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@ public interface Codec<T: @Serializable Any> {
* @return optional value that is decoded
*/
public suspend fun decode(): T?

/**
* Tells the store factory whether to create a new store or reuse an existing one.
* @return the id of the codec.
*/
public fun id(): Any = this
}
20 changes: 14 additions & 6 deletions kstore/src/commonMain/kotlin/io/github/xxfast/kstore/KStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,22 @@ import kotlinx.serialization.Serializable
*
* @return store that contains a value of type [T]
*/
public inline fun <reified T : @Serializable Any> storeOf(
public fun <T : @Serializable Any> storeOf(
codec: Codec<T>,
default: T? = null,
enableCache: Boolean = true,
): KStore<T> = KStore(
default = default,
enableCache = enableCache,
codec = codec
)
): KStore<T> {
return KStoreFactory.getOrCreate(
key = codec,
creator = {
KStore(
default = default,
enableCache = enableCache,
codec = codec
)
}
)
}

/**
* Creates a store with a custom encoder and a decoder
Expand Down Expand Up @@ -109,5 +116,6 @@ public class KStore<T : @Serializable Any>(

override fun close() {
if (lock.isLocked) lock.unlock()
KStoreFactory.release(key = codec)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.github.xxfast.kstore

/**
* A factory for reusing and managing KStore instances keyed by an arbitrary identity (e.g., file path).
*/
internal object KStoreFactory {
private val storeMap: MutableMap<Any, KStore<*>> = mutableMapOf()
private val referenceCountMap: MutableMap<Any, Int> = mutableMapOf()

/**
* Get or create a `KStore<T>` for the given key.
* Ensures that only one instance exists per key.
*
* @param key Unique identity to associate the store with (e.g., file path or custom string).
* @param creator Lambda to create a new store if one doesn't exist for the key.
*/
@Suppress("UNCHECKED_CAST")
internal fun <T : Any> getOrCreate(
key: Any,
creator: () -> KStore<T>
): KStore<T> {
referenceCountMap[key] = referenceCountMap.getOrElse(key, { 0 }) + 1

val existing = storeMap[key]
if (existing != null) {
return existing as KStore<T>
}

val created = creator()
storeMap[key] = created
return created
}

/**
* Explicitly remove a store instance for a key (e.g., to free memory).
*/
internal fun release(key: Any) {
val referenceCount = referenceCountMap.getOrElse(key, { 0 })
if (referenceCount <= 1) {
storeMap.remove(key)
referenceCountMap.remove(key)
} else {
referenceCountMap[key] = referenceCount - 1
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import kotlin.test.assertEquals
import kotlin.test.assertSame

class KStoreTests {
private val store: KStore<Cat> = storeOf(codec = TestCodec())
private val codec = TestCodec<Cat>()
private val store: KStore<Cat> = storeOf(codec = codec)

@AfterTest
fun cleanup() {
Expand Down Expand Up @@ -107,6 +108,26 @@ class KStoreTests {
}
}

@Test
fun testUpdatesWithMultipleInstances() = runTest {
val store2 = storeOf(codec = codec)
store.updates.test {
assertEquals(null, awaitItem())
store2.set(MYLO)
assertEquals(MYLO, awaitItem())
store2.set(OREO)
assertEquals(OREO, awaitItem())
}

store2.updates.test {
assertEquals(OREO, awaitItem())
store.set(MYLO)
assertEquals(MYLO, awaitItem())
store.set(OREO)
assertEquals(OREO, awaitItem())
}
}

@Test
fun testDelete() = runTest {
store.set(MYLO)
Expand Down