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

Support wasmJs target #4965

Open
wants to merge 11 commits into
base: master
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,7 @@ docs/upgrading.md
docs/contributing.md

# Jenv local setting
.java-version
.java-version

# Kotlin multiplatform caches
.kotlin
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,52 @@ package app.cash.sqldelight.multiplatform

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
import org.jetbrains.kotlin.konan.target.HostManager

class MultiplatformConventions : Plugin<Project> {
override fun apply(project: Project) {
project.plugins.apply("org.jetbrains.kotlin.multiplatform")

(project.kotlinExtension as KotlinMultiplatformExtension).apply {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
this.freeCompilerArgs.addAll(
"-Xexpect-actual-classes",
)
}

jvm()

js {
browser {
@OptIn(ExperimentalWasmDsl::class)
listOf(js(), wasmJs()).forEach {
it.browser {
testTask {
it.useKarma {
useChromeHeadless()
}
}
}
compilations.configureEach {
it.compilations.configureEach {
it.kotlinOptions {
moduleKind = "umd"
}
}
}

@OptIn(ExperimentalKotlinGradlePluginApi::class)
applyDefaultHierarchyTemplate {
common {
group("jsCommon") {
withJs()
withWasm()
}
}
}

// tier 1
linuxX64()
macosX64()
Expand Down
3 changes: 3 additions & 0 deletions drivers/driver-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ kotlin {
js {
browser()
}
wasmJs {
browser()
}

// same targets as in `native-driver`
iosX64()
Expand Down
51 changes: 26 additions & 25 deletions drivers/native-driver/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.NativeOutputKind
import org.jetbrains.kotlin.konan.target.HostManager

plugins {
alias(libs.plugins.kotlin.multiplatform)
Expand Down Expand Up @@ -35,44 +36,44 @@ kotlin {
mingwX64()
watchosDeviceArm64()

targetHierarchy.default { target ->
target.group("native") {
applyDefaultHierarchyTemplate {
it.group("native") {
it.group("nativeLinuxLike") {
it.withLinux()
it.withApple()
// see https://youtrack.jetbrains.com/issue/KT-64483
it.group("linux") {}
it.group("apple") {}
// https://github.com/touchlab/SQLiter/issues/117
// it.withAndroidNative()
// withAndroidNative()
}
}
}

sourceSets {
commonMain {
dependencies {
api projects.runtime
api libs.stately.concurrency
api libs.sqliter
}
commonMain.dependencies {
api projects.runtime
api libs.stately.concurrency
api libs.sqliter
}
commonTest {
dependencies {
implementation libs.kotlin.test
implementation libs.testhelp
implementation projects.drivers.driverTest
}

commonTest.dependencies {
implementation libs.kotlin.test
implementation libs.testhelp
implementation projects.drivers.driverTest
}
}

configure([targets.iosX64, targets.iosArm64, targets.tvosX64, targets.tvosArm64, targets.watchosX64, targets.watchosArm32, targets.watchosArm64, targets.macosX64, targets.macosArm64, targets.iosSimulatorArm64, targets.watchosSimulatorArm64, targets.tvosSimulatorArm64]) {
binaries.configureEach {
// we only need to link sqlite for the test binaries
if (outputKind == NativeOutputKind.TEST) {
linkerOpts += ["-lsqlite3"]
targets.configureEach {
if (it instanceof KotlinNativeTarget && it.konanTarget.family.appleFamily) {
it.binaries.configureEach {
// we only need to link sqlite for the test binaries
if (outputKind == NativeOutputKind.TEST) {
linkerOpts += ["-lsqlite3"]
}
}
}
}

configure([targets.linuxX64]) {
linuxX64 {
compilations.configureEach {
if (name == "test") {
cinterops {
Expand All @@ -86,7 +87,7 @@ kotlin {
}
}

configure([targets.mingwX64]) {
mingwX64 {
binaries.configureEach {
// we only need to link sqlite for the test binaries
if (outputKind == NativeOutputKind.TEST) {
Expand Down
44 changes: 27 additions & 17 deletions drivers/web-worker-driver/build.gradle
Original file line number Diff line number Diff line change
@@ -1,43 +1,53 @@
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation

plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.publish)
alias(libs.plugins.dokka)
}

kotlin {
js {
browser {
[js(), wasmJs()].forEach {
it.browser {
testTask {
useKarma {
useChromeHeadless()
}
}
}
}
applyDefaultHierarchyTemplate {
it.common {
it.withJs()
it.withWasm()
}
}

sourceSets {
jsMain {
dependencies {
api projects.runtime
implementation libs.kotlin.coroutines.core
}
commonMain.dependencies {
api projects.runtime
implementation libs.kotlin.coroutines.core
}

jsTest {
dependencies {
implementation libs.kotlin.test.js
implementation npm("sql.js", libs.versions.sqljs.get())
implementation npm("@cashapp/sqldelight-sqljs-worker", project(":drivers:web-worker-driver:sqljs").projectDir)
implementation devNpm("copy-webpack-plugin", "9.1.0")
implementation libs.kotlin.coroutines.test
implementation project(":extensions:async-extensions")
}
commonTest.dependencies {
implementation libs.kotlin.test
implementation npm("sql.js", libs.versions.sqljs.get())
implementation npm("@cashapp/sqldelight-sqljs-worker", project(":drivers:web-worker-driver:sqljs").projectDir)
implementation devNpm("copy-webpack-plugin", "9.1.0")
implementation libs.kotlin.coroutines.test
implementation project(":extensions:async-extensions")
}
}
}

tasks.withType(KotlinCompilation.class).configureEach {
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}

apply from: "$rootDir/gradle/gradle-mvn-push.gradle"

tasks.named("dokkaHtmlMultiModule") {
dependsOn(rootProject.tasks.named("dokkaHtmlMultiModule"))
dependsOn(rootProject.tasks.named("dokkaHtmlMultiModule"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package app.cash.sqldelight.driver.worker

import app.cash.sqldelight.db.SqlDriver

expect fun createDefaultWebWorkerDriver(): SqlDriver
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package app.cash.sqldelight.driver.worker

import app.cash.sqldelight.Query
import app.cash.sqldelight.Transacter
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlCursor
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.db.SqlPreparedStatement
import app.cash.sqldelight.driver.worker.api.WorkerAction
import app.cash.sqldelight.driver.worker.api.WorkerActions
import app.cash.sqldelight.driver.worker.api.WorkerResultWithRowCount
import app.cash.sqldelight.driver.worker.api.WorkerWrapperRequest
import app.cash.sqldelight.driver.worker.expected.JsWorkerSqlCursor
import app.cash.sqldelight.driver.worker.expected.JsWorkerSqlPreparedStatement
import app.cash.sqldelight.driver.worker.expected.Worker
import app.cash.sqldelight.driver.worker.expected.checkWorkerResults

/**
* A [SqlDriver] implementation for interacting with SQL databases running in a Web Worker.
*
* This driver is dialect-agnostic and is instead dependent on the Worker script's implementation
* to handle queries and send results back from the Worker.
*
* @property worker The Worker running a SQL implementation that this driver communicates with.
* @see [WebWorkerDriver.fromScriptUrl]
*/
class WebWorkerDriver(private val worker: Worker) : SqlDriver {
private val listeners = mutableMapOf<String, MutableSet<Query.Listener>>()
private var messageCounter = 0
private var transaction: Transacter.Transaction? = null
private val wrapper = WorkerWrapper(worker)

override fun <R> executeQuery(
identifier: Int?,
sql: String,
mapper: (SqlCursor) -> QueryResult<R>,
parameters: Int,
binders: (SqlPreparedStatement.() -> Unit)?,
): QueryResult<R> {
val bound = JsWorkerSqlPreparedStatement()
binders?.invoke(bound)

return QueryResult.AsyncValue {
val response = wrapper.sendMessage(
action = WorkerActions.exec,
sql = sql,
statement = bound,
)

return@AsyncValue mapper(JsWorkerSqlCursor(checkWorkerResults(response.result))).await()
}
}

override fun execute(
identifier: Int?,
sql: String,
parameters: Int,
binders: (SqlPreparedStatement.() -> Unit)?,
): QueryResult<Long> {
val bound = JsWorkerSqlPreparedStatement()
binders?.invoke(bound)

return QueryResult.AsyncValue {
val response = wrapper.sendMessage(
action = WorkerActions.exec,
sql = sql,
statement = bound,
)
checkWorkerResults(response.result)
return@AsyncValue response.rowCount
}
}

override fun addListener(vararg queryKeys: String, listener: Query.Listener) {
queryKeys.forEach {
listeners.getOrPut(it) { mutableSetOf() }.add(listener)
}
}

override fun removeListener(vararg queryKeys: String, listener: Query.Listener) {
queryKeys.forEach {
listeners[it]?.remove(listener)
}
}

override fun notifyListeners(vararg queryKeys: String) {
queryKeys.flatMap { listeners[it].orEmpty() }
.distinct()
.forEach(Query.Listener::queryResultsChanged)
}

override fun close() = wrapper.terminate()

override fun newTransaction(): QueryResult<Transacter.Transaction> = QueryResult.AsyncValue {
val enclosing = transaction
val transaction = Transaction(enclosing)
this.transaction = transaction
if (enclosing == null) {
wrapper.sendMessage(WorkerActions.beginTransaction)
}

return@AsyncValue transaction
}

override fun currentTransaction(): Transacter.Transaction? = transaction

private inner class Transaction(
override val enclosingTransaction: Transacter.Transaction?,
) : Transacter.Transaction() {
override fun endTransaction(successful: Boolean): QueryResult<Unit> = QueryResult.AsyncValue {
if (enclosingTransaction == null) {
if (successful) {
wrapper.sendMessage(WorkerActions.endTransaction)
} else {
wrapper.sendMessage(WorkerActions.rollbackTransaction)
}
}
transaction = enclosingTransaction
}
}

private suspend fun WorkerWrapper.sendMessage(
action: WorkerAction,
sql: String? = null,
statement: JsWorkerSqlPreparedStatement? = null,
): WorkerResultWithRowCount {
val id = messageCounter++

println("beforeExecute")

return execute(
WorkerWrapperRequest(
id = id,
action = action,
sql = sql,
statement = statement,
),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package app.cash.sqldelight.driver.worker

import app.cash.sqldelight.driver.worker.api.WorkerResultWithRowCount
import app.cash.sqldelight.driver.worker.api.WorkerWrapperRequest
import app.cash.sqldelight.driver.worker.expected.Worker

internal expect class WorkerWrapper(worker: Worker) {
suspend fun execute(
request: WorkerWrapperRequest,
): WorkerResultWithRowCount

fun terminate()
}
Loading