Skip to content

Commit

Permalink
Logging Support (#171)
Browse files Browse the repository at this point in the history
  • Loading branch information
cedrickcooke authored Dec 3, 2024
1 parent 22f4bed commit 936706c
Show file tree
Hide file tree
Showing 16 changed files with 339 additions and 100 deletions.
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

0 comments on commit 936706c

Please sign in to comment.