diff --git a/kstore-file/src/commonMain/kotlin/io/github/xxfast/kstore/file/FileCodec.kt b/kstore-file/src/commonMain/kotlin/io/github/xxfast/kstore/file/FileCodec.kt index b5c0bfa..6049c0d 100644 --- a/kstore-file/src/commonMain/kotlin/io/github/xxfast/kstore/file/FileCodec.kt +++ b/kstore-file/src/commonMain/kotlin/io/github/xxfast/kstore/file/FileCodec.kt @@ -75,4 +75,8 @@ public class FileCodec( SystemFileSystem.atomicMove(source = tempFile, destination = file) } + + override fun id(): Any { + return this.file.toString() + this.serializer.descriptor.serialName + } } diff --git a/kstore-storage/src/commonMain/kotlin/io/github/xxfast/kstore/storage/StorageCodec.kt b/kstore-storage/src/commonMain/kotlin/io/github/xxfast/kstore/storage/StorageCodec.kt index 650df68..f217b14 100644 --- a/kstore-storage/src/commonMain/kotlin/io/github/xxfast/kstore/storage/StorageCodec.kt +++ b/kstore-storage/src/commonMain/kotlin/io/github/xxfast/kstore/storage/StorageCodec.kt @@ -31,4 +31,8 @@ public class StorageCodec( override suspend fun decode(): T? = storage[key] ?.let { format.decodeFromString(serializer, it) } + + override fun id(): Any { + return storage.toString() + serializer.descriptor.serialName + } } diff --git a/kstore/src/commonMain/kotlin/io/github/xxfast/kstore/Codec.kt b/kstore/src/commonMain/kotlin/io/github/xxfast/kstore/Codec.kt index 1a987bb..a0fc839 100644 --- a/kstore/src/commonMain/kotlin/io/github/xxfast/kstore/Codec.kt +++ b/kstore/src/commonMain/kotlin/io/github/xxfast/kstore/Codec.kt @@ -17,4 +17,10 @@ public interface Codec { * @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 } diff --git a/kstore/src/commonMain/kotlin/io/github/xxfast/kstore/KStore.kt b/kstore/src/commonMain/kotlin/io/github/xxfast/kstore/KStore.kt index 05bafe2..e304e0d 100644 --- a/kstore/src/commonMain/kotlin/io/github/xxfast/kstore/KStore.kt +++ b/kstore/src/commonMain/kotlin/io/github/xxfast/kstore/KStore.kt @@ -18,15 +18,22 @@ import kotlinx.serialization.Serializable * * @return store that contains a value of type [T] */ -public inline fun storeOf( +public fun storeOf( codec: Codec, default: T? = null, enableCache: Boolean = true, -): KStore = KStore( - default = default, - enableCache = enableCache, - codec = codec -) +): KStore { + return KStoreFactory.getOrCreate( + key = codec, + creator = { + KStore( + default = default, + enableCache = enableCache, + codec = codec + ) + } + ) +} /** * Creates a store with a custom encoder and a decoder @@ -109,5 +116,6 @@ public class KStore( override fun close() { if (lock.isLocked) lock.unlock() + KStoreFactory.release(key = codec) } } diff --git a/kstore/src/commonMain/kotlin/io/github/xxfast/kstore/KStoreFactory.kt b/kstore/src/commonMain/kotlin/io/github/xxfast/kstore/KStoreFactory.kt new file mode 100644 index 0000000..2de9ce1 --- /dev/null +++ b/kstore/src/commonMain/kotlin/io/github/xxfast/kstore/KStoreFactory.kt @@ -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> = mutableMapOf() + private val referenceCountMap: MutableMap = mutableMapOf() + + /** + * Get or create a `KStore` 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 getOrCreate( + key: Any, + creator: () -> KStore + ): KStore { + referenceCountMap[key] = referenceCountMap.getOrElse(key, { 0 }) + 1 + + val existing = storeMap[key] + if (existing != null) { + return existing as KStore + } + + 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 + } + } +} diff --git a/kstore/src/commonTest/kotlin/io/github/xxfast/kstore/KStoreTests.kt b/kstore/src/commonTest/kotlin/io/github/xxfast/kstore/KStoreTests.kt index 32c2f7c..2854275 100644 --- a/kstore/src/commonTest/kotlin/io/github/xxfast/kstore/KStoreTests.kt +++ b/kstore/src/commonTest/kotlin/io/github/xxfast/kstore/KStoreTests.kt @@ -12,7 +12,8 @@ import kotlin.test.assertEquals import kotlin.test.assertSame class KStoreTests { - private val store: KStore = storeOf(codec = TestCodec()) + private val codec = TestCodec() + private val store: KStore = storeOf(codec = codec) @AfterTest fun cleanup() { @@ -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)