diff --git a/alchemist-composeui/build.gradle.kts b/alchemist-composeui/build.gradle.kts index 911215f8e8..f671732845 100644 --- a/alchemist-composeui/build.gradle.kts +++ b/alchemist-composeui/build.gradle.kts @@ -1,3 +1,4 @@ +import Libs.alchemist import Util.devServer import Util.webCommonConfiguration import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl @@ -30,11 +31,22 @@ kotlin { sourceSets { val commonMain by getting { dependencies { + api(alchemist("graphql")) implementation(compose.runtime) implementation(compose.ui) implementation(compose.foundation) - implementation(compose.material) + implementation(compose.material3) implementation(compose.components.resources) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.apollo.runtime) + } + } + + val jvmMain by getting { + dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.kotlin.coroutines.swing) } } } diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt index 53ccbf338b..c3f15d20bc 100644 --- a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/App.kt @@ -9,37 +9,127 @@ package it.unibo.alchemist.boundary.composeui -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.Button -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import com.apollographql.apollo3.api.Error +import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationStatus +import it.unibo.alchemist.boundary.composeui.viewmodels.SimulationViewModel /** * Application entry point, this will be rendered the same in all the platforms. */ @Composable -fun app() { - MaterialTheme { - var showContent by remember { mutableStateOf(false) } - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = { showContent = !showContent }) { - Text("Click me!") - } - AnimatedVisibility(showContent) { - val greeting = remember { getPlatform() } - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Text("Compose: $greeting") +fun App(viewModel: SimulationViewModel = viewModel { SimulationViewModel() }) { + val status by viewModel.status.collectAsStateWithLifecycle() + val errors by viewModel.errors.collectAsStateWithLifecycle() + val nodes by viewModel.nodes.collectAsStateWithLifecycle() + Scaffold( + topBar = { TopBar(status) }, + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding).padding(horizontal = 8.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + ControlButton(status, viewModel::play, viewModel::pause) + OutlinedCard( + modifier = Modifier.fillMaxSize(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + border = BorderStroke(1.dp, Color.Black), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.verticalScroll( + rememberScrollState(), + ), + ) { + if (errors.isNotEmpty()) { + ErrorDialog(viewModel::fetch, errors) + } + for (node in nodes) { + NodeDrawer(node.id) + } } } } } } + +/** + * Top bar put on top of the application. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopBar(status: SimulationStatus) { + TopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text( + "Simulation: $status", + ) + }, + ) +} + +/** + * Button to control the simulation. + */ +@Composable +fun ControlButton(status: SimulationStatus, resume: () -> Unit, pause: () -> Unit) { + if (status == SimulationStatus.Running) { + Button(onClick = { pause() }) { + Text("Pause", modifier = Modifier.padding(8.dp)) + } + } else { + Button(onClick = { resume() }) { + Text("Resume", modifier = Modifier.padding(8.dp)) + } + } +} + +/** + * Display the error dialog, currently used to circumvent the null issue we're facing when subscribing to simulation. + */ +@Composable +fun ErrorDialog(dismiss: () -> Unit, errors: List) { + AlertDialog( + onDismissRequest = dismiss, + title = { Text("Error") }, + text = { + for (error in errors) { + Text(error.message) + } + }, + confirmButton = { + Button(onClick = dismiss) { + Text("OK") + } + }, + ) +} diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/NodeDrawer.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/NodeDrawer.kt new file mode 100644 index 0000000000..3ba4e301dc --- /dev/null +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/NodeDrawer.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2010-2025, Danilo Pianini and contributors + * listed, for each module, in the respective subproject's build.gradle.kts file. + * + * This file is part of Alchemist, and is distributed under the terms of the + * GNU General Public License, with a linking exception, + * as described in the file LICENSE in the Alchemist distribution's top directory. + */ + +package it.unibo.alchemist.boundary.composeui + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import it.unibo.alchemist.boundary.composeui.viewmodels.NodeViewModel + +/** + * Display the information of a node, subscribing to its own channel for data. + */ +@Composable +fun NodeDrawer(nodeId: Int) { + val nodeModel: NodeViewModel = viewModel(key = "node-$nodeId") { NodeViewModel(nodeId) } + val nodeInfo by nodeModel.nodeInfo.collectAsStateWithLifecycle() + Text("Node $nodeId: $nodeInfo") +} diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/NodeViewModel.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/NodeViewModel.kt new file mode 100644 index 0000000000..50614adaee --- /dev/null +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/NodeViewModel.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010-2025, Danilo Pianini and contributors + * listed, for each module, in the respective subproject's build.gradle.kts file. + * + * This file is part of Alchemist, and is distributed under the terms of the + * GNU General Public License, with a linking exception, + * as described in the file LICENSE in the Alchemist distribution's top directory. + */ + +package it.unibo.alchemist.boundary.composeui.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.apollographql.apollo3.api.Error +import it.unibo.alchemist.boundary.graphql.client.GraphQLClientFactory +import it.unibo.alchemist.boundary.graphql.client.NodeInfoSubscription +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +data class Molecule(val name: String) + +data class MoleculeConcentration(val concentration: String, val molecule: Molecule) + +data class NodeInfo( + val id: Int, + val moleculeCount: Int, + val properties: List, + val contents: List, +) + +class NodeViewModel(private val nodeId: Int) : ViewModel() { + private val _nodeInfo = MutableStateFlow(null) + val nodeInfo = _nodeInfo.asStateFlow() + + private val _errors = MutableStateFlow>(emptyList()) + val errors = _errors.asStateFlow() + + // TODO: parameterize the host and port and separate client in different file + private val client = GraphQLClientFactory.subscriptionClient( + "127.0.0.1", + 3000, + ) + + private fun load() { + _errors.value = emptyList() + viewModelScope.launch { + client.subscription(NodeInfoSubscription(nodeId)) + .toFlow() + .collect { response -> + response.data?.let { data -> + _nodeInfo.value = NodeInfo( + data.environment.nodeById.id, + data.environment.nodeById.moleculeCount, + data.environment.nodeById.properties, + data.environment.nodeById.contents.entries.map { + MoleculeConcentration( + it.concentration, + Molecule(it.molecule.name), + ) + }, + ) + } + } + } + } + + init { + load() + } +} diff --git a/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationViewModel.kt b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationViewModel.kt new file mode 100644 index 0000000000..0c1d58fd66 --- /dev/null +++ b/alchemist-composeui/src/commonMain/kotlin/it/unibo/alchemist/boundary/composeui/viewmodels/SimulationViewModel.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2010-2025, Danilo Pianini and contributors + * listed, for each module, in the respective subproject's build.gradle.kts file. + * + * This file is part of Alchemist, and is distributed under the terms of the + * GNU General Public License, with a linking exception, + * as described in the file LICENSE in the Alchemist distribution's top directory. + */ + +package it.unibo.alchemist.boundary.composeui.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.apollographql.apollo3.api.Error +import it.unibo.alchemist.boundary.graphql.client.GraphQLClientFactory +import it.unibo.alchemist.boundary.graphql.client.PauseSimulationMutation +import it.unibo.alchemist.boundary.graphql.client.PlaySimulationMutation +import it.unibo.alchemist.boundary.graphql.client.SimulationSubscription +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +enum class SimulationStatus { + Init, + Ready, + Paused, + Running, + Terminated, +} + +data class Node(val id: Int, val coordinates: List) + +class SimulationViewModel : ViewModel() { + private val _nodes = MutableStateFlow>(emptyList()) + private val _status = MutableStateFlow(SimulationStatus.Init) + private val _errors = MutableStateFlow>(emptyList()) + + val nodes = _nodes.asStateFlow() + val status = _status.asStateFlow() + val errors = _errors.asStateFlow() + + // TODO: parameterize the host and port and separate client in different file + private val client = GraphQLClientFactory.subscriptionClient( + "127.0.0.1", + 3000, + ) + + fun pause() { + viewModelScope.launch { + client.mutation(PauseSimulationMutation()).execute() + } + } + + fun play() { + viewModelScope.launch { + client.mutation(PlaySimulationMutation()).execute() + } + } + + fun fetch() { + _errors.value = emptyList() + viewModelScope.launch { + client.subscription(SimulationSubscription()) + .toFlow() + .collect { response -> + if (response.hasErrors()) { + response.errors?.let { errors -> + _errors.update { errors } + } + } + response.data?.let { data -> + _status.update { + when (data.simulation.status) { + "READY" -> SimulationStatus.Ready + "PAUSED" -> SimulationStatus.Paused + "RUNNING" -> SimulationStatus.Running + "TERMINATED" -> SimulationStatus.Terminated + else -> SimulationStatus.Init + } + } + _nodes.value = data.simulation.environment.nodeToPos.entries.map { + Node( + id = it.id, + coordinates = it.position.coordinates, + ) + } + } + } + } + } + + init { + fetch() + } +} diff --git a/alchemist-composeui/src/jsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt b/alchemist-composeui/src/jsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt index 9811caadc1..b8ef9b2cab 100644 --- a/alchemist-composeui/src/jsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt +++ b/alchemist-composeui/src/jsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt @@ -21,7 +21,7 @@ import org.jetbrains.skiko.wasm.onWasmReady fun main() { onWasmReady { ComposeViewport(checkNotNull(document.body)) { - app() + App() } } } diff --git a/alchemist-composeui/src/jvmMain/kotlin/it/unibo/alchemist/boundary/composeui/ComposeMonitor.kt b/alchemist-composeui/src/jvmMain/kotlin/it/unibo/alchemist/boundary/composeui/ComposeMonitor.kt index 77f47406b5..ae3df386ab 100644 --- a/alchemist-composeui/src/jvmMain/kotlin/it/unibo/alchemist/boundary/composeui/ComposeMonitor.kt +++ b/alchemist-composeui/src/jvmMain/kotlin/it/unibo/alchemist/boundary/composeui/ComposeMonitor.kt @@ -10,21 +10,28 @@ package it.unibo.alchemist.boundary.composeui import androidx.compose.ui.window.Window -import androidx.compose.ui.window.application +import androidx.compose.ui.window.awaitApplication import it.unibo.alchemist.boundary.OutputMonitor import it.unibo.alchemist.model.Environment +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlin.concurrent.thread /** * Monitor extension that uses JVM Compose UI to display the simulation. */ class ComposeMonitor : OutputMonitor { override fun initialized(environment: Environment) { - application { - Window( - onCloseRequest = { }, - title = "Alchemist", - ) { - app() + thread( + name = "ComposeMonitor", + isDaemon = true, + ) { + runBlocking { + launch { + awaitApplication { + Window(onCloseRequest = ::exitApplication, title = "Alchemist") { App() } + } + } } } } diff --git a/alchemist-composeui/src/wasmJsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt b/alchemist-composeui/src/wasmJsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt index 76a83dea20..c8b1253dd4 100644 --- a/alchemist-composeui/src/wasmJsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt +++ b/alchemist-composeui/src/wasmJsMain/kotlin/it/unibo/alchemist/boundary/composeui/Main.kt @@ -19,6 +19,6 @@ import kotlinx.browser.document @OptIn(ExperimentalComposeUiApi::class) fun main() { ComposeViewport(checkNotNull(document.body)) { - app() + App() } } diff --git a/alchemist-graphql-surrogates/src/main/kotlin/it/unibo/alchemist/boundary/graphql/schema/model/surrogates/SimulationSurrogate.kt b/alchemist-graphql-surrogates/src/main/kotlin/it/unibo/alchemist/boundary/graphql/schema/model/surrogates/SimulationSurrogate.kt index f4c747f2bf..e245666cdb 100644 --- a/alchemist-graphql-surrogates/src/main/kotlin/it/unibo/alchemist/boundary/graphql/schema/model/surrogates/SimulationSurrogate.kt +++ b/alchemist-graphql-surrogates/src/main/kotlin/it/unibo/alchemist/boundary/graphql/schema/model/surrogates/SimulationSurrogate.kt @@ -34,6 +34,12 @@ data class SimulationSurrogate>(@GraphQLIgnore override v @GraphQLDescription("The time of the simulation") fun time(): Double = origin.time.toDouble() + /** + * The step at which the simulation is. + */ + @GraphQLDescription("The step at which the simulation is") + fun step(): String = origin.step.toString() + /** * The environment of the simulation. */ diff --git a/alchemist-graphql/build.gradle.kts b/alchemist-graphql/build.gradle.kts index 571bd0d5ea..0f5fb20947 100644 --- a/alchemist-graphql/build.gradle.kts +++ b/alchemist-graphql/build.gradle.kts @@ -10,8 +10,11 @@ import Libs.alchemist import Libs.incarnation import Util.allVerificationTasks +import Util.devServer +import Util.webCommonConfiguration import com.apollographql.apollo3.gradle.internal.ApolloGenerateSourcesTask import com.expediagroup.graphql.plugin.gradle.tasks.AbstractGenerateClientTask +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { id("kotlin-multiplatform-convention") @@ -22,6 +25,13 @@ plugins { } kotlin { + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + webCommonConfiguration() + devServer() + } + sourceSets { val commonMain by getting { dependencies { diff --git a/alchemist-graphql/src/commonMain/resources/graphql/NodeInfo.graphql b/alchemist-graphql/src/commonMain/resources/graphql/NodeInfo.graphql new file mode 100644 index 0000000000..a0f0e1302c --- /dev/null +++ b/alchemist-graphql/src/commonMain/resources/graphql/NodeInfo.graphql @@ -0,0 +1,17 @@ +subscription NodeInfo($id: Int!) { + environment { + nodeById(id: $id) { + id + moleculeCount + properties + contents { + entries { + concentration + molecule { + name + } + } + } + } + } +} \ No newline at end of file diff --git a/alchemist-graphql/src/commonMain/resources/graphql/NodesSubscription.graphql b/alchemist-graphql/src/commonMain/resources/graphql/NodesSubscription.graphql deleted file mode 100644 index 4a4f29dfa2..0000000000 --- a/alchemist-graphql/src/commonMain/resources/graphql/NodesSubscription.graphql +++ /dev/null @@ -1,18 +0,0 @@ -subscription NodesSubscription { - simulation { - status - time - environment { - nodes { - contents { - entries { - molecule { - name - } - concentration - } - } - } - } - } -} diff --git a/alchemist-graphql/src/commonMain/resources/graphql/SimulationSubscription.graphql b/alchemist-graphql/src/commonMain/resources/graphql/SimulationSubscription.graphql new file mode 100644 index 0000000000..00f49426ec --- /dev/null +++ b/alchemist-graphql/src/commonMain/resources/graphql/SimulationSubscription.graphql @@ -0,0 +1,16 @@ +subscription SimulationSubscription { + simulation { + status + environment { + nodeToPos { + entries { + id + position { + coordinates + dimensions + } + } + } + } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 46a92d512d..0c63e3e35f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ scalacache = "0.28.0" [libraries] androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } antlr4 = { module = "org.antlr:antlr4", version.ref = "antlr4" } antlr4-runtime = { module = "org.antlr:antlr4-runtime", version.ref = "antlr4" } apache-commons-cli = "commons-cli:commons-cli:1.9.0" @@ -73,6 +74,7 @@ kotest-framework-engine = { module = "io.kotest:kotest-framework-engine", versio kotest-runner = { module = "io.kotest:kotest-runner-junit5-jvm", version.ref = "kotest" } kotlin-cli = "org.jetbrains.kotlinx:kotlinx-cli:0.3.6" kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlin-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlin-jvm-plugin = { module = "org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin", version.ref = "kotlin" } kotlin-multiplatform-plugin = { module = "org.jetbrains.kotlin.multiplatform:org.jetbrains.kotlin.multiplatform.gradle.plugin", version.ref = "kotlin" } diff --git a/javadoc-io.json b/javadoc-io.json index 1f875dc79e..c630afc0b5 100644 --- a/javadoc-io.json +++ b/javadoc-io.json @@ -249,14 +249,23 @@ "first": "https://javadoc.io/doc/org.graphstream/gs-core/2.0", "second": "https://javadoc.io/doc/org.graphstream/gs-core/2.0/element-list" }, + "org.jetbrains.androidx.lifecycle/lifecycle-runtime-compose/2.9.0": { + "first": "https://javadoc.io/doc/org.jetbrains.androidx.lifecycle/lifecycle-runtime-compose/2.9.0" + }, + "org.jetbrains.androidx.lifecycle/lifecycle-viewmodel-compose/2.9.0": { + "first": "https://javadoc.io/doc/org.jetbrains.androidx.lifecycle/lifecycle-viewmodel-compose/2.9.0" + }, "org.jetbrains.compose.components/components-resources/1.8.1": { "first": "https://javadoc.io/doc/org.jetbrains.compose.components/components-resources/1.8.1" }, + "org.jetbrains.compose.desktop/desktop-jvm-linux-x64/1.8.1": { + "first": "https://javadoc.io/doc/org.jetbrains.compose.desktop/desktop-jvm-linux-x64/1.8.1" + }, "org.jetbrains.compose.foundation/foundation/1.8.1": { "first": "https://javadoc.io/doc/org.jetbrains.compose.foundation/foundation/1.8.1" }, - "org.jetbrains.compose.material/material/1.8.1": { - "first": "https://javadoc.io/doc/org.jetbrains.compose.material/material/1.8.1" + "org.jetbrains.compose.material3/material3/1.8.1": { + "first": "https://javadoc.io/doc/org.jetbrains.compose.material3/material3/1.8.1" }, "org.jetbrains.compose.runtime/runtime/1.8.1": { "first": "https://javadoc.io/doc/org.jetbrains.compose.runtime/runtime/1.8.1" @@ -330,6 +339,9 @@ "org.jetbrains.kotlinx/kotlinx-coroutines-core/1.10.2": { "first": "https://javadoc.io/doc/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.10.2" }, + "org.jetbrains.kotlinx/kotlinx-coroutines-swing/1.10.2": { + "first": "https://javadoc.io/doc/org.jetbrains.kotlinx/kotlinx-coroutines-swing/1.10.2" + }, "org.jetbrains.kotlinx/kotlinx-coroutines-test/1.10.2": { "first": "https://javadoc.io/doc/org.jetbrains.kotlinx/kotlinx-coroutines-test/1.10.2" }, diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 3fc6f0615f..8e85a47f6b 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -59,6 +59,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.5" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1"