Feedback connection for client.

This commit is contained in:
Alexander Nozik 2021-07-16 20:52:01 +03:00
parent a87692ae1f
commit 87260cea86
8 changed files with 125 additions and 88 deletions

View File

@ -22,6 +22,8 @@
- position, rotation and size moved to properties
- prototypes moved to children
- Immutable Solid instances
- Property listeners are not triggered if there are no changes.
- Feedback websocket connection in the client.
### Deprecated

View File

@ -74,9 +74,12 @@ public open class VisionBase(
}
override fun setProperty(name: Name, item: MetaItem?, notify: Boolean) {
getOrCreateProperties().setItem(name, item)
if (notify) {
invalidateProperty(name)
val oldItem = properties?.getItem(name)
if(oldItem!= item) {
getOrCreateProperties().setItem(name, item)
if (notify) {
invalidateProperty(name)
}
}
}

View File

@ -65,14 +65,14 @@ private fun Vision.isolate(manager: VisionManager): Vision {
}
/**
* @param void flag showing that this vision child should be removed
* @param delete flag showing that this vision child should be removed
* @param vision a new value for vision content
* @param properties updated properties
* @param children a map of children changed in ths [VisionChange]. If a child to be removed, set [void] flag to true.
* @param children a map of children changed in ths [VisionChange]. If a child to be removed, set [delete] flag to true.
*/
@Serializable
public data class VisionChange(
public val void: Boolean = false,
public val delete: Boolean = false,
public val vision: Vision? = null,
@Serializable(MetaSerializer::class) public val properties: Meta? = null,
public val children: Map<Name, VisionChange>? = null,

View File

@ -134,7 +134,7 @@ public open class VisionGroupBase(
override fun update(change: VisionChange) {
change.children?.forEach { (name, change) ->
when {
change.void -> set(name, null)
change.delete -> set(name, null)
change.vision != null -> set(name, change.vision)
else -> get(name)?.update(change)
}

View File

@ -39,6 +39,8 @@ public class VisionManager(meta: Meta) : AbstractPlugin(meta) {
public fun decodeFromString(string: String): Vision = jsonFormat.decodeFromString(visionSerializer, string)
public fun encodeToString(vision: Vision): String = jsonFormat.encodeToString(visionSerializer, vision)
public fun encodeToString(change: VisionChange): String =
jsonFormat.encodeToString(VisionChange.serializer(), change)
public fun decodeFromJson(json: JsonElement): Vision = jsonFormat.decodeFromJsonElement(visionSerializer, json)

View File

@ -2,6 +2,9 @@ package space.kscience.visionforge
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.w3c.dom.*
import org.w3c.dom.url.URL
import space.kscience.dataforge.context.*
@ -13,14 +16,14 @@ import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNEC
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_ENDPOINT_ATTRIBUTE
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_FETCH_ATTRIBUTE
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_NAME_ATTRIBUTE
import kotlin.collections.set
import kotlin.reflect.KClass
import kotlin.time.Duration
public class VisionClient : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag
private val visionManager: VisionManager by require(VisionManager)
private val visionMap = HashMap<Element, Vision>()
//private val visionMap = HashMap<Element, Vision>()
/**
* Up-going tree traversal in search for endpoint attribute
@ -53,11 +56,73 @@ public class VisionClient : AbstractPlugin() {
private fun Element.getFlag(attribute: String): Boolean = attributes[attribute]?.value != null
private fun renderVision(element: Element, vision: Vision?, outputMeta: Meta) {
private fun renderVision(name: String, element: Element, vision: Vision?, outputMeta: Meta) {
if (vision != null) {
visionMap[element] = vision
val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision")
renderer.render(element, vision, outputMeta)
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr ->
val wsUrl = if (attr.value.isBlank() || attr.value == "auto") {
val endpoint = resolveEndpoint(element)
logger.info { "Vision server is resolved to $endpoint" }
URL(endpoint).apply {
pathname += "/ws"
}
} else {
URL(attr.value)
}.apply {
protocol = "ws"
searchParams.append("name", name)
}
logger.info { "Updating vision data from $wsUrl" }
//Individual websocket for this element
WebSocket(wsUrl.toString()).apply {
onmessage = { messageEvent ->
val stringData: String? = messageEvent.data as? String
if (stringData != null) {
val change: VisionChange = visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(),
stringData
)
if (change.vision != null) {
renderer.render(element, vision, outputMeta)
}
logger.debug { "Got update $change for output with name $name" }
vision.update(change)
} else {
console.error("WebSocket message data is not a string")
}
}
//Backward change propagation
var feedbackJob: Job? = null
onopen = {
feedbackJob = vision.flowChanges(
visionManager,
Duration.Companion.milliseconds(300)
).onEach { change ->
send(visionManager.encodeToString(change))
}.launchIn(visionManager.context)
console.info("WebSocket update channel established for output '$name'")
}
onclose = {
feedbackJob?.cancel()
console.info("WebSocket update channel closed for output '$name'")
}
onerror = {
feedbackJob?.cancel()
console.error("WebSocket update channel error for output '$name'")
}
}
}
}
}
@ -79,85 +144,39 @@ public class VisionClient : AbstractPlugin() {
visionManager.decodeFromString(it)
}
if (embeddedVision != null) {
logger.info { "Found embedded vision for output with name $name" }
renderVision(element, embeddedVision, outputMeta)
}
element.attributes[OUTPUT_FETCH_ATTRIBUTE]?.let { attr ->
val fetchUrl = if (attr.value.isBlank() || attr.value == "auto") {
val endpoint = resolveEndpoint(element)
logger.info { "Vision server is resolved to $endpoint" }
URL(endpoint).apply {
pathname += "/vision"
}
} else {
URL(attr.value)
}.apply {
searchParams.append("name", name)
when {
embeddedVision != null -> {
logger.info { "Found embedded vision for output with name $name" }
renderVision(name, element, embeddedVision, outputMeta)
}
element.attributes[OUTPUT_FETCH_ATTRIBUTE] != null -> {
val attr = element.attributes[OUTPUT_FETCH_ATTRIBUTE]!!
logger.info { "Fetching vision data from $fetchUrl" }
window.fetch(fetchUrl).then { response ->
if (response.ok) {
response.text().then { text ->
val vision = visionManager.decodeFromString(text)
renderVision(element, vision, outputMeta)
val fetchUrl = if (attr.value.isBlank() || attr.value == "auto") {
val endpoint = resolveEndpoint(element)
logger.info { "Vision server is resolved to $endpoint" }
URL(endpoint).apply {
pathname += "/vision"
}
} else {
logger.error { "Failed to fetch initial vision state from $fetchUrl" }
URL(attr.value)
}.apply {
searchParams.append("name", name)
}
}
}
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr ->
val wsUrl = if (attr.value.isBlank() || attr.value == "auto") {
val endpoint = resolveEndpoint(element)
logger.info { "Vision server is resolved to $endpoint" }
URL(endpoint).apply {
pathname += "/ws"
}
} else {
URL(attr.value)
}.apply {
protocol = "ws"
searchParams.append("name", name)
}
logger.info { "Updating vision data from $wsUrl" }
WebSocket(wsUrl.toString()).apply {
onmessage = { messageEvent ->
val stringData: String? = messageEvent.data as? String
if (stringData != null) {
val change = visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(),
stringData
)
if (change.vision != null) {
renderVision(element, change.vision, outputMeta)
logger.info { "Fetching vision data from $fetchUrl" }
window.fetch(fetchUrl).then { response ->
if (response.ok) {
response.text().then { text ->
val vision = visionManager.decodeFromString(text)
renderVision(name, element, vision, outputMeta)
}
logger.debug { "Got update $change for output with name $name" }
visionMap[element]?.update(change)
?: console.info("Target vision for element $element with name $name not found")
} else {
console.error("WebSocket message data is not a string")
logger.error { "Failed to fetch initial vision state from $fetchUrl" }
}
}
onopen = {
console.info("WebSocket update channel established for output '$name'")
}
onclose = {
console.info("WebSocket update channel closed for output '$name'")
}
onerror = {
console.error("WebSocket update channel error for output '$name'")
}
}
else -> error("No embedded vision data / fetch url for $name")
}
}

View File

@ -19,7 +19,9 @@ import io.ktor.server.engine.embeddedServer
import io.ktor.websocket.WebSockets
import io.ktor.websocket.webSocket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.html.*
import kotlinx.html.stream.createHTML
@ -131,6 +133,15 @@ public class VisionServer internal constructor(
application.log.debug("Opened server socket for $name")
val vision: Vision = visions[name.toName()] ?: error("Plot with id='$name' not registered")
launch {
incoming.consumeEach {
val change = visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(), it.data.decodeToString()
)
vision.update(change)
}
}
try {
withContext(visionManager.context.coroutineContext) {
vision.flowChanges(visionManager, Duration.milliseconds(updateInterval)).collect { update ->

View File

@ -14,17 +14,17 @@ import info.laht.threekt.math.Matrix4
import info.laht.threekt.math.Vector3
import info.laht.threekt.objects.Mesh
external class CSG {
fun clone(): CSG
fun toPolygons(): Array<Polygon>
fun toGeometry(toMatrix: Matrix4): BufferGeometry
fun union(csg: CSG): CSG
fun subtract(csg: CSG): CSG
fun intersect(csg: CSG): CSG
fun inverse(): CSG
public external class CSG {
public fun clone(): CSG
public fun toPolygons(): Array<Polygon>
public fun toGeometry(toMatrix: Matrix4): BufferGeometry
public fun union(csg: CSG): CSG
public fun subtract(csg: CSG): CSG
public fun intersect(csg: CSG): CSG
public fun inverse(): CSG
companion object {
public companion object {
fun fromPolygons(polygons: Array<Polygon>): CSG
fun fromGeometry(geom: BufferGeometry, objectIndex: dynamic = definedExternally): CSG
fun fromMesh(mesh: Mesh, objectIndex: dynamic = definedExternally): CSG