diff --git a/kstore-file/src/commonMain/kotlin/io/github/xxfast/kstore/file/extensions/KVersionedStore.kt b/kstore-file/src/commonMain/kotlin/io/github/xxfast/kstore/file/extensions/KVersionedStore.kt index db3996d..49e2229 100644 --- a/kstore-file/src/commonMain/kotlin/io/github/xxfast/kstore/file/extensions/KVersionedStore.kt +++ b/kstore-file/src/commonMain/kotlin/io/github/xxfast/kstore/file/extensions/KVersionedStore.kt @@ -59,6 +59,8 @@ public class VersionedCodec( private val serializer: KSerializer, private val migration: Migration, private val versionPath: Path = Path("$file.version"), // TODO: Save to file metadata instead + private val tempPath: Path = Path("$file.temp"), + private val tempVersionPath: Path = Path("$versionPath.temp"), ) : Codec { override suspend fun decode(): T? = @@ -79,12 +81,22 @@ public class VersionedCodec( } override suspend fun encode(value: T?) { - if (value != null) { - SystemFileSystem.sink(versionPath).buffered().use { json.encode(Int.serializer(), version, it) } - SystemFileSystem.sink(file).buffered().use { json.encode(serializer, value, it) } - } else { + if (value == null) { SystemFileSystem.delete(versionPath, mustExist = false) SystemFileSystem.delete(file, mustExist = false) + return } + + try { + SystemFileSystem.sink(tempPath).buffered().use { json.encode(serializer, value, it) } + SystemFileSystem.sink(tempVersionPath).buffered().use { json.encode(Int.serializer(), version, it) } + } catch (e: Throwable) { + SystemFileSystem.delete(tempPath, mustExist = false) + SystemFileSystem.delete(tempVersionPath, mustExist = false) + throw e + } + + SystemFileSystem.atomicMove(source = tempPath, destination = file) + SystemFileSystem.atomicMove(source = tempVersionPath, destination = versionPath) } } diff --git a/kstore-file/src/commonTest/kotlin/io/github/xxfast/kstore/file/extensions/KVersionedStoreTests.kt b/kstore-file/src/commonTest/kotlin/io/github/xxfast/kstore/file/extensions/KVersionedStoreTests.kt index db5ef70..9387ef7 100644 --- a/kstore-file/src/commonTest/kotlin/io/github/xxfast/kstore/file/extensions/KVersionedStoreTests.kt +++ b/kstore-file/src/commonTest/kotlin/io/github/xxfast/kstore/file/extensions/KVersionedStoreTests.kt @@ -5,7 +5,10 @@ import io.github.xxfast.kstore.file.storeOf import kotlinx.coroutines.test.runTest import kotlinx.io.files.Path import kotlinx.io.files.SystemFileSystem +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive @@ -13,16 +16,35 @@ import kotlinx.serialization.json.long import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlin.test.assertFailsWith @Serializable data class CatV0(val name: String, val lives: Int = 9) @Serializable data class CatV1(val name: String, val lives: Int = 9, val cuteness: Int = 12) @Serializable data class CatV2(val name: String, val lives: Int = 9, val age: Int = 9 - lives, val kawaiiness: Long) @Serializable data class CatV3(val name: String, val lives: Int = 9, val age: Int = 9 - lives, val isCute: Boolean) +@Serializable +data class CatV41(val name: String, val friends: Map) { + object TodoSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Int", PrimitiveKind.INT) + override fun deserialize(decoder: Decoder): Int = TODO("Not yet implemented") + override fun serialize(encoder: Encoder, value: Int): Unit = TODO("Not yet implemented") + } +} + +@Serializable +data class CatV42(val name: String, val friends: Map) + + val MYLO_V0 = CatV0(name = "mylo", lives = 7) val MYLO_V1 = CatV1(name = "mylo", lives = 7, cuteness = 12) val MYLO_V2 = CatV2(name = "mylo", lives = 7, age = 2, kawaiiness = 12L) val MYLO_V3 = CatV3(name = "mylo", lives = 7, age = 2, isCute = true) +val MYLO_V41 = CatV41(name = "mylo", friends = mapOf("oreo" to 5, "kat" to 10)) +val MYLO_V42 = CatV42(name = "mylo", friends = mapOf("oreo" to 5, "kat" to 10)) class KVersionedStoreTests { private val file: Path = Path("test_migration.json") @@ -73,10 +95,15 @@ class KVersionedStoreTests { } } + private val storeV41: KStore = storeOf(file = file, version = 4) + private val storeV42: KStore = storeOf(file = file, version = 4) + @AfterTest fun cleanup() { SystemFileSystem.delete(file, mustExist = false) SystemFileSystem.delete(Path("${file.name}.version"), mustExist = false) + SystemFileSystem.delete(Path("${file.name}.temp"), mustExist = false) + SystemFileSystem.delete(Path("${file.name}.version.temp"), mustExist = false) } @Test @@ -127,4 +154,16 @@ class KVersionedStoreTests { val actual: CatV2? = storeV2.get() assertEquals(expect, actual) } + + @Test + fun testTransactionalEncode() = runTest { + assertFailsWith { storeV41.set(MYLO_V41) } + assertEquals(null, storeV41.get()) + + storeV42.set(MYLO_V42) + assertFailsWith { storeV41.set(MYLO_V41) } + + assertEquals(MYLO_V42, storeV42.get()) + assertFailsWith { storeV41.get() } + } }