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 {
group = "space.kscience"
version = "0.3.0-dev-15"
version = "0.3.0-dev-16"
}
subprojects {

View File

@ -45,7 +45,7 @@ include(
":ui:ring",
// ":ui:material",
":ui:bootstrap",
// ":ui:compose",
":ui:compose",
":visionforge-core",
":visionforge-solid",
// ":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.names.Name
import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.isEmpty
import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties
import space.kscience.visionforge.Vision.Companion.TYPE
@ -36,7 +37,7 @@ public interface Vision : Described {
/**
* Update this vision using a dif represented by [VisionChange].
*/
public fun update(change: VisionChange) {
public fun receiveChange(change: VisionChange) {
if (change.children?.isNotEmpty() == true) {
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?
public companion object {

View File

@ -11,10 +11,15 @@ import space.kscience.dataforge.names.Name
* An event propagated from client to a server
*/
@Serializable
public sealed interface VisionEvent{
public sealed interface VisionEvent {
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")
}
}
@ -24,17 +29,21 @@ public sealed interface VisionEvent{
*/
@Serializable
@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
@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")
/**
* 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)
}

View File

@ -6,10 +6,7 @@ import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.ValueType
import space.kscience.dataforge.meta.descriptors.MetaDescriptor
import space.kscience.dataforge.meta.descriptors.value
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.parseAsName
import space.kscience.dataforge.names.plus
import space.kscience.dataforge.names.*
import space.kscience.visionforge.AbstractVisionGroup.Companion.updateProperties
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 val children: VisionChildren
override fun update(change: VisionChange) {
override fun receiveChange(change: VisionChange) {
change.children?.forEach { (name, change) ->
if (change.vision != null || change.vision == NullVision) {
error("VisionGroup is read-only")
} else {
children.getChild(name)?.update(change)
children.getChild(name)?.receiveChange(change)
}
}
change.properties?.let {
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 {
@ -37,12 +43,12 @@ public interface MutableVisionGroup : VisionGroup {
public fun createGroup(): MutableVisionGroup
override fun update(change: VisionChange) {
override fun receiveChange(change: VisionChange) {
change.children?.forEach { (name, change) ->
when {
change.vision == NullVision -> children.setChild(name, null)
change.vision != null -> children.setChild(name, change.vision)
else -> children.getChild(name)?.update(change)
else -> children.getChild(name)?.receiveChange(change)
}
}
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.int
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.isEmpty
import space.kscience.dataforge.names.parseAsName
import space.kscience.visionforge.html.VisionTagConsumer
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE
@ -120,19 +121,21 @@ public class JsVisionClient : AbstractPlugin(), VisionClient {
onmessage = { messageEvent ->
val stringData: String? = messageEvent.data as? String
if (stringData != null) {
val change: VisionChange = visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(),
val event: VisionEvent = visionManager.jsonFormat.decodeFromString(
VisionEvent.serializer(),
stringData
)
// If change contains root vision replacement, do it
change.vision?.let { vision ->
renderVision(element, name, vision, outputMeta)
if(event is VisionChangeEvent && event.targetName.isEmpty()) {
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.")
vision.update(change)
vision.receiveEvent(event)
} else {
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.util.pipeline.*
import io.ktor.websocket.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -71,14 +72,11 @@ public class VisionRoute(
/**
* Serve visions in a given [route] without providing a page template.
* [visions] could be changed during the service.
*
* @return a [Flow] of backward events, including vision change events
*/
public fun Application.serveVisionData(
configuration: VisionRoute,
onEvent: suspend Vision.(VisionEvent) -> Unit = { event ->
if (event is VisionChangeEvent) {
update(event.change)
}
},
resolveVision: (Name) -> Vision?,
) {
require(WebSockets)
@ -102,16 +100,18 @@ public fun Application.serveVisionData(
val event = configuration.visionManager.jsonFormat.decodeFromString(
VisionEvent.serializer(), data
)
vision.onEvent(event)
vision.receiveEvent(event)
}
}
try {
withContext(configuration.context.coroutineContext) {
vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update ->
val event = VisionChangeEvent(Name.EMPTY, update)
val json = configuration.visionManager.jsonFormat.encodeToString(
VisionChange.serializer(),
update
VisionEvent.serializer(),
event
)
application.log.debug("Sending update for $name: \n$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].
*
* @return a [Flow] containing backward propagated events, including vision change events
*/
public fun Application.visionPage(
route: String,
@ -154,7 +156,7 @@ public fun Application.visionPage(
headers: Collection<HtmlFragment>,
connector: EngineConnectorConfig? = null,
visionFragment: HtmlVisionFragment,
) {
){
require(WebSockets)
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("origin".asName(), SolidMaterial.MATERIAL_COLOR_KEY, Meta("red".asValue()))
}
targetVision.update(dif)
targetVision.receiveChange(dif)
assertTrue { targetVision.children.getChild("top") is SolidGroup }
assertEquals("red", (targetVision.children.getChild("origin") as Solid).color.string) // Should work
assertEquals(