Skip to content

Commit 7c2f77d

Browse files
committed
Initial port for wasmJs
1 parent a8bb7db commit 7c2f77d

39 files changed

+1439
-23
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
![badge][badge-js]
2+
![badge][badge-wasmJs]
23
[![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/)
34

45
# Kotlin IndexedDB
56

6-
A wrapper around [IndexedDB] which allows for access from Kotlin/JS code using `suspend` blocks and linear, non-callback based control flow.
7+
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.
78

89
## Usage
910

@@ -184,4 +185,5 @@ limitations under the License.
184185
[IndexedDB]: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
185186
[Using IndexedDB]: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
186187
[//]: # (Images)
187-
[badge-js]: http://img.shields.io/badge/platform-js-F8DB5D.svg?style=flat
188+
[badge-js]: https://img.shields.io/badge/platform-js-F8DB5D.svg?style=flat
189+
[badge-wasmJs]: https://img.shields.io/badge/platform-wasmJs-F8DB5D.svg?style=flat

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ plugins {
1313
}
1414

1515
tasks.dokkaHtmlMultiModule.configure {
16-
outputDirectory.set(buildDir.resolve("gh-pages"))
16+
outputDirectory.set(layout.buildDirectory.dir("gh-pages"))
1717
}
1818

1919
allprojects {

core/build.gradle.kts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
2+
13
plugins {
24
kotlin("multiplatform")
35
id("org.jmailen.kotlinter")
@@ -13,30 +15,36 @@ kotlin {
1315
binaries.library()
1416
}
1517

18+
@OptIn(ExperimentalWasmDsl::class)
19+
wasmJs {
20+
browser()
21+
binaries.library()
22+
}
23+
1624
sourceSets {
17-
val commonMain by getting {
18-
dependencies {
19-
api(libs.coroutines.core)
20-
}
25+
commonMain.dependencies {
26+
api(libs.coroutines.core)
27+
}
28+
29+
commonTest.dependencies {
30+
implementation(kotlin("test-common"))
31+
implementation(kotlin("test-annotations-common"))
32+
}
33+
34+
jsMain.dependencies {
35+
implementation(project(":external"))
2136
}
2237

23-
val commonTest by getting {
24-
dependencies {
25-
implementation(kotlin("test-common"))
26-
implementation(kotlin("test-annotations-common"))
27-
}
38+
jsTest.dependencies {
39+
implementation(kotlin("test-js"))
2840
}
2941

30-
val jsMain by getting {
31-
dependencies {
32-
implementation(project(":external"))
33-
}
42+
wasmJsMain.dependencies {
43+
implementation(project(":external"))
3444
}
3545

36-
val jsTest by getting {
37-
dependencies {
38-
implementation(kotlin("test-js"))
39-
}
46+
wasmJsTest.dependencies {
47+
implementation(kotlin("test-wasm-js"))
4048
}
4149
}
4250
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.juul.indexeddb
2+
3+
import com.juul.indexeddb.external.IDBCursor
4+
import com.juul.indexeddb.external.IDBCursorWithValue
5+
import kotlinx.coroutines.channels.SendChannel
6+
7+
public open class Cursor internal constructor(
8+
internal open val cursor: IDBCursor,
9+
private val channel: SendChannel<*>,
10+
) {
11+
public val key: JsAny
12+
get() = cursor.key
13+
14+
public val primaryKey: JsAny
15+
get() = cursor.primaryKey
16+
17+
public fun close() {
18+
channel.close()
19+
}
20+
21+
public fun `continue`() {
22+
cursor.`continue`()
23+
}
24+
25+
public fun advance(count: Int) {
26+
cursor.advance(count)
27+
}
28+
29+
public fun `continue`(key: Key) {
30+
cursor.`continue`(key.toJs())
31+
}
32+
33+
public fun continuePrimaryKey(key: Key, primaryKey: Key) {
34+
cursor.continuePrimaryKey(key.toJs(), primaryKey.toJs())
35+
}
36+
37+
public enum class Direction(
38+
internal val constant: String,
39+
) {
40+
Next("next"),
41+
NextUnique("nextunique"),
42+
Previous("prev"),
43+
PreviousUnique("prevunique"),
44+
}
45+
}
46+
47+
public class CursorWithValue internal constructor(
48+
override val cursor: IDBCursorWithValue,
49+
channel: SendChannel<*>,
50+
) : Cursor(cursor, channel) {
51+
public val value: JsAny?
52+
get() = cursor.value
53+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.juul.indexeddb
2+
3+
import com.juul.indexeddb.external.IDBCursor
4+
5+
public sealed class CursorStart {
6+
7+
internal abstract fun apply(cursor: IDBCursor)
8+
9+
public data class Advance(
10+
val count: Int,
11+
) : CursorStart() {
12+
override fun apply(cursor: IDBCursor) {
13+
cursor.advance(count)
14+
}
15+
}
16+
17+
public data class Continue(
18+
val key: Key,
19+
) : CursorStart() {
20+
override fun apply(cursor: IDBCursor) {
21+
cursor.`continue`(key.toJs())
22+
}
23+
}
24+
25+
public data class ContinuePrimaryKey(
26+
val key: Key,
27+
val primaryKey: Key,
28+
) : CursorStart() {
29+
override fun apply(cursor: IDBCursor) {
30+
cursor.continuePrimaryKey(key.toJs(), primaryKey.toJs())
31+
}
32+
}
33+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package com.juul.indexeddb
2+
3+
import com.juul.indexeddb.external.IDBDatabase
4+
import com.juul.indexeddb.external.IDBFactory
5+
import com.juul.indexeddb.external.IDBTransactionDurability
6+
import com.juul.indexeddb.external.IDBTransactionOptions
7+
import com.juul.indexeddb.external.IDBVersionChangeEvent
8+
import com.juul.indexeddb.external.indexedDB
9+
import kotlinx.browser.window
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.withContext
12+
13+
/**
14+
* Inside the [initialize] block, you must not call any `suspend` functions except for:
15+
* - those provided by this library and scoped on [Transaction] (and its subclasses)
16+
* - flow operations on the flows returns by [Transaction.openCursor] and [Transaction.openKeyCursor]
17+
* - `suspend` functions composed entirely of other legal functions
18+
*/
19+
public suspend fun openDatabase(
20+
name: String,
21+
version: Int,
22+
initialize: suspend VersionChangeTransaction.(
23+
database: Database,
24+
oldVersion: Int,
25+
newVersion: Int,
26+
) -> Unit,
27+
): Database = withContext(Dispatchers.Unconfined) {
28+
val indexedDB: IDBFactory? = selfIndexedDB
29+
val factory = checkNotNull(indexedDB) { "Your browser doesn't support IndexedDB." }
30+
val request = factory.open(name, version)
31+
val versionChangeEvent = request.onNextEvent("success", "upgradeneeded", "error", "blocked") { event ->
32+
when (event.type) {
33+
"upgradeneeded" -> event as IDBVersionChangeEvent
34+
"error" -> throw ErrorEventException(event)
35+
"blocked" -> throw OpenBlockedException(name, event)
36+
else -> null
37+
}
38+
}
39+
Database(request.result).also { database ->
40+
if (versionChangeEvent != null) {
41+
val transaction = VersionChangeTransaction(checkNotNull(request.transaction))
42+
transaction.initialize(
43+
database,
44+
versionChangeEvent.oldVersion,
45+
versionChangeEvent.newVersion,
46+
)
47+
transaction.awaitCompletion()
48+
}
49+
}
50+
}
51+
52+
public suspend fun deleteDatabase(name: String) {
53+
val factory = checkNotNull(window.indexedDB) { "Your browser doesn't support IndexedDB." }
54+
val request = factory.deleteDatabase(name)
55+
request.onNextEvent("success", "error", "blocked") { event ->
56+
when (event.type) {
57+
"error", "blocked" -> throw ErrorEventException(event)
58+
else -> null
59+
}
60+
}
61+
}
62+
63+
public class Database internal constructor(
64+
database: IDBDatabase,
65+
) {
66+
private var database: IDBDatabase? = database
67+
68+
init {
69+
// listen for database structure changes (e.g., upgradeneeded while DB is open or deleteDatabase)
70+
database.addEventListener("versionchange") { close() }
71+
// listen for force close, e.g., browser profile on a USB drive that's ejected or db deleted through dev tools
72+
database.addEventListener("close") { close() }
73+
}
74+
75+
internal fun ensureDatabase(): IDBDatabase = checkNotNull(database) { "database is closed" }
76+
77+
/**
78+
* Inside the [action] block, you must not call any `suspend` functions except for:
79+
* - those provided by this library and scoped on [Transaction] (and its subclasses)
80+
* - flow operations on the flows returns by [Transaction.openCursor] and [Transaction.openKeyCursor]
81+
* - `suspend` functions composed entirely of other legal functions
82+
*/
83+
public suspend fun <T> transaction(
84+
vararg store: String,
85+
durability: IDBTransactionDurability = IDBTransactionDurability.Default,
86+
action: suspend Transaction.() -> T,
87+
): T = withContext(Dispatchers.Unconfined) {
88+
check(store.isNotEmpty()) {
89+
"At least one store needs to be passed to transaction"
90+
}
91+
92+
val transaction = Transaction(
93+
ensureDatabase().transaction(
94+
storeNames = ReadonlyArray(
95+
*store.map { it.toJsString() }.toTypedArray(),
96+
),
97+
mode = "readonly",
98+
options = IDBTransactionOptions(durability),
99+
),
100+
)
101+
val result = transaction.action()
102+
transaction.awaitCompletion()
103+
result
104+
}
105+
106+
/**
107+
* Inside the [action] block, you must not call any `suspend` functions except for:
108+
* - those provided by this library and scoped on [Transaction] (and its subclasses)
109+
* - flow operations on the flows returns by [Transaction.openCursor] and [Transaction.openKeyCursor]
110+
* - `suspend` functions composed entirely of other legal functions
111+
*/
112+
public suspend fun <T> writeTransaction(
113+
vararg store: String,
114+
durability: IDBTransactionDurability = IDBTransactionDurability.Default,
115+
action: suspend WriteTransaction.() -> T,
116+
): T = withContext(Dispatchers.Unconfined) {
117+
check(store.isNotEmpty()) {
118+
"At least one store needs to be passed to writeTransaction"
119+
}
120+
121+
val transaction = WriteTransaction(
122+
ensureDatabase()
123+
.transaction(
124+
storeNames = ReadonlyArray(
125+
*store.map { it.toJsString() }.toTypedArray(),
126+
),
127+
mode = "readwrite",
128+
options = IDBTransactionOptions(durability),
129+
),
130+
)
131+
132+
with(transaction) {
133+
// Force overlapping transactions to not call `action` until prior transactions complete.
134+
objectStore(store.first())
135+
.openKeyCursor(autoContinue = false)
136+
.collect { it.close() }
137+
}
138+
val result = transaction.action()
139+
transaction.awaitCompletion()
140+
result
141+
}
142+
143+
public fun close() {
144+
database?.close()
145+
database = null
146+
}
147+
}
148+
149+
@Suppress("RedundantNullableReturnType")
150+
private val selfIndexedDB: IDBFactory? = js("self.indexedDB || self.webkitIndexedDB")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.juul.indexeddb
2+
3+
import org.w3c.dom.events.Event
4+
5+
public abstract class EventException(
6+
message: String?,
7+
cause: Throwable?,
8+
public val event: Event,
9+
) : Exception(message, cause)
10+
11+
public class EventHandlerException(
12+
cause: Throwable?,
13+
event: Event,
14+
) : EventException("An inner exception was thrown: $cause", cause, event)
15+
16+
public class ErrorEventException(
17+
event: Event,
18+
) : EventException("An error event was received.", cause = null, event)
19+
public class OpenBlockedException(
20+
public val name: String,
21+
event: Event,
22+
) : EventException("Resource in use: $name.", cause = null, event)
23+
public class AbortTransactionException(
24+
event: Event,
25+
) : EventException("Transaction aborted while waiting for completion.", cause = null, event)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.juul.indexeddb
2+
3+
import com.juul.indexeddb.external.IDBCursor
4+
import com.juul.indexeddb.external.IDBCursorWithValue
5+
import com.juul.indexeddb.external.IDBIndex
6+
import com.juul.indexeddb.external.ReadonlyArray
7+
8+
public class Index internal constructor(
9+
internal val index: IDBIndex,
10+
) : Queryable() {
11+
override fun requestGet(key: Key): Request<*> =
12+
Request(index.get(key.toJs()))
13+
14+
override fun requestGetAll(query: Key?): Request<ReadonlyArray<*>> =
15+
Request(index.getAll(query?.toJs()))
16+
17+
override fun requestOpenCursor(query: Key?, direction: Cursor.Direction): Request<IDBCursorWithValue?> =
18+
Request(index.openCursor(query?.toJs(), direction.constant))
19+
20+
override fun requestOpenKeyCursor(query: Key?, direction: Cursor.Direction): Request<IDBCursor?> =
21+
Request(index.openKeyCursor(query?.toJs(), direction.constant))
22+
23+
override fun requestCount(query: Key?): Request<JsNumber> =
24+
Request(index.count(query?.toJs()))
25+
}

0 commit comments

Comments
 (0)