Repository: arkivanov/Decompose
Pull request #564 (2023-12-08 21:21:38)

Added support for WASM for Browser
As part of #74.


Patch:
diff --git a/build.gradle.kts b/build.gradle.kts
index 3b882064..ccd1247e 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -33,6 +33,7 @@ setupDefaults(
         androidTarget()
         jvm()
         js { browser() }
+        wasmJs { browser() }
         iosCompat()
         watchosCompat()
         tvosCompat()
diff --git a/decompose/build.gradle.kts b/decompose/build.gradle.kts
index 84149d13..b7ac5927 100644
--- a/decompose/build.gradle.kts
+++ b/decompose/build.gradle.kts
@@ -5,6 +5,7 @@ import com.arkivanov.gradle.setupBinaryCompatibilityValidator
 import com.arkivanov.gradle.setupMultiplatform
 import com.arkivanov.gradle.setupPublication
 import com.arkivanov.gradle.setupSourceSets
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
 
 plugins {
     id("kotlin-multiplatform")
@@ -27,10 +28,15 @@ kotlin {
         val darwin by bundle()
         val itvos by bundle()
         val js by bundle()
-        val nonJs by bundle()
+        val wasmJs by bundle()
+        val nonWeb by bundle()
+        val web by bundle()
 
         (darwin) dependsOn common
-        (allSet - js) dependsOn nonJs
+        nonWeb dependsOn common
+        (allSet - js - wasmJs) dependsOn nonWeb
+        web dependsOn common
+        (js + wasmJs) dependsOn web
         (iosSet + tvosSet) dependsOn itvos
         (darwinSet - iosSet - tvosSet + itvos) dependsOn darwin
 
@@ -51,6 +57,9 @@ kotlin {
 
         common.test.dependencies {
             implementation(deps.jetbrains.kotlinx.kotlinxCoroutinesCore)
+
+            // Workaround: https://github.com/Kotlin/kotlinx.coroutines/issues/3968
+            implementation("org.jetbrains.kotlinx:atomicfu:0.23.1")
         }
 
         android.main.dependencies {
@@ -64,3 +73,9 @@ kotlin {
         }
     }
 }
+
+tasks.named<KotlinCompilationTask<*>>("compileKotlinWasmJs").configure {
+    compilerOptions {
+        freeCompilerArgs.add("-Xwasm-kclass-fqn")
+    }
+}
diff --git a/decompose/src/nonJsTest/kotlin/com/arkivanov/decompose/RelayThreadingTest.kt b/decompose/src/nonWebTest/kotlin/com/arkivanov/decompose/RelayThreadingTest.kt
similarity index 100%
rename from decompose/src/nonJsTest/kotlin/com/arkivanov/decompose/RelayThreadingTest.kt
rename to decompose/src/nonWebTest/kotlin/com/arkivanov/decompose/RelayThreadingTest.kt
diff --git a/decompose/src/nonJsTest/kotlin/com/arkivanov/decompose/value/AbstractMutableValueThreadingTest.kt b/decompose/src/nonWebTest/kotlin/com/arkivanov/decompose/value/AbstractMutableValueThreadingTest.kt
similarity index 100%
rename from decompose/src/nonJsTest/kotlin/com/arkivanov/decompose/value/AbstractMutableValueThreadingTest.kt
rename to decompose/src/nonWebTest/kotlin/com/arkivanov/decompose/value/AbstractMutableValueThreadingTest.kt
diff --git a/decompose/src/nonJsTest/kotlin/com/arkivanov/decompose/value/operator/AbstractValueMapThreadingTest.kt b/decompose/src/nonWebTest/kotlin/com/arkivanov/decompose/value/operator/AbstractValueMapThreadingTest.kt
similarity index 100%
rename from decompose/src/nonJsTest/kotlin/com/arkivanov/decompose/value/operator/AbstractValueMapThreadingTest.kt
rename to decompose/src/nonWebTest/kotlin/com/arkivanov/decompose/value/operator/AbstractValueMapThreadingTest.kt
diff --git a/decompose/src/wasmJsMain/kotlin/com/arkivanov/decompose/Utils.kt b/decompose/src/wasmJsMain/kotlin/com/arkivanov/decompose/Utils.kt
new file mode 100644
index 00000000..ea6142a4
--- /dev/null
+++ b/decompose/src/wasmJsMain/kotlin/com/arkivanov/decompose/Utils.kt
@@ -0,0 +1,6 @@
+package com.arkivanov.decompose
+
+import kotlin.reflect.KClass
+
+internal actual val KClass<*>.uniqueName: String?
+    get() = qualifiedName
diff --git a/decompose/src/jsMain/kotlin/com/arkivanov/decompose/Lock.kt b/decompose/src/webMain/kotlin/com/arkivanov/decompose/Lock.kt
similarity index 100%
rename from decompose/src/jsMain/kotlin/com/arkivanov/decompose/Lock.kt
rename to decompose/src/webMain/kotlin/com/arkivanov/decompose/Lock.kt
diff --git a/decompose/src/jsMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt b/decompose/src/webMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
similarity index 100%
rename from decompose/src/jsMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
rename to decompose/src/webMain/kotlin/com/arkivanov/decompose/errorhandler/PrintError.kt
diff --git a/decompose/src/jsMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt b/decompose/src/webMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
similarity index 100%
rename from decompose/src/jsMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
rename to decompose/src/webMain/kotlin/com/arkivanov/decompose/mainthread/CheckMainThread.kt
diff --git a/deps.versions.toml b/deps.versions.toml
index 75fc8ac8..35616f58 100644
--- a/deps.versions.toml
+++ b/deps.versions.toml
@@ -2,13 +2,13 @@
 
 decompose = "2.2.1-compose-experimental"
 kotlin = "1.9.21"
-essenty = "2.0.0-dev02"
+essenty = "2.0.0-dev03"
 reaktive = "1.2.3"
 junit = "4.13.2"
 jetbrainsCompose = "1.6.0-alpha01"
 jetbrainsKotlinWrappers = "1.0.0-pre.608"
-jetbrainsKotlinxCoroutines = "1.6.4"
-jetbrainsKotlinxSerialization = "1.6.0"
+jetbrainsKotlinxCoroutines = "1.8.0-RC"
+jetbrainsKotlinxSerialization = "1.6.2"
 jetbrainsBinaryCompatibilityValidator = "0.13.2"
 jetpackCompose = "1.5.0"
 jetpackComposeCompiler = "1.5.6"
diff --git a/extensions-compose/build.gradle.kts b/extensions-compose/build.gradle.kts
index 0501e813..6fd8b895 100644
--- a/extensions-compose/build.gradle.kts
+++ b/extensions-compose/build.gradle.kts
@@ -20,6 +20,7 @@ setupMultiplatform {
     macosCompat()
     iosCompat()
     js { browser() }
+    wasmJs { browser() }
 }
 
 setupPublication()
diff --git a/gradle.properties b/gradle.properties
index cd97a4bb..49006c23 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,6 +10,7 @@ kotlin.mpp.androidSourceSetLayoutVersion=2
 kotlin.mpp.applyDefaultHierarchyTemplate=false
 org.jetbrains.compose.experimental.macos.enabled=true
 org.jetbrains.compose.experimental.jscanvas.enabled=true
+org.jetbrains.compose.experimental.wasm.enabled=true
 
 # For compatibility with Kotlin 1.9.0
 android.experimental.lint.version=8.1.0


Test patch:
diff --git a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/value/MutableValueTest.kt b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/value/MutableValueTest.kt
index 41d54c11..9c7f3fb2 100644
--- a/decompose/src/commonTest/kotlin/com/arkivanov/decompose/value/MutableValueTest.kt
+++ b/decompose/src/commonTest/kotlin/com/arkivanov/decompose/value/MutableValueTest.kt
@@ -9,142 +9,142 @@ import kotlin.test.assertTrue
 @Suppress("TestFunctionName")
 class MutableValueTest {
 
-    private val value = MutableValue(0)
+    private val value = MutableValue("0")
 
     @Test
     fun WHEN_created_THEN_initial_value() {
-        assertEquals(0, value.value)
+        assertEquals("0", value.value)
     }
 
     @Test
     fun WHEN_value_changed_THEN_new_value() {
-        value.value = 1
+        value.value = "1"
 
-        assertEquals(1, value.value)
+        assertEquals("1", value.value)
     }
 
     @Test
     fun WHEN_subscribe_THEN_current_value_emitted() {
-        val values = ArrayList<Int>()
+        val values = ArrayList<String>()
 
         value.subscribe { values += it }
 
-        assertContentEquals(listOf(0), values)
+        assertContentEquals(listOf("0"), values)
     }
 
     @Test
     fun GIVEN_multiple_observers_subscribed_WHEN_value_changed_THEN_new_value_emitted() {
-        val values = List(10) { ArrayList<Int>() }
+        val values = List(10) { ArrayList<String>() }
 
         repeat(10) { index ->
             value.subscribe { values[index] += it }
         }
 
-        value.value = 1
-        value.value = 2
-        value.value = 3
+        value.value = "1"
+        value.value = "2"
+        value.value = "3"
 
-        assertContentEquals(List(10) { listOf(0, 1, 2, 3) }, values)
+        assertContentEquals(List(10) { listOf("0", "1", "2", "3") }, values)
     }
 
     @Test
     fun GIVEN_unsubscribed_WHEN_value_changed_THEN_not_emitted() {
-        val values = ArrayList<Int>()
+        val values = ArrayList<String>()
         val cancellation = value.subscribe { values += it }
         cancellation.cancel()
         values.clear()
 
-        value.value = 1
+        value.value = "1"
 
         assertContentEquals(emptyList(), values)
     }
 
     @Test
     fun GIVEN_multiple_subscribes_and_one_unsubscribed_WHEN_value_changed_THEN_value_emitted_to_subscribed() {
-        val values = ArrayList<Int>()
+        val values = ArrayList<String>()
         val cancellation = value.subscribe {}
         value.subscribe { values += it }
         cancellation.cancel()
         values.clear()
 
-        value.value = 1
+        value.value = "1"
 
-        assertContentEquals(listOf(1), values)
+        assertContentEquals(listOf("1"), values)
     }
 
     @Test
     fun GIVEN_multiple_subscribes_and_one_unsubscribed_WHEN_value_changed_THEN_value_not_emitted_to_unsubscribed() {
-        val values = ArrayList<Int>()
+        val values = ArrayList<String>()
         val cancellation = value.subscribe { values += it }
         value.subscribe {}
         cancellation.cancel()
         values.clear()
 
-        value.value = 1
+        value.value = "1"
 
         assertContentEquals(emptyList(), values)
     }
 
     @Test
     fun WHEN_compareAndSet_with_expected_value_THEN_returns_true() {
-        val result = value.compareAndSet(expected = 0, new = 1)
+        val result = value.compareAndSet(expected = value.value, new = "1")
 
         assertTrue(result)
     }
 
     @Test
     fun WHEN_compareAndSet_with_expected_value_THEN_new_value() {
-        value.compareAndSet(expected = 0, new = 1)
+        value.compareAndSet(expected = value.value, new = "1")
 
-        assertEquals(1, value.value)
+        assertEquals("1", value.value)
     }
 
     @Test
     fun WHEN_compareAndSet_with_unexpected_value_THEN_returns_false() {
-        val result = value.compareAndSet(expected = 1, new = 2)
+        val result = value.compareAndSet(expected = "1", new = "2")
 
         assertFalse(result)
     }
 
     @Test
     fun WHEN_compareAndSet_with_unexpected_value_THEN_old_value() {
-        value.compareAndSet(expected = 1, new = 2)
+        value.compareAndSet(expected = "1", new = "2")
 
-        assertEquals(0, value.value)
+        assertEquals("0", value.value)
     }
 
     @Test
     fun WHEN_update_THEN_new_value() {
-        value.update { it + 1 }
+        value.update { it + "1" }
 
-        assertEquals(1, value.value)
+        assertEquals("01", value.value)
     }
 
     @Test
     fun WHEN_getAndUpdate_THEN_new_value() {
-        value.getAndUpdate { it + 1 }
+        value.getAndUpdate { it + "1" }
 
-        assertEquals(1, value.value)
+        assertEquals("01", value.value)
     }
 
     @Test
     fun WHEN_getAndUpdate_THEN_returns_old_value() {
-        val result = value.getAndUpdate { it + 1 }
+        val result = value.getAndUpdate { it + "1" }
 
-        assertEquals(0, result)
+        assertEquals("0", result)
     }
 
     @Test
     fun WHEN_updateAndGet_THEN_new_value() {
-        value.updateAndGet { it + 1 }
+        value.updateAndGet { it + "1" }
 
-        assertEquals(1, value.value)
+        assertEquals("01", value.value)
     }
 
     @Test
     fun WHEN_updateAndGet_THEN_returns_new_value() {
-        val result = value.updateAndGet { it + 1 }
+        val result = value.updateAndGet { it + "1" }
 
-        assertEquals(1, result)
+        assertEquals("01", result)
     }
 }
diff --git a/decompose/src/nonJsMain/kotlin/com/arkivanov/decompose/Utils.nonJs.kt b/decompose/src/nonWebMain/kotlin/com/arkivanov/decompose/Utils.nonJs.kt
similarity index 100%
rename from decompose/src/nonJsMain/kotlin/com/arkivanov/decompose/Utils.nonJs.kt
rename to decompose/src/nonWebMain/kotlin/com/arkivanov/decompose/Utils.nonJs.kt
diff --git a/decompose/src/nonJsTest/kotlin/com/arkivanov/decompose/AbstractThreadingTest.kt b/decompose/src/nonWebTest/kotlin/com/arkivanov/decompose/AbstractThreadingTest.kt
similarity index 93%
rename from decompose/src/nonJsTest/kotlin/com/arkivanov/decompose/AbstractThreadingTest.kt
rename to decompose/src/nonWebTest/kotlin/com/arkivanov/decompose/AbstractThreadingTest.kt
index a2b97927..35c5e8d6 100644
--- a/decompose/src/nonJsTest/kotlin/com/arkivanov/decompose/AbstractThreadingTest.kt
+++ b/decompose/src/nonWebTest/kotlin/com/arkivanov/decompose/AbstractThreadingTest.kt
@@ -1,6 +1,7 @@
 package com.arkivanov.decompose
 
 import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.update
@@ -13,11 +14,11 @@ import kotlin.test.AfterTest
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.seconds
 
+@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
 abstract class AbstractThreadingTest {
 
     protected open val threadCount: Int = 8
 
-    @OptIn(DelicateCoroutinesApi::class)
     private val dispatcher by lazy { newFixedThreadPoolContext(nThreads = threadCount, name = "AbstractThreadingTest pool") }
 
     @AfterTest