Refactor server API

This commit is contained in:
Alexander Nozik 2022-12-06 15:54:34 +03:00
parent fd8f693151
commit fef1df3ab4
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
20 changed files with 142 additions and 108 deletions

View File

@ -10,13 +10,14 @@ import space.kscience.visionforge.jupyter.VFIntegrationBase
import space.kscience.visionforge.plotly.PlotlyPlugin
import space.kscience.visionforge.plotly.asVision
import space.kscience.visionforge.solid.Solids
import space.kscience.visionforge.visionManager
@DFExperimental
internal class VisionForgePlayGroundForJupyter : VFIntegrationBase(
Context("VisionForge") {
plugin(Solids)
plugin(PlotlyPlugin)
}
}.visionManager
) {
override fun Builder.afterLoaded() {

View File

@ -8,8 +8,9 @@ import kotlinx.html.*
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.fetch
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.VisionOfHtmlForm
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.formFragment
import space.kscience.visionforge.html.bindForm
import space.kscience.visionforge.onPropertyChange
import space.kscience.visionforge.server.EngineConnectorConfig
import space.kscience.visionforge.server.close
@ -28,8 +29,18 @@ fun main() {
resources()
}
visionPage(connector, visionManager, VisionPage.scriptHeader("js/visionforge-playground.js")) {
val form = formFragment("form") {
val form = VisionOfHtmlForm("form").apply {
onPropertyChange(visionManager.context) {
println(this)
}
}
visionPage(
connector,
visionManager,
VisionPage.scriptHeader("js/visionforge-playground.js"),
) {
bindForm(form) {
label {
htmlFor = "fname"
+"First name:"
@ -61,10 +72,7 @@ fun main() {
}
}
vision("form") { form }
form.onPropertyChange {
println(this)
}
vision(form)
}
}.start(false)

View File

@ -1,11 +1,7 @@
package space.kscience.visionforge.examples
import space.kscience.dataforge.context.Global
import space.kscience.visionforge.html.HtmlVisionFragment
import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.importScriptHeader
import space.kscience.visionforge.makeFile
import space.kscience.visionforge.html.*
import space.kscience.visionforge.visionManager
import java.awt.Desktop
import java.nio.file.Path

View File

@ -20,10 +20,10 @@ import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.meta.string
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.HtmlFormFragment
import space.kscience.visionforge.html.HtmlVisionFragment
import space.kscience.visionforge.html.VisionCollector
import space.kscience.visionforge.html.visionFragment
import space.kscience.visionforge.server.EngineConnectorConfig
import space.kscience.visionforge.server.VisionRoute
@ -107,7 +107,7 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
//server.serveVisionsFromFragment(consumer, "content-${counter++}", fragment)
val cellRoute = "content-${counter++}"
val collector: VisionCollector = mutableMapOf()
val collector: MutableMap<Name, Vision> = mutableMapOf()
val url = engine.environment.connectors.first().let {
url {
@ -121,15 +121,15 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
engine.application.serveVisionData(VisionRoute(cellRoute, visionManager), collector)
visionFragment(
context,
visionManager,
embedData = true,
updatesUrl = url,
collector = collector,
onVisionRendered = { name, vision -> collector[name] = vision },
fragment = fragment
)
} else {
//if not, use static rendering
visionFragment(context, fragment = fragment)
visionFragment(visionManager, fragment = fragment)
}
}
renderScriptForId(id)

View File

@ -9,6 +9,7 @@ import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.*
import kotlin.random.Random
import kotlin.random.nextUInt
@ -17,9 +18,12 @@ import kotlin.random.nextUInt
* A base class for different Jupyter VF integrations
*/
@DFExperimental
public abstract class VFIntegrationBase(final override val context: Context) : JupyterIntegration(), ContextAware {
public abstract class VFIntegrationBase(
public val visionManager: VisionManager,
) : JupyterIntegration(), ContextAware {
protected val handler: VFForNotebook = VFForNotebook(context)
override val context: Context get() = visionManager.context
protected val handler: VFForNotebook = VFForNotebook(visionManager.context)
protected abstract fun Builder.afterLoaded()
@ -67,7 +71,7 @@ public abstract class VFIntegrationBase(final override val context: Context) : J
val id = "fragment[${page.hashCode()}/${Random.nextUInt()}]"
div {
this.id = id
visionFragment(context, fragment = page.content)
visionFragment(visionManager, fragment = page.content)
}
renderScriptForId(id)
}

View File

@ -0,0 +1,27 @@
package space.kscience.visionforge.jupyter
import kotlinx.html.FORM
import kotlinx.html.form
import kotlinx.html.id
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.visionforge.html.HtmlFragment
import space.kscience.visionforge.html.VisionOfHtmlForm
public class HtmlFormFragment internal constructor(
public val vision: VisionOfHtmlForm,
public val formBody: HtmlFragment,
){
public val values: Meta? get() = vision.values
public operator fun get(valueName: String): Meta? = values?.get(valueName)
}
public fun HtmlFormFragment(id: String? = null, builder: FORM.() -> Unit): HtmlFormFragment {
val realId = id ?: "form[${builder.hashCode().toUInt()}]"
return HtmlFormFragment(VisionOfHtmlForm(realId)) {
form {
this.id = realId
builder()
}
}
}

View File

@ -7,12 +7,13 @@ import space.kscience.gdml.Gdml
import space.kscience.visionforge.gdml.toVision
import space.kscience.visionforge.jupyter.VFIntegrationBase
import space.kscience.visionforge.solid.Solids
import space.kscience.visionforge.visionManager
@DFExperimental
internal class GdmlForJupyter : VFIntegrationBase(
Context("GDML") {
plugin(Solids)
}
}.visionManager
) {
override fun Builder.afterLoaded() {

View File

@ -25,7 +25,7 @@ public class ThreeWithControlsPlugin : AbstractPlugin(), ElementVisionRenderer {
override fun rateVision(vision: Vision): Int =
if (vision is Solid) ElementVisionRenderer.DEFAULT_RATING * 2 else ElementVisionRenderer.ZERO_RATING
override fun render(element: Element, vision: Vision, meta: Meta) {
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
createRoot(element).render {
child(ThreeCanvasWithControls) {
attrs {

View File

@ -1,7 +1,6 @@
package space.kscience.visionforge.html
import kotlinx.html.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name
@ -15,8 +14,6 @@ public typealias HtmlVisionFragment = VisionTagConsumer<*>.() -> Unit
@DFExperimental
public fun HtmlVisionFragment(content: VisionTagConsumer<*>.() -> Unit): HtmlVisionFragment = content
public typealias VisionCollector = MutableMap<Name, Pair<VisionOutput, Vision>>
/**
* Render a fragment in the given consumer and return a map of extracted visions
@ -27,15 +24,18 @@ public typealias VisionCollector = MutableMap<Name, Pair<VisionOutput, Vision>>
* @param idPrefix a prefix to be used before vision ids
*/
public fun TagConsumer<*>.visionFragment(
context: Context,
visionManager: VisionManager,
embedData: Boolean = true,
fetchDataUrl: String? = null,
updatesUrl: String? = null,
idPrefix: String? = null,
collector: VisionCollector = mutableMapOf(),
onVisionRendered: (Name, Vision) -> Unit = { _, _ -> },
fragment: HtmlVisionFragment,
) {
val consumer = object : VisionTagConsumer<Any?>(this@visionFragment, context, idPrefix) {
val collector: MutableMap<Name, Pair<VisionOutput, Vision>> = mutableMapOf()
val consumer = object : VisionTagConsumer<Any?>(this@visionFragment, visionManager, idPrefix) {
override fun <T> TagConsumer<T>.vision(name: Name?, buildOutput: VisionOutput.() -> Vision): T {
//Avoid re-creating cached visions
@ -47,6 +47,7 @@ public fun TagConsumer<*>.visionFragment(
val (output, vision) = collector.getOrPut(actualName) {
val output = VisionOutput(context, actualName)
val vision = output.buildOutput()
onVisionRendered(actualName, vision)
output to vision
}
@ -54,8 +55,15 @@ public fun TagConsumer<*>.visionFragment(
}
override fun DIV.renderVision(manager: VisionManager, name: Name, vision: Vision, outputMeta: Meta) {
// Toggle update mode
val (_, actualVision) = collector.getOrPut(name) {
val output = VisionOutput(context, name)
onVisionRendered(name, vision)
output to vision
}
// Toggle update mode
updatesUrl?.let {
attributes[OUTPUT_CONNECT_ATTRIBUTE] = it
}
@ -69,7 +77,7 @@ public fun TagConsumer<*>.visionFragment(
type = "text/json"
attributes["class"] = OUTPUT_DATA_CLASS
unsafe {
+"\n${manager.encodeToString(vision)}\n"
+"\n${manager.encodeToString(actualVision)}\n"
}
}
}
@ -80,19 +88,20 @@ public fun TagConsumer<*>.visionFragment(
}
public fun FlowContent.visionFragment(
context: Context,
visionManager: VisionManager,
embedData: Boolean = true,
fetchDataUrl: String? = null,
updatesUrl: String? = null,
onVisionRendered: (Name, Vision) -> Unit = { _, _ -> },
idPrefix: String? = null,
visionCache: VisionCollector = mutableMapOf(),
fragment: HtmlVisionFragment,
): Unit = consumer.visionFragment(
context,
embedData,
fetchDataUrl,
updatesUrl,
idPrefix,
visionCache,
visionManager = visionManager,
embedData = embedData,
fetchDataUrl = fetchDataUrl,
updatesUrl = updatesUrl,
idPrefix = idPrefix,
onVisionRendered = onVisionRendered,
fragment = fragment
)

View File

@ -7,7 +7,6 @@ import kotlinx.html.id
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.node
@Serializable
@ -18,29 +17,10 @@ public class VisionOfHtmlForm(
public var values: Meta? by mutableProperties.node()
}
public class HtmlFormFragment internal constructor(
public val vision: VisionOfHtmlForm,
public val formBody: HtmlFragment,
){
public val values: Meta? get() = vision.values
public operator fun get(valueName: String): Meta? = values?.get(valueName)
}
public fun HtmlFormFragment(id: String? = null, builder: FORM.() -> Unit): HtmlFormFragment {
val realId = id ?: "form[${builder.hashCode().toUInt()}]"
return HtmlFormFragment(VisionOfHtmlForm(realId)) {
form {
this.id = realId
builder()
}
}
}
public fun <R> TagConsumer<R>.formFragment(
id: String? = null,
public fun <R> TagConsumer<R>.bindForm(
visionOfForm: VisionOfHtmlForm,
builder: FORM.() -> Unit,
): VisionOfHtmlForm {
val formFragment = HtmlFormFragment(id, builder)
fragment(formFragment.formBody)
return formFragment.vision
): R = form {
this.id = visionOfForm.formId
builder()
}

View File

@ -2,6 +2,7 @@ package space.kscience.visionforge.html
import kotlinx.html.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer
@ -56,9 +57,11 @@ public class VisionOutput @PublishedApi internal constructor(public val context:
@VisionDSL
public abstract class VisionTagConsumer<R>(
private val root: TagConsumer<R>,
public val context: Context,
public val visionManager: VisionManager,
private val idPrefix: String? = null,
) : TagConsumer<R> by root {
) : TagConsumer<R> by root, ContextAware {
override val context: Context get() = visionManager.context
public open fun resolveId(name: Name): String = (idPrefix ?: "output") + "[$name]"

View File

@ -18,7 +18,7 @@ fun FlowContent.renderVisionFragment(
fragment: HtmlVisionFragment,
): Map<Name, Vision> {
val visionMap = HashMap<Name, Vision>()
val consumer = object : VisionTagConsumer<Any?>(consumer, Global, idPrefix) {
val consumer = object : VisionTagConsumer<Any?>(consumer, Global.visionManager, idPrefix) {
override fun DIV.renderVision(manager: VisionManager, name: Name, vision: Vision, outputMeta: Meta) {
visionMap[name] = vision
renderer(name, vision, outputMeta)

View File

@ -4,6 +4,7 @@ import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.w3c.dom.*
import org.w3c.dom.url.URL
@ -68,18 +69,6 @@ public class VisionClient : AbstractPlugin() {
changeCollector.propertyChanged(visionName, propertyName, item)
}
public fun visionPropertyChanged(visionName: Name, propertyName: Name, item: Boolean) {
visionPropertyChanged(visionName, propertyName, Meta(item))
}
public fun visionPropertyChanged(visionName: Name, propertyName: Name, item: String) {
visionPropertyChanged(visionName, propertyName, Meta(item))
}
public fun visionPropertyChanged(visionName: Name, propertyName: Name, item: Number) {
visionPropertyChanged(visionName, propertyName, Meta(item))
}
public fun visionChanged(name: Name?, child: Vision?) {
changeCollector.setChild(name, child)
}
@ -139,10 +128,12 @@ public class VisionClient : AbstractPlugin() {
onopen = {
feedbackJob = visionManager.context.launch {
delay(feedbackAggregationTime.milliseconds)
if (!changeCollector.isEmpty()) {
send(visionManager.encodeToString(changeCollector.deepCopy(visionManager)))
changeCollector.reset()
while (isActive) {
delay(feedbackAggregationTime.milliseconds)
if (!changeCollector.isEmpty()) {
send(visionManager.encodeToString(changeCollector.deepCopy(visionManager)))
changeCollector.reset()
}
}
}
logger.info { "WebSocket update channel established for output '$name'" }
@ -247,6 +238,21 @@ public class VisionClient : AbstractPlugin() {
}
}
public fun VisionClient.visionPropertyChanged(visionName: Name, propertyName: String, item: Meta?) {
visionPropertyChanged(visionName, propertyName.parseAsName(true), item)
}
public fun VisionClient.visionPropertyChanged(visionName: Name, propertyName: String, item: Number) {
visionPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
}
public fun VisionClient.visionPropertyChanged(visionName: Name, propertyName: String, item: String) {
visionPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
}
public fun VisionClient.visionPropertyChanged(visionName: Name, propertyName: String, item: Boolean) {
visionPropertyChanged(visionName, propertyName.parseAsName(true), Meta(item))
}
private fun whenDocumentLoaded(block: Document.() -> Unit): Unit {
if (document.body != null) {

View File

@ -9,8 +9,11 @@ import org.w3c.dom.HTMLFormElement
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.get
import org.w3c.xhr.FormData
import space.kscience.dataforge.context.debug
import space.kscience.dataforge.context.logger
import space.kscience.dataforge.meta.DynamicMeta
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.toMap
import space.kscience.dataforge.meta.valueSequence
import space.kscience.visionforge.html.VisionOfHtmlForm
import space.kscience.visionforge.html.VisionOfNumberField
@ -33,7 +36,7 @@ internal fun textVisionRenderer(
value = it ?: ""
}
onChangeFunction = {
// client.visionPropertyChanged(name, VisionOfTextField::text.name.pa, value)
client.visionPropertyChanged(name, VisionOfTextField::text.name, value)
}
}
}
@ -55,7 +58,7 @@ internal fun numberVisionRenderer(
value = it?.toDouble() ?: 0.0
}
onChangeFunction = {
// vision.value = value.toDoubleOrNull()
client.visionPropertyChanged(name, VisionOfNumberField::value.name, value)
}
}
}
@ -90,9 +93,10 @@ internal fun formVisionRenderer(
val form = document.getElementById(vision.formId) as? HTMLFormElement
?: error("An element with id = '${vision.formId} is not a form")
console.info("Adding hooks to form '$form'")
client.logger.debug{"Adding hooks to form with id = '$vision.formId'"}
vision.useProperty(VisionOfHtmlForm::values) { values ->
client.logger.debug{"Updating form '${vision.formId}' with values $values"}
val inputs = form.getElementsByTagName("input")
values?.valueSequence()?.forEach { (token, value) ->
(inputs[token.toString()] as? HTMLInputElement)?.value = value.toString()
@ -102,8 +106,8 @@ internal fun formVisionRenderer(
form.onsubmit = { event ->
event.preventDefault()
val formData = FormData(form).toMeta()
//console.log(formData.toString())
//vision.values = formData
client.visionPropertyChanged(name, VisionOfHtmlForm::values.name, formData)
console.info("Sent: ${formData.toMap()}")
false
}
}

View File

@ -1,4 +1,4 @@
package space.kscience.visionforge
package space.kscience.visionforge.html
import kotlinx.html.body
import kotlinx.html.head
@ -6,10 +6,7 @@ import kotlinx.html.meta
import kotlinx.html.stream.createHTML
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.html.HtmlFragment
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.fragment
import space.kscience.visionforge.html.visionFragment
import space.kscience.visionforge.visionManager
import java.awt.Desktop
import java.nio.file.Files
import java.nio.file.Path
@ -87,7 +84,7 @@ public fun VisionPage.makeFile(
}
}
body {
visionFragment(Global, fragment = content)
visionFragment(Global.visionManager, fragment = content)
}
}.finalize()

View File

@ -28,7 +28,7 @@ public actual class PlotlyPlugin : VisionPlugin(), ElementVisionRenderer {
else -> ElementVisionRenderer.ZERO_RATING
}
override fun render(element: Element, vision: Vision, meta: Meta) {
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
val plot = (vision as? VisionOfPlotly)?.plot ?: error("VisionOfPlotly expected but ${vision::class} found")
val config = PlotlyConfig.read(meta)
element.plot(plot, config)

View File

@ -15,7 +15,6 @@ import io.ktor.server.util.*
import io.ktor.server.websocket.*
import io.ktor.util.pipeline.*
import io.ktor.websocket.*
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -97,8 +96,8 @@ public fun Application.serveVisionData(
val vision: Vision = resolveVision(Name.parse(name)) ?: error("Plot with id='$name' not registered")
launch {
incoming.consumeEach {
val data = it.data.decodeToString()
for(frame in incoming) {
val data = frame.data.decodeToString()
application.log.debug("Received update: \n$data")
val change = configuration.visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(), data
@ -143,8 +142,8 @@ public fun Application.serveVisionData(
public fun Application.serveVisionData(
configuration: VisionRoute,
cache: VisionCollector,
): Unit = serveVisionData(configuration) { cache[it]?.second }
data: Map<Name, Vision>,
): Unit = serveVisionData(configuration) { data[it] }
//
///**
@ -179,7 +178,7 @@ public fun Application.visionPage(
) {
require(WebSockets)
val collector: VisionCollector = mutableMapOf()
val collector: MutableMap<Name, Vision> = mutableMapOf()
val html = createHTML().apply {
head {
@ -193,7 +192,7 @@ public fun Application.visionPage(
body {
//Load the fragment and remember all loaded visions
visionFragment(
context = configuration.context,
visionManager = configuration.visionManager,
embedData = configuration.dataMode == VisionRoute.Mode.EMBED,
fetchDataUrl = if (configuration.dataMode != VisionRoute.Mode.EMBED) {
url {
@ -210,7 +209,7 @@ public fun Application.visionPage(
path(route, "ws")
}
} else null,
visionCache = collector,
onVisionRendered = { name, vision -> collector[name] = vision },
fragment = visionFragment
)
}

View File

@ -35,7 +35,7 @@ public class TableVisionJsPlugin : AbstractPlugin(), ElementVisionRenderer {
else -> ElementVisionRenderer.ZERO_RATING
}
override fun render(element: Element, vision: Vision, meta: Meta) {
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
val table: VisionOfTable = (vision as? VisionOfTable)
?: error("VisionOfTable expected but ${vision::class} found")

View File

@ -86,7 +86,7 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
}.launchIn(context)
vision.children.changes.onEach { childName ->
if(childName.isEmpty()) return@onEach
if (childName.isEmpty()) return@onEach
val child = vision.children.getChild(childName)
@ -147,7 +147,7 @@ public class ThreePlugin : AbstractPlugin(), ElementVisionRenderer {
render(vision)
}
override fun render(element: Element, vision: Vision, meta: Meta) {
override fun render(element: Element, name: Name, vision: Vision, meta: Meta) {
renderSolid(
element,
vision as? Solid ?: error("Solid expected but ${vision::class} found"),

View File

@ -3,7 +3,6 @@ package space.kscience.visionforge.three
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.html.*
import space.kscience.visionforge.makeFile
import space.kscience.visionforge.visionManager
import java.awt.Desktop
import java.nio.file.Path