Add direct event processing to Vision

This commit is contained in:
Alexander Nozik 2023-11-15 10:26:37 +03:00
parent 399be206be
commit 1ea5ef86e6
8 changed files with 67 additions and 32 deletions

View File

@ -12,7 +12,7 @@ val fxVersion by extra("11")
allprojects { allprojects {
group = "space.kscience" group = "space.kscience"
version = "0.3.0-dev-15" version = "0.3.0-dev-16"
} }
subprojects { subprojects {

View File

@ -45,7 +45,7 @@ include(
":ui:ring", ":ui:ring",
// ":ui:material", // ":ui:material",
":ui:bootstrap", ":ui:bootstrap",
// ":ui:compose", ":ui:compose",
":visionforge-core", ":visionforge-core",
":visionforge-solid", ":visionforge-solid",
// ":visionforge-fx", // ":visionforge-fx",

View File

@ -11,6 +11,7 @@ import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.misc.Type import space.kscience.dataforge.misc.Type
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.isEmpty
import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties
import space.kscience.visionforge.Vision.Companion.TYPE import space.kscience.visionforge.Vision.Companion.TYPE
@ -36,7 +37,7 @@ public interface Vision : Described {
/** /**
* Update this vision using a dif represented by [VisionChange]. * Update this vision using a dif represented by [VisionChange].
*/ */
public fun update(change: VisionChange) { public fun receiveChange(change: VisionChange) {
if (change.children?.isNotEmpty() == true) { if (change.children?.isNotEmpty() == true) {
error("Vision is not a group") error("Vision is not a group")
} }
@ -45,6 +46,20 @@ public interface Vision : Described {
} }
} }
/**
* Receive and process a generic [VisionEvent].
*/
public fun receiveEvent(event: VisionEvent) {
if(event.targetName.isEmpty()) {
when (event) {
is VisionChangeEvent -> receiveChange(event.change)
else -> TODO()
}
} else {
error("Vision is not a group and can't process an event with non-empty target")
}
}
override val descriptor: MetaDescriptor? override val descriptor: MetaDescriptor?
public companion object { public companion object {

View File

@ -11,10 +11,15 @@ import space.kscience.dataforge.names.Name
* An event propagated from client to a server * An event propagated from client to a server
*/ */
@Serializable @Serializable
public sealed interface VisionEvent{ public sealed interface VisionEvent {
public val targetName: Name public val targetName: Name
public companion object{ /**
* Create a copy of this event with the same type and content, but different [targetName]
*/
public fun changeTarget(newTarget: Name): VisionEvent
public companion object {
public val CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload") public val CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload")
} }
} }
@ -24,17 +29,21 @@ public sealed interface VisionEvent{
*/ */
@Serializable @Serializable
@SerialName("meta") @SerialName("meta")
public class VisionMetaEvent(override val targetName: Name, public val meta: Meta) : VisionEvent public data class VisionMetaEvent(override val targetName: Name, public val meta: Meta) : VisionEvent {
override fun changeTarget(newTarget: Name): VisionMetaEvent = VisionMetaEvent(newTarget, meta)
}
@Serializable @Serializable
@SerialName("change") @SerialName("change")
public class VisionChangeEvent(override val targetName: Name, public val change: VisionChange) : VisionEvent public data class VisionChangeEvent(override val targetName: Name, public val change: VisionChange) : VisionEvent {
override fun changeTarget(newTarget: Name): VisionChangeEvent = VisionChangeEvent(newTarget, change)
}
public val Vision.Companion.CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload") public val Vision.Companion.CLICK_EVENT_KEY: Name get() = Name.of("events", "click", "payload")
/** /**
* Set the payload to be sent to server on click * Set the payload to be sent to server on click
*/ */
public fun Vision.onClickPayload(payloadBuilder: MutableMeta.() -> Unit){ public fun Vision.onClickPayload(payloadBuilder: MutableMeta.() -> Unit) {
properties[VisionEvent.CLICK_EVENT_KEY] = Meta(payloadBuilder) properties[VisionEvent.CLICK_EVENT_KEY] = Meta(payloadBuilder)
} }

View File

@ -6,10 +6,7 @@ import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.ValueType import space.kscience.dataforge.meta.ValueType
import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.value import space.kscience.dataforge.meta.descriptors.value
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.*
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.plus
import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties
import space.kscience.visionforge.Vision.Companion.STYLE_KEY import space.kscience.visionforge.Vision.Companion.STYLE_KEY
@ -17,18 +14,27 @@ import space.kscience.visionforge.Vision.Companion.STYLE_KEY
public interface VisionGroup : Vision { public interface VisionGroup : Vision {
public val children: VisionChildren public val children: VisionChildren
override fun update(change: VisionChange) { override fun receiveChange(change: VisionChange) {
change.children?.forEach { (name, change) -> change.children?.forEach { (name, change) ->
if (change.vision != null || change.vision == NullVision) { if (change.vision != null || change.vision == NullVision) {
error("VisionGroup is read-only") error("VisionGroup is read-only")
} else { } else {
children.getChild(name)?.update(change) children.getChild(name)?.receiveChange(change)
} }
} }
change.properties?.let { change.properties?.let {
updateProperties(it, Name.EMPTY) updateProperties(it, Name.EMPTY)
} }
} }
override fun receiveEvent(event: VisionEvent) {
if (event.targetName.isEmpty()) {
super.receiveEvent(event)
} else {
val target = children[event.targetName] ?: error("Child vision with name ${event.targetName} not found")
target.receiveEvent(event.changeTarget(Name.EMPTY))
}
}
} }
public interface MutableVisionGroup : VisionGroup { public interface MutableVisionGroup : VisionGroup {
@ -37,12 +43,12 @@ public interface MutableVisionGroup : VisionGroup {
public fun createGroup(): MutableVisionGroup public fun createGroup(): MutableVisionGroup
override fun update(change: VisionChange) { override fun receiveChange(change: VisionChange) {
change.children?.forEach { (name, change) -> change.children?.forEach { (name, change) ->
when { when {
change.vision == NullVision -> children.setChild(name, null) change.vision == NullVision -> children.setChild(name, null)
change.vision != null -> children.setChild(name, change.vision) change.vision != null -> children.setChild(name, change.vision)
else -> children.getChild(name)?.update(change) else -> children.getChild(name)?.receiveChange(change)
} }
} }
change.properties?.let { change.properties?.let {

View File

@ -20,6 +20,7 @@ import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.get import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int import space.kscience.dataforge.meta.int
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.isEmpty
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
import space.kscience.visionforge.html.VisionTagConsumer import space.kscience.visionforge.html.VisionTagConsumer
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE
@ -120,19 +121,21 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
onmessage = { messageEvent -> onmessage = { messageEvent ->
val stringData: String? = messageEvent.data as? String val stringData: String? = messageEvent.data as? String
if (stringData != null) { if (stringData != null) {
val change: VisionChange = visionManager.jsonFormat.decodeFromString( val event: VisionEvent = visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(), VisionEvent.serializer(),
stringData stringData
) )
// If change contains root vision replacement, do it // If change contains root vision replacement, do it
change.vision?.let { vision -> if(event is VisionChangeEvent && event.targetName.isEmpty()) {
renderVision(element, name, vision, outputMeta) event.change.vision?.let { vision ->
renderVision(element, name, vision, outputMeta)
}
} }
logger.debug { "Got update $change for output with name $name" } logger.debug { "Got $event for output with name $name" }
if (vision == null) error("Can't update vision because it is not loaded.") if (vision == null) error("Can't update vision because it is not loaded.")
vision.update(change) vision.receiveEvent(event)
} else { } else {
logger.error { "WebSocket message data is not a string" } logger.error { "WebSocket message data is not a string" }
} }

View File

@ -14,6 +14,7 @@ import io.ktor.server.util.*
import io.ktor.server.websocket.* import io.ktor.server.websocket.*
import io.ktor.util.pipeline.* import io.ktor.util.pipeline.*
import io.ktor.websocket.* import io.ktor.websocket.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -71,14 +72,11 @@ public class VisionRoute(
/** /**
* Serve visions in a given [route] without providing a page template. * Serve visions in a given [route] without providing a page template.
* [visions] could be changed during the service. * [visions] could be changed during the service.
*
* @return a [Flow] of backward events, including vision change events
*/ */
public fun Application.serveVisionData( public fun Application.serveVisionData(
configuration: VisionRoute, configuration: VisionRoute,
onEvent: suspend Vision.(VisionEvent) -> Unit = { event ->
if (event is VisionChangeEvent) {
update(event.change)
}
},
resolveVision: (Name) -> Vision?, resolveVision: (Name) -> Vision?,
) { ) {
require(WebSockets) require(WebSockets)
@ -102,16 +100,18 @@ public fun Application.serveVisionData(
val event = configuration.visionManager.jsonFormat.decodeFromString( val event = configuration.visionManager.jsonFormat.decodeFromString(
VisionEvent.serializer(), data VisionEvent.serializer(), data
) )
vision.onEvent(event)
vision.receiveEvent(event)
} }
} }
try { try {
withContext(configuration.context.coroutineContext) { withContext(configuration.context.coroutineContext) {
vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update -> vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update ->
val event = VisionChangeEvent(Name.EMPTY, update)
val json = configuration.visionManager.jsonFormat.encodeToString( val json = configuration.visionManager.jsonFormat.encodeToString(
VisionChange.serializer(), VisionEvent.serializer(),
update event
) )
application.log.debug("Sending update for $name: \n$json") application.log.debug("Sending update for $name: \n$json")
outgoing.send(Frame.Text(json)) outgoing.send(Frame.Text(json))
@ -147,6 +147,8 @@ public fun Application.serveVisionData(
/** /**
* Serve a page, potentially containing any number of visions at a given [route] with given [header]. * Serve a page, potentially containing any number of visions at a given [route] with given [header].
*
* @return a [Flow] containing backward propagated events, including vision change events
*/ */
public fun Application.visionPage( public fun Application.visionPage(
route: String, route: String,
@ -154,7 +156,7 @@ public fun Application.visionPage(
headers: Collection<HtmlFragment>, headers: Collection<HtmlFragment>,
connector: EngineConnectorConfig? = null, connector: EngineConnectorConfig? = null,
visionFragment: HtmlVisionFragment, visionFragment: HtmlVisionFragment,
) { ){
require(WebSockets) require(WebSockets)
val collector: MutableMap<Name, Vision> = mutableMapOf() val collector: MutableMap<Name, Vision> = mutableMapOf()

View File

@ -28,7 +28,7 @@ internal class VisionUpdateTest {
propertyChanged("top".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue())) propertyChanged("top".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
propertyChanged("origin".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue())) propertyChanged("origin".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
} }
targetVision.update(dif) targetVision.receiveChange(dif)
assertTrue { targetVision.children.getChild("top") is SolidGroup } assertTrue { targetVision.children.getChild("top") is SolidGroup }
assertEquals("red", (targetVision.children.getChild("origin") as Solid).color.string) // Should work assertEquals("red", (targetVision.children.getChild("origin") as Solid).color.string) // Should work
assertEquals( assertEquals(