diff --git a/core/src/jsMain/kotlin/Database.kt b/core/src/jsMain/kotlin/Database.kt index 11630f2..80823da 100644 --- a/core/src/jsMain/kotlin/Database.kt +++ b/core/src/jsMain/kotlin/Database.kt @@ -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: @@ -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, @@ -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) { @@ -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" } @@ -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 } @@ -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() @@ -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" } + } } } diff --git a/core/src/jsMain/kotlin/Index.kt b/core/src/jsMain/kotlin/Index.kt index 3579cd1..039c2b3 100644 --- a/core/src/jsMain/kotlin/Index.kt +++ b/core/src/jsMain/kotlin/Index.kt @@ -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 = Request(index.get(key.toJs())) diff --git a/core/src/jsMain/kotlin/ObjectStore.kt b/core/src/jsMain/kotlin/ObjectStore.kt index 0fb5aa3..d1e5e6e 100644 --- a/core/src/jsMain/kotlin/ObjectStore.kt +++ b/core/src/jsMain/kotlin/ObjectStore.kt @@ -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 = Request(objectStore.get(key.toJs())) diff --git a/core/src/jsMain/kotlin/Queryable.kt b/core/src/jsMain/kotlin/Queryable.kt index 5130bb1..f9d1688 100644 --- a/core/src/jsMain/kotlin/Queryable.kt +++ b/core/src/jsMain/kotlin/Queryable.kt @@ -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 internal abstract fun requestGetAll(query: Key?): Request> internal abstract fun requestOpenCursor(query: Key?, direction: Cursor.Direction): Request diff --git a/core/src/jsMain/kotlin/Transaction.kt b/core/src/jsMain/kotlin/Transaction.kt index fa93116..8686e58 100644 --- a/core/src/jsMain/kotlin/Transaction.kt +++ b/core/src/jsMain/kotlin/Transaction.kt @@ -3,6 +3,9 @@ package com.juul.indexeddb import com.juul.indexeddb.external.IDBCursor import com.juul.indexeddb.external.IDBRequest import com.juul.indexeddb.external.IDBTransaction +import com.juul.indexeddb.logs.Logger +import com.juul.indexeddb.logs.NoOpLogger +import com.juul.indexeddb.logs.Type import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -11,10 +14,14 @@ import org.w3c.dom.events.Event public open class Transaction internal constructor( internal val transaction: IDBTransaction, + internal val logger: Logger, + internal val transactionId: Long, ) { + internal var operationId: Int = 0 - internal suspend fun awaitCompletion() { + internal suspend fun awaitCompletion(onComplete: ((Event) -> Unit)? = null) { transaction.onNextEvent("complete", "abort", "error") { event -> + onComplete?.invoke(event) when (event.type) { "abort" -> throw AbortTransactionException(event) "error" -> throw ErrorEventException(event) @@ -32,7 +39,9 @@ public open class Transaction internal constructor( } } } + internal fun abort() { + logger.log(Type.Transaction) { "Aborting transaction" } transaction.abort() } @@ -43,22 +52,17 @@ public open class Transaction internal constructor( } } - public fun objectStore(name: String): ObjectStore = - ObjectStore(transaction.objectStore(name)) - - public suspend fun Queryable.get(key: Key): dynamic { - val request = requestGet(key).request + internal suspend inline fun Queryable.request( + functionName: String, + crossinline makeRequest: () -> IDBRequest, + ): T { + val id = operationId++ + logger.log(Type.Query) { "$functionName request on $type `$name` (transaction $transactionId, operation $id)" } + val request = makeRequest() return request.onNextEvent("success", "error") { event -> - when (event.type) { - "error" -> throw ErrorEventException(event) - else -> request.result + logger.log(Type.Query, event) { + "$functionName response on $type `$name` (transaction $transactionId, operation $id)" } - } - } - - public suspend fun Queryable.getAll(query: Key? = null): Array { - val request = requestGetAll(query).request - return request.onNextEvent("success", "error") { event -> when (event.type) { "error" -> throw ErrorEventException(event) else -> request.result @@ -66,6 +70,15 @@ public open class Transaction internal constructor( } } + public fun objectStore(name: String): ObjectStore = + ObjectStore(transaction.objectStore(name)) + + public suspend fun Queryable.get(key: Key): dynamic = + request("get") { requestGet(key).request } + + public suspend fun Queryable.getAll(query: Key? = null): Array = + request("getAll") { requestGetAll(query).request } + @Deprecated( "In the future, `autoContinue` will be a required parameter.", ReplaceWith("openCursor(query, direction, cursorStart, autoContinue = true)"), @@ -96,12 +109,14 @@ public open class Transaction internal constructor( cursorStart: CursorStart? = null, autoContinue: Boolean, ): Flow = openCursorImpl( + "openCursor", query, direction, cursorStart, open = this::requestOpenCursor, wrap = ::CursorWithValue, autoContinue, + logger, ) @Deprecated( @@ -134,22 +149,51 @@ public open class Transaction internal constructor( cursorStart: CursorStart? = null, autoContinue: Boolean, ): Flow = openCursorImpl( + "openKeyCursor", query, direction, cursorStart, open = this::requestOpenKeyCursor, wrap = ::Cursor, autoContinue, + logger, ) - private suspend fun openCursorImpl( + /** + * Opens a key cursor, then immediately close it. This has the effect of being the minimally expensive query that + * still waits for the transaction to be available. + */ + internal suspend fun Queryable.awaitTransaction() { + openCursorImpl( + "openKeyCursor", + query = null, + direction = Cursor.Direction.Next, + cursorStart = null, + open = this::requestOpenKeyCursor, + wrap = ::Cursor, + autoContinue = false, + logger = NoOpLogger, + ).collect { + it.close() + } + // Since this function is an internal implementation detail, undo incrementing the operation id to avoid + // confusion where request 0 went. + operationId -= 1 + } + + private fun Queryable.openCursorImpl( + functionName: String, query: Key?, direction: Cursor.Direction, cursorStart: CursorStart?, open: (Key?, Cursor.Direction) -> Request, wrap: (U, SendChannel<*>) -> T, autoContinue: Boolean, + logger: Logger, ): Flow = callbackFlow { + val id = operationId++ + logger.log(Type.Cursor) { "$functionName request on $type `$name` (transaction $transactionId, operation $id)" } + var cursorStartAction = cursorStart val request = open(query, direction).request var finished = false @@ -160,6 +204,9 @@ public open class Transaction internal constructor( cursorStartAction?.apply(cursor) cursorStartAction = null } else if (cursor != null) { + logger.log(Type.Cursor, event) { + "Cursor value on $type `$name` (transaction $transactionId, operation $id)" + } val result = trySend(wrap(cursor, channel)) when { result.isSuccess -> if (autoContinue && !finished) cursor.`continue`() @@ -174,21 +221,15 @@ public open class Transaction internal constructor( request.addEventListener("success", onSuccess) request.addEventListener("error", onError) awaitClose { + logger.log(Type.Cursor) { "Cursor closed on $type `$name` (transaction $transactionId, operation $id)" } finished = true request.removeEventListener("success", onSuccess) request.removeEventListener("error", onError) } } - public suspend fun Queryable.count(query: Key? = null): Int { - val request = requestCount(query).request - return request.onNextEvent("success", "error") { event -> - when (event.type) { - "error" -> throw ErrorEventException(event) - else -> request.result - } - } - } + public suspend fun Queryable.count(query: Key? = null): Int = + request("count") { requestCount(query).request } public fun ObjectStore.index(name: String): Index = Index(objectStore.index(name)) @@ -196,7 +237,9 @@ public open class Transaction internal constructor( public open class WriteTransaction internal constructor( transaction: IDBTransaction, -) : Transaction(transaction) { + logger: Logger, + transactionId: Long, +) : Transaction(transaction, logger, transactionId) { /** * Adds a new item to the database using an in-line or auto-incrementing key. If an item with the same @@ -207,15 +250,8 @@ public open class WriteTransaction internal constructor( * Generally, you'll want to create an explicit `external interface` and pass that in, to guarantee that Kotlin * doesn't mangle, prefix, or otherwise mess with your field names. */ - public suspend fun ObjectStore.add(item: dynamic): dynamic { - val request = objectStore.add(item) - return request.onNextEvent("success", "error") { event -> - when (event.type) { - "error" -> throw ErrorEventException(event) - else -> request.result - } - } - } + public suspend fun ObjectStore.add(item: dynamic): dynamic = + request("add") { objectStore.add(item) } /** * Adds a new item to the database using an explicit out-of-line key. If an item with the same key already @@ -226,15 +262,8 @@ public open class WriteTransaction internal constructor( * Generally, you'll want to create an explicit `external interface` and pass that in, to guarantee that Kotlin * doesn't mangle, prefix, or otherwise mess with your field names. */ - public suspend fun ObjectStore.add(item: dynamic, key: Key): dynamic { - val request = objectStore.add(item, key.toJs()) - return request.onNextEvent("success", "error") { event -> - when (event.type) { - "error" -> throw ErrorEventException(event) - else -> request.result - } - } - } + public suspend fun ObjectStore.add(item: dynamic, key: Key): dynamic = + request("add") { objectStore.add(item, key.toJs()) } /** * Adds an item to or updates an item in the database using an in-line or auto-incrementing key. If an item @@ -246,15 +275,8 @@ public open class WriteTransaction internal constructor( * Generally, you'll want to create an explicit `external interface` and pass that in, to guarantee that Kotlin * doesn't mangle, prefix, or otherwise mess with your field names. */ - public suspend fun ObjectStore.put(item: dynamic): dynamic { - val request = objectStore.put(item) - return request.onNextEvent("success", "error") { event -> - when (event.type) { - "error" -> throw ErrorEventException(event) - else -> request.result - } - } - } + public suspend fun ObjectStore.put(item: dynamic): dynamic = + request("put") { objectStore.put(item) } /** * Adds an item to or updates an item in the database using an explicit out-of-line key. If an item with the @@ -265,34 +287,15 @@ public open class WriteTransaction internal constructor( * Generally, you'll want to create an explicit `external interface` and pass that in, to guarantee that Kotlin * doesn't mangle, prefix, or otherwise mess with your field names. */ - public suspend fun ObjectStore.put(item: dynamic, key: Key): dynamic { - val request = objectStore.put(item, key.toJs()) - return request.onNextEvent("success", "error") { event -> - when (event.type) { - "error" -> throw ErrorEventException(event) - else -> request.result - } - } - } + public suspend fun ObjectStore.put(item: dynamic, key: Key): dynamic = + request("put") { objectStore.put(item, key.toJs()) } public suspend fun ObjectStore.delete(key: Key) { - val request = objectStore.delete(key.toJs()) - request.onNextEvent("success", "error") { event -> - when (event.type) { - "error" -> throw ErrorEventException(event) - else -> Unit - } - } + request("delete") { objectStore.delete(key.toJs()) } } public suspend fun ObjectStore.clear() { - val request = objectStore.clear() - request.onNextEvent("success", "error") { event -> - when (event.type) { - "error" -> throw ErrorEventException(event) - else -> Unit - } - } + request("clear") { objectStore.clear() } } public suspend fun CursorWithValue.delete() { @@ -318,28 +321,40 @@ public open class WriteTransaction internal constructor( public class VersionChangeTransaction internal constructor( transaction: IDBTransaction, -) : WriteTransaction(transaction) { + logger: Logger, + transactionId: Long, +) : WriteTransaction(transaction, logger, transactionId) { /** Creates an object-store that uses explicit out-of-line keys. */ - public fun Database.createObjectStore(name: String): ObjectStore = - ObjectStore(ensureDatabase().createObjectStore(name)) + public fun Database.createObjectStore(name: String): ObjectStore { + logger.log(Type.Database) { "Creating object store: $name" } + return ObjectStore(ensureDatabase().createObjectStore(name)) + } /** Creates an object-store that uses in-line keys. */ - public fun Database.createObjectStore(name: String, keyPath: KeyPath): ObjectStore = - ObjectStore(ensureDatabase().createObjectStore(name, keyPath.toWrappedJs())) + public fun Database.createObjectStore(name: String, keyPath: KeyPath): ObjectStore { + logger.log(Type.Database) { "Creating object store: $name" } + return ObjectStore(ensureDatabase().createObjectStore(name, keyPath.toWrappedJs())) + } /** Creates an object-store that uses out-of-line keys with a key-generator. */ - public fun Database.createObjectStore(name: String, autoIncrement: AutoIncrement): ObjectStore = - ObjectStore(ensureDatabase().createObjectStore(name, autoIncrement.toJs())) + public fun Database.createObjectStore(name: String, autoIncrement: AutoIncrement): ObjectStore { + logger.log(Type.Database) { "Creating object store: $name" } + return ObjectStore(ensureDatabase().createObjectStore(name, autoIncrement.toJs())) + } public fun Database.deleteObjectStore(name: String) { + logger.log(Type.Database) { "Deleting object store: $name" } ensureDatabase().deleteObjectStore(name) } - public fun ObjectStore.createIndex(name: String, keyPath: KeyPath, unique: Boolean): Index = - Index(objectStore.createIndex(name, keyPath.toUnwrappedJs(), jso { this.unique = unique })) + public fun ObjectStore.createIndex(name: String, keyPath: KeyPath, unique: Boolean): Index { + logger.log(Type.Database) { "Creating index: $name" } + return Index(objectStore.createIndex(name, keyPath.toUnwrappedJs(), jso { this.unique = unique })) + } public fun ObjectStore.deleteIndex(name: String) { + logger.log(Type.Database) { "Deleting index: $name" } objectStore.deleteIndex(name) } } diff --git a/core/src/jsMain/kotlin/logs/EventAsMessageLogger.kt b/core/src/jsMain/kotlin/logs/EventAsMessageLogger.kt new file mode 100644 index 0000000..e0c0242 --- /dev/null +++ b/core/src/jsMain/kotlin/logs/EventAsMessageLogger.kt @@ -0,0 +1,37 @@ +package com.juul.indexeddb.logs + +import com.juul.indexeddb.external.IDBVersionChangeEvent +import org.w3c.dom.events.Event + +public fun Logger.embedEventsInMessages( + separator: String = "\n ", +): Logger = EventAsMessageLogger(separator, this) + +private class EventAsMessageLogger( + private val separator: String, + private val delegate: Logger, +) : Logger { + + override fun log(type: Type, event: Event?, message: () -> String) { + delegate.log(type, null) { + buildString { + append(message()) + if (event != null) { + for (line in propertyStrings(event)) { + append(separator) + append(line) + } + } + } + } + } + + private fun propertyStrings(event: Event): List = buildList { + // event.target explicitly excluded since it just prints stuff like [object IDBTransaction], not actually useful + add("event.type: ${event.type}") + if (event is IDBVersionChangeEvent) { + add("event.oldVersion: ${event.oldVersion}") + add("event.newVersion: ${event.newVersion}") + } + } +} diff --git a/core/src/jsMain/kotlin/logs/FilteringLogger.kt b/core/src/jsMain/kotlin/logs/FilteringLogger.kt new file mode 100644 index 0000000..fe9120d --- /dev/null +++ b/core/src/jsMain/kotlin/logs/FilteringLogger.kt @@ -0,0 +1,21 @@ +package com.juul.indexeddb.logs + +import org.w3c.dom.events.Event + +public fun Logger.filterTypes(vararg whitelist: Type): Logger = + filterTypes(whitelist.toSet()) + +public fun Logger.filterTypes(whitelist: Set): Logger = + FilteringLogger(whitelist, this) + +private class FilteringLogger( + val whitelist: Set, + val delegate: Logger, +) : Logger { + + override fun log(type: Type, event: Event?, message: () -> String) { + if (type in whitelist) { + delegate.log(type, event, message) + } + } +} diff --git a/core/src/jsMain/kotlin/logs/Logger.kt b/core/src/jsMain/kotlin/logs/Logger.kt new file mode 100644 index 0000000..54043dc --- /dev/null +++ b/core/src/jsMain/kotlin/logs/Logger.kt @@ -0,0 +1,7 @@ +package com.juul.indexeddb.logs + +import org.w3c.dom.events.Event + +public interface Logger { + public fun log(type: Type, event: Event? = null, message: () -> String) +} diff --git a/core/src/jsMain/kotlin/logs/NoOpLogger.kt b/core/src/jsMain/kotlin/logs/NoOpLogger.kt new file mode 100644 index 0000000..32da937 --- /dev/null +++ b/core/src/jsMain/kotlin/logs/NoOpLogger.kt @@ -0,0 +1,8 @@ +package com.juul.indexeddb.logs + +import org.w3c.dom.events.Event + +public object NoOpLogger : Logger { + + override fun log(type: Type, event: Event?, message: () -> String) {} +} diff --git a/core/src/jsMain/kotlin/logs/PrintLogger.kt b/core/src/jsMain/kotlin/logs/PrintLogger.kt new file mode 100644 index 0000000..79b3c69 --- /dev/null +++ b/core/src/jsMain/kotlin/logs/PrintLogger.kt @@ -0,0 +1,14 @@ +package com.juul.indexeddb.logs + +import org.w3c.dom.events.Event + +public object PrintLogger : Logger { + + override fun log(type: Type, event: Event?, message: () -> String) { + val msg = message() + when (event) { + null -> println("$type: $msg") + else -> println("$type (event=${event.type}): $msg") + } + } +} diff --git a/core/src/jsMain/kotlin/logs/Type.kt b/core/src/jsMain/kotlin/logs/Type.kt new file mode 100644 index 0000000..78ffe13 --- /dev/null +++ b/core/src/jsMain/kotlin/logs/Type.kt @@ -0,0 +1,8 @@ +package com.juul.indexeddb.logs + +public enum class Type { + Database, + Transaction, + Query, + Cursor, +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ed0ca58..d6cf9e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ kotlin = "2.0.21" [libraries] coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +khronicle-core = { module = "com.juul.khronicle:khronicle-core", version = "0.5.0" } [plugins] dokka = { id = "org.jetbrains.dokka", version = "1.9.20" } diff --git a/logging-khronicle/build.gradle.kts b/logging-khronicle/build.gradle.kts new file mode 100644 index 0000000..9dfb22e --- /dev/null +++ b/logging-khronicle/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + kotlin("multiplatform") + id("org.jmailen.kotlinter") + id("org.jetbrains.dokka") + id("com.vanniktech.maven.publish") +} + +kotlin { + explicitApi() + + js { + browser() + binaries.library() + } + + sourceSets { + val commonMain by getting { + dependencies { + api(project(":core")) + api(libs.khronicle.core) + } + } + } +} diff --git a/logging-khronicle/src/jsMain/kotlin/Event.kt b/logging-khronicle/src/jsMain/kotlin/Event.kt new file mode 100644 index 0000000..8702818 --- /dev/null +++ b/logging-khronicle/src/jsMain/kotlin/Event.kt @@ -0,0 +1,6 @@ +package com.juul.indexeddb.logs + +import com.juul.khronicle.Key +import org.w3c.dom.events.Event + +public object Event : Key diff --git a/logging-khronicle/src/jsMain/kotlin/KhronicleLogger.kt b/logging-khronicle/src/jsMain/kotlin/KhronicleLogger.kt new file mode 100644 index 0000000..c2046c6 --- /dev/null +++ b/logging-khronicle/src/jsMain/kotlin/KhronicleLogger.kt @@ -0,0 +1,25 @@ +package com.juul.indexeddb.logs + +import com.juul.khronicle.Log +import com.juul.khronicle.LogLevel +import org.w3c.dom.events.Event as JsEvent + +public object KhronicleLogger : Logger { + + override fun log(type: Type, event: JsEvent?, message: () -> String) { + val level = when (event?.type) { + "error", "blocked" -> LogLevel.Error + else -> when (type) { + Type.Database -> LogLevel.Info + Type.Transaction -> LogLevel.Debug + else -> LogLevel.Verbose + } + } + Log.dynamic(level = level, tag = "IndexedDB/$type") { metadata -> + if (event != null) { + metadata[Event] = event + } + message() + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 339019b..eccfbb7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,4 +11,5 @@ pluginManagement { include( "core", "external", + "logging-khronicle", )