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..d36a95e 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 (transaction.unsafeCast()["commit"] != undefined) { + transaction.commit() + } + } + public fun objectStore(name: String): ObjectStore = ObjectStore(transaction.objectStore(name)) 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()