From 7f77ebfa03a677f05fe312b4a5fc3c7fd508f500 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Tue, 5 Nov 2024 22:58:26 +0100 Subject: [PATCH 1/2] feature: add support for rollbacks --- core/src/jsMain/kotlin/Database.kt | 14 ++++- core/src/jsMain/kotlin/Transaction.kt | 25 ++++++++- core/src/jsTest/kotlin/TransactionTest.kt | 68 +++++++++++++++++++++++ 3 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 core/src/jsTest/kotlin/TransactionTest.kt diff --git a/core/src/jsMain/kotlin/Database.kt b/core/src/jsMain/kotlin/Database.kt index 9ede687..11630f2 100644 --- a/core/src/jsMain/kotlin/Database.kt +++ b/core/src/jsMain/kotlin/Database.kt @@ -83,6 +83,7 @@ public class Database internal constructor( ensureDatabase().transaction(arrayOf(*store), "readonly", transactionOptions(durability)), ) val result = transaction.action() + transaction.commit() transaction.awaitCompletion() result } @@ -107,9 +108,16 @@ public class Database internal constructor( .openKeyCursor(autoContinue = false) .collect { it.close() } } - val result = transaction.action() - transaction.awaitCompletion() - result + try { + val result = transaction.action() + transaction.commit() + transaction.awaitCompletion() + result + } catch (e: Throwable) { + transaction.abort() + transaction.awaitFailure() + throw e + } } public fun close() { diff --git a/core/src/jsMain/kotlin/Transaction.kt b/core/src/jsMain/kotlin/Transaction.kt index 8aee08a..5cab0ce 100644 --- a/core/src/jsMain/kotlin/Transaction.kt +++ b/core/src/jsMain/kotlin/Transaction.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import org.w3c.dom.events.Event +import kotlin.js.Json public open class Transaction internal constructor( internal val transaction: IDBTransaction, @@ -23,6 +24,26 @@ public open class Transaction internal constructor( } } + internal suspend fun awaitFailure() { + transaction.onNextEvent("complete", "abort", "error") { event -> + when (event.type) { + "abort" -> Unit + "error" -> Unit + else -> Unit + } + } + } + internal fun abort() { + transaction.abort() + } + + internal fun commit() { + // Check if function exists before calling it. + if (jsTypeOf(transaction.asDynamic().commit) === "function") { + transaction.commit() + } + } + public fun objectStore(name: String): ObjectStore = ObjectStore(transaction.objectStore(name)) @@ -132,6 +153,7 @@ public open class Transaction internal constructor( ): Flow = callbackFlow { var cursorStartAction = cursorStart val request = open(query, direction).request + var finished = false val onSuccess: (Event) -> Unit = { event -> @Suppress("UNCHECKED_CAST") val cursor = (event.target as IDBRequest).result @@ -141,7 +163,7 @@ public open class Transaction internal constructor( } else if (cursor != null) { val result = trySend(wrap(cursor, channel)) when { - result.isSuccess -> if (autoContinue) cursor.`continue`() + result.isSuccess -> if (autoContinue && !finished) cursor.`continue`() result.isFailure -> channel.close(IllegalStateException("Send failed. Did you suspend illegally?")) result.isClosed -> channel.close() } @@ -153,6 +175,7 @@ public open class Transaction internal constructor( request.addEventListener("success", onSuccess) request.addEventListener("error", onError) awaitClose { + finished = true request.removeEventListener("success", onSuccess) request.removeEventListener("error", onError) } diff --git a/core/src/jsTest/kotlin/TransactionTest.kt b/core/src/jsTest/kotlin/TransactionTest.kt new file mode 100644 index 0000000..b0411f4 --- /dev/null +++ b/core/src/jsTest/kotlin/TransactionTest.kt @@ -0,0 +1,68 @@ +package com.juul.indexeddb + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class TransactionTest { + @Test + fun readWithinTransaction() = runTest { + val database = openDatabase("read-within-transaction", 1) { database, oldVersion, newVersion -> + if (oldVersion < 1) { + database.createObjectStore("users", KeyPath("id")) + } + } + onCleanup { + database.close() + deleteDatabase("read-within-transaction") + } + + val user = database.writeTransaction("users") { + objectStore("users").add( + jso { + id = "7740f7c4-f889-498a-bc6d-f88dabdcfb9a" + username = "Username" + }, + ) + objectStore("users") + .get(Key("7740f7c4-f889-498a-bc6d-f88dabdcfb9a")) + } + + assertEquals("Username", user.username) + } + + @Test + fun whenExceptionIsThrowWithinTransaction_transactionIsAborted() = runTest { + val database = openDatabase("abort-transaction", 1) { database, oldVersion, newVersion -> + if (oldVersion < 1) { + database.createObjectStore("users", KeyPath("id")) + } + } + onCleanup { + database.close() + deleteDatabase("abort-transaction") + } + + assertFailsWith { + database.writeTransaction("users") { + objectStore("users").add( + jso { + id = "7740f7c4-f889-498a-bc6d-f88dabdcfb9a" + username = "Username" + }, + ) + + // Abort transaction + throw ExceptionToAbortTransaction() + } + } + + // because transaction is aborted, new values shouldn't be stored + val users = database.transaction("users") { + objectStore("users").getAll().toList() + } + + assertEquals(listOf(), users) + } +} +private class ExceptionToAbortTransaction : Exception() From 403e93fbe3f16c9ea676163acd5a6e245890c433 Mon Sep 17 00:00:00 2001 From: Travis Wyatt Date: Mon, 11 Nov 2024 10:25:34 -0800 Subject: [PATCH 2/2] `./gradlew formatKotlin` --- core/src/jsMain/kotlin/Transaction.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/jsMain/kotlin/Transaction.kt b/core/src/jsMain/kotlin/Transaction.kt index 5cab0ce..fa93116 100644 --- a/core/src/jsMain/kotlin/Transaction.kt +++ b/core/src/jsMain/kotlin/Transaction.kt @@ -8,7 +8,6 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import org.w3c.dom.events.Event -import kotlin.js.Json public open class Transaction internal constructor( internal val transaction: IDBTransaction,