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

Initial port for wasmJs #170

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
![badge][badge-js]
![badge][badge-wasmJs]
[![Slack](https://img.shields.io/badge/Slack-%23juul--libraries-ECB22E.svg?logo=data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNTQgNTQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMTkuNzEyLjEzM2E1LjM4MSA1LjM4MSAwIDAgMC01LjM3NiA1LjM4NyA1LjM4MSA1LjM4MSAwIDAgMCA1LjM3NiA1LjM4Nmg1LjM3NlY1LjUyQTUuMzgxIDUuMzgxIDAgMCAwIDE5LjcxMi4xMzNtMCAxNC4zNjVINS4zNzZBNS4zODEgNS4zODEgMCAwIDAgMCAxOS44ODRhNS4zODEgNS4zODEgMCAwIDAgNS4zNzYgNS4zODdoMTQuMzM2YTUuMzgxIDUuMzgxIDAgMCAwIDUuMzc2LTUuMzg3IDUuMzgxIDUuMzgxIDAgMCAwLTUuMzc2LTUuMzg2IiBmaWxsPSIjMzZDNUYwIi8+PHBhdGggZD0iTTUzLjc2IDE5Ljg4NGE1LjM4MSA1LjM4MSAwIDAgMC01LjM3Ni01LjM4NiA1LjM4MSA1LjM4MSAwIDAgMC01LjM3NiA1LjM4NnY1LjM4N2g1LjM3NmE1LjM4MSA1LjM4MSAwIDAgMCA1LjM3Ni01LjM4N20tMTQuMzM2IDBWNS41MkE1LjM4MSA1LjM4MSAwIDAgMCAzNC4wNDguMTMzYTUuMzgxIDUuMzgxIDAgMCAwLTUuMzc2IDUuMzg3djE0LjM2NGE1LjM4MSA1LjM4MSAwIDAgMCA1LjM3NiA1LjM4NyA1LjM4MSA1LjM4MSAwIDAgMCA1LjM3Ni01LjM4NyIgZmlsbD0iIzJFQjY3RCIvPjxwYXRoIGQ9Ik0zNC4wNDggNTRhNS4zODEgNS4zODEgMCAwIDAgNS4zNzYtNS4zODcgNS4zODEgNS4zODEgMCAwIDAtNS4zNzYtNS4zODZoLTUuMzc2djUuMzg2QTUuMzgxIDUuMzgxIDAgMCAwIDM0LjA0OCA1NG0wLTE0LjM2NWgxNC4zMzZhNS4zODEgNS4zODEgMCAwIDAgNS4zNzYtNS4zODYgNS4zODEgNS4zODEgMCAwIDAtNS4zNzYtNS4zODdIMzQuMDQ4YTUuMzgxIDUuMzgxIDAgMCAwLTUuMzc2IDUuMzg3IDUuMzgxIDUuMzgxIDAgMCAwIDUuMzc2IDUuMzg2IiBmaWxsPSIjRUNCMjJFIi8+PHBhdGggZD0iTTAgMzQuMjQ5YTUuMzgxIDUuMzgxIDAgMCAwIDUuMzc2IDUuMzg2IDUuMzgxIDUuMzgxIDAgMCAwIDUuMzc2LTUuMzg2di01LjM4N0g1LjM3NkE1LjM4MSA1LjM4MSAwIDAgMCAwIDM0LjI1bTE0LjMzNi0uMDAxdjE0LjM2NEE1LjM4MSA1LjM4MSAwIDAgMCAxOS43MTIgNTRhNS4zODEgNS4zODEgMCAwIDAgNS4zNzYtNS4zODdWMzQuMjVhNS4zODEgNS4zODEgMCAwIDAtNS4zNzYtNS4zODcgNS4zODEgNS4zODEgMCAwIDAtNS4zNzYgNS4zODciIGZpbGw9IiNFMDFFNUEiLz48L2c+PC9zdmc+&labelColor=611f69)](https://kotlinlang.slack.com/messages/juul-libraries/)

# Kotlin IndexedDB

A wrapper around [IndexedDB] which allows for access from Kotlin/JS code using `suspend` blocks and linear, non-callback based control flow.
A wrapper around [IndexedDB] which allows for access from Kotlin/JS or Kotlin/WasmJs code using `suspend` blocks and linear, non-callback based control flow.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: typically "Kotlin/Wasm" is used (without the "Js" attached).

Suggested change
A wrapper around [IndexedDB] which allows for access from Kotlin/JS or Kotlin/WasmJs code using `suspend` blocks and linear, non-callback based control flow.
A wrapper around [IndexedDB] which allows for access from Kotlin/JS or Kotlin/Wasm code using `suspend` blocks and linear, non-callback based control flow.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the original wording because it is more precise. The use of IndexedDB is tied to the browser and thus to the wasmJS target and not WASM in general.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did that to differentiate it from wasmWasi, although I guess the fact that it's a browser API should be enough to differentiate implicitly.


## Usage

Expand Down Expand Up @@ -76,7 +77,7 @@ a [`WriteTransaction`] sharing that store.

```kotlin
val bill = database.transaction("customers") {
objectStore("customers").get(Key("444-44-4444")) as Customer
objectStore("customers").get(IDBKey("444-44-4444")) as Customer
}
assertEquals("Bill", bill.name)
```
Expand Down Expand Up @@ -184,4 +185,5 @@ limitations under the License.
[IndexedDB]: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
[Using IndexedDB]: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
[//]: # (Images)
[badge-js]: http://img.shields.io/badge/platform-js-F8DB5D.svg?style=flat
[badge-js]: https://img.shields.io/badge/platform-js-F8DB5D.svg?style=flat
[badge-wasmJs]: https://img.shields.io/badge/platform-wasmJs-F8DB5D.svg?style=flat
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ plugins {
}

tasks.dokkaHtmlMultiModule.configure {
outputDirectory.set(buildDir.resolve("gh-pages"))
outputDirectory.set(layout.buildDirectory.dir("gh-pages"))
}

allprojects {
Expand Down
43 changes: 26 additions & 17 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask

plugins {
kotlin("multiplatform")
id("org.jmailen.kotlinter")
Expand All @@ -13,30 +16,36 @@ kotlin {
binaries.library()
}

@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.library()
}

sourceSets {
val commonMain by getting {
dependencies {
api(libs.coroutines.core)
}
commonMain.dependencies {
api(project(":external"))

api(libs.coroutines.core)
}

val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.coroutines.test)
}

val jsMain by getting {
dependencies {
implementation(project(":external"))
}
jsTest.dependencies {
implementation(kotlin("test-js"))
}

val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
wasmJsTest.dependencies {
implementation(kotlin("test-wasm-js"))
}
}
}

tasks.withType<KotlinCompilationTask<*>>().configureEach {
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package com.juul.indexeddb

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like quite a few files are missing their package directive now. Just gonna leave this one comment to cover all of them.

import com.juul.indexeddb.external.IDBCursor
import com.juul.indexeddb.external.IDBCursorWithValue
import com.juul.indexeddb.external.IDBKey
import com.juul.indexeddb.external.JsAny
import kotlinx.coroutines.channels.SendChannel

public open class Cursor internal constructor(
internal open val cursor: IDBCursor,
private val channel: SendChannel<*>,
) {
public val key: dynamic
public val key: IDBKey
get() = cursor.key

public val primaryKey: dynamic
public val primaryKey: IDBKey
get() = cursor.primaryKey

public fun close() {
Expand All @@ -26,12 +26,12 @@ public open class Cursor internal constructor(
cursor.advance(count)
}

public fun `continue`(key: Key) {
cursor.`continue`(key.toJs())
public fun `continue`(key: IDBKey) {
cursor.`continue`(key)
}

public fun continuePrimaryKey(key: Key, primaryKey: Key) {
cursor.continuePrimaryKey(key.toJs(), primaryKey.toJs())
public fun continuePrimaryKey(key: IDBKey, primaryKey: IDBKey) {
cursor.continuePrimaryKey(key, primaryKey)
}

public enum class Direction(
Expand All @@ -48,6 +48,6 @@ public class CursorWithValue internal constructor(
override val cursor: IDBCursorWithValue,
channel: SendChannel<*>,
) : Cursor(cursor, channel) {
public val value: dynamic
public val value: JsAny?
get() = cursor.value
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.juul.indexeddb

import com.juul.indexeddb.external.IDBCursor
import com.juul.indexeddb.external.IDBKey

public sealed class CursorStart {

Expand All @@ -15,19 +14,19 @@ public sealed class CursorStart {
}

public data class Continue(
val key: Key,
val key: IDBKey,
) : CursorStart() {
override fun apply(cursor: IDBCursor) {
cursor.`continue`(key.toJs())
cursor.`continue`(key)
}
}

public data class ContinuePrimaryKey(
val key: Key,
val primaryKey: Key,
val key: IDBKey,
val primaryKey: IDBKey,
) : CursorStart() {
override fun apply(cursor: IDBCursor) {
cursor.continuePrimaryKey(key.toJs(), primaryKey.toJs())
cursor.continuePrimaryKey(key, primaryKey)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.juul.indexeddb

import com.juul.indexeddb.external.IDBDatabase
import com.juul.indexeddb.external.IDBFactory
import com.juul.indexeddb.external.IDBTransactionDurability
import com.juul.indexeddb.external.IDBTransactionOptions
import com.juul.indexeddb.external.IDBVersionChangeEvent
import com.juul.indexeddb.external.indexedDB
import kotlinx.browser.window
import com.juul.indexeddb.external.window
import com.juul.indexeddb.selfIndexedDB
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

Expand All @@ -23,7 +24,7 @@ public suspend fun openDatabase(
newVersion: Int,
) -> Unit,
): Database = withContext(Dispatchers.Unconfined) {
val indexedDB: IDBFactory? = js("self.indexedDB || self.webkitIndexedDB") as? IDBFactory
val indexedDB: IDBFactory? = selfIndexedDB
val factory = checkNotNull(indexedDB) { "Your browser doesn't support IndexedDB." }
val request = factory.open(name, version)
val versionChangeEvent = request.onNextEvent("success", "upgradeneeded", "error", "blocked") { event ->
Expand All @@ -37,7 +38,11 @@ public suspend fun openDatabase(
Database(request.result).also { database ->
if (versionChangeEvent != null) {
val transaction = VersionChangeTransaction(checkNotNull(request.transaction))
transaction.initialize(database, versionChangeEvent.oldVersion, versionChangeEvent.newVersion)
transaction.initialize(
database,
versionChangeEvent.oldVersion,
versionChangeEvent.newVersion,
)
transaction.awaitCompletion()
}
}
Expand All @@ -61,9 +66,9 @@ public class Database internal constructor(

init {
// listen for database structure changes (e.g., upgradeneeded while DB is open or deleteDatabase)
database.addEventListener("versionchange", { close() })
database.addEventListener("versionchange") { close() }
// 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") { close() }
}

internal fun ensureDatabase(): IDBDatabase = checkNotNull(database) { "database is closed" }
Expand All @@ -75,13 +80,28 @@ public class Database internal constructor(
* - `suspend` functions composed entirely of other legal functions
*/
public suspend fun <T> transaction(
vararg store: String,
durability: Durability = Durability.Default,
store: String,
vararg moreStores: String,
durability: IDBTransactionDurability = IDBTransactionDurability.Default,
action: suspend Transaction.() -> T,
): T = withContext(Dispatchers.Unconfined) {
val transaction = Transaction(
ensureDatabase().transaction(arrayOf(*store), "readonly", transactionOptions(durability)),
)
val transaction = when {
moreStores.isEmpty() -> Transaction(
ensureDatabase().transaction(
storeName = store,
mode = "readonly",
options = IDBTransactionOptions(durability),
),
)

else -> Transaction(
ensureDatabase().transaction(
storeNames = toReadonlyArray(store, *moreStores),
mode = "readonly",
options = IDBTransactionOptions(durability),
),
)
}
val result = transaction.action()
transaction.awaitCompletion()
result
Expand All @@ -94,16 +114,34 @@ public class Database internal constructor(
* - `suspend` functions composed entirely of other legal functions
*/
public suspend fun <T> writeTransaction(
vararg store: String,
durability: Durability = Durability.Default,
store: String,
vararg moreStores: String,
durability: IDBTransactionDurability = IDBTransactionDurability.Default,
action: suspend WriteTransaction.() -> T,
): T = withContext(Dispatchers.Unconfined) {
val transaction = WriteTransaction(
ensureDatabase().transaction(arrayOf(*store), "readwrite", transactionOptions(durability)),
)
val transaction = when {
moreStores.isEmpty() -> WriteTransaction(
ensureDatabase()
.transaction(
storeName = store,
mode = "readwrite",
options = IDBTransactionOptions(durability),
),
)

else -> WriteTransaction(
ensureDatabase()
.transaction(
storeNames = toReadonlyArray(store, *moreStores),
mode = "readwrite",
options = IDBTransactionOptions(durability),
),
)
}

with(transaction) {
// Force overlapping transactions to not call `action` until prior transactions complete.
objectStore(store.first())
objectStore(store)
.openKeyCursor(autoContinue = false)
.collect { it.close() }
}
Expand All @@ -117,7 +155,3 @@ public class Database internal constructor(
database = null
}
}

private fun transactionOptions(durability: Durability): dynamic = jso {
this.durability = durability.jsValue
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
package com.juul.indexeddb

import org.w3c.dom.events.Event
import com.juul.indexeddb.external.Event

public abstract class EventException(
message: String?,
Expand Down
31 changes: 31 additions & 0 deletions core/src/commonMain/kotlin/Index.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import com.juul.indexeddb.external.IDBCursor
import com.juul.indexeddb.external.IDBCursorWithValue
import com.juul.indexeddb.external.IDBIndex
import com.juul.indexeddb.external.IDBKey
import com.juul.indexeddb.external.JsNumber
import com.juul.indexeddb.external.ReadonlyArray

public class Index internal constructor(
internal val index: IDBIndex,
) : Queryable() {
override fun requestGet(key: IDBKey): Request<*> =
Request(index.get(key))

override fun requestGetAll(query: IDBKey?): Request<ReadonlyArray<*>> =
Request(index.getAll(query))

override fun requestOpenCursor(
query: IDBKey?,
direction: Cursor.Direction,
): Request<IDBCursorWithValue?> =
Request(index.openCursor(query, direction.constant))

override fun requestOpenKeyCursor(
query: IDBKey?,
direction: Cursor.Direction,
): Request<IDBCursor?> =
Request(index.openKeyCursor(query, direction.constant))

override fun requestCount(query: IDBKey?): Request<JsNumber> =
Request(index.count(query))
}
Loading
Loading