Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logging Support #171

Merged
merged 9 commits into from
Dec 3, 2024
Merged
86 changes: 71 additions & 15 deletions core/src/jsMain/kotlin/Database.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import com.juul.indexeddb.external.IDBDatabase
import com.juul.indexeddb.external.IDBFactory
import com.juul.indexeddb.external.IDBVersionChangeEvent
import com.juul.indexeddb.external.indexedDB
import com.juul.indexeddb.logs.Logger
import com.juul.indexeddb.logs.NoOpLogger
import com.juul.indexeddb.logs.Type
import kotlinx.browser.window
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.w3c.dom.events.Event

/**
* Inside the [initialize] block, you must not call any `suspend` functions except for:
Expand All @@ -17,6 +21,7 @@ import kotlinx.coroutines.withContext
public suspend fun openDatabase(
name: String,
version: Int,
logger: Logger = NoOpLogger,
initialize: suspend VersionChangeTransaction.(
database: Database,
oldVersion: Int,
Expand All @@ -25,6 +30,7 @@ public suspend fun openDatabase(
): Database = withContext(Dispatchers.Unconfined) {
val indexedDB: IDBFactory? = js("self.indexedDB || self.webkitIndexedDB") as? IDBFactory
val factory = checkNotNull(indexedDB) { "Your browser doesn't support IndexedDB." }
logger.log(Type.Database) { "Opening database `$name` at version `$version`" }
val request = factory.open(name, version)
val versionChangeEvent = request.onNextEvent("success", "upgradeneeded", "error", "blocked") { event ->
when (event.type) {
Expand All @@ -34,36 +40,59 @@ public suspend fun openDatabase(
else -> null
}
}
Database(request.result).also { database ->
Database(request.result, logger).also { database ->
if (versionChangeEvent != null) {
val transaction = VersionChangeTransaction(checkNotNull(request.transaction))
logger.log(Type.Database, versionChangeEvent) {
"Upgrading database `$name` from version `${versionChangeEvent.oldVersion}` to `${versionChangeEvent.newVersion}`"
}
val id = database.transactionId++
logger.log(Type.Transaction) { "Opening versionchange transaction $id on database `$name`" }
val transaction = VersionChangeTransaction(checkNotNull(request.transaction), logger, id)
transaction.initialize(database, versionChangeEvent.oldVersion, versionChangeEvent.newVersion)
transaction.awaitCompletion()
transaction.awaitCompletion { event ->
logger.log(Type.Transaction, event) { "Closed versionchange transaction $id on database `$name`" }
}
}
logger.log(Type.Database) { "Opened database `$name`" }
}
}

public suspend fun deleteDatabase(name: String) {
public suspend fun deleteDatabase(
name: String,
logger: Logger = NoOpLogger,
) {
logger.log(Type.Database) { "Deleting database `$name`" }
val factory = checkNotNull(window.indexedDB) { "Your browser doesn't support IndexedDB." }
val request = factory.deleteDatabase(name)
request.onNextEvent("success", "error", "blocked") { event ->
when (event.type) {
"error", "blocked" -> throw ErrorEventException(event)
else -> null
"error", "blocked" -> {
logger.log(Type.Database, event) { "Delete failed for database `$name`" }
throw ErrorEventException(event)
}

else -> logger.log(Type.Database, event) { "Deleted database `$name`" }
}
}
}

public class Database internal constructor(
database: IDBDatabase,
private val logger: Logger,
) {
private val name = database.name
private var database: IDBDatabase? = database
internal var transactionId = 0L

init {
val callback = { event: Event ->
logger.log(Type.Database, event) { "Closing database `$name` due to event" }
tryClose()
}
// listen for database structure changes (e.g., upgradeneeded while DB is open or deleteDatabase)
database.addEventListener("versionchange", { close() })
database.addEventListener("versionchange", callback)
// listen for force close, e.g., browser profile on a USB drive that's ejected or db deleted through dev tools
database.addEventListener("close", { close() })
database.addEventListener("close", callback)
}

internal fun ensureDatabase(): IDBDatabase = checkNotNull(database) { "database is closed" }
Expand All @@ -79,12 +108,21 @@ public class Database internal constructor(
durability: Durability = Durability.Default,
action: suspend Transaction.() -> T,
): T = withContext(Dispatchers.Unconfined) {
val id = transactionId++
logger.log(Type.Transaction) {
"Opened readonly transaction $id using stores ${store.joinToString { "`$it`" }} on database `$name`"
}

val transaction = Transaction(
ensureDatabase().transaction(arrayOf(*store), "readonly", transactionOptions(durability)),
logger,
id,
)
val result = transaction.action()
transaction.commit()
transaction.awaitCompletion()
transaction.awaitCompletion { event ->
logger.log(Type.Transaction, event) { "Closed readonly transaction $id on database `$name`" }
}
result
}

Expand All @@ -99,19 +137,26 @@ public class Database internal constructor(
durability: Durability = Durability.Default,
action: suspend WriteTransaction.() -> T,
): T = withContext(Dispatchers.Unconfined) {
val id = transactionId++
logger.log(Type.Transaction) {
"Opening readwrite transaction $id using stores ${store.joinToString { "`$it`" }} on database `$name`"
}

val transaction = WriteTransaction(
ensureDatabase().transaction(arrayOf(*store), "readwrite", transactionOptions(durability)),
logger,
id,
)
with(transaction) {
// Force overlapping transactions to not call `action` until prior transactions complete.
objectStore(store.first())
.openKeyCursor(autoContinue = false)
.collect { it.close() }
objectStore(store.first()).awaitTransaction()
}
try {
val result = transaction.action()
transaction.commit()
transaction.awaitCompletion()
transaction.awaitCompletion { event ->
logger.log(Type.Transaction, event) { "Closed readwrite transaction $id on database `$name`" }
}
result
} catch (e: Throwable) {
transaction.abort()
Expand All @@ -121,8 +166,19 @@ public class Database internal constructor(
}

public fun close() {
database?.close()
database = null
logger.log(Type.Database) { "Closing database `$name` due to explicit `close()`" }
tryClose()
}

private fun tryClose() {
val db = database
if (db != null) {
db.close()
database = null
logger.log(Type.Database) { "Closed database `$name`" }
} else {
logger.log(Type.Database) { "Close skipped, database `$name` already closed" }
}
}
}

Expand Down
7 changes: 7 additions & 0 deletions core/src/jsMain/kotlin/Index.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import com.juul.indexeddb.external.IDBIndex
public class Index internal constructor(
internal val index: IDBIndex,
) : Queryable() {

override val type: String
get() = "index"

override val name: String
get() = index.name

override fun requestGet(key: Key): Request<dynamic> =
Request(index.get(key.toJs()))

Expand Down
7 changes: 7 additions & 0 deletions core/src/jsMain/kotlin/ObjectStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import com.juul.indexeddb.external.IDBObjectStore
public class ObjectStore internal constructor(
internal val objectStore: IDBObjectStore,
) : Queryable() {

override val type: String
get() = "object store"

override val name: String
get() = objectStore.name

override fun requestGet(key: Key): Request<dynamic> =
Request(objectStore.get(key.toJs()))

Expand Down
2 changes: 2 additions & 0 deletions core/src/jsMain/kotlin/Queryable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.juul.indexeddb.external.IDBCursor
import com.juul.indexeddb.external.IDBCursorWithValue

public sealed class Queryable {
internal abstract val type: String
internal abstract val name: String
internal abstract fun requestGet(key: Key): Request<dynamic>
internal abstract fun requestGetAll(query: Key?): Request<Array<dynamic>>
internal abstract fun requestOpenCursor(query: Key?, direction: Cursor.Direction): Request<IDBCursorWithValue?>
Expand Down
Loading